mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-17 08:38:30 +09:00
215 lines
12 KiB
C++
215 lines
12 KiB
C++
// MobileGL - MobileGL/MG_Remote/Server/ServerLoop.h
|
|
// 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
|
|
|
|
// The apply thread and the server's private backend object. Owner: package v1 - the highest
|
|
// risk item in P5. Signatures by c0.
|
|
//
|
|
// WHY THE THREAD IS THE POINT. DirectGLES has 16 IsBackendContextCurrentOnThisThread() guards
|
|
// (DirectGLES.cpp:12034..12428) and Managers.cpp has 16 CanTouchGLNow() guards (:1494..:3428);
|
|
// every one of them DEGRADES when the answer is false - fences become always-signaled, queries
|
|
// return null handles, Present creates no frame fence so the buffer pool's recycle watermark
|
|
// never advances, and the two persistent-map acquisitions (Managers.cpp:1494, :2170) DECLINE,
|
|
// which would make PersistentCoherentMapScenario unreachable. Making the apply thread the
|
|
// context owner for life turns all 32 of those answers true on the server and removes the
|
|
// whole degradation class at once. It is also exactly the shape P6's spawned server inherits.
|
|
//
|
|
// P5 BUILDS ONE THREAD, NOT TWO. No mgl-srv-io: inproc's control plane is in the same process.
|
|
// P6 splits it.
|
|
//
|
|
// PARKING AND SHUTDOWN. The thread parks on Doorbell::Wait(consumerParked, ready, spinUs,
|
|
// kWaitForever) and shuts down when Wait returns false with Dead() set. Doorbell::Kill()
|
|
// (Doorbell.h:211-221) IS THE ONLY THING that wakes a thread parked on kWaitForever - a fact
|
|
// ARCHITECTURE.md's teardown order (:537) omits and InProcessTransportTest.cpp:344 already
|
|
// pins. Kill BEFORE join; join before the client frees any emitter-owned Vector; and the join
|
|
// must be bounded (that test uses 5 s) so a regression is a red test and not a hung CI job.
|
|
//
|
|
// THE EGL OWNERSHIP MOVE. eglMakeCurrent runs ONCE on this thread and is never released
|
|
// (DirectGLES.cpp:11925 plus the six cache invalidations at :11933-11953, which become a
|
|
// one-time startup cost instead of a per-make-current storm). The client's nine EGL virtuals
|
|
// become BLOCKING control requests executed here. ReleaseEGLResources and
|
|
// ~BackendObject_DirectGLES MUST be blocking: MobileGL::Destroy() (MobileGL/Init.cpp:68)
|
|
// otherwise walks on while the server still holds the context.
|
|
//
|
|
// THE FALLBACK IS PRE-DECLARED, NOT INVENTED UNDER PRESSURE (R-1). If the context migration is
|
|
// still not running ClearThenReadPixels at the end of v1's fourth working day, the integrator -
|
|
// not the package - declares `inproc-inline`: the client thread drains the ring itself, no
|
|
// thread is created, no context migrates, and a second package picks up the thread arm.
|
|
|
|
#pragma once
|
|
#include <Includes.h>
|
|
|
|
#include "ServerSession.h"
|
|
|
|
#include <atomic>
|
|
#include <condition_variable>
|
|
#include <mutex>
|
|
#include <thread>
|
|
|
|
namespace MobileGL::MG_Remote::Server {
|
|
|
|
class ServerLoop {
|
|
public:
|
|
// Creates the apply thread, names it mgl-srv-apply, applies
|
|
// MOBILEGL_IPC_SERVER_AFFINITY (borrowing ShaderCompilePool's big-core detection) and
|
|
// LOGS THE RESOLVED MASK - an affinity that silently did nothing is indistinguishable
|
|
// from one that worked, and the split's whole performance claim rests on both halves
|
|
// landing on fast cores.
|
|
MobileGLResult Start(ServerSession& session);
|
|
|
|
// Kill the doorbell, join the thread (bounded), then destroy the private backend object
|
|
// ON THAT THREAD before it exits. Blocking by contract - see the header note.
|
|
void Stop();
|
|
|
|
Bool Running() const;
|
|
|
|
// The server role's own backend object. NOT pActiveBackendObject: that global holds the
|
|
// client's BackendObject_Remote. Table 3's ruling is that the server holds its
|
|
// BackendObject_DirectGLES privately here, and that the seven backend-internal reads of
|
|
// pActiveBackendObject - ClampSamplesToBackendSupport (BackendObject_DirectGLES.cpp:815,
|
|
// :819) and five in Utils.cpp (:74, :82, :126, :220, :260), all of them format-capability
|
|
// lookups - take the format cache as a parameter instead. That is six functions across
|
|
// two files, and it is why no thread-keyed shim is needed for MOBILEGL_BUILD_DISAGGREGATED_INPROC.
|
|
MG_Backend::BackendObject* Backend();
|
|
|
|
// Run one blocking control request on the apply thread and wait for it. This is how all
|
|
// nine EGL lifecycle virtuals cross; it is deliberately NOT a queue of async messages,
|
|
// because every one of them has a return value the caller acts on immediately.
|
|
//
|
|
// A raw function pointer plus a user pointer, not std::function: this runs on the
|
|
// teardown path too, and the teardown path may not allocate - ID-8's leak-at-exit rule
|
|
// exists because frontend destructors reach here from exit handlers.
|
|
using ControlWork = MobileGLResult (*)(void* user);
|
|
MobileGLResult RunOnApplyThread(ControlWork work, void* user);
|
|
|
|
// ---- v1's additions beyond c0's signature block ---------------------------------
|
|
|
|
// Constructs the SERVER ROLE's private BackendObject and runs its non-GL Initialize().
|
|
// Called from MG_Backend::Init()'s single hook, BEFORE ClientSession::Start(), because
|
|
// ServerSession::Accept() publishes the first CapsSnapshot from it and because the two
|
|
// CallMask halves - which have no default and Fatal when unset (ServerSession.h) - are
|
|
// answered from what this backend is.
|
|
//
|
|
// NO GL AND NO EGL HAPPENS HERE. BackendObject_DirectGLES::Initialize() loads the
|
|
// driver's entry points; the native context does not exist until the client's first
|
|
// eglMakeCurrent crosses as a blocking control request and runs on the apply thread.
|
|
// That split is what lets the backend object be BUILT on the app thread while its
|
|
// context is never OWNED by it.
|
|
MobileGLResult CreateBackend(BackendType type);
|
|
|
|
// True on the apply thread itself. RunOnApplyThread uses it to run inline rather than
|
|
// deadlock when the apply thread posts to itself - which the EGL teardown path does,
|
|
// because ~BackendObject_DirectGLES runs THERE and reaches ReleaseEGLResources.
|
|
static Bool OnApplyThread();
|
|
|
|
// The CPU mask MOBILEGL_IPC_SERVER_AFFINITY resolved to and sched_setaffinity accepted.
|
|
// 0 means "no affinity was applied" - the honest answer for `off`, for a platform with
|
|
// no affinity call, and for a failed syscall - and is exactly why the RESOLVED mask is
|
|
// logged rather than the string an operator typed.
|
|
Uint64 ResolvedAffinityMask() const;
|
|
|
|
// Diagnostics the tests read. DrainedRecords is how many records this thread has handed
|
|
// to the applier; ParkCount how many times it actually parked. A shutdown test that
|
|
// asserts only "Stop() returned" cannot tell a thread that parked and was woken by Kill
|
|
// from one that never parked at all - which is the R-16 shape of a check that cannot
|
|
// fail for its own reason.
|
|
Uint64 DrainedRecords() const;
|
|
Uint64 ParkCount() const;
|
|
|
|
private:
|
|
void ApplyThreadMain();
|
|
// Part of the apply thread's park predicate: a posted control request must be able to
|
|
// un-park a thread waiting on kWaitForever, which a Notify alone cannot do.
|
|
Bool ControlIsPending() const;
|
|
// Runs a posted control request, if there is one. Returns true if it ran one.
|
|
Bool PumpControlRequest();
|
|
// Pops and applies every record currently in the ring; returns how many it applied.
|
|
Uint64 DrainRing();
|
|
void SignalExited();
|
|
|
|
ServerSession* m_session = nullptr;
|
|
std::atomic<Bool> m_running{false};
|
|
std::atomic<Bool> m_stopRequested{false};
|
|
std::thread m_thread;
|
|
std::atomic<std::thread::id> m_applyThreadId{};
|
|
|
|
// The private backend object. Destroyed ON the apply thread while it still owns the
|
|
// context - see Stop().
|
|
UniquePtr<MG_Backend::BackendObject> m_backend;
|
|
|
|
// The blocking control mailbox. ONE slot, because the verb barrier already leaves one
|
|
// client thread runnable at a time; m_callerMutex serialises anything that is not.
|
|
std::mutex m_callerMutex;
|
|
std::mutex m_controlMutex;
|
|
std::condition_variable m_controlPosted;
|
|
std::condition_variable m_controlDone;
|
|
ControlWork m_controlWork = nullptr;
|
|
void* m_controlUser = nullptr;
|
|
MobileGLResult m_controlResult = MOBILEGL_OK;
|
|
Bool m_controlPending = false;
|
|
Bool m_controlFinished = false;
|
|
|
|
// The BOUNDED join's other half. std::thread::join has no deadline, so a lost wakeup
|
|
// would wedge CI rather than fail it; the thread signals here last and Stop() waits
|
|
// with a deadline (InProcessTransportTest.cpp:344's five seconds).
|
|
std::mutex m_exitMutex;
|
|
std::condition_variable m_exitCv;
|
|
Bool m_exited = false;
|
|
|
|
Uint64 m_affinityMask = 0;
|
|
std::atomic<Uint64> m_drained{0};
|
|
std::atomic<Uint64> m_parks{0};
|
|
};
|
|
|
|
ServerLoop& ServerLoopInstance();
|
|
|
|
// ---------------------------------------------------------------------------------
|
|
// THE EGL OWNERSHIP MOVE - the part that can sink the phase, expressed as twelve calls
|
|
// ---------------------------------------------------------------------------------
|
|
//
|
|
// Under split the app thread must never reach the driver's eglMakeCurrent. Today it does:
|
|
// EGLImpl.cpp:284 -> BackendObject_DirectGLES.cpp:963 -> DirectGLES.cpp:11925, and the
|
|
// owner slot g_backendContextOwnerThread (DirectGLES.cpp:11865) is stamped with whatever
|
|
// thread got there. So BackendObject_Remote's nine EGL virtuals - package c1's - call these
|
|
// twelve, each of which is a BLOCKING control request that runs the SERVER's backend object
|
|
// on mgl-srv-apply. eglMakeCurrent then runs ONCE, on that thread, and is never released:
|
|
// g_backendContextOwnerThread is written once, DirectGLES.cpp:11933-11953's six cache
|
|
// invalidations become a one-off startup cost instead of a per-migration storm, the
|
|
// per-frame EGL re-verification stamp is permanently true, and the 16
|
|
// IsBackendContextCurrentOnThisThread() sites plus the 16 CanTouchGLNow() sites answer TRUE
|
|
// on the server instead of silently degrading.
|
|
//
|
|
// TWO OF THEM MUST BLOCK OR THE PROCESS TEARS ITS OWN CONTEXT DOWN UNDER ITSELF:
|
|
// ReleaseEGLResources (reached from EGLImpl.cpp:326, which for DirectGLES runs
|
|
// DestroyEGLContext) and ~BackendObject_DirectGLES (reached from
|
|
// pActiveBackendObject.reset() at MobileGL/Init.cpp:68). Both are blocking here - the first
|
|
// by being one of these calls, the second because ServerLoop::Stop() destroys the private
|
|
// backend ON the apply thread before that thread exits and Stop() itself waits.
|
|
//
|
|
// WHY THEY ARE FREE FUNCTIONS AND NOT MEMBERS: c1 needs exactly this surface and nothing
|
|
// else of the server, so the seam between the two packages is a list of twelve signatures
|
|
// rather than a class with a lifecycle.
|
|
Bool ServerInitializeEGLDisplay(EGLDisplay dpy, EGLint* major, EGLint* minor);
|
|
Bool ServerCreateEGLWindowSurface(EGLSurface surface, const MG_Backend::WindowHandle& handle);
|
|
Bool ServerResizeEGLWindowSurface(EGLSurface surface, Uint32 width, Uint32 height);
|
|
Bool ServerCreateEGLPbufferSurface(EGLSurface surface, EGLint width, EGLint height);
|
|
// Also RE-PUBLISHES THE CAPS SNAPSHOT on success (R-12). BackendObject::MakeEGLCurrent runs
|
|
// InitCapabilities() on the first make-current per surface (BackendObject.cpp:341-347), so
|
|
// this is the moment the server's answers stop being the empty ones Accept() published -
|
|
// and a SECOND arrival IS the invalidation signal, which is how DirectGLES, which has no
|
|
// OnCapsInvalidated producer at all, tells the client without a dev-shaped backend edit.
|
|
Bool ServerMakeEGLCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx);
|
|
Bool ServerSwapEGLBuffers(EGLDisplay dpy, EGLSurface draw);
|
|
void ServerSetEGLSwapInterval(Int interval);
|
|
void ServerReleaseEGLSurface(EGLSurface surface);
|
|
void ServerReleaseEGLResources();
|
|
Bool ServerInitCapabilities();
|
|
Bool ServerInitWindowSurface();
|
|
void ServerSetWindowHandle(const MG_Backend::WindowHandle& handle);
|
|
|
|
} // namespace MobileGL::MG_Remote::Server
|