[Feat] (Espryt, P11 B2): import a T0 store with glBufferStorageExternalEXT as the adopted twin - no staged copy, server writes queued GPU-ordered, CPU readers on the import's coherent map, fence-gated release, re-import after a context loss, and the sustained-lock POST in a throwaway context

This commit is contained in:
2026-09-29 17:50:50 -04:00
parent 590d74525d
commit 2a69f61c88
4 changed files with 526 additions and 7 deletions
+12 -6
View File
@@ -483,7 +483,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
const MG_Pipe::MGPipeHandle handle = MG_Pipe::MGPipeApplier().VerbIndirectBuffer;
if (!MG_Pipe::MGPipeHandleIsNull(handle)) {
auto* resource = BufferImpl::FindBufferResourceForHandle(handle);
const Uint8* const base = resource ? resource->hostBytes : nullptr;
const Uint8* const base = resource ? BufferImpl::SplitHostBytes(*resource) : nullptr;
const SizeT commandOffset = reinterpret_cast<SizeT>(indirect);
if (base == nullptr) {
MGLOG_E_ONCE("%s skipped: the verb's indirect buffer {%u, %u} has no staged "
@@ -1284,6 +1284,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
ProcessDeferredBufferReleases();
#if MOBILEGL_BUILD_DISAGGREGATED
ProcessDeferredT0Retires(); // P11 B2
#endif
// All buffers we need are:
// 1.VBO 2.IBO (if needed) 3.UBO 4.IndirectBuffer (if needed)
@@ -1531,6 +1534,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
ProcessDeferredBufferReleases();
#if MOBILEGL_BUILD_DISAGGREGATED
ProcessDeferredT0Retires(); // P11 B2
#endif
// THE COMPUTE PATH IS THE ONE THAT NEEDS THE FRONTEND-INDEXED UNIFORM PASS, and it
// is why the uniform class is emitted at all: compute does NOT go through the
// per-program block remap in BindCurrentProgramWithResources, so these are the
@@ -9215,7 +9221,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// The staged shadow is the source of truth for CPU reads on this side: a
// persistent map's blocks and a shader write's writeback were consumed by the
// applier before this verb ran.
const Uint8* hostBytes = elementResource ? elementResource->hostBytes : nullptr;
const Uint8* hostBytes = elementResource ? BufferImpl::SplitHostBytes(*elementResource) : nullptr;
if (hostBytes == nullptr) {
MGLOG_E_ONCE("Draw skipped: GL_PRIMITIVE_RESTART with restart index %u needs a CPU-readable copy of "
"the bound element array buffer and none is available.",
@@ -9532,8 +9538,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
auto* drawResource = BufferImpl::FindBufferResourceForHandle(applierState.VerbIndirectBuffer);
auto* paramResource =
BufferImpl::FindBufferResourceForHandle(applierState.VerbIndirectParameterBuffer);
const Uint8* const drawBytes = drawResource ? drawResource->hostBytes : nullptr;
const Uint8* const parameterBytes = paramResource ? paramResource->hostBytes : nullptr;
const Uint8* const drawBytes = drawResource ? BufferImpl::SplitHostBytes(*drawResource) : nullptr;
const Uint8* const parameterBytes = paramResource ? BufferImpl::SplitHostBytes(*paramResource) : nullptr;
if (commandBytes > BufferImpl::ResourceWidthForHandle(applierState.VerbIndirectBuffer)) {
MGLOG_E_ONCE("MultiDrawElementsIndirectCount skipped: invalid GL_DRAW_INDIRECT_BUFFER binding or range");
return;
@@ -9686,8 +9692,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
auto* drawResource = BufferImpl::FindBufferResourceForHandle(applierState.VerbIndirectBuffer);
auto* paramResource =
BufferImpl::FindBufferResourceForHandle(applierState.VerbIndirectParameterBuffer);
const Uint8* const drawBytes = drawResource ? drawResource->hostBytes : nullptr;
const Uint8* const parameterBytes = paramResource ? paramResource->hostBytes : nullptr;
const Uint8* const drawBytes = drawResource ? BufferImpl::SplitHostBytes(*drawResource) : nullptr;
const Uint8* const parameterBytes = paramResource ? BufferImpl::SplitHostBytes(*paramResource) : nullptr;
if (commandBytes > BufferImpl::ResourceWidthForHandle(applierState.VerbIndirectBuffer)) {
MGLOG_E_ONCE("MultiDrawArraysIndirectCount skipped: invalid GL_DRAW_INDIRECT_BUFFER binding or range");
return;
+490
View File
@@ -30,6 +30,12 @@
// emits the record through the client's emit helper instead of hopping a stack struct to the
// apply thread.
#include <MG_Remote/Client/WireTables.h>
// P11 B2 (T0): the client's AHardwareBuffer store - its reference counting and, on Android, the
// allocation the POST self-test makes.
#include <MG_Remote/Transport/AdoptT0.h>
#if defined(__ANDROID__)
#include <android/hardware_buffer.h>
#endif
#endif
#include "Utils.h"
@@ -1231,6 +1237,77 @@ namespace MobileGL::MG_Backend::DirectGLES {
ServerStaged().RequireCoverage(&resource, hostBase, start, end, site);
}
}
// -------------------------------------------------------------------------------
// P11 B2 (T0) - WHEN AN IMPORTED STORE'S AHardwareBuffer MAY BE LET GO.
//
// A T0 store is the client's AHardwareBuffer imported with glBufferStorageExternalEXT
// (Ops_H_ImportExternal below). The twin holds its own reference, and every path that
// ends the store - a respecify, a destroy, a re-import - deletes the GL id and then
// RETIRES the reference here: a fence is inserted after the delete, and the reference is
// released only once that fence has signalled, i.e. once every command that could still
// read or write the pages has completed on this context's GPU. The client's own
// reference ends independently (it releases on respecify/destroy); the pages live until
// the last of the two. No link watermark is involved (F-L2): the fence is this server's.
//
// Swept (non-blocking) at every import, every deferred-release drain and every ensure;
// a context loss releases the lot, because the GPU work those fences named ended with
// the context. Apply thread only, like every GL call here.
// -------------------------------------------------------------------------------
struct T0RetiredImport {
GLsync fence = nullptr;
void* ahb = nullptr;
Uint generation = 0;
};
Vector<T0RetiredImport> g_t0Retired;
// T0 twins whose destroy found no current context: Ops_OnDestroy queued them on the
// deferred-release list and keeps a second reference here, so the reference is
// retired only after that drain deleted the id (ProcessDeferredT0Retires). Guarded by
// g_deferredBufferReleasesMutex, like the list it shadows.
Vector<SharedPtr<BackendBufferResource>> g_t0DeferredDestroys;
std::atomic<Bool> g_hasT0DeferredDestroys{false};
Uint64 g_t0ImportsLive = 0;
void SweepT0Retired(Bool contextGone) {
for (SizeT i = 0; i < g_t0Retired.size();) {
T0RetiredImport& retired = g_t0Retired[i];
const Bool sameContext = !contextGone && retired.generation == g_bufferContextGeneration &&
CanTouchGLNow();
Bool done = contextGone || retired.generation != g_bufferContextGeneration ||
retired.fence == nullptr;
if (!done && sameContext && g_GLESFuncs.glClientWaitSync) {
const GLenum status = g_GLESFuncs.glClientWaitSync(retired.fence, 0, 0);
done = status == GL_ALREADY_SIGNALED || status == GL_CONDITION_SATISFIED ||
status == GL_WAIT_FAILED;
}
if (!done) {
++i;
continue;
}
if (retired.fence != nullptr && sameContext && g_GLESFuncs.glDeleteSync)
g_GLESFuncs.glDeleteSync(retired.fence);
MG_Remote::Transport::AdoptT0::ReleaseImported(retired.ahb);
retired = g_t0Retired.back();
g_t0Retired.pop_back();
}
}
// Call AFTER the store's GL id is deleted (or known dead): the fence then follows every
// command that used it. Clears the twin's reference whatever the context state.
void RetireT0Import(GLESBufferResource& resource) {
if (resource.externalAhb == nullptr) return;
T0RetiredImport retired;
retired.ahb = resource.externalAhb;
retired.generation = resource.contextGeneration;
if (CanTouchGLNow() && resource.contextGeneration == g_bufferContextGeneration &&
g_GLESFuncs.glFenceSync) {
retired.fence = g_GLESFuncs.glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
}
resource.externalAhb = nullptr;
if (g_t0ImportsLive > 0) --g_t0ImportsLive;
g_t0Retired.push_back(retired);
SweepT0Retired(false);
}
#endif // MOBILEGL_BUILD_DISAGGREGATED
// The four hostBytes sites read the same in both builds. The non-split expansion is the
@@ -2075,6 +2152,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
auto* glesResource = static_cast<GLESBufferResource*>(resource.get());
if (glesResource->contextGeneration != g_bufferContextGeneration) {
glesResource->id = 0; // id belonged to a destroyed context
#if MOBILEGL_BUILD_DISAGGREGATED
RetireT0Import(*glesResource); // P11 B2: its GL store died with that context
#endif
return;
}
if (CanTouchGLNow()) {
@@ -2090,9 +2170,19 @@ namespace MobileGL::MG_Backend::DirectGLES {
g_GLESFuncs.glDeleteBuffers(1, &glesResource->id);
glesResource->id = 0;
}
#if MOBILEGL_BUILD_DISAGGREGATED
// P11 B2: a T0 store's AHardwareBuffer goes only after a fence behind the delete.
RetireT0Import(*glesResource);
#endif
return;
}
const std::lock_guard<std::mutex> lock(g_deferredBufferReleasesMutex);
#if MOBILEGL_BUILD_DISAGGREGATED
if (glesResource->externalAhb != nullptr) { // P11 B2: retired after the drain deletes it
g_t0DeferredDestroys.push_back(resource);
g_hasT0DeferredDestroys.store(true, std::memory_order_release);
}
#endif
g_deferredBufferReleases.push_back(std::move(resource));
g_hasDeferredBufferReleases.store(true, std::memory_order_release);
}
@@ -2232,6 +2322,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
resource->id = 0;
resource->immutableStorage = false;
}
#if MOBILEGL_BUILD_DISAGGREGATED
// P11 B2: a respecified T0 store ends here on the server's side too. Its
// AHardwareBuffer is released only after a fence behind the delete (or at once
// when the store's context is already gone) - never under a frame in flight,
// however the client's own release (at this respecify) raced it.
RetireT0Import(*resource);
#endif
resource->storageInitialized = false;
resource->storageSize = 0;
resource->pendingRespecify = true;
@@ -2282,6 +2379,26 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
#endif
if (!resource) return;
#if MOBILEGL_BUILD_DISAGGREGATED
// P11 B2: A T0 STORE TAKES A SERVER-ORIGINATED WRITE GPU-ORDERED. Its pages are the
// client's AHardwareBuffer, so there is no staged copy to refresh and the adopted
// early return below ("the coherent map already has the bytes") is false for bytes
// the SERVER produced - P9 W1's pack-buffer landing (PipeApplier.cpp
// LandReadbackInBuffer) is the one producer, and it would lose its pixels there. They
// are queued exactly as a resident SubData is (Ops_H_ResidentSubData): landed after
// every command already recorded, before the next one that reads the store.
if (resource->externalAhb != nullptr && bytes != nullptr && size != 0) {
{
const std::lock_guard<std::mutex> lock(resource->pendingMutex);
auto& write = resource->pendingResidentWrites.emplace_back();
write.offset = offset;
const auto* source = static_cast<const Uint8*>(bytes);
write.bytes.assign(source, source + size);
}
resource->syncedChangeSerial = ResourceSerialOf(res);
return;
}
#endif
// M-2: UNDER pendingMutex, because this line runs BEFORE the CanTouchGLNow()
// test below - i.e. on the arm D-A2 deliberately keeps reachable off the render
// thread - while every reader of hostBytes (Ops_H_Readback's drain, the
@@ -2656,6 +2773,319 @@ namespace MobileGL::MG_Backend::DirectGLES {
return result;
}
#if MOBILEGL_BUILD_DISAGGREGATED && defined(__ANDROID__)
// ---- P11 B2: T0 - THE CLIENT'S AHardwareBuffer AS A STORE'S RESIDENT TWIN ------------
//
// The split twin of Ops_H_MapPersistent: where a monolith map_persistent mints a
// coherent persistent store (glBufferStorageEXT) and hands its pointer to the frontend,
// a T0 map_persistent IMPORTS the pages the client already holds and maps -
// glBufferStorageExternalEXT on eglGetNativeClientBufferANDROID(ahb) - and the result
// is the SAME resource shape (immutable, persistentMapped, persistentPtr = this
// server's coherent map of those pages), so every adopted-store path in this file
// applies unchanged: draws bind it with nothing to upload, resident SubData lands
// GPU-ordered (DrainResidentWritesNow), a readback drains and waits and posts no
// bytes. The R-11 staged copy is dropped: the pages are the client's.
//
// THE ENTRY POINTS ARE OPTIONAL: neither is in the loader's tables (an EGL function
// missing there is MGLOG_F), and eglGetProcAddress hands back live-looking stubs, so
// the extension strings decide and the pointers are only then taken.
using T0GetNativeClientBufferFn = EGLClientBuffer (*)(const struct AHardwareBuffer*);
using T0BufferStorageExternalFn = void (*)(GLenum, GLintptr, GLsizeiptr, EGLClientBuffer, GLbitfield);
T0GetNativeClientBufferFn g_t0GetNativeClientBuffer = nullptr;
T0BufferStorageExternalFn g_t0BufferStorageExternal = nullptr;
constexpr GLbitfield kT0MapReadBit = 0x0001;
constexpr GLbitfield kT0MapWriteBit = 0x0002;
constexpr GLbitfield kT0StorageFlags =
kT0MapReadBit | kT0MapWriteBit | kMapPersistentBit | kMapCoherentBit | kDynamicStorageBit;
constexpr GLbitfield kT0MapFlags = kT0MapReadBit | kT0MapWriteBit | kMapPersistentBit | kMapCoherentBit;
Bool HasExtensionWord(const char* list, const char* word) {
if (list == nullptr) return false;
const SizeT n = std::strlen(word);
for (const char* at = std::strstr(list, word); at != nullptr; at = std::strstr(at + 1, word)) {
const Bool startOk = at == list || at[-1] == ' ';
const Bool endOk = at[n] == '\0' || at[n] == ' ';
if (startOk && endOk) return true;
}
return false;
}
Bool ResolveT0EntryPoints(String& why) {
if (g_t0GetNativeClientBuffer != nullptr && g_t0BufferStorageExternal != nullptr) return true;
if (!g_EGLFuncs.eglGetProcAddress || !g_EGLFuncs.eglQueryString || !g_EGLFuncs.eglGetCurrentDisplay ||
!g_GLESFuncs.glGetString || !g_GLESFuncs.glMapBufferRange || !g_GLESFuncs.glGenBuffers) {
why = "the loader has no eglGetProcAddress/eglQueryString/glGetString/glMapBufferRange";
return false;
}
const char* egl = g_EGLFuncs.eglQueryString(g_EGLFuncs.eglGetCurrentDisplay(), EGL_EXTENSIONS);
const char* gl = reinterpret_cast<const char*>(g_GLESFuncs.glGetString(GL_EXTENSIONS));
if (!HasExtensionWord(egl, "EGL_ANDROID_get_native_client_buffer")) {
why = "EGL_ANDROID_get_native_client_buffer is not advertised";
return false;
}
if (!HasExtensionWord(gl, "GL_EXT_external_buffer") || !HasExtensionWord(gl, "GL_EXT_buffer_storage")) {
why = "GL_EXT_external_buffer / GL_EXT_buffer_storage is not advertised";
return false;
}
g_t0GetNativeClientBuffer = reinterpret_cast<T0GetNativeClientBufferFn>(
g_EGLFuncs.eglGetProcAddress("eglGetNativeClientBufferANDROID"));
g_t0BufferStorageExternal = reinterpret_cast<T0BufferStorageExternalFn>(
g_EGLFuncs.eglGetProcAddress("glBufferStorageExternalEXT"));
if (g_t0GetNativeClientBuffer == nullptr || g_t0BufferStorageExternal == nullptr) {
why = "eglGetProcAddress gave no eglGetNativeClientBufferANDROID / glBufferStorageExternalEXT";
g_t0GetNativeClientBuffer = nullptr;
g_t0BufferStorageExternal = nullptr;
return false;
}
return true;
}
void DrainGLErrors() {
if (!g_GLESFuncs.glGetError) return;
for (Uint guard = 0; guard < 16 && g_GLESFuncs.glGetError() != GL_NO_ERROR; ++guard) {
}
}
// A fresh GL buffer whose storage IS `ahb`, mapped persistent + coherent. 0 on failure
// (with `why`); nothing on the resource is touched here. `throwaway` = the POST's own
// context is current: bind raw and keep Espryt's binding caches (which describe the
// session's context, whose name space this is not) out of it.
Uint ImportAhbAsBuffer(void* ahb, Uint64 size, void** outPtr, String& why, Bool throwaway = false) {
*outPtr = nullptr;
const EGLClientBuffer clientBuffer =
g_t0GetNativeClientBuffer(static_cast<const struct AHardwareBuffer*>(ahb));
if (clientBuffer == nullptr) {
why = "eglGetNativeClientBufferANDROID returned null";
return 0;
}
Uint id = 0;
g_GLESFuncs.glGenBuffers(1, &id);
if (id == 0) {
why = "glGenBuffers gave no name";
return 0;
}
DrainGLErrors();
if (throwaway) g_GLESFuncs.glBindBuffer(TempBufferTarget, id);
else BindBufferId(TempBufferTarget, id);
g_t0BufferStorageExternal(TempBufferTarget, 0, static_cast<GLsizeiptr>(size), clientBuffer,
kT0StorageFlags);
const GLenum stored = g_GLESFuncs.glGetError ? g_GLESFuncs.glGetError() : GL_NO_ERROR;
void* ptr = stored == GL_NO_ERROR
? g_GLESFuncs.glMapBufferRange(TempBufferTarget, 0, static_cast<GLsizeiptr>(size), kT0MapFlags)
: nullptr;
if (ptr == nullptr) {
char text[160];
std::snprintf(text, sizeof(text), "glBufferStorageExternalEXT -> 0x%04x, map %s", stored,
stored == GL_NO_ERROR ? "failed" : "not attempted");
why = text;
if (!throwaway) {
NoteBufferIdDeleted(id);
++g_bufferBackendIdGeneration;
}
g_GLESFuncs.glDeleteBuffers(1, &id);
return 0;
}
*outPtr = ptr;
return id;
}
Bool T0Answer(char* out, Uint64 outBytes, Bool ok, const char* fmt, ...) {
va_list args;
va_start(args, fmt);
std::vsnprintf(out, static_cast<SizeT>(outBytes), fmt, args);
va_end(args);
return ok;
}
// Installs an imported store on the twin. The caller has retired whatever was there.
void InstallT0Store(GLESBufferResource& resource, Uint id, void* ptr, Uint64 size) {
resource.id = id;
resource.immutableStorage = true;
resource.persistentPtr = ptr;
resource.persistentMapped = true;
resource.storageSize = static_cast<SizeT>(size);
resource.storageInitialized = true;
resource.pendingRespecify = false;
resource.contextGeneration = g_bufferContextGeneration;
const std::lock_guard<std::mutex> lock(resource.pendingMutex);
resource.pendingRanges.clear();
resource.pendingResidentWrites.clear();
}
Bool Ops_H_ImportExternal(MG_Pipe::MGPipeHandle res, void* ahb, Uint64 size) {
SweepT0Retired(false);
if (!CanTouchGLNow() || size == 0 || ahb == nullptr) return false;
String why;
if (!ResolveT0EntryPoints(why)) {
MGLOG_W_ONCE("Espryt T0: no import on this context - %s", why.c_str());
return false;
}
auto* resource = GetOrCreateBufferResourceForHandle(res);
if (resource == nullptr) return false;
// IMPORT FIRST, retire second: a refused import leaves the T2 store (and its staged
// copy) exactly as it was, so the DECLINED the client gets leaves nothing to repair.
void* ptr = nullptr;
const Uint id = ImportAhbAsBuffer(ahb, size, &ptr, why);
if (id == 0) {
MGLOG_W("Espryt T0: store {slot=%u, gen=%u} (%llu bytes) not imported - %s", res.Slot, res.Gen,
static_cast<unsigned long long>(size), why.c_str());
return false;
}
if (resource->contextGeneration == g_bufferContextGeneration && resource->id != 0) {
NoteBufferIdDeleted(resource->id);
// Driver VAOs may have the old id baked into bindings keyed on versions this
// replacement does not move (the Ops_H_MapPersistent re-mint's rule).
++g_bufferBackendIdGeneration;
g_GLESFuncs.glDeleteBuffers(1, &resource->id);
}
resource->id = 0;
RetireT0Import(*resource); // a previous import of this handle, fenced
InstallT0Store(*resource, id, ptr, size);
MG_Remote::Transport::AdoptT0::AcquireImported(ahb);
resource->externalAhb = ahb;
++g_t0ImportsLive;
// The bytes are the client's pages now; R-11's copy would answer a later drain
// with pre-import content.
MGL_SERVER_STAGED_DROP(*resource);
resource->hostBytes = nullptr;
resource->syncedChangeSerial = ResourceSerialOf(res);
MGLOG_D("Espryt T0: imported {slot=%u, gen=%u} as buffer %u (%llu bytes); %llu live imports",
res.Slot, res.Gen, id, static_cast<unsigned long long>(size),
static_cast<unsigned long long>(g_t0ImportsLive));
return true;
}
Bool Ops_H_ImportExternalTracked(MG_Pipe::MGPipeHandle res, void* ahb, Uint64 size) {
const Bool imported = Ops_H_ImportExternal(res, ahb, size);
BumpBufferMutationEpoch();
return imported;
}
// THE POST (B2, the standing rule: decide by probe, never by driver name). The AHB API
// promises CPU coherence only across an unlock/lock; T0 keeps the client's lock for the
// store's life, so the pattern it depends on is exactly what this exercises, on this
// context's own driver, before kCapAdoptT0 may be published:
// hold - a 64 KiB BLOB allocated and locked by this test, never unlocked while it runs;
// read - this server's own host map of the import reads the pattern the CPU wrote
// through the held pointer (every hostBytes fallback reads persistentPtr);
// gpu - a compute dispatch copies that pattern out of the import (the GPU read) and
// fills another region of it (the GPU write);
// back - after glFinish the fill is read through the HELD pointer and the copy is
// read back from the dispatch's output.
// In a throwaway ES 3.1 pbuffer context, so none of this context's state or Espryt's
// binding caches is disturbed; the caller's bind is restored before returning.
Bool Ops_H_SelfTestExternal(char* whyOut, Uint64 whyBytes) {
const auto answer = [&](Bool ok, const char* text, const char* arg = "") {
return T0Answer(whyOut, whyBytes, ok, text, arg);
};
String why;
if (!ResolveT0EntryPoints(why)) return answer(false, "%s", why.c_str());
if (!g_EGLFuncs.eglChooseConfig || !g_EGLFuncs.eglCreateContext || !g_EGLFuncs.eglMakeCurrent ||
!g_EGLFuncs.eglCreatePbufferSurface || !g_GLESFuncs.glCreateShaderProgramv ||
!g_GLESFuncs.glDispatchCompute || !g_GLESFuncs.glMemoryBarrier || !g_GLESFuncs.glFinish)
return answer(false, "the loader lacks the EGL/GLES 3.1 entry points the test needs");
const EGLDisplay display = g_EGLFuncs.eglGetCurrentDisplay();
const EGLContext savedContext = g_EGLFuncs.eglGetCurrentContext();
const EGLSurface savedDraw = g_EGLFuncs.eglGetCurrentSurface(EGL_DRAW);
const EGLSurface savedRead = g_EGLFuncs.eglGetCurrentSurface(EGL_READ);
const EGLint configAttribs[] = {EGL_RENDERABLE_TYPE, 0x0040 /*EGL_OPENGL_ES3_BIT*/, EGL_SURFACE_TYPE,
EGL_PBUFFER_BIT, EGL_NONE};
EGLConfig config = nullptr;
EGLint configs = 0;
if (!g_EGLFuncs.eglChooseConfig(display, configAttribs, &config, 1, &configs) || configs < 1)
return answer(false, "no ES3 pbuffer config for the throwaway context");
const EGLint surfaceAttribs[] = {EGL_WIDTH, 1, EGL_HEIGHT, 1, EGL_NONE};
const EGLSurface surface = g_EGLFuncs.eglCreatePbufferSurface(display, config, surfaceAttribs);
const EGLint contextAttribs[] = {0x3098 /*EGL_CONTEXT_MAJOR_VERSION*/, 3, 0x30FB /*MINOR*/, 1, EGL_NONE};
const EGLContext context = surface != EGL_NO_SURFACE
? g_EGLFuncs.eglCreateContext(display, config, EGL_NO_CONTEXT, contextAttribs)
: EGL_NO_CONTEXT;
const auto restore = [&] {
g_EGLFuncs.eglMakeCurrent(display, savedDraw, savedRead, savedContext);
if (context != EGL_NO_CONTEXT) g_EGLFuncs.eglDestroyContext(display, context);
if (surface != EGL_NO_SURFACE) g_EGLFuncs.eglDestroySurface(display, surface);
};
if (context == EGL_NO_CONTEXT || !g_EGLFuncs.eglMakeCurrent(display, surface, surface, context)) {
restore();
return answer(false, "the throwaway ES 3.1 context could not be made current");
}
constexpr Uint64 kBytes = 64 * 1024;
constexpr Uint32 kWords = 1024; // one 4 KiB region
constexpr Uint32 kFillAtWord = 4 * kWords; // region 4
MG_Remote::Transport::AdoptT0::HeldStore held;
Vector<Uint32> pattern(kBytes / 4);
for (SizeT i = 0; i < pattern.size(); ++i) pattern[i] = 0x5A000000u ^ static_cast<Uint32>(i * 2654435761u);
if (!MG_Remote::Transport::AdoptT0::AllocateHeld(kBytes, pattern.data(), held, why)) {
restore();
return answer(false, "hold: %s", why.c_str());
}
Bool ok = false;
GLuint out = 0, program = 0;
void* hostMap = nullptr;
const GLuint imported = ImportAhbAsBuffer(held.ahb, kBytes, &hostMap, why, /*throwaway=*/true);
char detail[256] = {};
if (imported == 0) {
std::snprintf(detail, sizeof(detail), "import: %s", why.c_str());
} else if (std::memcmp(hostMap, pattern.data(), kWords * 4) != 0) {
std::snprintf(detail, sizeof(detail), "read: the server's host map of the import does not "
"hold the CPU's pattern");
} else {
static const char* kSource =
"#version 310 es\n"
"layout(local_size_x = 64) in;\n"
"layout(std430, binding = 0) buffer Ext { uint w[]; } ext;\n"
"layout(std430, binding = 1) buffer Out { uint w[]; } outb;\n"
"void main() {\n"
" uint i = gl_GlobalInvocationID.x;\n"
" outb.w[i] = ext.w[i];\n"
" ext.w[4096u + i] = 0x6C0FFEE0u ^ i;\n"
"}\n";
program = g_GLESFuncs.glCreateShaderProgramv(GL_COMPUTE_SHADER, 1, &kSource);
GLint linked = 0;
if (program != 0) g_GLESFuncs.glGetProgramiv(program, GL_LINK_STATUS, &linked);
g_GLESFuncs.glGenBuffers(1, &out);
g_GLESFuncs.glBindBuffer(GL_SHADER_STORAGE_BUFFER, out);
g_GLESFuncs.glBufferData(GL_SHADER_STORAGE_BUFFER, kWords * 4, nullptr, GL_DYNAMIC_READ);
if (!linked) {
std::snprintf(detail, sizeof(detail), "gpu: the test's compute program did not link");
} else {
g_GLESFuncs.glUseProgram(program);
g_GLESFuncs.glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, imported);
g_GLESFuncs.glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, out);
g_GLESFuncs.glDispatchCompute(kWords / 64, 1, 1);
g_GLESFuncs.glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT | GL_SHADER_STORAGE_BARRIER_BIT |
0x00004000 /*GL_CLIENT_MAPPED_BUFFER_BARRIER_BIT_EXT*/);
g_GLESFuncs.glFinish();
Uint32 gpuRead = 0, heldSaw = 0;
const auto* heldWords = static_cast<const Uint32*>(held.ptr);
for (Uint32 i = 0; i < kWords; ++i)
if (heldWords[kFillAtWord + i] == (0x6C0FFEE0u ^ i)) ++heldSaw;
g_GLESFuncs.glBindBuffer(GL_SHADER_STORAGE_BUFFER, out);
const auto* copied = static_cast<const Uint32*>(g_GLESFuncs.glMapBufferRange(
GL_SHADER_STORAGE_BUFFER, 0, kWords * 4, GL_MAP_READ_BIT));
if (copied != nullptr) {
for (Uint32 i = 0; i < kWords; ++i)
if (copied[i] == pattern[i]) ++gpuRead;
g_GLESFuncs.glUnmapBuffer(GL_SHADER_STORAGE_BUFFER);
}
ok = gpuRead == kWords && heldSaw == kWords;
std::snprintf(detail, sizeof(detail),
"64 KiB AHB, lock held throughout: host map read ok, GPU read %u/%u words of "
"the CPU's pattern, GPU write seen through the held lock %u/%u words",
gpuRead, kWords, heldSaw, kWords);
}
}
if (program != 0) g_GLESFuncs.glDeleteProgram(program);
if (out != 0) g_GLESFuncs.glDeleteBuffers(1, &out);
if (imported != 0) g_GLESFuncs.glDeleteBuffers(1, &imported);
g_GLESFuncs.glFinish();
restore();
MG_Remote::Transport::AdoptT0::ReleaseHeld(held);
return answer(ok, "%s", detail);
}
#endif // MOBILEGL_BUILD_DISAGGREGATED && __ANDROID__
#if MOBILEGL_BUILD_DISAGGREGATED
// ---- P5c (tx): the TEXTURE half of the resource family, staged server-side --------
//
@@ -2823,6 +3253,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
.TextureSubData = Ops_H_TextureSubData,
.TextureRespecify = Ops_H_TextureRespecify,
.TextureDestroy = Ops_H_TextureDestroy,
#if defined(__ANDROID__)
// P11 B2 (T0). Android only: nowhere else is there an AHardwareBuffer to import.
.ImportExternal = Ops_H_ImportExternalTracked,
.SelfTestExternal = Ops_H_SelfTestExternal,
#endif
#endif
};
#endif // MOBILEGL_PIPE_PUSH
@@ -3253,6 +3688,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
// but with the same recycled-identity failure mode: a new context's allocator may
// hand out a {slot, gen} the old one's store still answers for.
MG_Remote::Server::ServerStagedTexture().DropAll();
// P11 B2: every retired T0 import's fence named work on the context that just ended,
// so all of them let go now. LIVE imports keep their reference: the next ensure
// imports them again on the new context (EnsureBufferResourceForHandle).
ProcessDeferredT0Retires();
SweepT0Retired(/*contextGone=*/true);
#endif
#endif
}
@@ -3289,6 +3729,33 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
}
#if MOBILEGL_BUILD_DISAGGREGATED
// P11 B2: the T0 half of the deferred-release drain, run right after it at both sync
// points (ProcessDeferredBufferReleases itself moves verbatim, G5). Retired imports whose
// fence signalled let go of their AHardwareBuffer; a deferred T0 destroy is retired once
// the drain deleted its id (the fence then follows the delete) or its context is gone.
void ProcessDeferredT0Retires() {
if (!g_t0Retired.empty()) SweepT0Retired(false);
if (!g_hasT0DeferredDestroys.load(std::memory_order_acquire)) return;
Vector<SharedPtr<BackendBufferResource>> ready;
{
const std::lock_guard<std::mutex> lock(g_deferredBufferReleasesMutex);
for (SizeT i = 0; i < g_t0DeferredDestroys.size();) {
auto* r = static_cast<GLESBufferResource*>(g_t0DeferredDestroys[i].get());
if (r->id == 0 || r->contextGeneration != g_bufferContextGeneration) {
ready.push_back(std::move(g_t0DeferredDestroys[i]));
g_t0DeferredDestroys[i] = std::move(g_t0DeferredDestroys.back());
g_t0DeferredDestroys.pop_back();
} else {
++i;
}
}
g_hasT0DeferredDestroys.store(!g_t0DeferredDestroys.empty(), std::memory_order_release);
}
for (auto& resource : ready) RetireT0Import(*static_cast<GLESBufferResource*>(resource.get()));
}
#endif
GLESBufferResource* GetBufferResource(MG_State::GLState::BufferObject* bufferObject) {
if (!bufferObject) return nullptr;
return static_cast<GLESBufferResource*>(bufferObject->GetBackendResource().get());
@@ -3466,6 +3933,29 @@ namespace MobileGL::MG_Backend::DirectGLES {
!ServerStaged().HasShadow(resource)) {
resource->hostBytes = nullptr;
}
#if defined(__ANDROID__)
// P11 B2: A T0 STORE OUTLIVES ITS CONTEXT. Its pages are the client's
// AHardwareBuffer, which this twin still holds, so the store is imported again on
// the new context - the client keeps writing through the same pointer and nothing
// has to be re-sent. A refusal here leaves the twin without a store and says so.
if (resource->externalAhb != nullptr) {
String why;
void* ptr = nullptr;
const Uint64 size = ResourceWidthOf(res);
const Uint id = ResolveT0EntryPoints(why) ? ImportAhbAsBuffer(resource->externalAhb, size, &ptr, why)
: 0;
if (id != 0) {
InstallT0Store(*resource, id, ptr, size);
MGLOG_I("Espryt T0: store {slot=%u, gen=%u} re-imported on the new context (%llu bytes)",
res.Slot, res.Gen, static_cast<unsigned long long>(size));
} else {
MGLOG_E("Espryt T0: store {slot=%u, gen=%u} could not be re-imported after a context loss - "
"%s; the store has no GPU backing",
res.Slot, res.Gen, why.c_str());
RetireT0Import(*resource);
}
}
#endif
#endif
}
+23
View File
@@ -1042,6 +1042,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
// the legacy arm did; this member exists for the drains that have no object, which
// in P3a is the readback flush and the fp64 narrowing.
const Uint8* hostBytes = nullptr;
#endif
#if MOBILEGL_BUILD_DISAGGREGATED
// P11 B2 (T0): the client's AHardwareBuffer this store IS - imported with
// glBufferStorageExternalEXT, so `id` is immutable, persistentMapped and
// persistentPtr is this server's own coherent map of the client's pages. The
// reference is this twin's own, released only after a fence that follows the
// store's last use has signalled (RetireT0Import). Survives an ES context loss:
// the next ensure re-imports it. Null for every store T0 did not adopt.
void* externalAhb = nullptr;
#endif
};
@@ -1104,6 +1113,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
void RequireStagedCoverage(GLESBufferResource& resource, const Uint8* hostBase, SizeT start,
SizeT end, const char* site);
// P11 B2: THE CPU-READABLE BYTES OF A SPLIT SERVER'S BUFFER TWIN, for the handle-arm
// readers that have no frontend object (indirect commands, primitive-restart
// substitution, the multi-draw index rebase): the R-11 staged copy, or - for a T0 store,
// which has none - this server's own coherent map of the client's AHardwareBuffer, read
// exactly as the monolith arm reads the adopted map through MappedData().
inline const Uint8* SplitHostBytes(const GLESBufferResource& resource) {
if (resource.hostBytes != nullptr) return resource.hostBytes;
return resource.externalAhb != nullptr ? static_cast<const Uint8*>(resource.persistentPtr) : nullptr;
}
// M-3's WHOLE-STORE predicate, and the GUARD every caller of RequireStagedCoverage over
// a whole-store read has to carry: true when the application DECLARED the store's
// content (glBufferData(size, data)), so a coverage gap is a missing record; false when
@@ -1267,6 +1286,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
// Deletes GL buffers whose owning frontend objects died (possibly on a
// thread without a current ES context). Called from draw-time sync.
void ProcessDeferredBufferReleases();
#if MOBILEGL_BUILD_DISAGGREGATED
// P11 B2: the T0 half of that drain - run right after it (retired AHardwareBuffers).
void ProcessDeferredT0Retires();
#endif
// glBindBuffer with a redundant-bind cache for GL_ARRAY_BUFFER.
void BindBufferId(GLenum target, Uint id);
+1 -1
View File
@@ -199,7 +199,7 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
auto* resource = BufferImpl::FindBufferResourceForHandle(res);
if (resource == nullptr) RefuseMissingIndexBufferRecord(entry, res, "backend resource");
view.Size = BufferImpl::ResourceWidthForHandle(res);
view.HostBytes = resource->hostBytes;
view.HostBytes = BufferImpl::SplitHostBytes(*resource);
if (view.HostBytes != nullptr && IndexBufferRecordHasDefinedContent(st, res)) {
BufferImpl::RequireStagedCoverage(*resource, view.HostBytes, 0, view.Size,
"multidraw_index_rebase");