mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-09 04:38:30 +09:00
- S-1 PipeStats::OnPresent read each frame accumulator and then store(0)'d it; a Bump from a staging thread landing in between was lost from the Tracy plot and from every frame. Each accumulator is now exchange(0, relaxed) and the exchanged value is what is plotted, so every add lands in exactly one frame. - T-3 FdPassing without MSG_CMSG_CLOEXEC (macOS, BSD) handed back descriptors that survived exec; every received fd now gets FD_CLOEXEC by hand under !MSG_CMSG_CLOEXEC. MSG_NOSIGNAL is defined to 0 where the platform lacks it (FdPassing.cpp, Doorbell.cpp) and SO_NOSIGPIPE is set on the socketpair and on a SocketDoorbell's descriptor where it exists, so a write to a hung-up peer is EPIPE rather than a fatal signal. - T-4 the missing-flatbuffers fallback wrote OFF into the cache with FORCE, so a plain re-configure after `git submodule update` stayed OFF silently. It is a normal-variable set now, shadowing the cache for that configure only; verified by hiding flatbuffers.h, configuring with ON (warning, transport off, cache still ON) and re-configuring plainly with the header back (transport ON). - P-2 three LIVE GLFunctionsTable entries had no carrier: GetGpuTimestampNs (glGetInteger64v(GL_TIMESTAMP), a synchronous server answer), QueryCounterTimestamp (glQueryCounter, a one-shot stamp, not a begin/end pair) and WaitSync (the GPU-side wait FenceWait's client wait does not express). QueryTimestamp (MGPTimestampRequest, kCtxQuery, kReplySlot), QueryCounter (MGPQueryDesc with Kind = GL_TIMESTAMP, kCtxQuery) and FenceWaitServer (MGPFenceWait, kScreen) are APPENDED at the end of PipeCalls.def because the opcode is the position: SetSwapInterval stays 68, the three take 69-71, and PipeCatalogue.LateArrivalsAreAppendedWithoutRenumbering pins that. Header counts 71 (screen 11, query 8); the seven generators regenerated. - P-3 MGPBufferRange inlined a 32-byte MGHostSpan into every range of every class - dead space on every SSBO, atomic-counter and XFB range, and D-B8 says not to freeze the named-UBO payload before the stage-ubo-named numbers exist. The range is 24 bytes now; the host spans are an optional second var-tail behind the ranges, announced by MGPShaderBuffers::HostSpanCount (0 or Count), with set_shader_buffers keeping its kVarTail|kHostSpan flags. PipeCatalogue.BufferRangeCarriesNoInlineHostSpan pins the sizes, the flags and the comparator's view of the count. - P-4 QueryEnvUint64 parsed with base 0 (a leading zero meant octal: MOBILEGL_PIPE_PUSH=010 read as 8) and accepted -1 as every bit set; it is decimal or explicit 0x now and a '-' anywhere is refused with the warning (smoke through the integration binary: -1 and 12abc warn, 010 and 0x10 parse). The CI stdio gate's alternation now also catches fprintf(stdout, puts( and std::cout/cerr; it is green over MG_Backend and MG_State. MGPSubData states how the buffer half expresses [offset, size): UnionBox.X / UnionBox.W with Target == Buffer, Y = Z = 0, H = D = 1, one record bounded at a 2^31-1 offset and 2^32-1 size beyond which the emitter splits (the same rule the ring's half-capacity bound already imposes); MGPipeSetSubDataBufferRange / MGPipeSubDataBufferOffset / Size are the only spelling and PipeCatalogue.SubDataBufferRangeRidesInTheUnionBox pins the encoding and its bounds. gen_pipe.py now refuses, in both modes, a call payload named in PipeCalls.def with no field list in PipeFields.def (the four memcmp-fallback member types are the documented exception); shown by dropping P(MGPSwapInterval), which exits 1 naming the payload. - The MGPPixelPackState size assertion compared sizeof against itself; it asserts the literal 28 PixelStoreParameters measures. - Verified: ctest -L unit green in both the default and the split configuration, gen_pipe.py --check clean with the generated files committed, nm --defined-only of the default libMobileGL.so has no MG_Remote symbol, and the full integration-gpu suite passes (the *IsActuallyArmedWhenTheEnvironmentPinsItOn family trips under -j 8 as documented and passes serially).
260 lines
9.2 KiB
C++
260 lines
9.2 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
|
|
|
|
// Same fallback as FdPassing.cpp: on macOS / BSD the protection is SO_NOSIGPIPE on the
|
|
// socket, set in SocketDoorbell's constructor, not a per-send flag.
|
|
#if !defined(_WIN32) && !defined(MSG_NOSIGNAL)
|
|
#define MSG_NOSIGNAL 0
|
|
#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);
|
|
// The death latch is tested under the same mutex Kill sets it under, so
|
|
// a Kill cannot slip between this test and the wait below: it either
|
|
// returns here or wakes the predicate.
|
|
if (m_dead.load(std::memory_order_relaxed)) {
|
|
return false;
|
|
}
|
|
if (m_impl->signals != 0) {
|
|
--m_impl->signals;
|
|
return true;
|
|
}
|
|
if (timeoutMs == 0) {
|
|
return false;
|
|
}
|
|
const auto woken = [this] {
|
|
return m_impl->signals != 0 || m_dead.load(std::memory_order_relaxed);
|
|
};
|
|
if (timeoutMs == kWaitForever) {
|
|
m_impl->cv.wait(lock, woken);
|
|
} else if (!m_impl->cv.wait_for(lock, std::chrono::milliseconds(timeoutMs), woken)) {
|
|
return false;
|
|
}
|
|
if (m_dead.load(std::memory_order_relaxed)) {
|
|
// Woken by Kill, not by an event. The caller re-tests its condition
|
|
// regardless (Doorbell::Wait always does) and then sees Dead().
|
|
return false;
|
|
}
|
|
--m_impl->signals;
|
|
return true;
|
|
}
|
|
|
|
void CondVarDoorbell::Kill() {
|
|
{
|
|
std::lock_guard<std::mutex> lock(m_impl->mutex);
|
|
m_dead.store(true, std::memory_order_release);
|
|
}
|
|
// notify_all, not notify_one: both a raw Park and a Doorbell::Wait may
|
|
// be parked here, and after this nobody will ring again.
|
|
m_impl->cv.notify_all();
|
|
}
|
|
|
|
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) {
|
|
#if defined(SO_NOSIGPIPE)
|
|
// The per-socket form of MSG_NOSIGNAL, on the platforms that lack the per-call one:
|
|
// a Notify to a hung-up peer must come back as EPIPE, not as a fatal signal.
|
|
if (m_fd >= 0) {
|
|
const int one = 1;
|
|
(void)::setsockopt(m_fd, SOL_SOCKET, SO_NOSIGPIPE, &one, sizeof(one));
|
|
}
|
|
#endif
|
|
}
|
|
|
|
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 || errno == ECONNRESET)) {
|
|
// The peer is gone: it can never ring back either, so latch it
|
|
// here too rather than waiting for a Park to discover it.
|
|
m_dead = true;
|
|
return;
|
|
}
|
|
MGLOG_D("MG_Remote doorbell: send failed (errno=%d)", errno);
|
|
return;
|
|
}
|
|
}
|
|
|
|
bool SocketDoorbell::Park(std::uint32_t timeoutMs) {
|
|
if (m_fd < 0 || m_dead) {
|
|
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
|
|
}
|
|
// revents has to be inspected, not just `ready > 0`. Once the peer
|
|
// closes its end the descriptor is permanently poll-ready with
|
|
// nothing to read (measured on Linux: revents=POLLIN|POLLHUP,
|
|
// recv()==0), so treating any readiness as a wakeup turns every
|
|
// park on a dead peer into a 100% CPU spin - unbounded, because
|
|
// Doorbell::Wait re-parks until its deadline and kWaitForever has
|
|
// none.
|
|
if ((pfd.revents & (POLLERR | POLLNVAL)) != 0) {
|
|
MGLOG_D("MG_Remote doorbell: fd %d unusable (revents=0x%X)", m_fd,
|
|
static_cast<unsigned>(pfd.revents));
|
|
m_dead = true;
|
|
return false;
|
|
}
|
|
if ((pfd.revents & POLLIN) != 0) {
|
|
if (Drain() != 0) {
|
|
return true; // a real wakeup byte
|
|
}
|
|
if (m_dead) {
|
|
return false; // EOF, not an event
|
|
}
|
|
// Ready but empty and still alive: someone else drained it.
|
|
// Report the wakeup and let the caller re-test its condition.
|
|
return true;
|
|
}
|
|
if ((pfd.revents & POLLHUP) != 0) {
|
|
m_dead = true;
|
|
return false;
|
|
}
|
|
// Readiness with no bit we requested or recognise: there is
|
|
// nothing to consume and no way to make progress, so refuse to
|
|
// poll this descriptor again.
|
|
MGLOG_D("MG_Remote doorbell: fd %d ready with revents=0x%X", m_fd,
|
|
static_cast<unsigned>(pfd.revents));
|
|
m_dead = true;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
std::uint64_t SocketDoorbell::Drain() {
|
|
// Level-triggered to edge-triggered: swallow every queued byte so one
|
|
// stale wakeup cannot make later Parks return without an event.
|
|
std::uint64_t consumed = 0;
|
|
std::uint8_t scratch[64];
|
|
for (;;) {
|
|
const ssize_t got = ::recv(m_fd, scratch, sizeof(scratch), MSG_DONTWAIT);
|
|
if (got > 0) {
|
|
consumed += static_cast<std::uint64_t>(got);
|
|
continue;
|
|
}
|
|
if (got == 0) {
|
|
// Orderly shutdown on a stream socket: the peer is gone and
|
|
// will never ring again.
|
|
m_dead = true;
|
|
return consumed;
|
|
}
|
|
if (errno == EINTR) {
|
|
continue;
|
|
}
|
|
if (errno == EAGAIN || errno == EWOULDBLOCK) {
|
|
return consumed; // drained
|
|
}
|
|
MGLOG_D("MG_Remote doorbell: recv failed (errno=%d)", errno);
|
|
m_dead = true;
|
|
return consumed;
|
|
}
|
|
}
|
|
|
|
void SocketDoorbell::Reset() {
|
|
if (m_fd < 0 || m_dead) {
|
|
return;
|
|
}
|
|
(void)Drain();
|
|
}
|
|
|
|
#endif // !_WIN32
|
|
|
|
} // namespace MobileGL::MG_Remote::Transport
|