mesa/.gitlab-ci/container/patches/build-deqp-android-Implement-headless-WSI-fallback-using-AImageR.patch
Valentine Burley 6cf5da8dd3 ci/deqp: Rewrite headless Android WSI patch
Update the headless Android WSI patch to fix intermittent timeout issues. It
now uses an ImageReader listener to actively drain and instantly release frames
from the buffer queue. This acts as a "null compositor" that prevents buffer
starvation while maintaining stable GPU backpressure.

This fixes dEQP-VK.wsi.android.maintenance1.* in newer VKCTS versions and
resolves the race conditions that caused occasional teardown crashes.

Also rebase build-deqp-gl_Build-Don-t-build-Vulkan-utilities-for-GL-builds.patch
on top of the updated WSI patch.

Signed-off-by: Valentine Burley <valentine.burley@collabora.com>
Part-of: <https://gitlab.freedesktop.org/mesa/mesa/-/merge_requests/41541>
2026-05-15 17:38:59 +00:00

367 lines
12 KiB
Diff

From a801d75fcb16db89d2af67dd40040bc7c5f59df0 Mon Sep 17 00:00:00 2001
From: Valentine Burley <valentine.burley@collabora.com>
Date: Thu, 23 Apr 2026 11:03:14 +0200
Subject: [PATCH] android: Implement headless WSI fallback using AImageReader
When running dEQP as a native executable (DEQP_ANDROID_EXE) on Android,
a system-provided NativeActivity window is typically unavailable. This
prevents Vulkan and EGL WSI tests from running as they require a valid
ANativeWindow.
This patch:
- Implements a headless fallback using the AImageReader NDK API to
create an off-screen ANativeWindow in tcuAndroidPlatform.cpp.
- Implements an active "null compositor" via an AImageReader listener
that immediately drains and releases acquired images. This ensures the
producer never runs out of buffers, fixing intermittent timeouts in
tests that require active consumption (e.g. maintenance1 tests).
- Uses AImageReader_acquireNextImageAsync and AImage_deleteAsync (API 26+)
to handle GPU sync fences.
- Adds thread-safe teardown logic using an atomic flag and a non-blocking
mutex lock (std::try_to_lock) to prevent race conditions between the
main thread and the background listener callback.
- Adds ImageReaderNativeWindow (EGL) and ImageReaderVulkanWindow (Vulkan)
to manage the AImageReader and ImageQueue lifecycle.
- Extracts the AImageReader creation logic into a shared helper
acquireImageReaderWindow().
- Links against mediandk in android.cmake to provide AImageReader
support for Android API levels >= 24.
- Uses AImageReader_newWithUsage (API 26+) with explicit hardware buffer flags
(SAMPLED_IMAGE | COMPOSER_OVERLAY) to mimic SurfaceFlinger allocation
constraints.
- On Android API levels < 24 (where AImageReader is unavailable), window
acquisition failure now consistently throws ResourceError instead of
NotSupportedError. This aligns the EGL path with the existing Vulkan
behavior and reverts EGL to the previous CTS behavior.
This allows surface and swapchain tests to function correctly in headless
environments for both EGL and Vulkan.
Components: Android, EGL, Framework, Vulkan
VK-GL-CTS issue: 6468
Affects:
dEQP-EGL.*
dEQP-VK.wsi.android.*
Change-Id: I462e617ae60e4dc3d9f0aeec11fd1628d0c6ff12
Signed-off-by: Valentine Burley <valentine.burley@collabora.com>
---
external/openglcts/README.md | 7 +-
.../platform/android/tcuAndroidPlatform.cpp | 237 +++++++++++++++++-
targets/android/android.cmake | 5 +
3 files changed, 242 insertions(+), 7 deletions(-)
diff --git a/external/openglcts/README.md b/external/openglcts/README.md
index 2eae316f58..24b270aee5 100644
--- a/external/openglcts/README.md
+++ b/external/openglcts/README.md
@@ -323,9 +323,10 @@ This is identical to the builds on other platforms and is better for iterative
runs of headless tests as CTS can be invoked and the output can be checked from
a single interactive terminal.
-This build doesn't support WSI tests and shouldn't be used for conformance
-submissions, it also isn't recommended for longer running tests since Android
-will terminate this process as soon as the `adb shell` session ends which may
+This build supports WSI tests via a headless AImageReader fallback for both EGL
+and Vulkan (Android API 24+). However, it shouldn't be used for conformance
+submissions. It also isn't recommended for longer running tests since Android
+will terminate this process as soon as the `adb shell` session ends, which may
happen due to an unintentional device disconnection.
cmake <path to openglcts> -GNinja -DCMAKE_BUILD_TYPE=Debug \
diff --git a/framework/platform/android/tcuAndroidPlatform.cpp b/framework/platform/android/tcuAndroidPlatform.cpp
index af56dabb83..45861c8aee 100644
--- a/framework/platform/android/tcuAndroidPlatform.cpp
+++ b/framework/platform/android/tcuAndroidPlatform.cpp
@@ -35,6 +35,15 @@
// Assume no call translation is needed
#include <android/native_window.h>
+#if DE_ANDROID_API >= 24
+#include <media/NdkImageReader.h>
+#include <media/NdkImage.h>
+#include <mutex>
+#include <atomic>
+#endif
+#if DE_ANDROID_API >= 26
+#include <android/hardware_buffer.h>
+#endif
struct egl_native_pixmap_t;
DE_STATIC_ASSERT(sizeof(eglw::EGLNativeDisplayType) == sizeof(void *));
DE_STATIC_ASSERT(sizeof(eglw::EGLNativePixmapType) == sizeof(struct egl_native_pixmap_t *));
@@ -136,6 +145,141 @@ private:
WindowRegistry &m_windowRegistry;
};
+#if DE_ANDROID_API >= 24
+struct ImageQueue
+{
+ std::mutex lock;
+ std::atomic<bool> closing{false};
+ AImageReader_ImageListener listener;
+
+ ImageQueue()
+ {
+ listener.context = this;
+ listener.onImageAvailable = onImageAvailable;
+ }
+
+ ~ImageQueue()
+ {
+ }
+
+ static void onImageAvailable(void *context, AImageReader *reader)
+ {
+ ImageQueue *queue = reinterpret_cast<ImageQueue *>(context);
+
+ std::unique_lock<std::mutex> guard(queue->lock, std::try_to_lock);
+ if (!guard.owns_lock() || queue->closing.load(std::memory_order_acquire))
+ return;
+
+ AImage *image = nullptr;
+
+#if DE_ANDROID_API >= 26
+ int fenceFd = -1;
+ while (AImageReader_acquireNextImageAsync(reader, &image, &fenceFd) == AMEDIA_OK && image != nullptr)
+ {
+ AImage_deleteAsync(image, fenceFd);
+ }
+#else
+ while (AImageReader_acquireNextImage(reader, &image) == AMEDIA_OK && image != nullptr)
+ {
+ AImage_delete(image);
+ }
+#endif
+ }
+};
+
+static ANativeWindow *acquireImageReaderWindow(int width, int height, int32_t format, AImageReader **outReader,
+ ImageQueue **outQueue)
+{
+ AImageReader *reader = nullptr;
+#if DE_ANDROID_API >= 26
+ uint64_t usage = AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE | AHARDWAREBUFFER_USAGE_COMPOSER_OVERLAY;
+ media_status_t status = AImageReader_newWithUsage(width, height, format, usage, 4, &reader);
+#else
+ media_status_t status = AImageReader_new(width, height, format, 4, &reader);
+#endif
+ if (status != AMEDIA_OK || !reader)
+ throw ResourceError("Failed to create AImageReader", nullptr, __FILE__, __LINE__);
+
+ *outQueue = new ImageQueue();
+ AImageReader_setImageListener(reader, &(*outQueue)->listener);
+
+ ANativeWindow *nativeWindow = nullptr;
+ status = AImageReader_getWindow(reader, &nativeWindow);
+ if (status != AMEDIA_OK || !nativeWindow)
+ {
+ AImageReader_setImageListener(reader, nullptr);
+ delete *outQueue;
+ *outQueue = nullptr;
+ AImageReader_delete(reader);
+ throw ResourceError("Failed to get window from AImageReader", nullptr, __FILE__, __LINE__);
+ }
+
+ *outReader = reader;
+ return nativeWindow;
+}
+
+class ImageReaderNativeWindow : public eglu::NativeWindow
+{
+public:
+ ImageReaderNativeWindow(AImageReader *reader, ImageQueue *queue, ANativeWindow *window, int width, int height)
+ : eglu::NativeWindow(WINDOW_CAPABILITIES)
+ , m_reader(reader)
+ , m_queue(queue)
+ , m_window(window)
+ , m_size(width, height)
+ {
+ }
+
+ virtual ~ImageReaderNativeWindow(void)
+ {
+ if (m_reader)
+ {
+ if (m_queue)
+ {
+ m_queue->closing.store(true, std::memory_order_release);
+ std::lock_guard<std::mutex> guard(m_queue->lock);
+ AImageReader_setImageListener(m_reader, nullptr);
+ }
+ AImageReader_delete(m_reader);
+ }
+ if (m_queue)
+ delete m_queue;
+ }
+
+ virtual eglw::EGLNativeWindowType getLegacyNative(void)
+ {
+ return m_window;
+ }
+ virtual void *getPlatformExtension(void)
+ {
+ return m_window;
+ }
+ virtual void *getPlatformNative(void)
+ {
+ return m_window;
+ }
+ tcu::IVec2 getScreenSize(void) const
+ {
+ return m_size;
+ }
+ void setSurfaceSize(tcu::IVec2 size)
+ {
+ int32_t format = 0; // 0 means keep the existing format
+ ANativeWindow_setBuffersGeometry(m_window, size.x(), size.y(), format);
+ m_size = size;
+ }
+ virtual void processEvents(void)
+ {
+ }
+
+private:
+ AImageReader *m_reader;
+ ImageQueue *m_queue;
+ ANativeWindow *m_window;
+ tcu::IVec2 m_size;
+};
+#endif
+
// NativeWindow
NativeWindow::NativeWindow(Window *window, int width, int height, int32_t format)
@@ -197,10 +341,30 @@ eglu::NativeWindow *NativeWindowFactory::createWindow(const eglu::WindowParams &
{
Window *window = m_windowRegistry.tryAcquireWindow();
- if (!window)
- throw NotSupportedError("Native window is not available", nullptr, __FILE__, __LINE__);
-
- return new NativeWindow(window, params.width, params.height, format);
+ if (window)
+ {
+ return new NativeWindow(window, params.width, params.height, format);
+ }
+ else
+ {
+#if DE_ANDROID_API >= 24
+ int width = params.width != eglu::WindowParams::SIZE_DONT_CARE ? params.width : 256;
+ int height = params.height != eglu::WindowParams::SIZE_DONT_CARE ? params.height : 256;
+ width = width > 0 ? width : 256;
+ height = height > 0 ? height : 256;
+
+ AImageReader *reader = nullptr;
+ ImageQueue *queue = nullptr;
+ // Always use AIMAGE_FORMAT_RGBA_8888: the AImageReader is only used as a
+ // surface handle provider, and AIMAGE_FORMAT_* constants are not
+ // interchangeable with ANativeWindow_LegacyFormat values.
+ ANativeWindow *nativeWindow = acquireImageReaderWindow(width, height, AIMAGE_FORMAT_RGBA_8888, &reader, &queue);
+
+ return new ImageReaderNativeWindow(reader, queue, nativeWindow, width, height);
+#else
+ throw ResourceError("Native window is not available", nullptr, __FILE__, __LINE__);
+#endif
+ }
}
// NativeDisplayFactory
@@ -279,6 +443,55 @@ private:
tcu::Android::Window &m_window;
};
+#if DE_ANDROID_API >= 24
+class ImageReaderVulkanWindow : public vk::wsi::AndroidWindowInterface
+{
+public:
+ ImageReaderVulkanWindow(AImageReader *reader, ImageQueue *queue, ANativeWindow *window)
+ : vk::wsi::AndroidWindowInterface(vk::pt::AndroidNativeWindowPtr(window))
+ , m_reader(reader)
+ , m_queue(queue)
+ {
+ }
+
+ void setVisible(bool visible)
+ {
+ DE_UNREF(visible);
+ }
+
+ void resize(const UVec2 &newSize)
+ {
+ DE_UNREF(newSize);
+ }
+
+ void setMinimized(bool minimized)
+ {
+ DE_UNREF(minimized);
+ TCU_THROW(NotSupportedError, "Minimized on Android is not implemented");
+ }
+
+ ~ImageReaderVulkanWindow(void)
+ {
+ if (m_reader)
+ {
+ if (m_queue)
+ {
+ m_queue->closing.store(true, std::memory_order_release);
+ std::lock_guard<std::mutex> guard(m_queue->lock);
+ AImageReader_setImageListener(m_reader, nullptr);
+ }
+ AImageReader_delete(m_reader);
+ }
+ if (m_queue)
+ delete m_queue;
+ }
+
+private:
+ AImageReader *m_reader;
+ ImageQueue *m_queue;
+};
+#endif
+
class VulkanDisplay : public vk::wsi::Display
{
public:
@@ -306,7 +519,23 @@ public:
}
}
else
+ {
+#if DE_ANDROID_API >= 24
+ uint32_t width = initialSize ? initialSize->x() : 256;
+ uint32_t height = initialSize ? initialSize->y() : 256;
+ width = width > 0 ? width : 256;
+ height = height > 0 ? height : 256;
+
+ AImageReader *reader = nullptr;
+ ImageQueue *queue = nullptr;
+ ANativeWindow *nativeWindow =
+ acquireImageReaderWindow(width, height, AIMAGE_FORMAT_RGBA_8888, &reader, &queue);
+
+ return new ImageReaderVulkanWindow(reader, queue, nativeWindow);
+#else
TCU_THROW(ResourceError, "Native window is not available");
+#endif
+ }
}
private:
diff --git a/targets/android/android.cmake b/targets/android/android.cmake
index 33843fcc09..6da33d3c17 100644
--- a/targets/android/android.cmake
+++ b/targets/android/android.cmake
@@ -64,6 +64,11 @@ if (DE_ANDROID_API GREATER 8)
set(DEQP_PLATFORM_LIBRARIES ${DEQP_PLATFORM_LIBRARIES} ${ANDROID_LIBRARY})
endif ()
+if (DE_ANDROID_API GREATER 23)
+ find_library(MEDIANDK_LIBRARY NAMES mediandk PATHS /usr/lib)
+ set(DEQP_PLATFORM_LIBRARIES ${DEQP_PLATFORM_LIBRARIES} ${MEDIANDK_LIBRARY})
+endif ()
+
# Android uses customized execserver
include_directories(execserver)
set(DEQP_PLATFORM_LIBRARIES xscore ${DEQP_PLATFORM_LIBRARIES})
--
2.51.0