Fix 3D texture corruption and stuck/dead-key input bugs
build / build (push) Successful in 2m13s

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:
ml
2026-08-29 19:26:53 +02:00
parent a2fb95e57b
commit d6e438a541
15 changed files with 490 additions and 85 deletions
+64 -1
View File
@@ -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
+41 -10
View File
@@ -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
+132 -6
View File
@@ -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;
} }
+3 -2
View File
@@ -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.
+9
View File
@@ -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);
+28
View File
@@ -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