mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-18 00:58:30 +09:00
1941 lines
105 KiB
C++
1941 lines
105 KiB
C++
// MobileGL - MobileGL/MG_Test/Wire/ServerLoopTest.cpp
|
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
|
// Licensed under the GNU Lesser General Public License v3.0:
|
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
|
// SPDX-License-Identifier: LGPL-3.0-only
|
|
// End of Source File Header
|
|
|
|
// Package v1's suite: the apply thread, the blocking control mailbox, the verb stamp, the
|
|
// bounded shutdown, and R-11's server-owned staging copy.
|
|
//
|
|
// WHAT THIS SUITE REFUSES TO DO, and why it is written the way it is (R-16).
|
|
//
|
|
// Every case here drives the REAL path: a real ServerSession over four real ShmSegment-backed
|
|
// segments, a real apply thread parked on a real Doorbell, and records that go in through w1's
|
|
// encoder and come out through w1's decoder. None of them writes a record field by hand, none
|
|
// of them calls PipeApplier::ApplyOne directly, and none of them sets a flag the case then
|
|
// observes. That matters because the three defects the first P5 wave shipped were all of that
|
|
// shape - a persistent-map case that wrote the record field itself still passed with the
|
|
// producer deleted, and a lane probe armed against stub SOURCE TEXT went green having run
|
|
// monolith on 8 of 11 lanes.
|
|
//
|
|
// The two things this suite deliberately CANNOT reach are named rather than faked:
|
|
// * there is no GL context here, so ServerLoop::CreateBackend is not called and the five
|
|
// class-B verbs DECLINE. That is asserted as a decline, not skipped - a Clear that was
|
|
// APPLIED with no backend would mean the sink found a table it should not have.
|
|
// * R-11's end-to-end control is MOBILEGL_IPC_AUDIT=1 over the reduced path, which needs the
|
|
// client's emit table (package c1). What IS reachable here is the property that control
|
|
// exists to test: bytes that were copied survive the source being overwritten with 0xDD.
|
|
|
|
#include <Config.h>
|
|
#include <MG_Backend/BackendObject.h>
|
|
#include <MG_Backend/BackendObjects.h>
|
|
#include <MG_Backend/DirectGLES/BackendObject_DirectGLES.h>
|
|
#include <MG_Backend/DirectGLES/DirectGLES.h>
|
|
#include <MG_Backend/DirectGLES/Managers.h>
|
|
#include <MG_Backend/DirectGLES/Utils.h>
|
|
#include <MG_Backend/MGPipe/PipeInputs.h>
|
|
#include <MG_Impl/Pipe/PipeFill.h>
|
|
#include <MG_Pipe/MGPipe.h>
|
|
#include <MG_Pipe/PipeApply.h>
|
|
#include <MG_Remote/CapsCodec.h>
|
|
#include <MG_Remote/Client/ClientSession.h>
|
|
#include <MG_Remote/Protocol/generated/protocol_generated.h>
|
|
#include <MG_Remote/Server/PipeApplier.h>
|
|
#include <MG_Remote/Server/ServerLoop.h>
|
|
#include <MG_Remote/Server/ServerSession.h>
|
|
#include <MG_Remote/Server/StagedShadow.h>
|
|
#include <MG_Remote/Transport/InProcessTransport.h>
|
|
#include <MG_Remote/Transport/ReplySlot.h>
|
|
#include <MG_Remote/Transport/Ring.h>
|
|
#include <MG_Remote/Transport/SessionRings.h>
|
|
#include <MG_Remote/Wire/PipeWireCodec.h>
|
|
#include <MG_State/GLState/BufferState/BufferObject.h>
|
|
#include <MG_State/GLState/Core.h>
|
|
#include <MG_State/GLState/TextureState/TextureEnum.h>
|
|
#include <MGGitHash.h>
|
|
|
|
#include <csignal>
|
|
|
|
#include <gtest/gtest.h>
|
|
|
|
#include <atomic>
|
|
#include <chrono>
|
|
#include <cstring>
|
|
#include <cstdio>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <functional>
|
|
#include <memory>
|
|
#include <string>
|
|
#include <thread>
|
|
#include <vector>
|
|
|
|
#if defined(_WIN32)
|
|
#include <process.h>
|
|
#else
|
|
#include <unistd.h>
|
|
#endif
|
|
#if defined(__linux__) || defined(__ANDROID__)
|
|
#include <sched.h>
|
|
#endif
|
|
|
|
using namespace MobileGL;
|
|
|
|
// NOT `using namespace MobileGL::MG_Remote`. MobileGL::Wire (the flatbuffers control-plane
|
|
// schema) and MobileGL::MG_Remote::Wire (the G3 codec) are two different namespaces with the
|
|
// same last name, and a using-directive over the second makes every mention of `Wire`
|
|
// ambiguous - including the ones in the generated header.
|
|
namespace Transport = MobileGL::MG_Remote::Transport;
|
|
namespace Server = MobileGL::MG_Remote::Server;
|
|
namespace Codec = MobileGL::MG_Remote::Wire;
|
|
using MobileGL::MG_Remote::CapsAbiFingerprint;
|
|
|
|
namespace {
|
|
|
|
std::string g_logPath;
|
|
|
|
std::string ReadLog() {
|
|
std::ifstream in(g_logPath, std::ios::binary);
|
|
if (!in) return {};
|
|
return std::string(std::istreambuf_iterator<char>(in), std::istreambuf_iterator<char>());
|
|
}
|
|
|
|
unsigned ProcessId() {
|
|
#if defined(_WIN32)
|
|
return static_cast<unsigned>(_getpid());
|
|
#else
|
|
return static_cast<unsigned>(::getpid());
|
|
#endif
|
|
}
|
|
|
|
Transport::SessionSegmentSizes TestSizes() {
|
|
Transport::SessionSegmentSizes sizes;
|
|
sizes.CmdRingBytes = 64ull * 1024;
|
|
sizes.StageBytes = 64ull * 1024;
|
|
sizes.ReplyBytes = 64ull * 1024;
|
|
// 1 MiB, not 16 KiB: the EGL cases have the server republish its caps snapshot into the
|
|
// event ring several times with no client draining it, and a ring that fills would turn a
|
|
// "no republish" assertion into a ring-full one.
|
|
sizes.EventRingBytes = 1024ull * 1024;
|
|
sizes.ReplySlotCount = 8;
|
|
return sizes;
|
|
}
|
|
|
|
// The client half of a session, wired exactly the way ClientSession::Start wires it: the
|
|
// transport pair, a real Hello, the server's Accept, the client's attach to the SAME
|
|
// mapping, and w1's encoder over the shared SEG_CMD ring. It is NOT ClientSession itself,
|
|
// because ClientSession::Start finishes by installing package c1's BackendObject_Remote,
|
|
// which does not exist yet - and a fixture that skipped the handshake to get around that
|
|
// would be testing a session this tree does not build.
|
|
struct ServerFixture {
|
|
std::unique_ptr<Transport::InProcessTransport> clientTransport;
|
|
std::unique_ptr<Transport::InProcessTransport> serverTransport;
|
|
Transport::SessionSegments clientSegments;
|
|
Transport::RingProducer cmd;
|
|
Transport::SessionProducer producer;
|
|
Codec::SegmentTable clientTable;
|
|
Codec::PipeWireEncoder encoder;
|
|
Server::ServerSession* session = nullptr;
|
|
|
|
bool Handshake() {
|
|
Transport::InProcessTransport::CreatePair(clientTransport, serverTransport);
|
|
{
|
|
::flatbuffers::FlatBufferBuilder builder(512);
|
|
auto stamp = builder.CreateString(GIT_COMMIT_HASH_SHORT);
|
|
auto hello = ::MobileGL::Wire::CreateHello(
|
|
builder, MOBILEGL_PROTOCOL_ABI_MAJOR, MOBILEGL_PROTOCOL_ABI_MINOR, stamp,
|
|
/*backendType=*/0u, /*pid=*/0u, /*configBlob=*/0, CapsAbiFingerprint());
|
|
auto root = ::MobileGL::Wire::CreateCtrlEnvelope(
|
|
builder, ::MobileGL::Wire::CtrlMsg::Hello, hello.Union());
|
|
::MobileGL::Wire::FinishCtrlEnvelopeBuffer(builder, root);
|
|
if (clientTransport->SendFrame(MobileGLByteSpan{builder.GetBufferPointer(),
|
|
builder.GetSize()}) != MOBILEGL_OK) {
|
|
return false;
|
|
}
|
|
}
|
|
session = &Server::ServerSessionInstance();
|
|
session->SetSegmentSizes(TestSizes());
|
|
// The two halves v1 owns. Neither has a default and CallMask() Fatals on an unset
|
|
// one, which is s1's BLOCKER fix and the reason this is stated rather than derived.
|
|
session->SetCapabilityBits(0);
|
|
session->SetConsumedSubsystems(MG_Pipe::kMGPipeSubsystemsMigratedAtP4a);
|
|
if (session->Accept(*serverTransport) != MOBILEGL_OK) return false;
|
|
if (clientSegments.AttachInProcess(session->Shm(), Transport::MemoryRole::Client) !=
|
|
MOBILEGL_OK) {
|
|
return false;
|
|
}
|
|
Transport::RingControl* control = clientSegments.CmdControl();
|
|
cmd = Transport::RingProducer(control, clientSegments.CmdRingBase(),
|
|
clientSegments.CmdRingCapacity(), Transport::RingCursorSet::Cmd);
|
|
if (!cmd.Valid()) return false;
|
|
producer.Attach(control, &cmd, &clientTransport->PeerDoorbell(),
|
|
&clientTransport->SelfDoorbell(), MG_Config::Ipc.SpinUs);
|
|
clientTable.Install(Codec::kSegCmd, Codec::SegmentView{clientSegments.CmdRingBase(),
|
|
clientSegments.CmdRingCapacity()});
|
|
clientTable.Install(Codec::kSegStage,
|
|
Codec::SegmentView{clientSegments.StageBase(), clientSegments.StageBytes()});
|
|
clientTable.Install(Codec::kSegReply,
|
|
Codec::SegmentView{clientSegments.ReplyBase(), clientSegments.ReplyBytes()});
|
|
encoder = Codec::PipeWireEncoder(control, &cmd, nullptr, &clientTable);
|
|
return true;
|
|
}
|
|
|
|
bool StartLoop() {
|
|
return Server::ServerLoopInstance().Start(*session) == MOBILEGL_OK;
|
|
}
|
|
|
|
// Emit one record and wait for the server to apply it. This is the verb barrier's own
|
|
// wait (R-3/R-5) - appliedSeq, not a sleep - so a case that goes green here has really
|
|
// seen the apply thread move the watermark.
|
|
bool EmitAndWait(MG_Pipe::MGPWireOp op, const void* payload, Uint64 payloadBytes,
|
|
Uint32 timeoutMs = 5000) {
|
|
const Uint64 seq = encoder.EncodeRecord(op, payload, payloadBytes);
|
|
if (seq == Codec::kInvalidSeq) return false;
|
|
encoder.Publish();
|
|
producer.PublishAndNotify(seq);
|
|
return producer.WaitForApplied(seq, timeoutMs) == Transport::SessionWait::Reached;
|
|
}
|
|
|
|
bool EmitAndWaitWithTail(MG_Pipe::MGPWireOp op, const void* payload, Uint64 payloadBytes,
|
|
const void* tail, Uint64 tailBytes, Uint32 timeoutMs = 5000) {
|
|
const Uint64 seq = encoder.EncodeRecord(op, payload, payloadBytes, tail, tailBytes);
|
|
if (seq == Codec::kInvalidSeq) return false;
|
|
encoder.Publish();
|
|
producer.PublishAndNotify(seq);
|
|
return producer.WaitForApplied(seq, timeoutMs) == Transport::SessionWait::Reached;
|
|
}
|
|
|
|
// C10: poll the flag the Doorbell sets ONLY while it is actually blocked in Park
|
|
// (RingControl::consumerParked, stored inside Doorbell::Wait's blocking section and
|
|
// cleared on wake), NOT ServerLoop::ParkCount(), which increments on the way TOWARD a park
|
|
// and stays set even if Wait returns without ever blocking. A loop whose wait was replaced
|
|
// by `true` never sets this, so a poll for it TIMES OUT - which is what makes "the apply
|
|
// thread really parked" a claim that can go red for its own reason.
|
|
bool WaitUntilTrulyParked(Uint32 timeoutMs = 5000) {
|
|
Transport::RingControl* control = clientSegments.CmdControl();
|
|
if (control == nullptr) return false;
|
|
const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(timeoutMs);
|
|
while (std::chrono::steady_clock::now() < deadline) {
|
|
if (control->consumerParked.load(std::memory_order_acquire) == 1u) return true;
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void Stop() {
|
|
// Table 3's order: the client publishes and lets the server drain (EmitAndWait
|
|
// already did), then Doorbell::Kill through the transport's Shutdown, THEN the
|
|
// bounded join, and only then is anything an emitter owns released.
|
|
if (clientTransport) clientTransport->Shutdown();
|
|
Server::ServerLoopInstance().Stop();
|
|
producer.Detach();
|
|
encoder = Codec::PipeWireEncoder();
|
|
cmd = Transport::RingProducer();
|
|
clientSegments.Close();
|
|
if (session != nullptr) session->Close();
|
|
clientTransport.reset();
|
|
serverTransport.reset();
|
|
}
|
|
};
|
|
|
|
MG_Pipe::MGPClear WholeFramebufferClear() {
|
|
MG_Pipe::MGPClear clear{};
|
|
clear.Kind = Server::kMGPClearKindWhole;
|
|
clear.BufferMask = 0x00004000; // GL_COLOR_BUFFER_BIT
|
|
clear.DrawBufferIndex = -1;
|
|
clear.ValueClass = Server::kMGPClearValueClassFloat;
|
|
return clear;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
// =====================================================================================
|
|
// The thread
|
|
// =====================================================================================
|
|
|
|
// A control request must be able to un-park a thread waiting on kWaitForever. A Notify alone
|
|
// cannot do that - Doorbell::Wait consumes it with one Park, re-tests a condition nothing
|
|
// published, finds the bell alive and parks again - so the park predicate has to carry the
|
|
// control flag too. The case asserts the thread REALLY PARKED first, because a loop that
|
|
// spun instead would pass this without the predicate ever mattering.
|
|
TEST(ServerLoopTest, AControlRequestRunsOnTheApplyThreadAndUnparksIt) {
|
|
ServerFixture fixture;
|
|
ASSERT_TRUE(fixture.Handshake());
|
|
ASSERT_TRUE(fixture.StartLoop());
|
|
|
|
Server::ServerLoop& loop = Server::ServerLoopInstance();
|
|
// C10: arm on the ACTUAL blocked park (consumerParked), not ParkCount() - a loop that spun
|
|
// instead of blocking would pass a ParkCount() check without the wait ever mattering, which
|
|
// was the whole finding.
|
|
ASSERT_TRUE(fixture.WaitUntilTrulyParked())
|
|
<< "the apply thread never entered the blocking park, so this case would prove nothing "
|
|
"about waking it (ParkCount() counts an intention, not a park)";
|
|
|
|
struct Probe {
|
|
std::thread::id ranOn{};
|
|
Bool onApplyThread = false;
|
|
} probe;
|
|
// POSTED FROM A HELPER THREAD AND WAITED FOR WITH A DEADLINE (review v2 N-9). A park predicate
|
|
// that lost the control flag never un-parks the apply thread and the post blocks for ever; the
|
|
// case has to SAY that, in its own words, rather than hit ctest's timeout - a timeout is what an
|
|
// unrelated hang produces too. Stop() afterwards is what frees the helper: m_stopRequested is in
|
|
// the predicate, the thread exits, and its exit block answers the still-posted request
|
|
// NOT_INITIALIZED (C2's block), so the join below cannot wedge either.
|
|
MobileGLResult rc = MOBILEGL_ERR_INVALID_ARGUMENT;
|
|
std::atomic<Bool> answered{false};
|
|
std::thread::id posterId{};
|
|
std::thread poster([&] {
|
|
posterId = std::this_thread::get_id();
|
|
rc = loop.RunOnApplyThread(
|
|
+[](void* user) -> MobileGLResult {
|
|
auto* p = static_cast<Probe*>(user);
|
|
p->ranOn = std::this_thread::get_id();
|
|
p->onApplyThread = Server::ServerLoop::OnApplyThread();
|
|
return MOBILEGL_OK;
|
|
},
|
|
&probe);
|
|
answered.store(true, std::memory_order_release);
|
|
});
|
|
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5);
|
|
while (!answered.load(std::memory_order_acquire) && std::chrono::steady_clock::now() < deadline) {
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
|
}
|
|
if (!answered.load(std::memory_order_acquire)) {
|
|
ADD_FAILURE() << "a posted control request did not un-park the apply thread within 5 s: the park "
|
|
"predicate no longer carries the control flag, so a Notify alone cannot break a "
|
|
"kWaitForever park (Doorbell::Wait consumes it, re-tests a condition nothing "
|
|
"published, and parks again)";
|
|
fixture.Stop();
|
|
poster.join();
|
|
return;
|
|
}
|
|
poster.join();
|
|
|
|
EXPECT_EQ(rc, MOBILEGL_OK);
|
|
// The CALLER is the poster thread, not the test thread (N-9 moved the post onto a helper).
|
|
EXPECT_NE(probe.ranOn, posterId)
|
|
<< "the control request ran on the CALLER, which means the EGL lifecycle calls would "
|
|
"reach the driver from the app thread and the context would never migrate";
|
|
EXPECT_NE(probe.ranOn, std::this_thread::get_id());
|
|
EXPECT_TRUE(probe.onApplyThread)
|
|
<< "the control request ran on the CALLER (OnApplyThread() answered false inside it)";
|
|
EXPECT_FALSE(Server::ServerLoop::OnApplyThread());
|
|
|
|
fixture.Stop();
|
|
}
|
|
|
|
// Re-entrancy is not a deadlock: ~BackendObject_DirectGLES reaches ReleaseEGLResources FROM the
|
|
// apply thread, so a post from there must run inline.
|
|
TEST(ServerLoopTest, APostFromTheApplyThreadItselfRunsInlineRatherThanDeadlocking) {
|
|
ServerFixture fixture;
|
|
ASSERT_TRUE(fixture.Handshake());
|
|
ASSERT_TRUE(fixture.StartLoop());
|
|
|
|
struct Outer {
|
|
Bool innerRan = false;
|
|
MobileGLResult innerRc = MOBILEGL_ERR_INVALID_ARGUMENT;
|
|
} outer;
|
|
const MobileGLResult rc = Server::ServerLoopInstance().RunOnApplyThread(
|
|
+[](void* user) -> MobileGLResult {
|
|
auto* o = static_cast<Outer*>(user);
|
|
o->innerRc = Server::ServerLoopInstance().RunOnApplyThread(
|
|
+[](void* inner) -> MobileGLResult {
|
|
*static_cast<Bool*>(inner) = true;
|
|
return MOBILEGL_OK;
|
|
},
|
|
&o->innerRan);
|
|
return MOBILEGL_OK;
|
|
},
|
|
&outer);
|
|
|
|
EXPECT_EQ(rc, MOBILEGL_OK);
|
|
EXPECT_EQ(outer.innerRc, MOBILEGL_OK);
|
|
EXPECT_TRUE(outer.innerRan);
|
|
|
|
fixture.Stop();
|
|
}
|
|
|
|
// The bounded join. The thread is parked on kWaitForever when Stop() is called, so this is the
|
|
// exact lost-wakeup shape table 3 step 2 is about - and the bound is what turns a regression
|
|
// into a red test rather than a wedged CI job.
|
|
TEST(ServerLoopTest, StopKillsTheDoorbellJoinsAndTheThreadReallyExits) {
|
|
ServerFixture fixture;
|
|
ASSERT_TRUE(fixture.Handshake());
|
|
ASSERT_TRUE(fixture.StartLoop());
|
|
Server::ServerLoop& loop = Server::ServerLoopInstance();
|
|
|
|
// C10: the same actual-park arming - a shutdown test whose thread never blocked would not be
|
|
// exercising the lost-wakeup path table 3 step 2 is about.
|
|
ASSERT_TRUE(fixture.WaitUntilTrulyParked())
|
|
<< "the thread must be BLOCKED in the park for this to be a shutdown test";
|
|
ASSERT_TRUE(loop.Running());
|
|
|
|
const auto began = std::chrono::steady_clock::now();
|
|
fixture.Stop();
|
|
const auto took = std::chrono::steady_clock::now() - began;
|
|
|
|
EXPECT_FALSE(loop.Running());
|
|
EXPECT_LT(std::chrono::duration_cast<std::chrono::milliseconds>(took).count(), 5000)
|
|
<< "Stop() took the whole bound, which means it hit the join timeout rather than a "
|
|
"wakeup";
|
|
}
|
|
|
|
// MOBILEGL_IPC_SERVER_AFFINITY is kept as the raw string BECAUSE the resolved mask is what gets
|
|
// logged: an affinity that silently did nothing looks exactly like one that worked. So the
|
|
// resolved mask has to be readable, and `off` has to resolve to zero rather than to "auto".
|
|
TEST(ServerLoopTest, TheAffinityStringResolvesToAMaskAndTheMaskIsLogged) {
|
|
const String saved = MG_Config::Ipc.ServerAffinity;
|
|
MG_Config::Ipc.ServerAffinity = "off";
|
|
|
|
ServerFixture fixture;
|
|
ASSERT_TRUE(fixture.Handshake());
|
|
ASSERT_TRUE(fixture.StartLoop());
|
|
ASSERT_TRUE(fixture.WaitUntilTrulyParked());
|
|
EXPECT_EQ(Server::ServerLoopInstance().ResolvedAffinityMask(), 0u)
|
|
<< "`off` did not resolve to no-affinity";
|
|
|
|
const std::string log = ReadLog();
|
|
EXPECT_NE(log.find("RESOLVED mask"), std::string::npos)
|
|
<< "the apply thread did not log its resolved affinity mask; an affinity that silently "
|
|
"did nothing is indistinguishable from one that worked";
|
|
EXPECT_NE(log.find("mgl-srv-apply started"), std::string::npos);
|
|
// M-4: `off` must be a RECOGNISED string, not fall through to the numeric-parse "not
|
|
// recognised" arm. Deleting the `off` branch in RequestedAffinityMask makes off unrecognised;
|
|
// this asserts it did not, so that branch's deletion goes red here (its own reason).
|
|
EXPECT_EQ(log.find("is not `auto`, `off` or a number"), std::string::npos)
|
|
<< "`off` was treated as an unrecognised affinity string; its own branch is gone";
|
|
|
|
fixture.Stop();
|
|
MG_Config::Ipc.ServerAffinity = saved;
|
|
}
|
|
|
|
// M-4, the explicit-mask half: an EXPLICIT mask the box can honour must resolve to itself and be
|
|
// LOGGED. `off` -> 0 and `0x3` -> 0x3 are two answers that must DIFFER, so the resolver cannot be a
|
|
// constant. This case does NOT gate codex 11's effective-mask read-back - on an unrestricted box the
|
|
// request and the effective set are the same and `return requested` stays green here (review v2
|
|
// item 6, which struck the claim the first version of this comment made); the read-back's own gate
|
|
// is TheResolvedAffinityMaskIsTheKernelsEffectiveSetNotTheRequest below. Red once by making the
|
|
// resolver return a constant 0: the explicit-mask assert reads 0.
|
|
TEST(ServerLoopTest, TheExplicitAffinityMaskResolvesToItselfAndIsLogged) {
|
|
#if defined(__linux__) || defined(__ANDROID__)
|
|
if (std::thread::hardware_concurrency() < 2) {
|
|
GTEST_SKIP() << "needs at least 2 online cpus to honour 0x3";
|
|
}
|
|
const String saved = MG_Config::Ipc.ServerAffinity;
|
|
MG_Config::Ipc.ServerAffinity = "0x3"; // cpu 0 and cpu 1, both online on any 2+-cpu box
|
|
|
|
ServerFixture fixture;
|
|
ASSERT_TRUE(fixture.Handshake());
|
|
ASSERT_TRUE(fixture.StartLoop());
|
|
ASSERT_TRUE(fixture.WaitUntilTrulyParked());
|
|
|
|
EXPECT_EQ(Server::ServerLoopInstance().ResolvedAffinityMask(), 0x3u)
|
|
<< "an explicit mask the box can honour did not resolve to itself (0x3); the effective "
|
|
"mask read back from the kernel differs from the request, or the resolver is broken";
|
|
const std::string log = ReadLog();
|
|
EXPECT_NE(log.find("RESOLVED mask 0x3"), std::string::npos)
|
|
<< "the logged RESOLVED mask is not the effective 0x3";
|
|
|
|
fixture.Stop();
|
|
MG_Config::Ipc.ServerAffinity = saved;
|
|
#else
|
|
GTEST_SKIP() << "affinity is a Linux/Android facility";
|
|
#endif
|
|
}
|
|
|
|
// =====================================================================================
|
|
// The record path: stamp, apply, watermark
|
|
// =====================================================================================
|
|
|
|
// The whole phase's prerequisite. MGPipeApplyAccess deliberately does not stamp the poison
|
|
// generations, so under split NOTHING stamps unless the applier does - every FilledGen[] stays
|
|
// 0, MGPipeInputFieldIsFresh answers false for everything, and a server-side read aborts on the
|
|
// FIRST field inside SyncRenderState. This case asserts the stamp happened by reading the verb
|
|
// the stamp SET, which the case itself never writes.
|
|
TEST(ServerLoopTest, AClearRecordCrossesAndIsStampedAsAVerbBoundary) {
|
|
ServerFixture fixture;
|
|
ASSERT_TRUE(fixture.Handshake());
|
|
ASSERT_TRUE(fixture.StartLoop());
|
|
|
|
// The pre-state is the honest one: nothing has stamped yet in this process.
|
|
ASSERT_NE(MG_Pipe::gPipeInputs.CurrentVerb(), MG_Pipe::MGPipeVerb::Clear);
|
|
|
|
const MG_Pipe::MGPClear clear = WholeFramebufferClear();
|
|
ASSERT_TRUE(fixture.EmitAndWait(MG_Pipe::MGPWireOp::Clear, &clear, sizeof(clear)));
|
|
|
|
Server::ServerLoop& loop = Server::ServerLoopInstance();
|
|
EXPECT_EQ(loop.DrainedRecords(), 1u);
|
|
EXPECT_EQ(MG_Pipe::gPipeInputs.CurrentVerb(), MG_Pipe::MGPipeVerb::Clear)
|
|
<< "PipeApplier::StampVerbBoundary did not run, so every server-side PipeInputs read "
|
|
"would abort on the first field inside SyncRenderState";
|
|
|
|
// MANDATORY on leaving the applier (p1's M-5): without it a SPAWNED server latches the flag
|
|
// for its whole life, every later read is judged against the last verb's mask, and the
|
|
// sticky forwards start aborting under strict on the very case their exemption exists for.
|
|
EXPECT_FALSE(MG_Pipe::gPipeInputs.ServerStampedVerb())
|
|
<< "MGPipeServerClearVerbBoundary was not called when the apply thread left the applier";
|
|
|
|
// No backend object in this process, so the five class-B verbs DECLINE. Asserted rather
|
|
// than skipped: a Clear that was APPLIED here would mean the sink found a function table
|
|
// it had no business finding.
|
|
EXPECT_EQ(fixture.session->Applier().Verbs().Clears(), 0u);
|
|
|
|
fixture.Stop();
|
|
}
|
|
|
|
// R-9's batching ban, made checkable rather than merely stated. RingControl::appliedSeq has ONE
|
|
// writer (SessionConsumer::ApplyOne, +1 per record) and PipeWireDecoder keeps its own tally; a
|
|
// batched publish moves one and not the other, which a single counter could never tell apart.
|
|
TEST(ServerLoopTest, TheSessionWatermarkAndTheDecoderTallyAgreeAfterEveryRecord) {
|
|
ServerFixture fixture;
|
|
ASSERT_TRUE(fixture.Handshake());
|
|
ASSERT_TRUE(fixture.StartLoop());
|
|
|
|
const MG_Pipe::MGPClear clear = WholeFramebufferClear();
|
|
for (int i = 0; i < 8; ++i) {
|
|
ASSERT_TRUE(fixture.EmitAndWait(MG_Pipe::MGPWireOp::Clear, &clear, sizeof(clear))) << i;
|
|
EXPECT_EQ(fixture.session->Consumer().AppliedSeq(), static_cast<Uint64>(i + 1));
|
|
EXPECT_EQ(fixture.session->Applier().DecoderAppliedSeq(), static_cast<Uint64>(i + 1));
|
|
}
|
|
EXPECT_EQ(Server::ServerLoopInstance().DrainedRecords(), 8u)
|
|
<< "the loop's own record tally (DrainedRecords) did not move with the eight records it "
|
|
"applied, so the ordering-defect control has nothing of its own to hold against appliedSeq";
|
|
|
|
// retiredSeq is the watermark w1's SEG_STAGE allocator reclaims against; a loop that
|
|
// applies and never retires ends the first MOBILEGL_IPC_STAGE_MB in Fatal{RingOverrun}.
|
|
//
|
|
// IT IS POLLED, NOT READ ONCE, and that is R-9's own rule rather than a papered-over race:
|
|
// appliedSeq is the ONLY watermark P5 forbids batching, and every other one "may be
|
|
// published LATE but never EARLY". The apply thread retires after the drain batch and
|
|
// before it parks, so the barrier can release the client between the two - reading it
|
|
// instantly would be asserting a promise R-9 deliberately does not make. The BOUND is what
|
|
// keeps this a check: a loop that never retires times out here instead of passing.
|
|
const auto retireDeadline = std::chrono::steady_clock::now() + std::chrono::seconds(2);
|
|
while (fixture.clientSegments.CmdControl()->retiredSeq.load() < 8u &&
|
|
std::chrono::steady_clock::now() < retireDeadline) {
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
|
}
|
|
EXPECT_GE(fixture.clientSegments.CmdControl()->retiredSeq.load(), 8u)
|
|
<< "the apply loop advanced appliedSeq but never retired within 2 s, so SEG_STAGE would "
|
|
"never be reclaimed and the first MOBILEGL_IPC_STAGE_MB would end in "
|
|
"Fatal{RingOverrun}";
|
|
|
|
fixture.Stop();
|
|
}
|
|
|
|
// =====================================================================================
|
|
// R-2.5 / rule C's mechanical control, running for real
|
|
// =====================================================================================
|
|
|
|
// MOBILEGL_IPC_AUDIT=1 fills a retired SEG_STAGE run with 0xDD once the applier has RETURNED,
|
|
// so an applier that kept the pointer reads the poison on the next frame instead of bytes that
|
|
// happen to still be there. Without this, an inproc implementation that kept a pointer is
|
|
// INDISTINGUISHABLE from one that copied - which is R-2's entire argument.
|
|
//
|
|
// The case asserts three separate things, because each one alone can be true for the wrong
|
|
// reason: that the record's bytes really crossed (memcmp before the poison), that the poison
|
|
// really ran (PoisonedStageBytes moved), and that it landed on the run the record named (the
|
|
// bytes read 0xDD afterwards, through the CLIENT's mapping of the same segment).
|
|
TEST(ServerLoopTest, TheAuditPoisonFillsExactlyTheStagedRunAfterTheApplierReturns) {
|
|
const Bool savedAudit = MG_Config::Ipc.Audit;
|
|
// Set BEFORE the loop starts: PipeWireDecoder reads it in its constructor, and its
|
|
// constructor runs on the apply thread inside ServerLoop::Start.
|
|
MG_Config::Ipc.Audit = true;
|
|
|
|
ServerFixture fixture;
|
|
ASSERT_TRUE(fixture.Handshake());
|
|
ASSERT_TRUE(fixture.StartLoop());
|
|
|
|
MG_Pipe::MGPResourceDesc create{};
|
|
create.Resource.Slot = 61;
|
|
create.Resource.Gen = 1;
|
|
create.Target = static_cast<Uint8>(MG_Pipe::MGPipeResourceTarget::Tex2D);
|
|
create.InternalFormat = 1;
|
|
create.Width = 4;
|
|
create.Height = 4;
|
|
create.Depth = 1;
|
|
create.ArrayLayers = 1;
|
|
create.Levels = 1;
|
|
create.Samples = 1;
|
|
ASSERT_TRUE(fixture.EmitAndWait(MG_Pipe::MGPWireOp::ResourceCreate, &create, sizeof(create)));
|
|
|
|
Vector<Uint8> texels(4 * 4 * 4, 0x5A);
|
|
MG_Pipe::MGPSubData upload{};
|
|
upload.Res = create.Resource;
|
|
upload.Target = MG_Pipe::MGPipePackSubDataTarget(
|
|
static_cast<Uint32>(MG_Pipe::MGPipeResourceTarget::Tex2D), 0u);
|
|
upload.Level = 0;
|
|
upload.UnionBox = MG_Pipe::MGPBox{0, 0, 0, 4, 4, 1};
|
|
upload.RegionCount = 1;
|
|
upload.Blob = fixture.encoder.StageBytes(texels.data(), texels.size());
|
|
ASSERT_NE(upload.Blob.Size, 0u) << "rule A: a content record must declare non-zero bytes";
|
|
|
|
// The bytes are in SEG_STAGE and readable through the CLIENT's own mapping right now - the
|
|
// record has not been published yet, so nothing has retired it.
|
|
const void* stagedBefore =
|
|
fixture.clientTable.Resolve(upload.Blob.Seg, upload.Blob.Offset, upload.Blob.Size);
|
|
ASSERT_NE(stagedBefore, nullptr);
|
|
ASSERT_EQ(std::memcmp(stagedBefore, texels.data(), texels.size()), 0);
|
|
|
|
MG_Pipe::MGPSubRegion region{};
|
|
region.W = 4;
|
|
region.H = 4;
|
|
region.D = 1;
|
|
ASSERT_TRUE(fixture.EmitAndWaitWithTail(MG_Pipe::MGPWireOp::ResourceSubData, &upload,
|
|
sizeof(upload), ®ion, sizeof(region)));
|
|
|
|
EXPECT_GT(fixture.session->Applier().PoisonedStageBytes(), 0u)
|
|
<< "MOBILEGL_IPC_AUDIT=1 poisoned nothing, so R-2.5's only mechanical control against a "
|
|
"retained SEG_STAGE pointer never ran";
|
|
|
|
const auto* poisoned = static_cast<const Uint8*>(
|
|
fixture.clientTable.Resolve(upload.Blob.Seg, upload.Blob.Offset, upload.Blob.Size));
|
|
ASSERT_NE(poisoned, nullptr);
|
|
for (Uint64 i = 0; i < upload.Blob.Size; ++i) {
|
|
ASSERT_EQ(poisoned[i], 0xDD) << "staged byte " << i << " was not poisoned; an applier "
|
|
"that kept this pointer would still read real bytes and "
|
|
"the control would prove nothing";
|
|
}
|
|
|
|
fixture.Stop();
|
|
MG_Config::Ipc.Audit = savedAudit;
|
|
}
|
|
|
|
// =====================================================================================
|
|
// R-11 - the server's own copy of the staged bytes
|
|
// =====================================================================================
|
|
|
|
// THIS IS THE PROPERTY MOBILEGL_IPC_AUDIT=1's 0xDD FILL EXISTS TO TEST, at unit scope: after
|
|
// the source bytes are overwritten - which is exactly what the decoder does to a retired
|
|
// SEG_STAGE run - the server's copy still reads the original. An implementation that returned
|
|
// `raw - offset` cannot pass this, and the monolith arm below is the control that says so: it
|
|
// is the SAME call with the same inputs, and it must see the 0xDD.
|
|
TEST(StagedShadowTest, TheSplitArmCopiesAndSurvivesTheSourceBeingPoisoned) {
|
|
Server::StagedShadowStore splitStore(/*copies=*/true);
|
|
Server::StagedShadowStore monolithStore(/*copies=*/false);
|
|
const int key = 0;
|
|
|
|
Vector<Uint8> staged(32, 0xAB);
|
|
const Uint8* splitBase = splitStore.Adopt(&key, 64, staged.data(), 16, staged.size());
|
|
const Uint8* monolithBase = monolithStore.Adopt(&key, 64, staged.data(), 16, staged.size());
|
|
|
|
ASSERT_NE(splitBase, nullptr);
|
|
EXPECT_EQ(monolithBase, staged.data() - 16)
|
|
<< "the monolith arm must be the ORIGINAL expression, character for character, or every "
|
|
"push and verify lane is running new code it was never measured against";
|
|
EXPECT_NE(splitBase, monolithBase);
|
|
|
|
// w1's retired-stage poison, by hand and at the right moment: the record has retired, so
|
|
// the staging run is dead.
|
|
std::fill(staged.begin(), staged.end(), Uint8{0xDD});
|
|
|
|
for (SizeT i = 0; i < 32; ++i) {
|
|
EXPECT_EQ(splitBase[16 + i], 0xAB) << "byte " << i << " of the server's copy is the "
|
|
"poison, so the copy never happened";
|
|
}
|
|
// And the control: the monolith arm reads the poison, which is what makes the assertion
|
|
// above a statement about copying rather than about the test's own buffer.
|
|
EXPECT_EQ(monolithBase[16], 0xDD);
|
|
}
|
|
|
|
// The extent is EXACTLY what the record declared. Adjacent runs merge because there is no gap
|
|
// between them; runs with a gap do not, and that is the whole mechanism - it is what makes a
|
|
// missing record detectable instead of papered over by a widened INVALIDATE_RANGE.
|
|
TEST(StagedShadowTest, CoverageIsExactAndAGapIsNotCovered) {
|
|
Server::StagedShadowStore store(/*copies=*/true);
|
|
const int key = 0;
|
|
Vector<Uint8> bytes(16, 0x11);
|
|
|
|
store.Adopt(&key, 64, bytes.data(), 0, 16);
|
|
store.Adopt(&key, 64, bytes.data(), 32, 16);
|
|
EXPECT_EQ(store.CoveredRunCount(&key), 2u) << "two runs with a 16-byte gap merged into one";
|
|
EXPECT_TRUE(store.IsCovered(&key, 0, 16));
|
|
EXPECT_TRUE(store.IsCovered(&key, 32, 48));
|
|
EXPECT_FALSE(store.IsCovered(&key, 16, 32)) << "the gap reads as covered";
|
|
EXPECT_FALSE(store.IsCovered(&key, 0, 48)) << "a span across the gap reads as covered";
|
|
EXPECT_FALSE(store.IsCovered(&key, 8, 40));
|
|
|
|
// Filling the gap merges all three into one run, which is the proof that adjacency (not
|
|
// proximity) is the merge rule.
|
|
store.Adopt(&key, 64, bytes.data(), 16, 16);
|
|
EXPECT_EQ(store.CoveredRunCount(&key), 1u);
|
|
EXPECT_TRUE(store.IsCovered(&key, 0, 48));
|
|
}
|
|
|
|
// m-5's discriminator, at unit scope: HasShadow is TRUE for a key that was Adopted and FALSE once
|
|
// DropAll has run - which is exactly what tells the generation-reset block whether a twin's
|
|
// hostBytes names a live server shadow (keep it) or a freed one (null it). A version that answered
|
|
// "always live" would let a freed base reach the driver; "always gone" would drop a base a subdata
|
|
// just staged in the same generation (that regression really happened - TriangleScenario read a
|
|
// shifted VBO). Both directions are asserted here.
|
|
TEST(StagedShadowTest, HasShadowIsTrueAfterAdoptAndFalseAfterTheShadowIsDropped) {
|
|
Server::StagedShadowStore store(/*copies=*/true);
|
|
const int key = 0;
|
|
Vector<Uint8> bytes(16, Uint8{0x44});
|
|
EXPECT_FALSE(store.HasShadow(&key)) << "nothing staged yet";
|
|
store.Adopt(&key, 16, bytes.data(), 0, 16);
|
|
EXPECT_TRUE(store.HasShadow(&key)) << "a staged key must read live, or the reset block nulls a "
|
|
"base a subdata just filled";
|
|
store.DropAll();
|
|
EXPECT_FALSE(store.HasShadow(&key)) << "after DropAll the base is freed and must read gone, or "
|
|
"a surviving twin hands the driver a dangling pointer";
|
|
}
|
|
|
|
TEST(StagedShadowTest, DropForgetsOneResourceAndDropAllForgetsEveryOne) {
|
|
Server::StagedShadowStore store(/*copies=*/true);
|
|
const int a = 0;
|
|
const int b = 0;
|
|
Vector<Uint8> bytes(8, 0x22);
|
|
store.Adopt(&a, 8, bytes.data(), 0, 8);
|
|
store.Adopt(&b, 8, bytes.data(), 0, 8);
|
|
ASSERT_EQ(store.TrackedResources(), 2u);
|
|
store.Drop(&a);
|
|
EXPECT_EQ(store.TrackedResources(), 1u);
|
|
EXPECT_FALSE(store.IsCovered(&a, 0, 8));
|
|
EXPECT_TRUE(store.IsCovered(&b, 0, 8));
|
|
store.DropAll();
|
|
EXPECT_EQ(store.TrackedResources(), 0u);
|
|
}
|
|
|
|
// The Fatal, and it asserts ITS OWN failure string rather than "the process died". A death
|
|
// test that only checks for a crash goes green on any other abort in the same body, which is
|
|
// exactly the shape this wave shipped three of.
|
|
#if !defined(_WIN32)
|
|
TEST(StagedShadowTest, ADrainOutsideTheStagedCoverageIsFatalByName) {
|
|
Server::StagedShadowStore store(/*copies=*/true);
|
|
const int key = 0;
|
|
Vector<Uint8> bytes(16, 0x33);
|
|
const Uint8* base = store.Adopt(&key, 64, bytes.data(), 0, 16);
|
|
|
|
// In coverage: no Fatal, and this is asserted first so that the death below cannot be a
|
|
// function that aborts on everything.
|
|
store.RequireCoverage(&key, base, 0, 16, "unit");
|
|
// A base that is not this resource's shadow is not this rule's subject - that is the
|
|
// legacy arm, whose MappedData() is valid for the whole store.
|
|
store.RequireCoverage(&key, bytes.data(), 0, 64, "unit");
|
|
|
|
// m-2: name the death MODE and the diagnostic, not just "the process died". The empty regex
|
|
// accepted any death, including a SIGSEGV inside RequireCoverage; KilledBySignal(SIGABRT) pins
|
|
// it to the Fatal's abort() (a segfault is SIGSEGV and fails this), and the log grep names the
|
|
// exact wording. The log flush is pinned: Log.cpp's WriteToFile fflushes after every write and
|
|
// MGLOG_F logs before abort(), so the line is on disk in the forked child before it dies.
|
|
EXPECT_EXIT(store.RequireCoverage(&key, base, 0, 64, "unit_out_of_range"),
|
|
::testing::KilledBySignal(SIGABRT), ".*");
|
|
const std::string log = ReadLog();
|
|
EXPECT_NE(log.find("Fatal{StageSnapshotTooNarrow, \"unit_out_of_range\"}"), std::string::npos)
|
|
<< "the abort happened but not for this rule's reason; the log says: " << log;
|
|
}
|
|
#endif
|
|
|
|
// =====================================================================================
|
|
// R-11's PRODUCTION wiring (M-1 / codex 8), the EGL lifecycle seams (C2/M-6/M-7/C6/C7)
|
|
// =====================================================================================
|
|
|
|
// M-1 / codex 8: the R-11 gate that drives the PRODUCTION path - the real resource op table
|
|
// installed by RegisterBufferBackendOps, dispatched through MGPipeGetResourceOps()->SubData, i.e.
|
|
// the exact Managers.cpp:2118 call site R-11 changed - and NOT StagedShadowStore in isolation.
|
|
// StagedShadowTest stays as the container test; this is the gate that goes red under the two
|
|
// perturbations the reviewer used (restore `hostBytes = raw - offset`, neuter the coverage clamp).
|
|
#if !defined(_WIN32)
|
|
TEST(StagedShadowProductionTest, SubDataThroughTheRealOpsTableCopiesAndSurvivesTheSourcePoison) {
|
|
// MG_Config::Transport is InProcess (main), so ServerStaged() latches its copying arm on.
|
|
MG_Backend::DirectGLES::BufferImpl::RegisterBufferBackendOps();
|
|
const MG_Pipe::MGPipeResourceOps* ops = MG_Pipe::MGPipeGetResourceOps();
|
|
ASSERT_NE(ops, nullptr) << "RegisterBufferBackendOps did not install the resource op table";
|
|
ASSERT_NE(ops->SubData, nullptr);
|
|
|
|
MG_Pipe::MGPipeHandle res{};
|
|
res.Slot = 7;
|
|
res.Gen = 1;
|
|
auto* twin = MG_Backend::DirectGLES::BufferImpl::GetOrCreateBufferResourceForHandle(res);
|
|
ASSERT_NE(twin, nullptr);
|
|
|
|
Vector<Uint8> src(32, Uint8{0xAB});
|
|
MG_Pipe::MGPSubData rec{};
|
|
rec.Res = res;
|
|
rec.Target = MG_Pipe::MGPipePackSubDataTarget(MG_Pipe::kMGPipeResourceTargetBuffer, 0u);
|
|
ASSERT_TRUE(MG_Pipe::MGPipeSetSubDataBufferRange(rec, 0, src.size()));
|
|
// THE PRODUCTION CALL. Not StagedShadowStore::Adopt directly - the whole point of M-1.
|
|
ops->SubData(res, rec, src.data());
|
|
|
|
auto* found = MG_Backend::DirectGLES::BufferImpl::FindBufferResourceForHandle(res);
|
|
ASSERT_NE(found, nullptr);
|
|
ASSERT_NE(found->hostBytes, nullptr) << "Ops_H_SubData recorded no base at all";
|
|
EXPECT_NE(found->hostBytes, static_cast<const Uint8*>(src.data()))
|
|
<< "hostBytes points into the CLIENT's staging bytes (raw - offset), the exact rule-C "
|
|
"violation R-11 exists to fix - restore that line and this goes red";
|
|
|
|
// w1's retired-stage poison, by hand and at the right moment: the record has 'retired', so the
|
|
// client's staging run is dead. A server that copied still reads the original bytes.
|
|
std::fill(src.begin(), src.end(), Uint8{0xDD});
|
|
for (SizeT i = 0; i < src.size(); ++i) {
|
|
ASSERT_EQ(found->hostBytes[i], 0xAB)
|
|
<< "byte " << i << " of the server base is the poison: Ops_H_SubData kept a pointer "
|
|
"into SEG_STAGE instead of copying";
|
|
}
|
|
MG_Backend::DirectGLES::BufferImpl::UnregisterBufferBackendOps();
|
|
}
|
|
#endif
|
|
|
|
// C2: after the loop has stopped, a forwarder call must return NOT_INITIALIZED and must NOT run
|
|
// inline on the caller. The pre-fix code read m_running outside the control mutex and ran work
|
|
// inline for !m_running - which in the race the verifier's latch harness reproduced
|
|
// (v1-codex-verify.md C2) hung forever posting into a mailbox no thread pumps. The fix clears
|
|
// m_running UNDER the mutex and re-checks it there; this is the deterministic half of it (no
|
|
// latch needed: after Stop() m_running is false for certain).
|
|
TEST(ServerLoopTest, AForwarderCallAfterTheLoopStoppedReturnsNotInitializedAndDoesNotRunInline) {
|
|
ServerFixture fixture;
|
|
ASSERT_TRUE(fixture.Handshake());
|
|
ASSERT_TRUE(fixture.StartLoop());
|
|
ASSERT_TRUE(fixture.WaitUntilTrulyParked());
|
|
fixture.Stop();
|
|
ASSERT_FALSE(Server::ServerLoopInstance().Running());
|
|
|
|
struct Probe {
|
|
Bool ran = false;
|
|
} probe;
|
|
const MobileGLResult rc = Server::ServerLoopInstance().RunOnApplyThread(
|
|
+[](void* user) -> MobileGLResult {
|
|
static_cast<Probe*>(user)->ran = true;
|
|
return MOBILEGL_OK;
|
|
},
|
|
&probe);
|
|
|
|
EXPECT_EQ(rc, MOBILEGL_ERR_NOT_INITIALIZED)
|
|
<< "a forwarder call after Stop() did not return NOT_INITIALIZED (old code ran it inline)";
|
|
EXPECT_FALSE(probe.ran) << "the work ran on the caller after the loop stopped";
|
|
}
|
|
|
|
// M-7: the OTHER !m_running arm - a backend built but no thread (Start refused / std::thread
|
|
// threw) - must be a NAMED Fatal, never a silent inline EGL-on-the-app-thread fallback. A split
|
|
// lane that ran eglMakeCurrent on the app thread would render correctly with the context on the
|
|
// wrong thread, which is the one outcome R-1 exists to make impossible.
|
|
#if !defined(_WIN32)
|
|
TEST(ServerLoopTest, AForwarderWithABackendButNoThreadIsFatalNotAnAppThreadFallback) {
|
|
Server::ServerLoop& loop = Server::ServerLoopInstance();
|
|
ASSERT_EQ(loop.CreateBackend(BackendType::DirectGLES), MOBILEGL_OK);
|
|
ASSERT_FALSE(loop.Running());
|
|
// A backend exists, no apply thread runs. A forwarder must abort by name.
|
|
EXPECT_EXIT(Server::ServerReleaseEGLResources(), ::testing::KilledBySignal(SIGABRT), ".*");
|
|
const std::string log = ReadLog();
|
|
EXPECT_NE(log.find("Fatal{ApplyThreadNotRunning"), std::string::npos)
|
|
<< "the abort was not the M-7 named Fatal; the log says: " << log;
|
|
loop.Stop(); // drop the backend in the parent
|
|
}
|
|
#endif
|
|
|
|
// M-6: ServerLoop::Stop() with no thread ever started must still DROP the private backend, which
|
|
// ShutdownSplitRoles relies on for the early-Start-failure path. If it does not, the server's
|
|
// BackendObject stays alive with g_resourceOps pointing into it and every later CreateBackend in
|
|
// the process is refused - which is exactly the leak M-6 names.
|
|
TEST(ServerLoopTest, StopWithoutAStartedThreadStillDropsThePrivateBackend) {
|
|
Server::ServerLoop& loop = Server::ServerLoopInstance();
|
|
ASSERT_EQ(loop.CreateBackend(BackendType::DirectGLES), MOBILEGL_OK);
|
|
ASSERT_NE(loop.Backend(), nullptr);
|
|
loop.Stop(); // the !joinable arm
|
|
EXPECT_EQ(loop.Backend(), nullptr) << "Stop() left the private backend alive with no thread";
|
|
EXPECT_FALSE(loop.Running());
|
|
// A second bring-up must now succeed; CreateBackend refuses if m_backend != nullptr.
|
|
EXPECT_EQ(loop.CreateBackend(BackendType::DirectGLES), MOBILEGL_OK)
|
|
<< "the leaked backend blocks the next split bring-up (CreateBackend's m_backend guard)";
|
|
loop.Stop();
|
|
}
|
|
|
|
// C6: the server's format-capability accessor reads the SERVER's own backend cache, not the
|
|
// process global (which under split is the CLIENT's BackendObject_Remote). Pointer-identity: no
|
|
// mask injection needed - ActiveBackendFormatCaps() must return the server's cache address, not
|
|
// the global's. Red once by making ActiveBackendFormatCaps return pActiveBackendObject's.
|
|
TEST(ServerLoopTest, TheServerFormatCapsAccessorReadsTheServersOwnBackendNotTheGlobal) {
|
|
Server::ServerLoop& loop = Server::ServerLoopInstance();
|
|
ASSERT_EQ(loop.CreateBackend(BackendType::DirectGLES), MOBILEGL_OK);
|
|
MG_Backend::BackendObject* server = loop.Backend();
|
|
ASSERT_NE(server, nullptr);
|
|
|
|
// A DIFFERENT object in the global the seven C6 reads used to follow.
|
|
auto client = MakeUnique<MG_Backend::DirectGLES::BackendObject_DirectGLES>();
|
|
MG_Backend::BackendObject* clientRaw = client.get();
|
|
MG_Backend::pActiveBackendObject = std::move(client);
|
|
|
|
const MG_Backend::FormatCapabilityCache* caps = MG_Backend::DirectGLES::ActiveBackendFormatCaps();
|
|
EXPECT_EQ(caps, &server->GetFormatCapabilities())
|
|
<< "ActiveBackendFormatCaps returned the process global's cache; under split that is the "
|
|
"CLIENT's BackendObject_Remote, not the server's private backend";
|
|
EXPECT_NE(caps, &clientRaw->GetFormatCapabilities());
|
|
|
|
loop.Stop();
|
|
MG_Backend::pActiveBackendObject.reset();
|
|
}
|
|
|
|
// C7 / ID-54: the make-current DECISION, pure. A new tuple binds; an identical repeat is a no-op; a
|
|
// release request (the three NO_* markers) is recorded, not forwarded. The native-bind COUNT - what
|
|
// the driver actually saw - is measured by ServerLoopEglTest's C7 control on a real context below,
|
|
// which is the control round 2 said needed the joint lane. Red once by deleting the RepeatNoOp arm:
|
|
// an identical repeat then classifies as NativeBind.
|
|
TEST(ServerLoopTest, MakeCurrentClassifiesRepeatAndReleaseWithoutRebinding) {
|
|
const EGLDisplay dpy = reinterpret_cast<EGLDisplay>(0x1);
|
|
const EGLSurface draw = reinterpret_cast<EGLSurface>(0x2);
|
|
const EGLSurface read = reinterpret_cast<EGLSurface>(0x2);
|
|
const EGLContext ctx = reinterpret_cast<EGLContext>(0x3);
|
|
|
|
// Nothing current yet: a genuine bind.
|
|
EXPECT_EQ(Server::ClassifyEglMakeCurrent(false, EGL_NO_DISPLAY, EGL_NO_SURFACE, EGL_NO_SURFACE,
|
|
EGL_NO_CONTEXT, dpy, draw, read, ctx),
|
|
Server::EglBindAction::NativeBind);
|
|
// The SAME tuple already held: a no-op, no native bind, no owner write.
|
|
EXPECT_EQ(Server::ClassifyEglMakeCurrent(true, dpy, draw, read, ctx, dpy, draw, read, ctx),
|
|
Server::EglBindAction::RepeatNoOp)
|
|
<< "an identical make-current was classified as a native rebind; the owner would be "
|
|
"written twice and the caches invalidated for nothing";
|
|
// A DIFFERENT tuple: a real rebind.
|
|
const EGLContext otherCtx = reinterpret_cast<EGLContext>(0x9);
|
|
EXPECT_EQ(Server::ClassifyEglMakeCurrent(true, dpy, draw, read, ctx, dpy, draw, read, otherCtx),
|
|
Server::EglBindAction::NativeBind);
|
|
// A release request, whatever is current: recorded, not forwarded (the context is held).
|
|
EXPECT_EQ(Server::ClassifyEglMakeCurrent(true, dpy, draw, read, ctx, dpy, EGL_NO_SURFACE,
|
|
EGL_NO_SURFACE, EGL_NO_CONTEXT),
|
|
Server::EglBindAction::ClientRelease);
|
|
}
|
|
|
|
// =====================================================================================
|
|
// Round 3: the gates the v2 review found missing (N-5), each driving the PRODUCTION wiring
|
|
// =====================================================================================
|
|
|
|
// M-6 (review v2 item 7): the fix is the ServerLoop::Stop() call INSIDE ShutdownSplitRoles, so the
|
|
// gate calls ShutdownSplitRoles - not Stop - after the shape M-6 names: InitSplitRoles step 1 built
|
|
// the server's private backend and step 3 (ClientSession::Start) failed EARLY. The failure is a real
|
|
// one, not a skipped step: P5's client refuses every transport but InProcess by name, before Accept
|
|
// and before any ring exists. Red once by deleting Init.cpp's `ServerLoopInstance().Stop()` line:
|
|
// the backend outlives the shutdown and the next CreateBackend is refused.
|
|
TEST(ServerLoopTest, ShutdownSplitRolesAfterAnEarlyStartFailureDropsTheServersPrivateBackend) {
|
|
Server::ServerLoop& loop = Server::ServerLoopInstance();
|
|
ASSERT_EQ(loop.CreateBackend(BackendType::DirectGLES), MOBILEGL_OK);
|
|
ASSERT_NE(MobileGL::MG_Remote::Client::ClientSessionInstance().Start(MG_Config::TransportMode::Spawn, ""),
|
|
MOBILEGL_OK)
|
|
<< "the forced early Start failure did not fail; the case has nothing to shut down after";
|
|
ASSERT_NE(loop.Backend(), nullptr);
|
|
ASSERT_FALSE(loop.Running());
|
|
|
|
MG_Backend::ShutdownSplitRoles();
|
|
|
|
EXPECT_EQ(loop.Backend(), nullptr)
|
|
<< "ShutdownSplitRoles left the server's private backend alive after an early "
|
|
"ClientSession::Start failure (M-6): every later split bring-up in this process is refused "
|
|
"at CreateBackend's m_backend guard";
|
|
EXPECT_FALSE(loop.Running());
|
|
EXPECT_EQ(loop.CreateBackend(BackendType::DirectGLES), MOBILEGL_OK)
|
|
<< "the leaked backend blocks the next split bring-up";
|
|
loop.Stop();
|
|
}
|
|
|
|
// codex 11 (review v2 item 6): the resolved mask must be the EFFECTIVE set the kernel took, read
|
|
// back with sched_getaffinity, not the request. The review's point was that on an unrestricted box
|
|
// the two never differ, so `return requested` stays green; this makes them differ without a cpuset:
|
|
// the request names one cpu this thread may use (taken from the kernel's own answer, so the case
|
|
// constructs nothing it observes) PLUS cpu 63, which does not exist on any box with fewer than 64
|
|
// cpus. sched_setaffinity accepts the mask with the absent cpu dropped, so the effective set is the
|
|
// one real cpu - and a resolver that echoes the request reports a cpu the thread can never run on.
|
|
// Red once by making ApplyAffinity `return requested;`: the resolved mask reads 0x8000000000000001-
|
|
// shaped instead of 0x1-shaped, and the log line names the wrong mask.
|
|
#if defined(__linux__) || defined(__ANDROID__)
|
|
TEST(ServerLoopTest, TheResolvedAffinityMaskIsTheKernelsEffectiveSetNotTheRequest) {
|
|
const unsigned hw = std::thread::hardware_concurrency();
|
|
if (hw == 0 || hw >= 64) {
|
|
GTEST_SKIP() << "needs a box with fewer than 64 cpus so that cpu 63 is absent (has " << hw << ")";
|
|
}
|
|
cpu_set_t allowed;
|
|
CPU_ZERO(&allowed);
|
|
ASSERT_EQ(sched_getaffinity(0, sizeof(allowed), &allowed), 0);
|
|
int usable = -1;
|
|
for (int cpu = 0; cpu < 63; ++cpu) {
|
|
if (CPU_ISSET(cpu, &allowed)) {
|
|
usable = cpu;
|
|
break;
|
|
}
|
|
}
|
|
if (usable < 0 || CPU_ISSET(63, &allowed)) {
|
|
GTEST_SKIP() << "this thread's allowed set is not the shape the case needs";
|
|
}
|
|
const Uint64 requested = (1ull << usable) | (1ull << 63);
|
|
const Uint64 effective = 1ull << usable;
|
|
char requestedText[40];
|
|
std::snprintf(requestedText, sizeof(requestedText), "0x%llx", static_cast<unsigned long long>(requested));
|
|
char effectiveLine[64];
|
|
std::snprintf(effectiveLine, sizeof(effectiveLine), "RESOLVED mask 0x%llx (0 = no affinity applied)",
|
|
static_cast<unsigned long long>(effective));
|
|
|
|
const String saved = MG_Config::Ipc.ServerAffinity;
|
|
MG_Config::Ipc.ServerAffinity = requestedText;
|
|
|
|
ServerFixture fixture;
|
|
ASSERT_TRUE(fixture.Handshake());
|
|
ASSERT_TRUE(fixture.StartLoop());
|
|
ASSERT_TRUE(fixture.WaitUntilTrulyParked());
|
|
|
|
EXPECT_EQ(Server::ServerLoopInstance().ResolvedAffinityMask(), effective)
|
|
<< "the resolved affinity mask reports the REQUESTED set (" << requestedText
|
|
<< ") rather than the effective one the kernel took: cpu 63 does not exist on this " << hw
|
|
<< "-cpu box, sched_setaffinity dropped it, and codex 11's sched_getaffinity read-back is what "
|
|
"makes that visible";
|
|
const std::string log = ReadLog();
|
|
EXPECT_NE(log.find(effectiveLine), std::string::npos)
|
|
<< "the logged RESOLVED mask is not the effective one (" << effectiveLine << ")";
|
|
|
|
fixture.Stop();
|
|
MG_Config::Ipc.ServerAffinity = saved;
|
|
}
|
|
#endif
|
|
|
|
namespace {
|
|
// A second DirectGLES object whose format cache the case can FILL, so that the server's (empty,
|
|
// never InitCapabilities'd) cache and the global's answer differently. Filling an INPUT is not
|
|
// constructing the observed state (R-16): what is observed is which object each read follows.
|
|
struct CapsProbeBackend final : MG_Backend::DirectGLES::BackendObject_DirectGLES {
|
|
MG_Backend::FormatCapabilityCache& Caps() { return MutableFormatCapabilities(); }
|
|
};
|
|
} // namespace
|
|
|
|
// C6 (review v2 item 9): the seven table-3 reads, asserted by their ANSWERS with two objects
|
|
// installed - not by the accessor's address, which is what round 2 gated and what reverting one
|
|
// call site at a time never moved. The server's cache is empty; the global's is not.
|
|
//
|
|
// Four of the seven are VALUE reads and the two objects' caches disagree for them: the
|
|
// HasCachedFormatCapability pair behind the caveat decision (Utils.cpp) and the
|
|
// ClampSamplesToBackendSupport pair (BackendObject_DirectGLES.cpp). Two are NULL-CHECK reads -
|
|
// GenerateFormatInfo's and BackendFormatAddsAlpha's "is there a backend at all" - which two live
|
|
// objects cannot tell apart, so for those the global is NULL and the server is present, which is
|
|
// exactly the window between InitSplitRoles step 1 (the server backend) and step 4 (the client
|
|
// object) that the reads used to answer "no backend" in. The seventh, UsesWidenedPacked16NormStorage's
|
|
// gate in front of a memoised driver probe, is a null check whose two arms give the same answer on
|
|
// the reference rasterizer (the probe says "not mirrored", the no-backend arm says false); it has
|
|
// no answer-level control on this box and the report says so.
|
|
//
|
|
// Red once, one site at a time: revert the HasCachedFormatCapability pair -> the caveat decision
|
|
// follows the global; revert ClampSamples -> the clamp answers 6; revert GenerateFormatInfo's null
|
|
// check -> RGB8 widens to RGBA8; revert BackendFormatAddsAlpha's -> adds-alpha answers true.
|
|
TEST(ServerLoopTest, TheSevenFormatCapabilityReadsAnswerFromTheServersBackendNotTheGlobal) {
|
|
namespace TextureImpl = MG_Backend::DirectGLES::TextureImpl;
|
|
Server::ServerLoop& loop = Server::ServerLoopInstance();
|
|
ASSERT_EQ(loop.CreateBackend(BackendType::DirectGLES), MOBILEGL_OK);
|
|
const SizeT tex2d = MG_Backend::GetFormatCapabilityTargetIndex(TextureTarget::Texture2D);
|
|
const SizeT rb = MG_Backend::GetRenderbufferFormatCapabilityTargetIndex();
|
|
const SizeT rgb8 = static_cast<SizeT>(TextureInternalFormat::RGB8);
|
|
const SizeT rgba8 = static_cast<SizeT>(TextureInternalFormat::RGBA8);
|
|
|
|
// (A) the global holds a DIFFERENT object whose cache says the opposite of "empty": RGB8 is
|
|
// caveat-only there, and RGBA8 was "probed" at six samples - a count no driver's own maximum
|
|
// is, so it cannot collide with the fallback the server's empty cache falls to.
|
|
auto client = MakeUnique<CapsProbeBackend>();
|
|
for (const SizeT target : {tex2d, rb}) {
|
|
client->Caps().CaveatCaps[target][rgb8] |= MG_Backend::FormatCapability::Creatable;
|
|
client->Caps().CaveatCaps[target][rgb8] |= MG_Backend::FormatCapability::FramebufferRenderable;
|
|
client->Caps().SampleCounts[target][rgba8] = {6, 2};
|
|
}
|
|
MG_Backend::pActiveBackendObject = std::move(client);
|
|
|
|
EXPECT_FALSE(TextureImpl::ShouldUseCaveatTextureFormat(TextureInternalFormat::RGB8, TextureTarget::Texture2D))
|
|
<< "the caveat decision followed the global's cache (caveat-only RGB8), not the server's "
|
|
"(empty): the HasCachedFormatCapability pair reads pActiveBackendObject again";
|
|
EXPECT_FALSE(TextureImpl::ShouldUseCaveatRenderbufferFormat(TextureInternalFormat::RGB8))
|
|
<< "the renderbuffer caveat decision followed the global's cache, not the server's";
|
|
EXPECT_NE(MG_Backend::DirectGLES::ClampSamplesToBackendSupport(tex2d, TextureInternalFormat::RGBA8, GL_RGBA8, 8), 6)
|
|
<< "the sample clamp answered from the global's probed counts (6), not the server's";
|
|
EXPECT_NE(MG_Backend::DirectGLES::ClampSamplesToBackendSupport(rb, TextureInternalFormat::RGBA8, GL_RGBA8, 8), 6)
|
|
<< "the renderbuffer sample clamp answered from the global's probed counts (6), not the server's";
|
|
|
|
// (B) the null-check reads: global NULL, server present. RGB8_SNORM, not RGB8: the fallback
|
|
// normalisation's three-channel widening is APPLICABLE to GL_RGB8_SNORM and not to GL_RGB8
|
|
// (TextureFormatProcessor.cpp's applicability table), so it is the format whose answer the
|
|
// "no backend" arm actually changes - the first cut of this case used RGB8 and both arms agreed.
|
|
MG_Backend::pActiveBackendObject.reset();
|
|
constexpr GLenum kGlRgb8Snorm = 0x8F96;
|
|
GLenum internalFormat = 0;
|
|
GLenum format = 0;
|
|
GLenum type = 0;
|
|
TextureImpl::GenerateTextureFormatInfo(TextureInternalFormat::RGB8Snorm, &internalFormat, &format, &type,
|
|
TextureTarget::Texture2D);
|
|
EXPECT_EQ(internalFormat, kGlRgb8Snorm)
|
|
<< "the format info followed the global (null, so the fallback normalisation widened RGB8_SNORM) "
|
|
"instead of the server's backend (present, so native RGB8_SNORM); got 0x" << std::hex
|
|
<< internalFormat;
|
|
EXPECT_FALSE(TextureImpl::BackendTextureFormatAddsAlpha(TextureInternalFormat::RGB8Snorm, TextureTarget::Texture2D))
|
|
<< "the adds-alpha answer followed the global (null, so the fallback adds alpha) instead of "
|
|
"the server's backend";
|
|
EXPECT_FALSE(TextureImpl::BackendRenderbufferFormatAddsAlpha(TextureInternalFormat::RGB8Snorm))
|
|
<< "the renderbuffer adds-alpha answer followed the global (null) instead of the server's backend";
|
|
|
|
loop.Stop();
|
|
}
|
|
|
|
// =====================================================================================
|
|
// The EGL fixture: the SERVER's real DirectGLES backend, a real headless context (mesa
|
|
// surfaceless, llvmpipe) on the apply thread, driven only through v1's twelve forwarders
|
|
// =====================================================================================
|
|
//
|
|
// Round 2 shipped its C7, M-3 and m-5 claims with the caveat "the native count is a joint-lane
|
|
// control (needs a real EGL context)", and the review answered that nothing then gates them. This
|
|
// fixture is the answer to that: the server role's own backend, brought up exactly the way
|
|
// InitSplitRoles and the client's EGL virtuals bring it up - CreateBackend on the app thread, then
|
|
// InitializeEGLDisplay / CreateEGLPbufferSurface / MakeEGLCurrent as BLOCKING control requests on
|
|
// mgl-srv-apply - with no client object, no emit table and no scenario. What the joint lane adds is
|
|
// c1's client; what it does not add is anything these cases observe: the driver's own eglMakeCurrent
|
|
// count, the applier's read_pixels reply, and the backend's buffer twins.
|
|
//
|
|
// THE NATIVE COUNT IS READ WHERE THE DRIVER READS IT. DirectGLES dispatches every EGL call through
|
|
// its function table (g_EGLFuncs, filled by BackendObject_DirectGLES::Initialize); the fixture puts
|
|
// a counting trampoline in front of that table's eglMakeCurrent and forwards to the driver's entry
|
|
// point. Nothing in the production tree is edited or told, and no production counter is trusted for
|
|
// this number - ServerLoop::NativeBindCount() is the forwarder's view, and the whole point of the C7
|
|
// control is that the two can disagree (they did: 2 native binds per process with the forwarder
|
|
// counting 1, review v2 item 10).
|
|
//
|
|
// NO USABLE EGL IS A SKIP, UNLESS MOBILEGL_ITEST_REQUIRE_GPU SAYS OTHERWISE - the integration
|
|
// harness's own rule (ScenarioFixture.h): a developer box without mesa should not fail a run it could
|
|
// not perform, but a lane that relies on these cases sets the variable, and then an unusable EGL is
|
|
// a FAILURE naming the step, never a green that ran nothing (review v2 N-1's lesson).
|
|
#if !defined(_WIN32)
|
|
namespace {
|
|
|
|
std::atomic<int> g_nativeEglBinds{0};
|
|
std::atomic<int> g_nativeEglReleases{0};
|
|
decltype(MG_Backend::DirectGLES::g_EGLFuncs.eglMakeCurrent) g_driverEglMakeCurrent = nullptr;
|
|
|
|
EGLBoolean CountingEglMakeCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx) {
|
|
if (ctx == EGL_NO_CONTEXT) {
|
|
g_nativeEglReleases.fetch_add(1, std::memory_order_acq_rel);
|
|
} else {
|
|
g_nativeEglBinds.fetch_add(1, std::memory_order_acq_rel);
|
|
}
|
|
return g_driverEglMakeCurrent(dpy, draw, read, ctx);
|
|
}
|
|
|
|
Bool RequireGpuFromEnvironment() {
|
|
const char* value = std::getenv("MOBILEGL_ITEST_REQUIRE_GPU");
|
|
return value != nullptr && value[0] != '\0' && std::strcmp(value, "0") != 0;
|
|
}
|
|
|
|
// The integration harness's headless pin (HeadlessGL.cpp, EnsureHeadlessPlatform), for the same
|
|
// reason: mesa's surfaceless platform needs no display, and glvnd has to be told which vendor to
|
|
// load. The vendor json is the one MG_IntegrationTest/CMakeLists.txt pins by default; an operator
|
|
// who set __EGL_VENDOR_LIBRARY_FILENAMES keeps theirs.
|
|
void PinHeadlessEglEnvironment() {
|
|
if (std::getenv("EGL_PLATFORM") == nullptr) setenv("EGL_PLATFORM", "surfaceless", 1);
|
|
unsetenv("DISPLAY");
|
|
unsetenv("WAYLAND_DISPLAY");
|
|
if (std::getenv("__EGL_VENDOR_LIBRARY_FILENAMES") == nullptr) {
|
|
const char* mesa = "/usr/share/glvnd/egl_vendor.d/50_mesa.json";
|
|
std::error_code ec;
|
|
if (std::filesystem::exists(mesa, ec)) setenv("__EGL_VENDOR_LIBRARY_FILENAMES", mesa, 1);
|
|
}
|
|
}
|
|
|
|
// A control request that runs an arbitrary callable on the apply thread. A std::function is
|
|
// fine in a test; production's ControlWork is a raw pointer for the teardown path's sake.
|
|
MobileGLResult OnApply(const std::function<void()>& body) {
|
|
std::function<void()> copy = body;
|
|
return Server::ServerLoopInstance().RunOnApplyThread(
|
|
+[](void* user) -> MobileGLResult {
|
|
(*static_cast<std::function<void()>*>(user))();
|
|
return MOBILEGL_OK;
|
|
},
|
|
©);
|
|
}
|
|
|
|
struct EglServerFixture : ServerFixture {
|
|
// The client's VIRTUAL handles. EGLImpl mints small integers, and every one of them is 0x1
|
|
// on this host (the N-3 finding's whole point), so the cases use exactly that shape.
|
|
static EGLDisplay Dpy() { return reinterpret_cast<EGLDisplay>(static_cast<std::uintptr_t>(0x1)); }
|
|
static EGLSurface Surf() { return reinterpret_cast<EGLSurface>(static_cast<std::uintptr_t>(0x1)); }
|
|
static EGLContext Ctx() { return reinterpret_cast<EGLContext>(static_cast<std::uintptr_t>(0x1)); }
|
|
|
|
// The backend's draw-shaped paths (ReadPixels syncs the bound textures and the read
|
|
// framebuffer) walk the process's frontend GLContext, which a bare unit process does not
|
|
// have; installed for the case's life and put back afterwards (SplitBufferTest's shape).
|
|
UniquePtr<MG_State::GLState::GLContext> savedContext;
|
|
|
|
// Empty on success, else the step that failed - the harness's SkipReason shape.
|
|
std::string BringUp() {
|
|
PinHeadlessEglEnvironment();
|
|
savedContext = Move(MG_State::pGLContext);
|
|
MG_State::pGLContext = MakeUnique<MG_State::GLState::GLContext>();
|
|
Server::ServerLoop& loop = Server::ServerLoopInstance();
|
|
// InitSplitRoles step 1: the server's private backend, no GL and no EGL yet; its
|
|
// Initialize() loads the driver's EGL/GLES entry points into the two tables.
|
|
if (loop.CreateBackend(BackendType::DirectGLES) != MOBILEGL_OK) return "CreateBackend refused";
|
|
const MG_External::EGLFunctionsTable& egl = MG_Backend::DirectGLES::g_EGLFuncs;
|
|
if (egl.eglMakeCurrent == nullptr) return "libEGL did not load (no eglMakeCurrent entry point)";
|
|
if (egl.eglMakeCurrent != &CountingEglMakeCurrent) g_driverEglMakeCurrent = egl.eglMakeCurrent;
|
|
g_nativeEglBinds.store(0);
|
|
g_nativeEglReleases.store(0);
|
|
MG_External::EGLFunctionsTable counted = egl;
|
|
counted.eglMakeCurrent = &CountingEglMakeCurrent;
|
|
MG_Backend::DirectGLES::SetEGLFuncsTable(counted);
|
|
// InitSplitRoles step 2: the session answers its caps snapshots from this backend, so
|
|
// ServerMakeEGLCurrent's R-12 republish has something to publish (ID-67's control).
|
|
Server::ServerSessionInstance().SetBackend(loop.Backend());
|
|
if (!Handshake()) return "the session handshake";
|
|
if (!StartLoop()) return "ServerLoop::Start";
|
|
EGLint major = 0;
|
|
EGLint minor = 0;
|
|
if (!Server::ServerInitializeEGLDisplay(Dpy(), &major, &minor)) return "ServerInitializeEGLDisplay";
|
|
// THE SURFACE'S OWN CREATION is what binds natively (InitPbufferSurface -> MakeCurrent).
|
|
if (!Server::ServerCreateEGLPbufferSurface(Surf(), 64, 64)) {
|
|
return "ServerCreateEGLPbufferSurface - no usable headless EGL (EGL_PLATFORM=surfaceless, "
|
|
"__EGL_VENDOR_LIBRARY_FILENAMES -> mesa/llvmpipe) on this box";
|
|
}
|
|
return {};
|
|
}
|
|
|
|
Bool MakeCurrent() { return Server::ServerMakeEGLCurrent(Dpy(), Surf(), Surf(), Ctx()); }
|
|
Bool ReleaseCurrent() {
|
|
return Server::ServerMakeEGLCurrent(Dpy(), EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
|
|
}
|
|
|
|
void TearDown() {
|
|
Server::ServerReleaseEGLResources();
|
|
Stop();
|
|
// The backend Stop() just destroyed must not be the session's answer for the next case.
|
|
Server::ServerSessionInstance().SetBackend(nullptr);
|
|
MG_State::pGLContext = Move(savedContext);
|
|
}
|
|
};
|
|
|
|
// FAIL() and GTEST_SKIP() both return from the enclosing void test body.
|
|
#define MGL_EGL_BRING_UP_OR_BAIL(fixture) \
|
|
do { \
|
|
const std::string why = (fixture).BringUp(); \
|
|
if (!why.empty()) { \
|
|
(fixture).Stop(); \
|
|
MG_State::pGLContext = Move((fixture).savedContext); \
|
|
if (RequireGpuFromEnvironment()) { \
|
|
FAIL() << "MOBILEGL_ITEST_REQUIRE_GPU is set, so the server's EGL bring-up failing is " \
|
|
"a failure, not a skip. It failed at: " \
|
|
<< why; \
|
|
} \
|
|
GTEST_SKIP() << "no usable headless EGL for the server backend: " << why; \
|
|
} \
|
|
} while (0)
|
|
|
|
// The production shape of a buffer under split, step by step through the applier's own entry
|
|
// points and the REAL op table (Initialize registered it): resource_create mints the record,
|
|
// resource_respecify stores the descriptor - its bytes never cross under split (table 1 row 19),
|
|
// they arrive as resource_subdata right behind it - and resource_subdata stages bytes into the
|
|
// server shadow, minting the twin if no draw has (ID-52 item 3). `definedContent` is the
|
|
// descriptor's HasDefinedContent: what glBufferData(size, data) sets and glBufferData(size, NULL)
|
|
// clears.
|
|
MG_Pipe::MGPResourceDesc BufferDesc(Uint32 slot, Uint32 width, Bool definedContent) {
|
|
MG_Pipe::MGPResourceDesc desc{};
|
|
desc.Resource = MG_Pipe::MGPipeHandle{slot, 1};
|
|
desc.Target = MG_Pipe::kMGPipeResourceTargetBuffer;
|
|
desc.Width = width;
|
|
desc.Height = 1;
|
|
desc.Depth = 1;
|
|
desc.ArrayLayers = 1;
|
|
desc.Levels = 1;
|
|
desc.Samples = 1;
|
|
desc.Usage = static_cast<Uint32>(BufferUsage::StaticDraw);
|
|
desc.HasDefinedContent = definedContent ? 1 : 0;
|
|
return desc;
|
|
}
|
|
|
|
MG_Pipe::MGPipeHandle DeclareBuffer(const MG_Pipe::MGPResourceDesc& desc) {
|
|
EXPECT_TRUE(MG_Pipe::MGPipeApplyResourceCreate(desc)) << "resource_create refused slot " << desc.Resource.Slot;
|
|
EXPECT_TRUE(MG_Pipe::MGPipeApplyResourceRespecify(desc, nullptr)) << "resource_respecify refused";
|
|
return desc.Resource;
|
|
}
|
|
|
|
void StageBytes(MG_Pipe::MGPipeHandle res, SizeT offset, SizeT size, Uint8 fill) {
|
|
const MG_Pipe::MGPipeResourceOps* ops = MG_Pipe::MGPipeGetResourceOps();
|
|
ASSERT_NE(ops, nullptr) << "no resource op table is registered";
|
|
ASSERT_NE(ops->SubData, nullptr);
|
|
Vector<Uint8> bytes(size, fill);
|
|
MG_Pipe::MGPSubData rec{};
|
|
rec.Res = res;
|
|
rec.Target = MG_Pipe::MGPipePackSubDataTarget(MG_Pipe::kMGPipeResourceTargetBuffer, 0u);
|
|
ASSERT_TRUE(MG_Pipe::MGPipeSetSubDataBufferRange(rec, offset, size));
|
|
ops->SubData(res, rec, bytes.data());
|
|
}
|
|
|
|
MG_Backend::DirectGLES::BufferImpl::GLESBufferResource* Ensure(MG_Pipe::MGPipeHandle res) {
|
|
return MG_Backend::DirectGLES::BufferImpl::EnsureBufferResourceForHandle({}, res);
|
|
}
|
|
|
|
MG_Backend::DirectGLES::BufferImpl::GLESBufferResource* Twin(MG_Pipe::MGPipeHandle res) {
|
|
return MG_Backend::DirectGLES::BufferImpl::FindBufferResourceForHandle(res);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
// C7 / ID-54, THE NATIVE HALF, measured where the review said it was not: at the driver. Surface
|
|
// creation binds the context natively on the apply thread (that is bind #1 of the "2 per process"
|
|
// the review counted); the client's first eglMakeCurrent onto that surface used to be bind #2 and is
|
|
// now deduped by BackendObject_DirectGLES's ID-54 arm; an identical repeat is a forwarder-level
|
|
// RepeatNoOp; a client release-current is recorded and never reaches the driver; a bind after the
|
|
// release is a RepeatNoOp again because the driver never lost the context. ONE native bind, ZERO
|
|
// native releases, and the apply thread is still the owner at the end. Red once, three ways: (a) make
|
|
// ApplyMakeCurrent ignore the classification (the repeat is forwarded and the release reaches the
|
|
// driver), (b) make the backend's native call unconditional again (2 native binds), (c) forward the
|
|
// ClientRelease arm to the backend (1 native release).
|
|
TEST(ServerLoopEglTest, MakeCurrentBindsNativelyOncePerContextAndNeverForwardsAClientRelease) {
|
|
EglServerFixture fixture;
|
|
MGL_EGL_BRING_UP_OR_BAIL(fixture);
|
|
Server::ServerLoop& loop = Server::ServerLoopInstance();
|
|
|
|
ASSERT_EQ(g_nativeEglBinds.load(), 1) << "the surface's own creation did not bind natively exactly once";
|
|
ASSERT_EQ(loop.NativeBindCount(), 0u);
|
|
|
|
ASSERT_TRUE(fixture.MakeCurrent());
|
|
EXPECT_EQ(g_nativeEglBinds.load(), 1)
|
|
<< "the client's first make-current cost a second native eglMakeCurrent on a surface its own "
|
|
"creation had already made current on this thread (ID-54: bind once per tuple)";
|
|
EXPECT_EQ(loop.NativeBindCount(), 1u) << "the first make-current was not forwarded at all";
|
|
|
|
ASSERT_TRUE(fixture.MakeCurrent());
|
|
EXPECT_EQ(loop.NativeBindCount(), 1u)
|
|
<< "an identical repeat make-current was forwarded as a native bind; the owner slot would be "
|
|
"written again and the caches invalidated for nothing";
|
|
EXPECT_EQ(g_nativeEglBinds.load(), 1);
|
|
|
|
ASSERT_TRUE(fixture.ReleaseCurrent());
|
|
EXPECT_EQ(loop.ClientReleaseCount(), 1u) << "the client release-current was not recorded";
|
|
EXPECT_EQ(g_nativeEglReleases.load(), 0)
|
|
<< "a client release-current reached the driver as eglMakeCurrent(NO_CONTEXT): the apply thread "
|
|
"lost the context it holds for life (ID-54: never unbind on a client release)";
|
|
|
|
ASSERT_TRUE(fixture.MakeCurrent());
|
|
EXPECT_EQ(loop.NativeBindCount(), 1u) << "a bind after the recorded, unforwarded release was forwarded again";
|
|
EXPECT_EQ(g_nativeEglBinds.load(), 1);
|
|
|
|
// The owner is still the apply thread by EGL ground truth (IsBackendContextCurrentOnThisThread
|
|
// re-verifies against eglGetCurrentContext), and not this thread.
|
|
Bool ownerIsApplyThread = false;
|
|
ASSERT_EQ(OnApply([&] { ownerIsApplyThread = MG_Backend::DirectGLES::IsBackendContextCurrentOnThisThread(); }),
|
|
MOBILEGL_OK);
|
|
EXPECT_TRUE(ownerIsApplyThread) << "after the sequence the apply thread no longer owns the context";
|
|
EXPECT_FALSE(MG_Backend::DirectGLES::IsBackendContextCurrentOnThisThread()) << "the app thread owns the context";
|
|
|
|
fixture.TearDown();
|
|
}
|
|
|
|
// ID-67: an IDENTICAL tuple is a native no-op AND publishes no caps snapshot (the client's mirror
|
|
// generation must not move, nothing accumulates); a DIFFERENT tuple - a second MobileGL context onto
|
|
// the same surface, the same driver triple - is a real native bind AND a republish (R-12 arm (a)),
|
|
// so the client adopts it without a pump or a Present. Both halves measured: native binds at the EGL
|
|
// table, republishes at ServerMakeEGLCurrent's own tally. Red once, three ways: make the backend's
|
|
// skip answer "same tuple" for any tuple (the different tuple stays at 1 native bind), republish on
|
|
// a repeat (the repeat reads 2 republishes), republish only on the first bind (the different tuple
|
|
// reads 1).
|
|
TEST(ServerLoopEglTest, ADifferentTupleBindsNativelyAndRepublishesAnIdenticalRepeatDoesNeither) {
|
|
EglServerFixture fixture;
|
|
MGL_EGL_BRING_UP_OR_BAIL(fixture);
|
|
Server::ServerLoop& loop = Server::ServerLoopInstance();
|
|
const EGLDisplay dpy = EglServerFixture::Dpy();
|
|
const EGLSurface surf = EglServerFixture::Surf();
|
|
const EGLContext ctxA = EglServerFixture::Ctx();
|
|
const EGLContext ctxB = reinterpret_cast<EGLContext>(static_cast<std::uintptr_t>(0x2));
|
|
ASSERT_EQ(g_nativeEglBinds.load(), 1) << "the surface's own creation did not bind natively exactly once";
|
|
ASSERT_EQ(loop.MakeCurrentRepublishCount(), 0u);
|
|
|
|
// The first tuple adopts the creation's bind, and republishes (InitCapabilities has now run).
|
|
ASSERT_TRUE(Server::ServerMakeEGLCurrent(dpy, surf, surf, ctxA));
|
|
EXPECT_EQ(g_nativeEglBinds.load(), 1);
|
|
EXPECT_EQ(loop.MakeCurrentRepublishCount(), 1u)
|
|
<< "the first make-current did not republish the caps snapshot (R-12 arm (a))";
|
|
|
|
// Identical: 0 native binds, 0 republishes.
|
|
ASSERT_TRUE(Server::ServerMakeEGLCurrent(dpy, surf, surf, ctxA));
|
|
EXPECT_EQ(g_nativeEglBinds.load(), 1) << "an identical repeat bound natively";
|
|
EXPECT_EQ(loop.MakeCurrentRepublishCount(), 1u)
|
|
<< "an identical repeat republished the caps snapshot: the client's mirror generation moved "
|
|
"for nothing and unpumped snapshots accumulate (ID-67)";
|
|
|
|
// Different (a second context onto the same surface): 1 native bind, 1 republish.
|
|
ASSERT_TRUE(Server::ServerMakeEGLCurrent(dpy, surf, surf, ctxB));
|
|
EXPECT_EQ(g_nativeEglBinds.load(), 2)
|
|
<< "a make-current with a DIFFERENT tuple (a second context onto the same surface) did not "
|
|
"bind natively: DirectGLES's invalidations for the new frontend context never ran (ID-67)";
|
|
EXPECT_EQ(loop.NativeBindCount(), 2u);
|
|
EXPECT_EQ(loop.MakeCurrentRepublishCount(), 2u)
|
|
<< "a make-current with a different tuple did not republish the caps snapshot; the client "
|
|
"would adopt nothing without a pump or a Present (R-12 arm (a), ID-67)";
|
|
|
|
// Identical again, then back to the first: 0 + 0, then 1 + 1.
|
|
ASSERT_TRUE(Server::ServerMakeEGLCurrent(dpy, surf, surf, ctxB));
|
|
EXPECT_EQ(g_nativeEglBinds.load(), 2);
|
|
EXPECT_EQ(loop.MakeCurrentRepublishCount(), 2u);
|
|
ASSERT_TRUE(Server::ServerMakeEGLCurrent(dpy, surf, surf, ctxA));
|
|
EXPECT_EQ(g_nativeEglBinds.load(), 3);
|
|
EXPECT_EQ(loop.NativeBindCount(), 3u);
|
|
EXPECT_EQ(loop.MakeCurrentRepublishCount(), 3u);
|
|
|
|
fixture.TearDown();
|
|
}
|
|
|
|
// N-3: a destroy-recreate with the SAME handle values must be a real bind again. Before the fix the
|
|
// tuple survived ServerReleaseEGLResources, the recreated context's first make-current classified as
|
|
// a RepeatNoOp, nothing ran in the base class (no InitCapabilities, no current-thread record) and no
|
|
// caps were republished - a silent no-context-current from the forwarder's point of view, visible
|
|
// only as the next swap failing. Red once by deleting the ForgetCurrentTuple() calls the forwarders
|
|
// make: NativeBindCount() stays 1 and the swap after the re-create fails.
|
|
TEST(ServerLoopEglTest, ADestroyedContextForgetsTheTupleSoTheSameHandleValuesBindAgain) {
|
|
EglServerFixture fixture;
|
|
MGL_EGL_BRING_UP_OR_BAIL(fixture);
|
|
Server::ServerLoop& loop = Server::ServerLoopInstance();
|
|
ASSERT_TRUE(fixture.MakeCurrent());
|
|
ASSERT_EQ(loop.NativeBindCount(), 1u);
|
|
ASSERT_TRUE(Server::ServerSwapEGLBuffers(EglServerFixture::Dpy(), EglServerFixture::Surf()))
|
|
<< "the swap BEFORE the destroy failed, so the case could not tell lost bookkeeping from a swap "
|
|
"that never worked";
|
|
ASSERT_EQ(g_nativeEglBinds.load(), 1);
|
|
|
|
// The context goes (DestroyEGLContext on the apply thread) and comes back under the same
|
|
// handle values.
|
|
Server::ServerReleaseEGLResources();
|
|
EGLint major = 0;
|
|
EGLint minor = 0;
|
|
ASSERT_TRUE(Server::ServerInitializeEGLDisplay(EglServerFixture::Dpy(), &major, &minor));
|
|
ASSERT_TRUE(Server::ServerCreateEGLPbufferSurface(EglServerFixture::Surf(), 64, 64));
|
|
ASSERT_EQ(g_nativeEglBinds.load(), 2) << "the recreated surface did not bind natively";
|
|
|
|
ASSERT_TRUE(fixture.MakeCurrent());
|
|
EXPECT_EQ(loop.NativeBindCount(), 2u)
|
|
<< "the make-current after a destroy-recreate with the same handle values was classified as a "
|
|
"RepeatNoOp: the tuple outlived the context it named (N-3), so no base-class bookkeeping "
|
|
"ran and the caps snapshot was not republished";
|
|
EXPECT_TRUE(Server::ServerSwapEGLBuffers(EglServerFixture::Dpy(), EglServerFixture::Surf()))
|
|
<< "the swap after the re-create failed: BackendObject::SwapEGLBuffers found no current-thread "
|
|
"record, because the make-current that should have made one was treated as a repeat";
|
|
EXPECT_EQ(g_nativeEglBinds.load(), 2) << "one native bind per context lifetime, not more";
|
|
|
|
fixture.TearDown();
|
|
}
|
|
|
|
// ID-49's tight-size half, gated (review v2 N-5: "nothing, unit and joint"). The client's DstSize is
|
|
// deliberately WRONG - 80, the size a ROW_LENGTH=8 / SKIP_* client would compute for a 4x3 RGBA8
|
|
// read whose tight extent is 48 - and the reply must still be the tight 48 bytes: posted at 48,
|
|
// counted at 48, read into a scratch that grew to 48 and never to the client's 80, and stamped 48 in
|
|
// the slot header the client reads. Red once, three ways: post at info.DstSize, size the scratch from
|
|
// info.DstSize, compute `tight` from info.DstSize.
|
|
TEST(ServerLoopEglTest, AReadPixelsReplyIsTheTightExtentWhateverDstSizeTheClientSent) {
|
|
EglServerFixture fixture;
|
|
MGL_EGL_BRING_UP_OR_BAIL(fixture);
|
|
ASSERT_TRUE(fixture.MakeCurrent());
|
|
|
|
// THE CLIENT'S HALF OF THE VERB, exactly as glReadPixels runs it before it reaches a backend
|
|
// (MGP_FILL(ReadPixels) in MG_Impl): the validate point fills gPipeInputs' residual fields
|
|
// from the frontend context - the texture-unit base, the bound framebuffers, the pack state
|
|
// - which the server's ReadPixels reads BARRIER-PULLED under R-1 while the client is parked
|
|
// in the barrier (PipeInputs.h's class table). Without it the apply thread reads a block
|
|
// nobody filled and walks a null texture-unit base; with it the case drives the same two
|
|
// halves the inproc lane drives, in the same order, on the same process-wide block.
|
|
MG_Pipe::MGPipeValidateForVerb(MG_Pipe::MGPipeVerb::ReadPixels);
|
|
|
|
MG_Pipe::MGPReadbackInfo info{};
|
|
info.Res = MG_Pipe::kMGPipeNullHandle; // read_pixels: the bound read surface answers
|
|
info.Box = MG_Pipe::MGPBox{0, 0, 0, 4, 3, 1};
|
|
info.Format = 0x1908; // GL_RGBA
|
|
info.Type = 0x1401; // GL_UNSIGNED_BYTE
|
|
info.DstSize = 80; // wrong on purpose; tight is 4 * 3 * 4 = 48
|
|
const Uint64 seq = fixture.encoder.EncodeRecord(MG_Pipe::MGPWireOp::ReadPixels, &info, sizeof(info));
|
|
ASSERT_NE(seq, Codec::kInvalidSeq);
|
|
fixture.encoder.Publish();
|
|
fixture.producer.PublishAndNotify(seq);
|
|
ASSERT_EQ(fixture.producer.WaitForApplied(seq, 5000), Transport::SessionWait::Reached);
|
|
|
|
const Server::ServerVerbSink& verbs = fixture.session->Applier().Verbs();
|
|
ASSERT_EQ(verbs.Readbacks(), 1u) << "the read_pixels record was declined or errored rather than applied";
|
|
EXPECT_EQ(verbs.ReadbackBytes(), 48u)
|
|
<< "the reply was posted at the client's DstSize (80), not the tight w*h*bpp extent (48) (ID-49)";
|
|
EXPECT_EQ(verbs.ReadbackScratchBytes(), 48u)
|
|
<< "the scratch was sized from the client's DstSize rather than the tight extent; a DstSize "
|
|
"smaller than tight would then be the heap overflow codex 1 found";
|
|
|
|
// And through the client's own view of the slot: the seq stamped back, OK, 48 bytes.
|
|
Transport::ReplySlotPool replies(fixture.clientSegments.ReplyBase(), fixture.clientSegments.ReplyBytes(),
|
|
fixture.clientSegments.ReplySlotCount());
|
|
ASSERT_TRUE(replies.Valid());
|
|
Vector<Uint8> pixels(128, 0);
|
|
Int32 status = -1;
|
|
Uint64 size = 0;
|
|
EXPECT_TRUE(replies.Read(seq, pixels.data(), pixels.size(), &status, &size));
|
|
EXPECT_EQ(status, static_cast<Int32>(Transport::kReplyStatusOk));
|
|
EXPECT_EQ(size, 48u) << "the slot header carries the client's DstSize, not the tight extent";
|
|
|
|
fixture.TearDown();
|
|
}
|
|
|
|
// M-3 / codex 4, the POOL-REUSE reader (review v2 item 4: "nothing can notice it going away"). The
|
|
// production sequence, on the real op table and the real backend: A - glBufferData(64, data), drawn
|
|
// (ensured), then deleted on the apply thread, so its id retires into the buffer pool; two presents
|
|
// with a finish between them move the frame-completion watermark past the id's retire serial, which
|
|
// is the pool's hand-out rule. B - glBufferData(64, data2), whose 64 bytes cross as resource_subdata
|
|
// behind the respecify (table 1 row 19) - and only 16 of them arrive, the MISSING-RECORD shape. B's
|
|
// first ensure finds A's id in the pool and would seed the driver's whole 64-byte store from a shadow
|
|
// 48 bytes of which nothing staged. The refusal fires BEFORE the glBufferSubData, so the forked
|
|
// child - whose only thread is a copy of this one and holds no context - reaches it with no GL call
|
|
// that matters (a no-context call dispatches to a no-op). Red once by deleting the pool-reuse
|
|
// MGL_SERVER_STAGED_REQUIRE: the child seeds the store and does not die. (The ORPHANED shape of the
|
|
// same sequence, glBufferData(64, NULL) + 16 bytes, must NOT die: TheStreamingIdiom... below.)
|
|
TEST(StagedShadowProductionTest, ASparseShadowForcedThroughAPoolReuseUploadIsFatalByName) {
|
|
EglServerFixture fixture;
|
|
MGL_EGL_BRING_UP_OR_BAIL(fixture);
|
|
ASSERT_TRUE(fixture.MakeCurrent());
|
|
|
|
const MG_Pipe::MGPipeHandle a = DeclareBuffer(BufferDesc(21, 64, true));
|
|
StageBytes(a, 0, 64, 0x11);
|
|
Bool aEnsured = false;
|
|
ASSERT_EQ(OnApply([&] {
|
|
aEnsured = Ensure(a) != nullptr;
|
|
MG_Pipe::MGPipeGetResourceOps()->Destroy(a);
|
|
for (int frame = 0; frame < 2; ++frame) {
|
|
if (MG_Backend::DirectGLES::g_GLESFuncs.glFinish) MG_Backend::DirectGLES::g_GLESFuncs.glFinish();
|
|
MG_Backend::DirectGLES::Present();
|
|
}
|
|
}),
|
|
MOBILEGL_OK);
|
|
ASSERT_TRUE(aEnsured) << "A's ensure minted no storage, so nothing retired into the pool";
|
|
|
|
const MG_Pipe::MGPipeHandle b = DeclareBuffer(BufferDesc(22, 64, true));
|
|
StageBytes(b, 0, 16, 0x22);
|
|
ASSERT_NE(Twin(b), nullptr) << "the subdata did not mint B's twin (ID-52 item 3)";
|
|
ASSERT_EQ(Twin(b)->id, 0u) << "B already has a store; the pool-reuse arm cannot be reached";
|
|
|
|
EXPECT_EXIT(Ensure(b), ::testing::KilledBySignal(SIGABRT), ".*");
|
|
const std::string log = ReadLog();
|
|
EXPECT_NE(log.find("Fatal{StageSnapshotTooNarrow, \"pool_reuse_whole_store\"}"), std::string::npos)
|
|
<< "the abort was not the pool-reuse reader's coverage refusal (M-3); the log says: " << log;
|
|
|
|
fixture.TearDown();
|
|
}
|
|
|
|
// M-3 / codex 4, the RESPECIFY reader. A - glBufferData(64, data), drawn (ensured: id, storage,
|
|
// serial all current). Then glBufferData(64, data2) again, whose bytes do not cross (table 1 row 19):
|
|
// the record says "defined content", the old shadow is dropped, the store is pending a respecify -
|
|
// and only 16 of the 64 bytes then arrive, staged from the app thread the way the applier queues
|
|
// them off the context thread. The next ensure would RespecifyStorageWith(initialData = the sparse
|
|
// shadow) over the whole store. The refusal fires before that glBufferData. Red once by deleting the
|
|
// respecify MGL_SERVER_STAGED_REQUIRE.
|
|
TEST(StagedShadowProductionTest, ASparseShadowForcedThroughAStorageRespecifyIsFatalByName) {
|
|
EglServerFixture fixture;
|
|
MGL_EGL_BRING_UP_OR_BAIL(fixture);
|
|
ASSERT_TRUE(fixture.MakeCurrent());
|
|
|
|
const MG_Pipe::MGPResourceDesc desc = BufferDesc(23, 64, true);
|
|
const MG_Pipe::MGPipeHandle res = DeclareBuffer(desc);
|
|
StageBytes(res, 0, 64, 0x33);
|
|
Bool ensured = false;
|
|
ASSERT_EQ(OnApply([&] { ensured = Ensure(res) != nullptr; }), MOBILEGL_OK);
|
|
ASSERT_TRUE(ensured);
|
|
ASSERT_NE(Twin(res)->id, 0u);
|
|
|
|
// Off the context thread on purpose (the apply thread is parked): the respecify takes the
|
|
// "cannot touch GL now" arm and leaves the store PENDING, exactly as an emitted record does.
|
|
ASSERT_TRUE(MG_Pipe::MGPipeApplyResourceRespecify(desc, nullptr));
|
|
ASSERT_TRUE(Twin(res)->pendingRespecify) << "the respecify did not leave the store pending";
|
|
StageBytes(res, 0, 16, 0x44);
|
|
|
|
EXPECT_EXIT(Ensure(res), ::testing::KilledBySignal(SIGABRT), ".*");
|
|
const std::string log = ReadLog();
|
|
EXPECT_NE(log.find("Fatal{StageSnapshotTooNarrow, \"respecify_whole_store\"}"), std::string::npos)
|
|
<< "the abort was not the respecify reader's coverage refusal (M-3); the log says: " << log;
|
|
|
|
fixture.TearDown();
|
|
}
|
|
|
|
// M-1 / codex 8, THE COVERAGE HALF (review v2 item 2: "gated nowhere"). glBufferData(64, NULL) +
|
|
// glBufferSubData(0, 16) + a draw: the shadow covers [0, 16) and the NULL respecify seeds nothing, so
|
|
// that is legal. Then a resource_flush_range over [16, 48) with no bytes (that is what the row
|
|
// carries under split, item 12) queues a range NOTHING STAGED, and the next draw-time drain would
|
|
// take it through the three-tier ladder - whose tier 1 is a range-invalidating map that declares
|
|
// the old bytes dead. RequireStagedCoverageForPendingRanges refuses before the drain, by name. Red
|
|
// once by neutering its loop body: the child then dies one tier later, inside UploadRangeFrom's own
|
|
// check, with a DIFFERENT site name - which this assertion refuses - or, on the tier-1 map, not at
|
|
// all.
|
|
TEST(StagedShadowProductionTest, AQueuedRangeOutsideTheStagedCoverageIsFatalAtTheDrainByName) {
|
|
EglServerFixture fixture;
|
|
MGL_EGL_BRING_UP_OR_BAIL(fixture);
|
|
ASSERT_TRUE(fixture.MakeCurrent());
|
|
|
|
const MG_Pipe::MGPipeHandle res = DeclareBuffer(BufferDesc(24, 64, false));
|
|
StageBytes(res, 0, 16, 0x55);
|
|
Bool ensured = false;
|
|
ASSERT_EQ(OnApply([&] { ensured = Ensure(res) != nullptr; }), MOBILEGL_OK);
|
|
ASSERT_TRUE(ensured);
|
|
ASSERT_NE(Twin(res)->id, 0u);
|
|
ASSERT_FALSE(Twin(res)->pendingRespecify);
|
|
|
|
MG_Pipe::MGPFlushRange flush{};
|
|
flush.Res = res;
|
|
flush.Offset = 16;
|
|
flush.Size = 32;
|
|
MG_Pipe::MGPipeGetResourceOps()->FlushRange(res, flush, nullptr); // off the context thread: queued
|
|
ASSERT_FALSE(Twin(res)->pendingRanges.empty()) << "the flush queued nothing, so no drain is owed";
|
|
|
|
EXPECT_EXIT(Ensure(res), ::testing::KilledBySignal(SIGABRT), ".*");
|
|
const std::string log = ReadLog();
|
|
EXPECT_NE(log.find("Fatal{StageSnapshotTooNarrow, \"ensure_flush_pending\"}"), std::string::npos)
|
|
<< "the abort was not the pending-coverage clamp's (M-1, RequireStagedCoverageForPendingRanges); "
|
|
"the log says: " << log;
|
|
|
|
fixture.TearDown();
|
|
}
|
|
|
|
// m-5 / codex 5 (review v2 item 5: "the shipped control cannot reach the freed base"). This one can:
|
|
// glBufferData(64, data) drawn (the twin's hostBytes names the server shadow), then the context is
|
|
// lost (ServerReleaseEGLResources -> DestroyEGLContext -> OnBackendContextDestroyed -> DropAll frees
|
|
// every shadow; the twin table survives), then the context comes back and the twin is re-armed at
|
|
// its next ensure. The re-armed twin's base must be NULL. Without the null it still names the freed
|
|
// allocation and the whole-store respecify that follows reads it - the use-after-free ASan would
|
|
// name, asserted here as the dangling pointer itself. Red once by deleting the null.
|
|
TEST(StagedShadowProductionTest, ATwinSurvivingContextLossDropsItsFreedShadowBase) {
|
|
EglServerFixture fixture;
|
|
MGL_EGL_BRING_UP_OR_BAIL(fixture);
|
|
ASSERT_TRUE(fixture.MakeCurrent());
|
|
|
|
const MG_Pipe::MGPipeHandle res = DeclareBuffer(BufferDesc(25, 64, true));
|
|
StageBytes(res, 0, 64, 0x66);
|
|
Bool ensured = false;
|
|
ASSERT_EQ(OnApply([&] { ensured = Ensure(res) != nullptr; }), MOBILEGL_OK);
|
|
ASSERT_TRUE(ensured);
|
|
ASSERT_NE(Twin(res)->hostBytes, nullptr) << "the drawn twin names no shadow, so there is nothing to free";
|
|
|
|
Server::ServerReleaseEGLResources();
|
|
ASSERT_NE(Twin(res), nullptr) << "the twin did not survive context loss; the m-5 hazard needs a survivor";
|
|
|
|
EGLint major = 0;
|
|
EGLint minor = 0;
|
|
ASSERT_TRUE(Server::ServerInitializeEGLDisplay(EglServerFixture::Dpy(), &major, &minor));
|
|
ASSERT_TRUE(Server::ServerCreateEGLPbufferSurface(EglServerFixture::Surf(), 64, 64));
|
|
ASSERT_TRUE(fixture.MakeCurrent());
|
|
ASSERT_EQ(OnApply([&] { ensured = Ensure(res) != nullptr; }), MOBILEGL_OK);
|
|
ASSERT_TRUE(ensured);
|
|
|
|
EXPECT_EQ(Twin(res)->hostBytes, nullptr)
|
|
<< "a twin that survived context loss still names the freed server shadow (m-5): the "
|
|
"whole-store respecify that re-armed it read freed memory (ASan: heap-use-after-free at "
|
|
"RespecifyStorageWith)";
|
|
|
|
fixture.TearDown();
|
|
}
|
|
|
|
// M-2 / ID-52 item 3, SHIPPED AND EXECUTED (joint report joint-v1.md 3, "Audit and its R-16
|
|
// limit": the corrupt-staged-bytes perturbation existed only by hand). Under an active transport
|
|
// the server's staged copy is the draw's ONLY base; the frontend object's MappedData() - which under
|
|
// inproc is the same process's memory and always non-null - may not be read. So: a real frontend
|
|
// BufferObject whose shadow holds pattern A, a server shadow staged with pattern B through the real
|
|
// op table (what resource_subdata delivers), and the production ensure path given BOTH. The driver
|
|
// store must hold B. That is the executable form of "corrupt only the staged bytes and the draw
|
|
// shows it": B is the corruption, the upload is the draw, the mapped read-back is the picture. Red
|
|
// once by restoring the MappedData() fallback in liveHostBase(): the store then holds A, the
|
|
// client's bytes, and the R-2.5 audit could never reach a draw again.
|
|
TEST(StagedShadowProductionTest, TheEnsurePathUploadsTheServerShadowNotTheClientObjectsBytes) {
|
|
EglServerFixture fixture;
|
|
MGL_EGL_BRING_UP_OR_BAIL(fixture);
|
|
ASSERT_TRUE(fixture.MakeCurrent());
|
|
|
|
// The frontend object: glBufferData(64, A). Its shadow is A and it declares defined content.
|
|
Vector<Uint8> clientBytes(64, Uint8{0xA5});
|
|
auto buffer = MakeShared<MG_State::GLState::BufferObject>(31u);
|
|
buffer->Respecify(clientBytes.size(), clientBytes.data());
|
|
ASSERT_NE(buffer->MappedData(), nullptr);
|
|
ASSERT_EQ(buffer->MappedData()[0], 0xA5);
|
|
ASSERT_TRUE(buffer->HasDefinedContent());
|
|
|
|
// The server shadow for the twin the draw will use: B, staged the way the wire delivers it.
|
|
const MG_Pipe::MGPipeHandle res = DeclareBuffer(BufferDesc(26, 64, true));
|
|
StageBytes(res, 0, 64, 0x5B);
|
|
|
|
// The production ensure, WITH the frontend object (the draw-time shape), on the apply thread;
|
|
// then the driver's store read back through a READ map on the same thread.
|
|
Vector<Uint8> store(64, 0);
|
|
Bool ensured = false;
|
|
Bool mapped = false;
|
|
ASSERT_EQ(OnApply([&] {
|
|
ensured = MG_Backend::DirectGLES::BufferImpl::EnsureBufferResourceForHandle(buffer, res) != nullptr;
|
|
auto* twin = Twin(res);
|
|
if (!ensured || twin == nullptr || twin->id == 0) return;
|
|
const auto& gl = MG_Backend::DirectGLES::g_GLESFuncs;
|
|
gl.glBindBuffer(GL_ARRAY_BUFFER, twin->id);
|
|
void* view = gl.glMapBufferRange(GL_ARRAY_BUFFER, 0, 64, GL_MAP_READ_BIT);
|
|
if (view != nullptr) {
|
|
std::memcpy(store.data(), view, 64);
|
|
gl.glUnmapBuffer(GL_ARRAY_BUFFER);
|
|
mapped = true;
|
|
}
|
|
}),
|
|
MOBILEGL_OK);
|
|
ASSERT_TRUE(ensured);
|
|
ASSERT_TRUE(mapped) << "the driver store could not be mapped for reading, so the case cannot see the upload";
|
|
|
|
EXPECT_EQ(store[0], 0x5B)
|
|
<< "the ensure path uploaded the CLIENT object's bytes (MappedData(), pattern A = 0xA5) rather "
|
|
"than the server shadow (pattern B = 0x5B): under an active transport the staged copy must "
|
|
"be the draw's only base (M-2 / ID-52 item 3), or a corrupt staged upload renders the "
|
|
"frontend's correct bytes and the R-2.5 audit cannot reach a draw";
|
|
EXPECT_EQ(store[63], 0x5B);
|
|
// And the frontend object was not written through: its shadow is still A.
|
|
EXPECT_EQ(buffer->MappedData()[0], 0xA5);
|
|
|
|
fixture.TearDown();
|
|
}
|
|
|
|
namespace {
|
|
std::string ReadLogFrom(SizeT offset) {
|
|
const std::string whole = ReadLog();
|
|
return offset < whole.size() ? whole.substr(offset) : std::string();
|
|
}
|
|
|
|
// THE STREAMING IDIOM, end to end, in a FORKED CHILD that brings the server up itself (the
|
|
// integration harness's pre-flight shape): a Fatal on the apply thread ends the child, not the
|
|
// case, so the case can name it. glBufferData(64, NULL) then glBufferSubData(0, 16) then a draw:
|
|
// the frontend object ORPHANS its store and writes 16 bytes - which flips ITS HasDefinedContent
|
|
// to true - while on the wire that is a resource_respecify with HasDefinedContent CLEAR and one
|
|
// 16-byte resource_subdata, so the server shadow covers [0, 16) of 64 and the descriptor says
|
|
// the rest is undefined by the application's own declaration. The draw's whole-store upload
|
|
// (the respecify reader, or the pool-reuse reader when a 64-byte id is waiting in the pool)
|
|
// must go through and leave 16 bytes of pattern and 48 of zero in the driver's store. Exit 0
|
|
// when it did, 7 when the wrong bytes landed, 8 when the child's bring-up failed.
|
|
enum class IdiomArm { Respecify, PoolReuse };
|
|
|
|
[[noreturn]] void RunTheStreamingIdiomAndExit(IdiomArm arm) {
|
|
EglServerFixture fixture;
|
|
if (!fixture.BringUp().empty() || !fixture.MakeCurrent()) ::_exit(8);
|
|
Vector<Uint8> sixteen(16, Uint8{0x77});
|
|
auto buffer = MakeShared<MG_State::GLState::BufferObject>(arm == IdiomArm::PoolReuse ? 33u : 32u);
|
|
buffer->Respecify(64, nullptr);
|
|
buffer->UploadSubData(DataPtr{sixteen.data(), sixteen.size()}, 0);
|
|
Bool ok = false;
|
|
(void)OnApply([&] {
|
|
if (arm == IdiomArm::PoolReuse) {
|
|
const MG_Pipe::MGPipeHandle a = DeclareBuffer(BufferDesc(29, 64, true));
|
|
StageBytes(a, 0, 64, 0x11);
|
|
if (Ensure(a) == nullptr) return;
|
|
MG_Pipe::MGPipeGetResourceOps()->Destroy(a);
|
|
for (int frame = 0; frame < 2; ++frame) {
|
|
if (MG_Backend::DirectGLES::g_GLESFuncs.glFinish) MG_Backend::DirectGLES::g_GLESFuncs.glFinish();
|
|
MG_Backend::DirectGLES::Present();
|
|
}
|
|
}
|
|
const Uint32 slot = arm == IdiomArm::PoolReuse ? 30u : 28u;
|
|
const MG_Pipe::MGPipeHandle b = DeclareBuffer(BufferDesc(slot, 64, false));
|
|
StageBytes(b, 0, 16, 0x77);
|
|
auto* twin = MG_Backend::DirectGLES::BufferImpl::EnsureBufferResourceForHandle(buffer, b); // the draw
|
|
if (twin == nullptr || twin->id == 0) return;
|
|
const auto& gl = MG_Backend::DirectGLES::g_GLESFuncs;
|
|
gl.glBindBuffer(GL_ARRAY_BUFFER, twin->id);
|
|
const auto* view = static_cast<const Uint8*>(gl.glMapBufferRange(GL_ARRAY_BUFFER, 0, 64, GL_MAP_READ_BIT));
|
|
if (view == nullptr) return;
|
|
ok = view[0] == 0x77 && view[15] == 0x77 && view[16] == 0 && view[63] == 0;
|
|
gl.glUnmapBuffer(GL_ARRAY_BUFFER);
|
|
});
|
|
::_exit(ok ? 0 : 7);
|
|
}
|
|
} // namespace
|
|
|
|
// M-3, ROUND 3's RULE, positive direction (the round-2 refusal aborted six LargeArenaAdoption /
|
|
// ResourceSubsystemControl entries on the joint by this exact name): the ordinary streaming idiom
|
|
// through the RESPECIFY reader is uploaded, not refused. Red once by making the refusal
|
|
// unconditional again (the descriptor's HasDefinedContent ignored): the child dies of
|
|
// Fatal{StageSnapshotTooNarrow, "respecify_whole_store"} and the appended log names it.
|
|
TEST(StagedShadowProductionTest, TheStreamingIdiomOrphanThenPartialSubDataIsUploadedNotRefused) {
|
|
{
|
|
EglServerFixture probe;
|
|
MGL_EGL_BRING_UP_OR_BAIL(probe);
|
|
probe.TearDown();
|
|
}
|
|
const SizeT mark = ReadLog().size();
|
|
EXPECT_EXIT(RunTheStreamingIdiomAndExit(IdiomArm::Respecify), ::testing::ExitedWithCode(0), ".*")
|
|
<< "the streaming idiom - glBufferData(64, NULL), glBufferSubData(0, 16), draw - did not put "
|
|
"its 16 bytes and 48 undefined (zero) bytes into the driver's store through the respecify "
|
|
"reader: exit 7 is the wrong bytes, a signal is the whole-store refusal firing on a store "
|
|
"the application itself orphaned";
|
|
const std::string appended = ReadLogFrom(mark);
|
|
EXPECT_EQ(appended.find("Fatal{StageSnapshotTooNarrow"), std::string::npos)
|
|
<< "the orphan-then-partial-subdata idiom was refused by name (M-3's whole-store refusal must "
|
|
"key on the descriptor's HasDefinedContent, not on the frontend flag a partial subdata "
|
|
"flips); the log says: " << appended;
|
|
}
|
|
|
|
// The same idiom through the POOL-REUSE reader: a 64-byte id retired to the pool by a previous
|
|
// buffer's delete, handed to the orphaned store's first draw, seeded from the sparse shadow.
|
|
TEST(StagedShadowProductionTest, TheStreamingIdiomThroughAPoolReuseIsUploadedNotRefused) {
|
|
{
|
|
EglServerFixture probe;
|
|
MGL_EGL_BRING_UP_OR_BAIL(probe);
|
|
probe.TearDown();
|
|
}
|
|
const SizeT mark = ReadLog().size();
|
|
EXPECT_EXIT(RunTheStreamingIdiomAndExit(IdiomArm::PoolReuse), ::testing::ExitedWithCode(0), ".*")
|
|
<< "the streaming idiom through a recycled pool id did not put its 16 bytes and 48 undefined "
|
|
"(zero) bytes into the driver's store: exit 7 is the wrong bytes, a signal is the "
|
|
"whole-store refusal firing on a store the application itself orphaned";
|
|
const std::string appended = ReadLogFrom(mark);
|
|
EXPECT_EQ(appended.find("Fatal{StageSnapshotTooNarrow"), std::string::npos)
|
|
<< "the orphan-then-partial-subdata idiom through a pool reuse was refused by name (M-3's "
|
|
"whole-store refusal must key on the descriptor's HasDefinedContent); the log says: "
|
|
<< appended;
|
|
}
|
|
#endif // !_WIN32
|
|
|
|
// =====================================================================================
|
|
// P5b d1: a draw_vbo record of every d1 shape crosses to the REAL ServerVerbSink with its
|
|
// fields intact (MG_Remote/CONTRACT-P5B.md §2 d1). No backend object lives in this process,
|
|
// so each record is DECLINED after the sink has witnessed it - which is exactly the point:
|
|
// the witness is taken before the backend is consulted, so these cases pin the wire's fields
|
|
// and not a backend call. The emitter half (GL args -> these fields) is RemoteClientTest's
|
|
// PlanDraw* cases; the backend half is the DirectGLES.Split. IndexedDrawFamilyScenario lane.
|
|
// =====================================================================================
|
|
|
|
namespace {
|
|
MG_Pipe::MGPipeHandle D1Handle(Uint32 slot) {
|
|
MG_Pipe::MGPipeHandle h{};
|
|
h.Slot = slot;
|
|
h.Gen = 1;
|
|
return h;
|
|
}
|
|
MG_Pipe::MGPDrawInfo D1DrawInfo(Uint8 indexSize, Uint32 numDraws) {
|
|
MG_Pipe::MGPDrawInfo info{};
|
|
info.Mode = 0x0004; // GL_TRIANGLES
|
|
info.IndexSize = indexSize;
|
|
info.InstanceCount = 1;
|
|
info.MinIndex = ~0u;
|
|
info.MaxIndex = ~0u;
|
|
info.NumDraws = numDraws;
|
|
return info;
|
|
}
|
|
} // namespace
|
|
|
|
// Red once by: dropping `m_lastDraw.Info = info;` from OnDrawVbo - InstanceCount reads 1 and
|
|
// IndexBias reads 0 below.
|
|
TEST(ServerLoopTest, AnInstancedBaseVertexDrawRecordReachesTheSinkWithItsFieldsIntact) {
|
|
ServerFixture fixture;
|
|
ASSERT_TRUE(fixture.Handshake());
|
|
ASSERT_TRUE(fixture.StartLoop());
|
|
|
|
MG_Pipe::MGPDrawInfo info = D1DrawInfo(/*indexSize=*/2, /*numDraws=*/1);
|
|
info.InstanceCount = 7; // DrawElementsInstancedBaseVertex(..., 7, 5): the Minecraft slot
|
|
info.IndexResource = D1Handle(31);
|
|
const MG_Pipe::MGPDrawRange range{/*Start=*/12, /*Count=*/36, /*IndexBias=*/5};
|
|
ASSERT_TRUE(fixture.EmitAndWaitWithTail(MG_Pipe::MGPWireOp::DrawVbo, &info, sizeof(info), &range,
|
|
sizeof(range)));
|
|
|
|
const Server::ServerVerbSink& verbs = fixture.session->Applier().Verbs();
|
|
EXPECT_EQ(verbs.DrawRecords(), 1u) << "the record did not reach OnDrawVbo";
|
|
EXPECT_EQ(verbs.Draws(), 0u) << "no backend object lives here, so the draw must be DECLINED "
|
|
"after the witness, never applied";
|
|
const Server::ServerVerbSink::LastDrawRecord& seen = verbs.LastDraw();
|
|
EXPECT_EQ(seen.Info.IndexSize, 2u);
|
|
EXPECT_EQ(seen.Info.InstanceCount, 7u);
|
|
EXPECT_EQ(seen.Info.IndexResource.Slot, 31u);
|
|
EXPECT_EQ(seen.FirstRange.Start, 12u);
|
|
EXPECT_EQ(seen.FirstRange.Count, 36u);
|
|
EXPECT_EQ(seen.FirstRange.IndexBias, 5);
|
|
EXPECT_FALSE(seen.HadUserIndices);
|
|
EXPECT_FALSE(seen.HadIndirect);
|
|
EXPECT_FALSE(MG_Pipe::gPipeInputs.ServerStampedVerb());
|
|
|
|
fixture.Stop();
|
|
}
|
|
|
|
// Red once by: setting `m_lastDraw.UserIndexBytes = 0` in OnDrawVbo's span arm - the byte
|
|
// count below reads 0 while HadUserIndices stays true.
|
|
TEST(ServerLoopTest, AClientIndexArrayCrossesAsAStagedSpanAndTheSinkSeesItsByteCount) {
|
|
ServerFixture fixture;
|
|
ASSERT_TRUE(fixture.Handshake());
|
|
ASSERT_TRUE(fixture.StartLoop());
|
|
|
|
// d1's rule for a client index array: the CLIENT stages count * IndexSize bytes and the
|
|
// record names the run (Ptr = nullptr, SEG_STAGE); the sink resolves it for the call only.
|
|
const Uint32 indices[6] = {0, 1, 2, 2, 3, 0};
|
|
const MG_Pipe::MGPBlobRef staged = fixture.encoder.StageBytes(indices, sizeof(indices));
|
|
MG_Pipe::MGHostSpan span{};
|
|
span.Ptr = nullptr;
|
|
span.Seg = staged.Seg;
|
|
span.Offset = staged.Offset;
|
|
span.Size = staged.Size;
|
|
|
|
MG_Pipe::MGPDrawInfo info = D1DrawInfo(/*indexSize=*/4, /*numDraws=*/1);
|
|
info.Flags = MG_Pipe::kDrawHasUserIndices;
|
|
const MG_Pipe::MGPDrawRange range{0, 6, 0};
|
|
const Codec::WireTail tails[2] = {{&range, sizeof(range)}, {&span, sizeof(span)}};
|
|
const Uint64 seq = fixture.encoder.EncodeRecord(MG_Pipe::MGPWireOp::DrawVbo, &info, sizeof(info),
|
|
tails, 2);
|
|
ASSERT_NE(seq, Codec::kInvalidSeq);
|
|
fixture.encoder.Publish();
|
|
fixture.producer.PublishAndNotify(seq);
|
|
ASSERT_EQ(fixture.producer.WaitForApplied(seq, 5000), Transport::SessionWait::Reached);
|
|
|
|
const Server::ServerVerbSink::LastDrawRecord& seen = fixture.session->Applier().Verbs().LastDraw();
|
|
EXPECT_TRUE(seen.HadUserIndices) << "the span did not reach OnDrawVbo";
|
|
EXPECT_EQ(seen.UserIndexBytes, sizeof(indices));
|
|
EXPECT_EQ(seen.Info.Flags & MG_Pipe::kDrawHasUserIndices, MG_Pipe::kDrawHasUserIndices);
|
|
EXPECT_EQ(seen.FirstRange.Count, 6u);
|
|
EXPECT_EQ(fixture.session->Applier().Verbs().Draws(), 0u);
|
|
|
|
fixture.Stop();
|
|
}
|
|
|
|
// Red once by: dropping `m_lastDraw.Indirect = *indirect;` - DrawCount reads 0 below.
|
|
TEST(ServerLoopTest, AnIndirectDrawRecordReachesTheSinkWithItsBlockIntact) {
|
|
ServerFixture fixture;
|
|
ASSERT_TRUE(fixture.Handshake());
|
|
ASSERT_TRUE(fixture.StartLoop());
|
|
|
|
MG_Pipe::MGPDrawInfo info = D1DrawInfo(/*indexSize=*/0, /*numDraws=*/0);
|
|
info.Flags = MG_Pipe::kDrawIsIndirect;
|
|
MG_Pipe::MGPDrawIndirect block{};
|
|
block.Buffer = D1Handle(40);
|
|
block.ParameterBuffer = D1Handle(41); // the *IndirectCount shape
|
|
block.Offset = 32;
|
|
block.ParameterOffset = 8;
|
|
block.Stride = 16;
|
|
block.DrawCount = 3;
|
|
const Codec::WireTail tails[2] = {{nullptr, 0}, {&block, sizeof(block)}};
|
|
const Uint64 seq = fixture.encoder.EncodeRecord(MG_Pipe::MGPWireOp::DrawVbo, &info, sizeof(info),
|
|
tails, 2);
|
|
ASSERT_NE(seq, Codec::kInvalidSeq);
|
|
fixture.encoder.Publish();
|
|
fixture.producer.PublishAndNotify(seq);
|
|
ASSERT_EQ(fixture.producer.WaitForApplied(seq, 5000), Transport::SessionWait::Reached);
|
|
|
|
const Server::ServerVerbSink::LastDrawRecord& seen = fixture.session->Applier().Verbs().LastDraw();
|
|
EXPECT_TRUE(seen.HadIndirect) << "the indirect block did not reach OnDrawVbo";
|
|
EXPECT_FALSE(seen.HadUserIndices);
|
|
EXPECT_EQ(seen.Indirect.Buffer.Slot, 40u);
|
|
EXPECT_EQ(seen.Indirect.ParameterBuffer.Slot, 41u);
|
|
EXPECT_EQ(seen.Indirect.Offset, 32u);
|
|
EXPECT_EQ(seen.Indirect.ParameterOffset, 8u);
|
|
EXPECT_EQ(seen.Indirect.Stride, 16u);
|
|
EXPECT_EQ(seen.Indirect.DrawCount, 3u);
|
|
EXPECT_EQ(seen.Info.NumDraws, 0u);
|
|
|
|
fixture.Stop();
|
|
}
|
|
|
|
int main(int argc, char** argv) {
|
|
// Before anything logs: MG_Util::Debug::InitFile() reads the variable once, on the first
|
|
// write, and caches the FILE*. The name carries this process's pid, because
|
|
// gtest_discover_tests runs every case as its own process, in parallel under ctest -j.
|
|
namespace fs = std::filesystem;
|
|
const fs::path path =
|
|
fs::temp_directory_path() / ("mobilegl-serverloop-test-" + std::to_string(ProcessId()) + ".log");
|
|
std::error_code ec;
|
|
fs::remove(path, ec);
|
|
g_logPath = path.string();
|
|
#if defined(_WIN32)
|
|
_putenv_s("MOBILEGL_LOG_FILE_PATH", g_logPath.c_str());
|
|
#else
|
|
setenv("MOBILEGL_LOG_FILE_PATH", g_logPath.c_str(), 1);
|
|
#endif
|
|
// THIS PROCESS IS A SPLIT ONE. Everything under test reads MG_Config::Transport - the
|
|
// staged-shadow arm, the applier's stamp path, ConfigLoader's own knobs - and a suite that
|
|
// left it at Monolith would be a server test running the monolith answers, which is the
|
|
// failure this phase is built to make impossible.
|
|
MG_Config::Transport = MG_Config::TransportMode::InProcess;
|
|
::testing::InitGoogleTest(&argc, argv);
|
|
const int rc = RUN_ALL_TESTS();
|
|
fs::remove(path, ec);
|
|
return rc;
|
|
}
|