mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
[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:
@@ -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()");
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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?
|
||||
// =====================================================================================
|
||||
|
||||
@@ -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
@@ -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";
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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{®istry, &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;
|
||||
|
||||
@@ -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
|
||||
|
||||
// =====================================================================================
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user