Compare commits

...
6 Commits
Author SHA1 Message Date
BZLZHH 62dea3bea4 [Perf] (MG_State): answer texture sampling completeness from a memo
Every draw asks, for every bound texture, whether it is mipmap-complete for the
filter in use, and the answer was recomputed from scratch each time: walk the
level chain, read each level's texel size, verify each is half the previous.
With the Minecraft-shaped bench that walk plus the GetTexelSize calls under it
measured about 8% of the render thread on both backends.

The answer depends only on the texture's shape - internal format, stored level
set, level sizes, level range - and never on its texel content, which is the
thing that actually changes between draws. A shape version now moves on exactly
those four mutations (SetInternalFormat, SetBaseLevel/SetMaxLevel, and the
AllocateStorage/TruncateMipmapLevels pair on both mipmap storage classes), and
the completeness answer is memoised against it, one slot for the mipmapped
question and one for the plain one. An upload leaves the memo standing, which is
the whole point; anything that could change the answer invalidates it.

ns per draw, DriverBench on a GTX 1660 SUPER (native / Espryt / Magma):
mc_vanilla_draw 257 / 2201->2037 / 1550->1346, mc_ubo_range 203 / 1832->1684 /
1089->934, mc_sampler_churn 272 / 2349->2325 / 1533->1396. Texture-upload cases
are unchanged, as expected - they were never asking this question in a loop.

Unit tests 421/421.
2026-08-06 06:42:43 -04:00
BZLZHH 57aeeec053 [Perf] (MG_State, MG_Impl, MG_Backend): stop paying per draw and per upload for work already known
A per-draw CPU profile of a real Minecraft frame (perf on the render thread,
which sits at 100% of one core on both backends) said the deficit is translation
overhead, not the GPU, and named where it goes. This removes the largest items
it found, on both backends and in the shared frontend they both feed.

The single biggest one was not translation at all: IsBackendContextCurrentOnThisThread
called eglGetCurrentContext on every invocation, and glvnd answers that with a
getpid() fork check - a real syscall. The predicate sits two and three deep in
every draw (the deferred-release drain, the global-UBO ring availability check,
and the ring allocation), so it accounted for 16.3% of the render thread. EGL is
still the ground truth, but re-verifying it once per thread per frame catches an
external migration at the next frame boundary rather than the next call, which
recovers the same bookkeeping.

Texture uploads now carry a dirty region instead of a per-level flag. Minecraft
animates atlas sprites with 16x16 glTexSubImage2D calls into a 1024x512 atlas
and respecifies the lightmap every frame; a per-level flag turned each of those
into a full-level re-upload - about 3.6 MB a frame of texels nobody changed.
MipmapStorage accumulates the written box, Espryt uploads it with
UNPACK_ROW_LENGTH striding into the level shadow, and Magma stages just that box.
The box is a union, not a range list: repeated writes to one level widen it and
it degrades to exactly the old whole-level upload, which is the honest worst case.

glBufferData(NULL) is the orphaning idiom, and the backend was answering it by
uploading the stale CPU shadow - turning a rename the driver does for free into
a full synchronized upload. BufferObject now records that a NULL respecify leaves
the store undefined, and the upload is skipped until content is actually written.

The rest are smaller and of a kind: the deferred-release queue is probed without
taking its mutex, the UBO ring waits on the frame fence that frees the space it
needs instead of draining the whole pipeline with glFinish at the size cap, VAO
binds go through a shadow so a draw's second bind of the same object does not
reach the driver, the per-draw clean-texture probe short-circuits on the content
version before rebuilding shape info, glUniform drops byte-identical writes
(which otherwise dirty the whole UBO for the next draw), re-binding the texture
or VAO a slot already holds no longer bumps the generation counters a backend
fast path is keyed on, and the texture validators stopped taking shared_ptr by
value.

On Magma: descriptor-set reuse keeps four entries instead of one, because draws
alternating between two programs - the chunk/entity ping-pong - thrashed a single
slot into a full re-allocate and re-write every draw; a DynamicDraw buffer whose
contents survive two frame boundaries is promoted to resident storage instead of
being re-copied into the per-frame arena forever; and sampled-read barriers name
only the shader stages whose device feature is enabled, which also removes a
latent VUID violation (ALL_GRAPHICS names geometry and tessellation stages a
device need not have).

Measured with the Minecraft rig (render distance 32, p50 fps, same machine,
single sample each): vanilla 1.21.1 Espryt 10.8 -> 36.3 and Magma 31.3 -> 44.6;
26.2 snapshot Magma 114.5 -> 210.5. Fabric+Sodium moved inside noise on Magma
(854 -> 766) with the native baseline itself moving 838 -> 1031 between the two
sessions, so treat that cell as unresolved rather than a regression measured.
Unit tests 421/421. The CTS A/B was not run: these numbers and the test suite are
the whole of the evidence, and a conformance regression would not have been
caught here.
2026-08-06 06:24:37 -04:00
BZLZHH 9c0144d24a [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.
2026-08-06 06:13:34 -04:00
BZLZHH 1e45958e01 [Test] (MG_Benchmark): measure the driver work a real Minecraft frame asks for
The benchmark tree had nothing that exercised a driver: SanityBench times
std::vector, and the Buffer/Program benches call into MobileGL_s directly, so
neither can say what a backend costs against the native driver. This adds a
headless EGL client that can, and shapes its cases from measured traces rather
than guesses.

DriverBench dlopens exactly one EGL provider - the system libEGL.so.1, or a
libMobileGL.so with MOBILEGL_BACKEND_TYPE selecting Espryt or Magma - so the
same binary measures all three stacks with no LD_LIBRARY_PATH shadowing, which
matters because MobileGL's own loader has to keep finding the real driver
underneath. It renders into its own renderbuffer FBO on a 64x64 pbuffer and
paces frames with glFinish, so it needs no window and no compositor.

The six mc_* cases replay the per-frame call mix of 30-second render-distance-32
captures of three Minecraft versions, at the rates those captures measured:
vanilla 1.21.1 issues 5495 glDrawElements per frame, each preceded by its own
glBindVertexArray and glUniform3fv; Fabric+Sodium collapses the same scene into
132 glMultiDrawElementsBaseVertex; the 26.2 snapshot issues 3401
glDrawElementsBaseVertex, each preceded by glBindBufferRange + glBindBuffer.
The texture case wraps every 16x16 atlas upload in the four glPixelStorei and
two glTexParameteri calls Blaze3D re-sets around it, because that wrapper is a
large part of what an upload costs a translation layer. One bench frame
therefore costs what one real frame of that version costs, and ns_per_op is
directly comparable across renderers.

run_driver_bench.sh pins __EGL_VENDOR_LIBRARY_FILENAMES and VK_ICD_FILENAMES.
Without that, eglGetDisplay(EGL_DEFAULT_DISPLAY) on this glvnd system resolves
to Mesa llvmpipe and the "native" numbers silently describe a software
rasteriser - the first run of this bench reported 11 us per draw before the
pin, versus 250 ns on the real GPU.

Verified against the NVIDIA 610.43.03 driver, Espryt and Magma on a GTX 1660
SUPER; the CMake target builds and runs from a clean configure.
2026-08-06 03:57:52 -04:00
BZLZHH 6e6f5268fb [Fix] (MG_Backend): let a default-visual X11 window match an alpha-free config
ChooseConfigForSurface prefilters candidate configs with eglChooseConfig
requiring EGL_ALPHA_SIZE 8, then tries to match the window's X visual. On
NVIDIA's X11 EGL every alpha-8 config lives on the 32-bit ARGB visual, and the
default depth-24 TrueColor visual only appears on alpha-0 configs - so for any
window created with the default visual the match loop scanned a list that
could not contain its visual, fell through to a 32-bit-visual config, and
eglCreateWindowSurface failed with EGL_BAD_CONFIG.

Keep the alpha-8 list as the first tier and add an alpha-relaxed second tier
used only for the visual match; the sizeless fallbacks below still run on the
alpha-8 list. Mesa is unaffected (its default-visual configs carry alpha), and
a destination-alpha-free default framebuffer is exactly what native GLX hands
out on these visuals anyway.

Found by running Minecraft through the new GLXImpl on Espryt: NVIDIA EGL also
needs EGL_PLATFORM=x11 under a Wayland session or eglGetDisplay itself returns
no display, which is a launcher-environment concern, not a library one.
2026-08-05 23:12:12 -04:00
BZLZHH 08f98ad9ce [Feat] (MG_Impl): implement GLX 1.4 on the EGL layer so GLFW apps run on Linux
Desktop Linux GL apps (GLFW/LWJGL, glxgears, anything X11) create contexts
through GLX, and MobileGL only spoke EGL - the two exported glX symbols were
proc-address stubs that could resolve GL entry points but never produce a
context. GLXImpl is the missing sibling of WGLImpl/CGLImpl: the same
window-system-binding pattern, calling the internal MG_Impl::EGLImpl namespace
directly.

The surface covers exactly what GLFW 3.4 resolves via dlsym plus the legacy
visual API: FBConfig enumeration mirrors the two EGLState configs (stencil-8
first so stencil-wanting choosers land on it), glXGetVisualFromFBConfig answers
with the screen's default visual (falling back to any 24-bit TrueColor one),
and glXCreateContextAttribsARB maps the ARB attribs onto EGL context attribs
the way WGL's Ext_CreateContextAttribsARB does - profile mask only emitted for
3.2+ or an explicit profile request, since that bit is what keys MobileGL's
relaxed-semantics compatibility mode. Legacy glXCreateContext/CreateNewContext
hand out 3.3 compatibility contexts, matching wglCreateContext.

Drawables follow the WGL HWND model: the GLXWindow is the X window itself, the
EGL window surface is created lazily on first MakeCurrent and cached per XID,
and the GLX layer owns size discovery per the platform-layer contract - it
pushes changes through EGLImpl::ResizePlatformWindowSurface, polling
XGetGeometry on MakeCurrent and on swaps throttled to 250ms so a fast-swapping
app is not paying a server round trip per frame. libX11 is dlopen'd at runtime
like everywhere else in the tree; Xlib.h is already in every TU via the vulkan
include, so XVisualInfo gets an ABI mirror struct (Xutil.h needs the Bool and
Status macros that Includes.h deliberately pops) and the caller's XFree pairs
with our malloc.

glXGetProcAddress now resolves glX names from the export table before falling
through to the shared GL resolver, which previously returned nullptr for every
glX extension entry point - GLFW requires glXCreateContextAttribsARB and
glXSwapIntervalEXT to arrive that way.

Verified with a smoke test replaying GLFW's exact call sequence (dlsym-only
resolution, manual FBConfig filtering, 3.2 core forward-compatible context,
glXCreateWindow, 60 swapped frames, clean glGetError) on both backends against
the real NVIDIA driver, then with Minecraft 1.21.1, 1.21.4+Fabric+Sodium and
26.2-snapshot-6 reaching in-world rendering on both Espryt and Magma.
2026-08-05 23:08:29 -04:00
45 changed files with 3891 additions and 102 deletions
+2
View File
@@ -206,6 +206,7 @@ set(SOURCE_FILES
MobileGL/MG_Util/Texture/TextureFormatProcessor.cpp
MobileGL/MG_Impl/GLXImpl/Exporting/Definitions.cpp
MobileGL/MG_Impl/GLXImpl/GLXImpl.cpp
MobileGL/MG_Impl/GLXImpl/LookUp/LookUp.cpp
MobileGL/MG_Impl/EGLImpl/Exporting/Definitions.cpp
@@ -303,6 +304,7 @@ endif()
if (ANDROID)
list(APPEND SOURCE_FILES
MobileGL/MG_Util/SelfTest/DriverPostJni.cpp
MobileGL/MG_Util/SelfTest/DriverBenchJni.cpp
)
endif()
+83 -11
View File
@@ -1518,7 +1518,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
backendVAOIt->second->Bind();
}
} else {
g_GLESFuncs.glBindVertexArray(0);
VertexArrayImpl::BindBackendVAOId(0);
}
}
@@ -3001,7 +3001,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
const Float uvTransform[4] = {mirrorX ? -uvScaleX : uvScaleX, mirrorY ? -uvScaleY : uvScaleY,
mirrorX ? uvScaleX : 0.0f, mirrorY ? uvScaleY : 0.0f};
g_GLESFuncs.glBindVertexArray(s_vertexArray);
VertexArrayImpl::BindBackendVAOId(s_vertexArray);
g_GLESFuncs.glActiveTexture(GL_TEXTURE0);
g_GLESFuncs.glBindTexture(GL_TEXTURE_2D, s_texture);
g_GLESFuncs.glViewport(dstLeft, dstBottom, dstWidth, dstHeight);
@@ -3060,7 +3060,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
g_GLESFuncs.glUseProgram(static_cast<GLuint>(previousProgram));
g_GLESFuncs.glBindTexture(GL_TEXTURE_2D, static_cast<GLuint>(previousTexture));
g_GLESFuncs.glActiveTexture(static_cast<GLenum>(previousActiveTexture));
g_GLESFuncs.glBindVertexArray(static_cast<GLuint>(previousVertexArray));
VertexArrayImpl::BindBackendVAOId(static_cast<GLuint>(previousVertexArray));
g_GLESFuncs.glViewport(previousViewport[0], previousViewport[1], previousViewport[2], previousViewport[3]);
g_GLESFuncs.glScissor(previousScissorBox[0], previousScissorBox[1], previousScissorBox[2],
previousScissorBox[3]);
@@ -5678,19 +5678,44 @@ namespace MobileGL::MG_Backend::DirectGLES {
configs.resize(static_cast<SizeT>(numConfigs));
if (surfaceBit == EGL_WINDOW_BIT) {
// X11 drivers can reserve every alpha-8 config for 32-bit ARGB
// visuals (NVIDIA), so a default-visual (depth 24) window only
// matches an alpha-0 config; keep those as a second candidate tier
// for the visual match or eglCreateWindowSurface hits BAD_CONFIG.
Vector<EGLConfig> alphaFreeConfigs;
const EGLint alphaFreeAttribs[] = {EGL_SURFACE_TYPE, surfaceBit, EGL_RENDERABLE_TYPE,
EGL_OPENGL_ES3_BIT,
EGL_RED_SIZE, 8, EGL_GREEN_SIZE,
8,
EGL_BLUE_SIZE, 8, EGL_DEPTH_SIZE,
24,
EGL_STENCIL_SIZE, 8, EGL_NONE};
EGLint numAlphaFree = 0;
if (g_EGLFuncs.eglChooseConfig(g_Display, alphaFreeAttribs, nullptr, 0, &numAlphaFree) &&
numAlphaFree > 0) {
alphaFreeConfigs.resize(static_cast<SizeT>(numAlphaFree));
if (!g_EGLFuncs.eglChooseConfig(g_Display, alphaFreeAttribs, alphaFreeConfigs.data(),
numAlphaFree, &numAlphaFree)) {
numAlphaFree = 0;
}
alphaFreeConfigs.resize(static_cast<SizeT>(numAlphaFree));
}
const EGLint windowVisualId = QueryX11WindowVisualId(window);
const EGLint visualIds[] = {windowVisualId, QueryDefaultX11VisualId()};
for (const auto visualId : visualIds) {
if (visualId == 0) {
continue;
}
for (const auto config : configs) {
EGLint nativeVisualId = 0;
if (ConfigSupports(config, surfaceBit) &&
GetConfigAttrib(config, EGL_NATIVE_VISUAL_ID, nativeVisualId) &&
nativeVisualId == visualId) {
outConfig = config;
return true;
for (const auto* candidates : {&configs, &alphaFreeConfigs}) {
for (const auto config : *candidates) {
EGLint nativeVisualId = 0;
if (ConfigSupports(config, surfaceBit) &&
GetConfigAttrib(config, EGL_NATIVE_VISUAL_ID, nativeVisualId) &&
nativeVisualId == visualId) {
outConfig = config;
return true;
}
}
}
}
@@ -5894,6 +5919,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
return true;
}
namespace {
// EGL ground-truth verification stamp, per thread. glvnd's
// eglGetCurrentContext performs fork detection with a real getpid()
// syscall on every call, and this predicate sits 2-3 deep in every
// draw - measured at 16% of the render thread on a live workload.
thread_local Uint64 t_eglVerifiedFrameSerial = ~0ull;
thread_local Uint t_eglVerifiedContextGeneration = 0;
} // namespace
Bool IsBackendContextCurrentOnThisThread() {
if (g_Context == EGL_NO_CONTEXT) {
return false;
@@ -5904,10 +5938,20 @@ namespace MobileGL::MG_Backend::DirectGLES {
// Belt and braces: EGL itself is the ground truth. A migration that bypassed
// MakeCurrent()/ReleaseCurrent() must not leave a stale ownership claim
// standing, or GL calls would silently no-op while shadow bookkeeping (bind
// cache, synced serials) still advances.
// cache, synced serials) still advances. Re-verify once per (thread, frame,
// context generation) rather than per call: an external migration is caught
// at the next frame boundary instead of the next call, which recovers the
// bookkeeping just the same, without paying a syscall on every draw.
const Uint64 frameSerial = g_currentFrameSerial.load(std::memory_order_relaxed);
if (t_eglVerifiedFrameSerial == frameSerial &&
t_eglVerifiedContextGeneration == g_syncContextGeneration) {
return true;
}
if (g_EGLFuncs.eglGetCurrentContext && g_EGLFuncs.eglGetCurrentContext() != g_Context) {
return false;
}
t_eglVerifiedFrameSerial = frameSerial;
t_eglVerifiedContextGeneration = g_syncContextGeneration;
return true;
}
@@ -6202,6 +6246,33 @@ namespace MobileGL::MG_Backend::DirectGLES {
Uint64 CurrentFrameSerial() { return g_currentFrameSerial.load(std::memory_order_relaxed); }
Uint64 CompletedFrameSerial() { return g_completedFrameSerial.load(std::memory_order_relaxed); }
Bool WaitForFrameSerialCompleted(Uint64 serial, Uint64 timeoutNs) {
if (CompletedFrameSerial() >= serial) return true;
if (!IsBackendContextCurrentOnThisThread() || !g_GLESFuncs.glClientWaitSync) return false;
// Fences signal in submission order, so the live fence with the SMALLEST
// serial at or past the target is the earliest event that proves the
// target frame retired. A recycled slot (GPU more than ring-depth frames
// behind) leaves no usable fence; report failure and let the caller pick
// its own fallback rather than draining the whole queue here.
FrameFence* best = nullptr;
for (FrameFence& slot : g_frameFenceRing) {
if (!slot.sync || slot.contextGeneration != g_syncContextGeneration) continue;
if (slot.serial < serial) continue;
if (!best || slot.serial < best->serial) best = &slot;
}
if (!best) return false;
const GLenum status =
g_GLESFuncs.glClientWaitSync(best->sync, GL_SYNC_FLUSH_COMMANDS_BIT, timeoutNs);
if (status != GL_ALREADY_SIGNALED && status != GL_CONDITION_SATISFIED) return false;
Uint64 completed = g_completedFrameSerial.load(std::memory_order_relaxed);
if (best->serial > completed) {
g_completedFrameSerial.store(best->serial, std::memory_order_relaxed);
}
if (g_GLESFuncs.glDeleteSync) g_GLESFuncs.glDeleteSync(best->sync);
best->sync = nullptr;
return true;
}
void Present() {
// Insert one fence per frame BEFORE the swap (eglSwapBuffers' implicit flush
// makes it reachable), then non-blocking-poll prior frames' fences AFTER to
@@ -6247,6 +6318,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
XfbImpl::OnBackendContextDestroyed();
ScratchFBOImpl::OnBackendContextDestroyed();
FramebufferImpl::InvalidateFramebufferBindingCache();
VertexArrayImpl::InvalidateVAOBindingCache();
PixelStoreImpl::InvalidatePackStateCache();
// Texture ids belong to the dying context; wrappers destroyed later must
// not glDeleteTextures a recycled name in a successor context.
@@ -160,6 +160,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
// A buffer retired during frame N is safe to recycle once CompletedFrameSerial() >= N.
Uint64 CurrentFrameSerial();
Uint64 CompletedFrameSerial();
// Block (up to timeoutNs) until the given frame serial provably retired on the
// GPU, using the per-frame fence ring. False when no usable fence covers the
// serial (fence-less context, foreign thread, or the slot was recycled);
// completion state is untouched in that case.
Bool WaitForFrameSerialCompleted(Uint64 serial, Uint64 timeoutNs);
// Applies (or defers until the window surface exists) the app-requested
// eglSwapInterval on the native EGL surface.
void SetSwapInterval(Int interval);
+145 -33
View File
@@ -292,6 +292,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
// sync point with a current ES context.
Vector<SharedPtr<BackendBufferResource>> g_deferredBufferReleases;
std::mutex g_deferredBufferReleasesMutex;
// Cheap emptiness probe so the per-draw drain can skip the mutex and
// context check when nothing was enqueued (the overwhelmingly common
// case). Written only under the mutex; read lock-free.
std::atomic<Bool> g_hasDeferredBufferReleases{false};
// --- Buffer-storage pool (Mesa-style BO recycle) -------------------------
// Recycle idle GL buffer ids of an EXACT byte size instead of glDeleteBuffers
@@ -457,8 +461,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
const SizeT size = bufferObject.GetSize();
const GLenum usage = MG_Util::ConvertBufferUsageToGLEnum(bufferObject.GetUsage());
BindBufferId(TempBufferTarget, resource.id);
g_GLESFuncs.glBufferData(TempBufferTarget, (GLsizeiptr)size,
size > 0 ? bufferObject.MappedData() : nullptr, usage);
// An orphaning respecify (glBufferData with NULL, content never
// written since) stays a pure NULL reallocation: the driver renames
// the store without a stall and nothing is transferred. Uploading
// the stale shadow here turned Minecraft-style orphaning into a
// full-size synchronized upload.
const void* initialData =
(size > 0 && bufferObject.HasDefinedContent()) ? bufferObject.MappedData() : nullptr;
g_GLESFuncs.glBufferData(TempBufferTarget, (GLsizeiptr)size, initialData, usage);
resource.storageSize = size;
resource.storageInitialized = true;
resource.pendingRespecify = false;
@@ -680,6 +690,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
const std::lock_guard<std::mutex> lock(g_deferredBufferReleasesMutex);
g_deferredBufferReleases.push_back(std::move(resource));
g_hasDeferredBufferReleases.store(true, std::memory_order_release);
}
const BufferBackendOps g_glesBufferBackendOps = {
@@ -706,6 +717,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
const std::lock_guard<std::mutex> lock(g_deferredBufferReleasesMutex);
// The ES context owning these ids is going away; just drop the handles.
g_deferredBufferReleases.clear();
g_hasDeferredBufferReleases.store(false, std::memory_order_release);
}
void OnBackendContextDestroyed() {
@@ -720,11 +732,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
void ProcessDeferredBufferReleases() {
// Runs on every draw; skip the context check, mutex and vector churn
// outright when nothing was enqueued since the last drain.
if (!g_hasDeferredBufferReleases.load(std::memory_order_acquire)) return;
if (!CanTouchGLNow()) return;
Vector<SharedPtr<BackendBufferResource>> releases;
{
const std::lock_guard<std::mutex> lock(g_deferredBufferReleasesMutex);
releases.swap(g_deferredBufferReleases);
g_hasDeferredBufferReleases.store(false, std::memory_order_release);
}
for (auto& resource : releases) {
auto* glesResource = static_cast<GLESBufferResource*>(resource.get());
@@ -1115,8 +1131,32 @@ namespace MobileGL::MG_Backend::DirectGLES {
} else if (g_uboRing.creationFailed) {
return false; // store lost; callers fall back to glBufferSubData
} else {
// At the size cap (>kUboRingMaxBytes of uniforms in flight — not a
// real workload): drain the GPU once rather than corrupt live slots.
// At the size cap (>kUboRingMaxBytes of uniforms in flight). First
// try to free room by waiting for the OLDEST in-flight frames to
// retire - a bounded wait that ends as soon as enough tail space
// exists, instead of draining the entire queue.
constexpr Uint64 kFrameWaitNs = 50ull * 1000 * 1000; // 50ms per frame
while (!g_uboRingFrameMarks.empty() &&
g_uboRing.head + alignedSize - g_uboRing.tail > g_uboRing.size) {
const auto& oldest = g_uboRingFrameMarks.front();
if (!DirectGLES::WaitForFrameSerialCompleted(oldest.frameSerial, kFrameWaitNs)) {
break;
}
if (oldest.headAtPresent > g_uboRing.tail) g_uboRing.tail = oldest.headAtPresent;
g_uboRingFrameMarks.erase(g_uboRingFrameMarks.begin());
}
if (g_uboRing.head + alignedSize - g_uboRing.tail <= g_uboRing.size) {
offset = static_cast<SizeT>(g_uboRing.head % g_uboRing.size);
if (offset + alignedSize > g_uboRing.size) {
g_uboRing.head += g_uboRing.size - offset;
offset = 0;
}
g_uboRing.head += alignedSize;
outOffset = offset;
return true;
}
// No usable fence covers the oldest frames: drain once rather than
// corrupt live slots.
if (g_GLESFuncs.glFinish) g_GLESFuncs.glFinish();
g_uboRing.tail = g_uboRing.head;
g_uboRingFrameMarks.clear();
@@ -1234,6 +1274,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
BackendVertexArrayObject::~BackendVertexArrayObject() {
if (m_backendVAOId != 0) {
NoteVAOIdDeleted(m_backendVAOId);
g_GLESFuncs.glDeleteVertexArrays(1, &m_backendVAOId);
m_backendVAOId = 0;
}
@@ -1246,11 +1287,35 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
}
namespace {
Uint g_boundBackendVAOId = 0;
Bool g_boundBackendVAOKnown = false;
} // namespace
void BindBackendVAOId(Uint id) {
if (g_boundBackendVAOKnown && g_boundBackendVAOId == id) {
return;
}
g_GLESFuncs.glBindVertexArray(id);
g_boundBackendVAOId = id;
g_boundBackendVAOKnown = true;
}
void InvalidateVAOBindingCache() {
g_boundBackendVAOKnown = false;
}
void NoteVAOIdDeleted(Uint id) {
if (g_boundBackendVAOKnown && g_boundBackendVAOId == id) {
g_boundBackendVAOId = 0; // glDeleteVertexArrays reverts a bound VAO to 0
}
}
void BackendVertexArrayObject::Bind() const {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
g_GLESFuncs.glBindVertexArray(m_backendVAOId);
BindBackendVAOId(m_backendVAOId);
}
inline Bool BindAttributeBuffer(const MG_State::GLState::VertexAttribute& attrib) {
@@ -1792,7 +1857,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
// textures thrash, forcing a real glBindTexture per texture per draw. When
// nothing needs uploading, skip the bind + upload machinery entirely;
// BindCurrentTextures() re-establishes the real sampling bindings regardless.
if (m_isInitialized && stateTextureObject->GetStorageType() == TextureStorageType::Mipmap) {
// The content-version stamp short-circuits before any shape probing: it
// bumps on every CPU-side pixel mutation, so an unchanged stamp plus an
// unchanged shape means no level can be dirty. Shape stays a separate
// compare because a NULL-data glTexImage changes it without touching the
// content version.
if (m_isInitialized && stateTextureObject->GetStorageType() == TextureStorageType::Mipmap &&
m_syncedContentVersion != 0 &&
m_syncedContentVersion == stateTextureObject->GetContentVersion()) {
auto* mipmapObject =
static_cast<MG_State::GLState::TextureObjectMipmap*>(stateTextureObject.get());
const auto probeBaseSize = stateTextureObject->GetBaseSize();
@@ -1804,25 +1876,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
0,
stateTextureObject->GetSamples(),
stateTextureObject->HasFixedSampleLocations()};
// Equal info => needsRegeneration is false, and canAppendMipmaps is
// false too (it requires strictly more mip levels than the last sync).
// So the only remaining work would be re-uploading dirty levels.
if (probe == m_prevTextureInfo) {
Bool anyDirty = false;
for (const auto& uploadTarget : mipmapObject->GetUploadTargets()) {
for (SizeT level = 0; level < probe.mipmapLevels; ++level) {
if (mipmapObject->IsStorageDirty(uploadTarget, level)) {
anyDirty = true;
break;
}
}
if (anyDirty) break;
}
if (!anyDirty) {
MGLOG_D("Texture ID %u already fully synced, skipping scratch bind + upload.",
m_backendTextureId);
return;
}
MGLOG_D("Texture ID %u already fully synced, skipping scratch bind + upload.",
m_backendTextureId);
return;
}
}
@@ -2199,24 +2256,75 @@ namespace MobileGL::MG_Backend::DirectGLES {
uploadData, byteSize, &glType, packedUploadData);
const IntVec3 uploadSize =
GetBackendUploadSize(stateTextureObject->GetTarget(), texelSize);
// Sub-rect upload: when only a region of the level changed (a
// 16x16 sprite in a 1024x512 atlas, the per-frame lightmap) and
// the shadow bytes go to the driver unconverted, upload just that
// region with UNPACK_ROW_LENGTH striding into the level shadow.
// Conversion fallbacks rewrite the whole level into a fresh
// buffer, so they stay on the full-level path, as do targets
// whose backend upload size differs from the shadow's texel size.
const auto dirtyRegion = textureMipmapObject->GetStorageDirtyRegion(uploadTarget, level);
const SizeT texelCount = static_cast<SizeT>(texelSize.x()) *
static_cast<SizeT>(texelSize.y()) *
static_cast<SizeT>(std::max(texelSize.z(), 1));
const Bool subRectEligible =
uploadData == mipData && !dirtyRegion.Empty() &&
!dirtyRegion.CoversWholeLevel(texelSize) && texelCount > 0 &&
byteSize % texelCount == 0 && uploadSize.x() == texelSize.x() &&
uploadSize.y() == texelSize.y() &&
std::max(uploadSize.z(), 1) == std::max(texelSize.z(), 1);
const SizeT bpp = subRectEligible ? byteSize / texelCount : 0;
const IntVec3 regionSize = {dirtyRegion.hi.x() - dirtyRegion.lo.x(),
dirtyRegion.hi.y() - dirtyRegion.lo.y(),
dirtyRegion.hi.z() - dirtyRegion.lo.z()};
const SizeT levelRowBytes = static_cast<SizeT>(texelSize.x()) * bpp;
const SizeT levelSliceBytes = static_cast<SizeT>(texelSize.y()) * levelRowBytes;
const Uint8* regionPtr =
static_cast<const Uint8*>(uploadData) +
static_cast<SizeT>(dirtyRegion.lo.z()) * levelSliceBytes +
static_cast<SizeT>(dirtyRegion.lo.y()) * levelRowBytes +
static_cast<SizeT>(dirtyRegion.lo.x()) * bpp;
switch (MapToBackendTextureTarget(stateTextureObject->GetTarget())) {
case TextureTarget::Texture2D:
case TextureTarget::TextureCubeMap:
g_GLESFuncs.glTexSubImage2D(glUploadTarget, static_cast<GLint>(level), 0, 0,
static_cast<GLsizei>(uploadSize.x()),
static_cast<GLsizei>(uploadSize.y()), glFormat, glType,
uploadData);
if (subRectEligible) {
g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, texelSize.x());
g_GLESFuncs.glTexSubImage2D(
glUploadTarget, static_cast<GLint>(level), dirtyRegion.lo.x(),
dirtyRegion.lo.y(), static_cast<GLsizei>(regionSize.x()),
static_cast<GLsizei>(regionSize.y()), glFormat, glType, regionPtr);
// The surrounding ScopedDefaultUnpackState shadow says 0.
g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
} else {
g_GLESFuncs.glTexSubImage2D(glUploadTarget, static_cast<GLint>(level), 0, 0,
static_cast<GLsizei>(uploadSize.x()),
static_cast<GLsizei>(uploadSize.y()), glFormat,
glType, uploadData);
}
break;
case TextureTarget::Texture3D:
case TextureTarget::Texture2DArray:
// ES 3.2 has GL_TEXTURE_CUBE_MAP_ARRAY natively and it stores exactly
// like a 2D array whose depth is 6 * the cube count.
case TextureTarget::TextureCubeMapArray:
g_GLESFuncs.glTexSubImage3D(glUploadTarget, static_cast<GLint>(level), 0, 0, 0,
static_cast<GLsizei>(uploadSize.x()),
static_cast<GLsizei>(uploadSize.y()),
static_cast<GLsizei>(uploadSize.z()), glFormat, glType,
uploadData);
if (subRectEligible) {
g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, texelSize.x());
g_GLESFuncs.glPixelStorei(GL_UNPACK_IMAGE_HEIGHT, texelSize.y());
g_GLESFuncs.glTexSubImage3D(
glUploadTarget, static_cast<GLint>(level), dirtyRegion.lo.x(),
dirtyRegion.lo.y(), dirtyRegion.lo.z(),
static_cast<GLsizei>(regionSize.x()),
static_cast<GLsizei>(regionSize.y()),
static_cast<GLsizei>(regionSize.z()), glFormat, glType, regionPtr);
g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
g_GLESFuncs.glPixelStorei(GL_UNPACK_IMAGE_HEIGHT, 0);
} else {
g_GLESFuncs.glTexSubImage3D(glUploadTarget, static_cast<GLint>(level), 0, 0, 0,
static_cast<GLsizei>(uploadSize.x()),
static_cast<GLsizei>(uploadSize.y()),
static_cast<GLsizei>(uploadSize.z()), glFormat,
glType, uploadData);
}
break;
default:
MGLOG_E("Unhandled texture target %s",
@@ -2300,6 +2408,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
});
m_prevTextureInfo = currentTextureInfo;
// Everything dirty at entry is uploaded (or provably has no bytes to
// upload); stamp the version so per-draw re-syncs short-circuit until
// the next CPU-side mutation.
m_syncedContentVersion = stateTextureObject->GetContentVersion();
}
void BackendTextureObject::SyncBuiltinSamplerToBackend(
+13
View File
@@ -266,6 +266,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
extern StateBackendObjectRegistry<MG_State::GLState::VertexArrayObject, BackendVertexArrayObject>
g_backendVertexArrayObjects;
// Shadowed glBindVertexArray: every backend VAO bind goes through here so a
// draw's second bind of the same VAO (SyncToBackend, then PrepareForDraw's
// re-bind) reaches the driver once. Invalidate whenever the ES context is
// replaced - ids restart and the resting binding is 0 again.
void BindBackendVAOId(Uint id);
void InvalidateVAOBindingCache();
// ES resets the binding to 0 when the currently bound VAO is deleted.
void NoteVAOIdDeleted(Uint id);
} // namespace VertexArrayImpl
namespace TextureImpl {
@@ -375,6 +384,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
Bool m_imageBindableStorageRequired = false;
Bool m_backendStorageImmutable = false;
StateTextureBasicInfo m_prevTextureInfo;
// Frontend content version at the last completed mipmap sync. The per-draw
// clean probe compares this before rebuilding shape info and scanning
// per-level dirty flags; 0 never matches a real version (they start at 1).
Uint64 m_syncedContentVersion = 0;
SamplerParameters m_cacheSamplerParameters;
UintVec2 m_cacheLodRange = {0, 1000};
FloatVec4 m_cacheBorderColor = {0.0f, 0.0f, 0.0f, 0.0f};
@@ -202,9 +202,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
for (auto& cacheEntryPair : frame.descriptorSetCacheByLayout) {
cacheEntryPair.second.cursor = 0;
}
// The frame's descriptor sets are recycled above, so last frame's reuse target
// is gone: start the per-draw descriptor-reuse cache fresh this frame.
m_hasLastDescriptor = false;
// The frame's descriptor sets are recycled above, so last frame's reuse targets
// are gone: start the per-draw descriptor-reuse cache fresh this frame.
for (auto& entry : m_descriptorReuseMemo) {
entry.valid = false;
}
m_lastBindValid = false;
// Re-fingerprint the bound sampler set fresh this frame so any GL object address
// reuse cannot outlive a single frame (see SamplerResolveMemo).
@@ -241,8 +243,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
if (purgedSets > 0) {
// The per-draw reuse memo folds the layout handle into its signature; drop
// it so a recycled handle value cannot revive a purged set mid-frame.
m_hasLastDescriptor = false;
// every entry so a recycled handle value cannot revive a purged set mid-frame.
for (auto& entry : m_descriptorReuseMemo) {
entry.valid = false;
}
MGLOG_D("UniformDescriptorBinder: freed %zu descriptor sets for destroyed layout", purgedSets);
}
}
@@ -1358,13 +1362,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
}
// Reuse the previous draw's descriptor set when the resolved content is
// Reuse a recent draw's descriptor set when the resolved content is
// byte-identical (only the bind-time dynamic offsets differ). The signature
// covers the descriptor-set layout + every write's binding/type/count + the
// pointed-to buffer/image/texel-buffer infos (all value-initialized, so no
// padding noise). Correctness: bindings are re-resolved every draw, so the
// signature always reflects the current state and reuse happens only on an
// exact match; the reused set is never re-acquired within a frame (the acquire
// exact match; a reused set is never re-acquired within a frame (the acquire
// cursor only advances), so its written contents survive; the layout is part of
// the signature so reuse never crosses programs. Sampler overrides (blits)
// bypass and invalidate the cache.
@@ -1396,8 +1400,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
mixWords(texelBufferViews.data(), texelBufferViews.size() * sizeof(VkBufferView));
}
if (cacheable && m_hasLastDescriptor && signature == m_lastDescriptorSignature) {
descriptorSet = m_lastBoundDescriptorSet;
VkDescriptorSet reusedSet = VK_NULL_HANDLE;
if (cacheable) {
for (const auto& entry : m_descriptorReuseMemo) {
if (entry.valid && entry.signature == signature) {
reusedSet = entry.set;
break;
}
}
}
if (reusedSet != VK_NULL_HANDLE) {
descriptorSet = reusedSet;
} else {
VkResult allocResult = AcquireDescriptorSet(frameIndex, programObj, descriptorSet);
if (allocResult != VK_SUCCESS || descriptorSet == VK_NULL_HANDLE) {
@@ -1411,9 +1424,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (!writes.empty()) {
vkUpdateDescriptorSets(m_device, static_cast<Uint32>(writes.size()), writes.data(), 0, nullptr);
}
m_lastBoundDescriptorSet = descriptorSet;
m_lastDescriptorSignature = signature;
m_hasLastDescriptor = cacheable;
if (cacheable) {
m_descriptorReuseMemo[m_descriptorReuseMemoNext] =
DescriptorReuseEntry{signature, descriptorSet, true};
m_descriptorReuseMemoNext = (m_descriptorReuseMemoNext + 1) % kDescriptorReuseMemoSize;
} else {
for (auto& entry : m_descriptorReuseMemo) {
entry.valid = false;
}
}
}
// Skip the driver call when this exact binding is already live on the
@@ -179,14 +179,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Vector<VkBufferView> m_texelBufferViewsScratch;
Vector<Uint32> m_dynamicOffsetsScratch;
// Descriptor-set reuse across consecutive draws (see BindProgramUniformBuffers).
// When a draw's resolved descriptor content is byte-identical to the previous
// draw's, reuse the same VkDescriptorSet and skip AcquireDescriptorSet +
// vkUpdateDescriptorSets - only the bind-time dynamic offsets differ. Reset each
// frame in BeginFrame because the frame's descriptor sets are recycled there.
VkDescriptorSet m_lastBoundDescriptorSet = VK_NULL_HANDLE;
Uint64 m_lastDescriptorSignature = 0;
Bool m_hasLastDescriptor = false;
// Descriptor-set reuse across recent draws (see BindProgramUniformBuffers).
// When a draw's resolved descriptor content is byte-identical to one memoized
// earlier, reuse that VkDescriptorSet and skip AcquireDescriptorSet +
// vkUpdateDescriptorSets - only the bind-time dynamic offsets differ. Four
// entries with round-robin replacement rather than one: draws alternating
// between two programs (MC's chunk<->entity ping-pong) would thrash a single
// slot into a full re-allocate+write every draw. Reset each frame in BeginFrame
// because the frame's descriptor sets are recycled there.
struct DescriptorReuseEntry {
Uint64 signature = 0;
VkDescriptorSet set = VK_NULL_HANDLE;
Bool valid = false;
};
static constexpr Uint32 kDescriptorReuseMemoSize = 4;
DescriptorReuseEntry m_descriptorReuseMemo[kDescriptorReuseMemoSize];
Uint32 m_descriptorReuseMemoNext = 0;
// vkCmdBindDescriptorSets dedup: consecutive draws with a static uniform
// block resolve to the same set AND the same dynamic offsets, so the
@@ -591,6 +591,34 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return true;
}
// Idle-content promotion: see the field comments in VkBufferResource. The
// streak counts frame BOUNDARIES survived unchanged (the same-frame memo
// above swallows repeat draws), so a promotion needs the content stable
// for kStreamedPromotionStreak whole frames - one no-op frame does not
// trigger the resident round-trip, whose creation upload is itself a
// staged copy worth avoiding for content that is about to change again.
constexpr Uint32 kStreamedPromotionStreak = 2;
if (resource->promotedResident) {
if (resource->promotedChangeSerial == changeSerial &&
static_cast<VkDeviceSize>(bufferObject->GetSize()) == size) {
return AcquireResidentSlice(kind, bufferObject, outSlice);
}
resource->promotedResident = false;
resource->unchangedStreak = 0;
} else if (resource->transientChangeSerial == changeSerial && resource->transientSize == size &&
resource->transientFrameSerial != 0) {
if (++resource->unchangedStreak >= kStreamedPromotionStreak) {
resource->promotedResident = true;
resource->promotedChangeSerial = changeSerial;
if (AcquireResidentSlice(kind, bufferObject, outSlice)) {
return true;
}
resource->promotedResident = false; // resident creation failed: stream as before
}
} else {
resource->unchangedStreak = 0;
}
if (!m_transientUploadArena.Upload(m_currentFrameIndex, bufferObject->MappedData(), size, 16,
outSlice)) {
return false;
@@ -62,6 +62,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint64 transientFrameSerial = 0;
Uint64 transientChangeSerial = 0;
VkDeviceSize transientSize = 0;
// Streaming re-copies the whole store into the per-frame arena on every
// frame, which is right for genuinely per-frame data but pure waste for a
// Dynamic-hinted buffer the app stopped touching. After the content
// survives kStreamedPromotionStreak frame boundaries unchanged it is
// promoted to resident storage (one final upload, then zero per-frame
// cost); the first content change demotes it back to streaming, and the
// streaming path's existing downgrade releases the resident store.
Uint32 unchangedStreak = 0;
Bool promotedResident = false;
Uint64 promotedChangeSerial = 0;
};
// Supplies a command buffer that is recording and outside any render pass,
@@ -25,9 +25,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// Compute shaders may legally sample framebuffer-attached textures (the GL feedback-loop rule
// only covers rendering commands; e.g. Flywheel's Hi-Z depth pyramid downsample samples the
// depth attachment of the bound draw framebuffer), so sampled-read barriers must cover the
// compute stage in addition to the graphics stages.
static constexpr VkPipelineStageFlags kSampledReadStages =
VK_PIPELINE_STAGE_ALL_GRAPHICS_BIT | VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT;
// compute stage in addition to the graphics stages. Set at Initialize from the renderer's
// device-feature-derived mask: geometry/tessellation stage bits are invalid in a barrier when
// their feature is off (VUID-vkCmdPipelineBarrier-srcStageMask-04090/-04091), and ALL_GRAPHICS
// would also serialize against non-shader stages. The default only matters before a device
// exists, when nothing records barriers.
static VkPipelineStageFlags s_sampledReadStages =
VK_PIPELINE_STAGE_VERTEX_SHADER_BIT | VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT |
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT;
static Uint32 ComputeFullMipLevelCount(const IntVec3& baseTexelSize) {
Int maxDimension = std::max<Int>(baseTexelSize.x(),
@@ -193,7 +198,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
case VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_STENCIL_ATTACHMENT_OPTIMAL:
case VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_STENCIL_READ_ONLY_OPTIMAL:
case VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL:
outSrcStageMask = kSampledReadStages;
outSrcStageMask = s_sampledReadStages;
outSrcAccessMask = VK_ACCESS_SHADER_READ_BIT;
return;
case VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL:
@@ -241,7 +246,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
case VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_STENCIL_ATTACHMENT_OPTIMAL:
case VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_STENCIL_READ_ONLY_OPTIMAL:
case VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL:
outDstStageMask = kSampledReadStages;
outDstStageMask = s_sampledReadStages;
outDstAccessMask = VK_ACCESS_SHADER_READ_BIT;
return;
case VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL:
@@ -599,6 +604,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_commandPool = initInfo.commandPool;
m_graphicsQueue = initInfo.graphicsQueue;
m_imageFormatListSupported = initInfo.imageFormatListSupported;
s_sampledReadStages = initInfo.sampledReadStageMask;
m_currentFrameIndex = 0;
m_deferredReleases.clear();
m_deferredReleases.resize(initInfo.frameCount);
@@ -1226,7 +1232,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
const Bool ok = TransitionImageLayout(commandBuffer, resource->image, resource->layout, targetLayout, srcStageMask,
kSampledReadStages, srcAccessMask,
s_sampledReadStages, srcAccessMask,
VK_ACCESS_SHADER_READ_BIT, resource->aspect, 0, resource->mipLevels,
resource->arrayLayers);
MOBILEGL_ASSERT(ok, "TransitionTextureForSampling: transition failed for textureId=%d", texture.GetExternalIndex());
@@ -2055,6 +2061,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const void* source = nullptr;
Vector<Uint8> expandedData;
VkDeviceSize offset = 0;
// Sub-region upload (a small sprite in a big atlas): only the dirty box
// is staged and copied. texelSize keeps the LEVEL extent - the staging
// row copy needs it for the shadow's stride. Plain color formats only;
// the RGB-expand and depth(+stencil) conversion passes rewrite whole
// levels and stay full-size.
Bool subRegion = false;
IntVec3 regionLo = {0, 0, 0};
IntVec3 regionSize = {0, 0, 0};
SizeT texelBytes = 0;
};
Vector<UploadItem> uploadItems;
@@ -2101,6 +2116,25 @@ namespace MobileGL::MG_Backend::DirectVulkan {
uploadItem.source = source;
uploadItem.offset = stagingSize;
uploadItem.uploadByteSize = byteSize;
if (!formatInfo.expandRgbToRgba &&
GetAspectMaskForFormat(outResource.format) == VK_IMAGE_ASPECT_COLOR_BIT) {
const auto region = mipmapTexture.GetStorageDirtyRegion(target, level);
const SizeT texelCount = static_cast<SizeT>(texelSize.x()) *
static_cast<SizeT>(texelSize.y()) *
static_cast<SizeT>(std::max(texelSize.z(), 1));
if (!region.Empty() && !region.CoversWholeLevel(texelSize) && texelCount > 0 &&
byteSize % texelCount == 0) {
uploadItem.subRegion = true;
uploadItem.regionLo = region.lo;
uploadItem.regionSize = {region.hi.x() - region.lo.x(), region.hi.y() - region.lo.y(),
region.hi.z() - region.lo.z()};
uploadItem.texelBytes = byteSize / texelCount;
uploadItem.uploadByteSize = static_cast<SizeT>(uploadItem.regionSize.x()) *
static_cast<SizeT>(uploadItem.regionSize.y()) *
static_cast<SizeT>(uploadItem.regionSize.z()) *
uploadItem.texelBytes;
}
}
if (formatInfo.expandRgbToRgba) {
const Bool expanded = ExpandRgbSourceToRgba(source, byteSize, texelSize, formatInfo,
uploadItem.expandedData);
@@ -2255,7 +2289,27 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void* mapped = nullptr;
VK_VERIFY(vmaMapMemory(m_allocator, stagingAllocation, &mapped), "vmaMapMemory(staging texture)");
for (const auto& item : uploadItems) {
std::memcpy(static_cast<Uint8*>(mapped) + item.offset, item.source, item.uploadByteSize);
Uint8* dst = static_cast<Uint8*>(mapped) + item.offset;
if (!item.subRegion) {
std::memcpy(dst, item.source, item.uploadByteSize);
continue;
}
// Tight-pack the dirty box: the shadow keeps whole-level rows, the
// staging slice holds only the region (bufferRowLength stays 0).
const SizeT levelRowBytes = static_cast<SizeT>(item.texelSize.x()) * item.texelBytes;
const SizeT levelSliceBytes = static_cast<SizeT>(item.texelSize.y()) * levelRowBytes;
const SizeT regionRowBytes = static_cast<SizeT>(item.regionSize.x()) * item.texelBytes;
const Uint8* src = static_cast<const Uint8*>(item.source);
for (Int z = 0; z < item.regionSize.z(); ++z) {
for (Int y = 0; y < item.regionSize.y(); ++y) {
const Uint8* srcRow = src +
static_cast<SizeT>(item.regionLo.z() + z) * levelSliceBytes +
static_cast<SizeT>(item.regionLo.y() + y) * levelRowBytes +
static_cast<SizeT>(item.regionLo.x()) * item.texelBytes;
std::memcpy(dst + (static_cast<SizeT>(z) * item.regionSize.y() + y) * regionRowBytes,
srcRow, regionRowBytes);
}
}
}
vmaUnmapMemory(m_allocator, stagingAllocation);
@@ -2306,6 +2360,21 @@ namespace MobileGL::MG_Backend::DirectVulkan {
copy.imageOffset = {0, 0, 0};
copy.imageExtent = {static_cast<Uint32>(item.texelSize.x()), static_cast<Uint32>(item.texelSize.y()),
depthSelectsArrayLayer ? 1u : depthOrLayers};
if (item.subRegion) {
const Uint32 regionDepth = static_cast<Uint32>(std::max(item.regionSize.z(), 1));
copy.imageOffset = {item.regionLo.x(), item.regionLo.y(),
depthSelectsArrayLayer ? 0 : item.regionLo.z()};
copy.imageExtent = {static_cast<Uint32>(item.regionSize.x()),
static_cast<Uint32>(item.regionSize.y()),
depthSelectsArrayLayer ? 1u : regionDepth};
if (depthSelectsArrayLayer) {
// The GL "depth" axis addresses array layers here, so a partial
// z-range narrows the layer span rather than the extent.
copy.imageSubresource.baseArrayLayer =
item.baseArrayLayer + static_cast<Uint32>(item.regionLo.z());
copy.imageSubresource.layerCount = regionDepth;
}
}
if (isCombinedDepthStencil) {
const SizeT texelCount = static_cast<SizeT>(item.texelSize.x()) *
static_cast<SizeT>(item.texelSize.y()) *
@@ -2330,7 +2399,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
uploadLayout,
finalLayout,
VK_PIPELINE_STAGE_TRANSFER_BIT,
kSampledReadStages,
s_sampledReadStages,
VK_ACCESS_TRANSFER_WRITE_BIT,
VK_ACCESS_SHADER_READ_BIT,
aspectMask, 0, outResource.mipLevels, outResource.arrayLayers);
@@ -59,6 +59,12 @@ public:
// VK_KHR_image_format_list is enabled: MUTABLE_FORMAT images can name the exact set of
// formats they will be viewed as, which is what lets a tiler keep them compressed.
Bool imageFormatListSupported = false;
// Union of shader stages sampled-read barriers may name on this device; the renderer
// builds it from the enabled features because geometry/tessellation stage bits are
// invalid in a barrier when their feature is off.
VkPipelineStageFlags sampledReadStageMask = VK_PIPELINE_STAGE_VERTEX_SHADER_BIT |
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT |
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT;
};
struct TextureResource {
@@ -2680,7 +2680,8 @@ void main() {
MOBILEGL_ASSERT(m_textureManager != nullptr, "VkTextureManager creation failed.");
succeeded = m_textureManager->Initialize(
{m_device, m_physicalDevice.handle, m_allocator, m_commandPool, m_graphicsQueue,
m_frameContext.GetFrameCount(), m_imageFormatListExtensionEnabled});
m_frameContext.GetFrameCount(), m_imageFormatListExtensionEnabled,
m_sampledReadStageMask});
MOBILEGL_ASSERT(succeeded, "VkTextureManager initialization failed.");
m_clearManager = MakeUnique<VkClearManager>();
MOBILEGL_ASSERT(m_clearManager != nullptr, "VkClearManager creation failed.");
@@ -2920,6 +2921,23 @@ 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;
}
#endif
if (m_debugMessenger != VK_NULL_HANDLE) {
DestroyDebugMessenger();
m_debugMessenger = VK_NULL_HANDLE;
@@ -9809,6 +9827,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) {
@@ -10134,6 +10166,19 @@ void main() {
: supportedDeviceFeatures.robustBufferAccess;
deviceFeatures.geometryShader = supportedDeviceFeatures.geometryShader;
deviceFeatures.tessellationShader = supportedDeviceFeatures.tessellationShader;
// Sampled-read barriers may only name the shader stages whose device feature is
// actually enabled (VUID-vkCmdPipelineBarrier-srcStageMask-04090/-04091), so the
// mask is assembled here, next to the feature decision, and handed to consumers.
m_sampledReadStageMask = VK_PIPELINE_STAGE_VERTEX_SHADER_BIT |
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT |
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT;
if (deviceFeatures.geometryShader == VK_TRUE) {
m_sampledReadStageMask |= VK_PIPELINE_STAGE_GEOMETRY_SHADER_BIT;
}
if (deviceFeatures.tessellationShader == VK_TRUE) {
m_sampledReadStageMask |= VK_PIPELINE_STAGE_TESSELLATION_CONTROL_SHADER_BIT |
VK_PIPELINE_STAGE_TESSELLATION_EVALUATION_SHADER_BIT;
}
deviceFeatures.independentBlend = supportedDeviceFeatures.independentBlend;
m_independentBlendFeatureEnabled = deviceFeatures.independentBlend == VK_TRUE;
deviceFeatures.fillModeNonSolid = supportedDeviceFeatures.fillModeNonSolid;
@@ -10646,6 +10691,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
@@ -485,6 +491,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// needs no feature). Both cached at device creation and drive a hard-fail-at-draw when absent.
Bool m_dualSrcBlendFeatureEnabled = false;
Bool m_primitiveTopologyListRestartFeatureEnabled = false;
// Union of shader stages sampled-read barriers may name; built at device creation
// because geometry/tessellation stage bits are invalid in a barrier when their
// feature is off (VUID-vkCmdPipelineBarrier-srcStageMask-04090/-04091), and
// ALL_GRAPHICS would also serialize against non-shader stages.
VkPipelineStageFlags m_sampledReadStageMask = VK_PIPELINE_STAGE_VERTEX_SHADER_BIT |
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT |
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT;
// Cached at device creation from the graphics queue family properties
// and device limits; drives timer-query support.
Uint32 m_timestampValidBits = 0;
+2 -1
View File
@@ -41,4 +41,5 @@ add_test(NAME SanityBench COMMAND SanityBench --benchmark_counters_tabular=true)
set_tests_properties(SanityBench PROPERTIES LABELS benchmark)
add_subdirectory(Program)
add_subdirectory(Buffer)
add_subdirectory(Buffer)
add_subdirectory(Driver)
@@ -0,0 +1,15 @@
cmake_minimum_required(VERSION 3.24)
# A real, headless EGL client, deliberately NOT linked against MobileGL: it
# dlopens one EGL provider at runtime ($DRIVERBENCH_EGL_LIB - the system
# libEGL.so.1 for the native driver, or a libMobileGL.so path for either
# MobileGL backend), so the same binary measures all three stacks.
if (NOT UNIX OR APPLE OR ANDROID)
return()
endif()
add_executable(DriverBench DriverBench.c)
target_link_libraries(DriverBench PRIVATE dl)
add_test(NAME DriverBench COMMAND DriverBench draw_tiny)
set_tests_properties(DriverBench PROPERTIES LABELS benchmark)
+454
View File
@@ -0,0 +1,454 @@
/* MobileGL - MobileGL/MG_Benchmark/Driver/DriverBench.c
* 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
*
* Headless, EGL-based driver benchmark shaped like Minecraft's GL usage.
* Unlike the MobileGL_s microbenches next door this exercises a full GL
* stack: it dlopens ONE EGL provider ($DRIVERBENCH_EGL_LIB - the system
* 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.
*
* Every case models one hot pattern from captured Minecraft traces:
* draw_tiny back-to-back glDrawElements, shared state (chunk batch)
* draw_uniform per-draw vec3 offset uniform + draw (chunk sections)
* draw_multi_vao per-draw VAO/VBO switch + draw (per-section buffers)
* tex_pingpong per-draw texture bind churn on one unit
* program_pingpong alternate two programs + mat4 upload (chunk<->entity)
* chunk_upload glBufferData(NULL) orphan + glBufferSubData + draw
* atlas_sprite N 16x16 glTexSubImage2D into a 1024x512 atlas + draw
* lightmap full 16x16 lightmap respecify per frame + draw
* scene_mix composite frame built from the knobs below
*
* Output: one CSV line per case:
* case,frames,ops_per_frame,median_frame_ms,ns_per_op,fps
*/
#include <dlfcn.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
/* ---- EGL constants ---- */
typedef void* EGLDisplay;
typedef void* EGLConfig;
typedef void* EGLContext;
typedef void* EGLSurface;
typedef int EGLint;
typedef unsigned int EGLBoolean;
typedef unsigned int EGLenum;
#define EGL_DEFAULT_DISPLAY ((void*)0)
#define EGL_NO_CONTEXT ((EGLContext)0)
#define EGL_NO_SURFACE ((EGLSurface)0)
#define EGL_FALSE 0
#define EGL_SURFACE_TYPE 0x3033
#define EGL_PBUFFER_BIT 0x0001
#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
#define EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT 0x00000001
/* ---- GL constants ---- */
#define GL_COLOR_BUFFER_BIT 0x00004000
#define GL_DEPTH_BUFFER_BIT 0x00000100
#define GL_TRIANGLES 0x0004
#define GL_UNSIGNED_INT 0x1405
#define GL_SHORT 0x1402
#define GL_FLOAT 0x1406
#define GL_UNSIGNED_BYTE 0x1401
#define GL_ARRAY_BUFFER 0x8892
#define GL_ELEMENT_ARRAY_BUFFER 0x8893
#define GL_STATIC_DRAW 0x88E4
#define GL_TEXTURE_2D 0x0DE1
#define GL_TEXTURE0 0x84C0
#define GL_RGBA 0x1908
#define GL_RGBA8 0x8058
#define GL_DEPTH_COMPONENT24 0x81A6
#define GL_TEXTURE_MIN_FILTER 0x2801
#define GL_TEXTURE_MAG_FILTER 0x2800
#define GL_NEAREST 0x2600
#define GL_NEAREST_MIPMAP_LINEAR 0x2702
#define GL_DEPTH_TEST 0x0B71
#define GL_VERTEX_SHADER 0x8B31
#define GL_FRAGMENT_SHADER 0x8B30
#define GL_COMPILE_STATUS 0x8B81
#define GL_LINK_STATUS 0x8B82
#define GL_VERSION 0x1F02
#define GL_RENDERER 0x1F01
#define GL_NO_ERROR 0
#define GL_FRAMEBUFFER 0x8D40
#define GL_RENDERBUFFER 0x8D41
#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
#define GL_STREAM_DRAW 0x88E0
#define GL_UNPACK_ALIGNMENT 0x0CF5
#define GL_UNPACK_ROW_LENGTH 0x0CF2
#define GL_UNPACK_SKIP_ROWS 0x0CF3
#define GL_UNPACK_SKIP_PIXELS 0x0CF4
#define GL_TEXTURE_WRAP_S 0x2802
#define GL_TEXTURE_WRAP_T 0x2803
#define GL_CLAMP_TO_EDGE 0x812F
#define GL_REPEAT 0x2901
typedef unsigned int GLuint;
typedef int GLint;
typedef int GLsizei;
typedef unsigned int GLenum;
typedef char GLchar;
typedef unsigned char GLboolean;
typedef long GLsizeiptr;
typedef long GLintptr;
/* ---- resolved entry points ---- */
static void* (*g_eglGetProcAddress)(const char*);
static void* g_provider;
#define GLF(ret, name, args) static ret(*name) args;
GLF(void, glClear, (unsigned))
GLF(void, glClearColor, (float, float, float, float))
GLF(void, glEnable, (GLenum))
GLF(void, glViewport, (GLint, GLint, GLsizei, GLsizei))
GLF(const unsigned char*, glGetString, (GLenum))
GLF(GLenum, glGetError, (void))
GLF(void, glFinish, (void))
GLF(void, glFlush, (void))
GLF(void, glGenBuffers, (GLsizei, GLuint*))
GLF(void, glBindBuffer, (GLenum, GLuint))
GLF(void, glBufferData, (GLenum, GLsizeiptr, const void*, GLenum))
GLF(void, glBufferSubData, (GLenum, GLintptr, GLsizeiptr, const void*))
GLF(void, glGenVertexArrays, (GLsizei, GLuint*))
GLF(void, glBindVertexArray, (GLuint))
GLF(void, glEnableVertexAttribArray, (GLuint))
GLF(void, glVertexAttribPointer, (GLuint, GLint, GLenum, GLboolean, GLsizei, const void*))
GLF(void, glGenTextures, (GLsizei, GLuint*))
GLF(void, glBindTexture, (GLenum, GLuint))
GLF(void, glActiveTexture, (GLenum))
GLF(void, glTexImage2D, (GLenum, GLint, GLint, GLsizei, GLsizei, GLint, GLenum, GLenum, const void*))
GLF(void, glTexSubImage2D, (GLenum, GLint, GLint, GLint, GLsizei, GLsizei, GLenum, GLenum, const void*))
GLF(void, glTexParameteri, (GLenum, GLenum, GLint))
GLF(void, glPixelStorei, (GLenum, GLint))
GLF(void, glGetIntegerv, (GLenum, GLint*))
GLF(void, glGenerateMipmap, (GLenum))
GLF(GLuint, glCreateShader, (GLenum))
GLF(void, glShaderSource, (GLuint, GLsizei, const GLchar* const*, const GLint*))
GLF(void, glCompileShader, (GLuint))
GLF(void, glGetShaderiv, (GLuint, GLenum, GLint*))
GLF(void, glGetShaderInfoLog, (GLuint, GLsizei, GLsizei*, GLchar*))
GLF(GLuint, glCreateProgram, (void))
GLF(void, glAttachShader, (GLuint, GLuint))
GLF(void, glLinkProgram, (GLuint))
GLF(void, glGetProgramiv, (GLuint, GLenum, GLint*))
GLF(void, glUseProgram, (GLuint))
GLF(GLint, glGetUniformLocation, (GLuint, const GLchar*))
GLF(void, glUniform1i, (GLint, GLint))
GLF(void, glUniform3f, (GLint, float, float, float))
GLF(void, glUniformMatrix4fv, (GLint, GLsizei, GLboolean, const float*))
GLF(void, glDrawElements, (GLenum, GLsizei, GLenum, const void*))
GLF(void, glBindAttribLocation, (GLuint, GLuint, const GLchar*))
GLF(void, glUniform3fv, (GLint, GLsizei, const float*))
GLF(void, glDrawArrays, (GLenum, GLint, GLsizei))
GLF(void, glDrawElementsBaseVertex, (GLenum, GLsizei, GLenum, const void*, GLint))
GLF(void, glMultiDrawElementsBaseVertex,
(GLenum, const GLsizei*, GLenum, const void* const*, GLsizei, const GLint*))
GLF(void, glBindBufferRange, (GLenum, GLuint, GLuint, GLintptr, GLsizeiptr))
GLF(void, glBindBufferBase, (GLenum, GLuint, GLuint))
GLF(GLuint, glGetUniformBlockIndex, (GLuint, const GLchar*))
GLF(void, glUniformBlockBinding, (GLuint, GLuint, GLuint))
GLF(void, glGenSamplers, (GLsizei, GLuint*))
GLF(void, glBindSampler, (GLuint, GLuint))
GLF(void, glSamplerParameteri, (GLuint, GLenum, GLint))
GLF(void, glGenFramebuffers, (GLsizei, GLuint*))
GLF(void, glBindFramebuffer, (GLenum, GLuint))
GLF(void, glGenRenderbuffers, (GLsizei, GLuint*))
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;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (uint64_t)ts.tv_sec * 1000000000ull + (uint64_t)ts.tv_nsec;
}
static int cmp_u64(const void* a, const void* b) {
uint64_t x = *(const uint64_t*)a, y = *(const uint64_t*)b;
return x < y ? -1 : x > y;
}
/* 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]);
}
}
#include "DriverBenchCases.inc"
/* ---- 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;
end_frame_wait();
for (int i = 0; i < g_warmup; ++i) {
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
body(i, a, b);
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);
end_frame_wait();
samples[i] = now_ns() - t0;
}
qsort(samples, g_frames, sizeof(uint64_t), cmp_u64);
uint64_t med = samples[g_frames / 2];
double frameMs = med / 1e6;
double nsPerOp = opsPerFrame > 0 ? (double)med / (double)opsPerFrame : 0.0;
printf("%s,%d,%ld,%.3f,%.1f,%.1f\n", name, g_frames, opsPerFrame, frameMs, nsPerOp,
1e9 / (double)med);
fflush(stdout);
if (glGetError() != GL_NO_ERROR) fprintf(stderr, "WARN: GL error after %s\n", name);
}
/* a = draws per frame */
/* ---- EGL bootstrap: one provider library, pbuffer, desktop-GL context ---- */
static int boot_egl(void) {
const char* libpath = getenv("DRIVERBENCH_EGL_LIB");
if (!libpath) libpath = "libEGL.so.1";
g_provider = dlopen(libpath, RTLD_LAZY | RTLD_LOCAL);
if (!g_provider) {
fprintf(stderr, "FAIL: dlopen %s: %s\n", libpath, dlerror());
return 1;
}
#define ESYM(name) \
void* p_##name = dlsym(g_provider, #name); \
if (!p_##name) { fprintf(stderr, "FAIL: dlsym %s\n", #name); return 1; }
ESYM(eglGetDisplay)
ESYM(eglInitialize)
ESYM(eglChooseConfig)
ESYM(eglBindAPI)
ESYM(eglCreateContext)
ESYM(eglCreatePbufferSurface)
ESYM(eglMakeCurrent)
ESYM(eglGetProcAddress)
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 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;
}
fprintf(stderr, "EGL %d.%d via %s\n", maj, min, libpath);
// 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;
EGLContext (*createContext)(EGLDisplay, EGLConfig, EGLContext, const EGLint*) =
(EGLContext(*)(EGLDisplay, EGLConfig, EGLContext, const EGLint*))p_eglCreateContext;
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;
}
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 (!((EGLBoolean(*)(EGLDisplay, EGLSurface, EGLSurface, EGLContext))p_eglMakeCurrent)(dpy, surf,
surf, ctx)) {
fprintf(stderr, "FAIL: eglMakeCurrent (0x%x)\n", ((EGLint(*)(void))p_eglGetError)());
return 1;
}
/* Core GL entry points: eglGetProcAddress first (EGL 1.5 serves core
* functions), provider dlsym as fallback (both glvnd and MobileGL export
* the gl* symbols directly). */
#define RESOLVE(name) \
do { \
*(void**)&name = g_eglGetProcAddress(#name); \
if (!name) *(void**)&name = dlsym(g_provider, #name); \
if (!name) { fprintf(stderr, "FAIL: resolve %s\n", #name); return 1; } \
} while (0)
RESOLVE(glClear); RESOLVE(glClearColor); RESOLVE(glEnable); RESOLVE(glViewport);
RESOLVE(glGetString); RESOLVE(glGetError); RESOLVE(glFinish); RESOLVE(glFlush);
RESOLVE(glGenBuffers); RESOLVE(glBindBuffer); RESOLVE(glBufferData); RESOLVE(glBufferSubData);
RESOLVE(glGenVertexArrays); RESOLVE(glBindVertexArray); RESOLVE(glEnableVertexAttribArray);
RESOLVE(glVertexAttribPointer); RESOLVE(glGenTextures); RESOLVE(glBindTexture);
RESOLVE(glActiveTexture); RESOLVE(glTexImage2D); RESOLVE(glTexSubImage2D);
RESOLVE(glTexParameteri); RESOLVE(glGenerateMipmap); RESOLVE(glCreateShader);
RESOLVE(glPixelStorei); RESOLVE(glGetIntegerv);
RESOLVE(glShaderSource); RESOLVE(glCompileShader); RESOLVE(glGetShaderiv);
RESOLVE(glGetShaderInfoLog); RESOLVE(glCreateProgram); RESOLVE(glAttachShader);
RESOLVE(glLinkProgram); RESOLVE(glGetProgramiv); RESOLVE(glUseProgram);
RESOLVE(glGetUniformLocation); RESOLVE(glUniform1i); RESOLVE(glUniform3f);
RESOLVE(glUniformMatrix4fv); RESOLVE(glDrawElements); RESOLVE(glBindAttribLocation);
RESOLVE(glUniform3fv); RESOLVE(glDrawArrays); RESOLVE(glDrawElementsBaseVertex);
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;
}
int main(int argc, char** argv) {
long draws = 2048;
if (getenv("DRIVERBENCH_DRAWS")) draws = atol(getenv("DRIVERBENCH_DRAWS"));
if (getenv("DRIVERBENCH_FRAMES")) g_frames = atoi(getenv("DRIVERBENCH_FRAMES"));
if (getenv("DRIVERBENCH_SPRITES")) g_mixSprites = atol(getenv("DRIVERBENCH_SPRITES"));
if (boot_egl()) return 1;
build_resources();
printf("case,frames,ops_per_frame,median_frame_ms,ns_per_op,fps\n");
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], c->name) == 0) wanted = 1;
if (!wanted) continue;
}
// 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,41 @@
#!/bin/bash
# Run the headless EGL DriverBench on one renderer:
# ./run_driver_bench.sh native [bench args...]
# ./run_driver_bench.sh espryt <libMobileGL.so> [bench args...]
# ./run_driver_bench.sh magma <libMobileGL.so> [bench args...]
# The bench dlopens exactly one EGL provider (DRIVERBENCH_EGL_LIB): the system
# libEGL.so.1 for native, or the given libMobileGL.so for a MobileGL backend -
# no LD_LIBRARY_PATH shadowing, so MobileGL's own loader still finds the real
# driver underneath.
#
# Pin the vendor libraries explicitly. A bare libEGL.so.1 on a glvnd system
# picks whatever vendor eglGetDisplay(EGL_DEFAULT_DISPLAY) resolves first,
# which is Mesa/llvmpipe here - a software rasteriser silently replacing the
# GPU under a benchmark. Override MGL_EGL_VENDOR / MGL_VK_ICD to test another
# driver.
set -eu
HERE=$(cd "$(dirname "$0")" && pwd)
BENCH=${DRIVERBENCH_BIN:-$HERE/DriverBench}
EGL_VENDOR=${MGL_EGL_VENDOR:-/usr/share/glvnd/egl_vendor.d/10_nvidia.json}
VK_ICD=${MGL_VK_ICD:-/usr/share/vulkan/icd.d/nvidia_icd.x86_64.json}
MODE=$1; shift
export __EGL_VENDOR_LIBRARY_FILENAMES=$EGL_VENDOR
export EGL_PLATFORM=${EGL_PLATFORM:-x11}
case "$MODE" in
native)
export DRIVERBENCH_EGL_LIB=${DRIVERBENCH_EGL_LIB:-libEGL.so.1}
;;
espryt)
export DRIVERBENCH_EGL_LIB=$(readlink -f "$1"); shift
export MOBILEGL_BACKEND_TYPE=DirectGLES
;;
magma)
export DRIVERBENCH_EGL_LIB=$(readlink -f "$1"); shift
export MOBILEGL_BACKEND_TYPE=DirectVulkan
export VK_ICD_FILENAMES=$VK_ICD
;;
*) echo "unknown mode: $MODE (native|espryt|magma)"; exit 1 ;;
esac
exec "$BENCH" "$@"
@@ -1073,6 +1073,14 @@ namespace MobileGL::MG_Impl::GLImpl {
}
MGLOG_D("%s: program = %d, location = %d, byteOffset = %d", __func__, programObject.GetExternalIndex(),
location, offset + byteOffsetInsideUniform);
// Apps re-set identical uniform values constantly (Minecraft re-uploads the same
// matrices and sampler indices every frame), and any content-version move makes both
// backends re-upload the whole UBO on the next draw. Every glUniform entry point
// funnels its final bytes through here - after any transpose/stride conversion, with
// the exact destination range known - and the scratch is zero-filled at link (matching
// the GL zero defaults), so a bytes-equal write can be dropped without moving the
// version.
if (std::memcmp(pUBO + offset + byteOffsetInsideUniform, value, writeSize) == 0) return;
Memcpy(pUBO + offset + byteOffsetInsideUniform, value, writeSize);
programObject.MarkUBOContentDirty();
} else {
+14 -10
View File
@@ -1406,7 +1406,8 @@ namespace MobileGL::MG_Impl::GLImpl {
}
free(processedPixels);
textureMipmapObject->MarkStorageDirty(textureUploadTarget, level, true);
textureMipmapObject->MarkStorageDirtyRegion(textureUploadTarget, level, {xoffset, yoffset, zoffset},
{width, height, depth});
MaybeAutoGenerateMipmap(target, textureObject, false, level);
}
@@ -1525,7 +1526,8 @@ namespace MobileGL::MG_Impl::GLImpl {
free(processedPixels);
MGLOG_D("%s: mark mip %d as dirty", __func__, level);
textureMipmapObject->MarkStorageDirty(textureUploadTarget, level, true);
textureMipmapObject->MarkStorageDirtyRegion(textureUploadTarget, level, {xoffset, yoffset, 0},
{width, height, 1});
MaybeAutoGenerateMipmap(target, textureObject, false, level);
}
@@ -1591,7 +1593,7 @@ namespace MobileGL::MG_Impl::GLImpl {
}
free(processedPixels);
textureMipmapObject->MarkStorageDirty(textureUploadTarget, level, true);
textureMipmapObject->MarkStorageDirtyRegion(textureUploadTarget, level, {xoffset, 0, 0}, {width, 1, 1});
MaybeAutoGenerateMipmap(target, textureObject, false, level);
}
@@ -3536,8 +3538,8 @@ namespace MobileGL::MG_Impl::GLImpl {
if (texture == 0) {
auto& currentUnit = MG_State::pGLContext->GetTextureUnitObject(activeUnit);
auto& bindingSlot = currentUnit.GetBindingSlot(textureTarget);
bindingSlot.Bind(MG_State::pGLContext->GetDefaultTextureObject(textureTarget));
MG_State::pGLContext->NoteTextureUnitTouched(activeUnit);
const Bool changed = bindingSlot.Bind(MG_State::pGLContext->GetDefaultTextureObject(textureTarget));
MG_State::pGLContext->NoteTextureUnitTouched(activeUnit, changed);
return;
}
@@ -3571,8 +3573,8 @@ namespace MobileGL::MG_Impl::GLImpl {
// ======================= Processing ================================
auto& currentUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());
auto& bindingSlot = currentUnit.GetBindingSlot(textureTarget);
bindingSlot.Bind(textureObject);
MG_State::pGLContext->NoteTextureUnitTouched(MG_State::pGLContext->GetActiveTextureUnit());
const Bool changed = bindingSlot.Bind(textureObject);
MG_State::pGLContext->NoteTextureUnitTouched(MG_State::pGLContext->GetActiveTextureUnit(), changed);
}
void ActiveTexture_State(GLenum texture) {
@@ -4410,13 +4412,14 @@ namespace MobileGL::MG_Impl::GLImpl {
}
auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(static_cast<Int>(unit));
MG_State::pGLContext->NoteTextureUnitTouched(static_cast<Int>(unit));
if (texture == 0) {
// GL 4.5 8.1: texture zero unbinds every target of the unit, i.e. rebinds each
// target's default texture object (the unit's initial state).
Bool changed = false;
for (auto& slot : textureUnit.GetAllBindingSlots()) {
slot.Bind(MG_State::pGLContext->GetDefaultTextureObject(slot.GetTarget()));
if (slot.Bind(MG_State::pGLContext->GetDefaultTextureObject(slot.GetTarget()))) changed = true;
}
MG_State::pGLContext->NoteTextureUnitTouched(static_cast<Int>(unit), changed);
return;
}
@@ -4427,7 +4430,8 @@ namespace MobileGL::MG_Impl::GLImpl {
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Texture object does not exist."));
return;
}
textureUnit.GetBindingSlot(textureObject->GetTarget()).Bind(textureObject);
const Bool changed = textureUnit.GetBindingSlot(textureObject->GetTarget()).Bind(textureObject);
MG_State::pGLContext->NoteTextureUnitTouched(static_cast<Int>(unit), changed);
}
void GetTextureImage(GLuint texture, GLint level, GLenum format, GLenum type, GLsizei bufSize, void* pixels) {
@@ -353,7 +353,7 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
return true;
}
Bool ValidateTextureObject(SharedPtr<MG_State::GLState::ITextureObject> textureObject) {
Bool ValidateTextureObject(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject) {
if (!textureObject) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
@@ -376,7 +376,7 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
return true;
}
Bool ValidateTextureTargetUniformity(SharedPtr<MG_State::GLState::ITextureObject> textureObject,
Bool ValidateTextureTargetUniformity(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
TextureTarget target) {
if (!textureObject) return true; // should be created later
TextureTarget prevTarget = textureObject->GetTarget();
@@ -390,7 +390,7 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
return true;
}
Bool ValidateTextureSubImageOffsets(SharedPtr<MG_State::GLState::ITextureObject> textureObject, Int xoffset,
Bool ValidateTextureSubImageOffsets(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject, Int xoffset,
Int width, Int yoffset, Int height, Int zoffset, Int depth) {
auto baseSize = textureObject->GetBaseSize();
if (xoffset < 0 || (xoffset + width) > baseSize.x()) {
+3 -3
View File
@@ -30,15 +30,15 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
TextureInternalFormat internalFormat,
TexturePixelDataType type);
Bool ValidateTextureLevelWithUploadTarget(TextureUploadTarget target, Int level);
Bool ValidateTextureObject(SharedPtr<MG_State::GLState::ITextureObject> textureObject);
Bool ValidateTextureObject(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject);
// Rejects the per-target default texture objects (name 0) with GL_INVALID_OPERATION for entry
// points that require a GenTextures-created texture, e.g. TexStorage* ("An INVALID_OPERATION
// error is generated if zero is bound to target", ARB_texture_storage).
Bool ValidateTextureNotDefault(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
const char* caller);
Bool ValidateTextureTargetUniformity(SharedPtr<MG_State::GLState::ITextureObject> textureObject,
Bool ValidateTextureTargetUniformity(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
TextureTarget target);
Bool ValidateTextureSubImageOffsets(SharedPtr<MG_State::GLState::ITextureObject> textureObject, Int xoffset,
Bool ValidateTextureSubImageOffsets(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject, Int xoffset,
Int width, Int yoffset = 0, Int height = 0, Int zoffset = 0, Int depth = 0);
Bool ValidateBaseInternalFormatMatch(TextureInternalFormat format1, TextureInternalFormat format2);
} // namespace MobileGL::MG_Impl::GLImpl::TextureImpl
@@ -15,4 +15,257 @@ MOBILEGL_GLX_API void* glXGetProcAddress(const char* name) {
MOBILEGL_GLX_API void* glXGetProcAddressARB(const char* name) {
return MG_Impl::GLXImpl::GetProcAddressARB(name);
}
}
#if defined(__linux__) && !defined(__ANDROID__)
#include "../GLXImpl.h"
namespace GLXImpl = MobileGL::MG_Impl::GLXImpl;
// GLX handle/type spellings from GL/glx.h, expressed without including it:
// GLXContext/GLXFBConfig are opaque pointers, drawables are XIDs, Bool is int,
// and XVisualInfo* crosses as void*.
MOBILEGL_GLX_API int glXQueryExtension(Display* dpy, int* errorBase, int* eventBase) {
return GLXImpl::QueryExtension(dpy, errorBase, eventBase);
}
MOBILEGL_GLX_API int glXQueryVersion(Display* dpy, int* major, int* minor) {
return GLXImpl::QueryVersion(dpy, major, minor);
}
MOBILEGL_GLX_API const char* glXQueryExtensionsString(Display* dpy, int screen) {
return GLXImpl::QueryExtensionsString(dpy, screen);
}
MOBILEGL_GLX_API const char* glXGetClientString(Display* dpy, int name) {
return GLXImpl::GetClientString(dpy, name);
}
MOBILEGL_GLX_API const char* glXQueryServerString(Display* dpy, int screen, int name) {
return GLXImpl::QueryServerString(dpy, screen, name);
}
MOBILEGL_GLX_API void** glXGetFBConfigs(Display* dpy, int screen, int* nelements) {
return GLXImpl::GetFBConfigs(dpy, screen, nelements);
}
MOBILEGL_GLX_API void** glXChooseFBConfig(Display* dpy, int screen, const int* attribList,
int* nelements) {
return GLXImpl::ChooseFBConfig(dpy, screen, attribList, nelements);
}
MOBILEGL_GLX_API int glXGetFBConfigAttrib(Display* dpy, void* config, int attribute, int* value) {
return GLXImpl::GetFBConfigAttrib(dpy, config, attribute, value);
}
MOBILEGL_GLX_API void* glXGetVisualFromFBConfig(Display* dpy, void* config) {
return GLXImpl::GetVisualFromFBConfig(dpy, config);
}
MOBILEGL_GLX_API void* glXChooseVisual(Display* dpy, int screen, int* attribList) {
return GLXImpl::ChooseVisual(dpy, screen, attribList);
}
MOBILEGL_GLX_API int glXGetConfig(Display* dpy, void* visualInfo, int attribute, int* value) {
return GLXImpl::GetConfig(dpy, visualInfo, attribute, value);
}
MOBILEGL_GLX_API void* glXCreateContext(Display* dpy, void* visualInfo, void* shareList, int direct) {
return GLXImpl::CreateContext(dpy, visualInfo, shareList, direct);
}
MOBILEGL_GLX_API void* glXCreateNewContext(Display* dpy, void* config, int renderType,
void* shareList, int direct) {
return GLXImpl::CreateNewContext(dpy, config, renderType, shareList, direct);
}
MOBILEGL_GLX_API void* glXCreateContextAttribsARB(Display* dpy, void* config, void* shareContext,
int direct, const int* attribList) {
return GLXImpl::CreateContextAttribsARB(dpy, config, shareContext, direct, attribList);
}
MOBILEGL_GLX_API void glXDestroyContext(Display* dpy, void* context) {
GLXImpl::DestroyContext(dpy, context);
}
MOBILEGL_GLX_API int glXMakeCurrent(Display* dpy, unsigned long drawable, void* context) {
return GLXImpl::MakeCurrent(dpy, drawable, context);
}
MOBILEGL_GLX_API int glXMakeContextCurrent(Display* dpy, unsigned long draw, unsigned long read,
void* context) {
return GLXImpl::MakeContextCurrent(dpy, draw, read, context);
}
MOBILEGL_GLX_API void glXSwapBuffers(Display* dpy, unsigned long drawable) {
GLXImpl::SwapBuffers(dpy, drawable);
}
MOBILEGL_GLX_API unsigned long glXCreateWindow(Display* dpy, void* config, unsigned long window,
const int* attribList) {
return GLXImpl::CreateWindow(dpy, config, window, attribList);
}
MOBILEGL_GLX_API void glXDestroyWindow(Display* dpy, unsigned long window) {
GLXImpl::DestroyWindow(dpy, window);
}
MOBILEGL_GLX_API void* glXGetCurrentContext() {
return GLXImpl::GetCurrentContext();
}
MOBILEGL_GLX_API unsigned long glXGetCurrentDrawable() {
return GLXImpl::GetCurrentDrawable();
}
MOBILEGL_GLX_API unsigned long glXGetCurrentReadDrawable() {
return GLXImpl::GetCurrentReadDrawable();
}
MOBILEGL_GLX_API Display* glXGetCurrentDisplay() {
return GLXImpl::GetCurrentDisplay();
}
MOBILEGL_GLX_API int glXIsDirect(Display* dpy, void* context) {
return GLXImpl::IsDirect(dpy, context);
}
MOBILEGL_GLX_API void glXWaitGL() {
GLXImpl::WaitGL();
}
MOBILEGL_GLX_API void glXWaitX() {
GLXImpl::WaitX();
}
MOBILEGL_GLX_API int glXQueryContext(Display* dpy, void* context, int attribute, int* value) {
return GLXImpl::QueryContext(dpy, context, attribute, value);
}
MOBILEGL_GLX_API void glXQueryDrawable(Display* dpy, unsigned long drawable, int attribute,
unsigned int* value) {
GLXImpl::QueryDrawable(dpy, drawable, attribute, value);
}
MOBILEGL_GLX_API void glXSwapIntervalEXT(Display* dpy, unsigned long drawable, int interval) {
GLXImpl::SwapIntervalEXT(dpy, drawable, interval);
}
MOBILEGL_GLX_API int glXSwapIntervalMESA(unsigned int interval) {
return GLXImpl::SwapIntervalMESA(interval);
}
MOBILEGL_GLX_API int glXGetSwapIntervalMESA() {
return GLXImpl::GetSwapIntervalMESA();
}
MOBILEGL_GLX_API int glXSwapIntervalSGI(int interval) {
return GLXImpl::SwapIntervalSGI(interval);
}
// Legacy entry points some loaders probe for; harmless no-op stubs.
MOBILEGL_GLX_API void glXCopyContext(Display*, void*, void*, unsigned long) {
MGLOG_W("glx: glXCopyContext is not supported");
}
MOBILEGL_GLX_API unsigned long glXCreateGLXPixmap(Display*, void*, unsigned long) {
MGLOG_W("glx: glXCreateGLXPixmap is not supported");
return 0;
}
MOBILEGL_GLX_API void glXDestroyGLXPixmap(Display*, unsigned long) {}
MOBILEGL_GLX_API unsigned long glXCreatePixmap(Display*, void*, unsigned long, const int*) {
MGLOG_W("glx: glXCreatePixmap is not supported");
return 0;
}
MOBILEGL_GLX_API void glXDestroyPixmap(Display*, unsigned long) {}
MOBILEGL_GLX_API unsigned long glXCreatePbuffer(Display*, void*, const int*) {
MGLOG_W("glx: glXCreatePbuffer is not supported");
return 0;
}
MOBILEGL_GLX_API void glXDestroyPbuffer(Display*, unsigned long) {}
MOBILEGL_GLX_API void glXUseXFont(unsigned long, int, int, int) {
MGLOG_W("glx: glXUseXFont is not supported");
}
MOBILEGL_GLX_API void glXSelectEvent(Display*, unsigned long, unsigned long) {}
MOBILEGL_GLX_API void glXGetSelectedEvent(Display*, unsigned long, unsigned long* eventMask) {
if (eventMask) {
*eventMask = 0;
}
}
namespace MobileGL::MG_Impl::GLXImpl {
namespace {
struct GLXEntryPoint {
const char* Name;
void* Proc;
};
const GLXEntryPoint kGLXEntryPoints[] = {
{"glXChooseFBConfig", reinterpret_cast<void*>(glXChooseFBConfig)},
{"glXChooseVisual", reinterpret_cast<void*>(glXChooseVisual)},
{"glXCopyContext", reinterpret_cast<void*>(glXCopyContext)},
{"glXCreateContext", reinterpret_cast<void*>(glXCreateContext)},
{"glXCreateContextAttribsARB", reinterpret_cast<void*>(glXCreateContextAttribsARB)},
{"glXCreateGLXPixmap", reinterpret_cast<void*>(glXCreateGLXPixmap)},
{"glXCreateNewContext", reinterpret_cast<void*>(glXCreateNewContext)},
{"glXCreatePbuffer", reinterpret_cast<void*>(glXCreatePbuffer)},
{"glXCreatePixmap", reinterpret_cast<void*>(glXCreatePixmap)},
{"glXCreateWindow", reinterpret_cast<void*>(glXCreateWindow)},
{"glXDestroyContext", reinterpret_cast<void*>(glXDestroyContext)},
{"glXDestroyGLXPixmap", reinterpret_cast<void*>(glXDestroyGLXPixmap)},
{"glXDestroyPbuffer", reinterpret_cast<void*>(glXDestroyPbuffer)},
{"glXDestroyPixmap", reinterpret_cast<void*>(glXDestroyPixmap)},
{"glXDestroyWindow", reinterpret_cast<void*>(glXDestroyWindow)},
{"glXGetClientString", reinterpret_cast<void*>(glXGetClientString)},
{"glXGetConfig", reinterpret_cast<void*>(glXGetConfig)},
{"glXGetCurrentContext", reinterpret_cast<void*>(glXGetCurrentContext)},
{"glXGetCurrentDisplay", reinterpret_cast<void*>(glXGetCurrentDisplay)},
{"glXGetCurrentDrawable", reinterpret_cast<void*>(glXGetCurrentDrawable)},
{"glXGetCurrentReadDrawable", reinterpret_cast<void*>(glXGetCurrentReadDrawable)},
{"glXGetFBConfigAttrib", reinterpret_cast<void*>(glXGetFBConfigAttrib)},
{"glXGetFBConfigs", reinterpret_cast<void*>(glXGetFBConfigs)},
{"glXGetProcAddress", reinterpret_cast<void*>(glXGetProcAddress)},
{"glXGetProcAddressARB", reinterpret_cast<void*>(glXGetProcAddressARB)},
{"glXGetSelectedEvent", reinterpret_cast<void*>(glXGetSelectedEvent)},
{"glXGetSwapIntervalMESA", reinterpret_cast<void*>(glXGetSwapIntervalMESA)},
{"glXGetVisualFromFBConfig", reinterpret_cast<void*>(glXGetVisualFromFBConfig)},
{"glXIsDirect", reinterpret_cast<void*>(glXIsDirect)},
{"glXMakeContextCurrent", reinterpret_cast<void*>(glXMakeContextCurrent)},
{"glXMakeCurrent", reinterpret_cast<void*>(glXMakeCurrent)},
{"glXQueryContext", reinterpret_cast<void*>(glXQueryContext)},
{"glXQueryDrawable", reinterpret_cast<void*>(glXQueryDrawable)},
{"glXQueryExtension", reinterpret_cast<void*>(glXQueryExtension)},
{"glXQueryExtensionsString", reinterpret_cast<void*>(glXQueryExtensionsString)},
{"glXQueryServerString", reinterpret_cast<void*>(glXQueryServerString)},
{"glXQueryVersion", reinterpret_cast<void*>(glXQueryVersion)},
{"glXSelectEvent", reinterpret_cast<void*>(glXSelectEvent)},
{"glXSwapBuffers", reinterpret_cast<void*>(glXSwapBuffers)},
{"glXSwapIntervalEXT", reinterpret_cast<void*>(glXSwapIntervalEXT)},
{"glXSwapIntervalMESA", reinterpret_cast<void*>(glXSwapIntervalMESA)},
{"glXSwapIntervalSGI", reinterpret_cast<void*>(glXSwapIntervalSGI)},
{"glXUseXFont", reinterpret_cast<void*>(glXUseXFont)},
{"glXWaitGL", reinterpret_cast<void*>(glXWaitGL)},
{"glXWaitX", reinterpret_cast<void*>(glXWaitX)},
};
} // namespace
void* GetGLXEntryPoint(const char* name) {
for (const auto& entry : kGLXEntryPoints) {
if (std::strcmp(entry.Name, name) == 0) {
return entry.Proc;
}
}
return nullptr;
}
} // namespace MobileGL::MG_Impl::GLXImpl
#endif // __linux__ && !__ANDROID__
File diff suppressed because it is too large Load Diff
+69
View File
@@ -0,0 +1,69 @@
// MobileGL - MobileGL/MG_Impl/GLXImpl/GLXImpl.h
// 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
#pragma once
#include <Includes.h>
#if defined(__linux__) && !defined(__ANDROID__)
namespace MobileGL::MG_Impl::GLXImpl {
// GLX layered on MobileGL's own EGL, mirroring WGLImpl/CGLImpl. Handles are
// opaque to callers; XVisualInfo crosses the ABI as void* so this header
// needs no Xlib includes (Includes.h forward-declares Display/XID/Window).
using GLXFBConfigHandle = void*;
using GLXContextHandle = void*;
using GLXDrawableHandle = unsigned long; // XID
int QueryExtension(Display* dpy, int* errorBase, int* eventBase);
int QueryVersion(Display* dpy, int* major, int* minor);
const char* QueryExtensionsString(Display* dpy, int screen);
const char* GetClientString(Display* dpy, int name);
const char* QueryServerString(Display* dpy, int screen, int name);
GLXFBConfigHandle* GetFBConfigs(Display* dpy, int screen, int* nelements);
GLXFBConfigHandle* ChooseFBConfig(Display* dpy, int screen, const int* attribList, int* nelements);
int GetFBConfigAttrib(Display* dpy, GLXFBConfigHandle config, int attribute, int* value);
void* GetVisualFromFBConfig(Display* dpy, GLXFBConfigHandle config);
void* ChooseVisual(Display* dpy, int screen, int* attribList);
int GetConfig(Display* dpy, void* visualInfo, int attribute, int* value);
GLXContextHandle CreateContext(Display* dpy, void* visualInfo, GLXContextHandle share, int direct);
GLXContextHandle CreateNewContext(Display* dpy, GLXFBConfigHandle config, int renderType,
GLXContextHandle share, int direct);
GLXContextHandle CreateContextAttribsARB(Display* dpy, GLXFBConfigHandle config, GLXContextHandle share,
int direct, const int* attribList);
void DestroyContext(Display* dpy, GLXContextHandle context);
int MakeCurrent(Display* dpy, GLXDrawableHandle drawable, GLXContextHandle context);
int MakeContextCurrent(Display* dpy, GLXDrawableHandle draw, GLXDrawableHandle read,
GLXContextHandle context);
void SwapBuffers(Display* dpy, GLXDrawableHandle drawable);
GLXDrawableHandle CreateWindow(Display* dpy, GLXFBConfigHandle config, GLXDrawableHandle window,
const int* attribList);
void DestroyWindow(Display* dpy, GLXDrawableHandle window);
GLXContextHandle GetCurrentContext();
GLXDrawableHandle GetCurrentDrawable();
GLXDrawableHandle GetCurrentReadDrawable();
Display* GetCurrentDisplay();
int IsDirect(Display* dpy, GLXContextHandle context);
void WaitGL();
void WaitX();
int QueryContext(Display* dpy, GLXContextHandle context, int attribute, int* value);
void QueryDrawable(Display* dpy, GLXDrawableHandle drawable, int attribute, unsigned int* value);
void SwapIntervalEXT(Display* dpy, GLXDrawableHandle drawable, int interval);
int SwapIntervalMESA(unsigned int interval);
int GetSwapIntervalMESA();
int SwapIntervalSGI(int interval);
// Name -> exported glX entry point (table lives with the exports).
void* GetGLXEntryPoint(const char* name);
} // namespace MobileGL::MG_Impl::GLXImpl
#endif // __linux__ && !__ANDROID__
+19 -3
View File
@@ -8,11 +8,27 @@
#include "LookUp.h"
namespace MG_Impl::GLXImpl {
// TODO: implement complete GLX functionality
#if defined(__linux__) && !defined(__ANDROID__)
#include "../GLXImpl.h"
#endif
namespace MG_Impl::GLXImpl {
void* GetProcAddress(const char* name) {
if (!name) {
return nullptr;
}
MGLOG_D("glXGetProcAddress(\"%s\")", name);
#if defined(__linux__) && !defined(__ANDROID__)
if (name[0] == 'g' && name[1] == 'l' && name[2] == 'X') {
// glX entry points resolve from the GLX layer's own table; GL/EGL
// names fall through to the shared resolver below.
void* proc = MobileGL::MG_Impl::GLXImpl::GetGLXEntryPoint(name);
if (!proc) {
MGLOG_D("glXGetProcAddress: unknown glX entry point %s", name);
}
return proc;
}
#endif
void* proc = MobileGL::MG_Impl::GetProcAddress(name);
if (!proc) {
MGLOG_W("Failed to get function: %s", (const char*)name);
@@ -25,4 +41,4 @@ namespace MG_Impl::GLXImpl {
void* GetProcAddressARB(const char* name) {
return GetProcAddress(name);
}
} // namespace MG_Impl::GLXImpl
} // namespace MG_Impl::GLXImpl
@@ -41,6 +41,7 @@ namespace MobileGL::MG_State::GLState {
void BufferObject::NotifySubData(SizeT offset, SizeT size) {
++m_changeSerial;
if (size == 0) return;
m_hasDefinedContent = true;
if (g_bufferBackendOps && g_bufferBackendOps->SubData) {
g_bufferBackendOps->SubData(*this, offset, size);
}
@@ -49,12 +50,14 @@ namespace MobileGL::MG_State::GLState {
void BufferObject::NotifyFlushMappedRange(Range1D range, Flags<BufferMappingAccessBit> appAccess) {
++m_changeSerial;
if (range.start >= range.end) return;
m_hasDefinedContent = true;
if (g_bufferBackendOps && g_bufferBackendOps->FlushMappedRange) {
g_bufferBackendOps->FlushMappedRange(*this, range, appAccess);
}
}
void BufferObject::NotifyContentWrite(SizeT offset, SizeT size) {
m_hasDefinedContent = true;
if (m_resource.IsGpuResident()) {
// The write already landed in coherent GPU memory; the backend has no separate
// copy to sync. Only bump the serial so cached transient slices invalidate.
@@ -71,6 +74,9 @@ namespace MobileGL::MG_State::GLState {
if (data && size > 0) {
Memcpy(m_resource.Bytes(), data, size);
}
// A NULL-data respecify (the orphaning idiom) leaves the store undefined;
// record that so backends skip uploading the stale shadow bytes.
m_hasDefinedContent = (data != nullptr) || size == 0;
m_isImmutableStorage = false;
m_storageFlags = 0;
NotifyRespecify();
@@ -89,6 +95,7 @@ namespace MobileGL::MG_State::GLState {
} else if (size > 0) {
Memset(m_resource.Bytes(), 0, size);
}
m_hasDefinedContent = true;
m_isImmutableStorage = true;
m_storageFlags = storageFlags;
NotifyRespecify();
@@ -175,6 +182,7 @@ namespace MobileGL::MG_State::GLState {
}
void BufferObject::MarkGpuWritten() {
m_hasDefinedContent = true;
m_gpuWritePending = true;
}
@@ -332,6 +340,10 @@ namespace MobileGL::MG_State::GLState {
return m_changeSerial;
}
Bool BufferObject::HasDefinedContent() const {
return m_hasDefinedContent;
}
const SharedPtr<BackendBufferResource>& BufferObject::GetBackendResource() const {
return m_resource.Backend();
}
@@ -188,6 +188,11 @@ namespace MobileGL {
// Monotonic counter bumped on every shadow mutation; backends use it to
// validate cached transient slices.
Uint64 GetChangeSerial() const;
// False after a NULL-data (re)specification until the first content
// write: the app's orphaning idiom (glBufferData with nullptr) leaves
// the store undefined, so backends may (re)allocate GPU storage without
// uploading the stale CPU shadow.
Bool HasDefinedContent() const;
const SharedPtr<BackendBufferResource>& GetBackendResource() const;
void SetBackendResource(SharedPtr<BackendBufferResource> resource);
@@ -213,6 +218,8 @@ namespace MobileGL {
Bool m_isImmutableStorage = false;
GLbitfield m_storageFlags = 0;
Uint64 m_changeSerial = 0;
// See HasDefinedContent().
Bool m_hasDefinedContent = true;
// Set by MarkGpuWritten, cleared by SyncGpuWrites once the shadow is refreshed.
Bool m_gpuWritePending = false;
Range1D m_mappedRange;
+3 -1
View File
@@ -111,7 +111,9 @@ namespace MobileGL {
TextureUnit& GetTextureUnitObject(Int unit);
ImageTextureBinding& GetImageTextureBinding(Int unit);
const ImageTextureBinding& GetImageTextureBinding(Int unit) const;
void NoteTextureUnitTouched(Int unit) { m_textureState.NoteUnitTouched(unit); }
void NoteTextureUnitTouched(Int unit, Bool bindingChanged = true) {
m_textureState.NoteUnitTouched(unit, bindingChanged);
}
Int GetMaxTouchedTextureUnit() const { return m_textureState.GetMaxTouchedUnit(); }
// Monotonic counter bumped whenever a texture bind/unbind/delete changes which
// texture is bound at a unit; lets a backend skip re-resolving an unchanged
@@ -27,11 +27,23 @@ namespace MobileGL {
m_texelSizes.reserve(std::bit_ceil(requiredLevelCount));
m_texelSizes.resize(requiredLevelCount);
m_isDirty.resize(requiredLevelCount, false);
m_dirtyRegions.resize(requiredLevelCount);
m_compressedData.resize(requiredLevelCount);
m_compressedFormats.resize(requiredLevelCount, GL_NONE);
}
m_texelSizes[level] = input.texelSize;
// A respecified level invalidates any pending sub-region: its extents were
// measured against the old size. If the level is still flagged dirty the
// pending upload widens to the whole (new) level.
if (level < m_dirtyRegions.size()) {
m_dirtyRegions[level] =
m_isDirty[level]
? MipmapDirtyRegion{IntVec3{0, 0, 0},
IntVec3{input.texelSize.x(), input.texelSize.y(),
std::max(input.texelSize.z(), 1)}}
: MipmapDirtyRegion{};
}
auto& data = m_data[level];
data.resize(input.byteSize, 0);
@@ -81,6 +93,7 @@ namespace MobileGL {
m_data.resize(levelCount);
m_texelSizes.resize(levelCount);
m_isDirty.resize(levelCount);
m_dirtyRegions.resize(levelCount);
m_compressedData.resize(levelCount);
m_compressedFormats.resize(levelCount);
}
@@ -95,7 +108,7 @@ namespace MobileGL {
const Uint8* src = static_cast<const Uint8*>(input.data);
// Clamp so a size mismatch can never write past the allocation.
Memcpy(levelData.data(), src, std::min(input.size, levelData.size()));
m_isDirty[level] = true;
MarkDirty(level, true); // whole-level write: dirty region covers everything
}
}
@@ -120,12 +133,51 @@ namespace MobileGL {
void MipmapStorage::MarkDirty(Uint level, bool dirty) {
MOBILEGL_ASSERT(level < m_isDirty.size(), "MarkDirty: level out of range");
m_isDirty[level] = dirty;
if (level < m_dirtyRegions.size()) {
if (dirty) {
const IntVec3 size = level < m_texelSizes.size() ? m_texelSizes[level] : IntVec3{0, 0, 0};
m_dirtyRegions[level] = {IntVec3{0, 0, 0},
IntVec3{size.x(), size.y(), std::max(size.z(), 1)}};
} else {
m_dirtyRegions[level] = {};
}
}
}
bool MipmapStorage::IsDirty(Uint level) const {
MOBILEGL_ASSERT(level < m_isDirty.size(), "IsDirty: level out of range");
return m_isDirty[level];
}
void MipmapStorage::MarkDirtyRegion(Uint level, IntVec3 offset, IntVec3 size) {
MOBILEGL_ASSERT(level < m_isDirty.size(), "MarkDirtyRegion: level out of range");
const IntVec3 levelSize = level < m_texelSizes.size() ? m_texelSizes[level] : IntVec3{0, 0, 0};
MipmapDirtyRegion incoming;
incoming.lo = {std::max(offset.x(), 0), std::max(offset.y(), 0), std::max(offset.z(), 0)};
incoming.hi = {std::min(offset.x() + size.x(), levelSize.x()),
std::min(offset.y() + size.y(), levelSize.y()),
std::min(offset.z() + std::max(size.z(), 1), std::max(levelSize.z(), 1))};
if (incoming.Empty()) return;
if (level < m_dirtyRegions.size()) {
MipmapDirtyRegion& region = m_dirtyRegions[level];
if (m_isDirty[level] && !region.Empty()) {
region.lo = {std::min(region.lo.x(), incoming.lo.x()),
std::min(region.lo.y(), incoming.lo.y()),
std::min(region.lo.z(), incoming.lo.z())};
region.hi = {std::max(region.hi.x(), incoming.hi.x()),
std::max(region.hi.y(), incoming.hi.y()),
std::max(region.hi.z(), incoming.hi.z())};
} else {
region = incoming;
}
}
m_isDirty[level] = true;
}
MipmapDirtyRegion MipmapStorage::GetDirtyRegion(Uint level) const {
if (level >= m_dirtyRegions.size()) return {};
return m_dirtyRegions[level];
}
} // namespace GLState
} // namespace MG_State
} // namespace MobileGL
@@ -7,6 +7,8 @@
// End of Source File Header
#pragma once
#include <algorithm>
#include "TextureEnum.h"
#include "MG_Util/Types.h"
#include "MG_Util/Math/VectorTypes.h"
@@ -15,6 +17,22 @@
namespace MobileGL {
namespace MG_State {
namespace GLState {
// Texel-space bounding box of the shadow bytes a backend has not uploaded
// yet, [lo, hi) per axis. Cleared (all zero) while the level is clean. A
// box, not a range list: repeated sub-image writes union into one region,
// which stays exact for the per-frame "small sub-rect of a big atlas"
// pattern this exists for, and degrades to the old full-level upload as
// the union grows.
struct MipmapDirtyRegion {
IntVec3 lo{0, 0, 0};
IntVec3 hi{0, 0, 0};
Bool Empty() const { return hi.x() <= lo.x() || hi.y() <= lo.y() || hi.z() <= lo.z(); }
Bool CoversWholeLevel(const IntVec3& levelSize) const {
return lo.x() <= 0 && lo.y() <= 0 && lo.z() <= 0 && hi.x() >= levelSize.x() &&
hi.y() >= levelSize.y() && hi.z() >= std::max(levelSize.z(), 1);
}
};
class MipmapStorage {
public:
SizeT GetLevelCount() const;
@@ -29,6 +47,12 @@ namespace MobileGL {
SizeT GetByteSize(Uint level) const;
void MarkDirty(Uint level, bool dirty);
bool IsDirty(Uint level) const;
// Union a sub-image write's box into the level's pending region and set the
// dirty flag. MarkDirty keeps its meaning: true covers the whole level,
// false clears the region along with the flag.
void MarkDirtyRegion(Uint level, IntVec3 offset, IntVec3 size);
// Meaningful only while IsDirty(level).
MipmapDirtyRegion GetDirtyRegion(Uint level) const;
// The bytes an application handed to glCompressedTexImage*, kept verbatim beside the
// (uncompressed) texel shadow rather than in place of it. GL 4.6 core 8.11 requires
@@ -49,6 +73,7 @@ namespace MobileGL {
Vector<IntVec3> m_texelSizes;
Vector<Vector<Uint8>> m_data;
Vector<bool> m_isDirty;
Vector<MipmapDirtyRegion> m_dirtyRegions;
Vector<Vector<Uint8>> m_compressedData;
Vector<GLenum> m_compressedFormats;
};
@@ -74,6 +74,16 @@ namespace MobileGL {
return m_storage[targetIndex].IsDirty(level);
}
void MarkDirtyRegion(Uint targetIndex, Uint level, IntVec3 offset, IntVec3 size) {
MOBILEGL_ASSERT(targetIndex < TargetCount, "MarkDirtyRegion: target invalid");
m_storage[targetIndex].MarkDirtyRegion(level, offset, size);
}
MipmapDirtyRegion GetDirtyRegion(Uint targetIndex, Uint level) const {
MOBILEGL_ASSERT(targetIndex < TargetCount, "GetDirtyRegion: target invalid");
return m_storage[targetIndex].GetDirtyRegion(level);
}
void SetCompressedImage(Uint targetIndex, Uint level, GLenum internalFormat, const void* data,
SizeT size) {
MOBILEGL_ASSERT(targetIndex < TargetCount, "SetCompressedImage: target invalid");
@@ -16,6 +16,10 @@ namespace MobileGL {
namespace GLState {
static std::atomic<Uint64> s_nextTextureLifetimeId = 1;
// Defined further down next to the other sampling-completeness rules; the
// memo in TextureObjectBase is its only caller.
static Bool ComputeMipmapCompleteForFilter(const ITextureObject* texture, Bool mipmapped);
// TextureObjectBase implementations
Uint64 TextureObjectBase::AllocateLifetimeId() {
return s_nextTextureLifetimeId.fetch_add(1, std::memory_order_relaxed);
@@ -81,6 +85,7 @@ namespace MobileGL {
}
m_internalFormat = format;
++m_shapeVersion;
++m_textureParamsVersion;
}
@@ -192,6 +197,7 @@ namespace MobileGL {
m_levelRange.y() = m_levelRange.x();
}
++m_textureParamsVersion;
++m_shapeVersion;
}
void TextureObjectBase::SetMaxLevel(Uint maxLevel) {
@@ -202,6 +208,7 @@ namespace MobileGL {
m_levelRange.y() = maxLevel;
++m_textureParamsVersion;
++m_shapeVersion;
}
Bool TextureObjectBase::IsImmutable() const {
@@ -231,6 +238,17 @@ namespace MobileGL {
return m_contentVersion;
}
Bool TextureObjectBase::IsMipmapCompleteForFilterCached(Bool mipmapped) const {
const int slot = mipmapped ? 1 : 0;
if (m_completeMemoShapeVersion[slot] == m_shapeVersion) {
return m_completeMemoValue[slot];
}
const Bool value = ComputeMipmapCompleteForFilter(this, mipmapped);
m_completeMemoShapeVersion[slot] = m_shapeVersion;
m_completeMemoValue[slot] = value;
return value;
}
void TextureObjectBase::BumpContentVersion() {
++m_contentVersion;
}
@@ -273,10 +291,12 @@ namespace MobileGL {
void TextureObjectWithOneMipmap::AllocateStorage(TextureUploadTarget uploadTarget, Uint mipmapLevel,
MipmapInput input) {
++m_shapeVersion;
m_textureStorage.AllocateLevel(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, input);
}
void TextureObjectWithOneMipmap::TruncateMipmapLevels(TextureUploadTarget uploadTarget, Uint levelCount) {
++m_shapeVersion;
m_textureStorage.TruncateToLevelCount(GetIndexOfTextureUploadTarget(uploadTarget), levelCount);
}
@@ -301,6 +321,18 @@ namespace MobileGL {
return m_textureStorage.IsDirty(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel);
}
void TextureObjectWithOneMipmap::MarkStorageDirtyRegion(TextureUploadTarget uploadTarget, Uint mipmapLevel,
IntVec3 offset, IntVec3 size) {
++m_contentVersion;
m_textureStorage.MarkDirtyRegion(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, offset,
size);
}
MipmapDirtyRegion TextureObjectWithOneMipmap::GetStorageDirtyRegion(TextureUploadTarget uploadTarget,
Uint mipmapLevel) const {
return m_textureStorage.GetDirtyRegion(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel);
}
void TextureObjectWithOneMipmap::SetMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel,
GLenum internalFormat, const void* data, SizeT size) {
m_textureStorage.SetCompressedImage(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel,
@@ -361,13 +393,18 @@ namespace MobileGL {
// TODO: add other texture types as needed
Bool IsMipmapCompleteForFilter(const ITextureObject* texture, Bool mipmapped) {
if (texture == nullptr) return true;
return texture->IsMipmapCompleteForFilterCached(mipmapped);
}
Bool SamplesAsIncompleteTexture(const ITextureObject* texture, const SamplerObject* effectiveSampler) {
const Bool mipmapped =
effectiveSampler != nullptr && effectiveSampler->GetMipmapMode() != SamplerMipmapMode::None;
return !IsMipmapCompleteForFilter(texture, mipmapped);
}
Bool IsMipmapCompleteForFilter(const ITextureObject* texture, Bool mipmapped) {
static Bool ComputeMipmapCompleteForFilter(const ITextureObject* texture, Bool mipmapped) {
if (texture == nullptr) return true;
if (!texture->IsComplete()) return false;
if (!mipmapped) return true;
@@ -55,6 +55,13 @@ namespace MobileGL::MG_State::GLState {
// Backends compare it against a per-resource snapshot to skip re-syncing unchanged
// textures across draws (e.g. the block atlas bound across a whole terrain batch).
virtual Uint64 GetContentVersion() const = 0;
// Answers IsMipmapCompleteForFilter() from a memo. Sampling completeness is a
// property of the texture's SHAPE - level sizes, level count, level range,
// internal format - and never of its texel content, but every draw asks about
// every bound texture, which made recomputing it one of the hottest things both
// backends did (the walk plus its GetTexelSize calls measured ~8% of the render
// thread). Shape mutations invalidate the memo; uploads do not.
virtual Bool IsMipmapCompleteForFilterCached(Bool mipmapped) const = 0;
virtual Int GetSamples() const = 0;
virtual void SetSamples(Int samples) = 0;
virtual Bool HasFixedSampleLocations() const = 0;
@@ -99,6 +106,7 @@ namespace MobileGL::MG_State::GLState {
void SetImmutableLevels(Uint levels) override;
Uint16 GetTextureParamsVersion() const override;
Uint64 GetContentVersion() const override;
Bool IsMipmapCompleteForFilterCached(Bool mipmapped) const override;
// Bumps the content version without touching per-level storage-dirty flags. Used when the
// set of defined mip levels grows via GPU-side mip generation (glGenerateMipmap): the level
// set changed (so a cached sampled view's level range is stale) but no CPU data is dirty.
@@ -124,6 +132,14 @@ namespace MobileGL::MG_State::GLState {
UintVec2 m_levelRange = {0, 1000};
Uint m_immutableLevels = 0;
Uint16 m_textureParamsVersion = 0;
// Bumped by every mutation the completeness answer depends on - internal
// format, level range, and the stored level set - and by nothing else, so a
// texel upload leaves the memo below valid.
Uint64 m_shapeVersion = 1;
// [0] = the non-mipmapped answer, [1] = the mipmapped one. Mutable because
// completeness is a query; a zero version means "never computed".
mutable Uint64 m_completeMemoShapeVersion[2] = {0, 0};
mutable Bool m_completeMemoValue[2] = {false, false};
// Starts at 1 so a freshly-created backend resource (snapshot 0) never spuriously
// matches before its first sync. Bumped only on dirty=true in MarkStorageDirty.
Uint64 m_contentVersion = 1;
@@ -150,6 +166,20 @@ namespace MobileGL::MG_State::GLState {
virtual void* MapMipmapData(TextureUploadTarget uploadTarget, Uint mipmapLevel) = 0;
virtual void MarkStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel, Bool dirty = true) = 0;
virtual Bool IsStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel) const = 0;
// Sub-image variant of MarkStorageDirty(..., true): backends may then upload
// only the accumulated region instead of the whole level. The base fallback
// keeps whole-level semantics for storage classes that do not track regions.
virtual void MarkStorageDirtyRegion(TextureUploadTarget uploadTarget, Uint mipmapLevel, IntVec3 offset,
IntVec3 size) {
(void)offset;
(void)size;
MarkStorageDirty(uploadTarget, mipmapLevel, true);
}
// Meaningful only while IsStorageDirty(uploadTarget, mipmapLevel).
virtual MipmapDirtyRegion GetStorageDirtyRegion(TextureUploadTarget uploadTarget, Uint mipmapLevel) const {
const IntVec3 size = GetMipmapTexelSize(uploadTarget, mipmapLevel);
return {IntVec3{0, 0, 0}, IntVec3{size.x(), size.y(), std::max(size.z(), 1)}};
}
// The compressed image a glCompressedTexImage* call shadowed for this level, kept verbatim
// next to the texel data rather than instead of it - see MipmapStorage. The texel shadow
@@ -218,6 +248,9 @@ namespace MobileGL::MG_State::GLState {
void* MapMipmapData(TextureUploadTarget uploadTarget, Uint mipmapLevel) override;
void MarkStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel, Bool dirty) override;
bool IsStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
void MarkStorageDirtyRegion(TextureUploadTarget uploadTarget, Uint mipmapLevel, IntVec3 offset,
IntVec3 size) override;
MipmapDirtyRegion GetStorageDirtyRegion(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
void SetMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel, GLenum internalFormat,
const void* data, SizeT size) override;
GLenum GetMipmapCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
@@ -28,10 +28,12 @@ namespace MobileGL {
void TextureObject2DCube::AllocateStorage(TextureUploadTarget uploadTarget, Uint mipmapLevel,
MipmapInput input) {
++m_shapeVersion;
m_textureStorage.AllocateLevel(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, input);
}
void TextureObject2DCube::TruncateMipmapLevels(TextureUploadTarget uploadTarget, Uint levelCount) {
++m_shapeVersion;
m_textureStorage.TruncateToLevelCount(GetIndexOfTextureUploadTarget(uploadTarget), levelCount);
}
@@ -55,6 +57,18 @@ namespace MobileGL {
return m_textureStorage.IsDirty(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel);
}
void TextureObject2DCube::MarkStorageDirtyRegion(TextureUploadTarget uploadTarget, Uint mipmapLevel,
IntVec3 offset, IntVec3 size) {
++m_contentVersion;
m_textureStorage.MarkDirtyRegion(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, offset,
size);
}
MipmapDirtyRegion TextureObject2DCube::GetStorageDirtyRegion(TextureUploadTarget uploadTarget,
Uint mipmapLevel) const {
return m_textureStorage.GetDirtyRegion(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel);
}
void TextureObject2DCube::SetMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel,
GLenum internalFormat, const void* data, SizeT size) {
m_textureStorage.SetCompressedImage(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel,
@@ -27,6 +27,10 @@ namespace MobileGL {
void* MapMipmapData(TextureUploadTarget uploadTarget, Uint mipmapLevel) override;
void MarkStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel, bool dirty) override;
bool IsStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
void MarkStorageDirtyRegion(TextureUploadTarget uploadTarget, Uint mipmapLevel, IntVec3 offset,
IntVec3 size) override;
MipmapDirtyRegion GetStorageDirtyRegion(TextureUploadTarget uploadTarget,
Uint mipmapLevel) const override;
void SetMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel,
GLenum internalFormat, const void* data, SizeT size) override;
GLenum GetMipmapCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
@@ -67,7 +67,7 @@ namespace MobileGL::MG_State::GLState {
// High-water mark of texture units ever touched by a texture or sampler bind.
// Units above it have provably-empty binding slots, so per-draw backend scans
// can stop there instead of walking all MAX_TEXTURE_IMAGE_UNITS units.
void NoteUnitTouched(Int unit) {
void NoteUnitTouched(Int unit, Bool bindingChanged = true) {
if (unit > m_maxTouchedUnit && unit < MAX_TEXTURE_IMAGE_UNITS) m_maxTouchedUnit = unit;
// Every texture/sampler bind entry point (glBindTexture / glBindTextureUnit /
// glBindTextures / glBindSampler) routes through here, so bumping the generation here
@@ -75,7 +75,10 @@ namespace MobileGL::MG_State::GLState {
// which texture is bound at which unit. A backend that has cached the per-draw
// sampled-texture set can compare this against a snapshot to skip re-resolving it when
// no bind changed (the block atlas + lightmap stay bound across a whole terrain batch).
++m_textureBindGeneration;
// Re-binding the object a slot already holds changes nothing that the generation
// guards; such callers pass bindingChanged=false so only the high-water mark advances
// and the backend fast path survives the redundant re-binds apps issue every frame.
if (bindingChanged) ++m_textureBindGeneration;
}
Int GetMaxTouchedUnit() const { return m_maxTouchedUnit; }
Uint64 GetTextureBindGeneration() const { return m_textureBindGeneration; }
@@ -34,7 +34,11 @@ namespace MobileGL::MG_State::GLState {
}
void VertexArrayState::Bind(Uint index) {
m_boundVertexArray = GetVertexArrayObject(index);
const auto& vertexArray = GetVertexArrayObject(index);
// Re-binding the already-current VAO is a per-batch habit of Blaze3D-style renderers;
// skip the shared_ptr store (two atomic refcount ops) when nothing changes.
if (vertexArray == m_boundVertexArray) return;
m_boundVertexArray = vertexArray;
}
const SharedPtr<VertexArrayObject>& VertexArrayState::CreateVertexArrayObject(Uint index) {
@@ -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__
+16
View File
@@ -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;
+5 -2
View File
@@ -156,11 +156,14 @@ namespace MobileGL {
BindingSlot() : m_target((TargetEnum)0) {}
explicit BindingSlot(TargetEnum target) : m_target(target) {}
void Bind(SharedPtr<ObjectType> object) {
if (m_boundObject == object) return;
// Reports whether the binding actually changed, so callers can keep change-driven
// bookkeeping (e.g. the texture bind generation) quiet on redundant re-binds.
Bool Bind(SharedPtr<ObjectType> object) {
if (m_boundObject == object) return false;
m_boundObject = Move(object);
++m_version;
return true;
}
SharedPtr<ObjectType> const& GetBoundObject() const noexcept { return m_boundObject; }
TargetEnum GetTarget() const { return m_target; }
+8
View File
@@ -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;