Repository navigation
Preserve pointer identity when replacing unique Fabric pointer moves - #59922
alleneubank wants to merge 1 commit into
Conversation
|
Hi @alleneubank! Thank you for your pull request and welcome to our community. Action RequiredIn order to merge any pull request (code, docs, etc.), we require contributors to sign our Contributor License Agreement, and we don't seem to have one on file for you. ProcessIn order for us to review and merge your suggested changes, please sign at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need to sign the corporate CLA. Once the CLA is signed, our tooling will perform checks and validations. Afterwards, the pull request will be tagged with If you have received this in error or have any questions, please contact us at cla@meta.com. Thanks! |
|
Thank you for signing our Contributor License Agreement. We can now accept your code for this (and any) Meta Open Source project. Thanks! |
Summary:
Fixes #59918
Status: red/green was observed at runtime on source-built RNTester v0.88.1 (iOS and Android, details below). The proposed GoogleTest fixture has not been compiled or run: no exported open-source runner registers ReactCommon core tests, so the Test Plan asks for a maintainer run.
Queued Fabric
pointermoveevents from different contacts on the same native view can replace one another. This affects the typed iOSPointerEventpayload and Android's dynamic pointer payload. Under JS-thread load, one contact can receive no move callbacks even though both contacts moved. This is different from dropping intermediate samples from the same pointer.Expected: an eligible replacement matches the target, event type, and pointer identity. Two different contacts retain separate moves. Consecutive moves for one contact still coalesce, and the latest same-target event remains an ordering barrier.
Actual: the unique-event replacement compares target and type but omits pointer identity. For one target, queueing primary pointer 2 at x=120 followed by secondary pointer 3 at x=210 yields only pointer 3's move. Reversing their order can instead drop the secondary pointer.
Source and current status
All root-cause links below are pinned to React Native v0.86.0, commit
a632f9efe24bac8b3a113c78469948f55bde0f5d:ValueFactorydiscriminator, including for itsDynamicPointerEventsubclass; checking only the typed discriminator misses Android.On 2026-11-04, the same faulty replacement was present in main at 38f6e192c8b0a04ddf93b0f21c394c24217f9692. Its production file is identical in v0.86.0 and latest stable v0.88.1. The source-built latest-stable observations and remaining runtime gaps are reported below. Bounded public issue/PR searches found no matching report; that is not an exhaustive duplicate check.
Proposed fix against main
The helper reads typed pointer identity directly and dynamic
topPointerMoveidentity through the existing numeric extractor. Optional double values avoid narrowing Android's numeric IDs. Other dynamic event types retain the existing behavior even if they carry a property namedpointerId. The existing same-target scan and its break stay intact. No public signature, discriminator, or object layout is changed. The core-to-view header dependency and extraction cost still need upstream review.Patch base: RN main commit
38f6e192c8b0a04ddf93b0f21c394c24217f9692. The production change and complete test proposal are shown below; the patch contains both.Regression test proposal
The complete proposed GoogleTest fixture follows the adjacent EventQueueProcessorTest.cpp. It enqueues real RawEvents and drains RN EventQueue through EventQueueProcessor and Hermes. The dynamic arm uses the production DynamicEventPayload factory. The 12 parameterized cases cover distinct pointer identity in both orders, same-pointer replacement, pointer/type/phase/nonunique barriers, cross-target behavior, and nonpointer replacement. The last two nonpointer cases are intentionally run under both fixture parameters. Test destination:
packages/react-native/ReactCommon/react/renderer/core/tests/EventQueueTest.cpp.EventQueueTest.cpp
Standalone reproduction
The failing reproducer is an RNTesterPlayground change with native pointer-dispatch opt-ins, separate from the production correction. The complete patch is below. It has no dependency on a gesture library.
38f6e192c8b0a04ddf93b0f21c394c24217f9692and apply only the reproducer patch. Runyarn installandyarn --cwd packages/react-native-codegen buildfrom the repository root. For a latest-stable comparison use v0.88.1; the Android application context differs slightly, so place the sameReactFeatureFlags.dispatchPointerEvents = trueline beforesuper.onCreate()there.bundle install, thenRCT_USE_PREBUILT_RNCORE=0 bundle exec pod installinpackages/rn-tester, and open its generated workspace. On main, a locked Hermes-version mismatch is resolved with the repository CI'sbundle exec pod update hermes-engine --no-repo-update. On Android build:packages:rn-tester:android:app:assembleDebugwith the desired architecture. A prebuilt RN core cannot exercise the C++ correction.The recorder retains at most two contacts, records their real native callbacks, and clears move counters when the second down reaches JS. Callback totals depend on the actual native samples and timing. The fresh-run observations below state which trials have completed.
Complete failing RNTesterPlayground patch
Changelog:
[GENERAL] [FIXED] - Preserve moves from different pointers on the same Fabric event target on iOS and Android.
Test Plan:
Fresh evidence on 2026-11-04: source-built RNTester v0.88.1 (
c30b7f86850d47422637a64c70508e7411dbd8ea), iOS 27.2 arm64 simulator and Android API 35 arm64 emulator. iOS usesRCT_USE_PREBUILT_RNCORE=0; Android builds ReactAndroid from source. Each native arm waits for at least three free cores by the host's admission guard. This is functional evidence, not a performance measurement.The recorder initializes down=1 when first seeing an identity; that field does not independently exclude duplicate down callbacks. Move, up and cancel fields count the callbacks received. The iOS paired driver requests 40 movement steps per contact; Android writes 20 paired movement frames and records the kernel event stream. Those schedules are distinct from JS callback counts. The Android lone swipe API does not establish native ingress counts. Both unpatched paired trials reproduce the loss of one contact's moves while both complete down/up lifecycles. Corrected trials used the same paired input schedules and retained movement for both identities. Same-pointer coalescing is still permitted, so callback totals may differ between runs.
Native GoogleTest target investigation on pinned main (2026-11-05 UTC): no exported OSS runner for the ReactCommon core GoogleTests was found at
38f6e192c8b0a04ddf93b0f21c394c24217f9692. The complete tracked file namespace contains no Buck/TARGETS manifests. Searches of every tracked CMake entry, Podspec, Xcode project, and exported GitHub workflow/action found no core GoogleTest registration. The adjacent known-presentEventQueueProcessorTest.cppwas inspected, so the absent registration is not inferred from a failed filename lookup.OSS CI builds RNTester and Fantom, and runs Fantom’s Jest integration suite. The exported iOS test script uses RNTester/XCTest; the shared test-ios-rntester action defaults
run-unit-teststo false. Test All builds Fantom usingyarn workspace @react-native/fantom buildand runs it withyarn fantom; this does not run these GoogleTests. The core CMake target globs production*.cpp, nottests/*.cpp. Fantom's executable globs only its tester source directories and has no GoogleTest linkage. Swift Package Manager explicitly excludescore/tests.The discovery commands, run before dependency installation, included:
The manifest-registration search returned no matches. The CI search found only unrelated strict-C++-API configuration and an XCTest comment; the real build actions and CMake entries were then read directly.
No runner was invented and no standalone substitute harness was used. Maintainer test step: in the internal ReactCommon test environment, register the supplied
EventQueueTest.cppalongsideEventQueueProcessorTest.cpp, then run the internal target with--gtest_filter=PointerPayloads/EventQueueTest.*, first without the queue correction and then with it. The internal target name and invocation are not available in the exported tree and are deliberately unspecified. Fixture compilation, discovery, and red/green assertions remain unverified; this draft does not claim those results.Not checked: GoogleTest discovery/compilation/execution, more than two contacts, concurrent producers, extraction performance, or a fixed release. This remains an unfinished draft.
A separate Android guest run passed entry, lone input, clearing, paired input, exchanged contact positions, and return navigation. Paired move counts were 19/19, 20/20 with positions exchanged, and 12/12 after return; all contacts had down=1/up=1/cancel=0. Screenshot review approved the supplied light-mode portrait states. Other themes, larger text and other widths were not reviewed.
Reproduction environment
Captured by running
npx @react-native-community/cli infofrom the v0.88.1 RNTester checkout. Tool paths are redacted; all version/configuration fields are preserved. The CLI reports several source-workspace packages as Not Found and architecture booleans as false. Source identity and the actual build are recorded separately; these scanner fields are not runtime evidence. The global CocoaPods binary reported by the CLI differs from the repository-bundled CocoaPods 1.14.2 used for the source build.