Pipe: per-framebuffer state memo, object-held handle hints, per-buffer handle reuse and a cached per-verb gate (ValidateForVerb 0.328 -> 0.282 ms/frame; FCL GL CPU Magma 2.68 -> 2.62, Espryt 3.03 -> 3.00; red-once per piece in its unit case).

This commit is contained in:
2026-10-09 07:42:03 -04:00
parent 9ff97c2e7e
commit 485dff1421
17 changed files with 432 additions and 22 deletions
+3
View File
@@ -148,6 +148,9 @@ namespace MobileGL::MG_Config {
// record is byte-identical to the one its handle last published (a glBindVertexBuffer
// that only changed the buffer) re-publishes nothing. Auto is ON; ForceOff re-creates.
QuirkOverride PipeVertexElementsDedup = QuirkOverride::Auto;
// P15: the client reuses a bound framebuffer's built set_framebuffer_state while nothing it
// reads has moved (MGPipeFramebufferEmitter::BuildBoundFramebufferState).
QuirkOverride PipeFramebufferStateMemo = QuirkOverride::Auto;
// MOBILEGL_ADVERTISE_FP64: add GL_ARB_gpu_shader_fp64 to the advertised extension
// string. `double` in a shader always WORKS - it is narrowed to 32 bits before any
// module reaches a backend (ShaderTranspiler::DemoteFloat64Pass) - but the extension
+1
View File
@@ -374,6 +374,7 @@ namespace MobileGL::MG_ConfigLoader {
features.MagmaWriteRename = QueryEnvQuirkOverride("MOBILEGL_MAGMA_WRITE_RENAME");
features.EsprytVaoShadow = QueryEnvQuirkOverride("MOBILEGL_ESPRYT_VAO_SHADOW");
features.PipeVertexElementsDedup = QueryEnvQuirkOverride("MOBILEGL_PIPE_VERTEX_ELEMENTS_DEDUP");
features.PipeFramebufferStateMemo = QueryEnvQuirkOverride("MOBILEGL_PIPE_FRAMEBUFFER_STATE_MEMO");
features.AdvertiseFp64 = QueryEnvFlag("MOBILEGL_ADVERTISE_FP64");
features.MagmaR11G11B10FFallback = QueryEnvFlag("MOBILEGL_MAGMA_R11G11B10F_FALLBACK");
features.MagmaAllowInvalidSpirv = QueryEnvFlag("MOBILEGL_MAGMA_ALLOW_INVALID_SPIRV");
+130 -3
View File
@@ -275,13 +275,13 @@ namespace MobileGL::MG_Pipe {
Bool drawOk = false;
Bool readOk = false;
if (shared) {
drawOk = BuildFramebufferState(*drawFbo, MGPipeFramebufferTarget::Both, drawState);
drawOk = BuildBoundFramebufferState(*drawFbo, MGPipeFramebufferTarget::Both, drawState);
} else {
if (drawFbo) {
drawOk = BuildFramebufferState(*drawFbo, MGPipeFramebufferTarget::Draw, drawState);
drawOk = BuildBoundFramebufferState(*drawFbo, MGPipeFramebufferTarget::Draw, drawState);
}
if (readFbo) {
readOk = BuildFramebufferState(*readFbo, MGPipeFramebufferTarget::Read, readState);
readOk = BuildBoundFramebufferState(*readFbo, MGPipeFramebufferTarget::Read, readState);
}
}
if (!drawOk && !readOk) return 0;
@@ -414,6 +414,8 @@ namespace MobileGL::MG_Pipe {
const MGPFramebufferState& LastNamed() const { return m_lastNamed; }
Uint64 EmissionCount() const { return m_emissions; }
Uint64 RefusedCount() const { return m_refusals; }
// P15: bound-target builds answered from the memo (BuildBoundFramebufferState).
Uint64 MemoHitCount() const { return m_memoHits; }
// A fresh context: what the server has is no longer what this emitter last sent. Only
// LATCHES reset here - MGPipeApplierReset clears the applier's DrawFramebuffer and
@@ -438,6 +440,7 @@ namespace MobileGL::MG_Pipe {
void ResetForTest() {
Reset();
ResetCounters();
for (auto& memo : m_buildMemo) memo.Valid = false;
m_lastDraw = MGPFramebufferState{};
m_lastRead = MGPFramebufferState{};
m_lastNamed = MGPFramebufferState{};
@@ -563,6 +566,40 @@ namespace MobileGL::MG_Pipe {
return true;
}
// BuildFramebufferState for a BOUND target, reusing the last record built for the same
// framebuffer and target while nothing the build reads has moved. The build runs per draw
// and its answer is a function of:
// - the framebuffer: lifetime id (identity) and object version (attachments, draw and
// read buffers, no-attachment defaults all move it);
// - each attached texture: lifetime id, shape version (sizes, level set, internal
// format: completeness and geometry) and its samples / fixed-locations pair, which
// move the params version rather than the shape;
// - each attached renderbuffer: lifetime id plus its size, format and samples, read
// directly (its storage has no version);
// - the handles it minted: the slot allocator's reset epoch, and the texture emitter's
// table epoch for the sticky bind bits SurfaceOf notes (a dropped table must be
// re-noted).
// Not for the default framebuffer, whose geometry follows the current surface.
// Kill switch MOBILEGL_PIPE_FRAMEBUFFER_STATE_MEMO=0.
Bool BuildBoundFramebufferState(const FramebufferObject& fbo, MGPipeFramebufferTarget target,
MGPFramebufferState& out) {
const Bool memoOn = MG_Config::Features.PipeFramebufferStateMemo != MG_Config::QuirkOverride::ForceOff &&
!fbo.IsDefaultFramebuffer();
// Direct-mapped on (framebuffer, target): the build only runs when the bound framebuffer
// changed, so a per-target single entry would miss on every A/B/A switch between targets.
const Uint64 key = fbo.GetLifetimeId() * 4u + static_cast<Uint64>(target);
BuildMemo& memo = m_buildMemo[static_cast<SizeT>((key * 0x9E3779B97F4A7C15ull) >> 60) % m_buildMemo.size()];
if (memoOn && memo.Valid && MemoHolds(memo, fbo, target)) {
out = memo.State;
++m_memoHits;
return true;
}
memo.Valid = false;
if (!BuildFramebufferState(fbo, target, out)) return false;
if (memoOn) FillMemo(memo, fbo, target, out);
return true;
}
static Bool IsBoundTo(const FramebufferObject& fbo, MobileGL::FramebufferTarget target) {
if (MG_State::pGLContext == nullptr) return false;
const auto& bound = MG_State::pGLContext->GetFramebufferBindingSlot(target).GetBoundObject();
@@ -642,6 +679,96 @@ namespace MobileGL::MG_Pipe {
out.FixedSampleLocations = fbo.GetDefaultFixedSampleLocations() ? 1 : 0;
}
struct MemoAttachment {
Uint32 Point = 0;
Bool IsTexture = false;
Uint64 LifetimeId = 0;
Uint64 ShapeVersion = 0;
Int Width = 0, Height = 0, Samples = 0;
Uint32 Format = 0;
Bool FixedSampleLocations = false;
};
struct BuildMemo {
Bool Valid = false;
Uint8 Target = 0;
Uint16 ObjectVersion = 0;
Uint64 FboLifetimeId = 0;
Uint64 AllocatorEpoch = 0;
Uint64 TextureTableEpoch = 0;
Uint32 AttachmentCount = 0;
Array<MemoAttachment, static_cast<SizeT>(FramebufferAttachmentType::FramebufferAttachmentTypeCount)>
Attachments{};
MGPFramebufferState State{};
};
static Bool AttachmentMatches(const MemoAttachment& entry,
const MG_State::GLState::FramebufferAttachmentObject& attachment) {
if (attachment.IsEmpty() || attachment.IsTexture() != entry.IsTexture) return false;
if (entry.IsTexture) {
const auto& texture = attachment.GetTexture();
return texture->GetLifetimeId() == entry.LifetimeId &&
texture->GetShapeVersion() == entry.ShapeVersion &&
texture->GetSamples() == entry.Samples &&
texture->HasFixedSampleLocations() == entry.FixedSampleLocations;
}
const auto& renderbuffer = attachment.GetRenderbuffer();
return renderbuffer->GetLifetimeId() == entry.LifetimeId && renderbuffer->GetWidth() == entry.Width &&
renderbuffer->GetHeight() == entry.Height && renderbuffer->GetSamples() == entry.Samples &&
static_cast<Uint32>(renderbuffer->GetInternalFormat()) == entry.Format;
}
Bool MemoHolds(const BuildMemo& memo, const FramebufferObject& fbo, MGPipeFramebufferTarget target) const {
if (memo.Target != static_cast<Uint8>(target) || memo.FboLifetimeId != fbo.GetLifetimeId() ||
memo.ObjectVersion != fbo.GetObjectVersion() || memo.AllocatorEpoch != MGPipeSlots().ResetEpoch() ||
memo.TextureTableEpoch != MGPipeTextureEmitterInstance().TableEpoch()) {
return false;
}
const auto& all = fbo.GetAllAttachmentObjects();
for (Uint32 i = 0; i < memo.AttachmentCount; ++i) {
const MemoAttachment& entry = memo.Attachments[i];
if (!AttachmentMatches(entry, all[entry.Point])) return false;
}
return true;
}
void FillMemo(BuildMemo& memo, const FramebufferObject& fbo, MGPipeFramebufferTarget target,
const MGPFramebufferState& state) {
memo.Target = static_cast<Uint8>(target);
memo.FboLifetimeId = fbo.GetLifetimeId();
memo.ObjectVersion = fbo.GetObjectVersion();
memo.AllocatorEpoch = MGPipeSlots().ResetEpoch();
memo.TextureTableEpoch = MGPipeTextureEmitterInstance().TableEpoch();
memo.AttachmentCount = 0;
const auto& all = fbo.GetAllAttachmentObjects();
for (SizeT point = 0; point < all.size(); ++point) {
const auto& attachment = all[point];
if (attachment.IsEmpty()) continue;
MemoAttachment& entry = memo.Attachments[memo.AttachmentCount++];
entry = MemoAttachment{};
entry.Point = static_cast<Uint32>(point);
entry.IsTexture = attachment.IsTexture();
if (entry.IsTexture) {
const auto& texture = attachment.GetTexture();
entry.LifetimeId = texture->GetLifetimeId();
entry.ShapeVersion = texture->GetShapeVersion();
entry.Samples = texture->GetSamples();
entry.FixedSampleLocations = texture->HasFixedSampleLocations();
} else {
const auto& renderbuffer = attachment.GetRenderbuffer();
entry.LifetimeId = renderbuffer->GetLifetimeId();
entry.Width = renderbuffer->GetWidth();
entry.Height = renderbuffer->GetHeight();
entry.Samples = renderbuffer->GetSamples();
entry.Format = static_cast<Uint32>(renderbuffer->GetInternalFormat());
}
}
memo.State = state;
memo.Valid = true;
}
// Direct-mapped on (framebuffer lifetime id, target); Named never reaches it.
Array<BuildMemo, 16> m_buildMemo{};
Uint64 m_memoHits = 0;
Array<Uint64, 2> m_lastEmitted{};
// The per-FRAMEBUFFER suppressor for Named records, slot-indexed with the generation
// checked, exactly as the applier's own table is. A framebuffer has no wire lifetime
+40 -9
View File
@@ -2640,6 +2640,44 @@ namespace MobileGL::MG_Pipe {
// consumer mask gains bit 13 in the same
// commit, or R-8 withholds the whole family.
kMGPipeWiredBufferBindingSubsystem;
// P15: the dirty bits whose family this build, push mask and consumer emit - every
// condition of the per-bit gate in MGPipeValidateForVerb except the dirty bit itself.
Uint32 ComputeLiveDirtyBits(Uint64 pushMask) {
Uint32 live = 0;
for (Uint32 b = 0; b < kMGPipeDirtyCount; ++b) {
const Uint64 subsystem = MGPipeSubsystemForDirty(static_cast<MGPipeDirty>(b));
if (subsystem != 0 && (pushMask & subsystem) != 0 && (kMGPipeWiredSubsystems & subsystem) != 0 &&
P4aFamilyHasItsConsumer(subsystem) && P4aFamilyDependenciesAreSet(subsystem, pushMask) &&
P5eFamilyIsLive(subsystem, pushMask)) {
live |= MGPipeDirtyBit(static_cast<MGPipeDirty>(b));
}
}
return live;
}
// Per verb, these conditions used to be re-asked for every bit (~15 consumer-registration
// and dependency-table walks per draw). In monolith their only inputs are the push mask and
// the registered consumer, so the answer is kept until either moves. Under a transport the
// caps mirror answers and can move with a new session, so it is recomputed every verb.
Uint32 LiveDirtyBits(Uint64 pushMask) {
#if MOBILEGL_BUILD_DISAGGREGATED
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) return ComputeLiveDirtyBits(pushMask);
#endif
static Bool valid = false;
static Uint64 cachedPushMask = 0;
static const MGPipeResourceOps* cachedConsumer = nullptr;
static Uint32 cachedLive = 0;
const MGPipeResourceOps* consumer = MGPipeGetResourceOps();
if (!valid || cachedPushMask != pushMask || cachedConsumer != consumer) {
cachedLive = ComputeLiveDirtyBits(pushMask);
cachedPushMask = pushMask;
cachedConsumer = consumer;
valid = true;
}
return cachedLive;
}
// Each family constant is either 0 or its own subsystem bit and nothing else. Without
// this a header that set the wrong constant - the sampler bit in the program header,
// say - would switch the wrong family on and every gate would still pass.
@@ -3621,15 +3659,8 @@ namespace MobileGL::MG_Pipe {
// binding-point family and asks bit 13's OWN consumer bit rather than the resource
// family's. It answers true for every subsystem but bit 13, so nothing that emitted
// before P5e changes.
const auto wants = [&](MGPipeDirty bit) {
const Uint64 subsystem = MGPipeSubsystemForDirty(bit);
return subsystem != 0 && (pushMask & subsystem) != 0 &&
(kMGPipeWiredSubsystems & subsystem) != 0 &&
P4aFamilyHasItsConsumer(subsystem) &&
P4aFamilyDependenciesAreSet(subsystem, pushMask) &&
P5eFamilyIsLive(subsystem, pushMask) &&
(dirty & MGPipeDirtyBit(bit)) != 0;
};
const Uint32 liveDirty = LiveDirtyBits(pushMask) & dirty;
const auto wants = [&](MGPipeDirty bit) { return (liveDirty & MGPipeDirtyBit(bit)) != 0; };
Uint64 payloadBytes = 0;
// A fresh context is a fresh server, and that is true of EVERY subsystem, so it is
+2 -1
View File
@@ -663,7 +663,8 @@ namespace MobileGL::MG_Pipe {
// composite.
MGPipeHandle AcquireShaderCsoHandle(const ProgramObject& program) {
const Uint64 lifetimeId = program.GetLifetimeId();
const MGPipeHandle existing = MGPipeSlots().FindByLifetimeId(MGPipeKind::ShaderCso, lifetimeId);
const MGPipeHandle existing =
MGPipeSlots().FindByLifetimeIdHinted(MGPipeKind::ShaderCso, lifetimeId, program.PipeHandleHint());
if (!MGPipeHandleIsNull(existing)) return existing;
// A composite is minted off ITS OWN lifetime id, out of the reserved band, and is
// an ordinary ShaderCso handle in every other respect - the same kind, the same
+4 -2
View File
@@ -281,7 +281,8 @@ namespace MobileGL::MG_Pipe {
// gating it would make the vertex-input subsystem emit null handles whenever the
// resource subsystem is off. Only the CALLS are gated (D-A1).
MGPipeHandle Acquire(BufferObject& buffer) {
const MGPipeHandle handle = MGPipeSlots().Acquire(MGPipeKind::Buffer, buffer.GetLifetimeId());
const MGPipeHandle handle =
MGPipeSlots().AcquireHinted(MGPipeKind::Buffer, buffer.GetLifetimeId(), buffer.PipeHandleHint());
const SizeT slot = handle.Slot;
if (slot >= m_bySlot.size()) m_bySlot.resize(slot + 1);
m_bySlot[slot].Object = &buffer;
@@ -292,7 +293,8 @@ namespace MobileGL::MG_Pipe {
// The handle a buffer already has, or the null handle. Never mints - the emission
// path calls Acquire, the query paths call this.
MGPipeHandle Find(const BufferObject& buffer) const {
return MGPipeSlots().FindByLifetimeId(MGPipeKind::Buffer, buffer.GetLifetimeId());
return MGPipeSlots().FindByLifetimeIdHinted(MGPipeKind::Buffer, buffer.GetLifetimeId(),
buffer.PipeHandleHint());
}
// D-D's inverse, and a RAW pointer is exact here: the entry exists only between the
+5 -2
View File
@@ -859,7 +859,9 @@ namespace MobileGL::MG_Pipe {
unitSampler ? unitSampler.get() : texture->GetSamplerObject().get();
if (MG_State::GLState::SamplesAsIncompleteTexture(texture.get(), effective)) continue;
entry.Texture = MGPipeSlots().Acquire(MGPipeKind::Texture, texture->GetLifetimeId());
entry.Texture = MGPipeSlots().AcquireHinted(
MGPipeKind::Texture, texture->GetLifetimeId(),
texture->PipeHandleHint(ITextureObject::PipeHintKind::Texture));
// D-A4: a texture the sampler-view resolution names in an emitted MGPBoundView
// is SAMPLER-bound from then on (sticky; the texture emitter's contract door,
// since this header is included BY TextureEmit.h). One early-out per unit per
@@ -976,7 +978,8 @@ namespace MobileGL::MG_Pipe {
MGPipeHandle AcquireSamplerView(const ITextureObject& texture, MGPipeHandle textureHandle,
Uint64& payloadBytes) {
const MGPipeHandle handle =
MGPipeSlots().Acquire(MGPipeKind::SamplerViewCso, texture.GetLifetimeId());
MGPipeSlots().AcquireHinted(MGPipeKind::SamplerViewCso, texture.GetLifetimeId(),
texture.PipeHandleHint(ITextureObject::PipeHintKind::SamplerView));
const SizeT slot = handle.Slot;
if (slot >= m_viewLatch.size()) m_viewLatch.resize(slot + 1);
ViewLatch& latch = m_viewLatch[slot];
+25
View File
@@ -223,6 +223,31 @@ namespace MobileGL::MG_Pipe {
return MGPipeHandle{it->second, entry->Gen};
}
MGPipeHandle MGPipeSlotAllocator::FindByLifetimeIdHinted(MGPipeKind kind, Uint64 lifetimeId,
Uint64& hint) const {
if (hint != 0 && lifetimeId != 0) {
#if MOBILEGL_BUILD_DISAGGREGATED
MGPipeRefuseAllocatorFromApplyThread("FindByLifetimeIdHinted");
#endif
const Uint32 slot = static_cast<Uint32>(hint);
const Uint32 gen = static_cast<Uint32>(hint >> 32);
const SlotState* entry = EntryOf(StateOf(kind), kind, slot);
if (entry != nullptr && entry->Live && entry->Gen == gen && entry->LifetimeId == lifetimeId)
return MGPipeHandle{slot, gen};
}
const MGPipeHandle found = FindByLifetimeId(kind, lifetimeId);
hint = MGPipeHandleIsNull(found) ? 0 : MGPipePackHandleHint(found);
return found;
}
MGPipeHandle MGPipeSlotAllocator::AcquireHinted(MGPipeKind kind, Uint64 lifetimeId, Uint64& hint) {
const MGPipeHandle existing = FindByLifetimeIdHinted(kind, lifetimeId, hint);
if (!MGPipeHandleIsNull(existing)) return existing;
const MGPipeHandle minted = Acquire(kind, lifetimeId);
hint = MGPipeHandleIsNull(minted) ? 0 : MGPipePackHandleHint(minted);
return minted;
}
MGPipeHandle MGPipeSlotAllocator::Acquire(MGPipeKind kind, Uint64 lifetimeId) {
#if MOBILEGL_BUILD_DISAGGREGATED
MGPipeRefuseAllocatorFromApplyThread("Acquire");
+12
View File
@@ -72,6 +72,12 @@ namespace MobileGL::MG_Pipe {
MGPipeHandle FindByLifetimeId(MGPipeKind kind, Uint64 lifetimeId) const;
// FindByLifetimeId, then AllocateFor when it misses. The ordinary client path.
MGPipeHandle Acquire(MGPipeKind kind, Uint64 lifetimeId);
// P15: the same two answers, first trying `hint` - the handle this object was last given,
// kept on the frontend object itself (MGPipePackHandleHint). The hint is believed only when
// the slot it names is live, at that generation, and owned by this very lifetime id, so a
// reset, a free or a recycle can only make it miss; the hint is refreshed on every miss.
MGPipeHandle FindByLifetimeIdHinted(MGPipeKind kind, Uint64 lifetimeId, Uint64& hint) const;
MGPipeHandle AcquireHinted(MGPipeKind kind, Uint64 lifetimeId, Uint64& hint);
// Returns the slot to the free list. The Gen bump happens on the NEXT handout of that
// slot, not here, so a handle that is freed twice cannot skip a generation and the
@@ -179,6 +185,12 @@ namespace MobileGL::MG_Pipe {
// The monolith's one client allocator. Under split there is one per client context.
MGPipeSlotAllocator& MGPipeSlots();
// A handle packed into the Uint64 a frontend object keeps as its hint (0 = none; slot 0 is
// never handed out).
inline Uint64 MGPipePackHandleHint(MGPipeHandle handle) {
return static_cast<Uint64>(handle.Slot) | (static_cast<Uint64>(handle.Gen) << 32);
}
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (hd, CONTRACT-P5C §3.1 / §6 layer 1): with an active transport this allocator is a
// CLIENT-only surface. Its singleton accessor and original three method guards reject apply -
+11 -1
View File
@@ -590,7 +590,11 @@ namespace MobileGL::MG_Pipe {
// gating the mint would make the other subsystems emit null handles in exactly the
// A/B arm that exists to isolate them. Only the CALLS are gated.
MGPipeHandle AcquireTexture(Uint64 lifetimeId, ITextureObject* object) {
const MGPipeHandle handle = MGPipeSlots().Acquire(MGPipeKind::Texture, lifetimeId);
const MGPipeHandle handle =
object != nullptr
? MGPipeSlots().AcquireHinted(MGPipeKind::Texture, lifetimeId,
object->PipeHandleHint(ITextureObject::PipeHintKind::Texture))
: MGPipeSlots().Acquire(MGPipeKind::Texture, lifetimeId);
Entry& entry = EntryFor(m_textures, handle);
RetireIfRecycled(entry, handle);
entry.Texture = object;
@@ -1287,7 +1291,12 @@ namespace MobileGL::MG_Pipe {
// MGPipeResourceTracker::ResetForTest for the rule this restates: a texture handle and
// the applier record it names are SHARE-GROUP OBJECT STATE, so nothing here is
// per-context and no re-publication path exists or may exist.
// Moves whenever the handle table below is dropped (ResetForTest), so a memo that skipped
// NoteTextureBoundAs / AcquireTexture for handles it already noted knows to note again.
Uint64 TableEpoch() const { return m_tableEpoch; }
void ResetForTest() {
++m_tableEpoch;
// EVERY REFERENCE THIS EMITTER OWES IS GIVEN BACK FIRST. A case that dropped the
// table without releasing would pin cache entries for the rest of the process and
// the next case's LRU would mint over capacity for reasons it cannot see.
@@ -1689,6 +1698,7 @@ namespace MobileGL::MG_Pipe {
MGPTextureParams m_lastParams{};
MGPSubData m_lastSubData{};
Uint64 m_tableEpoch = 0;
Uint64 m_creates = 0;
Uint64 m_respecifies = 0;
Uint64 m_paramSets = 0;
+20 -4
View File
@@ -256,13 +256,29 @@ namespace MobileGL::MG_Pipe {
for (SizeT i = 0; i < kAttribs; ++i) {
if (vao->GetAttribute(static_cast<Uint>(i)).Enabled) count = static_cast<Uint32>(i) + 1;
}
// P15: interleaved attributes share one buffer (FCL MC 1.21.5: every chunk draw
// re-points ~4 attributes at the next chunk's buffer), so the previous attribute's
// answer is reused for the same object - one lookup and one sticky-bit note per
// buffer instead of per attribute. Scoped to this loop: same object, same handle.
const MG_State::GLState::BufferObject* lastBuffer = nullptr;
MGPipeHandle lastHandle = kMGPipeNullHandle;
for (SizeT i = 0; i < count; ++i) {
const auto& attrib = vao->GetAttribute(static_cast<Uint>(i));
MGPVertexBuffer& entry = m_entries[i];
entry = MGPVertexBuffer{};
entry.Res = attrib.Buffer ? MGPipeSlots().Acquire(MGPipeKind::Buffer,
attrib.Buffer->GetLifetimeId())
: kMGPipeNullHandle;
const Bool sameBuffer = attrib.Buffer && attrib.Buffer.get() == lastBuffer;
if (sameBuffer) {
entry.Res = lastHandle;
} else {
entry.Res = attrib.Buffer ? MGPipeSlots().AcquireHinted(MGPipeKind::Buffer,
attrib.Buffer->GetLifetimeId(),
attrib.Buffer->PipeHandleHint())
: kMGPipeNullHandle;
if (attrib.Buffer) {
lastBuffer = attrib.Buffer.get();
lastHandle = entry.Res;
}
}
if (ownedClientBuffers != nullptr && attrib.Enabled && !attrib.Buffer) {
entry.Res = (*ownedClientBuffers)[i];
}
@@ -271,7 +287,7 @@ namespace MobileGL::MG_Pipe {
// glNamedBuffer* may never be bound at any resource emission, but a draw
// that fetches from it resolves it right here, on the GL thread, at every
// draw. Sticky, so one draw is enough for the rest of its life.
MGPipeResourceTrackerInstance().NoteBoundAs(entry.Res, BufferTarget::Vertex);
if (!sameBuffer) MGPipeResourceTrackerInstance().NoteBoundAs(entry.Res, BufferTarget::Vertex);
// The attribute's own byte offset lives in MGPVertexAttribWire::Offset,
// so the entry's is the BINDING's, which the frontend already folded in.
entry.Offset = 0;
@@ -202,6 +202,9 @@ namespace MobileGL {
// glGenBuffers) and never the heap address (recycled by the allocator): both
// let a deleted-and-recreated buffer answer to a dead one's cache entry.
Uint64 GetLifetimeId() const { return m_lifetimeId; }
// P15: the pipe client's last Buffer handle for this object (MGPipeSlotAllocator::
// AcquireHinted). Only a hint: it is checked against the allocator on every use.
Uint64& PipeHandleHint() const { return m_pipeHandleHint; }
// Monotonic counter bumped on every shadow mutation; backends use it to
// validate cached transient slices.
Uint64 GetChangeSerial() const;
@@ -238,6 +241,7 @@ namespace MobileGL {
const Uint m_externalIndex = 0;
const Uint64 m_lifetimeId = AllocateLifetimeId();
mutable Uint64 m_pipeHandleHint = 0;
SizeT m_size = 0;
BufferUsage m_usage = BufferUsage::StaticDraw;
// Owns the buffer's bytes (CPU shadow or backend persistent GPU map) and
@@ -1281,6 +1281,9 @@ namespace MobileGL::MG_State::GLState {
// the next glCreateProgram, this distinguishes a deleted-and-recreated program from the
// original, so an identity cache can't false-hit on name recycling.
Uint64 GetLifetimeId() const { return m_lifetimeId; }
// P15: the pipe client's last ShaderCso handle for this program (MGPipeSlotAllocator::
// FindByLifetimeIdHinted). Only a hint: it is checked against the allocator on every use.
Uint64& PipeHandleHint() const { return m_pipeHandleHint; }
private:
// ---- The one and only join gate for link output (P1 invariant I5) ----
@@ -1372,6 +1375,7 @@ namespace MobileGL::MG_State::GLState {
// writes them.
const Uint m_externalIndex = 0;
const Uint64 m_lifetimeId = 0;
mutable Uint64 m_pipeHandleHint = 0;
// The attach lists are mutated only in Link()'s GL-thread prologue, which is why
// glGetAttachedShaders / GL_ATTACHED_SHADERS / the orphan-shader sweep need no join.
Vector<SharedPtr<ShaderObject>> m_shaders;
@@ -82,6 +82,10 @@ namespace MobileGL::MG_State::GLState {
virtual Bool HasFixedSampleLocations() const = 0;
virtual void SetFixedSampleLocations(Bool fixedSampleLocations) = 0;
virtual Uint64 GetLifetimeId() const = 0;
// P15: the pipe client's last handles for this texture, one per kind it is looked up under
// (MGPipeSlotAllocator::AcquireHinted). Only hints: checked against the allocator on use.
enum class PipeHintKind : Uint8 { Texture = 0, SamplerView = 1, Count = 2 };
virtual Uint64& PipeHandleHint(PipeHintKind kind) const = 0;
// Which aspect of a packed depth/stencil texture a sampler reads (GL 4.6 core 8.10).
// DEPTH_COMPONENT until set, and meaningless for every other format.
virtual GLenum GetDepthStencilTextureMode() const = 0;
@@ -164,6 +168,7 @@ namespace MobileGL::MG_State::GLState {
Bool HasFixedSampleLocations() const override;
void SetFixedSampleLocations(Bool fixedSampleLocations) override;
Uint64 GetLifetimeId() const override;
Uint64& PipeHandleHint(PipeHintKind kind) const override { return m_pipeHandleHints[static_cast<SizeT>(kind)]; }
// A plain texture owns its storage; TextureObjectView overrides this.
const SharedPtr<ITextureObject>& GetViewStorageOwner() const override;
Uint GetViewMinLevel() const override { return m_viewMinLevel; }
@@ -241,6 +246,7 @@ namespace MobileGL::MG_State::GLState {
const Uint m_externalIndex;
const Uint64 m_lifetimeId;
mutable Uint64 m_pipeHandleHints[static_cast<SizeT>(PipeHintKind::Count)] = {};
const TextureTarget m_target = TextureTarget::Unknown;
TextureInternalFormat m_internalFormat = TextureInternalFormat::Unknown;
SharedPtr<SamplerObject> m_sampler = nullptr;
@@ -1264,6 +1264,111 @@ TEST(FramebufferEmit, AFramebufferRecordThatNamesNoUsableHandleIsRefusedRatherTh
<< "an out-of-range slot resized the table instead of being refused";
}
// ============================ P15: the bound-target build memo ============================
//
// EmitFramebufferState reuses the record it built for the same framebuffer and target while
// nothing the build reads has moved (MGPipeFramebufferEmitter::BuildBoundFramebufferState). Each
// case first proves the memo ENGAGED (an unchanged second emission is a hit), then moves one input
// the framebuffer's own version does not see and expects the record to follow it, by field name.
namespace {
MGPipeHandle HandleOfTexture(const SharedPtr<TextureObject2D>& texture) {
return MGPipeSlots().FindByLifetimeId(MGPipeKind::Texture, texture->GetLifetimeId());
}
} // namespace
TEST(FramebufferEmit, TheBuildMemoEngagesAndFollowsAnAttachedTexturesRespecify) {
FramebufferScope scope;
const auto color = MakeColorTexture(80, 32);
const auto fbo = MakeShared<FramebufferObject>(20);
fbo->AttachTexture(FramebufferAttachmentType::Color0, color, TextureUploadTarget::Texture2D);
BindDrawAndRead(fbo, fbo);
Framebuffers().EmitFramebufferState(Ctx());
const Uint64 hits = Framebuffers().MemoHitCount();
EXPECT_EQ(Framebuffers().EmitFramebufferState(Ctx()), 0u);
ASSERT_EQ(Framebuffers().MemoHitCount(), hits + 1) << "an unchanged framebuffer did not reuse its built record";
// The texture's level 0 grows; the framebuffer's object version does not move.
color->AllocateStorage(TextureUploadTarget::Texture2D, 0, MipmapInput{IntVec3{64, 64, 1}, 64u * 64u * 4u});
EXPECT_EQ(Framebuffers().EmitFramebufferState(Ctx()), sizeof(MGPFramebufferState));
EXPECT_EQ(Framebuffers().LastDraw().Width, 64u) << "MGPFramebufferState::Width";
EXPECT_EQ(Framebuffers().LastDraw().Height, 64u) << "MGPFramebufferState::Height";
}
// The build runs when the bound framebuffer changes, so the memo has to hold more than the last
// framebuffer: an A/B/A switch must find A's record again.
TEST(FramebufferEmit, TheBuildMemoKeepsEachFramebufferAcrossABindingSwitch) {
FramebufferScope scope;
const auto colorA = MakeColorTexture(83, 32);
const auto colorB = MakeColorTexture(84, 16);
const auto a = MakeShared<FramebufferObject>(24);
const auto b = MakeShared<FramebufferObject>(25);
a->AttachTexture(FramebufferAttachmentType::Color0, colorA, TextureUploadTarget::Texture2D);
b->AttachTexture(FramebufferAttachmentType::Color0, colorB, TextureUploadTarget::Texture2D);
BindDrawAndRead(a, a);
Framebuffers().EmitFramebufferState(Ctx());
BindDrawAndRead(b, b);
Framebuffers().EmitFramebufferState(Ctx());
const Uint64 hits = Framebuffers().MemoHitCount();
BindDrawAndRead(a, a);
Framebuffers().EmitFramebufferState(Ctx());
EXPECT_EQ(Framebuffers().MemoHitCount(), hits + 1) << "switching back to a framebuffer rebuilt its record";
EXPECT_EQ(Framebuffers().LastDraw().Width, 32u) << "MGPFramebufferState::Width";
}
TEST(FramebufferEmit, TheBuildMemoFollowsARestoragedAttachedRenderbuffer) {
FramebufferScope scope;
const auto renderbuffer = MakeShared<RenderbufferObject>(4);
renderbuffer->SetInternalFormat(TextureInternalFormat::RGBA8);
renderbuffer->AllocateStorage(IntVec2{32, 32});
const auto fbo = MakeShared<FramebufferObject>(21);
fbo->AttachRenderbuffer(FramebufferAttachmentType::Color0, renderbuffer);
BindDrawAndRead(fbo, fbo);
Framebuffers().EmitFramebufferState(Ctx());
const Uint64 hits = Framebuffers().MemoHitCount();
Framebuffers().EmitFramebufferState(Ctx());
ASSERT_EQ(Framebuffers().MemoHitCount(), hits + 1) << "an unchanged framebuffer did not reuse its built record";
renderbuffer->AllocateStorage(IntVec2{48, 40});
Framebuffers().EmitFramebufferState(Ctx());
EXPECT_EQ(Framebuffers().LastDraw().Width, 48u) << "MGPFramebufferState::Width";
EXPECT_EQ(Framebuffers().LastDraw().Height, 40u) << "MGPFramebufferState::Height";
}
TEST(FramebufferEmit, TheBuildMemoFollowsAnAttachedTexturesSampleCount) {
FramebufferScope scope;
const auto color = MakeColorTexture(81, 32);
const auto fbo = MakeShared<FramebufferObject>(22);
fbo->AttachTexture(FramebufferAttachmentType::Color0, color, TextureUploadTarget::Texture2D);
BindDrawAndRead(fbo, fbo);
Framebuffers().EmitFramebufferState(Ctx());
Framebuffers().EmitFramebufferState(Ctx());
// Samples move the params version, not the shape.
color->SetSamples(4);
Framebuffers().EmitFramebufferState(Ctx());
EXPECT_EQ(Framebuffers().LastDraw().Samples, 4u) << "MGPFramebufferState::Samples";
}
TEST(FramebufferEmit, TheBuildMemoReNotesBindBitsAfterTheTextureTableIsDropped) {
FramebufferScope scope;
const auto color = MakeColorTexture(82, 32);
const auto fbo = MakeShared<FramebufferObject>(23);
fbo->AttachTexture(FramebufferAttachmentType::Color0, color, TextureUploadTarget::Texture2D);
BindDrawAndRead(fbo, fbo);
Framebuffers().EmitFramebufferState(Ctx());
const MGPipeHandle handle = HandleOfTexture(color);
ASSERT_FALSE(MGPipeHandleIsNull(handle));
ASSERT_NE(MGPipeTextureEmitterInstance().TextureBindMask(handle) & kMGPipeBindRenderTarget, 0)
<< "the attachment's RENDER_TARGET bit was never noted";
// A dropped table (a replaced session) forgets the sticky bits; the next build must note them again.
MGPipeTextureEmitterInstance().ResetForTest();
MGPipeSetHashSuppressorInstance().InvalidateAll();
Framebuffers().EmitFramebufferState(Ctx());
EXPECT_NE(MGPipeTextureEmitterInstance().TextureBindMask(handle) & kMGPipeBindRenderTarget, 0)
<< "MGPResourceDesc::BindMask lost RENDER_TARGET after the texture table was dropped";
}
int main(int argc, char** argv) {
// Before anything logs: the logger reads this variable once, on its first write, and
// caches the handle. The name carries this process's pid, and the file is removed on the
@@ -97,6 +97,38 @@ namespace {
EXPECT_TRUE(allocator.IsLive(MGPipeKind::Buffer, reused));
}
// P15: a hint (the handle a frontend object was last given, kept on the object) is a shortcut
// and never an answer of its own. It must give the live handle back, and it must miss - never
// name another object's handle - after a free, a recycle of its slot, or a reset.
TEST(SlotAllocator, AHandleHintIsOnlyBelievedWhileItsSlotStillBelongsToTheObject) {
MGPipeSlotAllocator allocator;
constexpr Uint64 kFirst = 1001, kSecond = 1002;
Uint64 hint = 0;
const MGPipeHandle minted = allocator.AcquireHinted(MGPipeKind::Buffer, kFirst, hint);
ASSERT_FALSE(MGPipeHandleIsNull(minted));
EXPECT_EQ(hint, MGPipePackHandleHint(minted)) << "a mint must leave the hint naming the new handle";
EXPECT_TRUE(allocator.FindByLifetimeIdHinted(MGPipeKind::Buffer, kFirst, hint) == minted);
// The slot is freed and handed to another object. The dead object's hint must miss, even
// when it is forged to carry the successor's exact slot and generation: the owner moved.
allocator.Free(MGPipeKind::Buffer, minted);
EXPECT_TRUE(MGPipeHandleIsNull(allocator.FindByLifetimeIdHinted(MGPipeKind::Buffer, kFirst, hint)))
<< "a freed object's hint still answered";
Uint64 staleHint = MGPipePackHandleHint(minted);
const MGPipeHandle successor = allocator.AcquireHinted(MGPipeKind::Buffer, kSecond, staleHint);
EXPECT_EQ(successor.Slot, minted.Slot) << "the free list did not hand the slot back";
Uint64 forged = MGPipePackHandleHint(successor); // the successor's exact slot and generation
EXPECT_TRUE(MGPipeHandleIsNull(allocator.FindByLifetimeIdHinted(MGPipeKind::Buffer, kFirst, forged)))
<< "a hint naming another object's live slot answered for the dead object";
// A reset forgets every handle; a hint from before it can only miss.
Uint64 beforeReset = MGPipePackHandleHint(successor);
allocator.Reset();
EXPECT_TRUE(MGPipeHandleIsNull(allocator.FindByLifetimeIdHinted(MGPipeKind::Buffer, kSecond, beforeReset)))
<< "a hint survived the allocator's reset";
}
TEST(SlotAllocator, FreedSlotComesBackBeforeHighWaterGrows) {
MGPipeSlotAllocator allocator;
@@ -277,6 +277,34 @@ namespace {
}
}
// P15: the rebuild reuses the previous attribute's handle when it names the SAME buffer object
// (interleaved attributes), and only then. Two buffers interleaved A, A, B, A: every entry must
// carry its own buffer's handle, including the A after the B.
TEST(VertexInputEmit, InterleavedAttributesOnTwoBuffersEachCarryTheirOwnBuffersHandle) {
EmitterScope scope;
const SharedPtr<VertexArrayObject> vao = MakeVao(1);
const SharedPtr<BufferObject> a = Ctx().CreateBufferObject(1);
const SharedPtr<BufferObject> b = Ctx().CreateBufferObject(2);
a->Respecify(256, nullptr);
b->Respecify(256, nullptr);
const SharedPtr<BufferObject>* owners[] = {&a, &a, &b, &a};
for (Uint i = 0; i < 4; ++i) {
vao->SetAttributeFormat(i, 4, DataType::Float32, false, 16, static_cast<SizeT>(4 * i), false, false, 16);
vao->MirrorPointerIntoBinding(i, *owners[i], static_cast<SizeT>(4 * i), 16);
vao->BindAttributeBuffer(i, *owners[i]);
vao->EnableAttribute(i);
}
Emitter().EmitVertexBuffers(Ctx(), 0);
ASSERT_EQ(Emitter().LastVertexBuffers().Count, 4u);
for (Uint i = 0; i < 4; ++i) {
const MGPipeHandle expected =
MGPipeSlots().FindByLifetimeId(MGPipeKind::Buffer, (*owners[i])->GetLifetimeId());
ASSERT_FALSE(MGPipeHandleIsNull(expected));
EXPECT_TRUE(Emitter().LastEntries()[i].Res == expected)
<< "MGPVertexBuffer::Res of entry " << i << " is not its own buffer's handle";
}
}
// KHR-GL43.vertex_attrib_binding.basic-input-case7/8: a pointer call's stride 0 means
// "tightly packed" and the frontend already resolved it to the element size, so a zero
// that reaches the wire can only have come from the binding model - where it means every