[Refactor] (P13): Espryt's frontend-keyed slot-table entries and the apply-thread refusals go, death notices release twins by handle, and the link ratchet refuses any '# P13' line (49 -> 36 symbols).

This commit is contained in:
2026-10-06 20:55:16 -04:00
parent 556ff46601
commit fa77b1874f
21 changed files with 492 additions and 2498 deletions
+112 -577
View File
@@ -568,52 +568,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
}
void SyncBufferBindingPoints(BufferTarget target, GLenum glTarget) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
// Only sync up to the high-water mark of app-touched points; the fixed array is 84
// deep but apps bind a handful, so the never-touched tail is already at GL default 0.
auto bindingPointCnt = MG_Pipe::gPipeInputs.GetTouchedBufferBindingPointCount(target);
// ...and never past what the ES driver itself can hold. MobileGL advertises the GL 4.5
// minimum of 84 uniform binding points while the ES 3.2 minimum is 72, so a frontend
// index in that gap would reach glBindBufferBase as GL_INVALID_VALUE. Nothing is lost
// by stopping: this frontend-indexed pass exists for the compute path, and the
// per-program rebind in BindCurrentProgramWithResources - which is what actually feeds
// a shader - remaps every block a program declares onto a compacted ES point, so a
// block bound at GL point 83 still reaches its shader.
if (target == BufferTarget::Uniform && g_GLESCapabilities.MaxUniformBufferBindings > 0) {
bindingPointCnt = std::min(bindingPointCnt,
static_cast<SizeT>(g_GLESCapabilities.MaxUniformBufferBindings));
}
for (SizeT i = 0; i < bindingPointCnt; ++i) {
auto& point = MG_Pipe::gPipeInputs.GetBufferBindingPoint(target, i);
auto& obj = point.GetBoundObject();
if (!obj) {
BindBufferBaseCached(glTarget, static_cast<GLuint>(i), 0);
continue;
}
auto* backendResource = EnsureBufferResource(obj);
if (!backendResource || backendResource->id == 0) {
MGLOG_E_ONCE("No backend buffer found for %s binding point %zu.",
MG_Util::ConvertGLEnumToString(glTarget).c_str(), i);
continue;
}
const auto& range = point.GetRange();
auto backendBufferId = backendResource->id;
if (range.start == 0 && range.end >= obj->GetSize()) {
BindBufferBaseCached(glTarget, static_cast<GLuint>(i), backendBufferId);
} else {
const auto start = std::min(range.start, obj->GetSize());
const auto end = std::min(range.end, obj->GetSize());
BindBufferRangeCached(glTarget, static_cast<GLuint>(i), backendBufferId,
static_cast<GLintptr>(start), static_cast<GLsizeiptr>(end - start));
}
}
}
void SyncAtomicCounterBuffers(const Vector<Int>& glBindings, Int esslBindingTop) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
@@ -623,207 +577,63 @@ namespace MobileGL::MG_Backend::DirectGLES {
// program declares - and only the points themselves move sides. The bound is the
// applier's window instead of GetBufferBindingPointCount's sticky forward, which is
// the row this site was the Espryt reader of.
if (BindingPointsComeFromRecords()) {
const MG_Pipe::MGPipeApplierState& st = MG_Pipe::MGPipeApplier();
const Uint32 windowStart = st.ShaderBufferStart[MG_Pipe::kMGPipeShaderBufferClassAtomicCounter];
const SizeT windowEnd = static_cast<SizeT>(windowStart) +
st.ShaderBufferCount[MG_Pipe::kMGPipeShaderBufferClassAtomicCounter];
for (const Int glBinding : glBindings) {
if (glBinding < 0) continue;
const Int esslBinding = esslBindingTop - glBinding;
// Already diagnosed once when the block was transpiled; nothing was bound to
// it there either, so there is nothing to unbind here.
if (esslBinding < 0) continue;
// OUTSIDE THE WINDOW IS "NOTHING BOUND", which is what the frontend array's
// default says too - so it takes the unbind arm rather than being skipped.
// Skipping would leave whatever the driver last had at that ESSL point,
// which is the hole the frontend walk's `continue` never had because its
// bound was the whole 84-point array.
const MG_Pipe::MGPBufferRange* entry = nullptr;
if (static_cast<SizeT>(glBinding) >= windowStart &&
static_cast<SizeT>(glBinding) < windowEnd) {
entry = &st.BoundShaderBuffers[MG_Pipe::kMGPipeShaderBufferClassAtomicCounter]
[static_cast<SizeT>(glBinding)];
}
if (entry == nullptr || MG_Pipe::MGPipeHandleIsNull(entry->Res)) {
BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, static_cast<Uint>(esslBinding), 0);
continue;
}
auto* backendResource = EnsureBufferResourceForHandle(nullptr, entry->Res);
if (!backendResource || backendResource->id == 0) {
MGLOG_E_ONCE("No backend buffer found for atomic counter binding point %d "
"(handle {%u, %u}).",
glBinding, entry->Res.Slot, entry->Res.Gen);
continue;
}
if (entry->Offset == 0 && entry->Size == MG_Pipe::kMGPipeWholeBuffer) {
BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, static_cast<Uint>(esslBinding),
backendResource->id);
} else {
const SizeT storage = backendResource->storageSize;
const SizeT rangeStart = std::min<SizeT>(static_cast<SizeT>(entry->Offset), storage);
const SizeT rangeEnd =
std::min<SizeT>(static_cast<SizeT>(entry->Offset + entry->Size), storage);
BindBufferRangeCached(GL_SHADER_STORAGE_BUFFER, static_cast<Uint>(esslBinding),
backendResource->id, static_cast<GLintptr>(rangeStart),
static_cast<GLsizeiptr>(rangeEnd - rangeStart));
}
// AND NO MarkBufferGpuWritten HERE. It was the second of the three backend
// GPU-write sites and it reached into a client object; the client's
// MarkAtomicCounterBindings (GpuWritePending.cpp) already marks the same set
// from MarkGpuWritesForDraw, WIDER on purpose - it marks every touched
// counter point rather than only the ones this program declares, which is
// the safe direction for an over-approximate set.
//
// P8-C: the server's own twin is marked instead, for the server's own CPU
// readers (SplitHostBytesForCpuRead) - a counter buffer later bound as a
// parameter or element buffer is read on this side.
backendResource->serverGpuWritten = true;
}
return;
}
const SizeT pointCount = MG_Pipe::gPipeInputs.GetBufferBindingPointCount(BufferTarget::AtomicCounter);
const MG_Pipe::MGPipeApplierState& st = MG_Pipe::MGPipeApplier();
const Uint32 windowStart = st.ShaderBufferStart[MG_Pipe::kMGPipeShaderBufferClassAtomicCounter];
const SizeT windowEnd = static_cast<SizeT>(windowStart) +
st.ShaderBufferCount[MG_Pipe::kMGPipeShaderBufferClassAtomicCounter];
for (const Int glBinding : glBindings) {
if (glBinding < 0 || static_cast<SizeT>(glBinding) >= pointCount) continue;
if (glBinding < 0) continue;
const Int esslBinding = esslBindingTop - glBinding;
// Already diagnosed once when the block was transpiled; nothing was bound to it
// there either, so there is nothing to unbind here.
// Already diagnosed once when the block was transpiled; nothing was bound to
// it there either, so there is nothing to unbind here.
if (esslBinding < 0) continue;
auto& point = MG_Pipe::gPipeInputs.GetBufferBindingPoint(BufferTarget::AtomicCounter,
static_cast<Uint>(glBinding));
auto& obj = point.GetBoundObject();
if (!obj) {
// OUTSIDE THE WINDOW IS "NOTHING BOUND", which is what the frontend array's
// default says too - so it takes the unbind arm rather than being skipped.
// Skipping would leave whatever the driver last had at that ESSL point,
// which is the hole the frontend walk's `continue` never had because its
// bound was the whole 84-point array.
const MG_Pipe::MGPBufferRange* entry = nullptr;
if (static_cast<SizeT>(glBinding) >= windowStart &&
static_cast<SizeT>(glBinding) < windowEnd) {
entry = &st.BoundShaderBuffers[MG_Pipe::kMGPipeShaderBufferClassAtomicCounter]
[static_cast<SizeT>(glBinding)];
}
if (entry == nullptr || MG_Pipe::MGPipeHandleIsNull(entry->Res)) {
BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, static_cast<Uint>(esslBinding), 0);
continue;
}
auto* backendResource = EnsureBufferResource(obj);
auto* backendResource = EnsureBufferResourceForHandle(nullptr, entry->Res);
if (!backendResource || backendResource->id == 0) {
MGLOG_E_ONCE("No backend buffer found for atomic counter binding point %d.", glBinding);
MGLOG_E_ONCE("No backend buffer found for atomic counter binding point %d "
"(handle {%u, %u}).",
glBinding, entry->Res.Slot, entry->Res.Gen);
continue;
}
const auto& range = point.GetRange();
if (range.start == 0 && range.end >= obj->GetSize()) {
if (entry->Offset == 0 && entry->Size == MG_Pipe::kMGPipeWholeBuffer) {
BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, static_cast<Uint>(esslBinding),
backendResource->id);
} else {
const auto start = std::min(range.start, obj->GetSize());
const auto end = std::min(range.end, obj->GetSize());
const SizeT storage = backendResource->storageSize;
const SizeT rangeStart = std::min<SizeT>(static_cast<SizeT>(entry->Offset), storage);
const SizeT rangeEnd =
std::min<SizeT>(static_cast<SizeT>(entry->Offset + entry->Size), storage);
BindBufferRangeCached(GL_SHADER_STORAGE_BUFFER, static_cast<Uint>(esslBinding),
backendResource->id, static_cast<GLintptr>(start),
static_cast<GLsizeiptr>(end - start));
backendResource->id, static_cast<GLintptr>(rangeStart),
static_cast<GLsizeiptr>(rangeEnd - rangeStart));
}
// The whole point of a counter is that the shader INCREMENTS it, and every
// conformance case reads the result back with glMapBufferRange or
// glGetBufferSubData - which serve the frontend's CPU shadow until the buffer is
// flagged (BufferObject::SyncGpuWrites), exactly as for a storage buffer.
MarkBufferGpuWritten(obj);
}
}
// The handle arm of the resolved-draw-buffers memo (D-G4). Two substitutions and
// nothing else:
//
// validity the frontend VAO's wrapping configuration version is replaced by the
// bound vertex-elements CSO ({slot, gen} AND its server-owned content
// serial) plus the vertex-buffer set's own serial - so an emission the
// suppressor let through is what re-opens the memo, not a counter the
// backend reads out of the frontend;
// identity an entry names its resource by handle and the clean probe compares that,
// instead of a raw frontend address that a successor object can reproduce.
//
// The WALK itself still reads the frontend VAO's attributes, and deliberately: the
// buffers a draw needs ensured is a pull site P3a does not migrate (dirty bits 15-17
// are P4b's and the resolution move is P8's), and EnsureBufferResource still owes
// BufferObject::SyncPersistentMappedRange one call (D-N).
void SyncVaoAttributeBuffersByHandle(const SharedPtr<MG_State::GLState::VertexArrayObject>& currentVAOObject,
VertexArrayImpl::BackendVertexArrayObject::ResolvedDrawBuffers* memo,
Uint64 bufferEpoch) {
const auto& st = MG_Pipe::MGPipeApplier();
const MG_Pipe::MGPipeHandle elements = st.BoundVertexElements;
Uint64 elementsSerial = 0;
Bool haveElementsRecord = false;
if (!MG_Pipe::MGPipeHandleIsNull(elements) && elements.Slot < st.VertexElementsCsos.size()) {
const auto& record = st.VertexElementsCsos[elements.Slot];
if (record.Live && record.Gen == elements.Gen) {
elementsSerial = record.ContentSerial;
haveElementsRecord = true;
}
}
const Uint64 buffersSerial = st.VertexBuffersSerial;
// NO LIVE ELEMENTS RECORD IS A MISS, NEVER A HIT. With none, the key above is
// {null, 0, buffersSerial} - a key that describes no configuration at all and that
// NEVER CHANGES while the state stays that way, so a memo stamped with it would hit
// on every later draw of a VAO whose attributes have moved. That state is reachable:
// MGPipeApplierReset() empties VertexElementsCsos and BoundVertexElements at every
// change of the current context, and the client re-emits only at its next
// create/bind. The legacy arm's configuration version caught exactly this by moving.
const Bool memoKeyIsMeaningful = haveElementsRecord;
if (memo && memo->valid && memoKeyIsMeaningful && memo->elementsHandle == elements &&
memo->elementsSerial == elementsSerial && memo->buffersSerial == buffersSerial) {
if (memo->vboCleanEpoch != bufferEpoch) {
Bool allClean = true;
for (Uint i = 0; i < memo->count; ++i) {
auto& entry = memo->entries[i];
// entry.frontend is the same object the legacy arm probes and is kept
// alive by the VAO attribute's SharedPtr for as long as this memo is
// valid; it answers the live-map question no record carries in P3a.
if (IsBufferDrawCleanByHandle(entry.handle, entry.resource, entry.frontend)) continue;
allClean = false;
entry.resource =
EnsureBufferResource(currentVAOObject->GetAttribute(entry.attribIndex).Buffer);
}
memo->vboCleanEpoch = allClean ? bufferEpoch : 0;
}
return;
}
// Full walk, once per distinct buffer, rebuilding the memo as it goes.
MG_State::GLState::BufferObject* syncedBuffers[MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS];
Uint syncedBufferCount = 0;
const auto& allAttributes = currentVAOObject->GetAllAttributes();
for (Uint attribIndex = 0; attribIndex < allAttributes.size(); ++attribIndex) {
const auto& attrib = allAttributes[attribIndex];
if (!attrib.Enabled) continue;
const auto& bufferObject = attrib.Buffer;
if (!bufferObject) continue;
auto* const bufferKey = bufferObject.get();
Bool alreadySynced = false;
for (Uint i = 0; i < syncedBufferCount; ++i) {
if (syncedBuffers[i] == bufferKey) {
alreadySynced = true;
break;
}
}
if (alreadySynced) continue;
auto* resource = EnsureBufferResource(bufferObject);
if (memo) {
auto& entry = memo->entries[syncedBufferCount];
entry.frontend = bufferKey;
entry.attribIndex = static_cast<Uint8>(attribIndex);
entry.resource = resource;
entry.handle = HandleOfBuffer(bufferKey);
}
syncedBuffers[syncedBufferCount++] = bufferKey;
}
if (memo) {
memo->count = syncedBufferCount;
memo->elementsHandle = elements;
memo->elementsSerial = elementsSerial;
memo->buffersSerial = buffersSerial;
// Only a key that describes a real configuration is worth remembering; see
// memoKeyIsMeaningful above. The walk still ran and the buffers are ensured -
// this only refuses to let the NEXT draw skip it.
memo->valid = memoKeyIsMeaningful;
// Rebuilt via EnsureBufferResource, not probed clean: the next probe pass
// stamps the epoch.
memo->vboCleanEpoch = 0;
// AND NO MarkBufferGpuWritten HERE. It was the second of the three backend
// GPU-write sites and it reached into a client object; the client's
// MarkAtomicCounterBindings (GpuWritePending.cpp) already marks the same set
// from MarkGpuWritesForDraw, WIDER on purpose - it marks every touched
// counter point rather than only the ones this program declares, which is
// the safe direction for an over-approximate set.
//
// P8-C: the server's own twin is marked instead, for the server's own CPU
// readers (SplitHostBytesForCpuRead) - a counter buffer later bound as a
// parameter or element buffer is read on this side.
backendResource->serverGpuWritten = true;
}
return;
}
// P5e (vi), CONTRACT-P5E §5.1: THE SAME MEMO, THE SAME THREE-VALUE KEY, AND A WALK THAT
@@ -1033,13 +843,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
// binder (e.g. Flywheel's indirect vertex shaders pull instance data from storage buffers).
// P5e (sb, §5.6): the record arm, selected by transport + bit 13. Same `if (…) else`
// shape the indirect buffer above already uses.
if (BindingPointsComeFromRecords()) {
SyncBufferBindingPointsByRecord(MG_Pipe::kMGPipeShaderBufferClassShaderStorage,
GL_SHADER_STORAGE_BUFFER);
} else
{
SyncBufferBindingPoints(BufferTarget::ShaderStorage, GL_SHADER_STORAGE_BUFFER);
}
SyncBufferBindingPointsByRecord(MG_Pipe::kMGPipeShaderBufferClassShaderStorage,
GL_SHADER_STORAGE_BUFFER);
}
void SyncComputeBuffers(Bool includeDispatchIndirectBuffer) {
@@ -1054,16 +859,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
// is why the uniform class is emitted at all: compute does NOT go through the
// per-program block remap in BindCurrentProgramWithResources, so these are the
// points a compute shader actually reads.
if (BindingPointsComeFromRecords()) {
SyncBufferBindingPointsByRecord(MG_Pipe::kMGPipeShaderBufferClassUniform,
GL_UNIFORM_BUFFER);
SyncBufferBindingPointsByRecord(MG_Pipe::kMGPipeShaderBufferClassShaderStorage,
GL_SHADER_STORAGE_BUFFER);
} else
{
SyncBufferBindingPoints(BufferTarget::Uniform, GL_UNIFORM_BUFFER);
SyncBufferBindingPoints(BufferTarget::ShaderStorage, GL_SHADER_STORAGE_BUFFER);
}
SyncBufferBindingPointsByRecord(MG_Pipe::kMGPipeShaderBufferClassUniform,
GL_UNIFORM_BUFFER);
SyncBufferBindingPointsByRecord(MG_Pipe::kMGPipeShaderBufferClassShaderStorage,
GL_SHADER_STORAGE_BUFFER);
if (includeDispatchIndirectBuffer) {
// P5c (hd, CONTRACT-P5C §3.5): see the draw-indirect twin above - the verb
// record's handle, never the frontend binding slot or the client allocator.
@@ -1805,41 +1604,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
} // namespace XfbImpl
namespace VertexArrayImpl {
// Resolve-or-create the VAO's backend twin, once per draw: PrepareForDraw passes
// the result to the buffer sync (resolved-buffers memo host), the VAO sync and
// the draw-time bind, which each used to run their own registry Find. The raw
// pointer stays valid for the whole draw: the frontend VAO is pinned by the
// context binding, and a live object's twin is never erased nor replaced - on the
// legacy arm that is TwinLookupMemo's contract, and on the {slot, gen} arm it is
// simply that nothing but the sweep frees a slot and the sweep only takes slots
// whose frontend object is already gone.
// MONOLITH GLUE AS OF P5e (vi), CONTRACT-P5E §4.1 / §5.8, AND THE SCOPE IS GONE WITH THE
// DEBT IT NAMED. This overload is reached only when there IS a frontend VAO to resolve
// from, i.e. from the push-monolith and legacy arms of PrepareForDraw; a live transport
// takes ResolveVaoTwin(st.BoundVertexElements) instead (Managers.cpp, id's body). The
// MGPipeFrontendKeyedRegistryScope that used to wrap this - one of the three scope sites
// this family owned - is DELETED rather than narrowed, and that deletion is what makes
// the revert loud: putting `Find(vao.get())` back on the transport arm now aborts
// Fatal{RoleViolation, "MGPipeSlots"} from HandleOf instead of being quietly exempted.
BackendVertexArrayObject* ResolveVaoTwin(const SharedPtr<MG_State::GLState::VertexArrayObject>& vao) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (EsprytSlotTablesEnabled()) {
// No memo on this arm. The memo existed to turn the registry's hash probe into
// an array index; the slot table's Find already IS the array index, and the
// memo's whole safety argument - owner-equality against a recycled heap address
// - is answered by {slot, gen} instead of re-derived per lookup.
auto* slot = g_backendVertexArrayObjects.Find(vao.get());
auto& backendObj = slot ? *slot : g_backendVertexArrayObjects.GetOrCreate(vao);
if (!backendObj) {
backendObj = MakeShared<BackendVertexArrayObject>();
}
return backendObj.get();
}
return nullptr;
}
// P5e (vi): the record arm's entry, and it takes NO frontend object - CONTRACT-P5E §4.1
// ("the object parameter is deleted from the transport overload, not defaulted to
// null"). A null twin here is the null BoundVertexElements, i.e. "no VAO bound", which
@@ -1850,9 +1614,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
// Kept for symmetry with the overload above and free on this arm: the slot table's
// sweep is a no-op (Managers.h's CollectGarbageIfNeeded on the handle registry).
g_backendVertexArrayObjects.CollectGarbageIfNeeded();
if (!vaoTwin) return;
vaoTwin->SyncToBackendFromApplier();
}
@@ -1943,12 +1704,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
static void SyncClientSideVertexArraysForDrawArrays(GLint first, GLsizei count) {
if (!BufferImpl::AnyClientSideVertexArrayInRecord()) return;
if (MG_Pipe::MGPipeApplierIsUnbarrieredApply()) {
MGLOG_F("MGPipe: Fatal{RoleViolation, \"MGPipeSlots\"} - a draw carrying a "
"client-memory vertex array is being applied UNBARRIERED. Both the "
"client's refusal (Fatal{UnmigratedVerb, \"DrawArrays+CLIENT_ARRAYS\"}) "
"and escalation (ii) of CONTRACT-P5E §2.1 exist to make this "
"unreachable; the bytes below are the application's and are moving");
// P13: client-memory arrays are monolith-only - under a transport the client refuses
// them (Fatal{UnmigratedVerb, "DrawArrays+CLIENT_ARRAYS"}) before any record goes out,
// so a record naming one here means the bytes below belong to another process.
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
MGLOG_F("MGPipe: Fatal{RoleViolation, \"ClientArrays\"} - a draw carrying a "
"client-memory vertex array reached the backend under a transport; the "
"client's refusal exists to make this unreachable");
std::abort();
}
auto* twin = ResolveVaoTwin(MG_Pipe::MGPipeApplier().BoundVertexElements);
@@ -2220,87 +1982,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
} // namespace VertexArrayImpl
namespace TextureImpl {
SharedPtr<BackendTextureObject>& SyncTextureObjectToBackend(
const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
Bool imageBindableStorageRequired) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
// P5c (G6, CONTRACT-P5C §5.4): this sync still arrives holding the FRONTEND object
// (the object-class rows) and resolves its twin by frontend identity - the
// frontend-keyed registry P3b/P4b rekeys onto handles. The probe (and the mint on
// a first sync) is named debt inside the scope, not an unwrapped violation.
const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry;
auto* backendTextureSlot = g_backendTextureObjects.Find(textureObject.get());
auto& backendSlot = backendTextureSlot ? *backendTextureSlot
: g_backendTextureObjects.GetOrCreate(textureObject);
if (!backendSlot) {
backendSlot = MakeShared<BackendTextureObject>();
}
// A by-VALUE copy of the twin for the duration of the syncs below. `backendSlot` is a
// reference INTO the open-addressed registry, and syncing can RE-ENTER this function:
// a texture created by glTextureView has to sync the texture whose storage it views
// first (SyncTextureViewToBackend), and that nested call may insert, grow the map and
// relocate every entry - leaving the reference dangling. Holding the object itself
// keeps the calls below working on the right twin regardless; the slot is re-resolved
// at the end for the reference this function returns.
//
// The slot-table arm has no such hazard - an entry is an array element and a nested
// insert can only reallocate the vector, which the re-resolve at the tail already
// handles - so the copy is a refcount it does not need to pay. It keeps the copy for
// exactly one thing: holding the twin alive across the nested sync.
const SharedPtr<BackendTextureObject> backendObj = backendSlot;
if (imageBindableStorageRequired) {
backendObj->RequireImageBindableStorage(textureObject);
}
backendObj->SyncTextureParamsToBackend(textureObject);
backendObj->SyncBuiltinSamplerToBackend(textureObject);
backendObj->SyncMipmapsToBackend(textureObject);
// The storage sync may RE-MINT the driver texture - a fresh glTexStorage after a
// shape change, an image-bindable widening, or the glTextureView that an
// ARB_texture_view view is created on - which discards every parameter the two calls
// above just pushed. Re-push them here rather than leaving it to the next sync: the
// very next thing that happens is usually the draw this sync was run for, and until
// the filters land the new texture is at the ES defaults, which for a single-level or
// integer texture is not merely mis-filtered but INCOMPLETE, i.e. it samples zero.
if (backendObj->NeedsParameterResync()) {
backendObj->SyncTextureParamsToBackend(textureObject);
backendObj->SyncBuiltinSamplerToBackend(textureObject);
}
if (EsprytSlotTablesEnabled()) {
// One re-resolve, and only because a nested GetOrCreate may have GROWN the
// vector and moved the element; the entry itself cannot have been erased, since
// nothing on this arm erases a live slot. No second Find-or-create, no
// put-the-twin-back repair.
auto* slot = g_backendTextureObjects.Find(textureObject.get());
MOBILEGL_ASSERT(slot != nullptr && *slot != nullptr,
"the texture twin resolved at entry is gone after its own sync");
if (slot != nullptr && *slot != nullptr) {
return *slot;
}
// Cannot happen - the caller holds the frontend object, so its slot cannot be
// reclaimed underneath this call - but the return is a reference, and a null
// deref in a release build is a worse way to learn that than a re-created twin.
auto& repaired = g_backendTextureObjects.GetOrCreate(textureObject);
if (!repaired) {
repaired = backendObj;
}
return repaired;
}
auto* refreshedSlot = g_backendTextureObjects.Find(textureObject.get());
auto& refreshedBackendObj = refreshedSlot ? *refreshedSlot
: g_backendTextureObjects.GetOrCreate(textureObject);
if (!refreshedBackendObj) {
// A collection ran during the nested sync and took this slot with it; put the
// twin the caller is about to use back, rather than handing back an empty one.
refreshedBackendObj = backendObj;
}
return refreshedBackendObj;
}
// Identity snapshot of what one texture unit has bound: the object in every binding
// slot plus the unit's sampler object. This - not the context's texture bind
// generation - is what the per-draw texture memos key on: the generation also bumps
@@ -2846,7 +2527,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
g_backendTextureObjects.CollectGarbageIfNeeded();
// All textures we need are:
// 1. textures bound to texture units (TODO: only sync ones that are used in current program)
@@ -3710,9 +3390,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
g_backendFramebufferObjects.CollectGarbageIfNeeded();
TextureImpl::g_backendTextureObjects.CollectGarbageIfNeeded();
RenderbufferImpl::g_backendRenderbufferObjects.CollectGarbageIfNeeded();
// The handle arm first, and it declines rather than half-running: with no record
// yet it hands the walk straight back to the pre-handle arm below, unchanged.
@@ -4826,8 +4503,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
g_backendProgramObjects.CollectGarbageIfNeeded();
SamplerImpl::g_backendSamplerObjects.CollectGarbageIfNeeded();
g_currentDrawFrontendProgram = nullptr;
g_currentDrawBackendProgram = nullptr;
@@ -5245,48 +4920,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
static Array<UnitSamplerLookupMemo, MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS>
g_unitSamplerLookupMemos;
static SamplerImpl::BackendSamplerObject* ResolveUnitSamplerBackend(
Int unit, const SharedPtr<MG_State::GLState::SamplerObject>& samplerObject) {
// P5e (tx2), CONTRACT-P5E §4.2's last paragraph: THE IDENTITY SAMPLER FAMILY IS DELETED
// UNDER A TRANSPORT AND BECOMES A NAMED REFUSAL.
//
// This function is the apply-thread MINT for a unit the record set does not cover: it
// probes the client allocator with HandleOf(samplerObject), keeps a WeakPtr to a frontend
// sampler in a per-unit memo, and its caller in the program pass creates a twin off the
// frontend object when the lookup misses. Every one of those is a rule-F violation, and
// the window rule (§5.3) makes the case it exists for UNREPRESENTABLE: a unit whose
// BoundSamplerStates[u] is null while the frontend held a sampler is a MISSING RECORD,
// not a unit to be served from the client's object.
MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"BindSamplerStates.Count\"} - unit %d holds a "
"frontend sampler object that the applied bind_sampler_states window does not "
"describe. Minting a twin for it would probe the client's allocator from the "
"apply thread; CONTRACT-P5E.md §4.2 deletes the identity sampler family under a "
"transport and §5.3 makes the missing entry unrepresentable",
static_cast<int>(unit));
std::abort();
// P5c (G6, CONTRACT-P5C §5.4): frontend-keyed twin resolution, named debt inside the
// scope - the refusal above is what retires it under a transport.
const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry;
auto& memo = g_unitSamplerLookupMemos[static_cast<SizeT>(unit)];
if (EsprytSlotTablesEnabled()) {
const MG_Pipe::MGPipeHandle handle =
SamplerImpl::g_backendSamplerObjects.HandleOf(samplerObject.get());
if (memo.backend && !MG_Pipe::MGPipeHandleIsNull(handle) && memo.frontendHandle == handle) {
return memo.backend;
}
auto* slot = SamplerImpl::g_backendSamplerObjects.FindByHandle(handle);
if (slot && *slot) {
// A MISS is still never cached: the twin may not exist yet when the unit pass
// runs, because the program pass creates it later in the same draw.
memo.frontendHandle = handle;
memo.backend = slot->get();
return memo.backend;
}
return nullptr;
}
return nullptr;
}
// Puts each touched unit's frontend sampler object on the backend unit. Not part
// of the texture memo below (BindCurrentProgramWithResources may rewrite sampled
// units' samplers right after this runs - raw-depth-fetch substitution,
@@ -5419,21 +5052,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
walkedFromRecords = true;
}
if (!walkedFromRecords)
{
for (Int unit = 0; unit <= maxTouchedUnit; ++unit) {
const auto& samplerObject = MG_Pipe::gPipeInputs.GetTextureUnitObject(unit).GetSamplerObject();
if (samplerObject) {
if (auto* backendSampler = ResolveUnitSamplerBackend(unit, samplerObject)) {
backendSampler->Bind(unit);
}
} else {
// Symmetric with the bind above: a sampler object left on the unit by an
// earlier draw keeps being applied, and on a multisample texture - which
// takes no sampler object at all - the draw is rejected outright.
SamplerImpl::UnbindSampler(unit);
}
}
if (!walkedFromRecords) {
// P13: no bind_sampler_states window applied, i.e. no unit holds a sampler object -
// the record arm emits one for every unit that does. Unbind, so a sampler object
// left on a unit by an earlier draw is not applied to this one.
for (Int unit = 0; unit <= maxTouchedUnit; ++unit) SamplerImpl::UnbindSampler(unit);
}
g_unitSamplerWalkContextId = keys.contextId;
g_unitSamplerWalkEpoch = keys.unitBindingsEpoch;
@@ -5863,92 +5486,58 @@ namespace MobileGL::MG_Backend::DirectGLES {
// same one - Start is 0 by contract - so `binding` addresses the
// applier's window directly, and a binding at or above Count means
// "nothing bound", which is what the frontend array's default said too.
if (BufferImpl::BindingPointsComeFromRecords()) {
const MG_Pipe::MGPipeApplierState& st = MG_Pipe::MGPipeApplier();
const Uint32 windowStart =
st.ShaderBufferStart[MG_Pipe::kMGPipeShaderBufferClassUniform];
const SizeT windowEnd =
static_cast<SizeT>(windowStart) +
st.ShaderBufferCount[MG_Pipe::kMGPipeShaderBufferClassUniform];
if (static_cast<SizeT>(binding) < windowStart ||
static_cast<SizeT>(binding) >= windowEnd) {
continue; // nothing bound there - the frontend arm's `if (bufferObj)`
}
const MG_Pipe::MGPBufferRange& entry =
st.BoundShaderBuffers[MG_Pipe::kMGPipeShaderBufferClassUniform]
[static_cast<SizeT>(binding)];
if (MG_Pipe::MGPipeHandleIsNull(entry.Res)) continue;
// The clean probe is the SAME five questions IsBufferDrawClean asks
// the frontend object, asked by handle: the twin's identity, the
// resource record's Serial against the twin's synced one, and the
// pending-work sets - all server-owned (D-A4). The drawCleanEpoch
// short-circuit is unchanged; it was never the point read.
auto* backendResource = BufferImpl::FindBufferResourceForHandle(entry.Res);
if (!backendResource || backendResource->drawCleanEpoch != bufferEpoch) {
if (backendResource && BufferImpl::IsBufferDrawCleanByHandle(
entry.Res, backendResource, nullptr)) {
backendResource->drawCleanEpoch = bufferEpoch;
} else {
backendResource =
BufferImpl::EnsureBufferResourceForHandle(nullptr, entry.Res);
}
}
if (backendResource && backendResource->id != 0) {
// WHOLE-VS-RANGE IS THE RECORD's TEST, not `range.end == 0`: a
// base binding travels as kMGPipeWholeBuffer precisely so the
// extent is re-resolved HERE, against the storage this server
// holds, rather than frozen at the client's emission.
if (entry.Offset == 0 && entry.Size == MG_Pipe::kMGPipeWholeBuffer) {
BufferImpl::BindBufferBaseCached(GL_UNIFORM_BUFFER, lastUBOBinding,
backendResource->id);
} else {
const SizeT storage = backendResource->storageSize;
const SizeT rangeStart =
std::min<SizeT>(static_cast<SizeT>(entry.Offset), storage);
const SizeT rangeEnd = std::min<SizeT>(
static_cast<SizeT>(entry.Offset + entry.Size), storage);
BufferImpl::BindBufferRangeCached(
GL_UNIFORM_BUFFER, lastUBOBinding, backendResource->id,
static_cast<GLintptr>(rangeStart),
static_cast<GLsizeiptr>(rangeEnd - rangeStart));
}
} else {
MGLOG_E_ONCE("No backend buffer found for UBO binding, cannot bind UBO.");
}
continue;
const MG_Pipe::MGPipeApplierState& st = MG_Pipe::MGPipeApplier();
const Uint32 windowStart =
st.ShaderBufferStart[MG_Pipe::kMGPipeShaderBufferClassUniform];
const SizeT windowEnd =
static_cast<SizeT>(windowStart) +
st.ShaderBufferCount[MG_Pipe::kMGPipeShaderBufferClassUniform];
if (static_cast<SizeT>(binding) < windowStart ||
static_cast<SizeT>(binding) >= windowEnd) {
continue; // nothing bound there - the frontend arm's `if (bufferObj)`
}
auto& point = MG_Pipe::gPipeInputs.GetBufferBindingPoint(BufferTarget::Uniform, binding);
auto& bufferObj = point.GetBoundObject();
auto range = point.GetRange();
if (bufferObj) {
// Clean-probe (or epoch-stamped) fast path before the full
// EnsureBufferResource: an unchanged static UBO needs no
// storage work, only its binding re-established below.
auto* backendResource = BufferImpl::GetBufferResource(bufferObj.get());
if (!backendResource || backendResource->drawCleanEpoch != bufferEpoch) {
if (backendResource &&
BufferImpl::IsBufferDrawClean(bufferObj.get(), backendResource)) {
backendResource->drawCleanEpoch = bufferEpoch;
} else {
backendResource = BufferImpl::EnsureBufferResource(bufferObj);
}
}
if (backendResource && backendResource->id != 0) {
// glBindBufferBase/Range set the generic GL_UNIFORM_BUFFER binding
// as a side effect, so no separate BindBufferId is needed here.
if (range.end == 0) {
BufferImpl::BindBufferBaseCached(GL_UNIFORM_BUFFER, lastUBOBinding,
backendResource->id);
} else {
BufferImpl::BindBufferRangeCached(
GL_UNIFORM_BUFFER, lastUBOBinding, backendResource->id,
(GLintptr)range.start, (GLintptr)(range.end - range.start));
}
const MG_Pipe::MGPBufferRange& entry =
st.BoundShaderBuffers[MG_Pipe::kMGPipeShaderBufferClassUniform]
[static_cast<SizeT>(binding)];
if (MG_Pipe::MGPipeHandleIsNull(entry.Res)) continue;
// The clean probe is the SAME five questions IsBufferDrawClean asks
// the frontend object, asked by handle: the twin's identity, the
// resource record's Serial against the twin's synced one, and the
// pending-work sets - all server-owned (D-A4). The drawCleanEpoch
// short-circuit is unchanged; it was never the point read.
auto* backendResource = BufferImpl::FindBufferResourceForHandle(entry.Res);
if (!backendResource || backendResource->drawCleanEpoch != bufferEpoch) {
if (backendResource && BufferImpl::IsBufferDrawCleanByHandle(
entry.Res, backendResource, nullptr)) {
backendResource->drawCleanEpoch = bufferEpoch;
} else {
MGLOG_E_ONCE("No backend buffer found for UBO binding, cannot bind UBO.");
backendResource =
BufferImpl::EnsureBufferResourceForHandle(nullptr, entry.Res);
}
}
if (backendResource && backendResource->id != 0) {
// WHOLE-VS-RANGE IS THE RECORD's TEST, not `range.end == 0`: a
// base binding travels as kMGPipeWholeBuffer precisely so the
// extent is re-resolved HERE, against the storage this server
// holds, rather than frozen at the client's emission.
if (entry.Offset == 0 && entry.Size == MG_Pipe::kMGPipeWholeBuffer) {
BufferImpl::BindBufferBaseCached(GL_UNIFORM_BUFFER, lastUBOBinding,
backendResource->id);
} else {
const SizeT storage = backendResource->storageSize;
const SizeT rangeStart =
std::min<SizeT>(static_cast<SizeT>(entry.Offset), storage);
const SizeT rangeEnd = std::min<SizeT>(
static_cast<SizeT>(entry.Offset + entry.Size), storage);
BufferImpl::BindBufferRangeCached(
GL_UNIFORM_BUFFER, lastUBOBinding, backendResource->id,
static_cast<GLintptr>(rangeStart),
static_cast<GLsizeiptr>(rangeEnd - rangeStart));
}
} else {
MGLOG_E_ONCE("No backend buffer found for UBO binding, cannot bind UBO.");
}
continue;
}
}
@@ -6906,42 +6495,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
return true;
}
// MONOLITH GLUE, AND LOUD ABOUT IT AS OF P5e (vi). CONTRACT-P5E §5.1 makes the restart
// substitution read st.IndexBuffer.Res, and the constructor above already does: its
// transport arm resolves the element buffer and the bytes from the applier and its own
// staged shadow (P5c hd), and it sets serverElementBinding >= 0 BEFORE the null test,
// so the tail at BoundElementArrayBufferId() is unreachable with a live transport.
// These two are therefore only called from the monolith branch - and the refusal below
// is what keeps that a fact rather than a reading: a future caller that reaches them
// from the apply thread aborts by name instead of pulling GetBoundVertexArray behind
// the family's back.
void RefuseElementArrayBufferFromTheFrontend(const char* entry) {
MGLOG_F("MGPipe: Fatal{RoleViolation, \"MGPipeSlots\"} - %s read the frontend VAO's "
"element-array slot with a live transport. The index buffer of this family is "
"MGPipeApplier().IndexBuffer.Res (CONTRACT-P5E §5.1); the frontend slot is "
"monolith glue and answers for whatever the CLIENT has bound now, not for the "
"record this draw is being applied from",
entry);
std::abort();
}
const SharedPtr<MG_State::GLState::BufferObject>& BoundElementArrayBuffer() {
static const SharedPtr<MG_State::GLState::BufferObject> none;
RefuseElementArrayBufferFromTheFrontend("BoundElementArrayBuffer");
const auto& vao = MG_Pipe::gPipeInputs.GetBoundVertexArray();
if (!vao) return none;
return vao->GetIndexBufferBindingSlot().GetBoundObject();
}
// The GL name PrepareForDraw left on GL_ELEMENT_ARRAY_BUFFER, i.e. what the
// substitution has to put back.
Uint BoundElementArrayBufferId() {
RefuseElementArrayBufferFromTheFrontend("BoundElementArrayBufferId");
const auto& ibo = BoundElementArrayBuffer();
if (!ibo) return 0;
const auto* resource = BufferImpl::EnsureBufferResource(ibo);
return resource ? resource->id : 0;
}
} // namespace
RestartSubstitutionKind ResolveRestartSubstitution(GLenum indexType) {
@@ -7124,12 +6677,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
return;
}
m_previousBinding = 0;
if (serverElementBinding >= 0) {
m_previousBinding = static_cast<Uint>(serverElementBinding);
} else
{
m_previousBinding = BoundElementArrayBufferId();
}
if (serverElementBinding >= 0) m_previousBinding = static_cast<Uint>(serverElementBinding);
BufferImpl::BindBufferId(GL_ELEMENT_ARRAY_BUFFER, g_restartIndices.id);
m_substituted = true;
m_indexType = destinationType;
@@ -10257,9 +9805,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
// dereference. The frontend validator is what keeps this unreachable and what reports
// the error the application is owed; declining is only how a future gap up there stops
// being a crash. See the level guard in VulkanRenderer::CopyImageSubData.
if (!dstEndpoint.IsRenderbuffer() && dstEndpoint.Texture) {
TextureImpl::NoteDriverSideTextureWrite(dstEndpoint.Texture);
}
// The handle arm's endpoint names its texture by handle, and the server has no frontend
// object to note. The staged store's half of the note waits for the copy (P8-E, below).
const Bool handleArmDestination =
@@ -12036,19 +11581,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
SplitReadbackPackBuffer();
Bool usePBO = false;
GLuint packBufferId = 0;
if (pixelPackBufferObject) {
auto* backendResource = BufferImpl::EnsureBufferResource(pixelPackBufferObject);
MGLOG_D("ReadPixels: Using PBO %u", pixelPackBufferObject->GetExternalIndex());
if (!backendResource || backendResource->id == 0) {
MGLOG_E_ONCE("ReadPixels: No backend buffer found for PBO %u.",
pixelPackBufferObject ? pixelPackBufferObject->GetExternalIndex() : 0);
return;
}
usePBO = true;
packBufferId = backendResource->id;
} else {
MGLOG_D("ReadPixels: Not using PBO");
}
// P13: SplitReadbackPackBuffer() is always null - a pack buffer reaches the readback
// by handle on the verb path - so this entry never packs into a buffer.
MGLOG_D("ReadPixels: Not using PBO");
ScopedPixelPackBuffer packBufferBinding(packBufferId);
MGLOG_D("ReadPixels: glReadPixels()");
+31 -697
View File
@@ -211,22 +211,17 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
namespace {
// P2 step e2's dispatcher: the frontend told us an object died, so free its slot and
// drop its twin NOW. One entry point for all six kinds, because the answer is the same
// for all six - which is why the notice carries the kind rather than there being six
// ops tables.
//
// Each arm below names the registry GLOBAL of its kind, but DestroyByLifetimeId is
// static: it is answered by every table of that kind that exists at the moment - the
// global's own and any by-value copy a fixture or a context reset is holding - and
// the slot goes back once, after all of them have let go (SlotTables.h, the holder
// list). Naming one instance here is a spelling, not a choice of holder.
// P2 step e2's dispatcher: the frontend told us an object died, so drop its twin NOW.
// One entry point for all six kinds, because the answer is the same for all six - which
// is why the notice carries the kind rather than there being six ops tables. The release
// is by the handle the notice carries and reaches every holder of the kind (SlotTables.h,
// the holder list); the client frees the slot afterwards.
//
// A notice that arrives after exit() has begun is dropped: past that point the twin's
// destructor must not call into the driver (see InProcessTeardown()), and the process
// is about to hand every GPU object back anyway. That twin is a deliberate leak, not
// garbage for a later collection - there is none on this arm.
void OnFrontendStateObjectDestroyed(MG_Pipe::MGPipeKind kind, Uint64 lifetimeId) {
// is about to hand every GPU object back anyway. That twin is a deliberate leak.
void OnFrontendStateObjectDestroyed(MG_Pipe::MGPipeKind kind, Uint64 lifetimeId,
MG_Pipe::MGPipeHandle handle) {
if (InProcessTeardown()) return;
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (ct), CONTRACT-P5C.md §5.2: with an active transport the death crosses AS A
@@ -241,109 +236,22 @@ namespace MobileGL::MG_Backend::DirectGLES {
// nor the client's allocator is touched from there any more.
if (MG_Config::Transport != MG_Config::TransportMode::Monolith &&
!MG_Remote::Server::ServerLoop::OnApplyThread()) {
// P5e (id), CONTRACT-P5E §4.4: THE RECORD IS THE ONLY DELIVERY NOW. What used
// to sit here was a mailbox hop for the ONE shape the record cannot carry - a
// transport configured with no client session at all, i.e. a ServerLoop
// fixture driving a server role with no client, never a real split (there a
// session exists whenever the server does). It posted a stack struct to the
// apply thread, which re-entered this function and ran the switch below; that
// switch's DestroyByLifetimeId probes and FREES in the CLIENT'S allocator from
// the apply thread, which is precisely the read P5e is retiring - and it is
// not a read a barrier could ever make safe, because under run-ahead there is
// no wait behind which the allocator stands still.
//
// WHAT REPLACES IT: nothing, and that is the answer rather than an omission. A
// fixture with no client session has no client allocator worth the name and no
// handle ever crossed, so there is no twin the server built from a record; its
// twins die with the backend at Stop (InProcessTeardown / the registry's own
// destruction), which is what already happened for every death raised past
// `Running() == false`. ID-96 enumerates the fixtures this changed and what
// each one drives instead; every one of them now drives `object_death` by
// handle through the applier, which is the delivery a real split uses.
// ID-96, ANSWERED BY MEASUREMENT: the set of fixtures that lose death delivery
// here is EMPTY. A probe that aborted in exactly this branch (NoSession with a
// RUNNING server loop, i.e. the only shape the deleted hop ever fired for) was
// run over the whole unit lane (2204 entries) and the whole integration-split
// lane (111) and never fired once. The one fixture family that could reach it -
// ServerLoopTest.cpp's ServerFixture / EglServerFixture, the only server role in
// the tree with no client session - has exactly one case that lets a re-keyed
// frontend object die off the apply thread
// (ServerLoopEglTest.FrontendFramebufferDeathDeletesOnTheContextOwner), and that
// case is already refused at its FIRST step by P5c's allocator guard, so its
// `framebuffer.reset()` was unreachable at 2fde7034 too.
// P5e (id), CONTRACT-P5E §4.4: THE RECORD IS THE ONLY DELIVERY. A server role
// with no client session (a ServerLoop fixture) has no twin built from a record,
// so it has nothing to release here; its twins die with the backend at Stop.
(void)MG_Remote::Client::EmitObjectDeathRecord(kind, lifetimeId);
return;
}
// Falling through HERE under a transport means the death was raised ON the apply
// thread - the server backend destroying a frontend object it created itself
// (Magma's hidden blit/depth-mipmap resources). Those sites carry their own named
// scope (MagmaP7AllocatorDebtScope), so the allocator probe below is admitted
// there and refused everywhere else; this function no longer opens a scope of its
// own, because the one arm that needed it is the mailbox hop that just went.
// thread - the server backend destroying a frontend object it created itself.
#endif
switch (kind) {
case MG_Pipe::MGPipeKind::Texture:
TextureImpl::g_backendTextureObjects.DestroyByLifetimeId(lifetimeId);
break;
case MG_Pipe::MGPipeKind::Framebuffer:
FramebufferImpl::g_backendFramebufferObjects.DestroyByLifetimeId(lifetimeId);
break;
case MG_Pipe::MGPipeKind::Renderbuffer:
RenderbufferImpl::g_backendRenderbufferObjects.DestroyByLifetimeId(lifetimeId);
break;
case MG_Pipe::MGPipeKind::SamplerCso:
SamplerImpl::g_backendSamplerObjects.DestroyByLifetimeId(lifetimeId);
break;
case MG_Pipe::MGPipeKind::ShaderCso:
PrgramImpl::g_backendProgramObjects.DestroyByLifetimeId(lifetimeId);
break;
case MG_Pipe::MGPipeKind::SamplerViewCso:
// P4a (D-I1, D5): the sixth kind, and the only one whose notice names a
// lifetime id that belongs to ANOTHER object - a sampler view is minted off its
// texture's id, one per ITextureObject, so ~TextureObjectBase raises this arm
// and the texture arm above from the same id. That is legal precisely because
// the two kinds have separate slot spaces, and it is why the two arms are
// written out separately rather than folded: the allocator resolves
// (kind, lifetimeId), so each finds its own slot or nothing.
//
// IDEMPOTENT, like every arm here. This is the REDUNDANT SECOND PATH: the
// client's MGPipeEmitSamplerViewCsoDestroyAndFree emits delete_sampler_view,
// raises this notice and frees the slot, in that order. Whichever of the two
// frees first wins; the other resolves nothing, because the allocator erases
// its lifetimeId -> slot mapping on Free and OnFrontendObjectDestroyed answers
// false for a handle it cannot resolve. This twin owns no driver id at all, so
// even a double release is a pointer reset.
SamplerViewImpl::BackendSamplerViewTable::OnFrontendObjectDestroyed(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 did not until P7 wave 2 package C, and whose table even now only EMITS
// the wire record - 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:
// Buffer death crosses as ResourceDestroy (P3a): the catalogue has a call for
// it, so no seventh NotifyStateObjectDestroyed raiser is added. The notice
// exists for kinds that have NO such call - it carries {kind, lifetimeId} and
// not the object, and one entry point serves all of them because the answer is
// the same. The order at the buffer's death is fixed and not negotiable:
// resource_destroy first (the applier clears Live, the backend retires the
// twin), then MGPipeSlots().Free - the allocator erases its lifetimeId -> slot
// mapping on Free, so a notice resolved twice finds nothing the second time.
// On the legacy arm the signal is still BufferBackendOps::OnDestroy. Every
// other kind has no backend twin table here.
break;
}
// P13: the twin is released BY HANDLE, the one the notice carries - the same
// per-kind release the wire's object_death runs on the server
// (ReleaseTwinsForWireObjectDeath below), so no arm resolves a lifetime id in the
// allocator any more. The slot itself goes back in the caller (PipeFill.cpp's
// NotifyAndFree), after this returns. A null handle is an object that never had a
// slot of this kind, hence no twin.
if (!MG_Pipe::MGPipeHandleIsNull(handle)) (void)ReleaseTwinsForWireObjectDeath(handle, kind);
}
const MG_State::GLState::StateObjectDeathOps g_glesStateObjectDeathOps = {
@@ -351,7 +259,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
};
} // namespace
#if MOBILEGL_BUILD_DISAGGREGATED
// P5e (id), CONTRACT-P5E §4.2 / §4.3: THE TWIN DEATH MOMENT, AND THE ABA ANSWER.
//
// The moment: the apply of `object_death` / the kind's own delete opcode / `resource_destroy`
@@ -410,6 +317,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
}
#if MOBILEGL_BUILD_DISAGGREGATED
// P12: see Managers.h. Every table is replaced by an empty one, holders first (views before
// the textures they view, framebuffers before their attachments, VAOs before the buffers they
// fetch from), so the last reference to each twin goes here and its destructor - answered
@@ -3627,67 +3535,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
resource.hostBytes = MGL_SERVER_STAGED_ADOPT(resource, ResourceWidthOf(res), bytes, offset, size);
}
MG_Pipe::MGPipeHandle HandleOfBuffer(const MG_State::GLState::BufferObject* bufferObject) {
if (bufferObject == nullptr) return MG_Pipe::kMGPipeNullHandle;
// Through the table's own single-entry front memo rather than straight into
// MGPipeSlots().FindByLifetimeId, which is a hash lookup: this runs inside
// EnsureBufferResource (every SSBO / UBO / atomic / XFB / PBO / IBO bind), inside
// IsBufferDrawClean, and once per SSBO per draw through MarkBufferGpuWritten, where
// the legacy arm paid a pointer compare. HandleOf answers identically - the same
// allocator probe on a miss - and remembers the answer, which the minting overload
// was previously the only writer of. A null answer is deliberately never memoised
// (see the comment at HandleOf).
// P5c merge coordination (CONTRACT-P5C §3.1's named exemption): every caller of
// this function is a site whose handle-carrying records (set_shader_buffers /
// set_stream_output_targets) the client does not emit until sb, so the probe
// below runs inside the scoped exemption. The scope, not a silent guard removal,
// is what keeps the debt named and greppable.
//
// P5e (id), ruling 12: THE SCOPE CHANGED UNDER THIS SITE AND THE SITE HAD TO MOVE.
// The exemption it used to name is now MagmaP7AllocatorDebtScope and is keyed on a
// DirectVulkan server (Magma's four P7 debts); this is Espryt's, and it belongs to
// the frontend-keyed registry family with the rest of the Espryt debt. Which scope
// it names is not cosmetic: the Magma one would not exempt it here at all. It
// retires when vi/sb carry the handle into EnsureBufferResourceForHandle /
// IsBufferDrawCleanByHandle / MarkBufferGpuWrittenByHandle, which is the last
// caller set this function has.
const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry;
return g_backendBufferResources.HandleOf(bufferObject);
}
SizeT ResourceWidthForHandle(MG_Pipe::MGPipeHandle res) { return ResourceWidthOf(res); }
Uint64 ResourceSerialForHandle(MG_Pipe::MGPipeHandle res) { return ResourceSerialOf(res); }
Uint CurrentBufferContextGeneration() { return g_bufferContextGeneration; }
void MarkBufferGpuWritten(const SharedPtr<MG_State::GLState::BufferObject>& bufferObject) {
if (!bufferObject) return;
const MG_Pipe::MGPipeHandle res = HandleOfBuffer(bufferObject.get());
if (MG_Pipe::MGPipeHandleIsNull(res) || MG_Pipe::gMGPipeCallbacks.OnGpuWritten == nullptr) {
// Deliberately NOT a fall-back to MarkGpuWritten: on this arm the resource
// family is switched over, and quietly reaching into the frontend object again
// would hide a missing handle or a missing reverse channel behind a picture
// that still looks right - which is what the subsystem A/B exists to expose.
MGLOG_E_ONCE("MGPipe: no reverse channel for the GPU-write announcement of buffer %u "
"(handle %s, OnGpuWritten %s)",
bufferObject->GetExternalIndex(),
MG_Pipe::MGPipeHandleIsNull(res) ? "missing" : "present",
MG_Pipe::gMGPipeCallbacks.OnGpuWritten == nullptr ? "unset" : "set");
return;
}
// WHOLE RESOURCE, stated rather than implied: the extent is spelled as one range of
// kMGPipeWholeBuffer rather than as "zero ranges", because a zero count is the
// shape a fully NARROWED announcement will legitimately have once P8/P9 build the
// client's conservative set, and the two must not be the same record.
//
// THE OTHER HALF OF THE CONTRACT, which the range shape alone does not say: what
// this replaces, BufferObject::MarkGpuWritten, sets BOTH m_hasDefinedContent and
// m_gpuWritePending. The client's OnGpuWritten must set both too, or the next
// ResourceRespecify carries HasDefinedContent = 0, Ops_H_Respecify orphans instead
// of uploading, and the shader-written contents are dropped with nothing saying so.
const MG_Pipe::MGPRange whole{0, MG_Pipe::kMGPipeWholeBuffer};
MG_Pipe::gMGPipeCallbacks.OnGpuWritten(res, 1, &whole);
}
// See the declaration: re-mints of a live resource's driver id. Written only on
// the context thread (all re-mint sites run there), read only by the VAO sync.
Uint64 g_bufferBackendIdGeneration = 0;
@@ -3893,10 +3744,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
return resource->syncedChangeSerial.load(std::memory_order_acquire) == record->Serial;
}
Bool IsBufferDrawClean(const MG_State::GLState::BufferObject* frontend, const GLESBufferResource* resource) {
return IsBufferDrawCleanByHandle(HandleOfBuffer(frontend), resource, frontend);
}
// The handle arm of the ensure path. Same shape, same order, same branches; the four
// frontend reads become the applier's stored descriptor, its Serial and the shadow base
// the last content-carrying call handed over.
@@ -4146,26 +3993,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
return resource;
}
GLESBufferResource* EnsureBufferResource(const SharedPtr<MG_State::GLState::BufferObject>& bufferObject) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (!bufferObject) return nullptr;
const MG_Pipe::MGPipeHandle res = HandleOfBuffer(bufferObject.get());
if (MG_Pipe::MGPipeHandleIsNull(res)) {
// The client never created this resource on the wire. There is deliberately
// no fall-back to the legacy arm: silently twinning it off the frontend
// object would hide a missing emission behind a working picture, which is
// exactly what the subsystem A/B exists to make visible.
MGLOG_E_ONCE("MGPipe: buffer %u has no resource handle - the resource family is switched "
"over but nothing emitted resource_create for it",
bufferObject->GetExternalIndex());
return nullptr;
}
return EnsureBufferResourceForHandle(bufferObject, res);
}
void BindBufferId(GLenum target, Uint id) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
@@ -5224,17 +5051,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
return twin ? twin->get() : nullptr;
}
MG_Pipe::MGPipeHandle HandleOfSamplerViewForTexture(
const MG_State::GLState::ITextureObject* textureObject) {
// Through the table's own single-entry front memo rather than straight into
// MGPipeSlots().FindByLifetimeId, which is a hash lookup. HandleOf answers
// identically - the same allocator probe on a miss - and remembers the answer; a
// null answer is deliberately never memoised (SlotTables.h, at HandleOf).
// P5c (G6, CONTRACT-P5C §5.4): frontend-keyed twin resolution, named debt inside
// the scope - P3b/P4b rekeys the sampler-view registry onto handles.
const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry;
return g_backendSamplerViews.HandleOf(textureObject);
}
} // namespace SamplerViewImpl
namespace VertexArrayImpl {
@@ -5361,23 +5177,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
BindBackendVAOId(m_backendVAOId);
}
inline Bool BindAttributeBuffer(const MG_State::GLState::VertexAttribute& attrib) {
const auto& bufferObject = attrib.Buffer;
if (!bufferObject) {
MGLOG_W_ONCE("Attribute has no bound buffer, skipping.");
return false;
}
auto* backendResource = BufferImpl::EnsureBufferResource(bufferObject);
if (!backendResource || backendResource->id == 0) {
MGLOG_E_ONCE("No backend buffer found for attribute's buffer, cannot bind attribute.");
return false;
}
BufferImpl::BindBufferId(GL_ARRAY_BUFFER, backendResource->id);
return true;
}
// ES 3.1 core. Queried through the loader rather than the version, because the whole
// point of using it is to express something the pointer API cannot, and falling back
// silently on a driver that lacks it is better than crashing on a null entry point.
@@ -5406,41 +5205,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
return static_cast<SizeT>(baseInstance) * static_cast<SizeT>(attrib.Stride);
}
// Declares one attribute through the ES binding-point API, the only spelling that can
// carry a stride of zero. Returns false when the attribute has no usable buffer, in
// which case nothing was emitted.
inline Bool SyncZeroStrideAttribute(Uint attribIndex, const MG_State::GLState::VertexAttribute& attrib) {
const auto& bufferObject = attrib.Buffer;
if (!bufferObject) {
MGLOG_W_ONCE("Zero-stride attribute %u has no bound buffer, skipping.", attribIndex);
return false;
}
auto* backendResource = BufferImpl::EnsureBufferResource(bufferObject);
if (!backendResource || backendResource->id == 0) {
MGLOG_E_ONCE("No backend buffer for zero-stride attribute %u, cannot bind it.", attribIndex);
return false;
}
if (!attrib.IsInteger) {
const GLint glSize = attrib.IsBgra ? static_cast<GLint>(GL_BGRA) : attrib.Size;
g_GLESFuncs.glVertexAttribFormat(attribIndex, glSize,
MG_Util::ConvertDataTypeToGLEnum(attrib.Type),
attrib.Normalized ? GL_TRUE : GL_FALSE, 0);
} else {
g_GLESFuncs.glVertexAttribIFormat(attribIndex, attrib.Size,
MG_Util::ConvertDataTypeToGLEnum(attrib.Type), 0);
}
g_GLESFuncs.glVertexAttribBinding(attribIndex, attribIndex);
// The resolved offset goes on the binding point, not into a relative offset: the
// relative offset is capped by GL_MAX_VERTEX_ATTRIB_RELATIVE_OFFSET (2047 at
// minimum) while a buffer offset is not, so anything else would break on a large
// one. BindBufferId is bypassed deliberately - glBindVertexBuffer binds into the
// VAO's binding point, not the GL_ARRAY_BUFFER target that cache tracks.
g_GLESFuncs.glBindVertexBuffer(attribIndex, backendResource->id,
static_cast<GLintptr>(attrib.Offset), 0);
return true;
}
// ---- the handle arm's small helpers -------------------------------------------
//
// Each one is its object-shaped counterpart above with the frontend reads replaced by
@@ -6467,162 +6231,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
return true;
}
void BackendTextureObject::RequireImageBindableStorage(
const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject) {
if (m_imageBindableStorageRequired) {
return;
}
// Read before the two flags below schedule the re-mint: whether the driver texture
// already holds this object's levels, and so may hold texels the shadow never saw.
// Also what `trp=` counts on the push builds: a transition that re-mints storage that
// ALREADY EXISTED on the backend. A texture reaching here uninitialised is allocated
// image-bindable up front and replays nothing.
const Bool hadBackendStorage = m_isInitialized && m_backendTextureId != 0 &&
m_contextGeneration == g_backendContextGeneration;
m_imageBindableStorageRequired = true;
// ES only needs the storage to be immutable, so an immutable allocation that needs no
// channel widening is already the one a re-mint would produce: keep it, and with it
// every texel the GPU wrote. Pending uploads and real redefinitions still go through
// the normal shape and dirty checks of the next sync.
if (hadBackendStorage && m_backendStorageImmutable && !stateTextureObject->IsTextureView() &&
!IsMultisampleTextureTarget(stateTextureObject->GetTarget()) &&
!GetImageBindableStorageWidening(stateTextureObject->GetFormat())) {
m_forceTextureParamsResync = true;
return;
}
m_isInitialized = false;
// A view owns no levels. Its sync hands the requirement to the storage texture
// (SyncTextureViewToBackend), whose own transition below re-mints and replays them; a
// re-dirty issued here, through the view, would reach that texture first and claim
// every level for the shadow before the driver's copy could be read back.
if (stateTextureObject->IsTextureView()) {
m_forceTextureParamsResync = true;
return;
}
// Every level this object has ALREADY uploaded has to be replayed, because the
// regeneration this transition schedules re-mints the storage in the image carrier and
// only uploads levels the shadow still calls dirty - which, for a texture that was
// synced before its first glBindImageTexture, is none of them. The new storage would
// come out ALLOCATED AND EMPTY, and every texel the application defined before that
// bind would be gone: the shader reads zeroes and the shadow still holds the data, so
// glGetTexImage (which falls back to the shadow) keeps answering correctly and only
// the image loads are wrong. Reached whenever anything syncs the texture first - a
// glGetTexImage, a draw that samples it, an FBO attach - which is why it survived so
// long: the scenario that binds the image immediately after uploading never sees it.
//
// And the shadow is only the CLIENT's copy. Whatever the GPU wrote into a level - a
// render or clear into it, a copy, a glGenerateMipmap - lives in the driver texture
// alone, so replaying the shadow as it stands put the pre-render bytes (or the zeroes
// of a null-data definition) into the new storage. The driver's levels are therefore
// read back into the shadow first, with the client writes still pending merged over
// them, while the old storage still exists; a level the driver cannot hand back keeps
// the shadow, which is all this used to replay. (This was the known monolith defect of
// W2-REMINT.md §6, the half of ID-P9-8 left to dev; dev's 568090f0 fixed it and P11 M
// merged it.)
//
// THIS IS THE MONOLITH ARM ONLY. Under a transport the re-mint is the server's own
// (RequireImageBindableStorageByHandle: the GPU level read back, the staged store for
// what the client uploaded), and nothing selects this function there - the image
// units sync from the record, the metadata respecify calls the by-handle overload, and
// a frontend object reaching the loop below would meet the layer-1 guard
// (Fatal{RoleViolation, "texture-legacy-arm"}) first. P9 W2 retired the
// `texture-remint-pull` marker this comment used to describe: no pull protocol exists
// because no byte of a re-mint has to come from the client (notes/p9/W2-REMINT.md).
// The two arms now answer the same way: both read the GPU's level back first.
//
// THE COUNT IS TAKEN WHERE A LEVEL IS ACTUALLY REPLAYED, not on entry (review N-4): a
// glBindImageTexture on a texture whose storage is not yet defined reaches here, finds
// no defined level and replays nothing.
// P4a (review M-1): ON THE HANDLE ARM THE RE-DIRTY HAS TO REACH THE APPLIER'S SET,
// not only the frontend's model. The client cleared its own flags when it emitted
// those levels, so MarkStorageDirty below re-arms a model the handle arm does not
// read: the pending set would stay empty, MGB_LEVEL_NEEDS_UPLOAD would answer false
// for every level, and the widened image carrier this transition schedules would be
// allocated EMPTY - the very bug the paragraph above says "survived so long",
// re-introduced one arm over. The dispatch that triggered the bind would read zeroes.
//
// Both models are written, not one: the frontend's because the legacy arm reads it
// and because the client's own NoteLevelDirty hook rides on it, the applier's
// because that is what this arm consumes.
Bool countedRemint = false;
const MG_Pipe::MGPipeHandle rearmRes =
[&]() {
// P5c (G6, CONTRACT-P5C §5.4): frontend-keyed resolution, named
// debt inside the scope - P3b/P4b rekeys the registry.
const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry;
return g_backendTextureObjects.HandleOf(stateTextureObject.get());
}();
const Uint32 rearmResourceTarget =
MG_Pipe::MGPipeResourceTargetForTextureTarget(stateTextureObject->GetTarget());
if (MG_Pipe::MGPipeHandleIsNull(rearmRes)) {
MGLOG_E_ONCE("MGPipe: texture %u needs image-bindable storage but has no handle on the "
"handle arm, so the levels it owes cannot be re-armed in the applier's "
"pending set - they would be allocated empty",
stateTextureObject->GetExternalIndex());
}
if (auto* mipmapObject = MG_State::GLState::AsMipmapTexture(stateTextureObject.get())) {
const auto levelCount = mipmapObject->GetMipmapLevelCount();
for (const auto& uploadTarget : stateTextureObject->GetUploadTargets()) {
for (Uint level = 0; level < levelCount; ++level) {
const auto levelTexelSize = mipmapObject->GetMipmapTexelSize(uploadTarget, level);
if (levelTexelSize.x() <= 0 || levelTexelSize.y() <= 0) continue;
if (mipmapObject->GetMipmapByteSize(uploadTarget, level) == 0) continue;
// This level's entry in the applier's pending set, read BEFORE the re-arm
// below turns it into a whole-level box: the client writes it still owes
// (see AdoptDriverLevelIntoShadow). Resolved per level - the re-arm may grow
// the record's set and move what an earlier pointer named.
const MG_Pipe::MGPipeResourceRecord* rearmRecord =
MG_Pipe::MGPipeHandleIsNull(rearmRes) ? nullptr : PipeTextureRecordForHandle(rearmRes);
const MG_Pipe::MGPipeResourceRecord::PendingUpload* rearmPending =
rearmRecord != nullptr ? FindPipeTextureUpload(*rearmRecord, static_cast<Uint16>(uploadTarget),
static_cast<Uint16>(level))
: nullptr;
if (hadBackendStorage &&
!AdoptDriverLevelIntoShadow(m_backendTextureId, *this, *stateTextureObject,
*mipmapObject, uploadTarget, level
,
rearmPending
)) {
MGLOG_D("Texture %u level %u: re-minting from the shadow, the driver's copy is not "
"readable here",
stateTextureObject->GetExternalIndex(), level);
}
mipmapObject->MarkStorageDirty(uploadTarget, level, true);
// THE COUNTER BEHIND ROADMAP OPEN QUESTION 2 (final review M-A): once per
// transition that replays a level of storage the backend already held,
// not once per level.
if (!countedRemint) {
countedRemint = true;
if (hadBackendStorage && MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::TextureRemintPulls, 1);
}
}
if (!MG_Pipe::MGPipeHandleIsNull(rearmRes)) {
const MG_Pipe::MGPBox wholeLevel{0,
0,
0,
static_cast<Uint32>(levelTexelSize.x()),
static_cast<Uint32>(levelTexelSize.y()),
static_cast<Uint32>(std::max(levelTexelSize.z(), 1))};
RearmPipeTextureLevelUpload(
rearmRes,
MG_Pipe::MGPipePackSubDataTarget(rearmResourceTarget,
static_cast<Uint32>(uploadTarget)),
static_cast<Uint16>(level), wholeLevel);
}
}
}
}
// The storage this re-mints may also be CHANNEL WIDENED (a GL_RG32F image is not
// bindable on this driver at all, so it becomes a GL_RGBA32F carrying two channels),
// and a widened texture's sampled view has to answer the channels the logical format
// does not have with 0 and 1 - which is a swizzle. The parameter sync is gated on the
// frontend's params version, which this transition does not move, so without the
// override an application that never touched GL_TEXTURE_SWIZZLE_* would keep the
// driver at its defaults and sample the carrier's surplus channels raw.
m_forceTextureParamsResync = true;
}
void BackendTextureObject::RecreateBackendTexture() {
if (m_backendTextureId != 0) {
ScratchFBOImpl::NoteTextureIdDeleted(m_backendTextureId);
@@ -7891,99 +7499,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
m_syncedContentVersion = stateTextureObject->GetContentVersion();
}
void BackendTextureObject::SyncTextureViewToBackend(
const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject) {
const auto& storageObject = stateTextureObject->GetViewStorageOwner();
if (!storageObject) {
MGLOG_E_ONCE("Texture %u claims to be a view but names no storage owner.",
stateTextureObject->GetExternalIndex());
return;
}
if (!g_GLESCapabilities.SupportsTextureView) {
// Unreachable through the API: the frontend refuses glTextureView with
// GL_INVALID_OPERATION when the backend does not advertise GL_ARB_texture_view,
// and DirectGLES only advertises it when this capability is set.
MGLOG_E_ONCE("Texture view %u reached the backend on a driver without "
"EXT/OES_texture_view.",
stateTextureObject->GetExternalIndex());
return;
}
// Deliberately a by-VALUE copy of the SharedPtr: SyncTextureObjectToBackend hands back
// a reference INTO the open-addressed registry map, and the params/sampler syncs below
// (plus any nested growth) can rehash it out from under a reference.
const SharedPtr<BackendTextureObject> storageBackendObject =
SyncTextureObjectToBackend(storageObject, m_imageBindableStorageRequired);
if (!storageBackendObject) {
MGLOG_E_ONCE("Failed to sync the storage texture of view %u.",
stateTextureObject->GetExternalIndex());
return;
}
const Uint storageBackendTextureId = storageBackendObject->GetBackendTextureId();
if (storageBackendTextureId == 0) {
MGLOG_D("Storage texture of view %u has no ES name yet.",
stateTextureObject->GetExternalIndex());
return;
}
if (m_isInitialized && m_viewSourceBackendTextureId == storageBackendTextureId) {
StampViewSyncKeys(stateTextureObject);
return;
}
// Either the first sync, or the storage was re-minted underneath us. A name that has
// already been through glTextureView cannot be viewed again, so start from a fresh
// one (this also scrubs the binding caches and bumps the FBO attachment generation).
RecreateBackendTexture();
GLenum glInternalFormat = 0;
GLenum glFormat = 0;
GLenum glType = 0;
TextureImpl::GenerateTextureFormatInfo(stateTextureObject->GetFormat(), &glInternalFormat, &glFormat,
&glType, stateTextureObject->GetTarget());
const GLenum target = ConvertTextureTargetToBackendGLEnum(stateTextureObject->GetTarget());
DebugImpl::ErrorLopper::Clear();
ResolveTextureViewEntryPoint()(m_backendTextureId, target, storageBackendTextureId, glInternalFormat,
stateTextureObject->GetViewMinLevel(),
stateTextureObject->GetViewNumLevels(),
stateTextureObject->GetViewMinLayer(),
stateTextureObject->GetViewNumLayers());
const GLenum error = g_GLESFuncs.glGetError();
if (error != GL_NO_ERROR) {
MGLOG_E_ONCE("glTextureView(view=%u target=%s origtexture=%u internalformat=%s levels=[%u,%u) "
"layers=[%u,%u)) failed: %s",
m_backendTextureId, MG_Util::ConvertGLEnumToString(target).c_str(),
storageBackendTextureId, MG_Util::ConvertGLEnumToString(glInternalFormat).c_str(),
stateTextureObject->GetViewMinLevel(),
stateTextureObject->GetViewMinLevel() + stateTextureObject->GetViewNumLevels(),
stateTextureObject->GetViewMinLayer(),
stateTextureObject->GetViewMinLayer() + stateTextureObject->GetViewNumLayers(),
MG_Util::ConvertGLEnumToString(error).c_str());
return;
}
m_viewSourceBackendTextureId = storageBackendTextureId;
m_isInitialized = true;
// A view's storage is immutable by construction (its origtexture had to be), which is
// what keeps the respecify paths away from this name.
m_backendStorageImmutable = true;
const auto baseSize = stateTextureObject->GetBaseSize();
m_prevTextureInfo = {stateTextureObject->GetFormat(),
static_cast<SizeT>(baseSize.x()),
static_cast<SizeT>(baseSize.y()),
static_cast<SizeT>(baseSize.z()),
static_cast<SizeT>(stateTextureObject->GetViewNumLevels()),
0,
stateTextureObject->GetSamples(),
stateTextureObject->HasFixedSampleLocations()};
MGLOG_D("Texture view %u (ES %u) now views storage texture %u (ES %u), levels [%u,%u) layers [%u,%u)",
stateTextureObject->GetExternalIndex(), m_backendTextureId, storageObject->GetExternalIndex(),
storageBackendTextureId, stateTextureObject->GetViewMinLevel(),
stateTextureObject->GetViewMinLevel() + stateTextureObject->GetViewNumLevels(),
stateTextureObject->GetViewMinLayer(),
stateTextureObject->GetViewMinLayer() + stateTextureObject->GetViewNumLayers());
StampViewSyncKeys(stateTextureObject);
}
void BackendTextureObject::SyncMipmapsToBackend(
const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject) {
// P5e (tx2): null IS the by-handle arm once a handle has been noted on this twin;
@@ -8052,17 +7567,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
SyncTextureViewToBackendByRecord(pushedRes, *pushedStorage, stateTextureObject);
return;
}
if (pushedStorage != nullptr) {
// Not a view: fall through to the storage body with the frontend test skipped.
} else
// A texture created by glTextureView owns no storage: the levels, the format and
// every texel belong to the texture it views, and this name only has to be made to
// ALIAS them. Everything below - storage allocation, respecification, per-level
// uploads - would be re-doing the storage texture's work on the wrong name.
if (stateTextureObject->IsTextureView()) {
SyncTextureViewToBackend(stateTextureObject);
return;
}
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
@@ -9377,8 +8881,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// Need to sync texture buffer if not synced yet
auto* backendBufferResource =
pushedStorage != nullptr ? BufferImpl::EnsureBufferResourceForHandle(buffer, pushedTexBuffer)
: BufferImpl::EnsureBufferResource(buffer);
BufferImpl::EnsureBufferResourceForHandle(buffer, pushedTexBuffer);
if (!backendBufferResource || backendBufferResource->id == 0) {
MGLOG_E_ONCE("Failed to sync backing buffer for texture buffer with ID: %u",
MGB_TEXTURE_DIAG_NAME(pushedStorage, stateTextureObject));
@@ -9579,22 +9082,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
#undef MGB_LEVEL_TEXELS
#undef MGB_STAGED_TEXTURE_LIVE
// P5e (tx2), CONTRACT-P5E §5.2: THIS TWIN'S OWN RECORD. On the by-handle arm the handle
// is already known - the caller resolved the record before it resolved this twin and
// noted the handle on the twin itself - so the client allocator is not probed at all.
// What is left below is the MONOLITH GLUE half (HandleOfBuffer's shape): a twin minted
// from a frontend object and never adopted by handle still re-derives one.
// P5e (tx2), CONTRACT-P5E §5.2: THIS TWIN'S OWN RECORD, by the handle the caller noted on
// the twin before it resolved it; the client allocator is never probed.
const MG_Pipe::MGPipeResourceRecord* BackendTextureObject::ResolveOwnRecord(
const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject) const {
MG_Pipe::MGPipeHandle res = m_pushedSyncHandle;
if (MG_Pipe::MGPipeHandleIsNull(res)) {
if (!stateTextureObject) return nullptr;
// P5c (G6, CONTRACT-P5C §5.4): frontend-keyed resolution, named debt inside the
// scope - the by-handle arm above is what retires it.
const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry;
res = g_backendTextureObjects.HandleOf(stateTextureObject.get());
}
return PipeTextureRecordForHandle(res);
(void)stateTextureObject;
// P13: a twin is only ever reached by handle, and the handle is noted on it first.
if (MG_Pipe::MGPipeHandleIsNull(m_pushedSyncHandle)) return nullptr;
return PipeTextureRecordForHandle(m_pushedSyncHandle);
}
const SamplerParameters* BackendTextureObject::ResolvePushedBuiltinSampler(
@@ -10143,21 +9638,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
#endif
void NoteDriverSideTextureWrite(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject) {
SharedPtr<MG_State::GLState::ITextureObject> storage = textureObject;
// Bounded: GL resolves a view of a view to the original storage, so this takes one step.
for (Uint hop = 0; storage && storage->IsTextureView() && hop < 4; ++hop) {
storage = storage->GetViewStorageOwner();
}
if (!storage) return;
auto* slot = g_backendTextureObjects.Find(storage.get());
auto& backendObj = slot ? *slot : g_backendTextureObjects.GetOrCreate(storage);
if (!backendObj) {
backendObj = MakeShared<BackendTextureObject>();
}
backendObj->NoteDriverSideWrite();
}
// P5e (id), CONTRACT-P5E §4.1. See Managers.h for the contract.
BackendTextureObject* ResolveTextureTwin(MG_Pipe::MGPipeHandle res) {
if (MG_Pipe::MGPipeHandleIsNull(res)) return nullptr;
@@ -14236,40 +13716,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
m_syncedSamplerSerial = record->Serial;
pushedParams = &record->Params;
} else {
// NO HANDLE CARRIED. Two shapes, and they are told apart by whether this
// object is in the twin registry at all:
//
// * not registered -> a sampler this BACKEND minted for its own use (the
// raw-depth-fetch sampler, DirectGLES.cpp:207-218). The client has never
// seen it, no record can exist for it now or after any package lands,
// and the object IS the authority for server-owned state. Not a seam.
// * registered -> an application sampler whose caller did not carry the
// unit's handle. That is the E-side call-site gap; it is named once and
// the values are taken from the object, which in monolith are the very
// values the client content-addressed, so the picture stays right while
// the gap is visible rather than silent.
// P5c (G6, CONTRACT-P5C §5.4): the registration probe below resolves the
// sampler's handle by frontend identity - frontend-keyed twin resolution,
// named debt inside the scope - P3b/P4b rekeys the registry onto handles.
const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry;
if (!MG_Pipe::MGPipeHandleIsNull(
g_backendSamplerObjects.HandleOf(stateSamplerObject.get()))) {
MGLOG_E_ONCE("MGPipe: sampler %u was synced without its unit's SamplerCso handle, so "
"the content-addressed record cannot be named and the object's own "
"parameters are used - the caller must pass "
"MGPipeApplier().BoundSamplerStates[unit]",
stateSamplerObject->GetExternalIndex());
}
// Spelled exactly as the pre-handle arm below spells it (compare widened,
// assign narrowed) so the two cannot drift on a version past 65535.
const Uint currentSamplerVersion = stateSamplerObject->GetVersion();
if (m_isInitialized && m_syncedSamplerVersion == currentSamplerVersion) {
MGLOG_D("Sampler parameters have not changed for sampler ID: %u, skipping sync.",
stateSamplerObject->GetExternalIndex());
return;
}
m_syncedSamplerVersion = currentSamplerVersion;
pushedParams = &stateSamplerObject->GetAllSamplerParameters();
// P13: every sampler twin is a CSO twin, synced with its handle
// (ResolveSamplerCsoTwin); a sampler with no record has nothing to push.
MGLOG_E_ONCE("MGPipe: sampler %u was synced without a SamplerCso handle", samplerName);
return;
}
MGLOG_D("Syncing sampler with backend ID %u to backend for state ID %u", m_backendSamplerId, samplerName);
@@ -14474,122 +13924,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
#define MGB_RBO_SAMPLES \
(pushedRecord != nullptr ? static_cast<Int>(pushedRecord->Desc.Samples) : static_cast<Int>(stateRBOObject->GetSamples()))
void BackendRenderbufferObject::SyncToBackend(
const SharedPtr<MG_State::GLState::RenderbufferObject>& stateRBOObject) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (!stateRBOObject) {
MGLOG_E_ONCE("State RBO object is null, cannot sync to backend.");
return;
}
MGLOG_D("Syncing RBO with backend ID %u to backend for state ID %u", m_backendRBOId,
stateRBOObject->GetExternalIndex());
const MG_Pipe::MGPipeResourceRecord* pushedRecord = nullptr;
if (TextureResourceSubsystemEnabled()) {
// P5c (G6, CONTRACT-P5C §5.4): the renderbuffer's handle is resolved by
// frontend identity below - frontend-keyed twin resolution, named debt inside
// the scope - P3b/P4b rekeys the registry onto handles.
const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry;
// MONOLITH GLUE, named as such: the Renderbuffer handle of an object this
// backend still arrives holding. Under a real split it rides in the payload.
const MG_Pipe::MGPipeHandle res = g_backendRenderbufferObjects.HandleOf(stateRBOObject.get());
pushedRecord = PipeRenderbufferRecordForHandle(res);
if (pushedRecord == nullptr) {
MGLOG_E_ONCE("MGPipe: renderbuffer %u has no applier record on the handle arm, so "
"its storage cannot be allocated from the pushed descriptor "
"(handle {%u, %u})",
stateRBOObject->GetExternalIndex(), res.Slot, res.Gen);
return;
}
if (m_isInitialized && m_syncedResourceSerial != 0 &&
m_syncedResourceSerial == pushedRecord->Serial) {
MGLOG_D("RBO %u already initialized with matching parameters, skipping re-allocation.",
stateRBOObject->GetExternalIndex());
return;
}
} else {
// UNREACHABLE: ResolveTextureResourceSubsystemArm stops at its first call when
// the bit is clear and the pre-handle arm is not compiled. Kept, and kept loud.
MGLOG_E_ONCE("MGPipe: the texture-resource subsystem bit is clear and "
"MOBILEGL_PIPE_LEGACY_MEMOS=0 removed the pre-handle renderbuffer "
"four-field cache, so this configuration has no arm at all");
return;
}
// A RE-STORAGE IS A REDEFINITION ON THE SAME DRIVER ID (P4a fable seam F-3, the
// pre-handle half): glRenderbufferStorage below re-allocates behind the name the
// framebuffer twins already attached, the frontend's renderbuffer setters bump no
// version and no framebuffer version sees them, so without this the FBO memo kept
// the widening masks of the storage the renderbuffer was attached with. Same
// generation a texture re-mint takes, for the same reason; the first allocation is
// not a redefinition.
// PUSH BUILDS ONLY, for the texture twin's reason (G1: the pull library stays
// byte-identical to the P4a baseline).
if (m_isInitialized) {
++FramebufferImpl::g_attachmentBackendIdGeneration;
}
Bind();
// Allocate storage
TextureInternalFormat internalFormat = MGB_RBO_FORMAT;
Int width = MGB_RBO_WIDTH;
Int height = MGB_RBO_HEIGHT;
Int samples = MGB_RBO_SAMPLES;
GLenum glInternalFormat, glType, glFormat;
TextureImpl::GenerateRenderbufferFormatInfo(internalFormat, &glInternalFormat, &glFormat, &glType);
// The allocation is deferred to here, so an ES driver that refuses it (a
// multi-gigabyte renderbuffer is refused routinely) used to leave m_isInitialized
// true over a renderbuffer with no storage and say nothing at all: the attachment
// then rendered nowhere. Drain first so the check cannot pick up an unrelated stale
// flag, and report GL_OUT_OF_MEMORY to the application. The error lands on whatever
// entry point triggered the sync rather than on glRenderbufferStorage itself, which
// is where the deferred model puts it - still far better than silence.
DebugImpl::ErrorLopper::Clear();
if (samples > 0) {
// Same clamp as the multisample texture path: the frontend accepts the count it
// advertised, the driver only takes the count it supports for this format, and
// the state object keeps reporting the requested one.
const auto backendSamples = static_cast<GLsizei>(ClampSamplesToBackendSupport(
GetRenderbufferFormatCapabilityTargetIndex(), internalFormat, glFormat, samples));
g_GLESFuncs.glRenderbufferStorageMultisample(GL_RENDERBUFFER, backendSamples, glInternalFormat,
static_cast<GLsizei>(width),
static_cast<GLsizei>(height));
} else {
g_GLESFuncs.glRenderbufferStorage(GL_RENDERBUFFER, glInternalFormat, static_cast<GLsizei>(width),
static_cast<GLsizei>(height));
}
if (g_GLESFuncs.glGetError() == GL_OUT_OF_MEMORY) {
MGLOG_E_ONCE("Renderbuffer %u storage allocation ran out of memory: %dx%d, samples=%d, format=%s",
stateRBOObject->GetExternalIndex(), width, height, samples,
MG_Util::ConvertGLEnumToString(glInternalFormat).c_str());
if (MG_Pipe::gPipeInputs.IsLive()) {
MG_Pipe::gPipeInputs.RecordError(
ErrorCode::OutOfMemory,
MakeUnique<GenericErrorInfo>("DirectGLES", "BackendRenderbufferObject::SyncToBackend",
"The ES driver could not allocate the renderbuffer storage."));
}
}
DebugImpl::ErrorLopper::Clear();
m_cacheInternalFormat = internalFormat;
m_cacheWidth = width;
m_cacheHeight = height;
m_cacheSamples = samples;
// Stamped in the same breath as the four cache members and AFTER the allocation, so
// an allocation the driver refused with GL_OUT_OF_MEMORY above leaves the twin
// describing what it actually holds. The deferred OOM report and its RecordError are
// untouched: the error still lands on whatever entry point triggered the sync, which
// is where the deferred model puts it.
if (pushedRecord != nullptr) m_syncedResourceSerial = pushedRecord->Serial;
m_isInitialized = true;
MGLOG_D("RBO %u sync completed. backend ID %u", stateRBOObject->GetExternalIndex(), m_backendRBOId);
}
#undef MGB_RBO_FORMAT
#undef MGB_RBO_WIDTH
#undef MGB_RBO_HEIGHT
+6 -293
View File
@@ -287,19 +287,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
EndViewportRoutingPasses(passCount);
}
// The backend twin table. Two arms live behind this one interface (ARCHITECTURE.md 9.6 -
// after Track H the MOBILEGL_PIPE_PUSH bitmap alone is not a valid A/B, because with a bit
// clear the backend would still be running the re-keyed code):
//
// legacy (MOBILEGL_PIPE_LEGACY_MEMOS): UnorderedMap<StateObject*, Entry> keyed on the
// frontend heap ADDRESS, with a weak_ptr per entry as the ABA defence, an erase
// inside Find, and a garbage sweep as the only death signal. Pre-P2 code verbatim.
// handles (MOBILEGL_PIPE_PUSH and kMGPipeSubsystemEsprytSlots): BackendSlotTable, keyed
// on MGPipeHandle{Slot, Gen}. See SlotTables.h for what that buys.
//
// Which arm runs is fixed once per process (EsprytSlotTablesEnabled()): the two arms hold
// their twins in different containers, so a mid-run flip would strand every twin already
// built. Every call site below this class is arm-agnostic and unchanged.
// The backend twin table: BackendSlotTable buckets keyed on MGPipeHandle{Slot, Gen} (see
// SlotTables.h for what that buys). P13 deleted the legacy arm - an UnorderedMap keyed on
// the frontend heap ADDRESS with a garbage sweep as its only death signal - together with
// every frontend-keyed entry (GetOrCreate(object), Find(object), HandleOf, the per-handle
// state note, the lifetime-id death walk): a twin is resolved only by the handle a record
// carried.
//
// The kind is a template parameter ONLY in the push build. G1 requires the pull build's
// symbol set to be byte-for-byte the pre-P2 one, and a third template argument changes
@@ -315,21 +308,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
class StateBackendObjectRegistry {
public:
using StatePtr = SharedPtr<StateObject>;
using StateWeakPtr = std::weak_ptr<StateObject>;
using BackendPtr = SharedPtr<BackendObject>;
// The backend twin and the weak reference that decides whether the raw key still
// names the state object the twin was built for. Both live in one entry: a
// separate liveness map answered nothing the backend probe had not already found
// and cost a second hash lookup on every Find, which the draw path runs ~10 times.
struct Entry {
BackendPtr backend;
StateWeakPtr stateRef;
};
using BackendMap = UnorderedMap<StateObject*, Entry>;
using iterator = typename BackendMap::iterator;
using const_iterator = typename BackendMap::const_iterator;
using SlotTable = BackendSlotTable<StateObject, BackendObject, kKind>;
// P14 S6: the ONE bucket of this kind the calling thread is in. Every handle-keyed entry
// below resolves through it, so the six twin registries answer per {session, share
@@ -337,117 +316,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
using SlotTables = SlotTableRegistry<StateObject, BackendObject, kKind>;
using TwinKey = DirectGLES::TwinKey;
BackendPtr& GetOrCreate(const StatePtr& stateObj) {
#if MOBILEGL_BUILD_DISAGGREGATED
// Check before arm selection: disabling slot tables must not expose the
// legacy raw-pointer registry on a transport apply thread.
MG_Pipe::MGPipeRefuseFrontendKeyedRegistryFromApplyThread("Registry.GetOrCreate(StatePtr)");
#endif
MOBILEGL_ASSERT(stateObj != nullptr, "State object must not be null");
if (EsprytSlotTablesEnabled()) {
// The slot table arms the teardown sentinel itself, at its own first
// insertion (D13; SlotTables.h) - so a table used outside a registry arms
// it too, which is right: it is the twin, not the registry, that owns the
// driver id a guarded destructor exists for.
return m_slotTables.ForCallingThread().GetOrCreate(stateObj);
}
// Twin creation is the moment a driver-owned id starts needing a guarded
// destructor; cold path, so the once-guard costs nothing per draw. It is armed
// here, at the first insertion - a destructor hook on the table itself is wrong
// for the reason spelled out above InProcessTeardown().
EnsureProcessTeardownSentinel();
// Sweep BEFORE the entry reference below exists: the map is open-addressed and an
// erase relocates the rest of the probe cluster, so collecting once that reference
// is taken would invalidate it. The sweep is therefore owed from an earlier call
// rather than triggered by this one.
if (m_creationTick >= kCreationGCInterval) {
m_creationTick = 0;
CollectGarbage();
}
const SizeT entryCountBeforeInsert = m_entries.size();
auto& entry = m_entries[stateObj.get()];
if (m_entries.size() != entryCountBeforeInsert) {
// A key the registry has never held. Nothing tells the backend that a texture or
// renderbuffer was DELETED - the twin, and the driver storage it owns, lives
// until a collection - and CollectGarbageIfNeeded is ticked only from the
// per-draw sync paths, which a CTS-shaped workload runs about ten times per
// case. 1024 of those ticks then span ~100 cases, so ~100 cases' worth of dead
// (and, for this suite, gigabyte-sized) objects stay allocated at once. Object
// CHURN rather than draw count is what makes the sweep urgent, so a twin the
// registry has never seen ticks it too - and it does so on the path that is
// about to allocate, which is exactly when the memory is needed.
++m_creationTick;
}
if (entry.stateRef.expired()) {
// The previous owner of this address is gone and the allocator handed it
// to a new object: its twin describes ids the new state object never made.
entry.backend.reset();
}
entry.stateRef = stateObj;
return entry.backend;
}
// Null when no live state object owns this key.
//
// On the HANDLE arm the result is a stable array element: only a GetOrCreate that grows
// the table can move it, and nothing else on the table invalidates it.
//
// On the LEGACY arm the result points into the map, so it stays valid only until the
// next GetOrCreate/Find/CollectGarbage on this registry. Take that literally, including
// for Find: the map is open-addressed and erases by shifting the rest of the probe
// cluster into the hole, so an erase relocates entries OTHER than the erased one - and
// Find erases, whenever it lands on a key whose state object has expired. Callers that
// need the twin across another registry call must copy the BackendPtr out (or keep only
// the pointee, which is heap-allocated and never moves).
BackendPtr* Find(StateObject* stateObj) {
#if MOBILEGL_BUILD_DISAGGREGATED
// Check before arm selection: disabling slot tables must not expose the
// legacy raw-pointer registry on a transport apply thread.
MG_Pipe::MGPipeRefuseFrontendKeyedRegistryFromApplyThread("Registry.Find(StateObject*)");
#endif
if (EsprytSlotTablesEnabled()) {
return m_slotTables.ForCallingThread().Find(stateObj);
}
const auto entryIt = m_entries.find(stateObj);
if (entryIt == m_entries.end()) {
return nullptr;
}
if (entryIt->second.stateRef.expired()) {
m_entries.erase(entryIt);
return nullptr;
}
return &entryIt->second.backend;
}
const BackendPtr* Find(StateObject* stateObj) const {
return const_cast<StateBackendObjectRegistry*>(this)->Find(stateObj);
}
iterator begin() {
#if MOBILEGL_BUILD_DISAGGREGATED
MG_Pipe::MGPipeRefuseFrontendKeyedRegistryFromApplyThread("Registry.begin");
#endif
return m_entries.begin();
}
const_iterator begin() const {
#if MOBILEGL_BUILD_DISAGGREGATED
MG_Pipe::MGPipeRefuseFrontendKeyedRegistryFromApplyThread("Registry.begin");
#endif
return m_entries.begin();
}
iterator end() { return m_entries.end(); }
const_iterator end() const { return m_entries.end(); }
// The {slot, gen} this object's twin is keyed on, or the null handle. This is what a
// backend memo stores instead of a raw pointer, a GL name or a bare lifetime id.
MG_Pipe::MGPipeHandle HandleOf(const StateObject* stateObj) const {
if (EsprytSlotTablesEnabled()) {
return m_slotTables.HandleOf(stateObj);
}
return MG_Pipe::kMGPipeNullHandle;
}
// The twin at a handle, or null when the slot is free or its Gen has moved on. This is
// the lookup a backend memo that already holds a handle wants: no lifetime-id probe.
BackendPtr* FindByHandle(MG_Pipe::MGPipeHandle handle) {
@@ -503,27 +371,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
return m_slotTables.LiveGenAt(slot);
}
// P5c (hd): the two halves of SlotTables.h's state note, forwarded. A caller holding
// both the record's handle and the frontend object (the record-driven sync) notes the
// object so a later handle-only resolution can reach it without the client allocator.
//
// P5e (id): under MOBILEGL_PIPE_PUSH rather than MOBILEGL_BUILD_DISAGGREGATED, because
// the re-typed ForEachLive's caller resolves its object through StateForHandle in the
// push-monolith build too. Each half is a named Fatal from an unbarriered apply
// (SlotTables.h); on the legacy arm there is no note and StateForHandle answers null,
// which is the answer the legacy walk's own weak_ptr test already gives.
void NoteStateForHandle(MG_Pipe::MGPipeHandle handle, const StatePtr& stateObj) {
if (EsprytSlotTablesEnabled()) {
m_slotTables.NoteStateForHandle(handle, stateObj);
}
}
StatePtr StateForHandle(MG_Pipe::MGPipeHandle handle) const {
if (EsprytSlotTablesEnabled()) {
return m_slotTables.StateForHandle(handle);
}
return nullptr;
}
// NO ReleaseByHandle HERE THROUGH P5, AND THAT WAS A DECISION (review M-4). The death
// half of GetOrCreateByHandle existed only for a kind whose announcement is its own
// destroy CALL rather than the shared death notice - which was the BUFFER family
@@ -549,24 +396,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
return false;
}
// P2 step e2. STATIC, because a death notice is about an object and not about a
// registry instance: it is answered by EVERY table of this kind that exists - this
// registry's own, and any by-value copy of it a fixture or a context reset is holding
// (SlotTables.h explains the holder list and why one holder was a leak).
//
// The legacy arm cannot answer this at all - its key is the frontend heap ADDRESS and
// the object is already gone by the time the notice arrives - so there it is a no-op
// and the garbage sweep stays its only death signal. That asymmetry is not an
// oversight: it is the A/B the compile-time arm exists to make measurable
// (ARCHITECTURE.md 9.6), and announced-versus-discovered death is one of the things
// being measured.
static Bool DestroyByLifetimeId(Uint64 lifetimeId) {
if (EsprytSlotTablesEnabled()) {
return SlotTable::OnFrontendObjectDestroyed(lifetimeId);
}
return false;
}
// P5e (id), CONTRACT-P5E §4.1: fn(MGPipeHandle, const BackendPtr& twin) over every live
// entry of the HANDLE ARM, and of that arm only.
//
@@ -628,61 +457,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (EsprytSlotTablesEnabled()) m_slotTables.ForEachBucket(fn);
}
// The seven DirectGLES.cpp call sites drive the LEGACY arm and nothing else. On the
// handle arm death is announced by the frontend object's destructor
// (MG_State/GLState/StateObjectDeathNotice.h), so there is no garbage to collect on a
// tick, the slot table has no collector to forward to, and this is the predicted
// branch plus a return - which is how ROADMAP.md:18's "delete the GC" is delivered
// without deleting the legacy arm's own collector while that arm is still compiled
// beside it.
void CollectGarbageIfNeeded() {
if (EsprytSlotTablesEnabled()) {
return;
}
}
// Pre-P2 API, kept for the legacy arm. On the handle arm there is nothing it could
// collect: a twin leaves with its object's death notice, and a notice dropped during
// process teardown is a deliberate leak (SlotTables.h), not garbage awaiting a call.
void CollectGarbageNow() {
if (EsprytSlotTablesEnabled()) {
return;
}
CollectGarbage();
}
private:
void CollectGarbage() {
if (m_isCollecting) {
return;
}
m_isCollecting = true;
Vector<StateObject*> staleKeys;
staleKeys.reserve(m_entries.size());
for (const auto& [stateKey, entry] : m_entries) {
if (entry.stateRef.expired()) {
staleKeys.push_back(stateKey);
}
}
for (auto* stateKey : staleKeys) {
m_entries.erase(stateKey);
}
m_isCollecting = false;
}
private:
static constexpr Uint32 kGCInterval = 1024;
// Creations are far rarer than draws, so this counts in a much smaller unit than
// kGCInterval does.
static constexpr Uint32 kCreationGCInterval = 64;
BackendMap m_entries;
Uint32 m_gcTick = 0;
Uint32 m_creationTick = 0;
Bool m_isCollecting = false;
// P14 S6: the ONE bucket per {session, share group}, hidden behind the same member names
// the single table had, so every handle-keyed call site above and every caller below is
// unchanged. See SlotTables.h's SlotTableRegistry for the resolution chain.
@@ -1194,15 +969,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
// to answer it differently.
Bool ResourceContentIsDeclared(MG_Pipe::MGPipeHandle res);
// MONOLITH GLUE, and named as such: the handle of a resource this backend is looking
// at through a frontend object, resolved through the client allocator's lifetime-id
// index. Every caller is a site P3a deliberately does NOT migrate - the SSBO / UBO /
// indirect / pack-PBO binding walks are dirty bits 15-17 and P4b's, and the index
// host mirror is P8's - so they still arrive holding the object. Under a real split
// neither the object nor its lifetime id exists on this side and every one of them
// has to receive the handle in a payload instead.
MG_Pipe::MGPipeHandle HandleOfBuffer(const MG_State::GLState::BufferObject* bufferObject);
// TIER 1 OF THE THREE-TIER FLUSH LADDER, AS A PURE FUNCTION (P5 b1).
//
// `GL_MAP_INVALIDATE_RANGE_BIT` is not a hint, it is an ASSERTION THAT THE OLD BYTES
@@ -1267,18 +1033,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
GLESBufferResource* EnsureBufferResourceForHandle(
const SharedPtr<MG_State::GLState::BufferObject>& bufferObject, MG_Pipe::MGPipeHandle res);
// P3a (D-D): "the GPU wrote through this resource", announced on the reverse channel
// instead of poked into the frontend object. ARCHITECTURE.md calls OnGpuWritten a
// NARROWING channel - the client builds its pending set conservatively at each
// draw/dispatch emission point and this callback only ever takes entries out of it -
// so in P3a, where the client's conservative set is exactly what the three
// MarkGpuWritten sites marked, the announced set is the whole resource and the
// observable behaviour is identical. P8/P9 narrow it; the channel is what they need.
//
// The legacy arm keeps calling BufferObject::MarkGpuWritten directly, and the pull
// build never sees this function at all (G1).
void MarkBufferGpuWritten(const SharedPtr<MG_State::GLState::BufferObject>& bufferObject);
// The applier's stored extent for this resource, 0 when it has no record. The one
// thing outside BufferImpl that needs it is the fp64 narrowing, whose source extent
// used to be BufferObject::GetSize().
@@ -1305,25 +1059,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
void OnServerSessionEndedWithOthersLive();
void OnBackendContextDestroyed();
// Get-or-create the backend resource and bring its storage up to date
// (creates the GL buffer, replays pending ops, pushes persistent-mapped
// ranges). Requires the ES context to be current. Returns nullptr only
// for null input.
GLESBufferResource* EnsureBufferResource(const SharedPtr<MG_State::GLState::BufferObject>& bufferObject);
// Existing resource or nullptr; performs no GL calls.
GLESBufferResource* GetBufferResource(MG_State::GLState::BufferObject* bufferObject);
// True when EnsureBufferResource(frontend) would provably fall straight through
// every branch and do no work — i.e. `resource` is still the frontend's own
// resource, its id belongs to the live ES context, and either it is the
// zero-copy coherent persistent store (draw-time sync is a no-op by design) or
// the storage is initialized at the right size with no pending ops and a synced
// change serial while the buffer is not mapped (an active map may owe a
// per-draw persistent-range push, so it always takes the full path).
// `frontend` must be non-null and alive; the caller guarantees that by holding
// (or shadowing something that holds) a SharedPtr to it. Enables the per-VAO
// resolved-buffers memo to skip EnsureBufferResource on clean static buffers.
Bool IsBufferDrawClean(const MG_State::GLState::BufferObject* frontend, const GLESBufferResource* resource);
// Deletes GL buffers whose owning frontend objects died (possibly on a
// thread without a current ES context). Called from draw-time sync.
void ProcessDeferredBufferReleases();
@@ -1888,7 +1625,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
// attachment sync and the image sweep both call it, which is why it is declared
// once here rather than twice in two packages' worktrees.
void SyncMipmapsToBackendByHandle(MG_Pipe::MGPipeHandle texture);
void SyncTextureViewToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
void StampViewSyncKeys(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
// P5e (tx2). THE VIEW ARM UNDER A TRANSPORT, and what it can and cannot answer.
//
@@ -1911,12 +1647,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
// parameter and sampler halves are unchanged and run on this name as on any other.
void SyncBuiltinSamplerToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
void SyncTextureParamsToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
// Marks the texture as one whose ES storage has to be image-bindable, which for a
// non-core image format means re-minting it in the widening's carrier. Takes the state
// object because the levels already uploaded have to be marked dirty again: the
// re-mint allocates fresh storage and only replays what the shadow still calls dirty.
void RequireImageBindableStorage(
const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
// Server-owned promotion: preserve an already image-bindable native
// allocation, or capture the old native contents, merge pending
// upload regions and re-arm the server's whole-level replay set.
@@ -2252,14 +1982,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
// table's answer is an array element and only a GetOrCreate that GROWS the table moves
// it, which this function has already done by the time it returns.
BackendTextureObject* ResolveTextureTwin(MG_Pipe::MGPipeHandle res);
SharedPtr<BackendTextureObject>& SyncTextureObjectToBackend(
const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
Bool imageBindableStorageRequired = false);
// Every path that lets the DRIVER write a texture's texels calls this (see
// BackendTextureObject::m_driverMayHoldGpuWrites): an application framebuffer attachment,
// a writable image binding, a copy, blit or mipmap generation into it. A view writes its
// storage texture's image, so the storage texture is the one marked.
void NoteDriverSideTextureWrite(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject);
// ---- P5e SEAM (MG_Remote/CONTRACT-P5E.md §4.2, §5.2; declared by c0e, bodied by
// id/tx2) --------------------------------------------------------------------------
//
@@ -3490,14 +3212,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
BackendSamplerViewObject* FindSamplerViewForHandle(MG_Pipe::MGPipeHandle view);
// MONOLITH GLUE, and named as such, the HandleOfBuffer shape: the SamplerViewCso handle
// of a texture this backend is looking at through a frontend object. Legal only because
// the view is minted off the texture's own lifetime id; under a real split neither the
// object nor its lifetime id exists on this side and the handle has to arrive in the
// payload (which, for every path P4a switches over, it does - this is for the paths
// P3b/P4b still owns).
MG_Pipe::MGPipeHandle HandleOfSamplerViewForTexture(
const MG_State::GLState::ITextureObject* textureObject);
} // namespace SamplerViewImpl
namespace RenderbufferImpl {
@@ -3509,7 +3223,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
~BackendRenderbufferObject();
BackendRenderbufferObject(const BackendRenderbufferObject&) = delete;
BackendRenderbufferObject& operator=(const BackendRenderbufferObject&) = delete;
void SyncToBackend(const SharedPtr<MG_State::GLState::RenderbufferObject>& stateRBOObject);
// P5e SEAM (declared by c0e, bodied by fb): the renderbuffer twin of the
// framebuffer overload above. MGPSurface::Res names the renderbuffer and the
// resource record already carries its format, extent and sample count (P4a), so the
+35 -324
View File
@@ -17,7 +17,6 @@
// translation units that include it without copying Managers.h's include order.
#include "DirectGLES.h"
#include <MG_Impl/Pipe/SlotAllocator.h>
#if MOBILEGL_BUILD_DISAGGREGATED
#include <MG_Pipe/PipeSessionFail.h>
#endif
@@ -43,63 +42,32 @@
// Death is ANNOUNCED, and that is what lets this table have no garbage collector - the
// deliverable ROADMAP.md:18 spells "GC" in and the one D13 makes a precondition of the switch-
// over. All six re-keyed object classes raise MG_State::GLState::NotifyStateObjectDestroyed()
// from their destructor (BufferBackendOps' shape, one entry point for six kinds), the backend
// consumes it in Managers.cpp, and OnFrontendObjectDestroyed() below drops the twin in EVERY
// table of the kind and returns the slot, at the moment the frontend object's last SharedPtr
// goes. So:
// * there is NO draw-path tick, NO creation tick and NO sweep of any kind on this arm. The
// seven CollectGarbageIfNeeded call sites in DirectGLES.cpp drive the LEGACY registry only;
// from their destructor carrying {kind, lifetimeId, handle}; the backend consumes it in
// Managers.cpp and releases the twin BY THAT HANDLE in every holder of the kind
// (ReleaseTwinByHandle below - the same release the wire's object_death runs on a server), and
// the CLIENT returns the slot after the notice (PipeFill.cpp's NotifyAndFree). So:
// * there is NO draw-path tick, NO creation tick and NO sweep of any kind;
// * a twin, and the driver storage it owns, is freed when the application lets go of the
// object rather than up to 64 creations or 1024 draw ticks later. That is what
// Managers.h's "dead gigabytes" note asked for.
//
// EVERY HOLDER OF THE KIND, not one. Two live tables of one kind is a real configuration - the
// ScopedDirectGLESTextureBindings fixture keeps a by-value copy of the Texture registry for the
// length of a test, and a context reset does the same in reverse - and the slot allocator
// erases its lifetimeId -> slot mapping on Free, so a notice delivered to one holder and
// resolved again by the next would find nothing to resolve. Every table therefore links itself
// into a per-table-type list at construction and out at destruction, and one notice resolves
// the handle ONCE, drops the twin in each holder BY HANDLE, and frees the slot once, last. No
// holder can be left naming a live entry for a dead object, and there is nothing a sweep could
// still find. (The list is per table TYPE; the kind is the type's template parameter, and each
// of the six kinds has exactly one table type in this backend. Magma's subsystem-4 table mints
// out of its own per-renderer allocator, not MGPipeSlots(), so it is not a holder here.)
// length of a test, and a context reset does the same in reverse. Every table therefore links
// itself into a per-table-type list at construction and out at destruction, and one release
// drops the twin in each holder by handle. (The list is per table TYPE; the kind is the type's
// template parameter, and each of the six kinds has exactly one table type in this backend.)
//
// The weak_ptr per entry survives as the MONOLITH-GLUE half of the table's identity: the
// minting GetOrCreate stores it, NoteStateForHandle/StateForHandle read it, and the legacy
// death notice walks it. It is never an identity test - that is what Gen is for - and it is
// never read to decide whether an entry is dead: a destructor that runs after exit() has begun
// has its notice dropped by InProcessTeardown(), and that twin is then a DELIBERATE leak (the
// process is exiting, the driver reclaims the object, and a twin destructor must not call into
// a driver that may already be unloaded), not something to be collected later.
// A destructor that runs after exit() has begun has its notice dropped by InProcessTeardown(),
// and that twin is then a DELIBERATE leak (the process is exiting, the driver reclaims the
// object, and a twin destructor must not call into a driver that may already be unloaded).
//
// P5e (id, CONTRACT-P5E §4.1): ForEachLive() NO LONGER HANDS IT OUT. It answers
// fn(MGPipeHandle, const BackendPtr&), because a walk that produced a frontend SharedPtr out
// of server memory was the last place the server could hold one across records - and a caller
// that still needs the object (the detach walk) asks StateForHandle for it BY HANDLE, inside
// the scope that names the debt, so the read is one greppable site rather than a property of
// the iteration.
//
// P3+ DEBT, recorded rather than hidden: this header is under MG_Backend/ and it MINTS
// handles (MGPipeSlots().Acquire below) off a frontend SharedPtr's GetLifetimeId().
// MGPipeHandles.h:13-16 says a handle is minted by the CLIENT and never by the server, and
// under a real split neither the frontend object nor its lifetime id exists on this side of
// the wire. This is monolith glue: the minting and the lifetimeId -> handle resolution both
// belong on the client, and the backend should receive the handle in the verb payload. It is
// NOT part of "Track H done" and check_include_closure.py does not probe MG_Backend headers,
// so nothing catches it automatically.
//
// P5c (hd, CONTRACT-P5C §3.1) is what gives the debt teeth: with an active transport the
// minting GetOrCreate, HandleOf and OnFrontendObjectDestroyed raise
// Fatal{RoleViolation, "MGPipeSlots"} when reached from the apply thread (the same check the
// allocator's own three entries carry, repeated here so the refusal names this surface), and
// the split paths resolve through GetOrCreate(handle) / ReleaseByHandle instead.
//
// P5f (fr): the frontend-keyed half survives only as MONOLITH GLUE. HandleOf,
// GetOrCreate(StatePtr), NoteStateForHandle and StateForHandle refuse EVERY transport
// apply before touching a frontend object, with or without a barrier or named scope.
// Find(StateObject*) delegates to HandleOf; handle-only iteration never reads stateRef.
// The registry guard is independent of Magma's legacy allocator debt scope.
// P13: THE TABLE IS HANDLE-ONLY. The frontend-keyed half - a GetOrCreate that minted handles
// off a frontend object's lifetime id from inside MG_Backend, the object-keyed Find / HandleOf
// with their lifetime-id memo, the weak per-entry state note and the lifetime-id death walk -
// was monolith glue, unreachable once every family ran the record arm, and is deleted. A handle
// is minted by the CLIENT and arrives in the record; nothing on this side touches the client's
// slot allocator (the link ratchet's P13 exit gate pins that).
namespace MobileGL::MG_Backend::DirectGLES {
@@ -148,7 +116,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// True when this process runs the {slot, gen} arm. Fixed for the life of the process: the
// two arms hold their twins in different containers, so flipping mid-run would strand them.
//
// INLINE on purpose. Every Find / GetOrCreate / HandleOf / ForEachLive on the twin tables
// INLINE on purpose. Every FindByHandle / GetOrCreate / ForEachLive on the twin tables
// consults it, i.e. it is on the per-draw path several times per draw. As an out-of-line
// function in Managers.cpp (no LTO in any shipped configuration) that was a call through
// the PLT per lookup; here the caller sees a guard-variable load and a perfectly-predicted
@@ -181,7 +149,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// are two clients, and nothing may be shared with either from the other.
//
// THE HOLDER LIST STAYS PROCESS-WIDE AND THAT IS NOT A COMPROMISE. A notice is about an
// OBJECT, not about a bucket: OnFrontendObjectDestroyed and ReleaseTwinByHandle must reach
// OBJECT, not about a bucket: ReleaseTwinByHandle must reach
// the by-value copies a fixture or a context reset is holding, whichever bucket they sit in,
// and "which bucket holds it" is exactly the question that produced the two-holder leak the
// list exists to fix. Each SHARED holder of the list is ONE bucket; the walk therefore covers
@@ -328,15 +296,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
return Shared(key)->GetOrCreate(handle);
}
Uint32 LiveGenAt(Uint32 slot) const { return ForCallingThread().LiveGenAt(slot); }
MG_Pipe::MGPipeHandle HandleOf(const StateObject* stateObj) const {
return ForCallingThread().HandleOf(stateObj);
}
void NoteStateForHandle(MG_Pipe::MGPipeHandle handle, const StatePtr& stateObj) {
ForCallingThread().NoteStateForHandle(handle, stateObj);
}
StatePtr StateForHandle(MG_Pipe::MGPipeHandle handle) const {
return ForCallingThread().StateForHandle(handle);
}
template <typename Fn>
void ForEachLive(Fn&& fn) const {
// THE CALLING THREAD'S BUCKET, not every bucket, and that is the semantic answer
@@ -361,9 +320,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
static Bool ReleaseTwinByHandle(MG_Pipe::MGPipeHandle handle) {
return Table::ReleaseTwinByHandle(handle);
}
static Bool OnFrontendObjectDestroyed(Uint64 lifetimeId) {
return Table::OnFrontendObjectDestroyed(lifetimeId);
}
// The instance spelling, and it HANDS THE TWIN OUT rather than answering a Bool: the two
// tables that ARE a bucket registry (buffers, sampler views) have always been called as
// `g_backendBufferResources.ReleaseByHandle(res)` and their callers still have to decide
@@ -405,8 +361,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
template <typename StateObject, typename BackendObject, MG_Pipe::MGPipeKind kKind>
class BackendSlotTable {
public:
using StatePtr = SharedPtr<StateObject>;
using StateWeakPtr = std::weak_ptr<StateObject>;
using BackendPtr = SharedPtr<BackendObject>;
// The largest slot index this table will grow to for a handle that ARRIVED in a call's
@@ -436,14 +390,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
struct Entry {
BackendPtr backend;
// THE MONOLITH-GLUE HALF of the entry (P5e, CONTRACT-P5E §4.1): written by the
// minting GetOrCreate and by NoteStateForHandle, read only by StateForHandle and
// the legacy death notice. Never compared against another object to decide identity
// - that is what Gen is for - never dereferenced for its address, and never read to
// decide whether the slot is dead: death is announced, not discovered. ForEachLive
// stopped reading it at P5e: a walk that handed a frontend SharedPtr out of server
// memory on every step was the last place the server could hold one across records.
StateWeakPtr stateRef;
// The generation this entry's twin was built for. An entry whose Gen no longer
// matches the allocator's is a twin of the slot's PREVIOUS owner.
Uint32 Gen = 0;
@@ -453,27 +399,22 @@ namespace MobileGL::MG_Backend::DirectGLES {
// Every constructor links the table into the per-type holder list and the destructor
// unlinks it, so a by-value copy (the ScopedDirectGLESTextureBindings fixture's saved
// registry) is a holder for exactly as long as it exists. Copy and move carry the
// ENTRIES and the memo; the links are the table's own and are never copied.
// ENTRIES; the links are the table's own and are never copied.
BackendSlotTable() { LinkHolder(); }
BackendSlotTable(const BackendSlotTable& other):
m_ownerSession(other.m_ownerSession),
m_slots(other.m_slots),
m_band(other.m_band),
m_nullTwin(other.m_nullTwin),
m_memoLifetimeId(other.m_memoLifetimeId),
m_memoHandle(other.m_memoHandle) {
m_nullTwin(other.m_nullTwin) {
LinkHolder();
}
BackendSlotTable(BackendSlotTable&& other) noexcept:
m_ownerSession(other.m_ownerSession),
m_slots(std::move(other.m_slots)),
m_band(std::move(other.m_band)),
m_nullTwin(std::move(other.m_nullTwin)),
m_memoLifetimeId(other.m_memoLifetimeId),
m_memoHandle(other.m_memoHandle) {
m_nullTwin(std::move(other.m_nullTwin)) {
other.m_slots.clear();
other.m_band.clear();
other.ForgetHandle();
LinkHolder();
}
BackendSlotTable& operator=(const BackendSlotTable& other) {
@@ -481,8 +422,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
m_slots = other.m_slots;
m_band = other.m_band;
m_nullTwin = other.m_nullTwin;
m_memoLifetimeId = other.m_memoLifetimeId;
m_memoHandle = other.m_memoHandle;
}
return *this;
}
@@ -491,11 +430,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
m_slots = std::move(other.m_slots);
m_band = std::move(other.m_band);
m_nullTwin = std::move(other.m_nullTwin);
m_memoLifetimeId = other.m_memoLifetimeId;
m_memoHandle = other.m_memoHandle;
other.m_slots.clear();
other.m_band.clear();
other.ForgetHandle();
}
return *this;
}
@@ -507,88 +443,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
void SetOwnerSession(Uint64 session) { m_ownerSession = session; }
Uint64 OwnerSession() const { return m_ownerSession; }
// Resolve-or-create. The handle comes from the client allocator keyed on the frontend
// object's lifetime id, so two calls for the same live object always land on the same
// slot, and a successor object at the same heap address never does.
BackendPtr& GetOrCreate(const StatePtr& stateObj) {
// No assert on null here, unlike the map arm: null is TOLERATED, so a DEBUG build
// must not trap where the release build quietly does the documented thing.
if (stateObj == nullptr) {
// The registry this replaces inserted a null KEY and handed back that entry's
// twin (DirectGLES.cpp's SyncTextureObjectToBackend documents relying on
// exactly that tolerance), so a release build never dereferenced null here.
// Keep the shape exactly, INCLUDING across calls: the map kept its null-keyed
// entry, so a second null call was handed the same twin the first one got.
// Resetting here instead would have destroyed it - an arm difference in the one
// path that documents relying on this. One per-table parking slot, never live,
// never handed a handle, because a null object has no identity and
// therefore cannot have a {slot, gen}.
return m_nullTwin;
}
// D13: the teardown sentinel is armed by the slot table's first insertion. Twin
// creation is the moment a driver-owned id starts needing a guarded destructor;
// this is the cold path, so the once-guard costs nothing per draw. On the legacy
// arm StateBackendObjectRegistry::GetOrCreate arms it itself.
EnsureProcessTeardownSentinel();
#if MOBILEGL_BUILD_DISAGGREGATED
// This overload mints from frontend identity and is monolith-only. Refuse
// before either the client object or allocator is touched, even when a
// legacy named scope is open and the current record is barriered.
MG_Pipe::MGPipeRefuseFrontendKeyedRegistryFromApplyThread("GetOrCreate(StatePtr)");
#endif
const MG_Pipe::MGPipeHandle handle =
MG_Pipe::MGPipeSlots().Acquire(kKind, stateObj->GetLifetimeId());
MOBILEGL_ASSERT(!MG_Pipe::MGPipeHandleIsNull(handle),
"MGPipe slot space of kind %u is exhausted",
static_cast<Uint32>(kKind));
Entry& entry = EntryAt(handle.Slot);
if (entry.Live && entry.Gen != handle.Gen) {
// The slot was reclaimed and handed to a new object: the twin at it describes
// driver ids the new state object never made.
entry.backend.reset();
}
entry.Gen = handle.Gen;
entry.Live = true;
entry.stateRef = stateObj;
// No creation tick and no sweep here. The registry this replaces needed both,
// because nothing told it a texture or a renderbuffer had been DELETED and object
// CHURN rather than draw count is what made that urgent. Every one of the six kinds
// now announces its own death from its destructor, so a dead twin's slot is already
// back before the next creation asks for one.
RememberHandle(stateObj->GetLifetimeId(), handle);
return entry.backend;
}
// P3a: resolve-or-create BY HANDLE, and it is the shape that discharges the debt this
// header records against itself at the top of the file.
// P3a: resolve-or-create BY HANDLE - since P13 the only resolve-or-create there is. The
// handle ARRIVED, in the call's payload, already minted by the client; this never
// touches the allocator. It indexes the slot, notices a generation that no longer
// matches (the slot was recycled, so the twin at it describes driver ids the new
// resource never made) and hands back the twin pointer.
//
// The overload above mints - it calls MGPipeSlots().Acquire off a frontend object's
// lifetime id, from inside MG_Backend - which is monolith glue: a handle is minted by
// the CLIENT, and under a real split neither the object nor its lifetime id exists on
// this side. This overload never touches the allocator at all. The handle ARRIVED, in
// the call's payload, already minted by the side that owns minting; all this does is
// index the slot, notice a generation that no longer matches (the slot was recycled,
// so the twin at it describes driver ids the new resource never made) and hand back
// the twin pointer. FindByHandle beside it is the same shape and already existed.
// Death stays ANNOUNCED: the family's own destroy call or the shared death notice
// releases the twin by handle, and the slot is freed by the CLIENT after that returns.
//
// No StatePtr, therefore no Entry::stateRef unless a caller that holds the object notes
// it (NoteStateForHandle): a handle-keyed entry has no frontend object to weakly hold
// by itself. P5e (§4.1) makes that stop mattering for the iteration - ForEachLive keys
// on Live && backend, so such an entry is VISIBLE to the walk and it is StateForHandle,
// asked by the one caller that needs the object, that answers null for it. Which is the
// same set the old stateRef-locking walk produced, decided at one site instead of in
// the loop.
//
// Death stays ANNOUNCED, as it is on the other overload: for a handle-keyed kind the
// announcement is the family's own destroy call, not the shared death notice, and the
// slot is freed by the CLIENT after that call returns.
//
// UNUSED AT THE CONTRACT COMMIT, deliberately: it is a member of a class template, so
// an uninstantiated one costs nothing anywhere, and the backend package is what gives
// it its first caller.
BackendPtr& GetOrCreate(MG_Pipe::MGPipeHandle handle) {
MOBILEGL_ASSERT(!MG_Pipe::MGPipeHandleIsNull(handle),
"GetOrCreate(handle) named the reserved null handle");
@@ -618,15 +481,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
#endif
}
// Same arming as the minting overload, and for the same reason: twin creation is
// the moment a driver-owned id starts needing a guarded destructor.
// Twin creation is the moment a driver-owned id starts needing a guarded destructor.
EnsureProcessTeardownSentinel();
// THE TWO DIRECTIONS ARE NOT SYMMETRIC HERE, where they are on the minting overload.
// There the handle comes straight out of MGPipeSlots().Acquire and can never be
// BEHIND the entry, so a bare `!=` only ever means "the slot was recycled forward".
// Here the handle arrived in a payload, so `handle.Gen < entry.Gen` is a reachable
// input, and adopting it would destroy the INCUMBENT LIVE twin - a driver buffer id,
// THE TWO DIRECTIONS ARE NOT SYMMETRIC. The handle arrived in a payload, so
// `handle.Gen < entry.Gen` is a reachable input, and adopting it would destroy the INCUMBENT LIVE twin - a driver buffer id,
// a persistent map, a pooled store, released by a defaulted destructor that issues
// no glDeleteBuffers and no pool enrolment - and then stamp the slot back to the
// dead resource's generation, after which the incumbent's own FindByHandle refuses
@@ -668,31 +527,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
return entry->Live ? entry->Gen : 0;
}
// Remember/read the frontend object last used by the monolith sync path.
// Handles supplied to the server never need this weak state. Neither method
// touches the allocator, so each carries its own unconditional apply guard.
void NoteStateForHandle(MG_Pipe::MGPipeHandle handle, const StatePtr& stateObj) {
#if MOBILEGL_BUILD_DISAGGREGATED
MG_Pipe::MGPipeRefuseFrontendKeyedRegistryFromApplyThread("NoteStateForHandle");
#endif
if (MG_Pipe::MGPipeHandleIsNull(handle)) return;
Entry* const entry = EntryOrNull(handle.Slot);
if (entry == nullptr || !entry->Live || entry->Gen != handle.Gen) return;
entry->stateRef = stateObj;
}
// The frontend object noted for this handle, or null. The handle answers identity;
// this answers only "which object was this twin last synced from".
StatePtr StateForHandle(MG_Pipe::MGPipeHandle handle) const {
#if MOBILEGL_BUILD_DISAGGREGATED
MG_Pipe::MGPipeRefuseFrontendKeyedRegistryFromApplyThread("StateForHandle");
#endif
if (MG_Pipe::MGPipeHandleIsNull(handle)) return nullptr;
const Entry* const entry = EntryOrNull(handle.Slot);
if (entry == nullptr || !entry->Live || entry->Gen != handle.Gen) return nullptr;
return entry->stateRef.lock();
}
// P3a: the death half of the overload above, for a kind whose announcement is its own
// destroy CALL rather than the shared death notice (D-L). Hands the twin OUT rather
// than destroying it in place, because the caller may still have to decide what
@@ -708,29 +542,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
// left alone: the successor's own GetOrCreate resets it.
BackendPtr ReleaseByHandle(MG_Pipe::MGPipeHandle handle) {
if (MG_Pipe::MGPipeHandleIsNull(handle)) return BackendPtr{};
if (m_memoHandle.Slot == handle.Slot) ForgetHandle();
Entry* const entry = EntryOrNull(handle.Slot);
if (entry == nullptr || !entry->Live || entry->Gen != handle.Gen) return BackendPtr{};
BackendPtr dead = std::move(entry->backend);
entry->backend.reset();
entry->stateRef.reset();
entry->Live = false;
return dead;
}
// Null when no live twin of this object exists. Unlike the registry's Find this NEVER
// mutates the table, so the returned pointer survives any later Find on it; only a
// GetOrCreate that grows the vector can move it, and callers that hold one across a
// possible insertion still copy the BackendPtr out.
BackendPtr* Find(StateObject* stateObj) {
if (stateObj == nullptr) return nullptr;
return FindByHandle(HandleOf(stateObj));
}
const BackendPtr* Find(StateObject* stateObj) const {
return const_cast<BackendSlotTable*>(this)->Find(stateObj);
}
BackendPtr* FindByHandle(MG_Pipe::MGPipeHandle handle) {
if (MG_Pipe::MGPipeHandleIsNull(handle)) return nullptr;
Entry* const entry = EntryOrNull(handle.Slot);
@@ -738,65 +557,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
return &entry->backend;
}
// The handle this object's twin is keyed on, or the null handle. This is what a backend
// memo stores instead of a raw pointer, a GL name or a bare lifetime id.
//
// A NULL answer is never memoised. The memo is per table and the allocator is per
// kind, so with two holders of one kind the OTHER table can be the one that acquires;
// a cached "no handle" here would then outlive the twin's creation over there, and
// nothing on this table's own acquire path would ever refresh it. A miss costs the
// allocator probe it always cost; a hit is refreshed the moment anyone acquires.
MG_Pipe::MGPipeHandle HandleOf(const StateObject* stateObj) const {
if (stateObj == nullptr) return MG_Pipe::kMGPipeNullHandle;
#if MOBILEGL_BUILD_DISAGGREGATED
// Resolve frontend identity only in the monolith path. A cache hit is
// subject to the same guard as a fresh allocator lookup.
MG_Pipe::MGPipeRefuseFrontendKeyedRegistryFromApplyThread("HandleOf");
#endif
const Uint64 lifetimeId = stateObj->GetLifetimeId();
if (lifetimeId == m_memoLifetimeId) return m_memoHandle;
const MG_Pipe::MGPipeHandle handle =
MG_Pipe::MGPipeSlots().FindByLifetimeId(kKind, lifetimeId);
if (!MG_Pipe::MGPipeHandleIsNull(handle)) RememberHandle(lifetimeId, handle);
return handle;
}
// P2 step e2's backend half. The frontend object with this lifetime id has just been
// DESTROYED: resolve its handle ONCE, drop its twin in EVERY table of this type, and
// return the slot to the allocator - in that order, because the allocator forgets the
// lifetime id on Free and a holder told second could no longer resolve it.
//
// The slot is returned whether or not any holder still had a twin at it: the lifetime
// id is dead and MG_State never hands one out twice, so nothing can acquire it again,
// and a slot minted for it that no table holds (a table reset with `= {}` drops its
// entries without freeing) would otherwise stay allocated for the life of the process.
//
// STATIC, and deliberately so: a notice is about an object, not about a table, and
// "which table holds it" is exactly the question that produced the two-holder leak.
// Returns whether the object had a slot of this kind, i.e. whether anything was freed;
// a second call for the same id answers false because the allocator no longer maps it.
static Bool OnFrontendObjectDestroyed(Uint64 lifetimeId) {
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (hd): the Find/Free pair below is monolith-only with an active transport -
// a frontend death is announced to the server by the object_death record (ct),
// and a direct call from the apply thread is Fatal{RoleViolation, "MGPipeSlots"}.
MG_Pipe::MGPipeRefuseAllocatorFromApplyThread("OnFrontendObjectDestroyed");
#endif
const MG_Pipe::MGPipeHandle handle =
MG_Pipe::MGPipeSlots().FindByLifetimeId(kKind, lifetimeId);
if (MG_Pipe::MGPipeHandleIsNull(handle)) return false;
for (BackendSlotTable* holder = s_firstHolder; holder != nullptr;) {
// The successor is read BEFORE the release: ReleaseTwinAt runs the twin's
// destructor, which is a driver call, and nothing that outlives it may be a
// reference into this holder.
BackendSlotTable* const next = holder->m_nextHolder;
holder->ReleaseTwinAt(handle);
holder = next;
}
MG_Pipe::MGPipeSlots().Free(kKind, handle);
return true;
}
// P5c (ct), CONTRACT-P5C.md §5.2: the handle-keyed half of the above, for a death that
// arrived AS A WIRE RECORD (object_death) rather than as the shared notice. The
// handle IS the resolution - it crossed in the record's payload - so the client's
@@ -848,14 +608,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
// it must not have - and replaces P3a's own fn(StatePtr, BackendPtr), which handed a
// frontend SharedPtr OUT OF SERVER MEMORY on every step.
//
// WHAT THE CALLER LOST AND HOW IT GETS IT BACK, because the two are not the same thing.
// The walk no longer locks Entry::stateRef, so a caller that needs the frontend object
// asks StateForHandle(handle) for it - which is the same weak reference, read at one
// named site inside the scope that owns the debt, instead of at every step of an
// iteration. Coverage is UNCHANGED by construction: an entry ForEachLive used to skip
// because its stateRef did not lock is one whose StateForHandle answers null, and the
// caller skips it there instead.
//
// The handle is the entry's own {slot, gen}, band-aware: a composite ShaderCso's slot
// is reported as kMGPipeShaderCsoCompositeSlotBase + its band index, i.e. the slot the
// client minted, never the index into m_band.
@@ -895,7 +647,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
Vector<Entry> slots = std::move(m_slots);
Vector<Entry> band = std::move(m_band);
BackendPtr nullTwin = std::move(m_nullTwin);
ForgetHandle();
slots.clear();
band.clear();
nullTwin.reset();
@@ -931,13 +682,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
private:
// Drop the twin at `handle` if THIS table holds it. Frees nothing: the slot belongs to
// the kind, not to the table, and OnFrontendObjectDestroyed returns it once, after
// every holder has let go.
// the kind, not to the table, and the client returns it once, after the release.
Bool ReleaseTwinAt(MG_Pipe::MGPipeHandle handle) {
// Forget the memo whenever it names this slot, even if this table has no entry
// there: a memo can be a handle learned from the allocator for an object another
// holder twinned, and it must not survive the slot's next handout.
if (m_memoHandle.Slot == handle.Slot) ForgetHandle();
// The twin's destructor is a driver call and could, in principle, re-enter
// GetOrCreate on this table and resize m_slots. So NOTHING that outlives the
// destructor may be a reference into m_slots: the twin is moved out into a local,
@@ -948,7 +694,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (entry == nullptr || !entry->Live || entry->Gen != handle.Gen) return false;
dead = std::move(entry->backend);
entry->backend.reset();
entry->stateRef.reset();
entry->Live = false;
}
dead.reset();
@@ -983,8 +728,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
return const_cast<BackendSlotTable*>(this)->EntryOrNull(slot);
}
// Grows the right space to hold `slot`. Every caller bounds `slot` first - the minting
// overload because the allocator produced it, the handle overload against
// Grows the right space to hold `slot`. Every caller bounds `slot` first - against
// kMaxHandleSlot - because this is the one place a client-supplied number decides an
// allocation size. A composite slot grows m_band by (slot - base) + 1, so the ordinary
// table never learns the band exists.
@@ -995,15 +739,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
return table[index];
}
void RememberHandle(Uint64 lifetimeId, MG_Pipe::MGPipeHandle handle) const {
m_memoLifetimeId = lifetimeId;
m_memoHandle = handle;
}
void ForgetHandle() const {
m_memoLifetimeId = 0;
m_memoHandle = MG_Pipe::kMGPipeNullHandle;
}
// The holder list: intrusive and doubly linked, so registering and unregistering are
// two pointer writes with no allocation, and its head is a constant-initialised
// static - which is what lets the process-lifetime registry globals in Managers.cpp
@@ -1041,30 +776,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
// Handed back by GetOrCreate for a null state object. Never live, never handed a handle.
BackendPtr m_nullTwin;
// ONE-entry resolution memo, lifetimeId -> handle. It exists because without it every
// resolution goes through the allocator's ByLifetimeId hash, which the deleted
// TwinLookupMemos existed to avoid and which D13 promises to replace with "direct slot
// indexing".
//
// It is one entry and therefore only helps a caller that asks for the SAME object twice
// running - ResolveVaoTwin and SyncCurrentProgram do, once per draw each. Two callers
// it does NOT help, recorded rather than claimed away: BindCurrentFBO resolves BOTH
// targets in a frame, and ResolveUnitSamplerBackend asks for a different sampler per
// texture unit, so both thrash a single-entry memo and pay the probe P1 did not (P1 had
// a per-unit memo and a direct-mapped 6-slot array there). Making the memo per-unit /
// per-target is the fix, and G11 - the device-side gate that would price it - is owed.
//
// It cannot serve a stale answer, by three independent arguments:
// * the key is a lifetime id, which MG_State never hands out twice, so a recycled
// heap address cannot hit this memo the way it could hit an address-keyed one;
// * a null answer is never stored, so another holder's acquire cannot be hidden by
// a "no handle" this table remembered earlier; and
// * even a hit for a slot that has since been freed and re-handed is caught, because
// the caller resolves the handle through FindByHandle, which compares Gen.
// Cleared anyway when a death notice names the memoised slot. 0 is never a live
// lifetime id (MG_State's counters start at 1), so a zeroed memo is a guaranteed miss.
mutable Uint64 m_memoLifetimeId = 0;
mutable MG_Pipe::MGPipeHandle m_memoHandle = MG_Pipe::kMGPipeNullHandle;
};
} // namespace MobileGL::MG_Backend::DirectGLES
@@ -13,6 +13,7 @@
#include "MG_State/GLState/FramebufferState/FramebufferObject.h"
#include "MG_State/GLState/Core.h"
#include <MG_Backend/MGPipe/PipeInputs.h>
#include <MG_Pipe/PipeApply.h>
#include "MG_State/GLState/TextureState/TextureState.h"
#include "MG_Util/Classifiers/TextureEnumClassifier.h"
#include "MG_Util/Converters/MGToGL/TextureEnumConverter.h"
@@ -453,9 +454,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
UpdateDynamicBackendParameters();
UpdateAdvertisedExtensions();
if (MG_Pipe::gPipeInputs.IsLive()) {
MG_Pipe::gPipeInputs.InvalidateCompileEnv();
}
MG_Pipe::MGPipeNotifyCompileEnvChanged();
PopulateFormatCapabilities(physicalDevice.handle, vkGetPhysicalDeviceFormatProperties, m_vulkanCaps,
MutableFormatCapabilities());
PrintFormatCapabilities(GetFormatCapabilities());
@@ -1062,9 +1061,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_vulkanCaps = capabilities;
UpdateDynamicBackendParameters();
UpdateAdvertisedExtensions();
if (MG_Pipe::gPipeInputs.IsLive()) {
MG_Pipe::gPipeInputs.InvalidateCompileEnv();
}
MG_Pipe::MGPipeNotifyCompileEnvChanged();
MutableFormatCapabilities().Clear();
}
@@ -87,7 +87,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// What leaked was the SERVER's twin: a framebuffer has NO wire delete opcode at all
// (BRIEF-P4A D-I2), so `object_death` is its only death delivery, and without a
// consumer here the server's record stayed Live for the life of the session.
void OnMagmaFrontendStateObjectDestroyed(MG_Pipe::MGPipeKind kind, Uint64 lifetimeId) {
void OnMagmaFrontendStateObjectDestroyed(MG_Pipe::MGPipeKind kind, Uint64 lifetimeId,
MG_Pipe::MGPipeHandle /*handle*/) {
// Monolith has no record to emit and no server twin to tell.
if (MG_Config::Transport == MG_Config::TransportMode::Monolith) return;
// Raised ON the apply thread: the server role destroying a frontend object it
@@ -47,8 +47,6 @@
#include <MG_Pipe/PipeSessionFail.h>
// P5c (T5 / tx): the server's staged-texture shadow GenerateMipmap defines its chain on.
#include <MG_Backend/Record/StagedTextureStore.h>
// P5c (G6): the named-blit arm's endpoint resolution runs inside the frontend-keyed scope.
#include <MG_Impl/Pipe/SlotAllocator.h>
// P7 wave 2 package B3: rule I's tally for WireDraw.inc's silent draw drops.
#include "WireDeclineTally.h"
#include "WireColorBlitFilter.h"
@@ -4919,7 +4917,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint32 slot = freeSlot;
if (slot == kNoSlot) {
for (Uint32 candidate = 0; candidate < kNoSlot; ++candidate) {
if (MG_Pipe::gPipeInputs.HasOpenTransformFeedbackSpan(m_xfbCounterSlotOwner[candidate])) {
if (m_xfbCounterSlotOwner[candidate] != 0 &&
MG_Pipe::MGPipeApplier().StreamOutputSpans.count(m_xfbCounterSlotOwner[candidate]) != 0) {
continue;
}
if (slot == kNoSlot || m_xfbCounterSlotLastUse[candidate] < m_xfbCounterSlotLastUse[slot]) {
+13 -2
View File
@@ -1440,7 +1440,7 @@ namespace MobileGL::MG_Pipe {
// 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);
MG_State::GLState::NotifyStateObjectDestroyed(MGPipeKind::VertexElementsCso, lifetimeId, handle);
if (!MGPipeHandleIsNull(handle)) MGPipeSlots().Free(MGPipeKind::VertexElementsCso, handle);
return published;
}
@@ -1979,7 +1979,7 @@ namespace MobileGL::MG_Pipe {
// P4a: whether a slot exists is this client's business, and a consumer that records
// notices must not stop seeing a class announce itself.
void NotifyAndFree(MGPipeKind kind, Uint64 lifetimeId, MGPipeHandle handle) {
MG_State::GLState::NotifyStateObjectDestroyed(kind, lifetimeId);
MG_State::GLState::NotifyStateObjectDestroyed(kind, lifetimeId, handle);
if (!MGPipeHandleIsNull(handle)) MGPipeSlots().Free(kind, handle);
}
@@ -2265,6 +2265,17 @@ namespace MobileGL::MG_Pipe {
return lifetimeId != 0 && MGPipeApplier().StreamOutputSpans.count(lifetimeId) != 0;
}
namespace {
// The frontend half of MGPipeNotifyCompileEnvChanged (PipeApply.h): registered once, at
// static initialisation, by the translation unit that owns the forward.
const Bool kCompileEnvHookRegistered = [] {
MGPipeSetCompileEnvChangedHook([] {
if (gPipeInputs.IsLive()) gPipeInputs.InvalidateCompileEnv();
});
return true;
}();
}
void PipeInputs::InvalidateCompileEnv() {
MGP_STICKY_FORWARD_PULL(InvalidateCompileEnv);
#if MOBILEGL_BUILD_DISAGGREGATED
-25
View File
@@ -21,12 +21,6 @@
namespace MobileGL::MG_Pipe {
#if MOBILEGL_BUILD_DISAGGREGATED
Bool MGPipeApplierIsUnbarrieredApply() {
if (MG_Config::Transport == MG_Config::TransportMode::Monolith) return false;
if (!MG_Remote::Server::ServerLoop::OnApplyThread()) return false;
return !MGPipeApplierCurrentRecordIsBarriered();
}
void MGPipeRefuseAllocatorFromApplyThread(const char* entry) {
if (MG_Config::Transport == MG_Config::TransportMode::Monolith) return;
// D10: OR'd with the PROCESS-ROLE fact, not replaced by it. OnApplyThread() answers
@@ -49,25 +43,6 @@ namespace MobileGL::MG_Pipe {
std::abort();
}
// P5f (fr): all frontend-identity registry surfaces are monolith glue, including
// the ones that do not touch the allocator. Neither a wait nor a named scope can
// make a frontend SharedPtr exist in a separate server process.
void MGPipeRefuseFrontendKeyedRegistryFromApplyThread(const char* entry) {
if (MG_Config::Transport == MG_Config::TransportMode::Monolith) return;
// D10: OR'd with the PROCESS-ROLE fact, not replaced by it. OnApplyThread() answers
// about the INPROC applier thread and is the only fact a ServerLoop fixture with no
// client session has; MGPipeServerArm() adds the spawn case, where EVERY thread is a
// server thread. Replacing the first with the second disarmed the refusal in every unit
// test that drives the apply thread directly - measured, 14 RemoteGuards cases went
// green-by-omission before this was put back as a disjunction.
if (!MG_Remote::Server::ServerLoop::OnApplyThread() && !MG_Pipe::MGPipeServerArm()) return;
MGLOG_F("MGPipe: Fatal{RoleViolation, \"MGPipeSlots\"} - the apply thread reached "
"BackendSlotTable::%s. Frontend-identity registry operations are monolith-only, "
"including barriered records; resolve the twin from the record's handle instead",
entry);
std::abort();
}
#endif
namespace {
+1 -50
View File
@@ -173,57 +173,8 @@ namespace MobileGL::MG_Pipe {
// pull build's bytes do not move (G1).
void MGPipeRefuseAllocatorFromApplyThread(const char* entry);
// Shared predicate for remaining field/verb guards. Registry and allocator
// guards below refuse both barriered and unbarriered transport apply.
Bool MGPipeApplierIsUnbarrieredApply();
// P5f (fr): frontend-object twin lookup, minting and weak-state access are refused
// on EVERY transport apply, including barriered records and both named scopes.
// The handle overloads remain server-local; the frontend overloads remain monolith
// glue. The registry guard is independent of the allocator guard.
void MGPipeRefuseFrontendKeyedRegistryFromApplyThread(const char* entry);
// P5f (fr): these historical scope names remain greppable markers around
// monolith glue until P3b/P4b remove those bodies. They hold no state and grant
// no exemption: allocator and frontend-registry access always refuse apply.
//
// P7 wave 2-B2: MagmaP7AllocatorDebtScope has NO BACKEND USES LEFT. P5e ruling 12 named
// four Magma apply-thread allocator debts (VulkanRenderer.cpp x3, ResourceTracker.h x1);
// P5f fv and P5f fm retired two of them, and this package retires the surviving pair -
// ShutdownBlitResources and ShutdownDepthMipmapResources, where the objects the debt was
// about are simply never created off the monolith arm. The class stays because
// MG_Test/Wire/RemoteClientTest uses it as a NEGATIVE CONTROL: two RemoteGuards cases
// construct it on the apply thread and assert the allocator refuses anyway, which is the
// property P5f (fr) established and the only thing this name now asserts.
class MagmaP7AllocatorDebtScope {
public:
MagmaP7AllocatorDebtScope() = default;
~MagmaP7AllocatorDebtScope() = default;
MagmaP7AllocatorDebtScope(const MagmaP7AllocatorDebtScope&) = delete;
MagmaP7AllocatorDebtScope& operator=(const MagmaP7AllocatorDebtScope&) = delete;
};
class MGPipeFrontendKeyedRegistryScope {
public:
MGPipeFrontendKeyedRegistryScope() = default;
~MGPipeFrontendKeyedRegistryScope() = default;
MGPipeFrontendKeyedRegistryScope(const MGPipeFrontendKeyedRegistryScope&) = delete;
MGPipeFrontendKeyedRegistryScope& operator=(const MGPipeFrontendKeyedRegistryScope&) = delete;
};
#else
// P13 W5: no transport, no apply thread - the role guards have nothing to refuse, and the two
// scopes nothing to mark (the record arm calls all of them in every build).
// P13 W5: no transport, no apply thread - the allocator's role guard has nothing to refuse.
inline void MGPipeRefuseAllocatorFromApplyThread(const char*) {}
inline Bool MGPipeApplierIsUnbarrieredApply() { return false; }
inline void MGPipeRefuseFrontendKeyedRegistryFromApplyThread(const char*) {}
class MagmaP7AllocatorDebtScope {
public:
MagmaP7AllocatorDebtScope() = default;
};
class MGPipeFrontendKeyedRegistryScope {
public:
MGPipeFrontendKeyedRegistryScope() = default;
};
#endif
} // namespace MobileGL::MG_Pipe
@@ -14,6 +14,7 @@
#include <MG_State/GLState/Core.h>
#include <MG_Backend/DirectGLES/Managers.h>
#include <MG_Backend/DirectGLES/DirectGLES.h>
#include <MG_Impl/Pipe/SlotAllocator.h>
#define MGITEST_P4A_SEAM_PEEK_LIVE 1
#endif
@@ -89,7 +90,11 @@ namespace MGITest {
const auto& object = MobileGL::MG_State::pGLContext->GetSamplerObject(
static_cast<MobileGL::Uint>(glSamplerName));
if (object) {
if (auto* const slot = MGB::SamplerImpl::g_backendSamplerObjects.Find(object.get()); slot && *slot) {
// Resolved the client's way (the harness is the client): the object's identity handle,
// if one was ever minted, then the twin at it.
const MGP::MGPipeHandle identity =
MGP::MGPipeSlots().FindByLifetimeId(MGP::MGPipeKind::SamplerCso, object->GetLifetimeId());
if (auto* const slot = MGB::SamplerImpl::g_backendSamplerObjects.FindByHandle(identity); slot && *slot) {
out->IdentityTwinSamplerId = static_cast<unsigned>((*slot)->GetBackendSamplerId());
}
}
@@ -16,9 +16,9 @@
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
#include <MG_Backend/DirectGLES/Managers.h>
#include <MG_Backend/DirectGLES/DirectGLES.h>
#include <MG_Impl/Pipe/SlotAllocator.h>
#if MOBILEGL_BUILD_DISAGGREGATED
#include <Config.h>
#include <MG_Impl/Pipe/SlotAllocator.h>
#include <MG_Remote/Client/ClientSession.h>
#include <MG_Remote/Server/ServerLoop.h>
#endif
@@ -49,7 +49,11 @@ namespace MGITest {
MGB::TextureImpl::BackendTextureObject* EsprytTwin(unsigned glTextureName) {
MobileGL::MG_State::GLState::ITextureObject* const object = FrontendTexture(glTextureName);
if (object == nullptr) return nullptr;
auto* const found = MGB::TextureImpl::g_backendTextureObjects.Find(object);
// The harness is the client: it resolves the handle in the client's allocator and asks
// Espryt's table by handle, as the record does (P13 retired the object-keyed lookup).
const MGP::MGPipeHandle handle =
MGP::MGPipeSlots().FindByLifetimeId(MGP::MGPipeKind::Texture, object->GetLifetimeId());
auto* const found = MGB::TextureImpl::g_backendTextureObjects.FindByHandle(handle);
if (found == nullptr || !*found) return nullptr;
return found->get();
}
@@ -229,12 +233,12 @@ namespace MGITest {
// Espryt must be the backend running, or "no view" would be true of every texture on
// every other backend and the assertion would be vacuous where it is loudest.
if (EsprytTwin(glTextureName) == nullptr) return false;
// HandleOfSamplerViewForTexture is the monolith glue that derives the view's handle from
// the TEXTURE's lifetime id (D-F2: one view per ITextureObject), so this asks Espryt's
// own table the same way Espryt asks it - it does not consult the applier record's
// ViewCso, which is the client's statement about the same fact and would make one side
// of the seam vouch for the other.
const MGP::MGPipeHandle view = MGB::SamplerViewImpl::HandleOfSamplerViewForTexture(object);
// The view's handle is minted off the TEXTURE's lifetime id (D-F2: one view per
// ITextureObject); the harness resolves it in the client's allocator and asks Espryt's own
// table by it - it does not consult the applier record's ViewCso, which is the client's
// statement about the same fact and would make one side of the seam vouch for the other.
const MGP::MGPipeHandle view =
MGP::MGPipeSlots().FindByLifetimeId(MGP::MGPipeKind::SamplerViewCso, object->GetLifetimeId());
if (MGP::MGPipeHandleIsNull(view)) {
*outExists = false;
return true;
+12
View File
@@ -781,6 +781,18 @@ namespace MobileGL::MG_Pipe {
return (appliers.fetch_add(1, std::memory_order_relaxed) + 1) << 40;
}
namespace {
std::atomic<MGPipeCompileEnvChangedHook> g_compileEnvChangedHook{nullptr};
}
void MGPipeSetCompileEnvChangedHook(MGPipeCompileEnvChangedHook hook) {
g_compileEnvChangedHook.store(hook, std::memory_order_release);
}
void MGPipeNotifyCompileEnvChanged() {
if (const auto hook = g_compileEnvChangedHook.load(std::memory_order_acquire)) hook();
}
MGPipeApplierState& MGPipeApplier() {
const MGPipeApplierKeyResolver resolve = g_applierKeyResolver;
if (resolve == nullptr) return g_defaultApplier;
+10
View File
@@ -1019,6 +1019,16 @@ namespace MobileGL::MG_Pipe {
// is byte-for-byte the old behaviour.
MGPipeApplierState& MGPipeApplier();
// P13 W6: the backend's "my capabilities changed" notice, without naming the frontend. A
// backend whose advertised limits / extensions move after a context exists (Magma's caps
// re-query) calls MGPipeNotifyCompileEnvChanged(); the frontend registers the hook that
// invalidates its shader compile environment (PipeFill.cpp, static registration). Under a
// transport no hook is registered on the server side and the client invalidates on the caps
// re-publication instead (CONTRACT-P5C §4.2), so the notice is a no-op there.
using MGPipeCompileEnvChangedHook = void (*)();
void MGPipeSetCompileEnvChangedHook(MGPipeCompileEnvChangedHook hook);
void MGPipeNotifyCompileEnvChanged();
// =====================================================================================
// P14 S5 (docs/Disaggregated/design/11-state-ownership.md): WHOSE APPLIER IS IT?
// =====================================================================================
+2 -1
View File
@@ -1249,7 +1249,8 @@ namespace MobileGL::MG_Remote::Client {
}
namespace {
void WireStateObjectDestroyed(MG_Pipe::MGPipeKind kind, Uint64 lifetimeId) {
void WireStateObjectDestroyed(MG_Pipe::MGPipeKind kind, Uint64 lifetimeId,
MG_Pipe::MGPipeHandle /*handle*/) {
(void)EmitObjectDeathRecord(kind, lifetimeId);
}
@@ -37,8 +37,11 @@ namespace MobileGL::MG_State::GLState {
struct StateObjectDeathOps {
// The last SharedPtr to the frontend object with this lifetime id has dropped.
// Called from the object's destructor, so the object must NOT be touched.
void (*OnDestroyed)(MG_Pipe::MGPipeKind kind, Uint64 lifetimeId) = nullptr;
// Called from the object's destructor, so the object must NOT be touched. `handle` is
// the object's {slot, gen} of this kind, resolved by the caller while it still
// resolves (null when the object never had one); P13 keys the backend's twin release
// on it, so no consumer probes the allocator by lifetime id.
void (*OnDestroyed)(MG_Pipe::MGPipeKind kind, Uint64 lifetimeId, MG_Pipe::MGPipeHandle handle) = nullptr;
};
inline const StateObjectDeathOps* g_stateObjectDeathOps = nullptr;
@@ -51,10 +54,11 @@ namespace MobileGL::MG_State::GLState {
return g_stateObjectDeathOps;
}
inline void NotifyStateObjectDestroyed(MG_Pipe::MGPipeKind kind, Uint64 lifetimeId) {
inline void NotifyStateObjectDestroyed(MG_Pipe::MGPipeKind kind, Uint64 lifetimeId,
MG_Pipe::MGPipeHandle handle) {
const StateObjectDeathOps* ops = g_stateObjectDeathOps;
if (ops == nullptr || ops->OnDestroyed == nullptr) return;
ops->OnDestroyed(kind, lifetimeId);
ops->OnDestroyed(kind, lifetimeId, handle);
}
} // namespace MobileGL::MG_State::GLState
+201 -294
View File
@@ -3321,39 +3321,45 @@ namespace {
} // namespace
namespace {
// The CLIENT's half of every case below, played by the case itself: the handle a record would
// carry, minted in the client's allocator. Since P13 the table never mints (it has no entry that
// could); it only receives handles, exactly as here.
MobileGL::MG_Pipe::MGPipeHandle MintFor(MobileGL::MG_Pipe::MGPipeKind kind, MobileGL::Uint64 lifetimeId) {
return MobileGL::MG_Pipe::MGPipeSlots().Acquire(kind, lifetimeId);
}
} // namespace
// The property the whole slice exists for. The pre-P2 registry keyed twins on the frontend heap
// ADDRESS and defended the recycle with a weak_ptr; here the key is {slot, gen}, so a successor
// object landing on a slot its predecessor owned is a DIFFERENT handle, and the predecessor's
// handle resolves to nothing rather than to the successor's twin.
// landing on a slot its predecessor owned is a DIFFERENT handle, it is handed an EMPTY twin, and
// the predecessor's handle resolves to nothing rather than to the successor's twin.
TEST(DirectGLESSlotTable, ARecycledSlotIsANewHandleAndTheStaleOneResolvesToNothing) {
using namespace MobileGL;
constexpr MG_Pipe::MGPipeKind kKind = MG_Pipe::MGPipeKind::Query;
FakeSlotTable table;
auto first = MakeShared<FakeStateObject>(0xA1u);
table.GetOrCreate(first) = MakeShared<FakeBackendObject>();
(*table.Find(first.get()))->marker = 1;
const MG_Pipe::MGPipeHandle firstHandle = table.HandleOf(first.get());
const MG_Pipe::MGPipeHandle firstHandle = MintFor(kKind, 0xA1u);
ASSERT_FALSE(MG_Pipe::MGPipeHandleIsNull(firstHandle));
table.GetOrCreate(firstHandle) = MakeShared<FakeBackendObject>();
(*table.FindByHandle(firstHandle))->marker = 1;
EXPECT_EQ(table.LiveCount(), 1u);
// The frontend object dies and announces it (FakeStateObject is not one of the six re-keyed
// classes, so the notice its destructor would raise is raised by hand), and the slot is
// reclaimed - which is the only moment Gen moves.
first.reset();
EXPECT_TRUE(FakeSlotTable::OnFrontendObjectDestroyed(0xA1u));
// The object dies: the twin is released by its handle (the death notice's release), then the
// client returns the slot - which is the only moment Gen can move, on the next handout.
EXPECT_TRUE(FakeSlotTable::ReleaseTwinByHandle(firstHandle));
MG_Pipe::MGPipeSlots().Free(kKind, firstHandle);
EXPECT_EQ(table.LiveCount(), 0u);
EXPECT_EQ(table.FindByHandle(firstHandle), nullptr)
<< "a handle whose object is gone still resolved to a twin";
auto second = MakeShared<FakeStateObject>(0xA2u);
table.GetOrCreate(second) = MakeShared<FakeBackendObject>();
(*table.Find(second.get()))->marker = 2;
const MG_Pipe::MGPipeHandle secondHandle = table.HandleOf(second.get());
const MG_Pipe::MGPipeHandle secondHandle = MintFor(kKind, 0xA2u);
EXPECT_EQ(secondHandle.Slot, firstHandle.Slot) << "the free list did not hand the slot back";
EXPECT_NE(secondHandle.Gen, firstHandle.Gen) << "the generation did not move on slot reuse";
EXPECT_FALSE(firstHandle == secondHandle);
EXPECT_EQ(table.GetOrCreate(secondHandle), nullptr)
<< "the successor at a recycled slot inherited its predecessor's twin";
table.GetOrCreate(secondHandle) = MakeShared<FakeBackendObject>();
(*table.FindByHandle(secondHandle))->marker = 2;
// The stale handle must not resolve to its successor's twin. This is the ABA the address key
// could only paper over.
@@ -3361,113 +3367,82 @@ TEST(DirectGLESSlotTable, ARecycledSlotIsANewHandleAndTheStaleOneResolvesToNothi
ASSERT_NE(table.FindByHandle(secondHandle), nullptr);
EXPECT_EQ((*table.FindByHandle(secondHandle))->marker, 2);
second.reset();
EXPECT_TRUE(FakeSlotTable::OnFrontendObjectDestroyed(0xA2u));
EXPECT_TRUE(FakeSlotTable::ReleaseTwinByHandle(secondHandle));
MG_Pipe::MGPipeSlots().Free(kKind, secondHandle);
}
// Gen moves on reuse and ONLY on reuse: a live object that is looked up again, or respecified,
// keeps the handle it was minted with (MGPipeHandles.h).
// Gen moves on reuse and ONLY on reuse: a live entry that is resolved again - by GetOrCreate or by
// FindByHandle, any number of times - keeps the one twin it was built with (MGPipeHandles.h).
TEST(DirectGLESSlotTable, RepeatedLookupsOfALiveObjectKeepOneHandle) {
using namespace MobileGL;
constexpr MG_Pipe::MGPipeKind kKind = MG_Pipe::MGPipeKind::Query;
FakeSlotTable table;
auto object = MakeShared<FakeStateObject>(0xB1u);
// An object that is bound but never synced has no twin and no handle; asking is a miss.
EXPECT_TRUE(MG_Pipe::MGPipeHandleIsNull(table.HandleOf(object.get())));
table.GetOrCreate(object) = MakeShared<FakeBackendObject>();
const MG_Pipe::MGPipeHandle handle = table.HandleOf(object.get());
ASSERT_FALSE(MG_Pipe::MGPipeHandleIsNull(handle))
<< "the memo went on answering a null handle after the twin was created";
const MG_Pipe::MGPipeHandle handle = MintFor(kKind, 0xB1u);
// A handle no record has resolved yet has no twin; asking is a miss, not a mint.
EXPECT_EQ(table.FindByHandle(handle), nullptr);
EXPECT_EQ(table.LiveCount(), 0u);
table.GetOrCreate(handle) = MakeShared<FakeBackendObject>();
const FakeBackendObject* const twin = table.FindByHandle(handle)->get();
for (int i = 0; i < 8; ++i) {
auto* slot = table.GetOrCreate(object) ? table.Find(object.get()) : nullptr;
ASSERT_NE(slot, nullptr);
EXPECT_TRUE(table.HandleOf(object.get()) == handle) << "handle moved on lookup " << i;
EXPECT_EQ(table.GetOrCreate(handle).get(), twin) << "the twin was re-made on lookup " << i;
ASSERT_NE(table.FindByHandle(handle), nullptr);
EXPECT_EQ(table.FindByHandle(handle)->get(), twin);
}
EXPECT_EQ(table.LiveCount(), 1u);
object.reset();
EXPECT_TRUE(FakeSlotTable::OnFrontendObjectDestroyed(0xB1u));
EXPECT_TRUE(FakeSlotTable::ReleaseTwinByHandle(handle));
MG_Pipe::MGPipeSlots().Free(kKind, handle);
}
// The round-4 review's minor 8: HandleOf used to memoise a NULL answer, on the argument that
// GetOrCreate refreshes the memo. That holds for ONE table. The allocator is per kind and the
// memo is per table, so once a second holder of the kind can be the one that acquires, the first
// table's cached "no handle" outlives the twin's creation and nothing on its own acquire path
// ever corrects it - every Find through it is a miss for a twin that exists. This is that
// configuration, and it must resolve.
TEST(DirectGLESSlotTable, ANegativeLookupIsNotCachedAcrossAnotherHoldersAcquire) {
using namespace MobileGL;
FakeSharedKindSlotTable first;
FakeSharedKindSlotTable second;
auto object = MakeShared<FakeStateObject>(0xB2u);
// `first` asks before anyone has acquired: a miss, which must NOT be remembered.
EXPECT_TRUE(MG_Pipe::MGPipeHandleIsNull(first.HandleOf(object.get())));
// `second` is the holder that acquires.
second.GetOrCreate(object) = MakeShared<FakeBackendObject>();
const MG_Pipe::MGPipeHandle handle = second.HandleOf(object.get());
ASSERT_FALSE(MG_Pipe::MGPipeHandleIsNull(handle));
// `first` never acquired, so nothing on its own path refreshed its memo; it must still
// answer the handle the kind now has for this object.
EXPECT_TRUE(first.HandleOf(object.get()) == handle)
<< "the first holder kept answering the null handle it cached before the second holder "
"acquired, so every lookup through it misses a twin that exists";
object.reset();
EXPECT_TRUE(FakeSharedKindSlotTable::OnFrontendObjectDestroyed(0xB2u));
}
// The lookup does not mutate the table, which is what lets SyncTextureObjectToBackend stop paying
// a by-value copy plus a second Find to survive the registry's erase-inside-Find. An entry whose
// object has gone stays put until the death notice arrives, and a live entry's pointer is
// unaffected by looking up anything else.
// The lookup does not mutate the table: a returned pointer is an array element, so it survives any
// later lookup - including one for a handle whose twin is gone - and only a release takes an entry.
TEST(DirectGLESSlotTable, FindNeverMutatesTheTable) {
using namespace MobileGL;
constexpr MG_Pipe::MGPipeKind kKind = MG_Pipe::MGPipeKind::Query;
FakeSlotTable table;
auto kept = MakeShared<FakeStateObject>(0xC1u);
auto doomed = MakeShared<FakeStateObject>(0xC2u);
const MG_Pipe::MGPipeHandle kept = MintFor(kKind, 0xC1u);
const MG_Pipe::MGPipeHandle doomed = MintFor(kKind, 0xC2u);
table.GetOrCreate(kept) = MakeShared<FakeBackendObject>();
table.GetOrCreate(doomed) = MakeShared<FakeBackendObject>();
auto* keptSlot = table.Find(kept.get());
auto* keptSlot = table.FindByHandle(kept);
ASSERT_NE(keptSlot, nullptr);
const FakeBackendObject* keptTwin = keptSlot->get();
// The object goes but its notice is deliberately withheld for a moment, so that whatever
// changes between here and the notice is Find's doing. The registry's Find would have
// erased the expired entry here and relocated the rest of the probe cluster, invalidating
// keptSlot. This one answers what it answers and touches nothing.
doomed.reset();
EXPECT_EQ(table.Find(kept.get()), keptSlot);
EXPECT_EQ(table.LiveCount(), 2u) << "Find reclaimed a slot; only a death notice may do that";
// Lookups of the other entry, of a stale generation of it and of the null handle change
// nothing.
(void)table.FindByHandle(doomed);
(void)table.FindByHandle(MG_Pipe::MGPipeHandle{doomed.Slot, doomed.Gen + 1u});
(void)table.FindByHandle(MG_Pipe::kMGPipeNullHandle);
EXPECT_EQ(table.FindByHandle(kept), keptSlot);
EXPECT_EQ(table.LiveCount(), 2u) << "a lookup reclaimed a slot; only a release may do that";
EXPECT_EQ(keptSlot->get(), keptTwin);
EXPECT_TRUE(FakeSlotTable::OnFrontendObjectDestroyed(0xC2u));
EXPECT_TRUE(FakeSlotTable::ReleaseTwinByHandle(doomed));
MG_Pipe::MGPipeSlots().Free(kKind, doomed);
EXPECT_EQ(table.LiveCount(), 1u);
EXPECT_EQ(table.Find(kept.get())->get(), keptTwin);
EXPECT_EQ(table.FindByHandle(kept)->get(), keptTwin);
kept.reset();
EXPECT_TRUE(FakeSlotTable::OnFrontendObjectDestroyed(0xC1u));
EXPECT_TRUE(FakeSlotTable::ReleaseTwinByHandle(kept));
MG_Pipe::MGPipeSlots().Free(kKind, kept);
}
// ScopedDirectGLESTextureBindings saves a whole twin table by value, resets it with `= {}` and
// restores it. The slot table has to keep that shape or the fixture stops isolating anything -
// and the saved copy is a HOLDER for as long as it exists, so a death announced while it is
// held reaches it too (the round-4 review's minor 2, in the fixture's own shape).
// and the saved copy is a HOLDER for as long as it exists, so a release while it is held reaches
// it too (the round-4 review's minor 2, in the fixture's own shape).
TEST(DirectGLESSlotTable, AWholeTableSavesResetsAndRestores) {
using namespace MobileGL;
constexpr MG_Pipe::MGPipeKind kKind = MG_Pipe::MGPipeKind::Query;
const Uint32 holdersBefore = FakeSlotTable::HolderCount();
FakeSlotTable table;
auto object = MakeShared<FakeStateObject>(0xD1u);
table.GetOrCreate(object) = MakeShared<FakeBackendObject>();
(*table.Find(object.get()))->marker = 7;
const MG_Pipe::MGPipeHandle handle = table.HandleOf(object.get());
const MG_Pipe::MGPipeHandle handle = MintFor(kKind, 0xD1u);
table.GetOrCreate(handle) = MakeShared<FakeBackendObject>();
(*table.FindByHandle(handle))->marker = 7;
const FakeSlotTable saved = table;
EXPECT_EQ(FakeSlotTable::HolderCount(), holdersBefore + 2u)
@@ -3476,79 +3451,59 @@ TEST(DirectGLESSlotTable, AWholeTableSavesResetsAndRestores) {
EXPECT_EQ(FakeSlotTable::HolderCount(), holdersBefore + 2u)
<< "the reset changed the holder count - a temporary's registration leaked or the "
"table's own was lost";
EXPECT_EQ(table.Find(object.get()), nullptr) << "the reset left the twin reachable";
EXPECT_EQ(table.FindByHandle(handle), nullptr) << "the reset left the twin reachable";
table = saved;
ASSERT_NE(table.Find(object.get()), nullptr);
EXPECT_EQ((*table.Find(object.get()))->marker, 7);
ASSERT_NE(table.FindByHandle(handle), nullptr);
EXPECT_EQ((*table.FindByHandle(handle))->marker, 7);
// The object dies while BOTH the working table and the saved copy hold its twin. One
// notice, and neither may keep a live entry.
object.reset();
EXPECT_TRUE(FakeSlotTable::OnFrontendObjectDestroyed(0xD1u));
// release, and neither may keep a live entry.
EXPECT_TRUE(FakeSlotTable::ReleaseTwinByHandle(handle));
MG_Pipe::MGPipeSlots().Free(kKind, handle);
EXPECT_EQ(table.FindByHandle(handle), nullptr);
EXPECT_EQ(table.LiveCount(), 0u);
EXPECT_EQ(saved.LiveCount(), 0u)
<< "the saved copy kept the dead object's twin - the notice reached one holder only";
<< "the saved copy kept the dead object's twin - the release reached one holder only";
}
// The whole point of routing every twin through the client allocator: a table that keeps its own
// dense array still shares ONE identity per frontend object with every other holder of it.
//
// Comparing a.HandleOf(o) with b.HandleOf(o) alone would prove nothing - HandleOf never reads the
// table, so any two tables agree for any implementation. What is actually load-bearing, and what
// is asserted here, is that ONE slot of the kind is consumed for the object no matter how many
// tables hold a twin of it (a per-table allocator would pass the pure compare and fail this), and
// that the shared handle still addresses each table's OWN twin.
// One handle, two holders of the kind: the shared handle addresses each table's OWN twin, and
// neither table's twin is the other's.
TEST(DirectGLESSlotTable, TwoTablesOfTheSameKindShareOneSlotAndKeepTheirOwnTwin) {
using namespace MobileGL;
constexpr MG_Pipe::MGPipeKind kKind = MG_Pipe::MGPipeKind::Fence;
auto& slots = MG_Pipe::MGPipeSlots();
const Uint32 liveBefore = slots.LiveCount(kKind);
FakeSharedKindSlotTable a;
FakeSharedKindSlotTable b;
auto object = MakeShared<FakeStateObject>(0xE1u);
a.GetOrCreate(object) = MakeShared<FakeBackendObject>();
(*a.Find(object.get()))->marker = 1;
b.GetOrCreate(object) = MakeShared<FakeBackendObject>();
(*b.Find(object.get()))->marker = 2;
const MG_Pipe::MGPipeHandle handle = MintFor(kKind, 0xE1u);
ASSERT_FALSE(MG_Pipe::MGPipeHandleIsNull(handle));
a.GetOrCreate(handle) = MakeShared<FakeBackendObject>();
(*a.FindByHandle(handle))->marker = 1;
b.GetOrCreate(handle) = MakeShared<FakeBackendObject>();
(*b.FindByHandle(handle))->marker = 2;
EXPECT_EQ(slots.LiveCount(kKind), liveBefore + 1u)
<< "the two tables minted a slot each; Magma's table would then resolve a different "
"handle for the same object than Espryt's";
const MG_Pipe::MGPipeHandle handle = a.HandleOf(object.get());
ASSERT_FALSE(MG_Pipe::MGPipeHandleIsNull(handle));
EXPECT_TRUE(b.HandleOf(object.get()) == handle);
<< "twinning one handle in two tables consumed more than the client's one slot";
ASSERT_NE(a.FindByHandle(handle), nullptr);
ASSERT_NE(b.FindByHandle(handle), nullptr);
EXPECT_EQ((*a.FindByHandle(handle))->marker, 1);
EXPECT_EQ((*b.FindByHandle(handle))->marker, 2);
// TWO HOLDERS OF ONE SLOT is a real configuration, not a test artefact (the
// ScopedDirectGLESTextureBindings fixture above holds a second live table of kind Texture
// for the length of a test), and the death of the object is what makes it sharp: the
// allocator forgets the lifetime id on Free, so a notice delivered to ONE holder leaves
// the other with a live entry - and its twin's driver storage - that nothing can resolve
// and nothing sweeps. OneDeathNoticeDropsTheTwinInEveryHolderOfTheKind below is the case
// for that; here the cleanup only has to leave the kind's LiveCount where it was.
object.reset();
EXPECT_TRUE(FakeSharedKindSlotTable::OnFrontendObjectDestroyed(0xE1u));
EXPECT_EQ(slots.LiveCount(kKind), liveBefore)
<< "the shared slot outlived both holders and the object";
// TWO HOLDERS OF ONE SLOT is a real configuration (the ScopedDirectGLESTextureBindings fixture
// holds a second live table of kind Texture for the length of a test);
// OneDeathNoticeDropsTheTwinInEveryHolderOfTheKind below is the case for its death.
EXPECT_TRUE(FakeSharedKindSlotTable::ReleaseTwinByHandle(handle));
slots.Free(kKind, handle);
EXPECT_EQ(slots.LiveCount(kKind), liveBefore) << "the shared slot outlived both holders";
}
// The round-4 review's minor 2, closed: one notice, EVERY holder. Before this the dispatcher
// told one table per kind and DestroyByLifetimeId freed only a slot THIS table held, so with
// the sweep retired the second holder kept a live entry, and the twin, for the life of the
// process. Three holders here - two independent tables and a by-value copy, which is exactly
// what the fixture makes - and a fourth that never twinned the object and must be untouched.
// The round-4 review's minor 2, closed: one release, EVERY holder. Three holders here - two
// independent tables and a by-value copy, which is exactly what the fixture makes - and a fourth
// that never twinned the handle and must be untouched.
TEST(DirectGLESSlotTable, OneDeathNoticeDropsTheTwinInEveryHolderOfTheKind) {
using namespace MobileGL;
constexpr MG_Pipe::MGPipeKind kKind = MG_Pipe::MGPipeKind::Fence;
auto& slots = MG_Pipe::MGPipeSlots();
const Uint32 liveBefore = slots.LiveCount(kKind);
@@ -3557,24 +3512,21 @@ TEST(DirectGLESSlotTable, OneDeathNoticeDropsTheTwinInEveryHolderOfTheKind) {
FakeSharedKindSlotTable a;
FakeSharedKindSlotTable b;
FakeSharedKindSlotTable bystander;
auto object = MakeShared<FakeStateObject>(0xE2u);
auto uninvolved = MakeShared<FakeStateObject>(0xE3u);
const MG_Pipe::MGPipeHandle handle = MintFor(kKind, 0xE2u);
const MG_Pipe::MGPipeHandle uninvolved = MintFor(kKind, 0xE3u);
a.GetOrCreate(object) = MakeShared<FakeBackendObject>();
b.GetOrCreate(object) = MakeShared<FakeBackendObject>();
a.GetOrCreate(handle) = MakeShared<FakeBackendObject>();
b.GetOrCreate(handle) = MakeShared<FakeBackendObject>();
bystander.GetOrCreate(uninvolved) = MakeShared<FakeBackendObject>();
const FakeSharedKindSlotTable copyOfA = a; // the fixture's saved registry
EXPECT_EQ(FakeSharedKindSlotTable::HolderCount(), holdersBefore + 4u);
const MG_Pipe::MGPipeHandle handle = a.HandleOf(object.get());
ASSERT_FALSE(MG_Pipe::MGPipeHandleIsNull(handle));
// Observers on the twins, so "dropped" means destroyed and not merely unreachable.
const std::weak_ptr<FakeBackendObject> twinA = *a.FindByHandle(handle);
const std::weak_ptr<FakeBackendObject> twinB = *b.FindByHandle(handle);
EXPECT_EQ(slots.LiveCount(kKind), liveBefore + 2u);
// ONE notice. The object is still alive so that nothing but the notice can be at work.
EXPECT_TRUE(FakeSharedKindSlotTable::OnFrontendObjectDestroyed(object->GetLifetimeId()));
// ONE release.
EXPECT_TRUE(FakeSharedKindSlotTable::ReleaseTwinByHandle(handle));
EXPECT_EQ(a.FindByHandle(handle), nullptr) << "holder a kept the twin";
EXPECT_EQ(b.FindByHandle(handle), nullptr) << "holder b kept the twin";
@@ -3583,135 +3535,88 @@ TEST(DirectGLESSlotTable, OneDeathNoticeDropsTheTwinInEveryHolderOfTheKind) {
EXPECT_EQ(b.LiveCount(), 0u);
EXPECT_TRUE(twinA.expired()) << "a's twin is unreachable but still allocated";
EXPECT_TRUE(twinB.expired()) << "b's twin is unreachable but still allocated";
EXPECT_EQ(slots.LiveCount(kKind), liveBefore + 1u) << "the slot was not returned exactly once";
EXPECT_FALSE(FakeSharedKindSlotTable::OnFrontendObjectDestroyed(object->GetLifetimeId()))
<< "a second notice for the same object found a slot to free";
EXPECT_FALSE(FakeSharedKindSlotTable::ReleaseTwinByHandle(handle))
<< "a second release for the same handle found a twin to drop";
// The holder that never twinned the object is exactly as it was.
// The holder that never twinned the handle is exactly as it was.
EXPECT_EQ(bystander.LiveCount(), 1u);
ASSERT_NE(bystander.Find(uninvolved.get()), nullptr);
ASSERT_NE(bystander.FindByHandle(uninvolved), nullptr);
object.reset();
uninvolved.reset();
EXPECT_TRUE(FakeSharedKindSlotTable::OnFrontendObjectDestroyed(0xE3u));
slots.Free(kKind, handle);
EXPECT_TRUE(FakeSharedKindSlotTable::ReleaseTwinByHandle(uninvolved));
slots.Free(kKind, uninvolved);
EXPECT_EQ(bystander.LiveCount(), 0u);
EXPECT_EQ(slots.LiveCount(kKind), liveBefore);
}
// The sweep and both of its drivers are RETIRED on this arm (ROADMAP.md:18's "delete the GC"),
// and this is the property that replaces them. The registry this table replaces learned of a
// death only by finding an expired weak_ptr, so it needed a 1024-call draw tick AND a
// 64-creation tick and still held up to 64 dead, gigabyte-sized twins at once. An announced
// death returns the slot before the next creation asks for one, so NOTHING accumulates -
// nothing below calls CollectGarbageIfNeeded() or CollectGarbageNow(), and on this arm the
// former does nothing at all.
// The sweep and both of its drivers are RETIRED (ROADMAP.md:18's "delete the GC"), and this is
// the property that replaces them: an announced death releases the twin before the next creation
// asks for a slot, so NOTHING accumulates under object churn.
TEST(DirectGLESSlotTable, AnnouncedDeathKeepsObjectChurnFromAccumulatingWithoutASweep) {
using namespace MobileGL;
constexpr MG_Pipe::MGPipeKind kKind = MG_Pipe::MGPipeKind::Query;
auto& slots = MG_Pipe::MGPipeSlots();
constexpr Uint32 kChurn = 256u;
const Uint32 highWaterBefore = slots.HighWater(MG_Pipe::MGPipeKind::Query);
const Uint32 liveBefore = slots.LiveCount(MG_Pipe::MGPipeKind::Query);
const Uint32 highWaterBefore = slots.HighWater(kKind);
const Uint32 liveBefore = slots.LiveCount(kKind);
FakeSlotTable table;
Uint32 peakLive = 0;
for (Uint32 i = 0; i < kChurn; ++i) {
auto object = MakeShared<FakeStateObject>(0xF0000000ull + i);
table.GetOrCreate(object) = MakeShared<FakeBackendObject>();
const MG_Pipe::MGPipeHandle handle = MintFor(kKind, 0xF0000000ull + i);
table.GetOrCreate(handle) = MakeShared<FakeBackendObject>();
peakLive = std::max(peakLive, table.LiveCount());
// What a real object's destructor raises. FakeStateObject is not one of the six
// re-keyed frontend classes, so the firing half is driven by hand here; that those six
// classes really do fire it is EveryReKeyedObjectClassAnnouncesItsOwnDeath below, and
// that the registries answer it per kind is
// EverySwitchedOverKindResolvesItsTwinThroughTheHandleArm.
EXPECT_TRUE(FakeSlotTable::OnFrontendObjectDestroyed(object->GetLifetimeId()));
// What a real object's death delivers (Espryt's notice consumer releases by the handle
// the notice carries; the client frees after it). That the six real classes do raise it
// is EveryReKeyedObjectClassAnnouncesItsOwnDeath below.
EXPECT_TRUE(FakeSlotTable::ReleaseTwinByHandle(handle));
slots.Free(kKind, handle);
}
EXPECT_EQ(peakLive, 1u)
<< peakLive << " twins were live at once with " << kChurn
<< " objects churned and every death announced - the notice stopped freeing the twin";
<< " objects churned and every death announced - the release stopped dropping the twin";
EXPECT_EQ(table.LiveCount(), 0u);
EXPECT_EQ(slots.LiveCount(MG_Pipe::MGPipeKind::Query), liveBefore)
<< "the churn leaked slots the announced deaths should have returned";
EXPECT_EQ(slots.LiveCount(kKind), liveBefore) << "the churn leaked slots";
// 2 and not 1: on a cold allocator the high-water mark counts the RESERVED slot 0
// (kMGPipeFirstAllocatableSlot is 1) as well as the one slot this loop recycles, and ctest
// runs every case in its own process, so this case sees a cold allocator. What the bound
// rules out is the thing that matters - 256 churned objects growing the space by 256.
EXPECT_LE(slots.HighWater(MG_Pipe::MGPipeKind::Query) - highWaterBefore, 2u)
// (kMGPipeFirstAllocatableSlot is 1) as well as the one slot this loop recycles.
EXPECT_LE(slots.HighWater(kKind) - highWaterBefore, 2u)
<< "the slot space grew with the churn instead of being recycled";
}
// The map arm inserted a null key and handed back that entry's twin, and
// SyncTextureObjectToBackend documents relying on it. Release builds compile the assert out, so
// on the handle arm this has to be a defined answer rather than a dereference of null.
TEST(DirectGLESSlotTable, GetOrCreateToleratesANullStateObject) {
using namespace MobileGL;
FakeSlotTable table;
const SharedPtr<FakeStateObject> none;
auto& twin = table.GetOrCreate(none);
EXPECT_EQ(twin, nullptr);
EXPECT_EQ(table.LiveCount(), 0u) << "a null object took a slot";
EXPECT_EQ(table.Find(nullptr), nullptr);
EXPECT_TRUE(MG_Pipe::MGPipeHandleIsNull(table.HandleOf(nullptr)));
// ...and a SECOND null call is handed the same parking slot rather than destroying what the
// first one was given. The map arm kept its null-keyed entry until a sweep, so a table that
// reset here would answer differently on the two arms in the one path that documents
// relying on this tolerance.
twin = MakeShared<FakeBackendObject>();
twin->marker = 5;
auto& again = table.GetOrCreate(none);
ASSERT_NE(again, nullptr) << "the second null call destroyed the first one's parked twin";
EXPECT_EQ(again->marker, 5);
EXPECT_EQ(&again, &twin);
EXPECT_EQ(table.LiveCount(), 0u);
}
// P2 step e2, the backend half. A sweep is a stand-in for a death notice; this is the notice.
// Nothing below calls CollectGarbage*: the slot comes back, and the twin goes, at the moment
// the frontend says the object is gone - which for a texture atlas or a renderbuffer is the
// difference between freeing a driver allocation now and freeing it 64 creations from now.
// P2 step e2, the backend half. A sweep is a stand-in for a death notice; this is the release the
// notice drives. It is idempotent, and it is GENERATION-EXACT: a late release for a dead handle
// must not drop the twin of the slot's next owner.
TEST(DirectGLESSlotTable, AnAnnouncedDeathReturnsTheSlotWithoutASweep) {
using namespace MobileGL;
constexpr MG_Pipe::MGPipeKind kKind = MG_Pipe::MGPipeKind::Query;
auto& slots = MG_Pipe::MGPipeSlots();
const Uint32 liveBefore = slots.LiveCount(MG_Pipe::MGPipeKind::Query);
const Uint32 liveBefore = slots.LiveCount(kKind);
FakeSlotTable table;
auto object = MakeShared<FakeStateObject>(0x1E2A0001ull);
table.GetOrCreate(object) = MakeShared<FakeBackendObject>();
const MG_Pipe::MGPipeHandle handle = table.HandleOf(object.get());
ASSERT_FALSE(MG_Pipe::MGPipeHandleIsNull(handle));
EXPECT_EQ(slots.LiveCount(MG_Pipe::MGPipeKind::Query), liveBefore + 1u);
const MG_Pipe::MGPipeHandle handle = MintFor(kKind, 0x1E2A0001ull);
table.GetOrCreate(handle) = MakeShared<FakeBackendObject>();
EXPECT_EQ(slots.LiveCount(kKind), liveBefore + 1u);
// The object is STILL ALIVE here, which is the point: there is no weak_ptr test anywhere
// that could fire, so anything that changes is the notice's doing and nothing else's.
EXPECT_TRUE(FakeSlotTable::OnFrontendObjectDestroyed(object->GetLifetimeId()));
EXPECT_EQ(table.LiveCount(), 0u) << "the twin survived its own destroy notice";
EXPECT_TRUE(FakeSlotTable::ReleaseTwinByHandle(handle));
EXPECT_EQ(table.LiveCount(), 0u) << "the twin survived its own release";
EXPECT_EQ(table.FindByHandle(handle), nullptr);
EXPECT_EQ(table.Find(object.get()), nullptr);
EXPECT_EQ(slots.LiveCount(MG_Pipe::MGPipeKind::Query), liveBefore)
<< "the slot was not returned to the allocator";
// Idempotent: a repeated release finds nothing and says so.
EXPECT_FALSE(FakeSlotTable::ReleaseTwinByHandle(handle));
slots.Free(kKind, handle);
EXPECT_EQ(slots.LiveCount(kKind), liveBefore) << "the slot was not returned to the allocator";
// Idempotent: the allocator no longer maps the id, so a repeated notice frees nothing and
// says so - and it says so for every holder, since the notice is about the object and not
// about a table.
EXPECT_FALSE(FakeSlotTable::OnFrontendObjectDestroyed(object->GetLifetimeId()));
// A slot minted for an object that NO table holds any more - the fixture's `table = {}`
// drops entries without freeing - still goes back when the object dies: the id is dead and
// cannot be acquired again, so keeping the slot would be the process-lifetime leak the
// review named.
auto orphaned = MakeShared<FakeStateObject>(0x1E2A0002ull);
{
FakeSlotTable transient;
transient.GetOrCreate(orphaned) = MakeShared<FakeBackendObject>();
}
EXPECT_EQ(slots.LiveCount(MG_Pipe::MGPipeKind::Query), liveBefore + 1u);
orphaned.reset();
EXPECT_TRUE(FakeSlotTable::OnFrontendObjectDestroyed(0x1E2A0002ull))
<< "a slot no holder had an entry for was left allocated";
EXPECT_EQ(slots.LiveCount(MG_Pipe::MGPipeKind::Query), liveBefore);
// The successor at the recycled slot, and then a LATE release naming the dead generation.
const MG_Pipe::MGPipeHandle successor = MintFor(kKind, 0x1E2A0002ull);
ASSERT_EQ(successor.Slot, handle.Slot);
table.GetOrCreate(successor) = MakeShared<FakeBackendObject>();
EXPECT_FALSE(FakeSlotTable::ReleaseTwinByHandle(handle))
<< "a release for a dead generation dropped the slot's next owner's twin";
EXPECT_NE(table.FindByHandle(successor), nullptr);
EXPECT_TRUE(FakeSlotTable::ReleaseTwinByHandle(successor));
slots.Free(kKind, successor);
}
// The firing side of e2, on ALL SIX re-keyed object classes - the round-3 review's MAJOR 3.
@@ -3726,7 +3631,7 @@ TEST(DirectGLESSlotTable, EveryReKeyedObjectClassAnnouncesItsOwnDeath) {
static Vector<std::pair<MG_Pipe::MGPipeKind, Uint64>> notices;
notices.clear();
const MG_State::GLState::StateObjectDeathOps recording = {
.OnDestroyed = [](MG_Pipe::MGPipeKind kind, Uint64 lifetimeId) {
.OnDestroyed = [](MG_Pipe::MGPipeKind kind, Uint64 lifetimeId, MG_Pipe::MGPipeHandle) {
notices.emplace_back(kind, lifetimeId);
},
};
@@ -3837,48 +3742,50 @@ namespace {
// and that a successor object landing on the freed slot gets a different Gen while the
// predecessor's handle resolves to nothing.
template <typename Registry, typename MakeObject>
void ExpectTheHandleArmDrivesThisKind(const char* kindName, Registry& registry, MakeObject make) {
void ExpectTheHandleArmDrivesThisKind(const char* kindName, MobileGL::MG_Pipe::MGPipeKind kind,
Registry& registry, MakeObject make) {
using namespace MobileGL;
auto first = make();
ASSERT_NE(first, nullptr) << kindName;
auto& firstTwin = registry.GetOrCreate(first);
ASSERT_NE(MG_State::GLState::GetStateObjectDeathOps(), nullptr)
<< kindName << ": twinning an object did not install the death-notice consumer";
const MG_Pipe::MGPipeHandle firstHandle = registry.HandleOf(first.get());
ASSERT_FALSE(MG_Pipe::MGPipeHandleIsNull(firstHandle))
<< kindName << ": GetOrCreate on the real registry minted no handle, so this kind is "
"not running on the {slot, gen} arm at all";
EXPECT_EQ(registry.Find(first.get()), &firstTwin)
<< kindName << ": Find resolved a different twin slot than GetOrCreate handed back";
EXPECT_EQ(registry.FindByHandle(firstHandle), &firstTwin)
<< kindName << ": the handle does not address the twin GetOrCreate handed back";
// The frontend object dies. NOTHING below sweeps - the destructor's own notice is the
// only thing that can free the slot, which is what makes this the e2/e3 pair end to end.
// The client's mint (the record would carry this handle), then the backend's adoption.
const Uint64 firstLifetimeId = first->GetLifetimeId();
const MG_Pipe::MGPipeHandle firstHandle = MG_Pipe::MGPipeSlots().Acquire(kind, firstLifetimeId);
ASSERT_FALSE(MG_Pipe::MGPipeHandleIsNull(firstHandle)) << kindName;
auto* const firstTwin = registry.GetOrCreateByHandle(firstHandle);
ASSERT_NE(firstTwin, nullptr)
<< kindName << ": the real registry refused a handle the client minted, so this kind is "
"not running on the {slot, gen} arm at all";
ASSERT_NE(MG_State::GLState::GetStateObjectDeathOps(), nullptr)
<< kindName << ": no death-notice consumer is installed";
EXPECT_EQ(registry.FindByHandle(firstHandle), firstTwin)
<< kindName << ": the handle does not address the twin GetOrCreateByHandle handed back";
// The frontend object dies. NOTHING below sweeps - the destructor's own notice (which
// carries the handle) releases the twin and the client frees the slot after it, which is
// what makes this the e2/e3 pair end to end.
first.reset();
EXPECT_EQ(registry.FindByHandle(firstHandle), nullptr)
<< kindName << ": the twin outlived the announced death of its object";
EXPECT_FALSE(registry.DestroyByLifetimeId(firstLifetimeId))
EXPECT_TRUE(MG_Pipe::MGPipeHandleIsNull(MG_Pipe::MGPipeSlots().FindByLifetimeId(kind, firstLifetimeId)))
<< kindName << ": the slot was still held after its object announced its death";
auto second = make();
ASSERT_NE(second, nullptr) << kindName;
auto& secondTwin = registry.GetOrCreate(second);
const MG_Pipe::MGPipeHandle secondHandle = registry.HandleOf(second.get());
const MG_Pipe::MGPipeHandle secondHandle =
MG_Pipe::MGPipeSlots().Acquire(kind, second->GetLifetimeId());
ASSERT_FALSE(MG_Pipe::MGPipeHandleIsNull(secondHandle)) << kindName;
EXPECT_EQ(secondHandle.Slot, firstHandle.Slot)
<< kindName << ": the freed slot was not handed back, so this walk did not exercise "
"the recycle it exists to test";
EXPECT_NE(secondHandle.Gen, firstHandle.Gen)
<< kindName << ": Gen did not move on slot reuse - the predecessor's handle would "
"resolve to the successor's twin, which is the ABA the address key "
"could only paper over";
"resolve to the successor's twin";
auto* const secondTwin = registry.GetOrCreateByHandle(secondHandle);
ASSERT_NE(secondTwin, nullptr) << kindName;
EXPECT_EQ(registry.FindByHandle(firstHandle), nullptr)
<< kindName << ": the STALE handle resolved to a twin";
EXPECT_EQ(registry.FindByHandle(secondHandle), &secondTwin) << kindName;
EXPECT_EQ(registry.FindByHandle(secondHandle), secondTwin) << kindName;
second.reset();
EXPECT_EQ(registry.FindByHandle(secondHandle), nullptr) << kindName;
@@ -3897,18 +3804,18 @@ TEST(DirectGLESSlotTable, EverySwitchedOverKindResolvesItsTwinThroughTheHandleAr
"null handle, so there is no {slot, gen} walk to drive";
}
ExpectTheHandleArmDrivesThisKind("Texture", TextureImpl::g_backendTextureObjects, [] {
ExpectTheHandleArmDrivesThisKind("Texture", MG_Pipe::MGPipeKind::Texture, TextureImpl::g_backendTextureObjects, [] {
return SharedPtr<ITextureObject>(MakeShared<TextureObject2D>(0u));
});
ExpectTheHandleArmDrivesThisKind("Framebuffer", FramebufferImpl::g_backendFramebufferObjects,
ExpectTheHandleArmDrivesThisKind("Framebuffer", MG_Pipe::MGPipeKind::Framebuffer, FramebufferImpl::g_backendFramebufferObjects,
[] { return MakeShared<FramebufferObject>(1u); });
ExpectTheHandleArmDrivesThisKind("Renderbuffer", RenderbufferImpl::g_backendRenderbufferObjects,
ExpectTheHandleArmDrivesThisKind("Renderbuffer", MG_Pipe::MGPipeKind::Renderbuffer, RenderbufferImpl::g_backendRenderbufferObjects,
[] { return MakeShared<RenderbufferObject>(0u); });
ExpectTheHandleArmDrivesThisKind("SamplerCso", SamplerImpl::g_backendSamplerObjects,
ExpectTheHandleArmDrivesThisKind("SamplerCso", MG_Pipe::MGPipeKind::SamplerCso, SamplerImpl::g_backendSamplerObjects,
[] { return MakeShared<SamplerObject>(0u); });
ExpectTheHandleArmDrivesThisKind("ShaderCso", PrgramImpl::g_backendProgramObjects,
ExpectTheHandleArmDrivesThisKind("ShaderCso", MG_Pipe::MGPipeKind::ShaderCso, PrgramImpl::g_backendProgramObjects,
[] { return MakeShared<ProgramObject>(0u); });
ExpectTheHandleArmDrivesThisKind("VertexElementsCso", VertexArrayImpl::g_backendVertexArrayObjects,
ExpectTheHandleArmDrivesThisKind("VertexElementsCso", MG_Pipe::MGPipeKind::VertexElementsCso, VertexArrayImpl::g_backendVertexArrayObjects,
[] { return MakeShared<VertexArrayObject>(0u); });
// P3a: the SEVENTH table, and the only one the helper above cannot drive - which is the
@@ -4004,10 +3911,6 @@ TEST(DirectGLESSlotTable, EverySwitchedOverKindResolvesItsTwinThroughTheHandleAr
const BackendSamplerViewObject* const firstRaw = firstTwin.get();
EXPECT_EQ(table.FindByHandle(first), &firstTwin)
<< "SamplerViewCso: the handle does not address the twin GetOrCreate handed back";
EXPECT_EQ(table.HandleOf(owner.get()), first)
<< "SamplerViewCso: HandleOf did not resolve the view minted off this texture's "
"lifetime id, so a backend path that still arrives holding the object cannot find "
"its view";
const SharedPtr<BackendSamplerViewObject> released = table.ReleaseByHandle(first);
EXPECT_NE(released, nullptr) << "SamplerViewCso: delete_sampler_view found no twin to retire";
@@ -4106,21 +4009,21 @@ TEST(DirectGLESSlotTable, ADeathNoticeForEveryP4aKindIsIdempotent) {
g_backendSamplerViews.GetOrCreate(view) = MakeShared<BackendSamplerViewObject>();
ASSERT_NE(g_backendSamplerViews.FindByHandle(view), nullptr);
ops->OnDestroyed(MG_Pipe::MGPipeKind::SamplerViewCso, lifetimeId);
ops->OnDestroyed(MG_Pipe::MGPipeKind::SamplerViewCso, lifetimeId, view);
EXPECT_EQ(g_backendSamplerViews.FindByHandle(view), nullptr)
<< "the sampler-view notice left the twin behind, so the slot's next owner would "
"inherit a dead texture's resolved view";
// The second path, in the order the client's helper runs it: the notice, then the Free
// (since P13 the notice releases by handle and never frees - the slot is the client's).
// A redundant notice before and after the Free finds nothing to release.
ops->OnDestroyed(MG_Pipe::MGPipeKind::SamplerViewCso, lifetimeId, view);
MG_Pipe::MGPipeSlots().Free(MG_Pipe::MGPipeKind::SamplerViewCso, view);
ops->OnDestroyed(MG_Pipe::MGPipeKind::SamplerViewCso, lifetimeId, view);
EXPECT_EQ(g_backendSamplerViews.FindByHandle(view), nullptr);
EXPECT_TRUE(MG_Pipe::MGPipeHandleIsNull(
MG_Pipe::MGPipeSlots().FindByLifetimeId(MG_Pipe::MGPipeKind::SamplerViewCso, lifetimeId)))
<< "the notice did not return the slot";
// The second path. The client's helper calls Free right after raising the notice, and a
// Free on a slot that is no longer live at that generation is a proven no-op
// (SlotAllocator.cpp:117-119) - so this is the shape that actually ships, twice over.
ops->OnDestroyed(MG_Pipe::MGPipeKind::SamplerViewCso, lifetimeId);
MG_Pipe::MGPipeSlots().Free(MG_Pipe::MGPipeKind::SamplerViewCso, view);
ops->OnDestroyed(MG_Pipe::MGPipeKind::SamplerViewCso, lifetimeId);
EXPECT_EQ(g_backendSamplerViews.FindByHandle(view), nullptr);
<< "the Free did not return the slot";
// And the slot really is back: a successor gets it with a moved generation, which is
// the property a double free would break by skipping one.
@@ -4154,15 +4057,20 @@ TEST(DirectGLESSlotTable, ADeathNoticeForEveryP4aKindIsIdempotent) {
auto sampler = MakeShared<SamplerObject>(0u);
auto program = MakeShared<ProgramObject>(0u);
TextureImpl::g_backendTextureObjects.GetOrCreate(SharedPtr<ITextureObject>(texture));
RenderbufferImpl::g_backendRenderbufferObjects.GetOrCreate(renderbuffer);
FramebufferImpl::g_backendFramebufferObjects.GetOrCreate(framebuffer);
SamplerImpl::g_backendSamplerObjects.GetOrCreate(sampler);
PrgramImpl::g_backendProgramObjects.GetOrCreate(program);
// The client's mint, then the backend's adoption by that handle.
const auto handleFor = [](MG_Pipe::MGPipeKind kind, Uint64 lifetimeId) {
return MG_Pipe::MGPipeSlots().FindByLifetimeId(kind, lifetimeId);
return MG_Pipe::MGPipeSlots().Acquire(kind, lifetimeId);
};
(void)TextureImpl::g_backendTextureObjects.GetOrCreateByHandle(
handleFor(MG_Pipe::MGPipeKind::Texture, texture->GetLifetimeId()));
(void)RenderbufferImpl::g_backendRenderbufferObjects.GetOrCreateByHandle(
handleFor(MG_Pipe::MGPipeKind::Renderbuffer, renderbuffer->GetLifetimeId()));
(void)FramebufferImpl::g_backendFramebufferObjects.GetOrCreateByHandle(
handleFor(MG_Pipe::MGPipeKind::Framebuffer, framebuffer->GetLifetimeId()));
(void)SamplerImpl::g_backendSamplerObjects.GetOrCreateByHandle(
handleFor(MG_Pipe::MGPipeKind::SamplerCso, sampler->GetLifetimeId()));
(void)PrgramImpl::g_backendProgramObjects.GetOrCreateByHandle(
handleFor(MG_Pipe::MGPipeKind::ShaderCso, program->GetLifetimeId()));
cases.push_back({"Texture", MG_Pipe::MGPipeKind::Texture, texture->GetLifetimeId(),
handleFor(MG_Pipe::MGPipeKind::Texture, texture->GetLifetimeId())});
cases.push_back({"Renderbuffer", MG_Pipe::MGPipeKind::Renderbuffer, renderbuffer->GetLifetimeId(),
@@ -4186,9 +4094,9 @@ TEST(DirectGLESSlotTable, ADeathNoticeForEveryP4aKindIsIdempotent) {
EXPECT_TRUE(MG_Pipe::MGPipeHandleIsNull(
MG_Pipe::MGPipeSlots().FindByLifetimeId(one.kind, one.lifetimeId)))
<< one.name << ": the object's own death did not return its slot";
// Second and third deliveries: nothing to resolve, nothing to free, no abort.
ops->OnDestroyed(one.kind, one.lifetimeId);
ops->OnDestroyed(one.kind, one.lifetimeId);
// Second and third deliveries of the dead handle: nothing to release, no abort.
ops->OnDestroyed(one.kind, one.lifetimeId, one.handle);
ops->OnDestroyed(one.kind, one.lifetimeId, one.handle);
EXPECT_TRUE(MG_Pipe::MGPipeHandleIsNull(
MG_Pipe::MGPipeSlots().FindByLifetimeId(one.kind, one.lifetimeId)))
<< one.name << ": a redundant notice resurrected a mapping";
@@ -4227,10 +4135,10 @@ TEST(DirectGLESSlotTable, ASavedCopyOfARealRegistryDropsTheTwinOnTheSameNotice)
auto& registry = TextureImpl::g_backendTextureObjects;
SharedPtr<ITextureObject> texture = MakeShared<TextureObject2D>(0u);
auto& twin = registry.GetOrCreate(texture);
(void)twin;
const MG_Pipe::MGPipeHandle handle = registry.HandleOf(texture.get());
const MG_Pipe::MGPipeHandle handle =
MG_Pipe::MGPipeSlots().Acquire(MG_Pipe::MGPipeKind::Texture, texture->GetLifetimeId());
ASSERT_FALSE(MG_Pipe::MGPipeHandleIsNull(handle));
ASSERT_NE(registry.GetOrCreateByHandle(handle), nullptr);
// The fixture's shape: copy the registry while the twin is live.
auto saved = registry;
@@ -4241,7 +4149,6 @@ TEST(DirectGLESSlotTable, ASavedCopyOfARealRegistryDropsTheTwinOnTheSameNotice)
EXPECT_EQ(registry.FindByHandle(handle), nullptr) << "the registry global kept the twin";
EXPECT_EQ(saved.FindByHandle(handle), nullptr)
<< "the saved copy kept the dead texture's twin - the dispatcher told one holder only";
EXPECT_FALSE(registry.DestroyByLifetimeId(0)) << "sanity: a null id frees nothing";
}
+4 -148
View File
@@ -2343,36 +2343,6 @@ namespace {
MGPipeKind::Texture>;
} // namespace
TEST(RemoteGuards, SlotTableHandleOfFromTheApplyThreadIsFatalByName) {
const auto child = RunInChild([] {
StartControlSession();
Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting([](void*) {
MG_State::pGLContext = MakeUnique<MG_State::GLState::GLContext>();
TestTextureTable table;
MG_State::GLState::TextureObject2D tex(44);
table.HandleOf(&tex);
return MOBILEGL_OK;
}, nullptr);
ClientSessionInstance().Stop();
});
ExpectNamedAbort(child, "Fatal{RoleViolation, \"MGPipeSlots\"}");
}
TEST(RemoteGuards, SlotTableMintingGetOrCreateFromTheApplyThreadIsFatalByName) {
const auto child = RunInChild([] {
StartControlSession();
Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting([](void*) {
MG_State::pGLContext = MakeUnique<MG_State::GLState::GLContext>();
TestTextureTable table;
auto tex = MakeShared<MG_State::GLState::TextureObject2D>(45);
table.GetOrCreate(tex);
return MOBILEGL_OK;
}, nullptr);
ClientSessionInstance().Stop();
});
ExpectNamedAbort(child, "Fatal{RoleViolation, \"MGPipeSlots\"}");
}
// The four accessor drives share one shape: the BufferObject is constructed on the CLIENT
// thread (its own resource_create publication is a legal client-side emit), and only the
// accessor runs on the apply thread, where it must die at the accessor's own guard.
@@ -2385,9 +2355,8 @@ MGL_BUFFER_GUARD_TEST(BufferSyncPersistentMappedRangeFromTheApplyThreadIsFatalBy
MGL_BUFFER_GUARD_TEST(BufferHasDefinedContentFromTheApplyThreadIsFatalByName, 707, (void)buffer.HasDefinedContent())
#undef MGL_BUFFER_GUARD_TEST
// Historical P5d/P5e probes remain named for compatibility. P5f retires both
// scope exemptions, including barriered records; all allocator access on apply is
// client-memory access regardless of whether that memory happens to be stable.
// All allocator access on apply is client-memory access regardless of whether that memory
// happens to be stable (P5f), so the allocator refuses it by name.
TEST(RemoteGuards, AnAllocatorProbeFromTheApplyThreadOutsideEveryScopeIsFatalByName) {
const auto child = RunInChild([] {
StartControlSession();
@@ -2402,38 +2371,8 @@ TEST(RemoteGuards, AnAllocatorProbeFromTheApplyThreadOutsideEveryScopeIsFatalByN
ExpectNamedAbort(child, "Fatal{RoleViolation, \"MGPipeSlots\"}");
}
// Construct on the client and run only HandleOf on apply. The false barrier stamp
// keeps the original P5e scenario; the tests below add the old barriered loophole.
TEST(RemoteGuards, AnAllocatorProbeInsideAnExemptionScopeFromTheApplyThreadIsFatalEvenAtTheOldDebtSites) {
struct Probe {
TestTextureTable* table;
MG_State::GLState::TextureObject2D* texture;
};
const auto child = RunInChild([] {
StartControlSession();
MG_State::GLState::TextureObject2D texture(46);
TestTextureTable table;
Probe probe{&table, &texture};
Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting(
+[](void* self) -> MobileGLResult {
auto& probe = *static_cast<Probe*>(self);
// The record being applied is one the client did NOT park behind. The stamp is
// the thread_local the sink writes (ID-103), so setting it here puts this
// thread in exactly the state an unbarriered apply is in.
MG_Pipe::MGPipeApplierSetCurrentRecordBarriered(false);
const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry;
probe.table->HandleOf(probe.texture);
return MOBILEGL_OK;
},
&probe);
ClientSessionInstance().Stop();
});
ExpectNamedAbort(child, "Fatal{RoleViolation, \"MGPipeSlots\"}");
}
// P5f (fr): barriers and old named scopes no longer admit frontend identity.
// Construct the object and table on the client so each death comes from the exact
// probed member, not a constructor's resource_create or a missing record.
// P5f (fr): a barriered record does not admit the client allocator either (the named scopes
// that once exempted it were deleted at P13, with the last frontend-keyed backend entry).
TEST(RemoteGuards, BarrieredLegacyScopesCannotExemptAllocator) {
for (const auto backend : {BackendType::DirectGLES, BackendType::DirectVulkan}) {
const auto child = RunInChild([backend] {
@@ -2442,8 +2381,6 @@ TEST(RemoteGuards, BarrieredLegacyScopesCannotExemptAllocator) {
Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting(+[](void* self) -> MobileGLResult {
MG_Pipe::MGPipeApplierSetCurrentRecordBarriered(true);
MG_Config::ActiveBackendType = *static_cast<BackendType*>(self);
const MG_Pipe::MGPipeFrontendKeyedRegistryScope registryScope;
const MG_Pipe::MagmaP7AllocatorDebtScope magmaScope;
// HighWater had no per-method guard in the old implementation.
(void)MGPipeSlots().HighWater(MGPipeKind::Texture);
return MOBILEGL_OK;
@@ -2454,71 +2391,6 @@ TEST(RemoteGuards, BarrieredLegacyScopesCannotExemptAllocator) {
}
}
TEST(RemoteGuards, BarrieredFrontendRegistryMembersRefuseBothLegacyScopes) {
struct Probe {
TestTextureTable* table;
SharedPtr<MG_State::GLState::TextureObject2D>* texture;
MGPipeHandle handle;
int operation;
};
const char* members[] = {"HandleOf", "GetOrCreate(StatePtr)", "NoteStateForHandle", "StateForHandle"};
for (int operation = 0; operation != 4; ++operation) {
SCOPED_TRACE(members[operation]);
const auto child = RunInChild([operation] {
StartControlSession();
auto texture = MakeShared<MG_State::GLState::TextureObject2D>(47);
TestTextureTable table;
table.GetOrCreate(texture) = MakeShared<FakeTwin>();
Probe probe{&table, &texture, table.HandleOf(texture.get()), operation};
Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting(+[](void* self) -> MobileGLResult {
auto& probe = *static_cast<Probe*>(self);
MG_Pipe::MGPipeApplierSetCurrentRecordBarriered(true);
MG_Config::ActiveBackendType = BackendType::DirectVulkan;
const MG_Pipe::MGPipeFrontendKeyedRegistryScope registryScope;
const MG_Pipe::MagmaP7AllocatorDebtScope magmaScope;
switch (probe.operation) {
case 0: probe.table->HandleOf(probe.texture->get()); break;
case 1: probe.table->GetOrCreate(*probe.texture); break;
case 2: probe.table->NoteStateForHandle(probe.handle, *probe.texture); break;
case 3: probe.table->StateForHandle(probe.handle); break;
}
return MOBILEGL_OK;
}, &probe);
ClientSessionInstance().Stop();
});
ExpectNamedAbort(child, "Fatal{RoleViolation, \"MGPipeSlots\"}");
EXPECT_NE(child.Log.find(std::string{"BackendSlotTable::"} + members[operation]), std::string::npos);
}
}
TEST(RemoteGuards, LegacyRegistryWrapperCannotExposeFrontendKeysOnApply) {
using Registry = MG_Backend::DirectGLES::StateBackendObjectRegistry<
MG_State::GLState::TextureObject2D, FakeTwin, MGPipeKind::Texture>;
struct Probe { Registry* registry; SharedPtr<MG_State::GLState::TextureObject2D>* texture; int operation; };
for (int operation = 0; operation != 3; ++operation) {
const auto child = RunInChild([operation] {
StartControlSession();
auto texture = MakeShared<MG_State::GLState::TextureObject2D>(48);
Registry registry;
Probe probe{&registry, &texture, operation};
Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting(+[](void* self) -> MobileGLResult {
auto& probe = *static_cast<Probe*>(self);
MG_Pipe::MGPipeApplierSetCurrentRecordBarriered(true);
const MG_Pipe::MGPipeFrontendKeyedRegistryScope scope;
switch (probe.operation) {
case 0: probe.registry->Find(probe.texture->get()); break;
case 1: probe.registry->GetOrCreate(*probe.texture); break;
case 2: (void)probe.registry->begin(); break;
}
return MOBILEGL_OK;
}, &probe);
ClientSessionInstance().Stop();
});
ExpectNamedAbort(child, "Fatal{RoleViolation, \"MGPipeSlots\"}");
EXPECT_NE(child.Log.find("BackendSlotTable::Registry."), std::string::npos);
}
}
TEST(RemoteGuards, HandleRegistryMembersWorkOnBarrieredAndUnbarrieredApply) {
const auto child = RunInChild([] {
StartControlSession();
@@ -2547,22 +2419,6 @@ TEST(RemoteGuards, HandleRegistryMembersWorkOnBarrieredAndUnbarrieredApply) {
EXPECT_EQ(WEXITSTATUS(child.Status), 0) << child.Log;
}
// A marker held by the GL thread never affects the apply guard either.
TEST(RemoteGuards, AnExemptionScopeHeldOnTheGLThreadDoesNotExemptTheApplyThread) {
const auto child = RunInChild([] {
StartControlSession();
const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry;
Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting(
+[](void*) -> MobileGLResult {
MG_Pipe::MGPipeRefuseAllocatorFromApplyThread("FindByLifetimeId");
return MOBILEGL_OK;
},
nullptr);
ClientSessionInstance().Stop();
});
ExpectNamedAbort(child, "Fatal{RoleViolation, \"MGPipeSlots\"}");
}
TEST(RemoteGuards, CapsMirrorFallbackWithNoServerBackendIsFatalByName) {
const auto child = RunInChild([] {
MG_Config::Transport = MG_Config::TransportMode::InProcess;
-43
View File
@@ -2479,49 +2479,6 @@ TEST(StagedShadowProductionTest, TheStreamingIdiomThroughAPoolReuseIsUploadedNot
"whole-store refusal must key on the descriptor's HasDefinedContent); the log says: "
<< appended;
}
namespace {
// The drive below, in a FORKED CHILD that brings the server up itself (the integration
// harness's pre-flight shape), so a Fatal on the apply thread ends the child and not the
// case. Exit 8 = the child's bring-up failed; exit 0 = the apply-thread twin mint ran,
// which is the refusal this case pins having been removed.
[[noreturn]] void RunFrontendFramebufferDeathOnApplyThreadAndExit() {
EglServerFixture fixture;
if (!fixture.BringUp().empty() || !fixture.MakeCurrent()) ::_exit(8);
namespace GLES = MG_Backend::DirectGLES;
auto framebuffer = MakeShared<MG_State::GLState::FramebufferObject>(901u);
(void)OnApply([&] {
auto& twin = GLES::FramebufferImpl::g_backendFramebufferObjects.GetOrCreate(framebuffer);
twin = MakeShared<GLES::FramebufferImpl::BackendFramebufferObject>();
});
framebuffer.reset();
::_exit(0);
}
} // namespace
// P5c (hd, CONTRACT-P5C §3.1 / §6 layer 1): this case's original drive - mint the framebuffer
// twin on the apply thread off the frontend object's lifetime id, then let the frontend death
// notice reach the in-process death switch there - is refused by name at the first step:
// the minting GetOrCreate(StatePtr) is monolith glue and the apply-thread call is
// Fatal{RoleViolation, "MGPipeSlots"}. The deletion-on-owner semantics it pinned return with
// ct's object_death record (the framebuffer family's first wire delete), whose sink releases
// the twin BY HANDLE. The name stays; the pin is now the refusal that stands until ct lands.
TEST(ServerLoopEglTest, FrontendFramebufferDeathDeletesOnTheContextOwner) {
MG_Config::Features.PipePush |= MG_Pipe::kMGPipeSubsystemEsprytSlots;
{
EglServerFixture probe;
MGL_EGL_BRING_UP_OR_BAIL(probe);
probe.TearDown();
}
const SizeT mark = ReadLog().size();
EXPECT_EXIT(RunFrontendFramebufferDeathOnApplyThreadAndExit(), ::testing::KilledBySignal(SIGABRT), ".*")
<< "the apply-thread mint of a frontend framebuffer's twin was not refused; exit 8 is the "
"child's own bring-up failing";
const std::string appended = ReadLogFrom(mark);
EXPECT_NE(appended.find("Fatal{RoleViolation, \"MGPipeSlots\"}"), std::string::npos)
<< "the abort was not the allocator guard's; the log says: " << appended;
}
#endif // !_WIN32
// =====================================================================================
+3 -16
View File
@@ -5,24 +5,14 @@
# base commit : <unknown>
# configure : -DMOBILEGL_BUILD_DISAGGREGATED=ON -DMOBILEGL_BUILD_DISAGGREGATED_INPROC=ON -DCMAKE_BUILD_TYPE=Release
# build dir : /home/swung/mgl-p13-split
# count : 49
# count : 36
#
# One demangled symbol per line. A trailing ` # note` is an annotation the parser
# ignores; `# P13` marks a symbol that cannot fall before P13 establishes module
# target boundaries. The LIST is the baseline, not the count: a ratchet that
# ignores. `# P13` (debts only P13's module work could clear) is refused since
# P13 closed them. The LIST is the baseline, not the count: a ratchet that
# stored only a number would go green on the day one symbol is cleared and another
# added (ID-P7-7).
MobileGL::MG_Impl::GLImpl::FramebufferImpl::pDefaultFramebufferInfo
MobileGL::MG_Pipe::MGPipeApplierIsUnbarrieredApply() # P13
MobileGL::MG_Pipe::MGPipeRefuseAllocatorFromApplyThread(char const*) # P13
MobileGL::MG_Pipe::MGPipeRefuseFrontendKeyedRegistryFromApplyThread(char const*) # P13
MobileGL::MG_Pipe::MGPipeSlotAllocator::Acquire(MobileGL::MG_Pipe::MGPipeKind, unsigned long) # P13
MobileGL::MG_Pipe::MGPipeSlotAllocator::FindByLifetimeId(MobileGL::MG_Pipe::MGPipeKind, unsigned long) const # P13
MobileGL::MG_Pipe::MGPipeSlotAllocator::Free(MobileGL::MG_Pipe::MGPipeKind, MobileGL::MG_Pipe::MGPipeHandle) # P13
MobileGL::MG_Pipe::MGPipeSlots() # P13
MobileGL::MG_Pipe::PipeInputs::GetBufferBindingPointCount(MobileGL::BufferTarget) const
MobileGL::MG_Pipe::PipeInputs::HasOpenTransformFeedbackSpan(unsigned long) const # P13 monolith arm: MGB_CTX accessor / GL readback packer (CONTRACT-P7 §12)
MobileGL::MG_Pipe::PipeInputs::InvalidateCompileEnv() # P13 monolith arm: MGB_CTX accessor / GL readback packer (CONTRACT-P7 §12)
MobileGL::MG_Pipe::PipeInputs::IsLive() const
MobileGL::MG_Pipe::PipeInputs::RecordError(MobileGL::ErrorCode, std::unique_ptr<MobileGL::ErrorInfo, std::default_delete<MobileGL::ErrorInfo> >)
MobileGL::MG_Remote::Client::AdoptTierIsEmulate()
@@ -50,14 +40,11 @@ MobileGL::MG_State::GLState::DecodeProgramArchive(unsigned char const*, unsigned
MobileGL::MG_State::GLState::GetBufferBackendOps()
MobileGL::MG_State::GLState::ProgramArtifactsSchemaFingerprint()
MobileGL::MG_State::GLState::ProgramObject::JoinPendingLink() const
MobileGL::MG_State::GLState::RenderbufferObject::GetExternalIndex() const
MobileGL::MG_State::GLState::SamplerObject::GetAllSamplerParameters() const
MobileGL::MG_State::GLState::SamplerObject::GetExternalIndex() const
MobileGL::MG_State::GLState::SamplerObject::GetLifetimeId() const
MobileGL::MG_State::GLState::SamplerObject::GetVersion() const
MobileGL::MG_State::GLState::SetBufferBackendOps(MobileGL::MG_State::GLState::BufferBackendOps const*)
MobileGL::MG_State::GLState::TextureObjectBuffer::GetBufferBindingSlot(MobileGL::TextureUploadTarget)
MobileGL::MG_State::GLState::TextureUnit::GetAllBindingSlots()
MobileGL::MG_State::GLState::TextureUnit::GetSamplerObject() const
MobileGL::MG_State::GLState::VertexArrayObject::GetAllAttributes() const
MobileGL::MG_State::GLState::VertexArrayObject::GetAttribute(unsigned int) const
+29 -5
View File
@@ -40,6 +40,11 @@ regression this gate exists to catch: new backend code reaching into a frontend
symbols that disappeared, with an instruction to re-baseline in the same commit. Progress is
never a red build; new reach always is.
THE P13 EXIT GATE. A baseline line annotated `# P13` named a debt that only P13's module work
could clear. P13 cleared every one, so --assert-monotone now also fails on ANY `# P13`
annotation left in (or put back into) the baseline: that class of debt has no later phase to
wait for.
GUARD RAILS. The build must be a disaggregated one
(-DMOBILEGL_BUILD_DISAGGREGATED=ON -DMOBILEGL_BUILD_DISAGGREGATED_INPROC=ON; every build pushes
since P13 W3a, so MOBILEGL_PIPE_PUSH is no longer an option to check): with the option OFF, MG_Remote is not compiled at all, the SERVER set
@@ -176,11 +181,16 @@ A6_TOTAL = 184
# known object path rather than by splitting the whole line.
NM_BODY_RE = re.compile(r"^\s*([0-9a-fA-F]*)\s+([A-Za-z?])\s(.+)$")
# A baseline line may carry a trailing ` # note` annotation (ID-P7-7 asks for `# P13` on the
# symbols that cannot fall before P13). No demangled C++ name contains " #", so splitting on
# A baseline line may carry a trailing ` # note` annotation (ID-P7-7 asked for `# P13` on the
# symbols that could not fall before P13). No demangled C++ name contains " #", so splitting on
# the first occurrence is unambiguous.
BASELINE_ANNOTATION = " #"
def p13_debts(notes):
"""The baseline symbols still annotated `# P13` - none may remain after P13 (exit gate)."""
return sorted(symbol for symbol, note in notes.items() if note.split()[:1] == ["P13"])
REQUIRED_FLAGS = (
("MOBILEGL_BUILD_DISAGGREGATED", "ON"),
("MOBILEGL_BUILD_DISAGGREGATED_INPROC", "ON"),
@@ -628,6 +638,11 @@ def self_test():
problems.append("baseline annotation not read: {}".format(notes))
if len(notes) != 2:
problems.append("baseline annotations: {}".format(notes))
if p13_debts(notes) != ["MobileGL::MG_Pipe::MGPipeSlots()",
"MobileGL::Vec4<int>::Vec4(int, int, int, int)"]:
problems.append("P13 exit gate did not name both `# P13` lines: {}".format(p13_debts(notes)))
if p13_debts({"x": "P13x", "y": "kept for P14"}):
problems.append("P13 exit gate matched a note that is not a `# P13` annotation")
finally:
os.unlink(tmp)
@@ -808,6 +823,14 @@ def main():
say(" referred by: " + ", ".join(result["referrers"][symbol]))
if args.assert_monotone and new:
exit_code = 1
p13 = p13_debts(baseline_notes)
if p13:
say("{} baseline symbol(s) still carry a `# P13` annotation; P13 closed that class of "
"debt, so none may remain:".format(len(p13)))
for symbol in p13:
say(" P13: " + symbol)
if args.assert_monotone:
exit_code = 1
if not new and not gone:
say("ratchet: unchanged at {} symbol(s)".format(len(result["symbols"])))
@@ -850,7 +873,8 @@ def main():
say("json written to " + args.json)
if exit_code:
say("FAIL --assert-monotone: {} new frontend symbol(s) (named above)".format(len(new)))
say("FAIL --assert-monotone: {} new frontend symbol(s), {} `# P13` annotation(s) (named "
"above)".format(len(new), len(p13_debts(baseline_notes))))
return exit_code
@@ -871,8 +895,8 @@ def write_baseline(path, symbols, notes, cache_values, build_dir):
"# count : {}\n"
"#\n"
"# One demangled symbol per line. A trailing ` # note` is an annotation the parser\n"
"# ignores; `# P13` marks a symbol that cannot fall before P13 establishes module\n"
"# target boundaries. The LIST is the baseline, not the count: a ratchet that\n"
"# ignores. `# P13` (debts only P13's module work could clear) is refused since\n"
"# P13 closed them. The LIST is the baseline, not the count: a ratchet that\n"
"# stored only a number would go green on the day one symbol is cleared and another\n"
"# added (ID-P7-7).\n".format(
head, flags, (cache_values or {}).get("CMAKE_BUILD_TYPE", "?"), build_dir,