mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
lavapipe/llvmpipe stall the apply thread for tens of seconds inside texture-handle
registration (vkCreateImageView -> lvp_CreateImageView -> llvmpipe_register_texture,
a JIT/futex path): one measured stall is 29.3-39.7 s and a whole inproc retrace run
accumulates ~44.6 s of it. At a 30 s bound those runs red intermittently on
Fatal{BarrierTimeout, "ResourceRespecify"} while the same stall under monolith is
merely slow - monolith has no cap on that path. Since the monolith shares the stall,
the bound, not the workload, was the defect.
kBarrierTimeoutMs becomes 120000, >3x the largest observed stall, which still leaves
the watchdog job: a record that is never retired still terminates the run and the
Fatal still names the seq. The comment now says deadlock detector rather than
performance gate and carries the measurement.
The fake-clock case that pins the budget moves with it: FenceWaitBudgetHonorsGl-
TimeoutAndFiniteTransportChunks expected 30000/30001/90000 and now expects
120000/120001/180000, so the assertion still pins the value instead of drifting.
Texts that wrote the old number down are updated: ARCHITECTURE 17.1, CONTRACT-P5E
2.4 (which now names kBarrierTimeoutMs rather than the literal), and the two
bring-up comments in PipeRoute.h / WireTables.cpp that dated a self-wait to
"thirty seconds".
733 lines
33 KiB
C++
733 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) {
|
|
// 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) {
|
|
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) {
|
|
// 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;
|
|
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));
|
|
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()->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
|