add horizontal freedom and reset options to positional menu

This commit is contained in:
amwatson 2024-03-25 02:54:47 -05:00
parent 5fe11a8be5
commit 3f90b3cc6c
10 changed files with 133 additions and 69 deletions

View file

@ -434,10 +434,6 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
override fun onResume() {
super.onResume()
Choreographer.getInstance().postFrameCallback(this)
if (NativeLibrary.isRunning()) {
NativeLibrary.unPauseEmulation()
return
}
if (DirectoryInitialization.areCitraDirectoriesReady()) {
emulationState.run(emulationActivity.isActivityRecreated)
@ -447,9 +443,6 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
}
override fun onPause() {
if (NativeLibrary.isRunning()) {
emulationState.pause()
}
Choreographer.getInstance().removeFrameCallback(this)
super.onPause()
}

View file

@ -3,6 +3,7 @@ package org.citra.citra_emu.vr.ui
import android.view.KeyEvent
import android.view.View
import android.widget.Button
import android.widget.ToggleButton
import org.citra.citra_emu.NativeLibrary
import org.citra.citra_emu.R
import org.citra.citra_emu.vr.VrActivity
@ -11,14 +12,16 @@ import org.citra.citra_emu.vr.utils.VrMessageQueue
class VrRibbonLayer(activity: VrActivity) : VrUILayer(activity, R.layout.vr_ribbon) {
enum class MenuType(val resId: Int) {
MAIN( R.id.main_panel),
MAIN(R.id.main_panel),
POSITION(R.id.position_panel)
}
private var menuTypeCurrent : MenuType = MenuType.MAIN
override fun onSurfaceCreated() {
private var menuTypeCurrent: MenuType = MenuType.MAIN
override fun onSurfaceCreated() {
super.onSurfaceCreated()
initializeMainView()
initializeMainPanel()
initializePositionalPanel()
}
fun switchMenus(menuTypeNew: MenuType) {
@ -33,19 +36,37 @@ class VrRibbonLayer(activity: VrActivity) : VrUILayer(activity, R.layout.vr_ribb
VrMessageQueue.post(VrMessageQueue.MessageType.CHANGE_LOWER_MENU, 1)
}
fun initializeMainView() {
private fun initializePositionalPanel() {
val horizontalLockToggle = window?.findViewById<ToggleButton>(R.id.horizontalAxisToggle)
horizontalLockToggle?.setOnCheckedChangeListener { _, isChecked ->
VrMessageQueue.post(VrMessageQueue.MessageType.CHANGE_LOCK_HORIZONTAL_AXIS, if (isChecked) 1 else 0)
}
val btnReset = window?.findViewById<Button>(R.id.btnReset)
btnReset?.setOnClickListener { _ ->
VrMessageQueue.post(VrMessageQueue.MessageType.RESET_PANEL_POSITIONS, 0)
false
}
horizontalLockToggle?.isChecked = true;
VrMessageQueue.post(VrMessageQueue.MessageType.CHANGE_LOCK_HORIZONTAL_AXIS, if (horizontalLockToggle?.isChecked == true) 1 else 0)
}
private fun initializeMainPanel() {
window?.findViewById<Button>(R.id.buttonSelect)?.setOnTouchListener { _, motionEvent ->
val action: Int = when (motionEvent.action) {
KeyEvent.ACTION_DOWN -> {
// Normal key events.
NativeLibrary.ButtonState.PRESSED
}
KeyEvent.ACTION_UP -> NativeLibrary.ButtonState.RELEASED
else -> return@setOnTouchListener false
}
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice,
NativeLibrary.onGamePadEvent(
NativeLibrary.TouchScreenDevice,
NativeLibrary.ButtonType.BUTTON_SELECT,
action)
action
)
false
}
window?.findViewById<Button>(R.id.buttonStart)?.setOnTouchListener { _, motionEvent ->
@ -54,12 +75,15 @@ class VrRibbonLayer(activity: VrActivity) : VrUILayer(activity, R.layout.vr_ribb
// Normal key events.
NativeLibrary.ButtonState.PRESSED
}
KeyEvent.ACTION_UP -> NativeLibrary.ButtonState.RELEASED
else -> return@setOnTouchListener false
}
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice,
NativeLibrary.onGamePadEvent(
NativeLibrary.TouchScreenDevice,
NativeLibrary.ButtonType.BUTTON_START,
action)
action
)
false
}
window?.findViewById<Button>(R.id.buttonExit)?.setOnTouchListener { _, motionEvent ->
@ -69,7 +93,7 @@ class VrRibbonLayer(activity: VrActivity) : VrUILayer(activity, R.layout.vr_ribb
val btnNext = window?.findViewById<Button>(R.id.buttonNextMenu)
val btnPrev = window?.findViewById<Button>(R.id.buttonPrevMenu)
btnNext?.setOnClickListener{ _ ->
btnNext?.setOnClickListener { _ ->
val nextIdx = (menuTypeCurrent.ordinal + 1) % MenuType.values().size
switchMenus(MenuType.values()[nextIdx])
if ((nextIdx + 1) >= MenuType.values().size)
@ -79,7 +103,8 @@ class VrRibbonLayer(activity: VrActivity) : VrUILayer(activity, R.layout.vr_ribb
}
btnPrev?.setOnClickListener { _ ->
val prevIdx = (menuTypeCurrent.ordinal - 1 + MenuType.values().size) % MenuType.values().size
val prevIdx =
(menuTypeCurrent.ordinal - 1 + MenuType.values().size) % MenuType.values().size
switchMenus(MenuType.values()[prevIdx])
if ((prevIdx - 1) <= 0)
btnPrev.visibility = View.INVISIBLE

View file

@ -6,7 +6,9 @@ object VrMessageQueue {
SHOW_KEYBOARD(0),
SHOW_ERROR_MESSAGE(1),
EXIT_NEEDED(2),
CHANGE_LOWER_MENU(3)
CHANGE_LOWER_MENU(3),
CHANGE_LOCK_HORIZONTAL_AXIS(4),
RESET_PANEL_POSITIONS(5)
}
@JvmStatic
fun post(messageType: MessageType, payload: Long) {

View file

@ -419,25 +419,23 @@ bool GameSurfaceLayer::GetRayIntersectionWithPanel(const XrVector3f& start,
void GameSurfaceLayer::SetTopPanelFromController(const XrVector3f& controllerPosition) {
static constexpr XrVector3f viewerPosition{0.0f, 0.0f, 0.0f}; // Set viewer position
const float sphereRadius = XrMath::Vector3f::Length(
mTopPanel.mPanelFromWorld.position - viewerPosition); // Set the initial distance of the
constexpr XrVector3f viewerPosition{0.0f, 0.0f, 0.0f}; // Set viewer position
constexpr XrVector3f windowUpDirection{0.0f, 1.0f, 0.0f}; // Y is up
// window from the viewer
static constexpr XrVector3f windowUpDirection{0.0f, 1.0f, 0.0f}; // Y is up
const XrVector3f windowPosition =
// Set the initial distance of the window from the viewer.
const float sphereRadius =
XrMath::Vector3f::Length(mTopPanel.mPanelFromWorld.position - viewerPosition);
XrVector3f windowPosition =
CalculatePanelPosition(viewerPosition, controllerPosition, sphereRadius);
const XrQuaternionf windowRotation =
CalculatePanelRotation(windowPosition, viewerPosition, windowUpDirection);
if (windowPosition.z >= -0.5f) { return; }
if (XrMath::Vector3f::LengthSq(windowPosition - mLowerPanel.mPanelFromWorld.position) <
XrMath::Vector3f::LengthSq(mTopPanel.mInitialPose.position -
mLowerPanel.mInitialPose.position)) {
return;
}
if (XrMath::Quatf::GetPitchInRadians(windowRotation) > MATH_FLOAT_PI / 3.0f) { return; }
const XrQuaternionf windowRotation =
CalculatePanelRotation(windowPosition, viewerPosition, windowUpDirection);
mTopPanel.mPanelFromWorld = XrPosef{windowRotation, windowPosition};
}
@ -525,6 +523,12 @@ XrPosef GameSurfaceLayer::GetTopPanelFromHeadPose(uint32_t eye, const XrPosef& h
return XrPosef{headPose.orientation, panelPosition};
}
void GameSurfaceLayer::ResetPanelPositions() {
mTopPanel.mPanelFromWorld = mTopPanel.mInitialPose;
mLowerPanel.mPanelFromWorld = mLowerPanel.mInitialPose;
}
// based on thumbstick, modify the depth of the top panel
// Next error code: -2
int32_t GameSurfaceLayer::Init(const XrSession& session, const jobject activityObject) {

View file

@ -170,10 +170,10 @@ public:
XrPosef& result3d) const;
void SetTopPanelFromController(const XrVector3f& controllerPosition);
void SetTopPanelFromThumbstick(const float thumbstickY);
XrPosef SetLowerPanelFromThumbstick(const float thumbstickY);
void SetLowerPanelFromController(const XrVector3f& controllerPosition);
XrPosef GetTopPanelFromHeadPose(uint32_t eye, const XrPosef& headPose);
const XrPosef& GetLowerPanelPose() const { return mLowerPanel.mPanelFromWorld; }
void ResetPanelPositions();
private:
int Init(const XrSession& session, const jobject activityObject);

View file

@ -18,6 +18,11 @@ License : Licensed under GPLv3 or any later version.
#define NUM_EYES 2 // this will never change, it just helps people know what we mean.
#endif
// closest (to view origin) z-depth to put a panel
#ifndef Z_MAX
#define Z_MAX -0.5f
#endif
#define BAIL_ON_COND(cond, errorStr, returnCode) \
do { \
if (cond) { \

View file

@ -31,7 +31,9 @@ struct Message {
SHOW_KEYBOARD = 0, // payload 0 = hide keyboard, 1 = show keyboard
SHOW_ERROR_MESSAGE = 1, // payload 0 = show error message, 1 = hide error message
EXIT_NEEDED = 2, // payload ignored
CHANGE_LOWER_MENU = 3 // payload indicates menu ID
CHANGE_LOWER_MENU = 3, // payload indicates menu ID
CHANGE_LOCK_HORIZONTAL_AXIS = 4, // payload 0 = unlock, 1 = lock
RESET_PANEL_POSITIONS = 5, // payload ignored
};
Message() {}

View file

@ -90,6 +90,10 @@ public:
static XrVector3f Cross(const XrVector3f& a, const XrVector3f& b) {
return XrVector3f{a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, a.x * b.y - a.y * b.x};
}
static float Dot(const XrVector3f& a, const XrVector3f& b) {
return a.x * b.x + a.y * b.y + a.z * b.z;
}
};
class Matrixf {

View file

@ -229,6 +229,12 @@ public:
if (appState.mIsXrSessionActive) {
Frame(jni, appState);
} else {
// FIXME: currently, the way this is set up, some messages can be discarded if they
// aren't processed on the next frame.
// This currently requires that all AppState state-related events be handled in
// HandleStateChanges. and not in the frame for 100% correctness (consequence is
// losing messages on unmount). This is possibly solved by handling MessageQueue
// events inside the frame call.
// TODO should block here
mFrameIndex = 0;
}
@ -741,7 +747,8 @@ private:
appState.mLowerMenuType == LowerMenuType::POSITIONAL_MENU &&
triggerState.currentState) {
mGameSurfaceLayer->SetLowerPanelFromController(
XrVector3f{0, cursorPose3d.position.y, cursorPose3d.position.z});
{appState.mIsHorizontalAxisLocked ? 0.0f : cursorPose3d.position.x,
cursorPose3d.position.y, cursorPose3d.position.z});
sIsLowerPanelBeingPositioned = true;
} else if (appState.mLowerMenuType == LowerMenuType::MAIN_MENU) {
@ -759,7 +766,7 @@ private:
}
}
// 5. Hit test the top panel if positional menu is active.
// 5. Top panel (only if positional menu is active)
if (!shouldRenderCursor &&
appState.mLowerMenuType == LowerMenuType::POSITIONAL_MENU) {
shouldRenderCursor = mGameSurfaceLayer->GetRayIntersectionWithPanelTopPanel(
@ -769,8 +776,9 @@ private:
if (shouldRenderCursor && triggerState.currentState) {
// null out X component -- screen should stay
// center
mGameSurfaceLayer->SetTopPanelFromController(
XrVector3f{0, cursorPose3d.position.y, cursorPose3d.position.z});
mGameSurfaceLayer->SetTopPanelFromController(XrVector3f{
appState.mIsHorizontalAxisLocked ? 0.0f : cursorPose3d.position.x,
cursorPose3d.position.y, cursorPose3d.position.z});
// If trigger is pressed, thumbstick controls
// the depth
const XrActionStateVector2f& thumbstickState =
@ -853,19 +861,36 @@ private:
}
void HandleStateChanges(JNIEnv* jni, AppState& newState) const {
const bool shouldPauseEmulation = !newState.mHasFocus || newState.mShouldShowErrorMessage ||
newState.mLowerMenuType == LowerMenuType::POSITIONAL_MENU;
if (shouldPauseEmulation != mLastAppState.mIsEmulationPaused) {
ALOGI("State change: Emulation paused: {} -> {}", mLastAppState.mIsEmulationPaused,
newState.mIsEmulationPaused);
if (shouldPauseEmulation) {
PauseEmulation(jni);
newState.mIsEmulationPaused = true;
} else {
ResumeEmulation(jni);
newState.mIsEmulationPaused = false;
{
const bool shouldPauseEmulation =
!newState.mHasFocus || newState.mShouldShowErrorMessage ||
newState.mLowerMenuType == LowerMenuType::POSITIONAL_MENU;
if (shouldPauseEmulation != mLastAppState.mIsEmulationPaused) {
ALOGI("State change: Emulation paused: {} -> {} (F={}, E={}, MP={})",
mLastAppState.mIsEmulationPaused, shouldPauseEmulation, newState.mHasFocus,
newState.mShouldShowErrorMessage,
newState.mLowerMenuType == LowerMenuType::POSITIONAL_MENU ? "P"
: newState.mLowerMenuType == LowerMenuType::MAIN_MENU ? "M"
: "U");
if (shouldPauseEmulation) {
PauseEmulation(jni);
newState.mIsEmulationPaused = true;
} else {
ResumeEmulation(jni);
newState.mIsEmulationPaused = false;
}
}
}
if (newState.mNumPanelResets > mLastAppState.mNumPanelResets) {
mGameSurfaceLayer->ResetPanelPositions();
mRibbonLayer->SetPanelWithPose(mGameSurfaceLayer->GetLowerPanelPose());
}
if (newState.mIsHorizontalAxisLocked && !mLastAppState.mIsHorizontalAxisLocked) {
mGameSurfaceLayer->ResetPanelPositions();
mRibbonLayer->SetPanelWithPose(mGameSurfaceLayer->GetLowerPanelPose());
}
}
void OXRPollEvents(JNIEnv* jni, AppState& newAppState) const {
@ -1073,12 +1098,23 @@ private:
}
case Message::Type::CHANGE_LOWER_MENU: {
newAppState.mLowerMenuType = static_cast<LowerMenuType>(message.mPayload);
ALOGI("Received CHANGE_LOWER_MENU message: {}, state change {} -> {}",
ALOGD("Received CHANGE_LOWER_MENU message: {}, state change {} -> {}",
message.mPayload, mLastAppState.mLowerMenuType,
newAppState.mLowerMenuType);
break;
}
case Message::Type::CHANGE_LOCK_HORIZONTAL_AXIS: {
ALOGD("Received CHANGE_LOCK_HORIZONTAL_AXIS message: {}, state change {} -> {}",
message.mPayload, mLastAppState.mIsHorizontalAxisLocked,
message.mPayload == 1);
newAppState.mIsHorizontalAxisLocked = message.mPayload == 1;
break;
}
case Message::Type::RESET_PANEL_POSITIONS: {
ALOGD("Received RESET_PANEL_POSITIONS message");
newAppState.mNumPanelResets++;
break;
}
default:
ALOGE("Unknown message type: %d", message.mType);
break;
@ -1102,7 +1138,9 @@ private:
class AppState {
public:
LowerMenuType mLowerMenuType = LowerMenuType::MAIN_MENU;
LowerMenuType mLowerMenuType = LowerMenuType::MAIN_MENU;
int32_t mNumPanelResets = 0;
bool mIsHorizontalAxisLocked = true;
bool mIsKeyboardActive = false;
bool mShouldShowErrorMessage = false;

View file

@ -3,7 +3,9 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/position_panel"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
android:layout_height="wrap_content"
android:theme="@style/Theme.Material3.DayNight"
>
<FrameLayout
android:layout_width="1000dp"
@ -35,35 +37,24 @@
app:layout_constraintTop_toTopOf="@+id/lowerPanelPlaceHolder">
<Button
android:id="@+id/buttonSelect"
android:id="@+id/btnReset"
style="@style/VrRibbonButtonStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:text="@string/button_select" />
android:text="Reset" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<Button
android:id="@+id/buttonStart"
style="@style/VrRibbonButtonStyle"
android:layout_width="match_parent"
<ToggleButton
android:id="@+id/horizontalAxisToggle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/button_start" />
android:text="Lock Horizontal Axis"
/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<Button
android:id="@+id/buttonExit"
style="@style/VrRibbonButtonStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/emulation_close_game" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>