mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
The spawn work had the two roles append to one MOBILEGL_LOG_FILE_PATH under O_APPEND. It was correct - two processes at one offset would corrupt the file, O_APPEND made each write atomic - but it put two sessions' lines in one place and made every per-side assertion a search. This splits the sink: the client role writes <base>.client.log and the server role writes <base>.server.log. THE BASE NAME NAMES NO FILE ANY MORE, AND THAT IS THE POINT. Leaving the client at the old unsuffixed path would have been the compatible choice and the wrong one: a reader that was not updated would go on finding a file, go on passing, and go on seeing only half the session - and a refusal census that reports a confident zero for the half it cannot see is worse than one that fails. With both names moved, an un-updated reader gets "file not found" and says so. There were 34 such readers; every one is moved here. THE ROLE IS PER-THREAD, not per-process, because under inproc both roles are threads of one process. Log.cpp keys the sink on a thread-local role: the apply thread declares itself server (ServerLoop.cpp), a spawned image is server for its whole life (MOBILEGL_IPC_ROLE=server, read once), and everything else is the client. The naming rule (RoleLogPath) and the two whole-log operations (ReadRoleLogs / TruncateRoleLogs) are EXPORTED from the library, so no reader reimplements the derivation - a second copy is the copy that falls behind. Readers, by how they read: - unit mains set MOBILEGL_LOG_FILE_PATH and read a role file: they keep the base in the env and derive the client path from the library helper. - the death-test child runners (ReadLog) now concatenate BOTH roles: a death test asserts the CHILD said something, and the applier's refusals - the very diagnostics these check - are written under the server role. - the offset-based Emit readers derive the client path once after setenv. - PipeStatsWindow reads both, because draws/vbs increment in the backend's PrepareForDraw - the apply thread, the server role - so the summary line is in the server file even under inproc. - run_trace_case.cmake's split census reads both files; the client keeps the transport markers. - split_log_paths.py (and its self-test, and the control stub) derive both role files from the declared base. - the Android script pulls both role files off the device and concatenates them, so every downstream check is unchanged - and it judges a role file by CONTENT, because `run-as test -f` through `exec-out` returned 0 for a missing file on the device and `exec-out` folds cat's own not-found line into stdout. The monolith arm legitimately has no server file, and that is a silent skip rather than a one-line server log. Guarded so a pull build is untouched: RoleLogPath and the role sink are wholly inside MOBILEGL_BUILD_DISAGGREGATED; a non-disaggregated build writes the base path exactly as before. G1 re-measured - 0 symbol diff, .text unchanged at 0xa52203. Verified on device 2f7cbe2e: monolith writes only the client log (no phantom server log), inproc and spawn each write both and pass their transport proofs (spawn: server role in pid 16125). Unit 2353 (one pre-existing environmental failure), integration-split 186/186, integration-spawn 102/102, retrace 6/6, control smoke 12/12, split-log-paths self-test 9/9.
2719 lines
144 KiB
C++
2719 lines
144 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_Util/Debug/Log.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_Backend/DirectVulkan/Renderer/VulkanRenderer.h>
|
|
#include <MG_Impl/Pipe/PipeFill.h>
|
|
#include <MG_Impl/Pipe/ResourceTracker.h>
|
|
#include <MG_Pipe/MGPipe.h>
|
|
#include <MG_Pipe/MGPipeCallbacks.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/Protocol/SurfaceOpCodec.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/ErrorState/ErrorInfo.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() {
|
|
// BOTH ROLES' LOGS (P6). A death test asserts that the CHILD said something; which
|
|
// role's thread said it is not what these cases are about, and refusals raised on the
|
|
// apply thread are written under the SERVER role by construction.
|
|
return MobileGL::MG_Util::Debug::ReadRoleLogs(g_logPath.c_str());
|
|
}
|
|
|
|
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(Server::ServerSession* owner = nullptr) {
|
|
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 = owner != nullptr ? owner : &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.RunProbeOnApplyThreadForTesting(
|
|
+[](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();
|
|
}
|
|
|
|
// P5d round 3, package T item 1: the mailbox's one-bit shadow. ControlIsPending() no longer
|
|
// takes m_controlMutex - it reads m_controlPosted - and that word has TWO failure modes, only
|
|
// one of which any existing case can see.
|
|
//
|
|
// THE SET SIDE is already covered: a poster that failed to publish the shadow leaves the park
|
|
// predicate with nothing to see, and AControlRequestRunsOnTheApplyThreadAndUnparksIt above says
|
|
// so in its own words within five seconds.
|
|
//
|
|
// THE CLEAR SIDE IS THIS CASE, and it is the one nothing else can reach. A shadow that the pump
|
|
// takes the request from but never clears makes `ready` permanently true: the loop then spins at
|
|
// full clock for the rest of the session, never sets consumerParked again, and every symptom is
|
|
// a performance one - the process stays correct, the phone gets hot and a core is gone.
|
|
//
|
|
// IT HAS TO OBSERVE THE WINDOW, NOT THE END STATE, and that is the whole shape of the case.
|
|
// PumpControlRequest clears the shadow TWICE: once when it takes the request (the load-bearing
|
|
// clear) and once defensively, on the next iteration, when it finds the shadow set with
|
|
// m_controlPending false. Assert only "the shadow is clear after the request was answered" and
|
|
// the defensive clear covers for a missing take-clear on the very next pump - the case then
|
|
// passes with the line it exists to pin deleted. So the request's work BLOCKS on a gate this
|
|
// thread holds, and the shadow is read while the apply thread is still inside work(): in that
|
|
// window m_controlPending is still true, the pump is not running, and the ONLY thing that can
|
|
// make ControlIsPending() false is the clear at the take. Delete it and this read returns true,
|
|
// deterministically, on every run.
|
|
TEST(ServerLoopTest, TheControlShadowIsClearedWhenThePumpTakesTheRequest) {
|
|
ServerFixture fixture;
|
|
ASSERT_TRUE(fixture.Handshake());
|
|
ASSERT_TRUE(fixture.StartLoop());
|
|
|
|
Server::ServerLoop& loop = Server::ServerLoopInstance();
|
|
ASSERT_TRUE(fixture.WaitUntilTrulyParked())
|
|
<< "the apply thread never parked in the first place, so this case could prove nothing "
|
|
"about it parking AGAIN";
|
|
EXPECT_FALSE(loop.ControlIsPending())
|
|
<< "the shadow is set with no request posted: `ready` is true for ever and the loop "
|
|
"will never park";
|
|
|
|
// The gate the posted work blocks on. `running` is released by the apply thread once it is
|
|
// INSIDE work(); `release` is set by this thread once it has read the shadow.
|
|
struct Gate {
|
|
std::atomic<Bool> running{false};
|
|
std::atomic<Bool> release{false};
|
|
};
|
|
Gate gate;
|
|
|
|
// Posted from a helper for N-9's reason - a predicate that cannot see the request blocks the
|
|
// poster for ever, and the case has to say that rather than hit ctest's timeout.
|
|
std::atomic<Bool> answered{false};
|
|
MobileGLResult rc = MOBILEGL_ERR_INVALID_ARGUMENT;
|
|
std::thread poster([&] {
|
|
rc = loop.RunProbeOnApplyThreadForTesting(
|
|
+[](void* user) -> MobileGLResult {
|
|
Gate* g = static_cast<Gate*>(user);
|
|
g->running.store(true, std::memory_order_release);
|
|
const auto gateDeadline =
|
|
std::chrono::steady_clock::now() + std::chrono::seconds(5);
|
|
while (!g->release.load(std::memory_order_acquire) &&
|
|
std::chrono::steady_clock::now() < gateDeadline) {
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
|
}
|
|
return MOBILEGL_OK;
|
|
},
|
|
&gate);
|
|
answered.store(true, std::memory_order_release);
|
|
});
|
|
|
|
// Wait until the work is really running, i.e. until the pump has taken the request.
|
|
const auto startDeadline = std::chrono::steady_clock::now() + std::chrono::seconds(5);
|
|
while (!gate.running.load(std::memory_order_acquire) &&
|
|
std::chrono::steady_clock::now() < startDeadline) {
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
|
}
|
|
const Bool started = gate.running.load(std::memory_order_acquire);
|
|
if (started) {
|
|
// THE ASSERTION. The apply thread is inside work(), so nothing is pumping and
|
|
// m_controlPending is still true; the shadow is false only because the take cleared it.
|
|
EXPECT_FALSE(loop.ControlIsPending())
|
|
<< "the pump took a control request and left the shadow set: `ready` stays true for "
|
|
"as long as the request runs, and if the take-clear is gone entirely the idle "
|
|
"poll spins a core at full clock for the rest of the session";
|
|
} else {
|
|
ADD_FAILURE() << "the posted control request never started running: the shadow's SET side "
|
|
"is broken, so the park predicate no longer carries the control flag";
|
|
}
|
|
gate.release.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));
|
|
}
|
|
const Bool gotAnswer = answered.load(std::memory_order_acquire);
|
|
if (!gotAnswer) {
|
|
ADD_FAILURE() << "the posted control request was never answered";
|
|
fixture.Stop();
|
|
poster.join();
|
|
return;
|
|
}
|
|
poster.join();
|
|
EXPECT_EQ(rc, MOBILEGL_OK);
|
|
|
|
// And the loop must be able to BLOCK again - the end state the defensive clear also
|
|
// guarantees, kept because it is the symptom an operator would actually see.
|
|
EXPECT_TRUE(fixture.WaitUntilTrulyParked())
|
|
<< "the apply thread never re-entered the blocking park after a control request: the "
|
|
"shadow is stuck set, so the idle poll now spins a core at full clock for the rest "
|
|
"of the session";
|
|
|
|
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().RunProbeOnApplyThreadForTesting(
|
|
+[](void* user) -> MobileGLResult {
|
|
auto* o = static_cast<Outer*>(user);
|
|
o->innerRc = Server::ServerLoopInstance().RunProbeOnApplyThreadForTesting(
|
|
+[](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();
|
|
}
|
|
|
|
TEST(ServerLoopTest, WireSequenceSurvivesRealDispatchAndFailureReply) {
|
|
ServerFixture fixture;
|
|
ASSERT_TRUE(fixture.Handshake());
|
|
ASSERT_TRUE(fixture.StartLoop());
|
|
flatbuffers::FlatBufferBuilder builder(256);
|
|
const auto op = ::MobileGL::Wire::CreateSurfaceOp(
|
|
builder, 4242, ::MobileGL::Wire::SurfaceOpKind::SetSwapInterval);
|
|
const auto envelope = ::MobileGL::Wire::CreateCtrlEnvelope(
|
|
builder, ::MobileGL::Wire::CtrlMsg::SurfaceOp, op.Union());
|
|
::MobileGL::Wire::FinishCtrlEnvelopeBuffer(builder, envelope);
|
|
Server::SurfaceControlFrame reply;
|
|
EXPECT_EQ(MG_Remote::ServerApplyWireSurfaceOp(
|
|
*::MobileGL::Wire::GetCtrlEnvelope(builder.GetBufferPointer())->msg_as_SurfaceOp(), &reply),
|
|
MOBILEGL_ERR_NOT_INITIALIZED);
|
|
EXPECT_EQ(Server::ServerLoopInstance().ControlFramesDispatched(), 1u);
|
|
flatbuffers::FlatBufferBuilder replies(256);
|
|
MG_Remote::EncodeSurfaceReplyFrame(reply, &replies);
|
|
const auto* wireReply = ::MobileGL::Wire::GetCtrlEnvelope(replies.GetBufferPointer())->msg_as_SurfaceReply();
|
|
EXPECT_EQ(wireReply->seq(), 4242u);
|
|
EXPECT_FALSE(wireReply->ok());
|
|
fixture.Stop();
|
|
}
|
|
|
|
TEST(ServerLoopTest, ConcurrentProbesKeepTheirArgumentsAcrossNestedProbes) {
|
|
ServerFixture fixture;
|
|
ASSERT_TRUE(fixture.Handshake());
|
|
ASSERT_TRUE(fixture.StartLoop());
|
|
constexpr int kPosters = 8;
|
|
constexpr int kCalls = 128;
|
|
struct Probe {
|
|
std::atomic<int> calls{0};
|
|
std::atomic<int> nested{0};
|
|
std::atomic<int> errors{0};
|
|
} probes[kPosters];
|
|
std::atomic<bool> go{false};
|
|
std::vector<std::thread> posters;
|
|
for (auto& probe : probes) {
|
|
posters.emplace_back([&go, &probe] {
|
|
while (!go.load(std::memory_order_acquire)) std::this_thread::yield();
|
|
for (int i = 0; i < kCalls; ++i) {
|
|
const auto rc = Server::ServerLoopInstance().RunProbeOnApplyThreadForTesting(
|
|
+[](void* user) -> MobileGLResult {
|
|
auto* p = static_cast<Probe*>(user);
|
|
p->calls.fetch_add(1);
|
|
// Same argument type keeps a regressed mixed pair observable without UB.
|
|
return Server::ServerLoopInstance().RunProbeOnApplyThreadForTesting(
|
|
+[](void* nestedUser) -> MobileGLResult {
|
|
static_cast<Probe*>(nestedUser)->nested.fetch_add(1);
|
|
return MOBILEGL_OK;
|
|
}, p);
|
|
}, &probe);
|
|
if (rc != MOBILEGL_OK) probe.errors.fetch_add(1);
|
|
}
|
|
});
|
|
}
|
|
go.store(true, std::memory_order_release);
|
|
for (auto& poster : posters) poster.join();
|
|
for (const auto& probe : probes) {
|
|
EXPECT_EQ(probe.calls.load(), kCalls) << "another poster or nested probe replaced this hook/user pair";
|
|
EXPECT_EQ(probe.nested.load(), kCalls);
|
|
EXPECT_EQ(probe.errors.load(), 0);
|
|
}
|
|
EXPECT_EQ(Server::ServerLoopInstance().ControlFramesDispatched(), 2u * kPosters * kCalls);
|
|
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()->Progress.retiredSeq.load() < 8u &&
|
|
std::chrono::steady_clock::now() < retireDeadline) {
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
|
}
|
|
EXPECT_GE(fixture.clientSegments.CmdControl()->Progress.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().RunProbeOnApplyThreadForTesting(
|
|
+[](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";
|
|
}
|
|
|
|
// P5f (fc): the frame channel's own red-once handle. A forwarder no longer posts a function
|
|
// pointer and a stack address; it packs a SurfaceControlFrame and the loop's dispatch counter is
|
|
// the proof the frame crossed. The revert this guards: turn ServerSetEGLSwapInterval back into a
|
|
// direct `Backend()->SetEGLSwapInterval(...)` (or any shape that skips the frame) and the counter
|
|
// below does not move - the test goes red even though the swap interval itself would still reach a
|
|
// backend in an inproc build, which is exactly why "the EGL call happened" cannot be the check.
|
|
TEST(ServerLoopTest, AVoidForwarderCrossesAsOneDispatchedFrame) {
|
|
ServerFixture fixture;
|
|
ASSERT_TRUE(fixture.Handshake());
|
|
ASSERT_TRUE(fixture.StartLoop());
|
|
Server::ServerLoop& loop = Server::ServerLoopInstance();
|
|
ASSERT_TRUE(fixture.WaitUntilTrulyParked());
|
|
ASSERT_EQ(loop.ControlFramesDispatched(), 0u);
|
|
|
|
// No backend lives in this process, so the dispatch declines with NOT_INITIALIZED - but the
|
|
// frame must still have CROSSED the channel, which is the property under test.
|
|
Server::ServerSetEGLSwapInterval(1);
|
|
|
|
EXPECT_EQ(loop.ControlFramesDispatched(), 1u)
|
|
<< "ServerSetEGLSwapInterval did not dispatch exactly one control frame; the forwarder "
|
|
"reached the backend (or did nothing) without crossing the frame channel";
|
|
|
|
fixture.Stop();
|
|
}
|
|
|
|
// P5d round 3, package D: THE IDENTITY MUST DIE WITH THE THREAD, and this is the case that says
|
|
// so. OnApplyThread() is no longer `m_running && id == mine` - it is one relaxed load of
|
|
// Server::Detail::g_applyThreadKey compared against the caller's own thread pointer - so there
|
|
// is no second term left to make a stale identity harmless. If ApplyThreadMain's exit block
|
|
// stops clearing the key (deleted, or lost in a merge with a package that rewrites that same C2
|
|
// critical section), the key keeps naming a thread that no longer exists, and libc hands that
|
|
// thread's freed TLS block - hence the very thread-pointer value the key holds - to the NEXT
|
|
// thread the process creates: a ShaderCompilePool worker, or the GL thread of a second context
|
|
// after a context-loss restart. That thread then answers OnApplyThread() TRUE, which inverts
|
|
// RunsAsTheServerRole() and PersistentMapTracker::OnServerRole() on the client, aborts
|
|
// BufferObject's accessors with Fatal{RoleViolation, "buffer-legacy-arm"}, and makes
|
|
// RunSurfaceControlFrame run EGL work inline on it - the "EGL on the app thread" outcome R-1 exists to
|
|
// make impossible.
|
|
//
|
|
// Two halves, because the mechanism and the consequence fail differently. (a) is deterministic
|
|
// on every platform: after the join the key is 0, full stop. (b) is the one that would have
|
|
// caught the real bug: threads created after the join must not inherit the answer. (b) is not
|
|
// guaranteed to reproduce recycling on every allocator, which is exactly why (a) is here too.
|
|
TEST(ServerLoopTest, AThreadCreatedAfterTheLoopStoppedIsNotMistakenForTheApplyThread) {
|
|
ServerFixture fixture;
|
|
ASSERT_TRUE(fixture.Handshake());
|
|
ASSERT_TRUE(fixture.StartLoop());
|
|
ASSERT_TRUE(fixture.WaitUntilTrulyParked());
|
|
|
|
// The key really was published while the thread ran - otherwise (a) below would pass for the
|
|
// wrong reason (a key that was never set is also 0).
|
|
ASSERT_NE(Server::Detail::g_applyThreadKey.load(std::memory_order_relaxed), 0ull)
|
|
<< "the apply thread is parked and running but published no identity: ApplyThreadMain's "
|
|
"first statement no longer stores Detail::CurrentThreadKey()";
|
|
|
|
fixture.Stop();
|
|
ASSERT_FALSE(Server::ServerLoopInstance().Running());
|
|
|
|
// (a) the clear itself.
|
|
EXPECT_EQ(Server::Detail::g_applyThreadKey.load(std::memory_order_relaxed), 0ull)
|
|
<< "the apply-thread identity outlived the apply thread: ApplyThreadMain's C2 exit block "
|
|
"no longer clears Detail::g_applyThreadKey beside m_running";
|
|
|
|
// (b) what the clear is FOR. Sequentially, so each thread is created after the previous one
|
|
// has been joined and is therefore the likeliest candidate to receive the recycled block.
|
|
for (int i = 0; i < 8; ++i) {
|
|
std::atomic<Bool> onApplyThread{true};
|
|
std::thread probe([&] {
|
|
onApplyThread.store(Server::ServerLoop::OnApplyThread(), std::memory_order_release);
|
|
});
|
|
probe.join();
|
|
ASSERT_FALSE(onApplyThread.load(std::memory_order_acquire))
|
|
<< "thread " << i << " created after the apply thread was joined answers "
|
|
"OnApplyThread() TRUE - it inherited the exited thread's TLS block and the stale "
|
|
"key still names it. Every role guard in the tree is now inverted on that thread";
|
|
}
|
|
}
|
|
|
|
// 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 probe hook 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().RunProbeOnApplyThreadForTesting(
|
|
+[](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
|
|
|
|
TEST(ServerLoopEglTest, SuccessfulVoidWireOperationsReplyWithSuccessAndOriginalSequence) {
|
|
EglServerFixture fixture;
|
|
MGL_EGL_BRING_UP_OR_BAIL(fixture);
|
|
// Release last: it deliberately destroys the native context used by the preceding ops.
|
|
const auto kinds = {::MobileGL::Wire::SurfaceOpKind::SetSwapInterval,
|
|
::MobileGL::Wire::SurfaceOpKind::SetWindowHandle,
|
|
::MobileGL::Wire::SurfaceOpKind::ReleaseSurface,
|
|
::MobileGL::Wire::SurfaceOpKind::ReleaseResources};
|
|
Uint64 seq = 9000;
|
|
for (const auto kind : kinds) {
|
|
flatbuffers::FlatBufferBuilder builder(256);
|
|
const auto op = ::MobileGL::Wire::CreateSurfaceOp(
|
|
builder, ++seq, kind, 0, 0, ::MobileGL::Wire::WindowKind::None, 0, 0, 0, 1);
|
|
const auto envelope = ::MobileGL::Wire::CreateCtrlEnvelope(
|
|
builder, ::MobileGL::Wire::CtrlMsg::SurfaceOp, op.Union());
|
|
::MobileGL::Wire::FinishCtrlEnvelopeBuffer(builder, envelope);
|
|
Server::SurfaceControlFrame reply;
|
|
EXPECT_EQ(MG_Remote::ServerApplyWireSurfaceOp(
|
|
*::MobileGL::Wire::GetCtrlEnvelope(builder.GetBufferPointer())->msg_as_SurfaceOp(), &reply),
|
|
MOBILEGL_OK) << static_cast<int>(kind);
|
|
flatbuffers::FlatBufferBuilder replies(256);
|
|
MG_Remote::EncodeSurfaceReplyFrame(reply, &replies);
|
|
const auto* wireReply = ::MobileGL::Wire::GetCtrlEnvelope(replies.GetBufferPointer())->msg_as_SurfaceReply();
|
|
EXPECT_EQ(wireReply->seq(), seq) << static_cast<int>(kind);
|
|
EXPECT_TRUE(wireReply->ok()) << "successful void operation encoded failure: " << static_cast<int>(kind);
|
|
}
|
|
fixture.TearDown();
|
|
}
|
|
|
|
// 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();
|
|
}
|
|
|
|
TEST(ServerLoopEglTest, RenderShadowCannotSkipAnUnchangedVersionAfterContextEpochChanges) {
|
|
EglServerFixture fixture;
|
|
MGL_EGL_BRING_UP_OR_BAIL(fixture);
|
|
ASSERT_TRUE(fixture.MakeCurrent());
|
|
Bool nativeRestored = false, servedRestored = false;
|
|
ASSERT_EQ(OnApply([&] {
|
|
using namespace MG_Backend::DirectGLES;
|
|
MG_Pipe::MGPipeServerStampVerbBoundary(MG_Pipe::MGPipeVerb::Clear);
|
|
RenderStateImpl::SyncRenderState(true);
|
|
// Simulate the successor driver's default/foreign state while retaining exactly
|
|
// the same parameter bytes and version. Epoch, not a value diff, must force it.
|
|
g_GLESFuncs.glEnable(GL_BLEND);
|
|
++g_backendContextGeneration;
|
|
RenderStateImpl::SyncRenderState(true);
|
|
nativeRestored = g_GLESFuncs.glIsEnabled(GL_BLEND) == GL_FALSE;
|
|
g_GLESFuncs.glEnable(GL_BLEND);
|
|
MG_Pipe::MGPipeApplierReset();
|
|
MG_Pipe::MGPipeServerStampVerbBoundary(MG_Pipe::MGPipeVerb::Clear);
|
|
RenderStateImpl::SyncRenderState(true);
|
|
servedRestored = g_GLESFuncs.glIsEnabled(GL_BLEND) == GL_FALSE;
|
|
MG_Pipe::MGPipeServerClearVerbBoundary();
|
|
}), MOBILEGL_OK);
|
|
EXPECT_TRUE(nativeRestored) << "unchanged render version hid a new native context";
|
|
EXPECT_TRUE(servedRestored) << "unchanged render version hid a new served context";
|
|
fixture.TearDown();
|
|
}
|
|
|
|
TEST(ServerLoopEglTest, ServerLivenessFollowsControlFramesAcrossReleaseAndRecreation) {
|
|
EglServerFixture fixture;
|
|
MGL_EGL_BRING_UP_OR_BAIL(fixture);
|
|
const auto live = [] {
|
|
Bool answer = false;
|
|
EXPECT_EQ(OnApply([&] { answer = MG_Pipe::gPipeInputs.IsLive(); }), MOBILEGL_OK);
|
|
return answer;
|
|
};
|
|
EXPECT_FALSE(live()) << "surface creation alone is not a served current context";
|
|
ASSERT_TRUE(fixture.MakeCurrent());
|
|
EXPECT_TRUE(live());
|
|
auto client = Move(MG_State::pGLContext);
|
|
EXPECT_TRUE(live()) << "server liveness must not consult the client GLContext";
|
|
MG_State::pGLContext = Move(client);
|
|
ASSERT_TRUE(fixture.ReleaseCurrent());
|
|
EXPECT_FALSE(live());
|
|
ASSERT_TRUE(fixture.MakeCurrent()); // identical held native tuple, new logical binding
|
|
EXPECT_TRUE(live());
|
|
Server::ServerReleaseEGLResources();
|
|
EXPECT_FALSE(live());
|
|
EGLint major = 0, minor = 0;
|
|
ASSERT_TRUE(Server::ServerInitializeEGLDisplay(EglServerFixture::Dpy(), &major, &minor));
|
|
ASSERT_TRUE(Server::ServerCreateEGLPbufferSurface(EglServerFixture::Surf(), 64, 64));
|
|
EXPECT_FALSE(live());
|
|
ASSERT_TRUE(fixture.MakeCurrent());
|
|
EXPECT_TRUE(live());
|
|
fixture.TearDown();
|
|
EXPECT_FALSE(MG_Pipe::MGPipeServerContextIsLive());
|
|
}
|
|
|
|
// P5f (fc): InitializeEGLDisplay's out-pointers are the frame's reply fields now (the one place
|
|
// the old mailbox carried pointers INTO the poster's stack). The answer is the driver's real
|
|
// version through the frame - the BringUp already consumed one init; a second call is an
|
|
// idempotent eglInitialize and its version answer must still cross back.
|
|
TEST(ServerLoopEglTest, TheInitializeDisplayReplyArrivesThroughTheFrame) {
|
|
EglServerFixture fixture;
|
|
MGL_EGL_BRING_UP_OR_BAIL(fixture);
|
|
|
|
EGLint major = 0;
|
|
EGLint minor = 0;
|
|
ASSERT_TRUE(Server::ServerInitializeEGLDisplay(EglServerFixture::Dpy(), &major, &minor));
|
|
EXPECT_GE(major, 1) << "eglInitialize's major version did not cross back through the frame's "
|
|
"reply fields";
|
|
|
|
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) {
|
|
// Arm the record consumers before any backend helper can latch its subsystem choice.
|
|
struct PushMaskScope {
|
|
Uint64 saved = MG_Config::Features.PipePush;
|
|
PushMaskScope() { MG_Config::Features.PipePush = MG_Pipe::kMGPipeSubsystemsMigratedAtP5e; }
|
|
~PushMaskScope() { MG_Config::Features.PipePush = saved; }
|
|
} pushMaskScope;
|
|
EglServerFixture fixture;
|
|
MGL_EGL_BRING_UP_OR_BAIL(fixture);
|
|
ASSERT_TRUE(fixture.MakeCurrent());
|
|
|
|
// This fixture owns its encoder directly, so publish the same record-owned inputs the
|
|
// normal client would send. Residual frontend pointers are deliberately not a source.
|
|
MG_Pipe::MGPFramebufferState framebuffer{};
|
|
framebuffer.Fbo = MG_Pipe::kMGPipeDefaultFramebuffer;
|
|
framebuffer.Target = static_cast<Uint8>(MG_Pipe::MGPipeFramebufferTarget::Both);
|
|
framebuffer.IsDefault = 1;
|
|
framebuffer.Complete = 1;
|
|
framebuffer.Width = framebuffer.Height = 64;
|
|
framebuffer.Layers = framebuffer.Samples = 1;
|
|
framebuffer.FixedSampleLocations = 1;
|
|
for (auto& drawBuffer : framebuffer.DrawBuffers) drawBuffer = -1;
|
|
framebuffer.DrawBuffers[0] = 0;
|
|
framebuffer.ContentHash = 1;
|
|
ASSERT_TRUE(fixture.EmitAndWait(MG_Pipe::MGPWireOp::SetFramebufferState,
|
|
&framebuffer, sizeof(framebuffer)));
|
|
|
|
MG_Pipe::MGPPixelPackState pack{};
|
|
pack.Pack.Alignment = 4;
|
|
pack.Pack.RowLength = 8; // the application's padded layout must not size server scratch
|
|
ASSERT_TRUE(fixture.EmitAndWait(MG_Pipe::MGPWireOp::SetPixelPackState, &pack, sizeof(pack)));
|
|
// Unit zero is part of the initial touched-unit window even when unbound.
|
|
// Explicit empty bindings distinguish that state from records that never arrived.
|
|
MG_Pipe::MGPSamplerViews views{};
|
|
views.Count = 1;
|
|
MG_Pipe::MGPBoundView unboundView{};
|
|
ASSERT_TRUE(fixture.EmitAndWaitWithTail(MG_Pipe::MGPWireOp::SetSamplerViews,
|
|
&views, sizeof(views), &unboundView, sizeof(unboundView)));
|
|
MG_Pipe::MGPSamplerStates samplers{};
|
|
samplers.Count = 1;
|
|
const MG_Pipe::MGPipeHandle unboundSampler = MG_Pipe::kMGPipeNullHandle;
|
|
ASSERT_TRUE(fixture.EmitAndWaitWithTail(MG_Pipe::MGPWireOp::BindSamplerStates,
|
|
&samplers, sizeof(samplers), &unboundSampler, sizeof(unboundSampler)));
|
|
MG_Pipe::MGPContextValues context{}; // unit zero is unbound; no open XFB capture
|
|
ASSERT_TRUE(fixture.EmitAndWait(MG_Pipe::MGPWireOp::SetContextValues, &context, sizeof(context)));
|
|
|
|
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());
|
|
|
|
// Keep this object coherently mapped: Ensure's retained backend sync site must not
|
|
// re-enter the client producer and replace the staged 0x5B bytes with this 0xA5 map.
|
|
ASSERT_NE(buffer->AcquireMemoryRange({0, 64}, BufferMappingAccessBit::Write |
|
|
BufferMappingAccessBit::Persistent | BufferMappingAccessBit::Coherent), nullptr);
|
|
ASSERT_TRUE(buffer->IsMapped());
|
|
|
|
// 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;
|
|
}
|
|
namespace {
|
|
// The drive below, in a FORKED CHILD that brings the server up itself (the integration
|
|
// harness's pre-flight shape), so a Fatal on the apply thread ends the child and not the
|
|
// case. Exit 8 = the child's bring-up failed; exit 0 = the apply-thread twin mint ran,
|
|
// which is the refusal this case pins having been removed.
|
|
[[noreturn]] void RunFrontendFramebufferDeathOnApplyThreadAndExit() {
|
|
EglServerFixture fixture;
|
|
if (!fixture.BringUp().empty() || !fixture.MakeCurrent()) ::_exit(8);
|
|
namespace GLES = MG_Backend::DirectGLES;
|
|
auto framebuffer = MakeShared<MG_State::GLState::FramebufferObject>(901u);
|
|
(void)OnApply([&] {
|
|
auto& twin = GLES::FramebufferImpl::g_backendFramebufferObjects.GetOrCreate(framebuffer);
|
|
twin = MakeShared<GLES::FramebufferImpl::BackendFramebufferObject>();
|
|
});
|
|
framebuffer.reset();
|
|
::_exit(0);
|
|
}
|
|
} // namespace
|
|
|
|
// P5c (hd, CONTRACT-P5C §3.1 / §6 layer 1): this case's original drive - mint the framebuffer
|
|
// twin on the apply thread off the frontend object's lifetime id, then let the frontend death
|
|
// notice reach the in-process death switch there - is refused by name at the first step:
|
|
// the minting GetOrCreate(StatePtr) is monolith glue and the apply-thread call is
|
|
// Fatal{RoleViolation, "MGPipeSlots"}. The deletion-on-owner semantics it pinned return with
|
|
// ct's object_death record (the framebuffer family's first wire delete), whose sink releases
|
|
// the twin BY HANDLE. The name stays; the pin is now the refusal that stands until ct lands.
|
|
TEST(ServerLoopEglTest, FrontendFramebufferDeathDeletesOnTheContextOwner) {
|
|
MG_Config::Features.PipePush |= MG_Pipe::kMGPipeSubsystemEsprytSlots;
|
|
{
|
|
EglServerFixture probe;
|
|
MGL_EGL_BRING_UP_OR_BAIL(probe);
|
|
probe.TearDown();
|
|
}
|
|
const SizeT mark = ReadLog().size();
|
|
EXPECT_EXIT(RunFrontendFramebufferDeathOnApplyThreadAndExit(), ::testing::KilledBySignal(SIGABRT), ".*")
|
|
<< "the apply-thread mint of a frontend framebuffer's twin was not refused; exit 8 is the "
|
|
"child's own bring-up failing";
|
|
const std::string appended = ReadLogFrom(mark);
|
|
EXPECT_NE(appended.find("Fatal{RoleViolation, \"MGPipeSlots\"}"), std::string::npos)
|
|
<< "the abort was not the allocator guard's; 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();
|
|
}
|
|
|
|
namespace {
|
|
Uint32 g_fvErrorCode = 0;
|
|
String g_fvErrorMessage;
|
|
Uint32 g_fvErrorCalls = 0;
|
|
|
|
void CaptureFvError(Uint32 code, const char* message) {
|
|
g_fvErrorCode = code;
|
|
g_fvErrorMessage = message ? message : "";
|
|
++g_fvErrorCalls;
|
|
}
|
|
|
|
struct FvGlobals {
|
|
MG_Pipe::MGPipeCallbacks callbacks = MG_Pipe::gMGPipeCallbacks;
|
|
Bool strict = MG_Config::Ipc.StrictErrors;
|
|
Bool split = MG_Config::Ipc.RoleSplitState;
|
|
~FvGlobals() {
|
|
MG_Pipe::MGPipeServerClearVerbBoundary();
|
|
MG_Pipe::gMGPipeCallbacks = callbacks;
|
|
MG_Config::Ipc.StrictErrors = strict;
|
|
MG_Config::Ipc.RoleSplitState = split;
|
|
}
|
|
};
|
|
|
|
struct FvSession : ServerFixture {
|
|
~FvSession() { Stop(); }
|
|
};
|
|
}
|
|
|
|
namespace {
|
|
// Exercise the real hidden-resource constructors without adding a production test API.
|
|
// Explicit template instantiation permits naming a private member ([temp.explicit]).
|
|
using FvRenderer = MG_Backend::DirectVulkan::VulkanRenderer;
|
|
struct FvBlitInitTag {
|
|
using type = Bool (FvRenderer::*)();
|
|
friend type FvPrivateMember(FvBlitInitTag);
|
|
};
|
|
struct FvMipmapInitTag {
|
|
using type = Bool (FvRenderer::*)();
|
|
friend type FvPrivateMember(FvMipmapInitTag);
|
|
};
|
|
template <class Tag, typename Tag::type Member> struct FvMemberAccess {
|
|
friend typename Tag::type FvPrivateMember(Tag) { return Member; }
|
|
};
|
|
template struct FvMemberAccess<FvBlitInitTag, &FvRenderer::InitializeBlitResources>;
|
|
template struct FvMemberAccess<FvMipmapInitTag, &FvRenderer::InitializeDepthMipmapResources>;
|
|
}
|
|
|
|
TEST(P5fReverseChannel, TransportDoesNotConstructHiddenFrontendPrograms) {
|
|
// No Vulkan device, program factory or client GLContext is supplied. A transport
|
|
// initialization must not need any of them merely to skip these monolith resources.
|
|
FvRenderer renderer({});
|
|
EXPECT_TRUE((renderer.*FvPrivateMember(FvBlitInitTag{}))());
|
|
EXPECT_TRUE((renderer.*FvPrivateMember(FvMipmapInitTag{}))());
|
|
}
|
|
|
|
TEST(P5fReverseChannel, GlErrorsUseTheOwnedCallbackWithoutAResidualPull) {
|
|
FvGlobals restore;
|
|
MG_Config::Ipc.StrictErrors = true;
|
|
MG_Config::Ipc.RoleSplitState = true;
|
|
MG_Pipe::gMGPipeCallbacks.OnGlError = &CaptureFvError;
|
|
g_fvErrorCalls = 0;
|
|
MG_Pipe::MGPipeResetResidualPullCountForTesting();
|
|
MG_Pipe::MGPipeServerStampVerbBoundary(MG_Pipe::MGPipeVerb::Clear);
|
|
MG_Pipe::gPipeInputs.RecordError(ErrorCode::InvalidOperation,
|
|
MakeUnique<GenericErrorInfo>("fv", "draw", "driver message"));
|
|
EXPECT_EQ(g_fvErrorCalls, 1u);
|
|
EXPECT_EQ(g_fvErrorCode, static_cast<Uint32>(ErrorCode::InvalidOperation));
|
|
EXPECT_EQ(g_fvErrorMessage, "[fv] [draw] driver message");
|
|
EXPECT_EQ(MG_Pipe::MGPipeResidualPullCount(), 0u);
|
|
}
|
|
|
|
TEST(P5fReverseChannel, AcceptCloseAndReacceptOwnTheGlErrorCallback) {
|
|
FvGlobals restore;
|
|
MG_Pipe::gMGPipeCallbacks.OnGlError = nullptr;
|
|
FvSession fixture;
|
|
ASSERT_TRUE(fixture.Handshake());
|
|
const auto producer = MG_Pipe::gMGPipeCallbacks.OnGlError;
|
|
ASSERT_NE(producer, nullptr);
|
|
EXPECT_EQ(fixture.session->Accept(*fixture.serverTransport), MOBILEGL_ERR_INVALID_ARGUMENT);
|
|
EXPECT_EQ(MG_Pipe::gMGPipeCallbacks.OnGlError, producer);
|
|
fixture.Stop();
|
|
EXPECT_EQ(MG_Pipe::gMGPipeCallbacks.OnGlError, nullptr);
|
|
ASSERT_TRUE(fixture.Handshake());
|
|
EXPECT_EQ(MG_Pipe::gMGPipeCallbacks.OnGlError, producer);
|
|
fixture.Stop();
|
|
EXPECT_EQ(MG_Pipe::gMGPipeCallbacks.OnGlError, nullptr);
|
|
}
|
|
|
|
TEST(P5fReverseChannel, CloseDoesNotEraseAnotherGlErrorOwner) {
|
|
FvGlobals restore;
|
|
MG_Pipe::gMGPipeCallbacks.OnGlError = nullptr;
|
|
FvSession fixture;
|
|
ASSERT_TRUE(fixture.Handshake());
|
|
MG_Pipe::gMGPipeCallbacks.OnGlError = &CaptureFvError;
|
|
fixture.Stop();
|
|
EXPECT_EQ(MG_Pipe::gMGPipeCallbacks.OnGlError, &CaptureFvError);
|
|
}
|
|
|
|
#if !defined(_WIN32)
|
|
TEST(P5fReverseChannel, GlErrorCallbackRejectsDoubleInstallation) {
|
|
EXPECT_EXIT({
|
|
MG_Pipe::gMGPipeCallbacks.OnGlError = &CaptureFvError;
|
|
FvSession fixture;
|
|
(void)fixture.Handshake();
|
|
std::_Exit(9);
|
|
}, ::testing::KilledBySignal(SIGABRT), "");
|
|
EXPECT_NE(ReadLog().find("callback-double-install"), std::string::npos);
|
|
}
|
|
|
|
TEST(P5fReverseChannel, ASecondSessionCannotClaimTheSameReverseChannel) {
|
|
EXPECT_EXIT({
|
|
FvSession owner;
|
|
if (!owner.Handshake()) std::_Exit(7);
|
|
std::unique_ptr<Transport::InProcessTransport> client;
|
|
std::unique_ptr<Transport::InProcessTransport> server;
|
|
Transport::InProcessTransport::CreatePair(client, server);
|
|
Server::ServerSession other;
|
|
// The owner refusal precedes receiving a Hello or replacing the segment resolver.
|
|
(void)other.Accept(*server);
|
|
std::_Exit(9);
|
|
}, ::testing::KilledBySignal(SIGABRT), "");
|
|
EXPECT_NE(ReadLog().find("another ServerSession already owns"), std::string::npos);
|
|
}
|
|
|
|
TEST(P5fReverseChannel, RecordErrorWithoutCallbackIsNamedFatal) {
|
|
EXPECT_EXIT({
|
|
MG_Config::Ipc.StrictErrors = true;
|
|
MG_Config::Ipc.RoleSplitState = true;
|
|
MG_Pipe::gMGPipeCallbacks.OnGlError = nullptr;
|
|
MG_Pipe::gPipeInputs.RecordError(ErrorCode::InvalidOperation, nullptr);
|
|
std::_Exit(9);
|
|
}, ::testing::KilledBySignal(SIGABRT), "");
|
|
EXPECT_NE(ReadLog().find("OnGlError.callback-missing"), std::string::npos);
|
|
}
|
|
|
|
TEST(P5fReverseChannel, LegacyGpuWrittenNeverReadsAFrontendObjectInTransport) {
|
|
for (Bool installed : {false, true}) {
|
|
EXPECT_EXIT({
|
|
MG_Pipe::gMGPipeCallbacks.OnGpuWritten = installed ?
|
|
+[](MG_Pipe::MGPipeHandle, Uint, const MG_Pipe::MGPRange*) {} : nullptr;
|
|
// Deliberately unreadable object: a guard after a lifetime-id probe or
|
|
// either MarkGpuWritten fallback would SIGSEGV instead of the named abort.
|
|
SharedPtr<MG_State::GLState::BufferObject> foreign(
|
|
reinterpret_cast<MG_State::GLState::BufferObject*>(std::uintptr_t{1}),
|
|
[](MG_State::GLState::BufferObject*) {});
|
|
MG_Pipe::MGPipeAnnounceBufferGpuWritten(foreign);
|
|
std::_Exit(9);
|
|
}, ::testing::KilledBySignal(SIGABRT), "");
|
|
}
|
|
EXPECT_NE(ReadLog().find("gpu-written-legacy-object"), std::string::npos);
|
|
}
|
|
#endif
|
|
|
|
TEST(P5fReverseChannel, GlErrorMessagesShareFifoWithOtherReverseEvents) {
|
|
FvGlobals restore;
|
|
MG_Config::Ipc.StrictErrors = true;
|
|
MG_Config::Ipc.RoleSplitState = true;
|
|
MG_Pipe::gMGPipeCallbacks.OnGlError = nullptr;
|
|
FvSession fixture;
|
|
ASSERT_TRUE(fixture.Handshake());
|
|
ASSERT_TRUE(fixture.StartLoop());
|
|
ASSERT_EQ(Server::ServerLoopInstance().RunProbeOnApplyThreadForTesting(
|
|
+[](void*) -> MobileGLResult {
|
|
MG_Pipe::MGPipeServerStampVerbBoundary(MG_Pipe::MGPipeVerb::Clear);
|
|
MG_Pipe::gPipeInputs.RecordError(ErrorCode::InvalidOperation,
|
|
MakeUnique<GenericErrorInfo>("fv", "driver error"));
|
|
const MG_Pipe::MGPRange whole{0, MG_Pipe::kMGPipeWholeBuffer};
|
|
MG_Pipe::gMGPipeCallbacks.OnGpuWritten({17, 3}, 1, &whole);
|
|
Uint8 bytes[] = {11, 22, 33, 44};
|
|
MG_Pipe::gMGPipeCallbacks.OnBufferWriteback({17, 3}, 24,
|
|
MG_Pipe::MGPBlobRef{reinterpret_cast<Uint64>(bytes), sizeof(bytes),
|
|
MG_Pipe::kMGHostSpanSegNone, 0});
|
|
std::memset(bytes, 0xDD, sizeof(bytes));
|
|
String longMessage(Transport::kEventGlErrorMaxMessageBytes * 2, 'Q');
|
|
MG_Pipe::gMGPipeCallbacks.OnGlError(static_cast<Uint32>(ErrorCode::InvalidValue),
|
|
longMessage.c_str());
|
|
std::fill(longMessage.begin(), longMessage.end(), '?');
|
|
MG_Pipe::gMGPipeCallbacks.OnGlError(static_cast<Uint32>(ErrorCode::InvalidEnum), nullptr);
|
|
MG_Pipe::MGPipeServerClearVerbBoundary();
|
|
return MOBILEGL_OK;
|
|
}, nullptr), MOBILEGL_OK);
|
|
|
|
Transport::EventRingConsumer events(fixture.clientSegments.EventControl(),
|
|
fixture.clientSegments.CmdControl(), fixture.clientSegments.EventRingBase(),
|
|
fixture.clientSegments.EventRingCapacity(), fixture.clientSegments.EventSegmentBase());
|
|
ASSERT_TRUE(events.Valid());
|
|
const Uint16 kinds[] = {Transport::kEventGlError, Transport::kEventGpuWritten,
|
|
Transport::kEventBufferWriteback, Transport::kEventGlError, Transport::kEventGlError};
|
|
for (SizeT i = 0; i < 5; ++i) {
|
|
Transport::RingRecordView event;
|
|
ASSERT_TRUE(events.Pop(event)) << i;
|
|
EXPECT_EQ(event.kind, kinds[i]) << i;
|
|
if (event.kind == Transport::kEventGlError) {
|
|
const auto* head = static_cast<const Transport::EventGlErrorHead*>(event.payload);
|
|
ASSERT_GE(event.payloadSize, sizeof(*head) + head->MessageBytes);
|
|
ASSERT_GT(head->MessageBytes, 0u);
|
|
const auto* text = reinterpret_cast<const char*>(head + 1);
|
|
EXPECT_EQ(text[head->MessageBytes - 1], '\0');
|
|
const String message(text, head->MessageBytes - 1);
|
|
if (i == 0) {
|
|
EXPECT_EQ(head->Code, static_cast<Uint32>(ErrorCode::InvalidOperation));
|
|
EXPECT_EQ(message, "[fv] driver error");
|
|
} else if (i == 3) {
|
|
EXPECT_EQ(head->Code, static_cast<Uint32>(ErrorCode::InvalidValue));
|
|
EXPECT_EQ(head->MessageBytes, Transport::kEventGlErrorMaxMessageBytes);
|
|
EXPECT_EQ(message, String(Transport::kEventGlErrorMaxMessageBytes - 1, 'Q'));
|
|
} else {
|
|
EXPECT_EQ(head->Code, static_cast<Uint32>(ErrorCode::InvalidEnum));
|
|
EXPECT_EQ(head->MessageBytes, 1u);
|
|
EXPECT_TRUE(message.empty());
|
|
}
|
|
} else if (event.kind == Transport::kEventGpuWritten) {
|
|
const auto* head = static_cast<const Transport::EventGpuWrittenHead*>(event.payload);
|
|
EXPECT_EQ(head->Resource.Slot, 17u);
|
|
EXPECT_EQ(head->Resource.Gen, 3u);
|
|
EXPECT_EQ(head->RangeCount, 1u);
|
|
const auto* range = reinterpret_cast<const Transport::EventRange*>(head + 1);
|
|
EXPECT_EQ(range->Size, MG_Pipe::kMGPipeWholeBuffer);
|
|
} else {
|
|
const auto* head = static_cast<const Transport::EventBufferWritebackHead*>(event.payload);
|
|
EXPECT_EQ(head->Offset, 24u);
|
|
EXPECT_EQ(head->Size, 4u);
|
|
const Uint8 expected[] = {11, 22, 33, 44};
|
|
EXPECT_EQ(std::memcmp(head + 1, expected, sizeof(expected)), 0);
|
|
}
|
|
}
|
|
Transport::RingRecordView extra;
|
|
EXPECT_FALSE(events.Pop(extra));
|
|
events.Drained();
|
|
}
|
|
|
|
TEST(P5fReverseChannel, ANonSingletonSessionOwnsAllFourReverseCallbacks) {
|
|
FvGlobals restore;
|
|
ASSERT_EQ(Server::ServerSession::Active(), nullptr);
|
|
ASSERT_FALSE(Server::ServerSessionInstance().Accepted());
|
|
Server::ServerSession owner;
|
|
FvSession fixture;
|
|
ASSERT_TRUE(fixture.Handshake(&owner));
|
|
ASSERT_EQ(Server::ServerSession::Active(), &owner);
|
|
ASSERT_NE(fixture.session, &Server::ServerSessionInstance());
|
|
ASSERT_TRUE(fixture.StartLoop());
|
|
ASSERT_EQ(Server::ServerLoopInstance().RunProbeOnApplyThreadForTesting(
|
|
+[](void*) -> MobileGLResult {
|
|
MG_Pipe::gMGPipeCallbacks.OnGlError(static_cast<Uint32>(ErrorCode::InvalidOperation),
|
|
"non-singleton owner");
|
|
const MG_Pipe::MGPRange whole{0, MG_Pipe::kMGPipeWholeBuffer};
|
|
MG_Pipe::gMGPipeCallbacks.OnGpuWritten({41, 7}, 1, &whole);
|
|
const Uint8 bytes[] = {17, 34, 51, 68};
|
|
MG_Pipe::gMGPipeCallbacks.OnBufferWriteback({41, 7}, 20,
|
|
MG_Pipe::MGPBlobRef{reinterpret_cast<Uint64>(bytes), sizeof(bytes),
|
|
MG_Pipe::kMGHostSpanSegNone, 0});
|
|
MG_Pipe::MGPSurfaceInfo surface{};
|
|
surface.Width = 321;
|
|
surface.Height = 123;
|
|
surface.InternalFormat = static_cast<Uint32>(TextureInternalFormat::RGBA8);
|
|
surface.Samples = 4;
|
|
surface.Layers = 2;
|
|
surface.IsDefault = 1;
|
|
MG_Pipe::gMGPipeCallbacks.OnSurfaceChanged(&surface);
|
|
return MOBILEGL_OK;
|
|
}, nullptr), MOBILEGL_OK);
|
|
|
|
Transport::EventRingConsumer events(fixture.clientSegments.EventControl(),
|
|
fixture.clientSegments.CmdControl(), fixture.clientSegments.EventRingBase(),
|
|
fixture.clientSegments.EventRingCapacity(), fixture.clientSegments.EventSegmentBase());
|
|
ASSERT_TRUE(events.Valid());
|
|
Transport::RingRecordView event;
|
|
ASSERT_TRUE(events.Pop(event));
|
|
ASSERT_EQ(event.kind, Transport::kEventGlError);
|
|
const auto* error = static_cast<const Transport::EventGlErrorHead*>(event.payload);
|
|
EXPECT_EQ(error->Code, static_cast<Uint32>(ErrorCode::InvalidOperation));
|
|
EXPECT_STREQ(reinterpret_cast<const char*>(error + 1), "non-singleton owner");
|
|
|
|
ASSERT_TRUE(events.Pop(event));
|
|
ASSERT_EQ(event.kind, Transport::kEventGpuWritten);
|
|
const auto* written = static_cast<const Transport::EventGpuWrittenHead*>(event.payload);
|
|
EXPECT_EQ(written->Resource.Slot, 41u);
|
|
EXPECT_EQ(written->Resource.Gen, 7u);
|
|
ASSERT_EQ(written->RangeCount, 1u);
|
|
const auto* range = reinterpret_cast<const Transport::EventRange*>(written + 1);
|
|
EXPECT_EQ(range->Offset, 0u);
|
|
EXPECT_EQ(range->Size, MG_Pipe::kMGPipeWholeBuffer);
|
|
|
|
ASSERT_TRUE(events.Pop(event));
|
|
ASSERT_EQ(event.kind, Transport::kEventBufferWriteback);
|
|
const auto* writeback = static_cast<const Transport::EventBufferWritebackHead*>(event.payload);
|
|
EXPECT_EQ(writeback->Resource.Slot, 41u);
|
|
EXPECT_EQ(writeback->Resource.Gen, 7u);
|
|
EXPECT_EQ(writeback->Offset, 20u);
|
|
ASSERT_EQ(writeback->Size, 4u);
|
|
const Uint8 expected[] = {17, 34, 51, 68};
|
|
EXPECT_EQ(std::memcmp(writeback + 1, expected, sizeof(expected)), 0);
|
|
|
|
ASSERT_TRUE(events.Pop(event));
|
|
ASSERT_EQ(event.kind, Transport::kEventSurfaceChanged);
|
|
const auto* surface = static_cast<const Transport::EventSurfaceChangedHead*>(event.payload);
|
|
EXPECT_EQ(surface->Width, 321u);
|
|
EXPECT_EQ(surface->Height, 123u);
|
|
EXPECT_EQ(surface->InternalFormat, static_cast<Uint32>(TextureInternalFormat::RGBA8));
|
|
EXPECT_EQ(surface->Samples, 4u);
|
|
EXPECT_EQ(surface->Layers, 2u);
|
|
EXPECT_EQ(surface->IsDefault, 1u);
|
|
EXPECT_FALSE(events.Pop(event));
|
|
events.Drained();
|
|
EXPECT_FALSE(Server::ServerSessionInstance().Accepted());
|
|
}
|
|
|
|
#if !defined(_WIN32)
|
|
TEST(P5fReverseChannel, CachedReverseCallbacksRejectAMissingSessionOwner) {
|
|
FvGlobals restore;
|
|
Server::ServerSession owner;
|
|
FvSession fixture;
|
|
ASSERT_TRUE(fixture.Handshake(&owner));
|
|
const auto callbacks = MG_Pipe::gMGPipeCallbacks;
|
|
fixture.Stop();
|
|
ASSERT_EQ(Server::ServerSession::Active(), nullptr);
|
|
const char* names[] = {"OnGlError", "OnGpuWritten", "OnBufferWriteback", "OnSurfaceChanged"};
|
|
for (Uint32 callback = 0; callback < 4; ++callback) {
|
|
const auto logSize = ReadLog().size();
|
|
EXPECT_EXIT({
|
|
switch (callback) {
|
|
case 0: callbacks.OnGlError(0, "retired"); break;
|
|
case 1: callbacks.OnGpuWritten(MG_Pipe::kMGPipeNullHandle, 0, nullptr); break;
|
|
case 2: callbacks.OnBufferWriteback(MG_Pipe::kMGPipeNullHandle, 0, MG_Pipe::MGPBlobRef{}); break;
|
|
case 3: callbacks.OnSurfaceChanged(nullptr); break;
|
|
}
|
|
std::_Exit(9);
|
|
}, ::testing::KilledBySignal(SIGABRT), "");
|
|
const auto marker = String(names[callback]) + ".session-missing";
|
|
EXPECT_NE(ReadLog().substr(logSize).find(marker), String::npos) << marker;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
int main(int argc, char** argv) {
|
|
// Before anything logs: MG_Util::Debug::InitFile() reads the variable once, on the first
|
|
// write, and caches the FILE*. The name carries this process's pid, because
|
|
// gtest_discover_tests runs every case as its own process, in parallel under ctest -j.
|
|
namespace fs = std::filesystem;
|
|
const fs::path path =
|
|
fs::temp_directory_path() / ("mobilegl-serverloop-test-" + std::to_string(ProcessId()) + ".log");
|
|
std::error_code ec;
|
|
fs::remove(path, ec);
|
|
g_logPath = path.string();
|
|
#if defined(_WIN32)
|
|
_putenv_s("MOBILEGL_LOG_FILE_PATH", g_logPath.c_str());
|
|
#else
|
|
setenv("MOBILEGL_LOG_FILE_PATH", g_logPath.c_str(), 1);
|
|
#endif
|
|
// THIS PROCESS IS A SPLIT ONE. Everything under test reads MG_Config::Transport - the
|
|
// staged-shadow arm, the applier's stamp path, ConfigLoader's own knobs - and a suite that
|
|
// left it at Monolith would be a server test running the monolith answers, which is the
|
|
// failure this phase is built to make impossible.
|
|
MG_Config::Transport = MG_Config::TransportMode::InProcess;
|
|
::testing::InitGoogleTest(&argc, argv);
|
|
const int rc = RUN_ALL_TESTS();
|
|
fs::remove(path, ec);
|
|
return rc;
|
|
}
|