[Fix] (DirectGLES): scope object deaths to their session, and keep container objects and staging rings per native context

This commit is contained in:
2026-10-02 05:07:59 -04:00
parent 335637af38
commit cd610934d7
5 changed files with 433 additions and 114 deletions
+140 -53
View File
@@ -10169,7 +10169,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// that second one is single-sample on both sides, where ES does allow conversion.
static Bool ResolveThenBlit(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0,
GLint dstX1, GLint dstY1, GLenum filter) {
static Uint s_resolveFramebuffer = 0;
static NativeContextName s_resolveFramebuffer; // per context: a framebuffer is never shared
static Uint s_resolveRenderbuffer = 0;
static GLenum s_resolveFormat = 0;
static GLsizei s_resolveWidth = 0;
@@ -10204,8 +10204,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
if (s_resolveFramebuffer == 0) {
g_GLESFuncs.glGenFramebuffers(1, &s_resolveFramebuffer);
if (s_resolveFramebuffer == 0) return false;
}
if (s_resolveRenderbuffer == 0) {
g_GLESFuncs.glGenRenderbuffers(1, &s_resolveRenderbuffer);
if (s_resolveFramebuffer == 0 || s_resolveRenderbuffer == 0) return false;
if (s_resolveRenderbuffer == 0) return false;
s_resolveFormat = 0;
}
@@ -10485,12 +10488,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
namespace ReplicateBlitImpl {
static Uint s_contextGeneration = ~0u;
static GLuint s_framebuffer = 0;
static NativeContextName s_framebuffer; // per context, as is the vertex array below
static GLuint s_texture = 0;
static GLenum s_textureFormat = 0;
static GLsizei s_textureWidth = 0;
static GLsizei s_textureHeight = 0;
static GLuint s_vertexArray = 0;
static NativeContextName s_vertexArray;
static GLuint s_depthProgram = 0;
static GLuint s_stencilProgram = 0;
static GLint s_depthUvTransform = -1;
@@ -14138,11 +14141,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
// caller. No flip anywhere.
namespace DepthStencilSamplingReadImpl {
static Uint s_contextGeneration = ~0u;
static GLuint s_colorFramebuffer = 0;
static NativeContextName s_colorFramebuffer; // per context, as are the vertex array and the
// slots' framebuffers below
static GLuint s_colorTexture = 0;
static GLsizei s_colorWidth = 0;
static GLsizei s_colorHeight = 0;
static GLuint s_vertexArray = 0;
static NativeContextName s_vertexArray;
static GLuint s_depthProgram = 0;
static GLuint s_stencilProgram = 0;
static GLint s_depthUvTransform = -1;
@@ -14162,7 +14166,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
// other aspect attaches its own (differently formatted) texture to its own point
// and the framebuffer is then incomplete - which reads as "no candidate format
// worked" and writes nothing at all.
GLuint framebuffer = 0;
GLuint texture = 0;
GLenum format = 0;
GLsizei width = 0;
@@ -14173,6 +14176,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
GLenum defaultFramebufferFormat = 0;
};
static StageSlot s_slots[2]; // [0] depth, [1] stencil
// Each slot's own framebuffer (see StageSlot), one per native context.
static NativeContextName s_slotFramebuffers[2];
static NativeContextName& SlotFramebuffer(const StageSlot& slot) { return s_slotFramebuffers[&slot - s_slots]; }
// `precision highp int` is not decoration: ESSL 3.00 defaults integers to mediump in
// the fragment language, which is allowed to be 16 bits - it would saw the top half
@@ -14214,6 +14220,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
s_programsFailed = false;
s_slots[0] = StageSlot{};
s_slots[1] = StageSlot{};
s_slotFramebuffers[0] = 0;
s_slotFramebuffers[1] = 0;
s_contextGeneration = g_backendContextGeneration;
}
if (s_programsFailed) {
@@ -14233,10 +14241,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
s_depthUvTransform = g_GLESFuncs.glGetUniformLocation(s_depthProgram, "uUvTransform");
s_stencilUvTransform = g_GLESFuncs.glGetUniformLocation(s_stencilProgram, "uUvTransform");
}
for (StageSlot& slot : s_slots) {
if (slot.framebuffer == 0) {
g_GLESFuncs.glGenFramebuffers(1, &slot.framebuffer);
if (slot.framebuffer == 0) return false;
for (NativeContextName& framebuffer : s_slotFramebuffers) {
if (framebuffer == 0) {
g_GLESFuncs.glGenFramebuffers(1, &framebuffer);
if (framebuffer == 0) return false;
}
}
if (s_colorFramebuffer == 0) {
@@ -14519,7 +14527,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
StageSlot& slot = s_slots[stencilAspect ? 1 : 0];
FramebufferImpl::BindFramebufferId(GL_DRAW_FRAMEBUFFER, slot.framebuffer);
FramebufferImpl::BindFramebufferId(GL_DRAW_FRAMEBUFFER, SlotFramebuffer(slot));
if (!StageAspect(slot, candidates, stencilAspect, isDefault, x, y, width, height)) {
MGLOG_E_ONCE("ReadPixels: no ES-compatible scratch format for the %s source",
stencilAspect ? "stencil" : "depth");
@@ -16221,6 +16229,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
Uint64 ContextToken = 0;
Uint64 ShareGroupToken = 0;
EGLContext Context = EGL_NO_CONTEXT;
// Context's lifetime serial (CurrentNativeContextSerial); a new one per eglCreateContext.
Uint64 Serial = 0;
EGLConfig Config = nullptr;
// The session surface this context draws to / reads from, as last made current for it.
EGLSurface Draw = EGL_NO_SURFACE;
@@ -16330,8 +16340,45 @@ namespace MobileGL::MG_Backend::DirectGLES {
// And the surface it was made current with: a bind to the same context on a different
// surface (one client context drawing to several windows) is a real switch.
thread_local EGLSurface t_boundNativeDraw = EGL_NO_SURFACE;
// ...and that context's lifetime serial, the key of its container objects.
thread_local Uint64 t_boundNativeSerial = 0;
std::atomic<Uint64> g_nativeContextSerials{0};
std::mutex& PerNativeContextRegistryMutex() {
static auto* mutex = new std::mutex();
return *mutex;
}
Vector<PerNativeContextBase*>& PerNativeContextRegistry() {
static auto* registry = new Vector<PerNativeContextBase*>();
return *registry;
}
} // namespace
Uint64 CurrentNativeContextSerial() { return t_boundNativeSerial; }
namespace {
std::atomic<Uint64> g_perNativeContextEpoch{1};
}
Uint64 PerNativeContextEpoch() { return g_perNativeContextEpoch.load(std::memory_order_acquire); }
void BumpPerNativeContextEpoch() { g_perNativeContextEpoch.fetch_add(1, std::memory_order_acq_rel); }
PerNativeContextBase::PerNativeContextBase() {
const std::lock_guard<std::mutex> lock(PerNativeContextRegistryMutex());
PerNativeContextRegistry().push_back(this);
}
PerNativeContextBase::~PerNativeContextBase() {
const std::lock_guard<std::mutex> lock(PerNativeContextRegistryMutex());
auto& registry = PerNativeContextRegistry();
registry.erase(std::remove(registry.begin(), registry.end(), this), registry.end());
}
void ForgetNativeContextObjects(Uint64 serial) {
if (serial == 0) return;
const std::lock_guard<std::mutex> lock(PerNativeContextRegistryMutex());
for (PerNativeContextBase* holder : PerNativeContextRegistry()) holder->ForgetContext(serial);
}
static NativeSessionState& AcquireNativeSession(Uint64 sessionKey) {
for (auto* session : NativeSessions()) {
if (session->SessionKey == sessionKey) return *session;
@@ -16910,6 +16957,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
EGL_NONE};
tuple.Context = g_EGLFuncs.eglCreateContext(g_Display, tuple.Config, share, contextAttribs);
if (tuple.Context != EGL_NO_CONTEXT) {
tuple.Serial = g_nativeContextSerials.fetch_add(1, std::memory_order_relaxed) + 1;
++g_displayUsers;
return true;
}
@@ -16918,6 +16966,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
const EGLint legacyContextAttribs[] = {EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE};
tuple.Context = g_EGLFuncs.eglCreateContext(g_Display, tuple.Config, share, legacyContextAttribs);
if (tuple.Context == EGL_NO_CONTEXT) return false;
tuple.Serial = g_nativeContextSerials.fetch_add(1, std::memory_order_relaxed) + 1;
++g_displayUsers;
return true;
}
@@ -16963,6 +17012,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
g_EGLFuncs.eglDestroyContext(g_Display, tuple->Context);
}
// This thread no longer has it current either way; a context created later at the same
// address must not read as "already bound".
if (t_boundNativeContext == tuple->Context) {
t_boundNativeContext = EGL_NO_CONTEXT;
t_boundNativeDraw = EGL_NO_SURFACE;
t_boundNativeSerial = 0;
}
ForgetNativeContextObjects(tuple->Serial);
tuple->Serial = 0;
tuple->Context = EGL_NO_CONTEXT;
tuple->Config = nullptr;
tuple->Draw = EGL_NO_SURFACE;
@@ -17241,7 +17299,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
// the allocate path.
std::atomic<Uint64> g_currentFrameSerial{0};
std::atomic<Uint64> g_completedFrameSerial{0};
constexpr int kFrameFenceRingDepth = 4;
// Deep enough for several sessions presenting in turn: a slot is only reused once its
// fence has signaled (InsertFrameFence waits for it rather than dropping it).
constexpr int kFrameFenceRingDepth = 32;
struct FrameFence {
GLsync sync = nullptr;
Uint contextGeneration = 0;
@@ -17331,6 +17391,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
InvalidateEglVerifiedStamp();
t_boundNativeContext = tuple.Context;
t_boundNativeDraw = session.Draw;
t_boundNativeSerial = tuple.Serial;
g_backendContextOwnerThread.store(std::this_thread::get_id(), std::memory_order_release);
// The ops table may have been unregistered when a previous ES context was
// destroyed (e.g. a probe context); re-register now that GL is usable.
@@ -17375,6 +17436,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
InvalidateEglVerifiedStamp();
t_boundNativeContext = EGL_NO_CONTEXT;
t_boundNativeDraw = EGL_NO_SURFACE;
t_boundNativeSerial = 0;
g_backendContextOwnerThread.store(std::thread::id{}, std::memory_order_release);
return true;
}
@@ -17800,31 +17862,63 @@ namespace MobileGL::MG_Backend::DirectGLES {
Uint64 CurrentFrameSerial() { return g_currentFrameSerial.load(std::memory_order_relaxed); }
Uint64 CompletedFrameSerial() { return g_completedFrameSerial.load(std::memory_order_relaxed); }
namespace {
// THE WATERMARK IS CONTIGUOUS. Fences signal in submission order only within ONE context,
// and the serials are handed out to every native context of every session in turn, so the
// highest signaled serial says nothing about an older fence of another context still
// pending. A serial is complete when no live fence at or below it is pending.
void PollFrameFences() {
if (!g_GLESFuncs.glGetSynciv) return;
Uint64 lowestPending = ~Uint64{0};
for (FrameFence& slot : g_frameFenceRing) {
if (!slot.sync) continue;
if (slot.contextGeneration != g_syncContextGeneration) {
slot.sync = nullptr; // its context reclaimed it, and its work is over
continue;
}
GLint status = GL_SIGNALED;
GLsizei length = 0;
g_GLESFuncs.glGetSynciv(slot.sync, GL_SYNC_STATUS, 1, &length, &status);
if (status == GL_SIGNALED) {
if (g_GLESFuncs.glDeleteSync) g_GLESFuncs.glDeleteSync(slot.sync);
slot.sync = nullptr;
} else if (slot.serial < lowestPending) {
lowestPending = slot.serial;
}
}
const Uint64 completed =
lowestPending == ~Uint64{0} ? g_currentFrameSerial.load(std::memory_order_relaxed) : lowestPending - 1;
if (completed > g_completedFrameSerial.load(std::memory_order_relaxed)) {
g_completedFrameSerial.store(completed, std::memory_order_relaxed);
}
}
FrameFence* OldestPendingFrameFence() {
FrameFence* oldest = nullptr;
for (FrameFence& slot : g_frameFenceRing) {
if (!slot.sync || slot.contextGeneration != g_syncContextGeneration) continue;
if (!oldest || slot.serial < oldest->serial) oldest = &slot;
}
return oldest;
}
} // namespace
Bool WaitForFrameSerialCompleted(Uint64 serial, Uint64 timeoutNs) {
if (CompletedFrameSerial() >= serial) return true;
if (!IsBackendContextCurrentOnThisThread() || !g_GLESFuncs.glClientWaitSync) return false;
// Fences signal in submission order, so the live fence with the SMALLEST
// serial at or past the target is the earliest event that proves the
// target frame retired. A recycled slot (GPU more than ring-depth frames
// behind) leaves no usable fence; report failure and let the caller pick
// its own fallback rather than draining the whole queue here.
FrameFence* best = nullptr;
for (FrameFence& slot : g_frameFenceRing) {
if (!slot.sync || slot.contextGeneration != g_syncContextGeneration) continue;
if (slot.serial < serial) continue;
if (!best || slot.serial < best->serial) best = &slot;
// Oldest pending fence first: the watermark can only pass a serial once every older fence
// (of any context) has signaled. Each fence was flushed when it was made (Present's swap),
// so waiting from this context needs no flush of the other.
for (int guard = 0; guard < kFrameFenceRingDepth + 1; ++guard) {
PollFrameFences();
if (CompletedFrameSerial() >= serial) return true;
FrameFence* oldest = OldestPendingFrameFence();
if (!oldest) return false;
const GLenum status = g_GLESFuncs.glClientWaitSync(oldest->sync, GL_SYNC_FLUSH_COMMANDS_BIT, timeoutNs);
if (status != GL_ALREADY_SIGNALED && status != GL_CONDITION_SATISFIED) return false;
}
if (!best) return false;
const GLenum status =
g_GLESFuncs.glClientWaitSync(best->sync, GL_SYNC_FLUSH_COMMANDS_BIT, timeoutNs);
if (status != GL_ALREADY_SIGNALED && status != GL_CONDITION_SATISFIED) return false;
Uint64 completed = g_completedFrameSerial.load(std::memory_order_relaxed);
if (best->serial > completed) {
g_completedFrameSerial.store(best->serial, std::memory_order_relaxed);
}
if (g_GLESFuncs.glDeleteSync) g_GLESFuncs.glDeleteSync(best->sync);
best->sync = nullptr;
return true;
PollFrameFences();
return CompletedFrameSerial() >= serial;
}
void Present() {
@@ -17835,8 +17929,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (canFence) {
const Uint64 serial = g_currentFrameSerial.fetch_add(1, std::memory_order_relaxed) + 1;
FrameFence& slot = g_frameFenceRing[serial % kFrameFenceRingDepth];
if (slot.sync && slot.contextGeneration == g_syncContextGeneration && g_GLESFuncs.glDeleteSync) {
g_GLESFuncs.glDeleteSync(slot.sync);
if (slot.sync && slot.contextGeneration == g_syncContextGeneration) {
// The slot's fence is a ring-depth of serials old and may be another context's:
// dropping it unsignaled would let the watermark pass work nobody waited for.
if (g_GLESFuncs.glClientWaitSync) {
constexpr Uint64 kOldFenceWaitNs = 100ull * 1000 * 1000;
(void)g_GLESFuncs.glClientWaitSync(slot.sync, GL_SYNC_FLUSH_COMMANDS_BIT, kOldFenceWaitNs);
}
if (g_GLESFuncs.glDeleteSync) g_GLESFuncs.glDeleteSync(slot.sync);
}
slot = {g_GLESFuncs.glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0), g_syncContextGeneration, serial};
}
@@ -17857,23 +17957,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
g_EGLFuncs.eglSwapBuffers(g_Display, drawSurface);
}
if (canFence && g_GLESFuncs.glGetSynciv) {
// Fences signal in submission order within one context, so the highest
// signaled serial is a valid contiguous completion watermark.
Uint64 completed = g_completedFrameSerial.load(std::memory_order_relaxed);
for (FrameFence& slot : g_frameFenceRing) {
if (!slot.sync || slot.contextGeneration != g_syncContextGeneration) continue;
GLint status = GL_SIGNALED;
GLsizei length = 0;
g_GLESFuncs.glGetSynciv(slot.sync, GL_SYNC_STATUS, 1, &length, &status);
if (status == GL_SIGNALED) {
if (slot.serial > completed) completed = slot.serial;
if (g_GLESFuncs.glDeleteSync) g_GLESFuncs.glDeleteSync(slot.sync);
slot.sync = nullptr;
}
}
g_completedFrameSerial.store(completed, std::memory_order_relaxed);
}
if (canFence) PollFrameFences();
// After the watermark advanced: retire grown-away ring stores and record the
// frame's ring high-water marks for slot reclamation.
@@ -17969,8 +18053,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
t_boundNativeContext = EGL_NO_CONTEXT;
t_boundNativeDraw = EGL_NO_SURFACE;
t_boundNativeSerial = 0;
for (auto* tuple : NativeContexts()) {
if (tuple->SessionKey != sessionKey || tuple->Context == EGL_NO_CONTEXT) continue;
ForgetNativeContextObjects(tuple->Serial);
tuple->Serial = 0;
if (g_EGLFuncs.eglDestroyContext) g_EGLFuncs.eglDestroyContext(g_Display, tuple->Context);
tuple->Context = EGL_NO_CONTEXT;
tuple->Config = nullptr;
@@ -293,6 +293,98 @@ namespace MobileGL::MG_Backend::DirectGLES {
// has gone.
void DestroyNativeContextFor(Uint64 contextToken);
// ---- objects that belong to ONE native context ----
//
// Framebuffers, vertex arrays, transform feedbacks and queries are container objects: GL never
// shares them, not even between contexts of one share group, and every context numbers its own
// from 1. A process-wide scratch FBO id minted in one context and bound in another therefore
// names THAT context's own framebuffer - often an application's - and the scratch path's
// attach/scrub then rewires it onto staging textures. With one process serving several
// sessions (each with its own native contexts) that silently corrupted other clients' frames.
//
// The calling thread's current native context as a lifetime serial: assigned when the context
// is created and never reused (a recycled EGLContext address is a new serial). 0 when none was
// made current through MakeCurrent.
Uint64 CurrentNativeContextSerial();
class PerNativeContextBase {
public:
PerNativeContextBase();
PerNativeContextBase(const PerNativeContextBase&) = delete;
PerNativeContextBase& operator=(const PerNativeContextBase&) = delete;
// The context died, and its names with it: forget them (no GL call - there is no context
// left to make one in).
virtual void ForgetContext(Uint64 serial) = 0;
protected:
~PerNativeContextBase();
};
// Called where a native context is destroyed.
void ForgetNativeContextObjects(Uint64 serial);
// Moves whenever any per-context entry is dropped; a thread's cached entry is valid while
// it has not moved.
Uint64 PerNativeContextEpoch();
void BumpPerNativeContextEpoch();
// One T per native context, created on first use from that context.
template <typename T>
class PerNativeContext final : public PerNativeContextBase {
public:
T& Current() {
const Uint64 serial = CurrentNativeContextSerial();
// The draw path asks for the same entry over and over from one thread: a one-entry
// thread-local cache, invalidated by any drop anywhere (rare).
thread_local CurrentCache cache;
const Uint64 epoch = PerNativeContextEpoch();
if (cache.Owner == this && cache.Serial == serial && cache.Epoch == epoch) return *cache.Entry;
const std::lock_guard<std::mutex> lock(m_mutex);
T& entry = m_bySerial[serial]; // node-based: the reference survives other contexts' inserts
cache = {this, serial, epoch, &entry};
return entry;
}
template <typename F>
void ForEach(F&& visit) {
const std::lock_guard<std::mutex> lock(m_mutex);
for (auto& entry : m_bySerial) visit(entry.second);
}
void Clear() {
const std::lock_guard<std::mutex> lock(m_mutex);
BumpPerNativeContextEpoch();
m_bySerial.clear();
}
void ForgetContext(Uint64 serial) override {
const std::lock_guard<std::mutex> lock(m_mutex);
BumpPerNativeContextEpoch();
m_bySerial.erase(serial);
}
private:
struct CurrentCache {
const PerNativeContext* Owner = nullptr;
Uint64 Serial = 0;
Uint64 Epoch = 0;
T* Entry = nullptr;
};
std::mutex m_mutex;
std::unordered_map<Uint64, T> m_bySerial;
};
// The GL name of one per-context object, spelled like the plain `GLuint` it replaces: it reads
// as the CURRENT context's name, `= 0` resets that one, and `&name` is where glGen* writes it.
class NativeContextName {
public:
operator GLuint() const { return m_names.Current(); }
NativeContextName& operator=(GLuint name) {
m_names.Current() = name;
return *this;
}
GLuint* operator&() { return &m_names.Current(); }
private:
mutable PerNativeContext<GLuint> m_names;
};
// P14 S4. THE NATIVE HALF OF A CONTEXT SWITCH. `bind_context` moves which context the session's
// records belong to; this makes the calling thread's native tuple follow it, binding the
// session's current surface to the newly named context's native EGLContext. A no-op when the
+135 -60
View File
@@ -1083,14 +1083,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
const char* label = "";
};
PersistentRing g_uboRing{{}, {}, {}, kUboRingInitialBytes, kUboRingMaxBytes, 0, "Global-UBO ring"};
PersistentRing g_unpackRing{{},
{},
{},
kUnpackRingInitialBytes,
kUnpackRingMaxBytes,
kUnpackRingAlignment,
"Texture unpack ring"};
// THE RINGS ARE PER NATIVE CONTEXT. Their slots are reclaimed by frame marks recorded at
// Present, which say "everything this context wrote before its fence N is done once N is".
// One process-wide set used to be shared by every session's contexts: a fence from one
// context retired bytes another context's not-yet-executed copies still had to read
// (a glTexSubImage from the unpack ring is a deferred GPU read), and the next session's
// staging overwrote them - its pixels landed, at the wrong offsets, in the first one's
// textures. With one set per context, a context's bytes wait for that context's own
// fence. (The buffers themselves are shared objects; only who stages into them is split.)
// Staging ring for app buffer updates whose destination store may still be
// referenced by in-flight GPU work. Mali's glBufferSubData resolves that WAR
// hazard by BLOCKING in the call (osup_sync_object_wait) until every
@@ -1099,13 +1099,67 @@ namespace MobileGL::MG_Backend::DirectGLES {
// chunks stream in). Staging the bytes here and issuing a
// glCopyBufferSubData instead keeps the hazard on the GPU timeline where it
// is just job ordering, and the CPU never waits.
PersistentRing g_uploadRing{{},
{},
{},
kUploadRingInitialBytes,
kUploadRingMaxBytes,
kUploadRingAlignment,
"Buffer upload ring"};
struct RingSet {
PersistentRing ubo{{}, {}, {}, kUboRingInitialBytes, kUboRingMaxBytes, 0, "Global-UBO ring"};
PersistentRing unpack{{},
{},
{},
kUnpackRingInitialBytes,
kUnpackRingMaxBytes,
kUnpackRingAlignment,
"Texture unpack ring"};
PersistentRing upload{{},
{},
{},
kUploadRingInitialBytes,
kUploadRingMaxBytes,
kUploadRingAlignment,
"Buffer upload ring"};
RingSet();
~RingSet();
RingSet(const RingSet&) = delete;
RingSet& operator=(const RingSet&) = delete;
};
// A dead context's ring buffers: shared objects, so they outlive it; deleted at the next
// Present of any context of the same share-group generation.
struct OrphanRingBuffer {
Uint id = 0;
Uint contextGeneration = 0;
};
std::mutex g_orphanRingBuffersMutex;
Vector<OrphanRingBuffer>& OrphanRingBuffers() {
static auto* orphans = new Vector<OrphanRingBuffer>();
return *orphans;
}
// Each set's generations start in a range of their own: a per-program slot cache keyed
// by (generation, offset) must never take one context's ring for another's.
std::atomic<Uint32> g_ringGenerationBases{0};
RingSet::RingSet() {
const Uint32 base = (g_ringGenerationBases.fetch_add(1, std::memory_order_relaxed) + 1) << 12;
ubo.store.generation = base;
unpack.store.generation = base;
upload.store.generation = base;
}
RingSet::~RingSet() {
const std::lock_guard<std::mutex> lock(g_orphanRingBuffersMutex);
for (const PersistentRing* ring : {&ubo, &unpack, &upload}) {
if (ring->store.id != 0) {
OrphanRingBuffers().push_back({ring->store.id, ring->store.contextGeneration});
}
for (const auto& retired : ring->retired) {
if (retired.id != 0) OrphanRingBuffers().push_back({retired.id, retired.contextGeneration});
}
}
}
DirectGLES::PerNativeContext<RingSet> g_ringSets;
PersistentRing& UboRing() { return g_ringSets.Current().ubo; }
PersistentRing& UnpackRing() { return g_ringSets.Current().unpack; }
PersistentRing& UploadRing() { return g_ringSets.Current().upload; }
// The ES context the ring's id/map belonged to is gone (or was never
// seen): drop every handle without GL calls and re-arm creation. The
@@ -1383,7 +1437,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
Bool UploadRingUsableNow() {
if (MG_Config::Features.EsprytDisableUploadRing) return false;
if (!g_GLESFuncs.glCopyBufferSubData) return false;
return RingAvailable(g_uploadRing);
return RingAvailable(UploadRing());
}
// A partial range below this goes through the staging ring instead of a
@@ -1532,9 +1586,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
SizeT ringOffset = 0;
if (ringUsable && size <= kUploadRingMaxBytes &&
RingAllocate(g_uploadRing, size, ringOffset)) {
Memcpy(g_uploadRing.store.mappedPtr + ringOffset, hostBase + start, size);
BindBufferId(GL_COPY_READ_BUFFER, g_uploadRing.store.id);
RingAllocate(UploadRing(), size, ringOffset)) {
Memcpy(UploadRing().store.mappedPtr + ringOffset, hostBase + start, size);
BindBufferId(GL_COPY_READ_BUFFER, UploadRing().store.id);
BindBufferId(GL_COPY_WRITE_BUFFER, resource.id);
g_GLESFuncs.glCopyBufferSubData(GL_COPY_READ_BUFFER, GL_COPY_WRITE_BUFFER,
(GLintptr)ringOffset, (GLintptr)start, (GLsizeiptr)size);
@@ -1611,9 +1665,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
SizeT ringOffset = 0;
if (ringUsable && size <= kUploadRingMaxBytes &&
RingAllocate(g_uploadRing, size, ringOffset)) {
Memcpy(g_uploadRing.store.mappedPtr + ringOffset, write.bytes.data(), size);
BindBufferId(GL_COPY_READ_BUFFER, g_uploadRing.store.id);
RingAllocate(UploadRing(), size, ringOffset)) {
Memcpy(UploadRing().store.mappedPtr + ringOffset, write.bytes.data(), size);
BindBufferId(GL_COPY_READ_BUFFER, UploadRing().store.id);
BindBufferId(GL_COPY_WRITE_BUFFER, resource.id);
g_GLESFuncs.glCopyBufferSubData(GL_COPY_READ_BUFFER, GL_COPY_WRITE_BUFFER,
(GLintptr)ringOffset, (GLintptr)write.offset,
@@ -1714,7 +1768,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
Bool UploadRingUsableNow() {
if (MG_Config::Features.EsprytDisableUploadRing) return false;
if (!g_GLESFuncs.glCopyBufferSubData) return false;
return RingAvailable(g_uploadRing);
return RingAvailable(UploadRing());
}
// A partial range below this goes through the staging ring instead of a
@@ -1820,9 +1874,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
SizeT ringOffset = 0;
if (ringUsable && size <= kUploadRingMaxBytes &&
RingAllocate(g_uploadRing, size, ringOffset)) {
Memcpy(g_uploadRing.store.mappedPtr + ringOffset, bufferObject.MappedData() + start, size);
BindBufferId(GL_COPY_READ_BUFFER, g_uploadRing.store.id);
RingAllocate(UploadRing(), size, ringOffset)) {
Memcpy(UploadRing().store.mappedPtr + ringOffset, bufferObject.MappedData() + start, size);
BindBufferId(GL_COPY_READ_BUFFER, UploadRing().store.id);
BindBufferId(GL_COPY_WRITE_BUFFER, resource.id);
g_GLESFuncs.glCopyBufferSubData(GL_COPY_READ_BUFFER, GL_COPY_WRITE_BUFFER,
(GLintptr)ringOffset, (GLintptr)start, (GLsizeiptr)size);
@@ -1866,9 +1920,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
SizeT ringOffset = 0;
if (ringUsable && size <= kUploadRingMaxBytes &&
RingAllocate(g_uploadRing, size, ringOffset)) {
Memcpy(g_uploadRing.store.mappedPtr + ringOffset, write.bytes.data(), size);
BindBufferId(GL_COPY_READ_BUFFER, g_uploadRing.store.id);
RingAllocate(UploadRing(), size, ringOffset)) {
Memcpy(UploadRing().store.mappedPtr + ringOffset, write.bytes.data(), size);
BindBufferId(GL_COPY_READ_BUFFER, UploadRing().store.id);
BindBufferId(GL_COPY_WRITE_BUFFER, resource.id);
g_GLESFuncs.glCopyBufferSubData(GL_COPY_READ_BUFFER, GL_COPY_WRITE_BUFFER,
(GLintptr)ringOffset, (GLintptr)write.offset,
@@ -3895,8 +3949,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
InvalidatePixelBufferBindingCaches();
// The rings' ids and persistent maps died with the context; drop the
// handles (no GL) and let the next draw / texture upload recreate them.
ResetRingForNewContext(g_uboRing);
ResetRingForNewContext(g_unpackRing);
ResetRingForNewContext(UboRing());
ResetRingForNewContext(UnpackRing());
#if MOBILEGL_PIPE_PUSH
// R-11's server copies die with the context for the same reason the rings' ids do:
// the resource twins they are keyed by are about to be rebuilt against a new
@@ -4905,9 +4959,23 @@ namespace MobileGL::MG_Backend::DirectGLES {
return true;
}
void DeleteOrphanRingBuffers() {
const std::lock_guard<std::mutex> lock(g_orphanRingBuffersMutex);
auto& orphans = OrphanRingBuffers();
if (orphans.empty()) return;
for (const OrphanRingBuffer& orphan : orphans) {
// A buffer of a share group that is gone was reclaimed with it.
if (orphan.contextGeneration != g_bufferContextGeneration) continue;
ScrubBufferBindingShadowsForId(orphan.id);
g_GLESFuncs.glDeleteBuffers(1, &orphan.id);
}
orphans.clear();
}
// Present()-time upkeep shared by both rings.
void RingOnPresent(PersistentRing& ring) {
if (!CanTouchGLNow()) return;
DeleteOrphanRingBuffers();
// Delete grown-away stores the GPU is provably done with.
const Uint64 completed = DirectGLES::CompletedFrameSerial();
@@ -4952,36 +5020,36 @@ namespace MobileGL::MG_Backend::DirectGLES {
Bool UboRingAvailable() {
if (MG_Config::Features.EsprytDisableUboRing) return false;
return RingAvailable(g_uboRing);
return RingAvailable(UboRing());
}
Bool UboRingAllocate(SizeT size, SizeT& outOffset) { return RingAllocate(g_uboRing, size, outOffset); }
Bool UboRingAllocate(SizeT size, SizeT& outOffset) { return RingAllocate(UboRing(), size, outOffset); }
void* UboRingMappedPtr() { return g_uboRing.store.mappedPtr; }
Uint UboRingBufferId() { return g_uboRing.store.id; }
Uint32 UboRingGeneration() { return g_uboRing.store.generation; }
void* UboRingMappedPtr() { return UboRing().store.mappedPtr; }
Uint UboRingBufferId() { return UboRing().store.id; }
Uint32 UboRingGeneration() { return UboRing().store.generation; }
void UboRingOnPresent() { RingOnPresent(g_uboRing); }
void UboRingOnPresent() { RingOnPresent(UboRing()); }
Bool UnpackRingAvailable() {
if (MG_Config::Features.EsprytDisableUnpackRing) return false;
return RingAvailable(g_unpackRing);
return RingAvailable(UnpackRing());
}
Bool UnpackRingAllocate(SizeT size, SizeT& outOffset) {
// A request the ring could never satisfy even empty would otherwise walk
// the whole grow/drain ladder before failing.
if (size > kUnpackRingMaxBytes) return false;
return RingAllocate(g_unpackRing, size, outOffset);
return RingAllocate(UnpackRing(), size, outOffset);
}
void* UnpackRingMappedPtr() { return g_unpackRing.store.mappedPtr; }
Uint UnpackRingBufferId() { return g_unpackRing.store.id; }
void* UnpackRingMappedPtr() { return UnpackRing().store.mappedPtr; }
Uint UnpackRingBufferId() { return UnpackRing().store.id; }
SizeT UnpackRingMaxBytes() { return kUnpackRingMaxBytes; }
void UnpackRingOnPresent() { RingOnPresent(g_unpackRing); }
void UnpackRingOnPresent() { RingOnPresent(UnpackRing()); }
void UploadRingOnPresent() { RingOnPresent(g_uploadRing); }
void UploadRingOnPresent() { RingOnPresent(UploadRing()); }
} // namespace BufferImpl
#if MOBILEGL_PIPE_PUSH
@@ -12891,11 +12959,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
namespace ScratchFBOImpl {
namespace {
ScratchFramebuffer g_tempFramebuffer;
ScratchFramebuffer g_blitReadFramebuffer;
ScratchFramebuffer g_blitDrawFramebuffer;
Uint g_completeTinyFBOId = 0;
Uint g_completeTinyRBOId = 0;
// ONE SET PER NATIVE CONTEXT: a framebuffer belongs to the context that made it
// (DirectGLES.h, PerNativeContext), and so does the attachment shadow kept for it.
struct ScratchSet {
ScratchFramebuffer temp;
ScratchFramebuffer blitRead;
ScratchFramebuffer blitDraw;
Uint completeTinyFBOId = 0;
Uint completeTinyRBOId = 0;
};
PerNativeContext<ScratchSet> g_scratchSets;
// Detach every point the shadow no longer vouches for. Used when the
// shadow is unknown (context reset, texture id deleted while attached).
@@ -12950,13 +13023,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
} // namespace
ScratchFramebuffer& TempFramebuffer() {
return g_tempFramebuffer;
return g_scratchSets.Current().temp;
}
ScratchFramebuffer& BlitReadFramebuffer() {
return g_blitReadFramebuffer;
return g_scratchSets.Current().blitRead;
}
ScratchFramebuffer& BlitDrawFramebuffer() {
return g_blitDrawFramebuffer;
return g_scratchSets.Current().blitDraw;
}
Uint EnsureId(ScratchFramebuffer& fb) {
@@ -13125,6 +13198,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
Uint EnsureCompleteTinyFramebufferId() {
ScratchSet& set = g_scratchSets.Current();
Uint& g_completeTinyFBOId = set.completeTinyFBOId;
Uint& g_completeTinyRBOId = set.completeTinyRBOId;
if (g_completeTinyFBOId != 0) {
return g_completeTinyFBOId;
}
@@ -13152,19 +13228,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (textureId == 0) {
return;
}
for (ScratchFramebuffer* fb : {&g_tempFramebuffer, &g_blitReadFramebuffer, &g_blitDrawFramebuffer}) {
if (fb->colorTex == textureId || fb->depthTex == textureId) {
fb->attachmentsKnown = false;
// Textures ARE shared, so the deleted id may sit in any context's scratch FBO.
g_scratchSets.ForEach([textureId](ScratchSet& set) {
for (ScratchFramebuffer* fb : {&set.temp, &set.blitRead, &set.blitDraw}) {
if (fb->colorTex == textureId || fb->depthTex == textureId) {
fb->attachmentsKnown = false;
}
}
}
});
}
void OnBackendContextDestroyed() {
g_tempFramebuffer = {};
g_blitReadFramebuffer = {};
g_blitDrawFramebuffer = {};
g_completeTinyFBOId = 0;
g_completeTinyRBOId = 0;
g_scratchSets.Clear();
}
} // namespace ScratchFBOImpl
+23 -1
View File
@@ -224,6 +224,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
return found->second;
}
SharedPtr<Table> fresh = MakeShared<Table>();
fresh->SetOwnerSession(key.SessionKey);
m_buckets.emplace(key, fresh);
m_memoKey = key;
m_memoTable = fresh;
@@ -458,6 +459,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// ENTRIES and the memo; the links are the table's own and are never copied.
BackendSlotTable() { LinkHolder(); }
BackendSlotTable(const BackendSlotTable& other):
m_ownerSession(other.m_ownerSession),
m_slots(other.m_slots),
m_band(other.m_band),
m_nullTwin(other.m_nullTwin),
@@ -466,6 +468,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
LinkHolder();
}
BackendSlotTable(BackendSlotTable&& other) noexcept:
m_ownerSession(other.m_ownerSession),
m_slots(std::move(other.m_slots)),
m_band(std::move(other.m_band)),
m_nullTwin(std::move(other.m_nullTwin)),
@@ -501,6 +504,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
~BackendSlotTable() { UnlinkHolder(); }
// The served session whose bucket this table is (SlotTableRegistry::Shared), 0 for a
// table that belongs to none (the process-wide registry on a sessionless thread, a
// fixture's copy of one).
void SetOwnerSession(Uint64 session) { m_ownerSession = session; }
Uint64 OwnerSession() const { return m_ownerSession; }
// Resolve-or-create. The handle comes from the client allocator keyed on the frontend
// object's lifetime id, so two calls for the same live object always land on the same
// slot, and a successor object at the same heap address never does.
@@ -802,12 +811,24 @@ namespace MobileGL::MG_Backend::DirectGLES {
// double Free would be refused by generation anyway. Idempotent against the kind's
// own delete opcode, exactly as the notice arm is: a twin the delete already released
// fails ReleaseTwinAt's generation check and the walk moves on.
//
// ONE SESSION'S HOLDERS, WHEN THE DEATH IS ONE SESSION'S. A handle is numbered by its own
// client's allocator, so with several clients served by one process the same {slot, gen}
// names a live, unrelated object in every other session: walking every holder released
// THOSE twins too - a client deleting a texture (or exiting) deleted the compositor's
// driver texture with the same handle, and its windows sampled freed memory. On a thread
// that belongs to a session only that session's buckets answer; a sessionless thread
// (the in-library backend, a unit fixture) keeps the process-wide walk.
static Bool ReleaseTwinByHandle(MG_Pipe::MGPipeHandle handle) {
if (MG_Pipe::MGPipeHandleIsNull(handle)) return false;
TwinKey key;
const Bool sessionScoped = CurrentTwinKey(&key) && key.SessionKey != 0;
Bool released = false;
for (BackendSlotTable* holder = s_firstHolder; holder != nullptr;) {
BackendSlotTable* const next = holder->m_nextHolder;
released = holder->ReleaseTwinAt(handle) || released;
if (!sessionScoped || holder->m_ownerSession == key.SessionKey) {
released = holder->ReleaseTwinAt(handle) || released;
}
holder = next;
}
return released;
@@ -1012,6 +1033,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
static inline BackendSlotTable* s_firstHolder = nullptr;
BackendSlotTable* m_prevHolder = nullptr;
BackendSlotTable* m_nextHolder = nullptr;
Uint64 m_ownerSession = 0;
// Indexed by MGPipeHandle::Slot; [0] is the reserved slot and is never live.
Vector<Entry> m_slots;
@@ -288,4 +288,47 @@ namespace {
EXPECT_EQ(table.BucketCount(), 0u);
}
// AN OBJECT'S DEATH IS ITS OWN SESSION'S. Every client numbers its objects with its own
// allocator, so the same {slot, gen} is live in other sessions at the same time and names
// their objects. The object_death release used to walk every holder of the kind: a client
// deleting a texture released the compositor's twin with the same handle - its driver
// texture was deleted while it was still drawn from (the lock screen exiting turned the
// desktop's windows to noise).
TEST_F(TwinOwnershipTest, AnObjectDeathReleasesOnlyItsOwnSessionsTwins) {
RegistryShapedTable table;
const P::MGPipeHandle handle{23u, 1u};
const auto mint = [&](Uint64 session, Uint64 group, Uint64 stamp) {
BindKey(session, group);
MobileGL::SharedPtr<Twin>* slot = table.GetOrCreateByHandle(handle);
ASSERT_NE(slot, nullptr);
*slot = MobileGL::MakeShared<Twin>();
(*slot)->SyncedSerial = stamp;
};
mint(kSessionA, kGroupOne, 1u);
mint(kSessionA, kGroupTwo, 2u);
mint(kSessionB, kGroupOne, 3u);
// Session A's client deleted its object; the death is applied on A's thread.
BindKey(kSessionA, kGroupOne);
EXPECT_TRUE(RegistryShapedTable::ReleaseByHandle(handle));
EXPECT_EQ(table.FindByHandle(handle), nullptr) << "the dying object's twin survived its death";
// A handle is unique across one client's share groups, so the session's other group lets go too.
BindKey(kSessionA, kGroupTwo);
EXPECT_EQ(table.FindByHandle(handle), nullptr);
BindKey(kSessionB, kGroupOne);
const MobileGL::SharedPtr<Twin>* const survivor = table.FindByHandle(handle);
ASSERT_NE(survivor, nullptr) << "another session's live object was released by this session's death";
EXPECT_EQ((*survivor)->SyncedSerial, 3u);
// A thread that belongs to no session (the in-library backend) keeps the process-wide walk.
BindKey(0, 0);
EXPECT_TRUE(RegistryShapedTable::ReleaseByHandle(handle));
BindKey(kSessionB, kGroupOne);
EXPECT_EQ(table.FindByHandle(handle), nullptr);
table.DropBucketsOfSession(kSessionA);
table.DropBucketsOfSession(kSessionB);
}
} // namespace