[Feat] (MG_Remote, P5): land the client role - BackendObject_Remote, the 71-slot emit table, the caps mirror and the verb barrier

This commit is contained in:
2026-09-11 16:46:24 -04:00
parent bfaf5f9d0e
commit d628d906d1
18 changed files with 2235 additions and 71 deletions
+25 -2
View File
@@ -31,6 +31,11 @@
#include <MG_Util/ShaderTranspiler/Types.h>
#include <MG_Backend/BackendObjects.h>
#include <MG_Impl/Pipe/PipeFill.h>
// CONTRACT-P5.md §7 / ID-14: a null check on a GLFunctionsTable slot may not survive into the
// client under split - it becomes a caps-mirror read. SlotCaps.h carries the rule and the test
// that decides which of its two spellings a site takes; in a pull build both expand to exactly
// the check they replaced.
#include <MG_Remote/Client/SlotCaps.h>
namespace MobileGL::MG_Impl::GLImpl {
// Declared rather than #included from GL_RenderState.h on purpose: that header also declares
@@ -1354,7 +1359,16 @@ namespace MobileGL::MG_Impl::GLImpl {
// full 64-bit GPU timestamp survives; LWJGL reads it this way.
Int64 timestamp = 0;
if (!MG_Config::Features.DisableTimerQuery) {
if (const auto getGpuTimestampNs = MG_Backend::gBackendFunctionsTable.GL.GetGpuTimestampNs) {
// glGetInteger64v(GL_TIMESTAMP). GetGpuTimestampNs is class C - a LIVE GPU
// timestamp is not a static property, so R-15 does not reach it - and the
// documented answer when it is unavailable is 0 (BackendObject.h:192), which
// is correct rather than merely quiet. kCapTimerQuery is the published bit.
//
// The POINTER-valued macro, so the init-statement below is unchanged in a pull
// build and G1 cannot see this edit: the Bool-valued spelling moved this
// function by -150 bytes for no behavioural reason at all.
if (const auto getGpuTimestampNs =
MGL_BACKEND_SLOT_PTR_CAP(GetGpuTimestampNs, MG_Pipe::kCapTimerQuery)) {
MGP_FILL(GetGpuTimestampNs);
timestamp = getGpuTimestampNs();
}
@@ -2265,7 +2279,16 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_TIMESTAMP: {
Int64 timestamp = 0;
if (!MG_Config::Features.DisableTimerQuery) {
if (const auto getGpuTimestampNs = MG_Backend::gBackendFunctionsTable.GL.GetGpuTimestampNs) {
// glGetInteger64v(GL_TIMESTAMP). GetGpuTimestampNs is class C - a LIVE GPU
// timestamp is not a static property, so R-15 does not reach it - and the
// documented answer when it is unavailable is 0 (BackendObject.h:192), which
// is correct rather than merely quiet. kCapTimerQuery is the published bit.
//
// The POINTER-valued macro, so the init-statement below is unchanged in a pull
// build and G1 cannot see this edit: the Bool-valued spelling moved this
// function by -150 bytes for no behavioural reason at all.
if (const auto getGpuTimestampNs =
MGL_BACKEND_SLOT_PTR_CAP(GetGpuTimestampNs, MG_Pipe::kCapTimerQuery)) {
MGP_FILL(GetGpuTimestampNs);
timestamp = getGpuTimestampNs();
}
+17 -6
View File
@@ -13,6 +13,10 @@
#include <MG_State/GLState/Core.h>
#include <MG_State/GLState/ErrorState/ErrorInfo.h>
#include <MG_Impl/Pipe/PipeFill.h>
// CONTRACT-P5.md §7 / ID-14: a null check on a GLFunctionsTable slot may not survive into the
// client under split - it becomes a caps-mirror read, whatever class the slot itself is in.
// The two macros carry that rule; in a pull build each expands to exactly the check it replaced.
#include <MG_Remote/Client/SlotCaps.h>
namespace MobileGL::MG_Impl::GLImpl {
namespace {
@@ -478,7 +482,7 @@ namespace MobileGL::MG_Impl::GLImpl {
const Bool isOcclusionQuery =
(target == GL_SAMPLES_PASSED || target == GL_ANY_SAMPLES_PASSED ||
target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) &&
MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery != nullptr;
MGL_BACKEND_SLOT_CAP(BeginOcclusionQuery, MG_Pipe::kCapOcclusionQuery);
const Bool isPipelineStatisticsQuery = IsPipelineStatisticsQueryTarget(target);
if (target != GL_TIME_ELAPSED && !isTransformFeedbackQuery && !isOcclusionQuery &&
!isPipelineStatisticsQuery) {
@@ -528,10 +532,12 @@ namespace MobileGL::MG_Impl::GLImpl {
} else if (isTransformFeedbackQuery) {
// Prefer real GPU transform-feedback queries (exact with geometry shaders);
// the CPU accounting delta stays as the fallback when the backend lacks them.
const Bool xfbQuerySupported =
MGL_BACKEND_SLOT_CAP(BeginXfbPrimitivesQuery, MG_Pipe::kCapXfbPrimitivesQuery);
const auto beginXfbPrimitivesQuery = MG_Backend::gBackendFunctionsTable.GL.BeginXfbPrimitivesQuery;
MGP_FILL(BeginXfbPrimitivesQuery);
queryObject->backendHandle =
beginXfbPrimitivesQuery ? beginXfbPrimitivesQuery(target == GL_PRIMITIVES_GENERATED) : nullptr;
xfbQuerySupported ? beginXfbPrimitivesQuery(target == GL_PRIMITIVES_GENERATED) : nullptr;
queryObject->counterSnapshot = TransformFeedbackCounterForTarget(target);
queryObject->accountedCaptureDrawSnapshot =
MG_State::pGLContext->GetTransformFeedbackAccountedCaptureDraws();
@@ -541,10 +547,12 @@ namespace MobileGL::MG_Impl::GLImpl {
MGP_FILL(BeginOcclusionQuery);
queryObject->backendHandle = MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery();
} else {
const Bool timerQuerySupported =
MGL_BACKEND_SLOT_CAP(BeginTimeElapsedQuery, MG_Pipe::kCapTimerQuery);
const auto beginTimeElapsedQuery = MG_Backend::gBackendFunctionsTable.GL.BeginTimeElapsedQuery;
MGP_FILL(BeginTimeElapsedQuery);
queryObject->backendHandle =
(!TimerQueryDisabled() && beginTimeElapsedQuery) ? beginTimeElapsedQuery() : nullptr;
(!TimerQueryDisabled() && timerQuerySupported) ? beginTimeElapsedQuery() : nullptr;
}
activeQueryId = id;
}
@@ -555,7 +563,7 @@ namespace MobileGL::MG_Impl::GLImpl {
const Bool isOcclusionQuery =
(target == GL_SAMPLES_PASSED || target == GL_ANY_SAMPLES_PASSED ||
target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) &&
MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery != nullptr;
MGL_BACKEND_SLOT_CAP(BeginOcclusionQuery, MG_Pipe::kCapOcclusionQuery);
const Bool isPipelineStatisticsQuery = IsPipelineStatisticsQueryTarget(target);
if (target != GL_TIME_ELAPSED && !isTransformFeedbackQuery && !isOcclusionQuery &&
!isPipelineStatisticsQuery) {
@@ -657,7 +665,10 @@ namespace MobileGL::MG_Impl::GLImpl {
ResetQueryObjectLocked(queryObject); // discard any previous result
queryObject->target = target;
const auto queryCounterTimestamp = MG_Backend::gBackendFunctionsTable.GL.QueryCounterTimestamp;
const Bool timerQuerySupported =
MGL_BACKEND_SLOT_CAP(QueryCounterTimestamp, MG_Pipe::kCapTimerQuery);
const auto queryCounterTimestamp =
timerQuerySupported ? MG_Backend::gBackendFunctionsTable.GL.QueryCounterTimestamp : nullptr;
MGP_FILL(QueryCounterTimestamp);
queryObject->backendHandle =
(!TimerQueryDisabled() && queryCounterTimestamp) ? queryCounterTimestamp() : nullptr;
@@ -782,7 +793,7 @@ namespace MobileGL::MG_Impl::GLImpl {
}
if (target == GL_SAMPLES_PASSED || target == GL_ANY_SAMPLES_PASSED ||
target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) {
const Bool occlusionSupported = MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery != nullptr;
const Bool occlusionSupported = MGL_BACKEND_SLOT_CAP(BeginOcclusionQuery, MG_Pipe::kCapOcclusionQuery);
*params = occlusionSupported ? (target == GL_SAMPLES_PASSED ? 32 : 1) : 0;
return;
}
+10 -1
View File
@@ -10,6 +10,11 @@
#include <MG_Backend/BackendObjects.h>
#include <MG_State/GLState/Core.h>
#include <MG_Impl/Pipe/PipeFill.h>
// CONTRACT-P5.md §7 / ID-14: a null check on a GLFunctionsTable slot may not survive into the
// client under split - it becomes a caps-mirror read. SlotCaps.h carries the rule and the test
// that decides which of its two spellings a site takes; in a pull build both expand to exactly
// the check they replaced.
#include <MG_Remote/Client/SlotCaps.h>
namespace MobileGL::MG_Impl::GLImpl {
namespace {
@@ -56,7 +61,11 @@ namespace MobileGL::MG_Impl::GLImpl {
auto* syncObject = new SyncObject;
syncObject->condition = condition;
syncObject->flags = flags;
if (const auto backendFenceSync = MG_Backend::gBackendFunctionsTable.GL.FenceSync) {
// 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)) {
MGP_FILL(FenceSync);
syncObject->backendHandle = backendFenceSync();
}
@@ -31,6 +31,11 @@
#include <MG_Util/Math/FixedPointConversion.h>
#include <MG_State/GLState/TextureState/TextureObjectBuffer.h>
#include <MG_Impl/Pipe/PipeFill.h>
// CONTRACT-P5.md §7 / ID-14: a null check on a GLFunctionsTable slot may not survive into the
// client under split - it becomes a caps-mirror read. SlotCaps.h carries the rule and the test
// that decides which of its two spellings a site takes; in a pull build both expand to exactly
// the check they replaced.
#include <MG_Remote/Client/SlotCaps.h>
// P4a, ID-18 M2. The ONE door MG_State and MG_Impl have into the client's emitters; the three
// call sites below are declarations only, exactly as the frontend's mutators are.
#include <MG_Pipe/PipeMutation.h>
@@ -6534,7 +6539,7 @@ namespace MobileGL::MG_Impl::GLImpl {
GLenum type, GLsizei bufSize, void* pixels, const char* caller) {
if (MG_Backend::pActiveBackendObject != nullptr &&
MG_Backend::pActiveBackendObject->GetBackendType() == BackendType::DirectVulkan &&
MG_Backend::gBackendFunctionsTable.GL.GetTextureImage != nullptr) {
MGL_BACKEND_SLOT_LOCAL(GetTextureImage)) {
MGP_FILL(GetTextureImage);
MG_Backend::gBackendFunctionsTable.GL.GetTextureImage(textureObject, uploadTarget, level, format, type,
bufSize, pixels);
@@ -6796,7 +6801,7 @@ namespace MobileGL::MG_Impl::GLImpl {
void GetTexImage(GLenum target, GLint level, GLenum format, GLenum type, GLvoid* pixels) {
if (!GetTexImage_State(target, level, format, type, pixels)) return;
if (MG_Backend::gBackendFunctionsTable.GL.GetTexImage != nullptr) {
if (MGL_BACKEND_SLOT_LOCAL(GetTexImage)) {
GetTexImage_Backend(target, level, format, type, pixels);
return;
}
+52 -4
View File
@@ -39,6 +39,15 @@
#include <MG_Pipe/PipeMutation.h>
#include <Config.h>
#if MOBILEGL_BUILD_DISAGGREGATED
// R-8 (c1): the client's liveness gates read the caps mirror, never MGPipeGetResourceOps().
// Behind the build option for G1's reason - nothing under MG_Remote may be reachable from a
// pull build - and every use below is additionally gated on the resolved TRANSPORT, because
// build-split runs MOBILEGL_TRANSPORT=monolith in every unit and integration-gpu lane and those
// lanes must keep answering exactly what they answered before.
#include <MG_Remote/Client/CapsMirror.h>
#endif
#include <atomic>
#include <cstdlib>
#include <cstring>
@@ -609,12 +618,38 @@ namespace MobileGL::MG_Pipe {
}
} // namespace
// R-8 (c1). THE SECOND CONJUNCT MOVES UNDER SPLIT, AND ONLY UNDER SPLIT.
//
// `MGPipeGetResourceOps() != nullptr` asks "has a backend registered the consumer". That
// table is the SERVER's registration and it is a PROCESS-WIDE global (PipeApply.cpp:402):
// under inproc a client reading it answers correctly BY ACCIDENT, and under spawn the
// client process has no backend at all, so the read answers null and five record families
// stop emitting - silently, while the emitters go on clearing their per-level dirty flags
// on the acceptance they never asked for. That is ID-39's 66 lost DirectVulkan uploads with
// a wire in between. The client asks the caps mirror instead, which carries the answer the
// SERVER gave at the handshake (CallMask bits 32..47).
//
// s1 made the server end Fatal when nobody sets the mask; this is the client end.
Bool MGPipeResourceSubsystemEnabled() {
return (MG_Config::Features.PipePush & kMGPipeSubsystemResources) != 0 &&
MGPipeGetResourceOps() != nullptr;
if ((MG_Config::Features.PipePush & kMGPipeSubsystemResources) == 0) return false;
#if MOBILEGL_BUILD_DISAGGREGATED
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
return MG_Remote::Client::CapsMirrorInstance().ServerConsumes(kMGPipeSubsystemResources);
}
#endif
return MGPipeGetResourceOps() != nullptr;
}
Bool MGPipeResourceOpsHaveSubDataResident() {
#if MOBILEGL_BUILD_DISAGGREGATED
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
// CONTRACT-P5.md §7's THIRD NAMED CAPABILITY PROBE. `ops->SubDataResident != nullptr`
// is not a safety check - it decides whether the resident-upload path EXISTS - and
// under split there is no op table here to probe. kCapResidentSubData is the bit the
// server publishes for exactly this question.
return MG_Remote::Client::CapsMirrorInstance().HasCap(kCapResidentSubData);
}
#endif
const MGPipeResourceOps* ops = MGPipeGetResourceOps();
return ops != nullptr && ops->SubDataResident != nullptr;
}
@@ -936,8 +971,21 @@ namespace MobileGL::MG_Pipe {
// MGPipeEmitResourceRespecify above uses for buffers) publishes it. Nothing here needs
// to remember the window.
Bool P4aFamilyHasItsConsumer(Uint64 subsystem) {
return (subsystem & kMGPipeP4aFamilySubsystems) == 0 ||
MGPipeGetResourceOps() != nullptr;
if ((subsystem & kMGPipeP4aFamilySubsystems) == 0) return true;
#if MOBILEGL_BUILD_DISAGGREGATED
// R-8 (c1), the same move as MGPipeResourceSubsystemEnabled's and for the same
// reason. ALL FOUR FAMILIES RIDE THE ONE SIGNAL, exactly as they do in monolith:
// the paragraph above explains why the resource consumer IS the texture family's
// consumer, and the split spelling of "a backend registered the resource op table"
// is "the server published the resource subsystem's consumer bit". Asking per
// family here would be a NEW rule, and a client that withheld more than the server
// refuses leaves the server's handle arm live with no records to read.
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
return MG_Remote::Client::CapsMirrorInstance().ServerConsumes(
kMGPipeSubsystemResources);
}
#endif
return MGPipeGetResourceOps() != nullptr;
}
// ================================================================================