From b5cab4410012410fce6a0f73278bc184c8e7bdb5 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Fri, 2 Oct 2026 09:40:12 -0400 Subject: [PATCH] [Feature] (SharedImage): sync_file write/read fences per image, so sessions order their access without CPU waits --- MobileGL/MG_Backend/BackendObject.h | 11 +- .../MG_Remote/Server/SharedImageRegistry.cpp | 121 ++++++++++++++++++ .../MG_Remote/Server/SharedImageRegistry.h | 52 ++++++++ MobileGL/MG_Test/Wire/SharedImageTest.cpp | 93 ++++++++++++++ 4 files changed, 274 insertions(+), 3 deletions(-) diff --git a/MobileGL/MG_Backend/BackendObject.h b/MobileGL/MG_Backend/BackendObject.h index 43dec21a..d8eaa176 100644 --- a/MobileGL/MG_Backend/BackendObject.h +++ b/MobileGL/MG_Backend/BackendObject.h @@ -620,9 +620,14 @@ namespace MobileGL { // 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). + // differ. The image is ORDERED THROUGH ITS SYNC STATE (SharedImageRegistry.h): the copy + // waits for the image's pending read fences before it writes, and its own completion + // is published with SharedImages::PublishWrite as a sync_file - the call returns once + // the copy is submitted, never waiting for it on the CPU (a backend with no fence export + // waits and publishes -1). A reader binding the image waits for that fence on its GPU + // when the write generation moved, and at its frame boundary publishes its frame's fence + // with PublishRead / ReadTracker. The application's GL state is untouched. False = not + // supported / failed (logged). virtual Bool BlitDefaultFramebufferToSharedImage(const SharedImageView& image) { (void)image; return false; diff --git a/MobileGL/MG_Remote/Server/SharedImageRegistry.cpp b/MobileGL/MG_Remote/Server/SharedImageRegistry.cpp index fb458c48..d6010906 100644 --- a/MobileGL/MG_Remote/Server/SharedImageRegistry.cpp +++ b/MobileGL/MG_Remote/Server/SharedImageRegistry.cpp @@ -21,6 +21,8 @@ #include #endif #if !defined(_WIN32) +#include +#include #include #include #include @@ -182,6 +184,8 @@ namespace MobileGL::MG_Remote::Server::SharedImages { } #if !defined(_WIN32) if (Fd >= 0) ::close(Fd); + if (WriteFence >= 0) ::close(WriteFence); + for (const int fence : ReadFences) ::close(fence); #endif ReleaseNative(Native); } @@ -273,6 +277,123 @@ namespace MobileGL::MG_Remote::Server::SharedImages { return live; } + // ---- synchronisation ------------------------------------------------------------------------ + + namespace SyncFile { + Bool Signaled(int fd) { return Wait(fd, 0); } + + Bool Wait(int fd, Uint32 timeoutMs) { +#if defined(_WIN32) + (void)fd, (void)timeoutMs; + return true; +#else + if (fd < 0) return true; + pollfd p{fd, POLLIN, 0}; + for (;;) { + const int ready = ::poll(&p, 1, static_cast(timeoutMs)); + if (ready > 0) return true; + if (ready == 0) return false; + if (errno != EINTR) return true; // not a fence one can wait on: nothing to order + } +#endif + } + } // namespace SyncFile + + namespace { + void CloseFd(int fd) { +#if !defined(_WIN32) + if (fd >= 0) ::close(fd); +#endif + } + + int Dup(int fd) { +#if defined(_WIN32) + (void)fd; + return -1; +#else + return fd >= 0 ? ::fcntl(fd, F_DUPFD_CLOEXEC, 0) : -1; +#endif + } + + // Signaled read fences are history; a reader that keeps sampling a still image publishes + // one per frame, so the list is pruned whenever it is touched and bounded besides. + constexpr SizeT kMaxReadFences = 8; + void PruneReads(const Image& image) { + auto& fences = image.ReadFences; + for (SizeT i = 0; i < fences.size();) { + if (SyncFile::Signaled(fences[i])) { + CloseFd(fences[i]); + fences[i] = fences.back(); + fences.pop_back(); + } else { + ++i; + } + } + } + } // namespace + + void PublishWrite(const Image& image, int fence) { + const std::lock_guard lock(image.SyncMutex); + CloseFd(image.WriteFence); + image.WriteFence = fence; + ++image.WriteGeneration; + } + + int DupWriteFence(const Image& image, Uint64* generation) { + const std::lock_guard lock(image.SyncMutex); + if (generation != nullptr) *generation = image.WriteGeneration; + if (image.WriteFence >= 0 && SyncFile::Signaled(image.WriteFence)) { + CloseFd(image.WriteFence); + image.WriteFence = -1; + } + return Dup(image.WriteFence); + } + + void PublishRead(const Image& image, int fence) { + if (fence < 0) return; + const std::lock_guard lock(image.SyncMutex); + PruneReads(image); + if (image.ReadFences.size() >= kMaxReadFences) { + // A reader whose frames never complete: the oldest is waited out rather than dropped, + // since dropping it would let a writer overwrite what that frame still reads. + SyncFile::Wait(image.ReadFences.front(), 1000); + CloseFd(image.ReadFences.front()); + image.ReadFences.erase(image.ReadFences.begin()); + } + image.ReadFences.push_back(fence); + } + + Vector DupPendingReadFences(const Image& image) { + const std::lock_guard lock(image.SyncMutex); + PruneReads(image); + Vector out; + out.reserve(image.ReadFences.size()); + for (const int fence : image.ReadFences) { + const int copy = Dup(fence); + if (copy >= 0) out.push_back(copy); + } + return out; + } + + Bool WaitForReads(const Image& image, Uint32 timeoutMs) { + Bool all = true; + for (const int fence : DupPendingReadFences(image)) { + all = SyncFile::Wait(fence, timeoutMs) && all; + CloseFd(fence); + } + return all; + } + + void ReadTracker::NoteRead(const ImageRef& image) { + if (image != nullptr) m_images.emplace(image->Id, image); + } + + void ReadTracker::PublishFrame(int fence) { + for (const auto& [id, image] : m_images) PublishRead(*image, Dup(fence)); + CloseFd(fence); + m_images.clear(); + } + void SessionHolder::Hold(const ImageRef& image) { if (image == nullptr) return; const std::lock_guard lock(m_mutex); diff --git a/MobileGL/MG_Remote/Server/SharedImageRegistry.h b/MobileGL/MG_Remote/Server/SharedImageRegistry.h index ac950ed0..dfd28c04 100644 --- a/MobileGL/MG_Remote/Server/SharedImageRegistry.h +++ b/MobileGL/MG_Remote/Server/SharedImageRegistry.h @@ -68,6 +68,15 @@ namespace MobileGL::MG_Remote::Server::SharedImages { Uint64 IdentityDev = 0; Uint64 IdentityIno = 0; + // SYNCHRONISATION, as sync_file descriptors (-1 = nothing pending). An image has one + // writer at a time (the client session presenting into it) and any number of readers (a + // compositor session sampling it). Guarded by SyncMutex; reached only through the + // functions below, which is why these are mutable on an otherwise immutable record. + mutable std::mutex SyncMutex; + mutable int WriteFence = -1; + mutable Uint64 WriteGeneration = 0; + mutable Vector ReadFences; + Image() = default; Image(const Image&) = delete; Image& operator=(const Image&) = delete; @@ -88,6 +97,49 @@ namespace MobileGL::MG_Remote::Server::SharedImages { // Live images, for tests and diagnostics. SizeT LiveCount(); + // ---- synchronisation (docs/Disaggregated/notes/anland/plan-ahb-dmabuf.md) --------------- + // + // THE SERVER ORDERS THE IMAGE BETWEEN ITS SESSIONS, the way a kernel's implicit sync would: + // a writer publishes the fence of its write and moves on; a reader that finds the generation + // moved waits for that fence on its GPU before it samples; at the end of each frame a reader + // publishes that frame's fence, and the next writer waits for it before it overwrites what + // is still being read. Every fence is a sync_file, so no two backends need to agree on more. + + // The writer's fence: the copy just recorded into `image`. Takes ownership of `fence` (-1: + // the write has already completed). Moves the write generation. + void PublishWrite(const Image& image, int fence); + // The current write generation and a duplicate of its still-pending fence (-1 when there is + // none or it has signaled). The caller closes the descriptor. + int DupWriteFence(const Image& image, Uint64* generation); + // A reader's fence: a frame that sampled `image`. Takes ownership of `fence`. + void PublishRead(const Image& image, int fence); + // Duplicates of the read fences still pending; signaled ones are dropped on the way. The + // caller closes each. + Vector DupPendingReadFences(const Image& image); + // CPU waits, for a backend that cannot wait on its GPU. False on timeout. + Bool WaitForReads(const Image& image, Uint32 timeoutMs); + + namespace SyncFile { + // Whether `fd` (a sync_file) has signaled; an invalid descriptor counts as signaled. + Bool Signaled(int fd); + // Waits for it. False on timeout. + Bool Wait(int fd, Uint32 timeoutMs); + } // namespace SyncFile + + // One reader's frame: the images it sampled since its last frame boundary. A compositor + // session notes each image as it binds it and, at its present, publishes the frame's fence + // to all of them at once. + class ReadTracker { + public: + void NoteRead(const ImageRef& image); + // Takes ownership of `fence` (a sync_file for the frame, -1 if it has completed). + void PublishFrame(int fence); + SizeT Pending() const { return m_images.size(); } + + private: + UnorderedMap m_images; + }; + // 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 { diff --git a/MobileGL/MG_Test/Wire/SharedImageTest.cpp b/MobileGL/MG_Test/Wire/SharedImageTest.cpp index ca18318b..ce89b9e4 100644 --- a/MobileGL/MG_Test/Wire/SharedImageTest.cpp +++ b/MobileGL/MG_Test/Wire/SharedImageTest.cpp @@ -21,6 +21,7 @@ #include #include +#include #include #include @@ -162,3 +163,95 @@ TEST(SharedImage, TheInboxKeepsSharedImageAndT0DescriptorsApart) { // A shared-image record whose descriptor never comes is a bounded wait, not a hang. EXPECT_EQ(inbox.TakeSharedImageFd(7, 20, &fd, why), Server::AdoptInbox::Outcome::TimedOut); } + +// ---- synchronisation. A pipe stands in for a sync_file: its read end polls readable (signaled) +// once something is written to it, which is all the registry asks of a fence. +namespace { + struct FakeFence { + int read = -1; + int write = -1; + FakeFence() { + int p[2]; + EXPECT_EQ(::pipe(p), 0); + read = p[0]; + write = p[1]; + } + ~FakeFence() { + if (read >= 0) ::close(read); + if (write >= 0) ::close(write); + } + int Take() { return std::exchange(read, -1); } // hands the read end over + int Dup() const { return ::dup(read); } + void Signal() { ASSERT_EQ(::write(write, "x", 1), 1); } + }; +} // namespace + +TEST(SharedImageSync, AWriteMovesTheGenerationAndItsFenceIsHandedOutUntilItSignals) { + std::string why; + SI::ImageRef image = SI::Allocate(8, 8, SI::kFourccAbgr8888, why); + ASSERT_NE(image, nullptr) << why; + Uint64 generation = 99; + EXPECT_EQ(SI::DupWriteFence(*image, &generation), -1); + EXPECT_EQ(generation, 0u); + + FakeFence fence; + const int pending = fence.Dup(); + SI::PublishWrite(*image, pending); + const int copy = SI::DupWriteFence(*image, &generation); + EXPECT_EQ(generation, 1u); + ASSERT_GE(copy, 0); + EXPECT_FALSE(SI::SyncFile::Signaled(copy)); + fence.Signal(); + EXPECT_TRUE(SI::SyncFile::Wait(copy, 1000)); + ::close(copy); + // Signaled: nothing is left to wait for. + EXPECT_EQ(SI::DupWriteFence(*image, &generation), -1); + // A write that had already completed still moves the generation. + SI::PublishWrite(*image, -1); + EXPECT_EQ(SI::DupWriteFence(*image, &generation), -1); + EXPECT_EQ(generation, 2u); +} + +TEST(SharedImageSync, PendingReadsAreWaitedForAndSignaledOnesAreDropped) { + std::string why; + SI::ImageRef image = SI::Allocate(8, 8, SI::kFourccAbgr8888, why); + ASSERT_NE(image, nullptr) << why; + FakeFence first, second; + SI::PublishRead(*image, first.Dup()); + SI::PublishRead(*image, second.Dup()); + Vector pending = SI::DupPendingReadFences(*image); + EXPECT_EQ(pending.size(), 2u); + for (const int fd : pending) ::close(fd); + EXPECT_FALSE(SI::WaitForReads(*image, 10)); + + first.Signal(); + pending = SI::DupPendingReadFences(*image); + EXPECT_EQ(pending.size(), 1u); + for (const int fd : pending) ::close(fd); + second.Signal(); + EXPECT_TRUE(SI::WaitForReads(*image, 10)); + EXPECT_TRUE(SI::DupPendingReadFences(*image).empty()); +} + +// A reader's frame fence reaches every image it noted, and only those. +TEST(SharedImageSync, AReadTrackerPublishesOneFrameToEveryImageItRead) { + std::string why; + SI::ImageRef a = SI::Allocate(8, 8, SI::kFourccAbgr8888, why); + SI::ImageRef b = SI::Allocate(8, 8, SI::kFourccAbgr8888, why); + SI::ImageRef untouched = SI::Allocate(8, 8, SI::kFourccAbgr8888, why); + ASSERT_TRUE(a && b && untouched) << why; + SI::ReadTracker tracker; + tracker.NoteRead(a); + tracker.NoteRead(b); + tracker.NoteRead(a); + EXPECT_EQ(tracker.Pending(), 2u); + FakeFence frame; + tracker.PublishFrame(frame.Dup()); + EXPECT_EQ(tracker.Pending(), 0u); + EXPECT_FALSE(SI::WaitForReads(*a, 10)); + EXPECT_FALSE(SI::WaitForReads(*b, 10)); + EXPECT_TRUE(SI::WaitForReads(*untouched, 10)); + frame.Signal(); + EXPECT_TRUE(SI::WaitForReads(*a, 10)); + EXPECT_TRUE(SI::WaitForReads(*b, 10)); +}