[Test] (MG_Benchmark, MG_Util, MG_Backend, android-plugin): run the driver benchmark on a phone

The Minecraft-shaped driver benchmark could only be run from a desktop shell
against a desktop driver, which is the wrong machine: MobileGL exists to run on
mobile GPUs, and nothing said what its translation costs there. This puts the
same cases on an Android device, both in the plugin's POST screen and from a
shell, and adds the native-driver baseline they have to be read against.

The cases move into DriverBenchCases.inc so both harnesses run byte-identical
bodies - the desktop program resolving entry points from one EGL provider, and
DriverBenchJni.cpp calling MobileGL's frontend in-process. The JNI file binds
every gl*/egl* name to MG_Impl by macro rather than by linkage: this library
legitimately has the platform libEGL and libGLESv3 in its own lookup scope, and
a benchmark that quietly measured the device driver instead of the translation
layer would have looked like very good news.

Frames are now closed with a fence wait instead of glFinish. MobileGL implements
glFinish and glFlush as no-ops, so the old loop timed submit-plus-GPU on a native
driver and submit-only on a MobileGL backend, and the two numbers did not
describe the same work.

To measure a device's own driver the cases needed to be expressible in GLES:
ESSL 3.20 twins of the four shaders (chosen at runtime from GL_VERSION, since
MobileGL is deliberately still fed desktop GLSL - translating it is the thing
under test), a multi-draw hook that loops DrawElementsBaseVertex where the
multi-draw entry point does not exist, and an EGL bootstrap that falls back from
desktop GL to GLES 3. The binary cross-compiles for arm64 unchanged.

BenchService hosts each run in its own process and exits afterwards. That is not
caution: the backend is latched from MOBILEGL_BACKEND_TYPE at initialization, so
Espryt and Magma can never share a process, and Espryt's teardown terminates the
process-default EGL display, which would take the POST activity's own EGL
objects with it.

Running it found that Magma could not create a windowless context on Mali at
all - CreateInstance required VK_EXT_headless_surface, which no mobile driver
here exposes, and aborted the process. The Xlib path already probes and falls
back to a hidden window for the same reason on NVIDIA; Android now probes too
and hands the WSI an AImageReader's ANativeWindow, a real producer surface
attached to no display whose images are never acquired. DriverPost reports the
extension's absence as a WARN so the fallback is visible rather than silent.

Measured on a Mali-G77 MC9 (native / Espryt / Magma, ns per operation):
5495 chunk draws 14397 / 36934 / 33763, the 26.2 per-draw uniform-range pattern
13710 / 31205 / 21252, sodium-style multi-draw 256956 / 238389 / 209527. The
translation costs about 2.4x per draw here against 5-9x on the desktop, because
the mobile driver's own per-call cost dwarfs it - and both backends beat the
native driver on multi-draw, which it has to emulate.

Desktop unit tests 421/421; the POST screen and both Run Bench buttons verified
on the device.
This commit is contained in:
BZLZHH
2026-08-06 06:13:34 -04:00
parent 1e45958e01
commit 9c0144d24a
11 changed files with 1362 additions and 502 deletions
@@ -2920,6 +2920,21 @@ void main() {
}
#endif
#if defined(VK_USE_PLATFORM_ANDROID_KHR)
// The AImageReader owns the ANativeWindow the pbuffer fallback handed to the
// WSI, so it outlives the surface and is released only here.
if (m_fallbackImageReader != nullptr && m_platformLibrary != nullptr) {
using AImageReaderDeleteFn = void (*)(void*);
auto* imageReaderDelete =
reinterpret_cast<AImageReaderDeleteFn>(dlsym(m_platformLibrary, "AImageReader_delete"));
if (imageReaderDelete) {
imageReaderDelete(m_fallbackImageReader);
}
m_fallbackImageReader = nullptr;
m_window = 0;
dlclose(m_platformLibrary);
m_platformLibrary = nullptr;
}
if (m_debugMessenger != VK_NULL_HANDLE) {
DestroyDebugMessenger();
m_debugMessenger = VK_NULL_HANDLE;
@@ -9809,6 +9824,20 @@ void main() {
if (!m_window) {
#ifdef VK_USE_PLATFORM_METAL_EXT
exts.push_back(VK_EXT_METAL_SURFACE_EXTENSION_NAME);
#elif defined VK_USE_PLATFORM_ANDROID_KHR
m_headlessSurfaceSupported = IsExtensionSupported(m_extensions, VK_EXT_HEADLESS_SURFACE_EXTENSION_NAME);
if (m_headlessSurfaceSupported) {
exts.push_back(VK_EXT_HEADLESS_SURFACE_EXTENSION_NAME);
} else {
// No mobile ICD seen so far implements VK_EXT_headless_surface
// (Mali r32p1 does not), and this used to abort the process the
// moment an application asked for a pbuffer context. CreateSurface()
// gives the WSI an AImageReader window instead, so request the
// Android surface extension for it.
MGLOG_I("%s not available; falling back to an AImageReader %s surface for the pbuffer context.",
VK_EXT_HEADLESS_SURFACE_EXTENSION_NAME, VK_KHR_ANDROID_SURFACE_EXTENSION_NAME);
exts.push_back(VK_KHR_ANDROID_SURFACE_EXTENSION_NAME);
}
#elif defined VK_USE_PLATFORM_XLIB_KHR
m_headlessSurfaceSupported = IsExtensionSupported(m_extensions, VK_EXT_HEADLESS_SURFACE_EXTENSION_NAME);
if (m_headlessSurfaceSupported) {
@@ -10646,6 +10675,54 @@ void main() {
m_window = reinterpret_cast<NativeWindowType>(
CreateInternalMetalLayer(m_config.SurfaceWidth, m_config.SurfaceHeight, &m_platformDisplay));
m_platformLibrary = reinterpret_cast<void*>(m_window);
#elif defined VK_USE_PLATFORM_ANDROID_KHR
if (m_headlessSurfaceSupported) {
auto* createHeadlessSurface = reinterpret_cast<PFN_vkCreateHeadlessSurfaceEXT>(
vkGetInstanceProcAddr(m_instance, "vkCreateHeadlessSurfaceEXT"));
MOBILEGL_ASSERT(createHeadlessSurface != nullptr,
"VK_EXT_headless_surface is not available for DirectVulkan pbuffer surface");
VkHeadlessSurfaceCreateInfoEXT sci{VK_STRUCTURE_TYPE_HEADLESS_SURFACE_CREATE_INFO_EXT};
VK_VERIFY(createHeadlessSurface(m_instance, &sci, nullptr, &m_surface),
"vkCreateHeadlessSurfaceEXT failed");
return;
}
// Windowless context on a driver without VK_EXT_headless_surface: give
// the WSI an AImageReader's ANativeWindow. It is a real, valid producer
// surface that is attached to no display and whose images this code never
// acquires, which is exactly the "drawable nobody sees" the Xlib fallback
// below builds out of an unmapped window. libmediandk is dlopen'd rather
// than linked so a device without it degrades to the old error instead of
// failing to load the library at all.
{
void* mediaLib = dlopen("libmediandk.so", RTLD_NOW | RTLD_LOCAL);
MOBILEGL_ASSERT(mediaLib != nullptr,
"VK_EXT_headless_surface is unavailable and libmediandk.so could not be loaded "
"for the pbuffer surface fallback");
using AImageReaderNewFn = int (*)(int32_t, int32_t, int32_t, int32_t, void**);
using AImageReaderGetWindowFn = int (*)(void*, void**);
auto* imageReaderNew = reinterpret_cast<AImageReaderNewFn>(dlsym(mediaLib, "AImageReader_new"));
auto* imageReaderGetWindow =
reinterpret_cast<AImageReaderGetWindowFn>(dlsym(mediaLib, "AImageReader_getWindow"));
MOBILEGL_ASSERT(imageReaderNew != nullptr && imageReaderGetWindow != nullptr,
"libmediandk.so is missing AImageReader_new/AImageReader_getWindow");
constexpr int32_t kAndroidFormatRgba8888 = 0x1; // AIMAGE_FORMAT_RGBA_8888
const int32_t width = static_cast<int32_t>(std::max<Uint32>(m_config.SurfaceWidth, 1));
const int32_t height = static_cast<int32_t>(std::max<Uint32>(m_config.SurfaceHeight, 1));
void* reader = nullptr;
// maxImages must cover the swapchain's images; the reader never
// acquires any, so this only sizes its buffer queue.
const int status = imageReaderNew(width, height, kAndroidFormatRgba8888, 8, &reader);
MOBILEGL_ASSERT(status == 0 && reader != nullptr,
"AImageReader_new failed (%d) for the pbuffer surface fallback", status);
void* nativeWindow = nullptr;
const int windowStatus = imageReaderGetWindow(reader, &nativeWindow);
MOBILEGL_ASSERT(windowStatus == 0 && nativeWindow != nullptr,
"AImageReader_getWindow failed (%d) for the pbuffer surface fallback", windowStatus);
m_fallbackImageReader = reader;
m_platformLibrary = mediaLib;
m_window = reinterpret_cast<NativeWindowType>(nativeWindow);
}
#elif defined VK_USE_PLATFORM_XLIB_KHR
if (m_headlessSurfaceSupported) {
auto* createHeadlessSurface =