Skip to content

Fix native FlyByCamera touch gestures and input regression coverage - #2981

Open
toaster0123 wants to merge 2 commits into
jMonkeyEngine:masterfrom
toaster0123:fix/flycam-touch-pointer-lifecycle-2943
Open

toaster0123 wants to merge 2 commits into
jMonkeyEngine:masterfrom
toaster0123:fix/flycam-touch-pointer-lifecycle-2943

Conversation

@toaster0123

Copy link
Copy Markdown

Summary

Supersedes #2943 and builds on its native-touch implementation by Copilot and @riccardobl, addressing the review at #2943 (review).

  • Register native touch input with FlyByCamera and latch the first unconsumed DOWN, including nonzero pointer IDs when another control has consumed an earlier pointer.
  • Apply only that pointer's movement; release it on UP, and require a fresh DOWN rather than inheriting a remaining finger. Clear gesture ownership on disable/unregister.
  • Avoid duplicate rotation when mouse emulation is enabled, while retaining touch down/up lifecycle handling.
  • Document native touch-drag behavior in both drag-to-rotate modes and preserve existing yaw/pitch, inverted-Y, and rotation-speed semantics.
  • Exercise the real InputManager event pipeline with queued touch and mouse events instead of invoking FlyByCamera.onTouch() directly.

Related: #2929. The original PR remains open and unchanged.

Verification

  • Focused Gradle test task: 35/35 FlyByCamera tests pass.
  • Direct javac --release 8 -Xlint:unchecked -Xlint:-options -Werror compilation of all core main/plugin/tool sources passes; the same 35 cases also pass under JUnit Platform 6.1.3 directly.
  • Regression check: the new suite fails 14 cases against the original Add native touch rotation to FlyByCamera #2943 implementation, and passes all 35 with this change.
  • Full :jme3-core:build passes after a clean rebuild: 518 test cases, 0 failures, 0 errors, 1 skipped; includes core Javadoc, jars, and configured Checkstyle tasks.
  • Sandbox-only build configuration forwards the existing network proxy/truststore to test JVMs and preloads the existing Mockito dependency as a Java agent, avoiding unsupported dynamic self-attachment. These settings are outside the repository. The first unconfigured full run failed on those environment constraints; no tests were excluded to obtain the passing run.
  • Existing renderer Checkstyle warnings remain; this repository scopes its Checkstyle tasks to renderer sources. git diff --check passes for this change.
  • Android device/manual gesture testing and the whole multi-module engine build were not run locally. Remote CI is pending publication.

Supersedes jMonkeyEngine#2943, retaining its native touch mapping and rotation math while addressing pointer ownership, mouse emulation, lifecycle resets, and input-pipeline regression coverage.

Based on commits 61bacec and 84f4232 from the original PR.

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: riccardobl <4943530+riccardobl@users.noreply.github.com>
@riccardobl
riccardobl marked this pull request as ready for review September 30, 2026 16:49
Comment on lines +610 to +615
case MOVE:
if (touchPointerId == -1 || event.getPointerId() != touchPointerId
|| (inputManager != null && inputManager.isSimulateMouse())) {
return;
}
break;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A zero-delta MOVE still falls through to two rotateCamera calls, and each one ends in cam.setAxes(...) → onFrameChange(), which rebuilds all six world frustum planes, the view matrix, and the view-projection matrix. Android delivers batched MOVE events every frame while a pointer is down, so a finger that just rests on the screen (or rests while a virtual joystick is in use) pays that cost ~120 times a second for no visible change. A cheap guard keeps the behaviour identical and the existing zeroDeltaDoesNotRotateOrReleasePointer test still holds.

Suggested change
case MOVE:
if (touchPointerId == -1 || event.getPointerId() != touchPointerId
|| (inputManager != null && inputManager.isSimulateMouse())) {
return;
}
break;
case MOVE:
if (touchPointerId == -1 || event.getPointerId() != touchPointerId
|| (inputManager != null && inputManager.isSimulateMouse())) {
return;
}
if (event.getDeltaX() == 0f && event.getDeltaY() == 0f) {
return;
}
break;

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed: MOVE now returns immediately when both deltas are zero, after the pointer/emulation checks, without clearing pointer ownership. I strengthened the queued-input regression to count Camera.onFrameChange() calls: three idle moves (including signed zero) trigger no rebuilds, and a subsequent nonzero move from the same pointer still rotates. The strengthened test fails on the previous implementation with six unnecessary rebuilds and passes with the guard. All 35 FlyByCamera cases and the full core build pass. Please re-review the updated patch.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed — the guard landed exactly where it should: after the touchPointerId / isSimulateMouse() checks, so pointer ownership is untouched by a zero-delta MOVE and the drag keeps working. Signed zero is covered too (-0f == 0f is true in Java).

The strengthened test is the part I like most: counting onFrameChange() calls makes this a real performance regression rather than just "rotation didn't change", and it still asserts the follow-up nonzero move rotates from the same pointer. Good call.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified the guard in the current patch: MOVE returns before either rotateCamera call when both deltas are zero (signed zero included, since -0f == 0f), and pointer ownership is untouched. The rebuild-counting test is a nice way to pin that down. Nothing blocking from me.

private static final String FLYCAM_JOYSTICK_UP = "FLYCAM_JoystickUp";
private static final String FLYCAM_JOYSTICK_DOWN = "FLYCAM_JoystickDown";
private static final String FLYCAM_TOUCH = "FLYCAM_Touch";
private static final float TOUCH_ROTATION_SCALE = 1f / 1024f;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small maintainability note: 1f / 1024f mirrors the hard-coded 1024f in InputManager.onMouseMotionEventQueued, and keeping touch and mouse rotation in lockstep is the whole point of this change. If the engine's mouse scale is ever tweaked, this constant would silently drift out of parity and the hard-coded 1024f expectations in the test would fail with a confusing message. It would be more robust to lift the scale into a shared constant on InputManager and use it from both sides — happy to see that as a follow-up rather than blocking on it here.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in this update with a package-private InputManager.MOUSE_AXIS_SCALE constant shared by the mouse X/Y dispatch path and native FlyByCamera touch rotation. This removes the duplicate production scale without adding public API or changing the existing power-of-two scaling. The tests intentionally retain independent numeric expectations for the current sensitivity, alongside the real mouse/touch parity cases for mixed-axis movement, inverted Y, and rotation speed, so accidental scale changes remain detectable.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checked, and this is the right shape. InputManager.MOUSE_AXIS_SCALE is package-private (no new public API), and both the mouse X/Y dispatch in onMouseMotionEventQueued and the native touch rotation in FlyByCamera read from it, so the two paths can't drift. 1f / 1024f is a compile-time constant and an exact power of two, so the multiplication is bit-identical to the old / 1024f — mouse sensitivity is unchanged. Nice follow-up anyway.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice — InputManager.MOUSE_AXIS_SCALE is used by both onMouseMotionEventQueued (X and Y) and the native touch path, and keeping it package-private keeps it out of the public API. Good call on keeping the hard-coded 1024f expectations in the tests so an accidental scale change still shows up as a clear failure.

@jaime-jmebot jaime-jmebot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work on the pointer lifecycle — the first-unconsumed-DOWN latch, the fresh-DOWN-only rule, and the isSimulateMouse() de-duplication all look right to me, and I traced the yaw/pitch math against assertDirection and it checks out.

  • The test suite is the highlight: driving the real InputManager queue instead of poking onTouch() directly is what makes the consumed-pointer and isSimulateMouse cases actually meaningful, and the touch-vs-mouse parity test is exactly the kind of regression net this needed.
  • One small fix: bail out of MOVE when both deltas are zero, otherwise a resting finger triggers two full camera matrix rebuilds per frame (inline comment has the patch).
  • Non-blocking: TOUCH_ROTATION_SCALE duplicates the 1024f in InputManager; sharing one constant would keep the parity guarantee from drifting.

Once the zero-delta guard lands I think this is good to go.

Address both review comments on PR jMonkeyEngine#2981. Preserve pointer ownership for zero-delta moves and test that no camera frame rebuild occurs before the next real drag. Keep touch and mouse scaling in one package-private InputManager constant while retaining independent numeric regression expectations.

@jaime-jmebot jaime-jmebot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both follow-ups are addressed and the patch reads well now.

  • The zero-delta MOVE guard sits after the pointer/emulation checks, so it skips the rotation without dropping the active pointer — exactly the behaviour the previous version got wrong. The onFrameChange() counting in the test turns it into a genuine regression guard, and it still proves a resting finger doesn't permanently kill the drag.
  • InputManager.MOUSE_AXIS_SCALE is package-private, shared by the mouse dispatch and the touch path, and 1f / 1024f is an exact power of two, so mouse sensitivity is bit-for-bit unchanged. Keeping literal 1024f expectations in the tests still catches accidental scale changes.

The pointer lifecycle (first unconsumed DOWN, fresh-DOWN-only release rule, reset on disable/unregister) and the InputManager-driven tests all still check out on re-read. Nice work — approved from me.

@jaime-jmebot jaime-jmebot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both points from the last round are addressed in the current patch:

  • MOVE now bails out on a zero delta (signed zero included) before either rotateCamera call, so a resting finger no longer triggers a full frustum/view/view-projection rebuild every frame, and the active pointer survives. The rebuild-counting test pins this down.
  • InputManager.MOUSE_AXIS_SCALE is shared by the mouse X/Y dispatch path and the native touch rotation, package-private so no new public API, with the tests keeping independent numeric expectations as a tripwire.

Pointer ownership, the fresh-DOWN rule, isSimulateMouse de-duplication, and the touch-vs-mouse parity coverage all look good to me. Approved — nice work.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants