mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 18:13:23 +09:00
442 lines
16 KiB
C++
442 lines
16 KiB
C++
// 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 <fcntl.h>
|
|
#include <poll.h>
|
|
#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 = FourccIgnoresAlpha(image.Fourcc) ? 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);
|
|
if (WriteFence >= 0) ::close(WriteFence);
|
|
for (const int fence : ReadFences) ::close(fence);
|
|
#endif
|
|
ReleaseNative(Native);
|
|
}
|
|
|
|
Bool FourccSupported(Uint32 fourcc) {
|
|
return fourcc == kFourccAbgr8888 || fourcc == kFourccXbgr8888 || fourcc == kFourccArgb8888 ||
|
|
fourcc == kFourccXrgb8888;
|
|
}
|
|
|
|
Bool FourccIgnoresAlpha(Uint32 fourcc) { return fourcc == kFourccXbgr8888 || fourcc == kFourccXrgb8888; }
|
|
|
|
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;
|
|
}
|
|
|
|
// ---- 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 ReadTracker::PublishFrameAsWrite(int fence) {
|
|
for (const auto& [id, image] : m_images) {
|
|
PublishWrite(*image, Dup(fence));
|
|
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);
|
|
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
|