mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
[Android] retain trace replay ownership across Activity recreation
This commit is contained in:
@@ -52,8 +52,11 @@
|
||||
android:name="pojavEnv"
|
||||
tools:node="remove" />
|
||||
|
||||
<!-- Keep the native render Surface through runtime overlay/display changes.
|
||||
See the integer resource for the API 34-compatible configuration mask. -->
|
||||
<activity
|
||||
android:name=".trace.TraceReplayActivity"
|
||||
android:configChanges="@integer/trace_replay_config_changes"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.MobileGLPlugin">
|
||||
<intent-filter>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "spawn_spike.hpp"
|
||||
#include "trace_env_overrides.hpp"
|
||||
#include "trace_replay_lease.hpp"
|
||||
|
||||
#include <android/native_window.h>
|
||||
#include <android/native_window_jni.h>
|
||||
@@ -119,6 +120,18 @@ Java_top_mobilegl_plugin_trace_TraceReplayActivity_nativeRunTraceReplay(JNIEnv*
|
||||
jboolean benchmarkFinish,
|
||||
jstring benchmarkResultPath,
|
||||
jstring envOverrides) {
|
||||
// Claim before changing GLWS globals, environment, logs or output files. The lease
|
||||
// outlives replayState, so another invocation cannot race native-window cleanup.
|
||||
mobilegl_trace::TraceReplayLease lease;
|
||||
if (!lease) {
|
||||
mobilegl_trace::Result rejected;
|
||||
rejected.statusCode = mobilegl_trace::STATUS_RETRACE_FAILED;
|
||||
rejected.message = "another trace replay is already running in this process";
|
||||
// No paths: a duplicate Activity can share outputDir with the live invocation.
|
||||
// Publishing this failure there would overwrite that invocation's real result.
|
||||
return MakeResult(env, rejected);
|
||||
}
|
||||
|
||||
mobilegl_trace::Request request;
|
||||
request.tracePath = ToString(env, tracePath);
|
||||
request.goldenPath = ToString(env, goldenPath);
|
||||
@@ -157,12 +170,20 @@ Java_top_mobilegl_plugin_trace_TraceReplayActivity_nativeRunTraceReplay(JNIEnv*
|
||||
request.benchmarkResultPath = ToString(env, benchmarkResultPath);
|
||||
request.envOverrides = SplitSemicolonList(ToString(env, envOverrides));
|
||||
|
||||
ScopedTraceReplayState replayState;
|
||||
mobilegl_trace_set_requested_size(request.width, request.height);
|
||||
const bool needsNativeWindow =
|
||||
request.backend == "DirectVulkan" ||
|
||||
(request.backend == "DirectGLES" && !request.usePbuffer);
|
||||
ANativeWindow *window = needsNativeWindow && surface != nullptr ? ANativeWindow_fromSurface(env, surface) : nullptr;
|
||||
if (needsNativeWindow && window == nullptr) {
|
||||
auto result = MakeFailureResult(request, mobilegl_trace::STATUS_RETRACE_FAILED,
|
||||
"render surface was destroyed before native replay started");
|
||||
if (!result.resultPath.empty()) {
|
||||
mobilegl_trace::WriteResultJson(request, result);
|
||||
}
|
||||
return MakeResult(env, result);
|
||||
}
|
||||
ScopedTraceReplayState replayState;
|
||||
mobilegl_trace_set_requested_size(request.width, request.height);
|
||||
mobilegl_trace_set_native_window(window);
|
||||
if (window != nullptr) {
|
||||
ANativeWindow_release(window);
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
|
||||
namespace mobilegl_trace {
|
||||
|
||||
// apitrace, its GLWS adapter and the environment are process-global. --singlethread
|
||||
// controls trace dispatch only; it does not make two JNI invocations independent.
|
||||
class TraceReplayLease {
|
||||
public:
|
||||
TraceReplayLease() : acquired_(!running_.test_and_set(std::memory_order_acquire)) {}
|
||||
~TraceReplayLease() {
|
||||
if (acquired_) {
|
||||
running_.clear(std::memory_order_release);
|
||||
}
|
||||
}
|
||||
|
||||
TraceReplayLease(const TraceReplayLease&) = delete;
|
||||
TraceReplayLease& operator=(const TraceReplayLease&) = delete;
|
||||
|
||||
explicit operator bool() const { return acquired_; }
|
||||
|
||||
private:
|
||||
inline static std::atomic_flag running_ = ATOMIC_FLAG_INIT;
|
||||
const bool acquired_;
|
||||
};
|
||||
|
||||
} // namespace mobilegl_trace
|
||||
+43
-13
@@ -25,7 +25,8 @@ public final class TraceReplayActivity extends Activity {
|
||||
|
||||
private TextView statusView;
|
||||
private TraceReplayRequest request;
|
||||
private boolean started;
|
||||
private TraceReplaySession<TraceReplayResult> replaySession;
|
||||
private final TraceReplaySession.Listener<TraceReplayResult> replayListener = this::onReplayComplete;
|
||||
private final Handler mainHandler = new Handler(Looper.getMainLooper());
|
||||
|
||||
@Override
|
||||
@@ -62,6 +63,18 @@ public final class TraceReplayActivity extends Activity {
|
||||
return;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
TraceReplaySession<TraceReplayResult> retained =
|
||||
(TraceReplaySession<TraceReplayResult>) getLastNonConfigurationInstance();
|
||||
replaySession = retained != null ? retained : new TraceReplaySession<>(
|
||||
command -> new Thread(command, "MobileGLTraceReplay").start(),
|
||||
command -> new Handler(Looper.getMainLooper()).post(command));
|
||||
if (replaySession.hasStarted()) {
|
||||
Log.i(TAG, "Reattaching to retained trace replay: " + request.outputDir);
|
||||
statusView.setText("Running trace replay\n" + request.outputDir);
|
||||
}
|
||||
replaySession.attach(replayListener);
|
||||
|
||||
SurfaceHolder holder = surfaceView.getHolder();
|
||||
if (request.width > 0 && request.height > 0) {
|
||||
holder.setFixedSize(request.width, request.height);
|
||||
@@ -87,20 +100,43 @@ public final class TraceReplayActivity extends Activity {
|
||||
mainHandler.postDelayed(() -> startReplay(holder), 250);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object onRetainNonConfigurationInstance() {
|
||||
return replaySession;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
// A callback queued by an old Surface must not start after that Activity is gone.
|
||||
mainHandler.removeCallbacksAndMessages(null);
|
||||
if (replaySession != null) {
|
||||
replaySession.detach(replayListener);
|
||||
}
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
private void startReplay(SurfaceHolder holder) {
|
||||
if (started) {
|
||||
if (isFinishing() || isDestroyed() || replaySession.hasStarted()) {
|
||||
return;
|
||||
}
|
||||
Surface surface = holder.getSurface();
|
||||
if (surface == null || !surface.isValid()) {
|
||||
return;
|
||||
}
|
||||
started = true;
|
||||
statusView.setText("Running trace replay\n" + request.outputDir);
|
||||
new Thread(() -> runRequest(request, surface), "MobileGLTraceReplay").start();
|
||||
TraceReplayRequest replayRequest = request;
|
||||
// The worker retains the request and Surface, not the Activity. JNI also acquires
|
||||
// its own ANativeWindow reference until the replay and native cleanup finish.
|
||||
replaySession.start(() -> runRequest(replayRequest, surface));
|
||||
}
|
||||
|
||||
private void runRequest(TraceReplayRequest request, Surface surface) {
|
||||
private void onReplayComplete(TraceReplayResult result) {
|
||||
statusView.setText(result.toString());
|
||||
finish();
|
||||
}
|
||||
|
||||
private static TraceReplayResult runRequest(TraceReplayRequest request, Surface surface) {
|
||||
Log.i(TAG, "Starting native trace replay: " + request.outputDir);
|
||||
TraceReplayResult result = nativeRunTraceReplay(
|
||||
surface,
|
||||
request.tracePath,
|
||||
@@ -135,11 +171,7 @@ public final class TraceReplayActivity extends Activity {
|
||||
request.envOverrides
|
||||
);
|
||||
Log.i(TAG, result.toString());
|
||||
TraceReplayResult finalResult = result;
|
||||
runOnUiThread(() -> {
|
||||
statusView.setText(finalResult.toString());
|
||||
finish();
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
private static native TraceReplayResult nativeRunTraceReplay(
|
||||
@@ -202,9 +234,7 @@ public final class TraceReplayActivity extends Activity {
|
||||
}
|
||||
|
||||
private void runSpawnSpike(String libraryName) {
|
||||
// The surface callbacks fire regardless; this keeps them from starting a replay
|
||||
// underneath the spike.
|
||||
started = true;
|
||||
// onCreate returns before installing replay surface callbacks in this mode.
|
||||
File outputDir = new File(request.outputDir);
|
||||
String serverPath = new File(getApplicationInfo().nativeLibraryDir, libraryName)
|
||||
.getAbsolutePath();
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
package top.mobilegl.plugin.trace;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/** One native invocation, retained when Android replaces the Activity that displays it. */
|
||||
final class TraceReplaySession<T> {
|
||||
interface Listener<T> {
|
||||
void onComplete(T result);
|
||||
}
|
||||
|
||||
private final Executor worker;
|
||||
private final Executor completion;
|
||||
private boolean started;
|
||||
private boolean completed;
|
||||
private T result;
|
||||
private Listener<T> listener;
|
||||
|
||||
TraceReplaySession(Executor worker, Executor completion) {
|
||||
this.worker = worker;
|
||||
this.completion = completion;
|
||||
}
|
||||
|
||||
// All session state is accessed on the main thread. Only replay.get() runs on the worker.
|
||||
boolean hasStarted() {
|
||||
return started;
|
||||
}
|
||||
|
||||
boolean start(Supplier<T> replay) {
|
||||
if (started) {
|
||||
return false;
|
||||
}
|
||||
started = true;
|
||||
worker.execute(() -> {
|
||||
T replayResult = replay.get();
|
||||
completion.execute(() -> {
|
||||
result = replayResult;
|
||||
completed = true;
|
||||
if (listener != null) {
|
||||
listener.onComplete(result);
|
||||
}
|
||||
});
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
void attach(Listener<T> newListener) {
|
||||
listener = newListener;
|
||||
if (completed) {
|
||||
listener.onComplete(result);
|
||||
}
|
||||
}
|
||||
|
||||
void detach(Listener<T> oldListener) {
|
||||
if (listener == oldListener) {
|
||||
listener = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- CONFIG_ASSETS_PATHS (0x80000000) exists in Android 34, but that SDK has no
|
||||
public configChanges flag name for it. Package parsing resolves this integer
|
||||
through TypedArray.getInt. Keep it unqualified: overlay installation must not
|
||||
recreate the Activity and destroy the live native render Surface.
|
||||
0x0f80 = orientation | screenLayout | uiMode | screenSize | smallestScreenSize,
|
||||
matching PluginActivity. Trace framebuffer dimensions are request parameters,
|
||||
and these configuration changes need no new view or native replay. -->
|
||||
<integer name="trace_replay_config_changes">0x80000f80</integer>
|
||||
</resources>
|
||||
Reference in New Issue
Block a user