mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-18 09:08:31 +09:00
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:
@@ -22,6 +22,10 @@
|
||||
#include <MG_Remote/Server/StagedShadow.h>
|
||||
#include <MG_Remote/Server/StagedTextureStore.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
|
||||
|
||||
#include "Utils.h"
|
||||
@@ -207,10 +211,34 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
void OnFrontendStateObjectDestroyed(MG_Pipe::MGPipeKind kind, Uint64 lifetimeId) {
|
||||
if (InProcessTeardown()) return;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// Death notices have no framebuffer wire opcode. Keep the lifetime/slot valid
|
||||
// until the context owner has destroyed its twin and updated its binding cache.
|
||||
// P5c (ct), CONTRACT-P5C.md §5.2: with an active transport the death crosses AS A
|
||||
// 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 &&
|
||||
!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};
|
||||
MG_Remote::Server::ServerLoopInstance().RunOnApplyThread(
|
||||
+[](void* user) -> MobileGLResult {
|
||||
@@ -296,6 +324,43 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
};
|
||||
} // 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
|
||||
// combination of the two knobs and so that bring-up and first-use cannot disagree.
|
||||
EsprytSlotArmVerdict ClassifyEsprytSlotArm(Bool subsystemBitSet, Bool legacyMemosEnabled) {
|
||||
|
||||
@@ -484,17 +484,30 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
#endif
|
||||
|
||||
// NO ReleaseByHandle HERE, AND THAT IS A DECISION (review M-4). The death half of
|
||||
// GetOrCreateByHandle exists for a kind whose announcement is its own destroy CALL
|
||||
// rather than the shared death notice - which is the BUFFER family
|
||||
// NO ReleaseByHandle HERE THROUGH P5, AND THAT WAS A DECISION (review M-4). The death
|
||||
// half of GetOrCreateByHandle existed only for a kind whose announcement is its own
|
||||
// destroy CALL rather than the shared death notice - which was the BUFFER family
|
||||
// (BackendBufferResourceTable::ReleaseByHandle, SlotTables.h, called from
|
||||
// resource_destroy) and none of the five kinds this registry serves: every one of them
|
||||
// dies through DestroyByLifetimeId below, because P4a adds no server-side destroy arm
|
||||
// for a texture, a renderbuffer, a framebuffer, a sampler CSO or a shader CSO. v1
|
||||
// declared one here anyway and it had no caller on either arm, which made its bound and
|
||||
// its wording things nobody would exercise until P5. The one-line wrapper comes back in
|
||||
// the commit that gives it a caller; SlotTable::ReleaseByHandle underneath is untouched
|
||||
// and is what SanityTest drives directly.
|
||||
// resource_destroy) and none of the five kinds this registry serves: every one of
|
||||
// them died through DestroyByLifetimeId below, because P4a added no server-side
|
||||
// destroy arm for a texture, a renderbuffer, a framebuffer, a sampler CSO or a shader
|
||||
// CSO. P5c (ct) is the commit that gives the wrapper its caller: object_death carries
|
||||
// the dead object's HANDLE on the wire (CONTRACT-P5C.md §5.2), and the static
|
||||
// ReleaseByHandle below is what the sink's per-kind dispatch calls.
|
||||
|
||||
// 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
|
||||
// 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>
|
||||
g_backendRenderbufferObjects;
|
||||
} // 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
|
||||
|
||||
@@ -484,6 +484,28 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
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
|
||||
// list; nothing on a shipping path asks.
|
||||
static Uint32 HolderCount() {
|
||||
|
||||
@@ -2690,8 +2690,38 @@ namespace MobileGL::MG_Pipe {
|
||||
// - what the server has is no longer what any suppressor slot last emitted;
|
||||
// - and the residual block owes a fresh publication whatever else moved.
|
||||
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();
|
||||
MGPipeApplierReset();
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
if (!resetCrossed)
|
||||
#endif
|
||||
MGPipeApplierReset();
|
||||
MGPipeSetHashSuppressorInstance().InvalidateAll();
|
||||
// P3a: and the vertex-input emitter's latches. NOT because the applier dropped
|
||||
// its vertex-elements records - it does not, they are share-group object state
|
||||
|
||||
@@ -2172,6 +2172,29 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
|
||||
endforeach()
|
||||
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.
|
||||
if (MOBILEGL_BUILD_DISAGGREGATED)
|
||||
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
|
||||
@@ -1633,6 +1633,33 @@ namespace MobileGL::MG_Pipe {
|
||||
};
|
||||
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)
|
||||
// ---------------------------------------------------------------------------------
|
||||
|
||||
@@ -37,6 +37,13 @@
|
||||
// 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.
|
||||
#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
|
||||
|
||||
#include <algorithm>
|
||||
@@ -1212,6 +1219,28 @@ namespace MobileGL::MG_Pipe {
|
||||
} // namespace
|
||||
|
||||
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.BoundRenderStateCso = kMGPipeNullHandle;
|
||||
g_applier.Residual = ResidualValueBlock{};
|
||||
|
||||
@@ -41,17 +41,18 @@
|
||||
// that the expansion, the two generated tables and this number agree.
|
||||
//
|
||||
// class entries group (as the plan tabulates it)
|
||||
// kScreen 11 screen: caps 1 + resource 3 + persistent map 2 + fence 4, plus the
|
||||
// appended server-side fence wait 1
|
||||
// kScreen 12 screen: caps 1 + resource 3 + persistent map 2 + fence 4, plus the
|
||||
// appended server-side fence wait 1 and P5c's applier_reset 1
|
||||
// kCtxQuery 8 query object namespace 6, plus the appended timestamp pair 2
|
||||
// kCtxCso 13 CSO create/bind/delete
|
||||
// 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
|
||||
// P5b-appended verbs (bind_shader_image, patch_parameter,
|
||||
// bind_stream_output, set_storage_block_binding,
|
||||
// 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
|
||||
// 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
|
||||
// reflection archive" is CORRECTED by the row: the archive carries the bindings the
|
||||
// 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
|
||||
#define MGP_CALL_LIST(X) \
|
||||
@@ -230,7 +237,26 @@
|
||||
X(SetStorageBlockBinding, MGPStorageBlockBinding, kCtxVerb, kHasBlob) \
|
||||
/* glCopyTexImage2D / glCopyTexSubImage2D - a copy whose SOURCE is the read framebuffer, */ \
|
||||
/* 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
|
||||
|
||||
// Explicitly NOT migrated (plan 4.4.6 / appendix A "explicit deletions"):
|
||||
|
||||
@@ -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(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
|
||||
// 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
|
||||
@@ -379,6 +385,7 @@
|
||||
P(MGPSurfaceInfo) \
|
||||
P(MGPImageBind) P(MGPPatchParameter) P(MGPStreamOutputBind) P(MGPStorageBlockBinding) \
|
||||
P(MGPCopyFromFramebuffer) \
|
||||
P(MGPApplierReset) \
|
||||
P(RenderStateParameters) P(PixelStoreParameters) P(SamplerParameters) P(PerBufferBlendState) \
|
||||
P(StencilFaceState) \
|
||||
P(DynamicBackendParameters) P(MGHostSpan) \
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
// 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.
|
||||
|
||||
// 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).
|
||||
struct MGPipeScreen {
|
||||
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 (*FenceDestroy)(const MGPHandleOnly* 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).
|
||||
struct MGPipeContext {
|
||||
void (*QueryCreate)(const MGPQueryDesc* payload);
|
||||
@@ -96,11 +97,12 @@ struct MGPipeContext {
|
||||
void (*BindStreamOutput)(const MGPStreamOutputBind* payload);
|
||||
void (*SetStorageBlockBinding)(const MGPStorageBlockBinding* payload, const void* blobBytes, Uint64 blobByteCount);
|
||||
void (*CopyFramebufferToTexture)(const MGPCopyFromFramebuffer* payload);
|
||||
void (*ObjectDeath)(const MGPHandleOnly* payload);
|
||||
};
|
||||
|
||||
inline constexpr SizeT kMGPipeScreenCallCount = 11;
|
||||
inline constexpr SizeT kMGPipeContextCallCount = 65;
|
||||
inline constexpr SizeT kMGPipeCallCount = 76;
|
||||
inline constexpr SizeT kMGPipeScreenCallCount = 12;
|
||||
inline constexpr SizeT kMGPipeContextCallCount = 66;
|
||||
inline constexpr SizeT kMGPipeCallCount = 78;
|
||||
|
||||
// A table that is not exactly its call count of function pointers has grown a
|
||||
// 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) {
|
||||
gMGPipeContext.CopyFramebufferToTexture(payload);
|
||||
}
|
||||
|
||||
inline void MGP_ApplierReset(const MGPApplierReset* payload) {
|
||||
gMGPipeScreen.ApplierReset(payload);
|
||||
}
|
||||
|
||||
inline void MGP_ObjectDeath(const MGPHandleOnly* payload) {
|
||||
gMGPipeContext.ObjectDeath(payload);
|
||||
}
|
||||
|
||||
@@ -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 MGPStorageBlockBinding& a, const MGPStorageBlockBinding& 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 PixelStoreParameters& a, const PixelStoreParameters& 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 <>
|
||||
struct MGPipeHasFieldVerifier<MGPCopyFromFramebuffer> : std::true_type {};
|
||||
template <>
|
||||
struct MGPipeHasFieldVerifier<MGPApplierReset> : std::true_type {};
|
||||
template <>
|
||||
struct MGPipeHasFieldVerifier<RenderStateParameters> : std::true_type {};
|
||||
template <>
|
||||
struct MGPipeHasFieldVerifier<PixelStoreParameters> : std::true_type {};
|
||||
@@ -662,6 +665,11 @@ inline Bool MGPipeVerify(const MGPCopyFromFramebuffer& a, const MGPCopyFromFrame
|
||||
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) {
|
||||
MGP_FIELDS_RenderStateParameters(MGP_VERIFY_FIELD)
|
||||
return true;
|
||||
@@ -709,4 +717,4 @@ inline Bool MGPipeVerify(const MGPVertexBindingPointWire& a, const MGPVertexBind
|
||||
|
||||
#undef MGP_VERIFY_FIELD
|
||||
|
||||
inline constexpr SizeT kMGPipeVerifiedPayloadCount = 77;
|
||||
inline constexpr SizeT kMGPipeVerifiedPayloadCount = 78;
|
||||
|
||||
@@ -146,7 +146,9 @@ enum class MGPWireOp : Uint16 {
|
||||
BindStreamOutput = 74,
|
||||
SetStorageBlockBinding = 75,
|
||||
CopyFramebufferToTexture = 76,
|
||||
kOpCount = 77,
|
||||
ApplierReset = 77,
|
||||
ObjectDeath = 78,
|
||||
kOpCount = 79,
|
||||
};
|
||||
|
||||
// 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),
|
||||
/* 75 SetStorageBlockBinding */ static_cast<Uint32>(kHasBlob),
|
||||
/* 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_cast<SizeT>(MGPWireOp::kOpCount),
|
||||
@@ -905,6 +909,22 @@ static_assert(sizeof(MGPWireRec_CopyFramebufferToTexture) ==
|
||||
((sizeof(MGPWireRecHeader) + sizeof(MGPCopyFromFramebuffer) + 7u) & ~SizeT(7u)),
|
||||
"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) {
|
||||
MGLOG_F("MGPipe: protocol corruption applying %s: size=%llu remaining=%llu", call,
|
||||
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:
|
||||
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_CopyFramebufferToTexture, "CopyFramebufferToTexture");
|
||||
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::kOpCount:
|
||||
default:
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
|
||||
#include <MG_Pipe/MGPipe.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_Util/Debug/Log.h>
|
||||
|
||||
@@ -503,6 +507,81 @@ namespace MobileGL::MG_Remote::Client {
|
||||
|
||||
} // 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) {
|
||||
if (g_clientTablesUninstalled.load(std::memory_order_acquire)) {
|
||||
MGLOG_F("MGPipe: Fatal{ClientTablesUninstalled, \"%s\"} - the client tables are "
|
||||
|
||||
@@ -35,6 +35,8 @@
|
||||
#pragma once
|
||||
#include <Includes.h>
|
||||
|
||||
#include <MG_Pipe/MGPipeHandles.h>
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
|
||||
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.
|
||||
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
|
||||
|
||||
#endif // MOBILEGL_BUILD_DISAGGREGATED
|
||||
|
||||
@@ -14,6 +14,10 @@
|
||||
|
||||
#include <Config.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_Pipe/PipeApply.h>
|
||||
#include <MG_Util/Converters/GLToMG/TextureEnumConverter.h>
|
||||
@@ -942,6 +946,67 @@ namespace MobileGL::MG_Remote::Server {
|
||||
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
|
||||
// -----------------------------------------------------------------------------------
|
||||
|
||||
@@ -150,6 +150,32 @@ namespace MobileGL::MG_Remote::Server {
|
||||
Bool OnGenerateMipmap(const MG_Pipe::MGPMipPlan& plan) 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
|
||||
// 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).
|
||||
@@ -231,6 +257,14 @@ namespace MobileGL::MG_Remote::Server {
|
||||
Uint64 m_streamOutputControls = 0;
|
||||
Uint64 m_streamOutputBinds = 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 writes into a caller buffer, so one staging vector per session sits
|
||||
// between them. Grown, never shrunk, and never handed out past the call.
|
||||
|
||||
@@ -239,7 +239,9 @@ namespace MobileGL::MG_Remote::Wire {
|
||||
X(PatchParameter, MGPPatchParameter) \
|
||||
X(BindStreamOutput, MGPStreamOutputBind) \
|
||||
X(SetStorageBlockBinding, MGPStorageBlockBinding) \
|
||||
X(CopyFramebufferToTexture, MGPCopyFromFramebuffer)
|
||||
X(CopyFramebufferToTexture, MGPCopyFromFramebuffer) \
|
||||
X(ApplierReset, MGPApplierReset) \
|
||||
X(ObjectDeath, MGPHandleOnly)
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -2085,6 +2087,20 @@ namespace MobileGL::MG_Remote::Wire {
|
||||
m_verbs->OnCopyFramebufferToTexture(
|
||||
*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:
|
||||
// Class C, wave 3 (census-classC.md "static cross"); not a verb (FillPoints.def:21).
|
||||
return false;
|
||||
|
||||
@@ -534,6 +534,25 @@ namespace MobileGL::MG_Remote::Wire {
|
||||
(void)copy;
|
||||
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.
|
||||
|
||||
@@ -125,11 +125,13 @@ TEST(PipeCatalogue, GeneratedTablesHoldTheWholeCatalogue) {
|
||||
EXPECT_EQ(kMGPipeContextCallCount, kMGPipeCallCount - ClassCount<kScreen>());
|
||||
|
||||
// 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<kCtxCso>(), 13u);
|
||||
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,
|
||||
// patch_parameter, bind_stream_output, set_storage_block_binding,
|
||||
// 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
|
||||
// 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
|
||||
// NO MGPipeApply* ENTRY POINT: the other 37 have an applier, R-17 installs adapters over them,
|
||||
// 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 (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
|
||||
// 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
|
||||
@@ -156,10 +159,12 @@ TEST(PipeCatalogue, UninstalledTablesAreAllNull) {
|
||||
const void* const* context = reinterpret_cast<const void* const*>(&gMGPipeContext);
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
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
|
||||
// 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.SetStreamOutputTargets, nullptr);
|
||||
EXPECT_EQ(gMGPipeContext.DrawVbo, nullptr);
|
||||
@@ -168,6 +173,8 @@ TEST(PipeCatalogue, UninstalledTablesAreAllNull) {
|
||||
EXPECT_EQ(gMGPipeScreen.GetCaps, nullptr);
|
||||
EXPECT_EQ(gMGPipeContext.QueryCreate, nullptr);
|
||||
EXPECT_EQ(gMGPipeScreen.FenceCreate, nullptr);
|
||||
EXPECT_EQ(gMGPipeScreen.ApplierReset, nullptr);
|
||||
EXPECT_EQ(gMGPipeContext.ObjectDeath, nullptr);
|
||||
#else
|
||||
// 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.
|
||||
@@ -235,6 +242,12 @@ TEST(PipeCatalogue, ExactlyTheRoutedRowsAreInstalledAndTheRestAreStillNull) {
|
||||
EXPECT_EQ(gMGPipeContext.Present, nullptr);
|
||||
EXPECT_EQ(gMGPipeContext.SetSwapInterval, 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
|
||||
// A pull build compiles no applier and no routing, so the pre-migration statement is still
|
||||
// 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::SetStorageBlockBinding), 75);
|
||||
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
|
||||
// else, and the extended draw row keeps its two flags.
|
||||
EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::SetStorageBlockBinding), static_cast<Uint32>(kHasBlob));
|
||||
@@ -575,6 +596,11 @@ TEST(PipeCatalogue, LateArrivalsAreAppendedWithoutRenumbering) {
|
||||
EXPECT_EQ(sizeof(MGPCopyFromFramebuffer), 48u);
|
||||
EXPECT_EQ(sizeof(MGPCopyRegion), 72u);
|
||||
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.
|
||||
EXPECT_EQ(static_cast<Uint32>(kDrawIsIndirect), 1u << 5);
|
||||
EXPECT_EQ(static_cast<Uint32>(kDrawHasUserIndices) & static_cast<Uint32>(kDrawIsIndirect), 0u);
|
||||
@@ -771,8 +797,10 @@ TEST(PipeCatalogue, FloatVectorsCompareBitwise) {
|
||||
TEST(PipeCatalogue, SixValueStructsHaveFieldLists) {
|
||||
// 72 through P5; P5b appended five call payloads (MG_Remote/CONTRACT-P5B.md: MGPImageBind,
|
||||
// MGPPatchParameter, MGPStreamOutputBind, MGPStorageBlockBinding, MGPCopyFromFramebuffer),
|
||||
// each with its own field list, so the comparator sees every one of them: 77.
|
||||
EXPECT_EQ(kMGPipeVerifiedPayloadCount, 77u);
|
||||
// each with its own field list, so the comparator sees every one of them: 77. P5c appended
|
||||
// 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<PixelStoreParameters>::value);
|
||||
static_assert(MGPipeHasFieldVerifier<PerBufferBlendState>::value);
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
#include <Config.h>
|
||||
#include <MG_Remote/Server/PipeApplier.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/MGPipeRenderStateSpans.h>
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
@@ -268,6 +272,16 @@ namespace {
|
||||
FramebufferCopies.push_back(copy);
|
||||
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<MGPBlit> Blits;
|
||||
std::vector<MGPPresent> Presents;
|
||||
@@ -293,6 +307,8 @@ namespace {
|
||||
std::vector<MGPPatchParameter> Patches;
|
||||
std::vector<MGPMipPlan> MipPlans;
|
||||
std::vector<MGPCopyFromFramebuffer> FramebufferCopies;
|
||||
std::vector<MGPApplierReset> ApplierResets;
|
||||
std::vector<MGPHandleOnly> ObjectDeaths;
|
||||
};
|
||||
|
||||
Replies& Answers() { return m_replies; }
|
||||
@@ -1243,6 +1259,73 @@ TEST_F(PipeWireCodecTest, BindStreamOutputReachesTheSink) {
|
||||
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
|
||||
// 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_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
|
||||
|
||||
Reference in New Issue
Block a user