[Fix] (Espryt): sweep the twin table on object churn again, refuse to run an arm the operator disabled, and give the hot lookups back their array probe

- The handle arm took only ONE of the registry's two sweep drivers. The map arm sweeps
  every 64 first-time insertions BECAUSE object churn, not draw count, is what makes the
  sweep urgent: a CTS-shaped case runs ~10 per-draw ticks, so the 1024-tick draw-path
  driver alone spans ~100 cases' worth of dead, gigabyte-sized twins. BackendSlotTable now
  carries the same kCreationGCInterval = 64 creation tick, swept before the entry reference
  exists for the same reason the map arm sweeps there. Without this the slice REGRESSED the
  memory it was supposed to leave unchanged.
- Fatal{PipeLegacyMemosDisabled} now aborts. It logged and then returned false, which fell
  straight into the legacy arm the operator had just made unreachable: a green run measured
  on the wrong arm, and the exact lever HandleRecycleScenario's arms are selected with. It
  is also resolved at backend context creation now, not on the first twin lookup, so a
  process that twins nothing still learns its knobs leave it with no arm at all.
- EsprytSlotTablesEnabled() becomes an inline latch over an out-of-line resolver. It is
  consulted on every Find/GetOrCreate/HandleOf/ForEachLive/CollectGarbage*, i.e. several
  times per draw, and as a cross-TU call with no LTO that was a PLT call per lookup.
- HandleOf keeps a one-entry lifetimeId -> handle memo, so the three per-draw resolution
  paths whose TwinLookupMemos this slice deleted go back to an integer compare plus an
  array index instead of the allocator's ByLifetimeId hash - which is the "direct slot
  indexing" the memo removal was traded for. It cannot serve a stale answer: a lifetime id
  is never handed out twice, and FindByHandle compares Gen anyway.
- GetOrCreate(nullptr) returns a parking slot instead of dereferencing null in a release
  build; the map arm inserted a null key and SyncTextureObjectToBackend documents relying
  on that tolerance.
- ReclaimDeadSlots moves the twin out before it writes the entry, so a twin destructor that
  re-entered GetOrCreate and grew m_slots could not make the writes land in freed memory.
- The framebuffer binding-slot cache is gated on kMGPipeSubsystemEsprytSlots rather than on
  the compile-time MOBILEGL_PIPE_PUSH, so MOBILEGL_PIPE_PUSH=0 stays the faithful
  all-subsystems-pull control ConfigLoader.cpp documents. The poison bypass stays closed on
  the arm that ships.
- MGB_TWIN_KIND_PARAM/ARG stop leaking into every TU that includes Managers.h: the twelve
  declaration and definition sites name a TwinRegistry alias template that swallows the
  kind in the pull build, and the one remaining macro is #undef'd after the class.
- The twin lookup inside BindCurrentFBO stops shadowing the framebuffer binding slot in a
  function whose whole subject is which "slot" is meant.
This commit is contained in:
2026-09-07 23:18:09 -04:00
parent 149e26a79a
commit 9a8369296e
4 changed files with 220 additions and 49 deletions
+24 -15
View File
@@ -171,20 +171,30 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
#if MOBILEGL_PIPE_PUSH
Bool EsprytSlotTablesEnabled() {
// Latched once, not read per call: the two arms of StateBackendObjectRegistry keep
// their twins in different containers, so an answer that changed mid-run would strand
// every twin already built (and, for the driver ids those twins own, leak them).
static const Bool enabled = [] {
Bool ResolveEsprytSlotTablesArm() {
// Resolved once and latched by the inline EsprytSlotTablesEnabled() in SlotTables.h:
// the two arms of StateBackendObjectRegistry keep their twins in different containers,
// so an answer that changed mid-run would strand every twin already built (and, for
// the driver ids those twins own, leak them). InitDisplayAndContext() forces the
// resolution at backend context creation, so the trap below fires before the first
// draw rather than on the first twin lookup - a short-lived process that never twins
// anything used to never learn its knobs left it with no arm at all.
{
const Bool bitSet =
(MG_Config::Features.PipePush & MG_Pipe::kMGPipeSubsystemEsprytSlots) != 0;
#if MOBILEGL_PIPE_LEGACY_MEMOS
if (!bitSet && !MG_Config::Features.PipeLegacyMemos) {
// The operator asked for the handle arm to be OFF and the legacy arm to be
// unreachable at the same time, which leaves no arm at all. Say so at startup
// rather than silently running the thing they turned off (ARCHITECTURE.md 9.6).
// unreachable at the same time, which leaves no arm at all. This is a Fatal{},
// and a Fatal{} in this codebase STOPS (MG_Impl/Pipe/PipeFill.cpp's BadKnob and
// its verify trap are both MGLOG_F + abort). Returning here instead would run
// the very arm the operator disabled and hand back a green result measured on
// it - which is exactly the lever HandleRecycleScenario's arms are selected
// with, so a mis-set A/B would be scored silently against the wrong arm
// (ARCHITECTURE.md 9.6).
MGLOG_F("MGPipe: Fatal{PipeLegacyMemosDisabled, \"kMGPipeSubsystemEsprytSlots "
"is clear but MOBILEGL_PIPE_LEGACY_MEMOS=0\"}");
std::abort();
}
return bitSet;
#else
@@ -196,8 +206,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
return true;
#endif
}();
return enabled;
}
}
#endif
@@ -2775,7 +2784,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
return true;
}
StateBackendObjectRegistry<MG_State::GLState::VertexArrayObject, BackendVertexArrayObject MGB_TWIN_KIND_ARG(MG_Pipe::MGPipeKind::VertexElementsCso)>
TwinRegistry<MG_State::GLState::VertexArrayObject, BackendVertexArrayObject, MG_Pipe::MGPipeKind::VertexElementsCso>
g_backendVertexArrayObjects;
} // namespace VertexArrayImpl
@@ -5103,7 +5112,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
Array<Array<BackendTextureObject*, (SizeT)TextureTarget::TextureTargetCount>,
MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS>
g_boundTexturesCache;
StateBackendObjectRegistry<MG_State::GLState::ITextureObject, BackendTextureObject MGB_TWIN_KIND_ARG(MG_Pipe::MGPipeKind::Texture)> g_backendTextureObjects;
TwinRegistry<MG_State::GLState::ITextureObject, BackendTextureObject, MG_Pipe::MGPipeKind::Texture> g_backendTextureObjects;
} // namespace TextureImpl
namespace FramebufferImpl {
@@ -5856,7 +5865,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
return m_backendColorSlots[index];
}
StateBackendObjectRegistry<MG_State::GLState::FramebufferObject, BackendFramebufferObject MGB_TWIN_KIND_ARG(MG_Pipe::MGPipeKind::Framebuffer)>
TwinRegistry<MG_State::GLState::FramebufferObject, BackendFramebufferObject, MG_Pipe::MGPipeKind::Framebuffer>
g_backendFramebufferObjects;
Array<Uint16, SizeT(FramebufferTarget::FramebufferTargetCount)> g_fboSyncedSlotVersions = {0};
// Tracks the bound FBO's object version (bumped on any attachment/drawbuffer change)
@@ -6167,7 +6176,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// context never answers GL_NO_ERROR, and the build runs on the thread that would
// then spin forever.
constexpr Int kMaxDrainedProgramErrors = 32;
StateBackendObjectRegistry<MG_State::GLState::ProgramObject, BackendProgramObjectImpl MGB_TWIN_KIND_ARG(MG_Pipe::MGPipeKind::ShaderCso)> g_backendProgramObjects;
TwinRegistry<MG_State::GLState::ProgramObject, BackendProgramObjectImpl, MG_Pipe::MGPipeKind::ShaderCso> g_backendProgramObjects;
BackendProgramObjectImpl::BackendProgramObjectImpl() {
#ifdef TRACY_ENABLE
@@ -8685,7 +8694,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
Array<BackendSamplerObject*, MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS> g_boundSamplersCache;
StateBackendObjectRegistry<MG_State::GLState::SamplerObject, BackendSamplerObject MGB_TWIN_KIND_ARG(MG_Pipe::MGPipeKind::SamplerCso)> g_backendSamplerObjects;
TwinRegistry<MG_State::GLState::SamplerObject, BackendSamplerObject, MG_Pipe::MGPipeKind::SamplerCso> g_backendSamplerObjects;
} // namespace SamplerImpl
namespace RenderbufferImpl {
@@ -8797,7 +8806,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
MGLOG_D("RBO %u sync completed. backend ID %u", stateRBOObject->GetExternalIndex(), m_backendRBOId);
}
StateBackendObjectRegistry<MG_State::GLState::RenderbufferObject, BackendRenderbufferObject MGB_TWIN_KIND_ARG(MG_Pipe::MGPipeKind::Renderbuffer)>
TwinRegistry<MG_State::GLState::RenderbufferObject, BackendRenderbufferObject, MG_Pipe::MGPipeKind::Renderbuffer>
g_backendRenderbufferObjects;
} // namespace RenderbufferImpl
} // namespace MobileGL::MG_Backend::DirectGLES