diff --git a/MobileGL/Config.h b/MobileGL/Config.h index a3cd05ad..1eff35c0 100644 --- a/MobileGL/Config.h +++ b/MobileGL/Config.h @@ -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; diff --git a/MobileGL/ConfigLoader.cpp b/MobileGL/ConfigLoader.cpp index 6539acbf..9af9c376 100644 --- a/MobileGL/ConfigLoader.cpp +++ b/MobileGL/ConfigLoader.cpp @@ -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(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) { diff --git a/MobileGL/Init.cpp b/MobileGL/Init.cpp index 43204961..a2fa8ff3 100644 --- a/MobileGL/Init.cpp +++ b/MobileGL/Init.cpp @@ -25,6 +25,7 @@ #include // MGPipeVerifyFlushSummary, before Debug::Close in DestroyImpl #endif #if MOBILEGL_BUILD_DISAGGREGATED +#include #include #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(); diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index 3b55e563..6a0ea539 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -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& 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(MG_Util::GetGLTypeSize(type)); @@ -4283,6 +4283,16 @@ namespace MobileGL::MG_Backend::DirectGLES { static_cast(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(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(indirect); const SizeT commandBytes = commandOffset + static_cast(stride) * static_cast(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(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(indirect); const SizeT commandBytes = commandOffset + static_cast(stride) * static_cast(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(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(nullptr) : #endif diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index deb8fa16..c72f7061 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -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 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(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(mapped), static_cast(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 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 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"); } diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.h b/MobileGL/MG_Backend/DirectGLES/Managers.h index 78e8bd6f..dcffc32b 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.h +++ b/MobileGL/MG_Backend/DirectGLES/Managers.h @@ -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(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(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; diff --git a/MobileGL/MG_Backend/DirectGLES/MultiDraw.cpp b/MobileGL/MG_Backend/DirectGLES/MultiDraw.cpp index 72abc1f5..b4703bf5 100644 --- a/MobileGL/MG_Backend/DirectGLES/MultiDraw.cpp +++ b/MobileGL/MG_Backend/DirectGLES/MultiDraw.cpp @@ -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); diff --git a/MobileGL/MG_Impl/Pipe/PipeFill.cpp b/MobileGL/MG_Impl/Pipe/PipeFill.cpp index 71b8446e..3481c385 100644 --- a/MobileGL/MG_Impl/Pipe/PipeFill.cpp +++ b/MobileGL/MG_Impl/Pipe/PipeFill.cpp @@ -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 diff --git a/MobileGL/MG_Impl/Pipe/VertexInputEmit.h b/MobileGL/MG_Impl/Pipe/VertexInputEmit.h index daf6c8b3..f732bb08 100644 --- a/MobileGL/MG_Impl/Pipe/VertexInputEmit.h +++ b/MobileGL/MG_Impl/Pipe/VertexInputEmit.h @@ -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(i)).Buffer) m_attributes[i].Offset = 0; #endif } for (SizeT i = 0; i < kBindings; ++i) { m_bindingPoints[i] = MGPipeBuildVertexBindingPointWire(vao.GetBindingPoint(static_cast(i))); #if MOBILEGL_BUILD_DISAGGREGATED - if (MG_Config::Transport != MG_Config::TransportMode::Monolith && + if (MG_Config::DataArmIsRecord() && !vao.GetBindingPoint(static_cast(i)).Buffer) m_bindingPoints[i].Offset = 0; #endif } diff --git a/MobileGL/MG_IntegrationTest/Harness/SplitRuntimePeek.h b/MobileGL/MG_IntegrationTest/Harness/SplitRuntimePeek.h index 6e874ef1..4dc343ce 100644 --- a/MobileGL/MG_IntegrationTest/Harness/SplitRuntimePeek.h +++ b/MobileGL/MG_IntegrationTest/Harness/SplitRuntimePeek.h @@ -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; diff --git a/MobileGL/MG_IntegrationTest/Scenarios/GpuWrittenDrawInputScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/GpuWrittenDrawInputScenario.cpp index 3a21788b..ec1484af 100644 --- a/MobileGL/MG_IntegrationTest/Scenarios/GpuWrittenDrawInputScenario.cpp +++ b/MobileGL/MG_IntegrationTest/Scenarios/GpuWrittenDrawInputScenario.cpp @@ -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; diff --git a/MobileGL/MG_IntegrationTest/Scenarios/WireIndirectDrawScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/WireIndirectDrawScenario.cpp index 7b0da08d..26f0e0cf 100644 --- a/MobileGL/MG_IntegrationTest/Scenarios/WireIndirectDrawScenario.cpp +++ b/MobileGL/MG_IntegrationTest/Scenarios/WireIndirectDrawScenario.cpp @@ -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)"; } diff --git a/MobileGL/MG_Remote/Client/EmitTables.cpp b/MobileGL/MG_Remote/Client/EmitTables.cpp index 10e9eddb..314ca2a9 100644 --- a/MobileGL/MG_Remote/Client/EmitTables.cpp +++ b/MobileGL/MG_Remote/Client/EmitTables.cpp @@ -35,6 +35,8 @@ #include "PersistentMapTracker.h" #include "../Server/ServerLoop.h" +#include "../Server/PipeApplier.h" +#include #include #include @@ -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(payload); + const auto* ranges = (tailCount > 0 && tails[0].Size != 0) + ? static_cast(tails[0].Bytes) + : nullptr; + const auto* indirect = ((info.Flags & MG_Pipe::kDrawIsIndirect) != 0 && tailCount > 1) + ? static_cast(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(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; } diff --git a/MobileGL/MG_Remote/Client/EmitTables.h b/MobileGL/MG_Remote/Client/EmitTables.h index f038f72c..e7c5dcf9 100644 --- a/MobileGL/MG_Remote/Client/EmitTables.h +++ b/MobileGL/MG_Remote/Client/EmitTables.h @@ -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. diff --git a/MobileGL/MG_Remote/Client/GpuWritePending.cpp b/MobileGL/MG_Remote/Client/GpuWritePending.cpp index 64ce3ae8..162665ab 100644 --- a/MobileGL/MG_Remote/Client/GpuWritePending.cpp +++ b/MobileGL/MG_Remote/Client/GpuWritePending.cpp @@ -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) { diff --git a/MobileGL/MG_Remote/Client/PersistentMapTracker.cpp b/MobileGL/MG_Remote/Client/PersistentMapTracker.cpp index 4f43d178..b6e781ae 100644 --- a/MobileGL/MG_Remote/Client/PersistentMapTracker.cpp +++ b/MobileGL/MG_Remote/Client/PersistentMapTracker.cpp @@ -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 diff --git a/MobileGL/MG_Remote/Server/PipeApplier.h b/MobileGL/MG_Remote/Server/PipeApplier.h index 1536aa6a..cb788e04 100644 --- a/MobileGL/MG_Remote/Server/PipeApplier.h +++ b/MobileGL/MG_Remote/Server/PipeApplier.h @@ -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). diff --git a/MobileGL/MG_Remote/Server/StagedShadow.h b/MobileGL/MG_Remote/Server/StagedShadow.h index b21554d6..c0e337a4 100644 --- a/MobileGL/MG_Remote/Server/StagedShadow.h +++ b/MobileGL/MG_Remote/Server/StagedShadow.h @@ -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 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 lock(m_mutex); diff --git a/MobileGL/MG_State/GLState/BufferState/BufferObject.cpp b/MobileGL/MG_State/GLState/BufferState/BufferObject.cpp index 05b62505..06f717de 100644 --- a/MobileGL/MG_State/GLState/BufferState/BufferObject.cpp +++ b/MobileGL/MG_State/GLState/BufferState/BufferObject.cpp @@ -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 diff --git a/MobileGL/MG_Test/Buffer/BufferTest.cpp b/MobileGL/MG_Test/Buffer/BufferTest.cpp index 99242fdc..1cf4d222 100644 --- a/MobileGL/MG_Test/Buffer/BufferTest.cpp +++ b/MobileGL/MG_Test/Buffer/BufferTest.cpp @@ -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 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;