[MG_Pipe, MG_Remote, MG_Backend, MG_Impl, MG_IntegrationTest] (Disaggregated): P5c ct - the applier_reset and object_death control records

Two appended wire records replace the two cross-role control bypasses:
applier_reset (opcode 77, MGPApplierReset{Uint64 ContextSerial}, kScreen) is emitted by
the GL thread at the FreshlyPrimed make-current edge, before the client-side resets, so
the record's barrier orders the server's MGPipeApplierReset against every verb that
follows; a direct MGPipeApplierReset() from a non-apply thread with a live session is
Fatal{RoleViolation, "g_applier"} (MGPipeApply.cpp carries the layer-2 guard; the
apply-thread and no-session bring-up shapes keep the direct call verbatim).
object_death (opcode 78, reuses MGPipeHandleOnly, kCtxObject) is the framebuffer
family's first wire delete opcode: the client thread resolves the dying object's handle
in its OWN allocator, emits nothing for a handle the server never saw, and EmitAndWaits
otherwise - the wait preserving the death's ordering against in-flight records that
name the handle, the only property the blocking mailbox hop provided. The sink releases
per-kind twins through a handle-keyed ReleaseTwinByHandle (seven kinds, SamplerViewCso's
idempotent second path kept); a null arriving handle is
Fatal{ProtocolCorruption, "ObjectDeath.Handle"}. A documented NoSession fallback arm
delivers a death to a server-only fixture's loop; monolith keeps the mailbox hop and
the FreshlyPrimed direct call character for character.

Evidence: gen_pipe --check/--self-test and gen_pipe_field_ownership --check/--self-test
green; unit 2155/2155; integration-split 111/111 (107 plus four new CtWireScenario
entries, all genuinely run, each with its own log path and an EXPECT_EXIT death case);
red-once - shorting the emission to the direct call turns the four Ct cases red with
Fatal{RoleViolation, "g_applier"} by name, then restores green verbatim.
CONTRACT-P5C sections 1, 5.1-5.2, 7.
This commit is contained in:
2026-09-17 06:08:54 -04:00
parent 1eeb4740ca
commit a7e55ee62a
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/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 {
@@ -290,6 +318,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) {