[Feat] (MG_Remote, P5b sync): migrate fence lifecycle and wait replies to the apply thread

This commit is contained in:
2026-09-16 13:46:49 -04:00
parent 56c6e49d1d
commit f018c0ef05
18 changed files with 464 additions and 31 deletions
+4 -5
View File
@@ -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();
}
@@ -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
@@ -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()
@@ -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 <GL/gl.h>
#include <GL/glcorearb.h>
#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
+6 -1
View File
@@ -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.
//
+3 -1
View File
@@ -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;
+1 -1
View File
@@ -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)
@@ -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.
+29
View File
@@ -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.
+89 -9
View File
@@ -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<MG_Backend::BackendSyncHandle, UniquePtr<RemoteFenceProxy>> 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<std::mutex> lock(g_fenceMutex);
BeforeReadOnlyVerb();
auto proxy = MakeUnique<RemoteFenceProxy>();
proxy->Handle = MG_Pipe::MGPipeSlots().Allocate(MG_Pipe::MGPipeKind::Fence);
const MG_Pipe::MGPHandleOnly desc{proxy->Handle, static_cast<Uint32>(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<std::mutex> 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<std::mutex> lock(g_fenceMutex);
const MG_Pipe::MGPHandleOnly desc{FenceHandle(proxy), static_cast<Uint32>(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<std::mutex> 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<std::mutex> 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<Uint32>(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<MG_State::GLState::ITextureObject>&, 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;
+2 -5
View File
@@ -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
+94 -1
View File
@@ -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<Uint32>(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<Uint32>(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<Uint32>(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<Uint32>(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;
+13
View File
@@ -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<Uint32, FenceEntry> m_fences;
MG_Backend::BackendObject* m_backend = nullptr;
Uint64 m_clears = 0;
Uint64 m_draws = 0;
+15 -3
View File
@@ -1537,12 +1537,24 @@ namespace MobileGL::MG_Remote::Wire {
MGPipeApplyUnmapPersistent(*static_cast<const MGPHandleOnly*>(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<const MGPHandleOnly*>(payload));
case MGPWireOp::FenceDestroy:
return m_verbs != nullptr && m_verbs->OnFenceDestroy(*static_cast<const MGPHandleOnly*>(payload));
case MGPWireOp::FenceWaitServer:
return m_verbs != nullptr && m_verbs->OnFenceWaitServer(*static_cast<const MGPFenceWait*>(payload));
case MGPWireOp::FenceStatus:
case MGPWireOp::FenceWait: {
Uint32 result = 0;
const Bool ok = m_verbs != nullptr &&
(op == MGPWireOp::FenceStatus
? m_verbs->OnFenceStatus(*static_cast<const MGPHandleOnly*>(payload), result)
: m_verbs->OnFenceWait(*static_cast<const MGPFenceWait*>(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:
+5
View File
@@ -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;
+1 -1
View File
@@ -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:
+139
View File
@@ -36,6 +36,8 @@
// MG_Config::Transport and MG_Config::Ipc.AdoptTier: the two knobs R-6's tier gate reads.
#include <Config.h>
#include <MG_Remote/Server/PipeApplier.h>
#include <MG_Backend/DirectGLES/BackendObject_DirectGLES.h>
#include <MG_Pipe/MGPipe.h>
#include <MG_Pipe/MGPipeRenderStateSpans.h>
#include <MG_Pipe/PipeApply.h>
@@ -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<Uint32>(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<Uint32>(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<Uint32>(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<Uint32>(MGPipeKind::Fence), 0};
sink.OnFenceCreate(old);
sink.OnFenceDestroy(old);
sink.OnFenceCreate({{9, 1}, static_cast<Uint32>(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());
}
+2 -2
View File
@@ -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);
}