add VRUILayer [java] class

This commit is contained in:
amwatson 2024-02-08 19:48:56 -06:00
parent 12429f1ab5
commit 043f57ec8c
4 changed files with 193 additions and 0 deletions

View file

@ -25,3 +25,4 @@
-dontwarn java.beans.VetoableChangeSupport
-keep class org.citra.citra_emu.vr.GameSurfaceLayer { *; }
-keep class org.citra.citra_emu.vr.VrActivity { *; }
-keep class org.citra.citra_emu.vr.ui.VrUILayer { *; }

View file

@ -0,0 +1,171 @@
package org.citra.citra_emu.vr.ui
import android.app.Presentation
import android.content.Context
import android.graphics.Bitmap
import android.graphics.drawable.ColorDrawable
import android.hardware.display.DisplayManager
import android.hardware.display.VirtualDisplay
import android.os.SystemClock
import android.util.DisplayMetrics
import android.view.InputDevice
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.MotionEvent.PointerCoords
import android.view.MotionEvent.PointerProperties
import android.view.Surface
import android.view.View
import android.view.Window
import android.view.WindowManager
import org.citra.citra_emu.R
import org.citra.citra_emu.utils.Log
import org.citra.citra_emu.vr.VrActivity
import java.io.File
import java.io.FileOutputStream
import kotlin.math.roundToInt
/*
* This class populates an "SwapchainAndroidSurfaceKHR" with the
* contents of a secondary virtual display. It allows for smooth animations,
* but the perf doesn't scale well with texture size and it doesn't support mip
*levels. Therefore, it is important to set the the display size of the texture
*(using resource sizes, display density and the native density constant) to be
*something that's close enough to 1:1 texels:pixels so as to not require mips.
**/
abstract class VrUILayer(
val activity: VrActivity,
densityDpi: Int = DEFAULT_DENSITY.toInt(),
private val layoutId: Int = R.layout.vr_keyboard
) {
private val requestedDensity: Float = densityDpi.toFloat()
private var virtualDisplay: VirtualDisplay? = null
private var presentation: Presentation? = null
val window: Window?
get() = presentation!!.window
/// Called from JNI ////
fun getBoundsForView(handle: Long): Int {
val inflater = activity.getSystemService(
Context.LAYOUT_INFLATER_SERVICE
) as LayoutInflater
val contentView = inflater.inflate(layoutId, null, false)
if (contentView == null) {
Log.error( "Failed to inflate content view")
return -1
}
contentView.measure(
View.MeasureSpec.UNSPECIFIED,
View.MeasureSpec.UNSPECIFIED
)
val displayMetrics = activity.resources.displayMetrics
val widthDp = contentView.measuredWidth.toFloat() / displayMetrics.density /
(DEFAULT_DENSITY / requestedDensity)
val heightDp = contentView.measuredHeight.toFloat() / displayMetrics.density /
(DEFAULT_DENSITY / requestedDensity)
// roundToInt() matching the rounding Android uses to convert view dimensions to display units
nativeSetBounds(
handle, 0, 0, widthDp.roundToInt(),
heightDp.roundToInt()
)
return 0
}
private fun dispatchTouchEvent(x: Float, y: Float, action: Int) {
val eventTime = SystemClock.uptimeMillis()
MotionEvent.obtain(
eventTime, // Use the same timestamp for both downTime and eventTime
eventTime,
action,
1, // Only one pointer is used here
arrayOf(PointerProperties().apply {
id = 0
toolType = MotionEvent.TOOL_TYPE_FINGER
}),
arrayOf(PointerCoords().apply {
this.x = x
this.y = y
pressure = 1f
size = 1f
}),
0, 0, // MetaState and buttonState
1f, 1f, // Precision X and Y
0, 0, // Device ID and Edge Flags
InputDevice.SOURCE_TOUCHSCREEN,
0 // Flags
).apply {
presentation?.window?.decorView?.dispatchTouchEvent(this)
}
}
fun sendClickToUI(x: Float, y: Float, motionType: Int): Int {
val action = when (motionType) {
0 -> MotionEvent.ACTION_UP
1 -> MotionEvent.ACTION_DOWN
2 -> MotionEvent.ACTION_MOVE
else -> MotionEvent.ACTION_HOVER_ENTER
}
activity.runOnUiThread { dispatchTouchEvent(x, y, action) }
return 0
}
fun setSurface(
surface: Surface, widthDp: Int,
heightDp: Int
): Int {
activity.runOnUiThread { setSurface_(surface, widthDp, heightDp) }
return 0
}
private fun setSurface_(
surface: Surface, widthDp: Int,
heightDp: Int
) {
val flags = DisplayManager.VIRTUAL_DISPLAY_FLAG_PRESENTATION
val displayManager = activity.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
virtualDisplay = displayManager.createVirtualDisplay(
"CitraVR", widthDp, heightDp, requestedDensity.toInt(), surface,
flags
)
presentation = Presentation(activity.applicationContext, virtualDisplay!!.display).apply {
window?.setType(WindowManager.LayoutParams.TYPE_PRIVATE_PRESENTATION)
setContentView(layoutId)
// Sets the background to transparent. Remove to set background to black
// (useful for catching overrendering)
window?.setBackgroundDrawable(ColorDrawable(0))
show()
}
onSurfaceCreated()
}
protected fun onSurfaceCreated() {}
fun writeBitmapToDisk(bmp: Bitmap, outName: String?) {
val sdCard = activity.externalCacheDir
if (sdCard != null && outName != null) {
val file = File(sdCard.absolutePath, outName)
try {
FileOutputStream(file).use { out ->
bmp.compress(Bitmap.CompressFormat.PNG, 100, out)
}
} catch (e: Exception) {
Log.error("Failed to write bitmap to disk: ${e.message}")
}
}
}
companion object {
// DPI android uses as "1:1" with dp coordinates.
// AKA "baseline density"
private const val DEFAULT_DENSITY = DisplayMetrics.DENSITY_MEDIUM.toFloat()
private external fun nativeSetBounds(
handle: Long, leftInDp: Int, topInDp: Int,
rightInDp: Int, bottomInDp: Int
)
/*** Debug/Testing */
const val DEBUG_WRITE_VIEW_TO_DISK = false
}
}

View file

@ -96,6 +96,20 @@ struct BoundsHandle
};
};
//-----------------------------------------------------------------------------
// JNI functions
extern "C" JNIEXPORT void JNICALL
Java_org_citra_citra_1emu_vr_ui_UILayer_00024Companion_nativeSetBounds(
JNIEnv* env, jobject thiz, jlong handle, jint left, jint top, jint right,
jint bottom)
{
AndroidWindowBounds* b = BoundsHandle(handle).p;
b->mLeftInDp = left;
b->mTopInDp = top;
b->mRightInDp = right;
b->mBottomInDp = bottom;
}
//-----------------------------------------------------------------------------
// Local sysprops

View file

@ -17,6 +17,7 @@ License : Licensed under GPLv3 or any later version.
#include "layers/CursorLayer.h"
#include "layers/GameSurfaceLayer.h"
#include "layers/PassthroughLayer.h"
#include "layers/UILayer.h"
#include "vr_settings.h"
#include "utils/Common.h"
@ -277,6 +278,11 @@ public:
jni, mActivityObject, gOpenXr->mSession);
#endif
mUILayer = std::make_unique<UILayer>(
"org/citra/citra_emu/vr/ui/VrUILayer",
XrVector3f{0, 0.0f, -1.5f}, jni, mActivityObject, gOpenXr->mSession);
//////////////////////////////////////////////////
// Intialize JNI methods
//////////////////////////////////////////////////
@ -1036,6 +1042,7 @@ private:
#endif
std::unique_ptr<GameSurfaceLayer> mGameSurfaceLayer;
std::unique_ptr<PassthroughLayer> mPassthroughLayer;
std::unique_ptr<UILayer> mUILayer;
std::unique_ptr<InputStateStatic> mInputStateStatic;
InputStateFrame mInputStateFrame;