mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-17 00:28:31 +09:00
[Test] (MG_Test, Pipe): pin table 2 at runtime - the stamp rule, the four-way read verdict, rsp, the strict abort and the cancelled sticky exemption
This commit is contained in:
@@ -175,8 +175,41 @@ foreach(pipeTest ResourceEmitTest VertexInputEmitTest
|
|||||||
endif()
|
endif()
|
||||||
endforeach()
|
endforeach()
|
||||||
|
|
||||||
|
# P5 package p1: table 2 (PipeInputs field ownership) at runtime. Registered in EVERY
|
||||||
|
# configuration rather than behind `if (MOBILEGL_BUILD_DISAGGREGATED)` like the Wire suite,
|
||||||
|
# because half of it is arithmetic over the generated table that a push or verify build can
|
||||||
|
# check just as well - and a suite that only exists in one configuration is a suite three
|
||||||
|
# configurations cannot notice breaking. Its split-only cases compile away there.
|
||||||
|
#
|
||||||
|
# Links gtest rather than gtest_main and carries its own main(), like PipeInputsTest and
|
||||||
|
# RenderStateSpansTest: its abort cases read the Fatal line back out of a log file the process
|
||||||
|
# names before anything logs.
|
||||||
|
add_executable(
|
||||||
|
FieldOwnershipTest
|
||||||
|
FieldOwnershipTest.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(FieldOwnershipTest PRIVATE
|
||||||
|
${MGL_ROOT}/include
|
||||||
|
${MGL_ROOT}/MobileGL
|
||||||
|
${MGL_ROOT}/MobileGL/MG_Pipe
|
||||||
|
${MGL_ROOT}/3rdparty/xxHash
|
||||||
|
${MGL_ROOT}/3rdparty/Vulkan-Headers/include
|
||||||
|
${MGL_ROOT}/3rdparty/SPIRV-Reflect
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(FieldOwnershipTest PRIVATE
|
||||||
|
GTest::gtest
|
||||||
|
${LINK_LIBRARIES}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (MSVC)
|
||||||
|
target_compile_options(FieldOwnershipTest PRIVATE /Zc:preprocessor)
|
||||||
|
endif()
|
||||||
|
|
||||||
include(GoogleTest)
|
include(GoogleTest)
|
||||||
gtest_discover_tests(PipeCatalogueTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
gtest_discover_tests(PipeCatalogueTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||||
|
gtest_discover_tests(FieldOwnershipTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||||
foreach(pipeTest ResourceEmitTest VertexInputEmitTest
|
foreach(pipeTest ResourceEmitTest VertexInputEmitTest
|
||||||
FramebufferEmitTest TextureEmitTest SamplerEmitTest ImageEmitTest
|
FramebufferEmitTest TextureEmitTest SamplerEmitTest ImageEmitTest
|
||||||
ProgramEmitTest CompositeResolverTest)
|
ProgramEmitTest CompositeResolverTest)
|
||||||
|
|||||||
@@ -0,0 +1,540 @@
|
|||||||
|
// 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
|
||||||
|
// BARRIER-PULLED read is counted rather than fatal, that MOBILEGL_IPC_STRICT_ERRORS=1 turns
|
||||||
|
// it into a named abort, and that the seven sticky forwards' poison exemption is cancelled.
|
||||||
|
//
|
||||||
|
// 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 <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() {
|
||||||
|
std::ifstream in(g_logPath, std::ios::binary);
|
||||||
|
std::ostringstream ss;
|
||||||
|
ss << in.rdbuf();
|
||||||
|
return ss.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();
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
void TearDown() override {
|
||||||
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||||
|
MGPipeServerClearVerbBoundary();
|
||||||
|
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, VerbBoundaryOpsAreTheFourTheStampRuleNames) {
|
||||||
|
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): 31 of the 63 fields
|
||||||
|
// are served by NO pushed record - 24 non-sticky plus the seven sticky - so 32 are.
|
||||||
|
EXPECT_EQ(kMGPipeRecordSuppliedFieldCount, SizeT{32});
|
||||||
|
EXPECT_EQ(kMGPipeApplierDerivedFieldCount + kMGPipeBarrierPulledFieldCount + kMGPipeFatalFieldCount,
|
||||||
|
SizeT{31});
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The 21 the reduced path actually reads (scout-unmigrated-census section 3: the union of
|
||||||
|
// kClear's 7, kDraw's 19 and kReadback's 12). Twenty of them are BARRIER-PULLED; the
|
||||||
|
// twenty-first is GetPixelStoreParameters, whose PACK half the applier writes and whose UNPACK
|
||||||
|
// half has no carrier and no backend reader at all.
|
||||||
|
TEST_F(FieldOwnershipTest, TheReducedPathsUnmigratedFieldsAreAllAccountedFor) {
|
||||||
|
const MGPipeInputField pulled[] = {
|
||||||
|
MGPipeInputField::GetActiveTextureUnit,
|
||||||
|
MGPipeInputField::GetBoundVertexArray,
|
||||||
|
MGPipeInputField::GetBufferBindingSlot,
|
||||||
|
MGPipeInputField::GetBufferBindingPoint,
|
||||||
|
MGPipeInputField::GetTouchedBufferBindingPointCount,
|
||||||
|
MGPipeInputField::GetCurrentVertexAttribute,
|
||||||
|
MGPipeInputField::GetFramebufferBindingSlot,
|
||||||
|
MGPipeInputField::GetImageTextureBinding,
|
||||||
|
MGPipeInputField::GetMaxTouchedTextureUnit,
|
||||||
|
MGPipeInputField::GetProgramForDraw,
|
||||||
|
MGPipeInputField::GetSamplingResolutionGeneration,
|
||||||
|
MGPipeInputField::GetTextureBindGeneration,
|
||||||
|
MGPipeInputField::GetTextureContextId,
|
||||||
|
MGPipeInputField::GetTextureUnitObject,
|
||||||
|
MGPipeInputField::GetTransformFeedbackCapturedVertices,
|
||||||
|
MGPipeInputField::GetTransformFeedbackGeneration,
|
||||||
|
MGPipeInputField::GetTransformFeedbackProgram,
|
||||||
|
MGPipeInputField::GetBoundTransformFeedbackLifetimeId,
|
||||||
|
MGPipeInputField::IsTransformFeedbackActive,
|
||||||
|
MGPipeInputField::IsTransformFeedbackPaused,
|
||||||
|
};
|
||||||
|
for (const auto field : pulled) {
|
||||||
|
EXPECT_EQ(MGPipeFieldOwnershipOf(field), MGPipeFieldOwnership::kBarrierPulled)
|
||||||
|
<< kMGPipeInputFieldNames[Index(field)] << " left the reduced path's debt";
|
||||||
|
}
|
||||||
|
EXPECT_EQ(MGPipeFieldOwnershipOf(MGPipeInputField::GetPixelStoreParameters),
|
||||||
|
MGPipeFieldOwnership::kApplierDerived);
|
||||||
|
EXPECT_EQ(MGPipeFieldOwnershipOf(MGPipeInputField::GetPixelStoreParameters, 0u),
|
||||||
|
MGPipeFieldOwnership::kApplierDerived);
|
||||||
|
EXPECT_EQ(MGPipeFieldOwnershipOf(MGPipeInputField::GetPixelStoreParameters, 1u),
|
||||||
|
MGPipeFieldOwnership::kFatal);
|
||||||
|
|
||||||
|
// The three off the reduced path, each for its own checkable reason.
|
||||||
|
EXPECT_EQ(MGPipeFieldOwnershipOf(MGPipeInputField::GetBoundTransformFeedbackName),
|
||||||
|
MGPipeFieldOwnership::kFatal);
|
||||||
|
EXPECT_EQ(MGPipeFieldOwnershipOf(MGPipeInputField::GetTransformFeedbackPausedPrimitiveCounter),
|
||||||
|
MGPipeFieldOwnership::kFatal);
|
||||||
|
EXPECT_EQ(MGPipeFieldOwnershipOf(MGPipeInputField::GetProgramForDispatch),
|
||||||
|
MGPipeFieldOwnership::kFatal);
|
||||||
|
}
|
||||||
|
|
||||||
|
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], "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The stamp map, and the one slot of class B that is deliberately NOT in it.
|
||||||
|
TEST_F(FieldOwnershipTest, VerbBoundaryOpsAreTheFourTheStampRuleNames) {
|
||||||
|
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);
|
||||||
|
EXPECT_EQ(kMGPipeVerbBoundaryOpCount, SizeT{4});
|
||||||
|
// 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::SetDynamicState), MGPipeVerb::kVerbCount);
|
||||||
|
EXPECT_EQ(MGPipeVerbForWireOp(MGPWireOp::GetCaps), 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.
|
||||||
|
// ---------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#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});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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];
|
||||||
|
EXPECT_FALSE(MGPipeInputFieldIsFresh(gPipeInputs.FilledState(), field))
|
||||||
|
<< kMGPipeInputFieldNames[Index(field)] << " is still exempt under split";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
MGPipeServerStampVerbBoundary(MGPipeVerb::ReadPixels);
|
||||||
|
(void)gPipeInputs.GetPixelStoreParameters(false); // the half that has a carrier
|
||||||
|
EXPECT_EQ(MGPipeResidualPullCount(), Uint64{0});
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(FieldOwnershipTest, ABarrierPulledReadAfterAServerStampIsCountedNotFatal) {
|
||||||
|
MGPipeServerStampVerbBoundary(MGPipeVerb::ReadPixels);
|
||||||
|
ASSERT_EQ(MGPipeResidualPullCount(), Uint64{0});
|
||||||
|
(void)gPipeInputs.GetActiveTextureUnit();
|
||||||
|
EXPECT_EQ(MGPipeResidualPullCount(), Uint64{1});
|
||||||
|
(void)gPipeInputs.GetTextureContextId();
|
||||||
|
(void)gPipeInputs.GetMaxTouchedTextureUnit();
|
||||||
|
EXPECT_EQ(MGPipeResidualPullCount(), Uint64{3});
|
||||||
|
}
|
||||||
|
|
||||||
|
// The seven carry no MGP_INPUT_CHECK, so freshness can never reach them; this is the only
|
||||||
|
// thing that puts them in `rsp`.
|
||||||
|
TEST_F(FieldOwnershipTest, AStickyForwardIsCountedAsAResidualPull) {
|
||||||
|
MGPipeServerStampVerbBoundary(MGPipeVerb::Clear);
|
||||||
|
ASSERT_EQ(MGPipeResidualPullCount(), Uint64{0});
|
||||||
|
(void)gPipeInputs.ValidateProgramName(1u);
|
||||||
|
(void)gPipeInputs.GetProgramObject(1u);
|
||||||
|
EXPECT_EQ(MGPipeResidualPullCount(), Uint64{2});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ... 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 verb's own may-read table still holds on the server: kClear does not read
|
||||||
|
// GetActiveTextureUnit, so reading it there is a stale answer rather than a residual pull,
|
||||||
|
// and it stays Fatal. Counting it would trade a loud staleness for a quiet one.
|
||||||
|
TEST_F(FieldOwnershipTest, ABarrierPulledFieldOutsideTheVerbsClassIsStillFatal) {
|
||||||
|
#if MGTEST_HAVE_FORK
|
||||||
|
const ChildResult r = RunInChild([] {
|
||||||
|
MGPipeServerStampVerbBoundary(MGPipeVerb::Clear);
|
||||||
|
(void)gPipeInputs.GetActiveTextureUnit(); // BARRIER-PULLED, but not in kClear's class
|
||||||
|
});
|
||||||
|
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
|
||||||
|
EXPECT_NE(r.Log.find("Fatal{UnmigratedPipeInput, \"GetActiveTextureUnit@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::ReadPixels);
|
||||||
|
(void)gPipeInputs.GetActiveTextureUnit();
|
||||||
|
EXPECT_EQ(PS::FrameCalls(PS::CallClass::ResidualPulls), Uint64{1});
|
||||||
|
EXPECT_NE(PS::FormatWindowLine().find("rsp="), std::string::npos) << PS::FormatWindowLine();
|
||||||
|
PS::ResetForTesting();
|
||||||
|
PS::SetEnabledForTesting(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
#if MGTEST_HAVE_FORK
|
||||||
|
|
||||||
|
// R-7.3's proof that the instrumentation can go red. An instrumentation that cannot is
|
||||||
|
// decoration, and the set it counts is not empty.
|
||||||
|
TEST_F(FieldOwnershipTest, StrictErrorsTurnsABarrierPulledReadIntoANamedAbort) {
|
||||||
|
const ChildResult r = RunInChild([] {
|
||||||
|
MG_Config::Ipc.StrictErrors = true;
|
||||||
|
MGPipeServerStampVerbBoundary(MGPipeVerb::ReadPixels);
|
||||||
|
(void)gPipeInputs.GetActiveTextureUnit();
|
||||||
|
});
|
||||||
|
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
|
||||||
|
EXPECT_NE(r.Log.find("Fatal{UnmigratedPipeInput, \"GetActiveTextureUnit@ReadPixels\"}"),
|
||||||
|
std::string::npos)
|
||||||
|
<< r.Log;
|
||||||
|
EXPECT_NE(r.Log.find("BARRIER-PULLED"), std::string::npos) << r.Log;
|
||||||
|
EXPECT_NE(r.Log.find("MOBILEGL_IPC_STRICT_ERRORS=1"), std::string::npos) << r.Log;
|
||||||
|
// The strict line names the phase that owes the answer; a strict abort that did not would
|
||||||
|
// leave the reader exactly where the gate found them.
|
||||||
|
EXPECT_NE(r.Log.find("retires in P3b/P4b"), std::string::npos) << r.Log;
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(FieldOwnershipTest, TheSameReadWithoutStrictErrorsSurvivesAndIsCounted) {
|
||||||
|
const ChildResult r = RunInChild([] {
|
||||||
|
MGPipeServerStampVerbBoundary(MGPipeVerb::ReadPixels);
|
||||||
|
(void)gPipeInputs.GetActiveTextureUnit();
|
||||||
|
if (MGPipeResidualPullCount() != 1) ::_exit(7);
|
||||||
|
});
|
||||||
|
ASSERT_TRUE(ExitedWith(r, 0)) << DescribeStatus(r) << "\n" << r.Log;
|
||||||
|
EXPECT_EQ(r.Log.find("Fatal{"), std::string::npos) << r.Log;
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(FieldOwnershipTest, StrictErrorsAlsoPromotesTheStickyForwards) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A FATAL-class read aborts whatever the knob says: no carrier, and the reduced path never
|
||||||
|
// reads it, so it is a real defect rather than a debt.
|
||||||
|
TEST_F(FieldOwnershipTest, AFatalClassReadAbortsEvenWithoutStrictErrors) {
|
||||||
|
const ChildResult r = RunInChild([] {
|
||||||
|
MGPipeServerStampVerbBoundary(MGPipeVerb::DrawArrays);
|
||||||
|
(void)gPipeInputs.GetProgramForDispatch();
|
||||||
|
});
|
||||||
|
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
|
||||||
|
EXPECT_NE(r.Log.find("Fatal{UnmigratedPipeInput, \"GetProgramForDispatch@DrawArrays\"}"),
|
||||||
|
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;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
#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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user