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/EmitTables.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
|
||||
# MarkGpuWritten producers are on the server's side of the line, and the
|
||||
# block-granularity persistent-map push that tier T2 makes mandatory.
|
||||
|
||||
@@ -31,6 +31,11 @@
|
||||
#include <MG_Util/ShaderTranspiler/Types.h>
|
||||
#include <MG_Backend/BackendObjects.h>
|
||||
#include <MG_Impl/Pipe/PipeFill.h>
|
||||
// CONTRACT-P5.md §7 / ID-14: a null check on a GLFunctionsTable slot may not survive into the
|
||||
// client under split - it becomes a caps-mirror read. SlotCaps.h carries the rule and the test
|
||||
// that decides which of its two spellings a site takes; in a pull build both expand to exactly
|
||||
// the check they replaced.
|
||||
#include <MG_Remote/Client/SlotCaps.h>
|
||||
|
||||
namespace MobileGL::MG_Impl::GLImpl {
|
||||
// Declared rather than #included from GL_RenderState.h on purpose: that header also declares
|
||||
@@ -1354,7 +1359,16 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// full 64-bit GPU timestamp survives; LWJGL reads it this way.
|
||||
Int64 timestamp = 0;
|
||||
if (!MG_Config::Features.DisableTimerQuery) {
|
||||
if (const auto getGpuTimestampNs = MG_Backend::gBackendFunctionsTable.GL.GetGpuTimestampNs) {
|
||||
// glGetInteger64v(GL_TIMESTAMP). GetGpuTimestampNs is class C - a LIVE GPU
|
||||
// timestamp is not a static property, so R-15 does not reach it - and the
|
||||
// documented answer when it is unavailable is 0 (BackendObject.h:192), which
|
||||
// is correct rather than merely quiet. kCapTimerQuery is the published bit.
|
||||
//
|
||||
// The POINTER-valued macro, so the init-statement below is unchanged in a pull
|
||||
// build and G1 cannot see this edit: the Bool-valued spelling moved this
|
||||
// function by -150 bytes for no behavioural reason at all.
|
||||
if (const auto getGpuTimestampNs =
|
||||
MGL_BACKEND_SLOT_PTR_CAP(GetGpuTimestampNs, MG_Pipe::kCapTimerQuery)) {
|
||||
MGP_FILL(GetGpuTimestampNs);
|
||||
timestamp = getGpuTimestampNs();
|
||||
}
|
||||
@@ -2265,7 +2279,16 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_TIMESTAMP: {
|
||||
Int64 timestamp = 0;
|
||||
if (!MG_Config::Features.DisableTimerQuery) {
|
||||
if (const auto getGpuTimestampNs = MG_Backend::gBackendFunctionsTable.GL.GetGpuTimestampNs) {
|
||||
// glGetInteger64v(GL_TIMESTAMP). GetGpuTimestampNs is class C - a LIVE GPU
|
||||
// timestamp is not a static property, so R-15 does not reach it - and the
|
||||
// documented answer when it is unavailable is 0 (BackendObject.h:192), which
|
||||
// is correct rather than merely quiet. kCapTimerQuery is the published bit.
|
||||
//
|
||||
// The POINTER-valued macro, so the init-statement below is unchanged in a pull
|
||||
// build and G1 cannot see this edit: the Bool-valued spelling moved this
|
||||
// function by -150 bytes for no behavioural reason at all.
|
||||
if (const auto getGpuTimestampNs =
|
||||
MGL_BACKEND_SLOT_PTR_CAP(GetGpuTimestampNs, MG_Pipe::kCapTimerQuery)) {
|
||||
MGP_FILL(GetGpuTimestampNs);
|
||||
timestamp = getGpuTimestampNs();
|
||||
}
|
||||
|
||||
@@ -13,6 +13,10 @@
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_State/GLState/ErrorState/ErrorInfo.h>
|
||||
#include <MG_Impl/Pipe/PipeFill.h>
|
||||
// CONTRACT-P5.md §7 / ID-14: a null check on a GLFunctionsTable slot may not survive into the
|
||||
// client under split - it becomes a caps-mirror read, whatever class the slot itself is in.
|
||||
// The two macros carry that rule; in a pull build each expands to exactly the check it replaced.
|
||||
#include <MG_Remote/Client/SlotCaps.h>
|
||||
|
||||
namespace MobileGL::MG_Impl::GLImpl {
|
||||
namespace {
|
||||
@@ -478,7 +482,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
const Bool isOcclusionQuery =
|
||||
(target == GL_SAMPLES_PASSED || target == GL_ANY_SAMPLES_PASSED ||
|
||||
target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) &&
|
||||
MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery != nullptr;
|
||||
MGL_BACKEND_SLOT_CAP(BeginOcclusionQuery, MG_Pipe::kCapOcclusionQuery);
|
||||
const Bool isPipelineStatisticsQuery = IsPipelineStatisticsQueryTarget(target);
|
||||
if (target != GL_TIME_ELAPSED && !isTransformFeedbackQuery && !isOcclusionQuery &&
|
||||
!isPipelineStatisticsQuery) {
|
||||
@@ -528,10 +532,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
} else if (isTransformFeedbackQuery) {
|
||||
// Prefer real GPU transform-feedback queries (exact with geometry shaders);
|
||||
// the CPU accounting delta stays as the fallback when the backend lacks them.
|
||||
const Bool xfbQuerySupported =
|
||||
MGL_BACKEND_SLOT_CAP(BeginXfbPrimitivesQuery, MG_Pipe::kCapXfbPrimitivesQuery);
|
||||
const auto beginXfbPrimitivesQuery = MG_Backend::gBackendFunctionsTable.GL.BeginXfbPrimitivesQuery;
|
||||
MGP_FILL(BeginXfbPrimitivesQuery);
|
||||
queryObject->backendHandle =
|
||||
beginXfbPrimitivesQuery ? beginXfbPrimitivesQuery(target == GL_PRIMITIVES_GENERATED) : nullptr;
|
||||
xfbQuerySupported ? beginXfbPrimitivesQuery(target == GL_PRIMITIVES_GENERATED) : nullptr;
|
||||
queryObject->counterSnapshot = TransformFeedbackCounterForTarget(target);
|
||||
queryObject->accountedCaptureDrawSnapshot =
|
||||
MG_State::pGLContext->GetTransformFeedbackAccountedCaptureDraws();
|
||||
@@ -541,10 +547,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
MGP_FILL(BeginOcclusionQuery);
|
||||
queryObject->backendHandle = MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery();
|
||||
} else {
|
||||
const Bool timerQuerySupported =
|
||||
MGL_BACKEND_SLOT_CAP(BeginTimeElapsedQuery, MG_Pipe::kCapTimerQuery);
|
||||
const auto beginTimeElapsedQuery = MG_Backend::gBackendFunctionsTable.GL.BeginTimeElapsedQuery;
|
||||
MGP_FILL(BeginTimeElapsedQuery);
|
||||
queryObject->backendHandle =
|
||||
(!TimerQueryDisabled() && beginTimeElapsedQuery) ? beginTimeElapsedQuery() : nullptr;
|
||||
(!TimerQueryDisabled() && timerQuerySupported) ? beginTimeElapsedQuery() : nullptr;
|
||||
}
|
||||
activeQueryId = id;
|
||||
}
|
||||
@@ -555,7 +563,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
const Bool isOcclusionQuery =
|
||||
(target == GL_SAMPLES_PASSED || target == GL_ANY_SAMPLES_PASSED ||
|
||||
target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) &&
|
||||
MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery != nullptr;
|
||||
MGL_BACKEND_SLOT_CAP(BeginOcclusionQuery, MG_Pipe::kCapOcclusionQuery);
|
||||
const Bool isPipelineStatisticsQuery = IsPipelineStatisticsQueryTarget(target);
|
||||
if (target != GL_TIME_ELAPSED && !isTransformFeedbackQuery && !isOcclusionQuery &&
|
||||
!isPipelineStatisticsQuery) {
|
||||
@@ -657,7 +665,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
ResetQueryObjectLocked(queryObject); // discard any previous result
|
||||
queryObject->target = target;
|
||||
const auto queryCounterTimestamp = MG_Backend::gBackendFunctionsTable.GL.QueryCounterTimestamp;
|
||||
const Bool timerQuerySupported =
|
||||
MGL_BACKEND_SLOT_CAP(QueryCounterTimestamp, MG_Pipe::kCapTimerQuery);
|
||||
const auto queryCounterTimestamp =
|
||||
timerQuerySupported ? MG_Backend::gBackendFunctionsTable.GL.QueryCounterTimestamp : nullptr;
|
||||
MGP_FILL(QueryCounterTimestamp);
|
||||
queryObject->backendHandle =
|
||||
(!TimerQueryDisabled() && queryCounterTimestamp) ? queryCounterTimestamp() : nullptr;
|
||||
@@ -782,7 +793,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
if (target == GL_SAMPLES_PASSED || target == GL_ANY_SAMPLES_PASSED ||
|
||||
target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) {
|
||||
const Bool occlusionSupported = MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery != nullptr;
|
||||
const Bool occlusionSupported = MGL_BACKEND_SLOT_CAP(BeginOcclusionQuery, MG_Pipe::kCapOcclusionQuery);
|
||||
*params = occlusionSupported ? (target == GL_SAMPLES_PASSED ? 32 : 1) : 0;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,11 @@
|
||||
#include <MG_Backend/BackendObjects.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_Impl/Pipe/PipeFill.h>
|
||||
// CONTRACT-P5.md §7 / ID-14: a null check on a GLFunctionsTable slot may not survive into the
|
||||
// client under split - it becomes a caps-mirror read. SlotCaps.h carries the rule and the test
|
||||
// that decides which of its two spellings a site takes; in a pull build both expand to exactly
|
||||
// the check they replaced.
|
||||
#include <MG_Remote/Client/SlotCaps.h>
|
||||
|
||||
namespace MobileGL::MG_Impl::GLImpl {
|
||||
namespace {
|
||||
@@ -56,7 +61,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
auto* syncObject = new SyncObject;
|
||||
syncObject->condition = condition;
|
||||
syncObject->flags = flags;
|
||||
if (const auto backendFenceSync = MG_Backend::gBackendFunctionsTable.GL.FenceSync) {
|
||||
// The family's ONE gate. FenceSync is class C under split, and "absent" is the
|
||||
// answer the whole fallback chain below is written against: every later site already
|
||||
// checks syncObject->backendHandle, which stays null from here. The POINTER-valued
|
||||
// macro keeps the init-statement byte-identical in a pull build (G1).
|
||||
if (const auto backendFenceSync = MGL_BACKEND_SLOT_PTR_LOCAL(FenceSync)) {
|
||||
MGP_FILL(FenceSync);
|
||||
syncObject->backendHandle = backendFenceSync();
|
||||
}
|
||||
|
||||
@@ -31,6 +31,11 @@
|
||||
#include <MG_Util/Math/FixedPointConversion.h>
|
||||
#include <MG_State/GLState/TextureState/TextureObjectBuffer.h>
|
||||
#include <MG_Impl/Pipe/PipeFill.h>
|
||||
// CONTRACT-P5.md §7 / ID-14: a null check on a GLFunctionsTable slot may not survive into the
|
||||
// client under split - it becomes a caps-mirror read. SlotCaps.h carries the rule and the test
|
||||
// that decides which of its two spellings a site takes; in a pull build both expand to exactly
|
||||
// the check they replaced.
|
||||
#include <MG_Remote/Client/SlotCaps.h>
|
||||
// P4a, ID-18 M2. The ONE door MG_State and MG_Impl have into the client's emitters; the three
|
||||
// call sites below are declarations only, exactly as the frontend's mutators are.
|
||||
#include <MG_Pipe/PipeMutation.h>
|
||||
@@ -6534,7 +6539,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
GLenum type, GLsizei bufSize, void* pixels, const char* caller) {
|
||||
if (MG_Backend::pActiveBackendObject != nullptr &&
|
||||
MG_Backend::pActiveBackendObject->GetBackendType() == BackendType::DirectVulkan &&
|
||||
MG_Backend::gBackendFunctionsTable.GL.GetTextureImage != nullptr) {
|
||||
MGL_BACKEND_SLOT_LOCAL(GetTextureImage)) {
|
||||
MGP_FILL(GetTextureImage);
|
||||
MG_Backend::gBackendFunctionsTable.GL.GetTextureImage(textureObject, uploadTarget, level, format, type,
|
||||
bufSize, pixels);
|
||||
@@ -6796,7 +6801,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
void GetTexImage(GLenum target, GLint level, GLenum format, GLenum type, GLvoid* pixels) {
|
||||
if (!GetTexImage_State(target, level, format, type, pixels)) return;
|
||||
if (MG_Backend::gBackendFunctionsTable.GL.GetTexImage != nullptr) {
|
||||
if (MGL_BACKEND_SLOT_LOCAL(GetTexImage)) {
|
||||
GetTexImage_Backend(target, level, format, type, pixels);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -39,6 +39,15 @@
|
||||
#include <MG_Pipe/PipeMutation.h>
|
||||
#include <Config.h>
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// R-8 (c1): the client's liveness gates read the caps mirror, never MGPipeGetResourceOps().
|
||||
// Behind the build option for G1's reason - nothing under MG_Remote may be reachable from a
|
||||
// pull build - and every use below is additionally gated on the resolved TRANSPORT, because
|
||||
// build-split runs MOBILEGL_TRANSPORT=monolith in every unit and integration-gpu lane and those
|
||||
// lanes must keep answering exactly what they answered before.
|
||||
#include <MG_Remote/Client/CapsMirror.h>
|
||||
#endif
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
@@ -609,12 +618,38 @@ namespace MobileGL::MG_Pipe {
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// R-8 (c1). THE SECOND CONJUNCT MOVES UNDER SPLIT, AND ONLY UNDER SPLIT.
|
||||
//
|
||||
// `MGPipeGetResourceOps() != nullptr` asks "has a backend registered the consumer". That
|
||||
// table is the SERVER's registration and it is a PROCESS-WIDE global (PipeApply.cpp:402):
|
||||
// under inproc a client reading it answers correctly BY ACCIDENT, and under spawn the
|
||||
// client process has no backend at all, so the read answers null and five record families
|
||||
// stop emitting - silently, while the emitters go on clearing their per-level dirty flags
|
||||
// on the acceptance they never asked for. That is ID-39's 66 lost DirectVulkan uploads with
|
||||
// a wire in between. The client asks the caps mirror instead, which carries the answer the
|
||||
// SERVER gave at the handshake (CallMask bits 32..47).
|
||||
//
|
||||
// s1 made the server end Fatal when nobody sets the mask; this is the client end.
|
||||
Bool MGPipeResourceSubsystemEnabled() {
|
||||
return (MG_Config::Features.PipePush & kMGPipeSubsystemResources) != 0 &&
|
||||
MGPipeGetResourceOps() != nullptr;
|
||||
if ((MG_Config::Features.PipePush & kMGPipeSubsystemResources) == 0) return false;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
|
||||
return MG_Remote::Client::CapsMirrorInstance().ServerConsumes(kMGPipeSubsystemResources);
|
||||
}
|
||||
#endif
|
||||
return MGPipeGetResourceOps() != nullptr;
|
||||
}
|
||||
|
||||
Bool MGPipeResourceOpsHaveSubDataResident() {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
|
||||
// CONTRACT-P5.md §7's THIRD NAMED CAPABILITY PROBE. `ops->SubDataResident != nullptr`
|
||||
// is not a safety check - it decides whether the resident-upload path EXISTS - and
|
||||
// under split there is no op table here to probe. kCapResidentSubData is the bit the
|
||||
// server publishes for exactly this question.
|
||||
return MG_Remote::Client::CapsMirrorInstance().HasCap(kCapResidentSubData);
|
||||
}
|
||||
#endif
|
||||
const MGPipeResourceOps* ops = MGPipeGetResourceOps();
|
||||
return ops != nullptr && ops->SubDataResident != nullptr;
|
||||
}
|
||||
@@ -936,8 +971,21 @@ namespace MobileGL::MG_Pipe {
|
||||
// MGPipeEmitResourceRespecify above uses for buffers) publishes it. Nothing here needs
|
||||
// to remember the window.
|
||||
Bool P4aFamilyHasItsConsumer(Uint64 subsystem) {
|
||||
return (subsystem & kMGPipeP4aFamilySubsystems) == 0 ||
|
||||
MGPipeGetResourceOps() != nullptr;
|
||||
if ((subsystem & kMGPipeP4aFamilySubsystems) == 0) return true;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// R-8 (c1), the same move as MGPipeResourceSubsystemEnabled's and for the same
|
||||
// reason. ALL FOUR FAMILIES RIDE THE ONE SIGNAL, exactly as they do in monolith:
|
||||
// the paragraph above explains why the resource consumer IS the texture family's
|
||||
// consumer, and the split spelling of "a backend registered the resource op table"
|
||||
// is "the server published the resource subsystem's consumer bit". Asking per
|
||||
// family here would be a NEW rule, and a client that withheld more than the server
|
||||
// refuses leaves the server's handle arm live with no records to read.
|
||||
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
|
||||
return MG_Remote::Client::CapsMirrorInstance().ServerConsumes(
|
||||
kMGPipeSubsystemResources);
|
||||
}
|
||||
#endif
|
||||
return MGPipeGetResourceOps() != nullptr;
|
||||
}
|
||||
|
||||
// ================================================================================
|
||||
|
||||
@@ -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
|
||||
// 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
|
||||
// caps accessor that answers a default is how a split lane runs on the wrong device's limits.
|
||||
// P5 package c1.
|
||||
//
|
||||
// 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 <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 {
|
||||
|
||||
#define MGP5_C0_STUB(what) \
|
||||
do { \
|
||||
MGLOG_F("MGPipe: Fatal{UnimplementedCapsMirror, \"%s\"} - P5 package c1 has not landed " \
|
||||
"this yet; c0 shipped the signature only", \
|
||||
what); \
|
||||
std::abort(); \
|
||||
} while (0)
|
||||
namespace {
|
||||
CapsAdoptedHook g_capsAdoptedHook = nullptr;
|
||||
|
||||
void CapsMirror::Adopt(const MG_Pipe::MGPCaps&, const MG_Backend::FormatCapabilityCache&,
|
||||
const RendererInfo&, const String&, BackendType) {
|
||||
MGP5_C0_STUB("CapsMirror::Adopt");
|
||||
// R-8's negative control, counted at the funnel. Not atomics: every reader of this
|
||||
// mirror is the GL thread, by the same argument that makes one gPipeInputs legal.
|
||||
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
|
||||
// snapshot. Everything else aborts, so nothing can accidentally answer from a zeroed mirror.
|
||||
void SetCapsAdoptedHook(CapsAdoptedHook hook) { g_capsAdoptedHook = hook; }
|
||||
|
||||
Bool CapsMirror::Valid() const { return m_generation != 0; }
|
||||
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 {
|
||||
MGP5_C0_STUB("CapsMirror::Dynamic");
|
||||
WarnPlaceholder("Dynamic", m_generation);
|
||||
return m_caps.Dynamic;
|
||||
}
|
||||
|
||||
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"); }
|
||||
Uint64 CapsMirror::CallMask() const { MGP5_C0_STUB("CapsMirror::CallMask"); }
|
||||
Bool CapsMirror::HasCap(MG_Pipe::MGPCapBit) const { MGP5_C0_STUB("CapsMirror::HasCap"); }
|
||||
Bool CapsMirror::ServerConsumes(Uint64) const { MGP5_C0_STUB("CapsMirror::ServerConsumes"); }
|
||||
|
||||
const String& CapsMirror::ApiVersion() const {
|
||||
WarnPlaceholder("ApiVersion", m_generation);
|
||||
return m_apiVersion;
|
||||
}
|
||||
|
||||
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 {
|
||||
MGP5_C0_STUB("CapsMirror::PrefersCpuXfbPrimitiveAccounting");
|
||||
return HasCap(MG_Pipe::kCapCpuXfbPrimitiveAccounting);
|
||||
}
|
||||
|
||||
CapsMirror& CapsMirrorInstance() {
|
||||
@@ -58,6 +170,4 @@ namespace MobileGL::MG_Remote::Client {
|
||||
return instance;
|
||||
}
|
||||
|
||||
#undef MGP5_C0_STUB
|
||||
|
||||
} // namespace MobileGL::MG_Remote::Client
|
||||
|
||||
@@ -97,4 +97,28 @@ namespace MobileGL::MG_Remote::Client {
|
||||
// reach pipe or backend state from an exit handler.
|
||||
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
|
||||
|
||||
@@ -18,9 +18,12 @@
|
||||
#include "../Transport/InProcessTransport.h"
|
||||
|
||||
#include <MGGitHash.h>
|
||||
#include <MG_Pipe/MGPipeCallbacks.h>
|
||||
#include <MG_Util/Debug/Log.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
namespace MobileGL::MG_Remote::Client {
|
||||
@@ -100,6 +103,29 @@ namespace MobileGL::MG_Remote::Client {
|
||||
#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) {
|
||||
switch (mode) {
|
||||
case MG_Config::TransportMode::Monolith: return "monolith";
|
||||
@@ -111,6 +137,170 @@ namespace MobileGL::MG_Remote::Client {
|
||||
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
|
||||
|
||||
// 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);
|
||||
|
||||
// ---- 7. the first CapsSnapshot, if the server had a backend to publish one from.
|
||||
if (m_transport->PeekFrameSize() != 0) {
|
||||
std::vector<Uint8> frame;
|
||||
if (ReceiveEnvelope(*m_transport, frame, 0) == MOBILEGL_OK) {
|
||||
const ::MobileGL::Wire::CtrlEnvelope* envelope = ParseEnvelope(frame);
|
||||
if (envelope != nullptr &&
|
||||
envelope->msg_type() == ::MobileGL::Wire::CtrlMsg::CapsSnapshot) {
|
||||
// The mirror's Adopt and the two blob DECODERS are c1's and w1's. s1 stops
|
||||
// at "the snapshot arrived and is verifiable": adopting it here would put
|
||||
// the caps mirror's invalidation rule (R-12: a second arrival IS the
|
||||
// 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()));
|
||||
}
|
||||
}
|
||||
// ONE DRAIN, ONE ADOPTER (c1): PumpControlPlane below is the only thing in the client
|
||||
// that turns a CapsSnapshot into a CapsMirror generation, so R-12's "a second arrival
|
||||
// IS the invalidation" lives in exactly one place. s1's step here used to stop at
|
||||
// "the snapshot arrived and is verifiable"; it now goes all the way, through the same
|
||||
// function every later arrival goes through.
|
||||
if (PumpControlPlane() == 0) {
|
||||
MGLOG_W("MG_Remote client: the handshake carried no CapsSnapshot - the caps mirror is "
|
||||
"a PLACEHOLDER until one arrives. Every caps read until then answers a "
|
||||
"default and says so");
|
||||
}
|
||||
|
||||
m_started = true;
|
||||
g_active = this;
|
||||
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
|
||||
// locally. s1 supplies the four primitives it composes from: Encoder(), Producer(),
|
||||
// WaitForApplied() and ReadReply().
|
||||
Uint64 ClientSession::EmitAndWait(MG_Pipe::MGPWireOp, const void*, Uint64, const void*, Uint64,
|
||||
void*, Uint64, Int32*) {
|
||||
MGP5_C0_STUB("ClientSession::EmitAndWait");
|
||||
//
|
||||
// THE ORDER IS ENCODE -> PUBLISH+NOTIFY -> WAIT -> READ REPLY, and it is not negotiable.
|
||||
// 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; }
|
||||
|
||||
// False, not a Fatal, for both: these are the R-1 mutual-exclusion assertion's two probes,
|
||||
// and an assertion helper that aborts when asked is worse than useless. Package c1 gives
|
||||
// them real answers when it lands the barrier.
|
||||
Bool ClientSession::InBarrierWait() { return false; }
|
||||
Bool ClientSession::ApplyThreadIsInsideApplier() { return false; }
|
||||
// R-1's mutual-exclusion invariant, as two probes that answer honestly.
|
||||
//
|
||||
// THE CLIENT'S FLAG IS THREAD-LOCAL AND THE SERVER'S IS NOT, and the asymmetry is the
|
||||
// point: "am I inside a barrier wait" is a question about the calling thread, while "is the
|
||||
// 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; }
|
||||
|
||||
|
||||
@@ -100,6 +100,36 @@ namespace MobileGL::MG_Remote::Client {
|
||||
static Bool InBarrierWait();
|
||||
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 owns construction and lifetime; c1 owns the barrier POLICY. So the plumbing is
|
||||
|
||||
@@ -6,13 +6,36 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// 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 "ClientSession.h"
|
||||
#include "GpuWritePending.h"
|
||||
#include "PersistentMapTracker.h"
|
||||
|
||||
#include <MG_Util/Converters/GLToMG/TextureEnumConverter.h>
|
||||
#include <MG_Util/Debug/Log.h>
|
||||
#include <MG_Util/Metrics/TextureMetrics.h>
|
||||
|
||||
#include <MG_State/GLState/Core.h>
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
namespace MobileGL::MG_Remote::Client {
|
||||
|
||||
@@ -36,12 +59,478 @@ namespace MobileGL::MG_Remote::Client {
|
||||
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() {
|
||||
MGLOG_F("MGPipe: Fatal{UnimplementedEmitTable, \"RemoteEmitTable\"} - P5 package c1 has "
|
||||
"not landed this yet; c0 shipped the signature only");
|
||||
std::abort();
|
||||
// Leaked at exit like every other MG_Remote singleton (ID-8): MG_Backend::Init()
|
||||
// copies it into gBackendFunctionsTable and MobileGL::Destroy() clears that copy from
|
||||
// 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
|
||||
|
||||
@@ -60,6 +60,22 @@
|
||||
|
||||
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.
|
||||
// A reference to a never-destroyed block, like every other MG_Remote singleton (ID-8).
|
||||
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.
|
||||
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
|
||||
|
||||
@@ -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/ShaderSourceProcessor.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/Types.h>
|
||||
#include <Config.h>
|
||||
@@ -4695,6 +4702,21 @@ TEST(DirectGLESBufferDrawProbe, UnderSplitTheRecordAloneAnswersTheLiveHostMapQue
|
||||
MG_Pipe::MGPipeSetResourceOps(&ops);
|
||||
MG_Config::Transport = MG_Config::TransportMode::InProcess;
|
||||
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
|
||||
|
||||
@@ -65,3 +65,30 @@ if (MSVC)
|
||||
endif ()
|
||||
|
||||
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