[MG_Remote] (Disaggregated): carry native query results and complete XFB object deletion

This commit is contained in:
2026-09-20 11:52:13 -04:00
parent c0b40ff508
commit 70df57bb23
21 changed files with 686 additions and 71 deletions
@@ -1422,7 +1422,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return false;
}
if (query->kind == VulkanTimerQuery::Kind::XfbGenerated &&
!query->pausedPrimitivesCountedByGpu && MGB_CTX_LIVE) {
!query->pausedPrimitivesCountedByGpu &&
#if MOBILEGL_BUILD_DISAGGREGATED
MG_Config::Transport == MG_Config::TransportMode::Monolith &&
#endif
MGB_CTX_LIVE) {
primitives += MGB_CTX->GetTransformFeedbackPausedPrimitiveCounter() -
query->pausedPrimitiveSnapshot;
}
@@ -1470,6 +1474,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
query->kind = generated ? VulkanTimerQuery::Kind::XfbGenerated : VulkanTimerQuery::Kind::XfbWritten;
query->rendererGeneration = GetRendererGeneration();
query->pausedPrimitiveSnapshot =
#if MOBILEGL_BUILD_DISAGGREGATED
MG_Config::Transport == MG_Config::TransportMode::Monolith &&
#endif
MGB_CTX_LIVE ? MGB_CTX->GetTransformFeedbackPausedPrimitiveCounter() : 0;
// Read AFTER StartXfbQueryCapture, which is where a failed reroute-pool creation
// disarms: the answer is then what this span will actually do for every draw.
+16
View File
@@ -187,6 +187,22 @@ namespace MobileGL::MG_Backend {
serverBackend->GetBackendFunctions().GL.EndTransformFeedback != nullptr) {
capBits |= MG_Pipe::kCapBackendOwnsXfbCapture;
}
if (const auto* backend = loop.Backend()) {
const auto& gl = backend->GetBackendFunctions().GL;
// The query owner publishes only complete native query/reply paths.
// Timer hardware support is refreshed when caps are published after
// make-current; no CPU primitive-accounting preference is advertised.
if (gl.IsQueryResultAvailable && gl.GetQueryResult64 && gl.DeleteBackendQuery &&
gl.FenceSync && gl.ClientWaitSync && gl.DeleteSync) {
if (gl.BeginXfbPrimitivesQuery && gl.EndXfbPrimitivesQuery)
capBits |= MG_Pipe::kCapXfbPrimitivesQuery;
if (gl.BeginOcclusionQuery && gl.EndOcclusionQuery)
capBits |= MG_Pipe::kCapOcclusionQuery;
if (gl.IsTimerQuerySupported && gl.BeginTimeElapsedQuery &&
gl.EndTimeElapsedQuery && gl.QueryCounterTimestamp)
capBits |= MG_Pipe::kCapTimerQuery;
}
}
// Each backend has an independent implementation-readiness gate.
// The runtime RunAhead knob can decline the feature, never create it.
const Bool runAheadReady = MG_Config::ActiveBackendType == BackendType::DirectVulkan
+10 -1
View File
@@ -154,7 +154,16 @@
X(FenceStatus, GetSyncStatus) \
X(FenceWait, ClientWaitSync) \
X(FenceDestroy, DeleteSync) \
X(FenceWaitServer, WaitSync)
X(FenceWaitServer, WaitSync) \
X(QueryCreate, BeginXfbPrimitivesQuery) \
X(QueryBegin, BeginXfbPrimitivesQuery) \
X(QueryEnd, EndXfbPrimitivesQuery) \
X(QueryAvailable, IsQueryResultAvailable) \
X(QueryResult, GetQueryResult64) \
X(QueryDestroy, DeleteBackendQuery) \
X(QueryCounter, QueryCounterTimestamp) \
X(QueryTimestamp, GetGpuTimestampNs) \
X(DeleteStreamOutput, DeleteTransformFeedback)
// X(Op, Why) - verb-shaped calls that are deliberately NOT stamp points.
//
+4 -2
View File
@@ -142,7 +142,7 @@
// sat catalogued-and-dead since P4a. 80 unique records.
// ---------------------------------------------------------------------------------------
#define MGP_CALL_LIST_DOCUMENTED_COUNT 80
#define MGP_CALL_LIST_DOCUMENTED_COUNT 81
// clang-format off
#define MGP_CALL_LIST(X) \
@@ -337,7 +337,9 @@
/* ---- the same validate point: a rebuild inside the verb must already see the bindings. ---- */ \
/* ---- kWaitNone: nothing about it is read back, and under run-ahead a kCtxState row that ---- */ \
/* ---- names only handles and values is exactly what rule F admits. ---- */ \
X(SetProgramBindings, MGPProgramBindings, kCtxState, kVarTail|kHostSpan, kWaitNone)
X(SetProgramBindings, MGPProgramBindings, kCtxState, kVarTail|kHostSpan, kWaitNone) \
/* Deletion carries the GL argument and its never-reused capture identity. */ \
X(DeleteStreamOutput, MGPStreamOutputBind, kCtxVerb, kNone, kWaitNone)
// clang-format on
// Explicitly NOT migrated (plan 4.4.6 / appendix A "explicit deletions"):
@@ -292,12 +292,21 @@ constexpr MGPipeVerb MGPipeVerbForWireOp(MGPWireOp op) {
case MGPWireOp::FenceWait: return MGPipeVerb::ClientWaitSync;
case MGPWireOp::FenceDestroy: return MGPipeVerb::DeleteSync;
case MGPWireOp::FenceWaitServer: return MGPipeVerb::WaitSync;
case MGPWireOp::QueryCreate: return MGPipeVerb::BeginXfbPrimitivesQuery;
case MGPWireOp::QueryBegin: return MGPipeVerb::BeginXfbPrimitivesQuery;
case MGPWireOp::QueryEnd: return MGPipeVerb::EndXfbPrimitivesQuery;
case MGPWireOp::QueryAvailable: return MGPipeVerb::IsQueryResultAvailable;
case MGPWireOp::QueryResult: return MGPipeVerb::GetQueryResult64;
case MGPWireOp::QueryDestroy: return MGPipeVerb::DeleteBackendQuery;
case MGPWireOp::QueryCounter: return MGPipeVerb::QueryCounterTimestamp;
case MGPWireOp::QueryTimestamp: return MGPipeVerb::GetGpuTimestampNs;
case MGPWireOp::DeleteStreamOutput: return MGPipeVerb::DeleteTransformFeedback;
default:
return MGPipeVerb::kVerbCount;
}
}
inline constexpr SizeT kMGPipeVerbBoundaryOpCount = 23;
inline constexpr SizeT kMGPipeVerbBoundaryOpCount = 32;
inline constexpr SizeT kMGPipeVerbBoundaryExemptCount = 3;
// ---- THE §7 ALLOWLIST, DERIVED RATHER THAN WRITTEN (ID-116) --------------------------
+4 -3
View File
@@ -29,7 +29,7 @@ struct MGPipeScreen {
void (*ApplierReset)(const MGPApplierReset* payload);
};
// context: 68 calls. A null entry means the backend does not implement this
// context: 69 calls. A null entry means the backend does not implement this
// call and the frontend keeps its own path (plan B section 4.1).
struct MGPipeContext {
void (*QueryCreate)(const MGPQueryDesc* payload);
@@ -100,11 +100,12 @@ struct MGPipeContext {
void (*ObjectDeath)(const MGPHandleOnly* payload);
void (*SetContextValues)(const MGPContextValues* payload);
void (*SetProgramBindings)(const MGPProgramBindings* payload, const void* varTail, Uint32 varTailCount);
void (*DeleteStreamOutput)(const MGPStreamOutputBind* payload);
};
inline constexpr SizeT kMGPipeScreenCallCount = 12;
inline constexpr SizeT kMGPipeContextCallCount = 68;
inline constexpr SizeT kMGPipeCallCount = 80;
inline constexpr SizeT kMGPipeContextCallCount = 69;
inline constexpr SizeT kMGPipeCallCount = 81;
// A table that is not exactly its call count of function pointers has grown a
// member that no generator knows about.
@@ -336,3 +336,7 @@ inline void MGP_SetContextValues(const MGPContextValues* payload) {
inline void MGP_SetProgramBindings(const MGPProgramBindings* payload, const void* varTail, Uint32 varTailCount) {
gMGPipeContext.SetProgramBindings(payload, varTail, varTailCount);
}
inline void MGP_DeleteStreamOutput(const MGPStreamOutputBind* payload) {
gMGPipeContext.DeleteStreamOutput(payload);
}
+16 -1
View File
@@ -150,7 +150,8 @@ enum class MGPWireOp : Uint16 {
ObjectDeath = 78,
SetContextValues = 79,
SetProgramBindings = 80,
kOpCount = 81,
DeleteStreamOutput = 81,
kOpCount = 82,
};
// THE FLAGS, EXPORTED ONCE, INDEXED BY OPCODE (P5 R-13.4). MGPWireRecHeader::Flags is
@@ -252,6 +253,7 @@ inline constexpr Uint32 kMGPipeCallFlags[static_cast<SizeT>(MGPWireOp::kOpCount)
/* 78 ObjectDeath */ static_cast<Uint32>(kNone),
/* 79 SetContextValues */ static_cast<Uint32>(kNone),
/* 80 SetProgramBindings */ static_cast<Uint32>(kVarTail | kHostSpan),
/* 81 DeleteStreamOutput */ static_cast<Uint32>(kNone),
};
static_assert(sizeof(kMGPipeCallFlags) / sizeof(kMGPipeCallFlags[0]) ==
static_cast<SizeT>(MGPWireOp::kOpCount),
@@ -352,6 +354,7 @@ inline constexpr Uint8 kMGPipeCallClasses[static_cast<SizeT>(MGPWireOp::kOpCount
/* 78 ObjectDeath */ static_cast<Uint8>(kCtxObject),
/* 79 SetContextValues */ static_cast<Uint8>(kCtxState),
/* 80 SetProgramBindings */ static_cast<Uint8>(kCtxState),
/* 81 DeleteStreamOutput */ static_cast<Uint8>(kCtxVerb),
};
static_assert(sizeof(kMGPipeCallClasses) / sizeof(kMGPipeCallClasses[0]) ==
static_cast<SizeT>(MGPWireOp::kOpCount),
@@ -456,6 +459,7 @@ inline constexpr Uint8 kMGPipeWaitClasses[static_cast<SizeT>(MGPWireOp::kOpCount
/* 78 ObjectDeath */ static_cast<Uint8>(kWaitNone),
/* 79 SetContextValues */ static_cast<Uint8>(kWaitNone),
/* 80 SetProgramBindings */ static_cast<Uint8>(kWaitNone),
/* 81 DeleteStreamOutput */ static_cast<Uint8>(kWaitNone),
};
static_assert(sizeof(kMGPipeWaitClasses) / sizeof(kMGPipeWaitClasses[0]) ==
static_cast<SizeT>(MGPWireOp::kOpCount),
@@ -1162,6 +1166,14 @@ static_assert(sizeof(MGPWireRec_SetProgramBindings) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPProgramBindings) + 7u) & ~SizeT(7u)),
"MGPWireRec_SetProgramBindings gained padding; the wire format moved");
struct alignas(8) MGPWireRec_DeleteStreamOutput {
MGPWireRecHeader Header;
MGPStreamOutputBind Payload;
};
static_assert(sizeof(MGPWireRec_DeleteStreamOutput) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPStreamOutputBind) + 7u) & ~SizeT(7u)),
"MGPWireRec_DeleteStreamOutput gained padding; the wire format moved");
[[noreturn]] inline void MGPipeWireProtocolFatal(const char* call, Uint64 size, Uint64 remaining) {
MGLOG_F("MGPipe: protocol corruption applying %s: size=%llu remaining=%llu", call,
static_cast<unsigned long long>(size), static_cast<unsigned long long>(remaining));
@@ -1451,6 +1463,9 @@ inline Bool MGPipeApplyWireRecord(MGPWireOp op, const void* record, Uint64 size,
case MGPWireOp::SetProgramBindings:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_SetProgramBindings, "SetProgramBindings");
break;
case MGPWireOp::DeleteStreamOutput:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_DeleteStreamOutput, "DeleteStreamOutput");
break;
case MGPWireOp::kInvalid:
case MGPWireOp::kOpCount:
default:
+45 -30
View File
@@ -34,6 +34,7 @@
#include "PersistentMapTracker.h"
#include <MG_Util/Converters/GLToMG/TextureEnumConverter.h>
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
#include <MG_Util/Debug/Log.h>
#include <MG_Util/Metrics/PipeStats.h>
#include <MG_Util/Metrics/TextureMetrics.h>
@@ -915,11 +916,9 @@ namespace MobileGL::MG_Remote::Client {
// split form - the server writes the reply into the buffer resource and the client
// marks it GPU-written (b1's MarkReadPixelsPackBuffer becoming the producer contract
// §3 names) - is a P6 ROADMAP item. In P5 it is class C's shape (R-4), refused here.
if (MG_State::pGLContext != nullptr &&
MG_State::pGLContext->GetBufferBindingSlot(::MobileGL::BufferTarget::PixelPack)
.GetBoundObject()) {
UnmigratedVerbFatal("ReadPixels+PACK_BUFFER");
}
const auto pbo = MG_State::pGLContext != nullptr
? MG_State::pGLContext->GetBufferBindingSlot(::MobileGL::BufferTarget::PixelPack).GetBoundObject()
: SharedPtr<MG_State::GLState::BufferObject>{};
BeforeReadOnlyVerb();
@@ -972,7 +971,7 @@ namespace MobileGL::MG_Remote::Client {
Int32 status = 0;
Uint64 replySize = 0;
if (ReadbackPackStateIsTight(width, bytesPerPixel, pack)) {
if (!pbo && ReadbackPackStateIsTight(width, bytesPerPixel, pack)) {
// THE COMMON CASE, AND IT KEEPS THE ZERO-COPY. A neutral pack state means the
// destination layout IS the tight layout, so the reply lands straight in the
// application's buffer and there is no bounce at all. It is a fast path for the
@@ -998,10 +997,25 @@ namespace MobileGL::MG_Remote::Client {
// pointer at all (the bounce is the only thing that held the partial bytes).
RequireReadbackReplyComplete(status, replySize, tight);
ApplyReadbackByteSwap(bounce.data(), tight, type, pack);
if (pbo) {
// pixels is an offset, never a host pointer. Upload only the
// requested rows so PACK padding and untouched bytes survive.
const Uint64 alignment = std::max<Int>(pack.Alignment, 1);
const Uint64 rowPixels = pack.RowLength > 0 ? pack.RowLength : width;
const Uint64 stride = (rowPixels * bytesPerPixel + alignment - 1) / alignment * alignment;
const Uint64 start = reinterpret_cast<SizeT>(pixels) + pack.SkipRows * stride +
pack.SkipPixels * bytesPerPixel;
const SizeT rowBytes = static_cast<SizeT>(width) * bytesPerPixel;
for (Int y = 0; y < height; ++y)
pbo->UploadSubData({bounce.data() + y * rowBytes, rowBytes}, start + y * stride);
return;
}
ScatterTightReadbackIntoPackState(bounce.data(), pixels, width, height, bytesPerPixel,
pack);
}
#include "TextureReadbackEmit.inc"
void EmitPresent() {
ClientSession& session = RequireSession("Present");
BeforeReadOnlyVerb();
@@ -1565,7 +1579,9 @@ namespace MobileGL::MG_Remote::Client {
g_fenceProxies.erase(proxy);
}
// CLASS C - 16 slots remain after d1/i1/t2/f1; each names its first blocker.
#include "QueryEmit.inc"
// CLASS C - each remaining slot names its first blocker.
// =============================================================================
//
// PARTITIONED BY THE P5b PACKAGE THAT OWNS THE FLIP (MG_Remote/CONTRACT-P5B.md,
@@ -1614,35 +1630,19 @@ namespace MobileGL::MG_Remote::Client {
// not by omission (unmeasured; the driver object leaks on the server until P9's XFB
// namespace work, and a bind of name 0 is what the backend does on delete of the bound
// one, DirectGLES.cpp:1422). It therefore still aborts by its own name.
#define MGR_UNMIGRATED_T2_SLOTS(X) \
X(DeleteTransformFeedback, void, (GLuint))
#define MGR_UNMIGRATED_T2_SLOTS(X)
#define MGR_UNMIGRATED_F1_SLOTS(X)
// The wave-3 tail. SetSwapInterval is hand-written below (it is not a GL.* slot).
#define MGR_UNMIGRATED_TAIL_SLOTS(X) \
X(GetTexImage, void, (GLenum, GLint, GLenum, GLenum, GLvoid*)) \
X(GetTextureImage, void, \
(const SharedPtr<MG_State::GLState::ITextureObject>&, TextureUploadTarget, GLint, GLenum, \
GLenum, GLsizei, GLvoid*)) \
X(EndTimeElapsedQuery, void, (MG_Backend::BackendQueryHandle)) \
X(DeleteBackendQuery, void, (MG_Backend::BackendQueryHandle)) \
X(EndOcclusionQuery, void, (MG_Backend::BackendQueryHandle)) \
X(EndXfbPrimitivesQuery, void, (MG_Backend::BackendQueryHandle))
#define MGR_UNMIGRATED_TAIL_SLOTS(X)
// The non-void ones, kept apart only because the macro body differs: a [[noreturn]]
// call is a complete body for a void slot and for a value-returning one alike, but a
// compiler that does not see UnmigratedVerbFatal's attribute through the macro would
// 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(BeginTimeElapsedQuery, MG_Backend::BackendQueryHandle, ()) \
X(QueryCounterTimestamp, MG_Backend::BackendQueryHandle, ()) \
X(IsQueryResultAvailable, Bool, (MG_Backend::BackendQueryHandle)) \
X(GetQueryResult64, Bool, (MG_Backend::BackendQueryHandle, Bool, Uint64*)) \
X(BeginOcclusionQuery, MG_Backend::BackendQueryHandle, ()) \
X(BeginXfbPrimitivesQuery, MG_Backend::BackendQueryHandle, (Bool)) \
X(GetGpuTimestampNs, Int64, ())
#define MGR_UNMIGRATED_TAIL_VALUE_SLOTS(X)
// The union, for the places that want every class-C row at once (the definitions and
// the assignments). A package never edits THIS; it edits its own list above.
@@ -1686,13 +1686,14 @@ namespace MobileGL::MG_Remote::Client {
constexpr Uint32 kEmittedSlotsP5 = 5; // Clear, DrawArrays, ReadPixels, Blit, Present
constexpr Uint32 kEmittedSlotsD1 = 19;
constexpr Uint32 kEmittedSlotsI1 = 7;
constexpr Uint32 kEmittedSlotsT2 = 6;
constexpr Uint32 kEmittedSlotsT2 = 7;
constexpr Uint32 kEmittedSlotsF1 = 11;
constexpr Uint32 kEmittedSlotsTail = 1; // BlitNamedFramebuffer
constexpr Uint32 kEmittedSlotsTail = 3; // BlitNamedFramebuffer, both texture readbacks
constexpr Uint32 kEmittedSlotsSync = 5;
constexpr Uint32 kEmittedSlotsQueries = 11;
constexpr Uint32 kEmittedSlots =
kEmittedSlotsP5 + kEmittedSlotsD1 + kEmittedSlotsI1 + kEmittedSlotsT2 + kEmittedSlotsF1 +
kEmittedSlotsTail + kEmittedSlotsSync;
kEmittedSlotsTail + kEmittedSlotsSync + kEmittedSlotsQueries;
constexpr Uint32 kLocallyAnsweredSlots = 2; // GetIntegeri_v, IsTimerQuerySupported
// EACH PACKAGE'S OWNERSHIP, PINNED. A package that flips a slot removes one row and
@@ -1703,7 +1704,7 @@ 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 + kEmittedSlotsSync == 20,
static_assert(kUnmigratedTail + kEmittedSlotsTail + kEmittedSlotsSync + kEmittedSlotsQueries == 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,
@@ -1729,6 +1730,18 @@ namespace MobileGL::MG_Remote::Client {
table.GL.GetSyncStatus = &EmitGetSyncStatus;
table.GL.WaitSync = &EmitWaitSync;
table.GL.DeleteSync = &EmitDeleteSync;
table.GL.BeginTimeElapsedQuery = &EmitBeginTimeElapsedQuery;
table.GL.EndTimeElapsedQuery = &EmitEndQuery;
table.GL.QueryCounterTimestamp = &EmitQueryCounterTimestamp;
table.GL.BeginOcclusionQuery = &EmitBeginOcclusionQuery;
table.GL.EndOcclusionQuery = &EmitEndQuery;
table.GL.BeginXfbPrimitivesQuery = &EmitBeginXfbPrimitivesQuery;
table.GL.EndXfbPrimitivesQuery = &EmitEndQuery;
table.GL.IsQueryResultAvailable = &EmitIsQueryResultAvailable;
table.GL.GetQueryResult64 = &EmitGetQueryResult64;
table.GL.DeleteBackendQuery = &EmitDeleteBackendQuery;
table.GL.GetGpuTimestampNs = &EmitGetGpuTimestampNs;
table.GL.DeleteTransformFeedback = &EmitDeleteTransformFeedback;
// ---- class A
table.GL.GetIntegeri_v = &AnswerGetIntegeri_v;
@@ -1791,6 +1804,8 @@ namespace MobileGL::MG_Remote::Client {
table.GL.CopyTexImage2D = &EmitCopyTexImage2D;
table.GL.CopyTexSubImage2D = &EmitCopyTexSubImage2D;
table.GL.GenerateMipmap = &EmitGenerateMipmap;
table.GL.GetTexImage = &EmitGetTexImage;
table.GL.GetTextureImage = &EmitGetTextureImage;
// ---- class B, P5b package i1 (kEmittedSlotsI1 = 7)
table.GL.BindImageTexture = &EmitBindImageTexture;
+104
View File
@@ -0,0 +1,104 @@
// Opaque local proxies carry only {slot, generation, target} across the wire.
struct RemoteQueryProxy { MG_Pipe::MGPQueryDesc Desc{}; };
std::mutex g_queryProxyMutex;
UnorderedMap<MG_Backend::BackendQueryHandle, UniquePtr<RemoteQueryProxy>> g_queryProxies;
const MG_Pipe::MGPQueryDesc& QueryDesc(MG_Backend::BackendQueryHandle handle) {
const auto found = g_queryProxies.find(handle);
if (found == g_queryProxies.end())
Wire::WireProtocolFatal("Query.proxy", "unknown local query proxy");
return found->second->Desc;
}
template <typename Result>
Result ReadQueryReply(ClientSession& session, MG_Pipe::MGPWireOp op, const void* request, Uint64 size) {
Result result{};
Int32 status = Wire::ReplySink::kStatusError;
Uint64 returned = 0;
session.EmitAndWait(op, request, size, nullptr, 0, &result, sizeof(result), &status, &returned);
if (status != Wire::ReplySink::kStatusOk || returned != sizeof(result))
Wire::WireProtocolFatal("Query.reply", "missing or malformed backend query reply");
return result;
}
MG_Backend::BackendQueryHandle EmitCreateQuery(GLenum kind, Bool counter) {
auto& session = RequireSession("QueryBegin");
const std::lock_guard<std::mutex> lock(g_queryProxyMutex);
BeforeReadOnlyVerb();
auto proxy = MakeUnique<RemoteQueryProxy>();
proxy->Desc.Query = MG_Pipe::MGPipeSlots().Allocate(MG_Pipe::MGPipeKind::Query);
proxy->Desc.Kind = kind;
session.EmitAndWait(MG_Pipe::MGPWireOp::QueryCreate, &proxy->Desc, sizeof(proxy->Desc),
nullptr, 0, nullptr, 0, nullptr);
session.EmitAndWait(counter ? MG_Pipe::MGPWireOp::QueryCounter : MG_Pipe::MGPWireOp::QueryBegin,
&proxy->Desc, sizeof(proxy->Desc), nullptr, 0, nullptr, 0, nullptr);
auto* local = proxy.get();
g_queryProxies.emplace(local, Move(proxy));
return local;
}
MG_Backend::BackendQueryHandle EmitBeginTimeElapsedQuery() { return EmitCreateQuery(GL_TIME_ELAPSED, false); }
MG_Backend::BackendQueryHandle EmitQueryCounterTimestamp() { return EmitCreateQuery(GL_TIMESTAMP, true); }
MG_Backend::BackendQueryHandle EmitBeginOcclusionQuery() { return EmitCreateQuery(GL_SAMPLES_PASSED, false); }
MG_Backend::BackendQueryHandle EmitBeginXfbPrimitivesQuery(Bool generated) {
return EmitCreateQuery(generated ? GL_PRIMITIVES_GENERATED : GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN, false);
}
void EmitEndQuery(MG_Backend::BackendQueryHandle handle) {
auto& session = RequireSession("QueryEnd");
const std::lock_guard<std::mutex> lock(g_queryProxyMutex);
const auto& desc = QueryDesc(handle);
BeforeReadOnlyVerb();
session.EmitAndWait(MG_Pipe::MGPWireOp::QueryEnd, &desc, sizeof(desc), nullptr, 0, nullptr, 0, nullptr);
}
Bool EmitIsQueryResultAvailable(MG_Backend::BackendQueryHandle handle) {
auto& session = RequireSession("IsQueryResultAvailable");
const std::lock_guard<std::mutex> lock(g_queryProxyMutex);
const MG_Pipe::MGPHandleOnly request{QueryDesc(handle).Query, static_cast<Uint32>(MG_Pipe::MGPipeKind::Query), 0};
const auto result = ReadQueryReply<Uint32>(session, MG_Pipe::MGPWireOp::QueryAvailable, &request, sizeof(request));
if (result > 1) Wire::WireProtocolFatal("QueryAvailable.reply", "availability is not boolean");
return result != 0;
}
Bool EmitGetQueryResult64(MG_Backend::BackendQueryHandle handle, Bool wait, Uint64* value) {
auto& session = RequireSession("GetQueryResult64");
const std::lock_guard<std::mutex> lock(g_queryProxyMutex);
MG_Pipe::MGPQueryResultRequest request{};
request.Query = QueryDesc(handle).Query;
request.Wait = wait ? 1 : 0;
const auto reply = ReadQueryReply<Wire::QueryResultReply>(session, MG_Pipe::MGPWireOp::QueryResult,
&request, sizeof(request));
if (reply.Produced > 1 || reply.Reserved)
Wire::WireProtocolFatal("QueryResult.reply", "invalid result metadata");
if (!reply.Produced) return false;
if (value) *value = reply.Value;
return true;
}
void EmitDeleteBackendQuery(MG_Backend::BackendQueryHandle handle) {
const std::lock_guard<std::mutex> lock(g_queryProxyMutex);
const auto query = QueryDesc(handle).Query;
if (auto* session = ClientSession::Active(); session && session->Started()) {
const MG_Pipe::MGPHandleOnly request{query, static_cast<Uint32>(MG_Pipe::MGPipeKind::Query), 0};
session->EmitAndWait(MG_Pipe::MGPWireOp::QueryDestroy, &request, sizeof(request),
nullptr, 0, nullptr, 0, nullptr);
}
MG_Pipe::MGPipeSlots().Free(MG_Pipe::MGPipeKind::Query, query);
g_queryProxies.erase(handle);
}
Int64 EmitGetGpuTimestampNs() {
auto& session = RequireSession("GetGpuTimestampNs");
BeforeReadOnlyVerb();
const MG_Pipe::MGPTimestampRequest request{};
return ReadQueryReply<Int64>(session, MG_Pipe::MGPWireOp::QueryTimestamp, &request, sizeof(request));
}
void EmitDeleteTransformFeedback(GLuint name) {
auto& session = RequireSession("DeleteTransformFeedback");
BeforeReadOnlyVerb();
const auto& context = *MG_State::pGLContext;
const MG_Pipe::MGPStreamOutputBind object{name, 0, context.GetTransformFeedbackLifetimeId(name)};
session.EmitAndWait(MG_Pipe::MGPWireOp::DeleteStreamOutput, &object, sizeof(object),
nullptr, 0, nullptr, 0, nullptr);
if (context.GetBoundTransformFeedbackName() == name) {
const MG_Pipe::MGPStreamOutputBind fallback{0, 0, context.GetTransformFeedbackLifetimeId(0)};
session.EmitAndWait(MG_Pipe::MGPWireOp::BindStreamOutput, &fallback, sizeof(fallback),
nullptr, 0, nullptr, 0, nullptr);
}
}
+72 -12
View File
@@ -19,6 +19,8 @@
// §5.2). The same dependency ServerLoop.cpp already takes for CreateBackend; a server built
// on Magma simply holds no twins in these tables and every release resolves to nothing.
#include <MG_Backend/DirectGLES/Managers.h>
#include <MG_Backend/DirectGLES/DirectGLES.h>
#include <MG_Backend/DirectVulkan/DirectVulkan.h>
#include <MG_Remote/Client/ClientSession.h>
#include <MG_Pipe/PipeApply.h>
#include <MG_Util/Converters/GLToMG/TextureEnumConverter.h>
@@ -72,7 +74,10 @@ namespace MobileGL::MG_Remote::Server {
// -----------------------------------------------------------------------------------
void ServerVerbSink::SetBackend(MG_Backend::BackendObject* backend) {
if (m_backend != backend) ReleaseFences();
if (m_backend != backend) {
ReleaseQueries();
ReleaseFences();
}
m_backend = backend;
}
@@ -182,20 +187,27 @@ namespace MobileGL::MG_Remote::Server {
m_fences.clear();
}
#include "QueryServer.inc"
Bool ServerVerbSink::OnClear(const MG_Pipe::MGPClear& clear) {
const MG_Backend::GlobalBackendFunctionsTable* table = Table("clear");
if (table == nullptr) return false;
const MG_Backend::GLFunctionsTable& gl = table->GL;
// Named records precede the verb; bound-form backends re-sync the live draw binding.
if (!MG_Pipe::MGPipeHandleIsNull(clear.Fbo) &&
clear.Fbo != MG_Pipe::MGPipeApplier().BoundFramebuffer[0]) {
const char* slot = clear.Kind == kMGPClearKindDepthStencil ? "ClearNamedFramebufferfi+UNBOUND" :
clear.ValueClass == kMGPClearValueClassInt ? "ClearNamedFramebufferiv+UNBOUND" :
clear.ValueClass == kMGPClearValueClassUint ? "ClearNamedFramebufferuiv+UNBOUND" :
"ClearNamedFramebufferfv+UNBOUND";
MGLOG_F("MGPipe: Fatal{UnmigratedVerb, \"%s\"}", slot);
std::abort();
// The named framebuffer record already crossed before this verb. Scope
// only the server's resolved draw target; the next verb still observes
// the application's unchanged binding. Both backends sync from this
// handle and never need the frontend FramebufferObject.
auto& applier = MG_Pipe::MGPipeApplier();
struct ScopedClearTarget {
MG_Pipe::MGPipeApplierState& State;
MG_Pipe::MGPipeHandle Saved;
~ScopedClearTarget() { State.BoundFramebuffer[0] = Saved; }
} clearTarget{applier, applier.BoundFramebuffer[0]};
if (!MG_Pipe::MGPipeHandleIsNull(clear.Fbo)) {
if (!applier.FramebufferRecordFor(clear.Fbo))
Wire::WireProtocolFatal("Clear.Fbo", "missing named framebuffer record");
applier.BoundFramebuffer[0] = clear.Fbo;
}
switch (clear.Kind) {
case kMGPClearKindWhole:
@@ -332,6 +344,41 @@ namespace MobileGL::MG_Remote::Server {
return true;
}
Bool ServerVerbSink::OnGetTextureImage(const MG_Pipe::MGPReadbackInfo& info, Uint64 seq,
Wire::ReplySink* replies) {
if (!replies) Wire::WireProtocolFatal("GetTextureImage.reply", "missing reply sink");
const SizeT bpp = MG_Util::GetInputBytesPerPixel(
MG_Util::ConvertGLEnumToTextureInputFormat(info.Format),
MG_Util::ConvertGLEnumToTexturePixelDataType(info.Type));
if (!bpp || !info.Box.W || !info.Box.H || !info.Box.D || info.Box.X || info.Box.Y || info.Box.Z) {
Wire::WireProtocolFatal("GetTextureImage.extent", "invalid tight image extent");
}
const Uint64 tight = static_cast<Uint64>(info.Box.W) * info.Box.H * info.Box.D * bpp;
if (tight / bpp / info.Box.W / info.Box.H != info.Box.D ||
info.DstOffset > tight || !info.DstSize || info.DstSize > tight - info.DstOffset)
Wire::WireProtocolFatal("GetTextureImage.range", "invalid reply window");
auto image = info;
image.DstOffset = 0;
image.DstSize = tight;
Vector<Uint8> bytes;
Bool ok = false;
#if MOBILEGL_BUILD_DISAGGREGATED
if (m_backend && m_backend->GetBackendType() == BackendType::DirectGLES)
ok = MG_Backend::DirectGLES::ReadTextureImageWire(image, bytes);
else if (m_backend && m_backend->GetBackendType() == BackendType::DirectVulkan &&
MG_Backend::DirectVulkan::pVulkanRenderer)
ok = MG_Backend::DirectVulkan::pVulkanRenderer->ReadTextureImageWire(image, bytes);
#endif
if (!ok || bytes.size() != tight) {
replies->PostReply(seq, Wire::ReplySink::kStatusError, nullptr, 0);
return false;
}
replies->PostReply(seq, Wire::ReplySink::kStatusOk, bytes.data() + info.DstOffset, info.DstSize);
m_readbackBytes += info.DstSize;
++m_readbacks;
return true;
}
Bool ServerVerbSink::OnReadPixels(const MG_Pipe::MGPReadbackInfo& info, Uint64 seq,
Wire::ReplySink* replies) {
if (replies == nullptr) {
@@ -1008,7 +1055,6 @@ namespace MobileGL::MG_Remote::Server {
Bool ServerVerbSink::OnBindStreamOutput(const MG_Pipe::MGPStreamOutputBind& bind) {
const MG_Backend::GlobalBackendFunctionsTable* table = Table("bind_stream_output");
if (table == nullptr) return false;
if (table->GL.BindTransformFeedback == nullptr) return false;
// THE GL NAME IS THE ARGUMENT, NOT THE LifetimeId BESIDE IT. Espryt keys its driver
// objects by the GL name (XfbImpl::g_xfbObjects[name], DirectGLES.cpp:1401) and creates
// the ES object on first bind; passing the lifetime id would index a map that has never
@@ -1016,11 +1062,25 @@ namespace MobileGL::MG_Remote::Server {
// identifies the P5f capture snapshot on this side. Keeping it separate preserves
// the backend GL-name argument while Begin/End find the right server-owned span.
MG_Pipe::MGPipeApplier().BoundStreamOutputLifetimeId = bind.LifetimeId;
table->GL.BindTransformFeedback(static_cast<GLuint>(bind.GlName));
if (table->GL.BindTransformFeedback) table->GL.BindTransformFeedback(static_cast<GLuint>(bind.GlName));
++m_streamOutputBinds;
return true;
}
Bool ServerVerbSink::OnDeleteStreamOutput(const MG_Pipe::MGPStreamOutputBind& object) {
const auto* table = Table("DeleteTransformFeedback");
if (!table) return false;
if (!object.GlName || object.Pad0)
Wire::WireProtocolFatal("DeleteStreamOutput.name", "default object or reserved field");
auto& state = MG_Pipe::MGPipeApplier();
if (object.LifetimeId) {
state.StreamOutputSpans.erase(object.LifetimeId);
if (state.BoundStreamOutputLifetimeId == object.LifetimeId) state.BoundStreamOutputLifetimeId = 0;
}
if (table->GL.DeleteTransformFeedback) table->GL.DeleteTransformFeedback(object.GlName);
return true;
}
Bool ServerVerbSink::OnPatchParameter(const MG_Pipe::MGPPatchParameter& patch) {
const MG_Backend::GlobalBackendFunctionsTable* table = Table("patch_parameter");
if (table == nullptr) return false;
+24
View File
@@ -141,11 +141,22 @@ namespace MobileGL::MG_Remote::Server {
Bool OnFenceWait(const MG_Pipe::MGPFenceWait&, Uint32&) override;
Bool OnFenceWaitServer(const MG_Pipe::MGPFenceWait&) override;
void ReleaseFences();
Bool OnQueryCreate(const MG_Pipe::MGPQueryDesc&) override;
Bool OnQueryBegin(const MG_Pipe::MGPQueryDesc&) override;
Bool OnQueryEnd(const MG_Pipe::MGPQueryDesc&) override;
Bool OnQueryCounter(const MG_Pipe::MGPQueryDesc&) override;
Bool OnQueryAvailable(const MG_Pipe::MGPHandleOnly&, Uint32&) override;
Bool OnQueryResult(const MG_Pipe::MGPQueryResultRequest&, Wire::QueryResultReply&) override;
Bool OnQueryDestroy(const MG_Pipe::MGPHandleOnly&) override;
Bool OnQueryTimestamp(const MG_Pipe::MGPTimestampRequest&, Int64&) override;
void ReleaseQueries();
Bool OnClear(const MG_Pipe::MGPClear& clear) override;
Bool OnBlit(const MG_Pipe::MGPBlit& blit) override;
Bool OnPresent(const MG_Pipe::MGPPresent& present) override;
Bool OnReadPixels(const MG_Pipe::MGPReadbackInfo& info, Uint64 seq,
Wire::ReplySink* replies) override;
Bool OnGetTextureImage(const MG_Pipe::MGPReadbackInfo& info, Uint64 seq,
Wire::ReplySink* replies) override;
Bool OnDrawVbo(const MG_Pipe::MGPDrawInfo& info, const MG_Pipe::MGPDrawRange* ranges,
const MG_Pipe::MGHostSpan* userIndices,
const MG_Pipe::MGPDrawIndirect* indirect) override;
@@ -179,6 +190,7 @@ namespace MobileGL::MG_Remote::Server {
Bool OnPauseStreamOutput(const MG_Pipe::MGPStreamOutputControl& control) override;
Bool OnResumeStreamOutput(const MG_Pipe::MGPStreamOutputControl& control) override;
Bool OnBindStreamOutput(const MG_Pipe::MGPStreamOutputBind& bind) override;
Bool OnDeleteStreamOutput(const MG_Pipe::MGPStreamOutputBind& object) override;
Bool OnPatchParameter(const MG_Pipe::MGPPatchParameter& patch) override;
Bool OnGenerateMipmap(const MG_Pipe::MGPMipPlan& plan) override;
Bool OnCopyFramebufferToTexture(const MG_Pipe::MGPCopyFromFramebuffer& copy) override;
@@ -273,6 +285,18 @@ namespace MobileGL::MG_Remote::Server {
};
FenceEntry& FindFence(MG_Pipe::MGPipeHandle handle);
UnorderedMap<Uint32, FenceEntry> m_fences;
struct QueryEntry {
Uint32 Gen = 0;
Uint32 Kind = 0;
Bool Live = false;
Bool Active = false;
MG_Backend::BackendQueryHandle Native = nullptr;
MG_Backend::BackendSyncHandle Completion = nullptr;
};
QueryEntry& FindQuery(MG_Pipe::MGPipeHandle handle);
void EndNativeQuery(QueryEntry& entry);
Bool QueryGpuComplete(QueryEntry& entry, Bool wait);
UnorderedMap<Uint32, QueryEntry> m_queries;
MG_Backend::BackendObject* m_backend = nullptr;
Uint64 m_clears = 0;
Uint64 m_draws = 0;
+171
View File
@@ -0,0 +1,171 @@
// Server-owned query identities and actual backend query/fence results.
ServerVerbSink::QueryEntry& ServerVerbSink::FindQuery(MG_Pipe::MGPipeHandle handle) {
const auto found = m_queries.find(handle.Slot);
if (!handle.Slot || found == m_queries.end() || !found->second.Live || found->second.Gen != handle.Gen)
Wire::WireProtocolFatal("Query.handle", "missing, destroyed or stale query handle");
return found->second;
}
Bool ServerVerbSink::OnQueryCreate(const MG_Pipe::MGPQueryDesc& desc) {
if (!Table("QueryCreate")) return false;
if (!desc.Query.Slot || desc.Stream != 0)
Wire::WireProtocolFatal("QueryCreate.identity", "reserved handle or unsupported stream");
switch (desc.Kind) {
case GL_TIME_ELAPSED: case GL_TIMESTAMP: case GL_SAMPLES_PASSED:
case GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN: case GL_PRIMITIVES_GENERATED: break;
default: Wire::WireProtocolFatal("QueryCreate.Kind", "unknown query target");
}
const Bool seen = m_queries.find(desc.Query.Slot) != m_queries.end();
auto& entry = m_queries[desc.Query.Slot];
if (entry.Live || (seen && desc.Query.Gen <= entry.Gen))
Wire::WireProtocolFatal("QueryCreate.handle", "duplicate or stale query generation");
entry = QueryEntry{};
entry.Gen = desc.Query.Gen;
entry.Kind = desc.Kind;
entry.Live = true;
return true;
}
Bool ServerVerbSink::OnQueryBegin(const MG_Pipe::MGPQueryDesc& desc) {
auto& entry = FindQuery(desc.Query);
const auto* table = Table("QueryBegin");
if (!table) return false;
if (desc.Stream || desc.Kind != entry.Kind || entry.Active || entry.Native)
Wire::WireProtocolFatal("QueryBegin.state", "query target or lifecycle mismatch");
const auto& gl = table->GL;
switch (entry.Kind) {
case GL_TIME_ELAPSED:
if (!gl.BeginTimeElapsedQuery) return false;
entry.Native = gl.BeginTimeElapsedQuery();
break;
case GL_SAMPLES_PASSED:
if (!gl.BeginOcclusionQuery) return false;
entry.Native = gl.BeginOcclusionQuery();
break;
case GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN:
case GL_PRIMITIVES_GENERATED:
if (!gl.BeginXfbPrimitivesQuery) return false;
entry.Native = gl.BeginXfbPrimitivesQuery(entry.Kind == GL_PRIMITIVES_GENERATED);
break;
default: Wire::WireProtocolFatal("QueryBegin.Kind", "target is not a begin/end query");
}
// A failed native allocation remains a real, unreadable query. Returning a
// null proxy to the frontend would silently select CPU primitive accounting.
entry.Active = true;
return true;
}
void ServerVerbSink::EndNativeQuery(QueryEntry& entry) {
if (!entry.Active || !m_backend) return;
const auto& gl = m_backend->GetBackendFunctions().GL;
if (entry.Native) {
switch (entry.Kind) {
case GL_TIME_ELAPSED: if (gl.EndTimeElapsedQuery) gl.EndTimeElapsedQuery(entry.Native); break;
case GL_SAMPLES_PASSED: if (gl.EndOcclusionQuery) gl.EndOcclusionQuery(entry.Native); break;
case GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN:
case GL_PRIMITIVES_GENERATED:
if (gl.EndXfbPrimitivesQuery) gl.EndXfbPrimitivesQuery(entry.Native);
break;
default: break;
}
}
entry.Active = false;
}
Bool ServerVerbSink::OnQueryEnd(const MG_Pipe::MGPQueryDesc& desc) {
auto& entry = FindQuery(desc.Query);
const auto* table = Table("QueryEnd");
if (!table) return false;
if (desc.Stream || desc.Kind != entry.Kind || !entry.Active)
Wire::WireProtocolFatal("QueryEnd.state", "query target or lifecycle mismatch");
EndNativeQuery(entry);
if (entry.Native && table->GL.FenceSync) entry.Completion = table->GL.FenceSync();
return true;
}
Bool ServerVerbSink::OnQueryCounter(const MG_Pipe::MGPQueryDesc& desc) {
auto& entry = FindQuery(desc.Query);
const auto* table = Table("QueryCounter");
if (!table || !table->GL.QueryCounterTimestamp) return false;
if (desc.Stream || desc.Kind != GL_TIMESTAMP || entry.Kind != GL_TIMESTAMP || entry.Native || entry.Active)
Wire::WireProtocolFatal("QueryCounter.state", "query target or lifecycle mismatch");
entry.Native = table->GL.QueryCounterTimestamp();
if (entry.Native && table->GL.FenceSync) entry.Completion = table->GL.FenceSync();
return true;
}
Bool ServerVerbSink::QueryGpuComplete(QueryEntry& entry, Bool wait) {
if (!m_backend || !entry.Native || entry.Active) return false;
const auto& gl = m_backend->GetBackendFunctions().GL;
if (!entry.Completion || !gl.ClientWaitSync || !gl.IsQueryResultAvailable) return false;
// Polling may submit the pending batch, but never waits for GPU completion.
// A blocking result explicitly submits/waits, including present-less workloads.
const GLenum completion = gl.ClientWaitSync(entry.Completion, GL_SYNC_FLUSH_COMMANDS_BIT,
wait ? GL_TIMEOUT_IGNORED : 0);
if (completion != GL_ALREADY_SIGNALED && completion != GL_CONDITION_SATISFIED) return false;
return gl.IsQueryResultAvailable(entry.Native);
}
Bool ServerVerbSink::OnQueryAvailable(const MG_Pipe::MGPHandleOnly& request, Uint32& result) {
if (request.Kind != static_cast<Uint32>(MG_Pipe::MGPipeKind::Query))
Wire::WireProtocolFatal("Query.Kind", "expected query namespace");
auto& entry = FindQuery(request.Handle);
result = QueryGpuComplete(entry, false) ? 1 : 0;
return true;
}
Bool ServerVerbSink::OnQueryResult(const MG_Pipe::MGPQueryResultRequest& request, Wire::QueryResultReply& reply) {
if (request.Wait > 1) Wire::WireProtocolFatal("QueryResult.Wait", "wait must be boolean");
auto& entry = FindQuery(request.Query);
const auto* table = Table("QueryResult");
if (!table || !table->GL.GetQueryResult64) return false;
reply = {};
if (QueryGpuComplete(entry, request.Wait != 0))
reply.Produced = table->GL.GetQueryResult64(entry.Native, request.Wait != 0, &reply.Value) ? 1 : 0;
return true;
}
Bool ServerVerbSink::OnQueryDestroy(const MG_Pipe::MGPHandleOnly& request) {
if (request.Kind != static_cast<Uint32>(MG_Pipe::MGPipeKind::Query))
Wire::WireProtocolFatal("Query.Kind", "expected query namespace");
auto& entry = FindQuery(request.Handle);
const auto* table = Table("QueryDestroy");
if (!table) return false;
EndNativeQuery(entry);
if (entry.Native && table->GL.DeleteBackendQuery) table->GL.DeleteBackendQuery(entry.Native);
if (entry.Completion && table->GL.DeleteSync) table->GL.DeleteSync(entry.Completion);
entry.Native = nullptr;
entry.Completion = nullptr;
entry.Live = false;
return true;
}
Bool ServerVerbSink::OnQueryTimestamp(const MG_Pipe::MGPTimestampRequest& request, Int64& result) {
if (request.Reserved) Wire::WireProtocolFatal("QueryTimestamp.Reserved", "reserved field is not zero");
const auto* table = Table("QueryTimestamp");
if (!table || !table->GL.QueryCounterTimestamp || !table->GL.GetQueryResult64 ||
!table->GL.DeleteBackendQuery || !table->GL.FenceSync || !table->GL.DeleteSync) return false;
QueryEntry entry{};
entry.Native = table->GL.QueryCounterTimestamp();
if (entry.Native) entry.Completion = table->GL.FenceSync();
Uint64 timestamp = 0;
const Bool produced = QueryGpuComplete(entry, true) &&
table->GL.GetQueryResult64(entry.Native, true, &timestamp);
if (entry.Native) table->GL.DeleteBackendQuery(entry.Native);
if (entry.Completion) table->GL.DeleteSync(entry.Completion);
if (produced) result = static_cast<Int64>(timestamp);
return produced;
}
void ServerVerbSink::ReleaseQueries() {
if (m_backend) {
const auto& gl = m_backend->GetBackendFunctions().GL;
for (auto& [slot, entry] : m_queries) {
if (!entry.Live) continue;
EndNativeQuery(entry);
if (entry.Native && gl.DeleteBackendQuery) gl.DeleteBackendQuery(entry.Native);
if (entry.Completion && gl.DeleteSync) gl.DeleteSync(entry.Completion);
}
}
m_queries.clear();
}
+4 -1
View File
@@ -632,7 +632,10 @@ namespace MobileGL::MG_Remote::Server {
// R-8/C-4: bits 32..47 of CallMask are the CONSUMER MASK, and this is the only place
// they are produced.
const Uint64 callMask = CallMask();
Uint64 callMask = CallMask();
const auto timerSupported = m_backend->GetBackendFunctions().GL.IsTimerQuerySupported;
if ((callMask & MG_Pipe::kCapTimerQuery) && (!timerSupported || !timerSupported()))
callMask &= ~static_cast<Uint64>(MG_Pipe::kCapTimerQuery);
::flatbuffers::FlatBufferBuilder builder(4096);
auto dynamicVector =
+32 -9
View File
@@ -243,7 +243,8 @@ namespace MobileGL::MG_Remote::Wire {
X(ApplierReset, MGPApplierReset) \
X(ObjectDeath, MGPHandleOnly) \
X(SetContextValues, MGPContextValues) \
X(SetProgramBindings, MGPProgramBindings)
X(SetProgramBindings, MGPProgramBindings) \
X(DeleteStreamOutput, MGPStreamOutputBind)
namespace {
@@ -1657,16 +1658,37 @@ namespace MobileGL::MG_Remote::Wire {
ok ? &result : nullptr, ok ? sizeof(result) : 0);
return ok;
}
// Query migration follows the measured first blockers.
case MGPWireOp::QueryCreate:
return m_verbs && m_verbs->OnQueryCreate(*static_cast<const MGPQueryDesc*>(payload));
case MGPWireOp::QueryBegin:
return m_verbs && m_verbs->OnQueryBegin(*static_cast<const MGPQueryDesc*>(payload));
case MGPWireOp::QueryEnd:
case MGPWireOp::QueryAvailable:
case MGPWireOp::QueryResult:
return m_verbs && m_verbs->OnQueryEnd(*static_cast<const MGPQueryDesc*>(payload));
case MGPWireOp::QueryDestroy:
case MGPWireOp::QueryTimestamp:
return m_verbs && m_verbs->OnQueryDestroy(*static_cast<const MGPHandleOnly*>(payload));
case MGPWireOp::QueryCounter:
return false;
return m_verbs && m_verbs->OnQueryCounter(*static_cast<const MGPQueryDesc*>(payload));
case MGPWireOp::QueryAvailable: {
Uint32 result = 0;
const Bool ok = m_verbs && m_verbs->OnQueryAvailable(*static_cast<const MGPHandleOnly*>(payload), result);
PostReply(op, seq, ok ? ReplySink::kStatusOk : ReplySink::kStatusDeclined,
ok ? &result : nullptr, ok ? sizeof(result) : 0);
return ok;
}
case MGPWireOp::QueryResult: {
QueryResultReply result{};
const Bool ok = m_verbs && m_verbs->OnQueryResult(*static_cast<const MGPQueryResultRequest*>(payload), result);
PostReply(op, seq, ok ? ReplySink::kStatusOk : ReplySink::kStatusDeclined,
ok ? &result : nullptr, ok ? sizeof(result) : 0);
return ok;
}
case MGPWireOp::QueryTimestamp: {
Int64 result = 0;
const Bool ok = m_verbs && m_verbs->OnQueryTimestamp(*static_cast<const MGPTimestampRequest*>(payload), result);
PostReply(op, seq, ok ? ReplySink::kStatusOk : ReplySink::kStatusDeclined,
ok ? &result : nullptr, ok ? sizeof(result) : 0);
return ok;
}
// ---- CSOs ------------------------------------------------------------------------
case MGPWireOp::CreateRenderState: {
@@ -2103,9 +2125,8 @@ namespace MobileGL::MG_Remote::Wire {
m_verbs->OnGenerateMipmap(*static_cast<const MGPMipPlan*>(payload));
case MGPWireOp::GetTextureImage:
// Off the reduced path (BRIEF §4) and outside P5b's measured 25: its emit-table slot
// is Fatal{UnmigratedVerb} on the client (contract §7 class C, wave 3 / P9).
return false;
return m_verbs != nullptr &&
m_verbs->OnGetTextureImage(*static_cast<const MGPReadbackInfo*>(payload), seq, m_replies);
// ---- the five class-B verbs: no MGPipeApply* exists, so v1's sink or nothing -------
case MGPWireOp::Blit:
@@ -2205,6 +2226,8 @@ namespace MobileGL::MG_Remote::Wire {
case MGPWireOp::BindStreamOutput:
return m_verbs != nullptr &&
m_verbs->OnBindStreamOutput(*static_cast<const MGPStreamOutputBind*>(payload));
case MGPWireOp::DeleteStreamOutput:
return m_verbs && m_verbs->OnDeleteStreamOutput(*static_cast<const MGPStreamOutputBind*>(payload));
case MGPWireOp::SetStorageBlockBinding: {
// The block name is the ONE string on the wire (CONTRACT-P5B.md i1): a kHasBlob
+19
View File
@@ -445,6 +445,13 @@ namespace MobileGL::MG_Remote::Wire {
// exclusion list), so inventing a consumer for them would be building a semantics nobody
// can test this phase. Their arms validate both tails - which is the part a later phase
// must not have to re-derive - and return false.
struct QueryResultReply {
Uint64 Value = 0;
Uint32 Produced = 0;
Uint32 Reserved = 0;
};
static_assert(sizeof(QueryResultReply) == 16);
class WireVerbSink {
public:
virtual ~WireVerbSink() = default;
@@ -453,6 +460,15 @@ namespace MobileGL::MG_Remote::Wire {
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 OnQueryCreate(const MG_Pipe::MGPQueryDesc&) { return false; }
virtual Bool OnQueryBegin(const MG_Pipe::MGPQueryDesc&) { return false; }
virtual Bool OnQueryEnd(const MG_Pipe::MGPQueryDesc&) { return false; }
virtual Bool OnQueryCounter(const MG_Pipe::MGPQueryDesc&) { return false; }
virtual Bool OnQueryAvailable(const MG_Pipe::MGPHandleOnly&, Uint32&) { return false; }
virtual Bool OnQueryResult(const MG_Pipe::MGPQueryResultRequest&, QueryResultReply&) { return false; }
virtual Bool OnQueryDestroy(const MG_Pipe::MGPHandleOnly&) { return false; }
virtual Bool OnQueryTimestamp(const MG_Pipe::MGPTimestampRequest&, Int64&) { return false; }
virtual Bool OnDeleteStreamOutput(const MG_Pipe::MGPStreamOutputBind&) { return false; }
virtual Bool OnClear(const MG_Pipe::MGPClear& clear) {
(void)clear;
return false;
@@ -474,6 +490,9 @@ namespace MobileGL::MG_Remote::Wire {
(void)replies;
return false;
}
virtual Bool OnGetTextureImage(const MG_Pipe::MGPReadbackInfo&, Uint64, ReplySink*) {
return false;
}
// `ranges` is info.NumDraws entries. `userIndices` is null unless the record set
// kDrawHasUserIndices; `indirect` is null unless it set kDrawIsIndirect (P5b d1,
// CONTRACT-P5B.md). The layout refuses a record that sets both, so at most one of the
+7
View File
@@ -1455,6 +1455,13 @@ namespace MobileGL::MG_State {
return it != m_transformFeedbackObjects.end() && it->second.everBound;
}
#if MOBILEGL_BUILD_DISAGGREGATED
Uint64 GLContext::GetTransformFeedbackLifetimeId(Uint index) const {
const auto found = m_transformFeedbackObjects.find(index);
return found == m_transformFeedbackObjects.end() ? 0 : found->second.lifetimeId;
}
#endif
void GLContext::MarkTransformFeedbackObjectForDeletion(Uint index) {
if (index == 0 || !m_transformFeedbackNames.IsValid(index)) return;
// Deleting the bound object reverts to the default one (GL 4.6 core 13.2.1);
+3
View File
@@ -531,6 +531,9 @@ namespace MobileGL {
// Never returns 0 - the counter starts at 1 so a zero-initialised memo slot cannot
// be mistaken for a live object.
Uint64 GetBoundTransformFeedbackLifetimeId() const { return m_boundTransformFeedbackLifetimeId; }
#if MOBILEGL_BUILD_DISAGGREGATED
Uint64 GetTransformFeedbackLifetimeId(Uint index) const;
#endif
// Whether the object carrying this identity still has an OPEN capture span - one
// that glBeginTransformFeedback started and glEndTransformFeedback has not closed,
// paused or not. A backend that hands out a bounded set of per-object slots must
+1 -1
View File
@@ -156,7 +156,7 @@ TEST(PipeCatalogue, GeneratedTablesHoldTheWholeCatalogue) {
// 13 + the five P5b-appended verbs (MG_Remote/CONTRACT-P5B.md): bind_shader_image,
// patch_parameter, bind_stream_output, set_storage_block_binding,
// copy_framebuffer_to_texture.
EXPECT_EQ(ClassCount<kCtxVerb>(), 18u);
EXPECT_EQ(ClassCount<kCtxVerb>(), 19u);
}
// A row nobody has migrated is null - which is exactly what "this subsystem has not been
+125
View File
@@ -3011,6 +3011,131 @@ TEST(FenceWireRoundTrip, NullNativeFenceUsesOnlyTheExistingMonolithFallback) {
sink.SetBackend(nullptr);
}
namespace {
int nativeQueryToken = 0;
Bool queryGpuReady = false;
Bool queryGenerated = false;
Uint32 queryReads = 0, queryEnds = 0, queryDeletes = 0;
constexpr Uint64 queryNativeValue = 0xfedcba9876543210ull;
void InstallQueryProbe(FenceProbeBackend& backend) {
InstallFenceProbe(backend);
queryGpuReady = queryGenerated = false;
queryReads = queryEnds = queryDeletes = 0;
backend.Table.GL.BeginXfbPrimitivesQuery = +[](Bool generated) -> MG_Backend::BackendQueryHandle {
queryGenerated = generated;
return &nativeQueryToken;
};
backend.Table.GL.EndXfbPrimitivesQuery = +[](MG_Backend::BackendQueryHandle query) {
EXPECT_EQ(query, &nativeQueryToken);
++queryEnds;
};
backend.Table.GL.IsQueryResultAvailable = +[](MG_Backend::BackendQueryHandle query) -> Bool {
EXPECT_EQ(query, &nativeQueryToken);
return queryGpuReady;
};
backend.Table.GL.GetQueryResult64 = +[](MG_Backend::BackendQueryHandle query, Bool, Uint64* value) -> Bool {
EXPECT_EQ(query, &nativeQueryToken);
++queryReads;
*value = queryNativeValue;
return true;
};
backend.Table.GL.DeleteBackendQuery = +[](MG_Backend::BackendQueryHandle query) {
EXPECT_EQ(query, &nativeQueryToken);
++queryDeletes;
};
backend.Table.GL.ClientWaitSync = +[](MG_Backend::BackendSyncHandle sync, GLbitfield flags,
GLuint64 timeout) -> GLenum {
EXPECT_EQ(sync, &nativeFenceToken);
fenceFlags = flags;
fenceTimeout = timeout;
return queryGpuReady ? GL_CONDITION_SATISFIED : GL_TIMEOUT_EXPIRED;
};
}
}
TEST(QueryWireRoundTrip, PrimitiveQueriesPreservePendingStateNativeResultsAndIdentity) {
Wire2 wire;
FenceProbeBackend backend;
InstallQueryProbe(backend);
Server::ServerVerbSink sink;
sink.SetBackend(&backend);
wire.Decoder().SetVerbSink(&sink);
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;
};
const MGPQueryDesc desc{{51, 0}, GL_PRIMITIVES_GENERATED, 0};
send(MGPWireOp::QueryCreate, desc);
send(MGPWireOp::QueryBegin, desc);
EXPECT_TRUE(queryGenerated);
send(MGPWireOp::QueryEnd, desc);
EXPECT_EQ(queryEnds, 1u);
MGPQueryResultRequest request{};
request.Query = desc.Query;
const auto pendingSeq = send(MGPWireOp::QueryResult, request);
ASSERT_EQ(wire.Answers().All.back().Bytes.size(), sizeof(QueryResultReply));
QueryResultReply reply{};
std::memcpy(&reply, wire.Answers().All.back().Bytes.data(), sizeof(reply));
EXPECT_EQ(wire.Answers().All.back().Seq, pendingSeq);
EXPECT_EQ(wire.Answers().All.back().Status, ReplySink::kStatusOk);
EXPECT_EQ(reply.Produced, 0u);
EXPECT_EQ(queryReads, 0u) << "NO_WAIT must not call a potentially blocking native result read";
EXPECT_EQ(fenceTimeout, 0u);
queryGpuReady = true;
request.Wait = 1;
send(MGPWireOp::QueryResult, request);
std::memcpy(&reply, wire.Answers().All.back().Bytes.data(), sizeof(reply));
EXPECT_EQ(reply.Produced, 1u);
EXPECT_EQ(reply.Value, queryNativeValue);
EXPECT_EQ(queryReads, 1u);
EXPECT_EQ(fenceTimeout, GL_TIMEOUT_IGNORED);
EXPECT_EQ(fenceFlags, GL_SYNC_FLUSH_COMMANDS_BIT);
const MGPHandleOnly handle{desc.Query, static_cast<Uint32>(MGPipeKind::Query), 0};
send(MGPWireOp::QueryAvailable, handle);
Uint32 available = 0;
ASSERT_EQ(wire.Answers().All.back().Bytes.size(), sizeof(available));
std::memcpy(&available, wire.Answers().All.back().Bytes.data(), sizeof(available));
EXPECT_EQ(available, 1u);
send(MGPWireOp::QueryDestroy, handle);
EXPECT_EQ(queryDeletes, 1u);
EXPECT_EQ(fenceDeletes, 1u);
const MGPQueryDesc replacement{{51, 1}, GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN, 0};
send(MGPWireOp::QueryCreate, replacement);
send(MGPWireOp::QueryBegin, replacement);
EXPECT_FALSE(queryGenerated);
sink.SetBackend(nullptr);
EXPECT_EQ(queryEnds, 2u);
EXPECT_EQ(queryDeletes, 2u);
}
TEST(QueryWireRoundTrip, FailedNativeCreationDoesNotBecomeAnAvailableZeroResult) {
FenceProbeBackend backend;
InstallQueryProbe(backend);
backend.Table.GL.BeginXfbPrimitivesQuery = +[](Bool) -> MG_Backend::BackendQueryHandle { return nullptr; };
Server::ServerVerbSink sink;
sink.SetBackend(&backend);
const MGPQueryDesc desc{{7, 0}, GL_PRIMITIVES_GENERATED, 0};
ASSERT_TRUE(sink.OnQueryCreate(desc));
ASSERT_TRUE(sink.OnQueryBegin(desc));
ASSERT_TRUE(sink.OnQueryEnd(desc));
Uint32 available = 1;
EXPECT_TRUE(sink.OnQueryAvailable({desc.Query, static_cast<Uint32>(MGPipeKind::Query), 0}, available));
EXPECT_EQ(available, 0u);
MGPQueryResultRequest request{};
request.Query = desc.Query;
request.Wait = 1;
QueryResultReply reply{};
EXPECT_TRUE(sink.OnQueryResult(request, reply));
EXPECT_EQ(reply.Produced, 0u);
EXPECT_EQ(queryReads, 0u);
sink.SetBackend(nullptr);
}
#if MGTEST_HAVE_FORK
TEST(FenceWireRoundTrip, DestroyedAndRecycledWireHandlesNeverReachTheBackend) {
const auto r = RunInChild([] {
+7 -9
View File
@@ -181,8 +181,8 @@ namespace {
TEST(RemoteEmitTable, TheThreeClassesPartitionAllSeventyOneSlots) {
// P5 baseline five + f1 eleven + i1 seven + t2 six emitted slots.
EXPECT_EQ(LocallyAnsweredSlotCount(), 2u);
EXPECT_EQ(ImplementedVerbCount(), 54u);
EXPECT_EQ(UnmigratedSlotCount(), 15u);
EXPECT_EQ(ImplementedVerbCount(), 68u);
EXPECT_EQ(UnmigratedSlotCount(), 1u);
EXPECT_EQ(LocallyAnsweredSlotCount() + ImplementedVerbCount() + UnmigratedSlotCount(),
kRemoteEmitSlotCount);
}
@@ -270,8 +270,9 @@ TEST(RemoteEmitTable, DeleteTransformFeedbackHasNoRowAndStillAbortsByItsOwnName)
// failed.
const ChildResult r = RunInChild([] { RemoteEmitTable().GL.DeleteTransformFeedback(7); });
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
EXPECT_NE(r.Log.find("Fatal{UnmigratedVerb, \"DeleteTransformFeedback\"}"), std::string::npos)
EXPECT_NE(r.Log.find("Fatal{NoClientSession, \"DeleteTransformFeedback\"}"), std::string::npos)
<< r.Log;
EXPECT_EQ(r.Log.find("Fatal{UnmigratedVerb"), std::string::npos) << r.Log;
}
#endif // MGTEST_HAVE_FORK
@@ -326,12 +327,9 @@ TEST(RemoteEmitTable, AnUnmigratedSlotAbortsAndNamesItself) {
// GetTexImage is the wave-3 tail (CONTRACT-P5B.md §7): no P5b package flips it, so this
// case keeps its subject across the four P5b landings. (It was DrawElements until d1 made
// that a class-B emitter.)
const ChildResult r = RunInChild([] {
RemoteEmitTable().GL.GetTexImage(0x0DE1 /*GL_TEXTURE_2D*/, 0, 0x1908 /*GL_RGBA*/,
0x1401 /*GL_UNSIGNED_BYTE*/, nullptr);
});
const ChildResult r = RunInChild([] { RemoteEmitTable().SetSwapInterval(1); });
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
EXPECT_NE(r.Log.find("Fatal{UnmigratedVerb, \"GetTexImage\"}"), std::string::npos) << r.Log;
EXPECT_NE(r.Log.find("Fatal{UnmigratedVerb, \"SetSwapInterval\"}"), std::string::npos) << r.Log;
}
TEST(RemoteEmitTable, EachUnmigratedSlotNamesItsOwnSlot) {
@@ -383,7 +381,7 @@ TEST(RemoteEmitTable, TheSevenI1SlotsAreClassBAndAreNotTheFatalThunk) {
// assignment would look like (class C is assigned FIRST in BuildRemoteEmitTable precisely so
// that the mistake is loud rather than null).
const MG_Backend::GlobalBackendFunctionsTable& table = RemoteEmitTable();
const void* fatal = reinterpret_cast<const void*>(table.GL.GetTexImage); // wave-3 tail, class C
const void* fatal = reinterpret_cast<const void*>(table.SetSwapInterval);
ASSERT_NE(fatal, nullptr);
const void* const i1[] = {
reinterpret_cast<const void*>(table.GL.BindImageTexture),