mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
582 lines
29 KiB
C++
582 lines
29 KiB
C++
// MobileGL - MobileGL/MG_Test/Program/ProgramArtifactsCodecTest.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 reflection ARCHIVE's serializer (P4a): create_shader_state's payload is per-stage SPIR-V
|
|
// plus LinkArtifacts + SpirvArtifacts, whole structs, and the codec is what turns them into
|
|
// bytes. In monolith it is never called on the hot path - the two structs ride beside the
|
|
// record through the apply entry point's companion pointers - so THIS SUITE plus the verify
|
|
// lane's round trip are the only things that exercise it until a transport exists.
|
|
//
|
|
// WHAT IT HAS TO PIN, and each of the three is a different failure:
|
|
// * a fully populated archive survives a round trip FIELD BY FIELD, including both of
|
|
// XfbVarying's spellings (the GL name AND the block instance / member / element triple)
|
|
// and every one of TypeFacts' twenty members - a codec that dropped one would be invisible
|
|
// until a backend read a reflection answer that had quietly become zero;
|
|
// * a TRUNCATED stream is refused rather than guessed at;
|
|
// * a VERSION or wire-schema mismatch is refused before decoding; native STL object
|
|
// sizes do not describe a cross-platform serialized archive.
|
|
//
|
|
// AND ONE PROPERTY THAT IS NOT ABOUT BYTES AT ALL: LinkArtifacts has 58 members and its
|
|
// VisitFields table visits 57. The 58th is the live SharedPtr<glslang::TProgram>, which is
|
|
// null for every archived instance by construction and points into an arena no archive owns.
|
|
// The codec has no arm for it, and the count is asserted here because that is the only place
|
|
// it can be: VisitFields needs an instance, and these structs carry strings, vectors and maps,
|
|
// so no static_assert can walk them.
|
|
//
|
|
// Every case is a visible SKIP in a pull build rather than a vanishing test, so `ctest -N`
|
|
// stays name-for-name identical between the pull and the push trees.
|
|
|
|
#include <gtest/gtest.h>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <fstream>
|
|
#include <string>
|
|
|
|
#if !defined(_WIN32)
|
|
#include <sys/resource.h>
|
|
#include <unistd.h>
|
|
#endif
|
|
|
|
#include "Includes.h"
|
|
#include <MG_State/GLState/ProgramState/ProgramArtifactsCodec.h>
|
|
|
|
using namespace MobileGL;
|
|
using namespace MobileGL::MG_State::GLState;
|
|
|
|
namespace {
|
|
TypeFacts MakeTypeFacts() {
|
|
// Every member set to something that is NOT its default, so a field the codec skips
|
|
// reads back as the default and the comparison names it.
|
|
TypeFacts facts{};
|
|
facts.isArray = true;
|
|
facts.isSizedArray = true;
|
|
facts.isMatrix = true;
|
|
facts.isVector = true;
|
|
facts.isOpaque = true;
|
|
facts.isTexture = true;
|
|
facts.isImage = true;
|
|
facts.isDouble = true;
|
|
facts.isVoid = true;
|
|
facts.isBuffer = true;
|
|
facts.isPatch = true;
|
|
facts.hasIndex = true;
|
|
facts.hasFormat = true;
|
|
facts.vectorSize = 3;
|
|
facts.matrixCols = 4;
|
|
facts.matrixRows = 2;
|
|
facts.layoutIndex = 7;
|
|
facts.layoutFormat = 0x8814u;
|
|
facts.layoutMatrix = 1;
|
|
facts.basicType = 11;
|
|
return facts;
|
|
}
|
|
|
|
ResourceReflection MakeReflection(const char* name, Int location) {
|
|
ResourceReflection reflection{};
|
|
reflection.name = name;
|
|
reflection.glDefineType = GL_FLOAT_VEC4;
|
|
reflection.offset = 16;
|
|
reflection.size = 4;
|
|
reflection.index = 2;
|
|
reflection.counterIndex = 3;
|
|
reflection.arrayStride = 16;
|
|
reflection.topLevelArraySize = 5;
|
|
reflection.topLevelArrayStride = 32;
|
|
reflection.binding = 6;
|
|
reflection.location = location;
|
|
reflection.stages = 0x3u;
|
|
reflection.arraySize = 8;
|
|
reflection.type = MakeTypeFacts();
|
|
return reflection;
|
|
}
|
|
|
|
XfbVarying MakeXfbVarying() {
|
|
XfbVarying varying{};
|
|
// BOTH SPELLINGS. `name` is the GL one ("Block.member"), which the interface queries
|
|
// and the ESSL driver-side capture list need; the triple below is what a SPIR-V
|
|
// backend needs instead, because the decoration target is the block's instance
|
|
// variable and the member index inside it.
|
|
varying.name = "Captured.position";
|
|
varying.type = GL_FLOAT_VEC3;
|
|
varying.size = 2;
|
|
varying.bufferIndex = 1;
|
|
varying.offsetBytes = 12;
|
|
varying.byteSize = 24;
|
|
varying.packedOffsetBytes = 8;
|
|
varying.blockInstanceName = "capturedInstance";
|
|
varying.blockName = "Captured";
|
|
varying.blockMemberIndex = 1;
|
|
varying.blockMemberElement = 3;
|
|
return varying;
|
|
}
|
|
|
|
LinkArtifacts MakeLinkArtifacts() {
|
|
LinkArtifacts link{};
|
|
link.uniformReflection = {MakeReflection("uColour", 0), MakeReflection("uMatrix", 1)};
|
|
link.blockReflection = {MakeReflection("Block", -1)};
|
|
link.pipeInputReflection = {MakeReflection("inPosition", 0)};
|
|
link.pipeOutputReflection = {MakeReflection("outColour", 0)};
|
|
link.lastStageIsFragment = true;
|
|
link.computeLocalSize = {8u, 4u, 2u};
|
|
link.uniformIndexByName = {{"uColour", 0}, {"uMatrix", 1}};
|
|
link.attribs = {"inPosition", "inNormal"};
|
|
link.attribTypes = {GL_FLOAT_VEC3, GL_FLOAT_VEC3};
|
|
link.linkedFragDataLocation = {{"outColour", 0u}};
|
|
link.linkedFragDataIndex = {{"outColour", 1u}};
|
|
link.glUniformIndexToTProgram = {0, 1};
|
|
link.tProgramUniformIndexToGl = {0, 1};
|
|
link.glBlockIndexToTProgram = {0};
|
|
link.tProgramBlockIndexToGl = {-1};
|
|
link.glUniformBlockIndexToBlock = {0};
|
|
link.blockIndexToGlUniformBlock = {0};
|
|
link.linkedExplicitUniformLocations = {{"uColour", 3}};
|
|
link.uniformLocations = {{"uColour", 0u}, {"uMatrix", 4u}};
|
|
link.writtenUniformLocationBits = {0x5ull};
|
|
link.writtenUniformIndexBits = {0x3ull};
|
|
link.writtenUniformIndices = {0u, 1u};
|
|
link.uniformIndexInTProgram = {0, 1};
|
|
link.uniformSamplerOrImageUnitIndex = {-1, 2};
|
|
link.explicitOpaqueUniformBindings = {{"uSampler", 5u}};
|
|
link.uniformBlockIndexByName = {{"Block", 0u}};
|
|
link.uniformBlockBinding = {2};
|
|
link.shaderStorageBlockBinding = {{"Storage", 1}};
|
|
link.storageBlocksWithoutBinding = {"Storage"};
|
|
link.uniformBlocksWithoutBinding = {"Block"};
|
|
link.activeUniformCount = 2u;
|
|
link.usesReservedNumSamples = true;
|
|
link.maxUniformLocation = 4u;
|
|
link.uniformNameMaxLength = 9;
|
|
link.attribInNameMaxLength = 11;
|
|
link.uniformBlockNameMaxLength = 6;
|
|
link.infoLog = "linked with warnings";
|
|
link.linkStatus = true;
|
|
link.xfbVaryings = {MakeXfbVarying()};
|
|
link.xfbInterfaceNames = {"gl_NextBuffer", "Captured.position"};
|
|
link.xfbStrides = {32u, 0u};
|
|
link.gsStripTriangles = {3u, 5u};
|
|
link.gsStripCaptureFixup = true;
|
|
link.gsInputPrimitive = GL_TRIANGLES;
|
|
link.tcsOutputVertices = 3;
|
|
link.gsOutputPrimitive = GL_TRIANGLE_STRIP;
|
|
link.gsMaxVertices = 12;
|
|
link.gsInvocations = 2;
|
|
link.tessGenMode = GL_QUADS;
|
|
link.tessGenSpacing = GL_FRACTIONAL_ODD;
|
|
link.tessGenVertexOrder = GL_CW;
|
|
link.tessGenPointMode = true;
|
|
link.xfbBufferMode = GL_SEPARATE_ATTRIBS;
|
|
link.xfbVaryingNameMaxLength = 18;
|
|
link.xfbNeedsScatteredCapture = true;
|
|
link.xfbPackedStride = 24u;
|
|
|
|
// The one glslang-typed member that DOES travel: a plain aggregate of a string,
|
|
// scalars and two vectors. The codec has a hand-written arm for it because
|
|
// ProgramArtifacts.h gives it no VisitFields table.
|
|
glslang::TIntermediate::TUniformInitializer initializer;
|
|
initializer.name = "uInitialised";
|
|
initializer.basicType = glslang::EbtInt;
|
|
initializer.vectorSize = 2;
|
|
initializer.matrixCols = 0;
|
|
initializer.matrixRows = 0;
|
|
initializer.arraySize = 3;
|
|
initializer.intValues = {1, 2, 3, 4, 5, 6};
|
|
initializer.floatValues = {};
|
|
link.uniformInitialValues.push_back(initializer);
|
|
return link;
|
|
}
|
|
|
|
// How many archive bytes follow a u64 `count` that is itself followed by `next` - the LAST such
|
|
// spot, so a fixture can find the count of a field near the archive's end. This is
|
|
// Remaining() at the moment TakeCount reads that count. SIZE_MAX when there is no such spot.
|
|
SizeT BytesAfterTheCount(const Vector<Uint8>& bytes, Uint64 count, const Vector<Uint8>& next) {
|
|
Vector<Uint8> needle(sizeof(count));
|
|
std::memcpy(needle.data(), &count, sizeof(count));
|
|
needle.insert(needle.end(), next.begin(), next.end());
|
|
if (bytes.size() < needle.size()) return static_cast<SizeT>(-1);
|
|
for (SizeT at = bytes.size() - needle.size() + 1; at-- > 0;) {
|
|
if (std::memcmp(bytes.data() + at, needle.data(), needle.size()) == 0) {
|
|
return bytes.size() - at - sizeof(count);
|
|
}
|
|
}
|
|
return static_cast<SizeT>(-1);
|
|
}
|
|
|
|
// The encoded width of one DEFAULT element of LinkArtifacts::uniformReflection, measured
|
|
// through the encoder itself: an archive with one such element minus an archive with none.
|
|
// A default ResourceReflection has an empty name, so this is the smallest encoding one can
|
|
// have - the charge PH-5's TakeCount makes per element.
|
|
SizeT EncodedBytesOfOneDefaultResourceReflection() {
|
|
Vector<Uint8> none;
|
|
EncodeProgramArtifacts(LinkArtifacts{}, SpirvArtifacts{}, none);
|
|
LinkArtifacts one{};
|
|
one.uniformReflection.resize(1);
|
|
Vector<Uint8> withOne;
|
|
EncodeProgramArtifacts(one, SpirvArtifacts{}, withOne);
|
|
return withOne.size() - none.size();
|
|
}
|
|
|
|
SpirvArtifacts MakeSpirvArtifacts() {
|
|
SpirvArtifacts spirv{};
|
|
spirv.generatedSpirv = {{0x07230203u, 0x00010300u, 0u}, {0x07230203u, 0x00010300u, 1u}};
|
|
spirv.enableSpirvValidation = true;
|
|
spirv.uniformOffsets = {0u, 16u, kInvalidUniformOffset};
|
|
spirv.globalUboScratch = {1, 2, 3, 4, 5, 6, 7, 8};
|
|
spirv.reservedNumSamplesOffset = 32u;
|
|
spirv.spirvStatus = true;
|
|
spirv.nativeFloat64 = true;
|
|
spirv.pointSizeDemoted = true;
|
|
return spirv;
|
|
}
|
|
} // namespace
|
|
|
|
// The round trip, field by field. A re-encode equality alone would prove the codec is
|
|
// self-consistent and nothing else - a field it skips in BOTH directions round-trips
|
|
// perfectly - so the members are read back explicitly first, and the byte comparison is the
|
|
// catch-all underneath them.
|
|
TEST(ProgramArtifactsCodec, RoundTripsAFullyPopulatedArchive) {
|
|
const LinkArtifacts link = MakeLinkArtifacts();
|
|
const SpirvArtifacts spirv = MakeSpirvArtifacts();
|
|
|
|
Vector<Uint8> bytes;
|
|
EncodeProgramArtifacts(link, spirv, bytes);
|
|
ASSERT_FALSE(bytes.empty());
|
|
|
|
LinkArtifacts decodedLink;
|
|
SpirvArtifacts decodedSpirv;
|
|
ASSERT_TRUE(DecodeProgramArtifacts(bytes.data(), bytes.size(), decodedLink, decodedSpirv));
|
|
|
|
// The four reflection vectors, with their TypeFacts.
|
|
ASSERT_EQ(decodedLink.uniformReflection.size(), 2u);
|
|
EXPECT_EQ(decodedLink.uniformReflection[0].name, "uColour");
|
|
EXPECT_EQ(decodedLink.uniformReflection[1].location, 1);
|
|
EXPECT_EQ(decodedLink.uniformReflection[0].arrayStride, 16);
|
|
EXPECT_EQ(decodedLink.uniformReflection[0].stages, 0x3u);
|
|
EXPECT_TRUE(decodedLink.uniformReflection[0].type.isSizedArray);
|
|
EXPECT_EQ(decodedLink.uniformReflection[0].type.matrixCols, 4);
|
|
EXPECT_EQ(decodedLink.uniformReflection[0].type.layoutFormat, 0x8814u);
|
|
EXPECT_EQ(decodedLink.uniformReflection[0].type.basicType, 11);
|
|
ASSERT_EQ(decodedLink.blockReflection.size(), 1u);
|
|
ASSERT_EQ(decodedLink.pipeInputReflection.size(), 1u);
|
|
ASSERT_EQ(decodedLink.pipeOutputReflection.size(), 1u);
|
|
|
|
// BOTH XfbVarying SPELLINGS.
|
|
ASSERT_EQ(decodedLink.xfbVaryings.size(), 1u);
|
|
EXPECT_EQ(decodedLink.xfbVaryings[0].name, "Captured.position");
|
|
EXPECT_EQ(decodedLink.xfbVaryings[0].blockInstanceName, "capturedInstance");
|
|
EXPECT_EQ(decodedLink.xfbVaryings[0].blockName, "Captured");
|
|
EXPECT_EQ(decodedLink.xfbVaryings[0].blockMemberIndex, 1);
|
|
EXPECT_EQ(decodedLink.xfbVaryings[0].blockMemberElement, 3);
|
|
EXPECT_EQ(decodedLink.xfbVaryings[0].packedOffsetBytes, 8u);
|
|
|
|
// The maps, the set and the fixed array - the four container shapes the archive is made
|
|
// of, each with a reader that has to agree with its writer about the length prefix.
|
|
EXPECT_EQ(decodedLink.uniformIndexByName.size(), 2u);
|
|
EXPECT_EQ(decodedLink.uniformIndexByName.at("uMatrix"), 1);
|
|
EXPECT_EQ(decodedLink.shaderStorageBlockBinding.at("Storage"), 1);
|
|
EXPECT_EQ(decodedLink.storageBlocksWithoutBinding.count("Storage"), 1u);
|
|
EXPECT_EQ(decodedLink.uniformBlocksWithoutBinding.count("Block"), 1u);
|
|
EXPECT_EQ(decodedLink.computeLocalSize[0], 8u);
|
|
EXPECT_EQ(decodedLink.computeLocalSize[2], 2u);
|
|
|
|
// The glslang-typed aggregate, through the codec's one hand-written arm.
|
|
ASSERT_EQ(decodedLink.uniformInitialValues.size(), 1u);
|
|
EXPECT_EQ(decodedLink.uniformInitialValues[0].name, "uInitialised");
|
|
EXPECT_EQ(decodedLink.uniformInitialValues[0].basicType, glslang::EbtInt);
|
|
EXPECT_EQ(decodedLink.uniformInitialValues[0].arraySize, 3);
|
|
ASSERT_EQ(decodedLink.uniformInitialValues[0].intValues.size(), 6u);
|
|
EXPECT_EQ(decodedLink.uniformInitialValues[0].intValues[5], 6);
|
|
EXPECT_TRUE(decodedLink.uniformInitialValues[0].floatValues.empty());
|
|
|
|
// The scalars at the tail, which is where a length-prefix that drifted by one would first
|
|
// read as garbage rather than as a short read.
|
|
EXPECT_EQ(decodedLink.infoLog, "linked with warnings");
|
|
EXPECT_TRUE(decodedLink.linkStatus);
|
|
EXPECT_EQ(decodedLink.tessGenSpacing, static_cast<GLenum>(GL_FRACTIONAL_ODD));
|
|
EXPECT_TRUE(decodedLink.tessGenPointMode);
|
|
EXPECT_EQ(decodedLink.xfbPackedStride, 24u);
|
|
|
|
// SpirvArtifacts, including the nested vector of SPIR-V words and the sentinel offset.
|
|
ASSERT_EQ(decodedSpirv.generatedSpirv.size(), 2u);
|
|
ASSERT_EQ(decodedSpirv.generatedSpirv[0].size(), 3u);
|
|
EXPECT_EQ(decodedSpirv.generatedSpirv[1][2], 1u);
|
|
ASSERT_EQ(decodedSpirv.uniformOffsets.size(), 3u);
|
|
EXPECT_EQ(decodedSpirv.uniformOffsets[2], kInvalidUniformOffset);
|
|
EXPECT_EQ(decodedSpirv.globalUboScratch.size(), 8u);
|
|
EXPECT_EQ(decodedSpirv.reservedNumSamplesOffset, 32u);
|
|
EXPECT_TRUE(decodedSpirv.nativeFloat64);
|
|
EXPECT_TRUE(decodedSpirv.pointSizeDemoted);
|
|
|
|
// THE LIVE TProgram IS NEVER CARRIED and never reconstructed.
|
|
EXPECT_EQ(decodedLink.program, nullptr);
|
|
|
|
// The catch-all: re-encoding what came back has to produce the same bytes, which covers
|
|
// every member the explicit reads above do not name.
|
|
Vector<Uint8> reencoded;
|
|
EncodeProgramArtifacts(decodedLink, decodedSpirv, reencoded);
|
|
EXPECT_EQ(reencoded, bytes);
|
|
}
|
|
|
|
// A COMPACT archive - one whose wire form is SMALLER than its in-memory form - is still an archive
|
|
// this encoder wrote, and it has to come back (codex closeout finding 3). Ten one-character
|
|
// xfbInterfaceNames are 90 bytes on the wire and ten 32-byte std::strings in memory; with every
|
|
// vector behind them empty, the bytes that remain at their count are fewer than 10 x sizeof(String),
|
|
// and PH-5's first bound - which charged sizeof(value_type) per element - refused the archive. The
|
|
// same holds one level down for SpirvArtifacts::generatedSpirv: ten EMPTY modules are ten u64
|
|
// counts on the wire and ten 24-byte vectors in memory (the next case). Red with TakeCount's vector
|
|
// charge put back to sizeof(Element): the decode answers false. Each case's ASSERT_LT is the
|
|
// fixture's own proof that it can go red that way.
|
|
TEST(ProgramArtifactsCodec, ACompactArchiveOfTenShortXfbNamesRoundTrips) {
|
|
{
|
|
LinkArtifacts link{};
|
|
link.xfbInterfaceNames = {"a", "b", "c", "d", "e", "f", "g", "h", "i", "j"};
|
|
Vector<Uint8> bytes;
|
|
EncodeProgramArtifacts(link, SpirvArtifacts{}, bytes);
|
|
const Uint64 one = 1;
|
|
Vector<Uint8> firstName(sizeof(one));
|
|
std::memcpy(firstName.data(), &one, sizeof(one));
|
|
firstName.push_back(static_cast<Uint8>('a'));
|
|
const SizeT remaining = BytesAfterTheCount(bytes, link.xfbInterfaceNames.size(), firstName);
|
|
ASSERT_LT(remaining, link.xfbInterfaceNames.size() * sizeof(String))
|
|
<< "fixture: a sizeof(value_type) charge would admit this count, so the case could not go red";
|
|
|
|
LinkArtifacts decodedLink;
|
|
SpirvArtifacts decodedSpirv;
|
|
ASSERT_TRUE(DecodeProgramArtifacts(bytes.data(), bytes.size(), decodedLink, decodedSpirv))
|
|
<< "a LinkArtifacts with ten short XFB interface names, which this encoder wrote, was refused";
|
|
EXPECT_EQ(decodedLink.xfbInterfaceNames, link.xfbInterfaceNames);
|
|
EXPECT_TRUE(decodedLink.xfbStrides.empty());
|
|
EXPECT_TRUE(decodedLink.gsStripTriangles.empty());
|
|
Vector<Uint8> reencoded;
|
|
EncodeProgramArtifacts(decodedLink, decodedSpirv, reencoded);
|
|
EXPECT_EQ(reencoded, bytes);
|
|
}
|
|
}
|
|
|
|
// The same compact shape one level down: a Vector<Vector<unsigned>> element (one SPIR-V module) is
|
|
// its u64 count on the wire when empty and a 24-byte object in memory.
|
|
TEST(ProgramArtifactsCodec, ACompactArchiveOfTenEmptySpirvModulesRoundTrips) {
|
|
{
|
|
SpirvArtifacts spirv{};
|
|
spirv.generatedSpirv.resize(10);
|
|
Vector<Uint8> bytes;
|
|
EncodeProgramArtifacts(LinkArtifacts{}, spirv, bytes);
|
|
const SizeT remaining = BytesAfterTheCount(bytes, spirv.generatedSpirv.size(),
|
|
Vector<Uint8>(10 * sizeof(Uint64), 0));
|
|
ASSERT_LT(remaining, spirv.generatedSpirv.size() * sizeof(Vector<unsigned>))
|
|
<< "fixture: a sizeof(value_type) charge would admit this count, so the case could not go red";
|
|
|
|
LinkArtifacts decodedLink;
|
|
SpirvArtifacts decodedSpirv;
|
|
ASSERT_TRUE(DecodeProgramArtifacts(bytes.data(), bytes.size(), decodedLink, decodedSpirv))
|
|
<< "a SpirvArtifacts with ten empty modules, which this encoder wrote, was refused";
|
|
ASSERT_EQ(decodedSpirv.generatedSpirv.size(), 10u);
|
|
for (const auto& module : decodedSpirv.generatedSpirv) EXPECT_TRUE(module.empty());
|
|
Vector<Uint8> reencoded;
|
|
EncodeProgramArtifacts(decodedLink, decodedSpirv, reencoded);
|
|
EXPECT_EQ(reencoded, bytes);
|
|
}
|
|
}
|
|
|
|
// Negative control 1. Every prefix length is checked against the bytes that REMAIN, so a
|
|
// stream cut anywhere has to come back false with both outputs defaulted - never a partially
|
|
// filled archive, and never a resize driven by a count the stream cannot back.
|
|
TEST(ProgramArtifactsCodec, ATruncatedStreamIsRefusedNotGuessed) {
|
|
Vector<Uint8> bytes;
|
|
EncodeProgramArtifacts(MakeLinkArtifacts(), MakeSpirvArtifacts(), bytes);
|
|
ASSERT_GT(bytes.size(), 64u);
|
|
|
|
// Cut at a spread of points rather than one: the header, a length prefix, the middle of a
|
|
// string and the last byte all fail through different branches.
|
|
for (const SizeT cut : {SizeT{0}, SizeT{4}, SizeT{9}, bytes.size() / 3, bytes.size() / 2,
|
|
bytes.size() - 1}) {
|
|
LinkArtifacts link;
|
|
SpirvArtifacts spirv;
|
|
link.infoLog = "must be cleared";
|
|
EXPECT_FALSE(DecodeProgramArtifacts(bytes.data(), cut, link, spirv))
|
|
<< "a stream truncated at " << cut << " was accepted";
|
|
EXPECT_TRUE(link.infoLog.empty()) << "a refused decode left the output half-filled";
|
|
EXPECT_TRUE(link.uniformReflection.empty());
|
|
EXPECT_TRUE(spirv.generatedSpirv.empty());
|
|
}
|
|
|
|
// And TRAILING bytes are a mismatch too: the format accounts for every byte it writes, so
|
|
// anything left over means the reader and the writer disagree about the shape.
|
|
Vector<Uint8> withTail = bytes;
|
|
withTail.push_back(0);
|
|
LinkArtifacts link;
|
|
SpirvArtifacts spirv;
|
|
EXPECT_FALSE(DecodeProgramArtifacts(withTail.data(), withTail.size(), link, spirv));
|
|
}
|
|
|
|
// A count is not permission to reserve a container larger than the bytes left in the
|
|
// untrusted archive can back. This mutates the first LinkArtifacts vector count to the boundary
|
|
// value one element past PH-5's bound - the smallest count the remaining bytes cannot hold at
|
|
// the element's minimum encoded size - which the former raw <= Remaining check accepted. (Not
|
|
// discriminating on its own: under the byte bound the resize is small and the element reads run
|
|
// out of bytes, so the decode is false either way. ACountOnlyTheByteBoundAdmitsIsRefusedBeforeItsResize
|
|
// is the case that goes red.)
|
|
TEST(ProgramArtifactsCodec, AVectorCountCannotReservePastTheRemainingArchiveBytes) {
|
|
Vector<Uint8> bytes;
|
|
EncodeProgramArtifacts(LinkArtifacts{}, SpirvArtifacts{}, bytes);
|
|
constexpr SizeT countOffset = sizeof(Uint32) + sizeof(Uint64);
|
|
ASSERT_GT(bytes.size(), countOffset + sizeof(Uint64));
|
|
const SizeT available = bytes.size() - countOffset - sizeof(Uint64);
|
|
const SizeT perElement = EncodedBytesOfOneDefaultResourceReflection();
|
|
// What finding 3 is about, stated: an element's wire floor is below its object size, and
|
|
// above the one byte the former bound charged.
|
|
ASSERT_GT(perElement, 1u);
|
|
ASSERT_LT(perElement, sizeof(UniformReflection));
|
|
const Uint64 forgedCount = static_cast<Uint64>(available / perElement) + 1;
|
|
ASSERT_LT(forgedCount, static_cast<Uint64>(available))
|
|
<< "fixture must pass the old count <= remaining check";
|
|
std::memcpy(bytes.data() + countOffset, &forgedCount, sizeof(forgedCount));
|
|
|
|
LinkArtifacts link;
|
|
SpirvArtifacts spirv;
|
|
EXPECT_FALSE(DecodeProgramArtifacts(bytes.data(), bytes.size(), link, spirv));
|
|
EXPECT_TRUE(link.uniformReflection.empty());
|
|
EXPECT_TRUE(spirv.generatedSpirv.empty());
|
|
}
|
|
|
|
// PH-5's DISCRIMINATING control (P7 F2 latch). The case above cannot fail on PH-5's account: its
|
|
// count is one element past PH-5's bound, and with TakeCount put back to the old one-byte-per-
|
|
// element charge the same count is admitted, resize() is tiny, and the element reads run out of
|
|
// bytes - DecodeProgramArtifacts answers false either way (red-once: it stays green). What PH-5
|
|
// actually changed is the ALLOCATION a count may cause before the reads, so this case forges the
|
|
// count BETWEEN the two bounds - `count == Remaining()`, in an archive padded to 7 MiB - and
|
|
// decodes it in a forked child whose address space has 64 MiB of headroom. PH-5 charges each
|
|
// element its minimum encoded size (a ResourceReflection's is ~100 bytes; the codex closeout
|
|
// finding 3 fix, not sizeof), refuses before the resize, and the child exits 0; the old bound
|
|
// resizes to Remaining() elements of sizeof(UniformReflection) each, far past the headroom, and
|
|
// the child dies of std::bad_alloc. The same shape crosses the wire in PeerLatchTest's
|
|
// D11ArchiveVectorCount row.
|
|
#if !defined(_WIN32)
|
|
namespace {
|
|
[[noreturn]] void DecodeACountOnlyTheByteBoundAdmitsUnderACapAndExit() {
|
|
Vector<Uint8> bytes;
|
|
EncodeProgramArtifacts(LinkArtifacts{}, SpirvArtifacts{}, bytes);
|
|
constexpr SizeT countOffset = sizeof(Uint32) + sizeof(Uint64);
|
|
bytes.resize(SizeT{7} << 20, 0);
|
|
const Uint64 remaining = bytes.size() - countOffset - sizeof(Uint64);
|
|
std::memcpy(bytes.data() + countOffset, &remaining, sizeof(remaining));
|
|
constexpr Uint64 kHeadroom = Uint64{64} << 20;
|
|
if (remaining * sizeof(UniformReflection) < 4 * kHeadroom) ::_exit(64); // the case would prove nothing
|
|
Uint64 vmKb = 0;
|
|
{
|
|
std::ifstream status("/proc/self/status");
|
|
std::string line;
|
|
while (std::getline(status, line)) {
|
|
if (line.rfind("VmSize:", 0) == 0) vmKb = std::strtoull(line.c_str() + 7, nullptr, 10);
|
|
}
|
|
}
|
|
if (vmKb == 0) ::_exit(65);
|
|
rlimit cap{};
|
|
if (::getrlimit(RLIMIT_AS, &cap) != 0) ::_exit(66);
|
|
cap.rlim_cur = static_cast<rlim_t>(vmKb * 1024 + kHeadroom);
|
|
if (::setrlimit(RLIMIT_AS, &cap) != 0) ::_exit(67);
|
|
LinkArtifacts link;
|
|
SpirvArtifacts spirv;
|
|
const Bool decoded = DecodeProgramArtifacts(bytes.data(), bytes.size(), link, spirv);
|
|
::_exit(!decoded && link.uniformReflection.empty() ? 0 : 3);
|
|
}
|
|
} // namespace
|
|
#endif
|
|
|
|
TEST(ProgramArtifactsCodec, ACountOnlyTheByteBoundAdmitsIsRefusedBeforeItsResize) {
|
|
#if !defined(_WIN32)
|
|
EXPECT_EXIT(DecodeACountOnlyTheByteBoundAdmitsUnderACapAndExit(), ::testing::ExitedWithCode(0), ".*")
|
|
<< "std::bad_alloc escaping the child is the resize PH-5 exists to refuse: the vector count was "
|
|
"charged one byte per element instead of its minimum encoded size; exit 3 is a decode that "
|
|
"did not refuse; 64+ is the fixture";
|
|
#else
|
|
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off (the archive codec is push-only), or no RLIMIT_AS / fork here";
|
|
#endif
|
|
}
|
|
|
|
// Negative control 2: both the version and schema word must refuse mismatches.
|
|
// Monolith v1 retains the old native-size echo in that second word.
|
|
TEST(ProgramArtifactsCodec, AVersionMismatchIsRefused) {
|
|
Vector<Uint8> bytes;
|
|
EncodeProgramArtifacts(MakeLinkArtifacts(), MakeSpirvArtifacts(), bytes);
|
|
ASSERT_GT(bytes.size(), 12u);
|
|
|
|
LinkArtifacts link;
|
|
SpirvArtifacts spirv;
|
|
ASSERT_TRUE(DecodeProgramArtifacts(bytes.data(), bytes.size(), link, spirv));
|
|
|
|
// The version word first: a reader that saw a format it does not know must not try to
|
|
// guess the layout.
|
|
Vector<Uint8> wrongVersion = bytes;
|
|
++wrongVersion[0];
|
|
EXPECT_FALSE(DecodeProgramArtifacts(wrongVersion.data(), wrongVersion.size(), link, spirv));
|
|
|
|
// Then the wire schema (v2) or local native-size echo (v1). Neither may be ignored.
|
|
Vector<Uint8> wrongSize = bytes;
|
|
++wrongSize[4];
|
|
EXPECT_FALSE(DecodeProgramArtifacts(wrongSize.data(), wrongSize.size(), link, spirv));
|
|
|
|
// A null pointer is refused rather than dereferenced.
|
|
EXPECT_FALSE(DecodeProgramArtifacts(nullptr, 0, link, spirv));
|
|
}
|
|
|
|
// The codec walks the VisitFields tables and nothing else, so what those tables visit IS the
|
|
// archive. LinkArtifacts has 58 members and its table visits 57: the 58th is the live glslang
|
|
// TProgram, which is null for every archived instance by construction and points into an arena
|
|
// no archive owns. A codec arm for it would be a use-after-free waiting for a cache hit.
|
|
TEST(ProgramArtifactsCodec, TheTablesVisitEveryMemberExceptTheLiveProgram) {
|
|
// P7 OQ-8: 59 members, 58 visited. The 59th is still the live TProgram; the 58th is
|
|
// storageBlocks, which follows the record arm's guard (ProgramArtifacts.h).
|
|
EXPECT_EQ(ProgramArtifactsVisitedFieldCount<LinkArtifacts>(), 58u);
|
|
EXPECT_EQ(ProgramArtifactsVisitedFieldCount<StorageBlockReflection>(), 3u);
|
|
EXPECT_EQ(ProgramArtifactsVisitedFieldCount<SpirvArtifacts>(), 8u);
|
|
EXPECT_EQ(ProgramArtifactsVisitedFieldCount<ResourceReflection>(), 14u);
|
|
EXPECT_EQ(ProgramArtifactsVisitedFieldCount<XfbVarying>(), 11u);
|
|
EXPECT_EQ(ProgramArtifactsVisitedFieldCount<TypeFacts>(), 20u);
|
|
}
|
|
|
|
TEST(ProgramArtifactsCodec, PortableHeaderUsesWireSchemaRatherThanNativeContainerSize) {
|
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
|
Vector<Uint8> bytes;
|
|
EncodeProgramArtifacts(MakeLinkArtifacts(), MakeSpirvArtifacts(), bytes);
|
|
ASSERT_GT(bytes.size(), 12u);
|
|
Uint32 version = 0;
|
|
Uint64 schema = 0;
|
|
std::memcpy(&version, bytes.data(), sizeof(version));
|
|
std::memcpy(&schema, bytes.data() + sizeof(version), sizeof(schema));
|
|
// 3 since P7 OQ-8 (CONTRACT-P7 §5.3): LinkArtifacts gained storageBlocks, so a v2 reader
|
|
// would run out of bytes mid-stream rather than notice. The schema word beside it moved
|
|
// too - it is derived from the VisitFields tables - and so did `wireFingerprint`, which is
|
|
// what makes a mixed-version pair refuse at the handshake instead of at the first program.
|
|
EXPECT_EQ(version, kProgramArtifactsCodecVersion);
|
|
EXPECT_EQ(version, 3u);
|
|
EXPECT_EQ(schema, ProgramArtifactsSchemaFingerprint());
|
|
EXPECT_NE(schema, 0u);
|
|
EXPECT_NE(schema, sizeof(LinkArtifacts));
|
|
RecordProperty("program_archive_wire_schema", std::to_string(schema));
|
|
|
|
// The old Linux/native echo (1056) and unpinned libc++ echo (0) both fail.
|
|
// This control distinguishes a portable schema from simply deleting the check.
|
|
for (const Uint64 legacyEcho : {Uint64{1056}, Uint64{0}}) {
|
|
auto legacy = bytes;
|
|
std::memcpy(legacy.data() + sizeof(version), &legacyEcho, sizeof(legacyEcho));
|
|
LinkArtifacts link;
|
|
SpirvArtifacts spirv;
|
|
EXPECT_FALSE(DecodeProgramArtifacts(legacy.data(), legacy.size(), link, spirv));
|
|
EXPECT_TRUE(link.uniformReflection.empty());
|
|
EXPECT_TRUE(spirv.generatedSpirv.empty());
|
|
}
|
|
auto v1 = bytes;
|
|
v1[0] = 1;
|
|
LinkArtifacts link;
|
|
SpirvArtifacts spirv;
|
|
EXPECT_FALSE(DecodeProgramArtifacts(v1.data(), v1.size(), link, spirv));
|
|
#else
|
|
GTEST_SKIP() << "portable archive headers apply to the disaggregated wire";
|
|
#endif
|
|
}
|