mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-18 09:08:31 +09:00
Two appended wire records replace the two cross-role control bypasses:
applier_reset (opcode 77, MGPApplierReset{Uint64 ContextSerial}, kScreen) is emitted by
the GL thread at the FreshlyPrimed make-current edge, before the client-side resets, so
the record's barrier orders the server's MGPipeApplierReset against every verb that
follows; a direct MGPipeApplierReset() from a non-apply thread with a live session is
Fatal{RoleViolation, "g_applier"} (MGPipeApply.cpp carries the layer-2 guard; the
apply-thread and no-session bring-up shapes keep the direct call verbatim).
object_death (opcode 78, reuses MGPipeHandleOnly, kCtxObject) is the framebuffer
family's first wire delete opcode: the client thread resolves the dying object's handle
in its OWN allocator, emits nothing for a handle the server never saw, and EmitAndWaits
otherwise - the wait preserving the death's ordering against in-flight records that
name the handle, the only property the blocking mailbox hop provided. The sink releases
per-kind twins through a handle-keyed ReleaseTwinByHandle (seven kinds, SamplerViewCso's
idempotent second path kept); a null arriving handle is
Fatal{ProtocolCorruption, "ObjectDeath.Handle"}. A documented NoSession fallback arm
delivers a death to a server-only fixture's loop; monolith keeps the mailbox hop and
the FreshlyPrimed direct call character for character.
Evidence: gen_pipe --check/--self-test and gen_pipe_field_ownership --check/--self-test
green; unit 2155/2155; integration-split 111/111 (107 plus four new CtWireScenario
entries, all genuinely run, each with its own log path and an EXPECT_EXIT death case);
red-once - shorting the emission to the direct call turns the four Ct cases red with
Fatal{RoleViolation, "g_applier"} by name, then restores green verbatim.
CONTRACT-P5C sections 1, 5.1-5.2, 7.
245 lines
12 KiB
C++
245 lines
12 KiB
C++
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/CtWireScenario.cpp
|
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
|
// Licensed under the GNU Lesser General Public License v3.0:
|
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
|
// SPDX-License-Identifier: LGPL-3.0-only
|
|
// End of Source File Header
|
|
|
|
// P5c ct (MG_Remote/CONTRACT-P5C.md §5): the two control records, end to end over inproc.
|
|
//
|
|
// applier_reset (§5.1): this process's first validate primes the pipe tracker, and the
|
|
// FreshlyPrimed edge is the record's producer (MG_Impl/Pipe/PipeFill.cpp). What the scenario
|
|
// asserts is the SERVER sink's own counters - moved by nobody else - plus the pixels, so a
|
|
// reset that never crossed is red by the tally and a client that fell through to the driver
|
|
// is red by ScenarioFixture's emit-ordinal rule.
|
|
//
|
|
// object_death (§5.2): a texture and a framebuffer die, the deaths cross, and the slots
|
|
// recycle. The picture after recycling is the behavioural half: a stale twin answering for
|
|
// the recycled object renders the DEAD object's content (or errors), which is the exact
|
|
// shape LiveGenAt's generation check exists to refuse.
|
|
//
|
|
// RED ONCE for the reset (executed, recorded in the package report): reverting PipeFill's
|
|
// FreshlyPrimed arm to the GL-thread direct MGPipeApplierReset() call aborts this whole lane
|
|
// with Fatal{RoleViolation, "g_applier"} out of the applier's layer-2 guard. The automated
|
|
// half of that control is TheDirectApplierResetCallOnTheGLThreadIsRoleViolation below, and
|
|
// the guard's two non-Fatal arms are pinned in PipeWireCodecTest's CtWireFatals suite.
|
|
|
|
#include "../Harness/ScenarioFixture.h"
|
|
#include "../Harness/SplitRuntimePeek.h"
|
|
|
|
#include <MG_Pipe/PipeApply.h>
|
|
#include <MG_Remote/Server/ServerSession.h>
|
|
|
|
#if !defined(_WIN32)
|
|
#include <csignal>
|
|
#include <cstdlib>
|
|
#include <fstream>
|
|
#include <sstream>
|
|
#endif
|
|
|
|
#ifdef GLAPI
|
|
#undef GLAPI
|
|
#endif
|
|
#define GL_GLEXT_PROTOTYPES
|
|
#include <GL/gl.h>
|
|
#include <GL/glcorearb.h>
|
|
#undef GL_GLEXT_PROTOTYPES
|
|
|
|
namespace MGITest {
|
|
namespace {
|
|
|
|
MobileGL::MG_Remote::Server::ServerVerbSink& ServerVerbs() {
|
|
return MobileGL::MG_Remote::Server::ServerSessionInstance().Applier().Verbs();
|
|
}
|
|
|
|
class CtWireScenario : public ScenarioTest {
|
|
protected:
|
|
void SetUp() override {
|
|
ScenarioTest::SetUp();
|
|
if (!Ready()) return;
|
|
const auto why = SplitRuntimeSkipReason();
|
|
if (!why.empty()) GTEST_SKIP() << why;
|
|
}
|
|
|
|
// One 4x4 RGBA8 texture whose every texel is `rgba`, uploaded so the object crosses
|
|
// (no handle, no record - CONTRACT-P5C.md §5.2: an object that never crossed emits
|
|
// nothing at death, and this case needs a real death).
|
|
GLuint MakeSolidTexture(const GLubyte rgba[4]) {
|
|
GLuint texture = 0;
|
|
glGenTextures(1, &texture);
|
|
glBindTexture(GL_TEXTURE_2D, texture);
|
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 4, 4, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
|
GLubyte texels[4 * 4 * 4];
|
|
for (int i = 0; i < 4 * 4; ++i) {
|
|
for (int c = 0; c < 4; ++c) texels[i * 4 + c] = rgba[c];
|
|
}
|
|
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 4, 4, GL_RGBA, GL_UNSIGNED_BYTE, texels);
|
|
return texture;
|
|
}
|
|
|
|
// The texture's level-0 content, read back through a throwaway FBO, as four bytes.
|
|
// The FBO is bound AND deleted here, so it never leaks a framebuffer death into the
|
|
// tally a case is watching.
|
|
void ReadTextureLevel(GLuint texture, GLubyte out[4]) {
|
|
GLuint fbo = 0;
|
|
glGenFramebuffers(1, &fbo);
|
|
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
|
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
|
|
glReadPixels(1, 1, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, out);
|
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
|
glDeleteFramebuffers(1, &fbo);
|
|
}
|
|
};
|
|
|
|
TEST_F(CtWireScenario, ApplierResetCrossesAtThePrimedEdgeInSerialOrder) {
|
|
if (!Ready()) return;
|
|
// The first verb of the process primes the tracker; the edge is the producer. The
|
|
// clear also gives the case its pixels, so a lane that emitted nothing is red twice
|
|
// (here and in the fixture's emit-ordinal rule).
|
|
glClearColor(0.2f, 0.4f, 0.6f, 1.0f);
|
|
glClear(GL_COLOR_BUFFER_BIT);
|
|
EXPECT_GE(ServerVerbs().ApplierResets(), 1u)
|
|
<< "the FreshlyPrimed edge emitted no applier_reset; the server's g_applier was "
|
|
"never reset for this session";
|
|
// The serial sequence is the session's own count (§1: asserted, never dispatched
|
|
// on): every record the sink accepted carried the serial it expected, or the counter
|
|
// and the accepted tally would disagree.
|
|
EXPECT_EQ(ServerVerbs().ExpectedApplierResetSerial(), ServerVerbs().ApplierResets())
|
|
<< "an applier_reset record was dropped or replayed on the wire";
|
|
|
|
GLubyte pixel[4]{};
|
|
glReadPixels(1, 1, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixel);
|
|
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "Ct.ApplierReset.error";
|
|
const int expected[4] = {51, 102, 153, 255};
|
|
for (int i = 0; i < 4; ++i) EXPECT_NEAR(pixel[i], expected[i], 1) << "Ct.ApplierReset.pixels";
|
|
}
|
|
|
|
TEST_F(CtWireScenario, TextureDeathCrossesAndTheRecycledSlotAnswersTheNewObject) {
|
|
if (!Ready()) return;
|
|
const GLubyte red[4] = {255, 0, 0, 255};
|
|
const GLubyte green[4] = {0, 255, 0, 255};
|
|
|
|
// A texture lives and crosses (the upload emits its records). It is also READ BACK
|
|
// once before it dies: the read forces the server-side sync that creates the twin -
|
|
// and, one level down, resolves the Espryt slot arm that INSTALLS the death-notice
|
|
// ops, which no process has before its first twin lookup. The red picture is the
|
|
// pre-death control the recycle below is read against.
|
|
GLuint texture = MakeSolidTexture(red);
|
|
GLubyte before[4]{};
|
|
ReadTextureLevel(texture, before);
|
|
const int redExpected[4] = {255, 0, 0, 255};
|
|
for (int i = 0; i < 4; ++i) ASSERT_NEAR(before[i], redExpected[i], 1) << "Ct.TextureDeath.before";
|
|
|
|
// The object dies unbound so the destructor - and the death record - fire at the
|
|
// delete, and the tally is read AFTER the EmitAndWait the delete blocked on.
|
|
glBindTexture(GL_TEXTURE_2D, 0);
|
|
const MobileGL::Uint64 deathsBefore = ServerVerbs().ObjectDeaths();
|
|
glDeleteTextures(1, &texture);
|
|
EXPECT_GT(ServerVerbs().ObjectDeaths(), deathsBefore)
|
|
<< "the texture's death produced no object_death record; the server's twin was "
|
|
"never told to let go";
|
|
|
|
// The slot recycles forward: a new texture (the frontend hands the same GL name back
|
|
// more often than not, which is exactly the ABA shape) must answer with ITS content,
|
|
// not the dead twin's. Red then green, read back as pixels: a stale twin is red.
|
|
texture = MakeSolidTexture(green);
|
|
GLubyte pixel[4]{};
|
|
ReadTextureLevel(texture, pixel);
|
|
glBindTexture(GL_TEXTURE_2D, 0);
|
|
glDeleteTextures(1, &texture);
|
|
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "Ct.TextureDeath.error";
|
|
const int expected[4] = {0, 255, 0, 255};
|
|
for (int i = 0; i < 4; ++i) EXPECT_NEAR(pixel[i], expected[i], 1)
|
|
<< "Ct.TextureDeath.pixels - the recycled texture read back the dead twin's content";
|
|
}
|
|
|
|
TEST_F(CtWireScenario, FramebufferDeathCrossesAndTheRecycledSlotAnswersTheNewObject) {
|
|
if (!Ready()) return;
|
|
// Framebuffer is the kind object_death EXISTS for: it has no other wire delete
|
|
// opcode (CONTRACT-P5C.md §5.2). The renderbuffer goes along so the FBO has storage.
|
|
GLuint fbo = 0, renderbuffer = 0;
|
|
glGenFramebuffers(1, &fbo);
|
|
glGenRenderbuffers(1, &renderbuffer);
|
|
glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer);
|
|
glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, 4, 4);
|
|
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
|
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, renderbuffer);
|
|
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), GLenum(GL_FRAMEBUFFER_COMPLETE))
|
|
<< "Ct.FramebufferDeath.setup";
|
|
glClearColor(0.0f, 0.0f, 1.0f, 1.0f);
|
|
glClear(GL_COLOR_BUFFER_BIT);
|
|
GLubyte pixel[4]{};
|
|
glReadPixels(1, 1, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixel);
|
|
const int blue[4] = {0, 0, 255, 255};
|
|
for (int i = 0; i < 4; ++i) ASSERT_NEAR(pixel[i], blue[i], 1) << "Ct.FramebufferDeath.first";
|
|
|
|
// Die unbound, framebuffer first so the attachment's own death is a separate record.
|
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
|
glBindRenderbuffer(GL_RENDERBUFFER, 0);
|
|
const MobileGL::Uint64 deathsBefore = ServerVerbs().ObjectDeaths();
|
|
glDeleteFramebuffers(1, &fbo);
|
|
EXPECT_GT(ServerVerbs().ObjectDeaths(), deathsBefore)
|
|
<< "the framebuffer's death produced no object_death record - and no other "
|
|
"opcode can carry it";
|
|
glDeleteRenderbuffers(1, &renderbuffer);
|
|
|
|
// Recycle: a new FBO and a new backing store, cleared to a different colour. A stale
|
|
// twin answering for the recycled framebuffer renders the blue of the dead one.
|
|
glGenFramebuffers(1, &fbo);
|
|
glGenRenderbuffers(1, &renderbuffer);
|
|
glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer);
|
|
glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, 4, 4);
|
|
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
|
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, renderbuffer);
|
|
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), GLenum(GL_FRAMEBUFFER_COMPLETE))
|
|
<< "Ct.FramebufferDeath.recycle";
|
|
glClearColor(1.0f, 1.0f, 0.0f, 1.0f);
|
|
glClear(GL_COLOR_BUFFER_BIT);
|
|
glReadPixels(1, 1, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixel);
|
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
|
glBindRenderbuffer(GL_RENDERBUFFER, 0);
|
|
glDeleteFramebuffers(1, &fbo);
|
|
glDeleteRenderbuffers(1, &renderbuffer);
|
|
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "Ct.FramebufferDeath.error";
|
|
const int yellow[4] = {255, 255, 0, 255};
|
|
for (int i = 0; i < 4; ++i) EXPECT_NEAR(pixel[i], yellow[i], 1)
|
|
<< "Ct.FramebufferDeath.pixels - the recycled framebuffer read back the dead twin's content";
|
|
}
|
|
|
|
#if !defined(_WIN32)
|
|
TEST_F(CtWireScenario, TheDirectApplierResetCallOnTheGLThreadIsRoleViolation) {
|
|
if (!Ready()) return;
|
|
// A verb in the PARENT, so the fixture's emit-ordinal rule has something to see: the
|
|
// death statement below runs only in the child, and a parent that emitted nothing
|
|
// is the shape that rule exists to fail.
|
|
glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
|
|
glClear(GL_COLOR_BUFFER_BIT);
|
|
// THE RED-ONCE FOR §5.1, AUTOMATED. This process has a LIVE inproc session, and the
|
|
// GL-thread direct call the applier_reset record replaced is a named Fatal there
|
|
// (CONTRACT-P5C.md §5.1 / §6 layer 2) - which is exactly the shape reverting
|
|
// PipeFill's FreshlyPrimed arm would take, so the guard is what turns that revert
|
|
// red. EXPECT_EXIT re-runs the case in a child process: the child brings up its own
|
|
// session (its SetUp is this same fixture's) and aborts at the statement. The two
|
|
// NON-Fatal arms - monolith, and a transport with no client session - are the unit
|
|
// suite's CtWireFatals.TheDirectApplierResetCallSurvivesMonolithAndAWirelessTransport.
|
|
//
|
|
// The regex is ".*" because the library's Fatal line goes to its log file, not to
|
|
// the stderr a death test matches (ServerLoopEglTest's own EXPECT_EXIT does the
|
|
// same); the NAME is asserted from the log below, which the child truncated and
|
|
// wrote before it died.
|
|
EXPECT_EXIT(MobileGL::MG_Pipe::MGPipeApplierReset(), ::testing::KilledBySignal(SIGABRT), ".*");
|
|
if (const char* logPath = std::getenv("MOBILEGL_LOG_FILE_PATH"); logPath != nullptr) {
|
|
std::ifstream in(logPath, std::ios::binary);
|
|
std::ostringstream log;
|
|
log << in.rdbuf();
|
|
EXPECT_NE(log.str().find("Fatal{RoleViolation, \"g_applier\"}"), std::string::npos)
|
|
<< "the child aborted, but not with the layer-2 guard's own line:\n"
|
|
<< log.str();
|
|
}
|
|
}
|
|
#endif
|
|
|
|
} // namespace
|
|
} // namespace MGITest
|