Merge branch 'p5c-hd' into feat/disaggregated (P5c hd: handle-keyed resolution)

Conflict: VulkanRenderer.cpp's disaggregated include block took both halves.

Merge coordination (integrator, with the evidence inline):

- hd's Fatal{RoleViolation, MGPipeSlots} guard as landed was kind-blind: it fired on the
  G6 frontend-keyed twin registry (texture/VAO/program/sampler resolver probes - P3b/P4b's
  to rekey) and on the families whose handle-carrying records the client does not emit
  until P4b (the buffer binding-point ensures, the GPU-written announcement). Integration-
  split went 104/107 red at the first Clear. Two NAMED, scoped exemptions keep the guard's
  teeth for every other caller (SlotAllocator.{h,cpp}): MGPipeReverseAnnouncementScope
  (buffer ensure + announcement family, P4b/P7) and MGPipeFrontendKeyedRegistryScope
  (the G6 registry family, P3b/P4b; also the mailbox death switch until ct's object_death
  record retires it). ~30 resolver sites wrapped, each naming its debt in the comment.
- ScopedRestartIndexSubstitution needed NO exemption: the EBO resolves from the record's
  IndexBuffer.Res and the bytes read from the server staged shadow - CONTRACT-P5B d1's
  own sentence, implemented.
- Magma's named blit takes a G6 consume arm (frontend FBOs resolved by lifetime id inside
  the scope, VerbBlitNamedConsumed set, loud refusal kept for a failed resolution), so the
  two DirectVulkan.Split.NamedBlit correctness cases stay green; Magma's hidden blit and
  depth-mipmap resources' teardown probes are wrapped and named (P7: give them non-frontend
  storage).
- MG_State gains GLContext::FindFramebufferObjectByLifetimeId (behaviour-neutral accessor
  for the arm above).

Gates on the merged tree: unit 2166/2166; integration-split 107/107 (2 design skips);
hd's RemoteGuards death tests still abort the unwrapped paths by name; PrimitiveRestart
7/7; the two Magma named-blit cases render. CONTRACT-P5C sections 3, 5.4.
This commit is contained in:
2026-09-17 06:57:16 -04:00
23 changed files with 1772 additions and 185 deletions
+640 -14
View File
@@ -362,6 +362,42 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
const Uint8* ResolveIndirectCommandBytes(const void* indirect, SizeT requiredBytes, const char* label) {
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (hd, CONTRACT-P5C §3.5): under an active transport the command buffer resolves
// from the handle the verb record carried, and its CPU bytes are the SERVER's staged
// shadow (rule E) - the client's GL_DRAW_INDIRECT_BUFFER binding slot, the frontend
// object's MappedData()/GetSize() (B3) and the client slot allocator (T2) are never
// read. Coverage is asserted: commands read from bytes no record staged would be
// zero-filled, which is a missing record, not a valid command stream.
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
const MG_Pipe::MGPipeHandle handle = MG_Pipe::MGPipeApplier().VerbIndirectBuffer;
if (!MG_Pipe::MGPipeHandleIsNull(handle)) {
auto* resource = BufferImpl::FindBufferResourceForHandle(handle);
const Uint8* const base = resource ? resource->hostBytes : nullptr;
const SizeT commandOffset = reinterpret_cast<SizeT>(indirect);
if (base == nullptr) {
MGLOG_E_ONCE("%s skipped: the verb's indirect buffer {%u, %u} has no staged "
"shadow on this side (GPU-written indirect commands are P8's)",
label, handle.Slot, handle.Gen);
return nullptr;
}
if (commandOffset + requiredBytes > BufferImpl::ResourceWidthForHandle(handle)) {
MGLOG_E_ONCE("%s skipped: invalid GL_DRAW_INDIRECT_BUFFER binding or range", label);
return nullptr;
}
BufferImpl::RequireStagedCoverage(*resource, base, commandOffset,
commandOffset + requiredBytes, "indirect_command_bytes");
return base + commandOffset; }
// No buffer was bound: the frontend already raised the GL error; keep the old
// fall-through shape (a null indirect pointer is the same skip it was).
if (!indirect) {
MGLOG_E_ONCE("%s skipped: indirect pointer is null", label);
return nullptr;
}
MGLOG_E_ONCE("%s skipped: the verb carried no indirect buffer handle", label);
return nullptr;
}
#endif
auto drawBuffer = MGB_CTX->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
if (drawBuffer) {
drawBuffer->SyncPersistentMappedRange();
@@ -907,12 +943,28 @@ namespace MobileGL::MG_Backend::DirectGLES {
// Indirect Buffer Object - must also be bound to GL_DRAW_INDIRECT_BUFFER on the ES
// context since indirect draws now execute natively on the GPU.
if (includeIndirectBuffer) {
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (hd, CONTRACT-P5C §3.5): under an active transport the buffer is the verb
// record's handle; the frontend binding slot and the client allocator are
// never read (T2). The twin's ensure is what SyncBoundBuffer's did.
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
const MG_Pipe::MGPipeHandle handle = MG_Pipe::MGPipeApplier().VerbIndirectBuffer;
if (!MG_Pipe::MGPipeHandleIsNull(handle)) {
auto* resource = EnsureBufferResourceForHandle(nullptr, handle);
if (resource && resource->id != 0) {
BindBufferId(GL_DRAW_INDIRECT_BUFFER, resource->id);
}
}
} else
#endif
{
auto& possibleIndirectBuffer =
MGB_CTX->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
if (possibleIndirectBuffer) {
SyncBoundBuffer(BufferTarget::DrawIndirect, GL_DRAW_INDIRECT_BUFFER);
}
}
}
// UBO binding points are (re)established per draw by BindCurrentProgramWithResources at
// their compacted link-time points: CacheResourceLocations glUniformBlockBinding's the
@@ -937,6 +989,19 @@ namespace MobileGL::MG_Backend::DirectGLES {
SyncBufferBindingPoints(BufferTarget::ShaderStorage, GL_SHADER_STORAGE_BUFFER);
MarkShaderStorageBuffersGpuWritten();
if (includeDispatchIndirectBuffer) {
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (hd, CONTRACT-P5C §3.5): see the draw-indirect twin above - the verb
// record's handle, never the frontend binding slot or the client allocator.
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
const MG_Pipe::MGPipeHandle handle = MG_Pipe::MGPipeApplier().VerbDispatchIndirectBuffer;
if (!MG_Pipe::MGPipeHandleIsNull(handle)) {
auto* resource = EnsureBufferResourceForHandle(nullptr, handle);
if (resource && resource->id != 0) {
BindBufferId(GL_DISPATCH_INDIRECT_BUFFER, resource->id);
}
}
} else
#endif
SyncBoundBuffer(BufferTarget::DispatchIndirect, GL_DISPATCH_INDIRECT_BUFFER);
}
}
@@ -1452,6 +1517,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (G6, CONTRACT-P5C §5.4): frontend-keyed twin resolution, named debt inside
// the scope - P3b/P4b rekeys the registry onto handles.
const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry;
#endif
#if MOBILEGL_PIPE_PUSH
if (EsprytSlotTablesEnabled()) {
// No memo on this arm. The memo existed to turn the registry's hash probe into
@@ -1569,6 +1639,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
Bool imageBindableStorageRequired) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (G6, CONTRACT-P5C §5.4): this sync still arrives holding the FRONTEND object
// (the object-class rows) and resolves its twin by frontend identity - the
// frontend-keyed registry P3b/P4b rekeys onto handles. The probe (and the mint on
// a first sync) is named debt inside the scope, not an unwrapped violation.
const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry;
#endif
auto* backendTextureSlot = g_backendTextureObjects.Find(textureObject.get());
auto& backendSlot = backendTextureSlot ? *backendTextureSlot
@@ -2418,6 +2495,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
static const MG_Pipe::MGPImageView* ResolveShaderImageRecord(
Uint unit, const MG_State::GLState::ITextureObject* boundTexture) {
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (G6, CONTRACT-P5C §5.4): the image seam's identity check below resolves the
// bound texture's handle by frontend identity - frontend-keyed twin resolution,
// named debt inside the scope - P3b/P4b rekeys the registry onto handles.
const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry;
#endif
// P4a decline-site I1: M - the mask says this family is not switched on, and it
// IS D's SamplerSubsystemEnabled() now. Silent, confirmed at the verification
// round - and it is what keeps I2 below quiet on a mask that never armed images.
@@ -2852,6 +2935,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
!bound || bound == MG_Impl::GLImpl::FramebufferImpl::pDefaultFramebufferInfo->defaultFBO;
if ((record.IsDefault != 0) != boundIsDefault) return false;
if (boundIsDefault) return true;
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (hd): the object-handle half of the identity check is a client-allocator
// probe (T2). Under an active transport the record is the only statement of
// identity (rule E) and the check does not run - the record was resolved from the
// applier's OWN bound handle, so it is this binding's by construction (ID-19).
// Monolith keeps the corroboration verbatim.
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) return true;
#endif
return record.Fbo == g_backendFramebufferObjects.HandleOf(bound.get());
}
@@ -3041,6 +3132,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (!backendObj) {
backendObj = MakeShared<BackendFramebufferObject>();
}
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (hd): with an active transport the twin's sync keys its applier-record
// lookup on the record's own handle (m_pushedSyncHandle), never on the client
// allocator (T2), and the table remembers which frontend object the twin is
// synced from so a later handle-only resolution (the named blit) can reach it.
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
backendObj->m_pushedSyncHandle = record.Fbo;
g_backendFramebufferObjects.NoteStateForHandle(record.Fbo, currentFBO);
}
#endif
// THE "SAME FBO AS DRAW" SKIP IS NOW A FIELD, not a pointer comparison against
// the object the previous iteration happened to sync. Target = Both is the
@@ -3109,6 +3210,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
MG_State::GLState::FramebufferObject* lastUpdatedFBO = nullptr;
for (auto& target : fboTargets) {
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (G6, CONTRACT-P5C §5.4): the twin resolutions this iteration may run
// below are frontend-keyed, named debt inside the scope - P3b/P4b rekeys the
// registry onto handles.
const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry;
#endif
auto& slot = GetFramebufferBindingSlotChecked(target);
auto& currentFBO = slot.GetBoundObject();
@@ -4036,6 +4143,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
// Anything else and the frontend's own MapUBO answers, exactly as it does today.
static const MG_Pipe::MGPipeShaderCsoRecord* ResolveGlobalConstantsRecord(
const MG_State::GLState::ProgramObject* program) {
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (G6, CONTRACT-P5C §5.4): the program's handle is resolved by frontend
// identity below - frontend-keyed twin resolution, named debt inside the scope -
// P3b/P4b rekeys the registry onto handles.
const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry;
#endif
// P4a decline-site P1: M - the mask says this family is not switched on, or there
// is no current program at all; it IS D's ProgramSubsystemEnabled() now. Silent,
// confirmed at the verification round.
@@ -4122,6 +4235,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
void SyncCurrentProgram(const SharedPtr<MG_State::GLState::ProgramObject>& currentProgram) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (G6, CONTRACT-P5C §5.4): the twin resolution below still keys on the frontend
// program object - named debt inside the scope - P3b/P4b rekeys the registry onto
// handles.
const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry;
#endif
g_backendProgramObjects.CollectGarbageIfNeeded();
SamplerImpl::g_backendSamplerObjects.CollectGarbageIfNeeded();
@@ -4374,6 +4493,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
// and the twin memo replaces even that with an array probe on the steady path.
const auto& currentFBO = slot.GetBoundObject();
if (currentFBO && currentFBO != MG_Impl::GLImpl::FramebufferImpl::pDefaultFramebufferInfo->defaultFBO) {
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (G6, CONTRACT-P5C §5.4): frontend-keyed twin resolution, named debt inside
// the scope - P3b/P4b rekeys the registry onto handles.
const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry;
#endif
FramebufferImpl::BackendFramebufferObject* twin = nullptr;
#if MOBILEGL_PIPE_PUSH
if (EsprytSlotTablesEnabled()) {
@@ -4446,12 +4570,80 @@ namespace MobileGL::MG_Backend::DirectGLES {
backendObj->Bind(target);
}
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (hd, CONTRACT-P5C §3.2/§3.3): the handle-keyed form of SyncAndBindFramebufferObject.
// With an active transport a framebuffer reaches the server as the handle a record
// carried - the named blit's ReadFbo/DrawFbo, or the applier's own bound handle for a
// binding restore - and the client's binding slots and slot allocator are never probed
// (T2/T3). The twin is resolved (or minted) BY HANDLE; its sync is keyed on THIS handle's
// applier record through m_pushedSyncHandle; and the frontend object the sync body still
// walks comes from the table's own state note (the object-class channel P3b/P4b retires),
// never from a client binding slot. A twin with no noted object was never synced through
// any binding - the DSA-on-a-never-bound-framebuffer case, which the split path already
// refuses for the named clears - and is loud rather than silently unconfigured.
void SyncAndBindFramebufferByHandle(MG_Pipe::MGPipeHandle fbo, FramebufferTarget target,
Bool forceSync = false) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (MG_Pipe::MGPipeHandleIsNull(fbo) || fbo == MG_Pipe::kMGPipeDefaultFramebuffer) {
// Same reset as the object form's default branch, for its reason.
if (target == FramebufferTarget::Draw) {
FramebufferImpl::g_alphaWidenedDrawBufferMask = 0;
FramebufferImpl::g_integerColorDrawBufferMask = 0;
}
FramebufferImpl::BindFramebufferId(
target == FramebufferTarget::Draw ? GL_DRAW_FRAMEBUFFER : GL_READ_FRAMEBUFFER, 0);
return;
}
auto& registry = FramebufferImpl::g_backendFramebufferObjects;
auto* twinSlot = registry.GetOrCreateByHandle(fbo);
if (twinSlot == nullptr) {
MGLOG_E_ONCE("MGPipe: framebuffer handle {%u, %u} is refused by the twin slot table "
"(live generation %u); nothing is bound for the %s target",
fbo.Slot, fbo.Gen, registry.LiveGenAt(fbo.Slot),
target == FramebufferTarget::Read ? "READ" : "DRAW");
return;
}
auto& backendObj = *twinSlot;
if (!backendObj) {
backendObj = MakeShared<FramebufferImpl::BackendFramebufferObject>();
}
backendObj->m_pushedSyncHandle = fbo;
const auto stateObj = registry.StateForHandle(fbo);
if (stateObj) {
if (forceSync) {
backendObj->InvalidateSyncedState();
}
backendObj->SyncToBackend(stateObj, target);
} else {
MGLOG_E_ONCE("MGPipe: framebuffer handle {%u, %u} has no frontend object noted on "
"this side - it was never synced through a binding, so its twin is "
"bound unconfigured",
fbo.Slot, fbo.Gen);
}
backendObj->Bind(target);
}
#endif // MOBILEGL_BUILD_DISAGGREGATED
void ForceBindCurrentFBO(FramebufferTarget target) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
auto& slot = GetFramebufferBindingSlotChecked(target);
const auto& fbo = slot.GetBoundObject();
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (hd): with an active transport the bound framebuffer's handle is the applier's
// own working state; the object form's registry probe never runs (T2).
if (MG_Config::Transport != MG_Config::TransportMode::Monolith && FramebufferSubsystemEnabled()) {
SyncAndBindFramebufferByHandle(
MG_Pipe::MGPipeApplier().BoundFramebuffer[static_cast<SizeT>(
target == FramebufferTarget::Read ? MG_Pipe::MGPipeFramebufferTarget::Read
: MG_Pipe::MGPipeFramebufferTarget::Draw)],
target);
} else
#endif
SyncAndBindFramebufferObject(fbo, target);
FramebufferImpl::g_fboSyncedSlotVersions[(SizeT)target] = slot.GetVersion();
FramebufferImpl::g_fboSyncedObjectVersions[(SizeT)target] = fbo ? fbo->GetObjectVersion() : 0;
@@ -4642,6 +4834,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
// Bind texture object
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (G6, CONTRACT-P5C §5.4): frontend-keyed twin resolution, named debt
// inside the scope - P3b/P4b rekeys the registry onto handles.
const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry;
#endif
auto* backendTextureSlot = TextureImpl::g_backendTextureObjects.Find(textureObject.get());
if (!backendTextureSlot || !*backendTextureSlot) {
fullyResolved = false;
@@ -4704,6 +4901,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
static SamplerImpl::BackendSamplerObject* ResolveUnitSamplerBackend(
Int unit, const SharedPtr<MG_State::GLState::SamplerObject>& samplerObject) {
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (G6, CONTRACT-P5C §5.4): frontend-keyed twin resolution, named debt inside the
// scope - P3b/P4b rekeys the registry onto handles.
const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry;
#endif
auto& memo = g_unitSamplerLookupMemos[static_cast<SizeT>(unit)];
#if MOBILEGL_PIPE_PUSH
if (EsprytSlotTablesEnabled()) {
@@ -5032,6 +5234,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (currentProgram && currentProgram->GetLinkStatus() && currentProgram->GetSpirvStatus()) {
#ifdef TRACY_ENABLE
ZoneScopedNC("BindCurrentProgram", TRACY_ZONECOLOR_BACKEND);
#endif
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (G6, CONTRACT-P5C §5.4): the program/sampler twin resolutions below still key
// on frontend objects - named debt inside the scope - P3b/P4b rekeys the registry
// onto handles.
const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry;
#endif
// The twin SyncCurrentProgram just resolved for this draw; the registry
// Find only runs if the stash somehow does not match (defensive fallback).
@@ -5389,6 +5597,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (PrgramImpl::g_currentDrawFrontendProgram == currentProgram.get()) {
return PrgramImpl::g_currentDrawBackendProgram;
}
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (G6, CONTRACT-P5C §5.4): frontend-keyed twin resolution, named debt inside the
// scope - P3b/P4b rekeys the registry onto handles.
const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry;
#endif
if (auto* backendProgramSlot = PrgramImpl::g_backendProgramObjects.Find(currentProgram.get())) {
return backendProgramSlot->get();
}
@@ -5656,14 +5869,36 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (!restart.DrawIsValid()) return;
type = restart.IndexType();
indexSize = MG_Util::GetGLTypeSize(type);
const Bool useNative = drawIndirectBuffer != nullptr && SupportsNativeIndirectDraws();
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (hd, CONTRACT-P5C §3.5): under an active transport the command buffer is the
// handle the verb record carried (MGPipeApplier's verb stash) and the SSBO view's
// resource resolves from it by handle - the frontend binding slot and the client slot
// allocator are never read (T2).
const MG_Pipe::MGPipeHandle verbBufferHandle =
MG_Config::Transport != MG_Config::TransportMode::Monolith
? MG_Pipe::MGPipeApplier().VerbIndirectBuffer
: MG_Pipe::kMGPipeNullHandle;
#endif
const Bool useNative =
#if MOBILEGL_BUILD_DISAGGREGATED
MG_Config::Transport != MG_Config::TransportMode::Monolith
? !MG_Pipe::MGPipeHandleIsNull(verbBufferHandle) && SupportsNativeIndirectDraws()
:
#endif
drawIndirectBuffer != nullptr && SupportsNativeIndirectDraws();
if (useNative) {
// gl_BaseInstance must observe GPU-written command fields; expose the indirect
// buffer to the program's mg_IndirectParams SSBO view and address it per draw.
const auto backendProgram = GetCurrentBackendProgram();
const Int paramsBinding = backendProgram ? backendProgram->GetIndirectParamsBinding() : -1;
if (paramsBinding >= 0) {
auto* resource = BufferImpl::EnsureBufferResource(drawIndirectBuffer);
auto* resource =
#if MOBILEGL_BUILD_DISAGGREGATED
MG_Config::Transport != MG_Config::TransportMode::Monolith
? BufferImpl::EnsureBufferResourceForHandle(nullptr, verbBufferHandle)
:
#endif
BufferImpl::EnsureBufferResource(drawIndirectBuffer);
if (resource && resource->id != 0) {
BufferImpl::BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, static_cast<GLuint>(paramsBinding),
resource->id);
@@ -5728,12 +5963,32 @@ namespace MobileGL::MG_Backend::DirectGLES {
// draw is what leaves a stale value, and restoring afterwards would only protect the
// NEXT draw while these commands ran with the stale one.
SetCurrentBaseVertex(0);
const Bool useNative = drawIndirectBuffer != nullptr && SupportsNativeIndirectDraws();
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (hd, CONTRACT-P5C §3.5): see ExecuteIndexedIndirectCommands - the verb's handle,
// never the frontend binding slot or the client allocator.
const MG_Pipe::MGPipeHandle verbBufferHandle =
MG_Config::Transport != MG_Config::TransportMode::Monolith
? MG_Pipe::MGPipeApplier().VerbIndirectBuffer
: MG_Pipe::kMGPipeNullHandle;
#endif
const Bool useNative =
#if MOBILEGL_BUILD_DISAGGREGATED
MG_Config::Transport != MG_Config::TransportMode::Monolith
? !MG_Pipe::MGPipeHandleIsNull(verbBufferHandle) && SupportsNativeIndirectDraws()
:
#endif
drawIndirectBuffer != nullptr && SupportsNativeIndirectDraws();
if (useNative) {
const auto backendProgram = GetCurrentBackendProgram();
const Int paramsBinding = backendProgram ? backendProgram->GetIndirectParamsBinding() : -1;
if (paramsBinding >= 0) {
auto* resource = BufferImpl::EnsureBufferResource(drawIndirectBuffer);
auto* resource =
#if MOBILEGL_BUILD_DISAGGREGATED
MG_Config::Transport != MG_Config::TransportMode::Monolith
? BufferImpl::EnsureBufferResourceForHandle(nullptr, verbBufferHandle)
:
#endif
BufferImpl::EnsureBufferResource(drawIndirectBuffer);
if (resource && resource->id != 0) {
BufferImpl::BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, static_cast<GLuint>(paramsBinding),
resource->id);
@@ -5819,6 +6074,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
return 0;
}
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (G6, CONTRACT-P5C §5.4): frontend-keyed twin resolution, named debt inside the
// scope - P3b/P4b rekeys the registry onto handles.
const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry;
#endif
auto* backendProgramSlot = PrgramImpl::g_backendProgramObjects.Find(programObject.get());
auto& backendObj =
backendProgramSlot ? *backendProgramSlot : PrgramImpl::g_backendProgramObjects.GetOrCreate(programObject);
@@ -6163,13 +6423,88 @@ namespace MobileGL::MG_Backend::DirectGLES {
const SizeT sourceIndexSize = MG_Util::GetGLTypeSize(indexType);
const Uint32 fixedMax = MG_Util::FixedRestartIndexForGLType(indexType);
const Uint32 applicationRestartIndex = MGB_CTX->GetPrimitiveRestartIndex();
const auto& indexBuffer = BoundElementArrayBuffer();
const Uint8* source = nullptr;
SizeT indexCount = 0;
SizeT sourceByteOffset = 0;
Bool hasElementBuffer = false;
#if MOBILEGL_BUILD_DISAGGREGATED
// -1: monolith, or this arm never resolved a backend id, and the tail asks
// BoundElementArrayBufferId() exactly as it always did.
Int serverElementBinding = -1;
#endif
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (hd, CONTRACT-P5C §3.5): with an active transport this constructor runs on the
// apply thread, where the frontend BufferObject's legacy accessors are Fatal by name
// (BufferObject.cpp's "buffer-legacy-arm" refusal). The element buffer therefore
// resolves from the applier's set_index_buffer state (T2) and the bytes read from the
// server's own staged shadow - "the backend's ScopedRestartIndexSubstitution reads the
// index bytes it needs on its side" (CONTRACT-P5B d1), the same arm
// MultiDrawElementsIndirectCount takes for its command buffer. A draw whose record
// carried client indices (no index buffer bound) takes the client-pointer arm below,
// `indices` already resolved through the server's segment resolver. Monolith takes
// the original body, verbatim.
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
const auto& applierState = MG_Pipe::MGPipeApplier();
const MG_Pipe::MGPipeHandle elements = applierState.IndexBuffer.Res;
serverElementBinding = 0;
if (!MG_Pipe::MGPipeHandleIsNull(elements)) {
auto* elementResource = BufferImpl::FindBufferResourceForHandle(elements);
// The WHOLE buffer is rewritten, not just this draw's range, so that every
// index keeps its position: an indirect draw's firstIndex lives in GPU memory
// and cannot be adjusted from here. It is an ELEMENT index, so it survives
// widening unchanged.
const SizeT sizeBytes = BufferImpl::ResourceWidthForHandle(elements);
if (sizeBytes < sourceIndexSize) {
return; // Nothing to restart on; let the driver see the draw unchanged.
}
if (sizeBytes > kMaxRestartRewriteBytes) {
MGLOG_E_ONCE("Draw skipped: GL_PRIMITIVE_RESTART with restart index %u needs the %zu-byte element "
"array buffer rewritten every draw, which is past the %zu-byte ceiling. Use "
"GL_PRIMITIVE_RESTART_FIXED_INDEX, or set glPrimitiveRestartIndex to the all-ones "
"value of the index type.",
applicationRestartIndex, sizeBytes, kMaxRestartRewriteBytes);
m_valid = false;
return;
}
// The staged shadow is the source of truth for CPU reads on this side: a
// persistent map's blocks and a shader write's writeback were consumed by the
// applier before this verb ran.
const Uint8* hostBytes = elementResource ? elementResource->hostBytes : nullptr;
if (hostBytes == nullptr) {
MGLOG_E_ONCE("Draw skipped: GL_PRIMITIVE_RESTART with restart index %u needs a CPU-readable copy of "
"the bound element array buffer and none is available.",
applicationRestartIndex);
m_valid = false;
return;
}
// The M-3 rule for a whole-store reader (Managers.cpp:3238): for a store whose
// content the application SUPPLIED a coverage gap is a missing record and is
// Fatal by name; for one it ORPHANED the gap is its own undefined content and
// reading the shadow's zero-fill is exactly what the monolith arm's
// MappedData() answers.
const MG_Pipe::MGPipeResourceRecord* elementRecord = nullptr;
if (elements.Slot < applierState.Resources.size()) {
const auto& candidate = applierState.Resources[elements.Slot];
if (candidate.Live && candidate.Gen == elements.Gen) elementRecord = &candidate;
}
if (elementRecord != nullptr && elementRecord->Desc.HasDefinedContent != 0) {
BufferImpl::RequireStagedCoverage(*elementResource, hostBytes, 0, sizeBytes,
"primitive_restart_substitution");
}
hasElementBuffer = true;
serverElementBinding = static_cast<Int>(elementResource->id);
source = hostBytes;
indexCount = sizeBytes / sourceIndexSize;
sourceByteOffset = reinterpret_cast<SizeT>(indices);
}
} else
#endif
{
const auto& indexBuffer = BoundElementArrayBuffer();
if (indexBuffer) {
hasElementBuffer = true;
// The WHOLE buffer is rewritten, not just this draw's range, so that every index
// keeps its position: an indirect draw's firstIndex lives in GPU memory and cannot be
// adjusted from here. It is an ELEMENT index, so it survives widening unchanged.
@@ -6200,7 +6535,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
indexCount = sizeBytes / sourceIndexSize;
sourceByteOffset = reinterpret_cast<SizeT>(indices);
} else {
}
}
if (!hasElementBuffer) {
// No element array buffer: `indices` is a client pointer, so only the draw's own
// range is readable and an indirect draw has nothing to read at all.
if (count <= 0 || indices == nullptr || sourceIndexSize == 0) {
@@ -6254,14 +6591,22 @@ namespace MobileGL::MG_Backend::DirectGLES {
m_valid = false;
return;
}
m_previousBinding = 0;
#if MOBILEGL_BUILD_DISAGGREGATED
if (serverElementBinding >= 0) {
m_previousBinding = static_cast<Uint>(serverElementBinding);
} else
#endif
{
m_previousBinding = BoundElementArrayBufferId();
}
BufferImpl::BindBufferId(GL_ELEMENT_ARRAY_BUFFER, g_restartIndices.id);
m_substituted = true;
m_indexType = destinationType;
// The rewritten copy starts at byte 0 of the scratch buffer and holds one
// destination-width element per source element, so an EBO-sourced draw keeps its ELEMENT
// offset (rescaled to the new width) and a client-memory draw reads from the front.
m_indices = indexBuffer
m_indices = hasElementBuffer
? reinterpret_cast<const void*>((sourceByteOffset / sourceIndexSize) * destinationIndexSize)
: nullptr;
}
@@ -6292,6 +6637,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
PrepareForDraw(syncBit);
const auto& currentVAO = MGB_CTX->GetBoundVertexArray();
if (currentVAO) {
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (G6, CONTRACT-P5C §5.4): frontend-keyed twin resolution, named debt inside
// the scope - P3b/P4b rekeys the registry onto handles.
const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry;
#endif
auto* backendVAOSlot = VertexArrayImpl::g_backendVertexArrayObjects.Find(currentVAO.get());
if (backendVAOSlot && *backendVAOSlot) {
(*backendVAOSlot)->SyncClientSideAttributesForDrawArrays(currentVAO, first, count);
@@ -6333,6 +6683,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
for (GLsizei i = 0; i < drawcount; ++i) {
// Client-side arrays are uploaded per sub-draw range, like the single DrawArrays path.
if (currentVAO) {
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (G6, CONTRACT-P5C §5.4): frontend-keyed twin resolution, named debt
// inside the scope - P3b/P4b rekeys the registry onto handles.
const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry;
#endif
auto* backendVAOSlot = VertexArrayImpl::g_backendVertexArrayObjects.Find(currentVAO.get());
if (backendVAOSlot && *backendVAOSlot) {
(*backendVAOSlot)->SyncClientSideAttributesForDrawArrays(currentVAO, first[i], count[i]);
@@ -6400,6 +6755,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
const auto& drawIndirectBuffer =
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (hd, CONTRACT-P5C §3.5): with an active transport the frontend binding slot
// is never read - the Execute* helpers resolve the buffer from the verb record's
// handle (T2). Monolith reads the slot as it always did.
MG_Config::Transport != MG_Config::TransportMode::Monolith
? SharedPtr<MG_State::GLState::BufferObject>(nullptr)
:
#endif
MGB_CTX->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
ExecuteIndexedIndirectCommands(mode, type, indexSize, commandBytes, reinterpret_cast<SizeT>(indirect),
drawIndirectBuffer, drawcount, stride, "MultiDrawElementsIndirect");
@@ -6431,6 +6794,53 @@ namespace MobileGL::MG_Backend::DirectGLES {
return;
}
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (hd, CONTRACT-P5C §3.5): with an active transport both buffers resolve from the
// verb record's handles (MGPDrawIndirect::Buffer / ParameterBuffer) and the count
// reads from the SERVER's staged shadow - the frontend binding slots, the frontend
// accessors (B3) and the client allocator (T2) are never read. Monolith takes the
// original body below, verbatim.
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
const auto& applierState = MG_Pipe::MGPipeApplier();
const SizeT commandOffset = reinterpret_cast<SizeT>(indirect);
const SizeT commandBytes = commandOffset + static_cast<SizeT>(stride) * static_cast<SizeT>(maxdrawcount - 1) +
sizeof(DrawElementsIndirectCommand);
auto* drawResource = BufferImpl::FindBufferResourceForHandle(applierState.VerbIndirectBuffer);
auto* paramResource =
BufferImpl::FindBufferResourceForHandle(applierState.VerbIndirectParameterBuffer);
const Uint8* const drawBytes = drawResource ? drawResource->hostBytes : nullptr;
const Uint8* const parameterBytes = paramResource ? paramResource->hostBytes : nullptr;
if (commandBytes > BufferImpl::ResourceWidthForHandle(applierState.VerbIndirectBuffer)) {
MGLOG_E_ONCE("MultiDrawElementsIndirectCount skipped: invalid GL_DRAW_INDIRECT_BUFFER binding or range");
return;
}
if (drawcount < 0 || static_cast<SizeT>(drawcount) + sizeof(Uint32) >
BufferImpl::ResourceWidthForHandle(applierState.VerbIndirectParameterBuffer)) {
MGLOG_E_ONCE("MultiDrawElementsIndirectCount skipped: invalid GL_PARAMETER_BUFFER binding or range");
return;
}
// No staged shadow means no count to read, not a wrong one - the monolith body's
// own rule for a buffer with no CPU shadow.
if (parameterBytes == nullptr || drawBytes == nullptr) {
MGLOG_E_ONCE("MultiDrawElementsIndirectCount skipped: CPU fallback cannot read the parameter or "
"draw-indirect buffer");
return;
}
BufferImpl::RequireStagedCoverage(*drawResource, drawBytes, commandOffset, commandBytes,
"multidraw_elements_indirect_count_commands");
BufferImpl::RequireStagedCoverage(*paramResource, parameterBytes, static_cast<SizeT>(drawcount),
static_cast<SizeT>(drawcount) + sizeof(Uint32),
"multidraw_elements_indirect_count_parameter");
Uint32 actualDrawCount = 0;
std::memcpy(&actualDrawCount, parameterBytes + drawcount, sizeof(actualDrawCount));
actualDrawCount = std::min<Uint32>(actualDrawCount, static_cast<Uint32>(maxdrawcount));
ExecuteIndexedIndirectCommands(mode, type, indexSize, drawBytes + commandOffset, commandOffset,
nullptr, static_cast<GLsizei>(actualDrawCount), stride,
"MultiDrawElementsIndirectCount");
return;
}
#endif
auto drawBuffer = MGB_CTX->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
auto parameterBuffer = MGB_CTX->GetBufferBindingSlot(BufferTarget::Parameter).GetBoundObject();
if (!drawBuffer) {
@@ -6502,6 +6912,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
const auto& drawIndirectBuffer =
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (hd, CONTRACT-P5C §3.5): with an active transport the frontend binding slot
// is never read - the Execute* helpers resolve the buffer from the verb record's
// handle (T2). Monolith reads the slot as it always did.
MG_Config::Transport != MG_Config::TransportMode::Monolith
? SharedPtr<MG_State::GLState::BufferObject>(nullptr)
:
#endif
MGB_CTX->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
ExecuteArraysIndirectCommands(mode, commandBytes, reinterpret_cast<SizeT>(indirect), drawIndirectBuffer,
drawcount, stride, "MultiDrawArraysIndirect");
@@ -6533,6 +6951,48 @@ namespace MobileGL::MG_Backend::DirectGLES {
DrawSyncFlags syncBit = DrawSyncBit::IndirectBuffer | DrawSyncBit::Instancing;
PrepareForDraw(syncBit);
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (hd, CONTRACT-P5C §3.5): see the indexed twin - the verb record's handles and
// the SERVER's staged shadow, never the frontend bindings or the client allocator.
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
const auto& applierState = MG_Pipe::MGPipeApplier();
const SizeT commandOffset = reinterpret_cast<SizeT>(indirect);
const SizeT commandBytes = commandOffset + static_cast<SizeT>(stride) * static_cast<SizeT>(maxdrawcount - 1) +
sizeof(DrawArraysIndirectCommand);
auto* drawResource = BufferImpl::FindBufferResourceForHandle(applierState.VerbIndirectBuffer);
auto* paramResource =
BufferImpl::FindBufferResourceForHandle(applierState.VerbIndirectParameterBuffer);
const Uint8* const drawBytes = drawResource ? drawResource->hostBytes : nullptr;
const Uint8* const parameterBytes = paramResource ? paramResource->hostBytes : nullptr;
if (commandBytes > BufferImpl::ResourceWidthForHandle(applierState.VerbIndirectBuffer)) {
MGLOG_E_ONCE("MultiDrawArraysIndirectCount skipped: invalid GL_DRAW_INDIRECT_BUFFER binding or range");
return;
}
if (drawcount < 0 || static_cast<SizeT>(drawcount) + sizeof(Uint32) >
BufferImpl::ResourceWidthForHandle(applierState.VerbIndirectParameterBuffer)) {
MGLOG_E_ONCE("MultiDrawArraysIndirectCount skipped: invalid GL_PARAMETER_BUFFER binding or range");
return;
}
if (parameterBytes == nullptr || drawBytes == nullptr) {
MGLOG_E_ONCE("MultiDrawArraysIndirectCount skipped: CPU fallback cannot read the parameter or "
"draw-indirect buffer");
return;
}
BufferImpl::RequireStagedCoverage(*drawResource, drawBytes, commandOffset, commandBytes,
"multidraw_arrays_indirect_count_commands");
BufferImpl::RequireStagedCoverage(*paramResource, parameterBytes, static_cast<SizeT>(drawcount),
static_cast<SizeT>(drawcount) + sizeof(Uint32),
"multidraw_arrays_indirect_count_parameter");
Uint32 actualDrawCount = 0;
std::memcpy(&actualDrawCount, parameterBytes + drawcount, sizeof(actualDrawCount));
actualDrawCount = std::min<Uint32>(actualDrawCount, static_cast<Uint32>(maxdrawcount));
ExecuteArraysIndirectCommands(mode, drawBytes + commandOffset, commandOffset, nullptr,
static_cast<GLsizei>(actualDrawCount), stride,
"MultiDrawArraysIndirectCount");
return;
}
#endif
auto drawBuffer = MGB_CTX->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
auto parameterBuffer = MGB_CTX->GetBufferBindingSlot(BufferTarget::Parameter).GetBoundObject();
if (!drawBuffer) {
@@ -6706,6 +7166,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
const auto& drawIndirectBuffer =
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (hd, CONTRACT-P5C §3.5): with an active transport the frontend binding slot
// is never read - the Execute* helpers resolve the buffer from the verb record's
// handle (T2). Monolith reads the slot as it always did.
MG_Config::Transport != MG_Config::TransportMode::Monolith
? SharedPtr<MG_State::GLState::BufferObject>(nullptr)
:
#endif
MGB_CTX->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
ExecuteIndexedIndirectCommands(mode, type, indexSize, commandBytes, reinterpret_cast<SizeT>(indirect),
drawIndirectBuffer, 1, sizeof(DrawElementsIndirectCommand),
@@ -6747,6 +7215,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
const auto& drawIndirectBuffer =
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (hd, CONTRACT-P5C §3.5): with an active transport the frontend binding slot
// is never read - the Execute* helpers resolve the buffer from the verb record's
// handle (T2). Monolith reads the slot as it always did.
MG_Config::Transport != MG_Config::TransportMode::Monolith
? SharedPtr<MG_State::GLState::BufferObject>(nullptr)
:
#endif
MGB_CTX->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
ExecuteArraysIndirectCommands(mode, commandBytes, reinterpret_cast<SizeT>(indirect), drawIndirectBuffer, 1,
sizeof(DrawArraysIndirectCommand), "DrawArraysIndirect");
@@ -7763,6 +8239,63 @@ namespace MobileGL::MG_Backend::DirectGLES {
DebugImpl::OpenGLScopeMarker marker(__func__);
#endif
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (hd, CONTRACT-P5C §3.3): THE NAMED ARM. A blit record whose ReadFbo/DrawFbo are
// non-null is a glBlitNamedFramebuffer: both framebuffers resolve from the record's
// handles through the FBO twin table (the client's ScopedBlitBindings staging and this
// function's binding-slot read at the bottom are gone from the split path), and the
// sink is told the pair was consumed - a backend that reaches here without consuming
// it has no named arm and the verb declines there.
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
auto& applierState = MG_Pipe::MGPipeApplier();
const MG_Pipe::MGPipeHandle readFbo = applierState.VerbBlitReadFbo;
const MG_Pipe::MGPipeHandle drawFbo = applierState.VerbBlitDrawFbo;
if (!MG_Pipe::MGPipeHandleIsNull(readFbo) || !MG_Pipe::MGPipeHandleIsNull(drawFbo)) {
applierState.VerbBlitNamedConsumed = true;
TextureImpl::SyncNeccessaryTextures();
RenderStateImpl::SyncRenderState();
SyncAndBindFramebufferByHandle(readFbo, FramebufferTarget::Read, /*forceSync=*/true);
SyncAndBindFramebufferByHandle(drawFbo, FramebufferTarget::Draw, /*forceSync=*/true);
MGLOG_D("ES BlitNamedFramebuffer({%u,%u} -> {%u,%u}, %d, %d, %d, %d, %d, %d, %d, %d, 0x%x, %s)",
readFbo.Slot, readFbo.Gen, drawFbo.Slot, drawFbo.Gen, srcX0, srcY0, srcX1,
srcY1, dstX0, dstY0, dstX1, dstY1, mask,
MG_Util::ConvertGLEnumToString(filter).c_str());
// See the bound arm below: only the probed defect makes this do anything. It
// reads the two frontend objects' attachments (the object-class channel P3b/P4b
// retires), reached through the twins' state notes - and for a default endpoint
// the default framebuffer object itself, exactly as the bound arm's binding-slot
// read hands it. A missing object only skips the workaround.
const auto objectFor = [](MG_Pipe::MGPipeHandle handle) {
if (handle == MG_Pipe::kMGPipeDefaultFramebuffer) {
return SharedPtr<MG_State::GLState::FramebufferObject>(
MG_Impl::GLImpl::FramebufferImpl::pDefaultFramebufferInfo->defaultFBO);
}
return FramebufferImpl::g_backendFramebufferObjects.StateForHandle(handle);
};
const auto readObj = objectFor(readFbo);
const auto drawObj = objectFor(drawFbo);
if (readObj && drawObj) {
mask &= ~BlitLayeredDestinationAspects(readObj, drawObj, srcX0, srcY0, srcX1, srcY1,
dstX0, dstY0, dstX1, dstY1, mask);
}
if (mask != 0) {
IssueBlitWithResolveFallback(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1,
mask, filter);
}
// The driver's bindings are now the two NAMED framebuffers; put the bound ones
// back exactly as the monolith named entry point does.
ForceBindCurrentFBO(FramebufferTarget::Read);
ForceBindCurrentFBO(FramebufferTarget::Draw);
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) {
MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str());
});
return;
}
}
#endif
TextureImpl::SyncNeccessaryTextures();
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) {
MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str());
@@ -7859,6 +8392,29 @@ namespace MobileGL::MG_Backend::DirectGLES {
return false;
}
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (hd, CONTRACT-P5C §3.4): under an active transport the destination is the handle
// the copy_framebuffer_to_texture record carried; the unit binding slot and the client
// allocator are never read (T2/T4).
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
const MG_Pipe::MGPipeHandle dstHandle = MG_Pipe::MGPipeApplier().VerbCopyTexDst;
auto* backendTextureSlot = TextureImpl::g_backendTextureObjects.GetOrCreateByHandle(dstHandle);
if (backendTextureSlot == nullptr) {
MGLOG_E_ONCE("%s: the verb's destination texture handle {%u, %u} is refused by "
"the twin slot table (live generation %u)",
__func__, dstHandle.Slot, dstHandle.Gen,
TextureImpl::g_backendTextureObjects.LiveGenAt(dstHandle.Slot));
return false;
}
auto& backendObj = *backendTextureSlot;
if (!backendObj) {
backendObj = MakeShared<TextureImpl::BackendTextureObject>();
}
backendObj->Bind(TextureImpl::ConvertTextureTargetToBackendGLEnum(textureTarget), unit);
return true;
}
#endif
const auto& bindingSlot = textureUnit.GetBindingSlot(textureTarget);
{
const auto& textureObject = bindingSlot.GetBoundObject();
@@ -8089,8 +8645,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
// The frontend has already defined the generated chain before emitting this verb.
// Its resource_respecify carries that shape, so the server only verifies the
// descriptor; it must not allocate or dirty the client's level shadows again.
// Identity resolution is the same existing registry lookup used by texture sync.
const auto handle = TextureImpl::g_backendTextureObjects.HandleOf(texture.get());
// P5c (hd): identity is the handle the generate_mipmap record carried
// (MGPMipPlan::Res, the verb stash) - the client allocator is never probed (T2).
const auto handle = MG_Pipe::MGPipeApplier().VerbMipRes;
const auto* record = PipeTextureRecordForHandle(handle);
if (record == nullptr || record->Desc.Width == 0 || record->Desc.Levels == 0) {
MG_Pipe::MGPipeUnmigratedEmulation("generate-mipmap-storage");
@@ -8188,6 +8745,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
return;
}
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (G6, CONTRACT-P5C §5.4): frontend-keyed twin resolution, named debt inside
// the scope - P3b/P4b rekeys the registry onto handles.
const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry;
#endif
auto* backendTextureSlot = TextureImpl::g_backendTextureObjects.Find(texture.get());
if (!backendTextureSlot || !*backendTextureSlot) {
return;
@@ -8508,6 +9070,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
// The record resolution is the same registry lookup texture sync and
// EnsureGenerateMipmapStorageAllocated's disaggregated arm already make.
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
// P5c (G6, CONTRACT-P5C §5.4): frontend-keyed twin resolution, named debt inside
// the scope - P3b/P4b rekeys the registry onto handles.
const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry;
const auto pushedHandle = TextureImpl::g_backendTextureObjects.HandleOf(texture.get());
const auto* pushedRecord = PipeTextureRecordForHandle(pushedHandle);
if (pushedRecord == nullptr || pushedRecord->Desc.Width == 0 || pushedRecord->Desc.Levels == 0) {
@@ -8560,6 +9125,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
// same §1 extent derivation, same registry resolution, same refusal when the handle
// arm has no record to read.
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
// P5c (G6, CONTRACT-P5C §5.4): frontend-keyed twin resolution, named debt inside
// the scope - P3b/P4b rekeys the registry onto handles.
const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry;
const auto pushedHandle = TextureImpl::g_backendTextureObjects.HandleOf(texture.get());
const auto* pushedRecord = PipeTextureRecordForHandle(pushedHandle);
if (pushedRecord == nullptr || pushedRecord->Desc.Width == 0 || pushedRecord->Desc.Levels == 0) {
@@ -8623,6 +9191,28 @@ namespace MobileGL::MG_Backend::DirectGLES {
// Bind necessary FBO and texture
BindCurrentFBO(FramebufferTarget::Read);
Uint activeTextureUnit = MGB_CTX->GetActiveTextureUnit();
TextureImpl::BackendTextureObject* dstBackendTexture = nullptr;
TextureInternalFormat mgInternalFormat{};
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (hd, CONTRACT-P5C §3.4): with an active transport the destination resolves from
// the handle the copy_framebuffer_to_texture record carried (MGPCopyFromFramebuffer::
// Dst, the verb stash) and its format is the applier descriptor's - the client's
// texture-unit binding slot and the client slot allocator are never read (T2/T4).
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
const MG_Pipe::MGPipeHandle dstHandle = MG_Pipe::MGPipeApplier().VerbCopyTexDst;
const auto* dstRecord = PipeTextureRecordForHandle(dstHandle);
auto* backendTextureSlot = TextureImpl::g_backendTextureObjects.FindByHandle(dstHandle);
if (backendTextureSlot == nullptr || *backendTextureSlot == nullptr || dstRecord == nullptr) {
MGLOG_E_ONCE("CopyTexImage2D: the verb's destination texture {%u, %u} has no twin "
"or no applier record on this side",
dstHandle.Slot, dstHandle.Gen);
return;
}
dstBackendTexture = backendTextureSlot->get();
mgInternalFormat = static_cast<TextureInternalFormat>(dstRecord->Desc.InternalFormat);
} else
#endif
{
const auto& textureObject = MGB_CTX->GetTextureUnitObject((Int)activeTextureUnit)
.GetBindingSlot(MG_Util::ConvertGLEnumToTextureTarget(target))
.GetBoundObject();
@@ -8632,9 +9222,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
textureObject ? textureObject->GetExternalIndex() : 0);
return;
}
(*backendTextureSlot)->Bind(target, activeTextureUnit);
dstBackendTexture = backendTextureSlot->get();
mgInternalFormat = textureObject->GetFormat();
}
dstBackendTexture->Bind(target, activeTextureUnit);
auto mgInternalFormat = textureObject->GetFormat();
GLenum format = GL_DEPTH_COMPONENT;
GLenum type = GL_UNSIGNED_INT;
TextureImpl::GenerateTextureFormatInfo(mgInternalFormat, &internalformat, &format, &type,
@@ -8668,7 +9260,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str());
});
auto currentTex = (GLint)(*backendTextureSlot)->GetBackendTextureId();
auto currentTex = (GLint)dstBackendTexture->GetBackendTextureId();
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) {
MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str());
});
@@ -8718,6 +9310,23 @@ namespace MobileGL::MG_Backend::DirectGLES {
// Bind necessary FBO and texture
BindCurrentFBO(FramebufferTarget::Read);
auto activeTextureUnit = MGB_CTX->GetActiveTextureUnit();
TextureImpl::BackendTextureObject* dstBackendTexture = nullptr;
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (hd, CONTRACT-P5C §3.4): see CopyTexImage2D - the record's Dst handle, never the
// client's unit binding slot or the client allocator (T2/T4).
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
const MG_Pipe::MGPipeHandle dstHandle = MG_Pipe::MGPipeApplier().VerbCopyTexDst;
auto* backendTextureSlot = TextureImpl::g_backendTextureObjects.FindByHandle(dstHandle);
if (backendTextureSlot == nullptr || *backendTextureSlot == nullptr) {
MGLOG_E_ONCE("CopyTexSubImage2D: the verb's destination texture {%u, %u} has no "
"twin on this side",
dstHandle.Slot, dstHandle.Gen);
return;
}
dstBackendTexture = backendTextureSlot->get();
} else
#endif
{
const auto& textureObject = MGB_CTX->GetTextureUnitObject(activeTextureUnit)
.GetBindingSlot(MG_Util::ConvertGLEnumToTextureTarget(target))
.GetBoundObject();
@@ -8727,7 +9336,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
textureObject ? textureObject->GetExternalIndex() : 0);
return;
}
(*backendTextureSlot)->Bind(target, activeTextureUnit);
dstBackendTexture = backendTextureSlot->get();
}
dstBackendTexture->Bind(target, activeTextureUnit);
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) {
MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str());
@@ -8749,7 +9360,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
});
} else {
MGLOG_D("%s: Backend depth", __func__);
auto currentTex = (*backendTextureSlot)->GetBackendTextureId();
auto currentTex = dstBackendTexture->GetBackendTextureId();
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) {
MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str());
});
@@ -8992,6 +9603,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
static SharedPtr<RenderbufferImpl::BackendRenderbufferObject> SyncRenderbufferObjectToBackend(
const SharedPtr<MG_State::GLState::RenderbufferObject>& renderbufferObject) {
if (!renderbufferObject) return nullptr;
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (G6, CONTRACT-P5C §5.4): frontend-keyed twin resolution (and the mint on a first
// sync), named debt inside the scope - P3b/P4b rekeys the registry onto handles.
const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry;
#endif
SharedPtr<RenderbufferImpl::BackendRenderbufferObject> backendRenderbufferObject;
if (auto* slot = RenderbufferImpl::g_backendRenderbufferObjects.Find(renderbufferObject.get())) {
backendRenderbufferObject = *slot;
@@ -9321,6 +9937,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
auto& programObject = MGB_CTX->GetProgramObject(program);
if (!programObject) return;
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (G6, CONTRACT-P5C §5.4): frontend-keyed twin resolution, named debt inside the
// scope - P3b/P4b rekeys the registry onto handles.
const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry;
#endif
auto* backendProgramSlot = PrgramImpl::g_backendProgramObjects.Find(programObject.get());
if (!backendProgramSlot || !*backendProgramSlot) return;
auto& backendObj = *backendProgramSlot;
@@ -11171,6 +11792,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
MGLOG_D("GetTexImage: bound texture object = %p (name=%u)", textureObject.get(),
textureObject ? textureObject->GetExternalIndex() : 0);
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (G6, CONTRACT-P5C §5.4): frontend-keyed twin resolution, named debt inside the
// scope - P3b/P4b rekeys the registry onto handles.
const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry;
#endif
auto* backendTextureSlot = TextureImpl::g_backendTextureObjects.Find(textureObject.get());
if (!backendTextureSlot || !*backendTextureSlot) {
+156 -20
View File
@@ -220,6 +220,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
}, &death);
return;
}
// P5c merge coordination: while the death notice still rides the mailbox it lands
// HERE, on the apply thread, and every arm's DestroyByLifetimeId probes the client
// allocator. That hop is exactly what ct's object_death record replaces (the sink
// then resolves by the record's handle and the scope dies with the mailbox); until
// ct lands the probes are named debt inside the G6 scope, not unwrapped violations.
const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry;
#endif
switch (kind) {
case MG_Pipe::MGPipeKind::Texture:
@@ -2841,6 +2847,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
return twin ? twin->get() : nullptr;
}
#if MOBILEGL_BUILD_DISAGGREGATED
void RequireStagedCoverage(GLESBufferResource& resource, const Uint8* hostBase, SizeT start,
SizeT end, const char* site) {
ServerStaged().RequireCoverage(&resource, hostBase, start, end, site);
}
#endif
MG_Pipe::MGPipeHandle HandleOfBuffer(const MG_State::GLState::BufferObject* bufferObject) {
if (bufferObject == nullptr) return MG_Pipe::kMGPipeNullHandle;
// Through the table's own single-entry front memo rather than straight into
@@ -2851,6 +2864,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
// allocator probe on a miss - and remembers the answer, which the minting overload
// was previously the only writer of. A null answer is deliberately never memoised
// (see the comment at HandleOf).
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c merge coordination (CONTRACT-P5C §3.1's named exemption): every caller of
// this function is a site whose handle-carrying records (set_shader_buffers /
// set_stream_output_targets) the client does not emit until P4b, so the probe
// below runs inside the scoped exemption. The scope, not a silent guard removal,
// is what keeps the debt named and greppable.
const MG_Pipe::MGPipeReverseAnnouncementScope reverseAnnouncement;
#endif
return g_backendBufferResources.HandleOf(bufferObject);
}
@@ -3245,7 +3266,17 @@ namespace MobileGL::MG_Backend::DirectGLES {
// D-N keeps this call here for P3a. It can queue ranges and move the record, so
// everything below is read AFTER it.
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (hd, CONTRACT-P5C §3.8): under an active transport the frontend accessor is a
// layer-1 surface ("buffer-legacy-arm") and the client's own pre-verb
// persistent-map push is the only producer; the call is skipped outright. Under
// monolith D-N's placement stands.
if (bufferObject && MG_Config::Transport == MG_Config::TransportMode::Monolith) {
bufferObject->SyncPersistentMappedRange();
}
#else
if (bufferObject) bufferObject->SyncPersistentMappedRange();
#endif
const auto* record = ResourceRecordOf(res);
const SizeT size = record != nullptr ? static_cast<SizeT>(record->Desc.Width) : 0;
@@ -3273,24 +3304,28 @@ namespace MobileGL::MG_Backend::DirectGLES {
// and its bytes are read through persistentPtr.
if (hostBase != nullptr) resource->hostBytes = hostBase;
// "DOES THE SHADOW THIS PATH IS ABOUT TO UPLOAD HOLD MEANINGFUL BYTES?" - and it has
// to be asked of the SAME thing the bytes come from, which is why it is not
// record->Desc.HasDefinedContent. The descriptor states what was true at the last
// resource_respecify and NOTHING refreshes it afterwards: resource_subdata,
// resource_flush_range, buffer_subdata_resident and OnGpuWritten all define content
// (BufferObject.cpp:85, :101, :127, :365, :441) without re-emitting a descriptor,
// and re-emitting one per glBufferSubData would be new wire traffic D-J forbids. So
// after the ordinary glBufferData(NULL) + glBufferSubData idiom the descriptor still
// says "undefined", this full re-upload passed nullptr, RespecifyStorageWith
// CLEARED the queued ranges and stamped syncedChangeSerial - i.e. an empty store
// declared current, with the application's bytes dropped and nothing saying so.
// The legacy arm pairs bufferObject.MappedData() with bufferObject.HasDefinedContent()
// (RespecifyStorageNow) and this arm pairs the same two, so the source of the bytes
// and the statement about them can never disagree. Declared, like C-1's IsMapped():
// it is a frontend read this function keeps for P3a and it retires the moment the
// client publishes a live content flag beside the descriptor (P5/P8). With no
// frontend object (the handle-only drains) the descriptor is all there is.
// to be asked of the SAME thing the bytes come from. The legacy arm pairs
// bufferObject.MappedData() with bufferObject.HasDefinedContent() (RespecifyStorageNow)
// and this arm pairs the same two, so the source of the bytes and the statement about
// them can never disagree.
//
// P5c (hd, CONTRACT-P5C §3.6 / B1): with an active transport the answer is the
// DESCRIPTOR's bit - which the client publishes on ResourceCreate/Respecify - OR the
// staged coverage the content records behind it delivered. The coverage half is what
// keeps the answer equal to the frontend flag's in the one case the descriptor alone
// cannot see: an ORPHANING respecify (descriptor clear) followed by glBufferSubData,
// which defines content without a new descriptor - the bytes crossed as
// resource_subdata and the staged store's coverage is exactly "content defined since
// the last orphan". With no coverage and a clear bit the store is undefined by the
// application's own declaration and the upload stays a pure NULL reallocation.
const Bool shadowHasContent =
bufferObject ? bufferObject->HasDefinedContent() : (record->Desc.HasDefinedContent != 0);
#if MOBILEGL_BUILD_DISAGGREGATED
MG_Config::Transport != MG_Config::TransportMode::Monolith
? (record->Desc.HasDefinedContent != 0 ||
ServerStaged().CoveredRunCount(resource) != 0)
:
#endif
(bufferObject ? bufferObject->HasDefinedContent() : (record->Desc.HasDefinedContent != 0));
const void* initialData = shadowHasContent ? hostBase : nullptr;
// M-3 / codex 4: a RespecifyStorageWith(..., initialData != nullptr) is a WHOLE-STORE
// [0, size) upload from the base, so it owes the same coverage the pending-range drain
@@ -4438,6 +4473,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
// MGPipeSlots().FindByLifetimeId, which is a hash lookup. HandleOf answers
// identically - the same allocator probe on a miss - and remembers the answer; a
// null answer is deliberately never memoised (SlotTables.h, at HandleOf).
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (G6, CONTRACT-P5C §5.4): frontend-keyed twin resolution, named debt inside
// the scope - P3b/P4b rekeys the sampler-view registry onto handles.
const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry;
#endif
return g_backendSamplerViews.HandleOf(textureObject);
}
} // namespace SamplerViewImpl
@@ -5769,7 +5809,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
// because that is what this arm consumes.
Bool markedRemintPull = false;
const MG_Pipe::MGPipeHandle rearmRes =
TextureResourceSubsystemEnabled() ? g_backendTextureObjects.HandleOf(stateTextureObject.get())
TextureResourceSubsystemEnabled()
? [&]() {
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (G6, CONTRACT-P5C §5.4): frontend-keyed resolution, named
// debt inside the scope - P3b/P4b rekeys the registry.
const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry;
#endif
return g_backendTextureObjects.HandleOf(stateTextureObject.get());
}()
: MG_Pipe::kMGPipeNullHandle;
const Uint32 rearmResourceTarget =
MG_Pipe::MGPipeResourceTargetForTextureTarget(stateTextureObject->GetTarget());
@@ -6932,6 +6980,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
const MG_Pipe::MGPipeResourceRecord* pushedStorage = nullptr;
MG_Pipe::MGPipeHandle pushedRes = MG_Pipe::kMGPipeNullHandle;
if (TextureResourceSubsystemEnabled()) {
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (G6, CONTRACT-P5C §5.4): the sync arrives holding the frontend object
// (the object-class rows) and resolves its handle by frontend identity -
// named debt inside the scope, P3b/P4b rekeys the registry onto handles.
const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry;
#endif
pushedRes = g_backendTextureObjects.HandleOf(stateTextureObject.get());
pushedStorage = PipeTextureRecordForHandle(pushedRes);
if (pushedStorage == nullptr) {
@@ -8279,6 +8333,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
// object this backend still arrives holding. Under a real split the handle rides in
// the payload; the client mints it off this object's lifetime id, so the allocator
// resolves it here through the table's own single-entry front memo.
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (G6, CONTRACT-P5C §5.4): frontend-keyed resolution, named debt inside the
// scope - P3b/P4b rekeys the registry onto handles.
const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry;
#endif
const MG_Pipe::MGPipeHandle res = g_backendTextureObjects.HandleOf(stateTextureObject.get());
const auto* record = PipeTextureRecordForHandle(res);
if (record == nullptr) {
@@ -8510,6 +8569,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
#if MOBILEGL_PIPE_PUSH
const MG_Pipe::MGPipeResourceRecord* BackendTextureObject::ResolvePushedTextureParams(
const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject) {
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (G6, CONTRACT-P5C §5.4): frontend-keyed resolution, named debt inside the
// scope - P3b/P4b rekeys the registry onto handles.
const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry;
#endif
const MG_Pipe::MGPipeHandle res = g_backendTextureObjects.HandleOf(stateTextureObject.get());
const auto* record = PipeTextureRecordForHandle(res);
if (record == nullptr) {
@@ -9044,6 +9108,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (attachmentObject.IsTexture()) {
const auto& textureObject = attachmentObject.GetTexture();
SharedPtr<TextureImpl::BackendTextureObject> backendTextureObject;
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (G6, CONTRACT-P5C §5.4): frontend-keyed twin resolution (and the mint on
// a first attach), named debt inside the scope - P3b/P4b rekeys the registry.
const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry;
#endif
if (auto* backendTextureSlot = TextureImpl::g_backendTextureObjects.Find(textureObject.get())) {
backendTextureObject = *backendTextureSlot;
} else {
@@ -9093,6 +9162,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
} else if (attachmentObject.IsRenderbuffer()) {
const auto& renderbufferObject = attachmentObject.GetRenderbuffer();
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (G6, CONTRACT-P5C §5.4): frontend-keyed twin resolution (and the mint on
// a first attach), named debt inside the scope - P3b/P4b rekeys the registry.
const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry;
#endif
SharedPtr<RenderbufferImpl::BackendRenderbufferObject> backendRenderbufferObject;
if (auto* backendRenderbufferSlot =
RenderbufferImpl::g_backendRenderbufferObjects.Find(renderbufferObject.get())) {
@@ -9405,7 +9479,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
surface.Res.Slot, surface.Res.Gen);
return false;
}
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (hd): the corroborating cross-check against the frontend object's handle
// is a client-allocator probe (T2). Under an active transport the record is the
// ONLY statement of identity (rule E) and the check does not run; monolith
// keeps it verbatim. The record's own handle was already validated above (N-3).
if (MG_Config::Transport == MG_Config::TransportMode::Monolith &&
!(TextureImpl::g_backendTextureObjects.HandleOf(textureObject.get()) == surface.Res)) {
#else
if (!(TextureImpl::g_backendTextureObjects.HandleOf(textureObject.get()) == surface.Res)) {
#endif
MGLOG_E_ONCE("MGPipe: attachment record names texture {%u, %u} but the frontend "
"attachment's texture %u is handle {%u, %u} - refusing rather than "
"attaching one of them",
@@ -9477,6 +9560,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
surface.Res.Slot, surface.Res.Gen);
return false;
}
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (G6, CONTRACT-P5C §5.4): the corroborating cross-check below resolves the
// frontend renderbuffer's handle by frontend identity - named debt inside the
// scope - P3b/P4b rekeys the registry onto handles.
const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry;
#endif
if (!(RenderbufferImpl::g_backendRenderbufferObjects.HandleOf(renderbufferObject.get()) ==
surface.Res)) {
MGLOG_E_ONCE("MGPipe: attachment record names renderbuffer {%u, %u} but the frontend "
@@ -9620,7 +9709,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
// FramebufferAttachmentType::None, i.e. GL_NONE. A surface that matches no colour
// point cannot be a legal read buffer and is refused rather than guessed.
if (FramebufferSubsystemEnabled()) {
const MG_Pipe::MGPipeHandle fbo = g_backendFramebufferObjects.HandleOf(stateFBOObject.get());
// P5c (hd): with an active transport the handle is the caller's
// (m_pushedSyncHandle), never the client allocator's - see SyncToBackend.
const MG_Pipe::MGPipeHandle fbo =
#if MOBILEGL_BUILD_DISAGGREGATED
MG_Config::Transport != MG_Config::TransportMode::Monolith
? m_pushedSyncHandle
:
#endif
g_backendFramebufferObjects.HandleOf(stateFBOObject.get());
// ID-19: the OBJECT's record, not "the record of whatever is bound to READ". A
// framebuffer whose read buffer is being pushed need not be the read binding at
// all - glNamedFramebufferReadBuffer and the DSA clears reach here by name - and
@@ -9848,7 +9945,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
// there is no framebuffer create or destroy in the catalogue and none is invented
// - so the handle exists purely to key set_framebuffer_state, which is exactly
// what it is used for here.
const MG_Pipe::MGPipeHandle fbo = g_backendFramebufferObjects.HandleOf(stateFBOObject.get());
//
// P5c (hd): with an active transport the handle is the one the caller is applying
// (m_pushedSyncHandle - the record-driven sync set it), and the client allocator
// is never probed (T2). A null there means a caller reached this arm without a
// record, which the null-record refusal below names.
const MG_Pipe::MGPipeHandle fbo =
#if MOBILEGL_BUILD_DISAGGREGATED
MG_Config::Transport != MG_Config::TransportMode::Monolith
? m_pushedSyncHandle
:
#endif
g_backendFramebufferObjects.HandleOf(stateFBOObject.get());
pushedRecord = PushedFramebufferRecord(fbo);
if (pushedRecord == nullptr) {
MGLOG_E_ONCE("MGPipe: framebuffer %u has no applier record on the handle arm, so it "
@@ -10126,6 +10234,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
// Verify that the backend object's name and parameters match the frontend attachment state
if (attachmentObject.IsTexture()) {
const auto& textureObject = attachmentObject.GetTexture();
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (G6, CONTRACT-P5C §5.4): frontend-keyed twin resolution, named
// debt inside the scope - P3b/P4b rekeys the registry.
const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry;
#endif
auto* backendTextureSlot = TextureImpl::g_backendTextureObjects.Find(textureObject.get());
MOBILEGL_ASSERT(backendTextureSlot != nullptr && *backendTextureSlot != nullptr,
"No backend texture found while framebuffer reports texture attachment.");
@@ -10142,6 +10255,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
"Attachment texture level mismatch between GLES and state object.");
} else if (attachmentObject.IsRenderbuffer()) {
const auto& renderbufferObject = attachmentObject.GetRenderbuffer();
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (G6, CONTRACT-P5C §5.4): frontend-keyed twin resolution, named
// debt inside the scope - P3b/P4b rekeys the registry.
const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry;
#endif
auto* backendRboSlot =
RenderbufferImpl::g_backendRenderbufferObjects.Find(renderbufferObject.get());
MOBILEGL_ASSERT(
@@ -12682,6 +12800,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
// stateProgramObject. The verify build is where the codec is exercised, by
// serialising, deserialising and field-comparing before storing.
if (ProgramSubsystemEnabled()) {
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (G6, CONTRACT-P5C §5.4): the program's ShaderCso handle is resolved by
// frontend identity below - frontend-keyed twin resolution, named debt inside
// the scope - P3b/P4b rekeys the registry onto handles.
const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry;
#endif
// MONOLITH GLUE: the ShaderCso handle of a program this backend still arrives
// holding. The reader hides the composite band, so a program-pipeline composite
// - which the server must never learn is one - resolves through the same call.
@@ -13000,6 +13124,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
// the values are taken from the object, which in monolith are the very
// values the client content-addressed, so the picture stays right while
// the gap is visible rather than silent.
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (G6, CONTRACT-P5C §5.4): the registration probe below resolves the
// sampler's handle by frontend identity - frontend-keyed twin resolution,
// named debt inside the scope - P3b/P4b rekeys the registry onto handles.
const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry;
#endif
if (!MG_Pipe::MGPipeHandleIsNull(
g_backendSamplerObjects.HandleOf(stateSamplerObject.get()))) {
MGLOG_E_ONCE("MGPipe: sampler %u was synced without its unit's SamplerCso handle, so "
@@ -13291,6 +13421,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
#if MOBILEGL_PIPE_PUSH
const MG_Pipe::MGPipeResourceRecord* pushedRecord = nullptr;
if (TextureResourceSubsystemEnabled()) {
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (G6, CONTRACT-P5C §5.4): the renderbuffer's handle is resolved by
// frontend identity below - frontend-keyed twin resolution, named debt inside
// the scope - P3b/P4b rekeys the registry onto handles.
const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry;
#endif
// MONOLITH GLUE, named as such: the Renderbuffer handle of an object this
// backend still arrives holding. Under a real split it rides in the payload.
const MG_Pipe::MGPipeHandle res = g_backendRenderbufferObjects.HandleOf(stateRBOObject.get());
+33
View File
@@ -467,6 +467,23 @@ namespace MobileGL::MG_Backend::DirectGLES {
return m_slotTable.LiveGenAt(slot);
}
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (hd): the two halves of SlotTables.h's state note, forwarded. A caller holding
// both the record's handle and the frontend object (the record-driven sync) notes the
// object so a later handle-only resolution can reach it without the client allocator.
void NoteStateForHandle(MG_Pipe::MGPipeHandle handle, const StatePtr& stateObj) {
if (EsprytSlotTablesEnabled()) {
m_slotTable.NoteStateForHandle(handle, stateObj);
}
}
StatePtr StateForHandle(MG_Pipe::MGPipeHandle handle) const {
if (EsprytSlotTablesEnabled()) {
return m_slotTable.StateForHandle(handle);
}
return nullptr;
}
#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
@@ -892,6 +909,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
GLESBufferResource* GetOrCreateBufferResourceForHandle(MG_Pipe::MGPipeHandle res);
GLESBufferResource* FindBufferResourceForHandle(MG_Pipe::MGPipeHandle res);
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (hd): the staged-coverage assertion (StagedShadowStore::RequireCoverage) for a
// read of the server shadow outside the upload ladders - the indirect command-byte
// resolver. A no-op for a base that is not this resource's server shadow.
void RequireStagedCoverage(GLESBufferResource& resource, const Uint8* hostBase, SizeT start,
SizeT end, const char* site);
#endif
// MONOLITH GLUE, and named as such: the handle of a resource this backend is looking
// at through a frontend object, resolved through the client allocator's lifetime-id
// index. Every caller is a site P3a deliberately does NOT migrate - the SSBO / UBO /
@@ -1810,6 +1835,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
// still run when SyncCurrentFBO skips the READ-target sync because the same GL FBO is
// bound as both draw and read (otherwise glReadBuffer changes would be silently dropped).
void SyncReadBufferToBackend(const SharedPtr<MG_State::GLState::FramebufferObject>& stateFBOObject);
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (hd, CONTRACT-P5C §3.2): the framebuffer handle the CURRENT sync is keyed on.
// A caller applying a record sets it before SyncToBackend / SyncReadBufferToBackend,
// which then read the applier's record for THAT handle instead of probing the
// client's slot allocator for the frontend object's lifetime id (T2). Read only
// with an active transport; monolith resolves through HandleOf as it always did.
MG_Pipe::MGPipeHandle m_pushedSyncHandle = MG_Pipe::kMGPipeNullHandle;
#endif
void InvalidateSyncedState();
Uint GetBackendFramebufferId() const { return m_backendFBOId; }
void Bind(FramebufferTarget target) const;
@@ -75,6 +75,12 @@
// belong on the client, and the backend should receive the handle in the verb payload. It is
// NOT part of "Track H done" and check_include_closure.py does not probe MG_Backend headers,
// so nothing catches it automatically.
//
// P5c (hd, CONTRACT-P5C §3.1) is what gives the debt teeth: with an active transport the
// minting GetOrCreate, HandleOf and OnFrontendObjectDestroyed raise
// Fatal{RoleViolation, "MGPipeSlots"} when reached from the apply thread (the same check the
// allocator's own three entries carry, repeated here so the refusal names this surface), and
// the split paths resolve through GetOrCreate(handle) / ReleaseByHandle instead.
namespace MobileGL::MG_Backend::DirectGLES {
#if MOBILEGL_PIPE_PUSH
@@ -228,6 +234,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
// arm StateBackendObjectRegistry::GetOrCreate arms it itself.
EnsureProcessTeardownSentinel();
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (hd, CONTRACT-P5C §3.1): this overload MINTS - it is monolith glue, and with
// an active transport a call from the apply thread is Fatal{RoleViolation,
// "MGPipeSlots"} before the allocator is touched. Split paths call the handle
// overload below. (The check also lives at the allocator's own three entries; it
// is repeated at this entry so the refusal names this surface even if the entry
// set changes.)
MG_Pipe::MGPipeRefuseAllocatorFromApplyThread("GetOrCreate(StatePtr)");
#endif
const MG_Pipe::MGPipeHandle handle =
MG_Pipe::MGPipeSlots().Acquire(kKind, stateObj->GetLifetimeId());
MOBILEGL_ASSERT(!MG_Pipe::MGPipeHandleIsNull(handle),
@@ -334,6 +350,34 @@ namespace MobileGL::MG_Backend::DirectGLES {
return entry.Live ? entry.Gen : 0;
}
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (hd): remember the frontend object a HANDLE-keyed twin was synced from. The
// minting overload sets stateRef itself; the handle overload cannot (no object
// crosses), so a caller that legitimately holds the object - the record-driven sync,
// which arrived holding it through the object-class barrier-pulled rows - notes it
// here. It is what lets a later handle-only resolution (P5c's named blit) reach the
// frontend object the twin's sync body still walks, without probing the client's slot
// allocator (T2). Same liveness rules as the minted stateRef: never an identity test,
// never read to decide the slot is dead.
void NoteStateForHandle(MG_Pipe::MGPipeHandle handle, const StatePtr& stateObj) {
if (MG_Pipe::MGPipeHandleIsNull(handle) || handle.Slot >= m_slots.size()) return;
Entry& entry = m_slots[handle.Slot];
if (!entry.Live || entry.Gen != handle.Gen) return;
entry.stateRef = stateObj;
}
// The frontend object noted for this handle, or null. The handle answers identity;
// this answers only "which object was this twin last synced from".
StatePtr StateForHandle(MG_Pipe::MGPipeHandle handle) const {
if (MG_Pipe::MGPipeHandleIsNull(handle) || handle.Slot >= m_slots.size()) {
return nullptr;
}
const Entry& entry = m_slots[handle.Slot];
if (!entry.Live || entry.Gen != handle.Gen) return nullptr;
return entry.stateRef.lock();
}
#endif
// P3a: the death half of the overload above, for a kind whose announcement is its own
// destroy CALL rather than the shared death notice (D-L). Hands the twin OUT rather
// than destroying it in place, because the caller may still have to decide what
@@ -391,6 +435,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
// allocator probe it always cost; a hit is refreshed the moment anyone acquires.
MG_Pipe::MGPipeHandle HandleOf(const StateObject* stateObj) const {
if (stateObj == nullptr) return MG_Pipe::kMGPipeNullHandle;
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (hd): same guard as the minting overload - a lifetime-id probe from the
// apply thread is Fatal{RoleViolation, "MGPipeSlots"} with an active transport.
MG_Pipe::MGPipeRefuseAllocatorFromApplyThread("HandleOf");
#endif
const Uint64 lifetimeId = stateObj->GetLifetimeId();
if (lifetimeId == m_memoLifetimeId) return m_memoHandle;
const MG_Pipe::MGPipeHandle handle =
@@ -414,6 +463,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
// Returns whether the object had a slot of this kind, i.e. whether anything was freed;
// a second call for the same id answers false because the allocator no longer maps it.
static Bool OnFrontendObjectDestroyed(Uint64 lifetimeId) {
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (hd): the Find/Free pair below is monolith-only with an active transport -
// a frontend death is announced to the server by the object_death record (ct),
// and a direct call from the apply thread is Fatal{RoleViolation, "MGPipeSlots"}.
MG_Pipe::MGPipeRefuseAllocatorFromApplyThread("OnFrontendObjectDestroyed");
#endif
const MG_Pipe::MGPipeHandle handle =
MG_Pipe::MGPipeSlots().FindByLifetimeId(kKind, lifetimeId);
if (MG_Pipe::MGPipeHandleIsNull(handle)) return false;
+10
View File
@@ -31,6 +31,7 @@
#include <algorithm>
#include <cmath>
#include <cctype>
#include <cstdlib>
#include <cstring>
#include <format>
#include <regex>
@@ -46,6 +47,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (MG_Backend::BackendObject* server = MG_Remote::Server::ServerLoopInstance().Backend()) {
return &server->GetFormatCapabilities();
}
// P5c (hd, CONTRACT-P5C §3.7): the mirror fallback is REFUSED with an active
// transport. The server's backend exists whenever the session does, so reaching
// here is a bring-up ordering defect, and a silent read of the client caps mirror
// (pActiveBackendObject is client memory, rule E) would hide it.
MGLOG_F("MGPipe: Fatal{RoleViolation, \"caps-mirror\"} - the format-capability "
"fallback to the client caps mirror was reached with an active transport "
"but no server backend; the server's own backend is the only legal source "
"on the apply thread");
std::abort();
}
return pActiveBackendObject ? &pActiveBackendObject->GetFormatCapabilities() : nullptr;
}
@@ -1,4 +1,4 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp
// MobileGL - MobileGL/MG_Backend/DirectVulkan/DirectVulkan.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
@@ -17,6 +17,10 @@
#include "MG_Util/Metrics/PipeStats.h"
#include "MG_Util/Metrics/TextureMetrics.h"
#include "MG_Util/Miscellany/IndexGenerator.h"
#if MOBILEGL_BUILD_DISAGGREGATED
#include <Config.h>
#include <MG_Remote/Server/ServerLoop.h>
#endif
#include <atomic>
#include <bit>
#include <cstring>
@@ -710,7 +714,21 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void ShaderStorageBlockBinding(GLuint program, const GLchar* storageBlockName, GLuint storageBlockBinding) {
auto* programObject = TryGetDirectVulkanProgram(program);
if (!programObject || storageBlockName == nullptr) return;
const Int maxBindings = pActiveBackendObject
const Int maxBindings =
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (hd, CONTRACT-P5C §3.7): with an active transport the dynamic parameters are
// the SERVER's own backend's - the client caps mirror (pActiveBackendObject) is
// client memory the apply thread may not name (rule E). Monolith reads the mirror
// as it always did.
MG_Config::Transport != MG_Config::TransportMode::Monolith
? (MG_Remote::Server::ServerLoopInstance().Backend() != nullptr
? static_cast<Int>(MG_Remote::Server::ServerLoopInstance().Backend()
->GetDynamicParameters()
.MaxShaderStorageBufferBindings)
: 0)
:
#endif
pActiveBackendObject
? pActiveBackendObject->GetDynamicParameters().MaxShaderStorageBufferBindings
: 0;
if (storageBlockBinding >= static_cast<GLuint>(maxBindings)) {
@@ -10,6 +10,10 @@
#include "MagmaPipeArms.h"
#include "MG_Util/Converters/MGToStr/DataTypeConverter.h"
#include <MG_Backend/BackendObjects.h>
#if MOBILEGL_BUILD_DISAGGREGATED
#include <Config.h>
#include <MG_Remote/Server/ServerLoop.h>
#endif
#include <utility>
namespace MobileGL::MG_Backend::DirectVulkan {
@@ -246,6 +250,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// set, a dvec3/dvec4 would be declined by ToVkVertexFormat AND left 64-bit in the
// module, so a float32 stream would be fed to a Float64 input.
const Bool narrowFloat64Arrays =
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (hd, CONTRACT-P5C §3.7): with an active transport the answer is the
// SERVER's own backend's - the client caps mirror is client memory (rule E).
MG_Config::Transport != MG_Config::TransportMode::Monolith
? (MG_Remote::Server::ServerLoopInstance().Backend() == nullptr ||
!MG_Remote::Server::ServerLoopInstance().Backend()
->GetDynamicParameters()
.SupportsFloat64VertexAttributes)
:
#endif
MG_Backend::pActiveBackendObject == nullptr ||
!MG_Backend::pActiveBackendObject->GetDynamicParameters().SupportsFloat64VertexAttributes;
if (sourceVkFormat == VK_FORMAT_UNDEFINED && attr.Type == DataType::Float64 && narrowFloat64Arrays) {
@@ -499,7 +513,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// demoted `vec` input - the same thing DirectGLES does for the same state. The
// frontend RECORDS the format either way, so this gate is the only thing standing
// between a legal glVertexAttribLFormat and a mismatched pipeline.
if (MG_Backend::pActiveBackendObject == nullptr ||
if (
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (hd, CONTRACT-P5C §3.7): see the narrowFloat64Arrays site above.
MG_Config::Transport != MG_Config::TransportMode::Monolith
? (MG_Remote::Server::ServerLoopInstance().Backend() == nullptr ||
!MG_Remote::Server::ServerLoopInstance().Backend()
->GetDynamicParameters()
.SupportsFloat64VertexAttributes)
:
#endif
MG_Backend::pActiveBackendObject == nullptr ||
!MG_Backend::pActiveBackendObject->GetDynamicParameters().SupportsFloat64VertexAttributes) {
return VK_FORMAT_UNDEFINED;
}
@@ -40,6 +40,9 @@
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (T5 / tx): the server's staged-texture shadow GenerateMipmap defines its chain on.
#include <MG_Remote/Server/StagedTextureStore.h>
#include <MG_Remote/Server/ServerLoop.h>
// P5c (G6): the named-blit arm's endpoint resolution runs inside the frontend-keyed scope.
#include <MG_Impl/Pipe/SlotAllocator.h>
#endif
#include <algorithm>
#include <bit>
@@ -672,6 +675,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
static void ApplyLineWidthState(VkCommandBuffer commandBuffer) {
Float lineWidth = MGB_CTX->GetLineWidth();
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (hd, CONTRACT-P5C §3.7): with an active transport the dynamic parameters are the
// SERVER's own backend's - the client caps mirror is client memory (rule E). Monolith
// reads the mirror as it always did.
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
if (MG_Backend::BackendObject* server = MG_Remote::Server::ServerLoopInstance().Backend()) {
const auto& dynamicParameters = server->GetDynamicParameters();
const Float minLineWidth = dynamicParameters.AliasedLineWidthRangeMin;
const Float maxLineWidth = dynamicParameters.AliasedLineWidthRangeMax;
if (lineWidth < minLineWidth) {
lineWidth = minLineWidth;
} else if (lineWidth > maxLineWidth) {
lineWidth = maxLineWidth;
}
}
} else
#endif
if (MG_Backend::pActiveBackendObject != nullptr) {
const auto& dynamicParameters = MG_Backend::pActiveBackendObject->GetDynamicParameters();
const Float minLineWidth = dynamicParameters.AliasedLineWidthRangeMin;
@@ -4522,6 +4542,17 @@ void main() {
}
void VulkanRenderer::ShutdownBlitResources() {
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (G6, CONTRACT-P5C §3.1's named exemption): the hidden blit program and samplers
// are FRONTEND objects the server backend created on the apply thread, and under an
// active transport they die here on that same thread. Their destructors run the client
// death helper, whose lifetime-id probe is the frontend-keyed registry family - it
// resolves to nothing (no handle was ever minted for these objects) and routes no
// delete, so the scope admits the probe as named debt rather than letting the guard
// Fatal at server teardown. P7 gives these resources storage that is not a frontend
// object.
const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry;
#endif
m_blitResources = {};
}
@@ -4596,6 +4627,12 @@ void main() {
}
void VulkanRenderer::ShutdownDepthMipmapResources() {
#if MOBILEGL_BUILD_DISAGGREGATED
// Same shape as ShutdownBlitResources above: the hidden depth-mipmap program is a
// frontend object created and destroyed by the server backend on the apply thread, and
// its destructor's lifetime-id probe is admitted here as named debt.
const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry;
#endif
m_depthMipmapResources = {};
}
@@ -8928,6 +8965,53 @@ void main() {
void VulkanRenderer::BlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1,
GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1,
GLbitfield mask, GLenum filter) {
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (G6, CONTRACT-P5C §3.3/§5.4): MAGMA'S NAMED ARM. A blit record whose
// ReadFbo/DrawFbo are non-null is a glBlitNamedFramebuffer: both framebuffers resolve
// from the record's handles, and the sink is told the pair was consumed - a backend
// that leaves the flag clear has no named arm and the verb declines there, loudly.
// Magma has no FBO twin registry (it reads the frontend object wherever it syncs), so
// the resolution here is frontend-keyed - the handle was minted over the frontend
// object's lifetime id, and the two probes below (the client allocator's slot entry
// and the frontend context's framebuffer pool) are named debt inside the scope, the
// same shape as Espryt's StateForHandle arm: P3b/P4b retire it by carrying the object
// identity in the record.
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
auto& applierState = MG_Pipe::MGPipeApplier();
const MG_Pipe::MGPipeHandle readHandle = applierState.VerbBlitReadFbo;
const MG_Pipe::MGPipeHandle drawHandle = applierState.VerbBlitDrawFbo;
if (!MG_Pipe::MGPipeHandleIsNull(readHandle) || !MG_Pipe::MGPipeHandleIsNull(drawHandle)) {
const auto resolveEndpoint = [](MG_Pipe::MGPipeHandle handle)
-> SharedPtr<MG_State::GLState::FramebufferObject> {
const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry;
if (handle == MG_Pipe::kMGPipeDefaultFramebuffer) {
return MG_State::pGLContext ? MG_State::pGLContext->GetFramebufferObject(0) : nullptr;
}
if (!MG_Pipe::MGPipeSlots().IsLive(MG_Pipe::MGPipeKind::Framebuffer, handle)) {
return nullptr;
}
const Uint64 lifetimeId =
MG_Pipe::MGPipeSlots().LifetimeIdOfSlot(MG_Pipe::MGPipeKind::Framebuffer, handle.Slot);
if (lifetimeId == 0 || MG_State::pGLContext == nullptr) return nullptr;
return MG_State::pGLContext->FindFramebufferObjectByLifetimeId(lifetimeId);
};
auto readFbo = resolveEndpoint(readHandle);
auto drawFbo = resolveEndpoint(drawHandle);
if (!readFbo || !drawFbo) {
// Leave the pair UNCONSUMED: the sink's decline is the loud answer a
// missing endpoint deserves, not a silent blit of whatever is bound.
MGLOG_E_ONCE("MGPipe: Magma's named blit could not resolve an endpoint (read {%u, %u}, draw "
"{%u, %u}) to a frontend framebuffer; the verb declines at the sink",
readHandle.Slot, readHandle.Gen, drawHandle.Slot, drawHandle.Gen);
return;
}
applierState.VerbBlitNamedConsumed = true;
BlitNamedFramebuffer(readFbo, drawFbo, srcX0, srcY0, srcX1, srcY1, dstX0, dstY0,
dstX1, dstY1, mask, filter);
return;
}
}
#endif
auto readFbo = MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject();
auto drawFbo = MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
BlitNamedFramebuffer(readFbo, drawFbo, srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter);
+10 -2
View File
@@ -714,8 +714,16 @@ namespace MobileGL::MG_Pipe {
bufferObject->MarkGpuWritten();
return;
}
const MGPipeHandle res =
MGPipeSlots().FindByLifetimeId(MGPipeKind::Buffer, bufferObject->GetLifetimeId());
const MGPipeHandle res = [&]() {
#if MOBILEGL_BUILD_DISAGGREGATED
// CONTRACT-P5C §3.1's named exemption, Magma's half: the binding records that
// would carry this buffer's handle are P4b's to emit and Magma's server-side
// binding table is P7's, so until then the probe runs inside the scope - the
// debt's named, greppable form rather than a silent guard removal.
const MGPipeReverseAnnouncementScope reverseAnnouncement;
#endif
return MGPipeSlots().FindByLifetimeId(MGPipeKind::Buffer, bufferObject->GetLifetimeId());
}();
if (MGPipeHandleIsNull(res)) {
MGLOG_E_ONCE("MGPipe: no handle for the GPU-write announcement of buffer %u - the "
"reverse channel is installed but the mint is missing, so the mark "
+69
View File
@@ -9,7 +9,67 @@
// SlotAllocator.h. Compiled only under MOBILEGL_PIPE_PUSH.
#include <MG_Impl/Pipe/SlotAllocator.h>
#if MOBILEGL_BUILD_DISAGGREGATED
#include <Config.h>
#include <MG_Remote/Server/ServerLoop.h>
#include <MG_Util/Debug/Log.h>
#include <cstdlib>
#endif
namespace MobileGL::MG_Pipe {
#if MOBILEGL_BUILD_DISAGGREGATED
void MGPipeRefuseAllocatorFromApplyThread(const char* entry) {
if (MG_Config::Transport == MG_Config::TransportMode::Monolith) return;
if (!MG_Remote::Server::ServerLoop::OnApplyThread()) return;
// The named exemption of CONTRACT-P5C §3.1: the sites whose handle-carrying records
// are not emitted yet (P4b/P7) probe read-only inside the scope. The G6
// frontend-keyed registry family (P3b/P4b) does the same inside its own scope.
if (MGPipeReverseAnnouncementScope::Active()) return;
if (MGPipeFrontendKeyedRegistryScope::Active()) return;
MGLOG_F("MGPipe: Fatal{RoleViolation, \"MGPipeSlots\"} - the apply thread called "
"MGPipeSlots().%s. With an active transport the client slot allocator is "
"client-only memory (CONTRACT-P5C §3.1, rule E): a handle arrives already "
"minted in a record, and a server that resolves or mints one off a frontend "
"object's lifetime id is reading memory that will not exist on its side of a "
"real split",
entry);
std::abort();
}
namespace {
thread_local Uint32 g_reverseAnnouncementScopeDepth = 0;
}
MGPipeReverseAnnouncementScope::MGPipeReverseAnnouncementScope() {
++g_reverseAnnouncementScopeDepth;
}
MGPipeReverseAnnouncementScope::~MGPipeReverseAnnouncementScope() {
--g_reverseAnnouncementScopeDepth;
}
Bool MGPipeReverseAnnouncementScope::Active() {
return g_reverseAnnouncementScopeDepth != 0;
}
namespace {
thread_local Uint32 g_frontendKeyedRegistryScopeDepth = 0;
}
MGPipeFrontendKeyedRegistryScope::MGPipeFrontendKeyedRegistryScope() {
++g_frontendKeyedRegistryScopeDepth;
}
MGPipeFrontendKeyedRegistryScope::~MGPipeFrontendKeyedRegistryScope() {
--g_frontendKeyedRegistryScopeDepth;
}
Bool MGPipeFrontendKeyedRegistryScope::Active() {
return g_frontendKeyedRegistryScopeDepth != 0;
}
#endif
namespace {
// The ShaderCso band the ordinary allocator must never enter: the top 1/16 of the
// ShaderCso slot space is reserved for PROGRAM PIPELINE COMPOSITES, which are minted
@@ -166,6 +226,9 @@ namespace MobileGL::MG_Pipe {
}
MGPipeHandle MGPipeSlotAllocator::FindByLifetimeId(MGPipeKind kind, Uint64 lifetimeId) const {
#if MOBILEGL_BUILD_DISAGGREGATED
MGPipeRefuseAllocatorFromApplyThread("FindByLifetimeId");
#endif
if (lifetimeId == 0) return kMGPipeNullHandle;
const KindState& state = StateOf(kind);
const auto it = state.ByLifetimeId.find(lifetimeId);
@@ -176,12 +239,18 @@ namespace MobileGL::MG_Pipe {
}
MGPipeHandle MGPipeSlotAllocator::Acquire(MGPipeKind kind, Uint64 lifetimeId) {
#if MOBILEGL_BUILD_DISAGGREGATED
MGPipeRefuseAllocatorFromApplyThread("Acquire");
#endif
const MGPipeHandle existing = FindByLifetimeId(kind, lifetimeId);
if (!MGPipeHandleIsNull(existing)) return existing;
return AllocateFor(kind, lifetimeId);
}
void MGPipeSlotAllocator::Free(MGPipeKind kind, MGPipeHandle handle) {
#if MOBILEGL_BUILD_DISAGGREGATED
MGPipeRefuseAllocatorFromApplyThread("Free");
#endif
KindState& state = StateOf(kind);
SlotState* entry = EntryOf(state, kind, handle.Slot);
if (entry == nullptr) return;
+43
View File
@@ -163,4 +163,47 @@ namespace MobileGL::MG_Pipe {
// The monolith's one client allocator. Under split there is one per client context.
MGPipeSlotAllocator& MGPipeSlots();
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (hd, CONTRACT-P5C §3.1 / §6 layer 1): with an active transport this allocator is a
// CLIENT-only surface. Acquire, FindByLifetimeId and Free called from the apply thread -
// i.e. a server that resolves or mints handles off a frontend object's lifetime id (T2),
// which is memory that will not exist on its side of a real split - are
// Fatal{RoleViolation, "MGPipeSlots"}. Compiled out entirely outside split builds, so the
// pull build's bytes do not move (G1).
void MGPipeRefuseAllocatorFromApplyThread(const char* entry);
// The NAMED EXEMPTION to the rule above (CONTRACT-P5C §3.1): the family of sites whose
// handle-carrying records the client does not EMIT yet. set_shader_buffers and
// set_stream_output_targets exist in the catalogue but are P4b's to emit
// (SetHashSuppressor.h says so), so the buffer binding-point ensures and the GPU-written
// announcement they feed have no record handle to resolve from today. Inside this scope
// ONE read-only lifetime-id probe stays legal; the scope is the debt's measurable,
// greppable form, and it retires with P4b's emission (Espryt) and P7's server-side
// binding table (Magma). Every other apply-thread allocator access stays Fatal.
class MGPipeReverseAnnouncementScope {
public:
MGPipeReverseAnnouncementScope();
~MGPipeReverseAnnouncementScope();
MGPipeReverseAnnouncementScope(const MGPipeReverseAnnouncementScope&) = delete;
MGPipeReverseAnnouncementScope& operator=(const MGPipeReverseAnnouncementScope&) = delete;
static Bool Active();
};
// The SECOND named exemption family (CONTRACT-P5C §5.4): the frontend-keyed twin
// registry (audit row G6). The texture / sampler-view / FBO-legacy HandleOf probes are
// how the server answers "which twin is this frontend object" while the registry is
// keyed by frontend identity - server-PRIVATE state whose rekey onto handles is
// P3b/P4b's, not P5c's. A probe inside this scope stays a read-only, barrier-held debt;
// wrapping a NEW site in it is the greppable act of naming that debt, and an unwrapped
// probe from the apply thread is still Fatal{RoleViolation, "MGPipeSlots"}.
class MGPipeFrontendKeyedRegistryScope {
public:
MGPipeFrontendKeyedRegistryScope();
~MGPipeFrontendKeyedRegistryScope();
MGPipeFrontendKeyedRegistryScope(const MGPipeFrontendKeyedRegistryScope&) = delete;
MGPipeFrontendKeyedRegistryScope& operator=(const MGPipeFrontendKeyedRegistryScope&) = delete;
static Bool Active();
};
#endif
} // namespace MobileGL::MG_Pipe
+44
View File
@@ -693,6 +693,50 @@ namespace MobileGL::MG_Pipe {
MGPipeHandle BoundShaderCso = kMGPipeNullHandle;
Uint64 ProgramBindingSerial = 0;
#if MOBILEGL_BUILD_DISAGGREGATED
// ---- P5c (hd, CONTRACT-P5C §3.2): THE CURRENT VERB'S OWN HANDLES. ----------------
//
// Server state, written by ServerVerbSink at the top of a verb's dispatch and read by
// the backend DURING THAT SAME VERB, so the apply thread resolves the verb's objects
// from the handles the record carried instead of probing the client's slot allocator
// for a frontend object's lifetime id (T2). It is per-verb WORKING state, not object
// state: every writer overwrites the whole set for its verb (null included), so a
// value is only ever read between its verb's write and that verb's return, and there
// is nothing to clear at a reset.
//
// Blit: both null = the bound-form blit (the bindings answer, as before). A non-null
// pair names the read/draw framebuffers of a NAMED blit (MGPBlit::ReadFbo/DrawFbo);
// the backend must then run its named-blit arm and raise VerbBlitNamedConsumed - the
// sink reads that flag back so a backend with no named arm is a loud decline rather
// than a silent blit of whatever is bound.
MGPipeHandle VerbBlitReadFbo = kMGPipeNullHandle;
MGPipeHandle VerbBlitDrawFbo = kMGPipeNullHandle;
Bool VerbBlitNamedConsumed = false;
// copy_framebuffer_to_texture's destination texture (MGPCopyFromFramebuffer::Dst).
MGPipeHandle VerbCopyTexDst = kMGPipeNullHandle;
// generate_mipmap's texture (MGPMipPlan::Res).
MGPipeHandle VerbMipRes = kMGPipeNullHandle;
// The current indirect draw's command buffer and (for the *Count forms) parameter
// buffer (MGPDrawIndirect::Buffer / ParameterBuffer).
MGPipeHandle VerbIndirectBuffer = kMGPipeNullHandle;
MGPipeHandle VerbIndirectParameterBuffer = kMGPipeNullHandle;
// dispatch_indirect's command buffer (MGPGridInfo::IndirectBuffer).
MGPipeHandle VerbDispatchIndirectBuffer = kMGPipeNullHandle;
// Every verb's writer calls this FIRST and then sets its own fields, so no field ever
// outlives the verb that wrote it and a reader can treat non-null as "this verb's".
void ClearVerbHandles() {
VerbBlitReadFbo = kMGPipeNullHandle;
VerbBlitDrawFbo = kMGPipeNullHandle;
VerbBlitNamedConsumed = false;
VerbCopyTexDst = kMGPipeNullHandle;
VerbMipRes = kMGPipeNullHandle;
VerbIndirectBuffer = kMGPipeNullHandle;
VerbIndirectParameterBuffer = kMGPipeNullHandle;
VerbDispatchIndirectBuffer = kMGPipeNullHandle;
}
#endif
// ---- THE THREE FRAMEBUFFER ACCESSORS (ID-19(b)/(d)). They are functions rather than
// members because the storage moved under them and their callers must not have to know
// it did: `DrawFramebuffer()` / `ReadFramebuffer()` answer the question the two members
+7 -30
View File
@@ -734,42 +734,19 @@ namespace MobileGL::MG_Remote::Client {
nullptr, 0, nullptr);
}
// DSA blits use the existing bound-form backend while the verb barrier holds.
// Only client shadow bindings change here; no driver call or frontend pointer crosses
// the wire. Bind() bumps their versions on entry and restore, so the next ordinary
// verb republishes the application's original bindings even if no GL bind intervenes.
class ScopedBlitBindings {
public:
using Fbo = MG_State::GLState::FramebufferObject;
ScopedBlitBindings(const SharedPtr<Fbo>& read, const SharedPtr<Fbo>& draw)
: m_read(MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Read)),
m_draw(MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw)),
m_savedRead(m_read.GetBoundObject()), m_savedDraw(m_draw.GetBoundObject()) {
m_read.Bind(read);
m_draw.Bind(draw);
}
~ScopedBlitBindings() {
m_read.Bind(m_savedRead);
m_draw.Bind(m_savedDraw);
}
ScopedBlitBindings(const ScopedBlitBindings&) = delete;
ScopedBlitBindings& operator=(const ScopedBlitBindings&) = delete;
private:
BindingSlot<Fbo>& m_read;
BindingSlot<Fbo>& m_draw;
SharedPtr<Fbo> m_savedRead;
SharedPtr<Fbo> m_savedDraw;
};
void EmitBlitNamedFramebuffer(
const SharedPtr<MG_State::GLState::FramebufferObject>& read,
const SharedPtr<MG_State::GLState::FramebufferObject>& draw,
GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0,
GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter) {
ClientSession& session = RequireSession("BlitNamedFramebuffer");
const ScopedBlitBindings bindings(read, draw);
// The frontend's first validate preceded this temporary lowering. Refresh both
// emitted framebuffer state and the existing BARRIER-PULLED binding fields now.
// P5c (hd, CONTRACT-P5C §3.3): the scoped rebind of the client's own read/draw
// binding slots is DELETED. It existed only to stage values for the server's
// binding-slot read, and that read is gone: the sink resolves both framebuffers
// from the record's handles. The validate still refreshes both framebuffers'
// emitted state (their Named set_framebuffer_state records), which is now the
// only description the server syncs from. No driver call or binding change
// happens on the client.
MG_Pipe::MGPipeValidateForVerb(MG_Pipe::MGPipeVerb::BlitNamedFramebuffer);
BeforeReadOnlyVerb();
MG_Pipe::MGPBlit record{};
+59 -6
View File
@@ -229,16 +229,35 @@ namespace MobileGL::MG_Remote::Server {
const MG_Backend::GlobalBackendFunctionsTable* table = Table("blit");
if (table == nullptr) return false;
if (table->GL.BlitFramebuffer == nullptr) return false;
// Same ruling as OnClear's: the read and draw framebuffers are already bound by the
// set_framebuffer_state records that preceded this one, so the unnamed entry point is
// the one that matches what the server's state actually is. For the named form the
// client temporarily binds the two named objects and validates before emitting; its
// existing BARRIER-PULLED fields stay fixed until this call returns. ReadFbo/DrawFbo
// retain the original identities for the later handle-only endpoint (CONTRACT-P5B §6).
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (hd, CONTRACT-P5C §3.3): the record's handles cross to the backend as the verb's
// own state. The bound form carries two nulls and nothing changes; the named form's
// pair is what the backend's named-blit arm resolves - the sink no longer relies on
// "the read and draw framebuffers are already bound" (the client's ScopedBlitBindings
// staging is deleted with this), and a backend that does not consume the pair has no
// named arm, which is a loud decline rather than a blit of whatever is bound.
auto& applierState = MG_Pipe::MGPipeApplier();
applierState.ClearVerbHandles();
applierState.VerbBlitReadFbo = blit.ReadFbo;
applierState.VerbBlitDrawFbo = blit.DrawFbo;
const Bool named = !MG_Pipe::MGPipeHandleIsNull(blit.ReadFbo) ||
!MG_Pipe::MGPipeHandleIsNull(blit.DrawFbo);
#endif
table->GL.BlitFramebuffer(blit.SrcX0, blit.SrcY0, blit.SrcX1, blit.SrcY1, blit.DstX0,
blit.DstY0, blit.DstX1, blit.DstY1,
static_cast<GLbitfield>(blit.Mask),
static_cast<GLenum>(blit.Filter));
#if MOBILEGL_BUILD_DISAGGREGATED
if (named && !applierState.VerbBlitNamedConsumed) {
MGLOG_E_ONCE("MGPipe: a named blit (read {%u, %u}, draw {%u, %u}) reached a backend "
"with no named-blit arm; the verb is DECLINED rather than applied to "
"the bound framebuffers",
blit.ReadFbo.Slot, blit.ReadFbo.Gen, blit.DrawFbo.Slot, blit.DrawFbo.Gen);
applierState.VerbBlitReadFbo = MG_Pipe::kMGPipeNullHandle;
applierState.VerbBlitDrawFbo = MG_Pipe::kMGPipeNullHandle;
return false;
}
#endif
++m_blits;
return true;
}
@@ -447,6 +466,15 @@ namespace MobileGL::MG_Remote::Server {
if (indirect != nullptr) {
// The layout already refused a record that sets both flags or declares ranges
// beside the block, so NumDraws is 0 and there is no span here.
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (hd, CONTRACT-P5C §3.5): the command/parameter buffer handles cross as the
// verb's own state; the backend's indirect arm resolves the buffer twins from
// them instead of reading the client's GL_DRAW_INDIRECT_BUFFER binding slot.
auto& applierState = MG_Pipe::MGPipeApplier();
applierState.ClearVerbHandles();
applierState.VerbIndirectBuffer = indirect->Buffer;
applierState.VerbIndirectParameterBuffer = indirect->ParameterBuffer;
#endif
const auto offset = reinterpret_cast<const void*>(static_cast<std::uintptr_t>(indirect->Offset));
const auto drawCount = static_cast<GLsizei>(indirect->DrawCount);
const auto stride = static_cast<GLsizei>(indirect->Stride);
@@ -650,6 +678,14 @@ namespace MobileGL::MG_Remote::Server {
// IndirectBuffer travels for P7's sake; the BINDING is server state, put there by
// the set_buffer_bindings record that preceded this one, exactly as OnClear's Fbo
// is not re-resolved here. glDispatchComputeIndirect takes only the offset.
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (hd, CONTRACT-P5C §3.5): the buffer handle itself is now also the verb's own
// state, so the backend's dispatch-indirect arm resolves the twin from the record
// rather than from the client's GL_DISPATCH_INDIRECT_BUFFER binding slot.
auto& applierState = MG_Pipe::MGPipeApplier();
applierState.ClearVerbHandles();
applierState.VerbDispatchIndirectBuffer = grid.IndirectBuffer;
#endif
gl.DispatchComputeIndirect(static_cast<GLintptr>(grid.IndirectOffset));
} else {
if (gl.DispatchCompute == nullptr) return false;
@@ -870,6 +906,15 @@ namespace MobileGL::MG_Remote::Server {
Bool ServerVerbSink::OnGenerateMipmap(const MG_Pipe::MGPMipPlan& plan) {
const auto* table = Table("GenerateMipmap");
if (table == nullptr || table->GL.GenerateMipmap == nullptr) return false;
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (hd, CONTRACT-P5C §3.2): the texture the client resolved at Target on the active
// unit crosses as the verb's own state; the backend's mip-descriptor check resolves
// the record from it instead of probing the client allocator for the bound object's
// lifetime id (T2's mip half).
auto& applierState = MG_Pipe::MGPipeApplier();
applierState.ClearVerbHandles();
applierState.VerbMipRes = plan.Res;
#endif
table->GL.GenerateMipmap(plan.Target);
return true;
}
@@ -877,6 +922,14 @@ namespace MobileGL::MG_Remote::Server {
Bool ServerVerbSink::OnCopyFramebufferToTexture(const MG_Pipe::MGPCopyFromFramebuffer& copy) {
const auto* table = Table(copy.SubImage ? "CopyTexSubImage2D" : "CopyTexImage2D");
if (table == nullptr) return false;
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (hd, CONTRACT-P5C §3.4): the destination texture the client resolved at the
// active unit crosses as the verb's own state; the backend resolves its twin from the
// handle instead of reading the client's texture-unit binding slot (T4).
auto& applierState = MG_Pipe::MGPipeApplier();
applierState.ClearVerbHandles();
applierState.VerbCopyTexDst = copy.Dst;
#endif
if (copy.SubImage) {
if (table->GL.CopyTexSubImage2D == nullptr) return false;
table->GL.CopyTexSubImage2D(copy.Target, copy.Level, copy.XOffset, copy.YOffset,
@@ -29,6 +29,28 @@ namespace MobileGL::MG_State::GLState {
const BufferBackendOps* g_bufferBackendOps = nullptr;
// Starts at 1 so a zero-initialized cache slot can never carry a live buffer's id.
std::atomic<Uint64> g_nextBufferLifetimeId{1};
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (hd, CONTRACT-P5C §3.8 / §6 layer 1): the frontend BufferObject's legacy
// accessors are a layer-1 surface. With an active transport, the pre-handle buffer
// arm that reads them (Managers.cpp's RespecifyStorageNow / UploadRangeNow /
// IsBufferDrawClean / the old EnsureBufferResource body) stays compiled but may not
// run: the apply thread calling one is Fatal{RoleViolation, "buffer-legacy-arm"} -
// the same shape as Fatal{PipeLegacyMemosDisabled} - so a cleared subsystem bit 7 no
// longer leaves the arm silently readable. Client-thread callers (the GL thread's
// own state) are unaffected.
void RefuseLegacyBufferArmFromApplyThread(const char* accessor) {
if (!MG_Remote::Client::PersistentMapTracker::PushIsArmed()) return;
if (!MG_Remote::Client::PersistentMapTracker::OnServerRole()) return;
MGLOG_F("MGPipe: Fatal{RoleViolation, \"buffer-legacy-arm\"} - the apply thread "
"called BufferObject::%s on a frontend object. With an active transport "
"the server reads the applier's resource record and its own staged shadow; "
"a frontend object is client memory (rule E) and the pre-handle buffer arm "
"is monolith-only",
accessor);
std::abort();
}
#endif
}
Uint64 BufferObject::AllocateLifetimeId() {
@@ -378,6 +400,9 @@ namespace MobileGL::MG_State::GLState {
void BufferObject::SyncPersistentMappedRange() {
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (hd): the named refusal comes FIRST - the silent return below used to let a
// server-side caller slip through with one MGLOG_D's worth of evidence (B3).
RefuseLegacyBufferArmFromApplyThread("SyncPersistentMappedRange");
// Split's client pre-verb hook publishes these bytes. The retained backend sync
// sites must do nothing on the apply thread: re-entering this producer there would
// overwrite the server shadow through the monolith adapter without crossing the wire.
@@ -914,6 +939,9 @@ namespace MobileGL::MG_State::GLState {
}
const Uint8* BufferObject::MappedData() const {
#if MOBILEGL_BUILD_DISAGGREGATED
RefuseLegacyBufferArmFromApplyThread("MappedData");
#endif
return m_resource.Bytes();
}
@@ -934,10 +962,18 @@ namespace MobileGL::MG_State::GLState {
}
Uint64 BufferObject::GetChangeSerial() const {
#if MOBILEGL_BUILD_DISAGGREGATED
RefuseLegacyBufferArmFromApplyThread("GetChangeSerial");
#endif
return m_changeSerial;
}
Bool BufferObject::HasDefinedContent() const {
#if MOBILEGL_BUILD_DISAGGREGATED
// §3.6's surface as well as §3.8's: the descriptor's bit and the staged coverage
// answer on this side; the frontend flag is client memory.
RefuseLegacyBufferArmFromApplyThread("HasDefinedContent");
#endif
return m_hasDefinedContent;
}
@@ -950,6 +986,9 @@ namespace MobileGL::MG_State::GLState {
}
Bool BufferObject::IsMapped() const {
#if MOBILEGL_BUILD_DISAGGREGATED
RefuseLegacyBufferArmFromApplyThread("IsMapped");
#endif
return m_isMapped;
}
+6
View File
@@ -1212,6 +1212,12 @@ namespace MobileGL::MG_State {
return m_framebufferState.ValidateFramebufferObject(index);
}
#if MOBILEGL_BUILD_DISAGGREGATED
SharedPtr<FramebufferObject> GLContext::FindFramebufferObjectByLifetimeId(Uint64 lifetimeId) const {
return m_framebufferState.FindFramebufferObjectByLifetimeId(lifetimeId);
}
#endif
// Sampler
void GLContext::GenSamplerNames(Uint number, Vector<Uint>& samplers) {
m_samplerState.GenerateNames(number, samplers);
+5
View File
@@ -544,6 +544,11 @@ namespace MobileGL {
// Framebuffer
void GenFramebufferNames(Uint number, Vector<Uint>& framebuffers);
const SharedPtr<FramebufferObject>& GetFramebufferObject(Uint index);
#if MOBILEGL_BUILD_DISAGGREGATED
// See FramebufferState::FindFramebufferObjectByLifetimeId - the named-blit
// consumer's handle-to-frontend resolution on a backend with no FBO twins.
SharedPtr<FramebufferObject> FindFramebufferObjectByLifetimeId(Uint64 lifetimeId) const;
#endif
BindingSlot<FramebufferObject>& GetFramebufferBindingSlot(FramebufferTarget target);
const SharedPtr<FramebufferObject>& CreateFramebufferObject(Uint index);
void MarkFramebufferObjectForDeletion(Uint index);
@@ -78,4 +78,15 @@ namespace MobileGL::MG_State::GLState {
Bool FramebufferState::ValidateFramebufferObject(Uint index) const {
return m_framebufferObjects.find(index) != m_framebufferObjects.end();
}
#if MOBILEGL_BUILD_DISAGGREGATED
SharedPtr<FramebufferObject> FramebufferState::FindFramebufferObjectByLifetimeId(Uint64 lifetimeId) const {
for (const auto& [index, framebufferObject] : m_framebufferObjects) {
if (framebufferObject && framebufferObject->GetLifetimeId() == lifetimeId) {
return framebufferObject;
}
}
return nullptr;
}
#endif
} // namespace MobileGL::MG_State::GLState
@@ -25,6 +25,16 @@ namespace MobileGL::MG_State::GLState {
Bool ValidateName(Uint index) const;
Bool ValidateFramebufferObject(Uint index) const;
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (G6, CONTRACT-P5C §3.3/§5.4): the reverse of HandleFor() for a server that holds
// only the handle - the named-blit consumer on a backend with no FBO twin registry
// (Magma) resolves the verb's ReadFbo/DrawFbo to the frontend object by the lifetime
// id the handle was minted over. Named debt, reached inside
// MGPipeFrontendKeyedRegistryScope; P3b/P4b retire it by carrying the object identity
// in the record. nullptr when no live framebuffer owns the id.
SharedPtr<FramebufferObject> FindFramebufferObjectByLifetimeId(Uint64 lifetimeId) const;
#endif
#if MOBILEGL_PIPE_PUSH
// P2 brief D4: "did the attachment set or the default geometry of ANY framebuffer
// move". It does NOT cover a BIND - a bind writes a BindingSlot, not the object - so
+16 -13
View File
@@ -419,22 +419,26 @@ TEST(RemoteClientControls, BoundPackBufferOffsetReadRefusesByName) {
}
TEST(RemoteClientControls, ServerRoleRespecifyDoesNotEmitClientInitialBytes) {
// P5c (hd): the in-process server-role drive itself is now refused by name. The emit
// path's first move is the client allocator's Acquire for the buffer's handle, and with
// an active transport that entry is Fatal{RoleViolation, "MGPipeSlots"} from the apply
// thread (CONTRACT-P5C §3.1, rule E). "No client initial bytes" therefore holds the
// strong way - there is no client-emitter path for the server role to reach at all; ev's
// SEG_EVENT producers are what replaces the mechanism this case used to drive.
const auto child = RunInChild([] {
CapsPeer backend;
Srv::ServerSessionInstance().SetBackend(&backend);
StartControlSession();
MG_Config::Features.PipePush |= kMGPipeSubsystemResources;
const auto before = ClientWireRecordsEmitted();
if (Srv::ServerLoopInstance().RunOnApplyThread([](void*) {
Srv::ServerLoopInstance().RunOnApplyThread([](void*) {
MG_State::GLState::BufferObject buffer(701);
Uint8 data[64]{};
buffer.Respecify(sizeof(data), data);
return MOBILEGL_OK;
}, nullptr) != MOBILEGL_OK) ::_exit(92);
if (ClientWireRecordsEmitted() != before) ::_exit(93);
}, nullptr);
ClientSessionInstance().Stop();
});
ExpectChildSuccess(child);
ExpectNamedAbort(child, "Fatal{RoleViolation, \"MGPipeSlots\"}");
}
TEST(RemoteClientControls, HeaderOnlyDynamicStateCrossesWithoutBlobMissing) {
@@ -451,34 +455,33 @@ TEST(RemoteClientControls, HeaderOnlyDynamicStateCrossesWithoutBlobMissing) {
}
TEST(RemoteClientControls, ServerRoleFlushDoesNotRunTheClientSubDataFollowup) {
// P5c (hd): same refusal as the respecify sibling above - a server-role flush reaching the
// client emit path is stopped at the allocator entry before any follow-up question arises
// (Fatal{RoleViolation, "MGPipeSlots"}; ev's events replace the mechanism).
const auto child = RunInChild([] {
CapsPeer backend;
Srv::ServerSessionInstance().SetBackend(&backend);
StartControlSession();
MG_Config::Features.PipePush |= kMGPipeSubsystemResources;
const auto rc = Srv::ServerLoopInstance().RunOnApplyThread([](void*) {
static int subdata = 0, flushes = 0;
Srv::ServerLoopInstance().RunOnApplyThread([](void*) {
MGPipeResourceOps ops{};
ops.Create = [](MGPipeHandle, const MGPResourceDesc&) {};
ops.Respecify = [](MGPipeHandle, const MGPResourceDesc&, const void*) {};
ops.SubData = [](MGPipeHandle, const MGPSubData&, const void*) { ++subdata; };
ops.FlushRange = [](MGPipeHandle, const MGPFlushRange&, const void*) { ++flushes; };
ops.SubData = [](MGPipeHandle, const MGPSubData&, const void*) {};
ops.FlushRange = [](MGPipeHandle, const MGPFlushRange&, const void*) {};
ops.Destroy = [](MGPipeHandle) {};
MGPipeSetResourceOps(&ops);
{
MG_State::GLState::BufferObject buffer(702);
buffer.Resize(64);
subdata = flushes = 0;
MGPipeEmitResourceFlushRange(buffer, 16, 16, 0);
if (subdata != 0 || flushes != 1) ::_exit(95);
}
MGPipeSetResourceOps(nullptr);
return MOBILEGL_OK;
}, nullptr);
if (rc != MOBILEGL_OK) ::_exit(96);
ClientSessionInstance().Stop();
});
ExpectChildSuccess(child);
ExpectNamedAbort(child, "Fatal{RoleViolation, \"MGPipeSlots\"}");
}
// Uses the shipped EGL/GL frontend and real DirectGLES driver. Invoked explicitly by the
// package gate because a native driver is not a prerequisite of the unit lane.
+329
View File
@@ -1230,10 +1230,12 @@ struct F1Peer : Codec::WireVerbSink {
MGPClear clear{};
MGPCopyFromFramebuffer copy{};
MGPMipPlan mip{};
MGPBlit blit{};
unsigned calls = 0;
Bool OnClear(const MGPClear& v) override { clear = v; ++calls; return true; }
Bool OnCopyFramebufferToTexture(const MGPCopyFromFramebuffer& v) override { copy = v; ++calls; return true; }
Bool OnGenerateMipmap(const MGPMipPlan& v) override { mip = v; ++calls; return true; }
Bool OnBlit(const MGPBlit& v) override { blit = v; ++calls; return true; }
void Install() {
if (Srv::ServerLoopInstance().RunOnApplyThread([](void* self) {
auto& decoder = Srv::ServerSessionInstance().Applier().*PeerMember(DecoderTag{});
@@ -1514,6 +1516,333 @@ TEST(RemoteF1, UnboundNamedfiRefusesByName) {
}
#endif
// =====================================================================================
// P5c (hd, CONTRACT-P5C §3): the record's handles are the server's resolution. The sink
// publishes each verb's own handles into the applier's verb stash (§3.2), the named blit's
// client emitter no longer rebinds anything (§3.3), and the layer-1 guards refuse the
// client-only surfaces from the apply thread by name (§3.1, §3.7, §3.8).
// =====================================================================================
#if MGTEST_HAVE_FORK
#include <MG_Backend/DirectGLES/SlotTables.h>
#include <MG_Backend/DirectGLES/Utils.h>
#include <MG_Impl/Pipe/SlotAllocator.h>
#include <MG_State/GLState/BufferState/BufferObject.h>
TEST(RemoteF1, BlitNamedCarriesHandlesAndLeavesTheClientBindingsAlone) {
// Red once (executed, reverted): re-bind the named arguments in the client shadow before
// emitting (the deleted ScopedBlitBindings); the two binding-slot asserts fail.
const auto child = RunInChild([] {
StartControlSession();
F1Peer peer;
peer.Install();
MG_State::pGLContext = MakeUnique<MG_State::GLState::GLContext>();
auto boundRead = MakeShared<MG_State::GLState::FramebufferObject>(11);
auto boundDraw = MakeShared<MG_State::GLState::FramebufferObject>(12);
auto namedRead = MakeShared<MG_State::GLState::FramebufferObject>(13);
auto namedDraw = MakeShared<MG_State::GLState::FramebufferObject>(14);
MG_State::pGLContext->GetFramebufferBindingSlot(MobileGL::FramebufferTarget::Read).Bind(boundRead);
MG_State::pGLContext->GetFramebufferBindingSlot(MobileGL::FramebufferTarget::Draw).Bind(boundDraw);
RemoteEmitTable().GL.BlitNamedFramebuffer(namedRead, namedDraw, 0, 0, 8, 8, 0, 0, 8, 8,
GL_COLOR_BUFFER_BIT, GL_NEAREST);
const auto& r = peer.blit;
if (peer.calls != 1 || r.ReadFbo != MGPipeFramebufferEmitter::HandleFor(*namedRead) ||
r.DrawFbo != MGPipeFramebufferEmitter::HandleFor(*namedDraw)) ::_exit(101);
// The client's own bindings are untouched: the server resolves the named pair from the
// record, not from a staged binding override (T3).
if (MG_State::pGLContext->GetFramebufferBindingSlot(MobileGL::FramebufferTarget::Read)
.GetBoundObject()
.get() != boundRead.get()) ::_exit(102);
if (MG_State::pGLContext->GetFramebufferBindingSlot(MobileGL::FramebufferTarget::Draw)
.GetBoundObject()
.get() != boundDraw.get()) ::_exit(103);
ClientSessionInstance().Stop();
});
ExpectChildSuccess(child);
}
namespace {
Bool g_stubBlitCalled = false;
MGPipeHandle g_observedBlitRead = kMGPipeNullHandle;
MGPipeHandle g_observedBlitDraw = kMGPipeNullHandle;
void StubBlitFramebuffer(GLint, GLint, GLint, GLint, GLint, GLint, GLint, GLint, GLbitfield, GLenum) {
g_stubBlitCalled = true;
const auto& st = MG_Pipe::MGPipeApplier();
g_observedBlitRead = st.VerbBlitReadFbo;
g_observedBlitDraw = st.VerbBlitDrawFbo;
}
void StubBlitFramebufferConsuming(GLint x0, GLint y0, GLint x1, GLint y1, GLint dx0, GLint dy0,
GLint dx1, GLint dy1, GLbitfield mask, GLenum filter) {
StubBlitFramebuffer(x0, y0, x1, y1, dx0, dy0, dx1, dy1, mask, filter);
MG_Pipe::MGPipeApplier().VerbBlitNamedConsumed = true;
}
MGPipeHandle g_observedCopyDst = kMGPipeNullHandle;
void StubCopyTexImage2D(GLenum, GLint, GLenum, GLint, GLint, GLsizei, GLsizei, GLint) {
g_observedCopyDst = MG_Pipe::MGPipeApplier().VerbCopyTexDst;
}
MGPipeHandle g_observedMipRes = kMGPipeNullHandle;
void StubGenerateMipmap(GLenum) { g_observedMipRes = MG_Pipe::MGPipeApplier().VerbMipRes; }
MGPipeHandle g_observedIndirect = kMGPipeNullHandle;
MGPipeHandle g_observedParameter = kMGPipeNullHandle;
void StubMultiDrawArraysIndirectCount(GLenum, const void*, GLintptr, GLsizei, GLsizei) {
const auto& st = MG_Pipe::MGPipeApplier();
g_observedIndirect = st.VerbIndirectBuffer;
g_observedParameter = st.VerbIndirectParameterBuffer;
}
MGPipeHandle g_observedDispatchIndirect = kMGPipeNullHandle;
void StubDispatchComputeIndirect(GLintptr) {
g_observedDispatchIndirect = MG_Pipe::MGPipeApplier().VerbDispatchIndirectBuffer;
}
} // namespace
TEST(RemoteF1, NamedBlitStashesHandlesAndDeclinesWhenTheBackendHasNoNamedArm) {
// Red once (executed, reverted): do not write the verb stash; the observed handles are null.
const auto child = RunInChild([] {
MG_Config::Transport = MG_Config::TransportMode::InProcess;
CapsPeer backend;
backend.table.GL.BlitFramebuffer = &StubBlitFramebuffer;
Srv::ServerVerbSink sink;
sink.SetBackend(&backend);
MGPBlit r{};
r.ReadFbo = {41, 2};
r.DrawFbo = {42, 3};
r.Mask = GL_COLOR_BUFFER_BIT;
r.Filter = GL_NEAREST;
const Bool applied = sink.OnBlit(r);
if (applied) ::_exit(101); // a named pair the backend did not consume must decline
if (!g_stubBlitCalled) ::_exit(102);
// The backend saw the record's handles as the verb's own state (CONTRACT-P5C §3.2/§3.3).
if (g_observedBlitRead != r.ReadFbo || g_observedBlitDraw != r.DrawFbo) ::_exit(103);
::_exit(0);
});
ExpectChildSuccess(child);
}
TEST(RemoteF1, NamedBlitIsAppliedWhenTheBackendConsumesThePair) {
const auto child = RunInChild([] {
MG_Config::Transport = MG_Config::TransportMode::InProcess;
CapsPeer backend;
backend.table.GL.BlitFramebuffer = &StubBlitFramebufferConsuming;
Srv::ServerVerbSink sink;
sink.SetBackend(&backend);
MGPBlit r{};
r.ReadFbo = {41, 2};
r.DrawFbo = {42, 3};
r.Mask = GL_COLOR_BUFFER_BIT;
r.Filter = GL_NEAREST;
if (!sink.OnBlit(r)) ::_exit(101);
if (g_observedBlitRead != r.ReadFbo || g_observedBlitDraw != r.DrawFbo) ::_exit(102);
::_exit(0);
});
ExpectChildSuccess(child);
}
TEST(RemoteF1, BoundBlitCarriesNoHandlesAndIsAppliedAsBefore) {
const auto child = RunInChild([] {
MG_Config::Transport = MG_Config::TransportMode::InProcess;
CapsPeer backend;
backend.table.GL.BlitFramebuffer = &StubBlitFramebuffer;
Srv::ServerVerbSink sink;
sink.SetBackend(&backend);
MGPBlit r{};
r.Mask = GL_COLOR_BUFFER_BIT;
r.Filter = GL_NEAREST;
if (!sink.OnBlit(r)) ::_exit(101);
if (!MGPipeHandleIsNull(g_observedBlitRead) || !MGPipeHandleIsNull(g_observedBlitDraw))
::_exit(102);
::_exit(0);
});
ExpectChildSuccess(child);
}
TEST(RemoteF1, CopyFramebufferToTextureStashesTheDestinationHandle) {
// Red once (executed, reverted): do not write VerbCopyTexDst; the observed handle is null.
const auto child = RunInChild([] {
MG_Config::Transport = MG_Config::TransportMode::InProcess;
CapsPeer backend;
backend.table.GL.CopyTexImage2D = &StubCopyTexImage2D;
Srv::ServerVerbSink sink;
sink.SetBackend(&backend);
MGPCopyFromFramebuffer r{};
r.Dst = {51, 4};
r.Target = GL_TEXTURE_2D;
r.Level = 1;
r.InternalFormat = GL_RGBA8;
r.Width = 8;
r.Height = 8;
if (!sink.OnCopyFramebufferToTexture(r)) ::_exit(101);
if (g_observedCopyDst != r.Dst) ::_exit(102);
::_exit(0);
});
ExpectChildSuccess(child);
}
TEST(RemoteF1, GenerateMipmapStashesTheTextureHandle) {
// Red once (executed, reverted): do not write VerbMipRes; the observed handle is null.
const auto child = RunInChild([] {
MG_Config::Transport = MG_Config::TransportMode::InProcess;
CapsPeer backend;
backend.table.GL.GenerateMipmap = &StubGenerateMipmap;
Srv::ServerVerbSink sink;
sink.SetBackend(&backend);
MGPMipPlan r{};
r.Res = {52, 5};
r.Target = GL_TEXTURE_2D;
if (!sink.OnGenerateMipmap(r)) ::_exit(101);
if (g_observedMipRes != r.Res) ::_exit(102);
::_exit(0);
});
ExpectChildSuccess(child);
}
TEST(RemoteF1, AnIndirectDrawStashesTheCommandAndParameterBuffers) {
// Red once (executed, reverted): do not write the indirect stash; the observed handles are null.
const auto child = RunInChild([] {
MG_Config::Transport = MG_Config::TransportMode::InProcess;
CapsPeer backend;
backend.table.GL.MultiDrawArraysIndirectCount = &StubMultiDrawArraysIndirectCount;
Srv::ServerVerbSink sink;
sink.SetBackend(&backend);
MGPDrawInfo info{};
info.Mode = GL_TRIANGLES;
info.IndexSize = 0;
info.NumDraws = 0;
info.InstanceCount = 1;
MGPDrawIndirect indirect{};
indirect.Buffer = {61, 1};
indirect.ParameterBuffer = {62, 1};
indirect.Offset = 64;
indirect.DrawCount = 3;
indirect.Stride = 16;
if (!sink.OnDrawVbo(info, nullptr, nullptr, &indirect)) ::_exit(101);
if (g_observedIndirect != indirect.Buffer || g_observedParameter != indirect.ParameterBuffer)
::_exit(102);
::_exit(0);
});
ExpectChildSuccess(child);
}
TEST(RemoteF1, DispatchIndirectStashesTheCommandBuffer) {
// Red once (executed, reverted): do not write VerbDispatchIndirectBuffer; the observed
// handle is null.
const auto child = RunInChild([] {
MG_Config::Transport = MG_Config::TransportMode::InProcess;
CapsPeer backend;
backend.table.GL.DispatchComputeIndirect = &StubDispatchComputeIndirect;
Srv::ServerVerbSink sink;
sink.SetBackend(&backend);
MGPGridInfo grid{};
grid.IsIndirect = 1;
grid.IndirectBuffer = {63, 1};
grid.IndirectOffset = 128;
if (!sink.OnLaunchGrid(grid)) ::_exit(101);
if (g_observedDispatchIndirect != grid.IndirectBuffer) ::_exit(102);
::_exit(0);
});
ExpectChildSuccess(child);
}
// ---- the layer-1 guards (CONTRACT-P5C §6), each verified red once by its own Fatal ---------
TEST(RemoteGuards, AllocatorAcquireFromTheApplyThreadIsFatalByName) {
const auto child = RunInChild([] {
StartControlSession();
Srv::ServerLoopInstance().RunOnApplyThread([](void*) {
MGPipeSlots().Acquire(MGPipeKind::Texture, 424242);
return MOBILEGL_OK;
}, nullptr);
ClientSessionInstance().Stop();
});
ExpectNamedAbort(child, "Fatal{RoleViolation, \"MGPipeSlots\"}");
}
TEST(RemoteGuards, AllocatorFindByLifetimeIdFromTheApplyThreadIsFatalByName) {
const auto child = RunInChild([] {
StartControlSession();
Srv::ServerLoopInstance().RunOnApplyThread([](void*) {
MGPipeSlots().FindByLifetimeId(MGPipeKind::Texture, 424242);
return MOBILEGL_OK;
}, nullptr);
ClientSessionInstance().Stop();
});
ExpectNamedAbort(child, "Fatal{RoleViolation, \"MGPipeSlots\"}");
}
TEST(RemoteGuards, AllocatorFreeFromTheApplyThreadIsFatalByName) {
const auto child = RunInChild([] {
StartControlSession();
Srv::ServerLoopInstance().RunOnApplyThread([](void*) {
MGPipeSlots().Free(MGPipeKind::Texture, {7, 1});
return MOBILEGL_OK;
}, nullptr);
ClientSessionInstance().Stop();
});
ExpectNamedAbort(child, "Fatal{RoleViolation, \"MGPipeSlots\"}");
}
namespace {
struct FakeTwin {
int marker = 0;
};
using TestTextureTable =
MG_Backend::DirectGLES::BackendSlotTable<MG_State::GLState::TextureObject2D, FakeTwin,
MGPipeKind::Texture>;
} // namespace
TEST(RemoteGuards, SlotTableHandleOfFromTheApplyThreadIsFatalByName) {
const auto child = RunInChild([] {
StartControlSession();
Srv::ServerLoopInstance().RunOnApplyThread([](void*) {
MG_State::pGLContext = MakeUnique<MG_State::GLState::GLContext>();
TestTextureTable table;
MG_State::GLState::TextureObject2D tex(44);
table.HandleOf(&tex);
return MOBILEGL_OK;
}, nullptr);
ClientSessionInstance().Stop();
});
ExpectNamedAbort(child, "Fatal{RoleViolation, \"MGPipeSlots\"}");
}
TEST(RemoteGuards, SlotTableMintingGetOrCreateFromTheApplyThreadIsFatalByName) {
const auto child = RunInChild([] {
StartControlSession();
Srv::ServerLoopInstance().RunOnApplyThread([](void*) {
MG_State::pGLContext = MakeUnique<MG_State::GLState::GLContext>();
TestTextureTable table;
auto tex = MakeShared<MG_State::GLState::TextureObject2D>(45);
table.GetOrCreate(tex);
return MOBILEGL_OK;
}, nullptr);
ClientSessionInstance().Stop();
});
ExpectNamedAbort(child, "Fatal{RoleViolation, \"MGPipeSlots\"}");
}
// The four accessor drives share one shape: the BufferObject is constructed on the CLIENT
// thread (its own resource_create publication is a legal client-side emit), and only the
// accessor runs on the apply thread, where it must die at the accessor's own guard.
#define MGL_BUFFER_GUARD_TEST(Name, Id, Call) TEST(RemoteGuards, Name) { const auto child = RunInChild([] { StartControlSession(); MG_State::GLState::BufferObject buffer(Id); Srv::ServerLoopInstance().RunOnApplyThread([](void* self) { auto& buffer = *static_cast<MG_State::GLState::BufferObject*>(self); Call; return MOBILEGL_OK; }, &buffer); ClientSessionInstance().Stop(); }); ExpectNamedAbort(child, "Fatal{RoleViolation, \"buffer-legacy-arm\"}"); }
MGL_BUFFER_GUARD_TEST(BufferMappedDataFromTheApplyThreadIsFatalByName, 703, (void)buffer.MappedData())
MGL_BUFFER_GUARD_TEST(BufferIsMappedFromTheApplyThreadIsFatalByName, 704, (void)buffer.IsMapped())
MGL_BUFFER_GUARD_TEST(BufferChangeSerialFromTheApplyThreadIsFatalByName, 705, (void)buffer.GetChangeSerial())
MGL_BUFFER_GUARD_TEST(BufferSyncPersistentMappedRangeFromTheApplyThreadIsFatalByName, 706,
buffer.SyncPersistentMappedRange())
#undef MGL_BUFFER_GUARD_TEST
TEST(RemoteGuards, CapsMirrorFallbackWithNoServerBackendIsFatalByName) {
const auto child = RunInChild([] {
MG_Config::Transport = MG_Config::TransportMode::InProcess;
// No server backend exists in this process: the format-caps fallback to the client
// mirror is refused by name (CONTRACT-P5C §3.7).
(void)MG_Backend::DirectGLES::ActiveBackendFormatCaps();
});
ExpectNamedAbort(child, "Fatal{RoleViolation, \"caps-mirror\"}");
}
#endif
int main(int argc, char** argv) {
namespace fs = std::filesystem;
const fs::path path =
+43 -38
View File
@@ -1629,44 +1629,6 @@ TEST(StagedShadowProductionTest, ATwinSurvivingContextLossDropsItsFreedShadowBas
// shows it": B is the corruption, the upload is the draw, the mapped read-back is the picture. Red
// once by restoring the MappedData() fallback in liveHostBase(): the store then holds A, the
// client's bytes, and the R-2.5 audit could never reach a draw again.
TEST(ServerLoopEglTest, FrontendFramebufferDeathDeletesOnTheContextOwner) {
MG_Config::Features.PipePush |= MG_Pipe::kMGPipeSubsystemEsprytSlots;
EglServerFixture fixture;
MGL_EGL_BRING_UP_OR_BAIL(fixture);
ASSERT_TRUE(fixture.MakeCurrent());
namespace GLES = MG_Backend::DirectGLES;
static auto nativeDelete = GLES::g_GLESFuncs.glDeleteFramebuffers;
static std::atomic<Uint32> deleted{0};
static std::atomic<Bool> wrongThread{false};
nativeDelete = GLES::g_GLESFuncs.glDeleteFramebuffers;
deleted.store(0);
wrongThread.store(false);
auto framebuffer = MakeShared<MG_State::GLState::FramebufferObject>(901u);
GLuint driverId = 0;
ASSERT_EQ(OnApply([&] {
auto& twin = GLES::FramebufferImpl::g_backendFramebufferObjects.GetOrCreate(framebuffer);
twin = MakeShared<GLES::FramebufferImpl::BackendFramebufferObject>();
driverId = twin->GetBackendFramebufferId();
twin->Bind(FramebufferTarget::Draw);
GLES::g_GLESFuncs.glDeleteFramebuffers = +[](GLsizei count, const GLuint* names) {
if (!Server::ServerLoop::OnApplyThread()) wrongThread.store(true);
deleted.fetch_add(count);
nativeDelete(count, names);
};
}), MOBILEGL_OK);
ASSERT_NE(driverId, 0u);
framebuffer.reset(); // Frontend destructor runs on this client thread.
EXPECT_EQ(deleted.load(), 1u);
EXPECT_FALSE(wrongThread.load());
ASSERT_EQ(OnApply([&] {
GLES::g_GLESFuncs.glDeleteFramebuffers = nativeDelete;
GLint bound = -1;
GLES::g_GLESFuncs.glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &bound);
EXPECT_EQ(bound, 0) << "native deletion must unbind the framebuffer in the owner context";
}), MOBILEGL_OK);
fixture.TearDown();
}
TEST(StagedShadowProductionTest, TheEnsurePathUploadsTheServerShadowNotTheClientObjectsBytes) {
EglServerFixture fixture;
MGL_EGL_BRING_UP_OR_BAIL(fixture);
@@ -1820,6 +1782,49 @@ TEST(StagedShadowProductionTest, TheStreamingIdiomThroughAPoolReuseIsUploadedNot
"whole-store refusal must key on the descriptor's HasDefinedContent); the log says: "
<< appended;
}
namespace {
// The drive below, in a FORKED CHILD that brings the server up itself (the integration
// harness's pre-flight shape), so a Fatal on the apply thread ends the child and not the
// case. Exit 8 = the child's bring-up failed; exit 0 = the apply-thread twin mint ran,
// which is the refusal this case pins having been removed.
[[noreturn]] void RunFrontendFramebufferDeathOnApplyThreadAndExit() {
EglServerFixture fixture;
if (!fixture.BringUp().empty() || !fixture.MakeCurrent()) ::_exit(8);
namespace GLES = MG_Backend::DirectGLES;
auto framebuffer = MakeShared<MG_State::GLState::FramebufferObject>(901u);
(void)OnApply([&] {
auto& twin = GLES::FramebufferImpl::g_backendFramebufferObjects.GetOrCreate(framebuffer);
twin = MakeShared<GLES::FramebufferImpl::BackendFramebufferObject>();
});
framebuffer.reset();
::_exit(0);
}
} // namespace
// P5c (hd, CONTRACT-P5C §3.1 / §6 layer 1): this case's original drive - mint the framebuffer
// twin on the apply thread off the frontend object's lifetime id, then let the frontend death
// notice reach the in-process death switch there - is refused by name at the first step:
// the minting GetOrCreate(StatePtr) is monolith glue and the apply-thread call is
// Fatal{RoleViolation, "MGPipeSlots"}. The deletion-on-owner semantics it pinned return with
// ct's object_death record (the framebuffer family's first wire delete), whose sink releases
// the twin BY HANDLE. The name stays; the pin is now the refusal that stands until ct lands.
TEST(ServerLoopEglTest, FrontendFramebufferDeathDeletesOnTheContextOwner) {
MG_Config::Features.PipePush |= MG_Pipe::kMGPipeSubsystemEsprytSlots;
{
EglServerFixture probe;
MGL_EGL_BRING_UP_OR_BAIL(probe);
probe.TearDown();
}
const SizeT mark = ReadLog().size();
EXPECT_EXIT(RunFrontendFramebufferDeathOnApplyThreadAndExit(), ::testing::KilledBySignal(SIGABRT), ".*")
<< "the apply-thread mint of a frontend framebuffer's twin was not refused; exit 8 is the "
"child's own bring-up failing";
const std::string appended = ReadLogFrom(mark);
EXPECT_NE(appended.find("Fatal{RoleViolation, \"MGPipeSlots\"}"), std::string::npos)
<< "the abort was not the allocator guard's; the log says: " << appended;
}
#endif // !_WIN32
// =====================================================================================
+5 -6
View File
@@ -58,12 +58,11 @@ def cases():
[SUITE+'ReadPixelsPutsTheTightExtentOnTheWire'], 'RemoteClientTest'),
('M4-escape-errors-deleted', WIRE, escape_error,
[SUITE+'ResourceRespecifyErrorIsNotADecline', SUITE+'MapPersistentErrorIsNotADecline'], 'RemoteClientTest'),
('M5-server-role-guards-deleted', FILL,
replace('!MG_Remote::Client::RunsAsTheServerRole() && ', '', 2),
[SUITE+'ServerRoleRespecifyDoesNotEmitClientInitialBytes'], 'RemoteClientTest'),
('M5-server-flush-guard-deleted', FILL,
replace('!MG_Remote::Client::RunsAsTheServerRole() && size != 0', 'size != 0'),
[SUITE+'ServerRoleFlushDoesNotRunTheClientSubDataFollowup'], 'RemoteClientTest'),
('M5-allocator-guards-deleted', 'MobileGL/MG_Impl/Pipe/SlotAllocator.cpp',
lambda src: replace('MGPipeRefuseAllocatorFromApplyThread("FindByLifetimeId");', '')(
replace('MGPipeRefuseAllocatorFromApplyThread("Acquire");', '')(src)),
[SUITE+'ServerRoleRespecifyDoesNotEmitClientInitialBytes',
SUITE+'ServerRoleFlushDoesNotRunTheClientSubDataFollowup'], 'RemoteClientTest'),
('M8-real-pack-binding', EMIT,
replace('if (MG_State::pGLContext != nullptr &&', 'if (false && MG_State::pGLContext != nullptr &&'),
[SUITE+'BoundPackBufferOffsetReadRefusesByName'], 'RemoteClientTest'),