From a5b463a5e8126cbf48d72b38b3cc2271e851c66f Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 1 Oct 2026 12:31:16 -0400 Subject: [PATCH] [Feature] (Remote): expose a package-neutral embedded display server API --- CMakeLists.txt | 2 + MobileGL/MG_Remote/Server/EmbeddedServer.cpp | 90 ++++++++++++++++++++ MobileGL/MG_Remote/Server/EmbeddedServer.h | 79 +++++++++++++++++ MobileGL/MG_Test/Wire/ServerDisplayTest.cpp | 72 ++++++++++++++++ 4 files changed, 243 insertions(+) create mode 100644 MobileGL/MG_Remote/Server/EmbeddedServer.cpp create mode 100644 MobileGL/MG_Remote/Server/EmbeddedServer.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 2012fa7b..d54df6ea 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -677,6 +677,8 @@ if (MOBILEGL_BUILD_DISAGGREGATED) # (the display Activity's SurfaceView on Android; nobody installs one on the host, so a # ServerOwned request is refused by name there). MobileGL/MG_Remote/Server/ServerDisplay.cpp + # Package-neutral C ABI for an APK that owns the server and its ANativeWindow. + MobileGL/MG_Remote/Server/EmbeddedServer.cpp # P7 gate 5 (g5-msprobe): the Magma wire arm's two multisample depth/stencil resolve # arms and the probe that measures the first against the second, shared by the # renderer's bring-up and the Vulkan POST. Wire-only - the monolith records neither arm - diff --git a/MobileGL/MG_Remote/Server/EmbeddedServer.cpp b/MobileGL/MG_Remote/Server/EmbeddedServer.cpp new file mode 100644 index 00000000..ef7e0df5 --- /dev/null +++ b/MobileGL/MG_Remote/Server/EmbeddedServer.cpp @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: LGPL-3.0-only +#include "EmbeddedServer.h" +#include "ServerDisplay.h" + +#if defined(__ANDROID__) +#include +#endif + +namespace { + using namespace MobileGL::MG_Remote::Server; + + int DetachResult(ServerWindowDetach result) { + switch (result) { + case ServerWindowDetach::NoWindow: return MOBILEGL_SERVER_WINDOW_NO_WINDOW; + case ServerWindowDetach::Released: return MOBILEGL_SERVER_WINDOW_RELEASED; + case ServerWindowDetach::ReleasedBySession: return MOBILEGL_SERVER_WINDOW_RELEASED_BY_SESSION; + case ServerWindowDetach::TimedOut: return MOBILEGL_SERVER_WINDOW_TIMED_OUT; + } + return MOBILEGL_SERVER_WINDOW_TIMED_OUT; + } +} + +extern "C" int mobilegl_server_display_install(const struct mobilegl_server_display_hooks* hooks) { + if (hooks == nullptr || hooks->acquire == nullptr || hooks->release == nullptr) + return MOBILEGL_SERVER_DISPLAY_INVALID_ARGUMENT; + auto& display = ServerDisplayInstance(); + if (display.HasDisplay() || display.Attached() || display.Leased()) + return MOBILEGL_SERVER_DISPLAY_ALREADY_INSTALLED; + ServerDisplayHooks nativeHooks; + nativeHooks.acquire = hooks->acquire; + nativeHooks.release = hooks->release; + nativeHooks.requestGeometry = hooks->request_geometry; + nativeHooks.user = hooks->user; + display.Install(nativeHooks); + return MOBILEGL_SERVER_DISPLAY_OK; +} + +extern "C" int mobilegl_server_display_install_android(mobilegl_server_geometry_callback request_geometry, void* user) { +#if defined(__ANDROID__) + const auto androidHooks = AndroidNativeWindowHooks(request_geometry, user); + const mobilegl_server_display_hooks hooks{androidHooks.acquire, androidHooks.release, + androidHooks.requestGeometry, androidHooks.user}; + return mobilegl_server_display_install(&hooks); +#else + (void)request_geometry; + (void)user; + return MOBILEGL_SERVER_DISPLAY_UNSUPPORTED_PLATFORM; +#endif +} + +extern "C" int mobilegl_server_display_attach(void* window, uint32_t width, uint32_t height) { + if (window == nullptr) return MOBILEGL_SERVER_DISPLAY_INVALID_ARGUMENT; + auto& display = ServerDisplayInstance(); + if (!display.HasDisplay()) return MOBILEGL_SERVER_DISPLAY_NOT_INSTALLED; + display.Attach(window, width, height); + return MOBILEGL_SERVER_DISPLAY_OK; +} + +extern "C" int mobilegl_server_display_attach_android(struct ANativeWindow* window, uint32_t width, uint32_t height) { +#if defined(__ANDROID__) + if (window == nullptr) return MOBILEGL_SERVER_DISPLAY_INVALID_ARGUMENT; + if (width == 0) { + const int32_t actual = ANativeWindow_getWidth(window); + if (actual > 0) width = static_cast(actual); + } + if (height == 0) { + const int32_t actual = ANativeWindow_getHeight(window); + if (actual > 0) height = static_cast(actual); + } + return mobilegl_server_display_attach(window, width, height); +#else + (void)window; + (void)width; + (void)height; + return MOBILEGL_SERVER_DISPLAY_UNSUPPORTED_PLATFORM; +#endif +} + +extern "C" int mobilegl_server_display_detach(uint32_t timeout_ms) { + return DetachResult(ServerDisplayInstance().Detach(timeout_ms)); +} + +extern "C" int mobilegl_server_display_uninstall(uint32_t timeout_ms) { + auto& display = ServerDisplayInstance(); + const int result = DetachResult(display.Detach(timeout_ms)); + // Detach has already removed the attached window even if its lease is still live. Uninstall + // keeps the original hooks for EndLease's deferred release, without another timeout budget. + display.Uninstall(0); + return result; +} diff --git a/MobileGL/MG_Remote/Server/EmbeddedServer.h b/MobileGL/MG_Remote/Server/EmbeddedServer.h new file mode 100644 index 00000000..27c46473 --- /dev/null +++ b/MobileGL/MG_Remote/Server/EmbeddedServer.h @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// Public C interface for an application that owns a MobileGL split server. +#pragma once + +#include + +#if defined(_WIN32) +#define MOBILEGL_SERVER_EXPORT +#else +#define MOBILEGL_SERVER_EXPORT __attribute__((visibility("default"))) +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +struct ANativeWindow; + +typedef void (*mobilegl_server_geometry_callback)(void* user, uint32_t width, uint32_t height); + +// Opaque-window hooks also let non-Android hosts embed the display without depending on +// MobileGL's C++ headers. Both reference callbacks are required. Geometry may be null. +struct mobilegl_server_display_hooks { + void (*acquire)(void* user, void* window); + void (*release)(void* user, void* window); + mobilegl_server_geometry_callback request_geometry; + void* user; +}; + +enum mobilegl_server_display_result { + MOBILEGL_SERVER_DISPLAY_OK = 0, + MOBILEGL_SERVER_DISPLAY_INVALID_ARGUMENT = -1, + MOBILEGL_SERVER_DISPLAY_ALREADY_INSTALLED = -2, + MOBILEGL_SERVER_DISPLAY_NOT_INSTALLED = -3, + MOBILEGL_SERVER_DISPLAY_UNSUPPORTED_PLATFORM = -4 +}; + +enum mobilegl_server_window_detach { + MOBILEGL_SERVER_WINDOW_NO_WINDOW = 0, + MOBILEGL_SERVER_WINDOW_RELEASED = 1, + MOBILEGL_SERVER_WINDOW_RELEASED_BY_SESSION = 2, + MOBILEGL_SERVER_WINDOW_TIMED_OUT = 3 +}; + +// Install once before serving; owner lifecycle calls must be serialized. A live display or +// an old lease awaiting release refuses a replacement, so its window always uses its original +// reference hooks. The geometry callback runs on the apply thread and must return promptly. +// A positive size requests fixed buffer dimensions; 0/0 requests the host's layout dimensions. +// Report the resulting dimensions with attach, for example from SurfaceHolder.surfaceChanged. +// Callback user data must live until uninstall has returned and any timed-out lease has ended. +MOBILEGL_SERVER_EXPORT int mobilegl_server_display_install(const struct mobilegl_server_display_hooks* hooks); +MOBILEGL_SERVER_EXPORT int mobilegl_server_display_install_android(mobilegl_server_geometry_callback request_geometry, + void* user); + +// Attach retains its own reference. Attaching the same window updates its dimensions without +// another retain. A different window detaches the previous one first (up to 3 seconds). +// Android attach uses ANativeWindow_getWidth/Height for zero dimensions; the caller may release +// its ANativeWindow_fromSurface reference immediately after this call. +MOBILEGL_SERVER_EXPORT int mobilegl_server_display_attach(void* window, uint32_t width, uint32_t height); +MOBILEGL_SERVER_EXPORT int mobilegl_server_display_attach_android(struct ANativeWindow* window, uint32_t width, + uint32_t height); + +// Returns mobilegl_server_window_detach. A timeout never drops a reference while the backend +// is rendering: release happens when the apply thread ends its lease. Surface loss currently +// ends the live split session; a fresh client session can use a newly attached window. +MOBILEGL_SERVER_EXPORT int mobilegl_server_display_detach(uint32_t timeout_ms); +MOBILEGL_SERVER_EXPORT int mobilegl_server_display_uninstall(uint32_t timeout_ms); + +// Blocking serve on the caller's thread; stop is non-blocking and safe on the UI thread. +// Endpoint: filesystem path, @abstract AF_UNIX name (SCM_RIGHTS + shared-memory rings), or +// tcp://host:port. One client session per process; independent workers need distinct endpoints. +// Before loading libMobileGL set MOBILEGL_IPC_ROLE=server and MOBILEGL_IPC_DIAL=no, and unset +// MOBILEGL_TRANSPORT, MOBILEGL_IPC_SERVER_PATH, MOBILEGL_IPC_RING_MB, MOBILEGL_IPC_STAGE_MB. +MOBILEGL_SERVER_EXPORT int mobilegl_server_serve_inprocess(const char* endpoint); +MOBILEGL_SERVER_EXPORT void mobilegl_server_stop_inprocess(void); + +#ifdef __cplusplus +} +#endif diff --git a/MobileGL/MG_Test/Wire/ServerDisplayTest.cpp b/MobileGL/MG_Test/Wire/ServerDisplayTest.cpp index 9c84ac4c..d4ef8f6e 100644 --- a/MobileGL/MG_Test/Wire/ServerDisplayTest.cpp +++ b/MobileGL/MG_Test/Wire/ServerDisplayTest.cpp @@ -19,6 +19,7 @@ // wait by name. #include +#include #include @@ -118,6 +119,77 @@ namespace { } // namespace +// The public embedding ABI must preserve the same ownership rules for an APK that supplies its +// own Surface, including a late apply-thread release after its UI thread has stopped waiting. +class EmbeddedServerTest : public ::testing::Test { +protected: + FakePlatform platform; + + mobilegl_server_display_hooks Hooks() { + return {&FakeAcquire, &FakeRelease, &FakeGeometry, &platform}; + } + void TearDown() override { + ServerDisplayInstance().EndLease(this); + (void)mobilegl_server_display_uninstall(0); + } +}; + +TEST_F(EmbeddedServerTest, AHostsReferenceIsRetainedAndGeometryUpdatesDoNotRetainAgain) { + const auto hooks = Hooks(); + ASSERT_EQ(mobilegl_server_display_install(&hooks), MOBILEGL_SERVER_DISPLAY_OK); + ASSERT_EQ(mobilegl_server_display_attach(FakeWindow(0), 32, 24), MOBILEGL_SERVER_DISPLAY_OK); + ASSERT_EQ(mobilegl_server_display_attach(FakeWindow(0), 64, 48), MOBILEGL_SERVER_DISPLAY_OK); + EXPECT_EQ(platform.acquires.load(), 1); + ServerWindowLease lease; + ASSERT_EQ(ServerDisplayInstance().AcquireFor(64, 48, 100, this, nullptr, nullptr, nullptr, &lease), + ServerWindowAcquire::Acquired); + EXPECT_EQ(platform.lastWidth, 64u); + EXPECT_EQ(platform.lastHeight, 48u); + EXPECT_EQ(lease.width, 64u); + EXPECT_EQ(lease.height, 48u); + ServerDisplayInstance().EndLease(this); + EXPECT_EQ(mobilegl_server_display_detach(100), MOBILEGL_SERVER_WINDOW_RELEASED); + EXPECT_EQ(platform.releases.load(), 1); + EXPECT_EQ(mobilegl_server_display_uninstall(100), MOBILEGL_SERVER_WINDOW_NO_WINDOW); +} + +TEST_F(EmbeddedServerTest, ATimeoutKeepsTheOriginalHooksUntilTheBackendLetsGo) { + const auto hooks = Hooks(); + ASSERT_EQ(mobilegl_server_display_install(&hooks), MOBILEGL_SERVER_DISPLAY_OK); + ASSERT_EQ(mobilegl_server_display_attach(FakeWindow(0), 64, 48), MOBILEGL_SERVER_DISPLAY_OK); + ServerWindowLease lease; + // The holder deliberately has no lost callback: it is a wedged apply thread. Uninstall must + // remain bounded and retain the window until this holder explicitly releases its lease. + ASSERT_EQ(ServerDisplayInstance().AcquireFor(0, 0, 100, this, nullptr, nullptr, nullptr, &lease), + ServerWindowAcquire::Acquired); + EXPECT_EQ(mobilegl_server_display_uninstall(1), MOBILEGL_SERVER_WINDOW_TIMED_OUT); + EXPECT_FALSE(ServerDisplayInstance().HasDisplay()); + EXPECT_EQ(platform.releases.load(), 0); + EXPECT_EQ(mobilegl_server_display_install(&hooks), MOBILEGL_SERVER_DISPLAY_ALREADY_INSTALLED); + ServerDisplayInstance().EndLease(this); + EXPECT_EQ(platform.releases.load(), 1); + EXPECT_EQ(mobilegl_server_display_install(&hooks), MOBILEGL_SERVER_DISPLAY_OK); +} + +TEST_F(EmbeddedServerTest, AnOwnerCannotReplaceLiveReferenceHooksOrAttachBeforeInstall) { + const auto hooks = Hooks(); + EXPECT_EQ(mobilegl_server_display_attach(FakeWindow(0), 64, 48), MOBILEGL_SERVER_DISPLAY_NOT_INSTALLED); + EXPECT_EQ(mobilegl_server_display_install(nullptr), MOBILEGL_SERVER_DISPLAY_INVALID_ARGUMENT); + mobilegl_server_display_hooks incomplete{}; + EXPECT_EQ(mobilegl_server_display_install(&incomplete), MOBILEGL_SERVER_DISPLAY_INVALID_ARGUMENT); + ASSERT_EQ(mobilegl_server_display_install(&hooks), MOBILEGL_SERVER_DISPLAY_OK); + EXPECT_EQ(mobilegl_server_display_install(&hooks), MOBILEGL_SERVER_DISPLAY_ALREADY_INSTALLED); + EXPECT_EQ(mobilegl_server_display_attach(nullptr, 64, 48), MOBILEGL_SERVER_DISPLAY_INVALID_ARGUMENT); +} + +#if !defined(__ANDROID__) +TEST_F(EmbeddedServerTest, AndroidWindowsAreRefusedByNameOnOtherPlatforms) { + EXPECT_EQ(mobilegl_server_display_install_android(nullptr, nullptr), MOBILEGL_SERVER_DISPLAY_UNSUPPORTED_PLATFORM); + EXPECT_EQ(mobilegl_server_display_attach_android(nullptr, 64, 48), MOBILEGL_SERVER_DISPLAY_UNSUPPORTED_PLATFORM); + EXPECT_FALSE(ServerDisplayInstance().HasDisplay()); +} +#endif + // Nobody installed a display: HasDisplay is false and a wait for a window answers NoDisplay at once // - the host server's and the exec'd supervisor's shape, and what makes a ServerOwned request a named // refusal there.