From 2f64ede8c65e9577676e658e82017672ad58f03e Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 1 Oct 2026 12:53:27 -0400 Subject: [PATCH] [Feature] (Remote): serve concurrent shared-memory clients in isolated workers --- MobileGL/MG_Remote/Server/ServerMain.cpp | 35 +++++-- MobileGL/MG_Test/Wire/InProcessServerTest.cpp | 91 +++++++++++++++++++ docs/Disaggregated/guide/embedded-server.md | 49 ++++++++++ 3 files changed, 165 insertions(+), 10 deletions(-) create mode 100644 docs/Disaggregated/guide/embedded-server.md diff --git a/MobileGL/MG_Remote/Server/ServerMain.cpp b/MobileGL/MG_Remote/Server/ServerMain.cpp index b3a38187..228e1010 100644 --- a/MobileGL/MG_Remote/Server/ServerMain.cpp +++ b/MobileGL/MG_Remote/Server/ServerMain.cpp @@ -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(::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 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 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); diff --git a/MobileGL/MG_Test/Wire/InProcessServerTest.cpp b/MobileGL/MG_Test/Wire/InProcessServerTest.cpp index f1a0721c..1d5f49e7 100644 --- a/MobileGL/MG_Test/Wire/InProcessServerTest.cpp +++ b/MobileGL/MG_Test/Wire/InProcessServerTest.cpp @@ -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(::MobileGL::Wire::SurfaceOpKind::CreateWindowSurface), static_cast(::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 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(server.pid)); + EXPECT_NE(b.serverPid, static_cast(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(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 diff --git a/docs/Disaggregated/guide/embedded-server.md b/docs/Disaggregated/guide/embedded-server.md new file mode 100644 index 00000000..e840777e --- /dev/null +++ b/docs/Disaggregated/guide/embedded-server.md @@ -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 +/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 支持仍需另行实现。