mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-07 19:58:32 +09:00
- New CMake option MOBILEGL_BUILD_DISAGGREGATED (default OFF, plan B appendix B). ON appends the MG_Remote sources to SOURCE_FILES, puts 3rdparty/flatbuffers/include on the include path and defines MOBILEGL_BUILD_DISAGGREGATED=1. OFF compiles nothing from MG_Remote and adds no include path and no library, which is one of the two byte-level equalities plan B section 10.3 keeps: measured `nm --defined-only build-linux/libMobileGL.so | grep -ic MG_Remote` = 0 with the option OFF and 93 with it ON, with an identical ldd set in both configurations.
- The option forces itself OFF with message(WARNING) when 3rdparty/flatbuffers/include/flatbuffers/flatbuffers.h is missing, so a checkout without the submodule still configures and builds rather than failing a hundred lines later on a missing header.
- ITransport.h: SendFrame / ReceiveFrame / PeekFrameSize / ShareFd / ReceiveFd / Shutdown. ReceiveFrame's contract is the fix for a defect of the earlier branch: a destination buffer smaller than the pending message returns MOBILEGL_ERR_BUFFER_TOO_SMALL with the required size and KEEPS the message queued. The earlier transport failed the call and popped the message anyway, which wedges the stream permanently the first time a reader guesses a size wrong.
- Framing.h: [u32 'MGLF'][u32 len][payload], 64 MiB cap, validated on read. A bad magic or an oversized length latches the reader dead, is logged at ERROR and makes every later call return MOBILEGL_ERR_PROTOCOL_MISMATCH. This is the second inherited defect: Feat/CS-Delta-IPC's Framing.h:41-45 Feed() always returned OK and its header peek merely returned false, so a desynchronized stream became a silent permanent hang; its LocalSocketTransport.cpp:232-236 then allocated on the peer-supplied length with no cap. The magic is also byte-ordered so it reads "MGLF" on the wire, where the earlier constant spelled "FLGM".
- Ring.h/.cpp: RingControl exactly as the inherited design (plan B section 8.1 -> earlier section 6.2): two independent cursor triples (cmd and stage, each {head, appliedTail, retiredTail}), appliedSeq / submittedSeq / retiredSeq / completedFrameSerial / presentAckSerial, serverEpoch, ringGeneration, consumerParked, producerParked, eventRingFull, eventDropped. One 4096-byte page with each contended group on its own cache line, pinned by static_assert on size and alignment and by an offset test.
- The SPSC pair uses monotonic byte cursors and a power-of-two mask, so a torn read can never look like a valid earlier position. A record never straddles the wrap: the producer emits a kPad filler to the boundary, which is always a multiple of 8 and therefore always has room for a header. The producer reclaims against retiredTail rather than appliedTail, so the day the server borrows a ring slot into the GPU timeline (kRecBorrowSlot) it does not silently degrade to early recycling. HardDrainRing bumps ringGeneration only when the ring is quiesced, and leaves the cursors monotonic so cached offsets are recognisably stale.
- The consumer bounds-checks every record header before dispatch (8-aligned, at least a header, no larger than what the producer published, contiguous inside the mapping) and reports corruption to the caller instead of dispatching into undefined behaviour. That is the runtime half of the earlier section 6.3 discipline: SEG_CMD is written by another process, so a compile-time static_assert on record sizes proves nothing about what is in the mapping.
- Doorbell.h/.cpp: spin then park, both directions (earlier section 6.2a). CondVarDoorbell for inproc, SocketDoorbell for spawn (one byte, codes 0x01 ring-advanced and 0x02 watermark-advanced) - no futex, eventfd or named event anywhere. The lost-wakeup window is closed by ordering: the waiter stores its park flag seq_cst and then re-tests the condition, NotifyIfParked loads the same flag seq_cst after the watermark is published, so one of the two always sees the other. kDefaultSpinUs is 50, the MOBILEGL_IPC_SPIN_US default.
- ShmSegment.{h,cpp} + ShmSegmentPosix.cpp: memfd_create by raw syscall on desktop Linux (the glibc wrapper is too recent to rely on), ASharedMemory_create on Android (API 26; libc's memfd_create wrapper is API 30, above MobileGL's floor), shm_open + immediate shm_unlink as the fallback. Adopt() refuses a descriptor whose fstat size is smaller than the size the peer announced, so a short segment cannot turn every later offset into an out-of-bounds map. ShmSegmentWin32.cpp (CreateFileMappingW in Local\) is compile-guarded and untested - this project's Windows machine is not a correctness gate.
- The Android path is compile-verified, not just written: an arm64-v8a NDK build with the option ON links, ShmSegmentPosix.cpp.o carries an undefined ASharedMemory_create, and ShmSegmentWin32.cpp.o is empty there. That build is also what caught the missing <cstddef> in ShmSegment.h and Framing.h, where std::size_t / std::ptrdiff_t only resolved through a transitive include on the host sysroot.
- FdPassing.{h,cpp}: SCM_RIGHTS in the FIRST transport commit, as plan B section 8.1 demands, over a dedicated AF_UNIX SOCK_DGRAM socketpair rather than the control byte stream (message boundaries survive on every POSIX - SOCK_SEQPACKET does not exist on macOS - and ancillary data can never be split from its payload). The third inherited defect this replaces: Feat/CS-Delta-IPC deferred fd passing and hardcoded `out->fd = -1` in LocalSocketTransport.cpp:296, so on the only platform that matters its data plane could not move one byte between processes. MSG_CTRUNC, an unexpected descriptor count and a malformed sideband header all close every descriptor received before failing, and a too-small sideband buffer is refused before the recvmsg so a datagram is never half-consumed.
- InProcessTransport.{h,cpp}: two in-memory queues plus the condvar doorbell pair. It keeps the frame size cap so nothing that passes in inproc becomes illegal after the switch to spawn, hands descriptors over with dup() under the same ownership rule as SCM_RIGHTS, and lets a peer's queued messages be drained after Shutdown - usually the last one says why it is going away.
- Not done here on purpose: the MOBILEGL_IPC_* environment variables are parsed in ConfigLoader.cpp, which belongs to another P0 work package running in parallel; this commit only exposes the constants (kDefaultSpinUs, kMaxFramePayloadSize, FdPassing::kMaxSidebandBytes) so that plumbing has something to set.
169 lines
5.3 KiB
C++
169 lines
5.3 KiB
C++
// MobileGL - MobileGL/MG_Remote/Transport/Doorbell.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 "Doorbell.h"
|
|
|
|
#include <MG_Util/Debug/Log.h>
|
|
|
|
#include <condition_variable>
|
|
#include <mutex>
|
|
|
|
#if !defined(_WIN32)
|
|
#include <cerrno>
|
|
#include <poll.h>
|
|
#include <sys/socket.h>
|
|
#include <unistd.h>
|
|
#endif
|
|
|
|
namespace MobileGL::MG_Remote::Transport {
|
|
|
|
// -----------------------------------------------------------------------
|
|
// CondVarDoorbell
|
|
// -----------------------------------------------------------------------
|
|
|
|
struct CondVarDoorbell::Impl {
|
|
std::mutex mutex;
|
|
std::condition_variable cv;
|
|
// Counted, not a flag: a wakeup that arrives while nobody is parked
|
|
// must still be observed by the next Park.
|
|
std::uint32_t signals = 0;
|
|
};
|
|
|
|
CondVarDoorbell::CondVarDoorbell() : m_impl(new Impl()) {}
|
|
|
|
CondVarDoorbell::~CondVarDoorbell() { delete m_impl; }
|
|
|
|
void CondVarDoorbell::Notify() {
|
|
{
|
|
std::lock_guard<std::mutex> lock(m_impl->mutex);
|
|
++m_impl->signals;
|
|
}
|
|
m_impl->cv.notify_one();
|
|
}
|
|
|
|
bool CondVarDoorbell::Park(std::uint32_t timeoutMs) {
|
|
std::unique_lock<std::mutex> lock(m_impl->mutex);
|
|
if (m_impl->signals != 0) {
|
|
--m_impl->signals;
|
|
return true;
|
|
}
|
|
if (timeoutMs == 0) {
|
|
return false;
|
|
}
|
|
if (timeoutMs == kWaitForever) {
|
|
m_impl->cv.wait(lock, [this] { return m_impl->signals != 0; });
|
|
} else if (!m_impl->cv.wait_for(lock, std::chrono::milliseconds(timeoutMs),
|
|
[this] { return m_impl->signals != 0; })) {
|
|
return false;
|
|
}
|
|
--m_impl->signals;
|
|
return true;
|
|
}
|
|
|
|
void CondVarDoorbell::Reset() {
|
|
std::lock_guard<std::mutex> lock(m_impl->mutex);
|
|
m_impl->signals = 0;
|
|
}
|
|
|
|
#if !defined(_WIN32)
|
|
|
|
// -----------------------------------------------------------------------
|
|
// SocketDoorbell
|
|
// -----------------------------------------------------------------------
|
|
|
|
SocketDoorbell::SocketDoorbell(int fd, std::uint8_t code, bool ownsFd)
|
|
: m_fd(fd), m_code(code), m_ownsFd(ownsFd) {}
|
|
|
|
SocketDoorbell::~SocketDoorbell() {
|
|
if (m_ownsFd && m_fd >= 0) {
|
|
::close(m_fd);
|
|
}
|
|
}
|
|
|
|
void SocketDoorbell::Notify() {
|
|
if (m_fd < 0) {
|
|
return;
|
|
}
|
|
const std::uint8_t byte = m_code;
|
|
for (;;) {
|
|
const ssize_t written = ::send(m_fd, &byte, 1, MSG_DONTWAIT | MSG_NOSIGNAL);
|
|
if (written == 1) {
|
|
return;
|
|
}
|
|
if (written < 0 && errno == EINTR) {
|
|
continue;
|
|
}
|
|
if (written < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
|
|
// The socket buffer already holds unread wakeups: the peer has
|
|
// one pending, which is all a doorbell promises.
|
|
return;
|
|
}
|
|
if (written < 0 && errno == EPIPE) {
|
|
return; // peer gone; the waiter learns it from its own read
|
|
}
|
|
MGLOG_D("MG_Remote doorbell: send failed (errno=%d)", errno);
|
|
return;
|
|
}
|
|
}
|
|
|
|
bool SocketDoorbell::Park(std::uint32_t timeoutMs) {
|
|
if (m_fd < 0) {
|
|
return false;
|
|
}
|
|
const auto start = std::chrono::steady_clock::now();
|
|
for (;;) {
|
|
int pollTimeout = -1;
|
|
if (timeoutMs != kWaitForever) {
|
|
const auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
std::chrono::steady_clock::now() - start)
|
|
.count();
|
|
const long long remaining = static_cast<long long>(timeoutMs) - elapsed;
|
|
pollTimeout = remaining <= 0 ? 0 : static_cast<int>(remaining);
|
|
}
|
|
struct pollfd pfd{};
|
|
pfd.fd = m_fd;
|
|
pfd.events = POLLIN;
|
|
const int ready = ::poll(&pfd, 1, pollTimeout);
|
|
if (ready < 0) {
|
|
if (errno == EINTR) {
|
|
continue; // a signal is not a wakeup; keep the deadline
|
|
}
|
|
MGLOG_D("MG_Remote doorbell: poll failed (errno=%d)", errno);
|
|
return false;
|
|
}
|
|
if (ready == 0) {
|
|
return false; // timed out
|
|
}
|
|
Reset();
|
|
return true;
|
|
}
|
|
}
|
|
|
|
void SocketDoorbell::Reset() {
|
|
if (m_fd < 0) {
|
|
return;
|
|
}
|
|
// Level-triggered to edge-triggered: swallow every queued byte so one
|
|
// stale wakeup cannot make later Parks return without an event.
|
|
std::uint8_t scratch[64];
|
|
for (;;) {
|
|
const ssize_t got = ::recv(m_fd, scratch, sizeof(scratch), MSG_DONTWAIT);
|
|
if (got > 0) {
|
|
continue;
|
|
}
|
|
if (got < 0 && errno == EINTR) {
|
|
continue;
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
#endif // !_WIN32
|
|
|
|
} // namespace MobileGL::MG_Remote::Transport
|