diff --git a/CMakeLists.txt b/CMakeLists.txt index de6af5ef..8b56fa98 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -304,6 +304,7 @@ endif() if (ANDROID) list(APPEND SOURCE_FILES MobileGL/MG_Util/SelfTest/DriverPostJni.cpp + MobileGL/MG_Util/SelfTest/DriverBenchJni.cpp ) endif() diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index 094f6e83..dbb561c3 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -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(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( CreateInternalMetalLayer(m_config.SurfaceWidth, m_config.SurfaceHeight, &m_platformDisplay)); m_platformLibrary = reinterpret_cast(m_window); +#elif defined VK_USE_PLATFORM_ANDROID_KHR + if (m_headlessSurfaceSupported) { + auto* createHeadlessSurface = reinterpret_cast( + 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(dlsym(mediaLib, "AImageReader_new")); + auto* imageReaderGetWindow = + reinterpret_cast(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(std::max(m_config.SurfaceWidth, 1)); + const int32_t height = static_cast(std::max(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(nativeWindow); + } #elif defined VK_USE_PLATFORM_XLIB_KHR if (m_headlessSurfaceSupported) { auto* createHeadlessSurface = diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h index b7fcbb72..24af2fe7 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h @@ -444,6 +444,12 @@ namespace MobileGL::MG_Backend::DirectVulkan { // above (rather than being handed one by the caller), so Shutdown() knows it // owns that window and must destroy it. Bool m_ownsFallbackXlibWindow = false; + // Android has the same shortfall: no Mali/Adreno driver seen so far exposes + // VK_EXT_headless_surface, so a windowless (EGL pbuffer) context gets an + // AImageReader's ANativeWindow to hand the WSI instead. Nothing is ever + // displayed - the reader's images are simply never acquired. Owned here, so + // Shutdown() deletes it. + void* m_fallbackImageReader = nullptr; VulkanRendererConfig m_config; Bool m_swapchainResizeRequested = false; // Presentation is suspended while the window is zero-area (minimized): the diff --git a/MobileGL/MG_Benchmark/Driver/DriverBench.c b/MobileGL/MG_Benchmark/Driver/DriverBench.c index 7b26eca0..2c43d84d 100644 --- a/MobileGL/MG_Benchmark/Driver/DriverBench.c +++ b/MobileGL/MG_Benchmark/Driver/DriverBench.c @@ -53,11 +53,16 @@ typedef unsigned int EGLenum; #define EGL_RENDERABLE_TYPE 0x3040 #define EGL_OPENGL_BIT 0x0008 #define EGL_RED_SIZE 0x3024 +#define EGL_GREEN_SIZE 0x3023 +#define EGL_BLUE_SIZE 0x3022 #define EGL_DEPTH_SIZE 0x3025 #define EGL_WIDTH 0x3057 #define EGL_HEIGHT 0x3056 #define EGL_NONE 0x3038 #define EGL_OPENGL_API 0x30A2 +#define EGL_OPENGL_ES_API 0x30A0 +#define EGL_OPENGL_ES3_BIT 0x0040 +#define EGL_CONTEXT_CLIENT_VERSION 0x3098 #define EGL_CONTEXT_MAJOR_VERSION 0x3098 #define EGL_CONTEXT_MINOR_VERSION 0x30FB #define EGL_CONTEXT_OPENGL_PROFILE_MASK 0x30FD @@ -96,6 +101,8 @@ typedef unsigned int EGLenum; #define GL_COLOR_ATTACHMENT0 0x8CE0 #define GL_DEPTH_ATTACHMENT 0x8D00 #define GL_FRAMEBUFFER_COMPLETE 0x8CD5 +#define GL_SYNC_GPU_COMMANDS_COMPLETE 0x9117 +#define GL_SYNC_FLUSH_COMMANDS_BIT 0x00000001 #define GL_UNIFORM_BUFFER 0x8A11 #define GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT 0x8A34 #define GL_DYNAMIC_DRAW 0x88E8 @@ -183,6 +190,9 @@ GLF(void, glBindRenderbuffer, (GLenum, GLuint)) GLF(void, glRenderbufferStorage, (GLenum, GLenum, GLsizei, GLsizei)) GLF(void, glFramebufferRenderbuffer, (GLenum, GLenum, GLenum, GLuint)) GLF(GLenum, glCheckFramebufferStatus, (GLenum)) +GLF(void*, glFenceSync, (GLenum, unsigned)) +GLF(GLenum, glClientWaitSync, (void*, unsigned, unsigned long long)) +GLF(void, glDeleteSync, (void*)) static uint64_t now_ns(void) { struct timespec ts; @@ -195,236 +205,69 @@ static int cmp_u64(const void* a, const void* b) { return x < y ? -1 : x > y; } -/* ---- shared scene resources (Minecraft-shaped) ---- */ -#define MAX_SECTIONS 512 -static GLuint g_progChunk, g_progEntity; -static GLint g_uOffsetChunk, g_uMvpChunk, g_uMvpEntity; -static GLuint g_vao[MAX_SECTIONS], g_vbo[MAX_SECTIONS]; -static GLuint g_sharedIbo; -static GLuint g_texAtlas, g_texLight, g_texEntity; -static int g_quadsPerSection = 128; /* 128 quads = 512 verts, 768 indices */ -static unsigned char* g_scratch; -/* Uniform ring + sampler for the 26.2-shaped cases (see the case block below). */ -static GLuint g_uboRing; -static GLint g_uboAlign = 256; -static size_t g_uboSlot = 256; -static GLuint g_sampler; -static float g_mvp[16] = {0.002f, 0, 0, 0, 0, 0.002f, 0, 0, 0, 0, -0.001f, 0, -1.f, -1.f, 0.f, 1.f}; -/* Minecraft chunk vertex: pos 3f, color 4ub, uv 2f, packed light 2s -> 32 B */ -#define VERT_STRIDE 32 -static void fill_section_vertices(unsigned char* dst, int quads, unsigned seed) { - for (int q = 0; q < quads * 4; ++q) { - float* f = (float*)(dst + q * VERT_STRIDE); - unsigned r = seed = seed * 1664525u + 1013904223u; - f[0] = (float)(q & 31) * 8.0f + (float)(r & 7); - f[1] = (float)((q >> 5) & 31) * 8.0f; - f[2] = (float)(q % 7) * 0.1f; - dst[q * VERT_STRIDE + 12] = (unsigned char)r; - dst[q * VERT_STRIDE + 13] = (unsigned char)(r >> 8); - dst[q * VERT_STRIDE + 14] = (unsigned char)(r >> 16); - dst[q * VERT_STRIDE + 15] = 255; - f[4] = (float)(r & 1023) / 1024.0f; - f[5] = (float)((r >> 10) & 511) / 512.0f; - ((short*)(dst + q * VERT_STRIDE + 24))[0] = 15 << 4; - ((short*)(dst + q * VERT_STRIDE + 24))[1] = 15 << 4; +/* Scene, cases and the case table live next door so the Android plugin's + * in-process benchmark runs byte-identical bodies. */ +static void bench_gl_failed(const char* what, const char* detail) { + fprintf(stderr, "FAIL: %s %s\n", what, detail ? detail : ""); + exit(1); +} + +/* GLES has glDrawElementsBaseVertex (3.2 core) but no multi-draw form of it, so + * against a native mobile driver the multi-draw case issues the same sub-draws + * one at a time - which is what the extension folds up, and what an application + * without it would have to write. Desktop GL and MobileGL take the real call. */ +static void bench_multi_draw_elements_base_vertex(GLenum mode, const GLsizei* counts, GLenum type, + const void* const* offsets, GLsizei drawCount, + const GLint* baseVertices) { + if (glMultiDrawElementsBaseVertex) { + glMultiDrawElementsBaseVertex(mode, counts, type, offsets, drawCount, baseVertices); + return; + } + for (GLsizei i = 0; i < drawCount; ++i) { + glDrawElementsBaseVertex(mode, counts[i], type, offsets[i], baseVertices[i]); } } -static GLuint make_shader(GLenum kind, const char* src) { - GLuint sh = glCreateShader(kind); - glShaderSource(sh, 1, &src, NULL); - glCompileShader(sh); - GLint ok = 0; - glGetShaderiv(sh, GL_COMPILE_STATUS, &ok); - if (!ok) { - char log[1024]; - glGetShaderInfoLog(sh, sizeof log, NULL, log); - fprintf(stderr, "FAIL: shader compile: %s\n", log); - exit(1); - } - return sh; -} +#include "DriverBenchCases.inc" -static GLuint make_program(const char* vs_src, const char* fs_src) { - GLuint prog = glCreateProgram(); - glAttachShader(prog, make_shader(GL_VERTEX_SHADER, vs_src)); - glAttachShader(prog, make_shader(GL_FRAGMENT_SHADER, fs_src)); - glBindAttribLocation(prog, 0, "aPos"); - glBindAttribLocation(prog, 1, "aColor"); - glBindAttribLocation(prog, 2, "aUv"); - glBindAttribLocation(prog, 3, "aLight"); - glLinkProgram(prog); - GLint ok = 0; - glGetProgramiv(prog, GL_LINK_STATUS, &ok); - if (!ok) { - fprintf(stderr, "FAIL: program link\n"); - exit(1); - } - return prog; -} - -static const char* kChunkVs = - "#version 150 core\n" - "in vec3 aPos; in vec4 aColor; in vec2 aUv; in vec2 aLight;\n" - "uniform mat4 uMvp; uniform vec3 uOffset;\n" - "out vec4 vColor; out vec2 vUv; out vec2 vLight;\n" - "void main(){ gl_Position = uMvp * vec4(aPos + uOffset, 1.0);\n" - " vColor = aColor; vUv = aUv; vLight = aLight * (1.0/256.0); }\n"; -static const char* kChunkFs = - "#version 150 core\n" - "in vec4 vColor; in vec2 vUv; in vec2 vLight; out vec4 o;\n" - "uniform sampler2D uAtlas; uniform sampler2D uLight;\n" - "void main(){ o = texture(uAtlas, vUv) * vColor * texture(uLight, vLight); }\n"; -static const char* kEntityVs = - "#version 150 core\n" - "in vec3 aPos; in vec4 aColor; in vec2 aUv; in vec2 aLight;\n" - "uniform mat4 uMvp; uniform mat4 uModel;\n" - "out vec4 vColor; out vec2 vUv;\n" - "void main(){ gl_Position = uMvp * uModel * vec4(aPos, 1.0); vColor = aColor; vUv = aUv; }\n"; -static const char* kEntityFs = - "#version 150 core\n" - "in vec4 vColor; in vec2 vUv; out vec4 o; uniform sampler2D uTex;\n" - "void main(){ o = texture(uTex, vUv) * vColor; }\n"; - -static void setup_vao(GLuint vao, GLuint vbo, GLuint ibo) { - glBindVertexArray(vao); - glBindBuffer(GL_ARRAY_BUFFER, vbo); - glEnableVertexAttribArray(0); - glEnableVertexAttribArray(1); - glEnableVertexAttribArray(2); - glEnableVertexAttribArray(3); - glVertexAttribPointer(0, 3, GL_FLOAT, 0, VERT_STRIDE, (void*)0); - glVertexAttribPointer(1, 4, GL_UNSIGNED_BYTE, 1, VERT_STRIDE, (void*)12); - glVertexAttribPointer(2, 2, GL_FLOAT, 0, VERT_STRIDE, (void*)16); - glVertexAttribPointer(3, 2, GL_SHORT, 0, VERT_STRIDE, (void*)24); - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo); -} - -static void build_resources(void) { - /* offscreen render target: 1280x720 RBO FBO, like CTS fbo surface mode */ - GLuint fbo, rboColor, rboDepth; - glGenFramebuffers(1, &fbo); - glBindFramebuffer(GL_FRAMEBUFFER, fbo); - glGenRenderbuffers(1, &rboColor); - glBindRenderbuffer(GL_RENDERBUFFER, rboColor); - glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, 1280, 720); - glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, rboColor); - glGenRenderbuffers(1, &rboDepth); - glBindRenderbuffer(GL_RENDERBUFFER, rboDepth); - glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, 1280, 720); - glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, rboDepth); - if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { - fprintf(stderr, "FAIL: FBO incomplete\n"); - exit(1); - } - - g_progChunk = make_program(kChunkVs, kChunkFs); - g_progEntity = make_program(kEntityVs, kEntityFs); - glUseProgram(g_progChunk); - g_uMvpChunk = glGetUniformLocation(g_progChunk, "uMvp"); - g_uOffsetChunk = glGetUniformLocation(g_progChunk, "uOffset"); - glUniform1i(glGetUniformLocation(g_progChunk, "uAtlas"), 0); - glUniform1i(glGetUniformLocation(g_progChunk, "uLight"), 2); - glUniformMatrix4fv(g_uMvpChunk, 1, 0, g_mvp); - glUseProgram(g_progEntity); - g_uMvpEntity = glGetUniformLocation(g_progEntity, "uMvp"); - glUniform1i(glGetUniformLocation(g_progEntity, "uTex"), 0); - glUniformMatrix4fv(g_uMvpEntity, 1, 0, g_mvp); - glUseProgram(g_progChunk); - - /* shared quad index buffer, like Blaze3D's RenderSystem shared sequences */ - int maxQuads = 4096; - unsigned* idx = malloc((size_t)maxQuads * 6 * 4); - for (int q = 0; q < maxQuads; ++q) { - unsigned base = q * 4; - unsigned* p = idx + q * 6; - p[0] = base; p[1] = base + 1; p[2] = base + 2; - p[3] = base + 2; p[4] = base + 3; p[5] = base; - } - glGenBuffers(1, &g_sharedIbo); - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, g_sharedIbo); - glBufferData(GL_ELEMENT_ARRAY_BUFFER, maxQuads * 6 * 4, idx, GL_STATIC_DRAW); - free(idx); - - g_scratch = malloc(4 * 1024 * 1024); - memset(g_scratch, 0x5a, 4 * 1024 * 1024); - - glGenVertexArrays(MAX_SECTIONS, g_vao); - glGenBuffers(MAX_SECTIONS, g_vbo); - int bytes = g_quadsPerSection * 4 * VERT_STRIDE; - for (int i = 0; i < MAX_SECTIONS; ++i) { - fill_section_vertices(g_scratch, g_quadsPerSection, i * 7919u + 1); - glBindBuffer(GL_ARRAY_BUFFER, g_vbo[i]); - glBufferData(GL_ARRAY_BUFFER, bytes, g_scratch, GL_STATIC_DRAW); - setup_vao(g_vao[i], g_vbo[i], g_sharedIbo); - } - - glGenTextures(1, &g_texAtlas); - glActiveTexture(GL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, g_texAtlas); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 1024, 512, 0, GL_RGBA, GL_UNSIGNED_BYTE, g_scratch); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - glGenerateMipmap(GL_TEXTURE_2D); - - glGenTextures(1, &g_texLight); - glActiveTexture(GL_TEXTURE0 + 2); - glBindTexture(GL_TEXTURE_2D, g_texLight); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 16, 16, 0, GL_RGBA, GL_UNSIGNED_BYTE, g_scratch); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - - glGenTextures(1, &g_texEntity); - glActiveTexture(GL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, g_texEntity); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 64, 64, 0, GL_RGBA, GL_UNSIGNED_BYTE, g_scratch); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - glBindTexture(GL_TEXTURE_2D, g_texAtlas); - - // Uniform ring the 26.2-style case sub-ranges into, sized like a real - // frame's worth of per-draw uniform slots. - GLint align = 256; - glGetIntegerv(GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT, &align); - g_uboAlign = align > 0 ? align : 256; - g_uboSlot = (size_t)g_uboAlign; - glGenBuffers(1, &g_uboRing); - glBindBuffer(GL_UNIFORM_BUFFER, g_uboRing); - glBufferData(GL_UNIFORM_BUFFER, 4 * 1024 * 1024, g_scratch, GL_DYNAMIC_DRAW); - glBindBuffer(GL_UNIFORM_BUFFER, 0); - - glGenSamplers(1, &g_sampler); - glSamplerParameteri(g_sampler, GL_TEXTURE_MIN_FILTER, GL_NEAREST); - glSamplerParameteri(g_sampler, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - - glEnable(GL_DEPTH_TEST); - glClearColor(0.3f, 0.5f, 0.9f, 1.0f); - glViewport(0, 0, 1280, 720); - if (glGetError() != GL_NO_ERROR) { - fprintf(stderr, "FAIL: GL error during resource setup\n"); - exit(1); - } -} - -/* ---- bench driver: glFinish-paced frames on the offscreen FBO ---- */ +/* ---- bench driver: fence-paced frames on the offscreen FBO ---------------- + * Frames are closed with a real fence wait, not glFinish: MobileGL implements + * glFinish and glFlush as no-ops (MG_Impl/GLImpl/Exporting/Definitions.cpp), + * so a glFinish-paced loop would time only the CPU-side submit on a MobileGL + * backend while timing submit-plus-GPU on the native driver - the two numbers + * would not describe the same work. A sync object is honoured by every stack + * measured here. + */ typedef void (*case_fn)(int frame, long a, long b); static int g_warmup = 30, g_frames = 120; +static void end_frame_wait(void) { + if (glFenceSync && glClientWaitSync && glDeleteSync) { + void* sync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); + if (sync) { + glClientWaitSync(sync, GL_SYNC_FLUSH_COMMANDS_BIT, 1000000000ull); + glDeleteSync(sync); + return; + } + } + glFinish(); +} + static void run_case(const char* name, case_fn body, long a, long b, long opsPerFrame) { static uint64_t samples[4096]; if (g_frames > 4096) g_frames = 4096; - glFinish(); + end_frame_wait(); for (int i = 0; i < g_warmup; ++i) { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); body(i, a, b); - glFinish(); + end_frame_wait(); } for (int i = 0; i < g_frames; ++i) { uint64_t t0 = now_ns(); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); body(i, a, b); - glFinish(); + end_frame_wait(); samples[i] = now_ns() - t0; } qsort(samples, g_frames, sizeof(uint64_t), cmp_u64); @@ -438,247 +281,6 @@ static void run_case(const char* name, case_fn body, long a, long b, long opsPer } /* a = draws per frame */ -static void case_draw_tiny(int frame, long a, long b) { - (void)frame; (void)b; - glBindVertexArray(g_vao[0]); - for (long i = 0; i < a; ++i) glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0); -} - -static void case_draw_uniform(int frame, long a, long b) { - (void)frame; (void)b; - glBindVertexArray(g_vao[0]); - for (long i = 0; i < a; ++i) { - glUniform3f(g_uOffsetChunk, (float)(i & 15), (float)((i >> 4) & 15), 0.0f); - glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0); - } -} - -static void case_draw_multi_vao(int frame, long a, long b) { - (void)frame; (void)b; - for (long i = 0; i < a; ++i) { - glBindVertexArray(g_vao[i % MAX_SECTIONS]); - glUniform3f(g_uOffsetChunk, (float)(i & 15), (float)((i >> 4) & 15), 0.0f); - glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0); - } -} - -static void case_tex_pingpong(int frame, long a, long b) { - (void)frame; (void)b; - glBindVertexArray(g_vao[0]); - for (long i = 0; i < a; ++i) { - glBindTexture(GL_TEXTURE_2D, (i & 1) ? g_texEntity : g_texAtlas); - glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0); - } - glBindTexture(GL_TEXTURE_2D, g_texAtlas); -} - -static void case_program_pingpong(int frame, long a, long b) { - (void)frame; (void)b; - glBindVertexArray(g_vao[0]); - for (long i = 0; i < a; ++i) { - if (i & 1) { - glUseProgram(g_progEntity); - glUniformMatrix4fv(g_uMvpEntity, 1, 0, g_mvp); - } else { - glUseProgram(g_progChunk); - glUniform3f(g_uOffsetChunk, (float)(i & 15), 0.0f, 0.0f); - } - glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0); - } - glUseProgram(g_progChunk); -} - -/* a = uploads per frame, b = bytes per upload (0 => section size) */ -static void case_chunk_upload(int frame, long a, long b) { - if (b <= 0) b = g_quadsPerSection * 4 * VERT_STRIDE; - if (b > 4 * 1024 * 1024) b = 4 * 1024 * 1024; - for (long i = 0; i < a; ++i) { - int slot = (int)(((long)frame * a + i) % MAX_SECTIONS); - glBindBuffer(GL_ARRAY_BUFFER, g_vbo[slot]); - glBufferData(GL_ARRAY_BUFFER, b, NULL, GL_STATIC_DRAW); /* orphan */ - glBufferSubData(GL_ARRAY_BUFFER, 0, b, g_scratch); - glBindVertexArray(g_vao[slot]); - glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0); - } -} - -/* a = sprite updates per frame */ -static void case_atlas_sprite(int frame, long a, long b) { - (void)b; - glBindVertexArray(g_vao[0]); - glBindTexture(GL_TEXTURE_2D, g_texAtlas); - for (long i = 0; i < a; ++i) { - int x = (int)((frame * 13 + i * 17) % (1024 - 16)); - int y = (int)((frame * 7 + i * 29) % (512 - 16)); - glTexSubImage2D(GL_TEXTURE_2D, 0, x, y, 16, 16, GL_RGBA, GL_UNSIGNED_BYTE, g_scratch); - } - glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0); -} - -/* a = lightmap updates (+draw) per frame */ -static void case_lightmap(int frame, long a, long b) { - (void)frame; (void)b; - glBindVertexArray(g_vao[0]); - for (long i = 0; i < a; ++i) { - glActiveTexture(GL_TEXTURE0 + 2); - glBindTexture(GL_TEXTURE_2D, g_texLight); - glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 16, 16, GL_RGBA, GL_UNSIGNED_BYTE, g_scratch); - glActiveTexture(GL_TEXTURE0); - glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0); - } -} - -/* Composite: a = total draws, b = uploads per frame. Mix modeled on trace - * analysis: chunk draws with per-draw offset uniform across sections, 10% - * entity-style program flips, per-frame lightmap + sprite updates, b chunk - * re-uploads. */ -static long g_mixSprites = 8; -static void case_scene_mix(int frame, long a, long b) { - glActiveTexture(GL_TEXTURE0 + 2); - glBindTexture(GL_TEXTURE_2D, g_texLight); - glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 16, 16, GL_RGBA, GL_UNSIGNED_BYTE, g_scratch); - glActiveTexture(GL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, g_texAtlas); - for (long i = 0; i < g_mixSprites; ++i) { - int x = (int)((frame * 13 + i * 17) % (1024 - 16)); - int y = (int)((frame * 7 + i * 29) % (512 - 16)); - glTexSubImage2D(GL_TEXTURE_2D, 0, x, y, 16, 16, GL_RGBA, GL_UNSIGNED_BYTE, g_scratch); - } - for (long i = 0; i < b; ++i) { - int slot = (int)(((long)frame * b + i) % MAX_SECTIONS); - long bytes = g_quadsPerSection * 4 * VERT_STRIDE; - glBindBuffer(GL_ARRAY_BUFFER, g_vbo[slot]); - glBufferData(GL_ARRAY_BUFFER, bytes, NULL, GL_STATIC_DRAW); - glBufferSubData(GL_ARRAY_BUFFER, 0, bytes, g_scratch); - } - long entityEvery = 10; - for (long i = 0; i < a; ++i) { - if (i % entityEvery == entityEvery - 1) { - glUseProgram(g_progEntity); - glUniformMatrix4fv(g_uMvpEntity, 1, 0, g_mvp); - glBindTexture(GL_TEXTURE_2D, g_texEntity); - glBindVertexArray(g_vao[i % MAX_SECTIONS]); - glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0); - glUseProgram(g_progChunk); - glBindTexture(GL_TEXTURE_2D, g_texAtlas); - } else { - glBindVertexArray(g_vao[i % MAX_SECTIONS]); - glUniform3f(g_uOffsetChunk, (float)(i & 15), (float)((i >> 4) & 15), 0.0f); - glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0); - } - } -} - -/* ---- Trace-derived cases ------------------------------------------------- - * Per-frame call mixes measured from the three captured Minecraft traces - * (render distance 32, 1280x720, hovering in-world). Each case reproduces one - * renderer's dominant per-draw sequence at its measured rate, so the number a - * backend posts here is directly comparable to what that game version asks of - * the driver every frame. - * - * vanilla 1.21.1 : 5495 glDrawElements, 5490 glBindVertexArray, - * 5487 glUniform3fv, 95 glTexSubImage2D (+382 glPixelStorei, - * 247 glTexParameteri), 23 glBufferData per frame - * fabric+sodium : 132 glMultiDrawElementsBaseVertex, 279 glBindVertexArray, - * 132 glUniform3f, 32 glBufferData per frame - * 26.2 snapshot : 3401 glDrawElementsBaseVertex, each preceded by - * glBindBufferRange + glBindBuffer (3639/3412 per frame) - */ -/* vanilla: bind VAO, push the chunk offset, draw. a = draws per frame. */ -static void case_mc_vanilla_draw(int frame, long a, long b) { - (void)frame; (void)b; - float offset[3]; - for (long i = 0; i < a; ++i) { - glBindVertexArray(g_vao[i % MAX_SECTIONS]); - offset[0] = (float)(i & 15); - offset[1] = (float)((i >> 4) & 15); - offset[2] = 0.0f; - glUniform3fv(g_uOffsetChunk, 1, offset); - glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0); - } -} - -/* sodium: one multi-draw covers many chunk sections out of a shared buffer. - * a = multi-draws per frame, b = sub-draws inside each. */ -static void case_mc_sodium_multidraw(int frame, long a, long b) { - (void)frame; - enum { kMaxSub = 64 }; - if (b <= 0 || b > kMaxSub) b = 32; - GLsizei counts[kMaxSub]; - const void* offsets[kMaxSub]; - GLint baseVertices[kMaxSub]; - for (long s = 0; s < b; ++s) { - counts[s] = (GLsizei)(g_quadsPerSection * 6 / b); - offsets[s] = (const void*)(uintptr_t)(s * (g_quadsPerSection * 6 / b) * 4); - baseVertices[s] = 0; - } - for (long i = 0; i < a; ++i) { - glBindVertexArray(g_vao[i % MAX_SECTIONS]); - glBindVertexArray(g_vao[i % MAX_SECTIONS]); /* sodium rebinds ~2x per draw */ - glUniform3f(g_uOffsetChunk, (float)(i & 15), (float)((i >> 4) & 15), 0.0f); - glMultiDrawElementsBaseVertex(GL_TRIANGLES, counts, GL_UNSIGNED_INT, offsets, - (GLsizei)b, baseVertices); - } -} - -/* 26.2: every draw rebinds a fresh uniform-buffer range out of a ring. - * a = draws per frame. */ -static void case_mc_ubo_range(int frame, long a, long b) { - (void)b; - const size_t slots = (4u * 1024u * 1024u) / g_uboSlot; - for (long i = 0; i < a; ++i) { - const size_t slot = (size_t)(((long)frame * a + i) % (long)slots); - glBindBufferRange(GL_UNIFORM_BUFFER, 0, g_uboRing, (GLintptr)(slot * g_uboSlot), - (GLsizeiptr)g_uboSlot); - glBindBuffer(GL_UNIFORM_BUFFER, g_uboRing); - glDrawElementsBaseVertex(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0, 0); - } -} - -/* vanilla's animated-sprite path: every upload is wrapped in the pixel-store - * and filter state Blaze3D re-sets around it. a = uploads per frame. */ -static void case_mc_tex_stream(int frame, long a, long b) { - (void)b; - glBindVertexArray(g_vao[0]); - glBindTexture(GL_TEXTURE_2D, g_texAtlas); - for (long i = 0; i < a; ++i) { - glPixelStorei(GL_UNPACK_ALIGNMENT, 4); - glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); - glPixelStorei(GL_UNPACK_SKIP_ROWS, 0); - glPixelStorei(GL_UNPACK_SKIP_PIXELS, 0); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - int x = (int)((frame * 13 + i * 17) % (1024 - 16)); - int y = (int)((frame * 7 + i * 29) % (512 - 16)); - glTexSubImage2D(GL_TEXTURE_2D, 0, x, y, 16, 16, GL_RGBA, GL_UNSIGNED_BYTE, g_scratch); - } - glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0); -} - -/* Blaze3D re-resolves uniform locations by name every frame. a = lookups. */ -static void case_mc_uniform_lookup(int frame, long a, long b) { - (void)frame; (void)b; - static const char* names[4] = {"uMvp", "uOffset", "uAtlas", "uLight"}; - volatile GLint sink = 0; - for (long i = 0; i < a; ++i) sink += glGetUniformLocation(g_progChunk, names[i & 3]); - (void)sink; - glBindVertexArray(g_vao[0]); - glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0); -} - -/* 26.2 rebinds a sampler object per texture unit switch. a = switches. */ -static void case_mc_sampler_churn(int frame, long a, long b) { - (void)frame; (void)b; - glBindVertexArray(g_vao[0]); - for (long i = 0; i < a; ++i) { - glActiveTexture(GL_TEXTURE0 + (GLenum)(i & 3)); - glBindTexture(GL_TEXTURE_2D, (i & 1) ? g_texEntity : g_texAtlas); - glBindSampler((GLuint)(i & 3), g_sampler); - glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0); - } - glActiveTexture(GL_TEXTURE0); -} - /* ---- EGL bootstrap: one provider library, pbuffer, desktop-GL context ---- */ static int boot_egl(void) { const char* libpath = getenv("DRIVERBENCH_EGL_LIB"); @@ -711,29 +313,50 @@ static int boot_egl(void) { } fprintf(stderr, "EGL %d.%d via %s\n", maj, min, libpath); - ((EGLBoolean(*)(EGLenum))p_eglBindAPI)(EGL_OPENGL_API); - - const EGLint cfgAttribs[] = {EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, EGL_RED_SIZE, 8, - EGL_DEPTH_SIZE, 24, EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT, EGL_NONE}; - const EGLint cfgAttribsRelaxed[] = {EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, EGL_RED_SIZE, 8, EGL_NONE}; - EGLConfig cfg = NULL; - EGLint ncfg = 0; + // Desktop GL first (that is what MobileGL exposes and what the cases are + // written against), GLES 3 second so the same binary can measure a device's + // native driver as the baseline. The .inc picks ESSL shader sources when the + // context turns out to be ES. EGLBoolean (*chooseConfig)(EGLDisplay, const EGLint*, EGLConfig*, EGLint, EGLint*) = (EGLBoolean(*)(EGLDisplay, const EGLint*, EGLConfig*, EGLint, EGLint*))p_eglChooseConfig; - if (!chooseConfig(dpy, cfgAttribs, &cfg, 1, &ncfg) || ncfg < 1) { - if (!chooseConfig(dpy, cfgAttribsRelaxed, &cfg, 1, &ncfg) || ncfg < 1) { - fprintf(stderr, "FAIL: eglChooseConfig\n"); - return 1; - } - } - - const EGLint ctxAttribs[] = {EGL_CONTEXT_MAJOR_VERSION, 3, EGL_CONTEXT_MINOR_VERSION, 2, - EGL_CONTEXT_OPENGL_PROFILE_MASK, EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT, - EGL_NONE}; EGLContext (*createContext)(EGLDisplay, EGLConfig, EGLContext, const EGLint*) = (EGLContext(*)(EGLDisplay, EGLConfig, EGLContext, const EGLint*))p_eglCreateContext; - EGLContext ctx = createContext(dpy, cfg, EGL_NO_CONTEXT, ctxAttribs); - if (ctx == EGL_NO_CONTEXT) ctx = createContext(dpy, cfg, EGL_NO_CONTEXT, NULL); + EGLBoolean (*bindApi)(EGLenum) = (EGLBoolean(*)(EGLenum))p_eglBindAPI; + + EGLConfig cfg = NULL; + EGLint ncfg = 0; + EGLContext ctx = EGL_NO_CONTEXT; + + if (bindApi(EGL_OPENGL_API)) { + const EGLint cfgAttribs[] = {EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, EGL_RED_SIZE, 8, + EGL_DEPTH_SIZE, 24, EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT, EGL_NONE}; + if (chooseConfig(dpy, cfgAttribs, &cfg, 1, &ncfg) && ncfg >= 1) { + const EGLint ctxAttribs[] = {EGL_CONTEXT_MAJOR_VERSION, 3, EGL_CONTEXT_MINOR_VERSION, 2, + EGL_CONTEXT_OPENGL_PROFILE_MASK, + EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT, EGL_NONE}; + ctx = createContext(dpy, cfg, EGL_NO_CONTEXT, ctxAttribs); + if (ctx == EGL_NO_CONTEXT) ctx = createContext(dpy, cfg, EGL_NO_CONTEXT, NULL); + } + } + if (ctx == EGL_NO_CONTEXT) { + if (!bindApi(EGL_OPENGL_ES_API)) { + fprintf(stderr, "FAIL: neither OpenGL nor OpenGL ES is bindable on this provider\n"); + return 1; + } + const EGLint esCfgAttribs[] = {EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, EGL_RED_SIZE, 8, + EGL_GREEN_SIZE, 8, EGL_BLUE_SIZE, 8, EGL_DEPTH_SIZE, 24, + EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT, EGL_NONE}; + ncfg = 0; + if (!chooseConfig(dpy, esCfgAttribs, &cfg, 1, &ncfg) || ncfg < 1) { + const EGLint relaxed[] = {EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, EGL_RED_SIZE, 8, EGL_NONE}; + if (!chooseConfig(dpy, relaxed, &cfg, 1, &ncfg) || ncfg < 1) { + fprintf(stderr, "FAIL: eglChooseConfig\n"); + return 1; + } + } + const EGLint esCtxAttribs[] = {EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE}; + ctx = createContext(dpy, cfg, EGL_NO_CONTEXT, esCtxAttribs); + } if (ctx == EGL_NO_CONTEXT) { fprintf(stderr, "FAIL: eglCreateContext (0x%x)\n", ((EGLint(*)(void))p_eglGetError)()); return 1; @@ -775,24 +398,31 @@ static int boot_egl(void) { RESOLVE(glGetUniformLocation); RESOLVE(glUniform1i); RESOLVE(glUniform3f); RESOLVE(glUniformMatrix4fv); RESOLVE(glDrawElements); RESOLVE(glBindAttribLocation); RESOLVE(glUniform3fv); RESOLVE(glDrawArrays); RESOLVE(glDrawElementsBaseVertex); - RESOLVE(glMultiDrawElementsBaseVertex); RESOLVE(glBindBufferRange); RESOLVE(glBindBufferBase); + RESOLVE(glBindBufferRange); RESOLVE(glBindBufferBase); RESOLVE(glGetUniformBlockIndex); RESOLVE(glUniformBlockBinding); RESOLVE(glGenSamplers); RESOLVE(glBindSampler); RESOLVE(glSamplerParameteri); RESOLVE(glGenFramebuffers); RESOLVE(glBindFramebuffer); RESOLVE(glGenRenderbuffers); RESOLVE(glBindRenderbuffer); RESOLVE(glRenderbufferStorage); RESOLVE(glFramebufferRenderbuffer); RESOLVE(glCheckFramebufferStatus); + // Optional: end_frame_wait() falls back to glFinish when a stack has no + // sync objects, so resolve without failing the run. + *(void**)&glFenceSync = g_eglGetProcAddress("glFenceSync"); + if (!glFenceSync) *(void**)&glFenceSync = dlsym(g_provider, "glFenceSync"); + *(void**)&glClientWaitSync = g_eglGetProcAddress("glClientWaitSync"); + if (!glClientWaitSync) *(void**)&glClientWaitSync = dlsym(g_provider, "glClientWaitSync"); + *(void**)&glDeleteSync = g_eglGetProcAddress("glDeleteSync"); + if (!glDeleteSync) *(void**)&glDeleteSync = dlsym(g_provider, "glDeleteSync"); + // Desktop-only: GLES 3.2 has DrawElementsBaseVertex but no multi-draw form, + // so bench_multi_draw_elements_base_vertex() emulates it when this is null. + *(void**)&glMultiDrawElementsBaseVertex = g_eglGetProcAddress("glMultiDrawElementsBaseVertex"); + if (!glMultiDrawElementsBaseVertex) + *(void**)&glMultiDrawElementsBaseVertex = dlsym(g_provider, "glMultiDrawElementsBaseVertex"); fprintf(stderr, "renderer: %s\n", glGetString(GL_RENDERER)); fprintf(stderr, "version: %s\n", glGetString(GL_VERSION)); return 0; } -typedef struct { - const char* name; - case_fn fn; - long a, b, opsPerFrame; -} BenchCase; - int main(int argc, char** argv) { long draws = 2048; if (getenv("DRIVERBENCH_DRAWS")) draws = atol(getenv("DRIVERBENCH_DRAWS")); @@ -802,36 +432,23 @@ int main(int argc, char** argv) { if (boot_egl()) return 1; build_resources(); - // Rates are the measured per-frame call counts of each trace, so one - // bench frame costs what one real frame of that game version costs. - BenchCase cases[] = { - {"mc_vanilla_draw", case_mc_vanilla_draw, 5495, 0, 5495}, - {"mc_sodium_multidraw", case_mc_sodium_multidraw, 132, 32, 132}, - {"mc_ubo_range", case_mc_ubo_range, 3401, 0, 3401}, - {"mc_tex_stream", case_mc_tex_stream, 95, 0, 95}, - {"mc_uniform_lookup", case_mc_uniform_lookup, 41, 0, 41}, - {"mc_sampler_churn", case_mc_sampler_churn, 306, 0, 306}, - {"draw_tiny", case_draw_tiny, draws, 0, draws}, - {"draw_uniform", case_draw_uniform, draws, 0, draws}, - {"draw_multi_vao", case_draw_multi_vao, draws, 0, draws}, - {"tex_pingpong", case_tex_pingpong, draws / 2, 0, draws / 2}, - {"program_pingpong", case_program_pingpong, draws / 4, 0, draws / 4}, - {"chunk_upload", case_chunk_upload, 24, 0, 24}, - {"atlas_sprite", case_atlas_sprite, 32, 0, 32}, - {"lightmap", case_lightmap, 4, 0, 4}, - {"scene_mix", case_scene_mix, draws, 12, draws}, - }; - int ncases = (int)(sizeof cases / sizeof cases[0]); - printf("case,frames,ops_per_frame,median_frame_ms,ns_per_op,fps\n"); - for (int i = 0; i < ncases; ++i) { + for (int i = 0; i < kBenchCaseCount; ++i) { + const BenchCaseDesc* c = &kBenchCases[i]; if (argc > 1) { int wanted = 0; for (int j = 1; j < argc; ++j) - if (strcmp(argv[j], cases[i].name) == 0) wanted = 1; + if (strcmp(argv[j], c->name) == 0) wanted = 1; if (!wanted) continue; } - run_case(cases[i].name, cases[i].fn, cases[i].a, cases[i].b, cases[i].opsPerFrame); + // The generic cases scale with DRIVERBENCH_DRAWS; the mc_* rates are + // measured and must not move, or the numbers stop being comparable. + long a = c->a, ops = c->opsPerFrame; + if (strncmp(c->name, "mc_", 3) != 0 && a > 100) { + a = draws * a / 2048; + ops = c->opsPerFrame * draws / 2048; + } + run_case(c->name, c->fn, a, c->b, ops); } return 0; } diff --git a/MobileGL/MG_Benchmark/Driver/DriverBenchCases.inc b/MobileGL/MG_Benchmark/Driver/DriverBenchCases.inc new file mode 100644 index 00000000..7b8aaa2c --- /dev/null +++ b/MobileGL/MG_Benchmark/Driver/DriverBenchCases.inc @@ -0,0 +1,549 @@ +/* MobileGL - MobileGL/MG_Benchmark/Driver/DriverBenchCases.inc + * 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 + * + * The benchmark scene and its cases, with no harness and no GL loader: the + * includer supplies both. DriverBench.c drives it through function pointers + * resolved from one EGL provider; MG_Util/SelfTest/DriverBenchJni.cpp drives + * it through MobileGL's own frontend entry points inside the Android plugin. + * Sharing the bodies is the point - a number from the phone and a number from + * the desktop have to describe the same work. + * + * The includer must have declared, before including this file: the GL types + * and enums used below, and callable gl* entry points with the standard + * signatures. bench_gl_failed() is called (and must be defined) when shader + * compilation or linking fails, so a caller can report the failure instead of + * dying inside a benchmark. + */ + +/* ---- shared scene resources (Minecraft-shaped) ---- */ +#define MAX_SECTIONS 512 +static GLuint g_progChunk, g_progEntity; +static GLint g_uOffsetChunk, g_uMvpChunk, g_uMvpEntity; +static GLuint g_vao[MAX_SECTIONS], g_vbo[MAX_SECTIONS]; +static GLuint g_sharedIbo; +static GLuint g_texAtlas, g_texLight, g_texEntity; +static int g_quadsPerSection = 128; /* 128 quads = 512 verts, 768 indices */ +static unsigned char* g_scratch; +/* Uniform ring + sampler for the 26.2-shaped cases (see the case block below). */ +static GLuint g_uboRing; +static GLint g_uboAlign = 256; +static size_t g_uboSlot = 256; +static GLuint g_sampler; +static float g_mvp[16] = {0.002f, 0, 0, 0, 0, 0.002f, 0, 0, 0, 0, -0.001f, 0, -1.f, -1.f, 0.f, 1.f}; + +/* Minecraft chunk vertex: pos 3f, color 4ub, uv 2f, packed light 2s -> 32 B */ +#define VERT_STRIDE 32 +static void fill_section_vertices(unsigned char* dst, int quads, unsigned seed) { + for (int q = 0; q < quads * 4; ++q) { + float* f = (float*)(dst + q * VERT_STRIDE); + unsigned r = seed = seed * 1664525u + 1013904223u; + f[0] = (float)(q & 31) * 8.0f + (float)(r & 7); + f[1] = (float)((q >> 5) & 31) * 8.0f; + f[2] = (float)(q % 7) * 0.1f; + dst[q * VERT_STRIDE + 12] = (unsigned char)r; + dst[q * VERT_STRIDE + 13] = (unsigned char)(r >> 8); + dst[q * VERT_STRIDE + 14] = (unsigned char)(r >> 16); + dst[q * VERT_STRIDE + 15] = 255; + f[4] = (float)(r & 1023) / 1024.0f; + f[5] = (float)((r >> 10) & 511) / 512.0f; + ((short*)(dst + q * VERT_STRIDE + 24))[0] = 15 << 4; + ((short*)(dst + q * VERT_STRIDE + 24))[1] = 15 << 4; + } +} + +static GLuint make_shader(GLenum kind, const char* src) { + GLuint sh = glCreateShader(kind); + glShaderSource(sh, 1, &src, NULL); + glCompileShader(sh); + GLint ok = 0; + glGetShaderiv(sh, GL_COMPILE_STATUS, &ok); + if (!ok) { + char log[1024]; + glGetShaderInfoLog(sh, sizeof log, NULL, log); + bench_gl_failed("shader compile", log); + return 0; + } + return sh; +} + +static GLuint make_program(const char* vs_src, const char* fs_src) { + GLuint prog = glCreateProgram(); + glAttachShader(prog, make_shader(GL_VERTEX_SHADER, vs_src)); + glAttachShader(prog, make_shader(GL_FRAGMENT_SHADER, fs_src)); + glBindAttribLocation(prog, 0, "aPos"); + glBindAttribLocation(prog, 1, "aColor"); + glBindAttribLocation(prog, 2, "aUv"); + glBindAttribLocation(prog, 3, "aLight"); + glLinkProgram(prog); + GLint ok = 0; + glGetProgramiv(prog, GL_LINK_STATUS, &ok); + if (!ok) { + bench_gl_failed("program link", ""); + return 0; + } + return prog; +} + +static const char* kChunkVs = + "#version 150 core\n" + "in vec3 aPos; in vec4 aColor; in vec2 aUv; in vec2 aLight;\n" + "uniform mat4 uMvp; uniform vec3 uOffset;\n" + "out vec4 vColor; out vec2 vUv; out vec2 vLight;\n" + "void main(){ gl_Position = uMvp * vec4(aPos + uOffset, 1.0);\n" + " vColor = aColor; vUv = aUv; vLight = aLight * (1.0/256.0); }\n"; +static const char* kChunkFs = + "#version 150 core\n" + "in vec4 vColor; in vec2 vUv; in vec2 vLight; out vec4 o;\n" + "uniform sampler2D uAtlas; uniform sampler2D uLight;\n" + "void main(){ o = texture(uAtlas, vUv) * vColor * texture(uLight, vLight); }\n"; +static const char* kEntityVs = + "#version 150 core\n" + "in vec3 aPos; in vec4 aColor; in vec2 aUv; in vec2 aLight;\n" + "uniform mat4 uMvp; uniform mat4 uModel;\n" + "out vec4 vColor; out vec2 vUv;\n" + "void main(){ gl_Position = uMvp * uModel * vec4(aPos, 1.0); vColor = aColor; vUv = aUv; }\n"; +static const char* kEntityFs = + "#version 150 core\n" + "in vec4 vColor; in vec2 vUv; out vec4 o; uniform sampler2D uTex;\n" + "void main(){ o = texture(uTex, vUv) * vColor; }\n"; + +// ESSL 3.20 twins of the four shaders above. The bodies are identical; only the +// version line and the precision qualifiers differ, so the two paths compile the +// same work. Needed because this bench also runs against a device's native GLES +// driver as the baseline MobileGL is measured against, and that driver rejects +// desktop GLSL - while MobileGL is fed desktop GLSL on purpose, since translating +// it is the thing under test. +static const char* kChunkVsEs = + "#version 320 es\n" + "precision highp float;\n" + "in vec3 aPos; in vec4 aColor; in vec2 aUv; in vec2 aLight;\n" + "uniform mat4 uMvp; uniform vec3 uOffset;\n" + "out vec4 vColor; out vec2 vUv; out vec2 vLight;\n" + "void main(){ gl_Position = uMvp * vec4(aPos + uOffset, 1.0);\n" + " vColor = aColor; vUv = aUv; vLight = aLight * (1.0/256.0); }\n"; +static const char* kChunkFsEs = + "#version 320 es\n" + "precision mediump float;\n" + "in vec4 vColor; in vec2 vUv; in vec2 vLight; out vec4 o;\n" + "uniform sampler2D uAtlas; uniform sampler2D uLight;\n" + "void main(){ o = texture(uAtlas, vUv) * vColor * texture(uLight, vLight); }\n"; +static const char* kEntityVsEs = + "#version 320 es\n" + "precision highp float;\n" + "in vec3 aPos; in vec4 aColor; in vec2 aUv; in vec2 aLight;\n" + "uniform mat4 uMvp; uniform mat4 uModel;\n" + "out vec4 vColor; out vec2 vUv;\n" + "void main(){ gl_Position = uMvp * uModel * vec4(aPos, 1.0); vColor = aColor; vUv = aUv; }\n"; +static const char* kEntityFsEs = + "#version 320 es\n" + "precision mediump float;\n" + "in vec4 vColor; in vec2 vUv; out vec4 o; uniform sampler2D uTex;\n" + "void main(){ o = texture(uTex, vUv) * vColor; }\n"; + +// True once build_resources() has seen a GL_VERSION beginning with "OpenGL ES". +static int g_isGlesContext = 0; + +static void setup_vao(GLuint vao, GLuint vbo, GLuint ibo) { + glBindVertexArray(vao); + glBindBuffer(GL_ARRAY_BUFFER, vbo); + glEnableVertexAttribArray(0); + glEnableVertexAttribArray(1); + glEnableVertexAttribArray(2); + glEnableVertexAttribArray(3); + glVertexAttribPointer(0, 3, GL_FLOAT, 0, VERT_STRIDE, (void*)0); + glVertexAttribPointer(1, 4, GL_UNSIGNED_BYTE, 1, VERT_STRIDE, (void*)12); + glVertexAttribPointer(2, 2, GL_FLOAT, 0, VERT_STRIDE, (void*)16); + glVertexAttribPointer(3, 2, GL_SHORT, 0, VERT_STRIDE, (void*)24); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo); +} + +static void build_resources(void) { + /* offscreen render target: 1280x720 RBO FBO, like CTS fbo surface mode */ + GLuint fbo, rboColor, rboDepth; + glGenFramebuffers(1, &fbo); + glBindFramebuffer(GL_FRAMEBUFFER, fbo); + glGenRenderbuffers(1, &rboColor); + glBindRenderbuffer(GL_RENDERBUFFER, rboColor); + glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, 1280, 720); + glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, rboColor); + glGenRenderbuffers(1, &rboDepth); + glBindRenderbuffer(GL_RENDERBUFFER, rboDepth); + glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, 1280, 720); + glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, rboDepth); + if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { + bench_gl_failed("FBO incomplete", ""); + return; + } + + const char* versionString = (const char*)glGetString(GL_VERSION); + g_isGlesContext = versionString != NULL && strncmp(versionString, "OpenGL ES", 9) == 0; + g_progChunk = g_isGlesContext ? make_program(kChunkVsEs, kChunkFsEs) : make_program(kChunkVs, kChunkFs); + g_progEntity = g_isGlesContext ? make_program(kEntityVsEs, kEntityFsEs) : make_program(kEntityVs, kEntityFs); + glUseProgram(g_progChunk); + g_uMvpChunk = glGetUniformLocation(g_progChunk, "uMvp"); + g_uOffsetChunk = glGetUniformLocation(g_progChunk, "uOffset"); + glUniform1i(glGetUniformLocation(g_progChunk, "uAtlas"), 0); + glUniform1i(glGetUniformLocation(g_progChunk, "uLight"), 2); + glUniformMatrix4fv(g_uMvpChunk, 1, 0, g_mvp); + glUseProgram(g_progEntity); + g_uMvpEntity = glGetUniformLocation(g_progEntity, "uMvp"); + glUniform1i(glGetUniformLocation(g_progEntity, "uTex"), 0); + glUniformMatrix4fv(g_uMvpEntity, 1, 0, g_mvp); + glUseProgram(g_progChunk); + + /* shared quad index buffer, like Blaze3D's RenderSystem shared sequences */ + int maxQuads = 4096; + unsigned* idx = (unsigned*)malloc((size_t)maxQuads * 6 * 4); + for (int q = 0; q < maxQuads; ++q) { + unsigned base = q * 4; + unsigned* p = idx + q * 6; + p[0] = base; p[1] = base + 1; p[2] = base + 2; + p[3] = base + 2; p[4] = base + 3; p[5] = base; + } + glGenBuffers(1, &g_sharedIbo); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, g_sharedIbo); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, maxQuads * 6 * 4, idx, GL_STATIC_DRAW); + free(idx); + + g_scratch = (unsigned char*)malloc(4 * 1024 * 1024); + memset(g_scratch, 0x5a, 4 * 1024 * 1024); + + glGenVertexArrays(MAX_SECTIONS, g_vao); + glGenBuffers(MAX_SECTIONS, g_vbo); + int bytes = g_quadsPerSection * 4 * VERT_STRIDE; + for (int i = 0; i < MAX_SECTIONS; ++i) { + fill_section_vertices(g_scratch, g_quadsPerSection, i * 7919u + 1); + glBindBuffer(GL_ARRAY_BUFFER, g_vbo[i]); + glBufferData(GL_ARRAY_BUFFER, bytes, g_scratch, GL_STATIC_DRAW); + setup_vao(g_vao[i], g_vbo[i], g_sharedIbo); + } + + glGenTextures(1, &g_texAtlas); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, g_texAtlas); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 1024, 512, 0, GL_RGBA, GL_UNSIGNED_BYTE, g_scratch); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glGenerateMipmap(GL_TEXTURE_2D); + + glGenTextures(1, &g_texLight); + glActiveTexture(GL_TEXTURE0 + 2); + glBindTexture(GL_TEXTURE_2D, g_texLight); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 16, 16, 0, GL_RGBA, GL_UNSIGNED_BYTE, g_scratch); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + + glGenTextures(1, &g_texEntity); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, g_texEntity); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 64, 64, 0, GL_RGBA, GL_UNSIGNED_BYTE, g_scratch); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glBindTexture(GL_TEXTURE_2D, g_texAtlas); + + // Uniform ring the 26.2-style case sub-ranges into, sized like a real + // frame's worth of per-draw uniform slots. + GLint align = 256; + glGetIntegerv(GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT, &align); + g_uboAlign = align > 0 ? align : 256; + g_uboSlot = (size_t)g_uboAlign; + glGenBuffers(1, &g_uboRing); + glBindBuffer(GL_UNIFORM_BUFFER, g_uboRing); + glBufferData(GL_UNIFORM_BUFFER, 4 * 1024 * 1024, g_scratch, GL_DYNAMIC_DRAW); + glBindBuffer(GL_UNIFORM_BUFFER, 0); + + glGenSamplers(1, &g_sampler); + glSamplerParameteri(g_sampler, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glSamplerParameteri(g_sampler, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + + glEnable(GL_DEPTH_TEST); + glClearColor(0.3f, 0.5f, 0.9f, 1.0f); + glViewport(0, 0, 1280, 720); + const GLenum setupError = glGetError(); + if (setupError != GL_NO_ERROR) { + char message[64]; + snprintf(message, sizeof message, "0x%04x", setupError); + bench_gl_failed("GL error during resource setup", message); + } +} + +static void case_draw_tiny(int frame, long a, long b) { + (void)frame; (void)b; + glBindVertexArray(g_vao[0]); + for (long i = 0; i < a; ++i) glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0); +} + +static void case_draw_uniform(int frame, long a, long b) { + (void)frame; (void)b; + glBindVertexArray(g_vao[0]); + for (long i = 0; i < a; ++i) { + glUniform3f(g_uOffsetChunk, (float)(i & 15), (float)((i >> 4) & 15), 0.0f); + glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0); + } +} + +static void case_draw_multi_vao(int frame, long a, long b) { + (void)frame; (void)b; + for (long i = 0; i < a; ++i) { + glBindVertexArray(g_vao[i % MAX_SECTIONS]); + glUniform3f(g_uOffsetChunk, (float)(i & 15), (float)((i >> 4) & 15), 0.0f); + glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0); + } +} + +static void case_tex_pingpong(int frame, long a, long b) { + (void)frame; (void)b; + glBindVertexArray(g_vao[0]); + for (long i = 0; i < a; ++i) { + glBindTexture(GL_TEXTURE_2D, (i & 1) ? g_texEntity : g_texAtlas); + glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0); + } + glBindTexture(GL_TEXTURE_2D, g_texAtlas); +} + +static void case_program_pingpong(int frame, long a, long b) { + (void)frame; (void)b; + glBindVertexArray(g_vao[0]); + for (long i = 0; i < a; ++i) { + if (i & 1) { + glUseProgram(g_progEntity); + glUniformMatrix4fv(g_uMvpEntity, 1, 0, g_mvp); + } else { + glUseProgram(g_progChunk); + glUniform3f(g_uOffsetChunk, (float)(i & 15), 0.0f, 0.0f); + } + glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0); + } + glUseProgram(g_progChunk); +} + +/* a = uploads per frame, b = bytes per upload (0 => section size) */ +static void case_chunk_upload(int frame, long a, long b) { + if (b <= 0) b = g_quadsPerSection * 4 * VERT_STRIDE; + if (b > 4 * 1024 * 1024) b = 4 * 1024 * 1024; + for (long i = 0; i < a; ++i) { + int slot = (int)(((long)frame * a + i) % MAX_SECTIONS); + glBindBuffer(GL_ARRAY_BUFFER, g_vbo[slot]); + glBufferData(GL_ARRAY_BUFFER, b, NULL, GL_STATIC_DRAW); /* orphan */ + glBufferSubData(GL_ARRAY_BUFFER, 0, b, g_scratch); + glBindVertexArray(g_vao[slot]); + glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0); + } +} + +/* a = sprite updates per frame */ +static void case_atlas_sprite(int frame, long a, long b) { + (void)b; + glBindVertexArray(g_vao[0]); + glBindTexture(GL_TEXTURE_2D, g_texAtlas); + for (long i = 0; i < a; ++i) { + int x = (int)((frame * 13 + i * 17) % (1024 - 16)); + int y = (int)((frame * 7 + i * 29) % (512 - 16)); + glTexSubImage2D(GL_TEXTURE_2D, 0, x, y, 16, 16, GL_RGBA, GL_UNSIGNED_BYTE, g_scratch); + } + glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0); +} + +/* a = lightmap updates (+draw) per frame */ +static void case_lightmap(int frame, long a, long b) { + (void)frame; (void)b; + glBindVertexArray(g_vao[0]); + for (long i = 0; i < a; ++i) { + glActiveTexture(GL_TEXTURE0 + 2); + glBindTexture(GL_TEXTURE_2D, g_texLight); + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 16, 16, GL_RGBA, GL_UNSIGNED_BYTE, g_scratch); + glActiveTexture(GL_TEXTURE0); + glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0); + } +} + +/* Composite: a = total draws, b = uploads per frame. Mix modeled on trace + * analysis: chunk draws with per-draw offset uniform across sections, 10% + * entity-style program flips, per-frame lightmap + sprite updates, b chunk + * re-uploads. */ +static long g_mixSprites = 8; +static void case_scene_mix(int frame, long a, long b) { + glActiveTexture(GL_TEXTURE0 + 2); + glBindTexture(GL_TEXTURE_2D, g_texLight); + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 16, 16, GL_RGBA, GL_UNSIGNED_BYTE, g_scratch); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, g_texAtlas); + for (long i = 0; i < g_mixSprites; ++i) { + int x = (int)((frame * 13 + i * 17) % (1024 - 16)); + int y = (int)((frame * 7 + i * 29) % (512 - 16)); + glTexSubImage2D(GL_TEXTURE_2D, 0, x, y, 16, 16, GL_RGBA, GL_UNSIGNED_BYTE, g_scratch); + } + for (long i = 0; i < b; ++i) { + int slot = (int)(((long)frame * b + i) % MAX_SECTIONS); + long bytes = g_quadsPerSection * 4 * VERT_STRIDE; + glBindBuffer(GL_ARRAY_BUFFER, g_vbo[slot]); + glBufferData(GL_ARRAY_BUFFER, bytes, NULL, GL_STATIC_DRAW); + glBufferSubData(GL_ARRAY_BUFFER, 0, bytes, g_scratch); + } + long entityEvery = 10; + for (long i = 0; i < a; ++i) { + if (i % entityEvery == entityEvery - 1) { + glUseProgram(g_progEntity); + glUniformMatrix4fv(g_uMvpEntity, 1, 0, g_mvp); + glBindTexture(GL_TEXTURE_2D, g_texEntity); + glBindVertexArray(g_vao[i % MAX_SECTIONS]); + glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0); + glUseProgram(g_progChunk); + glBindTexture(GL_TEXTURE_2D, g_texAtlas); + } else { + glBindVertexArray(g_vao[i % MAX_SECTIONS]); + glUniform3f(g_uOffsetChunk, (float)(i & 15), (float)((i >> 4) & 15), 0.0f); + glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0); + } + } +} + +/* ---- Trace-derived cases ------------------------------------------------- + * Per-frame call mixes measured from the three captured Minecraft traces + * (render distance 32, 1280x720, hovering in-world). Each case reproduces one + * renderer's dominant per-draw sequence at its measured rate, so the number a + * backend posts here is directly comparable to what that game version asks of + * the driver every frame. + * + * vanilla 1.21.1 : 5495 glDrawElements, 5490 glBindVertexArray, + * 5487 glUniform3fv, 95 glTexSubImage2D (+382 glPixelStorei, + * 247 glTexParameteri), 23 glBufferData per frame + * fabric+sodium : 132 glMultiDrawElementsBaseVertex, 279 glBindVertexArray, + * 132 glUniform3f, 32 glBufferData per frame + * 26.2 snapshot : 3401 glDrawElementsBaseVertex, each preceded by + * glBindBufferRange + glBindBuffer (3639/3412 per frame) + */ +/* vanilla: bind VAO, push the chunk offset, draw. a = draws per frame. */ +static void case_mc_vanilla_draw(int frame, long a, long b) { + (void)frame; (void)b; + float offset[3]; + for (long i = 0; i < a; ++i) { + glBindVertexArray(g_vao[i % MAX_SECTIONS]); + offset[0] = (float)(i & 15); + offset[1] = (float)((i >> 4) & 15); + offset[2] = 0.0f; + glUniform3fv(g_uOffsetChunk, 1, offset); + glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0); + } +} + +/* sodium: one multi-draw covers many chunk sections out of a shared buffer. + * a = multi-draws per frame, b = sub-draws inside each. */ +static void case_mc_sodium_multidraw(int frame, long a, long b) { + (void)frame; + enum { kMaxSub = 64 }; + if (b <= 0 || b > kMaxSub) b = 32; + GLsizei counts[kMaxSub]; + const void* offsets[kMaxSub]; + GLint baseVertices[kMaxSub]; + for (long s = 0; s < b; ++s) { + counts[s] = (GLsizei)(g_quadsPerSection * 6 / b); + offsets[s] = (const void*)(uintptr_t)(s * (g_quadsPerSection * 6 / b) * 4); + baseVertices[s] = 0; + } + for (long i = 0; i < a; ++i) { + glBindVertexArray(g_vao[i % MAX_SECTIONS]); + glBindVertexArray(g_vao[i % MAX_SECTIONS]); /* sodium rebinds ~2x per draw */ + glUniform3f(g_uOffsetChunk, (float)(i & 15), (float)((i >> 4) & 15), 0.0f); + // Routed through the includer: GLES has no multi-draw-with-base-vertex, so + // a native-driver harness emulates it with the loop the extension folds up. + bench_multi_draw_elements_base_vertex(GL_TRIANGLES, counts, GL_UNSIGNED_INT, offsets, + (GLsizei)b, baseVertices); + } +} + +/* 26.2: every draw rebinds a fresh uniform-buffer range out of a ring. + * a = draws per frame. */ +static void case_mc_ubo_range(int frame, long a, long b) { + (void)b; + const size_t slots = (4u * 1024u * 1024u) / g_uboSlot; + for (long i = 0; i < a; ++i) { + const size_t slot = (size_t)(((long)frame * a + i) % (long)slots); + glBindBufferRange(GL_UNIFORM_BUFFER, 0, g_uboRing, (GLintptr)(slot * g_uboSlot), + (GLsizeiptr)g_uboSlot); + glBindBuffer(GL_UNIFORM_BUFFER, g_uboRing); + glDrawElementsBaseVertex(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0, 0); + } +} + +/* vanilla's animated-sprite path: every upload is wrapped in the pixel-store + * and filter state Blaze3D re-sets around it. a = uploads per frame. */ +static void case_mc_tex_stream(int frame, long a, long b) { + (void)b; + glBindVertexArray(g_vao[0]); + glBindTexture(GL_TEXTURE_2D, g_texAtlas); + for (long i = 0; i < a; ++i) { + glPixelStorei(GL_UNPACK_ALIGNMENT, 4); + glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); + glPixelStorei(GL_UNPACK_SKIP_ROWS, 0); + glPixelStorei(GL_UNPACK_SKIP_PIXELS, 0); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + int x = (int)((frame * 13 + i * 17) % (1024 - 16)); + int y = (int)((frame * 7 + i * 29) % (512 - 16)); + glTexSubImage2D(GL_TEXTURE_2D, 0, x, y, 16, 16, GL_RGBA, GL_UNSIGNED_BYTE, g_scratch); + } + glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0); +} + +/* Blaze3D re-resolves uniform locations by name every frame. a = lookups. */ +static void case_mc_uniform_lookup(int frame, long a, long b) { + (void)frame; (void)b; + static const char* names[4] = {"uMvp", "uOffset", "uAtlas", "uLight"}; + volatile GLint sink = 0; + for (long i = 0; i < a; ++i) sink += glGetUniformLocation(g_progChunk, names[i & 3]); + (void)sink; + glBindVertexArray(g_vao[0]); + glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0); +} + +/* 26.2 rebinds a sampler object per texture unit switch. a = switches. */ +static void case_mc_sampler_churn(int frame, long a, long b) { + (void)frame; (void)b; + glBindVertexArray(g_vao[0]); + for (long i = 0; i < a; ++i) { + glActiveTexture(GL_TEXTURE0 + (GLenum)(i & 3)); + glBindTexture(GL_TEXTURE_2D, (i & 1) ? g_texEntity : g_texAtlas); + glBindSampler((GLuint)(i & 3), g_sampler); + glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0); + } + glActiveTexture(GL_TEXTURE0); +} + + +/* ---- the case table both harnesses iterate -------------------------------- + * a/b are the case's own knobs; opsPerFrame is what one bench frame is + * normalised by, so ns_per_op compares across renderers. The mc_* rates are + * the per-frame call counts measured from the captured traces. + */ +typedef void (*bench_case_fn)(int frame, long a, long b); + +typedef struct { + const char* name; + bench_case_fn fn; + long a, b, opsPerFrame; +} BenchCaseDesc; + +static const BenchCaseDesc kBenchCases[] = { + {"mc_vanilla_draw", case_mc_vanilla_draw, 5495, 0, 5495}, + {"mc_sodium_multidraw", case_mc_sodium_multidraw, 132, 32, 132}, + {"mc_ubo_range", case_mc_ubo_range, 3401, 0, 3401}, + {"mc_tex_stream", case_mc_tex_stream, 95, 0, 95}, + {"mc_uniform_lookup", case_mc_uniform_lookup, 41, 0, 41}, + {"mc_sampler_churn", case_mc_sampler_churn, 306, 0, 306}, + {"draw_tiny", case_draw_tiny, 2048, 0, 2048}, + {"draw_uniform", case_draw_uniform, 2048, 0, 2048}, + {"draw_multi_vao", case_draw_multi_vao, 2048, 0, 2048}, + {"tex_pingpong", case_tex_pingpong, 1024, 0, 1024}, + {"program_pingpong", case_program_pingpong, 512, 0, 512}, + {"chunk_upload", case_chunk_upload, 24, 0, 24}, + {"atlas_sprite", case_atlas_sprite, 32, 0, 32}, + {"lightmap", case_lightmap, 4, 0, 4}, + {"scene_mix", case_scene_mix, 2048, 12, 2048}, +}; +static const int kBenchCaseCount = (int)(sizeof kBenchCases / sizeof kBenchCases[0]); diff --git a/MobileGL/MG_Util/SelfTest/DriverBenchJni.cpp b/MobileGL/MG_Util/SelfTest/DriverBenchJni.cpp new file mode 100644 index 00000000..a27529a3 --- /dev/null +++ b/MobileGL/MG_Util/SelfTest/DriverBenchJni.cpp @@ -0,0 +1,340 @@ +// MobileGL - MobileGL/MG_Util/SelfTest/DriverBenchJni.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 + +// In-process driver benchmark for the plugin APK's POST screen: runs the +// shared MG_Benchmark case bodies THROUGH MobileGL's full translation stack +// on the backend the caller names, and returns per-case timings as JSON. +// +// Process contract (enforced by BenchService, which hosts this in its own +// android:process and exits afterwards): the backend is latched by +// MobileGL::Initialize() from MOBILEGL_BACKEND_TYPE, and Espryt's teardown +// terminates the process-default EGL display - both make a bench run +// unrepeatable and unsafe next to a live UI. One process, one backend, one +// run. +#ifdef __ANDROID__ + +#include + +#include +#include +#include +#include +#include +#include + +// Includes.h first: it pins GL_GLES_PROTOTYPES=0 so the GLES headers declare +// types and enums without also declaring the entry points this library defines. +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Bind every gl*/egl* name the shared cases use to MobileGL's own frontend, +// by name and not by linkage. Writing the bare gl* symbols here would leave the +// binding to the dynamic linker, and this library legitimately has the platform +// libEGL/libGLESv3 in its own lookup scope - a bench that quietly measured the +// device driver instead of the translation layer would look like very good news. +#define glActiveTexture MobileGL::MG_Impl::GLImpl::ActiveTexture +#define glAttachShader MobileGL::MG_Impl::GLImpl::AttachShader +#define glBindAttribLocation MobileGL::MG_Impl::GLImpl::BindAttribLocation +#define glBindBuffer MobileGL::MG_Impl::GLImpl::BindBuffer +#define glBindBufferRange MobileGL::MG_Impl::GLImpl::BindBufferRange +#define glBindFramebuffer MobileGL::MG_Impl::GLImpl::BindFramebuffer +#define glBindRenderbuffer MobileGL::MG_Impl::GLImpl::BindRenderbuffer +#define glBindSampler MobileGL::MG_Impl::GLImpl::BindSampler +#define glBindTexture MobileGL::MG_Impl::GLImpl::BindTexture +#define glBindVertexArray MobileGL::MG_Impl::GLImpl::BindVertexArray +#define glBufferData MobileGL::MG_Impl::GLImpl::BufferData +#define glBufferSubData MobileGL::MG_Impl::GLImpl::BufferSubData +#define glCheckFramebufferStatus MobileGL::MG_Impl::GLImpl::CheckFramebufferStatus +#define glClear MobileGL::MG_Impl::GLImpl::Clear +#define glClearColor MobileGL::MG_Impl::GLImpl::ClearColor +#define glClientWaitSync MobileGL::MG_Impl::GLImpl::ClientWaitSync +#define glCompileShader MobileGL::MG_Impl::GLImpl::CompileShader +#define glCreateProgram MobileGL::MG_Impl::GLImpl::CreateProgram +#define glCreateShader MobileGL::MG_Impl::GLImpl::CreateShader +#define glDeleteSync MobileGL::MG_Impl::GLImpl::DeleteSync +#define glDrawArrays MobileGL::MG_Impl::GLImpl::DrawArrays +#define glDrawElements MobileGL::MG_Impl::GLImpl::DrawElements +#define glDrawElementsBaseVertex MobileGL::MG_Impl::GLImpl::DrawElementsBaseVertex +#define glEnable MobileGL::MG_Impl::GLImpl::Enable +#define glEnableVertexAttribArray MobileGL::MG_Impl::GLImpl::EnableVertexAttribArray +#define glFenceSync MobileGL::MG_Impl::GLImpl::FenceSync +#define glFramebufferRenderbuffer MobileGL::MG_Impl::GLImpl::FramebufferRenderbuffer +#define glGenBuffers MobileGL::MG_Impl::GLImpl::GenBuffers +#define glGenFramebuffers MobileGL::MG_Impl::GLImpl::GenFramebuffers +#define glGenRenderbuffers MobileGL::MG_Impl::GLImpl::GenRenderbuffers +#define glGenSamplers MobileGL::MG_Impl::GLImpl::GenSamplers +#define glGenTextures MobileGL::MG_Impl::GLImpl::GenTextures +#define glGenVertexArrays MobileGL::MG_Impl::GLImpl::GenVertexArrays +#define glGenerateMipmap MobileGL::MG_Impl::GLImpl::GenerateMipmap +#define glGetError MobileGL::MG_Impl::GLImpl::GetError +#define glGetIntegerv MobileGL::MG_Impl::GLImpl::GetIntegerv +#define glGetProgramiv MobileGL::MG_Impl::GLImpl::GetProgramiv +#define glGetShaderInfoLog MobileGL::MG_Impl::GLImpl::GetShaderInfoLog +#define glGetShaderiv MobileGL::MG_Impl::GLImpl::GetShaderiv +#define glGetString MobileGL::MG_Impl::GLImpl::GetString +#define glGetUniformLocation MobileGL::MG_Impl::GLImpl::GetUniformLocation +#define glLinkProgram MobileGL::MG_Impl::GLImpl::LinkProgram +#define glMultiDrawElementsBaseVertex MobileGL::MG_Impl::GLImpl::MultiDrawElementsBaseVertex +#define glPixelStorei MobileGL::MG_Impl::GLImpl::PixelStorei +#define glRenderbufferStorage MobileGL::MG_Impl::GLImpl::RenderbufferStorage +#define glSamplerParameteri MobileGL::MG_Impl::GLImpl::SamplerParameteri +#define glShaderSource MobileGL::MG_Impl::GLImpl::ShaderSource +#define glTexImage2D MobileGL::MG_Impl::GLImpl::TexImage2D +#define glTexParameteri MobileGL::MG_Impl::GLImpl::TexParameteri +#define glTexSubImage2D MobileGL::MG_Impl::GLImpl::TexSubImage2D +#define glUniform1i MobileGL::MG_Impl::GLImpl::Uniform1i +#define glUniform3f MobileGL::MG_Impl::GLImpl::Uniform3f +#define glUniform3fv MobileGL::MG_Impl::GLImpl::Uniform3fv +#define glUniformMatrix4fv MobileGL::MG_Impl::GLImpl::UniformMatrix4fv +#define glUseProgram MobileGL::MG_Impl::GLImpl::UseProgram +#define glVertexAttribPointer MobileGL::MG_Impl::GLImpl::VertexAttribPointer +#define glViewport MobileGL::MG_Impl::GLImpl::Viewport +#define glFinish() ((void)0) // MobileGL's own glFinish is a no-op; never paced on + +#define eglChooseConfig MobileGL::MG_Impl::EGLImpl::ChooseConfig +#define eglCreateContext MobileGL::MG_Impl::EGLImpl::CreateContext +#define eglCreatePbufferSurface MobileGL::MG_Impl::EGLImpl::CreatePbufferSurface +#define eglGetDisplay MobileGL::MG_Impl::EGLImpl::GetDisplay +#define eglGetError MobileGL::MG_Impl::EGLImpl::GetError +#define eglInitialize MobileGL::MG_Impl::EGLImpl::Initialize +#define eglMakeCurrent MobileGL::MG_Impl::EGLImpl::MakeCurrent + +namespace { + + uint64_t NowNs() { + timespec ts{}; + clock_gettime(CLOCK_MONOTONIC, &ts); + return static_cast(ts.tv_sec) * 1000000000ull + static_cast(ts.tv_nsec); + } + + int CompareU64(const void* a, const void* b) { + const uint64_t x = *static_cast(a); + const uint64_t y = *static_cast(b); + return x < y ? -1 : x > y; + } + #define now_ns NowNs + #define cmp_u64 CompareU64 + + // Failure funnel for the shared scene builder: remember the first failure, + // let the run finish reporting it instead of aborting the service process. + std::string g_benchFailure; + void bench_gl_failed(const char* what, const char* detail) { + if (!g_benchFailure.empty()) return; + g_benchFailure = std::string(what) + (detail && detail[0] ? std::string(": ") + detail : ""); + } + + int g_frames = 120; + int g_warmup = 30; + + // MobileGL exports the desktop multi-draw entry point on every platform, so + // the shared cases always get the real call here (see the .inc for why this + // is routed through a hook at all). + void bench_multi_draw_elements_base_vertex(GLenum mode, const GLsizei* counts, GLenum type, + const void* const* offsets, GLsizei drawCount, + const GLint* baseVertices) { + glMultiDrawElementsBaseVertex(mode, counts, type, offsets, drawCount, baseVertices); + } + + #include "../../MG_Benchmark/Driver/DriverBenchCases.inc" + + // Frame boundary: a real fence wait. MobileGL's glFinish/glFlush are + // deliberate no-ops, so a glFinish-paced loop would time only CPU submit. + void EndFrameWait() { + GLsync sync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); + if (sync != nullptr) { + glClientWaitSync(sync, GL_SYNC_FLUSH_COMMANDS_BIT, 1000000000ull); + glDeleteSync(sync); + return; + } + glFinish(); + } + + std::string EscapeJson(const std::string& value) { + std::string out; + out.reserve(value.size() + 8); + for (const char c : value) { + switch (c) { + case '"': out += "\\\""; break; + case '\\': out += "\\\\"; break; + case '\n': out += "\\n"; break; + case '\r': out += "\\r"; break; + case '\t': out += "\\t"; break; + default: + if (static_cast(c) < 0x20 || static_cast(c) >= 0x7F) { + char buffer[8]; + snprintf(buffer, sizeof buffer, "\\u%04x", static_cast(c)); + out += buffer; + } else { + out += c; + } + } + } + return out; + } + + struct EglBench { + EGLDisplay display = EGL_NO_DISPLAY; + EGLSurface surface = EGL_NO_SURFACE; + EGLContext context = EGL_NO_CONTEXT; + }; + + // MobileGL's own EGL, driven exactly like a launcher drives it. No + // eglTerminate here: the service process exits right after the run, and + // Espryt's display teardown is precisely the hazard being avoided. + bool BootEgl(EglBench& out, std::string& error) { + out.display = eglGetDisplay(EGL_DEFAULT_DISPLAY); + if (out.display == EGL_NO_DISPLAY) { error = "eglGetDisplay failed"; return false; } + EGLint major = 0, minor = 0; + if (eglInitialize(out.display, &major, &minor) != EGL_TRUE) { + char buffer[48]; + snprintf(buffer, sizeof buffer, "eglInitialize failed (0x%04x)", eglGetError()); + error = buffer; + return false; + } + const EGLint configAttribs[] = {EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, EGL_RED_SIZE, 8, + EGL_GREEN_SIZE, 8, EGL_BLUE_SIZE, 8, EGL_DEPTH_SIZE, 24, + EGL_NONE}; + EGLConfig config = nullptr; + EGLint numConfigs = 0; + if (eglChooseConfig(out.display, configAttribs, &config, 1, &numConfigs) != EGL_TRUE || + numConfigs < 1) { + error = "eglChooseConfig found no pbuffer config"; + return false; + } + const EGLint pbufferAttribs[] = {EGL_WIDTH, 64, EGL_HEIGHT, 64, EGL_NONE}; + out.surface = eglCreatePbufferSurface(out.display, config, pbufferAttribs); + if (out.surface == EGL_NO_SURFACE) { + char buffer[56]; + snprintf(buffer, sizeof buffer, "eglCreatePbufferSurface failed (0x%04x)", eglGetError()); + error = buffer; + return false; + } + const EGLint contextAttribs[] = {EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE}; + out.context = eglCreateContext(out.display, config, EGL_NO_CONTEXT, contextAttribs); + if (out.context == EGL_NO_CONTEXT) { + char buffer[48]; + snprintf(buffer, sizeof buffer, "eglCreateContext failed (0x%04x)", eglGetError()); + error = buffer; + return false; + } + if (eglMakeCurrent(out.display, out.surface, out.surface, out.context) != EGL_TRUE) { + char buffer[48]; + snprintf(buffer, sizeof buffer, "eglMakeCurrent failed (0x%04x)", eglGetError()); + error = buffer; + return false; + } + return true; + } + + std::string RunBench(const std::string& backendType, int frames, int warmup) { + // Must land before the first EGL call: Initialize() reads the + // environment exactly once per process. + setenv("MOBILEGL_BACKEND_TYPE", backendType.c_str(), 1); + + EglBench egl; + std::string error; + if (!BootEgl(egl, error)) { + return std::string("{\"error\":\"") + EscapeJson(error) + "\"}"; + } + + const char* renderer = reinterpret_cast(glGetString(GL_RENDERER)); + const char* version = reinterpret_cast(glGetString(GL_VERSION)); + + g_benchFailure.clear(); + build_resources(); + if (!g_benchFailure.empty()) { + return std::string("{\"error\":\"") + EscapeJson(g_benchFailure) + "\"}"; + } + + if (frames < 8) frames = 8; + if (frames > 512) frames = 512; + if (warmup < 2) warmup = 2; + if (warmup > 128) warmup = 128; + g_frames = frames; + g_warmup = warmup; + + std::string rows; + static uint64_t samples[512]; + for (int c = 0; c < kBenchCaseCount; ++c) { + const BenchCaseDesc& benchCase = kBenchCases[c]; + EndFrameWait(); + for (int i = 0; i < g_warmup; ++i) { + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + benchCase.fn(i, benchCase.a, benchCase.b); + EndFrameWait(); + } + for (int i = 0; i < g_frames; ++i) { + const uint64_t t0 = NowNs(); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + benchCase.fn(i, benchCase.a, benchCase.b); + EndFrameWait(); + samples[i] = NowNs() - t0; + } + qsort(samples, static_cast(g_frames), sizeof(uint64_t), CompareU64); + const uint64_t median = samples[g_frames / 2]; + const double frameMs = static_cast(median) / 1e6; + const double nsPerOp = + benchCase.opsPerFrame > 0 ? static_cast(median) / static_cast(benchCase.opsPerFrame) : 0.0; + const GLenum caseError = glGetError(); + + char row[256]; + snprintf(row, sizeof row, + "%s{\"case\":\"%s\",\"frames\":%d,\"opsPerFrame\":%ld,\"medianFrameMs\":%.3f," + "\"nsPerOp\":%.1f,\"fps\":%.1f,\"glError\":%u}", + rows.empty() ? "" : ",", benchCase.name, g_frames, benchCase.opsPerFrame, frameMs, + nsPerOp, 1e9 / static_cast(median), caseError); + rows += row; + } + + std::string json = "{\"backend\":\"" + EscapeJson(backendType) + "\""; + json += ",\"renderer\":\"" + EscapeJson(renderer ? renderer : "unknown") + "\""; + json += ",\"version\":\"" + EscapeJson(version ? version : "unknown") + "\""; + json += ",\"frames\":" + std::to_string(g_frames); + json += ",\"cases\":[" + rows + "]}"; + + // Minimal teardown: unbind so the driver flushes, then let the + // process exit reclaim everything. eglTerminate stays un-called on + // purpose (Espryt would take the process-default display down). + eglMakeCurrent(egl.display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + return json; + } + +} // namespace + +extern "C" JNIEXPORT jstring JNICALL Java_top_mobilegl_plugin_BenchService_nativeRunDriverBench( + JNIEnv* env, jclass, jstring backendType, jint frames, jint warmupFrames) { + const char* backendChars = backendType ? env->GetStringUTFChars(backendType, nullptr) : nullptr; + const std::string backend = backendChars ? backendChars : "DirectGLES"; + if (backendChars) env->ReleaseStringUTFChars(backendType, backendChars); + + std::string result; + try { + result = RunBench(backend, frames, warmupFrames); + } catch (const std::exception& e) { + result = std::string("{\"error\":\"") + EscapeJson(e.what()) + "\"}"; + } catch (...) { + result = "{\"error\":\"unknown native exception\"}"; + } + return env->NewStringUTF(result.c_str()); +} + +#endif // __ANDROID__ diff --git a/MobileGL/MG_Util/SelfTest/DriverPost.cpp b/MobileGL/MG_Util/SelfTest/DriverPost.cpp index 1b373bb6..b6241196 100644 --- a/MobileGL/MG_Util/SelfTest/DriverPost.cpp +++ b/MobileGL/MG_Util/SelfTest/DriverPost.cpp @@ -1218,6 +1218,22 @@ namespace MobileGL::MG_Util::SelfTest { } } + // Windowless (EGL pbuffer) contexts want a headless surface. Almost no mobile + // ICD provides one - Mali r32p1 does not - so its absence is not fatal: the + // renderer hands the WSI an AImageReader window instead. Reported because the + // fallback costs a buffer queue the headless path does not need, and because + // this used to abort the process instead. + if (HasVkExtension(instanceExtensions, VK_EXT_HEADLESS_SURFACE_EXTENSION_NAME)) { + builder.Pass("Headless surface", + format("{} present; windowless contexts get a real headless surface", + VK_EXT_HEADLESS_SURFACE_EXTENSION_NAME)); + } else { + builder.Warn("Headless surface", + format("{} absent; a windowless (pbuffer) context falls back to an AImageReader " + "ANativeWindow, which needs libmediandk.so and an extra buffer queue", + VK_EXT_HEADLESS_SURFACE_EXTENSION_NAME)); + } + // The probe never creates a surface, so the instance is created without extensions. VkApplicationInfo appInfo{}; appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO; diff --git a/android-plugin/app/build.gradle.kts b/android-plugin/app/build.gradle.kts index dbd4a13e..e5305d9e 100644 --- a/android-plugin/app/build.gradle.kts +++ b/android-plugin/app/build.gradle.kts @@ -33,6 +33,13 @@ fun Project.mobileGlAbiFilters(): List { fun Project.mobileGlCmakeCompilerLauncher(): String = (findProperty("mobilegl.cmakeCompilerLauncher") ?: System.getenv("MOBILEGL_CMAKE_COMPILER_LAUNCHER") ?: "").toString().trim() +// Optional application-id suffix, so a development build can sit next to an +// already-installed plugin instead of having to replace it - a differently +// signed APK cannot upgrade one in place, and uninstalling costs the user their +// plugin settings and the launcher's binding to it. +fun Project.mobileGlApplicationIdSuffix(): String = + (findProperty("mobilegl.applicationIdSuffix") ?: "").toString().trim() + fun Project.runGit(vararg arguments: String): String? = runCatching { ProcessBuilder("git", *arguments) .directory(rootDir) @@ -102,6 +109,7 @@ android { defaultConfig { applicationId = "top.mobilegl.plugin" + mobileGlApplicationIdSuffix().takeIf { it.isNotEmpty() }?.let { applicationIdSuffix = it } minSdk = 26 targetSdk = 34 versionCode = mobileGlVersionMajor * 1_000_000 + mobileGlVersionMinor * 10_000 + mobileGlMonthlyRevision diff --git a/android-plugin/app/src/main/AndroidManifest.xml b/android-plugin/app/src/main/AndroidManifest.xml index 199d0479..36715b97 100644 --- a/android-plugin/app/src/main/AndroidManifest.xml +++ b/android-plugin/app/src/main/AndroidManifest.xml @@ -22,6 +22,15 @@ android:exported="true" android:theme="@style/Theme.MobileGLPlugin.NoDisplay" /> + + + diff --git a/android-plugin/app/src/main/java/top/mobilegl/plugin/BenchService.java b/android-plugin/app/src/main/java/top/mobilegl/plugin/BenchService.java new file mode 100644 index 00000000..d838aca1 --- /dev/null +++ b/android-plugin/app/src/main/java/top/mobilegl/plugin/BenchService.java @@ -0,0 +1,77 @@ +package top.mobilegl.plugin; + +import android.app.Service; +import android.content.Intent; +import android.os.IBinder; +import android.util.Log; + +/** + * Hosts one driver-benchmark run in its own process ({@code android:process=":bench"}) + * and exits when done. + * + * The isolation is load-bearing, not defensive: MobileGL latches its backend from + * {@code MOBILEGL_BACKEND_TYPE} on first 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 HWUI context down with it. A fresh process per + * tap sidesteps both, and {@link System#exit} at the end guarantees the next tap gets + * one. + */ +public final class BenchService extends Service { + private static final String TAG = "MobileGLBench"; + + public static final String ACTION_RESULT = "top.mobilegl.plugin.BENCH_RESULT"; + public static final String EXTRA_BACKEND = "backend"; + public static final String EXTRA_RESULT_JSON = "resultJson"; + public static final String EXTRA_FRAMES = "frames"; + public static final String EXTRA_WARMUP = "warmupFrames"; + + private static native String nativeRunDriverBench(String backendType, int frames, int warmupFrames); + + @Override + public IBinder onBind(Intent intent) { + return null; + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + final String backend = intent != null ? intent.getStringExtra(EXTRA_BACKEND) : null; + final int frames = intent != null ? intent.getIntExtra(EXTRA_FRAMES, 120) : 120; + final int warmup = intent != null ? intent.getIntExtra(EXTRA_WARMUP, 30) : 30; + if (backend == null) { + stopSelf(); + return START_NOT_STICKY; + } + + new Thread(() -> { + String result; + try { + System.loadLibrary("MobileGL"); + result = nativeRunDriverBench(backend, frames, warmup); + } catch (Throwable t) { + Log.e(TAG, "bench failed", t); + result = "{\"error\":\"" + t.getClass().getSimpleName() + ": " + + String.valueOf(t.getMessage()).replace("\\", "\\\\").replace("\"", "\\\"") + + "\"}"; + } + + Intent reply = new Intent(ACTION_RESULT); + reply.setPackage(getPackageName()); + reply.putExtra(EXTRA_BACKEND, backend); + reply.putExtra(EXTRA_RESULT_JSON, result); + sendBroadcast(reply); + Log.i(TAG, "bench done for " + backend + " (" + result.length() + " bytes)"); + + // The broadcast is already handed to system_server; give binder a + // moment, then take the whole process down so the next run starts + // from an uninitialized MobileGL. + try { + Thread.sleep(250); + } catch (InterruptedException ignored) { + } + stopSelf(); + System.exit(0); + }, "MobileGLDriverBench").start(); + + return START_NOT_STICKY; + } +} diff --git a/android-plugin/app/src/main/java/top/mobilegl/plugin/PostActivity.java b/android-plugin/app/src/main/java/top/mobilegl/plugin/PostActivity.java index 24e95e6d..0e69f4cb 100644 --- a/android-plugin/app/src/main/java/top/mobilegl/plugin/PostActivity.java +++ b/android-plugin/app/src/main/java/top/mobilegl/plugin/PostActivity.java @@ -1,17 +1,26 @@ package top.mobilegl.plugin; import android.app.Activity; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; import android.graphics.Typeface; +import android.os.Build; import android.os.Bundle; import android.util.Log; import android.util.TypedValue; import android.view.Gravity; import android.view.View; +import android.widget.Button; import android.widget.HorizontalScrollView; import android.widget.LinearLayout; import android.widget.ScrollView; import android.widget.TextView; +import java.util.HashMap; +import java.util.Map; + import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -61,6 +70,16 @@ public final class PostActivity extends Activity { private LinearLayout contentLayout; private TextView statusView; + /** + * Per-backend bench UI state. Unlike the POST itself (single-flight, latched), + * a bench may be re-run freely: every tap starts a fresh {@link BenchService} + * process, so the only state to manage here is the button and the results + * container the reply renders into. + */ + private final Map benchButtons = new HashMap<>(); + private final Map benchResultContainers = new HashMap<>(); + private BroadcastReceiver benchReceiver; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -108,9 +127,100 @@ public final class PostActivity extends Activity { deliveryTarget = new WeakReference<>(null); } } + if (benchReceiver != null) { + unregisterReceiver(benchReceiver); + benchReceiver = null; + } super.onDestroy(); } + /** Registered lazily on the first Run Bench tap; delivers results to the UI thread. */ + private void ensureBenchReceiver() { + if (benchReceiver != null) { + return; + } + benchReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String backend = intent.getStringExtra(BenchService.EXTRA_BACKEND); + String json = intent.getStringExtra(BenchService.EXTRA_RESULT_JSON); + if (backend != null && json != null) { + renderBenchResult(backend, json); + } + } + }; + IntentFilter filter = new IntentFilter(BenchService.ACTION_RESULT); + if (Build.VERSION.SDK_INT >= 33) { + registerReceiver(benchReceiver, filter, Context.RECEIVER_NOT_EXPORTED); + } else { + registerReceiver(benchReceiver, filter); + } + } + + /** Kicks one bench run for a backend in its own service process. */ + private void startBench(String backendType) { + ensureBenchReceiver(); + Button button = benchButtons.get(backendType); + if (button != null) { + button.setEnabled(false); + button.setText("Bench running... (can take a minute)"); + } + LinearLayout container = benchResultContainers.get(backendType); + if (container != null) { + container.removeAllViews(); + } + Intent intent = new Intent(this, BenchService.class); + intent.putExtra(BenchService.EXTRA_BACKEND, backendType); + intent.putExtra(BenchService.EXTRA_FRAMES, 120); + intent.putExtra(BenchService.EXTRA_WARMUP, 30); + startService(intent); + } + + private void renderBenchResult(String backendType, String json) { + Button button = benchButtons.get(backendType); + if (button != null) { + button.setEnabled(true); + button.setText("Run Bench"); + } + LinearLayout container = benchResultContainers.get(backendType); + if (container == null) { + return; + } + container.removeAllViews(); + try { + JSONObject root = new JSONObject(json); + String error = root.optString("error", ""); + if (!error.isEmpty()) { + container.addView(makeText("Bench failed: " + error, 12, COLOR_FAIL, false)); + return; + } + container.addView(makeText(root.optString("renderer", ""), 11, COLOR_INFO, false)); + String header = String.format(Locale.ROOT, "%-22s %10s %10s %9s", + "case", "ns/op", "frame ms", "fps"); + container.addView(makeText(header, 11, COLOR_DETAIL, true)); + JSONArray cases = root.optJSONArray("cases"); + if (cases == null) { + return; + } + for (int i = 0; i < cases.length(); ++i) { + JSONObject row = cases.optJSONObject(i); + if (row == null) { + continue; + } + String line = String.format(Locale.ROOT, "%-22s %10.1f %10.3f %9.1f", + row.optString("case", "?"), + row.optDouble("nsPerOp", 0), + row.optDouble("medianFrameMs", 0), + row.optDouble("fps", 0)); + TextView view = makeText(line, 11, + row.optInt("glError", 0) != 0 ? COLOR_WARN : COLOR_TEXT, false); + container.addView(view); + } + } catch (JSONException error) { + container.addView(makeText("Bench result unparsable: " + error, 12, COLOR_FAIL, false)); + } + } + /** * Loads libMobileGL.so on first use, off the UI thread, so the first frame * renders immediately. Any failure (including errors thrown by static @@ -253,6 +363,8 @@ public final class PostActivity extends Activity { addText(renderer, 12, COLOR_INFO, false, dp(2)); } + addBenchControls(name); + JSONArray checks = backend.optJSONArray("checks"); if (checks != null) { LinearLayout table = new LinearLayout(this); @@ -276,6 +388,54 @@ public final class PostActivity extends Activity { renderFormatCapabilities(backend.optJSONObject("formatCapabilities")); } + /** The MOBILEGL_BACKEND_TYPE value a POST section name stands for, or null. */ + private static String backendTypeForSection(String sectionName) { + switch (sectionName.toLowerCase(Locale.ROOT)) { + case "gles": + case "directgles": + return "DirectGLES"; + case "vulkan": + case "directvulkan": + return "DirectVulkan"; + default: + return null; + } + } + + /** + * One Run Bench button plus the container its results render into. The bench + * runs the Minecraft-shaped MG_Benchmark cases through the full MobileGL stack + * on this backend, in a throwaway service process (see BenchService). + */ + private void addBenchControls(String sectionName) { + final String backendType = backendTypeForSection(sectionName); + if (backendType == null) { + return; + } + Button button = new Button(this); + button.setText("Run Bench"); + button.setAllCaps(false); + button.setTextSize(TypedValue.COMPLEX_UNIT_SP, 13); + button.setOnClickListener(v -> startBench(backendType)); + LinearLayout.LayoutParams buttonParams = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, + LinearLayout.LayoutParams.WRAP_CONTENT + ); + buttonParams.topMargin = dp(6); + contentLayout.addView(button, buttonParams); + benchButtons.put(backendType, button); + + LinearLayout results = new LinearLayout(this); + results.setOrientation(LinearLayout.VERTICAL); + LinearLayout.LayoutParams resultParams = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT + ); + resultParams.topMargin = dp(4); + contentLayout.addView(results, resultParams); + benchResultContainers.put(backendType, results); + } + /** * Adds one two-column check row (name | status chip) to the table. Rows with * a non-empty detail get a collapse indicator and toggle the detail text on tap;