Skip to content

Commit 0d436aa

Browse files
committed
Support multi-window replay on Android
Introduce JAVA layer into gfxreconstruct to enable replay multi-window trace files on Android. The command line changes to am start -n "com.lunarg.gfxreconstruct.replay/.ReplayActivity"
1 parent 4428ab3 commit 0d436aa

31 files changed

+272
-7
lines changed

android/framework/application/CMakeLists.txt

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
add_library(gfxrecon_application STATIC "")
22

33
target_sources(gfxrecon_application
4-
PRIVATE
4+
PUBLIC
55
${GFXRECON_SOURCE_DIR}/framework/application/android_context.h
66
${GFXRECON_SOURCE_DIR}/framework/application/android_context.cpp
77
${GFXRECON_SOURCE_DIR}/framework/application/android_window.h
@@ -10,6 +10,7 @@ target_sources(gfxrecon_application
1010
${GFXRECON_SOURCE_DIR}/framework/application/application.cpp
1111
${GFXRECON_SOURCE_DIR}/framework/application/wsi_context.h
1212
${GFXRECON_SOURCE_DIR}/framework/application/wsi_context.cpp
13+
${GFXRECON_SOURCE_DIR}/framework/application/android_jni.cpp
1314
)
1415

1516
target_include_directories(gfxrecon_application

android/scripts/gfxrecon.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141

4242
# Application info
4343
app_name = 'com.lunarg.gfxreconstruct.replay'
44-
app_activity = '"com.lunarg.gfxreconstruct.replay/android.app.NativeActivity"'
44+
app_activity = '"com.lunarg.gfxreconstruct.replay/.ReplayActivity"'
4545
app_action = 'android.intent.action.MAIN'
4646
app_category = 'android.intent.category.LAUNCHER'
4747

android/tools/replay/src/main/AndroidManifest.xml android/tools/replay/AndroidManifest.xml

+2-2
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@
1414
android:theme="@style/AppTheme"
1515
android:supportsRtl="true"
1616
android:extractNativeLibs="true"
17-
android:hasCode="false">
18-
<activity android:name="android.app.NativeActivity"
17+
android:hasCode="true">
18+
<activity android:name=".ReplayActivity"
1919
android:exported="true"
2020
android:configChanges="orientation|screenSize|keyboard|keyboardHidden|screenLayout"
2121
android:screenOrientation="unspecified">

android/tools/replay/build.gradle

+7
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,13 @@ android {
3636
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
3737
}
3838
}
39+
sourceSets {
40+
main {
41+
manifest.srcFile 'AndroidManifest.xml'
42+
java.srcDirs = ['src']
43+
res.srcDirs = ['res']
44+
}
45+
}
3946
externalNativeBuild {
4047
cmake {
4148
path "CMakeLists.txt"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<resources>
3+
<style name="AppTheme" parent="@android:style/Theme.NoTitleBar.Fullscreen">
4+
<item name="colorPrimary">@color/colorPrimary</item>
5+
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
6+
<item name="colorAccent">@color/colorAccent</item>
7+
8+
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
9+
<item name="android:windowTranslucentStatus">true</item>
10+
<item name="android:windowTranslucentNavigation">true</item>
11+
</style>
12+
</resources>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
/*
2+
** Copyright (c) 2025 LunarG, Inc.
3+
** Copyright (c) 2025 Arm Limited and/or its affiliates <[email protected]>
4+
**
5+
** Permission is hereby granted, free of charge, to any person obtaining a
6+
** copy of this software and associated documentation files (the "Software"),
7+
** to deal in the Software without restriction, including without limitation
8+
** the rights to use, copy, modify, merge, publish, distribute, sublicense,
9+
** and/or sell copies of the Software, and to permit persons to whom the
10+
** Software is furnished to do so, subject to the following conditions:
11+
**
12+
** The above copyright notice and this permission notice shall be included in
13+
** all copies or substantial portions of the Software.
14+
**
15+
** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
** AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
** LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20+
** FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
21+
** DEALINGS IN THE SOFTWARE.
22+
*/
23+
24+
package com.lunarg.gfxreconstruct.replay;
25+
import java.util.List;
26+
import java.util.ArrayList;
27+
import android.app.NativeActivity;
28+
import android.os.Bundle;
29+
import android.widget.FrameLayout;
30+
import android.view.SurfaceView;
31+
import android.view.SurfaceHolder;
32+
import android.view.ViewGroup.LayoutParams;
33+
import android.view.Surface;
34+
import android.util.Log;
35+
import android.content.Context;
36+
import android.view.View;
37+
38+
public class ReplayActivity extends NativeActivity
39+
{
40+
private FrameLayout mFrameLayout;
41+
private static final String TAG = "gfxrecon";
42+
private Surface mSurface;
43+
public native void setSurface(Surface surface);
44+
private List<VKSurfaceView> mSurfaceviewList = new ArrayList<VKSurfaceView>();
45+
46+
@Override protected void onCreate(Bundle savedInstanceState)
47+
{
48+
super.onCreate(savedInstanceState);
49+
50+
// Create a FrameLayout to hold SurfaceView instances
51+
mFrameLayout = new FrameLayout(this);
52+
setContentView(mFrameLayout);
53+
54+
System.loadLibrary("gfxrecon-replay");
55+
}
56+
57+
private class VKSurfaceView extends SurfaceView implements SurfaceHolder.Callback
58+
{
59+
private int width;
60+
private int height;
61+
62+
public VKSurfaceView(Context context, int w, int h)
63+
{
64+
super(context);
65+
66+
width = w;
67+
height = h;
68+
69+
SurfaceHolder holder = getHolder();
70+
holder.addCallback(this);
71+
}
72+
73+
@Override public void surfaceCreated(SurfaceHolder holder)
74+
{
75+
mSurface = holder.getSurface();
76+
Log.i(TAG, "SurfaceHolder.Callback: surfaceCreated:" + mSurface);
77+
}
78+
79+
@Override public void surfaceDestroyed(SurfaceHolder holder)
80+
{
81+
mSurface = holder.getSurface();
82+
Log.i(TAG, "SurfaceHolder.Callback: surfaceDestroyed:" + mSurface);
83+
}
84+
85+
@Override public void surfaceChanged(SurfaceHolder holder, int format, int w, int h)
86+
{
87+
mSurface = holder.getSurface();
88+
Log.i(TAG, "SurfaceHolder.Callback: surfaceChanged:" + mSurface + " " + w + " " + h);
89+
}
90+
}
91+
92+
public void addNewView(int width, int height)
93+
{
94+
// Create a new SurfaceView
95+
final Context context = this;
96+
final int wid = width;
97+
final int hei = height;
98+
mSurface = null;
99+
runOnUiThread(new Runnable() {
100+
@Override public void run()
101+
{
102+
System.loadLibrary("gfxrecon-replay");
103+
VKSurfaceView newSurfaceView = new VKSurfaceView(context, wid, hei);
104+
mFrameLayout.addView(newSurfaceView,
105+
new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
106+
Log.i(TAG,
107+
"Create a new surface view:"
108+
+ " width:" + wid + " height:" + hei);
109+
mSurfaceviewList.add(newSurfaceView);
110+
}
111+
});
112+
while (mSurface == null)
113+
{
114+
try
115+
{
116+
Thread.sleep(100);
117+
}
118+
catch (Exception e)
119+
{
120+
Log.w(TAG, "Create new surface failed");
121+
e.printStackTrace();
122+
}
123+
}
124+
setSurface(mSurface);
125+
}
126+
127+
private void removeOneView(int surface_idx)
128+
{
129+
final int sur_idx = surface_idx;
130+
runOnUiThread(new Runnable() {
131+
@Override public void run()
132+
{
133+
if (mFrameLayout != null)
134+
{
135+
Log.i(TAG, "Remove one view");
136+
mFrameLayout.removeView(mSurfaceviewList.get(sur_idx));
137+
}
138+
else
139+
{
140+
Log.w(TAG, "View container has been destroyed!");
141+
}
142+
}
143+
});
144+
}
145+
}

framework/application/android_context.cpp

+38
Original file line numberDiff line numberDiff line change
@@ -108,5 +108,43 @@ void AndroidContext::SetOrientation(ScreenOrientation orientation)
108108
}
109109
}
110110

111+
void AndroidContext::requestNativeWindow(int width, int height)
112+
{
113+
JavaVM* jni_vm = nullptr;
114+
jobject jni_activity = nullptr;
115+
JNIEnv* env = nullptr;
116+
if ((android_app_ != nullptr) && (android_app_->activity != nullptr))
117+
{
118+
jni_vm = android_app_->activity->vm;
119+
jni_activity = android_app_->activity->clazz;
120+
}
121+
if ((jni_vm != nullptr) && (jni_activity != 0) && (jni_vm->AttachCurrentThread(&env, nullptr) == JNI_OK))
122+
{
123+
jclass object_class = env->GetObjectClass(jni_activity);
124+
jmethodID createsufaceview = env->GetMethodID(object_class, "addNewView", "(II)V");
125+
env->CallVoidMethod(jni_activity, createsufaceview, width, height);
126+
jni_vm->DetachCurrentThread();
127+
}
128+
}
129+
130+
void AndroidContext::destroyNativeWindow(int window_index)
131+
{
132+
JavaVM* jni_vm = nullptr;
133+
jobject jni_activity = nullptr;
134+
JNIEnv* env = nullptr;
135+
if ((android_app_ != nullptr) && (android_app_->activity != nullptr))
136+
{
137+
jni_vm = android_app_->activity->vm;
138+
jni_activity = android_app_->activity->clazz;
139+
}
140+
if ((jni_vm != nullptr) && (jni_activity != 0) && (jni_vm->AttachCurrentThread(&env, nullptr) == JNI_OK))
141+
{
142+
jclass object_class = env->GetObjectClass(jni_activity);
143+
jmethodID removesurfaceview = env->GetMethodID(object_class, "removeOneView", "(I)V");
144+
env->CallVoidMethod(jni_activity, removesurfaceview, window_index);
145+
jni_vm->DetachCurrentThread();
146+
}
147+
}
148+
111149
GFXRECON_END_NAMESPACE(application)
112150
GFXRECON_END_NAMESPACE(gfxrecon)

framework/application/android_context.h

+4
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ class AndroidContext : public WsiContext
5959

6060
void SetOrientation(ScreenOrientation orientation);
6161

62+
void requestNativeWindow(int width, int height);
63+
64+
void destroyNativeWindow(int window_index);
65+
6266
private:
6367
std::unique_ptr<AndroidWindow> window_{};
6468
struct android_app* android_app_{};

framework/application/android_jni.cpp

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
** Copyright (c) 2025 LunarG, Inc.
3+
** Copyright (c) 2025 Arm Limited and/or its affiliates <[email protected]>
4+
**
5+
** Permission is hereby granted, free of charge, to any person obtaining a
6+
** copy of this software and associated documentation files (the "Software"),
7+
** to deal in the Software without restriction, including without limitation
8+
** the rights to use, copy, modify, merge, publish, distribute, sublicense,
9+
** and/or sell copies of the Software, and to permit persons to whom the
10+
** Software is furnished to do so, subject to the following conditions:
11+
**
12+
** The above copyright notice and this permission notice shall be included in
13+
** all copies or substantial portions of the Software.
14+
**
15+
** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
** AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
** LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20+
** FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
21+
** DEALINGS IN THE SOFTWARE.
22+
*/
23+
24+
#include <jni.h>
25+
#include <android/log.h>
26+
#include "application/android_window.h"
27+
#include <android/native_window_jni.h>
28+
#include "util/logging.h"
29+
30+
extern "C" JNIEXPORT void JNICALL Java_com_lunarg_gfxreconstruct_replay_ReplayActivity_setSurface(JNIEnv* env,
31+
jobject obj,
32+
jobject surface)
33+
{
34+
gfxrecon::application::tmp_window = ANativeWindow_fromSurface(env, surface);
35+
GFXRECON_LOG_INFO("Created new window %p", gfxrecon::application::tmp_window);
36+
}

framework/application/android_window.cpp

+16-3
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535

3636
GFXRECON_BEGIN_NAMESPACE(gfxrecon)
3737
GFXRECON_BEGIN_NAMESPACE(application)
38+
ANativeWindow* tmp_window = nullptr;
3839

3940
AndroidWindow::AndroidWindow(AndroidContext* android_context, ANativeWindow* window) :
4041
android_context_(android_context), window_(window), width_(0), height_(0), pre_transform_(0)
@@ -158,13 +159,25 @@ decode::Window* AndroidWindowFactory::Create(
158159
GFXRECON_UNREFERENCED_PARAMETER(height);
159160
GFXRECON_UNREFERENCED_PARAMETER(force_windowed);
160161

161-
return android_context_->GetWindow();
162+
tmp_window = nullptr;
163+
android_context_->requestNativeWindow(width, height);
164+
AndroidWindow* tmpwin = nullptr;
165+
if (tmp_window != nullptr)
166+
{
167+
tmpwin = new AndroidWindow(android_context_, tmp_window);
168+
GFXRECON_LOG_INFO("Got android window %p", tmp_window);
169+
}
170+
else
171+
{
172+
GFXRECON_LOG_WARNING("Get android window failed");
173+
}
174+
return tmpwin;
162175
}
163176

164177
void AndroidWindowFactory::Destroy(decode::Window* window)
165178
{
166-
// Android currently has a single window whose lifetime is managed by AndroidContext.
167-
GFXRECON_UNREFERENCED_PARAMETER(window);
179+
int32_t windowidx = created_window_.at(window);
180+
android_context_->destroyNativeWindow(windowidx);
168181
}
169182

170183
VkBool32 AndroidWindowFactory::GetPhysicalDevicePresentationSupport(const encode::VulkanInstanceTable* table,

framework/application/android_window.h

+2
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,11 @@
3131
#include "util/platform.h"
3232

3333
#include <android_native_app_glue.h>
34+
#include <jni.h>
3435

3536
GFXRECON_BEGIN_NAMESPACE(gfxrecon)
3637
GFXRECON_BEGIN_NAMESPACE(application)
38+
extern ANativeWindow* tmp_window;
3739

3840
class AndroidWindow : public decode::Window
3941
{

framework/decode/vulkan_swapchain.cpp

+3
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,9 @@ VkResult VulkanSwapchain::CreateSurface(VkResult orig
9696
return VK_ERROR_UNKNOWN;
9797
}
9898

99+
window_factory->created_window_.emplace(window, window_index_);
100+
++window_index_;
101+
99102
result = window->CreateSurface(instance_table_, instance, flags, replay_surface);
100103

101104
if ((result == VK_SUCCESS) && (replay_surface != nullptr))

framework/decode/vulkan_swapchain.h

+1
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ class VulkanSwapchain
182182
application::Application* application_{ nullptr };
183183
ActiveWindows active_windows_;
184184
int32_t create_surface_count_{ 0 };
185+
int32_t window_index_{ 0 };
185186
VulkanSwapchainOptions swapchain_options_;
186187
};
187188

framework/decode/window.h

+3
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
#include "vulkan/vulkan.h"
3232

3333
#include <string>
34+
#include <unordered_map>
3435

3536
GFXRECON_BEGIN_NAMESPACE(gfxrecon)
3637
GFXRECON_BEGIN_NAMESPACE(decode)
@@ -108,6 +109,8 @@ class WindowFactory
108109
virtual VkBool32 GetPhysicalDevicePresentationSupport(const encode::VulkanInstanceTable* table,
109110
VkPhysicalDevice physical_device,
110111
uint32_t queue_family_index) = 0;
112+
113+
std::unordered_map<Window*, int32_t> created_window_;
111114
};
112115

113116
GFXRECON_END_NAMESPACE(decode)

0 commit comments

Comments
 (0)