[Feature] (Remote): serve concurrent shared-memory clients in isolated workers

This commit is contained in:
2026-10-01 12:53:27 -04:00
parent a5b463a5e8
commit 2f64ede8c6
3 changed files with 165 additions and 10 deletions
+25 -10
View File
@@ -1458,20 +1458,24 @@ extern "C" __attribute__((visibility("default"))) int mobilegl_server_main(int a
if (const int refused = CheckServerEnvironment(); refused != 0) return refused;
std::string endpoint;
bool serve = false;
unsigned long maxSessions = 1;
for (int i = 1; i < argc; ++i) {
const std::string arg(argv[i]);
if (arg == "--serve") serve = true;
else if (arg == "--max-sessions" && i + 1 < argc) {
char* end = nullptr;
const auto count = std::strtoul(argv[++i], &end, 10);
// This is a concurrency limit, not a cumulative session budget.
// P6.5 deliberately supports one active rendering session.
if (*end || count != 1) return 71;
// Opt-in concurrency is only for an offscreen unix supervisor: each client gets
// a process and independent backend state. In-process displays remain singleton.
if (*end || count < 1 || count > 64) return 71;
maxSessions = count;
} else if (endpoint.empty() && arg.compare(0, 2, "--") != 0) endpoint = arg;
else return 71;
}
if (endpoint.empty()) if (const char* value = std::getenv("MOBILEGL_IPC_ENDPOINT")) endpoint = value;
if (endpoint.empty()) return 71;
const bool tcpEndpoint = endpoint.compare(0, 6, "tcp://") == 0;
if (maxSessions > 1 && (!serve || tcpEndpoint)) return 71;
int listener = -1;
if (SocketTransport::Listen(endpoint, &listener) != MOBILEGL_OK) return 72;
WireLogError("MG_Remote server: pid=%d listening on %s", static_cast<int>(::getpid()), endpoint.c_str());
@@ -1489,7 +1493,6 @@ extern "C" __attribute__((visibility("default"))) int mobilegl_server_main(int a
reserved, rule);
}
unsigned long sessionsFaulted = 0;
const bool tcpEndpoint = endpoint.compare(0, 6, "tcp://") == 0;
const auto knobs = Server::PreAuthKnobs::FromEnvironment();
if (tcpEndpoint && serve) {
// PH-7 (5): the TCP supervisor reads and authenticates every first frame before it forks.
@@ -1500,8 +1503,20 @@ extern "C" __attribute__((visibility("default"))) int mobilegl_server_main(int a
return failed;
}
// From here: the unix endpoint (--serve or not), and the single-session TCP shape.
pid_t active = -1;
const auto reap = [&]() { return ReapActiveSession(active, sessionsFaulted); };
std::vector<pid_t> active;
active.reserve(maxSessions);
const auto reap = [&]() {
bool reaped = false;
for (auto child = active.begin(); child != active.end();) {
if (ReapActiveSession(*child, sessionsFaulted)) {
child = active.erase(child);
reaped = true;
} else {
++child;
}
}
return reaped;
};
// P11 PAIR: a unix client's two connections, paired by the nonce each presents first. The
// connections not yet paired stay in it between calls.
std::unique_ptr<Server::PairAcceptor> pairs;
@@ -1540,12 +1555,12 @@ extern "C" __attribute__((visibility("default"))) int mobilegl_server_main(int a
// exit_group closes files just before waitpid can observe the exit.
// Allow that small scheduling window; a live peer remains Busy.
const auto reapDeadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(50);
while (active > 0 && std::chrono::steady_clock::now() < reapDeadline) {
while (active.size() >= maxSessions && std::chrono::steady_clock::now() < reapDeadline) {
if (reap()) break;
::usleep(1000);
}
if (active > 0) {
RefuseBusy(*control, "one session is already active");
if (active.size() >= maxSessions) {
RefuseBusy(*control, maxSessions == 1 ? "one session is already active" : "the session limit is reached");
continue;
}
std::fflush(nullptr);
@@ -1565,7 +1580,7 @@ extern "C" __attribute__((visibility("default"))) int mobilegl_server_main(int a
RefuseBusy(*control, "could not create session child");
continue;
}
active = child;
active.push_back(child);
control->CloseLocalCopy();
}
LogSupervisorSummary(sessionsFaulted);
@@ -119,14 +119,105 @@ namespace {
r.applied = Emit(client, P::MGPWireOp::MemoryBarrier, &barrier, sizeof(barrier)) ? 1 : 0;
}
void ApplyOneSharedRecord(Remote::Client::ClientSession& client, PeerReport& r) {
const auto* link = client.DataLink();
r.status = link != nullptr && link->Capabilities().SharedMapping ? 1 : 0;
ApplyOneRecord(client, r);
}
void LatchTheSession(Remote::Client::ClientSession& client, PeerReport& r) {
// A CAMetalLayer* on the wire: Fatal{UnmigratedSurface, "MetalLayer@P12"}, latched.
(void)SendSurfaceOp(client, r, static_cast<Uint8>(::MobileGL::Wire::SurfaceOpKind::CreateWindowSurface),
static_cast<Uint8>(::MobileGL::Wire::WindowKind::MetalLayer), 0x1234);
}
bool LaunchUnixSupervisor(const std::string& label, unsigned maxSessions, ServerProcess* server) {
server->endpoint = "@mgl-unix-multi-" + label + "-" + std::to_string(::getpid());
server->logBase = "/tmp/mgl-unix-multi-" + label + "-" + std::to_string(::getpid()) + ".log";
::setenv("MOBILEGL_LOG_FILE_PATH", server->logBase.c_str(), 1);
PinHeadlessEgl();
Debug::TruncateRoleLogs(server->logBase.c_str());
Remote::Server::LaunchedServer launched;
std::vector<std::string> args{"--serve"};
if (maxSessions > 0) {
args.push_back("--max-sessions");
args.push_back(std::to_string(maxSessions));
}
if (Remote::Server::LaunchServerWithArgs(ServerImage(), server->endpoint, args, &launched) != MOBILEGL_OK)
return false;
server->pid = launched.pid;
bool listening = false;
(void)WaitFor([&] {
if (server->Log().find("listening on") != std::string::npos) return listening = true;
return server->WaitExit(0) && server->pid <= 0;
}, 10000);
// The listener takes the native address; the client control selector includes its
// transport prefix. Bare @name is not a supported MOBILEGL_IPC_CONTROL spelling.
if (listening) server->endpoint = "unix:" + server->endpoint;
return listening;
}
struct HeldPeer {
pid_t pid = -1;
void Release() {
ReleaseHeldPeer(pid);
pid = -1;
}
~HeldPeer() { Release(); }
};
bool UnixChildReaped(const ServerProcess& server, Uint32 child) {
return server.Log().find("session pid=" + std::to_string(child) + " reaped") != std::string::npos;
}
} // namespace
TEST(UnixSupervisor, ConcurrentSharedMemoryClientsHaveSeparateWorkersAndAnExitedSlotIsReused) {
ServerProcess server;
ASSERT_TRUE(LaunchUnixSupervisor("two", 2, &server)) << server.Log();
HeldPeer first;
const PeerReport a = RunPeer(server.endpoint, ApplyOneSharedRecord, false, 30000, 30000, &first.pid);
ASSERT_EQ(a.started, 1) << a.note << "\n" << server.Log();
ASSERT_EQ(a.applied, 1) << a.note;
EXPECT_EQ(a.status, 1) << "the unix client did not map shared segments";
HeldPeer second;
const PeerReport b = RunPeer(server.endpoint, ApplyOneSharedRecord, false, 30000, 30000, &second.pid);
ASSERT_EQ(b.started, 1) << b.note << "\n" << server.Log();
ASSERT_EQ(b.applied, 1) << b.note;
EXPECT_EQ(b.status, 1) << "the concurrent client did not map shared segments";
EXPECT_NE(a.serverPid, b.serverPid) << "concurrent clients shared process-global GL state";
EXPECT_NE(a.serverPid, static_cast<Uint32>(server.pid));
EXPECT_NE(b.serverPid, static_cast<Uint32>(server.pid));
const PeerReport excess = RunPeer(server.endpoint, ApplyOneRecord, true, 15000);
EXPECT_EQ(excess.started, 0) << "the configured live-session limit was ignored";
EXPECT_NE(server.Log().find("Refuse{Busy} the session limit is reached"), std::string::npos) << server.Log();
// The second worker must not hold the first peer's connection open after its EOF. This also
// proves CloseLocalCopy and CloseInForkedChild cover descriptors inherited by each worker.
first.Release();
ASSERT_TRUE(WaitFor([&] { return UnixChildReaped(server, a.serverPid); }, 15000)) << server.Log();
EXPECT_EQ(::kill(static_cast<pid_t>(b.serverPid), 0), 0) << "reaping one session killed its sibling";
const PeerReport replacement = RunPeer(server.endpoint, ApplyOneSharedRecord, true);
EXPECT_EQ(replacement.started, 1) << replacement.note << "\n" << server.Log();
EXPECT_EQ(replacement.applied, 1) << replacement.note;
EXPECT_EQ(replacement.status, 1);
EXPECT_NE(replacement.serverPid, b.serverPid);
second.Release();
EXPECT_TRUE(WaitFor([&] { return UnixChildReaped(server, b.serverPid); }, 15000)) << server.Log();
}
TEST(UnixSupervisor, ConcurrencyRequiresAnExplicitLimit) {
ServerProcess server;
ASSERT_TRUE(LaunchUnixSupervisor("default", 0, &server)) << server.Log();
HeldPeer first;
const PeerReport a = RunPeer(server.endpoint, ApplyOneRecord, false, 30000, 30000, &first.pid);
ASSERT_EQ(a.started, 1) << a.note << "\n" << server.Log();
const PeerReport b = RunPeer(server.endpoint, ApplyOneRecord, true, 15000);
EXPECT_EQ(b.started, 0);
EXPECT_NE(server.Log().find("Refuse{Busy} one session is already active"), std::string::npos) << server.Log();
}
// D5, THE HEADLINE. Three sessions, one process: a healthy one, one that latches, and a healthy one
// after it. Red with ResetSessionLatch removed from the supervisor's reap: session C's Welcome arrives
// but the latch still stands, so its apply thread declines the ring (applied 0) and its EGL ops are
@@ -0,0 +1,49 @@
# 在宿主 APK 中嵌入 split server
宿主应用可直接加载 `libMobileGL.so`,把自己持有的 `ANativeWindow` 交给 server。
窗口、输入和 Android Service 的生命周期由宿主 APK 管理;不需要安装 MobileGL plugin APK。
公共接口是 [`EmbeddedServer.h`](../../../MobileGL/MG_Remote/Server/EmbeddedServer.h),
只使用 C 类型,也可通过 `dlsym` 调用。构建必须开启 `MOBILEGL_BUILD_DISAGGREGATED`。
## 有窗口的合成器服务
在加载库前设置 `MOBILEGL_IPC_ROLE=server`、`MOBILEGL_IPC_DIAL=no`,清除
`MOBILEGL_TRANSPORT`、`MOBILEGL_IPC_SERVER_PATH`、`MOBILEGL_IPC_RING_MB` 和
`MOBILEGL_IPC_STAGE_MB`。建议放在宿主 APK 的独立服务进程中,以免渲染故障结束 UI 进程。
1. 调用 `mobilegl_server_display_install_android(request_geometry, user)`。
回调可为空;有回调时,它在 apply 线程运行,必须快速返回,把尺寸变更交给宿主线程。
2. 从宿主的 `Surface` 获取 `ANativeWindow`,调用
`mobilegl_server_display_attach_android(window, width, height)`。
server 独立持有引用,调用者随后释放自己的引用。同一窗口再次 attach 只更新尺寸。
3. 在服务线程调用 `mobilegl_server_serve_inprocess("@anland-mobilegl-compositor")`。
这会在当前进程接受一个 client 会话。AF_UNIX 控制连接用 `SCM_RIGHTS` 传递共享内存段,
数据通过 SHM 环传输。客户端设置 `MOBILEGL_TRANSPORT=spawn`、
`MOBILEGL_IPC_CONTROL=unix:@anland-mobilegl-compositor`、`MOBILEGL_IPC_DATA=shm` 和
`MOBILEGL_IPC_SURFACE=server`。
4. Surface 销毁时调用 `mobilegl_server_display_detach(3000)`。
返回值 `0/1/2/3` 分别为无窗口、直接释放、会话释放后完成、超时。
超时会保留引用,直到 apply 线程停止使用窗口;当前失窗会结束活动会话。
5. 服务停止时调用 `mobilegl_server_stop_inprocess()`,等待 serve 线程返回,再调用
`mobilegl_server_display_uninstall(3000)`。几何回调的 user 对象应至少活到此时。
`width/height` 为零时,Android attach 从 `ANativeWindow` 查询尺寸。几何回调收到正尺寸表示
请求固定缓冲尺寸,收到 `0/0` 表示恢复宿主布局尺寸;宿主应通过后续 attach 报告实际结果。
安装、attach、detach 和 uninstall 由宿主串行调用。库拒绝替换已安装的 display 或尚未结束
的旧窗口租约,避免晚到的释放使用另一组引用回调。
## 并发的离屏应用服务
一个进程仍只有一套 backend/GL 状态。有窗口的服务只接一个合成器;其他 EGL client 可连接
宿主 APK 打包的 `libMobileGLServer.so` 可执行文件,由另一个服务进程负责启动和结束:
```text
<nativeLibraryDir>/libMobileGLServer.so @anland-mobilegl --serve --max-sessions 16
```
`--max-sessions` 是活动会话上限,接受 `1..64`,默认仍为 `1`。大于 `1` 只适用于
AF_UNIX 的 `--serve` 离屏 supervisor。每个 client 单独 fork worker,拥有独立 backend,
共享内存与其他会话隔离;worker 退出后归还名额,supervisor 结束时所有 worker 一起结束。
这些 worker 没有 display,客户端应清除 `MOBILEGL_IPC_SURFACE`,由客户端 WSI 提交窗口图像。
进程隔离解决多个应用同时连接的问题;同一应用中的真正多 context/share-group 支持仍需另行实现。