mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
1519 lines
91 KiB
C++
1519 lines
91 KiB
C++
// MobileGL - MobileGL/MG_Test/Pipe/PipeCatalogueTest.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
|
|
|
|
// The arithmetic of the MGPipe catalogue (plan B section 4.4, appendix A). Everything here
|
|
// is cheap on purpose: it is the test that fails when PipeCalls.def and the seven generated
|
|
// files stop agreeing, and it must not need a GL context to say so.
|
|
|
|
#include <gtest/gtest.h>
|
|
|
|
#include <cstring>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <regex>
|
|
#include <sstream>
|
|
#include <iterator>
|
|
#include <limits>
|
|
#include <type_traits>
|
|
|
|
#include "Includes.h"
|
|
#include <MG_Pipe/MGPipe.h>
|
|
// P4a: MGPipeUnmigratedEmulation's declaration, and the applier's records the catalogue's size
|
|
// pins now reach. Push-only, like the translation unit that defines them - in a pull build the
|
|
// symbol does not exist and the one case that calls it is compiled out.
|
|
#include <MG_Impl/Pipe/SlotAllocator.h>
|
|
#include <MG_Pipe/PipeApply.h>
|
|
// P5 R-17: the routing that INSTALLS the two tables. Included here so that the installation
|
|
// case below states the partition deterministically rather than depending on whether some
|
|
// other object in this particular test binary happened to drag the installer in.
|
|
#include <MG_Pipe/PipeRoute.h>
|
|
|
|
using namespace MobileGL;
|
|
using namespace MobileGL::MG_Pipe;
|
|
|
|
TEST(PipeCatalogue, FrontendNeverTakesAnApplierAddress) {
|
|
namespace fs = std::filesystem;
|
|
auto base = fs::current_path();
|
|
while (!fs::is_directory(base / "MobileGL/MG_Impl") && base != base.root_path())
|
|
base = base.parent_path();
|
|
const auto root = base / "MobileGL/MG_Impl";
|
|
ASSERT_TRUE(fs::is_directory(root)) << "frontend source unavailable: " << root;
|
|
const std::regex comments(R"(/\*[\s\S]*?\*/|//[^\n]*)");
|
|
const std::regex address(R"(&\s*MGPipeApply[A-Za-z_0-9]*)");
|
|
for (const auto& entry : fs::recursive_directory_iterator(root)) {
|
|
if (!entry.is_regular_file()) continue;
|
|
const auto ext = entry.path().extension();
|
|
if (ext != ".cpp" && ext != ".h" && ext != ".inc") continue;
|
|
std::ifstream input(entry.path());
|
|
ASSERT_TRUE(input.good()) << entry.path();
|
|
std::ostringstream bytes;
|
|
bytes << input.rdbuf();
|
|
const auto code = std::regex_replace(bytes.str(), comments, "");
|
|
EXPECT_FALSE(std::regex_search(code, address))
|
|
<< "FrontendApplierAddress: " << entry.path()
|
|
<< " must take the route address; routing tables live outside MG_Impl";
|
|
}
|
|
}
|
|
|
|
namespace {
|
|
// Counting expansions of the catalogue. The Class parameter is a real enumerator, so a
|
|
// per-class count is a constant expression too.
|
|
#define MGP_COUNT_ONE(Name, Payload, Class, Flags, Wait) +1
|
|
#define MGP_COUNT_CLASS(Name, Payload, Class, Flags, Wait) +((Class) == countedClass ? 1 : 0)
|
|
|
|
constexpr SizeT kExpandedCallCount = 0 MGP_CALL_LIST(MGP_COUNT_ONE);
|
|
|
|
template <MGPipeCallClass countedClass>
|
|
constexpr SizeT ClassCount() {
|
|
return 0 MGP_CALL_LIST(MGP_COUNT_CLASS);
|
|
}
|
|
|
|
// Every payload named in the catalogue must be a memcpy-able POD, and so must every
|
|
// payload the verify comparator knows about.
|
|
#define MGP_ASSERT_CALL_PAYLOAD_POD(Name, Payload, Class, Flags, Wait) \
|
|
static_assert(std::is_trivially_copyable_v<Payload>, #Name "'s payload " #Payload " is not trivially copyable");
|
|
MGP_CALL_LIST(MGP_ASSERT_CALL_PAYLOAD_POD)
|
|
|
|
#define MGP_ASSERT_VERIFY_PAYLOAD_POD(Payload) \
|
|
static_assert(std::is_trivially_copyable_v<Payload>, #Payload " is not trivially copyable");
|
|
MGP_VERIFY_PAYLOAD_LIST(MGP_ASSERT_VERIFY_PAYLOAD_POD)
|
|
|
|
// The catalogue's own spelling of an opcode, for a failure message. The wire codec has one
|
|
// of these and MG_Pipe may not reach it (MG_Remote is above this layer), so the table is
|
|
// expanded from the catalogue here rather than a name being duplicated per EXPECT.
|
|
#define MGP_CATALOGUE_NAME_ROW(Name, Payload, Class, Flags, Wait) #Name,
|
|
constexpr const char* kCatalogueOpNames[] = {
|
|
"<kInvalid>",
|
|
MGP_CALL_LIST(MGP_CATALOGUE_NAME_ROW)
|
|
};
|
|
#undef MGP_CATALOGUE_NAME_ROW
|
|
static_assert(sizeof(kCatalogueOpNames) / sizeof(kCatalogueOpNames[0]) ==
|
|
static_cast<SizeT>(MGPWireOp::kOpCount),
|
|
"the diagnostic name table and the opcode space disagree");
|
|
|
|
const char* WireOpNameForDiag(MGPWireOp op) {
|
|
const SizeT index = static_cast<SizeT>(op);
|
|
return index < static_cast<SizeT>(MGPWireOp::kOpCount) ? kCatalogueOpNames[index]
|
|
: "<opcode out of range>";
|
|
}
|
|
} // namespace
|
|
|
|
// The handle is the whole object model. Eight bytes, a register pair, no padding.
|
|
TEST(PipeCatalogue, HandleIsEightBytes) {
|
|
static_assert(sizeof(MGPipeHandle) == 8);
|
|
static_assert(alignof(MGPipeHandle) == 4);
|
|
static_assert(std::is_trivially_copyable_v<MGPipeHandle>);
|
|
EXPECT_EQ(sizeof(MGPipeHandle), 8u);
|
|
|
|
// The two reserved handles, and the composite band that the program-pipeline resolver
|
|
// allocates out of.
|
|
EXPECT_TRUE(MGPipeHandleIsNull(kMGPipeNullHandle));
|
|
EXPECT_FALSE(MGPipeHandleIsNull(kMGPipeDefaultFramebuffer));
|
|
EXPECT_FALSE(MGPipeIsCompositeShaderSlot(kMGPipeFirstAllocatableSlot));
|
|
EXPECT_TRUE(MGPipeIsCompositeShaderSlot(kMGPipeShaderCsoCompositeSlotBase));
|
|
EXPECT_FALSE(MGPipeIsCompositeShaderSlot(kMGPipeShaderCsoSlotLimit));
|
|
}
|
|
|
|
// The catalogue, the number documented in its header, and the two generated tables are one
|
|
// fact stated three times. This is the test that notices when they stop being.
|
|
TEST(PipeCatalogue, EntryCountMatchesTheDocumentedCount) {
|
|
static_assert(kExpandedCallCount == MGP_CALL_LIST_DOCUMENTED_COUNT);
|
|
static_assert(kExpandedCallCount == kMGPipeCallCount);
|
|
EXPECT_EQ(kExpandedCallCount, static_cast<SizeT>(MGP_CALL_LIST_DOCUMENTED_COUNT));
|
|
EXPECT_EQ(kMGPipeCallCount, kExpandedCallCount);
|
|
}
|
|
|
|
TEST(PipeCatalogue, GeneratedTablesHoldTheWholeCatalogue) {
|
|
static_assert(ClassCount<kScreen>() == kMGPipeScreenCallCount);
|
|
static_assert(ClassCount<kScreen>() + ClassCount<kCtxCso>() + ClassCount<kCtxState>() +
|
|
ClassCount<kCtxObject>() + ClassCount<kCtxVerb>() + ClassCount<kCtxQuery>() ==
|
|
kMGPipeCallCount);
|
|
// The tables ARE their function pointers: a struct that is bigger than its call count
|
|
// has grown a member no generator knows about.
|
|
static_assert(sizeof(MGPipeScreen) == kMGPipeScreenCallCount * sizeof(void (*)()));
|
|
static_assert(sizeof(MGPipeContext) == kMGPipeContextCallCount * sizeof(void (*)()));
|
|
|
|
EXPECT_EQ(kMGPipeScreenCallCount, ClassCount<kScreen>());
|
|
EXPECT_EQ(kMGPipeContextCallCount, kMGPipeCallCount - ClassCount<kScreen>());
|
|
|
|
// The per-class counts PipeCalls.def documents in its header.
|
|
// kScreen is 11 + P5c's applier_reset (MG_Remote/CONTRACT-P5C.md §5.1); kCtxObject is
|
|
// 9 + P5c's object_death (§5.2), the framebuffer family's first wire delete opcode.
|
|
// kCtxState is 17 + P5c rv's set_context_values (§5.3), the residual-value record, + P5e's
|
|
// set_program_bindings (MG_Remote/CONTRACT-P5E.md §1), the post-link binding record.
|
|
// + P14 S1's bind_context (docs/Disaggregated/design/11-state-ownership.md), the in-band
|
|
// context binding - a kScreen row for applier_reset's reason: a context switch is a
|
|
// whole-session edge, not a context-family verb.
|
|
EXPECT_EQ(ClassCount<kScreen>(), 14u); // + shared_image (opcode 85), a kScreen table row
|
|
EXPECT_EQ(ClassCount<kCtxQuery>(), 8u);
|
|
EXPECT_EQ(ClassCount<kCtxCso>(), 13u);
|
|
EXPECT_EQ(ClassCount<kCtxState>(), 19u);
|
|
// + P9's get_texture_image_to_buffer (MG_Remote/CONTRACT-P9.md §1), beside get_texture_image.
|
|
EXPECT_EQ(ClassCount<kCtxObject>(), 11u);
|
|
// 13 + the five P5b-appended verbs (MG_Remote/CONTRACT-P5B.md): bind_shader_image,
|
|
// patch_parameter, bind_stream_output, set_storage_block_binding,
|
|
// copy_framebuffer_to_texture; + delete_stream_output; + P9's read_pixels_to_buffer.
|
|
EXPECT_EQ(ClassCount<kCtxVerb>(), 20u);
|
|
}
|
|
|
|
// A row nobody has migrated is null - which is exactly what "this subsystem has not been
|
|
// migrated, keep pulling" means (plan B section 4.1).
|
|
//
|
|
// UNTIL P5 R-17 THAT WAS EVERY ROW, and this case said so. It is now EXACTLY THE 41 ROWS WITH
|
|
// NO BODIED MGPipeApply* ENTRY POINT (80 - the 39 that have one; the number was 34 at P5, 39
|
|
// after P5b's five sink-only verbs, rv's set_context_values grew BOTH sides of the difference,
|
|
// P5e's set_program_bindings grew the null side alone, and P5e's set_shader_buffers moved one
|
|
// row from the null side to the other - sb gave it its applier, its adapter and its emitter in
|
|
// one commit):
|
|
// the other 39 have an applier, R-17 installs adapters over them,
|
|
// and a null there would no longer mean "keep pulling" - `MG_Impl/Pipe`'s call sites go through
|
|
// the thunks, so a null would mean "call through a null pointer". The number is asserted rather
|
|
// than the emptiness, because "39 installed" and "41 still null" are the two halves of a
|
|
// partition and a case that checked only one of them would pass an installer that had
|
|
// overwritten rows it does not own.
|
|
// THE NAME IS KEPT, AND SO IS THE STATEMENT IT MAKES - only the ROWS it makes it about have
|
|
// narrowed. G2/G14 compare ctest names against a pre-P5 baseline and require ZERO removed, so
|
|
// renaming a case is a removal even when the new name is better: it is indistinguishable, from
|
|
// the gate's side, from a case that was deleted. So this case stays, asserting the half that is
|
|
// still true, and the new half below is an ADDED name.
|
|
TEST(PipeCatalogue, UninstalledTablesAreAllNull) {
|
|
const void* const* screen = reinterpret_cast<const void* const*>(&gMGPipeScreen);
|
|
const void* const* context = reinterpret_cast<const void* const*>(&gMGPipeContext);
|
|
MGPipeInstallMonolithTables();
|
|
// The 41 rows with no MGPipeApply* body are still null, and null still means "this
|
|
// subsystem has not been migrated, keep pulling". Named rather than counted, because the
|
|
// count is the other case's job and two cases asserting the same number would both go red
|
|
// for one change. P5c's two control records are among them by design (CONTRACT-P5C.md §5:
|
|
// no MGPipeApply* entry point, no monolith producer - under a transport they reach
|
|
// WireVerbSink instead).
|
|
//
|
|
// P5e (sb): SetShaderBuffers HAS LEFT THIS LIST and the inversion is itself the proof the
|
|
// route landed - the row was pinned null "since P4a" by two cases, and its applier, its
|
|
// adapter and its emitter all arrive together. SetStreamOutputTargets stays, because XFB
|
|
// stays lockstep for the whole of P5e (CONTRACT-P5E.md §5.7).
|
|
EXPECT_NE(gMGPipeContext.SetShaderBuffers, nullptr);
|
|
EXPECT_EQ(gMGPipeContext.SetStreamOutputTargets, nullptr);
|
|
EXPECT_EQ(gMGPipeContext.DrawVbo, nullptr);
|
|
EXPECT_EQ(gMGPipeContext.Present, nullptr);
|
|
EXPECT_EQ(gMGPipeContext.SetSwapInterval, nullptr);
|
|
EXPECT_EQ(gMGPipeScreen.GetCaps, nullptr);
|
|
EXPECT_EQ(gMGPipeContext.QueryCreate, nullptr);
|
|
EXPECT_EQ(gMGPipeScreen.FenceCreate, nullptr);
|
|
EXPECT_EQ(gMGPipeScreen.ApplierReset, nullptr);
|
|
EXPECT_EQ(gMGPipeContext.ObjectDeath, nullptr);
|
|
// P5e's opcode 80 joins them: the row is catalogued, its codec layout is written and its
|
|
// tails are validated, but there is no MGPipeApplySetProgramBindings and no route - package
|
|
// pg installs both. A null here is the contract, not an omission.
|
|
EXPECT_EQ(gMGPipeContext.SetProgramBindings, nullptr);
|
|
(void)screen;
|
|
(void)context;
|
|
}
|
|
|
|
TEST(PipeCatalogue, ExactlyTheRoutedRowsAreInstalledAndTheRestAreStillNull) {
|
|
const void* const* screen = reinterpret_cast<const void* const*>(&gMGPipeScreen);
|
|
const void* const* context = reinterpret_cast<const void* const*>(&gMGPipeContext);
|
|
SizeT installed = 0;
|
|
SizeT nulls = 0;
|
|
|
|
// IDEMPOTENT, and called here on purpose: what this case observes is WHICH rows the
|
|
// installer fills, not whether an installer ran somewhere in this binary. Leaving that to
|
|
// ambient linkage is what made the same assertion pass in one build directory and fail in
|
|
// another - the object file carrying a static initialiser was dropped by the linker in the
|
|
// binaries that did not name a symbol in it.
|
|
MGPipeInstallMonolithTables();
|
|
|
|
for (SizeT i = 0; i < kMGPipeScreenCallCount; ++i) {
|
|
if (screen[i] != nullptr) ++installed; else ++nulls;
|
|
}
|
|
for (SizeT i = 0; i < kMGPipeContextCallCount; ++i) {
|
|
if (context[i] != nullptr) ++installed; else ++nulls;
|
|
}
|
|
EXPECT_EQ(installed + nulls, static_cast<SizeT>(kMGPipeCallCount));
|
|
|
|
// 35 + 5 = 40, and the split is the honest shape of R-17 rather than an implementation
|
|
// detail: 40 is the number of MGPipeApply* entry points PipeApply.h declares WITH A BODY
|
|
// (37 at P5, P5c rv's set_context_values was the 38th, P5e sb's set_shader_buffers is the
|
|
// 39th and P5e pg's set_program_bindings the 40th - the two landed in parallel packages,
|
|
// each of which read 38 as its base and wrote 39; this is the merge saying so), 35 of them
|
|
// fit a GENERATED row and go in the two tables, and FIVE cannot be
|
|
// expressed by any generated signature and go in the hand-written escape table beside them
|
|
// (ResourceRespecify's uncarried initialBytes, ResourceFlushRange's likewise,
|
|
// MapPersistent's size + seedBytes + void* return, CreateShaderState's seven blobrefs and
|
|
// two typed pointers - each one a CONTRACT-P5 ruling, see MG_Pipe/PipeRoute.h).
|
|
//
|
|
// set_program_bindings (opcode 80) HAS its body since package pg, and it is the fifth
|
|
// escape rather than a generated row; its `gMGPipeContext` slot therefore stays null and is
|
|
// pinned null below, which is what an escaped row looks like here.
|
|
//
|
|
// BOTH NUMBERS ARE ASSERTED. If the escape table were left out of this case, moving a row
|
|
// out of the generated tables and forgetting to install its escape would read as a smaller
|
|
// "installed" count and nothing else - and the call site would take a null.
|
|
EXPECT_EQ(installed, 35u) << "the routed rows and the applier's entry points disagree";
|
|
EXPECT_EQ(nulls, static_cast<SizeT>(kMGPipeCallCount) - 35u);
|
|
const void* const* escapes = reinterpret_cast<const void* const*>(&gMGPipeRouteEscapes);
|
|
SizeT escapesInstalled = 0;
|
|
for (SizeT i = 0; i < sizeof(MGPipeRouteEscapes) / sizeof(void*); ++i) {
|
|
if (escapes[i] != nullptr) ++escapesInstalled;
|
|
}
|
|
// P5e (pg) MADE IT FIVE: set_program_bindings carries three tails in three index spaces
|
|
// plus a parallel name array, which no generated (payload, varTail, varTailCount) row can
|
|
// express - so opcode 80's `gMGPipeContext` slot stays null (pinned below, unchanged) and
|
|
// its adapter lives in the escape table beside create_shader_state's. The applier's entry
|
|
// points are 40 with it and with sb's generated row.
|
|
EXPECT_EQ(escapesInstalled, 5u) << "an escape row is null; its call site would take a null "
|
|
"pointer rather than fall back to anything";
|
|
EXPECT_EQ(installed + escapesInstalled, 40u)
|
|
<< "the two tables plus the escapes must be exactly PipeApply.h's bodied entry points";
|
|
|
|
// And the rows that MUST still be null, named rather than counted: these are calls with no
|
|
// applier at all (CONTRACT-P5 table 1 rows 13, 14: "no applier entry point exists"), plus
|
|
// the two verbs the census measured as having zero MG_Impl call sites. An installer that
|
|
// filled one of these would be claiming an implementation that does not exist.
|
|
//
|
|
// P5e (sb): row 13 (set_shader_buffers) is no longer one of them - see the sibling case
|
|
// above. Row 14 (set_stream_output_targets) still is.
|
|
EXPECT_NE(gMGPipeContext.SetShaderBuffers, nullptr);
|
|
EXPECT_EQ(gMGPipeContext.SetStreamOutputTargets, nullptr);
|
|
EXPECT_EQ(gMGPipeContext.DrawVbo, nullptr);
|
|
EXPECT_EQ(gMGPipeContext.Present, nullptr);
|
|
EXPECT_EQ(gMGPipeContext.SetSwapInterval, nullptr);
|
|
EXPECT_EQ(gMGPipeScreen.GetCaps, nullptr);
|
|
// P5c's two control records (CONTRACT-P5C.md §5) take the same answer for a different
|
|
// reason: no MGPipeApply* exists for either and none may be installed - under a transport
|
|
// they cross to WireVerbSink, under monolith the GL thread's direct call and the death
|
|
// notice's mailbox are the producers, byte for byte as before (G1/G2).
|
|
EXPECT_EQ(gMGPipeScreen.ApplierReset, nullptr);
|
|
EXPECT_EQ(gMGPipeContext.ObjectDeath, nullptr);
|
|
// P5e's set_program_bindings (CONTRACT-P5E.md §1): catalogued with a null route until pg
|
|
// installs the applier entry point and the adapter beside it.
|
|
EXPECT_EQ(gMGPipeContext.SetProgramBindings, nullptr);
|
|
}
|
|
|
|
// The retirement ratchet of the migration carrier (section 6.3) reached its end in P13 W3c:
|
|
// ResidualValueBlock is deleted (a sizeof of zero cannot be written in C++, so the exit gate is
|
|
// the type's absence - nothing in the tree can name it any more). What is left to pin is the
|
|
// RETIRED ROW: op 46 keeps its number, because opcodes only ever grow, and its record keeps its
|
|
// layout so a decoder can bounds-check what it then declines.
|
|
TEST(PipeCatalogue, ResidualBlockSizeIsPinned) {
|
|
EXPECT_EQ(static_cast<Uint32>(MGPWireOp::SetResidualValueState), 46u);
|
|
EXPECT_EQ(sizeof(MGPResidualValueState), 32u);
|
|
}
|
|
|
|
// P0.5 moved the value structs into MG_Pipe/MGPipeValueTypes.h. These are the runtime twins
|
|
// of that header's static assertions, so the numbers show up in ctest output on every
|
|
// platform - including one where a static assertion is skipped. Every number here is also
|
|
// what MGL_RESIDUAL_BLOCK_SIZE (MGPipeTypes.h) and the Espryt offsetof spans depend on.
|
|
TEST(PipeCatalogue, ValueTypeLayoutsArePinned) {
|
|
EXPECT_EQ(sizeof(PixelStoreParameters), 28u);
|
|
EXPECT_EQ(sizeof(PerBufferBlendState), 28u);
|
|
EXPECT_EQ(sizeof(StencilFaceState), 28u);
|
|
EXPECT_EQ(sizeof(RenderStateParameters), 1168u);
|
|
EXPECT_EQ(sizeof(SamplerParameters), 100u);
|
|
EXPECT_EQ(sizeof(MG_State::GLState::VertexAttributeVersion), 6u);
|
|
EXPECT_TRUE(std::is_trivially_copyable_v<PixelStoreParameters>);
|
|
EXPECT_TRUE(std::is_trivially_copyable_v<PerBufferBlendState>);
|
|
EXPECT_TRUE(std::is_trivially_copyable_v<StencilFaceState>);
|
|
EXPECT_TRUE(std::is_trivially_copyable_v<RenderStateParameters>);
|
|
EXPECT_TRUE(std::is_standard_layout_v<RenderStateParameters>);
|
|
EXPECT_TRUE(std::is_trivially_copyable_v<SamplerParameters>);
|
|
EXPECT_TRUE(std::is_trivially_copyable_v<MG_State::GLState::VertexAttributeVersion>);
|
|
EXPECT_LT(offsetof(RenderStateParameters, BlendStates), offsetof(RenderStateParameters, LogicOp));
|
|
EXPECT_EQ(std::tuple_size_v<decltype(RenderStateParameters::BlendStates)>, static_cast<SizeT>(kMGMaxDrawBuffers));
|
|
EXPECT_EQ(std::tuple_size_v<decltype(RenderStateParameters::ColorMasks)>, static_cast<SizeT>(kMGMaxDrawBuffers));
|
|
EXPECT_EQ(kMGMaxDrawBuffers, 8u);
|
|
}
|
|
|
|
// P2 ATE THE TWO VALUE STRUCTS AND THE PATCH TAIL the name still remembers, and the name
|
|
// stays because a removed test name is a gate failure of its own (G14, additions only).
|
|
// What it now pins is the other half of the same statement: the carrier is one capability
|
|
// word, at offset 0, and the members it used to carry are gone rather than merely moved -
|
|
// which is exactly what "MGL_RESIDUAL_BLOCK_SIZE only ever goes down" has to mean.
|
|
TEST(PipeCatalogue, ResidualBlockIsExactlyItsTwoValueStructsPlusPatchTail) {
|
|
// P13 W3c deleted the last capability word, so the carrier is gone altogether; the members
|
|
// it used to carry are pinned on the carriers that took them over.
|
|
// The three carriers that took the retired members over.
|
|
EXPECT_EQ(sizeof(MGPPixelPackState), sizeof(PixelStoreParameters));
|
|
EXPECT_EQ(sizeof(MGPPatchState), 40u);
|
|
EXPECT_EQ(sizeof(MGPBindRenderState), 12u);
|
|
}
|
|
|
|
// P4a's two payload edits, which are the only two the phase makes, and both are the kind a
|
|
// compiler catches only where somebody asked it to. MGP_ASSERT_POD already pins both sizes in
|
|
// MGPipeTypes.h; what is pinned HERE is the SHAPE the two edits were made for, because that is
|
|
// what a later phase would silently undo.
|
|
TEST(PipeCatalogue, TextureParamsNameTheirBuiltinSamplerAndFramebufferStateNamesItsTarget) {
|
|
// 32 -> 40: the CSO handle carrying the SamplerParameters of the SamplerObject every
|
|
// ITextureObject owns, plus the second resync bit. Naming the CSO rather than widening
|
|
// this payload with a filter/wrap/border block is what keeps ONE authority for one value -
|
|
// duplicating SamplerParameters on the wire would give two.
|
|
EXPECT_EQ(sizeof(MGPTextureParams), 40u);
|
|
EXPECT_EQ(offsetof(MGPTextureParams, Res), 0u);
|
|
EXPECT_EQ(offsetof(MGPTextureParams, BuiltinSampler), 8u);
|
|
EXPECT_EQ(offsetof(MGPTextureParams, SamplerResync), 26u);
|
|
// The two resync bits are SEPARATE bytes and must stay so: ForceResync guards a swizzle
|
|
// override the frontend params version does not move for, SamplerResync guards an
|
|
// incomplete texture sampling (0,0,0,1) after a driver re-mint. Different failures,
|
|
// different owners, one byte each.
|
|
EXPECT_NE(offsetof(MGPTextureParams, ForceResync), offsetof(MGPTextureParams, SamplerResync));
|
|
|
|
// Pad0 -> Uint8 Target, and the SIZE DID NOT MOVE, which is the whole point: the record
|
|
// describes one framebuffer OBJECT and Target says which binding(s), if any, it also
|
|
// sets, and that costs a byte the struct already had. Named (ID-19) cost nothing at all -
|
|
// it is a fourth value of a byte that was already there, which is why the applier could
|
|
// be given a per-object table without a wire change.
|
|
EXPECT_EQ(sizeof(MGPFramebufferState), 304u);
|
|
EXPECT_EQ(static_cast<Uint8>(MGPipeFramebufferTarget::Draw), 0u);
|
|
EXPECT_EQ(static_cast<Uint8>(MGPipeFramebufferTarget::Read), 1u);
|
|
EXPECT_EQ(static_cast<Uint8>(MGPipeFramebufferTarget::Both), 2u);
|
|
// Named = 3, and it is pinned by VALUE rather than merely by existence: the applier
|
|
// validates a record with `Target >= Count`, so an enumerator inserted ahead of Named
|
|
// would silently re-point every Named record the client already emits at Draw or Read -
|
|
// and a Draw record for a framebuffer that is not bound is the exact corruption Named
|
|
// exists to prevent (a DSA clear/blit landing on an unattached driver framebuffer).
|
|
EXPECT_EQ(static_cast<Uint8>(MGPipeFramebufferTarget::Named), 3u);
|
|
// Count is the applier's refusal bound and it is 4 now, not 3: a wire that still refused
|
|
// 3 would drop every DSA record on the floor.
|
|
EXPECT_EQ(static_cast<Uint8>(MGPipeFramebufferTarget::Count), 4u);
|
|
// The byte must be able to hold every value, since Target is a Uint8 in the record and
|
|
// the enum is the only thing that says what fits.
|
|
EXPECT_LE(static_cast<Uint32>(MGPipeFramebufferTarget::Count), 256u);
|
|
EXPECT_EQ(sizeof(MGPFramebufferState::Target), 1u);
|
|
// The wire's colour-attachment width is ONE width, and it is the wire's rather than the
|
|
// driver's: a driver reporting more attachments than this is refused at bring-up, never
|
|
// truncated into the record.
|
|
EXPECT_EQ(kMGPipeMaxColorAttachments, 8u);
|
|
EXPECT_EQ(std::extent_v<decltype(MGPFramebufferState::Color)>, kMGPipeMaxColorAttachments);
|
|
EXPECT_EQ(std::extent_v<decltype(MGPFramebufferState::DrawBuffers)>, kMGPipeMaxColorAttachments);
|
|
|
|
// And the two unit bounds, which bound all three var-tail sets. One merged unit space, no
|
|
// stage dimension.
|
|
EXPECT_EQ(kMGPipeMaxTextureUnits, 192u);
|
|
EXPECT_EQ(kMGPipeMaxImageUnits, 192u);
|
|
}
|
|
|
|
// D-A3: the resource-target enum minted beside the field, and the property that makes it worth
|
|
// minting - EVERY TextureTarget has a row, checked at compile time by a table with no
|
|
// `default:` arm, so adding a target is a build break rather than a descriptor that silently
|
|
// describes the wrong kind of storage.
|
|
TEST(PipeCatalogue, EveryTextureTargetMapsToItsOwnResourceTarget) {
|
|
// The compile-time half is MGPipeEveryTextureTargetIsMapped's static_assert; this is the
|
|
// same walk at runtime, so the case names the offender instead of the build naming a line.
|
|
for (SizeT i = 0; i < static_cast<SizeT>(TextureTarget::TextureTargetCount); ++i) {
|
|
const auto target = static_cast<TextureTarget>(i);
|
|
EXPECT_NE(MGPipeResourceTargetForTextureTarget(target), kMGPipeResourceTargetUnmapped)
|
|
<< "TextureTarget " << i << " has no MGPResourceDesc::Target row";
|
|
EXPECT_LT(MGPipeResourceTargetForTextureTarget(target),
|
|
static_cast<Uint32>(MGPipeResourceTarget::Count));
|
|
}
|
|
// Buffer is 0 and stays 0: P3a's constant is what a zero-initialised record already says,
|
|
// and the narrowed ack predicate below compares against it.
|
|
EXPECT_EQ(static_cast<Uint32>(MGPipeResourceTarget::Buffer), 0u);
|
|
EXPECT_EQ(kMGPipeResourceTargetBuffer, 0u);
|
|
// No texture target may collide with the buffer target, or a texture descriptor would ask
|
|
// for a synchronous acknowledgement.
|
|
for (SizeT i = 0; i < static_cast<SizeT>(TextureTarget::TextureTargetCount); ++i) {
|
|
EXPECT_NE(MGPipeResourceTargetForTextureTarget(static_cast<TextureTarget>(i)),
|
|
static_cast<Uint32>(kMGPipeResourceTargetBuffer));
|
|
}
|
|
// A rectangle texture is NOT a 2D texture on the wire. Espryt lowers both to GL_TEXTURE_2D
|
|
// at bind time and lowers Texture1D the same way, and Tex1D still has an enumerator of its
|
|
// own; folding rectangle onto Tex2D here would erase a distinction both backends switch on.
|
|
EXPECT_NE(MGPipeResourceTargetForTextureTarget(TextureTarget::Texture2D),
|
|
MGPipeResourceTargetForTextureTarget(TextureTarget::TextureRectangle));
|
|
}
|
|
|
|
// P4a, D-D3 / ID-12: MGPSubData::Target is TWO facts in one Uint16 - the low byte says which
|
|
// KIND of storage the destination is, the high byte which cube face / upload target the level
|
|
// belongs to - and the packing is the contract's, not each emitter's.
|
|
//
|
|
// The property this case exists for is the COLLISION the packing prevents.
|
|
// TextureUploadTarget::Texture1D is 0 and the applier's buffer branch tests the WHOLE field
|
|
// == 0, so a texture record carrying the bare upload enumerator would be indistinguishable
|
|
// from a buffer record exactly when its owner is a 1D texture, and that texture's upload
|
|
// would be dispatched into the buffer path. Nothing else in the tree would have said so.
|
|
TEST(PipeCatalogue, SubDataTargetPacksAResourceTargetAndAnUploadTarget) {
|
|
// Both halves must fit their byte, or the encoding is not an encoding.
|
|
static_assert(static_cast<Uint32>(MGPipeResourceTarget::Count) <= 0x100u);
|
|
static_assert(static_cast<Uint32>(TextureUploadTarget::TextureUploadTargetCount) <= 0x100u);
|
|
|
|
// 0 first, and deliberately: it is the enumerator that makes the collision possible. Then
|
|
// the plain 2D upload, the first and last cube face, and the largest enumerator the enum
|
|
// has, which is what proves the byte is wide enough in practice and not just in principle.
|
|
const Uint32 uploadTargets[] = {
|
|
0u,
|
|
static_cast<Uint32>(TextureUploadTarget::Texture2D),
|
|
static_cast<Uint32>(TextureUploadTarget::CubeMapPositiveX),
|
|
static_cast<Uint32>(TextureUploadTarget::CubeMapNegativeZ),
|
|
static_cast<Uint32>(TextureUploadTarget::TextureUploadTargetCount) - 1u,
|
|
};
|
|
for (Uint32 resource = 0; resource < static_cast<Uint32>(MGPipeResourceTarget::Count);
|
|
++resource) {
|
|
for (const Uint32 upload : uploadTargets) {
|
|
const Uint16 packed = MGPipePackSubDataTarget(resource, upload);
|
|
EXPECT_EQ(MGPipeSubDataResourceTargetOf(packed), static_cast<Uint8>(resource))
|
|
<< "resource target " << resource << " upload target " << upload;
|
|
EXPECT_EQ(MGPipeSubDataUploadTargetOf(packed), static_cast<Uint8>(upload))
|
|
<< "resource target " << resource << " upload target " << upload;
|
|
}
|
|
}
|
|
|
|
// THE BUFFER INVARIANT, at compile time in MGPipeTypes.h and again here so a failure names
|
|
// itself: a buffer record's Target is exactly kMGPipeResourceTargetBuffer, whole field,
|
|
// upload byte and all, so P3a's records are unchanged on the wire.
|
|
static_assert(MGPipePackSubDataTarget(kMGPipeResourceTargetBuffer, 0u) ==
|
|
kMGPipeResourceTargetBuffer);
|
|
EXPECT_EQ(MGPipePackSubDataTarget(kMGPipeResourceTargetBuffer, 0u), kMGPipeResourceTargetBuffer);
|
|
EXPECT_EQ(MGPipePackSubDataTarget(kMGPipeResourceTargetBuffer,
|
|
static_cast<Uint32>(TextureUploadTarget::Texture1D)),
|
|
kMGPipeResourceTargetBuffer);
|
|
MGPSubData zeroed{};
|
|
EXPECT_EQ(zeroed.Target, kMGPipeResourceTargetBuffer);
|
|
|
|
// ...and the other side of it: a 1D texture's upload target IS 0, and packed it still
|
|
// cannot be mistaken for a buffer, because no texture's resource target is 0.
|
|
EXPECT_EQ(static_cast<Uint32>(TextureUploadTarget::Texture1D), 0u);
|
|
for (Uint32 resource = 1; resource < static_cast<Uint32>(MGPipeResourceTarget::Count);
|
|
++resource) {
|
|
EXPECT_NE(MGPipePackSubDataTarget(resource, 0u), kMGPipeResourceTargetBuffer)
|
|
<< "resource target " << resource << " collides with a buffer record";
|
|
}
|
|
EXPECT_NE(MGPipePackSubDataTarget(MGPipeResourceTargetForTextureTarget(TextureTarget::Texture1D),
|
|
static_cast<Uint32>(TextureUploadTarget::Texture1D)),
|
|
kMGPipeResourceTargetBuffer);
|
|
|
|
// What a real cube-face record reads back as, through the field rather than a local.
|
|
MGPSubData record{};
|
|
record.Target =
|
|
MGPipePackSubDataTarget(MGPipeResourceTargetForTextureTarget(TextureTarget::TextureCubeMap),
|
|
static_cast<Uint32>(TextureUploadTarget::CubeMapNegativeY));
|
|
EXPECT_EQ(MGPipeSubDataResourceTargetOf(record.Target),
|
|
static_cast<Uint8>(MGPipeResourceTarget::TexCube));
|
|
EXPECT_EQ(MGPipeSubDataUploadTargetOf(record.Target),
|
|
static_cast<Uint8>(TextureUploadTarget::CubeMapNegativeY));
|
|
// Six faces share one resource target: the high byte is the only thing that tells them
|
|
// apart, which is why it cannot be dropped.
|
|
EXPECT_EQ(MGPipeSubDataResourceTargetOf(
|
|
MGPipePackSubDataTarget(static_cast<Uint32>(MGPipeResourceTarget::TexCube),
|
|
static_cast<Uint32>(TextureUploadTarget::CubeMapPositiveX))),
|
|
MGPipeSubDataResourceTargetOf(record.Target));
|
|
EXPECT_NE(MGPipeSubDataUploadTargetOf(
|
|
MGPipePackSubDataTarget(static_cast<Uint32>(MGPipeResourceTarget::TexCube),
|
|
static_cast<Uint32>(TextureUploadTarget::CubeMapPositiveX))),
|
|
MGPipeSubDataUploadTargetOf(record.Target));
|
|
}
|
|
|
|
// P4a, ID-12: the three constants MGPSurface::Kind is spelled with, the texture target the
|
|
// record grew where its Pad0 was, and MGPTextureParams::DepthStencilMode's two numbers.
|
|
//
|
|
// All three were UNSTATED in the contract and were being re-invented on both sides of the
|
|
// boundary - which is the way a wire field acquires two meanings. The values themselves are
|
|
// unremarkable; what this case pins is that there is exactly one spelling of each.
|
|
TEST(PipeCatalogue, SurfaceNamesItsKindItsTextureTargetAndItsDepthStencilAspect) {
|
|
// MGPipeKind is REUSED rather than a second three-value enum minted beside the field.
|
|
EXPECT_EQ(kMGPipeSurfaceKindNone, static_cast<Uint8>(MGPipeKind::None));
|
|
EXPECT_EQ(kMGPipeSurfaceKindTexture, static_cast<Uint8>(MGPipeKind::Texture));
|
|
EXPECT_EQ(kMGPipeSurfaceKindRenderbuffer, static_cast<Uint8>(MGPipeKind::Renderbuffer));
|
|
EXPECT_NE(kMGPipeSurfaceKindTexture, kMGPipeSurfaceKindRenderbuffer);
|
|
// None == 0 is load-bearing: it is what makes a zero-initialised record already BE the
|
|
// empty attachment point, which every emitter and every reader relies on.
|
|
EXPECT_EQ(kMGPipeSurfaceKindNone, 0u);
|
|
|
|
// Pad0 -> Uint16 TextureTarget. THE SIZE DID NOT MOVE - the two bytes were already there -
|
|
// and neither did anything in front of it.
|
|
EXPECT_EQ(sizeof(MGPSurface), 24u);
|
|
EXPECT_EQ(offsetof(MGPSurface, UploadTarget), 20u);
|
|
EXPECT_EQ(offsetof(MGPSurface, TextureTarget), 22u);
|
|
// The sentinel is TextureTarget::Unknown widened, so it is a value no real target has.
|
|
EXPECT_EQ(kMGPipeSurfaceNoTextureTarget, 0xFFFFu);
|
|
EXPECT_EQ(kMGPipeSurfaceNoTextureTarget, static_cast<Uint16>(TextureTarget::Unknown));
|
|
for (SizeT i = 0; i < static_cast<SizeT>(TextureTarget::TextureTargetCount); ++i) {
|
|
EXPECT_NE(static_cast<Uint16>(i), kMGPipeSurfaceNoTextureTarget);
|
|
}
|
|
|
|
// A ZEROED MGPSurface CARRIES TextureTarget 0, AND 0 IS TextureTarget::Texture1D, NOT THE
|
|
// SENTINEL. That is documented rather than defended, and it is why the field's contract is
|
|
// "consulted only when Kind == kMGPipeSurfaceKindTexture": a zeroed record is Kind == None
|
|
// and names no texture at all, so a reader that gates on Kind can never see the 0. A
|
|
// reader that does not gate would read Texture1D out of an empty attachment point.
|
|
MGPSurface empty{};
|
|
EXPECT_EQ(empty.TextureTarget, 0u);
|
|
EXPECT_EQ(static_cast<Uint16>(TextureTarget::Texture1D), 0u);
|
|
EXPECT_EQ(empty.Kind, kMGPipeSurfaceKindNone);
|
|
EXPECT_TRUE(MGPipeHandleIsNull(empty.Res));
|
|
|
|
// A renderbuffer point names no texture and says so with the sentinel, which is what
|
|
// distinguishes "not a texture" from "a 1D texture" for a reader that looks anyway.
|
|
MGPSurface renderbuffer{};
|
|
renderbuffer.Kind = kMGPipeSurfaceKindRenderbuffer;
|
|
renderbuffer.TextureTarget = kMGPipeSurfaceNoTextureTarget;
|
|
EXPECT_NE(renderbuffer.TextureTarget, static_cast<Uint16>(TextureTarget::Texture1D));
|
|
|
|
// The half a compiler cannot catch: the PipeFields.def row. MGPSurface still asserts its
|
|
// size whether or not the field list names TextureTarget, so a comparator blind to the
|
|
// field would pass a target-only divergence under MOBILEGL_PIPE_VERIFY - and the field is
|
|
// exactly what the four cross-object masks key on.
|
|
MGPSurface a{};
|
|
MGPSurface b{};
|
|
const char* field = nullptr;
|
|
EXPECT_TRUE(MGPipeVerify(a, b, &field));
|
|
a.TextureTarget = static_cast<Uint16>(TextureTarget::TextureCubeMap);
|
|
EXPECT_FALSE(MGPipeVerify(a, b, &field));
|
|
EXPECT_STREQ(field, "TextureTarget");
|
|
|
|
// DepthStencilMode: 0 = GL_DEPTH_COMPONENT, 1 = GL_STENCIL_INDEX. Depth is 0 because it is
|
|
// the GL initial value and a texture that never asks for the stencil aspect never emits
|
|
// the call, so a zeroed record has to decode to what an untouched texture already has.
|
|
EXPECT_EQ(kMGPipeDepthStencilModeDepth, 0u);
|
|
EXPECT_EQ(kMGPipeDepthStencilModeStencil, 1u);
|
|
EXPECT_NE(kMGPipeDepthStencilModeDepth, kMGPipeDepthStencilModeStencil);
|
|
MGPTextureParams params{};
|
|
EXPECT_EQ(params.DepthStencilMode, kMGPipeDepthStencilModeDepth);
|
|
}
|
|
|
|
// G3's opcode numbering is the wire protocol. Position in PipeCalls.def, 1-based, no holes.
|
|
TEST(PipeCatalogue, WireOpcodesAreThePositionsInTheCatalogue) {
|
|
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::GetCaps), 1);
|
|
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::kOpCount), kMGPipeCallCount + 1);
|
|
EXPECT_EQ(sizeof(MGPWireRecHeader), 8u);
|
|
// Every record is a multiple of the stream's 8-byte granularity, which is half of the
|
|
// applier's precondition.
|
|
EXPECT_EQ(sizeof(MGPWireRec_DrawVbo) % 8, 0u);
|
|
EXPECT_EQ(sizeof(MGPWireRec_BindRenderState) % 8, 0u);
|
|
EXPECT_EQ(sizeof(MGPWireRec_SetResidualValueState) % 8, 0u);
|
|
}
|
|
|
|
// Records are append-only. The three carriers added after the first cut - for the live
|
|
// GLFunctionsTable entries GetGpuTimestampNs, QueryCounterTimestamp and WaitSync - sit at
|
|
// the END of the list, after SetSwapInterval, so no opcode the first cut assigned has moved.
|
|
TEST(PipeCatalogue, LateArrivalsAreAppendedWithoutRenumbering) {
|
|
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::SetSwapInterval), 68);
|
|
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::QueryTimestamp), 69);
|
|
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::QueryCounter), 70);
|
|
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::FenceWaitServer), 71);
|
|
// P5b (MG_Remote/CONTRACT-P5B.md) appended five verbs AFTER P0's three late arrivals, by
|
|
// the same rule: opcodes 72..76, and nothing before them moved. The five are pinned by
|
|
// VALUE, because the pin is the protocol - a row inserted ahead of one would silently
|
|
// re-point every record a P5b package emits at the wrong arm.
|
|
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::BindShaderImage), 72);
|
|
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::PatchParameter), 73);
|
|
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::BindStreamOutput), 74);
|
|
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::SetStorageBlockBinding), 75);
|
|
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::CopyFramebufferToTexture), 76);
|
|
// P5c (MG_Remote/CONTRACT-P5C.md §5) appended the two control records AFTER P5b's five, by
|
|
// the same rule: opcodes 77..78, and nothing before them moved. applier_reset is a kScreen
|
|
// row (a make-current is a whole-server edge, exactly as FenceWaitServer is a screen call)
|
|
// and object_death a kCtxObject one; both carry no blob, no reply and no tail.
|
|
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::ApplierReset), 77);
|
|
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::ObjectDeath), 78);
|
|
// P5c rv (§5.3) appended the residual-value record AFTER the two control records, by the
|
|
// same rule: opcode 79, and nothing before it moved. It is an ordinary kCtxState set_* row
|
|
// - fixed POD, no blob, no tail, no reply - with an MGPipeApply* entry point, which the two
|
|
// control records deliberately do not have.
|
|
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::SetContextValues), 79);
|
|
// P5e (MG_Remote/CONTRACT-P5E.md §1) appended ONE more, by the same rule: opcode 80, and
|
|
// nothing before it moved. set_program_bindings is a kCtxState row with THREE tails and a
|
|
// host span in the third; it lands with a null route and a sink that refuses it by name,
|
|
// exactly as set_shader_buffers has sat catalogued-and-dead since P4a.
|
|
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::SetProgramBindings), 80);
|
|
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::DeleteStreamOutput), 81);
|
|
// P9 (MG_Remote/CONTRACT-P9.md §1) appended the two pack-buffer readbacks, by the same rule:
|
|
// opcodes 82..83, one payload, no flag at all - no reply slot, no blob, no tail.
|
|
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::ReadPixelsToBuffer), 82);
|
|
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::GetTextureImageToBuffer), 83);
|
|
// P14 S1 (docs/Disaggregated/design/11-state-ownership.md) appended the in-band context
|
|
// binding, by the same rule: opcode 84, and nothing before it moved. bind_context is a
|
|
// kScreen control record with the applier_reset shape - one POD, no blob, no tail, no reply
|
|
// and no MGPipeApply* entry point - so the row beside it is pinned the same way.
|
|
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::BindContext), 84);
|
|
// Shared images (docs/Disaggregated/notes/anland/plan-ahb-dmabuf.md) appended opcode 85 by the
|
|
// same rule: a kScreen row with a reply slot - every operation is answered.
|
|
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::SharedImage), 85);
|
|
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::kOpCount), 86);
|
|
EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::SharedImage), static_cast<Uint32>(kReplySlot));
|
|
EXPECT_EQ(MGPipeCallClassFor(MGPWireOp::SharedImage), kScreen);
|
|
EXPECT_EQ(MGPipeWaitClassFor(MGPWireOp::SharedImage), kWaitReply);
|
|
EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::ReadPixelsToBuffer), static_cast<Uint32>(kNone));
|
|
EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::GetTextureImageToBuffer), static_cast<Uint32>(kNone));
|
|
EXPECT_EQ(sizeof(MGPReadbackToBuffer), 104u);
|
|
EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::SetProgramBindings),
|
|
static_cast<Uint32>(kVarTail | kHostSpan));
|
|
EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::ApplierReset), static_cast<Uint32>(kNone));
|
|
EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::ObjectDeath), static_cast<Uint32>(kNone));
|
|
EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::SetContextValues), static_cast<Uint32>(kNone));
|
|
// P14 S1's row, pinned like the two control records it sits beside: no blob, no tail, no
|
|
// reply, and kWaitNone - the record's placement in the ring is the whole of its ordering, so
|
|
// the client publishes and moves on rather than parking on an apply that reads no client
|
|
// memory (PipeCalls.def's wait-class column).
|
|
EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::BindContext), static_cast<Uint32>(kNone));
|
|
EXPECT_EQ(MGPipeCallClassFor(MGPWireOp::BindContext), kScreen);
|
|
EXPECT_EQ(MGPipeWaitClassFor(MGPWireOp::BindContext), kWaitNone);
|
|
// And the P5b rows carry what their contract says: one blob (the block name) and nothing
|
|
// else, and the extended draw row keeps its two flags.
|
|
EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::SetStorageBlockBinding), static_cast<Uint32>(kHasBlob));
|
|
EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::BindShaderImage), static_cast<Uint32>(kNone));
|
|
EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::PatchParameter), static_cast<Uint32>(kNone));
|
|
EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::BindStreamOutput), static_cast<Uint32>(kNone));
|
|
EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::CopyFramebufferToTexture), static_cast<Uint32>(kNone));
|
|
EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::DrawVbo), static_cast<Uint32>(kHostSpan | kVarTail));
|
|
// The P5b payload sizes, pinned like every other MGP_ASSERT_POD at runtime so the numbers
|
|
// show up in ctest output; MGPCopyRegion grew 64 -> 72 for its two GL names (i1).
|
|
EXPECT_EQ(sizeof(MGPImageBind), 40u);
|
|
EXPECT_EQ(sizeof(MGPPatchParameter), 8u);
|
|
EXPECT_EQ(sizeof(MGPStreamOutputBind), 16u);
|
|
EXPECT_EQ(sizeof(MGPStorageBlockBinding), 40u);
|
|
EXPECT_EQ(sizeof(MGPCopyFromFramebuffer), 48u);
|
|
EXPECT_EQ(sizeof(MGPCopyRegion), 72u);
|
|
EXPECT_EQ(sizeof(MGPDrawIndirect), 40u);
|
|
// The P5c payload, pinned like every other MGP_ASSERT_POD at runtime: one Uint64, no
|
|
// padding. object_death REUSES MGPHandleOnly (CONTRACT-P5C.md §1), so there is no second
|
|
// struct to pin - the 16 bytes are pinned above with the handle family.
|
|
EXPECT_EQ(sizeof(MGPApplierReset), 8u);
|
|
// P14 S1: bind_context's POD. ONE Uint64 - the same width the control frame's token is, so
|
|
// the two planes cannot disagree about how much of a token is real.
|
|
EXPECT_EQ(sizeof(MGPBindContext), 8u);
|
|
EXPECT_EQ(sizeof(MGPBindContext::ClientContextToken), 8u);
|
|
// Shared images: the request, its answer and the aux-socket sideband are fixed-width PODs; the
|
|
// sideband is the T0 Offer's size so one inbox reads both. The request carries a present's copy
|
|
// region (kMGPMaxDamageRects rectangles), as the present record carries its frame's damage.
|
|
EXPECT_EQ(sizeof(MGPSharedImageOp), 296u);
|
|
EXPECT_EQ(sizeof(MGPPresent), 272u);
|
|
EXPECT_EQ(sizeof(MGPSharedImageReply), 48u);
|
|
EXPECT_EQ(sizeof(MGPSharedImageFdOffer), 32u);
|
|
EXPECT_EQ(sizeof(MGPHandleOnly), 16u);
|
|
// rv's two (§5.3/§7.6): the residual-value POD - 2 + 15 Uint32s, 2 Uint8s and 2 pad bytes,
|
|
// then the three Uint64s - and the AMENDED attribute carrier, which grew 24 -> 56 to carry
|
|
// all three views verbatim (the frontend's cross-view conversion is the authoritative
|
|
// answer; the applier no longer reconverts).
|
|
EXPECT_EQ(sizeof(MGPContextValues), 96u);
|
|
EXPECT_EQ(sizeof(MGPContextValues::TouchedBufferBindingPointCount), 60u);
|
|
EXPECT_EQ(sizeof(MGPAttribValue), 56u);
|
|
EXPECT_EQ(sizeof(MGPVertexAttribDefaults), 8u);
|
|
// P5e's payloads (CONTRACT-P5E.md §1), pinned at runtime like every other MGP_ASSERT_POD.
|
|
//
|
|
// MGPProgramDesc GREW 192 -> 200, and that is the phase's one wire-format widening: the
|
|
// descriptor's four trailing Uint8s end exactly at offset 24 and MGPBlobRef is 8-aligned,
|
|
// so there was no spare byte for LinkStatus and the draft's "in the descriptor's existing
|
|
// pad" was wrong. Pinned here, by value, because a record whose size moved silently is a
|
|
// protocol break no other test would see - and because THIS number is the red-once the
|
|
// phase names: change MGPProgramBindings' or the descriptor's size and this case fails.
|
|
EXPECT_EQ(sizeof(MGPProgramDesc), 200u);
|
|
EXPECT_EQ(sizeof(MGPProgramBindings), 32u);
|
|
EXPECT_EQ(sizeof(MGPProgramSamplerUnit), 8u);
|
|
EXPECT_EQ(sizeof(MGPProgramStorageOverride), 40u);
|
|
// The three declared tail bounds. A program past one of them is a COUNTED REFUSAL and never
|
|
// a truncation, so the numbers are the contract rather than an implementation detail.
|
|
EXPECT_EQ(kMGPipeMaxProgramBlockBindings, 64u);
|
|
EXPECT_EQ(kMGPipeMaxProgramSamplerUnits, 256u);
|
|
EXPECT_EQ(kMGPipeMaxProgramStorageOverrides, 64u);
|
|
// LinkStatus is a real byte and a zeroed descriptor means "not linked", which is what makes
|
|
// the server's decline the safe default.
|
|
MGPProgramDesc program{};
|
|
EXPECT_EQ(program.LinkStatus, 0u);
|
|
// set_shader_buffers' capacity, pinned on the wire side; MG_Impl/Pipe/PipeFill.cpp is the
|
|
// one translation unit that also sees BufferState.h's constant and pins them together.
|
|
EXPECT_EQ(kMGPipeMaxBufferBindingPoints, 84u);
|
|
// The two draw-flag bits P5b's d1 arms are exclusive by contract and distinct by value.
|
|
EXPECT_EQ(static_cast<Uint32>(kDrawIsIndirect), 1u << 5);
|
|
EXPECT_EQ(static_cast<Uint32>(kDrawHasUserIndices) & static_cast<Uint32>(kDrawIsIndirect), 0u);
|
|
// P5e's draw flag: the next free bit, and disjoint from all five before it.
|
|
EXPECT_EQ(static_cast<Uint32>(kDrawClientArrays), 1u << 6);
|
|
EXPECT_EQ(static_cast<Uint32>(kDrawClientArrays) &
|
|
static_cast<Uint32>(kDrawHasUserIndices | kDrawPrimitiveRestart |
|
|
kDrawIndicesAreClient | kDrawHasIndexRange |
|
|
kDrawHasXfbCount | kDrawIsIndirect),
|
|
0u);
|
|
// ... and it still fits the one byte MGPDrawInfo::Flags is, which is why the record did not
|
|
// grow: MGPDrawInfo stays 56 bytes.
|
|
EXPECT_EQ(sizeof(MGPDrawInfo), 56u);
|
|
EXPECT_LE(static_cast<Uint32>(kDrawClientArrays), 0xFFu);
|
|
// The clear discriminants have ONE spelling now, and Whole is 0 so a zeroed record is a
|
|
// whole-framebuffer clear.
|
|
EXPECT_EQ(kMGPipeClearKindWhole, 0u);
|
|
EXPECT_EQ(kMGPipeClearKindDepthStencil + 1, kMGPipeClearKindCount);
|
|
EXPECT_EQ(kMGPipeClearValueClassUint + 1, kMGPipeClearValueClassCount);
|
|
}
|
|
|
|
// ---- P5e: the wait-class column (MG_Remote/CONTRACT-P5E.md §2.2) --------------------------
|
|
//
|
|
// THE POINT OF THIS CASE IS THAT IT DOES NOT READ PipeCalls.def. Every other statement about
|
|
// the column - the generated table, the two gates in gen_pipe.py, the static_asserts in
|
|
// PipeWire.inc - is derived from the def, so all of them move together when a row is edited.
|
|
// This case re-states CONTRACT-P5E §2.2's table BY NAME, in the contract's own grouping, so a
|
|
// row silently moved from kWaitApplied to kWaitNone has to be argued in two places or it goes
|
|
// red here. That is the difference between a table and a contract.
|
|
//
|
|
// It matters because the column is not decoration: on a run-ahead server kWaitNone is a promise
|
|
// that the record's apply reads NOTHING of the client's (rule F), and a row demoted to it by
|
|
// accident is a torn read that renders wrong rather than aborting.
|
|
TEST(PipeCatalogue, EveryRowCarriesTheWaitClassTheContractGivesIt) {
|
|
// kWaitPresent: present alone, paced by the credit taken BEFORE the encode (§2.4).
|
|
EXPECT_EQ(MGPipeWaitClassFor(MGPWireOp::Present), kWaitPresent);
|
|
|
|
// kWaitReply: every row whose answer is not derivable (R-5). Named, all fourteen, because
|
|
// the flag-to-class agreement is what keeps a waiter from hanging on a slot nobody posts.
|
|
const MGPWireOp replyRows[] = {
|
|
MGPWireOp::GetCaps, MGPWireOp::ResourceCreate, MGPWireOp::ResourceRespecify,
|
|
MGPWireOp::MapPersistent, MGPWireOp::FenceStatus, MGPWireOp::FenceWait,
|
|
MGPWireOp::QueryAvailable, MGPWireOp::QueryResult, MGPWireOp::QueryTimestamp,
|
|
MGPWireOp::SetTextureParams, MGPWireOp::ResourceSubData, MGPWireOp::ResourceReadback,
|
|
MGPWireOp::GetTextureImage, MGPWireOp::ReadPixels,
|
|
};
|
|
for (const MGPWireOp op : replyRows) {
|
|
EXPECT_EQ(MGPipeWaitClassFor(op), kWaitReply) << WireOpNameForDiag(op);
|
|
EXPECT_NE(MGPipeCallFlagsFor(op) & static_cast<Uint32>(kReplySlot), 0u)
|
|
<< WireOpNameForDiag(op);
|
|
}
|
|
|
|
// kWaitApplied: only applier_reset is left. P15 (user decision: everything fire-and-forget
|
|
// is async) moved generate_mipmap, resource_copy_region, copy_framebuffer_to_texture,
|
|
// set_storage_block_binding and the five stream-output rows to kWaitNone: their applies
|
|
// resolve everything from the record's own handles (VerbMipRes, VerbCopyTexDst, the copy's
|
|
// endpoints, the storage-block program CSO, the P5f server-side span), and the dual-block
|
|
// census - where any client-filled read is a named Fatal - is empty. applier_reset keeps its
|
|
// wait: it is once per make-current and its apply probes the client allocator, which is only
|
|
// legal inside a parked scope.
|
|
const MGPWireOp appliedRows[] = {
|
|
MGPWireOp::ApplierReset,
|
|
};
|
|
for (const MGPWireOp op : appliedRows) {
|
|
EXPECT_EQ(MGPipeWaitClassFor(op), kWaitApplied) << WireOpNameForDiag(op);
|
|
}
|
|
|
|
// kWaitNone: the steady draw path and every state/CSO/object row without a reply. Named for
|
|
// the rows the phase exists for, and then counted for the rest, so a new row cannot join
|
|
// the set unnoticed.
|
|
const MGPWireOp noneRows[] = {
|
|
MGPWireOp::DrawVbo, MGPWireOp::LaunchGrid, MGPWireOp::Clear,
|
|
MGPWireOp::Blit, MGPWireOp::MemoryBarrier, MGPWireOp::Flush,
|
|
MGPWireOp::SetSwapInterval, MGPWireOp::BindShaderImage, MGPWireOp::PatchParameter,
|
|
MGPWireOp::ObjectDeath, MGPWireOp::ResourceDestroy, MGPWireOp::UnmapPersistent,
|
|
MGPWireOp::FenceCreate, MGPWireOp::FenceDestroy, MGPWireOp::FenceWaitServer,
|
|
MGPWireOp::QueryCreate, MGPWireOp::QueryBegin, MGPWireOp::QueryEnd,
|
|
MGPWireOp::QueryDestroy, MGPWireOp::QueryCounter, MGPWireOp::SetContextValues,
|
|
MGPWireOp::SetProgramBindings, MGPWireOp::SetShaderBuffers,
|
|
MGPWireOp::SetVertexBuffers, MGPWireOp::SetFramebufferState,
|
|
MGPWireOp::CreateShaderState, MGPWireOp::SetDrawProgram,
|
|
// P9: the pack-buffer readbacks - the point of the two rows is that nothing waits.
|
|
MGPWireOp::ReadPixelsToBuffer, MGPWireOp::GetTextureImageToBuffer,
|
|
// P15: fire-and-forget.
|
|
MGPWireOp::GenerateMipmap, MGPWireOp::ResourceCopyRegion, MGPWireOp::CopyFramebufferToTexture,
|
|
MGPWireOp::SetStorageBlockBinding, MGPWireOp::BeginStreamOutput, MGPWireOp::EndStreamOutput,
|
|
MGPWireOp::PauseStreamOutput, MGPWireOp::ResumeStreamOutput, MGPWireOp::BindStreamOutput,
|
|
};
|
|
for (const MGPWireOp op : noneRows) {
|
|
EXPECT_EQ(MGPipeWaitClassFor(op), kWaitNone) << WireOpNameForDiag(op);
|
|
}
|
|
|
|
// The partition, by count. 15 + 1 + 1 = 17 rows wait (shared_image made the reply rows 15; P15
|
|
// left one kWaitApplied row); every other row of the catalogue does
|
|
// not. A row that changed class moves two of these numbers at once - which is why the
|
|
// kWaitApplied count went 9 -> 10 and the kWaitNone offset 24 -> 25 in the SAME commit that
|
|
// moved resource_copy_region (P5e gl, ID-118).
|
|
SizeT reply = 0, applied = 0, present = 0, none = 0, other = 0;
|
|
for (SizeT i = 1; i < static_cast<SizeT>(MGPWireOp::kOpCount); ++i) {
|
|
switch (MGPipeWaitClassFor(static_cast<MGPWireOp>(i))) {
|
|
case kWaitReply: ++reply; break;
|
|
case kWaitApplied: ++applied; break;
|
|
case kWaitPresent: ++present; break;
|
|
case kWaitNone: ++none; break;
|
|
default: ++other; break;
|
|
}
|
|
}
|
|
EXPECT_EQ(reply, 15u);
|
|
EXPECT_EQ(applied, 1u);
|
|
EXPECT_EQ(present, 1u);
|
|
EXPECT_EQ(none, static_cast<SizeT>(kMGPipeCallCount) - 17u);
|
|
EXPECT_EQ(other, 0u) << "a row carries the kWaitClassCount terminator as its class";
|
|
|
|
// And the reply half of the partition BOTH WAYS, over the whole catalogue: exactly the rows
|
|
// that own a slot wait for one. gen_pipe.py refuses a catalogue where they disagree; this
|
|
// is the same statement where a reader of the table will look for it.
|
|
for (SizeT i = 1; i < static_cast<SizeT>(MGPWireOp::kOpCount); ++i) {
|
|
const auto op = static_cast<MGPWireOp>(i);
|
|
const Bool ownsSlot = (MGPipeCallFlagsFor(op) & static_cast<Uint32>(kReplySlot)) != 0;
|
|
EXPECT_EQ(ownsSlot, MGPipeWaitClassFor(op) == kWaitReply) << WireOpNameForDiag(op);
|
|
}
|
|
|
|
// Opcode 0 is not a call and has no class, for the flags table's reason: a decoder holding
|
|
// a byte off a corrupt stream must reach its own Fatal rather than a wait decision.
|
|
EXPECT_EQ(MGPipeWaitClassFor(MGPWireOp::kInvalid), kWaitClassCount);
|
|
EXPECT_EQ(MGPipeWaitClassFor(MGPWireOp::kOpCount), kWaitClassCount);
|
|
}
|
|
|
|
// A well-formed record passes the applier's bounds gate. P0 has no applier, so "accepted"
|
|
// is reported as "not applied" rather than "fatal".
|
|
TEST(PipeCatalogue, ApplierAcceptsAWellFormedRecord) {
|
|
MGPWireRec_Present record{};
|
|
record.Header.Op = static_cast<Uint16>(MGPWireOp::Present);
|
|
record.Header.Size = sizeof(record);
|
|
record.Payload.FrameSerial = 42;
|
|
EXPECT_FALSE(MGPipeApplyWireRecord(MGPWireOp::Present, &record, sizeof(record), sizeof(record)));
|
|
}
|
|
|
|
// G4 reports the FIRST differing field by name, and compares field by field so that
|
|
// padding cannot produce a difference that does not exist.
|
|
TEST(PipeCatalogue, VerifyComparatorNamesTheDifferingField) {
|
|
MGPDrawInfo a{};
|
|
MGPDrawInfo b{};
|
|
const char* field = nullptr;
|
|
EXPECT_TRUE(MGPipeVerify(a, b, &field));
|
|
|
|
b.InstanceCount = 7;
|
|
EXPECT_FALSE(MGPipeVerify(a, b, &field));
|
|
EXPECT_STREQ(field, "InstanceCount");
|
|
|
|
// Padding bytes are not fields: writing to them cannot make two payloads differ.
|
|
MGPBindRenderState c{};
|
|
MGPBindRenderState d{};
|
|
c.Cso = MGPipeHandle{3, 1};
|
|
d.Cso = MGPipeHandle{3, 1};
|
|
field = nullptr;
|
|
EXPECT_TRUE(MGPipeVerify(c, d, &field));
|
|
|
|
// Nested payloads recurse, and arrays compare element-wise.
|
|
MGPFramebufferState left{};
|
|
MGPFramebufferState right{};
|
|
right.Color[3].Level = 2;
|
|
EXPECT_FALSE(MGPipeVerify(left, right, &field));
|
|
EXPECT_STREQ(field, "Color");
|
|
}
|
|
|
|
// G6's join over the backend read inventory. P0 allows unmapped rows; from P5 the gate is
|
|
// zero, so the numbers are asserted here to make a regression visible the day it happens.
|
|
TEST(PipeCatalogue, CoverageAccountsForEveryInventoryRow) {
|
|
EXPECT_EQ(kMGPipeInventoryReadPoints, 477u);
|
|
EXPECT_EQ(kMGPipeInventoryUnmapped, 0u);
|
|
EXPECT_EQ(kMGPipeInventoryMappedToCall + kMGPipeInventoryClientResolved +
|
|
kMGPipeInventoryReverseChannel + kMGPipeInventoryStructuralHandle +
|
|
kMGPipeInventoryUnmapped,
|
|
kMGPipeInventoryReadPoints);
|
|
EXPECT_GT(kMGPipeCoverageEntryCount, 0u);
|
|
}
|
|
|
|
// G5's field ids come from the same accessor list as the coverage table, and every field
|
|
// starts un-filled: reading one before its verb fills it is the poison's whole job.
|
|
TEST(PipeCatalogue, PipeInputFieldsStartUnfilled) {
|
|
EXPECT_EQ(kMGPipeInputFieldCount, 63u);
|
|
MGPipeFilledState state{};
|
|
// Before the first fill the serial is 0 as well: 0 == 0 must not read as fresh, on the
|
|
// sticky branch either (the window D6 names "<Field>@<none>").
|
|
EXPECT_EQ(state.CurrentVerbSerial, 0u);
|
|
for (SizeT f = 0; f < kMGPipeInputFieldCount; ++f) {
|
|
EXPECT_FALSE(MGPipeInputFieldIsFresh(state, static_cast<MGPipeInputField>(f))) << kMGPipeInputFieldNames[f];
|
|
}
|
|
state.CurrentVerbSerial = 1;
|
|
EXPECT_FALSE(MGPipeInputFieldIsFresh(state, MGPipeInputField::GetRenderStateParameters));
|
|
state.FilledGen[static_cast<SizeT>(MGPipeInputField::GetRenderStateParameters)] = 1;
|
|
EXPECT_TRUE(MGPipeInputFieldIsFresh(state, MGPipeInputField::GetRenderStateParameters));
|
|
// The next verb makes the same value stale, which a written-once bitmap could not see.
|
|
state.CurrentVerbSerial = 2;
|
|
EXPECT_FALSE(MGPipeInputFieldIsFresh(state, MGPipeInputField::GetRenderStateParameters));
|
|
}
|
|
|
|
// G5b: the verb enum is GLFunctionsTable's member list (69 entries), every class has verbs,
|
|
// and the seven sticky fields ride in every class mask (P1 brief D7).
|
|
TEST(PipeCatalogue, VerbTableIsTheFunctionTable) {
|
|
EXPECT_EQ(kMGPipeVerbCount, 69u);
|
|
EXPECT_EQ(kMGPipeVerbClassCount, 9u);
|
|
SizeT perClass[kMGPipeVerbClassCount] = {};
|
|
for (SizeT v = 0; v < kMGPipeVerbCount; ++v) {
|
|
++perClass[static_cast<SizeT>(kMGPipeVerbClass[v])];
|
|
}
|
|
for (SizeT c = 0; c < kMGPipeVerbClassCount; ++c) {
|
|
EXPECT_GT(perClass[c], 0u) << kMGPipeVerbClassNames[c];
|
|
for (SizeT f = 0; f < kMGPipeInputFieldCount; ++f) {
|
|
if (kMGPipeInputFieldSticky[f]) {
|
|
EXPECT_TRUE(MGPipeFieldMaskHas(kMGPipeClassFieldMask[c], static_cast<MGPipeInputField>(f)))
|
|
<< kMGPipeInputFieldNames[f] << " in " << kMGPipeVerbClassNames[c];
|
|
}
|
|
}
|
|
}
|
|
// The class table of D7, spot-checked at its edges: a draw reads the render state, a
|
|
// query reads only the paused-primitive counter, and GenerateMipmap is a texture op.
|
|
const auto& draw = kMGPipeClassFieldMask[static_cast<SizeT>(MGPipeVerbClass::kDraw)];
|
|
const auto& query = kMGPipeClassFieldMask[static_cast<SizeT>(MGPipeVerbClass::kQuery)];
|
|
EXPECT_TRUE(MGPipeFieldMaskHas(draw, MGPipeInputField::GetRenderStateParameters));
|
|
EXPECT_FALSE(MGPipeFieldMaskHas(query, MGPipeInputField::GetRenderStateParameters));
|
|
EXPECT_TRUE(MGPipeFieldMaskHas(query, MGPipeInputField::GetTransformFeedbackPausedPrimitiveCounter));
|
|
EXPECT_EQ(kMGPipeVerbClass[static_cast<SizeT>(MGPipeVerb::GenerateMipmap)], MGPipeVerbClass::kTextureOp);
|
|
EXPECT_STREQ(kMGPipeVerbNames[static_cast<SizeT>(MGPipeVerb::GetGpuTimestampNs)], "GetGpuTimestampNs");
|
|
}
|
|
|
|
// The sticky set is exactly the seven forwarded, argument-keyed accessors (P1 brief D6); no
|
|
// version or generation accessor is among them.
|
|
TEST(PipeCatalogue, StickyFieldsAreExactlyTheSeven) {
|
|
const char* const expected[] = {"GetBufferBindingPointCount", "GetProgramObject", "GetTextureObject",
|
|
"HasOpenTransformFeedbackSpan", "InvalidateCompileEnv", "ValidateProgramName",
|
|
"RecordError"};
|
|
SizeT count = 0;
|
|
for (SizeT f = 0; f < kMGPipeInputFieldCount; ++f) {
|
|
Bool listed = false;
|
|
for (const char* name : expected) {
|
|
if (std::strcmp(kMGPipeInputFieldNames[f], name) == 0) listed = true;
|
|
}
|
|
EXPECT_EQ(kMGPipeInputFieldSticky[f], listed) << kMGPipeInputFieldNames[f];
|
|
if (kMGPipeInputFieldSticky[f]) ++count;
|
|
}
|
|
EXPECT_EQ(count, 7u);
|
|
EXPECT_EQ(kMGPipeInputStickyFieldCount, 7u);
|
|
EXPECT_FALSE(kMGPipeInputFieldSticky[static_cast<SizeT>(MGPipeInputField::GetTextureContextId)]);
|
|
EXPECT_FALSE(kMGPipeInputFieldSticky[static_cast<SizeT>(MGPipeInputField::GetSamplingResolutionGeneration)]);
|
|
EXPECT_FALSE(kMGPipeInputFieldSticky[static_cast<SizeT>(MGPipeInputField::GetPipelineStateVersion)]);
|
|
}
|
|
|
|
// G4 compares floating point BY BITS (P1 brief D8): a NaN equals itself, a negative zero
|
|
// does not equal a positive one, and a vector type inside an Array inside a value struct is
|
|
// reached field by field - the differing member of the residual block is named.
|
|
TEST(PipeCatalogue, FloatVectorsCompareBitwise) {
|
|
const Float nan = std::numeric_limits<Float>::quiet_NaN();
|
|
const FloatVec4 a{nan, 1.f, 2.f, 3.f};
|
|
const FloatVec4 b{nan, 1.f, 2.f, 3.f};
|
|
EXPECT_TRUE(MGPipeFieldEqual(a, b));
|
|
EXPECT_FALSE(a == b); // IEEE ==, the comparison the comparator must NOT use
|
|
const FloatVec4 zero{0.f, 0.f, 0.f, 0.f};
|
|
const FloatVec4 negativeZero{-0.f, 0.f, 0.f, 0.f};
|
|
EXPECT_FALSE(MGPipeFieldEqual(zero, negativeZero));
|
|
EXPECT_TRUE(zero == negativeZero);
|
|
EXPECT_TRUE(MGPipeFieldEqual(1.5f, 1.5f));
|
|
EXPECT_FALSE(MGPipeFieldEqual(-0.f, 0.f));
|
|
|
|
// The nested-struct case is demonstrated on RenderStateParameters directly - which is where
|
|
// it actually matters now that the block travels as create/bind_render_state chunks.
|
|
RenderStateParameters leftState{};
|
|
RenderStateParameters rightState{};
|
|
const char* inner = nullptr;
|
|
EXPECT_TRUE(MGPipeVerify(leftState, rightState, &inner));
|
|
rightState.BlendStates[3].SrcFactorRGB = BlendFactor::DstColor;
|
|
EXPECT_FALSE(MGPipeVerify(leftState, rightState, &inner));
|
|
EXPECT_STREQ(inner, "BlendStates");
|
|
// P2's three new capability bools are members like any other, so the comparator names
|
|
// them rather than folding them into a neighbour's padding.
|
|
rightState = leftState;
|
|
rightState.FramebufferSrgbEnabled = true;
|
|
EXPECT_FALSE(MGPipeVerify(leftState, rightState, &inner));
|
|
EXPECT_STREQ(inner, "FramebufferSrgbEnabled");
|
|
// A NaN patch level equals itself too.
|
|
rightState = leftState;
|
|
leftState.PatchDefaultOuterLevel = FloatVec4{nan, 1.f, 1.f, 1.f};
|
|
rightState.PatchDefaultOuterLevel = FloatVec4{nan, 1.f, 1.f, 1.f};
|
|
EXPECT_TRUE(MGPipeVerify(leftState, rightState, &inner));
|
|
}
|
|
|
|
// The six value structs have field lists of their own (P1 brief D8): 63 + 6 payloads, and
|
|
// the struct that used to memcmp is compared member by member. P3a added the two vertex wire
|
|
// views as a seventh and eighth non-payload entry (63 + 8), for the same reason: they are the
|
|
// elements of create_vertex_elements' blob, and a memcmp over that blob would false-differ on
|
|
// MGPVertexAttribWire::Pad0. P4a adds SamplerParameters as a ninth (63 + 9 = 72), and the name
|
|
// of this case stays what it was, because a removed test name is a gate failure of its own.
|
|
//
|
|
// SamplerParameters IS THE SHARPEST OF THE NINE. It is 100 bytes with THREE BYTES OF TRAILING
|
|
// PADDING (96 bytes of members plus the one-byte borderColorForm), it rides
|
|
// MGPSamplerDesc::Parameters as a blob, and until P4a it had no field list and no verify-list
|
|
// row at all - so the comparator fell back to comparing the blob as BYTES and could
|
|
// false-differ on padding nobody writes. That is not a theoretical hazard for this struct:
|
|
// the client's CSO cache confirms a hash hit with a memcmp over the same bytes, so a codec or
|
|
// a cache that read the padding would mint a fresh CSO per call and the verify lane would
|
|
// abort at random.
|
|
TEST(PipeCatalogue, SixValueStructsHaveFieldLists) {
|
|
// 72 through P5; P5b appended five call payloads (MG_Remote/CONTRACT-P5B.md: MGPImageBind,
|
|
// MGPPatchParameter, MGPStreamOutputBind, MGPStorageBlockBinding, MGPCopyFromFramebuffer),
|
|
// each with its own field list, so the comparator sees every one of them: 77. P5c appended
|
|
// applier_reset's MGPApplierReset (CONTRACT-P5C.md §5.1) - object_death reuses
|
|
// MGPHandleOnly, which has had a list since P0 - and rv added set_context_values'
|
|
// MGPContextValues (§5.3): 79. P5e appended set_program_bindings' MGPProgramBindings AND
|
|
// the two TAIL ELEMENT types beside it, MGPProgramSamplerUnit and MGPProgramStorageOverride
|
|
// (MG_Remote/CONTRACT-P5E.md §1) - the tails are listed for the same reason MGPBufferRange
|
|
// and MGPVertexAttribWire are: the comparator has to see INTO an element whose members
|
|
// include an MGHostSpan and a pad word, or it would memcmp the padding: 82. P9 appended the
|
|
// pack-buffer readbacks' one payload, MGPReadbackToBuffer (CONTRACT-P9.md §1): 83. P14 S1
|
|
// appended bind_context's MGPBindContext (docs/Disaggregated/design/11-state-ownership.md):
|
|
// 84. A payload without a list here is one the comparator cannot see, so the row is not
|
|
// optional bookkeeping - gen_pipe.py refuses the build without it.
|
|
// + shared_image's MGPSharedImageOp: 85. P13 W3c deleted ResidualValueBlock and its list: 84.
|
|
EXPECT_EQ(kMGPipeVerifiedPayloadCount, 84u);
|
|
static_assert(MGPipeHasFieldVerifier<RenderStateParameters>::value);
|
|
static_assert(MGPipeHasFieldVerifier<PixelStoreParameters>::value);
|
|
static_assert(MGPipeHasFieldVerifier<PerBufferBlendState>::value);
|
|
static_assert(MGPipeHasFieldVerifier<StencilFaceState>::value);
|
|
static_assert(MGPipeHasFieldVerifier<DynamicBackendParameters>::value);
|
|
static_assert(MGPipeHasFieldVerifier<MGHostSpan>::value);
|
|
static_assert(MGPipeHasFieldVerifier<MGPVertexAttribWire>::value);
|
|
static_assert(MGPipeHasFieldVerifier<MGPVertexBindingPointWire>::value);
|
|
static_assert(MGPipeHasFieldVerifier<SamplerParameters>::value);
|
|
PixelStoreParameters p{};
|
|
PixelStoreParameters q{};
|
|
const char* field = nullptr;
|
|
EXPECT_TRUE(MGPipeVerify(p, q, &field));
|
|
q.SkipRows = 2;
|
|
EXPECT_FALSE(MGPipeVerify(p, q, &field));
|
|
EXPECT_STREQ(field, "SkipRows");
|
|
MGHostSpan s{};
|
|
MGHostSpan t{};
|
|
t.Pad0 = 0x5A; // padding is not a field
|
|
EXPECT_TRUE(MGPipeVerify(s, t, &field));
|
|
t.Offset = 8;
|
|
EXPECT_FALSE(MGPipeVerify(s, t, &field));
|
|
EXPECT_STREQ(field, "Offset");
|
|
|
|
// P4a's ninth, and its two halves. First: the comparator sees the members, INCLUDING
|
|
// borderColorForm - which is the field a backend picks glSamplerParameterIiv over fv by,
|
|
// and which no value comparison can infer because all three border representations are
|
|
// always numerically populated.
|
|
SamplerParameters left{};
|
|
SamplerParameters right{};
|
|
EXPECT_TRUE(MGPipeVerify(left, right, &field));
|
|
right.borderColorForm = BorderColorForm::Int;
|
|
EXPECT_FALSE(MGPipeVerify(left, right, &field));
|
|
EXPECT_STREQ(field, "borderColorForm");
|
|
right = left;
|
|
right.borderColorI = IntVec4{1, 0, 0, 0};
|
|
EXPECT_FALSE(MGPipeVerify(left, right, &field));
|
|
EXPECT_STREQ(field, "borderColorI");
|
|
right = left;
|
|
right.maxAnisotropy = 4.0f;
|
|
EXPECT_FALSE(MGPipeVerify(left, right, &field));
|
|
EXPECT_STREQ(field, "maxAnisotropy");
|
|
|
|
// Second, and this is the one a byte comparison gets wrong: the THREE TRAILING PADDING
|
|
// BYTES are not fields, so garbage in them cannot make two equal sampler states differ.
|
|
// Written through a byte pointer, because that is the only way to reach a byte the struct
|
|
// does not name.
|
|
static_assert(sizeof(SamplerParameters) == 100);
|
|
right = left;
|
|
auto* rightBytes = reinterpret_cast<unsigned char*>(&right);
|
|
for (SizeT i = sizeof(SamplerParameters) - 3; i < sizeof(SamplerParameters); ++i) {
|
|
rightBytes[i] = 0x5A;
|
|
}
|
|
EXPECT_TRUE(MGPipeVerify(left, right, &field))
|
|
<< "the comparator read a padding byte: field=" << (field != nullptr ? field : "(none)");
|
|
}
|
|
|
|
// G7 pins the member list the pipeline/dynamic split is derived from.
|
|
TEST(PipeCatalogue, PipelineSubsetMembersArePinned) {
|
|
// 44 as of P2, in DECLARATION order. It grew from the 24 members
|
|
// ComputePipelineStateHash used to hash because the chunk table's rule is "a byte is
|
|
// pipeline state iff a setter that calls BumpVersions() writes it", and that is a strict
|
|
// superset: sample coverage, front face, provoking vertex, the scissor-test mask, the
|
|
// back polygon mode, eleven capability bools the hash never read, and the three
|
|
// capabilities P2 gave storage to. 47 since GL_EXT_fragment_shading_rate added the rate
|
|
// and its two combiner ops.
|
|
EXPECT_EQ(kMGPipePipelineStateMemberCount, 47u);
|
|
EXPECT_STREQ(kMGPipePipelineStateMembers[0], "PatchVertices");
|
|
EXPECT_STREQ(kMGPipePipelineStateMembers[kMGPipePipelineStateMemberCount - 1],
|
|
"ScissorTestEnabledMask");
|
|
}
|
|
|
|
// The reverse channel is exactly SEVEN callbacks: plan B's section 7.1 wrote ten, and the tenth
|
|
// (OnXfbScatterReady) went with the design it belonged to when P5c/P5f moved the XFB scatter to
|
|
// the server's own staged shadow and its OnBufferWriteback return path. P3b/P4b espryt D1 slice 3
|
|
// deleted the declaration; this case is what makes the struct's shrink a measured fact rather
|
|
// than a claim, since the static_assert beside kMGPipeCallbackCount only proves the two agree.
|
|
// P9 took two more, neither of which ever had a producer: W2 deleted OnTexturePullRequest (a
|
|
// texture re-mint never needs the client's texels - the server reads the GPU level and its own
|
|
// staged store, notes/p9/W2-REMINT.md), and W3 deleted OnLog (the server's log lines are LogLine
|
|
// frames on the control plane, MG_Remote/Transport/LogForward.h, not reverse records).
|
|
//
|
|
// THE CASE NAME STAYS "…HasTenCallbacks" DELIBERATELY. G2/G14 say the ctest name set only ever
|
|
// grows, so renaming this would delete a name the gate is watching; the count it asserts is what
|
|
// has to be right, and the comment is where the number lives.
|
|
TEST(PipeCatalogue, ReverseChannelHasTenCallbacks) {
|
|
EXPECT_EQ(kMGPipeCallbackCount, 7u);
|
|
EXPECT_EQ(sizeof(MGPipeCallbacks), kMGPipeCallbackCount * sizeof(void (*)()));
|
|
}
|
|
|
|
// The one shape that changes with the transport. In a monolith it resolves to the pointer
|
|
// it was given; with no transport installed a segment-backed span resolves to nothing
|
|
// rather than to garbage.
|
|
TEST(PipeCatalogue, HostSpanResolvesTheMonolithPointer) {
|
|
static_assert(sizeof(MGHostSpan) == 32);
|
|
const Uint8 bytes[8] = {0, 1, 2, 3, 4, 5, 6, 7};
|
|
MGHostSpan span{};
|
|
span.Ptr = bytes;
|
|
span.Size = sizeof(bytes);
|
|
span.Offset = 2;
|
|
EXPECT_EQ(MGPipeHostBytes(span), bytes + 2);
|
|
|
|
MGHostSpan staged{};
|
|
staged.Seg = 4;
|
|
staged.Size = 16;
|
|
EXPECT_EQ(gMGPipeSegmentResolver, nullptr);
|
|
EXPECT_EQ(MGPipeHostBytes(staged), nullptr);
|
|
}
|
|
|
|
// D-B8: a bound buffer range carries no inline host span. The named-UBO bytes are an
|
|
// optional second var-tail announced by HostSpanCount, so the SSBO, atomic-counter and XFB
|
|
// ranges - the majority - pay nothing for a payload whose shape is not frozen yet.
|
|
TEST(PipeCatalogue, BufferRangeCarriesNoInlineHostSpan) {
|
|
static_assert(sizeof(MGPBufferRange) == 24);
|
|
// P5e (sb, ID-104): 32 -> 40. WritableMask was ONE Uint32 against an 84-point window, so
|
|
// it could describe only the first 32 points and a storage buffer bound at point 32 or
|
|
// above read as read-only with nothing able to see it. The ruling widens the FIELD rather
|
|
// than narrowing the window - narrowing would change what an application may bind - and the
|
|
// eight bytes are paid by a record that fires at most once per class per change, in a push
|
|
// build only. The ABI fingerprint already moved this phase for opcode 80.
|
|
static_assert(sizeof(MGPShaderBuffers) == 40);
|
|
static_assert(sizeof(MGPShaderBuffers::WritableMask) * 8u >= kMGPipeMaxBufferBindingPoints,
|
|
"the mask must cover the window the same record declares");
|
|
EXPECT_LT(sizeof(MGPBufferRange), sizeof(MGHostSpan));
|
|
|
|
// The call still declares the span it may carry, so the transport lays the tail out.
|
|
Uint32 flags = 0;
|
|
#define MGP_FLAGS_OF_SET_SHADER_BUFFERS(Name, Payload, Class, Flags, Wait) \
|
|
if (std::strcmp(#Name, "SetShaderBuffers") == 0) flags = static_cast<Uint32>(Flags);
|
|
MGP_CALL_LIST(MGP_FLAGS_OF_SET_SHADER_BUFFERS)
|
|
#undef MGP_FLAGS_OF_SET_SHADER_BUFFERS
|
|
EXPECT_EQ(flags & (kVarTail | kHostSpan), static_cast<Uint32>(kVarTail | kHostSpan));
|
|
|
|
// And the comparator sees the count that announces the tail.
|
|
MGPShaderBuffers a{};
|
|
MGPShaderBuffers b{};
|
|
const char* field = nullptr;
|
|
EXPECT_TRUE(MGPipeVerify(a, b, &field));
|
|
b.HostSpanCount = 4;
|
|
EXPECT_FALSE(MGPipeVerify(a, b, &field));
|
|
EXPECT_STREQ(field, "HostSpanCount");
|
|
|
|
// P5e (sb, ID-104): THE COMPARATOR SEES EVERY WORD OF THE WIDENED MASK, and the HIGH one
|
|
// is what the case is for. PipeFields.def names WritableMask once and the array overload
|
|
// of MGPipeFieldEqual walks it, so this would have passed before the widening too - on the
|
|
// 32 points that existed. Point 83 is the one the old field could not describe at all.
|
|
b = a;
|
|
ASSERT_TRUE(MGPipeVerify(a, b, &field));
|
|
MGPipeShaderBufferMaskSet(b.WritableMask, kMGPipeMaxBufferBindingPoints - 1);
|
|
EXPECT_TRUE(MGPipeShaderBufferMaskHas(b.WritableMask, kMGPipeMaxBufferBindingPoints - 1));
|
|
EXPECT_FALSE(MGPipeShaderBufferMaskHas(a.WritableMask, kMGPipeMaxBufferBindingPoints - 1));
|
|
EXPECT_FALSE(MGPipeVerify(a, b, &field));
|
|
EXPECT_STREQ(field, "WritableMask");
|
|
// And out of range is "not writable" / "write nothing" rather than a word past the end.
|
|
MGPipeShaderBufferMaskSet(b.WritableMask, kMGPipeMaxBufferBindingPoints);
|
|
EXPECT_FALSE(MGPipeShaderBufferMaskHas(b.WritableMask, kMGPipeMaxBufferBindingPoints));
|
|
}
|
|
|
|
// The buffer half of resource_subdata has no level and no box of its own: [offset, size)
|
|
// rides in UnionBox.X / UnionBox.W, and only through the two helpers, which also say where
|
|
// one record stops and the emitter has to split.
|
|
TEST(PipeCatalogue, SubDataBufferRangeRidesInTheUnionBox) {
|
|
MGPSubData record{};
|
|
record.Level = 3;
|
|
record.RegionCount = 2;
|
|
ASSERT_TRUE(MGPipeSetSubDataBufferRange(record, 4096, 65536));
|
|
EXPECT_EQ(record.UnionBox.X, 4096);
|
|
EXPECT_EQ(record.UnionBox.W, 65536u);
|
|
EXPECT_EQ(record.UnionBox.Y, 0);
|
|
EXPECT_EQ(record.UnionBox.Z, 0);
|
|
EXPECT_EQ(record.UnionBox.H, 1u);
|
|
EXPECT_EQ(record.UnionBox.D, 1u);
|
|
EXPECT_EQ(record.Level, 0);
|
|
EXPECT_EQ(record.RegionCount, 0u);
|
|
EXPECT_EQ(MGPipeSubDataBufferOffset(record), 4096u);
|
|
EXPECT_EQ(MGPipeSubDataBufferSize(record), 65536u);
|
|
|
|
// The largest range one record expresses...
|
|
ASSERT_TRUE(MGPipeSetSubDataBufferRange(record, 0x7FFFFFFFull, 0xFFFFFFFFull));
|
|
EXPECT_EQ(MGPipeSubDataBufferOffset(record), 0x7FFFFFFFull);
|
|
EXPECT_EQ(MGPipeSubDataBufferSize(record), 0xFFFFFFFFull);
|
|
// ...and beyond it the emitter splits: refused, record untouched.
|
|
EXPECT_FALSE(MGPipeSetSubDataBufferRange(record, 0x80000000ull, 1));
|
|
EXPECT_FALSE(MGPipeSetSubDataBufferRange(record, 0, 0x100000000ull));
|
|
EXPECT_EQ(MGPipeSubDataBufferOffset(record), 0x7FFFFFFFull);
|
|
EXPECT_EQ(MGPipeSubDataBufferSize(record), 0xFFFFFFFFull);
|
|
}
|
|
|
|
// P3a, D-H1: set_vertex_buffers carries the vertex-FETCH base instance explicitly, one per
|
|
// emitted set rather than one per entry, and the header grew 16 -> 24 bytes to hold it.
|
|
//
|
|
// The size is the cheap half. The half a compiler cannot catch is the PipeFields.def row:
|
|
// MGPVertexBuffers still HAS a ContentHash and still asserts its size whether or not the
|
|
// field list names BaseInstance, and a comparator blind to the field would let a
|
|
// baseInstance-only divergence through under MOBILEGL_PIPE_VERIFY - which is the one gate
|
|
// that would otherwise have seen the suppression bug the ContentHash rule exists to prevent.
|
|
// So the field list is pinned the only way it can be: by making the comparator name it.
|
|
TEST(PipeCatalogue, VertexBufferSetCarriesAnExplicitBaseInstance) {
|
|
static_assert(sizeof(MGPVertexBuffers) == 24);
|
|
static_assert(sizeof(MGPVertexBuffer) == 32); // the per-entry struct did NOT change
|
|
EXPECT_EQ(sizeof(MGPVertexBuffers), 24u);
|
|
|
|
MGPVertexBuffers a{};
|
|
MGPVertexBuffers b{};
|
|
const char* field = nullptr;
|
|
EXPECT_TRUE(MGPipeVerify(a, b, &field));
|
|
b.Pad0 = 0x5A; // padding is not a field
|
|
EXPECT_TRUE(MGPipeVerify(a, b, &field));
|
|
b.Pad0 = 0;
|
|
b.BaseInstance = 7;
|
|
EXPECT_FALSE(MGPipeVerify(a, b, &field));
|
|
EXPECT_STREQ(field, "BaseInstance");
|
|
|
|
// The set still carries no fetch shift per entry: an entry that disagreed with its own
|
|
// header is a shape the applier would have to police, and MGPVertexBuffer's Pad0 stays
|
|
// padding rather than becoming a second copy of the same number.
|
|
MGPVertexBuffer left{};
|
|
MGPVertexBuffer right{};
|
|
right.Pad0 = 0x5A;
|
|
EXPECT_TRUE(MGPipeVerify(left, right, &field));
|
|
}
|
|
|
|
// P3a, D-G2: the two vertex wire views. They are what create_vertex_elements' blob is made
|
|
// of, so their sizes are the blob's stride and the applier's bounds arithmetic; and IsLong is
|
|
// carried SEPARATELY from Type, because a GL_DOUBLE format converted to float and a long
|
|
// format that keeps all 64 bits are different requests that a backend has to tell apart.
|
|
TEST(PipeCatalogue, VertexWireViewsAreFlatAndCarryIsLongSeparately) {
|
|
static_assert(sizeof(MGPVertexAttribWire) == 24);
|
|
static_assert(sizeof(MGPVertexBindingPointWire) == 16);
|
|
EXPECT_EQ(sizeof(MGPVertexAttribWire), 24u);
|
|
EXPECT_EQ(sizeof(MGPVertexBindingPointWire), 16u);
|
|
|
|
MGPVertexAttribWire a{};
|
|
MGPVertexAttribWire b{};
|
|
const char* field = nullptr;
|
|
EXPECT_TRUE(MGPipeVerify(a, b, &field));
|
|
b.Pad0 = 0x5A;
|
|
EXPECT_TRUE(MGPipeVerify(a, b, &field));
|
|
b.Pad0 = 0;
|
|
// Type unchanged, IsLong moved: a comparator that folded the two would miss this.
|
|
b.IsLong = 1;
|
|
EXPECT_FALSE(MGPipeVerify(a, b, &field));
|
|
EXPECT_STREQ(field, "IsLong");
|
|
|
|
MGPVertexBindingPointWire p{};
|
|
MGPVertexBindingPointWire q{};
|
|
EXPECT_TRUE(MGPipeVerify(p, q, &field));
|
|
q.Divisor = 2;
|
|
EXPECT_FALSE(MGPipeVerify(p, q, &field));
|
|
EXPECT_STREQ(field, "Divisor");
|
|
}
|
|
|
|
// P3a, D-A5: the tree's FIRST kNeedsAck, and the reason it is not a bare flag.
|
|
//
|
|
// Flags are a PER-CALL static property and resource_respecify serves both glBufferData and
|
|
// glBufferStorage. A bare kNeedsAck on the call would acknowledge every glBufferData in a
|
|
// world upload - a round trip per chunk store the moment a transport is under it. So the flag
|
|
// declares that records of this call MAY need one and MGPipeResourceRespecifyNeedsAck decides
|
|
// per record: only an immutable store, which is a real synchronous allocation.
|
|
//
|
|
// This is the negative control for a future flag that over-acks: in monolith the ack is a
|
|
// no-op, so the mistake cannot be shipped from here, and the phase where it would bite
|
|
// inherits this pin rather than the guess.
|
|
TEST(PipeCatalogue, ResourceRespecifyAcksOnlyImmutableStorage) {
|
|
Uint32 flags = 0;
|
|
#define MGP_FLAGS_OF_RESOURCE_RESPECIFY(Name, Payload, Class, Flags, Wait) \
|
|
if (std::strcmp(#Name, "ResourceRespecify") == 0) flags = static_cast<Uint32>(Flags);
|
|
MGP_CALL_LIST(MGP_FLAGS_OF_RESOURCE_RESPECIFY)
|
|
#undef MGP_FLAGS_OF_RESOURCE_RESPECIFY
|
|
EXPECT_EQ(flags & static_cast<Uint32>(kNeedsAck), static_cast<Uint32>(kNeedsAck));
|
|
// And it is the ONLY call that carries it: a second one would be a second decision, and
|
|
// this predicate answers for exactly one call.
|
|
Uint32 ackingCalls = 0;
|
|
#define MGP_COUNT_ACKING_CALLS(Name, Payload, Class, Flags, Wait) \
|
|
if ((static_cast<Uint32>(Flags) & static_cast<Uint32>(kNeedsAck)) != 0) ++ackingCalls;
|
|
MGP_CALL_LIST(MGP_COUNT_ACKING_CALLS)
|
|
#undef MGP_COUNT_ACKING_CALLS
|
|
EXPECT_EQ(ackingCalls, 1u);
|
|
|
|
// P5 R-16: the same shape over kReplySlot, which had no count pin at all until the flag
|
|
// became load-bearing. It is what sizes the reply pool and what the decoder posts against,
|
|
// so the number is now a protocol quantity rather than a documentation one.
|
|
//
|
|
// FOURTEEN. Ten answers that were always declared - get_caps, map_persistent, the two fence
|
|
// reads, the three query reads, the two readbacks and read_pixels - plus the FOUR ACCEPTANCE
|
|
// ROWS, whose applier entry points return a Bool the client acts on destructively and which
|
|
// carried no flag because in monolith that answer is a direct call's return value. FIFTEEN since
|
|
// shared_image, whose every operation is answered.
|
|
Uint32 replySlotCalls = 0;
|
|
#define MGP_COUNT_REPLY_SLOT_CALLS(Name, Payload, Class, Flags, Wait) \
|
|
if ((static_cast<Uint32>(Flags) & static_cast<Uint32>(kReplySlot)) != 0) ++replySlotCalls;
|
|
MGP_CALL_LIST(MGP_COUNT_REPLY_SLOT_CALLS)
|
|
#undef MGP_COUNT_REPLY_SLOT_CALLS
|
|
EXPECT_EQ(replySlotCalls, 15u);
|
|
|
|
// And the four by name, because a count alone would let a row lose the flag while another
|
|
// gained one. These are exactly the MGPipeApply* entry points that return Bool
|
|
// (PipeApply.h:820, :868, :897, :1023); map_persistent's void* is the fifth answer and was
|
|
// already declared.
|
|
Uint32 acceptanceWithSlot = 0;
|
|
#define MGP_COUNT_ACCEPTANCE_ROWS(Name, Payload, Class, Flags, Wait) \
|
|
if ((std::strcmp(#Name, "ResourceCreate") == 0 || std::strcmp(#Name, "ResourceRespecify") == 0 || \
|
|
std::strcmp(#Name, "ResourceSubData") == 0 || std::strcmp(#Name, "SetTextureParams") == 0) && \
|
|
(static_cast<Uint32>(Flags) & static_cast<Uint32>(kReplySlot)) != 0) { \
|
|
++acceptanceWithSlot; \
|
|
}
|
|
MGP_CALL_LIST(MGP_COUNT_ACCEPTANCE_ROWS)
|
|
#undef MGP_COUNT_ACCEPTANCE_ROWS
|
|
EXPECT_EQ(acceptanceWithSlot, 4u);
|
|
|
|
// glBufferStorage: an immutable store, and the one entry point allowed a synchronous ack.
|
|
MGPResourceDesc immutable{};
|
|
immutable.Immutable = 1;
|
|
EXPECT_TRUE(MGPipeResourceRespecifyNeedsAck(immutable));
|
|
|
|
// glBufferData through the same call: never acknowledged, whatever else the descriptor
|
|
// says. The usage hint and a defined initial content are the two things a "well it looks
|
|
// synchronous" reading would key on, so both are set here on purpose.
|
|
MGPResourceDesc mutableStore{};
|
|
mutableStore.Immutable = 0;
|
|
mutableStore.Usage = 0x88E4; // GL_STATIC_DRAW, i.e. the most "final-looking" hint there is
|
|
mutableStore.HasDefinedContent = 1;
|
|
mutableStore.Width = 64u * 1024u;
|
|
EXPECT_FALSE(MGPipeResourceRespecifyNeedsAck(mutableStore));
|
|
|
|
// P4a: THE TWO IDIOMS THAT MADE THE PREDICATE HAVE TO NARROW. Textures travel on the same
|
|
// resource_respecify row as buffers, and glTexStorage* sets Immutable for a real reason -
|
|
// it is a descriptor fact the backend reads - so an Immutable-only predicate would have
|
|
// started acknowledging every immutable texture allocation the moment P4a's texture family
|
|
// landed. Texture allocation is already deferred to sync time in monolith (glTexImage* and
|
|
// glTexStorage* only mark the storage dirty, and even glRenderbufferStorage* allocates
|
|
// lazily inside SyncToBackend), so splitting changes no observable behaviour and this batch
|
|
// must not ack. glBufferStorage stays the only entry point allowed a synchronous one.
|
|
//
|
|
// This is the negative control for a future widening, in both directions: a predicate that
|
|
// stopped naming the buffer target would turn these two green-and-wrong.
|
|
MGPResourceDesc immutableTexture{};
|
|
immutableTexture.Immutable = 1; // glTexStorage2D
|
|
immutableTexture.Target =
|
|
static_cast<Uint8>(MGPipeResourceTargetForTextureTarget(TextureTarget::Texture2D));
|
|
immutableTexture.Width = 256;
|
|
immutableTexture.Height = 256;
|
|
immutableTexture.Levels = 9;
|
|
EXPECT_FALSE(MGPipeResourceRespecifyNeedsAck(immutableTexture));
|
|
|
|
MGPResourceDesc renderbuffer{};
|
|
renderbuffer.Immutable = 1; // glRenderbufferStorage: one shot, and still lazy in the backend
|
|
renderbuffer.Target = static_cast<Uint8>(MGPipeResourceTarget::Renderbuffer);
|
|
renderbuffer.Width = 1920;
|
|
renderbuffer.Height = 1080;
|
|
EXPECT_FALSE(MGPipeResourceRespecifyNeedsAck(renderbuffer));
|
|
|
|
// And the buffer half still answers true with the target spelled explicitly rather than
|
|
// relying on a zero-initialised record to mean "buffer".
|
|
MGPResourceDesc immutableBuffer{};
|
|
immutableBuffer.Immutable = 1;
|
|
immutableBuffer.Target = kMGPipeResourceTargetBuffer;
|
|
EXPECT_TRUE(MGPipeResourceRespecifyNeedsAck(immutableBuffer));
|
|
|
|
// And the opcode did not move: a flag-word edit is not a catalogue edit.
|
|
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::ResourceRespecify), 3);
|
|
|
|
// P4a, ID-18 M4. The metadata-update rule is a PROSE contract stated beside the predicate
|
|
// above - it compares an incoming descriptor against the applier's stored one, which this
|
|
// header cannot do - so what is pinnable here is the thing that would make the prose lie:
|
|
// a field added to MGPResourceDesc and classified into neither list. The size is the
|
|
// tripwire, and the two metadata fields are named so the classification cannot be lost to
|
|
// a rename either.
|
|
EXPECT_EQ(sizeof(MGPResourceDesc), 88u);
|
|
EXPECT_EQ(sizeof(MGPResourceDesc::BindMask), 2u);
|
|
EXPECT_EQ(sizeof(MGPResourceDesc::ImageBindableHint), 1u);
|
|
// HasDefinedContent sits next to ImageBindableHint and is deliberately on the OTHER side
|
|
// of the line: glBufferData(size, NULL) at an unchanged size is an orphaning
|
|
// reallocation, so a record that moves only it must still clear, and must never be read
|
|
// as a mask change.
|
|
EXPECT_NE(offsetof(MGPResourceDesc, HasDefinedContent),
|
|
offsetof(MGPResourceDesc, ImageBindableHint));
|
|
}
|
|
|
|
// G13b, D-M: "emulation 在 split 下显式 Fatal 直到 P8" costs P4a a NAMED, GREPPABLE call site
|
|
// per unmigrated emulation and nothing else - in monolith MGPipeUnmigratedEmulation is a no-op
|
|
// and the emulation still runs on exactly the code path it runs on today. What this pins is
|
|
// the LIST, because the whole value of the mechanism is that P5 and P8 edit one function
|
|
// instead of rediscovering five call sites, and a site that quietly disappears has to be a red
|
|
// gate rather than a surprise three phases later.
|
|
//
|
|
// The names are pinned here rather than counted in the backend, because the count alone cannot
|
|
// say WHICH one was lost. P8-SE: scripts/ci/unmigrated_emulation_sites.py holds this list against
|
|
// the `MGPipeUnmigratedEmulation("...")` call sites in the tree - equal sets, one site per name -
|
|
// because nothing did before and the list drifted twice unseen (P8-B, and P8-SE's dead name).
|
|
TEST(PipeCatalogue, EveryUnmigratedEmulationIsNamedOnce) {
|
|
// Every one of these is an emulation that reads or writes CLIENT memory a split server
|
|
// would not have: a CPU shadow mirror, a CPU mipmap fallback and a shadow-conversion
|
|
// readback.
|
|
//
|
|
// FOUR, NOT FIVE (P9 W2). The fifth was "texture-remint-pull", the re-dirty of
|
|
// already-uploaded levels that RequireImageBindableStorage performs, and the head of the one
|
|
// new stall class the design admitted. It went because the stall class did: under a transport
|
|
// the re-mint is the server's own (RequireImageBindableStorageByHandle reads the GPU level and
|
|
// replays the staged store), the frontend arm that carried the marker is monolith-only, and no
|
|
// pull protocol or ResourceSubDataComplete terminator exists (notes/p9/W2-REMINT.md).
|
|
//
|
|
// AND THE LIST HAD DRIFTED BY ONE UNTIL P8-B. The tree carried a sixth name this list never
|
|
// had: "generate-mipmap-cpu-filter", the split arm's RGB16F/RGB32F refusal in
|
|
// GenerateMipmapByRecord. P8-B2 retired it (the server filters its own level,
|
|
// GenerateThreeChannelFloatMipmapOnServer), and P8-B1 retired the split arm's two
|
|
// "generate-mipmap-storage" sites (the record's plan is the window). Every name left is on a
|
|
// MONOLITH arm - the split arms no longer reach an unmigrated emulation (notes/p8/B.md).
|
|
//
|
|
// THREE, NOT FOUR (P8-SE). "copy-image-shadow-mirror" sat after the mirror's own
|
|
// `Transport != Monolith` return, so no transport ever reached it and on monolith it was a
|
|
// no-op; the call and the name went together (notes/p8/SE.md).
|
|
//
|
|
// NONE (P13 W6). "get-tex-image-shadow" sat in GetTexImageViaShadowConversion, the monolith
|
|
// frontend arm's shadow readback, and the two mipmap names in that arm's storage grow and CPU
|
|
// filter; all three went with the frontend data arm. The list is EMPTY and the audit
|
|
// (scripts/ci/unmigrated_emulation_sites.py) now holds the tree at zero call sites: a new
|
|
// emulation that reaches into client memory has to be named here before it can land. The one
|
|
// entry below is the array's terminator, not a name - C++ has no empty braced array.
|
|
const char* const kNames[] = {nullptr};
|
|
EXPECT_EQ(std::size(kNames), 1u);
|
|
EXPECT_EQ(kNames[0], nullptr);
|
|
}
|
|
|
|
// THE ShaderCso COMPOSITE BAND IS A SECOND SPACE, AND THE ALLOCATOR REPORTS IT SEPARATELY.
|
|
//
|
|
// The band's base is 983040, so a composite handle passes every bound an ordinary one does and
|
|
// a slot-indexed table that forgets the band allocates ~983k entries for one program pipeline.
|
|
// That is why the allocator keeps two dense tables - and it is also why the two must be
|
|
// COUNTED apart: a high-water mark that folded them would be pinned at ~983k from the first
|
|
// composite mint onward, and every "the high-water mark did not move over N churn rounds"
|
|
// assertion about ORDINARY ShaderCso slots - the shape that catches a dense table that never
|
|
// shrinks, i.e. the ~1.3 KB-per-record leak the P3a final review found - would be vacuously
|
|
// true for the rest of the process. One merged number is one real assertion and one that
|
|
// cannot go red; two numbers are two real assertions, which is what the per-kind leak cases
|
|
// need.
|
|
//
|
|
// This case pins both halves: a leaked COMPOSITE moves the band's marks and not the ordinary
|
|
// one, and an ordinary leak still moves the ordinary mark with a composite outstanding.
|
|
TEST(PipeCatalogue, TheCompositeShaderBandIsCountedApartFromTheOrdinarySpace) {
|
|
MGPipeSlotAllocator slots;
|
|
|
|
const Uint32 ordinaryBefore = slots.HighWater(MGPipeKind::ShaderCso);
|
|
EXPECT_EQ(slots.CompositeHighWater(), kMGPipeShaderCsoCompositeSlotBase)
|
|
<< "the band's high-water mark starts at its base, so it is monotone from the first mint";
|
|
EXPECT_EQ(slots.CompositeLiveCount(), 0u);
|
|
EXPECT_EQ(slots.CompositeFreeCount(), 0u);
|
|
|
|
// A COMPOSITE MOVES THE BAND'S MARKS AND ONLY THOSE.
|
|
const MGPipeHandle composite = slots.AllocateComposite(9001);
|
|
ASSERT_FALSE(MGPipeHandleIsNull(composite));
|
|
ASSERT_TRUE(MGPipeIsCompositeShaderSlot(composite.Slot));
|
|
EXPECT_EQ(slots.HighWater(MGPipeKind::ShaderCso), ordinaryBefore)
|
|
<< "a composite mint moved the ORDINARY high-water mark, so the ordinary space's leak "
|
|
"assertion is vacuous from here on";
|
|
EXPECT_EQ(slots.CompositeHighWater(), kMGPipeShaderCsoCompositeSlotBase + 1u);
|
|
EXPECT_EQ(slots.CompositeLiveCount(), 1u);
|
|
// A live composite IS a live ShaderCso: the merged count is deliberate and stays.
|
|
EXPECT_EQ(slots.LiveCount(MGPipeKind::ShaderCso), 1u);
|
|
|
|
// AND THE ORDINARY MARK STILL MOVES WITH A COMPOSITE OUTSTANDING - the half that stopped
|
|
// existing when one number carried both spaces.
|
|
const MGPipeHandle ordinary = slots.Allocate(MGPipeKind::ShaderCso);
|
|
ASSERT_FALSE(MGPipeHandleIsNull(ordinary));
|
|
EXPECT_FALSE(MGPipeIsCompositeShaderSlot(ordinary.Slot));
|
|
EXPECT_GT(slots.HighWater(MGPipeKind::ShaderCso), ordinaryBefore);
|
|
EXPECT_EQ(slots.CompositeHighWater(), kMGPipeShaderCsoCompositeSlotBase + 1u)
|
|
<< "an ordinary mint moved the BAND's high-water mark";
|
|
|
|
// The slot goes back to the BAND's free list, and the high-water marks do not come back
|
|
// down - which is exactly what makes them a leak witness rather than a live count.
|
|
const Uint32 ordinaryHighWater = slots.HighWater(MGPipeKind::ShaderCso);
|
|
slots.Free(MGPipeKind::ShaderCso, composite);
|
|
EXPECT_EQ(slots.CompositeLiveCount(), 0u);
|
|
EXPECT_EQ(slots.CompositeFreeCount(), 1u);
|
|
EXPECT_EQ(slots.FreeCount(MGPipeKind::ShaderCso), 1u);
|
|
EXPECT_EQ(slots.CompositeHighWater(), kMGPipeShaderCsoCompositeSlotBase + 1u);
|
|
EXPECT_EQ(slots.HighWater(MGPipeKind::ShaderCso), ordinaryHighWater);
|
|
EXPECT_EQ(slots.LiveCount(MGPipeKind::ShaderCso), 1u);
|
|
}
|