diff --git a/CMakeLists.txt b/CMakeLists.txt index 358d7046..9869d457 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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). diff --git a/MobileGL/MG_Backend/BackendObject.h b/MobileGL/MG_Backend/BackendObject.h index 3db8f941..4d2339e3 100644 --- a/MobileGL/MG_Backend/BackendObject.h +++ b/MobileGL/MG_Backend/BackendObject.h @@ -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; diff --git a/MobileGL/MG_Pipe/MGPipeTypes.h b/MobileGL/MG_Pipe/MGPipeTypes.h index 1ad056f3..7de5801f 100644 --- a/MobileGL/MG_Pipe/MGPipeTypes.h +++ b/MobileGL/MG_Pipe/MGPipeTypes.h @@ -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 - diff --git a/MobileGL/MG_Pipe/PipeApply.cpp b/MobileGL/MG_Pipe/PipeApply.cpp index 24cb8723..7929da73 100644 --- a/MobileGL/MG_Pipe/PipeApply.cpp +++ b/MobileGL/MG_Pipe/PipeApply.cpp @@ -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* 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(MGPipeResourceTarget::TexBuffer)) { // Scope and BufOffset/BufSize are transient respecify carriers, not resource state. diff --git a/MobileGL/MG_Pipe/PipeApply.h b/MobileGL/MG_Pipe/PipeApply.h index 49d60087..71e52c2b 100644 --- a/MobileGL/MG_Pipe/PipeApply.h +++ b/MobileGL/MG_Pipe/PipeApply.h @@ -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 diff --git a/MobileGL/MG_Pipe/PipeCalls.def b/MobileGL/MG_Pipe/PipeCalls.def index 8b253634..2b33c513 100644 --- a/MobileGL/MG_Pipe/PipeCalls.def +++ b/MobileGL/MG_Pipe/PipeCalls.def @@ -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"): diff --git a/MobileGL/MG_Pipe/PipeFields.def b/MobileGL/MG_Pipe/PipeFields.def index 9504a43a..b4a513b4 100644 --- a/MobileGL/MG_Pipe/PipeFields.def +++ b/MobileGL/MG_Pipe/PipeFields.def @@ -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) \ diff --git a/MobileGL/MG_Pipe/generated/PipeTables.inc b/MobileGL/MG_Pipe/generated/PipeTables.inc index c5872d57..248e7f91 100644 --- a/MobileGL/MG_Pipe/generated/PipeTables.inc +++ b/MobileGL/MG_Pipe/generated/PipeTables.inc @@ -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. diff --git a/MobileGL/MG_Pipe/generated/PipeThunks.inc b/MobileGL/MG_Pipe/generated/PipeThunks.inc index 535d82b9..f1f5fb65 100644 --- a/MobileGL/MG_Pipe/generated/PipeThunks.inc +++ b/MobileGL/MG_Pipe/generated/PipeThunks.inc @@ -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); +} diff --git a/MobileGL/MG_Pipe/generated/PipeVerify.inc b/MobileGL/MG_Pipe/generated/PipeVerify.inc index 4f5a0f3f..e386af22 100644 --- a/MobileGL/MG_Pipe/generated/PipeVerify.inc +++ b/MobileGL/MG_Pipe/generated/PipeVerify.inc @@ -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 : std::true_type {}; template <> struct MGPipeHasFieldVerifier : std::true_type {}; template <> +struct MGPipeHasFieldVerifier : std::true_type {}; +template <> struct MGPipeHasFieldVerifier : std::true_type {}; template <> struct MGPipeHasFieldVerifier : 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; diff --git a/MobileGL/MG_Pipe/generated/PipeWire.inc b/MobileGL/MG_Pipe/generated/PipeWire.inc index 9b811d32..ddf80dcf 100644 --- a/MobileGL/MG_Pipe/generated/PipeWire.inc +++ b/MobileGL/MG_Pipe/generated/PipeWire.inc @@ -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(MGPWireOp::kOpCount) /* 82 ReadPixelsToBuffer */ static_cast(kNone), /* 83 GetTextureImageToBuffer */ static_cast(kNone), /* 84 BindContext */ static_cast(kNone), + /* 85 SharedImage */ static_cast(kReplySlot), }; static_assert(sizeof(kMGPipeCallFlags) / sizeof(kMGPipeCallFlags[0]) == static_cast(MGPWireOp::kOpCount), @@ -364,6 +366,7 @@ inline constexpr Uint8 kMGPipeCallClasses[static_cast(MGPWireOp::kOpCount /* 82 ReadPixelsToBuffer */ static_cast(kCtxVerb), /* 83 GetTextureImageToBuffer */ static_cast(kCtxObject), /* 84 BindContext */ static_cast(kScreen), + /* 85 SharedImage */ static_cast(kScreen), }; static_assert(sizeof(kMGPipeCallClasses) / sizeof(kMGPipeCallClasses[0]) == static_cast(MGPWireOp::kOpCount), @@ -472,6 +475,7 @@ inline constexpr Uint8 kMGPipeWaitClasses[static_cast(MGPWireOp::kOpCount /* 82 ReadPixelsToBuffer */ static_cast(kWaitNone), /* 83 GetTextureImageToBuffer */ static_cast(kWaitNone), /* 84 BindContext */ static_cast(kWaitNone), + /* 85 SharedImage */ static_cast(kWaitReply), }; static_assert(sizeof(kMGPipeWaitClasses) / sizeof(kMGPipeWaitClasses[0]) == static_cast(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(size), static_cast(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: diff --git a/MobileGL/MG_Pipe/generated/PipeWireLayout.inc b/MobileGL/MG_Pipe/generated/PipeWireLayout.inc index dc6fed1d..459abcf7 100644 --- a/MobileGL/MG_Pipe/generated/PipeWireLayout.inc +++ b/MobileGL/MG_Pipe/generated/PipeWireLayout.inc @@ -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(MGPWireOp::ReadPixelsToBuffer), static_cast(kNone), static_cast(kWaitNone)}, {"GetTextureImageToBuffer", "MGPReadbackToBuffer", static_cast(MGPWireOp::GetTextureImageToBuffer), static_cast(kNone), static_cast(kWaitNone)}, {"BindContext", "MGPBindContext", static_cast(MGPWireOp::BindContext), static_cast(kNone), static_cast(kWaitNone)}, + {"SharedImage", "MGPSharedImageOp", static_cast(MGPWireOp::SharedImage), static_cast(kReplySlot), static_cast(kWaitReply)}, }; diff --git a/MobileGL/MG_Remote/Client/BackendObject_Remote.cpp b/MobileGL/MG_Remote/Client/BackendObject_Remote.cpp index 48861f15..2b050bc7 100644 --- a/MobileGL/MG_Remote/Client/BackendObject_Remote.cpp +++ b/MobileGL/MG_Remote/Client/BackendObject_Remote.cpp @@ -16,6 +16,7 @@ #include "../Server/ServerLoop.h" +#include #include #include @@ -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 diff --git a/MobileGL/MG_Remote/Client/BackendObject_Remote.h b/MobileGL/MG_Remote/Client/BackendObject_Remote.h index 1af3d405..b195f2ff 100644 --- a/MobileGL/MG_Remote/Client/BackendObject_Remote.h +++ b/MobileGL/MG_Remote/Client/BackendObject_Remote.h @@ -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. diff --git a/MobileGL/MG_Remote/Client/ClientSession.cpp b/MobileGL/MG_Remote/Client/ClientSession.cpp index 76ba841c..6685d53f 100644 --- a/MobileGL/MG_Remote/Client/ClientSession.cpp +++ b/MobileGL/MG_Remote/Client/ClientSession.cpp @@ -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(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(answer.ImageId), static_cast(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(offer.Seq), static_cast(seq)); + ::close(fd); + } +#endif + } + void ClientSession::ReleaseT0Store(MG_Pipe::MGPipeHandle handle) { auto it = m_t0Stores.find(T0Key(handle)); if (it == m_t0Stores.end()) return; diff --git a/MobileGL/MG_Remote/Client/ClientSession.h b/MobileGL/MG_Remote/Client/ClientSession.h index 0670d491..8cf206e7 100644 --- a/MobileGL/MG_Remote/Client/ClientSession.h +++ b/MobileGL/MG_Remote/Client/ClientSession.h @@ -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 diff --git a/MobileGL/MG_Remote/Server/AdoptInbox.cpp b/MobileGL/MG_Remote/Server/AdoptInbox.cpp index cca4f93d..65838c27 100644 --- a/MobileGL/MG_Remote/Server/AdoptInbox.cpp +++ b/MobileGL/MG_Remote/Server/AdoptInbox.cpp @@ -10,6 +10,7 @@ #include "../Transport/FdPassing.h" +#include #include #include @@ -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(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(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(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}); } } diff --git a/MobileGL/MG_Remote/Server/AdoptInbox.h b/MobileGL/MG_Remote/Server/AdoptInbox.h index ea5ab141..7d10cd3d 100644 --- a/MobileGL/MG_Remote/Server/AdoptInbox.h +++ b/MobileGL/MG_Remote/Server/AdoptInbox.h @@ -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 m_kept; Uint64 m_stale = 0; diff --git a/MobileGL/MG_Remote/Server/PipeApplier.cpp b/MobileGL/MG_Remote/Server/PipeApplier.cpp index 20bfe2dc..c987c544 100644 --- a/MobileGL/MG_Remote/Server/PipeApplier.cpp +++ b/MobileGL/MG_Remote/Server/PipeApplier.cpp @@ -31,6 +31,9 @@ #include #include +#if !defined(_WIN32) +#include +#endif #include #include @@ -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(image->Id), static_cast(shared)); + return false; + } + holder.Hold(image); + fill(*image); + MGLOG_D("MG_Remote server: shared image %llu allocated, %ux%u stride %u", + static_cast(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(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(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(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 diff --git a/MobileGL/MG_Remote/Server/PipeApplier.h b/MobileGL/MG_Remote/Server/PipeApplier.h index f5e8e806..07564454 100644 --- a/MobileGL/MG_Remote/Server/PipeApplier.h +++ b/MobileGL/MG_Remote/Server/PipeApplier.h @@ -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. diff --git a/MobileGL/MG_Remote/Server/ServerSession.cpp b/MobileGL/MG_Remote/Server/ServerSession.cpp index b207a070..c2a4e00b 100644 --- a/MobileGL/MG_Remote/Server/ServerSession.cpp +++ b/MobileGL/MG_Remote/Server/ServerSession.cpp @@ -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(); diff --git a/MobileGL/MG_Remote/Server/ServerSession.h b/MobileGL/MG_Remote/Server/ServerSession.h index 11cbb285..d88de264 100644 --- a/MobileGL/MG_Remote/Server/ServerSession.h +++ b/MobileGL/MG_Remote/Server/ServerSession.h @@ -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; diff --git a/MobileGL/MG_Remote/Server/SharedImageRegistry.cpp b/MobileGL/MG_Remote/Server/SharedImageRegistry.cpp new file mode 100644 index 00000000..c0efa832 --- /dev/null +++ b/MobileGL/MG_Remote/Server/SharedImageRegistry.cpp @@ -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 + +#include +#include +#include +#include +#include +#include + +#if defined(__ANDROID__) +#include +#endif +#if !defined(_WIN32) +#include +#include +#include +#include +#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 nextId{1}; + UnorderedMap> byId; + std::map, std::weak_ptr> 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(st.st_dev); + *ino = static_cast(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& 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(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 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(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(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 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->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 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(key.first), static_cast(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 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(dev), + static_cast(ino)); + } + return image; +#endif + } + + ImageRef Find(Uint64 id) { + Registry& reg = Reg(); + const std::lock_guard 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 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 lock(m_mutex); + m_images[image->Id] = image; + } + + Bool SessionHolder::Release(Uint64 id) { + ImageRef dropped; + const std::lock_guard 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 lock(m_mutex); + const auto it = m_images.find(id); + return it != m_images.end() ? it->second : nullptr; + } + + void SessionHolder::Clear() { + UnorderedMap dropped; + { + const std::lock_guard lock(m_mutex); + dropped.swap(m_images); + } + } + + SizeT SessionHolder::Count() const { + const std::lock_guard lock(m_mutex); + return m_images.size(); + } + +} // namespace MobileGL::MG_Remote::Server::SharedImages diff --git a/MobileGL/MG_Remote/Server/SharedImageRegistry.h b/MobileGL/MG_Remote/Server/SharedImageRegistry.h new file mode 100644 index 00000000..b69949ce --- /dev/null +++ b/MobileGL/MG_Remote/Server/SharedImageRegistry.h @@ -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 + +#include +#include + +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(static_cast(a)) | (static_cast(static_cast(b)) << 8) | + (static_cast(static_cast(c)) << 16) | (static_cast(static_cast(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; + + // 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 m_images; + }; + +} // namespace MobileGL::MG_Remote::Server::SharedImages diff --git a/MobileGL/MG_Remote/Wire/PipeWireCodec.cpp b/MobileGL/MG_Remote/Wire/PipeWireCodec.cpp index 5d3463c1..53d16800 100644 --- a/MobileGL/MG_Remote/Wire/PipeWireCodec.cpp +++ b/MobileGL/MG_Remote/Wire/PipeWireCodec.cpp @@ -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(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(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 diff --git a/MobileGL/MG_Remote/Wire/PipeWireCodec.h b/MobileGL/MG_Remote/Wire/PipeWireCodec.h index 8eca0bfd..7cff8439 100644 --- a/MobileGL/MG_Remote/Wire/PipeWireCodec.h +++ b/MobileGL/MG_Remote/Wire/PipeWireCodec.h @@ -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.