[Feat] (P13 W4a): Espryt monolith takes the record arm for buffers, indirect, client arrays, binding points and fp64 through an in-process verb port

This commit is contained in:
2026-10-06 08:14:37 -04:00
parent 081854653a
commit af84eac8ee
20 changed files with 374 additions and 63 deletions
+2 -1
View File
@@ -744,7 +744,8 @@ namespace MobileGL::MG_Config {
// caps, whether a store copies).
//
// MOBILEGL_PIPE_DATA_ARM=record (default) | frontend picks monolith's arm for the families
// already switched. It exists for W4-W6 only - bisecting a monolith red to the switch, and
// already switched, on a backend whose monolith takes the record arm at all (Espryt from W4a,
// Magma at its single flip). It exists for W4-W6 only - bisecting a monolith red to the switch, and
// the per-thread CPU record (ID-P13-2) - and goes in W6 with the frontend arms. Latched once
// by MG_ConfigLoader::Init(): the two arms keep their twins in different places.
extern Bool MonolithTakesRecordArm;
+6 -2
View File
@@ -38,7 +38,7 @@ namespace MobileGL::MG_Config {
String TransportEndpoint;
Bool SplitTransportRequestedByConfig = false;
IpcTable Ipc;
Bool MonolithTakesRecordArm = true;
Bool MonolithTakesRecordArm = false;
#endif
} // namespace MobileGL::MG_Config
@@ -465,7 +465,11 @@ namespace MobileGL::MG_ConfigLoader {
String lowered = value;
std::transform(lowered.begin(), lowered.end(), lowered.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
MG_Config::MonolithTakesRecordArm = true;
// P13 W4: the backends switch one at a time. Espryt's record arm runs in monolith from W4a;
// Magma's verb entry points pick one arm for every family at once, so it joins at its
// single flip, after Espryt's last family.
const Bool backendReady = MG_Config::ActiveBackendType == BackendType::DirectGLES;
MG_Config::MonolithTakesRecordArm = backendReady;
if (lowered.empty() || lowered == "record") return;
if (lowered == "frontend") {
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
+10
View File
@@ -25,6 +25,7 @@
#include <MG_Backend/MGPipe/PipeInputs.h> // MGPipeVerifyFlushSummary, before Debug::Close in DestroyImpl
#endif
#if MOBILEGL_BUILD_DISAGGREGATED
#include <MG_Remote/Client/EmitTables.h>
#include <MG_Remote/Client/ServerProbe.h>
#endif
@@ -182,6 +183,15 @@ namespace MobileGL {
#endif
{
MG_Backend::Init();
#if MOBILEGL_BUILD_DISAGGREGATED
// P13 W4: monolith's record arm reads the verb's own state, which only a verb sink
// writes; the port routes the ported verbs through one (MG_Remote/Client/EmitTables.cpp).
// Installed HERE, by the library's role-neutral init, rather than by MG_Backend's: the
// port is client code, and the server image must not need it to link.
if (MG_Config::RecordArmAliasesFrontend() && MG_Backend::pActiveBackendObject) {
MG_Remote::Client::InstallMonolithVerbPort(MG_Backend::gBackendFunctionsTable);
}
#endif
}
MGLOG_D("MG_Backend initialized");
MG_Impl::Init();
+32 -22
View File
@@ -364,7 +364,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// object's MappedData()/GetSize() (B3) and the client slot allocator (T2) are never
// read. Coverage is asserted: commands read from bytes no record staged would be
// zero-filled, which is a missing record, not a valid command stream.
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
if (MG_Config::DataArmIsRecord()) {
const MG_Pipe::MGPipeHandle handle = MG_Pipe::MGPipeApplier().VerbIndirectBuffer;
if (!MG_Pipe::MGPipeHandleIsNull(handle)) {
auto* resource = BufferImpl::FindBufferResourceForHandle(handle);
@@ -591,7 +591,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// the .def's own "WHO READS IT" section names this function, and now it does. The reason
// is the table's and is printed from it.
Bool ResolveBufferBindingSubsystemArm() {
if (MG_Config::Transport == MG_Config::TransportMode::Monolith) return false;
if (!MG_Config::DataArmIsRecord()) return false;
const Uint64 mask = MG_Config::Features.PipePush;
const Bool bitSet = (mask & MG_Pipe::kMGPipeSubsystemBufferBindings) != 0;
if (bitSet &&
@@ -819,7 +819,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
//
// The applier's ShaderBufferWritableMask survives as the server's record of WHICH
// points a shader may write through, which is what P9's narrowing channel will name.
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) return;
if (MG_Config::DataArmIsRecord()) return;
#endif
const SizeT bindingPointCnt =
MG_Pipe::gPipeInputs.GetTouchedBufferBindingPointCount(BufferTarget::ShaderStorage);
@@ -1378,7 +1378,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// P5c (hd, CONTRACT-P5C §3.5): under an active transport the buffer is the verb
// record's handle; the frontend binding slot and the client allocator are
// never read (T2). The twin's ensure is what SyncBoundBuffer's did.
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
if (MG_Config::DataArmIsRecord()) {
const MG_Pipe::MGPipeHandle handle = MG_Pipe::MGPipeApplier().VerbIndirectBuffer;
if (!MG_Pipe::MGPipeHandleIsNull(handle)) {
auto* resource = EnsureBufferResourceForHandle(nullptr, handle);
@@ -1450,7 +1450,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (hd, CONTRACT-P5C §3.5): see the draw-indirect twin above - the verb
// record's handle, never the frontend binding slot or the client allocator.
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
if (MG_Config::DataArmIsRecord()) {
const MG_Pipe::MGPipeHandle handle = MG_Pipe::MGPipeApplier().VerbDispatchIndirectBuffer;
if (!MG_Pipe::MGPipeHandleIsNull(handle)) {
auto* resource = EnsureBufferResourceForHandle(nullptr, handle);
@@ -2506,7 +2506,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// from the GL thread (MG_Impl/Pipe/OwnedDrawInputs.h), so there is nothing for the
// server to stage - and reading gPipeInputs there is the role violation this family's
// other monolith glue is guarded against.
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) return true;
if (MG_Config::DataArmIsRecord()) return true;
const auto& currentVAO = MG_Pipe::gPipeInputs.GetBoundVertexArray();
if (!currentVAO) return true;
@@ -2560,7 +2560,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
static Bool SyncClientSideVertexArraysForIndirectFetch(
const Uint8* commandBytes, SizeT commandOffset, GLsizei stride, GLsizei index, Uint8 indexSize,
const SharedPtr<MG_State::GLState::BufferObject>& commandBuffer) {
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) return true;
if (MG_Config::DataArmIsRecord()) return true;
// P13 W4a (ID-P8-14a): ASK FIRST WHETHER THERE IS ANYTHING TO SNAPSHOT. The command
// words below are read only to size a client-memory fetch; a VAO whose attributes all
// come from buffers has none, and reading them anyway cost a whole-buffer readback of a
@@ -2601,7 +2601,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (count <= 0 || instanceCount <= 0) return true;
// MONOLITH ONLY - see SyncClientSideVertexArraysForFetch; this wrapper's own read of
// the bound VAO is the frontend read that must not happen on an apply thread.
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) return true;
if (MG_Config::DataArmIsRecord()) return true;
const auto& currentVAO = MG_Pipe::gPipeInputs.GetBoundVertexArray();
if (!currentVAO) return true;
const Uint8 indexSize = static_cast<Uint8>(MG_Util::GetGLTypeSize(type));
@@ -4283,6 +4283,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
static_cast<MG_State::GLState::TextureObjectBuffer*>(imageBinding.Texture.get());
const auto& bufferObject = textureBuffer->GetBufferBindingSlot().GetBoundObject();
BufferImpl::MarkBufferGpuWritten(bufferObject);
#if MOBILEGL_BUILD_DISAGGREGATED
// P13 W4a: image units stay on this arm until W4c, but monolith's buffer readers
// are already the record arm's (SplitHostBytesForCpuRead), and they ask the TWIN.
if (MG_Config::DataArmIsRecord() && bufferObject) {
if (auto* twin = BufferImpl::FindBufferResourceForHandle(
BufferImpl::HandleOfBuffer(bufferObject.get()))) {
twin->serverGpuWritten = true;
}
}
#endif
}
}
@@ -8302,13 +8312,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
// resource resolves from it by handle - the frontend binding slot and the client slot
// allocator are never read (T2).
const MG_Pipe::MGPipeHandle verbBufferHandle =
MG_Config::Transport != MG_Config::TransportMode::Monolith
MG_Config::DataArmIsRecord()
? MG_Pipe::MGPipeApplier().VerbIndirectBuffer
: MG_Pipe::kMGPipeNullHandle;
#endif
const Bool useNative =
#if MOBILEGL_BUILD_DISAGGREGATED
MG_Config::Transport != MG_Config::TransportMode::Monolith
MG_Config::DataArmIsRecord()
? !MG_Pipe::MGPipeHandleIsNull(verbBufferHandle) && SupportsNativeIndirectDraws()
:
#endif
@@ -8325,7 +8335,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (paramsBinding >= 0) {
auto* resource =
#if MOBILEGL_BUILD_DISAGGREGATED
MG_Config::Transport != MG_Config::TransportMode::Monolith
MG_Config::DataArmIsRecord()
? BufferImpl::EnsureBufferResourceForHandle(nullptr, verbBufferHandle)
:
#endif
@@ -8404,13 +8414,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
// P5c (hd, CONTRACT-P5C §3.5): see ExecuteIndexedIndirectCommands - the verb's handle,
// never the frontend binding slot or the client allocator.
const MG_Pipe::MGPipeHandle verbBufferHandle =
MG_Config::Transport != MG_Config::TransportMode::Monolith
MG_Config::DataArmIsRecord()
? MG_Pipe::MGPipeApplier().VerbIndirectBuffer
: MG_Pipe::kMGPipeNullHandle;
#endif
const Bool useNative =
#if MOBILEGL_BUILD_DISAGGREGATED
MG_Config::Transport != MG_Config::TransportMode::Monolith
MG_Config::DataArmIsRecord()
? !MG_Pipe::MGPipeHandleIsNull(verbBufferHandle) && SupportsNativeIndirectDraws()
:
#endif
@@ -8425,7 +8435,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (paramsBinding >= 0) {
auto* resource =
#if MOBILEGL_BUILD_DISAGGREGATED
MG_Config::Transport != MG_Config::TransportMode::Monolith
MG_Config::DataArmIsRecord()
? BufferImpl::EnsureBufferResourceForHandle(nullptr, verbBufferHandle)
:
#endif
@@ -8868,7 +8878,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// the family's back.
void RefuseElementArrayBufferFromTheFrontend(const char* entry) {
#if MOBILEGL_BUILD_DISAGGREGATED
if (MG_Config::Transport == MG_Config::TransportMode::Monolith) return;
if (!MG_Config::DataArmIsRecord()) return;
if (!BufferImpl::VertexInputReadsRecords()) return;
MGLOG_F("MGPipe: Fatal{RoleViolation, \"MGPipeSlots\"} - %s read the frontend VAO's "
"element-array slot with a live transport. The index buffer of this family is "
@@ -8971,7 +8981,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// carried client indices (no index buffer bound) takes the client-pointer arm below,
// `indices` already resolved through the server's segment resolver. Monolith takes
// the original body, verbatim.
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
if (MG_Config::DataArmIsRecord()) {
const auto& applierState = MG_Pipe::MGPipeApplier();
const MG_Pipe::MGPipeHandle elements = applierState.IndexBuffer.Res;
serverElementBinding = 0;
@@ -9270,7 +9280,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// P5c (hd, CONTRACT-P5C §3.5): with an active transport the frontend binding slot
// is never read - the Execute* helpers resolve the buffer from the verb record's
// handle (T2). Monolith reads the slot as it always did.
MG_Config::Transport != MG_Config::TransportMode::Monolith
MG_Config::DataArmIsRecord()
? SharedPtr<MG_State::GLState::BufferObject>(nullptr)
:
#endif
@@ -9311,7 +9321,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// reads from the SERVER's staged shadow - the frontend binding slots, the frontend
// accessors (B3) and the client allocator (T2) are never read. Monolith takes the
// original body below, verbatim.
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
if (MG_Config::DataArmIsRecord()) {
const auto& applierState = MG_Pipe::MGPipeApplier();
const SizeT commandOffset = reinterpret_cast<SizeT>(indirect);
const SizeT commandBytes = commandOffset + static_cast<SizeT>(stride) * static_cast<SizeT>(maxdrawcount - 1) +
@@ -9439,7 +9449,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// P5c (hd, CONTRACT-P5C §3.5): with an active transport the frontend binding slot
// is never read - the Execute* helpers resolve the buffer from the verb record's
// handle (T2). Monolith reads the slot as it always did.
MG_Config::Transport != MG_Config::TransportMode::Monolith
MG_Config::DataArmIsRecord()
? SharedPtr<MG_State::GLState::BufferObject>(nullptr)
:
#endif
@@ -9477,7 +9487,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (hd, CONTRACT-P5C §3.5): see the indexed twin - the verb record's handles and
// the SERVER's staged shadow, never the frontend bindings or the client allocator.
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
if (MG_Config::DataArmIsRecord()) {
const auto& applierState = MG_Pipe::MGPipeApplier();
const SizeT commandOffset = reinterpret_cast<SizeT>(indirect);
const SizeT commandBytes = commandOffset + static_cast<SizeT>(stride) * static_cast<SizeT>(maxdrawcount - 1) +
@@ -9710,7 +9720,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// P5c (hd, CONTRACT-P5C §3.5): with an active transport the frontend binding slot
// is never read - the Execute* helpers resolve the buffer from the verb record's
// handle (T2). Monolith reads the slot as it always did.
MG_Config::Transport != MG_Config::TransportMode::Monolith
MG_Config::DataArmIsRecord()
? SharedPtr<MG_State::GLState::BufferObject>(nullptr)
:
#endif
@@ -9759,7 +9769,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// P5c (hd, CONTRACT-P5C §3.5): with an active transport the frontend binding slot
// is never read - the Execute* helpers resolve the buffer from the verb record's
// handle (T2). Monolith reads the slot as it always did.
MG_Config::Transport != MG_Config::TransportMode::Monolith
MG_Config::DataArmIsRecord()
? SharedPtr<MG_State::GLState::BufferObject>(nullptr)
:
#endif
+114 -15
View File
@@ -2105,6 +2105,19 @@ namespace MobileGL::MG_Backend::DirectGLES {
void Ops_H_Respecify(MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPResourceDesc& desc,
const void* initialBytes) {
auto* resource = FindBufferResourceForHandle(res);
#if MOBILEGL_BUILD_DISAGGREGATED
// P13 W4a (§1.4): ON MONOLITH'S RECORD ARM THE TWIN IS MINTED HERE, not lazily at
// the first draw. The record arm's readers have no frontend object to re-read a
// base from, so the base this call carries - the client's shadow, for an orphaning
// respecify too (PipeFill passes it there on this arm) - is the only way they learn
// it. Minting is GL-free; the storage stays lazy.
const Bool aliasesFrontend = MG_Config::RecordArmAliasesFrontend();
if (resource == nullptr && aliasesFrontend && initialBytes != nullptr) {
resource = GetOrCreateBufferResourceForHandle(res);
}
#else
constexpr Bool aliasesFrontend = false;
#endif
if (resource != nullptr) {
// A respecify's companion pointer IS the shadow base (offset 0) - and an
// ORPHANING one (glBufferData with NULL) carries HasDefinedContent clear and
@@ -2116,7 +2129,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
// every reader below treats a null base as "no bytes to move", which is the
// honest answer, and the ensure path re-reads the live base from the
// frontend object it still holds.
if (desc.HasDefinedContent != 0 && initialBytes != nullptr) {
// P13 W4a: an ALIASING store records the base whether or not the content is
// defined - the pointer is the frontend shadow's new allocation either way, and
// the upload below stays gated on the descriptor (RespecifyStorageWith's
// initialData), so an orphan still uploads nothing.
if ((desc.HasDefinedContent != 0 || aliasesFrontend) && initialBytes != nullptr) {
// R-11: in monolith this is the client's shadow base, unchanged; under
// split it is copied into server-owned storage first. A respecify's
// companion pointer is the base at offset 0 and covers the whole store.
@@ -2134,6 +2151,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
#if MOBILEGL_BUILD_DISAGGREGATED
// P8-C: whatever a shader wrote belonged to the store this call replaces.
resource->serverGpuWritten = false;
resource->contentSinceRespecify = false;
#endif
}
if (!resource) return; // lazy: the ensure path full-uploads on creation
@@ -2205,7 +2223,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// Monolith is untouched: the twin stays lazy there because the frontend object's
// MappedData is the source and nothing is lost by deferring the allocation (D-A2).
if (resource == nullptr && bytes != nullptr &&
MG_Config::Transport != MG_Config::TransportMode::Monolith) {
MG_Config::DataArmIsRecord()) {
resource = GetOrCreateBufferResourceForHandle(res);
}
#endif
@@ -2252,6 +2270,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
// ("these bytes, at this base") and the drain takes this mutex to lift them.
resource->hostBytes =
MGL_SERVER_STAGED_ADOPT(*resource, ResourceWidthOf(res), bytes, offset, size);
#if MOBILEGL_BUILD_DISAGGREGATED
if (size != 0) resource->contentSinceRespecify = true;
#endif
}
if (resource->pendingRespecify) return; // full re-upload pending anyway
if (!CanTouchGLNow() || resource->id == 0 ||
@@ -2312,6 +2333,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
const std::lock_guard<std::mutex> lock(resource->pendingMutex);
resource->hostBytes = MGL_SERVER_STAGED_ADOPT(*resource, ResourceWidthOf(res), bytes,
start, end - start);
#if MOBILEGL_BUILD_DISAGGREGATED
if (end > start) resource->contentSinceRespecify = true;
#endif
}
if (resource->pendingRespecify) return;
if (!CanTouchGLNow() || resource->id == 0 ||
@@ -2373,7 +2397,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// An empty capture may conservatively request a writeback before
// any draw materialized this buffer. Resolve its staged resource
// exactly as a GPU consumer would, then return the real bytes.
auto* resource = MG_Config::Transport != MG_Config::TransportMode::Monolith
auto* resource = MG_Config::DataArmIsRecord()
? EnsureBufferResourceForHandle(nullptr, res) : FindBufferResourceForHandle(res);
#else
auto* resource = FindBufferResourceForHandle(res);
@@ -2446,6 +2470,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
// The shadow now matches the backend byte for byte; without this the next draw
// would see a newer serial and re-upload the readback over it.
resource->syncedChangeSerial = ResourceSerialOf(res);
#if MOBILEGL_BUILD_DISAGGREGATED
// P13 W4a: on monolith's record arm the base this twin reads IS the shadow the
// writeback just filled, so a whole-store readback is also the refresh
// SplitHostBytesForCpuRead would owe. (A copying store's copy is not the
// client's shadow, and keeps its mark.)
if (MG_Config::RecordArmAliasesFrontend() && readOffset == 0 && size >= ResourceWidthOf(res)) {
resource->serverGpuWritten = false;
}
#endif
}
void Ops_H_Destroy(MG_Pipe::MGPipeHandle res) {
@@ -3455,6 +3488,64 @@ namespace MobileGL::MG_Backend::DirectGLES {
return record != nullptr && record->Desc.HasDefinedContent != 0;
}
// P13 W4a (§1.4): THE GPU-WRITE REFRESH OF AN ALIASING STORE. Monolith's record arm reads
// the frontend shadow in place (StagedShadowStore keeps the caller's pointer), so a store a
// shader wrote is refreshed the way the client refreshes it under split: the bytes are
// read back from this process's GL buffer and handed to OnBufferWriteback, whose consumer
// (the frontend, one call away) copies them into that shadow. The backend never writes
// frontend memory itself. Ops_H_Readback's order, for its reasons: queued uploads first,
// resident writes next, then the read, the serial stamp and - last - the epoch bump.
const Uint8* RefreshAliasedStoreFromGpu(GLESBufferResource& resource, MG_Pipe::MGPipeHandle res,
const char* site) {
if (!CanTouchGLNow() || resource.id == 0 || !resource.storageInitialized ||
resource.pendingRespecify || resource.contextGeneration != g_bufferContextGeneration ||
!g_GLESFuncs.glMapBufferRange || !g_GLESFuncs.glUnmapBuffer) {
MGLOG_E_ONCE("%s: buffer {%u, %u} was written on the GPU but its GL store cannot be read "
"back here, so the CPU reads the shadow as it is",
site, res.Slot, res.Gen);
return SplitHostBytes(resource);
}
const SizeT width = ResourceWidthOf(res);
const SizeT size = std::min(width, resource.storageSize);
if (size == 0) return SplitHostBytes(resource);
if (MG_Pipe::gMGPipeCallbacks.OnBufferWriteback == nullptr) {
MGLOG_E_ONCE("%s: buffer {%u, %u} was written on the GPU and no reverse channel is "
"installed to refresh the shadow it aliases",
site, res.Slot, res.Gen);
return SplitHostBytes(resource);
}
FlushPendingRangesFrom(resource, resource.hostBytes, width);
DrainResidentWritesNow(resource);
BindBufferId(TempBufferTarget, resource.id);
void* mapped = g_GLESFuncs.glMapBufferRange(TempBufferTarget, 0, static_cast<GLsizeiptr>(size),
GL_MAP_READ_BIT);
if (mapped == nullptr) {
MGLOG_E_ONCE("%s: glMapBufferRange(read) failed for GPU-written buffer %u; the CPU reads the "
"shadow as it is",
site, resource.id);
return SplitHostBytes(resource);
}
MG_Pipe::gMGPipeCallbacks.OnBufferWriteback(
res, 0,
MG_Pipe::MGPBlobRef{reinterpret_cast<Uint64>(mapped), static_cast<Uint64>(size),
MG_Pipe::kMGHostSpanSegNone, 0});
if (resource.hostBytes == nullptr) {
// Every content-carrying record and every respecify of a store with a size hands
// its base over on this arm, so no base here means a shape nothing recorded. Keep
// the bytes in a private copy rather than answer "nothing" for GPU-written bytes.
MGLOG_W_ONCE("%s: GPU-written buffer {%u, %u} has no recorded shadow base; its refreshed "
"bytes are kept in a private copy",
site, res.Slot, res.Gen);
const std::lock_guard<std::mutex> lock(resource.pendingMutex);
resource.hostBytes = ServerStaged().AdoptPrivateCopy(&resource, width, mapped, size);
}
g_GLESFuncs.glUnmapBuffer(TempBufferTarget);
resource.serverGpuWritten = false;
resource.syncedChangeSerial = ResourceSerialOf(res);
BumpBufferMutationEpoch();
return resource.hostBytes;
}
// P8-C. The declaration says what; this says in what order, and it is Ops_H_Readback's
// order because it is the same question asked by the server of itself instead of by the
// client: (1) queued uploads land in the GL store first - reading back past them would
@@ -3465,15 +3556,22 @@ namespace MobileGL::MG_Backend::DirectGLES {
// serial stamp and the epoch bump, as Ops_H_ReadbackTracked does them.
const Uint8* SplitHostBytesForCpuRead(GLESBufferResource& resource, MG_Pipe::MGPipeHandle res,
const char* site) {
// A coherent store - T0's import, or (P13 W4a) a map monolith's record arm donated -
// has no copy: its bytes are read through the map once queued writes have landed.
const Bool coherent = resource.persistentMapped && resource.persistentPtr != nullptr;
Bool residentWritesQueued = false;
if (resource.externalAhb != nullptr) {
if (coherent) {
const std::lock_guard<std::mutex> lock(resource.pendingMutex);
residentWritesQueued = !resource.pendingResidentWrites.empty();
}
if (!resource.serverGpuWritten && !residentWritesQueued) return SplitHostBytes(resource);
// The marks are made only under a transport, where the staged store COPIES; the guard
// keeps a monolith store (which keeps the caller's pointer) from adopting a mapping.
if (!ServerStaged().CopiesIntoServerStorage()) return SplitHostBytes(resource);
if (!ServerStaged().CopiesIntoServerStorage() && !coherent) {
// P13 W4a (§1.4): an ALIASING store (monolith's record arm) is refreshed through
// the frontend - the writeback lands in the shadow the base points into. Any other
// non-copying store has no marks to answer.
if (MG_Config::RecordArmAliasesFrontend()) return RefreshAliasedStoreFromGpu(resource, res, site);
return SplitHostBytes(resource);
}
if (!CanTouchGLNow() || resource.id == 0 || !resource.storageInitialized ||
resource.pendingRespecify || resource.contextGeneration != g_bufferContextGeneration ||
!g_GLESFuncs.glMapBufferRange || !g_GLESFuncs.glUnmapBuffer) {
@@ -3482,7 +3580,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
site, res.Slot, res.Gen);
return SplitHostBytes(resource);
}
if (resource.externalAhb != nullptr) {
if (coherent) {
// T0: the copy IS this server's coherent map of the client's pages. Landing the
// queued writes and finishing makes them, and every shader write already queued,
// visible through it - Ops_H_Readback's persistent-map arm, for the same reason.
@@ -3803,7 +3901,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// A null object is the "no frontend to ask" case and is treated as "not mapped",
// which is what the record already says.
const Bool askTheObjectWhetherItIsMapped =
MG_Config::Transport == MG_Config::TransportMode::Monolith;
!MG_Config::DataArmIsRecord();
if (askTheObjectWhetherItIsMapped && frontend != nullptr && frontend->IsMapped()) return false;
if (resource->pendingRespecify || !resource->storageInitialized) return false;
if (!resource->pendingRanges.empty()) return false;
@@ -3870,7 +3968,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// so a corrupt staged upload rendered the frontend's correct bytes and the R-2.5
// 0xDD audit could not reach a draw. R-2 / table 3: honest inproc is inproc that
// does not read the client object's memory. Under monolith nothing changes.
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
if (MG_Config::DataArmIsRecord()) {
return resource->hostBytes;
}
#endif
@@ -3907,7 +4005,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// run on every twin's first ensure - harmless there only because liveHostBase()
// prefers MappedData() under monolith, and "under monolith nothing changes" should
// be true by construction rather than by luck.
if (MG_Config::Transport != MG_Config::TransportMode::Monolith &&
if (ServerStaged().CopiesIntoServerStorage() &&
!ServerStaged().HasShadow(resource)) {
resource->hostBytes = nullptr;
}
@@ -4015,7 +4113,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// layer-1 surface ("buffer-legacy-arm") and the client's own pre-verb
// persistent-map push is the only producer; the call is skipped outright. Under
// monolith D-N's placement stands.
if (bufferObject && MG_Config::Transport == MG_Config::TransportMode::Monolith) {
if (bufferObject && !MG_Config::DataArmIsRecord()) {
bufferObject->SyncPersistentMappedRange();
}
#else
@@ -4064,9 +4162,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
// application's own declaration and the upload stays a pure NULL reallocation.
const Bool shadowHasContent =
#if MOBILEGL_BUILD_DISAGGREGATED
MG_Config::Transport != MG_Config::TransportMode::Monolith
MG_Config::DataArmIsRecord()
? (record->Desc.HasDefinedContent != 0 ||
ServerStaged().CoveredRunCount(resource) != 0)
(ServerStaged().CopiesIntoServerStorage() ? ServerStaged().CoveredRunCount(resource) != 0
: resource->contentSinceRespecify))
:
#endif
(bufferObject ? bufferObject->HasDefinedContent() : (record->Desc.HasDefinedContent != 0));
@@ -6216,7 +6315,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// rather than a stale read.
#if MOBILEGL_BUILD_DISAGGREGATED
const Bool sourceGpuWritten =
MG_Config::Transport != MG_Config::TransportMode::Monolith && resource->serverGpuWritten;
MG_Config::DataArmIsRecord() && resource->serverGpuWritten;
if (sourceGpuWritten) {
BufferImpl::SplitHostBytesForCpuRead(*resource, binding.Res, "fp64_vertex_narrowing");
}
+12 -2
View File
@@ -1085,6 +1085,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
// by SplitHostBytesForCpuRead's refresh and by a respecify. Only the server's own
// CPU readers consult it; the client keeps its own set (GpuWritePending.h).
Bool serverGpuWritten = false;
// P13 W4a: content-carrying records (sub-data, flush-range) arrived since the last
// respecify. A copying store answers this with its coverage; an ALIASING one (monolith's
// record arm) keeps no coverage, so the twin says it - the streaming idiom (orphan, then
// glBufferSubData) defines content no descriptor declares.
Bool contentSinceRespecify = false;
#endif
};
@@ -1159,7 +1164,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
// 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;
// P13 W4a: any coherent persistent store, not only T0's - monolith's record arm donates
// its maps in-process (Ops_H_MapPersistent), and such a store keeps no shadow at all.
// Under split the wire's map_persistent declines, so there this is still T0 alone.
return resource.persistentMapped && resource.persistentPtr != nullptr
? static_cast<const Uint8*>(resource.persistentPtr)
: nullptr;
}
// P8-C: THE SAME BYTES, MADE CURRENT FIRST - for a reader that CONSUMES them (the restart
@@ -1315,7 +1325,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// selectors that can drift apart. One definition, spelled at every site that reads it.
inline Bool VertexInputReadsRecords() {
#if MOBILEGL_BUILD_DISAGGREGATED
return MG_Config::Transport != MG_Config::TransportMode::Monolith &&
return MG_Config::DataArmIsRecord() &&
VertexInputSubsystemEnabled();
#else
return false;
+1 -1
View File
@@ -294,7 +294,7 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
// above, never inferred from a null).
Uint BoundDrawIndirectBufferId() {
#if MOBILEGL_BUILD_DISAGGREGATED
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
if (MG_Config::DataArmIsRecord()) {
const MG_Pipe::MGPipeHandle res = MG_Pipe::MGPipeApplier().VerbIndirectBuffer;
if (MG_Pipe::MGPipeHandleIsNull(res)) return 0;
auto* resource = BufferImpl::EnsureBufferResourceForHandle(nullptr, res);
+7 -2
View File
@@ -984,8 +984,13 @@ namespace MobileGL::MG_Pipe {
// InitialBytesNotCarried self-check aborted by name on the first
// glBufferData(target, 0, NULL, usage) of the bsl-esc-menu trace. `GetSize() > 0` is
// what makes the answer mean "there are bytes here" rather than "the store is defined".
const void* initialBytes =
(desc.HasDefinedContent != 0 && buffer.GetSize() > 0) ? buffer.MappedData() : nullptr;
//
// P13 W4a: ON MONOLITH'S RECORD ARM THE BASE RIDES AN ORPHANING RESPECIFY TOO. That arm's
// twin aliases this shadow and has no object to re-read it from, and an orphan is exactly
// the call that reallocates it; the backend still uploads nothing for it, because its
// upload is gated on the descriptor's HasDefinedContent and not on the pointer.
const Bool carryBase = desc.HasDefinedContent != 0 || MG_Config::RecordArmAliasesFrontend();
const void* initialBytes = (carryBase && buffer.GetSize() > 0) ? buffer.MappedData() : nullptr;
// kNeedsAck rides on the CALL and MGPipeResourceRespecifyNeedsAck(desc) decides per
// record: only an immutable store (a glBufferStorage*) is a real synchronous
// allocation and only it is allowed one. In monolith the acknowledgement is
+2 -2
View File
@@ -385,14 +385,14 @@ namespace MobileGL::MG_Pipe {
#if MOBILEGL_BUILD_DISAGGREGATED
// Client addresses never cross the transport. Draw emission snapshots
// their referenced elements into owned buffers with a zero byte origin.
if (MG_Config::Transport != MG_Config::TransportMode::Monolith &&
if (MG_Config::DataArmIsRecord() &&
!vao.GetAttribute(static_cast<Uint>(i)).Buffer) m_attributes[i].Offset = 0;
#endif
}
for (SizeT i = 0; i < kBindings; ++i) {
m_bindingPoints[i] = MGPipeBuildVertexBindingPointWire(vao.GetBindingPoint(static_cast<Uint>(i)));
#if MOBILEGL_BUILD_DISAGGREGATED
if (MG_Config::Transport != MG_Config::TransportMode::Monolith &&
if (MG_Config::DataArmIsRecord() &&
!vao.GetBindingPoint(static_cast<Uint>(i)).Buffer) m_bindingPoints[i].Offset = 0;
#endif
}
@@ -64,7 +64,8 @@ namespace MGITest {
// The resolved transport, for a message: "monolith", "inproc", "spawn", "unix", "pipe".
std::string transportName = "monolith";
// P13 W4: MG_Config::DataArmIsRecord() - false in a build without the peek (and without
// MG_Remote), which stays on the frontend arm until W5.
// MG_Remote), which stays on the frontend arm until W5, and for Magma's monolith until
// its single flip.
bool dataArmIsRecord = false;
// ClientSession::Active() != nullptr.
bool sessionActive = false;
@@ -236,7 +236,9 @@ void main() {
// THE ARMS THIS FILE SKIPS, BY NAME: the monolith arm of a backend whose reason is
// non-null. Every split arm, and the inproc A/B, runs the case.
bool SkipBrokenMonolith(const char* espryt, const char* magma) {
if (PeekSplitRuntime().transportName != "monolith") return false;
// P13 W4a: a monolith on the record arm runs the split arm's code, so it runs the case.
const SplitRuntimeState runtime = PeekSplitRuntime();
if (runtime.transportName != "monolith" || runtime.dataArmIsRecord) return false;
const std::string backend = Gl().BackendName();
const char* why = backend == "DirectGLES" ? espryt : backend == "DirectVulkan" ? magma : nullptr;
if (why == nullptr) return false;
@@ -304,7 +304,7 @@ void main() { words[gl_GlobalInvocationID.x] = source[gl_GlobalInvocationID.x];
// monolith defects), left to P13 (ID-P8-13).
TEST_F(WireIndirectDrawScenario, ComputeWrittenCountWordIsTheOneTheGpuReads) {
if (!Ready() || IsSkipped()) return;
if (!m_magma && m_transport == "monolith") {
if (!m_magma && m_transport == "monolith" && !PeekSplitRuntime().dataArmIsRecord) {
GTEST_SKIP() << "Espryt's monolith arm reads the count word from the frontend shadow without "
"a sync (OQ15; recorded for P13, ID-P8-13)";
}
+119 -9
View File
@@ -35,6 +35,8 @@
#include "PersistentMapTracker.h"
#include "../Server/ServerLoop.h"
#include "../Server/PipeApplier.h"
#include <MG_Backend/BackendObjects.h>
#include <MG_Util/Converters/GLToMG/TextureEnumConverter.h>
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
@@ -239,6 +241,87 @@ namespace MobileGL::MG_Remote::Client {
MG_Pipe::MGPipeDrainDeferredDestroys();
}
// ---- P13 W4: THE MONOLITH VERB PORT ----------------------------------------------
//
// Monolith's record arm (MG_Config::RecordArmAliasesFrontend) reads the verb's own state -
// VerbIndirectBuffer, VerbDispatchIndirectBuffer, ... - and nothing in monolith wrote it:
// ServerVerbSink is its only writer, and monolith never decoded a verb record. So the
// ported slots run THESE emitters - the same plan, the same pre-verb hooks in the same
// order (persistent-map push, owned client-memory inputs, the GPU-write marks) - and hand
// the planned record, in this process and on this thread, to a ServerVerbSink bound to
// the one backend object, which stamps the verb state exactly as the apply thread does
// and calls the backend's REAL table (ServerVerbSink::Table, never gBackendFunctionsTable,
// so nothing re-enters this port). One planner and one sink body serve both shapes: the
// monolith record arm is the split record arm minus the codec and the ring.
Bool g_monolithVerbPort = false;
Server::ServerVerbSink& MonolithVerbSink() {
// Leaked at exit like every other MG_Remote singleton (ID-8).
static Server::ServerVerbSink& sink = *new Server::ServerVerbSink();
return sink;
}
// The session a ported verb emits through - or null, which is the monolith port.
ClientSession* VerbSessionFor(const char* slot) {
if (g_monolithVerbPort) return nullptr;
return &RequireSession(slot);
}
void ApplyOnMonolithPort(MG_Pipe::MGPWireOp op, const void* payload, const Wire::WireTail* tails,
Uint32 tailCount) {
Server::ServerVerbSink& sink = MonolithVerbSink();
MG_Backend::BackendObject* backend = MG_Backend::pActiveBackendObject.get();
if (sink.Backend() != backend) sink.SetBackend(backend);
Bool applied = false;
const char* row = nullptr;
switch (op) {
case MG_Pipe::MGPWireOp::DrawVbo: {
row = "draw_vbo";
const auto& info = *static_cast<const MG_Pipe::MGPDrawInfo*>(payload);
const auto* ranges = (tailCount > 0 && tails[0].Size != 0)
? static_cast<const MG_Pipe::MGPDrawRange*>(tails[0].Bytes)
: nullptr;
const auto* indirect = ((info.Flags & MG_Pipe::kDrawIsIndirect) != 0 && tailCount > 1)
? static_cast<const MG_Pipe::MGPDrawIndirect*>(tails[1].Bytes)
: nullptr;
applied = sink.OnDrawVbo(info, ranges, nullptr, indirect);
break;
}
case MG_Pipe::MGPWireOp::LaunchGrid:
row = "launch_grid";
applied = sink.OnLaunchGrid(*static_cast<const MG_Pipe::MGPGridInfo*>(payload));
break;
default:
// A slot InstallMonolithVerbPort routes here whose row this switch does not know:
// the two lists drifted, and falling through to the driver would be the very
// "ran the other arm and looked fine" this port exists to end.
UnmigratedVerbFatal("MonolithVerbPort");
}
if (!applied) {
MGLOG_E_ONCE("MGPipe: the monolith verb port's %s was declined by the sink; nothing was "
"drawn or dispatched",
row);
}
}
// The emission of a ported verb: the session's, or the port's.
void EmitVerbTails(ClientSession* session, MG_Pipe::MGPWireOp op, const void* payload, Uint64 payloadBytes,
const Wire::WireTail* tails, Uint32 tailCount) {
if (session != nullptr) {
session->EmitAndWaitTails(op, payload, payloadBytes, tails, tailCount, nullptr, 0, nullptr);
return;
}
ApplyOnMonolithPort(op, payload, tails, tailCount);
}
void EmitVerbNoTail(ClientSession* session, MG_Pipe::MGPWireOp op, const void* payload,
Uint64 payloadBytes) {
if (session != nullptr) {
session->EmitAndWait(op, payload, payloadBytes, nullptr, 0, nullptr, 0, nullptr);
return;
}
ApplyOnMonolithPort(op, payload, nullptr, 0);
}
// =============================================================================
// CLASS B - the five slots the verb census measured (CONTRACT-P5.md §7)
// =============================================================================
@@ -519,7 +602,7 @@ namespace MobileGL::MG_Remote::Client {
const MG_Pipe::MGPDrawRange* ranges, Uint32 numDraws,
const void* clientIndices, Uint64 clientIndexBytes,
const MG_Pipe::MGPDrawIndirect* indirect) {
ClientSession& session = RequireSession(slot);
ClientSession* session = VerbSessionFor(slot);
PersistentMapTracker::Instance().PushDrawConsumers();
MG_Pipe::MGPipeDrainDeferredDestroys();
// Snapshot before marking THIS draw's potential GPU writes: resolving
@@ -554,8 +637,7 @@ namespace MobileGL::MG_Remote::Client {
tails[1] = {indirect, sizeof(*indirect)};
tailCount = 2;
}
session.EmitAndWaitTails(MG_Pipe::MGPWireOp::DrawVbo, &info, sizeof(info), tails,
tailCount, nullptr, 0, nullptr);
EmitVerbTails(session, MG_Pipe::MGPWireOp::DrawVbo, &info, sizeof(info), tails, tailCount);
}
// ---- the single-draw indexed family: DrawElements, DrawElementsBaseVertex, the two
@@ -1293,7 +1375,7 @@ namespace MobileGL::MG_Remote::Client {
// carried both calls before its Fatal precisely so that the package which flipped this
// slot would inherit a call site that was already correct.
void EmitDispatchCompute(GLuint numGroupsX, GLuint numGroupsY, GLuint numGroupsZ) {
ClientSession& session = RequireSession("DispatchCompute");
ClientSession* session = VerbSessionFor("DispatchCompute");
PersistentMapTracker::Instance().PushDrawConsumers();
MarkGpuWritesForDispatch();
@@ -1307,12 +1389,11 @@ namespace MobileGL::MG_Remote::Client {
record.IndirectBuffer = MG_Pipe::kMGPipeNullHandle;
record.IndirectOffset = 0;
record.IsIndirect = 0;
session.EmitAndWait(MG_Pipe::MGPWireOp::LaunchGrid, &record, sizeof(record), nullptr, 0,
nullptr, 0, nullptr);
EmitVerbNoTail(session, MG_Pipe::MGPWireOp::LaunchGrid, &record, sizeof(record));
}
void EmitDispatchComputeIndirect(GLintptr indirect) {
ClientSession& session = RequireSession("DispatchComputeIndirect");
ClientSession* session = VerbSessionFor("DispatchComputeIndirect");
PersistentMapTracker::Instance().PushDrawConsumers();
MarkGpuWritesForDispatch();
@@ -1333,8 +1414,7 @@ namespace MobileGL::MG_Remote::Client {
MG_Pipe::MGPipeKind::Buffer, bound->GetLifetimeId());
}
}
session.EmitAndWait(MG_Pipe::MGPWireOp::LaunchGrid, &record, sizeof(record), nullptr, 0,
nullptr, 0, nullptr);
EmitVerbNoTail(session, MG_Pipe::MGPWireOp::LaunchGrid, &record, sizeof(record));
}
// glMemoryBarrier / glMemoryBarrierByRegion. The bits cross VERBATIM: the frontend has
@@ -2125,6 +2205,36 @@ namespace MobileGL::MG_Remote::Client {
return table;
}
void InstallMonolithVerbPort(MG_Backend::GlobalBackendFunctionsTable& table) {
// P13 W4a: the draw and dispatch families. Later W4 steps add their verbs here AND to
// ApplyOnMonolithPort's switch, in the same commit.
table.GL.DrawArrays = &EmitDrawArrays;
table.GL.DrawElements = &EmitDrawElements;
table.GL.DrawElementsBaseVertex = &EmitDrawElementsBaseVertex;
table.GL.DrawRangeElements = &EmitDrawRangeElements;
table.GL.DrawRangeElementsBaseVertex = &EmitDrawRangeElementsBaseVertex;
table.GL.DrawElementsInstanced = &EmitDrawElementsInstanced;
table.GL.DrawElementsInstancedBaseVertex = &EmitDrawElementsInstancedBaseVertex;
table.GL.DrawElementsInstancedBaseInstance = &EmitDrawElementsInstancedBaseInstance;
table.GL.DrawElementsInstancedBaseVertexBaseInstance = &EmitDrawElementsInstancedBaseVertexBaseInstance;
table.GL.DrawArraysInstanced = &EmitDrawArraysInstanced;
table.GL.DrawArraysInstancedBaseInstance = &EmitDrawArraysInstancedBaseInstance;
table.GL.MultiDrawArrays = &EmitMultiDrawArrays;
table.GL.MultiDrawElements = &EmitMultiDrawElements;
table.GL.MultiDrawElementsBaseVertex = &EmitMultiDrawElementsBaseVertex;
table.GL.DrawArraysIndirect = &EmitDrawArraysIndirect;
table.GL.DrawElementsIndirect = &EmitDrawElementsIndirect;
table.GL.MultiDrawArraysIndirect = &EmitMultiDrawArraysIndirect;
table.GL.MultiDrawElementsIndirect = &EmitMultiDrawElementsIndirect;
table.GL.MultiDrawArraysIndirectCount = &EmitMultiDrawArraysIndirectCount;
table.GL.MultiDrawElementsIndirectCount = &EmitMultiDrawElementsIndirectCount;
table.GL.DispatchCompute = &EmitDispatchCompute;
table.GL.DispatchComputeIndirect = &EmitDispatchComputeIndirect;
g_monolithVerbPort = true;
}
Bool MonolithVerbPortInstalled() { return g_monolithVerbPort; }
Uint32 ImplementedVerbCount() { return kEmittedSlots; }
Uint32 LocallyAnsweredSlotCount() { return kLocallyAnsweredSlots; }
Uint32 UnmigratedSlotCount() { return kUnmigratedSlots; }
+7
View File
@@ -88,6 +88,13 @@ namespace MobileGL::MG_Remote::Client {
// that the message wording lives in exactly one place.
[[noreturn]] void UnmigratedVerbFatal(const char* slot);
// P13 W4: monolith's record arm. Overwrites the ported verb slots of the backend's own
// table with this file's emitters, which hand their planned record to an in-process
// ServerVerbSink instead of a session (EmitTables.cpp, "THE MONOLITH VERB PORT"). Installed
// by MG_Backend's bring-up when MG_Config::RecordArmAliasesFrontend() holds.
void InstallMonolithVerbPort(MG_Backend::GlobalBackendFunctionsTable& table);
Bool MonolithVerbPortInstalled();
// How many of the 71 slots have a real emitter. Reported at bring-up and asserted by the
// gate: a table that silently loses an emitter should not be able to look the same as one
// that never had it.
@@ -124,7 +124,9 @@ namespace MobileGL::MG_Remote::Client {
} // namespace
Bool GpuWriteSetIsClientSide() {
return MG_Config::Transport != MG_Config::TransportMode::Monolith;
// P13 W4a: wherever the backend reads records - every wire, and monolith's record arm -
// the client owns the conservative set; the backend marks only its own twins.
return MG_Config::DataArmIsRecord();
}
Bool ImageUnitIsAWritableBufferTexture(const ImageTextureBinding& binding) {
@@ -704,7 +704,9 @@ namespace MobileGL::MG_Remote::Client {
}
Bool PersistentMapTracker::PushIsArmed() {
return MG_Config::Transport != MG_Config::TransportMode::Monolith;
// P13 W4a: the record arm's buffer reads never ask the frontend object to sync a map, so
// the client pushes the mapped span wherever that arm runs - monolith's included.
return MG_Config::DataArmIsRecord();
}
// P5d round 3 (package D): the body this forwards to is now one relaxed load of the apply
+1
View File
@@ -152,6 +152,7 @@ namespace MobileGL::MG_Remote::Server {
// The server's private backend. Null until ServerLoop::CreateBackend has run, and a
// verb that arrives before then declines by name rather than dereferencing.
void SetBackend(MG_Backend::BackendObject* backend);
MG_Backend::BackendObject* Backend() const { return m_backend; }
void SetMaxReplyBytes(Uint64 bytes) { m_maxReplyBytes = bytes; }
// P11 B2: T0's map_persistent - ServerSession::AdoptStoreT0 (it owns the aux socket).
+16
View File
@@ -120,6 +120,22 @@ namespace MobileGL::MG_Remote::Server {
return shadow.Bytes.data();
}
// P13 W4a: a COPY even in a non-copying store - the fallback for a GPU-written store on
// monolith's record arm that never recorded the shadow base it aliases (Managers.cpp's
// RefreshAliasedStoreFromGpu). Whole-store, covered, and dropped with the key.
const Uint8* AdoptPrivateCopy(const void* key, SizeT width, const void* bytes, SizeT size) {
const std::lock_guard<std::mutex> lock(m_mutex);
Shadow& shadow = m_shadows[key];
shadow.SessionKey = CurrentStagedBucket().SessionKey;
if (shadow.Bytes.size() < std::max(width, size)) shadow.Bytes.resize(std::max(width, size), 0);
if (size != 0 && bytes != nullptr) {
std::memcpy(shadow.Bytes.data(), bytes, size);
CoverageAdd(shadow.Covered, 0, size);
}
m_any.store(true, std::memory_order_release);
return shadow.Bytes.data();
}
void Drop(const void* key) {
if (!m_any.load(std::memory_order_acquire)) return;
const std::lock_guard<std::mutex> lock(m_mutex);
@@ -544,8 +544,11 @@ namespace MobileGL::MG_State::GLState {
// cleared by the time the applier calls back, and a second clear here would also
// retire a GPU write announced by something other than a readback - a ReadPixels into
// a pack PBO writes back through this same function.
if (MG_Config::Transport != MG_Config::TransportMode::Monolith &&
atOffset == 0 && data.size >= m_size) {
//
// P13 W4a: monolith's record arm refreshes an aliased store through this function too
// (Managers.cpp RefreshAliasedStoreFromGpu), and a whole-store landing there is exactly as
// current as one under split.
if (MG_Config::DataArmIsRecord() && atOffset == 0 && data.size >= m_size) {
m_gpuWritePending = false;
}
#endif
+28
View File
@@ -23,6 +23,31 @@
using namespace MobileGL;
namespace {
// P13 W4a: THE MONOLITH FRONTEND ARM, PINNED. The cases that use this assert how the frontend
// object pushes a map's bytes when the backend reads the object itself - the arm W4 retires
// family by family and W6 deletes. On the record arm (every wire, and monolith's from W4a) the
// client's persistent-map tracker owns that push instead, which the split lanes cover. When
// W6 deletes the frontend arm, these cases go with it.
class ScopedMonolithFrontendArm {
public:
ScopedMonolithFrontendArm() {
#if MOBILEGL_BUILD_DISAGGREGATED
m_saved = MG_Config::MonolithTakesRecordArm;
MG_Config::MonolithTakesRecordArm = false;
#endif
}
~ScopedMonolithFrontendArm() {
#if MOBILEGL_BUILD_DISAGGREGATED
MG_Config::MonolithTakesRecordArm = m_saved;
#endif
}
private:
Bool m_saved = false;
};
} // namespace
class BufferTest : public ::testing::Test {
protected:
// GL error flags are sticky per error code and the context outlives an individual test in this
@@ -232,6 +257,7 @@ TEST_F(BufferTest, AcquireMemoryRangeWithoutExplicit) {
}
TEST_F(BufferTest, AcquireMemoryRangeWithExplicit) {
const ScopedMonolithFrontendArm frontendArm;
auto& slot = MobileGL::MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Uniform);
Vector<Uint> bufferNames;
MobileGL::MG_State::pGLContext->GenBufferNames(1, bufferNames);
@@ -2493,6 +2519,7 @@ TEST_F(BufferTest, MapBufferRangeAndUnmapBufferReseedAnAdoptedShaderStorageBuffe
// before - the staging copy is written back into the shadow and the backend's flush op
// carries the range down.
TEST_F(BufferTest, ANonPersistentWriteMapOfAShadowBackedStoreStillFlushesThroughTheBackend) {
const ScopedMonolithFrontendArm frontendArm;
ZeroCopyMockBackend mock;
mock.provideMap = false;
g_zeroCopyMock = &mock;
@@ -2953,6 +2980,7 @@ TEST_F(BufferTest, RespecifyingAStoreWhileItIsMappedDoesNotLandTheStagedBytesInt
}
TEST_F(BufferTest, RespecifyingAShadowBackedStoreWhileItIsMappedPushesNoRangeDown) {
const ScopedMonolithFrontendArm frontendArm;
ZeroCopyMockBackend mock;
mock.provideMap = false;
g_zeroCopyMock = &mock;