mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-14 05:23:23 +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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user