[Feature] (SharedImage): sync_file write/read fences per image, so sessions order their access without CPU waits

This commit is contained in:
2026-10-02 09:40:12 -04:00
parent 672261764e
commit b5cab44100
4 changed files with 274 additions and 3 deletions
+8 -3
View File
@@ -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;
@@ -21,6 +21,8 @@
#include <android/hardware_buffer.h>
#endif
#if !defined(_WIN32)
#include <fcntl.h>
#include <poll.h>
#include <sys/mman.h>
#include <sys/socket.h>
#include <sys/stat.h>
@@ -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<int>(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<std::mutex> lock(image.SyncMutex);
CloseFd(image.WriteFence);
image.WriteFence = fence;
++image.WriteGeneration;
}
int DupWriteFence(const Image& image, Uint64* generation) {
const std::lock_guard<std::mutex> 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<std::mutex> 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<int> DupPendingReadFences(const Image& image) {
const std::lock_guard<std::mutex> lock(image.SyncMutex);
PruneReads(image);
Vector<int> 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<std::mutex> lock(m_mutex);
@@ -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<int> 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<int> 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<Uint64, ImageRef> 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 {
+93
View File
@@ -21,6 +21,7 @@
#include <memory>
#include <string>
#include <utility>
#include <sys/mman.h>
#include <unistd.h>
@@ -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<int> 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));
}