diff --git a/packages/react-native/React/Fabric/AppleEventBeat.h b/packages/react-native/React/Fabric/AppleEventBeat.h index 528145e23797..3b0e42aa1af0 100644 --- a/packages/react-native/React/Fabric/AppleEventBeat.h +++ b/packages/react-native/React/Fabric/AppleEventBeat.h @@ -7,7 +7,11 @@ #pragma once +#include #include +#include + +#import #include #include @@ -25,18 +29,27 @@ class RuntimeScheduler; * A synchronous request made while Core Animation is laying out the current * frame (the run loop observer that induces the beat has already run at that * point) is additionally induced from the display phase of the same commit - * cycle, so that its effects are mounted before the frame is presented. + * cycle, so that its effects are mounted before the frame is presented. The + * induce is scheduled on the layer of the requesting surface's root view — + * the tree Core Animation is laying out when the request is made from layout. */ class AppleEventBeat : public EventBeat, public RunLoopObserver::Delegate { public: + /* + * Resolves the layer of a surface's root view. Called on the main thread; + * returns nil when the surface is unknown or its view is not mounted. + */ + using SurfaceLayerResolver = std::function; + AppleEventBeat( std::shared_ptr ownerBox, std::unique_ptr uiRunLoopObserver, - RuntimeScheduler &RuntimeScheduler); + RuntimeScheduler &RuntimeScheduler, + SurfaceLayerResolver surfaceLayerResolver); ~AppleEventBeat() override; - void requestSynchronous() const override; + void requestSynchronous(std::optional surfaceId) const override; #pragma mark - RunLoopObserver::Delegate diff --git a/packages/react-native/React/Fabric/AppleEventBeat.mm b/packages/react-native/React/Fabric/AppleEventBeat.mm index b91adb43c2d6..00d2956e2f42 100644 --- a/packages/react-native/React/Fabric/AppleEventBeat.mm +++ b/packages/react-native/React/Fabric/AppleEventBeat.mm @@ -40,49 +40,25 @@ - (void)display @end -/* - * The windows that can commit a Core Animation transaction: the visible ones - * of every foreground scene. - */ -static NSArray *RCTFlushableWindows(void) -{ - NSMutableArray *windows = [NSMutableArray new]; - for (UIScene *scene in RCTSharedApplication().connectedScenes) { - if (![scene isKindOfClass:[UIWindowScene class]]) { - continue; - } - if (scene.activationState != UISceneActivationStateForegroundActive && - scene.activationState != UISceneActivationStateForegroundInactive) { - continue; - } - for (UIWindow *window in ((UIWindowScene *)scene).windows) { - if (!window.hidden) { - [windows addObject:window]; - } - } - } - if (windows.count == 0) { - // Apps on the legacy UIApplicationDelegate lifecycle own their window - // outside of any scene, so the enumeration above finds nothing. - UIWindow *keyWindow = RCTKeyWindow(); - if (keyWindow != nil) { - [windows addObject:keyWindow]; - } - } - return windows; -} - namespace facebook::react { /* - * Owns the flusher layers and keeps one attached to every window's layer so - * that whichever layer tree is being committed contains one of them. + * Owns the flusher layers, one per surface that has made a synchronous + * request, attached to the layer of that surface's root view. A request made + * from layout runs inside the commit of exactly that tree, so its layer is + * guaranteed a display phase in the current cycle — no assumption about + * which window is key or about all windows committing together. */ class AppleEventBeat::DisplayPhaseFlusher { public: - DisplayPhaseFlusher(std::function callback, std::weak_ptr weakOwner) + DisplayPhaseFlusher( + std::function callback, + std::weak_ptr weakOwner, + SurfaceLayerResolver surfaceLayerResolver) + : surfaceLayerResolver_(std::move(surfaceLayerResolver)) { - // Weak keys: a window that goes away takes its own layer with it. + // Weak keys: a root view that goes away takes its own flusher layer with + // it. layers_ = [NSMapTable weakToStrongObjectsMapTable]; auto sharedCallback = std::make_shared>(std::move(callback)); onDisplay_ = ^{ @@ -101,7 +77,7 @@ - (void)display // The beat can be destroyed on any thread; layer mutations belong on the // main thread. The block only retains the layers, and a display happening // before this executes is made safe by the owner check above. - NSMapTable *layers = layers_; + NSMapTable *layers = layers_; RCTExecuteOnMainQueue(^{ for (RCTEventBeatFlusherLayer *layer in layers.objectEnumerator) { layer.onDisplay = nil; @@ -113,42 +89,47 @@ - (void)display /* * Schedules the callback to run in the display phase of the current (or - * next) Core Animation commit cycle. Main thread only. - * - * Every window gets a layer rather than only the key window: the request can - * come from any of them — a modal and the LogBox are windows of their own — - * and only a layer in a tree that is committed is displayed in this cycle. - * The induce the display triggers is coalescing, so the extra layers cost a - * dirty zero-sized layer each, not extra beats. + * next) Core Animation commit cycle, on the layer tree of the surface's + * root view. Main thread only. Does nothing when the surface has no mounted + * view; the run loop observer then processes the request on its ordinary + * schedule instead. */ - void schedule() const + void schedule(SurfaceId surfaceId) const { - for (UIWindow *window in RCTFlushableWindows()) { - RCTEventBeatFlusherLayer *layer = [layers_ objectForKey:window]; - if (layer == nil) { - layer = [RCTEventBeatFlusherLayer new]; - layer.frame = CGRectZero; - layer.onDisplay = onDisplay_; - [layers_ setObject:layer forKey:window]; - } - if (layer.superlayer != window.layer) { - [window.layer addSublayer:layer]; - } - [layer setNeedsDisplay]; + CALayer *hostLayer = surfaceLayerResolver_ ? surfaceLayerResolver_(surfaceId) : nil; + if (hostLayer == nil) { + return; + } + RCTEventBeatFlusherLayer *layer = [layers_ objectForKey:hostLayer]; + if (layer == nil) { + layer = [RCTEventBeatFlusherLayer new]; + layer.frame = CGRectZero; + layer.onDisplay = onDisplay_; + [layers_ setObject:layer forKey:hostLayer]; + } + if (layer.superlayer != hostLayer) { + [layer removeFromSuperlayer]; + [hostLayer addSublayer:layer]; } + [layer setNeedsDisplay]; } private: - NSMapTable *layers_; + SurfaceLayerResolver surfaceLayerResolver_; + NSMapTable *layers_; void (^onDisplay_)(void); }; AppleEventBeat::AppleEventBeat(std::shared_ptr ownerBox, std::unique_ptr uiRunLoopObserver, - RuntimeScheduler &runtimeScheduler) + RuntimeScheduler &runtimeScheduler, + SurfaceLayerResolver surfaceLayerResolver) : EventBeat(std::move(ownerBox), runtimeScheduler), uiRunLoopObserver_(std::move(uiRunLoopObserver)), - displayPhaseFlusher_(std::make_unique([this]() { induce(); }, ownerBox_->owner)) + displayPhaseFlusher_(std::make_unique( + [this]() { induce(); }, + ownerBox_->owner, + std::move(surfaceLayerResolver))) { uiRunLoopObserver_->setDelegate(this); uiRunLoopObserver_->enable(); @@ -156,9 +137,9 @@ void schedule() const AppleEventBeat::~AppleEventBeat() = default; -void AppleEventBeat::requestSynchronous() const +void AppleEventBeat::requestSynchronous(std::optional surfaceId) const { - EventBeat::requestSynchronous(); + EventBeat::requestSynchronous(surfaceId); // The run loop observer that ordinarily induces the beat runs before Core // Animation commits the frame. A synchronous request made while Core @@ -167,8 +148,8 @@ void schedule() const // Scheduling an induce in the display phase of the current commit cycle // processes it before this frame is presented. Multiple requests within one // cycle coalesce into a single induce. - if (RCTIsMainQueue()) { - displayPhaseFlusher_->schedule(); + if (surfaceId.has_value() && RCTIsMainQueue()) { + displayPhaseFlusher_->schedule(*surfaceId); } } diff --git a/packages/react-native/React/Fabric/RCTSurfacePresenter.mm b/packages/react-native/React/Fabric/RCTSurfacePresenter.mm index 0e4bbe376463..6035710e11d5 100644 --- a/packages/react-native/React/Fabric/RCTSurfacePresenter.mm +++ b/packages/react-native/React/Fabric/RCTSurfacePresenter.mm @@ -292,11 +292,18 @@ - (RCTScheduler *)_createScheduler toolbox.runtimeExecutor = runtimeExecutor; toolbox.bridgelessBindingsExecutor = _bridgelessBindingsExecutor; - toolbox.eventBeatFactory = - [runtimeScheduler](std::shared_ptr ownerBox) -> std::unique_ptr { + RCTSurfaceRegistry *surfaceRegistry = _surfaceRegistry; + toolbox.eventBeatFactory = [runtimeScheduler, + surfaceRegistry](std::shared_ptr ownerBox) -> std::unique_ptr { auto runLoopObserver = std::make_unique(RunLoopObserver::Activity::BeforeWaiting, ownerBox->owner); - return std::make_unique(std::move(ownerBox), std::move(runLoopObserver), *runtimeScheduler); + // The registry is thread-safe, but the resolver is only called on the + // main thread from the display-phase flusher. + auto surfaceLayerResolver = [surfaceRegistry](SurfaceId surfaceId) -> CALayer * { + return [surfaceRegistry surfaceForRootTag:surfaceId].view.layer; + }; + return std::make_unique( + std::move(ownerBox), std::move(runLoopObserver), *runtimeScheduler, std::move(surfaceLayerResolver)); }; RCTScheduler *scheduler = [[RCTScheduler alloc] initWithToolbox:toolbox]; diff --git a/packages/react-native/ReactCommon/react/renderer/core/EventBeat.cpp b/packages/react-native/ReactCommon/react/renderer/core/EventBeat.cpp index 839e2f19f90b..aff7c248c4f0 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/EventBeat.cpp +++ b/packages/react-native/ReactCommon/react/renderer/core/EventBeat.cpp @@ -25,7 +25,8 @@ void EventBeat::request() const { isEventBeatRequested_ = true; } -void EventBeat::requestSynchronous() const { +void EventBeat::requestSynchronous( + std::optional /*surfaceId*/) const { react_native_assert( beatCallback_ && "Unexpected state: EventBeat::setBeatCallback was not called before EventBeat::requestSynchronous."); diff --git a/packages/react-native/ReactCommon/react/renderer/core/EventBeat.h b/packages/react-native/ReactCommon/react/renderer/core/EventBeat.h index b8da33cc9ac2..d24f0cfcb993 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/EventBeat.h +++ b/packages/react-native/ReactCommon/react/renderer/core/EventBeat.h @@ -7,6 +7,8 @@ #pragma once +#include +#include #include #include #include @@ -109,7 +111,13 @@ class EventBeat { * Both JS and UI thread are * blocked. */ - virtual void requestSynchronous() const; + /* + * The surface the synchronous request originates from, when known, lets + * platform implementations schedule an induce where that surface renders. + * Without it the induce follows the platform's ordinary beat timing. + */ + virtual void requestSynchronous( + std::optional surfaceId = std::nullopt) const; /* * Induces the next beat to happen as soon as possible. diff --git a/packages/react-native/ReactCommon/react/renderer/core/EventDispatcher.cpp b/packages/react-native/ReactCommon/react/renderer/core/EventDispatcher.cpp index 5fa5e6821a51..3c97aac5fb10 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/EventDispatcher.cpp +++ b/packages/react-native/ReactCommon/react/renderer/core/EventDispatcher.cpp @@ -37,8 +37,9 @@ void EventDispatcher::dispatchEvent(RawEvent&& rawEvent) const { eventQueue_.enqueueEvent(std::move(rawEvent)); } -void EventDispatcher::experimental_flushSync() const { - eventQueue_.experimental_flushSync(); +void EventDispatcher::experimental_flushSync( + std::optional surfaceId) const { + eventQueue_.experimental_flushSync(surfaceId); } void EventDispatcher::dispatchStateUpdate( diff --git a/packages/react-native/ReactCommon/react/renderer/core/EventDispatcher.h b/packages/react-native/ReactCommon/react/renderer/core/EventDispatcher.h index 88a9a953e423..def93019970c 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/EventDispatcher.h +++ b/packages/react-native/ReactCommon/react/renderer/core/EventDispatcher.h @@ -7,6 +7,8 @@ #pragma once +#include +#include #include #include #include @@ -44,7 +46,7 @@ class EventDispatcher { /* * Experimental API exposed to support EventEmitter::experimental_flushSync. */ - void experimental_flushSync() const; + void experimental_flushSync(std::optional surfaceId) const; /* * Dispatches a raw event with asynchronous batched priority. Before the diff --git a/packages/react-native/ReactCommon/react/renderer/core/EventEmitter.cpp b/packages/react-native/ReactCommon/react/renderer/core/EventEmitter.cpp index 2d6ac50e3730..c6402bc52a52 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/EventEmitter.cpp +++ b/packages/react-native/ReactCommon/react/renderer/core/EventEmitter.cpp @@ -7,6 +7,8 @@ #include "EventEmitter.h" +#include + #include #include #include @@ -229,6 +231,14 @@ void EventEmitter::setEnabled(bool enabled) { } } +std::optional EventEmitter::getSurfaceId() const { + std::scoped_lock lock(DispatchMutex()); + if (auto shadowNodeFamily = shadowNodeFamily_.lock()) { + return shadowNodeFamily->getSurfaceId(); + } + return std::nullopt; +} + void EventEmitter::setShadowNodeFamily( std::weak_ptr shadowNodeFamily) { shadowNodeFamily_ = std::move(shadowNodeFamily); diff --git a/packages/react-native/ReactCommon/react/renderer/core/EventEmitter.h b/packages/react-native/ReactCommon/react/renderer/core/EventEmitter.h index f3e9a4c334e7..0297477a4ce1 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/EventEmitter.h +++ b/packages/react-native/ReactCommon/react/renderer/core/EventEmitter.h @@ -7,6 +7,7 @@ #pragma once +#include #include #include @@ -63,6 +64,11 @@ class EventEmitter { const SharedEventTarget &getEventTarget() const; + /* + * The surface of the corresponding ShadowNodeFamily, when one is attached. + */ + std::optional getSurfaceId() const; + /* * Experimental API that will change in the future. */ @@ -74,8 +80,10 @@ class EventEmitter { return; } + auto surfaceId = getSurfaceId(); + syncFunc(); - eventDispatcher->experimental_flushSync(); + eventDispatcher->experimental_flushSync(surfaceId); } /* diff --git a/packages/react-native/ReactCommon/react/renderer/core/EventQueue.cpp b/packages/react-native/ReactCommon/react/renderer/core/EventQueue.cpp index 6e99fd71bb7a..ef5cd7c986f6 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/EventQueue.cpp +++ b/packages/react-native/ReactCommon/react/renderer/core/EventQueue.cpp @@ -86,8 +86,9 @@ void EventQueue::onEnqueue() const { eventBeat_->request(); } -void EventQueue::experimental_flushSync() const { - eventBeat_->requestSynchronous(); +void EventQueue::experimental_flushSync( + std::optional surfaceId) const { + eventBeat_->requestSynchronous(surfaceId); } void EventQueue::onBeat(jsi::Runtime& runtime) const { diff --git a/packages/react-native/ReactCommon/react/renderer/core/EventQueue.h b/packages/react-native/ReactCommon/react/renderer/core/EventQueue.h index f525767478aa..f399ec8f58bb 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/EventQueue.h +++ b/packages/react-native/ReactCommon/react/renderer/core/EventQueue.h @@ -7,6 +7,8 @@ #pragma once +#include +#include #include #include #include @@ -61,7 +63,7 @@ class EventQueue { /* * Experimental API exposed to support EventEmitter::experimental_flushSync. */ - void experimental_flushSync() const; + void experimental_flushSync(std::optional surfaceId) const; protected: /*