Stale GL_UNPACK state in gl4es and a missing pre-release GPU sync caused diagonal-banded corruption in the 3D view. Synthetic key press/release must track the real controller button or touch down/up edges, not fire together in one call - fixes a typed movement key sticking forever, then a follow-up that had silently broken A-button jump and on-screen keyboard arrow keys. Also adds force-rebuild.sh for the known stageEngine stale-cache issue.
This commit is contained in:
@@ -347,6 +347,13 @@ the same as any other Android Studio project:
|
|||||||
this automatically otherwise, so plain Java/C++ edits under
|
this automatically otherwise, so plain Java/C++ edits under
|
||||||
`android/app/src/main/` just need another Run/Debug press.
|
`android/app/src/main/` just need another Run/Debug press.
|
||||||
|
|
||||||
|
**If a native change genuinely doesn't seem to take effect** even after a
|
||||||
|
normal rebuild - `stageEngine` can wrongly report `UP-TO-DATE` for a
|
||||||
|
newly-added file under `android/engine-patches/` - run
|
||||||
|
`android/force-rebuild.sh` and rebuild. For more visibility than Android
|
||||||
|
Studio's collapsed Build panel, run `./gradlew :app:assembleDebug
|
||||||
|
--console=plain` from a terminal instead.
|
||||||
|
|
||||||
### 5.4. Engine/gl4es patch reference
|
### 5.4. Engine/gl4es patch reference
|
||||||
|
|
||||||
`android/engine-patches/` (applied to a scratch copy of `engine/`) and
|
`android/engine-patches/` (applied to a scratch copy of `engine/`) and
|
||||||
@@ -397,7 +404,36 @@ each one does:
|
|||||||
hook: redirects `SDLDraw()`/`opengl_swap_and_restore()` to submit into
|
hook: redirects `SDLDraw()`/`opengl_swap_and_restore()` to submit into
|
||||||
the XR swapchain (via `xr_frame_begin()`/`xr_frame_end()`) instead of
|
the XR swapchain (via `xr_frame_begin()`/`xr_frame_end()`) instead of
|
||||||
the normal window, for both the GL-rendered 3D path and the plain
|
the normal window, for both the GL-rendered 3D path and the plain
|
||||||
software-composited path (splash screen/cutscenes/menus).
|
software-composited path (splash screen/cutscenes/menus). Also logs
|
||||||
|
once per transition between the two (`INFO("Game rendering active:
|
||||||
|
...")`) for on-device logcat diagnostics.
|
||||||
|
- **`12-android-texture-row-length.patch`** - `OpenGL.cc`'s wall/floor
|
||||||
|
texture upload (`opengl_cache_texture()`/`set_texture()`, only reachable
|
||||||
|
when `gShockPrefs.doUseOpenGL` is true - it defaults `false` in
|
||||||
|
`Prefs.c`, so this path is dormant on Quest by default) sets
|
||||||
|
`GL_UNPACK_ROW_LENGTH` from the original 8-bit source bitmap's row
|
||||||
|
stride (`bm->row`), but the buffer actually uploaded is a separately-
|
||||||
|
allocated RGBA32 conversion of it with its own, unrelated stride. Fixed
|
||||||
|
to use the converted buffer's own pitch instead.
|
||||||
|
- **`13-android-swapchain-sync.patch`** - `android_draw_surface_as_quad()`
|
||||||
|
used `glFlush()` after drawing into the swapchain's FBO, which queues
|
||||||
|
the GPU work without waiting for it to finish before
|
||||||
|
`xr_swapchain_release()` hands the image to the OpenXR runtime for
|
||||||
|
cross-process consumption. Changed to `glFinish()`, which blocks until
|
||||||
|
the write has landed. The same gap existed in `xr_overlay.c`'s
|
||||||
|
`xr_overlay_render_and_build_layer()` (the keyboard/menu overlay blit
|
||||||
|
path) and was fixed there directly, no patch needed since that file is
|
||||||
|
first-party.
|
||||||
|
- **`14-android-quad-unpack-state.patch`** - fixes the diagonal-banded/
|
||||||
|
skewed corruption in the 3D game view during real gameplay (see README
|
||||||
|
5.5.2). `android_draw_surface_as_quad()`'s `glTexImage2D` upload of the
|
||||||
|
tightly-packed `uiRgba` buffer relied on GL's default unpack state
|
||||||
|
(`GL_UNPACK_ROW_LENGTH`/`GL_UNPACK_ALIGNMENT` at 0/4) instead of setting
|
||||||
|
it explicitly. gl4es implements both client-side and persists whatever
|
||||||
|
was last set across draw calls in the same context, so this upload
|
||||||
|
inherited non-default unpack state left behind elsewhere in the frame
|
||||||
|
and misread every row. Fixed by resetting both to the packed defaults
|
||||||
|
immediately before this upload.
|
||||||
|
|
||||||
`android/gl4es-patches/`:
|
`android/gl4es-patches/`:
|
||||||
|
|
||||||
@@ -459,6 +495,33 @@ trigger (a single always-nonzero struct field) sits well below the
|
|||||||
project's own layer boundary and doesn't show up in any of the local GL
|
project's own layer boundary and doesn't show up in any of the local GL
|
||||||
state that the menu's own code controls.
|
state that the menu's own code controls.
|
||||||
|
|
||||||
|
#### 5.5.2. Diagonal-banded corruption in the 3D game view
|
||||||
|
|
||||||
|
During real 3D gameplay (not menus/splash), the game quad rendered as
|
||||||
|
diagonally-banded color noise. Every OpenXR overlay quad (menu, keyboard,
|
||||||
|
laser-beam pointers) and the desktop build's software renderer were
|
||||||
|
unaffected, narrowing the bug to the one present path gameplay uses that
|
||||||
|
those don't: `android_draw_surface_as_quad()` in `engine/src/MacSrc/
|
||||||
|
OpenGL.cc`.
|
||||||
|
|
||||||
|
**Root cause:** its `glTexImage2D` upload of the tightly-packed `uiRgba`
|
||||||
|
conversion buffer relies on GL's default unpack state
|
||||||
|
(`GL_UNPACK_ROW_LENGTH`/`GL_UNPACK_ALIGNMENT` at 0/4) instead of setting
|
||||||
|
it explicitly. gl4es - the GLES/EGL translation layer this engine's
|
||||||
|
desktop-style GL calls go through on Quest - implements both unpack
|
||||||
|
parameters in software, manually repacking the source buffer row-by-row
|
||||||
|
using whatever stride was last set via `glPixelStorei`, and that state
|
||||||
|
persists across draw calls in the same GL context rather than resetting
|
||||||
|
per call. During gameplay, this upload inherits non-default unpack state
|
||||||
|
left behind elsewhere in the frame and misreads every row - a diagonal
|
||||||
|
shear/banding pattern. Menus never trigger it because nothing in the
|
||||||
|
menu-only code paths sets a custom unpack stride first.
|
||||||
|
|
||||||
|
**Fix** (`14-android-quad-unpack-state.patch`): explicitly reset both
|
||||||
|
`GL_UNPACK_ROW_LENGTH` and `GL_UNPACK_ALIGNMENT` to their tightly-packed
|
||||||
|
values immediately before this `glTexImage2D` call, removing the
|
||||||
|
dependency on whatever GL state preceded it.
|
||||||
|
|
||||||
## 6. License
|
## 6. License
|
||||||
|
|
||||||
The original tooling in this repository (the Docker build image, build
|
The original tooling in this repository (the Docker build image, build
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
// needs, compiled into the same "main" library as engine/ (see
|
// needs, compiled into the same "main" library as engine/ (see
|
||||||
// android/engine-patches/01-android-shared-lib.patch's ANDROID_EXTRA_SOURCES)
|
// android/engine-patches/01-android-shared-lib.patch's ANDROID_EXTRA_SOURCES)
|
||||||
// - engine/ itself is never modified.
|
// - engine/ itself is never modified.
|
||||||
|
#include <ctype.h>
|
||||||
#include <jni.h>
|
#include <jni.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
@@ -20,17 +21,30 @@ Java_de_ladkau_questshock_QuestShockActivity_nativeChdir(JNIEnv *env, jclass cla
|
|||||||
}
|
}
|
||||||
|
|
||||||
// KeyboardOverlay's hand-built on-screen keyboard (see
|
// KeyboardOverlay's hand-built on-screen keyboard (see
|
||||||
// KeyboardOverlay.commitPrintableChar()) has no real Android IME session
|
// KeyboardOverlay.handleCharKeyDown()/handleCharKeyUp()) has no real
|
||||||
// behind it, so printable characters can't reach the game via Android's
|
// Android IME session behind it, so printable characters can't reach the
|
||||||
// usual IME path - instead this builds and pushes an SDL_TEXTINPUT event
|
// game via Android's usual IME path - instead this builds and pushes an
|
||||||
// directly, the same event type/shape SDL's own Android backend pushes for
|
// SDL_TEXTINPUT event directly, the same event type/shape SDL's own
|
||||||
// typed text, using only the public SDL_Event/SDL_PushEvent API. The
|
// Android backend pushes for typed text, using only the public
|
||||||
// engine's pump_events() (sdl_events.c) only picks up printable characters
|
// SDL_Event/SDL_PushEvent API. The engine's pump_events() (sdl_events.c)
|
||||||
// from this event, not from SDL_KEYDOWN (see handleKeyPress() in
|
// only picks up printable characters from this event, not from
|
||||||
// KeyboardOverlay.java for why).
|
// SDL_KEYDOWN.
|
||||||
|
//
|
||||||
|
// pump_events()'s SDL_TEXTINPUT handling sets sshockKeyStates[code] for
|
||||||
|
// this character and only ever clears it again on a matching SDL_KEYUP.
|
||||||
|
// Any character bound to a continuously-polled motion key (space -> jump,
|
||||||
|
// w -> forward, etc., see MacSrc/Prefs.c's default keybinds.txt) is
|
||||||
|
// polled every physics tick for as long as that state stays set, exactly
|
||||||
|
// like a real held key - so nativeSendPrintableCharDown()/Up() are called
|
||||||
|
// from the key button's own ACTION_DOWN/ACTION_UP (see
|
||||||
|
// KeyboardOverlay.charKeyTouchListener()), not both at once: pushing the
|
||||||
|
// release in the same call as the press would set and clear
|
||||||
|
// sshockKeyStates[code] within a single pump_events() call, before the
|
||||||
|
// game loop's poll (which runs once per frame, before pump_events - see
|
||||||
|
// GameSrc/mainloop.c) ever has a chance to observe the press.
|
||||||
JNIEXPORT void JNICALL
|
JNIEXPORT void JNICALL
|
||||||
Java_de_ladkau_questshock_KeyboardOverlay_nativeSendPrintableChar(JNIEnv *env, jclass clazz,
|
Java_de_ladkau_questshock_KeyboardOverlay_nativeSendPrintableCharDown(JNIEnv *env, jclass clazz,
|
||||||
jchar c) {
|
jchar c) {
|
||||||
SDL_Event event;
|
SDL_Event event;
|
||||||
SDL_zero(event);
|
SDL_zero(event);
|
||||||
event.type = SDL_TEXTINPUT;
|
event.type = SDL_TEXTINPUT;
|
||||||
@@ -41,6 +55,23 @@ Java_de_ladkau_questshock_KeyboardOverlay_nativeSendPrintableChar(JNIEnv *env, j
|
|||||||
SDL_PushEvent(&event);
|
SDL_PushEvent(&event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
JNIEXPORT void JNICALL
|
||||||
|
Java_de_ladkau_questshock_KeyboardOverlay_nativeSendPrintableCharUp(JNIEnv *env, jclass clazz,
|
||||||
|
jchar c) {
|
||||||
|
SDL_Event releaseEvent;
|
||||||
|
SDL_zero(releaseEvent);
|
||||||
|
releaseEvent.type = SDL_KEYUP;
|
||||||
|
releaseEvent.key.timestamp = SDL_GetTicks();
|
||||||
|
releaseEvent.key.windowID = 0;
|
||||||
|
releaseEvent.key.state = SDL_RELEASED;
|
||||||
|
releaseEvent.key.repeat = 0;
|
||||||
|
// SDL keycodes for printable characters are the character's own
|
||||||
|
// (lowercased) byte value - see sdl_events.c's own comment on this.
|
||||||
|
releaseEvent.key.keysym.sym = (SDL_Keycode)tolower((unsigned char)c);
|
||||||
|
releaseEvent.key.keysym.mod = KMOD_NONE;
|
||||||
|
SDL_PushEvent(&releaseEvent);
|
||||||
|
}
|
||||||
|
|
||||||
// KeyboardOverlay's title-bar Close button - the keyboard is a fully
|
// KeyboardOverlay's title-bar Close button - the keyboard is a fully
|
||||||
// independent overlay quad from the menu launcher (see xr_overlay.h), so
|
// independent overlay quad from the menu launcher (see xr_overlay.h), so
|
||||||
// this is the only way to hide it once it's open; the controller's menu
|
// this is the only way to hide it once it's open; the controller's menu
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#include <android/log.h>
|
#include <android/log.h>
|
||||||
|
|
||||||
#include <GLES3/gl3.h>
|
#include <GLES3/gl3.h>
|
||||||
|
#include <SDL.h>
|
||||||
|
|
||||||
#include "xr_mouse.h"
|
#include "xr_mouse.h"
|
||||||
#include "xr_overlay.h"
|
#include "xr_overlay.h"
|
||||||
@@ -43,6 +44,18 @@ static XrActionSet g_action_set = XR_NULL_HANDLE;
|
|||||||
static XrAction g_aim_pose_action = XR_NULL_HANDLE;
|
static XrAction g_aim_pose_action = XR_NULL_HANDLE;
|
||||||
static XrAction g_select_click_action = XR_NULL_HANDLE;
|
static XrAction g_select_click_action = XR_NULL_HANDLE;
|
||||||
static XrAction g_menu_toggle_action = XR_NULL_HANDLE;
|
static XrAction g_menu_toggle_action = XR_NULL_HANDLE;
|
||||||
|
// WORKAROUND - the on-screen keyboard overlay is currently unusable,
|
||||||
|
// which blocks dismissing the "press Space to continue" tutorial screen
|
||||||
|
// shown right after starting a new game. This lets the right controller's
|
||||||
|
// A button send Space straight into the engine, bypassing the broken
|
||||||
|
// keyboard entirely. Remove once the keyboard overlay works.
|
||||||
|
static XrAction g_space_key_action = XR_NULL_HANDLE;
|
||||||
|
// Right controller's B button -> forward movement (Up arrow, one of the
|
||||||
|
// engine's default M_FORWARD keybinds per MacSrc/Prefs.c). Unlike Space,
|
||||||
|
// Up arrow is non-printable so a plain SDL_KEYDOWN/KEYUP is enough - no
|
||||||
|
// SDL_TEXTINPUT needed (see the space_key injection below for why that
|
||||||
|
// matters).
|
||||||
|
static XrAction g_forward_key_action = XR_NULL_HANDLE;
|
||||||
static XrPath g_hand_path[2];
|
static XrPath g_hand_path[2];
|
||||||
static XrSpace g_aim_space[2] = {XR_NULL_HANDLE, XR_NULL_HANDLE};
|
static XrSpace g_aim_space[2] = {XR_NULL_HANDLE, XR_NULL_HANDLE};
|
||||||
|
|
||||||
@@ -53,6 +66,8 @@ static XrSpace g_aim_space[2] = {XR_NULL_HANDLE, XR_NULL_HANDLE};
|
|||||||
// be visible at once.
|
// be visible at once.
|
||||||
static bool g_prev_select[2] = {false, false};
|
static bool g_prev_select[2] = {false, false};
|
||||||
static bool g_prev_menu = false;
|
static bool g_prev_menu = false;
|
||||||
|
static bool g_prev_space_key = false;
|
||||||
|
static bool g_prev_forward_key = false;
|
||||||
static bool g_game_prev_hit[2] = {false, false};
|
static bool g_game_prev_hit[2] = {false, false};
|
||||||
static bool g_game_touch_active[2] = {false, false};
|
static bool g_game_touch_active[2] = {false, false};
|
||||||
static bool g_menu_prev_hit[2] = {false, false};
|
static bool g_menu_prev_hit[2] = {false, false};
|
||||||
@@ -229,6 +244,30 @@ bool xr_input_init(XrInstance instance, XrSession session, int64_t swapchain_for
|
|||||||
"xrCreateAction(menu_toggle)"))
|
"xrCreateAction(menu_toggle)"))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
// WORKAROUND - right controller's A button, see comment on
|
||||||
|
// g_space_key_action's declaration above.
|
||||||
|
XrActionCreateInfo spaceKeyInfo = {XR_TYPE_ACTION_CREATE_INFO};
|
||||||
|
strncpy(spaceKeyInfo.actionName, "space_key", XR_MAX_ACTION_NAME_SIZE - 1);
|
||||||
|
strncpy(spaceKeyInfo.localizedActionName, "Space Key", XR_MAX_LOCALIZED_ACTION_NAME_SIZE - 1);
|
||||||
|
spaceKeyInfo.actionType = XR_ACTION_TYPE_BOOLEAN_INPUT;
|
||||||
|
spaceKeyInfo.countSubactionPaths = 1;
|
||||||
|
spaceKeyInfo.subactionPaths = &g_hand_path[RIGHT];
|
||||||
|
if (!xr_check(xrCreateAction(g_action_set, &spaceKeyInfo, &g_space_key_action),
|
||||||
|
"xrCreateAction(space_key)"))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Right controller's B button, see comment on g_forward_key_action's
|
||||||
|
// declaration above.
|
||||||
|
XrActionCreateInfo forwardKeyInfo = {XR_TYPE_ACTION_CREATE_INFO};
|
||||||
|
strncpy(forwardKeyInfo.actionName, "forward_key", XR_MAX_ACTION_NAME_SIZE - 1);
|
||||||
|
strncpy(forwardKeyInfo.localizedActionName, "Forward Key", XR_MAX_LOCALIZED_ACTION_NAME_SIZE - 1);
|
||||||
|
forwardKeyInfo.actionType = XR_ACTION_TYPE_BOOLEAN_INPUT;
|
||||||
|
forwardKeyInfo.countSubactionPaths = 1;
|
||||||
|
forwardKeyInfo.subactionPaths = &g_hand_path[RIGHT];
|
||||||
|
if (!xr_check(xrCreateAction(g_action_set, &forwardKeyInfo, &g_forward_key_action),
|
||||||
|
"xrCreateAction(forward_key)"))
|
||||||
|
return false;
|
||||||
|
|
||||||
XrPath profilePath;
|
XrPath profilePath;
|
||||||
xrStringToPath(instance, "/interaction_profiles/oculus/touch_controller", &profilePath);
|
xrStringToPath(instance, "/interaction_profiles/oculus/touch_controller", &profilePath);
|
||||||
|
|
||||||
@@ -240,17 +279,22 @@ bool xr_input_init(XrInstance instance, XrSession session, int64_t swapchain_for
|
|||||||
// action. Suggesting a binding to a component the profile doesn't
|
// action. Suggesting a binding to a component the profile doesn't
|
||||||
// have at all (like a nonexistent .../trigger/click) fails the entire
|
// have at all (like a nonexistent .../trigger/click) fails the entire
|
||||||
// xrSuggestInteractionProfileBindings call, not just that one entry.
|
// xrSuggestInteractionProfileBindings call, not just that one entry.
|
||||||
XrPath aimPoseLeftPath, aimPoseRightPath, triggerLeftPath, triggerRightPath, menuClickPath;
|
XrPath aimPoseLeftPath, aimPoseRightPath, triggerLeftPath, triggerRightPath, menuClickPath,
|
||||||
|
aClickPath, bClickPath;
|
||||||
xrStringToPath(instance, "/user/hand/left/input/aim/pose", &aimPoseLeftPath);
|
xrStringToPath(instance, "/user/hand/left/input/aim/pose", &aimPoseLeftPath);
|
||||||
xrStringToPath(instance, "/user/hand/right/input/aim/pose", &aimPoseRightPath);
|
xrStringToPath(instance, "/user/hand/right/input/aim/pose", &aimPoseRightPath);
|
||||||
xrStringToPath(instance, "/user/hand/left/input/trigger/value", &triggerLeftPath);
|
xrStringToPath(instance, "/user/hand/left/input/trigger/value", &triggerLeftPath);
|
||||||
xrStringToPath(instance, "/user/hand/right/input/trigger/value", &triggerRightPath);
|
xrStringToPath(instance, "/user/hand/right/input/trigger/value", &triggerRightPath);
|
||||||
xrStringToPath(instance, "/user/hand/left/input/menu/click", &menuClickPath);
|
xrStringToPath(instance, "/user/hand/left/input/menu/click", &menuClickPath);
|
||||||
|
xrStringToPath(instance, "/user/hand/right/input/a/click", &aClickPath);
|
||||||
|
xrStringToPath(instance, "/user/hand/right/input/b/click", &bClickPath);
|
||||||
|
|
||||||
XrActionSuggestedBinding bindings[] = {
|
XrActionSuggestedBinding bindings[] = {
|
||||||
{g_aim_pose_action, aimPoseLeftPath}, {g_aim_pose_action, aimPoseRightPath},
|
{g_aim_pose_action, aimPoseLeftPath}, {g_aim_pose_action, aimPoseRightPath},
|
||||||
{g_select_click_action, triggerLeftPath}, {g_select_click_action, triggerRightPath},
|
{g_select_click_action, triggerLeftPath}, {g_select_click_action, triggerRightPath},
|
||||||
{g_menu_toggle_action, menuClickPath},
|
{g_menu_toggle_action, menuClickPath},
|
||||||
|
{g_space_key_action, aClickPath},
|
||||||
|
{g_forward_key_action, bClickPath},
|
||||||
};
|
};
|
||||||
XrInteractionProfileSuggestedBinding suggested = {XR_TYPE_INTERACTION_PROFILE_SUGGESTED_BINDING};
|
XrInteractionProfileSuggestedBinding suggested = {XR_TYPE_INTERACTION_PROFILE_SUGGESTED_BINDING};
|
||||||
suggested.interactionProfile = profilePath;
|
suggested.interactionProfile = profilePath;
|
||||||
@@ -291,7 +335,7 @@ bool xr_input_init(XrInstance instance, XrSession session, int64_t swapchain_for
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LOGI("XR: input action set ready (aim pose + trigger + menu-toggle)");
|
LOGI("XR: input action set ready (aim pose + trigger + menu-toggle + A=space + B=forward)");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -356,8 +400,9 @@ static bool xr_input_try_overlay(XrOverlay *overlay, bool dragCapable, int hand,
|
|||||||
*outPlaneDistance = t;
|
*outPlaneDistance = t;
|
||||||
|
|
||||||
if (hit != *prevHit) {
|
if (hit != *prevHit) {
|
||||||
LOGI("XR: %s aim ray %s %s quad (u=%.2f v=%.2f)", kHandName[hand],
|
// Disabled for now - noisy while chasing an unrelated texture bug.
|
||||||
hit ? "entered" : "left", quadName, u, v);
|
// LOGI("XR: %s aim ray %s %s quad (u=%.2f v=%.2f)", kHandName[hand],
|
||||||
|
// hit ? "entered" : "left", quadName, u, v);
|
||||||
*prevHit = hit;
|
*prevHit = hit;
|
||||||
}
|
}
|
||||||
if (hit) {
|
if (hit) {
|
||||||
@@ -559,8 +604,9 @@ static bool xr_input_try_game_quad(int hand, const XrSpaceLocation *location, fl
|
|||||||
*outV = v;
|
*outV = v;
|
||||||
|
|
||||||
if (hit != g_game_prev_hit[hand]) {
|
if (hit != g_game_prev_hit[hand]) {
|
||||||
LOGI("XR: %s aim ray %s game quad (u=%.2f v=%.2f)", kHandName[hand],
|
// Disabled for now - noisy while chasing an unrelated texture bug.
|
||||||
hit ? "entered" : "left", u, v);
|
// LOGI("XR: %s aim ray %s game quad (u=%.2f v=%.2f)", kHandName[hand],
|
||||||
|
// hit ? "entered" : "left", u, v);
|
||||||
g_game_prev_hit[hand] = hit;
|
g_game_prev_hit[hand] = hit;
|
||||||
}
|
}
|
||||||
return hit;
|
return hit;
|
||||||
@@ -655,6 +701,82 @@ void xr_input_sync_and_draw(XrSpace baseSpace, XrTime time, bool draw) {
|
|||||||
g_prev_menu = menuDown;
|
g_prev_menu = menuDown;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (hand == RIGHT) {
|
||||||
|
XrActionStateGetInfo spaceKeyInfo = {XR_TYPE_ACTION_STATE_GET_INFO};
|
||||||
|
spaceKeyInfo.action = g_space_key_action;
|
||||||
|
spaceKeyInfo.subactionPath = g_hand_path[RIGHT];
|
||||||
|
XrActionStateBoolean spaceKeyState = {XR_TYPE_ACTION_STATE_BOOLEAN};
|
||||||
|
xrGetActionStateBoolean(g_session, &spaceKeyInfo, &spaceKeyState);
|
||||||
|
bool spaceKeyDown = spaceKeyState.isActive && spaceKeyState.currentState;
|
||||||
|
if (spaceKeyDown != g_prev_space_key) {
|
||||||
|
LOGI("XR: A button (space key) %s", spaceKeyDown ? "DOWN" : "UP");
|
||||||
|
if (spaceKeyDown) {
|
||||||
|
// The engine's pump_events() (sdl_events.c) only queues a
|
||||||
|
// KEYDOWN here for non-printable keys (ascii < 32) -
|
||||||
|
// printable keys like Space are expected to arrive via
|
||||||
|
// SDL_TEXTINPUT instead (that's the branch that actually
|
||||||
|
// queues the KBS_DOWN event the tutorial-dismiss handler
|
||||||
|
// checks for), so send that too, matching what real
|
||||||
|
// keyboard input generates for a Space press.
|
||||||
|
//
|
||||||
|
// sdl_events.c's SDL_TEXTINPUT handling also sets
|
||||||
|
// sshockKeyStates[Space] = pressed - the same array
|
||||||
|
// Space's own keybind (M_JUMP, see MacSrc/Prefs.c's
|
||||||
|
// default keybinds.txt) polls every physics tick via
|
||||||
|
// ui_poll_keyboard(). It's only ever cleared again by a
|
||||||
|
// matching SDL_KEYUP, sent below once this button's real
|
||||||
|
// physical up-edge fires (spaceKeyDown false) - not
|
||||||
|
// synthesized here alongside the press, since pushing
|
||||||
|
// both in the same batch would set and clear it within
|
||||||
|
// one pump_events() call, before the poll (which runs
|
||||||
|
// once per frame, before pump_events) ever sees the
|
||||||
|
// press.
|
||||||
|
SDL_Event textEvent;
|
||||||
|
SDL_zero(textEvent);
|
||||||
|
textEvent.type = SDL_TEXTINPUT;
|
||||||
|
textEvent.text.timestamp = SDL_GetTicks();
|
||||||
|
textEvent.text.windowID = 0;
|
||||||
|
textEvent.text.text[0] = ' ';
|
||||||
|
textEvent.text.text[1] = '\0';
|
||||||
|
SDL_PushEvent(&textEvent);
|
||||||
|
}
|
||||||
|
SDL_Event event;
|
||||||
|
SDL_zero(event);
|
||||||
|
event.type = spaceKeyDown ? SDL_KEYDOWN : SDL_KEYUP;
|
||||||
|
event.key.timestamp = SDL_GetTicks();
|
||||||
|
event.key.windowID = 0;
|
||||||
|
event.key.state = spaceKeyDown ? SDL_PRESSED : SDL_RELEASED;
|
||||||
|
event.key.repeat = 0;
|
||||||
|
event.key.keysym.scancode = SDL_SCANCODE_SPACE;
|
||||||
|
event.key.keysym.sym = SDLK_SPACE;
|
||||||
|
event.key.keysym.mod = KMOD_NONE;
|
||||||
|
SDL_PushEvent(&event);
|
||||||
|
g_prev_space_key = spaceKeyDown;
|
||||||
|
}
|
||||||
|
|
||||||
|
XrActionStateGetInfo forwardKeyInfo = {XR_TYPE_ACTION_STATE_GET_INFO};
|
||||||
|
forwardKeyInfo.action = g_forward_key_action;
|
||||||
|
forwardKeyInfo.subactionPath = g_hand_path[RIGHT];
|
||||||
|
XrActionStateBoolean forwardKeyState = {XR_TYPE_ACTION_STATE_BOOLEAN};
|
||||||
|
xrGetActionStateBoolean(g_session, &forwardKeyInfo, &forwardKeyState);
|
||||||
|
bool forwardKeyDown = forwardKeyState.isActive && forwardKeyState.currentState;
|
||||||
|
if (forwardKeyDown != g_prev_forward_key) {
|
||||||
|
LOGI("XR: B button (forward key) %s", forwardKeyDown ? "DOWN" : "UP");
|
||||||
|
SDL_Event event;
|
||||||
|
SDL_zero(event);
|
||||||
|
event.type = forwardKeyDown ? SDL_KEYDOWN : SDL_KEYUP;
|
||||||
|
event.key.timestamp = SDL_GetTicks();
|
||||||
|
event.key.windowID = 0;
|
||||||
|
event.key.state = forwardKeyDown ? SDL_PRESSED : SDL_RELEASED;
|
||||||
|
event.key.repeat = 0;
|
||||||
|
event.key.keysym.scancode = SDL_SCANCODE_UP;
|
||||||
|
event.key.keysym.sym = SDLK_UP;
|
||||||
|
event.key.keysym.mod = KMOD_NONE;
|
||||||
|
SDL_PushEvent(&event);
|
||||||
|
g_prev_forward_key = forwardKeyDown;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
XrActionStateGetInfo poseInfo = {XR_TYPE_ACTION_STATE_GET_INFO};
|
XrActionStateGetInfo poseInfo = {XR_TYPE_ACTION_STATE_GET_INFO};
|
||||||
poseInfo.action = g_aim_pose_action;
|
poseInfo.action = g_aim_pose_action;
|
||||||
poseInfo.subactionPath = g_hand_path[hand];
|
poseInfo.subactionPath = g_hand_path[hand];
|
||||||
@@ -782,6 +904,8 @@ void xr_input_shutdown(void) {
|
|||||||
g_aim_pose_action = XR_NULL_HANDLE;
|
g_aim_pose_action = XR_NULL_HANDLE;
|
||||||
g_select_click_action = XR_NULL_HANDLE;
|
g_select_click_action = XR_NULL_HANDLE;
|
||||||
g_menu_toggle_action = XR_NULL_HANDLE;
|
g_menu_toggle_action = XR_NULL_HANDLE;
|
||||||
|
g_space_key_action = XR_NULL_HANDLE;
|
||||||
|
g_forward_key_action = XR_NULL_HANDLE;
|
||||||
|
|
||||||
g_instance = XR_NULL_HANDLE;
|
g_instance = XR_NULL_HANDLE;
|
||||||
g_session = XR_NULL_HANDLE;
|
g_session = XR_NULL_HANDLE;
|
||||||
@@ -794,6 +918,8 @@ void xr_input_shutdown(void) {
|
|||||||
memset(g_keyboard_touch_active, 0, sizeof(g_keyboard_touch_active));
|
memset(g_keyboard_touch_active, 0, sizeof(g_keyboard_touch_active));
|
||||||
g_beam_quad_valid[LEFT] = g_beam_quad_valid[RIGHT] = false;
|
g_beam_quad_valid[LEFT] = g_beam_quad_valid[RIGHT] = false;
|
||||||
g_prev_menu = false;
|
g_prev_menu = false;
|
||||||
|
g_prev_space_key = false;
|
||||||
|
g_prev_forward_key = false;
|
||||||
g_keyboard_dragging = false;
|
g_keyboard_dragging = false;
|
||||||
g_keyboard_drag_hand = -1;
|
g_keyboard_drag_hand = -1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,8 +21,9 @@ extern "C" {
|
|||||||
void xr_mouse_move(int x, int y);
|
void xr_mouse_move(int x, int y);
|
||||||
|
|
||||||
// Pushes a synthetic SDL_MOUSEBUTTONDOWN/UP (SDL_BUTTON_LEFT) - matches
|
// Pushes a synthetic SDL_MOUSEBUTTONDOWN/UP (SDL_BUTTON_LEFT) - matches
|
||||||
// KeyboardOverlay's nativeSendPrintableChar() in questshock_native.c: a
|
// KeyboardOverlay's nativeSendPrintableCharDown()/Up() in
|
||||||
// plain SDL_Event built by hand and handed to the public SDL_PushEvent(),
|
// questshock_native.c: a plain SDL_Event built by hand and handed to the
|
||||||
|
// public SDL_PushEvent(),
|
||||||
// no engine patch needed. pump_events() (sdl_events.c) reads whatever
|
// no engine patch needed. pump_events() (sdl_events.c) reads whatever
|
||||||
// mouse_put_xy() last set, not any x/y carried on the event itself - call
|
// mouse_put_xy() last set, not any x/y carried on the event itself - call
|
||||||
// xr_mouse_move() first if position needs updating this frame.
|
// xr_mouse_move() first if position needs updating this frame.
|
||||||
|
|||||||
@@ -427,6 +427,15 @@ bool xr_overlay_render_and_build_layer(XrOverlay *overlay, XrCompositionLayerQua
|
|||||||
overlay->width, 0, GL_COLOR_BUFFER_BIT, GL_LINEAR);
|
overlay->width, 0, GL_COLOR_BUFFER_BIT, GL_LINEAR);
|
||||||
real_glBindFramebuffer(GL_READ_FRAMEBUFFER, (GLuint)prevReadFbo);
|
real_glBindFramebuffer(GL_READ_FRAMEBUFFER, (GLuint)prevReadFbo);
|
||||||
|
|
||||||
|
// Without this, nothing here waits for the blit to actually finish on
|
||||||
|
// the GPU before xr_swapchain_release() hands the image back to the
|
||||||
|
// compositor - glBlitFramebuffer only queues the copy. A cross-process
|
||||||
|
// consumer reading the image before the driver has finished writing it
|
||||||
|
// is a textbook source of the diagonal-banded/torn corruption seen on
|
||||||
|
// device (see xr_swapchain_acquire()'s counterpart in OpenGL.cc, which
|
||||||
|
// had the same gap).
|
||||||
|
glFinish();
|
||||||
|
|
||||||
xr_swapchain_release(overlay->instance, &overlay->swapchain);
|
xr_swapchain_release(overlay->instance, &overlay->swapchain);
|
||||||
|
|
||||||
memset(out_quad, 0, sizeof(*out_quad));
|
memset(out_quad, 0, sizeof(*out_quad));
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.app.Activity;
|
|||||||
import android.util.TypedValue;
|
import android.util.TypedValue;
|
||||||
import android.view.Gravity;
|
import android.view.Gravity;
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
|
import android.view.MotionEvent;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.Button;
|
import android.widget.Button;
|
||||||
@@ -17,13 +18,19 @@ import org.libsdl.app.SDLActivity;
|
|||||||
* The on-screen keyboard quad (see OverlayPanel for the shared off-screen-
|
* The on-screen keyboard quad (see OverlayPanel for the shared off-screen-
|
||||||
* render/touch/cursor plumbing this builds on) - a hand-built, full
|
* render/touch/cursor plumbing this builds on) - a hand-built, full
|
||||||
* US-layout key grid (there's no system IME to borrow once immersive) plus
|
* US-layout key grid (there's no system IME to borrow once immersive) plus
|
||||||
* a text field echoing what's been sent. Each key press is forwarded
|
* a text field echoing what's been sent. Every key that reaches the game
|
||||||
* straight to the game as real input (see handleKeyPress()/
|
* (everything but Close/Shift/Ctrl/Alt, which are local UI actions - see
|
||||||
* handleCharKeyPress()) - non-printable keys as an
|
* handleKeyDown()/handleKeyUp() and handleCharKeyDown()/handleCharKeyUp())
|
||||||
|
* is driven by that key button's own ACTION_DOWN/ACTION_UP (see
|
||||||
|
* charKeyTouchListener()/controlKeyTouchListener()), which
|
||||||
|
* OverlayPanel.handleTouch() dispatches for exactly as long as the
|
||||||
|
* controller's trigger is physically held while aimed at it - so a key
|
||||||
|
* held on this keyboard behaves like a real key held on a physical one,
|
||||||
|
* not an instant tap. Non-printable keys go through an
|
||||||
* SDLActivity.onNativeKeyDown()/onNativeKeyUp() pair, the same one a
|
* SDLActivity.onNativeKeyDown()/onNativeKeyUp() pair, the same one a
|
||||||
* physical Bluetooth keyboard's presses already drive, and printable keys
|
* physical Bluetooth keyboard's presses already drive; printable keys go
|
||||||
* via a synthesized SDL_TEXTINPUT event pushed from native code (see
|
* through a synthesized SDL_TEXTINPUT/SDL_KEYUP pair pushed from native
|
||||||
* commitPrintableChar()/nativeSendPrintableChar() in questshock_native.c) -
|
* code (see nativeSendPrintableCharDown()/Up() in questshock_native.c) -
|
||||||
* except while a Ctrl/Alt "armed" modifier is active, when they go through
|
* except while a Ctrl/Alt "armed" modifier is active, when they go through
|
||||||
* the same onNativeKeyDown()/onNativeKeyUp() path instead, so the modifier
|
* the same onNativeKeyDown()/onNativeKeyUp() path instead, so the modifier
|
||||||
* carries across (see toggleModifier()). Shift is a purely local layer
|
* carries across (see toggleModifier()). Shift is a purely local layer
|
||||||
@@ -93,9 +100,9 @@ public class KeyboardOverlay extends OverlayPanel {
|
|||||||
// A key whose printed character (and, while unmodified, whose game
|
// A key whose printed character (and, while unmodified, whose game
|
||||||
// input) depends on the Shift layer - covers letters, digits, and
|
// input) depends on the Shift layer - covers letters, digits, and
|
||||||
// punctuation uniformly (e.g. 'q'/'Q', '1'/'!', '-'/'_'). androidKeyCode
|
// punctuation uniformly (e.g. 'q'/'Q', '1'/'!', '-'/'_'). androidKeyCode
|
||||||
// is only used for the Ctrl/Alt-combo path (see handleCharKeyPress()) -
|
// is only used for the Ctrl/Alt-combo path (see handleCharKeyDown()) -
|
||||||
// the unmodified path goes through commitPrintableChar() instead, which
|
// the unmodified path goes through nativeSendPrintableCharDown()/Up()
|
||||||
// doesn't need an Android keycode at all.
|
// instead, which doesn't need an Android keycode at all.
|
||||||
private static final class CharKey {
|
private static final class CharKey {
|
||||||
final char base;
|
final char base;
|
||||||
final char shifted;
|
final char shifted;
|
||||||
@@ -224,7 +231,7 @@ public class KeyboardOverlay extends OverlayPanel {
|
|||||||
|
|
||||||
Button close = new Button(activity);
|
Button close = new Button(activity);
|
||||||
close.setText("Close");
|
close.setText("Close");
|
||||||
close.setOnClickListener(v -> handleKeyPress("Close"));
|
close.setOnClickListener(v -> nativeRequestClose());
|
||||||
close.setOnTouchListener(CLICK_ON_TOUCH_UP);
|
close.setOnTouchListener(CLICK_ON_TOUCH_UP);
|
||||||
bar.addView(close,
|
bar.addView(close,
|
||||||
new LinearLayout.LayoutParams(CLOSE_BUTTON_WIDTH, ViewGroup.LayoutParams.MATCH_PARENT));
|
new LinearLayout.LayoutParams(CLOSE_BUTTON_WIDTH, ViewGroup.LayoutParams.MATCH_PARENT));
|
||||||
@@ -234,9 +241,10 @@ public class KeyboardOverlay extends OverlayPanel {
|
|||||||
|
|
||||||
// Each row fills the panel's remaining height evenly (weight 1 on the
|
// Each row fills the panel's remaining height evenly (weight 1 on the
|
||||||
// row itself). Accepts a mix of String (a fixed-action control key,
|
// row itself). Accepts a mix of String (a fixed-action control key,
|
||||||
// handled by handleKeyPress()) and CharKey (a Shift-sensitive character
|
// handled by handleKeyDown()/handleKeyUp()) and CharKey (a
|
||||||
// key, handled by handleCharKeyPress()) per item, so a row can match
|
// Shift-sensitive character key, handled by handleCharKeyDown()/
|
||||||
// real keyboard geometry (e.g. "Tab" followed by a run of CharKeys).
|
// handleCharKeyUp()) per item, so a row can match real keyboard
|
||||||
|
// geometry (e.g. "Tab" followed by a run of CharKeys).
|
||||||
// Within a row, CharKeys and most control keys share width equally
|
// Within a row, CharKeys and most control keys share width equally
|
||||||
// except Space (wider, like a real keyboard) and Esc/Back/Enter/Tab/
|
// except Space (wider, like a real keyboard) and Esc/Back/Enter/Tab/
|
||||||
// Shift/Ctrl/Alt (narrower, see keyWeight()).
|
// Shift/Ctrl/Alt (narrower, see keyWeight()).
|
||||||
@@ -245,13 +253,12 @@ public class KeyboardOverlay extends OverlayPanel {
|
|||||||
row.setOrientation(LinearLayout.HORIZONTAL);
|
row.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
for (Object item : items) {
|
for (Object item : items) {
|
||||||
Button key = new Button(activity);
|
Button key = new Button(activity);
|
||||||
key.setOnTouchListener(CLICK_ON_TOUCH_UP);
|
|
||||||
float weight;
|
float weight;
|
||||||
if (item instanceof CharKey) {
|
if (item instanceof CharKey) {
|
||||||
CharKey charKey = (CharKey) item;
|
CharKey charKey = (CharKey) item;
|
||||||
charKey.button = key;
|
charKey.button = key;
|
||||||
key.setText(charKeyLabel(charKey, shiftActive));
|
key.setText(charKeyLabel(charKey, shiftActive));
|
||||||
key.setOnClickListener(v -> handleCharKeyPress(charKey));
|
key.setOnTouchListener(charKeyTouchListener(charKey));
|
||||||
charKeys.add(charKey);
|
charKeys.add(charKey);
|
||||||
// Space is a CharKey too (base == shifted == ' '), but keeps
|
// Space is a CharKey too (base == shifted == ' '), but keeps
|
||||||
// its traditional wide key like the other control keys do.
|
// its traditional wide key like the other control keys do.
|
||||||
@@ -259,8 +266,19 @@ public class KeyboardOverlay extends OverlayPanel {
|
|||||||
} else {
|
} else {
|
||||||
String label = (String) item;
|
String label = (String) item;
|
||||||
key.setText(label);
|
key.setText(label);
|
||||||
key.setOnClickListener(v -> handleKeyPress(label));
|
|
||||||
weight = keyWeight(label);
|
weight = keyWeight(label);
|
||||||
|
// Shift/Ctrl/Alt are local UI toggles with their own
|
||||||
|
// instantaneous press/release semantics (see
|
||||||
|
// toggleModifier()), not a value forwarded to the game for
|
||||||
|
// as long as the key is held - a plain click is enough.
|
||||||
|
// Every other control key reaches the game and needs real
|
||||||
|
// hold-duration semantics (see controlKeyTouchListener()).
|
||||||
|
if (label.equals("Shift") || label.equals("Ctrl") || label.equals("Alt")) {
|
||||||
|
key.setOnClickListener(v -> handleToggleKey(label));
|
||||||
|
key.setOnTouchListener(CLICK_ON_TOUCH_UP);
|
||||||
|
} else {
|
||||||
|
key.setOnTouchListener(controlKeyTouchListener(label));
|
||||||
|
}
|
||||||
if (label.equals("Shift")) {
|
if (label.equals("Shift")) {
|
||||||
shiftButton = key;
|
shiftButton = key;
|
||||||
} else if (label.equals("Ctrl")) {
|
} else if (label.equals("Ctrl")) {
|
||||||
@@ -276,6 +294,54 @@ public class KeyboardOverlay extends OverlayPanel {
|
|||||||
ViewGroup.LayoutParams.MATCH_PARENT, 0, 1f));
|
ViewGroup.LayoutParams.MATCH_PARENT, 0, 1f));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Drives a control key (everything controlKeyCode() maps, i.e. every
|
||||||
|
// key in addRow() except CharKeys and the Shift/Ctrl/Alt toggles) from
|
||||||
|
// its button's own touch lifecycle, so it reaches the game for exactly
|
||||||
|
// as long as the controller's trigger is held on it (see the class
|
||||||
|
// doc). ACTION_CANCEL (the ray straying off the button, or another
|
||||||
|
// overlay claiming it - see xr_input.c) releases the key the same as
|
||||||
|
// ACTION_UP, so a key can never be left stuck down.
|
||||||
|
private View.OnTouchListener controlKeyTouchListener(String label) {
|
||||||
|
return (v, event) -> {
|
||||||
|
switch (event.getAction()) {
|
||||||
|
case MotionEvent.ACTION_DOWN:
|
||||||
|
v.setPressed(true);
|
||||||
|
handleKeyDown(label);
|
||||||
|
break;
|
||||||
|
case MotionEvent.ACTION_UP:
|
||||||
|
v.setPressed(false);
|
||||||
|
handleKeyUp(label);
|
||||||
|
break;
|
||||||
|
case MotionEvent.ACTION_CANCEL:
|
||||||
|
v.setPressed(false);
|
||||||
|
handleKeyUp(label);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same as controlKeyTouchListener() above, for a CharKey.
|
||||||
|
private View.OnTouchListener charKeyTouchListener(CharKey key) {
|
||||||
|
return (v, event) -> {
|
||||||
|
switch (event.getAction()) {
|
||||||
|
case MotionEvent.ACTION_DOWN:
|
||||||
|
v.setPressed(true);
|
||||||
|
handleCharKeyDown(key);
|
||||||
|
break;
|
||||||
|
case MotionEvent.ACTION_UP:
|
||||||
|
v.setPressed(false);
|
||||||
|
handleCharKeyUp(key);
|
||||||
|
break;
|
||||||
|
case MotionEvent.ACTION_CANCEL:
|
||||||
|
v.setPressed(false);
|
||||||
|
handleCharKeyUp(key);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private static float keyWeight(String label) {
|
private static float keyWeight(String label) {
|
||||||
if (label.equals("Back") || label.equals("Esc") || label.equals("Enter")
|
if (label.equals("Back") || label.equals("Esc") || label.equals("Enter")
|
||||||
|| label.equals("Tab") || label.equals("Shift") || label.equals("Ctrl")
|
|| label.equals("Tab") || label.equals("Shift") || label.equals("Ctrl")
|
||||||
@@ -310,38 +376,31 @@ public class KeyboardOverlay extends OverlayPanel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// "Close" (the title bar's button - see buildTitleBar()) hides this
|
// Shift is a local layer toggle; Ctrl/Alt are one-shot armed modifiers
|
||||||
// overlay's quad entirely (see nativeRequestClose()) and never reaches
|
// (see toggleModifier()) - neither is forwarded to the game as a key of
|
||||||
// the game. Shift/Ctrl/Alt are handled separately below (Shift is a
|
// its own, so a plain click (rather than the hold-duration dispatch
|
||||||
// local layer toggle; Ctrl/Alt are one-shot armed modifiers) since
|
// every other key below uses) is enough.
|
||||||
// neither sends a plain key event of its own the way every other
|
private void handleToggleKey(String label) {
|
||||||
// control key here does. Everything else (Back/Esc/Enter/Tab/arrows/
|
|
||||||
// F1-F12) is non-printable, so it's forwarded as a real Android key
|
|
||||||
// event via controlKeyCode() above, then releases any armed Ctrl/Alt
|
|
||||||
// modifier (this key just consumed it). This all matters beyond just
|
|
||||||
// the visible typedTextView below: the engine's cutscene skip handler
|
|
||||||
// (cutsloop.c's cutscene_key_handler()) only reacts to Esc/Enter/Space,
|
|
||||||
// so those need to reach the game as actual game input, not just be
|
|
||||||
// echoed locally.
|
|
||||||
private void handleKeyPress(String label) {
|
|
||||||
if (label.equals("Close")) {
|
|
||||||
nativeRequestClose();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (label.equals("Shift")) {
|
if (label.equals("Shift")) {
|
||||||
shiftActive = !shiftActive;
|
shiftActive = !shiftActive;
|
||||||
updateShiftVisual();
|
updateShiftVisual();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (label.equals("Ctrl") || label.equals("Alt")) {
|
toggleModifier(label);
|
||||||
toggleModifier(label);
|
}
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Everything controlKeyCode() maps (Back/Esc/Enter/Tab/arrows/F1-F12)
|
||||||
|
// is non-printable, so it's forwarded as a real Android key event via
|
||||||
|
// that code - down on this key's own ACTION_DOWN, up on ACTION_UP/
|
||||||
|
// CANCEL (see controlKeyTouchListener()), the same down/up split a
|
||||||
|
// physical Bluetooth keyboard's own key events already carry. This all
|
||||||
|
// matters beyond just the visible typedTextView below: the engine's
|
||||||
|
// cutscene skip handler (cutsloop.c's cutscene_key_handler()) only
|
||||||
|
// reacts to Esc/Enter/Space, so those need to reach the game as actual
|
||||||
|
// game input, not just be echoed locally.
|
||||||
|
private void handleKeyDown(String label) {
|
||||||
int keyCode = controlKeyCode(label);
|
int keyCode = controlKeyCode(label);
|
||||||
SDLActivity.onNativeKeyDown(keyCode);
|
SDLActivity.onNativeKeyDown(keyCode);
|
||||||
SDLActivity.onNativeKeyUp(keyCode);
|
|
||||||
releaseArmedModifiers();
|
|
||||||
|
|
||||||
if (label.equals("Back")) {
|
if (label.equals("Back")) {
|
||||||
if (typedText.length() > 0) {
|
if (typedText.length() > 0) {
|
||||||
@@ -353,31 +412,45 @@ public class KeyboardOverlay extends OverlayPanel {
|
|||||||
typedTextView.setText(typedText.toString());
|
typedTextView.setText(typedText.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void handleKeyUp(String label) {
|
||||||
|
SDLActivity.onNativeKeyUp(controlKeyCode(label));
|
||||||
|
releaseArmedModifiers();
|
||||||
|
}
|
||||||
|
|
||||||
// A CharKey's printed character depends only on the Shift layer
|
// A CharKey's printed character depends only on the Shift layer
|
||||||
// (updateShiftVisual() keeps its button's label in sync). Which game
|
// (updateShiftVisual() keeps its button's label in sync). Which game
|
||||||
// input it produces depends on whether a modifier is armed: unmodified,
|
// input it produces depends on whether a modifier is armed: unmodified,
|
||||||
// it goes through commitPrintableChar() below like a normal typed
|
// it goes through nativeSendPrintableCharDown()/Up() below like a
|
||||||
// character; with Ctrl/Alt armed, it instead goes through the same
|
// normal typed character; with Ctrl/Alt armed, it instead goes through
|
||||||
// onNativeKeyDown()/onNativeKeyUp() path handleKeyPress() uses for
|
// the same onNativeKeyDown()/onNativeKeyUp() path handleKeyDown()/
|
||||||
// control keys, since that's the only path that carries
|
// handleKeyUp() use for control keys, since that's the only path that
|
||||||
// ev.key.keysym.mod through to the engine (sdl_events.c's pump_events()
|
// carries ev.key.keysym.mod through to the engine (sdl_events.c's
|
||||||
// reads Ctrl/Alt only off that path, never off SDL_TEXTINPUT).
|
// pump_events() reads Ctrl/Alt only off that path, never off
|
||||||
private void handleCharKeyPress(CharKey key) {
|
// SDL_TEXTINPUT).
|
||||||
|
private void handleCharKeyDown(CharKey key) {
|
||||||
char c = shiftActive ? key.shifted : key.base;
|
char c = shiftActive ? key.shifted : key.base;
|
||||||
if (ctrlArmed || altArmed) {
|
if (ctrlArmed || altArmed) {
|
||||||
SDLActivity.onNativeKeyDown(key.androidKeyCode);
|
SDLActivity.onNativeKeyDown(key.androidKeyCode);
|
||||||
SDLActivity.onNativeKeyUp(key.androidKeyCode);
|
|
||||||
releaseArmedModifiers();
|
|
||||||
} else {
|
} else {
|
||||||
commitPrintableChar(c);
|
nativeSendPrintableCharDown(c);
|
||||||
}
|
}
|
||||||
typedText.append(c);
|
typedText.append(c);
|
||||||
typedTextView.setText(typedText.toString());
|
typedTextView.setText(typedText.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void handleCharKeyUp(CharKey key) {
|
||||||
|
char c = shiftActive ? key.shifted : key.base;
|
||||||
|
if (ctrlArmed || altArmed) {
|
||||||
|
SDLActivity.onNativeKeyUp(key.androidKeyCode);
|
||||||
|
releaseArmedModifiers();
|
||||||
|
} else {
|
||||||
|
nativeSendPrintableCharUp(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Ctrl/Alt are "armed" rather than held: pressing one immediately sends
|
// Ctrl/Alt are "armed" rather than held: pressing one immediately sends
|
||||||
// its keydown (so SDL's own modifier tracking picks it up for whatever
|
// its keydown (so SDL's own modifier tracking picks it up for whatever
|
||||||
// key comes next - see handleCharKeyPress()/handleKeyPress()) and
|
// key comes next - see handleCharKeyDown()/handleKeyDown()) and
|
||||||
// brackets its label for feedback; pressing the same key again before
|
// brackets its label for feedback; pressing the same key again before
|
||||||
// it's been used cancels it (sends the matching keyup, un-brackets).
|
// it's been used cancels it (sends the matching keyup, un-brackets).
|
||||||
// The normal case - actually being consumed by the next key press - is
|
// The normal case - actually being consumed by the next key press - is
|
||||||
@@ -400,10 +473,10 @@ public class KeyboardOverlay extends OverlayPanel {
|
|||||||
button.setText(nowArmed ? "[" + label + "]" : label);
|
button.setText(nowArmed ? "[" + label + "]" : label);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Called after any key actually reaches the game (a control key in
|
// Called after any key actually reaches the game (a control key's
|
||||||
// handleKeyPress(), or a CharKey in handleCharKeyPress()) so an armed
|
// handleKeyUp(), or a CharKey's handleCharKeyUp()) so an armed Ctrl/Alt
|
||||||
// Ctrl/Alt only ever applies to the very next key, then releases -
|
// only ever applies to the very next key, then releases - matching a
|
||||||
// matching a real Ctrl/Alt+key combo's keyup once the combo is done.
|
// real Ctrl/Alt+key combo's keyup once the combo is done.
|
||||||
private void releaseArmedModifiers() {
|
private void releaseArmedModifiers() {
|
||||||
if (ctrlArmed) {
|
if (ctrlArmed) {
|
||||||
SDLActivity.onNativeKeyUp(KeyEvent.KEYCODE_CTRL_LEFT);
|
SDLActivity.onNativeKeyUp(KeyEvent.KEYCODE_CTRL_LEFT);
|
||||||
@@ -429,17 +502,15 @@ public class KeyboardOverlay extends OverlayPanel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Synthesizes the SDL_TEXTINPUT event pump_events() (sdl_events.c)
|
// Synthesizes the SDL_TEXTINPUT/SDL_KEYUP pair pump_events()
|
||||||
// requires for printable characters (see handleCharKeyPress() above),
|
// (sdl_events.c) requires for printable characters (see
|
||||||
// by building and pushing it directly in native code (see
|
// handleCharKeyDown()/handleCharKeyUp() above), by building and
|
||||||
// nativeSendPrintableChar() in questshock_native.c) rather than through
|
// pushing them directly in native code rather than through Android's
|
||||||
// Android's IME (there's no real IME session behind this off-screen,
|
// IME (there's no real IME session behind this off-screen,
|
||||||
// never-attached grid for that plumbing to hook into).
|
// never-attached grid for that plumbing to hook into).
|
||||||
private static void commitPrintableChar(char c) {
|
private static native void nativeSendPrintableCharDown(char c);
|
||||||
nativeSendPrintableChar(c);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static native void nativeSendPrintableChar(char c);
|
private static native void nativeSendPrintableCharUp(char c);
|
||||||
|
|
||||||
// Hides this overlay's quad (xr_overlay_set_visible(..., false)) - the
|
// Hides this overlay's quad (xr_overlay_set_visible(..., false)) - the
|
||||||
// only way to do so, since the controller's menu button doesn't affect
|
// only way to do so, since the controller's menu button doesn't affect
|
||||||
|
|||||||
@@ -199,11 +199,18 @@
|
|||||||
#include "InitMac.h"
|
#include "InitMac.h"
|
||||||
#include "Modding.h"
|
#include "Modding.h"
|
||||||
#include "OpenGL.h"
|
#include "OpenGL.h"
|
||||||
@@ -301,6 +305,21 @@
|
@@ -301,6 +305,28 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
void SDLDraw() {
|
void SDLDraw() {
|
||||||
+#ifdef __ANDROID__
|
+#ifdef __ANDROID__
|
||||||
|
+ // Diagnostic: logs once per transition (including the very first
|
||||||
|
+ // frame) between the OpenGL 3D render path and the plain
|
||||||
|
+ // software-composited 2D path (splash/menus/cutscenes) below - so a
|
||||||
|
+ // logcat capture immediately shows which one is active without
|
||||||
|
+ // needing to infer it from screenshots.
|
||||||
|
+ static int s_last_render_mode = -1; // -1 = not yet logged
|
||||||
|
+
|
||||||
+ // The true once-a-frame present hook for the immersive OpenXR path -
|
+ // The true once-a-frame present hook for the immersive OpenXR path -
|
||||||
+ // unlike opengl_swap_and_restore() below, this runs every frame
|
+ // unlike opengl_swap_and_restore() below, this runs every frame
|
||||||
+ // regardless of should_opengl_swap() (e.g. also during the splash
|
+ // regardless of should_opengl_swap() (e.g. also during the splash
|
||||||
@@ -221,17 +228,25 @@
|
|||||||
if (should_opengl_swap()) {
|
if (should_opengl_swap()) {
|
||||||
// We want the UI background to be transparent!
|
// We want the UI background to be transparent!
|
||||||
sdlPalette->colors[255].a = 0x00;
|
sdlPalette->colors[255].a = 0x00;
|
||||||
@@ -310,14 +329,27 @@
|
@@ -310,14 +336,35 @@
|
||||||
|
|
||||||
// Set the palette back, and we are done
|
// Set the palette back, and we are done
|
||||||
sdlPalette->colors[255].a = 0xff;
|
sdlPalette->colors[255].a = 0xff;
|
||||||
+#ifdef __ANDROID__
|
+#ifdef __ANDROID__
|
||||||
|
+ if (s_last_render_mode != 1) {
|
||||||
|
+ s_last_render_mode = 1;
|
||||||
|
+ INFO("Game rendering active: OpenGL (3D) mode");
|
||||||
|
+ }
|
||||||
+ xr_frame_end();
|
+ xr_frame_end();
|
||||||
+#endif
|
+#endif
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
+#ifdef __ANDROID__
|
+#ifdef __ANDROID__
|
||||||
|
+ if (s_last_render_mode != 0) {
|
||||||
|
+ s_last_render_mode = 0;
|
||||||
|
+ INFO("Game rendering active: software (2D) mode");
|
||||||
|
+ }
|
||||||
+ // Same software-rendered frame the desktop path below would present
|
+ // Same software-rendered frame the desktop path below would present
|
||||||
+ // via SDL_RenderCopy/SDL_RenderPresent, composited into the XR
|
+ // via SDL_RenderCopy/SDL_RenderPresent, composited into the XR
|
||||||
+ // swapchain image xr_frame_begin() bound above instead - SDL's
|
+ // swapchain image xr_frame_begin() bound above instead - SDL's
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
--- a/src/MacSrc/OpenGL.cc 2026-08-22 09:00:00.000000000 +0200
|
||||||
|
+++ b/src/MacSrc/OpenGL.cc 2026-08-22 09:05:00.000000000 +0200
|
||||||
|
@@ -562,7 +562,14 @@
|
||||||
|
static bool opengl_cache_texture(CachedTexture toCache, grs_bitmap *bm) {
|
||||||
|
SDL_GL_MakeCurrent(window, context);
|
||||||
|
|
||||||
|
- glPixelStorei(GL_UNPACK_ROW_LENGTH, bm->row);
|
||||||
|
+ // GL_UNPACK_ROW_LENGTH must describe the buffer actually passed to
|
||||||
|
+ // glTexImage2D() below (toCache.converted->pixels, a separately
|
||||||
|
+ // allocated SDL_PIXELFORMAT_RGBA32 surface from
|
||||||
|
+ // SDL_ConvertSurfaceFormat()) - not bm->row, the original 8-bit
|
||||||
|
+ // source bitmap's own unrelated row stride. Using bm->row here fed
|
||||||
|
+ // gl4es's GL_UNPACK_ROW_LENGTH emulation the wrong stride whenever
|
||||||
|
+ // the two differed, corrupting the texture upload.
|
||||||
|
+ glPixelStorei(GL_UNPACK_ROW_LENGTH, toCache.converted->pitch / 4);
|
||||||
|
|
||||||
|
if (texturesByBitsPtr.size() < MAX_CACHED_TEXTURES) {
|
||||||
|
// We have enough room, generate the new texture
|
||||||
|
@@ -696,7 +703,10 @@
|
||||||
|
t->lastDrawTime = *tmd_ticks;
|
||||||
|
|
||||||
|
if (isDirty) {
|
||||||
|
- glPixelStorei(GL_UNPACK_ROW_LENGTH, bm->row);
|
||||||
|
+ // Same fix as opengl_cache_texture() above - t->converted->pixels
|
||||||
|
+ // is what's actually uploaded, so its own pitch is what
|
||||||
|
+ // GL_UNPACK_ROW_LENGTH needs to describe, not bm->row.
|
||||||
|
+ glPixelStorei(GL_UNPACK_ROW_LENGTH, t->converted->pitch / 4);
|
||||||
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, bm->w, bm->h, 0, GL_RGBA, GL_UNSIGNED_BYTE, t->converted->pixels);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
--- a/src/MacSrc/OpenGL.cc
|
||||||
|
+++ b/src/MacSrc/OpenGL.cc
|
||||||
|
@@ -566,7 +566,11 @@
|
||||||
|
glDisableVertexAttribArray(0);
|
||||||
|
glDisableVertexAttribArray(tcAttrib);
|
||||||
|
glDisableVertexAttribArray(lightAttrib);
|
||||||
|
- glFlush();
|
||||||
|
+ // glFinish(), not glFlush(): xr_swapchain_release() (called by our
|
||||||
|
+ // caller right after this returns) hands the image to the OpenXR
|
||||||
|
+ // runtime for cross-process consumption, which requires the GPU write
|
||||||
|
+ // to have actually landed, not just be queued.
|
||||||
|
+ glFinish();
|
||||||
|
|
||||||
|
SDL_FreeSurface(uiRgba);
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
--- a/src/MacSrc/OpenGL.cc
|
||||||
|
+++ b/src/MacSrc/OpenGL.cc
|
||||||
|
@@ -525,6 +525,13 @@
|
||||||
|
bind_texture(s_uiTexture);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||||
|
+ // uiRgba is tightly packed, but gl4es implements GL_UNPACK_ROW_LENGTH/
|
||||||
|
+ // GL_UNPACK_ALIGNMENT client-side and persists whatever was last set
|
||||||
|
+ // across draw calls in the same context. Reset both to the packed
|
||||||
|
+ // defaults here so this upload doesn't inherit unrelated unpack state
|
||||||
|
+ // set elsewhere in the frame.
|
||||||
|
+ glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
|
||||||
|
+ glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||||
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, uiRgba->w, uiRgba->h, 0, GL_RGBA, GL_UNSIGNED_BYTE,
|
||||||
|
uiRgba->pixels);
|
||||||
|
|
||||||
Executable
+28
@@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Forces a fully fresh native build - works around stageEngine sometimes
|
||||||
|
# wrongly reporting UP-TO-DATE after android/engine-patches/ or
|
||||||
|
# android/gl4es-patches/ changes (see gradle_stage_engine_stale_cache memory
|
||||||
|
# note). Run from anywhere. Needs JAVA_HOME set to a JDK 17+; defaults to
|
||||||
|
# Android Studio's bundled JDK if found.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
cd "$SCRIPT_DIR"
|
||||||
|
|
||||||
|
if [ -z "${JAVA_HOME:-}" ]; then
|
||||||
|
if [ -d /opt/android-studio/jbr ]; then
|
||||||
|
export JAVA_HOME=/opt/android-studio/jbr
|
||||||
|
else
|
||||||
|
echo "JAVA_HOME not set and no JDK found at /opt/android-studio/jbr - export it yourself" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
./gradlew --stop
|
||||||
|
rm -rf \
|
||||||
|
../build/android-engine \
|
||||||
|
../build/android-gl4es \
|
||||||
|
../build/android-prebuilt \
|
||||||
|
app/.cxx \
|
||||||
|
app/build
|
||||||
|
echo "Done - next Gradle build/Run will rebuild from scratch."
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 381 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 641 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 575 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 707 KiB |
Reference in New Issue
Block a user