mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-17 00:28:31 +09:00
[Merge] (MGPipe, P5): joint - c1@5682f429d0c877ae5d50ad723b76253b182c98f6
This commit is contained in:
@@ -533,6 +533,13 @@ if (MOBILEGL_PIPE_PUSH)
|
||||
# by pointer beside the record), so the codec is live code only in the VERIFY lane,
|
||||
# where the applier serialises, deserialises and field-compares before storing.
|
||||
MobileGL/MG_State/GLState/ProgramState/ProgramArtifactsCodec.cpp
|
||||
# P5's contract (integrator ruling R-17, package c1): the MONOLITH arm of the
|
||||
# client->wire routing - the thirty-seven adapters that install gMGPipeScreen /
|
||||
# gMGPipeContext over the MGPipeApply* entry points, and the reply mailbox the four
|
||||
# acceptance rows answer through. Push-only for the same G1 reason as the five above:
|
||||
# the two tables are inline variables that are zero in a pull build and nothing there
|
||||
# can reach a thunk.
|
||||
MobileGL/MG_Pipe/PipeRoute.cpp
|
||||
)
|
||||
endif()
|
||||
|
||||
@@ -571,6 +578,15 @@ 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 c1, ruling R-17: the ENCODE TWIN of gMGPipeWireRecordApply - the thirty-seven
|
||||
# emitters installed over the two generated tables, which is what stops every
|
||||
# resource/CSO/texture/program record executing synchronously on the GL thread under
|
||||
# inproc. The monolith arm of the same routing is MG_Pipe/PipeRoute.cpp, in the
|
||||
# PIPE_PUSH list above, because it must exist in a push build that has no MG_Remote.
|
||||
MobileGL/MG_Remote/Client/WireTables.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.
|
||||
|
||||
@@ -43,6 +43,15 @@ namespace MobileGL {
|
||||
if (logLifecycle) {
|
||||
MGLOG_I("MobileGL closing...");
|
||||
}
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// P5: the split roles come down FIRST - ARCHITECTURE.md:537's order puts the whole
|
||||
// of it before MobileGL::Destroy(), and this function IS MobileGL::Destroy. A no-op
|
||||
// in a monolith RUN; absent from a monolith BUILD, because G1 admits no new pull
|
||||
// symbol and no resized one (the first version called it unconditionally and moved
|
||||
// DestroyImpl by 32 bytes). See BackendObjects.h for why the position rather than
|
||||
// the call is the load-bearing part.
|
||||
MG_Backend::ShutdownSplitRoles();
|
||||
#endif
|
||||
// Before any subsystem the counters name goes away, and before the last frame's
|
||||
// numbers can be lost: emits the final summary line and, when
|
||||
// MOBILEGL_PIPE_STATS_FILE is set, the JSON dump. A no-op when the counters are
|
||||
|
||||
@@ -15,4 +15,29 @@
|
||||
namespace MobileGL::MG_Backend {
|
||||
extern UniquePtr<BackendObject>& pActiveBackendObject;
|
||||
extern GlobalBackendFunctionsTable gBackendFunctionsTable;
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// P5 v1. The counterpart of Init()'s single split hook, and a NO-OP in every run that is
|
||||
// not split. It exists ONLY in a split build: G1 admits no new symbol in the pull build,
|
||||
// so MobileGL/Init.cpp's call sits under the same #if rather than calling a no-op.
|
||||
//
|
||||
// IT MUST RUN FIRST, BEFORE ANYTHING ELSE IN DestroyImpl. ARCHITECTURE.md:537's order is
|
||||
// publish -> the server drains and acks -> stop the apply thread (Kill, then join) -> close
|
||||
// the transport -> the client drains the compile pool -> MobileGL::Destroy() -> release the
|
||||
// sync/query handles; DestroyImpl IS MobileGL::Destroy, so everything before that arrow has
|
||||
// to happen at its top. Two consequences are load-bearing rather than tidy:
|
||||
//
|
||||
// * the apply thread is JOINED before PipeStats::Shutdown() dumps, so nothing is writing
|
||||
// a counter while the final line is produced;
|
||||
// * the apply thread is joined before pActiveBackendObject.reset(), so the server's own
|
||||
// BackendObject - which is NOT that global (table 3) - is destroyed on the thread that
|
||||
// owns the context, by ServerLoop::Stop, rather than on the app thread.
|
||||
//
|
||||
// The sync/query registries stay where they are, drained at MobileGL/Init.cpp:62 and :67
|
||||
// BEFORE pActiveBackendObject.reset(). ARCHITECTURE.md:537 puts them after
|
||||
// MobileGL::Destroy(); the two are only reconcilable if a split sync handle is
|
||||
// client-minted and needs no backend call, which is P10's. CONTRACT-P5 4 flags this as a
|
||||
// KNOWN OPEN ITEM and asks v1 to record which way it went: P5 keeps today's order.
|
||||
void ShutdownSplitRoles();
|
||||
#endif
|
||||
} // namespace MobileGL::MG_Backend
|
||||
|
||||
@@ -16,6 +16,12 @@
|
||||
#include <MG_Pipe/MGPipeHostSpan.h>
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
#endif
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// R-11's server-owned staging copy. Header-only and package v1's; see its own header block for
|
||||
// why GLESBufferResource does not simply gain a member.
|
||||
#include <MG_Remote/Server/StagedShadow.h>
|
||||
#endif
|
||||
|
||||
#include "Utils.h"
|
||||
#include "DirectGLES.h"
|
||||
#include "BackendObject_DirectGLES.h"
|
||||
@@ -1008,6 +1014,77 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return StorageMatchesSize(resource, bufferObject.GetSize());
|
||||
}
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// -------------------------------------------------------------------------------
|
||||
// R-11 - THE SERVER'S OWN COPY OF THE STAGED BYTES. Package v1.
|
||||
//
|
||||
// The rule, the evidence and the reason the storage is a side table rather than a
|
||||
// member of GLESBufferResource are all in MG_Remote/Server/StagedShadow.h. This is
|
||||
// only the instance and the four call sites.
|
||||
//
|
||||
// ONE PER PROCESS, and its copying arm is decided ONCE at first use: the two arms
|
||||
// hold the authoritative bytes in DIFFERENT places, so an answer that changed
|
||||
// mid-run would strand every resource already staged - the same reason
|
||||
// ResolveResourceSubsystemArm latches (Managers.h). Leaked at exit like every other
|
||||
// MG_Remote singleton (ID-8): ~BufferObject reaches the destroy path from exit
|
||||
// handlers, after this TU's globals would already be gone.
|
||||
// -------------------------------------------------------------------------------
|
||||
MG_Remote::Server::StagedShadowStore& ServerStaged() {
|
||||
static MG_Remote::Server::StagedShadowStore& store =
|
||||
*new MG_Remote::Server::StagedShadowStore(
|
||||
MG_Config::Transport != MG_Config::TransportMode::Monolith);
|
||||
return store;
|
||||
}
|
||||
|
||||
// THE COVERAGE RULE FOR THE THREE-TIER FLUSH DRAIN, ASSERTED BEFORE THE CALL AND NOT
|
||||
// INSIDE IT. FlushPendingRangesFrom is a G5-pinned body (p3a_untouched_regions.sh's
|
||||
// PINNED_FUNCTIONS, ID-41): the split arm CALLS it, it does not re-spell it, and it
|
||||
// does not add a line to it either. Its tier-1 arm - the range-invalidating map -
|
||||
// copies with its own Memcpy and never reaches UploadRangeFrom, so the one tier that
|
||||
// DECLARES THE OLD BYTES DEAD is the one tier no later check can see; the only place
|
||||
// left to say "every queued range is inside the staged coverage" is the instant before
|
||||
// the drain takes the queue. The clamp is the drain's own (limit = the smaller of the
|
||||
// frontend size and the backend store; bytes past either end have nowhere to land and
|
||||
// are not this rule's subject), so the two agree about which bytes are meant.
|
||||
//
|
||||
// Fires only for a base that IS this resource's server shadow: RequireCoverage
|
||||
// answers nothing for the legacy arm's MappedData(), which is valid for the whole
|
||||
// store. `pendingMutex` is taken here and released before the drain takes it again.
|
||||
void RequireStagedCoverageForPendingRanges(GLESBufferResource& resource, const Uint8* hostBase,
|
||||
SizeT frontendSize, const char* site) {
|
||||
if (hostBase == nullptr) return;
|
||||
const SizeT limit = std::min(frontendSize, resource.storageSize);
|
||||
const std::lock_guard<std::mutex> lock(resource.pendingMutex);
|
||||
for (const auto& range : resource.pendingRanges) {
|
||||
const SizeT end = std::min(range.end, limit);
|
||||
const SizeT start = std::min(range.start, end);
|
||||
if (start == end) continue;
|
||||
ServerStaged().RequireCoverage(&resource, hostBase, start, end, site);
|
||||
}
|
||||
}
|
||||
#endif // MOBILEGL_BUILD_DISAGGREGATED
|
||||
|
||||
// The four hostBytes sites read the same in both builds. The non-split expansion is the
|
||||
// ORIGINAL EXPRESSION, character for character - `raw - offset` - so a push or verify build
|
||||
// compiles exactly what it compiled before R-11 and the split arm is the only new behaviour.
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
#define MGL_SERVER_STAGED_ADOPT(res, width, bytes, offset, size) \
|
||||
ServerStaged().Adopt(&(res), (width), (bytes), (offset), (size))
|
||||
#define MGL_SERVER_STAGED_DROP(res) ServerStaged().Drop(&(res))
|
||||
#define MGL_SERVER_STAGED_DROP_ALL() ServerStaged().DropAll()
|
||||
#define MGL_SERVER_STAGED_REQUIRE(res, base, start, end, site) \
|
||||
ServerStaged().RequireCoverage(&(res), (base), (start), (end), (site))
|
||||
#define MGL_SERVER_STAGED_REQUIRE_PENDING(res, base, frontendSize, site) \
|
||||
RequireStagedCoverageForPendingRanges((res), (base), (frontendSize), (site))
|
||||
#else
|
||||
#define MGL_SERVER_STAGED_ADOPT(res, width, bytes, offset, size) \
|
||||
(static_cast<const Uint8*>(bytes) - (offset))
|
||||
#define MGL_SERVER_STAGED_DROP(res) ((void)0)
|
||||
#define MGL_SERVER_STAGED_DROP_ALL() ((void)0)
|
||||
#define MGL_SERVER_STAGED_REQUIRE(res, base, start, end, site) ((void)0)
|
||||
#define MGL_SERVER_STAGED_REQUIRE_PENDING(res, base, frontendSize, site) ((void)0)
|
||||
#endif
|
||||
|
||||
// The host bytes a range upload/flush reads. On the legacy arm the frontend object's
|
||||
// shadow; on the handle arm the base the last content-carrying call handed over.
|
||||
void UploadRangeFrom(GLESBufferResource& resource, const Uint8* hostBase, SizeT start, SizeT end) {
|
||||
@@ -1015,6 +1092,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (start >= end || hostBase == nullptr) return;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
MGL_SERVER_STAGED_REQUIRE(resource, hostBase, start, end, "upload_range");
|
||||
#endif
|
||||
BindBufferId(TempBufferTarget, resource.id);
|
||||
g_GLESFuncs.glBufferSubData(TempBufferTarget, (GLintptr)start, (GLsizeiptr)(end - start),
|
||||
hostBase + start);
|
||||
@@ -1946,9 +2026,21 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// every reader below treats a null base as "no bytes to move", which is the
|
||||
// honest answer, and the ensure path re-reads the live base from the
|
||||
// frontend object it still holds.
|
||||
resource->hostBytes = (desc.HasDefinedContent != 0 && initialBytes != nullptr)
|
||||
? static_cast<const Uint8*>(initialBytes)
|
||||
: nullptr;
|
||||
if (desc.HasDefinedContent != 0 && initialBytes != nullptr) {
|
||||
// R-11: in monolith this is the client's shadow base, unchanged; under
|
||||
// split it is copied into server-owned storage first. A respecify's
|
||||
// companion pointer is the base at offset 0 and covers the whole store.
|
||||
// Under split it is ALWAYS null (contract table 1 row 19 -
|
||||
// initialBytes does not cross, and the content arrives as
|
||||
// resource_subdata records right behind this record), so in practice
|
||||
// this arm is the monolith one and the else arm is the split one.
|
||||
resource->hostBytes = MGL_SERVER_STAGED_ADOPT(*resource, static_cast<SizeT>(desc.Width),
|
||||
initialBytes, 0,
|
||||
static_cast<SizeT>(desc.Width));
|
||||
} else {
|
||||
MGL_SERVER_STAGED_DROP(*resource);
|
||||
resource->hostBytes = nullptr;
|
||||
}
|
||||
}
|
||||
if (!resource) return; // lazy: the ensure path full-uploads on creation
|
||||
if (resource->immutableStorage) {
|
||||
@@ -2018,7 +2110,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// and the ensure path's republication (the 3c55e027 fix) is what runs at a draw.
|
||||
if (bytes != nullptr) {
|
||||
const std::lock_guard<std::mutex> lock(resource->pendingMutex);
|
||||
resource->hostBytes = static_cast<const Uint8*>(bytes) - offset;
|
||||
// R-11: MONOLITH keeps the client's base; SPLIT copies [offset, offset+size)
|
||||
// into server-owned storage and points hostBytes at that. Inside the SAME
|
||||
// lock as the queued range, because the base and the range are one fact
|
||||
// ("these bytes, at this base") and the drain takes this mutex to lift them.
|
||||
resource->hostBytes =
|
||||
MGL_SERVER_STAGED_ADOPT(*resource, ResourceWidthOf(res), bytes, offset, size);
|
||||
}
|
||||
if (resource->pendingRespecify) return; // full re-upload pending anyway
|
||||
if (!CanTouchGLNow() || resource->id == 0 ||
|
||||
@@ -2067,10 +2164,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (!resource) return;
|
||||
const SizeT start = static_cast<SizeT>(record.Offset);
|
||||
const SizeT end = start + static_cast<SizeT>(record.Size);
|
||||
// M-2, the second store site - same lock, same reason as Ops_H_SubData's.
|
||||
// M-2, the second store site - same lock, same reason as Ops_H_SubData's, and
|
||||
// the same R-11 copy. UNDER SPLIT `bytes` IS ALWAYS NULL HERE by ruling (C-6 /
|
||||
// contract table 1 row 20: resource_flush_range carries no bytes at all - the
|
||||
// ladder it drives rewrites its range from the authoritative shadow, which rule
|
||||
// C makes server-owned, so resource_subdata is already the only way bytes reach
|
||||
// it and a second carrier would be a forgeable way to say the same thing). So
|
||||
// this arm keeps whatever the preceding subdata records staged, which is
|
||||
// exactly what the ladder must read.
|
||||
if (bytes != nullptr) {
|
||||
const std::lock_guard<std::mutex> lock(resource->pendingMutex);
|
||||
resource->hostBytes = static_cast<const Uint8*>(bytes) - start;
|
||||
resource->hostBytes = MGL_SERVER_STAGED_ADOPT(*resource, ResourceWidthOf(res), bytes,
|
||||
start, end - start);
|
||||
}
|
||||
if (resource->pendingRespecify) return;
|
||||
if (!CanTouchGLNow() || resource->id == 0 ||
|
||||
@@ -2100,6 +2205,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
resource->hostBytes != nullptr) {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// The kill-switch arm's own Memcpy, and the one that passes
|
||||
// GL_MAP_INVALIDATE_RANGE_BIT - i.e. the one that tells the driver the old
|
||||
// bytes are dead. Bytes outside the staged coverage are exactly the ones
|
||||
// that claim is false for.
|
||||
MGL_SERVER_STAGED_REQUIRE(*resource, resource->hostBytes, start, end,
|
||||
"flush_range_invalidate_map");
|
||||
#endif
|
||||
BindBufferId(TempBufferTarget, resource->id);
|
||||
void* mappedData = g_GLESFuncs.glMapBufferRange(
|
||||
@@ -2145,7 +2258,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (size == 0) return;
|
||||
|
||||
// Queued app writes must land in the backend store before it is read back, or
|
||||
// the writeback below would revert them in the shadow.
|
||||
// the writeback below would revert them in the shadow. Under split every queued
|
||||
// range must lie inside the server shadow's staged coverage first (R-11 / the
|
||||
// M-6 ruling): the drain's tier-1 map would otherwise declare live GPU bytes
|
||||
// dead over a range nothing staged.
|
||||
MGL_SERVER_STAGED_REQUIRE_PENDING(*resource, resource->hostBytes, ResourceWidthOf(res),
|
||||
"readback_flush_pending");
|
||||
FlushPendingRangesFrom(*resource, resource->hostBytes, ResourceWidthOf(res));
|
||||
|
||||
BindBufferId(TempBufferTarget, resource->id);
|
||||
@@ -2187,6 +2305,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
|
||||
void Ops_H_Destroy(MG_Pipe::MGPipeHandle res) {
|
||||
// R-11's server copy dies with the resource, and BEFORE the twin leaves the
|
||||
// table - the side map is keyed by the twin's address, so this is the last
|
||||
// moment that address can be looked up.
|
||||
if (GLESBufferResource* dying = FindBufferResourceForHandle(res); dying != nullptr) {
|
||||
MGL_SERVER_STAGED_DROP(*dying);
|
||||
}
|
||||
// The twin comes OUT of the table first, so the three outcomes below are
|
||||
// reached with the entry already retired. The SLOT is the client's to free,
|
||||
// after this returns (D-L).
|
||||
@@ -2272,6 +2396,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// PipeResource::AdoptPersistentMap then does m_shadow->clear() +
|
||||
// shrink_to_fit(), so the shadow base any earlier content-carrying call
|
||||
// recorded is a FREED allocation from here on. The live bytes are persistentPtr.
|
||||
// R-11's server copy goes with it: the bytes are the coherent map's now, and a
|
||||
// stale server shadow would answer a later drain with pre-map content.
|
||||
MGL_SERVER_STAGED_DROP(*resource);
|
||||
resource->hostBytes = nullptr;
|
||||
resource->storageSize = static_cast<SizeT>(size);
|
||||
resource->storageInitialized = true;
|
||||
@@ -2631,6 +2758,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// handles (no GL) and let the next draw / texture upload recreate them.
|
||||
ResetRingForNewContext(g_uboRing);
|
||||
ResetRingForNewContext(g_unpackRing);
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// R-11's server copies die with the context for the same reason the rings' ids do:
|
||||
// the resource twins they are keyed by are about to be rebuilt against a new
|
||||
// generation, and a shadow that outlived its twin would be looked up by a RECYCLED
|
||||
// address on the next allocation - which is the quietest possible wrong answer.
|
||||
MGL_SERVER_STAGED_DROP_ALL();
|
||||
#endif
|
||||
}
|
||||
|
||||
void ProcessDeferredBufferReleases() {
|
||||
@@ -2920,6 +3054,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (resource->pendingRespecify || !resource->storageInitialized || resource->storageSize != size) {
|
||||
RespecifyStorageWith(*resource, size, usage, initialData, serial);
|
||||
} else if (!resource->pendingRanges.empty()) {
|
||||
// Same rule as Ops_H_Readback's, at the draw-time drain: with no frontend object
|
||||
// `hostBase` is the server shadow and every queued range must be staged.
|
||||
MGL_SERVER_STAGED_REQUIRE_PENDING(*resource, hostBase, size, "ensure_flush_pending");
|
||||
FlushPendingRangesFrom(*resource, hostBase, size);
|
||||
resource->syncedChangeSerial = serial;
|
||||
} else if (resource->syncedChangeSerial != serial) {
|
||||
|
||||
@@ -11,6 +11,26 @@
|
||||
#include <MG_Util/BackendLoaders/OpenGL/Loader.h>
|
||||
#include <MG_Util/Converters/MGToStr/GLExtensionConverter.h>
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
#include <MG_Remote/Client/ClientSession.h>
|
||||
#include <MG_Remote/Server/ServerLoop.h>
|
||||
#include <MG_Remote/Server/ServerSession.h>
|
||||
#endif
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
namespace MobileGL::MG_Remote::Client {
|
||||
// PACKAGE c1's, DECLARED HERE RATHER THAN INCLUDED. The client's BackendObject_Remote is
|
||||
// c1's file and does not exist while v1 is written, so v1 ships a WEAK definition of this
|
||||
// beside ServerLoop that aborts by name. c1's strong definition displaces it at link time
|
||||
// with no edit to this file - and until then the hook cannot silently succeed, which is the
|
||||
// only property that matters: a split lane that installed a WORKING monolith backend object
|
||||
// here would render correctly for entirely the wrong reason (ARCHITECTURE.md 10.3).
|
||||
UniquePtr<MG_Backend::BackendObject> CreateRemoteBackendObject();
|
||||
} // namespace MobileGL::MG_Remote::Client
|
||||
#endif
|
||||
|
||||
namespace MobileGL::MG_Backend {
|
||||
void LogBackendInfo() {
|
||||
if (!pActiveBackendObject) {
|
||||
@@ -45,9 +65,150 @@ namespace MobileGL::MG_Backend {
|
||||
return true;
|
||||
}
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
namespace {
|
||||
// WHICH MGPipe SUBSYSTEMS THIS SERVER HAS A CONSUMER FOR - CallMask bits 32..47 (R-8 /
|
||||
// C-4). It is stated from what the server's own backend IS, and NOT derived from
|
||||
// MGPipeGetResourceOps(): that is a PROCESS-WIDE global, so under inproc a derivation
|
||||
// would answer with whatever the client half of the same process registered and under
|
||||
// spawn it would collapse to P2's 0x7f. Either way CapsMirror::ServerConsumes would
|
||||
// then answer a client-side liveness gate with a guess, the client would stop emitting
|
||||
// whole record families, clear its dirty flags on acceptance anyway, and the lane would
|
||||
// go green with the uploads lost - ID-39's 66 lost uploads, reflected (ServerSession.h).
|
||||
//
|
||||
// DirectGLES consumes all thirteen migrated families (P2's 0..6, P3a's 7..8, P4a's
|
||||
// 9..12): it registers the resource op table in Initialize()
|
||||
// (BackendObject_DirectGLES.cpp:849) and reads every other family out of gPipeInputs.
|
||||
// DirectVulkan registers NO resource ops - MGPipeSetResourceOps has exactly one caller
|
||||
// in the whole tree and it is Managers.cpp:2594 - so bit 7 is CLEAR for it, which is
|
||||
// the same fact ObjectSubsystemControlScenario already pins from the client side.
|
||||
Uint64 ConsumedSubsystemsFor(BackendType type) {
|
||||
switch (type) {
|
||||
case BackendType::DirectGLES: return MG_Pipe::kMGPipeSubsystemsMigratedAtP4a;
|
||||
case BackendType::DirectVulkan:
|
||||
return MG_Pipe::kMGPipeSubsystemsMigratedAtP4a & ~MG_Pipe::kMGPipeSubsystemResources;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// THE CROSS-CHECK THAT MAKES A WRONG ANSWER LOUD. Claiming bit 7 while no resource op
|
||||
// table is registered is the exact failure the mask exists to prevent, one level down:
|
||||
// the client would keep emitting the resource family and the server would drop every
|
||||
// record of it. The check runs AFTER Initialize(), which is where DirectGLES registers
|
||||
// the table, so it can see the real answer rather than a promise.
|
||||
void AssertConsumerMaskIsHonest(Uint64 mask) {
|
||||
const Bool claimsResources = (mask & MG_Pipe::kMGPipeSubsystemResources) != 0;
|
||||
const Bool hasResourceOps = MG_Pipe::MGPipeGetResourceOps() != nullptr;
|
||||
if (claimsResources && !hasResourceOps) {
|
||||
MGLOG_F("MGPipe: Fatal{ConsumerMaskLie, \"kMGPipeSubsystemResources\"} - the "
|
||||
"server published a consumer bit for the resource family while "
|
||||
"MGPipeGetResourceOps() is null. The client's R-8 liveness gate would "
|
||||
"keep emitting resource_create / resource_subdata records that this "
|
||||
"server drops on the floor, and the client clears its dirty flags on "
|
||||
"acceptance anyway (ID-39). A mask is a statement about this backend, "
|
||||
"not a hope");
|
||||
std::abort();
|
||||
}
|
||||
if (!claimsResources && hasResourceOps) {
|
||||
// The safe direction: the legacy pull path keeps running. Said out loud anyway,
|
||||
// because it silently costs the whole P3a family its migration.
|
||||
MGLOG_W("MG_Remote server: a resource op table is registered but the consumer "
|
||||
"mask withholds kMGPipeSubsystemResources; the buffer family will fall "
|
||||
"back to the legacy path for this session");
|
||||
}
|
||||
}
|
||||
|
||||
// The single hook (ARCHITECTURE.md:29). Returns false when the split could not be
|
||||
// brought up, and the caller then REFUSES TO CONTINUE rather than falling back to the
|
||||
// switch below - a fallback here is "the split lane ran monolith and went green".
|
||||
Bool InitSplitRoles() {
|
||||
using namespace MobileGL::MG_Remote;
|
||||
|
||||
// 1. the SERVER role's private backend object, on the app thread, with no GL and no
|
||||
// EGL. The context is created and made current later, on mgl-srv-apply, when the
|
||||
// client's first eglMakeCurrent crosses as a blocking control request.
|
||||
Server::ServerLoop& loop = Server::ServerLoopInstance();
|
||||
const MobileGLResult created = loop.CreateBackend(MG_Config::ActiveBackendType);
|
||||
if (created != MOBILEGL_OK) return false;
|
||||
|
||||
// 2. the two CallMask halves. NEITHER HAS A DEFAULT and CallMask() is a named Fatal
|
||||
// on an unset one (s1's BLOCKER fix), so this is the "somebody" that block names.
|
||||
Server::ServerSession& session = Server::ServerSessionInstance();
|
||||
const Uint64 consumed = ConsumedSubsystemsFor(MG_Config::ActiveBackendType);
|
||||
AssertConsumerMaskIsHonest(consumed);
|
||||
session.SetConsumedSubsystems(consumed);
|
||||
// ZERO IS THE EXPLICIT ANSWER FOR P5, not an omission (ServerSession.h's block):
|
||||
// every optional capability bit belongs to the package that owns its question, and
|
||||
// withholding one leaves the legacy path running, which is the safe direction.
|
||||
// kCapNeedsHostIndexBytes and kCapNeedsHostUboBytes must be 0 for the whole of P5
|
||||
// by ruling - they are the only two things that ask for an MGHostSpan, and 0 is
|
||||
// what keeps every one of them out of the first IPC frame (contract table 0).
|
||||
session.SetCapabilityBits(0);
|
||||
session.SetBackend(loop.Backend());
|
||||
|
||||
// 3. the handshake, the four segments, and - at its end - the apply thread.
|
||||
const MobileGLResult started =
|
||||
Client::ClientSessionInstance().Start(MG_Config::Transport, MG_Config::TransportEndpoint);
|
||||
if (started != MOBILEGL_OK) {
|
||||
MGLOG_E("MG_Remote: the split session failed to start (rc=%d); MobileGL will not "
|
||||
"fall back to monolith - a lane named split that ran monolith is the one "
|
||||
"failure this phase is built to make impossible",
|
||||
static_cast<int>(started));
|
||||
return false;
|
||||
}
|
||||
|
||||
// 4. and only now the CLIENT's backend object in the one global that holds it.
|
||||
// Table 3: pActiveBackendObject holds BackendObject_Remote and the server's
|
||||
// BackendObject_DirectGLES stays private to ServerLoop.
|
||||
pActiveBackendObject = MG_Remote::Client::CreateRemoteBackendObject();
|
||||
if (!pActiveBackendObject) {
|
||||
MGLOG_E("MG_Remote: CreateRemoteBackendObject returned null");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
} // namespace
|
||||
#endif
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
void ShutdownSplitRoles() {
|
||||
if (MG_Config::Transport == MG_Config::TransportMode::Monolith) return;
|
||||
// ClientSession::Stop IS table 3's whole order and it is idempotent: publish and wait
|
||||
// for the server to drain (bounded - a lost record must be a red lane, not a hung
|
||||
// exit), Doorbell::Kill through the transport's Shutdown, ServerLoop::Stop's bounded
|
||||
// join - which also destroys the server's private BackendObject ON the apply thread
|
||||
// while it still owns the context - the transport, and only THEN anything an emitter
|
||||
// owns. A var-tail still named by an unapplied record is a use-after-free the join is
|
||||
// what prevents, which is why the order is not a preference.
|
||||
MG_Remote::Client::ClientSessionInstance().Stop();
|
||||
}
|
||||
#endif
|
||||
|
||||
void Init() {
|
||||
MGLOG_D("Initializing MobileGL Backend...");
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// THE SINGLE HOOK. In a build without MOBILEGL_BUILD_DISAGGREGATED, MG_Config::Transport
|
||||
// is a `constexpr Monolith` (Config.h) and this whole statement is discarded, so the
|
||||
// pull build gains no symbol, no branch and no byte - which is what G1 measures.
|
||||
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
|
||||
if (!InitSplitRoles()) {
|
||||
// NOT a fallback to the switch. pActiveBackendObject stays null and the next GL
|
||||
// call fails loudly, which is the only honest outcome: the operator asked for a
|
||||
// transport this process could not bring up.
|
||||
pActiveBackendObject = nullptr;
|
||||
return;
|
||||
}
|
||||
Bool remoteResult = InitSpecificBackendLibs();
|
||||
if (!remoteResult) {
|
||||
MGLOG_W("Failed to initialize MobileGL backend libraries for the remote object");
|
||||
return;
|
||||
}
|
||||
LogBackendInfo();
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
switch (MG_Config::ActiveBackendType) {
|
||||
case BackendType::DirectGLES:
|
||||
pActiveBackendObject = MakeUnique<DirectGLES::BackendObject_DirectGLES>();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
#include <MG_Pipe/MGPipeRenderStateSpans.h>
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
#include <MG_Pipe/PipeRoute.h>
|
||||
#include <MG_Util/Metrics/PipeStats.h>
|
||||
|
||||
#include <cstring>
|
||||
@@ -154,7 +155,7 @@ namespace MobileGL::MG_Pipe {
|
||||
// minted from a neighbour rather than from nothing.
|
||||
desc.ChunkMask = kAllPipelineChunks;
|
||||
desc.Blob.Size = kMGPipePipelineChunkBytes;
|
||||
MGPipeApplyCreateRenderState(desc, bytes.data());
|
||||
MGPipeRouteCreateRenderState(desc, bytes.data());
|
||||
payloadBytes += sizeof(MGPRenderStateDesc) + kMGPipePipelineChunkBytes;
|
||||
|
||||
Entry entry;
|
||||
@@ -175,7 +176,7 @@ namespace MobileGL::MG_Pipe {
|
||||
MGPHandleOnly handle{};
|
||||
handle.Handle = m_entries[index].Cso;
|
||||
handle.Kind = static_cast<Uint32>(MGPipeKind::RenderStateCso);
|
||||
MGPipeApplyDeleteRenderState(handle);
|
||||
MGPipeRouteDeleteRenderState(handle);
|
||||
MGPipeSlots().Free(MGPipeKind::RenderStateCso, m_entries[index].Cso);
|
||||
m_entries[index] = m_entries.back();
|
||||
m_entries.pop_back();
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
#include <MG_Impl/Pipe/Tracker.h>
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
#include <MG_Pipe/PipeRoute.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_Util/Metrics/PipeStats.h>
|
||||
|
||||
@@ -456,7 +457,7 @@ namespace MobileGL::MG_Pipe {
|
||||
m_lastDraw = state;
|
||||
if (state.Target == static_cast<Uint8>(MGPipeFramebufferTarget::Both)) m_lastRead = state;
|
||||
}
|
||||
MGPipeApplySetFramebufferState(state);
|
||||
MGPipeRouteSetFramebufferState(state);
|
||||
++m_emissions;
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::FramebufferEmissions, 1);
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
#include <MG_Impl/Pipe/SlotAllocator.h>
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
#include <MG_Pipe/PipeRoute.h>
|
||||
#include <MG_Pipe/PipeMutation.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_State/GLState/TextureState/TextureState.h>
|
||||
@@ -126,7 +127,7 @@ namespace MobileGL::MG_Pipe {
|
||||
m_lastImages.Start = 0;
|
||||
m_lastImages.Count = count;
|
||||
m_lastImages.ContentHash = hash;
|
||||
MGPipeApplySetShaderImages(m_lastImages, m_entries.data());
|
||||
MGPipeRouteSetShaderImages(m_lastImages, m_entries.data());
|
||||
++m_imageSets;
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::ShaderImageEmissions, 1);
|
||||
|
||||
@@ -36,9 +36,20 @@
|
||||
#include <MG_Impl/Pipe/VertexInputEmit.h>
|
||||
#include <MG_Pipe/MGPipeRenderStateSpans.h>
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
#include <MG_Pipe/PipeRoute.h>
|
||||
#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>
|
||||
#include <MG_Remote/Client/WireTables.h>
|
||||
#endif
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
@@ -609,12 +620,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;
|
||||
}
|
||||
@@ -642,7 +679,7 @@ namespace MobileGL::MG_Pipe {
|
||||
// LATCHED, so the destroy is gated on whether this create actually went out rather
|
||||
// than on whether a table is still registered when the object dies (D-L, m12).
|
||||
tracker.NotePublished(handle);
|
||||
MGPipeApplyResourceCreate(desc);
|
||||
MGPipeRouteResourceCreate(desc);
|
||||
}
|
||||
|
||||
void MGPipeEmitResourceRespecify(BufferObject& buffer) {
|
||||
@@ -675,7 +712,7 @@ namespace MobileGL::MG_Pipe {
|
||||
MGPipeBuildResourceDesc(buffer, handle, bindMask, /*storageDefined=*/false);
|
||||
tracker.NoteDesc(createDesc, true);
|
||||
tracker.NotePublished(handle);
|
||||
MGPipeApplyResourceCreate(createDesc);
|
||||
MGPipeRouteResourceCreate(createDesc);
|
||||
}
|
||||
const MGPResourceDesc desc = MGPipeBuildResourceDesc(buffer, handle, bindMask, true);
|
||||
tracker.NoteDesc(desc, false);
|
||||
@@ -688,7 +725,58 @@ namespace MobileGL::MG_Pipe {
|
||||
// allocation and only it is allowed one. In monolith the acknowledgement is
|
||||
// ((void)0), because the applier is one function call away and has already run by
|
||||
// the time this returns; the transport wires the doorbell to that same predicate.
|
||||
MGPipeApplyResourceRespecify(desc, initialBytes);
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// R-13.3's MISSING PRODUCER, and it is the reason the first joint inproc run died.
|
||||
// CONTRACT-P5 §2 row 19 rules that `initialBytes` is ALWAYS nullptr under split and
|
||||
// that "initial content arrives as ResourceSubData records immediately after this
|
||||
// one" - but nothing emitted those records, so the split arm's refusal
|
||||
// (Fatal{UncarriedInitialBytes}) fired on the first glBufferData with data, which is
|
||||
// the first thing every scenario does.
|
||||
//
|
||||
// IT IS THE CONTRACT'S OWN PRESCRIBED ROUTE, not a new one: "the chosen route reuses a
|
||||
// path that is already chunked (MGPipeForEachSubDataRecordRange) and already
|
||||
// acceptance-gated; it costs one extra record". So the respecify defines the storage
|
||||
// and the walk below ships the bytes, through the same emitter every later
|
||||
// glBufferSubData uses - which also means the HasLiveHostWrites bit and the
|
||||
// acceptance latch are computed in exactly one place instead of two.
|
||||
//
|
||||
// SPLIT-ONLY, and that is load-bearing for G2: under monolith the applier reads
|
||||
// `initialBytes` directly and a second upload would be a real behaviour change in the
|
||||
// arm the split arm is measured against.
|
||||
//
|
||||
// ROLE-AWARE (M5). Under inproc the apply thread reaches this very emitter when the
|
||||
// server's own backend respecifies a buffer (c1-v2 §4 R-17.3: that is where
|
||||
// Fatal{BarrierTimeout, "ResourceRespecify"} from mgl-srv-apply came from). On that
|
||||
// thread this branch would emit CLIENT wire records - which the server role does not
|
||||
// produce, so ClientWireRecordsEmitted() would not move and the self-check below would
|
||||
// abort the SERVER by name; it would also run the respecify(nullptr)+follow-up shape,
|
||||
// giving the server role a path monolith does not have. So the server role takes the
|
||||
// ELSE below, exactly as monolith does. RunsAsTheServerRole() is v1's
|
||||
// ServerLoop::OnApplyThread(), false on the GL thread that owns this fill.
|
||||
if (MG_Config::Transport != MG_Config::TransportMode::Monolith &&
|
||||
!MG_Remote::Client::RunsAsTheServerRole() && initialBytes != nullptr) {
|
||||
MGPipeRouteResourceRespecify(desc, nullptr);
|
||||
// COUNTED, NOT ASSUMED, and this is the only thing that can gate the follow-up at
|
||||
// all. Dropping the walk below leaves a respecify that went out with nullptr and
|
||||
// bytes that nothing carried - and no P5 scenario's PICTURE changes, because every
|
||||
// one of them re-uploads its vertices through the ordinary dirty path afterwards.
|
||||
// So the statement "the content followed" is made HERE, against the client's own
|
||||
// record ordinal, rather than left to a lane that cannot see it. Remove the call
|
||||
// below and this aborts by name on the first glBufferData that carries data.
|
||||
const Uint64 before = MG_Remote::Client::ClientWireRecordsEmitted();
|
||||
MGPipeEmitResourceSubData(buffer, 0, static_cast<SizeT>(buffer.GetSize()));
|
||||
if (MG_Remote::Client::ClientWireRecordsEmitted() == before) {
|
||||
MGLOG_F("MGPipe: Fatal{InitialBytesNotCarried, \"resource_respecify\"} - the "
|
||||
"respecify crossed with initialBytes = nullptr (R-13.3) and the "
|
||||
"resource_subdata records that were supposed to follow it emitted "
|
||||
"NOTHING, so %llu bytes of initial content exist on no side of the wire",
|
||||
static_cast<unsigned long long>(buffer.GetSize()));
|
||||
std::abort();
|
||||
}
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
MGPipeRouteResourceRespecify(desc, initialBytes);
|
||||
}
|
||||
|
||||
void MGPipeEmitResourceSubData(BufferObject& buffer, SizeT offset, SizeT size) {
|
||||
@@ -711,7 +799,11 @@ namespace MobileGL::MG_Pipe {
|
||||
// take the object, which is why it is set here and not in the builder.
|
||||
record.HasLiveHostWrites = buffer.HasLiveHostWritesForWire() ? 1 : 0;
|
||||
#endif
|
||||
MGPipeApplyResourceSubData(record, base + at);
|
||||
// `length` is this chunk's byte count, which the record also declares
|
||||
// (MGPipeBuildSubDataRecord writes it into the destination range) - passed
|
||||
// rather than re-read so the staged run and the record's own claim come from
|
||||
// one number.
|
||||
MGPipeRouteResourceSubData(record, base + at, length);
|
||||
});
|
||||
if (!encodable) {
|
||||
MGLOG_E_ONCE("MGPipe: resource_subdata range [%llu, +%llu) on buffer %u cannot be encoded - "
|
||||
@@ -742,7 +834,7 @@ namespace MobileGL::MG_Pipe {
|
||||
record.HasLiveHostWrites = buffer.HasLiveHostWritesForWire() ? 1 : 0;
|
||||
#endif
|
||||
// The application's STAGING store, valid for the duration of the call only.
|
||||
MGPipeApplyBufferSubDataResident(record, base + (at - offset));
|
||||
MGPipeRouteBufferSubDataResident(record, base + (at - offset), length);
|
||||
});
|
||||
if (!encodable) {
|
||||
MGLOG_E_ONCE("MGPipe: buffer_subdata_resident range [%llu, +%llu) on buffer %u cannot be encoded",
|
||||
@@ -762,7 +854,31 @@ namespace MobileGL::MG_Pipe {
|
||||
// arm reads INVALIDATE_RANGE / INVALIDATE_BUFFER / UNSYNCHRONIZED per call to choose
|
||||
// between a map+memcpy+unmap and an upload, so merging them here would change which.
|
||||
record.AccessFlags = accessFlags;
|
||||
MGPipeApplyResourceFlushRange(record, buffer.MappedData() + offset);
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// R-13.2's MISSING PRODUCER, the twin of the respecify one above, and the cause of the
|
||||
// seven PersistentCoherentMapScenario aborts on the first joint inproc run:
|
||||
// Fatal{ProtocolCorruption} resource_flush_range {slot=1, gen=0, glName=1}:
|
||||
// a non-empty flush carries no bytes (offset=0, size=120, storage=120 bytes)
|
||||
//
|
||||
// CONTRACT-P5 §2 row 20 rules that this record carries NO bytes under split - it is a
|
||||
// {range, AccessFlags} control record, and a blobref here "would be a second,
|
||||
// forgeable way to say the same thing" - and that "the bytes of [Offset, Offset+Size)
|
||||
// arrive AHEAD of it as ResourceSubData records covering exactly that range". Nothing
|
||||
// emitted those records, so v1's StagedShadowStore had nothing staged for the range
|
||||
// the flush names, which is precisely the refusal ID-37 asked it to make rather than
|
||||
// silently reading the bytes again.
|
||||
//
|
||||
// EXACTLY THAT RANGE, not the whole buffer: the flush's own [offset, size) is what
|
||||
// the ladder rewrites, and staging more would be the coverage WIDENING ID-37 forbids.
|
||||
// Split-only, for the respecify's G2 reason - and ROLE-AWARE for M5's reason, the twin of
|
||||
// the respecify branch above: the apply thread flushing the server's own buffer emits no
|
||||
// client wire records, so it takes the plain route below rather than this split follow-up.
|
||||
if (MG_Config::Transport != MG_Config::TransportMode::Monolith &&
|
||||
!MG_Remote::Client::RunsAsTheServerRole() && size != 0) {
|
||||
MGPipeEmitResourceSubData(buffer, offset, size);
|
||||
}
|
||||
#endif
|
||||
MGPipeRouteResourceFlushRange(record, buffer.MappedData() + offset);
|
||||
}
|
||||
|
||||
void MGPipeEmitResourceReadback(BufferObject& buffer) {
|
||||
@@ -776,7 +892,7 @@ namespace MobileGL::MG_Pipe {
|
||||
record.Size = buffer.GetSize();
|
||||
// The answer travels back through MGPipeClientOnBufferWriteback, and the server's
|
||||
// epoch bump happens AFTER that writeback, never before.
|
||||
MGPipeApplyResourceReadback(record);
|
||||
MGPipeRouteResourceReadback(record);
|
||||
}
|
||||
|
||||
// NO UnmapPersistent PRODUCER IN P3a, AND THAT IS DELIBERATE. The catalogue has the call
|
||||
@@ -799,7 +915,7 @@ namespace MobileGL::MG_Pipe {
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::MapPersistentRoundtrips, 1);
|
||||
}
|
||||
return MGPipeApplyMapPersistent(BufferHandleOnly(handle), buffer.GetSize(), buffer.MappedData());
|
||||
return MGPipeRouteMapPersistent(BufferHandleOnly(handle), buffer.GetSize(), buffer.MappedData());
|
||||
}
|
||||
|
||||
Bool MGPipeEmitResourceDestroyAndFree(BufferObject& buffer) {
|
||||
@@ -814,7 +930,7 @@ namespace MobileGL::MG_Pipe {
|
||||
const Bool published = tracker.WasPublished(handle);
|
||||
if (published) {
|
||||
tracker.NoteDestroy();
|
||||
MGPipeApplyResourceDestroy(BufferHandleOnly(handle));
|
||||
MGPipeRouteResourceDestroy(BufferHandleOnly(handle));
|
||||
}
|
||||
// THE ORDER IS FIXED (D-L): the applier clears the record and the backend drops its
|
||||
// twin while the handle still resolves, and only then does the slot go back. Free
|
||||
@@ -854,7 +970,7 @@ namespace MobileGL::MG_Pipe {
|
||||
MGPHandleOnly only{};
|
||||
only.Handle = handle;
|
||||
only.Kind = static_cast<Uint32>(MGPipeKind::VertexElementsCso);
|
||||
MGPipeApplyDeleteVertexElements(only);
|
||||
MGPipeRouteDeleteVertexElements(only);
|
||||
emitter.NoteRecordDestroyed(handle);
|
||||
}
|
||||
|
||||
@@ -936,8 +1052,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;
|
||||
}
|
||||
|
||||
// ================================================================================
|
||||
@@ -1395,9 +1524,21 @@ namespace MobileGL::MG_Pipe {
|
||||
// Step 1, shared: the wire delete goes out FIRST and only for a PUBLISHED handle, and
|
||||
// the latch is cleared with it so a second death path - a composite's two, a backend's
|
||||
// redundant notice - cannot emit a second delete for a record that is already gone.
|
||||
Bool EmitDeleteIfPublished(MGPipeKind kind, MGPipeHandle handle, void (*apply)(const MGPHandleOnly&)) {
|
||||
//
|
||||
// `route` IS A MGPipeRoute<Name> AND NEVER A MGPipeApply<Name> (B1). R-17 converted the
|
||||
// 40 direct applier CALLS to route calls by renaming `MGPipeApply<Name>(` -> but these
|
||||
// five sites take the entry point BY ADDRESS, `&MGPipeApply<Name>`, so the call-expression
|
||||
// rename missed them and four routed rows (DeleteSamplerView, DeleteShaderState,
|
||||
// DeleteSamplerState, ResourceDestroy for textures/renderbuffers) still ran the applier
|
||||
// synchronously on the GL thread under split - two writers on g_applier with the barrier
|
||||
// not consulted, and under spawn a silent no-op that leaks every one of those objects.
|
||||
// The parameter type is the route's, which is byte-identical to the applier's
|
||||
// (const MGPHandleOnly&, void return), so the fix is `&MGPipeRoute<Name>` at the five call
|
||||
// sites; the grep gate scripts/../p5-c1 redcheck refuses any `&MGPipeApply` under MG_Impl/.
|
||||
Bool EmitDeleteIfPublished(MGPipeKind kind, MGPipeHandle handle,
|
||||
void (*route)(const MGPHandleOnly&)) {
|
||||
if (!MGPipeHandleIsPublished(kind, handle)) return false;
|
||||
apply(HandleOnly(kind, handle));
|
||||
route(HandleOnly(kind, handle));
|
||||
MGPipeNoteHandleUnpublished(kind, handle);
|
||||
return true;
|
||||
}
|
||||
@@ -1418,7 +1559,7 @@ namespace MobileGL::MG_Pipe {
|
||||
const MGPipeHandle handle =
|
||||
MGPipeSlots().FindByLifetimeId(MGPipeKind::SamplerViewCso, lifetimeId);
|
||||
const Bool published =
|
||||
EmitDeleteIfPublished(MGPipeKind::SamplerViewCso, handle, &MGPipeApplyDeleteSamplerView);
|
||||
EmitDeleteIfPublished(MGPipeKind::SamplerViewCso, handle, &MGPipeRouteDeleteSamplerView);
|
||||
ForwardWhenWired<kMGPipeWiredSamplerSubsystem>(
|
||||
MGPipeSamplerEmitterInstance(), [&](auto& emitter) { emitter.NoteRecordDestroyed(handle); });
|
||||
// THE NOTICE IS RAISED FOR THIS KIND TOO, and the reason it once was not is wrong:
|
||||
@@ -1439,7 +1580,7 @@ namespace MobileGL::MG_Pipe {
|
||||
Bool MGPipeEmitTextureDestroyAndFree(Uint64 lifetimeId) {
|
||||
const MGPipeHandle handle = MGPipeSlots().FindByLifetimeId(MGPipeKind::Texture, lifetimeId);
|
||||
const Bool published =
|
||||
EmitDeleteIfPublished(MGPipeKind::Texture, handle, &MGPipeApplyResourceDestroy);
|
||||
EmitDeleteIfPublished(MGPipeKind::Texture, handle, &MGPipeRouteResourceDestroy);
|
||||
// The emitter retires its entry while the handle still resolves (C-2): the drain list
|
||||
// drops the dead texture's levels, the raw pointer goes, the built-in sampler's cache
|
||||
// reference is given back, the latches and the sticky mask are cleared.
|
||||
@@ -1480,7 +1621,7 @@ namespace MobileGL::MG_Pipe {
|
||||
const MGPipeHandle handle =
|
||||
MGPipeSlots().FindByLifetimeId(MGPipeKind::Renderbuffer, lifetimeId);
|
||||
const Bool published =
|
||||
EmitDeleteIfPublished(MGPipeKind::Renderbuffer, handle, &MGPipeApplyResourceDestroy);
|
||||
EmitDeleteIfPublished(MGPipeKind::Renderbuffer, handle, &MGPipeRouteResourceDestroy);
|
||||
ForwardWhenWired<kMGPipeWiredTextureSubsystem>(
|
||||
MGPipeTextureEmitterInstance(), [&](auto& emitter) { emitter.NoteRenderbufferDied(handle); });
|
||||
NotifyAndFree(MGPipeKind::Renderbuffer, lifetimeId, handle);
|
||||
@@ -1517,7 +1658,7 @@ namespace MobileGL::MG_Pipe {
|
||||
const MGPipeHandle handle =
|
||||
MGPipeSlots().FindByLifetimeId(MGPipeKind::SamplerCso, lifetimeId);
|
||||
const Bool published =
|
||||
EmitDeleteIfPublished(MGPipeKind::SamplerCso, handle, &MGPipeApplyDeleteSamplerState);
|
||||
EmitDeleteIfPublished(MGPipeKind::SamplerCso, handle, &MGPipeRouteDeleteSamplerState);
|
||||
// NOTHING TO RETIRE IN AN EMITTER FOR THIS KIND, stated rather than implied: a sampler
|
||||
// CSO is content-addressed and belongs to a value, so no emitter keeps an entry under
|
||||
// a SamplerObject's handle - the cache's entries are keyed by value and reference
|
||||
@@ -1538,7 +1679,7 @@ namespace MobileGL::MG_Pipe {
|
||||
const MGPipeHandle handle =
|
||||
MGPipeSlots().FindByLifetimeId(MGPipeKind::ShaderCso, lifetimeId);
|
||||
const Bool published =
|
||||
EmitDeleteIfPublished(MGPipeKind::ShaderCso, handle, &MGPipeApplyDeleteShaderState);
|
||||
EmitDeleteIfPublished(MGPipeKind::ShaderCso, handle, &MGPipeRouteDeleteShaderState);
|
||||
ForwardWhenWired<kMGPipeWiredProgramSubsystem>(
|
||||
MGPipeProgramEmitterInstance(), [&](auto& emitter) { emitter.NoteRecordDestroyed(handle); });
|
||||
NotifyAndFree(MGPipeKind::ShaderCso, lifetimeId, handle);
|
||||
@@ -2055,7 +2196,7 @@ namespace MobileGL::MG_Pipe {
|
||||
Uint64 EmitPixelPackState(GLContext& ctx) {
|
||||
MGPPixelPackState pack{};
|
||||
pack.Pack = ctx.GetPixelStoreParameters(false);
|
||||
MGPipeApplySetPixelPackState(pack);
|
||||
MGPipeRouteSetPixelPackState(pack);
|
||||
return sizeof(MGPPixelPackState);
|
||||
}
|
||||
|
||||
@@ -2068,7 +2209,7 @@ namespace MobileGL::MG_Pipe {
|
||||
patch.Vertices = live.PatchVertices;
|
||||
for (SizeT i = 0; i < 4; ++i) patch.Outer[i] = live.PatchDefaultOuterLevel[i];
|
||||
for (SizeT i = 0; i < 2; ++i) patch.Inner[i] = live.PatchDefaultInnerLevel[i];
|
||||
MGPipeApplySetPatchState(patch);
|
||||
MGPipeRouteSetPatchState(patch);
|
||||
return sizeof(MGPPatchState);
|
||||
}
|
||||
|
||||
@@ -2157,7 +2298,7 @@ namespace MobileGL::MG_Pipe {
|
||||
}
|
||||
if (header.Count == 0) return 0;
|
||||
g_attribDefaultLastHeader = header;
|
||||
MGPipeApplySetVertexAttribDefaults(header, tail.data());
|
||||
MGPipeRouteSetVertexAttribDefaults(header, tail.data());
|
||||
|
||||
// Did the applier reproduce it? Byte for byte, over the attributes this call
|
||||
// named - anything less would be a mirror that disagrees with the frontend in a
|
||||
@@ -2226,7 +2367,7 @@ namespace MobileGL::MG_Pipe {
|
||||
block.CapabilityBits |= Uint64{1} << i;
|
||||
}
|
||||
}
|
||||
MGPipeApplySetResidualValueState(block);
|
||||
MGPipeRouteSetResidualValueState(block);
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
// ByteClass::ResidualValueBlock has been a placeholder that "stays at 0
|
||||
// until P2" since P0. This is what makes it non-zero.
|
||||
@@ -2280,7 +2421,7 @@ namespace MobileGL::MG_Pipe {
|
||||
bind.Cso = cso;
|
||||
bind.Version = version;
|
||||
bind.PipelineVersion = pipelineVersion;
|
||||
MGPipeApplyBindRenderState(bind);
|
||||
MGPipeRouteBindRenderState(bind);
|
||||
payloadBytes += sizeof(MGPBindRenderState);
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::RenderStateCsoBinds, 1);
|
||||
@@ -2302,7 +2443,7 @@ namespace MobileGL::MG_Pipe {
|
||||
dyn.ChunkMask = chunkMask;
|
||||
dyn.Version = version;
|
||||
dyn.Blob.Size = blobBytes;
|
||||
MGPipeApplySetDynamicState(dyn, blob.data());
|
||||
MGPipeRouteSetDynamicState(dyn, blob.data());
|
||||
payloadBytes += sizeof(MGPDynamicState) + blobBytes;
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
#include <MG_Pipe/MGPipeHostSpan.h>
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
#include <MG_Pipe/PipeRoute.h>
|
||||
#include <MG_Pipe/PipeMutation.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_State/GLState/ProgramState/ProgramObject.h>
|
||||
@@ -137,19 +138,19 @@ namespace MobileGL::MG_Pipe {
|
||||
// exactly "nothing bound".
|
||||
const MGPipeHandle boundCso = !MGPipeHandleIsNull(drawCso) ? drawCso : dispatchCso;
|
||||
if (boundCso != m_boundCso) {
|
||||
MGPipeApplyBindShaderState(HandleOnly(boundCso));
|
||||
MGPipeRouteBindShaderState(HandleOnly(boundCso));
|
||||
m_boundCso = boundCso;
|
||||
++m_binds;
|
||||
bytes += sizeof(MGPHandleOnly);
|
||||
}
|
||||
if (drawCso != m_drawCso) {
|
||||
MGPipeApplySetDrawProgram(HandleOnly(drawCso));
|
||||
MGPipeRouteSetDrawProgram(HandleOnly(drawCso));
|
||||
m_drawCso = drawCso;
|
||||
++m_drawSets;
|
||||
bytes += sizeof(MGPHandleOnly);
|
||||
}
|
||||
if (dispatchCso != m_dispatchCso) {
|
||||
MGPipeApplySetDispatchProgram(HandleOnly(dispatchCso));
|
||||
MGPipeRouteSetDispatchProgram(HandleOnly(dispatchCso));
|
||||
m_dispatchCso = dispatchCso;
|
||||
++m_dispatchSets;
|
||||
bytes += sizeof(MGPHandleOnly);
|
||||
@@ -192,7 +193,12 @@ namespace MobileGL::MG_Pipe {
|
||||
m_lastConstants.Blob.Seg = kMGHostSpanSegNone;
|
||||
m_lastConstants.Blob.Offset = reinterpret_cast<Uint64>(program->GetUBOData());
|
||||
m_lastConstants.Blob.Size = 0;
|
||||
MGPipeApplySetGlobalConstants(m_lastConstants, program->GetUBOData());
|
||||
// `size` is GetUBOSize(), and it is passed because the record declares 0 - the
|
||||
// monolith convention (the bytes ride beside the record) that CONTRACT-P5 table 1
|
||||
// rule A cannot keep under split. It is the row's largest and least bounded blob,
|
||||
// per program per frame, so it is also the one R-10's max-record counter watches.
|
||||
MGPipeRouteSetGlobalConstants(m_lastConstants, program->GetUBOData(),
|
||||
static_cast<Uint64>(size));
|
||||
m_constantsCso = cso;
|
||||
m_constantsVersion = version;
|
||||
++m_constantSets;
|
||||
@@ -254,7 +260,7 @@ namespace MobileGL::MG_Pipe {
|
||||
m_lastDesc.Reflection.Offset = reinterpret_cast<Uint64>(&link);
|
||||
m_lastDesc.Reflection.Size = 0;
|
||||
|
||||
MGPipeApplyCreateShaderState(m_lastDesc, &link, &spirv);
|
||||
MGPipeRouteCreateShaderState(m_lastDesc, &link, &spirv);
|
||||
// THE CREATE WENT OUT, so the publication latch is taken here and nowhere else
|
||||
// (contract-v2 §3.1). MGPipeEmitShaderCsoDestroyAndFree reads it, and without it
|
||||
// delete_shader_state can never go out - for an ordinary program or for a
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
#include <MG_Pipe/MGPipeHostSpan.h>
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
#include <MG_Pipe/PipeRoute.h>
|
||||
#include <MG_Pipe/PipeMutation.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_State/GLState/ProgramState/ProgramObject.h>
|
||||
@@ -455,7 +456,7 @@ namespace MobileGL::MG_Pipe {
|
||||
// The applier is handed the CACHE's copy, so the pointer stays valid for the whole
|
||||
// call and the bytes it stores are provably the bytes the memcmp will confirm
|
||||
// against later.
|
||||
MGPipeApplyCreateSamplerState(desc, &m_entries.back().Params);
|
||||
MGPipeRouteCreateSamplerState(desc, &m_entries.back().Params);
|
||||
// THE CREATE ACTUALLY WENT OUT, so the publication latch is taken here and nowhere
|
||||
// else (contract-v2 §3.1). It is what the six death helpers read, and without it a
|
||||
// delete_sampler_state can never go out for this kind.
|
||||
@@ -490,7 +491,7 @@ namespace MobileGL::MG_Pipe {
|
||||
// only then does the slot go back. There is no NotifyStateObjectDestroyed step
|
||||
// here - a content-addressed CSO has no frontend object whose death is being
|
||||
// announced, which is precisely why this eviction is the only death path it has.
|
||||
MGPipeApplyDeleteSamplerState(handle);
|
||||
MGPipeRouteDeleteSamplerState(handle);
|
||||
// AND THE LATCH GOES WITH THE DELETE. This is the "an emitter that drops a record
|
||||
// for its own reasons calls MGPipeNoteHandleUnpublished" half of the publication
|
||||
// protocol (contract-v2 §3.1): the record is gone, so a death helper reaching this
|
||||
@@ -783,7 +784,7 @@ namespace MobileGL::MG_Pipe {
|
||||
m_lastViews.Start = 0;
|
||||
m_lastViews.Count = count;
|
||||
m_lastViews.ContentHash = hash;
|
||||
MGPipeApplySetSamplerViews(m_lastViews, m_views.data());
|
||||
MGPipeRouteSetSamplerViews(m_lastViews, m_views.data());
|
||||
++m_viewSets;
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::SamplerViewEmissions, 1);
|
||||
@@ -857,7 +858,7 @@ namespace MobileGL::MG_Pipe {
|
||||
m_lastStates.Start = 0;
|
||||
m_lastStates.Count = count;
|
||||
m_lastStates.ContentHash = hash;
|
||||
MGPipeApplyBindSamplerStates(m_lastStates, m_states.data());
|
||||
MGPipeRouteBindSamplerStates(m_lastStates, m_states.data());
|
||||
++m_stateSets;
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::SamplerStateEmissions, 1);
|
||||
@@ -914,7 +915,7 @@ namespace MobileGL::MG_Pipe {
|
||||
m_lastView.NumLayers = static_cast<Uint16>(texture.GetViewNumLayers());
|
||||
m_lastView.Samples = static_cast<Uint16>(texture.GetSamples() < 0 ? 0 : texture.GetSamples());
|
||||
m_lastView.FixedSampleLocations = texture.HasFixedSampleLocations() ? 1 : 0;
|
||||
MGPipeApplyCreateSamplerView(m_lastView);
|
||||
MGPipeRouteCreateSamplerView(m_lastView);
|
||||
// THE CREATE WENT OUT, so the publication latch is taken (contract-v2 §3.1). The
|
||||
// texture's death helper reads it, and without it delete_sampler_view can never go
|
||||
// out - the C-1 leak, one kind later. Re-taking it on a re-issue is right and
|
||||
|
||||
@@ -57,6 +57,7 @@
|
||||
#include <MG_Impl/Pipe/SlotAllocator.h>
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
#include <MG_Pipe/PipeRoute.h>
|
||||
#include <MG_Pipe/PipeMutation.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_State/GLState/RenderbufferState/RenderbufferObject.h>
|
||||
@@ -802,7 +803,7 @@ namespace MobileGL::MG_Pipe {
|
||||
++m_paramSets;
|
||||
// Not behind MGPipeTextureRecordsReachTheApplier() (see its comment): the call is
|
||||
// dispatched whenever this emitter runs, so the answer is always a real one.
|
||||
Bool accepted = MGPipeApplySetTextureParams(params);
|
||||
Bool accepted = MGPipeRouteSetTextureParams(params);
|
||||
if (!accepted) {
|
||||
// THE SELF-HEAL, the respecify path's shape, and the parameters are the one
|
||||
// publication that may be a texture's FIRST: the context's default textures are
|
||||
@@ -828,7 +829,7 @@ namespace MobileGL::MG_Pipe {
|
||||
// across it - `entry` is re-fetched below.
|
||||
EmitResourceRespecify(texture, MGPipeTextureRespecifyScope::WholeResource, 0, 0);
|
||||
}
|
||||
accepted = MGPipeApplySetTextureParams(params);
|
||||
accepted = MGPipeRouteSetTextureParams(params);
|
||||
}
|
||||
Entry& latched = EntryFor(m_textures, handle);
|
||||
if (!accepted) {
|
||||
@@ -1121,7 +1122,7 @@ namespace MobileGL::MG_Pipe {
|
||||
Bool dispatched = false;
|
||||
if constexpr (MGPipeTextureRecordsReachTheApplier()) {
|
||||
dispatched = true;
|
||||
accepted = MGPipeApplyResourceCreate(desc);
|
||||
accepted = MGPipeRouteResourceCreate(desc);
|
||||
}
|
||||
if (dispatched && !accepted) return;
|
||||
MGPipeNoteHandlePublished(kind, handle);
|
||||
@@ -1134,7 +1135,7 @@ namespace MobileGL::MG_Pipe {
|
||||
// replaces no storage at all.
|
||||
static Bool ApplyRespecify(const MGPResourceDesc& desc, const MGPRespecifiedLevel* level) {
|
||||
if constexpr (MGPipeTextureRecordsReachTheApplier()) {
|
||||
return MGPipeApplyResourceRespecify(desc, nullptr, level);
|
||||
return MGPipeRouteResourceRespecify(desc, nullptr, level);
|
||||
}
|
||||
(void)level;
|
||||
return false;
|
||||
@@ -1275,7 +1276,14 @@ namespace MobileGL::MG_Pipe {
|
||||
Bool dispatched = false;
|
||||
if constexpr (MGPipeTextureRecordsReachTheApplier()) {
|
||||
dispatched = true;
|
||||
accepted = MGPipeApplyResourceSubData(m_lastSubData, shadow,
|
||||
// `levelBytes` closes CONTRACT-P5 table 1 row 7's open half. The record still
|
||||
// declares Blob.Size 0 on the monolith arm - where the applier reads the
|
||||
// companion pointer and the destination box bounds the write - but under split
|
||||
// the staged run needs a length, and the comment above already says what it
|
||||
// is: "the bytes this record declares ARE the level shadow". The regions' own
|
||||
// SrcOffsets index into exactly that run.
|
||||
accepted = MGPipeRouteResourceSubData(m_lastSubData, shadow,
|
||||
static_cast<Uint64>(levelBytes),
|
||||
m_regions.empty() ? nullptr : m_regions.data());
|
||||
}
|
||||
++m_subDatas;
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
#include <MG_Impl/Pipe/Tracker.h>
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
#include <MG_Pipe/PipeRoute.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_Util/Metrics/PipeStats.h>
|
||||
|
||||
@@ -158,7 +159,7 @@ namespace MobileGL::MG_Pipe {
|
||||
const auto& vao = ctx.GetBoundVertexArray();
|
||||
if (!vao) {
|
||||
if (!MGPipeHandleIsNull(m_boundHandle)) {
|
||||
MGPipeApplyBindVertexElements(HandleOnly(kMGPipeNullHandle));
|
||||
MGPipeRouteBindVertexElements(HandleOnly(kMGPipeNullHandle));
|
||||
++m_binds;
|
||||
m_boundHandle = kMGPipeNullHandle;
|
||||
m_boundLifetimeId = 0;
|
||||
@@ -178,7 +179,7 @@ namespace MobileGL::MG_Pipe {
|
||||
latch.Gen != handle.Gen;
|
||||
if (configMoved) bytes += EmitCreate(*vao, handle, latch, configVersion);
|
||||
if (lifetimeId != m_boundLifetimeId || m_boundHandle != handle) {
|
||||
MGPipeApplyBindVertexElements(HandleOnly(handle));
|
||||
MGPipeRouteBindVertexElements(HandleOnly(handle));
|
||||
++m_binds;
|
||||
bytes += sizeof(MGPHandleOnly);
|
||||
m_boundHandle = handle;
|
||||
@@ -242,7 +243,7 @@ namespace MobileGL::MG_Pipe {
|
||||
// emulation is server-owned.
|
||||
m_lastBuffers.BaseInstance = baseInstance;
|
||||
m_lastBuffers.ContentHash = hash;
|
||||
MGPipeApplySetVertexBuffers(m_lastBuffers, m_entries.data());
|
||||
MGPipeRouteSetVertexBuffers(m_lastBuffers, m_entries.data());
|
||||
++m_bufferSets;
|
||||
return sizeof(MGPVertexBuffers) + static_cast<Uint64>(count) * sizeof(MGPVertexBuffer);
|
||||
}
|
||||
@@ -267,7 +268,7 @@ namespace MobileGL::MG_Pipe {
|
||||
MGPipeResourceTrackerInstance().NoteBoundAs(m_lastIndex.Res, BufferTarget::Index);
|
||||
}
|
||||
}
|
||||
MGPipeApplySetIndexBuffer(m_lastIndex);
|
||||
MGPipeRouteSetIndexBuffer(m_lastIndex);
|
||||
++m_indexSets;
|
||||
return sizeof(MGPIndexBuffer);
|
||||
}
|
||||
@@ -396,7 +397,7 @@ namespace MobileGL::MG_Pipe {
|
||||
m_lastElements.Blob.Seg = kMGHostSpanSegNone;
|
||||
m_lastElements.Blob.Offset = 0;
|
||||
m_lastElements.Blob.Size = kAttribBytes + kBindingBytes;
|
||||
MGPipeApplyCreateVertexElements(m_lastElements, m_blob.data());
|
||||
MGPipeRouteCreateVertexElements(m_lastElements, m_blob.data());
|
||||
++m_creates;
|
||||
latch.Published = true;
|
||||
latch.Gen = handle.Gen;
|
||||
|
||||
@@ -0,0 +1,366 @@
|
||||
// MobileGL - MobileGL/MG_Pipe/PipeRoute.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
|
||||
|
||||
// The MONOLITH arm of R-17's routing: thirty-seven adapters that unpack a generated table
|
||||
// row's parameters and call the MGPipeApply* entry point the call site used to call directly.
|
||||
// Owner: package c1. See PipeRoute.h for why the arm exists and what R-17 actually cost.
|
||||
//
|
||||
// G2 IS THE WHOLE SPECIFICATION OF THIS FILE. Under monolith transport the thunks must reach
|
||||
// EXACTLY the code they reach today - so every adapter below is a parameter shuffle and
|
||||
// nothing else. There is no branch, no cache, no early return and no logging on any of them,
|
||||
// because each of those is a way for `MOBILEGL_TRANSPORT=monolith` to stop being the control
|
||||
// arm that the split arm is measured against. The one thing an adapter may do beyond calling
|
||||
// through is POST A REPLY, and only the five rows whose call site consumes a value do that.
|
||||
//
|
||||
// WHO CALLS THE INSTALL, AND WHY IT IS NOT IN THIS FILE. `gMGPipeScreen` and `gMGPipeContext`
|
||||
// are inline variables with CONSTANT initialisation, so they are zero - every entry null,
|
||||
// which is the pre-migration state - before any dynamic initialiser runs. The installer is
|
||||
// driven from an inline variable in PipeRoute.h rather than from a static initialiser here,
|
||||
// and the comment beside `detail::gMonolithTablesInstalled` says why: a static initialiser in
|
||||
// THIS file is only in the program if THIS object file is in the link, and over a static
|
||||
// archive it was not. That makes "the tables are installed" an invariant rather than a step
|
||||
// someone can forget, and it is why no MGP_* thunk needs a null check - which matters, because
|
||||
// a null check on a table slot is precisely the shape CONTRACT-P5 §7 spends the 41 call sites
|
||||
// killing.
|
||||
|
||||
#include "PipeRoute.h"
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
|
||||
#include <MG_Util/Debug/Log.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
namespace MobileGL::MG_Pipe {
|
||||
|
||||
namespace {
|
||||
|
||||
// ---------------------------------------------------------------------------------
|
||||
// The reply mailbox
|
||||
// ---------------------------------------------------------------------------------
|
||||
|
||||
struct ReplyMailbox {
|
||||
Uint64 Slot = 0; // the MGPReplySlot::Id this answer belongs to; 0 = empty
|
||||
Int32 Status = 0; // 0 OK, 1 DECLINED, 2 ERROR (Transport::ReplyStatus)
|
||||
Uint64 Value = 0; // the Bool, or the pointer, the row answered with
|
||||
Uint64 NextTicket = 0;
|
||||
Uint64 Taken = 0;
|
||||
Uint64 Declined = 0;
|
||||
};
|
||||
|
||||
// THREAD-LOCAL, and one entry deep, because R-1's verb barrier makes the in-flight
|
||||
// depth exactly one. A second post before the first is taken is not a capacity problem:
|
||||
// it is a barrier that has stopped holding, and it Fatals rather than overwriting.
|
||||
thread_local ReplyMailbox g_reply;
|
||||
|
||||
MGPipeRouteArm g_arm = MGPipeRouteArm::kNone;
|
||||
|
||||
// The monolith adapters, kept as tables of their own so the split arm can reach them
|
||||
// on the server role's thread. See PipeRoute.h.
|
||||
MGPipeScreen g_monolithScreen{};
|
||||
MGPipeContext g_monolithContext{};
|
||||
MGPipeRouteEscapes g_monolithEscapes{};
|
||||
|
||||
} // namespace
|
||||
|
||||
const MGPipeScreen& MGPipeMonolithScreen() { return g_monolithScreen; }
|
||||
const MGPipeContext& MGPipeMonolithContext() { return g_monolithContext; }
|
||||
const MGPipeRouteEscapes& MGPipeMonolithEscapes() { return g_monolithEscapes; }
|
||||
|
||||
MGPReplySlot MGPipeMintReplySlot() {
|
||||
// 1-based, for ReplySlot.h's reason: "Seq is 1-based; 0 means 'no record'". A zero id
|
||||
// must stay unmintable so an un-posted mailbox is distinguishable from a posted one.
|
||||
return MGPReplySlot{++g_reply.NextTicket};
|
||||
}
|
||||
|
||||
void MGPipePostReply(const MGPReplySlot& slot, Int32 status, Uint64 value) {
|
||||
if (slot.Id == 0) {
|
||||
MGLOG_F("MGPipe: Fatal{ReplyToNoSlot} - a table row posted an answer against reply "
|
||||
"slot 0, which ReplySlot.h reserves for \"no record\"");
|
||||
std::abort();
|
||||
}
|
||||
if (g_reply.Slot != 0 && g_reply.Slot != slot.Id) {
|
||||
MGLOG_F("MGPipe: Fatal{ReplyOverrun} - slot %llu posted while slot %llu was still "
|
||||
"unread. The mailbox is one deep because R-1's verb barrier makes the "
|
||||
"in-flight depth one; two outstanding answers means the barrier is not "
|
||||
"holding",
|
||||
static_cast<unsigned long long>(slot.Id),
|
||||
static_cast<unsigned long long>(g_reply.Slot));
|
||||
std::abort();
|
||||
}
|
||||
g_reply.Slot = slot.Id;
|
||||
g_reply.Status = status;
|
||||
g_reply.Value = value;
|
||||
}
|
||||
|
||||
namespace {
|
||||
// The one reader. Fatals rather than defaulting, in both directions - see PipeRoute.h.
|
||||
Uint64 TakeReply(const MGPReplySlot& slot, const char* row, Int32* statusOut) {
|
||||
if (g_reply.Slot == 0) {
|
||||
MGLOG_F("MGPipe: Fatal{ReplyMissing, \"%s\"} - the row was routed and nothing "
|
||||
"posted an answer. R-5 forbids re-deriving acceptance on the client and "
|
||||
"forbids assuming it, so there is no default to fall back to",
|
||||
row);
|
||||
std::abort();
|
||||
}
|
||||
if (g_reply.Slot != slot.Id) {
|
||||
MGLOG_F("MGPipe: Fatal{ReplyMismatched, \"%s\"} - waiting on slot %llu, the "
|
||||
"mailbox holds slot %llu",
|
||||
row, static_cast<unsigned long long>(slot.Id),
|
||||
static_cast<unsigned long long>(g_reply.Slot));
|
||||
std::abort();
|
||||
}
|
||||
const Uint64 value = g_reply.Value;
|
||||
if (statusOut != nullptr) *statusOut = g_reply.Status;
|
||||
if (g_reply.Status == 1) ++g_reply.Declined;
|
||||
++g_reply.Taken;
|
||||
g_reply.Slot = 0;
|
||||
g_reply.Value = 0;
|
||||
g_reply.Status = 0;
|
||||
return value;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Bool MGPipeTakeReplyBool(const MGPReplySlot& slot, const char* row) {
|
||||
Int32 status = 0;
|
||||
const Uint64 value = TakeReply(slot, row, &status);
|
||||
// DECLINED IS A REAL ANSWER AND IT IS `false`, not a failure (ReplySlot.h). ERROR is
|
||||
// not an acceptance answer at all and may not be folded into either.
|
||||
if (status == 2) {
|
||||
MGLOG_F("MGPipe: Fatal{ReplyError, \"%s\"} - the row answered ERROR, which is not an "
|
||||
"acceptance answer; folding it into accepted or refused would make a "
|
||||
"transport fault look like a resource decision",
|
||||
row);
|
||||
std::abort();
|
||||
}
|
||||
if (status == 1) return false;
|
||||
return value != 0;
|
||||
}
|
||||
|
||||
void* MGPipeTakeReplyPointer(const MGPReplySlot& slot, const char* row) {
|
||||
Int32 status = 0;
|
||||
const Uint64 value = TakeReply(slot, row, &status);
|
||||
if (status == 2) {
|
||||
MGLOG_F("MGPipe: Fatal{ReplyError, \"%s\"}", row);
|
||||
std::abort();
|
||||
}
|
||||
// DECLINED is the null pointer, and it is R-6's answer for map_persistent under split.
|
||||
if (status == 1) return nullptr;
|
||||
return reinterpret_cast<void*>(static_cast<std::uintptr_t>(value));
|
||||
}
|
||||
|
||||
Uint64 MGPipeRepliesTaken() { return g_reply.Taken; }
|
||||
Uint64 MGPipeRepliesDeclined() { return g_reply.Declined; }
|
||||
|
||||
MGPipeRouteArm MGPipeInstalledArm() { return g_arm; }
|
||||
void MGPipeNoteInstalledArm(MGPipeRouteArm arm) { g_arm = arm; }
|
||||
Bool MGPipeTablesAreInstalled() { return g_arm != MGPipeRouteArm::kNone; }
|
||||
|
||||
// ---------------------------------------------------------------------------------
|
||||
// The thirty-seven monolith adapters
|
||||
// ---------------------------------------------------------------------------------
|
||||
//
|
||||
// Three shapes, so the eye can check them against PipeTables.inc in one pass rather than
|
||||
// reading thirty-seven bodies. A row that does not fit one of the three is written out by
|
||||
// hand BELOW the macros, never by widening a macro - a macro that grew a special case is
|
||||
// how one of these silently stops being a parameter shuffle.
|
||||
|
||||
namespace {
|
||||
|
||||
#define MGP_MONO_PLAIN(Name, Payload) \
|
||||
void Mono_##Name(const Payload* payload) { MGPipeApply##Name(*payload); }
|
||||
|
||||
#define MGP_MONO_BLOB(Name, Payload) \
|
||||
void Mono_##Name(const Payload* payload, const void* blobBytes, Uint64) { \
|
||||
MGPipeApply##Name(*payload, blobBytes); \
|
||||
}
|
||||
|
||||
#define MGP_MONO_TAIL(Name, Payload, TailType) \
|
||||
void Mono_##Name(const Payload* payload, const void* varTail, Uint32) { \
|
||||
MGPipeApply##Name(*payload, static_cast<const TailType*>(varTail)); \
|
||||
}
|
||||
|
||||
// -- screen -------------------------------------------------------------------
|
||||
MGP_MONO_PLAIN(ResourceDestroy, MGPHandleOnly)
|
||||
MGP_MONO_PLAIN(UnmapPersistent, MGPHandleOnly)
|
||||
|
||||
// -- context, plain -----------------------------------------------------------
|
||||
MGP_MONO_PLAIN(BindRenderState, MGPBindRenderState)
|
||||
MGP_MONO_PLAIN(DeleteRenderState, MGPHandleOnly)
|
||||
MGP_MONO_PLAIN(BindVertexElements, MGPHandleOnly)
|
||||
MGP_MONO_PLAIN(DeleteVertexElements, MGPHandleOnly)
|
||||
MGP_MONO_PLAIN(DeleteSamplerState, MGPHandleOnly)
|
||||
MGP_MONO_PLAIN(CreateSamplerView, MGPSamplerView)
|
||||
MGP_MONO_PLAIN(DeleteSamplerView, MGPHandleOnly)
|
||||
MGP_MONO_PLAIN(BindShaderState, MGPHandleOnly)
|
||||
MGP_MONO_PLAIN(DeleteShaderState, MGPHandleOnly)
|
||||
MGP_MONO_PLAIN(SetDrawProgram, MGPHandleOnly)
|
||||
MGP_MONO_PLAIN(SetDispatchProgram, MGPHandleOnly)
|
||||
MGP_MONO_PLAIN(SetFramebufferState, MGPFramebufferState)
|
||||
MGP_MONO_PLAIN(SetIndexBuffer, MGPIndexBuffer)
|
||||
MGP_MONO_PLAIN(SetPixelPackState, MGPPixelPackState)
|
||||
MGP_MONO_PLAIN(SetPatchState, MGPPatchState)
|
||||
|
||||
// -- context, blob companion --------------------------------------------------
|
||||
MGP_MONO_BLOB(CreateRenderState, MGPRenderStateDesc)
|
||||
MGP_MONO_BLOB(CreateVertexElements, MGPVertexElements)
|
||||
MGP_MONO_BLOB(SetDynamicState, MGPDynamicState)
|
||||
MGP_MONO_BLOB(SetGlobalConstants, MGPGlobalConstants)
|
||||
MGP_MONO_BLOB(BufferSubDataResident, MGPSubData)
|
||||
|
||||
// -- context, variable tail ---------------------------------------------------
|
||||
MGP_MONO_TAIL(SetVertexBuffers, MGPVertexBuffers, MGPVertexBuffer)
|
||||
MGP_MONO_TAIL(SetSamplerViews, MGPSamplerViews, MGPBoundView)
|
||||
MGP_MONO_TAIL(BindSamplerStates, MGPSamplerStates, MGPipeHandle)
|
||||
MGP_MONO_TAIL(SetShaderImages, MGPShaderImages, MGPImageView)
|
||||
MGP_MONO_TAIL(SetVertexAttribDefaults, MGPVertexAttribDefaults, MGPAttribValue)
|
||||
|
||||
#undef MGP_MONO_PLAIN
|
||||
#undef MGP_MONO_BLOB
|
||||
#undef MGP_MONO_TAIL
|
||||
|
||||
// -- the rows that fit none of the three shapes --------------------------------
|
||||
|
||||
// create_sampler_state: the blob is a TYPED frontend struct, not bytes the applier
|
||||
// reads through a void*. The cast is the whole adapter, and it is exact: the client
|
||||
// stages sizeof(SamplerParameters) bytes of the same object (CONTRACT-P5 table 1
|
||||
// row 17, including its padding trap - the bytes staged must be the bytes a later
|
||||
// memcmp compares).
|
||||
void Mono_CreateSamplerState(const MGPSamplerDesc* payload, const void* blobBytes, Uint64) {
|
||||
MGPipeApplyCreateSamplerState(*payload,
|
||||
static_cast<const SamplerParameters*>(blobBytes));
|
||||
}
|
||||
|
||||
// set_residual_value_state: CONTRACT-P5 table 1 row 6, "the hardest row in the table".
|
||||
// The applier takes `const ResidualValueBlock&` - a frontend type - and
|
||||
// MGPResidualValueState is never instantiated on the live path. So the BLOCK IS THE
|
||||
// BLOB, in both arms, and the adapter copies it back out. Requiring exact equality
|
||||
// rather than ">=" is the decoder's rule too (PipeWireCodec.cpp): a size that only
|
||||
// ever ratchets down makes a short read silently lose CapabilityBits.
|
||||
void Mono_SetResidualValueState(const MGPResidualValueState*, const void* blobBytes,
|
||||
Uint64 blobByteCount) {
|
||||
if (blobByteCount != sizeof(ResidualValueBlock) || blobBytes == nullptr) {
|
||||
MGLOG_F("MGPipe: Fatal{ResidualBlockSize} - set_residual_value_state carries %llu "
|
||||
"bytes, the block is %llu",
|
||||
static_cast<unsigned long long>(blobByteCount),
|
||||
static_cast<unsigned long long>(sizeof(ResidualValueBlock)));
|
||||
std::abort();
|
||||
}
|
||||
ResidualValueBlock block{};
|
||||
std::memcpy(&block, blobBytes, sizeof(block));
|
||||
MGPipeApplySetResidualValueState(block);
|
||||
}
|
||||
|
||||
// resource_readback carries kReplySlot but its answer is COMPLETION, not a value: the
|
||||
// bytes go server -> client through SEG_EVENT's OnBufferWriteback (CONTRACT-P5 table 1
|
||||
// row 22), and the split decoder posts exactly kStatusOk with a zero-length payload.
|
||||
// The monolith arm says the same thing, so the two arms hand their caller the same
|
||||
// answer rather than one of them handing it nothing.
|
||||
void Mono_ResourceReadback(const MGPReadback* payload, MGPReplySlot* reply) {
|
||||
MGPipeApplyResourceReadback(*payload);
|
||||
MGPipePostReply(*reply, 0, 0);
|
||||
}
|
||||
|
||||
// The three acceptance rows that DO fit a generated signature. Each posts the
|
||||
// applier's own answer; none of them invents one.
|
||||
void Mono_ResourceCreate(const MGPResourceDesc* payload, MGPReplySlot* reply) {
|
||||
const Bool accepted = MGPipeApplyResourceCreate(*payload);
|
||||
MGPipePostReply(*reply, accepted ? 0 : 1, accepted ? 1u : 0u);
|
||||
}
|
||||
|
||||
void Mono_SetTextureParams(const MGPTextureParams* payload, MGPReplySlot* reply) {
|
||||
const Bool accepted = MGPipeApplySetTextureParams(*payload);
|
||||
MGPipePostReply(*reply, accepted ? 0 : 1, accepted ? 1u : 0u);
|
||||
}
|
||||
|
||||
void Mono_ResourceSubData(const MGPSubData* payload, const void* blobBytes, Uint64,
|
||||
const void* varTail, Uint32, MGPReplySlot* reply) {
|
||||
const Bool accepted = MGPipeApplyResourceSubData(
|
||||
*payload, blobBytes, static_cast<const MGPSubRegion*>(varTail));
|
||||
MGPipePostReply(*reply, accepted ? 0 : 1, accepted ? 1u : 0u);
|
||||
}
|
||||
|
||||
// -- the four escapes ----------------------------------------------------------
|
||||
|
||||
Bool Mono_Escape_ResourceRespecify(const MGPResourceDesc* desc, const void* initialBytes,
|
||||
const MGPRespecifiedLevel* level) {
|
||||
return MGPipeApplyResourceRespecify(*desc, initialBytes, level);
|
||||
}
|
||||
|
||||
void Mono_Escape_ResourceFlushRange(const MGPFlushRange* record, const void* bytes) {
|
||||
MGPipeApplyResourceFlushRange(*record, bytes);
|
||||
}
|
||||
|
||||
void* Mono_Escape_MapPersistent(const MGPHandleOnly* handle, Uint64 size,
|
||||
const void* seedBytes) {
|
||||
return MGPipeApplyMapPersistent(*handle, size, seedBytes);
|
||||
}
|
||||
|
||||
void Mono_Escape_CreateShaderState(const MGPProgramDesc* desc,
|
||||
const MG_State::GLState::LinkArtifacts* link,
|
||||
const MG_State::GLState::SpirvArtifacts* spirv) {
|
||||
MGPipeApplyCreateShaderState(*desc, link, spirv);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void MGPipeInstallMonolithTables() {
|
||||
gMGPipeScreen.ResourceCreate = &Mono_ResourceCreate;
|
||||
gMGPipeScreen.ResourceDestroy = &Mono_ResourceDestroy;
|
||||
gMGPipeScreen.UnmapPersistent = &Mono_UnmapPersistent;
|
||||
|
||||
gMGPipeContext.CreateRenderState = &Mono_CreateRenderState;
|
||||
gMGPipeContext.BindRenderState = &Mono_BindRenderState;
|
||||
gMGPipeContext.DeleteRenderState = &Mono_DeleteRenderState;
|
||||
gMGPipeContext.CreateVertexElements = &Mono_CreateVertexElements;
|
||||
gMGPipeContext.BindVertexElements = &Mono_BindVertexElements;
|
||||
gMGPipeContext.DeleteVertexElements = &Mono_DeleteVertexElements;
|
||||
gMGPipeContext.CreateSamplerState = &Mono_CreateSamplerState;
|
||||
gMGPipeContext.DeleteSamplerState = &Mono_DeleteSamplerState;
|
||||
gMGPipeContext.CreateSamplerView = &Mono_CreateSamplerView;
|
||||
gMGPipeContext.DeleteSamplerView = &Mono_DeleteSamplerView;
|
||||
gMGPipeContext.BindShaderState = &Mono_BindShaderState;
|
||||
gMGPipeContext.DeleteShaderState = &Mono_DeleteShaderState;
|
||||
gMGPipeContext.SetDrawProgram = &Mono_SetDrawProgram;
|
||||
gMGPipeContext.SetDispatchProgram = &Mono_SetDispatchProgram;
|
||||
gMGPipeContext.SetDynamicState = &Mono_SetDynamicState;
|
||||
gMGPipeContext.SetFramebufferState = &Mono_SetFramebufferState;
|
||||
gMGPipeContext.SetVertexBuffers = &Mono_SetVertexBuffers;
|
||||
gMGPipeContext.SetIndexBuffer = &Mono_SetIndexBuffer;
|
||||
gMGPipeContext.SetSamplerViews = &Mono_SetSamplerViews;
|
||||
gMGPipeContext.BindSamplerStates = &Mono_BindSamplerStates;
|
||||
gMGPipeContext.SetShaderImages = &Mono_SetShaderImages;
|
||||
gMGPipeContext.SetGlobalConstants = &Mono_SetGlobalConstants;
|
||||
gMGPipeContext.SetVertexAttribDefaults = &Mono_SetVertexAttribDefaults;
|
||||
gMGPipeContext.SetPixelPackState = &Mono_SetPixelPackState;
|
||||
gMGPipeContext.SetPatchState = &Mono_SetPatchState;
|
||||
gMGPipeContext.SetResidualValueState = &Mono_SetResidualValueState;
|
||||
gMGPipeContext.SetTextureParams = &Mono_SetTextureParams;
|
||||
gMGPipeContext.ResourceSubData = &Mono_ResourceSubData;
|
||||
gMGPipeContext.BufferSubDataResident = &Mono_BufferSubDataResident;
|
||||
gMGPipeContext.ResourceReadback = &Mono_ResourceReadback;
|
||||
|
||||
gMGPipeRouteEscapes.ResourceRespecify = &Mono_Escape_ResourceRespecify;
|
||||
gMGPipeRouteEscapes.ResourceFlushRange = &Mono_Escape_ResourceFlushRange;
|
||||
gMGPipeRouteEscapes.MapPersistent = &Mono_Escape_MapPersistent;
|
||||
gMGPipeRouteEscapes.CreateShaderState = &Mono_Escape_CreateShaderState;
|
||||
|
||||
// KEPT, not merely installed. The client arm overwrites the three tables above; these
|
||||
// three copies are what it forwards to when it finds itself on the server's own thread.
|
||||
g_monolithScreen = gMGPipeScreen;
|
||||
g_monolithContext = gMGPipeContext;
|
||||
g_monolithEscapes = gMGPipeRouteEscapes;
|
||||
|
||||
MGPipeNoteInstalledArm(MGPipeRouteArm::kMonolith);
|
||||
}
|
||||
|
||||
} // namespace MobileGL::MG_Pipe
|
||||
|
||||
#endif // MOBILEGL_PIPE_PUSH
|
||||
@@ -0,0 +1,400 @@
|
||||
// MobileGL - MobileGL/MG_Pipe/PipeRoute.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 -> WIRE ROUTING OF THE 37 MGPipeApply* ENTRY POINTS (P5, integrator ruling R-17).
|
||||
// Owner: package c1.
|
||||
//
|
||||
// WHAT WAS MISSING. `gMGPipeWireRecordApply` is the DECODE hook and it has existed since w1:
|
||||
// a record arrives, `PipeWireCodec` picks it apart and calls `MGPipeApply<Name>`. There was no
|
||||
// encode twin. Every `MG_Impl/Pipe` emitter called `MGPipeApply<Name>` DIRECTLY, so under
|
||||
// `MOBILEGL_TRANSPORT=inproc` every resource, CSO, texture and program record still executed
|
||||
// synchronously on the GL thread against a context v1 is moving to the apply thread - where
|
||||
// all sixteen `IsBackendContextCurrentOnThisThread()` and sixteen `CanTouchGLNow()` guards
|
||||
// answer false. `ClearThenReadPixels` might have survived that; `TriangleScenario` needs a VBO
|
||||
// and a program and could not.
|
||||
//
|
||||
// The two generated tables (`gMGPipeScreen`, `gMGPipeContext`, `generated/PipeTables.inc`) are
|
||||
// the boundary this repository already had, and they were never installed - every entry null,
|
||||
// which `MGPipe.h` correctly calls "precisely the pre-migration state". R-17 installs them.
|
||||
// This header is the frontend-facing half: the names `MG_Impl/Pipe` calls, and the mechanism
|
||||
// that gets a kReplySlot row's answer back to its call site.
|
||||
//
|
||||
// ---------------------------------------------------------------------------------------
|
||||
// THE SIZE OF R-17, MEASURED RATHER THAN ESTIMATED
|
||||
// ---------------------------------------------------------------------------------------
|
||||
//
|
||||
// R-17 costed this as "37 thin client emitters over EmitAndWait". The emitters are thin. The
|
||||
// ROUTING was not, and the reason is that the generated table signature is the P2-era monolith
|
||||
// interface: `void (*Name)(const Payload* payload, ...)`, async-with-handle, no return value,
|
||||
// and NO COMPANION POINTER. Of the 37 entry points, as the tables stood:
|
||||
//
|
||||
// 24 the row could carry every argument the call site passes today
|
||||
// 13 it could not
|
||||
//
|
||||
// The thirteen split three ways, and only one of the three is a generator question:
|
||||
//
|
||||
// (a) NINE carry a blob companion - `chunkBytes`, `blobBytes`, `parameters`, `bytes`,
|
||||
// `initialBytes` - which is CONTRACT-P5 table 1's "companion pointer today" column. A
|
||||
// kVarTail row already gains `const void* varTail, Uint32 varTailCount`; a kHasBlob row
|
||||
// gained nothing. THE GENERATOR CHANGE IS THAT ONE ASYMMETRY, and nothing else: five
|
||||
// lines in `Call.Signature` giving a kHasBlob row `const void* blobBytes, Uint64
|
||||
// blobByteCount`. Ten rows gained the pair (the nine plus `GetCaps`, which has no applier
|
||||
// entry point). `--check` up to date, `--self-test` 9/9 trips.
|
||||
//
|
||||
// (b) FOUR return a value the row cannot: `ResourceCreate`, `ResourceRespecify`,
|
||||
// `ResourceSubData` and `SetTextureParams` return Bool and `MapPersistent` returns void*.
|
||||
// These are exactly CONTRACT-P5 §2's reply-slot rows, and R-5 forbids re-deriving any of
|
||||
// them on the client. The answer therefore has to come back THROUGH the reply slot, which
|
||||
// is what `MGPipeTakeReply*` below is, and it is the part of R-17 that no amount of
|
||||
// "thin emitter" covers: `MGPReplySlot` is `{Uint64 Id;}` - an IDENTIFIER, not a value -
|
||||
// so a reader had to exist for the identifier to be worth minting.
|
||||
//
|
||||
// (c) FOUR CANNOT GO THROUGH A GENERATED ROW AT ALL, and each one is a contract ruling
|
||||
// rather than an oversight. They are the ESCAPES below. Saying "four" out loud is the
|
||||
// honest answer to "tell me if it is bigger than it looks": it is, by four rows and one
|
||||
// reply-reading mechanism.
|
||||
//
|
||||
// ---------------------------------------------------------------------------------------
|
||||
// THE REPLY MAILBOX, AND WHY ONE ENTRY IS THE RIGHT DEPTH
|
||||
// ---------------------------------------------------------------------------------------
|
||||
//
|
||||
// `MGPReplySlot` carries an id; the answer lands beside it. The mailbox is ONE ENTRY DEEP and
|
||||
// thread-local, and that is not a simplification - it is R-1's verb barrier stated as a data
|
||||
// structure. While the barrier holds, the in-flight depth is exactly one (`ReplySlot.h`:
|
||||
// "under R-1's verb barrier the in-flight depth is one"), so a second posting before the first
|
||||
// is taken is not a capacity problem, it is a barrier that has stopped working. It Fatals.
|
||||
//
|
||||
// THE MAILBOX HAS NO DEFAULT ANSWER AND THIS IS THE WHOLE POINT. `MGPipeTakeReplyBool` on a
|
||||
// slot nothing posted to is `Fatal{ReplyMissing, "<row>"}`, not `false` and emphatically not
|
||||
// `true`. "Always accept" is ID-39's 66 lost DirectVulkan uploads with a wire in between, and
|
||||
// "always refuse" is an emitter that re-sends for ever. A row that forgets to answer is a bug
|
||||
// that must be impossible to ship, so it is impossible to READ.
|
||||
//
|
||||
// WHEN THE BARRIER RETIRES (R-1 opens family by family) this becomes a real slot pool keyed on
|
||||
// seq, which is what `Transport::ReplySlot` already is on the wire side. The mailbox is the
|
||||
// frontend-side handle onto it and nothing else; it is deliberately NOT a second id space,
|
||||
// because the split arm stamps the record's own seq into `MGPReplySlot::Id` (R-3).
|
||||
|
||||
#pragma once
|
||||
#include <Includes.h>
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
|
||||
#include "MGPipe.h"
|
||||
#include "PipeApply.h"
|
||||
|
||||
namespace MobileGL::MG_Pipe {
|
||||
|
||||
// ---------------------------------------------------------------------------------
|
||||
// The reply mailbox
|
||||
// ---------------------------------------------------------------------------------
|
||||
|
||||
// Mints the id for one call's answer. Monolith's ids and the wire's seqs are separate
|
||||
// spaces on purpose: under split the emitter OVERWRITES this with the record's own seq
|
||||
// (R-3: "the reply slot id IS the record sequence number"), so the id a caller finally
|
||||
// reads is the wire's, and under monolith it is a local ticket that only has to be unique
|
||||
// against the one outstanding call the barrier permits.
|
||||
MGPReplySlot MGPipeMintReplySlot();
|
||||
|
||||
// Posts one answer. Called by whichever table is installed - the monolith adapter with
|
||||
// the applier's return value, the client emitter with the bytes the server put in
|
||||
// SEG_REPLY. `status` is Transport::ReplyStatus' value space (0 OK, 1 DECLINED, 2 ERROR),
|
||||
// restated as a plain Int32 so MG_Pipe does not have to see MG_Remote at all.
|
||||
void MGPipePostReply(const MGPReplySlot& slot, Int32 status, Uint64 value);
|
||||
|
||||
// Takes the answer for `slot`. Fatals - naming `row` - if nothing was posted, if what was
|
||||
// posted belongs to a different slot, or if the answer has already been taken.
|
||||
Bool MGPipeTakeReplyBool(const MGPReplySlot& slot, const char* row);
|
||||
void* MGPipeTakeReplyPointer(const MGPReplySlot& slot, const char* row);
|
||||
|
||||
// How many answers this thread has taken, and how many of those were DECLINED. Counted
|
||||
// rather than inferred, for R-8's reason one level out: "the client accepted everything"
|
||||
// and "the client never asked" are otherwise the same observation from outside.
|
||||
Uint64 MGPipeRepliesTaken();
|
||||
Uint64 MGPipeRepliesDeclined();
|
||||
|
||||
// ---------------------------------------------------------------------------------
|
||||
// The escapes: the four rows no generated signature can express
|
||||
// ---------------------------------------------------------------------------------
|
||||
//
|
||||
// A hand-written third table, installed and uninstalled by the same two functions as the
|
||||
// generated pair, so there is ONE mechanism and not two. Each row's signature is the
|
||||
// applier's own, because that is the shape the monolith arm has to reproduce byte for
|
||||
// byte (G2), and each row is here for a ruling that is written down:
|
||||
//
|
||||
// ResourceRespecify `initialBytes` has NO kHasBlob and R-13.3 forbids giving it one
|
||||
// ("initialBytes is always nullptr under split; initial content
|
||||
// arrives as ResourceSubData records immediately after this one").
|
||||
// So the row is correct to omit it AND monolith must still pass it.
|
||||
// ResourceFlushRange `bytes` likewise, by R-13.2, and for a sharper reason: a blobref
|
||||
// here would be "a second, forgeable way to say the same thing".
|
||||
// MapPersistent `size` and `seedBytes` have no carrier at all - `MGPHandleOnly` is
|
||||
// {Handle, Kind} - and the call returns void*. R-6 makes the split
|
||||
// answer a constant DECLINE, so the carrier is not needed; the
|
||||
// monolith arm needs both arguments.
|
||||
// CreateShaderState SEVEN blobrefs (`Spirv[6]` + `Reflection`) and TWO typed frontend
|
||||
// pointers. One `blobBytes` pair cannot express seven runs, and
|
||||
// serialising under monolith to un-serialise in the adapter would put
|
||||
// `EncodeProgramArtifacts` on the monolith path, which `PipeApply.h`
|
||||
// explicitly promises it is not ("zero serialisation cost on the
|
||||
// monolith path").
|
||||
//
|
||||
// OVERTURN CONDITIONS, one per row: give `MGPResourceDesc` and `MGPFlushRange` a blobref
|
||||
// (overturns R-13.2/R-13.3, and c0 owns `MGPipeTypes.h`); give `MGPHandleOnly` a size
|
||||
// (same owner) and P6 a real remote map; measure that a per-stage SPIR-V run beats one
|
||||
// archive, at which point `CreateShaderState` needs a multi-blob row rather than this one.
|
||||
struct MGPipeRouteEscapes {
|
||||
Bool (*ResourceRespecify)(const MGPResourceDesc* desc, const void* initialBytes,
|
||||
const MGPRespecifiedLevel* level);
|
||||
void (*ResourceFlushRange)(const MGPFlushRange* record, const void* bytes);
|
||||
void* (*MapPersistent)(const MGPHandleOnly* handle, Uint64 size, const void* seedBytes);
|
||||
void (*CreateShaderState)(const MGPProgramDesc* desc,
|
||||
const MG_State::GLState::LinkArtifacts* link,
|
||||
const MG_State::GLState::SpirvArtifacts* spirv);
|
||||
};
|
||||
inline MGPipeRouteEscapes gMGPipeRouteEscapes{};
|
||||
|
||||
// ---------------------------------------------------------------------------------
|
||||
// The two install functions
|
||||
// ---------------------------------------------------------------------------------
|
||||
|
||||
// Installs the monolith adapters into all three tables. Idempotent.
|
||||
void MGPipeInstallMonolithTables();
|
||||
|
||||
// The monolith adapters, kept beside the installed tables rather than only written into
|
||||
// them. THE SPLIT ARM NEEDS THEM AT RUNTIME, and the reason is table 3's role split made
|
||||
// concrete: `gMGPipeScreen` / `gMGPipeContext` are PROCESS globals, and under `inproc` the
|
||||
// server role lives in the same process on the apply thread. When that thread runs the
|
||||
// server's own backend - the EGL bring-up, InitCapabilities, or the applier itself - it
|
||||
// reaches the very same `MG_Impl/Pipe` emitters the client does, and a wire emitter there
|
||||
// publishes a record and then waits for the apply thread to apply it. That thread IS the
|
||||
// apply thread, so it waits for itself: `Fatal{BarrierTimeout, "ResourceRespecify"}`,
|
||||
// logged by `mgl-srv-apply`, thirty seconds after bring-up starts.
|
||||
//
|
||||
// So the client emitters ask "am I the server role right now?" and, if so, run the
|
||||
// monolith adapter - which is exactly what `PipeWireCodec` already does on the decode side
|
||||
// by calling `MGPipeApply*` directly. The predicate is v1's `ServerLoop::OnApplyThread()`;
|
||||
// these three accessors are what makes the other half reachable.
|
||||
const MGPipeScreen& MGPipeMonolithScreen();
|
||||
const MGPipeContext& MGPipeMonolithContext();
|
||||
const MGPipeRouteEscapes& MGPipeMonolithEscapes();
|
||||
|
||||
namespace detail {
|
||||
// THE INSTALL THAT CANNOT BE LINKED AWAY, and the first version of this WAS.
|
||||
//
|
||||
// It began as a static initialiser inside PipeRoute.cpp, on the reasoning that
|
||||
// `gMGPipeScreen` and `gMGPipeContext` are inline variables with CONSTANT (zero)
|
||||
// initialisation - sequenced before every dynamic initialiser - so an installer in
|
||||
// dynamic init necessarily beats any GL entry point. That reasoning is correct and the
|
||||
// mechanism still failed, because it assumed PipeRoute.o would be in the link at all:
|
||||
// every MG_Test target is its OWN binary over a static archive, `CsoCache.h` reaches
|
||||
// the table through an inline thunk and names no symbol from PipeRoute.o, so the
|
||||
// linker dropped the object, the initialiser never ran, and five CsoCacheTest cases
|
||||
// took a null function pointer. The shared library linked it and was fine, which is
|
||||
// exactly the shape that ships.
|
||||
//
|
||||
// AN INLINE VARIABLE FIXES BOTH HALVES AT ONCE. Its initialiser is emitted as a COMDAT
|
||||
// in every translation unit that includes this header, so any binary that can call a
|
||||
// thunk has one copy of it; and naming `MGPipeInstallMonolithTables` from a header the
|
||||
// call sites already include is an undefined reference that forces the archive member
|
||||
// into the link. There is still exactly one install, and it still happens before main.
|
||||
inline const Bool gMonolithTablesInstalled = (MGPipeInstallMonolithTables(), true);
|
||||
} // namespace detail
|
||||
|
||||
// True once the tables hold something. Exposed for the gates only: a probe that armed
|
||||
// against an un-installed table would be arming against the pre-migration state.
|
||||
Bool MGPipeTablesAreInstalled();
|
||||
|
||||
// Which arm is installed, so a case can assert the arm it thinks it is testing rather than
|
||||
// trusting `MOBILEGL_TRANSPORT`. `MG_Config::Transport` says what was ASKED for; this says
|
||||
// what the table actually does.
|
||||
enum class MGPipeRouteArm : Uint8 { kNone, kMonolith, kClientWire };
|
||||
MGPipeRouteArm MGPipeInstalledArm();
|
||||
void MGPipeNoteInstalledArm(MGPipeRouteArm arm);
|
||||
|
||||
// ---------------------------------------------------------------------------------
|
||||
// The call-site names: MGPipeRoute<Name> for each of the thirty-seven
|
||||
// ---------------------------------------------------------------------------------
|
||||
//
|
||||
// EVERY ONE TAKES THE APPLIER'S OWN SIGNATURE, so converting a call site is a rename and
|
||||
// nothing else. That is not tidiness, it is the G1 lesson from round 1 stated as a rule:
|
||||
// rewriting `if (const auto f = TABLE.GL.Slot)` into a Bool-valued macro is semantically
|
||||
// identical and generates DIFFERENT code (-144 bytes, two symbols resized). A wrapper that
|
||||
// took `&expr` would force every site holding a temporary - `HandleOnly(handle)`,
|
||||
// `BufferHandleOnly(handle)` - to grow a named local, which is a second expression shape
|
||||
// change at forty sites. A reference parameter keeps the call site's text identical but
|
||||
// for the name.
|
||||
//
|
||||
// THREE OF THE THIRTY-SEVEN TAKE ONE ARGUMENT MORE THAN THEIR APPLIER, and the extra is
|
||||
// always the same thing: A BYTE COUNT THE RECORD DOES NOT DECLARE. CONTRACT-P5 table 1
|
||||
// rule A requires every kHasBlob row to declare its length under split; five rows already
|
||||
// do and the wrapper reads it back off the record, three do not and their call site is the
|
||||
// only place the number exists:
|
||||
// SetGlobalConstants declares 0 (ProgramEmit.h); the length is GetUBOSize()
|
||||
// ResourceSubData buffer half declares real, TEXTURE half declares 0 on the
|
||||
// grounds that the count is "the server's to compute" - which
|
||||
// table 1 row 7 says "cannot be a bounds check". The level
|
||||
// shadow's byte size is in scope at that call site.
|
||||
// BufferSubDataResident the application's staging store, sized by the caller
|
||||
// A defaulted argument was rejected: a default would let a site that has the number forget
|
||||
// to pass it and still compile, and the failure would be a silently short upload.
|
||||
|
||||
// ---- screen ------------------------------------------------------------------------
|
||||
inline Bool MGPipeRouteResourceCreate(const MGPResourceDesc& desc) {
|
||||
MGPReplySlot reply = MGPipeMintReplySlot();
|
||||
MGP_ResourceCreate(&desc, &reply);
|
||||
return MGPipeTakeReplyBool(reply, "resource_create");
|
||||
}
|
||||
inline Bool MGPipeRouteResourceRespecify(const MGPResourceDesc& desc, const void* initialBytes,
|
||||
const MGPRespecifiedLevel* level = nullptr) {
|
||||
return gMGPipeRouteEscapes.ResourceRespecify(&desc, initialBytes, level);
|
||||
}
|
||||
inline void MGPipeRouteResourceDestroy(const MGPHandleOnly& handle) {
|
||||
MGP_ResourceDestroy(&handle);
|
||||
}
|
||||
inline void* MGPipeRouteMapPersistent(const MGPHandleOnly& handle, Uint64 size,
|
||||
const void* seedBytes) {
|
||||
return gMGPipeRouteEscapes.MapPersistent(&handle, size, seedBytes);
|
||||
}
|
||||
inline void MGPipeRouteUnmapPersistent(const MGPHandleOnly& handle) {
|
||||
MGP_UnmapPersistent(&handle);
|
||||
}
|
||||
|
||||
// ---- resources ---------------------------------------------------------------------
|
||||
inline Bool MGPipeRouteResourceSubData(const MGPSubData& record, const void* bytes,
|
||||
Uint64 byteCount,
|
||||
const MGPSubRegion* regions = nullptr) {
|
||||
MGPReplySlot reply = MGPipeMintReplySlot();
|
||||
MGP_ResourceSubData(&record, bytes, byteCount, regions, record.RegionCount, &reply);
|
||||
return MGPipeTakeReplyBool(reply, "resource_subdata");
|
||||
}
|
||||
inline void MGPipeRouteBufferSubDataResident(const MGPSubData& record, const void* bytes,
|
||||
Uint64 byteCount) {
|
||||
MGP_BufferSubDataResident(&record, bytes, byteCount);
|
||||
}
|
||||
inline void MGPipeRouteResourceFlushRange(const MGPFlushRange& record, const void* bytes) {
|
||||
gMGPipeRouteEscapes.ResourceFlushRange(&record, bytes);
|
||||
}
|
||||
inline void MGPipeRouteResourceReadback(const MGPReadback& record) {
|
||||
MGPReplySlot reply = MGPipeMintReplySlot();
|
||||
MGP_ResourceReadback(&record, &reply);
|
||||
// The answer is COMPLETION, not a value: the bytes come back through SEG_EVENT's
|
||||
// OnBufferWriteback (table 1 row 22). It is still TAKEN, because an untaken mailbox
|
||||
// entry is what the next row's ReplyOverrun Fatal is looking for.
|
||||
(void)MGPipeTakeReplyBool(reply, "resource_readback");
|
||||
}
|
||||
|
||||
// ---- render state ------------------------------------------------------------------
|
||||
inline void MGPipeRouteCreateRenderState(const MGPRenderStateDesc& desc,
|
||||
const void* chunkBytes) {
|
||||
MGP_CreateRenderState(&desc, chunkBytes, desc.Blob.Size);
|
||||
}
|
||||
inline void MGPipeRouteBindRenderState(const MGPBindRenderState& bind) {
|
||||
MGP_BindRenderState(&bind);
|
||||
}
|
||||
inline void MGPipeRouteDeleteRenderState(const MGPHandleOnly& handle) {
|
||||
MGP_DeleteRenderState(&handle);
|
||||
}
|
||||
inline void MGPipeRouteSetDynamicState(const MGPDynamicState& dyn, const void* chunkBytes) {
|
||||
MGP_SetDynamicState(&dyn, chunkBytes, dyn.Blob.Size);
|
||||
}
|
||||
inline void MGPipeRouteSetPixelPackState(const MGPPixelPackState& pack) {
|
||||
MGP_SetPixelPackState(&pack);
|
||||
}
|
||||
inline void MGPipeRouteSetPatchState(const MGPPatchState& patch) { MGP_SetPatchState(&patch); }
|
||||
inline void MGPipeRouteSetVertexAttribDefaults(const MGPVertexAttribDefaults& hdr,
|
||||
const MGPAttribValue* tail) {
|
||||
MGP_SetVertexAttribDefaults(&hdr, tail, hdr.Count);
|
||||
}
|
||||
// The block IS the blob (table 1 row 6) and its size is a header constant both sides
|
||||
// read, so no call site has to know it.
|
||||
inline void MGPipeRouteSetResidualValueState(const ResidualValueBlock& block) {
|
||||
MGPResidualValueState record{};
|
||||
record.Version = 0;
|
||||
MGP_SetResidualValueState(&record, &block, sizeof(ResidualValueBlock));
|
||||
}
|
||||
|
||||
// ---- vertex input ------------------------------------------------------------------
|
||||
inline void MGPipeRouteCreateVertexElements(const MGPVertexElements& desc,
|
||||
const void* blobBytes) {
|
||||
MGP_CreateVertexElements(&desc, blobBytes, desc.Blob.Size);
|
||||
}
|
||||
inline void MGPipeRouteBindVertexElements(const MGPHandleOnly& handle) {
|
||||
MGP_BindVertexElements(&handle);
|
||||
}
|
||||
inline void MGPipeRouteDeleteVertexElements(const MGPHandleOnly& handle) {
|
||||
MGP_DeleteVertexElements(&handle);
|
||||
}
|
||||
inline void MGPipeRouteSetVertexBuffers(const MGPVertexBuffers& hdr,
|
||||
const MGPVertexBuffer* tail) {
|
||||
MGP_SetVertexBuffers(&hdr, tail, hdr.Count);
|
||||
}
|
||||
inline void MGPipeRouteSetIndexBuffer(const MGPIndexBuffer& record) {
|
||||
MGP_SetIndexBuffer(&record);
|
||||
}
|
||||
|
||||
// ---- framebuffer, samplers, images -------------------------------------------------
|
||||
inline void MGPipeRouteSetFramebufferState(const MGPFramebufferState& state) {
|
||||
MGP_SetFramebufferState(&state);
|
||||
}
|
||||
// The parameters row declares Size 0 today and table 1 row 17 puts it under rule A, so
|
||||
// the length is stated here once, from the type the applier stores by value.
|
||||
inline void MGPipeRouteCreateSamplerState(const MGPSamplerDesc& desc,
|
||||
const SamplerParameters* parameters) {
|
||||
MGP_CreateSamplerState(&desc, parameters, sizeof(SamplerParameters));
|
||||
}
|
||||
inline void MGPipeRouteDeleteSamplerState(const MGPHandleOnly& handle) {
|
||||
MGP_DeleteSamplerState(&handle);
|
||||
}
|
||||
inline void MGPipeRouteCreateSamplerView(const MGPSamplerView& view) {
|
||||
MGP_CreateSamplerView(&view);
|
||||
}
|
||||
inline void MGPipeRouteDeleteSamplerView(const MGPHandleOnly& handle) {
|
||||
MGP_DeleteSamplerView(&handle);
|
||||
}
|
||||
inline Bool MGPipeRouteSetTextureParams(const MGPTextureParams& params) {
|
||||
MGPReplySlot reply = MGPipeMintReplySlot();
|
||||
MGP_SetTextureParams(¶ms, &reply);
|
||||
return MGPipeTakeReplyBool(reply, "set_texture_params");
|
||||
}
|
||||
inline void MGPipeRouteSetSamplerViews(const MGPSamplerViews& hdr, const MGPBoundView* tail) {
|
||||
MGP_SetSamplerViews(&hdr, tail, hdr.Count);
|
||||
}
|
||||
inline void MGPipeRouteBindSamplerStates(const MGPSamplerStates& hdr, const MGPipeHandle* tail) {
|
||||
MGP_BindSamplerStates(&hdr, tail, hdr.Count);
|
||||
}
|
||||
inline void MGPipeRouteSetShaderImages(const MGPShaderImages& hdr, const MGPImageView* tail) {
|
||||
MGP_SetShaderImages(&hdr, tail, hdr.Count);
|
||||
}
|
||||
|
||||
// ---- programs ----------------------------------------------------------------------
|
||||
inline void MGPipeRouteCreateShaderState(const MGPProgramDesc& desc,
|
||||
const MG_State::GLState::LinkArtifacts* link,
|
||||
const MG_State::GLState::SpirvArtifacts* spirv) {
|
||||
gMGPipeRouteEscapes.CreateShaderState(&desc, link, spirv);
|
||||
}
|
||||
inline void MGPipeRouteBindShaderState(const MGPHandleOnly& handle) {
|
||||
MGP_BindShaderState(&handle);
|
||||
}
|
||||
inline void MGPipeRouteDeleteShaderState(const MGPHandleOnly& handle) {
|
||||
MGP_DeleteShaderState(&handle);
|
||||
}
|
||||
inline void MGPipeRouteSetDrawProgram(const MGPHandleOnly& handle) {
|
||||
MGP_SetDrawProgram(&handle);
|
||||
}
|
||||
inline void MGPipeRouteSetDispatchProgram(const MGPHandleOnly& handle) {
|
||||
MGP_SetDispatchProgram(&handle);
|
||||
}
|
||||
inline void MGPipeRouteSetGlobalConstants(const MGPGlobalConstants& record, const void* bytes,
|
||||
Uint64 byteCount) {
|
||||
MGP_SetGlobalConstants(&record, bytes, byteCount);
|
||||
}
|
||||
|
||||
} // namespace MobileGL::MG_Pipe
|
||||
|
||||
#endif // MOBILEGL_PIPE_PUSH
|
||||
@@ -15,7 +15,7 @@
|
||||
// share group: 11 calls. A null entry means the backend does not implement this
|
||||
// call and the frontend keeps its own path (plan B section 4.1).
|
||||
struct MGPipeScreen {
|
||||
void (*GetCaps)(const MGPCaps* payload, MGPReplySlot* reply);
|
||||
void (*GetCaps)(const MGPCaps* payload, const void* blobBytes, Uint64 blobByteCount, MGPReplySlot* reply);
|
||||
void (*ResourceCreate)(const MGPResourceDesc* payload, MGPReplySlot* reply);
|
||||
void (*ResourceRespecify)(const MGPResourceDesc* payload, MGPReplySlot* reply);
|
||||
void (*ResourceDestroy)(const MGPHandleOnly* payload);
|
||||
@@ -37,20 +37,20 @@ struct MGPipeContext {
|
||||
void (*QueryAvailable)(const MGPHandleOnly* payload, MGPReplySlot* reply);
|
||||
void (*QueryResult)(const MGPQueryResultRequest* payload, MGPReplySlot* reply);
|
||||
void (*QueryDestroy)(const MGPHandleOnly* payload);
|
||||
void (*CreateRenderState)(const MGPRenderStateDesc* payload);
|
||||
void (*CreateRenderState)(const MGPRenderStateDesc* payload, const void* blobBytes, Uint64 blobByteCount);
|
||||
void (*BindRenderState)(const MGPBindRenderState* payload);
|
||||
void (*DeleteRenderState)(const MGPHandleOnly* payload);
|
||||
void (*CreateVertexElements)(const MGPVertexElements* payload);
|
||||
void (*CreateVertexElements)(const MGPVertexElements* payload, const void* blobBytes, Uint64 blobByteCount);
|
||||
void (*BindVertexElements)(const MGPHandleOnly* payload);
|
||||
void (*DeleteVertexElements)(const MGPHandleOnly* payload);
|
||||
void (*CreateSamplerState)(const MGPSamplerDesc* payload);
|
||||
void (*CreateSamplerState)(const MGPSamplerDesc* payload, const void* blobBytes, Uint64 blobByteCount);
|
||||
void (*DeleteSamplerState)(const MGPHandleOnly* payload);
|
||||
void (*CreateSamplerView)(const MGPSamplerView* payload);
|
||||
void (*DeleteSamplerView)(const MGPHandleOnly* payload);
|
||||
void (*CreateShaderState)(const MGPProgramDesc* payload);
|
||||
void (*CreateShaderState)(const MGPProgramDesc* payload, const void* blobBytes, Uint64 blobByteCount);
|
||||
void (*BindShaderState)(const MGPHandleOnly* payload);
|
||||
void (*DeleteShaderState)(const MGPHandleOnly* payload);
|
||||
void (*SetDynamicState)(const MGPDynamicState* payload);
|
||||
void (*SetDynamicState)(const MGPDynamicState* payload, const void* blobBytes, Uint64 blobByteCount);
|
||||
void (*SetFramebufferState)(const MGPFramebufferState* payload);
|
||||
void (*SetVertexBuffers)(const MGPVertexBuffers* payload, const void* varTail, Uint32 varTailCount);
|
||||
void (*SetIndexBuffer)(const MGPIndexBuffer* payload);
|
||||
@@ -60,16 +60,16 @@ struct MGPipeContext {
|
||||
void (*SetShaderImages)(const MGPShaderImages* payload, const void* varTail, Uint32 varTailCount);
|
||||
void (*SetShaderBuffers)(const MGPShaderBuffers* payload, const void* varTail, Uint32 varTailCount);
|
||||
void (*SetStreamOutputTargets)(const MGPStreamOutputTargets* payload, const void* varTail, Uint32 varTailCount);
|
||||
void (*SetGlobalConstants)(const MGPGlobalConstants* payload);
|
||||
void (*SetGlobalConstants)(const MGPGlobalConstants* payload, const void* blobBytes, Uint64 blobByteCount);
|
||||
void (*SetVertexAttribDefaults)(const MGPVertexAttribDefaults* payload, const void* varTail, Uint32 varTailCount);
|
||||
void (*SetPixelPackState)(const MGPPixelPackState* payload);
|
||||
void (*SetPatchState)(const MGPPatchState* payload);
|
||||
void (*SetDrawProgram)(const MGPHandleOnly* payload);
|
||||
void (*SetDispatchProgram)(const MGPHandleOnly* payload);
|
||||
void (*SetResidualValueState)(const MGPResidualValueState* payload);
|
||||
void (*SetResidualValueState)(const MGPResidualValueState* payload, const void* blobBytes, Uint64 blobByteCount);
|
||||
void (*SetTextureParams)(const MGPTextureParams* payload, MGPReplySlot* reply);
|
||||
void (*ResourceSubData)(const MGPSubData* payload, const void* varTail, Uint32 varTailCount, MGPReplySlot* reply);
|
||||
void (*BufferSubDataResident)(const MGPSubData* payload);
|
||||
void (*ResourceSubData)(const MGPSubData* payload, const void* blobBytes, Uint64 blobByteCount, const void* varTail, Uint32 varTailCount, MGPReplySlot* reply);
|
||||
void (*BufferSubDataResident)(const MGPSubData* payload, const void* blobBytes, Uint64 blobByteCount);
|
||||
void (*ResourceSubDataComplete)(const MGPSubDataComplete* payload);
|
||||
void (*ResourceFlushRange)(const MGPFlushRange* payload);
|
||||
void (*ResourceReadback)(const MGPReadback* payload, MGPReplySlot* reply);
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
// unimplemented (null) entry is the caller's business to check, exactly as it is
|
||||
// with the table this replaces.
|
||||
|
||||
inline void MGP_GetCaps(const MGPCaps* payload, MGPReplySlot* reply) {
|
||||
gMGPipeScreen.GetCaps(payload, reply);
|
||||
inline void MGP_GetCaps(const MGPCaps* payload, const void* blobBytes, Uint64 blobByteCount, MGPReplySlot* reply) {
|
||||
gMGPipeScreen.GetCaps(payload, blobBytes, blobByteCount, reply);
|
||||
}
|
||||
|
||||
inline void MGP_ResourceCreate(const MGPResourceDesc* payload, MGPReplySlot* reply) {
|
||||
@@ -81,8 +81,8 @@ inline void MGP_QueryDestroy(const MGPHandleOnly* payload) {
|
||||
gMGPipeContext.QueryDestroy(payload);
|
||||
}
|
||||
|
||||
inline void MGP_CreateRenderState(const MGPRenderStateDesc* payload) {
|
||||
gMGPipeContext.CreateRenderState(payload);
|
||||
inline void MGP_CreateRenderState(const MGPRenderStateDesc* payload, const void* blobBytes, Uint64 blobByteCount) {
|
||||
gMGPipeContext.CreateRenderState(payload, blobBytes, blobByteCount);
|
||||
}
|
||||
|
||||
inline void MGP_BindRenderState(const MGPBindRenderState* payload) {
|
||||
@@ -93,8 +93,8 @@ inline void MGP_DeleteRenderState(const MGPHandleOnly* payload) {
|
||||
gMGPipeContext.DeleteRenderState(payload);
|
||||
}
|
||||
|
||||
inline void MGP_CreateVertexElements(const MGPVertexElements* payload) {
|
||||
gMGPipeContext.CreateVertexElements(payload);
|
||||
inline void MGP_CreateVertexElements(const MGPVertexElements* payload, const void* blobBytes, Uint64 blobByteCount) {
|
||||
gMGPipeContext.CreateVertexElements(payload, blobBytes, blobByteCount);
|
||||
}
|
||||
|
||||
inline void MGP_BindVertexElements(const MGPHandleOnly* payload) {
|
||||
@@ -105,8 +105,8 @@ inline void MGP_DeleteVertexElements(const MGPHandleOnly* payload) {
|
||||
gMGPipeContext.DeleteVertexElements(payload);
|
||||
}
|
||||
|
||||
inline void MGP_CreateSamplerState(const MGPSamplerDesc* payload) {
|
||||
gMGPipeContext.CreateSamplerState(payload);
|
||||
inline void MGP_CreateSamplerState(const MGPSamplerDesc* payload, const void* blobBytes, Uint64 blobByteCount) {
|
||||
gMGPipeContext.CreateSamplerState(payload, blobBytes, blobByteCount);
|
||||
}
|
||||
|
||||
inline void MGP_DeleteSamplerState(const MGPHandleOnly* payload) {
|
||||
@@ -121,8 +121,8 @@ inline void MGP_DeleteSamplerView(const MGPHandleOnly* payload) {
|
||||
gMGPipeContext.DeleteSamplerView(payload);
|
||||
}
|
||||
|
||||
inline void MGP_CreateShaderState(const MGPProgramDesc* payload) {
|
||||
gMGPipeContext.CreateShaderState(payload);
|
||||
inline void MGP_CreateShaderState(const MGPProgramDesc* payload, const void* blobBytes, Uint64 blobByteCount) {
|
||||
gMGPipeContext.CreateShaderState(payload, blobBytes, blobByteCount);
|
||||
}
|
||||
|
||||
inline void MGP_BindShaderState(const MGPHandleOnly* payload) {
|
||||
@@ -133,8 +133,8 @@ inline void MGP_DeleteShaderState(const MGPHandleOnly* payload) {
|
||||
gMGPipeContext.DeleteShaderState(payload);
|
||||
}
|
||||
|
||||
inline void MGP_SetDynamicState(const MGPDynamicState* payload) {
|
||||
gMGPipeContext.SetDynamicState(payload);
|
||||
inline void MGP_SetDynamicState(const MGPDynamicState* payload, const void* blobBytes, Uint64 blobByteCount) {
|
||||
gMGPipeContext.SetDynamicState(payload, blobBytes, blobByteCount);
|
||||
}
|
||||
|
||||
inline void MGP_SetFramebufferState(const MGPFramebufferState* payload) {
|
||||
@@ -173,8 +173,8 @@ inline void MGP_SetStreamOutputTargets(const MGPStreamOutputTargets* payload, co
|
||||
gMGPipeContext.SetStreamOutputTargets(payload, varTail, varTailCount);
|
||||
}
|
||||
|
||||
inline void MGP_SetGlobalConstants(const MGPGlobalConstants* payload) {
|
||||
gMGPipeContext.SetGlobalConstants(payload);
|
||||
inline void MGP_SetGlobalConstants(const MGPGlobalConstants* payload, const void* blobBytes, Uint64 blobByteCount) {
|
||||
gMGPipeContext.SetGlobalConstants(payload, blobBytes, blobByteCount);
|
||||
}
|
||||
|
||||
inline void MGP_SetVertexAttribDefaults(const MGPVertexAttribDefaults* payload, const void* varTail, Uint32 varTailCount) {
|
||||
@@ -197,20 +197,20 @@ inline void MGP_SetDispatchProgram(const MGPHandleOnly* payload) {
|
||||
gMGPipeContext.SetDispatchProgram(payload);
|
||||
}
|
||||
|
||||
inline void MGP_SetResidualValueState(const MGPResidualValueState* payload) {
|
||||
gMGPipeContext.SetResidualValueState(payload);
|
||||
inline void MGP_SetResidualValueState(const MGPResidualValueState* payload, const void* blobBytes, Uint64 blobByteCount) {
|
||||
gMGPipeContext.SetResidualValueState(payload, blobBytes, blobByteCount);
|
||||
}
|
||||
|
||||
inline void MGP_SetTextureParams(const MGPTextureParams* payload, MGPReplySlot* reply) {
|
||||
gMGPipeContext.SetTextureParams(payload, reply);
|
||||
}
|
||||
|
||||
inline void MGP_ResourceSubData(const MGPSubData* payload, const void* varTail, Uint32 varTailCount, MGPReplySlot* reply) {
|
||||
gMGPipeContext.ResourceSubData(payload, varTail, varTailCount, reply);
|
||||
inline void MGP_ResourceSubData(const MGPSubData* payload, const void* blobBytes, Uint64 blobByteCount, const void* varTail, Uint32 varTailCount, MGPReplySlot* reply) {
|
||||
gMGPipeContext.ResourceSubData(payload, blobBytes, blobByteCount, varTail, varTailCount, reply);
|
||||
}
|
||||
|
||||
inline void MGP_BufferSubDataResident(const MGPSubData* payload) {
|
||||
gMGPipeContext.BufferSubDataResident(payload);
|
||||
inline void MGP_BufferSubDataResident(const MGPSubData* payload, const void* blobBytes, Uint64 blobByteCount) {
|
||||
gMGPipeContext.BufferSubDataResident(payload, blobBytes, blobByteCount);
|
||||
}
|
||||
|
||||
inline void MGP_ResourceSubDataComplete(const MGPSubDataComplete* payload) {
|
||||
|
||||
@@ -0,0 +1,282 @@
|
||||
// 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 seam ------------------------------------------------------------------
|
||||
//
|
||||
// THE NINE EGL VIRTUALS CALL v1's TWELVE FORWARDERS AND NOTHING ELSE. c1 round 1 built
|
||||
// its own trampolines over ServerLoop::RunOnApplyThread and ServerLoop::Backend(),
|
||||
// which ran the right driver call on the right thread and was still wrong, because
|
||||
// three of the twelve do MORE than forward:
|
||||
//
|
||||
// ServerMakeEGLCurrent re-publishes the caps snapshot after the server's own
|
||||
// InitCapabilities has run (R-12 arm (a))
|
||||
// ServerInitCapabilities the same, on the explicit path
|
||||
// ServerSetWindowHandle hands the surface to the server's backend
|
||||
//
|
||||
// Calling `Backend()->MakeEGLCurrent(...)` skips the republish, so the client's mirror
|
||||
// keeps the snapshot Accept() sent BEFORE any context existed - every limit, every
|
||||
// advertised extension and the compile-env fingerprint read off an empty backend, with
|
||||
// InitCapabilities below happily reporting success because a snapshot did arrive once.
|
||||
// That is the failure this seam exists to make impossible: there is no second route to
|
||||
// the server's EGL, so there is no route that can skip what the forwarder does.
|
||||
//
|
||||
// The forwarders block on mgl-srv-apply themselves and run INLINE when the caller is
|
||||
// already on that thread, so this file no longer needs RunOnApplyThread, a per-call
|
||||
// args struct, or a null-backend check of its own - ServerBackendOrNull() inside each
|
||||
// forwarder is the one place that answers "the bring-up did not complete", and it
|
||||
// answers `false` rather than crashing or succeeding locally.
|
||||
|
||||
// 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;
|
||||
}
|
||||
// ASK THE SERVER RATHER THAN ASSUMING ServerMakeEGLCurrent HAS ALREADY ASKED IT. The
|
||||
// comment above says "by this point MakeEGLCurrent has already run the SERVER's
|
||||
// MakeEGLCurrent", and that is true on the make-current path - but the base class
|
||||
// reaches InitCapabilities lazily, once per SURFACE lifetime, and a surface can be
|
||||
// replaced without a new make-current. ServerInitCapabilities re-publishes the
|
||||
// snapshot the same way, so asking twice costs one snapshot and never asking costs
|
||||
// every limit in the mirror. It is idempotent on the server's side.
|
||||
if (!Server::ServerInitCapabilities()) {
|
||||
MGLOG_E("MG_Remote client: the server's InitCapabilities failed, so there is no "
|
||||
"snapshot to adopt and going current would read default limits");
|
||||
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) {
|
||||
if (!Server::ServerInitializeEGLDisplay(dpy, major, minor)) return false;
|
||||
return MG_Backend::BackendObject::InitializeEGLDisplay(dpy, major, minor);
|
||||
}
|
||||
|
||||
Bool BackendObject_Remote::CreateEGLWindowSurface(EGLSurface surface,
|
||||
const MG_Backend::WindowHandle& handle) {
|
||||
// The handle first: the server's backend has to know which window it is about to make
|
||||
// a surface for, and ServerSetWindowHandle is the only way to tell it.
|
||||
Server::ServerSetWindowHandle(handle);
|
||||
if (!Server::ServerCreateEGLWindowSurface(surface, handle)) return false;
|
||||
return MG_Backend::BackendObject::CreateEGLWindowSurface(surface, handle);
|
||||
}
|
||||
|
||||
Bool BackendObject_Remote::ResizeEGLWindowSurface(EGLSurface surface, Uint32 width, Uint32 height) {
|
||||
if (!Server::ServerResizeEGLWindowSurface(surface, width, height)) return false;
|
||||
return MG_Backend::BackendObject::ResizeEGLWindowSurface(surface, width, height);
|
||||
}
|
||||
|
||||
Bool BackendObject_Remote::CreateEGLPbufferSurface(EGLSurface surface, EGLint width, EGLint height) {
|
||||
if (!Server::ServerCreateEGLPbufferSurface(surface, width, height)) return false;
|
||||
return MG_Backend::BackendObject::CreateEGLPbufferSurface(surface, width, height);
|
||||
}
|
||||
|
||||
Bool BackendObject_Remote::MakeEGLCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read,
|
||||
EGLContext ctx) {
|
||||
// FORWARD FIRST, THEN RUN THE BASE. The server has to own the context before the 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 - and ServerMakeEGLCurrent is what publishes it.
|
||||
if (!Server::ServerMakeEGLCurrent(dpy, draw, read, ctx)) return false;
|
||||
if (!MG_Backend::BackendObject::MakeEGLCurrent(dpy, draw, read, ctx)) return false;
|
||||
|
||||
// R-12 ARM (a) ON EVERY SUCCESSFUL MAKE-CURRENT (codex 12). ServerMakeEGLCurrent above
|
||||
// republishes the caps snapshot on every call (ServerLoop.cpp:613-628), but the base
|
||||
// class only runs InitCapabilities - the one place that pumps and refreshes - on the
|
||||
// FIRST make-current per surface (BackendObject.cpp:341-347). A repeated make-current
|
||||
// onto an already-initialised surface therefore left the client mirror one generation
|
||||
// behind while unpumped snapshots accumulated, so a cap getter or a shader compile before
|
||||
// the next Present read the prior mirror. Adopting here closes that: "a second snapshot
|
||||
// arrival IS the invalidation" (R-12) now holds AT the make-current that caused it. It is
|
||||
// idempotent - on the first make-current InitCapabilities already pumped, so this adopts
|
||||
// 0 - and a release-current (draw/ctx cleared) publishes nothing and is skipped.
|
||||
if (draw != EGL_NO_SURFACE && ctx != EGL_NO_CONTEXT) {
|
||||
if (ClientSession* session = ClientSession::Active()) {
|
||||
session->PumpControlPlane();
|
||||
RefreshFormatCapabilities();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
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). Calling Server::ServerSwapEGLBuffers 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. The forwarder exists for a spawned P6 client whose
|
||||
// Present record cannot carry the swap; in P5 it has no caller and that is deliberate.
|
||||
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.
|
||||
Server::ServerSetEGLSwapInterval(interval);
|
||||
}
|
||||
|
||||
void BackendObject_Remote::ReleaseEGLSurface(EGLSurface surface) {
|
||||
Server::ServerReleaseEGLSurface(surface);
|
||||
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.
|
||||
Server::ServerReleaseEGLResources();
|
||||
MG_Backend::BackendObject::ReleaseEGLResources();
|
||||
}
|
||||
|
||||
// NO strong CreateRemoteBackendObject() lives here, and the reason is a link fact, not an
|
||||
// oversight. v1's Init.cpp calls MG_Remote::Client::CreateRemoteBackendObject() and ships a
|
||||
// __attribute__((weak)) placeholder for it beside ServerLoop that aborts by name; its comment
|
||||
// expects "c1's strong definition [to] displace it at link time". A strong definition here
|
||||
// does NOT: libMobileGL is linked from a static archive, ServerLoop.o (weak) is already in
|
||||
// the link and satisfies Init's reference, and nothing else references this TU's
|
||||
// CreateRemoteBackendObject - so BackendObject_Remote.o is never pulled to override it, and
|
||||
// the weak's abort fires (measured: readelf shows one local symbol, the log shows
|
||||
// Fatal{UnimplementedRemoteBackendObject}). The integrator's Init.cpp hunk works because it
|
||||
// constructs BackendObject_Remote DIRECTLY (MakeUnique<BackendObject_Remote>), which both
|
||||
// references this object - forcing its TU into the link - and bypasses the weak symbol. So
|
||||
// the merge-time construction stays v1's Init.cpp edit (or a --whole-archive / forced
|
||||
// reference the integrator adds); see c1-v3.md.
|
||||
|
||||
} // 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
|
||||
|
||||
@@ -16,11 +16,15 @@
|
||||
#include "../Server/ServerLoop.h"
|
||||
#include "../Server/ServerSession.h"
|
||||
#include "../Transport/InProcessTransport.h"
|
||||
#include "WireTables.h"
|
||||
|
||||
#include <MGGitHash.h>
|
||||
#include <MG_Pipe/MGPipeCallbacks.h>
|
||||
#include <MG_Util/Debug/Log.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
@@ -101,6 +105,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";
|
||||
@@ -112,6 +139,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
|
||||
@@ -327,23 +518,26 @@ 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");
|
||||
}
|
||||
|
||||
// BOTH, AND m_started FIRST. `Active()` is what the integration lane's skip reads and
|
||||
// `m_started` is what `EmitAndWait` reads, and c1's round-1 rewrite of this function
|
||||
// set only the second of the two - so a fully-handshaken session with an apply thread
|
||||
// running and a caps mirror adopted took Fatal{NoClientSession, "Clear"} on its very
|
||||
// first verb, which is the most confusing possible spelling of "the session is up".
|
||||
// The order matters for the same reason it does at the other end: `Active()` hands a
|
||||
// caller a session it may immediately emit on, so the flag that permits emitting has
|
||||
// to be true before the pointer that grants access to it is published. `Stop()` takes
|
||||
// them down in the mirror order (m_started = false, then g_active = nullptr).
|
||||
m_started = true;
|
||||
g_active = this;
|
||||
LogMemory("handshake");
|
||||
@@ -357,10 +551,43 @@ namespace MobileGL::MG_Remote::Client {
|
||||
Stop();
|
||||
return running;
|
||||
}
|
||||
|
||||
// ---- 9. AND ONLY NOW THE THIRTY-SEVEN WIRE EMITTERS (R-17). This is the line that
|
||||
// arms `integration-split`: the 21 `DirectGLES.Split.*` entries skip on
|
||||
// `ClientSession::Active() == nullptr`, and every one of the four arming facts is true
|
||||
// at exactly this point and at no earlier one.
|
||||
//
|
||||
// IT IS LAST, AND EACH OF THE FOUR REASONS IS A DIFFERENT FAILURE:
|
||||
// - after Hello/Welcome (step 2-4), or an emitter would publish into a ring the peer
|
||||
// has not mapped;
|
||||
// - after the first CapsSnapshot (step 7), because R-8's liveness gates read the caps
|
||||
// mirror and a PLACEHOLDER mirror consumes nothing - a record emitted before it
|
||||
// would go to a server this client has not been told consumes that family;
|
||||
// - after ServerLoop::Start (step 8), because EmitAndWait BLOCKS on appliedSeq and
|
||||
// with no apply thread nothing advances it: the first resource_create would spend
|
||||
// 30 seconds in the barrier and then Fatal{BarrierTimeout};
|
||||
// - on THIS thread, the one that called MG_Backend::Init(), because it is the GL
|
||||
// thread and table 3 makes gPipeInputs its to touch while the barrier holds.
|
||||
// The publication is safe without a fence because the apply thread never reads these
|
||||
// tables - the server decodes straight into MGPipeApply* - and this thread wrote them
|
||||
// before it can reach any GL entry point.
|
||||
InstallClientWireTables();
|
||||
return MOBILEGL_OK;
|
||||
}
|
||||
|
||||
void ClientSession::Stop() {
|
||||
// FIRST, BEFORE ANYTHING ELSE GOES AWAY (R-17 / codex 4). Every later step here frees
|
||||
// something an emitter dereferences - the rings, the segments, the transports - so a
|
||||
// routed GL-thread call that arrives during teardown must not run the applier on the
|
||||
// caller and must not reach a half-freed ring. Round 2 reinstalled the monolith adapters
|
||||
// HERE, which is running the applier on the caller - the forbidden path table 3 draws.
|
||||
// Uninstall now RAISES A FLAG and leaves the wire rows in place; the next routed call
|
||||
// aborts by name (Fatal{ClientTablesUninstalled}) inside RequireSession before it touches
|
||||
// anything. The monolith adapters go back only at the END of teardown
|
||||
// (ReinstallMonolithAfterTeardown), for the at-exit ~BufferObject deletes that reach a
|
||||
// process with no session at all - and by then the rings are gone, so the applier a
|
||||
// monolith adapter runs is a defined no-op rather than a use-after-free.
|
||||
UninstallClientWireTables();
|
||||
if (!m_started) {
|
||||
// Start's own failure paths land here with a half-built session. FIVE of them are
|
||||
// reached AFTER ServerSession::Accept has already returned OK, so tearing down
|
||||
@@ -377,6 +604,9 @@ namespace MobileGL::MG_Remote::Client {
|
||||
m_clientTransport.reset();
|
||||
m_serverTransport.reset();
|
||||
m_transport = nullptr;
|
||||
// The rings are gone; put the monolith adapters back for a process that will make no
|
||||
// more routed calls except, possibly, at-exit deletes (codex 4).
|
||||
ReinstallMonolithAfterTeardown();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -433,6 +663,12 @@ namespace MobileGL::MG_Remote::Client {
|
||||
if (g_active == this) {
|
||||
g_active = nullptr;
|
||||
}
|
||||
// AND ONLY NOW the monolith adapters go back (codex 4): every ring an emitter would have
|
||||
// used is freed above, so from here a routed call - an at-exit ~BufferObject delete - runs
|
||||
// the applier exactly as it does under monolith, which is the correct answer for a
|
||||
// process that no longer has a session. During the whole span above, the raised flag made
|
||||
// any routed call abort by name instead.
|
||||
ReinstallMonolithAfterTeardown();
|
||||
}
|
||||
|
||||
Wire::PipeWireEncoder& ClientSession::Encoder() { return m_encoder; }
|
||||
@@ -444,18 +680,187 @@ 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, Uint64* replySizeOut) {
|
||||
if (statusOut != nullptr) *statusOut = Wire::ReplySink::kStatusError;
|
||||
if (replySizeOut != nullptr) *replySizeOut = 0;
|
||||
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. It is teardown, not a server fault - so the answer
|
||||
// handed back is DECLINED, not the ERROR the status was pre-set to (M4): a
|
||||
// reply-owning row that saw ERROR here would abort Fatal{ReplyError} on a shutting-
|
||||
// down session, which is what the "teardown legitimately reaches here" comment
|
||||
// promised would NOT happen. DECLINED is honest - "the verb did not happen" - and the
|
||||
// acceptance rows already treat it as `false` / nullptr without aborting.
|
||||
if (statusOut != nullptr) *statusOut = Wire::ReplySink::kStatusDeclined;
|
||||
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 (reported as DECLINED, not ERROR)",
|
||||
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 (replySizeOut != nullptr) *replySizeOut = replySize;
|
||||
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; }
|
||||
|
||||
|
||||
@@ -96,11 +96,17 @@ namespace MobileGL::MG_Remote::Client {
|
||||
// name where a kReplySlot answer lands; pass {nullptr, 0} for a call that has none.
|
||||
// Returns the record's seq, which is also its reply-slot id.
|
||||
//
|
||||
// `replySizeOut` (optional) receives the answer's OWN byte count as the server stamped
|
||||
// it - which is not always `replyBytes`: a short OK reply stamps fewer, and a DECLINE or
|
||||
// ERROR stamps 0. ReadPixels is the one caller that must know, because scattering a
|
||||
// reply that arrived short would spray stale bytes as pixels (M2 / codex 11); it reads
|
||||
// this and refuses `replySize != DstSize` by name rather than trust the copy.
|
||||
//
|
||||
// Waiting is spin(MOBILEGL_IPC_SPIN_US) then park, through Doorbell::Wait, with
|
||||
// producerParked set before blocking - the shape Doorbell.h:121 already implements.
|
||||
Uint64 EmitAndWait(MG_Pipe::MGPWireOp op, const void* payload, Uint64 payloadBytes,
|
||||
const void* varTail, Uint64 varTailBytes, void* replyOut,
|
||||
Uint64 replyBytes, Int32* statusOut);
|
||||
Uint64 replyBytes, Int32* statusOut, Uint64* replySizeOut = nullptr);
|
||||
|
||||
// MOBILEGL_IPC_VERB_BARRIER. False is the R-1 negative control and is EXPECTED to be
|
||||
// red; it must be run once and the way it goes red recorded.
|
||||
@@ -113,6 +119,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,37 @@
|
||||
// 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/BufferState/BufferObject.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
namespace MobileGL::MG_Remote::Client {
|
||||
|
||||
@@ -36,12 +60,647 @@ namespace MobileGL::MG_Remote::Client {
|
||||
std::abort();
|
||||
}
|
||||
|
||||
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();
|
||||
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;
|
||||
}
|
||||
|
||||
Uint32 ImplementedVerbCount() { return 0; }
|
||||
// ID-49's two halves, in one place so the emitter and its control read the same
|
||||
// arithmetic.
|
||||
//
|
||||
// GL 4.6 8.4.4, pack side: the destination row stride is ROW_LENGTH (or the width)
|
||||
// pixels rounded UP to PACK_ALIGNMENT, the first written byte is offset by SKIP_ROWS
|
||||
// whole strides plus SKIP_PIXELS pixels, and only `width * bytesPerPixel` bytes of each
|
||||
// stride are written - the gaps belong to the application and are never touched. That
|
||||
// last clause is what the control checks with a sentinel.
|
||||
Bool ReadbackPackStateIsTight(GLsizei width, Uint64 bytesPerPixel,
|
||||
const PixelStoreParameters& pack) {
|
||||
// SKIP_IMAGES (and IMAGE_HEIGHT) are NOT consulted: glReadPixels is a 2-D read and GL
|
||||
// ignores the image-level pack parameters for it, exactly as the monolith conversion
|
||||
// path does at DirectGLES.cpp:10905 (honorPackImageParams=false). A non-zero
|
||||
// SkipImages therefore does not make the layout non-tight (codex 6).
|
||||
if (pack.SkipRows != 0 || pack.SkipPixels != 0) return false;
|
||||
if (pack.RowLength != 0 && pack.RowLength != width) return false;
|
||||
const Uint64 alignment = pack.Alignment > 0 ? static_cast<Uint64>(pack.Alignment) : 1ull;
|
||||
const Uint64 rowBytes = static_cast<Uint64>(width) * bytesPerPixel;
|
||||
return (rowBytes % alignment) == 0;
|
||||
}
|
||||
|
||||
// ---- 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.
|
||||
//
|
||||
// ID-49: THE PACK STATE NEVER CROSSES FOR A READ, AND DstSize IS THE TIGHT EXTENT.
|
||||
// The first version of this computed GL 4.6 8.4.4's PACKED size - row length, skips,
|
||||
// alignment - and handed it over as DstSize. v1's server allocates exactly DstSize and
|
||||
// the real backend honours the live pack state, so a 4x3 RGBA8 read with
|
||||
// PACK_ROW_LENGTH=8, SKIP_ROWS=1, SKIP_PIXELS=2 allocated 80 bytes and the driver wrote
|
||||
// to byte 120. That is the shape of the joint inproc lane's two
|
||||
// DepthReadbackHonoursThePackPixelStoreParameters SEGFAULTs, on both backends.
|
||||
//
|
||||
// So the wire carries a RECTANGLE and not a layout: the server reads with NEUTRAL pack
|
||||
// state into a tight w*h*bytesPerPixel run that IS the reply payload, and the CLIENT -
|
||||
// which is the side that holds the application's pack state, and the only side that
|
||||
// can - scatters those rows into the application's pointer. "OnReadPixels writes
|
||||
// exactly DstSize bytes" still holds; DstSize is now a number both sides derive from
|
||||
// the same three values instead of one side deriving it from state the other cannot
|
||||
// see.
|
||||
//
|
||||
// IT IS FORMAT-AGNOSTIC ON PURPOSE. The depth and depth-stencil reads the 21 split
|
||||
// entries touch take the same rule with no special case, because the rule is about the
|
||||
// LAYOUT and not about the component: bytesPerPixel is whatever the format sizes to.
|
||||
Uint64 ReadbackBytesPerPixel(GLenum format, GLenum type) {
|
||||
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();
|
||||
}
|
||||
return static_cast<Uint64>(bytesPerPixel);
|
||||
}
|
||||
|
||||
Uint64 TightReadbackBytes(GLsizei width, GLsizei height, GLenum format, GLenum type) {
|
||||
if (width <= 0 || height <= 0) return 0;
|
||||
return static_cast<Uint64>(width) * static_cast<Uint64>(height) *
|
||||
ReadbackBytesPerPixel(format, type);
|
||||
}
|
||||
|
||||
// M2 / codex 11: the reply the server posted is COMPLETE and OK. A short OK reply, and a
|
||||
// DECLINED or ERROR reply with a zero payload, both leave the destination full of stale
|
||||
// bytes; scattering or returning it is the silently truncated picture ID-47's own comment
|
||||
// says an SSIM comparison cannot see, arriving through the status field rather than
|
||||
// through truncation. `expected` is the record's own DstSize (CONTRACT-P5 row 23: the
|
||||
// reply's exact extent). The predicate is at namespace scope (below) and exposed for the
|
||||
// control, so R-16's "drive the production predicate" holds rather than a second copy of
|
||||
// the rule in the test.
|
||||
|
||||
// The same predicate at the call site, with the named Fatal each failure mode owns.
|
||||
// status > expected cannot reach here: EmitAndWait already aborts Fatal{ReplyTooLarge} on
|
||||
// an oversize reply, so the only failures left are a wrong status or a SHORT one.
|
||||
void RequireReadbackReplyComplete(Int32 status, Uint64 replySize, Uint64 expected) {
|
||||
if (status == Wire::ReplySink::kStatusError) {
|
||||
MGLOG_F("MGPipe: Fatal{ReplyError, \"ReadPixels\"} - the readback answered ERROR; "
|
||||
"the destination is left untouched rather than filled with stale bytes");
|
||||
std::abort();
|
||||
}
|
||||
if (status == Wire::ReplySink::kStatusDeclined) {
|
||||
MGLOG_F("MGPipe: Fatal{ReadbackDeclined, \"ReadPixels\"} - the server has no "
|
||||
"GL.ReadPixels and DECLINED; a decline is a real answer for an acceptance "
|
||||
"row (R-5) but a blocking readback has no pixels to return, so it is a "
|
||||
"Fatal here rather than a buffer of stale bytes");
|
||||
std::abort();
|
||||
}
|
||||
if (replySize != expected) {
|
||||
MGLOG_F("MGPipe: Fatal{ReadbackReplyShort, \"ReadPixels %llu < %llu\"} - the OK "
|
||||
"reply carried fewer bytes than the read's own DstSize (CONTRACT-P5 row "
|
||||
"23's exact extent); the missing rows would otherwise be scattered as "
|
||||
"whatever the destination held",
|
||||
static_cast<unsigned long long>(replySize),
|
||||
static_cast<unsigned long long>(expected));
|
||||
std::abort();
|
||||
}
|
||||
}
|
||||
|
||||
void EmitReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format,
|
||||
GLenum type, void* pixels) {
|
||||
ClientSession& session = RequireSession("ReadPixels");
|
||||
|
||||
// ID-57 / M8: A PACK-PBO DESTINATION IS REFUSED BY NAME, BEFORE ANY EMISSION AND
|
||||
// BEFORE `pixels` IS TOUCHED. With GL_PIXEL_PACK_BUFFER bound, the frontend permits
|
||||
// `pixels` to be a byte OFFSET into that buffer, not an address (GL_Framebuffer.cpp:
|
||||
// 3055 aligns it to the type size, one byte for UNSIGNED_BYTE) - and this emitter has
|
||||
// no PBO branch: it sets DstOffset = 0, hands the offset to EmitAndWait as a host
|
||||
// buffer, and the reply is memcpy'd to CPU address <offset>. Under monolith the
|
||||
// backend maps the PBO and writes the reply into the buffer (unchanged). The real
|
||||
// split form - the server writes the reply into the buffer resource and the client
|
||||
// marks it GPU-written (b1's MarkReadPixelsPackBuffer becoming the producer contract
|
||||
// §3 names) - is a P6 ROADMAP item. In P5 it is class C's shape (R-4), refused here.
|
||||
if (MG_State::pGLContext != nullptr &&
|
||||
MG_State::pGLContext->GetBufferBindingSlot(::MobileGL::BufferTarget::PixelPack)
|
||||
.GetBoundObject()) {
|
||||
UnmigratedVerbFatal("ReadPixels+PACK_BUFFER");
|
||||
}
|
||||
|
||||
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. (In P5 the refusal above means no PBO read reaches here at all; the
|
||||
// note stays for the P6 form.)
|
||||
if (width <= 0 || height <= 0) return;
|
||||
const Uint64 bytesPerPixel = ReadbackBytesPerPixel(format, type);
|
||||
// ONE tight-size function for production AND the control (M3 / codex 10a). The first
|
||||
// cut computed this inline here while the test drove TightReadbackByteCount, so a
|
||||
// `+16` on the production line stayed green - the test observed a different number.
|
||||
// Now the number the server allocates and the number the test asserts come from the
|
||||
// same body.
|
||||
const Uint64 tight = TightReadbackBytes(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 = tight;
|
||||
|
||||
// CHECKED BEFORE THE EMISSION, not after the answer (ID-47), and through S1's
|
||||
// HELPER rather than a copy of it here. A reply bigger than a slot is Fatal on the
|
||||
// SERVER too, and s1 keeps that as the last line of defence - but it fires on the
|
||||
// apply thread with the record already on the wire, where all the client sees is a
|
||||
// hang. This one names the read, at the call site that knows what the read was.
|
||||
//
|
||||
// THE NUMBER IS ID-49's TIGHT EXTENT and not the packed one: the pack state never
|
||||
// crosses, so the answer that has to fit a slot is w*h*bytesPerPixel. The cap is
|
||||
// the pool's own, read live, so s1's growth of SEG_REPLY to 16 MiB / eight 2 MiB
|
||||
// slots needed no edit in this file - only the merge.
|
||||
session.RequireReadPixelsReplyFits(static_cast<Uint32>(width),
|
||||
static_cast<Uint32>(height),
|
||||
static_cast<Uint32>(format),
|
||||
static_cast<Uint32>(type), tight);
|
||||
|
||||
PixelStoreParameters pack{};
|
||||
if (MG_State::pGLContext != nullptr) {
|
||||
pack = MG_State::pGLContext->GetPixelStoreParameters(/*isUnpack=*/false);
|
||||
}
|
||||
|
||||
Int32 status = 0;
|
||||
Uint64 replySize = 0;
|
||||
if (ReadbackPackStateIsTight(width, bytesPerPixel, pack)) {
|
||||
// THE COMMON CASE, AND IT KEEPS THE ZERO-COPY. A neutral pack state means the
|
||||
// destination layout IS the tight layout, so the reply lands straight in the
|
||||
// application's buffer and there is no bounce at all. It is a fast path for the
|
||||
// SAME bytes, not a second rule: ScatterTightReadback below is a memcpy of the
|
||||
// whole run in exactly this case, and the control drives that function.
|
||||
session.EmitAndWait(MG_Pipe::MGPWireOp::ReadPixels, &info, sizeof(info), nullptr, 0,
|
||||
pixels, tight, &status, &replySize);
|
||||
// M2 / codex 11: an OK reply that arrived short, or a DECLINE/ERROR, must not be
|
||||
// handed back as pixels. The Fatal aborts before the application reads the buffer,
|
||||
// so the bytes EmitAndWait already copied into `pixels` are never observed.
|
||||
RequireReadbackReplyComplete(status, replySize, tight);
|
||||
return;
|
||||
}
|
||||
|
||||
// The bounce is the price of the application having asked for a layout. It is the
|
||||
// tight size and never more, and it is freed before this returns - R-11's rule one
|
||||
// level out: nothing here outlives the call.
|
||||
Vector<Uint8> bounce(static_cast<SizeT>(tight));
|
||||
session.EmitAndWait(MG_Pipe::MGPWireOp::ReadPixels, &info, sizeof(info), nullptr, 0,
|
||||
bounce.data(), tight, &status, &replySize);
|
||||
// BEFORE THE SCATTER, so a short or non-OK reply never reaches the application's
|
||||
// pointer at all (the bounce is the only thing that held the partial bytes).
|
||||
RequireReadbackReplyComplete(status, replySize, tight);
|
||||
ScatterTightReadbackIntoPackState(bounce.data(), pixels, width, height, bytesPerPixel,
|
||||
pack);
|
||||
}
|
||||
|
||||
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() {
|
||||
// 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 kEmittedSlots; }
|
||||
Uint32 LocallyAnsweredSlotCount() { return kLocallyAnsweredSlots; }
|
||||
Uint32 UnmigratedSlotCount() { return kUnmigratedSlots; }
|
||||
|
||||
void SetDropClearEmissionForNegativeControl(Bool drop) { g_dropClearEmission = drop; }
|
||||
Uint64 DroppedClearEmissions() { return g_droppedClearEmissions; }
|
||||
|
||||
Bool ReadbackPackStateIsTightForTest(GLsizei width, Uint64 bytesPerPixel,
|
||||
const PixelStoreParameters& pack) {
|
||||
return ReadbackPackStateIsTight(width, bytesPerPixel, pack);
|
||||
}
|
||||
|
||||
Uint64 TightReadbackByteCount(GLsizei width, GLsizei height, GLenum format, GLenum type) {
|
||||
return TightReadbackBytes(width, height, format, type);
|
||||
}
|
||||
|
||||
// M2 / codex 11's predicate at namespace scope: the one RequireReadbackReplyComplete decides
|
||||
// on and the one the control drives. 0=OK / 1=DECLINED / 2=ERROR.
|
||||
Bool ReadbackReplyIsComplete(Int32 status, Uint64 replySize, Uint64 expected) {
|
||||
return status == Wire::ReplySink::kStatusOk && replySize == expected;
|
||||
}
|
||||
|
||||
// ID-49's scatter. Exported for the same reason as the refusal above: the control drives
|
||||
// THIS, which is what the emitter calls, rather than a second copy of 8.4.4's arithmetic.
|
||||
void ScatterTightReadbackIntoPackState(const void* tight, void* destination, GLsizei width,
|
||||
GLsizei height, Uint64 bytesPerPixel,
|
||||
const PixelStoreParameters& pack) {
|
||||
if (tight == nullptr || destination == nullptr || width <= 0 || height <= 0) return;
|
||||
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 strideBytes =
|
||||
((rowPixels * bytesPerPixel + alignment - 1) / alignment) * alignment;
|
||||
// m6: the client re-derives GL 4.6 8.4.4's pack layout, so it honours GL_PACK_ROW_LENGTH
|
||||
// VERBATIM - including the ill-formed 0 < ROW_LENGTH < width, where the row stride is
|
||||
// narrower than a written row and consecutive rows overlap. GL leaves that case to the
|
||||
// implementation; the client reproduces exactly what the monolith backend's own scatter
|
||||
// would do with the same state rather than clamping, so the two arms stay byte-identical.
|
||||
// The fast path (ReadbackPackStateIsTight) already rejects any ROW_LENGTH != width, so
|
||||
// this only runs on the scatter path the application asked for.
|
||||
const Uint64 writtenPerRow = static_cast<Uint64>(width) * bytesPerPixel;
|
||||
// SKIP_IMAGES and IMAGE_HEIGHT ARE IGNORED (codex 6). glReadPixels is a 2-D read; GL
|
||||
// does not apply the image-level pack parameters to it, and the monolith conversion path
|
||||
// says so explicitly with honorPackImageParams=false (DirectGLES.cpp:10905). Applying
|
||||
// SKIP_IMAGES here shifted a read with SKIP_IMAGES=1 by a whole image and overran an
|
||||
// application buffer sized for exactly `height` rows. Only SKIP_ROWS and SKIP_PIXELS -
|
||||
// the 2-D skips - offset the first written byte.
|
||||
auto* out = static_cast<Uint8*>(destination) +
|
||||
static_cast<Uint64>(pack.SkipRows) * strideBytes +
|
||||
static_cast<Uint64>(pack.SkipPixels) * bytesPerPixel;
|
||||
const auto* in = static_cast<const Uint8*>(tight);
|
||||
for (Uint64 row = 0; row < static_cast<Uint64>(height); ++row) {
|
||||
std::memcpy(out + row * strideBytes, in + row * writtenPerRow,
|
||||
static_cast<SizeT>(writtenPerRow));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace MobileGL::MG_Remote::Client
|
||||
|
||||
@@ -47,19 +47,37 @@
|
||||
// disappears. A null check on a slot may not survive into the client: it becomes a caps-mirror
|
||||
// read, which is what ARCHITECTURE.md:114 means by "CallMask replaces 'is this table slot null'".
|
||||
//
|
||||
// NOTE the asymmetry this table does not resolve: the resource, CSO, framebuffer, texture,
|
||||
// sampler and program families do NOT come through here. They are emitted from
|
||||
// MG_Impl/Pipe/* by direct MGPipeApply* calls (37 entry points, 41 call sites), and under
|
||||
// split each of those becomes an encode. This table covers only the verbs - the draws,
|
||||
// clears, blits, readbacks, queries, fences and present.
|
||||
// NOTE the asymmetry this table does not resolve, AND WHERE IT IS RESOLVED (R-17): the
|
||||
// resource, CSO, framebuffer, texture, sampler and program families do NOT come through here.
|
||||
// They were emitted from MG_Impl/Pipe/* by 40 direct calls to the 37 MGPipeApply* entry
|
||||
// points; those call sites now go through the two generated tables
|
||||
// (MG_Pipe/PipeRoute.h -> MG_Remote/Client/WireTables.cpp). This table covers only the verbs -
|
||||
// the draws, clears, blits, readbacks, queries, fences and present.
|
||||
|
||||
#pragma once
|
||||
#include <Includes.h>
|
||||
|
||||
#include <MG_Backend/BackendObject.h>
|
||||
#include <MG_Pipe/MGPipeValueTypes.h>
|
||||
|
||||
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 +95,63 @@ 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();
|
||||
|
||||
// ID-47. The CLIENT refuses a readback whose answer would not fit a reply slot, BEFORE it
|
||||
// emits the record, and names the read. THE REFUSAL ITSELF IS s1's -
|
||||
// ClientSession::RequireReadPixelsReplyFits, forwarding to
|
||||
// ReplySlotPool::RequireReadPixelsFits - and this package does not own a second copy of the
|
||||
// message: two spellings of one refusal is how the two sides come to disagree about which
|
||||
// reads are legal. What c1 owns is the CALL SITE and the number it passes, which is ID-49's
|
||||
// tight extent; the control below is over that, and s1-v3.md §1's cases are over the
|
||||
// helper.
|
||||
|
||||
// ID-49. `MGPReadbackInfo::DstSize` is the TIGHT w*h*bytesPerPixel extent - the reply
|
||||
// payload - and nothing about the application's pack state crosses the wire. The server
|
||||
// reads with a NEUTRAL pack state into that run; this is the number both sides derive.
|
||||
Uint64 TightReadbackByteCount(GLsizei width, GLsizei height, GLenum format, GLenum type);
|
||||
|
||||
// ID-49. Scatters the tight rows into the application's pointer per the application's own
|
||||
// pack state (ROW_LENGTH, SKIP_*, ALIGNMENT), which only the client holds. Exposed for the
|
||||
// same reason as the refusal above: the control drives the function the emitter calls
|
||||
// rather than a second copy of GL 4.6 8.4.4's arithmetic. THE GAPS ARE NEVER WRITTEN -
|
||||
// they belong to the application - and that is what the control checks with a sentinel.
|
||||
void ScatterTightReadbackIntoPackState(const void* tight, void* destination, GLsizei width,
|
||||
GLsizei height, Uint64 bytesPerPixel,
|
||||
const PixelStoreParameters& pack);
|
||||
|
||||
// ID-49. True when the destination layout IS the tight layout, which is the only condition
|
||||
// under which EmitReadPixels may read the reply straight into the application pointer and
|
||||
// skip the bounce. Exported because the FAST PATH and the SCATTER have to agree, and the
|
||||
// only honest way to state that is to drive both and compare - a case that tested either
|
||||
// alone would pass a predicate that said yes to a layout the scatter would have rearranged.
|
||||
Bool ReadbackPackStateIsTightForTest(GLsizei width, Uint64 bytesPerPixel,
|
||||
const PixelStoreParameters& pack);
|
||||
|
||||
// M2 / codex 11. True exactly when the readback reply is OK and carries the read's own exact
|
||||
// extent (CONTRACT-P5 row 23). EmitReadPixels calls this and Fatals by name when it is false
|
||||
// - a short OK reply, or a DECLINE/ERROR with a zero payload, is refused rather than scattered
|
||||
// as pixels. Exposed so the control drives the production predicate (R-16), not a copy: pass
|
||||
// 0=OK / 1=DECLINED / 2=ERROR as `status`.
|
||||
Bool ReadbackReplyIsComplete(Int32 status, Uint64 replySize, Uint64 expected);
|
||||
|
||||
} // 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
|
||||
@@ -0,0 +1,596 @@
|
||||
// MobileGL - MobileGL/MG_Remote/Client/WireTables.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
|
||||
|
||||
// The ENCODE TWIN of gMGPipeWireRecordApply: thirty-seven emitters that turn a table call into
|
||||
// a wire record. Owner: package c1 (P5 ruling R-17). See WireTables.h for the install order
|
||||
// and MG_Pipe/PipeRoute.h for what R-17 actually cost.
|
||||
//
|
||||
// EVERY EMITTER IS THE SAME FOUR STEPS, and the macros below exist so that a reader can check
|
||||
// thirty-seven rows against PipeTables.inc in one pass instead of reading thirty-seven bodies:
|
||||
//
|
||||
// 1. require a session - a slot that fell through to a driver this role does not have is
|
||||
// the failure R-4 exists to prevent, and there is no fall-through here either;
|
||||
// 2. stage the blob, if the row has one, and name the SEG_STAGE run in the payload's own
|
||||
// MGPBlobRef - which is why the payload is COPIED: the table hands it over const, and
|
||||
// the blobref is the one field the client must write after the caller is done with it;
|
||||
// 3. EmitAndWait, which is the barrier's wait and the reply's wait at once (R-3/R-5);
|
||||
// 4. post the answer, for the rows that have one, into MG_Pipe's reply mailbox.
|
||||
//
|
||||
// WHAT IS DELIBERATELY NOT HERE. b1's `PushPersistentMapsBeforeVerb` / `MarkGpuWritesFor*` are
|
||||
// NOT called from these thirty-seven. They are pre-VERB hooks and these are not verbs: they
|
||||
// are the resource, CSO and state records that a verb is later drawn against. The five class-B
|
||||
// verbs in EmitTables.cpp call them, once each, immediately before their record, which is the
|
||||
// ordering b1's B-1 fix depends on. Calling them here as well would push a persistent map
|
||||
// before every `set_dynamic_state` - hundreds of times a frame, and each one a real record.
|
||||
|
||||
#include "WireTables.h"
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
|
||||
#include "ClientSession.h"
|
||||
|
||||
#include "../Server/ServerLoop.h"
|
||||
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
#include <MG_Pipe/PipeRoute.h>
|
||||
#include <MG_State/GLState/ProgramState/ProgramArtifactsCodec.h>
|
||||
#include <MG_Util/Debug/Log.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
namespace MobileGL::MG_Remote::Client {
|
||||
|
||||
using MG_Pipe::MGPWireOp;
|
||||
|
||||
// TABLE 3's ROLE SPLIT, AS A RUNTIME CHECK. gMGPipeScreen / gMGPipeContext are PROCESS
|
||||
// globals and under `inproc` the server role is a thread in this same process, so the
|
||||
// apply thread running the server's own backend - the EGL bring-up, InitCapabilities,
|
||||
// the applier - reaches these very emitters. A record published there would be waited
|
||||
// for by the thread that is supposed to apply it: `Fatal{BarrierTimeout,
|
||||
// "ResourceRespecify"}` from `mgl-srv-apply`, thirty seconds into bring-up, which is
|
||||
// exactly how this was found.
|
||||
//
|
||||
// THE ANSWER IS NOT "SUPPRESS THE RECORD" - it is "run the server's own code", because
|
||||
// on that thread this process IS the server and the applier is one call away. It is
|
||||
// the same thing PipeWireCodec does on the decode side, where every arm calls
|
||||
// MGPipeApply* directly and never goes through a table.
|
||||
//
|
||||
// Under `spawn` (P6) the predicate is constantly false in the client process and
|
||||
// constantly true in the server's, so this costs one atomic load and changes nothing.
|
||||
//
|
||||
// NOT in the anonymous namespace, because M5 needs it from MG_Impl/Pipe/PipeFill.cpp too
|
||||
// (its split-only respecify/flush branches must not run on the apply thread). Declared in
|
||||
// WireTables.h.
|
||||
Bool RunsAsTheServerRole() { return Server::ServerLoop::OnApplyThread(); }
|
||||
|
||||
namespace {
|
||||
|
||||
Uint64 g_emitted = 0;
|
||||
Uint64 g_declined = 0;
|
||||
|
||||
// TEARDOWN REFUSAL (codex 4). ClientSession::Stop marks the routed tables uninstalled
|
||||
// BEFORE it frees the rings, the segments and the transport - so the window between then
|
||||
// and the moment the monolith adapters go back is one in which a routed GL-thread call
|
||||
// must not run the applier on the caller (the forbidden path table 3 draws) and must not
|
||||
// reach a half-freed ring. Round 2 reinstalled the monolith adapters at the top of Stop,
|
||||
// which is exactly running the applier on the caller; a routed mutation after
|
||||
// UninstallClientWireTables() moved no wire ordinal and never refused (the cross-family
|
||||
// verifier reproduced it). So Uninstall now RAISES THIS FLAG and leaves the wire rows in
|
||||
// place; RequireSession below reads it and refuses by name before it touches anything.
|
||||
// Atomic because the apply thread's own role check races a GL-thread teardown.
|
||||
std::atomic<Bool> g_clientTablesUninstalled{false};
|
||||
|
||||
ClientSession& RequireSession(const char* row) {
|
||||
// BEFORE the session lookup and before any ring access. A routed call that arrives
|
||||
// once Stop has begun tearing the session down is refused by name rather than run
|
||||
// on the caller's thread - the applier is server-exclusive (table 3), and a wire
|
||||
// emit into a ring being freed is a use-after-free. The monolith adapters are put
|
||||
// back only as the LAST step of teardown, for the at-exit ~BufferObject deletes that
|
||||
// legitimately reach a process with no session (see ReinstallMonolithAfterTeardown).
|
||||
if (g_clientTablesUninstalled.load(std::memory_order_acquire)) {
|
||||
MGLOG_F("MGPipe: Fatal{ClientTablesUninstalled, \"%s\"} - a routed call reached the "
|
||||
"client wire tables while ClientSession::Stop was tearing the session "
|
||||
"down. R-4/R-17: the applier is server-exclusive and the rings this emit "
|
||||
"would use are being freed, so the call is refused by name rather than run "
|
||||
"on the caller",
|
||||
row);
|
||||
std::abort();
|
||||
}
|
||||
ClientSession* session = ClientSession::Active();
|
||||
if (session == nullptr) {
|
||||
MGLOG_F("MGPipe: Fatal{NoClientSession, \"%s\"} - the client wire tables are "
|
||||
"installed but no ClientSession is active. A row may not fall through to "
|
||||
"a driver this role does not have",
|
||||
row);
|
||||
std::abort();
|
||||
}
|
||||
return *session;
|
||||
}
|
||||
|
||||
// Stages a mandatory blob. `StageBytes` Fatals on a zero size by design (R-2.2: "the
|
||||
// record declared no blob" and "the record declared an empty blob" must not be spelled
|
||||
// the same way on a wire), so a row whose decoder calls RequireDeclaredBlob or
|
||||
// ResolveOrFatal is checked HERE, on the producing side, where the row has a name.
|
||||
MG_Pipe::MGPBlobRef StageRequired(ClientSession& session, const char* row,
|
||||
const void* bytes, Uint64 count) {
|
||||
if (bytes == nullptr || count == 0) {
|
||||
MGLOG_F("MGPipe: Fatal{BlobMissing, \"%s\"} - the row's decoder requires a "
|
||||
"declared blob and the call site handed over %llu bytes at %p. Under "
|
||||
"monolith the companion pointer carries them; under split they have to "
|
||||
"be staged, and there is nothing to stage",
|
||||
row, static_cast<unsigned long long>(count), bytes);
|
||||
std::abort();
|
||||
}
|
||||
return session.Encoder().StageBytes(bytes, count);
|
||||
}
|
||||
|
||||
// Stages an OPTIONAL blob: the two sub-data rows, whose decoders resolve only when the
|
||||
// record's own size field says there are bytes. All three fields zero is the wire's
|
||||
// "no blob declared", and CheckBlobIsHonest refuses any other spelling of it.
|
||||
MG_Pipe::MGPBlobRef StageOptional(ClientSession& session, const void* bytes, Uint64 count) {
|
||||
if (bytes == nullptr || count == 0) return MG_Pipe::MGPBlobRef{};
|
||||
return session.Encoder().StageBytes(bytes, count);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------
|
||||
// The three regular shapes
|
||||
// ---------------------------------------------------------------------------------
|
||||
|
||||
#define MGP_WIRE_PLAIN(Name, Payload, Table) \
|
||||
void Wire_##Name(const MG_Pipe::Payload* payload) { \
|
||||
if (RunsAsTheServerRole()) { MG_Pipe::MGPipeMonolith##Table().Name(payload); return; } \
|
||||
ClientSession& session = RequireSession(#Name); \
|
||||
session.EmitAndWait(MGPWireOp::Name, payload, sizeof(*payload), nullptr, 0, nullptr, 0, \
|
||||
nullptr); \
|
||||
++g_emitted; \
|
||||
}
|
||||
|
||||
#define MGP_WIRE_BLOB(Name, Payload, BlobMember) \
|
||||
void Wire_##Name(const MG_Pipe::Payload* payload, const void* blobBytes, \
|
||||
Uint64 blobByteCount) { \
|
||||
if (RunsAsTheServerRole()) { \
|
||||
MG_Pipe::MGPipeMonolithContext().Name(payload, blobBytes, blobByteCount); \
|
||||
return; \
|
||||
} \
|
||||
ClientSession& session = RequireSession(#Name); \
|
||||
MG_Pipe::Payload record = *payload; \
|
||||
record.BlobMember = StageRequired(session, #Name, blobBytes, blobByteCount); \
|
||||
session.EmitAndWait(MGPWireOp::Name, &record, sizeof(record), nullptr, 0, nullptr, 0, \
|
||||
nullptr); \
|
||||
++g_emitted; \
|
||||
}
|
||||
|
||||
#define MGP_WIRE_TAIL(Name, Payload, TailType) \
|
||||
void Wire_##Name(const MG_Pipe::Payload* payload, const void* varTail, Uint32 varTailCount) { \
|
||||
if (RunsAsTheServerRole()) { \
|
||||
MG_Pipe::MGPipeMonolithContext().Name(payload, varTail, varTailCount); \
|
||||
return; \
|
||||
} \
|
||||
ClientSession& session = RequireSession(#Name); \
|
||||
session.EmitAndWait(MGPWireOp::Name, payload, sizeof(*payload), varTail, \
|
||||
static_cast<Uint64>(varTailCount) * sizeof(MG_Pipe::TailType), \
|
||||
nullptr, 0, nullptr); \
|
||||
++g_emitted; \
|
||||
}
|
||||
|
||||
// -- screen -------------------------------------------------------------------
|
||||
MGP_WIRE_PLAIN(ResourceDestroy, MGPHandleOnly, Screen)
|
||||
MGP_WIRE_PLAIN(UnmapPersistent, MGPHandleOnly, Screen)
|
||||
|
||||
// -- context, plain -----------------------------------------------------------
|
||||
MGP_WIRE_PLAIN(BindRenderState, MGPBindRenderState, Context)
|
||||
MGP_WIRE_PLAIN(DeleteRenderState, MGPHandleOnly, Context)
|
||||
MGP_WIRE_PLAIN(BindVertexElements, MGPHandleOnly, Context)
|
||||
MGP_WIRE_PLAIN(DeleteVertexElements, MGPHandleOnly, Context)
|
||||
MGP_WIRE_PLAIN(DeleteSamplerState, MGPHandleOnly, Context)
|
||||
MGP_WIRE_PLAIN(CreateSamplerView, MGPSamplerView, Context)
|
||||
MGP_WIRE_PLAIN(DeleteSamplerView, MGPHandleOnly, Context)
|
||||
MGP_WIRE_PLAIN(BindShaderState, MGPHandleOnly, Context)
|
||||
MGP_WIRE_PLAIN(DeleteShaderState, MGPHandleOnly, Context)
|
||||
MGP_WIRE_PLAIN(SetDrawProgram, MGPHandleOnly, Context)
|
||||
MGP_WIRE_PLAIN(SetDispatchProgram, MGPHandleOnly, Context)
|
||||
MGP_WIRE_PLAIN(SetFramebufferState, MGPFramebufferState, Context)
|
||||
MGP_WIRE_PLAIN(SetIndexBuffer, MGPIndexBuffer, Context)
|
||||
MGP_WIRE_PLAIN(SetPixelPackState, MGPPixelPackState, Context)
|
||||
MGP_WIRE_PLAIN(SetPatchState, MGPPatchState, Context)
|
||||
|
||||
// -- context, mandatory blob --------------------------------------------------
|
||||
MGP_WIRE_BLOB(CreateRenderState, MGPRenderStateDesc, Blob)
|
||||
MGP_WIRE_BLOB(CreateVertexElements, MGPVertexElements, Blob)
|
||||
MGP_WIRE_BLOB(CreateSamplerState, MGPSamplerDesc, Parameters)
|
||||
MGP_WIRE_BLOB(SetGlobalConstants, MGPGlobalConstants, Blob)
|
||||
|
||||
// -- context, variable tail ---------------------------------------------------
|
||||
MGP_WIRE_TAIL(SetVertexBuffers, MGPVertexBuffers, MGPVertexBuffer)
|
||||
MGP_WIRE_TAIL(SetSamplerViews, MGPSamplerViews, MGPBoundView)
|
||||
MGP_WIRE_TAIL(BindSamplerStates, MGPSamplerStates, MGPipeHandle)
|
||||
MGP_WIRE_TAIL(SetShaderImages, MGPShaderImages, MGPImageView)
|
||||
MGP_WIRE_TAIL(SetVertexAttribDefaults, MGPVertexAttribDefaults, MGPAttribValue)
|
||||
|
||||
#undef MGP_WIRE_PLAIN
|
||||
#undef MGP_WIRE_BLOB
|
||||
#undef MGP_WIRE_TAIL
|
||||
|
||||
// -- the rows that fit none of the three shapes --------------------------------
|
||||
|
||||
// set_dynamic_state. AN OPTIONAL BLOB, NOT A MANDATORY ONE (round-2 regression, the
|
||||
// census's Fatal{BlobMissing, "SetDynamicState"}). EmitRenderState (PipeFill.cpp:2405-
|
||||
// 2421) sends a set_dynamic_state whenever the RenderState VERSION moved, and when the
|
||||
// chunk-level suppressor finds NO dynamic chunk changed it sends the 32-byte header with
|
||||
// ChunkMask == 0 and blobByteCount == 0 - a version-only update, which the comment there
|
||||
// calls out as deliberate. The generic MGP_WIRE_BLOB wrapper stages through StageRequired,
|
||||
// which Fatals on a zero count, so a legal header-only update aborted the process with a
|
||||
// blob-shape complaint that had nothing to do with the actual state of the pipe. The
|
||||
// DECODER already handles it: PipeWireCodec.cpp:1571-1587 takes the `ChunkMask == 0` arm
|
||||
// (CheckBlobIsHonest, no blob resolved) and applies the header alone. So the emitter is
|
||||
// the only side that was wrong; it stages OPTIONALLY, exactly like the two sub-data rows.
|
||||
// A non-empty mask still stages (blobByteCount > 0), so the reduced-path scenarios, whose
|
||||
// first draw is freshly primed with every chunk, are byte-for-byte unchanged.
|
||||
void Wire_SetDynamicState(const MG_Pipe::MGPDynamicState* payload, const void* blobBytes,
|
||||
Uint64 blobByteCount) {
|
||||
if (RunsAsTheServerRole()) {
|
||||
MG_Pipe::MGPipeMonolithContext().SetDynamicState(payload, blobBytes, blobByteCount);
|
||||
return;
|
||||
}
|
||||
ClientSession& session = RequireSession("SetDynamicState");
|
||||
MG_Pipe::MGPDynamicState record = *payload;
|
||||
record.Blob = StageOptional(session, blobBytes, blobByteCount);
|
||||
session.EmitAndWait(MGPWireOp::SetDynamicState, &record, sizeof(record), nullptr, 0,
|
||||
nullptr, 0, nullptr);
|
||||
++g_emitted;
|
||||
}
|
||||
|
||||
// set_residual_value_state. CONTRACT-P5 table 1 row 6: the applier takes a frontend
|
||||
// `ResidualValueBlock&` and `MGPResidualValueState` is never instantiated on the live
|
||||
// path, so the encoder invents BOTH the record fill and the blob fill. The block IS
|
||||
// the blob, whole - the decoder requires exactly sizeof(ResidualValueBlock) and says
|
||||
// why ("a size that only ever ratchets down makes a short read silently lose
|
||||
// CapabilityBits"), so the two sides state the same number from the same header.
|
||||
void Wire_SetResidualValueState(const MG_Pipe::MGPResidualValueState* payload,
|
||||
const void* blobBytes, Uint64 blobByteCount) {
|
||||
if (RunsAsTheServerRole()) {
|
||||
MG_Pipe::MGPipeMonolithContext().SetResidualValueState(payload, blobBytes,
|
||||
blobByteCount);
|
||||
return;
|
||||
}
|
||||
ClientSession& session = RequireSession("SetResidualValueState");
|
||||
MG_Pipe::MGPResidualValueState record = *payload;
|
||||
record.Blob = StageRequired(session, "SetResidualValueState", blobBytes, blobByteCount);
|
||||
session.EmitAndWait(MGPWireOp::SetResidualValueState, &record, sizeof(record), nullptr,
|
||||
0, nullptr, 0, nullptr);
|
||||
++g_emitted;
|
||||
}
|
||||
|
||||
// resource_readback. kReplySlot, and the answer is COMPLETION only: the bytes travel
|
||||
// server -> client in SEG_EVENT through OnBufferWriteback, because the destination is
|
||||
// the client's shadow and its size is the resource's, not a slot's (CONTRACT-P5 table 1
|
||||
// row 22). So the reply buffer is deliberately {nullptr, 0} and the wait is what makes
|
||||
// the writeback already drained by the time this returns.
|
||||
void Wire_ResourceReadback(const MG_Pipe::MGPReadback* payload, MG_Pipe::MGPReplySlot* reply) {
|
||||
if (RunsAsTheServerRole()) { MG_Pipe::MGPipeMonolithContext().ResourceReadback(payload, reply); return; }
|
||||
ClientSession& session = RequireSession("ResourceReadback");
|
||||
Int32 status = 0;
|
||||
const Uint64 seq = session.EmitAndWait(MGPWireOp::ResourceReadback, payload,
|
||||
sizeof(*payload), nullptr, 0, nullptr, 0,
|
||||
&status);
|
||||
reply->Id = seq;
|
||||
MG_Pipe::MGPipePostReply(*reply, status, 0);
|
||||
++g_emitted;
|
||||
}
|
||||
|
||||
// ---- the three acceptance rows that fit a generated signature ----------------
|
||||
//
|
||||
// THE ANSWER IS THE SERVER'S AND NOTHING ELSE. `EmitAndWait` returns the record's seq
|
||||
// and fills `status` from the reply slot the server stamped; DECLINED is `false` and OK
|
||||
// is `true`, and neither is derived from anything this side knows. R-5 exists because
|
||||
// "always accept" is ID-39's 66 lost DirectVulkan uploads and "accept if we emitted" is
|
||||
// the same bug wearing a counter.
|
||||
|
||||
void Wire_ResourceCreate(const MG_Pipe::MGPResourceDesc* payload, MG_Pipe::MGPReplySlot* reply) {
|
||||
if (RunsAsTheServerRole()) { MG_Pipe::MGPipeMonolithScreen().ResourceCreate(payload, reply); return; }
|
||||
ClientSession& session = RequireSession("ResourceCreate");
|
||||
Int32 status = 0;
|
||||
const Uint64 seq = session.EmitAndWait(MGPWireOp::ResourceCreate, payload,
|
||||
sizeof(*payload), nullptr, 0, nullptr, 0,
|
||||
&status);
|
||||
reply->Id = seq;
|
||||
MG_Pipe::MGPipePostReply(*reply, status, status == 0 ? 1u : 0u);
|
||||
++g_emitted;
|
||||
if (status == 1) ++g_declined;
|
||||
}
|
||||
|
||||
void Wire_SetTextureParams(const MG_Pipe::MGPTextureParams* payload, MG_Pipe::MGPReplySlot* reply) {
|
||||
if (RunsAsTheServerRole()) { MG_Pipe::MGPipeMonolithContext().SetTextureParams(payload, reply); return; }
|
||||
ClientSession& session = RequireSession("SetTextureParams");
|
||||
Int32 status = 0;
|
||||
const Uint64 seq = session.EmitAndWait(MGPWireOp::SetTextureParams, payload,
|
||||
sizeof(*payload), nullptr, 0, nullptr, 0,
|
||||
&status);
|
||||
reply->Id = seq;
|
||||
MG_Pipe::MGPipePostReply(*reply, status, status == 0 ? 1u : 0u);
|
||||
++g_emitted;
|
||||
if (status == 1) ++g_declined;
|
||||
}
|
||||
|
||||
void Wire_ResourceSubData(const MG_Pipe::MGPSubData* payload, const void* blobBytes,
|
||||
Uint64 blobByteCount, const void* varTail, Uint32 varTailCount,
|
||||
MG_Pipe::MGPReplySlot* reply) {
|
||||
if (RunsAsTheServerRole()) {
|
||||
MG_Pipe::MGPipeMonolithContext().ResourceSubData(payload, blobBytes, blobByteCount,
|
||||
varTail, varTailCount, reply);
|
||||
return;
|
||||
}
|
||||
ClientSession& session = RequireSession("ResourceSubData");
|
||||
MG_Pipe::MGPSubData record = *payload;
|
||||
record.Blob = StageOptional(session, blobBytes, blobByteCount);
|
||||
Int32 status = 0;
|
||||
const Uint64 seq = session.EmitAndWait(
|
||||
MGPWireOp::ResourceSubData, &record, sizeof(record), varTail,
|
||||
static_cast<Uint64>(varTailCount) * sizeof(MG_Pipe::MGPSubRegion), nullptr, 0,
|
||||
&status);
|
||||
reply->Id = seq;
|
||||
MG_Pipe::MGPipePostReply(*reply, status, status == 0 ? 1u : 0u);
|
||||
++g_emitted;
|
||||
if (status == 1) ++g_declined;
|
||||
}
|
||||
|
||||
void Wire_BufferSubDataResident(const MG_Pipe::MGPSubData* payload, const void* blobBytes,
|
||||
Uint64 blobByteCount) {
|
||||
if (RunsAsTheServerRole()) {
|
||||
MG_Pipe::MGPipeMonolithContext().BufferSubDataResident(payload, blobBytes,
|
||||
blobByteCount);
|
||||
return;
|
||||
}
|
||||
ClientSession& session = RequireSession("BufferSubDataResident");
|
||||
MG_Pipe::MGPSubData record = *payload;
|
||||
record.Blob = StageOptional(session, blobBytes, blobByteCount);
|
||||
session.EmitAndWait(MGPWireOp::BufferSubDataResident, &record, sizeof(record), nullptr,
|
||||
0, nullptr, 0, nullptr);
|
||||
++g_emitted;
|
||||
}
|
||||
|
||||
// ---- the four escapes --------------------------------------------------------
|
||||
|
||||
// resource_respecify. R-13.3: `initialBytes` is ALWAYS nullptr under split and the
|
||||
// initial content arrives as resource_subdata records immediately after this one. The
|
||||
// caller's bytes are therefore not dropped - they are re-expressed - and PipeFill.cpp's
|
||||
// emitter is where that happens, because the chunking walk that has to size them
|
||||
// (MGPipeForEachSubDataRecordRange) lives there. What this emitter owes is the REFUSAL:
|
||||
// a non-null pointer arriving here means the call site was not converted, and silently
|
||||
// ignoring it would lose exactly the bytes R-13.3 promised would follow.
|
||||
Bool Wire_Escape_ResourceRespecify(const MG_Pipe::MGPResourceDesc* desc,
|
||||
const void* initialBytes,
|
||||
const MG_Pipe::MGPRespecifiedLevel* level) {
|
||||
if (RunsAsTheServerRole()) {
|
||||
return MG_Pipe::MGPipeMonolithEscapes().ResourceRespecify(desc, initialBytes, level);
|
||||
}
|
||||
ClientSession& session = RequireSession("ResourceRespecify");
|
||||
if (initialBytes != nullptr) {
|
||||
MGLOG_F("MGPipe: Fatal{UncarriedInitialBytes, \"resource_respecify\"} - a call "
|
||||
"site handed initial content to a split respecify. R-13.3 rules that "
|
||||
"initialBytes never crosses and that the content follows as "
|
||||
"resource_subdata; a caller that still passes it has bytes nothing will "
|
||||
"carry");
|
||||
std::abort();
|
||||
}
|
||||
// The scope rides in the descriptor's own pads (CONTRACT-P5 table 1 row 19b, LANDED)
|
||||
// and is written only through MGPipeSetRespecifiedLevel - three fields are one
|
||||
// value, and an open-coded writer that forgets the presence byte says "level 0 of
|
||||
// upload target 0" where it meant "the whole resource".
|
||||
MG_Pipe::MGPResourceDesc record = *desc;
|
||||
if (level != nullptr) {
|
||||
MG_Pipe::MGPipeSetRespecifiedLevel(record, level->UploadTarget, level->Level);
|
||||
} else {
|
||||
MG_Pipe::MGPipeClearRespecifiedLevel(record);
|
||||
}
|
||||
Int32 status = 0;
|
||||
const Uint64 seq =
|
||||
session.EmitAndWait(MGPWireOp::ResourceRespecify, &record, sizeof(record), nullptr,
|
||||
0, nullptr, 0, &status);
|
||||
(void)seq;
|
||||
++g_emitted;
|
||||
// ERROR IS NOT A DECLINE (M4 / codex 5 / R-5). status is 0 OK / 1 DECLINED / 2 ERROR;
|
||||
// an escape may not fold 2 into `false`, which is what a bare `return status == 0`
|
||||
// did - a transport fault then read as "the server said no". Every reply-owning row
|
||||
// - the generated acceptance rows through MGPipeTakeReplyBool, and now these two
|
||||
// escapes - answers ERROR with the same named Fatal.
|
||||
if (status == 2) {
|
||||
MGLOG_F("MGPipe: Fatal{ReplyError, \"resource_respecify\"} - the row answered "
|
||||
"ERROR, which is not an acceptance answer; folding it into accepted or "
|
||||
"refused would make a transport fault look like a resource decision");
|
||||
std::abort();
|
||||
}
|
||||
if (status == 1) ++g_declined;
|
||||
return status == 0;
|
||||
}
|
||||
|
||||
// resource_flush_range. R-13.2: it carries NO bytes under split - it is a
|
||||
// {range, AccessFlags} control record and the bytes of exactly that range arrive ahead
|
||||
// of it as resource_subdata. Same refusal as above, for the same reason: a blobref here
|
||||
// would be "a second, forgeable way to say the same thing".
|
||||
void Wire_Escape_ResourceFlushRange(const MG_Pipe::MGPFlushRange* record,
|
||||
const void* bytes) {
|
||||
if (RunsAsTheServerRole()) {
|
||||
MG_Pipe::MGPipeMonolithEscapes().ResourceFlushRange(record, bytes);
|
||||
return;
|
||||
}
|
||||
ClientSession& session = RequireSession("ResourceFlushRange");
|
||||
(void)bytes; // ruled uncarried; the emitter in PipeFill.cpp sends the range first
|
||||
session.EmitAndWait(MGPWireOp::ResourceFlushRange, record, sizeof(*record), nullptr, 0,
|
||||
nullptr, 0, nullptr);
|
||||
++g_emitted;
|
||||
}
|
||||
|
||||
// map_persistent. R-6/R-2.4: the split answer is a CONSTANT DECLINE, and it still costs
|
||||
// a record, because the server has to know the client asked - the applier's
|
||||
// MapPersistentRoundtrips counter is defined as "one per storage definition in both
|
||||
// modes" and a client that answered locally would zero it. `size` and `seedBytes` have
|
||||
// no carrier (MGPHandleOnly is {Handle, Kind}) and need none: nothing is minted.
|
||||
void* Wire_Escape_MapPersistent(const MG_Pipe::MGPHandleOnly* handle, Uint64 size,
|
||||
const void* seedBytes) {
|
||||
if (RunsAsTheServerRole()) {
|
||||
return MG_Pipe::MGPipeMonolithEscapes().MapPersistent(handle, size, seedBytes);
|
||||
}
|
||||
ClientSession& session = RequireSession("MapPersistent");
|
||||
(void)size;
|
||||
(void)seedBytes;
|
||||
Int32 status = 0;
|
||||
session.EmitAndWait(MGPWireOp::MapPersistent, handle, sizeof(*handle), nullptr, 0,
|
||||
nullptr, 0, &status);
|
||||
++g_emitted;
|
||||
// ERROR IS NOT A DECLINE (M4 / codex 5 / R-5), the same rule the respecify escape and
|
||||
// the generated acceptance rows obey: status 2 is a transport fault, and returning
|
||||
// nullptr for it would make it indistinguishable from R-6's legitimate decline.
|
||||
if (status == 2) {
|
||||
MGLOG_F("MGPipe: Fatal{ReplyError, \"map_persistent\"} - the row answered ERROR, "
|
||||
"which is not an acceptance answer; a transport fault is not a resource "
|
||||
"decision and may not be folded into the decline R-6 predicts");
|
||||
std::abort();
|
||||
}
|
||||
if (status == 1) ++g_declined;
|
||||
// NOT "always nullptr": the answer is READ. R-6 says the server declines, and the
|
||||
// day it stops declining this returns what it actually said rather than what the
|
||||
// ruling predicted.
|
||||
if (status == 0) {
|
||||
MGLOG_F("MGPipe: Fatal{UnexpectedMapAccept, \"map_persistent\"} - the server "
|
||||
"accepted a persistent map under split. R-6 makes the split answer a "
|
||||
"constant decline because there is no way to hand a host pointer across "
|
||||
"a process boundary in P5; a pointer arriving here is one this client "
|
||||
"cannot dereference");
|
||||
std::abort();
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// create_shader_state. SEVEN blobrefs and TWO typed frontend pointers; one blobBytes
|
||||
// pair cannot express seven runs. The serializer already exists and no package may
|
||||
// write a second one (CONTRACT-P5 table 1 row 3): EncodeProgramArtifacts produces one
|
||||
// archive, the decoder's DecodeProgramArtifacts consumes it, and the six per-stage runs
|
||||
// stay UNDECLARED because the modules already travel inside the archive - a declared
|
||||
// Spirv[i] is Fatal on the far side rather than ignored.
|
||||
void Wire_Escape_CreateShaderState(const MG_Pipe::MGPProgramDesc* desc,
|
||||
const MG_State::GLState::LinkArtifacts* link,
|
||||
const MG_State::GLState::SpirvArtifacts* spirv) {
|
||||
if (RunsAsTheServerRole()) {
|
||||
MG_Pipe::MGPipeMonolithEscapes().CreateShaderState(desc, link, spirv);
|
||||
return;
|
||||
}
|
||||
ClientSession& session = RequireSession("CreateShaderState");
|
||||
if (link == nullptr || spirv == nullptr) {
|
||||
MGLOG_F("MGPipe: Fatal{ArtefactsMissing, \"create_shader_state\"} - the record's "
|
||||
"two typed companions are null. Under monolith the applier reads the "
|
||||
"modules out of spirv->generatedSpirv; under split there is nothing to "
|
||||
"serialise, and emitting the record anyway would create a CSO with no "
|
||||
"code");
|
||||
std::abort();
|
||||
}
|
||||
// EncodeProgramArtifacts APPENDS and never fails - everything it walks is owned
|
||||
// plain data - so an empty archive means the two structs themselves were empty,
|
||||
// which is a linked program with no artefacts and is not a codec question.
|
||||
Vector<Uint8> archive;
|
||||
MG_State::GLState::EncodeProgramArtifacts(*link, *spirv, archive);
|
||||
if (archive.empty()) {
|
||||
MGLOG_F("MGPipe: Fatal{ArchiveEmpty, \"create_shader_state\"} - the program's "
|
||||
"artefacts serialised to nothing");
|
||||
std::abort();
|
||||
}
|
||||
MG_Pipe::MGPProgramDesc record = *desc;
|
||||
for (Uint32 i = 0; i < 6; ++i) record.Spirv[i] = MG_Pipe::MGPBlobRef{};
|
||||
record.Reflection = session.Encoder().StageBytes(archive.data(), archive.size());
|
||||
session.EmitAndWait(MGPWireOp::CreateShaderState, &record, sizeof(record), nullptr, 0,
|
||||
nullptr, 0, nullptr);
|
||||
++g_emitted;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void InstallClientWireTables() {
|
||||
using namespace MG_Pipe;
|
||||
|
||||
// A fresh install means the routed tables are live again: a Start after a previous
|
||||
// session's Stop clears the teardown-refusal flag so its own routed calls are not
|
||||
// refused. (Stop already puts the monolith adapters back and clears the flag; this is
|
||||
// the belt to that braces.)
|
||||
g_clientTablesUninstalled.store(false, std::memory_order_release);
|
||||
|
||||
gMGPipeScreen.ResourceCreate = &Wire_ResourceCreate;
|
||||
gMGPipeScreen.ResourceDestroy = &Wire_ResourceDestroy;
|
||||
gMGPipeScreen.UnmapPersistent = &Wire_UnmapPersistent;
|
||||
|
||||
gMGPipeContext.CreateRenderState = &Wire_CreateRenderState;
|
||||
gMGPipeContext.BindRenderState = &Wire_BindRenderState;
|
||||
gMGPipeContext.DeleteRenderState = &Wire_DeleteRenderState;
|
||||
gMGPipeContext.CreateVertexElements = &Wire_CreateVertexElements;
|
||||
gMGPipeContext.BindVertexElements = &Wire_BindVertexElements;
|
||||
gMGPipeContext.DeleteVertexElements = &Wire_DeleteVertexElements;
|
||||
gMGPipeContext.CreateSamplerState = &Wire_CreateSamplerState;
|
||||
gMGPipeContext.DeleteSamplerState = &Wire_DeleteSamplerState;
|
||||
gMGPipeContext.CreateSamplerView = &Wire_CreateSamplerView;
|
||||
gMGPipeContext.DeleteSamplerView = &Wire_DeleteSamplerView;
|
||||
gMGPipeContext.BindShaderState = &Wire_BindShaderState;
|
||||
gMGPipeContext.DeleteShaderState = &Wire_DeleteShaderState;
|
||||
gMGPipeContext.SetDrawProgram = &Wire_SetDrawProgram;
|
||||
gMGPipeContext.SetDispatchProgram = &Wire_SetDispatchProgram;
|
||||
gMGPipeContext.SetDynamicState = &Wire_SetDynamicState;
|
||||
gMGPipeContext.SetFramebufferState = &Wire_SetFramebufferState;
|
||||
gMGPipeContext.SetVertexBuffers = &Wire_SetVertexBuffers;
|
||||
gMGPipeContext.SetIndexBuffer = &Wire_SetIndexBuffer;
|
||||
gMGPipeContext.SetSamplerViews = &Wire_SetSamplerViews;
|
||||
gMGPipeContext.BindSamplerStates = &Wire_BindSamplerStates;
|
||||
gMGPipeContext.SetShaderImages = &Wire_SetShaderImages;
|
||||
gMGPipeContext.SetGlobalConstants = &Wire_SetGlobalConstants;
|
||||
gMGPipeContext.SetVertexAttribDefaults = &Wire_SetVertexAttribDefaults;
|
||||
gMGPipeContext.SetPixelPackState = &Wire_SetPixelPackState;
|
||||
gMGPipeContext.SetPatchState = &Wire_SetPatchState;
|
||||
gMGPipeContext.SetResidualValueState = &Wire_SetResidualValueState;
|
||||
gMGPipeContext.SetTextureParams = &Wire_SetTextureParams;
|
||||
gMGPipeContext.ResourceSubData = &Wire_ResourceSubData;
|
||||
gMGPipeContext.BufferSubDataResident = &Wire_BufferSubDataResident;
|
||||
gMGPipeContext.ResourceReadback = &Wire_ResourceReadback;
|
||||
|
||||
gMGPipeRouteEscapes.ResourceRespecify = &Wire_Escape_ResourceRespecify;
|
||||
gMGPipeRouteEscapes.ResourceFlushRange = &Wire_Escape_ResourceFlushRange;
|
||||
gMGPipeRouteEscapes.MapPersistent = &Wire_Escape_MapPersistent;
|
||||
gMGPipeRouteEscapes.CreateShaderState = &Wire_Escape_CreateShaderState;
|
||||
|
||||
MGPipeNoteInstalledArm(MGPipeRouteArm::kClientWire);
|
||||
}
|
||||
|
||||
void UninstallClientWireTables() {
|
||||
// MARK THE ROUTED TABLES UNINSTALLED (codex 4). It does NOT reinstall the monolith
|
||||
// adapters, and that is the whole fix: round 2 reinstalled them here, at the TOP of
|
||||
// Stop, so a routed GL-thread call arriving during teardown ran the applier on the
|
||||
// caller - the forbidden path - and moved no wire ordinal, with no refusal. Raising the
|
||||
// flag leaves the Wire_* rows in place; the next routed call reaches RequireSession,
|
||||
// reads the flag and aborts by name (Fatal{ClientTablesUninstalled}) before it touches a
|
||||
// ring that Stop is about to free. The monolith adapters are put back only once teardown
|
||||
// is complete, by ReinstallMonolithAfterTeardown, for the at-exit deletes that reach a
|
||||
// process with no session at all. Idempotent: safe to call when nothing was installed.
|
||||
g_clientTablesUninstalled.store(true, std::memory_order_release);
|
||||
}
|
||||
|
||||
void ReinstallMonolithAfterTeardown() {
|
||||
// THE LAST STEP OF ClientSession::Stop, after the rings, segments and transport are gone
|
||||
// and the apply thread has joined. Now a routed call can only be a process that no
|
||||
// longer has a session - the canonical case is ~BufferObject running from an exit
|
||||
// handler (ID-8) - and the monolith adapter, which runs the applier synchronously, is
|
||||
// the correct answer for it, exactly as it is in a pure-monolith build. Clearing the
|
||||
// flag re-enables the (now monolith) rows. A null row here would be an undiagnosed crash
|
||||
// at whatever GL call an exit handler makes; the applier is a defined no-op-or-apply.
|
||||
MG_Pipe::MGPipeInstallMonolithTables();
|
||||
g_clientTablesUninstalled.store(false, std::memory_order_release);
|
||||
}
|
||||
|
||||
Uint64 ClientWireRecordsEmitted() { return g_emitted; }
|
||||
Uint64 ClientWireRecordsDeclined() { return g_declined; }
|
||||
|
||||
} // namespace MobileGL::MG_Remote::Client
|
||||
|
||||
#endif // MOBILEGL_BUILD_DISAGGREGATED
|
||||
@@ -0,0 +1,88 @@
|
||||
// MobileGL - MobileGL/MG_Remote/Client/WireTables.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 ARM OF R-17's ROUTING: the encode twin of `gMGPipeWireRecordApply`. Owner: c1.
|
||||
//
|
||||
// Thirty-seven thin emitters over `ClientSession::EmitAndWait`, installed over the two
|
||||
// generated tables and the escape table that `MG_Pipe/PipeRoute.h` declares, so that under
|
||||
// split every resource, CSO, texture and program record leaves the GL thread as a WIRE RECORD
|
||||
// instead of executing synchronously against a context the apply thread now owns.
|
||||
//
|
||||
// WHAT ARMS `integration-split`. The 21 `DirectGLES.Split.*` entries skip on
|
||||
// `ClientSession::Active() == nullptr`. `Install()` below is called from the END of a
|
||||
// successful `ClientSession::Start()`, on the GL thread (the thread that called
|
||||
// `MG_Backend::Init()`), AFTER the Hello/Welcome handshake, after the first CapsSnapshot has
|
||||
// been adopted, and after the in-process server role's apply thread has been started. That
|
||||
// order is not a preference:
|
||||
// - after the handshake, because an emitter that published before Welcome would be writing
|
||||
// into a ring the peer has not mapped;
|
||||
// - after the first snapshot, because R-8's liveness gates read the caps mirror and a
|
||||
// placeholder mirror consumes nothing, so a record emitted before it would be emitted to a
|
||||
// server this client has not yet been told consumes that family;
|
||||
// - after the apply thread exists, because `EmitAndWait` BLOCKS on `appliedSeq` and nothing
|
||||
// would advance it - a barrier wait with no applier is the 30-second Fatal, not a hang;
|
||||
// - on the GL thread, because that is the only thread that may touch `gPipeInputs` while the
|
||||
// barrier holds (table 3), and installing from the apply thread would publish the table to
|
||||
// the GL thread with no synchronisation at all.
|
||||
// `Uninstall()` runs at the TOP of `Stop()`, before the rings go away, so the last thing any
|
||||
// straggling GL-thread call reaches is the monolith arm rather than a dangling session.
|
||||
|
||||
#pragma once
|
||||
#include <Includes.h>
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
|
||||
namespace MobileGL::MG_Remote::Client {
|
||||
|
||||
// Installs the thirty-seven wire emitters over gMGPipeScreen / gMGPipeContext /
|
||||
// gMGPipeRouteEscapes and records the arm. Idempotent.
|
||||
void InstallClientWireTables();
|
||||
|
||||
// Marks the routed tables uninstalled so a routed call refuses by name
|
||||
// (Fatal{ClientTablesUninstalled, "<row>"}) rather than running the applier on the caller
|
||||
// (codex 4). It does NOT restore the monolith adapters - that is ReinstallMonolithAfterTeardown
|
||||
// below, run only once the session's rings are freed. Idempotent.
|
||||
void UninstallClientWireTables();
|
||||
|
||||
// The LAST step of ClientSession::Stop: after the rings, segments and transport are gone,
|
||||
// puts the monolith adapters back and clears the refusal flag, so the at-exit ~BufferObject
|
||||
// deletes that reach a process with no session run the applier as they do under monolith.
|
||||
void ReinstallMonolithAfterTeardown();
|
||||
|
||||
// How many records the thirty-seven emitters have published. It counts the ROUTED rows
|
||||
// only - a resource_create, a set_vertex_buffers, a create_shader_state - and never the
|
||||
// five class-B verbs, so it is the one number that says "the resource/CSO/state path really
|
||||
// ran" as opposed to "a Clear crossed".
|
||||
//
|
||||
// WHAT IT IS NOT (M7, corrected). This is NOT what arms the split lane. WireTables.h round 2
|
||||
// claimed it was "t1's FOURTH arming fact", and that was wrong: the harness reads the
|
||||
// encoder's EmitSeq (Harness/SplitRuntimePeek.cpp:50, Harness/ScenarioFixture.h:85), which
|
||||
// moves for the class-B verbs too. So an armed Clear-only lane is green on EmitSeq while this
|
||||
// counter stays 0. Making the harness read THIS instead is t1's file (SplitRuntimePeek), so
|
||||
// the honest statement is the one here: c1 counts the routed ordinal at the only place that
|
||||
// can, PipeFill's InitialBytesNotCarried self-check reads it (PipeFill.cpp:755), and the
|
||||
// lane's own arming remains EmitSeq until t1 re-points it. c1-v3.md M7 has the full note.
|
||||
Uint64 ClientWireRecordsEmitted();
|
||||
|
||||
// How many of those were refused by the server, by acceptance row. Counted rather than
|
||||
// inferred, R-8's rule one level out.
|
||||
Uint64 ClientWireRecordsDeclined();
|
||||
|
||||
// TRUE ON THE SERVER ROLE's OWN THREAD (the apply thread), false everywhere else. It is
|
||||
// v1's ServerLoop::OnApplyThread(), exposed here because it is table 3's role split made
|
||||
// into one predicate and TWO packages read it: the wire emitters below (a routed call that
|
||||
// finds itself on the apply thread runs the monolith adapter, because on that thread this
|
||||
// process IS the server), and MG_Impl/Pipe/PipeFill.cpp's split-only respecify/flush
|
||||
// branches (M5: those branches emit CLIENT wire records and must not run on the server's
|
||||
// apply thread, which produces none - the InitialBytesNotCarried self-check would abort the
|
||||
// server otherwise). Declared here so PipeFill does not have to include a server header.
|
||||
Bool RunsAsTheServerRole();
|
||||
|
||||
} // namespace MobileGL::MG_Remote::Client
|
||||
|
||||
#endif // MOBILEGL_BUILD_DISAGGREGATED
|
||||
@@ -6,31 +6,26 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
// P5 c0 stubs for package v1 (with p1 for the stamp rule).
|
||||
// P5 package v1: the applier bridge, and the consumer for contract 7's five class-B verbs.
|
||||
|
||||
#include "PipeApplier.h"
|
||||
|
||||
#include "../Transport/ReplySlot.h"
|
||||
|
||||
#include <Config.h>
|
||||
#include <MG_Backend/MGPipe/PipeInputs.h>
|
||||
#include <MG_Util/Debug/Log.h>
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
namespace MobileGL::MG_Remote::Server {
|
||||
|
||||
#define MGP5_C0_STUB(what) \
|
||||
do { \
|
||||
MGLOG_F("MGPipe: Fatal{UnimplementedPipeApplier, \"%s\"} - P5 package v1 has not landed " \
|
||||
"this yet; c0 shipped the signature only", \
|
||||
what); \
|
||||
std::abort(); \
|
||||
} while (0)
|
||||
|
||||
ReplyPool::ReplyPool(void* base, Uint64 sizeBytes, Uint32 slotCount, Uint32 slotBytes)
|
||||
: m_base(static_cast<Uint8*>(base)), m_size(sizeBytes), m_slots(slotCount), m_slotBytes(slotBytes) {}
|
||||
|
||||
// PACKAGE s1's, not v1's, even though the class is declared in v1's header: the SEG_REPLY
|
||||
// slot pool is s1's deliverable (BRIEF §5) and its addressing lives in one place,
|
||||
// slot pool is s1's deliverable (BRIEF 5) and its addressing lives in one place,
|
||||
// Transport/ReplySlot.h, which the CLIENT reads the same slots back through. Duplicating
|
||||
// `seq % slots` on this side is how the two halves come to disagree about which slot an
|
||||
// answer is in - and because seq IS the reply-slot id (R-3), a disagreement reads another
|
||||
@@ -47,21 +42,324 @@ namespace MobileGL::MG_Remote::Server {
|
||||
|
||||
Uint32 ReplyPool::SlotBytes() const { return m_slotBytes; }
|
||||
|
||||
// -----------------------------------------------------------------------------------
|
||||
// ServerVerbSink - the five class-B verbs
|
||||
// -----------------------------------------------------------------------------------
|
||||
|
||||
void ServerVerbSink::SetBackend(MG_Backend::BackendObject* backend) { m_backend = backend; }
|
||||
|
||||
const MG_Backend::GlobalBackendFunctionsTable* ServerVerbSink::Table(const char* verb) const {
|
||||
if (m_backend == nullptr) {
|
||||
// DECLINE BY NAME, DO NOT DEREFERENCE. A verb that arrives before
|
||||
// ServerLoop::CreateBackend has run means the hook order changed under us, and the
|
||||
// honest answer is "this build did not apply it" - which DecodeAndApply reports as
|
||||
// false and the lane sees as a record that did not render, rather than as a crash
|
||||
// with no line saying which verb was first.
|
||||
MGLOG_E_ONCE("MG_Remote server: %s arrived with no backend object; the verb is "
|
||||
"DECLINED. ServerLoop::CreateBackend runs from MG_Backend::Init()'s "
|
||||
"hook, before ClientSession::Start",
|
||||
verb);
|
||||
return nullptr;
|
||||
}
|
||||
return &m_backend->GetBackendFunctions();
|
||||
}
|
||||
|
||||
Bool ServerVerbSink::OnClear(const MG_Pipe::MGPClear& clear) {
|
||||
const MG_Backend::GlobalBackendFunctionsTable* table = Table("clear");
|
||||
if (table == nullptr) return false;
|
||||
const MG_Backend::GLFunctionsTable& gl = table->GL;
|
||||
|
||||
// THE FBO HANDLE IS NOT RESOLVED HERE, AND THAT IS THE RULING RATHER THAN AN OMISSION.
|
||||
// MGPClear::Fbo names the framebuffer the clear belongs to, but the BINDING is already
|
||||
// server state: set_framebuffer_state (op 33) arrives ahead of the clear and the
|
||||
// applier has bound it. Re-resolving the handle to a frontend FramebufferObject here
|
||||
// would need the SharedPtr the four ClearNamedFramebuffer* entries take - a frontend
|
||||
// heap reference that table 2 lists as one of the six fields with no wire carrier. So
|
||||
// P5 clears THE BOUND FRAMEBUFFER, which for the reduced path (default FBO) is exactly
|
||||
// right, and the named form is P7's along with the handle it needs.
|
||||
switch (clear.Kind) {
|
||||
case kMGPClearKindWhole:
|
||||
if (gl.Clear == nullptr) return false;
|
||||
gl.Clear(static_cast<GLbitfield>(clear.BufferMask));
|
||||
break;
|
||||
case kMGPClearKindColor:
|
||||
switch (clear.ValueClass) {
|
||||
case kMGPClearValueClassFloat:
|
||||
if (gl.ClearBufferfv == nullptr) return false;
|
||||
gl.ClearBufferfv(GL_COLOR, clear.DrawBufferIndex,
|
||||
reinterpret_cast<const GLfloat*>(clear.ColorValue));
|
||||
break;
|
||||
case kMGPClearValueClassInt:
|
||||
if (gl.ClearBufferiv == nullptr) return false;
|
||||
gl.ClearBufferiv(GL_COLOR, clear.DrawBufferIndex,
|
||||
reinterpret_cast<const GLint*>(clear.ColorValue));
|
||||
break;
|
||||
case kMGPClearValueClassUint:
|
||||
if (gl.ClearBufferuiv == nullptr) return false;
|
||||
gl.ClearBufferuiv(GL_COLOR, clear.DrawBufferIndex,
|
||||
reinterpret_cast<const GLuint*>(clear.ColorValue));
|
||||
break;
|
||||
default:
|
||||
// A value class outside the three is a wire fault, not a fallback: all three
|
||||
// representations of a clear colour are numerically populated by the frontend
|
||||
// and only this field says which one the backend must use, so guessing renders
|
||||
// a plausible wrong colour.
|
||||
Wire::WireProtocolFatalAt("MGPClear::ValueClass", clear.ValueClass, 3);
|
||||
}
|
||||
break;
|
||||
case kMGPClearKindDepth:
|
||||
if (gl.ClearBufferfv == nullptr) return false;
|
||||
gl.ClearBufferfv(GL_DEPTH, 0, &clear.DepthValue);
|
||||
break;
|
||||
case kMGPClearKindStencil:
|
||||
if (gl.ClearBufferiv == nullptr) return false;
|
||||
gl.ClearBufferiv(GL_STENCIL, 0, &clear.StencilValue);
|
||||
break;
|
||||
case kMGPClearKindDepthStencil:
|
||||
if (gl.ClearBufferfi == nullptr) return false;
|
||||
gl.ClearBufferfi(GL_DEPTH_STENCIL, 0, clear.DepthValue, clear.StencilValue);
|
||||
break;
|
||||
default:
|
||||
Wire::WireProtocolFatalAt("MGPClear::Kind", clear.Kind, kMGPClearKindDepthStencil + 1);
|
||||
}
|
||||
++m_clears;
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool ServerVerbSink::OnBlit(const MG_Pipe::MGPBlit& blit) {
|
||||
const MG_Backend::GlobalBackendFunctionsTable* table = Table("blit");
|
||||
if (table == nullptr) return false;
|
||||
if (table->GL.BlitFramebuffer == nullptr) return false;
|
||||
// Same ruling as OnClear's: the read and draw framebuffers are already bound by the
|
||||
// set_framebuffer_state records that preceded this one, so the unnamed entry point is
|
||||
// the one that matches what the server's state actually is. BlitNamedFramebuffer needs
|
||||
// two frontend SharedPtrs, which table 2 lists as uncarried.
|
||||
table->GL.BlitFramebuffer(blit.SrcX0, blit.SrcY0, blit.SrcX1, blit.SrcY1, blit.DstX0,
|
||||
blit.DstY0, blit.DstX1, blit.DstY1,
|
||||
static_cast<GLbitfield>(blit.Mask),
|
||||
static_cast<GLenum>(blit.Filter));
|
||||
++m_blits;
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool ServerVerbSink::OnPresent(const MG_Pipe::MGPPresent& present) {
|
||||
const MG_Backend::GlobalBackendFunctionsTable* table = Table("present");
|
||||
if (table == nullptr) return false;
|
||||
if (table->Present == nullptr) return false;
|
||||
// Present is the ONLY frame-boundary drain the backend has (DirectGLES.cpp:12424-12470:
|
||||
// the fence poll, the four ring OnPresent hooks, TrimBufferPool, PipeStats::OnPresent),
|
||||
// which is why ARCHITECTURE.md:531 insists present <-> eglSwapBuffers stays strictly
|
||||
// 1:1. It is NOT eglSwapBuffers itself: the swap is the client's EGL call and crosses
|
||||
// as the SwapEGLBuffers control request, which runs Present on this thread through this
|
||||
// same table. Both paths therefore end here and the 1:1 is structural.
|
||||
table->Present();
|
||||
++m_presents;
|
||||
// FrameSerial 0 means "the server stamps its own" (c1-v1 8.3): P5 has no client-side
|
||||
// present credit, so the client sends 0 and the frame count on this side IS the serial.
|
||||
m_lastPresentSerial = present.FrameSerial != 0 ? present.FrameSerial : m_presents;
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool ServerVerbSink::OnReadPixels(const MG_Pipe::MGPReadbackInfo& info, Uint64 seq,
|
||||
Wire::ReplySink* replies) {
|
||||
if (replies == nullptr) {
|
||||
// The decoder always passes its ReplySink; a null one means the applier was built
|
||||
// without a reply pool, and answering nothing would leave the client's barrier
|
||||
// waiting for a slot that never gets stamped - a hang, not a wrong picture.
|
||||
MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"read_pixels without a reply sink\"} - "
|
||||
"the pixels' only destination in P5 is SEG_REPLY (contract table 1 row 23) "
|
||||
"and a client blocked on seq %llu would never be answered",
|
||||
static_cast<unsigned long long>(seq));
|
||||
std::abort();
|
||||
}
|
||||
const MG_Backend::GlobalBackendFunctionsTable* table = Table("read_pixels");
|
||||
if (table == nullptr || table->GL.ReadPixels == nullptr) {
|
||||
// DECLINED IS A REAL ANSWER (table 0's slot-header row) and it is the RIGHT one
|
||||
// here: the client is parked on this seq inside the verb barrier, so returning
|
||||
// false without posting would convert "not implemented" into "never returns".
|
||||
replies->PostReply(seq, Wire::ReplySink::kStatusDeclined, nullptr, 0);
|
||||
return false;
|
||||
}
|
||||
if (info.DstSize == 0) {
|
||||
replies->PostReply(seq, Wire::ReplySink::kStatusError, nullptr, 0);
|
||||
return false;
|
||||
}
|
||||
// THE CLIENT DECLARES THE BYTE COUNT AND THE SERVER DOES NOT RECOMPUTE IT. DstSize is
|
||||
// sized on the client from the same GL_PACK_* state the frontend owns, and it is what
|
||||
// the client will read back out of the slot; a server that recomputed from Box x
|
||||
// Format x Type would be a SECOND opinion about a pack alignment the client half owns,
|
||||
// and the two disagreeing is a short read with plausible pixels in it.
|
||||
if (info.DstSize > m_readbackScratch.size()) {
|
||||
m_readbackScratch.resize(static_cast<SizeT>(info.DstSize));
|
||||
}
|
||||
table->GL.ReadPixels(info.Box.X, info.Box.Y, static_cast<GLsizei>(info.Box.W),
|
||||
static_cast<GLsizei>(info.Box.H), static_cast<GLenum>(info.Format),
|
||||
static_cast<GLenum>(info.Type), m_readbackScratch.data());
|
||||
replies->PostReply(seq, Wire::ReplySink::kStatusOk, m_readbackScratch.data(), info.DstSize);
|
||||
m_readbackBytes += info.DstSize;
|
||||
++m_readbacks;
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool ServerVerbSink::OnDrawVbo(const MG_Pipe::MGPDrawInfo& info,
|
||||
const MG_Pipe::MGPDrawRange* ranges,
|
||||
const MG_Pipe::MGHostSpan* userIndices) {
|
||||
const MG_Backend::GlobalBackendFunctionsTable* table = Table("draw_vbo");
|
||||
if (table == nullptr) return false;
|
||||
if (userIndices != nullptr) {
|
||||
// kCapNeedsHostIndexBytes is 0 for the whole of P5 by ruling (table 0's cap-bit
|
||||
// row) precisely so this tail never appears; a span that arrived anyway means the
|
||||
// client's cap gate did not hold, and filling one is P8's.
|
||||
MGLOG_E_ONCE("MG_Remote server: draw_vbo carries an MGHostSpan of user indices. P5 "
|
||||
"rules kCapNeedsHostIndexBytes and kCapNeedsHostUboBytes to 0 so that "
|
||||
"no host span reaches the first IPC frame (contract table 0); filling "
|
||||
"one under split is P8's. The draw is DECLINED rather than drawn from "
|
||||
"a pointer that does not belong to this process");
|
||||
return false;
|
||||
}
|
||||
if (ranges == nullptr || info.NumDraws == 0) return false;
|
||||
|
||||
// P5 IMPLEMENTS THE TWO SHAPES ITS REDUCED PATH USES AND DECLINES THE REST BY NAME.
|
||||
// draw_vbo collapses all twenty draw entry points, and picking the right one needs the
|
||||
// instancing / base-vertex / base-instance / multi-draw cross product. TriangleScenario
|
||||
// is a single non-instanced array draw and OpenRA's are single indexed draws from a
|
||||
// bound element buffer; the rest are P8's, together with the MGPDrawIndirect record
|
||||
// that has no producer yet.
|
||||
const MG_Backend::GLFunctionsTable& gl = table->GL;
|
||||
const Bool instanced = info.InstanceCount > 1 || info.StartInstance != 0;
|
||||
if (info.NumDraws != 1 || instanced) {
|
||||
MGLOG_E_ONCE("MG_Remote server: draw_vbo with NumDraws=%u InstanceCount=%u "
|
||||
"StartInstance=%u is DECLINED - P5's reduced path is the single "
|
||||
"non-instanced draw (BRIEF 4); the multi-draw and instanced arms are "
|
||||
"P8's",
|
||||
info.NumDraws, info.InstanceCount, info.StartInstance);
|
||||
return false;
|
||||
}
|
||||
const MG_Pipe::MGPDrawRange& range = ranges[0];
|
||||
if (info.IndexSize == 0) {
|
||||
if (gl.DrawArrays == nullptr) return false;
|
||||
gl.DrawArrays(static_cast<GLenum>(info.Mode), static_cast<GLint>(range.Start),
|
||||
static_cast<GLsizei>(range.Count));
|
||||
} else {
|
||||
if (gl.DrawElementsBaseVertex == nullptr) return false;
|
||||
GLenum indexType = GL_UNSIGNED_INT;
|
||||
switch (info.IndexSize) {
|
||||
case 1: indexType = GL_UNSIGNED_BYTE; break;
|
||||
case 2: indexType = GL_UNSIGNED_SHORT; break;
|
||||
case 4: indexType = GL_UNSIGNED_INT; break;
|
||||
default:
|
||||
// IndexSize is "0 = arrays, else 1 / 2 / 4" (MGPipeTypes.h:1323) and nothing
|
||||
// else is a legal width; defaulting to 4 would read past the element buffer.
|
||||
Wire::WireProtocolFatalAt("MGPDrawInfo::IndexSize", info.IndexSize, 4);
|
||||
}
|
||||
// Start is the FIRST INDEX, so the byte offset into the bound element buffer is
|
||||
// Start * IndexSize - the same arithmetic PipeFill's emitter inverted.
|
||||
const auto offset = static_cast<std::uintptr_t>(range.Start) * info.IndexSize;
|
||||
gl.DrawElementsBaseVertex(static_cast<GLenum>(info.Mode),
|
||||
static_cast<GLsizei>(range.Count), indexType,
|
||||
reinterpret_cast<const void*>(offset), range.IndexBias);
|
||||
}
|
||||
++m_draws;
|
||||
return true;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------
|
||||
// PipeApplier
|
||||
// -----------------------------------------------------------------------------------
|
||||
|
||||
PipeApplier::PipeApplier(Wire::SegmentTable* segments, ReplyPool* replies)
|
||||
: m_segments(segments), m_replies(replies) {}
|
||||
|
||||
Bool PipeApplier::ApplyOne(const Transport::RingRecordView&) { MGP5_C0_STUB("PipeApplier::ApplyOne"); }
|
||||
|
||||
void PipeApplier::StampVerbBoundary(MG_Pipe::MGPWireOp) {
|
||||
MGP5_C0_STUB("PipeApplier::StampVerbBoundary");
|
||||
void PipeApplier::Attach(Transport::RingControl* control, MG_Backend::BackendObject* backend) {
|
||||
if (control == nullptr || m_segments == nullptr) {
|
||||
MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"PipeApplier::Attach\"} - no control "
|
||||
"page or no segment table; ServerSession::Accept builds both before the "
|
||||
"apply thread starts");
|
||||
std::abort();
|
||||
}
|
||||
m_verbs.SetBackend(backend);
|
||||
m_decoder = Wire::PipeWireDecoder(control, m_segments, m_replies);
|
||||
m_decoder.SetVerbSink(&m_verbs);
|
||||
m_attached = true;
|
||||
}
|
||||
|
||||
Uint64 PipeApplier::ResidualPullCount() const { return m_residualPulls; }
|
||||
|
||||
void PipeApplier::PoisonRetiredStageBytes(Uint64, Uint64) {
|
||||
MGP5_C0_STUB("PipeApplier::PoisonRetiredStageBytes");
|
||||
void PipeApplier::Detach() {
|
||||
m_decoder = Wire::PipeWireDecoder();
|
||||
m_verbs.SetBackend(nullptr);
|
||||
m_attached = false;
|
||||
}
|
||||
|
||||
#undef MGP5_C0_STUB
|
||||
Bool PipeApplier::Attached() const { return m_attached; }
|
||||
|
||||
Bool PipeApplier::ApplyOne(const Transport::RingRecordView& record) {
|
||||
if (!m_attached) {
|
||||
MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"PipeApplier::ApplyOne before Attach\"} "
|
||||
"- a record reached the applier with no decoder; the apply thread calls "
|
||||
"Attach once before its first pop");
|
||||
std::abort();
|
||||
}
|
||||
// ORDER IS THE CONTRACT'S: stamp, then apply. The stamp is what makes any server-side
|
||||
// read of gPipeInputs legal at all (PipeApplier.h's block 1), so a record applied
|
||||
// before it aborts on the FIRST field inside SyncRenderState.
|
||||
StampVerbBoundary(static_cast<MG_Pipe::MGPWireOp>(record.kind));
|
||||
const Bool applied = m_decoder.DecodeAndApply(record);
|
||||
// AND THE CLEAR IS INSIDE ApplyOne, NOT AFTER THE DRAIN BATCH. That is not tidiness,
|
||||
// it is the barrier invariant. s1's SessionConsumer::ApplyOne publishes appliedSeq the
|
||||
// instant this returns, and publishing appliedSeq is what makes the CLIENT runnable
|
||||
// again (R-1: the barrier waits on exactly that watermark). A clear that ran after the
|
||||
// batch would therefore be a second writer of gPipeInputs while the client is already
|
||||
// touching it - the one thing table 3 says may not be introduced before the barrier
|
||||
// retires - and the first version of this file had it there. It was caught by
|
||||
// AClearRecordCrossesAndIsStampedAsAVerbBoundary failing INTERMITTENTLY, which is what
|
||||
// a race looks like from the outside.
|
||||
//
|
||||
// THE COST, STATED: a record that is NOT a verb boundary now applies with the flag
|
||||
// disarmed, so a sticky forward pulled from inside such a record's applier is not
|
||||
// counted in `rsp`. Closing that needs an "enter the applier" entry point beside
|
||||
// MGPipeServerStampVerbBoundary that arms the flag WITHOUT re-stamping - re-stamping on
|
||||
// a non-verb op is what p1 forbids outright - and PipeInputs.cpp is p1's file. Left for
|
||||
// the integrator to sequence; it makes `rsp` larger, never smaller, so the number this
|
||||
// phase reports is a floor.
|
||||
LeaveApplier();
|
||||
return applied;
|
||||
}
|
||||
|
||||
// p1's rule verbatim (p1-v1 2). MGPipeVerbForWireOp is generated from MGP_VERB_OP_LIST in
|
||||
// FieldOwnership.def and answers kVerbCount for every op that is NOT a verb boundary, so
|
||||
// calling it unconditionally on every record is both correct and cheap. Four ops stamp:
|
||||
// Clear -> Clear, DrawVbo -> DrawArrays, ReadPixels -> ReadPixels, Blit -> BlitFramebuffer.
|
||||
//
|
||||
// PRESENT IS DELIBERATELY NOT ONE, although contract 7 puts it in class B: FillPoints.def:21
|
||||
// says Present and SetSwapInterval "go through BackendObject virtuals and read no frontend
|
||||
// state, so they are not verbs here". There is no MGPipeVerb::Present, and stamping there
|
||||
// would retire the previous verb's answers with nothing to put in their place.
|
||||
void PipeApplier::StampVerbBoundary(MG_Pipe::MGPWireOp op) {
|
||||
const MG_Pipe::MGPipeVerb verb = MG_Pipe::MGPipeVerbForWireOp(op);
|
||||
if (verb == MG_Pipe::MGPipeVerb::kVerbCount) return; // not a verb boundary: stamp nothing
|
||||
MG_Pipe::MGPipeServerStampVerbBoundary(verb);
|
||||
}
|
||||
|
||||
void PipeApplier::LeaveApplier() { MG_Pipe::MGPipeServerClearVerbBoundary(); }
|
||||
|
||||
Uint64 PipeApplier::ResidualPullCount() const { return MG_Pipe::MGPipeResidualPullCount(); }
|
||||
|
||||
// The decoder poisons EXACTLY the runs it resolved, from inside DecodeAndApply, once the
|
||||
// applier has returned - so this entry point is the manual one, for a caller that knows a
|
||||
// range is dead and is not the decoder. It is kept because c0's signature block declares
|
||||
// it and because the R-11 copy in Managers.cpp is verified by poisoning a range by hand in
|
||||
// a unit case; nothing on the live path calls it.
|
||||
void PipeApplier::PoisonRetiredStageBytes(Uint64 offset, Uint64 size) {
|
||||
if (size == 0 || m_segments == nullptr) return;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
if (!MG_Config::Ipc.Audit) return;
|
||||
#endif
|
||||
const void* run = m_segments->Resolve(Wire::kSegStage, offset, size);
|
||||
if (run == nullptr) return;
|
||||
std::memset(const_cast<void*>(run), 0xDD, static_cast<SizeT>(size));
|
||||
}
|
||||
|
||||
Uint64 PipeApplier::PoisonedStageBytes() const { return m_decoder.PoisonedStageBytes(); }
|
||||
|
||||
Uint64 PipeApplier::DecoderAppliedSeq() const { return m_decoder.AppliedSeq(); }
|
||||
|
||||
} // namespace MobileGL::MG_Remote::Server
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
#pragma once
|
||||
#include <Includes.h>
|
||||
|
||||
#include <MG_Backend/BackendObject.h>
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
|
||||
#include "../Transport/Ring.h"
|
||||
@@ -69,34 +70,147 @@ namespace MobileGL::MG_Remote::Server {
|
||||
Uint32 m_slotBytes = 0;
|
||||
};
|
||||
|
||||
// ---- MGPClear's two discriminants ---------------------------------------------------
|
||||
//
|
||||
// MGPClear (MGPipeTypes.h:1271) names `Kind` "Whole | Color | Depth | Stencil |
|
||||
// DepthStencil" and `ValueClass` "Float | Int | Uint" IN A COMMENT AND NOWHERE ELSE: the
|
||||
// catalogue ships no enum for either, and the record has no producer or consumer in the
|
||||
// tree, so P5 writes both halves and the two halves have to agree on a number. Declaring
|
||||
// them here rather than open-coding 0..4 on each side is table 0's own rule for exactly
|
||||
// this shape ("a decoder that open-codes it is the class-1 defect"), applied to a field
|
||||
// table 0 did not reach.
|
||||
//
|
||||
// THE ORDER IS THE COMMENT'S, LEFT TO RIGHT, and ValueClass reuses the numbering
|
||||
// MG_State/GLState/Core.h:39-41 already gives the identical three-way split on
|
||||
// MGPAttribValue::ValueClass. c1 encodes against these constants; a disagreement is a
|
||||
// clear of the wrong attachment with the wrong value type, which renders plausibly.
|
||||
// FLAGGED FOR THE INTEGRATOR: this belongs in MGPipeTypes.h, which is c0's file.
|
||||
inline constexpr Uint32 kMGPClearKindWhole = 0; // glClear(mask)
|
||||
inline constexpr Uint32 kMGPClearKindColor = 1; // glClearBuffer{f,i,ui}v(GL_COLOR, i, v)
|
||||
inline constexpr Uint32 kMGPClearKindDepth = 2; // glClearBufferfv(GL_DEPTH, 0, &d)
|
||||
inline constexpr Uint32 kMGPClearKindStencil = 3; // glClearBufferiv(GL_STENCIL, 0, &s)
|
||||
inline constexpr Uint32 kMGPClearKindDepthStencil = 4; // glClearBufferfi(GL_DEPTH_STENCIL,...)
|
||||
inline constexpr Uint32 kMGPClearValueClassFloat = 0;
|
||||
inline constexpr Uint32 kMGPClearValueClassInt = 1;
|
||||
inline constexpr Uint32 kMGPClearValueClassUint = 2;
|
||||
|
||||
// ---- the five class-B verbs' consumer ------------------------------------------------
|
||||
//
|
||||
// Contract §7 class B is Clear (57), Blit (56), ReadPixels (58), DrawVbo (59) and Present
|
||||
// (67), and NONE of them has an MGPipeApply* entry point - the 37 that exist are the object
|
||||
// and state families. So w1's decoder validates and hands over a checked argument list and
|
||||
// stops, and this is the other half: the SERVER'S OWN BACKEND CALL, through the private
|
||||
// GlobalBackendFunctionsTable ServerLoop holds. It is not gBackendFunctionsTable, which in
|
||||
// a split process is the client's emit table (table 3) - calling THAT here would re-emit
|
||||
// the record the server is in the middle of applying, which is an infinite loop that
|
||||
// renders nothing and looks like a hang.
|
||||
class ServerVerbSink final : public Wire::WireVerbSink {
|
||||
public:
|
||||
// The server's private backend. Null until ServerLoop::CreateBackend has run, and a
|
||||
// verb that arrives before then declines by name rather than dereferencing.
|
||||
void SetBackend(MG_Backend::BackendObject* backend);
|
||||
|
||||
Bool OnClear(const MG_Pipe::MGPClear& clear) override;
|
||||
Bool OnBlit(const MG_Pipe::MGPBlit& blit) override;
|
||||
Bool OnPresent(const MG_Pipe::MGPPresent& present) override;
|
||||
Bool OnReadPixels(const MG_Pipe::MGPReadbackInfo& info, Uint64 seq,
|
||||
Wire::ReplySink* replies) override;
|
||||
Bool OnDrawVbo(const MG_Pipe::MGPDrawInfo& info, const MG_Pipe::MGPDrawRange* ranges,
|
||||
const MG_Pipe::MGHostSpan* userIndices) override;
|
||||
|
||||
// Per-verb tallies. The lane asserts these moved, because "the scenario passed" on a
|
||||
// split build is also what a scenario that ran entirely on the monolith path looks
|
||||
// like (R-16: a probe may not arm against a stub).
|
||||
Uint64 Clears() const { return m_clears; }
|
||||
Uint64 Draws() const { return m_draws; }
|
||||
Uint64 Readbacks() const { return m_readbacks; }
|
||||
Uint64 Blits() const { return m_blits; }
|
||||
Uint64 Presents() const { return m_presents; }
|
||||
Uint64 LastPresentSerial() const { return m_lastPresentSerial; }
|
||||
Uint64 ReadbackBytes() const { return m_readbackBytes; }
|
||||
|
||||
private:
|
||||
const MG_Backend::GlobalBackendFunctionsTable* Table(const char* verb) const;
|
||||
|
||||
MG_Backend::BackendObject* m_backend = nullptr;
|
||||
Uint64 m_clears = 0;
|
||||
Uint64 m_draws = 0;
|
||||
Uint64 m_readbacks = 0;
|
||||
Uint64 m_blits = 0;
|
||||
Uint64 m_presents = 0;
|
||||
Uint64 m_lastPresentSerial = 0;
|
||||
Uint64 m_readbackBytes = 0;
|
||||
// ReadPixels' destination. The pixels go into the reply slot, but GLFunctionsTable::
|
||||
// ReadPixels writes into a caller buffer, so one staging vector per session sits
|
||||
// between them. Grown, never shrunk, and never handed out past the call.
|
||||
Vector<Uint8> m_readbackScratch;
|
||||
};
|
||||
|
||||
class PipeApplier {
|
||||
public:
|
||||
PipeApplier() = default;
|
||||
PipeApplier(Wire::SegmentTable* segments, ReplyPool* replies);
|
||||
|
||||
// Decode one record, stamp the verb, apply, post the reply if the call has one, then
|
||||
// advance appliedSeq by exactly one. P5 FORBIDS BATCHING appliedSeq (R-9): the barrier's
|
||||
// waiter reads it, and a batched watermark promises work that has not run.
|
||||
// Builds the decoder over the session's control page and points it at this applier's
|
||||
// verb sink. Separate from the constructor because ServerSession::Accept constructs the
|
||||
// applier before it has decided anything about the apply thread, and the decoder needs
|
||||
// the RingControl the constructor was never given.
|
||||
//
|
||||
// CALLED ON THE APPLY THREAD, ONCE, BEFORE THE FIRST RECORD. PipeWireDecoder is "not
|
||||
// thread safe: one decoder on the apply thread, by construction", and its constructor
|
||||
// installs the process-wide apply hook.
|
||||
void Attach(Transport::RingControl* control, MG_Backend::BackendObject* backend);
|
||||
void Detach();
|
||||
Bool Attached() const;
|
||||
|
||||
// Decode one record, stamp the verb, apply, post the reply if the call has one. THE
|
||||
// CALLER advances appliedSeq by exactly one, through s1's SessionConsumer::ApplyOne,
|
||||
// which is that watermark's single writer; P5 FORBIDS BATCHING it (R-9), because the
|
||||
// barrier's waiter reads it and a batched watermark promises work that has not run.
|
||||
Bool ApplyOne(const Transport::RingRecordView& record);
|
||||
|
||||
// p1's rule, v1's call site. Called at the verb boundary, before the record's applier
|
||||
// runs, with the verb the record belongs to.
|
||||
void StampVerbBoundary(MG_Pipe::MGPWireOp op);
|
||||
|
||||
// MANDATORY on leaving the applier (p1's M-5, PipeInputs.h's ServerStampedVerb block).
|
||||
// The client's MGPipeValidateForVerb / MGPipeLeaveVerb also clear it, which is enough
|
||||
// for inproc and NOT enough for a spawned server, where MG_Impl is not in the process:
|
||||
// there the flag would latch TRUE for the server's life, every later read anywhere
|
||||
// would be judged against the last verb's mask, and the sticky forwards would start
|
||||
// aborting under strict on exactly the case their exemption exists for.
|
||||
void LeaveApplier();
|
||||
|
||||
// R-7.2's counter, read by the gate. A BARRIER-PULLED field read on the server side
|
||||
// increments PipeStats::CallClass::ResidualPulls (short name `rsp`); its value at the
|
||||
// end of P5 IS the size of the P6/P7/P8 debt and goes into MEASUREMENTS.
|
||||
//
|
||||
// IT FORWARDS TO MGPipeResidualPullCount() AND KEEPS NO MEMBER OF ITS OWN. The member
|
||||
// c0's signature block declared is deleted rather than wired: the counter is
|
||||
// process-wide in PipeInputs.cpp because the reads that increment it happen inside the
|
||||
// BACKEND, arbitrarily deep under an MGPipeApply* call, with no PipeApplier in scope.
|
||||
// A second copy here could only ever be a number that disagreed with the one the exit
|
||||
// gate reads (p1-v1 5).
|
||||
Uint64 ResidualPullCount() const;
|
||||
|
||||
// R-11's audit: after a record retires, fill the SEG_STAGE bytes it referenced with
|
||||
// 0xDD. Only under MOBILEGL_IPC_AUDIT=1, because it costs a write of every staged byte.
|
||||
void PoisonRetiredStageBytes(Uint64 offset, Uint64 size);
|
||||
|
||||
// How many staged bytes the decoder has poisoned, and how many records it has applied -
|
||||
// the two numbers the audit lane asserts are non-zero, since an instrumentation that
|
||||
// cannot be observed to have run is decoration.
|
||||
Uint64 PoisonedStageBytes() const;
|
||||
Uint64 DecoderAppliedSeq() const;
|
||||
ServerVerbSink& Verbs() { return m_verbs; }
|
||||
const ServerVerbSink& Verbs() const { return m_verbs; }
|
||||
|
||||
private:
|
||||
Wire::SegmentTable* m_segments = nullptr;
|
||||
ReplyPool* m_replies = nullptr;
|
||||
Wire::PipeWireDecoder m_decoder;
|
||||
Uint64 m_residualPulls = 0;
|
||||
ServerVerbSink m_verbs;
|
||||
Bool m_attached = false;
|
||||
};
|
||||
|
||||
} // namespace MobileGL::MG_Remote::Server
|
||||
|
||||
@@ -6,36 +6,500 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
// P5 c0 stubs for package v1 - the phase's highest-risk package.
|
||||
// P5 package v1: the apply thread, its affinity, its parking, and the EGL ownership move.
|
||||
|
||||
#include "ServerLoop.h"
|
||||
|
||||
#include <Config.h>
|
||||
#include <MG_Backend/BackendObjects.h>
|
||||
#include <MG_Util/Debug/Log.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdlib>
|
||||
#include <cstdio>
|
||||
|
||||
#if defined(__linux__) || defined(__ANDROID__)
|
||||
#include <pthread.h>
|
||||
#include <sched.h>
|
||||
#endif
|
||||
|
||||
namespace MobileGL::MG_Remote::Server {
|
||||
|
||||
#define MGP5_C0_STUB(what) \
|
||||
do { \
|
||||
MGLOG_F("MGPipe: Fatal{UnimplementedServerLoop, \"%s\"} - P5 package v1 has not landed " \
|
||||
"this yet; c0 shipped the signature only", \
|
||||
what); \
|
||||
std::abort(); \
|
||||
} while (0)
|
||||
namespace {
|
||||
|
||||
MobileGLResult ServerLoop::Start(ServerSession&) { MGP5_C0_STUB("ServerLoop::Start"); }
|
||||
// The bounded join. InProcessTransportTest.cpp:344 uses five seconds for the same
|
||||
// reason: a lost wakeup must be a RED TEST and not a hung CI job.
|
||||
constexpr Uint32 kJoinTimeoutMs = 5000;
|
||||
|
||||
void ServerLoop::Stop() { MGP5_C0_STUB("ServerLoop::Stop"); }
|
||||
// A core counts as "big" if its cpufreq ceiling is within 15% of the fastest core's -
|
||||
// the identical rule and the identical constant as ShaderCompilePool.cpp:28 and :73-95.
|
||||
// The probe is re-derived here rather than exported from there because the two want
|
||||
// DIFFERENT ANSWERS from the same data: the pool wants a COUNT (how many workers), and
|
||||
// an affinity wants a MASK (which cpus). DetectBigCoreCount() cannot answer the second,
|
||||
// so exporting it would have meant either a second function in another package's file
|
||||
// or a mask reconstructed from a count, which is wrong on any asymmetric topology whose
|
||||
// big cores are not cpu0..cpuN-1.
|
||||
constexpr Uint64 kBigCoreFrequencyPercent = 85;
|
||||
|
||||
// Not a stub: teardown asks this to decide whether to Kill and join at all, and a teardown
|
||||
// helper that aborts when the thread was never started is a hang in the shutdown path.
|
||||
Bool ServerLoop::Running() const { return m_running; }
|
||||
Uint64 ReadCpuMaxFrequencyKHz(const Uint cpu) {
|
||||
char path[128];
|
||||
std::snprintf(path, sizeof(path),
|
||||
"/sys/devices/system/cpu/cpu%u/cpufreq/cpuinfo_max_freq", cpu);
|
||||
std::FILE* file = std::fopen(path, "r");
|
||||
if (file == nullptr) return 0;
|
||||
unsigned long long value = 0;
|
||||
const int scanned = std::fscanf(file, "%llu", &value);
|
||||
std::fclose(file);
|
||||
return scanned == 1 ? static_cast<Uint64>(value) : 0;
|
||||
}
|
||||
|
||||
MG_Backend::BackendObject* ServerLoop::Backend() { MGP5_C0_STUB("ServerLoop::Backend"); }
|
||||
// The set of cpus whose ceiling is within 15% of the peak, as a bit per cpu. Zero when
|
||||
// the topology cannot be read - Windows, macOS, a container that hides the cpufreq
|
||||
// tree, or a partially readable one - because with no asymmetry information the honest
|
||||
// answer is "do not pin", not "pin to a guess".
|
||||
Uint64 DetectBigCoreMask() {
|
||||
const Uint cpuCount = std::min(64u, std::max(1u, std::thread::hardware_concurrency()));
|
||||
Uint64 frequencies[64] = {};
|
||||
for (Uint cpu = 0; cpu < cpuCount; ++cpu) {
|
||||
frequencies[cpu] = ReadCpuMaxFrequencyKHz(cpu);
|
||||
if (frequencies[cpu] == 0) return 0;
|
||||
}
|
||||
Uint64 peak = 0;
|
||||
for (Uint cpu = 0; cpu < cpuCount; ++cpu) peak = std::max(peak, frequencies[cpu]);
|
||||
if (peak == 0) return 0;
|
||||
const Uint64 threshold = peak * kBigCoreFrequencyPercent / 100;
|
||||
Uint64 mask = 0;
|
||||
for (Uint cpu = 0; cpu < cpuCount; ++cpu) {
|
||||
if (frequencies[cpu] >= threshold) mask |= (1ull << cpu);
|
||||
}
|
||||
return mask;
|
||||
}
|
||||
|
||||
MobileGLResult ServerLoop::RunOnApplyThread(ControlWork, void*) {
|
||||
MGP5_C0_STUB("ServerLoop::RunOnApplyThread");
|
||||
// MOBILEGL_IPC_SERVER_AFFINITY = `auto` | `off` | an explicit mask (0x... or decimal).
|
||||
// The RAW STRING is what Config keeps, because the resolved mask is what gets logged -
|
||||
// "an affinity that silently did nothing looks exactly like one that worked"
|
||||
// (CONTRACT-P5 5).
|
||||
Uint64 RequestedAffinityMask(const char* raw, Bool* outRecognised) {
|
||||
*outRecognised = true;
|
||||
if (raw == nullptr || raw[0] == '\0') return DetectBigCoreMask();
|
||||
if (std::strcmp(raw, "auto") == 0) return DetectBigCoreMask();
|
||||
if (std::strcmp(raw, "off") == 0) return 0;
|
||||
char* end = nullptr;
|
||||
const unsigned long long parsed = std::strtoull(raw, &end, 0);
|
||||
if (end == raw || (end != nullptr && *end != '\0')) {
|
||||
*outRecognised = false;
|
||||
return 0;
|
||||
}
|
||||
return static_cast<Uint64>(parsed);
|
||||
}
|
||||
|
||||
void NameThisThread(const char* name) {
|
||||
#if defined(__linux__) || defined(__ANDROID__)
|
||||
pthread_setname_np(pthread_self(), name);
|
||||
#else
|
||||
(void)name;
|
||||
#endif
|
||||
}
|
||||
|
||||
// Returns the mask that was actually APPLIED, which is 0 when nothing was.
|
||||
Uint64 ApplyAffinity(Uint64 requested) {
|
||||
if (requested == 0) return 0;
|
||||
#if defined(__linux__) || defined(__ANDROID__)
|
||||
cpu_set_t set;
|
||||
CPU_ZERO(&set);
|
||||
Uint applied = 0;
|
||||
for (Uint cpu = 0; cpu < 64; ++cpu) {
|
||||
if ((requested & (1ull << cpu)) != 0) {
|
||||
CPU_SET(cpu, &set);
|
||||
++applied;
|
||||
}
|
||||
}
|
||||
if (applied == 0) return 0;
|
||||
if (sched_setaffinity(0, sizeof(set), &set) != 0) return 0;
|
||||
return requested;
|
||||
#else
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
Uint32 SpinUsFromConfig() {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
return MG_Config::Ipc.SpinUs;
|
||||
#else
|
||||
return Transport::kDefaultSpinUs;
|
||||
#endif
|
||||
}
|
||||
|
||||
const char* AffinityStringFromConfig() {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
return MG_Config::Ipc.ServerAffinity.c_str();
|
||||
#else
|
||||
return "off";
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// ---------------------------------------------------------------------------------
|
||||
// The private backend object
|
||||
// ---------------------------------------------------------------------------------
|
||||
|
||||
MobileGLResult ServerLoop::CreateBackend(BackendType type) {
|
||||
if (m_backend != nullptr) {
|
||||
return MOBILEGL_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
switch (type) {
|
||||
case BackendType::DirectGLES:
|
||||
m_backend = MakeUnique<MG_Backend::DirectGLES::BackendObject_DirectGLES>();
|
||||
break;
|
||||
case BackendType::DirectVulkan:
|
||||
m_backend = MakeUnique<MG_Backend::DirectVulkan::BackendObject_DirectVulkan>();
|
||||
break;
|
||||
default:
|
||||
// NOT a fallback to DirectGLES. An unknown backend under split has to be refused by
|
||||
// name: the alternative is a lane that says "split, DirectVulkan" and renders with
|
||||
// the other backend.
|
||||
MGLOG_E("MG_Remote server: MG_Config::ActiveBackendType is Unknown; the server role "
|
||||
"has no backend to own a context with and the session is refused");
|
||||
return MOBILEGL_ERR_UNSUPPORTED;
|
||||
}
|
||||
// Loads the driver entry points and registers the resource op table
|
||||
// (BackendObject_DirectGLES.cpp:844-851). NO GL CALL AND NO EGL CALL HAPPENS HERE - the
|
||||
// native context is created and made current later, on the apply thread, when the
|
||||
// client's eglMakeCurrent crosses as a blocking control request.
|
||||
m_backend->Initialize();
|
||||
return MOBILEGL_OK;
|
||||
}
|
||||
|
||||
MG_Backend::BackendObject* ServerLoop::Backend() { return m_backend.get(); }
|
||||
|
||||
// ---------------------------------------------------------------------------------
|
||||
// Start / the loop / Stop
|
||||
// ---------------------------------------------------------------------------------
|
||||
|
||||
MobileGLResult ServerLoop::Start(ServerSession& session) {
|
||||
if (m_running.load(std::memory_order_acquire)) {
|
||||
return MOBILEGL_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
if (!session.Accepted()) {
|
||||
MGLOG_E("MG_Remote server: ServerLoop::Start before ServerSession::Accept - there "
|
||||
"are no rings to park on");
|
||||
return MOBILEGL_ERR_NOT_INITIALIZED;
|
||||
}
|
||||
m_session = &session;
|
||||
m_stopRequested.store(false, std::memory_order_release);
|
||||
{
|
||||
const std::lock_guard<std::mutex> lock(m_exitMutex);
|
||||
m_exited = false;
|
||||
}
|
||||
m_running.store(true, std::memory_order_release);
|
||||
m_thread = std::thread([this] { ApplyThreadMain(); });
|
||||
return MOBILEGL_OK;
|
||||
}
|
||||
|
||||
Bool ServerLoop::Running() const { return m_running.load(std::memory_order_acquire); }
|
||||
|
||||
Bool ServerLoop::OnApplyThread() {
|
||||
ServerLoop& loop = ServerLoopInstance();
|
||||
return loop.m_running.load(std::memory_order_acquire) &&
|
||||
loop.m_applyThreadId.load(std::memory_order_acquire) == std::this_thread::get_id();
|
||||
}
|
||||
|
||||
Uint64 ServerLoop::ResolvedAffinityMask() const { return m_affinityMask; }
|
||||
Uint64 ServerLoop::DrainedRecords() const { return m_drained.load(std::memory_order_acquire); }
|
||||
Uint64 ServerLoop::ParkCount() const { return m_parks.load(std::memory_order_acquire); }
|
||||
|
||||
void ServerLoop::ApplyThreadMain() {
|
||||
m_applyThreadId.store(std::this_thread::get_id(), std::memory_order_release);
|
||||
NameThisThread("mgl-srv-apply");
|
||||
|
||||
Bool recognised = true;
|
||||
const char* raw = AffinityStringFromConfig();
|
||||
const Uint64 requested = RequestedAffinityMask(raw, &recognised);
|
||||
m_affinityMask = ApplyAffinity(requested);
|
||||
if (!recognised) {
|
||||
MGLOG_E("MG_Remote server: MOBILEGL_IPC_SERVER_AFFINITY='%s' is not `auto`, `off` or "
|
||||
"a number; NO affinity was applied. This is logged rather than defaulted "
|
||||
"because an affinity that silently did nothing is indistinguishable from "
|
||||
"one that worked",
|
||||
raw == nullptr ? "" : raw);
|
||||
}
|
||||
// THE RESOLVED MASK, ALWAYS, INCLUDING 0. Contract 5's whole point: the string is what
|
||||
// an operator typed and the mask is what the kernel took.
|
||||
MGLOG_I("MG_Remote server: mgl-srv-apply started. MOBILEGL_IPC_SERVER_AFFINITY='%s' "
|
||||
"requested mask 0x%llx, RESOLVED mask 0x%llx (0 = no affinity applied), spin "
|
||||
"%u us",
|
||||
raw == nullptr ? "" : raw, static_cast<unsigned long long>(requested),
|
||||
static_cast<unsigned long long>(m_affinityMask), SpinUsFromConfig());
|
||||
|
||||
ServerSession& session = *m_session;
|
||||
// The decoder is built HERE, on this thread, because PipeWireDecoder is "not thread
|
||||
// safe: one decoder on the apply thread, by construction" and its constructor installs
|
||||
// the process-wide apply hook.
|
||||
session.Applier().Attach(&session.Control(), m_backend.get());
|
||||
|
||||
Transport::RingControl& control = session.Control();
|
||||
Transport::Doorbell& bell = session.ConsumerDoorbell();
|
||||
Transport::RingConsumer& ring = session.CommandRing();
|
||||
const Uint32 spinUs = SpinUsFromConfig();
|
||||
|
||||
// THE PARK CONDITION IS THREE THINGS, NOT ONE, AND THAT IS THE WHOLE REASON THIS LOOP
|
||||
// DOES NOT CALL SessionConsumer::WaitForWork.
|
||||
//
|
||||
// WaitForWork's predicate is "a record is waiting" and nothing else. Park a thread on
|
||||
// kWaitForever with that predicate and neither a control request nor a Stop() can get
|
||||
// it out: Doorbell::Wait consumes the Notify with one Park, re-tests a condition
|
||||
// nothing published, finds the bell alive, and parks again - forever. Only
|
||||
// CondVarDoorbell::Kill() breaks that, and Kill is the CLIENT's teardown call, not
|
||||
// something an EGL make-current request may perform. So the predicate carries all
|
||||
// three arming conditions and the doorbell wakes the thread for any of them.
|
||||
const auto ready = [this, &control, &ring] {
|
||||
return control.cmdHead.load(std::memory_order_acquire) != ring.LocalTail() ||
|
||||
m_stopRequested.load(std::memory_order_acquire) || ControlIsPending();
|
||||
};
|
||||
|
||||
for (;;) {
|
||||
PumpControlRequest();
|
||||
if (m_stopRequested.load(std::memory_order_acquire)) break;
|
||||
DrainRing();
|
||||
if (m_stopRequested.load(std::memory_order_acquire)) break;
|
||||
if (ready()) continue;
|
||||
|
||||
m_parks.fetch_add(1, std::memory_order_acq_rel);
|
||||
const bool woke = bell.Wait(control.consumerParked, ready, spinUs, Transport::kWaitForever);
|
||||
if (!woke) {
|
||||
// Wait returns false only on a dead bell or an expired deadline, and this park
|
||||
// has no deadline. A dead bell IS the shutdown signal (table 3's teardown step
|
||||
// 2); treating it as anything else would spin at full clock for ever, since
|
||||
// parking on a dead bell no longer blocks.
|
||||
if (bell.Dead()) {
|
||||
MGLOG_D("MG_Remote server: the consumer doorbell is dead; mgl-srv-apply is "
|
||||
"shutting down");
|
||||
break;
|
||||
}
|
||||
MGLOG_E("MG_Remote server: Doorbell::Wait(kWaitForever) returned false on a live "
|
||||
"bell - that is impossible by Doorbell.h:139-178 and means the bell's "
|
||||
"Dead() answer moved under the waiter. Shutting the loop down rather "
|
||||
"than spinning");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Whatever is still queued gets applied before the context goes. The client's own
|
||||
// drain (ClientSession::Stop step 1) normally empties the ring first and is BOUNDED, so
|
||||
// a timeout there leaves records here; applying them costs nothing and dropping them
|
||||
// would leave the emitter's var-tails referenced by records nobody ever read.
|
||||
PumpControlRequest();
|
||||
DrainRing();
|
||||
|
||||
// THE BACKEND IS DESTROYED ON THIS THREAD, WHILE IT IS STILL THE CONTEXT OWNER.
|
||||
// ~BackendObject_DirectGLES calls DestroyEGLContext (BackendObject_DirectGLES.cpp:
|
||||
// 833-835), which runs eglMakeCurrent(NO_SURFACE) / eglDestroyContext / eglTerminate.
|
||||
// Those must happen on the thread eglMakeCurrent was issued from; doing it on the app
|
||||
// thread - which is what pActiveBackendObject.reset() at MobileGL/Init.cpp:68 would do
|
||||
// - destroys a context this thread still holds current.
|
||||
session.Applier().Detach();
|
||||
if (m_backend != nullptr) {
|
||||
MGLOG_I("MG_Remote server: destroying the server's BackendObject on mgl-srv-apply, "
|
||||
"which is the context owner");
|
||||
m_backend.reset();
|
||||
}
|
||||
|
||||
// Anything still parked in RunOnApplyThread has to be answered rather than left
|
||||
// waiting: this thread is the only thing that could have run it.
|
||||
{
|
||||
const std::lock_guard<std::mutex> lock(m_controlMutex);
|
||||
if (m_controlPending) {
|
||||
m_controlPending = false;
|
||||
m_controlFinished = true;
|
||||
m_controlResult = MOBILEGL_ERR_NOT_INITIALIZED;
|
||||
MGLOG_E("MG_Remote server: a control request was still posted when mgl-srv-apply "
|
||||
"exited; it is answered NOT_INITIALIZED rather than left blocking");
|
||||
}
|
||||
}
|
||||
m_controlDone.notify_all();
|
||||
|
||||
m_running.store(false, std::memory_order_release);
|
||||
SignalExited();
|
||||
}
|
||||
|
||||
Bool ServerLoop::ControlIsPending() const {
|
||||
const std::lock_guard<std::mutex> lock(const_cast<std::mutex&>(m_controlMutex));
|
||||
return m_controlPending;
|
||||
}
|
||||
|
||||
Bool ServerLoop::PumpControlRequest() {
|
||||
ControlWork work = nullptr;
|
||||
void* user = nullptr;
|
||||
{
|
||||
const std::lock_guard<std::mutex> lock(m_controlMutex);
|
||||
if (!m_controlPending) return false;
|
||||
work = m_controlWork;
|
||||
user = m_controlUser;
|
||||
}
|
||||
const MobileGLResult result = work == nullptr ? MOBILEGL_ERR_INVALID_ARGUMENT : work(user);
|
||||
{
|
||||
const std::lock_guard<std::mutex> lock(m_controlMutex);
|
||||
m_controlResult = result;
|
||||
m_controlPending = false;
|
||||
m_controlFinished = true;
|
||||
}
|
||||
m_controlDone.notify_all();
|
||||
return true;
|
||||
}
|
||||
|
||||
Uint64 ServerLoop::DrainRing() {
|
||||
ServerSession& session = *m_session;
|
||||
Transport::SessionConsumer& consumer = session.Consumer();
|
||||
PipeApplier& applier = session.Applier();
|
||||
Uint64 applied = 0;
|
||||
for (;;) {
|
||||
bool corrupt = false;
|
||||
const bool popped = consumer.ApplyOne(
|
||||
[this, &applier](const Transport::RingRecordView& record) {
|
||||
applier.ApplyOne(record);
|
||||
// THE TALLY MOVES HERE, INSIDE THE CALLBACK, AND NOT AFTER THE BATCH. s1's
|
||||
// SessionConsumer::ApplyOne publishes appliedSeq the instant this callback
|
||||
// returns, and publishing appliedSeq is what releases the client from the
|
||||
// verb barrier (R-1). Anything this thread records about the record AFTER
|
||||
// that publish is visible to the client only eventually - and a diagnostic
|
||||
// that can lag the watermark it describes is the exact shape of the
|
||||
// LeaveApplier race PipeApplier::ApplyOne's block describes, one level up.
|
||||
// The first version of this function tallied once per batch, below, and
|
||||
// AClearRecordCrossesAndIsStampedAsAVerbBoundary /
|
||||
// TheSessionWatermarkAndTheDecoderTallyAgreeAfterEveryRecord read
|
||||
// DrainedRecords() one behind appliedSeq on 1 run in ~40 under `-j 8`.
|
||||
// Per-record, before the publish, it can never be behind.
|
||||
m_drained.fetch_add(1, std::memory_order_acq_rel);
|
||||
},
|
||||
&corrupt);
|
||||
if (corrupt) {
|
||||
// Ring.h's own rule: a header the producer could not have written is
|
||||
// Fatal{ProtocolCorruption}, never a retry. Retrying re-reads the same bytes
|
||||
// for ever; skipping desynchronises seq, and seq IS the reply-slot id.
|
||||
MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"SEG_CMD record header\"} - the "
|
||||
"consumer refused a record header at applied seq %llu",
|
||||
static_cast<unsigned long long>(consumer.AppliedSeq()));
|
||||
std::abort();
|
||||
}
|
||||
if (!popped) break;
|
||||
++applied;
|
||||
}
|
||||
if (applied != 0) {
|
||||
// THE TWO TALLIES MUST AGREE, AND THAT IS WHAT MAKES R-9's BATCHING BAN CHECKABLE.
|
||||
// RingControl::appliedSeq has one writer (SessionConsumer::ApplyOne, +1 per record)
|
||||
// and PipeWireDecoder keeps its own count; a batched publish would move one and not
|
||||
// the other, which a single counter could not have told apart (w1-v1 5).
|
||||
if (applier.DecoderAppliedSeq() != consumer.AppliedSeq()) {
|
||||
MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"appliedSeq batched\"} - the "
|
||||
"session's watermark is %llu and the decoder applied %llu records. P5 "
|
||||
"forbids batching appliedSeq (R-9): the verb barrier's waiter reads it, "
|
||||
"and a watermark ahead of the decoder promises work that has not run",
|
||||
static_cast<unsigned long long>(consumer.AppliedSeq()),
|
||||
static_cast<unsigned long long>(applier.DecoderAppliedSeq()));
|
||||
std::abort();
|
||||
}
|
||||
// RETIRE. MANDATORY, not optional: RingProducer::FreeBytes() reclaims against
|
||||
// retiredTail only, and w1's SEG_STAGE linear allocator reclaims on retiredSeq - so
|
||||
// a loop that applies and never retires ends the first MOBILEGL_IPC_STAGE_MB of
|
||||
// staging in Fatal{RingOverrun, "SEG_STAGE"} (w1-v1 5). Once per drain batch, not
|
||||
// once per record: retiring LATE is always legal, retiring EARLY never is.
|
||||
consumer.RetireThrough(consumer.AppliedSeq());
|
||||
// LEAVING THE APPLIER (p1's M-5) IS *NOT* DONE HERE. It is done inside
|
||||
// PipeApplier::ApplyOne, before s1's SessionConsumer::ApplyOne publishes appliedSeq
|
||||
// - see the block there. A clear at this point races the client, which the barrier
|
||||
// has already released by then.
|
||||
}
|
||||
return applied;
|
||||
}
|
||||
|
||||
MobileGLResult ServerLoop::RunOnApplyThread(ControlWork work, void* user) {
|
||||
if (work == nullptr) return MOBILEGL_ERR_INVALID_ARGUMENT;
|
||||
// RE-ENTRANCY IS NOT A DEADLOCK. The teardown path posts from the apply thread itself -
|
||||
// ~BackendObject_DirectGLES reaches ReleaseEGLResources - and so does anything the
|
||||
// applier calls that wants "run this where the context is". Running inline is the
|
||||
// correct answer there and the only non-hanging one.
|
||||
if (OnApplyThread()) return work(user);
|
||||
// NO THREAD YET, OR ALREADY GONE: run inline on the caller. That is right for the
|
||||
// window between MG_Backend::Init()'s hook and ClientSession::Start (the backend object
|
||||
// exists, the thread does not, and nothing has made a context current), and for the
|
||||
// window after Stop(). It is NOT a silent fallback to monolith: the thread's absence in
|
||||
// those two windows is a fact about the lifecycle, not about the transport.
|
||||
if (!m_running.load(std::memory_order_acquire)) return work(user);
|
||||
|
||||
const std::lock_guard<std::mutex> callerLock(m_callerMutex);
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(m_controlMutex);
|
||||
m_controlWork = work;
|
||||
m_controlUser = user;
|
||||
m_controlPending = true;
|
||||
m_controlFinished = false;
|
||||
}
|
||||
// Publish THEN ring, in that order and never the other (Doorbell.h:186-193). The bell
|
||||
// is rung unconditionally rather than through NotifyIfParked because this side does not
|
||||
// know whether the apply thread is parked or spinning, and CondVarDoorbell remembers a
|
||||
// wakeup that arrives while nobody is waiting.
|
||||
if (m_session != nullptr) {
|
||||
m_session->ConsumerDoorbell().Notify();
|
||||
}
|
||||
std::unique_lock<std::mutex> lock(m_controlMutex);
|
||||
m_controlDone.wait(lock, [this] { return m_controlFinished; });
|
||||
return m_controlResult;
|
||||
}
|
||||
|
||||
void ServerLoop::SignalExited() {
|
||||
{
|
||||
const std::lock_guard<std::mutex> lock(m_exitMutex);
|
||||
m_exited = true;
|
||||
}
|
||||
m_exitCv.notify_all();
|
||||
}
|
||||
|
||||
void ServerLoop::Stop() {
|
||||
if (!m_thread.joinable()) {
|
||||
// Never started, or already stopped. The backend may still exist - the hook builds
|
||||
// it before the thread - and it has to go somewhere, so it goes here, on whatever
|
||||
// thread called Stop. No context was ever made current from another thread in that
|
||||
// case, which is exactly the condition that makes this safe.
|
||||
if (m_backend != nullptr) m_backend.reset();
|
||||
m_running.store(false, std::memory_order_release);
|
||||
return;
|
||||
}
|
||||
m_stopRequested.store(true, std::memory_order_release);
|
||||
if (m_session != nullptr) {
|
||||
// The bell may already be dead - ClientSession::Stop calls transport->Shutdown()
|
||||
// first, which is what table 3's step 2 requires - and Notify on a dead bell is
|
||||
// harmless. Ringing anyway covers the paths that Stop without a Kill.
|
||||
m_session->ConsumerDoorbell().Notify();
|
||||
}
|
||||
|
||||
// THE JOIN IS BOUNDED. std::thread::join has no deadline, so a lost wakeup would wedge
|
||||
// CI rather than fail it; the thread signals m_exited last and this waits with the same
|
||||
// five seconds InProcessTransportTest.cpp:344 uses.
|
||||
Bool exited = false;
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(m_exitMutex);
|
||||
exited = m_exitCv.wait_for(lock, std::chrono::milliseconds(kJoinTimeoutMs),
|
||||
[this] { return m_exited; });
|
||||
}
|
||||
if (!exited) {
|
||||
// ABORT, NOT DETACH. A detached apply thread still owns the EGL context and would
|
||||
// run on into the client's teardown, reading rings the client is about to unmap -
|
||||
// a use-after-free whose only symptom is an intermittent crash somewhere else.
|
||||
// Aborting here is red, immediate, and names the cause.
|
||||
MGLOG_F("MGPipe: Fatal{ApplyThreadJoinTimeout} - mgl-srv-apply did not exit within "
|
||||
"%u ms of Stop(). It parks on Doorbell::Wait(kWaitForever), which only "
|
||||
"CondVarDoorbell::Kill() can break (Doorbell.h:211-221, table 3 step 2), so "
|
||||
"this is a lost wakeup and not a slow thread. Aborting rather than "
|
||||
"detaching: a detached apply thread still owns the context and would read "
|
||||
"rings the client is about to unmap",
|
||||
kJoinTimeoutMs);
|
||||
std::abort();
|
||||
}
|
||||
m_thread.join();
|
||||
m_applyThreadId.store(std::thread::id{}, std::memory_order_release);
|
||||
m_running.store(false, std::memory_order_release);
|
||||
m_session = nullptr;
|
||||
}
|
||||
|
||||
ServerLoop& ServerLoopInstance() {
|
||||
@@ -44,6 +508,257 @@ namespace MobileGL::MG_Remote::Server {
|
||||
return instance;
|
||||
}
|
||||
|
||||
#undef MGP5_C0_STUB
|
||||
// ---------------------------------------------------------------------------------
|
||||
// The twelve EGL forwarders
|
||||
// ---------------------------------------------------------------------------------
|
||||
|
||||
namespace {
|
||||
|
||||
// One captureless trampoline for every call: ControlWork is a raw function pointer
|
||||
// plus a void*, not a std::function, because these run on the TEARDOWN path and the
|
||||
// teardown path may not allocate (ID-8 exists because frontend destructors reach here
|
||||
// from exit handlers).
|
||||
template <class Args>
|
||||
MobileGLResult RunOnApply(Args& args) {
|
||||
return ServerLoopInstance().RunOnApplyThread(
|
||||
+[](void* user) -> MobileGLResult { return static_cast<Args*>(user)->Run(); },
|
||||
&args);
|
||||
}
|
||||
|
||||
// Null means the hook never ran or the session is already down. Every forwarder
|
||||
// answers false / no-op rather than dereferencing: a client that calls eglMakeCurrent
|
||||
// on a process whose split bring-up failed must see a refusal, not a crash.
|
||||
MG_Backend::BackendObject* ServerBackendOrNull() { return ServerLoopInstance().Backend(); }
|
||||
|
||||
} // namespace
|
||||
|
||||
Bool ServerInitializeEGLDisplay(EGLDisplay dpy, EGLint* major, EGLint* minor) {
|
||||
struct Args {
|
||||
EGLDisplay dpy;
|
||||
EGLint* major;
|
||||
EGLint* minor;
|
||||
Bool ok = false;
|
||||
MobileGLResult Run() {
|
||||
MG_Backend::BackendObject* backend = ServerBackendOrNull();
|
||||
if (backend == nullptr) return MOBILEGL_ERR_NOT_INITIALIZED;
|
||||
ok = backend->InitializeEGLDisplay(dpy, major, minor);
|
||||
return MOBILEGL_OK;
|
||||
}
|
||||
} args{dpy, major, minor};
|
||||
return RunOnApply(args) == MOBILEGL_OK && args.ok;
|
||||
}
|
||||
|
||||
Bool ServerCreateEGLWindowSurface(EGLSurface surface, const MG_Backend::WindowHandle& handle) {
|
||||
struct Args {
|
||||
EGLSurface surface;
|
||||
const MG_Backend::WindowHandle* handle;
|
||||
Bool ok = false;
|
||||
MobileGLResult Run() {
|
||||
MG_Backend::BackendObject* backend = ServerBackendOrNull();
|
||||
if (backend == nullptr) return MOBILEGL_ERR_NOT_INITIALIZED;
|
||||
ok = backend->CreateEGLWindowSurface(surface, *handle);
|
||||
return MOBILEGL_OK;
|
||||
}
|
||||
} args{surface, &handle};
|
||||
return RunOnApply(args) == MOBILEGL_OK && args.ok;
|
||||
}
|
||||
|
||||
Bool ServerResizeEGLWindowSurface(EGLSurface surface, Uint32 width, Uint32 height) {
|
||||
struct Args {
|
||||
EGLSurface surface;
|
||||
Uint32 width;
|
||||
Uint32 height;
|
||||
Bool ok = false;
|
||||
MobileGLResult Run() {
|
||||
MG_Backend::BackendObject* backend = ServerBackendOrNull();
|
||||
if (backend == nullptr) return MOBILEGL_ERR_NOT_INITIALIZED;
|
||||
ok = backend->ResizeEGLWindowSurface(surface, width, height);
|
||||
return MOBILEGL_OK;
|
||||
}
|
||||
} args{surface, width, height};
|
||||
return RunOnApply(args) == MOBILEGL_OK && args.ok;
|
||||
}
|
||||
|
||||
Bool ServerCreateEGLPbufferSurface(EGLSurface surface, EGLint width, EGLint height) {
|
||||
struct Args {
|
||||
EGLSurface surface;
|
||||
EGLint width;
|
||||
EGLint height;
|
||||
Bool ok = false;
|
||||
MobileGLResult Run() {
|
||||
MG_Backend::BackendObject* backend = ServerBackendOrNull();
|
||||
if (backend == nullptr) return MOBILEGL_ERR_NOT_INITIALIZED;
|
||||
ok = backend->CreateEGLPbufferSurface(surface, width, height);
|
||||
return MOBILEGL_OK;
|
||||
}
|
||||
} args{surface, width, height};
|
||||
return RunOnApply(args) == MOBILEGL_OK && args.ok;
|
||||
}
|
||||
|
||||
Bool ServerMakeEGLCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx) {
|
||||
struct Args {
|
||||
EGLDisplay dpy;
|
||||
EGLSurface draw;
|
||||
EGLSurface read;
|
||||
EGLContext ctx;
|
||||
Bool ok = false;
|
||||
MobileGLResult Run() {
|
||||
MG_Backend::BackendObject* backend = ServerBackendOrNull();
|
||||
if (backend == nullptr) return MOBILEGL_ERR_NOT_INITIALIZED;
|
||||
// THE ONE eglMakeCurrent OF THE PROCESS'S LIFE, ON THIS THREAD. Every later
|
||||
// client-side eglMakeCurrent onto the same surface finds the context already
|
||||
// current here and costs nothing; the owner slot is written once.
|
||||
ok = backend->MakeEGLCurrent(dpy, draw, read, ctx);
|
||||
if (!ok) return MOBILEGL_OK;
|
||||
// R-12, arm (a): the caps snapshot is REPUBLISHED because InitCapabilities has
|
||||
// now run for real. DirectGLES has no OnCapsInvalidated producer at all, and
|
||||
// c0's answer is that a SECOND arrival IS the invalidation - so the client's
|
||||
// mirror is refreshed with no dev-shaped backend edit and with no eleventh
|
||||
// MGPipeCallbacks slot (MGPipeCallbacks.h:56-58's static_assert exists to make
|
||||
// that cost visible).
|
||||
ServerSession* session = ServerSession::Active();
|
||||
if (session != nullptr && session->Accepted()) {
|
||||
const MobileGLResult published = session->PublishCapsSnapshot();
|
||||
if (published != MOBILEGL_OK) {
|
||||
MGLOG_E("MG_Remote server: the post-make-current CapsSnapshot could not "
|
||||
"be published (rc=%d); the client's mirror still holds the empty "
|
||||
"snapshot Accept() sent before any context existed",
|
||||
static_cast<int>(published));
|
||||
}
|
||||
}
|
||||
return MOBILEGL_OK;
|
||||
}
|
||||
} args{dpy, draw, read, ctx};
|
||||
return RunOnApply(args) == MOBILEGL_OK && args.ok;
|
||||
}
|
||||
|
||||
Bool ServerSwapEGLBuffers(EGLDisplay dpy, EGLSurface draw) {
|
||||
struct Args {
|
||||
EGLDisplay dpy;
|
||||
EGLSurface draw;
|
||||
Bool ok = false;
|
||||
MobileGLResult Run() {
|
||||
MG_Backend::BackendObject* backend = ServerBackendOrNull();
|
||||
if (backend == nullptr) return MOBILEGL_ERR_NOT_INITIALIZED;
|
||||
ok = backend->SwapEGLBuffers(dpy, draw);
|
||||
return MOBILEGL_OK;
|
||||
}
|
||||
} args{dpy, draw};
|
||||
return RunOnApply(args) == MOBILEGL_OK && args.ok;
|
||||
}
|
||||
|
||||
void ServerSetEGLSwapInterval(Int interval) {
|
||||
struct Args {
|
||||
Int interval;
|
||||
MobileGLResult Run() {
|
||||
MG_Backend::BackendObject* backend = ServerBackendOrNull();
|
||||
if (backend == nullptr) return MOBILEGL_ERR_NOT_INITIALIZED;
|
||||
backend->SetEGLSwapInterval(interval);
|
||||
return MOBILEGL_OK;
|
||||
}
|
||||
} args{interval};
|
||||
(void)RunOnApply(args);
|
||||
}
|
||||
|
||||
void ServerReleaseEGLSurface(EGLSurface surface) {
|
||||
struct Args {
|
||||
EGLSurface surface;
|
||||
MobileGLResult Run() {
|
||||
MG_Backend::BackendObject* backend = ServerBackendOrNull();
|
||||
if (backend == nullptr) return MOBILEGL_ERR_NOT_INITIALIZED;
|
||||
backend->ReleaseEGLSurface(surface);
|
||||
return MOBILEGL_OK;
|
||||
}
|
||||
} args{surface};
|
||||
(void)RunOnApply(args);
|
||||
}
|
||||
|
||||
void ServerReleaseEGLResources() {
|
||||
// BLOCKING BY CONTRACT. EGLImpl.cpp:326 calls this on the app thread and then, if no
|
||||
// display and no context are left, calls MobileGL::Destroy() at :333. For DirectGLES it
|
||||
// runs DestroyEGLContext - eglMakeCurrent(NO_SURFACE), eglDestroyContext, eglTerminate -
|
||||
// and those must happen on the thread that made the context current. A fire-and-forget
|
||||
// here lets Destroy() walk on while the server still holds the context, which is
|
||||
// scout-install 5.3's named hazard.
|
||||
struct Args {
|
||||
MobileGLResult Run() {
|
||||
MG_Backend::BackendObject* backend = ServerBackendOrNull();
|
||||
if (backend == nullptr) return MOBILEGL_ERR_NOT_INITIALIZED;
|
||||
backend->ReleaseEGLResources();
|
||||
return MOBILEGL_OK;
|
||||
}
|
||||
} args{};
|
||||
(void)RunOnApply(args);
|
||||
}
|
||||
|
||||
Bool ServerInitCapabilities() {
|
||||
struct Args {
|
||||
Bool ok = false;
|
||||
MobileGLResult Run() {
|
||||
MG_Backend::BackendObject* backend = ServerBackendOrNull();
|
||||
if (backend == nullptr) return MOBILEGL_ERR_NOT_INITIALIZED;
|
||||
ok = backend->InitCapabilities();
|
||||
if (!ok) return MOBILEGL_OK;
|
||||
ServerSession* session = ServerSession::Active();
|
||||
if (session != nullptr && session->Accepted()) {
|
||||
(void)session->PublishCapsSnapshot();
|
||||
}
|
||||
return MOBILEGL_OK;
|
||||
}
|
||||
} args{};
|
||||
return RunOnApply(args) == MOBILEGL_OK && args.ok;
|
||||
}
|
||||
|
||||
Bool ServerInitWindowSurface() {
|
||||
struct Args {
|
||||
Bool ok = false;
|
||||
MobileGLResult Run() {
|
||||
MG_Backend::BackendObject* backend = ServerBackendOrNull();
|
||||
if (backend == nullptr) return MOBILEGL_ERR_NOT_INITIALIZED;
|
||||
ok = backend->InitWindowSurface();
|
||||
return MOBILEGL_OK;
|
||||
}
|
||||
} args{};
|
||||
return RunOnApply(args) == MOBILEGL_OK && args.ok;
|
||||
}
|
||||
|
||||
void ServerSetWindowHandle(const MG_Backend::WindowHandle& handle) {
|
||||
struct Args {
|
||||
const MG_Backend::WindowHandle* handle;
|
||||
MobileGLResult Run() {
|
||||
MG_Backend::BackendObject* backend = ServerBackendOrNull();
|
||||
if (backend == nullptr) return MOBILEGL_ERR_NOT_INITIALIZED;
|
||||
backend->SetWindowHandle(*handle);
|
||||
return MOBILEGL_OK;
|
||||
}
|
||||
} args{&handle};
|
||||
(void)RunOnApply(args);
|
||||
}
|
||||
|
||||
} // namespace MobileGL::MG_Remote::Server
|
||||
|
||||
// ---------------------------------------------------------------------------------
|
||||
// The weak placeholder for package c1's remote backend object
|
||||
// ---------------------------------------------------------------------------------
|
||||
//
|
||||
// MG_Backend/Init.cpp's hook calls MG_Remote::Client::CreateRemoteBackendObject(), which is
|
||||
// c1's BackendObject_Remote and does not exist while v1 is written. A WEAK definition here
|
||||
// lets v1 compile, link and be tested today, and c1's strong definition displaces it at link
|
||||
// time with no edit anywhere.
|
||||
//
|
||||
// IT ABORTS BY NAME AND DOES NOT RETURN A WORKING MONOLITH OBJECT. That distinction is the
|
||||
// whole point: a placeholder that handed back a BackendObject_DirectGLES would give a lane
|
||||
// called "split" a correct picture produced entirely by the monolith path, which is
|
||||
// ARCHITECTURE.md 10.3's failure and the one this phase exists to make impossible.
|
||||
#if defined(__GNUC__) || defined(__clang__)
|
||||
namespace MobileGL::MG_Remote::Client {
|
||||
__attribute__((weak)) UniquePtr<MG_Backend::BackendObject> CreateRemoteBackendObject() {
|
||||
MGLOG_F("MGPipe: Fatal{UnimplementedRemoteBackendObject} - MG_Backend::Init()'s split "
|
||||
"hook asked for the client's BackendObject_Remote and only v1's weak "
|
||||
"placeholder is linked in. That object is package c1's (BRIEF 5); until it "
|
||||
"lands there is no client role, and this build refuses to substitute the "
|
||||
"monolith backend for it");
|
||||
std::abort();
|
||||
}
|
||||
} // namespace MobileGL::MG_Remote::Client
|
||||
#endif
|
||||
|
||||
@@ -45,6 +45,11 @@
|
||||
|
||||
#include "ServerSession.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
|
||||
namespace MobileGL::MG_Remote::Server {
|
||||
|
||||
class ServerLoop {
|
||||
@@ -81,11 +86,129 @@ namespace MobileGL::MG_Remote::Server {
|
||||
using ControlWork = MobileGLResult (*)(void* user);
|
||||
MobileGLResult RunOnApplyThread(ControlWork work, void* user);
|
||||
|
||||
// ---- v1's additions beyond c0's signature block ---------------------------------
|
||||
|
||||
// Constructs the SERVER ROLE's private BackendObject and runs its non-GL Initialize().
|
||||
// Called from MG_Backend::Init()'s single hook, BEFORE ClientSession::Start(), because
|
||||
// ServerSession::Accept() publishes the first CapsSnapshot from it and because the two
|
||||
// CallMask halves - which have no default and Fatal when unset (ServerSession.h) - are
|
||||
// answered from what this backend is.
|
||||
//
|
||||
// NO GL AND NO EGL HAPPENS HERE. BackendObject_DirectGLES::Initialize() loads the
|
||||
// driver's entry points; the native context does not exist until the client's first
|
||||
// eglMakeCurrent crosses as a blocking control request and runs on the apply thread.
|
||||
// That split is what lets the backend object be BUILT on the app thread while its
|
||||
// context is never OWNED by it.
|
||||
MobileGLResult CreateBackend(BackendType type);
|
||||
|
||||
// True on the apply thread itself. RunOnApplyThread uses it to run inline rather than
|
||||
// deadlock when the apply thread posts to itself - which the EGL teardown path does,
|
||||
// because ~BackendObject_DirectGLES runs THERE and reaches ReleaseEGLResources.
|
||||
static Bool OnApplyThread();
|
||||
|
||||
// The CPU mask MOBILEGL_IPC_SERVER_AFFINITY resolved to and sched_setaffinity accepted.
|
||||
// 0 means "no affinity was applied" - the honest answer for `off`, for a platform with
|
||||
// no affinity call, and for a failed syscall - and is exactly why the RESOLVED mask is
|
||||
// logged rather than the string an operator typed.
|
||||
Uint64 ResolvedAffinityMask() const;
|
||||
|
||||
// Diagnostics the tests read. DrainedRecords is how many records this thread has handed
|
||||
// to the applier; ParkCount how many times it actually parked. A shutdown test that
|
||||
// asserts only "Stop() returned" cannot tell a thread that parked and was woken by Kill
|
||||
// from one that never parked at all - which is the R-16 shape of a check that cannot
|
||||
// fail for its own reason.
|
||||
Uint64 DrainedRecords() const;
|
||||
Uint64 ParkCount() const;
|
||||
|
||||
private:
|
||||
void ApplyThreadMain();
|
||||
// Part of the apply thread's park predicate: a posted control request must be able to
|
||||
// un-park a thread waiting on kWaitForever, which a Notify alone cannot do.
|
||||
Bool ControlIsPending() const;
|
||||
// Runs a posted control request, if there is one. Returns true if it ran one.
|
||||
Bool PumpControlRequest();
|
||||
// Pops and applies every record currently in the ring; returns how many it applied.
|
||||
Uint64 DrainRing();
|
||||
void SignalExited();
|
||||
|
||||
ServerSession* m_session = nullptr;
|
||||
Bool m_running = false;
|
||||
std::atomic<Bool> m_running{false};
|
||||
std::atomic<Bool> m_stopRequested{false};
|
||||
std::thread m_thread;
|
||||
std::atomic<std::thread::id> m_applyThreadId{};
|
||||
|
||||
// The private backend object. Destroyed ON the apply thread while it still owns the
|
||||
// context - see Stop().
|
||||
UniquePtr<MG_Backend::BackendObject> m_backend;
|
||||
|
||||
// The blocking control mailbox. ONE slot, because the verb barrier already leaves one
|
||||
// client thread runnable at a time; m_callerMutex serialises anything that is not.
|
||||
std::mutex m_callerMutex;
|
||||
std::mutex m_controlMutex;
|
||||
std::condition_variable m_controlPosted;
|
||||
std::condition_variable m_controlDone;
|
||||
ControlWork m_controlWork = nullptr;
|
||||
void* m_controlUser = nullptr;
|
||||
MobileGLResult m_controlResult = MOBILEGL_OK;
|
||||
Bool m_controlPending = false;
|
||||
Bool m_controlFinished = false;
|
||||
|
||||
// The BOUNDED join's other half. std::thread::join has no deadline, so a lost wakeup
|
||||
// would wedge CI rather than fail it; the thread signals here last and Stop() waits
|
||||
// with a deadline (InProcessTransportTest.cpp:344's five seconds).
|
||||
std::mutex m_exitMutex;
|
||||
std::condition_variable m_exitCv;
|
||||
Bool m_exited = false;
|
||||
|
||||
Uint64 m_affinityMask = 0;
|
||||
std::atomic<Uint64> m_drained{0};
|
||||
std::atomic<Uint64> m_parks{0};
|
||||
};
|
||||
|
||||
ServerLoop& ServerLoopInstance();
|
||||
|
||||
// ---------------------------------------------------------------------------------
|
||||
// THE EGL OWNERSHIP MOVE - the part that can sink the phase, expressed as twelve calls
|
||||
// ---------------------------------------------------------------------------------
|
||||
//
|
||||
// Under split the app thread must never reach the driver's eglMakeCurrent. Today it does:
|
||||
// EGLImpl.cpp:284 -> BackendObject_DirectGLES.cpp:963 -> DirectGLES.cpp:11925, and the
|
||||
// owner slot g_backendContextOwnerThread (DirectGLES.cpp:11865) is stamped with whatever
|
||||
// thread got there. So BackendObject_Remote's nine EGL virtuals - package c1's - call these
|
||||
// twelve, each of which is a BLOCKING control request that runs the SERVER's backend object
|
||||
// on mgl-srv-apply. eglMakeCurrent then runs ONCE, on that thread, and is never released:
|
||||
// g_backendContextOwnerThread is written once, DirectGLES.cpp:11933-11953's six cache
|
||||
// invalidations become a one-off startup cost instead of a per-migration storm, the
|
||||
// per-frame EGL re-verification stamp is permanently true, and the 16
|
||||
// IsBackendContextCurrentOnThisThread() sites plus the 16 CanTouchGLNow() sites answer TRUE
|
||||
// on the server instead of silently degrading.
|
||||
//
|
||||
// TWO OF THEM MUST BLOCK OR THE PROCESS TEARS ITS OWN CONTEXT DOWN UNDER ITSELF:
|
||||
// ReleaseEGLResources (reached from EGLImpl.cpp:326, which for DirectGLES runs
|
||||
// DestroyEGLContext) and ~BackendObject_DirectGLES (reached from
|
||||
// pActiveBackendObject.reset() at MobileGL/Init.cpp:68). Both are blocking here - the first
|
||||
// by being one of these calls, the second because ServerLoop::Stop() destroys the private
|
||||
// backend ON the apply thread before that thread exits and Stop() itself waits.
|
||||
//
|
||||
// WHY THEY ARE FREE FUNCTIONS AND NOT MEMBERS: c1 needs exactly this surface and nothing
|
||||
// else of the server, so the seam between the two packages is a list of twelve signatures
|
||||
// rather than a class with a lifecycle.
|
||||
Bool ServerInitializeEGLDisplay(EGLDisplay dpy, EGLint* major, EGLint* minor);
|
||||
Bool ServerCreateEGLWindowSurface(EGLSurface surface, const MG_Backend::WindowHandle& handle);
|
||||
Bool ServerResizeEGLWindowSurface(EGLSurface surface, Uint32 width, Uint32 height);
|
||||
Bool ServerCreateEGLPbufferSurface(EGLSurface surface, EGLint width, EGLint height);
|
||||
// Also RE-PUBLISHES THE CAPS SNAPSHOT on success (R-12). BackendObject::MakeEGLCurrent runs
|
||||
// InitCapabilities() on the first make-current per surface (BackendObject.cpp:341-347), so
|
||||
// this is the moment the server's answers stop being the empty ones Accept() published -
|
||||
// and a SECOND arrival IS the invalidation signal, which is how DirectGLES, which has no
|
||||
// OnCapsInvalidated producer at all, tells the client without a dev-shaped backend edit.
|
||||
Bool ServerMakeEGLCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx);
|
||||
Bool ServerSwapEGLBuffers(EGLDisplay dpy, EGLSurface draw);
|
||||
void ServerSetEGLSwapInterval(Int interval);
|
||||
void ServerReleaseEGLSurface(EGLSurface surface);
|
||||
void ServerReleaseEGLResources();
|
||||
Bool ServerInitCapabilities();
|
||||
Bool ServerInitWindowSurface();
|
||||
void ServerSetWindowHandle(const MG_Backend::WindowHandle& handle);
|
||||
|
||||
} // namespace MobileGL::MG_Remote::Server
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
// MobileGL - MobileGL/MG_Remote/Server/StagedShadow.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
|
||||
|
||||
// R-11 - THE SERVER'S OWN COPY OF THE STAGED BYTES. Owner: package v1.
|
||||
//
|
||||
// GLESBufferResource::hostBytes (Managers.h:839) is the tree's ONE violation of rule C ("an
|
||||
// applier entry point may not hold a pointer past its return"): Ops_H_SubData (Managers.cpp:
|
||||
// 1980-1983) and Ops_H_FlushRange (:2035) record the client's shadow base and six later drains
|
||||
// read it (:2000, :2062, :2080, :2111, :2741, :2843). In monolith that is correct - the bytes
|
||||
// belong to a frontend object that outlives the call. Under split the pointer names SEG_STAGE,
|
||||
// which is valid only until retiredSeq passes the record that named it, and w1's
|
||||
// MOBILEGL_IPC_AUDIT=1 fills retired staging bytes with 0xDD precisely so an implementation
|
||||
// that kept the pointer is DISTINGUISHABLE from one that copied. So this copies.
|
||||
//
|
||||
// THE SNAPSHOT EXTENT IS EXACTLY WHAT THE RECORD DECLARED, NEVER WIDENED (the integrator's
|
||||
// ruling on b1's open M-6 half). Widening looked free once before and was not: a page-aligned
|
||||
// INVALIDATE_RANGE clobbered GPU-written data - an SSBO counter beside the app's SubData - with
|
||||
// stale shadow bytes (Managers.cpp:1126-1129). Tier 1's INVALIDATE_RANGE is an ASSERTION that
|
||||
// the old bytes are dead, so declaring bytes covered that nothing staged is not a missing
|
||||
// optimisation, it is a silent data loss. The coverage set below is therefore exact, and a
|
||||
// drain that reaches outside it is Fatal rather than a re-read of whatever happens to be there.
|
||||
//
|
||||
// WHY IT IS A HEADER AND NOT A BLOCK INSIDE Managers.cpp. Two reasons, and the second is the
|
||||
// one that matters. Managers.h is package b1's, so v1's R-11 edit may not add a member to
|
||||
// GLESBufferResource and the storage has to live beside it rather than in it. And a block
|
||||
// inside Managers.cpp could only ever be exercised by a test that also has a GL context, a
|
||||
// resource twin and a live session - which is exactly how a rule ends up with no check that
|
||||
// can fail for its own reason (R-16). Here, CopiesIntoServerStorage is a parameter rather than
|
||||
// a read of MG_Config::Transport, so a unit case builds one store of each kind and asserts the
|
||||
// DIFFERENCE between them.
|
||||
|
||||
#pragma once
|
||||
#include <Includes.h>
|
||||
|
||||
#include <MG_Util/Debug/Log.h>
|
||||
#include <MG_Util/Math/VectorTypes.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <mutex>
|
||||
|
||||
namespace MobileGL::MG_Remote::Server {
|
||||
|
||||
// Keyed by the resource twin's ADDRESS, which is stable: the twins are heap-allocated and
|
||||
// held by SharedPtr in the backend slot table, and every event that ends a base's life -
|
||||
// an orphaning respecify, a successful map_persistent, destroy, context death - already has
|
||||
// a call site in Managers.cpp to drop it from.
|
||||
class StagedShadowStore {
|
||||
public:
|
||||
// `copies` is "this process is really split". False reproduces the monolith expression
|
||||
// character for character (`raw - offset`), which is what keeps every push and verify
|
||||
// lane byte-identical to what it was before R-11.
|
||||
explicit StagedShadowStore(Bool copies) : m_copies(copies) {}
|
||||
|
||||
Bool CopiesIntoServerStorage() const { return m_copies; }
|
||||
|
||||
// Copies [offset, offset+size) of the record's staged bytes into server-owned storage
|
||||
// and returns the SERVER base (offset 0 of the resource). Under monolith it returns the
|
||||
// client base unchanged and allocates nothing.
|
||||
const Uint8* Adopt(const void* key, SizeT width, const void* bytes, SizeT offset, SizeT size) {
|
||||
const auto* raw = static_cast<const Uint8*>(bytes);
|
||||
if (!m_copies) return raw - offset;
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
Shadow& shadow = m_shadows[key];
|
||||
const SizeT needed = std::max<SizeT>(width, offset + size);
|
||||
if (shadow.Bytes.size() < needed) shadow.Bytes.resize(needed, 0);
|
||||
if (size != 0 && raw != nullptr) {
|
||||
std::memcpy(shadow.Bytes.data() + offset, raw, size);
|
||||
CoverageAdd(shadow.Covered, offset, offset + size);
|
||||
}
|
||||
m_any.store(true, std::memory_order_release);
|
||||
// Growing REALLOCATES, so every caller assigns the returned base to hostBytes on
|
||||
// the same call. No other resource's base moves: each Shadow owns its own vector,
|
||||
// and a rehash of the map MOVES that vector, which preserves its data pointer.
|
||||
return shadow.Bytes.data();
|
||||
}
|
||||
|
||||
void Drop(const void* key) {
|
||||
if (!m_any.load(std::memory_order_acquire)) return;
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_shadows.erase(key);
|
||||
}
|
||||
|
||||
void DropAll() {
|
||||
if (!m_any.load(std::memory_order_acquire)) return;
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_shadows.clear();
|
||||
}
|
||||
|
||||
// Fatal when a drain reaches bytes no record staged. It fires ONLY for a base that is
|
||||
// this key's server shadow: the legacy arm passes the frontend object's own
|
||||
// MappedData(), which is valid for the whole store and is not this rule's subject.
|
||||
void RequireCoverage(const void* key, const Uint8* hostBase, SizeT start, SizeT end,
|
||||
const char* site) const {
|
||||
if (!m_any.load(std::memory_order_acquire) || hostBase == nullptr) return;
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
const auto it = m_shadows.find(key);
|
||||
if (it == m_shadows.end() || it->second.Bytes.data() != hostBase) return;
|
||||
if (CoverageHas(it->second.Covered, start, end)) return;
|
||||
MGLOG_F("MGPipe: Fatal{StageSnapshotTooNarrow, \"%s\"} - the server's ladder wants "
|
||||
"[%zu, %zu) of a buffer whose staged coverage does not include it. Under "
|
||||
"split the authoritative shadow is SERVER-OWNED (rule C) and "
|
||||
"resource_subdata is the only way bytes reach it, so bytes outside a staged "
|
||||
"range have never existed on this side. Re-reading them would move zeroes "
|
||||
"into the store, and an INVALIDATE_RANGE over them would declare live "
|
||||
"GPU-written bytes dead (Managers.cpp:1126-1129). This is a missing record, "
|
||||
"not a missing widening",
|
||||
site, start, end);
|
||||
std::abort();
|
||||
}
|
||||
|
||||
// Diagnostics the unit cases read, so that a check can assert WHAT HAPPENED rather than
|
||||
// that nothing blew up.
|
||||
Bool IsCovered(const void* key, SizeT start, SizeT end) const {
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
const auto it = m_shadows.find(key);
|
||||
if (it == m_shadows.end()) return false;
|
||||
return CoverageHas(it->second.Covered, start, end);
|
||||
}
|
||||
SizeT CoveredRunCount(const void* key) const {
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
const auto it = m_shadows.find(key);
|
||||
return it == m_shadows.end() ? 0 : it->second.Covered.size();
|
||||
}
|
||||
SizeT TrackedResources() const {
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
return m_shadows.size();
|
||||
}
|
||||
|
||||
// Adjacent ranges merge - there is no gap between them, so the union really is one run.
|
||||
// Ranges with a gap do NOT merge, and that is the whole mechanism: it is what makes a
|
||||
// missing record detectable instead of papered over.
|
||||
static void CoverageAdd(Vector<Range1D>& covered, SizeT start, SizeT end) {
|
||||
if (start >= end) return;
|
||||
Vector<Range1D> merged;
|
||||
merged.reserve(covered.size() + 1);
|
||||
SizeT s = start;
|
||||
SizeT e = end;
|
||||
for (const auto& range : covered) {
|
||||
if (range.end < s || range.start > e) {
|
||||
merged.push_back(range);
|
||||
continue;
|
||||
}
|
||||
s = std::min(s, range.start);
|
||||
e = std::max(e, range.end);
|
||||
}
|
||||
merged.push_back({s, e});
|
||||
std::sort(merged.begin(), merged.end(),
|
||||
[](const Range1D& a, const Range1D& b) { return a.start < b.start; });
|
||||
covered = std::move(merged);
|
||||
}
|
||||
|
||||
static Bool CoverageHas(const Vector<Range1D>& covered, SizeT start, SizeT end) {
|
||||
if (start >= end) return true;
|
||||
for (const auto& range : covered) {
|
||||
if (range.start <= start && end <= range.end) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private:
|
||||
struct Shadow {
|
||||
Vector<Uint8> Bytes;
|
||||
// Sorted, disjoint, EXACT.
|
||||
Vector<Range1D> Covered;
|
||||
};
|
||||
|
||||
const Bool m_copies;
|
||||
mutable std::mutex m_mutex;
|
||||
ska::flat_hash_map<const void*, Shadow> m_shadows;
|
||||
// Read on every UploadRangeFrom, so the monolith cost is one acquire load of a
|
||||
// never-written flag rather than a mutex and a hash lookup.
|
||||
std::atomic<Bool> m_any{false};
|
||||
};
|
||||
|
||||
} // namespace MobileGL::MG_Remote::Server
|
||||
@@ -1310,11 +1310,27 @@ namespace MobileGL::MG_Remote::Wire {
|
||||
// may not re-derive it, because an if-constexpr discard, a stale handle and a refused
|
||||
// record are all invisible from the call site).
|
||||
//
|
||||
// IT DOES NOT GO IN A REPLY SLOT. None of the four carries kReplySlot, and s1 sizes
|
||||
// ReplyPool from that table - see PostReply. The answer is recorded here and read
|
||||
// through LastAcceptance() / Accepted+DeclinedRecords() until the integrator rules on
|
||||
// IT NOW RIDES THE REPLY SLOT, AND THAT IS THE RULING THIS COMMENT ASKED FOR. The text
|
||||
// that stood here said the answer could not ride a slot "until the integrator rules on
|
||||
// which half of the contract moves (table 0 says these four use DECLINED; the
|
||||
// catalogue gives them no slot).
|
||||
// catalogue gives them no slot)". Both halves have since moved the same way: ID-31
|
||||
// gave `ResourceCreate` kReplySlot and `kMGPipeCallFlags` now carries the flag on all
|
||||
// four (PipeWire.inc rows 2, 3, 47, 48), and P5 ruling R-17 states that the acceptance
|
||||
// answers "come back through the reply slot inside the barrier's wait". So the
|
||||
// conflict is resolved in favour of table 0, the pool reserves these seqs like any
|
||||
// other, and `PostReply`'s own Fatal - which trips on a row with no kReplySlot - is
|
||||
// what keeps this honest if a flag is ever taken away again.
|
||||
//
|
||||
// WITHOUT THIS THE CLIENT CANNOT RUN AT ALL: `ClientSession::EmitAndWait` asks the
|
||||
// catalogue, not the caller, whether a row owns a slot, so all four would wait for an
|
||||
// answer nobody wrote and take `Fatal{ReplyMissing}` inside the barrier. Recording the
|
||||
// acceptance locally as well is kept, because `LastAcceptance()` and the two counters
|
||||
// are what the monolith-side decoder cases are asserted on.
|
||||
//
|
||||
// OWNERSHIP: MG_Remote/Wire/* is package w1's and w1 is not in wave 2. This hunk is
|
||||
// four lines inside one lambda, made under R-17 by c1 because it is the server half of
|
||||
// the routing R-17 assigns, and it is called out in c1-v2.md so the integrator can
|
||||
// move it if the call belongs elsewhere.
|
||||
const auto noteAcceptance = [&](Bool accepted) {
|
||||
m_lastAcceptanceKnown = true;
|
||||
m_lastAcceptance = accepted;
|
||||
@@ -1323,6 +1339,10 @@ namespace MobileGL::MG_Remote::Wire {
|
||||
} else {
|
||||
++m_declined;
|
||||
}
|
||||
// DECLINED is a real answer and carries no payload (ReplySlot.h): the four Bool
|
||||
// rows say `false` with it, exactly as MapPersistent says nullptr with it.
|
||||
PostReply(op, seq, accepted ? ReplySink::kStatusOk : ReplySink::kStatusDeclined,
|
||||
nullptr, 0);
|
||||
};
|
||||
|
||||
switch (op) {
|
||||
|
||||
@@ -25,6 +25,10 @@
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
#include <MG_Impl/Pipe/SlotAllocator.h>
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
// P5 R-17: the routing that INSTALLS the two tables. Included here so that the installation
|
||||
// case below states the partition deterministically rather than depending on whether some
|
||||
// other object in this particular test binary happened to drag the installer in.
|
||||
#include <MG_Pipe/PipeRoute.h>
|
||||
#endif
|
||||
|
||||
using namespace MobileGL;
|
||||
@@ -101,17 +105,111 @@ TEST(PipeCatalogue, GeneratedTablesHoldTheWholeCatalogue) {
|
||||
EXPECT_EQ(ClassCount<kCtxVerb>(), 13u);
|
||||
}
|
||||
|
||||
// An uninstalled pipe is every entry null - which is exactly what "this subsystem has not
|
||||
// been migrated, keep pulling" means (plan B section 4.1).
|
||||
// A row nobody has migrated is null - which is exactly what "this subsystem has not been
|
||||
// migrated, keep pulling" means (plan B section 4.1).
|
||||
//
|
||||
// UNTIL P5 R-17 THAT WAS EVERY ROW, and this case said so. It is now EXACTLY THE 34 ROWS WITH
|
||||
// NO MGPipeApply* ENTRY POINT: the other 37 have an applier, R-17 installs adapters over them,
|
||||
// and a null there would no longer mean "keep pulling" - `MG_Impl/Pipe`'s call sites go through
|
||||
// the thunks, so a null would mean "call through a null pointer". The number is asserted rather
|
||||
// than the emptiness, because "37 installed" and "34 still null" are the two halves of a
|
||||
// partition and a case that checked only one of them would pass an installer that had
|
||||
// overwritten rows it does not own.
|
||||
// THE NAME IS KEPT, AND SO IS THE STATEMENT IT MAKES - only the ROWS it makes it about have
|
||||
// narrowed. G2/G14 compare ctest names against a pre-P5 baseline and require ZERO removed, so
|
||||
// renaming a case is a removal even when the new name is better: it is indistinguishable, from
|
||||
// the gate's side, from a case that was deleted. So this case stays, asserting the half that is
|
||||
// still true, and the new half below is an ADDED name.
|
||||
TEST(PipeCatalogue, UninstalledTablesAreAllNull) {
|
||||
const void* const* screen = reinterpret_cast<const void* const*>(&gMGPipeScreen);
|
||||
for (SizeT i = 0; i < kMGPipeScreenCallCount; ++i) {
|
||||
EXPECT_EQ(screen[i], nullptr) << "screen entry " << i;
|
||||
}
|
||||
const void* const* context = reinterpret_cast<const void* const*>(&gMGPipeContext);
|
||||
for (SizeT i = 0; i < kMGPipeContextCallCount; ++i) {
|
||||
EXPECT_EQ(context[i], nullptr) << "context entry " << i;
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
MGPipeInstallMonolithTables();
|
||||
// The 34 rows with no MGPipeApply* entry point are still null, and null still means "this
|
||||
// subsystem has not been migrated, keep pulling". Named rather than counted, because the
|
||||
// count is the other case's job and two cases asserting the same number would both go red
|
||||
// for one change.
|
||||
EXPECT_EQ(gMGPipeContext.SetShaderBuffers, nullptr);
|
||||
EXPECT_EQ(gMGPipeContext.SetStreamOutputTargets, nullptr);
|
||||
EXPECT_EQ(gMGPipeContext.DrawVbo, nullptr);
|
||||
EXPECT_EQ(gMGPipeContext.Present, nullptr);
|
||||
EXPECT_EQ(gMGPipeContext.SetSwapInterval, nullptr);
|
||||
EXPECT_EQ(gMGPipeScreen.GetCaps, nullptr);
|
||||
EXPECT_EQ(gMGPipeContext.QueryCreate, nullptr);
|
||||
EXPECT_EQ(gMGPipeScreen.FenceCreate, nullptr);
|
||||
#else
|
||||
// A pull build compiles no applier and no routing, so the pre-migration statement is the
|
||||
// whole truth there and this case is the one that says so.
|
||||
for (SizeT i = 0; i < kMGPipeScreenCallCount; ++i) EXPECT_EQ(screen[i], nullptr) << i;
|
||||
for (SizeT i = 0; i < kMGPipeContextCallCount; ++i) EXPECT_EQ(context[i], nullptr) << i;
|
||||
#endif
|
||||
(void)screen;
|
||||
(void)context;
|
||||
}
|
||||
|
||||
TEST(PipeCatalogue, ExactlyTheRoutedRowsAreInstalledAndTheRestAreStillNull) {
|
||||
const void* const* screen = reinterpret_cast<const void* const*>(&gMGPipeScreen);
|
||||
const void* const* context = reinterpret_cast<const void* const*>(&gMGPipeContext);
|
||||
SizeT installed = 0;
|
||||
SizeT nulls = 0;
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// IDEMPOTENT, and called here on purpose: what this case observes is WHICH rows the
|
||||
// installer fills, not whether an installer ran somewhere in this binary. Leaving that to
|
||||
// ambient linkage is what made the same assertion pass in one build directory and fail in
|
||||
// another - the object file carrying a static initialiser was dropped by the linker in the
|
||||
// binaries that did not name a symbol in it.
|
||||
MGPipeInstallMonolithTables();
|
||||
#endif
|
||||
|
||||
for (SizeT i = 0; i < kMGPipeScreenCallCount; ++i) {
|
||||
if (screen[i] != nullptr) ++installed; else ++nulls;
|
||||
}
|
||||
for (SizeT i = 0; i < kMGPipeContextCallCount; ++i) {
|
||||
if (context[i] != nullptr) ++installed; else ++nulls;
|
||||
}
|
||||
EXPECT_EQ(installed + nulls, static_cast<SizeT>(kMGPipeCallCount));
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// 33 + 4 = 37, and the split is the honest shape of R-17 rather than an implementation
|
||||
// detail: 37 is the number of MGPipeApply* entry points PipeApply.h declares, 33 of them
|
||||
// fit a GENERATED row and go in the two tables, and FOUR cannot be expressed by any
|
||||
// generated signature and go in the hand-written escape table beside them
|
||||
// (ResourceRespecify's uncarried initialBytes, ResourceFlushRange's likewise,
|
||||
// MapPersistent's size + seedBytes + void* return, CreateShaderState's seven blobrefs and
|
||||
// two typed pointers - each one a CONTRACT-P5 ruling, see MG_Pipe/PipeRoute.h).
|
||||
//
|
||||
// BOTH NUMBERS ARE ASSERTED. If the escape table were left out of this case, moving a row
|
||||
// out of the generated tables and forgetting to install its escape would read as a smaller
|
||||
// "installed" count and nothing else - and the call site would take a null.
|
||||
EXPECT_EQ(installed, 33u) << "the routed rows and the applier's entry points disagree";
|
||||
EXPECT_EQ(nulls, static_cast<SizeT>(kMGPipeCallCount) - 33u);
|
||||
const void* const* escapes = reinterpret_cast<const void* const*>(&gMGPipeRouteEscapes);
|
||||
SizeT escapesInstalled = 0;
|
||||
for (SizeT i = 0; i < sizeof(MGPipeRouteEscapes) / sizeof(void*); ++i) {
|
||||
if (escapes[i] != nullptr) ++escapesInstalled;
|
||||
}
|
||||
EXPECT_EQ(escapesInstalled, 4u) << "an escape row is null; its call site would take a null "
|
||||
"pointer rather than fall back to anything";
|
||||
EXPECT_EQ(installed + escapesInstalled, 37u)
|
||||
<< "the two tables plus the escapes must be exactly PipeApply.h's entry points";
|
||||
|
||||
// And the rows that MUST still be null, named rather than counted: these are calls with no
|
||||
// applier at all (CONTRACT-P5 table 1 rows 13, 14: "no applier entry point exists"), plus
|
||||
// the two verbs the census measured as having zero MG_Impl call sites. An installer that
|
||||
// filled one of these would be claiming an implementation that does not exist.
|
||||
EXPECT_EQ(gMGPipeContext.SetShaderBuffers, nullptr);
|
||||
EXPECT_EQ(gMGPipeContext.SetStreamOutputTargets, nullptr);
|
||||
EXPECT_EQ(gMGPipeContext.DrawVbo, nullptr);
|
||||
EXPECT_EQ(gMGPipeContext.Present, nullptr);
|
||||
EXPECT_EQ(gMGPipeContext.SetSwapInterval, nullptr);
|
||||
EXPECT_EQ(gMGPipeScreen.GetCaps, nullptr);
|
||||
#else
|
||||
// A pull build compiles no applier and no routing, so the pre-migration statement is still
|
||||
// the whole truth there.
|
||||
EXPECT_EQ(installed, 0u);
|
||||
EXPECT_EQ(nulls, static_cast<SizeT>(kMGPipeCallCount));
|
||||
#endif
|
||||
}
|
||||
|
||||
// The retirement ratchet of the migration carrier (section 6.3): the constant and the
|
||||
|
||||
@@ -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
|
||||
@@ -4780,6 +4802,93 @@ TEST(DirectGLESBufferDrawProbe, UnderSplitTheRecordAloneAnswersTheLiveHostMapQue
|
||||
#endif
|
||||
}
|
||||
|
||||
// R-8's NEGATIVE CONTROL, at the level of the probe above rather than at the level of the
|
||||
// accessor. `CapsMirrorTest.AMaskWithoutAFamilyRefusesItAndNamesIt` already pins that
|
||||
// `ServerConsumes` counts and names a refusal; what it cannot pin is that the LIVENESS GATE
|
||||
// the probe above depends on actually asks it. This case is the pair: the op table is
|
||||
// registered - so the pre-R-8 read (`MGPipeGetResourceOps() != nullptr`) would answer
|
||||
// "enabled" - and the caps mask is EMPTY, so the only honest answer is "no consumer".
|
||||
//
|
||||
// AND THE REFUSAL IS COUNTED, NOT INFERRED. "No record appeared" is satisfied by a client that
|
||||
// never ran at all: a typo in the fixture, a subsystem bit left clear, a BufferObject that
|
||||
// threw. Asking `ConsumerRefusals()` for a DELTA and `LastRefusedSubsystem()` for the family's
|
||||
// own bit is a statement that the gate was reached, asked the mirror, and was told no - which
|
||||
// is the fact R-8 exists to establish. Put `MGPipeGetResourceOps() != nullptr` back into
|
||||
// MGPipeResourceSubsystemEnabled() and this goes red on the record, not on the counter.
|
||||
TEST(DirectGLESBufferDrawProbe, ACapsMaskWithoutTheResourceFamilyEmitsNothingAndCountsTheRefusal) {
|
||||
using namespace MobileGL;
|
||||
using namespace MobileGL::MG_Backend::DirectGLES;
|
||||
using namespace MobileGL::MG_State::GLState;
|
||||
|
||||
if (!EsprytSlotTablesEnabled()) {
|
||||
GTEST_SKIP() << "the handle-keyed resource table only exists on the {slot, gen} arm";
|
||||
}
|
||||
#if !MOBILEGL_BUILD_DISAGGREGATED
|
||||
GTEST_SKIP() << "MG_Config::Transport is a constexpr Monolith without the transport built in, "
|
||||
"so the split arm of this probe cannot be entered";
|
||||
#else
|
||||
const Uint64 previousPush = MG_Config::Features.PipePush;
|
||||
const auto previousTransport = MG_Config::Transport;
|
||||
MG_Pipe::MGPipeResourceOps ops{};
|
||||
MG_Config::Features.PipePush |= MG_Pipe::kMGPipeSubsystemResources;
|
||||
// THE SERVER's registration is present. Under the pre-R-8 gate this alone armed the client.
|
||||
MG_Pipe::MGPipeSetResourceOps(&ops);
|
||||
MG_Config::Transport = MG_Config::TransportMode::InProcess;
|
||||
|
||||
{
|
||||
// The CLIENT's answer: a snapshot that names no consumer at all. R-12 makes a second
|
||||
// arrival the invalidation, so adopting is how a mask is replaced; there is no
|
||||
// Invalidate() to call and inventing one would be a second spelling of the same edge.
|
||||
MG_Pipe::MGPCaps caps{};
|
||||
caps.CallMask = 0;
|
||||
MG_Remote::Client::CapsMirrorInstance().Adopt(caps, MG_Backend::FormatCapabilityCache{},
|
||||
RendererInfo{}, String{},
|
||||
BackendType::DirectGLES);
|
||||
}
|
||||
ASSERT_FALSE(MG_Remote::Client::CapsMirrorInstance().ServerConsumes(
|
||||
MG_Pipe::kMGPipeSubsystemResources))
|
||||
<< "the fixture's own mask consumes the family, so this case cannot observe a refusal";
|
||||
|
||||
MG_Remote::Client::ResetConsumerRefusalsForTest();
|
||||
{
|
||||
auto owner = MakeShared<BufferObject>(0u);
|
||||
owner->Respecify(256, nullptr);
|
||||
|
||||
// The MINT is unconditional in a push build (set_vertex_buffers names a buffer by
|
||||
// handle whether or not the resource family is on), so a handle is the expected state
|
||||
// and is NOT what this case reads.
|
||||
const MG_Pipe::MGPipeHandle res = MG_Pipe::MGPipeResourceTrackerInstance().Find(*owner);
|
||||
ASSERT_FALSE(MG_Pipe::MGPipeHandleIsNull(res))
|
||||
<< "the mint is unconditional; without a handle this case is observing the wrong "
|
||||
"absence";
|
||||
|
||||
auto& applier = MG_Pipe::MGPipeApplier();
|
||||
if (applier.Resources.size() > static_cast<SizeT>(res.Slot)) {
|
||||
EXPECT_FALSE(applier.Resources[res.Slot].Live)
|
||||
<< "resource_create reached the applier for a family the server told this client "
|
||||
"it does not consume - which is ID-39's 66 lost uploads in the other "
|
||||
"direction: records emitted to a consumer that is not there";
|
||||
}
|
||||
}
|
||||
|
||||
// THE COUNTED HALF. Both statements, because either alone is satisfiable by an accident:
|
||||
// a non-zero count alone could come from any family, and the family id alone could be left
|
||||
// over from an earlier case.
|
||||
EXPECT_GT(MG_Remote::Client::ConsumerRefusals(), 0u)
|
||||
<< "the liveness gate never asked the caps mirror. It is still reading "
|
||||
"MGPipeGetResourceOps(), which is the SERVER's registration and is null under a "
|
||||
"spawn - R-8's whole defect";
|
||||
EXPECT_EQ(MG_Remote::Client::LastRefusedSubsystem(), MG_Pipe::kMGPipeSubsystemResources)
|
||||
<< "a refusal was counted for some other family, so this case is not observing the "
|
||||
"resource gate it names";
|
||||
|
||||
MG_Config::Transport = previousTransport;
|
||||
MG_Pipe::MGPipeSetResourceOps(nullptr);
|
||||
MG_Config::Features.PipePush = previousPush;
|
||||
MG_Remote::Client::ResetConsumerRefusalsForTest();
|
||||
#endif
|
||||
}
|
||||
|
||||
// P3a REWORK M-1's gate (contract-review M2). The minting overload's symmetric `!=` is safe
|
||||
// because its handle comes out of the allocator and can never be behind the entry; the HANDLE
|
||||
// overload's input ARRIVES in a payload, so a generation BEHIND the live entry's is reachable -
|
||||
@@ -4914,4 +5023,8 @@ TEST(DirectGLESBufferDrawProbe, ALiveHostMapKeepsTheHandleArmProbeDirtyBetweenTw
|
||||
TEST(DirectGLESBufferDrawProbe, UnderSplitTheRecordAloneAnswersTheLiveHostMapQuestion) {
|
||||
GTEST_SKIP() << "the handle-keyed resource table is compiled only under MOBILEGL_PIPE_PUSH";
|
||||
}
|
||||
|
||||
TEST(DirectGLESBufferDrawProbe, ACapsMaskWithoutTheResourceFamilyEmitsNothingAndCountsTheRefusal) {
|
||||
GTEST_SKIP() << "the handle-keyed resource table is compiled only under MOBILEGL_PIPE_PUSH";
|
||||
}
|
||||
#endif // MOBILEGL_PIPE_PUSH
|
||||
|
||||
@@ -66,16 +66,28 @@ endif ()
|
||||
|
||||
gtest_discover_tests(PipeWireCodecTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||
|
||||
# P5 s1's handshake suite (wave 1.5, ID-46 findings 6 and 7): the two null-union guards driven
|
||||
# THROUGH ServerSession::Accept and ClientSession::StartOverTransportPair, and the ABI
|
||||
# fingerprint's sensitivity case driven from CapsAbiFingerprint(), the production entry point.
|
||||
# Separate from SessionTest for two reasons: it needs CapsCodec.h and the sessions, i.e. the GL
|
||||
# frontend's umbrella header that the ring-owning suite deliberately keeps out; and each guard's
|
||||
# refusal is asserted BY MESSAGE, which with the console sink compiled out (Defines.h) means a
|
||||
# log file this process names before anything logs - PipeWireCodecTest's own-main() shape.
|
||||
add_executable(SessionHandshakeTest SessionHandshakeTest.cpp)
|
||||
# P5 c1's, v1's and s1's suites. All three are registered on their own, and all three for
|
||||
# PipeWireCodecTest's reason: a Fatal arm reports through MGLOG_F, which writes to stdout and to
|
||||
# a named file and NEVER to stderr (and with the console sink compiled out, Defines.h, only to
|
||||
# the file), so asserting one means naming a log file before anything logs - which needs a
|
||||
# main() of its own. All three also reach MG_Pipe, MG_Backend and MG_State, so all three carry
|
||||
# those include paths.
|
||||
#
|
||||
# RemoteClientTest the 71-slot emit table, the caps mirror, R-8's liveness gates, R-17's
|
||||
# routing and ID-47/ID-49's readback rules (c1)
|
||||
# ServerLoopTest the apply thread, the blocking control mailbox, the verb stamp and
|
||||
# R-11's server-owned staging copy (v1)
|
||||
# SessionHandshakeTest the two null-union guards driven THROUGH ServerSession::Accept and
|
||||
# ClientSession::StartOverTransportPair, and the ABI fingerprint's
|
||||
# sensitivity case driven from CapsAbiFingerprint() (s1, ID-46 6 and 7)
|
||||
#
|
||||
# THIS FILE IS THE PHASE'S ONE RECURRING MERGE CONFLICT, and it is the same-point-append shape
|
||||
# BRIEF §5's ownership table exists to prevent: three packages, three targets, one end-of-file.
|
||||
# The loop is what stops there being a fourth: a package adding a suite adds a NAME.
|
||||
foreach (wiretest IN ITEMS RemoteClientTest ServerLoopTest SessionHandshakeTest)
|
||||
add_executable(${wiretest} ${wiretest}.cpp)
|
||||
|
||||
target_include_directories(SessionHandshakeTest PRIVATE
|
||||
target_include_directories(${wiretest} PRIVATE
|
||||
${MGL_ROOT}/include
|
||||
${MGL_ROOT}/MobileGL
|
||||
${MGL_ROOT}/MobileGL/MG_Pipe
|
||||
@@ -83,15 +95,16 @@ target_include_directories(SessionHandshakeTest PRIVATE
|
||||
${MGL_ROOT}/3rdparty/xxHash
|
||||
${MGL_ROOT}/3rdparty/Vulkan-Headers/include
|
||||
${MGL_ROOT}/3rdparty/SPIRV-Reflect
|
||||
)
|
||||
)
|
||||
|
||||
target_link_libraries(SessionHandshakeTest PRIVATE
|
||||
target_link_libraries(${wiretest} PRIVATE
|
||||
GTest::gtest
|
||||
${LINK_LIBRARIES}
|
||||
)
|
||||
)
|
||||
|
||||
if (MSVC)
|
||||
target_compile_options(SessionHandshakeTest PRIVATE /Zc:preprocessor)
|
||||
endif ()
|
||||
if (MSVC)
|
||||
target_compile_options(${wiretest} PRIVATE /Zc:preprocessor)
|
||||
endif ()
|
||||
|
||||
gtest_discover_tests(SessionHandshakeTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||
gtest_discover_tests(${wiretest} DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||
endforeach ()
|
||||
|
||||
@@ -233,6 +233,30 @@ namespace {
|
||||
std::uint64_t m_sessionApplied = 0;
|
||||
};
|
||||
|
||||
// ID-31 + R-17's cross-check, written once because four cases need it.
|
||||
//
|
||||
// The four Bool acceptance rows now answer into SEG_REPLY, and "an answer was written" is
|
||||
// a weak statement on its own - a PostReply that stamped a constant OK would satisfy it.
|
||||
// So the STATUSES are compared against the decoder's own accepted/declined counters, which
|
||||
// are produced by a different line of code from a different value. A single-sided
|
||||
// assertion here is exactly the shape R-16 was written after.
|
||||
void ExpectRepliesAgreeWithTheAcceptanceTally(Wire2& wire) {
|
||||
std::uint64_t ok = 0;
|
||||
std::uint64_t declined = 0;
|
||||
for (const auto& reply : wire.Answers().All) {
|
||||
EXPECT_NE(reply.Status, ReplySink::kStatusError)
|
||||
<< "seq " << reply.Seq << ": ERROR is not an acceptance answer";
|
||||
if (reply.Status == ReplySink::kStatusOk) ++ok;
|
||||
if (reply.Status == ReplySink::kStatusDeclined) ++declined;
|
||||
}
|
||||
EXPECT_EQ(ok, wire.Decoder().AcceptedRecords())
|
||||
<< "the slots say " << ok << " accepted, the decoder's tally says "
|
||||
<< wire.Decoder().AcceptedRecords();
|
||||
EXPECT_EQ(declined, wire.Decoder().DeclinedRecords())
|
||||
<< "the slots say " << declined << " declined, the decoder's tally says "
|
||||
<< wire.Decoder().DeclinedRecords();
|
||||
}
|
||||
|
||||
MGPipeHandle MakeHandle(Uint32 slot, Uint32 gen = 1) {
|
||||
MGPipeHandle handle{};
|
||||
handle.Slot = slot;
|
||||
@@ -576,12 +600,22 @@ TEST_F(PipeWireCodecTest, KNeedsAckRespecifyCarriesItsRedefinitionScope) {
|
||||
kInvalidSeq);
|
||||
ASSERT_TRUE(wire.PumpOne(&applied));
|
||||
EXPECT_TRUE(applied);
|
||||
// Two records, two ACCEPTANCE answers - and NOT in a reply slot. Neither ResourceCreate
|
||||
// nor ResourceRespecify carries kReplySlot, and s1 sizes ReplyPool from that table, so a
|
||||
// reply written here would overwrite some waiter's slot.
|
||||
EXPECT_TRUE(wire.Answers().All.empty());
|
||||
// Two records, two ACCEPTANCE answers, AND THEY NOW RIDE THE REPLY SLOT (ID-31 + R-17).
|
||||
// Both rows carry kReplySlot in kMGPipeCallFlags, ReplyPool is sized from that same table,
|
||||
// and CONTRACT-P5 table 0 always said DECLINED "is how the four Bool acceptance entry
|
||||
// points say false". The two halves agreed the moment ID-31 landed the flag.
|
||||
ASSERT_EQ(wire.Answers().All.size(), 2u);
|
||||
EXPECT_EQ(wire.Answers().All[0].Seq, 1u);
|
||||
EXPECT_EQ(wire.Answers().All[1].Seq, 2u);
|
||||
EXPECT_TRUE(wire.Answers().All[0].Bytes.empty());
|
||||
EXPECT_TRUE(wire.Answers().All[1].Bytes.empty());
|
||||
EXPECT_EQ(wire.Decoder().AcceptedRecords() + wire.Decoder().DeclinedRecords(), 2u);
|
||||
EXPECT_TRUE(wire.Decoder().LastAcceptanceKnown());
|
||||
// THE CROSS-CHECK, and it is the point of asserting the status at all: the slot's status
|
||||
// and the decoder's own tally are two independent statements of the same fact, so a
|
||||
// PostReply that stamped a constant would disagree with the counters even though every
|
||||
// other assertion above still passed.
|
||||
ExpectRepliesAgreeWithTheAcceptanceTally(wire);
|
||||
}
|
||||
|
||||
TEST_F(PipeWireCodecTest, KOptionalUnmapPersistentRoundTrips) {
|
||||
@@ -774,7 +808,12 @@ TEST_F(PipeWireCodecTest, ResourceSubDataCarriesABlobAndARegionTailTogether) {
|
||||
kInvalidSeq);
|
||||
ASSERT_TRUE(wire.PumpOne(&applied));
|
||||
EXPECT_TRUE(applied);
|
||||
EXPECT_TRUE(wire.Answers().All.empty());
|
||||
// ID-31 + R-17: ResourceCreate and ResourceSubData both carry kReplySlot now, so both
|
||||
// answer into a slot rather than only into the decoder's tally.
|
||||
ASSERT_EQ(wire.Answers().All.size(), 2u);
|
||||
EXPECT_EQ(wire.Answers().All[0].Seq, 1u);
|
||||
EXPECT_EQ(wire.Answers().All[1].Seq, 2u);
|
||||
ExpectRepliesAgreeWithTheAcceptanceTally(wire);
|
||||
// ACCEPTANCE IS NOT "APPLIED", and this case is where the difference shows. The record
|
||||
// crossed and reached MGPipeApplyResourceSubData, which is the codec's whole job; the
|
||||
// applier then DECLINED it, because no backend registered a P4a texture consumer in this
|
||||
@@ -1268,11 +1307,24 @@ TEST_F(PipeWireCodecTest, NoReplyIsWrittenForARowTheCatalogueGivesNoReplySlot) {
|
||||
// s1 sizes ReplyPool from MGPipeCallFlagsFor, so a reply written for a record the pool
|
||||
// reserved no slot for overwrites a waiter's answer - and because the slot header stamps
|
||||
// the WRITER's seq for self-check, the waiter's check then fails for ever and the barrier
|
||||
// HANGS rather than returning something wrong.
|
||||
// HANGS rather than returning something wrong. That statement is unchanged and is what
|
||||
// this case still asserts.
|
||||
//
|
||||
// WHAT CHANGED IS THE ROW IT ASSERTS IT ABOUT (ID-31 fallout, closed by c1 round 2). It
|
||||
// used to name ResourceCreate and SetTextureParams as rows "the catalogue gives no reply
|
||||
// slot", which ID-31 made false - it gave ResourceCreate the flag, and all four Bool
|
||||
// acceptance rows carry it now. A case that names a kReplySlot row while asserting a row
|
||||
// has no slot is not testing the rule, it is testing a stale catalogue, so the row moved
|
||||
// to one that genuinely carries kNone and the rule stayed exactly where it was.
|
||||
Wire2 wire;
|
||||
ASSERT_EQ(MGPipeCallFlagsFor(MGPWireOp::ResourceCreate) & static_cast<Uint32>(kReplySlot), 0u);
|
||||
ASSERT_EQ(MGPipeCallFlagsFor(MGPWireOp::SetTextureParams) & static_cast<Uint32>(kReplySlot), 0u);
|
||||
ASSERT_EQ(MGPipeCallFlagsFor(MGPWireOp::ResourceDestroy) & static_cast<Uint32>(kReplySlot), 0u)
|
||||
<< "ResourceDestroy gained a reply slot; this case needs a kNone row to mean anything";
|
||||
ASSERT_EQ(MGPipeCallFlagsFor(MGPWireOp::BindRenderState) & static_cast<Uint32>(kReplySlot), 0u);
|
||||
|
||||
// A create first, so the destroy has a live record to reach - and its OWN answer is the
|
||||
// control on the control: if PostReply were firing indiscriminately there would be two
|
||||
// answers here, not one, and the case would fail on the count rather than pass because
|
||||
// nothing was ever written.
|
||||
MGPResourceDesc create{};
|
||||
create.Resource = MakeHandle(131);
|
||||
create.Target = static_cast<Uint8>(MGPipeResourceTarget::Buffer);
|
||||
@@ -1286,23 +1338,63 @@ TEST_F(PipeWireCodecTest, NoReplyIsWrittenForARowTheCatalogueGivesNoReplySlot) {
|
||||
kInvalidSeq);
|
||||
bool applied = false;
|
||||
ASSERT_TRUE(wire.PumpOne(&applied));
|
||||
ASSERT_EQ(wire.Answers().All.size(), 1u) << "the kReplySlot row did not answer";
|
||||
|
||||
const MGPHandleOnly destroy = HandleOnly(131, MGPipeKind::Buffer);
|
||||
ASSERT_NE(wire.Encoder().EncodeRecord(MGPWireOp::ResourceDestroy, &destroy, sizeof(destroy)),
|
||||
kInvalidSeq);
|
||||
ASSERT_TRUE(wire.PumpOne(&applied));
|
||||
EXPECT_TRUE(applied);
|
||||
|
||||
EXPECT_TRUE(wire.Answers().All.empty()) << "a reply slot was written for a kNone row";
|
||||
// The acceptance answer R-5 requires is still produced - it just does not ride SEG_REPLY.
|
||||
EXPECT_TRUE(wire.Decoder().LastAcceptanceKnown());
|
||||
EXPECT_EQ(wire.Decoder().AcceptedRecords() + wire.Decoder().DeclinedRecords(), 1u);
|
||||
EXPECT_EQ(wire.Answers().All.size(), 1u) << "a reply slot was written for a kNone row";
|
||||
EXPECT_EQ(wire.Answers().All[0].Seq, 1u) << "the answer that exists is the create's";
|
||||
}
|
||||
|
||||
TEST_F(PipeWireCodecTest, EveryRowThatDoesWriteAReplyCarriesKReplySlot) {
|
||||
// The other half: the three rows in P5 that answer into a slot all carry the flag, so
|
||||
// PostReply's gate cannot be firing on any of them.
|
||||
for (const MGPWireOp op :
|
||||
{MGPWireOp::MapPersistent, MGPWireOp::ResourceReadback, MGPWireOp::ReadPixels}) {
|
||||
// The other half: every row in P5 that answers into a slot carries the flag, so
|
||||
// PostReply's gate cannot be firing on any of them. The four Bool acceptance rows joined
|
||||
// this list at ID-31 and R-17 - and they are listed BY NAME rather than derived from the
|
||||
// flags table, because deriving the expectation from the same table the assertion reads
|
||||
// would make this case true by construction.
|
||||
for (const MGPWireOp op : {MGPWireOp::MapPersistent, MGPWireOp::ResourceReadback,
|
||||
MGPWireOp::ReadPixels, MGPWireOp::ResourceCreate,
|
||||
MGPWireOp::ResourceRespecify, MGPWireOp::ResourceSubData,
|
||||
MGPWireOp::SetTextureParams}) {
|
||||
EXPECT_NE(MGPipeCallFlagsFor(op) & static_cast<Uint32>(kReplySlot), 0u) << WireOpName(op);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(PipeWireCodecTest, TheFourAcceptanceRowsAnswerThroughTheSlotAndNotOnlyThroughTheTally) {
|
||||
// R-17's server half, and the reason c1 could not simply write thirty-seven emitters: the
|
||||
// client asks the CATALOGUE whether a row owns a slot (ClientSession::EmitAndWait), so the
|
||||
// moment ID-31 gave these rows kReplySlot, a decoder that answered only into
|
||||
// LastAcceptance() left the client parked in the barrier until Fatal{ReplyMissing}.
|
||||
//
|
||||
// THE ASSERTION IS THAT THE ANSWER MATCHES THE APPLIER's, not that one exists. Under a
|
||||
// unit process no backend registers a P4a consumer, so set_texture_params is DECLINED -
|
||||
// and that is the useful direction: a PostReply hard-coded to OK would pass a case that
|
||||
// only counted answers.
|
||||
Wire2 wire;
|
||||
MGPTextureParams params{};
|
||||
params.Res = MakeHandle(77);
|
||||
params.BuiltinSampler = MakeHandle(78);
|
||||
ASSERT_NE(wire.Encoder().EncodeRecord(MGPWireOp::SetTextureParams, ¶ms, sizeof(params)),
|
||||
kInvalidSeq);
|
||||
bool applied = false;
|
||||
ASSERT_TRUE(wire.PumpOne(&applied));
|
||||
EXPECT_TRUE(applied) << "the record must still have CROSSED; acceptance is not 'applied'";
|
||||
|
||||
ASSERT_EQ(wire.Answers().All.size(), 1u);
|
||||
EXPECT_EQ(wire.Answers().All[0].Seq, 1u) << "the id is the record's own ordinal (R-3)";
|
||||
EXPECT_TRUE(wire.Answers().All[0].Bytes.empty())
|
||||
<< "an acceptance answer is a STATUS; DECLINED carries no payload";
|
||||
ASSERT_TRUE(wire.Decoder().LastAcceptanceKnown());
|
||||
EXPECT_EQ(wire.Answers().All[0].Status, wire.Decoder().LastAcceptance()
|
||||
? ReplySink::kStatusOk
|
||||
: ReplySink::kStatusDeclined);
|
||||
ExpectRepliesAgreeWithTheAcceptanceTally(wire);
|
||||
}
|
||||
|
||||
TEST_F(PipeWireCodecTest, TheAuditFillOverwritesExactlyTheRunsTheRecordResolved) {
|
||||
// R-2.5, the only mechanical control on rule C ("no applier entry point retains a pointer
|
||||
// past its return"). An instrumentation that cannot be observed to have run is decoration,
|
||||
|
||||
@@ -0,0 +1,811 @@
|
||||
// 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 <vector>
|
||||
#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/Transport/ReplySlot.h>
|
||||
#include <MG_Pipe/PipeRoute.h>
|
||||
#include <MG_Remote/Client/EmitTables.h>
|
||||
#include <MG_Remote/Client/WireTables.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";
|
||||
}
|
||||
|
||||
// =====================================================================================
|
||||
// ID-47: a readback larger than a reply slot is refused at the CLIENT, by name
|
||||
// =====================================================================================
|
||||
|
||||
TEST(RemoteReadback, ExactlyTheCapacityPassesAndOneByteMoreIsRefusedByName) {
|
||||
// ID-47's boundary pair, and it is THE CALL SITE'S half. The refusal itself is s1's
|
||||
// (ReplySlotPool::RequireReadPixelsFits, its own cases in s1-v3.md §1); what c1 owns is
|
||||
// that the number handed to it is the one the emitter computes - ID-49's TIGHT extent -
|
||||
// and that exactly the cap is legal while one byte more is not. So this drives the real
|
||||
// pool with the real helper, over the real arithmetic, and never restates the message.
|
||||
//
|
||||
// A real pool over a real mapping, because CanHold answers false for a null base and a
|
||||
// control built on a default-constructed pool would "refuse" everything for that reason.
|
||||
constexpr std::uint32_t kSlots = 8;
|
||||
constexpr std::uint64_t kSlotBytes = 2u * 1024u * 1024u;
|
||||
std::vector<Uint8> backing(static_cast<size_t>(kSlots) * kSlotBytes);
|
||||
Transport::ReplySlotPool pool(backing.data(), backing.size(), kSlots);
|
||||
const std::uint64_t cap = pool.MaxReplyBytes();
|
||||
ASSERT_GT(cap, 0u) << "the fixture's pool is not configured, so every answer would be refused";
|
||||
|
||||
// ID-47's own number: the E2 retrace snapshot reads 640x480 RGBA8 and it must now FIT.
|
||||
EXPECT_TRUE(pool.CanHold(TightReadbackByteCount(640, 480, 0x1908, 0x1401)))
|
||||
<< "the read ID-47 grew SEG_REPLY for still does not fit";
|
||||
|
||||
pool.RequireReadPixelsFits(724, 724, 0x1908, 0x1401, cap);
|
||||
SUCCEED() << "exactly the capacity is not an overflow";
|
||||
|
||||
#if MGTEST_HAVE_FORK
|
||||
const ChildResult child = RunInChild([&] {
|
||||
Transport::ReplySlotPool inner(backing.data(), backing.size(), kSlots);
|
||||
inner.RequireReadPixelsFits(640, 480, 0x1908, 0x1401, inner.MaxReplyBytes() + 1);
|
||||
});
|
||||
EXPECT_TRUE(DiedOfAbort(child)) << "one byte over the slot did not abort: " << DescribeStatus(child);
|
||||
// ITS OWN FAILURE STRING, AND THE READ'S OWN NUMBERS. A control that only asserted
|
||||
// "something died" would pass on Fatal{NoClientSession}, Fatal{UnmigratedVerb} or a
|
||||
// segfault, and this file has four other cases that abort for those reasons.
|
||||
EXPECT_NE(child.Log.find("Fatal{ReplyTooLarge"), std::string::npos) << child.Log;
|
||||
EXPECT_NE(child.Log.find("ReadPixels 640x480"), std::string::npos)
|
||||
<< "the message does not name the read, so an operator cannot tell which one: " << child.Log;
|
||||
EXPECT_NE(child.Log.find(std::to_string(cap + 1)), std::string::npos)
|
||||
<< "the message does not carry the byte count";
|
||||
#else
|
||||
GTEST_SKIP() << "the refusal reports through a Fatal + abort and needs fork() to read back";
|
||||
#endif
|
||||
}
|
||||
|
||||
// =====================================================================================
|
||||
// ID-49: the pack state never crosses; the client scatters the tight rows
|
||||
// =====================================================================================
|
||||
|
||||
TEST(RemoteReadback, DstSizeIsTheTightExtentAndNeverThePackedOne) {
|
||||
// The number v1 allocates on the server. It must not move with the pack state, because the
|
||||
// server reads with a NEUTRAL one and cannot see the client's: the first version of this
|
||||
// emitter declared GL 8.4.4's PACKED size, v1 allocated exactly that, and the driver then
|
||||
// wrote past it - two SEGFAULTs on the joint inproc lane, both backends.
|
||||
constexpr GLenum kRgba = 0x1908;
|
||||
constexpr GLenum kUByte = 0x1401;
|
||||
EXPECT_EQ(TightReadbackByteCount(4, 3, kRgba, kUByte), 4u * 3u * 4u);
|
||||
EXPECT_EQ(TightReadbackByteCount(640, 480, kRgba, kUByte), 640u * 480u * 4u);
|
||||
// ID-47's own arithmetic depends on this: 640x480 RGBA8 is what the E2 retrace snapshot
|
||||
// reads, and 1,228,800 is the number that forced SEG_REPLY to grow.
|
||||
EXPECT_EQ(TightReadbackByteCount(640, 480, kRgba, kUByte), 1228800u);
|
||||
}
|
||||
|
||||
TEST(RemoteReadback, TheTightRowsAreScatteredWhereThePackStateSaysAndTheGapsAreLeftAlone) {
|
||||
// ID-49's control, and it is the exact case the cross-family review found: 4x3 RGBA8 with
|
||||
// PACK_ROW_LENGTH=8, SKIP_ROWS=1, SKIP_PIXELS=2. Stride = 8*4 = 32; the first written byte
|
||||
// is 1*32 + 2*4 = 40; each row writes 4*4 = 16 bytes and the remaining 16 of its stride
|
||||
// belong to the application.
|
||||
constexpr Uint64 kBpp = 4;
|
||||
constexpr GLsizei kW = 4;
|
||||
constexpr GLsizei kH = 3;
|
||||
constexpr Uint8 kSentinel = 0xCD;
|
||||
|
||||
PixelStoreParameters pack{};
|
||||
pack.RowLength = 8;
|
||||
pack.SkipRows = 1;
|
||||
pack.SkipPixels = 2;
|
||||
pack.Alignment = 4;
|
||||
|
||||
// THE INPUT, not the state under test: every source byte is distinct, so a scatter that
|
||||
// wrote the right COUNT of bytes from the wrong offset cannot look correct.
|
||||
std::vector<Uint8> tight(static_cast<size_t>(kW) * kH * kBpp);
|
||||
for (size_t i = 0; i < tight.size(); ++i) tight[i] = static_cast<Uint8>(i);
|
||||
|
||||
std::vector<Uint8> destination(512, kSentinel);
|
||||
ScatterTightReadbackIntoPackState(tight.data(), destination.data(), kW, kH, kBpp, pack);
|
||||
|
||||
const size_t stride = 8 * 4;
|
||||
const size_t first = 1 * stride + 2 * 4;
|
||||
for (size_t row = 0; row < static_cast<size_t>(kH); ++row) {
|
||||
for (size_t byte = 0; byte < static_cast<size_t>(kW) * kBpp; ++byte) {
|
||||
EXPECT_EQ(destination[first + row * stride + byte],
|
||||
tight[row * static_cast<size_t>(kW) * kBpp + byte])
|
||||
<< "row " << row << " byte " << byte << " landed somewhere else";
|
||||
}
|
||||
}
|
||||
|
||||
// THE GAPS, which is the half that makes this a control rather than a copy of the loop
|
||||
// above: the bytes the pack state does not name belong to the application and must still
|
||||
// hold their sentinel. Removing the skips from the scatter passes the loop above and fails
|
||||
// here; widening the per-row copy to the stride passes both loops above and fails here.
|
||||
size_t touched = 0;
|
||||
for (size_t i = 0; i < destination.size(); ++i) {
|
||||
const bool inWrittenRow =
|
||||
i >= first && ((i - first) % stride) < static_cast<size_t>(kW) * kBpp &&
|
||||
((i - first) / stride) < static_cast<size_t>(kH);
|
||||
if (!inWrittenRow) {
|
||||
EXPECT_EQ(destination[i], kSentinel)
|
||||
<< "byte " << i << " is outside the rectangle GL names and was overwritten";
|
||||
} else {
|
||||
++touched;
|
||||
}
|
||||
}
|
||||
EXPECT_EQ(touched, tight.size()) << "the scatter wrote a different number of bytes than the "
|
||||
"reply carried";
|
||||
}
|
||||
|
||||
TEST(RemoteReadback, PackSkipImagesIsIgnoredForATwoDimensionalRead) {
|
||||
// codex 6: SKIP_IMAGES (and IMAGE_HEIGHT) are image-level pack parameters and GL ignores
|
||||
// them for glReadPixels, a 2-D read - the monolith conversion path says so with
|
||||
// honorPackImageParams=false (DirectGLES.cpp:10905). The first cut applied SKIP_IMAGES as a
|
||||
// whole-image offset: a 4x3 RGBA8 read with SKIP_IMAGES=1 wrote bytes 48..95 of a 96-byte
|
||||
// destination sized for one image, overrunning it. With the fix the reply lands at bytes
|
||||
// 0..47 and the second image's worth of bytes keeps its sentinel.
|
||||
constexpr Uint64 kBpp = 4;
|
||||
constexpr GLsizei kW = 4;
|
||||
constexpr GLsizei kH = 3;
|
||||
constexpr Uint8 kSentinel = 0xEE;
|
||||
|
||||
PixelStoreParameters pack{};
|
||||
pack.SkipImages = 1; // the parameter under test
|
||||
pack.ImageHeight = kH; // and its companion; both must be ignored
|
||||
|
||||
std::vector<Uint8> tight(static_cast<size_t>(kW) * kH * kBpp);
|
||||
for (size_t i = 0; i < tight.size(); ++i) tight[i] = static_cast<Uint8>(i + 1);
|
||||
|
||||
std::vector<Uint8> destination(96, kSentinel);
|
||||
ScatterTightReadbackIntoPackState(tight.data(), destination.data(), kW, kH, kBpp, pack);
|
||||
|
||||
for (size_t i = 0; i < tight.size(); ++i)
|
||||
EXPECT_EQ(destination[i], tight[i]) << "byte " << i << " should hold the reply at offset 0";
|
||||
for (size_t i = tight.size(); i < destination.size(); ++i)
|
||||
EXPECT_EQ(destination[i], kSentinel)
|
||||
<< "byte " << i << " is past the read's own extent and SKIP_IMAGES must not have moved "
|
||||
"the write there";
|
||||
// And the fast path takes it: an otherwise-neutral read with only SKIP_IMAGES set is tight.
|
||||
EXPECT_TRUE(ReadbackPackStateIsTightForTest(kW, kBpp, pack))
|
||||
<< "SKIP_IMAGES alone must not force the bounce path for a 2-D read";
|
||||
}
|
||||
|
||||
TEST(RemoteReadback, TheFastPathIsTakenExactlyWhenTheScatterWouldChangeNothing) {
|
||||
// EmitReadPixels reads the reply STRAIGHT into the application's pointer when
|
||||
// ReadbackPackStateIsTight says so, and pays for a bounce buffer otherwise. That is only
|
||||
// legal if the predicate and the scatter AGREE - so this case drives BOTH and compares
|
||||
// them, rather than testing either alone. A predicate that said "tight" for a layout the
|
||||
// scatter would have rearranged is a silently wrong picture with no bounce to blame.
|
||||
constexpr Uint64 kBpp = 4;
|
||||
constexpr GLsizei kW = 5;
|
||||
constexpr GLsizei kH = 3;
|
||||
std::vector<Uint8> tight(static_cast<size_t>(kW) * kH * kBpp);
|
||||
for (size_t i = 0; i < tight.size(); ++i) tight[i] = static_cast<Uint8>(i * 7 + 1);
|
||||
|
||||
// Six layouts, chosen so both answers appear: a bare default, an explicit equal row
|
||||
// length, an alignment the row already satisfies, an alignment it does not, a skip, and a
|
||||
// wider row. If every case agreed on "tight" the comparison below would be vacuous, so the
|
||||
// count of each answer is asserted too.
|
||||
std::vector<PixelStoreParameters> layouts(6);
|
||||
layouts[1].RowLength = kW;
|
||||
layouts[2].Alignment = 4; // 5*4 = 20, already a multiple of 4
|
||||
layouts[3].Alignment = 8; // 20 is not a multiple of 8 - the rows gain padding
|
||||
layouts[4].SkipPixels = 1;
|
||||
layouts[5].RowLength = 8;
|
||||
|
||||
int tightCount = 0;
|
||||
for (size_t i = 0; i < layouts.size(); ++i) {
|
||||
const PixelStoreParameters& pack = layouts[i];
|
||||
std::vector<Uint8> destination(4096, 0);
|
||||
ScatterTightReadbackIntoPackState(tight.data(), destination.data(), kW, kH, kBpp, pack);
|
||||
destination.resize(tight.size());
|
||||
const bool scatterChangedNothing = (destination == tight);
|
||||
const bool predicateSaysTight =
|
||||
ReadbackPackStateIsTightForTest(kW, kBpp, pack) != 0;
|
||||
EXPECT_EQ(predicateSaysTight, scatterChangedNothing)
|
||||
<< "layout " << i << ": the fast-path predicate and the scatter disagree";
|
||||
if (predicateSaysTight) ++tightCount;
|
||||
}
|
||||
EXPECT_GT(tightCount, 0) << "no layout took the fast path, so the equality above is vacuous";
|
||||
EXPECT_LT(tightCount, static_cast<int>(layouts.size()))
|
||||
<< "every layout took the fast path, so the equality above is vacuous";
|
||||
}
|
||||
|
||||
// =====================================================================================
|
||||
// R-17: the routing, its reply mailbox, and the arm that is actually installed
|
||||
// =====================================================================================
|
||||
|
||||
TEST(PipeRouting, TheTablesAreInstalledWithoutAnybodyHavingRememberedTo) {
|
||||
// The gate on the install MECHANISM rather than on the table contents (PipeCatalogueTest
|
||||
// owns the partition). Nothing in this file calls an installer; the tables are installed
|
||||
// because MG_Pipe/PipeRoute.h's inline variable is in this binary, which is the property
|
||||
// that a static initialiser inside PipeRoute.cpp did NOT have - the linker dropped that
|
||||
// object from every test binary that named no symbol in it, and five CsoCacheTest cases
|
||||
// took a null function pointer.
|
||||
EXPECT_TRUE(MGPipeTablesAreInstalled());
|
||||
EXPECT_EQ(static_cast<int>(MGPipeInstalledArm()), static_cast<int>(MGPipeRouteArm::kMonolith))
|
||||
<< "this process has no ClientSession, so the monolith adapters must be the arm";
|
||||
}
|
||||
|
||||
TEST(PipeRouting, AnAnswerIsWhatTheRowSaidAndDeclinedIsFalseRatherThanAFailure) {
|
||||
const Uint64 takenBefore = MGPipeRepliesTaken();
|
||||
const Uint64 declinedBefore = MGPipeRepliesDeclined();
|
||||
|
||||
const MGPReplySlot ok = MGPipeMintReplySlot();
|
||||
MGPipePostReply(ok, 0 /*OK*/, 1);
|
||||
EXPECT_TRUE(MGPipeTakeReplyBool(ok, "unit"));
|
||||
|
||||
const MGPReplySlot declined = MGPipeMintReplySlot();
|
||||
MGPipePostReply(declined, 1 /*DECLINED*/, 0);
|
||||
EXPECT_FALSE(MGPipeTakeReplyBool(declined, "unit"))
|
||||
<< "DECLINED is how the four Bool acceptance rows say false (R-5), not how they fail";
|
||||
|
||||
EXPECT_EQ(MGPipeRepliesTaken(), takenBefore + 2u);
|
||||
EXPECT_EQ(MGPipeRepliesDeclined(), declinedBefore + 1u)
|
||||
<< "the refusal was not COUNTED, so 'the client accepted everything' and 'the client "
|
||||
"never asked' are still the same observation from outside";
|
||||
|
||||
// The two slots are different ids, which is what makes the mismatch Fatal below meaningful.
|
||||
EXPECT_NE(ok.Id, declined.Id);
|
||||
EXPECT_NE(ok.Id, 0u) << "slot 0 is ReplySlot.h's 'no record' and must stay unmintable";
|
||||
}
|
||||
|
||||
#if MGTEST_HAVE_FORK
|
||||
TEST(PipeRouting, AnUnansweredRowIsFatalRatherThanAcceptedOrRefused) {
|
||||
// R-5's whole point, made structural. There is no default: "always accept" is ID-39's 66
|
||||
// lost DirectVulkan uploads with a wire in between, and "always refuse" is an emitter that
|
||||
// re-sends for ever. A row that forgets to answer has to be impossible to READ.
|
||||
const ChildResult child = RunInChild([] {
|
||||
const MGPReplySlot slot = MGPipeMintReplySlot();
|
||||
(void)MGPipeTakeReplyBool(slot, "resource_create");
|
||||
});
|
||||
EXPECT_TRUE(DiedOfAbort(child)) << DescribeStatus(child);
|
||||
EXPECT_NE(child.Log.find("Fatal{ReplyMissing"), std::string::npos) << child.Log;
|
||||
EXPECT_NE(child.Log.find("resource_create"), std::string::npos)
|
||||
<< "the Fatal does not name the row, so it cannot say WHICH answer went missing";
|
||||
}
|
||||
|
||||
TEST(PipeRouting, TwoOutstandingAnswersAreFatalBecauseTheBarrierIsOneDeep) {
|
||||
// The mailbox is one entry deep because R-1's verb barrier makes the in-flight depth one
|
||||
// (ReplySlot.h). A second posting before the first is taken is not a capacity problem, it
|
||||
// is a barrier that has stopped holding - so it must not be absorbed by a deeper mailbox.
|
||||
const ChildResult child = RunInChild([] {
|
||||
const MGPReplySlot first = MGPipeMintReplySlot();
|
||||
const MGPReplySlot second = MGPipeMintReplySlot();
|
||||
MGPipePostReply(first, 0, 1);
|
||||
MGPipePostReply(second, 0, 1);
|
||||
});
|
||||
EXPECT_TRUE(DiedOfAbort(child)) << DescribeStatus(child);
|
||||
EXPECT_NE(child.Log.find("Fatal{ReplyOverrun"), std::string::npos) << child.Log;
|
||||
}
|
||||
|
||||
TEST(PipeRouting, AnErrorStatusIsNotFoldedIntoAcceptedOrRefused) {
|
||||
// ERROR is a transport fault and DECLINED is a resource decision. Folding the first into
|
||||
// either arm of the second makes a broken wire look like a server that said no.
|
||||
const ChildResult child = RunInChild([] {
|
||||
const MGPReplySlot slot = MGPipeMintReplySlot();
|
||||
MGPipePostReply(slot, 2 /*ERROR*/, 0);
|
||||
(void)MGPipeTakeReplyBool(slot, "set_texture_params");
|
||||
});
|
||||
EXPECT_TRUE(DiedOfAbort(child)) << DescribeStatus(child);
|
||||
EXPECT_NE(child.Log.find("Fatal{ReplyError"), std::string::npos) << child.Log;
|
||||
EXPECT_NE(child.Log.find("set_texture_params"), std::string::npos) << child.Log;
|
||||
}
|
||||
#endif // MGTEST_HAVE_FORK
|
||||
|
||||
// =====================================================================================
|
||||
// B3 / codex 9: the CLIENT arm's 37 rows are observed, not just the monolith install
|
||||
// =====================================================================================
|
||||
|
||||
namespace {
|
||||
// How many function-pointer cells of `a` differ from `b`, walked as a block of void* -
|
||||
// the structs ARE their function pointers (PipeCatalogueTest static_asserts that shape). A
|
||||
// routed row that stayed on the monolith adapter reads EQUAL and is not counted, which is
|
||||
// exactly the defect this measures.
|
||||
template <class T>
|
||||
SizeT CountDifferingCells(const T& a, const T& b) {
|
||||
const void* const* pa = reinterpret_cast<const void* const*>(&a);
|
||||
const void* const* pb = reinterpret_cast<const void* const*>(&b);
|
||||
SizeT n = 0;
|
||||
for (SizeT i = 0; i < sizeof(T) / sizeof(void*); ++i)
|
||||
if (pa[i] != pb[i]) ++n;
|
||||
return n;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TEST(PipeRouting, TheInstalledClientArmIsWireAndNotMonolithAndEveryRoutedRowMoved) {
|
||||
// WHAT B3 SAYS IS MISSING. PipeCatalogueTest installs and COUNTS the monolith table, so a
|
||||
// client row that was never overwritten stays non-null and still counts - deleting one
|
||||
// `gMGPipe*.X = &Wire_X` assignment left every gate green (the cross-family verifier
|
||||
// reproduced it: catalogue 32/32, all lanes green). The only thing that catches it is
|
||||
// observing that the CLIENT install actually MOVED each routed row off the monolith adapter.
|
||||
//
|
||||
// The monolith adapters are kept beside the installed tables (MGPipeMonolith*()), so the
|
||||
// client install differs from them at exactly the routed rows and nowhere else. This reads
|
||||
// the pointers back rather than constructing them (R-16): a deleted assignment reads equal.
|
||||
InstallClientWireTables();
|
||||
EXPECT_EQ(static_cast<int>(MGPipeInstalledArm()), static_cast<int>(MGPipeRouteArm::kClientWire))
|
||||
<< "InstallClientWireTables did not record the client-wire arm";
|
||||
|
||||
const SizeT movedScreen = CountDifferingCells(gMGPipeScreen, MGPipeMonolithScreen());
|
||||
const SizeT movedContext = CountDifferingCells(gMGPipeContext, MGPipeMonolithContext());
|
||||
EXPECT_EQ(movedScreen + movedContext, 33u)
|
||||
<< "exactly the 33 generated routed rows must differ from the monolith adapters; "
|
||||
<< movedScreen + movedContext
|
||||
<< " did, so a row was left on the monolith adapter (it would run the applier on the GL "
|
||||
"thread under split) or an unrouted row was overwritten";
|
||||
|
||||
const SizeT movedEscapes = CountDifferingCells(gMGPipeRouteEscapes, MGPipeMonolithEscapes());
|
||||
EXPECT_EQ(movedEscapes, 4u)
|
||||
<< "the four escape routes must move off the monolith escapes too";
|
||||
|
||||
// Restore the monolith arm for the sibling cases that assert it (and for a clean binary).
|
||||
MGPipeInstallMonolithTables();
|
||||
EXPECT_EQ(static_cast<int>(MGPipeInstalledArm()), static_cast<int>(MGPipeRouteArm::kMonolith));
|
||||
}
|
||||
|
||||
#if MGTEST_HAVE_FORK
|
||||
TEST(PipeRouting, AClientWireRowWithNoSessionRefusesByNameRatherThanApplying) {
|
||||
// THE RUNTIME HALF of B3, and it distinguishes a Wire_* row from the monolith adapter by
|
||||
// BEHAVIOUR: with the client tables installed and no session, a routed call reaches
|
||||
// RequireSession and aborts Fatal{NoClientSession}. The monolith adapter (the deleted-
|
||||
// assignment state) would instead run MGPipeApply* and NOT abort with that string - so the
|
||||
// control goes red the moment a row falls back to monolith.
|
||||
const ChildResult child = RunInChild([] {
|
||||
InstallClientWireTables();
|
||||
MGPHandleOnly handle{};
|
||||
handle.Handle = MGPipeHandle{1, 0};
|
||||
handle.Kind = static_cast<Uint32>(MGPipeKind::Renderbuffer);
|
||||
gMGPipeScreen.ResourceDestroy(&handle); // Wire_ResourceDestroy, no session
|
||||
});
|
||||
EXPECT_TRUE(DiedOfAbort(child)) << DescribeStatus(child) << "\n" << child.Log;
|
||||
EXPECT_NE(child.Log.find("Fatal{NoClientSession, \"ResourceDestroy\"}"), std::string::npos)
|
||||
<< "the installed row did not refuse by name; it may be the monolith adapter (B3)\n"
|
||||
<< child.Log;
|
||||
}
|
||||
|
||||
TEST(PipeRouting, ARoutedCallDuringTeardownRefusesByNameNotRunsTheApplier) {
|
||||
// codex 4: UninstallClientWireTables marks the tables uninstalled; a routed call in that
|
||||
// window must abort by name rather than run the applier on the caller. Reverting Uninstall
|
||||
// to reinstall the monolith adapters (round 2's behaviour) makes this call run the applier
|
||||
// and NOT abort with this string - the red-once.
|
||||
const ChildResult child = RunInChild([] {
|
||||
InstallClientWireTables();
|
||||
UninstallClientWireTables();
|
||||
MGPHandleOnly handle{};
|
||||
handle.Handle = MGPipeHandle{1, 0};
|
||||
handle.Kind = static_cast<Uint32>(MGPipeKind::Renderbuffer);
|
||||
gMGPipeScreen.ResourceDestroy(&handle);
|
||||
});
|
||||
EXPECT_TRUE(DiedOfAbort(child)) << DescribeStatus(child) << "\n" << child.Log;
|
||||
EXPECT_NE(child.Log.find("Fatal{ClientTablesUninstalled, \"ResourceDestroy\"}"), std::string::npos)
|
||||
<< "a routed call after UninstallClientWireTables ran the applier on the caller instead "
|
||||
"of refusing by name (codex 4)\n"
|
||||
<< child.Log;
|
||||
}
|
||||
#endif // MGTEST_HAVE_FORK
|
||||
|
||||
// =====================================================================================
|
||||
// M2 / codex 11: a short or non-OK reply is refused, never scattered as pixels
|
||||
// =====================================================================================
|
||||
|
||||
TEST(RemoteReadback, AReplyIsScatteredOnlyWhenItIsOkAndExactlyTheReadsExtent) {
|
||||
// EmitReadPixels decides on ReadbackReplyIsComplete before it scatters or returns (the Fatal
|
||||
// wording each mode owns is at the call site). Driving the production predicate directly:
|
||||
// only a full OK reply is complete; a short OK reply, and a DECLINE or ERROR with a zero
|
||||
// payload, are not - and those are the shapes that would otherwise spray stale destination
|
||||
// bytes as pixels. `tight` is the read's own DstSize (CONTRACT-P5 row 23).
|
||||
constexpr Int32 kOk = 0, kDeclined = 1, kError = 2;
|
||||
const Uint64 tight = TightReadbackByteCount(4, 3, 0x1908, 0x1401); // 48
|
||||
EXPECT_TRUE(ReadbackReplyIsComplete(kOk, tight, tight)) << "a full OK reply is the only one scattered";
|
||||
EXPECT_FALSE(ReadbackReplyIsComplete(kOk, tight - 16, tight))
|
||||
<< "a SHORT OK reply (one row missing) must not be scattered - the missing rows would be "
|
||||
"whatever the destination held";
|
||||
EXPECT_FALSE(ReadbackReplyIsComplete(kDeclined, 0, tight))
|
||||
<< "a DECLINED reply carries no pixels";
|
||||
EXPECT_FALSE(ReadbackReplyIsComplete(kError, 0, tight)) << "an ERROR reply carries no pixels";
|
||||
EXPECT_FALSE(ReadbackReplyIsComplete(kOk, 0, tight)) << "an OK reply of zero bytes is not the extent";
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,625 @@
|
||||
// MobileGL - MobileGL/MG_Test/Wire/ServerLoopTest.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
|
||||
|
||||
// Package v1's suite: the apply thread, the blocking control mailbox, the verb stamp, the
|
||||
// bounded shutdown, and R-11's server-owned staging copy.
|
||||
//
|
||||
// WHAT THIS SUITE REFUSES TO DO, and why it is written the way it is (R-16).
|
||||
//
|
||||
// Every case here drives the REAL path: a real ServerSession over four real ShmSegment-backed
|
||||
// segments, a real apply thread parked on a real Doorbell, and records that go in through w1's
|
||||
// encoder and come out through w1's decoder. None of them writes a record field by hand, none
|
||||
// of them calls PipeApplier::ApplyOne directly, and none of them sets a flag the case then
|
||||
// observes. That matters because the three defects the first P5 wave shipped were all of that
|
||||
// shape - a persistent-map case that wrote the record field itself still passed with the
|
||||
// producer deleted, and a lane probe armed against stub SOURCE TEXT went green having run
|
||||
// monolith on 8 of 11 lanes.
|
||||
//
|
||||
// The two things this suite deliberately CANNOT reach are named rather than faked:
|
||||
// * there is no GL context here, so ServerLoop::CreateBackend is not called and the five
|
||||
// class-B verbs DECLINE. That is asserted as a decline, not skipped - a Clear that was
|
||||
// APPLIED with no backend would mean the sink found a table it should not have.
|
||||
// * R-11's end-to-end control is MOBILEGL_IPC_AUDIT=1 over the reduced path, which needs the
|
||||
// client's emit table (package c1). What IS reachable here is the property that control
|
||||
// exists to test: bytes that were copied survive the source being overwritten with 0xDD.
|
||||
|
||||
#include <Config.h>
|
||||
#include <MG_Backend/MGPipe/PipeInputs.h>
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
#include <MG_Remote/CapsCodec.h>
|
||||
#include <MG_Remote/Protocol/generated/protocol_generated.h>
|
||||
#include <MG_Remote/Server/PipeApplier.h>
|
||||
#include <MG_Remote/Server/ServerLoop.h>
|
||||
#include <MG_Remote/Server/ServerSession.h>
|
||||
#include <MG_Remote/Server/StagedShadow.h>
|
||||
#include <MG_Remote/Transport/InProcessTransport.h>
|
||||
#include <MG_Remote/Transport/Ring.h>
|
||||
#include <MG_Remote/Transport/SessionRings.h>
|
||||
#include <MG_Remote/Wire/PipeWireCodec.h>
|
||||
#include <MGGitHash.h>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <cstdio>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#if defined(_WIN32)
|
||||
#include <process.h>
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
using namespace MobileGL;
|
||||
|
||||
// NOT `using namespace MobileGL::MG_Remote`. MobileGL::Wire (the flatbuffers control-plane
|
||||
// schema) and MobileGL::MG_Remote::Wire (the G3 codec) are two different namespaces with the
|
||||
// same last name, and a using-directive over the second makes every mention of `Wire`
|
||||
// ambiguous - including the ones in the generated header.
|
||||
namespace Transport = MobileGL::MG_Remote::Transport;
|
||||
namespace Server = MobileGL::MG_Remote::Server;
|
||||
namespace Codec = MobileGL::MG_Remote::Wire;
|
||||
using MobileGL::MG_Remote::CapsAbiFingerprint;
|
||||
|
||||
namespace {
|
||||
|
||||
std::string g_logPath;
|
||||
|
||||
std::string ReadLog() {
|
||||
std::ifstream in(g_logPath, std::ios::binary);
|
||||
if (!in) return {};
|
||||
return std::string(std::istreambuf_iterator<char>(in), std::istreambuf_iterator<char>());
|
||||
}
|
||||
|
||||
unsigned ProcessId() {
|
||||
#if defined(_WIN32)
|
||||
return static_cast<unsigned>(_getpid());
|
||||
#else
|
||||
return static_cast<unsigned>(::getpid());
|
||||
#endif
|
||||
}
|
||||
|
||||
Transport::SessionSegmentSizes TestSizes() {
|
||||
Transport::SessionSegmentSizes sizes;
|
||||
sizes.CmdRingBytes = 64ull * 1024;
|
||||
sizes.StageBytes = 64ull * 1024;
|
||||
sizes.ReplyBytes = 64ull * 1024;
|
||||
sizes.EventRingBytes = 16ull * 1024;
|
||||
sizes.ReplySlotCount = 8;
|
||||
return sizes;
|
||||
}
|
||||
|
||||
// The client half of a session, wired exactly the way ClientSession::Start wires it: the
|
||||
// transport pair, a real Hello, the server's Accept, the client's attach to the SAME
|
||||
// mapping, and w1's encoder over the shared SEG_CMD ring. It is NOT ClientSession itself,
|
||||
// because ClientSession::Start finishes by installing package c1's BackendObject_Remote,
|
||||
// which does not exist yet - and a fixture that skipped the handshake to get around that
|
||||
// would be testing a session this tree does not build.
|
||||
struct ServerFixture {
|
||||
std::unique_ptr<Transport::InProcessTransport> clientTransport;
|
||||
std::unique_ptr<Transport::InProcessTransport> serverTransport;
|
||||
Transport::SessionSegments clientSegments;
|
||||
Transport::RingProducer cmd;
|
||||
Transport::SessionProducer producer;
|
||||
Codec::SegmentTable clientTable;
|
||||
Codec::PipeWireEncoder encoder;
|
||||
Server::ServerSession* session = nullptr;
|
||||
|
||||
bool Handshake() {
|
||||
Transport::InProcessTransport::CreatePair(clientTransport, serverTransport);
|
||||
{
|
||||
::flatbuffers::FlatBufferBuilder builder(512);
|
||||
auto stamp = builder.CreateString(GIT_COMMIT_HASH_SHORT);
|
||||
auto hello = ::MobileGL::Wire::CreateHello(
|
||||
builder, MOBILEGL_PROTOCOL_ABI_MAJOR, MOBILEGL_PROTOCOL_ABI_MINOR, stamp,
|
||||
/*backendType=*/0u, /*pid=*/0u, /*configBlob=*/0, CapsAbiFingerprint());
|
||||
auto root = ::MobileGL::Wire::CreateCtrlEnvelope(
|
||||
builder, ::MobileGL::Wire::CtrlMsg::Hello, hello.Union());
|
||||
::MobileGL::Wire::FinishCtrlEnvelopeBuffer(builder, root);
|
||||
if (clientTransport->SendFrame(MobileGLByteSpan{builder.GetBufferPointer(),
|
||||
builder.GetSize()}) != MOBILEGL_OK) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
session = &Server::ServerSessionInstance();
|
||||
session->SetSegmentSizes(TestSizes());
|
||||
// The two halves v1 owns. Neither has a default and CallMask() Fatals on an unset
|
||||
// one, which is s1's BLOCKER fix and the reason this is stated rather than derived.
|
||||
session->SetCapabilityBits(0);
|
||||
session->SetConsumedSubsystems(MG_Pipe::kMGPipeSubsystemsMigratedAtP4a);
|
||||
if (session->Accept(*serverTransport) != MOBILEGL_OK) return false;
|
||||
if (clientSegments.AttachInProcess(session->Shm(), Transport::MemoryRole::Client) !=
|
||||
MOBILEGL_OK) {
|
||||
return false;
|
||||
}
|
||||
Transport::RingControl* control = clientSegments.CmdControl();
|
||||
cmd = Transport::RingProducer(control, clientSegments.CmdRingBase(),
|
||||
clientSegments.CmdRingCapacity(), Transport::RingCursorSet::Cmd);
|
||||
if (!cmd.Valid()) return false;
|
||||
producer.Attach(control, &cmd, &clientTransport->PeerDoorbell(),
|
||||
&clientTransport->SelfDoorbell(), MG_Config::Ipc.SpinUs);
|
||||
clientTable.Install(Codec::kSegCmd, Codec::SegmentView{clientSegments.CmdRingBase(),
|
||||
clientSegments.CmdRingCapacity()});
|
||||
clientTable.Install(Codec::kSegStage,
|
||||
Codec::SegmentView{clientSegments.StageBase(), clientSegments.StageBytes()});
|
||||
clientTable.Install(Codec::kSegReply,
|
||||
Codec::SegmentView{clientSegments.ReplyBase(), clientSegments.ReplyBytes()});
|
||||
encoder = Codec::PipeWireEncoder(control, &cmd, nullptr, &clientTable);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool StartLoop() {
|
||||
return Server::ServerLoopInstance().Start(*session) == MOBILEGL_OK;
|
||||
}
|
||||
|
||||
// Emit one record and wait for the server to apply it. This is the verb barrier's own
|
||||
// wait (R-3/R-5) - appliedSeq, not a sleep - so a case that goes green here has really
|
||||
// seen the apply thread move the watermark.
|
||||
bool EmitAndWait(MG_Pipe::MGPWireOp op, const void* payload, Uint64 payloadBytes,
|
||||
Uint32 timeoutMs = 5000) {
|
||||
const Uint64 seq = encoder.EncodeRecord(op, payload, payloadBytes);
|
||||
if (seq == Codec::kInvalidSeq) return false;
|
||||
encoder.Publish();
|
||||
producer.PublishAndNotify(seq);
|
||||
return producer.WaitForApplied(seq, timeoutMs) == Transport::SessionWait::Reached;
|
||||
}
|
||||
|
||||
bool EmitAndWaitWithTail(MG_Pipe::MGPWireOp op, const void* payload, Uint64 payloadBytes,
|
||||
const void* tail, Uint64 tailBytes, Uint32 timeoutMs = 5000) {
|
||||
const Uint64 seq = encoder.EncodeRecord(op, payload, payloadBytes, tail, tailBytes);
|
||||
if (seq == Codec::kInvalidSeq) return false;
|
||||
encoder.Publish();
|
||||
producer.PublishAndNotify(seq);
|
||||
return producer.WaitForApplied(seq, timeoutMs) == Transport::SessionWait::Reached;
|
||||
}
|
||||
|
||||
void Stop() {
|
||||
// Table 3's order: the client publishes and lets the server drain (EmitAndWait
|
||||
// already did), then Doorbell::Kill through the transport's Shutdown, THEN the
|
||||
// bounded join, and only then is anything an emitter owns released.
|
||||
if (clientTransport) clientTransport->Shutdown();
|
||||
Server::ServerLoopInstance().Stop();
|
||||
producer.Detach();
|
||||
encoder = Codec::PipeWireEncoder();
|
||||
cmd = Transport::RingProducer();
|
||||
clientSegments.Close();
|
||||
if (session != nullptr) session->Close();
|
||||
clientTransport.reset();
|
||||
serverTransport.reset();
|
||||
}
|
||||
};
|
||||
|
||||
MG_Pipe::MGPClear WholeFramebufferClear() {
|
||||
MG_Pipe::MGPClear clear{};
|
||||
clear.Kind = Server::kMGPClearKindWhole;
|
||||
clear.BufferMask = 0x00004000; // GL_COLOR_BUFFER_BIT
|
||||
clear.DrawBufferIndex = -1;
|
||||
clear.ValueClass = Server::kMGPClearValueClassFloat;
|
||||
return clear;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// =====================================================================================
|
||||
// The thread
|
||||
// =====================================================================================
|
||||
|
||||
// A control request must be able to un-park a thread waiting on kWaitForever. A Notify alone
|
||||
// cannot do that - Doorbell::Wait consumes it with one Park, re-tests a condition nothing
|
||||
// published, finds the bell alive and parks again - so the park predicate has to carry the
|
||||
// control flag too. The case asserts the thread REALLY PARKED first, because a loop that
|
||||
// spun instead would pass this without the predicate ever mattering.
|
||||
TEST(ServerLoopTest, AControlRequestRunsOnTheApplyThreadAndUnparksIt) {
|
||||
ServerFixture fixture;
|
||||
ASSERT_TRUE(fixture.Handshake());
|
||||
ASSERT_TRUE(fixture.StartLoop());
|
||||
|
||||
Server::ServerLoop& loop = Server::ServerLoopInstance();
|
||||
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5);
|
||||
while (loop.ParkCount() == 0 && std::chrono::steady_clock::now() < deadline) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
}
|
||||
ASSERT_GT(loop.ParkCount(), 0u)
|
||||
<< "the apply thread never parked, so this case would prove nothing about waking it";
|
||||
|
||||
struct Probe {
|
||||
std::thread::id ranOn{};
|
||||
Bool onApplyThread = false;
|
||||
} probe;
|
||||
const MobileGLResult rc = loop.RunOnApplyThread(
|
||||
+[](void* user) -> MobileGLResult {
|
||||
auto* p = static_cast<Probe*>(user);
|
||||
p->ranOn = std::this_thread::get_id();
|
||||
p->onApplyThread = Server::ServerLoop::OnApplyThread();
|
||||
return MOBILEGL_OK;
|
||||
},
|
||||
&probe);
|
||||
|
||||
EXPECT_EQ(rc, MOBILEGL_OK);
|
||||
EXPECT_NE(probe.ranOn, std::this_thread::get_id())
|
||||
<< "the control request ran on the CALLER, which means the EGL lifecycle calls would "
|
||||
"reach the driver from the app thread and the context would never migrate";
|
||||
EXPECT_TRUE(probe.onApplyThread);
|
||||
EXPECT_FALSE(Server::ServerLoop::OnApplyThread());
|
||||
|
||||
fixture.Stop();
|
||||
}
|
||||
|
||||
// Re-entrancy is not a deadlock: ~BackendObject_DirectGLES reaches ReleaseEGLResources FROM the
|
||||
// apply thread, so a post from there must run inline.
|
||||
TEST(ServerLoopTest, APostFromTheApplyThreadItselfRunsInlineRatherThanDeadlocking) {
|
||||
ServerFixture fixture;
|
||||
ASSERT_TRUE(fixture.Handshake());
|
||||
ASSERT_TRUE(fixture.StartLoop());
|
||||
|
||||
struct Outer {
|
||||
Bool innerRan = false;
|
||||
MobileGLResult innerRc = MOBILEGL_ERR_INVALID_ARGUMENT;
|
||||
} outer;
|
||||
const MobileGLResult rc = Server::ServerLoopInstance().RunOnApplyThread(
|
||||
+[](void* user) -> MobileGLResult {
|
||||
auto* o = static_cast<Outer*>(user);
|
||||
o->innerRc = Server::ServerLoopInstance().RunOnApplyThread(
|
||||
+[](void* inner) -> MobileGLResult {
|
||||
*static_cast<Bool*>(inner) = true;
|
||||
return MOBILEGL_OK;
|
||||
},
|
||||
&o->innerRan);
|
||||
return MOBILEGL_OK;
|
||||
},
|
||||
&outer);
|
||||
|
||||
EXPECT_EQ(rc, MOBILEGL_OK);
|
||||
EXPECT_EQ(outer.innerRc, MOBILEGL_OK);
|
||||
EXPECT_TRUE(outer.innerRan);
|
||||
|
||||
fixture.Stop();
|
||||
}
|
||||
|
||||
// The bounded join. The thread is parked on kWaitForever when Stop() is called, so this is the
|
||||
// exact lost-wakeup shape table 3 step 2 is about - and the bound is what turns a regression
|
||||
// into a red test rather than a wedged CI job.
|
||||
TEST(ServerLoopTest, StopKillsTheDoorbellJoinsAndTheThreadReallyExits) {
|
||||
ServerFixture fixture;
|
||||
ASSERT_TRUE(fixture.Handshake());
|
||||
ASSERT_TRUE(fixture.StartLoop());
|
||||
Server::ServerLoop& loop = Server::ServerLoopInstance();
|
||||
|
||||
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5);
|
||||
while (loop.ParkCount() == 0 && std::chrono::steady_clock::now() < deadline) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
}
|
||||
ASSERT_GT(loop.ParkCount(), 0u) << "the thread must be parked for this to be a shutdown test";
|
||||
ASSERT_TRUE(loop.Running());
|
||||
|
||||
const auto began = std::chrono::steady_clock::now();
|
||||
fixture.Stop();
|
||||
const auto took = std::chrono::steady_clock::now() - began;
|
||||
|
||||
EXPECT_FALSE(loop.Running());
|
||||
EXPECT_LT(std::chrono::duration_cast<std::chrono::milliseconds>(took).count(), 5000)
|
||||
<< "Stop() took the whole bound, which means it hit the join timeout rather than a "
|
||||
"wakeup";
|
||||
}
|
||||
|
||||
// MOBILEGL_IPC_SERVER_AFFINITY is kept as the raw string BECAUSE the resolved mask is what gets
|
||||
// logged: an affinity that silently did nothing looks exactly like one that worked. So the
|
||||
// resolved mask has to be readable, and `off` has to resolve to zero rather than to "auto".
|
||||
TEST(ServerLoopTest, TheAffinityStringResolvesToAMaskAndTheMaskIsLogged) {
|
||||
const String saved = MG_Config::Ipc.ServerAffinity;
|
||||
MG_Config::Ipc.ServerAffinity = "off";
|
||||
|
||||
ServerFixture fixture;
|
||||
ASSERT_TRUE(fixture.Handshake());
|
||||
ASSERT_TRUE(fixture.StartLoop());
|
||||
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5);
|
||||
while (Server::ServerLoopInstance().ParkCount() == 0 &&
|
||||
std::chrono::steady_clock::now() < deadline) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
}
|
||||
EXPECT_EQ(Server::ServerLoopInstance().ResolvedAffinityMask(), 0u)
|
||||
<< "`off` did not resolve to no-affinity";
|
||||
|
||||
const std::string log = ReadLog();
|
||||
EXPECT_NE(log.find("RESOLVED mask"), std::string::npos)
|
||||
<< "the apply thread did not log its resolved affinity mask; an affinity that silently "
|
||||
"did nothing is indistinguishable from one that worked";
|
||||
EXPECT_NE(log.find("mgl-srv-apply started"), std::string::npos);
|
||||
|
||||
fixture.Stop();
|
||||
MG_Config::Ipc.ServerAffinity = saved;
|
||||
}
|
||||
|
||||
// =====================================================================================
|
||||
// The record path: stamp, apply, watermark
|
||||
// =====================================================================================
|
||||
|
||||
// The whole phase's prerequisite. MGPipeApplyAccess deliberately does not stamp the poison
|
||||
// generations, so under split NOTHING stamps unless the applier does - every FilledGen[] stays
|
||||
// 0, MGPipeInputFieldIsFresh answers false for everything, and a server-side read aborts on the
|
||||
// FIRST field inside SyncRenderState. This case asserts the stamp happened by reading the verb
|
||||
// the stamp SET, which the case itself never writes.
|
||||
TEST(ServerLoopTest, AClearRecordCrossesAndIsStampedAsAVerbBoundary) {
|
||||
ServerFixture fixture;
|
||||
ASSERT_TRUE(fixture.Handshake());
|
||||
ASSERT_TRUE(fixture.StartLoop());
|
||||
|
||||
// The pre-state is the honest one: nothing has stamped yet in this process.
|
||||
ASSERT_NE(MG_Pipe::gPipeInputs.CurrentVerb(), MG_Pipe::MGPipeVerb::Clear);
|
||||
|
||||
const MG_Pipe::MGPClear clear = WholeFramebufferClear();
|
||||
ASSERT_TRUE(fixture.EmitAndWait(MG_Pipe::MGPWireOp::Clear, &clear, sizeof(clear)));
|
||||
|
||||
Server::ServerLoop& loop = Server::ServerLoopInstance();
|
||||
EXPECT_EQ(loop.DrainedRecords(), 1u);
|
||||
EXPECT_EQ(MG_Pipe::gPipeInputs.CurrentVerb(), MG_Pipe::MGPipeVerb::Clear)
|
||||
<< "PipeApplier::StampVerbBoundary did not run, so every server-side PipeInputs read "
|
||||
"would abort on the first field inside SyncRenderState";
|
||||
|
||||
// MANDATORY on leaving the applier (p1's M-5): without it a SPAWNED server latches the flag
|
||||
// for its whole life, every later read is judged against the last verb's mask, and the
|
||||
// sticky forwards start aborting under strict on the very case their exemption exists for.
|
||||
EXPECT_FALSE(MG_Pipe::gPipeInputs.ServerStampedVerb())
|
||||
<< "MGPipeServerClearVerbBoundary was not called when the apply thread left the applier";
|
||||
|
||||
// No backend object in this process, so the five class-B verbs DECLINE. Asserted rather
|
||||
// than skipped: a Clear that was APPLIED here would mean the sink found a function table
|
||||
// it had no business finding.
|
||||
EXPECT_EQ(fixture.session->Applier().Verbs().Clears(), 0u);
|
||||
|
||||
fixture.Stop();
|
||||
}
|
||||
|
||||
// R-9's batching ban, made checkable rather than merely stated. RingControl::appliedSeq has ONE
|
||||
// writer (SessionConsumer::ApplyOne, +1 per record) and PipeWireDecoder keeps its own tally; a
|
||||
// batched publish moves one and not the other, which a single counter could never tell apart.
|
||||
TEST(ServerLoopTest, TheSessionWatermarkAndTheDecoderTallyAgreeAfterEveryRecord) {
|
||||
ServerFixture fixture;
|
||||
ASSERT_TRUE(fixture.Handshake());
|
||||
ASSERT_TRUE(fixture.StartLoop());
|
||||
|
||||
const MG_Pipe::MGPClear clear = WholeFramebufferClear();
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
ASSERT_TRUE(fixture.EmitAndWait(MG_Pipe::MGPWireOp::Clear, &clear, sizeof(clear))) << i;
|
||||
EXPECT_EQ(fixture.session->Consumer().AppliedSeq(), static_cast<Uint64>(i + 1));
|
||||
EXPECT_EQ(fixture.session->Applier().DecoderAppliedSeq(), static_cast<Uint64>(i + 1));
|
||||
}
|
||||
EXPECT_EQ(Server::ServerLoopInstance().DrainedRecords(), 8u);
|
||||
|
||||
// retiredSeq is the watermark w1's SEG_STAGE allocator reclaims against; a loop that
|
||||
// applies and never retires ends the first MOBILEGL_IPC_STAGE_MB in Fatal{RingOverrun}.
|
||||
//
|
||||
// IT IS POLLED, NOT READ ONCE, and that is R-9's own rule rather than a papered-over race:
|
||||
// appliedSeq is the ONLY watermark P5 forbids batching, and every other one "may be
|
||||
// published LATE but never EARLY". The apply thread retires after the drain batch and
|
||||
// before it parks, so the barrier can release the client between the two - reading it
|
||||
// instantly would be asserting a promise R-9 deliberately does not make. The BOUND is what
|
||||
// keeps this a check: a loop that never retires times out here instead of passing.
|
||||
const auto retireDeadline = std::chrono::steady_clock::now() + std::chrono::seconds(2);
|
||||
while (fixture.clientSegments.CmdControl()->retiredSeq.load() < 8u &&
|
||||
std::chrono::steady_clock::now() < retireDeadline) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
}
|
||||
EXPECT_GE(fixture.clientSegments.CmdControl()->retiredSeq.load(), 8u)
|
||||
<< "the apply loop advanced appliedSeq but never retired within 2 s, so SEG_STAGE would "
|
||||
"never be reclaimed and the first MOBILEGL_IPC_STAGE_MB would end in "
|
||||
"Fatal{RingOverrun}";
|
||||
|
||||
fixture.Stop();
|
||||
}
|
||||
|
||||
// =====================================================================================
|
||||
// R-2.5 / rule C's mechanical control, running for real
|
||||
// =====================================================================================
|
||||
|
||||
// MOBILEGL_IPC_AUDIT=1 fills a retired SEG_STAGE run with 0xDD once the applier has RETURNED,
|
||||
// so an applier that kept the pointer reads the poison on the next frame instead of bytes that
|
||||
// happen to still be there. Without this, an inproc implementation that kept a pointer is
|
||||
// INDISTINGUISHABLE from one that copied - which is R-2's entire argument.
|
||||
//
|
||||
// The case asserts three separate things, because each one alone can be true for the wrong
|
||||
// reason: that the record's bytes really crossed (memcmp before the poison), that the poison
|
||||
// really ran (PoisonedStageBytes moved), and that it landed on the run the record named (the
|
||||
// bytes read 0xDD afterwards, through the CLIENT's mapping of the same segment).
|
||||
TEST(ServerLoopTest, TheAuditPoisonFillsExactlyTheStagedRunAfterTheApplierReturns) {
|
||||
const Bool savedAudit = MG_Config::Ipc.Audit;
|
||||
// Set BEFORE the loop starts: PipeWireDecoder reads it in its constructor, and its
|
||||
// constructor runs on the apply thread inside ServerLoop::Start.
|
||||
MG_Config::Ipc.Audit = true;
|
||||
|
||||
ServerFixture fixture;
|
||||
ASSERT_TRUE(fixture.Handshake());
|
||||
ASSERT_TRUE(fixture.StartLoop());
|
||||
|
||||
MG_Pipe::MGPResourceDesc create{};
|
||||
create.Resource.Slot = 61;
|
||||
create.Resource.Gen = 1;
|
||||
create.Target = static_cast<Uint8>(MG_Pipe::MGPipeResourceTarget::Tex2D);
|
||||
create.InternalFormat = 1;
|
||||
create.Width = 4;
|
||||
create.Height = 4;
|
||||
create.Depth = 1;
|
||||
create.ArrayLayers = 1;
|
||||
create.Levels = 1;
|
||||
create.Samples = 1;
|
||||
ASSERT_TRUE(fixture.EmitAndWait(MG_Pipe::MGPWireOp::ResourceCreate, &create, sizeof(create)));
|
||||
|
||||
Vector<Uint8> texels(4 * 4 * 4, 0x5A);
|
||||
MG_Pipe::MGPSubData upload{};
|
||||
upload.Res = create.Resource;
|
||||
upload.Target = MG_Pipe::MGPipePackSubDataTarget(
|
||||
static_cast<Uint32>(MG_Pipe::MGPipeResourceTarget::Tex2D), 0u);
|
||||
upload.Level = 0;
|
||||
upload.UnionBox = MG_Pipe::MGPBox{0, 0, 0, 4, 4, 1};
|
||||
upload.RegionCount = 1;
|
||||
upload.Blob = fixture.encoder.StageBytes(texels.data(), texels.size());
|
||||
ASSERT_NE(upload.Blob.Size, 0u) << "rule A: a content record must declare non-zero bytes";
|
||||
|
||||
// The bytes are in SEG_STAGE and readable through the CLIENT's own mapping right now - the
|
||||
// record has not been published yet, so nothing has retired it.
|
||||
const void* stagedBefore =
|
||||
fixture.clientTable.Resolve(upload.Blob.Seg, upload.Blob.Offset, upload.Blob.Size);
|
||||
ASSERT_NE(stagedBefore, nullptr);
|
||||
ASSERT_EQ(std::memcmp(stagedBefore, texels.data(), texels.size()), 0);
|
||||
|
||||
MG_Pipe::MGPSubRegion region{};
|
||||
region.W = 4;
|
||||
region.H = 4;
|
||||
region.D = 1;
|
||||
ASSERT_TRUE(fixture.EmitAndWaitWithTail(MG_Pipe::MGPWireOp::ResourceSubData, &upload,
|
||||
sizeof(upload), ®ion, sizeof(region)));
|
||||
|
||||
EXPECT_GT(fixture.session->Applier().PoisonedStageBytes(), 0u)
|
||||
<< "MOBILEGL_IPC_AUDIT=1 poisoned nothing, so R-2.5's only mechanical control against a "
|
||||
"retained SEG_STAGE pointer never ran";
|
||||
|
||||
const auto* poisoned = static_cast<const Uint8*>(
|
||||
fixture.clientTable.Resolve(upload.Blob.Seg, upload.Blob.Offset, upload.Blob.Size));
|
||||
ASSERT_NE(poisoned, nullptr);
|
||||
for (Uint64 i = 0; i < upload.Blob.Size; ++i) {
|
||||
ASSERT_EQ(poisoned[i], 0xDD) << "staged byte " << i << " was not poisoned; an applier "
|
||||
"that kept this pointer would still read real bytes and "
|
||||
"the control would prove nothing";
|
||||
}
|
||||
|
||||
fixture.Stop();
|
||||
MG_Config::Ipc.Audit = savedAudit;
|
||||
}
|
||||
|
||||
// =====================================================================================
|
||||
// R-11 - the server's own copy of the staged bytes
|
||||
// =====================================================================================
|
||||
|
||||
// THIS IS THE PROPERTY MOBILEGL_IPC_AUDIT=1's 0xDD FILL EXISTS TO TEST, at unit scope: after
|
||||
// the source bytes are overwritten - which is exactly what the decoder does to a retired
|
||||
// SEG_STAGE run - the server's copy still reads the original. An implementation that returned
|
||||
// `raw - offset` cannot pass this, and the monolith arm below is the control that says so: it
|
||||
// is the SAME call with the same inputs, and it must see the 0xDD.
|
||||
TEST(StagedShadowTest, TheSplitArmCopiesAndSurvivesTheSourceBeingPoisoned) {
|
||||
Server::StagedShadowStore splitStore(/*copies=*/true);
|
||||
Server::StagedShadowStore monolithStore(/*copies=*/false);
|
||||
const int key = 0;
|
||||
|
||||
Vector<Uint8> staged(32, 0xAB);
|
||||
const Uint8* splitBase = splitStore.Adopt(&key, 64, staged.data(), 16, staged.size());
|
||||
const Uint8* monolithBase = monolithStore.Adopt(&key, 64, staged.data(), 16, staged.size());
|
||||
|
||||
ASSERT_NE(splitBase, nullptr);
|
||||
EXPECT_EQ(monolithBase, staged.data() - 16)
|
||||
<< "the monolith arm must be the ORIGINAL expression, character for character, or every "
|
||||
"push and verify lane is running new code it was never measured against";
|
||||
EXPECT_NE(splitBase, monolithBase);
|
||||
|
||||
// w1's retired-stage poison, by hand and at the right moment: the record has retired, so
|
||||
// the staging run is dead.
|
||||
std::fill(staged.begin(), staged.end(), Uint8{0xDD});
|
||||
|
||||
for (SizeT i = 0; i < 32; ++i) {
|
||||
EXPECT_EQ(splitBase[16 + i], 0xAB) << "byte " << i << " of the server's copy is the "
|
||||
"poison, so the copy never happened";
|
||||
}
|
||||
// And the control: the monolith arm reads the poison, which is what makes the assertion
|
||||
// above a statement about copying rather than about the test's own buffer.
|
||||
EXPECT_EQ(monolithBase[16], 0xDD);
|
||||
}
|
||||
|
||||
// The extent is EXACTLY what the record declared. Adjacent runs merge because there is no gap
|
||||
// between them; runs with a gap do not, and that is the whole mechanism - it is what makes a
|
||||
// missing record detectable instead of papered over by a widened INVALIDATE_RANGE.
|
||||
TEST(StagedShadowTest, CoverageIsExactAndAGapIsNotCovered) {
|
||||
Server::StagedShadowStore store(/*copies=*/true);
|
||||
const int key = 0;
|
||||
Vector<Uint8> bytes(16, 0x11);
|
||||
|
||||
store.Adopt(&key, 64, bytes.data(), 0, 16);
|
||||
store.Adopt(&key, 64, bytes.data(), 32, 16);
|
||||
EXPECT_EQ(store.CoveredRunCount(&key), 2u) << "two runs with a 16-byte gap merged into one";
|
||||
EXPECT_TRUE(store.IsCovered(&key, 0, 16));
|
||||
EXPECT_TRUE(store.IsCovered(&key, 32, 48));
|
||||
EXPECT_FALSE(store.IsCovered(&key, 16, 32)) << "the gap reads as covered";
|
||||
EXPECT_FALSE(store.IsCovered(&key, 0, 48)) << "a span across the gap reads as covered";
|
||||
EXPECT_FALSE(store.IsCovered(&key, 8, 40));
|
||||
|
||||
// Filling the gap merges all three into one run, which is the proof that adjacency (not
|
||||
// proximity) is the merge rule.
|
||||
store.Adopt(&key, 64, bytes.data(), 16, 16);
|
||||
EXPECT_EQ(store.CoveredRunCount(&key), 1u);
|
||||
EXPECT_TRUE(store.IsCovered(&key, 0, 48));
|
||||
}
|
||||
|
||||
TEST(StagedShadowTest, DropForgetsOneResourceAndDropAllForgetsEveryOne) {
|
||||
Server::StagedShadowStore store(/*copies=*/true);
|
||||
const int a = 0;
|
||||
const int b = 0;
|
||||
Vector<Uint8> bytes(8, 0x22);
|
||||
store.Adopt(&a, 8, bytes.data(), 0, 8);
|
||||
store.Adopt(&b, 8, bytes.data(), 0, 8);
|
||||
ASSERT_EQ(store.TrackedResources(), 2u);
|
||||
store.Drop(&a);
|
||||
EXPECT_EQ(store.TrackedResources(), 1u);
|
||||
EXPECT_FALSE(store.IsCovered(&a, 0, 8));
|
||||
EXPECT_TRUE(store.IsCovered(&b, 0, 8));
|
||||
store.DropAll();
|
||||
EXPECT_EQ(store.TrackedResources(), 0u);
|
||||
}
|
||||
|
||||
// The Fatal, and it asserts ITS OWN failure string rather than "the process died". A death
|
||||
// test that only checks for a crash goes green on any other abort in the same body, which is
|
||||
// exactly the shape this wave shipped three of.
|
||||
#if !defined(_WIN32)
|
||||
TEST(StagedShadowTest, ADrainOutsideTheStagedCoverageIsFatalByName) {
|
||||
Server::StagedShadowStore store(/*copies=*/true);
|
||||
const int key = 0;
|
||||
Vector<Uint8> bytes(16, 0x33);
|
||||
const Uint8* base = store.Adopt(&key, 64, bytes.data(), 0, 16);
|
||||
|
||||
// In coverage: no Fatal, and this is asserted first so that the death below cannot be a
|
||||
// function that aborts on everything.
|
||||
store.RequireCoverage(&key, base, 0, 16, "unit");
|
||||
// A base that is not this resource's shadow is not this rule's subject - that is the
|
||||
// legacy arm, whose MappedData() is valid for the whole store.
|
||||
store.RequireCoverage(&key, bytes.data(), 0, 64, "unit");
|
||||
|
||||
EXPECT_DEATH(store.RequireCoverage(&key, base, 0, 64, "unit_out_of_range"), "");
|
||||
const std::string log = ReadLog();
|
||||
EXPECT_NE(log.find("Fatal{StageSnapshotTooNarrow, \"unit_out_of_range\"}"), std::string::npos)
|
||||
<< "the abort happened but not for this rule's reason; the log says: " << log;
|
||||
}
|
||||
#endif
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
// Before anything logs: MG_Util::Debug::InitFile() reads the variable once, on the first
|
||||
// write, and caches the FILE*. The name carries this process's pid, because
|
||||
// gtest_discover_tests runs every case as its own process, in parallel under ctest -j.
|
||||
namespace fs = std::filesystem;
|
||||
const fs::path path =
|
||||
fs::temp_directory_path() / ("mobilegl-serverloop-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
|
||||
// THIS PROCESS IS A SPLIT ONE. Everything under test reads MG_Config::Transport - the
|
||||
// staged-shadow arm, the applier's stamp path, ConfigLoader's own knobs - and a suite that
|
||||
// left it at Monolith would be a server test running the monolith answers, which is the
|
||||
// failure this phase is built to make impossible.
|
||||
MG_Config::Transport = MG_Config::TransportMode::InProcess;
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
const int rc = RUN_ALL_TESTS();
|
||||
fs::remove(path, ec);
|
||||
return rc;
|
||||
}
|
||||
@@ -152,6 +152,19 @@ class Call(object):
|
||||
"""(parameter declaration list, argument list) for this call."""
|
||||
params = ["const %s* payload" % self.Payload]
|
||||
args = ["payload"]
|
||||
# P5 R-17. EXACTLY the shape kVarTail already has, one flag over: a kHasBlob row's
|
||||
# payload owns an MGPBlobRef, and a blobref names bytes that live somewhere the
|
||||
# payload cannot reach on its own. In monolith "somewhere" is the caller's own
|
||||
# memory and the companion is the raw pointer every MG_Impl call site passes today
|
||||
# (CONTRACT-P5 table 1's "companion pointer today" column, nine rows); under split
|
||||
# it is a SEG_STAGE run the client staged before it published. A table row that
|
||||
# omitted the pair could reproduce NEITHER, which is why nine of the thirty-seven
|
||||
# entry points had no routing that could be written at all.
|
||||
if "kHasBlob" in self.Flags:
|
||||
params.append("const void* blobBytes")
|
||||
params.append("Uint64 blobByteCount")
|
||||
args.append("blobBytes")
|
||||
args.append("blobByteCount")
|
||||
if "kVarTail" in self.Flags:
|
||||
params.append("const void* varTail")
|
||||
params.append("Uint32 varTailCount")
|
||||
|
||||
Reference in New Issue
Block a user