Files
MobileGL/MobileGL/MG_Test/Wire/RemoteClientControls.inc
T
BZLZHH f2b55b9d2d [Perf] (MG_Remote/Client): the buffer half of resource_respecify stops waiting - its answer is discarded at both call sites - and the measurement says the texture half is where the other 20,611 waits live
- The two halves of this row are not the same row. PipeFill.cpp:1105 and :1142 both call
  MGPipeRouteResourceRespecify(desc, nullptr) and drop the Bool, so for a buffer the wait bought a
  value that was thrown away. It goes fire-and-forget with the provisional-accept shape
  resource_subdata has had since item B.
- The texture half keeps its answer ON PURPOSE: there the Bool is the self-heal trigger AND the
  gate on the acceptance mirror, and TextureEmit.h:885-888 (ID-18 M3) says advancing that mirror on
  an unconfirmed answer is how the next identical call gets suppressed "against a record that was
  never stored". That half needs the provisional/really-accepted distinction plumbed through the
  reply mailbox before it can stop waiting; it is not a flag flip.
- MEASURED, same frame, same 101,421,396 B staged: waits 25,176 -> 25,154 and by_op 3=20633 ->
  3=20611. So the buffer half is worth 22 waits (0.1%) and the remaining 20,611 are texture
  respecifies - which is what says the next step is the mirror design, not another flag. Wall
  134.4 s -> 130.9 s.
- The ERROR-is-not-a-decline case moves to the texture half: a row that reads no answer cannot fold
  ERROR into a decision, so the rule it exists for belongs to the half that still waits (the
  transport fault is caught at the next row that does). New case
  ABufferRespecifyTakesNoReplyWhileATextureOneStillDoes pins both directions; red-once with the
  branch removed is exit 155. RemoteClientTest 169/169.
2026-09-25 22:53:38 +08:00

1155 lines
59 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>;
MG_Backend::BackendObject* ControlCapsPeer();
void StartControlSession(MG_Backend::BackendObject* capsSource = nullptr) {
::alarm(15);
MG_Config::Transport = MG_Config::TransportMode::InProcess;
Srv::ServerSessionInstance().SetBackend(capsSource ? capsSource : ControlCapsPeer());
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; }
};
MG_Backend::BackendObject* ControlCapsPeer() {
// A real encoded initial snapshot is required even when the apply fixture
// substitutes every verb and has no GPU backend of its own.
static CapsPeer backend;
return &backend;
}
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) {
// 120000 below is ClientSession.cpp's kBarrierTimeoutMs, the transport grace every CPU bound
// wait adds on top of the GL timeout. It was 30 s; lavapipe's texture-handle registration
// (vkCreateImageView -> llvmpipe_register_texture) exceeds that in a single apply stall.
struct WaitCase { Uint64 timeoutNs; MGPWireOp op; Uint32 expectedParkMs; };
const WaitCase cases[] = {
{0, MGPWireOp::FenceWait, 120000},
{1, MGPWireOp::FenceWait, 120001},
{60000000000ull, MGPWireOp::FenceWait, 180000},
{~Uint64(0), MGPWireOp::FenceWait, Transport::kWaitForever - 1},
{~Uint64(0), MGPWireOp::FenceWaitServer, 120000},
};
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 the barrier 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) {
// THE TEXTURE HALF, WHICH IS THE HALF THAT STILL READS ITS ANSWER (P12). This case used a
// zeroed descriptor, which is a BUFFER target - and the buffer half is now fire-and-forget
// because both of its call sites discard the Bool (PipeFill.cpp:1105, :1142). A row that reads
// no answer cannot fold ERROR into a decision, so the rule this case exists for moved to the
// half that does read one: the transport fault is caught here, at the next row that waits,
// instead of at a record whose verdict nobody ever looked at.
const auto child = RunInChild([] {
StartControlSession();
ErrorPeer peer;
peer.Install();
MGPResourceDesc desc{};
desc.Target = static_cast<Uint8>(MG_Pipe::MGPipeResourceTarget::Tex2D);
(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;
StartControlSession(&backend);
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) {
// Historical name retained: the live path now copies the reply into the PBO.
const auto child = RunInChild([] {
StartControlSession();
ReadPeer peer;
peer.size = 4;
peer.Install();
MG_State::pGLContext = MakeUnique<MG_State::GLState::GLContext>();
GLuint buffer = 0;
MG_Impl::GLImpl::GenBuffers(1, &buffer);
MG_Impl::GLImpl::BindBuffer(0x88EB, buffer);
Uint8 seed[64];
std::memset(seed, 0x5a, sizeof(seed));
MG_Impl::GLImpl::BufferData(0x88EB, sizeof(seed), seed, 0x88E1);
RemoteEmitTable().GL.ReadPixels(0, 0, 1, 1, 0x1908, 0x1401, reinterpret_cast<void*>(16));
Uint8 actual[64]{};
MG_Impl::GLImpl::GetBufferSubData(0x88EB, 0, sizeof(actual), actual);
for (SizeT i = 0; i < sizeof(actual); ++i)
if (actual[i] != (i >= 16 && i < 20 ? 0x37 : 0x5a)) ::_exit(104);
if (peer.observed != 4) ::_exit(105);
ClientSessionInstance().Stop();
});
ExpectChildSuccess(child);
}
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;
StartControlSession(&backend);
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;
StartControlSession(&backend);
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));
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_%s bytes at offset 16: %u %u %u %u\n", split ? "INPROC" : "MONOLITH",
pixels[0], pixels[1], pixels[2], pixels[3]);
std::fflush(stdout);
});
const bool ok = 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()->Progress.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()->Progress.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);
}
// =====================================================================================
// P12: a DECLINED reply is an answer, not a corruption
// =====================================================================================
//
// ReplySink's status space is three-valued and its own header says what the values mean:
// 0 OK, 1 DECLINED IS A REAL ANSWER, 2 ERROR (PipeWireCodec.h:457-460). The escape rows are
// held to the other half of the same rule - "ERROR IS NOT A DECLINE", an escape may not fold
// 2 into `false` (WireTables.cpp:446-450). The fence and query consumers folded BOTH 1 and 2
// into a single abort (EmitTables.cpp ReadFenceReply, QueryEmit.inc ReadQueryReply), so a
// client that had latched device-lost - where every verb is DECLINED by design,
// ClientSession.cpp:1837 - died of Fatal{ProtocolCorruption, "Fence.reply"} on its next
// glClientWaitSync instead of going quiet the way the design promises ("GL calls become
// no-ops", design/08-runtime-and-platform.md:33).
//
// THE PEER BELOW IS THE SERVER'S OWN DECLINE, not a substitution of the status byte: these
// verbs answer false when the server cannot answer them, and the codec posts exactly that as
// DECLINED with a zero-length payload (PipeWireCodec.cpp:1820-1830 for the two fence rows,
// :1841-1861 for the three query rows). The CLIENT's own decline is pinned separately, by
// LongFenceWaitBudgetStillWakesOnShutdown above.
//
// THE RED, RUN ONCE: take the `if (status == Wire::ReplySink::kStatusDeclined)` arm out of
// ReadFenceReply and the first case below dies of Fatal{ProtocolCorruption, "Fence.reply"}
// instead of asserting the no-op - which is the split client's Minecraft 26.2 crash.
struct DecliningSyncPeer : Codec::WireVerbSink {
Bool OnFenceCreate(const MG_Pipe::MGPHandleOnly&) override { return true; }
Bool OnFenceStatus(const MG_Pipe::MGPHandleOnly&, Uint32&) override { return false; }
Bool OnFenceWait(const MGPFenceWait&, Uint32&) override { return false; }
Bool OnQueryCreate(const MGPQueryDesc&) override { return true; }
Bool OnQueryBegin(const MGPQueryDesc&) override { return true; }
Bool OnQueryCounter(const MGPQueryDesc&) override { return true; }
Bool OnQueryAvailable(const MG_Pipe::MGPHandleOnly&, Uint32&) override { return false; }
Bool OnQueryResult(const MGPQueryResultRequest&, Codec::QueryResultReply&) override { return false; }
Bool OnQueryTimestamp(const MGPTimestampRequest&, Int64&) override { return false; }
void Install() {
const auto rc = Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting([](void* self) {
auto& decoder = Srv::ServerSessionInstance().Applier().*PeerMember(DecoderTag{});
decoder.SetVerbSink(static_cast<DecliningSyncPeer*>(self));
return MOBILEGL_OK;
}, this);
if (rc != MOBILEGL_OK) ::_exit(130);
}
};
TEST(RemoteClientControls, DeclinedFenceReplyAnswersSignaledInsteadOfAborting) {
const auto child = RunInChild([] {
StartControlSession();
DecliningSyncPeer peer;
peer.Install();
auto* sync = RemoteEmitTable().GL.FenceSync();
if (sync == nullptr) ::_exit(131);
// glClientWaitSync on a fence nobody can wait on: signaled now, as every fallback in
// this tree already answers (GL_Sync.cpp:103-106, PipeApplier.cpp:175-177).
if (RemoteEmitTable().GL.ClientWaitSync(sync, 0, 0) != GL_ALREADY_SIGNALED) ::_exit(132);
// GL_SYNC_STATUS: the same answer, in the boolean the row carries.
if (!RemoteEmitTable().GL.GetSyncStatus(sync)) ::_exit(133);
RemoteEmitTable().GL.DeleteSync(sync);
ClientSessionInstance().Stop();
});
ExpectChildSuccess(child);
}
TEST(RemoteClientControls, DeclinedQueryRepliesAnswerNoAnswerYetInsteadOfAborting) {
const auto child = RunInChild([] {
StartControlSession();
DecliningSyncPeer peer;
peer.Install();
auto* query = RemoteEmitTable().GL.BeginTimeElapsedQuery();
if (query == nullptr) ::_exit(134);
// GL_QUERY_RESULT_AVAILABLE: "not available" - the answer an unfinished query gives,
// and the one that makes a waiting application yield rather than spin.
if (RemoteEmitTable().GL.IsQueryResultAvailable(query)) ::_exit(135);
// GL_QUERY_RESULT: no value produced, and the caller's out-parameter untouched.
Uint64 nanoseconds = 7;
if (RemoteEmitTable().GL.GetQueryResult64(query, false, &nanoseconds) || nanoseconds != 7)
::_exit(136);
// glGetInteger64v(GL_TIMESTAMP): there is no timestamp to report.
if (RemoteEmitTable().GL.GetGpuTimestampNs() != 0) ::_exit(137);
RemoteEmitTable().GL.DeleteBackendQuery(query);
ClientSessionInstance().Stop();
});
ExpectChildSuccess(child);
}
TEST(RemoteClientControls, ErrorFenceReplyStillRefusesByName) {
// THE TOLERANCE IS NOT BLANKET. An ERROR is a transport fault or a server-side refusal,
// never "the server said no" (R-5/M4), and it keeps the named Fatal it always had.
const auto child = RunInChild([] {
StartControlSession();
// The rows still decline (so the fence is never handed to a backend that cannot
// create one) and EVERY answer that is posted says ERROR instead.
DecliningSyncPeer rows;
rows.Install();
ErrorPeer peer;
peer.Install();
auto* sync = RemoteEmitTable().GL.FenceSync();
if (sync == nullptr) ::_exit(138);
(void)RemoteEmitTable().GL.ClientWaitSync(sync, 0, 0);
});
ExpectNamedAbort(child, "Fatal{ProtocolCorruption, \"Fence.reply\"}");
}
TEST(RemoteClientControls, DeviceLostRespecifyDoesNotDieOfTheUncarriedBytesCheck) {
// P12 exit gate (a): killing the server must give a CLEAN device-lost latch. With the
// latch set every verb is DECLINED, so the resource_subdata that R-13.3 owes emits
// nothing - which is the latch working, not bytes the client forgot to carry. The check
// at PipeFill.cpp:1115 compared client record ordinals only, so the next glBufferData
// with data aborted the client by name instead.
const auto child = RunInChild([] {
StartControlSession();
// Taken through the production entry point, the way the barrier and the encoder take
// it when a descriptor whose far end only the peer held reports hangup.
ClientSessionInstance().LatchDeviceLost("RemoteClientControls: the self-check case");
MG_Config::Features.PipePush |= kMGPipeSubsystemResources;
MG_State::pGLContext = MakeUnique<MG_State::GLState::GLContext>();
GLuint buffer = 0;
MG_Impl::GLImpl::GenBuffers(1, &buffer);
MG_Impl::GLImpl::BindBuffer(0x8892, buffer);
Uint8 seed[64];
std::memset(seed, 0x5a, sizeof(seed));
MG_Impl::GLImpl::BufferData(0x8892, sizeof(seed), seed, 0x88E4);
ClientSessionInstance().Stop();
});
ExpectChildSuccess(child);
}
// =====================================================================================
// P12 item B: the texture half stops owning a reply on the wire arm
// =====================================================================================
//
// THE NUMBER IS THE GATE. `wait_replies` is what the handoff measured 55,428 of in one
// Minecraft frame; LinkMetrics.cpp:76-80 counts exactly the records that OWN a reply slot, so
// it is the same quantity the per-frame P65LinkMetrics line prints. The case drives it over a
// live split session: a pure level upload must cross the wire and open NO reply wait, while a
// create row - which is kWaitReply and always was - still moves the number.
//
// THE SECOND HALF IS THE NEGATIVE CONTROL FOR THE FIRST. Without it, a session that counted
// nothing at all (metrics off, no records) would pass the half that exists to fail.
//
// THE RED, RUN ONCE: take the `kClientWire` arm out of MGPipeSubDataWantsItsReply (PipeRoute.h)
// and the upload delta becomes 1 - the child exits 142.
TEST(RemoteClientControls, TextureUploadsStopOwningAReplyOnTheWireArm) {
const auto child = RunInChild([] {
::setenv("MOBILEGL_PIPE_STATS", "1", 1);
CapsPeer backend;
StartControlSession(&backend);
MG_Config::Features.PipePush |= kMGPipeSubsystemResources |
kMGPipeSubsystemTextureResources | kMGPipeSubsystemSamplers;
MG_State::pGLContext = MakeUnique<MG_State::GLState::GLContext>();
Uint8 pixels[4 * 4 * 4];
std::memset(pixels, 0x21, sizeof(pixels));
GLuint texture = 0;
MG_Impl::GLImpl::GenTextures(1, &texture);
MG_Impl::GLImpl::BindTexture(0x0DE1, texture);
// THE CONTROL ROW. resource_create is kWaitReply (PipeCalls.def), so it owns a reply
// slot and this counter MUST move on it.
const auto beforeCreate = Transport::LinkMetricsReplyWaits();
MG_Impl::GLImpl::TexImage2D(0x0DE1, 0, 0x8058, 4, 4, 0, 0x1908, 0x1401, pixels);
MGP_FILL(DrawArrays);
if (Transport::LinkMetricsReplyWaits() <= beforeCreate) ::_exit(140);
// THE ROW UNDER TEST. A second write into the same level is a pure resource_subdata: the
// resource is created, so no create crosses, and the level is dirty again - the only
// record it can produce is the one item B is about.
const auto beforeRecords = ClientWireRecordsEmitted();
const auto beforeWaits = Transport::LinkMetricsReplyWaits();
MG_Impl::GLImpl::TexSubImage2D(0x0DE1, 0, 0, 0, 2, 2, 0x1908, 0x1401, pixels);
MGP_FILL(DrawArrays);
if (ClientWireRecordsEmitted() <= beforeRecords) ::_exit(141);
if (Transport::LinkMetricsReplyWaits() != beforeWaits) ::_exit(142);
ClientSessionInstance().Stop();
});
ExpectChildSuccess(child);
}
// =====================================================================================
// P12 item B: WHICH ROWS BUY THE WAITS - the measurement the handoff left open (its 5.4)
// =====================================================================================
//
// A total cannot answer it. 55,428 reply waits in one frame is the same number whether they are
// uploads (item B), creates (per resource) or parameter sets (per call), and those three have
// different fixes - so the split is what says whether B is the whole story or a fraction of one.
// An atlas-shaped load makes it visible: one sprite per texture, one glTexImage2D and four
// glTexParameteri, which is the call shape an atlas stitch has.
//
// A MEASUREMENT WITH A STRUCTURAL GATE UNDER IT, not a performance gate: the assertions are the
// ones that can be WRONG about the mechanism (the upload rows contribute zero after item B, the
// create / respecify / parameter rows do not, and the per-op tally sums to the total), and the
// numbers themselves are printed for the register.
TEST(RemoteClientControls, ReplyWaitsByOpForAnAtlasShapedLoad) {
const auto child = RunInChild([] {
::setenv("MOBILEGL_PIPE_STATS", "1", 1);
CapsPeer backend;
StartControlSession(&backend);
MG_Config::Features.PipePush |= kMGPipeSubsystemResources |
kMGPipeSubsystemTextureResources | kMGPipeSubsystemSamplers;
MG_State::pGLContext = MakeUnique<MG_State::GLState::GLContext>();
constexpr int kSprites = 64;
Uint8 pixels[16 * 16 * 4];
std::memset(pixels, 0x5a, sizeof(pixels));
// SHAPE 1 - THE ATLAS STITCH, which is what item B was aimed at: ONE texture and many
// small writes into it. If B works, this shape's wait count stops tracking the upload
// count at all.
GLuint atlas = 0;
MG_Impl::GLImpl::GenTextures(1, &atlas);
MG_Impl::GLImpl::BindTexture(0x0DE1, atlas);
MG_Impl::GLImpl::TexImage2D(0x0DE1, 0, 0x8058, 256, 256, 0, 0x1908, 0x1401, nullptr);
MG_Impl::GLImpl::TexParameteri(0x0DE1, 0x2801, 0x2601);
MGP_FILL(DrawArrays);
const Uint64 afterAtlasSetup = Transport::LinkMetricsReplyWaits();
for (int i = 0; i < kSprites; ++i) {
MG_Impl::GLImpl::TexSubImage2D(0x0DE1, 0, (i % 8) * 16, (i / 8) * 16, 16, 16, 0x1908,
0x1401, pixels);
MGP_FILL(DrawArrays); // the drain point
}
const Uint64 atlasWaits = Transport::LinkMetricsReplyWaits() - afterAtlasSetup;
std::printf("shape 1 atlas stitch: %d sub-uploads into one level -> wait_replies=%llu\n",
kSprites, static_cast<unsigned long long>(atlasWaits));
const Uint64 atlasWaitsBefore = Transport::LinkMetricsReplyWaits();
// SHAPE 2 - A TEXTURE PER SPRITE, the other thing a stitcher can do (and what the mipmap
// and dynamic-texture paths look like): every sprite pays a storage definition and its
// own parameter set.
for (int i = 0; i < kSprites; ++i) {
GLuint texture = 0;
MG_Impl::GLImpl::GenTextures(1, &texture);
MG_Impl::GLImpl::BindTexture(0x0DE1, texture);
MG_Impl::GLImpl::TexImage2D(0x0DE1, 0, 0x8058, 16, 16, 0, 0x1908, 0x1401, pixels);
MG_Impl::GLImpl::TexParameteri(0x0DE1, 0x2802, 0x2901); // WRAP_S = CLAMP_TO_EDGE
MG_Impl::GLImpl::TexParameteri(0x0DE1, 0x2803, 0x2901); // WRAP_T
MG_Impl::GLImpl::TexParameteri(0x0DE1, 0x2801, 0x2601); // MIN_FILTER = LINEAR
MG_Impl::GLImpl::TexParameteri(0x0DE1, 0x2800, 0x2601); // MAG_FILTER = LINEAR
MGP_FILL(DrawArrays); // the drain point
}
// A PROBE FOR REDUNDANCY, before anyone tries to remove it: does a repeated set of the
// SAME value produce another record? If it does not, then the device run's 18,056
// set_texture_params records in one window are genuine changes and there is nothing to
// dedup - and any "fix" would be a fix for a problem that does not exist.
{
const Uint32 paramsOp = static_cast<Uint32>(MGPWireOp::SetTextureParams);
const Uint64 beforeSame = Transport::LinkMetricsRecordsFor(paramsOp);
MG_Impl::GLImpl::TexParameteri(0x0DE1, 0x2801, 0x2600); // MIN_FILTER = NEAREST
MGP_FILL(DrawArrays);
const Uint64 afterFirst = Transport::LinkMetricsRecordsFor(paramsOp);
MG_Impl::GLImpl::TexParameteri(0x0DE1, 0x2801, 0x2600); // the SAME value again
MGP_FILL(DrawArrays);
const Uint64 afterSame = Transport::LinkMetricsRecordsFor(paramsOp);
MG_Impl::GLImpl::TexParameteri(0x0DE1, 0x2801, 0x2601); // a DIFFERENT value
MGP_FILL(DrawArrays);
const Uint64 afterChange = Transport::LinkMetricsRecordsFor(paramsOp);
std::printf("params probe: first=%llu same-again=%llu changed=%llu (deltas %llu/%llu)\n",
static_cast<unsigned long long>(afterFirst - beforeSame),
static_cast<unsigned long long>(afterSame - beforeSame),
static_cast<unsigned long long>(afterChange - beforeSame),
static_cast<unsigned long long>(afterFirst - beforeSame),
static_cast<unsigned long long>(afterSame - afterFirst));
std::fflush(nullptr);
}
// PRINT FIRST, ASSERT SECOND: the table is the point of the case, and a gate that fired
// before it would hide the numbers on exactly the run that needs them - the red one.
//
// BOTH HALVES OF THE SAME FRAME: what it PAID (waits) and what it SENT (records). They are
// different questions and the pair is the point - item B did not stop the upload records
// from being sent, it stopped them from being waited for.
std::printf("records sent by op (whole session):\n");
for (Uint32 op = 1; op < Transport::LinkMetricsMaxOps; ++op) {
const Uint64 records = Transport::LinkMetricsRecordsFor(op);
if (records != 0)
std::printf(" op=%-24s records=%llu waits=%llu\n",
Codec::WireOpName(static_cast<MGPWireOp>(op)),
static_cast<unsigned long long>(records),
static_cast<unsigned long long>(Transport::LinkMetricsReplyWaitsFor(op)));
}
std::fflush(nullptr);
std::printf("shape 2 a texture per sprite: %d textures -> wait_replies=%llu\n", kSprites,
static_cast<unsigned long long>(Transport::LinkMetricsReplyWaits() - atlasWaitsBefore));
for (Uint32 op = 1; op < Transport::LinkMetricsMaxOps; ++op) {
const Uint64 waits = Transport::LinkMetricsReplyWaitsFor(op);
if (waits != 0)
std::printf(" op=%-24s waits=%llu\n", Codec::WireOpName(static_cast<MGPWireOp>(op)),
static_cast<unsigned long long>(waits));
}
std::fflush(nullptr);
// THE STRUCTURAL GATE. The upload row must be free (item B) and every other row this load
// drives must NOT be, or the case would also pass on a session that counted nothing.
if (Transport::LinkMetricsReplyWaitsFor(static_cast<Uint32>(MGPWireOp::ResourceSubData)) != 0)
::_exit(143);
// AND THE UPLOAD RECORDS WERE STILL SENT: the shape 1 stitch is 64 sub-uploads, so the row
// must show 64 records with 0 waits. A pair that went to 0/0 together would mean the
// emitter stopped sending, which is a different (and much worse) change than item B.
if (Transport::LinkMetricsRecordsFor(static_cast<Uint32>(MGPWireOp::ResourceSubData)) < 64)
::_exit(148);
if (Transport::LinkMetricsReplyWaitsFor(static_cast<Uint32>(MGPWireOp::ResourceCreate)) == 0)
::_exit(144);
if (Transport::LinkMetricsReplyWaitsFor(static_cast<Uint32>(MGPWireOp::ResourceRespecify)) == 0)
::_exit(145);
// AND SetTextureParams IS DELIBERATELY NOT ON THAT LIST ANY MORE (P12). It was the control
// that proved this counter was live; the first-use confirmation moved the row to
// fire-and-forget for a confirmed object, so its zero is the FIX rather than a dead counter
// - which is why create and respecify above carry that job now, and why the records line
// below still has to show the parameter records going out.
if (Transport::LinkMetricsRecordsFor(static_cast<Uint32>(MGPWireOp::SetTextureParams)) == 0)
::_exit(146);
Uint64 sum = 0;
for (Uint32 op = 0; op < Transport::LinkMetricsMaxOps; ++op)
sum += Transport::LinkMetricsReplyWaitsFor(op);
if (sum != Transport::LinkMetricsReplyWaits()) ::_exit(147);
ClientSessionInstance().Stop();
});
ExpectChildSuccess(child);
}
// =====================================================================================
// P12: the first-use confirmation - an object pays ONE round trip, at its first use
// =====================================================================================
//
// The rule the device run asked for: the round trip is owed by an object's CREATE (that is what
// says "the applier holds a record for this handle"), and every control record after it is
// fire-and-forget. The latch that decides is MGPipeHandleIsPublished, which the emitter sets only
// on an ACCEPTED create - so the row was asking the server a question the client had already
// answered, at ~5 ms per record (18,056 of one frame's 43,232 waits were this row).
//
// THE RED, RUN ONCE: put the unconditional EmitAndWait back into Wire_SetTextureParams and the
// last assertion below flips - the parameter records start taking replies again.
TEST(RemoteClientControls, AParameterChangeForAConfirmedObjectTakesNoReply) {
const auto child = RunInChild([] {
::setenv("MOBILEGL_PIPE_STATS", "1", 1);
CapsPeer backend;
StartControlSession(&backend);
MG_Config::Features.PipePush |= kMGPipeSubsystemResources |
kMGPipeSubsystemTextureResources | kMGPipeSubsystemSamplers;
MG_State::pGLContext = MakeUnique<MG_State::GLState::GLContext>();
const Uint32 paramsOp = static_cast<Uint32>(MGPWireOp::SetTextureParams);
const Uint32 createOp = static_cast<Uint32>(MGPWireOp::ResourceCreate);
GLuint texture = 0;
MG_Impl::GLImpl::GenTextures(1, &texture);
MG_Impl::GLImpl::BindTexture(0x0DE1, texture);
// FIRST USE, AND THE HARD SHAPE: parameters with no storage at all - the default-texture
// case the self-heal comment names. The confirmation is a create with no storage, it is
// BLOCKING (the one answer this object owes), and the parameters land with it.
MG_Impl::GLImpl::TexParameteri(0x0DE1, 0x2801, 0x2601); // MIN_FILTER = LINEAR
MGP_FILL(DrawArrays);
if (Transport::LinkMetricsRecordsFor(createOp) == 0) ::_exit(150);
if (Transport::LinkMetricsReplyWaitsFor(createOp) == 0) ::_exit(151);
// NOW THE OBJECT IS CONFIRMED. Eight real parameter changes: every one must still be SENT,
// and none of them may take a reply.
const Uint64 recordsBefore = Transport::LinkMetricsRecordsFor(paramsOp);
const Uint64 waitsBefore = Transport::LinkMetricsReplyWaitsFor(paramsOp);
for (int i = 0; i < 8; ++i) {
MG_Impl::GLImpl::TexParameteri(0x0DE1, 0x2801, (i % 2) != 0 ? 0x2601 : 0x2600);
MGP_FILL(DrawArrays);
}
if (Transport::LinkMetricsRecordsFor(paramsOp) != recordsBefore + 8) ::_exit(152);
if (Transport::LinkMetricsReplyWaitsFor(paramsOp) != waitsBefore) ::_exit(153);
ClientSessionInstance().Stop();
});
ExpectChildSuccess(child);
}
// =====================================================================================
// P12: a BUFFER respecify takes no reply; a TEXTURE one still does (deliberately)
// =====================================================================================
//
// The two halves of resource_respecify are not the same row. The buffer half's Bool is discarded
// at BOTH call sites (PipeFill.cpp:1105, :1142), so its wait bought a value that was thrown away -
// fire-and-forget costs nothing and saves a round trip per glBufferData, which is the shape a
// chunk uploader produces thousands of times per frame. The texture half's Bool is load-bearing:
// it triggers the self-heal AND gates the acceptance mirror, and advancing that mirror on a
// provisional accept is ID-18 M3 (TextureEmit.h:885-888). So this case asserts BOTH directions -
// the asymmetry is the design, not an oversight.
//
// THE RED, RUN ONCE: take the bufferHalf branch out of Wire_Escape_ResourceRespecify and exit 155
// fires (the buffer respecify starts waiting again).
TEST(RemoteClientControls, ABufferRespecifyTakesNoReplyWhileATextureOneStillDoes) {
const auto child = RunInChild([] {
::setenv("MOBILEGL_PIPE_STATS", "1", 1);
CapsPeer backend;
StartControlSession(&backend);
MG_Config::Features.PipePush |= kMGPipeSubsystemResources |
kMGPipeSubsystemTextureResources | kMGPipeSubsystemSamplers;
MG_State::pGLContext = MakeUnique<MG_State::GLState::GLContext>();
const Uint32 respecifyOp = static_cast<Uint32>(MGPWireOp::ResourceRespecify);
Uint8 seed[64];
std::memset(seed, 0x5a, sizeof(seed));
// ---- the BUFFER half: records out, replies not taken ----
GLuint buffer = 0;
MG_Impl::GLImpl::GenBuffers(1, &buffer);
MG_Impl::GLImpl::BindBuffer(0x8892, buffer);
MG_Impl::GLImpl::BufferData(0x8892, sizeof(seed), seed, 0x88E4);
MGP_FILL(DrawArrays);
const Uint64 recordsBefore = Transport::LinkMetricsRecordsFor(respecifyOp);
const Uint64 waitsBefore = Transport::LinkMetricsReplyWaitsFor(respecifyOp);
MG_Impl::GLImpl::BufferData(0x8892, sizeof(seed) * 2, seed, 0x88E4);
MGP_FILL(DrawArrays);
if (Transport::LinkMetricsRecordsFor(respecifyOp) <= recordsBefore) ::_exit(154);
if (Transport::LinkMetricsReplyWaitsFor(respecifyOp) != waitsBefore) ::_exit(155);
// ---- the TEXTURE half: the deliberate exception ----
Uint8 pixels[8 * 8 * 4];
std::memset(pixels, 0x21, sizeof(pixels));
GLuint texture = 0;
MG_Impl::GLImpl::GenTextures(1, &texture);
MG_Impl::GLImpl::BindTexture(0x0DE1, texture);
MG_Impl::GLImpl::TexImage2D(0x0DE1, 0, 0x8058, 8, 8, 0, 0x1908, 0x1401, pixels);
MGP_FILL(DrawArrays);
if (Transport::LinkMetricsReplyWaitsFor(respecifyOp) == waitsBefore) ::_exit(156);
ClientSessionInstance().Stop();
});
ExpectChildSuccess(child);
}
#endif