Files
MobileGL/MobileGL/MG_Test/Wire/RemoteClientControls.inc
T
swung0x48 d5ed139cc5 [MG_Remote, MG_Test] (Disaggregated): P5f fc - the EGL control plane crosses as value frames, not function pointers
The twelve Server* forwarders' one-slot mailbox carried a raw function
pointer plus a void* to a stack-local Args struct (P5c audit row G4);
neither means anything across a process boundary. The slot now carries
one SurfaceControlFrame by value: kind is the op, scalars are the
arguments, and ok/eglMajor/eglMinor come back in the same value. The
blocking handshake, the shadow, the doorbell order and the C2 exit
block are unchanged.

- protocol.fbs: append-only - SurfaceOpKind gains SetSwapInterval=8,
  ReleaseResources=9, SetWindowHandle=10; WindowKind gains MetalLayer=6;
  SurfaceOp gains readSurface and context (the MakeCurrent four-tuple).
  protocol_generated.h regenerated with the pinned flatc.
- Server/SurfaceControlFrame.{h,cpp}: the frame (trivially copyable by
  static_assert) and the op enum - ten wire kinds plus four inproc-only
  kinds (InitCapabilities is answered by CapsSnapshot on the wire,
  SwapBuffers/InitWindowSurface have no production caller - f0-egl F8,
  kept for the inproc test lane).
- Protocol/SurfaceOpCodec.{h,cpp}: the frame <-> SurfaceOp/SurfaceReply
  codec, the explicit WindowBackend <-> WindowKind table, and the wire
  entry ServerApplyWireSurfaceOp whose refusals are named:
  Fatal{UnmigratedSurface, "AndroidNativeWindow@P12"} and
  Fatal{ProtocolCorruption, "SurfaceOp"}.
- ServerLoop: RunOnApplyThread(fn, void*) -> RunSurfaceControlFrame(frame)
  plus the dispatch ApplySurfaceControlFrame; the old per-forwarder Args
  bodies moved into the dispatch unchanged (C7/ID-54, N-3, R-12 included).
  RunProbeOnApplyThreadForTesting is the test seam through the same
  channel (the hook lives in a member, never in the slot).
- Tests: RemoteClientTest/ServerLoopTest migrated to the probe seam; new
  SurfaceControlFrameTest suite (mapping tables, wire round-trips, the
  two named refusals) plus ServerLoopTest's AVoidForwarderCrossesAsOne-
  DispatchedFrame (the frame channel's red-once handle) and
  ServerLoopEglTest's TheInitializeDisplayReplyArrivesThroughTheFrame.
2026-09-19 20:30:21 -04:00

720 lines
33 KiB
C++

// c1f: exercise installed client producers over a live inproc session.
// Peer substitutions supply adversarial INPUTS, never client state being asserted.
#if MGTEST_HAVE_FORK
namespace {
namespace Srv = MobileGL::MG_Remote::Server;
namespace Codec = MobileGL::MG_Remote::Wire;
// Test-only member access for replacing the peer at its existing interfaces. Explicit
// instantiations may name private members; no production test knob or ABI change is needed.
template<class Tag, typename Tag::Type member> struct PeerAccess {
friend typename Tag::Type PeerMember(Tag) { return member; }
};
struct DecoderTag {
using Type = Codec::PipeWireDecoder Srv::PipeApplier::*;
friend Type PeerMember(DecoderTag);
};
template struct PeerAccess<DecoderTag, &Srv::PipeApplier::m_decoder>;
struct RepliesTag {
using Type = Codec::ReplySink* Codec::PipeWireDecoder::*;
friend Type PeerMember(RepliesTag);
};
template struct PeerAccess<RepliesTag, &Codec::PipeWireDecoder::m_replies>;
struct ProducerBellTag {
using Type = Transport::Doorbell* Transport::SessionProducer::*;
friend Type PeerMember(ProducerBellTag);
};
template struct PeerAccess<ProducerBellTag, &Transport::SessionProducer::m_selfBell>;
// Observe the actual transport park selected by EmitAndWait. The peer waits until
// the client parks, so no GPU or wall-clock 60-second sleep is required.
struct BudgetBell : Transport::Doorbell {
Transport::Doorbell* original;
std::atomic<Uint32> firstMs{0};
std::atomic<Bool> entered{false};
Bool shutDown = false;
explicit BudgetBell(Transport::Doorbell* bell) : original(bell) {}
void Notify() override { original->Notify(); }
void Reset() override { original->Reset(); }
bool Park(std::uint32_t timeoutMs) override {
if (!entered.load()) {
firstMs.store(timeoutMs);
entered.store(true);
}
return shutDown ? false : original->Park(timeoutMs);
}
bool Dead() const override { return (shutDown && entered.load()) || original->Dead(); }
};
struct FenceBudgetPeer : Codec::WireVerbSink {
BudgetBell& bell;
std::atomic<Bool> release{false};
explicit FenceBudgetPeer(BudgetBell& value) : bell(value) {}
void AwaitPark() {
while (!bell.entered.load() || (bell.shutDown && !release.load())) std::this_thread::yield();
}
Bool OnFenceWait(const MGPFenceWait&, Uint32& result) override {
AwaitPark();
result = GL_TIMEOUT_EXPIRED;
return true;
}
Bool OnFenceWaitServer(const MGPFenceWait&) override { AwaitPark(); return true; }
void Install() {
if (Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting([](void* self) {
auto& decoder = Srv::ServerSessionInstance().Applier().*PeerMember(DecoderTag{});
decoder.SetVerbSink(static_cast<FenceBudgetPeer*>(self));
return MOBILEGL_OK;
}, this) != MOBILEGL_OK) ::_exit(92);
}
};
struct BackendTag {
using Type = UniquePtr<MG_Backend::BackendObject> Srv::ServerLoop::*;
friend Type PeerMember(BackendTag);
};
template struct PeerAccess<BackendTag, &Srv::ServerLoop::m_backend>;
void StartControlSession() {
::alarm(15);
MG_Config::Transport = MG_Config::TransportMode::InProcess;
Srv::ServerSessionInstance().SetCapabilityBits(0);
Srv::ServerSessionInstance().SetConsumedSubsystems(kMGPipeSubsystemsMigratedAtP4a);
if (ClientSessionInstance().Start(MG_Config::TransportMode::InProcess, {}) != MOBILEGL_OK)
::_exit(81);
}
struct ReadPeer : Codec::WireVerbSink {
Int32 status = 0;
Uint64 size = 48;
Uint64 observed = 0;
Bool OnReadPixels(const MGPReadbackInfo& info, Uint64 seq, Codec::ReplySink* replies) override {
observed = info.DstSize;
Uint8 pixels[48];
std::memset(pixels, 0x37, sizeof(pixels));
replies->PostReply(seq, status, pixels, size);
return true;
}
void Install() {
auto rc = Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting([](void* self) {
auto& decoder = Srv::ServerSessionInstance().Applier().*PeerMember(DecoderTag{});
decoder.SetVerbSink(static_cast<ReadPeer*>(self));
return MOBILEGL_OK;
}, this);
if (rc != MOBILEGL_OK) ::_exit(82);
}
};
struct ErrorPeer : Codec::ReplySink {
Int32 status = 2;
void PostReply(Uint64 seq, Int32, const void*, Uint64) override {
// Keep the decoded request's sequence: this is an ERROR answer, not a wrong-slot test.
Srv::ServerSessionInstance().Replies().PostReply(seq, status, nullptr, 0);
}
void Install() {
auto rc = Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting([](void* self) {
auto& decoder = Srv::ServerSessionInstance().Applier().*PeerMember(DecoderTag{});
decoder.*PeerMember(RepliesTag{}) = static_cast<ErrorPeer*>(self);
return MOBILEGL_OK;
}, this);
if (rc != MOBILEGL_OK) ::_exit(83);
}
};
struct CapsPeer : MG_Backend::BackendObject {
MG_Backend::DynamicBackendParameters dynamic{};
MG_Backend::GlobalBackendFunctionsTable table{};
RendererInfo renderer{};
void Initialize() override {}
Bool InitCapabilities() override { return true; }
Bool InitWindowSurface() override { return true; }
Bool InitPbufferSurface(EGLint, EGLint) override { return true; }
const RendererInfo& GetRendererInfo() const override { return renderer; }
String GetBackendAPIVersionString() const override { return "4.6"; }
const MG_Backend::GlobalBackendFunctionsTable& GetBackendFunctions() const override { return table; }
const MG_Backend::DynamicBackendParameters& GetDynamicParameters() const override { return dynamic; }
BackendType GetBackendType() const override { return BackendType::DirectGLES; }
};
void ExpectNamedAbort(const ChildResult& child, const char* diagnostic) {
EXPECT_TRUE(DiedOfAbort(child)) << DescribeStatus(child) << '\n' << child.Log;
EXPECT_NE(child.Log.find(diagnostic), std::string::npos) << child.Log;
}
void ExpectChildSuccess(const ChildResult& child) {
EXPECT_TRUE(WIFEXITED(child.Status) && WEXITSTATUS(child.Status) == 0)
<< DescribeStatus(child) << '\n' << child.Log;
}
}
#define C1F_TEARDOWN(Name, Call) \
TEST(RemoteClientControls, TeardownRefuses##Name##OnALiveSession) { \
const auto child = RunInChild([] { \
StartControlSession(); \
UninstallClientWireTables(); \
RemoteEmitTable().Call; \
}); \
ExpectNamedAbort(child, "Fatal{ClientTablesUninstalled, \"" #Name "\"}"); \
}
C1F_TEARDOWN(Clear, GL.Clear(0))
C1F_TEARDOWN(DrawArrays, GL.DrawArrays(0x0004, 0, 0))
C1F_TEARDOWN(ReadPixels, GL.ReadPixels(0, 0, 0, 0, 0x1908, 0x1401, nullptr))
C1F_TEARDOWN(BlitFramebuffer, GL.BlitFramebuffer(0,0,0,0,0,0,0,0,0,0x2600))
C1F_TEARDOWN(Present, Present())
#undef C1F_TEARDOWN
TEST(RemoteClientControls, ReadPixelsPutsTheTightExtentOnTheWire) {
const auto child = RunInChild([] {
StartControlSession();
ReadPeer peer;
peer.Install();
Uint8 pixels[48]{};
RemoteEmitTable().GL.ReadPixels(0, 0, 4, 3, 0x1908, 0x1401, pixels);
if (peer.observed != 48 || pixels[47] != 0x37) ::_exit(84);
ClientSessionInstance().Stop();
});
ExpectChildSuccess(child);
}
TEST(RemoteClientControls, ShortOkReadPixelsReplyRefusesByName) {
const auto child = RunInChild([] {
StartControlSession();
ReadPeer peer;
peer.size = 32;
peer.Install();
Uint8 pixels[48]{};
RemoteEmitTable().GL.ReadPixels(0, 0, 4, 3, 0x1908, 0x1401, pixels);
});
ExpectNamedAbort(child, "Fatal{ReadbackReplyShort, \"ReadPixels 32 < 48\"}");
}
TEST(RemoteClientControls, ErrorReadPixelsReplyRefusesByName) {
const auto child = RunInChild([] {
StartControlSession();
ReadPeer peer;
peer.status = 2;
peer.size = 0;
peer.Install();
Uint8 pixels[48]{};
RemoteEmitTable().GL.ReadPixels(0, 0, 4, 3, 0x1908, 0x1401, pixels);
});
ExpectNamedAbort(child, "Fatal{ReplyError, \"ReadPixels\"}");
}
TEST(RemoteClientControls, UnknownReadPixelsReplyStatusRefusesTightAndBounce) {
for (const Bool bounce : {false, true}) {
const auto child = RunInChild([bounce] {
StartControlSession();
if (bounce) {
MG_State::pGLContext = MakeUnique<MG_State::GLState::GLContext>();
MG_State::pGLContext->SetPixelStoreParam(PixelStoreParam::PackRowLength, 8);
}
ReadPeer peer;
peer.status = 3;
peer.Install();
Uint8 pixels[128]{};
RemoteEmitTable().GL.ReadPixels(0, 0, 4, 3, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
});
ExpectNamedAbort(child, "Fatal{ReplyStatusInvalid, \"ReadPixels\"}");
}
}
TEST(RemoteClientControls, FenceWaitBudgetHonorsGlTimeoutAndFiniteTransportChunks) {
struct WaitCase { Uint64 timeoutNs; MGPWireOp op; Uint32 expectedParkMs; };
const WaitCase cases[] = {
{0, MGPWireOp::FenceWait, 30000},
{1, MGPWireOp::FenceWait, 30001},
{60000000000ull, MGPWireOp::FenceWait, 90000},
{~Uint64(0), MGPWireOp::FenceWait, Transport::kWaitForever - 1},
{~Uint64(0), MGPWireOp::FenceWaitServer, 30000},
};
for (const auto test : cases) {
const auto child = RunInChild([test] {
StartControlSession();
auto& session = ClientSessionInstance();
auto& producer = session.Producer();
BudgetBell bell(producer.SelfDoorbell());
FenceBudgetPeer peer(bell);
peer.Install();
producer.*PeerMember(ProducerBellTag{}) = &bell;
MGPFenceWait request{};
request.TimeoutNs = test.timeoutNs;
Uint32 result = 0;
Int32 status = -1;
Uint64 size = 0;
session.EmitAndWait(test.op, &request, sizeof(request), nullptr, 0,
&result, sizeof(result), &status, &size);
producer.*PeerMember(ProducerBellTag{}) = bell.original;
const auto observed = bell.firstMs.load();
// Doorbell converts the remaining deadline to whole milliseconds. Allow
// scheduler delay, but neither a 30s clamp nor an unbounded sentinel.
if (!bell.entered.load() || observed > test.expectedParkMs ||
observed < test.expectedParkMs - 100 || observed == Transport::kWaitForever) ::_exit(93);
if (test.op == MGPWireOp::FenceWait &&
(status != Codec::ReplySink::kStatusOk || size != sizeof(result) ||
result != GL_TIMEOUT_EXPIRED)) ::_exit(94);
session.Stop();
});
ExpectChildSuccess(child);
}
}
TEST(RemoteClientControls, LongFenceWaitBudgetStillWakesOnShutdown) {
const auto child = RunInChild([] {
StartControlSession();
auto& session = ClientSessionInstance();
auto& producer = session.Producer();
BudgetBell bell(producer.SelfDoorbell());
bell.shutDown = true;
FenceBudgetPeer peer(bell);
peer.Install();
producer.*PeerMember(ProducerBellTag{}) = &bell;
MGPFenceWait request{};
request.TimeoutNs = ~Uint64(0);
Uint32 result = 0;
Int32 status = -1;
Uint64 size = 7;
session.EmitAndWait(MGPWireOp::FenceWait, &request, sizeof(request), nullptr, 0,
&result, sizeof(result), &status, &size);
producer.*PeerMember(ProducerBellTag{}) = bell.original;
peer.release.store(true);
if (status != Codec::ReplySink::kStatusDeclined || size != 0) ::_exit(95);
session.Stop();
});
ExpectChildSuccess(child);
}
TEST(RemoteClientControls, ResourceRespecifyErrorIsNotADecline) {
const auto child = RunInChild([] {
StartControlSession();
ErrorPeer peer;
peer.Install();
MGPResourceDesc desc{};
(void)MGPipeRouteResourceRespecify(desc, nullptr);
});
ExpectNamedAbort(child, "Fatal{ReplyError, \"resource_respecify\"}");
}
TEST(RemoteClientControls, MapPersistentErrorIsNotADecline) {
const auto child = RunInChild([] {
StartControlSession();
ErrorPeer peer;
peer.Install();
MGPHandleOnly handle{};
(void)MGPipeRouteMapPersistent(handle, 64, nullptr);
});
ExpectNamedAbort(child, "Fatal{ReplyError, \"map_persistent\"}");
}
TEST(RemoteClientControls, AnIdenticalRepeatedMakeCurrentRepublishesNothing) {
const auto child = RunInChild([] {
StartControlSession();
if (Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting([](void*) {
auto backend = MakeUnique<CapsPeer>();
backend->dynamic.MaxComputeWorkGroupCount[0] = 113;
Srv::ServerSessionInstance().SetBackend(backend.get());
Srv::ServerLoopInstance().*PeerMember(BackendTag{}) = Move(backend);
return MOBILEGL_OK;
}, nullptr) != MOBILEGL_OK) ::_exit(85);
BackendObject_Remote remote;
auto dpy = reinterpret_cast<EGLDisplay>(1);
auto surface = reinterpret_cast<EGLSurface>(2);
auto ctx = reinterpret_cast<EGLContext>(3);
if (!remote.InitializeEGLDisplay(dpy, nullptr, nullptr) ||
!remote.CreateEGLPbufferSurface(surface, 4, 4) ||
!remote.MakeEGLCurrent(dpy, surface, surface, ctx)) ::_exit(86);
const auto generation = CapsMirrorInstance().Generation();
GLint before = 0;
remote.GetBackendFunctions().GL.GetIntegeri_v(0x91BE, 0, &before);
if (before != 113) ::_exit(87);
if (!remote.MakeEGLCurrent(dpy, surface, surface, ctx)) ::_exit(88);
if (CapsMirrorInstance().Generation() != generation) ::_exit(89);
ClientSessionInstance().Stop();
});
ExpectChildSuccess(child);
}
TEST(RemoteClientControls, ADifferentTupleMakeCurrentIsAdoptedWithoutAPumpOrPresent) {
const auto child = RunInChild([] {
StartControlSession();
if (Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting([](void*) {
auto backend = MakeUnique<CapsPeer>();
backend->dynamic.MaxComputeWorkGroupCount[0] = 113;
Srv::ServerSessionInstance().SetBackend(backend.get());
Srv::ServerLoopInstance().*PeerMember(BackendTag{}) = Move(backend);
return MOBILEGL_OK;
}, nullptr) != MOBILEGL_OK) ::_exit(90);
BackendObject_Remote remote;
auto dpy = reinterpret_cast<EGLDisplay>(1);
auto surface = reinterpret_cast<EGLSurface>(2);
auto ctx = reinterpret_cast<EGLContext>(3);
auto otherCtx = reinterpret_cast<EGLContext>(4);
if (!remote.InitializeEGLDisplay(dpy, nullptr, nullptr) ||
!remote.CreateEGLPbufferSurface(surface, 4, 4) ||
!remote.MakeEGLCurrent(dpy, surface, surface, ctx)) ::_exit(91);
const auto generation = CapsMirrorInstance().Generation();
GLint before = 0;
remote.GetBackendFunctions().GL.GetIntegeri_v(0x91BE, 0, &before);
if (before != 113) ::_exit(92);
if (Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting([](void*) {
static_cast<CapsPeer*>(Srv::ServerLoopInstance().Backend())->dynamic.MaxComputeWorkGroupCount[0] = 227;
return MOBILEGL_OK;
}, nullptr) != MOBILEGL_OK) ::_exit(93);
if (!remote.MakeEGLCurrent(dpy, surface, surface, otherCtx)) ::_exit(94);
GLint after = 0;
remote.GetBackendFunctions().GL.GetIntegeri_v(0x91BE, 0, &after);
if (CapsMirrorInstance().Generation() <= generation || after != 227) ::_exit(95);
ClientSessionInstance().Stop();
});
ExpectChildSuccess(child);
}
TEST(RemoteClientControls, BounceReadPixelsShortReplyRefusesBeforeScatter) {
const auto child = RunInChild([] {
StartControlSession();
MG_State::pGLContext = MakeUnique<MG_State::GLState::GLContext>();
MG_State::pGLContext->SetPixelStoreParam(PixelStoreParam::PackRowLength, 8);
ReadPeer peer;
peer.size = 32;
peer.Install();
Uint8 pixels[128]{};
RemoteEmitTable().GL.ReadPixels(0, 0, 4, 3, 0x1908, 0x1401, pixels);
});
ExpectNamedAbort(child, "Fatal{ReadbackReplyShort, \"ReadPixels 32 < 48\"}");
}
TEST(RemoteClientControls, DeleteTexturesAdvancesTheWireOrdinal) {
const auto child = RunInChild([] {
CapsPeer backend;
Srv::ServerSessionInstance().SetBackend(&backend);
StartControlSession();
ErrorPeer replies;
replies.status = 0; // The peer accepts the resource; the client must publish and delete it.
replies.Install();
MG_Config::Features.PipePush |= kMGPipeSubsystemResources | kMGPipeSubsystemTextureResources |
kMGPipeSubsystemSamplers;
MG_State::pGLContext = MakeUnique<MG_State::GLState::GLContext>();
GLuint texture = 0;
MG_Impl::GLImpl::GenTextures(1, &texture);
MG_Impl::GLImpl::BindTexture(0x0DE1, texture);
MG_Impl::GLImpl::TexImage2D(0x0DE1, 0, 0x8058, 1, 1, 0, 0x1908, 0x1401, nullptr);
const auto before = ClientWireRecordsEmitted();
MG_Impl::GLImpl::DeleteTextures(1, &texture);
const auto after = ClientWireRecordsEmitted();
if (before == 0 || after <= before) ::_exit(91);
ClientSessionInstance().Stop();
});
ExpectChildSuccess(child);
}
TEST(RemoteClientControls, BoundPackBufferOffsetReadRefusesByName) {
const auto child = RunInChild([] {
StartControlSession();
MG_State::pGLContext = MakeUnique<MG_State::GLState::GLContext>();
GLuint buffer = 0;
MG_Impl::GLImpl::GenBuffers(1, &buffer);
MG_Impl::GLImpl::BindBuffer(0x88EB, buffer);
MG_Impl::GLImpl::BufferData(0x88EB, 64, nullptr, 0x88E1);
RemoteEmitTable().GL.ReadPixels(0, 0, 1, 1, 0x1908, 0x1401, reinterpret_cast<void*>(16));
});
ExpectNamedAbort(child, "Fatal{UnmigratedVerb, \"ReadPixels+PACK_BUFFER\"}");
}
TEST(RemoteClientControls, ServerRoleRespecifyDoesNotEmitClientInitialBytes) {
// P5c (hd): the in-process server-role drive itself is now refused by name. The emit
// path's first move is the client allocator's Acquire for the buffer's handle, and with
// an active transport that entry is Fatal{RoleViolation, "MGPipeSlots"} from the apply
// thread (CONTRACT-P5C §3.1, rule E). "No client initial bytes" therefore holds the
// strong way - there is no client-emitter path for the server role to reach at all; ev's
// SEG_EVENT producers are what replaces the mechanism this case used to drive.
const auto child = RunInChild([] {
CapsPeer backend;
Srv::ServerSessionInstance().SetBackend(&backend);
StartControlSession();
MG_Config::Features.PipePush |= kMGPipeSubsystemResources;
Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting([](void*) {
MG_State::GLState::BufferObject buffer(701);
Uint8 data[64]{};
buffer.Respecify(sizeof(data), data);
return MOBILEGL_OK;
}, nullptr);
ClientSessionInstance().Stop();
});
ExpectNamedAbort(child, "Fatal{RoleViolation, \"MGPipeSlots\"}");
}
TEST(RemoteClientControls, HeaderOnlyDynamicStateCrossesWithoutBlobMissing) {
const auto child = RunInChild([] {
StartControlSession();
MGPDynamicState record{};
record.ChunkMask = 0;
const auto before = ClientWireRecordsEmitted();
gMGPipeContext.SetDynamicState(&record, nullptr, 0);
if (ClientWireRecordsEmitted() != before + 1) ::_exit(94);
ClientSessionInstance().Stop();
});
ExpectChildSuccess(child);
}
TEST(RemoteClientControls, ServerRoleFlushDoesNotRunTheClientSubDataFollowup) {
// P5c (hd): same refusal as the respecify sibling above - a server-role flush reaching the
// client emit path is stopped at the allocator entry before any follow-up question arises
// (Fatal{RoleViolation, "MGPipeSlots"}; ev's events replace the mechanism).
const auto child = RunInChild([] {
CapsPeer backend;
Srv::ServerSessionInstance().SetBackend(&backend);
StartControlSession();
MG_Config::Features.PipePush |= kMGPipeSubsystemResources;
Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting([](void*) {
MGPipeResourceOps ops{};
ops.Create = [](MGPipeHandle, const MGPResourceDesc&) {};
ops.Respecify = [](MGPipeHandle, const MGPResourceDesc&, const void*) {};
ops.SubData = [](MGPipeHandle, const MGPSubData&, const void*) {};
ops.FlushRange = [](MGPipeHandle, const MGPFlushRange&, const void*) {};
ops.Destroy = [](MGPipeHandle) {};
MGPipeSetResourceOps(&ops);
{
MG_State::GLState::BufferObject buffer(702);
buffer.Resize(64);
MGPipeEmitResourceFlushRange(buffer, 16, 16, 0);
}
MGPipeSetResourceOps(nullptr);
return MOBILEGL_OK;
}, nullptr);
ClientSessionInstance().Stop();
});
ExpectNamedAbort(child, "Fatal{RoleViolation, \"MGPipeSlots\"}");
}
// Uses the shipped EGL/GL frontend and real DirectGLES driver. Invoked explicitly by the
// package gate because a native driver is not a prerequisite of the unit lane.
int RunPackGpuControl(bool split) {
const auto child = RunInChild([=] {
::alarm(45);
setenv("EGL_PLATFORM", "surfaceless", 1);
setenv("MOBILEGL_BACKEND_TYPE", "DirectGLES", 1);
setenv("MOBILEGL_TRANSPORT", split ? "inproc" : "monolith", 1);
MobileGL::Initialize();
namespace E = MG_Impl::EGLImpl;
namespace G = MG_Impl::GLImpl;
const auto display = E::GetDisplay(EGL_DEFAULT_DISPLAY);
if (!E::Initialize(display, nullptr, nullptr)) ::_exit(101);
E::BindAPI(EGL_OPENGL_API);
EGLConfig config{};
EGLint count = 0;
const EGLint configAttribs[] = {EGL_SURFACE_TYPE, EGL_PBUFFER_BIT,
EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT, EGL_NONE};
if (!E::ChooseConfig(display, configAttribs, &config, 1, &count) || count < 1) ::_exit(102);
const auto context = E::CreateContext(display, config, EGL_NO_CONTEXT, nullptr);
const EGLint surfaceAttribs[] = {EGL_WIDTH, 4, EGL_HEIGHT, 4, EGL_NONE};
const auto surface = E::CreatePbufferSurface(display, config, surfaceAttribs);
if (!E::MakeCurrent(display, surface, surface, context)) ::_exit(103);
G::ClearColor(0, 1, 0, 1);
G::Clear(GL_COLOR_BUFFER_BIT);
GLuint pbo = 0;
G::GenBuffers(1, &pbo);
G::BindBuffer(GL_PIXEL_PACK_BUFFER, pbo);
G::BufferData(GL_PIXEL_PACK_BUFFER, 64, nullptr, GL_STREAM_READ);
G::ReadPixels(0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, reinterpret_cast<void*>(16));
if (split) ::_exit(104); // Must have refused by name, never returned or dereferenced 16.
Uint8 pixels[4]{};
G::GetBufferSubData(GL_PIXEL_PACK_BUFFER, 16, 4, pixels);
if (pixels[0] != 0 || pixels[1] != 255 || pixels[2] != 0 || pixels[3] != 255) ::_exit(105);
std::printf("C1F_PACK_MONOLITH bytes at offset 16: %u %u %u %u\n", pixels[0], pixels[1], pixels[2], pixels[3]);
std::fflush(stdout);
});
const bool ok = split
? DiedOfAbort(child) && child.Log.find("Fatal{UnmigratedVerb, \"ReadPixels+PACK_BUFFER\"}") != std::string::npos
: WIFEXITED(child.Status) && WEXITSTATUS(child.Status) == 0;
std::printf("C1F_PACK_%s: %s (%s)\n%s", split ? "INPROC" : "MONOLITH",
ok ? "PASS" : "FAIL", DescribeStatus(child).c_str(), child.Log.c_str());
return ok ? 0 : 1;
}
namespace {
struct PatternReadPeer : ReadPeer {
Bool OnReadPixels(const MGPReadbackInfo& info, Uint64 seq, Codec::ReplySink* replies) override {
Uint8 bytes[32];
for (Uint8 i = 0; i < sizeof(bytes); ++i) bytes[i] = i + 1;
replies->PostReply(seq, 0, bytes, info.DstSize);
return true;
}
};
std::atomic<Bool> retirementRetryStarted{false};
std::atomic<Bool> retirementWaitObserved{false};
void HoldRetirementUntilProducerWaits() {
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2);
while (std::chrono::steady_clock::now() < deadline) {
if (retirementRetryStarted.load() && ClientSessionInstance().Control()->producerParked.load()) {
retirementWaitObserved.store(true);
break;
}
std::this_thread::yield();
}
Srv::ServerLoopInstance().SetBeforeRetireHookForTesting(nullptr);
}
struct DrawPeer : Codec::WireVerbSink {
Bool OnDrawVbo(const MGPDrawInfo&, const MGPDrawRange*, const MGHostSpan*,
const MGPDrawIndirect*) override { return true; }
void Install() {
Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting([](void* self) {
auto& decoder = Srv::ServerSessionInstance().Applier().*PeerMember(DecoderTag{});
decoder.SetVerbSink(static_cast<DrawPeer*>(self));
return MOBILEGL_OK;
}, this);
}
};
}
TEST(RemoteClientControls, ReadPixelsSwapsComponentsInTightAndScatteredReplies) {
const auto child = RunInChild([] {
StartControlSession();
MG_State::pGLContext = MakeUnique<MG_State::GLState::GLContext>();
PatternReadPeer peer;
peer.Install();
struct Pair { GLenum format; GLenum type; Uint32 bpp; Uint32 group; };
for (const Pair pair : {Pair{GL_RGBA, GL_UNSIGNED_SHORT, 8, 2},
Pair{GL_RGBA, GL_FLOAT, 16, 4},
Pair{GL_RGBA, GL_UNSIGNED_INT_8_8_8_8, 4, 4},
Pair{GL_DEPTH_STENCIL, GL_FLOAT_32_UNSIGNED_INT_24_8_REV, 8, 4},
Pair{GL_RGBA, GL_UNSIGNED_BYTE, 4, 1}}) {
for (int scatter : {0, 1}) for (int swap : {0, 1}) {
auto& context = *MG_State::pGLContext;
context.SetPixelStoreParam(PixelStoreParam::PackSwapBytes, swap);
context.SetPixelStoreParam(PixelStoreParam::PackRowLength, scatter ? 3 : 0);
context.SetPixelStoreParam(PixelStoreParam::PackSkipPixels, scatter);
Uint8 result[160];
std::memset(result, 0xCD, sizeof(result));
RemoteEmitTable().GL.ReadPixels(0, 0, 1, 2, pair.format, pair.type, result);
for (Uint32 at = 0; at < sizeof(result); ++at) {
Uint8 expected = 0xCD;
for (Uint32 row = 0; row < 2; ++row) {
const Uint32 begin = (scatter ? 1 + 3 * row : row) * pair.bpp;
if (at >= begin && at < begin + pair.bpp) {
Uint32 i = at - begin;
if (swap) i = (i / pair.group) * pair.group + pair.group - 1 - i % pair.group;
expected = row * pair.bpp + i + 1;
}
}
if (result[at] != expected) ::_exit(110);
}
}
}
ClientSessionInstance().Stop();
});
ExpectChildSuccess(child);
}
TEST(RemoteClientControls, PersistentMapProducerRefusesTheApplyThread) {
const auto child = RunInChild([] {
StartControlSession();
Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting([](void*) {
PersistentMapTracker::Instance().PushAllMembers();
return MOBILEGL_OK;
}, nullptr);
});
ExpectNamedAbort(child, "Fatal{RoleViolation, \"PushAllMembers\"}");
}
TEST(RemoteClientControls, CommandWrapWaitsForRetirementAfterThePriorRecordApplied) {
const auto child = RunInChild([] {
MG_Config::Ipc.RingMb = 1;
StartControlSession();
DrawPeer peer;
peer.Install();
Vector<MGPDrawRange> ranges(32768);
MGPDrawInfo info{};
auto emit = [&](Uint32 count) {
info.NumDraws = count;
return ClientSessionInstance().EmitAndWait(MGPWireOp::DrawVbo, &info, sizeof(info),
ranges.data(), count * sizeof(MGPDrawRange), nullptr, 0, nullptr);
};
emit(28000); // Position the next record so that its successor needs a wrap pad.
peer.Install(); // A mailbox round trip also lets the previous drain retire.
Srv::ServerLoopInstance().SetBeforeRetireHookForTesting(&HoldRetirementUntilProducerWaits);
const auto prior = emit(32768);
if (ClientSessionInstance().Control()->retiredSeq.load() >= prior) ::_exit(111);
retirementRetryStarted.store(true);
const auto next = emit(32768);
if (next != prior + 1 || !retirementWaitObserved.load()) ::_exit(112);
ClientSessionInstance().Stop();
});
ExpectChildSuccess(child);
}
TEST(RemoteClientControls, StagingWaitsForRetirementAfterThePriorRecordApplied) {
const auto child = RunInChild([] {
MG_Config::Ipc.StageMb = 1;
StartControlSession();
Vector<Uint8> bytes(768 * 1024, 0x57);
auto& session = ClientSessionInstance();
session.Encoder().StageBytes(bytes.data(), bytes.size());
Srv::ServerLoopInstance().SetBeforeRetireHookForTesting(&HoldRetirementUntilProducerWaits);
MGPBindRenderState bind{};
const auto prior = session.EmitAndWait(MGPWireOp::BindRenderState, &bind, sizeof(bind),
nullptr, 0, nullptr, 0, nullptr);
if (session.Control()->retiredSeq.load() >= prior) ::_exit(113);
retirementRetryStarted.store(true);
session.Encoder().StageBytes(bytes.data(), bytes.size());
if (!retirementWaitObserved.load() || session.Encoder().StageReclaimWaits() != 1) ::_exit(114);
session.Stop();
});
ExpectChildSuccess(child);
}
// P5e (gl) RED-ONCE, ID-111. THE SERVER-SIDE MIRROR OF THE CLIENT LATCH - the line that makes
// ID-90 ("Magma keeps the lockstep for the whole of P5e") true in code rather than in prose.
//
// The control session calls SetCapabilityBits(0), so its CallMask IS set and does NOT carry
// bit 10. That is a Magma server's answer for ever (MGPipeRunAheadCapBitsFor returns kCapNone
// off DirectGLES) and an Espryt server's answer for the whole bring-up window. A DrawVbo then
// crosses for real, so what is under test is the stamp PipeApplier::ApplyOne writes and not a
// test-only branch beside it.
//
// FOUR ASSERTIONS, because each one alone can be true for the wrong reason:
// 1. the session really lacks the bit, said in its own words rather than inferred from the
// stamp being tested (ID-102: the action under test is the probe itself);
// 2. the wire really answers UNBARRIERED for this record - draw_vbo is kWaitNone - so the
// `wireSaysBarriered` arm is the one a flipped constant would take;
// 3. the record that actually crossed was stamped barriered;
// 4. and the stamp is STILL barriered at the POST-FLIP value of the wait-rule constant.
//
// (4) IS THE ONE THAT MATTERS AND IT IS STATED AT `true` RATHER THAN READ FROM THE CONSTANT.
// kMGPipeP5eClientWaitRuleLanded is false today, so (3) alone would pass for a server that had
// no capability conjunct at all - and would keep passing until the integration commit, at which
// point Magma SIGSEGVs on its first draw with the lane still green. Asking the pure function
// for the post-flip answer is how the claim gets tested before the day it starts deciding.
//
// THE RED: delete `serverPublishesRunAhead &&` from MGPipeApplierStampsBarriered
// (MG_Remote/Server/PipeApplier.h) and (4) flips to false - the child exits 124.
TEST(RemoteClientControls, ADrawOnASessionWithoutTheRunAheadBitStaysStampedBarriered) {
const auto child = RunInChild([] {
StartControlSession();
DrawPeer peer;
peer.Install();
MGPDrawInfo info{};
info.NumDraws = 1;
MGPDrawRange range{};
if (ClientSessionInstance().EmitAndWait(MGPWireOp::DrawVbo, &info, sizeof(info), &range,
sizeof(range), nullptr, 0, nullptr) == 0)
::_exit(120);
// Read the stamp on the thread that owns it: it is a thread_local describing "which
// record is the apply thread inside right now", and ApplyOne leaves it standing.
if (Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting([](void*) {
if (Srv::MGPipeServerPublishesRunAhead()) ::_exit(121);
MGPDrawInfo probe{};
probe.NumDraws = 1;
if (MGPipeBarriered(MGPWireOp::DrawVbo, &probe, MGPipeApplier())) ::_exit(122);
if (!MGPipeApplierCurrentRecordIsBarriered()) ::_exit(123);
if (!Srv::MGPipeApplierStampsBarriered(/*clientWaitRuleLanded=*/true,
Srv::MGPipeServerPublishesRunAhead(),
/*wireSaysBarriered=*/false))
::_exit(124);
return MOBILEGL_OK;
},
nullptr) != MOBILEGL_OK)
::_exit(125);
ClientSessionInstance().Stop();
});
ExpectChildSuccess(child);
}
#endif