[Feat] (TraceReplay): benchmark mode with per-frame timing for device fixtures

This commit is contained in:
2026-08-26 02:17:35 -04:00
parent 386bd7e461
commit c1d6a3c908
14 changed files with 669 additions and 46 deletions
@@ -218,7 +218,8 @@ add_library(glretrace_common STATIC
"${APITRACE_ROOT}/retrace/metric_backend_opengl.cpp" "${APITRACE_ROOT}/retrace/metric_backend_opengl.cpp"
"${APITRACE_ROOT}/retrace/metric_helper.cpp" "${APITRACE_ROOT}/retrace/metric_helper.cpp"
"${APITRACE_ROOT}/retrace/metric_writer.cpp" "${APITRACE_ROOT}/retrace/metric_writer.cpp"
apitrace_glws_android.cpp) apitrace_glws_android.cpp
trace_benchmark.cpp)
target_include_directories(glretrace_common PUBLIC target_include_directories(glretrace_common PUBLIC
"${APITRACE_GENERATED_DIR}" "${APITRACE_GENERATED_DIR}"
"${APITRACE_ROOT}/dispatch" "${APITRACE_ROOT}/dispatch"
@@ -1,6 +1,7 @@
#include "apitrace_fbo_dump.hpp" #include "apitrace_fbo_dump.hpp"
#include "glws.hpp" #include "glws.hpp"
#include "retrace.hpp" #include "retrace.hpp"
#include "trace_benchmark.hpp"
#include <android/native_window.h> #include <android/native_window.h>
#include <EGL/egl.h> #include <EGL/egl.h>
@@ -186,6 +187,9 @@ public:
char callNo[32]; char callNo[32];
snprintf(callNo, sizeof(callNo), "%u", retrace::callNo); snprintf(callNo, sizeof(callNo), "%u", retrace::callNo);
gEgl.swapBuffers(gDisplay, surface); gEgl.swapBuffers(gDisplay, surface);
// Frame boundary: retrace_eglSwapBuffers() has already run frame_complete() and
// handed the frame to us. No-op unless benchmark mode armed the timer.
mobilegl_trace::benchmark::OnFrameBoundary();
HoldAfterTargetPresent(callNo); HoldAfterTargetPresent(callNo);
} }
} }
@@ -0,0 +1,94 @@
#include "trace_benchmark.hpp"
#include <chrono>
#include <cstdlib>
#include <utility>
#include <dlfcn.h>
namespace mobilegl_trace {
namespace benchmark {
namespace {
using Clock = std::chrono::steady_clock;
using GlFinishFn = void (*)();
constexpr std::size_t kFrameReserve = 4096;
bool gEnabled = false;
bool gFinishEachFrame = false;
bool gResolvedGlFinish = false;
GlFinishFn gGlFinish = nullptr;
Clock::time_point gStart;
Clock::time_point gLastBoundary;
std::vector<double> gFrameMs;
// Same resolution order the glws layers use for MobileGL's entry points: the replay driver
// already dlopen()ed the library with RTLD_GLOBAL before retrace started, so RTLD_NOLOAD
// finds that handle instead of loading a second copy, and RTLD_DEFAULT is the fallback for
// the case where it was linked in rather than dlopen()ed.
GlFinishFn ResolveGlFinish() {
void *handle = nullptr;
const char *library = std::getenv("MOBILEGL_TRACE_LIBRARY");
if (library != nullptr && library[0] != '\0') {
handle = dlopen(library, RTLD_NOW | RTLD_GLOBAL | RTLD_NOLOAD);
}
if (handle == nullptr) {
handle = dlopen("libMobileGL.so", RTLD_NOW | RTLD_GLOBAL | RTLD_NOLOAD);
}
if (handle != nullptr) {
void *symbol = dlsym(handle, "glFinish");
if (symbol != nullptr) {
return reinterpret_cast<GlFinishFn>(symbol);
}
}
return reinterpret_cast<GlFinishFn>(dlsym(RTLD_DEFAULT, "glFinish"));
}
} // namespace
void Begin(bool finishEachFrame) {
gFrameMs.clear();
gFrameMs.reserve(kFrameReserve);
gFinishEachFrame = finishEachFrame;
gResolvedGlFinish = false;
gGlFinish = nullptr;
gStart = Clock::now();
gLastBoundary = gStart;
gEnabled = true;
}
void OnFrameBoundary() {
if (!gEnabled) {
return;
}
if (gFinishEachFrame) {
// Resolved on the first boundary rather than in Begin(): a context only exists once
// the trace has created one, and glFinish before that would be pointless anyway.
if (!gResolvedGlFinish) {
gGlFinish = ResolveGlFinish();
gResolvedGlFinish = true;
}
if (gGlFinish != nullptr) {
gGlFinish();
}
}
const Clock::time_point now = Clock::now();
gFrameMs.push_back(std::chrono::duration<double, std::milli>(now - gLastBoundary).count());
gLastBoundary = now;
}
Report End() {
Report report;
if (!gEnabled) {
return report;
}
gEnabled = false;
gFinishEachFrame = false;
report.totalSeconds = std::chrono::duration<double>(Clock::now() - gStart).count();
report.frameMs = std::move(gFrameMs);
gFrameMs.clear();
return report;
}
} // namespace benchmark
} // namespace mobilegl_trace
@@ -0,0 +1,45 @@
#pragma once
#include <vector>
namespace mobilegl_trace {
namespace benchmark {
// Per-frame wall-clock timing for the retrace loop, shared by the Android replay runner
// and the desktop CLI. Disarmed unless Begin() armed it, and the frame-boundary hook is a
// single bool test in that case, so the correctness harness pays nothing for it.
//
// Retrace runs --singlethread, so all of this is deliberately plain globals: Begin(),
// OnFrameBoundary() and End() are only ever reached from the one retrace thread.
// Arms timing for the retrace that is about to run.
//
// finishEachFrame issues a full glFinish through the replayed context at every frame
// boundary, so a recorded frame time covers GPU completion and not just CPU submission.
// That matters on tiled mobile GPUs, where a swap without a sync returns long before the
// tiler is done and the numbers degenerate into "how fast can we feed the driver". The
// price is that finishing every frame serializes CPU/GPU overlap, so the absolute frame
// times are pessimistic against a real running game - they are deterministic and
// comparable between backends and revisions, which is what a benchmark fixture is for.
// With finishEachFrame off the run measures CPU-side submission only.
void Begin(bool finishEachFrame);
// Frame-boundary hook. Called from the platform glws swapBuffers override, which is where
// apitrace's replay loop advances the frame: retrace_eglSwapBuffers() calls
// frame_complete() and then Drawable::swapBuffers().
void OnFrameBoundary();
struct Report {
// Wall time of every completed frame, in milliseconds.
std::vector<double> frameMs;
// Begin() to End(), in seconds. Covers trace parsing and the leading partial frame too,
// which is why it is reported next to the per-frame statistics rather than derived from
// them.
double totalSeconds = 0.0;
};
// Disarms timing and hands back what was recorded.
Report End();
} // namespace benchmark
} // namespace mobilegl_trace
@@ -3,10 +3,13 @@
#include <dlfcn.h> #include <dlfcn.h>
#include "apitrace_exit.hpp" #include "apitrace_exit.hpp"
#include "png.h" #include "png.h"
#include "trace_benchmark.hpp"
#include <algorithm> #include <algorithm>
#include <cerrno> #include <cerrno>
#include <chrono> #include <chrono>
#include <cmath>
#include <cstddef>
#include <cstdint> #include <cstdint>
#include <cstdio> #include <cstdio>
#include <cstdlib> #include <cstdlib>
@@ -421,32 +424,30 @@ std::string SnapshotCallSet(const Request& request) {
} }
int RunRetraceMain(const Request& request) { int RunRetraceMain(const Request& request) {
std::string prefix = request.outputDir + "/actual."; std::vector<std::string> args;
std::string callSet = SnapshotCallSet(request); args.emplace_back("mobilegl-glretrace");
args.emplace_back("-b");
args.emplace_back("--singlethread");
args.emplace_back("--no-context-check");
if (!request.benchmark) {
// The snapshot callset is also what stops the replay: apitrace exits once it has
// dumped the last call in -S. Benchmark mode wants the whole trace, so the -s/-S
// pair is left off entirely, which drops the readback and the PNG encode with it.
args.emplace_back("--snapshot-alpha");
args.emplace_back("-s");
args.emplace_back(request.outputDir + "/actual.");
args.emplace_back("-S");
args.emplace_back(SnapshotCallSet(request));
}
args.emplace_back(request.tracePath);
std::string arg0 = "mobilegl-glretrace"; std::vector<char*> argv;
std::string argBenchmark = "-b"; argv.reserve(args.size() + 1);
std::string argSingleThread = "--singlethread"; for (std::string& arg : args) {
std::string argNoContextCheck = "--no-context-check"; argv.push_back(arg.data());
std::string argSnapshotAlpha = "--snapshot-alpha"; }
std::string argSnapshotPrefix = "-s"; argv.push_back(nullptr);
std::string argSnapshotCall = "-S"; return MOBILEGL_APITRACE_RETRACE_MAIN(static_cast<int>(args.size()), argv.data());
std::string tracePath = request.tracePath;
char* argv[] = {
arg0.data(),
argBenchmark.data(),
argSingleThread.data(),
argNoContextCheck.data(),
argSnapshotAlpha.data(),
argSnapshotPrefix.data(),
prefix.data(),
argSnapshotCall.data(),
callSet.data(),
tracePath.data(),
nullptr,
};
return MOBILEGL_APITRACE_RETRACE_MAIN(10, argv);
} }
bool RunRetrace(const Request& request, Result& result) { bool RunRetrace(const Request& request, Result& result) {
@@ -475,6 +476,12 @@ bool RunRetrace(const Request& request, Result& result) {
return false; return false;
} }
if (request.benchmark) {
// Nothing was snapshotted, so there is nothing to collect or compare here; the
// caller turns the recorded frame times into the result instead.
return true;
}
std::string snapshotPath = SnapshotPathForCall(request); std::string snapshotPath = SnapshotPathForCall(request);
if (!Exists(snapshotPath)) { if (!Exists(snapshotPath)) {
result.statusCode = STATUS_RETRACE_FAILED; result.statusCode = STATUS_RETRACE_FAILED;
@@ -786,6 +793,82 @@ bool CompareWithGolden(const Request& request, Result& result) {
return result.passed; return result.passed;
} }
std::string BenchmarkResultPath(const Request& request) {
return request.benchmarkResultPath.empty() ? request.outputDir + "/benchmark.json"
: request.benchmarkResultPath;
}
// Folds the recorded frame times into the headline numbers. Everything but totalSeconds and
// the frame count is computed over the trailing benchmarkTailFrames frames only.
void SummarizeBenchmark(const Request& request, const benchmark::Report& report, Result& result) {
result.benchmarkFrames = static_cast<long long>(report.frameMs.size());
result.benchmarkTotalSeconds = report.totalSeconds;
result.benchmarkTailFrames = 0;
if (report.frameMs.empty()) {
return;
}
const int requestedTail =
request.benchmarkTailFrames > 0 ? request.benchmarkTailFrames : kDefaultBenchmarkTailFrames;
const std::size_t tail =
std::min(static_cast<std::size_t>(requestedTail), report.frameMs.size());
result.benchmarkTailFrames = static_cast<int>(tail);
std::vector<double> window(report.frameMs.end() - static_cast<std::ptrdiff_t>(tail),
report.frameMs.end());
double sum = 0.0;
for (double frameMs : window) {
sum += frameMs;
}
result.benchmarkMeanMs = sum / static_cast<double>(tail);
std::sort(window.begin(), window.end());
result.benchmarkMedianMs = (tail % 2 == 1)
? window[tail / 2]
: 0.5 * (window[tail / 2 - 1] + window[tail / 2]);
// Nearest-rank p95, so the reported value is always an observed frame time.
std::size_t rank = static_cast<std::size_t>(std::ceil(0.95 * static_cast<double>(tail)));
if (rank == 0) {
rank = 1;
}
result.benchmarkP95Ms = window[rank - 1];
result.benchmarkFps = result.benchmarkMeanMs > 0.0 ? 1000.0 / result.benchmarkMeanMs : -1.0;
}
bool WriteBenchmarkJson(const Request& request,
const Result& result,
const benchmark::Report& report) {
std::ofstream file(result.benchmarkResultPath, std::ios::out | std::ios::trunc);
if (!file) {
return false;
}
file << "{\n";
file << " \"tracePath\": \"" << JsonEscape(request.tracePath) << "\",\n";
file << " \"backend\": \"" << JsonEscape(request.backend) << "\",\n";
file << " \"benchmarkFinish\": " << (request.benchmarkFinish ? "true" : "false") << ",\n";
file << " \"width\": " << request.width << ",\n";
file << " \"height\": " << request.height << ",\n";
file << " \"totalFrames\": " << result.benchmarkFrames << ",\n";
file << " \"tailFrames\": " << result.benchmarkTailFrames << ",\n";
file << std::fixed << std::setprecision(6);
file << " \"totalSeconds\": " << result.benchmarkTotalSeconds << ",\n";
file << std::setprecision(3);
file << " \"meanFrameMs\": " << result.benchmarkMeanMs << ",\n";
file << " \"medianFrameMs\": " << result.benchmarkMedianMs << ",\n";
file << " \"p95FrameMs\": " << result.benchmarkP95Ms << ",\n";
file << " \"fps\": " << result.benchmarkFps << ",\n";
file << " \"frameTimesMs\": [";
for (std::size_t i = 0; i < report.frameMs.size(); ++i) {
if (i > 0) {
file << ", ";
}
file << report.frameMs[i];
}
file << "]\n";
file << "}\n";
return static_cast<bool>(file);
}
} // namespace } // namespace
extern "C" [[noreturn]] void mobilegl_apitrace_exit(int status) { extern "C" [[noreturn]] void mobilegl_apitrace_exit(int status) {
@@ -838,7 +921,25 @@ bool WriteResultJson(const Request& request, const Result& result) {
file << " \"deriveNumSubgroups\": " << (request.deriveNumSubgroups ? "true" : "false") << ",\n"; file << " \"deriveNumSubgroups\": " << (request.deriveNumSubgroups ? "true" : "false") << ",\n";
file << " \"iterationRPFixBarrier\": " << (request.iterationRPFixBarrier ? "true" : "false") << ",\n"; file << " \"iterationRPFixBarrier\": " << (request.iterationRPFixBarrier ? "true" : "false") << ",\n";
file << " \"holdMs\": " << request.holdMs << ",\n"; file << " \"holdMs\": " << request.holdMs << ",\n";
file << " \"mismatchPixels\": " << result.mismatchPixels << "\n"; file << " \"mismatchPixels\": " << result.mismatchPixels;
if (request.benchmark) {
// Headline numbers only; the per-frame array lives in benchmarkResultPath.
file << ",\n";
file << " \"benchmark\": true,\n";
file << " \"benchmarkResultPath\": \"" << JsonEscape(result.benchmarkResultPath) << "\",\n";
file << " \"benchmarkFinish\": " << (request.benchmarkFinish ? "true" : "false") << ",\n";
file << " \"benchmarkFrames\": " << result.benchmarkFrames << ",\n";
file << " \"benchmarkTailFrames\": " << result.benchmarkTailFrames << ",\n";
file << std::setprecision(6);
file << " \"benchmarkTotalSeconds\": " << result.benchmarkTotalSeconds << ",\n";
file << std::setprecision(3);
file << " \"benchmarkMeanFrameMs\": " << result.benchmarkMeanMs << ",\n";
file << " \"benchmarkMedianFrameMs\": " << result.benchmarkMedianMs << ",\n";
file << " \"benchmarkP95FrameMs\": " << result.benchmarkP95Ms << ",\n";
file << " \"benchmarkFps\": " << result.benchmarkFps << "\n";
} else {
file << "\n";
}
file << "}\n"; file << "}\n";
return true; return true;
} }
@@ -848,6 +949,9 @@ Result RunTraceReplay(const Request& request) {
result.resultPath = request.outputDir + "/result.json"; result.resultPath = request.outputDir + "/result.json";
result.actualPath = request.outputDir + "/actual.png"; result.actualPath = request.outputDir + "/actual.png";
result.diffPath = request.diffPath; result.diffPath = request.diffPath;
if (request.benchmark) {
result.benchmarkResultPath = BenchmarkResultPath(request);
}
const std::string mobileGlLogPath = request.outputDir + "/mobilegl.log"; const std::string mobileGlLogPath = request.outputDir + "/mobilegl.log";
if (!EnsureDirectory(request.outputDir)) { if (!EnsureDirectory(request.outputDir)) {
@@ -868,7 +972,8 @@ Result RunTraceReplay(const Request& request) {
return result; return result;
} }
if (request.targetCall < 0) { // Benchmark mode never snapshots, so it has no target call to stop at.
if (!request.benchmark && request.targetCall < 0) {
result.statusCode = STATUS_INVALID_ARGUMENT; result.statusCode = STATUS_INVALID_ARGUMENT;
result.message = "target_call must be set for dump-images style replay"; result.message = "target_call must be set for dump-images style replay";
return result; return result;
@@ -883,6 +988,40 @@ Result RunTraceReplay(const Request& request) {
return result; return result;
} }
if (request.benchmark) {
benchmark::Begin(request.benchmarkFinish);
const bool retraced = RunRetrace(request, result);
const benchmark::Report report = benchmark::End();
SummarizeBenchmark(request, report, result);
// Written even when the retrace failed: a partial timing series says where the
// replay got to, which is exactly what is wanted when triaging one.
const bool wroteJson = WriteBenchmarkJson(request, result, report);
HoldAfterRetrace(request);
if (!retraced) {
return result;
}
if (!wroteJson) {
result.statusCode = STATUS_IO_ERROR;
result.message = "benchmark completed but failed to write " + result.benchmarkResultPath;
return result;
}
// "Passed" in benchmark mode means the replay ran the trace to the end without
// error; there is no golden to be right or wrong about.
result.passed = true;
result.statusCode = STATUS_OK;
std::ostringstream message;
message << std::fixed << std::setprecision(3)
<< "benchmark completed; frames=" << result.benchmarkFrames
<< ", tailFrames=" << result.benchmarkTailFrames
<< ", meanMs=" << result.benchmarkMeanMs
<< ", medianMs=" << result.benchmarkMedianMs
<< ", p95Ms=" << result.benchmarkP95Ms
<< ", fps=" << result.benchmarkFps
<< ", benchmarkResultPath=" << result.benchmarkResultPath;
result.message = message.str();
return result;
}
if (!RunRetrace(request, result)) { if (!RunRetrace(request, result)) {
HoldAfterRetrace(request); HoldAfterRetrace(request);
return result; return result;
@@ -15,6 +15,8 @@ enum StatusCode {
STATUS_COMPARE_FAILED = 6, STATUS_COMPARE_FAILED = 6,
}; };
constexpr int kDefaultBenchmarkTailFrames = 200;
struct Request { struct Request {
std::string tracePath; std::string tracePath;
std::string goldenPath; std::string goldenPath;
@@ -30,6 +32,19 @@ struct Request {
// Named GL_TEXTURE_2D dump points, each `CALL,TEXTURE,LEVEL,DIR`. Debug-only; the replay // Named GL_TEXTURE_2D dump points, each `CALL,TEXTURE,LEVEL,DIR`. Debug-only; the replay
// behaves exactly as before when this is empty. // behaves exactly as before when this is empty.
std::vector<std::string> texture2dDumps; std::vector<std::string> texture2dDumps;
// Benchmark (frame-timing) mode. Off by default. When on, the replay runs the whole
// trace from start to finish and records a wall-clock timestamp at every frame
// boundary; no snapshot is taken and no golden comparison runs.
bool benchmark = false;
// Number of trailing frames the summary statistics are computed over. Clamped to the
// number of frames actually recorded. The tail is what is comparable between runs: the
// head of a trace is dominated by shader compiles and first-use uploads.
int benchmarkTailFrames = kDefaultBenchmarkTailFrames;
// glFinish through the replayed context at every frame boundary, so a frame time
// includes GPU completion instead of only CPU submission. See trace_benchmark.hpp.
bool benchmarkFinish = true;
// Where the timing JSON goes. Defaults to <outputDir>/benchmark.json.
std::string benchmarkResultPath;
int targetFrame = -1; int targetFrame = -1;
long long targetCall = -1; long long targetCall = -1;
int width = 0; int width = 0;
@@ -60,6 +75,16 @@ struct Result {
std::string matchedGoldenPath; std::string matchedGoldenPath;
double ssim = -1.0; double ssim = -1.0;
long long mismatchPixels = -1; long long mismatchPixels = -1;
// Benchmark headline numbers. Left at the defaults below unless the request asked for
// benchmark mode; benchmarkResultPath then names the JSON with the per-frame array.
std::string benchmarkResultPath;
long long benchmarkFrames = -1;
int benchmarkTailFrames = 0;
double benchmarkTotalSeconds = -1.0;
double benchmarkMeanMs = -1.0;
double benchmarkMedianMs = -1.0;
double benchmarkP95Ms = -1.0;
double benchmarkFps = -1.0;
}; };
Result RunTraceReplay(const Request& request); Result RunTraceReplay(const Request& request);
@@ -125,7 +125,11 @@ Java_top_mobilegl_plugin_trace_TraceReplayActivity_nativeRunTraceReplay(JNIEnv*
jboolean fixIterationRPSubgroupScratch, jboolean fixIterationRPSubgroupScratch,
jboolean deriveNumSubgroups, jboolean deriveNumSubgroups,
jboolean iterationRPFixBarrier, jboolean iterationRPFixBarrier,
jstring texture2dDumps) { jstring texture2dDumps,
jboolean benchmarkMode,
jint benchmarkTailFrames,
jboolean benchmarkFinish,
jstring benchmarkResultPath) {
mobilegl_trace::Request request; mobilegl_trace::Request request;
request.tracePath = ToString(env, tracePath); request.tracePath = ToString(env, tracePath);
request.goldenPath = ToString(env, goldenPath); request.goldenPath = ToString(env, goldenPath);
@@ -156,6 +160,12 @@ Java_top_mobilegl_plugin_trace_TraceReplayActivity_nativeRunTraceReplay(JNIEnv*
request.fixIterationRPSubgroupScratch = fixIterationRPSubgroupScratch == JNI_TRUE; request.fixIterationRPSubgroupScratch = fixIterationRPSubgroupScratch == JNI_TRUE;
request.deriveNumSubgroups = deriveNumSubgroups == JNI_TRUE; request.deriveNumSubgroups = deriveNumSubgroups == JNI_TRUE;
request.iterationRPFixBarrier = iterationRPFixBarrier == JNI_TRUE; request.iterationRPFixBarrier = iterationRPFixBarrier == JNI_TRUE;
request.benchmark = benchmarkMode == JNI_TRUE;
request.benchmarkTailFrames = benchmarkTailFrames > 0
? benchmarkTailFrames
: mobilegl_trace::kDefaultBenchmarkTailFrames;
request.benchmarkFinish = benchmarkFinish == JNI_TRUE;
request.benchmarkResultPath = ToString(env, benchmarkResultPath);
ScopedTraceReplayState replayState; ScopedTraceReplayState replayState;
mobilegl_trace_set_requested_size(request.width, request.height); mobilegl_trace_set_requested_size(request.width, request.height);
@@ -119,7 +119,11 @@ public final class TraceReplayActivity extends Activity {
request.fixIterationRPSubgroupScratch, request.fixIterationRPSubgroupScratch,
request.deriveNumSubgroups, request.deriveNumSubgroups,
request.iterationRPFixBarrier, request.iterationRPFixBarrier,
request.texture2dDumps request.texture2dDumps,
request.benchmark,
request.benchmarkTailFrames,
request.benchmarkFinish,
request.benchmarkResultPath
); );
Log.i(TAG, result.toString()); Log.i(TAG, result.toString());
TraceReplayResult finalResult = result; TraceReplayResult finalResult = result;
@@ -155,7 +159,11 @@ public final class TraceReplayActivity extends Activity {
boolean fixIterationRPSubgroupScratch, boolean fixIterationRPSubgroupScratch,
boolean deriveNumSubgroups, boolean deriveNumSubgroups,
boolean iterationRPFixBarrier, boolean iterationRPFixBarrier,
String texture2dDumps String texture2dDumps,
boolean benchmark,
int benchmarkTailFrames,
boolean benchmarkFinish,
String benchmarkResultPath
); );
private static final class TraceReplayRequest { private static final class TraceReplayRequest {
@@ -184,6 +192,13 @@ public final class TraceReplayActivity extends Activity {
final boolean deriveNumSubgroups; final boolean deriveNumSubgroups;
final boolean iterationRPFixBarrier; final boolean iterationRPFixBarrier;
final String texture2dDumps; final String texture2dDumps;
// Benchmark (frame-timing) mode. Replays the whole trace, times every frame
// boundary, and skips the snapshot and the SSIM comparison; "passed" then only
// means the replay ran to the end without error.
final boolean benchmark;
final int benchmarkTailFrames;
final boolean benchmarkFinish;
final String benchmarkResultPath;
private TraceReplayRequest( private TraceReplayRequest(
String tracePath, String tracePath,
@@ -210,7 +225,11 @@ public final class TraceReplayActivity extends Activity {
boolean fixIterationRPSubgroupScratch, boolean fixIterationRPSubgroupScratch,
boolean deriveNumSubgroups, boolean deriveNumSubgroups,
boolean iterationRPFixBarrier, boolean iterationRPFixBarrier,
String texture2dDumps String texture2dDumps,
boolean benchmark,
int benchmarkTailFrames,
boolean benchmarkFinish,
String benchmarkResultPath
) { ) {
this.tracePath = tracePath; this.tracePath = tracePath;
this.goldenPath = goldenPath; this.goldenPath = goldenPath;
@@ -237,11 +256,17 @@ public final class TraceReplayActivity extends Activity {
this.deriveNumSubgroups = deriveNumSubgroups; this.deriveNumSubgroups = deriveNumSubgroups;
this.iterationRPFixBarrier = iterationRPFixBarrier; this.iterationRPFixBarrier = iterationRPFixBarrier;
this.texture2dDumps = texture2dDumps; this.texture2dDumps = texture2dDumps;
this.benchmark = benchmark;
this.benchmarkTailFrames = benchmarkTailFrames;
this.benchmarkFinish = benchmarkFinish;
this.benchmarkResultPath = benchmarkResultPath;
} }
static TraceReplayRequest from(Intent intent, File filesDir, String defaultBackend) { static TraceReplayRequest from(Intent intent, File filesDir, String defaultBackend) {
String outputDir = readString(intent, "output_dir", new File(filesDir, "trace-replay").getAbsolutePath()); String outputDir = readString(intent, "output_dir", new File(filesDir, "trace-replay").getAbsolutePath());
String diffPath = readString(intent, "diff_path", ""); String diffPath = readString(intent, "diff_path", "");
String benchmarkResultPath =
readString(intent, "benchmark_result_path", outputDir + "/benchmark.json");
return new TraceReplayRequest( return new TraceReplayRequest(
readString(intent, "trace_path", ""), readString(intent, "trace_path", ""),
readString(intent, "golden_path", ""), readString(intent, "golden_path", ""),
@@ -267,7 +292,11 @@ public final class TraceReplayActivity extends Activity {
intent.getBooleanExtra("fix_iterationrp_subgroup_scratch", false), intent.getBooleanExtra("fix_iterationrp_subgroup_scratch", false),
intent.getBooleanExtra("derive_num_subgroups", false), intent.getBooleanExtra("derive_num_subgroups", false),
intent.getBooleanExtra("iterationrp_fix_barrier", false), intent.getBooleanExtra("iterationrp_fix_barrier", false),
readString(intent, "texture_2d_dumps", "") readString(intent, "texture_2d_dumps", ""),
intent.getBooleanExtra("benchmark", false),
intent.getIntExtra("benchmark_tail_frames", 200),
intent.getBooleanExtra("benchmark_finish", true),
benchmarkResultPath
); );
} }
+41 -1
View File
@@ -32,6 +32,10 @@ Usage:
[--avoid-angle-llvmpipe-explicit-lod-bias] \ [--avoid-angle-llvmpipe-explicit-lod-bias] \
[--coherent-as-flush] \ [--coherent-as-flush] \
[--dump-texture-2d CALL,TEXTURE,LEVEL,DIR] \ [--dump-texture-2d CALL,TEXTURE,LEVEL,DIR] \
[--benchmark] \
[--benchmark-tail-frames N] \
[--benchmark-finish 0|1] \
[--reuse-fixture] \
--timeout-seconds N --timeout-seconds N
Set MOBILEGL_USE_ANGLE=1 to run DirectGLES replay with packaged ANGLE Set MOBILEGL_USE_ANGLE=1 to run DirectGLES replay with packaged ANGLE
@@ -50,6 +54,12 @@ sample with an explicit LOD that ANGLE llvmpipe cannot take a LOD bias on
(MOBILEGL_AVOID_EXPLICIT_LOD_BIAS=1). (MOBILEGL_AVOID_EXPLICIT_LOD_BIAS=1).
Pass --coherent-as-flush for traces whose engine writes persistent Pass --coherent-as-flush for traces whose engine writes persistent
GL_MAP_FLUSH_EXPLICIT_BIT maps it never flushes (MOBILEGL_COHERENT_AS_FLUSH=1). GL_MAP_FLUSH_EXPLICIT_BIT maps it never flushes (MOBILEGL_COHERENT_AS_FLUSH=1).
Pass --benchmark to replay the whole trace as a frame-timing benchmark instead of
snapshotting one frame and comparing it against the golden. The run then also
copies benchmark.json (per-frame times plus mean/median/p95) out of the app, and
"passed" only means the replay reached the end of the trace without an error.
Pass --reuse-fixture to skip re-extracting and re-pushing the trace, for repeat
runs of a case whose fixture is already in /data/local/tmp.
EOF EOF
} }
@@ -109,6 +119,10 @@ avoid_angle_llvmpipe_sampler_mipmap_min_filter=0
avoid_angle_llvmpipe_explicit_lod_bias=0 avoid_angle_llvmpipe_explicit_lod_bias=0
coherent_as_flush=0 coherent_as_flush=0
texture_2d_dumps="" texture_2d_dumps=""
benchmark=0
benchmark_tail_frames=200
benchmark_finish=1
reuse_fixture=0
timeout_seconds="" timeout_seconds=""
while [ "$#" -gt 0 ]; do while [ "$#" -gt 0 ]; do
@@ -150,6 +164,10 @@ while [ "$#" -gt 0 ]; do
;; ;;
--coherent-as-flush) coherent_as_flush=1; shift 1 ;; --coherent-as-flush) coherent_as_flush=1; shift 1 ;;
--dump-texture-2d) texture_2d_dumps="$(next_arg "$@")"; shift 2 ;; --dump-texture-2d) texture_2d_dumps="$(next_arg "$@")"; shift 2 ;;
--benchmark) benchmark=1; shift 1 ;;
--benchmark-tail-frames) benchmark_tail_frames="$(next_arg "$@")"; shift 2 ;;
--benchmark-finish) benchmark_finish="$(next_arg "$@")"; shift 2 ;;
--reuse-fixture) reuse_fixture=1; shift 1 ;;
--timeout-seconds) timeout_seconds="$(next_arg "$@")"; shift 2 ;; --timeout-seconds) timeout_seconds="$(next_arg "$@")"; shift 2 ;;
-h|--help) usage; exit 0 ;; -h|--help) usage; exit 0 ;;
*) die "unknown argument: $1" ;; *) die "unknown argument: $1" ;;
@@ -339,7 +357,11 @@ run_retrace() {
fi fi
mkdir -p "${result_dir}" mkdir -p "${result_dir}"
# A repeat run of a case the previous invocation already installed and pushed only needs
# the on-device copy back into a fresh app output directory.
if [ "${reuse_fixture}" -eq 0 ]; then
"${ADB}" install -r "$(host_path_for_adb "${apk_file}")" "${ADB}" install -r "$(host_path_for_adb "${apk_file}")"
fi
copy_fixture_to_app copy_fixture_to_app
adb_device_path shell am force-stop "${package_name}" adb_device_path shell am force-stop "${package_name}"
"${ADB}" logcat -c "${ADB}" logcat -c
@@ -378,6 +400,16 @@ run_retrace() {
if [ -n "${texture_2d_dumps}" ]; then if [ -n "${texture_2d_dumps}" ]; then
set -- "$@" --es texture_2d_dumps "${texture_2d_dumps}" set -- "$@" --es texture_2d_dumps "${texture_2d_dumps}"
fi fi
if [ "${benchmark}" -eq 1 ]; then
set -- "$@" --ez benchmark true
set -- "$@" --ei benchmark_tail_frames "${benchmark_tail_frames}"
set -- "$@" --es benchmark_result_path "${app_dir}/output/benchmark.json"
if [ "${benchmark_finish}" = "0" ]; then
set -- "$@" --ez benchmark_finish false
else
set -- "$@" --ez benchmark_finish true
fi
fi
set -- "$@" \ set -- "$@" \
--es output_dir "${app_dir}/output" \ --es output_dir "${app_dir}/output" \
--es diff_path "${app_dir}/output/${safe_case}-diff.png" \ --es diff_path "${app_dir}/output/${safe_case}-diff.png" \
@@ -434,10 +466,16 @@ run_retrace() {
fi fi
adb_device_path exec-out run-as "${package_name}" cat "${app_dir}/output/result.json" > "${result_dir}/result.json" adb_device_path exec-out run-as "${package_name}" cat "${app_dir}/output/result.json" > "${result_dir}/result.json"
cat "${result_dir}/result.json" cat "${result_dir}/result.json"
# A benchmark run takes no snapshot, so there is no actual/diff pair to copy.
if [ "${benchmark}" -eq 0 ]; then
copy_app_artifact "${app_dir}/output/actual.png" "${result_dir}/${safe_case}-${backend}-actual.png" copy_app_artifact "${app_dir}/output/actual.png" "${result_dir}/${safe_case}-${backend}-actual.png"
copy_app_artifact "${app_dir}/output/${safe_case}-diff.png" "${result_dir}/${safe_case}-${backend}-diff.png" copy_app_artifact "${app_dir}/output/${safe_case}-diff.png" "${result_dir}/${safe_case}-${backend}-diff.png"
fi
copy_app_artifact "${app_dir}/output/retrace.log" "${result_dir}/retrace.log" copy_app_artifact "${app_dir}/output/retrace.log" "${result_dir}/retrace.log"
copy_app_artifact "${app_dir}/output/mobilegl.log" "${result_dir}/mobilegl.log" copy_app_artifact "${app_dir}/output/mobilegl.log" "${result_dir}/mobilegl.log"
if [ "${benchmark}" -eq 1 ]; then
copy_app_artifact "${app_dir}/output/benchmark.json" "${result_dir}/benchmark.json"
fi
copy_texture_2d_dumps copy_texture_2d_dumps
# A replay that wrote result.json but did not pass used to print nothing but # A replay that wrote result.json but did not pass used to print nothing but
@@ -468,6 +506,8 @@ run_retrace() {
} }
mkdir -p "${fixture_root}" "${result_root}" mkdir -p "${fixture_root}" "${result_root}"
prepare_fixture if [ "${reuse_fixture}" -eq 0 ]; then
prepare_fixture
fi
run_retrace run_retrace
+4 -2
View File
@@ -225,7 +225,8 @@ add_library(mobilegl_trace_glretrace_common STATIC
"${APITRACE_ROOT}/retrace/metric_helper.cpp" "${APITRACE_ROOT}/retrace/metric_helper.cpp"
"${APITRACE_ROOT}/retrace/metric_writer.cpp" "${APITRACE_ROOT}/retrace/metric_writer.cpp"
"${MOBILEGL_TRACE_ROOT}/apitrace_fbo_dump.cpp" "${MOBILEGL_TRACE_ROOT}/apitrace_fbo_dump.cpp"
"${MOBILEGL_TRACE_ROOT}/apitrace_glws_egl.cpp") "${MOBILEGL_TRACE_ROOT}/apitrace_glws_egl.cpp"
"${MOBILEGL_TRACE_SHARED_CPP_DIR}/trace_benchmark.cpp")
if(APPLE) if(APPLE)
set(MOBILEGL_TRACE_APPLE_FRAMEWORKS set(MOBILEGL_TRACE_APPLE_FRAMEWORKS
"-framework Cocoa" "-framework Cocoa"
@@ -239,7 +240,8 @@ target_include_directories(mobilegl_trace_glretrace_common PUBLIC
"${APITRACE_GENERATED_DIR}" "${APITRACE_GENERATED_DIR}"
"${APITRACE_ROOT}/dispatch" "${APITRACE_ROOT}/dispatch"
"${APITRACE_ROOT}/helpers" "${APITRACE_ROOT}/helpers"
"${APITRACE_ROOT}/retrace") "${APITRACE_ROOT}/retrace"
"${MOBILEGL_TRACE_SHARED_CPP_DIR}")
target_compile_definitions(mobilegl_trace_glretrace_common PRIVATE target_compile_definitions(mobilegl_trace_glretrace_common PRIVATE
exit=mobilegl_apitrace_exit exit=mobilegl_apitrace_exit
main=mobilegl_apitrace_main) main=mobilegl_apitrace_main)
+66
View File
@@ -280,6 +280,72 @@ unflushed persistent maps, e.g. the Create fixtures), pass
sundial-lite fixture), pass `--ez avoid_angle_llvmpipe_explicit_lod_bias true` so sundial-lite fixture), pass `--ez avoid_angle_llvmpipe_explicit_lod_bias true` so
the replay runs with `MOBILEGL_AVOID_EXPLICIT_LOD_BIAS=1`. the replay runs with `MOBILEGL_AVOID_EXPLICIT_LOD_BIAS=1`.
## Benchmark mode (frame timing)
Benchmark mode reuses the same fixtures as a performance harness instead of a
correctness one: it replays the trace from the first call to the last, takes a
wall-clock timestamp at every frame boundary, and takes no snapshot and runs no
SSIM comparison. `passed` then only means the replay reached the end of the trace
without an error.
Frame times include GPU completion by default, because a swap boundary on a tiled
mobile GPU returns long before the tiler is done and would otherwise time CPU
submission alone. That is what `benchmark_finish` / `--benchmark-finish` controls:
on (the default) it issues a `glFinish` through the replayed context at every
frame boundary, which serializes CPU/GPU overlap - pessimistic against a real
running game, but deterministic and comparable between backends and revisions.
Turn it off to measure CPU-side submission only.
The mean/median/p95 are computed over the last `benchmark_tail_frames` frames
(default 200, clamped to the frames actually recorded); the head of a trace is
dominated by shader compiles and first-use uploads. The full per-frame array is in
the timing JSON, next to the headline numbers, which also appear in `result.json`.
Whole device runs, one line per run plus the best of the repeats:
```sh
python tools/trace_replay/run_android_retrace_local.py --benchmark \
--case minecraft-1.21.4-fabric-iris-photon-in-world --backend DirectVulkan \
--benchmark-repeats 3
```
`--benchmark-tail-frames N`, `--benchmark-no-finish` and
`--benchmark-timeout-seconds N` are available; only the first repeat installs the
APK and pushes the trace. Each run's `benchmark.json` is kept next to the case
result as `benchmark-run<N>.json`.
The Activity takes the same settings directly:
```sh
adb shell am start -a top.mobilegl.plugin.TRACE_REPLAY \
-n $PKG/top.mobilegl.plugin.trace.TraceReplayActivity \
--es trace_path $APP_DIR/input/openra.trace \
--es output_dir $APP_DIR/output \
--es backend DirectGLES \
--ei width 640 --ei height 480 \
--ez benchmark true \
--ei benchmark_tail_frames 200 \
--ez benchmark_finish true \
--es benchmark_result_path $APP_DIR/output/benchmark.json
adb exec-out run-as $PKG cat files/trace-replay/output/benchmark.json > benchmark.json
```
`golden_path` and `target_call` are not needed in benchmark mode. The Linux CLI
takes the same options:
```sh
./mobilegl_trace_replay --trace openra.trace --output out --backend DirectGLES \
--benchmark --benchmark-tail-frames=200 --benchmark-finish=1 \
--benchmark-result=out/benchmark.json
```
Two caveats when reading the numbers. Frame times are taken at `eglSwapBuffers`,
so a trace that ends frames with `glFrameTerminatorGREMEDY` instead is not timed.
And on a window surface the swap can block on the compositor, which pins frame
times to the display refresh; replay against the pbuffer surface
(`--ez use_pbuffer true`) to measure the renderer rather than the presentation
path.
## Reproducing the Android DirectGLES lane on Linux (ANGLE on lavapipe) ## Reproducing the Android DirectGLES lane on Linux (ANGLE on lavapipe)
The APK workflow's DirectGLES lane is not the same stack as the Linux one, which The APK workflow's DirectGLES lane is not the same stack as the Linux one, which
+4
View File
@@ -2,6 +2,7 @@
#include "retrace.hpp" #include "retrace.hpp"
#include "apitrace_fbo_dump.hpp" #include "apitrace_fbo_dump.hpp"
#include "trace_benchmark.hpp"
#include <EGL/egl.h> #include <EGL/egl.h>
#include <EGL/eglext.h> #include <EGL/eglext.h>
@@ -414,6 +415,9 @@ public:
char callNo[32]; char callNo[32];
snprintf(callNo, sizeof(callNo), "%u", retrace::callNo); snprintf(callNo, sizeof(callNo), "%u", retrace::callNo);
gEgl.swapBuffers(gDisplay, surface); gEgl.swapBuffers(gDisplay, surface);
// Frame boundary: retrace_eglSwapBuffers() has already run frame_complete() and
// handed the frame to us. No-op unless benchmark mode armed the timer.
mobilegl_trace::benchmark::OnFrameBoundary();
#if defined(__APPLE__) #if defined(__APPLE__)
PumpMacOSEvents(); PumpMacOSEvents();
if (window && !windowShown) { if (window && !windowShown) {
+117 -2
View File
@@ -119,7 +119,7 @@ def render_summary():
shutil.copyfile(SUMMARY_DIR / SUMMARY_HTML, SUMMARY_DIR / "index.html") shutil.copyfile(SUMMARY_DIR / SUMMARY_HTML, SUMMARY_DIR / "index.html")
def run_case(case, backend): def run_case(case, backend, extra_args=None, timeout_seconds=None):
backend_info = BACKENDS[backend] backend_info = BACKENDS[backend]
apk = find_trace_apk() apk = find_trace_apk()
trace_archive = FIXTURES / case["trace_archive"] trace_archive = FIXTURES / case["trace_archive"]
@@ -176,8 +176,9 @@ def run_case(case, backend):
"--crop-height", "--crop-height",
str(case["crop_height"]), str(case["crop_height"]),
"--timeout-seconds", "--timeout-seconds",
str(case["timeout_seconds"]), str(timeout_seconds if timeout_seconds is not None else case["timeout_seconds"]),
] ]
command.extend(extra_args or [])
if alternate is not None: if alternate is not None:
command[command.index("--target-call"):command.index("--target-call")] = ["--alternate-golden", bash_path(alternate)] command[command.index("--target-call"):command.index("--target-call")] = ["--alternate-golden", bash_path(alternate)]
if backend_info["use_pbuffer"]: if backend_info["use_pbuffer"]:
@@ -204,12 +205,118 @@ def run_case(case, backend):
return result.returncode return result.returncode
def read_benchmark(case, backend, run_index):
"""Reads the benchmark.json the run just pulled and files it under the run number."""
result_dir = RESULT_ROOT / f"{safe_case(case['name'])}-{backend}"
source = result_dir / "benchmark.json"
if not source.exists():
return None
try:
report = json.loads(source.read_text(encoding="utf-8"))
except (OSError, ValueError) as error:
print(f"failed to read {source}: {error}", file=sys.stderr)
return None
shutil.copyfile(source, result_dir / f"benchmark-run{run_index}.json")
return report
def format_benchmark(report):
return (
f"frames={report.get('totalFrames', -1)}"
f" total={report.get('totalSeconds', -1):.1f}s"
f" tail={report.get('tailFrames', -1)}"
f" mean={report.get('meanFrameMs', -1):.3f}ms"
f" median={report.get('medianFrameMs', -1):.3f}ms"
f" p95={report.get('p95FrameMs', -1):.3f}ms"
f" fps={report.get('fps', -1):.1f}"
)
def run_benchmark_case(case, backend, args):
"""Runs the case as a frame-timing benchmark `--benchmark-repeats` times.
Only the first run installs the APK and pushes the trace; the repeats reuse what is
already on the device, so the numbers are not paying for an adb push each time.
"""
label = f"{case['name']} / {backend}"
reports = []
failures = 0
for run_index in range(1, args.benchmark_repeats + 1):
extra_args = [
"--benchmark",
"--benchmark-tail-frames",
str(args.benchmark_tail_frames),
"--benchmark-finish",
"0" if args.benchmark_no_finish else "1",
]
if run_index > 1:
extra_args.append("--reuse-fixture")
# The previous repeat's file would otherwise be read back as this run's result.
stale = RESULT_ROOT / f"{safe_case(case['name'])}-{backend}" / "benchmark.json"
if stale.exists():
stale.unlink()
rc = run_case(case, backend, extra_args=extra_args, timeout_seconds=args.benchmark_timeout_seconds)
report = read_benchmark(case, backend, run_index)
if rc != 0 or report is None:
print(f"{label} run {run_index}/{args.benchmark_repeats}: FAILED (exit {rc})", flush=True)
failures += 1
continue
reports.append((run_index, report))
print(
f"{label} run {run_index}/{args.benchmark_repeats}: {format_benchmark(report)}",
flush=True,
)
def mean_frame_ms(entry):
# A run that recorded no frames reports -1; it must not win "best" by being smallest.
mean = entry[1].get("meanFrameMs", -1)
return mean if mean > 0 else float("inf")
if reports:
best_index, best = min(reports, key=mean_frame_ms)
print(
f"{label} best of {args.benchmark_repeats} (run {best_index}): {format_benchmark(best)}",
flush=True,
)
return 1 if failures else 0
def parse_args(): def parse_args():
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument("--case", action="append", dest="cases", help="Case name to run; may be repeated.") parser.add_argument("--case", action="append", dest="cases", help="Case name to run; may be repeated.")
parser.add_argument("--backend", action="append", choices=sorted(BACKENDS), help="Backend to run; may be repeated.") parser.add_argument("--backend", action="append", choices=sorted(BACKENDS), help="Backend to run; may be repeated.")
parser.add_argument("--all", action="store_true", help="Run every case in the APK workflow matrix.") parser.add_argument("--all", action="store_true", help="Run every case in the APK workflow matrix.")
parser.add_argument("--keep-results", action="store_true", help="Do not clear the previous result root.") parser.add_argument("--keep-results", action="store_true", help="Do not clear the previous result root.")
parser.add_argument(
"--benchmark",
action="store_true",
help="Replay each case end to end as a frame-timing benchmark instead of comparing "
"one frame against its golden.",
)
parser.add_argument(
"--benchmark-repeats",
type=int,
default=3,
help="Benchmark runs per case/backend; the best (lowest mean frame time) is reported.",
)
parser.add_argument(
"--benchmark-tail-frames",
type=int,
default=200,
help="Frames at the end of the run the statistics are computed over.",
)
parser.add_argument(
"--benchmark-no-finish",
action="store_true",
help="Do not glFinish at every frame boundary, so frame times measure CPU submission "
"only instead of GPU completion.",
)
parser.add_argument(
"--benchmark-timeout-seconds",
type=int,
default=900,
help="Per-run timeout; a benchmark replays the whole trace, not just up to target_call.",
)
return parser.parse_args() return parser.parse_args()
@@ -221,12 +328,20 @@ def main():
if not selected_cases: if not selected_cases:
print("No cases selected. Use --all or --case NAME.", file=sys.stderr) print("No cases selected. Use --all or --case NAME.", file=sys.stderr)
return 2 return 2
if args.benchmark and args.benchmark_repeats < 1:
print("--benchmark-repeats must be at least 1.", file=sys.stderr)
return 2
if not args.keep_results and RESULT_ROOT.exists(): if not args.keep_results and RESULT_ROOT.exists():
shutil.rmtree(RESULT_ROOT) shutil.rmtree(RESULT_ROOT)
RESULT_ROOT.mkdir(parents=True, exist_ok=True) RESULT_ROOT.mkdir(parents=True, exist_ok=True)
failures = 0 failures = 0
for case in selected_cases: for case in selected_cases:
for backend in selected_backends: for backend in selected_backends:
if args.benchmark:
print(f"=== Android benchmark: {case['name']} / {backend} ===", flush=True)
# No SSIM verdicts to render here; the summary page is for the correctness lane.
failures += run_benchmark_case(case, backend, args)
continue
print(f"=== Android retrace: {case['name']} / {backend} ===", flush=True) print(f"=== Android retrace: {case['name']} / {backend} ===", flush=True)
rc = run_case(case, backend) rc = run_case(case, backend)
try: try:
+50 -1
View File
@@ -29,6 +29,15 @@ void PrintUsage(const char *argv0) {
<< " --crop-width N Compare crop width\n" << " --crop-width N Compare crop width\n"
<< " --crop-height N Compare crop height\n" << " --crop-height N Compare crop height\n"
<< " --coherent-as-flush Set MOBILEGL_COHERENT_AS_FLUSH=1 for the replay\n" << " --coherent-as-flush Set MOBILEGL_COHERENT_AS_FLUSH=1 for the replay\n"
<< " --benchmark Frame-timing mode: replay the whole trace, record a\n"
<< " wall-clock timestamp at every frame boundary, and skip\n"
<< " the snapshot and the golden comparison. --target-call is\n"
<< " not required in this mode.\n"
<< " --benchmark-tail-frames=N Frames at the end of the run the mean/median/p95 are\n"
<< " computed over (default: 200, clamped to the frame count)\n"
<< " --benchmark-finish=0/1 glFinish at every frame boundary so a frame time covers\n"
<< " GPU completion and not just CPU submission (default: 1)\n"
<< " --benchmark-result=PATH Timing JSON output (default: OUTPUT/benchmark.json)\n"
<< " --dump-fbo-attachments CALL:DIR[:FBO,FBO,...]\n" << " --dump-fbo-attachments CALL:DIR[:FBO,FBO,...]\n"
<< " At CALL, write every colour attachment and the depth\n" << " At CALL, write every colour attachment and the depth\n"
<< " attachment of every live framebuffer object into DIR as\n" << " attachment of every live framebuffer object into DIR as\n"
@@ -77,6 +86,23 @@ bool ReadEnvFlag(const char *name) {
return value != nullptr && std::string(value) == "1"; return value != nullptr && std::string(value) == "1";
} }
// The benchmark options are documented as --name=VALUE. The space-separated spelling every
// other option here uses is accepted too, so --benchmark-tail-frames=300 and
// --benchmark-tail-frames 300 both work; hasInlineValue says which one was given.
bool MatchOption(const std::string &arg, const char *name, std::string &inlineValue, bool &hasInlineValue) {
if (arg == name) {
hasInlineValue = false;
return true;
}
const std::string prefix = std::string(name) + "=";
if (arg.compare(0, prefix.size(), prefix) == 0) {
inlineValue = arg.substr(prefix.size());
hasInlineValue = true;
return true;
}
return false;
}
bool ParseArgs(int argc, char **argv, mobilegl_trace::Request &request) { bool ParseArgs(int argc, char **argv, mobilegl_trace::Request &request) {
request.backend = "DirectGLES"; request.backend = "DirectGLES";
request.fixIterationRPSubgroupScratch = ReadEnvFlag("MOBILEGL_FIX_ITERATIONRP_SUBGROUP_SCRATCH"); request.fixIterationRPSubgroupScratch = ReadEnvFlag("MOBILEGL_FIX_ITERATIONRP_SUBGROUP_SCRATCH");
@@ -85,6 +111,8 @@ bool ParseArgs(int argc, char **argv, mobilegl_trace::Request &request) {
for (int i = 1; i < argc; ++i) { for (int i = 1; i < argc; ++i) {
const std::string arg = argv[i]; const std::string arg = argv[i];
std::string optionValue;
bool optionHasValue = false;
if (arg == "--trace") { if (arg == "--trace") {
if (!ReadValue(argc, argv, i, request.tracePath)) return false; if (!ReadValue(argc, argv, i, request.tracePath)) return false;
} else if (arg == "--golden") { } else if (arg == "--golden") {
@@ -129,6 +157,17 @@ bool ParseArgs(int argc, char **argv, mobilegl_trace::Request &request) {
if (!ReadInt(argc, argv, i, request.cropHeight)) return false; if (!ReadInt(argc, argv, i, request.cropHeight)) return false;
} else if (arg == "--coherent-as-flush") { } else if (arg == "--coherent-as-flush") {
request.coherentAsFlush = true; request.coherentAsFlush = true;
} else if (arg == "--benchmark") {
request.benchmark = true;
} else if (MatchOption(arg, "--benchmark-tail-frames", optionValue, optionHasValue)) {
if (!optionHasValue && !ReadValue(argc, argv, i, optionValue)) return false;
request.benchmarkTailFrames = std::atoi(optionValue.c_str());
} else if (MatchOption(arg, "--benchmark-finish", optionValue, optionHasValue)) {
if (!optionHasValue && !ReadValue(argc, argv, i, optionValue)) return false;
request.benchmarkFinish = optionValue != "0";
} else if (MatchOption(arg, "--benchmark-result", optionValue, optionHasValue)) {
if (!optionHasValue && !ReadValue(argc, argv, i, optionValue)) return false;
request.benchmarkResultPath = optionValue;
} else if (arg == "--dump-fbo-attachments") { } else if (arg == "--dump-fbo-attachments") {
std::string dumpPoint; std::string dumpPoint;
if (!ReadValue(argc, argv, i, dumpPoint)) return false; if (!ReadValue(argc, argv, i, dumpPoint)) return false;
@@ -153,7 +192,7 @@ bool ParseArgs(int argc, char **argv, mobilegl_trace::Request &request) {
std::cerr << "--output is required\n"; std::cerr << "--output is required\n";
return false; return false;
} }
if (request.targetCall < 0) { if (request.targetCall < 0 && !request.benchmark) {
std::cerr << "--target-call is required\n"; std::cerr << "--target-call is required\n";
return false; return false;
} }
@@ -161,6 +200,10 @@ bool ParseArgs(int argc, char **argv, mobilegl_trace::Request &request) {
std::cerr << "--hold-ms must be non-negative\n"; std::cerr << "--hold-ms must be non-negative\n";
return false; return false;
} }
if (request.benchmarkTailFrames <= 0) {
std::cerr << "--benchmark-tail-frames must be positive\n";
return false;
}
return true; return true;
} }
@@ -181,9 +224,15 @@ int main(int argc, char **argv) {
mobilegl_trace::WriteResultJson(request, result); mobilegl_trace::WriteResultJson(request, result);
} }
if (request.benchmark) {
std::cout << result.message << "\n"
<< "result: " << result.resultPath << "\n"
<< "benchmark: " << result.benchmarkResultPath << "\n";
} else {
std::cout << result.message << "\n" std::cout << result.message << "\n"
<< "result: " << result.resultPath << "\n" << "result: " << result.resultPath << "\n"
<< "actual: " << result.actualPath << "\n" << "actual: " << result.actualPath << "\n"
<< "diff: " << result.diffPath << "\n"; << "diff: " << result.diffPath << "\n";
}
return result.passed ? 0 : result.statusCode; return result.passed ? 0 : result.statusCode;
} }