mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
2417 lines
145 KiB
C++
2417 lines
145 KiB
C++
// MobileGL - MobileGL/MG_Remote/Server/ServerLoop.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
|
|
|
|
// P5 package v1: the apply thread, its affinity, its parking, and the EGL ownership move.
|
|
|
|
#include "ServerLoop.h"
|
|
#include "ApplyThreadPolicy.h"
|
|
#include "SessionRuntime.h"
|
|
#include <MG_Remote/FatalFunnel.h>
|
|
#include <MG_Pipe/PipeSessionFail.h>
|
|
#include <MG_Remote/Transport/LinkMetrics.h>
|
|
#include <MG_Backend/MGPipe/PipeInputs.h>
|
|
|
|
#include <Config.h>
|
|
#include <MG_Backend/BackendObjects.h>
|
|
#include <MG_Backend/DirectGLES/DirectGLES.h>
|
|
#include <MG_Util/Debug/Log.h>
|
|
#include <MG_Util/Metrics/PipeStats.h>
|
|
|
|
#include <algorithm>
|
|
#include <atomic>
|
|
#include <chrono>
|
|
#include <condition_variable>
|
|
#include <cstdlib>
|
|
#include <cstdio>
|
|
#if defined(__ANDROID__)
|
|
#include <sys/system_properties.h>
|
|
#endif
|
|
#include <cstring>
|
|
#include <mutex>
|
|
#include <optional>
|
|
#include <thread>
|
|
|
|
#if defined(__linux__) || defined(__ANDROID__)
|
|
#include <pthread.h>
|
|
#include <sched.h>
|
|
#endif
|
|
|
|
namespace MobileGL::MG_Remote::Server {
|
|
|
|
namespace {
|
|
|
|
// P14: THE BACKEND'S TURN. The in-process display server runs one apply thread per
|
|
// session, and the backend those threads drive was written for one GL thread: one owner
|
|
// thread, one set of binding shadows, process-wide rings and pools. Two sessions applying
|
|
// at once raced all of it - a session's buffer ops were deferred while the other thread
|
|
// owned the backend, and a texture bind was skipped because the shadow described the
|
|
// other session's context. So an apply thread holds the turn while it does backend work
|
|
// (control requests, a drain batch, fence reports, teardown) and gives it up before it
|
|
// parks. FIFO, so a session with a busy ring cannot starve its neighbours; reentrant for
|
|
// the thread that holds it. When the turn moves to a different thread the backend takes
|
|
// ownership for it and drops its binding shadows (DirectGLES::OnBackendTurnHandoff).
|
|
class BackendTurnLock {
|
|
public:
|
|
void Lock() {
|
|
const std::thread::id self = std::this_thread::get_id();
|
|
std::unique_lock<std::mutex> lock(m_mutex);
|
|
if (m_owner == self) {
|
|
++m_depth;
|
|
return;
|
|
}
|
|
const Uint64 ticket = m_nextTicket++;
|
|
m_turn.wait(lock, [&] { return m_serving == ticket; });
|
|
m_owner = self;
|
|
m_depth = 1;
|
|
if (m_lastOwner != self) {
|
|
const Bool handoff = m_lastOwner != std::thread::id{};
|
|
m_lastOwner = self;
|
|
lock.unlock();
|
|
if (handoff) MG_Backend::DirectGLES::OnBackendTurnHandoff();
|
|
}
|
|
}
|
|
void Unlock() {
|
|
const std::lock_guard<std::mutex> lock(m_mutex);
|
|
if (--m_depth > 0) return;
|
|
m_owner = std::thread::id{};
|
|
++m_serving;
|
|
m_turn.notify_all();
|
|
}
|
|
// Gives the turn up however deep the calling thread holds it, for a wait that does no
|
|
// backend work; answers the depth Resume restores (0: the thread did not hold it).
|
|
Uint32 YieldIfHeld() {
|
|
const std::lock_guard<std::mutex> lock(m_mutex);
|
|
if (m_owner != std::this_thread::get_id()) return 0;
|
|
const Uint32 depth = m_depth;
|
|
m_depth = 0;
|
|
m_owner = std::thread::id{};
|
|
++m_serving;
|
|
m_turn.notify_all();
|
|
return depth;
|
|
}
|
|
void Resume(Uint32 depth) {
|
|
if (depth == 0) return;
|
|
Lock();
|
|
const std::lock_guard<std::mutex> lock(m_mutex);
|
|
m_depth = depth;
|
|
}
|
|
|
|
private:
|
|
std::mutex m_mutex;
|
|
std::condition_variable m_turn;
|
|
Uint64 m_nextTicket = 0;
|
|
Uint64 m_serving = 0;
|
|
std::thread::id m_owner{};
|
|
std::thread::id m_lastOwner{};
|
|
Uint32 m_depth = 0;
|
|
};
|
|
|
|
BackendTurnLock& BackendTurn() {
|
|
static BackendTurnLock& turn = *new BackendTurnLock(); // leaked at exit (ID-8)
|
|
return turn;
|
|
}
|
|
|
|
class ScopedBackendTurn {
|
|
public:
|
|
ScopedBackendTurn() { BackendTurn().Lock(); }
|
|
~ScopedBackendTurn() { BackendTurn().Unlock(); }
|
|
ScopedBackendTurn(const ScopedBackendTurn&) = delete;
|
|
ScopedBackendTurn& operator=(const ScopedBackendTurn&) = delete;
|
|
};
|
|
|
|
// A WAIT FOR THE SERVER WINDOW HOLDS NO TURN. It can last seconds - a window that is not up
|
|
// yet, or the lease of a session that is ending - and the other sessions' apply threads need
|
|
// the turn meanwhile: the ending one to tear its backend down, which is what ends its lease.
|
|
// Nothing of the backend is touched while it is given up; it is taken back (a handoff, when
|
|
// another thread had it) before the waiter goes on.
|
|
class ScopedBackendTurnYield {
|
|
public:
|
|
ScopedBackendTurnYield() : m_depth(BackendTurn().YieldIfHeld()) {}
|
|
~ScopedBackendTurnYield() { BackendTurn().Resume(m_depth); }
|
|
ScopedBackendTurnYield(const ScopedBackendTurnYield&) = delete;
|
|
ScopedBackendTurnYield& operator=(const ScopedBackendTurnYield&) = delete;
|
|
|
|
private:
|
|
const Uint32 m_depth;
|
|
};
|
|
|
|
// The process-wide test seam (ServerLoop.h).
|
|
std::atomic<ServerLoop::ServerOwnedSurfaceHook> g_serverOwnedSurfaceHook{nullptr};
|
|
|
|
// FatalFunnel's latch observer. The latching thread is one of the session's own (its scope
|
|
// resolves ServerLoopInstance to that session's loop): a session that latched is over, and
|
|
// its lease on the server window, if it holds one, ends once its apply thread has torn the
|
|
// backend down - which a fresh session of the same client waits for (MarkLeaseEnding).
|
|
void MarkLatchedSessionsLeaseEnding() { ServerDisplayInstance().MarkLeaseEnding(&ServerLoopInstance()); }
|
|
|
|
// The bounded join. InProcessTransportTest.cpp:344 uses five seconds for the same
|
|
// reason: a lost wakeup must be a RED TEST and not a hung CI job.
|
|
constexpr Uint32 kJoinTimeoutMs = 5000;
|
|
// THE BUSY BOUND. A thread that is not parked when kJoinTimeoutMs runs out is inside the
|
|
// backend - a draw or a fence wait the GPU has not finished - and on a GPU the driver has
|
|
// to reset (a shader that never ends: Adreno's preemption fault fires after ~10 s, once per
|
|
// stuck submission) that takes several times the lost-wakeup bound. Dying at 5 s there
|
|
// took the whole display server - every session, the compositor's included - down with
|
|
// the one client whose GPU work hung. The wait goes on, to this bound, which still turns
|
|
// a thread that never comes back into a named death.
|
|
constexpr Uint32 kBusyJoinTimeoutMs = 60000;
|
|
|
|
// A core counts as "big" if its cpufreq ceiling is within 15% of the fastest core's -
|
|
// the identical rule and the identical constant as ShaderCompilePool.cpp:28 and :73-95.
|
|
// The probe is re-derived here rather than exported from there because the two want
|
|
// DIFFERENT ANSWERS from the same data: the pool wants a COUNT (how many workers), and
|
|
// an affinity wants a MASK (which cpus). DetectBigCoreCount() cannot answer the second,
|
|
// so exporting it would have meant either a second function in another package's file
|
|
// or a mask reconstructed from a count, which is wrong on any asymmetric topology whose
|
|
// big cores are not cpu0..cpuN-1.
|
|
constexpr Uint64 kBigCoreFrequencyPercent = 85;
|
|
|
|
Uint64 ReadCpuMaxFrequencyKHz(const Uint cpu) {
|
|
char path[128];
|
|
std::snprintf(path, sizeof(path),
|
|
"/sys/devices/system/cpu/cpu%u/cpufreq/cpuinfo_max_freq", cpu);
|
|
std::FILE* file = std::fopen(path, "r");
|
|
if (file == nullptr) return 0;
|
|
unsigned long long value = 0;
|
|
const int scanned = std::fscanf(file, "%llu", &value);
|
|
std::fclose(file);
|
|
return scanned == 1 ? static_cast<Uint64>(value) : 0;
|
|
}
|
|
|
|
// The set of cpus whose ceiling is within 15% of the peak, as a bit per cpu. Zero when
|
|
// the topology cannot be read - Windows, macOS, a container that hides the cpufreq
|
|
// tree, or a partially readable one - because with no asymmetry information the honest
|
|
// answer is "do not pin", not "pin to a guess".
|
|
// P11 B1: and the PRIME set beside it - the cpus AT the peak ceiling - which the dialled-in
|
|
// shared-segment rule needs so it never hands the spinning apply thread the only prime core.
|
|
void DetectCoreTopology(Uint64* outBig, Uint64* outPrime) {
|
|
*outBig = *outPrime = 0;
|
|
const Uint cpuCount = std::min(64u, std::max(1u, std::thread::hardware_concurrency()));
|
|
Uint64 frequencies[64] = {};
|
|
for (Uint cpu = 0; cpu < cpuCount; ++cpu) {
|
|
frequencies[cpu] = ReadCpuMaxFrequencyKHz(cpu);
|
|
if (frequencies[cpu] == 0) return;
|
|
}
|
|
Uint64 peak = 0;
|
|
for (Uint cpu = 0; cpu < cpuCount; ++cpu) peak = std::max(peak, frequencies[cpu]);
|
|
if (peak == 0) return;
|
|
const Uint64 threshold = peak * kBigCoreFrequencyPercent / 100;
|
|
for (Uint cpu = 0; cpu < cpuCount; ++cpu) {
|
|
if (frequencies[cpu] >= threshold) *outBig |= (1ull << cpu);
|
|
if (frequencies[cpu] == peak) *outPrime |= (1ull << cpu);
|
|
}
|
|
}
|
|
|
|
// One bit per cpu the process can count, the same count DetectCoreTopology walks - what
|
|
// lets ApplyThreadPolicy tell a real prime set from a symmetric machine's "every cpu".
|
|
Uint64 OnlineCpuMask() {
|
|
const Uint cpuCount = std::min(64u, std::max(1u, std::thread::hardware_concurrency()));
|
|
return cpuCount >= 64 ? ~0ull : ((1ull << cpuCount) - 1);
|
|
}
|
|
|
|
// Whether MOBILEGL_IPC_SPIN_US was set at all: ConfigLoader answers 50 for "unset" and
|
|
// for "50", and the dialled-in peer's default spin (ApplyThreadPolicy.h) applies only to
|
|
// the first.
|
|
Bool SpinSetExplicitly() {
|
|
const char* text = std::getenv("MOBILEGL_IPC_SPIN_US");
|
|
return text != nullptr && text[0] != '\0';
|
|
}
|
|
|
|
// The per-frame P65ServerFrame line is a measurement aid, not a log: at one line per present
|
|
// of every session it floods the device log (and pushes the lines tests grep for out of the
|
|
// ring). Opt in with MOBILEGL_SERVER_FRAME_STATS=1, or on Android the property
|
|
// debug.mobilegl.server_frame_stats=1; read once.
|
|
Bool ServerFrameStatsEnabled() {
|
|
static const Bool enabled = [] {
|
|
const char* value = std::getenv("MOBILEGL_SERVER_FRAME_STATS");
|
|
#if defined(__ANDROID__)
|
|
char property[PROP_VALUE_MAX] = {};
|
|
if (value == nullptr && __system_property_get("debug.mobilegl.server_frame_stats", property) > 0)
|
|
value = property;
|
|
#endif
|
|
return value != nullptr && value[0] != ' |