mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-17 00:28:31 +09:00
[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:
@@ -571,6 +571,9 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
|
|||||||
MobileGL/MG_Remote/Client/ClientSession.cpp
|
MobileGL/MG_Remote/Client/ClientSession.cpp
|
||||||
MobileGL/MG_Remote/Client/EmitTables.cpp
|
MobileGL/MG_Remote/Client/EmitTables.cpp
|
||||||
MobileGL/MG_Remote/Client/CapsMirror.cpp
|
MobileGL/MG_Remote/Client/CapsMirror.cpp
|
||||||
|
# P5 c1: the client role's BackendObject. pActiveBackendObject holds one of these
|
||||||
|
# under split (table 3); the hook that installs it is v1's, in MG_Backend/Init.cpp.
|
||||||
|
MobileGL/MG_Remote/Client/BackendObject_Remote.cpp
|
||||||
# P5 b1's two: the conservative GPU-write set the client must build because all six
|
# P5 b1's two: the conservative GPU-write set the client must build because all six
|
||||||
# MarkGpuWritten producers are on the server's side of the line, and the
|
# MarkGpuWritten producers are on the server's side of the line, and the
|
||||||
# block-granularity persistent-map push that tier T2 makes mandatory.
|
# block-granularity persistent-map push that tier T2 makes mandatory.
|
||||||
|
|||||||
@@ -31,6 +31,11 @@
|
|||||||
#include <MG_Util/ShaderTranspiler/Types.h>
|
#include <MG_Util/ShaderTranspiler/Types.h>
|
||||||
#include <MG_Backend/BackendObjects.h>
|
#include <MG_Backend/BackendObjects.h>
|
||||||
#include <MG_Impl/Pipe/PipeFill.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 MobileGL::MG_Impl::GLImpl {
|
||||||
// Declared rather than #included from GL_RenderState.h on purpose: that header also declares
|
// 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.
|
// full 64-bit GPU timestamp survives; LWJGL reads it this way.
|
||||||
Int64 timestamp = 0;
|
Int64 timestamp = 0;
|
||||||
if (!MG_Config::Features.DisableTimerQuery) {
|
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);
|
MGP_FILL(GetGpuTimestampNs);
|
||||||
timestamp = getGpuTimestampNs();
|
timestamp = getGpuTimestampNs();
|
||||||
}
|
}
|
||||||
@@ -2265,7 +2279,16 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
case GL_TIMESTAMP: {
|
case GL_TIMESTAMP: {
|
||||||
Int64 timestamp = 0;
|
Int64 timestamp = 0;
|
||||||
if (!MG_Config::Features.DisableTimerQuery) {
|
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);
|
MGP_FILL(GetGpuTimestampNs);
|
||||||
timestamp = getGpuTimestampNs();
|
timestamp = getGpuTimestampNs();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,10 @@
|
|||||||
#include <MG_State/GLState/Core.h>
|
#include <MG_State/GLState/Core.h>
|
||||||
#include <MG_State/GLState/ErrorState/ErrorInfo.h>
|
#include <MG_State/GLState/ErrorState/ErrorInfo.h>
|
||||||
#include <MG_Impl/Pipe/PipeFill.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 MobileGL::MG_Impl::GLImpl {
|
||||||
namespace {
|
namespace {
|
||||||
@@ -478,7 +482,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
const Bool isOcclusionQuery =
|
const Bool isOcclusionQuery =
|
||||||
(target == GL_SAMPLES_PASSED || target == GL_ANY_SAMPLES_PASSED ||
|
(target == GL_SAMPLES_PASSED || target == GL_ANY_SAMPLES_PASSED ||
|
||||||
target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) &&
|
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);
|
const Bool isPipelineStatisticsQuery = IsPipelineStatisticsQueryTarget(target);
|
||||||
if (target != GL_TIME_ELAPSED && !isTransformFeedbackQuery && !isOcclusionQuery &&
|
if (target != GL_TIME_ELAPSED && !isTransformFeedbackQuery && !isOcclusionQuery &&
|
||||||
!isPipelineStatisticsQuery) {
|
!isPipelineStatisticsQuery) {
|
||||||
@@ -528,10 +532,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
} else if (isTransformFeedbackQuery) {
|
} else if (isTransformFeedbackQuery) {
|
||||||
// Prefer real GPU transform-feedback queries (exact with geometry shaders);
|
// Prefer real GPU transform-feedback queries (exact with geometry shaders);
|
||||||
// the CPU accounting delta stays as the fallback when the backend lacks them.
|
// 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;
|
const auto beginXfbPrimitivesQuery = MG_Backend::gBackendFunctionsTable.GL.BeginXfbPrimitivesQuery;
|
||||||
MGP_FILL(BeginXfbPrimitivesQuery);
|
MGP_FILL(BeginXfbPrimitivesQuery);
|
||||||
queryObject->backendHandle =
|
queryObject->backendHandle =
|
||||||
beginXfbPrimitivesQuery ? beginXfbPrimitivesQuery(target == GL_PRIMITIVES_GENERATED) : nullptr;
|
xfbQuerySupported ? beginXfbPrimitivesQuery(target == GL_PRIMITIVES_GENERATED) : nullptr;
|
||||||
queryObject->counterSnapshot = TransformFeedbackCounterForTarget(target);
|
queryObject->counterSnapshot = TransformFeedbackCounterForTarget(target);
|
||||||
queryObject->accountedCaptureDrawSnapshot =
|
queryObject->accountedCaptureDrawSnapshot =
|
||||||
MG_State::pGLContext->GetTransformFeedbackAccountedCaptureDraws();
|
MG_State::pGLContext->GetTransformFeedbackAccountedCaptureDraws();
|
||||||
@@ -541,10 +547,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
MGP_FILL(BeginOcclusionQuery);
|
MGP_FILL(BeginOcclusionQuery);
|
||||||
queryObject->backendHandle = MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery();
|
queryObject->backendHandle = MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery();
|
||||||
} else {
|
} else {
|
||||||
|
const Bool timerQuerySupported =
|
||||||
|
MGL_BACKEND_SLOT_CAP(BeginTimeElapsedQuery, MG_Pipe::kCapTimerQuery);
|
||||||
const auto beginTimeElapsedQuery = MG_Backend::gBackendFunctionsTable.GL.BeginTimeElapsedQuery;
|
const auto beginTimeElapsedQuery = MG_Backend::gBackendFunctionsTable.GL.BeginTimeElapsedQuery;
|
||||||
MGP_FILL(BeginTimeElapsedQuery);
|
MGP_FILL(BeginTimeElapsedQuery);
|
||||||
queryObject->backendHandle =
|
queryObject->backendHandle =
|
||||||
(!TimerQueryDisabled() && beginTimeElapsedQuery) ? beginTimeElapsedQuery() : nullptr;
|
(!TimerQueryDisabled() && timerQuerySupported) ? beginTimeElapsedQuery() : nullptr;
|
||||||
}
|
}
|
||||||
activeQueryId = id;
|
activeQueryId = id;
|
||||||
}
|
}
|
||||||
@@ -555,7 +563,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
const Bool isOcclusionQuery =
|
const Bool isOcclusionQuery =
|
||||||
(target == GL_SAMPLES_PASSED || target == GL_ANY_SAMPLES_PASSED ||
|
(target == GL_SAMPLES_PASSED || target == GL_ANY_SAMPLES_PASSED ||
|
||||||
target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) &&
|
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);
|
const Bool isPipelineStatisticsQuery = IsPipelineStatisticsQueryTarget(target);
|
||||||
if (target != GL_TIME_ELAPSED && !isTransformFeedbackQuery && !isOcclusionQuery &&
|
if (target != GL_TIME_ELAPSED && !isTransformFeedbackQuery && !isOcclusionQuery &&
|
||||||
!isPipelineStatisticsQuery) {
|
!isPipelineStatisticsQuery) {
|
||||||
@@ -657,7 +665,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
|
|
||||||
ResetQueryObjectLocked(queryObject); // discard any previous result
|
ResetQueryObjectLocked(queryObject); // discard any previous result
|
||||||
queryObject->target = target;
|
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);
|
MGP_FILL(QueryCounterTimestamp);
|
||||||
queryObject->backendHandle =
|
queryObject->backendHandle =
|
||||||
(!TimerQueryDisabled() && queryCounterTimestamp) ? queryCounterTimestamp() : nullptr;
|
(!TimerQueryDisabled() && queryCounterTimestamp) ? queryCounterTimestamp() : nullptr;
|
||||||
@@ -782,7 +793,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
if (target == GL_SAMPLES_PASSED || target == GL_ANY_SAMPLES_PASSED ||
|
if (target == GL_SAMPLES_PASSED || target == GL_ANY_SAMPLES_PASSED ||
|
||||||
target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) {
|
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;
|
*params = occlusionSupported ? (target == GL_SAMPLES_PASSED ? 32 : 1) : 0;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,11 @@
|
|||||||
#include <MG_Backend/BackendObjects.h>
|
#include <MG_Backend/BackendObjects.h>
|
||||||
#include <MG_State/GLState/Core.h>
|
#include <MG_State/GLState/Core.h>
|
||||||
#include <MG_Impl/Pipe/PipeFill.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 MobileGL::MG_Impl::GLImpl {
|
||||||
namespace {
|
namespace {
|
||||||
@@ -56,7 +61,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
auto* syncObject = new SyncObject;
|
auto* syncObject = new SyncObject;
|
||||||
syncObject->condition = condition;
|
syncObject->condition = condition;
|
||||||
syncObject->flags = flags;
|
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);
|
MGP_FILL(FenceSync);
|
||||||
syncObject->backendHandle = backendFenceSync();
|
syncObject->backendHandle = backendFenceSync();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,11 @@
|
|||||||
#include <MG_Util/Math/FixedPointConversion.h>
|
#include <MG_Util/Math/FixedPointConversion.h>
|
||||||
#include <MG_State/GLState/TextureState/TextureObjectBuffer.h>
|
#include <MG_State/GLState/TextureState/TextureObjectBuffer.h>
|
||||||
#include <MG_Impl/Pipe/PipeFill.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
|
// 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.
|
// call sites below are declarations only, exactly as the frontend's mutators are.
|
||||||
#include <MG_Pipe/PipeMutation.h>
|
#include <MG_Pipe/PipeMutation.h>
|
||||||
@@ -6534,7 +6539,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
GLenum type, GLsizei bufSize, void* pixels, const char* caller) {
|
GLenum type, GLsizei bufSize, void* pixels, const char* caller) {
|
||||||
if (MG_Backend::pActiveBackendObject != nullptr &&
|
if (MG_Backend::pActiveBackendObject != nullptr &&
|
||||||
MG_Backend::pActiveBackendObject->GetBackendType() == BackendType::DirectVulkan &&
|
MG_Backend::pActiveBackendObject->GetBackendType() == BackendType::DirectVulkan &&
|
||||||
MG_Backend::gBackendFunctionsTable.GL.GetTextureImage != nullptr) {
|
MGL_BACKEND_SLOT_LOCAL(GetTextureImage)) {
|
||||||
MGP_FILL(GetTextureImage);
|
MGP_FILL(GetTextureImage);
|
||||||
MG_Backend::gBackendFunctionsTable.GL.GetTextureImage(textureObject, uploadTarget, level, format, type,
|
MG_Backend::gBackendFunctionsTable.GL.GetTextureImage(textureObject, uploadTarget, level, format, type,
|
||||||
bufSize, pixels);
|
bufSize, pixels);
|
||||||
@@ -6796,7 +6801,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
|
|
||||||
void GetTexImage(GLenum target, GLint level, GLenum format, GLenum type, GLvoid* pixels) {
|
void GetTexImage(GLenum target, GLint level, GLenum format, GLenum type, GLvoid* pixels) {
|
||||||
if (!GetTexImage_State(target, level, format, type, pixels)) return;
|
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);
|
GetTexImage_Backend(target, level, format, type, pixels);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,15 @@
|
|||||||
#include <MG_Pipe/PipeMutation.h>
|
#include <MG_Pipe/PipeMutation.h>
|
||||||
#include <Config.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 <atomic>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
@@ -609,12 +618,38 @@ namespace MobileGL::MG_Pipe {
|
|||||||
}
|
}
|
||||||
} // namespace
|
} // 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() {
|
Bool MGPipeResourceSubsystemEnabled() {
|
||||||
return (MG_Config::Features.PipePush & kMGPipeSubsystemResources) != 0 &&
|
if ((MG_Config::Features.PipePush & kMGPipeSubsystemResources) == 0) return false;
|
||||||
MGPipeGetResourceOps() != nullptr;
|
#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() {
|
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();
|
const MGPipeResourceOps* ops = MGPipeGetResourceOps();
|
||||||
return ops != nullptr && ops->SubDataResident != nullptr;
|
return ops != nullptr && ops->SubDataResident != nullptr;
|
||||||
}
|
}
|
||||||
@@ -936,8 +971,21 @@ namespace MobileGL::MG_Pipe {
|
|||||||
// MGPipeEmitResourceRespecify above uses for buffers) publishes it. Nothing here needs
|
// MGPipeEmitResourceRespecify above uses for buffers) publishes it. Nothing here needs
|
||||||
// to remember the window.
|
// to remember the window.
|
||||||
Bool P4aFamilyHasItsConsumer(Uint64 subsystem) {
|
Bool P4aFamilyHasItsConsumer(Uint64 subsystem) {
|
||||||
return (subsystem & kMGPipeP4aFamilySubsystems) == 0 ||
|
if ((subsystem & kMGPipeP4aFamilySubsystems) == 0) return true;
|
||||||
MGPipeGetResourceOps() != nullptr;
|
#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;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ================================================================================
|
// ================================================================================
|
||||||
|
|||||||
@@ -0,0 +1,332 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Remote/Client/BackendObject_Remote.cpp
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
// P5 package c1. See BackendObject_Remote.h for the three traps and why each is paid here.
|
||||||
|
|
||||||
|
#include "BackendObject_Remote.h"
|
||||||
|
|
||||||
|
#include "CapsMirror.h"
|
||||||
|
#include "ClientSession.h"
|
||||||
|
#include "EmitTables.h"
|
||||||
|
|
||||||
|
#include "../Server/ServerLoop.h"
|
||||||
|
|
||||||
|
#include <MG_Util/Debug/Log.h>
|
||||||
|
|
||||||
|
// Declared rather than included: MG_Backend/BackendObjects.h drags in both concrete backend
|
||||||
|
// objects, and this translation unit must not depend on either - the client role links the same
|
||||||
|
// library but never constructs one.
|
||||||
|
namespace MobileGL::MG_Backend {
|
||||||
|
extern UniquePtr<BackendObject>& pActiveBackendObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace MobileGL::MG_Remote::Client {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// ---- the EGL bridge ---------------------------------------------------------------
|
||||||
|
//
|
||||||
|
// Every one of the nine crosses as a BLOCKING control request on the apply thread,
|
||||||
|
// because every one of them has a return value the caller acts on immediately. v1's
|
||||||
|
// ServerLoop::RunOnApplyThread takes a raw function pointer plus a user pointer rather
|
||||||
|
// than a std::function, deliberately: this path runs at teardown too, and the teardown
|
||||||
|
// path may not allocate (ID-8).
|
||||||
|
//
|
||||||
|
// A NULL SERVER BACKEND IS "false", NOT A CRASH AND NOT A LOCAL SUCCESS. Under inproc
|
||||||
|
// the apply thread creates it during ClientSession::Start, so a null one here means the
|
||||||
|
// bring-up did not complete - and answering `true` would let the frontend believe it
|
||||||
|
// has a context.
|
||||||
|
MG_Backend::BackendObject* ServerBackend() { return Server::ServerLoopInstance().Backend(); }
|
||||||
|
|
||||||
|
struct DisplayArgs {
|
||||||
|
EGLDisplay Dpy;
|
||||||
|
EGLint* Major;
|
||||||
|
EGLint* Minor;
|
||||||
|
Bool Ok;
|
||||||
|
};
|
||||||
|
MobileGLResult RunInitDisplay(void* user) {
|
||||||
|
auto* args = static_cast<DisplayArgs*>(user);
|
||||||
|
args->Ok = ServerBackend()->InitializeEGLDisplay(args->Dpy, args->Major, args->Minor);
|
||||||
|
return MOBILEGL_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct WindowSurfaceArgs {
|
||||||
|
EGLSurface Surface;
|
||||||
|
const MG_Backend::WindowHandle* Handle;
|
||||||
|
Bool Ok;
|
||||||
|
};
|
||||||
|
MobileGLResult RunCreateWindowSurface(void* user) {
|
||||||
|
auto* args = static_cast<WindowSurfaceArgs*>(user);
|
||||||
|
args->Ok = ServerBackend()->CreateEGLWindowSurface(args->Surface, *args->Handle);
|
||||||
|
return MOBILEGL_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ResizeArgs {
|
||||||
|
EGLSurface Surface;
|
||||||
|
Uint32 Width;
|
||||||
|
Uint32 Height;
|
||||||
|
Bool Ok;
|
||||||
|
};
|
||||||
|
MobileGLResult RunResize(void* user) {
|
||||||
|
auto* args = static_cast<ResizeArgs*>(user);
|
||||||
|
args->Ok = ServerBackend()->ResizeEGLWindowSurface(args->Surface, args->Width, args->Height);
|
||||||
|
return MOBILEGL_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PbufferArgs {
|
||||||
|
EGLSurface Surface;
|
||||||
|
EGLint Width;
|
||||||
|
EGLint Height;
|
||||||
|
Bool Ok;
|
||||||
|
};
|
||||||
|
MobileGLResult RunCreatePbuffer(void* user) {
|
||||||
|
auto* args = static_cast<PbufferArgs*>(user);
|
||||||
|
args->Ok = ServerBackend()->CreateEGLPbufferSurface(args->Surface, args->Width, args->Height);
|
||||||
|
return MOBILEGL_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct MakeCurrentArgs {
|
||||||
|
EGLDisplay Dpy;
|
||||||
|
EGLSurface Draw;
|
||||||
|
EGLSurface Read;
|
||||||
|
EGLContext Ctx;
|
||||||
|
Bool Ok;
|
||||||
|
};
|
||||||
|
MobileGLResult RunMakeCurrent(void* user) {
|
||||||
|
auto* args = static_cast<MakeCurrentArgs*>(user);
|
||||||
|
args->Ok = ServerBackend()->MakeEGLCurrent(args->Dpy, args->Draw, args->Read, args->Ctx);
|
||||||
|
return MOBILEGL_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SwapIntervalArgs {
|
||||||
|
Int Interval;
|
||||||
|
};
|
||||||
|
MobileGLResult RunSwapInterval(void* user) {
|
||||||
|
ServerBackend()->SetEGLSwapInterval(static_cast<SwapIntervalArgs*>(user)->Interval);
|
||||||
|
return MOBILEGL_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SurfaceArgs {
|
||||||
|
EGLSurface Surface;
|
||||||
|
};
|
||||||
|
MobileGLResult RunReleaseSurface(void* user) {
|
||||||
|
ServerBackend()->ReleaseEGLSurface(static_cast<SurfaceArgs*>(user)->Surface);
|
||||||
|
return MOBILEGL_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
MobileGLResult RunReleaseResources(void*) {
|
||||||
|
ServerBackend()->ReleaseEGLResources();
|
||||||
|
return MOBILEGL_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Runs `work` on the apply thread if there is a server backend to run it against, and
|
||||||
|
// says so by name when there is not.
|
||||||
|
Bool ForwardToApplyThread(const char* what, Server::ServerLoop::ControlWork work, void* user) {
|
||||||
|
if (ServerBackend() == nullptr) {
|
||||||
|
MGLOG_E("MG_Remote client: %s has no server backend to forward to - the apply "
|
||||||
|
"thread's bring-up did not complete, and answering success here would "
|
||||||
|
"tell the frontend it has a context it does not have",
|
||||||
|
what);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return Server::ServerLoopInstance().RunOnApplyThread(work, user) == MOBILEGL_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
// CapsMirror's adoption hook. A free function because the hook is a raw function
|
||||||
|
// pointer (ID-8: this can fire on a path that must not allocate), and it reaches the
|
||||||
|
// live object through pActiveBackendObject rather than through a second global.
|
||||||
|
void OnCapsAdopted() {
|
||||||
|
auto* self = dynamic_cast<BackendObject_Remote*>(MG_Backend::pActiveBackendObject.get());
|
||||||
|
if (self != nullptr) self->RefreshFormatCapabilities();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
BackendObject_Remote::BackendObject_Remote() {
|
||||||
|
// Installed in the constructor rather than at the first snapshot, because the first
|
||||||
|
// snapshot has usually already arrived by then: ClientSession::Start pumps the control
|
||||||
|
// plane during the handshake, and MG_Backend::Init() constructs this object after it.
|
||||||
|
// RefreshFormatCapabilities below picks up that already-adopted generation.
|
||||||
|
SetCapsAdoptedHook(&OnCapsAdopted);
|
||||||
|
RefreshFormatCapabilities();
|
||||||
|
}
|
||||||
|
|
||||||
|
BackendObject_Remote::~BackendObject_Remote() {
|
||||||
|
// The hook holds a raw function pointer, not a pointer to this - but the function it
|
||||||
|
// names reaches pActiveBackendObject, which is being destroyed right now. Uninstall.
|
||||||
|
SetCapsAdoptedHook(nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BackendObject_Remote::RefreshFormatCapabilities() {
|
||||||
|
CapsMirror& mirror = CapsMirrorInstance();
|
||||||
|
if (!mirror.Valid() || mirror.Generation() == m_formatsGeneration) return;
|
||||||
|
// TRAP 2. GetFormatCapabilities() is non-virtual and hands back this member, so the
|
||||||
|
// only way a remote object can answer it is to fill it.
|
||||||
|
MutableFormatCapabilities() = mirror.Formats();
|
||||||
|
m_formatsGeneration = mirror.Generation();
|
||||||
|
MGLOG_I("MG_Remote client: format capabilities filled from caps mirror generation %llu",
|
||||||
|
static_cast<unsigned long long>(m_formatsGeneration));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- the eight pure virtuals ----------------------------------------------------------
|
||||||
|
|
||||||
|
void BackendObject_Remote::Initialize() {
|
||||||
|
// NOT FORWARDED. The server's own BackendObject_DirectGLES is created and initialised
|
||||||
|
// by v1's ServerLoop, on the apply thread, before this object exists; forwarding here
|
||||||
|
// would be a second Initialize() on an already-initialised backend. What this call
|
||||||
|
// does is drain whatever the handshake left and take the caps that came with it.
|
||||||
|
if (ClientSession* session = ClientSession::Active()) {
|
||||||
|
session->PumpControlPlane();
|
||||||
|
}
|
||||||
|
RefreshFormatCapabilities();
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool BackendObject_Remote::InitCapabilities() {
|
||||||
|
// Reached from the base class's MakeEGLCurrent, lazily, once per surface lifetime
|
||||||
|
// (BackendObject.cpp:341-347). By this point MakeEGLCurrent below has already run the
|
||||||
|
// SERVER's MakeEGLCurrent on the apply thread, whose own base class ran the server
|
||||||
|
// backend's InitCapabilities and whose ServerSession re-published the snapshot - so
|
||||||
|
// the client's job here is to pick that snapshot up. R-12: re-arrival IS the
|
||||||
|
// invalidation, and this is the second place it is drained (the other is Present).
|
||||||
|
ClientSession* session = ClientSession::Active();
|
||||||
|
if (session == nullptr) {
|
||||||
|
MGLOG_E("MG_Remote client: InitCapabilities with no session");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
session->PumpControlPlane();
|
||||||
|
RefreshFormatCapabilities();
|
||||||
|
// A PLACEHOLDER MIRROR IS A FAILURE HERE, unlike at startup. LogBackendInfo reading a
|
||||||
|
// placeholder costs one wrong log line; a context going current on one costs every
|
||||||
|
// limit, every advertised extension and the compile-env fingerprint.
|
||||||
|
if (!CapsMirrorInstance().Valid()) {
|
||||||
|
MGLOG_E("MG_Remote client: InitCapabilities found no CapsSnapshot - the server has "
|
||||||
|
"not published one. Going current on a placeholder caps mirror would put "
|
||||||
|
"default limits into every glGetIntegerv answer and into the compile env");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool BackendObject_Remote::InitWindowSurface() {
|
||||||
|
// The real surface work happened on the apply thread inside the server backend's own
|
||||||
|
// ActivateEGLSurface; this is the client's half of the base state machine and has
|
||||||
|
// nothing of its own to do.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool BackendObject_Remote::InitPbufferSurface(EGLint, EGLint) { return true; }
|
||||||
|
|
||||||
|
const RendererInfo& BackendObject_Remote::GetRendererInfo() const {
|
||||||
|
// TRAP 1: a reference, so the storage is the mirror's and not a temporary's.
|
||||||
|
return CapsMirrorInstance().Renderer();
|
||||||
|
}
|
||||||
|
|
||||||
|
String BackendObject_Remote::GetBackendAPIVersionString() const {
|
||||||
|
return CapsMirrorInstance().ApiVersion();
|
||||||
|
}
|
||||||
|
|
||||||
|
const MG_Backend::GlobalBackendFunctionsTable& BackendObject_Remote::GetBackendFunctions() const {
|
||||||
|
return RemoteEmitTable();
|
||||||
|
}
|
||||||
|
|
||||||
|
const MG_Backend::DynamicBackendParameters& BackendObject_Remote::GetDynamicParameters() const {
|
||||||
|
return CapsMirrorInstance().Dynamic();
|
||||||
|
}
|
||||||
|
|
||||||
|
BackendType BackendObject_Remote::GetBackendType() const {
|
||||||
|
// TRAP 3: the SERVER's backend, never a new enumerator.
|
||||||
|
return CapsMirrorInstance().Backend();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- the nine EGL lifecycle virtuals ---------------------------------------------------
|
||||||
|
//
|
||||||
|
// FORWARD FIRST, THEN RUN THE BASE. The server has to own the context before the client's
|
||||||
|
// base class latches "the surface is initialised" and calls InitCapabilities, because
|
||||||
|
// InitCapabilities' answer comes from a snapshot the server can only publish once its own
|
||||||
|
// InitCapabilities has run.
|
||||||
|
|
||||||
|
Bool BackendObject_Remote::InitializeEGLDisplay(EGLDisplay dpy, EGLint* major, EGLint* minor) {
|
||||||
|
DisplayArgs args{dpy, major, minor, false};
|
||||||
|
if (!ForwardToApplyThread("InitializeEGLDisplay", &RunInitDisplay, &args) || !args.Ok) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return MG_Backend::BackendObject::InitializeEGLDisplay(dpy, major, minor);
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool BackendObject_Remote::CreateEGLWindowSurface(EGLSurface surface,
|
||||||
|
const MG_Backend::WindowHandle& handle) {
|
||||||
|
WindowSurfaceArgs args{surface, &handle, false};
|
||||||
|
if (!ForwardToApplyThread("CreateEGLWindowSurface", &RunCreateWindowSurface, &args) ||
|
||||||
|
!args.Ok) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return MG_Backend::BackendObject::CreateEGLWindowSurface(surface, handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool BackendObject_Remote::ResizeEGLWindowSurface(EGLSurface surface, Uint32 width, Uint32 height) {
|
||||||
|
ResizeArgs args{surface, width, height, false};
|
||||||
|
if (!ForwardToApplyThread("ResizeEGLWindowSurface", &RunResize, &args) || !args.Ok) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return MG_Backend::BackendObject::ResizeEGLWindowSurface(surface, width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool BackendObject_Remote::CreateEGLPbufferSurface(EGLSurface surface, EGLint width, EGLint height) {
|
||||||
|
PbufferArgs args{surface, width, height, false};
|
||||||
|
if (!ForwardToApplyThread("CreateEGLPbufferSurface", &RunCreatePbuffer, &args) || !args.Ok) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return MG_Backend::BackendObject::CreateEGLPbufferSurface(surface, width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool BackendObject_Remote::MakeEGLCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read,
|
||||||
|
EGLContext ctx) {
|
||||||
|
MakeCurrentArgs args{dpy, draw, read, ctx, false};
|
||||||
|
if (!ForwardToApplyThread("MakeEGLCurrent", &RunMakeCurrent, &args) || !args.Ok) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// AND ONLY NOW the client's own bookkeeping, which is what calls InitCapabilities.
|
||||||
|
return MG_Backend::BackendObject::MakeEGLCurrent(dpy, draw, read, ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool BackendObject_Remote::SwapEGLBuffers(EGLDisplay dpy, EGLSurface draw) {
|
||||||
|
// NOT FORWARDED, and this is the one that must not be. The base implementation's last
|
||||||
|
// act is GetBackendFunctions().Present() (BackendObject.cpp:396) - which is this
|
||||||
|
// client's class-B Present EMITTER, the only route by which Present is reached at all
|
||||||
|
// (it has zero MG_Impl call sites). Forwarding would present on the server directly and
|
||||||
|
// put no record on the wire, which is the shape every gate in this phase exists to
|
||||||
|
// catch.
|
||||||
|
return MG_Backend::BackendObject::SwapEGLBuffers(dpy, draw);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BackendObject_Remote::SetEGLSwapInterval(Int interval) {
|
||||||
|
// OVERRIDDEN BECAUSE THE BASE WOULD FATAL. BackendObject.cpp:402 null-checks
|
||||||
|
// GetBackendFunctions().SetSwapInterval and calls it when non-null - one of the 41
|
||||||
|
// null checks R-4 turns into "always supported" - and SetSwapInterval is class C, so
|
||||||
|
// the base implementation would abort on every eglSwapInterval. The answer is the
|
||||||
|
// caps-mirror-read rule's general shape: the question "can the presentation path take
|
||||||
|
// an interval" belongs to the server, so it is asked of the server.
|
||||||
|
SwapIntervalArgs args{interval};
|
||||||
|
ForwardToApplyThread("SetEGLSwapInterval", &RunSwapInterval, &args);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BackendObject_Remote::ReleaseEGLSurface(EGLSurface surface) {
|
||||||
|
SurfaceArgs args{surface};
|
||||||
|
ForwardToApplyThread("ReleaseEGLSurface", &RunReleaseSurface, &args);
|
||||||
|
MG_Backend::BackendObject::ReleaseEGLSurface(surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BackendObject_Remote::ReleaseEGLResources() {
|
||||||
|
// BLOCKING BY CONTRACT (ServerLoop.h's header note): MobileGL::Destroy()
|
||||||
|
// (MobileGL/Init.cpp:68) walks on the moment this returns, and the server still holds
|
||||||
|
// the context until the apply thread has run it.
|
||||||
|
ForwardToApplyThread("ReleaseEGLResources", &RunReleaseResources, nullptr);
|
||||||
|
MG_Backend::BackendObject::ReleaseEGLResources();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace MobileGL::MG_Remote::Client
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Remote/Client/BackendObject_Remote.h
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
// THE CLIENT ROLE'S BackendObject. Owner: package c1.
|
||||||
|
//
|
||||||
|
// MG_Backend::Init() installs one of these into pActiveBackendObject when the transport
|
||||||
|
// resolved (the hook is v1's; the object is c1's, table 3). It is NOT a backend: it owns no
|
||||||
|
// context, no driver and no GL state. It is the frontend's single answer to four questions -
|
||||||
|
// "what can the device do", "what backend is it", "what table do I call", and the nine EGL
|
||||||
|
// lifecycle calls - and each of the four is answered somewhere that is not here.
|
||||||
|
//
|
||||||
|
// THE THREE TRAPS THE SCOUT FOUND, AND WHERE EACH IS PAID:
|
||||||
|
//
|
||||||
|
// 1. GetRendererInfo() RETURNS A REFERENCE (BackendObject.h:590), so the object cannot
|
||||||
|
// synthesise one per call. CapsMirror owns the storage; this class forwards. And
|
||||||
|
// LogBackendInfo() reads it at MG_Backend/Init.cpp:21, DURING MG_Backend::Init(), before
|
||||||
|
// any surface exists - so the mirror answers a placeholder and P5 accepts one imprecise
|
||||||
|
// startup log line. MG_Backend::Init() is NOT restructured (scout-caps-reply §1.2 (a)).
|
||||||
|
//
|
||||||
|
// 2. GetFormatCapabilities() IS NOT VIRTUAL (BackendObject.h:594). It returns the base class's
|
||||||
|
// own m_formatCapabilities member, so there is no accessor to override: the cache has to be
|
||||||
|
// PUSHED into that member, and the only moment this object can know to is when a snapshot
|
||||||
|
// lands. CapsMirror's adoption hook is that moment.
|
||||||
|
//
|
||||||
|
// 3. GetBackendType() MUST ANSWER THE SERVER'S BACKEND. There is no "Remote" enumerator and
|
||||||
|
// there must not be one: GL_Framebuffer.cpp:47, GL_Texture.cpp:6536 and CompileEnv.cpp:122
|
||||||
|
// switch on this value, and a value they do not know takes a WRONG ARM rather than failing.
|
||||||
|
//
|
||||||
|
// THE EGL VIRTUALS ARE BOTH FORWARDED AND KEPT. The base class runs a real state machine -
|
||||||
|
// surface registration, per-thread current-context bookkeeping, the lazy InitCapabilities latch,
|
||||||
|
// and SwapEGLBuffers' route into GetBackendFunctions().Present() - and the client needs all of
|
||||||
|
// it, because Present is a class-B emitter reached through exactly that route (the verb census's
|
||||||
|
// trap 3: Present has zero MG_Impl call sites). So each override does BOTH: it runs the real
|
||||||
|
// EGL work on the apply thread, through v1's ServerLoop::RunOnApplyThread, and then lets the
|
||||||
|
// base class keep the client-side books.
|
||||||
|
//
|
||||||
|
// SetEGLSwapInterval IS THE ONE THAT MUST NOT REACH THE TABLE. The base implementation
|
||||||
|
// null-checks GetBackendFunctions().SetSwapInterval (BackendObject.cpp:402) - one of the 41
|
||||||
|
// null checks R-4 turns into "always supported" - and SetSwapInterval is class C, so the base
|
||||||
|
// implementation would Fatal on every eglSwapInterval. It is overridden to forward instead,
|
||||||
|
// which is the caps-mirror-read rule's shape for a slot whose answer is "ask the server".
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <Includes.h>
|
||||||
|
|
||||||
|
#include <MG_Backend/BackendObject.h>
|
||||||
|
|
||||||
|
namespace MobileGL::MG_Remote::Client {
|
||||||
|
|
||||||
|
class BackendObject_Remote final : public MG_Backend::BackendObject {
|
||||||
|
public:
|
||||||
|
BackendObject_Remote();
|
||||||
|
~BackendObject_Remote() override;
|
||||||
|
|
||||||
|
// ---- the eight pure virtuals ------------------------------------------------------
|
||||||
|
void Initialize() override;
|
||||||
|
Bool InitCapabilities() override;
|
||||||
|
Bool InitWindowSurface() override;
|
||||||
|
const RendererInfo& GetRendererInfo() const override;
|
||||||
|
String GetBackendAPIVersionString() const override;
|
||||||
|
const MG_Backend::GlobalBackendFunctionsTable& GetBackendFunctions() const override;
|
||||||
|
const MG_Backend::DynamicBackendParameters& GetDynamicParameters() const override;
|
||||||
|
BackendType GetBackendType() const override;
|
||||||
|
|
||||||
|
// ---- the nine EGL lifecycle virtuals ---------------------------------------------
|
||||||
|
Bool InitializeEGLDisplay(EGLDisplay dpy, EGLint* major, EGLint* minor) override;
|
||||||
|
Bool CreateEGLWindowSurface(EGLSurface surface, const MG_Backend::WindowHandle& handle) override;
|
||||||
|
Bool ResizeEGLWindowSurface(EGLSurface surface, Uint32 width, Uint32 height) override;
|
||||||
|
Bool CreateEGLPbufferSurface(EGLSurface surface, EGLint width, EGLint height) override;
|
||||||
|
Bool MakeEGLCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx) override;
|
||||||
|
Bool SwapEGLBuffers(EGLDisplay dpy, EGLSurface draw) override;
|
||||||
|
void SetEGLSwapInterval(Int interval) override;
|
||||||
|
void ReleaseEGLSurface(EGLSurface surface) override;
|
||||||
|
void ReleaseEGLResources() override;
|
||||||
|
|
||||||
|
// Copies the caps mirror's FormatCapabilityCache into the base class's
|
||||||
|
// m_formatCapabilities. Public because CapsMirror's adoption hook is a free function
|
||||||
|
// and this is what it calls; it is the whole of trap 2's answer.
|
||||||
|
void RefreshFormatCapabilities();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
Bool InitPbufferSurface(EGLint width, EGLint height) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
// The generation of the snapshot m_formatCapabilities was filled from. Exposed only
|
||||||
|
// through the log line on a refresh: a cache that silently stopped tracking the mirror
|
||||||
|
// is exactly the shape trap 2 exists to prevent.
|
||||||
|
Uint64 m_formatsGeneration = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace MobileGL::MG_Remote::Client
|
||||||
@@ -6,49 +6,161 @@
|
|||||||
// SPDX-License-Identifier: LGPL-3.0-only
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
// End of Source File Header
|
// End of Source File Header
|
||||||
|
|
||||||
// P5 c0 stubs for package c1. Every body is MGLOG_F + std::abort and never a silent no-op: a
|
// P5 package c1.
|
||||||
// caps accessor that answers a default is how a split lane runs on the wrong device's limits.
|
//
|
||||||
|
// EVERY ACCESSOR ANSWERS BEFORE THE FIRST SNAPSHOT, AND THAT IS THE RULING, NOT A WEAKENING.
|
||||||
|
// c0's stubs aborted on all but Valid()/Generation() so that nothing could answer from a zeroed
|
||||||
|
// mirror by accident. But scout-caps-reply §1.2's option (a) - the one the brief adopts - says
|
||||||
|
// LogBackendInfo() reads GetRendererInfo() at MG_Backend/Init.cpp:21, during MG_Backend::Init(),
|
||||||
|
// BEFORE any surface exists, and that P5 accepts one imprecise startup log line rather than
|
||||||
|
// restructure MG_Backend::Init(). An abort there is not a stricter mirror; it is a process that
|
||||||
|
// cannot start. So the mirror answers a placeholder and SAYS SO, once per accessor: a wrong
|
||||||
|
// number that announced itself is auditable, and the announcement is what a case asserts on.
|
||||||
|
//
|
||||||
|
// THE PLACEHOLDER IS A REAL OBJECT, NOT A TEMPORARY. GetRendererInfo() and
|
||||||
|
// GetDynamicParameters() return const references, so the storage has to outlive every caller;
|
||||||
|
// the members below are it, default-constructed, and Adopt() replaces them in place.
|
||||||
|
|
||||||
#include "CapsMirror.h"
|
#include "CapsMirror.h"
|
||||||
|
|
||||||
#include <MG_Util/Debug/Log.h>
|
#include "../CapsCodec.h"
|
||||||
|
|
||||||
#include <cstdlib>
|
#include <MG_State/GLState/Core.h>
|
||||||
|
#include <MG_Util/Debug/Log.h>
|
||||||
|
|
||||||
namespace MobileGL::MG_Remote::Client {
|
namespace MobileGL::MG_Remote::Client {
|
||||||
|
|
||||||
#define MGP5_C0_STUB(what) \
|
namespace {
|
||||||
do { \
|
CapsAdoptedHook g_capsAdoptedHook = nullptr;
|
||||||
MGLOG_F("MGPipe: Fatal{UnimplementedCapsMirror, \"%s\"} - P5 package c1 has not landed " \
|
|
||||||
"this yet; c0 shipped the signature only", \
|
|
||||||
what); \
|
|
||||||
std::abort(); \
|
|
||||||
} while (0)
|
|
||||||
|
|
||||||
void CapsMirror::Adopt(const MG_Pipe::MGPCaps&, const MG_Backend::FormatCapabilityCache&,
|
// R-8's negative control, counted at the funnel. Not atomics: every reader of this
|
||||||
const RendererInfo&, const String&, BackendType) {
|
// mirror is the GL thread, by the same argument that makes one gPipeInputs legal.
|
||||||
MGP5_C0_STUB("CapsMirror::Adopt");
|
Uint64 g_consumerRefusals = 0;
|
||||||
|
Uint64 g_lastRefusedSubsystem = 0;
|
||||||
|
Uint64 g_refusedSubsystemsLogged = 0;
|
||||||
|
|
||||||
|
// One line per accessor, once, and only while the mirror is a placeholder. MGLOG_W_ONCE
|
||||||
|
// keys on the call site, so each accessor gets its own line and a hot getter cannot
|
||||||
|
// flood the log.
|
||||||
|
void WarnPlaceholder(const char* what, Uint64 generation) {
|
||||||
|
if (generation != 0) return;
|
||||||
|
MGLOG_W("MG_Remote client: CapsMirror::%s read before the first CapsSnapshot - "
|
||||||
|
"answering a PLACEHOLDER. Expected exactly once at startup, from "
|
||||||
|
"LogBackendInfo (MG_Backend/Init.cpp:21); anything later means a caps read "
|
||||||
|
"beat the handshake",
|
||||||
|
what);
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void CapsMirror::Adopt(const MG_Pipe::MGPCaps& caps, const MG_Backend::FormatCapabilityCache& formats,
|
||||||
|
const RendererInfo& renderer, const String& apiVersion,
|
||||||
|
BackendType backend) {
|
||||||
|
m_caps = caps;
|
||||||
|
// The two blobrefs inside MGPCaps name SEG_STAGE runs that belong to the SERVER's
|
||||||
|
// encoder and are meaningless on this side; the decoded objects beside them are the
|
||||||
|
// answer. Clearing them is not tidiness - a later reader that resolved one would read
|
||||||
|
// whatever the stage allocator has since put there.
|
||||||
|
m_caps.FormatCapabilities = MG_Pipe::MGPBlobRef{};
|
||||||
|
m_caps.RendererInfo = MG_Pipe::MGPBlobRef{};
|
||||||
|
m_formats = formats;
|
||||||
|
m_renderer = renderer;
|
||||||
|
m_apiVersion = apiVersion;
|
||||||
|
m_backend = backend;
|
||||||
|
++m_generation;
|
||||||
|
|
||||||
|
// R-12: RE-ARRIVAL IS THE INVALIDATION, and this is the one place that acts on it.
|
||||||
|
// GLContext::GetCompileEnv() memoises on the raw pActiveBackendObject pointer
|
||||||
|
// (Core.cpp:34); under split that pointer is the single long-lived BackendObject_Remote
|
||||||
|
// and NEVER changes, so without this line the compile env, its preprocess memos and the
|
||||||
|
// advertised-extension list stay stale for ever after a server-side InitCapabilities
|
||||||
|
// re-run. DirectVulkan already spells the monolith half of this exactly this way
|
||||||
|
// (BackendObject_DirectVulkan.cpp:390).
|
||||||
|
if (m_generation > 1 && MG_State::pGLContext != nullptr) {
|
||||||
|
MG_State::pGLContext->InvalidateCompileEnv();
|
||||||
|
}
|
||||||
|
MGLOG_I("MG_Remote client: CapsMirror generation %llu adopted (backend=%d, callMask=0x%llx)",
|
||||||
|
static_cast<unsigned long long>(m_generation), static_cast<int>(m_backend),
|
||||||
|
static_cast<unsigned long long>(m_caps.CallMask));
|
||||||
|
|
||||||
|
// LAST, and after the generation has moved: the hook reads this mirror.
|
||||||
|
if (g_capsAdoptedHook != nullptr) g_capsAdoptedHook();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Not stubs: the two the placeholder contract above promises are readable before the first
|
void SetCapsAdoptedHook(CapsAdoptedHook hook) { g_capsAdoptedHook = hook; }
|
||||||
// snapshot. Everything else aborts, so nothing can accidentally answer from a zeroed mirror.
|
|
||||||
Bool CapsMirror::Valid() const { return m_generation != 0; }
|
Bool CapsMirror::Valid() const { return m_generation != 0; }
|
||||||
Uint64 CapsMirror::Generation() const { return m_generation; }
|
Uint64 CapsMirror::Generation() const { return m_generation; }
|
||||||
|
|
||||||
const RendererInfo& CapsMirror::Renderer() const { MGP5_C0_STUB("CapsMirror::Renderer"); }
|
const RendererInfo& CapsMirror::Renderer() const {
|
||||||
|
WarnPlaceholder("Renderer", m_generation);
|
||||||
|
return m_renderer;
|
||||||
|
}
|
||||||
|
|
||||||
const MG_Backend::DynamicBackendParameters& CapsMirror::Dynamic() const {
|
const MG_Backend::DynamicBackendParameters& CapsMirror::Dynamic() const {
|
||||||
MGP5_C0_STUB("CapsMirror::Dynamic");
|
WarnPlaceholder("Dynamic", m_generation);
|
||||||
|
return m_caps.Dynamic;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MG_Backend::FormatCapabilityCache& CapsMirror::Formats() const {
|
const MG_Backend::FormatCapabilityCache& CapsMirror::Formats() const {
|
||||||
MGP5_C0_STUB("CapsMirror::Formats");
|
WarnPlaceholder("Formats", m_generation);
|
||||||
|
return m_formats;
|
||||||
}
|
}
|
||||||
const String& CapsMirror::ApiVersion() const { MGP5_C0_STUB("CapsMirror::ApiVersion"); }
|
|
||||||
BackendType CapsMirror::Backend() const { MGP5_C0_STUB("CapsMirror::Backend"); }
|
const String& CapsMirror::ApiVersion() const {
|
||||||
Uint64 CapsMirror::CallMask() const { MGP5_C0_STUB("CapsMirror::CallMask"); }
|
WarnPlaceholder("ApiVersion", m_generation);
|
||||||
Bool CapsMirror::HasCap(MG_Pipe::MGPCapBit) const { MGP5_C0_STUB("CapsMirror::HasCap"); }
|
return m_apiVersion;
|
||||||
Bool CapsMirror::ServerConsumes(Uint64) const { MGP5_C0_STUB("CapsMirror::ServerConsumes"); }
|
}
|
||||||
|
|
||||||
|
BackendType CapsMirror::Backend() const {
|
||||||
|
WarnPlaceholder("Backend", m_generation);
|
||||||
|
return m_backend;
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint64 CapsMirror::CallMask() const { return m_caps.CallMask; }
|
||||||
|
|
||||||
|
Bool CapsMirror::HasCap(MG_Pipe::MGPCapBit bit) const {
|
||||||
|
return (m_caps.CallMask & static_cast<Uint64>(bit)) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool CapsMirror::ServerConsumes(Uint64 subsystemBit) const {
|
||||||
|
// THE ONLY LEGAL CLIENT-SIDE SPELLING (R-8). Never MGPipeGetResourceOps(): that is the
|
||||||
|
// SERVER's registration, a process-wide global, right by accident under inproc and null
|
||||||
|
// under spawn - and a null read there silently stops five record families while the
|
||||||
|
// client goes on clearing its dirty flags, which is ID-39's 66 lost uploads with a wire
|
||||||
|
// in between. Folded through CapsCodec.h's helper rather than shifted here, because two
|
||||||
|
// spellings of one bit layout is how the two sides come to disagree about it.
|
||||||
|
//
|
||||||
|
// A PLACEHOLDER MIRROR CONSUMES NOTHING, and that is the safe direction: with no
|
||||||
|
// snapshot the mask is 0, every family answers "no consumer", the client emits nothing
|
||||||
|
// and the LEGACY PULL PATH runs untouched. The unsafe direction - emitting to a server
|
||||||
|
// that has no consumer - is the one that loses uploads.
|
||||||
|
const Bool consumes = MGCapsServerConsumes(m_caps.CallMask, subsystemBit);
|
||||||
|
if (!consumes) {
|
||||||
|
++g_consumerRefusals;
|
||||||
|
g_lastRefusedSubsystem = subsystemBit;
|
||||||
|
if ((g_refusedSubsystemsLogged & subsystemBit) == 0) {
|
||||||
|
g_refusedSubsystemsLogged |= subsystemBit;
|
||||||
|
MGLOG_W("MG_Remote client: the server does not consume MGPipe subsystem 0x%llx - "
|
||||||
|
"this family emits NOTHING and the legacy pull path runs for it "
|
||||||
|
"(R-8). callMask=0x%llx, caps generation %llu",
|
||||||
|
static_cast<unsigned long long>(subsystemBit),
|
||||||
|
static_cast<unsigned long long>(m_caps.CallMask),
|
||||||
|
static_cast<unsigned long long>(m_generation));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return consumes;
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint64 ConsumerRefusals() { return g_consumerRefusals; }
|
||||||
|
Uint64 LastRefusedSubsystem() { return g_lastRefusedSubsystem; }
|
||||||
|
void ResetConsumerRefusalsForTest() {
|
||||||
|
g_consumerRefusals = 0;
|
||||||
|
g_lastRefusedSubsystem = 0;
|
||||||
|
g_refusedSubsystemsLogged = 0;
|
||||||
|
}
|
||||||
|
|
||||||
Bool CapsMirror::PrefersCpuXfbPrimitiveAccounting() const {
|
Bool CapsMirror::PrefersCpuXfbPrimitiveAccounting() const {
|
||||||
MGP5_C0_STUB("CapsMirror::PrefersCpuXfbPrimitiveAccounting");
|
return HasCap(MG_Pipe::kCapCpuXfbPrimitiveAccounting);
|
||||||
}
|
}
|
||||||
|
|
||||||
CapsMirror& CapsMirrorInstance() {
|
CapsMirror& CapsMirrorInstance() {
|
||||||
@@ -58,6 +170,4 @@ namespace MobileGL::MG_Remote::Client {
|
|||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
#undef MGP5_C0_STUB
|
|
||||||
|
|
||||||
} // namespace MobileGL::MG_Remote::Client
|
} // namespace MobileGL::MG_Remote::Client
|
||||||
|
|||||||
@@ -97,4 +97,28 @@ namespace MobileGL::MG_Remote::Client {
|
|||||||
// reach pipe or backend state from an exit handler.
|
// reach pipe or backend state from an exit handler.
|
||||||
CapsMirror& CapsMirrorInstance();
|
CapsMirror& CapsMirrorInstance();
|
||||||
|
|
||||||
|
// ---- c1's addition to c0's signature block -----------------------------------------
|
||||||
|
//
|
||||||
|
// WHY A HOOK AND NOT A READ. BackendObject::GetFormatCapabilities() is NON-VIRTUAL
|
||||||
|
// (BackendObject.h:594) and returns the base class's own m_formatCapabilities member, so a
|
||||||
|
// remote backend object cannot answer it lazily from the mirror - it has to PUSH the cache
|
||||||
|
// into that member, and the only moment it can know to is when a snapshot lands. A raw
|
||||||
|
// function pointer rather than std::function, for ID-8's reason: this can fire on paths
|
||||||
|
// that must not allocate. One hook, installed by BackendObject_Remote's constructor.
|
||||||
|
using CapsAdoptedHook = void (*)();
|
||||||
|
void SetCapsAdoptedHook(CapsAdoptedHook hook);
|
||||||
|
|
||||||
|
// R-8's NEGATIVE CONTROL NEEDS TO SEE THE WITHHOLDING, NOT INFER IT FROM AN ABSENCE.
|
||||||
|
// "the client emits nothing for a family the server does not consume" is, on its own,
|
||||||
|
// indistinguishable from "the client emits nothing because nothing called it" - and the
|
||||||
|
// second is how a gate goes green for the wrong reason. So the one funnel that answers the
|
||||||
|
// question counts its own refusals and names the family, once per family, in the log.
|
||||||
|
//
|
||||||
|
// Counted inside ServerConsumes(), which is R-8's only legal spelling, so a refusal that
|
||||||
|
// happened cannot fail to be counted and a count that moved cannot have come from anywhere
|
||||||
|
// else.
|
||||||
|
Uint64 ConsumerRefusals();
|
||||||
|
Uint64 LastRefusedSubsystem();
|
||||||
|
void ResetConsumerRefusalsForTest();
|
||||||
|
|
||||||
} // namespace MobileGL::MG_Remote::Client
|
} // namespace MobileGL::MG_Remote::Client
|
||||||
|
|||||||
@@ -18,9 +18,12 @@
|
|||||||
#include "../Transport/InProcessTransport.h"
|
#include "../Transport/InProcessTransport.h"
|
||||||
|
|
||||||
#include <MGGitHash.h>
|
#include <MGGitHash.h>
|
||||||
|
#include <MG_Pipe/MGPipeCallbacks.h>
|
||||||
#include <MG_Util/Debug/Log.h>
|
#include <MG_Util/Debug/Log.h>
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
|
#include <cstring>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
namespace MobileGL::MG_Remote::Client {
|
namespace MobileGL::MG_Remote::Client {
|
||||||
@@ -100,6 +103,29 @@ namespace MobileGL::MG_Remote::Client {
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- c1: the barrier's two flags ------------------------------------------------
|
||||||
|
//
|
||||||
|
// thread_local for the client's own "am I waiting", a shared atomic for "is the apply
|
||||||
|
// thread inside the applier" - see ClientSession::InBarrierWait's note.
|
||||||
|
thread_local Bool g_inBarrierWait = false;
|
||||||
|
std::atomic<Bool> g_applyThreadInsideApplier{false};
|
||||||
|
|
||||||
|
struct BarrierWaitScope {
|
||||||
|
BarrierWaitScope() { g_inBarrierWait = true; }
|
||||||
|
~BarrierWaitScope() { g_inBarrierWait = false; }
|
||||||
|
BarrierWaitScope(const BarrierWaitScope&) = delete;
|
||||||
|
BarrierWaitScope& operator=(const BarrierWaitScope&) = delete;
|
||||||
|
};
|
||||||
|
|
||||||
|
// BOUNDED, AND THE BOUND IS GENEROUS RATHER THAN TIGHT. The barrier is a correctness
|
||||||
|
// device, not a watchdog: a slow readback on a software rasterizer is a legitimate
|
||||||
|
// second-scale wait, while a lost record never completes at all. 30 s separates the two
|
||||||
|
// without turning a loaded CI machine into a red lane, and the Fatal names the seq.
|
||||||
|
constexpr Uint32 kBarrierTimeoutMs = 30000;
|
||||||
|
// How many queued control frames one pump will drain. A backlog deeper than this is a
|
||||||
|
// finding, not a steady state.
|
||||||
|
constexpr Uint32 kMaxControlFramesPerPump = 16;
|
||||||
|
|
||||||
const char* TransportModeName(MG_Config::TransportMode mode) {
|
const char* TransportModeName(MG_Config::TransportMode mode) {
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
case MG_Config::TransportMode::Monolith: return "monolith";
|
case MG_Config::TransportMode::Monolith: return "monolith";
|
||||||
@@ -111,6 +137,170 @@ namespace MobileGL::MG_Remote::Client {
|
|||||||
return "?";
|
return "?";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- c1: the reverse channel's reading end ---------------------------------------
|
||||||
|
//
|
||||||
|
// R-12's three: OnBufferWriteback (#3), OnGpuWritten (#2), OnSurfaceChanged (#7).
|
||||||
|
// Drained BY THE GL THREAD BETWEEN VERBS, which under the barrier means immediately
|
||||||
|
// after appliedSeq reaches this record - the one moment at which the apply thread is
|
||||||
|
// known not to be inside the applier.
|
||||||
|
//
|
||||||
|
// THE BYTES LIVE IN THE RING ITSELF, so Drained() is called only after every payload
|
||||||
|
// pointer popped here has been consumed: retiring earlier is R-11's violation one level
|
||||||
|
// down (EventRing.h:168-171 says so in as many words).
|
||||||
|
Uint32 DrainEventRing(Transport::EventRingConsumer& events) {
|
||||||
|
if (!events.Valid()) return 0;
|
||||||
|
Uint32 delivered = 0;
|
||||||
|
Transport::RingRecordView view{};
|
||||||
|
Bool corrupt = false;
|
||||||
|
while (events.Pop(view, &corrupt)) {
|
||||||
|
if (corrupt) {
|
||||||
|
MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"event-ring\"} - the reverse "
|
||||||
|
"channel's record stream is corrupt");
|
||||||
|
std::abort();
|
||||||
|
}
|
||||||
|
switch (view.kind) {
|
||||||
|
case Transport::kEventBufferWriteback: {
|
||||||
|
if (view.payloadSize < sizeof(Transport::EventBufferWritebackHead)) break;
|
||||||
|
const auto* head =
|
||||||
|
static_cast<const Transport::EventBufferWritebackHead*>(view.payload);
|
||||||
|
const void* bytes = static_cast<const Uint8*>(view.payload) + sizeof(*head);
|
||||||
|
if (view.payloadSize - sizeof(*head) < head->Size) {
|
||||||
|
MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"buffer-writeback\"} - the "
|
||||||
|
"head declares %llu inline bytes and the record carries %llu",
|
||||||
|
static_cast<unsigned long long>(head->Size),
|
||||||
|
static_cast<unsigned long long>(view.payloadSize - sizeof(*head)));
|
||||||
|
std::abort();
|
||||||
|
}
|
||||||
|
if (MG_Pipe::gMGPipeCallbacks.OnBufferWriteback != nullptr) {
|
||||||
|
// The blobref names SEG_EVENT and the IN-SEGMENT offset of those inline
|
||||||
|
// bytes - never a host address (R-2's rule B), which is the whole reason
|
||||||
|
// EventRingConsumer exposes OffsetInSegment at all.
|
||||||
|
MG_Pipe::MGPBlobRef blob{};
|
||||||
|
blob.Seg = Wire::kSegEvent;
|
||||||
|
blob.Offset = events.OffsetInSegment(bytes);
|
||||||
|
blob.Size = head->Size;
|
||||||
|
MG_Pipe::gMGPipeCallbacks.OnBufferWriteback(
|
||||||
|
MG_Pipe::MGPipeHandle{head->Resource.Slot, head->Resource.Gen},
|
||||||
|
head->Offset, blob);
|
||||||
|
++delivered;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case Transport::kEventGpuWritten: {
|
||||||
|
if (view.payloadSize < sizeof(Transport::EventGpuWrittenHead)) break;
|
||||||
|
const auto* head =
|
||||||
|
static_cast<const Transport::EventGpuWrittenHead*>(view.payload);
|
||||||
|
const Uint64 tail = view.payloadSize - sizeof(*head);
|
||||||
|
if (tail / sizeof(Transport::EventRange) < head->RangeCount) {
|
||||||
|
MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"gpu-written\"} - RangeCount "
|
||||||
|
"%u does not fit the record's %llu tail bytes",
|
||||||
|
static_cast<unsigned>(head->RangeCount),
|
||||||
|
static_cast<unsigned long long>(tail));
|
||||||
|
std::abort();
|
||||||
|
}
|
||||||
|
if (MG_Pipe::gMGPipeCallbacks.OnGpuWritten != nullptr) {
|
||||||
|
// EventRange and MGPRange are the same two Uint64s (EventRing.h:61-66
|
||||||
|
// asserts it), so the tail is handed over as-is rather than copied into
|
||||||
|
// a second array a later reader could get out of step with.
|
||||||
|
const auto* ranges = reinterpret_cast<const MG_Pipe::MGPRange*>(
|
||||||
|
static_cast<const Uint8*>(view.payload) + sizeof(*head));
|
||||||
|
MG_Pipe::gMGPipeCallbacks.OnGpuWritten(
|
||||||
|
MG_Pipe::MGPipeHandle{head->Resource.Slot, head->Resource.Gen},
|
||||||
|
static_cast<Uint>(head->RangeCount), ranges);
|
||||||
|
++delivered;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case Transport::kEventSurfaceChanged: {
|
||||||
|
if (view.payloadSize < sizeof(Transport::EventSurfaceChangedHead)) break;
|
||||||
|
if (MG_Pipe::gMGPipeCallbacks.OnSurfaceChanged != nullptr) {
|
||||||
|
const auto* head =
|
||||||
|
static_cast<const Transport::EventSurfaceChangedHead*>(view.payload);
|
||||||
|
MG_Pipe::MGPSurfaceInfo info{};
|
||||||
|
info.Width = head->Width;
|
||||||
|
info.Height = head->Height;
|
||||||
|
info.InternalFormat = head->InternalFormat;
|
||||||
|
info.Samples = head->Samples;
|
||||||
|
info.Layers = head->Layers;
|
||||||
|
info.IsDefault = head->IsDefault;
|
||||||
|
MG_Pipe::gMGPipeCallbacks.OnSurfaceChanged(&info);
|
||||||
|
++delivered;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
MGLOG_W("MG_Remote client: reverse-channel record kind %u is not consumed in "
|
||||||
|
"P5 (R-12 takes three and a half of the ten callbacks)",
|
||||||
|
static_cast<unsigned>(view.kind));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// AND ONLY NOW. Every payload pointer above has been consumed.
|
||||||
|
events.Drained();
|
||||||
|
return delivered;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- c1: the CapsSnapshot -> CapsMirror adoption, in ONE place -------------------
|
||||||
|
//
|
||||||
|
// Every field of the snapshot has exactly one reader, and a field that fails to decode
|
||||||
|
// is a REFUSAL rather than a partial adopt: CompileEnv.cpp:123 copies the whole
|
||||||
|
// DynamicBackendParameters struct into the compile env, so a mirror that adopted three
|
||||||
|
// of four members would put the fourth's default into a shader fingerprint.
|
||||||
|
Bool AdoptCapsSnapshot(const ::MobileGL::Wire::CapsSnapshot* snapshot) {
|
||||||
|
if (snapshot == nullptr) return false;
|
||||||
|
|
||||||
|
MG_Pipe::MGPCaps caps{};
|
||||||
|
const auto* dynamicBytes = snapshot->dynamicParameters();
|
||||||
|
if (dynamicBytes == nullptr || dynamicBytes->size() != sizeof(caps.Dynamic)) {
|
||||||
|
// The Hello/Welcome fingerprint already asserted both peers agree on
|
||||||
|
// sizeof(DynamicBackendParameters), so a disagreement HERE is a corrupt frame
|
||||||
|
// rather than an ABI skew - which is why it is a refusal and not FatalAbiMismatch.
|
||||||
|
MGLOG_E("MG_Remote client: CapsSnapshot carries %llu dynamic bytes, this build's "
|
||||||
|
"struct is %llu - the snapshot is refused whole",
|
||||||
|
static_cast<unsigned long long>(dynamicBytes == nullptr ? 0
|
||||||
|
: dynamicBytes->size()),
|
||||||
|
static_cast<unsigned long long>(sizeof(caps.Dynamic)));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
std::memcpy(&caps.Dynamic, dynamicBytes->data(), sizeof(caps.Dynamic));
|
||||||
|
caps.CallMask = snapshot->callMask();
|
||||||
|
|
||||||
|
RendererInfo renderer{};
|
||||||
|
const auto* rendererBytes = snapshot->rendererInfo();
|
||||||
|
if (rendererBytes == nullptr ||
|
||||||
|
!DecodeRendererInfo(rendererBytes->data(), rendererBytes->size(), renderer)) {
|
||||||
|
MGLOG_E("MG_Remote client: CapsSnapshot's rendererInfo blob did not decode");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
MG_Backend::FormatCapabilityCache formats{};
|
||||||
|
const auto* formatBytes = snapshot->formatCaps();
|
||||||
|
if (formatBytes == nullptr ||
|
||||||
|
!DecodeFormatCapabilities(formatBytes->data(), formatBytes->size(), formats)) {
|
||||||
|
MGLOG_E("MG_Remote client: CapsSnapshot's formatCaps blob did not decode");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const String apiVersion =
|
||||||
|
snapshot->apiVersion() == nullptr ? String{} : String{snapshot->apiVersion()->c_str()};
|
||||||
|
|
||||||
|
// THE SERVER'S BACKEND TYPE, NEVER A NEW "Remote" ENUMERATOR and never guessed from
|
||||||
|
// the renderer string: GL_Framebuffer.cpp:47, GL_Texture.cpp:6536 and
|
||||||
|
// CompileEnv.cpp:122 SWITCH on it, and a value they do not know takes a wrong arm
|
||||||
|
// rather than failing.
|
||||||
|
const Uint32 rawBackend = snapshot->backendType();
|
||||||
|
if (rawBackend >= static_cast<Uint32>(BackendType::BackendTypeCount)) {
|
||||||
|
MGLOG_E("MG_Remote client: CapsSnapshot names backend type %u, which this build "
|
||||||
|
"has no enumerator for - the snapshot is refused rather than folded onto "
|
||||||
|
"Unknown, which three frontend switches would silently mis-branch on",
|
||||||
|
static_cast<unsigned>(rawBackend));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
CapsMirrorInstance().Adopt(caps, formats, renderer, apiVersion,
|
||||||
|
static_cast<BackendType>(rawBackend));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
// Null, not a Fatal: MG_Backend::Init() asks whether a session exists before it decides to
|
// Null, not a Fatal: MG_Backend::Init() asks whether a session exists before it decides to
|
||||||
@@ -308,24 +498,17 @@ namespace MobileGL::MG_Remote::Client {
|
|||||||
m_encoder = Wire::PipeWireEncoder(control, &m_cmd, nullptr, &m_segments);
|
m_encoder = Wire::PipeWireEncoder(control, &m_cmd, nullptr, &m_segments);
|
||||||
|
|
||||||
// ---- 7. the first CapsSnapshot, if the server had a backend to publish one from.
|
// ---- 7. the first CapsSnapshot, if the server had a backend to publish one from.
|
||||||
if (m_transport->PeekFrameSize() != 0) {
|
// ONE DRAIN, ONE ADOPTER (c1): PumpControlPlane below is the only thing in the client
|
||||||
std::vector<Uint8> frame;
|
// that turns a CapsSnapshot into a CapsMirror generation, so R-12's "a second arrival
|
||||||
if (ReceiveEnvelope(*m_transport, frame, 0) == MOBILEGL_OK) {
|
// IS the invalidation" lives in exactly one place. s1's step here used to stop at
|
||||||
const ::MobileGL::Wire::CtrlEnvelope* envelope = ParseEnvelope(frame);
|
// "the snapshot arrived and is verifiable"; it now goes all the way, through the same
|
||||||
if (envelope != nullptr &&
|
// function every later arrival goes through.
|
||||||
envelope->msg_type() == ::MobileGL::Wire::CtrlMsg::CapsSnapshot) {
|
if (PumpControlPlane() == 0) {
|
||||||
// The mirror's Adopt and the two blob DECODERS are c1's and w1's. s1 stops
|
MGLOG_W("MG_Remote client: the handshake carried no CapsSnapshot - the caps mirror is "
|
||||||
// at "the snapshot arrived and is verifiable": adopting it here would put
|
"a PLACEHOLDER until one arrives. Every caps read until then answers a "
|
||||||
// the caps mirror's invalidation rule (R-12: a second arrival IS the
|
"default and says so");
|
||||||
// invalidation) in two places.
|
|
||||||
MGLOG_I("MG_Remote client: first CapsSnapshot received (%llu bytes); adopting "
|
|
||||||
"it is package c1's CapsMirror::Adopt over package w1's decoders",
|
|
||||||
static_cast<unsigned long long>(frame.size()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
m_started = true;
|
|
||||||
g_active = this;
|
g_active = this;
|
||||||
LogMemory("handshake");
|
LogMemory("handshake");
|
||||||
|
|
||||||
@@ -425,18 +608,179 @@ namespace MobileGL::MG_Remote::Client {
|
|||||||
// cost zero extra round trips - and the client may not re-derive any of those four answers
|
// cost zero extra round trips - and the client may not re-derive any of those four answers
|
||||||
// locally. s1 supplies the four primitives it composes from: Encoder(), Producer(),
|
// locally. s1 supplies the four primitives it composes from: Encoder(), Producer(),
|
||||||
// WaitForApplied() and ReadReply().
|
// WaitForApplied() and ReadReply().
|
||||||
Uint64 ClientSession::EmitAndWait(MG_Pipe::MGPWireOp, const void*, Uint64, const void*, Uint64,
|
//
|
||||||
void*, Uint64, Int32*) {
|
// THE ORDER IS ENCODE -> PUBLISH+NOTIFY -> WAIT -> READ REPLY, and it is not negotiable.
|
||||||
MGP5_C0_STUB("ClientSession::EmitAndWait");
|
// Splitting the wait from the read is how a package ends up answering an acceptance
|
||||||
|
// question locally, which is the c0f/c0g defect P4a paid two contract corrections for; and
|
||||||
|
// "always accept" is ID-39's 66 lost DirectVulkan uploads with a wire in between.
|
||||||
|
Uint64 ClientSession::EmitAndWait(MG_Pipe::MGPWireOp op, const void* payload, Uint64 payloadBytes,
|
||||||
|
const void* varTail, Uint64 varTailBytes, void* replyOut,
|
||||||
|
Uint64 replyBytes, Int32* statusOut) {
|
||||||
|
if (statusOut != nullptr) *statusOut = Wire::ReplySink::kStatusError;
|
||||||
|
if (!m_started) {
|
||||||
|
MGLOG_F("MGPipe: Fatal{NoClientSession, \"%s\"} - EmitAndWait on a session that has "
|
||||||
|
"not started. There is no fall-through: a record that could not be emitted "
|
||||||
|
"is a verb that did not happen",
|
||||||
|
Wire::WireOpName(op));
|
||||||
|
std::abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
// R-1's INVARIANT, AS A RUNTIME CHECK RATHER THAN A SENTENCE. While the barrier holds,
|
||||||
|
// at most one of {GL thread, apply thread} is runnable - and that is the whole reason a
|
||||||
|
// single process-wide gPipeInputs is legal (table 3). The client is about to publish a
|
||||||
|
// record whose fields the applier will read out of gPipeInputs, so the apply thread
|
||||||
|
// being inside the applier right now means the invariant has already been broken and
|
||||||
|
// the next record would be read against a half-written residual fill.
|
||||||
|
if (ApplyThreadIsInsideApplier()) {
|
||||||
|
MGLOG_F("MGPipe: Fatal{BarrierViolation, \"%s\"} - the apply thread is inside the "
|
||||||
|
"applier while the GL thread is emitting. R-1 makes at most one of them "
|
||||||
|
"runnable, which is what keeps one process-wide gPipeInputs legal",
|
||||||
|
Wire::WireOpName(op));
|
||||||
|
std::abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
const Uint64 seq =
|
||||||
|
m_encoder.EncodeRecord(op, payload, payloadBytes, varTail, varTailBytes);
|
||||||
|
if (seq == Wire::kInvalidSeq) {
|
||||||
|
// The ring refused it. NOT a silent drop and not a retry loop: R-10 says P5 does no
|
||||||
|
// chunking and must prove it needs none, so a refusal is the proof failing.
|
||||||
|
MGLOG_F("MGPipe: Fatal{RingOverrun, \"%s\"} - the command ring refused a %llu-byte "
|
||||||
|
"record. P5 does not chunk (R-10); this is the proof obligation failing, not "
|
||||||
|
"a back-pressure case",
|
||||||
|
Wire::WireOpName(op),
|
||||||
|
static_cast<unsigned long long>(payloadBytes + varTailBytes));
|
||||||
|
std::abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Publish the head, record submittedSeq, THEN ring - in that order, which is
|
||||||
|
// SessionProducer's one job and RingTest.cpp:446's pin. Notify-then-publish loses the
|
||||||
|
// wakeup.
|
||||||
|
m_producer.PublishAndNotify(seq);
|
||||||
|
|
||||||
|
// Does this row own a reply slot? kMGPipeCallFlags IS THE SINGLE SOURCE OF TRUTH
|
||||||
|
// (R-16 / ID-31) - fourteen rows now, because the four Bool acceptance entry points
|
||||||
|
// gained the flag. Asking the catalogue rather than the caller is what stops a caller
|
||||||
|
// that forgot to pass a buffer from silently turning an answer into a guess.
|
||||||
|
const Bool ownsReplySlot =
|
||||||
|
(MG_Pipe::MGPipeCallFlagsFor(op) & static_cast<Uint32>(MG_Pipe::kReplySlot)) != 0;
|
||||||
|
|
||||||
|
if (!m_barrierArmed && !ownsReplySlot) {
|
||||||
|
// R-1's NEGATIVE CONTROL ARM, and the only thing it turns off is the barrier. A
|
||||||
|
// reply-slot row still waits: the answer is not derivable here and R-5 forbids
|
||||||
|
// inventing one, so MOBILEGL_IPC_VERB_BARRIER=0 makes the queue free-running, not
|
||||||
|
// the client clairvoyant.
|
||||||
|
return seq;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BarrierWaitScope waiting;
|
||||||
|
const Transport::SessionWait wait = m_producer.WaitForApplied(seq, kBarrierTimeoutMs);
|
||||||
|
if (wait == Transport::SessionWait::ShutDown) {
|
||||||
|
// The doorbell died: the server went away. The only thing that returns from a
|
||||||
|
// kWaitForever park, and therefore the only way a client blocked in the barrier
|
||||||
|
// survives a server that is gone. Not a Fatal - teardown legitimately reaches here.
|
||||||
|
MGLOG_E("MG_Remote client: the barrier for %s (seq %llu) woke on a dead doorbell; the "
|
||||||
|
"server is gone and this verb did not happen",
|
||||||
|
Wire::WireOpName(op), static_cast<unsigned long long>(seq));
|
||||||
|
return seq;
|
||||||
|
}
|
||||||
|
if (wait != Transport::SessionWait::Reached) {
|
||||||
|
MGLOG_F("MGPipe: Fatal{BarrierTimeout, \"%s\"} - appliedSeq did not reach %llu within "
|
||||||
|
"%u ms. A bounded wait is deliberate: a wedged CI job and a lost record look "
|
||||||
|
"identical from outside, and only one of them is a bug worth finding",
|
||||||
|
Wire::WireOpName(op), static_cast<unsigned long long>(seq), kBarrierTimeoutMs);
|
||||||
|
std::abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
// THE REVERSE CHANNEL IS DRAINED HERE, and here is the only place it can be: under the
|
||||||
|
// barrier this is the one instant at which the apply thread is known not to be inside
|
||||||
|
// the applier, and MGPipeClientOnGpuWritten / OnBufferWriteback write frontend objects.
|
||||||
|
// It is what gives AwaitBufferWriteback something to have waited FOR: b1's third state
|
||||||
|
// clears when the writeback lands, and the writeback lands on this ring.
|
||||||
|
DrainEventRing(m_events);
|
||||||
|
|
||||||
|
if (!ownsReplySlot) return seq;
|
||||||
|
|
||||||
|
// THE SAME WAIT, NOT A SECOND ONE. appliedSeq >= seq already means the server wrote
|
||||||
|
// this record's answer, because it writes the slot before it advances the watermark.
|
||||||
|
Uint64 replySize = 0;
|
||||||
|
Int32 status = Wire::ReplySink::kStatusError;
|
||||||
|
if (!ReadReply(seq, replyOut, replyBytes, &status, &replySize)) {
|
||||||
|
MGLOG_F("MGPipe: Fatal{ReplyMissing, \"%s\"} - seq %llu carries kReplySlot and the "
|
||||||
|
"server applied it, but its slot does not stamp that seq. The stamp is what "
|
||||||
|
"makes a wrong-slot read detectable rather than plausible (R-3)",
|
||||||
|
Wire::WireOpName(op), static_cast<unsigned long long>(seq));
|
||||||
|
std::abort();
|
||||||
|
}
|
||||||
|
if (statusOut != nullptr) *statusOut = status;
|
||||||
|
if (status == Wire::ReplySink::kStatusError) {
|
||||||
|
MGLOG_E("MG_Remote client: %s (seq %llu) answered ERROR", Wire::WireOpName(op),
|
||||||
|
static_cast<unsigned long long>(seq));
|
||||||
|
}
|
||||||
|
// DECLINED is NOT an error and is deliberately not logged as one: it is how
|
||||||
|
// MapPersistent says nullptr (R-6) and how the four Bool acceptance rows say false
|
||||||
|
// (R-5). A client that treated it as a failure would re-create ID-39 from the other
|
||||||
|
// side.
|
||||||
|
if (replyOut != nullptr && replySize > replyBytes) {
|
||||||
|
MGLOG_F("MGPipe: Fatal{ReplyTooLarge, \"%s\"} - the answer is %llu bytes and the "
|
||||||
|
"caller offered %llu. P5 does not chunk a reply",
|
||||||
|
Wire::WireOpName(op), static_cast<unsigned long long>(replySize),
|
||||||
|
static_cast<unsigned long long>(replyBytes));
|
||||||
|
std::abort();
|
||||||
|
}
|
||||||
|
return seq;
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool ClientSession::BarrierArmed() const { return m_barrierArmed; }
|
Bool ClientSession::BarrierArmed() const { return m_barrierArmed; }
|
||||||
|
|
||||||
// False, not a Fatal, for both: these are the R-1 mutual-exclusion assertion's two probes,
|
// R-1's mutual-exclusion invariant, as two probes that answer honestly.
|
||||||
// and an assertion helper that aborts when asked is worse than useless. Package c1 gives
|
//
|
||||||
// them real answers when it lands the barrier.
|
// THE CLIENT'S FLAG IS THREAD-LOCAL AND THE SERVER'S IS NOT, and the asymmetry is the
|
||||||
Bool ClientSession::InBarrierWait() { return false; }
|
// point: "am I inside a barrier wait" is a question about the calling thread, while "is the
|
||||||
Bool ClientSession::ApplyThreadIsInsideApplier() { return false; }
|
// apply thread inside the applier" is a question the GL thread asks about a DIFFERENT
|
||||||
|
// thread - so the second has to be a shared atomic and the first must not be, or a second
|
||||||
|
// GL thread would see the first one's wait as its own.
|
||||||
|
Bool ClientSession::InBarrierWait() { return g_inBarrierWait; }
|
||||||
|
Bool ClientSession::ApplyThreadIsInsideApplier() {
|
||||||
|
return g_applyThreadInsideApplier.load(std::memory_order_acquire);
|
||||||
|
}
|
||||||
|
void ClientSession::NoteApplyThreadEnteredApplier() {
|
||||||
|
g_applyThreadInsideApplier.store(true, std::memory_order_release);
|
||||||
|
}
|
||||||
|
void ClientSession::NoteApplyThreadLeftApplier() {
|
||||||
|
g_applyThreadInsideApplier.store(false, std::memory_order_release);
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint32 ClientSession::PumpControlPlane() {
|
||||||
|
// NOT gated on m_started. The first snapshot arrives DURING Start(), before this session
|
||||||
|
// is started or active - and s1's half-built teardown path depends on m_started staying
|
||||||
|
// false until step 8 has succeeded, so the flag cannot be moved earlier to suit this.
|
||||||
|
if (m_transport == nullptr) return 0;
|
||||||
|
Uint32 adopted = 0;
|
||||||
|
// Bounded rather than `while (true)`: a server that queued frames faster than this
|
||||||
|
// drains them would otherwise hold the GL thread here for ever, and a frame backlog
|
||||||
|
// deeper than this is a finding rather than a steady state.
|
||||||
|
for (Uint32 guard = 0; guard < kMaxControlFramesPerPump; ++guard) {
|
||||||
|
if (m_transport->PeekFrameSize() == 0) break;
|
||||||
|
std::vector<Uint8> frame;
|
||||||
|
if (ReceiveEnvelope(*m_transport, frame, 0) != MOBILEGL_OK) break;
|
||||||
|
const ::MobileGL::Wire::CtrlEnvelope* envelope = ParseEnvelope(frame);
|
||||||
|
if (envelope == nullptr) {
|
||||||
|
MGLOG_E("MG_Remote client: an unverifiable control frame (%llu bytes) was dropped",
|
||||||
|
static_cast<unsigned long long>(frame.size()));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (envelope->msg_type() != ::MobileGL::Wire::CtrlMsg::CapsSnapshot) {
|
||||||
|
// SurfaceOp / SurfaceReply / ResyncRequest / AuxRequest / LogLine are P6's and
|
||||||
|
// P7's. Named rather than ignored, so a phase that starts sending one does not
|
||||||
|
// discover this loop swallowing it.
|
||||||
|
MGLOG_W("MG_Remote client: control message %d is not consumed in P5",
|
||||||
|
static_cast<int>(envelope->msg_type()));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (AdoptCapsSnapshot(envelope->msg_as_CapsSnapshot())) ++adopted;
|
||||||
|
}
|
||||||
|
return adopted;
|
||||||
|
}
|
||||||
|
|
||||||
Transport::SessionProducer& ClientSession::Producer() { return m_producer; }
|
Transport::SessionProducer& ClientSession::Producer() { return m_producer; }
|
||||||
|
|
||||||
|
|||||||
@@ -100,6 +100,36 @@ namespace MobileGL::MG_Remote::Client {
|
|||||||
static Bool InBarrierWait();
|
static Bool InBarrierWait();
|
||||||
static Bool ApplyThreadIsInsideApplier();
|
static Bool ApplyThreadIsInsideApplier();
|
||||||
|
|
||||||
|
// ---- c1's additions ---------------------------------------------------------------
|
||||||
|
|
||||||
|
// The apply thread's half of R-1's invariant. v1's apply loop brackets its
|
||||||
|
// DecodeAndApply with these; the client checks the flag before it publishes, so
|
||||||
|
// "at most one of {GL thread, apply thread} is runnable" is a runtime assertion rather
|
||||||
|
// than a sentence in a brief. A raw pair rather than an RAII type in this header
|
||||||
|
// because the server side owns its own scoping and must not have to include a client
|
||||||
|
// header to get it - ScopedApplierEntry below is the convenience, not the contract.
|
||||||
|
static void NoteApplyThreadEnteredApplier();
|
||||||
|
static void NoteApplyThreadLeftApplier();
|
||||||
|
struct ScopedApplierEntry {
|
||||||
|
ScopedApplierEntry() { NoteApplyThreadEnteredApplier(); }
|
||||||
|
~ScopedApplierEntry() { NoteApplyThreadLeftApplier(); }
|
||||||
|
ScopedApplierEntry(const ScopedApplierEntry&) = delete;
|
||||||
|
ScopedApplierEntry& operator=(const ScopedApplierEntry&) = delete;
|
||||||
|
};
|
||||||
|
|
||||||
|
// R-12's INVALIDATION EDGE. Drains whatever the server has queued on the control plane
|
||||||
|
// and adopts every CapsSnapshot in it - and a SECOND snapshot IS the invalidation,
|
||||||
|
// which is why there is no Invalidate(). Non-blocking: it peeks and returns.
|
||||||
|
//
|
||||||
|
// Called at the handshake, from BackendObject_Remote's Initialize/InitCapabilities, and
|
||||||
|
// once per Present. Present is the boundary every one of P5's three targets crosses,
|
||||||
|
// and a caps re-run can only follow a surface event, so once a frame is both sufficient
|
||||||
|
// and the cheapest place that is.
|
||||||
|
//
|
||||||
|
// Returns how many snapshots it adopted, so a case can assert the edge fired rather
|
||||||
|
// than assert that a number downstream of it happened to change.
|
||||||
|
Uint32 PumpControlPlane();
|
||||||
|
|
||||||
// ---- s1's additions: the four primitives c1's EmitAndWait composes ---------------
|
// ---- s1's additions: the four primitives c1's EmitAndWait composes ---------------
|
||||||
//
|
//
|
||||||
// s1 owns construction and lifetime; c1 owns the barrier POLICY. So the plumbing is
|
// s1 owns construction and lifetime; c1 owns the barrier POLICY. So the plumbing is
|
||||||
|
|||||||
@@ -6,13 +6,36 @@
|
|||||||
// SPDX-License-Identifier: LGPL-3.0-only
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
// End of Source File Header
|
// End of Source File Header
|
||||||
|
|
||||||
// P5 c0 stubs for package c1.
|
// P5 package c1: the 71-slot emit table.
|
||||||
|
//
|
||||||
|
// 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 5 slots emitted
|
||||||
|
// class C 64 slots Fatal{UnmigratedVerb, "<slot>"}
|
||||||
|
// 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.
|
||||||
|
//
|
||||||
|
// THE PRE-VERB HOOKS RUN BEFORE THE RECORD, NEVER AFTER (b1, ID-18). PushPersistentMapsBeforeVerb
|
||||||
|
// publishes the bytes an application wrote through a coherent map with no API call at all, and
|
||||||
|
// MarkGpuWritesForDraw builds the conservative GPU-write set the client now owns. Both describe
|
||||||
|
// the work the record is ABOUT TO START, so a hook deferred past its own record is the C-1
|
||||||
|
// regression re-committed at the transport layer.
|
||||||
|
|
||||||
#include "EmitTables.h"
|
#include "EmitTables.h"
|
||||||
|
|
||||||
|
#include "ClientSession.h"
|
||||||
|
#include "GpuWritePending.h"
|
||||||
|
#include "PersistentMapTracker.h"
|
||||||
|
|
||||||
|
#include <MG_Util/Converters/GLToMG/TextureEnumConverter.h>
|
||||||
#include <MG_Util/Debug/Log.h>
|
#include <MG_Util/Debug/Log.h>
|
||||||
|
#include <MG_Util/Metrics/TextureMetrics.h>
|
||||||
|
|
||||||
|
#include <MG_State/GLState/Core.h>
|
||||||
|
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
namespace MobileGL::MG_Remote::Client {
|
namespace MobileGL::MG_Remote::Client {
|
||||||
|
|
||||||
@@ -36,12 +59,478 @@ namespace MobileGL::MG_Remote::Client {
|
|||||||
std::abort();
|
std::abort();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
Bool g_dropClearEmission = false;
|
||||||
|
Uint64 g_droppedClearEmissions = 0;
|
||||||
|
|
||||||
|
// E2's control has to be armable from OUTSIDE the process that runs the replay, because
|
||||||
|
// the statement it makes is about a trace lane and not about a unit case: "drop one
|
||||||
|
// Clear emission and OpenRA's SSIM falls below 0.99". A recompile would make the control
|
||||||
|
// arm against source text, which is ID-22(a)'s defect.
|
||||||
|
//
|
||||||
|
// READ WITH getenv RATHER THAN THROUGH MG_Config, DELIBERATELY AND TEMPORARILY. Config.h
|
||||||
|
// is c0's and a new MOBILEGL_IPC_* knob goes through the integrator; this is a
|
||||||
|
// NEGATIVE-CONTROL switch no operator may ever set, and it announces itself at warning
|
||||||
|
// level every time it arms so it cannot be on by accident. Flagged for adoption into
|
||||||
|
// IpcTable if the integrator wants it there.
|
||||||
|
Bool ReadDropClearFromEnvironment() {
|
||||||
|
const char* value = std::getenv("MOBILEGL_IPC_E2_DROP_CLEAR");
|
||||||
|
const Bool armed = value != nullptr && value[0] == '1' && value[1] == '\0';
|
||||||
|
if (armed) {
|
||||||
|
MGLOG_W("MG_Remote client: MOBILEGL_IPC_E2_DROP_CLEAR=1 - E2's NEGATIVE CONTROL is "
|
||||||
|
"armed and every glClear will be DROPPED on the wire. This arm is expected "
|
||||||
|
"to fail its SSIM threshold; a lane that stays green with it set is not "
|
||||||
|
"going through the wire at all");
|
||||||
|
}
|
||||||
|
return armed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- the session, demanded rather than assumed --------------------------------
|
||||||
|
//
|
||||||
|
// Every class-B slot needs one. A null session here is NOT the monolith answer - the
|
||||||
|
// monolith answer is that this table was never installed at all, because
|
||||||
|
// MG_Backend::Init() only reaches BackendObject_Remote when the transport resolved. So
|
||||||
|
// a null one is a Fatal by name and not a fall-through to the driver: a pass-through
|
||||||
|
// slot is the "split lane ran monolith and went green" shape that every gate in this
|
||||||
|
// phase exists to prevent (R-4).
|
||||||
|
ClientSession& RequireSession(const char* slot) {
|
||||||
|
ClientSession* session = ClientSession::Active();
|
||||||
|
if (session == nullptr) {
|
||||||
|
MGLOG_F("MGPipe: Fatal{NoClientSession, \"%s\"} - the remote emit table is "
|
||||||
|
"installed but no ClientSession is active. A slot may not fall through "
|
||||||
|
"to a driver this role does not have",
|
||||||
|
slot);
|
||||||
|
std::abort();
|
||||||
|
}
|
||||||
|
return *session;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The two hooks b1 wrote and deliberately left with no caller, because the call site is
|
||||||
|
// this file's. ORDER: the push first (it produces resource_subdata records that must
|
||||||
|
// precede the verb on SEG_CMD), then the mark walk, then the verb record.
|
||||||
|
void BeforeDrawVerb() {
|
||||||
|
PushPersistentMapsBeforeVerb();
|
||||||
|
MarkGpuWritesForDraw();
|
||||||
|
}
|
||||||
|
|
||||||
|
// A verb that reads buffers but starts no shader: clear, blit, readback, present. The
|
||||||
|
// push still has to run - a coherent map is read by the GPU on any of them - but there
|
||||||
|
// is no shader that could write one, so no mark walk.
|
||||||
|
void BeforeReadOnlyVerb() { PushPersistentMapsBeforeVerb(); }
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// CLASS B - the five slots the verb census measured (CONTRACT-P5.md §7)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
void EmitClear(GLbitfield mask) {
|
||||||
|
ClientSession& session = RequireSession("Clear");
|
||||||
|
BeforeReadOnlyVerb();
|
||||||
|
|
||||||
|
if (g_dropClearEmission) {
|
||||||
|
// E2's negative control. Everything above still ran, so the only difference
|
||||||
|
// between this arm and the live one is the record - which is exactly the
|
||||||
|
// statement "the picture comes from the wire" that E2 exists to prove.
|
||||||
|
++g_droppedClearEmissions;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
MG_Pipe::MGPClear record{};
|
||||||
|
// The DRAW framebuffer is whatever the server's own SyncRenderState resolves from
|
||||||
|
// gPipeInputs, which the client's MGP_FILL(Clear) at GL_Drawing.cpp:534 has just
|
||||||
|
// written and the verb barrier keeps still (R-1). Naming a handle here would be a
|
||||||
|
// SECOND statement of the binding, and the second one is the one that goes stale.
|
||||||
|
record.Fbo = MG_Pipe::kMGPipeNullHandle;
|
||||||
|
record.Kind = kRemoteClearWhole;
|
||||||
|
record.DrawBufferIndex = -1;
|
||||||
|
record.BufferMask = static_cast<Uint32>(mask);
|
||||||
|
record.ValueClass = 0;
|
||||||
|
session.EmitAndWait(MG_Pipe::MGPWireOp::Clear, &record, sizeof(record), nullptr, 0,
|
||||||
|
nullptr, 0, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EmitDrawArrays(GLenum mode, GLint first, GLsizei count) {
|
||||||
|
ClientSession& session = RequireSession("DrawArrays");
|
||||||
|
BeforeDrawVerb();
|
||||||
|
|
||||||
|
MG_Pipe::MGPDrawInfo info{};
|
||||||
|
info.Mode = static_cast<Uint32>(mode);
|
||||||
|
info.IndexSize = 0; // arrays
|
||||||
|
info.Flags = 0; // NO kDrawHasUserIndices: the reduced path draws from a VBO
|
||||||
|
info.InstanceCount = 1;
|
||||||
|
info.StartInstance = 0;
|
||||||
|
info.RestartIndex = 0;
|
||||||
|
info.DrawIdOffset = 0;
|
||||||
|
info.IndexResource = MG_Pipe::kMGPipeNullHandle;
|
||||||
|
info.MinIndex = ~0u; // "unknown", MGPipeTypes.h:1330
|
||||||
|
info.MaxIndex = ~0u;
|
||||||
|
info.XfbCpuCapturedVertices = 0;
|
||||||
|
info.NumDraws = 1;
|
||||||
|
|
||||||
|
const MG_Pipe::MGPDrawRange range{static_cast<Uint32>(first), static_cast<Uint32>(count), 0};
|
||||||
|
// One tail of exactly NumDraws entries. w1's encoder recomputes that from the
|
||||||
|
// payload and Fatals on a disagreement, on THIS side - so a NumDraws that drifted
|
||||||
|
// from the tail is a producer-side abort rather than a corrupt stream a peer has to
|
||||||
|
// diagnose.
|
||||||
|
session.EmitAndWait(MG_Pipe::MGPWireOp::DrawVbo, &info, sizeof(info), &range,
|
||||||
|
sizeof(range), nullptr, 0, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EmitBlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0,
|
||||||
|
GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask,
|
||||||
|
GLenum filter) {
|
||||||
|
ClientSession& session = RequireSession("BlitFramebuffer");
|
||||||
|
BeforeReadOnlyVerb();
|
||||||
|
|
||||||
|
MG_Pipe::MGPBlit record{};
|
||||||
|
// Same reasoning as Clear's Fbo: the read and draw bindings are gPipeInputs', set
|
||||||
|
// by MGP_FILL(BlitFramebuffer) at GL_Framebuffer.cpp:660 and held still by the
|
||||||
|
// barrier. glBlitNamedFramebuffer, which DOES name two framebuffers, is class C.
|
||||||
|
record.ReadFbo = MG_Pipe::kMGPipeNullHandle;
|
||||||
|
record.DrawFbo = MG_Pipe::kMGPipeNullHandle;
|
||||||
|
record.SrcX0 = srcX0;
|
||||||
|
record.SrcY0 = srcY0;
|
||||||
|
record.SrcX1 = srcX1;
|
||||||
|
record.SrcY1 = srcY1;
|
||||||
|
record.DstX0 = dstX0;
|
||||||
|
record.DstY0 = dstY0;
|
||||||
|
record.DstX1 = dstX1;
|
||||||
|
record.DstY1 = dstY1;
|
||||||
|
record.Mask = static_cast<Uint32>(mask);
|
||||||
|
record.Filter = static_cast<Uint32>(filter);
|
||||||
|
session.EmitAndWait(MG_Pipe::MGPWireOp::Blit, &record, sizeof(record), nullptr, 0,
|
||||||
|
nullptr, 0, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// How many bytes glReadPixels will pack for this rectangle, from the PACK half of the
|
||||||
|
// pixel-store state. The client has to declare it - MGPReadbackInfo::DstSize is what
|
||||||
|
// sizes the reply and what the client checks against MaxReplyBytes() BEFORE it emits,
|
||||||
|
// because a reply bigger than a slot is Fatal rather than chunked (s1's ReplySlot.h).
|
||||||
|
//
|
||||||
|
// GL 4.6 8.4.4's arithmetic, and nothing cleverer: a row is rounded up to Alignment,
|
||||||
|
// the LAST row is not padded, and SkipRows/SkipPixels/SkipImages shift the destination
|
||||||
|
// rather than growing it (the application owns those bytes and we never write them).
|
||||||
|
Uint64 PackedReadbackBytes(GLsizei width, GLsizei height, GLenum format, GLenum type) {
|
||||||
|
if (width <= 0 || height <= 0) return 0;
|
||||||
|
const TextureInputFormat inputFormat =
|
||||||
|
MG_Util::ConvertGLEnumToTextureInputFormat(format);
|
||||||
|
const TexturePixelDataType dataType = MG_Util::ConvertGLEnumToTexturePixelDataType(type);
|
||||||
|
const SizeT bytesPerPixel = MG_Util::GetInputBytesPerPixel(inputFormat, dataType);
|
||||||
|
if (bytesPerPixel == 0) {
|
||||||
|
// NOT a guess and not a zero-length reply. A format this build cannot size is a
|
||||||
|
// readback whose answer would be silently truncated, which is the one failure a
|
||||||
|
// picture comparison cannot see.
|
||||||
|
MGLOG_F("MGPipe: Fatal{UnsizedReadback, \"read_pixels\"} format=0x%04x type=0x%04x "
|
||||||
|
"- the client must declare MGPReadbackInfo::DstSize and cannot size this "
|
||||||
|
"pair; P5's reduced path reads RGBA/UNSIGNED_BYTE",
|
||||||
|
static_cast<unsigned>(format), static_cast<unsigned>(type));
|
||||||
|
std::abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
PixelStoreParameters pack{};
|
||||||
|
if (MG_State::pGLContext != nullptr) {
|
||||||
|
pack = MG_State::pGLContext->GetPixelStoreParameters(/*isUnpack=*/false);
|
||||||
|
}
|
||||||
|
const Uint64 rowPixels =
|
||||||
|
pack.RowLength > 0 ? static_cast<Uint64>(pack.RowLength) : static_cast<Uint64>(width);
|
||||||
|
const Uint64 alignment = pack.Alignment > 0 ? static_cast<Uint64>(pack.Alignment) : 1ull;
|
||||||
|
const Uint64 rowBytes = rowPixels * static_cast<Uint64>(bytesPerPixel);
|
||||||
|
const Uint64 paddedRow = ((rowBytes + alignment - 1) / alignment) * alignment;
|
||||||
|
const Uint64 lastRow = static_cast<Uint64>(width) * static_cast<Uint64>(bytesPerPixel);
|
||||||
|
return paddedRow * (static_cast<Uint64>(height) - 1) + lastRow;
|
||||||
|
}
|
||||||
|
|
||||||
|
void EmitReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format,
|
||||||
|
GLenum type, void* pixels) {
|
||||||
|
ClientSession& session = RequireSession("ReadPixels");
|
||||||
|
BeforeReadOnlyVerb();
|
||||||
|
|
||||||
|
// THE PBO HALF IS b1's DESIGN AND b1 ALREADY WIRED ITS MARK, at
|
||||||
|
// GL_Framebuffer.cpp:3109 - immediately after this table call returns, inside
|
||||||
|
// ReadPixels_Backend itself. So this emitter deliberately does NOT call
|
||||||
|
// MarkReadPixelsPackBuffer(): a second call there would be the "wire it twice"
|
||||||
|
// shape, and the per-row counter b1's unit cases assert on would then count one
|
||||||
|
// read as two.
|
||||||
|
const Uint64 bytes = PackedReadbackBytes(width, height, format, type);
|
||||||
|
|
||||||
|
MG_Pipe::MGPReadbackInfo info{};
|
||||||
|
info.Res = MG_Pipe::kMGPipeNullHandle; // "the bound read surface answers"
|
||||||
|
info.Box = MG_Pipe::MGPBox{x, y, 0, static_cast<Uint32>(width),
|
||||||
|
static_cast<Uint32>(height), 1};
|
||||||
|
info.Format = static_cast<Uint32>(format);
|
||||||
|
info.Type = static_cast<Uint32>(type);
|
||||||
|
info.Target = 0;
|
||||||
|
info.Level = 0;
|
||||||
|
info.DstOffset = 0;
|
||||||
|
info.DstSize = bytes;
|
||||||
|
|
||||||
|
// CHECKED BEFORE THE EMISSION, not after the answer. A reply bigger than a slot is
|
||||||
|
// Fatal on the server, and a Fatal there is a dead apply thread with a client
|
||||||
|
// parked in the barrier for ever; here it is one line naming the number.
|
||||||
|
const Uint64 capacity = session.MaxReplyBytes();
|
||||||
|
if (bytes > capacity) {
|
||||||
|
MGLOG_F("MGPipe: Fatal{ReplyTooLarge, \"read_pixels\"} %llu bytes into a %llu-byte "
|
||||||
|
"reply slot - P5 does not chunk a readback (R-10); raise the reply pool's "
|
||||||
|
"slot size or shrink the read",
|
||||||
|
static_cast<unsigned long long>(bytes),
|
||||||
|
static_cast<unsigned long long>(capacity));
|
||||||
|
std::abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
Int32 status = 0;
|
||||||
|
// The pixels land straight in the application's buffer: the barrier's wait IS the
|
||||||
|
// reply's wait (R-3), so this costs no round trip beyond the one the barrier was
|
||||||
|
// already paying.
|
||||||
|
session.EmitAndWait(MG_Pipe::MGPWireOp::ReadPixels, &info, sizeof(info), nullptr, 0,
|
||||||
|
pixels, bytes, &status);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EmitPresent() {
|
||||||
|
ClientSession& session = RequireSession("Present");
|
||||||
|
BeforeReadOnlyVerb();
|
||||||
|
|
||||||
|
MG_Pipe::MGPPresent record{};
|
||||||
|
// FrameSerial 0 = "the server stamps its own". P5 has no client-side present credit
|
||||||
|
// (MOBILEGL_IPC_PRESENT_CREDIT is P6's), so a client-minted serial would be a second
|
||||||
|
// id space with no consumer.
|
||||||
|
record.FrameSerial = 0;
|
||||||
|
session.EmitAndWait(MG_Pipe::MGPWireOp::Present, &record, sizeof(record), nullptr, 0,
|
||||||
|
nullptr, 0, nullptr);
|
||||||
|
|
||||||
|
// R-12's invalidation edge, drained at the one boundary every target crosses. A
|
||||||
|
// second CapsSnapshot IS the invalidation; nothing else on the client can see that
|
||||||
|
// the server re-ran InitCapabilities, because GLContext::GetCompileEnv()'s memo is
|
||||||
|
// keyed on pActiveBackendObject.get() (Core.cpp:34) and that pointer never changes
|
||||||
|
// under split.
|
||||||
|
session.PumpControlPlane();
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// CLASS A - answered locally from the caps mirror (R-15). NO RECORD, EVER.
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
void AnswerGetIntegeri_v(GLenum target, GLuint index, GLint* data) {
|
||||||
|
if (data == nullptr) return;
|
||||||
|
const MG_Backend::DynamicBackendParameters& dynamic = CapsMirrorInstance().Dynamic();
|
||||||
|
// The ONLY two indexed pnames the device owns; every other indexed pname names
|
||||||
|
// frontend state and is answered in GL_Getter::GetIntegeri_v before any table is
|
||||||
|
// consulted (BackendObject.h:196-205). The existing gate is
|
||||||
|
// AdvertisedLimitsScenario.ComputeWorkGroupLimitsAreTheCapsBlocksAnswer, which pins
|
||||||
|
// that this answer and the caps copy are ONE number.
|
||||||
|
switch (target) {
|
||||||
|
case GL_MAX_COMPUTE_WORK_GROUP_COUNT:
|
||||||
|
if (index < 3) *data = static_cast<GLint>(dynamic.MaxComputeWorkGroupCount[index]);
|
||||||
|
return;
|
||||||
|
case GL_MAX_COMPUTE_WORK_GROUP_SIZE:
|
||||||
|
if (index < 3) *data = static_cast<GLint>(dynamic.MaxComputeWorkGroupSize[index]);
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
// Not a Fatal: the slot's own contract is "whatever pname the frontend has no
|
||||||
|
// case for at all", and the monolith backends answer such a pname by leaving
|
||||||
|
// the driver's own default in place. Answering a wrong number would be worse
|
||||||
|
// than answering none.
|
||||||
|
MGLOG_W_ONCE("MG_Remote client: GetIntegeri_v(0x%04x, %u) is not one of the two "
|
||||||
|
"device-owned indexed pnames and has no caps-mirror answer",
|
||||||
|
static_cast<unsigned>(target), static_cast<unsigned>(index));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool AnswerIsTimerQuerySupported() {
|
||||||
|
// A capability predicate, not a call. Today a null slot means COUNTER_BITS == 0
|
||||||
|
// (GL_Query.cpp:792) - which is precisely the null check R-4 forbids, so it moves
|
||||||
|
// here, to the bit the server published.
|
||||||
|
return CapsMirrorInstance().HasCap(MG_Pipe::kCapTimerQuery);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// CLASS C - Fatal{UnmigratedVerb}. 64 slots: 63 in GLFunctionsTable + SetSwapInterval.
|
||||||
|
// =============================================================================
|
||||||
|
//
|
||||||
|
// The list is an X-macro so the DEFINITION and the ASSIGNMENT cannot drift apart, and
|
||||||
|
// so the count is arithmetic rather than a comment. Two of them carry a pre-verb hook
|
||||||
|
// before the Fatal - see the note on DispatchCompute.
|
||||||
|
|
||||||
|
#define MGR_UNMIGRATED_GL_SLOTS(X) \
|
||||||
|
X(DrawElements, void, (GLenum, GLsizei, GLenum, const void*)) \
|
||||||
|
X(DrawElementsBaseVertex, void, (GLenum, GLsizei, GLenum, const void*, GLint)) \
|
||||||
|
X(MultiDrawArrays, void, (GLenum, const GLint*, const GLsizei*, GLsizei)) \
|
||||||
|
X(MultiDrawElements, void, (GLenum, const GLsizei*, GLenum, const GLvoid* const*, GLsizei)) \
|
||||||
|
X(MultiDrawElementsBaseVertex, void, \
|
||||||
|
(GLenum, const GLsizei*, GLenum, const GLvoid* const*, GLsizei, const GLint*)) \
|
||||||
|
X(MultiDrawElementsIndirect, void, (GLenum, GLenum, const void*, GLsizei, GLsizei)) \
|
||||||
|
X(MultiDrawArraysIndirect, void, (GLenum, const void*, GLsizei, GLsizei)) \
|
||||||
|
X(MultiDrawElementsIndirectCount, void, (GLenum, GLenum, const void*, GLintptr, GLsizei, GLsizei)) \
|
||||||
|
X(MultiDrawArraysIndirectCount, void, (GLenum, const void*, GLintptr, GLsizei, GLsizei)) \
|
||||||
|
X(DrawRangeElementsBaseVertex, void, \
|
||||||
|
(GLenum, GLuint, GLuint, GLsizei, GLenum, const void*, GLint)) \
|
||||||
|
X(DrawRangeElements, void, (GLenum, GLuint, GLuint, GLsizei, GLenum, const void*)) \
|
||||||
|
X(DrawElementsInstancedBaseVertexBaseInstance, void, \
|
||||||
|
(GLenum, GLsizei, GLenum, const void*, GLsizei, GLint, GLuint)) \
|
||||||
|
X(DrawElementsInstancedBaseVertex, void, (GLenum, GLsizei, GLenum, const void*, GLsizei, GLint)) \
|
||||||
|
X(DrawElementsInstancedBaseInstance, void, \
|
||||||
|
(GLenum, GLsizei, GLenum, const void*, GLsizei, GLuint)) \
|
||||||
|
X(DrawElementsInstanced, void, (GLenum, GLsizei, GLenum, const void*, GLsizei)) \
|
||||||
|
X(DrawArraysInstancedBaseInstance, void, (GLenum, GLint, GLsizei, GLsizei, GLuint)) \
|
||||||
|
X(DrawArraysInstanced, void, (GLenum, GLint, GLsizei, GLsizei)) \
|
||||||
|
X(DrawElementsIndirect, void, (GLenum, GLenum, const void*)) \
|
||||||
|
X(DrawArraysIndirect, void, (GLenum, const void*)) \
|
||||||
|
X(ClearBufferfi, void, (GLenum, GLint, GLfloat, GLint)) \
|
||||||
|
X(ClearBufferfv, void, (GLenum, GLint, const GLfloat*)) \
|
||||||
|
X(ClearBufferuiv, void, (GLenum, GLint, const GLuint*)) \
|
||||||
|
X(ClearBufferiv, void, (GLenum, GLint, const GLint*)) \
|
||||||
|
X(ClearNamedFramebufferfv, void, \
|
||||||
|
(const SharedPtr<MG_State::GLState::FramebufferObject>&, GLenum, GLint, const GLfloat*)) \
|
||||||
|
X(ClearNamedFramebufferfi, void, \
|
||||||
|
(const SharedPtr<MG_State::GLState::FramebufferObject>&, GLenum, GLint, GLfloat, GLint)) \
|
||||||
|
X(ClearNamedFramebufferiv, void, \
|
||||||
|
(const SharedPtr<MG_State::GLState::FramebufferObject>&, GLenum, GLint, const GLint*)) \
|
||||||
|
X(ClearNamedFramebufferuiv, void, \
|
||||||
|
(const SharedPtr<MG_State::GLState::FramebufferObject>&, GLenum, GLint, const GLuint*)) \
|
||||||
|
X(BlitNamedFramebuffer, void, \
|
||||||
|
(const SharedPtr<MG_State::GLState::FramebufferObject>&, \
|
||||||
|
const SharedPtr<MG_State::GLState::FramebufferObject>&, GLint, GLint, GLint, GLint, GLint, \
|
||||||
|
GLint, GLint, GLint, GLbitfield, GLenum)) \
|
||||||
|
X(CopyTexImage2D, void, (GLenum, GLint, GLenum, GLint, GLint, GLsizei, GLsizei, GLint)) \
|
||||||
|
X(CopyTexSubImage2D, void, (GLenum, GLint, GLint, GLint, GLint, GLint, GLsizei, GLsizei)) \
|
||||||
|
X(CopyImageSubData, void, \
|
||||||
|
(const MG_Backend::CopyImageEndpoint&, GLenum, GLint, GLint, GLint, GLint, \
|
||||||
|
const MG_Backend::CopyImageEndpoint&, GLenum, GLint, GLint, GLint, GLint, GLsizei, GLsizei, \
|
||||||
|
GLsizei)) \
|
||||||
|
X(GenerateMipmap, void, (GLenum)) \
|
||||||
|
X(GetTexImage, void, (GLenum, GLint, GLenum, GLenum, GLvoid*)) \
|
||||||
|
X(GetTextureImage, void, \
|
||||||
|
(const SharedPtr<MG_State::GLState::ITextureObject>&, TextureUploadTarget, GLint, GLenum, \
|
||||||
|
GLenum, GLsizei, GLvoid*)) \
|
||||||
|
X(MemoryBarrier, void, (GLbitfield)) \
|
||||||
|
X(MemoryBarrierByRegion, void, (GLbitfield)) \
|
||||||
|
X(BindImageTexture, void, (GLuint, GLuint, GLint, GLboolean, GLint, GLenum, GLenum)) \
|
||||||
|
X(ShaderStorageBlockBinding, void, (GLuint, const GLchar*, GLuint)) \
|
||||||
|
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)) \
|
||||||
|
X(EndXfbPrimitivesQuery, void, (MG_Backend::BackendQueryHandle)) \
|
||||||
|
X(PatchParameteri, void, (GLenum, GLint)) \
|
||||||
|
X(BeginTransformFeedback, void, (GLenum)) \
|
||||||
|
X(EndTransformFeedback, void, ()) \
|
||||||
|
X(PauseTransformFeedback, void, ()) \
|
||||||
|
X(ResumeTransformFeedback, void, ()) \
|
||||||
|
X(BindTransformFeedback, void, (GLuint)) \
|
||||||
|
X(DeleteTransformFeedback, void, (GLuint))
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
#define MGR_UNMIGRATED_GL_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)) \
|
||||||
|
X(GetQueryResult64, Bool, (MG_Backend::BackendQueryHandle, Bool, Uint64*)) \
|
||||||
|
X(BeginOcclusionQuery, MG_Backend::BackendQueryHandle, ()) \
|
||||||
|
X(BeginXfbPrimitivesQuery, MG_Backend::BackendQueryHandle, (Bool)) \
|
||||||
|
X(GetGpuTimestampNs, Int64, ())
|
||||||
|
|
||||||
|
#define MGR_DEFINE_UNMIGRATED(Name, Ret, Sig) \
|
||||||
|
Ret Name##_Unmigrated Sig { UnmigratedVerbFatal(#Name); }
|
||||||
|
|
||||||
|
MGR_UNMIGRATED_GL_SLOTS(MGR_DEFINE_UNMIGRATED)
|
||||||
|
MGR_UNMIGRATED_GL_VALUE_SLOTS(MGR_DEFINE_UNMIGRATED)
|
||||||
|
#undef MGR_DEFINE_UNMIGRATED
|
||||||
|
|
||||||
|
// THE TWO COMPUTE SLOTS CARRY b1's DISPATCH HOOK BEFORE THE FATAL, and this is stated
|
||||||
|
// rather than hidden. MarkGpuWritesForDispatch() belongs immediately before the
|
||||||
|
// dispatch record, and the dispatch record is class C in P5 - so the call site is here,
|
||||||
|
// in the right place, and is UNREACHABLE-IN-EFFECT: the abort follows it. There is no
|
||||||
|
// gate on it and this file says so; the phase that moves DispatchCompute into class B
|
||||||
|
// replaces the Fatal and inherits a call site that is already correct rather than
|
||||||
|
// discovering that the mark walk was never wired.
|
||||||
|
void DispatchCompute_Unmigrated(GLuint, GLuint, GLuint) {
|
||||||
|
PushPersistentMapsBeforeVerb();
|
||||||
|
MarkGpuWritesForDispatch();
|
||||||
|
UnmigratedVerbFatal("DispatchCompute");
|
||||||
|
}
|
||||||
|
void DispatchComputeIndirect_Unmigrated(GLintptr) {
|
||||||
|
PushPersistentMapsBeforeVerb();
|
||||||
|
MarkGpuWritesForDispatch();
|
||||||
|
UnmigratedVerbFatal("DispatchComputeIndirect");
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetSwapInterval_Unmigrated(Int) { UnmigratedVerbFatal("SetSwapInterval"); }
|
||||||
|
|
||||||
|
// The counts, as arithmetic. MGR_COUNT_ONE expands to `+ 1` per row.
|
||||||
|
#define MGR_COUNT_ONE(Name, Ret, Sig) +1
|
||||||
|
constexpr Uint32 kUnmigratedListedSlots =
|
||||||
|
0 MGR_UNMIGRATED_GL_SLOTS(MGR_COUNT_ONE) MGR_UNMIGRATED_GL_VALUE_SLOTS(MGR_COUNT_ONE);
|
||||||
|
#undef MGR_COUNT_ONE
|
||||||
|
// + DispatchCompute, DispatchComputeIndirect, SetSwapInterval, written out by hand
|
||||||
|
// because they carry a body the macro cannot.
|
||||||
|
constexpr Uint32 kUnmigratedSlots = kUnmigratedListedSlots + 3;
|
||||||
|
constexpr Uint32 kLocallyAnsweredSlots = 2; // GetIntegeri_v, IsTimerQuerySupported
|
||||||
|
constexpr Uint32 kEmittedSlots = 5; // Clear, DrawArrays, ReadPixels, Blit, Present
|
||||||
|
|
||||||
|
static_assert(kUnmigratedSlots == 64, "CONTRACT-P5.md §7 class C is 64 slots");
|
||||||
|
static_assert(kLocallyAnsweredSlots + kEmittedSlots + kUnmigratedSlots == kRemoteEmitSlotCount,
|
||||||
|
"the three classes no longer partition the 71 slots");
|
||||||
|
|
||||||
|
MG_Backend::GlobalBackendFunctionsTable BuildRemoteEmitTable() {
|
||||||
|
g_dropClearEmission = ReadDropClearFromEnvironment();
|
||||||
|
MG_Backend::GlobalBackendFunctionsTable table{};
|
||||||
|
|
||||||
|
// ---- class C first, so that a slot forgotten below stays Fatal rather than null.
|
||||||
|
// Order matters for exactly this reason: if class B's assignment were first, a
|
||||||
|
// typo in class C would leave a NULL slot, and a null slot is 91 potential null
|
||||||
|
// calls with no diagnostic. This way the worst a mistake can do is name a verb
|
||||||
|
// that was supposed to be emitted, loudly.
|
||||||
|
#define MGR_ASSIGN_UNMIGRATED(Name, Ret, Sig) table.GL.Name = &Name##_Unmigrated;
|
||||||
|
MGR_UNMIGRATED_GL_SLOTS(MGR_ASSIGN_UNMIGRATED)
|
||||||
|
MGR_UNMIGRATED_GL_VALUE_SLOTS(MGR_ASSIGN_UNMIGRATED)
|
||||||
|
#undef MGR_ASSIGN_UNMIGRATED
|
||||||
|
table.GL.DispatchCompute = &DispatchCompute_Unmigrated;
|
||||||
|
table.GL.DispatchComputeIndirect = &DispatchComputeIndirect_Unmigrated;
|
||||||
|
table.SetSwapInterval = &SetSwapInterval_Unmigrated;
|
||||||
|
|
||||||
|
// ---- class A
|
||||||
|
table.GL.GetIntegeri_v = &AnswerGetIntegeri_v;
|
||||||
|
table.GL.IsTimerQuerySupported = &AnswerIsTimerQuerySupported;
|
||||||
|
// NOT A SLOT and not a verb: a Bool member of the table, whose one non-test client
|
||||||
|
// reader is GL_Query.cpp:221. It does NOT ride inside MGPCaps::Dynamic - it is a
|
||||||
|
// member of GLFunctionsTable, which is exactly the thing a split client never
|
||||||
|
// receives - so it is answered from kCapCpuXfbPrimitiveAccounting.
|
||||||
|
table.GL.PrefersCpuXfbPrimitiveAccounting =
|
||||||
|
CapsMirrorInstance().PrefersCpuXfbPrimitiveAccounting();
|
||||||
|
|
||||||
|
// ---- class B
|
||||||
|
table.GL.Clear = &EmitClear;
|
||||||
|
table.GL.DrawArrays = &EmitDrawArrays;
|
||||||
|
table.GL.ReadPixels = &EmitReadPixels;
|
||||||
|
table.GL.BlitFramebuffer = &EmitBlitFramebuffer;
|
||||||
|
table.Present = &EmitPresent;
|
||||||
|
|
||||||
|
return table;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
const MG_Backend::GlobalBackendFunctionsTable& RemoteEmitTable() {
|
const MG_Backend::GlobalBackendFunctionsTable& RemoteEmitTable() {
|
||||||
MGLOG_F("MGPipe: Fatal{UnimplementedEmitTable, \"RemoteEmitTable\"} - P5 package c1 has "
|
// Leaked at exit like every other MG_Remote singleton (ID-8): MG_Backend::Init()
|
||||||
"not landed this yet; c0 shipped the signature only");
|
// copies it into gBackendFunctionsTable and MobileGL::Destroy() clears that copy from
|
||||||
std::abort();
|
// an exit handler, by which point a static destructor here would already have run.
|
||||||
|
static const MG_Backend::GlobalBackendFunctionsTable& table =
|
||||||
|
*new MG_Backend::GlobalBackendFunctionsTable{BuildRemoteEmitTable()};
|
||||||
|
return table;
|
||||||
}
|
}
|
||||||
|
|
||||||
Uint32 ImplementedVerbCount() { return 0; }
|
Uint32 ImplementedVerbCount() { return kEmittedSlots; }
|
||||||
|
Uint32 LocallyAnsweredSlotCount() { return kLocallyAnsweredSlots; }
|
||||||
|
Uint32 UnmigratedSlotCount() { return kUnmigratedSlots; }
|
||||||
|
|
||||||
|
void SetDropClearEmissionForNegativeControl(Bool drop) { g_dropClearEmission = drop; }
|
||||||
|
Uint64 DroppedClearEmissions() { return g_droppedClearEmissions; }
|
||||||
|
|
||||||
} // namespace MobileGL::MG_Remote::Client
|
} // namespace MobileGL::MG_Remote::Client
|
||||||
|
|||||||
@@ -60,6 +60,22 @@
|
|||||||
|
|
||||||
namespace MobileGL::MG_Remote::Client {
|
namespace MobileGL::MG_Remote::Client {
|
||||||
|
|
||||||
|
// MGPClear::Kind. MGPipeTypes.h:1273 states the list as a COMMENT - "Whole | Color | Depth
|
||||||
|
// | Stencil | DepthStencil" - and mints no enumerator, because until P5 the record had no
|
||||||
|
// producer. These are the values, in that comment's own order, and they are here rather
|
||||||
|
// than in MGPipeTypes.h because that file is c0's and this phase produces exactly ONE of
|
||||||
|
// them: glClear is the only entry point that reaches the Clear slot (the four
|
||||||
|
// glClearBuffer* and the four glClearNamedFramebuffer* are class C). v1's
|
||||||
|
// WireVerbSink::OnClear must therefore Fatal on anything but Whole rather than guess, and
|
||||||
|
// the phase that migrates the other eight moves these into the contract.
|
||||||
|
enum MGRemoteClearKind : Uint32 {
|
||||||
|
kRemoteClearWhole = 0,
|
||||||
|
kRemoteClearColor = 1,
|
||||||
|
kRemoteClearDepth = 2,
|
||||||
|
kRemoteClearStencil = 3,
|
||||||
|
kRemoteClearDepthStencil = 4,
|
||||||
|
};
|
||||||
|
|
||||||
// The table MG_Backend::Init() installs into gBackendFunctionsTable for the remote role.
|
// The table MG_Backend::Init() installs into gBackendFunctionsTable for the remote role.
|
||||||
// A reference to a never-destroyed block, like every other MG_Remote singleton (ID-8).
|
// A reference to a never-destroyed block, like every other MG_Remote singleton (ID-8).
|
||||||
const MG_Backend::GlobalBackendFunctionsTable& RemoteEmitTable();
|
const MG_Backend::GlobalBackendFunctionsTable& RemoteEmitTable();
|
||||||
@@ -77,4 +93,25 @@ namespace MobileGL::MG_Remote::Client {
|
|||||||
// a slot added to GLFunctionsTable without a decision here is a build break.
|
// a slot added to GLFunctionsTable without a decision here is a build break.
|
||||||
inline constexpr Uint32 kRemoteEmitSlotCount = 71;
|
inline constexpr Uint32 kRemoteEmitSlotCount = 71;
|
||||||
|
|
||||||
|
// The other two thirds of the census, so a case can assert the WHOLE partition rather than
|
||||||
|
// only the half that emits. CONTRACT-P5.md §7's three classes are 2 + 5 + 64, and
|
||||||
|
// EmitTables.cpp static_asserts that they sum to kRemoteEmitSlotCount: a slot that quietly
|
||||||
|
// changes class shows up as a build break in the sum, not as a silent behaviour change.
|
||||||
|
Uint32 LocallyAnsweredSlotCount(); // class A - answered from the caps mirror, R-15
|
||||||
|
Uint32 UnmigratedSlotCount(); // class C - Fatal{UnmigratedVerb}
|
||||||
|
|
||||||
|
// THE E2 NEGATIVE CONTROL (t1's debt against c1, BRIEF §7). When set, the Clear emitter
|
||||||
|
// SKIPS its record - it still runs the pre-verb hooks and still returns - so a replay that
|
||||||
|
// is really going through the wire loses one clear per frame and its SSIM falls below the
|
||||||
|
// 0.99 threshold, while a replay that fell through to the driver is unaffected. It is a
|
||||||
|
// function rather than a knob in Config.h for two reasons: the control has to be settable
|
||||||
|
// from a test process that has already started, and a knob would be a
|
||||||
|
// MOBILEGL_IPC_-shaped name for something no operator may ever set.
|
||||||
|
//
|
||||||
|
// Emissions actually skipped, so the control can assert that it DID something rather than
|
||||||
|
// that a picture changed - a control that silently never fired is the third shape of R-16's
|
||||||
|
// "a gate that cannot go red for its own reason".
|
||||||
|
void SetDropClearEmissionForNegativeControl(Bool drop);
|
||||||
|
Uint64 DroppedClearEmissions();
|
||||||
|
|
||||||
} // namespace MobileGL::MG_Remote::Client
|
} // namespace MobileGL::MG_Remote::Client
|
||||||
|
|||||||
@@ -0,0 +1,135 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Remote/Client/SlotCaps.h
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
// THE 41 NULL CHECKS. Owner: package c1 (CONTRACT-P5.md §7, ID-14).
|
||||||
|
//
|
||||||
|
// FORTY-ONE OF THE SIXTY-NINE GLFunctionsTable SLOTS ARE NULL-CHECKED AT THEIR CALL SITE, AND
|
||||||
|
// SEVERAL OF THOSE CHECKS ARE CAPABILITY PROBES RATHER THAN SAFETY CHECKS. R-4 forbids a null
|
||||||
|
// slot in the client's emit table - so in that table every one of those probes would answer
|
||||||
|
// "supported", and whatever fallback sits behind it would silently disappear. That is not a
|
||||||
|
// theoretical risk: it is how a split lane produces a plausible picture for the wrong reason.
|
||||||
|
// ARCHITECTURE.md:114 already said what replaces the probe - "CallMask replaces 'is this table
|
||||||
|
// slot null' as the implicit capability probe" - and this header is the concrete list.
|
||||||
|
//
|
||||||
|
// TWO SPELLINGS, AND WHICH ONE A SITE TAKES IS DECIDED BY THE SLOT'S CLASS, NOT BY TASTE:
|
||||||
|
//
|
||||||
|
// MGL_BACKEND_SLOT_CAP(Slot, CapBit)
|
||||||
|
// The capability has a published bit. Under split the answer is the SERVER's bit;
|
||||||
|
// under monolith it is exactly today's null check, character for character.
|
||||||
|
// The three cases CONTRACT-P5.md §7 names by hand are all of this shape:
|
||||||
|
// GL_Query.cpp:481 / :558 / :785 BeginOcclusionQuery -> kCapOcclusionQuery
|
||||||
|
// GL_Query.cpp:534 BeginXfbPrimitivesQuery -> kCapXfbPrimitivesQuery
|
||||||
|
// PipeFill.cpp SubDataResident -> kCapResidentSubData
|
||||||
|
// (the third is not a GLFunctionsTable slot but an op-table one, so it reads the bit
|
||||||
|
// directly in PipeFill.cpp rather than through this header).
|
||||||
|
//
|
||||||
|
// MGL_BACKEND_SLOT_LOCAL(Slot)
|
||||||
|
// The capability has NO published bit and the slot is CONTRACT-P5.md §7 class C -
|
||||||
|
// Fatal{UnmigratedVerb} in the client's table. Under split the honest answer is
|
||||||
|
// "absent", which is precisely what the monolith nullptr meant, so every fallback the
|
||||||
|
// site already has survives instead of being replaced by an abort. Under monolith it is
|
||||||
|
// again today's null check.
|
||||||
|
//
|
||||||
|
// WHY "ABSENT" AND NOT "LET IT FATAL". A Fatal is loud, and for the 28 UNGUARDED slots it is
|
||||||
|
// strictly better than today (a null there is already an immediate crash with no diagnostic).
|
||||||
|
// But a guarded site is guarded because the frontend has a real answer for the absent case -
|
||||||
|
// always-signaled syncs, a CPU readback, CPU primitive accounting - and turning that answer
|
||||||
|
// into an abort is a behaviour change nobody asked for, in the direction that stops a lane
|
||||||
|
// dead. The class-C slot IS absent from this client; saying so is the accurate answer, not a
|
||||||
|
// weakening of R-4.
|
||||||
|
//
|
||||||
|
// THE TEST THAT DECIDES WHETHER A GUARDED SITE IS CONVERTED AT ALL, and it is the half of the
|
||||||
|
// walk the contract leaves to whoever does it: A PROBE MOVES ONLY WHERE "ABSENT" IS A CORRECT
|
||||||
|
// AND SUFFICIENT ANSWER. Where the fallback behind the probe produces a RIGHT result, "absent"
|
||||||
|
// is the accurate description of a client that does not have the slot, and converting keeps
|
||||||
|
// the lane running. Where the fallback produces a SILENTLY WRONG result, converting would
|
||||||
|
// manufacture exactly the defect R-4 exists to prevent, and the probe is left alone so the
|
||||||
|
// 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.
|
||||||
|
// 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
|
||||||
|
// the unsupported answer
|
||||||
|
// GL_Query.cpp the four query probes -> CPU primitive accounting / target rejection /
|
||||||
|
// COUNTER_BITS 0, all of them spec answers
|
||||||
|
//
|
||||||
|
// NOT converted, deliberately, because "absent" would be wrong rather than quiet:
|
||||||
|
// GL_Drawing.cpp:1274/:1371/:1420/:1435/:1641/:1673 the transform-feedback span family. The
|
||||||
|
// frontend reads a null EndTransformFeedback as "this backend does NOT own the capture,
|
||||||
|
// so reorder the captured records for it" (FixupGsStripCaptureOrder, :1290). Under split
|
||||||
|
// the server's backend DOES own it, so answering "absent" would reorder a capture that
|
||||||
|
// was already in GL order - a silently corrupt buffer. Transform feedback is off P5's
|
||||||
|
// reduced path (BRIEF §4's exclusion list) and its slots are class C, so the first XFB
|
||||||
|
// call aborts by name, which is the outcome R-4 asks for.
|
||||||
|
// GL_Drawing.cpp:844 PatchParameteri. "Absent" means the patch size is never set and every
|
||||||
|
// tessellation draw silently uses the previous one. Class C; it aborts by name.
|
||||||
|
//
|
||||||
|
// WHAT THIS HEADER DELIBERATELY DOES NOT DO. It does not touch the 28 unguarded slots: those
|
||||||
|
// have no probe to convert, and calling one reaches Fatal{UnmigratedVerb, "<slot>"} by name,
|
||||||
|
// which is R-4's intent. And it does not invent a cap bit - a new MGPCapBit is an
|
||||||
|
// MGPipeTypes.h edit and that file is c0's, so a family that needs one goes through the
|
||||||
|
// integrator (the XFB span family is the first that will).
|
||||||
|
//
|
||||||
|
// G1: in a build without MOBILEGL_BUILD_DISAGGREGATED both macros expand to the null check the
|
||||||
|
// site already had, so the pull build's code generation is unchanged.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <Includes.h>
|
||||||
|
|
||||||
|
#include <Config.h>
|
||||||
|
|
||||||
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||||
|
#include <MG_Pipe/MGPipeTypes.h>
|
||||||
|
#include <MG_Remote/Client/CapsMirror.h>
|
||||||
|
|
||||||
|
#define MGL_BACKEND_SLOT_CAP(Slot, CapBit) \
|
||||||
|
(::MobileGL::MG_Config::Transport != ::MobileGL::MG_Config::TransportMode::Monolith \
|
||||||
|
? ::MobileGL::MG_Remote::Client::CapsMirrorInstance().HasCap(CapBit) \
|
||||||
|
: ::MobileGL::MG_Backend::gBackendFunctionsTable.GL.Slot != nullptr)
|
||||||
|
|
||||||
|
#define MGL_BACKEND_SLOT_LOCAL(Slot) \
|
||||||
|
(::MobileGL::MG_Config::Transport == ::MobileGL::MG_Config::TransportMode::Monolith && \
|
||||||
|
::MobileGL::MG_Backend::gBackendFunctionsTable.GL.Slot != nullptr)
|
||||||
|
|
||||||
|
// The POINTER-valued forms, for the several sites shaped `if (const auto f = TABLE.GL.Slot)`.
|
||||||
|
// They exist for G1 and for nothing else: a site rewritten from that shape into
|
||||||
|
// `if (MGL_BACKEND_SLOT_CAP(...)) { const auto f = TABLE.GL.Slot; ... }` is semantically the
|
||||||
|
// same and generated DIFFERENT CODE - the first measurement of this change moved
|
||||||
|
// GL_Getter.cpp's GetInteger64v by -150 bytes and GetIntegerv by +2, which is two "resized"
|
||||||
|
// symbols and a red G1. In a pull build these two expand to the slot expression ITSELF, so the
|
||||||
|
// init-statement survives verbatim and the pull build's code generation cannot move.
|
||||||
|
#define MGL_BACKEND_SLOT_PTR_CAP(Slot, CapBit) \
|
||||||
|
(::MobileGL::MG_Config::Transport != ::MobileGL::MG_Config::TransportMode::Monolith \
|
||||||
|
? (::MobileGL::MG_Remote::Client::CapsMirrorInstance().HasCap(CapBit) \
|
||||||
|
? ::MobileGL::MG_Backend::gBackendFunctionsTable.GL.Slot \
|
||||||
|
: nullptr) \
|
||||||
|
: ::MobileGL::MG_Backend::gBackendFunctionsTable.GL.Slot)
|
||||||
|
|
||||||
|
#define MGL_BACKEND_SLOT_PTR_LOCAL(Slot) \
|
||||||
|
(::MobileGL::MG_Config::Transport != ::MobileGL::MG_Config::TransportMode::Monolith \
|
||||||
|
? nullptr \
|
||||||
|
: ::MobileGL::MG_Backend::gBackendFunctionsTable.GL.Slot)
|
||||||
|
|
||||||
|
#else
|
||||||
|
|
||||||
|
#define MGL_BACKEND_SLOT_CAP(Slot, CapBit) \
|
||||||
|
(::MobileGL::MG_Backend::gBackendFunctionsTable.GL.Slot != nullptr)
|
||||||
|
#define MGL_BACKEND_SLOT_LOCAL(Slot) \
|
||||||
|
(::MobileGL::MG_Backend::gBackendFunctionsTable.GL.Slot != nullptr)
|
||||||
|
#define MGL_BACKEND_SLOT_PTR_CAP(Slot, CapBit) \
|
||||||
|
(::MobileGL::MG_Backend::gBackendFunctionsTable.GL.Slot)
|
||||||
|
#define MGL_BACKEND_SLOT_PTR_LOCAL(Slot) \
|
||||||
|
(::MobileGL::MG_Backend::gBackendFunctionsTable.GL.Slot)
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -41,6 +41,13 @@
|
|||||||
#include <MG_Util/ShaderTranspiler/CompileEnv.h>
|
#include <MG_Util/ShaderTranspiler/CompileEnv.h>
|
||||||
#include <MG_Util/ShaderTranspiler/ShaderSourceProcessor.h>
|
#include <MG_Util/ShaderTranspiler/ShaderSourceProcessor.h>
|
||||||
#include <MG_Util/Debug/Log.h>
|
#include <MG_Util/Debug/Log.h>
|
||||||
|
|
||||||
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||||
|
// P5 c1 / R-8: the client's liveness gates read the caps mirror's consumer mask under split, so
|
||||||
|
// a split-armed case has to arm that half too - registering an op table is the SERVER's arming.
|
||||||
|
#include <MG_Remote/CapsCodec.h>
|
||||||
|
#include <MG_Remote/Client/CapsMirror.h>
|
||||||
|
#endif
|
||||||
#include <MG_Util/BackendLoaders/OpenGL/Loader.h>
|
#include <MG_Util/BackendLoaders/OpenGL/Loader.h>
|
||||||
#include <MG_Util/Types.h>
|
#include <MG_Util/Types.h>
|
||||||
#include <Config.h>
|
#include <Config.h>
|
||||||
@@ -4695,6 +4702,21 @@ TEST(DirectGLESBufferDrawProbe, UnderSplitTheRecordAloneAnswersTheLiveHostMapQue
|
|||||||
MG_Pipe::MGPipeSetResourceOps(&ops);
|
MG_Pipe::MGPipeSetResourceOps(&ops);
|
||||||
MG_Config::Transport = MG_Config::TransportMode::InProcess;
|
MG_Config::Transport = MG_Config::TransportMode::InProcess;
|
||||||
MG_Config::Ipc.PersistentBlockKb = 64;
|
MG_Config::Ipc.PersistentBlockKb = 64;
|
||||||
|
// P5 c1 / R-8: UNDER SPLIT THE OP TABLE IS NO LONGER THE ARMING CONDITION, and this case is
|
||||||
|
// the first place that shows. `MGPipeSetResourceOps(&ops)` is the SERVER's registration; the
|
||||||
|
// client's liveness gate now reads the caps mirror's consumer mask instead, because under a
|
||||||
|
// spawn the client process has no op table at all and reading one would silently stop five
|
||||||
|
// record families. So the probe has to arm BOTH halves - and the fact that it did not is the
|
||||||
|
// defect R-8 exists to catch, reproduced here by a change rather than argued about.
|
||||||
|
const Uint64 previousCapsGeneration = MG_Remote::Client::CapsMirrorInstance().Generation();
|
||||||
|
{
|
||||||
|
MG_Pipe::MGPCaps caps{};
|
||||||
|
caps.CallMask = MG_Remote::MGCapsConsumerBits(MG_Pipe::kMGPipeSubsystemResources);
|
||||||
|
MG_Remote::Client::CapsMirrorInstance().Adopt(caps, MG_Backend::FormatCapabilityCache{},
|
||||||
|
RendererInfo{}, String{},
|
||||||
|
BackendType::DirectGLES);
|
||||||
|
}
|
||||||
|
(void)previousCapsGeneration;
|
||||||
|
|
||||||
{
|
{
|
||||||
// The constructor mints the handle and emits resource_create; Respecify emits the
|
// The constructor mints the handle and emits resource_create; Respecify emits the
|
||||||
|
|||||||
@@ -65,3 +65,30 @@ if (MSVC)
|
|||||||
endif ()
|
endif ()
|
||||||
|
|
||||||
gtest_discover_tests(PipeWireCodecTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
gtest_discover_tests(PipeWireCodecTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||||
|
|
||||||
|
# P5 c1's suite: the 71-slot emit table, the caps mirror and R-8's liveness gates. Registered
|
||||||
|
# on its own for PipeWireCodecTest's reason - MGLOG_F writes to stdout and to a named file and
|
||||||
|
# NEVER to stderr, so a Fatal arm can only be asserted by forking and reading that file back,
|
||||||
|
# which needs a main() of its own.
|
||||||
|
add_executable(RemoteClientTest RemoteClientTest.cpp)
|
||||||
|
|
||||||
|
target_include_directories(RemoteClientTest PRIVATE
|
||||||
|
${MGL_ROOT}/include
|
||||||
|
${MGL_ROOT}/MobileGL
|
||||||
|
${MGL_ROOT}/MobileGL/MG_Pipe
|
||||||
|
${MGL_ROOT}/3rdparty/flatbuffers/include
|
||||||
|
${MGL_ROOT}/3rdparty/xxHash
|
||||||
|
${MGL_ROOT}/3rdparty/Vulkan-Headers/include
|
||||||
|
${MGL_ROOT}/3rdparty/SPIRV-Reflect
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(RemoteClientTest PRIVATE
|
||||||
|
GTest::gtest
|
||||||
|
${LINK_LIBRARIES}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (MSVC)
|
||||||
|
target_compile_options(RemoteClientTest PRIVATE /Zc:preprocessor)
|
||||||
|
endif ()
|
||||||
|
|
||||||
|
gtest_discover_tests(RemoteClientTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||||
|
|||||||
@@ -0,0 +1,419 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Test/Wire/RemoteClientTest.cpp
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
// P5 package c1's suite: the 71-slot emit table, the caps mirror and R-8's liveness gates. No
|
||||||
|
// session, no transport and no thread - s1's SessionTest owns those and w1's PipeWireCodecTest
|
||||||
|
// owns the bytes.
|
||||||
|
//
|
||||||
|
// IT LINKS gtest RATHER THAN gtest_main AND CARRIES ITS OWN main(), for PipeWireCodecTest's and
|
||||||
|
// PipeInputsTest's reason: the Fatal arms report through MGLOG_F + std::abort, and MGLOG_F
|
||||||
|
// writes to STDOUT and to a named file, NEVER to stderr - so EXPECT_DEATH's stderr regex could
|
||||||
|
// only ever match the empty string. A case that drives one FORKS and reads the Fatal line back
|
||||||
|
// out of a log file this process names before anything logs. That is what makes each control
|
||||||
|
// assert ITS OWN failure string (R-16) instead of asserting that something, somewhere, died.
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <cstring>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <sstream>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "Includes.h"
|
||||||
|
|
||||||
|
#include <Config.h>
|
||||||
|
#include <MG_Pipe/MGPipe.h>
|
||||||
|
#include <MG_Remote/CapsCodec.h>
|
||||||
|
#include <MG_Remote/Client/CapsMirror.h>
|
||||||
|
#include <MG_Remote/Client/EmitTables.h>
|
||||||
|
|
||||||
|
#if !defined(_WIN32)
|
||||||
|
#include <csignal>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#define MGTEST_HAVE_FORK 1
|
||||||
|
#else
|
||||||
|
#define MGTEST_HAVE_FORK 0
|
||||||
|
#endif
|
||||||
|
|
||||||
|
using namespace MobileGL;
|
||||||
|
using namespace MobileGL::MG_Pipe;
|
||||||
|
using namespace MobileGL::MG_Remote;
|
||||||
|
using namespace MobileGL::MG_Remote::Client;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
std::string g_logPath;
|
||||||
|
|
||||||
|
std::string ReadLog() {
|
||||||
|
std::ifstream in(g_logPath, std::ios::binary);
|
||||||
|
if (!in) return {};
|
||||||
|
std::ostringstream out;
|
||||||
|
out << in.rdbuf();
|
||||||
|
return out.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
int ProcessId() {
|
||||||
|
#if defined(_WIN32)
|
||||||
|
return static_cast<int>(::_getpid());
|
||||||
|
#else
|
||||||
|
return static_cast<int>(::getpid());
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
// A caps snapshot the client could plausibly have received, with every field distinct from
|
||||||
|
// its default so a mirror that answered from a zeroed struct cannot look like one that
|
||||||
|
// adopted. THIS IS NOT THE STATE UNDER TEST - it is the INPUT to Adopt(), which is the
|
||||||
|
// producer; the assertions below read what the mirror hands the frontend's own accessors
|
||||||
|
// back, never what this function wrote (R-16).
|
||||||
|
struct Snapshot {
|
||||||
|
MGPCaps Caps{};
|
||||||
|
MG_Backend::FormatCapabilityCache Formats{};
|
||||||
|
RendererInfo Renderer{};
|
||||||
|
String ApiVersion;
|
||||||
|
BackendType Backend = BackendType::DirectGLES;
|
||||||
|
};
|
||||||
|
|
||||||
|
Snapshot MakeSnapshot(Uint64 consumedSubsystems, Uint64 capBits) {
|
||||||
|
Snapshot s;
|
||||||
|
s.Caps.CallMask = capBits | MGCapsConsumerBits(consumedSubsystems);
|
||||||
|
s.Caps.Dynamic.MaxComputeWorkGroupCount[0] = 65531;
|
||||||
|
s.Caps.Dynamic.MaxComputeWorkGroupCount[1] = 65532;
|
||||||
|
s.Caps.Dynamic.MaxComputeWorkGroupCount[2] = 65533;
|
||||||
|
s.Caps.Dynamic.MaxComputeWorkGroupSize[0] = 1021;
|
||||||
|
s.Caps.Dynamic.MaxComputeWorkGroupSize[1] = 1022;
|
||||||
|
s.Caps.Dynamic.MaxComputeWorkGroupSize[2] = 1023;
|
||||||
|
s.Caps.Dynamic.UniformBufferOffsetAlignment = 64;
|
||||||
|
s.Renderer.RendererName = "MobileGL Remote Test Renderer";
|
||||||
|
s.Renderer.BackendName = "Espryt";
|
||||||
|
s.Renderer.ExtraVendor = String{"c1"};
|
||||||
|
s.Renderer.RendererGLInfo.TargetGLVersion = Version{4, 6, 0, {}, {}};
|
||||||
|
s.Renderer.RendererGLInfo.TargetGLSLVersion = Version{4, 6, 0, {}, {}};
|
||||||
|
s.ApiVersion = "4.6";
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AdoptSnapshot(const Snapshot& s) {
|
||||||
|
CapsMirrorInstance().Adopt(s.Caps, s.Formats, s.Renderer, s.ApiVersion, s.Backend);
|
||||||
|
}
|
||||||
|
|
||||||
|
#if MGTEST_HAVE_FORK
|
||||||
|
struct ChildResult {
|
||||||
|
int Status = -1;
|
||||||
|
std::string Log;
|
||||||
|
};
|
||||||
|
|
||||||
|
template <class Body>
|
||||||
|
ChildResult RunInChild(Body body) {
|
||||||
|
ChildResult result;
|
||||||
|
// TRUNCATE, RATHER THAN REMEMBER AN OFFSET, and the difference is not cosmetic: this
|
||||||
|
// parent never logs, so MG_Util::Debug's FILE* is opened FOR THE FIRST TIME by each
|
||||||
|
// child - with "w", which truncates. An offset taken before the fork therefore points
|
||||||
|
// past the end of the child's own log, and `substr` hands back the tail of a DIFFERENT
|
||||||
|
// child's output. That is how the second death case in this file came to see the first
|
||||||
|
// one's slot name and assert on it: a control reading another control's message, which
|
||||||
|
// is one of the three shapes R-16 was written after.
|
||||||
|
{ std::ofstream truncate(g_logPath, std::ios::trunc | std::ios::binary); }
|
||||||
|
std::fflush(nullptr);
|
||||||
|
const pid_t pid = ::fork();
|
||||||
|
if (pid < 0) return result;
|
||||||
|
if (pid == 0) {
|
||||||
|
body();
|
||||||
|
::_exit(0);
|
||||||
|
}
|
||||||
|
int status = 0;
|
||||||
|
if (::waitpid(pid, &status, 0) != pid) return result;
|
||||||
|
result.Status = status;
|
||||||
|
result.Log = ReadLog();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DiedOfAbort(const ChildResult& r) {
|
||||||
|
return WIFSIGNALED(r.Status) && WTERMSIG(r.Status) == SIGABRT;
|
||||||
|
}
|
||||||
|
std::string DescribeStatus(const ChildResult& r) {
|
||||||
|
if (r.Status < 0) return "fork/waitpid failed";
|
||||||
|
if (WIFEXITED(r.Status)) return "exited " + std::to_string(WEXITSTATUS(r.Status));
|
||||||
|
if (WIFSIGNALED(r.Status)) return "signal " + std::to_string(WTERMSIG(r.Status));
|
||||||
|
return "status " + std::to_string(r.Status);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// =====================================================================================
|
||||||
|
// The emit table: the partition, and that no slot is null
|
||||||
|
// =====================================================================================
|
||||||
|
|
||||||
|
TEST(RemoteEmitTable, TheThreeClassesPartitionAllSeventyOneSlots) {
|
||||||
|
// CONTRACT-P5.md §7: 2 answered locally + 5 emitted + 64 Fatal. Read from the functions the
|
||||||
|
// table itself reports with - which is also what t1's arming condition reads - rather than
|
||||||
|
// recomputed here, so a table that lost an emitter cannot look like one that never had it.
|
||||||
|
EXPECT_EQ(LocallyAnsweredSlotCount(), 2u);
|
||||||
|
EXPECT_EQ(ImplementedVerbCount(), 5u);
|
||||||
|
EXPECT_EQ(UnmigratedSlotCount(), 64u);
|
||||||
|
EXPECT_EQ(LocallyAnsweredSlotCount() + ImplementedVerbCount() + UnmigratedSlotCount(),
|
||||||
|
kRemoteEmitSlotCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(RemoteEmitTable, NoSlotIsNull) {
|
||||||
|
// R-4's whole rule, asserted over the STRUCT rather than over the list that built it. 91
|
||||||
|
// MG_Impl sites call through this table directly; a null slot is 91 potential null calls,
|
||||||
|
// and the one thing a list-driven check could not catch is a slot the list forgot to name.
|
||||||
|
//
|
||||||
|
// Walked as a block of function pointers because that is exactly what the struct is - the
|
||||||
|
// static_asserts in EmitTables.cpp pin that shape - so a slot ADDED to GLFunctionsTable is
|
||||||
|
// covered here on the day it appears, without this file being edited.
|
||||||
|
const MG_Backend::GlobalBackendFunctionsTable& table = RemoteEmitTable();
|
||||||
|
const void* const* cells = reinterpret_cast<const void* const*>(&table);
|
||||||
|
const SizeT cellCount = sizeof(table) / sizeof(void*);
|
||||||
|
// The struct is 71 function pointers plus ONE cell holding the packed Bool
|
||||||
|
// PrefersCpuXfbPrimitiveAccounting and its padding (EmitTables.cpp static_asserts exactly
|
||||||
|
// that shape). That Bool is legitimately zero when the server did not publish
|
||||||
|
// kCapCpuXfbPrimitiveAccounting, so at most one cell may read null - and this is stated as
|
||||||
|
// a bound rather than an index, because an index would drift the day a slot is inserted.
|
||||||
|
SizeT nullCells = 0;
|
||||||
|
for (SizeT i = 0; i < cellCount; ++i) {
|
||||||
|
if (cells[i] == nullptr) ++nullCells;
|
||||||
|
}
|
||||||
|
EXPECT_LE(nullCells, 1u)
|
||||||
|
<< "a slot in the remote emit table is null (" << nullCells << " null cells out of "
|
||||||
|
<< cellCount
|
||||||
|
<< "). R-4 forbids it: 91 MG_Impl sites call through this table with no null check at all";
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(RemoteEmitTable, TheFiveEmittersAreTheOnesTheCensusMeasured) {
|
||||||
|
const MG_Backend::GlobalBackendFunctionsTable& table = RemoteEmitTable();
|
||||||
|
// Named, so that a table which emitted a DIFFERENT five would be red rather than merely
|
||||||
|
// counted. The census's answer is Clear, DrawArrays, ReadPixels, BlitFramebuffer, Present.
|
||||||
|
EXPECT_NE(table.GL.Clear, nullptr);
|
||||||
|
EXPECT_NE(table.GL.DrawArrays, nullptr);
|
||||||
|
EXPECT_NE(table.GL.ReadPixels, nullptr);
|
||||||
|
EXPECT_NE(table.GL.BlitFramebuffer, nullptr);
|
||||||
|
EXPECT_NE(table.Present, nullptr);
|
||||||
|
// And the two R-15 answers them locally, so they are not the same pointer as any Fatal one.
|
||||||
|
EXPECT_NE(table.GL.GetIntegeri_v, nullptr);
|
||||||
|
EXPECT_NE(table.GL.IsTimerQuerySupported, nullptr);
|
||||||
|
EXPECT_NE(reinterpret_cast<const void*>(table.GL.DrawArrays),
|
||||||
|
reinterpret_cast<const void*>(table.GL.DrawElements))
|
||||||
|
<< "DrawArrays is class B and DrawElements is class C; they cannot share a thunk";
|
||||||
|
}
|
||||||
|
|
||||||
|
#if MGTEST_HAVE_FORK
|
||||||
|
TEST(RemoteEmitTable, AnUnmigratedSlotAbortsAndNamesItself) {
|
||||||
|
// THE DEATH TEST ON THE UnmigratedVerbFatal ARM. It asserts the exact wording, not merely
|
||||||
|
// that the child died: a control that trips on any abort is satisfied by the wrong abort,
|
||||||
|
// which is one of the three shapes R-16 was written after.
|
||||||
|
const ChildResult r = RunInChild([] {
|
||||||
|
RemoteEmitTable().GL.DrawElements(0x0004 /*GL_TRIANGLES*/, 3, 0x1405 /*GL_UNSIGNED_INT*/,
|
||||||
|
nullptr);
|
||||||
|
});
|
||||||
|
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
|
||||||
|
EXPECT_NE(r.Log.find("Fatal{UnmigratedVerb, \"DrawElements\"}"), std::string::npos) << r.Log;
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(RemoteEmitTable, EachUnmigratedSlotNamesItsOwnSlot) {
|
||||||
|
// The half the case above cannot state on its own: that the name in the message is the
|
||||||
|
// slot's and not a constant. Two different slots, two different names.
|
||||||
|
const ChildResult r = RunInChild([] { RemoteEmitTable().GL.GenerateMipmap(0x0DE1); });
|
||||||
|
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
|
||||||
|
EXPECT_NE(r.Log.find("Fatal{UnmigratedVerb, \"GenerateMipmap\"}"), std::string::npos) << r.Log;
|
||||||
|
EXPECT_EQ(r.Log.find("DrawElements"), std::string::npos)
|
||||||
|
<< "the Fatal message names a slot other than the one that was called:\n"
|
||||||
|
<< r.Log;
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(RemoteEmitTable, SetSwapIntervalIsClassCAndSaysSo) {
|
||||||
|
// The slot the verb census found by NOT mirroring GLImpl: SetSwapInterval has zero MG_Impl
|
||||||
|
// call sites and is reached only through the EGL path, so a table built from the 89 GLImpl
|
||||||
|
// sites would have left it null.
|
||||||
|
const ChildResult r = RunInChild([] { RemoteEmitTable().SetSwapInterval(1); });
|
||||||
|
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
|
||||||
|
EXPECT_NE(r.Log.find("Fatal{UnmigratedVerb, \"SetSwapInterval\"}"), std::string::npos) << r.Log;
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(RemoteEmitTable, AClassBSlotWithNoSessionAbortsRatherThanFallingThrough) {
|
||||||
|
// The other half of "no slot may fall through to the driver". With no ClientSession the
|
||||||
|
// emitter has nowhere to put the record, and the one thing it may not do is return quietly:
|
||||||
|
// that is the split lane running monolith and going green.
|
||||||
|
const ChildResult r = RunInChild([] { RemoteEmitTable().GL.Clear(0x4000 /*COLOR_BUFFER_BIT*/); });
|
||||||
|
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
|
||||||
|
EXPECT_NE(r.Log.find("Fatal{NoClientSession, \"Clear\"}"), std::string::npos) << r.Log;
|
||||||
|
}
|
||||||
|
#endif // MGTEST_HAVE_FORK
|
||||||
|
|
||||||
|
// =====================================================================================
|
||||||
|
// The caps mirror: the three read paths the acceptance names
|
||||||
|
// =====================================================================================
|
||||||
|
|
||||||
|
TEST(CapsMirrorTest, GlGetStringReadsTheRendererStringsBackOutOfTheMirror) {
|
||||||
|
// glGetString's path is GL_Getter.cpp:596 -> pActiveBackendObject->GetRendererInfo(), which
|
||||||
|
// BackendObject_Remote answers from this mirror BY REFERENCE - so the test reads the
|
||||||
|
// reference, holds it across a second adoption, and requires it to follow. A mirror that
|
||||||
|
// handed back a temporary would pass an equality check and dangle here.
|
||||||
|
const Snapshot first = MakeSnapshot(kMGPipeSubsystemResources, 0);
|
||||||
|
AdoptSnapshot(first);
|
||||||
|
const RendererInfo& bound = CapsMirrorInstance().Renderer();
|
||||||
|
EXPECT_EQ(bound.RendererName, "MobileGL Remote Test Renderer");
|
||||||
|
EXPECT_EQ(bound.BackendName, "Espryt");
|
||||||
|
ASSERT_TRUE(bound.ExtraVendor.has_value());
|
||||||
|
EXPECT_EQ(*bound.ExtraVendor, "c1");
|
||||||
|
|
||||||
|
Snapshot second = MakeSnapshot(kMGPipeSubsystemResources, 0);
|
||||||
|
second.Renderer.RendererName = "A Different Device";
|
||||||
|
AdoptSnapshot(second);
|
||||||
|
EXPECT_EQ(bound.RendererName, "A Different Device")
|
||||||
|
<< "GetRendererInfo() returns a reference, so a re-arrival must be visible through a "
|
||||||
|
"reference a caller already holds";
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(CapsMirrorTest, GlGetIntegervReadsTheDynamicParametersBackOutOfTheMirror) {
|
||||||
|
// glGetIntegerv's limit family is GL_Getter.cpp:2400 -> GetDynamicParameters(), which binds
|
||||||
|
// a reference and then reads many members - which is why a partial snapshot is not an
|
||||||
|
// option and why the whole struct crosses.
|
||||||
|
AdoptSnapshot(MakeSnapshot(kMGPipeSubsystemResources, 0));
|
||||||
|
const MG_Backend::DynamicBackendParameters& dynamic = CapsMirrorInstance().Dynamic();
|
||||||
|
EXPECT_EQ(dynamic.MaxComputeWorkGroupCount[0], 65531);
|
||||||
|
EXPECT_EQ(dynamic.MaxComputeWorkGroupCount[2], 65533);
|
||||||
|
EXPECT_EQ(dynamic.MaxComputeWorkGroupSize[1], 1022);
|
||||||
|
EXPECT_EQ(dynamic.UniformBufferOffsetAlignment, 64u);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(CapsMirrorTest, GlGetStringiReadsTheAdvertisedExtensionListBackOutOfTheMirror) {
|
||||||
|
// glGetStringi(GL_EXTENSIONS) is GL_Getter.cpp:654 -> GetRendererInfo().RendererGLInfo, the
|
||||||
|
// same list CompileEnv.cpp:124 copies into the compile env.
|
||||||
|
Snapshot s = MakeSnapshot(kMGPipeSubsystemResources, 0);
|
||||||
|
s.Renderer.RendererGLInfo.Extensions.push_back(E_GL_ARB_timer_query);
|
||||||
|
AdoptSnapshot(s);
|
||||||
|
const auto& extensions = CapsMirrorInstance().Renderer().RendererGLInfo.Extensions;
|
||||||
|
ASSERT_EQ(extensions.size(), 1u);
|
||||||
|
EXPECT_EQ(extensions[0], E_GL_ARB_timer_query);
|
||||||
|
EXPECT_EQ(CapsMirrorInstance().Renderer().RendererGLInfo.TargetGLVersion.Major, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(CapsMirrorTest, ReArrivalIsTheInvalidationAndMovesTheGeneration) {
|
||||||
|
// R-12 has no Invalidate(), so Generation() is the ONLY thing on the client that can see a
|
||||||
|
// server context death - and a client memo has to key on it.
|
||||||
|
const Uint64 before = CapsMirrorInstance().Generation();
|
||||||
|
AdoptSnapshot(MakeSnapshot(kMGPipeSubsystemResources, 0));
|
||||||
|
const Uint64 after = CapsMirrorInstance().Generation();
|
||||||
|
EXPECT_EQ(after, before + 1);
|
||||||
|
AdoptSnapshot(MakeSnapshot(kMGPipeSubsystemResources, 0));
|
||||||
|
EXPECT_EQ(CapsMirrorInstance().Generation(), after + 1);
|
||||||
|
EXPECT_TRUE(CapsMirrorInstance().Valid());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(CapsMirrorTest, TheBackendTypeIsTheServersAndNeverANewEnumerator) {
|
||||||
|
Snapshot s = MakeSnapshot(kMGPipeSubsystemResources, 0);
|
||||||
|
s.Backend = BackendType::DirectVulkan;
|
||||||
|
AdoptSnapshot(s);
|
||||||
|
EXPECT_EQ(CapsMirrorInstance().Backend(), BackendType::DirectVulkan);
|
||||||
|
s.Backend = BackendType::DirectGLES;
|
||||||
|
AdoptSnapshot(s);
|
||||||
|
EXPECT_EQ(CapsMirrorInstance().Backend(), BackendType::DirectGLES);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(CapsMirrorTest, ThePrefersCpuXfbAnswerComesFromTheCapBitAndNotFromTheTable) {
|
||||||
|
// GLFunctionsTable::PrefersCpuXfbPrimitiveAccounting is a member of the FUNCTION TABLE,
|
||||||
|
// which is exactly the thing a split client never receives - so it cannot ride in
|
||||||
|
// MGPCaps::Dynamic and must come from kCapCpuXfbPrimitiveAccounting.
|
||||||
|
AdoptSnapshot(MakeSnapshot(kMGPipeSubsystemResources, 0));
|
||||||
|
EXPECT_FALSE(CapsMirrorInstance().PrefersCpuXfbPrimitiveAccounting());
|
||||||
|
AdoptSnapshot(MakeSnapshot(kMGPipeSubsystemResources, kCapCpuXfbPrimitiveAccounting));
|
||||||
|
EXPECT_TRUE(CapsMirrorInstance().PrefersCpuXfbPrimitiveAccounting());
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================================================================================
|
||||||
|
// R-8: the liveness gates read the caps mirror, and a family with no consumer says so
|
||||||
|
// =====================================================================================
|
||||||
|
|
||||||
|
TEST(CapsMirrorTest, AMaskWithoutAFamilyRefusesItAndNamesIt) {
|
||||||
|
// R-8's NEGATIVE CONTROL. "The client emits nothing for a family the server does not
|
||||||
|
// consume" is, on its own, indistinguishable from "nothing called it" - so the refusal is
|
||||||
|
// COUNTED at the one funnel that answers the question, and the count is what this asserts.
|
||||||
|
AdoptSnapshot(MakeSnapshot(kMGPipeSubsystemPrograms, 0));
|
||||||
|
ResetConsumerRefusalsForTest();
|
||||||
|
|
||||||
|
EXPECT_TRUE(CapsMirrorInstance().ServerConsumes(kMGPipeSubsystemPrograms));
|
||||||
|
EXPECT_EQ(ConsumerRefusals(), 0u) << "a family the server DOES consume must not be counted "
|
||||||
|
"as refused";
|
||||||
|
|
||||||
|
EXPECT_FALSE(CapsMirrorInstance().ServerConsumes(kMGPipeSubsystemResources));
|
||||||
|
EXPECT_EQ(ConsumerRefusals(), 1u);
|
||||||
|
EXPECT_EQ(LastRefusedSubsystem(), kMGPipeSubsystemResources)
|
||||||
|
<< "the refusal must name the family; a counter that only says 'something was withheld' "
|
||||||
|
"cannot tell five silent families apart";
|
||||||
|
|
||||||
|
EXPECT_FALSE(CapsMirrorInstance().ServerConsumes(kMGPipeSubsystemTextureResources));
|
||||||
|
EXPECT_EQ(ConsumerRefusals(), 2u);
|
||||||
|
EXPECT_EQ(LastRefusedSubsystem(), kMGPipeSubsystemTextureResources);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(CapsMirrorTest, APlaceholderMirrorConsumesNothing) {
|
||||||
|
// The safe direction, stated as a case. With no snapshot the mask is zero, every family
|
||||||
|
// answers "no consumer", the client emits nothing and the legacy pull path runs. The unsafe
|
||||||
|
// direction - emitting to a server that has no consumer - is ID-39's 66 lost uploads.
|
||||||
|
MGPCaps empty{};
|
||||||
|
CapsMirror mirror;
|
||||||
|
EXPECT_FALSE(mirror.Valid());
|
||||||
|
EXPECT_FALSE(mirror.ServerConsumes(kMGPipeSubsystemResources));
|
||||||
|
EXPECT_FALSE(mirror.ServerConsumes(kMGPipeSubsystemPrograms));
|
||||||
|
EXPECT_FALSE(mirror.HasCap(kCapResidentSubData));
|
||||||
|
(void)empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(CapsMirrorTest, TheConsumerBlockDoesNotCollideWithTheFeatureBits) {
|
||||||
|
// The two halves of CallMask, asserted against each other rather than against a constant:
|
||||||
|
// bits 0..8 are MGPCapBit and bits 32..47 are the consumer mask, and the whole reason R-8
|
||||||
|
// became implementable is that they do not overlap.
|
||||||
|
AdoptSnapshot(MakeSnapshot(kMGPipeSubsystemResources | kMGPipeSubsystemPrograms,
|
||||||
|
kCapTimerQuery | kCapOcclusionQuery));
|
||||||
|
EXPECT_TRUE(CapsMirrorInstance().HasCap(kCapTimerQuery));
|
||||||
|
EXPECT_TRUE(CapsMirrorInstance().HasCap(kCapOcclusionQuery));
|
||||||
|
EXPECT_FALSE(CapsMirrorInstance().HasCap(kCapXfbPrimitivesQuery));
|
||||||
|
EXPECT_TRUE(CapsMirrorInstance().ServerConsumes(kMGPipeSubsystemResources));
|
||||||
|
EXPECT_TRUE(CapsMirrorInstance().ServerConsumes(kMGPipeSubsystemPrograms));
|
||||||
|
EXPECT_FALSE(CapsMirrorInstance().ServerConsumes(kMGPipeSubsystemSamplers));
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================================================================================
|
||||||
|
// E2's emitter-drop control: the switch itself, driven through the real emitter's own counter
|
||||||
|
// =====================================================================================
|
||||||
|
|
||||||
|
TEST(RemoteEmitTable, TheE2DropSwitchStartsDisarmed) {
|
||||||
|
// The half a unit case can state. E2's statement - "drop one Clear emission and OpenRA's
|
||||||
|
// SSIM falls below 0.99" - is a TRACE LANE's, because the picture is the thing it is about;
|
||||||
|
// what belongs here is that the control is off unless someone armed it, so a lane that
|
||||||
|
// forgot to disarm cannot look like a lane that was never armed.
|
||||||
|
EXPECT_EQ(DroppedClearEmissions(), 0u);
|
||||||
|
SetDropClearEmissionForNegativeControl(true);
|
||||||
|
SetDropClearEmissionForNegativeControl(false);
|
||||||
|
EXPECT_EQ(DroppedClearEmissions(), 0u)
|
||||||
|
<< "arming and disarming the control must not, by itself, drop anything";
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char** argv) {
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
const fs::path path =
|
||||||
|
fs::temp_directory_path() / ("mobilegl-remoteclient-test-" + std::to_string(ProcessId()) + ".log");
|
||||||
|
std::error_code ec;
|
||||||
|
fs::remove(path, ec);
|
||||||
|
g_logPath = path.string();
|
||||||
|
#if defined(_WIN32)
|
||||||
|
_putenv_s("MOBILEGL_LOG_FILE_PATH", g_logPath.c_str());
|
||||||
|
#else
|
||||||
|
setenv("MOBILEGL_LOG_FILE_PATH", g_logPath.c_str(), 1);
|
||||||
|
#endif
|
||||||
|
::testing::InitGoogleTest(&argc, argv);
|
||||||
|
const int rc = RUN_ALL_TESTS();
|
||||||
|
fs::remove(path, ec);
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user