mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
[Feature] (Remote): serve concurrent shared-memory clients in isolated workers
This commit is contained in:
@@ -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 支持仍需另行实现。
|
||||
Reference in New Issue
Block a user