[Test] (MG_Benchmark, MG_Util): model four more Minecraft frame patterns in the driver bench

The captured traces contain per-frame patterns the bench did not exercise, and
first measurements show two of them are now the worst remaining multipliers -
which is exactly what the missing cases were hiding.

mc_pass_switch: the 26.2 snapshot switches render targets 132 times a frame and
re-declares draw buffers 198 times. Render-target churn is where a Vulkan
backend pays for render-pass breaks and where a tiler pays most on device, and
no case measured it. mc_state_toggle: Blaze3D brackets batches with blend
toggles - 46 enable/disable pairs and 28 blend-func changes per vanilla frame.
mc_tex_param: 26.2 re-sets texture parameters 612 times a frame, almost always
to the value already in place, so this measures redundant-parameter filtering.
mc_use_program: Sodium switches programs 62 times a frame with a mat4 upload on
each, roughly one switch per multi-draw.

All four live in the shared case file at the measured per-frame rates, so the
desktop harness, the on-device harness and the POST screen's Run Bench report
comparable numbers. First desktop measurements (ns/op, native / Espryt / Magma):
pass_switch 8877 / 18502 / 13896, state_toggle 1182 / 8526 / 8305,
tex_param 42 / 102 / 197, use_program 2182 / 10648 / 5096. The state-toggle
multiplier - 7x on both backends - is the largest newly exposed gap and the next
optimization target.

Unit tests 421/421; the Android JNI translation unit compiles against the
extended case set.
This commit is contained in:
BZLZHH
2026-08-06 09:39:02 -04:00
parent f2d210b12d
commit d524330032
3 changed files with 158 additions and 16 deletions
+63 -16
View File
@@ -12,8 +12,10 @@
* libEGL.so.1 for the native driver, or a libMobileGL.so path for either
* MobileGL backend selected with MOBILEGL_BACKEND_TYPE), creates a desktop-GL
* context on a small pbuffer, renders into its own FBO and paces frames with
* glFinish. No window system is required beyond what the provider itself
* needs - see run_driver_bench.sh.
* glFinish. No window system is required: the default display is tried first
* so a desktop run reaches the real driver, and a headless box (CI, a build
* server) falls back to EGL_MESA_platform_surfaceless - see
* run_driver_bench.sh.
*
* Every case models one hot pattern from captured Minecraft traces:
* draw_tiny back-to-back glDrawElements, shared state (chunk batch)
@@ -67,6 +69,7 @@ typedef unsigned int EGLenum;
#define EGL_CONTEXT_MINOR_VERSION 0x30FB
#define EGL_CONTEXT_OPENGL_PROFILE_MASK 0x30FD
#define EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT 0x00000001
#define EGL_PLATFORM_SURFACELESS_MESA 0x31DD
/* ---- GL constants ---- */
#define GL_COLOR_BUFFER_BIT 0x00004000
@@ -89,6 +92,11 @@ typedef unsigned int EGLenum;
#define GL_NEAREST 0x2600
#define GL_NEAREST_MIPMAP_LINEAR 0x2702
#define GL_DEPTH_TEST 0x0B71
#define GL_BLEND 0x0BE2
#define GL_SRC_ALPHA 0x0302
#define GL_ONE_MINUS_SRC_ALPHA 0x0303
#define GL_ONE 1
#define GL_ZERO 0
#define GL_VERTEX_SHADER 0x8B31
#define GL_FRAGMENT_SHADER 0x8B30
#define GL_COMPILE_STATUS 0x8B81
@@ -133,6 +141,9 @@ static void* g_provider;
GLF(void, glClear, (unsigned))
GLF(void, glClearColor, (float, float, float, float))
GLF(void, glEnable, (GLenum))
GLF(void, glDisable, (GLenum))
GLF(void, glBlendFuncSeparate, (GLenum, GLenum, GLenum, GLenum))
GLF(void, glDrawBuffers, (GLsizei, const GLenum*))
GLF(void, glViewport, (GLint, GLint, GLsizei, GLsizei))
GLF(const unsigned char*, glGetString, (GLenum))
GLF(GLenum, glGetError, (void))
@@ -280,7 +291,21 @@ static void run_case(const char* name, case_fn body, long a, long b, long opsPer
if (glGetError() != GL_NO_ERROR) fprintf(stderr, "WARN: GL error after %s\n", name);
}
/* a = draws per frame */
/* A display that needs no window system. eglGetPlatformDisplay is EGL 1.5
* core and eglGetPlatformDisplayEXT is the EGL_EXT_platform_base spelling
* older loaders ship; both are client entry points, so they resolve before
* any display exists. Only the attribute-list types differ between the two
* and this passes none, so one cast covers both. */
static EGLDisplay surfaceless_display(void) {
void* fn = dlsym(g_provider, "eglGetPlatformDisplay");
if (!fn) fn = g_eglGetProcAddress("eglGetPlatformDisplay");
if (!fn) fn = dlsym(g_provider, "eglGetPlatformDisplayEXT");
if (!fn) fn = g_eglGetProcAddress("eglGetPlatformDisplayEXT");
if (!fn) return NULL;
return ((EGLDisplay(*)(EGLenum, void*, const void*))fn)(EGL_PLATFORM_SURFACELESS_MESA,
EGL_DEFAULT_DISPLAY, NULL);
}
/* ---- EGL bootstrap: one provider library, pbuffer, desktop-GL context ---- */
static int boot_egl(void) {
const char* libpath = getenv("DRIVERBENCH_EGL_LIB");
@@ -304,14 +329,30 @@ static int boot_egl(void) {
ESYM(eglGetError)
g_eglGetProcAddress = (void* (*)(const char*))p_eglGetProcAddress;
EGLDisplay dpy = ((EGLDisplay(*)(void*))p_eglGetDisplay)(EGL_DEFAULT_DISPLAY);
if (!dpy) { fprintf(stderr, "FAIL: eglGetDisplay\n"); return 1; }
EGLint (*getError)(void) = (EGLint(*)(void))p_eglGetError;
EGLBoolean (*initialize)(EGLDisplay, EGLint*, EGLint*) =
(EGLBoolean(*)(EGLDisplay, EGLint*, EGLint*))p_eglInitialize;
/* The default display first: it is the one a windowed app would get, and
* on a desktop it is the one that reaches the real GPU - which is the
* driver this bench exists to measure. It does need a window system,
* though; Mesa's default platform is X11, so with no $DISPLAY (CI, a
* build server, ssh without forwarding) eglInitialize fails. Fall back to
* EGL_MESA_platform_surfaceless rather than give up: every case draws into
* the FBO built by build_resources(), so no window is needed for any of
* the work being timed. */
EGLint maj = 0, min = 0;
if (!((EGLBoolean(*)(EGLDisplay, EGLint*, EGLint*))p_eglInitialize)(dpy, &maj, &min)) {
fprintf(stderr, "FAIL: eglInitialize (0x%x)\n", ((EGLint(*)(void))p_eglGetError)());
return 1;
const char* how = "default display";
EGLDisplay dpy = ((EGLDisplay(*)(void*))p_eglGetDisplay)(EGL_DEFAULT_DISPLAY);
if (!dpy || !initialize(dpy, &maj, &min)) {
dpy = surfaceless_display();
how = "surfaceless display";
if (!dpy || !initialize(dpy, &maj, &min)) {
fprintf(stderr, "FAIL: eglInitialize (0x%x)\n", getError());
return 1;
}
}
fprintf(stderr, "EGL %d.%d via %s\n", maj, min, libpath);
fprintf(stderr, "EGL %d.%d via %s (%s)\n", maj, min, libpath, how);
// 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
@@ -348,7 +389,10 @@ static int boot_egl(void) {
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};
// EGL_SURFACE_TYPE 0 matches any config: a stack that offers no
// pbuffer at all is still usable through the surfaceless context
// path below.
const EGLint relaxed[] = {EGL_SURFACE_TYPE, 0, EGL_RED_SIZE, 8, EGL_NONE};
if (!chooseConfig(dpy, relaxed, &cfg, 1, &ncfg) || ncfg < 1) {
fprintf(stderr, "FAIL: eglChooseConfig\n");
return 1;
@@ -358,20 +402,22 @@ static int boot_egl(void) {
ctx = createContext(dpy, cfg, EGL_NO_CONTEXT, esCtxAttribs);
}
if (ctx == EGL_NO_CONTEXT) {
fprintf(stderr, "FAIL: eglCreateContext (0x%x)\n", ((EGLint(*)(void))p_eglGetError)());
fprintf(stderr, "FAIL: eglCreateContext (0x%x)\n", getError());
return 1;
}
/* The pbuffer only exists to have something to make current - nothing is
* ever drawn to it. Where there is no pbuffer config, EGL_NO_SURFACE is
* exactly what EGL_KHR_surfaceless_context takes, so the same call covers
* both. */
const EGLint pbAttribs[] = {EGL_WIDTH, 64, EGL_HEIGHT, 64, EGL_NONE};
EGLSurface surf = ((EGLSurface(*)(EGLDisplay, EGLConfig, const EGLint*))p_eglCreatePbufferSurface)(
dpy, cfg, pbAttribs);
if (surf == EGL_NO_SURFACE) {
fprintf(stderr, "FAIL: eglCreatePbufferSurface (0x%x)\n", ((EGLint(*)(void))p_eglGetError)());
return 1;
}
if (surf == EGL_NO_SURFACE)
fprintf(stderr, "no pbuffer (0x%x), using a surfaceless context\n", getError());
if (!((EGLBoolean(*)(EGLDisplay, EGLSurface, EGLSurface, EGLContext))p_eglMakeCurrent)(dpy, surf,
surf, ctx)) {
fprintf(stderr, "FAIL: eglMakeCurrent (0x%x)\n", ((EGLint(*)(void))p_eglGetError)());
fprintf(stderr, "FAIL: eglMakeCurrent (0x%x)\n", getError());
return 1;
}
@@ -385,6 +431,7 @@ static int boot_egl(void) {
if (!name) { fprintf(stderr, "FAIL: resolve %s\n", #name); return 1; } \
} while (0)
RESOLVE(glClear); RESOLVE(glClearColor); RESOLVE(glEnable); RESOLVE(glViewport);
RESOLVE(glDisable); RESOLVE(glBlendFuncSeparate); RESOLVE(glDrawBuffers);
RESOLVE(glGetString); RESOLVE(glGetError); RESOLVE(glFinish); RESOLVE(glFlush);
RESOLVE(glGenBuffers); RESOLVE(glBindBuffer); RESOLVE(glBufferData); RESOLVE(glBufferSubData);
RESOLVE(glGenVertexArrays); RESOLVE(glBindVertexArray); RESOLVE(glEnableVertexAttribArray);
@@ -34,6 +34,9 @@ static GLuint g_uboRing;
static GLint g_uboAlign = 256;
static size_t g_uboSlot = 256;
static GLuint g_sampler;
/* Two small offscreen targets for the 26.2-style render-pass churn case. */
static GLuint g_passFbo[2];
static GLuint g_passColor[2];
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 */
@@ -162,10 +165,13 @@ static void setup_vao(GLuint vao, GLuint vbo, GLuint ibo) {
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo);
}
static GLuint g_mainFbo;
static void build_resources(void) {
/* offscreen render target: 1280x720 RBO FBO, like CTS fbo surface mode */
GLuint fbo, rboColor, rboDepth;
glGenFramebuffers(1, &fbo);
g_mainFbo = fbo;
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glGenRenderbuffers(1, &rboColor);
glBindRenderbuffer(GL_RENDERBUFFER, rboColor);
@@ -257,6 +263,21 @@ static void build_resources(void) {
glBufferData(GL_UNIFORM_BUFFER, 4 * 1024 * 1024, g_scratch, GL_DYNAMIC_DRAW);
glBindBuffer(GL_UNIFORM_BUFFER, 0);
for (int i = 0; i < 2; ++i) {
glGenFramebuffers(1, &g_passFbo[i]);
glBindFramebuffer(GL_FRAMEBUFFER, g_passFbo[i]);
glGenRenderbuffers(1, &g_passColor[i]);
glBindRenderbuffer(GL_RENDERBUFFER, g_passColor[i]);
glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, 256, 256);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, g_passColor[i]);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
bench_gl_failed("pass FBO incomplete", "");
return;
}
}
/* back to the main offscreen target the harness set up */
glBindFramebuffer(GL_FRAMEBUFFER, g_mainFbo);
glGenSamplers(1, &g_sampler);
glSamplerParameteri(g_sampler, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glSamplerParameteri(g_sampler, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
@@ -516,6 +537,72 @@ static void case_mc_sampler_churn(int frame, long a, long b) {
}
/* 26.2 switches render targets constantly: 132 glBindFramebuffer and 198
* glDrawBuffers per frame. Pass switching is where a Vulkan backend pays for
* render-pass breaks, so this case is the one to watch on Magma. a = passes. */
static void case_mc_pass_switch(int frame, long a, long b) {
(void)frame; (void)b;
static const GLenum kColor0[1] = {GL_COLOR_ATTACHMENT0};
glBindVertexArray(g_vao[0]);
for (long i = 0; i < a; ++i) {
glBindFramebuffer(GL_FRAMEBUFFER, g_passFbo[i & 1]);
glDrawBuffers(1, kColor0);
glViewport(0, 0, 256, 256);
glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0);
glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0);
}
glBindFramebuffer(GL_FRAMEBUFFER, g_mainFbo);
glViewport(0, 0, 1280, 720);
}
/* Blaze3D toggles blend around batches: 46 glEnable/glDisable pairs and 28
* glBlendFuncSeparate per vanilla frame. a = toggle pairs. */
static void case_mc_state_toggle(int frame, long a, long b) {
(void)frame; (void)b;
glBindVertexArray(g_vao[0]);
for (long i = 0; i < a; ++i) {
glEnable(GL_BLEND);
glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ZERO);
glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0);
glDisable(GL_BLEND);
glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0);
}
}
/* 26.2 re-sets texture parameters relentlessly - 612 glTexParameteri per frame,
* almost always to the value already in place. Measures redundant-param
* filtering. a = parameter writes. */
static void case_mc_tex_param(int frame, long a, long b) {
(void)frame; (void)b;
glBindVertexArray(g_vao[0]);
glBindTexture(GL_TEXTURE_2D, g_texAtlas);
for (long i = 0; i < a; i += 4) {
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
}
glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0);
}
/* Sodium switches programs mid-frame far more than vanilla: 62 glUseProgram and
* 60 mat4 uploads per frame. a = program switches. */
static void case_mc_use_program(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);
glUniformMatrix4fv(g_uMvpChunk, 1, 0, g_mvp);
}
glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0);
}
glUseProgram(g_progChunk);
}
/* ---- 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
@@ -536,6 +623,10 @@ static const BenchCaseDesc kBenchCases[] = {
{"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},
{"mc_pass_switch", case_mc_pass_switch, 132, 0, 132},
{"mc_state_toggle", case_mc_state_toggle, 46, 0, 46},
{"mc_tex_param", case_mc_tex_param, 612, 0, 612},
{"mc_use_program", case_mc_use_program, 62, 0, 62},
{"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},
@@ -41,6 +41,7 @@
#include <MG_Impl/GLImpl/Getter/GL_Getter.h>
#include <MG_Impl/GLImpl/Program/GL_Program.h>
#include <MG_Impl/GLImpl/RenderState/GL_RenderState.h>
// Disable/BlendFuncSeparate live in GL_RenderState.h; DrawBuffers in GL_Framebuffer.h - both already included.
#include <MG_Impl/GLImpl/Sampler/GL_Sampler.h>
#include <MG_Impl/GLImpl/Sync/GL_Sync.h>
#include <MG_Impl/GLImpl/Texture/GL_Texture.h>
@@ -75,6 +76,9 @@
#define glDrawElements MobileGL::MG_Impl::GLImpl::DrawElements
#define glDrawElementsBaseVertex MobileGL::MG_Impl::GLImpl::DrawElementsBaseVertex
#define glEnable MobileGL::MG_Impl::GLImpl::Enable
#define glDisable MobileGL::MG_Impl::GLImpl::Disable
#define glBlendFuncSeparate MobileGL::MG_Impl::GLImpl::BlendFuncSeparate
#define glDrawBuffers MobileGL::MG_Impl::GLImpl::DrawBuffers
#define glEnableVertexAttribArray MobileGL::MG_Impl::GLImpl::EnableVertexAttribArray
#define glFenceSync MobileGL::MG_Impl::GLImpl::FenceSync
#define glFramebufferRenderbuffer MobileGL::MG_Impl::GLImpl::FramebufferRenderbuffer