[Feature] (SharedImage): server-allocated AHB images over a shared_image pipe verb, with dma-buf fds on the aux socket

This commit is contained in:
2026-10-02 07:52:25 -04:00
parent e06aabe812
commit 38842985f8
26 changed files with 954 additions and 27 deletions
+3
View File
@@ -675,6 +675,9 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
MobileGL/MG_Remote/Server/PairAcceptor.cpp
# P11 B2: T0's Offers on the aux socket, matched to their map_persistent records by seq.
MobileGL/MG_Remote/Server/AdoptInbox.cpp
# Shared images: server-allocated AHardwareBuffers exported to clients as dma-buf descriptors
# (docs/Disaggregated/notes/anland/plan-ahb-dmabuf.md).
MobileGL/MG_Remote/Server/SharedImageRegistry.cpp
MobileGL/MG_Remote/Server/ServerSpawn.cpp
# P5f fc: the value frame that replaced the control mailbox's function-pointer payload,
# and its codec against protocol.fbs's SurfaceOp/SurfaceReply (the spawn arm's seam).
+72
View File
@@ -583,10 +583,82 @@ namespace MobileGL {
Uint32 Height = 0;
};
// SHARED IMAGES (docs/Disaggregated/notes/anland/plan-ahb-dmabuf.md): server-allocated colour
// buffers exported to clients as dma-buf descriptors.
//
// The server-side view of one image, as a backend sees it: `NativeBuffer` is the image's
// AHardwareBuffer (null where there is none), `Fourcc` a DRM fourcc - ABGR8888 (R, G, B, A in
// memory, GL RGBA8) or XBGR8888 (alpha ignored).
struct SharedImageView {
Uint64 Id = 0;
Uint32 Width = 0;
Uint32 Height = 0;
Uint32 Fourcc = 0;
void* NativeBuffer = nullptr;
};
// The client-side answer of an allocation: the image's id and its exported descriptor
// (`Fd`, owned by the caller), with the plane layout a linux-dmabuf wl_buffer is created with.
struct SharedImageExport {
Uint64 Id = 0;
int Fd = -1;
Uint32 Width = 0;
Uint32 Height = 0;
Uint32 Fourcc = 0;
Uint32 Stride = 0;
Uint32 Offset = 0;
Uint64 Modifier = 0;
};
class BackendObject {
public:
virtual ~BackendObject() = default;
// ---- shared images, SERVER side (a backend implements these) ---------------------
//
// Copies the default framebuffer of the surface current on this (the apply) thread into
// `image`, rows TOP FIRST - GL's top row (y = height-1) lands in the image's first memory
// row, which is what a wl_buffer's first row is - scaled to the image's extent if the two
// differ. Returns once the copy has COMPLETED on the GPU (the client attaches the buffer
// to a wl_surface right after, and nothing else orders the compositor's read). The
// application's GL state is untouched. False = not supported / failed (logged).
virtual Bool BlitDefaultFramebufferToSharedImage(const SharedImageView& image) {
(void)image;
return false;
}
// ---- shared images, CLIENT side (BackendObject_Remote implements these) ----------
//
// A new image, allocated by the server; `out->Fd` is the caller's to close.
virtual Bool AllocateSharedImage(Uint32 width, Uint32 height, Uint32 fourcc, SharedImageExport* out) {
(void)width, (void)height, (void)fourcc, (void)out;
return false;
}
// Names the image `fd` was exported from (the caller keeps `fd`). False: not one of this
// server's images, or the size/format disagree.
virtual Bool ImportSharedImage(int fd, Uint32 width, Uint32 height, Uint32 fourcc, Uint64* outId) {
(void)fd, (void)width, (void)height, (void)fourcc, (void)outId;
return false;
}
// Drops this client's reference (an allocation's or an import's).
virtual Bool ReleaseSharedImage(Uint64 id) {
(void)id;
return false;
}
// In the GL stream: the current context's default framebuffer, copied top row first into
// the image (BlitDefaultFramebufferToSharedImage on the server). Returns when it completed.
virtual Bool PresentToSharedImage(Uint64 id) {
(void)id;
return false;
}
// In the GL stream: the texture whose lifetime id is `textureLifetimeId` takes the image as
// its level-0 storage (glEGLImageTargetTexture2DOES), until its next storage definition.
// The caller has already defined level 0 as Width x Height RGBA8 with no data.
virtual Bool AttachSharedImageToTexture(Uint64 textureLifetimeId, Uint64 id) {
(void)textureLifetimeId, (void)id;
return false;
}
virtual void Initialize() = 0;
virtual Bool InitCapabilities() = 0;
virtual Bool InitWindowSurface() = 0;
+59
View File
@@ -1978,6 +1978,65 @@ namespace MobileGL::MG_Pipe {
};
MGP_ASSERT_POD(MGPBindContext, 8);
// shared_image = opcode 85 (docs/Disaggregated/notes/anland/plan-ahb-dmabuf.md). Colour
// buffers the SERVER allocates and exports as dma-buf descriptors, so one client's frames
// reach another client's textures without leaving GPU memory. One row, five operations, all
// answered through the reply slot with an MGPSharedImageReply:
//
// Allocate a new Width x Height Format image; the reply names it, and its descriptor
// reaches the client on the aux socket BEFORE the reply is posted.
// Import the descriptor the client queued on the aux socket (sideband: a
// MGPSharedImageFdOffer naming this record's seq) is identified as an image this
// server exported; the reply names it. Width/Height/Format must match it.
// Release the session's reference to ImageId goes.
// Present the current context's default framebuffer is copied, top row first, into
// ImageId (a window frame becoming a wl_buffer's contents); the reply is posted
// when the copy has completed on the GPU.
// Attach the texture Texture's storage becomes ImageId (glEGLImageTargetTexture2DOES);
// it lasts until the texture's next storage-defining respecify.
//
// ImageId is a server-minted id, never reused within the server's life.
enum MGPSharedImageOpKind : Uint32 {
kMGPSharedImageAllocate = 1,
kMGPSharedImageImport = 2,
kMGPSharedImageRelease = 3,
kMGPSharedImagePresent = 4,
kMGPSharedImageAttach = 5,
};
struct MGPSharedImageOp {
Uint32 Op;
Uint32 Format; // DRM fourcc
Uint32 Width;
Uint32 Height;
Uint64 ImageId;
MGPipeHandle Texture; // Attach only
};
MGP_ASSERT_POD(MGPSharedImageOp, 32);
struct MGPSharedImageReply {
Uint64 ImageId;
Uint64 Modifier;
Uint32 Width;
Uint32 Height;
Uint32 Format;
Uint32 Stride;
Uint32 Offset;
Uint32 Pad0;
};
MGP_ASSERT_POD(MGPSharedImageReply, 40);
// The aux-socket sideband an Import's descriptor travels with (and an Allocate's answer).
inline constexpr Uint32 kMGPSharedImageFdMagic = 0x49534D47u; // 'MGSI'
struct MGPSharedImageFdOffer {
Uint32 Magic;
Uint32 Version;
Uint64 Seq;
Uint64 ImageId;
Uint64 Reserved;
};
MGP_ASSERT_POD(MGPSharedImageFdOffer, 32);
// set_context_values = opcode 79, P5c rv (CONTRACT-P5C.md §5.3): the rv field table as
// ONE fixed-width POD. It carries every value-class field that no set_* call supplies -
+12
View File
@@ -2238,6 +2238,16 @@ namespace MobileGL::MG_Pipe {
return true;
}
Bool MGPipeApplyAttachSharedImage(MGPipeHandle texture, Uint64 imageId) {
MGPipeResourceRecord* record = FindIn(MGPipeApplier().TextureResources, texture);
if (record == nullptr) return false;
record->SharedImageId = imageId;
// The storage moved, so every twin's synced serial is stale, and the sampling answer with it.
++record->Serial;
MGPipeApplierNoteTextureStateMoved();
return true;
}
Bool MGPipeApplyResourceRespecify(const MGPResourceDesc& desc, const void* initialBytes,
const MGPRespecifiedLevel* level) {
Vector<MGPipeResourceRecord>* table = ResourceTableForTarget(desc.Target);
@@ -2291,6 +2301,8 @@ namespace MobileGL::MG_Pipe {
// asking a frontend object for them. A metadata update replaces it too - that is how
// the mask arrives - and by construction only the non-storage fields differ.
record->Desc = desc;
// A shared image is the storage this call replaces - unless the call replaces none.
if (level != nullptr || !metadataOnly) record->SharedImageId = 0;
#if MOBILEGL_BUILD_DISAGGREGATED
if (level != nullptr && desc.Target != static_cast<Uint8>(MGPipeResourceTarget::TexBuffer)) {
// Scope and BufOffset/BufSize are transient respecify carriers, not resource state.
+11
View File
@@ -294,6 +294,13 @@ namespace MobileGL::MG_Pipe {
// DirectGLESBufferDrawProbe pair now pins from both sides.
Bool HasLiveHostWrites = false;
// SHARED IMAGES (docs/Disaggregated/notes/anland/plan-ahb-dmabuf.md). Nonzero: this
// texture's level-0 storage IS the server's shared image of this id (an EGLImage target),
// set by shared_image's Attach and cleared by the next respecify that redefines storage.
// The backend resolves the id at its texture sync (MG_Remote::Server::SharedImages::Find)
// and binds that buffer instead of allocating storage; Serial moves with it.
Uint64 SharedImageId = 0;
// ---- P4a. Only a record of kind Texture ever carries these; a buffer's stay at
// their defaults, which is what keeps ONE record type for the discriminated
// descriptor rather than a second one that would have to be kept in step with it.
@@ -1392,6 +1399,10 @@ namespace MobileGL::MG_Pipe {
// Returns true when the descriptor was stored - metadata updates included, since the record
// did move - and false when the call was refused: a descriptor whose target names no
// resource kind, or a handle this applier has no live record for at that generation.
// SHARED IMAGES: the texture `texture`'s level-0 storage becomes shared image `imageId`
// (MGPipeResourceRecord::SharedImageId). False when the texture has no live record.
Bool MGPipeApplyAttachSharedImage(MGPipeHandle texture, Uint64 imageId);
Bool MGPipeApplyResourceRespecify(const MGPResourceDesc& desc, const void* initialBytes,
const MGPRespecifiedLevel* level = nullptr);
// resource_subdata, buffer half: the destination range rides in the record's box through
+13 -4
View File
@@ -72,9 +72,9 @@
// that the expansion, the two generated tables and this number agree.
//
// class entries group (as the plan tabulates it)
// kScreen 13 screen: caps 1 + resource 3 + persistent map 2 + fence 4, plus the
// kScreen 14 screen: caps 1 + resource 3 + persistent map 2 + fence 4, plus the
// appended server-side fence wait 1 and P5c's applier_reset 1 and
// P14 S1's bind_context 1
// P14 S1's bind_context 1 and the shared-image row 1
// kCtxQuery 8 query object namespace 6, plus the appended timestamp pair 2
// kCtxCso 13 CSO create/bind/delete
// kCtxState 19 16 of the 18 set_* calls + the temporary set_residual_value_state
@@ -153,9 +153,11 @@
// design's per-record ContextSlot was NOT taken here: the token is announced at the edge
// (a context change) instead of stamped on every verb, which costs one record per
// make-current and leaves the 71 context-family payloads untouched. 84 unique records.
// shared_image (85), the server-allocated colour buffers exported as dma-bufs. 85 unique
// records.
// ---------------------------------------------------------------------------------------
#define MGP_CALL_LIST_DOCUMENTED_COUNT 84
#define MGP_CALL_LIST_DOCUMENTED_COUNT 85
// clang-format off
#define MGP_CALL_LIST(X) \
@@ -374,7 +376,14 @@
/* ---- context-family verb, and it has no MGPipeApply* entry point and no monolith producer. ---- */ \
/* ---- kWaitNone: the apply reads no client memory and the record's placement in the ring ---- */ \
/* ---- is the whole of its ordering. ---- */ \
X(BindContext, MGPBindContext, kScreen, kNone, kWaitNone)
X(BindContext, MGPBindContext, kScreen, kNone, kWaitNone) \
/* ---- APPENDED FOR SHARED IMAGES (docs/Disaggregated/notes/anland/plan-ahb-dmabuf.md), ---- */ \
/* ---- opcode 85: allocate / import / release / present / attach of a server-allocated ---- */ \
/* ---- colour buffer exported as a dma-buf. kScreen: the image table is the server's, and ---- */ \
/* ---- present / attach act on the session's current context the way bind_context does. ---- */ \
/* ---- kWaitReply: every operation's answer is acted on (an id, a descriptor, a fence). ---- */ \
/* ---- No MGPipeApply* entry point and no monolith producer. ---- */ \
X(SharedImage, MGPSharedImageOp, kScreen, kReplySlot, kWaitReply)
// clang-format on
// Explicitly NOT migrated (plan 4.4.6 / appendix A "explicit deletions"):
+5 -1
View File
@@ -301,6 +301,10 @@
#define MGP_FIELDS_MGPBindContext(F) \
F(ClientContextToken)
// shared_image's POD.
#define MGP_FIELDS_MGPSharedImageOp(F) \
F(Op) F(Format) F(Width) F(Height) F(ImageId) F(Texture)
// P5c rv (CONTRACT-P5C.md §5.3): set_context_values' POD, one row per member. The 15-entry
// array is one field, exactly as MGPFramebufferState's Color[8] is one.
#define MGP_FIELDS_MGPContextValues(F) \
@@ -427,7 +431,7 @@
P(MGPSurfaceInfo) \
P(MGPImageBind) P(MGPPatchParameter) P(MGPStreamOutputBind) P(MGPStorageBlockBinding) \
P(MGPCopyFromFramebuffer) \
P(MGPApplierReset) P(MGPBindContext) P(MGPContextValues) \
P(MGPApplierReset) P(MGPBindContext) P(MGPSharedImageOp) P(MGPContextValues) \
P(MGPProgramBindings) P(MGPProgramSamplerUnit) P(MGPProgramStorageOverride) \
P(RenderStateParameters) P(PixelStoreParameters) P(SamplerParameters) P(PerBufferBlendState) \
P(StencilFaceState) \
+4 -3
View File
@@ -12,7 +12,7 @@
// Regenerate with `python3 scripts/gen_pipe.py`; CI runs it and diffs the result.
// This file is included from MG_Pipe/MGPipe.h inside namespace MobileGL::MG_Pipe.
// share group: 13 calls. A null entry means the backend does not implement this
// share group: 14 calls. A null entry means the backend does not implement this
// call and the frontend keeps its own path (plan B section 4.1).
struct MGPipeScreen {
void (*GetCaps)(const MGPCaps* payload, const void* blobBytes, Uint64 blobByteCount, MGPReplySlot* reply);
@@ -28,6 +28,7 @@ struct MGPipeScreen {
void (*FenceWaitServer)(const MGPFenceWait* payload);
void (*ApplierReset)(const MGPApplierReset* payload);
void (*BindContext)(const MGPBindContext* payload);
void (*SharedImage)(const MGPSharedImageOp* payload, MGPReplySlot* reply);
};
// context: 71 calls. A null entry means the backend does not implement this
@@ -106,9 +107,9 @@ struct MGPipeContext {
void (*GetTextureImageToBuffer)(const MGPReadbackToBuffer* payload);
};
inline constexpr SizeT kMGPipeScreenCallCount = 13;
inline constexpr SizeT kMGPipeScreenCallCount = 14;
inline constexpr SizeT kMGPipeContextCallCount = 71;
inline constexpr SizeT kMGPipeCallCount = 84;
inline constexpr SizeT kMGPipeCallCount = 85;
// A table that is not exactly its call count of function pointers has grown a
// member that no generator knows about.
@@ -352,3 +352,7 @@ inline void MGP_GetTextureImageToBuffer(const MGPReadbackToBuffer* payload) {
inline void MGP_BindContext(const MGPBindContext* payload) {
gMGPipeScreen.BindContext(payload);
}
inline void MGP_SharedImage(const MGPSharedImageOp* payload, MGPReplySlot* reply) {
gMGPipeScreen.SharedImage(payload, reply);
}
+9 -1
View File
@@ -115,6 +115,7 @@ inline Bool MGPipeVerify(const MGPStorageBlockBinding& a, const MGPStorageBlockB
inline Bool MGPipeVerify(const MGPCopyFromFramebuffer& a, const MGPCopyFromFramebuffer& b, const char** outField);
inline Bool MGPipeVerify(const MGPApplierReset& a, const MGPApplierReset& b, const char** outField);
inline Bool MGPipeVerify(const MGPBindContext& a, const MGPBindContext& b, const char** outField);
inline Bool MGPipeVerify(const MGPSharedImageOp& a, const MGPSharedImageOp& b, const char** outField);
inline Bool MGPipeVerify(const MGPContextValues& a, const MGPContextValues& b, const char** outField);
inline Bool MGPipeVerify(const MGPProgramBindings& a, const MGPProgramBindings& b, const char** outField);
inline Bool MGPipeVerify(const MGPProgramSamplerUnit& a, const MGPProgramSamplerUnit& b, const char** outField);
@@ -272,6 +273,8 @@ struct MGPipeHasFieldVerifier<MGPApplierReset> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPBindContext> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPSharedImageOp> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPContextValues> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPProgramBindings> : std::true_type {};
@@ -698,6 +701,11 @@ inline Bool MGPipeVerify(const MGPBindContext& a, const MGPBindContext& b, const
return true;
}
inline Bool MGPipeVerify(const MGPSharedImageOp& a, const MGPSharedImageOp& b, const char** outField) {
MGP_FIELDS_MGPSharedImageOp(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPContextValues& a, const MGPContextValues& b, const char** outField) {
MGP_FIELDS_MGPContextValues(MGP_VERIFY_FIELD)
return true;
@@ -765,4 +773,4 @@ inline Bool MGPipeVerify(const MGPVertexBindingPointWire& a, const MGPVertexBind
#undef MGP_VERIFY_FIELD
inline constexpr SizeT kMGPipeVerifiedPayloadCount = 84;
inline constexpr SizeT kMGPipeVerifiedPayloadCount = 85;
+16 -1
View File
@@ -154,7 +154,8 @@ enum class MGPWireOp : Uint16 {
ReadPixelsToBuffer = 82,
GetTextureImageToBuffer = 83,
BindContext = 84,
kOpCount = 85,
SharedImage = 85,
kOpCount = 86,
};
// THE FLAGS, EXPORTED ONCE, INDEXED BY OPCODE (P5 R-13.4). MGPWireRecHeader::Flags is
@@ -260,6 +261,7 @@ inline constexpr Uint32 kMGPipeCallFlags[static_cast<SizeT>(MGPWireOp::kOpCount)
/* 82 ReadPixelsToBuffer */ static_cast<Uint32>(kNone),
/* 83 GetTextureImageToBuffer */ static_cast<Uint32>(kNone),
/* 84 BindContext */ static_cast<Uint32>(kNone),
/* 85 SharedImage */ static_cast<Uint32>(kReplySlot),
};
static_assert(sizeof(kMGPipeCallFlags) / sizeof(kMGPipeCallFlags[0]) ==
static_cast<SizeT>(MGPWireOp::kOpCount),
@@ -364,6 +366,7 @@ inline constexpr Uint8 kMGPipeCallClasses[static_cast<SizeT>(MGPWireOp::kOpCount
/* 82 ReadPixelsToBuffer */ static_cast<Uint8>(kCtxVerb),
/* 83 GetTextureImageToBuffer */ static_cast<Uint8>(kCtxObject),
/* 84 BindContext */ static_cast<Uint8>(kScreen),
/* 85 SharedImage */ static_cast<Uint8>(kScreen),
};
static_assert(sizeof(kMGPipeCallClasses) / sizeof(kMGPipeCallClasses[0]) ==
static_cast<SizeT>(MGPWireOp::kOpCount),
@@ -472,6 +475,7 @@ inline constexpr Uint8 kMGPipeWaitClasses[static_cast<SizeT>(MGPWireOp::kOpCount
/* 82 ReadPixelsToBuffer */ static_cast<Uint8>(kWaitNone),
/* 83 GetTextureImageToBuffer */ static_cast<Uint8>(kWaitNone),
/* 84 BindContext */ static_cast<Uint8>(kWaitNone),
/* 85 SharedImage */ static_cast<Uint8>(kWaitReply),
};
static_assert(sizeof(kMGPipeWaitClasses) / sizeof(kMGPipeWaitClasses[0]) ==
static_cast<SizeT>(MGPWireOp::kOpCount),
@@ -1210,6 +1214,14 @@ static_assert(sizeof(MGPWireRec_BindContext) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPBindContext) + 7u) & ~SizeT(7u)),
"MGPWireRec_BindContext gained padding; the wire format moved");
struct alignas(8) MGPWireRec_SharedImage {
MGPWireRecHeader Header;
MGPSharedImageOp Payload;
};
static_assert(sizeof(MGPWireRec_SharedImage) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPSharedImageOp) + 7u) & ~SizeT(7u)),
"MGPWireRec_SharedImage gained padding; the wire format moved");
[[noreturn]] inline void MGPipeWireProtocolFatal(const char* call, Uint64 size, Uint64 remaining) {
MGLOG_F("MGPipe: protocol corruption applying %s: size=%llu remaining=%llu", call,
static_cast<unsigned long long>(size), static_cast<unsigned long long>(remaining));
@@ -1511,6 +1523,9 @@ inline Bool MGPipeApplyWireRecord(MGPWireOp op, const void* record, Uint64 size,
case MGPWireOp::BindContext:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_BindContext, "BindContext");
break;
case MGPWireOp::SharedImage:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_SharedImage, "SharedImage");
break;
case MGPWireOp::kInvalid:
case MGPWireOp::kOpCount:
default:
@@ -433,6 +433,13 @@ inline constexpr WireLayoutMember kMGPipeWireLayoutMembers[] = {
{"MGPApplierReset.ContextSerial", offsetof(MGPApplierReset, ContextSerial), sizeof(((MGPApplierReset*)nullptr)->ContextSerial)},
{"MGPBindContext", 0, sizeof(MGPBindContext)},
{"MGPBindContext.ClientContextToken", offsetof(MGPBindContext, ClientContextToken), sizeof(((MGPBindContext*)nullptr)->ClientContextToken)},
{"MGPSharedImageOp", 0, sizeof(MGPSharedImageOp)},
{"MGPSharedImageOp.Op", offsetof(MGPSharedImageOp, Op), sizeof(((MGPSharedImageOp*)nullptr)->Op)},
{"MGPSharedImageOp.Format", offsetof(MGPSharedImageOp, Format), sizeof(((MGPSharedImageOp*)nullptr)->Format)},
{"MGPSharedImageOp.Width", offsetof(MGPSharedImageOp, Width), sizeof(((MGPSharedImageOp*)nullptr)->Width)},
{"MGPSharedImageOp.Height", offsetof(MGPSharedImageOp, Height), sizeof(((MGPSharedImageOp*)nullptr)->Height)},
{"MGPSharedImageOp.ImageId", offsetof(MGPSharedImageOp, ImageId), sizeof(((MGPSharedImageOp*)nullptr)->ImageId)},
{"MGPSharedImageOp.Texture", offsetof(MGPSharedImageOp, Texture), sizeof(((MGPSharedImageOp*)nullptr)->Texture)},
{"MGPContextValues", 0, sizeof(MGPContextValues)},
{"MGPContextValues.ActiveTextureUnit", offsetof(MGPContextValues, ActiveTextureUnit), sizeof(((MGPContextValues*)nullptr)->ActiveTextureUnit)},
{"MGPContextValues.MaxTouchedTextureUnit", offsetof(MGPContextValues, MaxTouchedTextureUnit), sizeof(((MGPContextValues*)nullptr)->MaxTouchedTextureUnit)},
@@ -765,4 +772,5 @@ inline constexpr WireCallLayout kMGPipeWireCallLayouts[] = {
{"ReadPixelsToBuffer", "MGPReadbackToBuffer", static_cast<Uint64>(MGPWireOp::ReadPixelsToBuffer), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitNone)},
{"GetTextureImageToBuffer", "MGPReadbackToBuffer", static_cast<Uint64>(MGPWireOp::GetTextureImageToBuffer), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitNone)},
{"BindContext", "MGPBindContext", static_cast<Uint64>(MGPWireOp::BindContext), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitNone)},
{"SharedImage", "MGPSharedImageOp", static_cast<Uint64>(MGPWireOp::SharedImage), static_cast<Uint64>(kReplySlot), static_cast<Uint64>(kWaitReply)},
};
@@ -16,6 +16,7 @@
#include "../Server/ServerLoop.h"
#include <MG_Impl/Pipe/SlotAllocator.h>
#include <MG_State/EGLState/Core.h>
#include <MG_Util/Debug/Log.h>
@@ -429,6 +430,78 @@ namespace MobileGL::MG_Remote::Client {
MG_Backend::BackendObject::ReleaseEGLResources();
}
// ---- shared images ----------------------------------------------------------------------
//
// One shared_image record each (ClientSession::EmitSharedImage). Allocate/Import/Release are
// table operations and order against nothing; Present and Attach ride the ring behind the GL
// calls they follow, which is the order they need.
Bool BackendObject_Remote::AllocateSharedImage(Uint32 width, Uint32 height, Uint32 fourcc,
MG_Backend::SharedImageExport* out) {
ClientSession* session = ClientSession::Active();
if (session == nullptr || out == nullptr) return false;
MG_Pipe::MGPSharedImageOp op{};
op.Op = MG_Pipe::kMGPSharedImageAllocate;
op.Format = fourcc;
op.Width = width;
op.Height = height;
MG_Pipe::MGPSharedImageReply reply{};
int fd = -1;
if (!session->EmitSharedImage(op, -1, &reply, &fd)) return false;
out->Id = reply.ImageId;
out->Fd = fd;
out->Width = reply.Width;
out->Height = reply.Height;
out->Fourcc = reply.Format;
out->Stride = reply.Stride;
out->Offset = reply.Offset;
out->Modifier = reply.Modifier;
return true;
}
Bool BackendObject_Remote::ImportSharedImage(int fd, Uint32 width, Uint32 height, Uint32 fourcc, Uint64* outId) {
ClientSession* session = ClientSession::Active();
if (session == nullptr || fd < 0) return false;
MG_Pipe::MGPSharedImageOp op{};
op.Op = MG_Pipe::kMGPSharedImageImport;
op.Format = fourcc;
op.Width = width;
op.Height = height;
MG_Pipe::MGPSharedImageReply reply{};
if (!session->EmitSharedImage(op, fd, &reply, nullptr)) return false;
if (outId != nullptr) *outId = reply.ImageId;
return true;
}
Bool BackendObject_Remote::ReleaseSharedImage(Uint64 id) {
ClientSession* session = ClientSession::Active();
if (session == nullptr || !session->Started()) return false;
MG_Pipe::MGPSharedImageOp op{};
op.Op = MG_Pipe::kMGPSharedImageRelease;
op.ImageId = id;
return session->EmitSharedImage(op, -1, nullptr, nullptr);
}
Bool BackendObject_Remote::PresentToSharedImage(Uint64 id) {
ClientSession* session = ClientSession::Active();
if (session == nullptr) return false;
MG_Pipe::MGPSharedImageOp op{};
op.Op = MG_Pipe::kMGPSharedImagePresent;
op.ImageId = id;
return session->EmitSharedImage(op, -1, nullptr, nullptr);
}
Bool BackendObject_Remote::AttachSharedImageToTexture(Uint64 textureLifetimeId, Uint64 id) {
ClientSession* session = ClientSession::Active();
if (session == nullptr) return false;
MG_Pipe::MGPSharedImageOp op{};
op.Op = MG_Pipe::kMGPSharedImageAttach;
op.ImageId = id;
// The handle the texture's resource records were published under (TextureEmit's own
// AcquireTexture is this call on the same lifetime id).
op.Texture = MG_Pipe::MGPipeSlots().Acquire(MG_Pipe::MGPipeKind::Texture, textureLifetimeId);
return session->EmitSharedImage(op, -1, nullptr, nullptr);
}
// NO strong CreateRemoteBackendObject() lives here, and the reason is a link fact, not an
// oversight. v1's Init.cpp calls MG_Remote::Client::CreateRemoteBackendObject() and ships a
// __attribute__((weak)) placeholder for it beside ServerLoop that aborts by name; its comment
@@ -78,6 +78,14 @@ namespace MobileGL::MG_Remote::Client {
void ReleaseEGLSurface(EGLSurface surface) override;
void ReleaseEGLResources() override;
// ---- shared images (docs/Disaggregated/notes/anland/plan-ahb-dmabuf.md) -------------
Bool AllocateSharedImage(Uint32 width, Uint32 height, Uint32 fourcc,
MG_Backend::SharedImageExport* out) override;
Bool ImportSharedImage(int fd, Uint32 width, Uint32 height, Uint32 fourcc, Uint64* outId) override;
Bool ReleaseSharedImage(Uint64 id) override;
Bool PresentToSharedImage(Uint64 id) override;
Bool AttachSharedImageToTexture(Uint64 textureLifetimeId, Uint64 id) override;
// Copies the caps mirror's FormatCapabilityCache into the base class's
// m_formatCapabilities. Public because CapsMirror's adoption hook is a free function
// and this is what it calls; it is the whole of trap 2's answer.
@@ -2857,6 +2857,59 @@ namespace MobileGL::MG_Remote::Client {
return nullptr;
}
Bool ClientSession::EmitSharedImage(const MG_Pipe::MGPSharedImageOp& op, int sendFd,
MG_Pipe::MGPSharedImageReply* reply, int* receivedFd) {
if (receivedFd != nullptr) *receivedFd = -1;
MobileGLResult shared = MOBILEGL_OK;
if (sendFd >= 0) {
m_prePublish = [&](Uint64 seq) {
MG_Pipe::MGPSharedImageFdOffer offer{MG_Pipe::kMGPSharedImageFdMagic, 1, seq, op.ImageId, 0};
shared = m_transport != nullptr ? m_transport->ShareFd(sendFd, MobileGLByteSpan{&offer, sizeof(offer)})
: MOBILEGL_ERR_NOT_INITIALIZED;
};
}
MG_Pipe::MGPSharedImageReply answer{};
Int32 status = Wire::ReplySink::kStatusError;
Uint64 returned = 0;
const Uint64 seq = EmitAndWait(MG_Pipe::MGPWireOp::SharedImage, &op, sizeof(op), nullptr, 0, &answer,
sizeof(answer), &status, &returned);
m_prePublish = nullptr;
if (shared != MOBILEGL_OK) {
MGLOG_E("MG_Remote client: a shared-image descriptor could not be queued on the aux socket (rc=%d)",
static_cast<int>(shared));
}
if (status != Wire::ReplySink::kStatusOk || returned != sizeof(answer)) return false;
if (reply != nullptr) *reply = answer;
if (receivedFd == nullptr) return true;
#if defined(_WIN32)
return false;
#else
if (m_transport == nullptr) return false;
for (;;) {
int fd = -1;
Uint8 sideband[Transport::FdPassing::kMaxSidebandBytes] = {};
std::uint64_t sidebandSize = 0;
const MobileGLResult received =
m_transport->ReceiveFd(&fd, MobileGLMutableByteSpan{sideband, sizeof(sideband)}, &sidebandSize, 2000);
if (received != MOBILEGL_OK) {
MGLOG_E("MG_Remote client: shared image %llu was answered but its descriptor did not arrive (rc=%d)",
static_cast<unsigned long long>(answer.ImageId), static_cast<int>(received));
return false;
}
MG_Pipe::MGPSharedImageFdOffer offer{};
if (sidebandSize == sizeof(offer)) std::memcpy(&offer, sideband, sizeof(offer));
if (offer.Magic == MG_Pipe::kMGPSharedImageFdMagic && offer.Seq == seq && offer.ImageId == answer.ImageId) {
*receivedFd = fd;
return true;
}
// An earlier record's descriptor whose answer was never taken: not this one.
MGLOG_W("MG_Remote client: an aux descriptor for record %llu arrived while record %llu's was awaited; closed",
static_cast<unsigned long long>(offer.Seq), static_cast<unsigned long long>(seq));
::close(fd);
}
#endif
}
void ClientSession::ReleaseT0Store(MG_Pipe::MGPipeHandle handle) {
auto it = m_t0Stores.find(T0Key(handle));
if (it == m_t0Stores.end()) return;
@@ -242,6 +242,13 @@ namespace MobileGL::MG_Remote::Client {
// server's inbox relies on), and wait for the answer. The held pointer on OK - the caller
// adopts it as the store's mapping - or null on DECLINED (the store stays T2).
void* AdoptPersistentT0(const MG_Pipe::MGPHandleOnly& handle, Uint64 size, const void* seed);
// shared_image (docs/Disaggregated/notes/anland/plan-ahb-dmabuf.md): emits `op` and waits
// for its answer. `sendFd` >= 0 (an Import) is queued on the aux socket for the record's
// seq BEFORE the record is published; `receivedFd` non-null (an Allocate) takes the
// descriptor the server queued before its answer. False = DECLINED (the server's log says
// why) or the descriptor did not travel.
Bool EmitSharedImage(const MG_Pipe::MGPSharedImageOp& op, int sendFd, MG_Pipe::MGPSharedImageReply* reply,
int* receivedFd);
// The client's reference to buffer `handle`'s T0 store ends because the buffer was
// respecified or destroyed: unlock and release it. The server's import holds its own
// reference until its own fence for the store's last use has completed. No-op for a
+35 -16
View File
@@ -10,6 +10,7 @@
#include "../Transport/FdPassing.h"
#include <MG_Pipe/MGPipeTypes.h>
#include <MG_Util/Debug/Log.h>
#include <chrono>
@@ -40,17 +41,31 @@ namespace MobileGL::MG_Remote::Server {
AdoptInbox::Outcome AdoptInbox::Take(Uint64 seq, Uint32 timeoutMs, int* outFd,
Transport::AdoptT0::Offer* outOffer, std::string& why) {
return TakeKind(Kind::T0, seq, timeoutMs, outFd, outOffer, why);
}
AdoptInbox::Outcome AdoptInbox::TakeSharedImageFd(Uint64 seq, Uint32 timeoutMs, int* outFd, std::string& why) {
Transport::AdoptT0::Offer unused{};
return TakeKind(Kind::SharedImage, seq, timeoutMs, outFd, &unused, why);
}
// Both kinds are 32-byte sidebands keyed by the record's seq. A shared-image offer
// (MGPSharedImageFdOffer) is carried in the T0 Offer's storage - only its magic and seq are read.
AdoptInbox::Outcome AdoptInbox::TakeKind(Kind kind, Uint64 seq, Uint32 timeoutMs, int* outFd,
Transport::AdoptT0::Offer* outOffer, std::string& why) {
using Clock = std::chrono::steady_clock;
static_assert(sizeof(MG_Pipe::MGPSharedImageFdOffer) == sizeof(Transport::AdoptT0::Offer),
"the two aux sidebands share one size");
*outFd = -1;
*outOffer = Transport::AdoptT0::Offer{};
if (auto it = m_kept.find(seq); it != m_kept.end()) {
if (auto it = m_kept.find(KeyFor(kind, seq)); it != m_kept.end()) {
*outFd = it->second.fd;
*outOffer = it->second.offer;
m_kept.erase(it);
return Outcome::Taken;
}
if (m_transport == nullptr) {
why = "the session has no control transport to receive the store on";
why = "the session has no control transport to receive the descriptor on";
return Outcome::Closed;
}
const auto deadline = Clock::now() + std::chrono::milliseconds(timeoutMs);
@@ -64,57 +79,61 @@ namespace MobileGL::MG_Remote::Server {
const MobileGLResult received = m_transport->ReceiveFd(
&fd, MobileGLMutableByteSpan{sideband, sizeof(sideband)}, &sidebandSize, left);
if (received == MOBILEGL_ERR_TIMEOUT) {
why = "no store arrived on the aux socket within " + std::to_string(timeoutMs) +
why = "no descriptor arrived on the aux socket within " + std::to_string(timeoutMs) +
" ms of its record";
return Outcome::TimedOut;
}
if (received != MOBILEGL_OK) {
why = "the aux socket closed (rc=" + std::to_string(static_cast<int>(received)) +
") before the store arrived";
") before the descriptor arrived";
return Outcome::Closed;
}
Transport::AdoptT0::Offer offer{};
if (sidebandSize != sizeof(offer)) {
++m_malformed;
MGLOG_W("MG_Remote server: T0 - an aux descriptor with a %llu-byte sideband is not a store "
"offer; closed",
MGLOG_W("MG_Remote server: an aux descriptor with a %llu-byte sideband is neither a T0 store nor a "
"shared image; closed",
static_cast<unsigned long long>(sidebandSize));
CloseFd(fd);
continue;
}
std::memcpy(&offer, sideband, sizeof(offer));
if (offer.magic != Transport::AdoptT0::kOfferMagic ||
offer.version != Transport::AdoptT0::kOfferVersion) {
Kind arrived;
if (offer.magic == Transport::AdoptT0::kOfferMagic && offer.version == Transport::AdoptT0::kOfferVersion) {
arrived = Kind::T0;
} else if (offer.magic == MG_Pipe::kMGPSharedImageFdMagic) {
arrived = Kind::SharedImage;
} else {
++m_malformed;
MGLOG_W("MG_Remote server: T0 - an aux descriptor whose sideband is not an Offer "
"(magic 0x%08x version %u); closed",
MGLOG_W("MG_Remote server: an aux descriptor whose sideband is neither a T0 Offer nor a shared "
"image (magic 0x%08x version %u); closed",
offer.magic, offer.version);
CloseFd(fd);
continue;
}
if (offer.seq == seq) {
if (arrived == kind && offer.seq == seq) {
*outFd = fd;
*outOffer = offer;
return Outcome::Taken;
}
if (offer.seq < seq) {
// Its record already timed out and was declined; the client has released the
// store on that answer, and this late copy of the handle is only a reference.
// Its record already timed out and was declined; this late copy is only a reference.
++m_stale;
MGLOG_W("MG_Remote server: T0 - the store for record %llu arrived after its record was "
MGLOG_W("MG_Remote server: the descriptor for record %llu arrived after its record was "
"answered; closed (stale)",
static_cast<unsigned long long>(offer.seq));
CloseFd(fd);
continue;
}
// For a later record: keep it for that record. Replacing a kept one would leak it.
if (auto it = m_kept.find(offer.seq); it != m_kept.end()) {
const Uint64 key = KeyFor(arrived, offer.seq);
if (auto it = m_kept.find(key); it != m_kept.end()) {
++m_malformed;
CloseFd(it->second.fd);
it->second = KeptOffer{fd, offer};
continue;
}
m_kept.emplace(offer.seq, KeptOffer{fd, offer});
m_kept.emplace(key, KeptOffer{fd, offer});
}
}
+9
View File
@@ -51,6 +51,11 @@ namespace MobileGL::MG_Remote::Server {
Outcome Take(Uint64 seq, Uint32 timeoutMs, int* outFd, Transport::AdoptT0::Offer* outOffer,
std::string& why);
// A shared image's descriptor for record `seq` (an Import; sideband MGPSharedImageFdOffer).
// Same matching and the same bound as Take; the two kinds share the aux socket and are kept
// apart by their sideband's magic.
Outcome TakeSharedImageFd(Uint64 seq, Uint32 timeoutMs, int* outFd, std::string& why);
// Closes every kept descriptor (session teardown).
void Clear();
@@ -59,10 +64,14 @@ namespace MobileGL::MG_Remote::Server {
SizeT Kept() const { return m_kept.size(); }
private:
enum class Kind : Uint8 { T0, SharedImage };
struct KeptOffer {
int fd = -1;
Transport::AdoptT0::Offer offer{};
};
static Uint64 KeyFor(Kind kind, Uint64 seq) { return (seq << 1) | (kind == Kind::SharedImage ? 1u : 0u); }
Outcome TakeKind(Kind kind, Uint64 seq, Uint32 timeoutMs, int* outFd, Transport::AdoptT0::Offer* outOffer,
std::string& why);
Transport::ITransport* m_transport = nullptr;
std::map<Uint64, KeptOffer> m_kept;
Uint64 m_stale = 0;
+106
View File
@@ -31,6 +31,9 @@
#include <MG_Util/Metrics/TextureMetrics.h>
#include <cstdlib>
#if !defined(_WIN32)
#include <unistd.h>
#endif
#include <cstring>
#include <limits>
@@ -1472,6 +1475,109 @@ namespace MobileGL::MG_Remote::Server {
// never created is corruption, not a late record. Token 0 is the release edge and is always
// legal (a session whose client released its context has no context, which is exactly the
// value a session that never bound one already answers).
Bool ServerVerbSink::OnSharedImage(const MG_Pipe::MGPSharedImageOp& op, Uint64 seq,
MG_Pipe::MGPSharedImageReply& reply) {
namespace SI = SharedImages;
ServerSession* session = ServerSession::Active();
if (session == nullptr) return false;
SI::SessionHolder& holder = session->SharedImageHolder();
const auto fill = [&reply](const SI::Image& image) {
reply.ImageId = image.Id;
reply.Modifier = image.Modifier;
reply.Width = image.Width;
reply.Height = image.Height;
reply.Format = image.Fourcc;
reply.Stride = image.Stride;
reply.Offset = image.Offset;
};
switch (op.Op) {
case MG_Pipe::kMGPSharedImageAllocate: {
std::string why;
SI::ImageRef image = SI::Allocate(op.Width, op.Height, op.Format, why);
if (image == nullptr) {
MGLOG_E("MG_Remote server: shared image %ux%u (fourcc 0x%08x) not allocated: %s", op.Width, op.Height,
op.Format, why.c_str());
return false;
}
Transport::ITransport* transport = session->ControlTransport();
// The descriptor goes out BEFORE the reply is posted, so the client finds it queued.
MG_Pipe::MGPSharedImageFdOffer offer{MG_Pipe::kMGPSharedImageFdMagic, 1, seq, image->Id, 0};
const MobileGLResult shared = transport != nullptr
? transport->ShareFd(image->Fd, MobileGLByteSpan{&offer, sizeof(offer)})
: MOBILEGL_ERR_NOT_INITIALIZED;
if (shared != MOBILEGL_OK) {
MGLOG_E("MG_Remote server: shared image %llu could not be sent on the aux socket (rc=%d)",
static_cast<unsigned long long>(image->Id), static_cast<int>(shared));
return false;
}
holder.Hold(image);
fill(*image);
MGLOG_D("MG_Remote server: shared image %llu allocated, %ux%u stride %u",
static_cast<unsigned long long>(image->Id), image->Width, image->Height, image->Stride);
return true;
}
case MG_Pipe::kMGPSharedImageImport: {
int fd = -1;
std::string why;
if (session->T0Inbox().TakeSharedImageFd(seq, Transport::AdoptT0::kOfferWaitMs, &fd, why) !=
AdoptInbox::Outcome::Taken) {
MGLOG_E("MG_Remote server: shared-image import (record %llu) has no descriptor: %s",
static_cast<unsigned long long>(seq), why.c_str());
return false;
}
SI::ImageRef image = SI::Identify(fd, why);
#if !defined(_WIN32)
::close(fd);
#endif
if (image == nullptr) {
MGLOG_W_ONCE("MG_Remote server: a dma-buf import was refused: %s", why.c_str());
return false;
}
if (image->Width != op.Width || image->Height != op.Height || !SI::FourccSupported(op.Format)) {
MGLOG_W("MG_Remote server: a dma-buf import of image %llu (%ux%u) as %ux%u fourcc 0x%08x was refused",
static_cast<unsigned long long>(image->Id), image->Width, image->Height, op.Width, op.Height,
op.Format);
return false;
}
holder.Hold(image);
fill(*image);
return true;
}
case MG_Pipe::kMGPSharedImageRelease:
reply.ImageId = op.ImageId;
return holder.Release(op.ImageId);
case MG_Pipe::kMGPSharedImagePresent: {
SI::ImageRef image = holder.Get(op.ImageId);
MG_Backend::BackendObject* backend = ServerLoopInstance().Backend();
if (image == nullptr || backend == nullptr) return false;
MG_Backend::SharedImageView view;
view.Id = image->Id;
view.Width = image->Width;
view.Height = image->Height;
view.Fourcc = image->Fourcc;
view.NativeBuffer = image->Native;
if (!backend->BlitDefaultFramebufferToSharedImage(view)) return false;
fill(*image);
return true;
}
case MG_Pipe::kMGPSharedImageAttach: {
SI::ImageRef image = holder.Get(op.ImageId);
if (image == nullptr) return false;
if (!MG_Pipe::MGPipeApplyAttachSharedImage(op.Texture, image->Id)) {
MGLOG_E("MG_Remote server: shared image %llu could not be attached to texture {slot=%u, gen=%u}: the "
"texture has no record",
static_cast<unsigned long long>(image->Id), op.Texture.Slot, op.Texture.Gen);
return false;
}
fill(*image);
return true;
}
default:
MGLOG_E("MG_Remote server: shared_image operation %u is not one this server knows", op.Op);
return false;
}
}
Bool ServerVerbSink::OnBindContext(const MG_Pipe::MGPBindContext& bind) {
// NO SESSION IS A DECLINE, NOT A FAULT, and it is the one case here that a peer's bytes
// cannot cause: a bind is answered by the session's own table, so with no accepted
+6
View File
@@ -254,6 +254,12 @@ namespace MobileGL::MG_Remote::Server {
// rather than a silent bind. Token 0 (the release) is always legal.
Bool OnBindContext(const MG_Pipe::MGPBindContext& bind) override;
// shared_image, opcode 85 (docs/Disaggregated/notes/anland/plan-ahb-dmabuf.md): the
// session's image table (allocate / import / release) and the two GL-ordered operations
// (present the default framebuffer into an image, attach an image to a texture).
Bool OnSharedImage(const MG_Pipe::MGPSharedImageOp& op, Uint64 seq,
MG_Pipe::MGPSharedImageReply& reply) override;
// P5c ct's tallies, for the same reason every other row's tally exists (R-16: a probe
// may not arm against a stub). ApplierResets counts the records ACCEPTED (serial
// checked, reset run); ObjectDeaths counts every record the sink dispatched.
@@ -728,6 +728,7 @@ namespace MobileGL::MG_Remote::Server {
m_t0Stores = m_t0Bytes = m_t0Declined = m_t0Refusals = 0;
m_adoptInbox.Clear();
m_adoptInbox.Attach(&transport);
m_sharedImages.Clear();
// P14 S1: the context registry is the session's, so it is emptied with the rest of the
// per-session state this Accept resets. A session that never crossed a bind_context
// leaves the current token at 0, which is the single-context shape's answer.
@@ -1110,6 +1111,7 @@ namespace MobileGL::MG_Remote::Server {
}
m_adoptInbox.Clear();
m_adoptInbox.Attach(nullptr);
m_sharedImages.Clear();
m_adoptT0Asked = m_adoptT0Settled = m_adoptT0 = m_adoptT0Quiet = false;
m_adoptAsk = 2;
m_consumer.Detach();
@@ -58,6 +58,7 @@
#include "../Transport/SessionRings.h"
#include "../Wire/PipeWireCodec.h"
#include "AdoptInbox.h"
#include "SharedImageRegistry.h"
#include "ApplyThreadPolicy.h"
#include "PipeApplier.h"
@@ -331,6 +332,12 @@ namespace MobileGL::MG_Remote::Server {
Bool AdoptT0Quiet() const { return m_adoptT0Quiet; }
AdoptInbox& T0Inbox() { return m_adoptInbox; }
// ---- shared images (docs/Disaggregated/notes/anland/plan-ahb-dmabuf.md) -------------
// The images this session allocated or imported and has not released; emptied when the
// session closes. The control transport is where an allocation's descriptor goes out.
SharedImages::SessionHolder& SharedImageHolder() { return m_sharedImages; }
Transport::ITransport* ControlTransport() const { return m_transport; }
// ---- P14 S1 (docs/Disaggregated/design/11-state-ownership.md): the session's contexts -
//
// THE PLACEHOLDER TABLE. `CreateContext`/`DestroyContext` on the control plane build and
@@ -416,6 +423,7 @@ namespace MobileGL::MG_Remote::Server {
Bool m_adoptT0Settled = false;
Bool m_adoptT0 = false;
AdoptInbox m_adoptInbox;
SharedImages::SessionHolder m_sharedImages;
Uint64 m_t0Stores = 0;
Uint64 m_t0Bytes = 0;
Uint64 m_t0Declined = 0;
@@ -0,0 +1,306 @@
// MobileGL - MobileGL/MG_Remote/Server/SharedImageRegistry.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#include "SharedImageRegistry.h"
#include <MG_Util/Debug/Log.h>
#include <atomic>
#include <cerrno>
#include <cstdio>
#include <cstring>
#include <map>
#include <utility>
#if defined(__ANDROID__)
#include <android/hardware_buffer.h>
#endif
#if !defined(_WIN32)
#include <sys/mman.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <unistd.h>
#endif
namespace MobileGL::MG_Remote::Server::SharedImages {
namespace {
std::string Format(const char* fmt, long long a, long long b = 0, long long c = 0) {
char buffer[256];
std::snprintf(buffer, sizeof(buffer), fmt, a, b, c);
return buffer;
}
struct Registry {
std::mutex mutex;
std::atomic<Uint64> nextId{1};
UnorderedMap<Uint64, std::weak_ptr<const Image>> byId;
std::map<std::pair<Uint64, Uint64>, std::weak_ptr<const Image>> byIdentity;
// Set once two live images were seen with one identity: the kernel shares an inode
// between dma-bufs, so a descriptor can no longer say which image it is.
Bool identityAmbiguous = false;
};
Registry& Reg() {
static Registry* registry = new Registry(); // outlives every holder at process exit
return *registry;
}
#if !defined(_WIN32)
Bool IdentityOf(int fd, Uint64* dev, Uint64* ino) {
struct stat st {};
if (::fstat(fd, &st) != 0) return false;
*dev = static_cast<Uint64>(st.st_dev);
*ino = static_cast<Uint64>(st.st_ino);
return true;
}
#endif
#if defined(__ANDROID__)
// Every descriptor of the buffer's native handle, through the public NDK only: the
// handle is written to a socket pair and read back, and only the SCM_RIGHTS payload is
// looked at - never the flattened bytes, whose layout is the platform's.
Bool NativeHandleFds(const AHardwareBuffer* ahb, Vector<int>& fds, std::string& why) {
int pair[2] = {-1, -1};
if (::socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, pair) != 0) {
why = Format("socketpair errno=%lld", errno);
return false;
}
const int sent = AHardwareBuffer_sendHandleToUnixSocket(ahb, pair[0]);
::close(pair[0]);
if (sent != 0) {
why = Format("AHardwareBuffer_sendHandleToUnixSocket rc=%lld errno=%lld", sent, errno);
::close(pair[1]);
return false;
}
for (;;) {
Uint8 data[4096];
alignas(cmsghdr) Uint8 control[CMSG_SPACE(sizeof(int) * 64)];
iovec iov{data, sizeof(data)};
msghdr msg{};
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
msg.msg_control = control;
msg.msg_controllen = sizeof(control);
const ssize_t got = ::recvmsg(pair[1], &msg, MSG_CMSG_CLOEXEC);
if (got < 0 && errno == EINTR) continue;
if (got <= 0) break;
for (cmsghdr* c = CMSG_FIRSTHDR(&msg); c != nullptr; c = CMSG_NXTHDR(&msg, c)) {
if (c->cmsg_level != SOL_SOCKET || c->cmsg_type != SCM_RIGHTS) continue;
const SizeT n = (c->cmsg_len - CMSG_LEN(0)) / sizeof(int);
const int* in = reinterpret_cast<const int*>(CMSG_DATA(c));
for (SizeT i = 0; i < n; ++i) fds.push_back(in[i]);
}
}
::close(pair[1]);
if (fds.empty()) {
why = "the buffer's native handle carries no descriptor";
return false;
}
return true;
}
Bool AllocateNative(Image& image, std::string& why) {
AHardwareBuffer_Desc desc{};
desc.width = image.Width;
desc.height = image.Height;
desc.layers = 1;
desc.format = image.Fourcc == kFourccXbgr8888 ? AHARDWAREBUFFER_FORMAT_R8G8B8X8_UNORM
: AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM;
desc.usage = AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE | AHARDWAREBUFFER_USAGE_GPU_COLOR_OUTPUT;
AHardwareBuffer* ahb = nullptr;
const int allocated = AHardwareBuffer_allocate(&desc, &ahb);
if (allocated != 0 || ahb == nullptr) {
why = Format("AHardwareBuffer_allocate(%lldx%lld) rc=%lld", image.Width, image.Height, allocated);
return false;
}
AHardwareBuffer_Desc described{};
AHardwareBuffer_describe(ahb, &described);
Vector<int> fds;
if (!NativeHandleFds(ahb, fds, why)) {
AHardwareBuffer_release(ahb);
return false;
}
image.Native = ahb;
image.Fd = fds[0];
for (SizeT i = 1; i < fds.size(); ++i) ::close(fds[i]);
image.Stride = described.stride * 4;
image.Offset = 0;
return true;
}
void ReleaseNative(void* native) {
if (native != nullptr) AHardwareBuffer_release(static_cast<AHardwareBuffer*>(native));
}
#elif !defined(_WIN32)
// A host has no AHardwareBuffer. A memfd stands in for the dma-buf so the registry, the
// descriptor round trip and identification can be exercised by the unit tests; no backend
// binds one.
Bool AllocateNative(Image& image, std::string& why) {
image.Stride = image.Width * 4;
image.Offset = 0;
const int fd = ::memfd_create("mobilegl-shared-image", MFD_CLOEXEC);
if (fd < 0) {
why = Format("memfd_create errno=%lld", errno);
return false;
}
if (::ftruncate(fd, static_cast<off_t>(image.Stride) * image.Height) != 0) {
why = Format("ftruncate errno=%lld", errno);
::close(fd);
return false;
}
image.Fd = fd;
return true;
}
void ReleaseNative(void*) {}
#else
Bool AllocateNative(Image&, std::string& why) {
why = "shared images need POSIX descriptors";
return false;
}
void ReleaseNative(void*) {}
#endif
} // namespace
Image::~Image() {
Registry& reg = Reg();
{
const std::lock_guard<std::mutex> lock(reg.mutex);
// Only this image's own entries: the weak pointer is already expired, but a newer
// image may have taken the identity over (the kernel recycles inodes).
if (auto it = reg.byId.find(Id); it != reg.byId.end() && it->second.expired()) reg.byId.erase(it);
const auto key = std::make_pair(IdentityDev, IdentityIno);
if (auto it = reg.byIdentity.find(key); it != reg.byIdentity.end() && it->second.expired())
reg.byIdentity.erase(it);
}
#if !defined(_WIN32)
if (Fd >= 0) ::close(Fd);
#endif
ReleaseNative(Native);
}
Bool FourccSupported(Uint32 fourcc) { return fourcc == kFourccAbgr8888 || fourcc == kFourccXbgr8888; }
ImageRef Allocate(Uint32 width, Uint32 height, Uint32 fourcc, std::string& why) {
if (width == 0 || height == 0 || width > 16384 || height > 16384) {
why = Format("%lldx%lld is not an image size", width, height);
return nullptr;
}
if (!FourccSupported(fourcc)) {
why = Format("fourcc 0x%08llx is not a shared-image format", fourcc);
return nullptr;
}
Registry& reg = Reg();
auto image = std::make_shared<Image>();
image->Id = reg.nextId.fetch_add(1, std::memory_order_relaxed);
image->Width = width;
image->Height = height;
image->Fourcc = fourcc;
if (!AllocateNative(*image, why)) return nullptr;
#if !defined(_WIN32)
if (!IdentityOf(image->Fd, &image->IdentityDev, &image->IdentityIno)) {
why = Format("fstat on the exported descriptor errno=%lld", errno);
return nullptr;
}
#endif
const std::lock_guard<std::mutex> lock(reg.mutex);
reg.byId[image->Id] = image;
const auto key = std::make_pair(image->IdentityDev, image->IdentityIno);
if (auto it = reg.byIdentity.find(key); it != reg.byIdentity.end() && !it->second.expired()) {
if (!reg.identityAmbiguous) {
MGLOG_E("MG_Remote server: two live shared images have one dma-buf identity (dev %llu ino %llu); "
"this kernel does not keep dma-buf inodes unique, so a returned descriptor cannot name its "
"image and every shared-image import is refused from here on",
static_cast<unsigned long long>(key.first), static_cast<unsigned long long>(key.second));
}
reg.identityAmbiguous = true;
}
reg.byIdentity[key] = image;
return image;
}
ImageRef Identify(int fd, std::string& why) {
#if defined(_WIN32)
(void)fd;
why = "shared images need POSIX descriptors";
return nullptr;
#else
Uint64 dev = 0, ino = 0;
if (!IdentityOf(fd, &dev, &ino)) {
why = Format("fstat on the imported descriptor errno=%lld", errno);
return nullptr;
}
Registry& reg = Reg();
const std::lock_guard<std::mutex> lock(reg.mutex);
if (reg.identityAmbiguous) {
why = "dma-buf identities are not unique on this kernel";
return nullptr;
}
const auto it = reg.byIdentity.find(std::make_pair(dev, ino));
ImageRef image = it != reg.byIdentity.end() ? it->second.lock() : nullptr;
if (image == nullptr) {
why = Format("the descriptor (dev %llu ino %llu) was not exported by this server", static_cast<long long>(dev),
static_cast<long long>(ino));
}
return image;
#endif
}
ImageRef Find(Uint64 id) {
Registry& reg = Reg();
const std::lock_guard<std::mutex> lock(reg.mutex);
const auto it = reg.byId.find(id);
return it != reg.byId.end() ? it->second.lock() : nullptr;
}
SizeT LiveCount() {
Registry& reg = Reg();
const std::lock_guard<std::mutex> lock(reg.mutex);
SizeT live = 0;
for (const auto& [id, weak] : reg.byId) live += weak.expired() ? 0 : 1;
return live;
}
void SessionHolder::Hold(const ImageRef& image) {
if (image == nullptr) return;
const std::lock_guard<std::mutex> lock(m_mutex);
m_images[image->Id] = image;
}
Bool SessionHolder::Release(Uint64 id) {
ImageRef dropped;
const std::lock_guard<std::mutex> lock(m_mutex);
const auto it = m_images.find(id);
if (it == m_images.end()) return false;
dropped = std::move(it->second); // destroyed after the unlock: ~Image takes the registry lock
m_images.erase(it);
return true;
}
ImageRef SessionHolder::Get(Uint64 id) const {
const std::lock_guard<std::mutex> lock(m_mutex);
const auto it = m_images.find(id);
return it != m_images.end() ? it->second : nullptr;
}
void SessionHolder::Clear() {
UnorderedMap<Uint64, ImageRef> dropped;
{
const std::lock_guard<std::mutex> lock(m_mutex);
dropped.swap(m_images);
}
}
SizeT SessionHolder::Count() const {
const std::lock_guard<std::mutex> lock(m_mutex);
return m_images.size();
}
} // namespace MobileGL::MG_Remote::Server::SharedImages
@@ -0,0 +1,98 @@
// MobileGL - MobileGL/MG_Remote/Server/SharedImageRegistry.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// SHARED IMAGES: colour buffers the server allocates and hands to clients as dma-buf descriptors,
// so a frame one client renders can be sampled by another (a Wayland window by its compositor)
// without leaving GPU memory (docs/Disaggregated/notes/anland/plan-ahb-dmabuf.md).
//
// THE SERVER IS THE ALLOCATOR AND THE ONLY IMPORTER. An image is an AHardwareBuffer; what leaves
// the process is a duplicate of its first native-handle descriptor, which on every Android kernel
// with dma-buf heaps is a dma-buf. A descriptor that comes BACK (a compositor importing a client's
// wl_buffer) is never imported as raw memory - its layout is the allocator's business - it is
// IDENTIFIED: the kernel gives each dma-buf its own inode, so (st_dev, st_ino) names the image it
// was exported from, and the backends bind that AHardwareBuffer through the Android import paths.
// Nothing here reads a vendor's handle layout or assumes a memory layout.
//
// LIFETIME. The registry holds weak references only. Whoever still needs an image holds an
// ImageRef: the session that allocated or imported it (until its release or the session's end)
// and a backend object built on it (an EGLImage, a VkImage) for as long as that object lives.
#pragma once
#include <Includes.h>
#include <mutex>
#include <string>
namespace MobileGL::MG_Remote::Server::SharedImages {
// DRM fourcc codes (drm_fourcc.h). Little-endian packed: ABGR8888 is R, G, B, A in memory,
// which is GL's RGBA8 and AHardwareBuffer's R8G8B8A8.
inline constexpr Uint32 FourCC(char a, char b, char c, char d) {
return static_cast<Uint32>(static_cast<Uint8>(a)) | (static_cast<Uint32>(static_cast<Uint8>(b)) << 8) |
(static_cast<Uint32>(static_cast<Uint8>(c)) << 16) | (static_cast<Uint32>(static_cast<Uint8>(d)) << 24);
}
inline constexpr Uint32 kFourccAbgr8888 = FourCC('A', 'B', '2', '4');
inline constexpr Uint32 kFourccXbgr8888 = FourCC('X', 'B', '2', '4');
// "The layout is the allocator's": what an AHardwareBuffer is to anybody but its allocator.
inline constexpr Uint64 kModifierInvalid = 0x00ffffffffffffffull;
inline constexpr Uint64 kModifierLinear = 0;
// Whether images of this fourcc can be allocated (and so imported) at all.
Bool FourccSupported(Uint32 fourcc);
struct Image {
Uint64 Id = 0;
Uint32 Width = 0;
Uint32 Height = 0;
Uint32 Fourcc = 0;
Uint32 Stride = 0; // bytes
Uint32 Offset = 0;
Uint64 Modifier = kModifierInvalid;
// AHardwareBuffer* on Android; null on a host, where the memory is a memfd (unit tests).
void* Native = nullptr;
// The descriptor clients receive duplicates of. Owned.
int Fd = -1;
Uint64 IdentityDev = 0;
Uint64 IdentityIno = 0;
Image() = default;
Image(const Image&) = delete;
Image& operator=(const Image&) = delete;
~Image();
};
using ImageRef = SharedPtr<const Image>;
// A new image of `width` x `height` `fourcc`, registered. Null with `why` on failure.
ImageRef Allocate(Uint32 width, Uint32 height, Uint32 fourcc, std::string& why);
// The live image `fd` was exported from, or null (`why` says which test failed): a descriptor
// this server never exported, or one whose identity the kernel does not keep unique.
ImageRef Identify(int fd, std::string& why);
// The live image with this id, or null.
ImageRef Find(Uint64 id);
// Live images, for tests and diagnostics.
SizeT LiveCount();
// One session's references: what it allocated or imported and has not released yet. Dropping
// the holder (the session ending) drops every one of them.
class SessionHolder {
public:
void Hold(const ImageRef& image);
Bool Release(Uint64 id);
ImageRef Get(Uint64 id) const;
void Clear();
SizeT Count() const;
private:
mutable std::mutex m_mutex;
UnorderedMap<Uint64, ImageRef> m_images;
};
} // namespace MobileGL::MG_Remote::Server::SharedImages
+16 -1
View File
@@ -264,7 +264,8 @@ namespace MobileGL::MG_Remote::Wire {
X(DeleteStreamOutput, MGPStreamOutputBind) \
X(ReadPixelsToBuffer, MGPReadbackToBuffer) \
X(GetTextureImageToBuffer, MGPReadbackToBuffer) \
X(BindContext, MGPBindContext)
X(BindContext, MGPBindContext) \
X(SharedImage, MGPSharedImageOp)
namespace {
@@ -2565,6 +2566,20 @@ namespace MobileGL::MG_Remote::Wire {
return m_verbs != nullptr &&
m_verbs->OnBindContext(*static_cast<const MGPBindContext*>(payload));
// ---- shared images (docs/Disaggregated/notes/anland/plan-ahb-dmabuf.md), opcode 85.
//
// A fixed-size POD; every operation is answered, and a refusal is an answer (DECLINED,
// the sink logs why) rather than a fault: an import of a descriptor this server never
// exported is something a compositor is allowed to try.
case MGPWireOp::SharedImage: {
MGPSharedImageReply result{};
const Bool ok = m_verbs != nullptr &&
m_verbs->OnSharedImage(*static_cast<const MGPSharedImageOp*>(payload), seq, result);
PostReply(op, seq, ok ? ReplySink::kStatusOk : ReplySink::kStatusDeclined, ok ? &result : nullptr,
ok ? sizeof(result) : 0);
return ok;
}
// ---- P5c rv (CONTRACT-P5C.md §5.3): the residual-value record, opcode 79 -------------
//
// An ordinary set_* row, unlike the two control records beside it: a fixed-width POD
+11
View File
@@ -687,6 +687,17 @@ namespace MobileGL::MG_Remote::Wire {
(void)bind;
return false;
}
// ---- shared images, opcode 85 (docs/Disaggregated/notes/anland/plan-ahb-dmabuf.md).
// `seq` is the record's reply-slot id, which an Import's descriptor names on the aux
// socket. True fills `reply`; false is a DECLINED answer.
virtual Bool OnSharedImage(const MG_Pipe::MGPSharedImageOp& op, Uint64 seq,
MG_Pipe::MGPSharedImageReply& reply) {
(void)op;
(void)seq;
(void)reply;
return false;
}
};
// Not thread safe: one decoder on the apply thread, by construction.