Files
MobileGL/MobileGL/MG_Test/Wire/RemoteClientTest.cpp
T
swung0x48 1e9f8bb0ff [Refactor] (docs/ci, MG_Impl/Pipe, MG_Remote/Client, MG_Test/Wire, scripts/data): drop the E2 drop knobs
- MOBILEGL_IPC_E2_DROP_DRAW and MOBILEGL_IPC_E2_DROP_CLEAR leave the shipped emitter with their per-draw and per-clear branches, setters, counters and per-frame evidence line; nothing sets them since the dropped-draws control left CI
- the wire-volume assertion on the OpenRA split legs replaces the control (758 DrawVbo records and 758/788 server draws locally, both backends, inproc and spawn)
- retire the unit case RemoteEmitTable.TheE2DropSwitchStartsDisarmed, which only toggled the clear setter; skip census: removed from junit-integration-split-strict/unit-strict (test/unit and unit-split borrow that list)
2026-10-17 15:59:52 -04:00

3512 lines
188 KiB
C++

// MobileGL - MobileGL/MG_Test/Wire/RemoteClientTest.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
// P5 package c1's suite: the 73-slot emit table, the caps mirror and R-8's liveness gates.
// The helper cases below are supplemented by RemoteClientControls.inc: installed producers
// over a live session, with adversarial peer replies and a repeated-make-current control.
//
// IT LINKS gtest RATHER THAN gtest_main AND CARRIES ITS OWN main(), for PipeWireCodecTest's and
// PipeInputsTest's reason: the Fatal arms report through MGLOG_F + std::abort, and MGLOG_F
// writes to STDOUT and to a named file, NEVER to stderr - so EXPECT_DEATH's stderr regex could
// only ever match the empty string. A case that drives one FORKS and reads the Fatal line back
// out of a log file this process names before anything logs. That is what makes each control
// assert ITS OWN failure string (R-16) instead of asserting that something, somewhere, died.
#include <gtest/gtest.h>
#include <MG_Util/Debug/Log.h>
#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <sstream>
#include <vector>
#include <string>
#include "Includes.h"
#include <Config.h>
#include <MG_Pipe/MGPipe.h>
#include <MG_Remote/CapsCodec.h>
#include <MG_Remote/Client/CapsMirror.h>
#include <MG_Remote/Transport/ReplySlot.h>
#include <MG_Remote/Transport/LinkMetrics.h>
// P12: StreamLink::kRetainedAnswersMax, the store capacity RemoteClientControls' window-ceiling
// case reads instead of restating it.
#include <MG_Remote/Transport/StreamLink.h>
#include <MG_Pipe/PipeRoute.h>
#include <MG_Remote/Client/EmitTables.h>
#include <MG_Remote/Client/WireTables.h>
#include <MG_Remote/Client/ClientSession.h>
#include <MG_State/GLState/BufferState/PersistentMapTracker.h>
#include <MG_Remote/Client/BackendObject_Remote.h>
#include <MG_Remote/Server/ServerLoop.h>
#include <MG_Remote/Server/ServerSession.h>
#include <MG_State/GLState/Core.h>
#include <MG_Impl/GLImpl/Texture/GL_Texture.h>
#include <MG_Impl/GLImpl/Buffer/GL_Buffer.h>
#include <MG_Pipe/PipeMutation.h>
#include <MG_Pipe/PipeApply.h>
#include <MG_Backend/MGPipe/PipeInputs.h>
#include <MG_Backend/DirectGLES/Managers.h>
#include <MG_Impl/Pipe/PipeFill.h>
// P12: MGPipeTextureEmitterInstance(), so a control case can name the handle of an object it
// created and read the publication latch the converted rows read.
#include <MG_Impl/Pipe/TextureEmit.h>
#include <Init.h>
#include <MG_Impl/EGLImpl/EGLImpl.h>
#include <MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h>
#include <MG_Impl/GLImpl/Drawing/GL_Drawing.h>
#include <MG_Impl/GLImpl/RenderState/GL_RenderState.h>
#include <atomic>
#include <chrono>
#include <thread>
#if !defined(_WIN32)
#include <csignal>
#include <sys/wait.h>
#include <unistd.h>
#define MGTEST_HAVE_FORK 1
#else
#define MGTEST_HAVE_FORK 0
#endif
using namespace MobileGL;
using namespace MobileGL::MG_Pipe;
using namespace MobileGL::MG_Remote;
using namespace MobileGL::MG_Remote::Client;
namespace {
std::string g_logPath;
// BOTH ROLES' LOGS. P6 gives the client and the server role a file each, and a death test
// asserts that the CHILD said something - which role's thread said it is not what these
// cases are about. The refusals raised on the apply thread (RefuseFromApplyThread and every
// guard that rides it) are written under the SERVER role by construction, so a reader that
// took only g_logPath would report "the child aborted, but said nothing" for the very
// diagnostics it exists to check.
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());
}
int ProcessId() {
#if defined(_WIN32)
return static_cast<int>(::_getpid());
#else
return static_cast<int>(::getpid());
#endif
}
// A caps snapshot the client could plausibly have received, with every field distinct from
// its default so a mirror that answered from a zeroed struct cannot look like one that
// adopted. THIS IS NOT THE STATE UNDER TEST - it is the INPUT to Adopt(), which is the
// producer; the assertions below read what the mirror hands the frontend's own accessors
// back, never what this function wrote (R-16).
struct Snapshot {
MGPCaps Caps{};
MG_Backend::FormatCapabilityCache Formats{};
RendererInfo Renderer{};
String ApiVersion;
BackendType Backend = BackendType::DirectGLES;
};
Snapshot MakeSnapshot(Uint64 consumedSubsystems, Uint64 capBits) {
Snapshot s;
s.Caps.CallMask = capBits | MGCapsConsumerBits(consumedSubsystems);
s.Caps.Dynamic.MaxComputeWorkGroupCount[0] = 65531;
s.Caps.Dynamic.MaxComputeWorkGroupCount[1] = 65532;
s.Caps.Dynamic.MaxComputeWorkGroupCount[2] = 65533;
s.Caps.Dynamic.MaxComputeWorkGroupSize[0] = 1021;
s.Caps.Dynamic.MaxComputeWorkGroupSize[1] = 1022;
s.Caps.Dynamic.MaxComputeWorkGroupSize[2] = 1023;
s.Caps.Dynamic.UniformBufferOffsetAlignment = 64;
s.Renderer.RendererName = "MobileGL Remote Test Renderer";
s.Renderer.BackendName = "Espryt";
s.Renderer.ExtraVendor = String{"c1"};
s.Renderer.RendererGLInfo.TargetGLVersion = Version{4, 6, 0, {}, {}};
s.Renderer.RendererGLInfo.TargetGLSLVersion = Version{4, 6, 0, {}, {}};
s.ApiVersion = "4.6";
return s;
}
void AdoptSnapshot(const Snapshot& s) {
CapsMirrorInstance().Adopt(s.Caps, s.Formats, s.Renderer, s.ApiVersion, s.Backend);
}
#if MGTEST_HAVE_FORK
struct ChildResult {
int Status = -1;
std::string Log;
};
template <class Body>
ChildResult RunInChild(Body body) {
ChildResult result;
// TRUNCATE, RATHER THAN REMEMBER AN OFFSET, and the difference is not cosmetic: this
// parent never logs, so MG_Util::Debug's FILE* is opened FOR THE FIRST TIME by each
// child - with "w", which truncates. An offset taken before the fork therefore points
// past the end of the child's own log, and `substr` hands back the tail of a DIFFERENT
// child's output. That is how the second death case in this file came to see the first
// one's slot name and assert on it: a control reading another control's message, which
// is one of the three shapes R-16 was written after.
MobileGL::MG_Util::Debug::TruncateRoleLogs(g_logPath.c_str());
std::fflush(nullptr);
const pid_t pid = ::fork();
if (pid < 0) return result;
if (pid == 0) {
body();
::_exit(0);
}
int status = 0;
if (::waitpid(pid, &status, 0) != pid) return result;
result.Status = status;
result.Log = ReadLog();
return result;
}
bool DiedOfAbort(const ChildResult& r) {
return WIFSIGNALED(r.Status) && WTERMSIG(r.Status) == SIGABRT;
}
std::string DescribeStatus(const ChildResult& r) {
if (r.Status < 0) return "fork/waitpid failed";
if (WIFEXITED(r.Status)) return "exited " + std::to_string(WEXITSTATUS(r.Status));
if (WIFSIGNALED(r.Status)) return "signal " + std::to_string(WTERMSIG(r.Status));
return "status " + std::to_string(r.Status);
}
#endif
} // namespace
// =====================================================================================
// The emit table: the partition, and that no slot is null
// =====================================================================================
TEST(RemoteEmitTable, TheThreeClassesPartitionEverySlot) {
// P5 baseline five + f1 thirteen + i1 seven + t2 six emitted slots, ... and since P10 B the
// swap interval: class C is EMPTY, every slot but the two local answers reaches the server.
EXPECT_EQ(LocallyAnsweredSlotCount(), 2u);
EXPECT_EQ(ImplementedVerbCount(), 71u);
EXPECT_EQ(UnmigratedSlotCount(), 0u);
EXPECT_EQ(LocallyAnsweredSlotCount() + ImplementedVerbCount() + UnmigratedSlotCount(), kRemoteEmitSlotCount);
}
TEST(RemoteEmitTable, TheSixXfbAndPatchSlotsAreNonNullAndDistinct) {
// P5b t2 (CONTRACT-P5B.md §2 t2), the half that needs no fork: the six slots exist and are
// six DIFFERENT functions. Six identical pointers would be one emitter assigned six times,
// which is how a copy-paste flip loses five records and still passes every count.
//
// Red once by assigning `table.GL.PauseTransformFeedback = &EmitResumeTransformFeedback;`
// in BuildRemoteEmitTable - the exact copy-paste this guards: "t2 slots 2 and 3 are one
// function".
const MG_Backend::GlobalBackendFunctionsTable& table = RemoteEmitTable();
const void* const six[] = {
reinterpret_cast<const void*>(table.GL.BeginTransformFeedback),
reinterpret_cast<const void*>(table.GL.EndTransformFeedback),
reinterpret_cast<const void*>(table.GL.PauseTransformFeedback),
reinterpret_cast<const void*>(table.GL.ResumeTransformFeedback),
reinterpret_cast<const void*>(table.GL.BindTransformFeedback),
reinterpret_cast<const void*>(table.GL.PatchParameteri),
};
for (SizeT i = 0; i < 6; ++i) {
EXPECT_NE(six[i], nullptr) << "t2 slot " << i << " is null";
for (SizeT j = i + 1; j < 6; ++j) {
EXPECT_NE(six[i], six[j]) << "t2 slots " << i << " and " << j << " are one function";
}
}
// And the one XFB slot t2 does NOT flip is still there to be Fatal - CONTRACT-P5B.md gives
// DeleteTransformFeedback no row (unmeasured), so it must not have been swept up.
EXPECT_NE(table.GL.DeleteTransformFeedback, nullptr);
}
#if MGTEST_HAVE_FORK
TEST(RemoteEmitTable, EachXfbAndPatchSlotIsClassBAndDemandsASessionByItsOwnName) {
// P5b t2, THE HALF THAT DECIDES THE CLASS. A pointer comparison cannot tell a class-B
// emitter from a class-C thunk - each unmigrated slot gets its own generated function, so
// every slot in the table is already a distinct non-null address. What distinguishes them is
// WHAT THEY SAY when called with no ClientSession: an emitter reaches RequireSession and
// dies Fatal{NoClientSession, "<slot>"}; a thunk dies Fatal{UnmigratedVerb, "<slot>"}. Both
// strings are asserted, because a case that only looked for the first would be satisfied by
// a build where every one of these had been flipped by accident.
//
// Red once by SWAPPING the Pause and Resume assignments in BuildRemoteEmitTable - the two
// emitters with the same signature, so the swap compiles and neither is orphaned (the first
// attempt redirected one slot at another's emitter and the build failed on the signature
// and on -Wunused-function, which is a control that did not run). The child called through
// PauseTransformFeedback died Fatal{NoClientSession, "ResumeTransformFeedback"} and this
// case failed with "PauseTransformFeedback did not reach the class-B emitter's session
// demand", so the string really is the slot's own name and not a shared constant.
struct Slot {
const char* Name;
void (*Call)();
};
static const Slot kSlots[] = {
{"BeginTransformFeedback", [] { RemoteEmitTable().GL.BeginTransformFeedback(0x0004); }},
{"EndTransformFeedback", [] { RemoteEmitTable().GL.EndTransformFeedback(); }},
{"PauseTransformFeedback", [] { RemoteEmitTable().GL.PauseTransformFeedback(); }},
{"ResumeTransformFeedback", [] { RemoteEmitTable().GL.ResumeTransformFeedback(); }},
{"BindTransformFeedback", [] { RemoteEmitTable().GL.BindTransformFeedback(0); }},
{"PatchParameteri", [] { RemoteEmitTable().GL.PatchParameteri(0x8E72, 3); }},
};
for (const Slot& slot : kSlots) {
const ChildResult r = RunInChild([&slot] { slot.Call(); });
ASSERT_TRUE(DiedOfAbort(r)) << slot.Name << ": " << DescribeStatus(r) << "\n" << r.Log;
EXPECT_NE(r.Log.find(std::string("Fatal{NoClientSession, \"") + slot.Name + "\"}"), std::string::npos)
<< slot.Name << " did not reach the class-B emitter's session demand:\n"
<< r.Log;
EXPECT_EQ(r.Log.find("Fatal{UnmigratedVerb"), std::string::npos) << slot.Name << " is still class C:\n"
<< r.Log;
}
}
TEST(RemoteEmitTable, DeleteTransformFeedbackHasNoRowAndStillAbortsByItsOwnName) {
// CONTRACT-P5B.md §2 t2 and c0b-v1.md §6: the seventh slot in t2's ownership block gets NO
// row in P5b - it is unmeasured, the driver object leaks on the server until P9's XFB
// namespace work, and a bind of name 0 is what the backend does on delete of the bound one.
// That is a RULING, so it is pinned rather than left to be re-derived from a count: a later
// round that gives it a row has to delete this case and say why.
//
// Red once by assigning `table.GL.DeleteTransformFeedback = &EmitBindTransformFeedback;` in
// BuildRemoteEmitTable - a package sweeping the whole XFB family into class B: the child
// died Fatal{NoClientSession, "BindTransformFeedback"} and the UnmigratedVerb expectation
// failed.
const ChildResult r = RunInChild([] { RemoteEmitTable().GL.DeleteTransformFeedback(7); });
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
EXPECT_NE(r.Log.find("Fatal{NoClientSession, \"DeleteTransformFeedback\"}"), std::string::npos) << r.Log;
EXPECT_EQ(r.Log.find("Fatal{UnmigratedVerb"), std::string::npos) << r.Log;
}
#endif // MGTEST_HAVE_FORK
TEST(RemoteEmitTable, NoSlotIsNull) {
// R-4's whole rule, asserted over the STRUCT rather than over the list that built it. 91
// MG_Impl sites call through this table directly; a null slot is 91 potential null calls,
// and the one thing a list-driven check could not catch is a slot the list forgot to name.
//
// Walked as a block of function pointers because that is exactly what the struct is - the
// static_asserts in EmitTables.cpp pin that shape - so a slot ADDED to GLFunctionsTable is
// covered here on the day it appears, without this file being edited.
const MG_Backend::GlobalBackendFunctionsTable& table = RemoteEmitTable();
const void* const* cells = reinterpret_cast<const void* const*>(&table);
const SizeT cellCount = sizeof(table) / sizeof(void*);
// The struct is 73 function pointers plus ONE cell holding the packed Bool
// PrefersCpuXfbPrimitiveAccounting and its padding (EmitTables.cpp static_asserts exactly
// that shape). That Bool is legitimately zero when the server did not publish
// kCapCpuXfbPrimitiveAccounting, so at most one cell may read null - and this is stated as
// a bound rather than an index, because an index would drift the day a slot is inserted.
SizeT nullCells = 0;
for (SizeT i = 0; i < cellCount; ++i) {
if (cells[i] == nullptr) ++nullCells;
}
EXPECT_LE(nullCells, 1u) << "a slot in the remote emit table is null (" << nullCells << " null cells out of "
<< cellCount
<< "). R-4 forbids it: 91 MG_Impl sites call through this table with no null check at all";
}
TEST(RemoteEmitTable, TheFiveEmittersAreTheOnesTheCensusMeasured) {
const MG_Backend::GlobalBackendFunctionsTable& table = RemoteEmitTable();
// Named, so that a table which emitted a DIFFERENT five would be red rather than merely
// counted. The census's answer is Clear, DrawArrays, ReadPixels, BlitFramebuffer, Present.
EXPECT_NE(table.GL.Clear, nullptr);
EXPECT_NE(table.GL.DrawArrays, nullptr);
EXPECT_NE(table.GL.ReadPixels, nullptr);
EXPECT_NE(table.GL.BlitFramebuffer, nullptr);
EXPECT_NE(table.Present, nullptr);
// And the two R-15 answers them locally, so they are not the same pointer as any Fatal one.
EXPECT_NE(table.GL.GetIntegeri_v, nullptr);
EXPECT_NE(table.GL.IsTimerQuerySupported, nullptr);
EXPECT_NE(reinterpret_cast<const void*>(table.GL.DrawArrays), reinterpret_cast<const void*>(table.GL.DrawElements))
<< "DrawArrays is class B and DrawElements is class C; they cannot share a thunk";
}
#if MGTEST_HAVE_FORK
TEST(RemoteEmitTable, AnUnmigratedShapeRefusalAbortsAndNamesItself) {
// THE DEATH TEST ON THE UnmigratedVerbFatal ARM. It asserts the exact wording, not merely
// that the child died: a control that trips on any abort is satisfied by the wrong abort,
// which is one of the three shapes R-16 was written after.
// NO SLOT IS CLASS C ANY MORE: P10 B retired SetSwapInterval, the last one and this case's
// subject until then. The funnel is still the named refusal of a class-B emitter's
// unrepresentable SHAPE (RefuseDrawByName, EmitF1Clear's default arm), and every refusal
// left on it is spelled "<slot>+<QUALIFIER>" - so that is what is driven here, through the
// funnel itself, because every emitter that reaches it needs a session and a context first.
const ChildResult r = RunInChild([] { UnmigratedVerbFatal("MultiDrawElementsBaseVertex+CLIENT_INDICES"); });
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
EXPECT_NE(r.Log.find("Fatal{UnmigratedVerb, \"MultiDrawElementsBaseVertex+CLIENT_INDICES\"}"), std::string::npos)
<< r.Log;
}
TEST(RemoteEmitTable, EachUnmigratedShapeRefusalNamesItsOwnShape) {
// The half the case above cannot state on its own: that the name in the message is the
// caller's and not a constant. Two different shapes, two different names.
const ChildResult r = RunInChild([] { UnmigratedVerbFatal("DrawArraysIndirect+CLIENT_COMMANDS"); });
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
EXPECT_NE(r.Log.find("Fatal{UnmigratedVerb, \"DrawArraysIndirect+CLIENT_COMMANDS\"}"), std::string::npos) << r.Log;
EXPECT_EQ(r.Log.find("MultiDrawElementsBaseVertex"), std::string::npos)
<< "the Fatal message names a shape other than the one that was refused:\n"
<< r.Log;
}
// P5b d1: a class-B draw slot with no session aborts Fatal{NoClientSession, "<slot>"} - the
// class-B shape - and NOT Fatal{UnmigratedVerb}: that is what distinguishes a flipped slot from
// the stub it replaced, by behaviour rather than by pointer. Red once by: moving DrawElements
// back into MGR_UNMIGRATED_D1_SLOTS - the log then reads UnmigratedVerb.
TEST(RemoteEmitTable, AFlippedDrawSlotDemandsASessionRatherThanNamingItselfUnmigrated) {
const ChildResult r = RunInChild(
[] { RemoteEmitTable().GL.DrawElements(0x0004 /*GL_TRIANGLES*/, 3, 0x1405 /*GL_UNSIGNED_INT*/, nullptr); });
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
EXPECT_NE(r.Log.find("Fatal{NoClientSession, \"DrawElements\"}"), std::string::npos) << r.Log;
EXPECT_EQ(r.Log.find("Fatal{UnmigratedVerb"), std::string::npos)
<< "DrawElements is class B since d1 and must not name itself unmigrated:\n"
<< r.Log;
}
TEST(RemoteEmitTable, SetSwapIntervalIsTheServerForwarderAndNotAFatal) {
// The slot the verb census found by NOT mirroring GLImpl: SetSwapInterval has zero MG_Impl
// call sites and is reached only through the EGL path (BackendObject::SetEGLSwapInterval),
// so a table built from the 89 GLImpl sites would have left it null. It was the last class-C
// slot; since P10 B it IS the control-frame forwarder the old BackendObject_Remote override
// called. With no apply loop in this child the forwarder answers NOT_INITIALIZED and returns
// (ServerLoop.cpp, PostSurfaceControlFrameWithCallerLock), so the child EXITS - the P5 stub
// died Fatal{UnmigratedVerb, "SetSwapInterval"} right here. That the frame then reaches a
// running server is ServerLoopTest's TheEmitTableSwapIntervalSlotCrossesAsOneDispatchedFrame.
EXPECT_EQ(reinterpret_cast<const void*>(RemoteEmitTable().SetSwapInterval),
reinterpret_cast<const void*>(&MobileGL::MG_Remote::Server::ServerSetEGLSwapInterval));
const ChildResult r = RunInChild([] { RemoteEmitTable().SetSwapInterval(1); });
ASSERT_TRUE(WIFEXITED(r.Status) && WEXITSTATUS(r.Status) == 0) << DescribeStatus(r) << "\n" << r.Log;
EXPECT_EQ(r.Log.find("Fatal{UnmigratedVerb"), std::string::npos) << r.Log;
}
#endif // MGTEST_HAVE_FORK
// ---- P5b package i1 (docs/archive/contracts/CONTRACT-P5B.md §2 i1) -------------------------------------
TEST(RemoteEmitTable, TheSevenI1SlotsAreClassBAndAreNotTheFatalThunk) {
// The census's five measured slots plus the two companions that share their rows. Named
// rather than counted, so a table that flipped a DIFFERENT seven is red here and not only
// in the arithmetic. This case used to compare each slot against a slot that was still
// class C; since P10 B there is none left to compare against, so the class is decided
// behaviourally by EachI1SlotReachesRequireSessionUnderItsOwnName below and this case keeps
// the pointer facts that need no fork.
const MG_Backend::GlobalBackendFunctionsTable& table = RemoteEmitTable();
const void* const i1[] = {
reinterpret_cast<const void*>(table.GL.BindImageTexture),
reinterpret_cast<const void*>(table.GL.DispatchCompute),
reinterpret_cast<const void*>(table.GL.DispatchComputeIndirect),
reinterpret_cast<const void*>(table.GL.MemoryBarrier),
reinterpret_cast<const void*>(table.GL.MemoryBarrierByRegion),
reinterpret_cast<const void*>(table.GL.CopyImageSubData),
reinterpret_cast<const void*>(table.GL.ShaderStorageBlockBinding),
};
static const char* const kNames[] = {
"BindImageTexture", "DispatchCompute", "DispatchComputeIndirect", "MemoryBarrier",
"MemoryBarrierByRegion", "CopyImageSubData", "ShaderStorageBlockBinding"};
for (SizeT i = 0; i < sizeof(i1) / sizeof(i1[0]); ++i) {
EXPECT_NE(i1[i], nullptr) << kNames[i] << " is null";
}
// The two barrier slots and the two dispatch slots share a WIRE ROW but not an emitter: the
// discriminant (ByRegion / IsIndirect) is set by the emitter, so one thunk for both would
// carry the wrong one.
EXPECT_NE(i1[3], i1[4]) << "MemoryBarrier and MemoryBarrierByRegion share memory_barrier (61) "
"but must set opposite ByRegion values";
EXPECT_NE(i1[1], i1[2]) << "DispatchCompute and DispatchComputeIndirect share launch_grid (60) "
"but must set opposite IsIndirect values";
}
#if MGTEST_HAVE_FORK
TEST(RemoteEmitTable, AClassBSlotWithNoSessionAbortsRatherThanFallingThrough) {
// The other half of "no slot may fall through to the driver". With no ClientSession the
// emitter has nowhere to put the record, and the one thing it may not do is return quietly:
// that is the split lane running monolith and going green.
const ChildResult r = RunInChild([] { RemoteEmitTable().GL.Clear(0x4000 /*COLOR_BUFFER_BIT*/); });
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
EXPECT_NE(r.Log.find("Fatal{NoClientSession, \"Clear\"}"), std::string::npos) << r.Log;
}
TEST(RemoteEmitTable, EachI1SlotReachesRequireSessionUnderItsOwnName) {
// The behavioural half: an i1 slot is class B, so with no ClientSession it reaches
// RequireSession and aborts Fatal{NoClientSession, "<slot>"} - NOT Fatal{UnmigratedVerb}
// (which would mean the flip never happened) and NOT quietly (which is the split lane
// running monolith and going green, R-4). The name in the message is the slot's own, which
// is the half a single case could not state.
//
// RED ONCE BY DOING X: put `X(MemoryBarrier, void, (GLbitfield))` back in
// MGR_UNMIGRATED_I1_SLOTS and drop `table.GL.MemoryBarrier = &EmitMemoryBarrier;` - the
// MemoryBarrier arm below then finds Fatal{UnmigratedVerb, "MemoryBarrier"} instead.
const ChildResult barrier = RunInChild([] { RemoteEmitTable().GL.MemoryBarrier(0x2000); });
ASSERT_TRUE(DiedOfAbort(barrier)) << DescribeStatus(barrier) << "\n" << barrier.Log;
EXPECT_NE(barrier.Log.find("Fatal{NoClientSession, \"MemoryBarrier\"}"), std::string::npos) << barrier.Log;
EXPECT_EQ(barrier.Log.find("Fatal{UnmigratedVerb"), std::string::npos)
<< "MemoryBarrier is class B from P5b i1 on:\n"
<< barrier.Log;
const ChildResult dispatch = RunInChild([] { RemoteEmitTable().GL.DispatchCompute(1, 1, 1); });
ASSERT_TRUE(DiedOfAbort(dispatch)) << DescribeStatus(dispatch) << "\n" << dispatch.Log;
EXPECT_NE(dispatch.Log.find("Fatal{NoClientSession, \"DispatchCompute\"}"), std::string::npos) << dispatch.Log;
const ChildResult bind =
RunInChild([] { RemoteEmitTable().GL.BindImageTexture(0, 1, 0, GL_FALSE, 0, 0x88BA, 0x8058); });
ASSERT_TRUE(DiedOfAbort(bind)) << DescribeStatus(bind) << "\n" << bind.Log;
EXPECT_NE(bind.Log.find("Fatal{NoClientSession, \"BindImageTexture\"}"), std::string::npos) << bind.Log;
const ChildResult ssbo = RunInChild([] { RemoteEmitTable().GL.ShaderStorageBlockBinding(1, "Blk", 2); });
ASSERT_TRUE(DiedOfAbort(ssbo)) << DescribeStatus(ssbo) << "\n" << ssbo.Log;
EXPECT_NE(ssbo.Log.find("Fatal{NoClientSession, \"ShaderStorageBlockBinding\"}"), std::string::npos) << ssbo.Log;
const ChildResult copy = RunInChild([] {
const MG_Backend::CopyImageEndpoint src{};
const MG_Backend::CopyImageEndpoint dst{};
RemoteEmitTable().GL.CopyImageSubData(src, 0x0DE1, 0, 0, 0, 0, dst, 0x0DE1, 0, 0, 0, 0, 1, 1, 1);
});
ASSERT_TRUE(DiedOfAbort(copy)) << DescribeStatus(copy) << "\n" << copy.Log;
EXPECT_NE(copy.Log.find("Fatal{NoClientSession, \"CopyImageSubData\"}"), std::string::npos) << copy.Log;
}
#endif // MGTEST_HAVE_FORK
// =====================================================================================
// The caps mirror: the three read paths the acceptance names
// =====================================================================================
TEST(CapsMirrorTest, GlGetStringReadsTheRendererStringsBackOutOfTheMirror) {
// glGetString's path is GL_Getter.cpp:596 -> pActiveBackendObject->GetRendererInfo(), which
// BackendObject_Remote answers from this mirror BY REFERENCE - so the test reads the
// reference, holds it across a second adoption, and requires it to follow. A mirror that
// handed back a temporary would pass an equality check and dangle here.
const Snapshot first = MakeSnapshot(kMGPipeSubsystemResources, 0);
AdoptSnapshot(first);
const RendererInfo& bound = CapsMirrorInstance().Renderer();
EXPECT_EQ(bound.RendererName, "MobileGL Remote Test Renderer");
EXPECT_EQ(bound.BackendName, "Espryt");
ASSERT_TRUE(bound.ExtraVendor.has_value());
EXPECT_EQ(*bound.ExtraVendor, "c1");
Snapshot second = MakeSnapshot(kMGPipeSubsystemResources, 0);
second.Renderer.RendererName = "A Different Device";
AdoptSnapshot(second);
EXPECT_EQ(bound.RendererName, "A Different Device")
<< "GetRendererInfo() returns a reference, so a re-arrival must be visible through a "
"reference a caller already holds";
}
TEST(CapsMirrorTest, GlGetIntegervReadsTheDynamicParametersBackOutOfTheMirror) {
// glGetIntegerv's limit family is GL_Getter.cpp:2400 -> GetDynamicParameters(), which binds
// a reference and then reads many members - which is why a partial snapshot is not an
// option and why the whole struct crosses.
AdoptSnapshot(MakeSnapshot(kMGPipeSubsystemResources, 0));
const MG_Backend::DynamicBackendParameters& dynamic = CapsMirrorInstance().Dynamic();
EXPECT_EQ(dynamic.MaxComputeWorkGroupCount[0], 65531);
EXPECT_EQ(dynamic.MaxComputeWorkGroupCount[2], 65533);
EXPECT_EQ(dynamic.MaxComputeWorkGroupSize[1], 1022);
EXPECT_EQ(dynamic.UniformBufferOffsetAlignment, 64u);
}
namespace {
// The client's compile switch, pinned for one case and put back after it: since P10 B the
// mirror re-derives GL_KHR_parallel_shader_compile from it on every adoption.
class ClientCompileSwitchScope {
public:
explicit ClientCompileSwitchScope(MG_Config::QuirkOverride mode)
: m_saved(MG_Config::Features.AsyncShaderCompile) {
MG_Config::Features.AsyncShaderCompile = mode;
}
~ClientCompileSwitchScope() { MG_Config::Features.AsyncShaderCompile = m_saved; }
ClientCompileSwitchScope(const ClientCompileSwitchScope&) = delete;
ClientCompileSwitchScope& operator=(const ClientCompileSwitchScope&) = delete;
private:
MG_Config::QuirkOverride m_saved;
};
} // namespace
TEST(CapsMirrorTest, GlGetStringiReadsTheAdvertisedExtensionListBackOutOfTheMirror) {
// glGetStringi(GL_EXTENSIONS) is GL_Getter.cpp:654 -> GetRendererInfo().RendererGLInfo, the
// same list CompileEnv.cpp:124 copies into the compile env. Compilation is pinned inline
// here, so the one entry the client decides for itself (the case below) is not in the list
// and what reads back is the server's list exactly.
const ClientCompileSwitchScope inlineCompile(MG_Config::QuirkOverride::ForceOff);
Snapshot s = MakeSnapshot(kMGPipeSubsystemResources, 0);
s.Renderer.RendererGLInfo.Extensions.push_back(E_GL_ARB_timer_query);
AdoptSnapshot(s);
const auto& extensions = CapsMirrorInstance().Renderer().RendererGLInfo.Extensions;
ASSERT_EQ(extensions.size(), 1u);
EXPECT_EQ(extensions[0], E_GL_ARB_timer_query);
EXPECT_EQ(CapsMirrorInstance().Renderer().RendererGLInfo.TargetGLVersion.Major, 4);
}
TEST(CapsMirrorTest, TheParallelShaderCompileEntryIsTheClientsCompileSwitchNotTheServers) {
// P10 B (CapsMirror.cpp, ReconcileClientCompileExtensions). GL_KHR_parallel_shader_compile
// describes the compile pool, and under split the pool is the CLIENT's - so whatever the
// server's list says about it, the mirror answers this process's switch, in both directions,
// and leaves every other entry alone. The integration half is the Tcp arm's
// AsyncOff.ExtensionStringMatchesTheConfiguration, whose fixture server compiles
// asynchronously while the client does not. Red once by removing the reconcile call from
// CapsMirror::Adopt: the first two expectations below fail.
auto count = [](const Vector<GLExtension>& list, GLExtension e) { return std::count(list.begin(), list.end(), e); };
Snapshot s = MakeSnapshot(kMGPipeSubsystemResources, 0);
{
// The server compiles asynchronously and says so; this client compiles inline.
const ClientCompileSwitchScope inlineCompile(MG_Config::QuirkOverride::ForceOff);
s.Renderer.RendererGLInfo.Extensions = {E_GL_ARB_timer_query, E_GL_KHR_parallel_shader_compile};
AdoptSnapshot(s);
const auto& extensions = CapsMirrorInstance().Renderer().RendererGLInfo.Extensions;
EXPECT_EQ(count(extensions, E_GL_KHR_parallel_shader_compile), 0)
<< "the server's advertisement survived a client that compiles inline";
EXPECT_EQ(count(extensions, E_GL_ARB_timer_query), 1);
}
{
// The other way round: the server does not, this client does.
const ClientCompileSwitchScope asyncCompile(MG_Config::QuirkOverride::ForceOn);
s.Renderer.RendererGLInfo.Extensions = {E_GL_ARB_timer_query};
AdoptSnapshot(s);
const auto& extensions = CapsMirrorInstance().Renderer().RendererGLInfo.Extensions;
EXPECT_EQ(count(extensions, E_GL_KHR_parallel_shader_compile), 1)
<< "a client that compiles asynchronously did not advertise it";
EXPECT_EQ(count(extensions, E_GL_ARB_timer_query), 1);
// And a list that already agrees is not touched - no second copy of the entry.
s.Renderer.RendererGLInfo.Extensions = {E_GL_KHR_parallel_shader_compile, E_GL_ARB_timer_query};
AdoptSnapshot(s);
EXPECT_EQ(CapsMirrorInstance().Renderer().RendererGLInfo.Extensions,
(Vector<GLExtension>{E_GL_KHR_parallel_shader_compile, E_GL_ARB_timer_query}));
}
}
TEST(CapsMirrorTest, ReArrivalIsTheInvalidationAndMovesTheGeneration) {
// R-12 has no Invalidate(), so Generation() is the ONLY thing on the client that can see a
// server context death - and a client memo has to key on it.
const Uint64 before = CapsMirrorInstance().Generation();
AdoptSnapshot(MakeSnapshot(kMGPipeSubsystemResources, 0));
const Uint64 after = CapsMirrorInstance().Generation();
EXPECT_EQ(after, before + 1);
AdoptSnapshot(MakeSnapshot(kMGPipeSubsystemResources, 0));
EXPECT_EQ(CapsMirrorInstance().Generation(), after + 1);
EXPECT_TRUE(CapsMirrorInstance().Valid());
}
TEST(CapsMirrorTest, TheBackendTypeIsTheServersAndNeverANewEnumerator) {
Snapshot s = MakeSnapshot(kMGPipeSubsystemResources, 0);
s.Backend = BackendType::DirectVulkan;
AdoptSnapshot(s);
EXPECT_EQ(CapsMirrorInstance().Backend(), BackendType::DirectVulkan);
s.Backend = BackendType::DirectGLES;
AdoptSnapshot(s);
EXPECT_EQ(CapsMirrorInstance().Backend(), BackendType::DirectGLES);
}
TEST(CapsMirrorTest, ThePrefersCpuXfbAnswerComesFromTheCapBitAndNotFromTheTable) {
// GLFunctionsTable::PrefersCpuXfbPrimitiveAccounting is a member of the FUNCTION TABLE,
// which is exactly the thing a split client never receives - so it cannot ride in
// MGPCaps::Dynamic and must come from kCapCpuXfbPrimitiveAccounting.
AdoptSnapshot(MakeSnapshot(kMGPipeSubsystemResources, 0));
EXPECT_FALSE(CapsMirrorInstance().PrefersCpuXfbPrimitiveAccounting());
AdoptSnapshot(MakeSnapshot(kMGPipeSubsystemResources, kCapCpuXfbPrimitiveAccounting));
EXPECT_TRUE(CapsMirrorInstance().PrefersCpuXfbPrimitiveAccounting());
}
// =====================================================================================
// R-8: the liveness gates read the caps mirror, and a family with no consumer says so
// =====================================================================================
TEST(CapsMirrorTest, AMaskWithoutAFamilyRefusesItAndNamesIt) {
// R-8's NEGATIVE CONTROL. "The client emits nothing for a family the server does not
// consume" is, on its own, indistinguishable from "nothing called it" - so the refusal is
// COUNTED at the one funnel that answers the question, and the count is what this asserts.
AdoptSnapshot(MakeSnapshot(kMGPipeSubsystemPrograms, 0));
ResetConsumerRefusalsForTest();
EXPECT_TRUE(CapsMirrorInstance().ServerConsumes(kMGPipeSubsystemPrograms));
EXPECT_EQ(ConsumerRefusals(), 0u) << "a family the server DOES consume must not be counted "
"as refused";
EXPECT_FALSE(CapsMirrorInstance().ServerConsumes(kMGPipeSubsystemResources));
EXPECT_EQ(ConsumerRefusals(), 1u);
EXPECT_EQ(LastRefusedSubsystem(), kMGPipeSubsystemResources)
<< "the refusal must name the family; a counter that only says 'something was withheld' "
"cannot tell five silent families apart";
EXPECT_FALSE(CapsMirrorInstance().ServerConsumes(kMGPipeSubsystemTextureResources));
EXPECT_EQ(ConsumerRefusals(), 2u);
EXPECT_EQ(LastRefusedSubsystem(), kMGPipeSubsystemTextureResources);
}
// =====================================================================================
// F1 (P7 wave 2): the placeholder is not an answer
// =====================================================================================
//
// THE CASE THIS REPLACES SAID THE OPPOSITE, and it was wrong in the one way a green test can
// be. `APlaceholderMirrorConsumesNothing` asserted that a mirror with no snapshot answers
// "no consumer" for every family, and called it "the safe direction ... the client emits
// nothing and the legacy pull path runs". There is no legacy pull path on the CLIENT under
// split. What actually happened, measured on lavapipe and on an Adreno 830: MG_State::Init()
// built GLContext's default texture objects before MG_Backend::Init() had started the session,
// every one of their resource_create records was withheld against callMask=0 at generation 0,
// and the only trace of it was one WARN line. The mirror must refuse, not answer.
TEST(CapsMirrorTest, APlaceholderMirrorRefusesToAnswerAndNamesTheFamily) {
#if MGTEST_HAVE_FORK
// A DEATH, AND ITS OWN STRING (R-16). The family word and the subsystem both have to be in
// the line, because "something aborted" is satisfied by any of the other 43 families.
const ChildResult result = RunInChild([] {
MG_Config::Transport = MG_Config::TransportMode::InProcess;
// AND THE SECOND HALF OF THE ARMING CONDITION, set by hand here because this suite
// does not run MG_ConfigLoader::Init: the gate is for a process whose CONFIGURATION
// asked for a split transport, i.e. one that is going to bring a client half up.
MG_Config::SplitTransportRequestedByConfig = true;
CapsMirror mirror;
EXPECT_FALSE(mirror.Valid());
// No session, no bring-up in flight: the wait cannot succeed and must not be taken.
(void)mirror.ServerConsumes(kMGPipeSubsystemTextureResources);
std::fflush(nullptr);
::_exit(0);
});
ASSERT_TRUE(DiedOfAbort(result)) << DescribeStatus(result) << "\n" << result.Log;
EXPECT_NE(result.Log.find("Fatal{CapsBeforeFirstSnapshot, \"TextureResources\"}"), std::string::npos)
<< "the refusal must name the family that asked; a death with no word is invisible to "
"the census, to run_trace_case.cmake and to every red-once:\n"
<< result.Log;
EXPECT_NE(result.Log.find("0x400"), std::string::npos) << "the subsystem bit belongs in the line beside its word:\n"
<< result.Log;
#else
GTEST_SKIP() << "the Fatal arm is only observable through a fork";
#endif
}
TEST(CapsMirrorTest, ThePlaceholderWaitsForASnapshotAnotherThreadIsFetching) {
#if MGTEST_HAVE_FORK
// THE POSITIVE HALF OF THE WAIT, and the only shape in which "block with a bounded wait" is
// a correct answer to F1: a bring-up that is genuinely in flight, on a thread that is not
// the asking one. The case above is why the wait cannot be the WHOLE fix - the production
// defect had the emitter and the handshake on ONE thread, in sequence, and no wait can
// help there.
//
// DRIVEN THROUGH THE PRODUCTION BRING-UP, not a flag this case sets: MG_Backend::Init() is
// what creates the server's backend, publishes its real mask and starts the session, and
// the in-flight fact the wait reads is ClientSession's own BringUpScope. The server's first
// snapshot is held back by the F1 test knob so the window is wide enough to enter on
// purpose rather than by luck - and by LESS than the wait's own budget, so a wait that is
// taken succeeds instead of expiring.
const ChildResult result = RunInChild([] {
::alarm(30);
::setenv("MOBILEGL_TEST_DELAY_FIRST_CAPS_MS", "50", 1);
// A FORK CHILD INHERITS WHATEVER THE PREVIOUS CASE ADOPTED, and a mirror that is
// already valid would make this case a statement about that snapshot rather than about
// the placeholder window. Back to generation 0 first, deliberately and by name.
ResetCapsMirrorForTest();
MG_Config::Transport = MG_Config::TransportMode::InProcess;
MG_Config::SplitTransportRequestedByConfig = true;
MG_Config::ActiveBackendType = BackendType::DirectVulkan;
MG_Config::Features.PipePush = kMGPipeSubsystemsMigratedAtP5e;
MG_Pipe::MGPipeSetResourceOps(nullptr);
std::atomic<bool> answered{false};
std::atomic<bool> consumes{false};
std::thread reader([&] {
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10);
while (!BringUpInFlight() && PublishedCapsGeneration() == 0 &&
std::chrono::steady_clock::now() < deadline) {
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
consumes.store(CapsMirrorInstance().ServerConsumes(kMGPipeSubsystemResources));
answered.store(true);
});
MG_Backend::Init();
reader.join();
EXPECT_TRUE(answered.load()) << "the reader never returned from ServerConsumes";
EXPECT_TRUE(consumes.load()) << "a reader that asked DURING the bring-up must get the server's real answer - "
"Magma publishes the resource family - rather than the placeholder's false";
EXPECT_GT(PublishedCapsGeneration(), 0u);
ClientSessionInstance().Stop();
MG_Remote::Server::ServerLoopInstance().Stop();
std::fflush(nullptr);
::_exit(::testing::Test::HasFailure() ? 1 : 0);
});
ASSERT_GE(result.Status, 0) << "fork/waitpid failed";
ASSERT_TRUE(WIFEXITED(result.Status)) << DescribeStatus(result) << "\n" << result.Log;
EXPECT_EQ(WEXITSTATUS(result.Status), 0) << result.Log;
#else
GTEST_SKIP() << "the production bring-up is process-global and needs fork to isolate";
#endif
}
TEST(CapsMirrorTest, ARealSnapshotThatWithholdsAFamilyStillAnswersFalse) {
// THE OTHER NEGATIVE CONTROL, and the one that keeps this package from being a mask
// defaulted to all-ones: a server that really does not consume a family is still answered
// "no", counted and named. Only the PLACEHOLDER is refused.
AdoptSnapshot(MakeSnapshot(kMGPipeSubsystemPrograms, 0));
ResetConsumerRefusalsForTest();
EXPECT_TRUE(CapsMirrorInstance().Valid());
EXPECT_FALSE(CapsMirrorInstance().ServerConsumes(kMGPipeSubsystemTextureResources));
EXPECT_EQ(ConsumerRefusals(), 1u);
EXPECT_EQ(LastRefusedSubsystem(), kMGPipeSubsystemTextureResources);
}
TEST(CapsMirrorTest, TheReadAccessorsStillAnswerFromThePlaceholder) {
// P5's ruling for the READ accessors is NOT narrowed by F1, and it must not be: LogBackendInfo
// reads GetRendererInfo() during MG_Backend::Init() and an abort there is a process that
// cannot start. Only ServerConsumes - a decision, not a read - changed.
MGPCaps empty{};
CapsMirror mirror;
EXPECT_FALSE(mirror.Valid());
EXPECT_EQ(mirror.Generation(), 0u);
// (F1 made ServerConsumes on a placeholder a refusal, so the two decision reads that used to
// sit here belong to the case above; the READ accessors below are what this case pins.)
// P7 wave 2 package C, OQ-10: the NEGATIVE half of the resident-sub-data pair. It used to
// be vacuous - nothing in the tree ever set kCapResidentSubData, so every reading of it was
// false - and MagmaTransportPublishesRealBufferConsumersWithoutRunAhead below is the
// positive half that makes this one mean "a mirror with no snapshot withholds the bit"
// rather than "the bit does not exist". Withholding is the SAFE direction: the client then
// keeps the ordered in-place host write (BufferObject::LandBytesIntoResidentStore) instead
// of emitting opcode 49 at a server that might have no arm for it - ID-39 in miniature.
EXPECT_FALSE(mirror.HasCap(kCapResidentSubData));
EXPECT_EQ(mirror.CallMask(), 0u);
EXPECT_EQ(mirror.Backend(), BackendType::Unknown);
(void)empty;
}
TEST(CapsMirrorTest, ObjectFamilyEmissionTracksIndependentConsumerCapsWithoutBufferOps) {
#if MGTEST_HAVE_FORK
// Deliberately synthetic masks with no buffer consumer: production Magma now
// publishes one, but object-family independence must survive a restricted peer.
// Isolate transport, push mask, caps generation and cached supply environment.
const ChildResult result = RunInChild([] {
MG_Config::Transport = MG_Config::TransportMode::InProcess;
MG_Config::Features.PipePush = kMGPipeSubsystemsMigratedAtP5e;
MG_Pipe::MGPipeSetResourceOps(nullptr);
const auto emits = [](Uint64 family) { return MGPipeP4aFamilyEmits(family, family); };
Snapshot snapshot = MakeSnapshot(kMGPipeSubsystemTextureResources, 0);
snapshot.Backend = BackendType::DirectVulkan;
AdoptSnapshot(snapshot);
const Uint64 firstGeneration = CapsMirrorInstance().Generation();
EXPECT_FALSE(CapsMirrorInstance().ServerConsumes(kMGPipeSubsystemResources));
EXPECT_TRUE(emits(kMGPipeSubsystemTextureResources));
EXPECT_FALSE(emits(kMGPipeSubsystemFramebuffer));
// The former shared buffer-consumer gate cannot distinguish these two
// snapshots: both withhold bit 7. The production per-family gate must.
snapshot.Caps.CallMask = MGCapsConsumerBits(kMGPipeSubsystemFramebuffer);
AdoptSnapshot(snapshot);
EXPECT_GT(CapsMirrorInstance().Generation(), firstGeneration);
EXPECT_FALSE(CapsMirrorInstance().ServerConsumes(kMGPipeSubsystemResources));
EXPECT_FALSE(emits(kMGPipeSubsystemTextureResources));
EXPECT_TRUE(emits(kMGPipeSubsystemFramebuffer));
snapshot.Caps.CallMask = MGCapsConsumerBits(kMGPipeSubsystemTextureResources);
AdoptSnapshot(snapshot);
EXPECT_TRUE(emits(kMGPipeSubsystemTextureResources));
EXPECT_FALSE(emits(kMGPipeSubsystemFramebuffer));
// Object accessors were retired by P5f rather than made record-supplied;
// this checks the actual per-family emitter gate, not a synthetic field mask.
std::fflush(nullptr);
::_exit(::testing::Test::HasFailure() ? 1 : 0);
});
ASSERT_GE(result.Status, 0) << "fork/waitpid failed";
ASSERT_TRUE(WIFEXITED(result.Status)) << DescribeStatus(result) << result.Log;
EXPECT_EQ(WEXITSTATUS(result.Status), 0) << result.Log;
#else
GTEST_SKIP() << "process-global consumer isolation requires fork";
#endif
}
// Historical name retained; the real bootstrap must now publish and arm Magma readiness.
TEST(CapsMirrorTest, MagmaTransportPublishesRealBufferConsumersWithoutRunAhead) {
#if MGTEST_HAVE_FORK
const ChildResult result = RunInChild([] {
::alarm(15);
MG_Config::Transport = MG_Config::TransportMode::InProcess;
MG_Config::ActiveBackendType = BackendType::DirectVulkan;
MG_Config::Features.PipePush = kMGPipeSubsystemsMigratedAtP5e;
MG_Config::Ipc.RunAhead = 1;
MG_Pipe::MGPipeSetResourceOps(nullptr);
// The production bootstrap creates the server backend and publishes its
// real mask before any native EGL/Vulkan context exists. No invented caps
// snapshot or fake resource table can make this test pass.
MG_Backend::Init();
auto& server = MG_Remote::Server::ServerSessionInstance();
EXPECT_TRUE(server.Accepted());
EXPECT_TRUE(server.CallMaskIsSet());
if (server.CallMaskIsSet()) {
const Uint64 mask = server.CallMask();
EXPECT_TRUE(MGCapsServerConsumes(mask, kMGPipeSubsystemResources));
EXPECT_TRUE(MGCapsServerConsumes(mask, kMGPipeSubsystemBufferBindings));
EXPECT_EQ(mask & static_cast<Uint64>(kCapRunAheadApply), static_cast<Uint64>(kCapRunAheadApply));
}
const auto* ops = MG_Pipe::MGPipeGetResourceOps();
EXPECT_NE(ops, nullptr);
if (ops) {
EXPECT_NE(ops->Create, nullptr);
EXPECT_NE(ops->Respecify, nullptr);
EXPECT_NE(ops->SubData, nullptr);
EXPECT_NE(ops->FlushRange, nullptr);
EXPECT_NE(ops->Readback, nullptr);
EXPECT_NE(ops->Destroy, nullptr);
EXPECT_NE(ops->MapPersistent, nullptr);
if (ops->MapPersistent)
EXPECT_EQ(ops->MapPersistent({7, 1}, 64, nullptr), nullptr)
<< "Magma must not donate a server address as a client persistent map";
// P7 wave 2 package C, OQ-10 (CONTRACT-P7 §5.4): THE TABLE DECIDES THE BIT, and
// this is the assertion that says so rather than asserting a constant. Magma
// registers a SubDataResident arm (VkBufferManager.cpp's g_vulkanWireResourceOps),
// so the published mask must carry kCapResidentSubData - and it must carry it
// BECAUSE of the arm, which is why the two sides are compared to each other
// instead of both to `true`. Written this way the case also covers the backend
// that does NOT have the arm, on the day one exists: no enum is consulted here,
// and deleting the `|=` in MG_Backend/Init.cpp reds exactly this line (red-once).
EXPECT_EQ((server.CallMask() & static_cast<Uint64>(kCapResidentSubData)) != 0,
ops->SubDataResident != nullptr)
<< "kCapResidentSubData must be published from the server's own wire resource "
"table (ID-39: never from the backend enum). Without it "
"MGPipeResourceOpsHaveSubDataResident answers false under every transport "
"and both backends fall back to the in-place memcpy, which makes Magma's "
"SubDataResident arm dead code on the wire";
EXPECT_NE(ops->SubDataResident, nullptr)
<< "Magma's wire resource table lost its resident sub-data arm; the bit above "
"would then correctly go dark and opcode 49 would stop crossing";
}
EXPECT_TRUE(ClientSessionInstance().RunAheadArmed());
ClientSessionInstance().Stop();
MG_Remote::Server::ServerLoopInstance().Stop();
std::fflush(nullptr);
::_exit(::testing::Test::HasFailure() ? 1 : 0);
});
ASSERT_GE(result.Status, 0) << "fork/waitpid failed";
ASSERT_TRUE(WIFEXITED(result.Status)) << DescribeStatus(result) << result.Log;
EXPECT_EQ(WEXITSTATUS(result.Status), 0) << result.Log;
#else
GTEST_SKIP() << "process-global backend bootstrap isolation requires fork";
#endif
}
TEST(CapsMirrorTest, TheConsumerBlockDoesNotCollideWithTheFeatureBits) {
// The two halves of CallMask, asserted against each other rather than against a constant:
// bits 0..8 are MGPCapBit and bits 32..47 are the consumer mask, and the whole reason R-8
// became implementable is that they do not overlap.
AdoptSnapshot(
MakeSnapshot(kMGPipeSubsystemResources | kMGPipeSubsystemPrograms, kCapTimerQuery | kCapOcclusionQuery));
EXPECT_TRUE(CapsMirrorInstance().HasCap(kCapTimerQuery));
EXPECT_TRUE(CapsMirrorInstance().HasCap(kCapOcclusionQuery));
EXPECT_FALSE(CapsMirrorInstance().HasCap(kCapXfbPrimitivesQuery));
EXPECT_TRUE(CapsMirrorInstance().ServerConsumes(kMGPipeSubsystemResources));
EXPECT_TRUE(CapsMirrorInstance().ServerConsumes(kMGPipeSubsystemPrograms));
EXPECT_FALSE(CapsMirrorInstance().ServerConsumes(kMGPipeSubsystemSamplers));
}
// =====================================================================================
// P5b d1: the draw family's record plan (docs/archive/contracts/CONTRACT-P5B.md §2 d1). These drive the
// PRODUCTION derivation the nineteen emitters call - PlanDrawInfo / PlanDrawRange /
// PlanDrawIndirect over a RemoteDrawBindings snapshot - so the fields on the wire are pinned
// here without a GL context (R-16: the production predicate, not a copy). The bindings' READ
// from the real context is the integration lane's (IndexedDrawFamilyScenario under
// DirectGLES.Split.), and the sink's consumption of the same fields is ServerLoopTest's.
// =====================================================================================
namespace {
MGPipeHandle D1Handle(Uint32 slot) {
MGPipeHandle h{};
h.Slot = slot;
h.Gen = 3;
return h;
}
RemoteDrawBindings D1BoundElementBuffer(Uint32 slot) {
RemoteDrawBindings b{};
b.ElementBufferBound = true;
b.ElementBuffer = D1Handle(slot);
return b;
}
} // namespace
// Red once by: returning `offset` instead of `offset / indexSize` from PlanDrawRange's
// element-buffer arm - Start reads 24 below.
TEST(RemoteDrawPlan, AnElementBufferDrawElementsCarriesTheHandleAndItsOffsetInIndices) {
const RemoteDrawBindings b = D1BoundElementBuffer(17);
const MGPDrawInfo info = PlanDrawInfo(0x0004 /*GL_TRIANGLES*/, RemoteIndexSizeFor(0x1403 /*USHORT*/),
/*instanceCount=*/1, /*baseInstance=*/0, /*numDraws=*/1, b);
EXPECT_EQ(info.Mode, 4u);
EXPECT_EQ(info.IndexSize, 2u);
EXPECT_EQ(info.IndexResource.Slot, 17u) << "IndexResource must be the VAO's element buffer";
EXPECT_EQ(info.InstanceCount, 1u);
EXPECT_EQ(info.StartInstance, 0u);
EXPECT_EQ(info.Flags, 0u) << "a plain DrawElements sets no flag";
EXPECT_EQ(info.MinIndex, ~0u);
EXPECT_EQ(info.NumDraws, 1u);
MGPDrawRange range{};
ASSERT_TRUE(PlanDrawRange(b, 2, reinterpret_cast<const void*>(24), 36, 0, range));
EXPECT_EQ(range.Start, 12u) << "Start is the byte offset in INDICES (24 / 2)";
EXPECT_EQ(range.Count, 36u);
EXPECT_EQ(range.IndexBias, 0);
}
// Red once by: dropping `out.IndexBias = baseVertex` - IndexBias reads 0 below. And the
// instanced head: swap InstanceCount and StartInstance in PlanDrawInfo - 7 and 2 trade places.
TEST(RemoteDrawPlan, TheInstancedBaseVertexBaseInstanceFormCarriesAllThreeNumbers) {
const RemoteDrawBindings b = D1BoundElementBuffer(9);
const MGPDrawInfo info = PlanDrawInfo(0x0004, 4, /*instanceCount=*/7, /*baseInstance=*/2, 1, b);
EXPECT_EQ(info.InstanceCount, 7u);
EXPECT_EQ(info.StartInstance, 2u);
MGPDrawRange range{};
ASSERT_TRUE(PlanDrawRange(b, 4, reinterpret_cast<const void*>(0), 6, /*baseVertex=*/5, range));
EXPECT_EQ(range.Start, 0u);
EXPECT_EQ(range.IndexBias, 5);
// An instanced call with a count of 0 crosses as 0, never as the plain draw's 1: the sink
// reads InstanceCount != 1 as instanced, and 0 instances must draw nothing.
EXPECT_EQ(PlanDrawInfo(0x0004, 4, 0, 0, 1, b).InstanceCount, 0u);
}
// Red once by: making PlanDrawRange's element-buffer arm `return true` on a remainder - the
// misaligned offset below plans as index 3 instead of being refused.
TEST(RemoteDrawPlan, AMisalignedElementOffsetIsRefusedNotRounded) {
const RemoteDrawBindings b = D1BoundElementBuffer(1);
MGPDrawRange range{};
EXPECT_FALSE(PlanDrawRange(b, 4, reinterpret_cast<const void*>(13), 3, 0, range))
<< "13 bytes is not a whole number of 4-byte indices; the emitter refuses "
"\"<slot>+INDEX_OFFSET\" rather than draw from index 3";
EXPECT_TRUE(PlanDrawRange(b, 4, reinterpret_cast<const void*>(12), 3, 0, range));
EXPECT_EQ(range.Start, 3u);
}
// Red once by: setting `out.Start = offset` in the no-element-buffer arm - Start reads the
// pointer's low bits instead of 0 (the staged run starts at its first index).
TEST(RemoteDrawPlan, AClientIndexArrayPlansFromIndexZeroWithNoHandle) {
RemoteDrawBindings b{}; // nothing bound: `indices` is the application's array
const std::uint16_t clientIndices[3] = {0, 1, 2};
const MGPDrawInfo info = PlanDrawInfo(0x0004, 2, 1, 0, 1, b);
EXPECT_TRUE(MGPipeHandleIsNull(info.IndexResource)) << "no element buffer, no handle";
MGPDrawRange range{};
ASSERT_TRUE(PlanDrawRange(b, 2, clientIndices, 3, 0, range));
EXPECT_EQ(range.Start, 0u);
EXPECT_EQ(range.Count, 3u);
// The span itself is added by the emission (kDrawHasUserIndices, count * IndexSize bytes
// staged); ServerLoopTest's AClientIndexArrayCrossesAsAStagedSpan pins the other side.
}
// Red once by: returning `first * sizeof(float)` or any scaled first for arrays - Start reads
// something other than 56064 below. (Arrays spell `first` through the pointer parameter.)
TEST(RemoteDrawPlan, AnArraysRangeIsFirstAndCountWithNoBias) {
RemoteDrawBindings b{};
MGPDrawRange range{};
ASSERT_TRUE(PlanDrawRange(b, 0, reinterpret_cast<const void*>(static_cast<std::intptr_t>(56064)), 16128, 0, range));
EXPECT_EQ(range.Start, 56064u);
EXPECT_EQ(range.Count, 16128u);
EXPECT_EQ(range.IndexBias, 0);
EXPECT_EQ(PlanDrawInfo(0x0004, 0, 1, 0, 1, b).IndexSize, 0u);
}
// Red once by: dropping `block.ParameterBuffer = ...` for the counted form - the parameter
// handle reads null below and the sink would dispatch the uncounted call.
TEST(RemoteDrawPlan, TheIndirectBlockNamesBothBuffersAndTheCallsOffsets) {
RemoteDrawBindings b{};
b.DrawIndirectBuffer = D1Handle(40);
b.ParameterBuffer = D1Handle(41);
const MGPDrawIndirect counted = PlanDrawIndirect(b, reinterpret_cast<const void*>(64),
/*drawCount=*/3, /*stride=*/20,
/*parameterOffset=*/8, /*hasParameterBuffer=*/true);
EXPECT_EQ(counted.Buffer.Slot, 40u);
EXPECT_EQ(counted.ParameterBuffer.Slot, 41u);
EXPECT_EQ(counted.Offset, 64u);
EXPECT_EQ(counted.ParameterOffset, 8u);
EXPECT_EQ(counted.Stride, 20u);
EXPECT_EQ(counted.DrawCount, 3u);
const MGPDrawIndirect plain = PlanDrawIndirect(b, reinterpret_cast<const void*>(64), 1, 0, 8, false);
EXPECT_TRUE(MGPipeHandleIsNull(plain.ParameterBuffer))
<< "an uncounted indirect draw names no parameter buffer even when one is bound";
EXPECT_EQ(plain.ParameterOffset, 0u);
EXPECT_EQ(plain.DrawCount, 1u);
// The head of an indirect record declares no ranges: the server never reads the indirect
// buffer to learn a count (MGPipeTypes.h kDrawIsIndirect).
EXPECT_EQ(PlanDrawInfo(0x0004, 4, 1, 0, 0, b).NumDraws, 0u);
}
// Red once by: returning 4 for GL_UNSIGNED_SHORT - the second line below reads 4.
TEST(RemoteDrawPlan, TheThreeIndexTypesSizeAndNothingElseDoes) {
EXPECT_EQ(RemoteIndexSizeFor(0x1401 /*GL_UNSIGNED_BYTE*/), 1u);
EXPECT_EQ(RemoteIndexSizeFor(0x1403 /*GL_UNSIGNED_SHORT*/), 2u);
EXPECT_EQ(RemoteIndexSizeFor(0x1405 /*GL_UNSIGNED_INT*/), 4u);
EXPECT_EQ(RemoteIndexSizeFor(0x1406 /*GL_FLOAT*/), 0u) << "not an index type: the emitter refuses "
"\"<slot>+INDEX_TYPE\"";
}
TEST(RemoteEmitTable, TheNineteenDrawSlotsAreEmittersDistinctFromEveryStub) {
// The table half of d1's flip: each of the nineteen draw pointers is non-null and is NOT the
// class-C thunk that stood there at the contract commit. Read from the struct (R-16).
// Red once by: leaving one `table.GL.X = &EmitX;` out of BuildRemoteEmitTable - that slot
// would then be the MGR_UNMIGRATED thunk, which the macro no longer defines, so the build
// breaks first; and by re-adding an X row to MGR_UNMIGRATED_D1_SLOTS, which breaks the
// ownership static_assert. Both are build breaks, which is the point of the arithmetic.
const MG_Backend::GlobalBackendFunctionsTable& t = RemoteEmitTable();
const void* stub = reinterpret_cast<const void*>(t.GL.GetTexImage); // a class-C thunk
const void* draws[19] = {
reinterpret_cast<const void*>(t.GL.DrawElements),
reinterpret_cast<const void*>(t.GL.DrawElementsBaseVertex),
reinterpret_cast<const void*>(t.GL.DrawRangeElements),
reinterpret_cast<const void*>(t.GL.DrawRangeElementsBaseVertex),
reinterpret_cast<const void*>(t.GL.DrawElementsInstanced),
reinterpret_cast<const void*>(t.GL.DrawElementsInstancedBaseVertex),
reinterpret_cast<const void*>(t.GL.DrawElementsInstancedBaseInstance),
reinterpret_cast<const void*>(t.GL.DrawElementsInstancedBaseVertexBaseInstance),
reinterpret_cast<const void*>(t.GL.DrawArraysInstanced),
reinterpret_cast<const void*>(t.GL.DrawArraysInstancedBaseInstance),
reinterpret_cast<const void*>(t.GL.MultiDrawArrays),
reinterpret_cast<const void*>(t.GL.MultiDrawElements),
reinterpret_cast<const void*>(t.GL.MultiDrawElementsBaseVertex),
reinterpret_cast<const void*>(t.GL.DrawArraysIndirect),
reinterpret_cast<const void*>(t.GL.DrawElementsIndirect),
reinterpret_cast<const void*>(t.GL.MultiDrawArraysIndirect),
reinterpret_cast<const void*>(t.GL.MultiDrawElementsIndirect),
reinterpret_cast<const void*>(t.GL.MultiDrawArraysIndirectCount),
reinterpret_cast<const void*>(t.GL.MultiDrawElementsIndirectCount),
};
for (int i = 0; i < 19; ++i) {
EXPECT_NE(draws[i], nullptr) << "draw slot " << i;
EXPECT_NE(draws[i], stub) << "draw slot " << i << " is a class-C thunk";
}
}
// =====================================================================================
// ID-47: a readback larger than a reply slot is refused at the CLIENT, by name
// =====================================================================================
TEST(RemoteReadback, ExactlyTheCapacityPassesAndOneByteMoreIsRefusedByName) {
// ID-47's boundary pair, and it is THE CALL SITE'S half. The refusal itself is s1's
// (ReplySlotPool::RequireReadPixelsFits, its own cases in s1-v3.md §1); what c1 owns is
// that the number handed to it is the one the emitter computes - ID-49's TIGHT extent -
// and that exactly the cap is legal while one byte more is not. So this drives the real
// pool with the real helper, over the real arithmetic, and never restates the message.
//
// A real pool over a real mapping, because CanHold answers false for a null base and a
// control built on a default-constructed pool would "refuse" everything for that reason.
constexpr std::uint32_t kSlots = 8;
constexpr std::uint64_t kSlotBytes = 2u * 1024u * 1024u;
std::vector<Uint8> backing(static_cast<size_t>(kSlots) * kSlotBytes);
Transport::ReplySlotPool pool(backing.data(), backing.size(), kSlots);
const std::uint64_t cap = pool.MaxReplyBytes();
ASSERT_GT(cap, 0u) << "the fixture's pool is not configured, so every answer would be refused";
// ID-47's own number: the E2 retrace snapshot reads 640x480 RGBA8 and it must now FIT.
EXPECT_TRUE(pool.CanHold(TightReadbackByteCount(640, 480, 0x1908, 0x1401)))
<< "the read ID-47 grew SEG_REPLY for still does not fit";
pool.RequireReadPixelsFits(724, 724, 0x1908, 0x1401, cap);
SUCCEED() << "exactly the capacity is not an overflow";
#if MGTEST_HAVE_FORK
const ChildResult child = RunInChild([&] {
Transport::ReplySlotPool inner(backing.data(), backing.size(), kSlots);
inner.RequireReadPixelsFits(640, 480, 0x1908, 0x1401, inner.MaxReplyBytes() + 1);
});
EXPECT_TRUE(DiedOfAbort(child)) << "one byte over the slot did not abort: " << DescribeStatus(child);
// ITS OWN FAILURE STRING, AND THE READ'S OWN NUMBERS. A control that only asserted
// "something died" would pass on Fatal{NoClientSession}, Fatal{UnmigratedVerb} or a
// segfault, and this file has four other cases that abort for those reasons.
EXPECT_NE(child.Log.find("Fatal{ReplyTooLarge"), std::string::npos) << child.Log;
EXPECT_NE(child.Log.find("ReadPixels 640x480"), std::string::npos)
<< "the message does not name the read, so an operator cannot tell which one: " << child.Log;
EXPECT_NE(child.Log.find(std::to_string(cap + 1)), std::string::npos)
<< "the message does not carry the byte count";
#else
GTEST_SKIP() << "the refusal reports through a Fatal + abort and needs fork() to read back";
#endif
}
// =====================================================================================
// ID-49: the pack state never crosses; the client scatters the tight rows
// =====================================================================================
TEST(RemoteReadback, DstSizeIsTheTightExtentAndNeverThePackedOne) {
// The number v1 allocates on the server. It must not move with the pack state, because the
// server reads with a NEUTRAL one and cannot see the client's: the first version of this
// emitter declared GL 8.4.4's PACKED size, v1 allocated exactly that, and the driver then
// wrote past it - two SEGFAULTs on the joint inproc lane, both backends.
constexpr GLenum kRgba = 0x1908;
constexpr GLenum kUByte = 0x1401;
EXPECT_EQ(TightReadbackByteCount(4, 3, kRgba, kUByte), 4u * 3u * 4u);
EXPECT_EQ(TightReadbackByteCount(640, 480, kRgba, kUByte), 640u * 480u * 4u);
// ID-47's own arithmetic depends on this: 640x480 RGBA8 is what the E2 retrace snapshot
// reads, and 1,228,800 is the number that forced SEG_REPLY to grow.
EXPECT_EQ(TightReadbackByteCount(640, 480, kRgba, kUByte), 1228800u);
}
TEST(RemoteReadback, TheTightRowsAreScatteredWhereThePackStateSaysAndTheGapsAreLeftAlone) {
// ID-49's control, and it is the exact case the cross-family review found: 4x3 RGBA8 with
// PACK_ROW_LENGTH=8, SKIP_ROWS=1, SKIP_PIXELS=2. Stride = 8*4 = 32; the first written byte
// is 1*32 + 2*4 = 40; each row writes 4*4 = 16 bytes and the remaining 16 of its stride
// belong to the application.
constexpr Uint64 kBpp = 4;
constexpr GLsizei kW = 4;
constexpr GLsizei kH = 3;
constexpr Uint8 kSentinel = 0xCD;
PixelStoreParameters pack{};
pack.RowLength = 8;
pack.SkipRows = 1;
pack.SkipPixels = 2;
pack.Alignment = 4;
// THE INPUT, not the state under test: every source byte is distinct, so a scatter that
// wrote the right COUNT of bytes from the wrong offset cannot look correct.
std::vector<Uint8> tight(static_cast<size_t>(kW) * kH * kBpp);
for (size_t i = 0; i < tight.size(); ++i)
tight[i] = static_cast<Uint8>(i);
std::vector<Uint8> destination(512, kSentinel);
ScatterTightReadbackIntoPackState(tight.data(), destination.data(), kW, kH, kBpp, pack);
const size_t stride = 8 * 4;
const size_t first = 1 * stride + 2 * 4;
for (size_t row = 0; row < static_cast<size_t>(kH); ++row) {
for (size_t byte = 0; byte < static_cast<size_t>(kW) * kBpp; ++byte) {
EXPECT_EQ(destination[first + row * stride + byte], tight[row * static_cast<size_t>(kW) * kBpp + byte])
<< "row " << row << " byte " << byte << " landed somewhere else";
}
}
// THE GAPS, which is the half that makes this a control rather than a copy of the loop
// above: the bytes the pack state does not name belong to the application and must still
// hold their sentinel. Removing the skips from the scatter passes the loop above and fails
// here; widening the per-row copy to the stride passes both loops above and fails here.
size_t touched = 0;
for (size_t i = 0; i < destination.size(); ++i) {
const bool inWrittenRow = i >= first && ((i - first) % stride) < static_cast<size_t>(kW) * kBpp &&
((i - first) / stride) < static_cast<size_t>(kH);
if (!inWrittenRow) {
EXPECT_EQ(destination[i], kSentinel)
<< "byte " << i << " is outside the rectangle GL names and was overwritten";
} else {
++touched;
}
}
EXPECT_EQ(touched, tight.size()) << "the scatter wrote a different number of bytes than the "
"reply carried";
}
TEST(RemoteReadback, PackSkipImagesIsIgnoredForATwoDimensionalRead) {
// codex 6: SKIP_IMAGES (and IMAGE_HEIGHT) are image-level pack parameters and GL ignores
// them for glReadPixels, a 2-D read - the monolith conversion path says so with
// honorPackImageParams=false (ReadPixels, DirectGLESReadback.cpp). The first cut applied SKIP_IMAGES as a
// whole-image offset: a 4x3 RGBA8 read with SKIP_IMAGES=1 wrote bytes 48..95 of a 96-byte
// destination sized for one image, overrunning it. With the fix the reply lands at bytes
// 0..47 and the second image's worth of bytes keeps its sentinel.
constexpr Uint64 kBpp = 4;
constexpr GLsizei kW = 4;
constexpr GLsizei kH = 3;
constexpr Uint8 kSentinel = 0xEE;
PixelStoreParameters pack{};
pack.SkipImages = 1; // the parameter under test
pack.ImageHeight = kH; // and its companion; both must be ignored
std::vector<Uint8> tight(static_cast<size_t>(kW) * kH * kBpp);
for (size_t i = 0; i < tight.size(); ++i)
tight[i] = static_cast<Uint8>(i + 1);
std::vector<Uint8> destination(96, kSentinel);
ScatterTightReadbackIntoPackState(tight.data(), destination.data(), kW, kH, kBpp, pack);
for (size_t i = 0; i < tight.size(); ++i)
EXPECT_EQ(destination[i], tight[i]) << "byte " << i << " should hold the reply at offset 0";
for (size_t i = tight.size(); i < destination.size(); ++i)
EXPECT_EQ(destination[i], kSentinel) << "byte " << i
<< " is past the read's own extent and SKIP_IMAGES must not have moved "
"the write there";
// And the fast path takes it: an otherwise-neutral read with only SKIP_IMAGES set is tight.
EXPECT_TRUE(ReadbackPackStateIsTightForTest(kW, kBpp, pack))
<< "SKIP_IMAGES alone must not force the bounce path for a 2-D read";
}
TEST(RemoteReadback, TheFastPathIsTakenExactlyWhenTheScatterWouldChangeNothing) {
// EmitReadPixels reads the reply STRAIGHT into the application's pointer when
// ReadbackPackStateIsTight says so, and pays for a bounce buffer otherwise. That is only
// legal if the predicate and the scatter AGREE - so this case drives BOTH and compares
// them, rather than testing either alone. A predicate that said "tight" for a layout the
// scatter would have rearranged is a silently wrong picture with no bounce to blame.
constexpr Uint64 kBpp = 4;
constexpr GLsizei kW = 5;
constexpr GLsizei kH = 3;
std::vector<Uint8> tight(static_cast<size_t>(kW) * kH * kBpp);
for (size_t i = 0; i < tight.size(); ++i)
tight[i] = static_cast<Uint8>(i * 7 + 1);
// Six layouts, chosen so both answers appear: a bare default, an explicit equal row
// length, an alignment the row already satisfies, an alignment it does not, a skip, and a
// wider row. If every case agreed on "tight" the comparison below would be vacuous, so the
// count of each answer is asserted too.
std::vector<PixelStoreParameters> layouts(6);
layouts[1].RowLength = kW;
layouts[2].Alignment = 4; // 5*4 = 20, already a multiple of 4
layouts[3].Alignment = 8; // 20 is not a multiple of 8 - the rows gain padding
layouts[4].SkipPixels = 1;
layouts[5].RowLength = 8;
int tightCount = 0;
for (size_t i = 0; i < layouts.size(); ++i) {
const PixelStoreParameters& pack = layouts[i];
std::vector<Uint8> destination(4096, 0);
ScatterTightReadbackIntoPackState(tight.data(), destination.data(), kW, kH, kBpp, pack);
destination.resize(tight.size());
const bool scatterChangedNothing = (destination == tight);
const bool predicateSaysTight = ReadbackPackStateIsTightForTest(kW, kBpp, pack) != 0;
EXPECT_EQ(predicateSaysTight, scatterChangedNothing)
<< "layout " << i << ": the fast-path predicate and the scatter disagree";
if (predicateSaysTight) ++tightCount;
}
EXPECT_GT(tightCount, 0) << "no layout took the fast path, so the equality above is vacuous";
EXPECT_LT(tightCount, static_cast<int>(layouts.size()))
<< "every layout took the fast path, so the equality above is vacuous";
}
// =====================================================================================
// P7 gate 5 (g5-readback): a read larger than one reply slot is BANDED, not refused
// =====================================================================================
namespace {
// The bands the production walk visits, in order - the emitter iterates the same walk.
std::vector<ReadbackBand> CollectReadbackBands(Uint64 width, Uint64 height, Uint64 bpp, Uint64 cap,
Bool* planned = nullptr) {
ReadbackBandPlan plan;
const Bool ok = PlanReadbackBands(width, height, bpp, cap, plan);
if (planned != nullptr) *planned = ok;
std::vector<ReadbackBand> bands;
if (ok) ForEachReadbackBand(width, height, plan, [&](const ReadbackBand& band) { bands.push_back(band); });
return bands;
}
} // namespace
TEST(RemoteReadback, TheCtsReadOfExactlyTwoMiBIsTwoBandsThatEachFitASlot) {
// KHR-GL46.direct_state_access.renderbuffers_storage's own read: 256x512 RGBA/FLOAT is
// 2,097,152 bytes against ID-47's 2,097,136. It used to be Fatal{ReplyTooLarge}; it is now two
// records - 511 rows (2,093,056 bytes) and the single top row - and BOTH are answers one slot
// holds, and each record's DstSize is its own tight extent, so the server's PH-3 bound (its
// tight answer against its own maxReplyBytes) is met per record.
constexpr Uint64 kCap = 2u * 1024u * 1024u - 16u;
const Uint64 bpp = 16;
ASSERT_EQ(TightReadbackByteCount(256, 512, 0x1908 /*RGBA*/, 0x1406 /*FLOAT*/), 2097152u);
const auto bands = CollectReadbackBands(256, 512, bpp, kCap);
ASSERT_EQ(bands.size(), 2u);
EXPECT_EQ(bands[0].FirstRow, 0u);
EXPECT_EQ(bands[0].Rows, 511u);
EXPECT_EQ(bands[1].FirstRow, 511u);
EXPECT_EQ(bands[1].Rows, 1u);
for (const ReadbackBand& band : bands) {
EXPECT_EQ(band.FirstColumn, 0u);
EXPECT_EQ(band.Columns, 256u);
EXPECT_LE(
TightReadbackByteCount(static_cast<GLsizei>(band.Columns), static_cast<GLsizei>(band.Rows), 0x1908, 0x1406),
kCap);
}
// A read that already fits is ONE record, unchanged from before: exactly the cap, and E2's
// 640x480 RGBA8 snapshot.
EXPECT_EQ(CollectReadbackBands(131071, 1, 16, kCap).size(), 1u) << "exactly the cap is one band";
EXPECT_EQ(CollectReadbackBands(640, 480, 4, kCap).size(), 1u);
// More than the whole 16 MiB SEG_REPLY: 2048x2100 RGBA8 is 255-row bands, nine of them.
EXPECT_EQ(CollectReadbackBands(2048, 2100, 4, kCap).size(), 9u);
}
TEST(RemoteReadback, EveryPlanCoversTheReadOnceInRowMajorOrderWithBandsThatFitAndStayContiguous) {
// The plan's invariants over a sweep that includes both shapes - whole-width row bands and,
// for a row wider than a reply, single-row column pieces - at small caps so every boundary is
// hit: the cap exactly one row, one byte short of a row, one pixel, one pixel short of two.
struct Shape {
Uint64 w, h, bpp, cap;
};
const Shape shapes[] = {
{7, 5, 4, 28}, {7, 5, 4, 27}, {7, 5, 4, 4}, {7, 5, 4, 7},
{7, 5, 16, 1000}, {20, 3, 16, 100}, {1, 9, 3, 3}, {9, 1, 3, 26},
{256, 512, 16, 2097136}, {5, 5, 8, 8}, {33, 17, 4, 131}, {1024, 513, 4, 2097136},
};
int rowShapes = 0, pieceShapes = 0;
for (const Shape& s : shapes) {
SCOPED_TRACE(std::to_string(s.w) + "x" + std::to_string(s.h) + " bpp " + std::to_string(s.bpp) + " cap " +
std::to_string(s.cap));
Bool planned = false;
const auto bands = CollectReadbackBands(s.w, s.h, s.bpp, s.cap, &planned);
ASSERT_TRUE(planned);
std::vector<int> covered(static_cast<size_t>(s.w * s.h), 0);
Uint64 previousTightEnd = 0;
Bool pieces = false;
for (const ReadbackBand& band : bands) {
ASSERT_GT(band.Rows, 0u);
ASSERT_GT(band.Columns, 0u);
EXPECT_LE(band.Rows * band.Columns * s.bpp, s.cap) << "a band's answer does not fit a reply";
// CONTIGUOUS IN THE TIGHT LAYOUT (whole-width rows, or a piece of one row), and in
// order: the direct path reads each answer straight into the application's buffer at
// exactly this offset, so a gap or an overlap here is a torn picture there.
EXPECT_TRUE(band.Columns == s.w || band.Rows == 1);
if (band.Columns != s.w) pieces = true;
const Uint64 tightStart = band.FirstRow * s.w + band.FirstColumn;
EXPECT_EQ(tightStart, previousTightEnd) << "bands are not consecutive in row-major order";
previousTightEnd = tightStart + (band.Rows - 1) * s.w + band.Columns;
for (Uint64 r = 0; r < band.Rows; ++r)
for (Uint64 c = 0; c < band.Columns; ++c)
++covered[static_cast<size_t>((band.FirstRow + r) * s.w + band.FirstColumn + c)];
}
EXPECT_EQ(previousTightEnd, s.w * s.h);
for (size_t i = 0; i < covered.size(); ++i)
ASSERT_EQ(covered[i], 1) << "pixel " << i;
// AS FEW RECORDS AS THE SHAPE ALLOWS: a whole-width band takes every row a reply holds.
if (!pieces) {
const Uint64 rowsPerReply = s.cap / (s.w * s.bpp);
EXPECT_EQ(bands.size(), (s.h + rowsPerReply - 1) / rowsPerReply);
++rowShapes;
} else {
EXPECT_GT(s.w * s.bpp, s.cap) << "a row that fits a reply was cut into pieces";
++pieceShapes;
}
}
EXPECT_GT(rowShapes, 0);
EXPECT_GT(pieceShapes, 0) << "the sweep never reached the column-piece arm";
}
TEST(RemoteReadback, NotEvenOnePixelFittingIsTheOnlyReadWithNoPlan) {
// The one read banding cannot answer, left to ID-47's named Fatal at the emitter: a single
// pixel larger than a reply (a server that declared a cap below 16 bytes), or no reply pool.
Bool planned = true;
EXPECT_TRUE(CollectReadbackBands(4, 4, 16, 15, &planned).empty());
EXPECT_FALSE(planned);
EXPECT_TRUE(CollectReadbackBands(4, 4, 4, 0, &planned).empty());
EXPECT_FALSE(planned);
// And one byte more is a plan, of single-pixel pieces.
const auto bands = CollectReadbackBands(4, 4, 16, 16, &planned);
EXPECT_TRUE(planned);
EXPECT_EQ(bands.size(), 16u);
// Degenerate reads plan nothing (the emitter returns before planning them).
EXPECT_TRUE(CollectReadbackBands(0, 4, 4, 64, &planned).empty());
EXPECT_FALSE(planned);
}
TEST(RemoteReadback, ABandedScatterWritesExactlyTheBytesTheWholeScatterWrites) {
// The bounce path's composition: every band scattered on its own must leave the destination
// byte-identical to one whole-read scatter, gaps included, for pack states that pad, skip,
// and - the ill-formed 0 < ROW_LENGTH < width case m6 keeps verbatim - overlap. Both band
// shapes are driven: row bands at a cap of two rows and a bit, pieces at a cap under a row.
constexpr GLsizei kW = 11;
constexpr GLsizei kH = 7;
constexpr Uint8 kSentinel = 0xEE;
for (const Uint64 bpp : {Uint64{3}, Uint64{4}, Uint64{16}}) {
std::vector<Uint8> tight(static_cast<size_t>(kW) * kH * bpp);
for (size_t i = 0; i < tight.size(); ++i)
tight[i] = static_cast<Uint8>(i * 13 + 5);
std::vector<PixelStoreParameters> layouts(6);
layouts[1].Alignment = 8;
layouts[2].RowLength = kW + 3;
layouts[2].Alignment = 8;
layouts[3].SkipRows = 2;
layouts[3].SkipPixels = 3;
layouts[4].RowLength = kW + 1;
layouts[4].SkipRows = 1;
layouts[4].SkipPixels = 1;
layouts[4].Alignment = 2;
layouts[5].RowLength = 4; // < width: consecutive rows overlap, so ORDER decides the bytes
for (const Uint64 cap : {kW * bpp * 2 + 5, kW * bpp - 1, bpp * 3}) {
for (size_t l = 0; l < layouts.size(); ++l) {
SCOPED_TRACE("bpp " + std::to_string(bpp) + " cap " + std::to_string(cap) + " layout " +
std::to_string(l));
const PixelStoreParameters& pack = layouts[l];
std::vector<Uint8> whole(4096, kSentinel);
ScatterTightReadbackIntoPackState(tight.data(), whole.data(), kW, kH, bpp, pack);
ReadbackBandPlan plan;
ASSERT_TRUE(PlanReadbackBands(kW, kH, bpp, cap, plan));
std::vector<Uint8> banded(4096, kSentinel);
int bandCount = 0;
ForEachReadbackBand(kW, kH, plan, [&](const ReadbackBand& band) {
// What the server would answer for this band: its own tight rectangle.
std::vector<Uint8> answer(static_cast<size_t>(band.Rows * band.Columns * bpp));
for (Uint64 r = 0; r < band.Rows; ++r) {
std::memcpy(answer.data() + r * band.Columns * bpp,
tight.data() + ((band.FirstRow + r) * kW + band.FirstColumn) * bpp,
static_cast<size_t>(band.Columns * bpp));
}
ScatterReadbackBandIntoPackState(answer.data(), banded.data(), kW, band, bpp, pack);
++bandCount;
});
EXPECT_GT(bandCount, 1) << "the cap did not band this read, so the comparison is vacuous";
EXPECT_EQ(banded, whole);
}
}
}
}
// =====================================================================================
// R-17: the routing, its reply mailbox, and the arm that is actually installed
// =====================================================================================
TEST(PipeRouting, TheTablesAreInstalledWithoutAnybodyHavingRememberedTo) {
// The gate on the install MECHANISM rather than on the table contents (PipeCatalogueTest
// owns the partition). Nothing in this file calls an installer; the tables are installed
// because MG_Pipe/PipeRoute.h's inline variable is in this binary, which is the property
// that a static initialiser inside PipeRoute.cpp did NOT have - the linker dropped that
// object from every test binary that named no symbol in it, and five CsoCacheTest cases
// took a null function pointer.
EXPECT_TRUE(MGPipeTablesAreInstalled());
EXPECT_EQ(static_cast<int>(MGPipeInstalledArm()), static_cast<int>(MGPipeRouteArm::kMonolith))
<< "this process has no ClientSession, so the monolith adapters must be the arm";
}
TEST(PipeRouting, AnAnswerIsWhatTheRowSaidAndDeclinedIsFalseRatherThanAFailure) {
const Uint64 takenBefore = MGPipeRepliesTaken();
const Uint64 declinedBefore = MGPipeRepliesDeclined();
const MGPReplySlot ok = MGPipeMintReplySlot();
MGPipePostReply(ok, 0 /*OK*/, 1);
EXPECT_TRUE(MGPipeTakeReplyBool(ok, "unit"));
const MGPReplySlot declined = MGPipeMintReplySlot();
MGPipePostReply(declined, 1 /*DECLINED*/, 0);
EXPECT_FALSE(MGPipeTakeReplyBool(declined, "unit"))
<< "DECLINED is how the four Bool acceptance rows say false (R-5), not how they fail";
EXPECT_EQ(MGPipeRepliesTaken(), takenBefore + 2u);
EXPECT_EQ(MGPipeRepliesDeclined(), declinedBefore + 1u)
<< "the refusal was not COUNTED, so 'the client accepted everything' and 'the client "
"never asked' are still the same observation from outside";
// The two slots are different ids, which is what makes the mismatch Fatal below meaningful.
EXPECT_NE(ok.Id, declined.Id);
EXPECT_NE(ok.Id, 0u) << "slot 0 is ReplySlot.h's 'no record' and must stay unmintable";
}
#if MGTEST_HAVE_FORK
TEST(PipeRouting, AnUnansweredRowIsFatalRatherThanAcceptedOrRefused) {
// R-5's whole point, made structural. There is no default: "always accept" is ID-39's 66
// lost DirectVulkan uploads with a wire in between, and "always refuse" is an emitter that
// re-sends for ever. A row that forgets to answer has to be impossible to READ.
const ChildResult child = RunInChild([] {
const MGPReplySlot slot = MGPipeMintReplySlot();
(void)MGPipeTakeReplyBool(slot, "resource_create");
});
EXPECT_TRUE(DiedOfAbort(child)) << DescribeStatus(child);
EXPECT_NE(child.Log.find("Fatal{ReplyMissing"), std::string::npos) << child.Log;
EXPECT_NE(child.Log.find("resource_create"), std::string::npos)
<< "the Fatal does not name the row, so it cannot say WHICH answer went missing";
}
TEST(PipeRouting, TwoOutstandingAnswersAreFatalBecauseTheBarrierIsOneDeep) {
// The mailbox is one entry deep because R-1's verb barrier makes the in-flight depth one
// (ReplySlot.h). A second posting before the first is taken is not a capacity problem, it
// is a barrier that has stopped holding - so it must not be absorbed by a deeper mailbox.
const ChildResult child = RunInChild([] {
const MGPReplySlot first = MGPipeMintReplySlot();
const MGPReplySlot second = MGPipeMintReplySlot();
MGPipePostReply(first, 0, 1);
MGPipePostReply(second, 0, 1);
});
EXPECT_TRUE(DiedOfAbort(child)) << DescribeStatus(child);
EXPECT_NE(child.Log.find("Fatal{ReplyOverrun"), std::string::npos) << child.Log;
}
TEST(PipeRouting, AnErrorStatusIsNotFoldedIntoAcceptedOrRefused) {
// ERROR is a transport fault and DECLINED is a resource decision. Folding the first into
// either arm of the second makes a broken wire look like a server that said no.
const ChildResult child = RunInChild([] {
const MGPReplySlot slot = MGPipeMintReplySlot();
MGPipePostReply(slot, 2 /*ERROR*/, 0);
(void)MGPipeTakeReplyBool(slot, "set_texture_params");
});
EXPECT_TRUE(DiedOfAbort(child)) << DescribeStatus(child);
EXPECT_NE(child.Log.find("Fatal{ReplyError"), std::string::npos) << child.Log;
EXPECT_NE(child.Log.find("set_texture_params"), std::string::npos) << child.Log;
}
#endif // MGTEST_HAVE_FORK
// =====================================================================================
// B3 / codex 9: the CLIENT arm's 38 rows are observed, not just the monolith install
// =====================================================================================
namespace {
// How many function-pointer cells of `a` differ from `b`, walked as a block of void* -
// the structs ARE their function pointers (PipeCatalogueTest static_asserts that shape). A
// routed row that stayed on the monolith adapter reads EQUAL and is not counted, which is
// exactly the defect this measures.
template <class T>
SizeT CountDifferingCells(const T& a, const T& b) {
const void* const* pa = reinterpret_cast<const void* const*>(&a);
const void* const* pb = reinterpret_cast<const void* const*>(&b);
SizeT n = 0;
for (SizeT i = 0; i < sizeof(T) / sizeof(void*); ++i)
if (pa[i] != pb[i]) ++n;
return n;
}
} // namespace
TEST(PipeRouting, TheInstalledClientArmIsWireAndNotMonolithAndEveryRoutedRowMoved) {
// WHAT B3 SAYS IS MISSING. PipeCatalogueTest installs and COUNTS the monolith table, so a
// client row that was never overwritten stays non-null and still counts - deleting one
// `gMGPipe*.X = &Wire_X` assignment left every gate green (the cross-family verifier
// reproduced it: catalogue 32/32, all lanes green). The only thing that catches it is
// observing that the CLIENT install actually MOVED each routed row off the monolith adapter.
//
// The monolith adapters are kept beside the installed tables (MGPipeMonolith*()), so the
// client install differs from them at exactly the routed rows and nowhere else. This reads
// the pointers back rather than constructing them (R-16): a deleted assignment reads equal.
InstallClientWireTables();
EXPECT_EQ(static_cast<int>(MGPipeInstalledArm()), static_cast<int>(MGPipeRouteArm::kClientWire))
<< "InstallClientWireTables did not record the client-wire arm";
const SizeT movedScreen = CountDifferingCells(gMGPipeScreen, MGPipeMonolithScreen());
#define C1F_MOVED(Table, Row) EXPECT_NE(gMGPipe##Table.Row, MGPipeMonolith##Table().Row) << #Table "." #Row
C1F_MOVED(Screen, ResourceCreate);
C1F_MOVED(Screen, ResourceDestroy);
C1F_MOVED(Screen, UnmapPersistent);
C1F_MOVED(Context, CreateRenderState);
C1F_MOVED(Context, BindRenderState);
C1F_MOVED(Context, DeleteRenderState);
C1F_MOVED(Context, CreateVertexElements);
C1F_MOVED(Context, BindVertexElements);
C1F_MOVED(Context, DeleteVertexElements);
C1F_MOVED(Context, CreateSamplerState);
C1F_MOVED(Context, DeleteSamplerState);
C1F_MOVED(Context, CreateSamplerView);
C1F_MOVED(Context, DeleteSamplerView);
C1F_MOVED(Context, BindShaderState);
C1F_MOVED(Context, DeleteShaderState);
C1F_MOVED(Context, SetDrawProgram);
C1F_MOVED(Context, SetDispatchProgram);
C1F_MOVED(Context, SetDynamicState);
C1F_MOVED(Context, SetFramebufferState);
C1F_MOVED(Context, SetVertexBuffers);
C1F_MOVED(Context, SetIndexBuffer);
C1F_MOVED(Context, SetSamplerViews);
C1F_MOVED(Context, BindSamplerStates);
C1F_MOVED(Context, SetShaderImages);
// P5e (sb, CONTRACT-P5E.md §5.6): set_shader_buffers is the 35th routed row. It rides both
// tables like every other set_* with an applier entry point - the class is a FIELD of the
// payload, so all three binding-point classes go through this one cell.
C1F_MOVED(Context, SetShaderBuffers);
C1F_MOVED(Context, SetGlobalConstants);
C1F_MOVED(Context, SetVertexAttribDefaults);
C1F_MOVED(Context, SetPixelPackState);
C1F_MOVED(Context, SetPatchState);
// P5c rv (CONTRACT-P5C.md §5.3): the residual-value record is the 34th routed row - an
// ordinary set_* row with an MGPipeApply* entry point, so it rides BOTH tables like its
// siblings (its producer is transport-gated in PipeFill.cpp, which is a different gate's
// business).
C1F_MOVED(Context, SetContextValues);
C1F_MOVED(Context, SetResidualValueState);
C1F_MOVED(Context, SetTextureParams);
C1F_MOVED(Context, ResourceSubData);
C1F_MOVED(Context, BufferSubDataResident);
C1F_MOVED(Context, ResourceReadback);
#undef C1F_MOVED
#define C1F_ESCAPE(Row) EXPECT_NE(gMGPipeRouteEscapes.Row, MGPipeMonolithEscapes().Row) << #Row
C1F_ESCAPE(ResourceRespecify);
C1F_ESCAPE(ResourceFlushRange);
C1F_ESCAPE(MapPersistent);
C1F_ESCAPE(CreateShaderState);
// P5e (pg): set_program_bindings is the fifth escape - three tails in three index spaces
// plus a parallel name array, which no generated row can express (MG_Pipe/PipeRoute.h).
C1F_ESCAPE(SetProgramBindings);
#undef C1F_ESCAPE
const SizeT movedContext = CountDifferingCells(gMGPipeContext, MGPipeMonolithContext());
EXPECT_EQ(movedScreen + movedContext, 35u)
<< "exactly the 35 generated routed rows must differ from the monolith adapters "
"(33 at P5, + set_context_values at P5c rv, + set_shader_buffers at P5e sb); "
<< movedScreen + movedContext
<< " did, so a row was left on the monolith adapter (it would run the applier on the GL "
"thread under split) or an unrouted row was overwritten";
const SizeT movedEscapes = CountDifferingCells(gMGPipeRouteEscapes, MGPipeMonolithEscapes());
EXPECT_EQ(movedEscapes, 5u) << "the five escape routes must move off the monolith escapes too";
// Restore the monolith arm for the sibling cases that assert it (and for a clean binary).
MGPipeInstallMonolithTables();
EXPECT_EQ(static_cast<int>(MGPipeInstalledArm()), static_cast<int>(MGPipeRouteArm::kMonolith));
}
#if MGTEST_HAVE_FORK
TEST(PipeRouting, AClientWireRowWithNoSessionRefusesByNameRatherThanApplying) {
// THE RUNTIME HALF of B3, and it distinguishes a Wire_* row from the monolith adapter by
// BEHAVIOUR: with the client tables installed and no session, a routed call reaches
// RequireSession and aborts Fatal{NoClientSession}. The monolith adapter (the deleted-
// assignment state) would instead run MGPipeApply* and NOT abort with that string - so the
// control goes red the moment a row falls back to monolith.
const ChildResult child = RunInChild([] {
InstallClientWireTables();
MGPHandleOnly handle{};
handle.Handle = MGPipeHandle{1, 0};
handle.Kind = static_cast<Uint32>(MGPipeKind::Renderbuffer);
gMGPipeScreen.ResourceDestroy(&handle); // Wire_ResourceDestroy, no session
});
EXPECT_TRUE(DiedOfAbort(child)) << DescribeStatus(child) << "\n" << child.Log;
EXPECT_NE(child.Log.find("Fatal{NoClientSession, \"ResourceDestroy\"}"), std::string::npos)
<< "the installed row did not refuse by name; it may be the monolith adapter (B3)\n"
<< child.Log;
}
TEST(PipeRouting, ARoutedCallDuringTeardownRefusesByNameNotRunsTheApplier) {
// codex 4: UninstallClientWireTables marks the tables uninstalled; a routed call in that
// window must abort by name rather than run the applier on the caller. Reverting Uninstall
// to reinstall the monolith adapters (round 2's behaviour) makes this call run the applier
// and NOT abort with this string - the red-once.
const ChildResult child = RunInChild([] {
InstallClientWireTables();
UninstallClientWireTables();
MGPHandleOnly handle{};
handle.Handle = MGPipeHandle{1, 0};
handle.Kind = static_cast<Uint32>(MGPipeKind::Renderbuffer);
gMGPipeScreen.ResourceDestroy(&handle);
});
EXPECT_TRUE(DiedOfAbort(child)) << DescribeStatus(child) << "\n" << child.Log;
EXPECT_NE(child.Log.find("Fatal{ClientTablesUninstalled, \"ResourceDestroy\"}"), std::string::npos)
<< "a routed call after UninstallClientWireTables ran the applier on the caller instead "
"of refusing by name (codex 4)\n"
<< child.Log;
}
#endif // MGTEST_HAVE_FORK
// =====================================================================================
// M2 / codex 11: a short or non-OK reply is refused, never scattered as pixels
// =====================================================================================
TEST(RemoteReadback, AReplyIsScatteredOnlyWhenItIsOkAndExactlyTheReadsExtent) {
// EmitReadPixels decides on ReadbackReplyIsComplete before it scatters or returns (the Fatal
// wording each mode owns is at the call site). Driving the production predicate directly:
// only a full OK reply is complete; a short OK reply, and a DECLINE or ERROR with a zero
// payload, are not - and those are the shapes that would otherwise spray stale destination
// bytes as pixels. `tight` is the read's own DstSize (CONTRACT-P5 row 23).
constexpr Int32 kOk = 0, kDeclined = 1, kError = 2;
const Uint64 tight = TightReadbackByteCount(4, 3, 0x1908, 0x1401); // 48
EXPECT_TRUE(ReadbackReplyIsComplete(kOk, tight, tight)) << "a full OK reply is the only one scattered";
EXPECT_FALSE(ReadbackReplyIsComplete(kOk, tight - 16, tight))
<< "a SHORT OK reply (one row missing) must not be scattered - the missing rows would be "
"whatever the destination held";
EXPECT_FALSE(ReadbackReplyIsComplete(kDeclined, 0, tight)) << "a DECLINED reply carries no pixels";
EXPECT_FALSE(ReadbackReplyIsComplete(kError, 0, tight)) << "an ERROR reply carries no pixels";
EXPECT_FALSE(ReadbackReplyIsComplete(kOk, 0, tight)) << "an OK reply of zero bytes is not the extent";
}
#include "RemoteClientControls.inc"
#include "RemoteClientAdoptT0.inc"
#include <MG_Impl/Pipe/FramebufferEmit.h>
#include <MG_Impl/Pipe/TextureEmit.h>
#include <MG_State/GLState/TextureState/TextureObject2D.h>
// f1: the installed emitters are decoded by a peer on the apply thread.
#if MGTEST_HAVE_FORK
namespace {
struct F1Peer : Codec::WireVerbSink {
MGPClear clear{};
MGPCopyFromFramebuffer copy{};
MGPMipPlan mip{};
MGPBlit blit{};
unsigned calls = 0;
Bool OnClear(const MGPClear& v) override {
clear = v;
++calls;
return true;
}
Bool OnCopyFramebufferToTexture(const MGPCopyFromFramebuffer& v) override {
copy = v;
++calls;
return true;
}
Bool OnGenerateMipmap(const MGPMipPlan& v) override {
mip = v;
++calls;
return true;
}
Bool OnBlit(const MGPBlit& v) override {
blit = v;
++calls;
return true;
}
void Install() {
if (Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting(
[](void* self) {
auto& decoder = Srv::ServerSessionInstance().Applier().*PeerMember(DecoderTag{});
decoder.SetVerbSink(static_cast<F1Peer*>(self));
return MOBILEGL_OK;
},
this) != MOBILEGL_OK)
::_exit(82);
}
};
} // namespace
TEST(RemoteF1, ClearBufferfvFieldsCross) {
// Red once (executed, reverted): zero the emitted clear values; F1.ClearBufferfv.fields fails.
const auto child = RunInChild([] {
StartControlSession();
F1Peer peer;
peer.Install();
const GLfloat value[4] = {1, 7, 13, 23};
RemoteEmitTable().GL.ClearBufferfv(GL_COLOR, 3, value);
const auto& r = peer.clear;
if (peer.calls != 1 || r.Kind != kMGPipeClearKindColor || r.ValueClass != kMGPipeClearValueClassFloat ||
r.DrawBufferIndex != 3 || std::memcmp(r.ColorValue, value, sizeof(value)) != 0 ||
!MGPipeHandleIsNull(r.Fbo))
::_exit(101);
ClientSessionInstance().Stop();
});
EXPECT_TRUE(WIFEXITED(child.Status) && WEXITSTATUS(child.Status) == 0)
<< "F1.ClearBufferfv.fields: " << DescribeStatus(child) << child.Log;
}
TEST(RemoteF1, ClearNamedFramebufferfvFieldsCross) {
// Red once (executed, reverted): zero the emitted clear values; F1.ClearNamedFramebufferfv.fields fails.
const auto child = RunInChild([] {
StartControlSession();
F1Peer peer;
peer.Install();
const GLfloat value[4] = {1, 7, 13, 23};
const auto fbo = MakeShared<MG_State::GLState::FramebufferObject>(73);
RemoteEmitTable().GL.ClearNamedFramebufferfv(fbo, GL_COLOR, 3, value);
const auto& r = peer.clear;
if (peer.calls != 1 || r.Kind != kMGPipeClearKindColor || r.ValueClass != kMGPipeClearValueClassFloat ||
r.DrawBufferIndex != 3 || std::memcmp(r.ColorValue, value, sizeof(value)) != 0 ||
r.Fbo != MGPipeFramebufferEmitter::HandleFor(*fbo))
::_exit(101);
ClientSessionInstance().Stop();
});
EXPECT_TRUE(WIFEXITED(child.Status) && WEXITSTATUS(child.Status) == 0)
<< "F1.ClearNamedFramebufferfv.fields: " << DescribeStatus(child) << child.Log;
}
TEST(RemoteF1, ClearBufferivFieldsCross) {
// Red once (executed, reverted): zero the emitted clear values; F1.ClearBufferiv.fields fails.
const auto child = RunInChild([] {
StartControlSession();
F1Peer peer;
peer.Install();
const GLint value[4] = {1, 7, 13, 23};
RemoteEmitTable().GL.ClearBufferiv(GL_COLOR, 3, value);
const auto& r = peer.clear;
if (peer.calls != 1 || r.Kind != kMGPipeClearKindColor || r.ValueClass != kMGPipeClearValueClassInt ||
r.DrawBufferIndex != 3 || std::memcmp(r.ColorValue, value, sizeof(value)) != 0 ||
!MGPipeHandleIsNull(r.Fbo))
::_exit(101);
ClientSessionInstance().Stop();
});
EXPECT_TRUE(WIFEXITED(child.Status) && WEXITSTATUS(child.Status) == 0)
<< "F1.ClearBufferiv.fields: " << DescribeStatus(child) << child.Log;
}
TEST(RemoteF1, ClearNamedFramebufferivFieldsCross) {
// Red once (executed, reverted): zero the emitted clear values; F1.ClearNamedFramebufferiv.fields fails.
const auto child = RunInChild([] {
StartControlSession();
F1Peer peer;
peer.Install();
const GLint value[4] = {1, 7, 13, 23};
const auto fbo = MakeShared<MG_State::GLState::FramebufferObject>(73);
RemoteEmitTable().GL.ClearNamedFramebufferiv(fbo, GL_COLOR, 3, value);
const auto& r = peer.clear;
if (peer.calls != 1 || r.Kind != kMGPipeClearKindColor || r.ValueClass != kMGPipeClearValueClassInt ||
r.DrawBufferIndex != 3 || std::memcmp(r.ColorValue, value, sizeof(value)) != 0 ||
r.Fbo != MGPipeFramebufferEmitter::HandleFor(*fbo))
::_exit(101);
ClientSessionInstance().Stop();
});
EXPECT_TRUE(WIFEXITED(child.Status) && WEXITSTATUS(child.Status) == 0)
<< "F1.ClearNamedFramebufferiv.fields: " << DescribeStatus(child) << child.Log;
}
TEST(RemoteF1, ClearBufferuivFieldsCross) {
// Red once (executed, reverted): zero the emitted clear values; F1.ClearBufferuiv.fields fails.
const auto child = RunInChild([] {
StartControlSession();
F1Peer peer;
peer.Install();
const GLuint value[4] = {1, 7, 13, 23};
RemoteEmitTable().GL.ClearBufferuiv(GL_COLOR, 3, value);
const auto& r = peer.clear;
if (peer.calls != 1 || r.Kind != kMGPipeClearKindColor || r.ValueClass != kMGPipeClearValueClassUint ||
r.DrawBufferIndex != 3 || std::memcmp(r.ColorValue, value, sizeof(value)) != 0 ||
!MGPipeHandleIsNull(r.Fbo))
::_exit(101);
ClientSessionInstance().Stop();
});
EXPECT_TRUE(WIFEXITED(child.Status) && WEXITSTATUS(child.Status) == 0)
<< "F1.ClearBufferuiv.fields: " << DescribeStatus(child) << child.Log;
}
TEST(RemoteF1, ClearNamedFramebufferuivFieldsCross) {
// Red once (executed, reverted): zero the emitted clear values; F1.ClearNamedFramebufferuiv.fields fails.
const auto child = RunInChild([] {
StartControlSession();
F1Peer peer;
peer.Install();
const GLuint value[4] = {1, 7, 13, 23};
const auto fbo = MakeShared<MG_State::GLState::FramebufferObject>(73);
RemoteEmitTable().GL.ClearNamedFramebufferuiv(fbo, GL_COLOR, 3, value);
const auto& r = peer.clear;
if (peer.calls != 1 || r.Kind != kMGPipeClearKindColor || r.ValueClass != kMGPipeClearValueClassUint ||
r.DrawBufferIndex != 3 || std::memcmp(r.ColorValue, value, sizeof(value)) != 0 ||
r.Fbo != MGPipeFramebufferEmitter::HandleFor(*fbo))
::_exit(101);
ClientSessionInstance().Stop();
});
EXPECT_TRUE(WIFEXITED(child.Status) && WEXITSTATUS(child.Status) == 0)
<< "F1.ClearNamedFramebufferuiv.fields: " << DescribeStatus(child) << child.Log;
}
TEST(RemoteF1, ClearBufferfiFieldsCross) {
// Red once (executed, reverted): zero the emitted clear values; F1.ClearBufferfi.fields fails.
const auto child = RunInChild([] {
StartControlSession();
F1Peer peer;
peer.Install();
RemoteEmitTable().GL.ClearBufferfi(GL_DEPTH_STENCIL, 0, 0.375f, 91);
const auto& r = peer.clear;
if (peer.calls != 1 || r.Kind != kMGPipeClearKindDepthStencil || r.DrawBufferIndex != 0 ||
r.DepthValue != 0.375f || r.StencilValue != 91 || !MGPipeHandleIsNull(r.Fbo))
::_exit(101);
ClientSessionInstance().Stop();
});
EXPECT_TRUE(WIFEXITED(child.Status) && WEXITSTATUS(child.Status) == 0)
<< "F1.ClearBufferfi.fields: " << DescribeStatus(child) << child.Log;
}
TEST(RemoteF1, ClearNamedFramebufferfiFieldsCross) {
// Red once (executed, reverted): zero the emitted clear values; F1.ClearNamedFramebufferfi.fields fails.
const auto child = RunInChild([] {
StartControlSession();
F1Peer peer;
peer.Install();
const auto fbo = MakeShared<MG_State::GLState::FramebufferObject>(73);
RemoteEmitTable().GL.ClearNamedFramebufferfi(fbo, GL_DEPTH_STENCIL, 0, 0.375f, 91);
const auto& r = peer.clear;
if (peer.calls != 1 || r.Kind != kMGPipeClearKindDepthStencil || r.DrawBufferIndex != 0 ||
r.DepthValue != 0.375f || r.StencilValue != 91 || r.Fbo != MGPipeFramebufferEmitter::HandleFor(*fbo))
::_exit(101);
ClientSessionInstance().Stop();
});
EXPECT_TRUE(WIFEXITED(child.Status) && WEXITSTATUS(child.Status) == 0)
<< "F1.ClearNamedFramebufferfi.fields: " << DescribeStatus(child) << child.Log;
}
namespace {
SharedPtr<MG_State::GLState::TextureObject2D> F1Texture() {
MG_State::pGLContext = MakeUnique<MG_State::GLState::GLContext>();
auto tex = MakeShared<MG_State::GLState::TextureObject2D>(91);
tex->SetInternalFormat(TextureInternalFormat::RGBA8);
for (Uint level = 0; level != 3; ++level) {
const Int size = 8 >> level;
tex->AllocateStorage(
TextureUploadTarget::Texture2D, level,
MG_State::GLState::MipmapInput{IntVec3{size, size, 1}, static_cast<SizeT>(size * size * 4)});
}
tex->SetBaseLevel(1);
MGPipeTextureEmitterInstance().AcquireTexture(tex->GetLifetimeId(), tex.get());
MG_State::pGLContext->GetTextureUnitObject(0).GetBindingSlot(TextureTarget::Texture2D).Bind(tex);
return tex;
}
} // namespace
TEST(RemoteF1, CopyTexImage2DFieldsCross) {
// Red once (executed, reverted): increment the emitted copy level; F1.CopyTexImage2D.fields fails.
const auto child = RunInChild([] {
StartControlSession();
F1Peer peer;
peer.Install();
const auto tex = F1Texture();
RemoteEmitTable().GL.CopyTexImage2D(GL_TEXTURE_2D, 2, GL_RGBA8, -3, 4, 11, 13, 0);
const auto& r = peer.copy;
if (peer.calls != 1 || r.Dst != MGPipeTextureEmitterInstance().FindTexture(*tex) || MGPipeHandleIsNull(r.Dst) ||
r.Target != GL_TEXTURE_2D || r.Level != 2 || r.InternalFormat != GL_RGBA8 || r.X != -3 || r.Y != 4 ||
r.Width != 11 || r.Height != 13 || r.XOffset != 0 || r.YOffset != 0 || r.SubImage != 0)
::_exit(101);
ClientSessionInstance().Stop();
});
EXPECT_TRUE(WIFEXITED(child.Status) && WEXITSTATUS(child.Status) == 0)
<< "F1.CopyTexImage2D.fields: " << DescribeStatus(child) << child.Log;
}
TEST(RemoteF1, CopyTexSubImage2DFieldsCross) {
// Red once (executed, reverted): increment the emitted copy level; F1.CopyTexSubImage2D.fields fails.
const auto child = RunInChild([] {
StartControlSession();
F1Peer peer;
peer.Install();
const auto tex = F1Texture();
RemoteEmitTable().GL.CopyTexSubImage2D(GL_TEXTURE_2D, 2, 5, 7, -3, 4, 11, 13);
const auto& r = peer.copy;
if (peer.calls != 1 || r.Dst != MGPipeTextureEmitterInstance().FindTexture(*tex) || MGPipeHandleIsNull(r.Dst) ||
r.Target != GL_TEXTURE_2D || r.Level != 2 || r.InternalFormat != 0 || r.X != -3 || r.Y != 4 ||
r.Width != 11 || r.Height != 13 || r.XOffset != 5 || r.YOffset != 7 || r.SubImage != 1)
::_exit(101);
ClientSessionInstance().Stop();
});
EXPECT_TRUE(WIFEXITED(child.Status) && WEXITSTATUS(child.Status) == 0)
<< "F1.CopyTexSubImage2D.fields: " << DescribeStatus(child) << child.Log;
}
TEST(RemoteF1, GenerateMipmapFieldsCross) {
// Red once (executed, reverted): increment the emitted base level; F1.GenerateMipmap.fields fails.
const auto child = RunInChild([] {
StartControlSession();
F1Peer peer;
peer.Install();
const auto tex = F1Texture();
RemoteEmitTable().GL.GenerateMipmap(GL_TEXTURE_2D);
// THE FENCE. Under MOBILEGL_IPC_BATCH_WAITS (default on) GenerateMipmap is kCtxObject -
// a value-class record whose emit no longer waits for its own apply (production-safe:
// the record carries its whole value and the in-order ring applies it before the next
// waited verb). The peer lives on the apply thread, so reading it needs a wait
// boundary: a following kCtxVerb still waits, and its wait covers everything emitted
// before it. When this clear returns, the peer has seen both records.
const GLfloat fence[4] = {0, 0, 0, 0};
RemoteEmitTable().GL.ClearBufferfv(GL_COLOR, 0, fence);
const auto& r = peer.mip;
if (peer.calls != 2 || r.Res != MGPipeTextureEmitterInstance().FindTexture(*tex) || MGPipeHandleIsNull(r.Res) ||
r.Target != GL_TEXTURE_2D || r.BaseLevel != 1 || r.LevelCount != 3)
::_exit(101);
ClientSessionInstance().Stop();
});
EXPECT_TRUE(WIFEXITED(child.Status) && WEXITSTATUS(child.Status) == 0)
<< "F1.GenerateMipmap.fields: " << DescribeStatus(child) << child.Log;
}
namespace {
// A second texture for the by-name slots, acquired but NOT bound: F1Texture's stays on the unit.
SharedPtr<MG_State::GLState::ITextureObject> F1NamedTexture() {
auto tex = MakeShared<MG_State::GLState::TextureObject2D>(92);
tex->SetInternalFormat(TextureInternalFormat::RGBA8);
for (Uint level = 0; level != 2; ++level) {
const Int size = 4 >> level;
tex->AllocateStorage(
TextureUploadTarget::Texture2D, level,
MG_State::GLState::MipmapInput{IntVec3{size, size, 1}, static_cast<SizeT>(size * size * 4)});
}
MGPipeTextureEmitterInstance().AcquireTexture(tex->GetLifetimeId(), tex.get());
return tex;
}
} // namespace
// The by-name slots name their texture themselves: the record carries the named texture's handle
// while another texture stays bound on the active unit.
TEST(RemoteF1, CopyTextureSubImage2DNamesItsTextureNotTheBoundOne) {
const auto child = RunInChild([] {
StartControlSession();
F1Peer peer;
peer.Install();
const auto bound = F1Texture();
const auto named = F1NamedTexture();
RemoteEmitTable().GL.CopyTextureSubImage2D(named, GL_TEXTURE_2D, 1, 1, 0, -3, 4, 1, 2);
const auto& r = peer.copy;
if (peer.calls != 1 || r.Dst != MGPipeTextureEmitterInstance().FindTexture(*named) ||
MGPipeHandleIsNull(r.Dst) || r.Dst == MGPipeTextureEmitterInstance().FindTexture(*bound) ||
r.Target != GL_TEXTURE_2D || r.Level != 1 || r.XOffset != 1 || r.YOffset != 0 || r.X != -3 || r.Y != 4 ||
r.Width != 1 || r.Height != 2 || r.SubImage != 1)
::_exit(101);
ClientSessionInstance().Stop();
});
EXPECT_TRUE(WIFEXITED(child.Status) && WEXITSTATUS(child.Status) == 0)
<< "F1.CopyTextureSubImage2D.fields: " << DescribeStatus(child) << child.Log;
}
TEST(RemoteF1, GenerateTextureMipmapNamesItsTextureNotTheBoundOne) {
const auto child = RunInChild([] {
StartControlSession();
F1Peer peer;
peer.Install();
const auto bound = F1Texture();
const auto named = F1NamedTexture();
RemoteEmitTable().GL.GenerateTextureMipmap(named, GL_TEXTURE_2D);
const GLfloat fence[4] = {0, 0, 0, 0}; // see GenerateMipmapFieldsCross
RemoteEmitTable().GL.ClearBufferfv(GL_COLOR, 0, fence);
const auto& r = peer.mip;
if (peer.calls != 2 || r.Res != MGPipeTextureEmitterInstance().FindTexture(*named) ||
MGPipeHandleIsNull(r.Res) || r.Res == MGPipeTextureEmitterInstance().FindTexture(*bound) ||
r.Target != GL_TEXTURE_2D || r.BaseLevel != 0 || r.LevelCount != 2)
::_exit(101);
ClientSessionInstance().Stop();
});
EXPECT_TRUE(WIFEXITED(child.Status) && WEXITSTATUS(child.Status) == 0)
<< "F1.GenerateTextureMipmap.fields: " << DescribeStatus(child) << child.Log;
}
#endif
#if MGTEST_HAVE_FORK
namespace {
MGPipeHandle observedNamedClear{}, observedNamedClearRead{};
Uint32 observedNamedClearValues[4]{};
Uint32 observedNamedClearCalls = 0;
GLenum observedNamedClearTarget = 0;
GLint observedNamedClearIndex = 0;
GLfloat observedNamedClearDepth = 0;
GLint observedNamedClearStencil = 0;
Bool observedNamedClearRecord = false;
void ObserveNamedClear(GLenum target, GLint index) {
const auto& state = MGPipeApplier();
observedNamedClear = state.BoundFramebuffer[0];
observedNamedClearRead = state.BoundFramebuffer[1];
observedNamedClearRecord = state.FramebufferRecordFor(observedNamedClear) != nullptr;
observedNamedClearTarget = target;
observedNamedClearIndex = index;
++observedNamedClearCalls;
}
void CheckNamedClearScopedTarget(Uint8 kind, Uint8 valueClass) {
MGPipeApplierReset();
MGPipeResourceOps resources{};
MGPipeSetResourceOps(&resources);
MGPFramebufferState state{};
state.Fbo = {31, 1};
state.Target = static_cast<Uint8>(MGPipeFramebufferTarget::Draw);
MGPipeApplySetFramebufferState(state);
const auto originalDraw = state.Fbo;
state.Fbo = {32, 2};
state.Target = static_cast<Uint8>(MGPipeFramebufferTarget::Read);
MGPipeApplySetFramebufferState(state);
const auto originalRead = state.Fbo;
state.Fbo = {33, 3};
state.Target = static_cast<Uint8>(MGPipeFramebufferTarget::Named);
MGPipeApplySetFramebufferState(state);
CapsPeer backend;
backend.table.GL.ClearBufferfv = +[](GLenum target, GLint index, const GLfloat* values) {
ObserveNamedClear(target, index);
std::memcpy(observedNamedClearValues, values, sizeof(observedNamedClearValues));
};
backend.table.GL.ClearBufferiv = +[](GLenum target, GLint index, const GLint* values) {
ObserveNamedClear(target, index);
std::memcpy(observedNamedClearValues, values, sizeof(observedNamedClearValues));
};
backend.table.GL.ClearBufferuiv = +[](GLenum target, GLint index, const GLuint* values) {
ObserveNamedClear(target, index);
std::memcpy(observedNamedClearValues, values, sizeof(observedNamedClearValues));
};
backend.table.GL.ClearBufferfi = +[](GLenum target, GLint index, GLfloat depth, GLint stencil) {
ObserveNamedClear(target, index);
observedNamedClearDepth = depth;
observedNamedClearStencil = stencil;
};
Srv::ServerVerbSink sink;
sink.SetBackend(&backend);
MGPClear clear{};
clear.Fbo = state.Fbo;
clear.Kind = kind;
clear.ValueClass = valueClass;
clear.DrawBufferIndex = kind == kMGPipeClearKindDepthStencil ? 0 : 3;
const Uint32 bits[4]{0x3e800000u, 0x3f000000u, 0xff000011u, 0x3f800000u};
std::memcpy(clear.ColorValue, bits, sizeof(bits));
clear.DepthValue = 0.375f;
clear.StencilValue = 91;
if (!sink.OnClear(clear) || observedNamedClearCalls != 1) ::_exit(101);
if (observedNamedClear != clear.Fbo || !observedNamedClearRecord || observedNamedClearRead != originalRead)
::_exit(102);
if (MGPipeApplier().BoundFramebuffer[0] != originalDraw || MGPipeApplier().BoundFramebuffer[1] != originalRead)
::_exit(103);
if (observedNamedClearIndex != clear.DrawBufferIndex) ::_exit(104);
if (kind == kMGPipeClearKindDepthStencil) {
if (observedNamedClearTarget != GL_DEPTH_STENCIL || observedNamedClearDepth != clear.DepthValue ||
observedNamedClearStencil != clear.StencilValue)
::_exit(105);
} else if (observedNamedClearTarget != GL_COLOR ||
std::memcmp(observedNamedClearValues, clear.ColorValue, sizeof(bits)))
::_exit(106);
MGPipeSetResourceOps(nullptr);
sink.SetBackend(nullptr);
}
} // namespace
TEST(RemoteF1, UnboundNamedfvRefusesByName) {
// Historical name retained; a named target now reaches the native clear hook.
const auto child =
RunInChild([] { CheckNamedClearScopedTarget(kMGPipeClearKindColor, kMGPipeClearValueClassFloat); });
ExpectChildSuccess(child);
}
TEST(RemoteF1, UnboundNamedivRefusesByName) {
const auto child =
RunInChild([] { CheckNamedClearScopedTarget(kMGPipeClearKindColor, kMGPipeClearValueClassInt); });
ExpectChildSuccess(child);
}
TEST(RemoteF1, UnboundNameduivRefusesByName) {
const auto child =
RunInChild([] { CheckNamedClearScopedTarget(kMGPipeClearKindColor, kMGPipeClearValueClassUint); });
ExpectChildSuccess(child);
}
TEST(RemoteF1, UnboundNamedfiRefusesByName) {
const auto child =
RunInChild([] { CheckNamedClearScopedTarget(kMGPipeClearKindDepthStencil, kMGPipeClearValueClassFloat); });
ExpectChildSuccess(child);
}
#endif
// =====================================================================================
// P5c (hd, CONTRACT-P5C §3): the record's handles are the server's resolution. The sink
// publishes each verb's own handles into the applier's verb stash (§3.2), the named blit's
// client emitter no longer rebinds anything (§3.3), and the layer-1 guards refuse the
// client-only surfaces from the apply thread by name (§3.1, §3.7, §3.8).
// =====================================================================================
#if MGTEST_HAVE_FORK
#include <MG_Backend/DirectGLES/SlotTables.h>
#include <MG_Backend/DirectGLES/Utils.h>
#include <MG_Impl/Pipe/SlotAllocator.h>
#include <MG_State/GLState/BufferState/BufferObject.h>
TEST(RemoteF1, BlitNamedCarriesHandlesAndLeavesTheClientBindingsAlone) {
// Red once (executed, reverted): re-bind the named arguments in the client shadow before
// emitting (the deleted ScopedBlitBindings); the two binding-slot asserts fail.
const auto child = RunInChild([] {
StartControlSession();
F1Peer peer;
peer.Install();
MG_State::pGLContext = MakeUnique<MG_State::GLState::GLContext>();
auto boundRead = MakeShared<MG_State::GLState::FramebufferObject>(11);
auto boundDraw = MakeShared<MG_State::GLState::FramebufferObject>(12);
auto namedRead = MakeShared<MG_State::GLState::FramebufferObject>(13);
auto namedDraw = MakeShared<MG_State::GLState::FramebufferObject>(14);
MG_State::pGLContext->GetFramebufferBindingSlot(MobileGL::FramebufferTarget::Read).Bind(boundRead);
MG_State::pGLContext->GetFramebufferBindingSlot(MobileGL::FramebufferTarget::Draw).Bind(boundDraw);
RemoteEmitTable().GL.BlitNamedFramebuffer(namedRead, namedDraw, 0, 0, 8, 8, 0, 0, 8, 8, GL_COLOR_BUFFER_BIT,
GL_NEAREST);
const auto& r = peer.blit;
if (peer.calls != 1 || r.ReadFbo != MGPipeFramebufferEmitter::HandleFor(*namedRead) ||
r.DrawFbo != MGPipeFramebufferEmitter::HandleFor(*namedDraw))
::_exit(101);
// The client's own bindings are untouched: the server resolves the named pair from the
// record, not from a staged binding override (T3).
if (MG_State::pGLContext->GetFramebufferBindingSlot(MobileGL::FramebufferTarget::Read).GetBoundObject().get() !=
boundRead.get())
::_exit(102);
if (MG_State::pGLContext->GetFramebufferBindingSlot(MobileGL::FramebufferTarget::Draw).GetBoundObject().get() !=
boundDraw.get())
::_exit(103);
ClientSessionInstance().Stop();
});
ExpectChildSuccess(child);
}
namespace {
Bool g_stubBlitCalled = false;
MGPipeHandle g_observedBlitRead = kMGPipeNullHandle;
MGPipeHandle g_observedBlitDraw = kMGPipeNullHandle;
void StubBlitFramebuffer(GLint, GLint, GLint, GLint, GLint, GLint, GLint, GLint, GLbitfield, GLenum) {
g_stubBlitCalled = true;
const auto& st = MG_Pipe::MGPipeApplier();
g_observedBlitRead = st.VerbBlitReadFbo;
g_observedBlitDraw = st.VerbBlitDrawFbo;
}
void StubBlitFramebufferConsuming(GLint x0, GLint y0, GLint x1, GLint y1, GLint dx0, GLint dy0, GLint dx1,
GLint dy1, GLbitfield mask, GLenum filter) {
StubBlitFramebuffer(x0, y0, x1, y1, dx0, dy0, dx1, dy1, mask, filter);
MG_Pipe::MGPipeApplier().VerbBlitNamedConsumed = true;
}
MGPipeHandle g_observedCopyDst = kMGPipeNullHandle;
void StubCopyTexImage2D(GLenum, GLint, GLenum, GLint, GLint, GLsizei, GLsizei, GLint) {
g_observedCopyDst = MG_Pipe::MGPipeApplier().VerbCopyTexDst;
}
MGPipeHandle g_observedMipRes = kMGPipeNullHandle;
void StubGenerateMipmap(GLenum) {
g_observedMipRes = MG_Pipe::MGPipeApplier().VerbMipRes;
}
MGPipeHandle g_observedIndirect = kMGPipeNullHandle;
MGPipeHandle g_observedParameter = kMGPipeNullHandle;
void StubMultiDrawArraysIndirectCount(GLenum, const void*, GLintptr, GLsizei, GLsizei) {
const auto& st = MG_Pipe::MGPipeApplier();
g_observedIndirect = st.VerbIndirectBuffer;
g_observedParameter = st.VerbIndirectParameterBuffer;
}
MGPipeHandle g_observedDispatchIndirect = kMGPipeNullHandle;
void StubDispatchComputeIndirect(GLintptr) {
g_observedDispatchIndirect = MG_Pipe::MGPipeApplier().VerbDispatchIndirectBuffer;
}
} // namespace
TEST(RemoteF1, NamedBlitStashesHandlesAndDeclinesWhenTheBackendHasNoNamedArm) {
// Red once (executed, reverted): do not write the verb stash; the observed handles are null.
const auto child = RunInChild([] {
MG_Config::Transport = MG_Config::TransportMode::InProcess;
CapsPeer backend;
backend.table.GL.BlitFramebuffer = &StubBlitFramebuffer;
Srv::ServerVerbSink sink;
sink.SetBackend(&backend);
MGPBlit r{};
r.ReadFbo = {41, 2};
r.DrawFbo = {42, 3};
r.Mask = GL_COLOR_BUFFER_BIT;
r.Filter = GL_NEAREST;
const Bool applied = sink.OnBlit(r);
if (applied) ::_exit(101); // a named pair the backend did not consume must decline
if (!g_stubBlitCalled) ::_exit(102);
// The backend saw the record's handles as the verb's own state (CONTRACT-P5C §3.2/§3.3).
if (g_observedBlitRead != r.ReadFbo || g_observedBlitDraw != r.DrawFbo) ::_exit(103);
::_exit(0);
});
ExpectChildSuccess(child);
}
TEST(RemoteF1, NamedBlitIsAppliedWhenTheBackendConsumesThePair) {
const auto child = RunInChild([] {
MG_Config::Transport = MG_Config::TransportMode::InProcess;
CapsPeer backend;
backend.table.GL.BlitFramebuffer = &StubBlitFramebufferConsuming;
Srv::ServerVerbSink sink;
sink.SetBackend(&backend);
MGPBlit r{};
r.ReadFbo = {41, 2};
r.DrawFbo = {42, 3};
r.Mask = GL_COLOR_BUFFER_BIT;
r.Filter = GL_NEAREST;
if (!sink.OnBlit(r)) ::_exit(101);
if (g_observedBlitRead != r.ReadFbo || g_observedBlitDraw != r.DrawFbo) ::_exit(102);
::_exit(0);
});
ExpectChildSuccess(child);
}
TEST(RemoteF1, BoundBlitCarriesNoHandlesAndIsAppliedAsBefore) {
const auto child = RunInChild([] {
MG_Config::Transport = MG_Config::TransportMode::InProcess;
CapsPeer backend;
backend.table.GL.BlitFramebuffer = &StubBlitFramebuffer;
Srv::ServerVerbSink sink;
sink.SetBackend(&backend);
MGPBlit r{};
r.Mask = GL_COLOR_BUFFER_BIT;
r.Filter = GL_NEAREST;
if (!sink.OnBlit(r)) ::_exit(101);
if (!MGPipeHandleIsNull(g_observedBlitRead) || !MGPipeHandleIsNull(g_observedBlitDraw)) ::_exit(102);
::_exit(0);
});
ExpectChildSuccess(child);
}
TEST(RemoteF1, CopyFramebufferToTextureStashesTheDestinationHandle) {
// Red once (executed, reverted): do not write VerbCopyTexDst; the observed handle is null.
const auto child = RunInChild([] {
MG_Config::Transport = MG_Config::TransportMode::InProcess;
CapsPeer backend;
backend.table.GL.CopyTexImage2D = &StubCopyTexImage2D;
Srv::ServerVerbSink sink;
sink.SetBackend(&backend);
MGPCopyFromFramebuffer r{};
r.Dst = {51, 4};
r.Target = GL_TEXTURE_2D;
r.Level = 1;
r.InternalFormat = GL_RGBA8;
r.Width = 8;
r.Height = 8;
if (!sink.OnCopyFramebufferToTexture(r)) ::_exit(101);
if (g_observedCopyDst != r.Dst) ::_exit(102);
::_exit(0);
});
ExpectChildSuccess(child);
}
TEST(RemoteF1, GenerateMipmapStashesTheTextureHandle) {
// Red once (executed, reverted): do not write VerbMipRes; the observed handle is null.
const auto child = RunInChild([] {
MG_Config::Transport = MG_Config::TransportMode::InProcess;
CapsPeer backend;
backend.table.GL.GenerateMipmap = &StubGenerateMipmap;
Srv::ServerVerbSink sink;
sink.SetBackend(&backend);
MGPMipPlan r{};
r.Res = {52, 5};
r.Target = GL_TEXTURE_2D;
if (!sink.OnGenerateMipmap(r)) ::_exit(101);
if (g_observedMipRes != r.Res) ::_exit(102);
::_exit(0);
});
ExpectChildSuccess(child);
}
TEST(RemoteF1, AnIndirectDrawStashesTheCommandAndParameterBuffers) {
// Red once (executed, reverted): do not write the indirect stash; the observed handles are null.
const auto child = RunInChild([] {
MG_Config::Transport = MG_Config::TransportMode::InProcess;
CapsPeer backend;
backend.table.GL.MultiDrawArraysIndirectCount = &StubMultiDrawArraysIndirectCount;
Srv::ServerVerbSink sink;
sink.SetBackend(&backend);
MGPDrawInfo info{};
info.Mode = GL_TRIANGLES;
info.IndexSize = 0;
info.NumDraws = 0;
info.InstanceCount = 1;
MGPDrawIndirect indirect{};
indirect.Buffer = {61, 1};
indirect.ParameterBuffer = {62, 1};
indirect.Offset = 64;
indirect.DrawCount = 3;
indirect.Stride = 16;
if (!sink.OnDrawVbo(info, nullptr, nullptr, &indirect)) ::_exit(101);
if (g_observedIndirect != indirect.Buffer || g_observedParameter != indirect.ParameterBuffer) ::_exit(102);
::_exit(0);
});
ExpectChildSuccess(child);
}
TEST(RemoteF1, DispatchIndirectStashesTheCommandBuffer) {
// Red once (executed, reverted): do not write VerbDispatchIndirectBuffer; the observed
// handle is null.
const auto child = RunInChild([] {
MG_Config::Transport = MG_Config::TransportMode::InProcess;
CapsPeer backend;
backend.table.GL.DispatchComputeIndirect = &StubDispatchComputeIndirect;
Srv::ServerVerbSink sink;
sink.SetBackend(&backend);
MGPGridInfo grid{};
grid.IsIndirect = 1;
grid.IndirectBuffer = {63, 1};
grid.IndirectOffset = 128;
if (!sink.OnLaunchGrid(grid)) ::_exit(101);
if (g_observedDispatchIndirect != grid.IndirectBuffer) ::_exit(102);
::_exit(0);
});
ExpectChildSuccess(child);
}
// ---- the layer-1 guards (CONTRACT-P5C §6), each verified red once by its own Fatal ---------
TEST(RemoteGuards, AllocatorAcquireFromTheApplyThreadIsFatalByName) {
const auto child = RunInChild([] {
StartControlSession();
Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting(
[](void*) {
MGPipeSlots().Acquire(MGPipeKind::Texture, 424242);
return MOBILEGL_OK;
},
nullptr);
ClientSessionInstance().Stop();
});
ExpectNamedAbort(child, "Fatal{RoleViolation, \"MGPipeSlots\"}");
}
TEST(RemoteGuards, AllocatorFindByLifetimeIdFromTheApplyThreadIsFatalByName) {
const auto child = RunInChild([] {
StartControlSession();
Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting(
[](void*) {
MGPipeSlots().FindByLifetimeId(MGPipeKind::Texture, 424242);
return MOBILEGL_OK;
},
nullptr);
ClientSessionInstance().Stop();
});
ExpectNamedAbort(child, "Fatal{RoleViolation, \"MGPipeSlots\"}");
}
TEST(RemoteGuards, AllocatorFreeFromTheApplyThreadIsFatalByName) {
const auto child = RunInChild([] {
StartControlSession();
Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting(
[](void*) {
MGPipeSlots().Free(MGPipeKind::Texture, {7, 1});
return MOBILEGL_OK;
},
nullptr);
ClientSessionInstance().Stop();
});
ExpectNamedAbort(child, "Fatal{RoleViolation, \"MGPipeSlots\"}");
}
namespace {
struct FakeTwin {
int marker = 0;
};
using TestTextureTable =
MG_Backend::DirectGLES::BackendSlotTable<MG_State::GLState::TextureObject2D, FakeTwin, MGPipeKind::Texture>;
} // namespace
// The four accessor drives share one shape: the BufferObject is constructed on the CLIENT
// thread (its own resource_create publication is a legal client-side emit), and only the
// accessor runs on the apply thread, where it must die at the accessor's own guard.
#define MGL_BUFFER_GUARD_TEST(Name, Id, Call) \
TEST(RemoteGuards, Name) { \
const auto child = RunInChild([] { \
StartControlSession(); \
MG_State::GLState::BufferObject buffer(Id); \
Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting( \
[](void* self) { \
auto& buffer = *static_cast<MG_State::GLState::BufferObject*>(self); \
Call; \
return MOBILEGL_OK; \
}, \
&buffer); \
ClientSessionInstance().Stop(); \
}); \
ExpectNamedAbort(child, "Fatal{RoleViolation, \"buffer-legacy-arm\"}"); \
}
MGL_BUFFER_GUARD_TEST(BufferMappedDataFromTheApplyThreadIsFatalByName, 703, (void)buffer.MappedData())
MGL_BUFFER_GUARD_TEST(BufferIsMappedFromTheApplyThreadIsFatalByName, 704, (void)buffer.IsMapped())
MGL_BUFFER_GUARD_TEST(BufferChangeSerialFromTheApplyThreadIsFatalByName, 705, (void)buffer.GetChangeSerial())
MGL_BUFFER_GUARD_TEST(BufferSyncPersistentMappedRangeFromTheApplyThreadIsFatalByName, 706,
buffer.SyncPersistentMappedRange())
MGL_BUFFER_GUARD_TEST(BufferHasDefinedContentFromTheApplyThreadIsFatalByName, 707, (void)buffer.HasDefinedContent())
#undef MGL_BUFFER_GUARD_TEST
// All allocator access on apply is client-memory access regardless of whether that memory
// happens to be stable (P5f), so the allocator refuses it by name.
TEST(RemoteGuards, AnAllocatorProbeFromTheApplyThreadOutsideEveryScopeIsFatalByName) {
const auto child = RunInChild([] {
StartControlSession();
Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting(
+[](void*) -> MobileGLResult {
MG_Pipe::MGPipeRefuseAllocatorFromApplyThread("FindByLifetimeId");
return MOBILEGL_OK;
},
nullptr);
ClientSessionInstance().Stop();
});
ExpectNamedAbort(child, "Fatal{RoleViolation, \"MGPipeSlots\"}");
}
// P5f (fr): a barriered record does not admit the client allocator either (the named scopes
// that once exempted it were deleted at P13, with the last frontend-keyed backend entry).
TEST(RemoteGuards, BarrieredLegacyScopesCannotExemptAllocator) {
for (const auto backend : {BackendType::DirectGLES, BackendType::DirectVulkan}) {
const auto child = RunInChild([backend] {
StartControlSession();
auto selectedBackend = backend;
Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting(
+[](void* self) -> MobileGLResult {
MG_Pipe::MGPipeApplierSetCurrentRecordBarriered(true);
MG_Config::ActiveBackendType = *static_cast<BackendType*>(self);
// HighWater had no per-method guard in the old implementation.
(void)MGPipeSlots().HighWater(MGPipeKind::Texture);
return MOBILEGL_OK;
},
&selectedBackend);
ClientSessionInstance().Stop();
});
ExpectNamedAbort(child, "Fatal{RoleViolation, \"MGPipeSlots\"}");
}
}
TEST(RemoteGuards, HandleRegistryMembersWorkOnBarrieredAndUnbarrieredApply) {
const auto child = RunInChild([] {
StartControlSession();
Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting(
+[](void*) -> MobileGLResult {
TestTextureTable table;
for (const bool barriered : {true, false}) {
MG_Pipe::MGPipeApplierSetCurrentRecordBarriered(barriered);
const MGPipeHandle handle{71, barriered ? 1u : 2u};
auto& twin = table.GetOrCreate(handle);
twin = MakeShared<FakeTwin>();
twin->marker = 19;
auto* found = table.FindByHandle(handle);
if (!found || !*found || (*found)->marker != 19) _exit(81);
unsigned live = 0;
table.ForEachLive([&](MGPipeHandle actual, const auto& backend) {
if (actual.Slot != handle.Slot || actual.Gen != handle.Gen || backend->marker != 19) _exit(82);
++live;
});
if (live != 1 || !table.ReleaseByHandle(handle) || table.FindByHandle(handle)) _exit(83);
}
return MOBILEGL_OK;
},
nullptr);
ClientSessionInstance().Stop();
});
ASSERT_TRUE(WIFEXITED(child.Status)) << child.Log;
EXPECT_EQ(WEXITSTATUS(child.Status), 0) << child.Log;
}
TEST(RemoteGuards, CapsMirrorFallbackWithNoServerBackendIsFatalByName) {
const auto child = RunInChild([] {
MG_Config::Transport = MG_Config::TransportMode::InProcess;
// No server backend exists in this process: the format-caps fallback to the client
// mirror is refused by name (CONTRACT-P5C §3.7).
(void)MG_Backend::DirectGLES::ActiveBackendFormatCaps();
});
ExpectNamedAbort(child, "Fatal{RoleViolation, \"caps-mirror\"}");
}
// P5c (gt, CONTRACT-P5C §6 layer 1): the TEXTURE family's object-surface list, in executable
// form - this block IS the pinned list §5.4(b) asks for (the buffer family's five are the
// MGL_BUFFER_GUARD_TEST block above). Every drive shares the buffer family's shape: the texture
// is constructed on the CLIENT thread (its resource_create mint and emission are legal
// client-side), and only the probed method runs on the apply thread, where the guard fires
// BEFORE any level precondition could - the method names below are exactly the ones the guard
// (MipmapStorage.cpp's RefuseLegacyTextureArmFromApplyThread) is hung on:
//
// AllocateStorage, TruncateMipmapLevels, UpdateMipmapSubData, MapMipmapData,
// MarkStorageDirty, MarkStorageDirtyRegion, IsStorageDirty, GetStorageDirtyRegion,
// GetStorageDirtyRects, GetStorageDirtyFootprint (P11 M2)
//
// P5e (tx2): THE SHAPE READS JOIN THE LIST, and the sentence that stood here is why they could
// not before - "the per-draw binding walk reads them every draw". P5e is the commit that makes
// that untrue: the unit work list is st.BoundSamplerViews[], the clean gate is
// IsDrawSyncCleanByRecord, and the three sync bodies read the resource record and the server's
// staged-texture store. Nothing on the draw path asks a frontend texture for a level count, a
// level extent, a level byte size or a compressed level any more, so the exemption becomes a
// guard and reverting any handle arm aborts BY ACCESSOR NAME:
//
// GetMipmapLevelCount, GetMipmapTexelSize, GetMipmapByteSize,
// GetCompressedFormat, GetCompressedByteSize, MapCompressedMipmapData,
// GetRequestedCompressedFormat
//
// Keyed on the SERVER BACKEND being DirectGLES (ruling 12's shape): Magma is lockstep through
// P7 and its named blit still asks a frontend texture IsComplete(), which is legal for a client
// parked in its own wait. Still NOT in the list: GetUploadTargets / GetTarget / IsComplete,
// which are TextureObject's own and not MipmapStorage's - their rows retire with the object
// class in P7/P9.
#define MGL_TEXTURE_GUARD_TEST(Name, Id, Call) \
TEST(RemoteGuards, Name) { \
const auto child = RunInChild([] { \
StartControlSession(); \
MG_State::GLState::TextureObject2D texture(Id); \
Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting( \
[](void* self) { \
auto& texture = *static_cast<MG_State::GLState::TextureObject2D*>(self); \
Call; \
return MOBILEGL_OK; \
}, \
&texture); \
ClientSessionInstance().Stop(); \
}); \
ExpectNamedAbort(child, "Fatal{RoleViolation, \"texture-legacy-arm\"}"); \
}
MGL_TEXTURE_GUARD_TEST(TextureAllocateStorageFromTheApplyThreadIsFatalByName, 711,
texture.AllocateStorage(TextureUploadTarget::Texture2D, 0, {{4, 4, 1}, 64}))
MGL_TEXTURE_GUARD_TEST(TextureTruncateMipmapLevelsFromTheApplyThreadIsFatalByName, 712,
texture.TruncateMipmapLevels(TextureUploadTarget::Texture2D, 1))
MGL_TEXTURE_GUARD_TEST(TextureUpdateMipmapSubDataFromTheApplyThreadIsFatalByName, 713,
texture.UpdateMipmapSubData(TextureUploadTarget::Texture2D, 0, {}))
MGL_TEXTURE_GUARD_TEST(TextureMapMipmapDataFromTheApplyThreadIsFatalByName, 714,
(void)texture.MapMipmapData(TextureUploadTarget::Texture2D, 0))
MGL_TEXTURE_GUARD_TEST(TextureMarkStorageDirtyFromTheApplyThreadIsFatalByName, 715,
texture.MarkStorageDirty(TextureUploadTarget::Texture2D, 0, true))
MGL_TEXTURE_GUARD_TEST(TextureMarkStorageDirtyRegionFromTheApplyThreadIsFatalByName, 716,
texture.MarkStorageDirtyRegion(TextureUploadTarget::Texture2D, 0, {0, 0, 0}, {1, 1, 1}))
MGL_TEXTURE_GUARD_TEST(TextureIsStorageDirtyFromTheApplyThreadIsFatalByName, 717,
(void)texture.IsStorageDirty(TextureUploadTarget::Texture2D, 0))
MGL_TEXTURE_GUARD_TEST(TextureGetStorageDirtyRegionFromTheApplyThreadIsFatalByName, 718,
(void)texture.GetStorageDirtyRegion(TextureUploadTarget::Texture2D, 0))
MGL_TEXTURE_GUARD_TEST(TextureGetStorageDirtyRectsFromTheApplyThreadIsFatalByName, 719,
(void)texture.GetStorageDirtyRects(TextureUploadTarget::Texture2D, 0, nullptr, 0))
// P11 M2: dev 9524c688's exact dirty footprint joins the dirty model's rows. The server's
// footprint is the record's region list (Managers.cpp's handle arm), never this accessor.
MGL_TEXTURE_GUARD_TEST(TextureGetStorageDirtyFootprintFromTheApplyThreadIsFatalByName, 720, ([&texture] {
Vector<MG_State::GLState::MipmapDirtyRegion> footprint;
texture.GetStorageDirtyFootprint(TextureUploadTarget::Texture2D, 0, footprint);
}()))
// ---- P5e (tx2): the seven shape reads the P4a/P5c list exempted ----------------------------
MGL_TEXTURE_GUARD_TEST(TextureGetMipmapLevelCountFromTheApplyThreadIsFatalByName, 721,
(void)texture.GetMipmapLevelCount())
MGL_TEXTURE_GUARD_TEST(TextureGetMipmapTexelSizeFromTheApplyThreadIsFatalByName, 722,
(void)texture.GetMipmapTexelSize(TextureUploadTarget::Texture2D, 0))
MGL_TEXTURE_GUARD_TEST(TextureGetMipmapByteSizeFromTheApplyThreadIsFatalByName, 723,
(void)texture.GetMipmapByteSize(TextureUploadTarget::Texture2D, 0))
MGL_TEXTURE_GUARD_TEST(TextureGetCompressedFormatFromTheApplyThreadIsFatalByName, 724,
(void)texture.GetMipmapCompressedFormat(TextureUploadTarget::Texture2D, 0))
MGL_TEXTURE_GUARD_TEST(TextureGetCompressedByteSizeFromTheApplyThreadIsFatalByName, 725,
(void)texture.GetMipmapCompressedByteSize(TextureUploadTarget::Texture2D, 0))
MGL_TEXTURE_GUARD_TEST(TextureMapCompressedMipmapDataFromTheApplyThreadIsFatalByName, 726,
(void)texture.MapMipmapCompressedImage(TextureUploadTarget::Texture2D, 0))
MGL_TEXTURE_GUARD_TEST(TextureGetRequestedCompressedFormatFromTheApplyThreadIsFatalByName, 727,
(void)texture.GetMipmapRequestedCompressedFormat(TextureUploadTarget::Texture2D, 0))
#undef MGL_TEXTURE_GUARD_TEST
// P5c (gt, CONTRACT-P5C §6 layer 2 / audit A1): the client-side half of the gPipeInputs
// single-writer rule. The apply thread is NOT actually inside the applier in this case - the
// flag is raised by hand, which is the exact overlap window the check exists to refuse: a fill
// that ran while a real apply was in flight would race the applier's reads of gPipeInputs.
//
// MOBILEGL_IPC_BATCH_WAITS (default 1) makes that overlap the INTENDED shape (the fill
// writes only fields no record supplies; pulled reads stay fenced by the pull-verbs' own
// wait), so the guard fires only with the batch off. This case forces it off and keeps the
// Fatal; the case below it pins the batched arm as legal.
TEST(RemoteGuards, ClientPipeInputsFillWhileTheApplierOwnsItIsFatalByName) {
const auto child = RunInChild([] {
MG_Config::Ipc.BatchWaits = 0;
StartControlSession();
ClientSession::NoteApplyThreadEnteredApplier();
MGPipeValidateForVerb(MGPipeVerb::Clear);
ClientSession::NoteApplyThreadLeftApplier();
ClientSessionInstance().Stop();
});
ExpectNamedAbort(child, "Fatal{RoleViolation, \"gPipeInputs\"}");
}
// The batched arm: with MOBILEGL_IPC_BATCH_WAITS=1 the fill is expected to run while the
// apply thread is inside the applier - that is the batch's whole point, and the disjoint-
// field model (record-supplied fields are never filled; pulled reads stay fenced) is what
// makes it safe.
TEST(RemoteGuards, ClientPipeInputsFillWhileTheApplierOwnsItIsAllowedWhenBatchingIsOn) {
const auto child = RunInChild([] {
MG_Config::Ipc.BatchWaits = 1;
StartControlSession();
ClientSession::NoteApplyThreadEnteredApplier();
MGPipeValidateForVerb(MGPipeVerb::Clear);
ClientSession::NoteApplyThreadLeftApplier();
ClientSessionInstance().Stop();
});
ExpectChildSuccess(child);
}
// The same fill with the flag down is the legal shape - this is the control that keeps the
// check from being "abort unconditionally".
TEST(RemoteGuards, ClientPipeInputsFillWithTheApplierIdleIsAllowed) {
const auto child = RunInChild([] {
StartControlSession();
MGPipeValidateForVerb(MGPipeVerb::Clear);
MGPipeLeaveVerb();
ClientSessionInstance().Stop();
});
ExpectChildSuccess(child);
}
// ===========================================================================================
// P5e (ra) - the wait rule, the present credit and gPipeInputs' ownership
// (docs/archive/contracts/CONTRACT-P5E.md §1, §2.3, §2.4, §3.1, §3.3, §3.5)
// ===========================================================================================
//
// EVERY CASE BELOW ARMS RUN-AHEAD BY PUBLISHING THE CAP BIT, because that is the only thing
// that arms it: RunAheadArmed() is a conjunction whose third term is the SERVER's own
// statement that it applies an unbarriered record without reading client memory, and in a
// tree where kMGPipeP5eRunAheadReady is still false no production session sets it. Driving the
// arm through the caps mirror rather than through a test-only setter is deliberate (ID-102's
// lesson): what these cases exercise is then the production latch and not a branch beside it.
//
// NONE OF THEM DRAWS. The draw path is the family packages' to migrate; until they land, a
// draw under run-ahead would abort inside the backend for a reason that has nothing to do with
// what is being asserted here.
void StartRunAheadSession() {
::alarm(15);
MG_Config::Transport = MG_Config::TransportMode::InProcess;
MG_Config::Ipc.RunAhead = 1;
// The run-ahead guards below are the SHARED-block rules; the dual block (the default since
// P15) makes a barriered fill the client's own and is covered by DualBlockScenario.
MG_Config::Ipc.RoleSplitState = false;
Srv::ServerSessionInstance().SetCapabilityBits(static_cast<Uint64>(MG_Pipe::kCapRunAheadApply));
Srv::ServerSessionInstance().SetConsumedSubsystems(kMGPipeSubsystemsMigratedAtP4a);
// The test peer publishes the first snapshot through the real handshake.
Srv::ServerSessionInstance().SetBackend(ControlCapsPeer());
if (ClientSessionInstance().Start(MG_Config::TransportMode::InProcess, {}) != MOBILEGL_OK) {
::_exit(81);
}
if (!ClientSessionInstance().RunAheadArmed()) ::_exit(83); // the latch never took
}
// THE SERVER HALF OF §2.4, SUBSTITUTED - and it is substituted because this fixture has no
// backend object at all, so ServerVerbSink::OnPresent DECLINES before it can reach
// ServerSession::ReturnPresentCredit ("present arrived with no backend object"). What the
// cases below assert is therefore the CLIENT's half of the credit: that it pays, when, and in
// whose id space. That the server returns exactly one credit per swap, after Present() has
// returned, is PipeApplier::OnPresent's own three lines and the device exit's `credit-waits`
// reading; a peer here cannot pin it without a backend to swap on.
struct PresentCreditPeer : Codec::WireVerbSink {
std::atomic<Uint64> lastSerial{0};
Bool OnPresent(const MGPPresent& present) override {
lastSerial.store(present.FrameSerial);
Srv::ServerSessionInstance().ReturnPresentCredit(present.FrameSerial);
return true;
}
void Install() {
if (Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting(
[](void* self) {
auto& decoder = Srv::ServerSessionInstance().Applier().*PeerMember(DecoderTag{});
decoder.SetVerbSink(static_cast<PresentCreditPeer*>(self));
return MOBILEGL_OK;
},
this) != MOBILEGL_OK) {
::_exit(82);
}
}
};
// §1: the conjunction, and the half of it that is not the client's to decide. The cap bit is
// absent here, so the knob alone changes nothing - which is the whole reason the knob is
// parsed on every arm rather than only where it means something (Config.h).
TEST(RemoteRunAhead, TheKnobAloneDoesNotArmRunAheadWithoutTheServersCapBit) {
const auto child = RunInChild([] {
MG_Config::Ipc.RunAhead = 1;
StartControlSession(); // SetCapabilityBits(0)
if (ClientSessionInstance().RunAheadArmed()) ::_exit(101);
ClientSessionInstance().Stop();
});
ExpectChildSuccess(child);
}
// §1 again, from the other side: MOBILEGL_IPC_VERB_BARRIER=0 is the LOCKSTEP arm's negative
// control and it disarms run-ahead outright, because the barrier is the first term of the
// conjunction. Under run-ahead its documented red is the first barriered row's stale pull -
// that one is a scenario, not a unit case; what is pinned here is that the two controls do not
// silently compose into a third mode nobody designed.
TEST(RemoteRunAhead, ClearingTheVerbBarrierDisarmsRunAheadEvenWithTheCapBit) {
const auto child = RunInChild([] {
MG_Config::Ipc.RunAhead = 1;
MG_Config::Ipc.VerbBarrier = 0;
::alarm(15);
MG_Config::Transport = MG_Config::TransportMode::InProcess;
Srv::ServerSessionInstance().SetCapabilityBits(static_cast<Uint64>(MG_Pipe::kCapRunAheadApply));
Srv::ServerSessionInstance().SetConsumedSubsystems(kMGPipeSubsystemsMigratedAtP4a);
Srv::ServerSessionInstance().SetBackend(ControlCapsPeer());
if (ClientSessionInstance().Start(MG_Config::TransportMode::InProcess, {}) != MOBILEGL_OK) {
::_exit(81);
}
if (ClientSessionInstance().RunAheadArmed()) ::_exit(101);
ClientSessionInstance().Stop();
});
ExpectChildSuccess(child);
}
// §2.4's RED-ONCE, BY COUNT. With a credit of 1 the client may have one present in flight, so
// the second and third presents each pay the credit before they encode - `credit-waits` is 2
// after three presents and the present-ack watermark has reached 2. Delete the
// WaitForPresentAck arm from ClientSession::AcquirePresentCredit and the counter stays 0 while
// all three presents publish: red by count, which is the only way a wait that is usually
// already satisfied can be tested at all.
//
// The serial is asserted too, because the counter alone would survive a credit paid against
// the wrong id space (the pre-P5e FrameSerial was 0 and the server stamped its own).
TEST(RemoteRunAhead, ACreditOneClientPaysTheCreditAtEveryPresentAfterTheFirst) {
const auto child = RunInChild([] {
MG_Config::Ipc.PresentCredit = 1;
StartRunAheadSession();
PresentCreditPeer peer;
peer.Install();
ClientSession& session = ClientSessionInstance();
if (session.PresentCreditWaits() != 0) ::_exit(101);
RemoteEmitTable().Present();
if (session.PresentCreditWaits() != 0) ::_exit(102); // the first one is free
RemoteEmitTable().Present();
RemoteEmitTable().Present();
if (session.PresentCreditWaits() != 2) ::_exit(103);
// ONE CREDIT PER SWAP, in the client's own 1-based space: the server returned the
// second present's credit, so the watermark is at least 2.
if (session.Control() == nullptr || session.Control()->Progress.presentAckSerial.load() < 2u) {
::_exit(104);
}
session.Stop();
});
ExpectChildSuccess(child);
}
// §2.4's other half: with the credit raised the client stops paying until it is that far
// ahead. It is the control that keeps the case above from passing because of an unconditional
// counter rather than because of the credit.
TEST(RemoteRunAhead, ACreditThreeClientPaysNothingForItsFirstThreePresents) {
const auto child = RunInChild([] {
MG_Config::Ipc.PresentCredit = 3;
StartRunAheadSession();
PresentCreditPeer peer;
peer.Install();
ClientSession& session = ClientSessionInstance();
RemoteEmitTable().Present();
RemoteEmitTable().Present();
RemoteEmitTable().Present();
if (session.PresentCreditWaits() != 0) ::_exit(101);
RemoteEmitTable().Present();
if (session.PresentCreditWaits() != 1) ::_exit(102);
session.Stop();
});
ExpectChildSuccess(child);
}
// §3.1 / §3.5's RED-ONCE. `Clear` is a kWaitNone row, so under run-ahead the client publishes
// it and moves on - and therefore does not fill gPipeInputs for it. This case is the GREEN
// half: the validate point runs the tracker walk and the emitters and touches the block not at
// all, so nothing aborts.
//
// THE RED: in MGPipeValidateForVerb change `const Bool fillOwed = barriered;` to `= true` -
// which is exactly "leave one CopyField in an unbarriered fill" - and the guard below it fires
// Fatal{RoleViolation, "gPipeInputs"} on this very call.
TEST(RemoteRunAhead, AnUnbarrieredVerbDoesNotFillPipeInputsAndIsAllowed) {
const auto child = RunInChild([] {
StartRunAheadSession();
MGPipeValidateForVerb(MGPipeVerb::Clear);
MGPipeLeaveVerb();
ClientSessionInstance().Stop();
});
ExpectChildSuccess(child);
}
// §3.5, the aborting arm, driven at the guard itself: a GL-thread touch that is NOT the
// residual fill of a barriered record. Under lockstep this same call is a no-op (the apply
// thread is not inside the applier and MOBILEGL_IPC_BATCH_WAITS is 1); under run-ahead there
// is no such window to be outside of, because the client never parks for an unbarriered
// record - so the answer stops depending on timing and becomes the rule.
TEST(RemoteGuards, ClientPipeInputsTouchOutsideABarrieredFillUnderRunAheadIsFatalByName) {
const auto child = RunInChild([] {
StartRunAheadSession();
ClientSession::RefusePipeInputsTouchWhileApplierOwnsIt("ra-test-surface",
/*isBarrieredFill=*/false);
ClientSessionInstance().Stop();
});
ExpectNamedAbort(child, "Fatal{RoleViolation, \"gPipeInputs\"}");
}
// ... and the control: the barriered fill says so and is let through. Without this the case
// above would pass just as well against "abort unconditionally".
//
// P5e (ra2): THIS CONTROL IS NOW ALSO HALF OF A PAIR, and the half it does NOT state is the
// one that cost the flip its lane. "The applier is not inside" is true here because nothing in
// this child ever entered it; the case below is the same call with that one fact reversed.
TEST(RemoteGuards, ClientPipeInputsTouchInsideABarrieredFillUnderRunAheadIsAllowed) {
const auto child = RunInChild([] {
StartRunAheadSession();
ClientSession::RefusePipeInputsTouchWhileApplierOwnsIt("ra-test-surface",
/*isBarrieredFill=*/true);
ClientSessionInstance().Stop();
});
ExpectChildSuccess(child);
}
// ---- P5e (ra2): THE FLIP'S OWN RED-ONCE, AND THE CLASS THE STRICT LANE CANNOT SEE ----------
//
// The strict lane runs under LOCKSTEP - ApplyOne stamps every record barriered - so nothing
// that happens only once the client stops waiting is visible to it, by construction (ID-132).
// This pair is what measures it instead, and it runs in the ordinary unit lane on any head,
// flip thrown or not, because StartRunAheadSession arms run-ahead itself.
//
// WHAT IT PINS: `isBarrieredFill` is the caller's sentence "this touch is the residual fill of
// a record this thread is about to park behind". That is a claim about the FUTURE. The order at
// the validate point is fill, then emit, then park, so at the instant of the write the apply
// thread is still draining the UNBARRIERED records the client ran ahead of - and the exemption
// that took the claim on trust made this guard unfireable on precisely the class it exists for.
// Measured on the flipped head: the GL thread bumped CurrentVerbSerial, withdrew the server's
// stamp and renamed m_currentVerb underneath a record the applier was inside, and the applier
// aborted with `Fatal{UnmigratedPipeInput, "<field>@<the CLIENT's verb>"}` - a verb no applier
// stamp can produce, which is what identifies the writer.
//
// THE PROBE IS THE GUARD CALL ITSELF (ID-102): the flag is raised by the same raw entry point
// PipeApplier's ScopedApplierEntry uses, on the client thread, BEFORE the call under test - so
// what aborts is this probe and not some object built on the wrong thread.
//
// THE RED: in ClientSession::RefusePipeInputsTouchWhileApplierOwnsIt put the run-ahead arm back
// to `if (isBarrieredFill) return;` and this case stops aborting, while the control above keeps
// passing - which is the difference between a rule and an exemption.
TEST(RemoteGuards, ClientBarrieredFillWhileTheApplierIsInsideUnderRunAheadIsFatalByName) {
const auto child = RunInChild([] {
StartRunAheadSession();
ClientSession::NoteApplyThreadEnteredApplier();
ClientSession::RefusePipeInputsTouchWhileApplierOwnsIt("ra-test-surface",
/*isBarrieredFill=*/true);
ClientSession::NoteApplyThreadLeftApplier();
ClientSessionInstance().Stop();
});
ExpectNamedAbort(child, "Fatal{RoleViolation, \"gPipeInputs\"}");
}
// ---- P5e (ra2): AND THE OTHER HALF - AN INDEXED DRAW IS AN UNBARRIERED VERB ----------------
//
// MGP_VERB_OP_LIST joins the whole draw family through ONE row, `DrawVbo -> DrawArrays`, so
// inverting it verb-first answers kOpCount for DrawElements and for every other indexed /
// instanced / multi / indirect verb. The "unknown verb answers barriered" default then made the
// client FILL for each of them while never parking, because the wait is decided per record and
// the record is a draw_vbo (kWaitNone). On the flipped lane that was 21 of the 69 red entries.
//
// The case states it where it is decidable without a GL context: the guard runs before the
// validate point's null-context return, so a verb that answers UNBARRIERED never reaches the
// guard at all and the child exits 0 even with the applier flag up.
//
// THE RED: delete the kDraw fallback in ClientVerbIsBarriered (PipeFill.cpp) and DrawElements
// answers barriered again - the fill runs, the guard above it sees the raised flag, and this
// case aborts with Fatal{RoleViolation, "gPipeInputs"} instead of exiting 0.
TEST(RemoteRunAhead, AnIndexedDrawVerbIsUnbarrieredAndTouchesPipeInputsNotAtAll) {
const auto child = RunInChild([] {
StartRunAheadSession();
ClientSession::NoteApplyThreadEnteredApplier();
MGPipeValidateForVerb(MGPipeVerb::DrawElements);
ClientSession::NoteApplyThreadLeftApplier();
MGPipeLeaveVerb();
ClientSessionInstance().Stop();
});
ExpectChildSuccess(child);
}
// P15: FIRE-AND-FORGET IS THE DEFAULT. The texture-op copies, mip generation and the transform
// feedback span calls owe the client no answer, so under run-ahead each publishes and returns.
// The wait classes are pinned at compile time (the red: put any of them back to kWaitApplied and
// this file stops compiling); the case below proves the client really does not wait on them.
static_assert(MGPipeWaitClassFor(MGPWireOp::GenerateMipmap) == kWaitNone);
static_assert(MGPipeWaitClassFor(MGPWireOp::ResourceCopyRegion) == kWaitNone);
static_assert(MGPipeWaitClassFor(MGPWireOp::CopyFramebufferToTexture) == kWaitNone);
static_assert(MGPipeWaitClassFor(MGPWireOp::BeginStreamOutput) == kWaitNone);
static_assert(MGPipeWaitClassFor(MGPWireOp::EndStreamOutput) == kWaitNone);
static_assert(MGPipeWaitClassFor(MGPWireOp::PauseStreamOutput) == kWaitNone);
static_assert(MGPipeWaitClassFor(MGPWireOp::ResumeStreamOutput) == kWaitNone);
static_assert(MGPipeWaitClassFor(MGPWireOp::BindStreamOutput) == kWaitNone);
static_assert(MGPipeWaitClassFor(MGPWireOp::SetStorageBlockBinding) == kWaitNone);
struct AcceptAllPeer : Codec::WireVerbSink {
Bool OnGenerateMipmap(const MGPMipPlan&) override { return true; }
Bool OnResourceCopyRegion(const MGPCopyRegion&) override { return true; }
Bool OnCopyFramebufferToTexture(const MGPCopyFromFramebuffer&) override { return true; }
Bool OnBeginStreamOutput(const MGPStreamOutputBegin&) override { return true; }
Bool OnEndStreamOutput(const MGPXfbAccounting&) override { return true; }
Bool OnPauseStreamOutput(const MGPStreamOutputControl&) override { return true; }
Bool OnResumeStreamOutput(const MGPStreamOutputControl&) override { return true; }
Bool OnBindStreamOutput(const MGPStreamOutputBind&) override { return true; }
void Install() {
if (Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting(
[](void* self) {
auto& decoder = Srv::ServerSessionInstance().Applier().*PeerMember(DecoderTag{});
decoder.SetVerbSink(static_cast<AcceptAllPeer*>(self));
return MOBILEGL_OK;
},
this) != MOBILEGL_OK) {
::_exit(82);
}
}
};
TEST(RemoteRunAhead, FireAndForgetRowsDoNotWaitForTheirApply) {
const auto child = RunInChild([] {
StartRunAheadSession();
AcceptAllPeer peer;
peer.Install();
ClientSession& session = ClientSessionInstance();
const Uint64 before = session.RecordWaits();
const auto emit = [&](MGPWireOp op, const auto& payload) {
session.EmitAndWait(op, &payload, sizeof(payload), nullptr, 0, nullptr, 0, nullptr);
};
emit(MGPWireOp::GenerateMipmap, MGPMipPlan{});
emit(MGPWireOp::ResourceCopyRegion, MGPCopyRegion{});
emit(MGPWireOp::CopyFramebufferToTexture, MGPCopyFromFramebuffer{});
emit(MGPWireOp::BindStreamOutput, MGPStreamOutputBind{});
emit(MGPWireOp::BeginStreamOutput, MGPStreamOutputBegin{});
emit(MGPWireOp::PauseStreamOutput, MGPStreamOutputControl{});
emit(MGPWireOp::ResumeStreamOutput, MGPStreamOutputControl{});
emit(MGPWireOp::EndStreamOutput, MGPXfbAccounting{});
if (session.RecordWaits() != before) ::_exit(121);
session.Stop();
});
ExpectChildSuccess(child);
}
// P15: a create is fire-and-forget under run-ahead. The handle is the client's own allocation
// and the server's accept is derivable (it declines only protocol corruption or a family with no
// consumer, which the client never emits for), so the call publishes and returns with a
// provisional accept instead of waiting for the answer.
//
// THE RED (done once while writing it): drop Wire_ResourceCreate's run-ahead fast path and the
// call takes the blocking path, so RecordWaits() moves and the child exits 112.
TEST(RemoteRunAhead, AResourceCreateDoesNotWaitForItsAnswer) {
const auto child = RunInChild([] {
StartRunAheadSession();
ClientSession& session = ClientSessionInstance();
const Uint64 before = session.RecordWaits();
MG_Pipe::MGPResourceDesc desc{};
desc.Resource = MG_Pipe::MGPipeHandle{MG_Pipe::kMGPipeFirstAllocatableSlot, 1};
desc.Target = MG_Pipe::kMGPipeResourceTargetBuffer;
desc.Width = 64;
MG_Pipe::MGPReplySlot reply = MG_Pipe::MGPipeMintReplySlot();
MG_Pipe::gMGPipeScreen.ResourceCreate(&desc, &reply);
if (!MG_Pipe::MGPipeTakeReplyBool(reply, "resource_create")) ::_exit(111);
if (session.RecordWaits() != before) ::_exit(112);
session.Stop();
});
ExpectChildSuccess(child);
}
// P15: the blit family. Every blit reaches the validate point as BlitNamedFramebuffer and
// publishes one `blit` record (kWaitNone, stamped as BlitFramebuffer), so it is unbarriered: no
// fill and no applier quiesce. A barriered answer here was a forced wait once per frame on FCL
// (MC blits its main target to the window every frame).
//
// THE RED (done once while writing it): take the blit rows out of MGP_VERB_RECORD_OP_LIST's
// answer and this case aborts with Fatal{RoleViolation, "gPipeInputs"}.
TEST(RemoteRunAhead, ABlitNamedVerbIsUnbarrieredAndTouchesPipeInputsNotAtAll) {
const auto child = RunInChild([] {
StartRunAheadSession();
ClientSession::NoteApplyThreadEnteredApplier();
MGPipeValidateForVerb(MGPipeVerb::BlitNamedFramebuffer);
ClientSession::NoteApplyThreadLeftApplier();
MGPipeLeaveVerb();
ClientSessionInstance().Stop();
});
ExpectChildSuccess(child);
}
// §3.3's RED-ONCE, and the one that turns the strict lane into a gate. The probe is the
// sticky forward itself, run on the apply thread inside a server-stamped verb with the
// current record marked UNBARRIERED - which is the state the sink puts that thread in for
// every kWaitNone row once the wait rule is live. MOBILEGL_IPC_STRICT_ERRORS is deliberately
// NOT set: the point is that the knob has stopped being the deciding input, because a row the
// client never filled has no value to count.
//
// THE RED: put CountBarrierPull's first line back to `if (MG_Config::Ipc.StrictErrors)` only
// and this case stops aborting - and, on the lane, every scenario that still pulls a row goes
// from a named abort to a wrong picture with a number beside it.
TEST(RemoteGuards, AResidualPullUnderAnUnbarrieredRecordIsFatalWithoutTheStrictKnob) {
const auto child = RunInChild([] {
MG_Config::Ipc.StrictErrors = false;
StartRunAheadSession();
Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting(
+[](void*) -> MobileGLResult {
MG_Pipe::MGPipeServerStampVerbBoundary(MGPipeVerb::Clear);
MG_Pipe::MGPipeApplierSetCurrentRecordBarriered(false);
MG_Pipe::MGPipeStickyForwardPull(MGPipeInputField::GetProgramObject);
return MOBILEGL_OK;
},
nullptr);
ClientSessionInstance().Stop();
});
ExpectNamedAbort(child, "Fatal{UnmigratedPipeInput, \"GetProgramObject@Clear\"}");
}
// P5f retires this formerly admitted read. A barrier can order a record but cannot
// turn a client ProgramObject into a server-owned value. The historical name stays
// registered for G14; both barrier states now have the same named refusal.
TEST(RemoteGuards, AResidualPullUnderABarrieredRecordStillOnlyCounts) {
const auto child = RunInChild([] {
MG_Config::Ipc.StrictErrors = false;
StartRunAheadSession();
Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting(
+[](void*) -> MobileGLResult {
MG_Pipe::MGPipeServerStampVerbBoundary(MGPipeVerb::Clear);
MG_Pipe::MGPipeApplierSetCurrentRecordBarriered(true);
const Uint64 before = MG_Pipe::MGPipeResidualPullCount();
MG_Pipe::MGPipeStickyForwardPull(MGPipeInputField::GetProgramObject);
if (MG_Pipe::MGPipeResidualPullCount() != before + 1) ::_exit(101);
return MOBILEGL_OK;
},
nullptr);
ClientSessionInstance().Stop();
});
ExpectNamedAbort(child, "Fatal{UnmigratedPipeInput, \"GetProgramObject@Clear\"}");
}
// §2.6's RED-ONCE, and the one the brief names: ~300 KiB of kEventGpuWritten published from
// the apply thread behind a sequence nobody waited for. SEG_EVENT is 256 KiB, so this is
// several ringfuls; under P5C's rule the FIRST Reserve that failed was Fatal{EventRingOverflow}
// and this case aborts by that name. With flow control the producer publishes, rings, parks on
// the latch and retries, and the burst completes.
//
// THE DRAIN RUNS ON A THREAD OF ITS OWN, and that is forced by the fixture rather than chosen:
// RunSurfaceControlFrame is synchronous, so the thread that posted the burst cannot also be the
// thread that empties the ring. In production it is the GL thread draining at its own waits
// (§2.6's deadlock argument); here it is a drainer beside the poster, which puts the producer
// in exactly the state that argument describes.
TEST(RemoteRunAhead, AnEventBurstBehindAnUnwaitedSequenceFlowControlsInsteadOfAborting) {
const auto child = RunInChild([] {
StartRunAheadSession();
std::atomic<bool> stop{false};
std::atomic<Uint64> delivered{0};
std::thread drainer([&stop, &delivered] {
// THE FIRST DRAIN IS DELAYED ON PURPOSE. A drainer that starts immediately can
// keep a 256 KiB ring from ever being full, and a red-once that depends on the
// scheduler losing a race is not a red-once. 50 ms is four orders of magnitude
// more than the burst needs to fill the ring, so the producer is provably parked
// on the latch before anything empties it.
std::this_thread::sleep_for(std::chrono::milliseconds(50));
while (!stop.load(std::memory_order_acquire)) {
delivered.fetch_add(ClientSessionInstance().DrainPublishedEvents());
std::this_thread::yield();
}
delivered.fetch_add(ClientSessionInstance().DrainPublishedEvents());
});
// 3000 records of 64 ranges each is ~3 MB against a 256 KiB ring - twelve ringfuls,
// with a drainer racing it - so the full latch is hit many times over rather than
// maybe once. That margin is what makes the red-once (restore the Fatal) reliable:
// a burst that merely might fill the ring would be a red-once that merely might be red.
Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting(
+[](void*) -> MobileGLResult {
MG_Pipe::MGPRange ranges[64];
for (Uint32 r = 0; r < 64; ++r)
ranges[r] = MG_Pipe::MGPRange{r * 64ull, 64ull};
for (Uint32 i = 0; i < 3000; ++i) {
MG_Pipe::gMGPipeCallbacks.OnGpuWritten(MG_Pipe::MGPipeHandle{7, 1}, 64, ranges);
}
return MOBILEGL_OK;
},
nullptr);
stop.store(true, std::memory_order_release);
drainer.join();
// LOSSLESS, which is the property flow control had to preserve: every one of the 3000
// records crossed. A drop policy would have been the other way to survive a full ring,
// and P5C's §4.4 refuses it - a writeback or a GPU-write mark that did not arrive is a
// stale buffer, not a missing statistic.
if (delivered.load() < 3000u) ::_exit(101);
ClientSessionInstance().Stop();
});
ExpectChildSuccess(child);
}
#include <MG_State/EGLState/Core.h>
// THE SERVER WINDOW'S EXTENT AFTER THE CREATION. A server whose window changed size republishes its
// extent - Espryt at the present after the change (RepublishServerWindowExtentIfMoved), Magma when its
// swapchain is rebuilt for it - as one surface-changed event carrying the new size. The client's drain
// must then make eglQuerySurface(EGL_WIDTH/EGL_HEIGHT) answer it for the server-owned window surface
// (what a headless compositor sizes its output from) and reallocate the default framebuffer's
// attachments at it; a format-only event must change no size. Red with the drain's
// ApplyServerOwnedSurfaceExtent call deleted (exit 93: the query keeps 1920x1080) and with
// ApplySurfaceChangedToClient's extent arm deleted (exit 94: the colour stays at the placeholder's).
TEST(RemoteRunAhead, ARepublishedServerWindowExtentResizesTheServerOwnedSurfaceAndTheDefaultFramebuffer) {
const auto child = RunInChild([] {
StartRunAheadSession();
// The client's EGL state with one server-owned window surface, as its creation leaves it.
MG_State::pEGLContext = MakeUnique<MG_State::EGLState::EGLContext>();
const EGLDisplay dpy = MG_State::pEGLContext->GetDisplay(EGL_DEFAULT_DISPLAY);
EGLint major = 0;
EGLint minor = 0;
(void)MG_State::pEGLContext->InitializeDisplay(dpy, &major, &minor);
const EGLint configAttribs[] = {EGL_SURFACE_TYPE, EGL_WINDOW_BIT, EGL_NONE};
EGLConfig config = nullptr;
EGLint count = 0;
(void)MG_State::pEGLContext->ChooseConfig(dpy, configAttribs, &config, 1, &count);
const EGLSurface surface =
MG_State::pEGLContext->CreateServerOwnedWindowSurface(dpy, config, nullptr, 1920, 1080, false);
if (surface == EGL_NO_SURFACE) ::_exit(90);
ClientSessionInstance().NoteServerOwnedWindowSurface(surface, 1920, 1080);
// The default framebuffer's placeholder attachments (MG_Impl::Init's shape).
const auto placeholder = [](TextureInternalFormat format) {
auto texture = MakeShared<MG_State::GLState::TextureObject2D>(0);
texture->SetInternalFormat(format);
texture->AllocateStorage(TextureUploadTarget::Texture2D, 0, {{512, 512, 1}, 0});
return texture;
};
auto& defaultFramebuffer = MG_Impl::GLImpl::FramebufferImpl::pDefaultFramebufferInfo;
defaultFramebuffer = MakeUnique<MG_Impl::GLImpl::FramebufferImpl::DefaultFramebufferInfo>(
MG_Impl::GLImpl::FramebufferImpl::DefaultFramebufferInfo{
nullptr, placeholder(TextureInternalFormat::RGBA8),
placeholder(TextureInternalFormat::Depth32FStencil8),
placeholder(TextureInternalFormat::Depth32FStencil8)});
const auto query = [&](EGLint attribute) {
EGLint value = -1;
(void)MG_State::pEGLContext->QuerySurface(dpy, surface, attribute, &value);
return value;
};
if (query(EGL_WIDTH) != 1920 || query(EGL_HEIGHT) != 1080) ::_exit(91);
// The server's republish, from the apply thread like the backend's own.
static MG_Pipe::MGPSurfaceInfo s_info{};
s_info = MG_Pipe::MGPSurfaceInfo{};
s_info.Width = 2560;
s_info.Height = 1600;
s_info.InternalFormat = static_cast<Uint32>(TextureInternalFormat::Depth24Stencil8);
s_info.Samples = 1;
s_info.Layers = 1;
s_info.IsDefault = 1;
const auto republish = [] {
Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting(
+[](void*) -> MobileGLResult {
MG_Pipe::gMGPipeCallbacks.OnSurfaceChanged(&s_info);
return MOBILEGL_OK;
},
nullptr);
};
republish();
if (ClientSessionInstance().DrainPublishedEvents() < 1u) ::_exit(92);
if (query(EGL_WIDTH) != 2560 || query(EGL_HEIGHT) != 1600) ::_exit(93);
const IntVec3 colour =
static_cast<MG_State::GLState::TextureObject2D*>(defaultFramebuffer->colorAttachment.get())
->GetMipmapTexelSize(TextureUploadTarget::Texture2D, 0);
const IntVec3 depth =
static_cast<MG_State::GLState::TextureObject2D*>(defaultFramebuffer->depthAttachment.get())
->GetMipmapTexelSize(TextureUploadTarget::Texture2D, 0);
if (colour.x() != 2560 || colour.y() != 1600 || depth.x() != 2560 || depth.y() != 1600) ::_exit(94);
// A format-only event (a pbuffer's, a client-named window's) says nothing about the size.
s_info.Width = 0;
s_info.Height = 0;
republish();
if (ClientSessionInstance().DrainPublishedEvents() < 1u) ::_exit(95);
if (query(EGL_WIDTH) != 2560 || query(EGL_HEIGHT) != 1600) ::_exit(96);
// AN APPLICATION THAT POLLS THE SIZE WHILE IT DRAWS NOTHING (the compositor's timer): the next
// resize is published and nothing drains it - no verb, no make-current - but eglQuerySurface
// itself takes it in. Red with BackendObject_Remote::RefreshSurfaceExtent a no-op (exit 97).
MG_Backend::pActiveBackendObject = MakeUnique<BackendObject_Remote>();
s_info.Width = 2560;
s_info.Height = 1412;
republish();
EGLint polledWidth = -1;
EGLint polledHeight = -1;
if (MG_Impl::EGLImpl::QuerySurface(dpy, surface, EGL_WIDTH, &polledWidth) != EGL_TRUE ||
MG_Impl::EGLImpl::QuerySurface(dpy, surface, EGL_HEIGHT, &polledHeight) != EGL_TRUE ||
polledWidth != 2560 || polledHeight != 1412)
::_exit(97);
MG_Backend::pActiveBackendObject.reset();
defaultFramebuffer.reset();
ClientSessionInstance().ForgetServerOwnedWindowSurface(surface);
MG_State::pEGLContext.reset();
ClientSessionInstance().Stop();
});
ExpectChildSuccess(child);
}
// ===========================================================================================
// PH-6 (ID-P7-2): a client that does not drain SEG_EVENT FORFEITS the reverse channel
// ===========================================================================================
//
// The flow-control case above is the healthy half: a client that drains late gets every event.
// These two are the other half, and they are why ReserveEventOrBlock's two BUSY refusals became
// a latch. Each child reads its OWN outcome - the latch, the server's drop tally, the shared
// page's eventDropped the client sees, the time spent against MOBILEGL_IPC_EVENT_WAIT_MS, and the
// apply thread leaving by itself - and exits non-zero on the first that is wrong; the parent then
// asserts the named line and the ABSENCE of Fatal{EventRingOverflow}.
namespace {
// Waits for mgl-srv-apply to leave on its own: the forfeit's stop is the ordinary one, so
// nobody calls Stop() to get it out.
bool ApplyThreadLeavesWithin(std::chrono::milliseconds budget) {
const auto until = std::chrono::steady_clock::now() + budget;
while (std::chrono::steady_clock::now() < until) {
if (!Srv::ServerLoopInstance().Running()) return true;
std::this_thread::sleep_for(std::chrono::milliseconds(5));
}
return false;
}
Uint64 MillisecondsSince(std::chrono::steady_clock::time_point start) {
return static_cast<Uint64>(
std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - start).count());
}
} // namespace
// STONEWALL: the client never drains. 3000 GPU-write marks of ~1 KiB against a 256 KiB ring:
// ~250 fit, the next one waits MOBILEGL_IPC_EVENT_WAIT_MS (300 ms here) and forfeits, and the
// remaining ~2750 are drops that cost no wait at all - so the whole burst returns in a little over
// the knob, not in 30 s and not with an abort.
//
// THE RED (recorded in the F2 package note): put ReserveEventOrBlock back to its two 30000 ms
// rounds and this child is killed by StartRunAheadSession's alarm(15) inside the first wait -
// before that change it would have aborted at 30 s with Fatal{EventRingOverflow} "waited 30000 ms".
TEST(RemoteRunAhead, AStonewallingClientForfeitsTheReverseChannelWithinTheWaitKnob) {
const auto child = RunInChild([] {
MG_Config::Ipc.EventWaitMs = 300;
StartRunAheadSession();
const auto start = std::chrono::steady_clock::now();
Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting(
+[](void*) -> MobileGLResult {
MG_Pipe::MGPRange ranges[64];
for (Uint32 r = 0; r < 64; ++r)
ranges[r] = MG_Pipe::MGPRange{r * 64ull, 64ull};
for (Uint32 i = 0; i < 3000; ++i) {
MG_Pipe::gMGPipeCallbacks.OnGpuWritten(MG_Pipe::MGPipeHandle{7, 1}, 64, ranges);
}
return MOBILEGL_OK;
},
nullptr);
const Uint64 elapsedMs = MillisecondsSince(start);
auto& server = Srv::ServerSessionInstance();
if (!server.ReverseChannelForfeited()) ::_exit(101);
const Uint64 drops = server.ForfeitDrops();
// Some fitted (the ring was empty) and some did not (it holds ~250 of 3000).
if (drops == 0 || drops >= 3000u) ::_exit(102);
// The shared page's counter is the one a client can read; the server's own tally is the
// one a peer cannot write. They agree when nothing else counts.
if (ClientSessionInstance().Events().DroppedEvents() != drops) ::_exit(103);
// The knob, spent once: at least most of it, and nowhere near the old 30 s.
if (elapsedMs < 250u || elapsedMs > 5000u) ::_exit(104);
if (!ApplyThreadLeavesWithin(std::chrono::milliseconds(3000))) ::_exit(105);
ClientSessionInstance().Stop();
});
ExpectChildSuccess(child);
EXPECT_NE(child.Log.find("ReverseChannelForfeit{NotDraining} - kEventGpuWritten"), std::string::npos) << child.Log;
EXPECT_NE(child.Log.find("mgl-srv-apply stops on ReverseChannelForfeit"), std::string::npos) << child.Log;
EXPECT_EQ(child.Log.find("Fatal{EventRingOverflow"), std::string::npos) << child.Log;
}
// TRICKLE: the client drains, one record per 50 ms, and the record the server is waiting to place
// is twenty times bigger than what each drain frees. Every drain clears the latch and rings the
// server; every retry still does not fit. The old shape gave up after TWO such rounds and called
// it "a drained ring that still refuses a record it fits is a corrupt cursor set" - ~100 ms into a
// session whose only fault was a slow reader, and a Fatal. Now the rounds share one deadline:
// the server keeps retrying for the whole knob (600 ms here) and then forfeits by name, TooSlow,
// with the short drains counted.
//
// THE RED (recorded in the F2 package note): the old two-round ReserveEventOrBlock aborts this
// child with Fatal{EventRingOverflow} "could not reserve ... on an emptied SEG_EVENT".
TEST(RemoteRunAhead, ATricklingClientGetsTheWholeWaitThenForfeitsByNameInsteadOfAborting) {
const auto child = RunInChild([] {
MG_Config::Ipc.EventWaitMs = 600;
StartRunAheadSession();
std::atomic<bool> stop{false};
std::atomic<Uint32> popped{0};
std::thread trickler([&stop, &popped] {
Transport::EventRingConsumer& events = ClientSessionInstance().Events();
while (!stop.load(std::memory_order_acquire)) {
std::this_thread::sleep_for(std::chrono::milliseconds(50));
Transport::RingRecordView view;
if (events.Pop(view)) popped.fetch_add(1);
// Releases what was popped, clears the latch and rings the server: a real drain,
// just a stingy one.
events.Drained();
}
});
const auto start = std::chrono::steady_clock::now();
Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting(
+[](void*) -> MobileGLResult {
// Fill with ONE-range marks (48 bytes a record) until under 4 KiB is left, without
// ever blocking - so the fill is not paced by the trickle - then ask for 64-range
// marks (~1 KiB each). The first of those that does not fit is the one the server
// waits for, and one popped 48-byte record never makes room for it.
auto& ring = Srv::ServerSessionInstance().Events().Ring();
const MG_Pipe::MGPRange one{0, 64};
while (ring.FreeBytes() > 4096u) {
MG_Pipe::gMGPipeCallbacks.OnGpuWritten(MG_Pipe::MGPipeHandle{7, 1}, 1, &one);
}
MG_Pipe::MGPRange ranges[64];
for (Uint32 r = 0; r < 64; ++r)
ranges[r] = MG_Pipe::MGPRange{r * 64ull, 64ull};
for (Uint32 i = 0; i < 16; ++i) {
MG_Pipe::gMGPipeCallbacks.OnGpuWritten(MG_Pipe::MGPipeHandle{7, 1}, 64, ranges);
}
return MOBILEGL_OK;
},
nullptr);
const Uint64 elapsedMs = MillisecondsSince(start);
stop.store(true, std::memory_order_release);
trickler.join();
auto& server = Srv::ServerSessionInstance();
if (!server.ReverseChannelForfeited()) ::_exit(101);
if (server.ForfeitDrops() == 0) ::_exit(102);
// The trickle really happened while the server waited - otherwise this is the stonewall
// case under another name.
if (popped.load() < 2u) ::_exit(103);
// The WHOLE budget was spent retrying, not two rounds of it.
if (elapsedMs < 540u || elapsedMs > 5000u) ::_exit(104);
if (!ApplyThreadLeavesWithin(std::chrono::milliseconds(3000))) ::_exit(105);
ClientSessionInstance().Stop();
});
ExpectChildSuccess(child);
EXPECT_NE(child.Log.find("ReverseChannelForfeit{TooSlow} - kEventGpuWritten"), std::string::npos) << child.Log;
EXPECT_EQ(child.Log.find("Fatal{EventRingOverflow"), std::string::npos) << child.Log;
}
// STOP WHILE THE SERVER WAITS (PH-6 fix round). ServerLoop::Stop() arrives while the apply thread is
// parked inside the reservation, on a bell that is still ALIVE - the spawn session's shape when its
// control stream ends (EOF on a half-close, a malformed frame, a LogFlush ack) while the client is
// not draining. The knob is 20 s, four times Stop()'s 5000 ms bounded join, so this child can only
// exit cleanly if the wait ends on the stop request itself and forfeits as `Stopped`.
//
// THE RED: take ApplyStopRequested() out of ReserveEventOrBlock's predicate and the ring Stop()
// sends is swallowed by a wait that re-tests only "has the client drained?"; the reservation sits
// out its 20 s and Stop() aborts the child after 5 s with Fatal{ApplyThreadJoinTimeout}.
TEST(RemoteRunAhead, AStopWhileTheServerWaitsForADrainEndsTheWaitByNameInsteadOfTheJoinTimeout) {
const auto child = RunInChild([] {
MG_Config::Ipc.EventWaitMs = 20000;
StartRunAheadSession();
// The producer's probe blocks inside the reservation, so it is posted from a thread of its
// own: RunProbeOnApplyThreadForTesting waits for the probe to return.
std::thread producer([] {
(void)Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting(
+[](void*) -> MobileGLResult {
MG_Pipe::MGPRange ranges[64];
for (Uint32 r = 0; r < 64; ++r)
ranges[r] = MG_Pipe::MGPRange{r * 64ull, 64ull};
for (Uint32 i = 0; i < 3000; ++i) {
MG_Pipe::gMGPipeCallbacks.OnGpuWritten(MG_Pipe::MGPipeHandle{7, 1}, 64, ranges);
}
return MOBILEGL_OK;
},
nullptr);
});
// The reservation is waiting once SEG_EVENT's latch is up: nothing in this child drains.
const auto signals = Srv::ServerSessionInstance().DataLink()->Signals();
const auto until = std::chrono::steady_clock::now() + std::chrono::milliseconds(5000);
while (signals.EventRingFull->load(std::memory_order_acquire) == 0) {
if (std::chrono::steady_clock::now() > until) ::_exit(106);
std::this_thread::sleep_for(std::chrono::milliseconds(5));
}
std::this_thread::sleep_for(std::chrono::milliseconds(200)); // well into the park
const auto start = std::chrono::steady_clock::now();
Srv::ServerLoopInstance().Stop();
const Uint64 stopMs = MillisecondsSince(start);
producer.join();
auto& server = Srv::ServerSessionInstance();
if (!server.ReverseChannelForfeited()) ::_exit(101);
if (server.ForfeitDrops() == 0) ::_exit(102);
// Stop() came back on the request, not on the join's 5000 ms or the knob's 20 s.
if (stopMs > 2000u) ::_exit(104);
ClientSessionInstance().Stop();
});
ExpectChildSuccess(child);
EXPECT_NE(child.Log.find("ReverseChannelForfeit{Stopped} - kEventGpuWritten"), std::string::npos) << child.Log;
EXPECT_EQ(child.Log.find("Fatal{ApplyThreadJoinTimeout"), std::string::npos) << child.Log;
EXPECT_EQ(child.Log.find("Fatal{EventRingOverflow"), std::string::npos) << child.Log;
}
// §2.7 / ruling 13: the deferred-destroy queue stays, and an enqueue from an unbarriered
// apply is the finding. Under strict it is the abort; this is the arm the lane owns.
TEST(RemoteGuards, ADeferredDestroyFromAnUnbarrieredApplyIsFatalUnderStrict) {
const auto child = RunInChild([] {
MG_Config::Ipc.StrictErrors = true;
StartRunAheadSession();
Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting(
+[](void*) -> MobileGLResult {
MG_Pipe::MGPipeApplierSetCurrentRecordBarriered(false);
(void)MG_Pipe::MGPipeDeferDestroyAndFreeIfOnApplyThread(MG_Pipe::MGPipeKind::Texture, 4242);
return MOBILEGL_OK;
},
nullptr);
ClientSessionInstance().Stop();
});
ExpectNamedAbort(child, "Fatal{RoleViolation, \"deferred-destroy\"}");
}
// The control, and the whole of ruling 13: a BARRIERED apply may still be a last owner (its
// fill's O-class rows, XFB's pinned targets), so the queue takes the death and the GL thread
// replays it. Deleting the queue would have made this an allocator touch from the server role.
TEST(RemoteGuards, ADeferredDestroyFromABarrieredApplyIsStillServed) {
const auto child = RunInChild([] {
MG_Config::Ipc.StrictErrors = true;
StartRunAheadSession();
Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting(
+[](void*) -> MobileGLResult {
MG_Pipe::MGPipeApplierSetCurrentRecordBarriered(true);
if (!MG_Pipe::MGPipeDeferDestroyAndFreeIfOnApplyThread(MG_Pipe::MGPipeKind::Texture, 4243)) {
::_exit(101);
}
return MOBILEGL_OK;
},
nullptr);
MG_Pipe::MGPipeDrainDeferredDestroys();
ClientSessionInstance().Stop();
});
ExpectChildSuccess(child);
}
#endif
// §2.5 / ruling 15 (ID-93): which half of resource_subdata wants its answer. One predicate,
// read by MGPipeRouteResourceSubData and by Wire_ResourceSubData - a second spelling of it is
// how the client comes to wait for an answer the emitter told the server not to bother with.
// The buffer target's whole packed field is 0 (MGPipeTypes.h asserts it beside the packer),
// which is what makes the test a comparison and not a mask.
TEST(RemoteRunAhead, OnlyTheTextureHalfOfResourceSubDataWantsItsReply) {
MG_Pipe::MGPSubData buffer{};
buffer.Target = MG_Pipe::MGPipePackSubDataTarget(MG_Pipe::kMGPipeResourceTargetBuffer, 0u);
EXPECT_FALSE(MG_Pipe::MGPipeSubDataWantsItsReply(buffer));
MG_Pipe::MGPSubData texture{};
texture.Target = MG_Pipe::MGPipePackSubDataTarget(static_cast<Uint32>(MG_Pipe::MGPipeResourceTarget::Tex2D),
static_cast<Uint32>(TextureUploadTarget::Texture2D));
EXPECT_TRUE(MG_Pipe::MGPipeSubDataWantsItsReply(texture));
}
#include "RemoteClientE1Controls.inc"
int main(int argc, char** argv) {
namespace fs = std::filesystem;
const fs::path path =
fs::temp_directory_path() / ("mobilegl-remoteclient-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
#if MGTEST_HAVE_FORK
// Explicit GPU control, kept out of the CPU-only unit label. No silent skip is allowed.
if (argc == 2 && std::string(argv[1]).starts_with("--c1f-pack-gpu=")) {
const bool split = std::string(argv[1]) == "--c1f-pack-gpu=inproc";
const int rc = RunPackGpuControl(split);
fs::remove(path, ec);
return rc;
}
#endif
::testing::InitGoogleTest(&argc, argv);
const int rc = RUN_ALL_TESTS();
fs::remove(path, ec);
return rc;
}