mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-17 16:48:31 +09:00
[Feat] (MG_Remote, P5b sync): migrate fence lifecycle and wait replies to the apply thread
This commit is contained in:
@@ -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
|
||||
@@ -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.
|
||||
//
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user