mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 04:08:32 +09:00
[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:
@@ -304,6 +304,7 @@ endif()
|
||||
if (ANDROID)
|
||||
list(APPEND SOURCE_FILES
|
||||
MobileGL/MG_Util/SelfTest/DriverPostJni.cpp
|
||||
MobileGL/MG_Util/SelfTest/DriverBenchJni.cpp
|
||||
)
|
||||
endif()
|
||||
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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]);
|
||||
@@ -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 <jni.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <ctime>
|
||||
#include <string>
|
||||
|
||||
// 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 <Includes.h>
|
||||
|
||||
#include <EGL/egl.h>
|
||||
#include <GLES3/gl32.h>
|
||||
|
||||
#include <MG_Impl/EGLImpl/EGLImpl.h>
|
||||
#include <MG_Impl/GLImpl/Buffer/GL_Buffer.h>
|
||||
#include <MG_Impl/GLImpl/Drawing/GL_Drawing.h>
|
||||
#include <MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h>
|
||||
#include <MG_Impl/GLImpl/Getter/GL_Getter.h>
|
||||
#include <MG_Impl/GLImpl/Program/GL_Program.h>
|
||||
#include <MG_Impl/GLImpl/RenderState/GL_RenderState.h>
|
||||
#include <MG_Impl/GLImpl/Sampler/GL_Sampler.h>
|
||||
#include <MG_Impl/GLImpl/Sync/GL_Sync.h>
|
||||
#include <MG_Impl/GLImpl/Texture/GL_Texture.h>
|
||||
#include <MG_Impl/GLImpl/VertexArray/GL_VertexArray.h>
|
||||
|
||||
// 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<uint64_t>(ts.tv_sec) * 1000000000ull + static_cast<uint64_t>(ts.tv_nsec);
|
||||
}
|
||||
|
||||
int CompareU64(const void* a, const void* b) {
|
||||
const uint64_t x = *static_cast<const uint64_t*>(a);
|
||||
const uint64_t y = *static_cast<const uint64_t*>(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<unsigned char>(c) < 0x20 || static_cast<unsigned char>(c) >= 0x7F) {
|
||||
char buffer[8];
|
||||
snprintf(buffer, sizeof buffer, "\\u%04x", static_cast<unsigned char>(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<const char*>(glGetString(GL_RENDERER));
|
||||
const char* version = reinterpret_cast<const char*>(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<size_t>(g_frames), sizeof(uint64_t), CompareU64);
|
||||
const uint64_t median = samples[g_frames / 2];
|
||||
const double frameMs = static_cast<double>(median) / 1e6;
|
||||
const double nsPerOp =
|
||||
benchCase.opsPerFrame > 0 ? static_cast<double>(median) / static_cast<double>(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<double>(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__
|
||||
@@ -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;
|
||||
|
||||
@@ -33,6 +33,13 @@ fun Project.mobileGlAbiFilters(): List<String> {
|
||||
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
|
||||
|
||||
@@ -22,6 +22,15 @@
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.MobileGLPlugin.NoDisplay" />
|
||||
|
||||
<!-- One benchmark run per process: the backend is latched at MobileGL
|
||||
init and Espryt teardown kills the process-default EGL display, so
|
||||
the bench must never share a process with the POST UI. The service
|
||||
exits after each run. -->
|
||||
<service
|
||||
android:name=".BenchService"
|
||||
android:exported="false"
|
||||
android:process=":bench" />
|
||||
|
||||
<meta-data
|
||||
android:name="fclPlugin_V2"
|
||||
android:resource="@string/config" />
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<String, Button> benchButtons = new HashMap<>();
|
||||
private final Map<String, LinearLayout> 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;
|
||||
|
||||
Reference in New Issue
Block a user