mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-12 06:08:30 +09:00
[Fix, Test] (MG_Pipe, clientfb, clientsp): retire every emitter's entry at the object's death - the six death helpers freed the slot and told no emitter, so a dead-but-unrecycled texture handle still resolved to the freed ITextureObject* (the allocator's generation moves only at the next hand-out) and the drain list kept the level: glTexImage2D; glDeleteTextures; <any verb> called a virtual on freed memory from the next validate point (final review C-2); the helpers now forward to the texture, renderbuffer, framebuffer, sampler-view and shader-CSO emitters between the wire delete and the free, ResolveTexture refuses a dead slot loudly on IsLive, the sticky-mask producers stamp the generation they write under, and the delete-then-use sequence is pinned for every kind, for a recycled slot, and under MALLOC_PERTURB_ on both backends
This commit is contained in:
@@ -389,6 +389,26 @@ namespace MobileGL::MG_Pipe {
|
|||||||
|
|
||||||
// ---- what a unit case reads. The emitter builds INTO these and hands the applier the
|
// ---- what a unit case reads. The emitter builds INTO these and hands the applier the
|
||||||
// same objects, so "what was emitted" costs no copy. ----
|
// same objects, so "what was emitted" costs no copy. ----
|
||||||
|
// ---- the death half (P4a final review C-2) ----
|
||||||
|
//
|
||||||
|
// Called by the contract's death helper before the slot is freed (there is no wire
|
||||||
|
// delete for this kind, D-I2, so this is the only client-side thing a framebuffer's
|
||||||
|
// death has to do). The per-object Named latch is the entry: a recycled handle's Gen
|
||||||
|
// already refuses the stale latch, so this is hygiene rather than a fix - the rule
|
||||||
|
// (ID-8) is that whatever mints a handle retires everything it keeps under it at the
|
||||||
|
// death, and every P4a kind takes the same shape. Gen-keyed for a late notice.
|
||||||
|
void NoteFramebufferDied(MGPipeHandle handle) {
|
||||||
|
const SizeT slot = handle.Slot;
|
||||||
|
if (MGPipeHandleIsNull(handle) || slot >= m_named.size()) return;
|
||||||
|
if (m_named[slot].Gen == handle.Gen) m_named[slot] = NamedEntry{};
|
||||||
|
}
|
||||||
|
// "Does this emitter hold a Named-record latch for this handle at its generation."
|
||||||
|
Bool NamedRecordIsLatched(MGPipeHandle handle) const {
|
||||||
|
const SizeT slot = handle.Slot;
|
||||||
|
if (MGPipeHandleIsNull(handle) || slot >= m_named.size()) return false;
|
||||||
|
return m_named[slot].Has && m_named[slot].Gen == handle.Gen;
|
||||||
|
}
|
||||||
|
|
||||||
const MGPFramebufferState& LastDraw() const { return m_lastDraw; }
|
const MGPFramebufferState& LastDraw() const { return m_lastDraw; }
|
||||||
const MGPFramebufferState& LastRead() const { return m_lastRead; }
|
const MGPFramebufferState& LastRead() const { return m_lastRead; }
|
||||||
const MGPFramebufferState& LastNamed() const { return m_lastNamed; }
|
const MGPFramebufferState& LastNamed() const { return m_lastNamed; }
|
||||||
|
|||||||
@@ -1370,11 +1370,24 @@ namespace MobileGL::MG_Pipe {
|
|||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
// THE EMITTER IS TOLD BETWEEN THE WIRE DELETE AND THE FREE (P4a final review C-2), for
|
||||||
|
// every kind that keeps client state under a handle: a texture's drain entries, pointer,
|
||||||
|
// cache reference and latches; a renderbuffer's entry; a framebuffer's Named latch; a
|
||||||
|
// sampler view's and a shader CSO's record memo. Before this the six helpers freed the slot
|
||||||
|
// and told nobody, so the texture emitter kept the freed ITextureObject* and the level on
|
||||||
|
// the drain list, and `glTexImage2D; glDeleteTextures; <verb>` called a virtual on freed
|
||||||
|
// memory from the next validate point. The forward is the P3a shape
|
||||||
|
// (MGPipeEmitVertexElementsDestroyAndFree's emitter.NoteRecordDestroyed) applied to the
|
||||||
|
// five P4a kinds that have an entry to retire; the content-addressed sampler CSO keeps
|
||||||
|
// none per object (its death is the cache's LRU, ID-17). Unconditional in a push build,
|
||||||
|
// like the mints: the entries exist whether or not the family bit is set.
|
||||||
Bool MGPipeEmitSamplerViewCsoDestroyAndFree(Uint64 lifetimeId) {
|
Bool MGPipeEmitSamplerViewCsoDestroyAndFree(Uint64 lifetimeId) {
|
||||||
const MGPipeHandle handle =
|
const MGPipeHandle handle =
|
||||||
MGPipeSlots().FindByLifetimeId(MGPipeKind::SamplerViewCso, lifetimeId);
|
MGPipeSlots().FindByLifetimeId(MGPipeKind::SamplerViewCso, lifetimeId);
|
||||||
const Bool published =
|
const Bool published =
|
||||||
EmitDeleteIfPublished(MGPipeKind::SamplerViewCso, handle, &MGPipeApplyDeleteSamplerView);
|
EmitDeleteIfPublished(MGPipeKind::SamplerViewCso, handle, &MGPipeApplyDeleteSamplerView);
|
||||||
|
ForwardWhenWired<kMGPipeWiredSamplerSubsystem>(
|
||||||
|
MGPipeSamplerEmitterInstance(), [&](auto& emitter) { emitter.NoteRecordDestroyed(handle); });
|
||||||
// THE NOTICE IS RAISED FOR THIS KIND TOO, and the reason it once was not is wrong:
|
// THE NOTICE IS RAISED FOR THIS KIND TOO, and the reason it once was not is wrong:
|
||||||
// NotifyStateObjectDestroyed takes a KIND and a lifetime id, not an object
|
// NotifyStateObjectDestroyed takes a KIND and a lifetime id, not an object
|
||||||
// (StateObjectDeathNotice.h - one entry point for every kind rather than one ops table
|
// (StateObjectDeathNotice.h - one entry point for every kind rather than one ops table
|
||||||
@@ -1394,6 +1407,11 @@ namespace MobileGL::MG_Pipe {
|
|||||||
const MGPipeHandle handle = MGPipeSlots().FindByLifetimeId(MGPipeKind::Texture, lifetimeId);
|
const MGPipeHandle handle = MGPipeSlots().FindByLifetimeId(MGPipeKind::Texture, lifetimeId);
|
||||||
const Bool published =
|
const Bool published =
|
||||||
EmitDeleteIfPublished(MGPipeKind::Texture, handle, &MGPipeApplyResourceDestroy);
|
EmitDeleteIfPublished(MGPipeKind::Texture, handle, &MGPipeApplyResourceDestroy);
|
||||||
|
// The emitter retires its entry while the handle still resolves (C-2): the drain list
|
||||||
|
// drops the dead texture's levels, the raw pointer goes, the built-in sampler's cache
|
||||||
|
// reference is given back, the latches and the sticky mask are cleared.
|
||||||
|
ForwardWhenWired<kMGPipeWiredTextureSubsystem>(
|
||||||
|
MGPipeTextureEmitterInstance(), [&](auto& emitter) { emitter.NoteTextureDied(handle); });
|
||||||
NotifyAndFree(MGPipeKind::Texture, lifetimeId, handle);
|
NotifyAndFree(MGPipeKind::Texture, lifetimeId, handle);
|
||||||
// THE SAMPLER VIEW DIES WITH ITS TEXTURE, because it is minted off the same lifetime
|
// THE SAMPLER VIEW DIES WITH ITS TEXTURE, because it is minted off the same lifetime
|
||||||
// id: one SamplerViewCso per ITextureObject (D-F2), re-issued on the same handle
|
// id: one SamplerViewCso per ITextureObject (D-F2), re-issued on the same handle
|
||||||
@@ -1430,6 +1448,8 @@ namespace MobileGL::MG_Pipe {
|
|||||||
MGPipeSlots().FindByLifetimeId(MGPipeKind::Renderbuffer, lifetimeId);
|
MGPipeSlots().FindByLifetimeId(MGPipeKind::Renderbuffer, lifetimeId);
|
||||||
const Bool published =
|
const Bool published =
|
||||||
EmitDeleteIfPublished(MGPipeKind::Renderbuffer, handle, &MGPipeApplyResourceDestroy);
|
EmitDeleteIfPublished(MGPipeKind::Renderbuffer, handle, &MGPipeApplyResourceDestroy);
|
||||||
|
ForwardWhenWired<kMGPipeWiredTextureSubsystem>(
|
||||||
|
MGPipeTextureEmitterInstance(), [&](auto& emitter) { emitter.NoteRenderbufferDied(handle); });
|
||||||
NotifyAndFree(MGPipeKind::Renderbuffer, lifetimeId, handle);
|
NotifyAndFree(MGPipeKind::Renderbuffer, lifetimeId, handle);
|
||||||
return published;
|
return published;
|
||||||
}
|
}
|
||||||
@@ -1454,6 +1474,8 @@ namespace MobileGL::MG_Pipe {
|
|||||||
// whatever it owed", which for a framebuffer is the death notice this just raised.
|
// whatever it owed", which for a framebuffer is the death notice this just raised.
|
||||||
const MGPipeHandle handle =
|
const MGPipeHandle handle =
|
||||||
MGPipeSlots().FindByLifetimeId(MGPipeKind::Framebuffer, lifetimeId);
|
MGPipeSlots().FindByLifetimeId(MGPipeKind::Framebuffer, lifetimeId);
|
||||||
|
ForwardWhenWired<kMGPipeWiredFramebufferSubsystem>(
|
||||||
|
MGPipeFramebufferEmitterInstance(), [&](auto& emitter) { emitter.NoteFramebufferDied(handle); });
|
||||||
NotifyAndFree(MGPipeKind::Framebuffer, lifetimeId, handle);
|
NotifyAndFree(MGPipeKind::Framebuffer, lifetimeId, handle);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -1463,6 +1485,11 @@ namespace MobileGL::MG_Pipe {
|
|||||||
MGPipeSlots().FindByLifetimeId(MGPipeKind::SamplerCso, lifetimeId);
|
MGPipeSlots().FindByLifetimeId(MGPipeKind::SamplerCso, lifetimeId);
|
||||||
const Bool published =
|
const Bool published =
|
||||||
EmitDeleteIfPublished(MGPipeKind::SamplerCso, handle, &MGPipeApplyDeleteSamplerState);
|
EmitDeleteIfPublished(MGPipeKind::SamplerCso, handle, &MGPipeApplyDeleteSamplerState);
|
||||||
|
// NOTHING TO RETIRE IN AN EMITTER FOR THIS KIND, stated rather than implied: a sampler
|
||||||
|
// CSO is content-addressed and belongs to a value, so no emitter keeps an entry under
|
||||||
|
// a SamplerObject's handle - the cache's entries are keyed by value and reference
|
||||||
|
// count, and the death of a bound sampler object releases its unit's reference at the
|
||||||
|
// next bind_sampler_states pass (SamplerEmit.h's reconciliation).
|
||||||
NotifyAndFree(MGPipeKind::SamplerCso, lifetimeId, handle);
|
NotifyAndFree(MGPipeKind::SamplerCso, lifetimeId, handle);
|
||||||
return published;
|
return published;
|
||||||
}
|
}
|
||||||
@@ -1479,6 +1506,8 @@ namespace MobileGL::MG_Pipe {
|
|||||||
MGPipeSlots().FindByLifetimeId(MGPipeKind::ShaderCso, lifetimeId);
|
MGPipeSlots().FindByLifetimeId(MGPipeKind::ShaderCso, lifetimeId);
|
||||||
const Bool published =
|
const Bool published =
|
||||||
EmitDeleteIfPublished(MGPipeKind::ShaderCso, handle, &MGPipeApplyDeleteShaderState);
|
EmitDeleteIfPublished(MGPipeKind::ShaderCso, handle, &MGPipeApplyDeleteShaderState);
|
||||||
|
ForwardWhenWired<kMGPipeWiredProgramSubsystem>(
|
||||||
|
MGPipeProgramEmitterInstance(), [&](auto& emitter) { emitter.NoteRecordDestroyed(handle); });
|
||||||
NotifyAndFree(MGPipeKind::ShaderCso, lifetimeId, handle);
|
NotifyAndFree(MGPipeKind::ShaderCso, lifetimeId, handle);
|
||||||
return published;
|
return published;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -324,14 +324,12 @@ namespace MobileGL::MG_Pipe {
|
|||||||
|
|
||||||
// The memo's other half, and the bound-mirror clearing beside it.
|
// The memo's other half, and the bound-mirror clearing beside it.
|
||||||
//
|
//
|
||||||
// NO PRODUCTION CALLER TODAY, stated rather than implied: since c0b the death path
|
// THE CALLER IS THE CONTRACT's DEATH HELPER (P4a final review C-2): the death path
|
||||||
// reads the contract's latch and never asks an emitter. It is kept because the memo
|
// reads the contract's latch for the wire delete and then forwards here, before the
|
||||||
// above needs a way to be told, and because everything it clears SELF-HEALS if it is
|
// slot is freed, so a dead handle no longer reads as published in this memo between
|
||||||
// not called - the slot's Gen moves on reuse, so `RecordGen == handle.Gen` refuses a
|
// the death and the recycle and the three bound mirrors never name a dead program.
|
||||||
// stale record latch, and the three bound mirrors below hold a handle whose generation
|
// Gen-keyed, so a late notice for a slot already handed out again clears nothing of
|
||||||
// can never be handed out again, so the next EmitShaderState compares against a
|
// the successor's.
|
||||||
// different handle and re-binds. Clearing them here is the cheaper answer, not the
|
|
||||||
// load-bearing one.
|
|
||||||
void NoteRecordDestroyed(MGPipeHandle handle) {
|
void NoteRecordDestroyed(MGPipeHandle handle) {
|
||||||
if (MGPipeHandleIsNull(handle)) return;
|
if (MGPipeHandleIsNull(handle)) return;
|
||||||
Vector<Latch>& table = TableOf(handle);
|
Vector<Latch>& table = TableOf(handle);
|
||||||
|
|||||||
@@ -986,11 +986,11 @@ namespace MobileGL::MG_Pipe {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// The memo's other half, for a caller that knows the applier has dropped this record.
|
// The memo's other half, for a caller that knows the applier has dropped this record.
|
||||||
// NO PRODUCTION CALLER TODAY, and that is stated rather than implied: the death path
|
// THE CALLER IS THE CONTRACT's DEATH HELPER (P4a final review C-2): the texture's
|
||||||
// goes through the contract's latch, not through here. It is kept because the memo
|
// helper drops the sampler view minted off the texture's lifetime id and forwards here
|
||||||
// above needs a way to be told, and because leaving the latch standing SELF-HEALS
|
// before the slot is freed, so a dead handle no longer reads as published in this memo
|
||||||
// anyway - the slot's Gen moves on reuse, so the `RecordGen == handle.Gen` test in
|
// between the death and the recycle. Gen-keyed, so a late notice for a slot already
|
||||||
// RecordIsPublished and in AcquireSamplerView already refuses a stale entry.
|
// handed out again clears nothing of the successor's.
|
||||||
void NoteRecordDestroyed(MGPipeHandle handle) {
|
void NoteRecordDestroyed(MGPipeHandle handle) {
|
||||||
if (MGPipeHandleIsNull(handle)) return;
|
if (MGPipeHandleIsNull(handle)) return;
|
||||||
const SizeT slot = handle.Slot;
|
const SizeT slot = handle.Slot;
|
||||||
|
|||||||
@@ -441,17 +441,79 @@ namespace MobileGL::MG_Pipe {
|
|||||||
|
|
||||||
// The texture a handle names, or null. A RAW pointer is exact here for
|
// The texture a handle names, or null. A RAW pointer is exact here for
|
||||||
// MGPipeResourceTracker::Resolve's reason: the entry exists only between the create the
|
// MGPipeResourceTracker::Resolve's reason: the entry exists only between the create the
|
||||||
// constructor emits and the destroy the destructor emits, and the Gen compare is what
|
// constructor emits and the destroy the destructor emits - and since the final review's
|
||||||
// refuses a stale handle rather than resolving it to whatever now occupies the slot.
|
// C-2 that sentence is ESTABLISHED rather than assumed: the contract's death helper
|
||||||
|
// forwards to NoteTextureDied below before it frees the slot, so a dead handle finds a
|
||||||
|
// null pointer here. The Gen compare refuses a RECYCLED handle rather than resolving it
|
||||||
|
// to whatever now occupies the slot.
|
||||||
|
//
|
||||||
|
// A DEAD SLOT IS REFUSED, LOUDLY. The allocator's generation moves only at the NEXT
|
||||||
|
// hand-out, so between a death and a recycle a dead handle compares equal to the slot's
|
||||||
|
// generation - which is why the guard is IsLive and not GenOfSlot (the review's C-2:
|
||||||
|
// that compare guarded a recycled slot and never a dead one, and the drain then called
|
||||||
|
// a virtual on the freed object once per verb). Reaching this arm at all means a death
|
||||||
|
// path skipped the emitter, which is a seam defect and not traffic: it is counted and
|
||||||
|
// logged once, and the answer is null.
|
||||||
ITextureObject* ResolveTexture(MGPipeHandle handle) const {
|
ITextureObject* ResolveTexture(MGPipeHandle handle) const {
|
||||||
const SizeT slot = handle.Slot;
|
const SizeT slot = handle.Slot;
|
||||||
if (MGPipeHandleIsNull(handle) || slot >= m_textures.size()) return nullptr;
|
if (MGPipeHandleIsNull(handle) || slot >= m_textures.size()) return nullptr;
|
||||||
const Entry& entry = m_textures[slot];
|
const Entry& entry = m_textures[slot];
|
||||||
if (entry.Texture == nullptr || entry.Gen != handle.Gen) return nullptr;
|
if (entry.Texture == nullptr || entry.Gen != handle.Gen) return nullptr;
|
||||||
if (MGPipeSlots().GenOfSlot(MGPipeKind::Texture, handle.Slot) != handle.Gen) return nullptr;
|
if (!MGPipeSlots().IsLive(MGPipeKind::Texture, handle)) {
|
||||||
|
++m_deadResolves;
|
||||||
|
MGLOG_E_ONCE("MGPipe: texture handle {slot=%u, gen=%u} is dead but the emitter still holds its "
|
||||||
|
"object - the death path did not retire the entry; refused rather than resolved",
|
||||||
|
handle.Slot, handle.Gen);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
return entry.Texture;
|
return entry.Texture;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- the death half (P4a final review C-2) ----
|
||||||
|
//
|
||||||
|
// CALLED BY THE CONTRACT'S DEATH HELPER, after the wire delete went out and BEFORE the
|
||||||
|
// slot is freed (ID-8's order: delete, notice, free - this sits between the first two).
|
||||||
|
// v2 had no such door: the helper freed the slot, the emitter kept the freed
|
||||||
|
// ITextureObject* and the level on the drain list, and `glTexImage2D; glDeleteTextures;
|
||||||
|
// <any verb>` walked freed memory at the next validate point - a SIGABRT ("pure virtual
|
||||||
|
// method called") at the shipping mask. Everything the entry owns goes here: the drain
|
||||||
|
// entries (nothing is owed for a dead texture - its record is gone with the wire delete),
|
||||||
|
// the built-in sampler's cache reference (ID-17: one per entry, released at the death
|
||||||
|
// and no longer at the recycle), the latches and the sticky mask. RetireIfRecycled stays
|
||||||
|
// as the belt for a slot whose death this emitter was never told about.
|
||||||
|
//
|
||||||
|
// Keyed on the GENERATION so a late notice for a slot that has already been handed out
|
||||||
|
// again cannot retire the successor's entry.
|
||||||
|
void NoteTextureDied(MGPipeHandle handle) {
|
||||||
|
const SizeT slot = handle.Slot;
|
||||||
|
if (MGPipeHandleIsNull(handle) || slot >= m_textures.size()) return;
|
||||||
|
Entry& entry = m_textures[slot];
|
||||||
|
if (entry.Gen != handle.Gen) return;
|
||||||
|
if (!entry.DrainKeys.empty()) {
|
||||||
|
// A death inside the drain cannot happen (no SharedPtr drops there), but if one
|
||||||
|
// ever did the loop below is iterating m_drain: the null pointer the reset
|
||||||
|
// leaves is what makes EmitOneLevel answer "nothing owed" and drop the entry.
|
||||||
|
if (!m_draining) {
|
||||||
|
SizeT kept = 0;
|
||||||
|
for (SizeT i = 0; i < m_drain.size(); ++i) {
|
||||||
|
if (m_drain[i].Handle == handle) continue;
|
||||||
|
m_drain[kept++] = m_drain[i];
|
||||||
|
}
|
||||||
|
m_drain.resize(kept);
|
||||||
|
}
|
||||||
|
entry.DrainKeys.clear();
|
||||||
|
}
|
||||||
|
MGPipeSamplerCsoCacheInstance().Release(entry.BuiltinSampler);
|
||||||
|
entry = Entry{};
|
||||||
|
}
|
||||||
|
void NoteRenderbufferDied(MGPipeHandle handle) {
|
||||||
|
const SizeT slot = handle.Slot;
|
||||||
|
if (MGPipeHandleIsNull(handle) || slot >= m_renderbuffers.size()) return;
|
||||||
|
Entry& entry = m_renderbuffers[slot];
|
||||||
|
if (entry.Gen != handle.Gen) return;
|
||||||
|
entry = Entry{};
|
||||||
|
}
|
||||||
|
|
||||||
// ---- the sticky bind mask (D-A4) ----
|
// ---- the sticky bind mask (D-A4) ----
|
||||||
//
|
//
|
||||||
// ORed, never cleared, and emitted on BOTH resource_create and every
|
// ORed, never cleared, and emitted on BOTH resource_create and every
|
||||||
@@ -474,6 +536,11 @@ namespace MobileGL::MG_Pipe {
|
|||||||
void NoteTextureBoundAs(MGPipeHandle handle, Uint16 bit) {
|
void NoteTextureBoundAs(MGPipeHandle handle, Uint16 bit) {
|
||||||
if (MGPipeHandleIsNull(handle)) return;
|
if (MGPipeHandleIsNull(handle)) return;
|
||||||
Entry& entry = EntryFor(m_textures, handle);
|
Entry& entry = EntryFor(m_textures, handle);
|
||||||
|
// The entry is stamped with the generation it is written under, and a predecessor's
|
||||||
|
// entry on a recycled slot is retired first (the same door AcquireTexture takes): a
|
||||||
|
// texture born while the family bit was clear has no create to have done it.
|
||||||
|
RetireIfRecycled(entry, handle);
|
||||||
|
entry.Gen = handle.Gen;
|
||||||
const Uint16 before = entry.BindMask;
|
const Uint16 before = entry.BindMask;
|
||||||
const Uint16 now = static_cast<Uint16>(before | bit);
|
const Uint16 now = static_cast<Uint16>(before | bit);
|
||||||
if (now == before) return;
|
if (now == before) return;
|
||||||
@@ -491,6 +558,8 @@ namespace MobileGL::MG_Pipe {
|
|||||||
void NoteRenderbufferBoundAs(MGPipeHandle handle, Uint16 bit) {
|
void NoteRenderbufferBoundAs(MGPipeHandle handle, Uint16 bit) {
|
||||||
if (MGPipeHandleIsNull(handle)) return;
|
if (MGPipeHandleIsNull(handle)) return;
|
||||||
Entry& entry = EntryFor(m_renderbuffers, handle);
|
Entry& entry = EntryFor(m_renderbuffers, handle);
|
||||||
|
RetireIfRecycled(entry, handle);
|
||||||
|
entry.Gen = handle.Gen;
|
||||||
const Uint16 before = entry.BindMask;
|
const Uint16 before = entry.BindMask;
|
||||||
const Uint16 now = static_cast<Uint16>(before | bit);
|
const Uint16 now = static_cast<Uint16>(before | bit);
|
||||||
if (now == before) return;
|
if (now == before) return;
|
||||||
@@ -706,9 +775,9 @@ namespace MobileGL::MG_Pipe {
|
|||||||
// what stops the LRU pulling a handle out from under a standing MGPTextureParams
|
// what stops the LRU pulling a handle out from under a standing MGPTextureParams
|
||||||
// record: the applier deliberately does not resolve BuiltinSampler, and an eviction
|
// record: the applier deliberately does not resolve BuiltinSampler, and an eviction
|
||||||
// is not a parameter change, so nothing would refuse and nothing would re-emit. The
|
// is not a parameter change, so nothing would refuse and nothing would re-emit. The
|
||||||
// previous handle is released when the content moves it, and the last one when the
|
// previous handle is released when the content moves it, and the last one at the
|
||||||
// slot is recycled (RetireIfRecycled) - which is the only moment this package can
|
// texture's death (NoteTextureDied, reached from the contract's death helper) - or
|
||||||
// see a texture die, the death helper being A's.
|
// at the recycle, as the belt, for a death this emitter was not told about.
|
||||||
MGPipeSamplerCsoCache& cache = MGPipeSamplerCsoCacheInstance();
|
MGPipeSamplerCsoCache& cache = MGPipeSamplerCsoCacheInstance();
|
||||||
Uint64 samplerBytes = 0;
|
Uint64 samplerBytes = 0;
|
||||||
const MGPipeHandle builtinSampler =
|
const MGPipeHandle builtinSampler =
|
||||||
@@ -879,6 +948,9 @@ namespace MobileGL::MG_Pipe {
|
|||||||
// itself returns no byte count - it is not emitted from the validate point's payload
|
// itself returns no byte count - it is not emitted from the validate point's payload
|
||||||
// histogram - so this is where the cache's answer lands.
|
// histogram - so this is where the cache's answer lands.
|
||||||
Uint64 SamplerCsoPayloadBytes() const { return m_samplerCsoPayloadBytes; }
|
Uint64 SamplerCsoPayloadBytes() const { return m_samplerCsoPayloadBytes; }
|
||||||
|
// Dead handles that still held an object when resolved: a death path that skipped the
|
||||||
|
// emitter. 0 on a healthy tree; a case that drives every death path asserts it.
|
||||||
|
Uint64 DeadResolveCount() const { return m_deadResolves; }
|
||||||
MGPipeHandle BuiltinSamplerOf(MGPipeHandle handle) const {
|
MGPipeHandle BuiltinSamplerOf(MGPipeHandle handle) const {
|
||||||
const SizeT slot = handle.Slot;
|
const SizeT slot = handle.Slot;
|
||||||
if (MGPipeHandleIsNull(handle) || slot >= m_textures.size()) return kMGPipeNullHandle;
|
if (MGPipeHandleIsNull(handle) || slot >= m_textures.size()) return kMGPipeNullHandle;
|
||||||
@@ -901,6 +973,7 @@ namespace MobileGL::MG_Pipe {
|
|||||||
m_creates = m_respecifies = m_paramSets = m_subDatas = 0;
|
m_creates = m_respecifies = m_paramSets = m_subDatas = 0;
|
||||||
m_refusedSubDatas = 0;
|
m_refusedSubDatas = 0;
|
||||||
m_samplerCsoPayloadBytes = 0;
|
m_samplerCsoPayloadBytes = 0;
|
||||||
|
m_deadResolves = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// A unit fixture's per-case reset; the library never calls it. See
|
// A unit fixture's per-case reset; the library never calls it. See
|
||||||
@@ -968,7 +1041,8 @@ namespace MobileGL::MG_Pipe {
|
|||||||
}
|
}
|
||||||
static Uint16 MaskOf(const Vector<Entry>& table, MGPipeHandle handle) {
|
static Uint16 MaskOf(const Vector<Entry>& table, MGPipeHandle handle) {
|
||||||
const SizeT slot = handle.Slot;
|
const SizeT slot = handle.Slot;
|
||||||
return slot < table.size() ? table[slot].BindMask : Uint16{0};
|
if (slot >= table.size() || table[slot].Gen != handle.Gen) return Uint16{0};
|
||||||
|
return table[slot].BindMask;
|
||||||
}
|
}
|
||||||
// A SLOT THE ALLOCATOR HAS HANDED OUT AGAIN CARRIES ITS PREDECESSOR'S ENTRY, and every
|
// A SLOT THE ALLOCATOR HAS HANDED OUT AGAIN CARRIES ITS PREDECESSOR'S ENTRY, and every
|
||||||
// field in it is a lie about the new object (m4). The sticky BindMask is the one that
|
// field in it is a lie about the new object (m4). The sticky BindMask is the one that
|
||||||
@@ -977,10 +1051,10 @@ namespace MobileGL::MG_Pipe {
|
|||||||
// the dead one's mask and its first descriptor said so. The generation is what
|
// the dead one's mask and its first descriptor said so. The generation is what
|
||||||
// distinguishes them and the reset is here because AcquireTexture is the one door.
|
// distinguishes them and the reset is here because AcquireTexture is the one door.
|
||||||
//
|
//
|
||||||
// IT IS ALSO THE ONLY MOMENT THIS PACKAGE CAN SEE A TEXTURE DIE. The death helper is
|
// IT IS THE BELT, NOT THE PATH (final review C-2): the death helper forwards to
|
||||||
// A's (MGPipeEmitTextureDestroyAndFree) and does not forward to this emitter, so the
|
// NoteTextureDied, which retires the entry - drain entries, cache reference, latches,
|
||||||
// built-in sampler's cache reference is dropped here - bounded by the number of live
|
// mask - at the death itself. This stays for a slot whose death this emitter was never
|
||||||
// texture slots rather than unbounded, which is the shape ID-17 rule 3 names.
|
// told about, and drops the same reference if one is still standing.
|
||||||
void RetireIfRecycled(Entry& entry, MGPipeHandle handle) {
|
void RetireIfRecycled(Entry& entry, MGPipeHandle handle) {
|
||||||
if (entry.Gen == handle.Gen) return;
|
if (entry.Gen == handle.Gen) return;
|
||||||
MGPipeSamplerCsoCacheInstance().Release(entry.BuiltinSampler);
|
MGPipeSamplerCsoCacheInstance().Release(entry.BuiltinSampler);
|
||||||
@@ -1200,6 +1274,7 @@ namespace MobileGL::MG_Pipe {
|
|||||||
Uint64 m_subDatas = 0;
|
Uint64 m_subDatas = 0;
|
||||||
Uint64 m_refusedSubDatas = 0;
|
Uint64 m_refusedSubDatas = 0;
|
||||||
Uint64 m_samplerCsoPayloadBytes = 0;
|
Uint64 m_samplerCsoPayloadBytes = 0;
|
||||||
|
mutable Uint64 m_deadResolves = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
inline MGPipeTextureEmitter& MGPipeTextureEmitterInstance() {
|
inline MGPipeTextureEmitter& MGPipeTextureEmitterInstance() {
|
||||||
@@ -1234,7 +1309,10 @@ namespace MobileGL::MG_Pipe {
|
|||||||
// keeping the inline definition here would not have compiled at all;
|
// keeping the inline definition here would not have compiled at all;
|
||||||
// * step 1 of the death order is inside MGPipeEmitTextureDestroyAndFree /
|
// * step 1 of the death order is inside MGPipeEmitTextureDestroyAndFree /
|
||||||
// ...RenderbufferDestroyAndFree, which read MGPipeHandleIsPublished and emit the
|
// ...RenderbufferDestroyAndFree, which read MGPipeHandleIsPublished and emit the
|
||||||
// resource_destroy themselves, so the destructors call ONE helper and not two;
|
// resource_destroy themselves, so the destructors call ONE helper and not two - and
|
||||||
|
// since the final review's C-2 the helper then forwards to NoteTextureDied /
|
||||||
|
// NoteRenderbufferDied above, so no ITextureObject* survives its object in this table
|
||||||
|
// and no dead level survives on the drain list;
|
||||||
// * the publication latch is PipeFill.cpp's {kind, slot, gen} table, written by
|
// * the publication latch is PipeFill.cpp's {kind, slot, gen} table, written by
|
||||||
// PublishCreate above and read by those helpers.
|
// PublishCreate above and read by those helpers.
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -824,6 +824,36 @@ gtest_discover_tests(MobileGLIntegrationTest
|
|||||||
ENVIRONMENT "${MGL_ITEST_GLES_UNLOCATED_IO_BLOCKS_ENVIRONMENT}"
|
ENVIRONMENT "${MGL_ITEST_GLES_UNLOCATED_IO_BLOCKS_ENVIRONMENT}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# P4a final review C-2: the dirty-then-delete case, with the allocator SCRIBBLING every freed
|
||||||
|
# block. The defect this pins was a client emitter resolving a dead-but-not-recycled texture
|
||||||
|
# handle to the freed ITextureObject* and calling a virtual on it from the next verb's drain;
|
||||||
|
# whether that reads the object's ghost or faults depends on what the allocator did with the
|
||||||
|
# block, so the ambient registrations above run the case as the application would see it and
|
||||||
|
# these two run it with MALLOC_PERTURB_ set, where a resolved-but-dead pointer faults rather
|
||||||
|
# than passes. Both backends: the death path is backend-neutral by ruling (ID-8).
|
||||||
|
mgl_itest_join_environment(MGL_ITEST_GLES_MALLOC_PERTURB_ENVIRONMENT
|
||||||
|
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MALLOC_PERTURB_=165" ${MGL_ITEST_COMMON_ENV})
|
||||||
|
mgl_itest_join_environment(MGL_ITEST_VULKAN_MALLOC_PERTURB_ENVIRONMENT
|
||||||
|
"MOBILEGL_BACKEND_TYPE=DirectVulkan" "MALLOC_PERTURB_=165" ${MGL_ITEST_VULKAN_ENV})
|
||||||
|
gtest_discover_tests(MobileGLIntegrationTest
|
||||||
|
TEST_PREFIX "DirectGLES.MallocPerturb."
|
||||||
|
TEST_FILTER "P4aFinalFixScenario.ADirtyTextureDeletedBeforeAnyVerbIsWalkedByTheNextDrain"
|
||||||
|
DISCOVERY_TIMEOUT 30
|
||||||
|
PROPERTIES
|
||||||
|
LABELS integration-gpu
|
||||||
|
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||||
|
ENVIRONMENT "${MGL_ITEST_GLES_MALLOC_PERTURB_ENVIRONMENT}"
|
||||||
|
)
|
||||||
|
gtest_discover_tests(MobileGLIntegrationTest
|
||||||
|
TEST_PREFIX "DirectVulkan.MallocPerturb."
|
||||||
|
TEST_FILTER "P4aFinalFixScenario.ADirtyTextureDeletedBeforeAnyVerbIsWalkedByTheNextDrain"
|
||||||
|
DISCOVERY_TIMEOUT 30
|
||||||
|
PROPERTIES
|
||||||
|
LABELS integration-gpu
|
||||||
|
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||||
|
ENVIRONMENT "${MGL_ITEST_VULKAN_MALLOC_PERTURB_ENVIRONMENT}"
|
||||||
|
)
|
||||||
|
|
||||||
# AsyncCompileScenario, with asynchronous compilation PINNED ON per backend.
|
# AsyncCompileScenario, with asynchronous compilation PINNED ON per backend.
|
||||||
#
|
#
|
||||||
# Not a duplicate of what the two ambient registrations already run: they run whatever
|
# Not a duplicate of what the two ambient registrations already run: they run whatever
|
||||||
|
|||||||
@@ -337,5 +337,155 @@ void main() { oColor = texture(uTex, vUv); }
|
|||||||
glDeleteTextures(1, &cleanup);
|
glDeleteTextures(1, &cleanup);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ======================================================================================
|
||||||
|
// C-2: delete-then-use, for every kind P4a mints, on both backends
|
||||||
|
// ======================================================================================
|
||||||
|
|
||||||
|
// A level goes dirty, the texture dies before any verb, and the next verb's drain walks
|
||||||
|
// the entry. Before the fix the emitter resolved the dead handle to the freed object and
|
||||||
|
// the drain called a virtual on it: SIGABRT in the first round. Eight rounds, and the
|
||||||
|
// lane registered with MALLOC_PERTURB_ scribbles every freed block so a resolved-but-
|
||||||
|
// dead pointer faults rather than reads the object's ghost.
|
||||||
|
TEST_F(P4aFinalFixScenario, ADirtyTextureDeletedBeforeAnyVerbIsWalkedByTheNextDrain) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
for (int round = 0; round < 8; ++round) {
|
||||||
|
GLuint texture = 0;
|
||||||
|
glGenTextures(1, &texture);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, texture);
|
||||||
|
const std::vector<std::uint8_t> texels = Solid(4, 255, 0, 0);
|
||||||
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 4, 4, 0, GL_RGBA, GL_UNSIGNED_BYTE, texels.data());
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
glDeleteTextures(1, &texture); // the last reference: the frontend object dies here
|
||||||
|
// Something else is allocated between the death and the drain, so the freed
|
||||||
|
// storage is not simply re-handed to the next object.
|
||||||
|
std::vector<std::uint8_t> churn(4096 + round * 1024, static_cast<std::uint8_t>(round));
|
||||||
|
(void)churn;
|
||||||
|
const Image image = DrawSampled(m_other); // the validate point: the drain runs here
|
||||||
|
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||||
|
if (round == 0) Report("ADirtyTextureDeletedBeforeAnyVerbIsWalkedByTheNextDrain", image);
|
||||||
|
EXPECT_TRUE(Mostly(image, "white", "the draw after a dirty texture died"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ABA: the slot the dead texture held is handed straight to the next texture (the free
|
||||||
|
// list is LIFO). The new texture's picture must be its own, and the dead one's drain
|
||||||
|
// entry must not be replayed onto it.
|
||||||
|
TEST_F(P4aFinalFixScenario, ATextureRecycledOntoTheDeadSlotDoesNotInheritItsDrainEntry) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
{
|
||||||
|
GLuint dead = 0;
|
||||||
|
glGenTextures(1, &dead);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, dead);
|
||||||
|
const std::vector<std::uint8_t> texels = Solid(4, 255, 0, 0);
|
||||||
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 4, 4, 0, GL_RGBA, GL_UNSIGNED_BYTE, texels.data());
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
glDeleteTextures(1, &dead); // dirty, dead, no verb between
|
||||||
|
}
|
||||||
|
const GLuint successor = MakeLevel0(0, 0, 255, /*maxLevel=*/0, /*size=*/8);
|
||||||
|
const Image image = DrawSampled(successor);
|
||||||
|
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||||
|
Report("ATextureRecycledOntoTheDeadSlotDoesNotInheritItsDrainEntry", image);
|
||||||
|
EXPECT_TRUE(Mostly(image, "blue", "the successor of a dead dirty texture on the recycled slot"));
|
||||||
|
const Image other = DrawSampled(m_other);
|
||||||
|
EXPECT_TRUE(Mostly(other, "white", "an unrelated draw after the recycled slot was used"));
|
||||||
|
GLuint cleanup = successor;
|
||||||
|
glDeleteTextures(1, &cleanup);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A renderbuffer with defined storage, attached, cleared through its framebuffer, then
|
||||||
|
// both die before the next verb.
|
||||||
|
TEST_F(P4aFinalFixScenario, ARenderbufferAndItsFramebufferDeletedAfterAClearLeaveTheNextDrawIntact) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
GLuint renderbuffer = 0;
|
||||||
|
glGenRenderbuffers(1, &renderbuffer);
|
||||||
|
glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer);
|
||||||
|
glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, 8, 8);
|
||||||
|
GLuint fbo = 0;
|
||||||
|
glGenFramebuffers(1, &fbo);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||||
|
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, renderbuffer);
|
||||||
|
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), GLenum(GL_FRAMEBUFFER_COMPLETE));
|
||||||
|
glViewport(0, 0, 8, 8);
|
||||||
|
glClearColor(0.0f, 1.0f, 0.0f, 1.0f);
|
||||||
|
glClear(GL_COLOR_BUFFER_BIT);
|
||||||
|
const Image cleared = ReadPixels(8, 8);
|
||||||
|
EXPECT_TRUE(RegionIsMostly(cleared, 0, 8, 0, 8, "green", 0.0, "the renderbuffer after the clear"));
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||||
|
glBindRenderbuffer(GL_RENDERBUFFER, 0);
|
||||||
|
glDeleteFramebuffers(1, &fbo);
|
||||||
|
glDeleteRenderbuffers(1, &renderbuffer); // the attachment's last reference went with the FBO
|
||||||
|
const Image image = DrawSampled(m_other);
|
||||||
|
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||||
|
Report("ARenderbufferAndItsFramebufferDeletedAfterAClearLeaveTheNextDrawIntact", image);
|
||||||
|
EXPECT_TRUE(Mostly(image, "white", "the draw after a renderbuffer and its framebuffer died"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// A sampler object bound to the unit the draw samples through, deleted while bound: GL
|
||||||
|
// unbinds it from every unit at glDeleteSamplers, and the texture's own parameters apply
|
||||||
|
// again. Both draws must be the texture's colour.
|
||||||
|
TEST_F(P4aFinalFixScenario, ASamplerObjectDeletedWhileBoundLeavesTheNextDrawIntact) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
const GLuint texture = MakeLevel0(255, 0, 0, /*maxLevel=*/0);
|
||||||
|
GLuint sampler = 0;
|
||||||
|
glGenSamplers(1, &sampler);
|
||||||
|
glSamplerParameteri(sampler, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||||
|
glSamplerParameteri(sampler, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||||
|
glBindSampler(0, sampler);
|
||||||
|
const Image withSampler = DrawSampled(texture);
|
||||||
|
EXPECT_TRUE(Mostly(withSampler, "red", "the draw through the bound sampler object"));
|
||||||
|
glDeleteSamplers(1, &sampler); // bound: unbound by the delete, then dies
|
||||||
|
const Image image = DrawSampled(texture);
|
||||||
|
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||||
|
Report("ASamplerObjectDeletedWhileBoundLeavesTheNextDrawIntact", image);
|
||||||
|
EXPECT_TRUE(Mostly(image, "red", "the draw after the bound sampler object died"));
|
||||||
|
glBindSampler(0, 0);
|
||||||
|
GLuint cleanup = texture;
|
||||||
|
glDeleteTextures(1, &cleanup);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A second program, in use when it is deleted (GL keeps it alive until it is no longer
|
||||||
|
// current), then released by a glUseProgram of the fixture's program: it dies there, and
|
||||||
|
// the draw that follows runs through the survivor.
|
||||||
|
TEST_F(P4aFinalFixScenario, AProgramDeletedWhileInUseLeavesTheNextDrawIntact) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
std::string error;
|
||||||
|
const GLuint second = CompileProgram(kVS, kFS, &error);
|
||||||
|
ASSERT_NE(second, 0u) << error;
|
||||||
|
const GLuint texture = MakeLevel0(255, 0, 0, /*maxLevel=*/0);
|
||||||
|
const Image throughSecond = DrawSampled(texture, second);
|
||||||
|
EXPECT_TRUE(Mostly(throughSecond, "red", "the draw through the second program"));
|
||||||
|
glDeleteProgram(second); // current: flagged for deletion, still very much alive
|
||||||
|
const Image stillCurrent = DrawSampled(texture, second);
|
||||||
|
EXPECT_TRUE(Mostly(stillCurrent, "red", "the draw through a program flagged for deletion"));
|
||||||
|
const Image image = DrawSampled(texture); // glUseProgram(m_program): the second dies here
|
||||||
|
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||||
|
Report("AProgramDeletedWhileInUseLeavesTheNextDrawIntact", image);
|
||||||
|
EXPECT_TRUE(Mostly(image, "red", "the draw after the deleted program was released"));
|
||||||
|
GLuint cleanup = texture;
|
||||||
|
glDeleteTextures(1, &cleanup);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A framebuffer handed to the server BY NAME (a DSA clear emits a Named record, ID-19(c))
|
||||||
|
// and deleted before the next verb; its attachment lives on and carries the clear.
|
||||||
|
TEST_F(P4aFinalFixScenario, AFramebufferDeletedAfterADsaClearLeavesItsAttachmentIntact) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
const GLuint texture = MakeLevel0(255, 0, 0, /*maxLevel=*/0);
|
||||||
|
GLuint fbo = 0;
|
||||||
|
glGenFramebuffers(1, &fbo);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||||
|
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
|
||||||
|
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), GLenum(GL_FRAMEBUFFER_COMPLETE));
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||||
|
const GLfloat green[4] = {0.0f, 1.0f, 0.0f, 1.0f};
|
||||||
|
glClearNamedFramebufferfv(fbo, GL_COLOR, 0, green);
|
||||||
|
glDeleteFramebuffers(1, &fbo); // unbound and named: dies here
|
||||||
|
const Image image = DrawSampled(texture);
|
||||||
|
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||||
|
Report("AFramebufferDeletedAfterADsaClearLeavesItsAttachmentIntact", image);
|
||||||
|
EXPECT_TRUE(Mostly(image, "green", "the attachment of a framebuffer that died after a DSA clear"));
|
||||||
|
GLuint cleanup = texture;
|
||||||
|
glDeleteTextures(1, &cleanup);
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
} // namespace MGITest
|
} // namespace MGITest
|
||||||
|
|||||||
@@ -234,7 +234,8 @@ TEST(FramebufferEmit, TheEmitterIsOneNeverDestroyedProcessSingleton) {
|
|||||||
X(FramebufferEmit, ANamedRecordIsSuppressedPerObjectAndNeverAgainstABoundRecord) \
|
X(FramebufferEmit, ANamedRecordIsSuppressedPerObjectAndNeverAgainstABoundRecord) \
|
||||||
X(FramebufferEmit, ADrawBufferTokenAboveTheWireWidthIsRefusedNotTruncated) \
|
X(FramebufferEmit, ADrawBufferTokenAboveTheWireWidthIsRefusedNotTruncated) \
|
||||||
X(FramebufferEmit, ALayeredCubeAttachmentDoesNotAssertAFaceItCannotKnow) \
|
X(FramebufferEmit, ALayeredCubeAttachmentDoesNotAssertAFaceItCannotKnow) \
|
||||||
X(FramebufferEmit, EveryNonTexturePointCarriesTheUnknownSentinelsRatherThanZero)
|
X(FramebufferEmit, EveryNonTexturePointCarriesTheUnknownSentinelsRatherThanZero) \
|
||||||
|
X(FramebufferEmit, ADeadFramebuffersNamedRecordLatchIsRetired)
|
||||||
|
|
||||||
#define MGL_DECLARE_PULL_SKIP(Suite, Name) \
|
#define MGL_DECLARE_PULL_SKIP(Suite, Name) \
|
||||||
TEST(Suite, Name) { GTEST_SKIP() << "compiled only under MOBILEGL_PIPE_PUSH"; }
|
TEST(Suite, Name) { GTEST_SKIP() << "compiled only under MOBILEGL_PIPE_PUSH"; }
|
||||||
@@ -772,6 +773,27 @@ TEST(FramebufferEmit, EveryNonTexturePointCarriesTheUnknownSentinelsRatherThanZe
|
|||||||
"only moved field is the attachment's texture target would be suppressed";
|
"only moved field is the attachment's texture target would be suppressed";
|
||||||
(void)probe;
|
(void)probe;
|
||||||
}
|
}
|
||||||
|
// ============================ final review C-2 ============================
|
||||||
|
//
|
||||||
|
// A framebuffer has no wire lifetime (D-I2), so the only client state under its handle is this
|
||||||
|
// emitter's per-object Named latch - and the death helper retires it before the slot is freed,
|
||||||
|
// the shape every P4a kind takes (ID-8). A recycled handle's Gen already refused the stale
|
||||||
|
// latch, so this pins the hygiene rather than a picture.
|
||||||
|
TEST(FramebufferEmit, ADeadFramebuffersNamedRecordLatchIsRetired) {
|
||||||
|
FramebufferScope scope;
|
||||||
|
MGPipeHandle handle{};
|
||||||
|
{
|
||||||
|
const auto fbo = MakeShared<FramebufferObject>(31);
|
||||||
|
const auto color = MakeColorTexture(32, 8);
|
||||||
|
fbo->AttachTexture(FramebufferAttachmentType::Color0, color, TextureUploadTarget::Texture2D);
|
||||||
|
handle = MGPipeFramebufferEmitter::HandleFor(*fbo);
|
||||||
|
ASSERT_GT(Framebuffers().EmitFramebufferByName(*fbo), 0u) << "the Named record did not go out";
|
||||||
|
ASSERT_TRUE(Framebuffers().NamedRecordIsLatched(handle));
|
||||||
|
}
|
||||||
|
EXPECT_FALSE(MGPipeSlots().IsLive(MGPipeKind::Framebuffer, handle));
|
||||||
|
EXPECT_FALSE(Framebuffers().NamedRecordIsLatched(handle))
|
||||||
|
<< "the dead framebuffer's Named latch survived its death";
|
||||||
|
}
|
||||||
#endif // MOBILEGL_PIPE_PUSH
|
#endif // MOBILEGL_PIPE_PUSH
|
||||||
|
|
||||||
// =========================================================================================
|
// =========================================================================================
|
||||||
|
|||||||
@@ -501,7 +501,8 @@ TEST(ProgramEmit, TheProgramRecordSurvivesAMakeCurrentWhileTheThreeBindingsDoNot
|
|||||||
X(ProgramEmit, AReLinkReIssuesOnTheSameHandle) \
|
X(ProgramEmit, AReLinkReIssuesOnTheSameHandle) \
|
||||||
X(ProgramEmit, TheDrawAndDispatchProgramsAreTwoIndependentSlots) \
|
X(ProgramEmit, TheDrawAndDispatchProgramsAreTwoIndependentSlots) \
|
||||||
X(ProgramEmit, AnUnchangedProgramEmitsNothingAtAll) \
|
X(ProgramEmit, AnUnchangedProgramEmitsNothingAtAll) \
|
||||||
X(ProgramEmit, AReIssuedCreateReSendsTheDefaultUniformBlock)
|
X(ProgramEmit, AReIssuedCreateReSendsTheDefaultUniformBlock) \
|
||||||
|
X(ProgramEmit, ADeadProgramsRecordLatchIsRetiredAtItsDeath)
|
||||||
|
|
||||||
#define MGL_DECLARE_PULL_SKIP(Suite, Name) \
|
#define MGL_DECLARE_PULL_SKIP(Suite, Name) \
|
||||||
TEST(Suite, Name) { GTEST_SKIP() << "compiled only under MOBILEGL_PIPE_PUSH"; }
|
TEST(Suite, Name) { GTEST_SKIP() << "compiled only under MOBILEGL_PIPE_PUSH"; }
|
||||||
@@ -774,6 +775,26 @@ void main() { gl_Position = vec4(0.0); EmitVertex(); }
|
|||||||
}
|
}
|
||||||
EXPECT_GT(Emitter().GlobalConstantsSetCount(), setsBefore);
|
EXPECT_GT(Emitter().GlobalConstantsSetCount(), setsBefore);
|
||||||
}
|
}
|
||||||
|
// FINAL REVIEW C-2: the death helper forwards to this emitter before the slot is freed, so
|
||||||
|
// a dead program's handle no longer reads as published in the record memo between the death
|
||||||
|
// and the recycle (the memo's own Gen test covers only the recycle).
|
||||||
|
TEST(ProgramEmit, ADeadProgramsRecordLatchIsRetiredAtItsDeath) {
|
||||||
|
EmitterScope scope;
|
||||||
|
const GLuint name = MakeVsFsProgram();
|
||||||
|
MGPipeHandle handle{};
|
||||||
|
{
|
||||||
|
const SharedPtr<ProgramObject>& program = Ctx().GetProgramObject(name);
|
||||||
|
ASSERT_TRUE(program);
|
||||||
|
Uint64 bytes = 0;
|
||||||
|
handle = Emitter().AcquireShaderCso(*program, bytes);
|
||||||
|
ASSERT_FALSE(MGPipeHandleIsNull(handle));
|
||||||
|
ASSERT_TRUE(Emitter().RecordIsPublished(handle));
|
||||||
|
}
|
||||||
|
GL::DeleteProgram(name); // not in use: the frontend object dies here
|
||||||
|
EXPECT_FALSE(MGPipeSlots().IsLive(MGPipeKind::ShaderCso, handle));
|
||||||
|
EXPECT_FALSE(Emitter().RecordIsPublished(handle))
|
||||||
|
<< "a dead program still reads as published in the program emitter's memo";
|
||||||
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
#endif // MOBILEGL_PIPE_PUSH
|
#endif // MOBILEGL_PIPE_PUSH
|
||||||
|
|
||||||
|
|||||||
@@ -219,7 +219,11 @@ TEST(TextureEmit, TheEmitterIsOneNeverDestroyedProcessSingleton) {
|
|||||||
X(TextureEmit, EveryDKTwoDependencyRowGatesItsOwnFamilyAndTheMirrorPairsStayLive) \
|
X(TextureEmit, EveryDKTwoDependencyRowGatesItsOwnFamilyAndTheMirrorPairsStayLive) \
|
||||||
X(TextureEmit, ALevelDefinedAfterAnEmittedButUnconsumedUploadKeepsThatUpload) \
|
X(TextureEmit, ALevelDefinedAfterAnEmittedButUnconsumedUploadKeepsThatUpload) \
|
||||||
X(TextureEmit, AChainTruncationKeepsTheSurvivingLevelsPendingUploads) \
|
X(TextureEmit, AChainTruncationKeepsTheSurvivingLevelsPendingUploads) \
|
||||||
X(TextureEmit, ARedefinitionOfANonBaseLevelAtANewSizeDropsOnlyThatLevelsPendingUpload)
|
X(TextureEmit, ARedefinitionOfANonBaseLevelAtANewSizeDropsOnlyThatLevelsPendingUpload) \
|
||||||
|
X(TextureEmit, ADeadTexturesHandleResolvesToNothingAndLeavesTheDrainList) \
|
||||||
|
X(TextureEmit, ATextureRecycledOntoADeadSlotDoesNotInheritTheDrainEntry) \
|
||||||
|
X(TextureEmit, ADeadRenderbuffersEntryIsRetiredWithItsSlot) \
|
||||||
|
X(TextureEmit, ADeadTexturesSamplerViewLatchIsRetiredAtItsDeath)
|
||||||
|
|
||||||
#define MGL_DECLARE_PULL_SKIP(Suite, Name) \
|
#define MGL_DECLARE_PULL_SKIP(Suite, Name) \
|
||||||
TEST(Suite, Name) { GTEST_SKIP() << "compiled only under MOBILEGL_PIPE_PUSH"; }
|
TEST(Suite, Name) { GTEST_SKIP() << "compiled only under MOBILEGL_PIPE_PUSH"; }
|
||||||
@@ -1236,6 +1240,11 @@ TEST(TextureEmit, ARecycledTextureSlotDoesNotInheritItsPredecessorsBindMask) {
|
|||||||
Textures().NoteTextureBoundAs(firstHandle, kMGPipeBindShaderImage);
|
Textures().NoteTextureBoundAs(firstHandle, kMGPipeBindShaderImage);
|
||||||
ASSERT_NE(Textures().TextureBindMask(firstHandle) & kMGPipeBindRenderTarget, 0);
|
ASSERT_NE(Textures().TextureBindMask(firstHandle) & kMGPipeBindRenderTarget, 0);
|
||||||
}
|
}
|
||||||
|
// Since the final review's C-2 the reference goes back AT THE DEATH, through the death
|
||||||
|
// helper's forward, and not first at the recycle.
|
||||||
|
EXPECT_EQ(cache.RefCountOf(firstCso), 0u)
|
||||||
|
<< "the dead texture's cache reference survived its death; the death helper did not reach the emitter";
|
||||||
|
EXPECT_EQ(Textures().TextureBindMask(firstHandle), 0u) << "a dead handle still reads its sticky mask";
|
||||||
const auto second = MakeTexture2D(22, 8);
|
const auto second = MakeTexture2D(22, 8);
|
||||||
const MGPipeHandle secondHandle = Textures().FindTexture(*second);
|
const MGPipeHandle secondHandle = Textures().FindTexture(*second);
|
||||||
ASSERT_EQ(secondHandle.Slot, firstHandle.Slot) << "the slot was not recycled; the case proves nothing";
|
ASSERT_EQ(secondHandle.Slot, firstHandle.Slot) << "the slot was not recycled; the case proves nothing";
|
||||||
@@ -1390,6 +1399,105 @@ TEST(TextureEmit, ARedefinitionOfANonBaseLevelAtANewSizeDropsOnlyThatLevelsPendi
|
|||||||
EXPECT_FALSE(onePending) << "level 1's 8x8 box survived its redefinition onto a 4x4 level";
|
EXPECT_FALSE(onePending) << "level 1's 8x8 box survived its redefinition onto a 4x4 level";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================ final review C-2 ============================
|
||||||
|
//
|
||||||
|
// A DEAD HANDLE RESOLVES TO NOTHING AND THE DRAIN LIST DROPS IT AT THE DEATH. The allocator's
|
||||||
|
// generation moves only at the next hand-out, so between a death and a recycle the dead handle
|
||||||
|
// compared equal to the slot's generation and the emitter answered the freed ITextureObject*;
|
||||||
|
// the drain then called a virtual on it once per verb until something recycled the slot.
|
||||||
|
TEST(TextureEmit, ADeadTexturesHandleResolvesToNothingAndLeavesTheDrainList) {
|
||||||
|
TextureScope scope;
|
||||||
|
MGPipeHandle handle{};
|
||||||
|
{
|
||||||
|
const auto texture = MakeShared<TextureObject2D>(91);
|
||||||
|
texture->SetInternalFormat(TextureInternalFormat::RGBA8);
|
||||||
|
texture->AllocateStorage(TextureUploadTarget::Texture2D, 0, MipmapInput{IntVec3{16, 16, 1}, 16 * 16 * 4});
|
||||||
|
// glTexSubImage2D: the level goes on the drain list; NO verb follows before the delete.
|
||||||
|
texture->MarkStorageDirtyRegion(TextureUploadTarget::Texture2D, 0, IntVec3{0, 0, 0}, IntVec3{16, 16, 1});
|
||||||
|
handle = Textures().FindTexture(*texture);
|
||||||
|
ASSERT_FALSE(MGPipeHandleIsNull(handle));
|
||||||
|
ASSERT_EQ(Textures().DrainListSize(), 1u);
|
||||||
|
} // glDeleteTextures: the last SharedPtr drops, ~TextureObjectBase frees the slot
|
||||||
|
EXPECT_FALSE(MGPipeSlots().IsLive(MGPipeKind::Texture, handle));
|
||||||
|
EXPECT_EQ(Textures().ResolveTexture(handle), nullptr)
|
||||||
|
<< "ResolveTexture hands back the freed ITextureObject* of a dead-but-not-recycled handle";
|
||||||
|
EXPECT_EQ(Textures().DrainListSize(), 0u)
|
||||||
|
<< "the dead texture's level is still on the drain list, so the next verb walks it";
|
||||||
|
// And the next drain has nothing to say about it: no record, no refusal, no emission.
|
||||||
|
Textures().DrainTextureSubData(Ctx());
|
||||||
|
EXPECT_EQ(Textures().SubDataCount(), 0u);
|
||||||
|
EXPECT_EQ(Textures().RefusedSubDataCount(), 0u);
|
||||||
|
// The null came from the DEATH PATH retiring the entry, not from the loud refusal that
|
||||||
|
// guards a death path that skipped the emitter.
|
||||||
|
EXPECT_EQ(Textures().DeadResolveCount(), 0u)
|
||||||
|
<< "the dead handle was refused by ResolveTexture's guard, so the death helper never told the emitter";
|
||||||
|
}
|
||||||
|
|
||||||
|
// The sampler view minted off the texture's lifetime id (D-F2) has its own record memo in the
|
||||||
|
// sampler emitter; the texture's death retires it through the view's death helper.
|
||||||
|
TEST(TextureEmit, ADeadTexturesSamplerViewLatchIsRetiredAtItsDeath) {
|
||||||
|
TextureScope scope;
|
||||||
|
MGPipeHandle viewHandle{};
|
||||||
|
{
|
||||||
|
const auto texture = MakeTexture2D(89, 8);
|
||||||
|
const MGPipeHandle handle = Textures().FindTexture(*texture);
|
||||||
|
Uint64 bytes = 0;
|
||||||
|
viewHandle = MGPipeSamplerEmitterInstance().AcquireSamplerView(*texture, handle, bytes);
|
||||||
|
ASSERT_FALSE(MGPipeHandleIsNull(viewHandle));
|
||||||
|
ASSERT_TRUE(MGPipeSamplerEmitterInstance().RecordIsPublished(viewHandle));
|
||||||
|
}
|
||||||
|
EXPECT_FALSE(MGPipeSlots().IsLive(MGPipeKind::SamplerViewCso, viewHandle));
|
||||||
|
EXPECT_FALSE(MGPipeSamplerEmitterInstance().RecordIsPublished(viewHandle))
|
||||||
|
<< "a dead sampler view still reads as published in the sampler emitter's memo";
|
||||||
|
}
|
||||||
|
|
||||||
|
// ABA: the recycled slot's new texture owns the drain list entry it makes and none of its
|
||||||
|
// predecessor's.
|
||||||
|
TEST(TextureEmit, ATextureRecycledOntoADeadSlotDoesNotInheritTheDrainEntry) {
|
||||||
|
TextureScope scope;
|
||||||
|
MGPipeHandle deadHandle{};
|
||||||
|
{
|
||||||
|
const auto dead = MakeTexture2D(97, 8);
|
||||||
|
dead->MarkStorageDirtyRegion(TextureUploadTarget::Texture2D, 0, IntVec3{0, 0, 0}, IntVec3{8, 8, 1});
|
||||||
|
deadHandle = Textures().FindTexture(*dead);
|
||||||
|
ASSERT_EQ(Textures().DrainListSize(), 1u);
|
||||||
|
}
|
||||||
|
EXPECT_EQ(Textures().DrainListSize(), 0u) << "the death did not retire the drain entry";
|
||||||
|
const auto successor = MakeTexture2D(98, 32);
|
||||||
|
const MGPipeHandle handle = Textures().FindTexture(*successor);
|
||||||
|
ASSERT_EQ(handle.Slot, deadHandle.Slot) << "the slot was not recycled; the case proves nothing";
|
||||||
|
ASSERT_NE(handle.Gen, deadHandle.Gen);
|
||||||
|
EXPECT_EQ(Textures().DrainListSize(), 0u) << "the successor inherited a drain entry it never made";
|
||||||
|
successor->MarkStorageDirtyRegion(TextureUploadTarget::Texture2D, 0, IntVec3{0, 0, 0}, IntVec3{32, 32, 1});
|
||||||
|
EXPECT_EQ(Textures().DrainListSize(), 1u);
|
||||||
|
Textures().DrainTextureSubData(Ctx());
|
||||||
|
EXPECT_EQ(Textures().SubDataCount(), 1u) << "exactly the successor's level went out";
|
||||||
|
EXPECT_TRUE(Textures().LastSubData().Res == handle);
|
||||||
|
EXPECT_EQ(Textures().LastSubData().UnionBox.W, 32u);
|
||||||
|
EXPECT_EQ(Textures().DrainListSize(), 0u);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The renderbuffer table has no pointer to dangle but the same stale entry: a dead
|
||||||
|
// renderbuffer's sticky mask must not be readable through its dead handle.
|
||||||
|
TEST(TextureEmit, ADeadRenderbuffersEntryIsRetiredWithItsSlot) {
|
||||||
|
TextureScope scope;
|
||||||
|
MGPipeHandle handle{};
|
||||||
|
{
|
||||||
|
const auto renderbuffer = MakeShared<RenderbufferObject>(94);
|
||||||
|
handle = Textures().FindRenderbuffer(*renderbuffer);
|
||||||
|
ASSERT_FALSE(MGPipeHandleIsNull(handle));
|
||||||
|
Textures().NoteRenderbufferBoundAs(handle, kMGPipeBindRenderTarget);
|
||||||
|
ASSERT_NE(Textures().RenderbufferBindMask(handle) & kMGPipeBindRenderTarget, 0);
|
||||||
|
}
|
||||||
|
EXPECT_FALSE(MGPipeSlots().IsLive(MGPipeKind::Renderbuffer, handle));
|
||||||
|
EXPECT_EQ(Textures().RenderbufferBindMask(handle), 0u)
|
||||||
|
<< "a dead renderbuffer's entry still answers through its dead handle";
|
||||||
|
const auto successor = MakeShared<RenderbufferObject>(99);
|
||||||
|
const MGPipeHandle successorHandle = Textures().FindRenderbuffer(*successor);
|
||||||
|
ASSERT_EQ(successorHandle.Slot, handle.Slot);
|
||||||
|
EXPECT_EQ(Textures().RenderbufferBindMask(successorHandle), 0u);
|
||||||
|
}
|
||||||
|
|
||||||
#endif // MOBILEGL_PIPE_PUSH
|
#endif // MOBILEGL_PIPE_PUSH
|
||||||
|
|
||||||
// =========================================================================================
|
// =========================================================================================
|
||||||
|
|||||||
Reference in New Issue
Block a user