[UI] Android surface [skip appveyor]

This commit is contained in:
Triang3l 2022-02-01 22:18:04 +03:00
parent c6fc8f706a
commit 413d7ded49
14 changed files with 369 additions and 30 deletions

View File

@ -81,4 +81,8 @@ android {
path file('../../../build/xenia.wks.Android.mk')
}
}
}
dependencies {
implementation 'org.jetbrains:annotations:15.0'
}

View File

@ -29,7 +29,9 @@
android:supportsRtl="true"
android:theme="@android:style/Theme.Material.Light">
<activity android:name="jp.xenia.emulator.WindowDemoActivity">
<activity
android:name="jp.xenia.emulator.WindowDemoActivity"
android:label="@string/activity_label_window_demo">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />

View File

@ -7,15 +7,15 @@ public class XeniaRuntimeException extends RuntimeException {
public XeniaRuntimeException() {
}
public XeniaRuntimeException(String name) {
public XeniaRuntimeException(final String name) {
super(name);
}
public XeniaRuntimeException(String name, Throwable cause) {
public XeniaRuntimeException(final String name, final Throwable cause) {
super(name, cause);
}
public XeniaRuntimeException(Exception cause) {
public XeniaRuntimeException(final Exception cause) {
super(cause);
}
}

View File

@ -1,8 +1,18 @@
package jp.xenia.emulator;
import android.os.Bundle;
public class WindowDemoActivity extends WindowedAppActivity {
@Override
protected String getWindowedAppIdentifier() {
return "xenia_ui_window_vulkan_demo";
}
@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_window_demo);
setWindowSurfaceView(findViewById(R.id.window_demo_surface_view));
}
}

View File

@ -0,0 +1,42 @@
package jp.xenia.emulator;
import android.content.Context;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.view.SurfaceView;
public class WindowSurfaceView extends SurfaceView {
public WindowSurfaceView(final Context context) {
super(context);
// Native drawing is invoked from onDraw.
setWillNotDraw(false);
}
public WindowSurfaceView(final Context context, final AttributeSet attrs) {
super(context, attrs);
setWillNotDraw(false);
}
public WindowSurfaceView(
final Context context, final AttributeSet attrs, final int defStyleAttr) {
super(context, attrs, defStyleAttr);
setWillNotDraw(false);
}
public WindowSurfaceView(
final Context context, final AttributeSet attrs, final int defStyleAttr,
final int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
setWillNotDraw(false);
}
@Override
protected void onDraw(final Canvas canvas) {
final Context context = getContext();
if (!(context instanceof WindowedAppActivity)) {
return;
}
final WindowedAppActivity activity = (WindowedAppActivity) context;
activity.onWindowSurfaceDraw(false);
}
}

View File

@ -3,6 +3,11 @@ package jp.xenia.emulator;
import android.app.Activity;
import android.content.res.AssetManager;
import android.os.Bundle;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.View;
import org.jetbrains.annotations.Nullable;
import jp.xenia.XeniaRuntimeException;
@ -12,21 +17,87 @@ public abstract class WindowedAppActivity extends Activity {
System.loadLibrary("xenia-ui-window-vulkan-demo");
}
private long mAppContext;
private final WindowSurfaceOnLayoutChangeListener mWindowSurfaceOnLayoutChangeListener =
new WindowSurfaceOnLayoutChangeListener();
private final WindowSurfaceHolderCallback mWindowSurfaceHolderCallback =
new WindowSurfaceHolderCallback();
private native long initializeWindowedAppOnCreateNative(
// May be 0 while destroying (mainly while the superclass is).
private long mAppContext = 0;
@Nullable
private WindowSurfaceView mWindowSurfaceView = null;
private native long initializeWindowedAppOnCreate(
String windowedAppIdentifier, AssetManager assetManager);
private native void onDestroyNative(long appContext);
private native void onWindowSurfaceLayoutChange(
long appContext, int left, int top, int right, int bottom);
private native void onWindowSurfaceChanged(long appContext, Surface windowSurface);
private native void paintWindow(long appContext, boolean forcePaint);
protected abstract String getWindowedAppIdentifier();
protected void setWindowSurfaceView(@Nullable final WindowSurfaceView windowSurfaceView) {
if (mWindowSurfaceView == windowSurfaceView) {
return;
}
// Detach from the old surface.
if (mWindowSurfaceView != null) {
mWindowSurfaceView.getHolder().removeCallback(mWindowSurfaceHolderCallback);
mWindowSurfaceView.removeOnLayoutChangeListener(mWindowSurfaceOnLayoutChangeListener);
mWindowSurfaceView = null;
if (mAppContext != 0) {
onWindowSurfaceChanged(mAppContext, null);
}
}
if (windowSurfaceView == null) {
return;
}
mWindowSurfaceView = windowSurfaceView;
// The native window code assumes that, when the surface exists, it covers the entire
// window.
// FIXME(Triang3l): This doesn't work if the layout has already been performed.
mWindowSurfaceView.addOnLayoutChangeListener(mWindowSurfaceOnLayoutChangeListener);
final SurfaceHolder windowSurfaceHolder = mWindowSurfaceView.getHolder();
windowSurfaceHolder.addCallback(mWindowSurfaceHolderCallback);
// If setting after the creation of the surface.
if (mAppContext != 0) {
final Surface windowSurface = windowSurfaceHolder.getSurface();
if (windowSurface != null) {
onWindowSurfaceChanged(mAppContext, windowSurface);
}
}
}
public void onWindowSurfaceDraw(final boolean forcePaint) {
if (mAppContext == 0) {
return;
}
paintWindow(mAppContext, forcePaint);
}
// Used from the native WindowedAppContext. May be called from non-UI threads.
protected void postInvalidateWindowSurface() {
if (mWindowSurfaceView == null) {
return;
}
mWindowSurfaceView.postInvalidate();
}
@Override
protected void onCreate(Bundle savedInstanceState) {
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final String windowedAppIdentifier = getWindowedAppIdentifier();
mAppContext = initializeWindowedAppOnCreateNative(windowedAppIdentifier, getAssets());
mAppContext = initializeWindowedAppOnCreate(windowedAppIdentifier, getAssets());
if (mAppContext == 0) {
finish();
throw new XeniaRuntimeException(
@ -36,10 +107,54 @@ public abstract class WindowedAppActivity extends Activity {
@Override
protected void onDestroy() {
setWindowSurfaceView(null);
if (mAppContext != 0) {
onDestroyNative(mAppContext);
}
mAppContext = 0;
super.onDestroy();
}
private class WindowSurfaceOnLayoutChangeListener implements View.OnLayoutChangeListener {
@Override
public void onLayoutChange(
final View v, final int left, final int top, final int right, final int bottom,
final int oldLeft, final int oldTop, final int oldRight, final int oldBottom) {
if (mAppContext != 0) {
onWindowSurfaceLayoutChange(mAppContext, left, top, right, bottom);
}
}
}
private class WindowSurfaceHolderCallback implements SurfaceHolder.Callback2 {
@Override
public void surfaceCreated(final SurfaceHolder holder) {
if (mAppContext == 0) {
return;
}
onWindowSurfaceChanged(mAppContext, holder.getSurface());
}
@Override
public void surfaceChanged(
final SurfaceHolder holder, final int format, final int width, final int height) {
if (mAppContext == 0) {
return;
}
onWindowSurfaceChanged(mAppContext, holder.getSurface());
}
@Override
public void surfaceDestroyed(final SurfaceHolder holder) {
if (mAppContext == 0) {
return;
}
onWindowSurfaceChanged(mAppContext, null);
}
@Override
public void surfaceRedrawNeeded(final SurfaceHolder holder) {
onWindowSurfaceDraw(true);
}
}
}

View File

@ -1,8 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
<jp.xenia.emulator.WindowSurfaceView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/window_demo_surface_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="jp.xenia.emulator.WindowDemoActivity">
</RelativeLayout>
tools:context="jp.xenia.emulator.WindowDemoActivity" />

View File

@ -1,3 +1,4 @@
<resources>
<string name="app_name">Xenia</string>
<string name="activity_label_window_demo">Xenia Window Demo</string>
</resources>

View File

@ -26,13 +26,13 @@
// presenting from the thread refreshing the guest output is not absolutely
// necessary, but still may be nice for bypassing the scheduling and the
// message queue.
// On GTK, the frame rate of draw signals is limited to the display refresh rate
// internally, so for the lowest latency especially in case the refresh rates
// differ significantly on the guest and the host (like 30/60 Hz presented to
// 144 Hz), drawing from the guest output refreshing thread is highly desirable.
// Presenting directly from the GPU emulation thread also makes debugging GPU
// emulation easier with external tools, as presenting in most cases happens
// exactly between emulation frames.
// On Android and GTK, the frame rate of draw events is limited to the display
// refresh rate internally, so for the lowest latency especially in case the
// refresh rates differ significantly on the guest and the host (like 30/60 Hz
// presented to 144 Hz), drawing from the guest output refreshing thread is
// highly desirable. Presenting directly from the GPU emulation thread also
// makes debugging GPU emulation easier with external tools, as presenting in
// most cases happens exactly between emulation frames.
DEFINE_bool(
host_present_from_non_ui_thread, true,
"Allow the GPU emulation thread to present the guest output to the host "

View File

@ -974,8 +974,8 @@ class Presenter {
// frame rates wasting the CPU and the GPU resources and starving everything
// else. The waits performed here must be interruptible by guest output
// presentation requests to prevent adding arbitrary amounts of latency to it.
// On GTK, this is not needed, the frame rate of draw signals is limited to
// the display refresh rate internally.
// On Android and GTK, this is not needed, the frame rate of draw events is
// limited to the display refresh rate internally.
#if XE_PLATFORM_WIN32
static Microsoft::WRL::ComPtr<IDXGIOutput> GetDXGIOutputForMonitor(
IDXGIFactory1* factory, HMONITOR monitor);

View File

@ -29,17 +29,42 @@ std::unique_ptr<Window> Window::Create(WindowedAppContext& app_context,
AndroidWindow::~AndroidWindow() {
EnterDestructor();
AndroidWindowedAppContext& android_app_context =
auto& android_app_context =
static_cast<AndroidWindowedAppContext&>(app_context());
if (android_app_context.GetActivityWindow() == this) {
android_app_context.SetActivityWindow(nullptr);
}
}
void AndroidWindow::OnActivitySurfaceLayoutChange() {
auto& android_app_context =
static_cast<const AndroidWindowedAppContext&>(app_context());
assert_true(android_app_context.GetActivityWindow() == this);
uint32_t physical_width =
uint32_t(android_app_context.window_surface_layout_right() -
android_app_context.window_surface_layout_left());
uint32_t physical_height =
uint32_t(android_app_context.window_surface_layout_bottom() -
android_app_context.window_surface_layout_top());
OnDesiredLogicalSizeUpdate(SizeToLogical(physical_width),
SizeToLogical(physical_height));
WindowDestructionReceiver destruction_receiver(this);
OnActualSizeUpdate(physical_width, physical_height, destruction_receiver);
if (destruction_receiver.IsWindowDestroyedOrClosed()) {
return;
}
}
uint32_t AndroidWindow::GetLatestDpiImpl() const {
auto& android_app_context =
static_cast<const AndroidWindowedAppContext&>(app_context());
return android_app_context.GetPixelDensity();
}
bool AndroidWindow::OpenImpl() {
// The window is a proxy between the main activity and Xenia, so there can be
// only one for an activity.
AndroidWindowedAppContext& android_app_context =
// only one open window for an activity.
auto& android_app_context =
static_cast<AndroidWindowedAppContext&>(app_context());
AndroidWindow* previous_activity_window =
android_app_context.GetActivityWindow();
@ -50,6 +75,10 @@ bool AndroidWindow::OpenImpl() {
return false;
}
android_app_context.SetActivityWindow(this);
// Report the initial layout.
OnActivitySurfaceLayoutChange();
return true;
}
@ -68,7 +97,7 @@ void AndroidWindow::RequestCloseImpl() {
}
OnAfterClose();
AndroidWindowedAppContext& android_app_context =
auto& android_app_context =
static_cast<AndroidWindowedAppContext&>(app_context());
if (android_app_context.GetActivityWindow() == this) {
android_app_context.SetActivityWindow(nullptr);
@ -78,13 +107,24 @@ void AndroidWindow::RequestCloseImpl() {
std::unique_ptr<Surface> AndroidWindow::CreateSurfaceImpl(
Surface::TypeFlags allowed_types) {
if (allowed_types & Surface::kTypeFlag_AndroidNativeWindow) {
// TODO(Triang3l): AndroidNativeWindowSurface for the ANativeWindow.
auto& android_app_context =
static_cast<const AndroidWindowedAppContext&>(app_context());
assert_true(android_app_context.GetActivityWindow() == this);
ANativeWindow* activity_window_surface =
android_app_context.GetWindowSurface();
if (activity_window_surface) {
return std::make_unique<AndroidNativeWindowSurface>(
activity_window_surface);
}
}
return nullptr;
}
void AndroidWindow::RequestPaintImpl() {
// TODO(Triang3l): postInvalidate.
auto& android_app_context =
static_cast<AndroidWindowedAppContext&>(app_context());
assert_true(android_app_context.GetActivityWindow() == this);
android_app_context.PostInvalidateWindowSurface();
}
std::unique_ptr<ui::MenuItem> MenuItem::Create(Type type,

View File

@ -29,7 +29,13 @@ class AndroidWindow : public Window {
uint32_t GetMediumDpi() const override { return 160; }
void OnActivitySurfaceLayoutChange();
void OnActivitySurfaceChanged() { OnSurfaceChanged(true); }
void PaintActivitySurface(bool force_paint) { OnPaint(force_paint); }
protected:
uint32_t GetLatestDpiImpl() const override;
bool OpenImpl() override;
void RequestCloseImpl() override;

View File

@ -2,7 +2,7 @@
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2021 Ben Vanik. All rights reserved. *
* Copyright 2022 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
@ -13,6 +13,8 @@
#include <android/configuration.h>
#include <android/log.h>
#include <android/looper.h>
#include <android/native_window.h>
#include <android/native_window_jni.h>
#include <fcntl.h>
#include <jni.h>
#include <unistd.h>
@ -22,6 +24,7 @@
#include "xenia/base/assert.h"
#include "xenia/base/logging.h"
#include "xenia/base/main_android.h"
#include "xenia/ui/window_android.h"
#include "xenia/ui/windowed_app.h"
namespace xe {
@ -52,6 +55,16 @@ void AndroidWindowedAppContext::PlatformQuitFromUIThread() {
}
}
void AndroidWindowedAppContext::PostInvalidateWindowSurface() {
// May be called from non-UI threads.
JNIEnv* jni_env = GetAndroidThreadJniEnv();
if (!jni_env) {
return;
}
jni_env->CallVoidMethod(activity_,
activity_method_post_invalidate_window_surface_);
}
AndroidWindowedAppContext*
AndroidWindowedAppContext::JniActivityInitializeWindowedAppOnCreate(
JNIEnv* jni_env, jobject activity, jstring windowed_app_identifier,
@ -100,9 +113,54 @@ void AndroidWindowedAppContext::JniActivityOnDestroy() {
app_->InvokeOnDestroy();
app_.reset();
}
// Expecting that the destruction of the app will destroy the window as well,
// no need to notify it explicitly.
assert_null(activity_window_);
RequestDestruction();
}
void AndroidWindowedAppContext::JniActivityOnWindowSurfaceLayoutChange(
jint left, jint top, jint right, jint bottom) {
window_surface_layout_left_ = left;
window_surface_layout_top_ = top;
window_surface_layout_right_ = right;
window_surface_layout_bottom_ = bottom;
if (activity_window_) {
activity_window_->OnActivitySurfaceLayoutChange();
}
}
void AndroidWindowedAppContext::JniActivityOnWindowSurfaceChanged(
jobject window_surface_object) {
// Detach from the old surface.
if (window_surface_) {
ANativeWindow* old_window_surface = window_surface_;
window_surface_ = nullptr;
if (activity_window_) {
activity_window_->OnActivitySurfaceChanged();
}
ANativeWindow_release(old_window_surface);
}
if (!window_surface_object) {
return;
}
window_surface_ =
ANativeWindow_fromSurface(ui_thread_jni_env_, window_surface_object);
if (!window_surface_) {
return;
}
if (activity_window_) {
activity_window_->OnActivitySurfaceChanged();
}
}
void AndroidWindowedAppContext::JniActivityPaintWindow(bool force_paint) {
if (!activity_window_) {
return;
}
activity_window_->PaintActivitySurface(force_paint);
}
AndroidWindowedAppContext::~AndroidWindowedAppContext() { Shutdown(); }
bool AndroidWindowedAppContext::Initialize(JNIEnv* ui_thread_jni_env,
@ -205,6 +263,11 @@ bool AndroidWindowedAppContext::Initialize(JNIEnv* ui_thread_jni_env,
activity_ids_obtained &=
(activity_method_finish_ = ui_thread_jni_env_->GetMethodID(
activity_class_, "finish", "()V")) != nullptr;
activity_ids_obtained &=
(activity_method_post_invalidate_window_surface_ =
ui_thread_jni_env_->GetMethodID(
activity_class_, "postInvalidateWindowSurface", "()V")) !=
nullptr;
if (!activity_ids_obtained) {
XELOGE("AndroidWindowedAppContext: Failed to get the activity class IDs");
Shutdown();
@ -407,7 +470,7 @@ bool AndroidWindowedAppContext::InitializeApp(std::unique_ptr<WindowedApp> (
extern "C" {
JNIEXPORT jlong JNICALL
Java_jp_xenia_emulator_WindowedAppActivity_initializeWindowedAppOnCreateNative(
Java_jp_xenia_emulator_WindowedAppActivity_initializeWindowedAppOnCreate(
JNIEnv* jni_env, jobject activity, jstring windowed_app_identifier,
jobject asset_manager) {
return reinterpret_cast<jlong>(
@ -423,4 +486,27 @@ Java_jp_xenia_emulator_WindowedAppActivity_onDestroyNative(
->JniActivityOnDestroy();
}
JNIEXPORT void JNICALL
Java_jp_xenia_emulator_WindowedAppActivity_onWindowSurfaceLayoutChange(
JNIEnv* jni_env, jobject activity, jlong app_context_ptr, jint left,
jint top, jint right, jint bottom) {
reinterpret_cast<xe::ui::AndroidWindowedAppContext*>(app_context_ptr)
->JniActivityOnWindowSurfaceLayoutChange(left, top, right, bottom);
}
JNIEXPORT void JNICALL
Java_jp_xenia_emulator_WindowedAppActivity_onWindowSurfaceChanged(
JNIEnv* jni_env, jobject activity, jlong app_context_ptr,
jobject window_surface_object) {
reinterpret_cast<xe::ui::AndroidWindowedAppContext*>(app_context_ptr)
->JniActivityOnWindowSurfaceChanged(window_surface_object);
}
JNIEXPORT void JNICALL Java_jp_xenia_emulator_WindowedAppActivity_paintWindow(
JNIEnv* jni_env, jobject activity, jlong app_context_ptr,
jboolean force_paint) {
reinterpret_cast<xe::ui::AndroidWindowedAppContext*>(app_context_ptr)
->JniActivityPaintWindow(bool(force_paint));
}
} // extern "C"

View File

@ -2,7 +2,7 @@
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2021 Ben Vanik. All rights reserved. *
* Copyright 2022 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
@ -13,6 +13,7 @@
#include <android/asset_manager.h>
#include <android/configuration.h>
#include <android/looper.h>
#include <android/native_window.h>
#include <jni.h>
#include <array>
#include <memory>
@ -33,6 +34,27 @@ class AndroidWindowedAppContext final : public WindowedAppContext {
void PlatformQuitFromUIThread() override;
uint32_t GetPixelDensity() const {
return configuration_ ? uint32_t(AConfiguration_getDensity(configuration_))
: 160;
}
int32_t window_surface_layout_left() const {
return window_surface_layout_left_;
}
int32_t window_surface_layout_top() const {
return window_surface_layout_top_;
}
int32_t window_surface_layout_right() const {
return window_surface_layout_right_;
}
int32_t window_surface_layout_bottom() const {
return window_surface_layout_bottom_;
}
ANativeWindow* GetWindowSurface() const { return window_surface_; }
void PostInvalidateWindowSurface();
// The single Window instance that will be receiving window callbacks.
// Multiple windows cannot be created as one activity or fragment can have
// only one layout. This window acts purely as a proxy between the activity
@ -46,6 +68,10 @@ class AndroidWindowedAppContext final : public WindowedAppContext {
JNIEnv* jni_env, jobject activity, jstring windowed_app_identifier,
jobject asset_manager);
void JniActivityOnDestroy();
void JniActivityOnWindowSurfaceLayoutChange(jint left, jint top, jint right,
jint bottom);
void JniActivityOnWindowSurfaceChanged(jobject window_surface_object);
void JniActivityPaintWindow(bool force_paint);
private:
enum class UIThreadLooperCallbackCommand : uint8_t {
@ -93,6 +119,7 @@ class AndroidWindowedAppContext final : public WindowedAppContext {
jobject activity_ = nullptr;
jmethodID activity_method_finish_ = nullptr;
jmethodID activity_method_post_invalidate_window_surface_ = nullptr;
// May be read by non-UI threads in NotifyUILoopOfPendingFunctions.
ALooper* ui_thread_looper_ = nullptr;
@ -101,6 +128,13 @@ class AndroidWindowedAppContext final : public WindowedAppContext {
std::array<int, 2> ui_thread_looper_callback_pipe_{-1, -1};
bool ui_thread_looper_callback_registered_ = false;
int32_t window_surface_layout_left_ = 0;
int32_t window_surface_layout_top_ = 0;
int32_t window_surface_layout_right_ = 0;
int32_t window_surface_layout_bottom_ = 0;
ANativeWindow* window_surface_ = nullptr;
AndroidWindow* activity_window_ = nullptr;
std::unique_ptr<WindowedApp> app_;