From d524330032902d282fb82830c0887f32e3f678d7 Mon Sep 17 00:00:00 2001 From: BZLZHH Date: Thu, 6 Aug 2026 09:39:02 -0400 Subject: [PATCH] [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. --- MobileGL/MG_Benchmark/Driver/DriverBench.c | 79 ++++++++++++---- .../MG_Benchmark/Driver/DriverBenchCases.inc | 91 +++++++++++++++++++ MobileGL/MG_Util/SelfTest/DriverBenchJni.cpp | 4 + 3 files changed, 158 insertions(+), 16 deletions(-) diff --git a/MobileGL/MG_Benchmark/Driver/DriverBench.c b/MobileGL/MG_Benchmark/Driver/DriverBench.c index 2c43d84d..6f966794 100644 --- a/MobileGL/MG_Benchmark/Driver/DriverBench.c +++ b/MobileGL/MG_Benchmark/Driver/DriverBench.c @@ -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); diff --git a/MobileGL/MG_Benchmark/Driver/DriverBenchCases.inc b/MobileGL/MG_Benchmark/Driver/DriverBenchCases.inc index 7b8aaa2c..17e16c8c 100644 --- a/MobileGL/MG_Benchmark/Driver/DriverBenchCases.inc +++ b/MobileGL/MG_Benchmark/Driver/DriverBenchCases.inc @@ -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}, diff --git a/MobileGL/MG_Util/SelfTest/DriverBenchJni.cpp b/MobileGL/MG_Util/SelfTest/DriverBenchJni.cpp index a27529a3..1637548c 100644 --- a/MobileGL/MG_Util/SelfTest/DriverBenchJni.cpp +++ b/MobileGL/MG_Util/SelfTest/DriverBenchJni.cpp @@ -41,6 +41,7 @@ #include #include #include +// Disable/BlendFuncSeparate live in GL_RenderState.h; DrawBuffers in GL_Framebuffer.h - both already included. #include #include #include @@ -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