mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-10 05:08:31 +09:00
[Test] (Pipe): pin every P4a record's lifecycle, its bounds gate and what a make-current does and does not clear
This commit is contained in:
@@ -32,19 +32,29 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <system_error>
|
||||
|
||||
#if defined(_WIN32)
|
||||
#include <process.h>
|
||||
#define MGTEST_HAVE_FORK 0
|
||||
#else
|
||||
#include <csignal>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
#define MGTEST_HAVE_FORK 1
|
||||
#endif
|
||||
|
||||
#include "Includes.h"
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
#include <MG_Impl/Pipe/SlotAllocator.h>
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
// create_shader_state takes the two artefact structs by pointer beside the record, so a case
|
||||
// that mints a composite record needs their definitions.
|
||||
#include <MG_State/GLState/ProgramState/ProgramArtifacts.h>
|
||||
#endif
|
||||
|
||||
using namespace MobileGL;
|
||||
@@ -60,6 +70,92 @@ namespace {
|
||||
return static_cast<int>(getpid());
|
||||
#endif
|
||||
}
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
std::string ReadLog() {
|
||||
std::ifstream in(g_logPath, std::ios::binary);
|
||||
std::ostringstream ss;
|
||||
ss << in.rdbuf();
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
// A fresh applier per case, BOTH SCOPES, and it takes both because there are two: a reset
|
||||
// is a make-current and deliberately KEEPS the object records, so a fixture that wants a
|
||||
// genuinely empty applier has to say the other one as well. Every case is its own process
|
||||
// under ctest, so this is belt and braces - but running the binary by hand must give the
|
||||
// same answers as running it under ctest.
|
||||
struct ApplierGuard {
|
||||
ApplierGuard() {
|
||||
MGPipeApplierReset();
|
||||
MGPipeApplierReleaseObjectRecords();
|
||||
}
|
||||
~ApplierGuard() {
|
||||
MGPipeApplierReset();
|
||||
MGPipeApplierReleaseObjectRecords();
|
||||
}
|
||||
};
|
||||
|
||||
#if MGTEST_HAVE_FORK
|
||||
struct ChildResult {
|
||||
int Status = -1;
|
||||
std::string Log;
|
||||
};
|
||||
|
||||
template <class Body>
|
||||
ChildResult RunInChild(Body body) {
|
||||
ChildResult result;
|
||||
std::error_code ec;
|
||||
std::filesystem::remove(g_logPath, ec);
|
||||
std::fflush(nullptr);
|
||||
const pid_t pid = ::fork();
|
||||
if (pid < 0) return result;
|
||||
if (pid == 0) {
|
||||
body();
|
||||
::_exit(0);
|
||||
}
|
||||
int status = 0;
|
||||
if (::waitpid(pid, &status, 0) != pid) return result;
|
||||
result.Status = status;
|
||||
result.Log = ReadLog();
|
||||
return result;
|
||||
}
|
||||
|
||||
Bool DiedOfAbort(const ChildResult& r) { return WIFSIGNALED(r.Status) && WTERMSIG(r.Status) == SIGABRT; }
|
||||
std::string DescribeStatus(const ChildResult& r) {
|
||||
if (r.Status < 0) return "fork/waitpid failed";
|
||||
if (WIFEXITED(r.Status)) return "exited " + std::to_string(WEXITSTATUS(r.Status));
|
||||
if (WIFSIGNALED(r.Status)) return "signal " + std::to_string(WTERMSIG(r.Status));
|
||||
return "status " + std::to_string(r.Status);
|
||||
}
|
||||
#endif // MGTEST_HAVE_FORK
|
||||
|
||||
// Drives a call a trip wire must REFUSE, and asserts the wire NAMED what it refused. The
|
||||
// two arms differ by design: a poison or verify build stops the process, so the drive is a
|
||||
// forked child and the parent reads SIGABRT plus the line out of the log; a shipped push
|
||||
// build logs and carries on from a defined state, so there the line is read back in process
|
||||
// and the caller goes on to assert that nothing moved.
|
||||
template <class Body>
|
||||
void ExpectRefusedNaming(const char* needle, Body body) {
|
||||
#if MOBILEGL_PIPE_POISON || MOBILEGL_PIPE_VERIFY
|
||||
#if MGTEST_HAVE_FORK
|
||||
const std::string tagged = std::string("Fatal{ProtocolCorruption} ") + needle;
|
||||
const ChildResult child = RunInChild(body);
|
||||
EXPECT_TRUE(DiedOfAbort(child)) << DescribeStatus(child) << "; log: " << child.Log;
|
||||
EXPECT_NE(child.Log.find(tagged), std::string::npos)
|
||||
<< "the gate fired without naming what it refused; wanted \"" << tagged << "\"; log: " << child.Log;
|
||||
#else
|
||||
(void)needle;
|
||||
(void)body; // no fork on this platform; the verdict here is std::abort()
|
||||
#endif
|
||||
#else
|
||||
const std::string tagged = std::string("ProtocolCorruption ") + needle;
|
||||
const std::string before = ReadLog();
|
||||
body();
|
||||
EXPECT_NE(ReadLog().substr(before.size()).find(tagged), std::string::npos)
|
||||
<< "the gate refused without saying what it refused; wanted \"" << tagged << "\"";
|
||||
#endif
|
||||
}
|
||||
#endif // MOBILEGL_PIPE_PUSH
|
||||
} // namespace
|
||||
|
||||
// The contract commit's one case, and it pins the property everything else in this suite is
|
||||
@@ -110,6 +206,147 @@ TEST(CompositeResolver, TheCompositeBandHasExactlyOneDoor) {
|
||||
#endif
|
||||
}
|
||||
|
||||
// =========================================================================================
|
||||
// The APPLIER's half of the composite band (the wire commits'). The client-side resolver - the
|
||||
// signature cache keyed on ComputeDrawProgramSignature, the two release paths, the eviction -
|
||||
// is the client package's and lands beside these.
|
||||
//
|
||||
// WHY THE APPLIER HAS A BAND AT ALL. It is not because the server knows what a composite is:
|
||||
// it does not, and create / bind / delete_shader_state name one exactly as they name any other
|
||||
// program. It is because the band starts at 983040, so ONE pipeline composite in a
|
||||
// slot-indexed vector would grow that vector to ~983k records of ~240 bytes each - a 236 MB
|
||||
// spike on the first pipeline draw. Both spaces stay dense against their own high-water mark.
|
||||
// =========================================================================================
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
namespace {
|
||||
using MG_State::GLState::LinkArtifacts;
|
||||
using MG_State::GLState::SpirvArtifacts;
|
||||
|
||||
MGPProgramDesc CompositeDesc(MGPipeHandle cso, Uint32 stageMask) {
|
||||
MGPProgramDesc desc{};
|
||||
desc.Cso = cso;
|
||||
desc.StageMask = stageMask;
|
||||
return desc;
|
||||
}
|
||||
|
||||
MGPHandleOnly ProgramHandle(MGPipeHandle cso) {
|
||||
return MGPHandleOnly{cso, static_cast<Uint32>(MGPipeKind::ShaderCso), 0};
|
||||
}
|
||||
} // namespace
|
||||
#endif
|
||||
|
||||
// The band's record lands in the band's own table and the ordinary one is not grown by it -
|
||||
// which is the whole 236 MB of it - and every entry point still names it as an ordinary
|
||||
// program.
|
||||
TEST(CompositeResolver, ACompositeRecordLandsInTheBandsOwnTableAndNeverGrowsTheOrdinaryOne) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
const LinkArtifacts link;
|
||||
const SpirvArtifacts spirv;
|
||||
const MGPipeHandle composite{kMGPipeShaderCsoCompositeSlotBase + 2, 1};
|
||||
ASSERT_TRUE(MGPipeIsCompositeShaderSlot(composite.Slot));
|
||||
|
||||
MGPipeApplyCreateShaderState(CompositeDesc(composite, 0x3u), &link, &spirv);
|
||||
EXPECT_TRUE(MGPipeApplier().ShaderCsos.empty())
|
||||
<< "one composite grew the ordinary table to the band's base - that is the 236 MB spike";
|
||||
ASSERT_EQ(MGPipeApplier().CompositeShaderCsos.size(), 3u)
|
||||
<< "the band's table is indexed by (slot - base) and stays dense against its own high water";
|
||||
EXPECT_TRUE(MGPipeApplier().CompositeShaderCsos[2].Live);
|
||||
EXPECT_EQ(MGPipeApplier().CompositeShaderCsos[2].Gen, 1u);
|
||||
EXPECT_EQ(MGPipeApplier().CompositeShaderCsos[2].Desc.StageMask, 0x3u);
|
||||
|
||||
// AND THE SERVER NEVER LEARNS IT IS A COMPOSITE: the ordinary bind and draw-program calls
|
||||
// resolve it exactly as they resolve any other program.
|
||||
MGPipeApplyBindShaderState(ProgramHandle(composite));
|
||||
MGPipeApplySetDrawProgram(ProgramHandle(composite));
|
||||
EXPECT_EQ(MGPipeApplier().BoundShaderCso, composite);
|
||||
EXPECT_EQ(MGPipeApplier().DrawProgram, composite);
|
||||
EXPECT_EQ(MGPipeApplier().RefusedObjectCalls, 0u);
|
||||
|
||||
// An ordinary program lands in the other table, and the two do not see each other even
|
||||
// though the composite's record is at index 2 of its own.
|
||||
MGPipeApplyCreateShaderState(CompositeDesc(MGPipeHandle{2, 1}, 0x7u), &link, &spirv);
|
||||
ASSERT_GT(MGPipeApplier().ShaderCsos.size(), 2u);
|
||||
EXPECT_EQ(MGPipeApplier().ShaderCsos[2].Desc.StageMask, 0x7u);
|
||||
EXPECT_EQ(MGPipeApplier().CompositeShaderCsos[2].Desc.StageMask, 0x3u)
|
||||
<< "an ordinary program at slot 2 wrote the composite at band index 2";
|
||||
#endif
|
||||
}
|
||||
|
||||
// The composite's slot has TWO independent release paths - the pipeline cache's eviction and
|
||||
// the composite program's own destructor - and both go through one client helper. The second
|
||||
// arrival here is a refused no-op, which is what makes the double free proven rather than
|
||||
// assumed, and it clears the bindings exactly once.
|
||||
TEST(CompositeResolver, ASecondDeleteOfACompositeIsARefusedNoOpRatherThanASecondRelease) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
const LinkArtifacts link;
|
||||
const SpirvArtifacts spirv;
|
||||
const MGPipeHandle composite{kMGPipeShaderCsoCompositeSlotBase, 3};
|
||||
|
||||
MGPipeApplyCreateShaderState(CompositeDesc(composite, 0x3u), &link, &spirv);
|
||||
MGPipeApplySetDrawProgram(ProgramHandle(composite));
|
||||
ASSERT_EQ(MGPipeApplier().DrawProgram, composite);
|
||||
|
||||
MGPipeApplyDeleteShaderState(ProgramHandle(composite));
|
||||
EXPECT_FALSE(MGPipeApplier().CompositeShaderCsos[0].Live);
|
||||
EXPECT_EQ(MGPipeApplier().CompositeShaderCsos[0].Gen, 3u);
|
||||
EXPECT_TRUE(MGPipeHandleIsNull(MGPipeApplier().DrawProgram));
|
||||
EXPECT_EQ(MGPipeApplier().RefusedObjectCalls, 0u);
|
||||
const Uint64 serialAfterFirst = MGPipeApplier().ProgramBindingSerial;
|
||||
|
||||
MGPipeApplyDeleteShaderState(ProgramHandle(composite));
|
||||
EXPECT_EQ(MGPipeApplier().RefusedObjectCalls, 1u);
|
||||
EXPECT_EQ(MGPipeApplier().ProgramBindingSerial, serialAfterFirst)
|
||||
<< "the second release moved the binding serial, so it was not a no-op";
|
||||
|
||||
// And the band's slot is re-usable afterwards: a recycled composite is a new identity and
|
||||
// starts its record over.
|
||||
MGPipeApplyCreateShaderState(CompositeDesc(MGPipeHandle{composite.Slot, 4}, 0x1u), &link, &spirv);
|
||||
EXPECT_TRUE(MGPipeApplier().CompositeShaderCsos[0].Live);
|
||||
EXPECT_EQ(MGPipeApplier().CompositeShaderCsos[0].Gen, 4u);
|
||||
EXPECT_EQ(MGPipeApplier().CompositeShaderCsos[0].Serial, 0u);
|
||||
#endif
|
||||
}
|
||||
|
||||
// The band is INSIDE the ShaderCso slot limit, so the bound the applier refuses at is the limit
|
||||
// itself and not the band's base - a bound below it would refuse the very slots the allocator's
|
||||
// one composite door is allowed to hand out.
|
||||
TEST(CompositeResolver, ASlotAtTheShaderCsoLimitIsRefusedWhileTheLastBandSlotIsNot) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
const LinkArtifacts link;
|
||||
const SpirvArtifacts spirv;
|
||||
|
||||
// The positive control: the LAST slot of the band is a legal composite handle.
|
||||
const MGPipeHandle last{kMGPipeShaderCsoSlotLimit - 1, 1};
|
||||
ASSERT_TRUE(MGPipeIsCompositeShaderSlot(last.Slot));
|
||||
MGPipeApplyCreateShaderState(CompositeDesc(last, 0x3u), &link, &spirv);
|
||||
ASSERT_EQ(MGPipeApplier().CompositeShaderCsos.size(),
|
||||
static_cast<SizeT>(kMGPipeShaderCsoSlotLimit - kMGPipeShaderCsoCompositeSlotBase));
|
||||
EXPECT_TRUE(MGPipeApplier().CompositeShaderCsos.back().Live);
|
||||
EXPECT_TRUE(MGPipeApplier().ShaderCsos.empty());
|
||||
|
||||
const MGPProgramDesc past = CompositeDesc(MGPipeHandle{kMGPipeShaderCsoSlotLimit, 1}, 0x3u);
|
||||
ExpectRefusedNaming("create_shader_state {slot=1048576, gen=1}: the slot is outside the record table's "
|
||||
"bound",
|
||||
[&past, &link, &spirv]() { MGPipeApplyCreateShaderState(past, &link, &spirv); });
|
||||
|
||||
// And an ORDINARY slot at or above the band's base is out of range by definition: the
|
||||
// allocator refuses the band for an ordinary program, so nothing legal can name one.
|
||||
MGPipeApplySetDrawProgram(ProgramHandle(MGPipeHandle{kMGPipeShaderCsoCompositeSlotBase - 1, 1}));
|
||||
EXPECT_EQ(MGPipeApplier().RefusedObjectCalls, 1u)
|
||||
<< "an ordinary slot below the band resolved against a record nobody created";
|
||||
#endif
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
namespace fs = std::filesystem;
|
||||
const fs::path path =
|
||||
|
||||
@@ -39,13 +39,19 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <system_error>
|
||||
|
||||
#if defined(_WIN32)
|
||||
#include <process.h>
|
||||
#define MGTEST_HAVE_FORK 0
|
||||
#else
|
||||
#include <csignal>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
#define MGTEST_HAVE_FORK 1
|
||||
#endif
|
||||
|
||||
#include "Includes.h"
|
||||
@@ -68,6 +74,113 @@ namespace {
|
||||
return static_cast<int>(getpid());
|
||||
#endif
|
||||
}
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
std::string ReadLog() {
|
||||
std::ifstream in(g_logPath, std::ios::binary);
|
||||
std::ostringstream ss;
|
||||
ss << in.rdbuf();
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
// A fresh applier per case, BOTH SCOPES, and it takes both because there are two: a reset
|
||||
// is a make-current and deliberately KEEPS the object records, so a fixture that wants a
|
||||
// genuinely empty applier has to say the other one as well. Every case is its own process
|
||||
// under ctest, so this is belt and braces - but running the binary by hand must give the
|
||||
// same answers as running it under ctest.
|
||||
struct ApplierGuard {
|
||||
ApplierGuard() {
|
||||
MGPipeApplierReset();
|
||||
MGPipeApplierReleaseObjectRecords();
|
||||
}
|
||||
~ApplierGuard() {
|
||||
MGPipeApplierReset();
|
||||
MGPipeApplierReleaseObjectRecords();
|
||||
}
|
||||
};
|
||||
|
||||
#if MGTEST_HAVE_FORK
|
||||
struct ChildResult {
|
||||
int Status = -1;
|
||||
std::string Log;
|
||||
};
|
||||
|
||||
template <class Body>
|
||||
ChildResult RunInChild(Body body) {
|
||||
ChildResult result;
|
||||
std::error_code ec;
|
||||
std::filesystem::remove(g_logPath, ec);
|
||||
std::fflush(nullptr);
|
||||
const pid_t pid = ::fork();
|
||||
if (pid < 0) return result;
|
||||
if (pid == 0) {
|
||||
body();
|
||||
::_exit(0);
|
||||
}
|
||||
int status = 0;
|
||||
if (::waitpid(pid, &status, 0) != pid) return result;
|
||||
result.Status = status;
|
||||
result.Log = ReadLog();
|
||||
return result;
|
||||
}
|
||||
|
||||
Bool DiedOfAbort(const ChildResult& r) { return WIFSIGNALED(r.Status) && WTERMSIG(r.Status) == SIGABRT; }
|
||||
std::string DescribeStatus(const ChildResult& r) {
|
||||
if (r.Status < 0) return "fork/waitpid failed";
|
||||
if (WIFEXITED(r.Status)) return "exited " + std::to_string(WEXITSTATUS(r.Status));
|
||||
if (WIFSIGNALED(r.Status)) return "signal " + std::to_string(WTERMSIG(r.Status));
|
||||
return "status " + std::to_string(r.Status);
|
||||
}
|
||||
#endif // MGTEST_HAVE_FORK
|
||||
|
||||
// Drives a call a trip wire must REFUSE, and asserts the wire NAMED what it refused. The
|
||||
// two arms differ by design: a poison or verify build stops the process, so the drive is a
|
||||
// forked child and the parent reads SIGABRT plus the line out of the log; a shipped push
|
||||
// build logs and carries on from a defined state, so there the line is read back in process
|
||||
// and the caller goes on to assert that nothing moved.
|
||||
template <class Body>
|
||||
void ExpectRefusedNaming(const char* needle, Body body) {
|
||||
#if MOBILEGL_PIPE_POISON || MOBILEGL_PIPE_VERIFY
|
||||
#if MGTEST_HAVE_FORK
|
||||
const std::string tagged = std::string("Fatal{ProtocolCorruption} ") + needle;
|
||||
const ChildResult child = RunInChild(body);
|
||||
EXPECT_TRUE(DiedOfAbort(child)) << DescribeStatus(child) << "; log: " << child.Log;
|
||||
EXPECT_NE(child.Log.find(tagged), std::string::npos)
|
||||
<< "the gate fired without naming what it refused; wanted \"" << tagged << "\"; log: " << child.Log;
|
||||
#else
|
||||
(void)needle;
|
||||
(void)body; // no fork on this platform; the verdict here is std::abort()
|
||||
#endif
|
||||
#else
|
||||
const std::string tagged = std::string("ProtocolCorruption ") + needle;
|
||||
const std::string before = ReadLog();
|
||||
body();
|
||||
EXPECT_NE(ReadLog().substr(before.size()).find(tagged), std::string::npos)
|
||||
<< "the gate refused without saying what it refused; wanted \"" << tagged << "\"";
|
||||
#endif
|
||||
}
|
||||
|
||||
// A record with real values in every field a case might read back, so a body that stored
|
||||
// the wrong one - or stored nothing - is visible BY FIELD.
|
||||
MGPFramebufferState FramebufferRecord(MGPipeHandle fbo, MGPipeFramebufferTarget target, Uint16 width) {
|
||||
MGPFramebufferState state{};
|
||||
state.Fbo = fbo;
|
||||
state.Target = static_cast<Uint8>(target);
|
||||
state.Width = width;
|
||||
state.Height = 64;
|
||||
state.Layers = 1;
|
||||
state.Samples = 1;
|
||||
state.Complete = 1;
|
||||
state.ContentHash = 0x1234u + width;
|
||||
for (Uint32 i = 0; i < kMGPipeMaxColorAttachments; ++i) {
|
||||
state.DrawBuffers[i] = static_cast<Int8>(i == 0 ? 0 : -1);
|
||||
}
|
||||
state.Color[0].Res = MGPipeHandle{9, 1};
|
||||
state.Color[0].InternalFormat = 0x8058u; // GL_RGBA8
|
||||
state.Color[0].Kind = 1;
|
||||
return state;
|
||||
}
|
||||
#endif // MOBILEGL_PIPE_PUSH
|
||||
} // namespace
|
||||
|
||||
// The one case the contract commit lands, and it is not a placeholder: it pins the SHAPE every
|
||||
@@ -89,6 +202,142 @@ TEST(FramebufferEmit, TheEmitterIsOneNeverDestroyedProcessSingleton) {
|
||||
#endif
|
||||
}
|
||||
|
||||
// =========================================================================================
|
||||
// The APPLIER's half of set_framebuffer_state (the wire commits'). The emitter's half - the
|
||||
// resolved read surface, the draw-buffer array in the content hash, a recycled handle never
|
||||
// suppressed against its predecessor, an attachment point above the wire width refused rather
|
||||
// than truncated - is the client package's and lands beside these.
|
||||
// =========================================================================================
|
||||
|
||||
// THE WHOLE POINT OF THE Target BYTE. GL has two independent framebuffer bindings and this
|
||||
// record carries one Fbo and one ReadSurface, so a record says which binding it describes;
|
||||
// Both is one object bound to both and writes both. Deleting either store, or the serial bump,
|
||||
// leaves this red.
|
||||
TEST(FramebufferEmit, ADrawRecordAndAReadRecordAreKeptApartAndBothWritesBoth) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
const Uint64 serialAtStart = MGPipeApplier().FramebufferSerial;
|
||||
|
||||
MGPipeApplySetFramebufferState(FramebufferRecord(MGPipeHandle{4, 1}, MGPipeFramebufferTarget::Draw, 100));
|
||||
EXPECT_EQ(MGPipeApplier().DrawFramebuffer.Fbo, (MGPipeHandle{4, 1}));
|
||||
EXPECT_EQ(MGPipeApplier().DrawFramebuffer.Width, 100u);
|
||||
EXPECT_EQ(MGPipeApplier().DrawFramebuffer.Color[0].InternalFormat, 0x8058u);
|
||||
EXPECT_EQ(MGPipeApplier().DrawFramebuffer.DrawBuffers[0], 0);
|
||||
EXPECT_EQ(MGPipeApplier().ReadFramebuffer.Fbo, kMGPipeNullHandle)
|
||||
<< "a Draw record landed in the read binding as well";
|
||||
const Uint64 afterDraw = MGPipeApplier().FramebufferSerial;
|
||||
EXPECT_GT(afterDraw, serialAtStart) << "an applied record must move the serial the twin memoises";
|
||||
|
||||
MGPipeApplySetFramebufferState(FramebufferRecord(MGPipeHandle{5, 2}, MGPipeFramebufferTarget::Read, 200));
|
||||
EXPECT_EQ(MGPipeApplier().ReadFramebuffer.Fbo, (MGPipeHandle{5, 2}));
|
||||
EXPECT_EQ(MGPipeApplier().ReadFramebuffer.Width, 200u);
|
||||
EXPECT_EQ(MGPipeApplier().DrawFramebuffer.Fbo, (MGPipeHandle{4, 1}))
|
||||
<< "a Read record overwrote the draw binding";
|
||||
EXPECT_EQ(MGPipeApplier().DrawFramebuffer.Width, 100u);
|
||||
EXPECT_GT(MGPipeApplier().FramebufferSerial, afterDraw);
|
||||
|
||||
// Both: one record, one serial bump, two destinations.
|
||||
const Uint64 beforeBoth = MGPipeApplier().FramebufferSerial;
|
||||
MGPipeApplySetFramebufferState(FramebufferRecord(MGPipeHandle{6, 3}, MGPipeFramebufferTarget::Both, 300));
|
||||
EXPECT_EQ(MGPipeApplier().DrawFramebuffer.Fbo, (MGPipeHandle{6, 3}));
|
||||
EXPECT_EQ(MGPipeApplier().ReadFramebuffer.Fbo, (MGPipeHandle{6, 3}));
|
||||
EXPECT_EQ(MGPipeApplier().DrawFramebuffer.Width, 300u);
|
||||
EXPECT_EQ(MGPipeApplier().ReadFramebuffer.Width, 300u);
|
||||
EXPECT_EQ(MGPipeApplier().FramebufferSerial, beforeBoth + 1)
|
||||
<< "a Both record is ONE record and moves the serial once";
|
||||
|
||||
// A framebuffer has a handle but NO wire lifetime, so there is no record to refuse against
|
||||
// and this entry point never counts an object refusal.
|
||||
EXPECT_EQ(MGPipeApplier().RefusedObjectCalls, 0u);
|
||||
#endif
|
||||
}
|
||||
|
||||
// A target outside the three is not a binding this server has, and guessing one would put a
|
||||
// draw's attachments into the read record or the other way round.
|
||||
TEST(FramebufferEmit, ATargetOutsideTheThreeBindingsIsRefusedNamingTheRecord) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
MGPFramebufferState bad = FramebufferRecord(MGPipeHandle{7, 4}, MGPipeFramebufferTarget::Draw, 100);
|
||||
bad.Target = static_cast<Uint8>(MGPipeFramebufferTarget::Count);
|
||||
const Uint64 serialBefore = MGPipeApplier().FramebufferSerial;
|
||||
|
||||
ExpectRefusedNaming("set_framebuffer_state {slot=7, gen=4, target=3}: the record names no framebuffer "
|
||||
"binding target",
|
||||
[&bad]() { MGPipeApplySetFramebufferState(bad); });
|
||||
EXPECT_EQ(MGPipeApplier().DrawFramebuffer.Fbo, kMGPipeNullHandle);
|
||||
EXPECT_EQ(MGPipeApplier().ReadFramebuffer.Fbo, kMGPipeNullHandle);
|
||||
EXPECT_EQ(MGPipeApplier().FramebufferSerial, serialBefore)
|
||||
<< "a refused record must not move the serial";
|
||||
#endif
|
||||
}
|
||||
|
||||
// The draw-buffer array is an INDEX into this record's own Color[], and -1 is NONE. An entry
|
||||
// outside that range would have the server read a colour attachment the record does not carry,
|
||||
// which is the truncation the wire width's cap refusal exists to prevent upstream.
|
||||
TEST(FramebufferEmit, ADrawBufferEntryOutsideTheRecordsOwnArrayIsRefusedRatherThanRead) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
|
||||
// The positive control first: -1 everywhere and the last legal index are both fine, so
|
||||
// what follows is refusing the value and not the loop around it.
|
||||
MGPFramebufferState legal = FramebufferRecord(MGPipeHandle{8, 1}, MGPipeFramebufferTarget::Draw, 100);
|
||||
legal.DrawBuffers[7] = static_cast<Int8>(kMGPipeMaxColorAttachments - 1);
|
||||
MGPipeApplySetFramebufferState(legal);
|
||||
ASSERT_EQ(MGPipeApplier().DrawFramebuffer.Fbo, (MGPipeHandle{8, 1}));
|
||||
const Uint64 serialBefore = MGPipeApplier().FramebufferSerial;
|
||||
|
||||
MGPFramebufferState past = FramebufferRecord(MGPipeHandle{8, 1}, MGPipeFramebufferTarget::Draw, 111);
|
||||
past.DrawBuffers[3] = static_cast<Int8>(kMGPipeMaxColorAttachments);
|
||||
ExpectRefusedNaming("set_framebuffer_state {slot=8, gen=1, target=0}: a draw-buffer entry names a "
|
||||
"colour attachment outside the record's own array",
|
||||
[&past]() { MGPipeApplySetFramebufferState(past); });
|
||||
|
||||
MGPFramebufferState negative = FramebufferRecord(MGPipeHandle{8, 1}, MGPipeFramebufferTarget::Draw, 222);
|
||||
negative.DrawBuffers[0] = -2;
|
||||
ExpectRefusedNaming("set_framebuffer_state {slot=8, gen=1, target=0}: a draw-buffer entry names a "
|
||||
"colour attachment outside the record's own array",
|
||||
[&negative]() { MGPipeApplySetFramebufferState(negative); });
|
||||
|
||||
EXPECT_EQ(MGPipeApplier().DrawFramebuffer.Width, 100u) << "a refused record was stored anyway";
|
||||
EXPECT_EQ(MGPipeApplier().FramebufferSerial, serialBefore);
|
||||
#endif
|
||||
}
|
||||
|
||||
// D-J4. The two framebuffer records are per-context WORKING state and a make-current takes
|
||||
// them - but their serial ADVANCES rather than restarting, because a counter that walks back
|
||||
// through values it has already stamped into a twin that outlived the switch is not a
|
||||
// generation at all. Restoring `= 0` anywhere in the reset leaves this red.
|
||||
TEST(FramebufferEmit, AMakeCurrentClearsBothRecordsAndAdvancesTheSerialRatherThanZeroingIt) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
MGPipeApplySetFramebufferState(FramebufferRecord(MGPipeHandle{4, 1}, MGPipeFramebufferTarget::Both, 100));
|
||||
const Uint64 serialBefore = MGPipeApplier().FramebufferSerial;
|
||||
ASSERT_EQ(MGPipeApplier().DrawFramebuffer.Width, 100u);
|
||||
|
||||
MGPipeApplierReset(); // the make-current
|
||||
|
||||
EXPECT_EQ(MGPipeApplier().DrawFramebuffer.Fbo, kMGPipeNullHandle);
|
||||
EXPECT_EQ(MGPipeApplier().ReadFramebuffer.Fbo, kMGPipeNullHandle);
|
||||
EXPECT_EQ(MGPipeApplier().DrawFramebuffer.Width, 0u);
|
||||
EXPECT_GT(MGPipeApplier().FramebufferSerial, serialBefore)
|
||||
<< "the serial was carried over or restarted; the cleared window is itself a change the "
|
||||
"twin has to hear about, and no stamped value may ever recur";
|
||||
|
||||
// And the teardown scope advances it again, for the same reason.
|
||||
const Uint64 afterReset = MGPipeApplier().FramebufferSerial;
|
||||
MGPipeApplierReleaseObjectRecords();
|
||||
EXPECT_GT(MGPipeApplier().FramebufferSerial, afterReset);
|
||||
#endif
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
// Before anything logs: the logger reads this variable once, on its first write, and
|
||||
// caches the handle. The name carries this process's pid, and the file is removed on the
|
||||
|
||||
@@ -30,13 +30,19 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <system_error>
|
||||
|
||||
#if defined(_WIN32)
|
||||
#include <process.h>
|
||||
#define MGTEST_HAVE_FORK 0
|
||||
#else
|
||||
#include <csignal>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
#define MGTEST_HAVE_FORK 1
|
||||
#endif
|
||||
|
||||
#include "Includes.h"
|
||||
@@ -59,6 +65,92 @@ namespace {
|
||||
return static_cast<int>(getpid());
|
||||
#endif
|
||||
}
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
std::string ReadLog() {
|
||||
std::ifstream in(g_logPath, std::ios::binary);
|
||||
std::ostringstream ss;
|
||||
ss << in.rdbuf();
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
// A fresh applier per case, BOTH SCOPES, and it takes both because there are two: a reset
|
||||
// is a make-current and deliberately KEEPS the object records, so a fixture that wants a
|
||||
// genuinely empty applier has to say the other one as well. Every case is its own process
|
||||
// under ctest, so this is belt and braces - but running the binary by hand must give the
|
||||
// same answers as running it under ctest.
|
||||
struct ApplierGuard {
|
||||
ApplierGuard() {
|
||||
MGPipeApplierReset();
|
||||
MGPipeApplierReleaseObjectRecords();
|
||||
}
|
||||
~ApplierGuard() {
|
||||
MGPipeApplierReset();
|
||||
MGPipeApplierReleaseObjectRecords();
|
||||
}
|
||||
};
|
||||
|
||||
#if MGTEST_HAVE_FORK
|
||||
struct ChildResult {
|
||||
int Status = -1;
|
||||
std::string Log;
|
||||
};
|
||||
|
||||
template <class Body>
|
||||
ChildResult RunInChild(Body body) {
|
||||
ChildResult result;
|
||||
std::error_code ec;
|
||||
std::filesystem::remove(g_logPath, ec);
|
||||
std::fflush(nullptr);
|
||||
const pid_t pid = ::fork();
|
||||
if (pid < 0) return result;
|
||||
if (pid == 0) {
|
||||
body();
|
||||
::_exit(0);
|
||||
}
|
||||
int status = 0;
|
||||
if (::waitpid(pid, &status, 0) != pid) return result;
|
||||
result.Status = status;
|
||||
result.Log = ReadLog();
|
||||
return result;
|
||||
}
|
||||
|
||||
Bool DiedOfAbort(const ChildResult& r) { return WIFSIGNALED(r.Status) && WTERMSIG(r.Status) == SIGABRT; }
|
||||
std::string DescribeStatus(const ChildResult& r) {
|
||||
if (r.Status < 0) return "fork/waitpid failed";
|
||||
if (WIFEXITED(r.Status)) return "exited " + std::to_string(WEXITSTATUS(r.Status));
|
||||
if (WIFSIGNALED(r.Status)) return "signal " + std::to_string(WTERMSIG(r.Status));
|
||||
return "status " + std::to_string(r.Status);
|
||||
}
|
||||
#endif // MGTEST_HAVE_FORK
|
||||
|
||||
// Drives a call a trip wire must REFUSE, and asserts the wire NAMED what it refused. The
|
||||
// two arms differ by design: a poison or verify build stops the process, so the drive is a
|
||||
// forked child and the parent reads SIGABRT plus the line out of the log; a shipped push
|
||||
// build logs and carries on from a defined state, so there the line is read back in process
|
||||
// and the caller goes on to assert that nothing moved.
|
||||
template <class Body>
|
||||
void ExpectRefusedNaming(const char* needle, Body body) {
|
||||
#if MOBILEGL_PIPE_POISON || MOBILEGL_PIPE_VERIFY
|
||||
#if MGTEST_HAVE_FORK
|
||||
const std::string tagged = std::string("Fatal{ProtocolCorruption} ") + needle;
|
||||
const ChildResult child = RunInChild(body);
|
||||
EXPECT_TRUE(DiedOfAbort(child)) << DescribeStatus(child) << "; log: " << child.Log;
|
||||
EXPECT_NE(child.Log.find(tagged), std::string::npos)
|
||||
<< "the gate fired without naming what it refused; wanted \"" << tagged << "\"; log: " << child.Log;
|
||||
#else
|
||||
(void)needle;
|
||||
(void)body; // no fork on this platform; the verdict here is std::abort()
|
||||
#endif
|
||||
#else
|
||||
const std::string tagged = std::string("ProtocolCorruption ") + needle;
|
||||
const std::string before = ReadLog();
|
||||
body();
|
||||
EXPECT_NE(ReadLog().substr(before.size()).find(tagged), std::string::npos)
|
||||
<< "the gate refused without saying what it refused; wanted \"" << tagged << "\"";
|
||||
#endif
|
||||
}
|
||||
#endif // MOBILEGL_PIPE_PUSH
|
||||
} // namespace
|
||||
|
||||
// See FramebufferEmitTest's twin for why this is a shape pin rather than a placeholder.
|
||||
@@ -75,6 +167,162 @@ TEST(ImageEmit, TheEmitterIsOneNeverDestroyedProcessSingleton) {
|
||||
#endif
|
||||
}
|
||||
|
||||
// =========================================================================================
|
||||
// The APPLIER's half of set_shader_images (the wire commits'). The emitter's half - the
|
||||
// high-water-zero early-out, the content hash covering Access and InternalFormat, the shutter
|
||||
// keyed on the FRONTEND sampling-resolution generation - is the client package's.
|
||||
// =========================================================================================
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
namespace {
|
||||
// Every field carries a value of its own, and two of them are the point: InternalFormat and
|
||||
// Access are live glBindImageTexture state that the format-less image bake keys on, so a
|
||||
// body that dropped either would leave the server baking against a format the shader was
|
||||
// not built for.
|
||||
MGPImageView ImageAt(Uint32 unit, Uint32 internalFormat, Uint8 access) {
|
||||
MGPImageView view{};
|
||||
view.Res = MGPipeHandle{unit + 1, 1};
|
||||
view.Unit = unit;
|
||||
view.InternalFormat = internalFormat;
|
||||
view.Layer = 3;
|
||||
view.Level = 2;
|
||||
view.Layered = 1;
|
||||
view.Access = access;
|
||||
return view;
|
||||
}
|
||||
} // namespace
|
||||
#endif
|
||||
|
||||
// The window rule, one field at a time: the entries land where the header says and nowhere
|
||||
// else, and every field of an entry survives. Deleting the copy loop, the two window
|
||||
// assignments or the serial bump leaves this red.
|
||||
TEST(ImageEmit, TheImageSetLandsInItsWindowWithEveryFieldTheShaderWasBuiltAgainst) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
// Access is a Uint8 on the wire - the client's own read/write/read-write encoding, not a
|
||||
// GL enum - and InternalFormat is the application's, which the server recasts.
|
||||
const MGPImageView entries[2] = {ImageAt(2, 0x8814u /* GL_RGBA32F */, 2 /* write only */),
|
||||
ImageAt(3, 0x8230u /* GL_RG32F */, 3 /* read write */)};
|
||||
MGPShaderImages header{};
|
||||
header.Start = 2;
|
||||
header.Count = 2;
|
||||
header.ContentHash = 0x5150u;
|
||||
const Uint64 serialBefore = MGPipeApplier().ShaderImagesSerial;
|
||||
// The other two sets' serials, taken AFTER the fixture: a reset and a teardown each advance
|
||||
// every working serial, so "unchanged" is measured from here rather than from zero.
|
||||
const Uint64 samplerViewsSerial = MGPipeApplier().SamplerViewsSerial;
|
||||
|
||||
MGPipeApplySetShaderImages(header, entries);
|
||||
|
||||
EXPECT_EQ(MGPipeApplier().ShaderImageStart, 2u);
|
||||
EXPECT_EQ(MGPipeApplier().ShaderImageCount, 2u);
|
||||
EXPECT_EQ(MGPipeApplier().BoundShaderImages[2].Res, (MGPipeHandle{3, 1}));
|
||||
EXPECT_EQ(MGPipeApplier().BoundShaderImages[2].InternalFormat, 0x8814u);
|
||||
EXPECT_EQ(MGPipeApplier().BoundShaderImages[2].Access, 2u);
|
||||
EXPECT_EQ(MGPipeApplier().BoundShaderImages[3].Access, 3u);
|
||||
EXPECT_EQ(MGPipeApplier().BoundShaderImages[2].Level, 2u);
|
||||
EXPECT_EQ(MGPipeApplier().BoundShaderImages[2].Layer, 3u);
|
||||
EXPECT_EQ(MGPipeApplier().BoundShaderImages[2].Layered, 1u);
|
||||
EXPECT_EQ(MGPipeApplier().BoundShaderImages[3].InternalFormat, 0x8230u);
|
||||
EXPECT_TRUE(MGPipeHandleIsNull(MGPipeApplier().BoundShaderImages[1].Res)) << "the set wrote below its window";
|
||||
EXPECT_TRUE(MGPipeHandleIsNull(MGPipeApplier().BoundShaderImages[4].Res)) << "the set wrote above its window";
|
||||
EXPECT_GT(MGPipeApplier().ShaderImagesSerial, serialBefore);
|
||||
|
||||
// "The last set as received": a narrower set says nothing about what it does not name.
|
||||
MGPShaderImages narrow{};
|
||||
narrow.Start = 2;
|
||||
narrow.Count = 1;
|
||||
MGPipeApplySetShaderImages(narrow, entries);
|
||||
EXPECT_EQ(MGPipeApplier().ShaderImageCount, 1u);
|
||||
EXPECT_EQ(MGPipeApplier().BoundShaderImages[3].InternalFormat, 0x8230u)
|
||||
<< "the entry outside the new window was cleared";
|
||||
EXPECT_EQ(MGPipeApplier().SamplerViewsSerial, samplerViewsSerial)
|
||||
<< "the image set moved another set's serial; the three are independent";
|
||||
#endif
|
||||
}
|
||||
|
||||
// The window gate, at the bound and one past it, and the null-tail arm. The image-unit space
|
||||
// is the same merged 192 the sampler units are.
|
||||
TEST(ImageEmit, AnImageWindowPastTheImageUnitSpaceIsRefusedRatherThanTruncated) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
const MGPImageView entry = ImageAt(0, 0x8058u, 2);
|
||||
|
||||
MGPShaderImages exact{};
|
||||
exact.Start = kMGPipeMaxImageUnits - 1;
|
||||
exact.Count = 1;
|
||||
MGPipeApplySetShaderImages(exact, &entry);
|
||||
ASSERT_EQ(MGPipeApplier().ShaderImageCount, 1u);
|
||||
const Uint64 serialBefore = MGPipeApplier().ShaderImagesSerial;
|
||||
|
||||
MGPShaderImages past{};
|
||||
past.Start = kMGPipeMaxImageUnits;
|
||||
past.Count = 1;
|
||||
past.ContentHash = 9;
|
||||
ExpectRefusedNaming("set_shader_images {start=192, count=1, hash=9}: the window runs past the merged "
|
||||
"texture-unit space",
|
||||
[&past, &entry]() { MGPipeApplySetShaderImages(past, &entry); });
|
||||
|
||||
MGPShaderImages noTail{};
|
||||
noTail.Start = 0;
|
||||
noTail.Count = 1;
|
||||
ExpectRefusedNaming("set_shader_images {start=0, count=1, hash=0}: a non-empty set carries no entries",
|
||||
[&noTail]() { MGPipeApplySetShaderImages(noTail, nullptr); });
|
||||
|
||||
EXPECT_EQ(MGPipeApplier().ShaderImagesSerial, serialBefore) << "a refused set moved the serial";
|
||||
EXPECT_EQ(MGPipeApplier().ShaderImageStart, kMGPipeMaxImageUnits - 1);
|
||||
#endif
|
||||
}
|
||||
|
||||
// An EMPTY set is not a refusal: it is what a program with no image uniforms publishes, and it
|
||||
// still moves the serial, because "no images" is a state the twin has to hear about.
|
||||
TEST(ImageEmit, AnEmptySetIsAppliedRatherThanRefusedAndStillMovesTheSerial) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
const MGPImageView entry = ImageAt(0, 0x8058u, 1);
|
||||
MGPShaderImages filled{};
|
||||
filled.Count = 1;
|
||||
MGPipeApplySetShaderImages(filled, &entry);
|
||||
const Uint64 serialBefore = MGPipeApplier().ShaderImagesSerial;
|
||||
|
||||
MGPShaderImages empty{};
|
||||
MGPipeApplySetShaderImages(empty, nullptr);
|
||||
EXPECT_EQ(MGPipeApplier().ShaderImageCount, 0u);
|
||||
EXPECT_GT(MGPipeApplier().ShaderImagesSerial, serialBefore);
|
||||
EXPECT_EQ(MGPipeApplier().BoundShaderImages[0].InternalFormat, 0x8058u)
|
||||
<< "an empty window cleared entries it never named";
|
||||
#endif
|
||||
}
|
||||
|
||||
// D-J4: the image set is per-context WORKING state, so a make-current takes it and ADVANCES
|
||||
// its serial rather than restarting it.
|
||||
TEST(ImageEmit, AMakeCurrentClearsTheImageSetAndAdvancesItsSerial) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
const MGPImageView entry = ImageAt(1, 0x8058u, 1);
|
||||
MGPShaderImages header{};
|
||||
header.Start = 1;
|
||||
header.Count = 1;
|
||||
MGPipeApplySetShaderImages(header, &entry);
|
||||
const Uint64 serialBefore = MGPipeApplier().ShaderImagesSerial;
|
||||
|
||||
MGPipeApplierReset();
|
||||
|
||||
EXPECT_EQ(MGPipeApplier().ShaderImageCount, 0u);
|
||||
EXPECT_EQ(MGPipeApplier().ShaderImageStart, 0u);
|
||||
EXPECT_TRUE(MGPipeHandleIsNull(MGPipeApplier().BoundShaderImages[1].Res));
|
||||
EXPECT_GT(MGPipeApplier().ShaderImagesSerial, serialBefore);
|
||||
#endif
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
namespace fs = std::filesystem;
|
||||
const fs::path path =
|
||||
|
||||
@@ -28,13 +28,19 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <system_error>
|
||||
|
||||
#if defined(_WIN32)
|
||||
#include <process.h>
|
||||
#define MGTEST_HAVE_FORK 0
|
||||
#else
|
||||
#include <csignal>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
#define MGTEST_HAVE_FORK 1
|
||||
#endif
|
||||
|
||||
#include "Includes.h"
|
||||
@@ -42,6 +48,10 @@
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
#include <MG_Impl/Pipe/ProgramEmit.h>
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
// The applier takes the two artefact structs BY POINTER beside the record, so a case that
|
||||
// drives create_shader_state needs their definitions - the applier's own header deliberately
|
||||
// only forward-declares them.
|
||||
#include <MG_State/GLState/ProgramState/ProgramArtifacts.h>
|
||||
#endif
|
||||
|
||||
using namespace MobileGL;
|
||||
@@ -57,6 +67,92 @@ namespace {
|
||||
return static_cast<int>(getpid());
|
||||
#endif
|
||||
}
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
std::string ReadLog() {
|
||||
std::ifstream in(g_logPath, std::ios::binary);
|
||||
std::ostringstream ss;
|
||||
ss << in.rdbuf();
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
// A fresh applier per case, BOTH SCOPES, and it takes both because there are two: a reset
|
||||
// is a make-current and deliberately KEEPS the object records, so a fixture that wants a
|
||||
// genuinely empty applier has to say the other one as well. Every case is its own process
|
||||
// under ctest, so this is belt and braces - but running the binary by hand must give the
|
||||
// same answers as running it under ctest.
|
||||
struct ApplierGuard {
|
||||
ApplierGuard() {
|
||||
MGPipeApplierReset();
|
||||
MGPipeApplierReleaseObjectRecords();
|
||||
}
|
||||
~ApplierGuard() {
|
||||
MGPipeApplierReset();
|
||||
MGPipeApplierReleaseObjectRecords();
|
||||
}
|
||||
};
|
||||
|
||||
#if MGTEST_HAVE_FORK
|
||||
struct ChildResult {
|
||||
int Status = -1;
|
||||
std::string Log;
|
||||
};
|
||||
|
||||
template <class Body>
|
||||
ChildResult RunInChild(Body body) {
|
||||
ChildResult result;
|
||||
std::error_code ec;
|
||||
std::filesystem::remove(g_logPath, ec);
|
||||
std::fflush(nullptr);
|
||||
const pid_t pid = ::fork();
|
||||
if (pid < 0) return result;
|
||||
if (pid == 0) {
|
||||
body();
|
||||
::_exit(0);
|
||||
}
|
||||
int status = 0;
|
||||
if (::waitpid(pid, &status, 0) != pid) return result;
|
||||
result.Status = status;
|
||||
result.Log = ReadLog();
|
||||
return result;
|
||||
}
|
||||
|
||||
Bool DiedOfAbort(const ChildResult& r) { return WIFSIGNALED(r.Status) && WTERMSIG(r.Status) == SIGABRT; }
|
||||
std::string DescribeStatus(const ChildResult& r) {
|
||||
if (r.Status < 0) return "fork/waitpid failed";
|
||||
if (WIFEXITED(r.Status)) return "exited " + std::to_string(WEXITSTATUS(r.Status));
|
||||
if (WIFSIGNALED(r.Status)) return "signal " + std::to_string(WTERMSIG(r.Status));
|
||||
return "status " + std::to_string(r.Status);
|
||||
}
|
||||
#endif // MGTEST_HAVE_FORK
|
||||
|
||||
// Drives a call a trip wire must REFUSE, and asserts the wire NAMED what it refused. The
|
||||
// two arms differ by design: a poison or verify build stops the process, so the drive is a
|
||||
// forked child and the parent reads SIGABRT plus the line out of the log; a shipped push
|
||||
// build logs and carries on from a defined state, so there the line is read back in process
|
||||
// and the caller goes on to assert that nothing moved.
|
||||
template <class Body>
|
||||
void ExpectRefusedNaming(const char* needle, Body body) {
|
||||
#if MOBILEGL_PIPE_POISON || MOBILEGL_PIPE_VERIFY
|
||||
#if MGTEST_HAVE_FORK
|
||||
const std::string tagged = std::string("Fatal{ProtocolCorruption} ") + needle;
|
||||
const ChildResult child = RunInChild(body);
|
||||
EXPECT_TRUE(DiedOfAbort(child)) << DescribeStatus(child) << "; log: " << child.Log;
|
||||
EXPECT_NE(child.Log.find(tagged), std::string::npos)
|
||||
<< "the gate fired without naming what it refused; wanted \"" << tagged << "\"; log: " << child.Log;
|
||||
#else
|
||||
(void)needle;
|
||||
(void)body; // no fork on this platform; the verdict here is std::abort()
|
||||
#endif
|
||||
#else
|
||||
const std::string tagged = std::string("ProtocolCorruption ") + needle;
|
||||
const std::string before = ReadLog();
|
||||
body();
|
||||
EXPECT_NE(ReadLog().substr(before.size()).find(tagged), std::string::npos)
|
||||
<< "the gate refused without saying what it refused; wanted \"" << tagged << "\"";
|
||||
#endif
|
||||
}
|
||||
#endif // MOBILEGL_PIPE_PUSH
|
||||
} // namespace
|
||||
|
||||
// See FramebufferEmitTest's twin for why this is a shape pin rather than a placeholder.
|
||||
@@ -74,6 +170,307 @@ TEST(ProgramEmit, TheEmitterIsOneNeverDestroyedProcessSingleton) {
|
||||
#endif
|
||||
}
|
||||
|
||||
// =========================================================================================
|
||||
// The APPLIER's half of the program family (the wire commits'): the shader CSO record, the
|
||||
// three bindings and the default uniform block. The emitter's half - the join at the validate
|
||||
// point, the never-uploaded sentinel that must never be emitted, the composite resolver - is
|
||||
// the client package's and lands beside these.
|
||||
// =========================================================================================
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
namespace {
|
||||
using MG_State::GLState::LinkArtifacts;
|
||||
using MG_State::GLState::SpirvArtifacts;
|
||||
|
||||
MGPProgramDesc ProgramDesc(MGPipeHandle cso, Uint32 stageMask, Uint32 globalUboSize) {
|
||||
MGPProgramDesc desc{};
|
||||
desc.Cso = cso;
|
||||
desc.StageMask = stageMask;
|
||||
desc.GlobalUboSize = globalUboSize;
|
||||
desc.ReservedNumSamplesOffset = 32;
|
||||
desc.SpirvStatus = 1;
|
||||
desc.NativeFloat64 = 1;
|
||||
desc.PointSizeDemoted = 1;
|
||||
desc.EnableSpirvValidation = 1;
|
||||
// ALL SEVEN BLOB REFS ARE DECLARED WITH Size 0 - "this record does not declare its
|
||||
// blob" - which is exactly what a monolith emission is: the artefacts ride beside the
|
||||
// record through the two companion pointers and the codec is never called.
|
||||
return desc;
|
||||
}
|
||||
|
||||
MGPHandleOnly ProgramHandle(MGPipeHandle cso) {
|
||||
return MGPHandleOnly{cso, static_cast<Uint32>(MGPipeKind::ShaderCso), 0};
|
||||
}
|
||||
|
||||
MGPGlobalConstants GlobalConstants(MGPipeHandle cso, Uint32 version) {
|
||||
MGPGlobalConstants record{};
|
||||
record.ShaderCso = cso;
|
||||
record.Version = version;
|
||||
return record;
|
||||
}
|
||||
|
||||
const MGPipeShaderCsoRecord& ProgramRecordOf(Uint32 slot) {
|
||||
EXPECT_GT(MGPipeApplier().ShaderCsos.size(), static_cast<SizeT>(slot));
|
||||
return MGPipeApplier().ShaderCsos[slot];
|
||||
}
|
||||
} // namespace
|
||||
#endif
|
||||
|
||||
// A create starts the record over and leaves Serial at 0; a RE-ISSUE on the same handle is how
|
||||
// a relink travels, and it takes the default uniform block with it - a block sized to a layout
|
||||
// that no longer exists is worse than no block, and the sentinel is the value that says
|
||||
// "nothing has been uploaded for this program".
|
||||
TEST(ProgramEmit, ACreateStoresTheDescriptorAndARelinkCountsUpAndDropsTheBlockKeyedToTheOldLayout) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
const LinkArtifacts link;
|
||||
const SpirvArtifacts spirv;
|
||||
const MGPipeHandle cso{5, 2};
|
||||
const Uint8 block[64] = {};
|
||||
|
||||
MGPipeApplyCreateShaderState(ProgramDesc(cso, 0x3u, 64), &link, &spirv);
|
||||
EXPECT_TRUE(ProgramRecordOf(5).Live);
|
||||
EXPECT_EQ(ProgramRecordOf(5).Gen, 2u);
|
||||
EXPECT_EQ(ProgramRecordOf(5).Serial, 0u) << "a create is not a mutation";
|
||||
EXPECT_EQ(ProgramRecordOf(5).Desc.StageMask, 0x3u);
|
||||
EXPECT_EQ(ProgramRecordOf(5).Desc.GlobalUboSize, 64u);
|
||||
EXPECT_EQ(ProgramRecordOf(5).Desc.ReservedNumSamplesOffset, 32u);
|
||||
EXPECT_EQ(ProgramRecordOf(5).Desc.NativeFloat64, 1u);
|
||||
EXPECT_EQ(ProgramRecordOf(5).GlobalConstantsVersion, ~Uint32{0})
|
||||
<< "a fresh record starts at the never-uploaded sentinel";
|
||||
|
||||
MGPipeApplySetGlobalConstants(GlobalConstants(cso, 7), block);
|
||||
ASSERT_EQ(ProgramRecordOf(5).GlobalConstants.size(), 64u);
|
||||
const Uint64 blockSerial = ProgramRecordOf(5).GlobalConstantsSerial;
|
||||
|
||||
// The relink.
|
||||
MGPipeApplyCreateShaderState(ProgramDesc(cso, 0x7u, 32), &link, &spirv);
|
||||
EXPECT_EQ(ProgramRecordOf(5).Serial, 1u);
|
||||
EXPECT_EQ(ProgramRecordOf(5).Desc.StageMask, 0x7u);
|
||||
EXPECT_TRUE(ProgramRecordOf(5).GlobalConstants.empty())
|
||||
<< "a block sized to the layout the relink replaced survived it";
|
||||
EXPECT_EQ(ProgramRecordOf(5).GlobalConstantsVersion, ~Uint32{0});
|
||||
EXPECT_GT(ProgramRecordOf(5).GlobalConstantsSerial, blockSerial)
|
||||
<< "the clearing was not announced, so a twin can still match what it uploaded before";
|
||||
|
||||
// A RECYCLED SLOT STARTS OVER: inheriting one field of the previous occupant is how a
|
||||
// program at a recycled slot inherits its predecessor's reflection.
|
||||
MGPipeApplyCreateShaderState(ProgramDesc(MGPipeHandle{5, 3}, 0x1u, 16), &link, &spirv);
|
||||
EXPECT_EQ(ProgramRecordOf(5).Gen, 3u);
|
||||
EXPECT_EQ(ProgramRecordOf(5).Serial, 0u) << "a recycled slot kept its predecessor's serial";
|
||||
EXPECT_EQ(ProgramRecordOf(5).Desc.StageMask, 0x1u);
|
||||
#endif
|
||||
}
|
||||
|
||||
// The three refusals a create can produce: no artefacts at all behind seven undeclared blobs, a
|
||||
// default uniform block no program can have, and a slot outside the record table's bound.
|
||||
TEST(ProgramEmit, ACreateWithNoArtefactsAnOversizedBlockOrACorruptSlotIsRefusedNamingTheProgram) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
const LinkArtifacts link;
|
||||
const SpirvArtifacts spirv;
|
||||
|
||||
const MGPProgramDesc desc = ProgramDesc(MGPipeHandle{4, 1}, 0x3u, 0);
|
||||
ExpectRefusedNaming("create_shader_state {slot=4, gen=1}: the record declares no blobs and carries no "
|
||||
"artefacts",
|
||||
[&desc, &spirv]() { MGPipeApplyCreateShaderState(desc, nullptr, &spirv); });
|
||||
ExpectRefusedNaming("create_shader_state {slot=4, gen=1}: the record declares no blobs and carries no "
|
||||
"artefacts",
|
||||
[&desc, &link]() { MGPipeApplyCreateShaderState(desc, &link, nullptr); });
|
||||
EXPECT_TRUE(MGPipeApplier().ShaderCsos.empty());
|
||||
|
||||
const MGPProgramDesc huge = ProgramDesc(MGPipeHandle{4, 1}, 0x3u, kMGPipeMaxGlobalConstantsBytes + 1);
|
||||
ExpectRefusedNaming("create_shader_state {slot=4, gen=1}: the default uniform block is larger than any "
|
||||
"program may declare",
|
||||
[&huge, &link, &spirv]() { MGPipeApplyCreateShaderState(huge, &link, &spirv); });
|
||||
EXPECT_TRUE(MGPipeApplier().ShaderCsos.empty()) << "the table was grown by a refused record";
|
||||
|
||||
const MGPProgramDesc pastTheBound = ProgramDesc(MGPipeHandle{kMGPipeMaxShaderCsoSlots, 1}, 0x3u, 0);
|
||||
ExpectRefusedNaming("create_shader_state {slot=1048576, gen=1}: the slot is outside the record table's "
|
||||
"bound",
|
||||
[&pastTheBound, &link, &spirv]() {
|
||||
MGPipeApplyCreateShaderState(pastTheBound, &link, &spirv);
|
||||
});
|
||||
EXPECT_TRUE(MGPipeApplier().ShaderCsos.empty());
|
||||
EXPECT_TRUE(MGPipeApplier().CompositeShaderCsos.empty());
|
||||
#endif
|
||||
}
|
||||
|
||||
// Three bindings, one serial, and each of them follows its OWN handle: set_draw_program and
|
||||
// set_dispatch_program are two calls because the frontend has two joins. A null handle is legal
|
||||
// and means "nothing bound"; a dead one leaves the previous binding standing and is counted.
|
||||
TEST(ProgramEmit, TheThreeBindingsFollowTheirOwnHandleAndADeadOneLeavesThePreviousBindingStanding) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
const LinkArtifacts link;
|
||||
const SpirvArtifacts spirv;
|
||||
const MGPipeHandle draw{2, 1};
|
||||
const MGPipeHandle dispatch{3, 1};
|
||||
MGPipeApplyCreateShaderState(ProgramDesc(draw, 0x3u, 0), &link, &spirv);
|
||||
MGPipeApplyCreateShaderState(ProgramDesc(dispatch, 0x20u, 0), &link, &spirv);
|
||||
|
||||
const Uint64 serialBefore = MGPipeApplier().ProgramBindingSerial;
|
||||
MGPipeApplyBindShaderState(ProgramHandle(draw));
|
||||
MGPipeApplySetDrawProgram(ProgramHandle(draw));
|
||||
MGPipeApplySetDispatchProgram(ProgramHandle(dispatch));
|
||||
EXPECT_EQ(MGPipeApplier().BoundShaderCso, draw);
|
||||
EXPECT_EQ(MGPipeApplier().DrawProgram, draw);
|
||||
EXPECT_EQ(MGPipeApplier().DispatchProgram, dispatch);
|
||||
EXPECT_EQ(MGPipeApplier().ProgramBindingSerial, serialBefore + 3);
|
||||
|
||||
// A dead handle: previous binding untouched, and COUNTED - a no-op nobody can see is a
|
||||
// dropped bind nobody can see.
|
||||
const Uint64 refusedBefore = MGPipeApplier().RefusedObjectCalls;
|
||||
MGPipeApplySetDrawProgram(ProgramHandle(MGPipeHandle{2, 9}));
|
||||
MGPipeApplyBindShaderState(ProgramHandle(MGPipeHandle{99, 1}));
|
||||
MGPipeApplySetDispatchProgram(ProgramHandle(MGPipeHandle{3, 9}));
|
||||
EXPECT_EQ(MGPipeApplier().RefusedObjectCalls, refusedBefore + 3);
|
||||
EXPECT_EQ(MGPipeApplier().DrawProgram, draw);
|
||||
EXPECT_EQ(MGPipeApplier().BoundShaderCso, draw);
|
||||
EXPECT_EQ(MGPipeApplier().DispatchProgram, dispatch);
|
||||
EXPECT_EQ(MGPipeApplier().ProgramBindingSerial, serialBefore + 3) << "a refused bind moved the serial";
|
||||
|
||||
// The null handle is a state, not an error.
|
||||
MGPipeApplySetDrawProgram(ProgramHandle(kMGPipeNullHandle));
|
||||
EXPECT_TRUE(MGPipeHandleIsNull(MGPipeApplier().DrawProgram));
|
||||
EXPECT_EQ(MGPipeApplier().ProgramBindingSerial, serialBefore + 4);
|
||||
EXPECT_EQ(MGPipeApplier().RefusedObjectCalls, refusedBefore + 3) << "a null bind was counted as a refusal";
|
||||
#endif
|
||||
}
|
||||
|
||||
// A delete drops the record whole, keeps the generation, and clears EVERY binding that named
|
||||
// it - unlike the unit sets, which are "the last set as received". A binding left pointing at a
|
||||
// dropped record would make the next verb refuse a state the applier itself created.
|
||||
TEST(ProgramEmit, ADeleteDropsTheRecordAndClearsEveryBindingThatNamedIt) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
const LinkArtifacts link;
|
||||
const SpirvArtifacts spirv;
|
||||
const MGPipeHandle cso{6, 4};
|
||||
MGPipeApplyCreateShaderState(ProgramDesc(cso, 0x3u, 0), &link, &spirv);
|
||||
MGPipeApplyBindShaderState(ProgramHandle(cso));
|
||||
MGPipeApplySetDrawProgram(ProgramHandle(cso));
|
||||
MGPipeApplySetDispatchProgram(ProgramHandle(cso));
|
||||
const Uint64 serialBefore = MGPipeApplier().ProgramBindingSerial;
|
||||
|
||||
MGPipeApplyDeleteShaderState(ProgramHandle(cso));
|
||||
EXPECT_FALSE(ProgramRecordOf(6).Live);
|
||||
EXPECT_EQ(ProgramRecordOf(6).Gen, 4u) << "a destroy keeps the generation";
|
||||
EXPECT_EQ(ProgramRecordOf(6).Desc.StageMask, 0u)
|
||||
<< "a stale read of a deleted slot must find nothing, not the program that used to be there";
|
||||
EXPECT_TRUE(MGPipeHandleIsNull(MGPipeApplier().BoundShaderCso));
|
||||
EXPECT_TRUE(MGPipeHandleIsNull(MGPipeApplier().DrawProgram));
|
||||
EXPECT_TRUE(MGPipeHandleIsNull(MGPipeApplier().DispatchProgram));
|
||||
EXPECT_GT(MGPipeApplier().ProgramBindingSerial, serialBefore);
|
||||
|
||||
// THE SECOND NOTICE IS A REFUSED NO-OP. A composite's slot has two independent release
|
||||
// paths and both arrive here; the second finding nothing is what makes the double free
|
||||
// proven rather than assumed.
|
||||
const Uint64 serialAfter = MGPipeApplier().ProgramBindingSerial;
|
||||
MGPipeApplyDeleteShaderState(ProgramHandle(cso));
|
||||
EXPECT_EQ(MGPipeApplier().RefusedObjectCalls, 1u);
|
||||
EXPECT_EQ(MGPipeApplier().ProgramBindingSerial, serialAfter);
|
||||
#endif
|
||||
}
|
||||
|
||||
// The default uniform block lands on the PROGRAM's record - it is (ShaderCso, Version) keyed
|
||||
// and belongs to the program, not to the context that uploaded it - and the length it is held
|
||||
// to is the program's own GlobalUboSize, which the create already bounded.
|
||||
TEST(ProgramEmit, TheDefaultUniformBlockLandsOnTheProgramsRecordAndTheSentinelIsRefused) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
const LinkArtifacts link;
|
||||
const SpirvArtifacts spirv;
|
||||
const MGPipeHandle cso{7, 1};
|
||||
MGPipeApplyCreateShaderState(ProgramDesc(cso, 0x3u, 8), &link, &spirv);
|
||||
|
||||
Uint8 block[8] = {1, 2, 3, 4, 5, 6, 7, 8};
|
||||
MGPipeApplySetGlobalConstants(GlobalConstants(cso, 11), block);
|
||||
ASSERT_EQ(ProgramRecordOf(7).GlobalConstants.size(), 8u);
|
||||
EXPECT_EQ(ProgramRecordOf(7).GlobalConstants[7], 8u);
|
||||
EXPECT_EQ(ProgramRecordOf(7).GlobalConstantsVersion, 11u);
|
||||
EXPECT_EQ(ProgramRecordOf(7).GlobalConstantsSerial, 1u);
|
||||
EXPECT_EQ(ProgramRecordOf(7).Serial, 0u) << "a block upload is not a relink";
|
||||
|
||||
// A DECLARED blob length that agrees is fine; one that does not is refused, and so is the
|
||||
// sentinel the backends read as "never uploaded".
|
||||
MGPGlobalConstants declared = GlobalConstants(cso, 12);
|
||||
declared.Blob.Size = 8;
|
||||
MGPipeApplySetGlobalConstants(declared, block);
|
||||
EXPECT_EQ(ProgramRecordOf(7).GlobalConstantsVersion, 12u);
|
||||
|
||||
MGPGlobalConstants lying = GlobalConstants(cso, 13);
|
||||
lying.Blob.Size = 9;
|
||||
ExpectRefusedNaming("set_global_constants {slot=7, gen=1}: the declared blob length is not the "
|
||||
"program's own default uniform block size",
|
||||
[&lying, &block]() { MGPipeApplySetGlobalConstants(lying, block); });
|
||||
|
||||
const MGPGlobalConstants sentinel = GlobalConstants(cso, ~Uint32{0});
|
||||
ExpectRefusedNaming("set_global_constants {slot=7, gen=1}: the version is the backends' "
|
||||
"never-uploaded sentinel",
|
||||
[&sentinel, &block]() { MGPipeApplySetGlobalConstants(sentinel, block); });
|
||||
|
||||
const MGPGlobalConstants noBytes = GlobalConstants(cso, 14);
|
||||
ExpectRefusedNaming("set_global_constants {slot=7, gen=1}: a non-empty block carries no bytes",
|
||||
[&noBytes]() { MGPipeApplySetGlobalConstants(noBytes, nullptr); });
|
||||
|
||||
EXPECT_EQ(ProgramRecordOf(7).GlobalConstantsVersion, 12u) << "a refused block was stored anyway";
|
||||
EXPECT_EQ(ProgramRecordOf(7).GlobalConstantsSerial, 2u);
|
||||
|
||||
// And a block for a program this applier does not have is the ordinary counted refusal.
|
||||
MGPipeApplySetGlobalConstants(GlobalConstants(MGPipeHandle{7, 2}, 15), block);
|
||||
EXPECT_EQ(MGPipeApplier().RefusedObjectCalls, 1u);
|
||||
#endif
|
||||
}
|
||||
|
||||
// D-J4 for this family: the program record is share-group state and survives a make-current -
|
||||
// re-emitting create_shader_state for a record the applier still holds would move its serial
|
||||
// for nothing - while the three bindings are working state and do not.
|
||||
TEST(ProgramEmit, TheProgramRecordSurvivesAMakeCurrentWhileTheThreeBindingsDoNot) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
const LinkArtifacts link;
|
||||
const SpirvArtifacts spirv;
|
||||
const MGPipeHandle cso{8, 1};
|
||||
const Uint8 block[4] = {9, 9, 9, 9};
|
||||
MGPipeApplyCreateShaderState(ProgramDesc(cso, 0x3u, 4), &link, &spirv);
|
||||
MGPipeApplySetGlobalConstants(GlobalConstants(cso, 21), block);
|
||||
MGPipeApplyBindShaderState(ProgramHandle(cso));
|
||||
MGPipeApplySetDrawProgram(ProgramHandle(cso));
|
||||
const Uint64 bindingSerial = MGPipeApplier().ProgramBindingSerial;
|
||||
|
||||
MGPipeApplierReset();
|
||||
|
||||
ASSERT_TRUE(ProgramRecordOf(8).Live) << "a make-current dropped a share-group program record";
|
||||
EXPECT_EQ(ProgramRecordOf(8).GlobalConstantsVersion, 21u);
|
||||
EXPECT_EQ(ProgramRecordOf(8).GlobalConstants.size(), 4u);
|
||||
EXPECT_TRUE(MGPipeHandleIsNull(MGPipeApplier().BoundShaderCso));
|
||||
EXPECT_TRUE(MGPipeHandleIsNull(MGPipeApplier().DrawProgram));
|
||||
EXPECT_GT(MGPipeApplier().ProgramBindingSerial, bindingSerial)
|
||||
<< "the binding serial was carried over or restarted rather than advanced";
|
||||
|
||||
// The bind that follows the switch still resolves, which is the whole point of the rule.
|
||||
MGPipeApplySetDrawProgram(ProgramHandle(cso));
|
||||
EXPECT_EQ(MGPipeApplier().DrawProgram, cso);
|
||||
EXPECT_EQ(MGPipeApplier().RefusedObjectCalls, 0u);
|
||||
|
||||
MGPipeApplierReleaseObjectRecords();
|
||||
EXPECT_TRUE(MGPipeApplier().ShaderCsos.empty());
|
||||
#endif
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
namespace fs = std::filesystem;
|
||||
const fs::path path =
|
||||
|
||||
@@ -1323,6 +1323,154 @@ namespace {
|
||||
#endif
|
||||
}
|
||||
|
||||
// =====================================================================================
|
||||
// P4a: the four resource entry points now BRANCH ON THE DESCRIPTOR'S TARGET.
|
||||
//
|
||||
// The slot spaces of kinds Buffer, Texture and Renderbuffer are independent - the client
|
||||
// allocator is per kind - so one slot-indexed table would alias three live objects onto one
|
||||
// record. These cases are about the branch and nothing else: which table a call lands in,
|
||||
// that the three do not see each other, and that a target or a kind the catalogue does not
|
||||
// name is refused rather than routed to whichever table came first. The texture family's
|
||||
// own behaviour (parameters, the sub-data validator, the pending-upload set) is in
|
||||
// TextureEmitTest beside the emitter cases it belongs with.
|
||||
// =====================================================================================
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
MGPResourceDesc TargetedDesc(MGPipeHandle res, MGPipeResourceTarget target, Uint32 width, Uint32 glName) {
|
||||
MGPResourceDesc desc = BufferDesc(res, width, glName);
|
||||
desc.Target = static_cast<Uint8>(target);
|
||||
return desc;
|
||||
}
|
||||
|
||||
MGPHandleOnly KindHandle(MGPipeHandle res, MGPipeKind kind) {
|
||||
return MGPHandleOnly{res, static_cast<Uint32>(kind), 0};
|
||||
}
|
||||
#endif
|
||||
|
||||
// ONE SLOT NUMBER, THREE LIVE OBJECTS, THREE RECORDS. This is the case that fails the
|
||||
// instant the applier goes back to one table: every assertion below is about slot 7 being
|
||||
// three different things at once, which is exactly what the client allocator hands out.
|
||||
TEST(ResourceEmit, TheThreeResourceKindsKeepTheirOwnSlotSpaceAndDoNotSeeEachOther) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
const MGPipeHandle shared{7, 3};
|
||||
|
||||
MGPipeApplyResourceCreate(TargetedDesc(shared, MGPipeResourceTarget::Buffer, 0, 11));
|
||||
MGPipeApplyResourceCreate(TargetedDesc(shared, MGPipeResourceTarget::Tex2D, 0, 22));
|
||||
MGPipeApplyResourceCreate(TargetedDesc(shared, MGPipeResourceTarget::Renderbuffer, 0, 33));
|
||||
|
||||
ASSERT_GT(MGPipeApplier().Resources.size(), 7u);
|
||||
ASSERT_GT(MGPipeApplier().TextureResources.size(), 7u);
|
||||
ASSERT_GT(MGPipeApplier().RenderbufferResources.size(), 7u);
|
||||
EXPECT_EQ(MGPipeApplier().Resources[7].Desc.GlNameForDiag, 11u);
|
||||
EXPECT_EQ(MGPipeApplier().TextureResources[7].Desc.GlNameForDiag, 22u);
|
||||
EXPECT_EQ(MGPipeApplier().RenderbufferResources[7].Desc.GlNameForDiag, 33u);
|
||||
|
||||
// A respecify of one of them moves ONE record's serial and one record's extent.
|
||||
MGPipeApplyResourceRespecify(TargetedDesc(shared, MGPipeResourceTarget::Tex2D, 256, 22), nullptr);
|
||||
EXPECT_EQ(MGPipeApplier().TextureResources[7].Desc.Width, 256u);
|
||||
EXPECT_EQ(MGPipeApplier().TextureResources[7].Serial, 1u);
|
||||
EXPECT_EQ(MGPipeApplier().Resources[7].Desc.Width, 0u) << "a texture respecify moved the buffer";
|
||||
EXPECT_EQ(MGPipeApplier().Resources[7].Serial, 0u);
|
||||
EXPECT_EQ(MGPipeApplier().RenderbufferResources[7].Serial, 0u);
|
||||
|
||||
// A renderbuffer restorage is the publication D-D2 asks for: the frontend raises no
|
||||
// version for it, so the emission IS the notice, and the applier holds the new extent.
|
||||
MGPipeApplyResourceRespecify(TargetedDesc(shared, MGPipeResourceTarget::Renderbuffer, 1024, 33),
|
||||
nullptr);
|
||||
EXPECT_EQ(MGPipeApplier().RenderbufferResources[7].Desc.Width, 1024u);
|
||||
EXPECT_EQ(MGPipeApplier().RenderbufferResources[7].Serial, 1u);
|
||||
|
||||
// And a destroy takes the record its KIND names, and only that one.
|
||||
MGPipeApplyResourceDestroy(KindHandle(shared, MGPipeKind::Texture));
|
||||
EXPECT_FALSE(MGPipeApplier().TextureResources[7].Live);
|
||||
EXPECT_EQ(MGPipeApplier().TextureResources[7].Gen, 3u) << "a destroy keeps the generation";
|
||||
EXPECT_TRUE(MGPipeApplier().Resources[7].Live) << "a texture destroy dropped the buffer's record";
|
||||
EXPECT_TRUE(MGPipeApplier().RenderbufferResources[7].Live);
|
||||
EXPECT_EQ(MGPipeApplier().RefusedResourceCalls, 0u);
|
||||
#endif
|
||||
}
|
||||
|
||||
// Neither branch may fall through to a table it was not named. A target or a kind outside
|
||||
// the catalogue would otherwise land in whichever table the code happened to reach first,
|
||||
// and destroy a live object of a kind the call was never about.
|
||||
TEST(ResourceEmit, AResourceTargetOrKindTheCatalogueDoesNotNameIsRefusedRatherThanRouted) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
const MGPipeHandle res{5, 1};
|
||||
|
||||
MGPResourceDesc unnamed = BufferDesc(res, 0, 44);
|
||||
unnamed.Target = static_cast<Uint8>(MGPipeResourceTarget::Count);
|
||||
ExpectRefusedNaming("resource_create {slot=5, gen=1, glName=44}: the descriptor names no resource "
|
||||
"target",
|
||||
[&unnamed]() { MGPipeApplyResourceCreate(unnamed); });
|
||||
EXPECT_TRUE(MGPipeApplier().Resources.empty());
|
||||
EXPECT_TRUE(MGPipeApplier().TextureResources.empty());
|
||||
EXPECT_TRUE(MGPipeApplier().RenderbufferResources.empty());
|
||||
|
||||
ExpectRefusedNaming("resource_respecify {slot=5, gen=1, glName=44}: the descriptor names no "
|
||||
"resource target",
|
||||
[&unnamed]() { MGPipeApplyResourceRespecify(unnamed, nullptr); });
|
||||
|
||||
const MGPHandleOnly wrongKind = KindHandle(res, MGPipeKind::SamplerCso);
|
||||
ExpectRefusedNaming("resource_destroy {slot=5, gen=1}: the handle names no resource kind",
|
||||
[&wrongKind]() { MGPipeApplyResourceDestroy(wrongKind); });
|
||||
EXPECT_EQ(MGPipeApplier().RefusedResourceCalls, 0u)
|
||||
<< "a corrupt record is not a dropped call and must not be counted as one";
|
||||
#endif
|
||||
}
|
||||
|
||||
// A texture's resource calls reach NO backend function pointer, and that is the structural
|
||||
// decision the phase rests on rather than an omission: nothing in the texture family
|
||||
// dispatches at GL-call time today, so the record IS the publication. A spy table that saw
|
||||
// one of them would mean P4a had grown an op-table path nobody designed.
|
||||
TEST(ResourceEmit, NoTextureOrRenderbufferResourceCallReachesTheBackendOpTable) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
g_spy = SpyState{};
|
||||
MGPipeSetResourceOps(&kSpyOps);
|
||||
const MGPipeHandle texture{3, 1};
|
||||
const MGPipeHandle renderbuffer{4, 1};
|
||||
const Uint8 texels[64] = {};
|
||||
|
||||
MGPipeApplyResourceCreate(TargetedDesc(texture, MGPipeResourceTarget::Tex2D, 0, 55));
|
||||
MGPipeApplyResourceRespecify(TargetedDesc(texture, MGPipeResourceTarget::Tex2D, 8, 55), nullptr);
|
||||
MGPipeApplyResourceCreate(TargetedDesc(renderbuffer, MGPipeResourceTarget::Renderbuffer, 0, 66));
|
||||
MGPipeApplyResourceRespecify(TargetedDesc(renderbuffer, MGPipeResourceTarget::Renderbuffer, 8, 66),
|
||||
nullptr);
|
||||
MGPSubData upload{};
|
||||
upload.Res = texture;
|
||||
upload.Target = static_cast<Uint16>(MGPipeResourceTarget::Tex2D);
|
||||
upload.UnionBox = MGPBox{0, 0, 0, 4, 4, 1};
|
||||
MGPipeApplyResourceSubData(upload, texels);
|
||||
MGPipeApplyResourceDestroy(KindHandle(texture, MGPipeKind::Texture));
|
||||
MGPipeApplyResourceDestroy(KindHandle(renderbuffer, MGPipeKind::Renderbuffer));
|
||||
|
||||
EXPECT_EQ(g_spy.Creates, 0u);
|
||||
EXPECT_EQ(g_spy.Respecifies, 0u);
|
||||
EXPECT_EQ(g_spy.SubDatas, 0u);
|
||||
EXPECT_EQ(g_spy.Destroys, 0u);
|
||||
|
||||
// The same five calls on a BUFFER still dispatch, which is what proves the count above
|
||||
// is the branch working rather than the table being uninstalled.
|
||||
const MGPipeHandle buffer{3, 1};
|
||||
MGPipeApplyResourceCreate(BufferDesc(buffer, 0, 77));
|
||||
MGPipeApplyResourceRespecify(BufferDesc(buffer, 64, 77), nullptr);
|
||||
MGPipeApplyResourceSubData(BufferWrite(buffer, 0, 16), texels);
|
||||
MGPipeApplyResourceDestroy(BufferHandle(buffer));
|
||||
EXPECT_EQ(g_spy.Creates, 1u);
|
||||
EXPECT_EQ(g_spy.Respecifies, 1u);
|
||||
EXPECT_EQ(g_spy.SubDatas, 1u);
|
||||
EXPECT_EQ(g_spy.Destroys, 1u);
|
||||
#endif
|
||||
}
|
||||
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
// G2 REQUIRES THE PULL AND PUSH ctest NAME SETS TO BE IDENTICAL, name for name, so a
|
||||
// push-only case cannot be ABSENT from a pull build - it has to be there and SKIP. This
|
||||
|
||||
@@ -35,13 +35,19 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <system_error>
|
||||
|
||||
#if defined(_WIN32)
|
||||
#include <process.h>
|
||||
#define MGTEST_HAVE_FORK 0
|
||||
#else
|
||||
#include <csignal>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
#define MGTEST_HAVE_FORK 1
|
||||
#endif
|
||||
|
||||
#include "Includes.h"
|
||||
@@ -64,6 +70,92 @@ namespace {
|
||||
return static_cast<int>(getpid());
|
||||
#endif
|
||||
}
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
std::string ReadLog() {
|
||||
std::ifstream in(g_logPath, std::ios::binary);
|
||||
std::ostringstream ss;
|
||||
ss << in.rdbuf();
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
// A fresh applier per case, BOTH SCOPES, and it takes both because there are two: a reset
|
||||
// is a make-current and deliberately KEEPS the object records, so a fixture that wants a
|
||||
// genuinely empty applier has to say the other one as well. Every case is its own process
|
||||
// under ctest, so this is belt and braces - but running the binary by hand must give the
|
||||
// same answers as running it under ctest.
|
||||
struct ApplierGuard {
|
||||
ApplierGuard() {
|
||||
MGPipeApplierReset();
|
||||
MGPipeApplierReleaseObjectRecords();
|
||||
}
|
||||
~ApplierGuard() {
|
||||
MGPipeApplierReset();
|
||||
MGPipeApplierReleaseObjectRecords();
|
||||
}
|
||||
};
|
||||
|
||||
#if MGTEST_HAVE_FORK
|
||||
struct ChildResult {
|
||||
int Status = -1;
|
||||
std::string Log;
|
||||
};
|
||||
|
||||
template <class Body>
|
||||
ChildResult RunInChild(Body body) {
|
||||
ChildResult result;
|
||||
std::error_code ec;
|
||||
std::filesystem::remove(g_logPath, ec);
|
||||
std::fflush(nullptr);
|
||||
const pid_t pid = ::fork();
|
||||
if (pid < 0) return result;
|
||||
if (pid == 0) {
|
||||
body();
|
||||
::_exit(0);
|
||||
}
|
||||
int status = 0;
|
||||
if (::waitpid(pid, &status, 0) != pid) return result;
|
||||
result.Status = status;
|
||||
result.Log = ReadLog();
|
||||
return result;
|
||||
}
|
||||
|
||||
Bool DiedOfAbort(const ChildResult& r) { return WIFSIGNALED(r.Status) && WTERMSIG(r.Status) == SIGABRT; }
|
||||
std::string DescribeStatus(const ChildResult& r) {
|
||||
if (r.Status < 0) return "fork/waitpid failed";
|
||||
if (WIFEXITED(r.Status)) return "exited " + std::to_string(WEXITSTATUS(r.Status));
|
||||
if (WIFSIGNALED(r.Status)) return "signal " + std::to_string(WTERMSIG(r.Status));
|
||||
return "status " + std::to_string(r.Status);
|
||||
}
|
||||
#endif // MGTEST_HAVE_FORK
|
||||
|
||||
// Drives a call a trip wire must REFUSE, and asserts the wire NAMED what it refused. The
|
||||
// two arms differ by design: a poison or verify build stops the process, so the drive is a
|
||||
// forked child and the parent reads SIGABRT plus the line out of the log; a shipped push
|
||||
// build logs and carries on from a defined state, so there the line is read back in process
|
||||
// and the caller goes on to assert that nothing moved.
|
||||
template <class Body>
|
||||
void ExpectRefusedNaming(const char* needle, Body body) {
|
||||
#if MOBILEGL_PIPE_POISON || MOBILEGL_PIPE_VERIFY
|
||||
#if MGTEST_HAVE_FORK
|
||||
const std::string tagged = std::string("Fatal{ProtocolCorruption} ") + needle;
|
||||
const ChildResult child = RunInChild(body);
|
||||
EXPECT_TRUE(DiedOfAbort(child)) << DescribeStatus(child) << "; log: " << child.Log;
|
||||
EXPECT_NE(child.Log.find(tagged), std::string::npos)
|
||||
<< "the gate fired without naming what it refused; wanted \"" << tagged << "\"; log: " << child.Log;
|
||||
#else
|
||||
(void)needle;
|
||||
(void)body; // no fork on this platform; the verdict here is std::abort()
|
||||
#endif
|
||||
#else
|
||||
const std::string tagged = std::string("ProtocolCorruption ") + needle;
|
||||
const std::string before = ReadLog();
|
||||
body();
|
||||
EXPECT_NE(ReadLog().substr(before.size()).find(tagged), std::string::npos)
|
||||
<< "the gate refused without saying what it refused; wanted \"" << tagged << "\"";
|
||||
#endif
|
||||
}
|
||||
#endif // MOBILEGL_PIPE_PUSH
|
||||
} // namespace
|
||||
|
||||
// See FramebufferEmitTest's twin for why this is a shape pin rather than a placeholder.
|
||||
@@ -80,6 +172,358 @@ TEST(SamplerEmit, TheEmitterIsOneNeverDestroyedProcessSingleton) {
|
||||
#endif
|
||||
}
|
||||
|
||||
// =========================================================================================
|
||||
// The APPLIER's half of the sampler family (the wire commits'): the CSO record, the
|
||||
// identity-addressed view record and its back-pointer, and the two unit sets. The emitter's
|
||||
// half - the content-addressed 256-entry cache, the canonical zero-initialised copy the hash
|
||||
// and the memcmp run over, the padding that cannot change the hash, borderColorForm crossing -
|
||||
// is the client package's and lands beside these.
|
||||
// =========================================================================================
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
namespace {
|
||||
// Every field carries a value of its own, INCLUDING borderColorForm and all three border
|
||||
// representations: they are always numerically populated, so the value alone cannot say
|
||||
// which driver entry point to use and the form is what does.
|
||||
SamplerParameters SamplerValues(Float lodBias, BorderColorForm form) {
|
||||
SamplerParameters params{};
|
||||
params.wrapS = SamplerWrapMode::ClampToEdge;
|
||||
params.wrapT = SamplerWrapMode::MirroredRepeat;
|
||||
params.minFilter = SamplerFilterMode::Linear;
|
||||
params.magFilter = SamplerFilterMode::Nearest;
|
||||
params.mipmapMode = SamplerMipmapMode::Nearest;
|
||||
params.lodBias = lodBias;
|
||||
params.maxAnisotropy = 4.0f;
|
||||
params.compareMode = SamplerCompareMode::CompareToTexture;
|
||||
params.borderColor = {0.25f, 0.5f, 0.75f, 1.0f};
|
||||
params.borderColorI = {-1, 2, -3, 4};
|
||||
params.borderColorUI = {5u, 6u, 7u, 8u};
|
||||
params.borderColorForm = form;
|
||||
return params;
|
||||
}
|
||||
|
||||
MGPSamplerDesc SamplerDesc(MGPipeHandle cso, Uint64 declaredBlobSize) {
|
||||
MGPSamplerDesc desc{};
|
||||
desc.Cso = cso;
|
||||
desc.Parameters.Size = declaredBlobSize;
|
||||
return desc;
|
||||
}
|
||||
|
||||
MGPSamplerView ViewOf(MGPipeHandle cso, MGPipeHandle texture, Uint16 minLevel) {
|
||||
MGPSamplerView view{};
|
||||
view.Cso = cso;
|
||||
view.Texture = texture;
|
||||
view.InternalFormat = 0x8058u; // GL_RGBA8
|
||||
view.Target = static_cast<Uint8>(MGPipeResourceTarget::Tex2D);
|
||||
view.MinLevel = minLevel;
|
||||
view.NumLevels = 4;
|
||||
view.MinLayer = 0;
|
||||
view.NumLayers = 1;
|
||||
view.Samples = 1;
|
||||
return view;
|
||||
}
|
||||
|
||||
MGPHandleOnly SamplerHandle(MGPipeHandle cso) {
|
||||
return MGPHandleOnly{cso, static_cast<Uint32>(MGPipeKind::SamplerCso), 0};
|
||||
}
|
||||
MGPHandleOnly ViewHandle(MGPipeHandle cso) {
|
||||
return MGPHandleOnly{cso, static_cast<Uint32>(MGPipeKind::SamplerViewCso), 0};
|
||||
}
|
||||
|
||||
MGPResourceDesc TextureDesc(MGPipeHandle res, Uint32 glName) {
|
||||
MGPResourceDesc desc{};
|
||||
desc.Resource = res;
|
||||
desc.Target = static_cast<Uint8>(MGPipeResourceTarget::Tex2D);
|
||||
desc.GlNameForDiag = glName;
|
||||
return desc;
|
||||
}
|
||||
} // namespace
|
||||
#endif
|
||||
|
||||
// A create starts the record over and leaves Serial at 0 - so a fresh backend twin that starts
|
||||
// its own synced serial at 0 agrees without either side publishing anything - while a re-issue
|
||||
// on a LIVE identity counts up, which is how a value change travels on a handle whose
|
||||
// generation moves only on slot reuse.
|
||||
TEST(SamplerEmit, ACreateStoresTheParametersByValueAndAReissueOnALiveIdentityCountsUp) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
const MGPipeHandle cso{6, 2};
|
||||
const SamplerParameters first = SamplerValues(0.5f, BorderColorForm::Int);
|
||||
MGPipeApplyCreateSamplerState(SamplerDesc(cso, 0), &first);
|
||||
|
||||
ASSERT_GT(MGPipeApplier().SamplerCsos.size(), 6u);
|
||||
const MGPipeSamplerCsoRecord& record = MGPipeApplier().SamplerCsos[6];
|
||||
EXPECT_TRUE(record.Live);
|
||||
EXPECT_EQ(record.Gen, 2u);
|
||||
EXPECT_EQ(record.Serial, 0u) << "a create is not a mutation";
|
||||
EXPECT_EQ(record.Params.wrapT, SamplerWrapMode::MirroredRepeat);
|
||||
EXPECT_EQ(record.Params.magFilter, SamplerFilterMode::Nearest);
|
||||
EXPECT_EQ(record.Params.compareMode, SamplerCompareMode::CompareToTexture);
|
||||
EXPECT_FLOAT_EQ(record.Params.lodBias, 0.5f);
|
||||
EXPECT_FLOAT_EQ(record.Params.borderColor.z(), 0.75f);
|
||||
EXPECT_EQ(record.Params.borderColorI.x(), -1);
|
||||
EXPECT_EQ(record.Params.borderColorUI.w(), 8u);
|
||||
EXPECT_EQ(record.Params.borderColorForm, BorderColorForm::Int)
|
||||
<< "borderColorForm crosses; without it the backend cannot choose an entry point";
|
||||
|
||||
const SamplerParameters second = SamplerValues(1.5f, BorderColorForm::Uint);
|
||||
MGPipeApplyCreateSamplerState(SamplerDesc(cso, sizeof(SamplerParameters)), &second);
|
||||
EXPECT_EQ(MGPipeApplier().SamplerCsos[6].Serial, 1u);
|
||||
EXPECT_FLOAT_EQ(MGPipeApplier().SamplerCsos[6].Params.lodBias, 1.5f);
|
||||
EXPECT_EQ(MGPipeApplier().SamplerCsos[6].Params.borderColorForm, BorderColorForm::Uint);
|
||||
|
||||
// A RECYCLED SLOT STARTS OVER. Inheriting one field of the previous occupant - a serial, a
|
||||
// filter - is precisely how a sampler at a recycled slot inherits its predecessor's state.
|
||||
const SamplerParameters third = SamplerValues(2.5f, BorderColorForm::Float);
|
||||
MGPipeApplyCreateSamplerState(SamplerDesc(MGPipeHandle{6, 3}, 0), &third);
|
||||
EXPECT_EQ(MGPipeApplier().SamplerCsos[6].Gen, 3u);
|
||||
EXPECT_EQ(MGPipeApplier().SamplerCsos[6].Serial, 0u) << "a recycled slot kept its predecessor's serial";
|
||||
EXPECT_FLOAT_EQ(MGPipeApplier().SamplerCsos[6].Params.lodBias, 2.5f);
|
||||
#endif
|
||||
}
|
||||
|
||||
// The one Blob rule, on this family's own blob: a non-zero declared length must be exactly one
|
||||
// SamplerParameters, a zero means "this record does not declare its blob" - which is what a
|
||||
// monolith emission is - and either way the bytes read are bounded by the TYPE.
|
||||
TEST(SamplerEmit, ARecordThatDoesNotDescribeItsOwnParametersIsRefusedNamingTheLength) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
const SamplerParameters values = SamplerValues(0.0f, BorderColorForm::Float);
|
||||
|
||||
const MGPSamplerDesc lying = SamplerDesc(MGPipeHandle{4, 1}, sizeof(SamplerParameters) + 1);
|
||||
ExpectRefusedNaming("create_sampler_state {slot=4, gen=1}: the declared blob length is not one "
|
||||
"SamplerParameters",
|
||||
[&lying, &values]() { MGPipeApplyCreateSamplerState(lying, &values); });
|
||||
EXPECT_TRUE(MGPipeApplier().SamplerCsos.empty());
|
||||
|
||||
const MGPSamplerDesc undeclared = SamplerDesc(MGPipeHandle{4, 1}, 0);
|
||||
ExpectRefusedNaming("create_sampler_state {slot=4, gen=1}: the record declares no parameters and "
|
||||
"carries none",
|
||||
[&undeclared]() { MGPipeApplyCreateSamplerState(undeclared, nullptr); });
|
||||
EXPECT_TRUE(MGPipeApplier().SamplerCsos.empty());
|
||||
|
||||
// And the slot bound, which is the one number in the family that reaches an allocator.
|
||||
const MGPSamplerDesc pastTheBound = SamplerDesc(MGPipeHandle{kMGPipeMaxSamplerCsoSlots, 1}, 0);
|
||||
ExpectRefusedNaming("create_sampler_state {slot=65536, gen=1}: the slot is outside the record table's "
|
||||
"bound",
|
||||
[&pastTheBound, &values]() { MGPipeApplyCreateSamplerState(pastTheBound, &values); });
|
||||
EXPECT_TRUE(MGPipeApplier().SamplerCsos.empty()) << "the table was grown by a corrupt slot";
|
||||
#endif
|
||||
}
|
||||
|
||||
// A death notice on a record the applier does not have is the ONE refusal a legal sequence
|
||||
// produces - the teardown order - so it stays a defined no-op, and it is COUNTED because
|
||||
// MOBILEGL_ASSERT compiles out at INFO and every build that matters is one.
|
||||
TEST(SamplerEmit, ADeleteDropsTheRecordAndAStaleNoticeIsCountedRatherThanSilentlyDropped) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
const MGPipeHandle cso{3, 7};
|
||||
const SamplerParameters values = SamplerValues(0.0f, BorderColorForm::Float);
|
||||
MGPipeApplyCreateSamplerState(SamplerDesc(cso, 0), &values);
|
||||
ASSERT_TRUE(MGPipeApplier().SamplerCsos[3].Live);
|
||||
|
||||
MGPipeApplyDeleteSamplerState(SamplerHandle(cso));
|
||||
EXPECT_FALSE(MGPipeApplier().SamplerCsos[3].Live);
|
||||
EXPECT_EQ(MGPipeApplier().SamplerCsos[3].Gen, 7u) << "a destroy keeps the generation";
|
||||
EXPECT_EQ(MGPipeApplier().SamplerCsos[3].Params.wrapT, SamplerWrapMode::Repeat)
|
||||
<< "a stale read of a deleted slot must find nothing, not the state that used to be there";
|
||||
EXPECT_EQ(MGPipeApplier().RefusedObjectCalls, 0u);
|
||||
|
||||
// The second notice - the one a teardown produces - is refused and counted.
|
||||
MGPipeApplyDeleteSamplerState(SamplerHandle(cso));
|
||||
EXPECT_EQ(MGPipeApplier().RefusedObjectCalls, 1u);
|
||||
MGPipeApplyDeleteSamplerState(SamplerHandle(MGPipeHandle{3, 8}));
|
||||
EXPECT_EQ(MGPipeApplier().RefusedObjectCalls, 2u);
|
||||
#endif
|
||||
}
|
||||
|
||||
// A sampler view is IDENTITY-addressed one per texture object, minted off that object's
|
||||
// lifetime id, so a restriction change is a re-issue on the same handle rather than a new one.
|
||||
// The texture's own back-pointer is written here and cleared by the delete, and both are silent
|
||||
// lookups: the texture bit and the sampler bit are independent, so a view arriving without its
|
||||
// texture is an ordering fact and not a refusal.
|
||||
TEST(SamplerEmit, AViewIsReissuedOnTheSameHandleAndKeepsItsTexturesBackPointerInStep) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
const MGPipeHandle texture{5, 1};
|
||||
const MGPipeHandle view{9, 2};
|
||||
MGPipeApplyResourceCreate(TextureDesc(texture, 42));
|
||||
|
||||
MGPipeApplyCreateSamplerView(ViewOf(view, texture, 0));
|
||||
ASSERT_GT(MGPipeApplier().SamplerViewCsos.size(), 9u);
|
||||
EXPECT_TRUE(MGPipeApplier().SamplerViewCsos[9].Live);
|
||||
EXPECT_EQ(MGPipeApplier().SamplerViewCsos[9].Serial, 0u);
|
||||
EXPECT_EQ(MGPipeApplier().SamplerViewCsos[9].View.InternalFormat, 0x8058u);
|
||||
EXPECT_EQ(MGPipeApplier().SamplerViewCsos[9].View.NumLevels, 4u);
|
||||
EXPECT_EQ(MGPipeApplier().TextureResources[5].ViewCso, view)
|
||||
<< "the texture's back-pointer to its one view was not written";
|
||||
|
||||
// A restriction change: same handle, serial up, nothing started over.
|
||||
MGPipeApplyCreateSamplerView(ViewOf(view, texture, 2));
|
||||
EXPECT_EQ(MGPipeApplier().SamplerViewCsos[9].Serial, 1u);
|
||||
EXPECT_EQ(MGPipeApplier().SamplerViewCsos[9].View.MinLevel, 2u);
|
||||
|
||||
// A view whose texture this applier has not been told about is stored anyway - refusing it
|
||||
// would make one legal A/B arm drop every view - and it counts no refusal.
|
||||
MGPipeApplyCreateSamplerView(ViewOf(MGPipeHandle{10, 1}, MGPipeHandle{77, 1}, 0));
|
||||
EXPECT_TRUE(MGPipeApplier().SamplerViewCsos[10].Live);
|
||||
EXPECT_EQ(MGPipeApplier().RefusedObjectCalls, 0u);
|
||||
|
||||
MGPipeApplyDeleteSamplerView(ViewHandle(view));
|
||||
EXPECT_FALSE(MGPipeApplier().SamplerViewCsos[9].Live);
|
||||
EXPECT_EQ(MGPipeApplier().SamplerViewCsos[9].Gen, 2u);
|
||||
EXPECT_EQ(MGPipeApplier().TextureResources[5].ViewCso, kMGPipeNullHandle)
|
||||
<< "the texture kept a back-pointer to a view that is gone";
|
||||
MGPipeApplyDeleteSamplerView(ViewHandle(view));
|
||||
EXPECT_EQ(MGPipeApplier().RefusedObjectCalls, 1u);
|
||||
#endif
|
||||
}
|
||||
|
||||
// THE WINDOW IS THE BOUND AND ENTRIES OUTSIDE IT ARE NOT CLEARED: a set that names four units
|
||||
// has said nothing about the other 188, and clearing them would unbind textures the client
|
||||
// never mentioned. Deleting the entry loop, the window gate or either serial bump leaves this
|
||||
// red.
|
||||
TEST(SamplerEmit, TheTwoUnitSetsLandInTheirWindowAndLeaveEverythingOutsideItAlone) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
|
||||
MGPBoundView views[2] = {};
|
||||
views[0].View = MGPipeHandle{1, 1};
|
||||
views[0].Texture = MGPipeHandle{2, 1};
|
||||
views[0].Unit = 4;
|
||||
views[1].View = kMGPipeNullHandle; // a unit the program does not resolve is legal
|
||||
views[1].Texture = MGPipeHandle{3, 1};
|
||||
views[1].Unit = 5;
|
||||
MGPSamplerViews viewHeader{};
|
||||
viewHeader.Start = 4;
|
||||
viewHeader.Count = 2;
|
||||
viewHeader.ContentHash = 0xABCDu;
|
||||
|
||||
const Uint64 viewSerial = MGPipeApplier().SamplerViewsSerial;
|
||||
MGPipeApplySetSamplerViews(viewHeader, views);
|
||||
EXPECT_EQ(MGPipeApplier().SamplerViewStart, 4u);
|
||||
EXPECT_EQ(MGPipeApplier().SamplerViewCount, 2u);
|
||||
EXPECT_EQ(MGPipeApplier().BoundSamplerViews[4].View, (MGPipeHandle{1, 1}));
|
||||
EXPECT_EQ(MGPipeApplier().BoundSamplerViews[5].Texture, (MGPipeHandle{3, 1}));
|
||||
EXPECT_TRUE(MGPipeHandleIsNull(MGPipeApplier().BoundSamplerViews[5].View));
|
||||
EXPECT_TRUE(MGPipeHandleIsNull(MGPipeApplier().BoundSamplerViews[3].View)) << "the set wrote below its window";
|
||||
EXPECT_TRUE(MGPipeHandleIsNull(MGPipeApplier().BoundSamplerViews[6].View)) << "the set wrote above its window";
|
||||
EXPECT_GT(MGPipeApplier().SamplerViewsSerial, viewSerial);
|
||||
|
||||
MGPipeHandle states[2] = {MGPipeHandle{8, 1}, kMGPipeNullHandle};
|
||||
MGPSamplerStates stateHeader{};
|
||||
stateHeader.Start = 4;
|
||||
stateHeader.Count = 2;
|
||||
const Uint64 stateSerial = MGPipeApplier().SamplerStatesSerial;
|
||||
MGPipeApplyBindSamplerStates(stateHeader, states);
|
||||
EXPECT_EQ(MGPipeApplier().BoundSamplerStates[4], (MGPipeHandle{8, 1}));
|
||||
EXPECT_TRUE(MGPipeHandleIsNull(MGPipeApplier().BoundSamplerStates[5]))
|
||||
<< "a unit with no sampler object carries a null CSO and the texture's built-in one applies";
|
||||
EXPECT_EQ(MGPipeApplier().SamplerStateCount, 2u);
|
||||
EXPECT_GT(MGPipeApplier().SamplerStatesSerial, stateSerial);
|
||||
EXPECT_EQ(MGPipeApplier().SamplerViewsSerial, viewSerial + 1)
|
||||
<< "one set moved the other set's serial; the three are independent";
|
||||
|
||||
// A NARROWER SET DOES NOT CLEAR WHAT IT DOES NOT NAME - "the last set as received".
|
||||
MGPSamplerViews narrow{};
|
||||
narrow.Start = 4;
|
||||
narrow.Count = 1;
|
||||
MGPipeApplySetSamplerViews(narrow, views);
|
||||
EXPECT_EQ(MGPipeApplier().SamplerViewCount, 1u);
|
||||
EXPECT_EQ(MGPipeApplier().BoundSamplerViews[5].Texture, (MGPipeHandle{3, 1}))
|
||||
<< "the entry outside the new window was cleared";
|
||||
#endif
|
||||
}
|
||||
|
||||
// The window gate itself, at the bound and one past it, plus the null-tail arm. A header that
|
||||
// describes more than its destination can hold is the same class of fault as a blob outside
|
||||
// its segment, and the destination here is the merged 192-unit space.
|
||||
TEST(SamplerEmit, AUnitWindowPastTheMergedUnitSpaceIsRefusedRatherThanTruncated) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
MGPBoundView entry{};
|
||||
entry.Texture = MGPipeHandle{2, 1};
|
||||
|
||||
// The positive control: a window ending EXACTLY at the bound is fine.
|
||||
MGPSamplerViews exact{};
|
||||
exact.Start = kMGPipeMaxTextureUnits - 1;
|
||||
exact.Count = 1;
|
||||
MGPipeApplySetSamplerViews(exact, &entry);
|
||||
ASSERT_EQ(MGPipeApplier().SamplerViewCount, 1u);
|
||||
const Uint64 serialBefore = MGPipeApplier().SamplerViewsSerial;
|
||||
|
||||
MGPSamplerViews past{};
|
||||
past.Start = kMGPipeMaxTextureUnits - 1;
|
||||
past.Count = 2;
|
||||
past.ContentHash = 7;
|
||||
ExpectRefusedNaming("set_sampler_views {start=191, count=2, hash=7}: the window runs past the merged "
|
||||
"texture-unit space",
|
||||
[&past, &entry]() { MGPipeApplySetSamplerViews(past, &entry); });
|
||||
|
||||
MGPSamplerStates statesPast{};
|
||||
statesPast.Start = 0;
|
||||
statesPast.Count = kMGPipeMaxTextureUnits + 1;
|
||||
ExpectRefusedNaming("bind_sampler_states {start=0, count=193, hash=0}: the window runs past the merged "
|
||||
"texture-unit space",
|
||||
[&statesPast]() {
|
||||
MGPipeHandle one = kMGPipeNullHandle;
|
||||
MGPipeApplyBindSamplerStates(statesPast, &one);
|
||||
});
|
||||
|
||||
MGPSamplerViews noTail{};
|
||||
noTail.Start = 0;
|
||||
noTail.Count = 3;
|
||||
ExpectRefusedNaming("set_sampler_views {start=0, count=3, hash=0}: a non-empty set carries no entries",
|
||||
[&noTail]() { MGPipeApplySetSamplerViews(noTail, nullptr); });
|
||||
|
||||
EXPECT_EQ(MGPipeApplier().SamplerViewsSerial, serialBefore) << "a refused set moved the serial";
|
||||
EXPECT_EQ(MGPipeApplier().SamplerViewStart, kMGPipeMaxTextureUnits - 1);
|
||||
#endif
|
||||
}
|
||||
|
||||
// D-J4 for this family: the CSO and the view are OBJECT records and survive a make-current;
|
||||
// the two unit sets are WORKING state and do not, and their serials advance rather than
|
||||
// restarting.
|
||||
TEST(SamplerEmit, AMakeCurrentTakesTheUnitSetsAndLeavesTheCsoAndViewRecordsStanding) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
const SamplerParameters values = SamplerValues(3.0f, BorderColorForm::Float);
|
||||
MGPipeApplyCreateSamplerState(SamplerDesc(MGPipeHandle{2, 1}, 0), &values);
|
||||
MGPipeApplyCreateSamplerView(ViewOf(MGPipeHandle{3, 1}, MGPipeHandle{4, 1}, 1));
|
||||
MGPBoundView entry{};
|
||||
entry.Texture = MGPipeHandle{4, 1};
|
||||
MGPSamplerViews header{};
|
||||
header.Count = 1;
|
||||
MGPipeApplySetSamplerViews(header, &entry);
|
||||
const Uint64 viewsSerial = MGPipeApplier().SamplerViewsSerial;
|
||||
const Uint64 statesSerial = MGPipeApplier().SamplerStatesSerial;
|
||||
|
||||
MGPipeApplierReset();
|
||||
|
||||
EXPECT_TRUE(MGPipeApplier().SamplerCsos[2].Live) << "a make-current dropped a share-group CSO record";
|
||||
EXPECT_FLOAT_EQ(MGPipeApplier().SamplerCsos[2].Params.lodBias, 3.0f);
|
||||
EXPECT_TRUE(MGPipeApplier().SamplerViewCsos[3].Live);
|
||||
EXPECT_EQ(MGPipeApplier().SamplerViewCount, 0u) << "the unit set is per context and must be cleared";
|
||||
EXPECT_TRUE(MGPipeHandleIsNull(MGPipeApplier().BoundSamplerViews[0].Texture));
|
||||
EXPECT_GT(MGPipeApplier().SamplerViewsSerial, viewsSerial);
|
||||
EXPECT_GT(MGPipeApplier().SamplerStatesSerial, statesSerial);
|
||||
#endif
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
namespace fs = std::filesystem;
|
||||
const fs::path path =
|
||||
|
||||
@@ -39,13 +39,19 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <system_error>
|
||||
|
||||
#if defined(_WIN32)
|
||||
#include <process.h>
|
||||
#define MGTEST_HAVE_FORK 0
|
||||
#else
|
||||
#include <csignal>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
#define MGTEST_HAVE_FORK 1
|
||||
#endif
|
||||
|
||||
#include "Includes.h"
|
||||
@@ -68,6 +74,92 @@ namespace {
|
||||
return static_cast<int>(getpid());
|
||||
#endif
|
||||
}
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
std::string ReadLog() {
|
||||
std::ifstream in(g_logPath, std::ios::binary);
|
||||
std::ostringstream ss;
|
||||
ss << in.rdbuf();
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
// A fresh applier per case, BOTH SCOPES, and it takes both because there are two: a reset
|
||||
// is a make-current and deliberately KEEPS the object records, so a fixture that wants a
|
||||
// genuinely empty applier has to say the other one as well. Every case is its own process
|
||||
// under ctest, so this is belt and braces - but running the binary by hand must give the
|
||||
// same answers as running it under ctest.
|
||||
struct ApplierGuard {
|
||||
ApplierGuard() {
|
||||
MGPipeApplierReset();
|
||||
MGPipeApplierReleaseObjectRecords();
|
||||
}
|
||||
~ApplierGuard() {
|
||||
MGPipeApplierReset();
|
||||
MGPipeApplierReleaseObjectRecords();
|
||||
}
|
||||
};
|
||||
|
||||
#if MGTEST_HAVE_FORK
|
||||
struct ChildResult {
|
||||
int Status = -1;
|
||||
std::string Log;
|
||||
};
|
||||
|
||||
template <class Body>
|
||||
ChildResult RunInChild(Body body) {
|
||||
ChildResult result;
|
||||
std::error_code ec;
|
||||
std::filesystem::remove(g_logPath, ec);
|
||||
std::fflush(nullptr);
|
||||
const pid_t pid = ::fork();
|
||||
if (pid < 0) return result;
|
||||
if (pid == 0) {
|
||||
body();
|
||||
::_exit(0);
|
||||
}
|
||||
int status = 0;
|
||||
if (::waitpid(pid, &status, 0) != pid) return result;
|
||||
result.Status = status;
|
||||
result.Log = ReadLog();
|
||||
return result;
|
||||
}
|
||||
|
||||
Bool DiedOfAbort(const ChildResult& r) { return WIFSIGNALED(r.Status) && WTERMSIG(r.Status) == SIGABRT; }
|
||||
std::string DescribeStatus(const ChildResult& r) {
|
||||
if (r.Status < 0) return "fork/waitpid failed";
|
||||
if (WIFEXITED(r.Status)) return "exited " + std::to_string(WEXITSTATUS(r.Status));
|
||||
if (WIFSIGNALED(r.Status)) return "signal " + std::to_string(WTERMSIG(r.Status));
|
||||
return "status " + std::to_string(r.Status);
|
||||
}
|
||||
#endif // MGTEST_HAVE_FORK
|
||||
|
||||
// Drives a call a trip wire must REFUSE, and asserts the wire NAMED what it refused. The
|
||||
// two arms differ by design: a poison or verify build stops the process, so the drive is a
|
||||
// forked child and the parent reads SIGABRT plus the line out of the log; a shipped push
|
||||
// build logs and carries on from a defined state, so there the line is read back in process
|
||||
// and the caller goes on to assert that nothing moved.
|
||||
template <class Body>
|
||||
void ExpectRefusedNaming(const char* needle, Body body) {
|
||||
#if MOBILEGL_PIPE_POISON || MOBILEGL_PIPE_VERIFY
|
||||
#if MGTEST_HAVE_FORK
|
||||
const std::string tagged = std::string("Fatal{ProtocolCorruption} ") + needle;
|
||||
const ChildResult child = RunInChild(body);
|
||||
EXPECT_TRUE(DiedOfAbort(child)) << DescribeStatus(child) << "; log: " << child.Log;
|
||||
EXPECT_NE(child.Log.find(tagged), std::string::npos)
|
||||
<< "the gate fired without naming what it refused; wanted \"" << tagged << "\"; log: " << child.Log;
|
||||
#else
|
||||
(void)needle;
|
||||
(void)body; // no fork on this platform; the verdict here is std::abort()
|
||||
#endif
|
||||
#else
|
||||
const std::string tagged = std::string("ProtocolCorruption ") + needle;
|
||||
const std::string before = ReadLog();
|
||||
body();
|
||||
EXPECT_NE(ReadLog().substr(before.size()).find(tagged), std::string::npos)
|
||||
<< "the gate refused without saying what it refused; wanted \"" << tagged << "\"";
|
||||
#endif
|
||||
}
|
||||
#endif // MOBILEGL_PIPE_PUSH
|
||||
} // namespace
|
||||
|
||||
// See FramebufferEmitTest's twin for why this is a shape pin rather than a placeholder: a
|
||||
@@ -83,6 +175,325 @@ TEST(TextureEmit, TheEmitterIsOneNeverDestroyedProcessSingleton) {
|
||||
#endif
|
||||
}
|
||||
|
||||
// =========================================================================================
|
||||
// The APPLIER's half of the texture family (the wire commits'): set_texture_params on the
|
||||
// texture's own record, and the sub-data validator plus the pending-upload set that replaces
|
||||
// the frontend dirty flags the client clears at emission. The emitter's half - the descriptor
|
||||
// builder for every target, the sticky bind mask, the drain list, the level-shadow strides -
|
||||
// is the client package's and lands beside these.
|
||||
// =========================================================================================
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
namespace {
|
||||
constexpr Uint16 kTex2D = static_cast<Uint16>(MGPipeResourceTarget::Tex2D);
|
||||
|
||||
MGPResourceDesc TextureDesc(MGPipeHandle res, Uint32 width, Uint32 glName) {
|
||||
MGPResourceDesc desc{};
|
||||
desc.Resource = res;
|
||||
desc.Target = static_cast<Uint8>(MGPipeResourceTarget::Tex2D);
|
||||
desc.Width = width;
|
||||
desc.Height = width;
|
||||
desc.GlNameForDiag = glName;
|
||||
return desc;
|
||||
}
|
||||
|
||||
// Every field carries a value of its own so a body that stored the wrong one is visible BY
|
||||
// FIELD, which is what the family's descriptor-consistency control needs of it.
|
||||
MGPTextureParams TextureParams(MGPipeHandle res, MGPipeHandle builtinSampler, Uint16 baseLevel) {
|
||||
MGPTextureParams params{};
|
||||
params.Res = res;
|
||||
params.BuiltinSampler = builtinSampler;
|
||||
params.BaseLevel = baseLevel;
|
||||
params.MaxLevel = 7;
|
||||
params.Swizzle[0] = 1;
|
||||
params.Swizzle[1] = 2;
|
||||
params.Swizzle[2] = 3;
|
||||
params.Swizzle[3] = 4;
|
||||
params.DepthStencilMode = 5;
|
||||
params.ForceResync = 1;
|
||||
params.SamplerResync = 1;
|
||||
params.MinLod = -2.0f;
|
||||
params.MaxLod = 9.0f;
|
||||
params.LodBias = 0.5f;
|
||||
return params;
|
||||
}
|
||||
|
||||
MGPSubData TextureUpload(MGPipeHandle res, Uint16 level, const MGPBox& box, Uint32 regionCount) {
|
||||
MGPSubData record{};
|
||||
record.Res = res;
|
||||
record.Target = kTex2D;
|
||||
record.Level = level;
|
||||
record.SourceIsVerbatimLevelShadow = 1;
|
||||
record.UnionBox = box;
|
||||
record.RegionCount = regionCount;
|
||||
return record;
|
||||
}
|
||||
|
||||
MGPSubRegion Region(Int32 x, Int32 y, Uint32 w, Uint32 h) {
|
||||
MGPSubRegion region{};
|
||||
region.X = x;
|
||||
region.Y = y;
|
||||
region.Z = 0;
|
||||
region.W = w;
|
||||
region.H = h;
|
||||
region.D = 1;
|
||||
region.SrcOffset = static_cast<Uint64>(y) * 64 + static_cast<Uint64>(x) * 4;
|
||||
region.SrcRowStride = 256;
|
||||
region.SrcSliceStride = 0;
|
||||
return region;
|
||||
}
|
||||
|
||||
const MGPipeResourceRecord& TextureRecordOf(Uint32 slot) {
|
||||
EXPECT_GT(MGPipeApplier().TextureResources.size(), static_cast<SizeT>(slot));
|
||||
return MGPipeApplier().TextureResources[slot];
|
||||
}
|
||||
} // namespace
|
||||
#endif
|
||||
|
||||
// set_texture_params IS ADDRESSED BY RESOURCE AND BY NOTHING ELSE, which is the whole reason
|
||||
// the call exists: a texture that is only an FBO attachment, only an image-unit binding or
|
||||
// only a glCopyImageSubData endpoint has no sampler view to hang its parameters on. Deleting
|
||||
// the store or the ParamsSerial bump leaves this red.
|
||||
TEST(TextureEmit, ATexturesParametersLandOnItsOwnRecordAndMoveOnlyTheirOwnSerial) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
const MGPipeHandle texture{7, 3};
|
||||
const MGPipeHandle sampler{2, 1};
|
||||
MGPipeApplyResourceCreate(TextureDesc(texture, 0, 41));
|
||||
MGPipeApplyResourceRespecify(TextureDesc(texture, 64, 41), nullptr);
|
||||
ASSERT_EQ(TextureRecordOf(7).ParamsSerial, 0u) << "a create and a respecify are not a parameter push";
|
||||
|
||||
MGPipeApplySetTextureParams(TextureParams(texture, sampler, 2));
|
||||
|
||||
const MGPipeResourceRecord& record = TextureRecordOf(7);
|
||||
EXPECT_EQ(record.Params.BuiltinSampler, sampler);
|
||||
EXPECT_EQ(record.Params.BaseLevel, 2u);
|
||||
EXPECT_EQ(record.Params.MaxLevel, 7u);
|
||||
EXPECT_EQ(record.Params.Swizzle[2], 3u);
|
||||
EXPECT_EQ(record.Params.DepthStencilMode, 5u);
|
||||
EXPECT_EQ(record.Params.ForceResync, 1u);
|
||||
EXPECT_EQ(record.Params.SamplerResync, 1u) << "the second resync bit is carried, not dropped";
|
||||
EXPECT_FLOAT_EQ(record.Params.MinLod, -2.0f);
|
||||
EXPECT_FLOAT_EQ(record.Params.LodBias, 0.5f);
|
||||
EXPECT_EQ(record.ParamsSerial, 1u);
|
||||
EXPECT_EQ(record.Serial, 1u) << "a parameter push is not a storage mutation and must not move Serial";
|
||||
|
||||
MGPipeApplySetTextureParams(TextureParams(texture, sampler, 3));
|
||||
EXPECT_EQ(TextureRecordOf(7).Params.BaseLevel, 3u);
|
||||
EXPECT_EQ(TextureRecordOf(7).ParamsSerial, 2u);
|
||||
|
||||
// A stale generation resolves to nothing: the call is a DEFINED no-op and it is COUNTED,
|
||||
// because MOBILEGL_ASSERT compiles out at INFO and a no-op nobody can see is a dropped
|
||||
// parameter push nobody can see.
|
||||
const Uint64 refusedBefore = MGPipeApplier().RefusedObjectCalls;
|
||||
MGPipeApplySetTextureParams(TextureParams(MGPipeHandle{7, 4}, sampler, 6));
|
||||
EXPECT_EQ(MGPipeApplier().RefusedObjectCalls, refusedBefore + 1);
|
||||
EXPECT_EQ(TextureRecordOf(7).Params.BaseLevel, 3u) << "a stale handle wrote the live record";
|
||||
EXPECT_EQ(TextureRecordOf(7).ParamsSerial, 2u);
|
||||
|
||||
// And a BUFFER of the same slot is not a texture: the two tables are independent, so this
|
||||
// is a refusal rather than a parameter push onto somebody else's record.
|
||||
MGPipeApplySetTextureParams(TextureParams(MGPipeHandle{9, 1}, sampler, 1));
|
||||
EXPECT_EQ(MGPipeApplier().RefusedObjectCalls, refusedBefore + 2);
|
||||
#endif
|
||||
}
|
||||
|
||||
// EVERY ITextureObject OWNS A SamplerObject, so a null built-in sampler CSO is not "no
|
||||
// sampler" - it is a record that would have the backend sample with whatever filter and wrap
|
||||
// state the unit last left behind. It is the corrupt-record verdict rather than the dropped-
|
||||
// call one, so it must NOT be counted as a refusal.
|
||||
TEST(TextureEmit, ARecordWithNoBuiltinSamplerCsoIsRefusedNamingTheTexture) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
const MGPipeHandle texture{6, 2};
|
||||
MGPipeApplyResourceCreate(TextureDesc(texture, 0, 77));
|
||||
MGPipeApplySetTextureParams(TextureParams(texture, MGPipeHandle{2, 1}, 1));
|
||||
ASSERT_EQ(TextureRecordOf(6).ParamsSerial, 1u);
|
||||
const Uint64 refusedBefore = MGPipeApplier().RefusedObjectCalls;
|
||||
|
||||
const MGPTextureParams noSampler = TextureParams(texture, kMGPipeNullHandle, 4);
|
||||
ExpectRefusedNaming("set_texture_params {slot=6, gen=2, glName=77}: the record names no built-in "
|
||||
"sampler CSO",
|
||||
[&noSampler]() { MGPipeApplySetTextureParams(noSampler); });
|
||||
EXPECT_EQ(TextureRecordOf(6).Params.BaseLevel, 1u) << "a refused record was stored anyway";
|
||||
EXPECT_EQ(TextureRecordOf(6).ParamsSerial, 1u);
|
||||
EXPECT_EQ(MGPipeApplier().RefusedObjectCalls, refusedBefore)
|
||||
<< "a corrupt record is not a dropped call and must not be counted as one";
|
||||
#endif
|
||||
}
|
||||
|
||||
// D-D5's safety net. The client clears its own dirty flags AT EMISSION and the backend's
|
||||
// upload loop has bail arms that would otherwise lose exactly those texels, so the emitted
|
||||
// shape accumulates SERVER-SIDE: boxes union, rect lists concatenate, and the moment either
|
||||
// side says "box only" the entry becomes box only - which is the frontend's own model, where
|
||||
// zero rects means "upload the union box instead" and covers every reason at once.
|
||||
TEST(TextureEmit, AnAccumulatedUploadUnionsItsBoxesAndCollapsesToTheBoxWhenARectListCannotDescribeIt) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
const MGPipeHandle texture{4, 1};
|
||||
const Uint8 texels[4096] = {};
|
||||
MGPipeApplyResourceCreate(TextureDesc(texture, 0, 88));
|
||||
MGPipeApplyResourceRespecify(TextureDesc(texture, 64, 88), nullptr);
|
||||
|
||||
const MGPSubRegion first[2] = {Region(0, 0, 4, 4), Region(8, 8, 4, 4)};
|
||||
MGPipeApplyResourceSubData(TextureUpload(texture, 0, MGPBox{0, 0, 0, 12, 12, 1}, 2), texels, first);
|
||||
ASSERT_EQ(TextureRecordOf(4).PendingUploads.size(), 1u);
|
||||
EXPECT_EQ(TextureRecordOf(4).PendingUploads[0].UploadTarget, kTex2D);
|
||||
EXPECT_EQ(TextureRecordOf(4).PendingUploads[0].Level, 0u);
|
||||
EXPECT_EQ(TextureRecordOf(4).PendingUploads[0].UnionBox.W, 12u);
|
||||
ASSERT_EQ(TextureRecordOf(4).PendingUploads[0].Regions.size(), 2u);
|
||||
EXPECT_EQ(TextureRecordOf(4).PendingUploads[0].Regions[1].X, 8);
|
||||
EXPECT_EQ(TextureRecordOf(4).PendingUploads[0].Regions[1].SrcRowStride, 256u)
|
||||
<< "the strides are CARRIED, never inferred: the pointer comparison they replace cannot "
|
||||
"survive a split";
|
||||
EXPECT_EQ(TextureRecordOf(4).Serial, 2u) << "an accepted upload moves the record's serial";
|
||||
|
||||
// A second emission behind a backend bail: the boxes union and the lists concatenate.
|
||||
const MGPSubRegion second[1] = {Region(16, 0, 8, 8)};
|
||||
MGPipeApplyResourceSubData(TextureUpload(texture, 0, MGPBox{16, 0, 0, 8, 8, 1}, 1), texels, second);
|
||||
ASSERT_EQ(TextureRecordOf(4).PendingUploads.size(), 1u) << "the (target, level) key split in two";
|
||||
EXPECT_EQ(TextureRecordOf(4).PendingUploads[0].UnionBox.X, 0);
|
||||
EXPECT_EQ(TextureRecordOf(4).PendingUploads[0].UnionBox.W, 24u);
|
||||
EXPECT_EQ(TextureRecordOf(4).PendingUploads[0].UnionBox.H, 12u);
|
||||
EXPECT_EQ(TextureRecordOf(4).PendingUploads[0].Regions.size(), 3u);
|
||||
|
||||
// A contribution with NO regions means "the box is the whole story", and the accumulated
|
||||
// entry has to say the same thing afterwards or the box would cover texels the rect list
|
||||
// does not name.
|
||||
MGPipeApplyResourceSubData(TextureUpload(texture, 0, MGPBox{0, 0, 0, 64, 64, 1}, 0), texels);
|
||||
ASSERT_EQ(TextureRecordOf(4).PendingUploads.size(), 1u);
|
||||
EXPECT_TRUE(TextureRecordOf(4).PendingUploads[0].Regions.empty())
|
||||
<< "a box-only contribution left a rect list that no longer covers the box";
|
||||
EXPECT_EQ(TextureRecordOf(4).PendingUploads[0].UnionBox.W, 64u);
|
||||
|
||||
// A different level is a different key, and a different upload target would be too.
|
||||
MGPipeApplyResourceSubData(TextureUpload(texture, 3, MGPBox{0, 0, 0, 8, 8, 1}, 0), texels);
|
||||
ASSERT_EQ(TextureRecordOf(4).PendingUploads.size(), 2u);
|
||||
EXPECT_EQ(TextureRecordOf(4).PendingUploads[1].Level, 3u);
|
||||
EXPECT_EQ(TextureRecordOf(4).PendingUploads[0].UnionBox.W, 64u) << "level 3 rewrote level 0's box";
|
||||
#endif
|
||||
}
|
||||
|
||||
// The texture half of the sub-data validator. Each of its four statements is about a record
|
||||
// that would make the server upload texels it was never told about, or read a tail it was not
|
||||
// given; removing any one of them leaves this red.
|
||||
TEST(TextureEmit, TheSubDataValidatorRefusesALevelABoxAndARegionTheRecordCannotDescribe) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
const MGPipeHandle texture{5, 2};
|
||||
const Uint8 texels[4096] = {};
|
||||
MGPipeApplyResourceCreate(TextureDesc(texture, 0, 99));
|
||||
MGPipeApplyResourceRespecify(TextureDesc(texture, 64, 99), nullptr);
|
||||
|
||||
// Positive control: the last legal level, a whole-level box and a region exactly filling
|
||||
// it are all fine, so what follows is refusing the value and not the arithmetic round it.
|
||||
const MGPSubRegion exact[1] = {Region(0, 0, 8, 8)};
|
||||
MGPipeApplyResourceSubData(TextureUpload(texture, 31, MGPBox{0, 0, 0, 8, 8, 1}, 1), texels, exact);
|
||||
ASSERT_EQ(TextureRecordOf(5).PendingUploads.size(), 1u);
|
||||
const Uint64 serialBefore = TextureRecordOf(5).Serial;
|
||||
|
||||
const MGPSubData deepLevel = TextureUpload(texture, 32, MGPBox{0, 0, 0, 8, 8, 1}, 0);
|
||||
ExpectRefusedNaming("resource_subdata {slot=5, gen=2, glName=99}: the level is above the bound any "
|
||||
"texture's storage can have",
|
||||
[&deepLevel, &texels]() { MGPipeApplyResourceSubData(deepLevel, texels); });
|
||||
|
||||
const MGPSubData negative = TextureUpload(texture, 0, MGPBox{-1, 0, 0, 8, 8, 1}, 0);
|
||||
ExpectRefusedNaming("resource_subdata {slot=5, gen=2, glName=99}: the union box has a negative origin "
|
||||
"or runs past the bound one record can encode",
|
||||
[&negative, &texels]() { MGPipeApplyResourceSubData(negative, texels); });
|
||||
|
||||
const MGPSubData missingTail = TextureUpload(texture, 0, MGPBox{0, 0, 0, 8, 8, 1}, 2);
|
||||
ExpectRefusedNaming("resource_subdata {slot=5, gen=2, glName=99}: the record declares sub-regions and "
|
||||
"carries none",
|
||||
[&missingTail, &texels]() { MGPipeApplyResourceSubData(missingTail, texels); });
|
||||
|
||||
// THE ONE INVARIANT THAT MATTERS: the union box IS the union of the regions. The server
|
||||
// picks the upload shape from the pair, so a region outside the box means the box misses
|
||||
// its texels and the region writes where the box never said it would.
|
||||
const MGPSubRegion outside[1] = {Region(16, 0, 4, 4)};
|
||||
const MGPSubData escapes = TextureUpload(texture, 0, MGPBox{0, 0, 0, 8, 8, 1}, 1);
|
||||
ExpectRefusedNaming("resource_subdata {slot=5, gen=2, glName=99}: a sub-region is not inside the union "
|
||||
"box the record declares",
|
||||
[&escapes, &texels, &outside]() {
|
||||
MGPipeApplyResourceSubData(escapes, texels, outside);
|
||||
});
|
||||
|
||||
const MGPSubData nothing = TextureUpload(texture, 0, MGPBox{0, 0, 0, 0, 0, 0}, 0);
|
||||
ExpectRefusedNaming("resource_subdata {slot=5, gen=2, glName=99}: the record describes no texels at all",
|
||||
[¬hing, &texels]() { MGPipeApplyResourceSubData(nothing, texels); });
|
||||
|
||||
EXPECT_EQ(TextureRecordOf(5).PendingUploads.size(), 1u)
|
||||
<< "a refused record was accumulated anyway";
|
||||
EXPECT_EQ(TextureRecordOf(5).Serial, serialBefore) << "not one refusal may move the serial";
|
||||
#endif
|
||||
}
|
||||
|
||||
// A respecify redefines the store, so the boxes and rects that describe the level it replaces
|
||||
// go with it - a box kept across a shrink would have the backend upload past the end of the
|
||||
// new level. Nothing is lost by it: the frontend entry points that respecify a texture re-mark
|
||||
// the levels they define.
|
||||
TEST(TextureEmit, ARespecifyDropsThePendingUploadsAgainstTheStorageItReplaces) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
const MGPipeHandle texture{3, 1};
|
||||
const Uint8 texels[4096] = {};
|
||||
MGPipeApplyResourceCreate(TextureDesc(texture, 0, 55));
|
||||
MGPipeApplyResourceRespecify(TextureDesc(texture, 64, 55), nullptr);
|
||||
MGPipeApplyResourceSubData(TextureUpload(texture, 0, MGPBox{0, 0, 0, 64, 64, 1}, 0), texels);
|
||||
ASSERT_EQ(TextureRecordOf(3).PendingUploads.size(), 1u);
|
||||
|
||||
MGPipeApplyResourceRespecify(TextureDesc(texture, 8, 55), nullptr);
|
||||
EXPECT_TRUE(TextureRecordOf(3).PendingUploads.empty())
|
||||
<< "a 64-wide box survived onto an 8-wide store";
|
||||
EXPECT_EQ(TextureRecordOf(3).Desc.Width, 8u);
|
||||
#endif
|
||||
}
|
||||
|
||||
// D-J4, for the kind that made the rule matter: a TEXTURE lives in a share group exactly as a
|
||||
// buffer does, so its record - and the parameters and the pending uploads that ride on it -
|
||||
// outlives a make-current, and only the applier's own teardown takes it.
|
||||
TEST(TextureEmit, TheTextureRecordAndItsParamsAndPendingUploadsSurviveAMakeCurrent) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
const MGPipeHandle texture{2, 5};
|
||||
const Uint8 texels[4096] = {};
|
||||
MGPipeApplyResourceCreate(TextureDesc(texture, 0, 66));
|
||||
MGPipeApplyResourceRespecify(TextureDesc(texture, 32, 66), nullptr);
|
||||
MGPipeApplySetTextureParams(TextureParams(texture, MGPipeHandle{1, 1}, 2));
|
||||
MGPipeApplyResourceSubData(TextureUpload(texture, 0, MGPBox{0, 0, 0, 32, 32, 1}, 0), texels);
|
||||
|
||||
MGPipeApplierReset(); // the make-current
|
||||
|
||||
ASSERT_TRUE(TextureRecordOf(2).Live) << "a make-current dropped a share-group object's record";
|
||||
EXPECT_EQ(TextureRecordOf(2).Desc.Width, 32u);
|
||||
EXPECT_EQ(TextureRecordOf(2).Params.BaseLevel, 2u);
|
||||
EXPECT_EQ(TextureRecordOf(2).ParamsSerial, 1u);
|
||||
ASSERT_EQ(TextureRecordOf(2).PendingUploads.size(), 1u)
|
||||
<< "the pending uploads are the safety net for a backend bail and cannot be per context";
|
||||
|
||||
// The write that follows the switch still lands, which is the whole point of the rule.
|
||||
MGPipeApplyResourceSubData(TextureUpload(texture, 1, MGPBox{0, 0, 0, 16, 16, 1}, 0), texels);
|
||||
EXPECT_EQ(TextureRecordOf(2).PendingUploads.size(), 2u);
|
||||
EXPECT_EQ(MGPipeApplier().RefusedResourceCalls, 0u);
|
||||
|
||||
// And the teardown scope - the only other thing that clears a record - does take it.
|
||||
MGPipeApplierReleaseObjectRecords();
|
||||
EXPECT_TRUE(MGPipeApplier().TextureResources.empty());
|
||||
#endif
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
namespace fs = std::filesystem;
|
||||
const fs::path path =
|
||||
|
||||
Reference in New Issue
Block a user