mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-17 00:28:31 +09:00
1769 lines
78 KiB
C++
1769 lines
78 KiB
C++
// MobileGL - MobileGL/MG_Test/Wire/PipeWireCodecTest.cpp
|
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
|
// Licensed under the GNU Lesser General Public License v3.0:
|
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
|
// SPDX-License-Identifier: LGPL-3.0-only
|
|
// End of Source File Header
|
|
|
|
// P5 package w1's suite: encoder -> SEG_CMD -> RingConsumer -> decoder -> the real
|
|
// MGPipeApply* free functions, with no session, no transport and no thread. s1 owns the
|
|
// session; this file owns the bytes.
|
|
//
|
|
// It links gtest rather than gtest_main and carries its own main(), for PipeInputsTest's
|
|
// reason: the R-2 arms report through MGLOG_F + std::abort, so a case that drives one FORKS
|
|
// and reads the Fatal line back out of a log file this process names before anything logs.
|
|
// NEVER EXPECT_DEATH - it re-runs the whole binary and would re-enter the applier's globals.
|
|
//
|
|
// WHAT A "ROUND TRIP" MEANS HERE. The decoder implements no semantics, so a case cannot
|
|
// assert on rendering; what it asserts is that the record crossed intact and that the arm
|
|
// reached the right consumer with the right arguments. For the five class-B verbs that is a
|
|
// recording WireVerbSink; for everything else it is the real applier, whose acceptance return
|
|
// comes back through the recording ReplySink on the record's own seq (R-3/R-5).
|
|
|
|
#include <gtest/gtest.h>
|
|
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <sstream>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include "Includes.h"
|
|
|
|
#include <MG_Pipe/MGPipe.h>
|
|
#include <MG_Pipe/MGPipeRenderStateSpans.h>
|
|
#include <MG_Pipe/PipeApply.h>
|
|
#include <MG_Remote/CapsCodec.h>
|
|
#include <MG_Remote/Transport/Ring.h>
|
|
#include <MG_Remote/Wire/PipeWireCodec.h>
|
|
#include <MG_State/GLState/ProgramState/ProgramArtifactsCodec.h>
|
|
|
|
#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;
|
|
using namespace MobileGL::MG_Remote;
|
|
using namespace MobileGL::MG_Remote::Wire;
|
|
namespace Transport = MobileGL::MG_Remote::Transport;
|
|
|
|
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
|
|
}
|
|
|
|
// ---- the fixture ----------------------------------------------------------------
|
|
//
|
|
// Two rings over two byte arrays plus a SegmentTable that covers exactly those arrays.
|
|
// "Exactly" is load-bearing: StageBytes asserts that the SEG_STAGE view resolves a staged
|
|
// run to the same address the producer wrote it at, so a view installed over the wrong
|
|
// base is a Fatal rather than a plausible pointer.
|
|
class Wire2 {
|
|
public:
|
|
static constexpr std::uint64_t kCmdBytes = 64 * 1024;
|
|
static constexpr std::uint64_t kStageBytes = 256 * 1024;
|
|
|
|
Wire2() : m_cmdBytes(kCmdBytes), m_stageBytes(kStageBytes) {
|
|
Transport::InitRingControl(m_control);
|
|
m_cmd = Transport::RingProducer(&m_control, m_cmdBytes.data(), kCmdBytes,
|
|
Transport::RingCursorSet::Cmd);
|
|
m_stage = Transport::RingProducer(&m_control, m_stageBytes.data(), kStageBytes,
|
|
Transport::RingCursorSet::Stage);
|
|
m_consumer = Transport::RingConsumer(&m_control, m_cmdBytes.data(), kCmdBytes,
|
|
Transport::RingCursorSet::Cmd);
|
|
m_segments.Install(kSegCmd, SegmentView{m_cmdBytes.data(), kCmdBytes});
|
|
m_segments.Install(kSegStage, SegmentView{m_stageBytes.data(), kStageBytes});
|
|
m_encoder = PipeWireEncoder(&m_control, &m_cmd, &m_stage, &m_segments);
|
|
m_decoder = PipeWireDecoder(&m_control, &m_segments, &m_replies);
|
|
m_decoder.SetVerbSink(&m_verbs);
|
|
}
|
|
|
|
PipeWireEncoder& Encoder() { return m_encoder; }
|
|
PipeWireDecoder& Decoder() { return m_decoder; }
|
|
SegmentTable& Segments() { return m_segments; }
|
|
Transport::RingControl& Control() { return m_control; }
|
|
Transport::RingProducer& Cmd() { return m_cmd; }
|
|
Transport::RingConsumer& Consumer() { return m_consumer; }
|
|
std::uint8_t* StageBase() { return m_stageBytes.data(); }
|
|
|
|
// Pops one record, decodes it, and then does what s1's SessionConsumer::ApplyOne does:
|
|
// advance RingControl's watermarks by ONE. THE DECODER DOES NOT WRITE RingControl -
|
|
// appliedSeq has exactly one writer and it is the session - so this fixture has to
|
|
// play that role, which is also what lets a case compare the session's watermark
|
|
// against the decoder's own tally and catch a batched publish.
|
|
//
|
|
// Returns whether there was a record at all, so a case cannot pass because nothing was
|
|
// there; `applied` is what the decoder reported.
|
|
bool PumpOne(bool* applied) {
|
|
m_encoder.Publish();
|
|
Transport::RingRecordView view{};
|
|
bool corrupt = false;
|
|
if (!m_consumer.Pop(view, &corrupt)) {
|
|
return false;
|
|
}
|
|
if (corrupt) {
|
|
return false;
|
|
}
|
|
const bool result = m_decoder.DecodeAndApply(view);
|
|
++m_sessionApplied;
|
|
m_control.appliedSeq.store(m_sessionApplied, std::memory_order_release);
|
|
// Nothing in P5 borrows a ring slot into the GPU timeline, so a record's SEG_STAGE
|
|
// runs retire as soon as it is applied (table 1's "retires: apply").
|
|
m_control.retiredSeq.store(m_sessionApplied, std::memory_order_release);
|
|
m_consumer.PublishRetired();
|
|
if (applied != nullptr) {
|
|
*applied = result;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
std::uint64_t SessionAppliedSeq() const { return m_sessionApplied; }
|
|
|
|
// ---- recorded answers ----
|
|
struct Reply {
|
|
std::uint64_t Seq = 0;
|
|
std::int32_t Status = 0;
|
|
std::vector<std::uint8_t> Bytes;
|
|
};
|
|
|
|
class Replies : public ReplySink {
|
|
public:
|
|
void PostReply(Uint64 seq, Int32 status, const void* bytes, Uint64 size) override {
|
|
Reply r;
|
|
r.Seq = seq;
|
|
r.Status = status;
|
|
if (bytes != nullptr && size != 0) {
|
|
const auto* p = static_cast<const std::uint8_t*>(bytes);
|
|
r.Bytes.assign(p, p + size);
|
|
}
|
|
All.push_back(std::move(r));
|
|
}
|
|
std::vector<Reply> All;
|
|
};
|
|
|
|
class Verbs : public WireVerbSink {
|
|
public:
|
|
Bool OnClear(const MGPClear& clear) override {
|
|
Clears.push_back(clear);
|
|
return true;
|
|
}
|
|
Bool OnBlit(const MGPBlit& blit) override {
|
|
Blits.push_back(blit);
|
|
return true;
|
|
}
|
|
Bool OnPresent(const MGPPresent& present) override {
|
|
Presents.push_back(present);
|
|
return true;
|
|
}
|
|
Bool OnReadPixels(const MGPReadbackInfo& info, Uint64 seq, ReplySink* replies) override {
|
|
Readbacks.push_back(info);
|
|
ReadbackSeqs.push_back(seq);
|
|
if (replies != nullptr) {
|
|
const std::uint8_t pixels[4] = {1, 2, 3, 4};
|
|
replies->PostReply(seq, ReplySink::kStatusOk, pixels, sizeof(pixels));
|
|
}
|
|
return true;
|
|
}
|
|
Bool OnDrawVbo(const MGPDrawInfo& info, const MGPDrawRange* ranges,
|
|
const MGHostSpan* userIndices) override {
|
|
Draws.push_back(info);
|
|
DrawRanges.clear();
|
|
for (Uint32 i = 0; i < info.NumDraws; ++i) {
|
|
DrawRanges.push_back(ranges[i]);
|
|
}
|
|
SawUserIndices = userIndices != nullptr;
|
|
if (userIndices != nullptr) {
|
|
LastSpan = *userIndices;
|
|
}
|
|
return true;
|
|
}
|
|
std::vector<MGPClear> Clears;
|
|
std::vector<MGPBlit> Blits;
|
|
std::vector<MGPPresent> Presents;
|
|
std::vector<MGPReadbackInfo> Readbacks;
|
|
std::vector<Uint64> ReadbackSeqs;
|
|
std::vector<MGPDrawInfo> Draws;
|
|
std::vector<MGPDrawRange> DrawRanges;
|
|
bool SawUserIndices = false;
|
|
MGHostSpan LastSpan{};
|
|
};
|
|
|
|
Replies& Answers() { return m_replies; }
|
|
Verbs& Sink() { return m_verbs; }
|
|
|
|
private:
|
|
Transport::RingControl m_control{};
|
|
std::vector<std::uint8_t> m_cmdBytes;
|
|
std::vector<std::uint8_t> m_stageBytes;
|
|
Transport::RingProducer m_cmd;
|
|
Transport::RingProducer m_stage;
|
|
Transport::RingConsumer m_consumer;
|
|
SegmentTable m_segments;
|
|
PipeWireEncoder m_encoder;
|
|
PipeWireDecoder m_decoder;
|
|
Replies m_replies;
|
|
Verbs m_verbs;
|
|
std::uint64_t m_sessionApplied = 0;
|
|
};
|
|
|
|
MGPipeHandle MakeHandle(Uint32 slot, Uint32 gen = 1) {
|
|
MGPipeHandle handle{};
|
|
handle.Slot = slot;
|
|
handle.Gen = gen;
|
|
return handle;
|
|
}
|
|
|
|
MGPHandleOnly HandleOnly(Uint32 slot, MGPipeKind kind) {
|
|
MGPHandleOnly record{};
|
|
record.Handle = MakeHandle(slot);
|
|
record.Kind = static_cast<Uint32>(kind);
|
|
return record;
|
|
}
|
|
|
|
class PipeWireCodecTest : public ::testing::Test {
|
|
protected:
|
|
void SetUp() override { MGPipeApplierReleaseObjectRecords(); }
|
|
void TearDown() override { MGPipeApplierReleaseObjectRecords(); }
|
|
};
|
|
|
|
#if MGTEST_HAVE_FORK
|
|
struct ChildResult {
|
|
int Status = -1;
|
|
std::string Log;
|
|
};
|
|
|
|
// Runs `body` in a forked child. The child must not use gtest assertions; it _exit(0)s
|
|
// when `body` returns, so a body expected to die must be ASSERTED dead by the parent
|
|
// (WIFSIGNALED), never assumed.
|
|
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;
|
|
}
|
|
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);
|
|
}
|
|
|
|
// Forges ONE record straight into SEG_CMD, bypassing the encoder. Every R-2 arm needs
|
|
// this: the encoder REFUSES to build a dishonest record, which is the point of it, so a
|
|
// case that drove the encoder could only ever test the encoder's own check.
|
|
void ForgeAndDecode(Wire2& wire, MGPWireOp op, const void* payload, std::uint64_t payloadBytes,
|
|
const void* tail, std::uint64_t tailBytes) {
|
|
const std::uint64_t body = payloadBytes + tailBytes;
|
|
void* slot = wire.Cmd().Reserve(static_cast<std::uint16_t>(op), Transport::kRecNone, body);
|
|
if (slot == nullptr) {
|
|
std::_Exit(9);
|
|
}
|
|
std::memcpy(slot, payload, static_cast<std::size_t>(payloadBytes));
|
|
if (tailBytes != 0) {
|
|
std::memcpy(static_cast<std::uint8_t*>(slot) + payloadBytes, tail,
|
|
static_cast<std::size_t>(tailBytes));
|
|
}
|
|
wire.Cmd().Publish();
|
|
Transport::RingRecordView view{};
|
|
bool corrupt = false;
|
|
if (!wire.Consumer().Pop(view, &corrupt) || corrupt) {
|
|
std::_Exit(10);
|
|
}
|
|
(void)wire.Decoder().DecodeAndApply(view);
|
|
}
|
|
#endif // MGTEST_HAVE_FORK
|
|
|
|
} // namespace
|
|
|
|
// =====================================================================================
|
|
// Segment table and staging
|
|
// =====================================================================================
|
|
|
|
TEST_F(PipeWireCodecTest, SegmentZeroIsNeverARealSegment) {
|
|
SegmentTable table;
|
|
std::vector<std::uint8_t> bytes(256);
|
|
table.Install(kSegStage, SegmentView{bytes.data(), bytes.size()});
|
|
EXPECT_EQ(table.Resolve(kSegNone, 0, 8), nullptr);
|
|
EXPECT_EQ(table.Get(kSegNone).Base, nullptr);
|
|
// P8's index-mirror sentinel is reserved, not resolvable in this phase.
|
|
EXPECT_EQ(table.Resolve(kMGHostSpanSegFromServerIndexMirror, 0, 8), nullptr);
|
|
}
|
|
|
|
TEST_F(PipeWireCodecTest, ResolveRefusesEveryRunThatLeavesItsSegment) {
|
|
SegmentTable table;
|
|
std::vector<std::uint8_t> bytes(256);
|
|
table.Install(kSegStage, SegmentView{bytes.data(), bytes.size()});
|
|
EXPECT_EQ(table.Resolve(kSegStage, 0, 256), bytes.data());
|
|
EXPECT_EQ(table.Resolve(kSegStage, 248, 8), bytes.data() + 248);
|
|
EXPECT_EQ(table.Resolve(kSegStage, 249, 8), nullptr);
|
|
EXPECT_EQ(table.Resolve(kSegStage, 256, 1), nullptr);
|
|
EXPECT_EQ(table.Resolve(kSegStage, 0, 0), nullptr);
|
|
// The arithmetic is a subtraction, so an offset that would wrap offset+size cannot come
|
|
// back as "inside".
|
|
EXPECT_EQ(table.Resolve(kSegStage, 0xFFFFFFFFFFFFFFF0ull, 32), nullptr);
|
|
}
|
|
|
|
TEST_F(PipeWireCodecTest, StagedBytesResolveBackToTheSameAddress) {
|
|
Wire2 wire;
|
|
const std::uint8_t pattern[37] = {0};
|
|
std::uint8_t source[37];
|
|
for (std::size_t i = 0; i < sizeof(source); ++i) {
|
|
source[i] = static_cast<std::uint8_t>(0xA0 + i);
|
|
}
|
|
(void)pattern;
|
|
const MGPBlobRef ref = wire.Encoder().StageBytes(source, sizeof(source));
|
|
EXPECT_EQ(ref.Seg, static_cast<Uint32>(kSegStage));
|
|
EXPECT_EQ(ref.Size, sizeof(source));
|
|
const void* back = wire.Segments().Resolve(ref.Seg, ref.Offset, ref.Size);
|
|
ASSERT_NE(back, nullptr);
|
|
EXPECT_EQ(std::memcmp(back, source, sizeof(source)), 0);
|
|
}
|
|
|
|
// =====================================================================================
|
|
// THE TRAP: two flag spaces in one 16-bit field
|
|
// =====================================================================================
|
|
|
|
TEST_F(PipeWireCodecTest, AVarTailRecordIsNotSkippedAsAWrapFiller) {
|
|
// MGPipeCallFlags::kVarTail is 1<<2 and RingRecordFlags::kRecPad is 1<<2, and
|
|
// MGPWireRecHeader::Flags IS RingRecordHeader::flags. An encoder that stamped the call's
|
|
// own flags - which the generated comment invites - would make RingConsumer::Pop skip
|
|
// every one of the nine kVarTail records as a wrap filler, silently, with no checksum
|
|
// anywhere on this ring. This case is the trip wire for that.
|
|
static_assert(static_cast<Uint16>(kVarTail) == static_cast<Uint16>(Transport::kRecPad),
|
|
"the collision this case exists for is gone; keep the case anyway");
|
|
// And the third one, which the first round missed and a reviewer found: it bites in the
|
|
// REVERSE direction, because the encoder stamps kRecVarTail on nine opcodes and anyone
|
|
// trusting PipeWire.inc's "MGPipeCallFlags of the call" reads those nine as kReplySlot.
|
|
static_assert(static_cast<Uint16>(kReplySlot) == static_cast<Uint16>(Transport::kRecVarTail),
|
|
"kReplySlot and kRecVarTail alias");
|
|
// Five of the six call-flag bits alias a ring bit; only kOptional is free, and only
|
|
// because RingRecordFlags has not reached 1<<5.
|
|
static_assert((static_cast<Uint16>(kNeedsAck | kHasBlob | kVarTail | kHostSpan | kReplySlot |
|
|
kOptional) &
|
|
static_cast<Uint16>(Transport::kRecNeedsAck | Transport::kRecHasBlob |
|
|
Transport::kRecPad | Transport::kRecBorrowSlot |
|
|
Transport::kRecVarTail)) == 0x1Fu,
|
|
"the overlap between the two flag spaces moved");
|
|
|
|
Wire2 wire;
|
|
MGPVertexBuffers header{};
|
|
header.Start = 0;
|
|
header.Count = 2;
|
|
header.ContentHash = 0x1234;
|
|
MGPVertexBuffer tail[2]{};
|
|
tail[0].Res = MakeHandle(11);
|
|
tail[0].Stride = 16;
|
|
tail[1].Res = MakeHandle(12);
|
|
tail[1].Stride = 32;
|
|
|
|
ASSERT_NE(wire.Encoder().EncodeRecord(MGPWireOp::SetVertexBuffers, &header, sizeof(header), tail,
|
|
sizeof(tail)),
|
|
kInvalidSeq);
|
|
wire.Encoder().Publish();
|
|
|
|
Transport::RingRecordView view{};
|
|
bool corrupt = false;
|
|
ASSERT_TRUE(wire.Consumer().Pop(view, &corrupt)) << "the record was skipped as a pad";
|
|
EXPECT_FALSE(corrupt);
|
|
EXPECT_EQ(view.kind, static_cast<std::uint16_t>(MGPWireOp::SetVertexBuffers));
|
|
EXPECT_EQ(view.flags & Transport::kRecPad, 0u);
|
|
EXPECT_NE(view.flags & Transport::kRecVarTail, 0u);
|
|
}
|
|
|
|
// =====================================================================================
|
|
// One round trip per flag class
|
|
// =====================================================================================
|
|
|
|
TEST_F(PipeWireCodecTest, KNoneRoundTripsAndReachesItsApplier) {
|
|
Wire2 wire;
|
|
MGPBindRenderState bind{};
|
|
bind.Cso = MakeHandle(4);
|
|
bind.Version = 7;
|
|
bind.PipelineVersion = 3;
|
|
ASSERT_NE(wire.Encoder().EncodeRecord(MGPWireOp::BindRenderState, &bind, sizeof(bind)),
|
|
kInvalidSeq);
|
|
bool applied = false;
|
|
ASSERT_TRUE(wire.PumpOne(&applied));
|
|
EXPECT_TRUE(applied);
|
|
EXPECT_EQ(wire.Decoder().AppliedSeq(), 1u);
|
|
EXPECT_EQ(wire.Control().appliedSeq.load(), 1u);
|
|
EXPECT_EQ(wire.Control().retiredSeq.load(), 1u);
|
|
}
|
|
|
|
TEST_F(PipeWireCodecTest, KHasBlobRoundTripsWithARealChunkBlob) {
|
|
Wire2 wire;
|
|
// A brand-new CSO must name EVERY pipeline chunk - the applier refuses an incremental
|
|
// create with no BaseCso (Fatal{PipeIncompleteCso}) - so this is the whole half.
|
|
const Uint32 mask = MGPipeRenderStateChunkDetail::kAllPipelineHalfBits;
|
|
const SizeT blobBytes = MGPipePipelineChunkBlobBytes(mask);
|
|
ASSERT_GT(blobBytes, 0u);
|
|
std::vector<std::uint8_t> chunks(blobBytes);
|
|
for (std::size_t i = 0; i < chunks.size(); ++i) {
|
|
chunks[i] = static_cast<std::uint8_t>(i * 7 + 1);
|
|
}
|
|
|
|
MGPRenderStateDesc desc{};
|
|
desc.Cso = MakeHandle(9);
|
|
desc.ChunkMask = mask;
|
|
desc.Blob = wire.Encoder().StageBytes(chunks.data(), chunks.size());
|
|
// R-2.2 in one line: what a monolith emission writes is Size 0, and what crosses must not
|
|
// be.
|
|
EXPECT_NE(desc.Blob.Size, 0u);
|
|
EXPECT_EQ(desc.Blob.Seg, static_cast<Uint32>(kSegStage));
|
|
|
|
ASSERT_NE(wire.Encoder().EncodeRecord(MGPWireOp::CreateRenderState, &desc, sizeof(desc)),
|
|
kInvalidSeq);
|
|
bool applied = false;
|
|
ASSERT_TRUE(wire.PumpOne(&applied));
|
|
EXPECT_TRUE(applied);
|
|
}
|
|
|
|
TEST_F(PipeWireCodecTest, KVarTailRoundTripsWithItsTailIntact) {
|
|
Wire2 wire;
|
|
MGPSamplerViews header{};
|
|
header.Start = 3;
|
|
header.Count = 4;
|
|
header.ContentHash = 99;
|
|
MGPBoundView tail[4]{};
|
|
for (Uint32 i = 0; i < 4; ++i) {
|
|
tail[i].View = MakeHandle(100 + i);
|
|
tail[i].Texture = MakeHandle(200 + i);
|
|
tail[i].Unit = 3 + i;
|
|
}
|
|
ASSERT_NE(wire.Encoder().EncodeRecord(MGPWireOp::SetSamplerViews, &header, sizeof(header), tail,
|
|
sizeof(tail)),
|
|
kInvalidSeq);
|
|
bool applied = false;
|
|
ASSERT_TRUE(wire.PumpOne(&applied));
|
|
EXPECT_TRUE(applied);
|
|
}
|
|
|
|
TEST_F(PipeWireCodecTest, AnEmptyVarTailIsALegalRecordAndNeedsNoPlaceholderEntry) {
|
|
// Count == 0 is legal for every kVarTail row - "bind nothing at this range" - and a caller
|
|
// that had to pass a {nullptr, 0} entry for each absent tail would be walking into a trap
|
|
// rather than through a check. SetStreamOutputTargets is the sharpest case: its layout has
|
|
// TWO tails and both are empty at Count 0.
|
|
Wire2 wire;
|
|
MGPStreamOutputTargets header{};
|
|
header.Count = 0;
|
|
ASSERT_NE(wire.Encoder().EncodeRecord(MGPWireOp::SetStreamOutputTargets, &header, sizeof(header)),
|
|
kInvalidSeq);
|
|
MGPVertexBuffers buffers{};
|
|
buffers.Count = 0;
|
|
ASSERT_NE(wire.Encoder().EncodeRecord(MGPWireOp::SetVertexBuffers, &buffers, sizeof(buffers)),
|
|
kInvalidSeq);
|
|
bool applied = false;
|
|
ASSERT_TRUE(wire.PumpOne(&applied));
|
|
EXPECT_FALSE(applied); // no applier for stream output; off the reduced path
|
|
ASSERT_TRUE(wire.PumpOne(&applied));
|
|
EXPECT_TRUE(applied);
|
|
}
|
|
|
|
TEST_F(PipeWireCodecTest, KReplySlotMapPersistentIsAConstantDecline) {
|
|
// R-6 / R-2.4. DECLINED is a real answer, not a failure, and the applier is not called at
|
|
// all: the record's payload is a bare MGPHandleOnly and carries NEITHER the size NOR the
|
|
// seedBytes MGPipeApplyMapPersistent takes.
|
|
Wire2 wire;
|
|
const MGPHandleOnly handle = HandleOnly(5, MGPipeKind::Buffer);
|
|
ASSERT_NE(wire.Encoder().EncodeRecord(MGPWireOp::MapPersistent, &handle, sizeof(handle)),
|
|
kInvalidSeq);
|
|
bool applied = false;
|
|
ASSERT_TRUE(wire.PumpOne(&applied));
|
|
EXPECT_TRUE(applied);
|
|
ASSERT_EQ(wire.Answers().All.size(), 1u);
|
|
EXPECT_EQ(wire.Answers().All[0].Seq, 1u);
|
|
EXPECT_EQ(wire.Answers().All[0].Status, ReplySink::kStatusDeclined);
|
|
EXPECT_TRUE(wire.Answers().All[0].Bytes.empty());
|
|
}
|
|
|
|
TEST_F(PipeWireCodecTest, KNeedsAckRespecifyCarriesItsRedefinitionScope) {
|
|
// Contract table 1 row 19b. Without the carrier every per-level glTexImage*D would take
|
|
// the whole-resource arm on the far side and eat the other levels' pending uploads, so
|
|
// this case is about the SCOPE surviving, not about the descriptor.
|
|
Wire2 wire;
|
|
MGPResourceDesc create{};
|
|
create.Resource = MakeHandle(21);
|
|
create.Target = static_cast<Uint8>(MGPipeResourceTarget::Tex2D);
|
|
create.InternalFormat = 1;
|
|
create.Width = 8;
|
|
create.Height = 8;
|
|
create.Depth = 1;
|
|
create.ArrayLayers = 1;
|
|
create.Levels = 2;
|
|
create.Samples = 1;
|
|
ASSERT_NE(wire.Encoder().EncodeRecord(MGPWireOp::ResourceCreate, &create, sizeof(create)),
|
|
kInvalidSeq);
|
|
bool applied = false;
|
|
ASSERT_TRUE(wire.PumpOne(&applied));
|
|
|
|
MGPResourceDesc respecify = create;
|
|
MGPipeSetRespecifiedLevel(respecify, 0x0102u, 1u);
|
|
EXPECT_FALSE(MGPipeRespecifyIsWholeResource(respecify));
|
|
ASSERT_NE(wire.Encoder().EncodeRecord(MGPWireOp::ResourceRespecify, &respecify, sizeof(respecify)),
|
|
kInvalidSeq);
|
|
ASSERT_TRUE(wire.PumpOne(&applied));
|
|
EXPECT_TRUE(applied);
|
|
// Two records, two ACCEPTANCE answers - and NOT in a reply slot. Neither ResourceCreate
|
|
// nor ResourceRespecify carries kReplySlot, and s1 sizes ReplyPool from that table, so a
|
|
// reply written here would overwrite some waiter's slot.
|
|
EXPECT_TRUE(wire.Answers().All.empty());
|
|
EXPECT_EQ(wire.Decoder().AcceptedRecords() + wire.Decoder().DeclinedRecords(), 2u);
|
|
EXPECT_TRUE(wire.Decoder().LastAcceptanceKnown());
|
|
}
|
|
|
|
TEST_F(PipeWireCodecTest, KOptionalUnmapPersistentRoundTrips) {
|
|
Wire2 wire;
|
|
const MGPHandleOnly handle = HandleOnly(6, MGPipeKind::Buffer);
|
|
ASSERT_NE(wire.Encoder().EncodeRecord(MGPWireOp::UnmapPersistent, &handle, sizeof(handle)),
|
|
kInvalidSeq);
|
|
bool applied = false;
|
|
ASSERT_TRUE(wire.PumpOne(&applied));
|
|
EXPECT_TRUE(applied);
|
|
}
|
|
|
|
TEST_F(PipeWireCodecTest, KHostSpanClassIsValidatedEvenThoughP5ProducesNone) {
|
|
// kCapNeedsHostUboBytes is 0 for the whole of P5 (table 0), so the second tail is always
|
|
// absent here - and the record still has to be REFUSED if it ever is not honest.
|
|
Wire2 wire;
|
|
MGPShaderBuffers header{};
|
|
header.Class = 0;
|
|
header.Start = 0;
|
|
header.Count = 2;
|
|
header.HostSpanCount = 0;
|
|
MGPBufferRange ranges[2]{};
|
|
ranges[0].Res = MakeHandle(31);
|
|
ranges[0].Size = 64;
|
|
ranges[1].Res = MakeHandle(32);
|
|
ranges[1].Size = 128;
|
|
ASSERT_NE(wire.Encoder().EncodeRecord(MGPWireOp::SetShaderBuffers, &header, sizeof(header),
|
|
ranges, sizeof(ranges)),
|
|
kInvalidSeq);
|
|
bool applied = false;
|
|
ASSERT_TRUE(wire.PumpOne(&applied));
|
|
// No applier entry point exists and the call is off the reduced path, so the honest answer
|
|
// is "this build does not implement it" - after the tails have been checked.
|
|
EXPECT_FALSE(applied);
|
|
}
|
|
|
|
// =====================================================================================
|
|
// Declared padding is payload, not slack
|
|
// =====================================================================================
|
|
|
|
TEST_F(PipeWireCodecTest, EveryPayloadByteCrossesIncludingTheOnesSpelledPad) {
|
|
// A PAD IS A FIELD SOMEBODY HAS NOT CLAIMED YET, and this phase is the proof: P5 put the
|
|
// respecify scope into MGPResourceDesc's two pads (contract table 1 row 19b) and b1 put
|
|
// MGPSubData::Pad0's low byte to work as HasLiveHostWrites. A codec that zeroed a pad "for
|
|
// determinism", or built a payload field by field, would DELETE those bits - and a dropped
|
|
// HasLiveHostWrites is not a visible failure, it is IsBufferDrawClean answering "clean"
|
|
// for a buffer with a live host writer, i.e. the frame drawing the last uploaded bytes
|
|
// with no diagnostic at all.
|
|
//
|
|
// So this case asserts the whole payload byte for byte rather than the named fields: a
|
|
// test that compared only the members would go green through exactly that bug.
|
|
// IT NAMES NO PAD MEMBER, deliberately. The whole struct is stamped with a recognisable
|
|
// byte first and only the fields the decoder validates are then written, so whatever is
|
|
// left - Pad0, Pad1, or the names a later phase gives them - still carries the stamp and a
|
|
// memcmp over the whole payload is the assertion. A case that named `Pad0` would stop
|
|
// COMPILING the day someone claims it, which is precisely the day it is most needed.
|
|
Wire2 wire;
|
|
std::vector<std::uint8_t> texels(64, 0x31);
|
|
|
|
MGPSubData upload{};
|
|
std::memset(&upload, 0xA5, sizeof(upload));
|
|
upload.Res = MakeHandle(101);
|
|
upload.Target = MGPipePackSubDataTarget(static_cast<Uint32>(MGPipeResourceTarget::Tex2D), 0u);
|
|
upload.Level = 2;
|
|
upload.SourceIsVerbatimLevelShadow = 1;
|
|
upload.UnionBox = MGPBox{1, 2, 0, 4, 4, 1};
|
|
upload.RegionCount = 0;
|
|
upload.Blob = wire.Encoder().StageBytes(texels.data(), texels.size());
|
|
|
|
ASSERT_NE(wire.Encoder().EncodeRecord(MGPWireOp::ResourceSubData, &upload, sizeof(upload)),
|
|
kInvalidSeq);
|
|
wire.Encoder().Publish();
|
|
|
|
Transport::RingRecordView view{};
|
|
bool corrupt = false;
|
|
ASSERT_TRUE(wire.Consumer().Pop(view, &corrupt));
|
|
ASSERT_FALSE(corrupt);
|
|
ASSERT_GE(view.payloadSize, sizeof(upload));
|
|
EXPECT_EQ(std::memcmp(view.payload, &upload, sizeof(upload)), 0)
|
|
<< "the payload did not cross byte for byte";
|
|
|
|
// And the stamp really did survive somewhere the named fields do not cover, so the case
|
|
// cannot pass by comparing a struct that has no unclaimed bytes left.
|
|
const auto* crossed = static_cast<const std::uint8_t*>(view.payload);
|
|
std::size_t stamped = 0;
|
|
for (std::size_t i = 0; i < sizeof(upload); ++i) {
|
|
if (crossed[i] == 0xA5) {
|
|
++stamped;
|
|
}
|
|
}
|
|
EXPECT_GT(stamped, 0u) << "no byte of the payload was left unclaimed; the case still checks "
|
|
"the memcmp above, but it no longer proves anything about pads";
|
|
}
|
|
|
|
TEST_F(PipeWireCodecTest, ResourceDescPadsCrossToo) {
|
|
// The same property over the struct P5 itself put two fields into. The helpers are the
|
|
// only legal reader (three fields are one value), but the BYTES are what the codec owes.
|
|
Wire2 wire;
|
|
MGPResourceDesc desc{};
|
|
desc.Resource = MakeHandle(111);
|
|
desc.Target = static_cast<Uint8>(MGPipeResourceTarget::TexCube);
|
|
desc.InternalFormat = 7;
|
|
desc.Width = 16;
|
|
desc.Height = 16;
|
|
desc.Depth = 1;
|
|
desc.ArrayLayers = 6;
|
|
desc.Levels = 3;
|
|
desc.Samples = 1;
|
|
MGPipeSetRespecifiedLevel(desc, 0x0304u, 2u);
|
|
|
|
ASSERT_NE(wire.Encoder().EncodeRecord(MGPWireOp::ResourceRespecify, &desc, sizeof(desc)),
|
|
kInvalidSeq);
|
|
wire.Encoder().Publish();
|
|
|
|
Transport::RingRecordView view{};
|
|
bool corrupt = false;
|
|
ASSERT_TRUE(wire.Consumer().Pop(view, &corrupt));
|
|
ASSERT_FALSE(corrupt);
|
|
EXPECT_EQ(std::memcmp(view.payload, &desc, sizeof(desc)), 0);
|
|
const auto* crossed = static_cast<const MGPResourceDesc*>(view.payload);
|
|
EXPECT_FALSE(MGPipeRespecifyIsWholeResource(*crossed));
|
|
EXPECT_EQ(MGPipeRespecifiedUploadTargetOf(*crossed), 0x0304u);
|
|
EXPECT_EQ(MGPipeRespecifiedLevelOf(*crossed), 2u);
|
|
|
|
}
|
|
|
|
// =====================================================================================
|
|
// The two double-tailed rows, and DrawVbo's conditional one
|
|
// =====================================================================================
|
|
|
|
TEST_F(PipeWireCodecTest, SetShaderBuffersCarriesBothTailsWhenTheSpanTailIsPresent) {
|
|
Wire2 wire;
|
|
MGPShaderBuffers header{};
|
|
header.Class = 0;
|
|
header.Count = 2;
|
|
header.HostSpanCount = 2; // 0 or Count, never anything else
|
|
MGPBufferRange ranges[2]{};
|
|
ranges[0].Res = MakeHandle(41);
|
|
ranges[1].Res = MakeHandle(42);
|
|
MGHostSpan spans[2]{};
|
|
spans[0].Ptr = nullptr;
|
|
spans[0].Seg = kSegStage;
|
|
spans[0].Size = 8;
|
|
spans[0].Offset = 0;
|
|
spans[1] = spans[0];
|
|
|
|
const WireTail tails[2] = {{ranges, sizeof(ranges)}, {spans, sizeof(spans)}};
|
|
ASSERT_NE(wire.Encoder().EncodeRecord(MGPWireOp::SetShaderBuffers, &header, sizeof(header),
|
|
tails, 2),
|
|
kInvalidSeq);
|
|
bool applied = false;
|
|
ASSERT_TRUE(wire.PumpOne(&applied));
|
|
EXPECT_FALSE(applied);
|
|
}
|
|
|
|
TEST_F(PipeWireCodecTest, ResourceSubDataCarriesABlobAndARegionTailTogether) {
|
|
// The only row in the catalogue that is BOTH kHasBlob and kVarTail, and the one rule A
|
|
// changes most: the texture half declared Size 0 in monolith "because the byte count is
|
|
// the server's to compute", which cannot be a bounds check.
|
|
Wire2 wire;
|
|
MGPResourceDesc create{};
|
|
create.Resource = MakeHandle(61);
|
|
create.Target = static_cast<Uint8>(MGPipeResourceTarget::Tex2D);
|
|
create.InternalFormat = 1;
|
|
create.Width = 4;
|
|
create.Height = 4;
|
|
create.Depth = 1;
|
|
create.ArrayLayers = 1;
|
|
create.Levels = 1;
|
|
create.Samples = 1;
|
|
ASSERT_NE(wire.Encoder().EncodeRecord(MGPWireOp::ResourceCreate, &create, sizeof(create)),
|
|
kInvalidSeq);
|
|
bool applied = false;
|
|
ASSERT_TRUE(wire.PumpOne(&applied));
|
|
|
|
std::vector<std::uint8_t> texels(4 * 4 * 4, 0x5A);
|
|
MGPSubData upload{};
|
|
upload.Res = create.Resource;
|
|
upload.Target = MGPipePackSubDataTarget(static_cast<Uint32>(MGPipeResourceTarget::Tex2D), 0u);
|
|
upload.Level = 0;
|
|
upload.UnionBox = MGPBox{0, 0, 0, 4, 4, 1};
|
|
upload.RegionCount = 1;
|
|
upload.Blob = wire.Encoder().StageBytes(texels.data(), texels.size());
|
|
MGPSubRegion region{};
|
|
region.W = 4;
|
|
region.H = 4;
|
|
region.D = 1;
|
|
ASSERT_NE(wire.Encoder().EncodeRecord(MGPWireOp::ResourceSubData, &upload, sizeof(upload),
|
|
®ion, sizeof(region)),
|
|
kInvalidSeq);
|
|
ASSERT_TRUE(wire.PumpOne(&applied));
|
|
EXPECT_TRUE(applied);
|
|
EXPECT_TRUE(wire.Answers().All.empty());
|
|
// ACCEPTANCE IS NOT "APPLIED", and this case is where the difference shows. The record
|
|
// crossed and reached MGPipeApplyResourceSubData, which is the codec's whole job; the
|
|
// applier then DECLINED it, because no backend registered a P4a texture consumer in this
|
|
// unit process (PipeApply.cpp's NoP4aConsumer belt - the one that turned "no consumer"
|
|
// into lost texels on Magma). That is exactly the answer R-5 says must travel rather than
|
|
// be re-derived on the client: a client that cleared its dirty flags on the strength of
|
|
// having EMITTED would drop these texels for good.
|
|
EXPECT_TRUE(wire.Decoder().LastAcceptanceKnown());
|
|
EXPECT_FALSE(wire.Decoder().LastAcceptance());
|
|
// Both records answered - the texture create is declined by the same NoP4aConsumer belt.
|
|
EXPECT_EQ(wire.Decoder().AcceptedRecords() + wire.Decoder().DeclinedRecords(), 2u);
|
|
// And the texels themselves crossed intact: the decline is the applier's, not the wire's.
|
|
const void* staged =
|
|
wire.Segments().Resolve(upload.Blob.Seg, upload.Blob.Offset, upload.Blob.Size);
|
|
ASSERT_NE(staged, nullptr);
|
|
EXPECT_EQ(std::memcmp(staged, texels.data(), texels.size()), 0);
|
|
}
|
|
|
|
TEST_F(PipeWireCodecTest, SetGlobalConstantsCarriesTheDefaultUniformBlock) {
|
|
Wire2 wire;
|
|
std::vector<std::uint8_t> block(256, 0x11);
|
|
MGPGlobalConstants record{};
|
|
record.ShaderCso = MakeHandle(71);
|
|
record.Version = 2;
|
|
record.Blob = wire.Encoder().StageBytes(block.data(), block.size());
|
|
ASSERT_NE(wire.Encoder().EncodeRecord(MGPWireOp::SetGlobalConstants, &record, sizeof(record)),
|
|
kInvalidSeq);
|
|
bool applied = false;
|
|
ASSERT_TRUE(wire.PumpOne(&applied));
|
|
EXPECT_TRUE(applied);
|
|
}
|
|
|
|
TEST_F(PipeWireCodecTest, SetStreamOutputTargetsCarriesItsRangesAndItsOffsets) {
|
|
Wire2 wire;
|
|
MGPStreamOutputTargets header{};
|
|
header.Count = 3;
|
|
header.Generation = 5;
|
|
MGPBufferRange ranges[3]{};
|
|
Uint32 offsets[3] = {16, 32, 48};
|
|
for (Uint32 i = 0; i < 3; ++i) {
|
|
ranges[i].Res = MakeHandle(51 + i);
|
|
ranges[i].Size = 256;
|
|
}
|
|
const WireTail tails[2] = {{ranges, sizeof(ranges)}, {offsets, sizeof(offsets)}};
|
|
ASSERT_NE(wire.Encoder().EncodeRecord(MGPWireOp::SetStreamOutputTargets, &header, sizeof(header),
|
|
tails, 2),
|
|
kInvalidSeq);
|
|
bool applied = false;
|
|
ASSERT_TRUE(wire.PumpOne(&applied));
|
|
EXPECT_FALSE(applied); // no applier entry point; off the reduced path
|
|
}
|
|
|
|
TEST_F(PipeWireCodecTest, DrawVboWithoutUserIndicesHasExactlyOneTail) {
|
|
Wire2 wire;
|
|
MGPDrawInfo info{};
|
|
info.Mode = 4;
|
|
info.InstanceCount = 1;
|
|
info.NumDraws = 3; // odd * 12 bytes: the case that makes the alignment rule matter
|
|
MGPDrawRange ranges[3] = {{0, 3, 0}, {3, 6, 0}, {9, 3, 1}};
|
|
ASSERT_NE(wire.Encoder().EncodeRecord(MGPWireOp::DrawVbo, &info, sizeof(info), ranges,
|
|
sizeof(ranges)),
|
|
kInvalidSeq);
|
|
bool applied = false;
|
|
ASSERT_TRUE(wire.PumpOne(&applied));
|
|
EXPECT_TRUE(applied);
|
|
ASSERT_EQ(wire.Sink().Draws.size(), 1u);
|
|
EXPECT_EQ(wire.Sink().Draws[0].NumDraws, 3u);
|
|
ASSERT_EQ(wire.Sink().DrawRanges.size(), 3u);
|
|
EXPECT_EQ(wire.Sink().DrawRanges[2].Start, 9u);
|
|
EXPECT_EQ(wire.Sink().DrawRanges[2].IndexBias, 1);
|
|
EXPECT_FALSE(wire.Sink().SawUserIndices);
|
|
}
|
|
|
|
TEST_F(PipeWireCodecTest, DrawVboConditionalSpanTailIsEightAlignedAndSurvives) {
|
|
Wire2 wire;
|
|
MGPDrawInfo info{};
|
|
info.Mode = 4;
|
|
info.IndexSize = 2;
|
|
info.Flags = kDrawHasUserIndices;
|
|
info.InstanceCount = 1;
|
|
info.NumDraws = 1; // 12 bytes: the span behind it would land on a 4-byte boundary
|
|
|
|
const std::uint16_t indices[4] = {0, 1, 2, 3};
|
|
const MGPBlobRef staged = wire.Encoder().StageBytes(indices, sizeof(indices));
|
|
MGHostSpan span{};
|
|
span.Ptr = nullptr; // rule B
|
|
span.Seg = staged.Seg;
|
|
span.Offset = staged.Offset;
|
|
span.Size = staged.Size;
|
|
|
|
MGPDrawRange ranges[1] = {{0, 4, 0}};
|
|
const WireTail tails[2] = {{ranges, sizeof(ranges)}, {&span, sizeof(span)}};
|
|
|
|
WireRecordLayout layout{};
|
|
ASSERT_TRUE(MGPipeWireRecordLayout(MGPWireOp::DrawVbo, &info, layout));
|
|
EXPECT_EQ(layout.TailCount, 2u);
|
|
EXPECT_EQ(layout.TailOffset[1] % 8, 0u) << "MGPDrawRange is twelve bytes; the span behind an "
|
|
"odd NumDraws must still be 8-aligned";
|
|
|
|
ASSERT_NE(wire.Encoder().EncodeRecord(MGPWireOp::DrawVbo, &info, sizeof(info), tails, 2),
|
|
kInvalidSeq);
|
|
bool applied = false;
|
|
ASSERT_TRUE(wire.PumpOne(&applied));
|
|
EXPECT_TRUE(applied);
|
|
EXPECT_TRUE(wire.Sink().SawUserIndices);
|
|
EXPECT_EQ(wire.Sink().LastSpan.Ptr, nullptr);
|
|
EXPECT_EQ(wire.Sink().LastSpan.Size, sizeof(indices));
|
|
}
|
|
|
|
// =====================================================================================
|
|
// CreateShaderState's seven blobs
|
|
// =====================================================================================
|
|
|
|
TEST_F(PipeWireCodecTest, CreateShaderStateCrossesAsOneArchiveAndSixUndeclaredRuns) {
|
|
Wire2 wire;
|
|
MG_State::GLState::LinkArtifacts link;
|
|
MG_State::GLState::SpirvArtifacts spirv;
|
|
spirv.spirvStatus = true;
|
|
spirv.nativeFloat64 = false;
|
|
spirv.generatedSpirv.resize(6);
|
|
for (std::size_t stage = 0; stage < 6; ++stage) {
|
|
spirv.generatedSpirv[stage].assign(4 + stage, static_cast<unsigned>(0x07230203 + stage));
|
|
}
|
|
spirv.globalUboScratch.assign(32, 0xAB);
|
|
|
|
Vector<Uint8> archive;
|
|
MG_State::GLState::EncodeProgramArtifacts(link, spirv, archive);
|
|
ASSERT_FALSE(archive.empty());
|
|
|
|
MGPProgramDesc desc{};
|
|
desc.Cso = MakeHandle(77);
|
|
desc.StageMask = 0x3f;
|
|
desc.SpirvStatus = 1;
|
|
// The ruling: Reflection names the WHOLE archive - which already carries every stage's
|
|
// modules - and Spirv[0..5] stay all-zero. Shipping the modules twice would double the
|
|
// biggest record in the catalogue for a reader that does not exist.
|
|
desc.Reflection = wire.Encoder().StageBytes(archive.data(), archive.size());
|
|
for (Uint32 i = 0; i < 6; ++i) {
|
|
EXPECT_EQ(desc.Spirv[i].Size, 0u);
|
|
EXPECT_EQ(desc.Spirv[i].Seg, 0u);
|
|
EXPECT_EQ(desc.Spirv[i].Offset, 0u);
|
|
}
|
|
|
|
ASSERT_NE(wire.Encoder().EncodeRecord(MGPWireOp::CreateShaderState, &desc, sizeof(desc)),
|
|
kInvalidSeq);
|
|
bool applied = false;
|
|
ASSERT_TRUE(wire.PumpOne(&applied));
|
|
EXPECT_TRUE(applied);
|
|
|
|
// And the archive really did carry the six modules: decode it the way the arm does.
|
|
MG_State::GLState::LinkArtifacts back;
|
|
MG_State::GLState::SpirvArtifacts backSpirv;
|
|
ASSERT_TRUE(MG_State::GLState::DecodeProgramArtifacts(archive.data(), archive.size(), back,
|
|
backSpirv));
|
|
ASSERT_EQ(backSpirv.generatedSpirv.size(), 6u);
|
|
for (std::size_t stage = 0; stage < 6; ++stage) {
|
|
EXPECT_EQ(backSpirv.generatedSpirv[stage].size(), 4 + stage);
|
|
EXPECT_EQ(backSpirv.generatedSpirv[stage][0], 0x07230203u + stage);
|
|
}
|
|
EXPECT_TRUE(backSpirv.spirvStatus);
|
|
}
|
|
|
|
// =====================================================================================
|
|
// SetResidualValueState - table 1's hardest row
|
|
// =====================================================================================
|
|
|
|
TEST_F(PipeWireCodecTest, ResidualValueBlockCrossesAsItsOwnBlob) {
|
|
// The applier takes `const ResidualValueBlock&` and MGPResidualValueState is never
|
|
// instantiated on the live path, so this is the first code in the tree that fills either.
|
|
Wire2 wire;
|
|
ResidualValueBlock block{};
|
|
block.CapabilityBits = 0x0123456789ABCDEFull;
|
|
|
|
MGPResidualValueState record{};
|
|
record.Version = 3;
|
|
record.Blob = wire.Encoder().StageBytes(&block, sizeof(block));
|
|
EXPECT_EQ(record.Blob.Size, static_cast<Uint64>(MGL_RESIDUAL_BLOCK_SIZE));
|
|
|
|
ASSERT_NE(wire.Encoder().EncodeRecord(MGPWireOp::SetResidualValueState, &record, sizeof(record)),
|
|
kInvalidSeq);
|
|
bool applied = false;
|
|
ASSERT_TRUE(wire.PumpOne(&applied));
|
|
EXPECT_TRUE(applied);
|
|
}
|
|
|
|
// =====================================================================================
|
|
// CreateSamplerState - a POD memcpy in which borderColorForm must survive
|
|
// =====================================================================================
|
|
|
|
TEST_F(PipeWireCodecTest, SamplerParametersCrossByteForByteIncludingBorderColorForm) {
|
|
Wire2 wire;
|
|
SamplerParameters params{};
|
|
params.borderColorForm = BorderColorForm::Int;
|
|
params.minLod = -3.5f;
|
|
params.maxLod = 11.25f;
|
|
|
|
MGPSamplerDesc desc{};
|
|
desc.Cso = MakeHandle(88);
|
|
desc.Parameters = wire.Encoder().StageBytes(¶ms, sizeof(params));
|
|
EXPECT_EQ(desc.Parameters.Size, sizeof(SamplerParameters));
|
|
|
|
ASSERT_NE(wire.Encoder().EncodeRecord(MGPWireOp::CreateSamplerState, &desc, sizeof(desc)),
|
|
kInvalidSeq);
|
|
bool applied = false;
|
|
ASSERT_TRUE(wire.PumpOne(&applied));
|
|
EXPECT_TRUE(applied);
|
|
|
|
const void* staged = wire.Segments().Resolve(desc.Parameters.Seg, desc.Parameters.Offset,
|
|
desc.Parameters.Size);
|
|
ASSERT_NE(staged, nullptr);
|
|
SamplerParameters back{};
|
|
std::memcpy(&back, staged, sizeof(back));
|
|
EXPECT_EQ(static_cast<int>(back.borderColorForm), static_cast<int>(BorderColorForm::Int));
|
|
EXPECT_FLOAT_EQ(back.minLod, -3.5f);
|
|
EXPECT_FLOAT_EQ(back.maxLod, 11.25f);
|
|
}
|
|
|
|
// =====================================================================================
|
|
// The class-B verbs
|
|
// =====================================================================================
|
|
|
|
TEST_F(PipeWireCodecTest, TheFiveClassBVerbsReachTheSinkAndNothingElse) {
|
|
Wire2 wire;
|
|
MGPClear clear{};
|
|
clear.Kind = 0;
|
|
clear.BufferMask = 0x4000;
|
|
clear.ColorValue[0] = 0x3f800000u;
|
|
ASSERT_NE(wire.Encoder().EncodeRecord(MGPWireOp::Clear, &clear, sizeof(clear)), kInvalidSeq);
|
|
|
|
MGPBlit blit{};
|
|
blit.SrcX1 = 64;
|
|
blit.DstX1 = 64;
|
|
ASSERT_NE(wire.Encoder().EncodeRecord(MGPWireOp::Blit, &blit, sizeof(blit)), kInvalidSeq);
|
|
|
|
MGPReadbackInfo readback{};
|
|
readback.Box = MGPBox{0, 0, 0, 2, 2, 1};
|
|
readback.DstSize = 16;
|
|
ASSERT_NE(wire.Encoder().EncodeRecord(MGPWireOp::ReadPixels, &readback, sizeof(readback)),
|
|
kInvalidSeq);
|
|
|
|
MGPPresent present{};
|
|
present.FrameSerial = 12;
|
|
ASSERT_NE(wire.Encoder().EncodeRecord(MGPWireOp::Present, &present, sizeof(present)), kInvalidSeq);
|
|
|
|
bool applied = false;
|
|
for (int i = 0; i < 4; ++i) {
|
|
ASSERT_TRUE(wire.PumpOne(&applied)) << "record " << i;
|
|
EXPECT_TRUE(applied) << "record " << i;
|
|
}
|
|
EXPECT_EQ(wire.Sink().Clears.size(), 1u);
|
|
EXPECT_EQ(wire.Sink().Blits.size(), 1u);
|
|
EXPECT_EQ(wire.Sink().Presents.size(), 1u);
|
|
ASSERT_EQ(wire.Sink().Readbacks.size(), 1u);
|
|
EXPECT_EQ(wire.Sink().Presents[0].FrameSerial, 12u);
|
|
// read_pixels BLOCKS in P5 and its pixels come back in the reply slot the record's own
|
|
// seq names (contract table 1 row 23).
|
|
ASSERT_EQ(wire.Answers().All.size(), 1u);
|
|
EXPECT_EQ(wire.Answers().All[0].Seq, wire.Sink().ReadbackSeqs[0]);
|
|
EXPECT_EQ(wire.Answers().All[0].Bytes.size(), 4u);
|
|
EXPECT_EQ(wire.Decoder().AppliedSeq(), 4u);
|
|
}
|
|
|
|
TEST_F(PipeWireCodecTest, AClassBVerbWithNoSinkDeclinesRatherThanInventsASemantics) {
|
|
Wire2 wire;
|
|
wire.Decoder().SetVerbSink(nullptr);
|
|
MGPClear clear{};
|
|
ASSERT_NE(wire.Encoder().EncodeRecord(MGPWireOp::Clear, &clear, sizeof(clear)), kInvalidSeq);
|
|
bool applied = true;
|
|
ASSERT_TRUE(wire.PumpOne(&applied));
|
|
EXPECT_FALSE(applied);
|
|
}
|
|
|
|
// =====================================================================================
|
|
// R-9, R-10, R-11
|
|
// =====================================================================================
|
|
|
|
TEST_F(PipeWireCodecTest, AppliedSeqAdvancesByExactlyOnePerRecordAndIsNeverBatched) {
|
|
// TWO COUNTS, ON PURPOSE. RingControl::appliedSeq has exactly one writer - the session -
|
|
// and the decoder keeps its own tally. They must agree after every record, and a single
|
|
// counter could not tell a batched publish from an honest one (R-9: a batched watermark
|
|
// makes the client's barrier resume on work the server has not run).
|
|
Wire2 wire;
|
|
MGPPresent present{};
|
|
for (Uint64 i = 1; i <= 5; ++i) {
|
|
present.FrameSerial = i;
|
|
ASSERT_EQ(wire.Encoder().EncodeRecord(MGPWireOp::Present, &present, sizeof(present)), i);
|
|
}
|
|
wire.Encoder().Publish();
|
|
for (Uint64 i = 1; i <= 5; ++i) {
|
|
bool applied = false;
|
|
ASSERT_TRUE(wire.PumpOne(&applied));
|
|
EXPECT_EQ(wire.Decoder().AppliedSeq(), i);
|
|
EXPECT_EQ(wire.Control().appliedSeq.load(), i);
|
|
EXPECT_EQ(wire.SessionAppliedSeq(), wire.Decoder().AppliedSeq());
|
|
}
|
|
EXPECT_EQ(wire.Encoder().EmitSeq(), 5u);
|
|
}
|
|
|
|
TEST_F(PipeWireCodecTest, TheDecoderWritesNoRingControlFieldOfItsOwn) {
|
|
// s1 owns RingControl::appliedSeq; the codec's job ends at "this record was applied". Two
|
|
// writers of a watermark is how a waiter resumes on a record the server has not run, and
|
|
// there is no checksum on this ring that would catch it - so this is asserted rather than
|
|
// documented.
|
|
Wire2 wire;
|
|
MGPPresent present{};
|
|
ASSERT_NE(wire.Encoder().EncodeRecord(MGPWireOp::Present, &present, sizeof(present)),
|
|
kInvalidSeq);
|
|
wire.Encoder().Publish();
|
|
|
|
Transport::RingRecordView view{};
|
|
bool corrupt = false;
|
|
ASSERT_TRUE(wire.Consumer().Pop(view, &corrupt));
|
|
ASSERT_FALSE(corrupt);
|
|
const std::uint64_t appliedBefore = wire.Control().appliedSeq.load();
|
|
const std::uint64_t retiredBefore = wire.Control().retiredSeq.load();
|
|
|
|
(void)wire.Decoder().DecodeAndApply(view);
|
|
|
|
EXPECT_EQ(wire.Control().appliedSeq.load(), appliedBefore);
|
|
EXPECT_EQ(wire.Control().retiredSeq.load(), retiredBefore);
|
|
EXPECT_EQ(wire.Decoder().AppliedSeq(), 1u); // the decoder's own tally did move
|
|
}
|
|
|
|
TEST_F(PipeWireCodecTest, MaxRecordBytesSeenStaysFarBelowHalfTheRing) {
|
|
// R-10's proof obligation. P5 does no chunking and must instead show it never needed any.
|
|
//
|
|
// THE CAP IS ASKED FOR AT RUNTIME AND NEVER DERIVED FROM MOBILEGL_IPC_RING_MB, and this
|
|
// phase is why: the number moved twice in one afternoon. s1 first found that the control
|
|
// page came out of the segment (making the cap 2 MiB at the default), then fixed it the
|
|
// other way round - MOBILEGL_IPC_RING_MB now names the RING and the segment adds a page on
|
|
// top - so the cap is 4 MiB again and the contract is true as written. Nothing in the
|
|
// codec changed either time, because every comparison goes through MaxRecordBytes().
|
|
Wire2 wire;
|
|
MGPDrawInfo info{};
|
|
info.NumDraws = 64;
|
|
std::vector<MGPDrawRange> ranges(64);
|
|
ASSERT_NE(wire.Encoder().EncodeRecord(MGPWireOp::DrawVbo, &info, sizeof(info), ranges.data(),
|
|
ranges.size() * sizeof(MGPDrawRange)),
|
|
kInvalidSeq);
|
|
const Uint64 largest = wire.Encoder().MaxRecordBytesSeen();
|
|
EXPECT_GT(largest, 0u);
|
|
EXPECT_EQ(largest, 8u + sizeof(MGPDrawInfo) + 64u * sizeof(MGPDrawRange));
|
|
EXPECT_LT(largest, wire.Cmd().MaxRecordBytes());
|
|
// The biggest fixed payload in the whole catalogue is MGPProgramDesc at 192 bytes plus
|
|
// MGPFramebufferState at 304, so a record only ever grows through its TAIL - which is why
|
|
// the counter is on the encoder and not a constant.
|
|
EXPECT_LT(8u + sizeof(MGPFramebufferState), wire.Cmd().MaxRecordBytes());
|
|
}
|
|
|
|
TEST_F(PipeWireCodecTest, ABigProgramArchiveDoesNotGrowItsRecordAtAll) {
|
|
// THE POINT OF R-10's "every blob goes through SEG_STAGE": create_shader_state's RECORD is
|
|
// 8 + sizeof(MGPProgramDesc) == 200 bytes whether the archive is one kilobyte or one
|
|
// megabyte, because the record carries {Seg, Offset, Size} and nothing else. So
|
|
// create_shader_state is one of the SMALLEST records in the catalogue, not the one most
|
|
// likely to approach MaxRecordBytes(); what approaches that cap is a var-tail, and the
|
|
// archive's own bound is MOBILEGL_IPC_STAGE_MB with a Fatal of its own.
|
|
Wire2 wire;
|
|
MG_State::GLState::LinkArtifacts link;
|
|
MG_State::GLState::SpirvArtifacts spirv;
|
|
spirv.generatedSpirv.resize(1);
|
|
spirv.generatedSpirv[0].assign(8 * 1024, 0x07230203u); // 32 KiB of module words
|
|
Vector<Uint8> archive;
|
|
MG_State::GLState::EncodeProgramArtifacts(link, spirv, archive);
|
|
ASSERT_GT(archive.size(), 32u * 1024u);
|
|
|
|
MGPProgramDesc desc{};
|
|
desc.Cso = MakeHandle(91);
|
|
desc.Reflection = wire.Encoder().StageBytes(archive.data(), archive.size());
|
|
ASSERT_NE(wire.Encoder().EncodeRecord(MGPWireOp::CreateShaderState, &desc, sizeof(desc)),
|
|
kInvalidSeq);
|
|
EXPECT_EQ(wire.Encoder().MaxRecordBytesSeen(), 8u + sizeof(MGPProgramDesc));
|
|
EXPECT_GE(wire.Encoder().StagedBytesInFlight(), archive.size());
|
|
}
|
|
|
|
TEST_F(PipeWireCodecTest, StagedBytesAreReclaimedOnlyBehindRetiredSeq) {
|
|
Wire2 wire;
|
|
const std::uint8_t payload[64] = {};
|
|
const MGPBlobRef first = wire.Encoder().StageBytes(payload, sizeof(payload));
|
|
(void)first;
|
|
const Uint64 inFlight = wire.Encoder().StagedBytesInFlight();
|
|
EXPECT_GE(inFlight, sizeof(payload));
|
|
|
|
MGPBindRenderState bind{};
|
|
ASSERT_NE(wire.Encoder().EncodeRecord(MGPWireOp::BindRenderState, &bind, sizeof(bind)),
|
|
kInvalidSeq);
|
|
// Nothing is retired yet, so nothing may be released: R-11's whole content on this side.
|
|
wire.Encoder().ReclaimStagedBytes();
|
|
EXPECT_EQ(wire.Encoder().StagedBytesInFlight(), inFlight);
|
|
|
|
bool applied = false;
|
|
ASSERT_TRUE(wire.PumpOne(&applied));
|
|
wire.Encoder().ReclaimStagedBytes();
|
|
EXPECT_EQ(wire.Encoder().StagedBytesInFlight(), 0u);
|
|
}
|
|
|
|
// ---- M2 / M3: SEG_STAGE's cursors and the mark queue --------------------------------------
|
|
|
|
TEST_F(PipeWireCodecTest, TheEncoderNeverWritesSegStagesConsumerCursors) {
|
|
// Ring.h makes stageAppliedTail / stageRetiredTail CONSUMER-owned and says the staging
|
|
// allocator reclaims behind retiredSeq "and nothing else may". A producer writing them is
|
|
// the same shape w1's own §8.1 argues against for appliedSeq, one segment over: if s1 ever
|
|
// attaches a RingConsumer to RingCursorSet::Stage - the obvious thing to do for a segment
|
|
// with a cursor triple - its m_localTail never moves (nothing Pops the stage ring) and the
|
|
// two cursors get walked forward by the client and back by the server.
|
|
Wire2 wire;
|
|
const std::uint8_t payload[128] = {};
|
|
(void)wire.Encoder().StageBytes(payload, sizeof(payload));
|
|
MGPBindRenderState bind{};
|
|
ASSERT_NE(wire.Encoder().EncodeRecord(MGPWireOp::BindRenderState, &bind, sizeof(bind)),
|
|
kInvalidSeq);
|
|
bool applied = false;
|
|
ASSERT_TRUE(wire.PumpOne(&applied));
|
|
wire.Encoder().ReclaimStagedBytes();
|
|
EXPECT_EQ(wire.Encoder().StagedBytesInFlight(), 0u) << "the reclaim did not run at all";
|
|
|
|
EXPECT_EQ(wire.Control().stageHead.load(), 0u);
|
|
EXPECT_EQ(wire.Control().stageAppliedTail.load(), 0u);
|
|
EXPECT_EQ(wire.Control().stageRetiredTail.load(), 0u);
|
|
}
|
|
|
|
TEST_F(PipeWireCodecTest, TheStageMarkQueueStaysBoundedWithOneRecordAlwaysInFlight) {
|
|
// The steady-state leak: the queue used to be cleared ONLY when it drained completely, so
|
|
// one unretired record at every reclaim meant front < size() for ever and 16 bytes per
|
|
// encoded record for the life of the context. That is exactly the regime W-3 says the
|
|
// design exists to survive once R-1's barrier retires family by family - and no SSIM
|
|
// comparison and no two-scenario lane would ever see it.
|
|
Wire2 wire;
|
|
MGPBindRenderState bind{};
|
|
const std::uint8_t blob[32] = {};
|
|
|
|
// PRIMING IS THE WHOLE POINT. Encoding and applying one record per iteration drains the
|
|
// queue at every reclaim, which is the one regime the old "clear only when front reaches
|
|
// size()" code handled - a case written that way stays green against the bug. One record
|
|
// encoded ahead of the one being applied is what makes `front < size()` permanent.
|
|
ASSERT_NE(wire.Encoder().EncodeRecord(MGPWireOp::BindRenderState, &bind, sizeof(bind)),
|
|
kInvalidSeq);
|
|
for (int i = 0; i < 4000; ++i) {
|
|
(void)wire.Encoder().StageBytes(blob, sizeof(blob));
|
|
ASSERT_NE(wire.Encoder().EncodeRecord(MGPWireOp::BindRenderState, &bind, sizeof(bind)),
|
|
kInvalidSeq);
|
|
bool applied = false;
|
|
ASSERT_TRUE(wire.PumpOne(&applied)) << "record " << i;
|
|
}
|
|
EXPECT_EQ(wire.Encoder().EmitSeq(), 4001u);
|
|
EXPECT_EQ(wire.Decoder().AppliedSeq(), 4000u) << "one record must still be in flight";
|
|
// Bounded by twice the records actually in flight, not by the records ever encoded.
|
|
EXPECT_LE(wire.Encoder().StageMarksHeld(), 4u)
|
|
<< "the mark queue is tracking history rather than flight";
|
|
}
|
|
|
|
// ---- M4: the reply slot is for kReplySlot rows only ----------------------------------------
|
|
|
|
TEST_F(PipeWireCodecTest, NoReplyIsWrittenForARowTheCatalogueGivesNoReplySlot) {
|
|
// s1 sizes ReplyPool from MGPipeCallFlagsFor, so a reply written for a record the pool
|
|
// reserved no slot for overwrites a waiter's answer - and because the slot header stamps
|
|
// the WRITER's seq for self-check, the waiter's check then fails for ever and the barrier
|
|
// HANGS rather than returning something wrong.
|
|
Wire2 wire;
|
|
ASSERT_EQ(MGPipeCallFlagsFor(MGPWireOp::ResourceCreate) & static_cast<Uint32>(kReplySlot), 0u);
|
|
ASSERT_EQ(MGPipeCallFlagsFor(MGPWireOp::SetTextureParams) & static_cast<Uint32>(kReplySlot), 0u);
|
|
|
|
MGPResourceDesc create{};
|
|
create.Resource = MakeHandle(131);
|
|
create.Target = static_cast<Uint8>(MGPipeResourceTarget::Buffer);
|
|
create.Width = 64;
|
|
create.Height = 1;
|
|
create.Depth = 1;
|
|
create.ArrayLayers = 1;
|
|
create.Levels = 1;
|
|
create.Samples = 1;
|
|
ASSERT_NE(wire.Encoder().EncodeRecord(MGPWireOp::ResourceCreate, &create, sizeof(create)),
|
|
kInvalidSeq);
|
|
bool applied = false;
|
|
ASSERT_TRUE(wire.PumpOne(&applied));
|
|
EXPECT_TRUE(applied);
|
|
|
|
EXPECT_TRUE(wire.Answers().All.empty()) << "a reply slot was written for a kNone row";
|
|
// The acceptance answer R-5 requires is still produced - it just does not ride SEG_REPLY.
|
|
EXPECT_TRUE(wire.Decoder().LastAcceptanceKnown());
|
|
EXPECT_EQ(wire.Decoder().AcceptedRecords() + wire.Decoder().DeclinedRecords(), 1u);
|
|
}
|
|
|
|
TEST_F(PipeWireCodecTest, EveryRowThatDoesWriteAReplyCarriesKReplySlot) {
|
|
// The other half: the three rows in P5 that answer into a slot all carry the flag, so
|
|
// PostReply's gate cannot be firing on any of them.
|
|
for (const MGPWireOp op :
|
|
{MGPWireOp::MapPersistent, MGPWireOp::ResourceReadback, MGPWireOp::ReadPixels}) {
|
|
EXPECT_NE(MGPipeCallFlagsFor(op) & static_cast<Uint32>(kReplySlot), 0u) << WireOpName(op);
|
|
}
|
|
}
|
|
|
|
TEST_F(PipeWireCodecTest, TheAuditFillOverwritesExactlyTheRunsTheRecordResolved) {
|
|
// R-2.5, the only mechanical control on rule C ("no applier entry point retains a pointer
|
|
// past its return"). An instrumentation that cannot be observed to have run is decoration,
|
|
// so this case asserts the bytes, not the flag.
|
|
Wire2 wire;
|
|
wire.Decoder().SetAuditPoison(true);
|
|
ResidualValueBlock block{};
|
|
block.CapabilityBits = 0x5555555555555555ull;
|
|
MGPResidualValueState record{};
|
|
record.Blob = wire.Encoder().StageBytes(&block, sizeof(block));
|
|
ASSERT_NE(wire.Encoder().EncodeRecord(MGPWireOp::SetResidualValueState, &record, sizeof(record)),
|
|
kInvalidSeq);
|
|
bool applied = false;
|
|
ASSERT_TRUE(wire.PumpOne(&applied));
|
|
EXPECT_TRUE(applied);
|
|
EXPECT_EQ(wire.Decoder().PoisonedStageBytes(), sizeof(ResidualValueBlock));
|
|
const auto* staged = wire.StageBase() + record.Blob.Offset;
|
|
for (std::size_t i = 0; i < sizeof(ResidualValueBlock); ++i) {
|
|
EXPECT_EQ(staged[i], 0xDD) << "byte " << i;
|
|
}
|
|
}
|
|
|
|
TEST_F(PipeWireCodecTest, TheAuditFillIsOffByDefaultSoTheHotPathPaysNothing) {
|
|
Wire2 wire;
|
|
ResidualValueBlock block{};
|
|
block.CapabilityBits = 0x77ull;
|
|
MGPResidualValueState record{};
|
|
record.Blob = wire.Encoder().StageBytes(&block, sizeof(block));
|
|
ASSERT_NE(wire.Encoder().EncodeRecord(MGPWireOp::SetResidualValueState, &record, sizeof(record)),
|
|
kInvalidSeq);
|
|
bool applied = false;
|
|
ASSERT_TRUE(wire.PumpOne(&applied));
|
|
EXPECT_EQ(wire.Decoder().PoisonedStageBytes(), 0u);
|
|
const auto* staged = wire.StageBase() + record.Blob.Offset;
|
|
EXPECT_NE(staged[0], 0xDD);
|
|
}
|
|
|
|
// =====================================================================================
|
|
// MGPCaps's two blob codecs
|
|
// =====================================================================================
|
|
|
|
TEST_F(PipeWireCodecTest, FormatCapabilitiesRoundTrip) {
|
|
MG_Backend::FormatCapabilityCache cache;
|
|
cache.FullCaps[0][1] = MG_Backend::FormatCapabilityFlags(
|
|
static_cast<Uint64>(MG_Backend::FormatCapability::Creatable) |
|
|
static_cast<Uint64>(MG_Backend::FormatCapability::Sampled));
|
|
cache.CaveatCaps[2][3] =
|
|
MG_Backend::FormatCapabilityFlags(static_cast<Uint64>(MG_Backend::FormatCapability::LinearFilter));
|
|
cache.SampleCounts[1][4] = Vector<Int>{1, 2, 4, 8};
|
|
|
|
Vector<Uint8> bytes;
|
|
ASSERT_TRUE(EncodeFormatCapabilities(cache, bytes));
|
|
ASSERT_FALSE(bytes.empty());
|
|
|
|
MG_Backend::FormatCapabilityCache back;
|
|
ASSERT_TRUE(DecodeFormatCapabilities(bytes.data(), bytes.size(), back));
|
|
EXPECT_EQ(back.FullCaps[0][1].GetRaw(), cache.FullCaps[0][1].GetRaw());
|
|
EXPECT_EQ(back.CaveatCaps[2][3].GetRaw(), cache.CaveatCaps[2][3].GetRaw());
|
|
EXPECT_EQ(back.SampleCounts[1][4], cache.SampleCounts[1][4]);
|
|
EXPECT_EQ(back.FullCaps[5][5].GetRaw(), 0u);
|
|
EXPECT_TRUE(back.SampleCounts[0][0].empty());
|
|
|
|
// Sparse: an almost-empty cache must not cost the square of two enum spaces.
|
|
EXPECT_LT(bytes.size(), 4096u);
|
|
}
|
|
|
|
TEST_F(PipeWireCodecTest, FormatCapabilitiesDecoderRefusesTruncationAndTrailingBytes) {
|
|
MG_Backend::FormatCapabilityCache cache;
|
|
cache.FullCaps[0][0] =
|
|
MG_Backend::FormatCapabilityFlags(static_cast<Uint64>(MG_Backend::FormatCapability::Creatable));
|
|
Vector<Uint8> bytes;
|
|
ASSERT_TRUE(EncodeFormatCapabilities(cache, bytes));
|
|
|
|
MG_Backend::FormatCapabilityCache back;
|
|
for (SizeT cut = 1; cut < bytes.size(); ++cut) {
|
|
EXPECT_FALSE(DecodeFormatCapabilities(bytes.data(), cut, back)) << "truncated to " << cut;
|
|
}
|
|
Vector<Uint8> longer = bytes;
|
|
longer.push_back(0);
|
|
EXPECT_FALSE(DecodeFormatCapabilities(longer.data(), longer.size(), back));
|
|
// A version word this build does not read is a refusal, never a guess.
|
|
Vector<Uint8> wrongVersion = bytes;
|
|
wrongVersion[0] = static_cast<Uint8>(wrongVersion[0] + 1);
|
|
EXPECT_FALSE(DecodeFormatCapabilities(wrongVersion.data(), wrongVersion.size(), back));
|
|
}
|
|
|
|
TEST_F(PipeWireCodecTest, RendererInfoRoundTrips) {
|
|
RendererInfo info;
|
|
info.RendererName = "Espryt";
|
|
info.BackendName = "DirectGLES";
|
|
info.ExtraVendor = String("Qualcomm");
|
|
info.RendererGLInfo.TargetGLVersion = Version{4, 6, 0, Optional<String>(), Optional<VersionType>()};
|
|
info.RendererGLInfo.TargetGLSLVersion =
|
|
Version{4, 60, 0, Optional<String>(String("-dev")), Optional<VersionType>(VersionType::Development)};
|
|
info.RendererGLInfo.Extensions = Vector<GLExtension>{static_cast<GLExtension>(1),
|
|
static_cast<GLExtension>(7)};
|
|
info.RendererGLInfo.IsCompatibilityProfile = true;
|
|
info.StaticBackendCapability.AllowVSOnlyPrograms = true;
|
|
|
|
Vector<Uint8> bytes;
|
|
ASSERT_TRUE(EncodeRendererInfo(info, bytes));
|
|
|
|
RendererInfo back;
|
|
ASSERT_TRUE(DecodeRendererInfo(bytes.data(), bytes.size(), back));
|
|
EXPECT_EQ(back.RendererName, info.RendererName);
|
|
EXPECT_EQ(back.BackendName, info.BackendName);
|
|
ASSERT_TRUE(back.ExtraVendor.has_value());
|
|
EXPECT_EQ(*back.ExtraVendor, "Qualcomm");
|
|
EXPECT_EQ(back.RendererGLInfo.TargetGLVersion.Major, 4);
|
|
EXPECT_EQ(back.RendererGLInfo.TargetGLSLVersion.Minor, 60);
|
|
ASSERT_TRUE(back.RendererGLInfo.TargetGLSLVersion.Suffix.has_value());
|
|
EXPECT_EQ(*back.RendererGLInfo.TargetGLSLVersion.Suffix, "-dev");
|
|
ASSERT_TRUE(back.RendererGLInfo.TargetGLSLVersion.Type.has_value());
|
|
EXPECT_EQ(static_cast<int>(*back.RendererGLInfo.TargetGLSLVersion.Type),
|
|
static_cast<int>(VersionType::Development));
|
|
ASSERT_EQ(back.RendererGLInfo.Extensions.size(), 2u);
|
|
EXPECT_EQ(static_cast<int>(back.RendererGLInfo.Extensions[1]), 7);
|
|
EXPECT_TRUE(back.RendererGLInfo.IsCompatibilityProfile);
|
|
EXPECT_TRUE(back.StaticBackendCapability.AllowVSOnlyPrograms);
|
|
}
|
|
|
|
TEST_F(PipeWireCodecTest, RendererInfoDecoderRefusesEveryTruncation) {
|
|
RendererInfo info;
|
|
info.RendererName = "Magma";
|
|
info.BackendName = "DirectVulkan";
|
|
Vector<Uint8> bytes;
|
|
ASSERT_TRUE(EncodeRendererInfo(info, bytes));
|
|
RendererInfo back;
|
|
for (SizeT cut = 1; cut < bytes.size(); ++cut) {
|
|
EXPECT_FALSE(DecodeRendererInfo(bytes.data(), cut, back)) << "truncated to " << cut;
|
|
}
|
|
EXPECT_FALSE(DecodeRendererInfo(nullptr, 0, back));
|
|
}
|
|
|
|
TEST_F(PipeWireCodecTest, TheAbiFingerprintIsStableWithinABuildAndNotZero) {
|
|
const Uint64 first = CapsAbiFingerprint();
|
|
EXPECT_NE(first, 0u);
|
|
EXPECT_EQ(first, CapsAbiFingerprint());
|
|
}
|
|
|
|
TEST_F(PipeWireCodecTest, TheConsumerMaskAnswersPerFamilyAndNotPerOpTable) {
|
|
// R-8's only legal client-side spelling. Under inproc a client that read
|
|
// MGPipeGetResourceOps() would be right BY ACCIDENT; under spawn that table is null and
|
|
// five whole record families emit nothing at all, silently.
|
|
const Uint64 mask = MGCapsConsumerBits(kMGPipeSubsystemResources | kMGPipeSubsystemPrograms);
|
|
EXPECT_TRUE(MGCapsServerConsumes(mask, kMGPipeSubsystemResources));
|
|
EXPECT_TRUE(MGCapsServerConsumes(mask, kMGPipeSubsystemPrograms));
|
|
EXPECT_FALSE(MGCapsServerConsumes(mask, kMGPipeSubsystemTextureResources));
|
|
// The feature bits below it are untouched by the consumer block.
|
|
EXPECT_EQ(mask & 0xFFFFFFFFull, 0u);
|
|
}
|
|
|
|
// =====================================================================================
|
|
// The Fatal arms. Forked, never EXPECT_DEATH.
|
|
// =====================================================================================
|
|
|
|
#if MGTEST_HAVE_FORK
|
|
|
|
TEST_F(PipeWireCodecTest, ANonNullHostSpanPointerIsFatal) {
|
|
// R-2 arm 1 / rule B. In ONE address space this pointer works, which is exactly why the
|
|
// rule has to be mechanical.
|
|
const ChildResult r = RunInChild([] {
|
|
Wire2 wire;
|
|
MGPShaderBuffers header{};
|
|
header.Count = 1;
|
|
header.HostSpanCount = 1;
|
|
MGPBufferRange range{};
|
|
MGHostSpan span{};
|
|
std::uint64_t here = 0;
|
|
span.Ptr = &here; // the inproc cheat
|
|
span.Seg = kSegStage;
|
|
span.Size = 8;
|
|
std::vector<std::uint8_t> tail(sizeof(range) + sizeof(span));
|
|
std::memcpy(tail.data(), &range, sizeof(range));
|
|
std::memcpy(tail.data() + sizeof(range), &span, sizeof(span));
|
|
ForgeAndDecode(wire, MGPWireOp::SetShaderBuffers, &header, sizeof(header), tail.data(),
|
|
tail.size());
|
|
});
|
|
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
|
|
EXPECT_NE(r.Log.find("Fatal{ProtocolCorruption, \"host-span\"}"), std::string::npos) << r.Log;
|
|
}
|
|
|
|
TEST_F(PipeWireCodecTest, AContentRecordThatDeclaresNoBlobIsFatal) {
|
|
// R-2 arm 2. This is the arm that inverts today's legal state: Blob.Size == 0 means "this
|
|
// record does not declare its blob", which is right for monolith and a lie under split.
|
|
const ChildResult r = RunInChild([] {
|
|
Wire2 wire;
|
|
MGPVertexElements desc{};
|
|
desc.Cso = MakeHandle(3);
|
|
desc.AttributeCount = 1;
|
|
desc.BindingPointCount = 1;
|
|
desc.Blob = MGPBlobRef{}; // all three fields zero: "absent"
|
|
ForgeAndDecode(wire, MGPWireOp::CreateVertexElements, &desc, sizeof(desc), nullptr, 0);
|
|
});
|
|
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
|
|
EXPECT_NE(r.Log.find("carries content and its blob declares none"), std::string::npos) << r.Log;
|
|
}
|
|
|
|
TEST_F(PipeWireCodecTest, ANonZeroSizeWithNoSegmentIsFatal) {
|
|
// R-2 arm 3.
|
|
const ChildResult r = RunInChild([] {
|
|
Wire2 wire;
|
|
MGPVertexElements desc{};
|
|
desc.Cso = MakeHandle(3);
|
|
desc.AttributeCount = 1;
|
|
desc.Blob.Seg = kSegNone;
|
|
desc.Blob.Offset = 0;
|
|
desc.Blob.Size = 64;
|
|
ForgeAndDecode(wire, MGPWireOp::CreateVertexElements, &desc, sizeof(desc), nullptr, 0);
|
|
});
|
|
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
|
|
EXPECT_NE(r.Log.find("with no segment"), std::string::npos) << r.Log;
|
|
}
|
|
|
|
TEST_F(PipeWireCodecTest, ARunThatLeavesItsSegmentIsFatal) {
|
|
// R-2 arm 4.
|
|
const ChildResult r = RunInChild([] {
|
|
Wire2 wire;
|
|
MGPVertexElements desc{};
|
|
desc.Cso = MakeHandle(3);
|
|
desc.AttributeCount = 1;
|
|
desc.Blob.Seg = kSegStage;
|
|
desc.Blob.Offset = Wire2::kStageBytes - 8;
|
|
desc.Blob.Size = 4096;
|
|
ForgeAndDecode(wire, MGPWireOp::CreateVertexElements, &desc, sizeof(desc), nullptr, 0);
|
|
});
|
|
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
|
|
EXPECT_NE(r.Log.find("does not lie inside that segment"), std::string::npos) << r.Log;
|
|
}
|
|
|
|
TEST_F(PipeWireCodecTest, AHalfDeclaredBlobIsFatalRatherThanReadAsAbsent) {
|
|
// The shape a MONOLITH emitter produces - Seg None, Offset a host address, Size 0. Reading
|
|
// it as "absent" would silently drop the bytes of every record an unconverted emitter sent.
|
|
const ChildResult r = RunInChild([] {
|
|
Wire2 wire;
|
|
MGPRenderStateDesc desc{};
|
|
desc.Cso = MakeHandle(3);
|
|
desc.ChunkMask = 0;
|
|
desc.Blob.Seg = kMGHostSpanSegNone;
|
|
desc.Blob.Offset = 0xDEADBEEFull; // a host address, the way ProgramEmit.h writes one
|
|
desc.Blob.Size = 0;
|
|
ForgeAndDecode(wire, MGPWireOp::CreateRenderState, &desc, sizeof(desc), nullptr, 0);
|
|
});
|
|
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
|
|
EXPECT_NE(r.Log.find("a blob with Size 0 declares"), std::string::npos) << r.Log;
|
|
}
|
|
|
|
TEST_F(PipeWireCodecTest, ATailThatDoesNotMatchItsOwnCountIsFatal) {
|
|
// THE CROSS-CHECK THIS PACKAGE EXISTS FOR. MGP_WIRE_CHECK_BOUNDS proves
|
|
// `size >= sizeof(MGPWireRec_X)` and CANNOT SEE THE TAIL, so this record - Count = 4000
|
|
// with eight bytes behind it - passes the generated gate today.
|
|
const ChildResult r = RunInChild([] {
|
|
Wire2 wire;
|
|
MGPSamplerViews header{};
|
|
header.Start = 0;
|
|
header.Count = 4000;
|
|
const std::uint64_t eightBytes = 0;
|
|
ForgeAndDecode(wire, MGPWireOp::SetSamplerViews, &header, sizeof(header), &eightBytes,
|
|
sizeof(eightBytes));
|
|
});
|
|
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
|
|
// The count is past the unit bound, so the layout refuses it before the length even
|
|
// matters - which is the stronger of the two answers.
|
|
EXPECT_NE(r.Log.find("Fatal{ProtocolCorruption"), std::string::npos) << r.Log;
|
|
}
|
|
|
|
TEST_F(PipeWireCodecTest, ATailWhoseLengthDisagreesWithItsCountIsFatal) {
|
|
// The same defect inside the legal count range, so the SIZE arithmetic is what catches it
|
|
// rather than the bound.
|
|
const ChildResult r = RunInChild([] {
|
|
Wire2 wire;
|
|
MGPSamplerViews header{};
|
|
header.Start = 0;
|
|
header.Count = 4; // 4 * sizeof(MGPBoundView) == 96 bytes of tail
|
|
const std::uint64_t eightBytes = 0;
|
|
ForgeAndDecode(wire, MGPWireOp::SetSamplerViews, &header, sizeof(header), &eightBytes,
|
|
sizeof(eightBytes));
|
|
});
|
|
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
|
|
EXPECT_NE(r.Log.find("its own count fields describe"), std::string::npos) << r.Log;
|
|
}
|
|
|
|
TEST_F(PipeWireCodecTest, VertexAttribDefaultsMustAgreeWithItsOwnMask) {
|
|
// Two declarants, Count and popcount(Mask), and nothing checked them before.
|
|
const ChildResult r = RunInChild([] {
|
|
Wire2 wire;
|
|
MGPVertexAttribDefaults header{};
|
|
header.Mask = 0x7; // three bits
|
|
header.Count = 2; // two entries
|
|
MGPAttribValue tail[2]{};
|
|
ForgeAndDecode(wire, MGPWireOp::SetVertexAttribDefaults, &header, sizeof(header), tail,
|
|
sizeof(tail));
|
|
});
|
|
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
|
|
EXPECT_NE(r.Log.find("SetVertexAttribDefaults.Count"), std::string::npos) << r.Log;
|
|
}
|
|
|
|
TEST_F(PipeWireCodecTest, APadRecordReachingTheDecoderIsFatal) {
|
|
// R-9: a pad does not advance seq and both sides skip it BEFORE counting. One that reached
|
|
// here has already been counted, and because the seq IS the reply-slot id, a drift of one
|
|
// silently reads another call's answer rather than failing.
|
|
const ChildResult r = RunInChild([] {
|
|
Wire2 wire;
|
|
Transport::RingRecordView view{};
|
|
std::uint8_t bytes[16] = {};
|
|
view.kind = Transport::kRingPadRecordKind;
|
|
view.flags = Transport::kRecPad;
|
|
view.payload = bytes + 8;
|
|
view.payloadSize = 8;
|
|
(void)wire.Decoder().DecodeAndApply(view);
|
|
});
|
|
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
|
|
EXPECT_NE(r.Log.find("wrap filler reached the decoder"), std::string::npos) << r.Log;
|
|
}
|
|
|
|
TEST_F(PipeWireCodecTest, ADeclaredPerStageSpirvRunIsFatal) {
|
|
// w1's ruling: the archive already carries every stage's modules, so a per-stage run would
|
|
// be a second, forgeable way to say the same thing.
|
|
const ChildResult r = RunInChild([] {
|
|
Wire2 wire;
|
|
MG_State::GLState::LinkArtifacts link;
|
|
MG_State::GLState::SpirvArtifacts spirv;
|
|
Vector<Uint8> archive;
|
|
MG_State::GLState::EncodeProgramArtifacts(link, spirv, archive);
|
|
MGPProgramDesc desc{};
|
|
desc.Cso = MakeHandle(3);
|
|
desc.Reflection = wire.Encoder().StageBytes(archive.data(), archive.size());
|
|
const std::uint32_t words[4] = {1, 2, 3, 4};
|
|
desc.Spirv[0] = wire.Encoder().StageBytes(words, sizeof(words));
|
|
ForgeAndDecode(wire, MGPWireOp::CreateShaderState, &desc, sizeof(desc), nullptr, 0);
|
|
});
|
|
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
|
|
EXPECT_NE(r.Log.find("CreateShaderState.Spirv[0]"), std::string::npos) << r.Log;
|
|
}
|
|
|
|
TEST_F(PipeWireCodecTest, AHostSpanCountThatIsNeitherZeroNorCountIsFatal) {
|
|
// MGPipeTypes.h:820-823: HostSpanCount is 0 OR Count, never anything else, so the two
|
|
// arrays stay index-aligned. A third value lets a record describe spans for ranges it does
|
|
// not have - and the arrays would then be read off by one for the rest of the tail.
|
|
const ChildResult r = RunInChild([] {
|
|
Wire2 wire;
|
|
MGPShaderBuffers header{};
|
|
header.Count = 4;
|
|
header.HostSpanCount = 3;
|
|
MGPBufferRange ranges[4]{};
|
|
MGHostSpan spans[3]{};
|
|
std::vector<std::uint8_t> tail(sizeof(ranges) + sizeof(spans));
|
|
std::memcpy(tail.data(), ranges, sizeof(ranges));
|
|
std::memcpy(tail.data() + sizeof(ranges), spans, sizeof(spans));
|
|
ForgeAndDecode(wire, MGPWireOp::SetShaderBuffers, &header, sizeof(header), tail.data(),
|
|
tail.size());
|
|
});
|
|
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
|
|
EXPECT_NE(r.Log.find("SetShaderBuffers.HostSpanCount"), std::string::npos) << r.Log;
|
|
}
|
|
|
|
// ---- M1: R-2 arm 4 over MGHostSpan, on both host-span rows ------------------------------
|
|
|
|
TEST_F(PipeWireCodecTest, AHostSpanRunPastItsSegmentIsFatalOnSetShaderBuffers) {
|
|
// Before the fix this child exited 0: CheckHostSpanIsHonest took no SegmentTable and no
|
|
// caller resolved, so arm 4 was implemented for blobrefs only. The span names a REAL
|
|
// segment and a run that leaves it.
|
|
const ChildResult r = RunInChild([] {
|
|
Wire2 wire;
|
|
MGPShaderBuffers header{};
|
|
header.Count = 1;
|
|
header.HostSpanCount = 1;
|
|
MGPBufferRange range{};
|
|
MGHostSpan span{};
|
|
span.Ptr = nullptr; // rule B satisfied, so only arm 4 can catch this
|
|
span.Seg = kSegStage;
|
|
span.Offset = Wire2::kStageBytes - 8;
|
|
span.Size = 1024;
|
|
std::vector<std::uint8_t> tail(sizeof(range) + sizeof(span));
|
|
std::memcpy(tail.data(), &range, sizeof(range));
|
|
std::memcpy(tail.data() + sizeof(range), &span, sizeof(span));
|
|
ForgeAndDecode(wire, MGPWireOp::SetShaderBuffers, &header, sizeof(header), tail.data(),
|
|
tail.size());
|
|
});
|
|
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
|
|
EXPECT_NE(r.Log.find("Fatal{ProtocolCorruption, \"host-span\"}"), std::string::npos) << r.Log;
|
|
EXPECT_NE(r.Log.find("does not lie inside that segment (R-2.3 arm 4)"), std::string::npos)
|
|
<< r.Log;
|
|
}
|
|
|
|
TEST_F(PipeWireCodecTest, AHostSpanRunPastItsSegmentIsFatalOnDrawVbo) {
|
|
// The same span on the other host-span row - the one whose sink WireVerbSink's header
|
|
// promises is handed "a DECODED, VALIDATED argument list". Before the fix it reached
|
|
// OnDrawVbo.
|
|
const ChildResult r = RunInChild([] {
|
|
Wire2 wire;
|
|
MGPDrawInfo info{};
|
|
info.Mode = 4;
|
|
info.IndexSize = 2;
|
|
info.Flags = kDrawHasUserIndices;
|
|
info.NumDraws = 1;
|
|
MGPDrawRange ranges[1] = {{0, 4, 0}};
|
|
MGHostSpan span{};
|
|
span.Ptr = nullptr;
|
|
span.Seg = kSegStage;
|
|
span.Offset = 0xFFFFFFFFull;
|
|
span.Size = 0xFFFFu;
|
|
// The layout realigns the span to 8 after a 12-byte MGPDrawRange, so the forged tail
|
|
// has to carry the same four pad bytes the encoder would.
|
|
std::vector<std::uint8_t> tail(16 + sizeof(span), 0);
|
|
std::memcpy(tail.data(), ranges, sizeof(ranges));
|
|
std::memcpy(tail.data() + 16, &span, sizeof(span));
|
|
ForgeAndDecode(wire, MGPWireOp::DrawVbo, &info, sizeof(info), tail.data(), tail.size());
|
|
});
|
|
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
|
|
EXPECT_NE(r.Log.find("does not lie inside that segment (R-2.3 arm 4)"), std::string::npos)
|
|
<< r.Log;
|
|
}
|
|
|
|
// ---- M5 / m8 / q2 ------------------------------------------------------------------------
|
|
|
|
TEST_F(PipeWireCodecTest, BufferSubDataResidentMayNotDeclareRegions) {
|
|
// Its catalogue row has no kVarTail and its applier takes no regions, so a non-zero
|
|
// RegionCount is a fault rather than a tail. The layout used to share ResourceSubData's
|
|
// arm, which REQUIRED 80 bytes of tail the arm then dropped.
|
|
const ChildResult r = RunInChild([] {
|
|
Wire2 wire;
|
|
MGPSubData rec{};
|
|
rec.Res = MakeHandle(7);
|
|
rec.RegionCount = 2;
|
|
MGPipeSetSubDataBufferRange(rec, 0, 64);
|
|
rec.RegionCount = 2; // after the helper, which zeroes it
|
|
ForgeAndDecode(wire, MGPWireOp::BufferSubDataResident, &rec, sizeof(rec), nullptr, 0);
|
|
});
|
|
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
|
|
EXPECT_NE(r.Log.find("BufferSubDataResident.RegionCount"), std::string::npos) << r.Log;
|
|
}
|
|
|
|
TEST_F(PipeWireCodecTest, TheEncoderRefusesThePerStageSpirvRunTheDecoderCallsFatal) {
|
|
// The rule lived only in the decoder, so an emitter that declared a per-stage run got a
|
|
// valid seq here and a Fatal on a peer that could only report "corrupt stream".
|
|
const ChildResult r = RunInChild([] {
|
|
Wire2 wire;
|
|
MG_State::GLState::LinkArtifacts link;
|
|
MG_State::GLState::SpirvArtifacts spirv;
|
|
Vector<Uint8> archive;
|
|
MG_State::GLState::EncodeProgramArtifacts(link, spirv, archive);
|
|
MGPProgramDesc desc{};
|
|
desc.Cso = MakeHandle(3);
|
|
desc.Reflection = wire.Encoder().StageBytes(archive.data(), archive.size());
|
|
const std::uint32_t words[4] = {1, 2, 3, 4};
|
|
desc.Spirv[0] = wire.Encoder().StageBytes(words, sizeof(words));
|
|
(void)wire.Encoder().EncodeRecord(MGPWireOp::CreateShaderState, &desc, sizeof(desc));
|
|
});
|
|
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
|
|
EXPECT_NE(r.Log.find("declares 16 bytes at the ENCODER"), std::string::npos) << r.Log;
|
|
}
|
|
|
|
TEST_F(PipeWireCodecTest, APartiallyZeroUploadBoxIsFatalRatherThanReadAsEmpty) {
|
|
// The content predicate used to be "all three extents non-zero", so a 4x4x0 box was read
|
|
// as carrying nothing and rule A's arm 2 sat out for a record that named a real
|
|
// destination. A box is either empty or whole.
|
|
const ChildResult r = RunInChild([] {
|
|
Wire2 wire;
|
|
MGPSubData rec{};
|
|
rec.Res = MakeHandle(9);
|
|
rec.Target = MGPipePackSubDataTarget(static_cast<Uint32>(MGPipeResourceTarget::Tex2D), 0u);
|
|
rec.UnionBox = MGPBox{0, 0, 0, 4, 4, 0};
|
|
ForgeAndDecode(wire, MGPWireOp::ResourceSubData, &rec, sizeof(rec), nullptr, 0);
|
|
});
|
|
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
|
|
EXPECT_NE(r.Log.find("has a zero extent on some axes and not others"), std::string::npos)
|
|
<< r.Log;
|
|
}
|
|
|
|
TEST_F(PipeWireCodecTest, ASecondProcessResolverIsFatalRatherThanASilentRace) {
|
|
// Table 3: one gMGPipeSegmentResolver per process, installed by the SERVER role only.
|
|
const ChildResult r = RunInChild([] {
|
|
SegmentTable a;
|
|
SegmentTable b;
|
|
std::vector<std::uint8_t> bytes(64);
|
|
a.Install(kSegStage, SegmentView{bytes.data(), bytes.size()});
|
|
b.Install(kSegStage, SegmentView{bytes.data(), bytes.size()});
|
|
a.InstallProcessResolver();
|
|
b.InstallProcessResolver();
|
|
});
|
|
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
|
|
EXPECT_NE(r.Log.find("already installed"), std::string::npos) << r.Log;
|
|
}
|
|
|
|
#else
|
|
|
|
TEST_F(PipeWireCodecTest, TheFatalArmsNeedFork) {
|
|
GTEST_SKIP() << "the R-2 Fatal arms are asserted by forking; POSIX only";
|
|
}
|
|
|
|
#endif // MGTEST_HAVE_FORK
|
|
|
|
TEST_F(PipeWireCodecTest, TheProcessResolverRoundTripsThroughMGPipeHostBytes) {
|
|
SegmentTable table;
|
|
std::vector<std::uint8_t> bytes(128);
|
|
for (std::size_t i = 0; i < bytes.size(); ++i) {
|
|
bytes[i] = static_cast<std::uint8_t>(i);
|
|
}
|
|
table.Install(kSegStage, SegmentView{bytes.data(), bytes.size()});
|
|
table.InstallProcessResolver();
|
|
|
|
MGHostSpan span{};
|
|
span.Ptr = nullptr;
|
|
span.Seg = kSegStage;
|
|
span.Offset = 16;
|
|
span.Size = 32;
|
|
EXPECT_EQ(MGPipeHostBytes(span), bytes.data() + 16);
|
|
|
|
SegmentTable::UninstallProcessResolver();
|
|
EXPECT_EQ(MGPipeHostBytes(span), nullptr);
|
|
}
|
|
|
|
int main(int argc, char** argv) {
|
|
// Before anything logs: MG_Util::Debug::InitFile() reads the variable once, on the first
|
|
// write, and caches the FILE*. The name carries this process's pid, because
|
|
// gtest_discover_tests runs every case as its own process, in parallel under ctest -j.
|
|
namespace fs = std::filesystem;
|
|
const fs::path path =
|
|
fs::temp_directory_path() / ("mobilegl-pipewirecodec-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;
|
|
}
|