From bd4c5b5770c756d8895390cf994f59504abbd10c Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Sat, 3 Oct 2026 01:34:23 -0400 Subject: [PATCH] [Test] (Gbm): drive mobilegl_gbm through the system loader the way Xwayland's glamor does --- MobileGL/MG_Test/CMakeLists.txt | 2 + MobileGL/MG_Test/Gbm/CMakeLists.txt | 25 +++ .../MG_Test/Gbm/FakeSharedImageLibrary.cpp | 105 ++++++++++ MobileGL/MG_Test/Gbm/GbmGlamorTest.cpp | 182 ++++++++++++++++++ 4 files changed, 314 insertions(+) create mode 100644 MobileGL/MG_Test/Gbm/CMakeLists.txt create mode 100644 MobileGL/MG_Test/Gbm/FakeSharedImageLibrary.cpp create mode 100644 MobileGL/MG_Test/Gbm/GbmGlamorTest.cpp diff --git a/MobileGL/MG_Test/CMakeLists.txt b/MobileGL/MG_Test/CMakeLists.txt index c9056a72..b058fd4d 100644 --- a/MobileGL/MG_Test/CMakeLists.txt +++ b/MobileGL/MG_Test/CMakeLists.txt @@ -105,4 +105,6 @@ endif() # is only registered there. Nothing under MG_Remote is compiled otherwise. if (MOBILEGL_BUILD_DISAGGREGATED) add_subdirectory(Wire) + # The GBM backend glamor uses (Xwayland on MobileGL), through the system's loader. + add_subdirectory(Gbm) endif() diff --git a/MobileGL/MG_Test/Gbm/CMakeLists.txt b/MobileGL/MG_Test/Gbm/CMakeLists.txt new file mode 100644 index 00000000..ed27434a --- /dev/null +++ b/MobileGL/MG_Test/Gbm/CMakeLists.txt @@ -0,0 +1,25 @@ +cmake_minimum_required(VERSION 3.14) + +# mobilegl_gbm.so through the system's GBM loader, the calls Xwayland's glamor makes, over a fake of +# libMobileGL's shared-image C ABI. Only where the GBM backend itself is built (gbm headers found). +find_library(MOBILEGL_TEST_GBM_LIBRARY gbm) +if (NOT TARGET mobilegl_gbm OR NOT MOBILEGL_TEST_GBM_LIBRARY) + message(STATUS "MobileGL: GbmGlamorTest not built (no mobilegl_gbm target or no libgbm)") + return() +endif() + +add_library(mobilegl_fake_shared_images MODULE FakeSharedImageLibrary.cpp) +set_target_properties(mobilegl_fake_shared_images PROPERTIES PREFIX "" CXX_VISIBILITY_PRESET hidden) +target_include_directories(mobilegl_fake_shared_images PRIVATE ${MGL_ROOT}/MobileGL/MG_Impl) + +add_executable(GbmGlamorTest GbmGlamorTest.cpp) +add_dependencies(GbmGlamorTest mobilegl_gbm mobilegl_fake_shared_images) +target_include_directories(GbmGlamorTest PRIVATE ${MOBILEGL_GBM_INCLUDE_DIR}) +target_compile_definitions(GbmGlamorTest PRIVATE + MOBILEGL_TEST_GBM_BACKEND_DIR="$" + MOBILEGL_TEST_FAKE_SHARED_IMAGE_LIBRARY="$" +) +target_link_libraries(GbmGlamorTest PRIVATE GTest::gtest_main ${MOBILEGL_TEST_GBM_LIBRARY} ${CMAKE_DL_LIBS}) + +include(GoogleTest) +gtest_discover_tests(GbmGlamorTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit) diff --git a/MobileGL/MG_Test/Gbm/FakeSharedImageLibrary.cpp b/MobileGL/MG_Test/Gbm/FakeSharedImageLibrary.cpp new file mode 100644 index 00000000..8657ede7 --- /dev/null +++ b/MobileGL/MG_Test/Gbm/FakeSharedImageLibrary.cpp @@ -0,0 +1,105 @@ +// MobileGL - MobileGL/MG_Test/Gbm/FakeSharedImageLibrary.cpp +// 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 + +// A stand-in for libMobileGL.so's shared-image C ABI (MG_Impl/SharedImageApi.h), loaded by +// mobilegl_gbm.so through MOBILEGL_GBM_LIBRARY in GbmGlamorTest: images are memfds, identified on +// import by inode as the server identifies its dma-bufs. fake_shared_image_* let the test look in. + +#include "SharedImageApi.h" + +#include +#include +#include + +#include +#include +#include + +#define FAKE_EXPORT extern "C" __attribute__((visibility("default"))) + +namespace { + struct FakeImage { + int fd = -1; + ino_t inode = 0; + uint32_t width = 0, height = 0, fourcc = 0; + int references = 0; + }; + std::mutex g_mutex; + std::map g_images; + uint64_t g_nextId = 1; + int g_imports = 0; + + bool Supported(uint32_t fourcc) { + return fourcc == MOBILEGL_SHARED_IMAGE_FOURCC_ABGR8888 || fourcc == MOBILEGL_SHARED_IMAGE_FOURCC_XBGR8888 || + fourcc == MOBILEGL_SHARED_IMAGE_FOURCC_ARGB8888 || fourcc == MOBILEGL_SHARED_IMAGE_FOURCC_XRGB8888; + } +} // namespace + +FAKE_EXPORT uint32_t mobilegl_shared_image_abi_version(void) { return MOBILEGL_SHARED_IMAGE_ABI_VERSION; } + +FAKE_EXPORT int mobilegl_server_available(void) { return 1; } + +FAKE_EXPORT int mobilegl_shared_image_formats(uint32_t*, uint32_t) { return 4; } + +FAKE_EXPORT int mobilegl_shared_image_allocate(uint32_t width, uint32_t height, uint32_t fourcc, + struct mobilegl_shared_image* out) { + if (width == 0 || height == 0 || !Supported(fourcc) || out == nullptr) return -EINVAL; + const int fd = ::memfd_create("fake-shared-image", MFD_CLOEXEC); + if (fd < 0 || ::ftruncate(fd, static_cast(width) * height * 4) != 0) return -EIO; + struct stat st {}; + ::fstat(fd, &st); + const std::lock_guard lock(g_mutex); + const uint64_t id = g_nextId++; + g_images[id] = FakeImage{fd, st.st_ino, width, height, fourcc, 1}; + out->width = width; + out->height = height; + out->fourcc = fourcc; + out->id = id; + out->fd = ::dup(fd); + out->stride = width * 4; + out->offset = 0; + out->modifier = 0x00ffffffffffffffull; + return 0; +} + +FAKE_EXPORT int mobilegl_shared_image_import(int fd, uint32_t width, uint32_t height, uint32_t fourcc, + uint64_t* out_id) { + struct stat st {}; + if (fd < 0 || ::fstat(fd, &st) != 0 || !Supported(fourcc)) return -EINVAL; + const std::lock_guard lock(g_mutex); + for (auto& [id, image] : g_images) { + if (image.inode != st.st_ino) continue; + if (image.width != width || image.height != height) return -EIO; + ++image.references; + ++g_imports; + if (out_id != nullptr) *out_id = id; + return 0; + } + return -EIO; // not one of "the server's" images +} + +FAKE_EXPORT int mobilegl_shared_image_release(uint64_t id) { + const std::lock_guard lock(g_mutex); + auto it = g_images.find(id); + if (it == g_images.end()) return -EINVAL; + if (--it->second.references == 0) { + ::close(it->second.fd); + g_images.erase(it); + } + return 0; +} + +FAKE_EXPORT int fake_shared_image_live(void) { + const std::lock_guard lock(g_mutex); + return static_cast(g_images.size()); +} + +FAKE_EXPORT int fake_shared_image_imports(void) { + const std::lock_guard lock(g_mutex); + return g_imports; +} diff --git a/MobileGL/MG_Test/Gbm/GbmGlamorTest.cpp b/MobileGL/MG_Test/Gbm/GbmGlamorTest.cpp new file mode 100644 index 00000000..99928042 --- /dev/null +++ b/MobileGL/MG_Test/Gbm/GbmGlamorTest.cpp @@ -0,0 +1,182 @@ +// MobileGL - MobileGL/MG_Test/Gbm/GbmGlamorTest.cpp +// 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 + +// mobilegl_gbm.so THROUGH THE SYSTEM'S GBM LOADER, the way Xwayland's glamor drives it +// (hw/xwayland/xwayland-glamor-gbm.c, 24.1): gbm_create_device on a character device with +// GBM_BACKEND=mobilegl, window pixmaps from gbm_bo_create_with_modifiers2 with the compositor's +// {INVALID, LINEAR} (else gbm_bo_create), their planes exported for EGL and linux-dmabuf, and a +// DRI3 client's buffer imported back with GBM_BO_IMPORT_FD / _FD_MODIFIER. The shared images come +// from a fake of libMobileGL's C ABI (FakeSharedImageLibrary.cpp). + +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +#ifndef DRM_FORMAT_MOD_INVALID +#define DRM_FORMAT_MOD_INVALID 0x00ffffffffffffffULL +#endif +#ifndef DRM_FORMAT_MOD_LINEAR +#define DRM_FORMAT_MOD_LINEAR 0ULL +#endif + +namespace { + using CountFn = int (*)(); + + class GbmGlamor : public ::testing::Test { + protected: + void SetUp() override { + ::setenv("GBM_BACKEND", "mobilegl", 1); + ::setenv("GBM_BACKENDS_PATH", MOBILEGL_TEST_GBM_BACKEND_DIR, 1); + ::setenv("MOBILEGL_GBM_LIBRARY", MOBILEGL_TEST_FAKE_SHARED_IMAGE_LIBRARY, 1); + m_fd = ::open("/dev/null", O_RDWR | O_CLOEXEC); + ASSERT_GE(m_fd, 0); + m_gbm = gbm_create_device(m_fd); + ASSERT_NE(m_gbm, nullptr) << "libgbm did not take the mobilegl backend"; + void* fake = dlopen(MOBILEGL_TEST_FAKE_SHARED_IMAGE_LIBRARY, RTLD_NOW | RTLD_NOLOAD); + ASSERT_NE(fake, nullptr) << "the backend did not load the shared-image library"; + m_live = reinterpret_cast(dlsym(fake, "fake_shared_image_live")); + m_imports = reinterpret_cast(dlsym(fake, "fake_shared_image_imports")); + ASSERT_TRUE(m_live && m_imports); + m_liveAtStart = m_live(); + } + void TearDown() override { + if (m_gbm != nullptr) gbm_device_destroy(m_gbm); + if (m_fd >= 0) ::close(m_fd); + } + + int m_fd = -1; + gbm_device* m_gbm = nullptr; + CountFn m_live = nullptr; + CountFn m_imports = nullptr; + int m_liveAtStart = 0; + }; +} // namespace + +TEST_F(GbmGlamor, TheDeviceIsMobileGLsAndTakesTheFormatsGlamorAsksFor) { + // Xwayland names the screen's GLX vendor after the backend (anything but "drm"). + EXPECT_STREQ(gbm_device_get_backend_name(m_gbm), "mobilegl"); + // gbm_format_for_depth: 24 -> XRGB8888 (ARGB8888 under GLES glamor), 32 -> ARGB8888. + EXPECT_TRUE(gbm_device_is_format_supported(m_gbm, GBM_FORMAT_XRGB8888, GBM_BO_USE_RENDERING)); + EXPECT_TRUE(gbm_device_is_format_supported(m_gbm, GBM_FORMAT_ARGB8888, GBM_BO_USE_RENDERING | GBM_BO_USE_SCANOUT)); + EXPECT_FALSE(gbm_device_is_format_supported(m_gbm, GBM_FORMAT_RGB565, GBM_BO_USE_RENDERING)); +} + +TEST_F(GbmGlamor, AWindowPixmapIsASharedImageWithOneExportablePlane) { + // The compositor's tranche for a MobileGL EGL lists INVALID and LINEAR (implicit modifiers). + const uint64_t modifiers[] = {DRM_FORMAT_MOD_INVALID, DRM_FORMAT_MOD_LINEAR}; + gbm_bo* bo = gbm_bo_create_with_modifiers2(m_gbm, 640, 480, GBM_FORMAT_XRGB8888, modifiers, 2, + GBM_BO_USE_RENDERING | GBM_BO_USE_SCANOUT); + ASSERT_NE(bo, nullptr) << std::strerror(errno); + EXPECT_EQ(m_live(), m_liveAtStart + 1); + EXPECT_EQ(gbm_bo_get_width(bo), 640u); + EXPECT_EQ(gbm_bo_get_height(bo), 480u); + EXPECT_EQ(gbm_bo_get_format(bo), GBM_FORMAT_XRGB8888); + EXPECT_EQ(gbm_bo_get_modifier(bo), DRM_FORMAT_MOD_INVALID) << "the layout is the allocator's"; + // xwl_glamor_gbm_create_pixmap_for_bo / init_buffer_params_with_modifiers: + ASSERT_EQ(gbm_bo_get_plane_count(bo), 1); + const int fd = gbm_bo_get_fd_for_plane(bo, 0); + ASSERT_GE(fd, 0); + EXPECT_NE(gbm_bo_get_handle_for_plane(bo, 0).u32, 0u); + EXPECT_GE(gbm_bo_get_stride_for_plane(bo, 0), 640u * 4u); + EXPECT_EQ(gbm_bo_get_offset(bo, 0), 0u); + // Exports are duplicates of one dma-buf: the same inode every time. + const int again = gbm_bo_get_fd(bo); + struct stat a {}, b {}; + ASSERT_EQ(::fstat(fd, &a), 0); + ASSERT_EQ(::fstat(again, &b), 0); + EXPECT_EQ(a.st_ino, b.st_ino); + ::close(fd); + ::close(again); + gbm_bo_destroy(bo); + EXPECT_EQ(m_live(), m_liveAtStart) << "destroying the bo released the image"; +} + +TEST_F(GbmGlamor, AModifierListWithoutInvalidIsRefusedAndGlamorsFallbackWorks) { + const uint64_t linearOnly[] = {DRM_FORMAT_MOD_LINEAR}; + EXPECT_EQ(gbm_bo_create_with_modifiers2(m_gbm, 64, 64, GBM_FORMAT_XRGB8888, linearOnly, 1, GBM_BO_USE_RENDERING), + nullptr); + // glamor's implicit-modifier fallback. + gbm_bo* bo = gbm_bo_create(m_gbm, 64, 64, GBM_FORMAT_XRGB8888, GBM_BO_USE_RENDERING); + ASSERT_NE(bo, nullptr); + EXPECT_EQ(gbm_bo_get_modifier(bo), DRM_FORMAT_MOD_INVALID); + gbm_bo_destroy(bo); + // ...which with a LINEAR-only tranche asks for a linear layout, and that no shared image promises. + EXPECT_EQ(gbm_bo_create(m_gbm, 64, 64, GBM_FORMAT_XRGB8888, GBM_BO_USE_RENDERING | GBM_BO_USE_LINEAR), nullptr); +} + +TEST_F(GbmGlamor, ADri3ClientsBufferImportsBackAsTheSameImage) { + // The GLX client's side: an image it allocated (here through the same ABI) handed over as a + // dma-buf with PixmapFromBuffer(s), modifier INVALID. + gbm_bo* clientBo = gbm_bo_create(m_gbm, 320, 200, GBM_FORMAT_XRGB8888, GBM_BO_USE_RENDERING); + ASSERT_NE(clientBo, nullptr); + const int fd = gbm_bo_get_fd(clientBo); + const int importsBefore = m_imports(); + + // glamor_pixmap_from_fds, modifier INVALID: GBM_BO_IMPORT_FD with the depth's format. + gbm_import_fd_data plain{}; + plain.fd = fd; + plain.width = 320; + plain.height = 200; + plain.stride = gbm_bo_get_stride(clientBo); + plain.format = GBM_FORMAT_XRGB8888; + gbm_bo* imported = gbm_bo_import(m_gbm, GBM_BO_IMPORT_FD, &plain, GBM_BO_USE_RENDERING); + ASSERT_NE(imported, nullptr); + EXPECT_EQ(m_imports(), importsBefore + 1); + // ...and re-exported for EGL_LINUX_DMA_BUF_EXT: still that image. + const int reexport = gbm_bo_get_fd_for_plane(imported, 0); + struct stat a {}, b {}; + ::fstat(fd, &a); + ::fstat(reexport, &b); + EXPECT_EQ(a.st_ino, b.st_ino); + ::close(reexport); + + // A client that sent an explicit modifier list: GBM_BO_IMPORT_FD_MODIFIER. + gbm_import_fd_modifier_data withModifier{}; + withModifier.width = 320; + withModifier.height = 200; + withModifier.format = GBM_FORMAT_XRGB8888; + withModifier.num_fds = 1; + withModifier.fds[0] = fd; + withModifier.strides[0] = static_cast(gbm_bo_get_stride(clientBo)); + withModifier.modifier = DRM_FORMAT_MOD_INVALID; + gbm_bo* importedModifier = gbm_bo_import(m_gbm, GBM_BO_IMPORT_FD_MODIFIER, &withModifier, GBM_BO_USE_RENDERING); + ASSERT_NE(importedModifier, nullptr); + + // A size that disagrees with the image is refused. + plain.width = 321; + EXPECT_EQ(gbm_bo_import(m_gbm, GBM_BO_IMPORT_FD, &plain, GBM_BO_USE_RENDERING), nullptr); + + gbm_bo_destroy(importedModifier); + gbm_bo_destroy(imported); + gbm_bo_destroy(clientBo); + ::close(fd); + EXPECT_EQ(m_live(), m_liveAtStart) << "every reference was dropped"; +} + +TEST_F(GbmGlamor, AForeignDmaBufIsRefused) { + // A buffer the MobileGL server never allocated (another driver's client) cannot become a pixmap. + const int foreign = ::memfd_create("foreign", MFD_CLOEXEC); + ASSERT_GE(foreign, 0); + ASSERT_EQ(::ftruncate(foreign, 64 * 64 * 4), 0); + gbm_import_fd_data data{}; + data.fd = foreign; + data.width = 64; + data.height = 64; + data.stride = 256; + data.format = GBM_FORMAT_XRGB8888; + EXPECT_EQ(gbm_bo_import(m_gbm, GBM_BO_IMPORT_FD, &data, GBM_BO_USE_RENDERING), nullptr); + ::close(foreign); +}