mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-18 09:08:31 +09:00
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:
File diff suppressed because it is too large
Load Diff
@@ -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,8 +5809,16 @@ 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())
|
||||
: MG_Pipe::kMGPipeNullHandle;
|
||||
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());
|
||||
if (TextureResourceSubsystemEnabled() && MG_Pipe::MGPipeHandleIsNull(rearmRes)) {
|
||||
@@ -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());
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,9 +714,23 @@ 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
|
||||
? pActiveBackendObject->GetDynamicParameters().MaxShaderStorageBufferBindings
|
||||
: 0;
|
||||
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)) {
|
||||
MGB_CTX->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 "
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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{};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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
|
||||
|
||||
// =====================================================================================
|
||||
|
||||
@@ -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'),
|
||||
|
||||
Reference in New Issue
Block a user