mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 20:28:32 +09:00
[Test] (MG_Benchmark, MG_Util, MG_Backend, android-plugin): run the driver benchmark on a phone
The Minecraft-shaped driver benchmark could only be run from a desktop shell against a desktop driver, which is the wrong machine: MobileGL exists to run on mobile GPUs, and nothing said what its translation costs there. This puts the same cases on an Android device, both in the plugin's POST screen and from a shell, and adds the native-driver baseline they have to be read against. The cases move into DriverBenchCases.inc so both harnesses run byte-identical bodies - the desktop program resolving entry points from one EGL provider, and DriverBenchJni.cpp calling MobileGL's frontend in-process. The JNI file binds every gl*/egl* name to MG_Impl by macro rather than by linkage: this library legitimately has the platform libEGL and libGLESv3 in its own lookup scope, and a benchmark that quietly measured the device driver instead of the translation layer would have looked like very good news. Frames are now closed with a fence wait instead of glFinish. MobileGL implements glFinish and glFlush as no-ops, so the old loop timed submit-plus-GPU on a native driver and submit-only on a MobileGL backend, and the two numbers did not describe the same work. To measure a device's own driver the cases needed to be expressible in GLES: ESSL 3.20 twins of the four shaders (chosen at runtime from GL_VERSION, since MobileGL is deliberately still fed desktop GLSL - translating it is the thing under test), a multi-draw hook that loops DrawElementsBaseVertex where the multi-draw entry point does not exist, and an EGL bootstrap that falls back from desktop GL to GLES 3. The binary cross-compiles for arm64 unchanged. BenchService hosts each run in its own process and exits afterwards. That is not caution: the backend is latched from MOBILEGL_BACKEND_TYPE at initialization, so Espryt and Magma can never share a process, and Espryt's teardown terminates the process-default EGL display, which would take the POST activity's own EGL objects with it. Running it found that Magma could not create a windowless context on Mali at all - CreateInstance required VK_EXT_headless_surface, which no mobile driver here exposes, and aborted the process. The Xlib path already probes and falls back to a hidden window for the same reason on NVIDIA; Android now probes too and hands the WSI an AImageReader's ANativeWindow, a real producer surface attached to no display whose images are never acquired. DriverPost reports the extension's absence as a WARN so the fallback is visible rather than silent. Measured on a Mali-G77 MC9 (native / Espryt / Magma, ns per operation): 5495 chunk draws 14397 / 36934 / 33763, the 26.2 per-draw uniform-range pattern 13710 / 31205 / 21252, sodium-style multi-draw 256956 / 238389 / 209527. The translation costs about 2.4x per draw here against 5-9x on the desktop, because the mobile driver's own per-call cost dwarfs it - and both backends beat the native driver on multi-draw, which it has to emulate. Desktop unit tests 421/421; the POST screen and both Run Bench buttons verified on the device.
This commit is contained in:
@@ -22,6 +22,15 @@
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.MobileGLPlugin.NoDisplay" />
|
||||
|
||||
<!-- One benchmark run per process: the backend is latched at MobileGL
|
||||
init and Espryt teardown kills the process-default EGL display, so
|
||||
the bench must never share a process with the POST UI. The service
|
||||
exits after each run. -->
|
||||
<service
|
||||
android:name=".BenchService"
|
||||
android:exported="false"
|
||||
android:process=":bench" />
|
||||
|
||||
<meta-data
|
||||
android:name="fclPlugin_V2"
|
||||
android:resource="@string/config" />
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
package top.mobilegl.plugin;
|
||||
|
||||
import android.app.Service;
|
||||
import android.content.Intent;
|
||||
import android.os.IBinder;
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* Hosts one driver-benchmark run in its own process ({@code android:process=":bench"})
|
||||
* and exits when done.
|
||||
*
|
||||
* The isolation is load-bearing, not defensive: MobileGL latches its backend from
|
||||
* {@code MOBILEGL_BACKEND_TYPE} on first initialization, so Espryt and Magma can never
|
||||
* share a process, and Espryt's teardown terminates the process-default EGL display,
|
||||
* which would take the POST activity's HWUI context down with it. A fresh process per
|
||||
* tap sidesteps both, and {@link System#exit} at the end guarantees the next tap gets
|
||||
* one.
|
||||
*/
|
||||
public final class BenchService extends Service {
|
||||
private static final String TAG = "MobileGLBench";
|
||||
|
||||
public static final String ACTION_RESULT = "top.mobilegl.plugin.BENCH_RESULT";
|
||||
public static final String EXTRA_BACKEND = "backend";
|
||||
public static final String EXTRA_RESULT_JSON = "resultJson";
|
||||
public static final String EXTRA_FRAMES = "frames";
|
||||
public static final String EXTRA_WARMUP = "warmupFrames";
|
||||
|
||||
private static native String nativeRunDriverBench(String backendType, int frames, int warmupFrames);
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
final String backend = intent != null ? intent.getStringExtra(EXTRA_BACKEND) : null;
|
||||
final int frames = intent != null ? intent.getIntExtra(EXTRA_FRAMES, 120) : 120;
|
||||
final int warmup = intent != null ? intent.getIntExtra(EXTRA_WARMUP, 30) : 30;
|
||||
if (backend == null) {
|
||||
stopSelf();
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
new Thread(() -> {
|
||||
String result;
|
||||
try {
|
||||
System.loadLibrary("MobileGL");
|
||||
result = nativeRunDriverBench(backend, frames, warmup);
|
||||
} catch (Throwable t) {
|
||||
Log.e(TAG, "bench failed", t);
|
||||
result = "{\"error\":\"" + t.getClass().getSimpleName() + ": "
|
||||
+ String.valueOf(t.getMessage()).replace("\\", "\\\\").replace("\"", "\\\"")
|
||||
+ "\"}";
|
||||
}
|
||||
|
||||
Intent reply = new Intent(ACTION_RESULT);
|
||||
reply.setPackage(getPackageName());
|
||||
reply.putExtra(EXTRA_BACKEND, backend);
|
||||
reply.putExtra(EXTRA_RESULT_JSON, result);
|
||||
sendBroadcast(reply);
|
||||
Log.i(TAG, "bench done for " + backend + " (" + result.length() + " bytes)");
|
||||
|
||||
// The broadcast is already handed to system_server; give binder a
|
||||
// moment, then take the whole process down so the next run starts
|
||||
// from an uninitialized MobileGL.
|
||||
try {
|
||||
Thread.sleep(250);
|
||||
} catch (InterruptedException ignored) {
|
||||
}
|
||||
stopSelf();
|
||||
System.exit(0);
|
||||
}, "MobileGLDriverBench").start();
|
||||
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,26 @@
|
||||
package top.mobilegl.plugin;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.graphics.Typeface;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.util.TypedValue;
|
||||
import android.view.Gravity;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.HorizontalScrollView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ScrollView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
@@ -61,6 +70,16 @@ public final class PostActivity extends Activity {
|
||||
private LinearLayout contentLayout;
|
||||
private TextView statusView;
|
||||
|
||||
/**
|
||||
* Per-backend bench UI state. Unlike the POST itself (single-flight, latched),
|
||||
* a bench may be re-run freely: every tap starts a fresh {@link BenchService}
|
||||
* process, so the only state to manage here is the button and the results
|
||||
* container the reply renders into.
|
||||
*/
|
||||
private final Map<String, Button> benchButtons = new HashMap<>();
|
||||
private final Map<String, LinearLayout> benchResultContainers = new HashMap<>();
|
||||
private BroadcastReceiver benchReceiver;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
@@ -108,9 +127,100 @@ public final class PostActivity extends Activity {
|
||||
deliveryTarget = new WeakReference<>(null);
|
||||
}
|
||||
}
|
||||
if (benchReceiver != null) {
|
||||
unregisterReceiver(benchReceiver);
|
||||
benchReceiver = null;
|
||||
}
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
/** Registered lazily on the first Run Bench tap; delivers results to the UI thread. */
|
||||
private void ensureBenchReceiver() {
|
||||
if (benchReceiver != null) {
|
||||
return;
|
||||
}
|
||||
benchReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
String backend = intent.getStringExtra(BenchService.EXTRA_BACKEND);
|
||||
String json = intent.getStringExtra(BenchService.EXTRA_RESULT_JSON);
|
||||
if (backend != null && json != null) {
|
||||
renderBenchResult(backend, json);
|
||||
}
|
||||
}
|
||||
};
|
||||
IntentFilter filter = new IntentFilter(BenchService.ACTION_RESULT);
|
||||
if (Build.VERSION.SDK_INT >= 33) {
|
||||
registerReceiver(benchReceiver, filter, Context.RECEIVER_NOT_EXPORTED);
|
||||
} else {
|
||||
registerReceiver(benchReceiver, filter);
|
||||
}
|
||||
}
|
||||
|
||||
/** Kicks one bench run for a backend in its own service process. */
|
||||
private void startBench(String backendType) {
|
||||
ensureBenchReceiver();
|
||||
Button button = benchButtons.get(backendType);
|
||||
if (button != null) {
|
||||
button.setEnabled(false);
|
||||
button.setText("Bench running... (can take a minute)");
|
||||
}
|
||||
LinearLayout container = benchResultContainers.get(backendType);
|
||||
if (container != null) {
|
||||
container.removeAllViews();
|
||||
}
|
||||
Intent intent = new Intent(this, BenchService.class);
|
||||
intent.putExtra(BenchService.EXTRA_BACKEND, backendType);
|
||||
intent.putExtra(BenchService.EXTRA_FRAMES, 120);
|
||||
intent.putExtra(BenchService.EXTRA_WARMUP, 30);
|
||||
startService(intent);
|
||||
}
|
||||
|
||||
private void renderBenchResult(String backendType, String json) {
|
||||
Button button = benchButtons.get(backendType);
|
||||
if (button != null) {
|
||||
button.setEnabled(true);
|
||||
button.setText("Run Bench");
|
||||
}
|
||||
LinearLayout container = benchResultContainers.get(backendType);
|
||||
if (container == null) {
|
||||
return;
|
||||
}
|
||||
container.removeAllViews();
|
||||
try {
|
||||
JSONObject root = new JSONObject(json);
|
||||
String error = root.optString("error", "");
|
||||
if (!error.isEmpty()) {
|
||||
container.addView(makeText("Bench failed: " + error, 12, COLOR_FAIL, false));
|
||||
return;
|
||||
}
|
||||
container.addView(makeText(root.optString("renderer", ""), 11, COLOR_INFO, false));
|
||||
String header = String.format(Locale.ROOT, "%-22s %10s %10s %9s",
|
||||
"case", "ns/op", "frame ms", "fps");
|
||||
container.addView(makeText(header, 11, COLOR_DETAIL, true));
|
||||
JSONArray cases = root.optJSONArray("cases");
|
||||
if (cases == null) {
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < cases.length(); ++i) {
|
||||
JSONObject row = cases.optJSONObject(i);
|
||||
if (row == null) {
|
||||
continue;
|
||||
}
|
||||
String line = String.format(Locale.ROOT, "%-22s %10.1f %10.3f %9.1f",
|
||||
row.optString("case", "?"),
|
||||
row.optDouble("nsPerOp", 0),
|
||||
row.optDouble("medianFrameMs", 0),
|
||||
row.optDouble("fps", 0));
|
||||
TextView view = makeText(line, 11,
|
||||
row.optInt("glError", 0) != 0 ? COLOR_WARN : COLOR_TEXT, false);
|
||||
container.addView(view);
|
||||
}
|
||||
} catch (JSONException error) {
|
||||
container.addView(makeText("Bench result unparsable: " + error, 12, COLOR_FAIL, false));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads libMobileGL.so on first use, off the UI thread, so the first frame
|
||||
* renders immediately. Any failure (including errors thrown by static
|
||||
@@ -253,6 +363,8 @@ public final class PostActivity extends Activity {
|
||||
addText(renderer, 12, COLOR_INFO, false, dp(2));
|
||||
}
|
||||
|
||||
addBenchControls(name);
|
||||
|
||||
JSONArray checks = backend.optJSONArray("checks");
|
||||
if (checks != null) {
|
||||
LinearLayout table = new LinearLayout(this);
|
||||
@@ -276,6 +388,54 @@ public final class PostActivity extends Activity {
|
||||
renderFormatCapabilities(backend.optJSONObject("formatCapabilities"));
|
||||
}
|
||||
|
||||
/** The MOBILEGL_BACKEND_TYPE value a POST section name stands for, or null. */
|
||||
private static String backendTypeForSection(String sectionName) {
|
||||
switch (sectionName.toLowerCase(Locale.ROOT)) {
|
||||
case "gles":
|
||||
case "directgles":
|
||||
return "DirectGLES";
|
||||
case "vulkan":
|
||||
case "directvulkan":
|
||||
return "DirectVulkan";
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* One Run Bench button plus the container its results render into. The bench
|
||||
* runs the Minecraft-shaped MG_Benchmark cases through the full MobileGL stack
|
||||
* on this backend, in a throwaway service process (see BenchService).
|
||||
*/
|
||||
private void addBenchControls(String sectionName) {
|
||||
final String backendType = backendTypeForSection(sectionName);
|
||||
if (backendType == null) {
|
||||
return;
|
||||
}
|
||||
Button button = new Button(this);
|
||||
button.setText("Run Bench");
|
||||
button.setAllCaps(false);
|
||||
button.setTextSize(TypedValue.COMPLEX_UNIT_SP, 13);
|
||||
button.setOnClickListener(v -> startBench(backendType));
|
||||
LinearLayout.LayoutParams buttonParams = new LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
);
|
||||
buttonParams.topMargin = dp(6);
|
||||
contentLayout.addView(button, buttonParams);
|
||||
benchButtons.put(backendType, button);
|
||||
|
||||
LinearLayout results = new LinearLayout(this);
|
||||
results.setOrientation(LinearLayout.VERTICAL);
|
||||
LinearLayout.LayoutParams resultParams = new LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
);
|
||||
resultParams.topMargin = dp(4);
|
||||
contentLayout.addView(results, resultParams);
|
||||
benchResultContainers.put(backendType, results);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds one two-column check row (name | status chip) to the table. Rows with
|
||||
* a non-empty detail get a collapse indicator and toggle the detail text on tap;
|
||||
|
||||
Reference in New Issue
Block a user