Merge branch 'p5c-ct' into feat/disaggregated (P5c ct: applier_reset / object_death control records)

# Conflicts:
#	MobileGL/MG_Backend/DirectGLES/Managers.h
This commit is contained in:
2026-09-17 06:58:45 -04:00
22 changed files with 1114 additions and 36 deletions
+67 -2
View File
@@ -22,6 +22,10 @@
#include <MG_Remote/Server/StagedShadow.h> #include <MG_Remote/Server/StagedShadow.h>
#include <MG_Remote/Server/StagedTextureStore.h> #include <MG_Remote/Server/StagedTextureStore.h>
#include <MG_Remote/Server/ServerLoop.h> #include <MG_Remote/Server/ServerLoop.h>
// P5c (ct): object_death's producer (CONTRACT-P5C.md §5.2) - the death notice's split arm
// emits the record through the client's emit helper instead of hopping a stack struct to the
// apply thread.
#include <MG_Remote/Client/WireTables.h>
#endif #endif
#include "Utils.h" #include "Utils.h"
@@ -207,10 +211,34 @@ namespace MobileGL::MG_Backend::DirectGLES {
void OnFrontendStateObjectDestroyed(MG_Pipe::MGPipeKind kind, Uint64 lifetimeId) { void OnFrontendStateObjectDestroyed(MG_Pipe::MGPipeKind kind, Uint64 lifetimeId) {
if (InProcessTeardown()) return; if (InProcessTeardown()) return;
#if MOBILEGL_BUILD_DISAGGREGATED #if MOBILEGL_BUILD_DISAGGREGATED
// Death notices have no framebuffer wire opcode. Keep the lifetime/slot valid // P5c (ct), CONTRACT-P5C.md §5.2: with an active transport the death crosses AS A
// until the context owner has destroyed its twin and updated its binding cache. // RECORD - object_death, the framebuffer family's first wire delete opcode. The GL
// thread resolves the dying object's handle in its OWN allocator inside
// EmitObjectDeathRecord: no handle means the server never saw the object and
// NOTHING crosses (which replaces the mailbox's unconditional delivery), and a
// handle means the record's EmitAndWait orders the death against in-flight verbs
// that name it - the one property the blocking RunOnApplyThread hop provided and
// the only one P5c keeps. The sink (ServerVerbSink::OnObjectDeath) releases the
// kind's twin table by handle on the apply thread, so neither a lifetime-id probe
// nor the client's allocator is touched from there any more.
if (MG_Config::Transport != MG_Config::TransportMode::Monolith && if (MG_Config::Transport != MG_Config::TransportMode::Monolith &&
!MG_Remote::Server::ServerLoop::OnApplyThread()) { !MG_Remote::Server::ServerLoop::OnApplyThread()) {
if (MG_Remote::Client::EmitObjectDeathRecord(kind, lifetimeId) !=
MG_Remote::Client::ObjectDeathEmit::NoSession) {
return;
}
// NoSession is the ONE shape the record cannot carry: a transport configured
// with no client session at all - a ServerLoop fixture driving a server role
// with no client, never a real split (there a session exists whenever the
// server does). The server loop IS there, so the death takes the delivery
// that predates the record, kept VERBATIM for exactly this arm: a stack
// struct hopped to the apply thread, which runs the switch below. NoHandle
// does NOT land here - a handle that never existed has no twin to kill (§5.2)
// - and a loop that is not RUNNING (teardown, pre-Start) has no thread to hop
// to; the twin dies with the server either way.
if (!MG_Remote::Server::ServerLoopInstance().Running()) {
return;
}
struct Death { MG_Pipe::MGPipeKind kind; Uint64 lifetimeId; } death{kind, lifetimeId}; struct Death { MG_Pipe::MGPipeKind kind; Uint64 lifetimeId; } death{kind, lifetimeId};
MG_Remote::Server::ServerLoopInstance().RunOnApplyThread( MG_Remote::Server::ServerLoopInstance().RunOnApplyThread(
+[](void* user) -> MobileGLResult { +[](void* user) -> MobileGLResult {
@@ -296,6 +324,43 @@ namespace MobileGL::MG_Backend::DirectGLES {
}; };
} // namespace } // namespace
#if MOBILEGL_BUILD_DISAGGREGATED
Bool ReleaseTwinsForWireObjectDeath(MG_Pipe::MGPipeHandle handle, MG_Pipe::MGPipeKind kind) {
// The handle-keyed twin of the notice switch above, and deliberately NOT that switch:
// the record carried the handle, so no lifetime id is probed and the client's
// allocator - a client-only surface under a transport (CONTRACT-P5C.md §3.1) - is
// never touched from the apply thread. Each arm names its kind's registry GLOBAL for
// the same spelling reason as the notice arms: ReleaseByHandle /
// ReleaseTwinByHandle is static and is answered by every table of the kind that
// exists, this global's own and any by-value copy a fixture or a context reset holds.
switch (kind) {
case MG_Pipe::MGPipeKind::Texture:
return TextureImpl::g_backendTextureObjects.ReleaseByHandle(handle);
case MG_Pipe::MGPipeKind::Framebuffer:
return FramebufferImpl::g_backendFramebufferObjects.ReleaseByHandle(handle);
case MG_Pipe::MGPipeKind::Renderbuffer:
return RenderbufferImpl::g_backendRenderbufferObjects.ReleaseByHandle(handle);
case MG_Pipe::MGPipeKind::SamplerCso:
return SamplerImpl::g_backendSamplerObjects.ReleaseByHandle(handle);
case MG_Pipe::MGPipeKind::ShaderCso:
return PrgramImpl::g_backendProgramObjects.ReleaseByHandle(handle);
case MG_Pipe::MGPipeKind::SamplerViewCso:
// The notice arm's REDUNDANT SECOND PATH, keyed by the view's handle the same
// way (CONTRACT-P5C.md §5.2): the client's delete_sampler_view may already have
// released the twin, in which case the generation check inside fails and this
// answers false. A false here is idempotency, never a leak - the view twin owns
// no driver id of its own.
return SamplerViewImpl::BackendSamplerViewTable::ReleaseTwinByHandle(handle);
case MG_Pipe::MGPipeKind::VertexElementsCso:
return VertexArrayImpl::g_backendVertexArrayObjects.ReleaseByHandle(handle);
default:
// Buffer's death crosses as resource_destroy (P3a), exactly as the notice
// switch's default arm rules; every other kind has no twin table here.
return false;
}
}
#endif
// The one sentence that decides the arm, written once so that a test can drive every // The one sentence that decides the arm, written once so that a test can drive every
// combination of the two knobs and so that bring-up and first-use cannot disagree. // combination of the two knobs and so that bring-up and first-use cannot disagree.
EsprytSlotArmVerdict ClassifyEsprytSlotArm(Bool subsystemBitSet, Bool legacyMemosEnabled) { EsprytSlotArmVerdict ClassifyEsprytSlotArm(Bool subsystemBitSet, Bool legacyMemosEnabled) {
+36 -10
View File
@@ -484,17 +484,30 @@ namespace MobileGL::MG_Backend::DirectGLES {
} }
#endif #endif
// NO ReleaseByHandle HERE, AND THAT IS A DECISION (review M-4). The death half of // NO ReleaseByHandle HERE THROUGH P5, AND THAT WAS A DECISION (review M-4). The death
// GetOrCreateByHandle exists for a kind whose announcement is its own destroy CALL // half of GetOrCreateByHandle existed only for a kind whose announcement is its own
// rather than the shared death notice - which is the BUFFER family // destroy CALL rather than the shared death notice - which was the BUFFER family
// (BackendBufferResourceTable::ReleaseByHandle, SlotTables.h, called from // (BackendBufferResourceTable::ReleaseByHandle, SlotTables.h, called from
// resource_destroy) and none of the five kinds this registry serves: every one of them // resource_destroy) and none of the five kinds this registry serves: every one of
// dies through DestroyByLifetimeId below, because P4a adds no server-side destroy arm // them died through DestroyByLifetimeId below, because P4a added no server-side
// for a texture, a renderbuffer, a framebuffer, a sampler CSO or a shader CSO. v1 // destroy arm for a texture, a renderbuffer, a framebuffer, a sampler CSO or a shader
// declared one here anyway and it had no caller on either arm, which made its bound and // CSO. P5c (ct) is the commit that gives the wrapper its caller: object_death carries
// its wording things nobody would exercise until P5. The one-line wrapper comes back in // the dead object's HANDLE on the wire (CONTRACT-P5C.md §5.2), and the static
// the commit that gives it a caller; SlotTable::ReleaseByHandle underneath is untouched // ReleaseByHandle below is what the sink's per-kind dispatch calls.
// and is what SanityTest drives directly.
// P5c (ct), CONTRACT-P5C.md §5.2: the wrapper M-4 below deferred, given its caller by
// object_death. The record carried the handle, so the release is keyed by it and the
// client's allocator is never asked from this side (rule E); every holder of the kind
// lets go, exactly as DestroyByLifetimeId walks them. What this does NOT do is the
// allocator Free the notice arm performs - the slot's owner is the client, which
// already returned it after the record went out. STATIC for the same reason
// DestroyByLifetimeId is: a death is about an object, not a table instance.
static Bool ReleaseByHandle(MG_Pipe::MGPipeHandle handle) {
if (EsprytSlotTablesEnabled()) {
return SlotTable::ReleaseTwinByHandle(handle);
}
return false;
}
// P2 step e2. STATIC, because a death notice is about an object and not about a // P2 step e2. STATIC, because a death notice is about an object and not about a
// registry instance: it is answered by EVERY table of this kind that exists - this // registry instance: it is answered by EVERY table of this kind that exists - this
@@ -2806,4 +2819,17 @@ namespace MobileGL::MG_Backend::DirectGLES {
extern TwinRegistry<MG_State::GLState::RenderbufferObject, BackendRenderbufferObject, MG_Pipe::MGPipeKind::Renderbuffer> extern TwinRegistry<MG_State::GLState::RenderbufferObject, BackendRenderbufferObject, MG_Pipe::MGPipeKind::Renderbuffer>
g_backendRenderbufferObjects; g_backendRenderbufferObjects;
} // namespace RenderbufferImpl } // namespace RenderbufferImpl
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (ct), CONTRACT-P5C.md §5.2: object_death's per-kind release, one entry point for all
// seven kinds for the same reason the notice switch is one - the answer is the same for
// all of them: every holder of the kind's twin table lets go of the twin at this handle.
// Called from ServerVerbSink::OnObjectDeath ON THE APPLY THREAD, with the handle the
// record carried; the client's allocator is never consulted (rule E). Returns whether any
// table released a twin - false for a kind this backend does not twin (Buffer: its death
// crosses as resource_destroy) and for a handle no holder holds, which the kind's own
// delete opcode may already have released (the idempotent-second-path shape the notice
// arms document).
Bool ReleaseTwinsForWireObjectDeath(MG_Pipe::MGPipeHandle handle, MG_Pipe::MGPipeKind kind);
#endif
} // namespace MobileGL::MG_Backend::DirectGLES } // namespace MobileGL::MG_Backend::DirectGLES
@@ -484,6 +484,28 @@ namespace MobileGL::MG_Backend::DirectGLES {
return true; return true;
} }
// P5c (ct), CONTRACT-P5C.md §5.2: the handle-keyed half of the above, for a death that
// arrived AS A WIRE RECORD (object_death) rather than as the shared notice. The
// handle IS the resolution - it crossed in the record's payload - so the client's
// allocator is never asked: under an active transport MGPipeSlots() is a client-only
// surface (rule E, §3.1) and this function runs on the apply thread. Every holder
// lets go exactly as the notice arm does, in the same successor-first order. What
// does NOT happen here is the allocator Free: the slot's owner - the client -
// returned it itself after the record went out (PipeFill.cpp's NotifyAndFree), and a
// double Free would be refused by generation anyway. Idempotent against the kind's
// own delete opcode, exactly as the notice arm is: a twin the delete already released
// fails ReleaseTwinAt's generation check and the walk moves on.
static Bool ReleaseTwinByHandle(MG_Pipe::MGPipeHandle handle) {
if (MG_Pipe::MGPipeHandleIsNull(handle)) return false;
Bool released = false;
for (BackendSlotTable* holder = s_firstHolder; holder != nullptr;) {
BackendSlotTable* const next = holder->m_nextHolder;
released = holder->ReleaseTwinAt(handle) || released;
holder = next;
}
return released;
}
// How many tables of this type exist right now. For the tests that pin the holder // How many tables of this type exist right now. For the tests that pin the holder
// list; nothing on a shipping path asks. // list; nothing on a shipping path asks.
static Uint32 HolderCount() { static Uint32 HolderCount() {
+30
View File
@@ -2690,7 +2690,37 @@ namespace MobileGL::MG_Pipe {
// - what the server has is no longer what any suppressor slot last emitted; // - what the server has is no longer what any suppressor slot last emitted;
// - and the residual block owes a fresh publication whatever else moved. // - and the residual block owes a fresh publication whatever else moved.
if (tracker.FreshlyPrimed()) { if (tracker.FreshlyPrimed()) {
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (ct), CONTRACT-P5C.md §5.1: with an active transport the server's reset
// crosses AS A RECORD, ahead of every reset below - the client-side ones (the CSO
// cache, the hash suppressor, the vertex-input emitter) and the emitters' latches
// - because the record's barrier is what orders the server's MGPipeApplierReset()
// against every verb that follows. The GL-thread direct call it replaces is
// Fatal{RoleViolation, "g_applier"} inside MGPipeApplierReset itself (§6 layer 2),
// so reverting this arm to the direct call goes red by name rather than rendering
// stale. Monolith keeps the direct call, byte for byte (G1).
//
// THE APPLY THREAD IS EXCLUDED (M5's rule): a validate running on the server's own
// thread produces no client record - EmitAndWait there would wait on the thread
// that has to apply the record - and the direct call is exactly what the apply
// thread is allowed to make (the sink's own path runs it there).
//
// AND A CONFIGURED-BUT-WIRELESS TRANSPORT TAKES THE DIRECT CALL TOO:
// EmitApplierResetRecord answers false when no live session could carry the
// record (the pre-Start bring-up window, a ServerLoop fixture with no client at
// all), and in that shape this process IS the only place the reset can run.
const Bool transportActive =
MG_Config::Transport != MG_Config::TransportMode::Monolith &&
!MG_Remote::Client::RunsAsTheServerRole();
Bool resetCrossed = false;
if (transportActive) {
resetCrossed = MG_Remote::Client::EmitApplierResetRecord();
}
#endif
MGPipeCsoCacheInstance().Reset(); MGPipeCsoCacheInstance().Reset();
#if MOBILEGL_BUILD_DISAGGREGATED
if (!resetCrossed)
#endif
MGPipeApplierReset(); MGPipeApplierReset();
MGPipeSetHashSuppressorInstance().InvalidateAll(); MGPipeSetHashSuppressorInstance().InvalidateAll();
// P3a: and the vertex-input emitter's latches. NOT because the applier dropped // P3a: and the vertex-input emitter's latches. NOT because the applier dropped
@@ -2172,6 +2172,29 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
endforeach() endforeach()
endif() endif()
# P5c ct (MG_Remote/CONTRACT-P5C.md §5): the two control records, split-only source for the
# same reason as f1's - the scenario reads the SERVER sink's tallies, which a monolith build
# has no symbol for. One block per case with a LOG PATH OF ITS OWN, the F1 pattern:
# SplitLogPaths.PrivateAndDistinct fails a lane whose cases share one file, and the death
# case's child truncates whatever path it inherits.
if (MOBILEGL_BUILD_DISAGGREGATED)
target_sources(MobileGLIntegrationTest PRIVATE Scenarios/CtWireScenario.cpp)
foreach(ctCase ApplierResetCrossesAtThePrimedEdgeInSerialOrder
TextureDeathCrossesAndTheRecycledSlotAnswersTheNewObject
FramebufferDeathCrossesAndTheRecycledSlotAnswersTheNewObject
TheDirectApplierResetCallOnTheGLThreadIsRoleViolation)
gtest_discover_tests(MobileGLIntegrationTest
TEST_PREFIX "DirectGLES.Split.Ct."
TEST_FILTER "CtWireScenario.${ctCase}"
DISCOVERY_TIMEOUT 30
PROPERTIES
LABELS "integration-gpu\;integration-split"
TIMEOUT ${MGL_ITEST_TIMEOUT}
ENVIRONMENT "${MGL_ITEST_GLES_SPLIT_ENVIRONMENT}\;MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/p5c-ct-${ctCase}.log"
)
endforeach()
endif()
# P5b measured named-blit blockers, exercising both backend bound-form lowerings. # P5b measured named-blit blockers, exercising both backend bound-form lowerings.
if (MOBILEGL_BUILD_DISAGGREGATED) if (MOBILEGL_BUILD_DISAGGREGATED)
foreach(namedBlitBackend DirectGLES DirectVulkan) foreach(namedBlitBackend DirectGLES DirectVulkan)
@@ -0,0 +1,244 @@
// 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
+27
View File
@@ -1633,6 +1633,33 @@ namespace MobileGL::MG_Pipe {
}; };
MGP_ASSERT_POD(MGPCopyFromFramebuffer, 48); MGP_ASSERT_POD(MGPCopyFromFramebuffer, 48);
// ---------------------------------------------------------------------------------
// P5c: the two appended rows (MG_Remote/CONTRACT-P5C.md §5). APPENDED, never inserted:
// applier_reset is opcode 77 and object_death opcode 78, and no earlier opcode moved.
//
// Neither has an MGPipeApply* entry point and neither gains one. Both reach
// MG_Remote::Wire::WireVerbSink like the five P5b verbs, and under monolith neither has a
// producer - the reset is the GL thread's direct MGPipeApplierReset() call and the death
// notice's mailbox hop, byte for byte as today (G1/G2).
// ---------------------------------------------------------------------------------
// applier_reset = tracker.FreshlyPrimed()'s server half (P5c §5.1). The one field is the
// client-side context's make-current serial at the edge that primed it: 0 is the first
// make-current. P5c has exactly one context per session, so the sink ASSERTS the value
// against the session's own count rather than dispatching on it; P6's multi-context shape
// is what reads it for real.
struct MGPApplierReset {
Uint64 ContextSerial;
};
MGP_ASSERT_POD(MGPApplierReset, 8);
// object_death carries no payload of its own: MGPHandleOnly (:93-98) IS the payload
// (CONTRACT-P5C.md §1) - Handle the dead frontend object's handle, Kind its MGPipeKind
// widened, one row for all seven kinds the death switch handles. A null handle never
// crosses: the client emits nothing when its own allocator cannot resolve the dying
// object (§5.2), so a null handle arriving is Fatal{ProtocolCorruption,
// "ObjectDeath.Handle"} at the sink.
// --------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------
// Reverse channel payloads (section 7.1) // Reverse channel payloads (section 7.1)
// --------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------
+29
View File
@@ -37,6 +37,13 @@
// MOBILEGL_PIPE_VERIFY block above on purpose: the tier is a property of the BUILD, not of the // MOBILEGL_PIPE_VERIFY block above on purpose: the tier is a property of the BUILD, not of the
// comparator, and a split build without the comparator still declines every acquisition. // comparator, and a split build without the comparator still declines every acquisition.
#include <MG_Remote/Client/PersistentMapTracker.h> #include <MG_Remote/Client/PersistentMapTracker.h>
// P5c (ct), CONTRACT-P5C.md §6 layer 2: MGPipeApplierReset's role guard asks
// ServerLoop::OnApplyThread() - the one predicate that tells the GL thread from the thread
// that owns g_applier under an active transport - and ClientSession::Active(), which is what
// tells a live wire from a configured-but-wireless one (the bring-up window and the
// server-role-only fixture: there the direct call is the only reset that exists).
#include <MG_Remote/Client/ClientSession.h>
#include <MG_Remote/Server/ServerLoop.h>
#endif #endif
#include <algorithm> #include <algorithm>
@@ -1212,6 +1219,28 @@ namespace MobileGL::MG_Pipe {
} // namespace } // namespace
void MGPipeApplierReset() { void MGPipeApplierReset() {
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (ct), CONTRACT-P5C.md §5.1 / §6 layer 2. With an active transport g_applier is
// SERVER-PRIVATE and the GL thread's reset crosses as the applier_reset RECORD
// (PipeFill.cpp's FreshlyPrimed arm emits it; ServerVerbSink::OnApplierReset runs this
// function ON the apply thread). Reaching here from any other thread WITH A LIVE WIRE
// is the direct call the record replaced - a write into server memory with no wire
// shape, and exactly the revert the red-once gate must catch - so it is a named Fatal
// rather than a silent reset of state another role owns. The window the Fatal
// deliberately does NOT cover is a configured-but-wireless transport: before
// ClientSession::Start() there is no wire for a record to cross (§6 layer 2's
// documented bring-up exception), and a ServerLoop fixture with no client at all has
// none either - there this call is the only reset that exists. Monolith keeps the
// direct call, byte for byte (G1); in a pull build none of this is compiled at all.
if (MG_Config::Transport != MG_Config::TransportMode::Monolith &&
MG_Remote::Client::ClientSession::Active() != nullptr &&
!MG_Remote::Server::ServerLoop::OnApplyThread()) {
MGLOG_F("MGPipe: Fatal{RoleViolation, \"g_applier\"} - MGPipeApplierReset() called "
"off the apply thread with an active transport; under split the reset "
"crosses as the applier_reset record (CONTRACT-P5C.md §5.1)");
std::abort();
}
#endif
g_applier.RenderStateCsos.clear(); g_applier.RenderStateCsos.clear();
g_applier.BoundRenderStateCso = kMGPipeNullHandle; g_applier.BoundRenderStateCso = kMGPipeNullHandle;
g_applier.Residual = ResidualValueBlock{}; g_applier.Residual = ResidualValueBlock{};
+32 -6
View File
@@ -41,17 +41,18 @@
// that the expansion, the two generated tables and this number agree. // that the expansion, the two generated tables and this number agree.
// //
// class entries group (as the plan tabulates it) // class entries group (as the plan tabulates it)
// kScreen 11 screen: caps 1 + resource 3 + persistent map 2 + fence 4, plus the // kScreen 12 screen: caps 1 + resource 3 + persistent map 2 + fence 4, plus the
// appended server-side fence wait 1 // appended server-side fence wait 1 and P5c's applier_reset 1
// kCtxQuery 8 query object namespace 6, plus the appended timestamp pair 2 // kCtxQuery 8 query object namespace 6, plus the appended timestamp pair 2
// kCtxCso 13 CSO create/bind/delete // kCtxCso 13 CSO create/bind/delete
// kCtxState 17 16 of the 17 set_* calls + the temporary set_residual_value_state // kCtxState 17 16 of the 17 set_* calls + the temporary set_residual_value_state
// kCtxObject 9 set_texture_params (the 17th set_*) + 8 object-scoped transfers // kCtxObject 10 set_texture_params (the 17th set_*) + 8 object-scoped transfers, plus
// P5c's object_death 1
// kCtxVerb 18 3 context-reading transfer calls + the 10 commands, plus the five // kCtxVerb 18 3 context-reading transfer calls + the 10 commands, plus the five
// P5b-appended verbs (bind_shader_image, patch_parameter, // P5b-appended verbs (bind_shader_image, patch_parameter,
// bind_stream_output, set_storage_block_binding, // bind_stream_output, set_storage_block_binding,
// copy_framebuffer_to_texture - MG_Remote/CONTRACT-P5B.md) // copy_framebuffer_to_texture - MG_Remote/CONTRACT-P5B.md)
// total 76 // total 78
// //
// Reconciliation with the plan's headline numbers (section 4.4 / appendix A), because they // Reconciliation with the plan's headline numbers (section 4.4 / appendix A), because they
// do not add up to a set of UNIQUE records and this file has to hold unique records: // do not add up to a set of UNIQUE records and this file has to hold unique records:
@@ -88,9 +89,15 @@
// 76 unique records. The catalogue footer's "ShaderStorageBlockBinding is folded into the // 76 unique records. The catalogue footer's "ShaderStorageBlockBinding is folded into the
// reflection archive" is CORRECTED by the row: the archive carries the bindings the // reflection archive" is CORRECTED by the row: the archive carries the bindings the
// program LINKED with, and glShaderStorageBlockBinding moves one AFTER link. // program LINKED with, and glShaderStorageBlockBinding moves one AFTER link.
// - P5c (MG_Remote/CONTRACT-P5C.md §5.1/§5.2) appended TWO more after those (opcodes 77..78),
// same rule: applier_reset, the make-current edge's server-side reset that used to be a
// GL-thread direct call into server memory, and object_death, the framebuffer family's
// FIRST wire delete opcode and the handle-keyed replacement for the death-notice mailbox.
// Neither has an MGPipeApply* entry point and neither gains one; both reach WireVerbSink.
// 78 unique records.
// --------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------
#define MGP_CALL_LIST_DOCUMENTED_COUNT 76 #define MGP_CALL_LIST_DOCUMENTED_COUNT 78
// clang-format off // clang-format off
#define MGP_CALL_LIST(X) \ #define MGP_CALL_LIST(X) \
@@ -230,7 +237,26 @@
X(SetStorageBlockBinding, MGPStorageBlockBinding, kCtxVerb, kHasBlob) \ X(SetStorageBlockBinding, MGPStorageBlockBinding, kCtxVerb, kHasBlob) \
/* glCopyTexImage2D / glCopyTexSubImage2D - a copy whose SOURCE is the read framebuffer, */ \ /* glCopyTexImage2D / glCopyTexSubImage2D - a copy whose SOURCE is the read framebuffer, */ \
/* which resource_copy_region (resource -> resource) cannot express. */ \ /* which resource_copy_region (resource -> resource) cannot express. */ \
X(CopyFramebufferToTexture, MGPCopyFromFramebuffer, kCtxVerb, kNone) X(CopyFramebufferToTexture, MGPCopyFromFramebuffer, kCtxVerb, kNone) \
/* ---- APPENDED BY P5c (MG_Remote/CONTRACT-P5C.md §5): the two control records. Opcodes ---- */ \
/* ---- 77..78. Neither has an MGPipeApply* entry point and neither gains one: both reach ---- */ \
/* ---- WireVerbSink, and under monolith neither has a producer - the reset is a direct ---- */ \
/* ---- MGPipeApplierReset() call and the death notice's mailbox hop, byte for byte as ---- */ \
/* ---- today (G1/G2). ---- */ \
/* tracker.FreshlyPrimed() - a make-current is a fresh server. Under a transport the GL */ \
/* thread may not reset the server's g_applier itself (rule E), so the reset crosses AS */ \
/* THE CALL, ahead of the block's client-side resets; the record's barrier orders it */ \
/* against every verb that follows. ContextSerial is the client context's make-current */ \
/* serial at the edge - ASSERTED against the session's, never dispatched on (P5c: one */ \
/* context per session). */ \
X(ApplierReset, MGPApplierReset, kScreen, kNone) \
/* A frontend state object died. The record carries the HANDLE (and kind) the client's own */ \
/* allocator resolves for it - nothing is emitted for an object that never crossed (the */ \
/* server never saw it, so there is no twin to kill) - and the sink releases the kind's */ \
/* twin table by handle. The framebuffer family's FIRST wire delete opcode: its death used */ \
/* to hop to the apply thread through a stack-struct mailbox and a lifetime-id probe of */ \
/* the client's allocator, both rule-E surfaces. */ \
X(ObjectDeath, MGPHandleOnly, kCtxObject, kNone)
// clang-format on // clang-format on
// Explicitly NOT migrated (plan 4.4.6 / appendix A "explicit deletions"): // Explicitly NOT migrated (plan 4.4.6 / appendix A "explicit deletions"):
+7
View File
@@ -262,6 +262,12 @@
F(Dst) F(Target) F(Level) F(InternalFormat) F(X) F(Y) F(Width) F(Height) F(XOffset) F(YOffset) \ F(Dst) F(Target) F(Level) F(InternalFormat) F(X) F(Y) F(Width) F(Height) F(XOffset) F(YOffset) \
F(SubImage) F(SubImage)
// ---- P5c's two appended rows (MG_Remote/CONTRACT-P5C.md §5). object_death's payload IS
// MGPHandleOnly, whose list already exists above; applier_reset's is one field.
#define MGP_FIELDS_MGPApplierReset(F) \
F(ContextSerial)
// ---- the value structs and the host span (P1 brief D8). Not call payloads themselves, but // ---- the value structs and the host span (P1 brief D8). Not call payloads themselves, but
// members of ones (ResidualValueBlock, MGPPixelPackState, MGPCaps) and of PipeInputs, so the // members of ones (ResidualValueBlock, MGPPixelPackState, MGPCaps) and of PipeInputs, so the
// comparator has to see INTO them: with these lists the memcmp fallback of MGPipeFieldEqual is // comparator has to see INTO them: with these lists the memcmp fallback of MGPipeFieldEqual is
@@ -379,6 +385,7 @@
P(MGPSurfaceInfo) \ P(MGPSurfaceInfo) \
P(MGPImageBind) P(MGPPatchParameter) P(MGPStreamOutputBind) P(MGPStorageBlockBinding) \ P(MGPImageBind) P(MGPPatchParameter) P(MGPStreamOutputBind) P(MGPStorageBlockBinding) \
P(MGPCopyFromFramebuffer) \ P(MGPCopyFromFramebuffer) \
P(MGPApplierReset) \
P(RenderStateParameters) P(PixelStoreParameters) P(SamplerParameters) P(PerBufferBlendState) \ P(RenderStateParameters) P(PixelStoreParameters) P(SamplerParameters) P(PerBufferBlendState) \
P(StencilFaceState) \ P(StencilFaceState) \
P(DynamicBackendParameters) P(MGHostSpan) \ P(DynamicBackendParameters) P(MGHostSpan) \
+7 -5
View File
@@ -12,7 +12,7 @@
// Regenerate with `python3 scripts/gen_pipe.py`; CI runs it and diffs the result. // Regenerate with `python3 scripts/gen_pipe.py`; CI runs it and diffs the result.
// This file is included from MG_Pipe/MGPipe.h inside namespace MobileGL::MG_Pipe. // This file is included from MG_Pipe/MGPipe.h inside namespace MobileGL::MG_Pipe.
// share group: 11 calls. A null entry means the backend does not implement this // share group: 12 calls. A null entry means the backend does not implement this
// call and the frontend keeps its own path (plan B section 4.1). // call and the frontend keeps its own path (plan B section 4.1).
struct MGPipeScreen { struct MGPipeScreen {
void (*GetCaps)(const MGPCaps* payload, const void* blobBytes, Uint64 blobByteCount, MGPReplySlot* reply); void (*GetCaps)(const MGPCaps* payload, const void* blobBytes, Uint64 blobByteCount, MGPReplySlot* reply);
@@ -26,9 +26,10 @@ struct MGPipeScreen {
void (*FenceWait)(const MGPFenceWait* payload, MGPReplySlot* reply); void (*FenceWait)(const MGPFenceWait* payload, MGPReplySlot* reply);
void (*FenceDestroy)(const MGPHandleOnly* payload); void (*FenceDestroy)(const MGPHandleOnly* payload);
void (*FenceWaitServer)(const MGPFenceWait* payload); void (*FenceWaitServer)(const MGPFenceWait* payload);
void (*ApplierReset)(const MGPApplierReset* payload);
}; };
// context: 65 calls. A null entry means the backend does not implement this // context: 66 calls. A null entry means the backend does not implement this
// call and the frontend keeps its own path (plan B section 4.1). // call and the frontend keeps its own path (plan B section 4.1).
struct MGPipeContext { struct MGPipeContext {
void (*QueryCreate)(const MGPQueryDesc* payload); void (*QueryCreate)(const MGPQueryDesc* payload);
@@ -96,11 +97,12 @@ struct MGPipeContext {
void (*BindStreamOutput)(const MGPStreamOutputBind* payload); void (*BindStreamOutput)(const MGPStreamOutputBind* payload);
void (*SetStorageBlockBinding)(const MGPStorageBlockBinding* payload, const void* blobBytes, Uint64 blobByteCount); void (*SetStorageBlockBinding)(const MGPStorageBlockBinding* payload, const void* blobBytes, Uint64 blobByteCount);
void (*CopyFramebufferToTexture)(const MGPCopyFromFramebuffer* payload); void (*CopyFramebufferToTexture)(const MGPCopyFromFramebuffer* payload);
void (*ObjectDeath)(const MGPHandleOnly* payload);
}; };
inline constexpr SizeT kMGPipeScreenCallCount = 11; inline constexpr SizeT kMGPipeScreenCallCount = 12;
inline constexpr SizeT kMGPipeContextCallCount = 65; inline constexpr SizeT kMGPipeContextCallCount = 66;
inline constexpr SizeT kMGPipeCallCount = 76; inline constexpr SizeT kMGPipeCallCount = 78;
// A table that is not exactly its call count of function pointers has grown a // A table that is not exactly its call count of function pointers has grown a
// member that no generator knows about. // member that no generator knows about.
@@ -320,3 +320,11 @@ inline void MGP_SetStorageBlockBinding(const MGPStorageBlockBinding* payload, co
inline void MGP_CopyFramebufferToTexture(const MGPCopyFromFramebuffer* payload) { inline void MGP_CopyFramebufferToTexture(const MGPCopyFromFramebuffer* payload) {
gMGPipeContext.CopyFramebufferToTexture(payload); gMGPipeContext.CopyFramebufferToTexture(payload);
} }
inline void MGP_ApplierReset(const MGPApplierReset* payload) {
gMGPipeScreen.ApplierReset(payload);
}
inline void MGP_ObjectDeath(const MGPHandleOnly* payload) {
gMGPipeContext.ObjectDeath(payload);
}
+9 -1
View File
@@ -112,6 +112,7 @@ inline Bool MGPipeVerify(const MGPPatchParameter& a, const MGPPatchParameter& b,
inline Bool MGPipeVerify(const MGPStreamOutputBind& a, const MGPStreamOutputBind& b, const char** outField); inline Bool MGPipeVerify(const MGPStreamOutputBind& a, const MGPStreamOutputBind& b, const char** outField);
inline Bool MGPipeVerify(const MGPStorageBlockBinding& a, const MGPStorageBlockBinding& b, const char** outField); inline Bool MGPipeVerify(const MGPStorageBlockBinding& a, const MGPStorageBlockBinding& b, const char** outField);
inline Bool MGPipeVerify(const MGPCopyFromFramebuffer& a, const MGPCopyFromFramebuffer& b, const char** outField); inline Bool MGPipeVerify(const MGPCopyFromFramebuffer& a, const MGPCopyFromFramebuffer& b, const char** outField);
inline Bool MGPipeVerify(const MGPApplierReset& a, const MGPApplierReset& b, const char** outField);
inline Bool MGPipeVerify(const RenderStateParameters& a, const RenderStateParameters& b, const char** outField); inline Bool MGPipeVerify(const RenderStateParameters& a, const RenderStateParameters& b, const char** outField);
inline Bool MGPipeVerify(const PixelStoreParameters& a, const PixelStoreParameters& b, const char** outField); inline Bool MGPipeVerify(const PixelStoreParameters& a, const PixelStoreParameters& b, const char** outField);
inline Bool MGPipeVerify(const SamplerParameters& a, const SamplerParameters& b, const char** outField); inline Bool MGPipeVerify(const SamplerParameters& a, const SamplerParameters& b, const char** outField);
@@ -259,6 +260,8 @@ struct MGPipeHasFieldVerifier<MGPStorageBlockBinding> : std::true_type {};
template <> template <>
struct MGPipeHasFieldVerifier<MGPCopyFromFramebuffer> : std::true_type {}; struct MGPipeHasFieldVerifier<MGPCopyFromFramebuffer> : std::true_type {};
template <> template <>
struct MGPipeHasFieldVerifier<MGPApplierReset> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<RenderStateParameters> : std::true_type {}; struct MGPipeHasFieldVerifier<RenderStateParameters> : std::true_type {};
template <> template <>
struct MGPipeHasFieldVerifier<PixelStoreParameters> : std::true_type {}; struct MGPipeHasFieldVerifier<PixelStoreParameters> : std::true_type {};
@@ -662,6 +665,11 @@ inline Bool MGPipeVerify(const MGPCopyFromFramebuffer& a, const MGPCopyFromFrame
return true; return true;
} }
inline Bool MGPipeVerify(const MGPApplierReset& a, const MGPApplierReset& b, const char** outField) {
MGP_FIELDS_MGPApplierReset(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const RenderStateParameters& a, const RenderStateParameters& b, const char** outField) { inline Bool MGPipeVerify(const RenderStateParameters& a, const RenderStateParameters& b, const char** outField) {
MGP_FIELDS_RenderStateParameters(MGP_VERIFY_FIELD) MGP_FIELDS_RenderStateParameters(MGP_VERIFY_FIELD)
return true; return true;
@@ -709,4 +717,4 @@ inline Bool MGPipeVerify(const MGPVertexBindingPointWire& a, const MGPVertexBind
#undef MGP_VERIFY_FIELD #undef MGP_VERIFY_FIELD
inline constexpr SizeT kMGPipeVerifiedPayloadCount = 77; inline constexpr SizeT kMGPipeVerifiedPayloadCount = 78;
+27 -1
View File
@@ -146,7 +146,9 @@ enum class MGPWireOp : Uint16 {
BindStreamOutput = 74, BindStreamOutput = 74,
SetStorageBlockBinding = 75, SetStorageBlockBinding = 75,
CopyFramebufferToTexture = 76, CopyFramebufferToTexture = 76,
kOpCount = 77, ApplierReset = 77,
ObjectDeath = 78,
kOpCount = 79,
}; };
// THE FLAGS, EXPORTED ONCE, INDEXED BY OPCODE (P5 R-13.4). MGPWireRecHeader::Flags is // THE FLAGS, EXPORTED ONCE, INDEXED BY OPCODE (P5 R-13.4). MGPWireRecHeader::Flags is
@@ -244,6 +246,8 @@ inline constexpr Uint32 kMGPipeCallFlags[static_cast<SizeT>(MGPWireOp::kOpCount)
/* 74 BindStreamOutput */ static_cast<Uint32>(kNone), /* 74 BindStreamOutput */ static_cast<Uint32>(kNone),
/* 75 SetStorageBlockBinding */ static_cast<Uint32>(kHasBlob), /* 75 SetStorageBlockBinding */ static_cast<Uint32>(kHasBlob),
/* 76 CopyFramebufferToTexture*/ static_cast<Uint32>(kNone), /* 76 CopyFramebufferToTexture*/ static_cast<Uint32>(kNone),
/* 77 ApplierReset */ static_cast<Uint32>(kNone),
/* 78 ObjectDeath */ static_cast<Uint32>(kNone),
}; };
static_assert(sizeof(kMGPipeCallFlags) / sizeof(kMGPipeCallFlags[0]) == static_assert(sizeof(kMGPipeCallFlags) / sizeof(kMGPipeCallFlags[0]) ==
static_cast<SizeT>(MGPWireOp::kOpCount), static_cast<SizeT>(MGPWireOp::kOpCount),
@@ -905,6 +909,22 @@ static_assert(sizeof(MGPWireRec_CopyFramebufferToTexture) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPCopyFromFramebuffer) + 7u) & ~SizeT(7u)), ((sizeof(MGPWireRecHeader) + sizeof(MGPCopyFromFramebuffer) + 7u) & ~SizeT(7u)),
"MGPWireRec_CopyFramebufferToTexture gained padding; the wire format moved"); "MGPWireRec_CopyFramebufferToTexture gained padding; the wire format moved");
struct alignas(8) MGPWireRec_ApplierReset {
MGPWireRecHeader Header;
MGPApplierReset Payload;
};
static_assert(sizeof(MGPWireRec_ApplierReset) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPApplierReset) + 7u) & ~SizeT(7u)),
"MGPWireRec_ApplierReset gained padding; the wire format moved");
struct alignas(8) MGPWireRec_ObjectDeath {
MGPWireRecHeader Header;
MGPHandleOnly Payload;
};
static_assert(sizeof(MGPWireRec_ObjectDeath) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPHandleOnly) + 7u) & ~SizeT(7u)),
"MGPWireRec_ObjectDeath gained padding; the wire format moved");
[[noreturn]] inline void MGPipeWireProtocolFatal(const char* call, Uint64 size, Uint64 remaining) { [[noreturn]] inline void MGPipeWireProtocolFatal(const char* call, Uint64 size, Uint64 remaining) {
MGLOG_F("MGPipe: protocol corruption applying %s: size=%llu remaining=%llu", call, MGLOG_F("MGPipe: protocol corruption applying %s: size=%llu remaining=%llu", call,
static_cast<unsigned long long>(size), static_cast<unsigned long long>(remaining)); static_cast<unsigned long long>(size), static_cast<unsigned long long>(remaining));
@@ -1182,6 +1202,12 @@ inline Bool MGPipeApplyWireRecord(MGPWireOp op, const void* record, Uint64 size,
case MGPWireOp::CopyFramebufferToTexture: case MGPWireOp::CopyFramebufferToTexture:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_CopyFramebufferToTexture, "CopyFramebufferToTexture"); MGP_WIRE_CHECK_BOUNDS(MGPWireRec_CopyFramebufferToTexture, "CopyFramebufferToTexture");
break; break;
case MGPWireOp::ApplierReset:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_ApplierReset, "ApplierReset");
break;
case MGPWireOp::ObjectDeath:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_ObjectDeath, "ObjectDeath");
break;
case MGPWireOp::kInvalid: case MGPWireOp::kInvalid:
case MGPWireOp::kOpCount: case MGPWireOp::kOpCount:
default: default:
+79
View File
@@ -38,6 +38,10 @@
#include <MG_Pipe/MGPipe.h> #include <MG_Pipe/MGPipe.h>
#include <MG_Pipe/PipeRoute.h> #include <MG_Pipe/PipeRoute.h>
// P5c (ct): EmitObjectDeathRecord resolves the dying object's handle in the CLIENT's own
// allocator - a client surface, asked on the client thread, which is the one place the lookup
// is legal under rule E (CONTRACT-P5C.md §5.2).
#include <MG_Impl/Pipe/SlotAllocator.h>
#include <MG_State/GLState/ProgramState/ProgramArtifactsCodec.h> #include <MG_State/GLState/ProgramState/ProgramArtifactsCodec.h>
#include <MG_Util/Debug/Log.h> #include <MG_Util/Debug/Log.h>
@@ -503,6 +507,81 @@ namespace MobileGL::MG_Remote::Client {
} // namespace } // namespace
// =====================================================================================
// P5c (ct), CONTRACT-P5C.md §5: the two control records' producers
// =====================================================================================
namespace {
// The ContextSerial of the NEXT applier_reset record. NOWHERE IN THE FRONTEND NUMBERS
// A MAKE-CURRENT: the tracker knows the EDGE (FreshlyPrimed) and no session or context
// carries a serial for it (checked at the contract commit), so the serial is this
// client's own count of applier_reset emissions - 0 for the first make-current, one
// more per primed edge. That is the only value the server can hold the client to in a
// one-context session: the sink keeps the same count and ASSERTS equality
// (CONTRACT-P5C.md §1: the value is asserted, never dispatched on; P6's multi-context
// shape is what will read it for real). GL-thread only, like every emitter here.
Uint64 g_applierResetContextSerial = 0;
} // namespace
Bool EmitApplierResetRecord() {
// NO SESSION, NO RECORD - and false rather than the RequireSession Fatal, because a
// validate can legitimately prime with a transport CONFIGURED but no live session:
// the bring-up window before ClientSession::Start() (§6 layer 2's documented
// exception), and the server-role-only shape a ServerLoop fixture drives with
// Transport=InProcess and no client at all. In both this process has no wire for the
// reset to cross, and the caller answers with the direct call the record replaced.
ClientSession* session = ClientSession::Active();
if (session == nullptr || !session->Started()) return false;
if (g_clientTablesUninstalled.load(std::memory_order_acquire)) return false;
MG_Pipe::MGPApplierReset record{};
record.ContextSerial = g_applierResetContextSerial++;
session->EmitAndWait(MGPWireOp::ApplierReset, &record, sizeof(record), nullptr, 0, nullptr,
0, nullptr);
++g_emitted;
return true;
}
ObjectDeathEmit EmitObjectDeathRecord(MG_Pipe::MGPipeKind kind, Uint64 lifetimeId) {
using MG_Pipe::MGPipeHandle;
// NO SESSION, NO RECORD - and that is NOT the RequireSession shape on purpose. A death
// notice can outlive the session by construction: frontend objects die at context
// teardown and at process exit, after Stop has joined the apply thread and freed the
// rings, and the twins this record would kill died with the server. Emitting into that
// window is a use-after-free; Fatal-ing on it is an abort at exit() for a legal death.
// NoSession is its own answer (not folded into NoHandle) because the caller's fallback
// for "no wire exists" is delivery to the server loop this process still has, while
// "no handle" means there is nothing to deliver at all.
ClientSession* session = ClientSession::Active();
if (session == nullptr || !session->Started()) return ObjectDeathEmit::NoSession;
// The same window, one step earlier: Stop has raised the teardown refusal but not yet
// joined the apply thread. A death landing here is destructor-driven, not an app bug,
// and the twin it would kill dies with the backend Stop is destroying - so it is
// skipped, where a routed call in the same window is Fatal{ClientTablesUninstalled}.
if (g_clientTablesUninstalled.load(std::memory_order_acquire)) {
return ObjectDeathEmit::NoSession;
}
// THE CLIENT'S OWN ALLOCATOR, ON THE CLIENT'S OWN THREAD (§5.2 step 1). A lifetime id
// the allocator cannot resolve names an object that never crossed - no create record
// ever carried its handle, so the server has no twin to kill and NOTHING is emitted.
// This replaces the mailbox's unconditional delivery, which hopped every death to the
// apply thread whether or not the server had ever seen the object.
const MGPipeHandle handle = MG_Pipe::MGPipeSlots().FindByLifetimeId(kind, lifetimeId);
if (MG_Pipe::MGPipeHandleIsNull(handle)) return ObjectDeathEmit::NoHandle;
MG_Pipe::MGPHandleOnly record{};
record.Handle = handle;
record.Kind = static_cast<Uint32>(kind);
// The WAIT is the only property the blocking mailbox hop provided and the only one P5c
// keeps (§5.2 step 3): it orders the death against in-flight verbs that name the
// handle, so the server's twin cannot be released underneath a verb that is using it.
session->EmitAndWait(MGPWireOp::ObjectDeath, &record, sizeof(record), nullptr, 0, nullptr,
0, nullptr);
++g_emitted;
return ObjectDeathEmit::Emitted;
}
void RequireClientTablesInstalled(const char* row) { void RequireClientTablesInstalled(const char* row) {
if (g_clientTablesUninstalled.load(std::memory_order_acquire)) { if (g_clientTablesUninstalled.load(std::memory_order_acquire)) {
MGLOG_F("MGPipe: Fatal{ClientTablesUninstalled, \"%s\"} - the client tables are " MGLOG_F("MGPipe: Fatal{ClientTablesUninstalled, \"%s\"} - the client tables are "
+29
View File
@@ -35,6 +35,8 @@
#pragma once #pragma once
#include <Includes.h> #include <Includes.h>
#include <MG_Pipe/MGPipeHandles.h>
#if MOBILEGL_BUILD_DISAGGREGATED #if MOBILEGL_BUILD_DISAGGREGATED
namespace MobileGL::MG_Remote::Client { namespace MobileGL::MG_Remote::Client {
@@ -86,6 +88,33 @@ namespace MobileGL::MG_Remote::Client {
// server otherwise). Declared here so PipeFill does not have to include a server header. // server otherwise). Declared here so PipeFill does not have to include a server header.
Bool RunsAsTheServerRole(); Bool RunsAsTheServerRole();
// ---- P5c (ct), CONTRACT-P5C.md §5: the two control records' client halves --------------
//
// NOT ROUTED ROWS - the two are appended to PipeCalls.def with no MGPipeApply* entry point
// and no table slot at all, so their producers call these directly. PipeFill.cpp's
// FreshlyPrimed arm calls the first; DirectGLES' OnFrontendStateObjectDestroyed calls the
// second.
// applier_reset (§5.1). Emitted on the GL thread at the tracker.FreshlyPrimed() edge,
// BEFORE the block's client-side resets, so the record's barrier orders the server's
// MGPipeApplierReset() against every verb that follows. ContextSerial is the client's own
// count of applier_reset emissions (0 = the first make-current): nothing in the frontend
// numbers a make-current today, and the sink asserts the value against its own count of
// the same records rather than dispatching on it (P5c: one context per session; P6 reads
// it for real). Returns false - nothing emitted - when no live session could carry the
// record (the bring-up window before Start, a server-role-only fixture, teardown); the
// caller then makes the direct call the record replaced.
Bool EmitApplierResetRecord();
// The three answers a death notice can produce (§5.2): the record crossed; the client's
// own allocator cannot resolve the dying object (the server never saw it, so there is no
// twin to kill and NOTHING crossed); or no live session could carry the record. The
// third is NOT folded into the second: a server role with no wire at all - a ServerLoop
// fixture's shape, never a real split's - still has a server loop the caller delivers to,
// where a handle that never existed has nothing to deliver.
enum class ObjectDeathEmit : Uint8 { Emitted, NoHandle, NoSession };
ObjectDeathEmit EmitObjectDeathRecord(MG_Pipe::MGPipeKind kind, Uint64 lifetimeId);
} // namespace MobileGL::MG_Remote::Client } // namespace MobileGL::MG_Remote::Client
#endif // MOBILEGL_BUILD_DISAGGREGATED #endif // MOBILEGL_BUILD_DISAGGREGATED
+65
View File
@@ -14,6 +14,10 @@
#include <Config.h> #include <Config.h>
#include <MG_Backend/MGPipe/PipeInputs.h> #include <MG_Backend/MGPipe/PipeInputs.h>
// P5c ct: object_death's per-kind release names the Espryt twin tables (CONTRACT-P5C.md
// §5.2). The same dependency ServerLoop.cpp already takes for CreateBackend; a server built
// on Magma simply holds no twins in these tables and every release resolves to nothing.
#include <MG_Backend/DirectGLES/Managers.h>
#include <MG_Remote/Client/ClientSession.h> #include <MG_Remote/Client/ClientSession.h>
#include <MG_Pipe/PipeApply.h> #include <MG_Pipe/PipeApply.h>
#include <MG_Util/Converters/GLToMG/TextureEnumConverter.h> #include <MG_Util/Converters/GLToMG/TextureEnumConverter.h>
@@ -942,6 +946,67 @@ namespace MobileGL::MG_Remote::Server {
return true; return true;
} }
// ---- P5c ct (MG_Remote/CONTRACT-P5C.md §5) ------------------------------------------
//
// TWO CONTROL RECORDS, NO BACKEND TABLE AND NO DECLINE ARM. Neither body consults
// Table(): the reset belongs to the applier this process owns, and the death release
// belongs to the twin tables - a backend that registered no slots (Magma's XFB shape)
// still has an applier to reset and still answers a death with the same generation
// check. A record that cannot be proved is Fatal, not declined: both refusals are
// ProtocolCorruption because by the time the sink runs the codec has already proved the
// record's SHAPE, and what is left to check are the contract facts about the peer (§1).
Bool ServerVerbSink::OnApplierReset(const MG_Pipe::MGPApplierReset& reset) {
// §1: the serial is ASSERTED, never dispatched on. P5c has exactly one context per
// session, so the only legal sequence is 0, 1, 2, ... and the session's own count of
// accepted resets IS the expected value; anything else means the two ends disagree
// about how many make-current edges have crossed, which no backend answer can fix.
if (reset.ContextSerial != m_applierResetSerial) {
MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"ApplierReset.ContextSerial\"} - the "
"record carries %llu and this session has accepted %llu reset(s); the "
"serial is asserted against the session's own count, not dispatched on "
"(one context per session in P5c)",
static_cast<unsigned long long>(reset.ContextSerial),
static_cast<unsigned long long>(m_applierResetSerial));
std::abort();
}
++m_applierResetSerial;
// THE WHOLE POINT OF THE RECORD: the reset runs HERE, on the apply thread, against
// the g_applier this role owns (PipeApply.cpp:409). The layer-2 guard inside
// MGPipeApplierReset passes because this IS the apply thread; the GL-thread direct
// call it replaced is the Fatal arm.
MG_Pipe::MGPipeApplierReset();
++m_applierResets;
return true;
}
Bool ServerVerbSink::OnObjectDeath(const MG_Pipe::MGPHandleOnly& death) {
// §1's zero ruling: a null handle means "the object never crossed", and the client
// emits NOTHING in that case (§5.2) - so a null handle arriving here is corruption,
// not a no-op.
if (MG_Pipe::MGPipeHandleIsNull(death.Handle)) {
MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"ObjectDeath.Handle\"} - a null "
"handle never crosses: the client emits nothing for an object its own "
"allocator cannot resolve (CONTRACT-P5C.md §5.2)");
std::abort();
}
if (death.Kind >= static_cast<Uint32>(MG_Pipe::MGPipeKind::KindCount)) {
MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"ObjectDeath.Kind\"} - %u is not an "
"MGPipeKind",
static_cast<unsigned>(death.Kind));
std::abort();
}
// The per-kind release, keyed by the handle the record carried. A false answer is
// NOT a decline: the kind's own delete opcode may already have released the twin
// (the idempotent second path every notice arm documents), and a kind this backend
// does not twin (Buffer, whose death crosses as resource_destroy) legally resolves
// to nothing.
MG_Backend::DirectGLES::ReleaseTwinsForWireObjectDeath(
death.Handle, static_cast<MG_Pipe::MGPipeKind>(death.Kind));
++m_objectDeaths;
return true;
}
// ----------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------
// PipeApplier // PipeApplier
// ----------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------
+34
View File
@@ -150,6 +150,32 @@ namespace MobileGL::MG_Remote::Server {
Bool OnGenerateMipmap(const MG_Pipe::MGPMipPlan& plan) override; Bool OnGenerateMipmap(const MG_Pipe::MGPMipPlan& plan) override;
Bool OnCopyFramebufferToTexture(const MG_Pipe::MGPCopyFromFramebuffer& copy) override; Bool OnCopyFramebufferToTexture(const MG_Pipe::MGPCopyFromFramebuffer& copy) override;
// ---- P5c ct (MG_Remote/CONTRACT-P5C.md §5): the two control records ----------------
//
// REAL BODIES FROM THE DAY THE ROWS EXIST, not the P5b stub shape: the rows were
// appended by the same package that lands these bodies, so there is no window in
// which a client can emit ahead of its server half.
//
// OnApplierReset (§5.1): the make-current edge's server half. ContextSerial is
// ASSERTED against this session's own count of applier_reset records - one context
// per session in P5c, so the legal sequence is 0, 1, 2, ... - and only then is
// MGPipeApplierReset() run, here, on the apply thread that owns g_applier.
// OnObjectDeath (§5.2): a null handle never crosses (the client emits nothing for an
// object its allocator cannot resolve), so one arriving is
// Fatal{ProtocolCorruption, "ObjectDeath.Handle"}; otherwise the per-kind release
// runs by the record's handle (SlotTables.h's ReleaseTwinByHandle).
Bool OnApplierReset(const MG_Pipe::MGPApplierReset& reset) override;
Bool OnObjectDeath(const MG_Pipe::MGPHandleOnly& death) override;
// P5c ct's tallies, for the same reason every other row's tally exists (R-16: a probe
// may not arm against a stub). ApplierResets counts the records ACCEPTED (serial
// checked, reset run); ObjectDeaths counts every record the sink dispatched.
Uint64 ApplierResets() const { return m_applierResets; }
Uint64 ObjectDeaths() const { return m_objectDeaths; }
// The ContextSerial the NEXT applier_reset record must carry. Exposed so a case can
// assert the sequence rather than only the count.
Uint64 ExpectedApplierResetSerial() const { return m_applierResetSerial; }
// Per-verb tallies. The lane asserts these moved, because "the scenario passed" on a // Per-verb tallies. The lane asserts these moved, because "the scenario passed" on a
// split build is also what a scenario that ran entirely on the monolith path looks // split build is also what a scenario that ran entirely on the monolith path looks
// like (R-16: a probe may not arm against a stub). // like (R-16: a probe may not arm against a stub).
@@ -231,6 +257,14 @@ namespace MobileGL::MG_Remote::Server {
Uint64 m_streamOutputControls = 0; Uint64 m_streamOutputControls = 0;
Uint64 m_streamOutputBinds = 0; Uint64 m_streamOutputBinds = 0;
Uint64 m_patchParameters = 0; Uint64 m_patchParameters = 0;
// P5c ct. m_applierResetSerial is BOTH the expected ContextSerial of the next record
// and the count of accepted resets: the two are one number because the serial is the
// session's own count of applier_reset records (CONTRACT-P5C.md §1: asserted, never
// dispatched on). m_objectDeaths counts records dispatched, released twin or not -
// the idempotent second path answering "nothing held it" is a legal record.
Uint64 m_applierResetSerial = 0;
Uint64 m_applierResets = 0;
Uint64 m_objectDeaths = 0;
// ReadPixels' destination. The pixels go into the reply slot, but GLFunctionsTable:: // ReadPixels' destination. The pixels go into the reply slot, but GLFunctionsTable::
// ReadPixels writes into a caller buffer, so one staging vector per session sits // ReadPixels writes into a caller buffer, so one staging vector per session sits
// between them. Grown, never shrunk, and never handed out past the call. // between them. Grown, never shrunk, and never handed out past the call.
+17 -1
View File
@@ -239,7 +239,9 @@ namespace MobileGL::MG_Remote::Wire {
X(PatchParameter, MGPPatchParameter) \ X(PatchParameter, MGPPatchParameter) \
X(BindStreamOutput, MGPStreamOutputBind) \ X(BindStreamOutput, MGPStreamOutputBind) \
X(SetStorageBlockBinding, MGPStorageBlockBinding) \ X(SetStorageBlockBinding, MGPStorageBlockBinding) \
X(CopyFramebufferToTexture, MGPCopyFromFramebuffer) X(CopyFramebufferToTexture, MGPCopyFromFramebuffer) \
X(ApplierReset, MGPApplierReset) \
X(ObjectDeath, MGPHandleOnly)
namespace { namespace {
@@ -2085,6 +2087,20 @@ namespace MobileGL::MG_Remote::Wire {
m_verbs->OnCopyFramebufferToTexture( m_verbs->OnCopyFramebufferToTexture(
*static_cast<const MGPCopyFromFramebuffer*>(payload)); *static_cast<const MGPCopyFromFramebuffer*>(payload));
// ---- P5c's two control records, opcodes 77..78 (MG_Remote/CONTRACT-P5C.md §5) --------
//
// Fixed-size PODs the bounds gate has already proved; neither carries a blob, a tail
// or a reply, so each arm hands over and stops. ObjectDeath's null-handle refusal is
// the sink's, not the codec's: the codec proves SHAPES, and "the client emits nothing
// for an object that never crossed" (§5.2) is a contract fact about the peer.
case MGPWireOp::ApplierReset:
return m_verbs != nullptr &&
m_verbs->OnApplierReset(*static_cast<const MGPApplierReset*>(payload));
case MGPWireOp::ObjectDeath:
return m_verbs != nullptr &&
m_verbs->OnObjectDeath(*static_cast<const MGPHandleOnly*>(payload));
case MGPWireOp::SetSwapInterval: case MGPWireOp::SetSwapInterval:
// Class C, wave 3 (census-classC.md "static cross"); not a verb (FillPoints.def:21). // Class C, wave 3 (census-classC.md "static cross"); not a verb (FillPoints.def:21).
return false; return false;
+19
View File
@@ -534,6 +534,25 @@ namespace MobileGL::MG_Remote::Wire {
(void)copy; (void)copy;
return false; return false;
} }
// ---- P5c (MG_Remote/CONTRACT-P5C.md §5): the two control records, opcodes 77..78.
//
// Same hand-over shape as the P5b rows: the codec validates the record (a fixed-size
// POD, no blob, no tail) and the SERVER'S sink does the work - OnApplierReset runs the
// server's own MGPipeApplierReset() after asserting ContextSerial against the
// session's (§5.1: ASSERTED, never dispatched on, P5c has one context per session),
// and OnObjectDeath releases the kind's twin table by the handle the record carried
// (§5.2). The default bodies return false ("this build does not implement it"), so a
// unit decoder without a server declines by the same answer every other unimplemented
// row gives.
virtual Bool OnApplierReset(const MG_Pipe::MGPApplierReset& reset) {
(void)reset;
return false;
}
virtual Bool OnObjectDeath(const MG_Pipe::MGPHandleOnly& death) {
(void)death;
return false;
}
}; };
// Not thread safe: one decoder on the apply thread, by construction. // Not thread safe: one decoder on the apply thread, by construction.
+37 -9
View File
@@ -125,11 +125,13 @@ TEST(PipeCatalogue, GeneratedTablesHoldTheWholeCatalogue) {
EXPECT_EQ(kMGPipeContextCallCount, kMGPipeCallCount - ClassCount<kScreen>()); EXPECT_EQ(kMGPipeContextCallCount, kMGPipeCallCount - ClassCount<kScreen>());
// The per-class counts PipeCalls.def documents in its header. // The per-class counts PipeCalls.def documents in its header.
EXPECT_EQ(ClassCount<kScreen>(), 11u); // kScreen is 11 + P5c's applier_reset (MG_Remote/CONTRACT-P5C.md §5.1); kCtxObject is
// 9 + P5c's object_death (§5.2), the framebuffer family's first wire delete opcode.
EXPECT_EQ(ClassCount<kScreen>(), 12u);
EXPECT_EQ(ClassCount<kCtxQuery>(), 8u); EXPECT_EQ(ClassCount<kCtxQuery>(), 8u);
EXPECT_EQ(ClassCount<kCtxCso>(), 13u); EXPECT_EQ(ClassCount<kCtxCso>(), 13u);
EXPECT_EQ(ClassCount<kCtxState>(), 17u); EXPECT_EQ(ClassCount<kCtxState>(), 17u);
EXPECT_EQ(ClassCount<kCtxObject>(), 9u); EXPECT_EQ(ClassCount<kCtxObject>(), 10u);
// 13 + the five P5b-appended verbs (MG_Remote/CONTRACT-P5B.md): bind_shader_image, // 13 + the five P5b-appended verbs (MG_Remote/CONTRACT-P5B.md): bind_shader_image,
// patch_parameter, bind_stream_output, set_storage_block_binding, // patch_parameter, bind_stream_output, set_storage_block_binding,
// copy_framebuffer_to_texture. // copy_framebuffer_to_texture.
@@ -139,8 +141,9 @@ TEST(PipeCatalogue, GeneratedTablesHoldTheWholeCatalogue) {
// A row nobody has migrated is null - which is exactly what "this subsystem has not been // A row nobody has migrated is null - which is exactly what "this subsystem has not been
// migrated, keep pulling" means (plan B section 4.1). // migrated, keep pulling" means (plan B section 4.1).
// //
// UNTIL P5 R-17 THAT WAS EVERY ROW, and this case said so. It is now EXACTLY THE 34 ROWS WITH // UNTIL P5 R-17 THAT WAS EVERY ROW, and this case said so. It is now EXACTLY THE 41 ROWS WITH
// NO MGPipeApply* ENTRY POINT: the other 37 have an applier, R-17 installs adapters over them, // NO MGPipeApply* ENTRY POINT (78 - the 37 that have one; the number was 34 at P5, 39 after
// P5b's five sink-only verbs): the other 37 have an applier, R-17 installs adapters over them,
// and a null there would no longer mean "keep pulling" - `MG_Impl/Pipe`'s call sites go through // and a null there would no longer mean "keep pulling" - `MG_Impl/Pipe`'s call sites go through
// the thunks, so a null would mean "call through a null pointer". The number is asserted rather // the thunks, so a null would mean "call through a null pointer". The number is asserted rather
// than the emptiness, because "37 installed" and "34 still null" are the two halves of a // than the emptiness, because "37 installed" and "34 still null" are the two halves of a
@@ -156,10 +159,12 @@ TEST(PipeCatalogue, UninstalledTablesAreAllNull) {
const void* const* context = reinterpret_cast<const void* const*>(&gMGPipeContext); const void* const* context = reinterpret_cast<const void* const*>(&gMGPipeContext);
#if MOBILEGL_PIPE_PUSH #if MOBILEGL_PIPE_PUSH
MGPipeInstallMonolithTables(); MGPipeInstallMonolithTables();
// The 34 rows with no MGPipeApply* entry point are still null, and null still means "this // The 41 rows with no MGPipeApply* entry point are still null, and null still means "this
// subsystem has not been migrated, keep pulling". Named rather than counted, because the // subsystem has not been migrated, keep pulling". Named rather than counted, because the
// count is the other case's job and two cases asserting the same number would both go red // count is the other case's job and two cases asserting the same number would both go red
// for one change. // for one change. P5c's two control records are among them by design (CONTRACT-P5C.md §5:
// no MGPipeApply* entry point, no monolith producer - under a transport they reach
// WireVerbSink instead).
EXPECT_EQ(gMGPipeContext.SetShaderBuffers, nullptr); EXPECT_EQ(gMGPipeContext.SetShaderBuffers, nullptr);
EXPECT_EQ(gMGPipeContext.SetStreamOutputTargets, nullptr); EXPECT_EQ(gMGPipeContext.SetStreamOutputTargets, nullptr);
EXPECT_EQ(gMGPipeContext.DrawVbo, nullptr); EXPECT_EQ(gMGPipeContext.DrawVbo, nullptr);
@@ -168,6 +173,8 @@ TEST(PipeCatalogue, UninstalledTablesAreAllNull) {
EXPECT_EQ(gMGPipeScreen.GetCaps, nullptr); EXPECT_EQ(gMGPipeScreen.GetCaps, nullptr);
EXPECT_EQ(gMGPipeContext.QueryCreate, nullptr); EXPECT_EQ(gMGPipeContext.QueryCreate, nullptr);
EXPECT_EQ(gMGPipeScreen.FenceCreate, nullptr); EXPECT_EQ(gMGPipeScreen.FenceCreate, nullptr);
EXPECT_EQ(gMGPipeScreen.ApplierReset, nullptr);
EXPECT_EQ(gMGPipeContext.ObjectDeath, nullptr);
#else #else
// A pull build compiles no applier and no routing, so the pre-migration statement is the // A pull build compiles no applier and no routing, so the pre-migration statement is the
// whole truth there and this case is the one that says so. // whole truth there and this case is the one that says so.
@@ -235,6 +242,12 @@ TEST(PipeCatalogue, ExactlyTheRoutedRowsAreInstalledAndTheRestAreStillNull) {
EXPECT_EQ(gMGPipeContext.Present, nullptr); EXPECT_EQ(gMGPipeContext.Present, nullptr);
EXPECT_EQ(gMGPipeContext.SetSwapInterval, nullptr); EXPECT_EQ(gMGPipeContext.SetSwapInterval, nullptr);
EXPECT_EQ(gMGPipeScreen.GetCaps, nullptr); EXPECT_EQ(gMGPipeScreen.GetCaps, nullptr);
// P5c's two control records (CONTRACT-P5C.md §5) take the same answer for a different
// reason: no MGPipeApply* exists for either and none may be installed - under a transport
// they cross to WireVerbSink, under monolith the GL thread's direct call and the death
// notice's mailbox are the producers, byte for byte as before (G1/G2).
EXPECT_EQ(gMGPipeScreen.ApplierReset, nullptr);
EXPECT_EQ(gMGPipeContext.ObjectDeath, nullptr);
#else #else
// A pull build compiles no applier and no routing, so the pre-migration statement is still // A pull build compiles no applier and no routing, so the pre-migration statement is still
// the whole truth there. // the whole truth there.
@@ -557,7 +570,15 @@ TEST(PipeCatalogue, LateArrivalsAreAppendedWithoutRenumbering) {
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::BindStreamOutput), 74); EXPECT_EQ(static_cast<Uint16>(MGPWireOp::BindStreamOutput), 74);
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::SetStorageBlockBinding), 75); EXPECT_EQ(static_cast<Uint16>(MGPWireOp::SetStorageBlockBinding), 75);
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::CopyFramebufferToTexture), 76); EXPECT_EQ(static_cast<Uint16>(MGPWireOp::CopyFramebufferToTexture), 76);
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::kOpCount), 77); // P5c (MG_Remote/CONTRACT-P5C.md §5) appended the two control records AFTER P5b's five, by
// the same rule: opcodes 77..78, and nothing before them moved. applier_reset is a kScreen
// row (a make-current is a whole-server edge, exactly as FenceWaitServer is a screen call)
// and object_death a kCtxObject one; both carry no blob, no reply and no tail.
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::ApplierReset), 77);
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::ObjectDeath), 78);
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::kOpCount), 79);
EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::ApplierReset), static_cast<Uint32>(kNone));
EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::ObjectDeath), static_cast<Uint32>(kNone));
// And the P5b rows carry what their contract says: one blob (the block name) and nothing // And the P5b rows carry what their contract says: one blob (the block name) and nothing
// else, and the extended draw row keeps its two flags. // else, and the extended draw row keeps its two flags.
EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::SetStorageBlockBinding), static_cast<Uint32>(kHasBlob)); EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::SetStorageBlockBinding), static_cast<Uint32>(kHasBlob));
@@ -575,6 +596,11 @@ TEST(PipeCatalogue, LateArrivalsAreAppendedWithoutRenumbering) {
EXPECT_EQ(sizeof(MGPCopyFromFramebuffer), 48u); EXPECT_EQ(sizeof(MGPCopyFromFramebuffer), 48u);
EXPECT_EQ(sizeof(MGPCopyRegion), 72u); EXPECT_EQ(sizeof(MGPCopyRegion), 72u);
EXPECT_EQ(sizeof(MGPDrawIndirect), 40u); EXPECT_EQ(sizeof(MGPDrawIndirect), 40u);
// The P5c payload, pinned like every other MGP_ASSERT_POD at runtime: one Uint64, no
// padding. object_death REUSES MGPHandleOnly (CONTRACT-P5C.md §1), so there is no second
// struct to pin - the 16 bytes are pinned above with the handle family.
EXPECT_EQ(sizeof(MGPApplierReset), 8u);
EXPECT_EQ(sizeof(MGPHandleOnly), 16u);
// The two draw-flag bits P5b's d1 arms are exclusive by contract and distinct by value. // The two draw-flag bits P5b's d1 arms are exclusive by contract and distinct by value.
EXPECT_EQ(static_cast<Uint32>(kDrawIsIndirect), 1u << 5); EXPECT_EQ(static_cast<Uint32>(kDrawIsIndirect), 1u << 5);
EXPECT_EQ(static_cast<Uint32>(kDrawHasUserIndices) & static_cast<Uint32>(kDrawIsIndirect), 0u); EXPECT_EQ(static_cast<Uint32>(kDrawHasUserIndices) & static_cast<Uint32>(kDrawIsIndirect), 0u);
@@ -771,8 +797,10 @@ TEST(PipeCatalogue, FloatVectorsCompareBitwise) {
TEST(PipeCatalogue, SixValueStructsHaveFieldLists) { TEST(PipeCatalogue, SixValueStructsHaveFieldLists) {
// 72 through P5; P5b appended five call payloads (MG_Remote/CONTRACT-P5B.md: MGPImageBind, // 72 through P5; P5b appended five call payloads (MG_Remote/CONTRACT-P5B.md: MGPImageBind,
// MGPPatchParameter, MGPStreamOutputBind, MGPStorageBlockBinding, MGPCopyFromFramebuffer), // MGPPatchParameter, MGPStreamOutputBind, MGPStorageBlockBinding, MGPCopyFromFramebuffer),
// each with its own field list, so the comparator sees every one of them: 77. // each with its own field list, so the comparator sees every one of them: 77. P5c appended
EXPECT_EQ(kMGPipeVerifiedPayloadCount, 77u); // applier_reset's MGPApplierReset (CONTRACT-P5C.md §5.1) - object_death reuses
// MGPHandleOnly, which has had a list since P0 - so: 78.
EXPECT_EQ(kMGPipeVerifiedPayloadCount, 78u);
static_assert(MGPipeHasFieldVerifier<RenderStateParameters>::value); static_assert(MGPipeHasFieldVerifier<RenderStateParameters>::value);
static_assert(MGPipeHasFieldVerifier<PixelStoreParameters>::value); static_assert(MGPipeHasFieldVerifier<PixelStoreParameters>::value);
static_assert(MGPipeHasFieldVerifier<PerBufferBlendState>::value); static_assert(MGPipeHasFieldVerifier<PerBufferBlendState>::value);
+265
View File
@@ -38,6 +38,10 @@
#include <Config.h> #include <Config.h>
#include <MG_Remote/Server/PipeApplier.h> #include <MG_Remote/Server/PipeApplier.h>
#include <MG_Backend/DirectGLES/BackendObject_DirectGLES.h> #include <MG_Backend/DirectGLES/BackendObject_DirectGLES.h>
// P5c ct: object_death's round trip releases REAL Espryt twin-table entries, so the suite
// drives the same registries the sink dispatches to (Managers.h). A suite that substituted a
// mock here would pin the dispatch and nothing about the release (R-16).
#include <MG_Backend/DirectGLES/Managers.h>
#include <MG_Pipe/MGPipe.h> #include <MG_Pipe/MGPipe.h>
#include <MG_Pipe/MGPipeRenderStateSpans.h> #include <MG_Pipe/MGPipeRenderStateSpans.h>
#include <MG_Pipe/PipeApply.h> #include <MG_Pipe/PipeApply.h>
@@ -268,6 +272,16 @@ namespace {
FramebufferCopies.push_back(copy); FramebufferCopies.push_back(copy);
return true; return true;
} }
// ---- P5c's rows (CONTRACT-P5C.md §5): the two control records, recorded the same
// way so a round trip asserts the arm and the record intact.
Bool OnApplierReset(const MGPApplierReset& reset) override {
ApplierResets.push_back(reset);
return true;
}
Bool OnObjectDeath(const MGPHandleOnly& death) override {
ObjectDeaths.push_back(death);
return true;
}
std::vector<MGPClear> Clears; std::vector<MGPClear> Clears;
std::vector<MGPBlit> Blits; std::vector<MGPBlit> Blits;
std::vector<MGPPresent> Presents; std::vector<MGPPresent> Presents;
@@ -293,6 +307,8 @@ namespace {
std::vector<MGPPatchParameter> Patches; std::vector<MGPPatchParameter> Patches;
std::vector<MGPMipPlan> MipPlans; std::vector<MGPMipPlan> MipPlans;
std::vector<MGPCopyFromFramebuffer> FramebufferCopies; std::vector<MGPCopyFromFramebuffer> FramebufferCopies;
std::vector<MGPApplierReset> ApplierResets;
std::vector<MGPHandleOnly> ObjectDeaths;
}; };
Replies& Answers() { return m_replies; } Replies& Answers() { return m_replies; }
@@ -1243,6 +1259,73 @@ TEST_F(PipeWireCodecTest, BindStreamOutputReachesTheSink) {
EXPECT_EQ(wire.Sink().StreamOutputBinds[0].LifetimeId, 0x1234567890ull); EXPECT_EQ(wire.Sink().StreamOutputBinds[0].LifetimeId, 0x1234567890ull);
} }
// =====================================================================================
// P5c ct (MG_Remote/CONTRACT-P5C.md §5): the two control records round-trip
// =====================================================================================
//
// Same shape as the P5b rows above: encode, pump, and assert the record reached the RIGHT
// sink method intact. What a serial ASSERT or a twin release does with the record is the
// SERVER sink's layer (SessionTest drives that); here the codec proves the bytes and the
// dispatch.
TEST_F(PipeWireCodecTest, ApplierResetReachesTheSinkWithItsSerial) {
Wire2 wire;
MGPApplierReset reset{};
reset.ContextSerial = 0; // 0 = the first make-current (CONTRACT-P5C.md §1)
ASSERT_NE(wire.Encoder().EncodeRecord(MGPWireOp::ApplierReset, &reset, sizeof(reset)),
kInvalidSeq);
bool applied = false;
ASSERT_TRUE(wire.PumpOne(&applied));
EXPECT_TRUE(applied);
ASSERT_EQ(wire.Sink().ApplierResets.size(), 1u);
EXPECT_EQ(wire.Sink().ApplierResets[0].ContextSerial, 0u);
// A second edge carries the next serial, and the two records arrive in order.
reset.ContextSerial = 1;
ASSERT_NE(wire.Encoder().EncodeRecord(MGPWireOp::ApplierReset, &reset, sizeof(reset)),
kInvalidSeq);
ASSERT_TRUE(wire.PumpOne(&applied));
EXPECT_TRUE(applied);
ASSERT_EQ(wire.Sink().ApplierResets.size(), 2u);
EXPECT_EQ(wire.Sink().ApplierResets[1].ContextSerial, 1u);
EXPECT_EQ(wire.Decoder().AppliedSeq(), 2u);
}
TEST_F(PipeWireCodecTest, ObjectDeathReachesTheSinkWithItsHandleAndKind) {
Wire2 wire;
// One record per kind the death switch handles (CONTRACT-P5C.md §1: one payload for all
// seven), because the kind is what the sink dispatches on and a row that widened it
// wrong would drop every death of that kind.
const MGPipeKind kinds[] = {
MGPipeKind::Texture, MGPipeKind::Framebuffer, MGPipeKind::Renderbuffer,
MGPipeKind::SamplerCso, MGPipeKind::ShaderCso, MGPipeKind::SamplerViewCso,
MGPipeKind::VertexElementsCso,
};
Uint32 slot = 40;
for (MGPipeKind kind : kinds) {
MGPHandleOnly death = HandleOnly(slot, kind);
ASSERT_NE(wire.Encoder().EncodeRecord(MGPWireOp::ObjectDeath, &death, sizeof(death)),
kInvalidSeq)
<< static_cast<Uint32>(kind);
++slot;
}
bool applied = false;
constexpr SizeT kKindCount = sizeof(kinds) / sizeof(kinds[0]);
for (SizeT i = 0; i < kKindCount; ++i) {
ASSERT_TRUE(wire.PumpOne(&applied)) << i;
EXPECT_TRUE(applied) << i;
}
ASSERT_EQ(wire.Sink().ObjectDeaths.size(), kKindCount);
slot = 40;
for (SizeT i = 0; i < kKindCount; ++i) {
EXPECT_EQ(wire.Sink().ObjectDeaths[i].Handle.Slot, slot) << i;
EXPECT_EQ(wire.Sink().ObjectDeaths[i].Handle.Gen, 1u) << i;
EXPECT_EQ(wire.Sink().ObjectDeaths[i].Kind, static_cast<Uint32>(kinds[i])) << i;
++slot;
}
EXPECT_EQ(wire.Decoder().AppliedSeq(), static_cast<Uint64>(kKindCount));
}
// ===================================================================================== // =====================================================================================
// P5b t2 (MG_Remote/CONTRACT-P5B.md §2 t2). c0b's cases above round-trip each row once; these // P5b t2 (MG_Remote/CONTRACT-P5B.md §2 t2). c0b's cases above round-trip each row once; these
// pin the fields t2's EMITTERS actually fill and the one ordering property the span family has. // pin the fields t2's EMITTERS actually fill and the one ordering property the span family has.
@@ -2893,3 +2976,185 @@ TEST(FenceWireRoundTrip, MissingConsumerDeclinesWithoutInventingASignaledAnswer)
EXPECT_EQ(wire.Answers().All[0].Status, ReplySink::kStatusDeclined); EXPECT_EQ(wire.Answers().All[0].Status, ReplySink::kStatusDeclined);
EXPECT_TRUE(wire.Answers().All[0].Bytes.empty()); EXPECT_TRUE(wire.Answers().All[0].Bytes.empty());
} }
// =====================================================================================
// P5c ct (MG_Remote/CONTRACT-P5C.md §5): the two control records, through the REAL sink
// =====================================================================================
//
// The codec cases above pin the bytes and the dispatch with a recording sink; these pin what
// the SERVER's sink does with the record: the serial assert, the applier reset that actually
// runs, the twin release that actually retires the table entry, and the layer-2 guard that
// turns the reverted GL-thread direct call red.
TEST(ApplierResetWireRoundTrip, TheSerialSequenceIsAssertedAndTheServerResetRuns) {
Wire2 wire;
Server::ServerVerbSink sink;
wire.Decoder().SetVerbSink(&sink);
auto send = [&](Uint64 serial) {
const MGPApplierReset reset{serial};
EXPECT_NE(wire.Encoder().EncodeRecord(MGPWireOp::ApplierReset, &reset, sizeof(reset)),
kInvalidSeq);
bool applied = false;
EXPECT_TRUE(wire.PumpOne(&applied));
EXPECT_TRUE(applied);
};
// The reset RUNS - asserted on the applier's own state, not on the tally (R-16: a probe
// may not arm against a stub, and a tally a stub could also move is a stub's witness).
// BoundVertexElements is one of the fields MGPipeApplierReset clears (PipeApply.cpp).
MGPipeApplier().BoundVertexElements = MakeHandle(77);
send(0); // 0 = the first make-current (CONTRACT-P5C.md §1)
EXPECT_TRUE(MGPipeHandleIsNull(MGPipeApplier().BoundVertexElements));
EXPECT_EQ(sink.ApplierResets(), 1u);
EXPECT_EQ(sink.ExpectedApplierResetSerial(), 1u);
// The second edge carries the next serial and is accepted in order.
MGPipeApplier().BoundVertexElements = MakeHandle(78);
send(1);
EXPECT_TRUE(MGPipeHandleIsNull(MGPipeApplier().BoundVertexElements));
EXPECT_EQ(sink.ApplierResets(), 2u);
EXPECT_EQ(sink.ExpectedApplierResetSerial(), 2u);
}
TEST(ObjectDeathWireRoundTrip, TheTextureAndFramebufferTwinsReleaseByHandleAndNeverByAStaleGeneration) {
Wire2 wire;
Server::ServerVerbSink sink;
wire.Decoder().SetVerbSink(&sink);
namespace gles = MG_Backend::DirectGLES;
// The handle arm answers only when kMGPipeSubsystemEsprytSlots is set, and a unit binary
// never runs ConfigLoader: Features.PipePush is 0 here, not the shipping default
// (kMGPipeSubsystemsMigratedAtP4a). Set the bit the way SanityTest's pipe fixtures do;
// the arm verdict latches on first use and this case is the first use in its process.
const Uint64 savedPush = MG_Config::Features.PipePush;
MG_Config::Features.PipePush = savedPush | kMGPipeSubsystemEsprytSlots;
struct RestorePush {
Uint64 bits;
~RestorePush() { MG_Config::Features.PipePush = bits; }
} restore{savedPush};
// Texture AND Framebuffer - the two kinds R-16 names for the death-recycle path. The
// others share the one dispatch and the one table walk, so two kinds pin the shape: one
// whose death also crosses as resource_destroy (Texture: the idempotent second path),
// and the kind whose FIRST wire delete opcode object_death is (Framebuffer).
struct KindCase {
MGPipeKind kind;
Uint32 slot;
};
const KindCase cases[] = {{MGPipeKind::Texture, 901}, {MGPipeKind::Framebuffer, 902}};
for (const KindCase& one : cases) {
const auto getOrCreate = [&](MGPipeHandle h) -> void* {
if (one.kind == MGPipeKind::Texture) {
return gles::TextureImpl::g_backendTextureObjects.GetOrCreateByHandle(h);
}
return gles::FramebufferImpl::g_backendFramebufferObjects.GetOrCreateByHandle(h);
};
const auto liveGen = [&](Uint32 slot) -> Uint32 {
if (one.kind == MGPipeKind::Texture) {
return gles::TextureImpl::g_backendTextureObjects.LiveGenAt(slot);
}
return gles::FramebufferImpl::g_backendFramebufferObjects.LiveGenAt(slot);
};
ASSERT_NE(getOrCreate(MakeHandle(one.slot, 1)), nullptr)
<< "the Espryt handle arm did not go live with kMGPipeSubsystemEsprytSlots set; "
"the verdict latched elsewhere in this process";
ASSERT_EQ(liveGen(one.slot), 1u) << static_cast<Uint32>(one.kind);
// The death crosses and the twin retires: the slot's live generation goes back to 0.
const Uint64 deathsBefore = sink.ObjectDeaths();
const MGPHandleOnly death{MakeHandle(one.slot, 1), static_cast<Uint32>(one.kind), 0};
EXPECT_NE(wire.Encoder().EncodeRecord(MGPWireOp::ObjectDeath, &death, sizeof(death)),
kInvalidSeq);
bool applied = false;
EXPECT_TRUE(wire.PumpOne(&applied));
EXPECT_TRUE(applied);
EXPECT_EQ(liveGen(one.slot), 0u) << static_cast<Uint32>(one.kind);
EXPECT_EQ(sink.ObjectDeaths(), deathsBefore + 1u);
// THE ABA HALF (LiveGenAt's semantics): the slot is recycled forward to a NEW object
// at generation 2, and the dead object's handle - replayed, as a duplicated or
// reordered record would replay it - must not answer for the new object. The record
// is still APPLIED (idempotency is legal; a generation mismatch is a no-op), but the
// new twin survives it.
ASSERT_NE(getOrCreate(MakeHandle(one.slot, 2)), nullptr);
ASSERT_EQ(liveGen(one.slot), 2u);
EXPECT_NE(wire.Encoder().EncodeRecord(MGPWireOp::ObjectDeath, &death, sizeof(death)),
kInvalidSeq);
ASSERT_TRUE(wire.PumpOne(&applied));
EXPECT_TRUE(applied);
EXPECT_EQ(liveGen(one.slot), 2u)
<< "a stale handle released the recycled slot's new twin (kind "
<< static_cast<Uint32>(one.kind) << ")";
// And the new object's own death retires it, leaving the table as the case found it.
const MGPHandleOnly newDeath{MakeHandle(one.slot, 2), static_cast<Uint32>(one.kind), 0};
EXPECT_NE(wire.Encoder().EncodeRecord(MGPWireOp::ObjectDeath, &newDeath, sizeof(newDeath)),
kInvalidSeq);
ASSERT_TRUE(wire.PumpOne(&applied));
EXPECT_TRUE(applied);
EXPECT_EQ(liveGen(one.slot), 0u);
}
}
#if MGTEST_HAVE_FORK
// The three Fatal arms, driven through the encoder and the REAL sink in a forked child (the
// file's rule: never EXPECT_DEATH here - it re-runs the whole binary and re-enters the
// applier's globals).
TEST(CtWireFatals, ASerialTheSessionCannotProveIsProtocolCorruption) {
const ChildResult r = RunInChild([] {
Wire2 wire;
Server::ServerVerbSink sink;
wire.Decoder().SetVerbSink(&sink);
// The FIRST reset a session sees must carry serial 0 (one context, the count is the
// session's own); leading with 1 is a sequence the session cannot prove.
const MGPApplierReset reset{1};
(void)wire.Encoder().EncodeRecord(MGPWireOp::ApplierReset, &reset, sizeof(reset));
bool applied = false;
(void)wire.PumpOne(&applied);
});
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
EXPECT_NE(r.Log.find("Fatal{ProtocolCorruption, \"ApplierReset.ContextSerial\"}"),
std::string::npos)
<< r.Log;
}
TEST(CtWireFatals, ANullDeathHandleIsProtocolCorruption) {
const ChildResult r = RunInChild([] {
Wire2 wire;
Server::ServerVerbSink sink;
wire.Decoder().SetVerbSink(&sink);
// §1's zero ruling: a null handle means "the object never crossed", and the client
// emits NOTHING then - so a null handle ON the wire is corruption, not a no-op.
const MGPHandleOnly death{kMGPipeNullHandle, static_cast<Uint32>(MGPipeKind::Texture), 0};
(void)wire.Encoder().EncodeRecord(MGPWireOp::ObjectDeath, &death, sizeof(death));
bool applied = false;
(void)wire.PumpOne(&applied);
});
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
EXPECT_NE(r.Log.find("Fatal{ProtocolCorruption, \"ObjectDeath.Handle\"}"), std::string::npos)
<< r.Log;
}
// THE GUARD'S TWO NON-FATAL ARMS, as unit controls. The Fatal arm itself needs a LIVE client
// session (the guard deliberately exempts the configured-but-wireless window - §6 layer 2's
// documented bring-up exception), which a unit binary has no handshake for; that arm is the
// integration lane's CtWireScenario.TheDirectApplierResetCallOnTheGLThreadIsRoleViolation,
// and the manual revert of PipeFill's FreshlyPrimed arm is the red-once beside it. What is
// pinnable here is that neither monolith nor a wireless transport is stopped.
TEST(CtWireFatals, TheDirectApplierResetCallSurvivesMonolithAndAWirelessTransport) {
const ChildResult wireless = RunInChild([] {
// Transport=InProcess with NO client session - the ServerLoop fixture's shape. The
// direct call is the only reset that exists there, so it must NOT be stopped.
MG_Config::Transport = MG_Config::TransportMode::InProcess;
MGPipeApplierReset();
});
EXPECT_FALSE(DiedOfAbort(wireless)) << DescribeStatus(wireless) << "\n" << wireless.Log;
const ChildResult monolith = RunInChild([] { MGPipeApplierReset(); });
EXPECT_FALSE(DiedOfAbort(monolith)) << DescribeStatus(monolith) << "\n" << monolith.Log;
}
#endif // MGTEST_HAVE_FORK