diff --git a/MobileGL/MG_Impl/GLImpl/Sync/GL_Sync.cpp b/MobileGL/MG_Impl/GLImpl/Sync/GL_Sync.cpp index 9e8e02e9..97199d96 100644 --- a/MobileGL/MG_Impl/GLImpl/Sync/GL_Sync.cpp +++ b/MobileGL/MG_Impl/GLImpl/Sync/GL_Sync.cpp @@ -61,11 +61,10 @@ namespace MobileGL::MG_Impl::GLImpl { auto* syncObject = new SyncObject; syncObject->condition = condition; syncObject->flags = flags; - // The family's ONE gate. FenceSync is class C under split, and "absent" is the - // answer the whole fallback chain below is written against: every later site already - // checks syncObject->backendHandle, which stays null from here. The POINTER-valued - // macro keeps the init-statement byte-identical in a pull build (G1). - if (const auto backendFenceSync = MGL_BACKEND_SLOT_PTR_LOCAL(FenceSync)) { + // P5b: FenceSync is now a class-B emitter under split. Its server sink keeps the + // backend's optional/null-native fallback; the client must reach the wire first. + // This is the same pointer expression MGL_BACKEND_SLOT_PTR_LOCAL had in a pull build. + if (const auto backendFenceSync = MG_Backend::gBackendFunctionsTable.GL.FenceSync) { MGP_FILL(FenceSync); syncObject->backendHandle = backendFenceSync(); } diff --git a/MobileGL/MG_IntegrationTest/CMakeLists.txt b/MobileGL/MG_IntegrationTest/CMakeLists.txt index 913bd1ba..a5a35e2b 100644 --- a/MobileGL/MG_IntegrationTest/CMakeLists.txt +++ b/MobileGL/MG_IntegrationTest/CMakeLists.txt @@ -63,6 +63,7 @@ add_executable(MobileGLIntegrationTest Scenarios/ResidentIndexScenario.cpp Scenarios/MultiDrawScenario.cpp Scenarios/IndexedDrawFamilyScenario.cpp + Scenarios/SyncWireScenario.cpp Scenarios/DrawParametersScenario.cpp Scenarios/AsyncCompileScenario.cpp Scenarios/XfbAfterClipDistanceScenario.cpp @@ -2068,6 +2069,17 @@ if (MOBILEGL_BUILD_DISAGGREGATED) ENVIRONMENT "${MGL_ITEST_GLES_SPLIT_ENVIRONMENT}" ) + gtest_discover_tests(MobileGLIntegrationTest + TEST_PREFIX "DirectGLES.Split." + TEST_LIST MGL_SPLIT_SYNC_TESTS + TEST_FILTER "SyncWireScenario.*" + DISCOVERY_TIMEOUT 30 + PROPERTIES + LABELS "integration-gpu\;integration-split" + TIMEOUT ${MGL_ITEST_TIMEOUT} + ENVIRONMENT "${MGL_ITEST_GLES_SPLIT_ENVIRONMENT}" + ) + # The split half of the counting pair. MGITEST_PERSISTENT_MAP_ARM=emulated is R-6: under split # the adopt tier is pinned at T2, the resource owner declines every acquisition and the client # pushes the mapping's dirty blocks - so pmap must be non-zero and mpr must be the monolith diff --git a/MobileGL/MG_IntegrationTest/Harness/SplitLogPaths.cmake.in b/MobileGL/MG_IntegrationTest/Harness/SplitLogPaths.cmake.in index 1a97eed0..ca76b5c5 100644 --- a/MobileGL/MG_IntegrationTest/Harness/SplitLogPaths.cmake.in +++ b/MobileGL/MG_IntegrationTest/Harness/SplitLogPaths.cmake.in @@ -4,7 +4,7 @@ file(MAKE_DIRECTORY "@CMAKE_CURRENT_BINARY_DIR@/split-logs") # P5b t2's two lanes ride the same rule: one private log path per entry, or # SplitLogPaths.PrivateAndDistinct is red for them (ID-53). foreach(entry IN LISTS MGL_SPLIT_CLEAR_TESTS MGL_SPLIT_TRIANGLE_TESTS MGL_SPLIT_PMAP_TESTS - MGL_SPLIT_T2_TESS_TESTS MGL_SPLIT_T2_XFB_TESTS) + MGL_SPLIT_T2_TESS_TESTS MGL_SPLIT_T2_XFB_TESTS MGL_SPLIT_SYNC_TESTS) set_tests_properties("${entry}" PROPERTIES ENVIRONMENT "MOBILEGL_LOG_FILE_PATH=@CMAKE_CURRENT_BINARY_DIR@/split-logs/${entry}.log") endforeach() diff --git a/MobileGL/MG_IntegrationTest/Scenarios/SyncWireScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/SyncWireScenario.cpp new file mode 100644 index 00000000..58530b60 --- /dev/null +++ b/MobileGL/MG_IntegrationTest/Scenarios/SyncWireScenario.cpp @@ -0,0 +1,42 @@ +// P5b sync migration: a real fence and reply cross the apply thread. +#include "../Harness/ScenarioFixture.h" +#include "../Harness/SplitRuntimePeek.h" +#ifdef GLAPI +#undef GLAPI +#endif +#define GL_GLEXT_PROTOTYPES +#include +#include +#undef GL_GLEXT_PROTOTYPES + +namespace MGITest { +class SyncWireScenario : public ScenarioTest {}; + +TEST_F(SyncWireScenario, FenceWaitStatusAndDeletionCrossAndPreserveRenderedPixels) { + if (!Ready()) return; + const auto why = SplitRuntimeSkipReason(); + if (!why.empty()) GTEST_SKIP() << why; + glClearColor(0.25f, 0.5f, 0.75f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); + const auto before = PeekSplitRuntime().emitSeq; + const auto sync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); + ASSERT_NE(sync, nullptr); + EXPECT_TRUE(glIsSync(sync)); + GLint status = 0; + glGetSynciv(sync, GL_SYNC_STATUS, 1, nullptr, &status); + EXPECT_TRUE(status == GL_SIGNALED || status == GL_UNSIGNALED); + const auto wait = glClientWaitSync(sync, GL_SYNC_FLUSH_COMMANDS_BIT, 5000000000ull); + EXPECT_TRUE(wait == GL_ALREADY_SIGNALED || wait == GL_CONDITION_SATISFIED) << wait; + glWaitSync(sync, 0, GL_TIMEOUT_IGNORED); + glDeleteSync(sync); + EXPECT_FALSE(glIsSync(sync)); + EXPECT_GE(PeekSplitRuntime().emitSeq, before + 5); + GLubyte pixel[4]{}; + glReadPixels(1, 1, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixel); + const int expected[4] = {64, 128, 191, 255}; + for (int i = 0; i < 4; ++i) EXPECT_NEAR(pixel[i], expected[i], 1); + EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)); + // This one remains live until full session teardown, exercising server orphan cleanup. + ASSERT_NE(glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0), nullptr); +} +} // namespace MGITest diff --git a/MobileGL/MG_Pipe/FieldOwnership.def b/MobileGL/MG_Pipe/FieldOwnership.def index 9cccb3ec..01fd0930 100644 --- a/MobileGL/MG_Pipe/FieldOwnership.def +++ b/MobileGL/MG_Pipe/FieldOwnership.def @@ -287,7 +287,12 @@ X(PatchParameter, PatchParameteri) \ X(BindStreamOutput, BindTransformFeedback) \ X(SetStorageBlockBinding, ShaderStorageBlockBinding) \ - X(CopyFramebufferToTexture, CopyTexImage2D) + X(CopyFramebufferToTexture, CopyTexImage2D) \ + X(FenceCreate, FenceSync) \ + X(FenceStatus, GetSyncStatus) \ + X(FenceWait, ClientWaitSync) \ + X(FenceDestroy, DeleteSync) \ + X(FenceWaitServer, WaitSync) // X(Op, Why) - verb-shaped calls that are deliberately NOT stamp points. // diff --git a/MobileGL/MG_Pipe/MGPipeTypes.h b/MobileGL/MG_Pipe/MGPipeTypes.h index c64e919f..7ebbb32d 100644 --- a/MobileGL/MG_Pipe/MGPipeTypes.h +++ b/MobileGL/MG_Pipe/MGPipeTypes.h @@ -408,8 +408,10 @@ namespace MobileGL::MG_Pipe { struct MGPFenceWait { MGPipeHandle Fence; Uint64 TimeoutNs; + Uint32 Flags; + Uint32 Pad0; }; - MGP_ASSERT_POD(MGPFenceWait, 16); + MGP_ASSERT_POD(MGPFenceWait, 24); struct MGPQueryDesc { MGPipeHandle Query; diff --git a/MobileGL/MG_Pipe/PipeFields.def b/MobileGL/MG_Pipe/PipeFields.def index f24ea712..ca0147a7 100644 --- a/MobileGL/MG_Pipe/PipeFields.def +++ b/MobileGL/MG_Pipe/PipeFields.def @@ -54,7 +54,7 @@ F(BufOffset) F(BufSize) #define MGP_FIELDS_MGPFenceWait(F) \ - F(Fence) F(TimeoutNs) + F(Fence) F(TimeoutNs) F(Flags) #define MGP_FIELDS_MGPQueryDesc(F) \ F(Query) F(Kind) F(Stream) diff --git a/MobileGL/MG_Pipe/generated/PipeFieldOwnership.inc b/MobileGL/MG_Pipe/generated/PipeFieldOwnership.inc index 44cdf75b..8c3b3cdd 100644 --- a/MobileGL/MG_Pipe/generated/PipeFieldOwnership.inc +++ b/MobileGL/MG_Pipe/generated/PipeFieldOwnership.inc @@ -287,12 +287,17 @@ constexpr MGPipeVerb MGPipeVerbForWireOp(MGPWireOp op) { case MGPWireOp::BindStreamOutput: return MGPipeVerb::BindTransformFeedback; case MGPWireOp::SetStorageBlockBinding: return MGPipeVerb::ShaderStorageBlockBinding; case MGPWireOp::CopyFramebufferToTexture: return MGPipeVerb::CopyTexImage2D; + case MGPWireOp::FenceCreate: return MGPipeVerb::FenceSync; + case MGPWireOp::FenceStatus: return MGPipeVerb::GetSyncStatus; + case MGPWireOp::FenceWait: return MGPipeVerb::ClientWaitSync; + case MGPWireOp::FenceDestroy: return MGPipeVerb::DeleteSync; + case MGPWireOp::FenceWaitServer: return MGPipeVerb::WaitSync; default: return MGPipeVerb::kVerbCount; } } -inline constexpr SizeT kMGPipeVerbBoundaryOpCount = 18; +inline constexpr SizeT kMGPipeVerbBoundaryOpCount = 23; inline constexpr SizeT kMGPipeVerbBoundaryExemptCount = 3; // The class sizes, as constants a test can pin without recounting the table. diff --git a/MobileGL/MG_Remote/CONTRACT-P5B.md b/MobileGL/MG_Remote/CONTRACT-P5B.md index 0b001117..c3399a3b 100644 --- a/MobileGL/MG_Remote/CONTRACT-P5B.md +++ b/MobileGL/MG_Remote/CONTRACT-P5B.md @@ -451,3 +451,32 @@ The wave-3 tail no P5b package owns, by name, so nobody discovers it by grep: `B - The backends (`MG_Backend/DirectGLES`, `DirectVulkan`): untouched by c0b; a package that must touch one does so behind `#if MOBILEGL_BUILD_DISAGGREGATED` and names the region in its report (G1 admits no pull-build symbol motion; G5's untouched regions are pinned). + + +## §9 Wave 3: fence sync (integrator-approved, 2026-09-16) + +The dynamic Minecraft census exposes `FenceSync`. All five sync slots migrate together on +existing opcodes: FenceCreate/FenceStatus/FenceWait/FenceDestroy/FenceWaitServer. No opcode +moves and no creation reply is introduced. The client allocates a Fence-kind `{slot, gen}`; +a local opaque proxy satisfies the frontend's `BackendSyncHandle` API. Only the handle crosses +SEG_CMD. The server owns a generation-checked table of native backend sync handles, creates, +waits, queries and deletes them exclusively on the apply thread, and releases remaining native +objects before the backend is detached. Client orphan deletion after session shutdown only +releases its proxy. Duplicate creation and missing, destroyed or stale wire handles are protocol +corruption, since GL argument errors were already handled by the frontend. + +`MGPFenceWait` grows 16 → 24 bytes: append `Uint32 Flags; Uint32 Pad0;` after TimeoutNs. +Flags preserves `GL_SYNC_FLUSH_COMMANDS_BIT` for ClientWaitSync; server WaitSync accepts only +zero flags and GL_TIMEOUT_IGNORED. Existing ABI fingerprinting rejects mixed layouts. +FenceStatus and FenceWait retain their existing reply slots: an OK reply is exactly one Uint32, +respectively 0/1 or the backend's GL wait enum. Missing backend declines and never manufactures +an OK result. A present backend with no FenceSync slot or a FenceSync call returning null uses +exactly GL_Sync.cpp's existing always-signaled fallback; a real native fence's failed/timeout +wait answer is returned unchanged. A missing native wait/status slot uses the same frontend +fallback. This is compatibility with existing monolith behavior, not an unconditional success. + +All five opcodes stamp their matching MGPipeVerb (`FenceSync`, `GetSyncStatus`, +`ClientWaitSync`, `DeleteSync`, `WaitSync`). Every record keeps the verb barrier. The sole `GL_Sync.cpp` FenceSync guard changes from the class-C LOCAL macro to its original +table pointer expression: in a pull build these are identical. Under split it reaches the +class-B emitter, and the server preserves the optional-native fallback. No backend changes. The wave-3 tail's five slots move C → B; d1/i1/t2/f1 ownership +counts remain unchanged. diff --git a/MobileGL/MG_Remote/Client/EmitTables.cpp b/MobileGL/MG_Remote/Client/EmitTables.cpp index 9b09a121..6a50b9cf 100644 --- a/MobileGL/MG_Remote/Client/EmitTables.cpp +++ b/MobileGL/MG_Remote/Client/EmitTables.cpp @@ -10,7 +10,7 @@ // // THE PARTITION IS CONTRACT-P5.md §7's AND IS NOT RE-DERIVED HERE (R-15, ID-12): // class A 2 slots answered locally from the caps mirror, never emitted, never Fatal -// class B 49 slots emitted; class C 20 slots name their unmigrated verb. +// class B 54 slots emitted; class C 15 slots name their unmigrated verb. // The three counts are static_asserted to sum to kRemoteEmitSlotCount below, so a slot that // changes class without changing the arithmetic is a build break rather than a behaviour // change nobody reviewed. @@ -1441,7 +1441,84 @@ namespace MobileGL::MG_Remote::Client { } // ============================================================================= - // CLASS C - 21 slots remain after d1/i1/t2/f1; each names its first blocker. + // The frontend sees a local opaque token; neither this address nor the driver's + // BackendSyncHandle is serialized. The Fence-kind slot allocator supplies wire identity. + struct RemoteFenceProxy { MG_Pipe::MGPipeHandle Handle; }; + std::mutex g_fenceMutex; + UnorderedMap> g_fenceProxies; + + MG_Pipe::MGPipeHandle FenceHandle(MG_Backend::BackendSyncHandle proxy) { + const auto it = g_fenceProxies.find(proxy); + if (it == g_fenceProxies.end()) + Wire::WireProtocolFatal("Fence.proxy", "unknown client fence proxy"); + return it->second->Handle; + } + + MG_Backend::BackendSyncHandle EmitFenceSync() { + ClientSession& session = RequireSession("FenceSync"); + const std::lock_guard lock(g_fenceMutex); + BeforeReadOnlyVerb(); + auto proxy = MakeUnique(); + proxy->Handle = MG_Pipe::MGPipeSlots().Allocate(MG_Pipe::MGPipeKind::Fence); + const MG_Pipe::MGPHandleOnly desc{proxy->Handle, static_cast(MG_Pipe::MGPipeKind::Fence), 0}; + session.EmitAndWait(MG_Pipe::MGPWireOp::FenceCreate, &desc, sizeof(desc), + nullptr, 0, nullptr, 0, nullptr); + auto* local = proxy.get(); + g_fenceProxies.emplace(local, std::move(proxy)); + return local; + } + + Uint32 ReadFenceReply(ClientSession& session, MG_Pipe::MGPWireOp op, + const void* payload, Uint64 bytes) { + Uint32 result = 0; + Int32 status = Wire::ReplySink::kStatusError; + Uint64 replyBytes = 0; + session.EmitAndWait(op, payload, bytes, nullptr, 0, &result, sizeof(result), + &status, &replyBytes); + if (status != Wire::ReplySink::kStatusOk || replyBytes != sizeof(result)) + Wire::WireProtocolFatal("Fence.reply", "missing or malformed sync result"); + return result; + } + + GLenum EmitClientWaitSync(MG_Backend::BackendSyncHandle proxy, GLbitfield flags, GLuint64 timeout) { + ClientSession& session = RequireSession("ClientWaitSync"); + const std::lock_guard lock(g_fenceMutex); + const MG_Pipe::MGPFenceWait request{FenceHandle(proxy), timeout, flags, 0}; + return ReadFenceReply(session, MG_Pipe::MGPWireOp::FenceWait, &request, sizeof(request)); + } + + Bool EmitGetSyncStatus(MG_Backend::BackendSyncHandle proxy) { + ClientSession& session = RequireSession("GetSyncStatus"); + const std::lock_guard lock(g_fenceMutex); + const MG_Pipe::MGPHandleOnly desc{FenceHandle(proxy), static_cast(MG_Pipe::MGPipeKind::Fence), 0}; + const Uint32 result = ReadFenceReply(session, MG_Pipe::MGPWireOp::FenceStatus, &desc, sizeof(desc)); + if (result > 1) Wire::WireProtocolFatal("FenceStatus.reply", "status must be boolean"); + return result != 0; + } + + void EmitWaitSync(MG_Backend::BackendSyncHandle proxy, GLbitfield flags, GLuint64 timeout) { + ClientSession& session = RequireSession("WaitSync"); + const std::lock_guard lock(g_fenceMutex); + const MG_Pipe::MGPFenceWait request{FenceHandle(proxy), timeout, flags, 0}; + session.EmitAndWait(MG_Pipe::MGPWireOp::FenceWaitServer, &request, sizeof(request), + nullptr, 0, nullptr, 0, nullptr); + } + + void EmitDeleteSync(MG_Backend::BackendSyncHandle proxy) { + const std::lock_guard lock(g_fenceMutex); + const auto handle = FenceHandle(proxy); + // MobileGL::Destroy stops the server BEFORE DestroyAllSyncObjects. Detach already + // released those native objects on the apply thread; only the local proxy remains. + if (auto* session = ClientSession::Active(); session != nullptr && session->Started()) { + const MG_Pipe::MGPHandleOnly desc{handle, static_cast(MG_Pipe::MGPipeKind::Fence), 0}; + session->EmitAndWait(MG_Pipe::MGPWireOp::FenceDestroy, &desc, sizeof(desc), + nullptr, 0, nullptr, 0, nullptr); + } + MG_Pipe::MGPipeSlots().Free(MG_Pipe::MGPipeKind::Fence, handle); + g_fenceProxies.erase(proxy); + } + + // CLASS C - 16 slots remain after d1/i1/t2/f1; each names its first blocker. // ============================================================================= // // PARTITIONED BY THE P5b PACKAGE THAT OWNS THE FLIP (MG_Remote/CONTRACT-P5B.md, @@ -1501,8 +1578,6 @@ namespace MobileGL::MG_Remote::Client { X(GetTextureImage, void, \ (const SharedPtr&, TextureUploadTarget, GLint, GLenum, \ GLenum, GLsizei, GLvoid*)) \ - X(WaitSync, void, (MG_Backend::BackendSyncHandle, GLbitfield, GLuint64)) \ - X(DeleteSync, void, (MG_Backend::BackendSyncHandle)) \ X(EndTimeElapsedQuery, void, (MG_Backend::BackendQueryHandle)) \ X(DeleteBackendQuery, void, (MG_Backend::BackendQueryHandle)) \ X(EndOcclusionQuery, void, (MG_Backend::BackendQueryHandle)) \ @@ -1514,9 +1589,6 @@ namespace MobileGL::MG_Remote::Client { // warn on the second. It does see it; they are split for readability. All ten are the // wave-3 tail. #define MGR_UNMIGRATED_TAIL_VALUE_SLOTS(X) \ - X(FenceSync, MG_Backend::BackendSyncHandle, ()) \ - X(ClientWaitSync, GLenum, (MG_Backend::BackendSyncHandle, GLbitfield, GLuint64)) \ - X(GetSyncStatus, Bool, (MG_Backend::BackendSyncHandle)) \ X(BeginTimeElapsedQuery, MG_Backend::BackendQueryHandle, ()) \ X(QueryCounterTimestamp, MG_Backend::BackendQueryHandle, ()) \ X(IsQueryResultAvailable, Bool, (MG_Backend::BackendQueryHandle)) \ @@ -1570,9 +1642,10 @@ namespace MobileGL::MG_Remote::Client { constexpr Uint32 kEmittedSlotsT2 = 6; constexpr Uint32 kEmittedSlotsF1 = 11; constexpr Uint32 kEmittedSlotsTail = 1; // BlitNamedFramebuffer + constexpr Uint32 kEmittedSlotsSync = 5; constexpr Uint32 kEmittedSlots = kEmittedSlotsP5 + kEmittedSlotsD1 + kEmittedSlotsI1 + kEmittedSlotsT2 + kEmittedSlotsF1 + - kEmittedSlotsTail; + kEmittedSlotsTail + kEmittedSlotsSync; constexpr Uint32 kLocallyAnsweredSlots = 2; // GetIntegeri_v, IsTimerQuerySupported // EACH PACKAGE'S OWNERSHIP, PINNED. A package that flips a slot removes one row and @@ -1583,7 +1656,8 @@ namespace MobileGL::MG_Remote::Client { static_assert(kUnmigratedI1 + kEmittedSlotsI1 == 7, "i1 owns the 7 image/compute/barrier/copy/SSBO slots"); static_assert(kUnmigratedT2 + kEmittedSlotsT2 == 7, "t2 owns the 7 XFB/tessellation slots"); static_assert(kUnmigratedF1 + kEmittedSlotsF1 == 11, "f1 owns the 11 clear/copy/mip slots"); - static_assert(kUnmigratedTail + kEmittedSlotsTail == 20, "the original wave-3 tail owns 20 slots"); + static_assert(kUnmigratedTail + kEmittedSlotsTail + kEmittedSlotsSync == 20, + "the original wave-3 tail owns 20 slots"); static_assert(kUnmigratedSlots + kEmittedSlots == 69, "class B and C own 69 slots"); static_assert(kLocallyAnsweredSlots + kEmittedSlots + kUnmigratedSlots == kRemoteEmitSlotCount, "the three classes no longer partition the 71 slots"); @@ -1603,6 +1677,12 @@ namespace MobileGL::MG_Remote::Client { #undef MGR_ASSIGN_UNMIGRATED table.SetSwapInterval = &SetSwapInterval_Unmigrated; + table.GL.FenceSync = &EmitFenceSync; + table.GL.ClientWaitSync = &EmitClientWaitSync; + table.GL.GetSyncStatus = &EmitGetSyncStatus; + table.GL.WaitSync = &EmitWaitSync; + table.GL.DeleteSync = &EmitDeleteSync; + // ---- class A table.GL.GetIntegeri_v = &AnswerGetIntegeri_v; table.GL.IsTimerQuerySupported = &AnswerIsTimerQuerySupported; diff --git a/MobileGL/MG_Remote/Client/SlotCaps.h b/MobileGL/MG_Remote/Client/SlotCaps.h index ca50ba04..517a3d16 100644 --- a/MobileGL/MG_Remote/Client/SlotCaps.h +++ b/MobileGL/MG_Remote/Client/SlotCaps.h @@ -52,11 +52,8 @@ // class-C slot Fatals by name instead. // // Converted, because the fallback is right: -// GL_Sync.cpp:59 FenceSync -> always-signaled syncs, which the table's own -// header documents as the fallback and which GL -// permits; every other sync site is already -// guarded on syncObject->backendHandle, so this -// one gate carries the whole family. +// FenceSync used this fallback through P5. P5b §9 moved it to class B: the frontend +// now calls the emitter and the server preserves the optional/native-null fallback. // GL_Texture.cpp:6537 GetTextureImage -> the frontend's own CPU readback, which is exact // GL_Texture.cpp:6799 GetTexImage -> the same // GL_Getter.cpp x2 GetGpuTimestampNs -> 0, which BackendObject.h:192 already names as diff --git a/MobileGL/MG_Remote/Server/PipeApplier.cpp b/MobileGL/MG_Remote/Server/PipeApplier.cpp index 0c8cdcbb..bcbd99ba 100644 --- a/MobileGL/MG_Remote/Server/PipeApplier.cpp +++ b/MobileGL/MG_Remote/Server/PipeApplier.cpp @@ -50,7 +50,10 @@ namespace MobileGL::MG_Remote::Server { // ServerVerbSink - the five class-B verbs // ----------------------------------------------------------------------------------- - void ServerVerbSink::SetBackend(MG_Backend::BackendObject* backend) { m_backend = backend; } + void ServerVerbSink::SetBackend(MG_Backend::BackendObject* backend) { + if (m_backend != backend) ReleaseFences(); + m_backend = backend; + } const MG_Backend::GlobalBackendFunctionsTable* ServerVerbSink::Table(const char* verb) const { if (m_backend == nullptr) { @@ -68,6 +71,96 @@ namespace MobileGL::MG_Remote::Server { return &m_backend->GetBackendFunctions(); } + ServerVerbSink::FenceEntry& ServerVerbSink::FindFence(MG_Pipe::MGPipeHandle handle) { + const auto it = m_fences.find(handle.Slot); + if (handle.Slot == 0 || it == m_fences.end() || + !it->second.Live || it->second.Gen != handle.Gen) { + Wire::WireProtocolFatal("Fence.handle", "missing, destroyed or stale fence handle"); + } + return it->second; + } + + Bool ServerVerbSink::OnFenceCreate(const MG_Pipe::MGPHandleOnly& desc) { + if (desc.Kind != static_cast(MG_Pipe::MGPipeKind::Fence)) + Wire::WireProtocolFatal("Fence.Kind", "expected Fence namespace"); + const auto* table = Table("FenceCreate"); + if (table == nullptr) return false; + const auto handle = desc.Handle; + if (handle.Slot == 0) { + Wire::WireProtocolFatal("FenceCreate.handle", "reserved fence handle"); + } + const Bool seen = m_fences.find(handle.Slot) != m_fences.end(); + auto& entry = m_fences[handle.Slot]; + if (entry.Live || (seen && handle.Gen <= entry.Gen)) { + Wire::WireProtocolFatal("FenceCreate.handle", "duplicate or stale fence generation"); + } + entry.Gen = handle.Gen; + entry.Live = true; + // GL_Sync.cpp treats an absent slot or a null creation result as always signaled. + entry.Native = table->GL.FenceSync == nullptr ? nullptr : table->GL.FenceSync(); + return true; + } + + Bool ServerVerbSink::OnFenceDestroy(const MG_Pipe::MGPHandleOnly& desc) { + if (desc.Kind != static_cast(MG_Pipe::MGPipeKind::Fence)) + Wire::WireProtocolFatal("Fence.Kind", "expected Fence namespace"); + auto& entry = FindFence(desc.Handle); + const auto* table = Table("FenceDestroy"); + if (table == nullptr) return false; + if (entry.Native != nullptr && table->GL.DeleteSync != nullptr) table->GL.DeleteSync(entry.Native); + entry.Native = nullptr; + entry.Live = false; + return true; + } + + Bool ServerVerbSink::OnFenceStatus(const MG_Pipe::MGPHandleOnly& desc, Uint32& result) { + if (desc.Kind != static_cast(MG_Pipe::MGPipeKind::Fence)) + Wire::WireProtocolFatal("Fence.Kind", "expected Fence namespace"); + auto& entry = FindFence(desc.Handle); + const auto* table = Table("FenceStatus"); + if (table == nullptr) return false; + result = entry.Native == nullptr || table->GL.GetSyncStatus == nullptr || + table->GL.GetSyncStatus(entry.Native); + return true; + } + + Bool ServerVerbSink::OnFenceWait(const MG_Pipe::MGPFenceWait& request, Uint32& result) { + if ((request.Flags & ~static_cast(GL_SYNC_FLUSH_COMMANDS_BIT)) != 0) { + Wire::WireProtocolFatal("FenceWait.Flags", "unknown client-wait flag"); + } + auto& entry = FindFence(request.Fence); + const auto* table = Table("FenceWait"); + if (table == nullptr) return false; + result = entry.Native == nullptr || table->GL.ClientWaitSync == nullptr + ? GL_ALREADY_SIGNALED + : table->GL.ClientWaitSync(entry.Native, request.Flags, request.TimeoutNs); + return true; + } + + Bool ServerVerbSink::OnFenceWaitServer(const MG_Pipe::MGPFenceWait& request) { + if (request.Flags != 0 || request.TimeoutNs != GL_TIMEOUT_IGNORED) { + Wire::WireProtocolFatal("FenceWaitServer.arguments", "invalid server wait arguments"); + } + auto& entry = FindFence(request.Fence); + const auto* table = Table("FenceWaitServer"); + if (table == nullptr) return false; + if (entry.Native != nullptr && table->GL.WaitSync != nullptr) + table->GL.WaitSync(entry.Native, request.Flags, request.TimeoutNs); + return true; + } + + void ServerVerbSink::ReleaseFences() { + // Detach runs on the apply thread before its private backend/context is destroyed. + if (m_backend != nullptr) { + const auto destroy = m_backend->GetBackendFunctions().GL.DeleteSync; + if (destroy != nullptr) { + for (auto& [slot, entry] : m_fences) + if (entry.Live && entry.Native != nullptr) destroy(entry.Native); + } + } + m_fences.clear(); + } + Bool ServerVerbSink::OnClear(const MG_Pipe::MGPClear& clear) { const MG_Backend::GlobalBackendFunctionsTable* table = Table("clear"); if (table == nullptr) return false; diff --git a/MobileGL/MG_Remote/Server/PipeApplier.h b/MobileGL/MG_Remote/Server/PipeApplier.h index 74331a63..4c407586 100644 --- a/MobileGL/MG_Remote/Server/PipeApplier.h +++ b/MobileGL/MG_Remote/Server/PipeApplier.h @@ -102,6 +102,12 @@ namespace MobileGL::MG_Remote::Server { // verb that arrives before then declines by name rather than dereferencing. void SetBackend(MG_Backend::BackendObject* backend); + Bool OnFenceCreate(const MG_Pipe::MGPHandleOnly&) override; + Bool OnFenceDestroy(const MG_Pipe::MGPHandleOnly&) override; + Bool OnFenceStatus(const MG_Pipe::MGPHandleOnly&, Uint32&) override; + Bool OnFenceWait(const MG_Pipe::MGPFenceWait&, Uint32&) override; + Bool OnFenceWaitServer(const MG_Pipe::MGPFenceWait&) override; + void ReleaseFences(); Bool OnClear(const MG_Pipe::MGPClear& clear) override; Bool OnBlit(const MG_Pipe::MGPBlit& blit) override; Bool OnPresent(const MG_Pipe::MGPPresent& present) override; @@ -201,6 +207,13 @@ namespace MobileGL::MG_Remote::Server { private: const MG_Backend::GlobalBackendFunctionsTable* Table(const char* verb) const; + struct FenceEntry { + Uint32 Gen = 0; + Bool Live = false; + MG_Backend::BackendSyncHandle Native = nullptr; + }; + FenceEntry& FindFence(MG_Pipe::MGPipeHandle handle); + UnorderedMap m_fences; MG_Backend::BackendObject* m_backend = nullptr; Uint64 m_clears = 0; Uint64 m_draws = 0; diff --git a/MobileGL/MG_Remote/Wire/PipeWireCodec.cpp b/MobileGL/MG_Remote/Wire/PipeWireCodec.cpp index 20974dd8..95e629a5 100644 --- a/MobileGL/MG_Remote/Wire/PipeWireCodec.cpp +++ b/MobileGL/MG_Remote/Wire/PipeWireCodec.cpp @@ -1537,12 +1537,24 @@ namespace MobileGL::MG_Remote::Wire { MGPipeApplyUnmapPersistent(*static_cast(payload)); return true; - // ---- fences and queries: off the reduced path (BRIEF §4) ------------------------- case MGPWireOp::FenceCreate: - case MGPWireOp::FenceStatus: - case MGPWireOp::FenceWait: + return m_verbs != nullptr && m_verbs->OnFenceCreate(*static_cast(payload)); case MGPWireOp::FenceDestroy: + return m_verbs != nullptr && m_verbs->OnFenceDestroy(*static_cast(payload)); case MGPWireOp::FenceWaitServer: + return m_verbs != nullptr && m_verbs->OnFenceWaitServer(*static_cast(payload)); + case MGPWireOp::FenceStatus: + case MGPWireOp::FenceWait: { + Uint32 result = 0; + const Bool ok = m_verbs != nullptr && + (op == MGPWireOp::FenceStatus + ? m_verbs->OnFenceStatus(*static_cast(payload), result) + : m_verbs->OnFenceWait(*static_cast(payload), result)); + PostReply(op, seq, ok ? ReplySink::kStatusOk : ReplySink::kStatusDeclined, + ok ? &result : nullptr, ok ? sizeof(result) : 0); + return ok; + } + // Query migration follows the measured first blockers. case MGPWireOp::QueryCreate: case MGPWireOp::QueryBegin: case MGPWireOp::QueryEnd: diff --git a/MobileGL/MG_Remote/Wire/PipeWireCodec.h b/MobileGL/MG_Remote/Wire/PipeWireCodec.h index 5cb7b3c8..5690b59c 100644 --- a/MobileGL/MG_Remote/Wire/PipeWireCodec.h +++ b/MobileGL/MG_Remote/Wire/PipeWireCodec.h @@ -414,6 +414,11 @@ namespace MobileGL::MG_Remote::Wire { class WireVerbSink { public: virtual ~WireVerbSink() = default; + virtual Bool OnFenceCreate(const MG_Pipe::MGPHandleOnly&) { return false; } + virtual Bool OnFenceDestroy(const MG_Pipe::MGPHandleOnly&) { return false; } + virtual Bool OnFenceStatus(const MG_Pipe::MGPHandleOnly&, Uint32&) { return false; } + virtual Bool OnFenceWait(const MG_Pipe::MGPFenceWait&, Uint32&) { return false; } + virtual Bool OnFenceWaitServer(const MG_Pipe::MGPFenceWait&) { return false; } virtual Bool OnClear(const MG_Pipe::MGPClear& clear) { (void)clear; return false; diff --git a/MobileGL/MG_Test/Pipe/FieldOwnershipTest.cpp b/MobileGL/MG_Test/Pipe/FieldOwnershipTest.cpp index 039a3b10..90168c7c 100644 --- a/MobileGL/MG_Test/Pipe/FieldOwnershipTest.cpp +++ b/MobileGL/MG_Test/Pipe/FieldOwnershipTest.cpp @@ -313,7 +313,7 @@ TEST_F(FieldOwnershipTest, VerbBoundaryOpsCoverEveryVerbShapedCall) { EXPECT_EQ(MGPipeVerbForWireOp(MGPWireOp::SetStorageBlockBinding), MGPipeVerb::ShaderStorageBlockBinding); EXPECT_EQ(MGPipeVerbForWireOp(MGPWireOp::CopyFramebufferToTexture), MGPipeVerb::CopyTexImage2D); - EXPECT_EQ(kMGPipeVerbBoundaryOpCount, SizeT{18}); + EXPECT_EQ(kMGPipeVerbBoundaryOpCount, SizeT{23}); EXPECT_EQ(kMGPipeVerbBoundaryExemptCount, SizeT{3}); // Present is class B (it is emitted in P5) and is STILL not a verb boundary: diff --git a/MobileGL/MG_Test/Wire/PipeWireCodecTest.cpp b/MobileGL/MG_Test/Wire/PipeWireCodecTest.cpp index 10f24e25..07f135a4 100644 --- a/MobileGL/MG_Test/Wire/PipeWireCodecTest.cpp +++ b/MobileGL/MG_Test/Wire/PipeWireCodecTest.cpp @@ -36,6 +36,8 @@ // MG_Config::Transport and MG_Config::Ipc.AdoptTier: the two knobs R-6's tier gate reads. #include +#include +#include #include #include #include @@ -2636,3 +2638,140 @@ int main(int argc, char** argv) { fs::remove(path, ec); return rc; } + + +namespace { + struct FenceProbeBackend final : MG_Backend::DirectGLES::BackendObject_DirectGLES { + MG_Backend::GlobalBackendFunctionsTable Table{}; + const MG_Backend::GlobalBackendFunctionsTable& GetBackendFunctions() const override { return Table; } + }; + Uint32 fenceDeletes = 0; + Uint32 fenceServerWaits = 0; + Uint32 fenceFlags = 0; + Uint64 fenceTimeout = 0; + int nativeFenceToken = 0; + + void InstallFenceProbe(FenceProbeBackend& backend) { + fenceDeletes = fenceServerWaits = fenceFlags = 0; + fenceTimeout = 0; + backend.Table.GL.FenceSync = +[]() -> MG_Backend::BackendSyncHandle { return &nativeFenceToken; }; + backend.Table.GL.ClientWaitSync = +[](MG_Backend::BackendSyncHandle native, GLbitfield flags, + GLuint64 timeout) -> GLenum { + EXPECT_EQ(native, &nativeFenceToken); + fenceFlags = flags; + fenceTimeout = timeout; + return GL_TIMEOUT_EXPIRED; + }; + backend.Table.GL.GetSyncStatus = +[](MG_Backend::BackendSyncHandle native) -> Bool { + EXPECT_EQ(native, &nativeFenceToken); + return false; + }; + backend.Table.GL.WaitSync = +[](MG_Backend::BackendSyncHandle native, GLbitfield flags, GLuint64 timeout) { + EXPECT_EQ(native, &nativeFenceToken); + EXPECT_EQ(flags, 0u); + EXPECT_EQ(timeout, GL_TIMEOUT_IGNORED); + ++fenceServerWaits; + }; + backend.Table.GL.DeleteSync = +[](MG_Backend::BackendSyncHandle native) { + EXPECT_EQ(native, &nativeFenceToken); + ++fenceDeletes; + }; + } +} + +TEST(FenceWireRoundTrip, PreservesWaitFlagsTimeoutAnswersAndNativeLifetime) { + Wire2 wire; + FenceProbeBackend backend; + InstallFenceProbe(backend); + Server::ServerVerbSink sink; + sink.SetBackend(&backend); + wire.Decoder().SetVerbSink(&sink); + const MGPHandleOnly fence{{41, 0}, static_cast(MGPipeKind::Fence), 0}; + auto send = [&](MGPWireOp op, const auto& payload) { + const auto seq = wire.Encoder().EncodeRecord(op, &payload, sizeof(payload)); + EXPECT_NE(seq, kInvalidSeq); + bool applied = false; + EXPECT_TRUE(wire.PumpOne(&applied)); + EXPECT_TRUE(applied); + return seq; + }; + send(MGPWireOp::FenceCreate, fence); + EXPECT_TRUE(wire.Answers().All.empty()); + const MGPFenceWait wait{fence.Handle, 0x123456789ull, GL_SYNC_FLUSH_COMMANDS_BIT, 0}; + const auto waitSeq = send(MGPWireOp::FenceWait, wait); + ASSERT_EQ(wire.Answers().All.size(), 1u); + const auto& answer = wire.Answers().All.back(); + EXPECT_EQ(answer.Seq, waitSeq); + EXPECT_EQ(answer.Status, ReplySink::kStatusOk); + ASSERT_EQ(answer.Bytes.size(), sizeof(Uint32)); + Uint32 value = 0; + std::memcpy(&value, answer.Bytes.data(), sizeof(value)); + EXPECT_EQ(value, GL_TIMEOUT_EXPIRED); // a real timeout must never become signaled + EXPECT_EQ(fenceFlags, GL_SYNC_FLUSH_COMMANDS_BIT); + EXPECT_EQ(fenceTimeout, wait.TimeoutNs); + send(MGPWireOp::FenceStatus, fence); + std::memcpy(&value, wire.Answers().All.back().Bytes.data(), sizeof(value)); + EXPECT_EQ(value, 0u); + send(MGPWireOp::FenceWaitServer, MGPFenceWait{fence.Handle, GL_TIMEOUT_IGNORED, 0, 0}); + EXPECT_EQ(fenceServerWaits, 1u); + send(MGPWireOp::FenceDestroy, fence); + EXPECT_EQ(fenceDeletes, 1u); + const MGPHandleOnly replacement{{41, 1}, static_cast(MGPipeKind::Fence), 0}; + send(MGPWireOp::FenceCreate, replacement); + sink.SetBackend(nullptr); // orphan cleanup occurs before backend destruction + EXPECT_EQ(fenceDeletes, 2u); +} + +TEST(FenceWireRoundTrip, NullNativeFenceUsesOnlyTheExistingMonolithFallback) { + Wire2 wire; + FenceProbeBackend backend; + InstallFenceProbe(backend); + backend.Table.GL.FenceSync = +[]() -> MG_Backend::BackendSyncHandle { return nullptr; }; + Server::ServerVerbSink sink; + sink.SetBackend(&backend); + wire.Decoder().SetVerbSink(&sink); + const MGPHandleOnly fence{{1, 1}, static_cast(MGPipeKind::Fence), 0}; + ASSERT_TRUE(sink.OnFenceCreate(fence)); + Uint32 result = 0; + EXPECT_TRUE(sink.OnFenceWait({fence.Handle, 0, 0, 0}, result)); + EXPECT_EQ(result, GL_ALREADY_SIGNALED); + EXPECT_TRUE(sink.OnFenceStatus(fence, result)); + EXPECT_EQ(result, 1u); + EXPECT_TRUE(sink.OnFenceDestroy(fence)); + EXPECT_EQ(fenceDeletes, 0u); + sink.SetBackend(nullptr); +} + +#if MGTEST_HAVE_FORK +TEST(FenceWireRoundTrip, DestroyedAndRecycledWireHandlesNeverReachTheBackend) { + const auto r = RunInChild([] { + FenceProbeBackend backend; + InstallFenceProbe(backend); + Server::ServerVerbSink sink; + sink.SetBackend(&backend); + const MGPHandleOnly old{{9, 0}, static_cast(MGPipeKind::Fence), 0}; + sink.OnFenceCreate(old); + sink.OnFenceDestroy(old); + sink.OnFenceCreate({{9, 1}, static_cast(MGPipeKind::Fence), 0}); + Uint32 result = 0; + sink.OnFenceWait({old.Handle, 0, 0, 0}, result); + }); + ASSERT_TRUE(DiedOfAbort(r)); + EXPECT_NE(r.Log.find("Fence.handle"), std::string::npos) << r.Log; +} +#endif + + +TEST(FenceWireRoundTrip, MissingConsumerDeclinesWithoutInventingASignaledAnswer) { + Wire2 wire; + const MGPFenceWait wait{{3, 0}, 0, 0, 0}; + const auto seq = wire.Encoder().EncodeRecord(MGPWireOp::FenceWait, &wait, sizeof(wait)); + ASSERT_NE(seq, kInvalidSeq); + bool applied = true; + ASSERT_TRUE(wire.PumpOne(&applied)); + EXPECT_FALSE(applied); + ASSERT_EQ(wire.Answers().All.size(), 1u); + EXPECT_EQ(wire.Answers().All[0].Seq, seq); + EXPECT_EQ(wire.Answers().All[0].Status, ReplySink::kStatusDeclined); + EXPECT_TRUE(wire.Answers().All[0].Bytes.empty()); +} diff --git a/MobileGL/MG_Test/Wire/RemoteClientTest.cpp b/MobileGL/MG_Test/Wire/RemoteClientTest.cpp index 93b7d745..96a156d2 100644 --- a/MobileGL/MG_Test/Wire/RemoteClientTest.cpp +++ b/MobileGL/MG_Test/Wire/RemoteClientTest.cpp @@ -177,8 +177,8 @@ namespace { TEST(RemoteEmitTable, TheThreeClassesPartitionAllSeventyOneSlots) { // P5 baseline five + f1 eleven + i1 seven + t2 six emitted slots. EXPECT_EQ(LocallyAnsweredSlotCount(), 2u); - EXPECT_EQ(ImplementedVerbCount(), 49u); - EXPECT_EQ(UnmigratedSlotCount(), 20u); + EXPECT_EQ(ImplementedVerbCount(), 54u); + EXPECT_EQ(UnmigratedSlotCount(), 15u); EXPECT_EQ(LocallyAnsweredSlotCount() + ImplementedVerbCount() + UnmigratedSlotCount(), kRemoteEmitSlotCount); }