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

# Conflicts:
#	MobileGL/MG_Backend/DirectGLES/Managers.h
This commit is contained in:
2026-09-17 06:58:45 -04:00
22 changed files with 1114 additions and 36 deletions
+67 -2
View File
@@ -22,6 +22,10 @@
#include <MG_Remote/Server/StagedShadow.h>
#include <MG_Remote/Server/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) {