mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
1119 lines
60 KiB
C++
1119 lines
60 KiB
C++
// MobileGL - MobileGL/MG_Test/Pipe/FieldOwnershipTest.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
|
|
|
|
// TABLE 2 at runtime (CONTRACT-P5.md section 3, BRIEF-P5 R-7, exit gate E4). Package p1.
|
|
//
|
|
// The generated table is checked two ways here, and they answer different questions:
|
|
//
|
|
// the ARITHMETIC cases run in any push build and say the table PARTITIONS the field set -
|
|
// 70 rows, four classes, every BARRIER-PULLED row naming the phase that retires it;
|
|
//
|
|
// the BEHAVIOUR cases run only in a split build and say the table is LOAD-BEARING - that a
|
|
// server verb stamp makes the record-supplied fields readable and withdraws the rest, that a
|
|
// retired pointer/forward reads are unconditionally named Fatal on the server, and that
|
|
// monolith storage remains readable without ever borrowing a server stamp.
|
|
//
|
|
// E4's negative control is ARecordSuppliedFieldIsReadableAfterAServerStamp: move one field
|
|
// from RECORD-SUPPLIED to FATAL in MG_Pipe/FieldOwnership.def and that case goes red by name.
|
|
// The generator's own controls are `gen_pipe_field_ownership.py --self-test`.
|
|
//
|
|
// Links gtest rather than gtest_main and carries its own main(), like PipeInputsTest: the
|
|
// abort cases read the Fatal line back out of a log file this process names before anything
|
|
// logs.
|
|
|
|
#include <gtest/gtest.h>
|
|
#include <MG_Util/Debug/Log.h>
|
|
|
|
#include <cctype>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <sstream>
|
|
#include <string>
|
|
|
|
#include "Includes.h"
|
|
#include <MG_Pipe/MGPipe.h>
|
|
|
|
#if MOBILEGL_PIPE_PUSH
|
|
#include <Config.h>
|
|
#include <MG_Backend/MGPipe/PipeInputs.h>
|
|
#include <MG_Impl/Pipe/PipeFill.h>
|
|
#include <MG_Pipe/PipeApply.h>
|
|
#include <MG_State/GLState/Core.h>
|
|
#include <MG_Util/Metrics/PipeStats.h>
|
|
#endif
|
|
|
|
#if !defined(_WIN32)
|
|
#include <csignal>
|
|
#include <sys/wait.h>
|
|
#include <unistd.h>
|
|
#define MGTEST_HAVE_FORK 1
|
|
#else
|
|
#include <process.h>
|
|
#define MGTEST_HAVE_FORK 0
|
|
#endif
|
|
|
|
using namespace MobileGL;
|
|
using namespace MobileGL::MG_Pipe;
|
|
|
|
namespace {
|
|
std::string g_logPath;
|
|
|
|
std::string ReadLog() {
|
|
// BOTH ROLES' LOGS (P6). A death test asserts that the CHILD said something; which
|
|
// role's thread said it is not what these cases are about, and refusals raised on the
|
|
// apply thread are written under the SERVER role by construction.
|
|
return MobileGL::MG_Util::Debug::ReadRoleLogs(g_logPath.c_str());
|
|
}
|
|
|
|
long ProcessId() {
|
|
#if defined(_WIN32)
|
|
return static_cast<long>(::_getpid());
|
|
#else
|
|
return static_cast<long>(::getpid());
|
|
#endif
|
|
}
|
|
|
|
#if MOBILEGL_PIPE_PUSH
|
|
using GLContext = MG_State::GLState::GLContext;
|
|
|
|
// A live frontend context, restored on the way out so the cases stay independent - the
|
|
// sticky forwards reach for one and the stamp cases must not depend on whether they found
|
|
// it (PipeInputsTest's idiom).
|
|
class FieldOwnershipTest : public ::testing::Test {
|
|
protected:
|
|
void SetUp() override {
|
|
m_previous = Move(MG_State::pGLContext);
|
|
MG_State::pGLContext = MakeUnique<GLContext>();
|
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
|
MG_Config::Ipc.StrictErrors = false;
|
|
MGPipeServerClearVerbBoundary();
|
|
MGPipeResetResidualPullCountForTesting();
|
|
// P5e (gl), ID-128: the escalation flag is per-thread state PipeApplier::ApplyOne
|
|
// stamps on every record, so a case that set it would otherwise hand its answer to
|
|
// the next one - and the arm it selects is "admitted", so the leak is silent.
|
|
MGPipeApplierSetCurrentRecordBarrieredByEscalation(false);
|
|
#endif
|
|
}
|
|
void TearDown() override {
|
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
|
MGPipeServerClearVerbBoundary();
|
|
MGPipeApplierSetCurrentRecordBarrieredByEscalation(false);
|
|
MG_Config::Ipc.StrictErrors = false;
|
|
#endif
|
|
MG_State::pGLContext = Move(m_previous);
|
|
}
|
|
UniquePtr<GLContext> m_previous;
|
|
};
|
|
|
|
SizeT Index(MGPipeInputField field) { return static_cast<SizeT>(field); }
|
|
|
|
#if MGTEST_HAVE_FORK
|
|
struct ChildResult {
|
|
int Status = -1;
|
|
std::string Log;
|
|
};
|
|
|
|
// Runs `body` in a forked child and returns its wait status and log delta. The child must
|
|
// not use gtest assertions; it _exit(0)s when `body` returns, so a body expected to die is
|
|
// asserted dead by the parent rather than assumed dead.
|
|
template <class Body>
|
|
ChildResult RunInChild(Body body) {
|
|
ChildResult result;
|
|
const std::string before = ReadLog();
|
|
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().substr(before.size());
|
|
return result;
|
|
}
|
|
|
|
Bool DiedOfAbort(const ChildResult& r) { return WIFSIGNALED(r.Status) && WTERMSIG(r.Status) == SIGABRT; }
|
|
Bool ExitedWith(const ChildResult& r, int code) { return WIFEXITED(r.Status) && WEXITSTATUS(r.Status) == code; }
|
|
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 // MGTEST_HAVE_FORK
|
|
#endif // MOBILEGL_PIPE_PUSH
|
|
} // namespace
|
|
|
|
#if !MOBILEGL_PIPE_PUSH
|
|
|
|
TEST(FieldOwnershipTest, EveryFieldAndForwardIsInExactlyOneClass) {
|
|
GTEST_SKIP() << "push not compiled in (MOBILEGL_PIPE_PUSH=OFF)";
|
|
}
|
|
TEST(FieldOwnershipTest, TheClassSizesPartitionTheFieldSet) {
|
|
GTEST_SKIP() << "push not compiled in (MOBILEGL_PIPE_PUSH=OFF)";
|
|
}
|
|
TEST(FieldOwnershipTest, EveryBarrierPulledRowNamesTheRetiringPhase) {
|
|
GTEST_SKIP() << "push not compiled in (MOBILEGL_PIPE_PUSH=OFF)";
|
|
}
|
|
TEST(FieldOwnershipTest, TheReducedPathsUnmigratedFieldsAreAllAccountedFor) {
|
|
GTEST_SKIP() << "push not compiled in (MOBILEGL_PIPE_PUSH=OFF)";
|
|
}
|
|
TEST(FieldOwnershipTest, TheSevenStickyForwardsAgreeWithTheirFieldRows) {
|
|
GTEST_SKIP() << "push not compiled in (MOBILEGL_PIPE_PUSH=OFF)";
|
|
}
|
|
TEST(FieldOwnershipTest, VerbBoundaryOpsCoverEveryVerbShapedCall) {
|
|
GTEST_SKIP() << "push not compiled in (MOBILEGL_PIPE_PUSH=OFF)";
|
|
}
|
|
TEST(FieldOwnershipTest, TheAdmittedPullTableIsID84sDerivationAndNotAList) {
|
|
GTEST_SKIP() << "push not compiled in (MOBILEGL_PIPE_PUSH=OFF)";
|
|
}
|
|
TEST(FieldOwnershipTest, TheResidualFillsSuppliedMemoReKeysOnEveryInputThatMovesAnAnswer) {
|
|
GTEST_SKIP() << "push not compiled in (MOBILEGL_PIPE_PUSH=OFF)";
|
|
}
|
|
|
|
#else // MOBILEGL_PIPE_PUSH
|
|
|
|
// ---------------------------------------------------------------------------------------
|
|
// The arithmetic: the table partitions the field set. Runs in push, verify and split.
|
|
// ---------------------------------------------------------------------------------------
|
|
|
|
TEST_F(FieldOwnershipTest, EveryFieldAndForwardIsInExactlyOneClass) {
|
|
for (SizeT i = 0; i < kMGPipeInputFieldCount; ++i) {
|
|
EXPECT_NE(kMGPipeFieldOwnership[i], MGPipeFieldOwnership::kUnclassified)
|
|
<< kMGPipeInputFieldNames[i] << " is in none of the four ownership classes";
|
|
}
|
|
for (SizeT i = 0; i < kMGPipeFieldOwnershipForwardCount; ++i) {
|
|
EXPECT_NE(kMGPipeFieldOwnershipForward[i], MGPipeFieldOwnership::kUnclassified)
|
|
<< "sticky forward " << i << " is in none of the four ownership classes";
|
|
}
|
|
EXPECT_EQ(kMGPipeFieldOwnershipRowCount, SizeT{70});
|
|
}
|
|
|
|
TEST_F(FieldOwnershipTest, TheClassSizesPartitionTheFieldSet) {
|
|
SizeT counted[5] = {};
|
|
for (SizeT i = 0; i < kMGPipeInputFieldCount; ++i) {
|
|
++counted[static_cast<SizeT>(kMGPipeFieldOwnership[i])];
|
|
}
|
|
EXPECT_EQ(counted[static_cast<SizeT>(MGPipeFieldOwnership::kRecordSupplied)],
|
|
kMGPipeRecordSuppliedFieldCount);
|
|
EXPECT_EQ(counted[static_cast<SizeT>(MGPipeFieldOwnership::kApplierDerived)],
|
|
kMGPipeApplierDerivedFieldCount);
|
|
EXPECT_EQ(counted[static_cast<SizeT>(MGPipeFieldOwnership::kBarrierPulled)],
|
|
kMGPipeBarrierPulledFieldCount);
|
|
EXPECT_EQ(counted[static_cast<SizeT>(MGPipeFieldOwnership::kFatal)], kMGPipeFatalFieldCount);
|
|
// The census's own arithmetic (scout-unmigrated-census section 2.1), updated by P5c rv
|
|
// (CONTRACT-P5C.md §5.3): rv moved the NINE value-class rows to RECORD-SUPPLIED through
|
|
// set_context_values / the amended set_vertex_attrib_defaults and the three texture
|
|
// shutters to APPLIER-DERIVED, so 22 of the 63 fields are served by NO pushed record -
|
|
// 15 BARRIER-PULLED (the object class: nine non-sticky rows plus six of the seven sticky
|
|
// forwards), four APPLIER-DERIVED and three FATAL - and 41 are.
|
|
EXPECT_EQ(kMGPipeRecordSuppliedFieldCount, SizeT{41});
|
|
EXPECT_EQ(kMGPipeApplierDerivedFieldCount + kMGPipeBarrierPulledFieldCount + kMGPipeFatalFieldCount,
|
|
SizeT{22});
|
|
}
|
|
|
|
TEST_F(FieldOwnershipTest, EveryBarrierPulledRowNamesTheRetiringPhase) {
|
|
SizeT pulled = 0;
|
|
for (SizeT i = 0; i < kMGPipeInputFieldCount; ++i) {
|
|
const Bool isPulled = kMGPipeFieldOwnership[i] == MGPipeFieldOwnership::kBarrierPulled;
|
|
const Bool named = std::string(kMGPipeFieldRetiringPhase[i]) != "-";
|
|
EXPECT_EQ(isPulled, named) << kMGPipeInputFieldNames[i]
|
|
<< ": only a BARRIER-PULLED row has a retiring phase, and it must have one";
|
|
pulled += isPulled ? 1 : 0;
|
|
}
|
|
EXPECT_EQ(pulled, kMGPipeBarrierPulledFieldCount);
|
|
EXPECT_EQ(pulled, SizeT{0}) << "P5f exit forbids reintroducing a residual pull";
|
|
}
|
|
|
|
// P5f terminal contract; the historical registration name is retained for G14.
|
|
TEST_F(FieldOwnershipTest, TheReducedPathsUnmigratedFieldsAreAllAccountedFor) {
|
|
// Historical name retained for G14. These are retired legacy getters, not records.
|
|
const MGPipeInputField retired[] = {
|
|
MGPipeInputField::GetBoundVertexArray, MGPipeInputField::GetBufferBindingSlot,
|
|
MGPipeInputField::GetBufferBindingPoint, MGPipeInputField::GetFramebufferBindingSlot,
|
|
MGPipeInputField::GetImageTextureBinding, MGPipeInputField::GetTextureUnitObject,
|
|
MGPipeInputField::GetProgramForDraw, MGPipeInputField::GetProgramForDispatch,
|
|
MGPipeInputField::GetTransformFeedbackProgram, MGPipeInputField::GetProgramObject,
|
|
MGPipeInputField::GetTextureObject, MGPipeInputField::ValidateProgramName,
|
|
MGPipeInputField::RecordError,
|
|
};
|
|
for (const auto field : retired)
|
|
EXPECT_EQ(MGPipeFieldOwnershipOf(field), MGPipeFieldOwnership::kFatal)
|
|
<< kMGPipeInputFieldNames[Index(field)] << " revived a client-memory accessor";
|
|
EXPECT_EQ(kMGPipeBarrierPulledFieldCount, SizeT{0});
|
|
EXPECT_EQ(kMGPipeAdmittedPullPairCount, SizeT{0});
|
|
EXPECT_EQ(MGPipeFieldOwnershipOf(MGPipeInputField::GetPixelStoreParameters, 0),
|
|
MGPipeFieldOwnership::kApplierDerived);
|
|
EXPECT_EQ(MGPipeFieldOwnershipOf(MGPipeInputField::GetPixelStoreParameters, 1),
|
|
MGPipeFieldOwnership::kFatal);
|
|
for (const auto field : {MGPipeInputField::GetBufferBindingPointCount,
|
|
MGPipeInputField::HasOpenTransformFeedbackSpan,
|
|
MGPipeInputField::GetTextureBindGeneration,
|
|
MGPipeInputField::GetSamplingResolutionGeneration,
|
|
MGPipeInputField::GetTextureContextId})
|
|
EXPECT_EQ(MGPipeFieldOwnershipOf(field), MGPipeFieldOwnership::kApplierDerived);
|
|
for (const auto field : {MGPipeInputField::GetActiveTextureUnit,
|
|
MGPipeInputField::GetMaxTouchedTextureUnit,
|
|
MGPipeInputField::GetTouchedBufferBindingPointCount,
|
|
MGPipeInputField::IsTransformFeedbackActive,
|
|
MGPipeInputField::IsTransformFeedbackPaused,
|
|
MGPipeInputField::GetTransformFeedbackGeneration,
|
|
MGPipeInputField::GetBoundTransformFeedbackLifetimeId,
|
|
MGPipeInputField::GetTransformFeedbackCapturedVertices,
|
|
MGPipeInputField::GetCurrentVertexAttribute})
|
|
EXPECT_EQ(MGPipeFieldOwnershipOf(field), MGPipeFieldOwnership::kRecordSupplied);
|
|
}
|
|
|
|
TEST_F(FieldOwnershipTest, TheSevenStickyForwardsAgreeWithTheirFieldRows) {
|
|
ASSERT_EQ(kMGPipeFieldOwnershipForwardCount, kMGPipeInputStickyFieldCount);
|
|
for (SizeT i = 0; i < kMGPipeFieldOwnershipForwardCount; ++i) {
|
|
const MGPipeInputField field = kMGPipeFieldOwnershipForwardField[i];
|
|
EXPECT_TRUE(kMGPipeInputFieldSticky[Index(field)])
|
|
<< kMGPipeInputFieldNames[Index(field)] << " has a forward row but is not sticky";
|
|
EXPECT_EQ(kMGPipeFieldOwnershipForward[i], MGPipeFieldOwnershipOf(field));
|
|
EXPECT_STRNE(kMGPipeFieldOwnershipForwardMechanism[i], "");
|
|
}
|
|
}
|
|
|
|
// Pin every boundary's representative verb, the complete count, and the three
|
|
// verb-shaped calls that are exempt by name.
|
|
TEST_F(FieldOwnershipTest, VerbBoundaryOpsCoverEveryVerbShapedCall) {
|
|
// CONTRACT §7 class B minus Present - the only four that can arrive in P5.
|
|
EXPECT_EQ(MGPipeVerbForWireOp(MGPWireOp::Clear), MGPipeVerb::Clear);
|
|
EXPECT_EQ(MGPipeVerbForWireOp(MGPWireOp::DrawVbo), MGPipeVerb::DrawArrays);
|
|
EXPECT_EQ(MGPipeVerbForWireOp(MGPWireOp::ReadPixels), MGPipeVerb::ReadPixels);
|
|
EXPECT_EQ(MGPipeVerbForWireOp(MGPWireOp::Blit), MGPipeVerb::BlitFramebuffer);
|
|
// Class C today, mapped anyway: an OMITTED stamp row is silent, because the record would
|
|
// apply under the previous verb's serial, mask and name.
|
|
EXPECT_EQ(MGPipeVerbForWireOp(MGPWireOp::LaunchGrid), MGPipeVerb::DispatchCompute);
|
|
EXPECT_EQ(MGPipeVerbForWireOp(MGPWireOp::MemoryBarrier), MGPipeVerb::MemoryBarrier);
|
|
EXPECT_EQ(MGPipeVerbForWireOp(MGPWireOp::BeginStreamOutput), MGPipeVerb::BeginTransformFeedback);
|
|
EXPECT_EQ(MGPipeVerbForWireOp(MGPWireOp::EndStreamOutput), MGPipeVerb::EndTransformFeedback);
|
|
EXPECT_EQ(MGPipeVerbForWireOp(MGPWireOp::PauseStreamOutput), MGPipeVerb::PauseTransformFeedback);
|
|
EXPECT_EQ(MGPipeVerbForWireOp(MGPWireOp::ResumeStreamOutput), MGPipeVerb::ResumeTransformFeedback);
|
|
EXPECT_EQ(MGPipeVerbForWireOp(MGPWireOp::GenerateMipmap), MGPipeVerb::GenerateMipmap);
|
|
EXPECT_EQ(MGPipeVerbForWireOp(MGPWireOp::GetTextureImage), MGPipeVerb::GetTextureImage);
|
|
// P5b (MG_Remote/CONTRACT-P5B.md): the renamed boundary the file left to "the phase that
|
|
// emits it" - resource_copy_region is glCopyImageSubData only - and the five appended verbs,
|
|
// each stamped as the GLFunctionsTable verb it reproduces. copy_framebuffer_to_texture also
|
|
// carries glCopyTexSubImage2D and stamps CopyTexImage2D for both: one kBlitOrCopy mask.
|
|
EXPECT_EQ(MGPipeVerbForWireOp(MGPWireOp::ResourceCopyRegion), MGPipeVerb::CopyImageSubData);
|
|
EXPECT_EQ(MGPipeVerbForWireOp(MGPWireOp::BindShaderImage), MGPipeVerb::BindImageTexture);
|
|
EXPECT_EQ(MGPipeVerbForWireOp(MGPWireOp::PatchParameter), MGPipeVerb::PatchParameteri);
|
|
EXPECT_EQ(MGPipeVerbForWireOp(MGPWireOp::BindStreamOutput), MGPipeVerb::BindTransformFeedback);
|
|
EXPECT_EQ(MGPipeVerbForWireOp(MGPWireOp::SetStorageBlockBinding),
|
|
MGPipeVerb::ShaderStorageBlockBinding);
|
|
EXPECT_EQ(MGPipeVerbForWireOp(MGPWireOp::CopyFramebufferToTexture), MGPipeVerb::CopyTexImage2D);
|
|
EXPECT_EQ(MGPipeVerbForWireOp(MGPWireOp::FenceCreate), MGPipeVerb::FenceSync);
|
|
EXPECT_EQ(MGPipeVerbForWireOp(MGPWireOp::FenceStatus), MGPipeVerb::GetSyncStatus);
|
|
EXPECT_EQ(MGPipeVerbForWireOp(MGPWireOp::FenceWait), MGPipeVerb::ClientWaitSync);
|
|
EXPECT_EQ(MGPipeVerbForWireOp(MGPWireOp::FenceDestroy), MGPipeVerb::DeleteSync);
|
|
EXPECT_EQ(MGPipeVerbForWireOp(MGPWireOp::FenceWaitServer), MGPipeVerb::WaitSync);
|
|
// The query target travels in MGPQueryDesc::Kind. Begin/end share the
|
|
// kQuery class across timer, occlusion and primitive queries, with the
|
|
// primitive-query verb as the canonical stamp for the shared wire opcode.
|
|
EXPECT_EQ(MGPipeVerbForWireOp(MGPWireOp::QueryCreate), MGPipeVerb::BeginXfbPrimitivesQuery);
|
|
EXPECT_EQ(MGPipeVerbForWireOp(MGPWireOp::QueryBegin), MGPipeVerb::BeginXfbPrimitivesQuery);
|
|
EXPECT_EQ(MGPipeVerbForWireOp(MGPWireOp::QueryEnd), MGPipeVerb::EndXfbPrimitivesQuery);
|
|
EXPECT_EQ(MGPipeVerbForWireOp(MGPWireOp::QueryAvailable), MGPipeVerb::IsQueryResultAvailable);
|
|
EXPECT_EQ(MGPipeVerbForWireOp(MGPWireOp::QueryResult), MGPipeVerb::GetQueryResult64);
|
|
EXPECT_EQ(MGPipeVerbForWireOp(MGPWireOp::QueryDestroy), MGPipeVerb::DeleteBackendQuery);
|
|
EXPECT_EQ(MGPipeVerbForWireOp(MGPWireOp::QueryCounter), MGPipeVerb::QueryCounterTimestamp);
|
|
EXPECT_EQ(MGPipeVerbForWireOp(MGPWireOp::QueryTimestamp), MGPipeVerb::GetGpuTimestampNs);
|
|
EXPECT_EQ(MGPipeVerbForWireOp(MGPWireOp::DeleteStreamOutput), MGPipeVerb::DeleteTransformFeedback);
|
|
// P9: the pack-buffer halves stamp as the verbs they reproduce.
|
|
EXPECT_EQ(MGPipeVerbForWireOp(MGPWireOp::ReadPixelsToBuffer), MGPipeVerb::ReadPixels);
|
|
EXPECT_EQ(MGPipeVerbForWireOp(MGPWireOp::GetTextureImageToBuffer), MGPipeVerb::GetTextureImage);
|
|
EXPECT_EQ(kMGPipeVerbBoundaryOpCount, SizeT{34});
|
|
EXPECT_EQ(kMGPipeVerbBoundaryExemptCount, SizeT{3});
|
|
|
|
// Present is class B (it is emitted in P5) and is STILL not a verb boundary:
|
|
// FillPoints.def:21 - "Present and SetSwapInterval go through BackendObject virtuals and
|
|
// read no frontend state, so they are not verbs here". Stamping there would retire the
|
|
// previous verb's answers with nothing to put in their place.
|
|
EXPECT_EQ(MGPipeVerbForWireOp(MGPWireOp::Present), MGPipeVerb::kVerbCount);
|
|
EXPECT_EQ(MGPipeVerbForWireOp(MGPWireOp::SetSwapInterval), MGPipeVerb::kVerbCount);
|
|
EXPECT_EQ(MGPipeVerbForWireOp(MGPWireOp::Flush), MGPipeVerb::kVerbCount);
|
|
// ... and a record that is part of a verb rather than a boundary of one.
|
|
EXPECT_EQ(MGPipeVerbForWireOp(MGPWireOp::SetDynamicState), MGPipeVerb::kVerbCount);
|
|
EXPECT_EQ(MGPipeVerbForWireOp(MGPWireOp::GetCaps), MGPipeVerb::kVerbCount);
|
|
}
|
|
|
|
// P5f terminal contract; the historical registration name is retained for G14.
|
|
TEST_F(FieldOwnershipTest, TheAdmittedPullTableIsID84sDerivationAndNotAList) {
|
|
// P5f retires every production debt row. Generator self-tests retain synthetic
|
|
// debt fixtures to exercise all historical admission disjuncts independently.
|
|
EXPECT_EQ(kMGPipeBarrierPulledFieldCount, SizeT{0});
|
|
EXPECT_EQ(kMGPipeAdmittedPullPairCount, SizeT{0});
|
|
for (SizeT v = 0; v < kMGPipeVerbCount; ++v)
|
|
for (SizeT f = 0; f < kMGPipeInputFieldCount; ++f)
|
|
EXPECT_FALSE(MGPipeBarrierPullAdmitted(static_cast<MGPipeInputField>(f),
|
|
static_cast<MGPipeVerb>(v)))
|
|
<< kMGPipeInputFieldNames[f] << "@" << kMGPipeVerbNames[v];
|
|
EXPECT_FALSE(MGPipeBarrierPullAdmitted(MGPipeInputField::GetFramebufferBindingSlot,
|
|
MGPipeVerb::kVerbCount));
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------------
|
|
// The behaviour: the table is load-bearing. Split builds only - it is the server verb stamp
|
|
// that arms all of it, and nothing in a monolith lane stamps.
|
|
// ---------------------------------------------------------------------------------------
|
|
|
|
// P5d round 3 (package C): THE RESIDUAL FILL's SUPPLIED-SET MEMO, and the one thing about it
|
|
// that can be wrong. The predicate is the same expression step 4 used to spell once per field
|
|
// per verb, so the change carries no new verdict - what it carries is a KEY, and a key that
|
|
// misses one of its inputs answers the PREVIOUS environment's question at the new one. In the
|
|
// direction that loses that means the fill SKIPS a field no call supplied - and that is SILENT,
|
|
// not an abort: the walk stamps FilledGen from the verb serial whether or not it copied, so the
|
|
// field reads FRESH with the previous verb's value and the draw goes out with a stale binding
|
|
// slot. Poison catches an UNSTAMPED read; it cannot catch a stamped-but-uncopied one. That is
|
|
// exactly why this case exists - there is no second line of defence behind it.
|
|
//
|
|
// So each input is moved on its own with the answer read back in between, and in an order where
|
|
// a memo that ignored that input would have to return the stale answer rather than the right one
|
|
// by luck. This case lives outside the split-only block because the memo is in the push build
|
|
// too (PipeFill.cpp is compiled at MOBILEGL_PIPE_PUSH), and the fields it names are the same
|
|
// three in both.
|
|
//
|
|
// THE FOURTH KEY INPUT - the P4a consumer signal - GETS STEP 4 AND A DIFFERENT SHAPE, because
|
|
// it cannot move an answer today and no honest case can pretend otherwise: every field whose
|
|
// emitter belongs to a P4a family is also a field the applier cannot supply whole (its storage
|
|
// is a frontend pointer), so that conjunct is DOMINATED and the mask is identical either way.
|
|
// Step 4 moves the signal for real and pins the domination instead, so that the day a P4a row
|
|
// gains a twin the case goes red and says what to write - the same day PipeFill.cpp's
|
|
// NoP4aFamilyFieldIsWhollySupplied() static_assert fires.
|
|
TEST_F(FieldOwnershipTest, TheResidualFillsSuppliedMemoReKeysOnEveryInputThatMovesAnAnswer) {
|
|
constexpr Uint64 kNoSubsystems = 0;
|
|
constexpr Uint64 kEverySubsystem = ~Uint64{0};
|
|
|
|
// 1. THE PUSH MASK. create_render_state carries GetRenderStateParameters whole and the
|
|
// applier writes it without deriving, so at a mask that carries the render-state bit the
|
|
// fill skips it - and at 0, which is what a unit lane runs at (Features.PipePush defaults
|
|
// to 0), nothing is emitted and every field is pulled.
|
|
EXPECT_FALSE(MGPipeResidualFillSuppliesField(MGPipeInputField::GetRenderStateParameters,
|
|
kNoSubsystems, true, false));
|
|
EXPECT_TRUE(MGPipeResidualFillSuppliesField(MGPipeInputField::GetRenderStateParameters,
|
|
kEverySubsystem, true, false));
|
|
EXPECT_FALSE(MGPipeResidualFillSuppliesField(MGPipeInputField::GetRenderStateParameters,
|
|
kNoSubsystems, true, false))
|
|
<< "the memo answered the previous mask: MOBILEGL_PIPE_PUSH is not in its key, and a "
|
|
"per-subsystem A/B would then fill from the wrong subsystem set";
|
|
|
|
// 2. THE DERIVATION LATCH. GetViewport reaches PipeInputs only through
|
|
// MGPipeDeriveRenderStateFields, so a build whose derivation is a stub must keep PULLING
|
|
// it - skipping it there leaves the mirror unwritten and the backend reading a default.
|
|
EXPECT_TRUE(MGPipeResidualFillSuppliesField(MGPipeInputField::GetViewport, kEverySubsystem,
|
|
true, false));
|
|
EXPECT_FALSE(MGPipeResidualFillSuppliesField(MGPipeInputField::GetViewport, kEverySubsystem,
|
|
false, false))
|
|
<< "the memo answered the previous latch: ApplierDerivesRenderStateFields is not in its key";
|
|
|
|
// 3. P5c rv's WIRE GATE (CONTRACT-P5C.md §5.3). set_context_values has no producer without a
|
|
// live wire, so its eight value-class fields keep being pulled under monolith - G1's
|
|
// byte-for-byte rule - and are skipped only when the record really crosses.
|
|
EXPECT_FALSE(MGPipeResidualFillSuppliesField(MGPipeInputField::GetActiveTextureUnit,
|
|
kEverySubsystem, true, false));
|
|
EXPECT_TRUE(MGPipeResidualFillSuppliesField(MGPipeInputField::GetActiveTextureUnit,
|
|
kEverySubsystem, true, true))
|
|
<< "the memo answered the previous wire state: contextValuesWireLive is not in its key, "
|
|
"and the emission's half of the gate and the fill's half would then disagree";
|
|
|
|
// 4. THE P4a CONSUMER SIGNAL, which is in the key and is DOMINATED, so this step pins the
|
|
// domination rather than pretending to move an answer. Under monolith the signal IS "did
|
|
// a backend register the resource op table" (P4aFamilyHasItsConsumer, PipeFill.cpp), and
|
|
// MG_Config::Transport is a constexpr Monolith in every lane but a split one - so the
|
|
// signal really moves here, at a mask that carries all four P4a bits, which is the only
|
|
// mask at which the fill reads it at all.
|
|
if (MG_Config::Transport == MG_Config::TransportMode::Monolith) {
|
|
const MGPipeResourceOps* const savedOps = MGPipeGetResourceOps();
|
|
MGPipeResourceOps ops{};
|
|
MGPipeFieldMask withConsumer{};
|
|
MGPipeFieldMask withoutConsumer{};
|
|
|
|
// An EMPTY table is enough: the predicate only asks whether ONE IS REGISTERED, which is
|
|
// how MG_Test/Pipe arms this subsystem everywhere else.
|
|
MGPipeSetResourceOps(&ops);
|
|
ASSERT_NE(MGPipeGetResourceOps(), nullptr)
|
|
<< "the fixture did not move the signal it names, so this step observes nothing";
|
|
for (SizeT i = 0; i < kMGPipeInputFieldCount; ++i) {
|
|
if (MGPipeResidualFillSuppliesField(static_cast<MGPipeInputField>(i), kEverySubsystem,
|
|
true, true)) {
|
|
withConsumer.Words[i / 64] |= (Uint64{1} << (i % 64));
|
|
}
|
|
}
|
|
|
|
MGPipeSetResourceOps(nullptr);
|
|
ASSERT_EQ(MGPipeGetResourceOps(), nullptr);
|
|
for (SizeT i = 0; i < kMGPipeInputFieldCount; ++i) {
|
|
if (MGPipeResidualFillSuppliesField(static_cast<MGPipeInputField>(i), kEverySubsystem,
|
|
true, true)) {
|
|
withoutConsumer.Words[i / 64] |= (Uint64{1} << (i % 64));
|
|
}
|
|
}
|
|
MGPipeSetResourceOps(savedOps);
|
|
|
|
// THE NAMED ROW, both ways: GetFramebufferBindingSlot is emitted by set_framebuffer_state
|
|
// and so rides the framebuffer family's consumer gate - and is pulled regardless, because
|
|
// its storage is a BindingSlot<FramebufferObject> no payload can carry.
|
|
EXPECT_FALSE(MGPipeFieldMaskHas(withConsumer, MGPipeInputField::GetFramebufferBindingSlot));
|
|
EXPECT_FALSE(
|
|
MGPipeFieldMaskHas(withoutConsumer, MGPipeInputField::GetFramebufferBindingSlot));
|
|
|
|
for (SizeT i = 0; i < kMGPipeInputFieldCount; ++i) {
|
|
const auto field = static_cast<MGPipeInputField>(i);
|
|
EXPECT_EQ(MGPipeFieldMaskHas(withConsumer, field),
|
|
MGPipeFieldMaskHas(withoutConsumer, field))
|
|
<< kMGPipeInputFieldNames[i]
|
|
<< "'s supplied answer moved with the P4a consumer signal. That is CORRECT "
|
|
"behaviour and this assertion is a trip wire, not a defect report: the "
|
|
"conjunct stopped being dominated, so the memo's fourth key input is now "
|
|
"observable and needs the move-it-and-read-it-back pair steps 1-3 give the "
|
|
"other three - under split through CapsMirrorInstance().Adopt() with and "
|
|
"without kMGPipeSubsystemResources in the CallMask, as "
|
|
"SanityTest.ACapsMaskWithoutTheResourceFamilyEmitsNothingAndCountsTheRefusal "
|
|
"already does. Rewrite this step into that pair; PipeFill.cpp's "
|
|
"NoP4aFamilyFieldIsWhollySupplied() static_assert fires on the same change";
|
|
}
|
|
}
|
|
}
|
|
|
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
|
|
|
TEST_F(FieldOwnershipTest, AServerStampMakesRecordSuppliedFieldsFreshAndWithdrawsTheRest) {
|
|
MGPipeServerStampVerbBoundary(MGPipeVerb::Clear);
|
|
EXPECT_TRUE(gPipeInputs.ServerStampedVerb());
|
|
EXPECT_EQ(gPipeInputs.CurrentVerb(), MGPipeVerb::Clear);
|
|
const MGPipeFieldMask& mask = kMGPipeClassFieldMask[static_cast<SizeT>(
|
|
kMGPipeVerbClass[static_cast<SizeT>(MGPipeVerb::Clear)])];
|
|
SizeT stamped = 0;
|
|
for (SizeT i = 0; i < kMGPipeInputFieldCount; ++i) {
|
|
const auto field = static_cast<MGPipeInputField>(i);
|
|
const Bool fresh = MGPipeInputFieldIsFresh(gPipeInputs.FilledState(), field);
|
|
// The stamp respects the verb's own may-read table for the client fill's reason: a
|
|
// field outside the class was never copied for this verb, so answering it would hand
|
|
// the server the previous verb's value.
|
|
const Bool answerable = MGPipeFieldMaskHas(mask, field) &&
|
|
(kMGPipeFieldOwnership[i] == MGPipeFieldOwnership::kRecordSupplied ||
|
|
kMGPipeFieldOwnership[i] == MGPipeFieldOwnership::kApplierDerived);
|
|
EXPECT_EQ(fresh, answerable)
|
|
<< kMGPipeInputFieldNames[i] << " is " << MGPipeFieldOwnershipName(kMGPipeFieldOwnership[i])
|
|
<< " and the stamp answered " << (fresh ? "fresh" : "stale");
|
|
stamped += fresh ? 1 : 0;
|
|
}
|
|
// Not vacuous: kClear really does have record-supplied fields to stamp.
|
|
EXPECT_GT(stamped, SizeT{0});
|
|
|
|
// AND FOUR NAMED FIELDS, NOT DERIVED FROM THE ARRAY UNDER TEST. The loop above recomputes
|
|
// `answerable` out of kMGPipeFieldOwnership, so it can only catch a stamp that disagrees
|
|
// with the table - never a table that is wrong. These four say what the stamp must do for
|
|
// four fields whose class is an argument of this package rather than a lookup.
|
|
const MGPipeFilledState& filled = gPipeInputs.FilledState();
|
|
EXPECT_TRUE(MGPipeInputFieldIsFresh(filled, MGPipeInputField::GetClearColor)); // supplied
|
|
EXPECT_TRUE(MGPipeInputFieldIsFresh(filled, MGPipeInputField::GetRenderStateParameters));
|
|
EXPECT_FALSE(MGPipeInputFieldIsFresh(filled, MGPipeInputField::GetFramebufferBindingSlot)); // pulled
|
|
EXPECT_FALSE(MGPipeInputFieldIsFresh(filled, MGPipeInputField::RecordError)); // sticky
|
|
}
|
|
|
|
// THE STICKY EXEMPTION, CANCELLED. generated/PipeFilled.inc answers "fresh" for a sticky field
|
|
// whatever the serial says - but it tests "never filled" FIRST, so the stamp's withdrawal
|
|
// (FilledGen = 0) wins over kMGPipeInputFieldSticky without a line of the generated file
|
|
// changing. Before this, the seven most dangerous fields were exempt by construction.
|
|
TEST_F(FieldOwnershipTest, TheStickyExemptionIsCancelledByTheServerStamp) {
|
|
MGPipeValidateForVerb(MGPipeVerb::Clear); // the CLIENT fills and stamps all 63, sticky included
|
|
EXPECT_TRUE(MGPipeInputFieldIsFresh(gPipeInputs.FilledState(), MGPipeInputField::RecordError));
|
|
MGPipeServerStampVerbBoundary(MGPipeVerb::Clear);
|
|
for (SizeT i = 0; i < kMGPipeFieldOwnershipForwardCount; ++i) {
|
|
const MGPipeInputField field = kMGPipeFieldOwnershipForwardField[i];
|
|
// P5f fe retired the scalar forwards to server-owned answers. Their freshness
|
|
// comes from that ownership; the remaining client forwards lose the exemption.
|
|
const Bool serverOwned = MGPipeFieldOwnershipOf(field) == MGPipeFieldOwnership::kApplierDerived;
|
|
EXPECT_EQ(MGPipeInputFieldIsFresh(gPipeInputs.FilledState(), field), serverOwned)
|
|
<< kMGPipeInputFieldNames[Index(field)] << " has the wrong server-stamp freshness";
|
|
}
|
|
}
|
|
|
|
// E4's NEGATIVE CONTROL. Move one field from RECORD-SUPPLIED to FATAL in FieldOwnership.def
|
|
// and this case goes red by name: the read below aborts instead of completing.
|
|
TEST_F(FieldOwnershipTest, ARecordSuppliedFieldIsReadableAfterAServerStamp) {
|
|
ASSERT_EQ(MGPipeFieldOwnershipOf(MGPipeInputField::GetClearColor),
|
|
MGPipeFieldOwnership::kRecordSupplied);
|
|
MGPipeServerStampVerbBoundary(MGPipeVerb::Clear);
|
|
(void)gPipeInputs.GetClearColor();
|
|
(void)gPipeInputs.GetRenderStateParameters();
|
|
EXPECT_EQ(MGPipeResidualPullCount(), Uint64{0});
|
|
// The pixel store is in kReadback's class, not kClear's, so its readable half is exercised
|
|
// under the verb that actually reads it. And P5c rv's record-supplied rows join it there:
|
|
// GetActiveTextureUnit is exactly the read that used to count into `rsp`.
|
|
MGPipeServerStampVerbBoundary(MGPipeVerb::ReadPixels);
|
|
(void)gPipeInputs.GetPixelStoreParameters(false); // the half that has a carrier
|
|
(void)gPipeInputs.GetActiveTextureUnit(); // P5c rv: set_context_values carries it
|
|
(void)gPipeInputs.GetMaxTouchedTextureUnit();
|
|
EXPECT_EQ(MGPipeResidualPullCount(), Uint64{0});
|
|
// And a shutter answers the applier's own Serial under a server stamp (APPLIER-DERIVED),
|
|
// not the client's residual-fill copy: still zero pulls, and the answer MOVES when the
|
|
// applier's texture state does.
|
|
(void)gPipeInputs.GetTextureBindGeneration();
|
|
EXPECT_EQ(MGPipeResidualPullCount(), Uint64{0});
|
|
}
|
|
|
|
// P5c rv (CONTRACT-P5C.md §5.3): the residual-value record's applier write, read back through
|
|
// the accessors a backend uses, under the stamps that publish them - and the three texture
|
|
// shutters answering the applier's own serials rather than the client's fill. kReadback's
|
|
// class carries the two texture-unit counters, kDraw's the rest.
|
|
TEST_F(FieldOwnershipTest, SetContextValuesLandsInPipeInputsAndTheShuttersAnswerTheApplier) {
|
|
MGPContextValues values{};
|
|
values.ActiveTextureUnit = 5;
|
|
values.MaxTouchedTextureUnit = 23;
|
|
values.TouchedBufferBindingPointCount[static_cast<Uint32>(BufferTarget::Uniform)] = 7;
|
|
values.IsTransformFeedbackActive = 1;
|
|
values.IsTransformFeedbackPaused = 0;
|
|
values.TransformFeedbackGeneration = 0x1112131415161718ull;
|
|
values.BoundTransformFeedbackLifetimeId = 0x2122232425262728ull;
|
|
values.TransformFeedbackCapturedVertices = 0x3132333435363738ull;
|
|
MGPipeApplySetContextValues(values);
|
|
|
|
MGPipeServerStampVerbBoundary(MGPipeVerb::ReadPixels);
|
|
EXPECT_EQ(gPipeInputs.GetActiveTextureUnit(), 5);
|
|
EXPECT_EQ(gPipeInputs.GetMaxTouchedTextureUnit(), 23);
|
|
MGPipeServerStampVerbBoundary(MGPipeVerb::DrawArrays);
|
|
EXPECT_EQ(gPipeInputs.GetTouchedBufferBindingPointCount(BufferTarget::Uniform), SizeT{7});
|
|
EXPECT_EQ(gPipeInputs.GetTouchedBufferBindingPointCount(BufferTarget::Vertex), SizeT{0});
|
|
EXPECT_TRUE(gPipeInputs.IsTransformFeedbackActive());
|
|
EXPECT_FALSE(gPipeInputs.IsTransformFeedbackPaused());
|
|
EXPECT_EQ(gPipeInputs.GetTransformFeedbackGeneration(), 0x1112131415161718ull);
|
|
EXPECT_EQ(gPipeInputs.GetBoundTransformFeedbackLifetimeId(), 0x2122232425262728ull);
|
|
EXPECT_EQ(gPipeInputs.GetTransformFeedbackCapturedVertices(), 0x3132333435363738ull);
|
|
// Eight record-supplied reads and not one residual pull.
|
|
EXPECT_EQ(MGPipeResidualPullCount(), Uint64{0});
|
|
|
|
// The three shutters answer the applier's own serials under a stamped verb. They are
|
|
// SHUTTERS - the value matters only in that it MOVES when the server's texture state does
|
|
// and never walks backwards - so the pin is the identity with the applier's counters, not
|
|
// any particular number.
|
|
EXPECT_EQ(gPipeInputs.GetTextureBindGeneration(), MGPipeApplierTextureShutterSerial());
|
|
EXPECT_EQ(gPipeInputs.GetSamplingResolutionGeneration(), MGPipeApplierTextureShutterSerial());
|
|
EXPECT_EQ(gPipeInputs.GetTextureContextId(), MGPipeApplierContextSerial());
|
|
const Uint64 before = MGPipeApplierTextureShutterSerial();
|
|
MGPipeApplierNoteTextureStateMoved();
|
|
EXPECT_EQ(gPipeInputs.GetTextureBindGeneration(), before + 1)
|
|
<< "a texture-state apply moved the serial but the shutter did not answer with it";
|
|
EXPECT_EQ(MGPipeResidualPullCount(), Uint64{0});
|
|
}
|
|
|
|
// P5f terminal contract; the historical registration name is retained for G14.
|
|
TEST_F(FieldOwnershipTest, ABarrierPulledReadAfterAServerStampIsCountedNotFatal) {
|
|
#if MGTEST_HAVE_FORK
|
|
// Historical name retained for G14: the formerly admitted read is now forbidden.
|
|
const ChildResult r = RunInChild([] {
|
|
|
|
MGPipeServerStampVerbBoundary(MGPipeVerb::DrawArrays);
|
|
(void)gPipeInputs.GetBoundVertexArray();
|
|
});
|
|
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
|
|
EXPECT_NE(r.Log.find("Fatal{UnmigratedPipeInput, \"GetBoundVertexArray@DrawArrays\"}"), std::string::npos) << r.Log;
|
|
EXPECT_EQ(r.Log.find("Admitted{"), std::string::npos) << r.Log;
|
|
#else
|
|
GTEST_SKIP() << "needs fork";
|
|
#endif
|
|
}
|
|
|
|
// P5f terminal contract; the historical registration name is retained for G14.
|
|
TEST_F(FieldOwnershipTest, AStickyForwardIsCountedAsAResidualPull) {
|
|
#if MGTEST_HAVE_FORK
|
|
// Historical name retained for G14: the formerly admitted read is now forbidden.
|
|
const ChildResult r = RunInChild([] {
|
|
|
|
MGPipeServerStampVerbBoundary(MGPipeVerb::Clear);
|
|
(void)gPipeInputs.ValidateProgramName(1u);
|
|
});
|
|
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
|
|
EXPECT_NE(r.Log.find("Fatal{UnmigratedPipeInput, \"ValidateProgramName@Clear\"}"), std::string::npos) << r.Log;
|
|
EXPECT_EQ(r.Log.find("Admitted{"), std::string::npos) << r.Log;
|
|
#else
|
|
GTEST_SKIP() << "needs fork";
|
|
#endif
|
|
}
|
|
|
|
// ... and outside a server-stamped verb they are ordinary monolith calls, which is what keeps
|
|
// InvalidateCompileEnv reachable from backend initialisation - the case the exemption exists
|
|
// for - and what keeps build-split's monolith lanes behaving as a verify build's do.
|
|
TEST_F(FieldOwnershipTest, NothingIsCountedOutsideAServerStampedVerb) {
|
|
MGPipeValidateForVerb(MGPipeVerb::ReadPixels);
|
|
(void)gPipeInputs.ValidateProgramName(1u);
|
|
gPipeInputs.InvalidateCompileEnv();
|
|
(void)gPipeInputs.GetActiveTextureUnit();
|
|
EXPECT_EQ(MGPipeResidualPullCount(), Uint64{0});
|
|
EXPECT_FALSE(gPipeInputs.ServerStampedVerb());
|
|
}
|
|
|
|
// THE APPLIER'S OWN CLEAR, reached directly rather than through the client's fill. In a spawned
|
|
// server MG_Impl is not in the process, so MGPipeValidateForVerb/MGPipeLeaveVerb never run and
|
|
// MGPipeServerClearVerbBoundary is the ONLY thing that can disarm the flag; without it the
|
|
// server latches TRUE after its first stamp and the sticky exemption - the one
|
|
// InvalidateCompileEnv is reached from backend initialisation under - is gone for good.
|
|
TEST_F(FieldOwnershipTest, TheAppliersOwnClearDisarmsTheStampWithoutTheClientsFill) {
|
|
MGPipeServerStampVerbBoundary(MGPipeVerb::ReadPixels);
|
|
ASSERT_TRUE(gPipeInputs.ServerStampedVerb());
|
|
(void)gPipeInputs.GetActiveTextureUnit();
|
|
ASSERT_EQ(MGPipeResidualPullCount(), Uint64{0});
|
|
MGPipeServerClearVerbBoundary();
|
|
EXPECT_FALSE(gPipeInputs.ServerStampedVerb());
|
|
(void)gPipeInputs.ValidateProgramName(1u);
|
|
gPipeInputs.InvalidateCompileEnv();
|
|
EXPECT_EQ(MGPipeResidualPullCount(), Uint64{0});
|
|
}
|
|
|
|
// The verb's own may-read table still holds on the server: kClear does not read
|
|
// GetProgramForDraw, so reading it there is a forbidden legacy access,
|
|
// and it stays Fatal. Counting it would trade a loud staleness for a quiet one.
|
|
// (P5c rv: the exemplar moved - GetActiveTextureUnit is RECORD-SUPPLIED since rv and would
|
|
// say nothing about the pulled set here.)
|
|
TEST_F(FieldOwnershipTest, ABarrierPulledFieldOutsideTheVerbsClassIsStillFatal) {
|
|
#if MGTEST_HAVE_FORK
|
|
const ChildResult r = RunInChild([] {
|
|
MGPipeServerStampVerbBoundary(MGPipeVerb::Clear);
|
|
(void)gPipeInputs.GetProgramForDraw(); // retired accessor, outside kClear too
|
|
});
|
|
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
|
|
EXPECT_NE(r.Log.find("Fatal{UnmigratedPipeInput, \"GetProgramForDraw@Clear\"}"), std::string::npos)
|
|
<< r.Log;
|
|
EXPECT_EQ(r.Log.find("BARRIER-PULLED"), std::string::npos) << r.Log;
|
|
#else
|
|
GTEST_SKIP() << "needs fork";
|
|
#endif
|
|
}
|
|
|
|
TEST_F(FieldOwnershipTest, ResidualPullsReachThePublishedPerFrameCounter) {
|
|
namespace PS = MG_Util::PipeStats;
|
|
PS::SetEnabledForTesting(true);
|
|
PS::ResetForTesting();
|
|
MGPipeServerStampVerbBoundary(MGPipeVerb::DrawArrays);
|
|
(void)gPipeInputs.GetCurrentVertexAttribute(0);
|
|
EXPECT_EQ(PS::FrameCalls(PS::CallClass::ResidualPulls), Uint64{0});
|
|
EXPECT_NE(PS::FormatWindowLine().find(" rsp=0"), std::string::npos) << PS::FormatWindowLine();
|
|
// Prove the reporting channel did not merely become a constant zero after retirement.
|
|
PS::AddCalls(PS::CallClass::ResidualPulls, 1);
|
|
EXPECT_EQ(PS::FrameCalls(PS::CallClass::ResidualPulls), Uint64{1});
|
|
EXPECT_NE(PS::FormatWindowLine().find(" rsp=1"), std::string::npos) << PS::FormatWindowLine();
|
|
PS::ResetForTesting();
|
|
PS::SetEnabledForTesting(false);
|
|
}
|
|
|
|
// ================================================================================
|
|
// P5f (f1): THE DUAL-BLOCK REHEARSAL AT THE BLOCK LEVEL (P5F-WIRE-COMPLETENESS.md §4)
|
|
// ================================================================================
|
|
//
|
|
// MOBILEGL_IPC_ROLE_SPLIT_STATE=1 gives the fill side its own PipeInputs block
|
|
// (MGPipeClientInputs()) and leaves gPipeInputs to the server alone. These cases pin the
|
|
// mechanism's three arms without a transport running: the selection folds to the shared block
|
|
// when the knob is off OR the transport is monolith, the two blocks are distinct objects when
|
|
// it is armed, and a BARRIER-PULLED read under it is a NAMED Fatal with no strict knob
|
|
// involved (the fork cases below).
|
|
|
|
namespace {
|
|
// Arms the rehearsal by hand - the knob is parsed from the environment once per process,
|
|
// so a case sets the two globals directly, and the guard puts them back on every exit
|
|
// path, ASSERT death included.
|
|
struct RoleSplitArm {
|
|
RoleSplitArm() {
|
|
m_previousTransport = MG_Config::Transport;
|
|
m_previousKnob = MG_Config::Ipc.RoleSplitState;
|
|
}
|
|
~RoleSplitArm() {
|
|
MG_Config::Transport = m_previousTransport;
|
|
MG_Config::Ipc.RoleSplitState = m_previousKnob;
|
|
}
|
|
void Arm(Bool arm) {
|
|
MG_Config::Ipc.RoleSplitState = arm;
|
|
MG_Config::Transport = arm ? MG_Config::TransportMode::InProcess
|
|
: MG_Config::TransportMode::Monolith;
|
|
}
|
|
MG_Config::TransportMode m_previousTransport;
|
|
Bool m_previousKnob;
|
|
};
|
|
} // namespace
|
|
|
|
TEST_F(FieldOwnershipTest, SplitBindingPointCapacityNeverConsultsTheFrontend) {
|
|
RoleSplitArm guard;
|
|
guard.Arm(true);
|
|
MGPipeServerStampVerbBoundary(MGPipeVerb::DrawArrays);
|
|
EXPECT_EQ(gPipeInputs.GetBufferBindingPointCount(BufferTarget::Uniform), 84u);
|
|
EXPECT_EQ(gPipeInputs.GetBufferBindingPointCount(BufferTarget::TransformFeedback), 84u);
|
|
EXPECT_EQ(gPipeInputs.GetBufferBindingPointCount(BufferTarget::PixelPack), 0u);
|
|
EXPECT_EQ(MGPipeFieldOwnershipOf(MGPipeInputField::GetBufferBindingPointCount),
|
|
MGPipeFieldOwnership::kApplierDerived);
|
|
MGPipeServerClearVerbBoundary();
|
|
}
|
|
|
|
TEST_F(FieldOwnershipTest, SplitOpenSpansAreOwnedByTheApplierAndSurviveOtherBindings) {
|
|
RoleSplitArm guard;
|
|
guard.Arm(true);
|
|
auto& state = MGPipeApplier();
|
|
state.StreamOutputSpans.clear();
|
|
MGPipeServerStampVerbBoundary(MGPipeVerb::DrawArrays);
|
|
EXPECT_FALSE(gPipeInputs.HasOpenTransformFeedbackSpan(0));
|
|
EXPECT_FALSE(gPipeInputs.HasOpenTransformFeedbackSpan(11));
|
|
state.StreamOutputSpans[11] = {};
|
|
state.StreamOutputSpans[12] = {};
|
|
state.BoundStreamOutputLifetimeId = 12;
|
|
EXPECT_TRUE(gPipeInputs.HasOpenTransformFeedbackSpan(11));
|
|
EXPECT_TRUE(gPipeInputs.HasOpenTransformFeedbackSpan(12));
|
|
state.StreamOutputSpans.erase(12);
|
|
EXPECT_TRUE(gPipeInputs.HasOpenTransformFeedbackSpan(11));
|
|
EXPECT_FALSE(gPipeInputs.HasOpenTransformFeedbackSpan(12));
|
|
EXPECT_EQ(MGPipeFieldOwnershipOf(MGPipeInputField::HasOpenTransformFeedbackSpan),
|
|
MGPipeFieldOwnership::kApplierDerived);
|
|
state.StreamOutputSpans.clear();
|
|
state.BoundStreamOutputLifetimeId = 0;
|
|
MGPipeServerClearVerbBoundary();
|
|
}
|
|
|
|
TEST_F(FieldOwnershipTest, SplitCaptureSnapshotSurvivesMakeCurrentUntilObjectRelease) {
|
|
RoleSplitArm guard;
|
|
guard.Arm(true);
|
|
auto& state = MGPipeApplier();
|
|
state.StreamOutputSpans.clear();
|
|
MGPStreamOutputBegin begin{};
|
|
begin.LifetimeId = 0x100000002ull;
|
|
begin.CaptureProgram = {7, 2};
|
|
begin.Targets[3] = {{9, 3}, 16, 64};
|
|
state.StreamOutputSpans[begin.LifetimeId] = begin;
|
|
state.BoundStreamOutputLifetimeId = begin.LifetimeId;
|
|
|
|
// Make-current resets binding state, not a live object's open capture. The
|
|
// returning context emits context values again, but never repeats Begin.
|
|
MGPipeApplierReset();
|
|
MGPContextValues returning{};
|
|
returning.BoundTransformFeedbackLifetimeId = begin.LifetimeId;
|
|
returning.IsTransformFeedbackActive = 1;
|
|
MGPipeApplySetContextValues(returning);
|
|
EXPECT_EQ(state.BoundStreamOutputLifetimeId, begin.LifetimeId);
|
|
EXPECT_TRUE(gPipeInputs.HasOpenTransformFeedbackSpan(begin.LifetimeId));
|
|
const auto found = state.StreamOutputSpans.find(begin.LifetimeId);
|
|
EXPECT_NE(found, state.StreamOutputSpans.end());
|
|
if (found != state.StreamOutputSpans.end()) {
|
|
EXPECT_EQ(found->second.CaptureProgram, begin.CaptureProgram);
|
|
EXPECT_EQ(found->second.Targets[3].Res, begin.Targets[3].Res);
|
|
EXPECT_EQ(found->second.Targets[3].Offset, 16u);
|
|
EXPECT_EQ(found->second.Targets[3].Size, 64u);
|
|
}
|
|
MGPipeApplierReleaseObjectRecords();
|
|
EXPECT_FALSE(gPipeInputs.HasOpenTransformFeedbackSpan(begin.LifetimeId));
|
|
EXPECT_EQ(state.BoundStreamOutputLifetimeId, 0u);
|
|
}
|
|
|
|
TEST_F(FieldOwnershipTest, RoleSplitOffFoldsTheFillSideOntoTheSharedBlock) {
|
|
EXPECT_FALSE(MGPipeRoleSplitRehearsalActive());
|
|
EXPECT_EQ(&MGPipeClientInputs(), &gPipeInputs);
|
|
}
|
|
|
|
// The knob alone is not the arm: under monolith transport the two roles are one thread and
|
|
// there is exactly one block, or every read would starve. This is the fold the whole unit
|
|
// lane and the integration-gpu lane of a split build stand on.
|
|
TEST_F(FieldOwnershipTest, RoleSplitUnderMonolithTransportIsStillOneBlock) {
|
|
RoleSplitArm guard;
|
|
MG_Config::Ipc.RoleSplitState = true;
|
|
MG_Config::Transport = MG_Config::TransportMode::Monolith;
|
|
EXPECT_FALSE(MGPipeRoleSplitRehearsalActive());
|
|
EXPECT_EQ(&MGPipeClientInputs(), &gPipeInputs);
|
|
}
|
|
|
|
// D1c/D10 (CONTRACT-P6 3.2, 3.4). THE PREDICATE THAT IS TRUE WITHOUT THE KNOB, and the reason
|
|
// four guards were compiled in and permanently disarmed in a spawn server.
|
|
//
|
|
// MGPipeRoleSplitRehearsalActive() answers "is the REHEARSAL armed", which needs
|
|
// MOBILEGL_IPC_ROLE_SPLIT_STATE. Under spawn the two roles are in DIFFERENT ADDRESS SPACES, so
|
|
// their PipeInputs blocks are distinct whatever that knob says - and every guard that asked the
|
|
// rehearsal question was therefore off in the one shape where it matters most. Measured on the
|
|
// real lane: the spawn retrace runs with role-split-state=0.
|
|
TEST_F(FieldOwnershipTest, SpawnMakesTheBlocksDistinctWithNoRehearsalKnobAtAll) {
|
|
RoleSplitArm guard;
|
|
MG_Config::Ipc.RoleSplitState = false;
|
|
MG_Config::Transport = MG_Config::TransportMode::Spawn;
|
|
|
|
EXPECT_FALSE(MGPipeRoleSplitRehearsalActive())
|
|
<< "the rehearsal must stay OFF; this case is about the shape, not the knob";
|
|
EXPECT_TRUE(MGPipeBlocksAreDistinct())
|
|
<< "a spawn server shares no storage object with its client, so any guard that asked "
|
|
"only the rehearsal question is disarmed exactly where it is needed";
|
|
}
|
|
|
|
// The other direction, so the predicate cannot be a constant: monolith shares one block, and
|
|
// neither the knob nor the transport alone makes it two.
|
|
TEST_F(FieldOwnershipTest, MonolithKeepsOneBlockAndTheWiderPredicateSaysSo) {
|
|
RoleSplitArm guard;
|
|
MG_Config::Ipc.RoleSplitState = false;
|
|
MG_Config::Transport = MG_Config::TransportMode::Monolith;
|
|
EXPECT_FALSE(MGPipeBlocksAreDistinct());
|
|
EXPECT_EQ(&MGPipeClientInputs(), &gPipeInputs);
|
|
}
|
|
|
|
TEST_F(FieldOwnershipTest, RoleSplitGivesTheFillSideADistinctBlockTheStampNeverTouches) {
|
|
RoleSplitArm guard;
|
|
guard.Arm(true);
|
|
ASSERT_TRUE(MGPipeRoleSplitRehearsalActive());
|
|
ASSERT_NE(&MGPipeClientInputs(), &gPipeInputs);
|
|
|
|
const Uint64 serverSerialBefore = gPipeInputs.FilledState().CurrentVerbSerial;
|
|
const Uint64 clientSerialBefore = MGPipeClientInputs().FilledState().CurrentVerbSerial;
|
|
MGPipeServerStampVerbBoundary(MGPipeVerb::DrawArrays);
|
|
EXPECT_EQ(gPipeInputs.FilledState().CurrentVerbSerial, serverSerialBefore + 1);
|
|
EXPECT_EQ(MGPipeClientInputs().FilledState().CurrentVerbSerial, clientSerialBefore)
|
|
<< "the server's verb stamp reached into the client block";
|
|
EXPECT_TRUE(gPipeInputs.ServerStampedVerb());
|
|
EXPECT_FALSE(MGPipeClientInputs().ServerStampedVerb());
|
|
|
|
// The fill side's clear is the client block's own: it must not disarm the server's stamp,
|
|
// which is CONTRACT-P5E §3.2's "the server's stamp is server-private" as a fact rather
|
|
// than as a comment.
|
|
MGPipeClientClearVerbBoundary();
|
|
EXPECT_TRUE(gPipeInputs.ServerStampedVerb())
|
|
<< "the client-role clear withdrew the SERVER's stamp";
|
|
MGPipeServerClearVerbBoundary();
|
|
EXPECT_FALSE(gPipeInputs.ServerStampedVerb());
|
|
|
|
// And the server block's identity is server-owned under the rehearsal (§3.2's other half):
|
|
// the stamp set it, and it is non-null - a null identity reads as a hit against
|
|
// DirectGLES' zero-initialised fb-slot memo cache, which is the unnamed crash this line
|
|
// exists to preclude.
|
|
EXPECT_NE(gPipeInputs.ContextIdentity(), nullptr);
|
|
}
|
|
|
|
// The fill side's writers land in the client block alone. MGPipeLeaveVerb rather than
|
|
// MGPipeValidateForVerb: the validate point's step 3 EMITS, which is session machinery this
|
|
// process does not have, while LeaveVerb's serial bump and verb reset are exactly the fill
|
|
// side's write shape with nothing else in the way.
|
|
TEST_F(FieldOwnershipTest, TheClientVerbLeaveWritesTheClientBlockAlone) {
|
|
RoleSplitArm guard;
|
|
guard.Arm(true);
|
|
ASSERT_NE(&MGPipeClientInputs(), &gPipeInputs);
|
|
const Uint64 serverSerialBefore = gPipeInputs.FilledState().CurrentVerbSerial;
|
|
const Uint64 clientSerialBefore = MGPipeClientInputs().FilledState().CurrentVerbSerial;
|
|
MGPipeLeaveVerb();
|
|
EXPECT_EQ(MGPipeClientInputs().FilledState().CurrentVerbSerial, clientSerialBefore + 1);
|
|
EXPECT_EQ(gPipeInputs.FilledState().CurrentVerbSerial, serverSerialBefore)
|
|
<< "a fill-side write reached the server block";
|
|
EXPECT_EQ(MGPipeClientInputs().CurrentVerb(), MGPipeVerb::kVerbCount);
|
|
}
|
|
|
|
#if MGTEST_HAVE_FORK
|
|
|
|
// P5f terminal contract; the historical registration name is retained for G14.
|
|
TEST_F(FieldOwnershipTest, AClientFreshStampCannotReviveARetiredServerGetter) {
|
|
const ChildResult r = RunInChild([] {
|
|
MGPipeServerStampVerbBoundary(MGPipeVerb::DrawArrays);
|
|
// Simulate a regressed writer stamping the old mirror as fresh. FATAL is a
|
|
// representation verdict, not a freshness verdict, and must still reject it.
|
|
auto& filled = const_cast<MGPipeFilledState&>(gPipeInputs.FilledState());
|
|
filled.FilledGen[Index(MGPipeInputField::GetBoundVertexArray)] = filled.CurrentVerbSerial;
|
|
(void)gPipeInputs.GetBoundVertexArray();
|
|
});
|
|
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
|
|
EXPECT_NE(r.Log.find("Fatal{UnmigratedPipeInput, \"GetBoundVertexArray@DrawArrays\"}"),
|
|
std::string::npos) << r.Log;
|
|
}
|
|
|
|
TEST_F(FieldOwnershipTest, StrictErrorsTurnsABarrierPulledReadIntoANamedAbort) {
|
|
// Historical name retained for G14: the formerly admitted read is now forbidden.
|
|
const ChildResult r = RunInChild([] {
|
|
MG_Config::Ipc.StrictErrors = true;
|
|
MGPipeServerStampVerbBoundary(MGPipeVerb::DrawArrays);
|
|
(void)gPipeInputs.GetBoundVertexArray();
|
|
});
|
|
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
|
|
EXPECT_NE(r.Log.find("Fatal{UnmigratedPipeInput, \"GetBoundVertexArray@DrawArrays\"}"), std::string::npos) << r.Log;
|
|
EXPECT_EQ(r.Log.find("Admitted{"), std::string::npos) << r.Log;
|
|
}
|
|
|
|
// P5f terminal contract; the historical registration name is retained for G14.
|
|
TEST_F(FieldOwnershipTest, TheSameReadWithoutStrictErrorsSurvivesAndIsCounted) {
|
|
// Historical name retained for G14: the formerly admitted read is now forbidden.
|
|
const ChildResult r = RunInChild([] {
|
|
MG_Config::Ipc.StrictErrors = false;
|
|
MGPipeServerStampVerbBoundary(MGPipeVerb::DrawArrays);
|
|
(void)gPipeInputs.GetBoundVertexArray();
|
|
});
|
|
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
|
|
EXPECT_NE(r.Log.find("Fatal{UnmigratedPipeInput, \"GetBoundVertexArray@DrawArrays\"}"), std::string::npos) << r.Log;
|
|
EXPECT_EQ(r.Log.find("Admitted{"), std::string::npos) << r.Log;
|
|
}
|
|
|
|
// P5f terminal contract; the historical registration name is retained for G14.
|
|
TEST_F(FieldOwnershipTest, AnEscalatedRecordsPullIsAdmittedAndSaysWhy) {
|
|
// Historical name retained for G14: the formerly admitted read is now forbidden.
|
|
const ChildResult r = RunInChild([] {
|
|
MG_Config::Ipc.StrictErrors = true;
|
|
MGPipeApplierSetCurrentRecordBarrieredByEscalation(true);
|
|
MGPipeServerStampVerbBoundary(MGPipeVerb::DrawArrays);
|
|
(void)gPipeInputs.GetBoundVertexArray();
|
|
});
|
|
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
|
|
EXPECT_NE(r.Log.find("Fatal{UnmigratedPipeInput, \"GetBoundVertexArray@DrawArrays\"}"), std::string::npos) << r.Log;
|
|
EXPECT_EQ(r.Log.find("Admitted{"), std::string::npos) << r.Log;
|
|
}
|
|
|
|
// P5f terminal contract; the historical registration name is retained for G14.
|
|
TEST_F(FieldOwnershipTest, StrictErrorsReachTheStickyForwardsAndNameThemByField) {
|
|
// Historical name retained for G14: the formerly admitted read is now forbidden.
|
|
const ChildResult r = RunInChild([] {
|
|
MG_Config::Ipc.StrictErrors = true;
|
|
MGPipeServerStampVerbBoundary(MGPipeVerb::DrawArrays);
|
|
(void)gPipeInputs.ValidateProgramName(1u);
|
|
});
|
|
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
|
|
EXPECT_NE(r.Log.find("Fatal{UnmigratedPipeInput, \"ValidateProgramName@DrawArrays\"}"), std::string::npos) << r.Log;
|
|
EXPECT_EQ(r.Log.find("Admitted{"), std::string::npos) << r.Log;
|
|
}
|
|
|
|
// P5f terminal contract; the historical registration name is retained for G14.
|
|
TEST_F(FieldOwnershipTest, AnAdmittedBarrierPullIsLoudOnceAndNotFatal) {
|
|
// Historical name retained for G14: the formerly admitted read is now forbidden.
|
|
const ChildResult r = RunInChild([] {
|
|
MG_Config::Ipc.StrictErrors = true;
|
|
MGPipeServerStampVerbBoundary(MGPipeVerb::ReadPixels);
|
|
(void)gPipeInputs.ValidateProgramName(1u);
|
|
});
|
|
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
|
|
EXPECT_NE(r.Log.find("Fatal{UnmigratedPipeInput, \"ValidateProgramName@ReadPixels\"}"), std::string::npos) << r.Log;
|
|
EXPECT_EQ(r.Log.find("Admitted{"), std::string::npos) << r.Log;
|
|
}
|
|
|
|
// P5f terminal contract; the historical registration name is retained for G14.
|
|
TEST_F(FieldOwnershipTest, AFatalClassReadAbortsEvenWithoutStrictErrors) {
|
|
const ChildResult r = RunInChild([] {
|
|
MGPipeServerStampVerbBoundary(MGPipeVerb::DrawArrays);
|
|
(void)gPipeInputs.GetTransformFeedbackPausedPrimitiveCounter();
|
|
});
|
|
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
|
|
EXPECT_NE(r.Log.find("Fatal{UnmigratedPipeInput, "
|
|
"\"GetTransformFeedbackPausedPrimitiveCounter@DrawArrays\"}"),
|
|
std::string::npos)
|
|
<< r.Log;
|
|
}
|
|
|
|
// P5f terminal contract; the historical registration name is retained for G14.
|
|
TEST_F(FieldOwnershipTest, TheComputeProgramIsServedUnderADispatchStampFromP5bOn) {
|
|
// Historical name retained for G14: the formerly admitted read is now forbidden.
|
|
const ChildResult r = RunInChild([] {
|
|
|
|
MGPipeServerStampVerbBoundary(MGPipeVerb::DispatchCompute);
|
|
(void)gPipeInputs.GetProgramForDispatch();
|
|
});
|
|
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
|
|
EXPECT_NE(r.Log.find("Fatal{UnmigratedPipeInput, \"GetProgramForDispatch@DispatchCompute\"}"), std::string::npos) << r.Log;
|
|
EXPECT_EQ(r.Log.find("Admitted{"), std::string::npos) << r.Log;
|
|
}
|
|
|
|
// The argument-keyed row: the pack half is answerable and the unpack half is not, and the
|
|
// difference is the accessor's own argument rather than a second field id. Splitting the field
|
|
// into two ids would have made the unpack half BARRIER-PULLED - silently served - which is
|
|
// weaker than this.
|
|
TEST_F(FieldOwnershipTest, TheUnpackHalfOfThePixelStoreAbortsWhileThePackHalfDoesNot) {
|
|
const ChildResult fatal = RunInChild([] {
|
|
MGPipeServerStampVerbBoundary(MGPipeVerb::ReadPixels);
|
|
(void)gPipeInputs.GetPixelStoreParameters(true);
|
|
});
|
|
ASSERT_TRUE(DiedOfAbort(fatal)) << DescribeStatus(fatal) << "\n" << fatal.Log;
|
|
EXPECT_NE(fatal.Log.find("Fatal{UnmigratedPipeInput, \"GetPixelStoreParameters@ReadPixels\"}"),
|
|
std::string::npos)
|
|
<< fatal.Log;
|
|
// The line must say WHICH HALF. Without this the message is byte-identical to what a
|
|
// genuinely stale read of the pack half would print, and the whole case for narrowing by
|
|
// argument instead of by a second field id is that the reader is told which half they
|
|
// asked for.
|
|
EXPECT_NE(fatal.Log.find("argument 0 = 1 is FATAL while the field is APPLIER-DERIVED"),
|
|
std::string::npos)
|
|
<< fatal.Log;
|
|
|
|
const ChildResult ok = RunInChild([] {
|
|
MGPipeServerStampVerbBoundary(MGPipeVerb::ReadPixels);
|
|
(void)gPipeInputs.GetPixelStoreParameters(false);
|
|
if (MGPipeResidualPullCount() != 0) ::_exit(7);
|
|
});
|
|
ASSERT_TRUE(ExitedWith(ok, 0)) << DescribeStatus(ok) << "\n" << ok.Log;
|
|
EXPECT_EQ(ok.Log.find("Fatal{"), std::string::npos) << ok.Log;
|
|
}
|
|
|
|
// One function edit, five call sites (Managers.cpp:5334, DirectGLES.cpp:8051, :8702, :8997,
|
|
// :10623). The arm is the TRANSPORT, not the build: build-split's own lanes run monolith and
|
|
// several of the five are on ordinary monolith paths they exercise.
|
|
TEST_F(FieldOwnershipTest, AnUnmigratedEmulationIsFatalUnderARealTransportAndInertUnderMonolith) {
|
|
const ChildResult monolith = RunInChild([] {
|
|
MG_Config::Transport = MG_Config::TransportMode::Monolith;
|
|
MGPipeUnmigratedEmulation("get-tex-image-shadow");
|
|
});
|
|
ASSERT_TRUE(ExitedWith(monolith, 0)) << DescribeStatus(monolith) << "\n" << monolith.Log;
|
|
EXPECT_EQ(monolith.Log.find("Fatal{"), std::string::npos) << monolith.Log;
|
|
|
|
const ChildResult split = RunInChild([] {
|
|
MG_Config::Transport = MG_Config::TransportMode::InProcess;
|
|
MGPipeUnmigratedEmulation("get-tex-image-shadow");
|
|
});
|
|
ASSERT_TRUE(DiedOfAbort(split)) << DescribeStatus(split) << "\n" << split.Log;
|
|
EXPECT_NE(split.Log.find("Fatal{UnmigratedEmulation, \"get-tex-image-shadow\"}"), std::string::npos)
|
|
<< split.Log;
|
|
}
|
|
|
|
// P5f terminal contract; the historical registration name is retained for G14.
|
|
TEST_F(FieldOwnershipTest, DualBlockMakesABarrierPulledReadANamedAbortWithoutStrict) {
|
|
// Historical name retained for G14: the formerly admitted read is now forbidden.
|
|
const ChildResult r = RunInChild([] {
|
|
MG_Config::Transport = MG_Config::TransportMode::InProcess;
|
|
MG_Config::Ipc.RoleSplitState = true;
|
|
MG_Config::Ipc.StrictErrors = false;
|
|
MGPipeServerStampVerbBoundary(MGPipeVerb::DrawArrays);
|
|
(void)gPipeInputs.GetBoundVertexArray();
|
|
});
|
|
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
|
|
EXPECT_NE(r.Log.find("Fatal{UnmigratedPipeInput, \"GetBoundVertexArray@DrawArrays\"}"), std::string::npos) << r.Log;
|
|
EXPECT_EQ(r.Log.find("Admitted{"), std::string::npos) << r.Log;
|
|
}
|
|
|
|
// P5f terminal contract; the historical registration name is retained for G14.
|
|
TEST_F(FieldOwnershipTest, DualBlockDoesNotArmUnderMonolithTransport) {
|
|
const ChildResult r = RunInChild([] {
|
|
MG_Config::Transport = MG_Config::TransportMode::Monolith;
|
|
MG_Config::Ipc.RoleSplitState = true;
|
|
// A real monolith fill, not a manufactured server stamp on a monolith process.
|
|
MGPipeValidateForVerb(MGPipeVerb::DrawArrays);
|
|
(void)gPipeInputs.GetBoundVertexArray();
|
|
if (MGPipeRoleSplitRehearsalActive() || MGPipeResidualPullCount() != 0) ::_exit(7);
|
|
});
|
|
ASSERT_TRUE(ExitedWith(r, 0)) << DescribeStatus(r) << "\n" << r.Log;
|
|
EXPECT_EQ(r.Log.find("Fatal{"), std::string::npos) << r.Log;
|
|
}
|
|
|
|
#endif // MGTEST_HAVE_FORK
|
|
#endif // MOBILEGL_BUILD_DISAGGREGATED
|
|
#endif // MOBILEGL_PIPE_PUSH
|
|
|
|
int main(int argc, char** argv) {
|
|
// Before anything logs: MG_Util::Debug::InitFile() reads the variable once, on the first
|
|
// write, and caches the FILE*. PipeInputsTest's idiom, and for its reason - the abort cases
|
|
// read their Fatal line back out of this file.
|
|
namespace fs = std::filesystem;
|
|
const fs::path path =
|
|
fs::temp_directory_path() / ("mobilegl-fieldownership-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
|
|
::testing::InitGoogleTest(&argc, argv);
|
|
const int rc = RUN_ALL_TESTS();
|
|
fs::remove(path, ec);
|
|
return rc;
|
|
}
|