summaryrefslogtreecommitdiffstats
path: root/gfx/layers
diff options
context:
space:
mode:
Diffstat (limited to 'gfx/layers')
-rw-r--r--gfx/layers/AndroidHardwareBuffer.cpp346
-rw-r--r--gfx/layers/AndroidHardwareBuffer.h181
-rw-r--r--gfx/layers/AnimationHelper.cpp836
-rw-r--r--gfx/layers/AnimationHelper.h180
-rw-r--r--gfx/layers/AnimationInfo.cpp992
-rw-r--r--gfx/layers/AnimationInfo.h164
-rw-r--r--gfx/layers/AnimationStorageData.h117
-rw-r--r--gfx/layers/AtomicRefCountedWithFinalize.h207
-rw-r--r--gfx/layers/AxisPhysicsMSDModel.cpp78
-rw-r--r--gfx/layers/AxisPhysicsMSDModel.h83
-rw-r--r--gfx/layers/AxisPhysicsModel.cpp101
-rw-r--r--gfx/layers/AxisPhysicsModel.h123
-rw-r--r--gfx/layers/BSPTree.cpp133
-rw-r--r--gfx/layers/BSPTree.h144
-rw-r--r--gfx/layers/BufferTexture.cpp533
-rw-r--r--gfx/layers/BufferTexture.h129
-rw-r--r--gfx/layers/BuildConstants.h64
-rw-r--r--gfx/layers/CanvasDrawEventRecorder.cpp518
-rw-r--r--gfx/layers/CanvasDrawEventRecorder.h285
-rw-r--r--gfx/layers/CanvasRenderer.cpp135
-rw-r--r--gfx/layers/CanvasRenderer.h151
-rw-r--r--gfx/layers/CompositionRecorder.cpp114
-rw-r--r--gfx/layers/CompositionRecorder.h78
-rw-r--r--gfx/layers/Compositor.cpp241
-rw-r--r--gfx/layers/Compositor.h416
-rw-r--r--gfx/layers/CompositorAnimationStorage.cpp429
-rw-r--r--gfx/layers/CompositorAnimationStorage.h213
-rw-r--r--gfx/layers/CompositorTypes.cpp53
-rw-r--r--gfx/layers/CompositorTypes.h292
-rw-r--r--gfx/layers/D3D11ShareHandleImage.cpp347
-rw-r--r--gfx/layers/D3D11ShareHandleImage.h116
-rw-r--r--gfx/layers/D3D11TextureIMFSampleImage.cpp108
-rw-r--r--gfx/layers/D3D11TextureIMFSampleImage.h107
-rw-r--r--gfx/layers/D3D11YCbCrImage.cpp432
-rw-r--r--gfx/layers/D3D11YCbCrImage.h94
-rw-r--r--gfx/layers/D3D9SurfaceImage.cpp275
-rw-r--r--gfx/layers/D3D9SurfaceImage.h122
-rw-r--r--gfx/layers/DMABUFSurfaceImage.cpp69
-rw-r--r--gfx/layers/DMABUFSurfaceImage.h37
-rw-r--r--gfx/layers/DcompSurfaceImage.cpp150
-rw-r--r--gfx/layers/DcompSurfaceImage.h131
-rw-r--r--gfx/layers/DirectionUtils.h62
-rw-r--r--gfx/layers/Effects.cpp27
-rw-r--r--gfx/layers/Effects.h201
-rw-r--r--gfx/layers/FrameMetrics.cpp342
-rw-r--r--gfx/layers/FrameMetrics.h1114
-rw-r--r--gfx/layers/GLImages.cpp99
-rw-r--r--gfx/layers/GLImages.h96
-rw-r--r--gfx/layers/GPUVideoImage.h104
-rw-r--r--gfx/layers/IMFYCbCrImage.cpp151
-rw-r--r--gfx/layers/IMFYCbCrImage.h43
-rw-r--r--gfx/layers/IPDLActor.h59
-rw-r--r--gfx/layers/ImageContainer.cpp1005
-rw-r--r--gfx/layers/ImageContainer.h943
-rw-r--r--gfx/layers/ImageDataSerializer.cpp359
-rw-r--r--gfx/layers/ImageDataSerializer.h113
-rw-r--r--gfx/layers/ImageTypes.h130
-rw-r--r--gfx/layers/LayerUserData.h28
-rw-r--r--gfx/layers/LayersTypes.cpp84
-rw-r--r--gfx/layers/LayersTypes.h523
-rw-r--r--gfx/layers/MacIOSurfaceHelpers.cpp183
-rw-r--r--gfx/layers/MacIOSurfaceHelpers.h30
-rw-r--r--gfx/layers/MacIOSurfaceImage.cpp250
-rw-r--r--gfx/layers/MacIOSurfaceImage.h74
-rw-r--r--gfx/layers/MemoryPressureObserver.cpp77
-rw-r--r--gfx/layers/MemoryPressureObserver.h59
-rw-r--r--gfx/layers/NativeLayer.h245
-rw-r--r--gfx/layers/NativeLayerCA.h481
-rw-r--r--gfx/layers/NativeLayerCA.mm1990
-rw-r--r--gfx/layers/NativeLayerWayland.cpp760
-rw-r--r--gfx/layers/NativeLayerWayland.h182
-rw-r--r--gfx/layers/OOPCanvasRenderer.h53
-rw-r--r--gfx/layers/PersistentBufferProvider.cpp674
-rw-r--r--gfx/layers/PersistentBufferProvider.h273
-rw-r--r--gfx/layers/ProfilerScreenshots.cpp140
-rw-r--r--gfx/layers/ProfilerScreenshots.h120
-rw-r--r--gfx/layers/RecordedCanvasEventImpl.h597
-rw-r--r--gfx/layers/RemoteTextureMap.cpp1028
-rw-r--r--gfx/layers/RemoteTextureMap.h296
-rw-r--r--gfx/layers/RepaintRequest.cpp27
-rw-r--r--gfx/layers/RepaintRequest.h337
-rw-r--r--gfx/layers/SampleTime.cpp75
-rw-r--r--gfx/layers/SampleTime.h69
-rw-r--r--gfx/layers/ScreenshotGrabber.cpp229
-rw-r--r--gfx/layers/ScreenshotGrabber.h139
-rw-r--r--gfx/layers/ScrollableLayerGuid.cpp77
-rw-r--r--gfx/layers/ScrollableLayerGuid.h86
-rw-r--r--gfx/layers/ScrollbarData.h139
-rw-r--r--gfx/layers/ShareableCanvasRenderer.cpp229
-rw-r--r--gfx/layers/ShareableCanvasRenderer.h63
-rw-r--r--gfx/layers/SourceSurfaceSharedData.cpp277
-rw-r--r--gfx/layers/SourceSurfaceSharedData.h345
-rw-r--r--gfx/layers/SurfacePool.h76
-rw-r--r--gfx/layers/SurfacePoolCA.h288
-rw-r--r--gfx/layers/SurfacePoolCA.mm440
-rw-r--r--gfx/layers/SurfacePoolWayland.cpp246
-rw-r--r--gfx/layers/SurfacePoolWayland.h127
-rw-r--r--gfx/layers/SyncObject.cpp50
-rw-r--r--gfx/layers/SyncObject.h75
-rw-r--r--gfx/layers/TextureSourceProvider.cpp19
-rw-r--r--gfx/layers/TextureSourceProvider.h76
-rw-r--r--gfx/layers/TextureWrapperImage.cpp49
-rw-r--r--gfx/layers/TextureWrapperImage.h37
-rw-r--r--gfx/layers/TransactionIdAllocator.h93
-rw-r--r--gfx/layers/TreeTraversal.h276
-rw-r--r--gfx/layers/UpdateImageHelper.h82
-rw-r--r--gfx/layers/ZoomConstraints.cpp23
-rw-r--r--gfx/layers/ZoomConstraints.h66
-rw-r--r--gfx/layers/apz/public/APZInputBridge.h297
-rw-r--r--gfx/layers/apz/public/APZPublicUtils.h82
-rw-r--r--gfx/layers/apz/public/APZSampler.h154
-rw-r--r--gfx/layers/apz/public/APZUpdater.h236
-rw-r--r--gfx/layers/apz/public/CompositorController.h33
-rw-r--r--gfx/layers/apz/public/GeckoContentController.h187
-rw-r--r--gfx/layers/apz/public/GeckoContentControllerTypes.h68
-rw-r--r--gfx/layers/apz/public/IAPZCTreeManager.h150
-rw-r--r--gfx/layers/apz/public/MatrixMessage.h64
-rw-r--r--gfx/layers/apz/src/APZCTreeManager.cpp3742
-rw-r--r--gfx/layers/apz/src/APZCTreeManager.h1064
-rw-r--r--gfx/layers/apz/src/APZInputBridge.cpp435
-rw-r--r--gfx/layers/apz/src/APZPublicUtils.cpp111
-rw-r--r--gfx/layers/apz/src/APZSampler.cpp216
-rw-r--r--gfx/layers/apz/src/APZUpdater.cpp546
-rw-r--r--gfx/layers/apz/src/APZUtils.cpp118
-rw-r--r--gfx/layers/apz/src/APZUtils.h220
-rw-r--r--gfx/layers/apz/src/AndroidAPZ.cpp36
-rw-r--r--gfx/layers/apz/src/AndroidAPZ.h34
-rw-r--r--gfx/layers/apz/src/AndroidFlingPhysics.cpp218
-rw-r--r--gfx/layers/apz/src/AndroidFlingPhysics.h45
-rw-r--r--gfx/layers/apz/src/AndroidVelocityTracker.cpp288
-rw-r--r--gfx/layers/apz/src/AndroidVelocityTracker.h42
-rw-r--r--gfx/layers/apz/src/AsyncDragMetrics.h54
-rw-r--r--gfx/layers/apz/src/AsyncPanZoomAnimation.h101
-rw-r--r--gfx/layers/apz/src/AsyncPanZoomController.cpp6654
-rw-r--r--gfx/layers/apz/src/AsyncPanZoomController.h1943
-rw-r--r--gfx/layers/apz/src/AutoDirWheelDeltaAdjuster.h89
-rw-r--r--gfx/layers/apz/src/AutoscrollAnimation.cpp93
-rw-r--r--gfx/layers/apz/src/AutoscrollAnimation.h42
-rw-r--r--gfx/layers/apz/src/Axis.cpp733
-rw-r--r--gfx/layers/apz/src/Axis.h462
-rw-r--r--gfx/layers/apz/src/CheckerboardEvent.cpp195
-rw-r--r--gfx/layers/apz/src/CheckerboardEvent.h218
-rw-r--r--gfx/layers/apz/src/DesktopFlingPhysics.h67
-rw-r--r--gfx/layers/apz/src/DragTracker.cpp59
-rw-r--r--gfx/layers/apz/src/DragTracker.h39
-rw-r--r--gfx/layers/apz/src/ExpectedGeckoMetrics.cpp26
-rw-r--r--gfx/layers/apz/src/ExpectedGeckoMetrics.h44
-rw-r--r--gfx/layers/apz/src/FlingAccelerator.cpp128
-rw-r--r--gfx/layers/apz/src/FlingAccelerator.h59
-rw-r--r--gfx/layers/apz/src/FocusState.cpp225
-rw-r--r--gfx/layers/apz/src/FocusState.h175
-rw-r--r--gfx/layers/apz/src/FocusTarget.cpp233
-rw-r--r--gfx/layers/apz/src/FocusTarget.h71
-rw-r--r--gfx/layers/apz/src/GenericFlingAnimation.h207
-rw-r--r--gfx/layers/apz/src/GenericScrollAnimation.cpp120
-rw-r--r--gfx/layers/apz/src/GenericScrollAnimation.h59
-rw-r--r--gfx/layers/apz/src/GestureEventListener.cpp663
-rw-r--r--gfx/layers/apz/src/GestureEventListener.h285
-rw-r--r--gfx/layers/apz/src/HitTestingTreeNode.cpp419
-rw-r--r--gfx/layers/apz/src/HitTestingTreeNode.h270
-rw-r--r--gfx/layers/apz/src/IAPZHitTester.cpp78
-rw-r--r--gfx/layers/apz/src/IAPZHitTester.h91
-rw-r--r--gfx/layers/apz/src/InputBlockState.cpp840
-rw-r--r--gfx/layers/apz/src/InputBlockState.h544
-rw-r--r--gfx/layers/apz/src/InputQueue.cpp1090
-rw-r--r--gfx/layers/apz/src/InputQueue.h277
-rw-r--r--gfx/layers/apz/src/KeyboardMap.cpp170
-rw-r--r--gfx/layers/apz/src/KeyboardMap.h118
-rw-r--r--gfx/layers/apz/src/KeyboardScrollAction.cpp37
-rw-r--r--gfx/layers/apz/src/KeyboardScrollAction.h48
-rw-r--r--gfx/layers/apz/src/Overscroll.h250
-rw-r--r--gfx/layers/apz/src/OverscrollHandoffState.cpp228
-rw-r--r--gfx/layers/apz/src/OverscrollHandoffState.h203
-rw-r--r--gfx/layers/apz/src/PotentialCheckerboardDurationTracker.cpp74
-rw-r--r--gfx/layers/apz/src/PotentialCheckerboardDurationTracker.h61
-rw-r--r--gfx/layers/apz/src/QueuedInput.cpp44
-rw-r--r--gfx/layers/apz/src/QueuedInput.h63
-rw-r--r--gfx/layers/apz/src/RecentEventsBuffer.h83
-rw-r--r--gfx/layers/apz/src/SampledAPZCState.cpp111
-rw-r--r--gfx/layers/apz/src/SampledAPZCState.h72
-rw-r--r--gfx/layers/apz/src/ScrollThumbUtils.cpp341
-rw-r--r--gfx/layers/apz/src/ScrollThumbUtils.h51
-rw-r--r--gfx/layers/apz/src/SimpleVelocityTracker.cpp135
-rw-r--r--gfx/layers/apz/src/SimpleVelocityTracker.h54
-rw-r--r--gfx/layers/apz/src/SmoothMsdScrollAnimation.cpp139
-rw-r--r--gfx/layers/apz/src/SmoothMsdScrollAnimation.h61
-rw-r--r--gfx/layers/apz/src/SmoothScrollAnimation.cpp46
-rw-r--r--gfx/layers/apz/src/SmoothScrollAnimation.h37
-rw-r--r--gfx/layers/apz/src/WRHitTester.cpp247
-rw-r--r--gfx/layers/apz/src/WRHitTester.h26
-rw-r--r--gfx/layers/apz/src/WheelScrollAnimation.cpp64
-rw-r--r--gfx/layers/apz/src/WheelScrollAnimation.h30
-rw-r--r--gfx/layers/apz/test/gtest/APZCBasicTester.h102
-rw-r--r--gfx/layers/apz/test/gtest/APZCTreeManagerTester.h223
-rw-r--r--gfx/layers/apz/test/gtest/APZTestAccess.cpp27
-rw-r--r--gfx/layers/apz/test/gtest/APZTestAccess.h36
-rw-r--r--gfx/layers/apz/test/gtest/APZTestCommon.cpp15
-rw-r--r--gfx/layers/apz/test/gtest/APZTestCommon.h1051
-rw-r--r--gfx/layers/apz/test/gtest/InputUtils.h149
-rw-r--r--gfx/layers/apz/test/gtest/MockHitTester.cpp38
-rw-r--r--gfx/layers/apz/test/gtest/MockHitTester.h37
-rw-r--r--gfx/layers/apz/test/gtest/TestAxisLock.cpp645
-rw-r--r--gfx/layers/apz/test/gtest/TestBasic.cpp639
-rw-r--r--gfx/layers/apz/test/gtest/TestEventRegions.cpp199
-rw-r--r--gfx/layers/apz/test/gtest/TestEventResult.cpp476
-rw-r--r--gfx/layers/apz/test/gtest/TestFlingAcceleration.cpp252
-rw-r--r--gfx/layers/apz/test/gtest/TestGestureDetector.cpp849
-rw-r--r--gfx/layers/apz/test/gtest/TestHitTesting.cpp352
-rw-r--r--gfx/layers/apz/test/gtest/TestInputQueue.cpp45
-rw-r--r--gfx/layers/apz/test/gtest/TestOverscroll.cpp1991
-rw-r--r--gfx/layers/apz/test/gtest/TestPanning.cpp251
-rw-r--r--gfx/layers/apz/test/gtest/TestPinching.cpp675
-rw-r--r--gfx/layers/apz/test/gtest/TestPointerEventsConsumable.cpp500
-rw-r--r--gfx/layers/apz/test/gtest/TestScrollHandoff.cpp809
-rw-r--r--gfx/layers/apz/test/gtest/TestSnapping.cpp305
-rw-r--r--gfx/layers/apz/test/gtest/TestSnappingOnMomentum.cpp104
-rw-r--r--gfx/layers/apz/test/gtest/TestTransformNotifications.cpp567
-rw-r--r--gfx/layers/apz/test/gtest/TestTreeManager.cpp347
-rw-r--r--gfx/layers/apz/test/gtest/TestWRScrollData.cpp273
-rw-r--r--gfx/layers/apz/test/gtest/TestWRScrollData.h63
-rw-r--r--gfx/layers/apz/test/gtest/moz.build39
-rw-r--r--gfx/layers/apz/test/gtest/mvm/TestMobileViewportManager.cpp220
-rw-r--r--gfx/layers/apz/test/gtest/mvm/moz.build13
-rw-r--r--gfx/layers/apz/test/mochitest/FissionTestHelperChild.sys.mjs157
-rw-r--r--gfx/layers/apz/test/mochitest/FissionTestHelperParent.sys.mjs103
-rw-r--r--gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js1881
-rw-r--r--gfx/layers/apz/test/mochitest/apz_test_utils.js1287
-rw-r--r--gfx/layers/apz/test/mochitest/browser.ini64
-rw-r--r--gfx/layers/apz/test/mochitest/browser_test_animations_without_apz_sampler.js134
-rw-r--r--gfx/layers/apz/test/mochitest/browser_test_autoscrolling_in_extension_popup_window.js189
-rw-r--r--gfx/layers/apz/test/mochitest/browser_test_autoscrolling_in_oop_frame.js120
-rw-r--r--gfx/layers/apz/test/mochitest/browser_test_background_tab_load_scroll.js117
-rw-r--r--gfx/layers/apz/test/mochitest/browser_test_background_tab_scroll.js66
-rw-r--r--gfx/layers/apz/test/mochitest/browser_test_content_response_timeout.js88
-rw-r--r--gfx/layers/apz/test/mochitest/browser_test_group_fission.js150
-rw-r--r--gfx/layers/apz/test/mochitest/browser_test_position_sticky.js105
-rw-r--r--gfx/layers/apz/test/mochitest/browser_test_reset_scaling_zoom.js44
-rw-r--r--gfx/layers/apz/test/mochitest/browser_test_scroll_thumb_dragging.js77
-rw-r--r--gfx/layers/apz/test/mochitest/browser_test_scrollbar_in_extension_popup_window.js138
-rw-r--r--gfx/layers/apz/test/mochitest/browser_test_scrolling_in_extension_popup_window.js128
-rw-r--r--gfx/layers/apz/test/mochitest/browser_test_scrolling_on_inactive_scroller_in_extension_popup_window.js137
-rw-r--r--gfx/layers/apz/test/mochitest/browser_test_select_popup_position.js130
-rw-r--r--gfx/layers/apz/test/mochitest/browser_test_select_zoom.js195
-rw-r--r--gfx/layers/apz/test/mochitest/browser_test_tab_drag_zoom.js103
-rw-r--r--gfx/layers/apz/test/mochitest/green100x100.pngbin0 -> 255 bytes
-rw-r--r--gfx/layers/apz/test/mochitest/helper_background_tab_load_scroll.html147
-rw-r--r--gfx/layers/apz/test/mochitest/helper_background_tab_scroll.html9
-rw-r--r--gfx/layers/apz/test/mochitest/helper_basic_onetouchpinch.html90
-rw-r--r--gfx/layers/apz/test/mochitest/helper_basic_pan.html73
-rw-r--r--gfx/layers/apz/test/mochitest/helper_basic_scrollend.html92
-rw-r--r--gfx/layers/apz/test/mochitest/helper_basic_zoom.html71
-rw-r--r--gfx/layers/apz/test/mochitest/helper_browser_test_utils.js11
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug1162771.html107
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug1271432.html573
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug1280013.html73
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug1285070.html44
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug1299195.html47
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug1326290.html63
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug1331693.html71
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug1346632.html89
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug1414336.html97
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug1462961.html74
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug1473108.html50
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug1490393-2.html65
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug1490393.html64
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug1502010_unconsumed_pan.html76
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug1506497_touch_action_fixed_on_fixed.html96
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug1509575.html71
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug1519339_hidden_smoothscroll.html61
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug1544966_zoom_on_touch_action_none.html89
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug1550510.html66
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug1637113_main_thread_hit_test.html70
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug1637135_narrow_viewport.html60
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug1638441_fixed_pos_hit_test.html67
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug1638458_contextmenu.html82
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug1648491_no_pointercancel_with_dtc.html89
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug1662800.html61
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug1663731_no_pointercancel_on_second_touchstart.html82
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug1669625.html79
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug1674935.html76
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug1682170_pointercancel_on_touchaction_pinchzoom.html75
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug1695598.html123
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug1714934_mouseevent_buttons.html40
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug1719330.html65
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug1756529.html226
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug1780701.html70
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug1783936.html74
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug982141.html130
-rw-r--r--gfx/layers/apz/test/mochitest/helper_check_dp_size.html124
-rw-r--r--gfx/layers/apz/test/mochitest/helper_checkerboard_apzforcedisabled.html93
-rw-r--r--gfx/layers/apz/test/mochitest/helper_checkerboard_no_multiplier.html57
-rw-r--r--gfx/layers/apz/test/mochitest/helper_checkerboard_scrollinfo.html91
-rw-r--r--gfx/layers/apz/test/mochitest/helper_checkerboard_zoom_during_load.html55
-rw-r--r--gfx/layers/apz/test/mochitest/helper_checkerboard_zoomoverflowhidden.html150
-rw-r--r--gfx/layers/apz/test/mochitest/helper_click.html42
-rw-r--r--gfx/layers/apz/test/mochitest/helper_click_interrupt_animation.html96
-rw-r--r--gfx/layers/apz/test/mochitest/helper_content_response_timeout.html26
-rw-r--r--gfx/layers/apz/test/mochitest/helper_disallow_doubletap_zoom_inside_oopif.html58
-rw-r--r--gfx/layers/apz/test/mochitest/helper_displayport_expiry.html77
-rw-r--r--gfx/layers/apz/test/mochitest/helper_div_pan.html43
-rw-r--r--gfx/layers/apz/test/mochitest/helper_dommousescroll.html33
-rw-r--r--gfx/layers/apz/test/mochitest/helper_doubletap_zoom.html50
-rw-r--r--gfx/layers/apz/test/mochitest/helper_doubletap_zoom_bug1702464.html90
-rw-r--r--gfx/layers/apz/test/mochitest/helper_doubletap_zoom_fixedpos.html88
-rw-r--r--gfx/layers/apz/test/mochitest/helper_doubletap_zoom_fixedpos_overflow.html113
-rw-r--r--gfx/layers/apz/test/mochitest/helper_doubletap_zoom_gencon.html101
-rw-r--r--gfx/layers/apz/test/mochitest/helper_doubletap_zoom_horizontal_center.html50
-rw-r--r--gfx/layers/apz/test/mochitest/helper_doubletap_zoom_hscrollable.html85
-rw-r--r--gfx/layers/apz/test/mochitest/helper_doubletap_zoom_hscrollable2.html109
-rw-r--r--gfx/layers/apz/test/mochitest/helper_doubletap_zoom_htmlelement.html76
-rw-r--r--gfx/layers/apz/test/mochitest/helper_doubletap_zoom_img.html43
-rw-r--r--gfx/layers/apz/test/mochitest/helper_doubletap_zoom_large_overflow.html300
-rw-r--r--gfx/layers/apz/test/mochitest/helper_doubletap_zoom_noscroll.html59
-rw-r--r--gfx/layers/apz/test/mochitest/helper_doubletap_zoom_nothing.html46
-rw-r--r--gfx/layers/apz/test/mochitest/helper_doubletap_zoom_nothing_listener.html47
-rw-r--r--gfx/layers/apz/test/mochitest/helper_doubletap_zoom_oopif.html50
-rw-r--r--gfx/layers/apz/test/mochitest/helper_doubletap_zoom_scrolled_overflowhidden.html82
-rw-r--r--gfx/layers/apz/test/mochitest/helper_doubletap_zoom_shadowdom.html69
-rw-r--r--gfx/layers/apz/test/mochitest/helper_doubletap_zoom_small.html43
-rw-r--r--gfx/layers/apz/test/mochitest/helper_doubletap_zoom_smooth.html161
-rw-r--r--gfx/layers/apz/test/mochitest/helper_doubletap_zoom_square.html61
-rw-r--r--gfx/layers/apz/test/mochitest/helper_doubletap_zoom_tablecell.html110
-rw-r--r--gfx/layers/apz/test/mochitest/helper_doubletap_zoom_tallwide.html85
-rw-r--r--gfx/layers/apz/test/mochitest/helper_doubletap_zoom_textarea.html43
-rw-r--r--gfx/layers/apz/test/mochitest/helper_drag_bug1719913.html91
-rw-r--r--gfx/layers/apz/test/mochitest/helper_drag_click.html69
-rw-r--r--gfx/layers/apz/test/mochitest/helper_drag_root_scrollbar.html61
-rw-r--r--gfx/layers/apz/test/mochitest/helper_drag_scroll.html653
-rw-r--r--gfx/layers/apz/test/mochitest/helper_drag_scrollbar_hittest.html100
-rw-r--r--gfx/layers/apz/test/mochitest/helper_empty.html4
-rw-r--r--gfx/layers/apz/test/mochitest/helper_fission_animation_styling_in_oopif.html166
-rw-r--r--gfx/layers/apz/test/mochitest/helper_fission_animation_styling_in_transformed_oopif.html130
-rw-r--r--gfx/layers/apz/test/mochitest/helper_fission_basic.html40
-rw-r--r--gfx/layers/apz/test/mochitest/helper_fission_checkerboard_severity.html138
-rw-r--r--gfx/layers/apz/test/mochitest/helper_fission_empty.html34
-rw-r--r--gfx/layers/apz/test/mochitest/helper_fission_event_region_override.html84
-rw-r--r--gfx/layers/apz/test/mochitest/helper_fission_force_empty_hit_region.html82
-rw-r--r--gfx/layers/apz/test/mochitest/helper_fission_inactivescroller_positionedcontent.html120
-rw-r--r--gfx/layers/apz/test/mochitest/helper_fission_inactivescroller_under_oopif.html88
-rw-r--r--gfx/layers/apz/test/mochitest/helper_fission_initial_displayport.html105
-rw-r--r--gfx/layers/apz/test/mochitest/helper_fission_irregular_areas.html101
-rw-r--r--gfx/layers/apz/test/mochitest/helper_fission_large_subframe.html67
-rw-r--r--gfx/layers/apz/test/mochitest/helper_fission_scroll_handoff.html50
-rw-r--r--gfx/layers/apz/test/mochitest/helper_fission_scroll_oopif.html158
-rw-r--r--gfx/layers/apz/test/mochitest/helper_fission_setResolution.html59
-rw-r--r--gfx/layers/apz/test/mochitest/helper_fission_tap.html87
-rw-r--r--gfx/layers/apz/test/mochitest/helper_fission_tap_in_nested_iframe_on_zoomed.html106
-rw-r--r--gfx/layers/apz/test/mochitest/helper_fission_tap_on_zoomed.html93
-rw-r--r--gfx/layers/apz/test/mochitest/helper_fission_touch.html99
-rw-r--r--gfx/layers/apz/test/mochitest/helper_fission_transforms.html89
-rw-r--r--gfx/layers/apz/test/mochitest/helper_fission_utils.js130
-rw-r--r--gfx/layers/apz/test/mochitest/helper_fixed_html_hittest.html61
-rw-r--r--gfx/layers/apz/test/mochitest/helper_fixed_pos_displayport.html101
-rw-r--r--gfx/layers/apz/test/mochitest/helper_fixed_position_scroll_hittest.html51
-rw-r--r--gfx/layers/apz/test/mochitest/helper_fullscreen.html53
-rw-r--r--gfx/layers/apz/test/mochitest/helper_hittest_backface_hidden.html67
-rw-r--r--gfx/layers/apz/test/mochitest/helper_hittest_basic.html141
-rw-r--r--gfx/layers/apz/test/mochitest/helper_hittest_bug1119497.html54
-rw-r--r--gfx/layers/apz/test/mochitest/helper_hittest_bug1257288.html74
-rw-r--r--gfx/layers/apz/test/mochitest/helper_hittest_bug1715187.html69
-rw-r--r--gfx/layers/apz/test/mochitest/helper_hittest_bug1715187_oopif.html13
-rw-r--r--gfx/layers/apz/test/mochitest/helper_hittest_bug1715369.html74
-rw-r--r--gfx/layers/apz/test/mochitest/helper_hittest_bug1715369_iframe.html13
-rw-r--r--gfx/layers/apz/test/mochitest/helper_hittest_bug1715369_oopif.html13
-rw-r--r--gfx/layers/apz/test/mochitest/helper_hittest_bug1730606-1.html124
-rw-r--r--gfx/layers/apz/test/mochitest/helper_hittest_bug1730606-2.html157
-rw-r--r--gfx/layers/apz/test/mochitest/helper_hittest_bug1730606-3.html56
-rw-r--r--gfx/layers/apz/test/mochitest/helper_hittest_bug1730606-4.html194
-rw-r--r--gfx/layers/apz/test/mochitest/helper_hittest_checkerboard.html57
-rw-r--r--gfx/layers/apz/test/mochitest/helper_hittest_clippath.html118
-rw-r--r--gfx/layers/apz/test/mochitest/helper_hittest_clipped_fixed_modal.html85
-rw-r--r--gfx/layers/apz/test/mochitest/helper_hittest_deep_scene_stack.html57
-rw-r--r--gfx/layers/apz/test/mochitest/helper_hittest_fixed-2.html74
-rw-r--r--gfx/layers/apz/test/mochitest/helper_hittest_fixed-3.html113
-rw-r--r--gfx/layers/apz/test/mochitest/helper_hittest_fixed.html82
-rw-r--r--gfx/layers/apz/test/mochitest/helper_hittest_fixed_bg.html53
-rw-r--r--gfx/layers/apz/test/mochitest/helper_hittest_fixed_in_scrolled_transform.html91
-rw-r--r--gfx/layers/apz/test/mochitest/helper_hittest_fixed_item_over_oop_iframe.html61
-rw-r--r--gfx/layers/apz/test/mochitest/helper_hittest_float_bug1434846.html56
-rw-r--r--gfx/layers/apz/test/mochitest/helper_hittest_float_bug1443518.html56
-rw-r--r--gfx/layers/apz/test/mochitest/helper_hittest_hidden_inactive_scrollframe.html55
-rw-r--r--gfx/layers/apz/test/mochitest/helper_hittest_hoisted_scrollinfo.html81
-rw-r--r--gfx/layers/apz/test/mochitest/helper_hittest_iframe_perspective-2.html69
-rw-r--r--gfx/layers/apz/test/mochitest/helper_hittest_iframe_perspective-3.html70
-rw-r--r--gfx/layers/apz/test/mochitest/helper_hittest_iframe_perspective.html60
-rw-r--r--gfx/layers/apz/test/mochitest/helper_hittest_iframe_perspective_child.html13
-rw-r--r--gfx/layers/apz/test/mochitest/helper_hittest_nested_transforms_bug1459696.html80
-rw-r--r--gfx/layers/apz/test/mochitest/helper_hittest_obscuration.html77
-rw-r--r--gfx/layers/apz/test/mochitest/helper_hittest_overscroll.html249
-rw-r--r--gfx/layers/apz/test/mochitest/helper_hittest_overscroll_contextmenu.html129
-rw-r--r--gfx/layers/apz/test/mochitest/helper_hittest_overscroll_subframe.html132
-rw-r--r--gfx/layers/apz/test/mochitest/helper_hittest_pointerevents_svg.html177
-rw-r--r--gfx/layers/apz/test/mochitest/helper_hittest_spam.html100
-rw-r--r--gfx/layers/apz/test/mochitest/helper_hittest_sticky_bug1478304.html58
-rw-r--r--gfx/layers/apz/test/mochitest/helper_hittest_touchaction.html353
-rw-r--r--gfx/layers/apz/test/mochitest/helper_horizontal_checkerboard.html65
-rw-r--r--gfx/layers/apz/test/mochitest/helper_iframe1.html14
-rw-r--r--gfx/layers/apz/test/mochitest/helper_iframe2.html14
-rw-r--r--gfx/layers/apz/test/mochitest/helper_iframe_pan.html49
-rw-r--r--gfx/layers/apz/test/mochitest/helper_iframe_textarea.html12
-rw-r--r--gfx/layers/apz/test/mochitest/helper_interrupted_reflow.html712
-rw-r--r--gfx/layers/apz/test/mochitest/helper_key_scroll.html109
-rw-r--r--gfx/layers/apz/test/mochitest/helper_long_tap.html166
-rw-r--r--gfx/layers/apz/test/mochitest/helper_main_thread_smooth_scroll_scrollend.html47
-rw-r--r--gfx/layers/apz/test/mochitest/helper_mainthread_scroll_bug1662379.html168
-rw-r--r--gfx/layers/apz/test/mochitest/helper_minimum_scale_1_0.html46
-rw-r--r--gfx/layers/apz/test/mochitest/helper_no_scalable_with_initial_scale.html48
-rw-r--r--gfx/layers/apz/test/mochitest/helper_onetouchpinch_nested.html103
-rw-r--r--gfx/layers/apz/test/mochitest/helper_overflowhidden_zoom.html83
-rw-r--r--gfx/layers/apz/test/mochitest/helper_override_root.html62
-rw-r--r--gfx/layers/apz/test/mochitest/helper_override_subdoc.html15
-rw-r--r--gfx/layers/apz/test/mochitest/helper_overscroll_behavior_bug1425573.html44
-rw-r--r--gfx/layers/apz/test/mochitest/helper_overscroll_behavior_bug1425603.html76
-rw-r--r--gfx/layers/apz/test/mochitest/helper_overscroll_behavior_bug1494440.html50
-rw-r--r--gfx/layers/apz/test/mochitest/helper_overscroll_in_apz_test_data.html29
-rw-r--r--gfx/layers/apz/test/mochitest/helper_overscroll_in_subscroller.html165
-rw-r--r--gfx/layers/apz/test/mochitest/helper_position_fixed_scroll_handoff-1.html88
-rw-r--r--gfx/layers/apz/test/mochitest/helper_position_fixed_scroll_handoff-2.html65
-rw-r--r--gfx/layers/apz/test/mochitest/helper_position_fixed_scroll_handoff-3.html77
-rw-r--r--gfx/layers/apz/test/mochitest/helper_position_fixed_scroll_handoff-4.html79
-rw-r--r--gfx/layers/apz/test/mochitest/helper_position_fixed_scroll_handoff-5.html110
-rw-r--r--gfx/layers/apz/test/mochitest/helper_position_sticky_flicker.html25
-rw-r--r--gfx/layers/apz/test/mochitest/helper_position_sticky_scroll_handoff.html88
-rw-r--r--gfx/layers/apz/test/mochitest/helper_programmatic_scroll_behavior.html81
-rw-r--r--gfx/layers/apz/test/mochitest/helper_relative_scroll_smoothness.html141
-rw-r--r--gfx/layers/apz/test/mochitest/helper_reset_zoom_bug1818967.html55
-rw-r--r--gfx/layers/apz/test/mochitest/helper_scroll_anchoring_on_wheel.html59
-rw-r--r--gfx/layers/apz/test/mochitest/helper_scroll_anchoring_smooth_scroll.html54
-rw-r--r--gfx/layers/apz/test/mochitest/helper_scroll_anchoring_smooth_scroll_with_set_timeout.html56
-rw-r--r--gfx/layers/apz/test/mochitest/helper_scroll_inactive_perspective.html45
-rw-r--r--gfx/layers/apz/test/mochitest/helper_scroll_inactive_zindex.html46
-rw-r--r--gfx/layers/apz/test/mochitest/helper_scroll_into_view_bug1516056.html62
-rw-r--r--gfx/layers/apz/test/mochitest/helper_scroll_into_view_bug1562757.html64
-rw-r--r--gfx/layers/apz/test/mochitest/helper_scroll_linked_effect_by_wheel.html65
-rw-r--r--gfx/layers/apz/test/mochitest/helper_scroll_linked_effect_detector.html108
-rw-r--r--gfx/layers/apz/test/mochitest/helper_scroll_on_position_fixed.html60
-rw-r--r--gfx/layers/apz/test/mochitest/helper_scroll_over_scrollbar.html48
-rw-r--r--gfx/layers/apz/test/mochitest/helper_scroll_snap_no_valid_snap_position.html45
-rw-r--r--gfx/layers/apz/test/mochitest/helper_scroll_snap_not_resnap_during_panning.html93
-rw-r--r--gfx/layers/apz/test/mochitest/helper_scroll_snap_not_resnap_during_scrollbar_dragging.html105
-rw-r--r--gfx/layers/apz/test/mochitest/helper_scroll_snap_on_page_down_scroll.html84
-rw-r--r--gfx/layers/apz/test/mochitest/helper_scroll_snap_resnap_after_async_scroll.html81
-rw-r--r--gfx/layers/apz/test/mochitest/helper_scroll_snap_resnap_after_async_scrollBy.html72
-rw-r--r--gfx/layers/apz/test/mochitest/helper_scroll_tables_perspective.html66
-rw-r--r--gfx/layers/apz/test/mochitest/helper_scroll_thumb_dragging.html20
-rw-r--r--gfx/layers/apz/test/mochitest/helper_scrollbar_snap_bug1501062.html135
-rw-r--r--gfx/layers/apz/test/mochitest/helper_scrollbarbutton_repeat.html101
-rw-r--r--gfx/layers/apz/test/mochitest/helper_scrollbarbuttonclick_checkerboard.html75
-rw-r--r--gfx/layers/apz/test/mochitest/helper_scrollby_bug1531796.html36
-rw-r--r--gfx/layers/apz/test/mochitest/helper_scrollend_bubbles.html99
-rw-r--r--gfx/layers/apz/test/mochitest/helper_scrollframe_activation_on_load.html89
-rw-r--r--gfx/layers/apz/test/mochitest/helper_scrollto_tap.html59
-rw-r--r--gfx/layers/apz/test/mochitest/helper_self_closer.html12
-rw-r--r--gfx/layers/apz/test/mochitest/helper_smoothscroll_spam.html51
-rw-r--r--gfx/layers/apz/test/mochitest/helper_smoothscroll_spam_interleaved.html57
-rw-r--r--gfx/layers/apz/test/mochitest/helper_subframe_style.css15
-rw-r--r--gfx/layers/apz/test/mochitest/helper_tall.html504
-rw-r--r--gfx/layers/apz/test/mochitest/helper_tap.html32
-rw-r--r--gfx/layers/apz/test/mochitest/helper_tap_default_passive.html81
-rw-r--r--gfx/layers/apz/test/mochitest/helper_tap_fullzoom.html33
-rw-r--r--gfx/layers/apz/test/mochitest/helper_tap_passive.html66
-rw-r--r--gfx/layers/apz/test/mochitest/helper_test_autoscrolling_in_oop_frame.html9
-rw-r--r--gfx/layers/apz/test/mochitest/helper_test_reset_scaling_zoom.html23
-rw-r--r--gfx/layers/apz/test/mochitest/helper_test_select_popup_position.html23
-rw-r--r--gfx/layers/apz/test/mochitest/helper_test_select_popup_position_transformed_in_parent.html25
-rw-r--r--gfx/layers/apz/test/mochitest/helper_test_select_popup_position_zoomed.html25
-rw-r--r--gfx/layers/apz/test/mochitest/helper_test_select_zoom.html43
-rw-r--r--gfx/layers/apz/test/mochitest/helper_test_tab_drag_zoom.html18
-rw-r--r--gfx/layers/apz/test/mochitest/helper_touch_action.html123
-rw-r--r--gfx/layers/apz/test/mochitest/helper_touch_action_complex.html137
-rw-r--r--gfx/layers/apz/test/mochitest/helper_touch_action_ordering_block.html39
-rw-r--r--gfx/layers/apz/test/mochitest/helper_touch_action_ordering_zindex.html37
-rw-r--r--gfx/layers/apz/test/mochitest/helper_touch_action_regions.html345
-rw-r--r--gfx/layers/apz/test/mochitest/helper_touch_action_zero_opacity_bug1500864.html45
-rw-r--r--gfx/layers/apz/test/mochitest/helper_touch_drag_root_scrollbar.html51
-rw-r--r--gfx/layers/apz/test/mochitest/helper_touchpad_pinch_and_pan.html49
-rw-r--r--gfx/layers/apz/test/mochitest/helper_transform_end_on_keyboard_scroll.html58
-rw-r--r--gfx/layers/apz/test/mochitest/helper_transform_end_on_wheel_scroll.html28
-rw-r--r--gfx/layers/apz/test/mochitest/helper_visual_scrollbars_pagescroll.html119
-rw-r--r--gfx/layers/apz/test/mochitest/helper_visual_smooth_scroll.html53
-rw-r--r--gfx/layers/apz/test/mochitest/helper_visualscroll_clamp_restore.html63
-rw-r--r--gfx/layers/apz/test/mochitest/helper_visualscroll_nonrcd_rsf.html88
-rw-r--r--gfx/layers/apz/test/mochitest/helper_wheelevents_handoff_on_iframe.html52
-rw-r--r--gfx/layers/apz/test/mochitest/helper_wheelevents_handoff_on_iframe_child.html11
-rw-r--r--gfx/layers/apz/test/mochitest/helper_wheelevents_handoff_on_non_scrollable_iframe.html113
-rw-r--r--gfx/layers/apz/test/mochitest/helper_wide_crossorigin_iframe.html33
-rw-r--r--gfx/layers/apz/test/mochitest/helper_wide_crossorigin_iframe_child.html71
-rw-r--r--gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_fixed_bug1673511.html42
-rw-r--r--gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_iframe.html68
-rw-r--r--gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_multiline.html94
-rw-r--r--gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_nozoom.html39
-rw-r--r--gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_nozoom_bug1738696.html51
-rw-r--r--gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_scroll.html51
-rw-r--r--gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_touch-action.html67
-rw-r--r--gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_zoom.html39
-rw-r--r--gfx/layers/apz/test/mochitest/helper_zoom_after_gpu_process_restart.html63
-rw-r--r--gfx/layers/apz/test/mochitest/helper_zoom_keyboardscroll.html74
-rw-r--r--gfx/layers/apz/test/mochitest/helper_zoom_oopif.html54
-rw-r--r--gfx/layers/apz/test/mochitest/helper_zoom_out_clamped_scrollpos.html85
-rw-r--r--gfx/layers/apz/test/mochitest/helper_zoom_out_with_mainthread_clamping.html110
-rw-r--r--gfx/layers/apz/test/mochitest/helper_zoom_prevented.html75
-rw-r--r--gfx/layers/apz/test/mochitest/helper_zoom_restore_position_tabswitch.html74
-rw-r--r--gfx/layers/apz/test/mochitest/helper_zoom_with_dynamic_toolbar.html45
-rw-r--r--gfx/layers/apz/test/mochitest/helper_zoom_with_touchpad.html110
-rw-r--r--gfx/layers/apz/test/mochitest/helper_zoomed_pan.html79
-rw-r--r--gfx/layers/apz/test/mochitest/mochitest.ini125
-rw-r--r--gfx/layers/apz/test/mochitest/test_abort_smooth_scroll_by_instant_scroll.html51
-rw-r--r--gfx/layers/apz/test/mochitest/test_bug1151667.html63
-rw-r--r--gfx/layers/apz/test/mochitest/test_bug1253683.html59
-rw-r--r--gfx/layers/apz/test/mochitest/test_bug1277814.html105
-rw-r--r--gfx/layers/apz/test/mochitest/test_bug1304689-2.html130
-rw-r--r--gfx/layers/apz/test/mochitest/test_bug1304689.html134
-rw-r--r--gfx/layers/apz/test/mochitest/test_frame_reconstruction.html218
-rw-r--r--gfx/layers/apz/test/mochitest/test_group_bug1534549.html37
-rw-r--r--gfx/layers/apz/test/mochitest/test_group_checkerboarding.html83
-rw-r--r--gfx/layers/apz/test/mochitest/test_group_displayport.html31
-rw-r--r--gfx/layers/apz/test/mochitest/test_group_double_tap_zoom-2.html89
-rw-r--r--gfx/layers/apz/test/mochitest/test_group_double_tap_zoom.html66
-rw-r--r--gfx/layers/apz/test/mochitest/test_group_fullscreen.html32
-rw-r--r--gfx/layers/apz/test/mochitest/test_group_hittest-1.html59
-rw-r--r--gfx/layers/apz/test/mochitest/test_group_hittest-2.html72
-rw-r--r--gfx/layers/apz/test/mochitest/test_group_hittest-3.html50
-rw-r--r--gfx/layers/apz/test/mochitest/test_group_hittest-overscroll.html54
-rw-r--r--gfx/layers/apz/test/mochitest/test_group_keyboard-2.html46
-rw-r--r--gfx/layers/apz/test/mochitest/test_group_keyboard.html60
-rw-r--r--gfx/layers/apz/test/mochitest/test_group_mainthread.html51
-rw-r--r--gfx/layers/apz/test/mochitest/test_group_minimum_scale_size.html48
-rw-r--r--gfx/layers/apz/test/mochitest/test_group_mouseevents.html82
-rw-r--r--gfx/layers/apz/test/mochitest/test_group_overrides.html37
-rw-r--r--gfx/layers/apz/test/mochitest/test_group_overscroll.html35
-rw-r--r--gfx/layers/apz/test/mochitest/test_group_overscroll_handoff.html46
-rw-r--r--gfx/layers/apz/test/mochitest/test_group_pointerevents.html43
-rw-r--r--gfx/layers/apz/test/mochitest/test_group_programmatic_scroll_behavior.html38
-rw-r--r--gfx/layers/apz/test/mochitest/test_group_scroll_linked_effect.html33
-rw-r--r--gfx/layers/apz/test/mochitest/test_group_scroll_snap.html67
-rw-r--r--gfx/layers/apz/test/mochitest/test_group_scrollend.html58
-rw-r--r--gfx/layers/apz/test/mochitest/test_group_scrollframe_activation.html49
-rw-r--r--gfx/layers/apz/test/mochitest/test_group_touchevents-2.html69
-rw-r--r--gfx/layers/apz/test/mochitest/test_group_touchevents-3.html47
-rw-r--r--gfx/layers/apz/test/mochitest/test_group_touchevents-4.html54
-rw-r--r--gfx/layers/apz/test/mochitest/test_group_touchevents-5.html51
-rw-r--r--gfx/layers/apz/test/mochitest/test_group_touchevents.html55
-rw-r--r--gfx/layers/apz/test/mochitest/test_group_wheelevents.html84
-rw-r--r--gfx/layers/apz/test/mochitest/test_group_zoom-2.html81
-rw-r--r--gfx/layers/apz/test/mochitest/test_group_zoom.html80
-rw-r--r--gfx/layers/apz/test/mochitest/test_group_zoomToFocusedInput.html49
-rw-r--r--gfx/layers/apz/test/mochitest/test_interrupted_reflow.html38
-rw-r--r--gfx/layers/apz/test/mochitest/test_layerization.html312
-rw-r--r--gfx/layers/apz/test/mochitest/test_relative_update.html92
-rw-r--r--gfx/layers/apz/test/mochitest/test_scroll_inactive_bug1190112.html553
-rw-r--r--gfx/layers/apz/test/mochitest/test_scroll_inactive_flattened_frame.html50
-rw-r--r--gfx/layers/apz/test/mochitest/test_scroll_subframe_scrollbar.html116
-rw-r--r--gfx/layers/apz/test/mochitest/test_smoothness.html71
-rw-r--r--gfx/layers/apz/test/mochitest/test_touch_listeners_impacting_wheel.html119
-rw-r--r--gfx/layers/apz/test/mochitest/test_wheel_scroll.html109
-rw-r--r--gfx/layers/apz/test/mochitest/test_wheel_transactions.html150
-rw-r--r--gfx/layers/apz/test/reftest/async-scrollbar-1-h-ref.html8
-rw-r--r--gfx/layers/apz/test/reftest/async-scrollbar-1-h-rtl-ref.html9
-rw-r--r--gfx/layers/apz/test/reftest/async-scrollbar-1-h-rtl.html13
-rw-r--r--gfx/layers/apz/test/reftest/async-scrollbar-1-h.html12
-rw-r--r--gfx/layers/apz/test/reftest/async-scrollbar-1-v-fullzoom-ref.html8
-rw-r--r--gfx/layers/apz/test/reftest/async-scrollbar-1-v-fullzoom.html14
-rw-r--r--gfx/layers/apz/test/reftest/async-scrollbar-1-v-ref.html8
-rw-r--r--gfx/layers/apz/test/reftest/async-scrollbar-1-v-rtl-ref.html9
-rw-r--r--gfx/layers/apz/test/reftest/async-scrollbar-1-v-rtl.html13
-rw-r--r--gfx/layers/apz/test/reftest/async-scrollbar-1-v.html12
-rw-r--r--gfx/layers/apz/test/reftest/async-scrollbar-1-vh-ref.html8
-rw-r--r--gfx/layers/apz/test/reftest/async-scrollbar-1-vh-rtl-ref.html9
-rw-r--r--gfx/layers/apz/test/reftest/async-scrollbar-1-vh-rtl.html13
-rw-r--r--gfx/layers/apz/test/reftest/async-scrollbar-1-vh.html12
-rw-r--r--gfx/layers/apz/test/reftest/frame-reconstruction-scroll-clamping-ref.html27
-rw-r--r--gfx/layers/apz/test/reftest/frame-reconstruction-scroll-clamping.html53
-rw-r--r--gfx/layers/apz/test/reftest/iframe-zoomed-child.html12
-rw-r--r--gfx/layers/apz/test/reftest/iframe-zoomed-ref.html20
-rw-r--r--gfx/layers/apz/test/reftest/iframe-zoomed.html25
-rw-r--r--gfx/layers/apz/test/reftest/initial-scale-1-ref.html9
-rw-r--r--gfx/layers/apz/test/reftest/initial-scale-1.html9
-rw-r--r--gfx/layers/apz/test/reftest/pinch-zoom-position-fixed-ref.html23
-rw-r--r--gfx/layers/apz/test/reftest/pinch-zoom-position-fixed.html37
-rw-r--r--gfx/layers/apz/test/reftest/pinch-zoom-position-sticky-ref.html27
-rw-r--r--gfx/layers/apz/test/reftest/pinch-zoom-position-sticky.html30
-rw-r--r--gfx/layers/apz/test/reftest/reftest.list50
-rw-r--r--gfx/layers/apz/test/reftest/root-scrollbar-async-zoomed-in-ref.html8
-rw-r--r--gfx/layers/apz/test/reftest/root-scrollbar-async-zoomed-in.html13
-rw-r--r--gfx/layers/apz/test/reftest/root-scrollbar-async-zoomed-out-ref.html8
-rw-r--r--gfx/layers/apz/test/reftest/root-scrollbar-async-zoomed-out.html13
-rw-r--r--gfx/layers/apz/test/reftest/root-scrollbar-zoomed-in-async-scroll.html12
-rw-r--r--gfx/layers/apz/test/reftest/root-scrollbar-zoomed-in-ref.html8
-rw-r--r--gfx/layers/apz/test/reftest/root-scrollbar-zoomed-in.html8
-rw-r--r--gfx/layers/apz/test/reftest/root-scrollbar-zoomed-out-async-scroll.html12
-rw-r--r--gfx/layers/apz/test/reftest/root-scrollbar-zoomed-out-ref.html8
-rw-r--r--gfx/layers/apz/test/reftest/root-scrollbar-zoomed-out.html8
-rw-r--r--gfx/layers/apz/test/reftest/root-scrollbars-1-ref.html14
-rw-r--r--gfx/layers/apz/test/reftest/root-scrollbars-1.html14
-rw-r--r--gfx/layers/apz/test/reftest/scaled-iframe-zoomed-ref.html21
-rw-r--r--gfx/layers/apz/test/reftest/scaled-iframe-zoomed.html26
-rw-r--r--gfx/layers/apz/test/reftest/subframe-scrollbar-zoomed-in-async-scroll-ref.html10
-rw-r--r--gfx/layers/apz/test/reftest/subframe-scrollbar-zoomed-in-async-scroll.html15
-rw-r--r--gfx/layers/apz/test/reftest/subframe-scrollbar-zoomed-out-async-scroll-ref.html10
-rw-r--r--gfx/layers/apz/test/reftest/subframe-scrollbar-zoomed-out-async-scroll.html15
-rw-r--r--gfx/layers/apz/testutil/APZTestData.cpp124
-rw-r--r--gfx/layers/apz/testutil/APZTestData.h252
-rw-r--r--gfx/layers/apz/util/APZCCallbackHelper.cpp940
-rw-r--r--gfx/layers/apz/util/APZCCallbackHelper.h195
-rw-r--r--gfx/layers/apz/util/APZEventState.cpp603
-rw-r--r--gfx/layers/apz/util/APZEventState.h190
-rw-r--r--gfx/layers/apz/util/APZTaskRunnable.cpp142
-rw-r--r--gfx/layers/apz/util/APZTaskRunnable.h89
-rw-r--r--gfx/layers/apz/util/APZThreadUtils.cpp119
-rw-r--r--gfx/layers/apz/util/APZThreadUtils.h75
-rw-r--r--gfx/layers/apz/util/ActiveElementManager.cpp178
-rw-r--r--gfx/layers/apz/util/ActiveElementManager.h97
-rw-r--r--gfx/layers/apz/util/CheckerboardReportService.cpp217
-rw-r--r--gfx/layers/apz/util/CheckerboardReportService.h138
-rw-r--r--gfx/layers/apz/util/ChromeProcessController.cpp356
-rw-r--r--gfx/layers/apz/util/ChromeProcessController.h102
-rw-r--r--gfx/layers/apz/util/ContentProcessController.cpp123
-rw-r--r--gfx/layers/apz/util/ContentProcessController.h93
-rw-r--r--gfx/layers/apz/util/DoubleTapToZoom.cpp376
-rw-r--r--gfx/layers/apz/util/DoubleTapToZoom.h62
-rw-r--r--gfx/layers/apz/util/InputAPZContext.cpp65
-rw-r--r--gfx/layers/apz/util/InputAPZContext.h69
-rw-r--r--gfx/layers/apz/util/ScrollLinkedEffectDetector.cpp48
-rw-r--r--gfx/layers/apz/util/ScrollLinkedEffectDetector.h48
-rw-r--r--gfx/layers/apz/util/ScrollingInteractionContext.cpp29
-rw-r--r--gfx/layers/apz/util/ScrollingInteractionContext.h40
-rw-r--r--gfx/layers/apz/util/TouchActionHelper.cpp131
-rw-r--r--gfx/layers/apz/util/TouchActionHelper.h46
-rw-r--r--gfx/layers/apz/util/TouchCounter.cpp74
-rw-r--r--gfx/layers/apz/util/TouchCounter.h36
-rw-r--r--gfx/layers/client/CanvasClient.cpp96
-rw-r--r--gfx/layers/client/CanvasClient.h71
-rw-r--r--gfx/layers/client/CompositableClient.cpp182
-rw-r--r--gfx/layers/client/CompositableClient.h208
-rw-r--r--gfx/layers/client/GPUVideoTextureClient.cpp57
-rw-r--r--gfx/layers/client/GPUVideoTextureClient.h55
-rw-r--r--gfx/layers/client/ImageClient.cpp277
-rw-r--r--gfx/layers/client/ImageClient.h118
-rw-r--r--gfx/layers/client/TextureClient.cpp1794
-rw-r--r--gfx/layers/client/TextureClient.h814
-rw-r--r--gfx/layers/client/TextureClientPool.cpp307
-rw-r--r--gfx/layers/client/TextureClientPool.h175
-rw-r--r--gfx/layers/client/TextureClientRecycleAllocator.cpp261
-rw-r--r--gfx/layers/client/TextureClientRecycleAllocator.h140
-rw-r--r--gfx/layers/client/TextureClientSharedSurface.cpp92
-rw-r--r--gfx/layers/client/TextureClientSharedSurface.h68
-rw-r--r--gfx/layers/client/TextureRecorded.cpp122
-rw-r--r--gfx/layers/client/TextureRecorded.h60
-rw-r--r--gfx/layers/composite/CompositableHost.cpp69
-rw-r--r--gfx/layers/composite/CompositableHost.h126
-rw-r--r--gfx/layers/composite/Diagnostics.h29
-rw-r--r--gfx/layers/composite/FontData.h304
-rw-r--r--gfx/layers/composite/FrameUniformityData.cpp38
-rw-r--r--gfx/layers/composite/FrameUniformityData.h49
-rw-r--r--gfx/layers/composite/GPUVideoTextureHost.cpp236
-rw-r--r--gfx/layers/composite/GPUVideoTextureHost.h92
-rw-r--r--gfx/layers/composite/ImageComposite.cpp378
-rw-r--r--gfx/layers/composite/ImageComposite.h139
-rw-r--r--gfx/layers/composite/RemoteTextureHostWrapper.cpp240
-rw-r--r--gfx/layers/composite/RemoteTextureHostWrapper.h124
-rw-r--r--gfx/layers/composite/TextureHost.cpp848
-rw-r--r--gfx/layers/composite/TextureHost.h1011
-rw-r--r--gfx/layers/d3d11/BlendShaderConstants.h18
-rw-r--r--gfx/layers/d3d11/CompositorD3D11.cpp1400
-rw-r--r--gfx/layers/d3d11/CompositorD3D11.h227
-rw-r--r--gfx/layers/d3d11/CompositorD3D11.hlsl186
-rw-r--r--gfx/layers/d3d11/DeviceAttachmentsD3D11.cpp249
-rw-r--r--gfx/layers/d3d11/DeviceAttachmentsD3D11.h96
-rw-r--r--gfx/layers/d3d11/HelpersD3D11.h67
-rw-r--r--gfx/layers/d3d11/ShaderDefinitionsD3D11.h40
-rw-r--r--gfx/layers/d3d11/TextureD3D11.cpp1962
-rw-r--r--gfx/layers/d3d11/TextureD3D11.h649
-rw-r--r--gfx/layers/d3d11/genshaders.py176
-rw-r--r--gfx/layers/d3d11/shaders.manifest13
-rw-r--r--gfx/layers/ipc/APZCTreeManagerChild.cpp225
-rw-r--r--gfx/layers/ipc/APZCTreeManagerChild.h105
-rw-r--r--gfx/layers/ipc/APZCTreeManagerParent.cpp199
-rw-r--r--gfx/layers/ipc/APZCTreeManagerParent.h81
-rw-r--r--gfx/layers/ipc/APZChild.cpp113
-rw-r--r--gfx/layers/ipc/APZChild.h79
-rw-r--r--gfx/layers/ipc/APZInputBridgeChild.cpp211
-rw-r--r--gfx/layers/ipc/APZInputBridgeChild.h62
-rw-r--r--gfx/layers/ipc/APZInputBridgeParent.cpp213
-rw-r--r--gfx/layers/ipc/APZInputBridgeParent.h75
-rw-r--r--gfx/layers/ipc/CanvasChild.cpp345
-rw-r--r--gfx/layers/ipc/CanvasChild.h152
-rw-r--r--gfx/layers/ipc/CanvasThread.cpp138
-rw-r--r--gfx/layers/ipc/CanvasThread.h97
-rw-r--r--gfx/layers/ipc/CanvasTranslator.cpp560
-rw-r--r--gfx/layers/ipc/CanvasTranslator.h306
-rw-r--r--gfx/layers/ipc/CompositableForwarder.cpp15
-rw-r--r--gfx/layers/ipc/CompositableForwarder.h110
-rw-r--r--gfx/layers/ipc/CompositableTransactionParent.cpp174
-rw-r--r--gfx/layers/ipc/CompositableTransactionParent.h64
-rw-r--r--gfx/layers/ipc/CompositorBench.cpp352
-rw-r--r--gfx/layers/ipc/CompositorBench.h30
-rw-r--r--gfx/layers/ipc/CompositorBridgeChild.cpp648
-rw-r--r--gfx/layers/ipc/CompositorBridgeChild.h251
-rw-r--r--gfx/layers/ipc/CompositorBridgeParent.cpp1988
-rw-r--r--gfx/layers/ipc/CompositorBridgeParent.h656
-rw-r--r--gfx/layers/ipc/CompositorManagerChild.cpp255
-rw-r--r--gfx/layers/ipc/CompositorManagerChild.h119
-rw-r--r--gfx/layers/ipc/CompositorManagerParent.cpp338
-rw-r--r--gfx/layers/ipc/CompositorManagerParent.h95
-rw-r--r--gfx/layers/ipc/CompositorThread.cpp197
-rw-r--r--gfx/layers/ipc/CompositorThread.h69
-rw-r--r--gfx/layers/ipc/CompositorVsyncScheduler.cpp382
-rw-r--r--gfx/layers/ipc/CompositorVsyncScheduler.h185
-rw-r--r--gfx/layers/ipc/CompositorVsyncSchedulerOwner.h34
-rw-r--r--gfx/layers/ipc/ContentCompositorBridgeParent.cpp461
-rw-r--r--gfx/layers/ipc/ContentCompositorBridgeParent.h187
-rw-r--r--gfx/layers/ipc/ISurfaceAllocator.cpp217
-rw-r--r--gfx/layers/ipc/ISurfaceAllocator.h285
-rw-r--r--gfx/layers/ipc/ImageBridgeChild.cpp939
-rw-r--r--gfx/layers/ipc/ImageBridgeChild.h376
-rw-r--r--gfx/layers/ipc/ImageBridgeParent.cpp428
-rw-r--r--gfx/layers/ipc/ImageBridgeParent.h151
-rw-r--r--gfx/layers/ipc/KnowsCompositor.cpp113
-rw-r--r--gfx/layers/ipc/KnowsCompositor.h261
-rw-r--r--gfx/layers/ipc/LayerTreeOwnerTracker.cpp68
-rw-r--r--gfx/layers/ipc/LayerTreeOwnerTracker.h73
-rw-r--r--gfx/layers/ipc/LayersMessageUtils.h1156
-rw-r--r--gfx/layers/ipc/LayersMessages.ipdlh362
-rw-r--r--gfx/layers/ipc/LayersSurfaces.ipdlh205
-rw-r--r--gfx/layers/ipc/PAPZ.ipdl81
-rw-r--r--gfx/layers/ipc/PAPZCTreeManager.ipdl95
-rw-r--r--gfx/layers/ipc/PAPZInputBridge.ipdl87
-rw-r--r--gfx/layers/ipc/PCanvas.ipdl52
-rw-r--r--gfx/layers/ipc/PCompositorBridge.ipdl214
-rw-r--r--gfx/layers/ipc/PCompositorBridgeTypes.ipdlh23
-rw-r--r--gfx/layers/ipc/PCompositorManager.ipdl97
-rw-r--r--gfx/layers/ipc/PImageBridge.ipdl65
-rw-r--r--gfx/layers/ipc/PTexture.ipdl40
-rw-r--r--gfx/layers/ipc/PUiCompositorController.ipdl48
-rw-r--r--gfx/layers/ipc/PVideoBridge.ipdl39
-rw-r--r--gfx/layers/ipc/PWebRenderBridge.ipdl139
-rw-r--r--gfx/layers/ipc/RefCountedShmem.cpp93
-rw-r--r--gfx/layers/ipc/RefCountedShmem.h48
-rw-r--r--gfx/layers/ipc/RemoteContentController.cpp519
-rw-r--r--gfx/layers/ipc/RemoteContentController.h123
-rw-r--r--gfx/layers/ipc/ShadowLayerUtils.h27
-rw-r--r--gfx/layers/ipc/SharedPlanarYCbCrImage.cpp174
-rw-r--r--gfx/layers/ipc/SharedPlanarYCbCrImage.h63
-rw-r--r--gfx/layers/ipc/SharedRGBImage.cpp156
-rw-r--r--gfx/layers/ipc/SharedRGBImage.h61
-rw-r--r--gfx/layers/ipc/SharedSurfacesChild.cpp587
-rw-r--r--gfx/layers/ipc/SharedSurfacesChild.h250
-rw-r--r--gfx/layers/ipc/SharedSurfacesMemoryReport.h59
-rw-r--r--gfx/layers/ipc/SharedSurfacesParent.cpp385
-rw-r--r--gfx/layers/ipc/SharedSurfacesParent.h135
-rw-r--r--gfx/layers/ipc/SurfaceDescriptor.h14
-rw-r--r--gfx/layers/ipc/SynchronousTask.h73
-rw-r--r--gfx/layers/ipc/TextureForwarder.h92
-rw-r--r--gfx/layers/ipc/UiCompositorControllerChild.cpp350
-rw-r--r--gfx/layers/ipc/UiCompositorControllerChild.h120
-rw-r--r--gfx/layers/ipc/UiCompositorControllerMessageTypes.h32
-rw-r--r--gfx/layers/ipc/UiCompositorControllerParent.cpp296
-rw-r--r--gfx/layers/ipc/UiCompositorControllerParent.h84
-rw-r--r--gfx/layers/ipc/VideoBridgeChild.cpp183
-rw-r--r--gfx/layers/ipc/VideoBridgeChild.h88
-rw-r--r--gfx/layers/ipc/VideoBridgeParent.cpp233
-rw-r--r--gfx/layers/ipc/VideoBridgeParent.h86
-rw-r--r--gfx/layers/ipc/VideoBridgeUtils.h38
-rw-r--r--gfx/layers/ipc/WebRenderMessages.ipdlh199
-rw-r--r--gfx/layers/layerviewer/hide.pngbin0 -> 3079 bytes
-rw-r--r--gfx/layers/layerviewer/index.html57
-rw-r--r--gfx/layers/layerviewer/layerTreeView.js972
-rw-r--r--gfx/layers/layerviewer/noise.pngbin0 -> 2118 bytes
-rw-r--r--gfx/layers/layerviewer/show.pngbin0 -> 3187 bytes
-rw-r--r--gfx/layers/layerviewer/tree.css37
-rw-r--r--gfx/layers/moz.build520
-rw-r--r--gfx/layers/opengl/CompositingRenderTargetOGL.cpp117
-rw-r--r--gfx/layers/opengl/CompositingRenderTargetOGL.h212
-rw-r--r--gfx/layers/opengl/CompositorOGL.cpp1747
-rw-r--r--gfx/layers/opengl/CompositorOGL.h446
-rw-r--r--gfx/layers/opengl/DMABUFTextureClientOGL.cpp123
-rw-r--r--gfx/layers/opengl/DMABUFTextureClientOGL.h67
-rw-r--r--gfx/layers/opengl/DMABUFTextureHostOGL.cpp186
-rw-r--r--gfx/layers/opengl/DMABUFTextureHostOGL.h69
-rw-r--r--gfx/layers/opengl/EGLImageHelpers.cpp58
-rw-r--r--gfx/layers/opengl/EGLImageHelpers.h28
-rw-r--r--gfx/layers/opengl/MacIOSurfaceTextureClientOGL.cpp120
-rw-r--r--gfx/layers/opengl/MacIOSurfaceTextureClientOGL.h60
-rw-r--r--gfx/layers/opengl/MacIOSurfaceTextureHostOGL.cpp240
-rw-r--r--gfx/layers/opengl/MacIOSurfaceTextureHostOGL.h84
-rw-r--r--gfx/layers/opengl/OGLShaderConfig.h255
-rw-r--r--gfx/layers/opengl/OGLShaderProgram.cpp1058
-rw-r--r--gfx/layers/opengl/OGLShaderProgram.h377
-rw-r--r--gfx/layers/opengl/TextureClientOGL.cpp328
-rw-r--r--gfx/layers/opengl/TextureClientOGL.h164
-rw-r--r--gfx/layers/opengl/TextureHostOGL.cpp1049
-rw-r--r--gfx/layers/opengl/TextureHostOGL.h652
-rw-r--r--gfx/layers/wr/AsyncImagePipelineManager.cpp771
-rw-r--r--gfx/layers/wr/AsyncImagePipelineManager.h281
-rw-r--r--gfx/layers/wr/ClipManager.cpp501
-rw-r--r--gfx/layers/wr/ClipManager.h150
-rw-r--r--gfx/layers/wr/DisplayItemCache.cpp203
-rw-r--r--gfx/layers/wr/DisplayItemCache.h210
-rw-r--r--gfx/layers/wr/HitTestInfoManager.cpp135
-rw-r--r--gfx/layers/wr/HitTestInfoManager.h66
-rw-r--r--gfx/layers/wr/IpcResourceUpdateQueue.cpp484
-rw-r--r--gfx/layers/wr/IpcResourceUpdateQueue.h195
-rw-r--r--gfx/layers/wr/OMTAController.cpp41
-rw-r--r--gfx/layers/wr/OMTAController.h44
-rw-r--r--gfx/layers/wr/OMTASampler.cpp248
-rw-r--r--gfx/layers/wr/OMTASampler.h156
-rw-r--r--gfx/layers/wr/RenderRootStateManager.cpp210
-rw-r--r--gfx/layers/wr/RenderRootStateManager.h97
-rw-r--r--gfx/layers/wr/RenderRootTypes.cpp110
-rw-r--r--gfx/layers/wr/RenderRootTypes.h75
-rw-r--r--gfx/layers/wr/StackingContextHelper.cpp275
-rw-r--r--gfx/layers/wr/StackingContextHelper.h129
-rw-r--r--gfx/layers/wr/WebRenderBridgeChild.cpp602
-rw-r--r--gfx/layers/wr/WebRenderBridgeChild.h269
-rw-r--r--gfx/layers/wr/WebRenderBridgeParent.cpp2909
-rw-r--r--gfx/layers/wr/WebRenderBridgeParent.h533
-rw-r--r--gfx/layers/wr/WebRenderCanvasRenderer.cpp90
-rw-r--r--gfx/layers/wr/WebRenderCanvasRenderer.h56
-rw-r--r--gfx/layers/wr/WebRenderCommandBuilder.cpp2959
-rw-r--r--gfx/layers/wr/WebRenderCommandBuilder.h237
-rw-r--r--gfx/layers/wr/WebRenderDrawEventRecorder.cpp33
-rw-r--r--gfx/layers/wr/WebRenderDrawEventRecorder.h49
-rw-r--r--gfx/layers/wr/WebRenderImageHost.cpp395
-rw-r--r--gfx/layers/wr/WebRenderImageHost.h100
-rw-r--r--gfx/layers/wr/WebRenderLayerManager.cpp821
-rw-r--r--gfx/layers/wr/WebRenderLayerManager.h282
-rw-r--r--gfx/layers/wr/WebRenderMessageUtils.h217
-rw-r--r--gfx/layers/wr/WebRenderScrollData.cpp508
-rw-r--r--gfx/layers/wr/WebRenderScrollData.h363
-rw-r--r--gfx/layers/wr/WebRenderScrollDataWrapper.h542
-rw-r--r--gfx/layers/wr/WebRenderTextureHost.cpp212
-rw-r--r--gfx/layers/wr/WebRenderTextureHost.h111
-rw-r--r--gfx/layers/wr/WebRenderUserData.cpp427
-rw-r--r--gfx/layers/wr/WebRenderUserData.h385
834 files changed, 162109 insertions, 0 deletions
diff --git a/gfx/layers/AndroidHardwareBuffer.cpp b/gfx/layers/AndroidHardwareBuffer.cpp
new file mode 100644
index 0000000000..6c474194aa
--- /dev/null
+++ b/gfx/layers/AndroidHardwareBuffer.cpp
@@ -0,0 +1,346 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "AndroidHardwareBuffer.h"
+
+#include <dlfcn.h>
+
+#include "mozilla/gfx/2D.h"
+#include "mozilla/gfx/gfxVars.h"
+#include "mozilla/layers/ImageBridgeChild.h"
+#include "mozilla/layers/TextureClientSharedSurface.h"
+#include "mozilla/TimeStamp.h"
+#include "mozilla/UniquePtrExtensions.h"
+
+namespace mozilla {
+namespace layers {
+
+static uint32_t ToAHardwareBuffer_Format(gfx::SurfaceFormat aFormat) {
+ switch (aFormat) {
+ case gfx::SurfaceFormat::R8G8B8A8:
+ case gfx::SurfaceFormat::B8G8R8A8:
+ return AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM;
+
+ case gfx::SurfaceFormat::R8G8B8X8:
+ case gfx::SurfaceFormat::B8G8R8X8:
+ return AHARDWAREBUFFER_FORMAT_R8G8B8X8_UNORM;
+
+ case gfx::SurfaceFormat::R5G6B5_UINT16:
+ return AHARDWAREBUFFER_FORMAT_R5G6B5_UNORM;
+
+ default:
+ MOZ_ASSERT_UNREACHABLE("Unsupported SurfaceFormat");
+ return AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM;
+ }
+}
+
+StaticAutoPtr<AndroidHardwareBufferApi> AndroidHardwareBufferApi::sInstance;
+
+/* static */
+void AndroidHardwareBufferApi::Init() {
+ MOZ_ASSERT(XRE_IsGPUProcess());
+
+ sInstance = new AndroidHardwareBufferApi();
+ if (!sInstance->Load()) {
+ sInstance = nullptr;
+ }
+}
+
+/* static */
+void AndroidHardwareBufferApi::Shutdown() { sInstance = nullptr; }
+
+AndroidHardwareBufferApi::AndroidHardwareBufferApi() {}
+
+bool AndroidHardwareBufferApi::Load() {
+ void* handle = dlopen("libandroid.so", RTLD_LAZY | RTLD_LOCAL);
+ MOZ_ASSERT(handle);
+ if (!handle) {
+ gfxCriticalNote << "Failed to load libandroid.so";
+ return false;
+ }
+
+ mAHardwareBuffer_allocate =
+ (_AHardwareBuffer_allocate)dlsym(handle, "AHardwareBuffer_allocate");
+ mAHardwareBuffer_acquire =
+ (_AHardwareBuffer_acquire)dlsym(handle, "AHardwareBuffer_acquire");
+ mAHardwareBuffer_release =
+ (_AHardwareBuffer_release)dlsym(handle, "AHardwareBuffer_release");
+ mAHardwareBuffer_describe =
+ (_AHardwareBuffer_describe)dlsym(handle, "AHardwareBuffer_describe");
+ mAHardwareBuffer_lock =
+ (_AHardwareBuffer_lock)dlsym(handle, "AHardwareBuffer_lock");
+ mAHardwareBuffer_unlock =
+ (_AHardwareBuffer_unlock)dlsym(handle, "AHardwareBuffer_unlock");
+ mAHardwareBuffer_sendHandleToUnixSocket =
+ (_AHardwareBuffer_sendHandleToUnixSocket)dlsym(
+ handle, "AHardwareBuffer_sendHandleToUnixSocket");
+ mAHardwareBuffer_recvHandleFromUnixSocket =
+ (_AHardwareBuffer_recvHandleFromUnixSocket)dlsym(
+ handle, "AHardwareBuffer_recvHandleFromUnixSocket");
+
+ if (!mAHardwareBuffer_allocate || !mAHardwareBuffer_acquire ||
+ !mAHardwareBuffer_release || !mAHardwareBuffer_describe ||
+ !mAHardwareBuffer_lock || !mAHardwareBuffer_unlock ||
+ !mAHardwareBuffer_sendHandleToUnixSocket ||
+ !mAHardwareBuffer_recvHandleFromUnixSocket) {
+ gfxCriticalNote << "Failed to load AHardwareBuffer";
+ return false;
+ }
+ return true;
+}
+
+void AndroidHardwareBufferApi::Allocate(const AHardwareBuffer_Desc* aDesc,
+ AHardwareBuffer** aOutBuffer) {
+ mAHardwareBuffer_allocate(aDesc, aOutBuffer);
+}
+
+void AndroidHardwareBufferApi::Acquire(AHardwareBuffer* aBuffer) {
+ mAHardwareBuffer_acquire(aBuffer);
+}
+
+void AndroidHardwareBufferApi::Release(AHardwareBuffer* aBuffer) {
+ mAHardwareBuffer_release(aBuffer);
+}
+
+void AndroidHardwareBufferApi::Describe(const AHardwareBuffer* aBuffer,
+ AHardwareBuffer_Desc* aOutDesc) {
+ mAHardwareBuffer_describe(aBuffer, aOutDesc);
+}
+
+int AndroidHardwareBufferApi::Lock(AHardwareBuffer* aBuffer, uint64_t aUsage,
+ int32_t aFence, const ARect* aRect,
+ void** aOutVirtualAddress) {
+ return mAHardwareBuffer_lock(aBuffer, aUsage, aFence, aRect,
+ aOutVirtualAddress);
+}
+
+int AndroidHardwareBufferApi::Unlock(AHardwareBuffer* aBuffer,
+ int32_t* aFence) {
+ return mAHardwareBuffer_unlock(aBuffer, aFence);
+}
+
+int AndroidHardwareBufferApi::SendHandleToUnixSocket(
+ const AHardwareBuffer* aBuffer, int aSocketFd) {
+ return mAHardwareBuffer_sendHandleToUnixSocket(aBuffer, aSocketFd);
+}
+
+int AndroidHardwareBufferApi::RecvHandleFromUnixSocket(
+ int aSocketFd, AHardwareBuffer** aOutBuffer) {
+ return mAHardwareBuffer_recvHandleFromUnixSocket(aSocketFd, aOutBuffer);
+}
+
+/* static */
+uint64_t AndroidHardwareBuffer::GetNextId() {
+ static std::atomic<uint64_t> sNextId = 0;
+ uint64_t id = ++sNextId;
+ return id;
+}
+
+/* static */
+already_AddRefed<AndroidHardwareBuffer> AndroidHardwareBuffer::Create(
+ gfx::IntSize aSize, gfx::SurfaceFormat aFormat) {
+ if (!AndroidHardwareBufferApi::Get()) {
+ return nullptr;
+ }
+
+ if (aFormat != gfx::SurfaceFormat::R8G8B8A8 &&
+ aFormat != gfx::SurfaceFormat::R8G8B8X8 &&
+ aFormat != gfx::SurfaceFormat::B8G8R8A8 &&
+ aFormat != gfx::SurfaceFormat::B8G8R8X8 &&
+ aFormat != gfx::SurfaceFormat::R5G6B5_UINT16) {
+ return nullptr;
+ }
+
+ AHardwareBuffer_Desc desc = {};
+ desc.width = aSize.width;
+ desc.height = aSize.height;
+ desc.layers = 1; // number of images
+ desc.usage = AHARDWAREBUFFER_USAGE_CPU_READ_OFTEN |
+ AHARDWAREBUFFER_USAGE_CPU_WRITE_OFTEN |
+ AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE |
+ AHARDWAREBUFFER_USAGE_GPU_COLOR_OUTPUT;
+ desc.format = ToAHardwareBuffer_Format(aFormat);
+
+ AHardwareBuffer* nativeBuffer = nullptr;
+ AndroidHardwareBufferApi::Get()->Allocate(&desc, &nativeBuffer);
+ if (!nativeBuffer) {
+ return nullptr;
+ }
+
+ AHardwareBuffer_Desc bufferInfo = {};
+ AndroidHardwareBufferApi::Get()->Describe(nativeBuffer, &bufferInfo);
+
+ RefPtr<AndroidHardwareBuffer> buffer = new AndroidHardwareBuffer(
+ nativeBuffer, aSize, bufferInfo.stride, aFormat, GetNextId());
+ AndroidHardwareBufferManager::Get()->Register(buffer);
+ return buffer.forget();
+}
+
+AndroidHardwareBuffer::AndroidHardwareBuffer(AHardwareBuffer* aNativeBuffer,
+ gfx::IntSize aSize,
+ uint32_t aStride,
+ gfx::SurfaceFormat aFormat,
+ uint64_t aId)
+ : mSize(aSize),
+ mStride(aStride),
+ mFormat(aFormat),
+ mId(aId),
+ mNativeBuffer(aNativeBuffer),
+ mIsRegistered(false) {
+ MOZ_ASSERT(mNativeBuffer);
+#ifdef DEBUG
+ AHardwareBuffer_Desc bufferInfo = {};
+ AndroidHardwareBufferApi::Get()->Describe(mNativeBuffer, &bufferInfo);
+ MOZ_ASSERT(mSize.width == (int32_t)bufferInfo.width);
+ MOZ_ASSERT(mSize.height == (int32_t)bufferInfo.height);
+ MOZ_ASSERT(mStride == bufferInfo.stride);
+ MOZ_ASSERT(ToAHardwareBuffer_Format(mFormat) == bufferInfo.format);
+#endif
+}
+
+AndroidHardwareBuffer::~AndroidHardwareBuffer() {
+ if (mIsRegistered) {
+ AndroidHardwareBufferManager::Get()->Unregister(this);
+ }
+ AndroidHardwareBufferApi::Get()->Release(mNativeBuffer);
+}
+
+int AndroidHardwareBuffer::Lock(uint64_t aUsage, const ARect* aRect,
+ void** aOutVirtualAddress) {
+ ipc::FileDescriptor fd = GetAndResetReleaseFence();
+ int32_t fenceFd = -1;
+ ipc::FileDescriptor::UniquePlatformHandle rawFd;
+ if (fd.IsValid()) {
+ rawFd = fd.TakePlatformHandle();
+ fenceFd = rawFd.get();
+ }
+ return AndroidHardwareBufferApi::Get()->Lock(mNativeBuffer, aUsage, fenceFd,
+ aRect, aOutVirtualAddress);
+}
+
+int AndroidHardwareBuffer::Unlock() {
+ int rawFd = -1;
+ // XXX All tested recent Android devices did not return valid fence.
+ int ret = AndroidHardwareBufferApi::Get()->Unlock(mNativeBuffer, &rawFd);
+ if (ret != 0) {
+ return ret;
+ }
+
+ ipc::FileDescriptor acquireFenceFd;
+ // The value -1 indicates that unlocking has already completed before
+ // the function returned and no further operations are necessary.
+ if (rawFd >= 0) {
+ acquireFenceFd = ipc::FileDescriptor(UniqueFileHandle(rawFd));
+ }
+
+ if (acquireFenceFd.IsValid()) {
+ SetAcquireFence(std::move(acquireFenceFd));
+ }
+ return 0;
+}
+
+void AndroidHardwareBuffer::SetReleaseFence(ipc::FileDescriptor&& aFenceFd) {
+ MonitorAutoLock lock(AndroidHardwareBufferManager::Get()->GetMonitor());
+ SetReleaseFence(std::move(aFenceFd), lock);
+}
+
+void AndroidHardwareBuffer::SetReleaseFence(ipc::FileDescriptor&& aFenceFd,
+ const MonitorAutoLock& aAutoLock) {
+ mReleaseFenceFd = std::move(aFenceFd);
+}
+
+void AndroidHardwareBuffer::SetAcquireFence(ipc::FileDescriptor&& aFenceFd) {
+ MonitorAutoLock lock(AndroidHardwareBufferManager::Get()->GetMonitor());
+
+ mAcquireFenceFd = std::move(aFenceFd);
+}
+
+ipc::FileDescriptor AndroidHardwareBuffer::GetAndResetReleaseFence() {
+ MonitorAutoLock lock(AndroidHardwareBufferManager::Get()->GetMonitor());
+
+ if (!mReleaseFenceFd.IsValid()) {
+ return ipc::FileDescriptor();
+ }
+
+ return std::move(mReleaseFenceFd);
+}
+
+ipc::FileDescriptor AndroidHardwareBuffer::GetAndResetAcquireFence() {
+ MonitorAutoLock lock(AndroidHardwareBufferManager::Get()->GetMonitor());
+
+ if (!mAcquireFenceFd.IsValid()) {
+ return ipc::FileDescriptor();
+ }
+
+ return std::move(mAcquireFenceFd);
+}
+
+ipc::FileDescriptor AndroidHardwareBuffer::GetAcquireFence() {
+ MonitorAutoLock lock(AndroidHardwareBufferManager::Get()->GetMonitor());
+
+ if (!mAcquireFenceFd.IsValid()) {
+ return ipc::FileDescriptor();
+ }
+
+ return mAcquireFenceFd;
+}
+
+StaticAutoPtr<AndroidHardwareBufferManager>
+ AndroidHardwareBufferManager::sInstance;
+
+/* static */
+void AndroidHardwareBufferManager::Init() {
+ MOZ_ASSERT(XRE_IsGPUProcess());
+
+ sInstance = new AndroidHardwareBufferManager();
+}
+
+/* static */
+void AndroidHardwareBufferManager::Shutdown() { sInstance = nullptr; }
+
+AndroidHardwareBufferManager::AndroidHardwareBufferManager()
+ : mMonitor("AndroidHardwareBufferManager.mMonitor") {}
+
+void AndroidHardwareBufferManager::Register(
+ RefPtr<AndroidHardwareBuffer> aBuffer) {
+ MonitorAutoLock lock(mMonitor);
+
+ aBuffer->mIsRegistered = true;
+ ThreadSafeWeakPtr<AndroidHardwareBuffer> weak(aBuffer);
+
+#ifdef DEBUG
+ const auto it = mBuffers.find(aBuffer->mId);
+ MOZ_ASSERT(it == mBuffers.end());
+#endif
+ mBuffers.emplace(aBuffer->mId, weak);
+}
+
+void AndroidHardwareBufferManager::Unregister(AndroidHardwareBuffer* aBuffer) {
+ MonitorAutoLock lock(mMonitor);
+
+ const auto it = mBuffers.find(aBuffer->mId);
+ MOZ_ASSERT(it != mBuffers.end());
+ if (it == mBuffers.end()) {
+ gfxCriticalNote << "AndroidHardwareBuffer id mismatch happened";
+ return;
+ }
+ mBuffers.erase(it);
+}
+
+already_AddRefed<AndroidHardwareBuffer> AndroidHardwareBufferManager::GetBuffer(
+ uint64_t aBufferId) {
+ MonitorAutoLock lock(mMonitor);
+
+ const auto it = mBuffers.find(aBufferId);
+ if (it == mBuffers.end()) {
+ return nullptr;
+ }
+ auto buffer = RefPtr<AndroidHardwareBuffer>(it->second);
+ return buffer.forget();
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/AndroidHardwareBuffer.h b/gfx/layers/AndroidHardwareBuffer.h
new file mode 100644
index 0000000000..6b8a66b26f
--- /dev/null
+++ b/gfx/layers/AndroidHardwareBuffer.h
@@ -0,0 +1,181 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_LAYERS_ANDROID_HARDWARE_BUFFER
+#define MOZILLA_LAYERS_ANDROID_HARDWARE_BUFFER
+
+#include <android/hardware_buffer.h>
+#include <atomic>
+#include <unordered_map>
+
+#include "mozilla/layers/TextureClient.h"
+#include "mozilla/gfx/Types.h"
+#include "mozilla/gfx/2D.h"
+#include "mozilla/Monitor.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/ThreadSafeWeakPtr.h"
+
+namespace mozilla {
+namespace layers {
+
+/**
+ * AndroidHardwareBufferApi provides apis for managing AHardwareBuffer.
+ * The apis are supported since Android O(APIVersion 26).
+ */
+class AndroidHardwareBufferApi final {
+ public:
+ static void Init();
+ static void Shutdown();
+
+ static AndroidHardwareBufferApi* Get() { return sInstance; }
+
+ void Allocate(const AHardwareBuffer_Desc* aDesc,
+ AHardwareBuffer** aOutBuffer);
+ void Acquire(AHardwareBuffer* aBuffer);
+ void Release(AHardwareBuffer* aBuffer);
+ void Describe(const AHardwareBuffer* aBuffer, AHardwareBuffer_Desc* aOutDesc);
+ int Lock(AHardwareBuffer* aBuffer, uint64_t aUsage, int32_t aFence,
+ const ARect* aRect, void** aOutVirtualAddress);
+ int Unlock(AHardwareBuffer* aBuffer, int32_t* aFence);
+ int SendHandleToUnixSocket(const AHardwareBuffer* aBuffer, int aSocketFd);
+ int RecvHandleFromUnixSocket(int aSocketFd, AHardwareBuffer** aOutBuffer);
+
+ private:
+ AndroidHardwareBufferApi();
+ bool Load();
+
+ using _AHardwareBuffer_allocate = int (*)(const AHardwareBuffer_Desc* desc,
+ AHardwareBuffer** outBuffer);
+ using _AHardwareBuffer_acquire = void (*)(AHardwareBuffer* buffer);
+ using _AHardwareBuffer_release = void (*)(AHardwareBuffer* buffer);
+ using _AHardwareBuffer_describe = void (*)(const AHardwareBuffer* buffer,
+ AHardwareBuffer_Desc* outDesc);
+ using _AHardwareBuffer_lock = int (*)(AHardwareBuffer* buffer, uint64_t usage,
+ int32_t fence, const ARect* rect,
+ void** outVirtualAddress);
+ using _AHardwareBuffer_unlock = int (*)(AHardwareBuffer* buffer,
+ int32_t* fence);
+ using _AHardwareBuffer_sendHandleToUnixSocket =
+ int (*)(const AHardwareBuffer* buffer, int socketFd);
+ using _AHardwareBuffer_recvHandleFromUnixSocket =
+ int (*)(int socketFd, AHardwareBuffer** outBuffer);
+
+ _AHardwareBuffer_allocate mAHardwareBuffer_allocate = nullptr;
+ _AHardwareBuffer_acquire mAHardwareBuffer_acquire = nullptr;
+ _AHardwareBuffer_release mAHardwareBuffer_release = nullptr;
+ _AHardwareBuffer_describe mAHardwareBuffer_describe = nullptr;
+ _AHardwareBuffer_lock mAHardwareBuffer_lock = nullptr;
+ _AHardwareBuffer_unlock mAHardwareBuffer_unlock = nullptr;
+ _AHardwareBuffer_sendHandleToUnixSocket
+ mAHardwareBuffer_sendHandleToUnixSocket = nullptr;
+ _AHardwareBuffer_recvHandleFromUnixSocket
+ mAHardwareBuffer_recvHandleFromUnixSocket = nullptr;
+
+ static StaticAutoPtr<AndroidHardwareBufferApi> sInstance;
+};
+
+/**
+ * AndroidHardwareBuffer is a wrapper of AHardwareBuffer. AHardwareBuffer wraps
+ * android GraphicBuffer. It is supported since Android O(APIVersion 26).
+ * The manager is mainly used for release fences delivery from
+ * host side to client side.
+ */
+class AndroidHardwareBuffer
+ : public SupportsThreadSafeWeakPtr<AndroidHardwareBuffer> {
+ public:
+ MOZ_DECLARE_REFCOUNTED_TYPENAME(AndroidHardwareBuffer)
+
+ static already_AddRefed<AndroidHardwareBuffer> Create(
+ gfx::IntSize aSize, gfx::SurfaceFormat aFormat);
+
+ virtual ~AndroidHardwareBuffer();
+
+ int Lock(uint64_t aUsage, const ARect* aRect, void** aOutVirtualAddress);
+ int Unlock();
+
+ AHardwareBuffer* GetNativeBuffer() const { return mNativeBuffer; }
+
+ void SetAcquireFence(ipc::FileDescriptor&& aFenceFd);
+
+ void SetReleaseFence(ipc::FileDescriptor&& aFenceFd);
+
+ ipc::FileDescriptor GetAndResetReleaseFence();
+
+ ipc::FileDescriptor GetAndResetAcquireFence();
+
+ ipc::FileDescriptor GetAcquireFence();
+
+ const gfx::IntSize mSize;
+ const uint32_t mStride;
+ const gfx::SurfaceFormat mFormat;
+ const uint64_t mId;
+
+ protected:
+ AndroidHardwareBuffer(AHardwareBuffer* aNativeBuffer, gfx::IntSize aSize,
+ uint32_t aStride, gfx::SurfaceFormat aFormat,
+ uint64_t aId);
+
+ void SetReleaseFence(ipc::FileDescriptor&& aFenceFd,
+ const MonitorAutoLock& aAutoLock);
+
+ AHardwareBuffer* mNativeBuffer;
+
+ // When true, AndroidHardwareBuffer is registered to
+ // AndroidHardwareBufferManager.
+ bool mIsRegistered;
+
+ // protected by AndroidHardwareBufferManager::mMonitor
+
+ // FileDescriptor of release fence.
+ // Release fence is a fence that is used for waiting until usage/composite of
+ // AHardwareBuffer is ended. The fence is delivered via ImageBridge.
+ ipc::FileDescriptor mReleaseFenceFd;
+
+ // FileDescriptor of acquire fence.
+ // Acquire fence is a fence that is used for waiting until rendering to
+ // its AHardwareBuffer is completed.
+ ipc::FileDescriptor mAcquireFenceFd;
+
+ static uint64_t GetNextId();
+
+ friend class AndroidHardwareBufferManager;
+};
+
+/**
+ * AndroidHardwareBufferManager manages AndroidHardwareBuffers that is
+ * allocated by client side.
+ * Host side only uses mMonitor for thread safety of AndroidHardwareBuffer.
+ */
+class AndroidHardwareBufferManager {
+ public:
+ static void Init();
+ static void Shutdown();
+
+ AndroidHardwareBufferManager();
+
+ static AndroidHardwareBufferManager* Get() { return sInstance; }
+
+ void Register(RefPtr<AndroidHardwareBuffer> aBuffer);
+
+ void Unregister(AndroidHardwareBuffer* aBuffer);
+
+ already_AddRefed<AndroidHardwareBuffer> GetBuffer(uint64_t aBufferId);
+
+ Monitor& GetMonitor() { return mMonitor; }
+
+ private:
+ Monitor mMonitor MOZ_UNANNOTATED;
+ std::unordered_map<uint64_t, ThreadSafeWeakPtr<AndroidHardwareBuffer>>
+ mBuffers;
+
+ static StaticAutoPtr<AndroidHardwareBufferManager> sInstance;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif
diff --git a/gfx/layers/AnimationHelper.cpp b/gfx/layers/AnimationHelper.cpp
new file mode 100644
index 0000000000..e72889e4d4
--- /dev/null
+++ b/gfx/layers/AnimationHelper.cpp
@@ -0,0 +1,836 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "AnimationHelper.h"
+#include "base/process_util.h"
+#include "gfx2DGlue.h" // for ThebesRect
+#include "gfxLineSegment.h" // for gfxLineSegment
+#include "gfxPoint.h" // for gfxPoint
+#include "gfxQuad.h" // for gfxQuad
+#include "gfxRect.h" // for gfxRect
+#include "gfxUtils.h" // for gfxUtils::TransformToQuad
+#include "mozilla/ServoStyleConsts.h" // for StyleComputedTimingFunction
+#include "mozilla/dom/AnimationEffectBinding.h" // for dom::FillMode
+#include "mozilla/dom/KeyframeEffectBinding.h" // for dom::IterationComposite
+#include "mozilla/dom/KeyframeEffect.h" // for dom::KeyFrameEffectReadOnly
+#include "mozilla/dom/Nullable.h" // for dom::Nullable
+#include "mozilla/layers/APZSampler.h" // for APZSampler
+#include "mozilla/layers/CompositorThread.h" // for CompositorThreadHolder
+#include "mozilla/LayerAnimationInfo.h" // for GetCSSPropertiesFor()
+#include "mozilla/MotionPathUtils.h" // for ResolveMotionPath()
+#include "mozilla/ServoBindings.h" // for Servo_ComposeAnimationSegment, etc
+#include "mozilla/StyleAnimationValue.h" // for StyleAnimationValue, etc
+#include "nsDeviceContext.h" // for AppUnitsPerCSSPixel
+#include "nsDisplayList.h" // for nsDisplayTransform, etc
+
+namespace mozilla {
+namespace layers {
+
+static dom::Nullable<TimeDuration> CalculateElapsedTimeForScrollTimeline(
+ const Maybe<APZSampler::ScrollOffsetAndRange> aScrollMeta,
+ const ScrollTimelineOptions& aOptions, const StickyTimeDuration& aEndTime,
+ const TimeDuration& aStartTime, float aPlaybackRate) {
+ // We return Nothing If the associated APZ controller is not available
+ // (because it may be destroyed but this animation is still alive).
+ if (!aScrollMeta) {
+ // This may happen after we reload a page. There may be a race condition
+ // because the animation is still alive but the APZ is destroyed. In this
+ // case, this animation is invalid, so we return nullptr.
+ return nullptr;
+ }
+
+ const bool isHorizontal =
+ aOptions.axis() == layers::ScrollDirection::eHorizontal;
+ double range =
+ isHorizontal ? aScrollMeta->mRange.width : aScrollMeta->mRange.height;
+ MOZ_ASSERT(
+ range > 0,
+ "We don't expect to get a zero or negative range on the compositor");
+
+ // The offset may be negative if the writing mode is from right to left.
+ // Use std::abs() here to avoid getting a negative progress.
+ double position =
+ std::abs(isHorizontal ? aScrollMeta->mOffset.x : aScrollMeta->mOffset.y);
+ double progress = position / range;
+ // Just in case to avoid getting a progress more than 100%, for overscrolling.
+ progress = std::min(progress, 1.0);
+ auto timelineTime = TimeDuration(aEndTime.MultDouble(progress));
+ return dom::Animation::CurrentTimeFromTimelineTime(timelineTime, aStartTime,
+ aPlaybackRate);
+}
+
+static dom::Nullable<TimeDuration> CalculateElapsedTime(
+ const APZSampler* aAPZSampler, const LayersId& aLayersId,
+ const MutexAutoLock& aProofOfMapLock, const PropertyAnimation& aAnimation,
+ const TimeStamp aPreviousFrameTime, const TimeStamp aCurrentFrameTime,
+ const AnimatedValue* aPreviousValue) {
+ // -------------------------------------
+ // Case 1: scroll-timeline animations.
+ // -------------------------------------
+ if (aAnimation.mScrollTimelineOptions) {
+ MOZ_ASSERT(
+ aAPZSampler,
+ "We don't send scroll animations to the compositor if APZ is disabled");
+
+ return CalculateElapsedTimeForScrollTimeline(
+ aAPZSampler->GetCurrentScrollOffsetAndRange(
+ aLayersId, aAnimation.mScrollTimelineOptions.value().source(),
+ aProofOfMapLock),
+ aAnimation.mScrollTimelineOptions.value(), aAnimation.mTiming.EndTime(),
+ aAnimation.mStartTime.refOr(aAnimation.mHoldTime),
+ aAnimation.mPlaybackRate);
+ }
+
+ // -------------------------------------
+ // Case 2: document-timeline animations.
+ // -------------------------------------
+ MOZ_ASSERT(
+ (!aAnimation.mOriginTime.IsNull() && aAnimation.mStartTime.isSome()) ||
+ aAnimation.mIsNotPlaying,
+ "If we are playing, we should have an origin time and a start time");
+
+ // Determine if the animation was play-pending and used a ready time later
+ // than the previous frame time.
+ //
+ // To determine this, _all_ of the following conditions need to hold:
+ //
+ // * There was no previous animation value (i.e. this is the first frame for
+ // the animation since it was sent to the compositor), and
+ // * The animation is playing, and
+ // * There is a previous frame time, and
+ // * The ready time of the animation is ahead of the previous frame time.
+ //
+ bool hasFutureReadyTime = false;
+ if (!aPreviousValue && !aAnimation.mIsNotPlaying &&
+ !aPreviousFrameTime.IsNull()) {
+ // This is the inverse of the calculation performed in
+ // AnimationInfo::StartPendingAnimations to calculate the start time of
+ // play-pending animations.
+ // Note that we have to calculate (TimeStamp + TimeDuration) last to avoid
+ // underflow in the middle of the calulation.
+ const TimeStamp readyTime =
+ aAnimation.mOriginTime +
+ (aAnimation.mStartTime.ref() +
+ aAnimation.mHoldTime.MultDouble(1.0 / aAnimation.mPlaybackRate));
+ hasFutureReadyTime = !readyTime.IsNull() && readyTime > aPreviousFrameTime;
+ }
+ // Use the previous vsync time to make main thread animations and compositor
+ // more closely aligned.
+ //
+ // On the first frame where we have animations the previous timestamp will
+ // not be set so we simply use the current timestamp. As a result we will
+ // end up painting the first frame twice. That doesn't appear to be
+ // noticeable, however.
+ //
+ // Likewise, if the animation is play-pending, it may have a ready time that
+ // is *after* |aPreviousFrameTime| (but *before* |aCurrentFrameTime|).
+ // To avoid flicker we need to use |aCurrentFrameTime| to avoid temporarily
+ // jumping backwards into the range prior to when the animation starts.
+ const TimeStamp& timeStamp = aPreviousFrameTime.IsNull() || hasFutureReadyTime
+ ? aCurrentFrameTime
+ : aPreviousFrameTime;
+
+ // If the animation is not currently playing, e.g. paused or
+ // finished, then use the hold time to stay at the same position.
+ TimeDuration elapsedDuration =
+ aAnimation.mIsNotPlaying || aAnimation.mStartTime.isNothing()
+ ? aAnimation.mHoldTime
+ : (timeStamp - aAnimation.mOriginTime - aAnimation.mStartTime.ref())
+ .MultDouble(aAnimation.mPlaybackRate);
+ return elapsedDuration;
+}
+
+enum class CanSkipCompose {
+ IfPossible,
+ No,
+};
+// This function samples the animation for a specific property. We may have
+// multiple animations for a single property, and the later animations override
+// the eariler ones. This function returns the sampled animation value,
+// |aAnimationValue| for a single CSS property.
+static AnimationHelper::SampleResult SampleAnimationForProperty(
+ const APZSampler* aAPZSampler, const LayersId& aLayersId,
+ const MutexAutoLock& aProofOfMapLock, TimeStamp aPreviousFrameTime,
+ TimeStamp aCurrentFrameTime, const AnimatedValue* aPreviousValue,
+ CanSkipCompose aCanSkipCompose,
+ nsTArray<PropertyAnimation>& aPropertyAnimations,
+ RefPtr<StyleAnimationValue>& aAnimationValue) {
+ MOZ_ASSERT(!aPropertyAnimations.IsEmpty(), "Should have animations");
+
+ auto reason = AnimationHelper::SampleResult::Reason::None;
+ bool hasInEffectAnimations = false;
+#ifdef DEBUG
+ // In cases where this function returns a SampleResult::Skipped, we actually
+ // do populate aAnimationValue in debug mode, so that we can MOZ_ASSERT at the
+ // call site that the value that would have been computed matches the stored
+ // value that we end up using. This flag is used to ensure we populate
+ // aAnimationValue in this scenario.
+ bool shouldBeSkipped = false;
+#endif
+ // Process in order, since later animations override earlier ones.
+ for (PropertyAnimation& animation : aPropertyAnimations) {
+ dom::Nullable<TimeDuration> elapsedDuration = CalculateElapsedTime(
+ aAPZSampler, aLayersId, aProofOfMapLock, animation, aPreviousFrameTime,
+ aCurrentFrameTime, aPreviousValue);
+
+ const auto progressTimelinePosition =
+ animation.mScrollTimelineOptions
+ ? dom::Animation::AtProgressTimelineBoundary(
+ TimeDuration::FromMilliseconds(
+ PROGRESS_TIMELINE_DURATION_MILLISEC),
+ elapsedDuration, animation.mStartTime.refOr(TimeDuration()),
+ animation.mPlaybackRate)
+ : dom::Animation::ProgressTimelinePosition::NotBoundary;
+
+ ComputedTiming computedTiming = dom::AnimationEffect::GetComputedTimingAt(
+ elapsedDuration, animation.mTiming, animation.mPlaybackRate,
+ progressTimelinePosition);
+
+ if (computedTiming.mProgress.IsNull()) {
+ // For the scroll-driven animations, it's possible to let it go between
+ // the active phase and the before/after phase, and so its progress
+ // becomes null. In this case, we shouldn't just skip this animation.
+ // Instead, we have to reset the previous sampled result. Basically, we
+ // use |mProgressOnLastCompose| to check if it goes from the active phase.
+ // If so, we set the returned |mReason| to ScrollToDelayPhase to let the
+ // caller know we need to use the base style for this property.
+ //
+ // If there are any other animations which need to be sampled together
+ // (in the same property animation group), this |reason| will be ignored.
+ if (animation.mScrollTimelineOptions &&
+ !animation.mProgressOnLastCompose.IsNull() &&
+ (computedTiming.mPhase == ComputedTiming::AnimationPhase::Before ||
+ computedTiming.mPhase == ComputedTiming::AnimationPhase::After)) {
+ // Appearally, we go back to delay, so need to reset the last
+ // composition meta data. This is necessary because
+ // 1. this animation is in delay so it shouldn't have any composition
+ // meta data, and
+ // 2. we will not go into this condition multiple times during delay
+ // phase because we rely on |mProgressOnLastCompose|.
+ animation.ResetLastCompositionValues();
+ reason = AnimationHelper::SampleResult::Reason::ScrollToDelayPhase;
+ }
+ continue;
+ }
+
+ dom::IterationCompositeOperation iterCompositeOperation =
+ animation.mIterationComposite;
+
+ // Skip calculation if the progress hasn't changed since the last
+ // calculation.
+ // Note that we don't skip calculate this animation if there is another
+ // animation since the other animation might be 'accumulate' or 'add', or
+ // might have a missing keyframe (i.e. this animation value will be used in
+ // the missing keyframe).
+ // FIXME Bug 1455476: We should do this optimizations for the case where
+ // the layer has multiple animations and multiple properties.
+ if (aCanSkipCompose == CanSkipCompose::IfPossible &&
+ !dom::KeyframeEffect::HasComputedTimingChanged(
+ computedTiming, iterCompositeOperation,
+ animation.mProgressOnLastCompose,
+ animation.mCurrentIterationOnLastCompose)) {
+#ifdef DEBUG
+ shouldBeSkipped = true;
+#else
+ return AnimationHelper::SampleResult::Skipped();
+#endif
+ }
+
+ uint32_t segmentIndex = 0;
+ size_t segmentSize = animation.mSegments.Length();
+ PropertyAnimation::SegmentData* segment = animation.mSegments.Elements();
+ while (segment->mEndPortion < computedTiming.mProgress.Value() &&
+ segmentIndex < segmentSize - 1) {
+ ++segment;
+ ++segmentIndex;
+ }
+
+ double positionInSegment =
+ (computedTiming.mProgress.Value() - segment->mStartPortion) /
+ (segment->mEndPortion - segment->mStartPortion);
+
+ double portion = StyleComputedTimingFunction::GetPortion(
+ segment->mFunction, positionInSegment, computedTiming.mBeforeFlag);
+
+ // Like above optimization, skip calculation if the target segment isn't
+ // changed and if the portion in the segment isn't changed.
+ // This optimization is needed for CSS animations/transitions with step
+ // timing functions (e.g. the throbber animation on tabs or frame based
+ // animations).
+ // FIXME Bug 1455476: Like the above optimization, we should apply this
+ // optimizations for multiple animation cases and multiple properties as
+ // well.
+ if (aCanSkipCompose == CanSkipCompose::IfPossible &&
+ animation.mSegmentIndexOnLastCompose == segmentIndex &&
+ !animation.mPortionInSegmentOnLastCompose.IsNull() &&
+ animation.mPortionInSegmentOnLastCompose.Value() == portion) {
+#ifdef DEBUG
+ shouldBeSkipped = true;
+#else
+ return AnimationHelper::SampleResult::Skipped();
+#endif
+ }
+
+ AnimationPropertySegment animSegment;
+ animSegment.mFromKey = 0.0;
+ animSegment.mToKey = 1.0;
+ animSegment.mFromValue = AnimationValue(segment->mStartValue);
+ animSegment.mToValue = AnimationValue(segment->mEndValue);
+ animSegment.mFromComposite = segment->mStartComposite;
+ animSegment.mToComposite = segment->mEndComposite;
+
+ // interpolate the property
+ aAnimationValue =
+ Servo_ComposeAnimationSegment(
+ &animSegment, aAnimationValue,
+ animation.mSegments.LastElement().mEndValue, iterCompositeOperation,
+ portion, computedTiming.mCurrentIteration)
+ .Consume();
+
+#ifdef DEBUG
+ if (shouldBeSkipped) {
+ return AnimationHelper::SampleResult::Skipped();
+ }
+#endif
+
+ hasInEffectAnimations = true;
+ animation.mProgressOnLastCompose = computedTiming.mProgress;
+ animation.mCurrentIterationOnLastCompose = computedTiming.mCurrentIteration;
+ animation.mSegmentIndexOnLastCompose = segmentIndex;
+ animation.mPortionInSegmentOnLastCompose.SetValue(portion);
+ }
+
+ auto rv = hasInEffectAnimations ? AnimationHelper::SampleResult::Sampled()
+ : AnimationHelper::SampleResult();
+ rv.mReason = reason;
+ return rv;
+}
+
+// This function samples the animations for a group of CSS properties. We may
+// have multiple CSS properties in a group (e.g. transform-like properties).
+// So the returned animation array, |aAnimationValues|, include all the
+// animation values of these CSS properties.
+AnimationHelper::SampleResult AnimationHelper::SampleAnimationForEachNode(
+ const APZSampler* aAPZSampler, const LayersId& aLayersId,
+ const MutexAutoLock& aProofOfMapLock, TimeStamp aPreviousFrameTime,
+ TimeStamp aCurrentFrameTime, const AnimatedValue* aPreviousValue,
+ nsTArray<PropertyAnimationGroup>& aPropertyAnimationGroups,
+ nsTArray<RefPtr<StyleAnimationValue>>& aAnimationValues /* out */) {
+ MOZ_ASSERT(!aPropertyAnimationGroups.IsEmpty(),
+ "Should be called with animation data");
+ MOZ_ASSERT(aAnimationValues.IsEmpty(),
+ "Should be called with empty aAnimationValues");
+
+ nsTArray<RefPtr<StyleAnimationValue>> baseStyleOfDelayAnimations;
+ nsTArray<RefPtr<StyleAnimationValue>> nonAnimatingValues;
+ for (PropertyAnimationGroup& group : aPropertyAnimationGroups) {
+ // Initialize animation value with base style.
+ RefPtr<StyleAnimationValue> currValue = group.mBaseStyle;
+
+ CanSkipCompose canSkipCompose =
+ aPreviousValue && aPropertyAnimationGroups.Length() == 1 &&
+ group.mAnimations.Length() == 1
+ ? CanSkipCompose::IfPossible
+ : CanSkipCompose::No;
+
+ MOZ_ASSERT(
+ !group.mAnimations.IsEmpty() ||
+ nsCSSPropertyIDSet::TransformLikeProperties().HasProperty(
+ group.mProperty),
+ "Only transform-like properties can have empty PropertyAnimation list");
+
+ // For properties which are not animating (i.e. their values are always the
+ // same), we store them in a different array, and then merge them into the
+ // final result (a.k.a. aAnimationValues) because we shouldn't take them
+ // into account for SampleResult. (In other words, these properties
+ // shouldn't affect the optimization.)
+ if (group.mAnimations.IsEmpty()) {
+ nonAnimatingValues.AppendElement(std::move(currValue));
+ continue;
+ }
+
+ SampleResult result = SampleAnimationForProperty(
+ aAPZSampler, aLayersId, aProofOfMapLock, aPreviousFrameTime,
+ aCurrentFrameTime, aPreviousValue, canSkipCompose, group.mAnimations,
+ currValue);
+
+ // FIXME: Bug 1455476: Do optimization for multiple properties. For now,
+ // the result is skipped only if the property count == 1.
+ if (result.IsSkipped()) {
+#ifdef DEBUG
+ aAnimationValues.AppendElement(std::move(currValue));
+#endif
+ return result;
+ }
+
+ if (!result.IsSampled()) {
+ if (result.mReason == SampleResult::Reason::ScrollToDelayPhase) {
+ MOZ_ASSERT(currValue && currValue == group.mBaseStyle);
+ baseStyleOfDelayAnimations.AppendElement(std::move(currValue));
+ }
+ continue;
+ }
+
+ // Insert the interpolation result into the output array.
+ MOZ_ASSERT(currValue);
+ aAnimationValues.AppendElement(std::move(currValue));
+ }
+
+ SampleResult rv =
+ aAnimationValues.IsEmpty() ? SampleResult() : SampleResult::Sampled();
+
+ // If there is no other sampled result, we may store these base styles
+ // (together with the non-animating values) to the webrenderer before it gets
+ // sync with the main thread.
+ if (rv.IsNone() && !baseStyleOfDelayAnimations.IsEmpty()) {
+ aAnimationValues.AppendElements(std::move(baseStyleOfDelayAnimations));
+ rv.mReason = SampleResult::Reason::ScrollToDelayPhase;
+ }
+
+ if (!aAnimationValues.IsEmpty()) {
+ aAnimationValues.AppendElements(std::move(nonAnimatingValues));
+ }
+ return rv;
+}
+
+static dom::FillMode GetAdjustedFillMode(const Animation& aAnimation) {
+ // Adjust fill mode so that if the main thread is delayed in clearing
+ // this animation we don't introduce flicker by jumping back to the old
+ // underlying value.
+ auto fillMode = static_cast<dom::FillMode>(aAnimation.fillMode());
+ float playbackRate = aAnimation.playbackRate();
+ switch (fillMode) {
+ case dom::FillMode::None:
+ if (playbackRate > 0) {
+ fillMode = dom::FillMode::Forwards;
+ } else if (playbackRate < 0) {
+ fillMode = dom::FillMode::Backwards;
+ }
+ break;
+ case dom::FillMode::Backwards:
+ if (playbackRate > 0) {
+ fillMode = dom::FillMode::Both;
+ }
+ break;
+ case dom::FillMode::Forwards:
+ if (playbackRate < 0) {
+ fillMode = dom::FillMode::Both;
+ }
+ break;
+ default:
+ break;
+ }
+ return fillMode;
+}
+
+#ifdef DEBUG
+static bool HasTransformLikeAnimations(const AnimationArray& aAnimations) {
+ nsCSSPropertyIDSet transformSet =
+ nsCSSPropertyIDSet::TransformLikeProperties();
+
+ for (const Animation& animation : aAnimations) {
+ if (animation.isNotAnimating()) {
+ continue;
+ }
+
+ if (transformSet.HasProperty(animation.property())) {
+ return true;
+ }
+ }
+
+ return false;
+}
+#endif
+
+AnimationStorageData AnimationHelper::ExtractAnimations(
+ const LayersId& aLayersId, const AnimationArray& aAnimations) {
+ AnimationStorageData storageData;
+ storageData.mLayersId = aLayersId;
+
+ nsCSSPropertyID prevID = eCSSProperty_UNKNOWN;
+ PropertyAnimationGroup* currData = nullptr;
+ DebugOnly<const layers::Animatable*> currBaseStyle = nullptr;
+
+ for (const Animation& animation : aAnimations) {
+ // Animations with same property are grouped together, so we can just
+ // check if the current property is the same as the previous one for
+ // knowing this is a new group.
+ if (prevID != animation.property()) {
+ // Got a different group, we should create a different array.
+ currData = storageData.mAnimation.AppendElement();
+ currData->mProperty = animation.property();
+ if (animation.transformData()) {
+ MOZ_ASSERT(!storageData.mTransformData,
+ "Only one entry has TransformData");
+ storageData.mTransformData = animation.transformData();
+ }
+
+ prevID = animation.property();
+
+ // Reset the debug pointer.
+ currBaseStyle = nullptr;
+ }
+
+ MOZ_ASSERT(currData);
+ if (animation.baseStyle().type() != Animatable::Tnull_t) {
+ MOZ_ASSERT(!currBaseStyle || *currBaseStyle == animation.baseStyle(),
+ "Should be the same base style");
+
+ currData->mBaseStyle = AnimationValue::FromAnimatable(
+ animation.property(), animation.baseStyle());
+ currBaseStyle = &animation.baseStyle();
+ }
+
+ // If this layers::Animation sets isNotAnimating to true, it only has
+ // base style and doesn't have any animation information, so we can skip
+ // the rest steps. (And so its PropertyAnimationGroup::mAnimation will be
+ // an empty array.)
+ if (animation.isNotAnimating()) {
+ MOZ_ASSERT(nsCSSPropertyIDSet::TransformLikeProperties().HasProperty(
+ animation.property()),
+ "Only transform-like properties could set this true");
+
+ if (animation.property() == eCSSProperty_offset_path) {
+ MOZ_ASSERT(currData->mBaseStyle,
+ "Fixed offset-path should have base style");
+ MOZ_ASSERT(HasTransformLikeAnimations(aAnimations));
+
+ AnimationValue value{currData->mBaseStyle};
+ const StyleOffsetPath& offsetPath = value.GetOffsetPathProperty();
+ if (offsetPath.IsPath()) {
+ MOZ_ASSERT(!storageData.mCachedMotionPath,
+ "Only one offset-path: path() is set");
+
+ RefPtr<gfx::PathBuilder> builder =
+ MotionPathUtils::GetCompositorPathBuilder();
+ storageData.mCachedMotionPath =
+ MotionPathUtils::BuildPath(offsetPath.AsPath(), builder);
+ }
+ }
+
+ continue;
+ }
+
+ PropertyAnimation* propertyAnimation =
+ currData->mAnimations.AppendElement();
+
+ propertyAnimation->mOriginTime = animation.originTime();
+ propertyAnimation->mStartTime = animation.startTime();
+ propertyAnimation->mHoldTime = animation.holdTime();
+ propertyAnimation->mPlaybackRate = animation.playbackRate();
+ propertyAnimation->mIterationComposite =
+ static_cast<dom::IterationCompositeOperation>(
+ animation.iterationComposite());
+ propertyAnimation->mIsNotPlaying = animation.isNotPlaying();
+ propertyAnimation->mTiming =
+ TimingParams{animation.duration(),
+ animation.delay(),
+ animation.endDelay(),
+ animation.iterations(),
+ animation.iterationStart(),
+ static_cast<dom::PlaybackDirection>(animation.direction()),
+ GetAdjustedFillMode(animation),
+ animation.easingFunction()};
+ propertyAnimation->mScrollTimelineOptions =
+ animation.scrollTimelineOptions();
+
+ nsTArray<PropertyAnimation::SegmentData>& segmentData =
+ propertyAnimation->mSegments;
+ for (const AnimationSegment& segment : animation.segments()) {
+ segmentData.AppendElement(PropertyAnimation::SegmentData{
+ AnimationValue::FromAnimatable(animation.property(),
+ segment.startState()),
+ AnimationValue::FromAnimatable(animation.property(),
+ segment.endState()),
+ segment.sampleFn(), segment.startPortion(), segment.endPortion(),
+ static_cast<dom::CompositeOperation>(segment.startComposite()),
+ static_cast<dom::CompositeOperation>(segment.endComposite())});
+ }
+ }
+
+#ifdef DEBUG
+ // Sanity check that the grouped animation data is correct by looking at the
+ // property set.
+ if (!storageData.mAnimation.IsEmpty()) {
+ nsCSSPropertyIDSet seenProperties;
+ for (const auto& group : storageData.mAnimation) {
+ nsCSSPropertyID id = group.mProperty;
+
+ MOZ_ASSERT(!seenProperties.HasProperty(id), "Should be a new property");
+ seenProperties.AddProperty(id);
+ }
+
+ MOZ_ASSERT(
+ seenProperties.IsSubsetOf(LayerAnimationInfo::GetCSSPropertiesFor(
+ DisplayItemType::TYPE_TRANSFORM)) ||
+ seenProperties.IsSubsetOf(LayerAnimationInfo::GetCSSPropertiesFor(
+ DisplayItemType::TYPE_OPACITY)) ||
+ seenProperties.IsSubsetOf(LayerAnimationInfo::GetCSSPropertiesFor(
+ DisplayItemType::TYPE_BACKGROUND_COLOR)),
+ "The property set of output should be the subset of transform-like "
+ "properties, opacity, or background_color.");
+
+ if (seenProperties.IsSubsetOf(LayerAnimationInfo::GetCSSPropertiesFor(
+ DisplayItemType::TYPE_TRANSFORM))) {
+ MOZ_ASSERT(storageData.mTransformData, "Should have TransformData");
+ }
+
+ if (seenProperties.HasProperty(eCSSProperty_offset_path)) {
+ MOZ_ASSERT(storageData.mTransformData, "Should have TransformData");
+ MOZ_ASSERT(storageData.mTransformData->motionPathData(),
+ "Should have MotionPathData");
+ }
+ }
+#endif
+
+ return storageData;
+}
+
+uint64_t AnimationHelper::GetNextCompositorAnimationsId() {
+ static uint32_t sNextId = 0;
+ ++sNextId;
+
+ uint32_t procId = static_cast<uint32_t>(base::GetCurrentProcId());
+ uint64_t nextId = procId;
+ nextId = nextId << 32 | sNextId;
+ return nextId;
+}
+
+gfx::Matrix4x4 AnimationHelper::ServoAnimationValueToMatrix4x4(
+ const nsTArray<RefPtr<StyleAnimationValue>>& aValues,
+ const TransformData& aTransformData, gfx::Path* aCachedMotionPath) {
+ using nsStyleTransformMatrix::TransformReferenceBox;
+
+ // This is a bit silly just to avoid the transform list copy from the
+ // animation transform list.
+ auto noneTranslate = StyleTranslate::None();
+ auto noneRotate = StyleRotate::None();
+ auto noneScale = StyleScale::None();
+ const StyleTransform noneTransform;
+
+ const StyleTranslate* translate = nullptr;
+ const StyleRotate* rotate = nullptr;
+ const StyleScale* scale = nullptr;
+ const StyleTransform* transform = nullptr;
+ const StyleOffsetPath* path = nullptr;
+ const StyleLengthPercentage* distance = nullptr;
+ const StyleOffsetRotate* offsetRotate = nullptr;
+ const StylePositionOrAuto* anchor = nullptr;
+
+ for (const auto& value : aValues) {
+ MOZ_ASSERT(value);
+ nsCSSPropertyID id = Servo_AnimationValue_GetPropertyId(value);
+ switch (id) {
+ case eCSSProperty_transform:
+ MOZ_ASSERT(!transform);
+ transform = Servo_AnimationValue_GetTransform(value);
+ break;
+ case eCSSProperty_translate:
+ MOZ_ASSERT(!translate);
+ translate = Servo_AnimationValue_GetTranslate(value);
+ break;
+ case eCSSProperty_rotate:
+ MOZ_ASSERT(!rotate);
+ rotate = Servo_AnimationValue_GetRotate(value);
+ break;
+ case eCSSProperty_scale:
+ MOZ_ASSERT(!scale);
+ scale = Servo_AnimationValue_GetScale(value);
+ break;
+ case eCSSProperty_offset_path:
+ MOZ_ASSERT(!path);
+ path = Servo_AnimationValue_GetOffsetPath(value);
+ break;
+ case eCSSProperty_offset_distance:
+ MOZ_ASSERT(!distance);
+ distance = Servo_AnimationValue_GetOffsetDistance(value);
+ break;
+ case eCSSProperty_offset_rotate:
+ MOZ_ASSERT(!offsetRotate);
+ offsetRotate = Servo_AnimationValue_GetOffsetRotate(value);
+ break;
+ case eCSSProperty_offset_anchor:
+ MOZ_ASSERT(!anchor);
+ anchor = Servo_AnimationValue_GetOffsetAnchor(value);
+ break;
+ default:
+ MOZ_ASSERT_UNREACHABLE("Unsupported transform-like property");
+ }
+ }
+
+ TransformReferenceBox refBox(nullptr, aTransformData.bounds());
+ Maybe<ResolvedMotionPathData> motion = MotionPathUtils::ResolveMotionPath(
+ path, distance, offsetRotate, anchor, aTransformData.motionPathData(),
+ refBox, aCachedMotionPath);
+
+ // We expect all our transform data to arrive in device pixels
+ gfx::Point3D transformOrigin = aTransformData.transformOrigin();
+ nsDisplayTransform::FrameTransformProperties props(
+ translate ? *translate : noneTranslate, rotate ? *rotate : noneRotate,
+ scale ? *scale : noneScale, transform ? *transform : noneTransform,
+ motion, transformOrigin);
+
+ return nsDisplayTransform::GetResultingTransformMatrix(
+ props, refBox, aTransformData.appUnitsPerDevPixel());
+}
+
+static uint8_t CollectOverflowedSideLines(const gfxQuad& aPrerenderedQuad,
+ SideBits aOverflowSides,
+ gfxLineSegment sideLines[4]) {
+ uint8_t count = 0;
+
+ if (aOverflowSides & SideBits::eTop) {
+ sideLines[count] = gfxLineSegment(aPrerenderedQuad.mPoints[0],
+ aPrerenderedQuad.mPoints[1]);
+ count++;
+ }
+ if (aOverflowSides & SideBits::eRight) {
+ sideLines[count] = gfxLineSegment(aPrerenderedQuad.mPoints[1],
+ aPrerenderedQuad.mPoints[2]);
+ count++;
+ }
+ if (aOverflowSides & SideBits::eBottom) {
+ sideLines[count] = gfxLineSegment(aPrerenderedQuad.mPoints[2],
+ aPrerenderedQuad.mPoints[3]);
+ count++;
+ }
+ if (aOverflowSides & SideBits::eLeft) {
+ sideLines[count] = gfxLineSegment(aPrerenderedQuad.mPoints[3],
+ aPrerenderedQuad.mPoints[0]);
+ count++;
+ }
+
+ return count;
+}
+
+enum RegionBits : uint8_t {
+ Inside = 0,
+ Left = (1 << 0),
+ Right = (1 << 1),
+ Bottom = (1 << 2),
+ Top = (1 << 3),
+};
+
+MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(RegionBits);
+
+static RegionBits GetRegionBitsForPoint(double aX, double aY,
+ const gfxRect& aClip) {
+ RegionBits result = RegionBits::Inside;
+ if (aX < aClip.X()) {
+ result |= RegionBits::Left;
+ } else if (aX > aClip.XMost()) {
+ result |= RegionBits::Right;
+ }
+
+ if (aY < aClip.Y()) {
+ result |= RegionBits::Bottom;
+ } else if (aY > aClip.YMost()) {
+ result |= RegionBits::Top;
+ }
+ return result;
+};
+
+// https://en.wikipedia.org/wiki/Cohen%E2%80%93Sutherland_algorithm
+static bool LineSegmentIntersectsClip(double aX0, double aY0, double aX1,
+ double aY1, const gfxRect& aClip) {
+ RegionBits b0 = GetRegionBitsForPoint(aX0, aY0, aClip);
+ RegionBits b1 = GetRegionBitsForPoint(aX1, aY1, aClip);
+
+ while (true) {
+ if (!(b0 | b1)) {
+ // Completely inside.
+ return true;
+ }
+
+ if (b0 & b1) {
+ // Completely outside.
+ return false;
+ }
+
+ double x, y;
+ // Choose an outside point.
+ RegionBits outsidePointBits = b1 > b0 ? b1 : b0;
+ if (outsidePointBits & RegionBits::Top) {
+ x = aX0 + (aX1 - aX0) * (aClip.YMost() - aY0) / (aY1 - aY0);
+ y = aClip.YMost();
+ } else if (outsidePointBits & RegionBits::Bottom) {
+ x = aX0 + (aX1 - aX0) * (aClip.Y() - aY0) / (aY1 - aY0);
+ y = aClip.Y();
+ } else if (outsidePointBits & RegionBits::Right) {
+ y = aY0 + (aY1 - aY0) * (aClip.XMost() - aX0) / (aX1 - aX0);
+ x = aClip.XMost();
+ } else if (outsidePointBits & RegionBits::Left) {
+ y = aY0 + (aY1 - aY0) * (aClip.X() - aX0) / (aX1 - aX0);
+ x = aClip.X();
+ }
+
+ if (outsidePointBits == b0) {
+ aX0 = x;
+ aY0 = y;
+ b0 = GetRegionBitsForPoint(aX0, aY0, aClip);
+ } else {
+ aX1 = x;
+ aY1 = y;
+ b1 = GetRegionBitsForPoint(aX1, aY1, aClip);
+ }
+ }
+ MOZ_ASSERT_UNREACHABLE();
+ return false;
+}
+
+// static
+bool AnimationHelper::ShouldBeJank(const LayoutDeviceRect& aPrerenderedRect,
+ SideBits aOverflowSides,
+ const gfx::Matrix4x4& aTransform,
+ const ParentLayerRect& aClipRect) {
+ if (aClipRect.IsEmpty()) {
+ return false;
+ }
+
+ gfxQuad prerenderedQuad = gfxUtils::TransformToQuad(
+ ThebesRect(aPrerenderedRect.ToUnknownRect()), aTransform);
+
+ gfxLineSegment sideLines[4];
+ uint8_t overflowSideCount =
+ CollectOverflowedSideLines(prerenderedQuad, aOverflowSides, sideLines);
+
+ gfxRect clipRect = ThebesRect(aClipRect.ToUnknownRect());
+ for (uint8_t j = 0; j < overflowSideCount; j++) {
+ if (LineSegmentIntersectsClip(sideLines[j].mStart.x, sideLines[j].mStart.y,
+ sideLines[j].mEnd.x, sideLines[j].mEnd.y,
+ clipRect)) {
+ return true;
+ }
+ }
+
+ // With step timing functions there are cases the transform jumps to a
+ // position where the partial pre-render area is totally outside of the clip
+ // rect without any intersection of the partial pre-render area and the clip
+ // rect happened in previous compositions but there remains visible area of
+ // the entire transformed area.
+ //
+ // So now all four points of the transformed partial pre-render rect are
+ // outside of the clip rect, if all these four points are in either side of
+ // the clip rect, we consider it's jank so that on the main-thread we will
+ // either a) rebuild the up-to-date display item if there remains visible area
+ // or b) no longer rebuild the display item if it's totally outside of the
+ // clip rect.
+ //
+ // Note that RegionBits::Left and Right are mutually exclusive,
+ // RegionBits::Top and Bottom are also mutually exclusive, so if there remains
+ // any bits, it means all four points are in the same side.
+ return GetRegionBitsForPoint(prerenderedQuad.mPoints[0].x,
+ prerenderedQuad.mPoints[0].y, clipRect) &
+ GetRegionBitsForPoint(prerenderedQuad.mPoints[1].x,
+ prerenderedQuad.mPoints[1].y, clipRect) &
+ GetRegionBitsForPoint(prerenderedQuad.mPoints[2].x,
+ prerenderedQuad.mPoints[2].y, clipRect) &
+ GetRegionBitsForPoint(prerenderedQuad.mPoints[3].x,
+ prerenderedQuad.mPoints[3].y, clipRect);
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/AnimationHelper.h b/gfx/layers/AnimationHelper.h
new file mode 100644
index 0000000000..7afe9a7056
--- /dev/null
+++ b/gfx/layers/AnimationHelper.h
@@ -0,0 +1,180 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_AnimationHelper_h
+#define mozilla_layers_AnimationHelper_h
+
+#include "mozilla/dom/Nullable.h"
+#include "mozilla/layers/AnimationStorageData.h"
+#include "mozilla/layers/LayersMessages.h" // for TransformData, etc
+#include "mozilla/webrender/WebRenderTypes.h" // for RenderRoot
+#include "mozilla/TimeStamp.h" // for TimeStamp
+#include "mozilla/TimingParams.h"
+#include "mozilla/Types.h" // for SideBits
+#include "X11UndefineNone.h"
+#include <unordered_map>
+
+namespace mozilla::layers {
+class Animation;
+class APZSampler;
+class CompositorAnimationStorage;
+struct AnimatedValue;
+
+typedef nsTArray<layers::Animation> AnimationArray;
+
+/**
+ * This utility class allows reusing code between the webrender and
+ * non-webrender compositor-side implementations. It provides
+ * utility functions for sampling animations at particular timestamps.
+ */
+class AnimationHelper {
+ public:
+ struct SampleResult {
+ enum class Type : uint8_t { None, Skipped, Sampled };
+ enum class Reason : uint8_t { None, ScrollToDelayPhase };
+ Type mType = Type::None;
+ Reason mReason = Reason::None;
+
+ SampleResult() = default;
+ SampleResult(Type aType, Reason aReason) : mType(aType), mReason(aReason) {}
+
+ static SampleResult Skipped() { return {Type::Skipped, Reason::None}; }
+ static SampleResult Sampled() { return {Type::Sampled, Reason::None}; }
+
+ bool IsNone() { return mType == Type::None; }
+ bool IsSkipped() { return mType == Type::Skipped; }
+ bool IsSampled() { return mType == Type::Sampled; }
+ };
+
+ /**
+ * Sample animations based on a given time stamp for a element(layer) with
+ * its animation data.
+ * Generally |aPreviousFrameTime| is used for the sampling if it's
+ * supplied to make the animation more in sync with other animations on the
+ * main-thread. But in the case where the animation just started at the time
+ * when the animation was sent to the compositor, |aCurrentFrameTime| is used
+ * for sampling instead to avoid flicker.
+ *
+ * Returns SampleResult::None if none of the animations are producing a result
+ * (e.g. they are in the delay phase with no backwards fill),
+ * SampleResult::Skipped if the animation output did not change since the last
+ * call of this function,
+ * SampleResult::Sampled if the animation output was updated.
+ *
+ * The only exception is the scroll-driven animation. When the user move the
+ * scrollbar to make the animations go from active phase to delay phase, this
+ * returns SampleResult::None but sets its |mReason| to
+ * Reason::ScrollToDelayPhase. The caller can check this flag so we can store
+ * the base style into the hash table to make sure we have the correct
+ * rendering result. The base style stays in the hash table until we sync with
+ * main thread.
+ *
+ * Using the same example from ExtractAnimations (below):
+ *
+ * Input |aPropertyAnimationGroups| (ignoring the base animation style):
+ *
+ * [
+ * Group A: [ { rotate, Animation A }, { rotate, Animation B } ],
+ * Group B: [ { scale, Animation B } ],
+ * Group C: [ { transform, Animation A }, { transform, Animation B } ],
+ * ]
+ *
+ * For each property group, this function interpolates each animation in turn,
+ * using the result of interpolating one animation as input for the next such
+ * that it reduces each property group to a single output value:
+ *
+ * [
+ * { rotate, StyleAnimationValue },
+ * { scale, StyleAnimationValue },
+ * { transform, StyleAnimationValue },
+ * ]
+ *
+ * For transform animations, the caller (SampleAnimations) will combine the
+ * result of the various transform properties into a final matrix.
+ */
+ static SampleResult SampleAnimationForEachNode(
+ const APZSampler* aAPZSampler, const LayersId& aLayersId,
+ const MutexAutoLock& aProofOfMapLock, TimeStamp aPreviousFrameTime,
+ TimeStamp aCurrentFrameTime, const AnimatedValue* aPreviousValue,
+ nsTArray<PropertyAnimationGroup>& aPropertyAnimationGroups,
+ nsTArray<RefPtr<StyleAnimationValue>>& aAnimationValues);
+
+ /**
+ * Extract organized animation data by property into an array of
+ * PropertyAnimationGroup objects.
+ *
+ * For example, suppose we have the following animations:
+ *
+ * Animation A: [ transform, rotate ]
+ * Animation B: [ rotate, scale ]
+ * Animation C: [ transform ]
+ * Animation D: [ opacity ]
+ *
+ * When we go to send transform-like properties to the compositor, we
+ * sort them as follows:
+ *
+ * [
+ * { rotate: Animation A (rotate segments only) },
+ * { rotate: Animation B ( " " ) },
+ * { scale: Animation B (scale segments only) },
+ * { transform: Animation A (transform segments only) },
+ * { transform: Animation C ( " " ) },
+ * ]
+ *
+ * In this function, we group these animations together by property producing
+ * output such as the following:
+ *
+ * [
+ * [ { rotate, Animation A }, { rotate, Animation B } ],
+ * [ { scale, Animation B } ],
+ * [ { transform, Animation A }, { transform, Animation B } ],
+ * ]
+ *
+ * In the process of grouping these animations, we also convert their values
+ * from the rather compact representation we use for transferring across the
+ * IPC boundary into something we can readily use for sampling.
+ *
+ * Note: the animation groups:
+ * 1. transform-like properties: transfrom, translate, rotate, scale,
+ * offset-*.
+ * 2. opacity property: opacity.
+ * 3. background color property: background-color.
+ */
+ static AnimationStorageData ExtractAnimations(
+ const LayersId& aLayersId, const AnimationArray& aAnimations);
+
+ /**
+ * Get a unique id to represent the compositor animation between child
+ * and parent side. This id will be used as a key to store animation
+ * data in the CompositorAnimationStorage per compositor.
+ * Each layer on the content side calls this when it gets new animation
+ * data.
+ */
+ static uint64_t GetNextCompositorAnimationsId();
+
+ /**
+ * Convert an array of animation values into a matrix given the corresponding
+ * transform parameters. |aValue| must be a transform-like value
+ * (e.g. transform, translate etc.).
+ */
+ static gfx::Matrix4x4 ServoAnimationValueToMatrix4x4(
+ const nsTArray<RefPtr<StyleAnimationValue>>& aValue,
+ const TransformData& aTransformData, gfx::Path* aCachedMotionPath);
+
+ /**
+ * Returns true if |aPrerenderedRect| transformed by |aTransform| were
+ * composited in |aClipRect| there appears area which wasn't pre-rendered
+ * on the main-thread. I.e. checkerboarding.
+ */
+ static bool ShouldBeJank(const LayoutDeviceRect& aPrerenderedRect,
+ SideBits aOverflowedSides,
+ const gfx::Matrix4x4& aTransform,
+ const ParentLayerRect& aClipRect);
+};
+
+} // namespace mozilla::layers
+
+#endif // mozilla_layers_AnimationHelper_h
diff --git a/gfx/layers/AnimationInfo.cpp b/gfx/layers/AnimationInfo.cpp
new file mode 100644
index 0000000000..be3d47b378
--- /dev/null
+++ b/gfx/layers/AnimationInfo.cpp
@@ -0,0 +1,992 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "AnimationInfo.h"
+#include "mozilla/LayerAnimationInfo.h"
+#include "mozilla/layers/WebRenderLayerManager.h"
+#include "mozilla/layers/AnimationHelper.h"
+#include "mozilla/layers/CompositorThread.h"
+#include "mozilla/dom/Animation.h"
+#include "mozilla/dom/CSSTransition.h"
+#include "mozilla/dom/KeyframeEffect.h"
+#include "mozilla/EffectSet.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/StaticPrefs_layout.h"
+#include "nsIContent.h"
+#include "nsLayoutUtils.h"
+#include "nsPresContextInlines.h"
+#include "nsStyleTransformMatrix.h"
+#include "PuppetWidget.h"
+
+namespace mozilla {
+namespace layers {
+
+using TransformReferenceBox = nsStyleTransformMatrix::TransformReferenceBox;
+
+AnimationInfo::AnimationInfo() : mCompositorAnimationsId(0), mMutated(false) {}
+
+AnimationInfo::~AnimationInfo() = default;
+
+void AnimationInfo::EnsureAnimationsId() {
+ if (!mCompositorAnimationsId) {
+ mCompositorAnimationsId = AnimationHelper::GetNextCompositorAnimationsId();
+ }
+}
+
+Animation* AnimationInfo::AddAnimation() {
+ MOZ_ASSERT(!CompositorThreadHolder::IsInCompositorThread());
+ // Here generates a new id when the first animation is added and
+ // this id is used to represent the animations in this layer.
+ EnsureAnimationsId();
+
+ MOZ_ASSERT(!mPendingAnimations, "should have called ClearAnimations first");
+
+ Animation* anim = mAnimations.AppendElement();
+
+ mMutated = true;
+
+ return anim;
+}
+
+Animation* AnimationInfo::AddAnimationForNextTransaction() {
+ MOZ_ASSERT(!CompositorThreadHolder::IsInCompositorThread());
+ MOZ_ASSERT(mPendingAnimations,
+ "should have called ClearAnimationsForNextTransaction first");
+
+ Animation* anim = mPendingAnimations->AppendElement();
+
+ return anim;
+}
+
+void AnimationInfo::ClearAnimations() {
+ mPendingAnimations = nullptr;
+
+ if (mAnimations.IsEmpty() && mStorageData.IsEmpty()) {
+ return;
+ }
+
+ mAnimations.Clear();
+ mStorageData.Clear();
+
+ mMutated = true;
+}
+
+void AnimationInfo::ClearAnimationsForNextTransaction() {
+ // Ensure we have a non-null mPendingAnimations to mark a future clear.
+ if (!mPendingAnimations) {
+ mPendingAnimations = MakeUnique<AnimationArray>();
+ }
+
+ mPendingAnimations->Clear();
+}
+
+bool AnimationInfo::StartPendingAnimations(const TimeStamp& aReadyTime) {
+ bool updated = false;
+ for (size_t animIdx = 0, animEnd = mAnimations.Length(); animIdx < animEnd;
+ animIdx++) {
+ Animation& anim = mAnimations[animIdx];
+
+ // If the animation is doing an async update of its playback rate, then we
+ // want to match whatever its current time would be at *aReadyTime*.
+ if (!std::isnan(anim.previousPlaybackRate()) && anim.startTime().isSome() &&
+ !anim.originTime().IsNull() && !anim.isNotPlaying()) {
+ TimeDuration readyTime = aReadyTime - anim.originTime();
+ anim.holdTime() = dom::Animation::CurrentTimeFromTimelineTime(
+ readyTime, anim.startTime().ref(), anim.previousPlaybackRate());
+ // Make start time null so that we know to update it below.
+ anim.startTime() = Nothing();
+ }
+
+ // If the animation is play-pending, resolve the start time.
+ if (anim.startTime().isNothing() && !anim.originTime().IsNull() &&
+ !anim.isNotPlaying()) {
+ TimeDuration readyTime = aReadyTime - anim.originTime();
+ anim.startTime() = Some(dom::Animation::StartTimeFromTimelineTime(
+ readyTime, anim.holdTime(), anim.playbackRate()));
+ updated = true;
+ }
+ }
+ return updated;
+}
+
+bool AnimationInfo::ApplyPendingUpdatesForThisTransaction() {
+ if (mPendingAnimations) {
+ mAnimations = std::move(*mPendingAnimations);
+ mPendingAnimations = nullptr;
+ return true;
+ }
+
+ return false;
+}
+
+bool AnimationInfo::HasTransformAnimation() const {
+ const nsCSSPropertyIDSet& transformSet =
+ LayerAnimationInfo::GetCSSPropertiesFor(DisplayItemType::TYPE_TRANSFORM);
+ for (uint32_t i = 0; i < mAnimations.Length(); i++) {
+ if (transformSet.HasProperty(mAnimations[i].property())) {
+ return true;
+ }
+ }
+ return false;
+}
+
+/* static */
+Maybe<uint64_t> AnimationInfo::GetGenerationFromFrame(
+ nsIFrame* aFrame, DisplayItemType aDisplayItemKey) {
+ MOZ_ASSERT(aFrame->IsPrimaryFrame() ||
+ nsLayoutUtils::IsFirstContinuationOrIBSplitSibling(aFrame));
+
+ // In case of continuation, KeyframeEffectReadOnly uses its first frame,
+ // whereas nsDisplayItem uses its last continuation, so we have to use the
+ // last continuation frame here.
+ if (nsLayoutUtils::IsFirstContinuationOrIBSplitSibling(aFrame)) {
+ aFrame = nsLayoutUtils::LastContinuationOrIBSplitSibling(aFrame);
+ }
+ RefPtr<WebRenderAnimationData> animationData =
+ GetWebRenderUserData<WebRenderAnimationData>(aFrame,
+ (uint32_t)aDisplayItemKey);
+ if (animationData) {
+ return animationData->GetAnimationInfo().GetAnimationGeneration();
+ }
+
+ return Nothing();
+}
+
+/* static */
+void AnimationInfo::EnumerateGenerationOnFrame(
+ const nsIFrame* aFrame, const nsIContent* aContent,
+ const CompositorAnimatableDisplayItemTypes& aDisplayItemTypes,
+ AnimationGenerationCallback aCallback) {
+ nsIWidget* widget = nsContentUtils::WidgetForContent(aContent);
+ if (!widget) {
+ return;
+ }
+ // If we haven't created a window renderer there's no animation generation
+ // that we can have, thus we call the callback function with |Nothing()| for
+ // the generation.
+ if (!widget->HasWindowRenderer()) {
+ for (auto displayItem : LayerAnimationInfo::sDisplayItemTypes) {
+ aCallback(Nothing(), displayItem);
+ }
+ return;
+ }
+ WindowRenderer* renderer = widget->GetWindowRenderer();
+ MOZ_ASSERT(renderer);
+ if (!renderer->AsWebRender()) {
+ return;
+ }
+
+ // In case of continuation, nsDisplayItem uses its last continuation, so we
+ // have to use the last continuation frame here.
+ if (nsLayoutUtils::IsFirstContinuationOrIBSplitSibling(aFrame)) {
+ aFrame = nsLayoutUtils::LastContinuationOrIBSplitSibling(aFrame);
+ }
+
+ for (auto displayItem : LayerAnimationInfo::sDisplayItemTypes) {
+ // For transform animations, the animation is on the primary frame but
+ // |aFrame| is the style frame.
+ const nsIFrame* frameToQuery =
+ displayItem == DisplayItemType::TYPE_TRANSFORM
+ ? nsLayoutUtils::GetPrimaryFrameFromStyleFrame(aFrame)
+ : aFrame;
+ RefPtr<WebRenderAnimationData> animationData =
+ GetWebRenderUserData<WebRenderAnimationData>(frameToQuery,
+ (uint32_t)displayItem);
+ Maybe<uint64_t> generation;
+ if (animationData) {
+ generation = animationData->GetAnimationInfo().GetAnimationGeneration();
+ }
+ aCallback(generation, displayItem);
+ }
+}
+
+static StyleTransformOperation ResolveTranslate(
+ TransformReferenceBox& aRefBox, const LengthPercentage& aX,
+ const LengthPercentage& aY = LengthPercentage::Zero(),
+ const Length& aZ = Length{0}) {
+ float x = nsStyleTransformMatrix::ProcessTranslatePart(
+ aX, &aRefBox, &TransformReferenceBox::Width);
+ float y = nsStyleTransformMatrix::ProcessTranslatePart(
+ aY, &aRefBox, &TransformReferenceBox::Height);
+ return StyleTransformOperation::Translate3D(
+ LengthPercentage::FromPixels(x), LengthPercentage::FromPixels(y), aZ);
+}
+
+static StyleTranslate ResolveTranslate(const StyleTranslate& aValue,
+ TransformReferenceBox& aRefBox) {
+ if (aValue.IsTranslate()) {
+ const auto& t = aValue.AsTranslate();
+ float x = nsStyleTransformMatrix::ProcessTranslatePart(
+ t._0, &aRefBox, &TransformReferenceBox::Width);
+ float y = nsStyleTransformMatrix::ProcessTranslatePart(
+ t._1, &aRefBox, &TransformReferenceBox::Height);
+ return StyleTranslate::Translate(LengthPercentage::FromPixels(x),
+ LengthPercentage::FromPixels(y), t._2);
+ }
+
+ MOZ_ASSERT(aValue.IsNone());
+ return StyleTranslate::None();
+}
+
+static StyleTransform ResolveTransformOperations(
+ const StyleTransform& aTransform, TransformReferenceBox& aRefBox) {
+ auto convertMatrix = [](const gfx::Matrix4x4& aM) {
+ return StyleTransformOperation::Matrix3D(StyleGenericMatrix3D<StyleNumber>{
+ aM._11, aM._12, aM._13, aM._14, aM._21, aM._22, aM._23, aM._24, aM._31,
+ aM._32, aM._33, aM._34, aM._41, aM._42, aM._43, aM._44});
+ };
+
+ Vector<StyleTransformOperation> result;
+ MOZ_RELEASE_ASSERT(
+ result.initCapacity(aTransform.Operations().Length()),
+ "Allocating vector of transform operations should be successful.");
+
+ for (const StyleTransformOperation& op : aTransform.Operations()) {
+ switch (op.tag) {
+ case StyleTransformOperation::Tag::TranslateX:
+ result.infallibleAppend(ResolveTranslate(aRefBox, op.AsTranslateX()));
+ break;
+ case StyleTransformOperation::Tag::TranslateY:
+ result.infallibleAppend(ResolveTranslate(
+ aRefBox, LengthPercentage::Zero(), op.AsTranslateY()));
+ break;
+ case StyleTransformOperation::Tag::TranslateZ:
+ result.infallibleAppend(
+ ResolveTranslate(aRefBox, LengthPercentage::Zero(),
+ LengthPercentage::Zero(), op.AsTranslateZ()));
+ break;
+ case StyleTransformOperation::Tag::Translate: {
+ const auto& translate = op.AsTranslate();
+ result.infallibleAppend(
+ ResolveTranslate(aRefBox, translate._0, translate._1));
+ break;
+ }
+ case StyleTransformOperation::Tag::Translate3D: {
+ const auto& translate = op.AsTranslate3D();
+ result.infallibleAppend(ResolveTranslate(aRefBox, translate._0,
+ translate._1, translate._2));
+ break;
+ }
+ case StyleTransformOperation::Tag::InterpolateMatrix: {
+ gfx::Matrix4x4 matrix;
+ nsStyleTransformMatrix::ProcessInterpolateMatrix(matrix, op, aRefBox);
+ result.infallibleAppend(convertMatrix(matrix));
+ break;
+ }
+ case StyleTransformOperation::Tag::AccumulateMatrix: {
+ gfx::Matrix4x4 matrix;
+ nsStyleTransformMatrix::ProcessAccumulateMatrix(matrix, op, aRefBox);
+ result.infallibleAppend(convertMatrix(matrix));
+ break;
+ }
+ case StyleTransformOperation::Tag::RotateX:
+ case StyleTransformOperation::Tag::RotateY:
+ case StyleTransformOperation::Tag::RotateZ:
+ case StyleTransformOperation::Tag::Rotate:
+ case StyleTransformOperation::Tag::Rotate3D:
+ case StyleTransformOperation::Tag::ScaleX:
+ case StyleTransformOperation::Tag::ScaleY:
+ case StyleTransformOperation::Tag::ScaleZ:
+ case StyleTransformOperation::Tag::Scale:
+ case StyleTransformOperation::Tag::Scale3D:
+ case StyleTransformOperation::Tag::SkewX:
+ case StyleTransformOperation::Tag::SkewY:
+ case StyleTransformOperation::Tag::Skew:
+ case StyleTransformOperation::Tag::Matrix:
+ case StyleTransformOperation::Tag::Matrix3D:
+ case StyleTransformOperation::Tag::Perspective:
+ result.infallibleAppend(op);
+ break;
+ default:
+ MOZ_ASSERT_UNREACHABLE("Function not handled yet!");
+ }
+ }
+
+ auto transform = StyleTransform{
+ StyleOwnedSlice<StyleTransformOperation>(std::move(result))};
+ MOZ_ASSERT(!transform.HasPercent());
+ MOZ_ASSERT(transform.Operations().Length() ==
+ aTransform.Operations().Length());
+ return transform;
+}
+
+static Maybe<ScrollTimelineOptions> GetScrollTimelineOptions(
+ dom::AnimationTimeline* aTimeline) {
+ if (!aTimeline || !aTimeline->IsScrollTimeline()) {
+ return Nothing();
+ }
+
+ const dom::ScrollTimeline* timeline = aTimeline->AsScrollTimeline();
+ MOZ_ASSERT(timeline->IsActive(),
+ "We send scroll animation to the compositor only if its timeline "
+ "is active");
+
+ ScrollableLayerGuid::ViewID source = ScrollableLayerGuid::NULL_SCROLL_ID;
+ DebugOnly<bool> success =
+ nsLayoutUtils::FindIDFor(timeline->SourceElement(), &source);
+ MOZ_ASSERT(success, "We should have a valid ViewID for the scroller");
+
+ return Some(ScrollTimelineOptions(source, timeline->Axis()));
+}
+
+// FIXME: Bug 1489392: We don't have to normalize the path here if we accept
+// the spec issue which would like to normalize svg paths at computed time.
+static StyleOffsetPath NormalizeOffsetPath(const StyleOffsetPath& aOffsetPath) {
+ if (aOffsetPath.IsPath()) {
+ return StyleOffsetPath::Path(
+ MotionPathUtils::NormalizeSVGPathData(aOffsetPath.AsPath()));
+ }
+ return StyleOffsetPath(aOffsetPath);
+}
+
+static void SetAnimatable(nsCSSPropertyID aProperty,
+ const AnimationValue& aAnimationValue,
+ nsIFrame* aFrame, TransformReferenceBox& aRefBox,
+ layers::Animatable& aAnimatable) {
+ MOZ_ASSERT(aFrame);
+
+ if (aAnimationValue.IsNull()) {
+ aAnimatable = null_t();
+ return;
+ }
+
+ switch (aProperty) {
+ case eCSSProperty_background_color: {
+ // We don't support color animation on the compositor yet so that we can
+ // resolve currentColor at this moment.
+ nscolor foreground =
+ aFrame->Style()->GetVisitedDependentColor(&nsStyleText::mColor);
+ aAnimatable = aAnimationValue.GetColor(foreground);
+ break;
+ }
+ case eCSSProperty_opacity:
+ aAnimatable = aAnimationValue.GetOpacity();
+ break;
+ case eCSSProperty_rotate:
+ aAnimatable = aAnimationValue.GetRotateProperty();
+ break;
+ case eCSSProperty_scale:
+ aAnimatable = aAnimationValue.GetScaleProperty();
+ break;
+ case eCSSProperty_translate:
+ aAnimatable =
+ ResolveTranslate(aAnimationValue.GetTranslateProperty(), aRefBox);
+ break;
+ case eCSSProperty_transform:
+ aAnimatable = ResolveTransformOperations(
+ aAnimationValue.GetTransformProperty(), aRefBox);
+ break;
+ case eCSSProperty_offset_path:
+ aAnimatable =
+ NormalizeOffsetPath(aAnimationValue.GetOffsetPathProperty());
+ break;
+ case eCSSProperty_offset_distance:
+ aAnimatable = aAnimationValue.GetOffsetDistanceProperty();
+ break;
+ case eCSSProperty_offset_rotate:
+ aAnimatable = aAnimationValue.GetOffsetRotateProperty();
+ break;
+ case eCSSProperty_offset_anchor:
+ aAnimatable = aAnimationValue.GetOffsetAnchorProperty();
+ break;
+ default:
+ MOZ_ASSERT_UNREACHABLE("Unsupported property");
+ }
+}
+
+void AnimationInfo::AddAnimationForProperty(
+ nsIFrame* aFrame, const AnimationProperty& aProperty,
+ dom::Animation* aAnimation, const Maybe<TransformData>& aTransformData,
+ Send aSendFlag) {
+ MOZ_ASSERT(aAnimation->GetEffect(),
+ "Should not be adding an animation without an effect");
+ MOZ_ASSERT(!aAnimation->GetCurrentOrPendingStartTime().IsNull() ||
+ !aAnimation->IsPlaying() ||
+ (aAnimation->GetTimeline() &&
+ aAnimation->GetTimeline()->TracksWallclockTime()),
+ "If the animation has an unresolved start time it should either"
+ " be static (so we don't need a start time) or else have a"
+ " timeline capable of converting TimeStamps (so we can calculate"
+ " one later");
+
+ layers::Animation* animation = (aSendFlag == Send::NextTransaction)
+ ? AddAnimationForNextTransaction()
+ : AddAnimation();
+
+ const TimingParams& timing = aAnimation->GetEffect()->NormalizedTiming();
+
+ // If we are starting a new transition that replaces an existing transition
+ // running on the compositor, it is possible that the animation on the
+ // compositor will have advanced ahead of the main thread. If we use as
+ // the starting point of the new transition, the current value of the
+ // replaced transition as calculated on the main thread using the refresh
+ // driver time, the new transition will jump when it starts. Instead, we
+ // re-calculate the starting point of the new transition by applying the
+ // current TimeStamp to the parameters of the replaced transition.
+ //
+ // We need to do this here, rather than when we generate the new transition,
+ // since after generating the new transition other requestAnimationFrame
+ // callbacks may run that introduce further lag between the main thread and
+ // the compositor.
+ dom::CSSTransition* cssTransition = aAnimation->AsCSSTransition();
+ if (cssTransition) {
+ cssTransition->UpdateStartValueFromReplacedTransition();
+ }
+
+ animation->originTime() =
+ !aAnimation->GetTimeline()
+ ? TimeStamp()
+ : aAnimation->GetTimeline()->ToTimeStamp(TimeDuration());
+
+ dom::Nullable<TimeDuration> startTime =
+ aAnimation->GetCurrentOrPendingStartTime();
+ if (startTime.IsNull()) {
+ animation->startTime() = Nothing();
+ } else {
+ animation->startTime() = Some(startTime.Value());
+ }
+
+ animation->holdTime() = aAnimation->GetCurrentTimeAsDuration().Value();
+
+ const ComputedTiming computedTiming =
+ aAnimation->GetEffect()->GetComputedTiming();
+ animation->delay() = timing.Delay();
+ animation->endDelay() = timing.EndDelay();
+ animation->duration() = computedTiming.mDuration;
+ animation->iterations() = static_cast<float>(computedTiming.mIterations);
+ animation->iterationStart() =
+ static_cast<float>(computedTiming.mIterationStart);
+ animation->direction() = static_cast<uint8_t>(timing.Direction());
+ animation->fillMode() = static_cast<uint8_t>(computedTiming.mFill);
+ animation->property() = aProperty.mProperty;
+ animation->playbackRate() =
+ static_cast<float>(aAnimation->CurrentOrPendingPlaybackRate());
+ animation->previousPlaybackRate() =
+ aAnimation->HasPendingPlaybackRate()
+ ? static_cast<float>(aAnimation->PlaybackRate())
+ : std::numeric_limits<float>::quiet_NaN();
+ animation->transformData() = aTransformData;
+ animation->easingFunction() = timing.TimingFunction();
+ animation->iterationComposite() = static_cast<uint8_t>(
+ aAnimation->GetEffect()->AsKeyframeEffect()->IterationComposite());
+ animation->isNotPlaying() = !aAnimation->IsPlaying();
+ animation->isNotAnimating() = false;
+ animation->scrollTimelineOptions() =
+ GetScrollTimelineOptions(aAnimation->GetTimeline());
+
+ TransformReferenceBox refBox(aFrame);
+
+ // If the animation is additive or accumulates, we need to pass its base value
+ // to the compositor.
+
+ AnimationValue baseStyle =
+ aAnimation->GetEffect()->AsKeyframeEffect()->BaseStyle(
+ aProperty.mProperty);
+ if (!baseStyle.IsNull()) {
+ SetAnimatable(aProperty.mProperty, baseStyle, aFrame, refBox,
+ animation->baseStyle());
+ } else {
+ animation->baseStyle() = null_t();
+ }
+
+ for (uint32_t segIdx = 0; segIdx < aProperty.mSegments.Length(); segIdx++) {
+ const AnimationPropertySegment& segment = aProperty.mSegments[segIdx];
+
+ AnimationSegment* animSegment = animation->segments().AppendElement();
+ SetAnimatable(aProperty.mProperty, segment.mFromValue, aFrame, refBox,
+ animSegment->startState());
+ SetAnimatable(aProperty.mProperty, segment.mToValue, aFrame, refBox,
+ animSegment->endState());
+
+ animSegment->startPortion() = segment.mFromKey;
+ animSegment->endPortion() = segment.mToKey;
+ animSegment->startComposite() =
+ static_cast<uint8_t>(segment.mFromComposite);
+ animSegment->endComposite() = static_cast<uint8_t>(segment.mToComposite);
+ animSegment->sampleFn() = segment.mTimingFunction;
+ }
+}
+
+// Let's use an example to explain this function:
+//
+// We have 4 playing animations (without any !important rule or transition):
+// Animation A: [ transform, rotate ].
+// Animation B: [ rotate, scale ].
+// Animation C: [ transform, margin-left ].
+// Animation D: [ opacity, margin-left ].
+//
+// Normally, GetAnimationsForCompositor(|transform-like properties|) returns:
+// [ Animation A, Animation B, Animation C ], which is the first argument of
+// this function.
+//
+// In this function, we want to re-organize the list as (Note: don't care
+// the order of properties):
+// [
+// { rotate: [ Animation A, Animation B ] },
+// { scale: [ Animation B ] },
+// { transform: [ Animation A, Animation C ] },
+// ]
+//
+// Therefore, AddAnimationsForProperty() will append each animation property
+// into AnimationInfo, as a final list of layers::Animation:
+// [
+// { rotate: Animation A },
+// { rotate: Animation B },
+// { scale: Animation B },
+// { transform: Animation A },
+// { transform: Animation C },
+// ]
+//
+// And then, for each transaction, we send this list to the compositor thread.
+static HashMap<nsCSSPropertyID, nsTArray<RefPtr<dom::Animation>>>
+GroupAnimationsByProperty(const nsTArray<RefPtr<dom::Animation>>& aAnimations,
+ const nsCSSPropertyIDSet& aPropertySet) {
+ HashMap<nsCSSPropertyID, nsTArray<RefPtr<dom::Animation>>> groupedAnims;
+ for (const RefPtr<dom::Animation>& anim : aAnimations) {
+ const dom::KeyframeEffect* effect = anim->GetEffect()->AsKeyframeEffect();
+ MOZ_ASSERT(effect);
+ for (const AnimationProperty& property : effect->Properties()) {
+ if (!aPropertySet.HasProperty(property.mProperty)) {
+ continue;
+ }
+
+ auto animsForPropertyPtr = groupedAnims.lookupForAdd(property.mProperty);
+ if (!animsForPropertyPtr) {
+ DebugOnly<bool> rv =
+ groupedAnims.add(animsForPropertyPtr, property.mProperty,
+ nsTArray<RefPtr<dom::Animation>>());
+ MOZ_ASSERT(rv, "Should have enough memory");
+ }
+ animsForPropertyPtr->value().AppendElement(anim);
+ }
+ }
+ return groupedAnims;
+}
+
+bool AnimationInfo::AddAnimationsForProperty(
+ nsIFrame* aFrame, const EffectSet* aEffects,
+ const nsTArray<RefPtr<dom::Animation>>& aCompositorAnimations,
+ const Maybe<TransformData>& aTransformData, nsCSSPropertyID aProperty,
+ Send aSendFlag, WebRenderLayerManager* aLayerManager) {
+ bool addedAny = false;
+ // Add from first to last (since last overrides)
+ for (dom::Animation* anim : aCompositorAnimations) {
+ if (!anim->IsRelevant()) {
+ continue;
+ }
+
+ dom::KeyframeEffect* keyframeEffect =
+ anim->GetEffect() ? anim->GetEffect()->AsKeyframeEffect() : nullptr;
+ MOZ_ASSERT(keyframeEffect,
+ "A playing animation should have a keyframe effect");
+ const AnimationProperty* property =
+ keyframeEffect->GetEffectiveAnimationOfProperty(aProperty, *aEffects);
+ if (!property) {
+ continue;
+ }
+
+ // Note that if the property is overridden by !important rules,
+ // GetEffectiveAnimationOfProperty returns null instead.
+ // This is what we want, since if we have animations overridden by
+ // !important rules, we don't want to send them to the compositor.
+ MOZ_ASSERT(
+ anim->CascadeLevel() != EffectCompositor::CascadeLevel::Animations ||
+ !aEffects->PropertiesWithImportantRules().HasProperty(aProperty),
+ "GetEffectiveAnimationOfProperty already tested the property "
+ "is not overridden by !important rules");
+
+ // Don't add animations that are pending if their timeline does not
+ // track wallclock time. This is because any pending animations on layers
+ // will have their start time updated with the current wallclock time.
+ // If we can't convert that wallclock time back to an equivalent timeline
+ // time, we won't be able to update the content animation and it will end
+ // up being out of sync with the layer animation.
+ //
+ // Currently this only happens when the timeline is driven by a refresh
+ // driver under test control. In this case, the next time the refresh
+ // driver is advanced it will trigger any pending animations.
+ if (anim->Pending() &&
+ (anim->GetTimeline() && !anim->GetTimeline()->TracksWallclockTime())) {
+ continue;
+ }
+
+ AddAnimationForProperty(aFrame, *property, anim, aTransformData, aSendFlag);
+ keyframeEffect->SetIsRunningOnCompositor(aProperty, true);
+ addedAny = true;
+ if (aTransformData && aTransformData->partialPrerenderData() &&
+ aLayerManager) {
+ aLayerManager->AddPartialPrerenderedAnimation(GetCompositorAnimationsId(),
+ anim);
+ }
+ }
+ return addedAny;
+}
+
+// Returns which pre-rendered area's sides are overflowed from the pre-rendered
+// rect.
+//
+// We don't need to make jank animations when we are going to composite the
+// area where there is no overflowed area even if it's outside of the
+// pre-rendered area.
+static SideBits GetOverflowedSides(const nsRect& aOverflow,
+ const nsRect& aPartialPrerenderArea) {
+ SideBits sides = SideBits::eNone;
+ if (aOverflow.X() < aPartialPrerenderArea.X()) {
+ sides |= SideBits::eLeft;
+ }
+ if (aOverflow.Y() < aPartialPrerenderArea.Y()) {
+ sides |= SideBits::eTop;
+ }
+ if (aOverflow.XMost() > aPartialPrerenderArea.XMost()) {
+ sides |= SideBits::eRight;
+ }
+ if (aOverflow.YMost() > aPartialPrerenderArea.YMost()) {
+ sides |= SideBits::eBottom;
+ }
+ return sides;
+}
+
+static std::pair<ParentLayerRect, gfx::Matrix4x4>
+GetClipRectAndTransformForPartialPrerender(
+ const nsIFrame* aFrame, int32_t aDevPixelsToAppUnits,
+ const nsIFrame* aClipFrame, const nsIScrollableFrame* aScrollFrame) {
+ MOZ_ASSERT(aClipFrame);
+
+ gfx::Matrix4x4 transformInClip =
+ nsLayoutUtils::GetTransformToAncestor(RelativeTo{aFrame->GetParent()},
+ RelativeTo{aClipFrame})
+ .GetMatrix();
+ if (aScrollFrame) {
+ transformInClip.PostTranslate(
+ LayoutDevicePoint::FromAppUnits(aScrollFrame->GetScrollPosition(),
+ aDevPixelsToAppUnits)
+ .ToUnknownPoint());
+ }
+
+ // We don't necessarily use nsLayoutUtils::CalculateCompositionSizeForFrame
+ // since this is a case where we don't use APZ at all.
+ return std::make_pair(
+ LayoutDeviceRect::FromAppUnits(aScrollFrame
+ ? aScrollFrame->GetScrollPortRect()
+ : aClipFrame->GetRectRelativeToSelf(),
+ aDevPixelsToAppUnits) *
+ LayoutDeviceToLayerScale2D() * LayerToParentLayerScale(),
+ transformInClip);
+}
+
+static PartialPrerenderData GetPartialPrerenderData(
+ const nsIFrame* aFrame, const nsDisplayItem* aItem) {
+ const nsRect& partialPrerenderedRect = aItem->GetUntransformedPaintRect();
+ nsRect overflow = aFrame->InkOverflowRectRelativeToSelf();
+
+ ScrollableLayerGuid::ViewID scrollId = ScrollableLayerGuid::NULL_SCROLL_ID;
+
+ const nsIFrame* clipFrame =
+ nsLayoutUtils::GetNearestOverflowClipFrame(aFrame->GetParent());
+ const nsIScrollableFrame* scrollFrame = do_QueryFrame(clipFrame);
+
+ if (!clipFrame) {
+ // If there is no suitable clip frame in the same document, use the
+ // root one.
+ scrollFrame = aFrame->PresShell()->GetRootScrollFrameAsScrollable();
+ if (scrollFrame) {
+ clipFrame = do_QueryFrame(scrollFrame);
+ } else {
+ // If there is no root scroll frame, use the viewport frame.
+ clipFrame = aFrame->PresShell()->GetRootFrame();
+ }
+ }
+
+ // If the scroll frame is asyncronously scrollable, try to find the scroll id.
+ if (scrollFrame &&
+ !scrollFrame->GetScrollStyles().IsHiddenInBothDirections() &&
+ nsLayoutUtils::AsyncPanZoomEnabled(aFrame)) {
+ const bool isInPositionFixed =
+ nsLayoutUtils::IsInPositionFixedSubtree(aFrame);
+ const ActiveScrolledRoot* asr = aItem->GetActiveScrolledRoot();
+ const nsIFrame* asrScrollableFrame =
+ asr ? do_QueryFrame(asr->mScrollableFrame) : nullptr;
+ if (!isInPositionFixed && asr &&
+ aFrame->PresContext() == asrScrollableFrame->PresContext()) {
+ scrollId = asr->GetViewId();
+ MOZ_ASSERT(clipFrame == asrScrollableFrame);
+ } else {
+ // Use the root scroll id in the same document if the target frame is in
+ // position:fixed subtree or there is no ASR or the ASR is in a different
+ // ancestor document.
+ scrollId =
+ nsLayoutUtils::ScrollIdForRootScrollFrame(aFrame->PresContext());
+ MOZ_ASSERT(clipFrame == aFrame->PresShell()->GetRootScrollFrame());
+ }
+ }
+
+ int32_t devPixelsToAppUnits = aFrame->PresContext()->AppUnitsPerDevPixel();
+
+ auto [clipRect, transformInClip] = GetClipRectAndTransformForPartialPrerender(
+ aFrame, devPixelsToAppUnits, clipFrame, scrollFrame);
+
+ return PartialPrerenderData{
+ LayoutDeviceRect::FromAppUnits(partialPrerenderedRect,
+ devPixelsToAppUnits),
+ GetOverflowedSides(overflow, partialPrerenderedRect),
+ scrollId,
+ clipRect,
+ transformInClip,
+ LayoutDevicePoint()}; // will be set by caller.
+}
+
+enum class AnimationDataType {
+ WithMotionPath,
+ WithoutMotionPath,
+};
+static Maybe<TransformData> CreateAnimationData(
+ nsIFrame* aFrame, nsDisplayItem* aItem, DisplayItemType aType,
+ layers::LayersBackend aLayersBackend, AnimationDataType aDataType,
+ const Maybe<LayoutDevicePoint>& aPosition) {
+ if (aType != DisplayItemType::TYPE_TRANSFORM) {
+ return Nothing();
+ }
+
+ // XXX Performance here isn't ideal for SVG. We'd prefer to avoid resolving
+ // the dimensions of refBox. That said, we only get here if there are CSS
+ // animations or transitions on this element, and that is likely to be a
+ // lot rarer than transforms on SVG (the frequency of which drives the need
+ // for TransformReferenceBox).
+ TransformReferenceBox refBox(aFrame);
+ const nsRect bounds(0, 0, refBox.Width(), refBox.Height());
+
+ // all data passed directly to the compositor should be in dev pixels
+ int32_t devPixelsToAppUnits = aFrame->PresContext()->AppUnitsPerDevPixel();
+ float scale = devPixelsToAppUnits;
+ gfx::Point3D offsetToTransformOrigin =
+ nsDisplayTransform::GetDeltaToTransformOrigin(aFrame, refBox, scale);
+ nsPoint origin;
+ if (aLayersBackend == layers::LayersBackend::LAYERS_WR) {
+ // leave origin empty, because we are sending it separately on the
+ // stacking context that we are pushing to WR, and WR will automatically
+ // include it when picking up the animated transform values
+ } else if (aItem) {
+ // This branch is for display items to leverage the cache of
+ // nsDisplayListBuilder.
+ origin = aItem->ToReferenceFrame();
+ } else {
+ // This branch is running for restyling.
+ // Animations are animated at the coordination of the reference
+ // frame outside, not the given frame itself. The given frame
+ // is also reference frame too, so the parent's reference frame
+ // are used.
+ nsIFrame* referenceFrame = nsLayoutUtils::GetReferenceFrame(
+ nsLayoutUtils::GetCrossDocParentFrameInProcess(aFrame));
+ origin = aFrame->GetOffsetToCrossDoc(referenceFrame);
+ }
+
+ Maybe<MotionPathData> motionPathData;
+ if (aDataType == AnimationDataType::WithMotionPath) {
+ const StyleTransformOrigin& styleOrigin =
+ aFrame->StyleDisplay()->mTransformOrigin;
+ CSSPoint motionPathOrigin = nsStyleTransformMatrix::Convert2DPosition(
+ styleOrigin.horizontal, styleOrigin.vertical, refBox);
+ CSSPoint anchorAdjustment =
+ MotionPathUtils::ComputeAnchorPointAdjustment(*aFrame);
+
+ motionPathData = Some(layers::MotionPathData(
+ motionPathOrigin, anchorAdjustment, RayReferenceData(aFrame)));
+ }
+
+ Maybe<PartialPrerenderData> partialPrerenderData;
+ if (aItem && static_cast<nsDisplayTransform*>(aItem)->IsPartialPrerender()) {
+ partialPrerenderData = Some(GetPartialPrerenderData(aFrame, aItem));
+
+ if (aLayersBackend == layers::LayersBackend::LAYERS_WR) {
+ MOZ_ASSERT(aPosition);
+ partialPrerenderData->position() = *aPosition;
+ }
+ }
+
+ return Some(TransformData(origin, offsetToTransformOrigin, bounds,
+ devPixelsToAppUnits, motionPathData,
+ partialPrerenderData));
+}
+
+void AnimationInfo::AddNonAnimatingTransformLikePropertiesStyles(
+ const nsCSSPropertyIDSet& aNonAnimatingProperties, nsIFrame* aFrame,
+ Send aSendFlag) {
+ auto appendFakeAnimation = [this, aSendFlag](nsCSSPropertyID aProperty,
+ Animatable&& aBaseStyle) {
+ layers::Animation* animation = (aSendFlag == Send::NextTransaction)
+ ? AddAnimationForNextTransaction()
+ : AddAnimation();
+ animation->property() = aProperty;
+ animation->baseStyle() = std::move(aBaseStyle);
+ animation->easingFunction() = Nothing();
+ animation->isNotAnimating() = true;
+ };
+
+ const nsStyleDisplay* display = aFrame->StyleDisplay();
+ // A simple optimization. We don't need to send offset-* properties if we
+ // don't have offset-path and offset-position.
+ // FIXME: Bug 1559232: Add offset-position here.
+ bool hasMotion =
+ !display->mOffsetPath.IsNone() ||
+ !aNonAnimatingProperties.HasProperty(eCSSProperty_offset_path);
+
+ for (nsCSSPropertyID id : aNonAnimatingProperties) {
+ switch (id) {
+ case eCSSProperty_transform:
+ if (!display->mTransform.IsNone()) {
+ TransformReferenceBox refBox(aFrame);
+ appendFakeAnimation(
+ id, ResolveTransformOperations(display->mTransform, refBox));
+ }
+ break;
+ case eCSSProperty_translate:
+ if (!display->mTranslate.IsNone()) {
+ TransformReferenceBox refBox(aFrame);
+ appendFakeAnimation(id,
+ ResolveTranslate(display->mTranslate, refBox));
+ }
+ break;
+ case eCSSProperty_rotate:
+ if (!display->mRotate.IsNone()) {
+ appendFakeAnimation(id, display->mRotate);
+ }
+ break;
+ case eCSSProperty_scale:
+ if (!display->mScale.IsNone()) {
+ appendFakeAnimation(id, display->mScale);
+ }
+ break;
+ case eCSSProperty_offset_path:
+ if (!display->mOffsetPath.IsNone()) {
+ appendFakeAnimation(id, NormalizeOffsetPath(display->mOffsetPath));
+ }
+ break;
+ case eCSSProperty_offset_distance:
+ if (hasMotion && !display->mOffsetDistance.IsDefinitelyZero()) {
+ appendFakeAnimation(id, display->mOffsetDistance);
+ }
+ break;
+ case eCSSProperty_offset_rotate:
+ if (hasMotion && (!display->mOffsetRotate.auto_ ||
+ display->mOffsetRotate.angle.ToDegrees() != 0.0)) {
+ appendFakeAnimation(id, display->mOffsetRotate);
+ }
+ break;
+ case eCSSProperty_offset_anchor:
+ if (hasMotion && !display->mOffsetAnchor.IsAuto()) {
+ appendFakeAnimation(id, display->mOffsetAnchor);
+ }
+ break;
+ default:
+ MOZ_ASSERT_UNREACHABLE("Unsupported transform-like properties");
+ }
+ }
+}
+
+void AnimationInfo::AddAnimationsForDisplayItem(
+ nsIFrame* aFrame, nsDisplayListBuilder* aBuilder, nsDisplayItem* aItem,
+ DisplayItemType aType, WebRenderLayerManager* aLayerManager,
+ const Maybe<LayoutDevicePoint>& aPosition) {
+ Send sendFlag = !aBuilder ? Send::NextTransaction : Send::Immediate;
+ if (sendFlag == Send::NextTransaction) {
+ ClearAnimationsForNextTransaction();
+ } else {
+ ClearAnimations();
+ }
+
+ // Update the animation generation on the layer. We need to do this before
+ // any early returns since even if we don't add any animations to the
+ // layer, we still need to mark it as up-to-date with regards to animations.
+ // Otherwise, in RestyleManager we'll notice the discrepancy between the
+ // animation generation numbers and update the layer indefinitely.
+ EffectSet* effects = EffectSet::GetForFrame(aFrame, aType);
+ uint64_t animationGeneration =
+ effects ? effects->GetAnimationGeneration() : 0;
+ SetAnimationGeneration(animationGeneration);
+ if (!effects || effects->IsEmpty()) {
+ return;
+ }
+
+ EffectCompositor::ClearIsRunningOnCompositor(aFrame, aType);
+ const nsCSSPropertyIDSet& propertySet =
+ LayerAnimationInfo::GetCSSPropertiesFor(aType);
+ const nsTArray<RefPtr<dom::Animation>> matchedAnimations =
+ EffectCompositor::GetAnimationsForCompositor(aFrame, propertySet);
+ if (matchedAnimations.IsEmpty()) {
+ return;
+ }
+
+ // If the frame is not prerendered, bail out.
+ // Do this check only during layer construction; during updating the
+ // caller is required to check it appropriately.
+ if (aItem && !aItem->CanUseAsyncAnimations(aBuilder)) {
+ // EffectCompositor needs to know that we refused to run this animation
+ // asynchronously so that it will not throttle the main thread
+ // animation.
+ aFrame->SetProperty(nsIFrame::RefusedAsyncAnimationProperty(), true);
+ return;
+ }
+
+ const HashMap<nsCSSPropertyID, nsTArray<RefPtr<dom::Animation>>>
+ compositorAnimations =
+ GroupAnimationsByProperty(matchedAnimations, propertySet);
+ Maybe<TransformData> transformData =
+ CreateAnimationData(aFrame, aItem, aType, aLayerManager->GetBackendType(),
+ compositorAnimations.has(eCSSProperty_offset_path) ||
+ !aFrame->StyleDisplay()->mOffsetPath.IsNone()
+ ? AnimationDataType::WithMotionPath
+ : AnimationDataType::WithoutMotionPath,
+ aPosition);
+ // Bug 1424900: Drop this pref check after shipping individual transforms.
+ // Bug 1582554: Drop this pref check after shipping motion path.
+ const bool hasMultipleTransformLikeProperties =
+ (StaticPrefs::layout_css_individual_transform_enabled() ||
+ StaticPrefs::layout_css_motion_path_enabled()) &&
+ aType == DisplayItemType::TYPE_TRANSFORM;
+ nsCSSPropertyIDSet nonAnimatingProperties =
+ nsCSSPropertyIDSet::TransformLikeProperties();
+ for (auto iter = compositorAnimations.iter(); !iter.done(); iter.next()) {
+ // Note: We can skip offset-* if there is no offset-path/offset-position
+ // animations and styles. However, it should be fine and may be better to
+ // send these information to the compositor because 1) they are simple data
+ // structure, 2) AddAnimationsForProperty() marks these animations as
+ // running on the composiror, so CanThrottle() returns true for them, and
+ // we avoid running these animations on the main thread.
+ bool added = AddAnimationsForProperty(aFrame, effects, iter.get().value(),
+ transformData, iter.get().key(),
+ sendFlag, aLayerManager);
+ if (added && transformData) {
+ // Only copy TransformLikeMetaData in the first animation property.
+ transformData.reset();
+ }
+
+ if (hasMultipleTransformLikeProperties && added) {
+ nonAnimatingProperties.RemoveProperty(iter.get().key());
+ }
+ }
+
+ // If some transform-like properties have animations, but others not, and
+ // those non-animating transform-like properties have non-none
+ // transform/translate/rotate/scale styles or non-initial value for motion
+ // path properties, we also pass their styles into the compositor, so the
+ // final transform matrix (on the compositor) could take them into account.
+ if (hasMultipleTransformLikeProperties &&
+ // For these cases we don't need to send the property style values to
+ // the compositor:
+ // 1. No property has running animations on the compositor. (i.e. All
+ // properties should be handled by main thread)
+ // 2. All properties have running animations on the compositor.
+ // (i.e. Those running animations should override the styles.)
+ !nonAnimatingProperties.Equals(
+ nsCSSPropertyIDSet::TransformLikeProperties()) &&
+ !nonAnimatingProperties.IsEmpty()) {
+ AddNonAnimatingTransformLikePropertiesStyles(nonAnimatingProperties, aFrame,
+ sendFlag);
+ }
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/AnimationInfo.h b/gfx/layers/AnimationInfo.h
new file mode 100644
index 0000000000..5c8d864d7b
--- /dev/null
+++ b/gfx/layers/AnimationInfo.h
@@ -0,0 +1,164 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GFX_ANIMATIONINFO_H
+#define GFX_ANIMATIONINFO_H
+
+#include "nsCSSPropertyIDSet.h"
+#include "nsDisplayItemTypes.h"
+#include "mozilla/Array.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/FunctionRef.h"
+#include "mozilla/layers/AnimationStorageData.h"
+#include "mozilla/layers/LayersMessages.h" // for TransformData
+
+struct RawServoAnimationValue;
+class nsIContent;
+class nsIFrame;
+
+namespace mozilla {
+
+class nsDisplayItem;
+class nsDisplayListBuilder;
+class EffectSet;
+struct AnimationProperty;
+
+namespace dom {
+class Animation;
+} // namespace dom
+
+namespace gfx {
+class Path;
+} // namespace gfx
+
+namespace layers {
+
+class Animation;
+class CompositorAnimations;
+class Layer;
+class WebRenderLayerManager;
+struct CompositorAnimationData;
+struct PropertyAnimationGroup;
+
+class AnimationInfo final {
+ typedef nsTArray<Animation> AnimationArray;
+
+ public:
+ AnimationInfo();
+ ~AnimationInfo();
+
+ // Ensure that this AnimationInfo has a valid (non-zero) animations id. This
+ // value is unique across layers.
+ void EnsureAnimationsId();
+
+ // Call AddAnimation to add a new animation to this layer from layout code.
+ // Caller must fill in all the properties of the returned animation.
+ // A later animation overrides an earlier one.
+ Animation* AddAnimation();
+
+ // These are a parallel to AddAnimation and clearAnimations, except
+ // they add pending animations that apply only when the next
+ // transaction is begun. (See also
+ // SetBaseTransformForNextTransaction.)
+ Animation* AddAnimationForNextTransaction();
+
+ void SetAnimationGeneration(uint64_t aCount) {
+ mAnimationGeneration = Some(aCount);
+ }
+ Maybe<uint64_t> GetAnimationGeneration() const {
+ return mAnimationGeneration;
+ }
+
+ // ClearAnimations clears animations on this layer.
+ void ClearAnimations();
+ void ClearAnimationsForNextTransaction();
+ bool StartPendingAnimations(const TimeStamp& aReadyTime);
+
+ uint64_t GetCompositorAnimationsId() { return mCompositorAnimationsId; }
+ // Note: We don't set mAnimations on the compositor thread, so this will
+ // always return an empty array on the compositor thread.
+ AnimationArray& GetAnimations() { return mAnimations; }
+ nsTArray<PropertyAnimationGroup>& GetPropertyAnimationGroups() {
+ return mStorageData.mAnimation;
+ }
+ const Maybe<TransformData>& GetTransformData() const {
+ return mStorageData.mTransformData;
+ }
+ const LayersId& GetLayersId() const { return mStorageData.mLayersId; }
+ bool ApplyPendingUpdatesForThisTransaction();
+ bool HasTransformAnimation() const;
+
+ gfx::Path* CachedMotionPath() { return mStorageData.mCachedMotionPath; }
+
+ // In case of continuation, |aFrame| must be the first or the last
+ // continuation frame, otherwise this function might return Nothing().
+ static Maybe<uint64_t> GetGenerationFromFrame(
+ nsIFrame* aFrame, DisplayItemType aDisplayItemKey);
+
+ using CompositorAnimatableDisplayItemTypes =
+ Array<DisplayItemType,
+ nsCSSPropertyIDSet::CompositorAnimatableDisplayItemCount()>;
+ using AnimationGenerationCallback = FunctionRef<bool(
+ const Maybe<uint64_t>& aGeneration, DisplayItemType aDisplayItemType)>;
+ // Enumerates animation generations on |aFrame| for the given display item
+ // types and calls |aCallback| with the animation generation.
+ //
+ // The enumeration stops if |aCallback| returns false.
+ static void EnumerateGenerationOnFrame(
+ const nsIFrame* aFrame, const nsIContent* aContent,
+ const CompositorAnimatableDisplayItemTypes& aDisplayItemTypes,
+ AnimationGenerationCallback);
+
+ void AddAnimationsForDisplayItem(
+ nsIFrame* aFrame, nsDisplayListBuilder* aBuilder, nsDisplayItem* aItem,
+ DisplayItemType aType, WebRenderLayerManager* aLayerManager,
+ const Maybe<LayoutDevicePoint>& aPosition = Nothing());
+
+ private:
+ enum class Send {
+ NextTransaction,
+ Immediate,
+ };
+ void AddAnimationForProperty(nsIFrame* aFrame,
+ const AnimationProperty& aProperty,
+ dom::Animation* aAnimation,
+ const Maybe<TransformData>& aTransformData,
+ Send aSendFlag);
+
+ bool AddAnimationsForProperty(
+ nsIFrame* aFrame, const EffectSet* aEffects,
+ const nsTArray<RefPtr<dom::Animation>>& aCompositorAnimations,
+ const Maybe<TransformData>& aTransformData, nsCSSPropertyID aProperty,
+ Send aSendFlag, WebRenderLayerManager* aLayerManager);
+
+ void AddNonAnimatingTransformLikePropertiesStyles(
+ const nsCSSPropertyIDSet& aNonAnimatingProperties, nsIFrame* aFrame,
+ Send aSendFlag);
+
+ protected:
+ // mAnimations (and mPendingAnimations) are only set on the main thread.
+ //
+ // Once the animations are received on the compositor thread/process we
+ // use AnimationHelper::ExtractAnimations to transform the rather compact
+ // representation of animation data we transfer into something we can more
+ // readily use for sampling and then store it in mPropertyAnimationGroups
+ // (below) or CompositorAnimationStorage.mAnimations for WebRender.
+ AnimationArray mAnimations;
+ UniquePtr<AnimationArray> mPendingAnimations;
+
+ uint64_t mCompositorAnimationsId;
+ // The extracted data produced by AnimationHelper::ExtractAnimations().
+ AnimationStorageData mStorageData;
+ // If this layer is used for OMTA, then this counter is used to ensure we
+ // stay in sync with the animation manager
+ Maybe<uint64_t> mAnimationGeneration;
+ bool mMutated;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // GFX_ANIMATIONINFO_H
diff --git a/gfx/layers/AnimationStorageData.h b/gfx/layers/AnimationStorageData.h
new file mode 100644
index 0000000000..ba02edaa9f
--- /dev/null
+++ b/gfx/layers/AnimationStorageData.h
@@ -0,0 +1,117 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_AnimationStorageData_h
+#define mozilla_layers_AnimationStorageData_h
+
+#include "mozilla/dom/Nullable.h"
+#include "mozilla/ServoStyleConsts.h" // for ComputedTimingFunction
+#include "mozilla/layers/LayersMessages.h" // for TransformData, etc
+#include "mozilla/layers/LayersTypes.h" // for LayersId
+#include "mozilla/TimeStamp.h" // for TimeStamp
+#include "mozilla/TimingParams.h"
+#include "X11UndefineNone.h"
+
+namespace mozilla {
+
+namespace dom {
+enum class CompositeOperation : uint8_t;
+enum class IterationCompositeOperation : uint8_t;
+}; // namespace dom
+
+namespace layers {
+
+struct PropertyAnimation {
+ struct SegmentData {
+ RefPtr<StyleAnimationValue> mStartValue;
+ RefPtr<StyleAnimationValue> mEndValue;
+ Maybe<mozilla::StyleComputedTimingFunction> mFunction;
+ float mStartPortion;
+ float mEndPortion;
+ dom::CompositeOperation mStartComposite;
+ dom::CompositeOperation mEndComposite;
+ };
+ nsTArray<SegmentData> mSegments;
+ TimingParams mTiming;
+
+ // These two variables correspond to the variables of the same name in
+ // KeyframeEffectReadOnly and are used for the same purpose: to skip composing
+ // animations whose progress has not changed.
+ dom::Nullable<double> mProgressOnLastCompose;
+ uint64_t mCurrentIterationOnLastCompose = 0;
+ // These two variables are used for a similar optimization above but are
+ // applied to the timing function in each keyframe.
+ uint32_t mSegmentIndexOnLastCompose = 0;
+ dom::Nullable<double> mPortionInSegmentOnLastCompose;
+
+ TimeStamp mOriginTime;
+ Maybe<TimeDuration> mStartTime;
+ TimeDuration mHoldTime;
+ float mPlaybackRate;
+ dom::IterationCompositeOperation mIterationComposite;
+ bool mIsNotPlaying;
+
+ // The information for scroll-driven animations.
+ Maybe<ScrollTimelineOptions> mScrollTimelineOptions;
+
+ void ResetLastCompositionValues() {
+ mCurrentIterationOnLastCompose = 0;
+ mSegmentIndexOnLastCompose = 0;
+ mProgressOnLastCompose.SetNull();
+ mPortionInSegmentOnLastCompose.SetNull();
+ }
+};
+
+struct PropertyAnimationGroup {
+ nsCSSPropertyID mProperty;
+
+ nsTArray<PropertyAnimation> mAnimations;
+ RefPtr<StyleAnimationValue> mBaseStyle;
+
+ bool IsEmpty() const { return mAnimations.IsEmpty(); }
+ void Clear() {
+ mAnimations.Clear();
+ mBaseStyle = nullptr;
+ }
+ void ResetLastCompositionValues() {
+ for (PropertyAnimation& animation : mAnimations) {
+ animation.ResetLastCompositionValues();
+ }
+ }
+};
+
+struct AnimationStorageData {
+ // Each entry in the array represents an animation list for one property. For
+ // transform-like properties (e.g. transform, rotate etc.), there may be
+ // multiple entries depending on how many transform-like properties we have.
+ nsTArray<PropertyAnimationGroup> mAnimation;
+ Maybe<TransformData> mTransformData;
+ // For motion path. We cached the gfx path for optimization.
+ RefPtr<gfx::Path> mCachedMotionPath;
+ // This is used to communicate with the main-thread. E.g. to tell the fact
+ // that this animation needs to be pre-rendered again on the main-thread, etc.
+ LayersId mLayersId;
+
+ AnimationStorageData() = default;
+ AnimationStorageData(AnimationStorageData&& aOther) = default;
+ AnimationStorageData& operator=(AnimationStorageData&& aOther) = default;
+
+ // Avoid any copy because mAnimation could be a large array.
+ AnimationStorageData(const AnimationStorageData& aOther) = delete;
+ AnimationStorageData& operator=(const AnimationStorageData& aOther) = delete;
+
+ bool IsEmpty() const { return mAnimation.IsEmpty(); }
+ void Clear() {
+ mAnimation.Clear();
+ mTransformData.reset();
+ mCachedMotionPath = nullptr;
+ }
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_AnimationStorageData_h
diff --git a/gfx/layers/AtomicRefCountedWithFinalize.h b/gfx/layers/AtomicRefCountedWithFinalize.h
new file mode 100644
index 0000000000..caae189212
--- /dev/null
+++ b/gfx/layers/AtomicRefCountedWithFinalize.h
@@ -0,0 +1,207 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_ATOMICREFCOUNTEDWITHFINALIZE_H_
+#define MOZILLA_ATOMICREFCOUNTEDWITHFINALIZE_H_
+
+#include "mozilla/RefPtr.h"
+#include "mozilla/Likely.h"
+#include "MainThreadUtils.h"
+#include "base/message_loop.h"
+#include "base/task.h"
+#include "mozilla/gfx/Logging.h"
+
+#define ADDREF_MANUALLY(obj) \
+ (obj)->AddRefManually(__FUNCTION__, __FILE__, __LINE__)
+#define RELEASE_MANUALLY(obj) \
+ (obj)->ReleaseManually(__FUNCTION__, __FILE__, __LINE__)
+
+namespace mozilla {
+
+template <class U>
+class StaticRefPtr;
+
+namespace gl {
+template <typename T>
+class RefSet;
+
+template <typename T>
+class RefQueue;
+} // namespace gl
+
+template <typename T>
+class AtomicRefCountedWithFinalize {
+ protected:
+ explicit AtomicRefCountedWithFinalize(const char* aName)
+ : mRecycleCallback(nullptr),
+ mClosure(nullptr),
+ mRefCount(0)
+#ifdef DEBUG
+ ,
+ mSpew(false),
+ mManualAddRefs(0),
+ mManualReleases(0)
+#endif
+#ifdef NS_BUILD_REFCNT_LOGGING
+ ,
+ mName(aName)
+#endif
+ {
+ }
+
+ ~AtomicRefCountedWithFinalize() {
+ if (mRefCount >= 0) {
+ gfxCriticalError() << "Deleting referenced object? " << mRefCount;
+ }
+ }
+
+ public:
+ // Mark user classes that are considered flawless.
+ template <class U>
+ friend class ::mozilla::StaticRefPtr;
+
+ template <class U>
+ friend struct mozilla::RefPtrTraits;
+
+ template <typename U>
+ friend class ::mozilla::gl::RefSet;
+
+ template <typename U>
+ friend class ::mozilla::gl::RefQueue;
+
+ // friend class mozilla::gl::SurfaceFactory;
+
+ void AddRefManually(const char* funcName, const char* fileName,
+ uint32_t lineNum) {
+#ifdef DEBUG
+ uint32_t count = ++mManualAddRefs;
+ if (mSpew) {
+ printf_stderr("AddRefManually() #%u in %s at %s:%u\n", count, funcName,
+ fileName, lineNum);
+ }
+#else
+ (void)funcName;
+ (void)fileName;
+ (void)lineNum;
+#endif
+ AddRef();
+ }
+
+ void ReleaseManually(const char* funcName, const char* fileName,
+ uint32_t lineNum) {
+#ifdef DEBUG
+ uint32_t count = ++mManualReleases;
+ if (mSpew) {
+ printf_stderr("ReleaseManually() #%u in %s at %s:%u\n", count, funcName,
+ fileName, lineNum);
+ }
+#else
+ (void)funcName;
+ (void)fileName;
+ (void)lineNum;
+#endif
+ Release();
+ }
+
+ private:
+ void AddRef() {
+ MOZ_ASSERT(mRefCount >= 0, "AddRef() during/after Finalize()/dtor.");
+#ifdef NS_BUILD_REFCNT_LOGGING
+ int currCount = ++mRefCount;
+ NS_LOG_ADDREF(this, currCount, mName, sizeof(*this));
+#else
+ ++mRefCount;
+#endif
+ }
+
+ void Release() {
+ MOZ_ASSERT(mRefCount > 0, "Release() during/after Finalize()/dtor.");
+ // Read mRecycleCallback early so that it does not get set to
+ // deleted memory, if the object is goes away. See bug 994903.
+ // This saves us in the case where there is no callback, so that
+ // we can do the "else if" below.
+ RecycleCallback recycleCallback = mRecycleCallback;
+ int currCount = --mRefCount;
+ if (currCount < 0) {
+ gfxCriticalError() << "Invalid reference count release" << currCount;
+ ++mRefCount;
+ return;
+ }
+#ifdef NS_BUILD_REFCNT_LOGGING
+ NS_LOG_RELEASE(this, currCount, mName);
+#endif
+
+ if (0 == currCount) {
+ mRefCount = detail::DEAD;
+ MOZ_ASSERT(IsDead());
+
+ // Recycle listeners must call ClearRecycleCallback
+ // before releasing their strong reference.
+ if (mRecycleCallback) {
+ gfxCriticalError() << "About to release with valid callback";
+ mRecycleCallback = nullptr;
+ }
+
+ MOZ_ASSERT(mManualAddRefs == mManualReleases);
+
+ T* derived = static_cast<T*>(this);
+ derived->Finalize();
+ delete derived;
+ } else if (1 == currCount && recycleCallback) {
+ // There is nothing enforcing this in the code, except how the callers
+ // are being careful to never let the reference count go down if there
+ // is a callback.
+ MOZ_ASSERT(!IsDead());
+ T* derived = static_cast<T*>(this);
+ recycleCallback(derived, mClosure);
+ }
+ }
+
+ public:
+ typedef void (*RecycleCallback)(T* aObject, void* aClosure);
+ /**
+ * Set a callback responsible for recycling this object
+ * before it is finalized.
+ */
+ void SetRecycleCallback(RecycleCallback aCallback, void* aClosure) {
+ MOZ_ASSERT(!IsDead());
+ mRecycleCallback = aCallback;
+ mClosure = aClosure;
+ }
+ void ClearRecycleCallback() {
+ MOZ_ASSERT(!IsDead());
+ SetRecycleCallback(nullptr, nullptr);
+ }
+
+ bool HasRecycleCallback() const {
+ MOZ_ASSERT(!IsDead());
+ return !!mRecycleCallback;
+ }
+
+ bool IsDead() const { return mRefCount < 0; }
+
+ bool HasOneRef() const { return mRefCount == 1; }
+
+ private:
+ RecycleCallback mRecycleCallback;
+ void* mClosure;
+ Atomic<int> mRefCount;
+#ifdef DEBUG
+ public:
+ bool mSpew;
+
+ private:
+ Atomic<uint32_t> mManualAddRefs;
+ Atomic<uint32_t> mManualReleases;
+#endif
+#ifdef NS_BUILD_REFCNT_LOGGING
+ const char* mName;
+#endif
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/gfx/layers/AxisPhysicsMSDModel.cpp b/gfx/layers/AxisPhysicsMSDModel.cpp
new file mode 100644
index 0000000000..a506db8a05
--- /dev/null
+++ b/gfx/layers/AxisPhysicsMSDModel.cpp
@@ -0,0 +1,78 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "AxisPhysicsMSDModel.h"
+#include <math.h> // for sqrt and fabs
+
+namespace mozilla {
+namespace layers {
+
+/**
+ * Constructs an AxisPhysicsMSDModel with initial values for state.
+ *
+ * @param aInitialPosition sets the initial position of the simulated spring,
+ * in AppUnits.
+ * @param aInitialDestination sets the resting position of the simulated spring,
+ * in AppUnits.
+ * @param aInitialVelocity sets the initial velocity of the simulated spring,
+ * in AppUnits / second. Critically-damped and over-damped systems are
+ * guaranteed not to overshoot aInitialDestination if this is set to 0;
+ * however, it is possible to overshoot and oscillate if not set to 0 or
+ * the system is under-damped.
+ * @param aSpringConstant sets the strength of the simulated spring. Greater
+ * values of mSpringConstant result in a stiffer / stronger spring.
+ * @param aDampingRatio controls the amount of dampening force and determines
+ * if the system is under-damped, critically-damped, or over-damped.
+ */
+AxisPhysicsMSDModel::AxisPhysicsMSDModel(double aInitialPosition,
+ double aInitialDestination,
+ double aInitialVelocity,
+ double aSpringConstant,
+ double aDampingRatio)
+ : AxisPhysicsModel(aInitialPosition, aInitialVelocity),
+ mDestination(aInitialDestination),
+ mSpringConstant(aSpringConstant),
+ mSpringConstantSqrtXTwo(sqrt(mSpringConstant) * 2.0),
+ mDampingRatio(aDampingRatio) {}
+
+AxisPhysicsMSDModel::~AxisPhysicsMSDModel() = default;
+
+double AxisPhysicsMSDModel::Acceleration(const State& aState) {
+ // Simulate a Mass-Damper-Spring Model; assume a unit mass
+
+ // Hooke’s Law: http://en.wikipedia.org/wiki/Hooke%27s_law
+ double spring_force = (mDestination - aState.p) * mSpringConstant;
+ double damp_force = -aState.v * mDampingRatio * mSpringConstantSqrtXTwo;
+
+ return spring_force + damp_force;
+}
+
+double AxisPhysicsMSDModel::GetDestination() const { return mDestination; }
+
+void AxisPhysicsMSDModel::SetDestination(double aDestination) {
+ mDestination = aDestination;
+}
+
+bool AxisPhysicsMSDModel::IsFinished(double aSmallestVisibleIncrement) const {
+ // In order to satisfy the condition of reaching the destination, the distance
+ // between the simulation position and the destination must be less than
+ // aSmallestVisibleIncrement while the speed is simultaneously less than
+ // finishVelocity. This enables an under-damped system to overshoot the
+ // destination when desired without prematurely triggering the finished state.
+ // If finishVelocity is set too low, the animation may end long after
+ // oscillation has finished, resulting in unnecessary processing.
+ // If set too high, the animation may prematurely terminate when expected
+ // to overshoot the destination in an under-damped system.
+ // aSmallestVisibleIncrement * 2 was selected through experimentation that
+ // revealed that a critically damped system will terminate within 100ms.
+ const double finishVelocity = aSmallestVisibleIncrement * 2;
+
+ return fabs(mDestination - GetPosition()) < aSmallestVisibleIncrement &&
+ fabs(GetVelocity()) <= finishVelocity;
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/AxisPhysicsMSDModel.h b/gfx/layers/AxisPhysicsMSDModel.h
new file mode 100644
index 0000000000..e303450ff2
--- /dev/null
+++ b/gfx/layers/AxisPhysicsMSDModel.h
@@ -0,0 +1,83 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_AxisPhysicsMSDModel_h
+#define mozilla_layers_AxisPhysicsMSDModel_h
+
+#include "AxisPhysicsModel.h"
+
+namespace mozilla {
+namespace layers {
+
+/**
+ * AxisPhysicsMSDModel encapsulates a 1-dimensional MSD (Mass-Spring-Damper)
+ * model. A unit mass is assumed.
+ */
+class AxisPhysicsMSDModel : public AxisPhysicsModel {
+ public:
+ AxisPhysicsMSDModel(double aInitialPosition, double aInitialDestination,
+ double aInitialVelocity, double aSpringConstant,
+ double aDampingRatio);
+
+ virtual ~AxisPhysicsMSDModel();
+
+ /**
+ * Gets the raw destination of this axis at this moment.
+ */
+ double GetDestination() const;
+
+ /**
+ * Sets the raw destination of this axis at this moment.
+ */
+ void SetDestination(double aDestination);
+
+ /**
+ * Returns true when the position is close to the destination and the
+ * velocity is low.
+ */
+ bool IsFinished(double aSmallestVisibleIncrement) const;
+
+ protected:
+ double Acceleration(const State& aState) override;
+
+ private:
+ /**
+ * mDestination represents the target position and the resting position of
+ * the simulated spring.
+ */
+ double mDestination;
+
+ /**
+ * Greater values of mSpringConstant result in a stiffer / stronger spring.
+ */
+ double mSpringConstant;
+
+ /**
+ * mSpringConstantSqrtTimesTwo is calculated from mSpringConstant to reduce
+ * calculations performed in the inner loop.
+ */
+ double mSpringConstantSqrtXTwo;
+
+ /**
+ * Damping Ratio: http://en.wikipedia.org/wiki/Damping_ratio
+ *
+ * When mDampingRatio < 1.0, this is an under damped system.
+ * - Overshoots destination and oscillates with the amplitude gradually
+ * decreasing to zero.
+ *
+ * When mDampingRatio == 1.0, this is a critically damped system.
+ * - Reaches destination as quickly as possible without oscillating.
+ *
+ * When mDampingRatio > 1.0, this is an over damped system.
+ * - Reaches destination (exponentially decays) without oscillating.
+ */
+ double mDampingRatio;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif
diff --git a/gfx/layers/AxisPhysicsModel.cpp b/gfx/layers/AxisPhysicsModel.cpp
new file mode 100644
index 0000000000..dc0cba3110
--- /dev/null
+++ b/gfx/layers/AxisPhysicsModel.cpp
@@ -0,0 +1,101 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "AxisPhysicsModel.h"
+
+namespace mozilla {
+namespace layers {
+
+/**
+ * The simulation is advanced forward in time with a fixed time step to ensure
+ * that it remains deterministic given variable framerates. To determine the
+ * position at any variable time, two samples are interpolated.
+ *
+ * kFixedtimestep is set to 120hz in order to ensure that every frame in a
+ * common 60hz refresh rate display will have at least one physics simulation
+ * sample. More accuracy can be obtained by reducing kFixedTimestep to smaller
+ * intervals, such as 240hz or 1000hz, at the cost of more CPU cycles. If
+ * kFixedTimestep is increased to much longer intervals, interpolation will
+ * become less effective at reducing temporal jitter and the simulation will
+ * lose accuracy.
+ */
+const double AxisPhysicsModel::kFixedTimestep = 1.0 / 120.0; // 120hz
+
+/**
+ * Constructs an AxisPhysicsModel with initial values for state.
+ *
+ * @param aInitialPosition sets the initial position of the simulation,
+ * in AppUnits.
+ * @param aInitialVelocity sets the initial velocity of the simulation,
+ * in AppUnits / second.
+ */
+AxisPhysicsModel::AxisPhysicsModel(double aInitialPosition,
+ double aInitialVelocity)
+ : mProgress(1.0),
+ mPrevState(aInitialPosition, aInitialVelocity),
+ mNextState(aInitialPosition, aInitialVelocity) {}
+
+AxisPhysicsModel::~AxisPhysicsModel() = default;
+
+double AxisPhysicsModel::GetVelocity() const {
+ return LinearInterpolate(mPrevState.v, mNextState.v, mProgress);
+}
+
+double AxisPhysicsModel::GetPosition() const {
+ return LinearInterpolate(mPrevState.p, mNextState.p, mProgress);
+}
+
+void AxisPhysicsModel::SetVelocity(double aVelocity) {
+ mNextState.v = aVelocity;
+ mNextState.p = GetPosition();
+ mProgress = 1.0;
+}
+
+void AxisPhysicsModel::SetPosition(double aPosition) {
+ mNextState.v = GetVelocity();
+ mNextState.p = aPosition;
+ mProgress = 1.0;
+}
+
+void AxisPhysicsModel::Simulate(const TimeDuration& aDeltaTime) {
+ for (mProgress += aDeltaTime.ToSeconds() / kFixedTimestep; mProgress > 1.0;
+ mProgress -= 1.0) {
+ Integrate(kFixedTimestep);
+ }
+}
+
+void AxisPhysicsModel::Integrate(double aDeltaTime) {
+ mPrevState = mNextState;
+
+ // RK4 (Runge-Kutta method) Integration
+ // http://en.wikipedia.org/wiki/Runge%E2%80%93Kutta_methods
+ Derivative a = Evaluate(mNextState, 0.0, Derivative());
+ Derivative b = Evaluate(mNextState, aDeltaTime * 0.5, a);
+ Derivative c = Evaluate(mNextState, aDeltaTime * 0.5, b);
+ Derivative d = Evaluate(mNextState, aDeltaTime, c);
+
+ double dpdt = 1.0 / 6.0 * (a.dp + 2.0 * (b.dp + c.dp) + d.dp);
+ double dvdt = 1.0 / 6.0 * (a.dv + 2.0 * (b.dv + c.dv) + d.dv);
+
+ mNextState.p += dpdt * aDeltaTime;
+ mNextState.v += dvdt * aDeltaTime;
+}
+
+AxisPhysicsModel::Derivative AxisPhysicsModel::Evaluate(
+ const State& aInitState, double aDeltaTime, const Derivative& aDerivative) {
+ State state(aInitState.p + aDerivative.dp * aDeltaTime,
+ aInitState.v + aDerivative.dv * aDeltaTime);
+
+ return Derivative(state.v, Acceleration(state));
+}
+
+double AxisPhysicsModel::LinearInterpolate(double aV1, double aV2,
+ double aBlend) {
+ return aV1 * (1.0 - aBlend) + aV2 * aBlend;
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/AxisPhysicsModel.h b/gfx/layers/AxisPhysicsModel.h
new file mode 100644
index 0000000000..4aa6a69c11
--- /dev/null
+++ b/gfx/layers/AxisPhysicsModel.h
@@ -0,0 +1,123 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_AxisPhysicsModel_h
+#define mozilla_layers_AxisPhysicsModel_h
+
+#include <sys/types.h> // for int32_t
+#include "mozilla/TimeStamp.h" // for TimeDuration
+
+namespace mozilla {
+namespace layers {
+
+/**
+ * AxisPhysicsModel encapsulates a generic 1-dimensional physically-based motion
+ * model.
+ *
+ * It performs frame-rate independent interpolation and RK4 integration for
+ * smooth animation with stable, deterministic behavior.
+ * Implementations are expected to subclass and override the Acceleration()
+ * method.
+ */
+class AxisPhysicsModel {
+ public:
+ AxisPhysicsModel(double aInitialPosition, double aInitialVelocity);
+ virtual ~AxisPhysicsModel();
+
+ /**
+ * Advance the physics simulation.
+ * |aDelta| is the time since the last sample.
+ */
+ void Simulate(const TimeDuration& aDeltaTime);
+
+ /**
+ * Gets the raw velocity of this axis at this moment.
+ */
+ double GetVelocity() const;
+
+ /**
+ * Sets the raw velocity of this axis at this moment.
+ */
+ void SetVelocity(double aVelocity);
+
+ /**
+ * Gets the raw position of this axis at this moment.
+ */
+ double GetPosition() const;
+
+ /**
+ * Sets the raw position of this axis at this moment.
+ */
+ void SetPosition(double aPosition);
+
+ protected:
+ struct State {
+ State(double ap, double av) : p(ap), v(av){};
+ double p; // Position
+ double v; // Velocity
+ };
+
+ struct Derivative {
+ Derivative() : dp(0.0), dv(0.0){};
+ Derivative(double aDp, double aDv) : dp(aDp), dv(aDv){};
+ double dp; // dp / delta time = Position
+ double dv; // dv / delta time = Velocity
+ };
+
+ /**
+ * Acceleration must be overridden and return the number of
+ * axis-position-units / second that should be added or removed from the
+ * velocity.
+ */
+ virtual double Acceleration(const State& aState) = 0;
+
+ private:
+ /**
+ * Duration of fixed delta time step (seconds)
+ */
+ static const double kFixedTimestep;
+
+ /**
+ * 0.0 - 1.0 value indicating progress between current and next simulation
+ * sample. Normalized to units of kFixedTimestep duration.
+ */
+ double mProgress;
+
+ /**
+ * Sample of simulation state as it existed
+ * (1.0 - mProgress) * kFixedTimestep seconds in the past.
+ */
+ State mPrevState;
+
+ /**
+ * Sample of simulation state as it will be in mProgress * kFixedTimestep
+ * seconds in the future.
+ */
+ State mNextState;
+
+ /**
+ * Perform RK4 (Runge-Kutta method) Integration to calculate the next
+ * simulation sample.
+ */
+ void Integrate(double aDeltaTime);
+
+ /**
+ * Apply delta velocity and position represented by aDerivative over
+ * aDeltaTime seconds, calculate new acceleration, and return new deltas.
+ */
+ Derivative Evaluate(const State& aInitState, double aDeltaTime,
+ const Derivative& aDerivative);
+
+ /**
+ * Helper function for performing linear interpolation (lerp) of double's
+ */
+ static double LinearInterpolate(double aV1, double aV2, double aBlend);
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif
diff --git a/gfx/layers/BSPTree.cpp b/gfx/layers/BSPTree.cpp
new file mode 100644
index 0000000000..bcec125e4b
--- /dev/null
+++ b/gfx/layers/BSPTree.cpp
@@ -0,0 +1,133 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "BSPTree.h"
+#include "mozilla/gfx/Polygon.h"
+
+namespace mozilla {
+
+class nsDisplayTransform;
+
+namespace layers {
+
+template <typename T>
+void BSPTree<T>::BuildDrawOrder(BSPTreeNode<T>* aNode,
+ nsTArray<BSPPolygon<T>>& aLayers) const {
+ const gfx::Point4D& normal = aNode->First().GetNormal();
+
+ BSPTreeNode<T>* front = aNode->front;
+ BSPTreeNode<T>* back = aNode->back;
+
+ // Since the goal is to return the draw order from back to front, we reverse
+ // the traversal order if the current polygon is facing towards the camera.
+ const bool reverseOrder = normal.z > 0.0f;
+
+ if (reverseOrder) {
+ std::swap(front, back);
+ }
+
+ if (front) {
+ BuildDrawOrder(front, aLayers);
+ }
+
+ for (BSPPolygon<T>& layer : aNode->layers) {
+ MOZ_ASSERT(layer.geometry);
+
+ if (layer.geometry->GetPoints().Length() >= 3) {
+ aLayers.AppendElement(std::move(layer));
+ }
+ }
+
+ if (back) {
+ BuildDrawOrder(back, aLayers);
+ }
+}
+
+template <typename T>
+void BSPTree<T>::BuildTree(BSPTreeNode<T>* aRoot, PolygonList<T>& aLayers) {
+ MOZ_ASSERT(!aLayers.empty());
+
+ aRoot->layers.push_back(std::move(aLayers.front()));
+ aLayers.pop_front();
+
+ if (aLayers.empty()) {
+ return;
+ }
+
+ const gfx::Polygon& plane = aRoot->First();
+ MOZ_ASSERT(!plane.IsEmpty());
+
+ const gfx::Point4D& planeNormal = plane.GetNormal();
+ const gfx::Point4D& planePoint = plane.GetPoints()[0];
+
+ PolygonList<T> backLayers, frontLayers;
+ for (BSPPolygon<T>& layerPolygon : aLayers) {
+ const nsTArray<gfx::Point4D>& geometry = layerPolygon.geometry->GetPoints();
+
+ // Calculate the plane-point distances for the polygon classification.
+ size_t pos = 0, neg = 0;
+ nsTArray<float> distances = CalculatePointPlaneDistances(
+ geometry, planeNormal, planePoint, pos, neg);
+
+ // Back polygon
+ if (pos == 0 && neg > 0) {
+ backLayers.push_back(std::move(layerPolygon));
+ }
+ // Front polygon
+ else if (pos > 0 && neg == 0) {
+ frontLayers.push_back(std::move(layerPolygon));
+ }
+ // Coplanar polygon
+ else if (pos == 0 && neg == 0) {
+ aRoot->layers.push_back(std::move(layerPolygon));
+ }
+ // Polygon intersects with the splitting plane.
+ else if (pos > 0 && neg > 0) {
+ nsTArray<gfx::Point4D> backPoints, frontPoints;
+ // Clip the polygon against the plane. We reuse the previously calculated
+ // distances to find the plane-edge intersections.
+ ClipPointsWithPlane(geometry, planeNormal, distances, backPoints,
+ frontPoints);
+
+ const gfx::Point4D& normal = layerPolygon.geometry->GetNormal();
+ T* data = layerPolygon.data;
+
+ if (backPoints.Length() >= 3) {
+ backLayers.emplace_back(data, std::move(backPoints), normal);
+ }
+
+ if (frontPoints.Length() >= 3) {
+ frontLayers.emplace_back(data, std::move(frontPoints), normal);
+ }
+ }
+ }
+
+ if (!backLayers.empty()) {
+ aRoot->back = new (mPool) BSPTreeNode(mListPointers);
+ BuildTree(aRoot->back, backLayers);
+ }
+
+ if (!frontLayers.empty()) {
+ aRoot->front = new (mPool) BSPTreeNode(mListPointers);
+ BuildTree(aRoot->front, frontLayers);
+ }
+}
+
+template void BSPTree<BSPTestData>::BuildTree(
+ BSPTreeNode<BSPTestData>* aRoot, PolygonList<BSPTestData>& aLayers);
+template void BSPTree<BSPTestData>::BuildDrawOrder(
+ BSPTreeNode<BSPTestData>* aNode,
+ nsTArray<BSPPolygon<BSPTestData>>& aLayers) const;
+
+template void BSPTree<nsDisplayTransform>::BuildTree(
+ BSPTreeNode<nsDisplayTransform>* aRoot,
+ PolygonList<nsDisplayTransform>& aLayers);
+template void BSPTree<nsDisplayTransform>::BuildDrawOrder(
+ BSPTreeNode<nsDisplayTransform>* aNode,
+ nsTArray<BSPPolygon<nsDisplayTransform>>& aLayers) const;
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/BSPTree.h b/gfx/layers/BSPTree.h
new file mode 100644
index 0000000000..ce19272d4f
--- /dev/null
+++ b/gfx/layers/BSPTree.h
@@ -0,0 +1,144 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_LAYERS_BSPTREE_H
+#define MOZILLA_LAYERS_BSPTREE_H
+
+#include <list>
+#include <utility>
+
+#include "mozilla/ArenaAllocator.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/gfx/Polygon.h"
+#include "nsTArray.h"
+
+namespace mozilla {
+namespace layers {
+
+/**
+ * Represents a layer that might have a non-rectangular geometry.
+ */
+template <typename T>
+struct BSPPolygon {
+ explicit BSPPolygon(T* aData) : data(aData) {}
+
+ BSPPolygon(T* aData, gfx::Polygon&& aGeometry)
+ : data(aData), geometry(Some(std::move(aGeometry))) {}
+
+ BSPPolygon(T* aData, nsTArray<gfx::Point4D>&& aPoints,
+ const gfx::Point4D& aNormal)
+ : data(aData) {
+ geometry.emplace(std::move(aPoints), aNormal);
+ }
+
+ T* data;
+ Maybe<gfx::Polygon> geometry;
+};
+
+/**
+ * Allocate BSPTreeNodes from a memory arena to improve performance with
+ * complex scenes.
+ * The arena size of 4096 bytes was selected as an arbitrary power of two.
+ * Depending on the platform, this size accommodates roughly 100 BSPTreeNodes.
+ */
+typedef mozilla::ArenaAllocator<4096, 8> BSPTreeArena;
+
+/**
+ * Aliases the container type used to store layers within BSPTreeNodes.
+ */
+template <typename T>
+using PolygonList = std::list<BSPPolygon<T>>;
+
+// For tests. Needs to be defined here rather than in TestBSPTree.cpp because we
+// need to explicitly instantiate the out-of-line BSPTree methods for it in
+// BSPTree.cpp.
+class BSPTestData {};
+using TestPolygon = BSPPolygon<BSPTestData>;
+
+/**
+ * Represents a node in a BSP tree. The node contains at least one layer with
+ * associated geometry that is used as a splitting plane, and at most two child
+ * nodes that represent the splitting planes that further subdivide the space.
+ */
+template <typename T>
+struct BSPTreeNode {
+ explicit BSPTreeNode(nsTArray<PolygonList<T>*>& aListPointers)
+ : front(nullptr), back(nullptr) {
+ // Store the layer list pointer to free memory when BSPTree is destroyed.
+ aListPointers.AppendElement(&layers);
+ }
+
+ const gfx::Polygon& First() const {
+ MOZ_ASSERT(!layers.empty());
+ MOZ_ASSERT(layers.front().geometry);
+ return *layers.front().geometry;
+ }
+
+ static void* operator new(size_t aSize, BSPTreeArena& mPool) {
+ return mPool.Allocate(aSize);
+ }
+
+ BSPTreeNode* front;
+ BSPTreeNode* back;
+ PolygonList<T> layers;
+};
+
+/**
+ * BSPTree class takes a list of layers as an input and uses binary space
+ * partitioning algorithm to create a tree structure that can be used for
+ * depth sorting.
+
+ * Sources for more information:
+ * https://en.wikipedia.org/wiki/Binary_space_partitioning
+ * ftp://ftp.sgi.com/other/bspfaq/faq/bspfaq.html
+ */
+template <typename T>
+class BSPTree final {
+ public:
+ /**
+ * The constructor modifies layers in the given list.
+ */
+ explicit BSPTree(std::list<BSPPolygon<T>>& aLayers) {
+ MOZ_ASSERT(!aLayers.empty());
+
+ mRoot = new (mPool) BSPTreeNode(mListPointers);
+ BuildTree(mRoot, aLayers);
+ }
+
+ ~BSPTree() {
+ for (PolygonList<T>* listPtr : mListPointers) {
+ listPtr->~list();
+ }
+ }
+
+ /**
+ * Builds and returns the back-to-front draw order for the created BSP tree.
+ */
+ nsTArray<BSPPolygon<T>> GetDrawOrder() const {
+ nsTArray<BSPPolygon<T>> layers;
+ BuildDrawOrder(mRoot, layers);
+ return layers;
+ }
+
+ private:
+ BSPTreeArena mPool;
+ BSPTreeNode<T>* mRoot;
+ nsTArray<PolygonList<T>*> mListPointers;
+
+ /**
+ * BuildDrawOrder and BuildTree are called recursively. The depth of the
+ * recursion depends on the amount of polygons and their intersections.
+ */
+ void BuildDrawOrder(BSPTreeNode<T>* aNode,
+ nsTArray<BSPPolygon<T>>& aLayers) const;
+
+ void BuildTree(BSPTreeNode<T>* aRoot, PolygonList<T>& aLayers);
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif /* MOZILLA_LAYERS_BSPTREE_H */
diff --git a/gfx/layers/BufferTexture.cpp b/gfx/layers/BufferTexture.cpp
new file mode 100644
index 0000000000..a3cedc1840
--- /dev/null
+++ b/gfx/layers/BufferTexture.cpp
@@ -0,0 +1,533 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "BufferTexture.h"
+
+#include <utility>
+
+#include "libyuv.h"
+#include "mozilla/fallible.h"
+#include "mozilla/gfx/2D.h"
+#include "mozilla/gfx/Logging.h"
+#include "mozilla/layers/CompositableForwarder.h"
+#include "mozilla/layers/ISurfaceAllocator.h"
+#include "mozilla/layers/ImageDataSerializer.h"
+#include "mozilla/layers/TextureForwarder.h"
+
+#include "gfxPlatform.h"
+
+#ifdef MOZ_WIDGET_GTK
+# include "gfxPlatformGtk.h"
+#endif
+
+using mozilla::ipc::IShmemAllocator;
+
+namespace mozilla {
+namespace layers {
+
+class MemoryTextureData : public BufferTextureData {
+ public:
+ static MemoryTextureData* Create(gfx::IntSize aSize,
+ gfx::SurfaceFormat aFormat,
+ gfx::BackendType aMoz2DBackend,
+ LayersBackend aLayersBackend,
+ TextureFlags aFlags,
+ TextureAllocationFlags aAllocFlags,
+ IShmemAllocator* aAllocator);
+
+ virtual TextureData* CreateSimilar(
+ LayersIPCChannel* aAllocator, LayersBackend aLayersBackend,
+ TextureFlags aFlags = TextureFlags::DEFAULT,
+ TextureAllocationFlags aAllocFlags = ALLOC_DEFAULT) const override;
+
+ virtual bool Serialize(SurfaceDescriptor& aOutDescriptor) override;
+
+ virtual void Deallocate(LayersIPCChannel*) override;
+
+ MemoryTextureData(const BufferDescriptor& aDesc,
+ gfx::BackendType aMoz2DBackend, uint8_t* aBuffer,
+ size_t aBufferSize, bool aAutoDeallocate = false)
+ : BufferTextureData(aDesc, aMoz2DBackend),
+ mBuffer(aBuffer),
+ mBufferSize(aBufferSize),
+ mAutoDeallocate(aAutoDeallocate) {
+ MOZ_ASSERT(aBuffer);
+ MOZ_ASSERT(aBufferSize);
+ }
+
+ virtual ~MemoryTextureData() override {
+ if (mAutoDeallocate) {
+ Deallocate(nullptr);
+ }
+ }
+
+ virtual uint8_t* GetBuffer() override { return mBuffer; }
+
+ virtual size_t GetBufferSize() override { return mBufferSize; }
+
+ protected:
+ uint8_t* mBuffer;
+ size_t mBufferSize;
+ bool mAutoDeallocate;
+};
+
+class ShmemTextureData : public BufferTextureData {
+ public:
+ static ShmemTextureData* Create(gfx::IntSize aSize,
+ gfx::SurfaceFormat aFormat,
+ gfx::BackendType aMoz2DBackend,
+ LayersBackend aLayersBackend,
+ TextureFlags aFlags,
+ TextureAllocationFlags aAllocFlags,
+ IShmemAllocator* aAllocator);
+
+ virtual TextureData* CreateSimilar(
+ LayersIPCChannel* aAllocator, LayersBackend aLayersBackend,
+ TextureFlags aFlags = TextureFlags::DEFAULT,
+ TextureAllocationFlags aAllocFlags = ALLOC_DEFAULT) const override;
+
+ virtual bool Serialize(SurfaceDescriptor& aOutDescriptor) override;
+
+ virtual void Deallocate(LayersIPCChannel* aAllocator) override;
+
+ ShmemTextureData(const BufferDescriptor& aDesc,
+ gfx::BackendType aMoz2DBackend, mozilla::ipc::Shmem aShmem)
+ : BufferTextureData(aDesc, aMoz2DBackend), mShmem(aShmem) {
+ MOZ_ASSERT(mShmem.Size<uint8_t>());
+ }
+
+ virtual uint8_t* GetBuffer() override { return mShmem.get<uint8_t>(); }
+
+ virtual size_t GetBufferSize() override { return mShmem.Size<uint8_t>(); }
+
+ protected:
+ mozilla::ipc::Shmem mShmem;
+};
+
+BufferTextureData* BufferTextureData::Create(
+ gfx::IntSize aSize, gfx::SurfaceFormat aFormat,
+ gfx::BackendType aMoz2DBackend, LayersBackend aLayersBackend,
+ TextureFlags aFlags, TextureAllocationFlags aAllocFlags,
+ mozilla::ipc::IShmemAllocator* aAllocator, bool aIsSameProcess) {
+ if (!aAllocator || aIsSameProcess) {
+ return MemoryTextureData::Create(aSize, aFormat, aMoz2DBackend,
+ aLayersBackend, aFlags, aAllocFlags,
+ aAllocator);
+ } else {
+ return ShmemTextureData::Create(aSize, aFormat, aMoz2DBackend,
+ aLayersBackend, aFlags, aAllocFlags,
+ aAllocator);
+ }
+}
+
+BufferTextureData* BufferTextureData::CreateInternal(
+ LayersIPCChannel* aAllocator, const BufferDescriptor& aDesc,
+ gfx::BackendType aMoz2DBackend, int32_t aBufferSize,
+ TextureFlags aTextureFlags) {
+ if (!aAllocator || aAllocator->IsSameProcess()) {
+ uint8_t* buffer = new (fallible) uint8_t[aBufferSize];
+ if (!buffer) {
+ return nullptr;
+ }
+
+ GfxMemoryImageReporter::DidAlloc(buffer);
+
+ return new MemoryTextureData(aDesc, aMoz2DBackend, buffer, aBufferSize);
+ } else {
+ ipc::Shmem shm;
+ if (!aAllocator->AllocUnsafeShmem(aBufferSize, &shm)) {
+ return nullptr;
+ }
+
+ return new ShmemTextureData(aDesc, aMoz2DBackend, shm);
+ }
+}
+
+BufferTextureData* BufferTextureData::CreateForYCbCr(
+ KnowsCompositor* aAllocator, const gfx::IntRect& aDisplay,
+ const gfx::IntSize& aYSize, uint32_t aYStride,
+ const gfx::IntSize& aCbCrSize, uint32_t aCbCrStride, StereoMode aStereoMode,
+ gfx::ColorDepth aColorDepth, gfx::YUVColorSpace aYUVColorSpace,
+ gfx::ColorRange aColorRange, gfx::ChromaSubsampling aSubsampling,
+ TextureFlags aTextureFlags) {
+ uint32_t bufSize = ImageDataSerializer::ComputeYCbCrBufferSize(
+ aYSize, aYStride, aCbCrSize, aCbCrStride);
+ if (bufSize == 0) {
+ return nullptr;
+ }
+
+ uint32_t yOffset;
+ uint32_t cbOffset;
+ uint32_t crOffset;
+ ImageDataSerializer::ComputeYCbCrOffsets(aYStride, aYSize.height, aCbCrStride,
+ aCbCrSize.height, yOffset, cbOffset,
+ crOffset);
+
+ YCbCrDescriptor descriptor =
+ YCbCrDescriptor(aDisplay, aYSize, aYStride, aCbCrSize, aCbCrStride,
+ yOffset, cbOffset, crOffset, aStereoMode, aColorDepth,
+ aYUVColorSpace, aColorRange, aSubsampling);
+
+ return CreateInternal(
+ aAllocator ? aAllocator->GetTextureForwarder() : nullptr, descriptor,
+ gfx::BackendType::NONE, bufSize, aTextureFlags);
+}
+
+void BufferTextureData::FillInfo(TextureData::Info& aInfo) const {
+ aInfo.size = GetSize();
+ aInfo.format = GetFormat();
+ aInfo.hasSynchronization = false;
+ aInfo.canExposeMappedData = true;
+
+ switch (aInfo.format) {
+ case gfx::SurfaceFormat::YUV:
+ case gfx::SurfaceFormat::UNKNOWN:
+ aInfo.supportsMoz2D = false;
+ break;
+ default:
+ aInfo.supportsMoz2D = true;
+ }
+}
+
+gfx::IntSize BufferTextureData::GetSize() const {
+ return ImageDataSerializer::SizeFromBufferDescriptor(mDescriptor);
+}
+
+gfx::IntRect BufferTextureData::GetPictureRect() const {
+ return ImageDataSerializer::RectFromBufferDescriptor(mDescriptor);
+}
+
+Maybe<gfx::IntSize> BufferTextureData::GetYSize() const {
+ return ImageDataSerializer::YSizeFromBufferDescriptor(mDescriptor);
+}
+
+Maybe<gfx::IntSize> BufferTextureData::GetCbCrSize() const {
+ return ImageDataSerializer::CbCrSizeFromBufferDescriptor(mDescriptor);
+}
+
+Maybe<int32_t> BufferTextureData::GetYStride() const {
+ return ImageDataSerializer::YStrideFromBufferDescriptor(mDescriptor);
+}
+
+Maybe<int32_t> BufferTextureData::GetCbCrStride() const {
+ return ImageDataSerializer::CbCrStrideFromBufferDescriptor(mDescriptor);
+}
+
+Maybe<gfx::YUVColorSpace> BufferTextureData::GetYUVColorSpace() const {
+ return ImageDataSerializer::YUVColorSpaceFromBufferDescriptor(mDescriptor);
+}
+
+Maybe<gfx::ColorDepth> BufferTextureData::GetColorDepth() const {
+ return ImageDataSerializer::ColorDepthFromBufferDescriptor(mDescriptor);
+}
+
+Maybe<StereoMode> BufferTextureData::GetStereoMode() const {
+ return ImageDataSerializer::StereoModeFromBufferDescriptor(mDescriptor);
+}
+
+Maybe<gfx::ChromaSubsampling> BufferTextureData::GetChromaSubsampling() const {
+ return ImageDataSerializer::ChromaSubsamplingFromBufferDescriptor(
+ mDescriptor);
+}
+
+gfx::SurfaceFormat BufferTextureData::GetFormat() const {
+ return ImageDataSerializer::FormatFromBufferDescriptor(mDescriptor);
+}
+
+already_AddRefed<gfx::DrawTarget> BufferTextureData::BorrowDrawTarget() {
+ if (mDescriptor.type() != BufferDescriptor::TRGBDescriptor) {
+ return nullptr;
+ }
+
+ const RGBDescriptor& rgb = mDescriptor.get_RGBDescriptor();
+
+ uint32_t stride = ImageDataSerializer::GetRGBStride(rgb);
+ RefPtr<gfx::DrawTarget> dt;
+ if (gfx::Factory::DoesBackendSupportDataDrawtarget(mMoz2DBackend)) {
+ dt = gfx::Factory::CreateDrawTargetForData(
+ mMoz2DBackend, GetBuffer(), rgb.size(), stride, rgb.format(), true);
+ }
+ if (!dt) {
+ // Fall back to supported platform backend. Note that mMoz2DBackend
+ // does not match the draw target type.
+ dt = gfxPlatform::CreateDrawTargetForData(GetBuffer(), rgb.size(), stride,
+ rgb.format(), true);
+ }
+
+ if (!dt) {
+ gfxCriticalNote << "BorrowDrawTarget failure, original backend "
+ << (int)mMoz2DBackend;
+ }
+
+ return dt.forget();
+}
+
+bool BufferTextureData::BorrowMappedData(MappedTextureData& aData) {
+ if (GetFormat() == gfx::SurfaceFormat::YUV) {
+ return false;
+ }
+
+ gfx::IntSize size = GetSize();
+
+ aData.data = GetBuffer();
+ aData.size = size;
+ aData.format = GetFormat();
+ aData.stride =
+ ImageDataSerializer::ComputeRGBStride(aData.format, size.width);
+
+ return true;
+}
+
+bool BufferTextureData::BorrowMappedYCbCrData(MappedYCbCrTextureData& aMap) {
+ if (mDescriptor.type() != BufferDescriptor::TYCbCrDescriptor) {
+ return false;
+ }
+
+ const YCbCrDescriptor& desc = mDescriptor.get_YCbCrDescriptor();
+
+ uint8_t* data = GetBuffer();
+ auto ySize = desc.ySize();
+ auto cbCrSize = desc.cbCrSize();
+
+ aMap.stereoMode = desc.stereoMode();
+ aMap.metadata = nullptr;
+ uint32_t bytesPerPixel =
+ BytesPerPixel(SurfaceFormatForColorDepth(desc.colorDepth()));
+
+ aMap.y.data = data + desc.yOffset();
+ aMap.y.size = ySize;
+ aMap.y.stride = desc.yStride();
+ aMap.y.skip = 0;
+ aMap.y.bytesPerPixel = bytesPerPixel;
+
+ aMap.cb.data = data + desc.cbOffset();
+ aMap.cb.size = cbCrSize;
+ aMap.cb.stride = desc.cbCrStride();
+ aMap.cb.skip = 0;
+ aMap.cb.bytesPerPixel = bytesPerPixel;
+
+ aMap.cr.data = data + desc.crOffset();
+ aMap.cr.size = cbCrSize;
+ aMap.cr.stride = desc.cbCrStride();
+ aMap.cr.skip = 0;
+ aMap.cr.bytesPerPixel = bytesPerPixel;
+
+ return true;
+}
+
+bool BufferTextureData::UpdateFromSurface(gfx::SourceSurface* aSurface) {
+ if (mDescriptor.type() != BufferDescriptor::TRGBDescriptor) {
+ return false;
+ }
+ const RGBDescriptor& rgb = mDescriptor.get_RGBDescriptor();
+
+ uint32_t stride = ImageDataSerializer::GetRGBStride(rgb);
+ RefPtr<gfx::DataSourceSurface> surface =
+ gfx::Factory::CreateWrappingDataSourceSurface(GetBuffer(), stride,
+ rgb.size(), rgb.format());
+
+ if (!surface) {
+ gfxCriticalError() << "Failed to get serializer as surface!";
+ return false;
+ }
+
+ RefPtr<gfx::DataSourceSurface> srcSurf = aSurface->GetDataSurface();
+
+ if (!srcSurf) {
+ gfxCriticalError() << "Failed to GetDataSurface in UpdateFromSurface (BT).";
+ return false;
+ }
+
+ if (surface->GetSize() != srcSurf->GetSize() ||
+ surface->GetFormat() != srcSurf->GetFormat()) {
+ gfxCriticalError() << "Attempt to update texture client from a surface "
+ "with a different size or format (BT)! This: "
+ << surface->GetSize() << " " << surface->GetFormat()
+ << " Other: " << aSurface->GetSize() << " "
+ << aSurface->GetFormat();
+ return false;
+ }
+
+ gfx::DataSourceSurface::MappedSurface sourceMap;
+ gfx::DataSourceSurface::MappedSurface destMap;
+ if (!srcSurf->Map(gfx::DataSourceSurface::READ, &sourceMap)) {
+ gfxCriticalError()
+ << "Failed to map source surface for UpdateFromSurface (BT).";
+ return false;
+ }
+
+ if (!surface->Map(gfx::DataSourceSurface::WRITE, &destMap)) {
+ srcSurf->Unmap();
+ gfxCriticalError()
+ << "Failed to map destination surface for UpdateFromSurface.";
+ return false;
+ }
+
+ for (int y = 0; y < srcSurf->GetSize().height; y++) {
+ memcpy(destMap.mData + destMap.mStride * y,
+ sourceMap.mData + sourceMap.mStride * y,
+ srcSurf->GetSize().width * BytesPerPixel(srcSurf->GetFormat()));
+ }
+
+ srcSurf->Unmap();
+ surface->Unmap();
+
+ return true;
+}
+
+bool MemoryTextureData::Serialize(SurfaceDescriptor& aOutDescriptor) {
+ MOZ_ASSERT(GetFormat() != gfx::SurfaceFormat::UNKNOWN);
+ if (GetFormat() == gfx::SurfaceFormat::UNKNOWN) {
+ return false;
+ }
+
+ uintptr_t ptr = reinterpret_cast<uintptr_t>(mBuffer);
+ aOutDescriptor = SurfaceDescriptorBuffer(mDescriptor, MemoryOrShmem(ptr));
+
+ return true;
+}
+
+static bool InitBuffer(uint8_t* buf, size_t bufSize, gfx::SurfaceFormat aFormat,
+ TextureAllocationFlags aAllocFlags, bool aAlreadyZero) {
+ if (!buf) {
+ gfxDebug() << "BufferTextureData: Failed to allocate " << bufSize
+ << " bytes";
+ return false;
+ }
+
+ if (aAllocFlags & ALLOC_CLEAR_BUFFER) {
+ if (aFormat == gfx::SurfaceFormat::B8G8R8X8) {
+ // Even though BGRX was requested, XRGB_UINT32 is what is meant,
+ // so use 0xFF000000 to put alpha in the right place.
+ libyuv::ARGBRect(buf, bufSize, 0, 0, bufSize / sizeof(uint32_t), 1,
+ 0xFF000000);
+ } else if (!aAlreadyZero) {
+ memset(buf, 0, bufSize);
+ }
+ }
+
+ return true;
+}
+
+MemoryTextureData* MemoryTextureData::Create(gfx::IntSize aSize,
+ gfx::SurfaceFormat aFormat,
+ gfx::BackendType aMoz2DBackend,
+ LayersBackend aLayersBackend,
+ TextureFlags aFlags,
+ TextureAllocationFlags aAllocFlags,
+ IShmemAllocator* aAllocator) {
+ // Should have used CreateForYCbCr.
+ MOZ_ASSERT(aFormat != gfx::SurfaceFormat::YUV);
+
+ if (aSize.width <= 0 || aSize.height <= 0) {
+ gfxDebug() << "Asking for buffer of invalid size " << aSize.width << "x"
+ << aSize.height;
+ return nullptr;
+ }
+
+ uint32_t bufSize = ImageDataSerializer::ComputeRGBBufferSize(aSize, aFormat);
+ if (!bufSize) {
+ return nullptr;
+ }
+
+ uint8_t* buf = new (fallible) uint8_t[bufSize];
+ if (!InitBuffer(buf, bufSize, aFormat, aAllocFlags, false)) {
+ return nullptr;
+ }
+
+ GfxMemoryImageReporter::DidAlloc(buf);
+
+ BufferDescriptor descriptor = RGBDescriptor(aSize, aFormat);
+
+ // Remote textures are not managed by a texture client, so we need to ensure
+ // that memory is freed when the owning MemoryTextureData goes away.
+ bool autoDeallocate = !!(aFlags & TextureFlags::REMOTE_TEXTURE);
+ return new MemoryTextureData(descriptor, aMoz2DBackend, buf, bufSize,
+ autoDeallocate);
+}
+
+void MemoryTextureData::Deallocate(LayersIPCChannel*) {
+ MOZ_ASSERT(mBuffer);
+ GfxMemoryImageReporter::WillFree(mBuffer);
+ delete[] mBuffer;
+ mBuffer = nullptr;
+}
+
+TextureData* MemoryTextureData::CreateSimilar(
+ LayersIPCChannel* aAllocator, LayersBackend aLayersBackend,
+ TextureFlags aFlags, TextureAllocationFlags aAllocFlags) const {
+ return MemoryTextureData::Create(GetSize(), GetFormat(), mMoz2DBackend,
+ aLayersBackend, aFlags, aAllocFlags,
+ aAllocator);
+}
+
+bool ShmemTextureData::Serialize(SurfaceDescriptor& aOutDescriptor) {
+ MOZ_ASSERT(GetFormat() != gfx::SurfaceFormat::UNKNOWN);
+ if (GetFormat() == gfx::SurfaceFormat::UNKNOWN) {
+ return false;
+ }
+
+ aOutDescriptor =
+ SurfaceDescriptorBuffer(mDescriptor, MemoryOrShmem(std::move(mShmem)));
+
+ return true;
+}
+
+ShmemTextureData* ShmemTextureData::Create(gfx::IntSize aSize,
+ gfx::SurfaceFormat aFormat,
+ gfx::BackendType aMoz2DBackend,
+ LayersBackend aLayersBackend,
+ TextureFlags aFlags,
+ TextureAllocationFlags aAllocFlags,
+ IShmemAllocator* aAllocator) {
+ MOZ_ASSERT(aAllocator);
+ // Should have used CreateForYCbCr.
+ MOZ_ASSERT(aFormat != gfx::SurfaceFormat::YUV);
+
+ if (!aAllocator) {
+ return nullptr;
+ }
+
+ if (aSize.width <= 0 || aSize.height <= 0) {
+ gfxDebug() << "Asking for buffer of invalid size " << aSize.width << "x"
+ << aSize.height;
+ return nullptr;
+ }
+
+ uint32_t bufSize = ImageDataSerializer::ComputeRGBBufferSize(aSize, aFormat);
+ if (!bufSize) {
+ return nullptr;
+ }
+
+ mozilla::ipc::Shmem shm;
+ if (!aAllocator->AllocUnsafeShmem(bufSize, &shm)) {
+ return nullptr;
+ }
+
+ uint8_t* buf = shm.get<uint8_t>();
+ if (!InitBuffer(buf, bufSize, aFormat, aAllocFlags, true)) {
+ return nullptr;
+ }
+
+ BufferDescriptor descriptor = RGBDescriptor(aSize, aFormat);
+
+ return new ShmemTextureData(descriptor, aMoz2DBackend, shm);
+}
+
+TextureData* ShmemTextureData::CreateSimilar(
+ LayersIPCChannel* aAllocator, LayersBackend aLayersBackend,
+ TextureFlags aFlags, TextureAllocationFlags aAllocFlags) const {
+ return ShmemTextureData::Create(GetSize(), GetFormat(), mMoz2DBackend,
+ aLayersBackend, aFlags, aAllocFlags,
+ aAllocator);
+}
+
+void ShmemTextureData::Deallocate(LayersIPCChannel* aAllocator) {
+ aAllocator->DeallocShmem(mShmem);
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/BufferTexture.h b/gfx/layers/BufferTexture.h
new file mode 100644
index 0000000000..a306e4dfeb
--- /dev/null
+++ b/gfx/layers/BufferTexture.h
@@ -0,0 +1,129 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_LAYERS_BUFFERETEXTURE
+#define MOZILLA_LAYERS_BUFFERETEXTURE
+
+#include "mozilla/RefPtr.h"
+#include "mozilla/gfx/2D.h"
+#include "mozilla/gfx/Types.h"
+#include "mozilla/ipc/SharedMemory.h"
+#include "mozilla/layers/TextureClient.h"
+
+namespace mozilla {
+namespace layers {
+
+class BufferTextureData : public TextureData {
+ public:
+ // ShmemAllocator needs to implement IShmemAllocator and IsSameProcess,
+ // as done in LayersIPCChannel and ISurfaceAllocator.
+ template <typename ShmemAllocator>
+ static BufferTextureData* Create(gfx::IntSize aSize,
+ gfx::SurfaceFormat aFormat,
+ gfx::BackendType aMoz2DBackend,
+ LayersBackend aLayersBackend,
+ TextureFlags aFlags,
+ TextureAllocationFlags aAllocFlags,
+ ShmemAllocator aAllocator);
+
+ static BufferTextureData* CreateForYCbCr(
+ KnowsCompositor* aAllocator, const gfx::IntRect& aDisplay,
+ const gfx::IntSize& aYSize, uint32_t aYStride,
+ const gfx::IntSize& aCbCrSize, uint32_t aCbCrStride,
+ StereoMode aStereoMode, gfx::ColorDepth aColorDepth,
+ gfx::YUVColorSpace aYUVColorSpace, gfx::ColorRange aColorRange,
+ gfx::ChromaSubsampling aSubsampling, TextureFlags aTextureFlags);
+
+ bool Lock(OpenMode aMode) override { return true; }
+
+ void Unlock() override {}
+
+ void FillInfo(TextureData::Info& aInfo) const override;
+
+ already_AddRefed<gfx::DrawTarget> BorrowDrawTarget() override;
+
+ bool BorrowMappedData(MappedTextureData& aMap) override;
+
+ bool BorrowMappedYCbCrData(MappedYCbCrTextureData& aMap) override;
+
+ // use TextureClient's default implementation
+ bool UpdateFromSurface(gfx::SourceSurface* aSurface) override;
+
+ BufferTextureData* AsBufferTextureData() override { return this; }
+
+ Maybe<gfx::IntSize> GetYSize() const;
+
+ Maybe<gfx::IntSize> GetCbCrSize() const;
+
+ Maybe<int32_t> GetYStride() const;
+
+ Maybe<int32_t> GetCbCrStride() const;
+
+ Maybe<gfx::YUVColorSpace> GetYUVColorSpace() const;
+
+ Maybe<gfx::ColorDepth> GetColorDepth() const;
+
+ Maybe<StereoMode> GetStereoMode() const;
+
+ Maybe<gfx::ChromaSubsampling> GetChromaSubsampling() const;
+
+ gfx::IntRect GetPictureRect() const;
+
+ gfx::IntSize GetSize() const;
+
+ gfx::SurfaceFormat GetFormat() const;
+
+ virtual size_t GetBufferSize() = 0;
+
+ protected:
+ static BufferTextureData* Create(
+ gfx::IntSize aSize, gfx::SurfaceFormat aFormat,
+ gfx::BackendType aMoz2DBackend, LayersBackend aLayersBackend,
+ TextureFlags aFlags, TextureAllocationFlags aAllocFlags,
+ mozilla::ipc::IShmemAllocator* aAllocator, bool aIsSameProcess);
+
+ static BufferTextureData* CreateInternal(LayersIPCChannel* aAllocator,
+ const BufferDescriptor& aDesc,
+ gfx::BackendType aMoz2DBackend,
+ int32_t aBufferSize,
+ TextureFlags aTextureFlags);
+
+ virtual uint8_t* GetBuffer() = 0;
+
+ BufferTextureData(const BufferDescriptor& aDescriptor,
+ gfx::BackendType aMoz2DBackend)
+ : mDescriptor(aDescriptor), mMoz2DBackend(aMoz2DBackend) {}
+
+ ~BufferTextureData() override = default;
+
+ BufferDescriptor mDescriptor;
+ gfx::BackendType mMoz2DBackend;
+};
+
+template <typename ShmemAllocator>
+inline BufferTextureData* BufferTextureData::Create(
+ gfx::IntSize aSize, gfx::SurfaceFormat aFormat,
+ gfx::BackendType aMoz2DBackend, LayersBackend aLayersBackend,
+ TextureFlags aFlags, TextureAllocationFlags aAllocFlags,
+ ShmemAllocator aAllocator) {
+ return Create(aSize, aFormat, aMoz2DBackend, aLayersBackend, aFlags,
+ aAllocFlags, aAllocator, aAllocator->IsSameProcess());
+}
+
+// nullptr allocator specialization
+template <>
+inline BufferTextureData* BufferTextureData::Create(
+ gfx::IntSize aSize, gfx::SurfaceFormat aFormat,
+ gfx::BackendType aMoz2DBackend, LayersBackend aLayersBackend,
+ TextureFlags aFlags, TextureAllocationFlags aAllocFlags, std::nullptr_t) {
+ return Create(aSize, aFormat, aMoz2DBackend, aLayersBackend, aFlags,
+ aAllocFlags, nullptr, true);
+}
+
+} // namespace layers
+} // namespace mozilla
+
+#endif
diff --git a/gfx/layers/BuildConstants.h b/gfx/layers/BuildConstants.h
new file mode 100644
index 0000000000..4478ce8da1
--- /dev/null
+++ b/gfx/layers/BuildConstants.h
@@ -0,0 +1,64 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef BUILD_CONSTANTS_H_
+#define BUILD_CONSTANTS_H_
+
+/**
+ * Why not just use ifdefs?
+ * ifdefs tend to result in code that compiles on one platform but not another.
+ * Given the number of build and platform configurations we have, it's best to
+ * aim to compile the same on as many platforms as possible, and let the
+ * compiler see the constexprs and handle dead-code elision itself.
+ */
+
+namespace mozilla {
+
+constexpr bool kIsDebug =
+#ifdef DEBUG
+ true;
+#else
+ false;
+#endif
+
+constexpr bool kIsWindows =
+#ifdef XP_WIN
+ true;
+#else
+ false;
+#endif
+
+constexpr bool kIsMacOS =
+#ifdef XP_MACOSX
+ true;
+#else
+ false;
+#endif
+
+constexpr bool kIsX11 =
+#ifdef MOZ_X11
+ true;
+#else
+ false;
+#endif
+
+constexpr bool kIsWayland =
+#ifdef MOZ_WAYLAND
+ true;
+#else
+ false;
+#endif
+
+constexpr bool kIsAndroid =
+#ifdef MOZ_WIDGET_ANDROID
+ true;
+#else
+ false;
+#endif
+
+} // namespace mozilla
+
+#endif // BUILD_CONSTANTS_H_
diff --git a/gfx/layers/CanvasDrawEventRecorder.cpp b/gfx/layers/CanvasDrawEventRecorder.cpp
new file mode 100644
index 0000000000..e797ad72aa
--- /dev/null
+++ b/gfx/layers/CanvasDrawEventRecorder.cpp
@@ -0,0 +1,518 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "CanvasDrawEventRecorder.h"
+
+#include <string.h>
+
+#include "mozilla/layers/SharedSurfacesChild.h"
+#include "nsThreadUtils.h"
+
+namespace mozilla {
+namespace layers {
+
+static const int32_t kCheckpointEventType = -1;
+static const uint32_t kMaxSpinCount = 200;
+
+static const TimeDuration kTimeout = TimeDuration::FromMilliseconds(100);
+static const int32_t kTimeoutRetryCount = 50;
+
+static const uint32_t kCacheLineSize = 64;
+static const uint32_t kStreamSize = 64 * 1024;
+static const uint32_t kShmemSize = kStreamSize + (2 * kCacheLineSize);
+
+static_assert((static_cast<uint64_t>(UINT32_MAX) + 1) % kStreamSize == 0,
+ "kStreamSize must be a power of two.");
+
+bool CanvasEventRingBuffer::InitWriter(
+ base::ProcessId aOtherPid, ipc::SharedMemoryBasic::Handle* aReadHandle,
+ CrossProcessSemaphoreHandle* aReaderSem,
+ CrossProcessSemaphoreHandle* aWriterSem,
+ UniquePtr<WriterServices> aWriterServices) {
+ mSharedMemory = MakeAndAddRef<ipc::SharedMemoryBasic>();
+ if (NS_WARN_IF(!mSharedMemory->Create(kShmemSize)) ||
+ NS_WARN_IF(!mSharedMemory->Map(kShmemSize))) {
+ return false;
+ }
+
+ *aReadHandle = mSharedMemory->CloneHandle();
+ if (NS_WARN_IF(!*aReadHandle)) {
+ return false;
+ }
+
+ mSharedMemory->CloseHandle();
+
+ mBuf = static_cast<char*>(mSharedMemory->memory());
+ mBufPos = mBuf;
+ mAvailable = kStreamSize;
+
+ static_assert(sizeof(ReadFooter) <= kCacheLineSize,
+ "ReadFooter must fit in kCacheLineSize.");
+ mRead = reinterpret_cast<ReadFooter*>(mBuf + kStreamSize);
+ mRead->count = 0;
+ mRead->returnCount = 0;
+ mRead->state = State::Processing;
+
+ static_assert(sizeof(WriteFooter) <= kCacheLineSize,
+ "WriteFooter must fit in kCacheLineSize.");
+ mWrite = reinterpret_cast<WriteFooter*>(mBuf + kStreamSize + kCacheLineSize);
+ mWrite->count = 0;
+ mWrite->returnCount = 0;
+ mWrite->requiredDifference = 0;
+ mWrite->state = State::Processing;
+
+ mReaderSemaphore.reset(
+ CrossProcessSemaphore::Create("SharedMemoryStreamParent", 0));
+ *aReaderSem = mReaderSemaphore->CloneHandle();
+ mReaderSemaphore->CloseHandle();
+ if (!IsHandleValid(*aReaderSem)) {
+ return false;
+ }
+ mWriterSemaphore.reset(
+ CrossProcessSemaphore::Create("SharedMemoryStreamChild", 0));
+ *aWriterSem = mWriterSemaphore->CloneHandle();
+ mWriterSemaphore->CloseHandle();
+ if (!IsHandleValid(*aWriterSem)) {
+ return false;
+ }
+
+ mWriterServices = std::move(aWriterServices);
+
+ mGood = true;
+ return true;
+}
+
+bool CanvasEventRingBuffer::InitReader(
+ ipc::SharedMemoryBasic::Handle aReadHandle,
+ CrossProcessSemaphoreHandle aReaderSem,
+ CrossProcessSemaphoreHandle aWriterSem,
+ UniquePtr<ReaderServices> aReaderServices) {
+ mSharedMemory = MakeAndAddRef<ipc::SharedMemoryBasic>();
+ if (NS_WARN_IF(!mSharedMemory->SetHandle(
+ std::move(aReadHandle), ipc::SharedMemory::RightsReadWrite)) ||
+ NS_WARN_IF(!mSharedMemory->Map(kShmemSize))) {
+ return false;
+ }
+
+ mSharedMemory->CloseHandle();
+
+ mBuf = static_cast<char*>(mSharedMemory->memory());
+ mRead = reinterpret_cast<ReadFooter*>(mBuf + kStreamSize);
+ mWrite = reinterpret_cast<WriteFooter*>(mBuf + kStreamSize + kCacheLineSize);
+ mReaderSemaphore.reset(CrossProcessSemaphore::Create(std::move(aReaderSem)));
+ mReaderSemaphore->CloseHandle();
+ mWriterSemaphore.reset(CrossProcessSemaphore::Create(std::move(aWriterSem)));
+ mWriterSemaphore->CloseHandle();
+
+ mReaderServices = std::move(aReaderServices);
+
+ mGood = true;
+ return true;
+}
+
+bool CanvasEventRingBuffer::WaitForAndRecalculateAvailableSpace() {
+ if (!good()) {
+ return false;
+ }
+
+ uint32_t bufPos = mOurCount % kStreamSize;
+ uint32_t maxToWrite = kStreamSize - bufPos;
+ mAvailable = std::min(maxToWrite, WaitForBytesToWrite());
+ if (!mAvailable) {
+ mBufPos = nullptr;
+ return false;
+ }
+
+ mBufPos = mBuf + bufPos;
+ return true;
+}
+
+void CanvasEventRingBuffer::write(const char* const aData, const size_t aSize) {
+ const char* curDestPtr = aData;
+ size_t remainingToWrite = aSize;
+ if (remainingToWrite > mAvailable) {
+ if (!WaitForAndRecalculateAvailableSpace()) {
+ return;
+ }
+ }
+
+ if (remainingToWrite <= mAvailable) {
+ memcpy(mBufPos, curDestPtr, remainingToWrite);
+ UpdateWriteTotalsBy(remainingToWrite);
+ return;
+ }
+
+ do {
+ memcpy(mBufPos, curDestPtr, mAvailable);
+ IncrementWriteCountBy(mAvailable);
+ curDestPtr += mAvailable;
+ remainingToWrite -= mAvailable;
+ if (!WaitForAndRecalculateAvailableSpace()) {
+ return;
+ }
+ } while (remainingToWrite > mAvailable);
+
+ memcpy(mBufPos, curDestPtr, remainingToWrite);
+ UpdateWriteTotalsBy(remainingToWrite);
+}
+
+void CanvasEventRingBuffer::IncrementWriteCountBy(uint32_t aCount) {
+ mOurCount += aCount;
+ mWrite->count = mOurCount;
+ if (mRead->state != State::Processing) {
+ CheckAndSignalReader();
+ }
+}
+
+void CanvasEventRingBuffer::UpdateWriteTotalsBy(uint32_t aCount) {
+ IncrementWriteCountBy(aCount);
+ mBufPos += aCount;
+ mAvailable -= aCount;
+}
+
+bool CanvasEventRingBuffer::WaitForAndRecalculateAvailableData() {
+ if (!good()) {
+ return false;
+ }
+
+ uint32_t bufPos = mOurCount % kStreamSize;
+ uint32_t maxToRead = kStreamSize - bufPos;
+ mAvailable = std::min(maxToRead, WaitForBytesToRead());
+ if (!mAvailable) {
+ SetIsBad();
+ mBufPos = nullptr;
+ return false;
+ }
+
+ mBufPos = mBuf + bufPos;
+ return true;
+}
+
+void CanvasEventRingBuffer::read(char* const aOut, const size_t aSize) {
+ char* curSrcPtr = aOut;
+ size_t remainingToRead = aSize;
+ if (remainingToRead > mAvailable) {
+ if (!WaitForAndRecalculateAvailableData()) {
+ return;
+ }
+ }
+
+ if (remainingToRead <= mAvailable) {
+ memcpy(curSrcPtr, mBufPos, remainingToRead);
+ UpdateReadTotalsBy(remainingToRead);
+ return;
+ }
+
+ do {
+ memcpy(curSrcPtr, mBufPos, mAvailable);
+ IncrementReadCountBy(mAvailable);
+ curSrcPtr += mAvailable;
+ remainingToRead -= mAvailable;
+ if (!WaitForAndRecalculateAvailableData()) {
+ return;
+ }
+ } while (remainingToRead > mAvailable);
+
+ memcpy(curSrcPtr, mBufPos, remainingToRead);
+ UpdateReadTotalsBy(remainingToRead);
+}
+
+void CanvasEventRingBuffer::IncrementReadCountBy(uint32_t aCount) {
+ mOurCount += aCount;
+ mRead->count = mOurCount;
+ if (mWrite->state != State::Processing) {
+ CheckAndSignalWriter();
+ }
+}
+
+void CanvasEventRingBuffer::UpdateReadTotalsBy(uint32_t aCount) {
+ IncrementReadCountBy(aCount);
+ mBufPos += aCount;
+ mAvailable -= aCount;
+}
+
+void CanvasEventRingBuffer::CheckAndSignalReader() {
+ do {
+ switch (mRead->state) {
+ case State::Processing:
+ case State::Failed:
+ return;
+ case State::AboutToWait:
+ // The reader is making a decision about whether to wait. So, we must
+ // wait until it has decided to avoid races. Check if the reader is
+ // closed to avoid hangs.
+ if (mWriterServices->ReaderClosed()) {
+ return;
+ }
+ continue;
+ case State::Waiting:
+ if (mRead->count != mOurCount) {
+ // We have to use compareExchange here because the reader can change
+ // from Waiting to Stopped.
+ if (mRead->state.compareExchange(State::Waiting, State::Processing)) {
+ mReaderSemaphore->Signal();
+ return;
+ }
+
+ MOZ_ASSERT(mRead->state == State::Stopped);
+ continue;
+ }
+ return;
+ case State::Stopped:
+ if (mRead->count != mOurCount) {
+ mRead->state = State::Processing;
+ mWriterServices->ResumeReader();
+ }
+ return;
+ default:
+ MOZ_ASSERT_UNREACHABLE("Invalid waiting state.");
+ return;
+ }
+ } while (true);
+}
+
+bool CanvasEventRingBuffer::HasDataToRead() {
+ return (mWrite->count != mOurCount);
+}
+
+bool CanvasEventRingBuffer::StopIfEmpty() {
+ // Double-check that the writer isn't waiting.
+ CheckAndSignalWriter();
+ mRead->state = State::AboutToWait;
+ if (HasDataToRead()) {
+ mRead->state = State::Processing;
+ return false;
+ }
+
+ mRead->state = State::Stopped;
+ return true;
+}
+
+bool CanvasEventRingBuffer::WaitForDataToRead(TimeDuration aTimeout,
+ int32_t aRetryCount) {
+ uint32_t spinCount = kMaxSpinCount;
+ do {
+ if (HasDataToRead()) {
+ return true;
+ }
+ } while (--spinCount != 0);
+
+ // Double-check that the writer isn't waiting.
+ CheckAndSignalWriter();
+ mRead->state = State::AboutToWait;
+ if (HasDataToRead()) {
+ mRead->state = State::Processing;
+ return true;
+ }
+
+ mRead->state = State::Waiting;
+ do {
+ if (mReaderSemaphore->Wait(Some(aTimeout))) {
+ MOZ_RELEASE_ASSERT(HasDataToRead());
+ return true;
+ }
+
+ if (mReaderServices->WriterClosed()) {
+ // Something has gone wrong on the writing side, just return false so
+ // that we can hopefully recover.
+ return false;
+ }
+ } while (aRetryCount-- > 0);
+
+ // We have to use compareExchange here because the writer can change our
+ // state if we are waiting. signaled
+ if (!mRead->state.compareExchange(State::Waiting, State::Stopped)) {
+ MOZ_RELEASE_ASSERT(HasDataToRead());
+ MOZ_RELEASE_ASSERT(mRead->state == State::Processing);
+ // The writer has just signaled us, so consume it before returning
+ MOZ_ALWAYS_TRUE(mReaderSemaphore->Wait());
+ return true;
+ }
+
+ return false;
+}
+
+int32_t CanvasEventRingBuffer::ReadNextEvent() {
+ int32_t nextEvent;
+ ReadElement(*this, nextEvent);
+ while (nextEvent == kCheckpointEventType && good()) {
+ ReadElement(*this, nextEvent);
+ }
+
+ return nextEvent;
+}
+
+uint32_t CanvasEventRingBuffer::CreateCheckpoint() {
+ WriteElement(*this, kCheckpointEventType);
+ return mOurCount;
+}
+
+bool CanvasEventRingBuffer::WaitForCheckpoint(uint32_t aCheckpoint) {
+ return WaitForReadCount(aCheckpoint, kTimeout);
+}
+
+void CanvasEventRingBuffer::CheckAndSignalWriter() {
+ do {
+ switch (mWrite->state) {
+ case State::Processing:
+ return;
+ case State::AboutToWait:
+ // The writer is making a decision about whether to wait. So, we must
+ // wait until it has decided to avoid races. Check if the writer is
+ // closed to avoid hangs.
+ if (mReaderServices->WriterClosed()) {
+ return;
+ }
+ continue;
+ case State::Waiting:
+ if (mWrite->count - mOurCount <= mWrite->requiredDifference) {
+ mWrite->state = State::Processing;
+ mWriterSemaphore->Signal();
+ }
+ return;
+ default:
+ MOZ_ASSERT_UNREACHABLE("Invalid waiting state.");
+ return;
+ }
+ } while (true);
+}
+
+bool CanvasEventRingBuffer::WaitForReadCount(uint32_t aReadCount,
+ TimeDuration aTimeout) {
+ uint32_t requiredDifference = mOurCount - aReadCount;
+ uint32_t spinCount = kMaxSpinCount;
+ do {
+ if (mOurCount - mRead->count <= requiredDifference) {
+ return true;
+ }
+ } while (--spinCount != 0);
+
+ // Double-check that the reader isn't waiting.
+ CheckAndSignalReader();
+ mWrite->state = State::AboutToWait;
+ if (mOurCount - mRead->count <= requiredDifference) {
+ mWrite->state = State::Processing;
+ return true;
+ }
+
+ mWrite->requiredDifference = requiredDifference;
+ mWrite->state = State::Waiting;
+
+ // Wait unless we detect the reading side has closed.
+ while (!mWriterServices->ReaderClosed() && mRead->state != State::Failed) {
+ if (mWriterSemaphore->Wait(Some(aTimeout))) {
+ MOZ_ASSERT(mOurCount - mRead->count <= requiredDifference);
+ return true;
+ }
+ }
+
+ // Either the reader has failed or we're stopping writing for some other
+ // reason (e.g. shutdown), so mark us as failed so the reader is aware.
+ mWrite->state = State::Failed;
+ mGood = false;
+ return false;
+}
+
+uint32_t CanvasEventRingBuffer::WaitForBytesToWrite() {
+ uint32_t streamFullReadCount = mOurCount - kStreamSize;
+ if (!WaitForReadCount(streamFullReadCount + 1, kTimeout)) {
+ return 0;
+ }
+
+ return mRead->count - streamFullReadCount;
+}
+
+uint32_t CanvasEventRingBuffer::WaitForBytesToRead() {
+ if (!WaitForDataToRead(kTimeout, kTimeoutRetryCount)) {
+ return 0;
+ }
+
+ return mWrite->count - mOurCount;
+}
+
+void CanvasEventRingBuffer::ReturnWrite(const char* aData, size_t aSize) {
+ uint32_t writeCount = mRead->returnCount;
+ uint32_t bufPos = writeCount % kStreamSize;
+ uint32_t bufRemaining = kStreamSize - bufPos;
+ uint32_t availableToWrite =
+ std::min(bufRemaining, (mWrite->returnCount + kStreamSize - writeCount));
+ while (availableToWrite < aSize) {
+ if (availableToWrite) {
+ memcpy(mBuf + bufPos, aData, availableToWrite);
+ writeCount += availableToWrite;
+ mRead->returnCount = writeCount;
+ bufPos = writeCount % kStreamSize;
+ bufRemaining = kStreamSize - bufPos;
+ aData += availableToWrite;
+ aSize -= availableToWrite;
+ } else if (mReaderServices->WriterClosed()) {
+ return;
+ }
+
+ availableToWrite = std::min(
+ bufRemaining, (mWrite->returnCount + kStreamSize - writeCount));
+ }
+
+ memcpy(mBuf + bufPos, aData, aSize);
+ writeCount += aSize;
+ mRead->returnCount = writeCount;
+}
+
+void CanvasEventRingBuffer::ReturnRead(char* aOut, size_t aSize) {
+ // First wait for the event returning the data to be read.
+ WaitForCheckpoint(mOurCount);
+ uint32_t readCount = mWrite->returnCount;
+
+ // If the event sending back data fails to play then it will ReturnWrite
+ // nothing. So, wait until something has been written or the reader has
+ // stopped processing.
+ while (readCount == mRead->returnCount) {
+ // We recheck the count, because the other side can write all the data and
+ // started waiting in between these two lines.
+ if (mRead->state != State::Processing && readCount == mRead->returnCount) {
+ return;
+ }
+ }
+
+ uint32_t bufPos = readCount % kStreamSize;
+ uint32_t bufRemaining = kStreamSize - bufPos;
+ uint32_t availableToRead =
+ std::min(bufRemaining, (mRead->returnCount - readCount));
+ while (availableToRead < aSize) {
+ if (availableToRead) {
+ memcpy(aOut, mBuf + bufPos, availableToRead);
+ readCount += availableToRead;
+ mWrite->returnCount = readCount;
+ bufPos = readCount % kStreamSize;
+ bufRemaining = kStreamSize - bufPos;
+ aOut += availableToRead;
+ aSize -= availableToRead;
+ } else if (mWriterServices->ReaderClosed()) {
+ return;
+ }
+
+ availableToRead = std::min(bufRemaining, (mRead->returnCount - readCount));
+ }
+
+ memcpy(aOut, mBuf + bufPos, aSize);
+ readCount += aSize;
+ mWrite->returnCount = readCount;
+}
+
+void CanvasDrawEventRecorder::StoreSourceSurfaceRecording(
+ gfx::SourceSurface* aSurface, const char* aReason) {
+ wr::ExternalImageId extId{};
+ nsresult rv = layers::SharedSurfacesChild::Share(aSurface, extId);
+ if (NS_FAILED(rv)) {
+ DrawEventRecorderPrivate::StoreSourceSurfaceRecording(aSurface, aReason);
+ return;
+ }
+
+ StoreExternalSurfaceRecording(aSurface, wr::AsUint64(extId));
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/CanvasDrawEventRecorder.h b/gfx/layers/CanvasDrawEventRecorder.h
new file mode 100644
index 0000000000..30bb028f4d
--- /dev/null
+++ b/gfx/layers/CanvasDrawEventRecorder.h
@@ -0,0 +1,285 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_CanvasDrawEventRecorder_h
+#define mozilla_layers_CanvasDrawEventRecorder_h
+
+#include "mozilla/gfx/DrawEventRecorder.h"
+#include "mozilla/ipc/CrossProcessSemaphore.h"
+#include "mozilla/ipc/SharedMemoryBasic.h"
+
+namespace mozilla {
+namespace layers {
+
+class CanvasEventRingBuffer final : public gfx::EventRingBuffer {
+ public:
+ /**
+ * WriterServices allows consumers of CanvasEventRingBuffer to provide
+ * functions required by the write side of a CanvasEventRingBuffer without
+ * introducing unnecessary dependencies on IPC code.
+ */
+ class WriterServices {
+ public:
+ virtual ~WriterServices() = default;
+
+ /**
+ * @returns true if the reader of the CanvasEventRingBuffer has permanently
+ * stopped processing, otherwise returns false.
+ */
+ virtual bool ReaderClosed() = 0;
+
+ /**
+ * Causes the reader to resume processing when it is in a stopped state.
+ */
+ virtual void ResumeReader() = 0;
+ };
+
+ /**
+ * ReaderServices allows consumers of CanvasEventRingBuffer to provide
+ * functions required by the read side of a CanvasEventRingBuffer without
+ * introducing unnecessary dependencies on IPC code.
+ */
+ class ReaderServices {
+ public:
+ virtual ~ReaderServices() = default;
+
+ /**
+ * @returns true if the writer of the CanvasEventRingBuffer has permanently
+ * stopped processing, otherwise returns false.
+ */
+ virtual bool WriterClosed() = 0;
+ };
+
+ CanvasEventRingBuffer() {}
+
+ /**
+ * Initialize the write side of a CanvasEventRingBuffer returning handles to
+ * the shared memory for the buffer and the two semaphores for waiting in the
+ * reader and the writer.
+ *
+ * @param aOtherPid process ID to share the handles to
+ * @param aReadHandle handle to the shared memory for the buffer
+ * @param aReaderSem reading blocked semaphore
+ * @param aWriterSem writing blocked semaphore
+ * @param aWriterServices provides functions required by the writer
+ * @returns true if initialization succeeds
+ */
+ bool InitWriter(base::ProcessId aOtherPid,
+ ipc::SharedMemoryBasic::Handle* aReadHandle,
+ CrossProcessSemaphoreHandle* aReaderSem,
+ CrossProcessSemaphoreHandle* aWriterSem,
+ UniquePtr<WriterServices> aWriterServices);
+
+ /**
+ * Initialize the read side of a CanvasEventRingBuffer.
+ *
+ * @param aReadHandle handle to the shared memory for the buffer
+ * @param aReaderSem reading blocked semaphore
+ * @param aWriterSem writing blocked semaphore
+ * @param aReaderServices provides functions required by the reader
+ * @returns true if initialization succeeds
+ */
+ bool InitReader(ipc::SharedMemoryBasic::Handle aReadHandle,
+ CrossProcessSemaphoreHandle aReaderSem,
+ CrossProcessSemaphoreHandle aWriterSem,
+ UniquePtr<ReaderServices> aReaderServices);
+
+ bool good() const final { return mGood; }
+
+ bool WriterFailed() const { return mWrite->state == State::Failed; }
+
+ void SetIsBad() final {
+ mGood = false;
+ mRead->state = State::Failed;
+ }
+
+ void write(const char* const aData, const size_t aSize) final;
+
+ bool HasDataToRead();
+
+ /*
+ * This will put the reader into a stopped state if there is no more data to
+ * read. If this returns false the caller is responsible for continuing
+ * translation at a later point. If it returns false the writer will start the
+ * translation again when more data is written.
+ *
+ * @returns true if stopped
+ */
+ bool StopIfEmpty();
+
+ /*
+ * Waits for data to become available. This will wait for aTimeout duration
+ * aRetryCount number of times, checking to see if the other side is closed in
+ * between each one.
+ *
+ * @param aTimeout duration to wait
+ * @param aRetryCount number of times to retry
+ * @returns true if data is available to read.
+ */
+ bool WaitForDataToRead(TimeDuration aTimeout, int32_t aRetryCount);
+
+ int32_t ReadNextEvent();
+
+ void read(char* const aOut, const size_t aSize) final;
+
+ /**
+ * Writes a checkpoint event to the buffer.
+ *
+ * @returns the write count after the checkpoint has been written
+ */
+ uint32_t CreateCheckpoint();
+
+ /**
+ * Waits until the given checkpoint has been read from the buffer.
+ *
+ * @params aCheckpoint the checkpoint to wait for
+ * @params aTimeout duration to wait while reader is not active
+ * @returns true if the checkpoint was reached, false if the reader is closed
+ * or we timeout.
+ */
+ bool WaitForCheckpoint(uint32_t aCheckpoint);
+
+ /**
+ * Used to send data back to the writer. This is done through the same shared
+ * memory so the writer must wait and read the response after it has submitted
+ * the event that uses this.
+ *
+ * @param aData the data to be written back to the writer
+ * @param aSize the number of chars to write
+ */
+ void ReturnWrite(const char* aData, size_t aSize);
+
+ /**
+ * Used to read data sent back from the reader via ReturnWrite. This is done
+ * through the same shared memory so the writer must wait until all expected
+ * data is read before writing new events to the buffer.
+ *
+ * @param aOut the pointer to read into
+ * @param aSize the number of chars to read
+ */
+ void ReturnRead(char* aOut, size_t aSize);
+
+ protected:
+ bool WaitForAndRecalculateAvailableSpace() final;
+ void UpdateWriteTotalsBy(uint32_t aCount) final;
+
+ private:
+ enum class State : uint32_t {
+ Processing,
+
+ /**
+ * This is the important state to make sure the other side signals or starts
+ * us as soon as data or space is available. We set AboutToWait first and
+ * then re-check the condition. If we went straight to Waiting or Stopped
+ * then in between the last check and setting the state, the other side
+ * could have used all available data or space and never have signaled us
+ * because it didn't know we were about to wait, causing a deadlock.
+ * While we are in this state, the other side must wait until we resolve the
+ * AboutToWait state to one of the other states and then signal or start us
+ * if it needs to.
+ */
+ AboutToWait,
+ Waiting,
+ Stopped,
+ Failed,
+ };
+
+ struct ReadFooter {
+ Atomic<uint32_t> count;
+ Atomic<uint32_t> returnCount;
+ Atomic<State> state;
+ };
+
+ struct WriteFooter {
+ Atomic<uint32_t> count;
+ Atomic<uint32_t> returnCount;
+ Atomic<uint32_t> requiredDifference;
+ Atomic<State> state;
+ };
+
+ CanvasEventRingBuffer(const CanvasEventRingBuffer&) = delete;
+ void operator=(const CanvasEventRingBuffer&) = delete;
+
+ void IncrementWriteCountBy(uint32_t aCount);
+
+ bool WaitForReadCount(uint32_t aReadCount, TimeDuration aTimeout);
+
+ bool WaitForAndRecalculateAvailableData();
+
+ void UpdateReadTotalsBy(uint32_t aCount);
+ void IncrementReadCountBy(uint32_t aCount);
+
+ void CheckAndSignalReader();
+
+ void CheckAndSignalWriter();
+
+ uint32_t WaitForBytesToWrite();
+
+ uint32_t WaitForBytesToRead();
+
+ RefPtr<ipc::SharedMemoryBasic> mSharedMemory;
+ UniquePtr<CrossProcessSemaphore> mReaderSemaphore;
+ UniquePtr<CrossProcessSemaphore> mWriterSemaphore;
+ UniquePtr<WriterServices> mWriterServices;
+ UniquePtr<ReaderServices> mReaderServices;
+ char* mBuf = nullptr;
+ uint32_t mOurCount = 0;
+ WriteFooter* mWrite = nullptr;
+ ReadFooter* mRead = nullptr;
+ bool mGood = false;
+};
+
+class CanvasDrawEventRecorder final : public gfx::DrawEventRecorderPrivate {
+ public:
+ MOZ_DECLARE_REFCOUNTED_VIRTUAL_TYPENAME(CanvasDrawEventRecorder, final)
+ explicit CanvasDrawEventRecorder(){};
+
+ bool Init(base::ProcessId aOtherPid, ipc::SharedMemoryBasic::Handle* aHandle,
+ CrossProcessSemaphoreHandle* aReaderSem,
+ CrossProcessSemaphoreHandle* aWriterSem,
+ UniquePtr<CanvasEventRingBuffer::WriterServices> aWriterServices) {
+ return mOutputStream.InitWriter(aOtherPid, aHandle, aReaderSem, aWriterSem,
+ std::move(aWriterServices));
+ }
+
+ void RecordEvent(const gfx::RecordedEvent& aEvent) final {
+ if (!mOutputStream.good()) {
+ return;
+ }
+
+ aEvent.RecordToStream(mOutputStream);
+ }
+
+ void StoreSourceSurfaceRecording(gfx::SourceSurface* aSurface,
+ const char* aReason) final;
+
+ void Flush() final {}
+
+ void ReturnRead(char* aOut, size_t aSize) {
+ mOutputStream.ReturnRead(aOut, aSize);
+ }
+
+ uint32_t CreateCheckpoint() { return mOutputStream.CreateCheckpoint(); }
+
+ /**
+ * Waits until the given checkpoint has been read by the translator.
+ *
+ * @params aCheckpoint the checkpoint to wait for
+ * @returns true if the checkpoint was reached, false if the reader is closed
+ * or we timeout.
+ */
+ bool WaitForCheckpoint(uint32_t aCheckpoint) {
+ return mOutputStream.WaitForCheckpoint(aCheckpoint);
+ }
+
+ private:
+ CanvasEventRingBuffer mOutputStream;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_CanvasDrawEventRecorder_h
diff --git a/gfx/layers/CanvasRenderer.cpp b/gfx/layers/CanvasRenderer.cpp
new file mode 100644
index 0000000000..dcfca029f6
--- /dev/null
+++ b/gfx/layers/CanvasRenderer.cpp
@@ -0,0 +1,135 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "CanvasRenderer.h"
+
+#include "BuildConstants.h"
+#include "ipc/KnowsCompositor.h"
+#include "mozilla/gfx/gfxVars.h"
+#include "mozilla/StaticPrefs_webgl.h"
+#include "nsICanvasRenderingContextInternal.h"
+#include "PersistentBufferProvider.h"
+#include "WebGLTypes.h"
+
+#ifdef MOZ_WAYLAND
+# include "mozilla/widget/DMABufSurface.h"
+#endif
+
+namespace mozilla {
+namespace layers {
+
+CanvasRendererData::CanvasRendererData() = default;
+CanvasRendererData::~CanvasRendererData() = default;
+
+// -
+
+BorrowedSourceSurface::BorrowedSourceSurface(
+ PersistentBufferProvider* const returnTo,
+ const RefPtr<gfx::SourceSurface> surf)
+ : mReturnTo(returnTo), mSurf(surf) {}
+
+BorrowedSourceSurface::~BorrowedSourceSurface() {
+ if (mReturnTo) {
+ auto forgettable = mSurf;
+ mReturnTo->ReturnSnapshot(forgettable.forget());
+ }
+}
+
+// -
+
+CanvasRenderer::CanvasRenderer() { MOZ_COUNT_CTOR(CanvasRenderer); }
+
+CanvasRenderer::~CanvasRenderer() { MOZ_COUNT_DTOR(CanvasRenderer); }
+
+void CanvasRenderer::Initialize(const CanvasRendererData& aData) {
+ mData = aData;
+}
+
+bool CanvasRenderer::IsDataValid(const CanvasRendererData& aData) const {
+ return mData.GetContext() == aData.GetContext();
+}
+
+std::shared_ptr<BorrowedSourceSurface> CanvasRenderer::BorrowSnapshot(
+ const bool requireAlphaPremult) const {
+ const auto context = mData.GetContext();
+ if (!context) return nullptr;
+ RefPtr<PersistentBufferProvider> provider = context->GetBufferProvider();
+
+ RefPtr<gfx::SourceSurface> ss;
+
+ if (provider) {
+ ss = provider->BorrowSnapshot();
+ }
+ if (!ss) {
+ provider = nullptr;
+ ss = context->GetFrontBufferSnapshot(requireAlphaPremult);
+ }
+ if (!ss) return nullptr;
+
+ return std::make_shared<BorrowedSourceSurface>(provider, ss);
+}
+
+void CanvasRenderer::FirePreTransactionCallback() const {
+ if (!mData.mDoPaintCallbacks) return;
+ const auto context = mData.GetContext();
+ if (!context) return;
+ context->OnBeforePaintTransaction();
+}
+
+void CanvasRenderer::FireDidTransactionCallback() const {
+ if (!mData.mDoPaintCallbacks) return;
+ const auto context = mData.GetContext();
+ if (!context) return;
+ context->OnDidPaintTransaction();
+}
+
+TextureType TexTypeForWebgl(KnowsCompositor* const knowsCompositor) {
+ if (!knowsCompositor) return TextureType::Unknown;
+ const auto layersBackend = knowsCompositor->GetCompositorBackendType();
+
+ switch (layersBackend) {
+ case LayersBackend::LAYERS_LAST:
+ MOZ_CRASH("Unexpected LayersBackend::LAYERS_LAST");
+
+ case LayersBackend::LAYERS_NONE:
+ return TextureType::Unknown;
+
+ case LayersBackend::LAYERS_WR:
+ break;
+ }
+
+ if (kIsWindows) {
+ if (knowsCompositor->SupportsD3D11()) {
+ return TextureType::D3D11;
+ }
+ }
+ if (kIsMacOS) {
+ return TextureType::MacIOSurface;
+ }
+
+#ifdef MOZ_WAYLAND
+ if (kIsWayland) {
+ if (!knowsCompositor->UsingSoftwareWebRender() &&
+ widget::DMABufDevice::IsDMABufWebGLEnabled()) {
+ return TextureType::DMABUF;
+ }
+ }
+#endif
+
+ if (kIsAndroid) {
+ if (gfx::gfxVars::UseAHardwareBufferSharedSurfaceWebglOop()) {
+ return TextureType::AndroidHardwareBuffer;
+ }
+ if (StaticPrefs::webgl_enable_surface_texture()) {
+ return TextureType::AndroidNativeWindow;
+ }
+ }
+
+ return TextureType::Unknown;
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/CanvasRenderer.h b/gfx/layers/CanvasRenderer.h
new file mode 100644
index 0000000000..e2c69e95cc
--- /dev/null
+++ b/gfx/layers/CanvasRenderer.h
@@ -0,0 +1,151 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GFX_CANVASRENDERER_H
+#define GFX_CANVASRENDERER_H
+
+#include <memory> // for weak_ptr
+#include <stdint.h> // for uint32_t
+#include "GLContextTypes.h" // for GLContext
+#include "gfxContext.h" // for gfxContext, etc
+#include "gfxTypes.h"
+#include "gfxPlatform.h" // for gfxImageFormat
+#include "mozilla/Assertions.h" // for MOZ_ASSERT, etc
+#include "mozilla/Preferences.h" // for Preferences
+#include "mozilla/RefPtr.h" // for RefPtr
+#include "mozilla/gfx/2D.h" // for DrawTarget
+#include "mozilla/Maybe.h"
+#include "mozilla/mozalloc.h" // for operator delete, etc
+#include "mozilla/WeakPtr.h" // for WeakPtr
+#include "nsISupportsImpl.h" // for MOZ_COUNT_CTOR, etc
+#include "nsICanvasRenderingContextInternal.h"
+
+namespace mozilla {
+namespace layers {
+
+class KnowsCompositor;
+class PersistentBufferProvider;
+class WebRenderCanvasRendererAsync;
+
+TextureType TexTypeForWebgl(KnowsCompositor*);
+
+struct CanvasRendererData final {
+ CanvasRendererData();
+ ~CanvasRendererData();
+
+ WeakPtr<nsICanvasRenderingContextInternal> mContext;
+
+ // The size of the canvas content
+ gfx::IntSize mSize = {0, 0};
+
+ bool mDoPaintCallbacks = false;
+ bool mIsOpaque = true;
+ bool mIsAlphaPremult = true;
+
+ gl::OriginPos mOriginPos = gl::OriginPos::TopLeft;
+
+ // Used in remote texture push callback
+ Maybe<RemoteTextureOwnerId> mRemoteTextureOwnerIdOfPushCallback = Nothing();
+
+ nsICanvasRenderingContextInternal* GetContext() const {
+ return mContext.get();
+ }
+};
+
+// Based class which used for canvas rendering. There are many derived classes
+// for different purposes. such as:
+//
+// ShareableCanvasRenderer provides IPC capabilities that allow sending canvas
+// content over IPC. This is pure virtual class because the IPC handling is
+// different in different LayerManager. So that we have following classes
+// inherit ShareableCanvasRenderer.
+//
+// WebRenderCanvasRenderer inherits ShareableCanvasRenderer and provides all
+// functionality that WebRender uses.
+// WebRenderCanvasRendererAsync inherits WebRenderCanvasRenderer and be used in
+// layers-free mode of WebRender.
+//
+// class diagram:
+//
+// +--------------+
+// |CanvasRenderer|
+// +-------+------+
+// ^
+// |
+// +-----------+-----------+
+// |ShareableCanvasRenderer|
+// +-----+-----------------+
+// ^
+// |
+// +-----------+-----------+
+// |WebRenderCanvasRenderer|
+// +-----------+-----------+
+// ^
+// |
+// +-------------+--------------+
+// |WebRenderCanvasRendererAsync|
+// +----------------------------+
+
+class BorrowedSourceSurface final {
+ public:
+ const WeakPtr<PersistentBufferProvider> mReturnTo;
+ const RefPtr<gfx::SourceSurface> mSurf; /// non-null
+
+ BorrowedSourceSurface(PersistentBufferProvider*, RefPtr<gfx::SourceSurface>);
+ ~BorrowedSourceSurface();
+};
+
+// -
+
+class CanvasRenderer : public RefCounted<CanvasRenderer> {
+ friend class CanvasRendererSourceSurface;
+
+ public:
+ MOZ_DECLARE_REFCOUNTED_TYPENAME(CanvasRenderer)
+
+ private:
+ bool mDirty = false;
+
+ protected:
+ CanvasRendererData mData;
+
+ public:
+ explicit CanvasRenderer();
+ virtual ~CanvasRenderer();
+
+ public:
+ virtual void Initialize(const CanvasRendererData&);
+ virtual bool IsDataValid(const CanvasRendererData&) const;
+
+ virtual void ClearCachedResources() {}
+ virtual void DisconnectClient() {}
+
+ const gfx::IntSize& GetSize() const { return mData.mSize; }
+ bool IsOpaque() const { return mData.mIsOpaque; }
+ bool YIsDown() const { return mData.mOriginPos == gl::OriginPos::TopLeft; }
+ Maybe<RemoteTextureOwnerId> GetRemoteTextureOwnerIdOfPushCallback() {
+ return mData.mRemoteTextureOwnerIdOfPushCallback;
+ }
+
+ void SetDirty() { mDirty = true; }
+ void ResetDirty() { mDirty = false; }
+ bool IsDirty() const { return mDirty; }
+
+ virtual WebRenderCanvasRendererAsync* AsWebRenderCanvasRendererAsync() {
+ return nullptr;
+ }
+
+ std::shared_ptr<BorrowedSourceSurface> BorrowSnapshot(
+ bool requireAlphaPremult = true) const;
+
+ void FirePreTransactionCallback() const;
+ void FireDidTransactionCallback() const;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif
diff --git a/gfx/layers/CompositionRecorder.cpp b/gfx/layers/CompositionRecorder.cpp
new file mode 100644
index 0000000000..61d43330e9
--- /dev/null
+++ b/gfx/layers/CompositionRecorder.cpp
@@ -0,0 +1,114 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "CompositionRecorder.h"
+#include "gfxUtils.h"
+#include "mozilla/gfx/2D.h"
+#include "mozilla/gfx/gfxVars.h"
+#include "nsIInputStream.h"
+#include "nsIBinaryOutputStream.h"
+#include "nsIObjectOutputStream.h"
+#include "prtime.h"
+
+#include <ctime>
+#include <iomanip>
+#include "stdio.h"
+#ifdef XP_WIN
+# include "direct.h"
+#else
+# include <sys/types.h>
+# include "sys/stat.h"
+#endif
+
+using namespace mozilla::gfx;
+
+namespace mozilla {
+namespace layers {
+
+CompositionRecorder::CompositionRecorder(TimeStamp aRecordingStart)
+ : mRecordingStart(aRecordingStart) {}
+
+void CompositionRecorder::RecordFrame(RecordedFrame* aFrame) {
+ mRecordedFrames.AppendElement(aFrame);
+}
+
+Maybe<FrameRecording> CompositionRecorder::GetRecording() {
+ FrameRecording recording;
+
+ recording.startTime() = mRecordingStart;
+
+ nsTArray<uint8_t> bytes;
+ for (RefPtr<RecordedFrame>& recordedFrame : mRecordedFrames) {
+ RefPtr<DataSourceSurface> surf = recordedFrame->GetSourceSurface();
+ if (!surf) {
+ return Nothing();
+ }
+
+ nsCOMPtr<nsIInputStream> imgStream;
+ nsresult rv = gfxUtils::EncodeSourceSurfaceAsStream(
+ surf, ImageType::PNG, u""_ns, getter_AddRefs(imgStream));
+ if (NS_FAILED(rv)) {
+ return Nothing();
+ }
+
+ uint64_t bufSize64;
+ rv = imgStream->Available(&bufSize64);
+ if (NS_FAILED(rv) || bufSize64 > UINT32_MAX) {
+ return Nothing();
+ }
+
+ const uint32_t frameLength = static_cast<uint32_t>(bufSize64);
+ size_t startIndex = bytes.Length();
+ bytes.SetLength(startIndex + frameLength);
+
+ uint8_t* bytePtr = &bytes[startIndex];
+ uint32_t bytesLeft = frameLength;
+
+ while (bytesLeft > 0) {
+ uint32_t bytesRead = 0;
+ rv = imgStream->Read(reinterpret_cast<char*>(bytePtr), bytesLeft,
+ &bytesRead);
+ if (NS_FAILED(rv) || bytesRead == 0) {
+ return Nothing();
+ }
+
+ bytePtr += bytesRead;
+ bytesLeft -= bytesRead;
+ }
+
+#ifdef DEBUG
+
+ // Currently, all implementers of imgIEncoder report their exact size
+ // through nsIInputStream::Available(), but let's explicitly state that we
+ // rely on that behavior for the algorithm above.
+
+ char dummy = 0;
+ uint32_t bytesRead = 0;
+ rv = imgStream->Read(&dummy, 1, &bytesRead);
+ MOZ_ASSERT(NS_SUCCEEDED(rv) && bytesRead == 0);
+
+#endif
+
+ RecordedFrameData frameData;
+
+ frameData.timeOffset() = recordedFrame->GetTimeStamp();
+ frameData.length() = frameLength;
+
+ recording.frames().AppendElement(std::move(frameData));
+
+ // Now that we're done, release the frame so we can free up its memory
+ recordedFrame = nullptr;
+ }
+
+ mRecordedFrames.Clear();
+
+ recording.bytes() = ipc::BigBuffer(bytes);
+
+ return Some(std::move(recording));
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/CompositionRecorder.h b/gfx/layers/CompositionRecorder.h
new file mode 100644
index 0000000000..5f5ec34b48
--- /dev/null
+++ b/gfx/layers/CompositionRecorder.h
@@ -0,0 +1,78 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_CompositionRecorder_h
+#define mozilla_layers_CompositionRecorder_h
+
+#include "mozilla/RefPtr.h"
+#include "mozilla/TimeStamp.h"
+#include "mozilla/layers/PCompositorBridgeTypes.h"
+#include "nsISupportsImpl.h"
+#include "nsTArray.h"
+#include "nsString.h"
+
+namespace mozilla {
+
+namespace gfx {
+class DataSourceSurface;
+}
+
+namespace layers {
+
+/**
+ * A captured frame from a |LayerManager|.
+ */
+class RecordedFrame {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(RecordedFrame)
+
+ // The resulting DataSourceSurface must not be kept alive beyond the lifetime
+ // of the RecordedFrame object, since it may refer to data owned by the frame.
+ virtual already_AddRefed<gfx::DataSourceSurface> GetSourceSurface() = 0;
+ TimeStamp GetTimeStamp() { return mTimeStamp; }
+
+ protected:
+ virtual ~RecordedFrame() = default;
+ RecordedFrame(const TimeStamp& aTimeStamp) : mTimeStamp(aTimeStamp) {}
+
+ private:
+ TimeStamp mTimeStamp;
+};
+
+/**
+ * A recorder for composited frames.
+ *
+ * This object collects frames sent to it by a |LayerManager| and writes them
+ * out as a series of images until recording has finished.
+ *
+ * If GPU-accelerated rendering is used, the frames will not be mapped into
+ * memory until |WriteCollectedFrames| is called.
+ */
+class CompositionRecorder {
+ public:
+ explicit CompositionRecorder(TimeStamp aRecordingStart);
+
+ /**
+ * Record a composited frame.
+ */
+ void RecordFrame(RecordedFrame* aFrame);
+
+ /**
+ * Get the array of frames that were recorded since the last call to
+ * GetRecording(), where each frame consists of a presentation time and
+ * the PNG-encoded contents as an array of bytes
+ */
+ Maybe<FrameRecording> GetRecording();
+
+ private:
+ nsTArray<RefPtr<RecordedFrame>> mRecordedFrames;
+ TimeStamp mRecordingStart;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_CompositionRecorder_h
diff --git a/gfx/layers/Compositor.cpp b/gfx/layers/Compositor.cpp
new file mode 100644
index 0000000000..e7c84918ad
--- /dev/null
+++ b/gfx/layers/Compositor.cpp
@@ -0,0 +1,241 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/layers/Compositor.h"
+#include "mozilla/layers/CompositionRecorder.h"
+#include "base/message_loop.h" // for MessageLoop
+#include "mozilla/gfx/Types.h"
+#include "mozilla/layers/CompositorBridgeParent.h" // for CompositorBridgeParent
+#include "mozilla/layers/Diagnostics.h"
+#include "mozilla/layers/Effects.h" // for Effect, EffectChain, etc
+#include "mozilla/layers/TextureClient.h"
+#include "mozilla/layers/TextureHost.h"
+#include "mozilla/layers/CompositorThread.h"
+#include "mozilla/mozalloc.h" // for operator delete, etc
+#include "GeckoProfiler.h"
+#include "gfx2DGlue.h"
+#include "nsAppRunner.h"
+
+namespace mozilla {
+
+namespace layers {
+
+class CompositorRecordedFrame final : public RecordedFrame {
+ public:
+ CompositorRecordedFrame(const TimeStamp& aTimeStamp,
+ RefPtr<AsyncReadbackBuffer>&& aBuffer)
+ : RecordedFrame(aTimeStamp), mBuffer(aBuffer) {}
+
+ virtual already_AddRefed<gfx::DataSourceSurface> GetSourceSurface() override {
+ if (mSurface) {
+ return do_AddRef(mSurface);
+ }
+
+ gfx::IntSize size = mBuffer->GetSize();
+
+ mSurface = gfx::Factory::CreateDataSourceSurface(
+ size, gfx::SurfaceFormat::B8G8R8A8,
+ /* aZero = */ false);
+
+ if (!mBuffer->MapAndCopyInto(mSurface, size)) {
+ mSurface = nullptr;
+ return nullptr;
+ }
+
+ return do_AddRef(mSurface);
+ }
+
+ private:
+ RefPtr<AsyncReadbackBuffer> mBuffer;
+ RefPtr<gfx::DataSourceSurface> mSurface;
+};
+
+Compositor::Compositor(widget::CompositorWidget* aWidget)
+ : mWidget(aWidget),
+ mIsDestroyed(false)
+#if defined(MOZ_WIDGET_ANDROID)
+ // If the default color isn't white for Fennec, there is a black
+ // flash before the first page of a tab is loaded.
+ ,
+ mClearColor(ToDeviceColor(sRGBColor::OpaqueWhite()))
+#else
+ ,
+ mClearColor(gfx::DeviceColor())
+#endif
+{
+}
+
+Compositor::~Compositor() {}
+
+void Compositor::Destroy() {
+ mWidget = nullptr;
+
+ TextureSourceProvider::Destroy();
+ mIsDestroyed = true;
+}
+
+void Compositor::EndFrame() { mLastCompositionEndTime = TimeStamp::Now(); }
+
+nsTArray<TexturedVertex> TexturedTrianglesToVertexArray(
+ const nsTArray<gfx::TexturedTriangle>& aTriangles) {
+ const auto VertexFromPoints = [](const gfx::Point& p, const gfx::Point& t) {
+ return TexturedVertex{{p.x, p.y}, {t.x, t.y}};
+ };
+
+ nsTArray<TexturedVertex> vertices;
+
+ for (const gfx::TexturedTriangle& t : aTriangles) {
+ vertices.AppendElement(VertexFromPoints(t.p1, t.textureCoords.p1));
+ vertices.AppendElement(VertexFromPoints(t.p2, t.textureCoords.p2));
+ vertices.AppendElement(VertexFromPoints(t.p3, t.textureCoords.p3));
+ }
+
+ return vertices;
+}
+
+static float WrapTexCoord(float v) {
+ // This should return values in range [0, 1.0)
+ return v - floorf(v);
+}
+
+static void SetRects(size_t n, decomposedRectArrayT* aLayerRects,
+ decomposedRectArrayT* aTextureRects, float x0, float y0,
+ float x1, float y1, float tx0, float ty0, float tx1,
+ float ty1, bool flip_y) {
+ if (flip_y) {
+ std::swap(ty0, ty1);
+ }
+ (*aLayerRects)[n] = gfx::Rect(x0, y0, x1 - x0, y1 - y0);
+ (*aTextureRects)[n] = gfx::Rect(tx0, ty0, tx1 - tx0, ty1 - ty0);
+}
+
+#ifdef DEBUG
+static inline bool FuzzyEqual(float a, float b) {
+ return fabs(a - b) < 0.0001f;
+}
+static inline bool FuzzyLTE(float a, float b) { return a <= b + 0.0001f; }
+#endif
+
+size_t DecomposeIntoNoRepeatRects(const gfx::Rect& aRect,
+ const gfx::Rect& aTexCoordRect,
+ decomposedRectArrayT* aLayerRects,
+ decomposedRectArrayT* aTextureRects) {
+ gfx::Rect texCoordRect = aTexCoordRect;
+
+ // If the texture should be flipped, it will have negative height. Detect that
+ // here and compensate for it. We will flip each rect as we emit it.
+ bool flipped = false;
+ if (texCoordRect.Height() < 0) {
+ flipped = true;
+ texCoordRect.MoveByY(texCoordRect.Height());
+ texCoordRect.SetHeight(-texCoordRect.Height());
+ }
+
+ // Wrap the texture coordinates so they are within [0,1] and cap width/height
+ // at 1. We rely on this below.
+ texCoordRect = gfx::Rect(gfx::Point(WrapTexCoord(texCoordRect.X()),
+ WrapTexCoord(texCoordRect.Y())),
+ gfx::Size(std::min(texCoordRect.Width(), 1.0f),
+ std::min(texCoordRect.Height(), 1.0f)));
+
+ NS_ASSERTION(
+ texCoordRect.X() >= 0.0f && texCoordRect.X() <= 1.0f &&
+ texCoordRect.Y() >= 0.0f && texCoordRect.Y() <= 1.0f &&
+ texCoordRect.Width() >= 0.0f && texCoordRect.Width() <= 1.0f &&
+ texCoordRect.Height() >= 0.0f && texCoordRect.Height() <= 1.0f &&
+ texCoordRect.XMost() >= 0.0f && texCoordRect.XMost() <= 2.0f &&
+ texCoordRect.YMost() >= 0.0f && texCoordRect.YMost() <= 2.0f,
+ "We just wrapped the texture coordinates, didn't we?");
+
+ // Get the top left and bottom right points of the rectangle. Note that
+ // tl.x/tl.y are within [0,1] but br.x/br.y are within [0,2].
+ gfx::Point tl = texCoordRect.TopLeft();
+ gfx::Point br = texCoordRect.BottomRight();
+
+ NS_ASSERTION(tl.x >= 0.0f && tl.x <= 1.0f && tl.y >= 0.0f && tl.y <= 1.0f &&
+ br.x >= tl.x && br.x <= 2.0f && br.y >= tl.y &&
+ br.y <= 2.0f && FuzzyLTE(br.x - tl.x, 1.0f) &&
+ FuzzyLTE(br.y - tl.y, 1.0f),
+ "Somehow generated invalid texture coordinates");
+
+ // Then check if we wrap in either the x or y axis.
+ bool xwrap = br.x > 1.0f;
+ bool ywrap = br.y > 1.0f;
+
+ // If xwrap is false, the texture will be sampled from tl.x .. br.x.
+ // If xwrap is true, then it will be split into tl.x .. 1.0, and
+ // 0.0 .. WrapTexCoord(br.x). Same for the Y axis. The destination
+ // rectangle is also split appropriately, according to the calculated
+ // xmid/ymid values.
+ if (!xwrap && !ywrap) {
+ SetRects(0, aLayerRects, aTextureRects, aRect.X(), aRect.Y(), aRect.XMost(),
+ aRect.YMost(), tl.x, tl.y, br.x, br.y, flipped);
+ return 1;
+ }
+
+ // If we are dealing with wrapping br.x and br.y are greater than 1.0 so
+ // wrap them here as well.
+ br = gfx::Point(xwrap ? WrapTexCoord(br.x.value) : br.x.value,
+ ywrap ? WrapTexCoord(br.y.value) : br.y.value);
+
+ // If we wrap around along the x axis, we will draw first from
+ // tl.x .. 1.0 and then from 0.0 .. br.x (which we just wrapped above).
+ // The same applies for the Y axis. The midpoints we calculate here are
+ // only valid if we actually wrap around.
+ GLfloat xmid =
+ aRect.X() + (1.0f - tl.x) / texCoordRect.Width() * aRect.Width();
+ GLfloat ymid =
+ aRect.Y() + (1.0f - tl.y) / texCoordRect.Height() * aRect.Height();
+
+ // Due to floating-point inaccuracy, we have to use XMost()-x and YMost()-y
+ // to calculate width and height, respectively, to ensure that size will
+ // remain consistent going from absolute to relative and back again.
+ NS_ASSERTION(
+ !xwrap || (xmid >= aRect.X() && xmid <= aRect.XMost() &&
+ FuzzyEqual((xmid - aRect.X()) + (aRect.XMost() - xmid),
+ aRect.XMost() - aRect.X())),
+ "xmid should be within [x,XMost()] and the wrapped rect should have the "
+ "same width");
+ NS_ASSERTION(
+ !ywrap || (ymid >= aRect.Y() && ymid <= aRect.YMost() &&
+ FuzzyEqual((ymid - aRect.Y()) + (aRect.YMost() - ymid),
+ aRect.YMost() - aRect.Y())),
+ "ymid should be within [y,YMost()] and the wrapped rect should have the "
+ "same height");
+
+ if (!xwrap && ywrap) {
+ SetRects(0, aLayerRects, aTextureRects, aRect.X(), aRect.Y(), aRect.XMost(),
+ ymid, tl.x, tl.y, br.x, 1.0f, flipped);
+ SetRects(1, aLayerRects, aTextureRects, aRect.X(), ymid, aRect.XMost(),
+ aRect.YMost(), tl.x, 0.0f, br.x, br.y, flipped);
+ return 2;
+ }
+
+ if (xwrap && !ywrap) {
+ SetRects(0, aLayerRects, aTextureRects, aRect.X(), aRect.Y(), xmid,
+ aRect.YMost(), tl.x, tl.y, 1.0f, br.y, flipped);
+ SetRects(1, aLayerRects, aTextureRects, xmid, aRect.Y(), aRect.XMost(),
+ aRect.YMost(), 0.0f, tl.y, br.x, br.y, flipped);
+ return 2;
+ }
+
+ SetRects(0, aLayerRects, aTextureRects, aRect.X(), aRect.Y(), xmid, ymid,
+ tl.x, tl.y, 1.0f, 1.0f, flipped);
+ SetRects(1, aLayerRects, aTextureRects, xmid, aRect.Y(), aRect.XMost(), ymid,
+ 0.0f, tl.y, br.x, 1.0f, flipped);
+ SetRects(2, aLayerRects, aTextureRects, aRect.X(), ymid, xmid, aRect.YMost(),
+ tl.x, 0.0f, 1.0f, br.y, flipped);
+ SetRects(3, aLayerRects, aTextureRects, xmid, ymid, aRect.XMost(),
+ aRect.YMost(), 0.0f, 0.0f, br.x, br.y, flipped);
+ return 4;
+}
+
+bool Compositor::ShouldRecordFrames() const {
+ return profiler_feature_active(ProfilerFeature::Screenshots) || mRecordFrames;
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/Compositor.h b/gfx/layers/Compositor.h
new file mode 100644
index 0000000000..b4e05674a8
--- /dev/null
+++ b/gfx/layers/Compositor.h
@@ -0,0 +1,416 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_GFX_COMPOSITOR_H
+#define MOZILLA_GFX_COMPOSITOR_H
+
+#include "Units.h" // for ScreenPoint
+#include "mozilla/Assertions.h" // for MOZ_ASSERT, etc
+#include "mozilla/RefPtr.h" // for already_AddRefed, RefCounted
+#include "mozilla/gfx/2D.h" // for DrawTarget
+#include "mozilla/gfx/MatrixFwd.h" // for Matrix, Matrix4x4
+#include "mozilla/gfx/Point.h" // for IntSize, Point
+#include "mozilla/gfx/Polygon.h" // for Polygon
+#include "mozilla/gfx/Rect.h" // for Rect, IntRect
+#include "mozilla/gfx/Types.h" // for Float
+#include "mozilla/gfx/Triangle.h" // for Triangle, TexturedTriangle
+#include "mozilla/layers/CompositorTypes.h" // for DiagnosticTypes, etc
+#include "mozilla/layers/LayersTypes.h" // for LayersBackend
+#include "mozilla/layers/SurfacePool.h" // for SurfacePoolHandle
+#include "mozilla/layers/TextureSourceProvider.h"
+#include "mozilla/widget/CompositorWidget.h"
+#include "nsISupportsImpl.h" // for MOZ_COUNT_CTOR, etc
+#include "nsRegion.h"
+#include <vector>
+#include "mozilla/WidgetUtils.h"
+
+/**
+ * Different elements of a web pages are rendered into separate "layers" before
+ * they are flattened into the final image that is brought to the screen.
+ * See Layers.h for more informations about layers and why we use retained
+ * structures.
+ * Most of the documentation for layers is directly in the source code in the
+ * form of doc comments. An overview can also be found in the the wiki:
+ * https://wiki.mozilla.org/Gecko:Overview#Graphics
+ *
+ *
+ * # Main interfaces and abstractions
+ *
+ * - CompositableClient and CompositableHost
+ * (client/CompositableClient.h composite/CompositableHost.h)
+ * - TextureClient and TextureHost
+ * (client/TextureClient.h composite/TextureHost.h)
+ * - TextureSource
+ * (composite/TextureHost.h)
+ * - Forwarders
+ * (ipc/CompositableForwarder.h ipc/ShadowLayers.h)
+ * - Compositor
+ * (this file)
+ * - IPDL protocols
+ * (.ipdl files under the gfx/layers/ipc directory)
+ *
+ * The *Client and Shadowable* classes are always used on the content thread.
+ * Forwarders are always used on the content thread.
+ * The *Host and Shadow* classes are always used on the compositor thread.
+ * Compositors, TextureSource, and Effects are always used on the compositor
+ * thread.
+ * Most enums and constants are declared in LayersTypes.h and CompositorTypes.h.
+ *
+ *
+ * # Texture transfer
+ *
+ * Most layer classes own a Compositable plus some extra information like
+ * transforms and clip rects. They are platform independent.
+ * Compositable classes manipulate Texture objects and are reponsible for
+ * things like tiling, buffer rotation or double buffering. Compositables
+ * are also platform-independent. Examples of compositable classes are:
+ * - ImageClient
+ * - CanvasClient
+ * - etc.
+ * Texture classes (TextureClient and TextureHost) are thin abstractions over
+ * platform-dependent texture memory. They are maniplulated by compositables
+ * and don't know about buffer rotations and such. The purposes of TextureClient
+ * and TextureHost are to synchronize, serialize and deserialize texture data.
+ * TextureHosts provide access to TextureSources that are views on the
+ * Texture data providing the necessary api for Compositor backend to composite
+ * them.
+ *
+ * Compositable and Texture clients and hosts are created using factory methods.
+ * They should only be created by using their constructor in exceptional
+ * circumstances. The factory methods are located:
+ * TextureClient - CompositableClient::CreateTextureClient
+ * TextureHost - TextureHost::CreateTextureHost, which calls a
+ * platform-specific function, e.g.,
+ * CreateTextureHostOGL CompositableClient - in the appropriate subclass, e.g.,
+ * CanvasClient::CreateCanvasClient
+ * CompositableHost - CompositableHost::Create
+ *
+ *
+ * # IPDL
+ *
+ * If off-main-thread compositing (OMTC) is enabled, compositing is performed
+ * in a dedicated thread. In some setups compositing happens in a dedicated
+ * process. Documentation may refer to either the compositor thread or the
+ * compositor process.
+ * See explanations in ShadowLayers.h.
+ *
+ *
+ * # Backend implementations
+ *
+ * Compositor backends like OpenGL or flavours of D3D live in their own
+ * directory under gfx/layers/. To add a new backend, implement at least the
+ * following interfaces:
+ * - Compositor (ex. CompositorOGL)
+ * - TextureHost (ex. SurfaceTextureHost)
+ * Depending on the type of data that needs to be serialized, you may need to
+ * add specific TextureClient implementations.
+ */
+
+class nsIWidget;
+
+namespace mozilla {
+namespace gfx {
+class DrawTarget;
+class DataSourceSurface;
+} // namespace gfx
+
+namespace layers {
+
+struct Effect;
+struct EffectChain;
+class Image;
+class Layer;
+class TextureSource;
+class DataTextureSource;
+class CompositingRenderTarget;
+class CompositorBridgeParent;
+class NativeLayer;
+class CompositorOGL;
+class CompositorD3D11;
+class TextureReadLock;
+struct GPUStats;
+class AsyncReadbackBuffer;
+class RecordedFrame;
+
+enum SurfaceInitMode { INIT_MODE_NONE, INIT_MODE_CLEAR };
+
+/**
+ * Common interface for compositor backends.
+ *
+ * Compositor provides a cross-platform interface to a set of operations for
+ * compositing quads. Compositor knows nothing about the layer tree. It must be
+ * told everything about each composited quad - contents, location, transform,
+ * opacity, etc.
+ *
+ * In theory it should be possible for different widgets to use the same
+ * compositor. In practice, we use one compositor per window.
+ *
+ * # Usage
+ *
+ * For an example of a user of Compositor, see LayerManagerComposite.
+ *
+ * Initialization: create a Compositor object, call Initialize().
+ *
+ * Destruction: destroy any resources associated with the compositor, call
+ * Destroy(), delete the Compositor object.
+ *
+ * Composition:
+ * call BeginFrame,
+ * for each quad to be composited:
+ * call MakeCurrent if necessary (not necessary if no other context has been
+ * made current),
+ * take care of any texture upload required to composite the quad, this step
+ * is backend-dependent,
+ * construct an EffectChain for the quad,
+ * call DrawQuad,
+ * call EndFrame.
+ *
+ * By default, the compositor will render to the screen if BeginFrameForWindow
+ * is called. To render to a target, call BeginFrameForTarget or
+ * or SetRenderTarget, the latter with a target created
+ * by CreateRenderTarget or CreateRenderTargetFromSource.
+ *
+ * The target and viewport methods can be called before any DrawQuad call and
+ * affect any subsequent DrawQuad calls.
+ */
+class Compositor : public TextureSourceProvider {
+ protected:
+ virtual ~Compositor();
+
+ public:
+ explicit Compositor(widget::CompositorWidget* aWidget);
+
+ bool IsValid() const override { return true; }
+
+ virtual bool Initialize(nsCString* const out_failureReason) = 0;
+ void Destroy() override;
+ bool IsDestroyed() const { return mIsDestroyed; }
+
+ /**
+ * Creates a Surface that can be used as a rendering target by this
+ * compositor.
+ */
+ virtual already_AddRefed<CompositingRenderTarget> CreateRenderTarget(
+ const gfx::IntRect& aRect, SurfaceInitMode aInit) = 0;
+
+ /**
+ * Grab a snapshot of aSource and store it in aDest, so that the pixels can
+ * be read on the CPU by mapping aDest at some point in the future.
+ * aSource and aDest must have the same size.
+ * If this is a GPU compositor, this call must not block on the GPU.
+ * Returns whether the operation was successful.
+ */
+ virtual bool ReadbackRenderTarget(CompositingRenderTarget* aSource,
+ AsyncReadbackBuffer* aDest) = 0;
+
+ /**
+ * Create an AsyncReadbackBuffer of the specified size. Can return null.
+ */
+ virtual already_AddRefed<AsyncReadbackBuffer> CreateAsyncReadbackBuffer(
+ const gfx::IntSize& aSize) = 0;
+
+ /**
+ * Draw a part of aSource into the current render target.
+ * Scaling is done with linear filtering.
+ * Returns whether the operation was successful.
+ */
+ virtual bool BlitRenderTarget(CompositingRenderTarget* aSource,
+ const gfx::IntSize& aSourceSize,
+ const gfx::IntSize& aDestSize) = 0;
+
+ /**
+ * Sets the given surface as the target for subsequent calls to DrawQuad.
+ * Passing null as aSurface sets the screen as the target.
+ */
+ virtual void SetRenderTarget(CompositingRenderTarget* aSurface) = 0;
+
+ /**
+ * Returns the current target for rendering. Will return null if we are
+ * rendering to the screen.
+ */
+ virtual already_AddRefed<CompositingRenderTarget> GetCurrentRenderTarget()
+ const = 0;
+
+ /**
+ * Returns a render target which contains the entire window's drawing.
+ * On platforms where no such render target is used during compositing (e.g.
+ * with buffered BasicCompositor, where only the invalid area is drawn to a
+ * render target), this will return null.
+ */
+ virtual already_AddRefed<CompositingRenderTarget> GetWindowRenderTarget()
+ const = 0;
+
+ /**
+ * Mostly the compositor will pull the size from a widget and this method will
+ * be ignored, but compositor implementations are free to use it if they like.
+ */
+ virtual void SetDestinationSurfaceSize(const gfx::IntSize& aSize) = 0;
+
+ /**
+ * Tell the compositor to draw a quad. What to do draw and how it is
+ * drawn is specified by aEffectChain. aRect is the quad to draw, in user
+ * space. aTransform transforms from user space to screen space. If texture
+ * coords are required, these will be in the primary effect in the effect
+ * chain. aVisibleRect is used to determine which edges should be antialiased,
+ * without applying the effect to the inner edges of a tiled layer.
+ */
+ virtual void DrawQuad(const gfx::Rect& aRect, const gfx::IntRect& aClipRect,
+ const EffectChain& aEffectChain, gfx::Float aOpacity,
+ const gfx::Matrix4x4& aTransform,
+ const gfx::Rect& aVisibleRect) = 0;
+
+ void SetClearColor(const gfx::DeviceColor& aColor) { mClearColor = aColor; }
+
+ /**
+ * Start a new frame for rendering to the window.
+ * Needs to be paired with a call to EndFrame() if the return value is not
+ * Nothing().
+ *
+ * aInvalidRegion is the invalid region of the window.
+ * aClipRect is the clip rect for all drawing (optional).
+ * aRenderBounds is the bounding rect for rendering.
+ * aOpaqueRegion is the area that contains opaque content.
+ * All coordinates are in window space.
+ *
+ * Returns the non-empty render bounds actually used by the compositor in
+ * window space, or Nothing() if composition should be aborted.
+ */
+ virtual Maybe<gfx::IntRect> BeginFrameForWindow(
+ const nsIntRegion& aInvalidRegion, const Maybe<gfx::IntRect>& aClipRect,
+ const gfx::IntRect& aRenderBounds, const nsIntRegion& aOpaqueRegion) = 0;
+
+ /**
+ * Flush the current frame to the screen and tidy up.
+ *
+ * Derived class overriding this should call Compositor::EndFrame.
+ */
+ virtual void EndFrame();
+
+ virtual void CancelFrame(bool aNeedFlush = true) {}
+
+#ifdef MOZ_DUMP_PAINTING
+ virtual const char* Name() const = 0;
+#endif // MOZ_DUMP_PAINTING
+
+ virtual CompositorD3D11* AsCompositorD3D11() { return nullptr; }
+
+ Compositor* AsCompositor() override { return this; }
+
+ TimeStamp GetLastCompositionEndTime() const override {
+ return mLastCompositionEndTime;
+ }
+
+ /**
+ * Notify the compositor that composition is being paused. This allows the
+ * compositor to temporarily release any resources.
+ * Between calling Pause and Resume, compositing may fail.
+ */
+ virtual void Pause() {}
+ /**
+ * Notify the compositor that composition is being resumed. The compositor
+ * regain any resources it requires for compositing.
+ * Returns true if succeeded.
+ */
+ virtual bool Resume() { return true; }
+
+ widget::CompositorWidget* GetWidget() const { return mWidget; }
+
+ /**
+ * Request the compositor to allow recording its frames.
+ *
+ * This is a noop on |CompositorOGL|.
+ */
+ virtual void RequestAllowFrameRecording(bool aWillRecord) {
+ mRecordFrames = aWillRecord;
+ }
+
+ protected:
+ /**
+ * Whether or not the compositor should be prepared to record frames. While
+ * this returns true, compositors are expected to maintain a full window
+ * render target that they return from GetWindowRenderTarget() between
+ * NormalDrawingDone() and EndFrame().
+ *
+ * This will be true when either we are recording a profile with screenshots
+ * enabled or the |LayerManagerComposite| has requested us to record frames
+ * for the |CompositionRecorder|.
+ */
+ bool ShouldRecordFrames() const;
+
+ /**
+ * Last Composition end time.
+ */
+ TimeStamp mLastCompositionEndTime;
+
+ widget::CompositorWidget* mWidget;
+
+ bool mIsDestroyed;
+
+ gfx::DeviceColor mClearColor;
+
+ bool mRecordFrames = false;
+
+ private:
+ static LayersBackend sBackend;
+};
+
+// Returns the number of rects. (Up to 4)
+typedef gfx::Rect decomposedRectArrayT[4];
+size_t DecomposeIntoNoRepeatRects(const gfx::Rect& aRect,
+ const gfx::Rect& aTexCoordRect,
+ decomposedRectArrayT* aLayerRects,
+ decomposedRectArrayT* aTextureRects);
+
+static inline bool BlendOpIsMixBlendMode(gfx::CompositionOp aOp) {
+ switch (aOp) {
+ case gfx::CompositionOp::OP_MULTIPLY:
+ case gfx::CompositionOp::OP_SCREEN:
+ case gfx::CompositionOp::OP_OVERLAY:
+ case gfx::CompositionOp::OP_DARKEN:
+ case gfx::CompositionOp::OP_LIGHTEN:
+ case gfx::CompositionOp::OP_COLOR_DODGE:
+ case gfx::CompositionOp::OP_COLOR_BURN:
+ case gfx::CompositionOp::OP_HARD_LIGHT:
+ case gfx::CompositionOp::OP_SOFT_LIGHT:
+ case gfx::CompositionOp::OP_DIFFERENCE:
+ case gfx::CompositionOp::OP_EXCLUSION:
+ case gfx::CompositionOp::OP_HUE:
+ case gfx::CompositionOp::OP_SATURATION:
+ case gfx::CompositionOp::OP_COLOR:
+ case gfx::CompositionOp::OP_LUMINOSITY:
+ return true;
+ default:
+ return false;
+ }
+}
+
+class AsyncReadbackBuffer {
+ public:
+ NS_INLINE_DECL_REFCOUNTING(AsyncReadbackBuffer)
+
+ gfx::IntSize GetSize() const { return mSize; }
+ virtual bool MapAndCopyInto(gfx::DataSourceSurface* aSurface,
+ const gfx::IntSize& aReadSize) const = 0;
+
+ protected:
+ explicit AsyncReadbackBuffer(const gfx::IntSize& aSize) : mSize(aSize) {}
+ virtual ~AsyncReadbackBuffer() = default;
+
+ gfx::IntSize mSize;
+};
+
+struct TexturedVertex {
+ float position[2];
+ float texCoords[2];
+};
+
+nsTArray<TexturedVertex> TexturedTrianglesToVertexArray(
+ const nsTArray<gfx::TexturedTriangle>& aTriangles);
+
+} // namespace layers
+} // namespace mozilla
+
+#endif /* MOZILLA_GFX_COMPOSITOR_H */
diff --git a/gfx/layers/CompositorAnimationStorage.cpp b/gfx/layers/CompositorAnimationStorage.cpp
new file mode 100644
index 0000000000..8def6e3107
--- /dev/null
+++ b/gfx/layers/CompositorAnimationStorage.cpp
@@ -0,0 +1,429 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "CompositorAnimationStorage.h"
+
+#include "AnimationHelper.h"
+#include "mozilla/gfx/MatrixFwd.h"
+#include "mozilla/layers/APZSampler.h" // for APZSampler
+#include "mozilla/layers/CompositorBridgeParent.h" // for CompositorBridgeParent
+#include "mozilla/layers/CompositorThread.h" // for CompositorThreadHolder
+#include "mozilla/layers/OMTAController.h" // for OMTAController
+#include "mozilla/ProfilerMarkers.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/ServoStyleConsts.h"
+#include "mozilla/webrender/WebRenderTypes.h" // for ToWrTransformProperty, etc
+#include "nsDeviceContext.h" // for AppUnitsPerCSSPixel
+#include "nsDisplayList.h" // for nsDisplayTransform, etc
+#include "nsLayoutUtils.h"
+#include "TreeTraversal.h" // for ForEachNode, BreadthFirstSearch
+
+namespace geckoprofiler::markers {
+
+using namespace mozilla;
+
+struct CompositorAnimationMarker {
+ static constexpr Span<const char> MarkerTypeName() {
+ return MakeStringSpan("CompositorAnimation");
+ }
+ static void StreamJSONMarkerData(baseprofiler::SpliceableJSONWriter& aWriter,
+ uint64_t aId, nsCSSPropertyID aProperty) {
+ aWriter.IntProperty("pid", int64_t(aId >> 32));
+ aWriter.IntProperty("id", int64_t(aId & 0xffffffff));
+ aWriter.StringProperty("property", nsCSSProps::GetStringValue(aProperty));
+ }
+ static MarkerSchema MarkerTypeDisplay() {
+ using MS = MarkerSchema;
+ MS schema{MS::Location::MarkerChart, MS::Location::MarkerTable};
+ schema.AddKeyLabelFormat("pid", "Process Id", MS::Format::Integer);
+ schema.AddKeyLabelFormat("id", "Animation Id", MS::Format::Integer);
+ schema.AddKeyLabelFormat("property", "Animated Property",
+ MS::Format::String);
+ schema.SetTableLabel("{marker.name} - {marker.data.property}");
+ return schema;
+ }
+};
+
+} // namespace geckoprofiler::markers
+
+namespace mozilla {
+namespace layers {
+
+using gfx::Matrix4x4;
+
+void CompositorAnimationStorage::ClearById(const uint64_t& aId) {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+ MutexAutoLock lock(mLock);
+
+ const auto& animationStorageData = mAnimations[aId];
+ if (animationStorageData) {
+ PROFILER_MARKER("ClearAnimation", GRAPHICS,
+ MarkerInnerWindowId(mCompositorBridge->GetInnerWindowId()),
+ CompositorAnimationMarker, aId,
+ animationStorageData->mAnimation.LastElement().mProperty);
+ }
+
+ mAnimatedValues.Remove(aId);
+ mAnimations.erase(aId);
+}
+
+bool CompositorAnimationStorage::HasAnimations() const {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+ MutexAutoLock lock(mLock);
+
+ return !mAnimations.empty();
+}
+
+AnimatedValue* CompositorAnimationStorage::GetAnimatedValue(
+ const uint64_t& aId) const {
+ mLock.AssertCurrentThreadOwns();
+
+ return mAnimatedValues.Get(aId);
+}
+
+OMTAValue CompositorAnimationStorage::GetOMTAValue(const uint64_t& aId) const {
+ MutexAutoLock lock(mLock);
+
+ OMTAValue omtaValue = mozilla::null_t();
+ auto animatedValue = GetAnimatedValue(aId);
+ if (!animatedValue) {
+ return omtaValue;
+ }
+
+ animatedValue->Value().match(
+ [&](const AnimationTransform& aTransform) {
+ gfx::Matrix4x4 transform = aTransform.mFrameTransform;
+ const TransformData& data = aTransform.mData;
+ float scale = data.appUnitsPerDevPixel();
+ gfx::Point3D transformOrigin = data.transformOrigin();
+
+ // Undo the rebasing applied by
+ // nsDisplayTransform::GetResultingTransformMatrixInternal
+ transform.ChangeBasis(-transformOrigin);
+
+ // Convert to CSS pixels (this undoes the operations performed by
+ // nsStyleTransformMatrix::ProcessTranslatePart which is called from
+ // nsDisplayTransform::GetResultingTransformMatrix)
+ double devPerCss = double(scale) / double(AppUnitsPerCSSPixel());
+ transform._41 *= devPerCss;
+ transform._42 *= devPerCss;
+ transform._43 *= devPerCss;
+ omtaValue = transform;
+ },
+ [&](const float& aOpacity) { omtaValue = aOpacity; },
+ [&](const nscolor& aColor) { omtaValue = aColor; });
+ return omtaValue;
+}
+
+void CompositorAnimationStorage::SetAnimatedValue(
+ uint64_t aId, AnimatedValue* aPreviousValue,
+ const gfx::Matrix4x4& aFrameTransform, const TransformData& aData) {
+ mLock.AssertCurrentThreadOwns();
+
+ if (!aPreviousValue) {
+ MOZ_ASSERT(!mAnimatedValues.Contains(aId));
+ mAnimatedValues.InsertOrUpdate(
+ aId,
+ MakeUnique<AnimatedValue>(gfx::Matrix4x4(), aFrameTransform, aData));
+ return;
+ }
+ MOZ_ASSERT(aPreviousValue->Is<AnimationTransform>());
+ MOZ_ASSERT(aPreviousValue == GetAnimatedValue(aId));
+
+ aPreviousValue->SetTransform(aFrameTransform, aData);
+}
+
+void CompositorAnimationStorage::SetAnimatedValue(uint64_t aId,
+ AnimatedValue* aPreviousValue,
+ nscolor aColor) {
+ mLock.AssertCurrentThreadOwns();
+
+ if (!aPreviousValue) {
+ MOZ_ASSERT(!mAnimatedValues.Contains(aId));
+ mAnimatedValues.InsertOrUpdate(aId, MakeUnique<AnimatedValue>(aColor));
+ return;
+ }
+
+ MOZ_ASSERT(aPreviousValue->Is<nscolor>());
+ MOZ_ASSERT(aPreviousValue == GetAnimatedValue(aId));
+ aPreviousValue->SetColor(aColor);
+}
+
+void CompositorAnimationStorage::SetAnimatedValue(uint64_t aId,
+ AnimatedValue* aPreviousValue,
+ float aOpacity) {
+ mLock.AssertCurrentThreadOwns();
+
+ if (!aPreviousValue) {
+ MOZ_ASSERT(!mAnimatedValues.Contains(aId));
+ mAnimatedValues.InsertOrUpdate(aId, MakeUnique<AnimatedValue>(aOpacity));
+ return;
+ }
+
+ MOZ_ASSERT(aPreviousValue->Is<float>());
+ MOZ_ASSERT(aPreviousValue == GetAnimatedValue(aId));
+ aPreviousValue->SetOpacity(aOpacity);
+}
+
+void CompositorAnimationStorage::SetAnimations(uint64_t aId,
+ const LayersId& aLayersId,
+ const AnimationArray& aValue) {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+ MutexAutoLock lock(mLock);
+
+ mAnimations[aId] = std::make_unique<AnimationStorageData>(
+ AnimationHelper::ExtractAnimations(aLayersId, aValue));
+
+ PROFILER_MARKER("SetAnimation", GRAPHICS,
+ MarkerInnerWindowId(mCompositorBridge->GetInnerWindowId()),
+ CompositorAnimationMarker, aId,
+ mAnimations[aId]->mAnimation.LastElement().mProperty);
+
+ // If there is the last animated value, then we need to store the id to remove
+ // the value if the new animation doesn't produce any animated data (i.e. in
+ // the delay phase) when we sample this new animation.
+ if (mAnimatedValues.Contains(aId)) {
+ mNewAnimations.insert(aId);
+ }
+}
+
+// Returns clip rect in the scroll frame's coordinate space.
+static ParentLayerRect GetClipRectForPartialPrerender(
+ const LayersId aLayersId, const PartialPrerenderData& aPartialPrerenderData,
+ const RefPtr<APZSampler>& aSampler, const MutexAutoLock& aProofOfMapLock) {
+ if (aSampler &&
+ aPartialPrerenderData.scrollId() != ScrollableLayerGuid::NULL_SCROLL_ID) {
+ return aSampler->GetCompositionBounds(
+ aLayersId, aPartialPrerenderData.scrollId(), aProofOfMapLock);
+ }
+
+ return aPartialPrerenderData.clipRect();
+}
+
+void CompositorAnimationStorage::StoreAnimatedValue(
+ nsCSSPropertyID aProperty, uint64_t aId,
+ const std::unique_ptr<AnimationStorageData>& aAnimationStorageData,
+ const AutoTArray<RefPtr<StyleAnimationValue>, 1>& aAnimationValues,
+ const MutexAutoLock& aProofOfMapLock, const RefPtr<APZSampler>& aApzSampler,
+ AnimatedValue* aAnimatedValueEntry,
+ JankedAnimationMap& aJankedAnimationMap) {
+ switch (aProperty) {
+ case eCSSProperty_background_color: {
+ SetAnimatedValue(aId, aAnimatedValueEntry,
+ Servo_AnimationValue_GetColor(aAnimationValues[0],
+ NS_RGBA(0, 0, 0, 0)));
+ break;
+ }
+ case eCSSProperty_opacity: {
+ MOZ_ASSERT(aAnimationValues.Length() == 1);
+ SetAnimatedValue(aId, aAnimatedValueEntry,
+ Servo_AnimationValue_GetOpacity(aAnimationValues[0]));
+ break;
+ }
+ case eCSSProperty_rotate:
+ case eCSSProperty_scale:
+ case eCSSProperty_translate:
+ case eCSSProperty_transform:
+ case eCSSProperty_offset_path:
+ case eCSSProperty_offset_distance:
+ case eCSSProperty_offset_rotate:
+ case eCSSProperty_offset_anchor: {
+ MOZ_ASSERT(aAnimationStorageData->mTransformData);
+
+ const TransformData& transformData =
+ *aAnimationStorageData->mTransformData;
+ MOZ_ASSERT(transformData.origin() == nsPoint());
+
+ gfx::Matrix4x4 frameTransform =
+ AnimationHelper::ServoAnimationValueToMatrix4x4(
+ aAnimationValues, transformData,
+ aAnimationStorageData->mCachedMotionPath);
+
+ if (const Maybe<PartialPrerenderData>& partialPrerenderData =
+ transformData.partialPrerenderData()) {
+ gfx::Matrix4x4 transform = frameTransform;
+ transform.PostTranslate(
+ partialPrerenderData->position().ToUnknownPoint());
+
+ gfx::Matrix4x4 transformInClip =
+ partialPrerenderData->transformInClip();
+ if (aApzSampler && partialPrerenderData->scrollId() !=
+ ScrollableLayerGuid::NULL_SCROLL_ID) {
+ AsyncTransform asyncTransform = aApzSampler->GetCurrentAsyncTransform(
+ aAnimationStorageData->mLayersId,
+ partialPrerenderData->scrollId(), LayoutAndVisual,
+ aProofOfMapLock);
+ transformInClip.PostTranslate(
+ asyncTransform.mTranslation.ToUnknownPoint());
+ }
+ transformInClip = transform * transformInClip;
+
+ ParentLayerRect clipRect = GetClipRectForPartialPrerender(
+ aAnimationStorageData->mLayersId, *partialPrerenderData,
+ aApzSampler, aProofOfMapLock);
+ if (AnimationHelper::ShouldBeJank(
+ partialPrerenderData->rect(),
+ partialPrerenderData->overflowedSides(), transformInClip,
+ clipRect)) {
+ if (aAnimatedValueEntry) {
+ frameTransform = aAnimatedValueEntry->Transform().mFrameTransform;
+ }
+ aJankedAnimationMap[aAnimationStorageData->mLayersId].AppendElement(
+ aId);
+ }
+ }
+
+ SetAnimatedValue(aId, aAnimatedValueEntry, frameTransform, transformData);
+ break;
+ }
+ default:
+ MOZ_ASSERT_UNREACHABLE("Unhandled animated property");
+ }
+}
+
+bool CompositorAnimationStorage::SampleAnimations(
+ const OMTAController* aOMTAController, TimeStamp aPreviousFrameTime,
+ TimeStamp aCurrentFrameTime) {
+ MutexAutoLock lock(mLock);
+
+ bool isAnimating = false;
+ auto cleanup = MakeScopeExit([&] { mNewAnimations.clear(); });
+
+ // Do nothing if there are no compositor animations
+ if (mAnimations.empty()) {
+ return isAnimating;
+ }
+
+ JankedAnimationMap janked;
+ RefPtr<APZSampler> apzSampler = mCompositorBridge->GetAPZSampler();
+
+ auto callback = [&](const MutexAutoLock& aProofOfMapLock) {
+ for (const auto& iter : mAnimations) {
+ const auto& animationStorageData = iter.second;
+ if (animationStorageData->mAnimation.IsEmpty()) {
+ continue;
+ }
+
+ const nsCSSPropertyID lastPropertyAnimationGroupProperty =
+ animationStorageData->mAnimation.LastElement().mProperty;
+ isAnimating = true;
+ AutoTArray<RefPtr<StyleAnimationValue>, 1> animationValues;
+ AnimatedValue* previousValue = GetAnimatedValue(iter.first);
+ AnimationHelper::SampleResult sampleResult =
+ AnimationHelper::SampleAnimationForEachNode(
+ apzSampler, animationStorageData->mLayersId, aProofOfMapLock,
+ aPreviousFrameTime, aCurrentFrameTime, previousValue,
+ animationStorageData->mAnimation, animationValues);
+
+ PROFILER_MARKER(
+ "SampleAnimation", GRAPHICS,
+ MarkerOptions(
+ MarkerThreadId(CompositorThreadHolder::GetThreadId()),
+ MarkerInnerWindowId(mCompositorBridge->GetInnerWindowId())),
+ CompositorAnimationMarker, iter.first,
+ lastPropertyAnimationGroupProperty);
+
+ if (!sampleResult.IsSampled()) {
+ // Note: Checking new animations first. If new animations arrive and we
+ // scroll back to delay phase in the meantime for scroll-driven
+ // animations, removing the previous animated value is still the
+ // preferable way because the newly animation case would probably more
+ // often than the scroll timeline. Besides, we expect the display items
+ // get sync with main thread at this moment, so dropping the old
+ // animation sampled result is more suitable.
+ // FIXME: Bug 1791884: We might have to revisit this to make sure we
+ // respect animation composition order.
+ if (mNewAnimations.find(iter.first) != mNewAnimations.end()) {
+ mAnimatedValues.Remove(iter.first);
+ } else if (sampleResult.mReason ==
+ AnimationHelper::SampleResult::Reason::ScrollToDelayPhase) {
+ // For the scroll-driven animations, its animation effect phases may
+ // be changed between the active phase and the before/after phase.
+ // Basically we don't produce any sampled animation value for
+ // before/after phase (if we don't have fills). In this case, we have
+ // to make sure the animations are not applied on the compositor.
+ // Removing the previous animated value is not enough because the
+ // display item in webrender may be out-of-date. Therefore, we should
+ // not just skip these animation values. Instead, storing their base
+ // styles (which are in |animationValues| already) to simulate these
+ // delayed animations.
+ //
+ // There are two possible situations:
+ // 1. If there is just a new animation arrived but there is no sampled
+ // animation value when we go from active phase, we remove the
+ // previous AnimatedValue. This is done in the above condition.
+ // 2. If |animation| is not replaced by a new arrived one, we detect
+ // it in SampleAnimationForProperty(), which sets
+ // ScrollToDelayPhase if it goes from the active phase to the
+ // before/after phase.
+ //
+ // For the 2nd case, we store the base styles until we have some other
+ // new sampled results or the new animations arrived (i.e. case 1).
+ StoreAnimatedValue(lastPropertyAnimationGroupProperty, iter.first,
+ animationStorageData, animationValues,
+ aProofOfMapLock, apzSampler, previousValue,
+ janked);
+ }
+ continue;
+ }
+
+ // Store the normal sampled result.
+ StoreAnimatedValue(lastPropertyAnimationGroupProperty, iter.first,
+ animationStorageData, animationValues, aProofOfMapLock,
+ apzSampler, previousValue, janked);
+ }
+ };
+
+ if (apzSampler) {
+ // Hold APZCTreeManager::mMapLock for all the animations inside this
+ // CompositorBridgeParent. This ensures that APZ cannot process a
+ // transaction on the updater thread in between sampling different
+ // animations.
+ apzSampler->CallWithMapLock(callback);
+ } else {
+ // A fallback way if we don't have |apzSampler|. We don't care about
+ // APZCTreeManager::mMapLock in this case because we don't use any APZ
+ // interface.
+ mozilla::Mutex dummy("DummyAPZMapLock");
+ MutexAutoLock lock(dummy);
+ callback(lock);
+ }
+
+ if (!janked.empty() && aOMTAController) {
+ aOMTAController->NotifyJankedAnimations(std::move(janked));
+ }
+
+ return isAnimating;
+}
+
+WrAnimations CompositorAnimationStorage::CollectWebRenderAnimations() const {
+ MutexAutoLock lock(mLock);
+
+ WrAnimations animations;
+
+ for (const auto& animatedValueEntry : mAnimatedValues) {
+ AnimatedValue* value = animatedValueEntry.GetWeak();
+ value->Value().match(
+ [&](const AnimationTransform& aTransform) {
+ animations.mTransformArrays.AppendElement(wr::ToWrTransformProperty(
+ animatedValueEntry.GetKey(), aTransform.mFrameTransform));
+ },
+ [&](const float& aOpacity) {
+ animations.mOpacityArrays.AppendElement(
+ wr::ToWrOpacityProperty(animatedValueEntry.GetKey(), aOpacity));
+ },
+ [&](const nscolor& aColor) {
+ animations.mColorArrays.AppendElement(wr::ToWrColorProperty(
+ animatedValueEntry.GetKey(),
+ ToDeviceColor(gfx::sRGBColor::FromABGR(aColor))));
+ });
+ }
+
+ return animations;
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/CompositorAnimationStorage.h b/gfx/layers/CompositorAnimationStorage.h
new file mode 100644
index 0000000000..7d6b7dc3a7
--- /dev/null
+++ b/gfx/layers/CompositorAnimationStorage.h
@@ -0,0 +1,213 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_CompositorAnimationStorage_h
+#define mozilla_layers_CompositorAnimationStorage_h
+
+#include "mozilla/layers/AnimationStorageData.h"
+#include "mozilla/layers/LayersMessages.h" // for TransformData, etc
+#include "mozilla/webrender/webrender_ffi.h"
+#include "mozilla/Variant.h"
+#include "nsClassHashtable.h"
+#include "X11UndefineNone.h"
+#include <memory>
+#include <unordered_map>
+#include <unordered_set>
+
+namespace mozilla {
+namespace layers {
+class APZSampler;
+class Animation;
+class CompositorBridgeParent;
+class OMTAController;
+
+typedef nsTArray<layers::Animation> AnimationArray;
+
+struct AnimationTransform {
+ /*
+ * This transform is calculated from sampleanimation in device pixel
+ * and used for layers (i.e. non WebRender)
+ */
+ gfx::Matrix4x4 mTransformInDevSpace;
+ /*
+ * This transform is calculated from frame used for WebRender and used by
+ * getOMTAStyle() for OMTA testing.
+ */
+ gfx::Matrix4x4 mFrameTransform;
+ TransformData mData;
+};
+
+struct AnimatedValue final {
+ typedef Variant<AnimationTransform, float, nscolor> AnimatedValueType;
+
+ const AnimatedValueType& Value() const { return mValue; }
+ const AnimationTransform& Transform() const {
+ return mValue.as<AnimationTransform>();
+ }
+ const float& Opacity() const { return mValue.as<float>(); }
+ const nscolor& Color() const { return mValue.as<nscolor>(); }
+ template <typename T>
+ bool Is() const {
+ return mValue.is<T>();
+ }
+
+ AnimatedValue(const gfx::Matrix4x4& aTransformInDevSpace,
+ const gfx::Matrix4x4& aFrameTransform,
+ const TransformData& aData)
+ : mValue(AsVariant(AnimationTransform{aTransformInDevSpace,
+ aFrameTransform, aData})) {}
+
+ explicit AnimatedValue(const float& aValue) : mValue(AsVariant(aValue)) {}
+
+ explicit AnimatedValue(nscolor aValue) : mValue(AsVariant(aValue)) {}
+
+ void SetTransform(const gfx::Matrix4x4& aFrameTransform,
+ const TransformData& aData) {
+ MOZ_ASSERT(mValue.is<AnimationTransform>());
+ AnimationTransform& previous = mValue.as<AnimationTransform>();
+ previous.mFrameTransform = aFrameTransform;
+ if (previous.mData != aData) {
+ previous.mData = aData;
+ }
+ }
+ void SetOpacity(float aOpacity) {
+ MOZ_ASSERT(mValue.is<float>());
+ mValue.as<float>() = aOpacity;
+ }
+ void SetColor(nscolor aColor) {
+ MOZ_ASSERT(mValue.is<nscolor>());
+ mValue.as<nscolor>() = aColor;
+ }
+
+ private:
+ AnimatedValueType mValue;
+};
+
+struct WrAnimations {
+ nsTArray<wr::WrOpacityProperty> mOpacityArrays;
+ nsTArray<wr::WrTransformProperty> mTransformArrays;
+ nsTArray<wr::WrColorProperty> mColorArrays;
+};
+
+// CompositorAnimationStorage stores the animations and animated values
+// keyed by a CompositorAnimationsId. The "animations" are a representation of
+// an entire animation over time, while the "animated values" are values sampled
+// from the animations at a particular point in time.
+//
+// There is one CompositorAnimationStorage per CompositorBridgeParent (i.e.
+// one per browser window), and the CompositorAnimationsId key is unique within
+// a particular CompositorAnimationStorage instance.
+//
+// Each layer which has animations gets a CompositorAnimationsId key, and reuses
+// that key during its lifetime. Likewise, in layers-free webrender, a display
+// item that is animated (e.g. nsDisplayTransform) gets a CompositorAnimationsId
+// key and reuses that key (it persists the key via the frame user-data
+// mechanism).
+class CompositorAnimationStorage final {
+ typedef nsClassHashtable<nsUint64HashKey, AnimatedValue> AnimatedValueTable;
+ typedef std::unordered_map<uint64_t, std::unique_ptr<AnimationStorageData>>
+ AnimationsTable;
+
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(CompositorAnimationStorage)
+ public:
+ explicit CompositorAnimationStorage(CompositorBridgeParent* aCompositorBridge)
+ : mLock("CompositorAnimationStorage::mLock"),
+ mCompositorBridge(aCompositorBridge) {}
+
+ OMTAValue GetOMTAValue(const uint64_t& aId) const;
+
+ /**
+ * Collect all animations in this class as WebRender type properties.
+ */
+ WrAnimations CollectWebRenderAnimations() const;
+
+ /**
+ * Set the animations based on the unique id
+ */
+ void SetAnimations(uint64_t aId, const LayersId& aLayersId,
+ const AnimationArray& aAnimations);
+
+ /**
+ * Sample animation based the given timestamps and store them in this
+ * CompositorAnimationStorage. The animated values after sampling will be
+ * stored in CompositorAnimationStorage as well.
+ *
+ * Returns true if there is any animation.
+ * Note that even if there are only in-delay phase animations (i.e. not
+ * visually effective), this function returns true to ensure we composite
+ * again on the next tick.
+ *
+ * Note: This is called only by WebRender.
+ */
+ bool SampleAnimations(const OMTAController* aOMTAController,
+ TimeStamp aPreviousFrameTime,
+ TimeStamp aCurrentFrameTime);
+
+ bool HasAnimations() const;
+
+ /**
+ * Clear AnimatedValues and Animations data
+ */
+ void ClearById(const uint64_t& aId);
+
+ private:
+ ~CompositorAnimationStorage() = default;
+
+ /**
+ * Return the animated value if a given id can map to its animated value
+ */
+ AnimatedValue* GetAnimatedValue(const uint64_t& aId) const;
+
+ /**
+ * Set the animation transform based on the unique id and also
+ * set up |aFrameTransform| and |aData| for OMTA testing.
+ * If |aPreviousValue| is not null, the animation transform replaces the value
+ * in the |aPreviousValue|.
+ * NOTE: |aPreviousValue| should be the value for the |aId|.
+ */
+ void SetAnimatedValue(uint64_t aId, AnimatedValue* aPreviousValue,
+ const gfx::Matrix4x4& aFrameTransform,
+ const TransformData& aData);
+
+ /**
+ * Similar to above but for opacity.
+ */
+ void SetAnimatedValue(uint64_t aId, AnimatedValue* aPreviousValue,
+ float aOpacity);
+
+ /**
+ * Similar to above but for color.
+ */
+ void SetAnimatedValue(uint64_t aId, AnimatedValue* aPreviousValue,
+ nscolor aColor);
+
+ using JankedAnimationMap =
+ std::unordered_map<LayersId, nsTArray<uint64_t>, LayersId::HashFn>;
+
+ /*
+ * Store the animated values from |aAnimationValues|.
+ */
+ void StoreAnimatedValue(
+ nsCSSPropertyID aProperty, uint64_t aId,
+ const std::unique_ptr<AnimationStorageData>& aAnimationStorageData,
+ const AutoTArray<RefPtr<StyleAnimationValue>, 1>& aAnimationValues,
+ const MutexAutoLock& aProofOfMapLock,
+ const RefPtr<APZSampler>& aApzSampler, AnimatedValue* aAnimatedValueEntry,
+ JankedAnimationMap& aJankedAnimationMap);
+
+ private:
+ AnimatedValueTable mAnimatedValues;
+ AnimationsTable mAnimations;
+ std::unordered_set<uint64_t> mNewAnimations;
+ mutable Mutex mLock MOZ_UNANNOTATED;
+ // CompositorBridgeParent owns this CompositorAnimationStorage instance.
+ CompositorBridgeParent* MOZ_NON_OWNING_REF mCompositorBridge;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_CompositorAnimationStorage_h
diff --git a/gfx/layers/CompositorTypes.cpp b/gfx/layers/CompositorTypes.cpp
new file mode 100644
index 0000000000..99df293793
--- /dev/null
+++ b/gfx/layers/CompositorTypes.cpp
@@ -0,0 +1,53 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "CompositorTypes.h"
+
+#include <ostream>
+
+namespace mozilla {
+namespace layers {
+
+std::ostream& operator<<(std::ostream& aStream, const TextureFlags& aFlags) {
+ if (aFlags == TextureFlags::NO_FLAGS) {
+ aStream << "NoFlags";
+ } else {
+#define AppendFlag(test) \
+ { \
+ if (!!(aFlags & (test))) { \
+ if (previous) { \
+ aStream << "|"; \
+ } \
+ aStream << #test; \
+ previous = true; \
+ } \
+ }
+ bool previous = false;
+ AppendFlag(TextureFlags::USE_NEAREST_FILTER);
+ AppendFlag(TextureFlags::ORIGIN_BOTTOM_LEFT);
+ AppendFlag(TextureFlags::DISALLOW_BIGIMAGE);
+ AppendFlag(TextureFlags::RB_SWAPPED);
+ AppendFlag(TextureFlags::NON_PREMULTIPLIED);
+ AppendFlag(TextureFlags::RECYCLE);
+ AppendFlag(TextureFlags::DEALLOCATE_CLIENT);
+ AppendFlag(TextureFlags::DEALLOCATE_SYNC);
+ AppendFlag(TextureFlags::IMMUTABLE);
+ AppendFlag(TextureFlags::IMMEDIATE_UPLOAD);
+ AppendFlag(TextureFlags::COMPONENT_ALPHA);
+ AppendFlag(TextureFlags::INVALID_COMPOSITOR);
+ AppendFlag(TextureFlags::RGB_FROM_YCBCR);
+ AppendFlag(TextureFlags::SNAPSHOT);
+ AppendFlag(TextureFlags::NON_BLOCKING_READ_LOCK);
+ AppendFlag(TextureFlags::BLOCKING_READ_LOCK);
+ AppendFlag(TextureFlags::WAIT_HOST_USAGE_END);
+ AppendFlag(TextureFlags::IS_OPAQUE);
+#undef AppendFlag
+ }
+ return aStream;
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/CompositorTypes.h b/gfx/layers/CompositorTypes.h
new file mode 100644
index 0000000000..60cc931855
--- /dev/null
+++ b/gfx/layers/CompositorTypes.h
@@ -0,0 +1,292 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_LAYERS_COMPOSITORTYPES_H
+#define MOZILLA_LAYERS_COMPOSITORTYPES_H
+
+#include <iosfwd>
+#include <stdint.h> // for uint32_t
+#include <sys/types.h> // for int32_t
+#include "LayersTypes.h" // for LayersBackend, etc
+#include "nsXULAppAPI.h" // for GeckoProcessType, etc
+#include "mozilla/gfx/Types.h"
+#include "mozilla/EnumSet.h"
+
+#include "mozilla/TypedEnumBits.h"
+
+namespace mozilla {
+namespace layers {
+
+/**
+ * Flags used by texture clients and texture hosts. These are passed from client
+ * side to host side when textures and compositables are created. Usually set
+ * by the compositableCient, they may be modified by either the compositable or
+ * texture clients.
+ */
+enum class TextureFlags : uint32_t {
+ NO_FLAGS = 0,
+ // Use nearest-neighbour texture filtering (as opposed to linear filtering).
+ USE_NEAREST_FILTER = 1 << 0,
+ // The compositor assumes everything is origin-top-left by default.
+ ORIGIN_BOTTOM_LEFT = 1 << 1,
+ // Force the texture to be represented using a single tile (note that this
+ // means tiled textures, not tiled layers).
+ DISALLOW_BIGIMAGE = 1 << 2,
+ // The buffer will be treated as if the RB bytes are swapped.
+ // This is useful for rendering using Cairo/Thebes, because there is no
+ // BGRX Android pixel format, and so we have to do byte swapping.
+ //
+ // For example, if the GraphicBuffer has an Android pixel format of
+ // PIXEL_FORMAT_RGBA_8888 and isRBSwapped is true, when it is sampled
+ // (for example, with GL), a BGRA shader should be used.
+ RB_SWAPPED = 1 << 3,
+ // Data in this texture has not been alpha-premultiplied.
+ // XXX - Apparently only used with ImageClient/Host
+ NON_PREMULTIPLIED = 1 << 4,
+ // The TextureClient should be recycled with recycle callback when no longer
+ // in used. When the texture is used in host side, ref count of TextureClient
+ // is transparently added by ShadowLayerForwarder or ImageBridgeChild.
+ RECYCLE = 1 << 5,
+ // If DEALLOCATE_CLIENT is set, the shared data is deallocated on the
+ // client side and requires some extra synchronizaion to ensure race-free
+ // deallocation.
+ // The default behaviour is to deallocate on the host side.
+ DEALLOCATE_CLIENT = 1 << 6,
+ DEALLOCATE_SYNC = 1 << 6, // XXX - make it a separate flag.
+ // After being shared ith the compositor side, an immutable texture is never
+ // modified, it can only be read. It is safe to not Lock/Unlock immutable
+ // textures.
+ IMMUTABLE = 1 << 9,
+ // The contents of the texture must be uploaded or copied immediately
+ // during the transaction, because the producer may want to write
+ // to it again.
+ IMMEDIATE_UPLOAD = 1 << 10,
+ // The texture is part of a component-alpha pair
+ COMPONENT_ALPHA = 1 << 11,
+ // The texture is being allocated for a compositor that no longer exists.
+ // This flag is only used in the parent process.
+ INVALID_COMPOSITOR = 1 << 12,
+ // The texture was created by converting from YCBCR to RGB
+ RGB_FROM_YCBCR = 1 << 13,
+ // The texture is used for snapshot.
+ SNAPSHOT = 1 << 14,
+ // Enable a non blocking read lock.
+ NON_BLOCKING_READ_LOCK = 1 << 15,
+ // Enable a blocking read lock.
+ BLOCKING_READ_LOCK = 1 << 16,
+ // Keep TextureClient alive when host side is used
+ WAIT_HOST_USAGE_END = 1 << 17,
+ // The texture is guaranteed to have alpha 1.0 everywhere; some backends
+ // have trouble with RGBX/BGRX formats, so we use RGBA/BGRA but set this
+ // hint when we know alpha is opaque (eg. WebGL)
+ IS_OPAQUE = 1 << 18,
+ // The ExternalImageId bound to the texture is borrowed and should not be
+ // explicitly released when the texture is freed. This is meant to be used
+ // with WebRenderTextureHost wrapping another TextureHost which was
+ // initialized with its own external image ID.
+ BORROWED_EXTERNAL_ID = 1 << 19,
+ // The texture is used for remote texture.
+ REMOTE_TEXTURE = 1 << 20,
+ // The texture is from a DRM source.
+ DRM_SOURCE = 1 << 21,
+ // The texture is dummy texture
+ DUMMY_TEXTURE = 1 << 22,
+
+ // OR union of all valid bits
+ ALL_BITS = (1 << 23) - 1,
+ // the default flags
+ DEFAULT = NO_FLAGS
+};
+MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(TextureFlags)
+
+std::ostream& operator<<(std::ostream& aStream, const TextureFlags& aFlags);
+
+static inline bool TextureRequiresLocking(TextureFlags aFlags) {
+ // If we're not double buffered, or uploading
+ // within a transaction, then we need to support
+ // locking correctly.
+ return !(aFlags & TextureFlags::IMMUTABLE);
+}
+
+/**
+ * The type of debug diagnostic to enable.
+ */
+enum class DiagnosticTypes : uint8_t {
+ NO_DIAGNOSTIC = 0,
+ TILE_BORDERS = 1 << 0,
+ LAYER_BORDERS = 1 << 1,
+ BIGIMAGE_BORDERS = 1 << 2,
+ FLASH_BORDERS = 1 << 3,
+ ALL_BITS = (1 << 4) - 1
+};
+MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(DiagnosticTypes)
+
+/**
+ * See gfx/layers/Effects.h
+ */
+enum class EffectTypes : uint8_t {
+ RGB,
+ YCBCR,
+ NV12,
+ MAX // sentinel for the count of all effect types
+};
+
+/**
+ * How the Compositable should manage textures.
+ */
+enum class CompositableType : uint8_t {
+ UNKNOWN,
+ IMAGE, // image with single buffering
+ COUNT
+};
+
+#ifdef XP_WIN
+typedef void* SyncHandle;
+#else
+typedef uintptr_t SyncHandle;
+#endif // XP_WIN
+
+/**
+ * Sent from the compositor to the content-side LayerManager, includes
+ * properties of the compositor and should (in the future) include information
+ * about what kinds of buffer and texture clients to create.
+ */
+struct TextureFactoryIdentifier {
+ LayersBackend mParentBackend;
+ WebRenderBackend mWebRenderBackend;
+ WebRenderCompositor mWebRenderCompositor;
+ GeckoProcessType mParentProcessType;
+ int32_t mMaxTextureSize;
+ bool mCompositorUseANGLE;
+ bool mCompositorUseDComp;
+ bool mUseCompositorWnd;
+ bool mSupportsTextureBlitting;
+ bool mSupportsPartialUploads;
+ bool mSupportsComponentAlpha;
+ bool mSupportsD3D11NV12;
+ SyncHandle mSyncHandle;
+
+ explicit TextureFactoryIdentifier(
+ LayersBackend aLayersBackend = LayersBackend::LAYERS_NONE,
+ GeckoProcessType aParentProcessType = GeckoProcessType_Default,
+ int32_t aMaxTextureSize = 4096, bool aCompositorUseANGLE = false,
+ bool aCompositorUseDComp = false, bool aUseCompositorWnd = false,
+ bool aSupportsTextureBlitting = false,
+ bool aSupportsPartialUploads = false, bool aSupportsComponentAlpha = true,
+ bool aSupportsD3D11NV12 = false, SyncHandle aSyncHandle = 0)
+ : mParentBackend(aLayersBackend),
+ mWebRenderBackend(WebRenderBackend::HARDWARE),
+ mWebRenderCompositor(WebRenderCompositor::DRAW),
+ mParentProcessType(aParentProcessType),
+ mMaxTextureSize(aMaxTextureSize),
+ mCompositorUseANGLE(aCompositorUseANGLE),
+ mCompositorUseDComp(aCompositorUseDComp),
+ mUseCompositorWnd(aUseCompositorWnd),
+ mSupportsTextureBlitting(aSupportsTextureBlitting),
+ mSupportsPartialUploads(aSupportsPartialUploads),
+ mSupportsComponentAlpha(aSupportsComponentAlpha),
+ mSupportsD3D11NV12(aSupportsD3D11NV12),
+ mSyncHandle(aSyncHandle) {}
+
+ explicit TextureFactoryIdentifier(
+ WebRenderBackend aWebRenderBackend,
+ WebRenderCompositor aWebRenderCompositor,
+ GeckoProcessType aParentProcessType = GeckoProcessType_Default,
+ int32_t aMaxTextureSize = 4096, bool aCompositorUseANGLE = false,
+ bool aCompositorUseDComp = false, bool aUseCompositorWnd = false,
+ bool aSupportsTextureBlitting = false,
+ bool aSupportsPartialUploads = false, bool aSupportsComponentAlpha = true,
+ bool aSupportsD3D11NV12 = false, SyncHandle aSyncHandle = 0)
+ : mParentBackend(LayersBackend::LAYERS_WR),
+ mWebRenderBackend(aWebRenderBackend),
+ mWebRenderCompositor(aWebRenderCompositor),
+ mParentProcessType(aParentProcessType),
+ mMaxTextureSize(aMaxTextureSize),
+ mCompositorUseANGLE(aCompositorUseANGLE),
+ mCompositorUseDComp(aCompositorUseDComp),
+ mUseCompositorWnd(aUseCompositorWnd),
+ mSupportsTextureBlitting(aSupportsTextureBlitting),
+ mSupportsPartialUploads(aSupportsPartialUploads),
+ mSupportsComponentAlpha(aSupportsComponentAlpha),
+ mSupportsD3D11NV12(aSupportsD3D11NV12),
+ mSyncHandle(aSyncHandle) {}
+
+ bool operator==(const TextureFactoryIdentifier& aOther) const {
+ return mParentBackend == aOther.mParentBackend &&
+ mWebRenderBackend == aOther.mWebRenderBackend &&
+ mWebRenderCompositor == aOther.mWebRenderCompositor &&
+ mParentProcessType == aOther.mParentProcessType &&
+ mMaxTextureSize == aOther.mMaxTextureSize &&
+ mCompositorUseANGLE == aOther.mCompositorUseANGLE &&
+ mCompositorUseDComp == aOther.mCompositorUseDComp &&
+ mUseCompositorWnd == aOther.mUseCompositorWnd &&
+ mSupportsTextureBlitting == aOther.mSupportsTextureBlitting &&
+ mSupportsPartialUploads == aOther.mSupportsPartialUploads &&
+ mSupportsComponentAlpha == aOther.mSupportsComponentAlpha &&
+ mSupportsD3D11NV12 == aOther.mSupportsD3D11NV12 &&
+ mSyncHandle == aOther.mSyncHandle;
+ }
+};
+
+/**
+ * Information required by the compositor from the content-side for creating or
+ * using compositables and textures.
+ * XXX - TextureInfo is a bad name: this information is useful for the
+ * compositable, not the Texture. And ith new Textures, only the compositable
+ * type is really useful. This may (should) be removed in the near future.
+ */
+struct TextureInfo {
+ CompositableType mCompositableType;
+ TextureFlags mTextureFlags;
+
+ TextureInfo()
+ : mCompositableType(CompositableType::UNKNOWN),
+ mTextureFlags(TextureFlags::NO_FLAGS) {}
+
+ explicit TextureInfo(CompositableType aType,
+ TextureFlags aTextureFlags = TextureFlags::DEFAULT)
+ : mCompositableType(aType), mTextureFlags(aTextureFlags) {}
+
+ bool operator==(const TextureInfo& aOther) const {
+ return mCompositableType == aOther.mCompositableType &&
+ mTextureFlags == aOther.mTextureFlags;
+ }
+};
+
+/**
+ * How a SurfaceDescriptor will be opened.
+ *
+ * See ShadowLayerForwarder::OpenDescriptor for example.
+ */
+enum class OpenMode : uint8_t {
+ OPEN_NONE = 0,
+ OPEN_READ = 0x1,
+ OPEN_WRITE = 0x2,
+ // This is only used in conjunction with OMTP to indicate that the DrawTarget
+ // that is being borrowed will be painted asynchronously, and so will outlive
+ // the write lock.
+ OPEN_ASYNC = 0x04,
+
+ OPEN_READ_WRITE = OPEN_READ | OPEN_WRITE,
+ OPEN_READ_WRITE_ASYNC = OPEN_READ | OPEN_WRITE | OPEN_ASYNC,
+ OPEN_READ_ASYNC = OPEN_READ | OPEN_ASYNC,
+ OPEN_READ_ONLY = OPEN_READ,
+ OPEN_WRITE_ONLY = OPEN_WRITE,
+};
+MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(OpenMode)
+
+// The kinds of mask texture a shader can support
+// We rely on the items in this enum being sequential
+enum class MaskType : uint8_t {
+ MaskNone = 0, // no mask layer
+ Mask, // mask layer
+ NumMaskTypes
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif
diff --git a/gfx/layers/D3D11ShareHandleImage.cpp b/gfx/layers/D3D11ShareHandleImage.cpp
new file mode 100644
index 0000000000..edd1711fcf
--- /dev/null
+++ b/gfx/layers/D3D11ShareHandleImage.cpp
@@ -0,0 +1,347 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "D3D11ShareHandleImage.h"
+#include <memory>
+#include "DXVA2Manager.h"
+#include "WMF.h"
+#include "d3d11.h"
+#include "gfxImageSurface.h"
+#include "gfxWindowsPlatform.h"
+#include "libyuv.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "mozilla/gfx/DeviceManagerDx.h"
+#include "mozilla/layers/CompositableClient.h"
+#include "mozilla/layers/CompositableForwarder.h"
+#include "mozilla/layers/TextureClient.h"
+#include "mozilla/layers/TextureD3D11.h"
+
+namespace mozilla {
+namespace layers {
+
+using namespace gfx;
+
+/* static */
+RefPtr<D3D11ShareHandleImage>
+D3D11ShareHandleImage::MaybeCreateNV12ImageAndSetData(
+ KnowsCompositor* aKnowsCompositor, ImageContainer* aContainer,
+ const PlanarYCbCrData& aData) {
+ MOZ_ASSERT(aKnowsCompositor);
+ MOZ_ASSERT(aContainer);
+
+ if (!aKnowsCompositor || !aContainer) {
+ return nullptr;
+ }
+
+ // Check if data could be used with NV12
+ if (aData.YPictureSize().width % 2 != 0 ||
+ aData.YPictureSize().height % 2 != 0 || aData.mYSkip != 0 ||
+ aData.mCbSkip != 0 || aData.mCrSkip != 0 ||
+ aData.mColorDepth != gfx::ColorDepth::COLOR_8 ||
+ aData.mColorRange != gfx::ColorRange::LIMITED ||
+ aData.mChromaSubsampling !=
+ gfx::ChromaSubsampling::HALF_WIDTH_AND_HEIGHT) {
+ return nullptr;
+ }
+
+ RefPtr<D3D11ShareHandleImage> image = new D3D11ShareHandleImage(
+ aData.YPictureSize(), aData.mPictureRect,
+ ToColorSpace2(aData.mYUVColorSpace), aData.mColorRange);
+
+ RefPtr<D3D11RecycleAllocator> allocator =
+ aContainer->GetD3D11RecycleAllocator(aKnowsCompositor,
+ gfx::SurfaceFormat::NV12);
+ if (!allocator) {
+ return nullptr;
+ }
+
+ auto syncObject = allocator->GetSyncObject();
+ if (!syncObject) {
+ MOZ_ASSERT_UNREACHABLE("unexpected to be called");
+ return nullptr;
+ }
+
+ MOZ_ASSERT(allocator->GetUsableSurfaceFormat() == gfx::SurfaceFormat::NV12);
+ if (allocator->GetUsableSurfaceFormat() != gfx::SurfaceFormat::NV12) {
+ MOZ_ASSERT_UNREACHABLE("unexpected to be called");
+ return nullptr;
+ }
+
+ RefPtr<ID3D11Texture2D> stagingTexture =
+ allocator->GetStagingTextureNV12(aData.YPictureSize());
+ if (!stagingTexture) {
+ return nullptr;
+ }
+
+ bool ok = image->AllocateTexture(allocator, allocator->mDevice);
+ if (!ok) {
+ return nullptr;
+ }
+
+ RefPtr<TextureClient> client = image->GetTextureClient(nullptr);
+ if (!client) {
+ return nullptr;
+ }
+
+ // The texture does not have keyed mutex. When keyed mutex exists, the texture
+ // could not be used for video overlay. Then it needs manual synchronization
+ RefPtr<ID3D11Texture2D> texture = image->GetTexture();
+ if (!texture) {
+ return nullptr;
+ }
+
+ RefPtr<ID3D11DeviceContext> context;
+ allocator->mDevice->GetImmediateContext(getter_AddRefs(context));
+ if (!context) {
+ return nullptr;
+ }
+
+ RefPtr<ID3D10Multithread> mt;
+ HRESULT hr = allocator->mDevice->QueryInterface(
+ (ID3D10Multithread**)getter_AddRefs(mt));
+ if (FAILED(hr) || !mt) {
+ gfxCriticalError() << "Multithread safety interface not supported. " << hr;
+ return nullptr;
+ }
+
+ if (!mt->GetMultithreadProtected()) {
+ gfxCriticalError() << "Device used not marked as multithread-safe.";
+ return nullptr;
+ }
+
+ D3D11MTAutoEnter mtAutoEnter(mt.forget());
+
+ AutoLockD3D11Texture lockSt(stagingTexture);
+
+ D3D11_MAP mapType = D3D11_MAP_WRITE;
+ D3D11_MAPPED_SUBRESOURCE mappedResource;
+
+ hr = context->Map(stagingTexture, 0, mapType, 0, &mappedResource);
+ if (FAILED(hr)) {
+ gfxCriticalNoteOnce << "Mapping D3D11 staging texture failed: "
+ << gfx::hexa(hr);
+ return nullptr;
+ }
+
+ const size_t destStride = mappedResource.RowPitch;
+ uint8_t* yDestPlaneStart = reinterpret_cast<uint8_t*>(mappedResource.pData);
+ uint8_t* uvDestPlaneStart = reinterpret_cast<uint8_t*>(mappedResource.pData) +
+ destStride * aData.YPictureSize().height;
+ // Convert I420 to NV12,
+ libyuv::I420ToNV12(aData.mYChannel, aData.mYStride, aData.mCbChannel,
+ aData.mCbCrStride, aData.mCrChannel, aData.mCbCrStride,
+ yDestPlaneStart, destStride, uvDestPlaneStart, destStride,
+ aData.YDataSize().width, aData.YDataSize().height);
+
+ context->Unmap(stagingTexture, 0);
+
+ context->CopyResource(texture, stagingTexture);
+
+ context->Flush();
+
+ client->SyncWithObject(syncObject);
+ if (!syncObject->Synchronize(true)) {
+ MOZ_ASSERT_UNREACHABLE("unexpected to be called");
+ return nullptr;
+ }
+
+ return image;
+}
+
+D3D11ShareHandleImage::D3D11ShareHandleImage(const gfx::IntSize& aSize,
+ const gfx::IntRect& aRect,
+ gfx::ColorSpace2 aColorSpace,
+ gfx::ColorRange aColorRange)
+ : Image(nullptr, ImageFormat::D3D11_SHARE_HANDLE_TEXTURE),
+ mSize(aSize),
+ mPictureRect(aRect),
+ mColorSpace(aColorSpace),
+ mColorRange(aColorRange) {}
+
+bool D3D11ShareHandleImage::AllocateTexture(D3D11RecycleAllocator* aAllocator,
+ ID3D11Device* aDevice) {
+ if (aAllocator) {
+ mTextureClient =
+ aAllocator->CreateOrRecycleClient(mColorSpace, mColorRange, mSize);
+ if (mTextureClient) {
+ D3D11TextureData* textureData = GetData();
+ MOZ_DIAGNOSTIC_ASSERT(textureData, "Wrong TextureDataType");
+ mTexture = textureData->GetD3D11Texture();
+ return true;
+ }
+ return false;
+ } else {
+ MOZ_ASSERT(aDevice);
+ CD3D11_TEXTURE2D_DESC newDesc(
+ DXGI_FORMAT_B8G8R8A8_UNORM, mSize.width, mSize.height, 1, 1,
+ D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE);
+ newDesc.MiscFlags = D3D11_RESOURCE_MISC_SHARED;
+
+ HRESULT hr =
+ aDevice->CreateTexture2D(&newDesc, nullptr, getter_AddRefs(mTexture));
+ return SUCCEEDED(hr);
+ }
+}
+
+gfx::IntSize D3D11ShareHandleImage::GetSize() const { return mSize; }
+
+TextureClient* D3D11ShareHandleImage::GetTextureClient(
+ KnowsCompositor* aKnowsCompositor) {
+ return mTextureClient;
+}
+
+already_AddRefed<gfx::SourceSurface>
+D3D11ShareHandleImage::GetAsSourceSurface() {
+ RefPtr<ID3D11Texture2D> src = GetTexture();
+ if (!src) {
+ gfxWarning() << "Cannot readback from shared texture because no texture is "
+ "available.";
+ return nullptr;
+ }
+
+ return gfx::Factory::CreateBGRA8DataSourceSurfaceForD3D11Texture(src);
+}
+
+ID3D11Texture2D* D3D11ShareHandleImage::GetTexture() const { return mTexture; }
+
+class MOZ_RAII D3D11TextureClientAllocationHelper
+ : public ITextureClientAllocationHelper {
+ public:
+ D3D11TextureClientAllocationHelper(gfx::SurfaceFormat aFormat,
+ gfx::ColorSpace2 aColorSpace,
+ gfx::ColorRange aColorRange,
+ const gfx::IntSize& aSize,
+ TextureAllocationFlags aAllocFlags,
+ ID3D11Device* aDevice,
+ TextureFlags aTextureFlags)
+ : ITextureClientAllocationHelper(aFormat, aSize, BackendSelector::Content,
+ aTextureFlags, aAllocFlags),
+ mColorSpace(aColorSpace),
+ mColorRange(aColorRange),
+ mDevice(aDevice) {}
+
+ bool IsCompatible(TextureClient* aTextureClient) override {
+ D3D11TextureData* textureData =
+ aTextureClient->GetInternalData()->AsD3D11TextureData();
+ if (!textureData || aTextureClient->GetFormat() != mFormat ||
+ aTextureClient->GetSize() != mSize) {
+ return false;
+ }
+ // TODO: Should we also check for change in the allocation flags if RGBA?
+ return (aTextureClient->GetFormat() != gfx::SurfaceFormat::NV12 &&
+ aTextureClient->GetFormat() != gfx::SurfaceFormat::P010 &&
+ aTextureClient->GetFormat() != gfx::SurfaceFormat::P016) ||
+ (textureData->mColorSpace == mColorSpace &&
+ textureData->GetColorRange() == mColorRange &&
+ textureData->GetTextureAllocationFlags() == mAllocationFlags);
+ }
+
+ already_AddRefed<TextureClient> Allocate(
+ KnowsCompositor* aAllocator) override {
+ D3D11TextureData* data =
+ D3D11TextureData::Create(mSize, mFormat, mAllocationFlags, mDevice);
+ if (!data) {
+ return nullptr;
+ }
+ data->mColorSpace = mColorSpace;
+ data->SetColorRange(mColorRange);
+ return MakeAndAddRef<TextureClient>(data, mTextureFlags,
+ aAllocator->GetTextureForwarder());
+ }
+
+ private:
+ const gfx::ColorSpace2 mColorSpace;
+ const gfx::ColorRange mColorRange;
+ const RefPtr<ID3D11Device> mDevice;
+};
+
+D3D11RecycleAllocator::D3D11RecycleAllocator(
+ KnowsCompositor* aAllocator, ID3D11Device* aDevice,
+ gfx::SurfaceFormat aPreferredFormat)
+ : TextureClientRecycleAllocator(aAllocator),
+ mDevice(aDevice),
+ mCanUseNV12(StaticPrefs::media_wmf_use_nv12_format() &&
+ gfx::DeviceManagerDx::Get()->CanUseNV12()),
+ mCanUseP010(StaticPrefs::media_wmf_use_nv12_format() &&
+ gfx::DeviceManagerDx::Get()->CanUseP010()),
+ mCanUseP016(StaticPrefs::media_wmf_use_nv12_format() &&
+ gfx::DeviceManagerDx::Get()->CanUseP016()) {
+ SetPreferredSurfaceFormat(aPreferredFormat);
+}
+
+void D3D11RecycleAllocator::SetPreferredSurfaceFormat(
+ gfx::SurfaceFormat aPreferredFormat) {
+ if ((aPreferredFormat == gfx::SurfaceFormat::NV12 && mCanUseNV12) ||
+ (aPreferredFormat == gfx::SurfaceFormat::P010 && mCanUseP010) ||
+ (aPreferredFormat == gfx::SurfaceFormat::P016 && mCanUseP016)) {
+ mUsableSurfaceFormat = aPreferredFormat;
+ return;
+ }
+ // We can't handle the native source format, set it to BGRA which will
+ // force the caller to convert it later.
+ mUsableSurfaceFormat = gfx::SurfaceFormat::B8G8R8A8;
+}
+
+already_AddRefed<TextureClient> D3D11RecycleAllocator::CreateOrRecycleClient(
+ gfx::ColorSpace2 aColorSpace, gfx::ColorRange aColorRange,
+ const gfx::IntSize& aSize) {
+ // When CompositorDevice or ContentDevice is updated,
+ // we could not reuse old D3D11Textures. It could cause video flickering.
+ RefPtr<ID3D11Device> device = gfx::DeviceManagerDx::Get()->GetImageDevice();
+ if (!!mImageDevice && mImageDevice != device) {
+ ShrinkToMinimumSize();
+ }
+ mImageDevice = device;
+
+ TextureAllocationFlags allocFlags = TextureAllocationFlags::ALLOC_DEFAULT;
+ if (StaticPrefs::media_wmf_use_sync_texture_AtStartup() ||
+ mDevice == DeviceManagerDx::Get()->GetCompositorDevice()) {
+ // If our device is the compositor device, we don't need any synchronization
+ // in practice.
+ allocFlags = TextureAllocationFlags::ALLOC_MANUAL_SYNCHRONIZATION;
+ }
+
+ D3D11TextureClientAllocationHelper helper(
+ mUsableSurfaceFormat, aColorSpace, aColorRange, aSize, allocFlags,
+ mDevice, layers::TextureFlags::DEFAULT);
+
+ RefPtr<TextureClient> textureClient = CreateOrRecycle(helper);
+ return textureClient.forget();
+}
+
+RefPtr<ID3D11Texture2D> D3D11RecycleAllocator::GetStagingTextureNV12(
+ gfx::IntSize aSize) {
+ if (!mStagingTexture || mStagingTextureSize != aSize) {
+ mStagingTexture = nullptr;
+
+ D3D11_TEXTURE2D_DESC desc = {};
+ desc.Width = aSize.width;
+ desc.Height = aSize.height;
+ desc.Format = DXGI_FORMAT_NV12;
+ desc.MipLevels = 1;
+ desc.ArraySize = 1;
+ desc.Usage = D3D11_USAGE_STAGING;
+ desc.BindFlags = 0;
+ desc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
+ desc.MiscFlags = D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX;
+ desc.SampleDesc.Count = 1;
+
+ HRESULT hr = mDevice->CreateTexture2D(&desc, nullptr,
+ getter_AddRefs(mStagingTexture));
+ if (FAILED(hr)) {
+ gfxCriticalNoteOnce << "allocating D3D11 NV12 staging texture failed: "
+ << gfx::hexa(hr);
+ return nullptr;
+ }
+ MOZ_ASSERT(mStagingTexture);
+ mStagingTextureSize = aSize;
+ }
+
+ return mStagingTexture;
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/D3D11ShareHandleImage.h b/gfx/layers/D3D11ShareHandleImage.h
new file mode 100644
index 0000000000..5c7a35420e
--- /dev/null
+++ b/gfx/layers/D3D11ShareHandleImage.h
@@ -0,0 +1,116 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GFX_D311_SHARE_HANDLE_IMAGE_H
+#define GFX_D311_SHARE_HANDLE_IMAGE_H
+
+#include "ImageContainer.h"
+#include "d3d11.h"
+#include "mozilla/Atomics.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/gfx/Types.h"
+#include "mozilla/layers/TextureClient.h"
+#include "mozilla/layers/TextureClientRecycleAllocator.h"
+#include "mozilla/layers/TextureD3D11.h"
+
+namespace mozilla {
+namespace gl {
+class GLBlitHelper;
+}
+namespace layers {
+
+class D3D11RecycleAllocator final : public TextureClientRecycleAllocator {
+ public:
+ D3D11RecycleAllocator(KnowsCompositor* aAllocator, ID3D11Device* aDevice,
+ gfx::SurfaceFormat aPreferredFormat);
+
+ already_AddRefed<TextureClient> CreateOrRecycleClient(
+ gfx::ColorSpace2 aColorSpace, gfx::ColorRange aColorRange,
+ const gfx::IntSize& aSize);
+
+ void SetPreferredSurfaceFormat(gfx::SurfaceFormat aPreferredFormat);
+ gfx::SurfaceFormat GetUsableSurfaceFormat() const {
+ return mUsableSurfaceFormat;
+ }
+
+ RefPtr<ID3D11Texture2D> GetStagingTextureNV12(gfx::IntSize aSize);
+
+ void SetSyncObject(RefPtr<SyncObjectClient>& aSyncObject) {
+ mSyncObject = aSyncObject;
+ }
+
+ RefPtr<SyncObjectClient> GetSyncObject() { return mSyncObject; }
+
+ const RefPtr<ID3D11Device> mDevice;
+ const bool mCanUseNV12;
+ const bool mCanUseP010;
+ const bool mCanUseP016;
+
+ private:
+ /**
+ * Used for checking if CompositorDevice/ContentDevice is updated.
+ */
+ RefPtr<ID3D11Device> mImageDevice;
+ gfx::SurfaceFormat mUsableSurfaceFormat;
+
+ RefPtr<ID3D11Texture2D> mStagingTexture;
+ gfx::IntSize mStagingTextureSize;
+
+ RefPtr<SyncObjectClient> mSyncObject;
+};
+
+// Image class that wraps a ID3D11Texture2D. This class copies the image
+// passed into SetData(), so that it can be accessed from other D3D devices.
+// This class also manages the synchronization of the copy, to ensure the
+// resource is ready to use.
+class D3D11ShareHandleImage final : public Image {
+ public:
+ static RefPtr<D3D11ShareHandleImage> MaybeCreateNV12ImageAndSetData(
+ KnowsCompositor* aAllocator, ImageContainer* aContainer,
+ const PlanarYCbCrData& aData);
+
+ D3D11ShareHandleImage(const gfx::IntSize& aSize, const gfx::IntRect& aRect,
+ gfx::ColorSpace2 aColorSpace,
+ gfx::ColorRange aColorRange);
+ virtual ~D3D11ShareHandleImage() = default;
+
+ bool AllocateTexture(D3D11RecycleAllocator* aAllocator,
+ ID3D11Device* aDevice);
+
+ gfx::IntSize GetSize() const override;
+ already_AddRefed<gfx::SourceSurface> GetAsSourceSurface() override;
+ TextureClient* GetTextureClient(KnowsCompositor* aKnowsCompositor) override;
+ gfx::IntRect GetPictureRect() const override { return mPictureRect; }
+
+ ID3D11Texture2D* GetTexture() const;
+
+ gfx::ColorRange GetColorRange() const { return mColorRange; }
+
+ private:
+ friend class gl::GLBlitHelper;
+ D3D11TextureData* GetData() const {
+ if (!mTextureClient) {
+ return nullptr;
+ }
+ return mTextureClient->GetInternalData()->AsD3D11TextureData();
+ }
+
+ gfx::IntSize mSize;
+ gfx::IntRect mPictureRect;
+
+ public:
+ const gfx::ColorSpace2 mColorSpace;
+
+ private:
+ gfx::ColorRange mColorRange;
+ RefPtr<TextureClient> mTextureClient;
+ RefPtr<ID3D11Texture2D> mTexture;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // GFX_D3DSURFACEIMAGE_H
diff --git a/gfx/layers/D3D11TextureIMFSampleImage.cpp b/gfx/layers/D3D11TextureIMFSampleImage.cpp
new file mode 100644
index 0000000000..738c008a04
--- /dev/null
+++ b/gfx/layers/D3D11TextureIMFSampleImage.cpp
@@ -0,0 +1,108 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include <d3d11.h>
+#include <memory>
+#include <mfobjects.h>
+
+#include "D3D11TextureIMFSampleImage.h"
+#include "WMF.h"
+#include "mozilla/gfx/SourceSurfaceRawData.h"
+#include "mozilla/layers/KnowsCompositor.h"
+#include "mozilla/layers/TextureForwarder.h"
+
+namespace mozilla {
+namespace layers {
+
+using namespace gfx;
+
+/* static */
+RefPtr<IMFSampleWrapper> IMFSampleWrapper::Create(IMFSample* aVideoSample) {
+ RefPtr<IMFSampleWrapper> wrapper = new IMFSampleWrapper(aVideoSample);
+ return wrapper;
+}
+
+IMFSampleWrapper::IMFSampleWrapper(IMFSample* aVideoSample)
+ : mVideoSample(aVideoSample) {}
+
+IMFSampleWrapper::~IMFSampleWrapper() {}
+
+void IMFSampleWrapper::ClearVideoSample() { mVideoSample = nullptr; }
+
+D3D11TextureIMFSampleImage::D3D11TextureIMFSampleImage(
+ IMFSample* aVideoSample, ID3D11Texture2D* aTexture, uint32_t aArrayIndex,
+ const gfx::IntSize& aSize, const gfx::IntRect& aRect,
+ gfx::ColorSpace2 aColorSpace, gfx::ColorRange aColorRange)
+ : Image(nullptr, ImageFormat::D3D11_TEXTURE_IMF_SAMPLE),
+ mVideoSample(IMFSampleWrapper::Create(aVideoSample)),
+ mTexture(aTexture),
+ mArrayIndex(aArrayIndex),
+ mSize(aSize),
+ mPictureRect(aRect),
+ mColorSpace(aColorSpace),
+ mColorRange(aColorRange) {
+ MOZ_ASSERT(XRE_IsGPUProcess());
+}
+
+void D3D11TextureIMFSampleImage::AllocateTextureClient(
+ KnowsCompositor* aKnowsCompositor, RefPtr<IMFSampleUsageInfo> aUsageInfo) {
+ mTextureClient = D3D11TextureData::CreateTextureClient(
+ mTexture, mArrayIndex, mSize, gfx::SurfaceFormat::NV12, mColorSpace,
+ mColorRange, aKnowsCompositor, aUsageInfo);
+ MOZ_ASSERT(mTextureClient);
+}
+
+gfx::IntSize D3D11TextureIMFSampleImage::GetSize() const { return mSize; }
+
+TextureClient* D3D11TextureIMFSampleImage::GetTextureClient(
+ KnowsCompositor* aKnowsCompositor) {
+ return mTextureClient;
+}
+
+already_AddRefed<gfx::SourceSurface>
+D3D11TextureIMFSampleImage::GetAsSourceSurface() {
+ RefPtr<ID3D11Texture2D> src = GetTexture();
+ if (!src) {
+ gfxWarning() << "Cannot readback from shared texture because no texture is "
+ "available.";
+ return nullptr;
+ }
+
+ RefPtr<gfx::SourceSurface> sourceSurface =
+ gfx::Factory::CreateBGRA8DataSourceSurfaceForD3D11Texture(src,
+ mArrayIndex);
+
+ // There is a case that mSize and size of mTexture are different. In this
+ // case, size of sourceSurface is different from mSize.
+ if (sourceSurface && sourceSurface->GetSize() != mSize) {
+ MOZ_RELEASE_ASSERT(sourceSurface->GetType() == SurfaceType::DATA_ALIGNED);
+ RefPtr<gfx::SourceSurfaceAlignedRawData> rawData =
+ static_cast<gfx::SourceSurfaceAlignedRawData*>(sourceSurface.get());
+ auto data = rawData->GetData();
+ auto stride = rawData->Stride();
+ auto size = rawData->GetSize();
+ auto format = rawData->GetFormat();
+ sourceSurface = gfx::Factory::CreateWrappingDataSourceSurface(
+ data, stride, Min(size, mSize), format,
+ [](void* aClosure) {
+ RefPtr<SourceSurfaceAlignedRawData> surface =
+ dont_AddRef(static_cast<SourceSurfaceAlignedRawData*>(aClosure));
+ },
+ rawData.forget().take());
+ }
+ return sourceSurface.forget();
+}
+
+ID3D11Texture2D* D3D11TextureIMFSampleImage::GetTexture() const {
+ return mTexture;
+}
+
+RefPtr<IMFSampleWrapper> D3D11TextureIMFSampleImage::GetIMFSampleWrapper() {
+ return mVideoSample;
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/D3D11TextureIMFSampleImage.h b/gfx/layers/D3D11TextureIMFSampleImage.h
new file mode 100644
index 0000000000..4077d386aa
--- /dev/null
+++ b/gfx/layers/D3D11TextureIMFSampleImage.h
@@ -0,0 +1,107 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GFX_D311_TEXTURE_IMF_SAMPLE_IMAGE_H
+#define GFX_D311_TEXTURE_IMF_SAMPLE_IMAGE_H
+
+#include "ImageContainer.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/gfx/Types.h"
+#include "mozilla/layers/TextureClient.h"
+#include "mozilla/layers/TextureD3D11.h"
+#include "mozilla/ThreadSafeWeakPtr.h"
+
+struct ID3D11Texture2D;
+struct IMFSample;
+
+namespace mozilla {
+namespace gl {
+class GLBlitHelper;
+}
+namespace layers {
+
+class IMFSampleWrapper : public SupportsThreadSafeWeakPtr<IMFSampleWrapper> {
+ public:
+ MOZ_DECLARE_REFCOUNTED_TYPENAME(IMFSampleWrapper)
+
+ static RefPtr<IMFSampleWrapper> Create(IMFSample* aVideoSample);
+ virtual ~IMFSampleWrapper();
+ void ClearVideoSample();
+
+ protected:
+ explicit IMFSampleWrapper(IMFSample* aVideoSample);
+
+ RefPtr<IMFSample> mVideoSample;
+};
+
+class IMFSampleUsageInfo final {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(IMFSampleUsageInfo)
+
+ IMFSampleUsageInfo() = default;
+
+ bool SupportsZeroCopyNV12Texture() { return mSupportsZeroCopyNV12Texture; }
+ void DisableZeroCopyNV12Texture() { mSupportsZeroCopyNV12Texture = false; }
+
+ protected:
+ ~IMFSampleUsageInfo() = default;
+
+ Atomic<bool> mSupportsZeroCopyNV12Texture{true};
+};
+
+// Image class that wraps ID3D11Texture2D of IMFSample
+// Expected to be used in GPU process.
+class D3D11TextureIMFSampleImage final : public Image {
+ public:
+ D3D11TextureIMFSampleImage(IMFSample* aVideoSample, ID3D11Texture2D* aTexture,
+ uint32_t aArrayIndex, const gfx::IntSize& aSize,
+ const gfx::IntRect& aRect,
+ gfx::ColorSpace2 aColorSpace,
+ gfx::ColorRange aColorRange);
+ virtual ~D3D11TextureIMFSampleImage() = default;
+
+ void AllocateTextureClient(KnowsCompositor* aKnowsCompositor,
+ RefPtr<IMFSampleUsageInfo> aUsageInfo);
+
+ gfx::IntSize GetSize() const override;
+ already_AddRefed<gfx::SourceSurface> GetAsSourceSurface() override;
+ TextureClient* GetTextureClient(KnowsCompositor* aKnowsCompositor) override;
+ gfx::IntRect GetPictureRect() const override { return mPictureRect; }
+
+ ID3D11Texture2D* GetTexture() const;
+ RefPtr<IMFSampleWrapper> GetIMFSampleWrapper();
+
+ gfx::ColorRange GetColorRange() const { return mColorRange; }
+
+ private:
+ friend class gl::GLBlitHelper;
+ D3D11TextureData* GetData() const {
+ if (!mTextureClient) {
+ return nullptr;
+ }
+ return mTextureClient->GetInternalData()->AsD3D11TextureData();
+ }
+
+ // When ref of IMFSample is held, its ID3D11Texture2D is not reused by
+ // IMFTransform.
+ RefPtr<IMFSampleWrapper> mVideoSample;
+ RefPtr<ID3D11Texture2D> mTexture;
+
+ public:
+ const uint32_t mArrayIndex;
+ const gfx::IntSize mSize;
+ const gfx::IntRect mPictureRect;
+ const gfx::ColorSpace2 mColorSpace;
+ const gfx::ColorRange mColorRange;
+
+ private:
+ RefPtr<TextureClient> mTextureClient;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // GFX_D311_TEXTURE_IMF_SAMPLE_IMAGE_H
diff --git a/gfx/layers/D3D11YCbCrImage.cpp b/gfx/layers/D3D11YCbCrImage.cpp
new file mode 100644
index 0000000000..02b2f4e23f
--- /dev/null
+++ b/gfx/layers/D3D11YCbCrImage.cpp
@@ -0,0 +1,432 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "D3D11YCbCrImage.h"
+
+#include "YCbCrUtils.h"
+#include "gfx2DGlue.h"
+#include "mozilla/gfx/DeviceManagerDx.h"
+#include "mozilla/gfx/gfxVars.h"
+#include "mozilla/layers/CompositableClient.h"
+#include "mozilla/layers/CompositableForwarder.h"
+#include "mozilla/layers/TextureD3D11.h"
+
+using namespace mozilla::gfx;
+
+namespace mozilla {
+namespace layers {
+
+D3D11YCbCrImage::D3D11YCbCrImage()
+ : Image(NULL, ImageFormat::D3D11_YCBCR_IMAGE) {}
+
+D3D11YCbCrImage::~D3D11YCbCrImage() {}
+
+bool D3D11YCbCrImage::SetData(KnowsCompositor* aAllocator,
+ ImageContainer* aContainer,
+ const PlanarYCbCrData& aData) {
+ mPictureRect = aData.mPictureRect;
+ mColorDepth = aData.mColorDepth;
+ mColorSpace = aData.mYUVColorSpace;
+ mColorRange = aData.mColorRange;
+ mChromaSubsampling = aData.mChromaSubsampling;
+
+ RefPtr<D3D11YCbCrRecycleAllocator> allocator =
+ aContainer->GetD3D11YCbCrRecycleAllocator(aAllocator);
+ if (!allocator) {
+ return false;
+ }
+
+ RefPtr<ID3D11Device> device = gfx::DeviceManagerDx::Get()->GetImageDevice();
+ if (!device) {
+ return false;
+ }
+
+ {
+ DXGIYCbCrTextureAllocationHelper helper(aData, TextureFlags::DEFAULT,
+ device);
+ mTextureClient = allocator->CreateOrRecycle(helper);
+ }
+
+ if (!mTextureClient) {
+ return false;
+ }
+
+ DXGIYCbCrTextureData* data =
+ mTextureClient->GetInternalData()->AsDXGIYCbCrTextureData();
+
+ ID3D11Texture2D* textureY = data->GetD3D11Texture(0);
+ ID3D11Texture2D* textureCb = data->GetD3D11Texture(1);
+ ID3D11Texture2D* textureCr = data->GetD3D11Texture(2);
+
+ RefPtr<ID3D10Multithread> mt;
+ HRESULT hr = device->QueryInterface((ID3D10Multithread**)getter_AddRefs(mt));
+
+ if (FAILED(hr) || !mt) {
+ gfxCriticalError() << "Multithread safety interface not supported. " << hr;
+ return false;
+ }
+
+ if (!mt->GetMultithreadProtected()) {
+ gfxCriticalError() << "Device used not marked as multithread-safe.";
+ return false;
+ }
+
+ D3D11MTAutoEnter mtAutoEnter(mt.forget());
+
+ RefPtr<ID3D11DeviceContext> ctx;
+ device->GetImmediateContext(getter_AddRefs(ctx));
+ if (!ctx) {
+ gfxCriticalError() << "Failed to get immediate context.";
+ return false;
+ }
+
+ AutoLockD3D11Texture lockY(textureY);
+ AutoLockD3D11Texture lockCb(textureCb);
+ AutoLockD3D11Texture lockCr(textureCr);
+
+ ctx->UpdateSubresource(textureY, 0, nullptr, aData.mYChannel, aData.mYStride,
+ aData.mYStride * aData.YDataSize().height);
+ ctx->UpdateSubresource(textureCb, 0, nullptr, aData.mCbChannel,
+ aData.mCbCrStride,
+ aData.mCbCrStride * aData.CbCrDataSize().height);
+ ctx->UpdateSubresource(textureCr, 0, nullptr, aData.mCrChannel,
+ aData.mCbCrStride,
+ aData.mCbCrStride * aData.CbCrDataSize().height);
+
+ return true;
+}
+
+IntSize D3D11YCbCrImage::GetSize() const { return mPictureRect.Size(); }
+
+TextureClient* D3D11YCbCrImage::GetTextureClient(
+ KnowsCompositor* aKnowsCompositor) {
+ return mTextureClient;
+}
+
+const DXGIYCbCrTextureData* D3D11YCbCrImage::GetData() const {
+ if (!mTextureClient) return nullptr;
+
+ return mTextureClient->GetInternalData()->AsDXGIYCbCrTextureData();
+}
+
+already_AddRefed<SourceSurface> D3D11YCbCrImage::GetAsSourceSurface() {
+ if (!mTextureClient) {
+ gfxWarning()
+ << "GetAsSourceSurface() called on uninitialized D3D11YCbCrImage.";
+ return nullptr;
+ }
+
+ gfx::IntSize size(mPictureRect.Size());
+ gfx::SurfaceFormat format =
+ gfx::ImageFormatToSurfaceFormat(gfxVars::OffscreenFormat());
+ HRESULT hr;
+
+ PlanarYCbCrData data;
+
+ DXGIYCbCrTextureData* dxgiData =
+ mTextureClient->GetInternalData()->AsDXGIYCbCrTextureData();
+
+ if (!dxgiData) {
+ gfxCriticalError() << "Failed to get texture client internal data.";
+ return nullptr;
+ }
+
+ RefPtr<ID3D11Texture2D> texY = dxgiData->GetD3D11Texture(0);
+ RefPtr<ID3D11Texture2D> texCb = dxgiData->GetD3D11Texture(1);
+ RefPtr<ID3D11Texture2D> texCr = dxgiData->GetD3D11Texture(2);
+ RefPtr<ID3D11Texture2D> softTexY, softTexCb, softTexCr;
+ D3D11_TEXTURE2D_DESC desc;
+
+ RefPtr<ID3D11Device> dev;
+ texY->GetDevice(getter_AddRefs(dev));
+
+ if (!dev || dev != gfx::DeviceManagerDx::Get()->GetImageDevice()) {
+ gfxCriticalError() << "D3D11Device is obsoleted";
+ return nullptr;
+ }
+
+ RefPtr<ID3D10Multithread> mt;
+ hr = dev->QueryInterface((ID3D10Multithread**)getter_AddRefs(mt));
+
+ if (FAILED(hr) || !mt) {
+ gfxCriticalError() << "Multithread safety interface not supported.";
+ return nullptr;
+ }
+
+ if (!mt->GetMultithreadProtected()) {
+ gfxCriticalError() << "Device used not marked as multithread-safe.";
+ return nullptr;
+ }
+
+ D3D11MTAutoEnter mtAutoEnter(mt.forget());
+
+ texY->GetDesc(&desc);
+ desc.BindFlags = 0;
+ desc.MiscFlags = 0;
+ desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
+ desc.Usage = D3D11_USAGE_STAGING;
+
+ dev->CreateTexture2D(&desc, nullptr, getter_AddRefs(softTexY));
+ if (!softTexY) {
+ gfxCriticalNote << "Failed to allocate softTexY";
+ return nullptr;
+ }
+
+ texCb->GetDesc(&desc);
+ desc.BindFlags = 0;
+ desc.MiscFlags = 0;
+ desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
+ desc.Usage = D3D11_USAGE_STAGING;
+
+ dev->CreateTexture2D(&desc, nullptr, getter_AddRefs(softTexCb));
+ if (!softTexCb) {
+ gfxCriticalNote << "Failed to allocate softTexCb";
+ return nullptr;
+ }
+
+ texCr->GetDesc(&desc);
+ desc.BindFlags = 0;
+ desc.MiscFlags = 0;
+ desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
+ desc.Usage = D3D11_USAGE_STAGING;
+
+ dev->CreateTexture2D(&desc, nullptr, getter_AddRefs(softTexCr));
+ if (!softTexCr) {
+ gfxCriticalNote << "Failed to allocate softTexCr";
+ return nullptr;
+ }
+
+ RefPtr<ID3D11DeviceContext> ctx;
+ dev->GetImmediateContext(getter_AddRefs(ctx));
+ if (!ctx) {
+ gfxCriticalError() << "Failed to get immediate context.";
+ return nullptr;
+ }
+
+ {
+ AutoLockD3D11Texture lockY(texY);
+ AutoLockD3D11Texture lockCb(texCb);
+ AutoLockD3D11Texture lockCr(texCr);
+ ctx->CopyResource(softTexY, texY);
+ ctx->CopyResource(softTexCb, texCb);
+ ctx->CopyResource(softTexCr, texCr);
+ }
+
+ D3D11_MAPPED_SUBRESOURCE mapY, mapCb, mapCr;
+ RefPtr<gfx::DataSourceSurface> surface;
+ mapY.pData = mapCb.pData = mapCr.pData = nullptr;
+
+ hr = ctx->Map(softTexY, 0, D3D11_MAP_READ, 0, &mapY);
+ if (FAILED(hr)) {
+ gfxCriticalError() << "Failed to map Y plane (" << hr << ")";
+ return nullptr;
+ }
+ hr = ctx->Map(softTexCb, 0, D3D11_MAP_READ, 0, &mapCb);
+ if (FAILED(hr)) {
+ gfxCriticalError() << "Failed to map Y plane (" << hr << ")";
+ return nullptr;
+ }
+ hr = ctx->Map(softTexCr, 0, D3D11_MAP_READ, 0, &mapCr);
+ if (FAILED(hr)) {
+ gfxCriticalError() << "Failed to map Y plane (" << hr << ")";
+ return nullptr;
+ }
+
+ MOZ_ASSERT(mapCb.RowPitch == mapCr.RowPitch);
+
+ data.mPictureRect = mPictureRect;
+ data.mStereoMode = StereoMode::MONO;
+ data.mColorDepth = mColorDepth;
+ data.mYUVColorSpace = mColorSpace;
+ data.mColorRange = mColorRange;
+ data.mChromaSubsampling = mChromaSubsampling;
+ data.mYSkip = data.mCbSkip = data.mCrSkip = 0;
+ data.mYChannel = static_cast<uint8_t*>(mapY.pData);
+ data.mYStride = mapY.RowPitch;
+ data.mCbChannel = static_cast<uint8_t*>(mapCb.pData);
+ data.mCrChannel = static_cast<uint8_t*>(mapCr.pData);
+ data.mCbCrStride = mapCb.RowPitch;
+
+ gfx::GetYCbCrToRGBDestFormatAndSize(data, format, size);
+ if (size.width > PlanarYCbCrImage::MAX_DIMENSION ||
+ size.height > PlanarYCbCrImage::MAX_DIMENSION) {
+ gfxCriticalError() << "Illegal image dest width or height";
+ return nullptr;
+ }
+
+ surface = gfx::Factory::CreateDataSourceSurface(size, format);
+ if (!surface) {
+ gfxCriticalError() << "Failed to create DataSourceSurface for image: "
+ << size << " " << format;
+ return nullptr;
+ }
+
+ DataSourceSurface::ScopedMap mapping(surface, DataSourceSurface::WRITE);
+ if (!mapping.IsMapped()) {
+ gfxCriticalError() << "Failed to map DataSourceSurface for D3D11YCbCrImage";
+ return nullptr;
+ }
+
+ gfx::ConvertYCbCrToRGB(data, format, size, mapping.GetData(),
+ mapping.GetStride());
+
+ ctx->Unmap(softTexY, 0);
+ ctx->Unmap(softTexCb, 0);
+ ctx->Unmap(softTexCr, 0);
+
+ return surface.forget();
+}
+
+class AutoCheckLockD3D11Texture final {
+ public:
+ explicit AutoCheckLockD3D11Texture(ID3D11Texture2D* aTexture)
+ : mIsLocked(false) {
+ aTexture->QueryInterface((IDXGIKeyedMutex**)getter_AddRefs(mMutex));
+ if (!mMutex) {
+ // If D3D11Texture does not have keyed mutex, we think that the
+ // D3D11Texture could be locked.
+ mIsLocked = true;
+ return;
+ }
+
+ // Test to see if the keyed mutex has been released
+ HRESULT hr = mMutex->AcquireSync(0, 0);
+ if (hr == S_OK || hr == WAIT_ABANDONED) {
+ mIsLocked = true;
+ // According to Microsoft documentation:
+ // WAIT_ABANDONED - The shared surface and keyed mutex are no longer in a
+ // consistent state. If AcquireSync returns this value, you should release
+ // and recreate both the keyed mutex and the shared surface
+ // So even if we do get WAIT_ABANDONED, the keyed mutex will have to be
+ // released.
+ mSyncAcquired = true;
+ }
+ }
+
+ ~AutoCheckLockD3D11Texture() {
+ if (!mSyncAcquired) {
+ return;
+ }
+ HRESULT hr = mMutex->ReleaseSync(0);
+ if (FAILED(hr)) {
+ NS_WARNING("Failed to unlock the texture");
+ }
+ }
+
+ bool IsLocked() const { return mIsLocked; }
+
+ private:
+ bool mIsLocked;
+ bool mSyncAcquired = false;
+ RefPtr<IDXGIKeyedMutex> mMutex;
+};
+
+DXGIYCbCrTextureAllocationHelper::DXGIYCbCrTextureAllocationHelper(
+ const PlanarYCbCrData& aData, TextureFlags aTextureFlags,
+ ID3D11Device* aDevice)
+ : ITextureClientAllocationHelper(
+ gfx::SurfaceFormat::YUV, aData.mPictureRect.Size(),
+ BackendSelector::Content, aTextureFlags, ALLOC_DEFAULT),
+ mData(aData),
+ mDevice(aDevice) {}
+
+bool DXGIYCbCrTextureAllocationHelper::IsCompatible(
+ TextureClient* aTextureClient) {
+ MOZ_ASSERT(aTextureClient->GetFormat() == gfx::SurfaceFormat::YUV);
+
+ DXGIYCbCrTextureData* dxgiData =
+ aTextureClient->GetInternalData()->AsDXGIYCbCrTextureData();
+ if (!dxgiData || aTextureClient->GetSize() != mData.mPictureRect.Size() ||
+ dxgiData->GetYSize() != mData.YDataSize() ||
+ dxgiData->GetCbCrSize() != mData.CbCrDataSize() ||
+ dxgiData->GetColorDepth() != mData.mColorDepth ||
+ dxgiData->GetYUVColorSpace() != mData.mYUVColorSpace) {
+ return false;
+ }
+
+ ID3D11Texture2D* textureY = dxgiData->GetD3D11Texture(0);
+ ID3D11Texture2D* textureCb = dxgiData->GetD3D11Texture(1);
+ ID3D11Texture2D* textureCr = dxgiData->GetD3D11Texture(2);
+
+ RefPtr<ID3D11Device> device;
+ textureY->GetDevice(getter_AddRefs(device));
+ if (!device || device != gfx::DeviceManagerDx::Get()->GetImageDevice()) {
+ return false;
+ }
+
+ // Test to see if the keyed mutex has been released.
+ // If D3D11Texture failed to lock, do not recycle the DXGIYCbCrTextureData.
+
+ AutoCheckLockD3D11Texture lockY(textureY);
+ AutoCheckLockD3D11Texture lockCr(textureCr);
+ AutoCheckLockD3D11Texture lockCb(textureCb);
+
+ if (!lockY.IsLocked() || !lockCr.IsLocked() || !lockCb.IsLocked()) {
+ return false;
+ }
+
+ return true;
+}
+
+already_AddRefed<TextureClient> DXGIYCbCrTextureAllocationHelper::Allocate(
+ KnowsCompositor* aAllocator) {
+ auto ySize = mData.YDataSize();
+ auto cbcrSize = mData.CbCrDataSize();
+ CD3D11_TEXTURE2D_DESC newDesc(mData.mColorDepth == gfx::ColorDepth::COLOR_8
+ ? DXGI_FORMAT_R8_UNORM
+ : DXGI_FORMAT_R16_UNORM,
+ ySize.width, ySize.height, 1, 1);
+ newDesc.MiscFlags = D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX;
+
+ RefPtr<ID3D10Multithread> mt;
+ HRESULT hr = mDevice->QueryInterface((ID3D10Multithread**)getter_AddRefs(mt));
+
+ if (FAILED(hr) || !mt) {
+ gfxCriticalError() << "Multithread safety interface not supported. " << hr;
+ return nullptr;
+ }
+
+ if (!mt->GetMultithreadProtected()) {
+ gfxCriticalError() << "Device used not marked as multithread-safe.";
+ return nullptr;
+ }
+
+ D3D11MTAutoEnter mtAutoEnter(mt.forget());
+
+ RefPtr<ID3D11Texture2D> textureY;
+ hr = mDevice->CreateTexture2D(&newDesc, nullptr, getter_AddRefs(textureY));
+ NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
+
+ newDesc.Width = cbcrSize.width;
+ newDesc.Height = cbcrSize.height;
+
+ RefPtr<ID3D11Texture2D> textureCb;
+ hr = mDevice->CreateTexture2D(&newDesc, nullptr, getter_AddRefs(textureCb));
+ NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
+
+ RefPtr<ID3D11Texture2D> textureCr;
+ hr = mDevice->CreateTexture2D(&newDesc, nullptr, getter_AddRefs(textureCr));
+ NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
+
+ TextureForwarder* forwarder =
+ aAllocator ? aAllocator->GetTextureForwarder() : nullptr;
+
+ return TextureClient::CreateWithData(
+ DXGIYCbCrTextureData::Create(
+ textureY, textureCb, textureCr, mData.mPictureRect.Size(), ySize,
+ cbcrSize, mData.mColorDepth, mData.mYUVColorSpace, mData.mColorRange),
+ mTextureFlags, forwarder);
+}
+
+already_AddRefed<TextureClient> D3D11YCbCrRecycleAllocator::Allocate(
+ SurfaceFormat aFormat, IntSize aSize, BackendSelector aSelector,
+ TextureFlags aTextureFlags, TextureAllocationFlags aAllocFlags) {
+ MOZ_ASSERT_UNREACHABLE("unexpected to be called");
+ return nullptr;
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/D3D11YCbCrImage.h b/gfx/layers/D3D11YCbCrImage.h
new file mode 100644
index 0000000000..2ea17025fc
--- /dev/null
+++ b/gfx/layers/D3D11YCbCrImage.h
@@ -0,0 +1,94 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GFX_D3D11_YCBCR_IMAGE_H
+#define GFX_D3D11_YCBCR_IMAGE_H
+
+#include "d3d11.h"
+#include "mozilla/layers/TextureClientRecycleAllocator.h"
+#include "mozilla/Maybe.h"
+#include "ImageContainer.h"
+
+namespace mozilla {
+namespace gl {
+class GLBlitHelper;
+}
+namespace layers {
+
+class ImageContainer;
+class DXGIYCbCrTextureClient;
+class DXGIYCbCrTextureData;
+
+class MOZ_RAII DXGIYCbCrTextureAllocationHelper
+ : public ITextureClientAllocationHelper {
+ public:
+ DXGIYCbCrTextureAllocationHelper(const PlanarYCbCrData& aData,
+ TextureFlags aTextureFlags,
+ ID3D11Device* aDevice);
+
+ bool IsCompatible(TextureClient* aTextureClient) override;
+
+ already_AddRefed<TextureClient> Allocate(
+ KnowsCompositor* aAllocator) override;
+
+ protected:
+ const PlanarYCbCrData& mData;
+ RefPtr<ID3D11Device> mDevice;
+};
+
+class D3D11YCbCrRecycleAllocator : public TextureClientRecycleAllocator {
+ public:
+ explicit D3D11YCbCrRecycleAllocator(KnowsCompositor* aKnowsCompositor)
+ : TextureClientRecycleAllocator(aKnowsCompositor) {}
+
+ protected:
+ already_AddRefed<TextureClient> Allocate(
+ gfx::SurfaceFormat aFormat, gfx::IntSize aSize, BackendSelector aSelector,
+ TextureFlags aTextureFlags, TextureAllocationFlags aAllocFlags) override;
+};
+
+class D3D11YCbCrImage : public Image {
+ friend class gl::GLBlitHelper;
+
+ public:
+ D3D11YCbCrImage();
+ virtual ~D3D11YCbCrImage();
+
+ // Copies the surface into a sharable texture's surface, and initializes
+ // the image.
+ bool SetData(KnowsCompositor* aAllocator, ImageContainer* aContainer,
+ const PlanarYCbCrData& aData);
+
+ gfx::IntSize GetSize() const override;
+
+ already_AddRefed<gfx::SourceSurface> GetAsSourceSurface() override;
+
+ TextureClient* GetTextureClient(KnowsCompositor* aKnowsCompositor) override;
+
+ gfx::IntRect GetPictureRect() const override { return mPictureRect; }
+
+ gfx::IntSize GetYSize() const {
+ return {mPictureRect.XMost(), mPictureRect.YMost()};
+ }
+ gfx::IntSize GetCbCrSize() const {
+ return ChromaSize(GetYSize(), mChromaSubsampling);
+ }
+
+ private:
+ const DXGIYCbCrTextureData* GetData() const;
+
+ gfx::IntRect mPictureRect;
+ gfx::ColorDepth mColorDepth;
+ gfx::YUVColorSpace mColorSpace;
+ gfx::ColorRange mColorRange;
+ gfx::ChromaSubsampling mChromaSubsampling;
+ RefPtr<TextureClient> mTextureClient;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // GFX_D3D11_YCBCR_IMAGE_H
diff --git a/gfx/layers/D3D9SurfaceImage.cpp b/gfx/layers/D3D9SurfaceImage.cpp
new file mode 100644
index 0000000000..2c8037a03f
--- /dev/null
+++ b/gfx/layers/D3D9SurfaceImage.cpp
@@ -0,0 +1,275 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "D3D9SurfaceImage.h"
+#include "gfx2DGlue.h"
+#include "gfxWindowsPlatform.h"
+#include "mozilla/layers/CompositableClient.h"
+#include "mozilla/layers/CompositableForwarder.h"
+#include "mozilla/layers/ImageBridgeChild.h"
+#include "mozilla/gfx/Types.h"
+#include "mozilla/ProfilerLabels.h"
+
+namespace mozilla {
+namespace layers {
+
+DXGID3D9TextureData::DXGID3D9TextureData(gfx::SurfaceFormat aFormat,
+ IDirect3DTexture9* aTexture,
+ HANDLE aHandle,
+ IDirect3DDevice9* aDevice)
+ : mDevice(aDevice), mTexture(aTexture), mFormat(aFormat), mHandle(aHandle) {
+ MOZ_COUNT_CTOR(DXGID3D9TextureData);
+}
+
+DXGID3D9TextureData::~DXGID3D9TextureData() {
+ gfxWindowsPlatform::sD3D9SharedTextures -= mDesc.Width * mDesc.Height * 4;
+ MOZ_COUNT_DTOR(DXGID3D9TextureData);
+}
+
+// static
+DXGID3D9TextureData* DXGID3D9TextureData::Create(gfx::IntSize aSize,
+ gfx::SurfaceFormat aFormat,
+ TextureFlags aFlags,
+ IDirect3DDevice9* aDevice) {
+ AUTO_PROFILER_LABEL("DXGID3D9TextureData::Create", GRAPHICS);
+ MOZ_ASSERT(aFormat == gfx::SurfaceFormat::B8G8R8A8);
+ if (aFormat != gfx::SurfaceFormat::B8G8R8A8) {
+ return nullptr;
+ }
+
+ RefPtr<IDirect3DTexture9> texture;
+ HANDLE shareHandle = nullptr;
+ HRESULT hr = aDevice->CreateTexture(
+ aSize.width, aSize.height, 1, D3DUSAGE_RENDERTARGET, D3DFMT_A8R8G8B8,
+ D3DPOOL_DEFAULT, getter_AddRefs(texture), &shareHandle);
+ if (FAILED(hr) || !shareHandle) {
+ return nullptr;
+ }
+
+ D3DSURFACE_DESC surfaceDesc;
+ hr = texture->GetLevelDesc(0, &surfaceDesc);
+ if (FAILED(hr)) {
+ return nullptr;
+ }
+ DXGID3D9TextureData* data =
+ new DXGID3D9TextureData(aFormat, texture, shareHandle, aDevice);
+ data->mDesc = surfaceDesc;
+
+ gfxWindowsPlatform::sD3D9SharedTextures += aSize.width * aSize.height * 4;
+ return data;
+}
+
+void DXGID3D9TextureData::FillInfo(TextureData::Info& aInfo) const {
+ aInfo.size = GetSize();
+ aInfo.format = mFormat;
+ aInfo.supportsMoz2D = false;
+ aInfo.canExposeMappedData = false;
+ aInfo.hasSynchronization = false;
+}
+
+already_AddRefed<IDirect3DSurface9> DXGID3D9TextureData::GetD3D9Surface()
+ const {
+ RefPtr<IDirect3DSurface9> textureSurface;
+ HRESULT hr = mTexture->GetSurfaceLevel(0, getter_AddRefs(textureSurface));
+ NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
+
+ return textureSurface.forget();
+}
+
+bool DXGID3D9TextureData::Serialize(SurfaceDescriptor& aOutDescriptor) {
+ SurfaceDescriptorD3D10 desc((WindowsHandle)(mHandle),
+ /* gpuProcessTextureId */ Nothing(),
+ /* arrayIndex */ 0, mFormat, GetSize(),
+ gfx::ColorSpace2::SRGB, gfx::ColorRange::FULL);
+ // In reality, with D3D9 we will only ever deal with RGBA textures.
+ bool isYUV = mFormat == gfx::SurfaceFormat::NV12 ||
+ mFormat == gfx::SurfaceFormat::P010 ||
+ mFormat == gfx::SurfaceFormat::P016;
+ if (isYUV) {
+ gfxCriticalError() << "Unexpected YUV format for DXGID3D9TextureData: "
+ << mFormat;
+ desc.colorSpace() = gfx::ColorSpace2::BT601_525;
+ desc.colorRange() = gfx::ColorRange::LIMITED;
+ }
+ aOutDescriptor = desc;
+ return true;
+}
+
+D3D9SurfaceImage::D3D9SurfaceImage()
+ : Image(nullptr, ImageFormat::D3D9_RGB32_TEXTURE),
+ mSize(0, 0),
+ mShareHandle(0),
+ mValid(true) {}
+
+D3D9SurfaceImage::~D3D9SurfaceImage() {}
+
+HRESULT
+D3D9SurfaceImage::AllocateAndCopy(D3D9RecycleAllocator* aAllocator,
+ IDirect3DSurface9* aSurface,
+ const gfx::IntRect& aRegion) {
+ NS_ENSURE_TRUE(aSurface, E_POINTER);
+ HRESULT hr;
+ RefPtr<IDirect3DSurface9> surface = aSurface;
+
+ RefPtr<IDirect3DDevice9> device;
+ hr = surface->GetDevice(getter_AddRefs(device));
+ NS_ENSURE_TRUE(SUCCEEDED(hr), E_FAIL);
+
+ RefPtr<IDirect3D9> d3d9;
+ hr = device->GetDirect3D(getter_AddRefs(d3d9));
+ NS_ENSURE_TRUE(SUCCEEDED(hr), E_FAIL);
+
+ D3DSURFACE_DESC desc;
+ surface->GetDesc(&desc);
+ // Ensure we can convert the textures format to RGB conversion
+ // in StretchRect. Fail if we can't.
+ hr = d3d9->CheckDeviceFormatConversion(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL,
+ desc.Format, D3DFMT_A8R8G8B8);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ // DXVA surfaces aren't created sharable, so we need to copy the surface
+ // to a sharable texture to that it's accessible to the layer manager's
+ // device.
+ if (aAllocator) {
+ mTextureClient = aAllocator->CreateOrRecycleClient(
+ gfx::SurfaceFormat::B8G8R8A8, aRegion.Size());
+ if (!mTextureClient) {
+ return E_FAIL;
+ }
+
+ DXGID3D9TextureData* texData =
+ static_cast<DXGID3D9TextureData*>(mTextureClient->GetInternalData());
+ mTexture = texData->GetD3D9Texture();
+ mShareHandle = texData->GetShareHandle();
+ mDesc = texData->GetDesc();
+ } else {
+ hr = device->CreateTexture(aRegion.Size().width, aRegion.Size().height, 1,
+ D3DUSAGE_RENDERTARGET, D3DFMT_A8R8G8B8,
+ D3DPOOL_DEFAULT, getter_AddRefs(mTexture),
+ &mShareHandle);
+ if (FAILED(hr) || !mShareHandle) {
+ return E_FAIL;
+ }
+
+ hr = mTexture->GetLevelDesc(0, &mDesc);
+ if (FAILED(hr)) {
+ return E_FAIL;
+ }
+ }
+
+ // Copy the image onto the texture, preforming YUV -> RGB conversion if
+ // necessary.
+ RefPtr<IDirect3DSurface9> textureSurface = GetD3D9Surface();
+ if (!textureSurface) {
+ return E_FAIL;
+ }
+
+ RECT src = {aRegion.X(), aRegion.Y(), aRegion.XMost(), aRegion.YMost()};
+ hr =
+ device->StretchRect(surface, &src, textureSurface, nullptr, D3DTEXF_NONE);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ mSize = aRegion.Size();
+ return S_OK;
+}
+
+already_AddRefed<IDirect3DSurface9> D3D9SurfaceImage::GetD3D9Surface() const {
+ RefPtr<IDirect3DSurface9> textureSurface;
+ HRESULT hr = mTexture->GetSurfaceLevel(0, getter_AddRefs(textureSurface));
+ NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
+ return textureSurface.forget();
+}
+
+HANDLE
+D3D9SurfaceImage::GetShareHandle() const { return mShareHandle; }
+
+gfx::IntSize D3D9SurfaceImage::GetSize() const { return mSize; }
+
+TextureClient* D3D9SurfaceImage::GetTextureClient(
+ KnowsCompositor* aKnowsCompositor) {
+ MOZ_ASSERT(mTextureClient);
+ MOZ_ASSERT(mTextureClient->GetAllocator() ==
+ aKnowsCompositor->GetTextureForwarder());
+ return mTextureClient;
+}
+
+already_AddRefed<gfx::SourceSurface> D3D9SurfaceImage::GetAsSourceSurface() {
+ if (!mTexture) {
+ return nullptr;
+ }
+
+ HRESULT hr;
+ RefPtr<gfx::DataSourceSurface> surface =
+ gfx::Factory::CreateDataSourceSurface(mSize,
+ gfx::SurfaceFormat::B8G8R8X8);
+ if (NS_WARN_IF(!surface)) {
+ return nullptr;
+ }
+
+ // Readback the texture from GPU memory into system memory, so that
+ // we can copy it into the Cairo image. This is expensive.
+ RefPtr<IDirect3DSurface9> textureSurface = GetD3D9Surface();
+ if (!textureSurface) {
+ return nullptr;
+ }
+
+ RefPtr<IDirect3DDevice9> device;
+ hr = textureSurface->GetDevice(getter_AddRefs(device));
+ NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
+
+ RefPtr<IDirect3DSurface9> systemMemorySurface;
+ hr = device->CreateOffscreenPlainSurface(
+ mSize.width, mSize.height, D3DFMT_A8R8G8B8, D3DPOOL_SYSTEMMEM,
+ getter_AddRefs(systemMemorySurface), 0);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
+
+ hr = device->GetRenderTargetData(textureSurface, systemMemorySurface);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
+
+ D3DLOCKED_RECT rect;
+ hr = systemMemorySurface->LockRect(&rect, nullptr, 0);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
+
+ gfx::DataSourceSurface::MappedSurface mappedSurface;
+ if (!surface->Map(gfx::DataSourceSurface::WRITE, &mappedSurface)) {
+ systemMemorySurface->UnlockRect();
+ return nullptr;
+ }
+
+ const unsigned char* src = (const unsigned char*)(rect.pBits);
+ const unsigned srcPitch = rect.Pitch;
+ for (int y = 0; y < mSize.height; y++) {
+ memcpy(mappedSurface.mData + mappedSurface.mStride * y,
+ (unsigned char*)(src) + srcPitch * y, mSize.width * 4);
+ }
+
+ systemMemorySurface->UnlockRect();
+ surface->Unmap();
+
+ return surface.forget();
+}
+
+already_AddRefed<TextureClient> D3D9RecycleAllocator::Allocate(
+ gfx::SurfaceFormat aFormat, gfx::IntSize aSize, BackendSelector aSelector,
+ TextureFlags aTextureFlags, TextureAllocationFlags aAllocFlags) {
+ TextureData* data =
+ DXGID3D9TextureData::Create(aSize, aFormat, aTextureFlags, mDevice);
+ if (!data) {
+ return nullptr;
+ }
+
+ return MakeAndAddRef<TextureClient>(data, aTextureFlags,
+ mKnowsCompositor->GetTextureForwarder());
+}
+
+already_AddRefed<TextureClient> D3D9RecycleAllocator::CreateOrRecycleClient(
+ gfx::SurfaceFormat aFormat, const gfx::IntSize& aSize) {
+ return CreateOrRecycle(aFormat, aSize, BackendSelector::Content,
+ TextureFlags::DEFAULT);
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/D3D9SurfaceImage.h b/gfx/layers/D3D9SurfaceImage.h
new file mode 100644
index 0000000000..183d5bb585
--- /dev/null
+++ b/gfx/layers/D3D9SurfaceImage.h
@@ -0,0 +1,122 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GFX_D3DSURFACEIMAGE_H
+#define GFX_D3DSURFACEIMAGE_H
+
+#include "mozilla/RefPtr.h"
+#include "ImageContainer.h"
+#include "d3d9.h"
+#include "mozilla/layers/TextureClientRecycleAllocator.h"
+
+namespace mozilla {
+namespace layers {
+
+class TextureClient;
+
+class D3D9RecycleAllocator : public TextureClientRecycleAllocator {
+ public:
+ D3D9RecycleAllocator(KnowsCompositor* aAllocator, IDirect3DDevice9* aDevice)
+ : TextureClientRecycleAllocator(aAllocator), mDevice(aDevice) {}
+
+ already_AddRefed<TextureClient> CreateOrRecycleClient(
+ gfx::SurfaceFormat aFormat, const gfx::IntSize& aSize);
+
+ protected:
+ already_AddRefed<TextureClient> Allocate(
+ gfx::SurfaceFormat aFormat, gfx::IntSize aSize, BackendSelector aSelector,
+ TextureFlags aTextureFlags, TextureAllocationFlags aAllocFlags) override;
+
+ RefPtr<IDirect3DDevice9> mDevice;
+};
+
+/**
+ * Wraps a D3D9 texture, shared with the compositor though DXGI.
+ * At the moment it is only used with D3D11 compositing, and the corresponding
+ * TextureHost is DXGITextureHostD3D11.
+ */
+class DXGID3D9TextureData : public TextureData {
+ public:
+ static DXGID3D9TextureData* Create(gfx::IntSize aSize,
+ gfx::SurfaceFormat aFormat,
+ TextureFlags aFlags,
+ IDirect3DDevice9* aDevice);
+
+ virtual ~DXGID3D9TextureData();
+
+ void FillInfo(TextureData::Info& aInfo) const override;
+
+ bool Lock(OpenMode) override { return true; }
+
+ void Unlock() override {}
+
+ bool Serialize(SurfaceDescriptor& aOutDescriptor) override;
+
+ void Deallocate(LayersIPCChannel* aAllocator) override {}
+
+ IDirect3DDevice9* GetD3D9Device() { return mDevice; }
+ IDirect3DTexture9* GetD3D9Texture() { return mTexture; }
+ HANDLE GetShareHandle() const { return mHandle; }
+ already_AddRefed<IDirect3DSurface9> GetD3D9Surface() const;
+
+ const D3DSURFACE_DESC& GetDesc() const { return mDesc; }
+
+ gfx::IntSize GetSize() const {
+ return gfx::IntSize(mDesc.Width, mDesc.Height);
+ }
+
+ protected:
+ DXGID3D9TextureData(gfx::SurfaceFormat aFormat, IDirect3DTexture9* aTexture,
+ HANDLE aHandle, IDirect3DDevice9* aDevice);
+
+ RefPtr<IDirect3DDevice9> mDevice;
+ RefPtr<IDirect3DTexture9> mTexture;
+ gfx::SurfaceFormat mFormat;
+ HANDLE mHandle;
+ D3DSURFACE_DESC mDesc;
+};
+
+// Image class that wraps a IDirect3DSurface9. This class copies the image
+// passed into SetData(), so that it can be accessed from other D3D devices.
+// This class also manages the synchronization of the copy, to ensure the
+// resource is ready to use.
+class D3D9SurfaceImage : public Image {
+ public:
+ D3D9SurfaceImage();
+ virtual ~D3D9SurfaceImage();
+
+ HRESULT AllocateAndCopy(D3D9RecycleAllocator* aAllocator,
+ IDirect3DSurface9* aSurface,
+ const gfx::IntRect& aRegion);
+
+ // Returns the description of the shared surface.
+ gfx::IntSize GetSize() const override;
+
+ already_AddRefed<gfx::SourceSurface> GetAsSourceSurface() override;
+
+ TextureClient* GetTextureClient(KnowsCompositor* aKnowsCompositor) override;
+
+ already_AddRefed<IDirect3DSurface9> GetD3D9Surface() const;
+
+ HANDLE GetShareHandle() const;
+
+ bool IsValid() const override { return mValid; }
+
+ void Invalidate() { mValid = false; }
+
+ private:
+ gfx::IntSize mSize;
+ RefPtr<TextureClient> mTextureClient;
+ RefPtr<IDirect3DTexture9> mTexture;
+ HANDLE mShareHandle;
+ D3DSURFACE_DESC mDesc;
+ bool mValid;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // GFX_D3DSURFACEIMAGE_H
diff --git a/gfx/layers/DMABUFSurfaceImage.cpp b/gfx/layers/DMABUFSurfaceImage.cpp
new file mode 100644
index 0000000000..8d9234bfd2
--- /dev/null
+++ b/gfx/layers/DMABUFSurfaceImage.cpp
@@ -0,0 +1,69 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "DMABUFSurfaceImage.h"
+#include "mozilla/widget/DMABufSurface.h"
+#include "mozilla/layers/CompositableClient.h"
+#include "mozilla/layers/CompositableForwarder.h"
+#include "mozilla/layers/DMABUFTextureClientOGL.h"
+#include "mozilla/layers/TextureForwarder.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/StaticMutex.h"
+#include "GLContext.h"
+#include "GLContextProvider.h"
+#include "GLBlitHelper.h"
+#include "GLReadTexImageHelper.h"
+#include "GLContextTypes.h" // for GLContext, etc
+#include "GLContextEGL.h"
+#include "GLContextProvider.h"
+#include "ScopedGLHelpers.h"
+
+using namespace mozilla;
+using namespace mozilla::layers;
+using namespace mozilla::gfx;
+using namespace mozilla::gl;
+
+#ifdef MOZ_LOGGING
+# undef DMABUF_LOG
+extern mozilla::LazyLogModule gDmabufLog;
+# define DMABUF_LOG(str, ...) \
+ MOZ_LOG(gDmabufLog, mozilla::LogLevel::Debug, (str, ##__VA_ARGS__))
+#else
+# define DMABUF_LOG(args)
+#endif /* MOZ_LOGGING */
+
+DMABUFSurfaceImage::DMABUFSurfaceImage(DMABufSurface* aSurface)
+ : Image(nullptr, ImageFormat::DMABUF), mSurface(aSurface) {
+ DMABUF_LOG("DMABUFSurfaceImage::DMABUFSurfaceImage (%p) aSurface %p UID %d\n",
+ this, aSurface, aSurface->GetUID());
+ mSurface->GlobalRefAdd();
+}
+
+DMABUFSurfaceImage::~DMABUFSurfaceImage() {
+ DMABUF_LOG(
+ "DMABUFSurfaceImage::~DMABUFSurfaceImage (%p) mSurface %p UID %d\n", this,
+ (void*)mSurface.get(), mSurface->GetUID());
+ mSurface->GlobalRefRelease();
+}
+
+already_AddRefed<gfx::SourceSurface> DMABUFSurfaceImage::GetAsSourceSurface() {
+ return mSurface->GetAsSourceSurface();
+}
+
+TextureClient* DMABUFSurfaceImage::GetTextureClient(
+ KnowsCompositor* aKnowsCompositor) {
+ if (!mTextureClient) {
+ BackendType backend = BackendType::NONE;
+ mTextureClient = TextureClient::CreateWithData(
+ DMABUFTextureData::Create(mSurface, backend), TextureFlags::DEFAULT,
+ aKnowsCompositor->GetTextureForwarder());
+ }
+ return mTextureClient;
+}
+
+gfx::IntSize DMABUFSurfaceImage::GetSize() const {
+ return gfx::IntSize::Truncate(mSurface->GetWidth(), mSurface->GetHeight());
+}
diff --git a/gfx/layers/DMABUFSurfaceImage.h b/gfx/layers/DMABUFSurfaceImage.h
new file mode 100644
index 0000000000..22dda8f88b
--- /dev/null
+++ b/gfx/layers/DMABUFSurfaceImage.h
@@ -0,0 +1,37 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef SURFACE_DMABUF_H
+#define SURFACE_DMABUF_H
+
+#include "ImageContainer.h"
+
+class DMABufSurface;
+
+namespace mozilla {
+namespace layers {
+
+class TextureClient;
+
+class DMABUFSurfaceImage : public Image {
+ public:
+ explicit DMABUFSurfaceImage(DMABufSurface* aSurface);
+ ~DMABUFSurfaceImage();
+
+ DMABufSurface* GetSurface() { return mSurface; }
+ gfx::IntSize GetSize() const override;
+ already_AddRefed<gfx::SourceSurface> GetAsSourceSurface() override;
+ TextureClient* GetTextureClient(KnowsCompositor* aKnowsCompositor) override;
+
+ private:
+ RefPtr<DMABufSurface> mSurface;
+ RefPtr<TextureClient> mTextureClient;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // SURFACE_DMABUF_H
diff --git a/gfx/layers/DcompSurfaceImage.cpp b/gfx/layers/DcompSurfaceImage.cpp
new file mode 100644
index 0000000000..70458fd8f0
--- /dev/null
+++ b/gfx/layers/DcompSurfaceImage.cpp
@@ -0,0 +1,150 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "DcompSurfaceImage.h"
+
+#include "mozilla/ipc/FileDescriptor.h"
+#include "mozilla/gfx/gfxVars.h"
+#include "mozilla/layers/CompositorTypes.h"
+#include "mozilla/layers/LayersSurfaces.h"
+#include "mozilla/layers/TextureForwarder.h"
+#include "mozilla/layers/KnowsCompositor.h"
+#include "mozilla/webrender/RenderDcompSurfaceTextureHost.h"
+#include "mozilla/webrender/WebRenderAPI.h"
+
+extern mozilla::LazyLogModule gDcompSurface;
+#define LOG(...) MOZ_LOG(gDcompSurface, LogLevel::Debug, (__VA_ARGS__))
+
+namespace mozilla::layers {
+
+already_AddRefed<TextureHost> CreateTextureHostDcompSurface(
+ const SurfaceDescriptor& aDesc, ISurfaceAllocator* aDeallocator,
+ LayersBackend aBackend, TextureFlags aFlags) {
+ MOZ_ASSERT(aDesc.type() == SurfaceDescriptor::TSurfaceDescriptorDcompSurface);
+ RefPtr<TextureHost> result = new DcompSurfaceHandleHost(
+ aFlags, aDesc.get_SurfaceDescriptorDcompSurface());
+ return result.forget();
+}
+
+/* static */
+already_AddRefed<TextureClient> DcompSurfaceTexture::CreateTextureClient(
+ HANDLE aHandle, gfx::IntSize aSize, gfx::SurfaceFormat aFormat,
+ KnowsCompositor* aKnowsCompositor) {
+ RefPtr<TextureClient> textureClient = MakeAndAddRef<TextureClient>(
+ new DcompSurfaceTexture(aHandle, aSize, aFormat), TextureFlags::NO_FLAGS,
+ aKnowsCompositor->GetTextureForwarder());
+ return textureClient.forget();
+}
+
+DcompSurfaceTexture::~DcompSurfaceTexture() {
+ LOG("Destroy DcompSurfaceTexture %p, close handle=%p", this, mHandle);
+ CloseHandle(mHandle);
+}
+
+bool DcompSurfaceTexture::Serialize(SurfaceDescriptor& aOutDescriptor) {
+ aOutDescriptor = SurfaceDescriptorDcompSurface(ipc::FileDescriptor(mHandle),
+ mSize, mFormat);
+ return true;
+}
+
+void DcompSurfaceTexture::GetSubDescriptor(
+ RemoteDecoderVideoSubDescriptor* aOutDesc) {
+ *aOutDesc = SurfaceDescriptorDcompSurface(ipc::FileDescriptor(mHandle), mSize,
+ mFormat);
+}
+
+DcompSurfaceImage::DcompSurfaceImage(HANDLE aHandle, gfx::IntSize aSize,
+ gfx::SurfaceFormat aFormat,
+ KnowsCompositor* aKnowsCompositor)
+ : Image(nullptr, ImageFormat::DCOMP_SURFACE),
+ mTextureClient(DcompSurfaceTexture::CreateTextureClient(
+ aHandle, aSize, aFormat, aKnowsCompositor)) {
+ // Dcomp surface supports DXGI_FORMAT_B8G8R8A8_UNORM,
+ // DXGI_FORMAT_R8G8B8A8_UNORM and DXGI_FORMAT_R16G16B16A16_FLOAT
+ MOZ_ASSERT(aFormat == gfx::SurfaceFormat::B8G8R8A8 ||
+ aFormat == gfx::SurfaceFormat::R8G8B8A8);
+}
+
+TextureClient* DcompSurfaceImage::GetTextureClient(
+ KnowsCompositor* aKnowsCompositor) {
+ return mTextureClient;
+}
+
+DcompSurfaceHandleHost::DcompSurfaceHandleHost(
+ TextureFlags aFlags, const SurfaceDescriptorDcompSurface& aDescriptor)
+ : TextureHost(TextureHostType::DcompSurface, aFlags),
+ mHandle(const_cast<ipc::FileDescriptor&>(aDescriptor.handle())
+ .TakePlatformHandle()),
+ mSize(aDescriptor.size()),
+ mFormat(aDescriptor.format()) {
+ LOG("Create DcompSurfaceHandleHost %p", this);
+}
+
+DcompSurfaceHandleHost::~DcompSurfaceHandleHost() {
+ LOG("Destroy DcompSurfaceHandleHost %p", this);
+}
+
+void DcompSurfaceHandleHost::CreateRenderTexture(
+ const wr::ExternalImageId& aExternalImageId) {
+ LOG("DcompSurfaceHandleHost %p CreateRenderTexture, ext-id=%" PRIu64, this,
+ wr::AsUint64(aExternalImageId));
+ RefPtr<wr::RenderTextureHost> texture =
+ new wr::RenderDcompSurfaceTextureHost(mHandle.get(), mSize, mFormat);
+ wr::RenderThread::Get()->RegisterExternalImage(aExternalImageId,
+ texture.forget());
+}
+
+void DcompSurfaceHandleHost::PushResourceUpdates(
+ wr::TransactionBuilder& aResources, ResourceUpdateOp aOp,
+ const Range<wr::ImageKey>& aImageKeys,
+ const wr::ExternalImageId& aExternalImageId) {
+ if (!gfx::gfxVars::UseWebRenderANGLE()) {
+ MOZ_ASSERT_UNREACHABLE("Unexpected to be called without ANGLE");
+ return;
+ }
+ MOZ_ASSERT(mHandle);
+ MOZ_ASSERT(aImageKeys.length() == 1);
+ auto method = aOp == TextureHost::ADD_IMAGE
+ ? &wr::TransactionBuilder::AddExternalImage
+ : &wr::TransactionBuilder::UpdateExternalImage;
+ wr::ImageDescriptor descriptor(mSize, GetFormat());
+ // Prefer TextureExternal unless the backend requires TextureRect.
+ TextureHost::NativeTexturePolicy policy =
+ TextureHost::BackendNativeTexturePolicy(aResources.GetBackendType(),
+ mSize);
+ auto imageType = policy == TextureHost::NativeTexturePolicy::REQUIRE
+ ? wr::ExternalImageType::TextureHandle(
+ wr::ImageBufferKind::TextureRect)
+ : wr::ExternalImageType::TextureHandle(
+ wr::ImageBufferKind::TextureExternal);
+ LOG("DcompSurfaceHandleHost %p PushResourceUpdate, exi-id=%" PRIu64
+ ", type=%s",
+ this, wr::AsUint64(aExternalImageId),
+ policy == TextureHost::NativeTexturePolicy::REQUIRE ? "rect" : "ext");
+ (aResources.*method)(aImageKeys[0], descriptor, aExternalImageId, imageType,
+ 0);
+}
+
+void DcompSurfaceHandleHost::PushDisplayItems(
+ wr::DisplayListBuilder& aBuilder, const wr::LayoutRect& aBounds,
+ const wr::LayoutRect& aClip, wr::ImageRendering aFilter,
+ const Range<wr::ImageKey>& aImageKeys, PushDisplayItemFlagSet aFlags) {
+ if (!gfx::gfxVars::UseWebRenderANGLE()) {
+ MOZ_ASSERT_UNREACHABLE("Unexpected to be called without ANGLE");
+ return;
+ }
+ LOG("DcompSurfaceHandleHost %p PushDisplayItems", this);
+ MOZ_ASSERT(aImageKeys.length() == 1);
+ aBuilder.PushImage(
+ aBounds, aClip, true, false, aFilter, aImageKeys[0],
+ !(mFlags & TextureFlags::NON_PREMULTIPLIED),
+ wr::ColorF{1.0f, 1.0f, 1.0f, 1.0f},
+ aFlags.contains(PushDisplayItemFlag::PREFER_COMPOSITOR_SURFACE),
+ SupportsExternalCompositing(aBuilder.GetBackendType()));
+}
+
+} // namespace mozilla::layers
+
+#undef LOG
diff --git a/gfx/layers/DcompSurfaceImage.h b/gfx/layers/DcompSurfaceImage.h
new file mode 100644
index 0000000000..c1eb412580
--- /dev/null
+++ b/gfx/layers/DcompSurfaceImage.h
@@ -0,0 +1,131 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GFX_LAYER_DCOMP_SURFACE_IMAGE_H
+#define GFX_LAYER_DCOMP_SURFACE_IMAGE_H
+
+#include "ImageContainer.h"
+#include "mozilla/layers/TextureClient.h"
+#include "mozilla/layers/TextureHost.h"
+
+namespace mozilla::wr {
+class DisplayListBuilder;
+class TransactionBuilder;
+} // namespace mozilla::wr
+
+namespace mozilla::layers {
+
+already_AddRefed<TextureHost> CreateTextureHostDcompSurface(
+ const SurfaceDescriptor& aDesc, ISurfaceAllocator* aDeallocator,
+ LayersBackend aBackend, TextureFlags aFlags);
+
+/**
+ * A texture data wrapping a dcomp surface handle, which is provided by the
+ * media foundation media engine. We won't be able to control real texture data
+ * because that is protected by the media engine.
+ */
+class DcompSurfaceTexture final : public TextureData {
+ public:
+ static already_AddRefed<TextureClient> CreateTextureClient(
+ HANDLE aHandle, gfx::IntSize aSize, gfx::SurfaceFormat aFormat,
+ KnowsCompositor* aKnowsCompositor);
+
+ ~DcompSurfaceTexture();
+
+ bool Serialize(SurfaceDescriptor& aOutDescriptor) override;
+ void GetSubDescriptor(RemoteDecoderVideoSubDescriptor* aOutDesc) override;
+ void FillInfo(TextureData::Info& aInfo) const {
+ aInfo.size = mSize;
+ aInfo.supportsMoz2D = false;
+ aInfo.canExposeMappedData = false;
+ aInfo.hasSynchronization = false;
+ }
+ bool Lock(OpenMode) override { return true; }
+ void Unlock() override {}
+ void Deallocate(LayersIPCChannel* aAllocator) override {}
+
+ private:
+ DcompSurfaceTexture(HANDLE aHandle, gfx::IntSize aSize,
+ gfx::SurfaceFormat aFormat)
+ : mHandle(aHandle), mSize(aSize), mFormat(aFormat) {}
+
+ const HANDLE mHandle;
+ const gfx::IntSize mSize;
+ const gfx::SurfaceFormat mFormat;
+};
+
+/**
+ * A image data which holds a dcomp texture which is provided by the media
+ * foundation media engine. As the real texture is protected, it does not
+ * support being accessed as a source surface.
+ */
+class DcompSurfaceImage : public Image {
+ public:
+ DcompSurfaceImage(HANDLE aHandle, gfx::IntSize aSize,
+ gfx::SurfaceFormat aFormat,
+ KnowsCompositor* aKnowsCompositor);
+ virtual ~DcompSurfaceImage() = default;
+
+ already_AddRefed<gfx::SourceSurface> GetAsSourceSurface() override {
+ return nullptr;
+ }
+
+ gfx::IntSize GetSize() const { return mTextureClient->GetSize(); };
+
+ TextureClient* GetTextureClient(KnowsCompositor* aKnowsCompositor) override;
+
+ private:
+ RefPtr<TextureClient> mTextureClient;
+};
+
+/**
+ * A Texture host living in GPU process which owns a dcomp surface handle which
+ * is provided by the media foundation media engine.
+ */
+class DcompSurfaceHandleHost : public TextureHost {
+ public:
+ DcompSurfaceHandleHost(TextureFlags aFlags,
+ const SurfaceDescriptorDcompSurface& aDescriptor);
+
+ gfx::SurfaceFormat GetFormat() const override { return mFormat; }
+
+ gfx::IntSize GetSize() const override { return mSize; }
+
+ const char* Name() override { return "DcompSurfaceHandleHost"; }
+
+ already_AddRefed<gfx::DataSourceSurface> GetAsSurface() override {
+ return nullptr;
+ }
+
+ void CreateRenderTexture(
+ const wr::ExternalImageId& aExternalImageId) override;
+
+ void PushResourceUpdates(
+ wr::TransactionBuilder& aResources, ResourceUpdateOp aOp,
+ const Range<wr::ImageKey>& aImageKeys,
+ const wr::ExternalImageId& aExternalImageId) override;
+
+ void PushDisplayItems(wr::DisplayListBuilder& aBuilder,
+ const wr::LayoutRect& aBounds,
+ const wr::LayoutRect& aClip, wr::ImageRendering aFilter,
+ const Range<wr::ImageKey>& aImageKeys,
+ PushDisplayItemFlagSet aFlags) override;
+
+ bool SupportsExternalCompositing(WebRenderBackend aBackend) override {
+ return true;
+ }
+
+ protected:
+ ~DcompSurfaceHandleHost();
+
+ // Handle will be closed automatically when `UniqueFileHandle` gets destroyed.
+ const mozilla::UniqueFileHandle mHandle;
+ const gfx::IntSize mSize;
+ const gfx::SurfaceFormat mFormat;
+};
+
+} // namespace mozilla::layers
+
+#endif // GFX_LAYER_DCOMP_SURFACE_IMAGE_H
diff --git a/gfx/layers/DirectionUtils.h b/gfx/layers/DirectionUtils.h
new file mode 100644
index 0000000000..84d9cb6969
--- /dev/null
+++ b/gfx/layers/DirectionUtils.h
@@ -0,0 +1,62 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GFX_DIRECTIONUTILS_H
+#define GFX_DIRECTIONUTILS_H
+
+#include "LayersTypes.h" // for ScrollDirection
+#include "Units.h" // for Coord, Point, and Rect types
+
+namespace mozilla {
+namespace layers {
+
+template <typename PointOrRect>
+CoordOf<PointOrRect> GetAxisStart(ScrollDirection aDir,
+ const PointOrRect& aValue) {
+ if (aDir == ScrollDirection::eHorizontal) {
+ return aValue.X();
+ } else {
+ return aValue.Y();
+ }
+}
+
+template <typename Rect>
+CoordOf<Rect> GetAxisEnd(ScrollDirection aDir, const Rect& aValue) {
+ if (aDir == ScrollDirection::eHorizontal) {
+ return aValue.XMost();
+ } else {
+ return aValue.YMost();
+ }
+}
+
+template <typename Rect>
+CoordOf<Rect> GetAxisLength(ScrollDirection aDir, const Rect& aValue) {
+ if (aDir == ScrollDirection::eHorizontal) {
+ return aValue.Width();
+ } else {
+ return aValue.Height();
+ }
+}
+
+template <typename FromUnits, typename ToUnits>
+float GetAxisScale(ScrollDirection aDir,
+ const gfx::ScaleFactors2D<FromUnits, ToUnits>& aValue) {
+ if (aDir == ScrollDirection::eHorizontal) {
+ return aValue.xScale;
+ } else {
+ return aValue.yScale;
+ }
+}
+
+inline ScrollDirection GetPerpendicularDirection(ScrollDirection aDir) {
+ return aDir == ScrollDirection::eHorizontal ? ScrollDirection::eVertical
+ : ScrollDirection::eHorizontal;
+}
+
+} // namespace layers
+} // namespace mozilla
+
+#endif /* GFX_DIRECTIONUTILS_H */
diff --git a/gfx/layers/Effects.cpp b/gfx/layers/Effects.cpp
new file mode 100644
index 0000000000..2de4f7e7bf
--- /dev/null
+++ b/gfx/layers/Effects.cpp
@@ -0,0 +1,27 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "Effects.h"
+#include "nsAString.h"
+#include "nsPrintfCString.h" // for nsPrintfCString
+#include "nsString.h" // for nsAutoCString
+
+using namespace mozilla::layers;
+
+void TexturedEffect::PrintInfo(std::stringstream& aStream,
+ const char* aPrefix) {
+ aStream << aPrefix;
+ aStream << nsPrintfCString("%s (0x%p)", Name(), this).get()
+ << " [texture-coords=" << mTextureCoords << "]";
+
+ if (mPremultiplied) {
+ aStream << " [premultiplied]";
+ } else {
+ aStream << " [not-premultiplied]";
+ }
+
+ aStream << " [filter=" << mSamplingFilter << "]";
+}
diff --git a/gfx/layers/Effects.h b/gfx/layers/Effects.h
new file mode 100644
index 0000000000..694b7c1240
--- /dev/null
+++ b/gfx/layers/Effects.h
@@ -0,0 +1,201 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_LAYERS_EFFECTS_H
+#define MOZILLA_LAYERS_EFFECTS_H
+
+#include "mozilla/Assertions.h" // for MOZ_ASSERT, etc
+#include "mozilla/RefPtr.h" // for RefPtr, already_AddRefed, etc
+#include "mozilla/gfx/Matrix.h" // for Matrix4x4
+#include "mozilla/gfx/Point.h" // for IntSize
+#include "mozilla/gfx/Rect.h" // for Rect
+#include "mozilla/gfx/Types.h" // for SamplingFilter, etc
+#include "mozilla/layers/CompositorTypes.h" // for EffectTypes, etc
+#include "mozilla/layers/TextureHost.h" // for CompositingRenderTarget, etc
+#include "mozilla/mozalloc.h" // for operator delete, etc
+#include "nscore.h" // for nsACString
+#include "mozilla/EnumeratedArray.h"
+
+namespace mozilla {
+namespace layers {
+
+/**
+ * Effects and effect chains are used by the compositor API (see Compositor.h).
+ * An effect chain represents a rendering method, for example some shader and
+ * the data required for that shader to run. An effect is some component of the
+ * chain and its data.
+ *
+ * An effect chain consists of a primary effect - how the 'texture' memory
+ * should be interpreted (RGBA, BGRX, YCBCR, etc.) - and any number of secondary
+ * effects
+ * - any way in which rendering can be changed, e.g., applying a mask layer.
+ *
+ * During the rendering process, an effect chain is created by the layer being
+ * rendered and the primary effect is added by the compositable host. Secondary
+ * effects may be added by the layer or compositable. The effect chain is passed
+ * to the compositor by the compositable host as a parameter to DrawQuad.
+ */
+
+struct TexturedEffect;
+
+struct Effect {
+ NS_INLINE_DECL_REFCOUNTING(Effect)
+
+ explicit Effect(EffectTypes aType) : mType(aType) {}
+
+ EffectTypes mType;
+
+ virtual TexturedEffect* AsTexturedEffect() { return nullptr; }
+ virtual void PrintInfo(std::stringstream& aStream, const char* aPrefix) = 0;
+
+ protected:
+ virtual ~Effect() = default;
+};
+
+// Render from a texture
+struct TexturedEffect : public Effect {
+ TexturedEffect(EffectTypes aType, TextureSource* aTexture,
+ bool aPremultiplied, gfx::SamplingFilter aSamplingFilter)
+ : Effect(aType),
+ mTextureCoords(0, 0, 1.0f, 1.0f),
+ mTexture(aTexture),
+ mPremultiplied(aPremultiplied),
+ mPremultipliedCopy(false),
+ mSamplingFilter(aSamplingFilter) {}
+
+ TexturedEffect* AsTexturedEffect() override { return this; }
+ virtual const char* Name() = 0;
+ void PrintInfo(std::stringstream& aStream, const char* aPrefix) override;
+
+ gfx::Rect mTextureCoords;
+ TextureSource* mTexture;
+ bool mPremultiplied;
+ bool mPremultipliedCopy;
+ gfx::SamplingFilter mSamplingFilter;
+};
+
+struct EffectRGB : public TexturedEffect {
+ EffectRGB(TextureSource* aTexture, bool aPremultiplied,
+ gfx::SamplingFilter aSamplingFilter, bool aFlipped = false)
+ : TexturedEffect(EffectTypes::RGB, aTexture, aPremultiplied,
+ aSamplingFilter) {}
+
+ const char* Name() override { return "EffectRGB"; }
+};
+
+struct EffectYCbCr : public TexturedEffect {
+ EffectYCbCr(TextureSource* aSource, gfx::YUVColorSpace aYUVColorSpace,
+ gfx::ColorRange aColorRange, gfx::ColorDepth aColorDepth,
+ gfx::SamplingFilter aSamplingFilter)
+ : TexturedEffect(EffectTypes::YCBCR, aSource, false, aSamplingFilter),
+ mYUVColorSpace(aYUVColorSpace),
+ mColorRange(aColorRange),
+ mColorDepth(aColorDepth) {}
+
+ const char* Name() override { return "EffectYCbCr"; }
+
+ gfx::YUVColorSpace mYUVColorSpace;
+ gfx::ColorRange mColorRange;
+ gfx::ColorDepth mColorDepth;
+};
+
+struct EffectNV12 : public EffectYCbCr {
+ EffectNV12(TextureSource* aSource, gfx::YUVColorSpace aYUVColorSpace,
+ gfx::ColorRange aColorRange, gfx::ColorDepth aColorDepth,
+ gfx::SamplingFilter aSamplingFilter)
+ : EffectYCbCr(aSource, aYUVColorSpace, aColorRange, aColorDepth,
+ aSamplingFilter) {
+ mType = EffectTypes::NV12;
+ }
+
+ const char* Name() override { return "EffectNV12"; }
+};
+
+struct EffectChain {
+ RefPtr<Effect> mPrimaryEffect;
+};
+
+/**
+ * Create a Textured effect corresponding to aFormat and using
+ * aSource as the (first) texture source.
+ *
+ * Note that aFormat can be different form aSource->GetFormat if, we are
+ * creating an effect that takes several texture sources (like with YCBCR
+ * where aFormat would be FORMAT_YCBCR and each texture source would be
+ * a one-channel A8 texture)
+ */
+inline already_AddRefed<TexturedEffect> CreateTexturedEffect(
+ gfx::SurfaceFormat aFormat, TextureSource* aSource,
+ const gfx::SamplingFilter aSamplingFilter, bool isAlphaPremultiplied) {
+ MOZ_ASSERT(aSource);
+ RefPtr<TexturedEffect> result;
+ switch (aFormat) {
+ case gfx::SurfaceFormat::B8G8R8A8:
+ case gfx::SurfaceFormat::B8G8R8X8:
+ case gfx::SurfaceFormat::R8G8B8X8:
+ case gfx::SurfaceFormat::R5G6B5_UINT16:
+ case gfx::SurfaceFormat::R8G8B8A8:
+ result = new EffectRGB(aSource, isAlphaPremultiplied, aSamplingFilter);
+ break;
+ case gfx::SurfaceFormat::YUV:
+ case gfx::SurfaceFormat::NV12:
+ case gfx::SurfaceFormat::P010:
+ case gfx::SurfaceFormat::P016:
+ MOZ_ASSERT_UNREACHABLE(
+ "gfx::SurfaceFormat::YUV/NV12/P010/P016 is invalid");
+ break;
+ default:
+ NS_WARNING("unhandled program type");
+ break;
+ }
+
+ return result.forget();
+}
+
+inline already_AddRefed<TexturedEffect> CreateTexturedEffect(
+ TextureHost* aHost, TextureSource* aSource,
+ const gfx::SamplingFilter aSamplingFilter, bool isAlphaPremultiplied) {
+ MOZ_ASSERT(aHost);
+ MOZ_ASSERT(aSource);
+
+ RefPtr<TexturedEffect> result;
+
+ switch (aHost->GetReadFormat()) {
+ case gfx::SurfaceFormat::YUV:
+ result = new EffectYCbCr(aSource, aHost->GetYUVColorSpace(),
+ aHost->GetColorRange(), aHost->GetColorDepth(),
+ aSamplingFilter);
+ break;
+ case gfx::SurfaceFormat::NV12:
+ case gfx::SurfaceFormat::P010:
+ case gfx::SurfaceFormat::P016:
+ result = new EffectNV12(aSource, aHost->GetYUVColorSpace(),
+ aHost->GetColorRange(), aHost->GetColorDepth(),
+ aSamplingFilter);
+ break;
+ default:
+ result = CreateTexturedEffect(aHost->GetReadFormat(), aSource,
+ aSamplingFilter, isAlphaPremultiplied);
+ break;
+ }
+ return result.forget();
+}
+
+/**
+ * Create a textured effect based on aSource format.
+ *
+ * This version excudes the possibility of component alpha.
+ */
+inline already_AddRefed<TexturedEffect> CreateTexturedEffect(
+ TextureSource* aTexture, const gfx::SamplingFilter aSamplingFilter) {
+ return CreateTexturedEffect(aTexture->GetFormat(), aTexture, aSamplingFilter,
+ true);
+}
+
+} // namespace layers
+} // namespace mozilla
+
+#endif
diff --git a/gfx/layers/FrameMetrics.cpp b/gfx/layers/FrameMetrics.cpp
new file mode 100644
index 0000000000..eb84d22db6
--- /dev/null
+++ b/gfx/layers/FrameMetrics.cpp
@@ -0,0 +1,342 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "FrameMetrics.h"
+
+#include <ostream>
+
+#include "gfxUtils.h"
+#include "nsStyleConsts.h"
+#include "nsStyleStruct.h"
+#include "mozilla/WritingModes.h"
+#include "mozilla/gfx/Types.h"
+
+namespace mozilla {
+namespace layers {
+
+const ScrollableLayerGuid::ViewID ScrollableLayerGuid::NULL_SCROLL_ID = 0;
+
+std::ostream& operator<<(std::ostream& aStream, const FrameMetrics& aMetrics) {
+ aStream << "{ [cb=" << aMetrics.GetCompositionBounds()
+ << "] [sr=" << aMetrics.GetScrollableRect()
+ << "] [s=" << aMetrics.GetVisualScrollOffset();
+ if (aMetrics.GetVisualScrollUpdateType() != FrameMetrics::eNone) {
+ aStream << "] [vd=" << aMetrics.GetVisualDestination();
+ }
+ if (aMetrics.IsScrollInfoLayer()) {
+ aStream << "] [scrollinfo";
+ }
+ aStream << "] [dp=" << aMetrics.GetDisplayPort()
+ << "] [rcs=" << aMetrics.GetBoundingCompositionSize()
+ << "] [v=" << aMetrics.GetLayoutViewport()
+ << nsPrintfCString("] [z=(ld=%.3f r=%.3f",
+ aMetrics.GetDevPixelsPerCSSPixel().scale,
+ aMetrics.GetPresShellResolution())
+ .get()
+ << " cr=" << aMetrics.GetCumulativeResolution()
+ << " z=" << aMetrics.GetZoom()
+ << " t=" << aMetrics.GetTransformToAncestorScale() << " )] [u=("
+ << (int)aMetrics.GetVisualScrollUpdateType() << " "
+ << aMetrics.GetScrollGeneration()
+ << ")] scrollId=" << aMetrics.GetScrollId();
+ if (aMetrics.IsRootContent()) {
+ aStream << " [rcd]";
+ }
+ aStream << " }";
+ return aStream;
+}
+
+void FrameMetrics::RecalculateLayoutViewportOffset() {
+ // For subframes, the visual and layout viewports coincide, so just
+ // keep the layout viewport offset in sync with the visual one.
+ if (!mIsRootContent) {
+ mLayoutViewport.MoveTo(GetVisualScrollOffset());
+ return;
+ }
+ // For the root, the two viewports can diverge, but the layout
+ // viewport needs to keep enclosing the visual viewport.
+ KeepLayoutViewportEnclosingVisualViewport(GetVisualViewport(),
+ mScrollableRect, mLayoutViewport);
+}
+
+/* static */
+void FrameMetrics::KeepLayoutViewportEnclosingVisualViewport(
+ const CSSRect& aVisualViewport, const CSSRect& aScrollableRect,
+ CSSRect& aLayoutViewport) {
+ // If the visual viewport is contained within the layout viewport, we don't
+ // need to make any adjustments, so we can exit early.
+ //
+ // Additionally, if the composition bounds changes (due to an orientation
+ // change, window resize, etc.), it may take a few frames for aLayoutViewport
+ // to update and during that time, the visual viewport may be larger than the
+ // layout viewport. In such situations, we take an early exit if the visual
+ // viewport contains the layout viewport.
+ if (aLayoutViewport.Contains(aVisualViewport) ||
+ aVisualViewport.Contains(aLayoutViewport)) {
+ return;
+ }
+
+ // If visual viewport size is greater than the layout viewport, move the
+ // layout viewport such that it remains inside the visual viewport. Otherwise,
+ // move the layout viewport such that the visual viewport is contained
+ // inside the layout viewport.
+ if ((aLayoutViewport.Width() < aVisualViewport.Width() &&
+ !FuzzyEqualsMultiplicative(aLayoutViewport.Width(),
+ aVisualViewport.Width())) ||
+ (aLayoutViewport.Height() < aVisualViewport.Height() &&
+ !FuzzyEqualsMultiplicative(aLayoutViewport.Height(),
+ aVisualViewport.Height()))) {
+ if (aLayoutViewport.X() < aVisualViewport.X()) {
+ // layout viewport moves right
+ aLayoutViewport.MoveToX(aVisualViewport.X());
+ } else if (aVisualViewport.XMost() < aLayoutViewport.XMost()) {
+ // layout viewport moves left
+ aLayoutViewport.MoveByX(aVisualViewport.XMost() -
+ aLayoutViewport.XMost());
+ }
+ if (aLayoutViewport.Y() < aVisualViewport.Y()) {
+ // layout viewport moves down
+ aLayoutViewport.MoveToY(aVisualViewport.Y());
+ } else if (aVisualViewport.YMost() < aLayoutViewport.YMost()) {
+ // layout viewport moves up
+ aLayoutViewport.MoveByY(aVisualViewport.YMost() -
+ aLayoutViewport.YMost());
+ }
+ } else {
+ if (aVisualViewport.X() < aLayoutViewport.X()) {
+ aLayoutViewport.MoveToX(aVisualViewport.X());
+ } else if (aLayoutViewport.XMost() < aVisualViewport.XMost()) {
+ aLayoutViewport.MoveByX(aVisualViewport.XMost() -
+ aLayoutViewport.XMost());
+ }
+ if (aVisualViewport.Y() < aLayoutViewport.Y()) {
+ aLayoutViewport.MoveToY(aVisualViewport.Y());
+ } else if (aLayoutViewport.YMost() < aVisualViewport.YMost()) {
+ aLayoutViewport.MoveByY(aVisualViewport.YMost() -
+ aLayoutViewport.YMost());
+ }
+ }
+
+ // Regardless of any adjustment above, the layout viewport is not allowed
+ // to go outside the scrollable rect.
+ aLayoutViewport = aLayoutViewport.MoveInsideAndClamp(aScrollableRect);
+}
+
+/* static */
+CSSRect FrameMetrics::CalculateScrollRange(
+ const CSSRect& aScrollableRect, const ParentLayerRect& aCompositionBounds,
+ const CSSToParentLayerScale& aZoom) {
+ CSSSize scrollPortSize =
+ CalculateCompositedSizeInCssPixels(aCompositionBounds, aZoom);
+ CSSRect scrollRange = aScrollableRect;
+ scrollRange.SetWidth(
+ std::max(scrollRange.Width() - scrollPortSize.width, 0.0f));
+ scrollRange.SetHeight(
+ std::max(scrollRange.Height() - scrollPortSize.height, 0.0f));
+ return scrollRange;
+}
+
+/* static */
+CSSSize FrameMetrics::CalculateCompositedSizeInCssPixels(
+ const ParentLayerRect& aCompositionBounds,
+ const CSSToParentLayerScale& aZoom) {
+ if (aZoom == CSSToParentLayerScale(0)) {
+ return CSSSize(); // avoid division by zero
+ }
+ return aCompositionBounds.Size() / aZoom;
+}
+
+bool FrameMetrics::ApplyScrollUpdateFrom(const ScrollPositionUpdate& aUpdate) {
+ // In applying a main-thread scroll update, try to preserve the relative
+ // offset between the visual and layout viewports.
+ CSSPoint relativeOffset = GetVisualScrollOffset() - GetLayoutScrollOffset();
+ MOZ_ASSERT(IsRootContent() || relativeOffset == CSSPoint());
+ // We need to set the two offsets together, otherwise a subsequent
+ // RecalculateLayoutViewportOffset() could see divergent layout and
+ // visual offsets.
+ bool offsetChanged = SetLayoutScrollOffset(aUpdate.GetDestination());
+ offsetChanged |=
+ ClampAndSetVisualScrollOffset(aUpdate.GetDestination() + relativeOffset);
+ return offsetChanged;
+}
+
+CSSPoint FrameMetrics::ApplyRelativeScrollUpdateFrom(
+ const ScrollPositionUpdate& aUpdate) {
+ MOZ_ASSERT(aUpdate.GetType() == ScrollUpdateType::Relative);
+ CSSPoint origin = GetVisualScrollOffset();
+ CSSPoint delta = (aUpdate.GetDestination() - aUpdate.GetSource());
+ ClampAndSetVisualScrollOffset(origin + delta);
+ return GetVisualScrollOffset() - origin;
+}
+
+CSSPoint FrameMetrics::ApplyPureRelativeScrollUpdateFrom(
+ const ScrollPositionUpdate& aUpdate) {
+ MOZ_ASSERT(aUpdate.GetType() == ScrollUpdateType::PureRelative);
+ CSSPoint origin = GetVisualScrollOffset();
+ ClampAndSetVisualScrollOffset(origin + aUpdate.GetDelta());
+ return GetVisualScrollOffset() - origin;
+}
+
+void FrameMetrics::UpdatePendingScrollInfo(const ScrollPositionUpdate& aInfo) {
+ // We only get this "pending scroll info" for paint-skip transactions,
+ // but PureRelative position updates always trigger a full paint, so
+ // we should never enter this code with a PureRelative update type. For
+ // the other types, the destination field on the ScrollPositionUpdate will
+ // tell us the final layout scroll position on the main thread.
+ MOZ_ASSERT(aInfo.GetType() != ScrollUpdateType::PureRelative);
+
+ // In applying a main-thread scroll update, try to preserve the relative
+ // offset between the visual and layout viewports.
+ CSSPoint relativeOffset = GetVisualScrollOffset() - GetLayoutScrollOffset();
+ MOZ_ASSERT(IsRootContent() || relativeOffset == CSSPoint());
+
+ SetLayoutScrollOffset(aInfo.GetDestination());
+ ClampAndSetVisualScrollOffset(aInfo.GetDestination() + relativeOffset);
+ mScrollGeneration = aInfo.GetGeneration();
+}
+
+ScrollSnapInfo::ScrollSnapInfo()
+ : mScrollSnapStrictnessX(StyleScrollSnapStrictness::None),
+ mScrollSnapStrictnessY(StyleScrollSnapStrictness::None) {}
+
+bool ScrollSnapInfo::HasScrollSnapping() const {
+ return mScrollSnapStrictnessY != StyleScrollSnapStrictness::None ||
+ mScrollSnapStrictnessX != StyleScrollSnapStrictness::None;
+}
+
+bool ScrollSnapInfo::HasSnapPositions() const {
+ if (!HasScrollSnapping()) {
+ return false;
+ }
+
+ for (const auto& target : mSnapTargets) {
+ if ((target.mSnapPositionX &&
+ mScrollSnapStrictnessX != StyleScrollSnapStrictness::None) ||
+ (target.mSnapPositionY &&
+ mScrollSnapStrictnessY != StyleScrollSnapStrictness::None)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+void ScrollSnapInfo::InitializeScrollSnapStrictness(
+ WritingMode aWritingMode, const nsStyleDisplay* aDisplay) {
+ if (aDisplay->mScrollSnapType.strictness == StyleScrollSnapStrictness::None) {
+ return;
+ }
+
+ mScrollSnapStrictnessX = StyleScrollSnapStrictness::None;
+ mScrollSnapStrictnessY = StyleScrollSnapStrictness::None;
+
+ switch (aDisplay->mScrollSnapType.axis) {
+ case StyleScrollSnapAxis::X:
+ mScrollSnapStrictnessX = aDisplay->mScrollSnapType.strictness;
+ break;
+ case StyleScrollSnapAxis::Y:
+ mScrollSnapStrictnessY = aDisplay->mScrollSnapType.strictness;
+ break;
+ case StyleScrollSnapAxis::Block:
+ if (aWritingMode.IsVertical()) {
+ mScrollSnapStrictnessX = aDisplay->mScrollSnapType.strictness;
+ } else {
+ mScrollSnapStrictnessY = aDisplay->mScrollSnapType.strictness;
+ }
+ break;
+ case StyleScrollSnapAxis::Inline:
+ if (aWritingMode.IsVertical()) {
+ mScrollSnapStrictnessY = aDisplay->mScrollSnapType.strictness;
+ } else {
+ mScrollSnapStrictnessX = aDisplay->mScrollSnapType.strictness;
+ }
+ break;
+ case StyleScrollSnapAxis::Both:
+ mScrollSnapStrictnessX = aDisplay->mScrollSnapType.strictness;
+ mScrollSnapStrictnessY = aDisplay->mScrollSnapType.strictness;
+ break;
+ }
+}
+
+std::ostream& operator<<(std::ostream& aStream,
+ const OverscrollBehavior& aBehavior) {
+ switch (aBehavior) {
+ case OverscrollBehavior::Auto: {
+ aStream << "auto";
+ break;
+ }
+ case OverscrollBehavior::Contain: {
+ aStream << "contain";
+ break;
+ }
+ case OverscrollBehavior::None: {
+ aStream << "none";
+ break;
+ }
+ }
+ return aStream;
+}
+
+OverscrollBehaviorInfo::OverscrollBehaviorInfo()
+ : mBehaviorX(OverscrollBehavior::Auto),
+ mBehaviorY(OverscrollBehavior::Auto) {}
+
+static OverscrollBehavior ToOverscrollBehavior(
+ StyleOverscrollBehavior aBehavior) {
+ switch (aBehavior) {
+ case StyleOverscrollBehavior::Auto:
+ return OverscrollBehavior::Auto;
+ case StyleOverscrollBehavior::Contain:
+ return OverscrollBehavior::Contain;
+ case StyleOverscrollBehavior::None:
+ return OverscrollBehavior::None;
+ }
+ MOZ_ASSERT_UNREACHABLE("Invalid overscroll behavior");
+ return OverscrollBehavior::Auto;
+}
+
+OverscrollBehaviorInfo OverscrollBehaviorInfo::FromStyleConstants(
+ StyleOverscrollBehavior aBehaviorX, StyleOverscrollBehavior aBehaviorY) {
+ OverscrollBehaviorInfo result;
+ result.mBehaviorX = ToOverscrollBehavior(aBehaviorX);
+ result.mBehaviorY = ToOverscrollBehavior(aBehaviorY);
+ return result;
+}
+
+bool OverscrollBehaviorInfo::operator==(
+ const OverscrollBehaviorInfo& aOther) const {
+ return mBehaviorX == aOther.mBehaviorX && mBehaviorY == aOther.mBehaviorY;
+}
+
+std::ostream& operator<<(std::ostream& aStream,
+ const OverscrollBehaviorInfo& aInfo) {
+ if (aInfo.mBehaviorX == aInfo.mBehaviorY) {
+ aStream << aInfo.mBehaviorX;
+ } else {
+ aStream << "{ x=" << aInfo.mBehaviorX << ", y=" << aInfo.mBehaviorY << " }";
+ }
+ return aStream;
+}
+
+std::ostream& operator<<(std::ostream& aStream,
+ const ScrollMetadata& aMetadata) {
+ aStream << "{ [description=" << aMetadata.GetContentDescription()
+ << "] [metrics=" << aMetadata.GetMetrics();
+ if (aMetadata.GetScrollParentId() != ScrollableLayerGuid::NULL_SCROLL_ID) {
+ aStream << "] [scrollParent=" << aMetadata.GetScrollParentId();
+ }
+ if (aMetadata.GetHasScrollgrab()) {
+ aStream << "] [scrollgrab";
+ }
+ aStream << "] [overscroll=" << aMetadata.GetOverscrollBehavior() << "] ["
+ << aMetadata.GetScrollUpdates().Length() << " scrollupdates"
+ << "] }";
+ return aStream;
+}
+
+StaticAutoPtr<const ScrollMetadata> ScrollMetadata::sNullMetadata;
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/FrameMetrics.h b/gfx/layers/FrameMetrics.h
new file mode 100644
index 0000000000..fde4176f2a
--- /dev/null
+++ b/gfx/layers/FrameMetrics.h
@@ -0,0 +1,1114 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GFX_FRAMEMETRICS_H
+#define GFX_FRAMEMETRICS_H
+
+#include <stdint.h> // for uint8_t, uint32_t, uint64_t
+#include <iosfwd>
+
+#include "Units.h" // for CSSRect, CSSPixel, etc
+#include "UnitTransforms.h" // for ViewAs
+#include "mozilla/DefineEnum.h" // for MOZ_DEFINE_ENUM
+#include "mozilla/HashFunctions.h" // for HashGeneric
+#include "mozilla/Maybe.h"
+#include "mozilla/gfx/BasePoint.h" // for BasePoint
+#include "mozilla/gfx/Rect.h" // for RoundedIn
+#include "mozilla/gfx/ScaleFactor.h" // for ScaleFactor
+#include "mozilla/gfx/Logging.h" // for Log
+#include "mozilla/layers/LayersTypes.h" // for ScrollDirection
+#include "mozilla/layers/ScrollableLayerGuid.h" // for ScrollableLayerGuid
+#include "mozilla/ScrollPositionUpdate.h" // for ScrollPositionUpdate
+#include "mozilla/ScrollSnapTargetId.h"
+#include "mozilla/StaticPtr.h" // for StaticAutoPtr
+#include "mozilla/TimeStamp.h" // for TimeStamp
+#include "nsTHashMap.h" // for nsTHashMap
+#include "nsString.h"
+#include "PLDHashTable.h" // for PLDHashNumber
+
+struct nsStyleDisplay;
+namespace mozilla {
+enum class StyleScrollSnapStop : uint8_t;
+enum class StyleScrollSnapStrictness : uint8_t;
+enum class StyleOverscrollBehavior : uint8_t;
+class WritingMode;
+} // namespace mozilla
+
+namespace IPC {
+template <typename T>
+struct ParamTraits;
+} // namespace IPC
+
+namespace mozilla {
+namespace layers {
+
+/**
+ * Metrics about a scroll frame that are sent to the compositor and used
+ * by APZ.
+ *
+ * This is used for two main purposes:
+ *
+ * (1) Sending information about a scroll frame to the compositor and APZ
+ * as part of a layers or WebRender transaction.
+ * (2) Storing information about a scroll frame in APZ that persists
+ * between transactions.
+ *
+ * TODO: Separate these two uses into two distinct structures.
+ *
+ * A related class, RepaintRequest, is used for sending information about a
+ * scroll frame back from the compositor to the main thread when requesting
+ * a repaint of the scroll frame's contents.
+ */
+struct FrameMetrics {
+ friend struct IPC::ParamTraits<mozilla::layers::FrameMetrics>;
+ friend std::ostream& operator<<(std::ostream& aStream,
+ const FrameMetrics& aMetrics);
+
+ typedef ScrollableLayerGuid::ViewID ViewID;
+
+ public:
+ // clang-format off
+ MOZ_DEFINE_ENUM_WITH_BASE_AT_CLASS_SCOPE(
+ ScrollOffsetUpdateType, uint8_t, (
+ eNone, // The default; the scroll offset was not updated
+ eMainThread, // The scroll offset was updated by the main thread.
+ eRestore // The scroll offset was updated by the main thread, but
+ // as a restore from history or after a frame
+ // reconstruction. In this case, APZ can ignore the
+ // offset change if the user has done an APZ scroll
+ // already.
+ ));
+ // clang-format on
+
+ FrameMetrics()
+ : mScrollId(ScrollableLayerGuid::NULL_SCROLL_ID),
+ mPresShellResolution(1),
+ mCompositionBounds(0, 0, 0, 0),
+ mCompositionBoundsWidthIgnoringScrollbars(0),
+ mDisplayPort(0, 0, 0, 0),
+ mScrollableRect(0, 0, 0, 0),
+ mCumulativeResolution(),
+ mDevPixelsPerCSSPixel(1),
+ mScrollOffset(0, 0),
+ mZoom(),
+ mBoundingCompositionSize(0, 0),
+ mPresShellId(-1),
+ mLayoutViewport(0, 0, 0, 0),
+ mTransformToAncestorScale(),
+ mPaintRequestTime(),
+ mVisualDestination(0, 0),
+ mVisualScrollUpdateType(eNone),
+ mCompositionSizeWithoutDynamicToolbar(),
+ mIsRootContent(false),
+ mIsScrollInfoLayer(false),
+ mHasNonZeroDisplayPortMargins(false),
+ mMinimalDisplayPort(false) {}
+
+ // Default copy ctor and operator= are fine
+
+ bool operator==(const FrameMetrics& aOther) const {
+ // Put mScrollId at the top since it's the most likely one to fail.
+ return mScrollId == aOther.mScrollId &&
+ mPresShellResolution == aOther.mPresShellResolution &&
+ mCompositionBounds.IsEqualEdges(aOther.mCompositionBounds) &&
+ mCompositionBoundsWidthIgnoringScrollbars ==
+ aOther.mCompositionBoundsWidthIgnoringScrollbars &&
+ mDisplayPort.IsEqualEdges(aOther.mDisplayPort) &&
+ mScrollableRect.IsEqualEdges(aOther.mScrollableRect) &&
+ mCumulativeResolution == aOther.mCumulativeResolution &&
+ mDevPixelsPerCSSPixel == aOther.mDevPixelsPerCSSPixel &&
+ mScrollOffset == aOther.mScrollOffset &&
+ // don't compare mZoom
+ mScrollGeneration == aOther.mScrollGeneration &&
+ mBoundingCompositionSize == aOther.mBoundingCompositionSize &&
+ mPresShellId == aOther.mPresShellId &&
+ mLayoutViewport.IsEqualEdges(aOther.mLayoutViewport) &&
+ mTransformToAncestorScale == aOther.mTransformToAncestorScale &&
+ mPaintRequestTime == aOther.mPaintRequestTime &&
+ mVisualDestination == aOther.mVisualDestination &&
+ mVisualScrollUpdateType == aOther.mVisualScrollUpdateType &&
+ mIsRootContent == aOther.mIsRootContent &&
+ mIsScrollInfoLayer == aOther.mIsScrollInfoLayer &&
+ mHasNonZeroDisplayPortMargins ==
+ aOther.mHasNonZeroDisplayPortMargins &&
+ mMinimalDisplayPort == aOther.mMinimalDisplayPort &&
+ mFixedLayerMargins == aOther.mFixedLayerMargins &&
+ mCompositionSizeWithoutDynamicToolbar ==
+ aOther.mCompositionSizeWithoutDynamicToolbar;
+ }
+
+ bool operator!=(const FrameMetrics& aOther) const {
+ return !operator==(aOther);
+ }
+
+ bool IsScrollable() const {
+ return mScrollId != ScrollableLayerGuid::NULL_SCROLL_ID;
+ }
+
+ CSSToScreenScale2D DisplayportPixelsPerCSSPixel() const {
+ // Note: mZoom includes the async zoom. We want to include the async zoom
+ // even though the size of the pixels of our *current* displayport does not
+ // yet reflect it, because this function is used in the context of a repaint
+ // request where we'll be asking for a *new* displayport which does reflect
+ // the async zoom. Note 2: we include the transform to ancestor scale
+ // because this function (as the name implies) is used only in various
+ // displayport calculation related places, and those calculations want the
+ // transform to ancestor scale to be included becaese they want to reason
+ // about pixels which are the same size as screen pixels (so displayport
+ // sizes are e.g. limited to a multiple of the screen size). Whereas mZoom
+ // and mCumulativeResolution do not include it because of expectations of
+ // the code where they are used.
+ return mZoom * mTransformToAncestorScale;
+ }
+
+ CSSToLayerScale LayersPixelsPerCSSPixel() const {
+ return mDevPixelsPerCSSPixel * mCumulativeResolution;
+ }
+
+ // Get the amount by which this frame has been zoomed since the last repaint.
+ LayerToParentLayerScale GetAsyncZoom() const {
+ return mZoom / LayersPixelsPerCSSPixel();
+ }
+
+ // Ensure the scrollableRect is at least as big as the compositionBounds
+ // because the scrollableRect can be smaller if the content is not large
+ // and the scrollableRect hasn't been updated yet.
+ // We move the scrollableRect up because we don't know if we can move it
+ // down. i.e. we know that scrollableRect can go back as far as zero.
+ // but we don't know how much further ahead it can go.
+ CSSRect GetExpandedScrollableRect() const {
+ CSSRect scrollableRect = mScrollableRect;
+ CSSSize compSize = CalculateCompositedSizeInCssPixels();
+ if (scrollableRect.Width() < compSize.width) {
+ scrollableRect.SetRectX(
+ std::max(0.f, scrollableRect.X() -
+ (compSize.width - scrollableRect.Width())),
+ compSize.width);
+ }
+
+ if (scrollableRect.Height() < compSize.height) {
+ scrollableRect.SetRectY(
+ std::max(0.f, scrollableRect.Y() -
+ (compSize.height - scrollableRect.Height())),
+ compSize.height);
+ }
+
+ return scrollableRect;
+ }
+
+ CSSSize CalculateCompositedSizeInCssPixels() const {
+ return CalculateCompositedSizeInCssPixels(mCompositionBounds, mZoom);
+ }
+
+ /*
+ * Calculate the composition bounds of this frame in the CSS pixels of
+ * the content surrounding the scroll frame (OuterCSS pixels).
+ * Note that it does not make sense to ask for the composition bounds in the
+ * CSS pixels of the scrolled content (that is, regular CSS pixels),
+ * because the origin of the composition bounds is not meaningful in that
+ * coordinate space. (The size is, use CalculateCompositedSizeInCssPixels()
+ * for that.)
+ */
+ OuterCSSRect CalculateCompositionBoundsInOuterCssPixels() const {
+ if (GetZoom() == CSSToParentLayerScale(0)) {
+ return OuterCSSRect(); // avoid division by zero
+ }
+ // The CSS pixels of the scrolled content and the CSS pixels of the
+ // surrounding content only differ if the scrolled content is rendered
+ // at a higher resolution, and the difference is the resolution.
+ return mCompositionBounds / GetZoom() * GetCSSToOuterCSSScale();
+ }
+
+ CSSSize CalculateBoundedCompositedSizeInCssPixels() const {
+ CSSSize size = CalculateCompositedSizeInCssPixels();
+ size.width = std::min(size.width, mBoundingCompositionSize.width);
+ size.height = std::min(size.height, mBoundingCompositionSize.height);
+ return size;
+ }
+
+ CSSRect CalculateScrollRange() const {
+ return CalculateScrollRange(mScrollableRect, mCompositionBounds, mZoom);
+ }
+
+ void ScrollBy(const CSSPoint& aPoint) {
+ SetVisualScrollOffset(GetVisualScrollOffset() + aPoint);
+ }
+
+ void ZoomBy(float aScale) { mZoom.scale *= aScale; }
+
+ /*
+ * Compares an APZ frame metrics with an incoming content frame metrics
+ * to see if APZ has a scroll offset that has not been incorporated into
+ * the content frame metrics.
+ */
+ bool HasPendingScroll(const FrameMetrics& aContentFrameMetrics) const {
+ return GetVisualScrollOffset() !=
+ aContentFrameMetrics.GetVisualScrollOffset();
+ }
+
+ /*
+ * Returns true if the layout scroll offset or visual scroll offset changed.
+ */
+ bool ApplyScrollUpdateFrom(const ScrollPositionUpdate& aUpdate);
+
+ /**
+ * Applies the relative scroll offset update contained in aOther to the
+ * scroll offset contained in this. The scroll delta is clamped to the
+ * scrollable region.
+ *
+ * @returns The clamped scroll offset delta that was applied
+ */
+ CSSPoint ApplyRelativeScrollUpdateFrom(const ScrollPositionUpdate& aUpdate);
+
+ CSSPoint ApplyPureRelativeScrollUpdateFrom(
+ const ScrollPositionUpdate& aUpdate);
+
+ void UpdatePendingScrollInfo(const ScrollPositionUpdate& aInfo);
+
+ public:
+ void SetPresShellResolution(float aPresShellResolution) {
+ mPresShellResolution = aPresShellResolution;
+ }
+
+ float GetPresShellResolution() const { return mPresShellResolution; }
+
+ void SetCompositionBounds(const ParentLayerRect& aCompositionBounds) {
+ mCompositionBounds = aCompositionBounds;
+ }
+
+ const ParentLayerRect& GetCompositionBounds() const {
+ return mCompositionBounds;
+ }
+
+ void SetCompositionBoundsWidthIgnoringScrollbars(
+ const ParentLayerCoord aCompositionBoundsWidthIgnoringScrollbars) {
+ mCompositionBoundsWidthIgnoringScrollbars =
+ aCompositionBoundsWidthIgnoringScrollbars;
+ }
+
+ const ParentLayerCoord GetCompositionBoundsWidthIgnoringScrollbars() const {
+ return mCompositionBoundsWidthIgnoringScrollbars;
+ }
+
+ void SetDisplayPort(const CSSRect& aDisplayPort) {
+ mDisplayPort = aDisplayPort;
+ }
+
+ const CSSRect& GetDisplayPort() const { return mDisplayPort; }
+
+ void SetCumulativeResolution(
+ const LayoutDeviceToLayerScale& aCumulativeResolution) {
+ mCumulativeResolution = aCumulativeResolution;
+ }
+
+ const LayoutDeviceToLayerScale& GetCumulativeResolution() const {
+ return mCumulativeResolution;
+ }
+
+ void SetDevPixelsPerCSSPixel(
+ const CSSToLayoutDeviceScale& aDevPixelsPerCSSPixel) {
+ mDevPixelsPerCSSPixel = aDevPixelsPerCSSPixel;
+ }
+
+ const CSSToLayoutDeviceScale& GetDevPixelsPerCSSPixel() const {
+ return mDevPixelsPerCSSPixel;
+ }
+
+ CSSToOuterCSSScale GetCSSToOuterCSSScale() const {
+ // The scale difference between CSS and OuterCSS pixels is the
+ // part of the zoom that's not subject to all enclosing content,
+ // i.e. the pres shell resolution.
+ return CSSToOuterCSSScale(mPresShellResolution);
+ }
+
+ void SetIsRootContent(bool aIsRootContent) {
+ mIsRootContent = aIsRootContent;
+ }
+
+ bool IsRootContent() const { return mIsRootContent; }
+
+ // Set scroll offset, first clamping to the scroll range.
+ // Return true if it changed.
+ bool ClampAndSetVisualScrollOffset(const CSSPoint& aScrollOffset) {
+ CSSPoint offsetBefore = GetVisualScrollOffset();
+ SetVisualScrollOffset(CalculateScrollRange().ClampPoint(aScrollOffset));
+ return (offsetBefore != GetVisualScrollOffset());
+ }
+
+ CSSPoint GetLayoutScrollOffset() const { return mLayoutViewport.TopLeft(); }
+ // Returns true if it changed.
+ bool SetLayoutScrollOffset(const CSSPoint& aLayoutScrollOffset) {
+ CSSPoint offsetBefore = GetLayoutScrollOffset();
+ mLayoutViewport.MoveTo(aLayoutScrollOffset);
+ return (offsetBefore != GetLayoutScrollOffset());
+ }
+
+ const CSSPoint& GetVisualScrollOffset() const { return mScrollOffset; }
+ void SetVisualScrollOffset(const CSSPoint& aVisualScrollOffset) {
+ mScrollOffset = aVisualScrollOffset;
+ }
+
+ void SetZoom(const CSSToParentLayerScale& aZoom) { mZoom = aZoom; }
+
+ const CSSToParentLayerScale& GetZoom() const { return mZoom; }
+
+ void SetScrollGeneration(
+ const MainThreadScrollGeneration& aScrollGeneration) {
+ mScrollGeneration = aScrollGeneration;
+ }
+
+ MainThreadScrollGeneration GetScrollGeneration() const {
+ return mScrollGeneration;
+ }
+
+ ViewID GetScrollId() const { return mScrollId; }
+
+ void SetScrollId(ViewID scrollId) { mScrollId = scrollId; }
+
+ void SetBoundingCompositionSize(const CSSSize& aBoundingCompositionSize) {
+ mBoundingCompositionSize = aBoundingCompositionSize;
+ }
+
+ const CSSSize& GetBoundingCompositionSize() const {
+ return mBoundingCompositionSize;
+ }
+
+ uint32_t GetPresShellId() const { return mPresShellId; }
+
+ void SetPresShellId(uint32_t aPresShellId) { mPresShellId = aPresShellId; }
+
+ void SetLayoutViewport(const CSSRect& aLayoutViewport) {
+ mLayoutViewport = aLayoutViewport;
+ }
+
+ const CSSRect& GetLayoutViewport() const { return mLayoutViewport; }
+
+ CSSRect GetVisualViewport() const {
+ return CSSRect(GetVisualScrollOffset(),
+ CalculateCompositedSizeInCssPixels());
+ }
+
+ void SetTransformToAncestorScale(
+ const ParentLayerToScreenScale2D& aTransformToAncestorScale) {
+ mTransformToAncestorScale = aTransformToAncestorScale;
+ }
+
+ const ParentLayerToScreenScale2D& GetTransformToAncestorScale() const {
+ return mTransformToAncestorScale;
+ }
+
+ const CSSRect& GetScrollableRect() const { return mScrollableRect; }
+
+ void SetScrollableRect(const CSSRect& aScrollableRect) {
+ mScrollableRect = aScrollableRect;
+ }
+
+ // If the frame is in vertical-RTL writing mode(E.g. "writing-mode:
+ // vertical-rl" in CSS), or if it's in horizontal-RTL writing-mode(E.g.
+ // "writing-mode: horizontal-tb; direction: rtl;" in CSS), then this function
+ // returns true. From the representation perspective, frames whose horizontal
+ // contents start at rightside also cause their horizontal scrollbars, if any,
+ // initially start at rightside. So we can also learn about the initial side
+ // of the horizontal scrollbar for the frame by calling this function.
+ bool IsHorizontalContentRightToLeft() const { return mScrollableRect.x < 0; }
+
+ void SetPaintRequestTime(const TimeStamp& aTime) {
+ mPaintRequestTime = aTime;
+ }
+ const TimeStamp& GetPaintRequestTime() const { return mPaintRequestTime; }
+
+ void SetIsScrollInfoLayer(bool aIsScrollInfoLayer) {
+ mIsScrollInfoLayer = aIsScrollInfoLayer;
+ }
+ bool IsScrollInfoLayer() const { return mIsScrollInfoLayer; }
+
+ void SetHasNonZeroDisplayPortMargins(bool aHasNonZeroDisplayPortMargins) {
+ mHasNonZeroDisplayPortMargins = aHasNonZeroDisplayPortMargins;
+ }
+ bool HasNonZeroDisplayPortMargins() const {
+ return mHasNonZeroDisplayPortMargins;
+ }
+
+ void SetMinimalDisplayPort(bool aMinimalDisplayPort) {
+ mMinimalDisplayPort = aMinimalDisplayPort;
+ }
+ bool IsMinimalDisplayPort() const { return mMinimalDisplayPort; }
+
+ void SetVisualDestination(const CSSPoint& aVisualDestination) {
+ mVisualDestination = aVisualDestination;
+ }
+ const CSSPoint& GetVisualDestination() const { return mVisualDestination; }
+
+ void SetVisualScrollUpdateType(ScrollOffsetUpdateType aUpdateType) {
+ mVisualScrollUpdateType = aUpdateType;
+ }
+ ScrollOffsetUpdateType GetVisualScrollUpdateType() const {
+ return mVisualScrollUpdateType;
+ }
+
+ // Determine if the visual viewport is outside of the layout viewport and
+ // adjust the x,y-offset in mLayoutViewport accordingly. This is necessary to
+ // allow APZ to async-scroll the layout viewport.
+ //
+ // This is a no-op if mIsRootContent is false.
+ void RecalculateLayoutViewportOffset();
+
+ void SetFixedLayerMargins(const ScreenMargin& aFixedLayerMargins) {
+ mFixedLayerMargins = aFixedLayerMargins;
+ }
+ const ScreenMargin& GetFixedLayerMargins() const {
+ return mFixedLayerMargins;
+ }
+
+ void SetCompositionSizeWithoutDynamicToolbar(const ParentLayerSize& aSize) {
+ MOZ_ASSERT(mIsRootContent);
+ mCompositionSizeWithoutDynamicToolbar = aSize;
+ }
+ const ParentLayerSize& GetCompositionSizeWithoutDynamicToolbar() const {
+ MOZ_ASSERT(mIsRootContent);
+ return mCompositionSizeWithoutDynamicToolbar;
+ }
+
+ // Helper function for RecalculateViewportOffset(). Exposed so that
+ // APZC can perform the operation on other copies of the layout
+ // and visual viewport rects (e.g. the "effective" ones used to implement
+ // the frame delay).
+ // Modifies |aLayoutViewport| to continue enclosing |aVisualViewport|
+ // if possible.
+ // The layout viewport needs to remain clamped to the scrollable rect,
+ // and we pass in the scrollable rect so this function can maintain that
+ // constraint.
+ static void KeepLayoutViewportEnclosingVisualViewport(
+ const CSSRect& aVisualViewport, const CSSRect& aScrollableRect,
+ CSSRect& aLayoutViewport);
+
+ // Helper functions exposed so we can perform operations on copies outside of
+ // frame metrics object.
+ static CSSRect CalculateScrollRange(const CSSRect& aScrollableRect,
+ const ParentLayerRect& aCompositionBounds,
+ const CSSToParentLayerScale& aZoom);
+ static CSSSize CalculateCompositedSizeInCssPixels(
+ const ParentLayerRect& aCompositionBounds,
+ const CSSToParentLayerScale& aZoom);
+
+ private:
+ // A ID assigned to each scrollable frame, unique within each LayersId..
+ ViewID mScrollId;
+
+ // The pres-shell resolution that has been induced on the document containing
+ // this scroll frame as a result of zooming this scroll frame (whether via
+ // user action, or choosing an initial zoom level on page load). This can
+ // only be different from 1.0 for frames that are zoomable, which currently
+ // is just the root content document's root scroll frame
+ // (mIsRootContent = true).
+ // This is a plain float rather than a ScaleFactor because in and of itself
+ // it does not convert between any coordinate spaces for which we have names.
+ float mPresShellResolution;
+
+ // This is the area within the widget that we're compositing to. It is in the
+ // layer coordinates of the scrollable content's parent layer.
+ //
+ // The size of the composition bounds corresponds to the size of the scroll
+ // frame's scroll port (but in a coordinate system where the size does not
+ // change during zooming).
+ //
+ // The origin of the composition bounds is relative to the scroll node origin.
+ // (The "scroll node origin" is the point such that applying the APZC's
+ // apzc-to-screen transform to it takes you to the window origin, which is
+ // what Screen event coordinates are relative to. In layout terms, it's
+ // the origin of the reference frame passed to ComputeScrollMetadata().)
+ // Unlike the scroll port's origin, it does not change during scrolling of
+ // the scrollable layer to which it is associated. However, it may change due
+ // to scrolling of ancestor layers.
+ //
+ // This value is provided by Gecko at layout/paint time.
+ ParentLayerRect mCompositionBounds;
+
+ // For RCD-RSF this is the width of the composition bounds ignoring
+ // scrollbars. For everything else this will be the same as the width of the
+ // composition bounds. Only needed for the "resolution changed" check in
+ // NotifyLayersUpdated, once that switches to using IsResolutionUpdated we can
+ // remove this.
+ ParentLayerCoord mCompositionBoundsWidthIgnoringScrollbars;
+
+ // The area of a scroll frame's contents that has been painted, relative to
+ // GetLayoutScrollOffset().
+ //
+ // Should not be larger than GetExpandedScrollableRect().
+ //
+ // To pre-render a margin of 100 CSS pixels around the scroll port,
+ // { x = -100, y = - 100,
+ // width = scrollPort.width + 200, height = scrollPort.height + 200 }
+ // where scrollPort = CalculateCompositedSizeInCssPixels().
+ CSSRect mDisplayPort;
+
+ // The scrollable bounds of a frame. This is determined by reflow.
+ // Ordinarily the x and y will be 0 and the width and height will be the
+ // size of the element being scrolled. However for RTL pages or elements
+ // the x value may be negative.
+ //
+ // For scrollable frames that are overflow:hidden the x and y are usually
+ // set to the value of the current scroll offset, and the width and height
+ // will match the composition bounds width and height. In effect this reduces
+ // the scrollable range to 0.
+ //
+ // This is in the same coordinate space as |mScrollOffset|, but a different
+ // coordinate space than |mDisplayPort|. Note also that this coordinate
+ // system is understood by window.scrollTo().
+ CSSRect mScrollableRect;
+
+ // The cumulative resolution of the current frame. This is the product of the
+ // pres-shell resolutions of the document containing this scroll frame and its
+ // in-process ancestors. This information is provided by Gecko at layout/paint
+ // time. Notably, for out of process iframes cumulative resolution will be 1.
+ // The reason for this is that AsyncPanZoomController::GetTransformToThis does
+ // not contain the resolution in the process of the root content document, but
+ // in oop iframes AsyncPanZoomController::GetTransformToThis does contain the
+ // resolution. This makes coordinate math work out in APZ code because in the
+ // old layers backend GetTransformToThis was a transform of rendered pixels,
+ // and the pixels were rendered with the scale applied already. The reason
+ // that AsyncPanZoomController::GetTransformToThis contains the scale in oop
+ // iframes is because we include the resolution in the transform that includes
+ // the iframe via this call
+ // https://searchfox.org/mozilla-central/rev/2eebd6e256fa0355e08421265e57ee1307836d92/layout/generic/nsSubDocumentFrame.cpp#1404
+ // So when coordinates are passed to the process of the oop iframe they have
+ // the resolution removed by unapplying that transform which includes the
+ // resolution.
+ LayoutDeviceToLayerScale mCumulativeResolution;
+
+ // The conversion factor between CSS pixels and device pixels for this frame.
+ // This can vary based on a variety of things, such as reflowing-zoom.
+ CSSToLayoutDeviceScale mDevPixelsPerCSSPixel;
+
+ // The position of the top-left of the scroll frame's scroll port, relative
+ // to the scrollable content's origin.
+ //
+ // This is in the same coordinate space as |mScrollableRect|, but a different
+ // coordinate space than |mDisplayPort|.
+ //
+ // It is required that the rect:
+ // { x = mScrollOffset.x, y = mScrollOffset.y,
+ // width = scrollPort.width,
+ // height = scrollPort.height }
+ // (where scrollPort = CalculateCompositedSizeInCssPixels())
+ // be within |mScrollableRect|.
+ CSSPoint mScrollOffset;
+
+ // The "user zoom". Content is painted by gecko at mCumulativeResolution *
+ // mDevPixelsPerCSSPixel, but will be drawn to the screen at mZoom. In the
+ // steady state, the two will be the same, but during an async zoom action the
+ // two may diverge. This information is initialized in Gecko but updated in
+ // the APZC.
+ CSSToParentLayerScale mZoom;
+
+ // The scroll generation counter used to acknowledge the scroll offset update.
+ MainThreadScrollGeneration mScrollGeneration;
+
+ // A bounding size for our composition bounds (no larger than the
+ // cross-process RCD-RSF's composition size), in local CSS pixels.
+ CSSSize mBoundingCompositionSize;
+
+ uint32_t mPresShellId;
+
+ // For a root scroll frame (RSF), the document's layout viewport
+ // (sometimes called "CSS viewport" in older code).
+ //
+ // Its size is the dimensions we're using to constrain the <html> element
+ // of the document (i.e. the initial containing block (ICB) size).
+ //
+ // Its origin is the RSF's layout scroll position, i.e. the scroll position
+ // exposed to web content via window.scrollX/Y.
+ //
+ // Note that only the root content document's RSF has a layout viewport
+ // that's distinct from the visual viewport. For an iframe RSF, the two
+ // are the same.
+ //
+ // For a scroll frame that is not an RSF, this metric is meaningless and
+ // invalid.
+ CSSRect mLayoutViewport;
+
+ // The scale induced by css transforms and presshell resolution in this
+ // process and any ancestor processes that encloses this scroll frame that is
+ // _not_ included in mCumulativeResolution. This means that in the process of
+ // the root content document this only includes css transform scale (which
+ // happens in that process, but we assume there can be no css transform scale
+ // above the root content document). In other processes it includes css
+ // transform scale and any resolution scale in the current process and all
+ // ancestor processes.
+ ParentLayerToScreenScale2D mTransformToAncestorScale;
+
+ // The time at which the APZC last requested a repaint for this scroll frame.
+ TimeStamp mPaintRequestTime;
+
+ // These fields are used when the main thread wants to set a visual viewport
+ // offset that's distinct from the layout viewport offset.
+ // In this case, mVisualScrollUpdateType is set to eMainThread, and
+ // mVisualDestination is set to desired visual destination (relative
+ // to the document, like mScrollOffset).
+ CSSPoint mVisualDestination;
+ ScrollOffsetUpdateType mVisualScrollUpdateType;
+
+ // 'fixed layer margins' on the main-thread. This is only used for the
+ // root-content scroll frame.
+ ScreenMargin mFixedLayerMargins;
+
+ // Similar to mCompositionBounds.Size() but not including the dynamic toolbar
+ // height.
+ // If we are not using a dynamic toolbar, this has the same value as
+ // mCompositionBounds.Size().
+ ParentLayerSize mCompositionSizeWithoutDynamicToolbar;
+
+ // Whether or not this is the root scroll frame for the root content document.
+ bool mIsRootContent : 1;
+
+ // True if this scroll frame is a scroll info layer. A scroll info layer is
+ // not layerized and its content cannot be truly async-scrolled, but its
+ // metrics are still sent to and updated by the compositor, with the updates
+ // being reflected on the next paint rather than the next composite.
+ bool mIsScrollInfoLayer : 1;
+
+ // Whether there are non-zero display port margins set on this element.
+ bool mHasNonZeroDisplayPortMargins : 1;
+
+ // Whether this scroll frame is using a minimal display port, which means that
+ // any set display port margins are ignored when calculating the display port
+ // and instead zero margins are used and further no tile or alignment
+ // boundaries are used that could potentially expand the size.
+ bool mMinimalDisplayPort : 1;
+
+ // WARNING!!!!
+ //
+ // When adding a new field:
+ //
+ // - First, consider whether the field can be added to ScrollMetadata
+ // instead. If so, prefer that.
+ //
+ // - Otherwise, the following places should be updated to include them
+ // (as needed):
+ // FrameMetrics::operator ==
+ // AsyncPanZoomController::NotifyLayersUpdated
+ // The ParamTraits specialization in LayersMessageUtils.h
+ //
+ // Please add new fields above this comment.
+};
+
+struct ScrollSnapInfo {
+ ScrollSnapInfo();
+
+ bool operator==(const ScrollSnapInfo& aOther) const {
+ return mScrollSnapStrictnessX == aOther.mScrollSnapStrictnessX &&
+ mScrollSnapStrictnessY == aOther.mScrollSnapStrictnessY &&
+ mSnapTargets == aOther.mSnapTargets &&
+ mXRangeWiderThanSnapport == aOther.mXRangeWiderThanSnapport &&
+ mYRangeWiderThanSnapport == aOther.mYRangeWiderThanSnapport &&
+ mSnapportSize == aOther.mSnapportSize;
+ }
+
+ bool HasScrollSnapping() const;
+ bool HasSnapPositions() const;
+
+ void InitializeScrollSnapStrictness(WritingMode aWritingMode,
+ const nsStyleDisplay* aDisplay);
+
+ // The scroll frame's scroll-snap-type.
+ StyleScrollSnapStrictness mScrollSnapStrictnessX;
+ StyleScrollSnapStrictness mScrollSnapStrictnessY;
+
+ struct SnapTarget {
+ // The scroll positions corresponding to scroll-snap-align values.
+ Maybe<nscoord> mSnapPositionX;
+ Maybe<nscoord> mSnapPositionY;
+
+ // https://drafts.csswg.org/css-scroll-snap/#scroll-snap-area
+ nsRect mSnapArea;
+
+ // https://drafts.csswg.org/css-scroll-snap/#propdef-scroll-snap-stop
+ StyleScrollSnapStop mScrollSnapStop;
+
+ // Use for tracking the last snapped target.
+ ScrollSnapTargetId mTargetId;
+
+ SnapTarget() = default;
+
+ SnapTarget(Maybe<nscoord>&& aSnapPositionX, Maybe<nscoord>&& aSnapPositionY,
+ nsRect&& aSnapArea, StyleScrollSnapStop aScrollSnapStop,
+ ScrollSnapTargetId aTargetId)
+ : mSnapPositionX(std::move(aSnapPositionX)),
+ mSnapPositionY(std::move(aSnapPositionY)),
+ mSnapArea(std::move(aSnapArea)),
+ mScrollSnapStop(aScrollSnapStop),
+ mTargetId(aTargetId) {}
+
+ bool operator==(const SnapTarget& aOther) const {
+ return mSnapPositionX == aOther.mSnapPositionX &&
+ mSnapPositionY == aOther.mSnapPositionY &&
+ mSnapArea == aOther.mSnapArea &&
+ mScrollSnapStop == aOther.mScrollSnapStop &&
+ mTargetId == aOther.mTargetId;
+ }
+ bool IsValidFor(const nsPoint& aDestination,
+ const nsSize aSnapportSize) const {
+ nsPoint snapPoint(mSnapPositionX ? *mSnapPositionX : aDestination.x,
+ mSnapPositionY ? *mSnapPositionY : aDestination.y);
+ nsRect snappedPort = nsRect(snapPoint, aSnapportSize);
+ // Ignore snap points if snapping to the point would leave the snap area
+ // outside of the snapport.
+ // https://drafts.csswg.org/css-scroll-snap-1/#snap-scope
+ return snappedPort.Intersects(mSnapArea);
+ }
+ };
+
+ CopyableTArray<SnapTarget> mSnapTargets;
+
+ struct ScrollSnapRange {
+ ScrollSnapRange() = default;
+
+ ScrollSnapRange(nscoord aStart, nscoord aEnd, ScrollSnapTargetId aTargetId)
+ : mStart(aStart), mEnd(aEnd), mTargetId(aTargetId) {}
+
+ nscoord mStart;
+ nscoord mEnd;
+ ScrollSnapTargetId mTargetId;
+
+ bool operator==(const ScrollSnapRange& aOther) const {
+ return mStart == aOther.mStart && mEnd == aOther.mEnd &&
+ mTargetId == aOther.mTargetId;
+ }
+
+ // Returns true if |aPoint| is a valid snap position in this range.
+ bool IsValid(nscoord aPoint, nscoord aSnapportSize) const {
+ MOZ_ASSERT(mEnd - mStart > aSnapportSize);
+ return mStart <= aPoint && aPoint <= mEnd - aSnapportSize;
+ }
+ };
+ // An array of the range that the target element is larger than the snapport
+ // on the axis.
+ // Snap positions in this range will be valid snap positions in the case where
+ // the distance between the closest snap position and the second closest snap
+ // position is still larger than the snapport size.
+ // See https://drafts.csswg.org/css-scroll-snap-1/#snap-overflow
+ //
+ // Note: This range contains scroll-margin values.
+ CopyableTArray<ScrollSnapRange> mXRangeWiderThanSnapport;
+ CopyableTArray<ScrollSnapRange> mYRangeWiderThanSnapport;
+
+ // Note: This snapport size has been already deflated by scroll-padding.
+ nsSize mSnapportSize;
+};
+
+// clang-format off
+MOZ_DEFINE_ENUM_CLASS_WITH_BASE(
+ OverscrollBehavior, uint8_t, (
+ Auto,
+ Contain,
+ None
+));
+// clang-format on
+
+std::ostream& operator<<(std::ostream& aStream,
+ const OverscrollBehavior& aBehavior);
+
+struct OverscrollBehaviorInfo {
+ OverscrollBehaviorInfo();
+
+ // Construct from StyleOverscrollBehavior values.
+ static OverscrollBehaviorInfo FromStyleConstants(
+ StyleOverscrollBehavior aBehaviorX, StyleOverscrollBehavior aBehaviorY);
+
+ bool operator==(const OverscrollBehaviorInfo& aOther) const;
+ friend std::ostream& operator<<(std::ostream& aStream,
+ const OverscrollBehaviorInfo& aInfo);
+
+ OverscrollBehavior mBehaviorX;
+ OverscrollBehavior mBehaviorY;
+};
+
+/**
+ * Metadata about a scroll frame that's sent to the compositor during a layers
+ * or WebRender transaction, and also stored by APZ between transactions.
+ * This includes the scroll frame's FrameMetrics, as well as other metadata.
+ * We don't put the other metadata into FrameMetrics to avoid FrameMetrics
+ * becoming too bloated (as a FrameMetrics is e.g. stored in memory shared
+ * with the content process).
+ */
+struct ScrollMetadata {
+ friend struct IPC::ParamTraits<mozilla::layers::ScrollMetadata>;
+ friend std::ostream& operator<<(std::ostream& aStream,
+ const ScrollMetadata& aMetadata);
+
+ typedef ScrollableLayerGuid::ViewID ViewID;
+
+ public:
+ static StaticAutoPtr<const ScrollMetadata>
+ sNullMetadata; // We sometimes need an empty metadata
+
+ ScrollMetadata()
+ : mMetrics(),
+ mSnapInfo(),
+ mScrollParentId(ScrollableLayerGuid::NULL_SCROLL_ID),
+ mContentDescription(),
+ mLineScrollAmount(0, 0),
+ mPageScrollAmount(0, 0),
+ mHasScrollgrab(false),
+ mIsLayersIdRoot(false),
+ mIsAutoDirRootContentRTL(false),
+ mForceDisableApz(false),
+ mResolutionUpdated(false),
+ mIsRDMTouchSimulationActive(false),
+ mDidContentGetPainted(true),
+ mForceMousewheelAutodir(false),
+ mForceMousewheelAutodirHonourRoot(false),
+ mIsPaginatedPresentation(false),
+ mOverscrollBehavior() {}
+
+ bool operator==(const ScrollMetadata& aOther) const {
+ return mMetrics == aOther.mMetrics && mSnapInfo == aOther.mSnapInfo &&
+ mScrollParentId == aOther.mScrollParentId &&
+ // don't compare mContentDescription
+ mLineScrollAmount == aOther.mLineScrollAmount &&
+ mPageScrollAmount == aOther.mPageScrollAmount &&
+ mHasScrollgrab == aOther.mHasScrollgrab &&
+ mIsLayersIdRoot == aOther.mIsLayersIdRoot &&
+ mIsAutoDirRootContentRTL == aOther.mIsAutoDirRootContentRTL &&
+ mForceDisableApz == aOther.mForceDisableApz &&
+ mResolutionUpdated == aOther.mResolutionUpdated &&
+ mIsRDMTouchSimulationActive == aOther.mIsRDMTouchSimulationActive &&
+ mDidContentGetPainted == aOther.mDidContentGetPainted &&
+ mForceMousewheelAutodir == aOther.mForceMousewheelAutodir &&
+ mForceMousewheelAutodirHonourRoot ==
+ aOther.mForceMousewheelAutodirHonourRoot &&
+ mIsPaginatedPresentation == aOther.mIsPaginatedPresentation &&
+ mDisregardedDirection == aOther.mDisregardedDirection &&
+ mOverscrollBehavior == aOther.mOverscrollBehavior &&
+ mScrollUpdates == aOther.mScrollUpdates;
+ }
+
+ bool operator!=(const ScrollMetadata& aOther) const {
+ return !operator==(aOther);
+ }
+
+ bool IsDefault() const {
+ ScrollMetadata def;
+
+ def.mMetrics.SetPresShellId(mMetrics.GetPresShellId());
+ return (def == *this);
+ }
+
+ FrameMetrics& GetMetrics() { return mMetrics; }
+ const FrameMetrics& GetMetrics() const { return mMetrics; }
+
+ void SetSnapInfo(ScrollSnapInfo&& aSnapInfo) {
+ mSnapInfo = std::move(aSnapInfo);
+ }
+ const ScrollSnapInfo& GetSnapInfo() const { return mSnapInfo; }
+
+ ViewID GetScrollParentId() const { return mScrollParentId; }
+
+ void SetScrollParentId(ViewID aParentId) { mScrollParentId = aParentId; }
+ const nsCString& GetContentDescription() const { return mContentDescription; }
+ void SetContentDescription(const nsCString& aContentDescription) {
+ mContentDescription = aContentDescription;
+ }
+ const LayoutDeviceIntSize& GetLineScrollAmount() const {
+ return mLineScrollAmount;
+ }
+ void SetLineScrollAmount(const LayoutDeviceIntSize& size) {
+ mLineScrollAmount = size;
+ }
+ const LayoutDeviceIntSize& GetPageScrollAmount() const {
+ return mPageScrollAmount;
+ }
+ void SetPageScrollAmount(const LayoutDeviceIntSize& size) {
+ mPageScrollAmount = size;
+ }
+ void SetHasScrollgrab(bool aHasScrollgrab) {
+ mHasScrollgrab = aHasScrollgrab;
+ }
+ bool GetHasScrollgrab() const { return mHasScrollgrab; }
+ void SetIsLayersIdRoot(bool aValue) { mIsLayersIdRoot = aValue; }
+ bool IsLayersIdRoot() const { return mIsLayersIdRoot; }
+ void SetIsAutoDirRootContentRTL(bool aValue) {
+ mIsAutoDirRootContentRTL = aValue;
+ }
+ bool IsAutoDirRootContentRTL() const { return mIsAutoDirRootContentRTL; }
+ void SetForceDisableApz(bool aForceDisable) {
+ mForceDisableApz = aForceDisable;
+ }
+ bool IsApzForceDisabled() const { return mForceDisableApz; }
+ void SetResolutionUpdated(bool aUpdated) { mResolutionUpdated = aUpdated; }
+ bool IsResolutionUpdated() const { return mResolutionUpdated; }
+
+ void SetIsRDMTouchSimulationActive(bool aValue) {
+ mIsRDMTouchSimulationActive = aValue;
+ }
+ bool GetIsRDMTouchSimulationActive() const {
+ return mIsRDMTouchSimulationActive;
+ }
+
+ void SetForceMousewheelAutodir(bool aValue) {
+ mForceMousewheelAutodir = aValue;
+ }
+ bool ForceMousewheelAutodir() const { return mForceMousewheelAutodir; }
+
+ void SetForceMousewheelAutodirHonourRoot(bool aValue) {
+ mForceMousewheelAutodirHonourRoot = aValue;
+ }
+ bool ForceMousewheelAutodirHonourRoot() const {
+ return mForceMousewheelAutodirHonourRoot;
+ }
+
+ void SetIsPaginatedPresentation(bool aValue) {
+ mIsPaginatedPresentation = aValue;
+ }
+ bool IsPaginatedPresentation() const { return mIsPaginatedPresentation; }
+
+ bool DidContentGetPainted() const { return mDidContentGetPainted; }
+
+ private:
+ // For use in IPC only
+ void SetDidContentGetPainted(bool aValue) { mDidContentGetPainted = aValue; }
+
+ public:
+ // For more details about the concept of a disregarded direction, refer to the
+ // code which defines mDisregardedDirection.
+ Maybe<ScrollDirection> GetDisregardedDirection() const {
+ return mDisregardedDirection;
+ }
+ void SetDisregardedDirection(const Maybe<ScrollDirection>& aValue) {
+ mDisregardedDirection = aValue;
+ }
+
+ void SetOverscrollBehavior(
+ const OverscrollBehaviorInfo& aOverscrollBehavior) {
+ mOverscrollBehavior = aOverscrollBehavior;
+ }
+ const OverscrollBehaviorInfo& GetOverscrollBehavior() const {
+ return mOverscrollBehavior;
+ }
+
+ void SetScrollUpdates(const nsTArray<ScrollPositionUpdate>& aUpdates) {
+ mScrollUpdates = aUpdates;
+ }
+
+ const nsTArray<ScrollPositionUpdate>& GetScrollUpdates() const {
+ return mScrollUpdates;
+ }
+
+ void UpdatePendingScrollInfo(nsTArray<ScrollPositionUpdate>&& aUpdates) {
+ MOZ_ASSERT(!aUpdates.IsEmpty());
+ mMetrics.UpdatePendingScrollInfo(aUpdates.LastElement());
+
+ mDidContentGetPainted = false;
+ mScrollUpdates.Clear();
+ mScrollUpdates.AppendElements(std::move(aUpdates));
+ }
+
+ private:
+ FrameMetrics mMetrics;
+
+ // Information used to determine where to snap to for a given scroll.
+ ScrollSnapInfo mSnapInfo;
+
+ // The ViewID of the scrollable frame to which overscroll should be handed
+ // off.
+ ViewID mScrollParentId;
+
+ // A description of the content element corresponding to this frame.
+ // This is empty unless this is a scrollable layer and the
+ // apz.printtree pref is turned on.
+ nsCString mContentDescription;
+
+ // The value of GetLineScrollAmount(), for scroll frames.
+ LayoutDeviceIntSize mLineScrollAmount;
+
+ // The value of GetPageScrollAmount(), for scroll frames.
+ LayoutDeviceIntSize mPageScrollAmount;
+
+ // Whether or not this frame is for an element marked 'scrollgrab'.
+ bool mHasScrollgrab : 1;
+
+ // Whether these framemetrics are for the root scroll frame (root element if
+ // we don't have a root scroll frame) for its layers id.
+ bool mIsLayersIdRoot : 1;
+
+ // The AutoDirRootContent is the <body> element in an HTML document, or the
+ // root scrollframe if there is no body. This member variable indicates
+ // whether this element's content in the horizontal direction starts from
+ // right to left (e.g. it's true either if "writing-mode: vertical-rl", or
+ // "writing-mode: horizontal-tb; direction: rtl" in CSS).
+ // When we do auto-dir scrolling (@see mozilla::WheelDeltaAdjustmentStrategy
+ // or refer to bug 1358017 for details), setting a pref can make the code use
+ // the writing mode of this root element instead of the target scrollframe,
+ // and so we need to know if the writing mode is RTL or not.
+ bool mIsAutoDirRootContentRTL : 1;
+
+ // Whether or not the compositor should actually do APZ-scrolling on this
+ // scrollframe.
+ bool mForceDisableApz : 1;
+
+ // Whether the pres shell resolution stored in mMetrics reflects a change
+ // originated by the main thread.
+ bool mResolutionUpdated : 1;
+
+ // Whether or not RDM and touch simulation are active for this document.
+ // It's important to note that if RDM is active then this field will be
+ // true for the content document but NOT the chrome document containing
+ // the browser UI and RDM controls.
+ bool mIsRDMTouchSimulationActive : 1;
+
+ // Whether this metadata is part of a transaction that also repainted the
+ // content (i.e. updated the displaylist or textures). This gets set to false
+ // for "paint-skip" transactions, where the main thread doesn't repaint but
+ // instead requests APZ to update the compositor scroll offset instead. APZ
+ // needs to be able to distinguish these paint-skip transactions so that it
+ // can use the correct transforms.
+ bool mDidContentGetPainted : 1;
+
+ // Whether privileged code has requested that autodir behaviour be
+ // enabled for the scroll frame.
+ bool mForceMousewheelAutodir : 1;
+ bool mForceMousewheelAutodirHonourRoot : 1;
+
+ // Whether this content is being displayed in a paginated fashion
+ // such as printing or print preview. In such cases, content that
+ // would normally only generate one display item may generated one
+ // display item per page, and the different instances may be subject
+ // to different transforms, which constrains the assumptions APZ can make.
+ bool mIsPaginatedPresentation : 1;
+
+ // The disregarded direction means the direction which is disregarded anyway,
+ // even if the scroll frame overflows in that direction and the direction is
+ // specified as scrollable. This could happen in some scenarios, for instance,
+ // a single-line text control frame should disregard wheel scroll in
+ // its block-flow direction even if it overflows in that direction.
+ Maybe<ScrollDirection> mDisregardedDirection;
+
+ // The overscroll behavior for this scroll frame.
+ OverscrollBehaviorInfo mOverscrollBehavior;
+
+ // The ordered list of scroll position updates for this scroll frame since
+ // the last transaction.
+ CopyableTArray<ScrollPositionUpdate> mScrollUpdates;
+
+ // WARNING!!!!
+ //
+ // When adding new fields to ScrollMetadata, the following places should be
+ // updated to include them (as needed):
+ // 1. ScrollMetadata::operator ==
+ // 2. AsyncPanZoomController::NotifyLayersUpdated
+ // 3. The ParamTraits specialization in LayersMessageUtils.h
+ //
+ // Please add new fields above this comment.
+};
+
+typedef nsTHashMap<ScrollableLayerGuid::ViewIDHashKey,
+ nsTArray<ScrollPositionUpdate>>
+ ScrollUpdatesMap;
+
+} // namespace layers
+} // namespace mozilla
+
+#endif /* GFX_FRAMEMETRICS_H */
diff --git a/gfx/layers/GLImages.cpp b/gfx/layers/GLImages.cpp
new file mode 100644
index 0000000000..064d0e07f6
--- /dev/null
+++ b/gfx/layers/GLImages.cpp
@@ -0,0 +1,99 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "GLImages.h"
+#include "GLContext.h"
+#include "GLContextProvider.h"
+#include "ScopedGLHelpers.h"
+#include "GLImages.h"
+#include "GLBlitHelper.h"
+#include "GLReadTexImageHelper.h"
+#include "GLLibraryEGL.h"
+#include "mozilla/gfx/Logging.h"
+
+using namespace mozilla;
+using namespace mozilla::gl;
+
+namespace mozilla {
+namespace layers {
+
+static RefPtr<GLContext> sSnapshotContext;
+
+already_AddRefed<gfx::SourceSurface> GLImage::GetAsSourceSurface() {
+ MOZ_ASSERT(NS_IsMainThread(), "Should be on the main thread");
+
+ if (!sSnapshotContext) {
+ nsCString discardFailureId;
+ sSnapshotContext = GLContextProvider::CreateHeadless({}, &discardFailureId);
+ if (!sSnapshotContext) {
+ NS_WARNING("Failed to create snapshot GLContext");
+ return nullptr;
+ }
+ }
+
+ sSnapshotContext->MakeCurrent();
+ ScopedTexture scopedTex(sSnapshotContext);
+ ScopedBindTexture boundTex(sSnapshotContext, scopedTex.Texture());
+
+ gfx::IntSize size = GetSize();
+ sSnapshotContext->fTexImage2D(LOCAL_GL_TEXTURE_2D, 0, LOCAL_GL_RGBA,
+ size.width, size.height, 0, LOCAL_GL_RGBA,
+ LOCAL_GL_UNSIGNED_BYTE, nullptr);
+
+ ScopedFramebufferForTexture autoFBForTex(sSnapshotContext,
+ scopedTex.Texture());
+ if (!autoFBForTex.IsComplete()) {
+ gfxCriticalError()
+ << "GetAsSourceSurface: ScopedFramebufferForTexture failed.";
+ return nullptr;
+ }
+
+ const gl::OriginPos destOrigin = gl::OriginPos::TopLeft;
+ {
+ const ScopedBindFramebuffer bindFB(sSnapshotContext, autoFBForTex.FB());
+ if (!sSnapshotContext->BlitHelper()->BlitImageToFramebuffer(this, size,
+ destOrigin)) {
+ return nullptr;
+ }
+ }
+
+ RefPtr<gfx::DataSourceSurface> source =
+ gfx::Factory::CreateDataSourceSurface(size, gfx::SurfaceFormat::B8G8R8A8);
+ if (NS_WARN_IF(!source)) {
+ return nullptr;
+ }
+
+ ScopedBindFramebuffer bind(sSnapshotContext, autoFBForTex.FB());
+ ReadPixelsIntoDataSurface(sSnapshotContext, source);
+ return source.forget();
+}
+
+#ifdef MOZ_WIDGET_ANDROID
+SurfaceTextureImage::SurfaceTextureImage(
+ AndroidSurfaceTextureHandle aHandle, const gfx::IntSize& aSize,
+ bool aContinuous, gl::OriginPos aOriginPos, bool aHasAlpha,
+ Maybe<gfx::Matrix4x4> aTransformOverride)
+ : GLImage(ImageFormat::SURFACE_TEXTURE),
+ mHandle(aHandle),
+ mSize(aSize),
+ mContinuous(aContinuous),
+ mOriginPos(aOriginPos),
+ mHasAlpha(aHasAlpha),
+ mTransformOverride(aTransformOverride) {
+ MOZ_ASSERT(mHandle);
+}
+
+Maybe<SurfaceDescriptor> SurfaceTextureImage::GetDesc() {
+ SurfaceDescriptor sd = SurfaceTextureDescriptor(
+ mHandle, mSize,
+ mHasAlpha ? gfx::SurfaceFormat::R8G8B8A8 : gfx::SurfaceFormat::R8G8B8X8,
+ false /* NOT continuous */, mTransformOverride);
+ return Some(sd);
+}
+#endif
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/GLImages.h b/gfx/layers/GLImages.h
new file mode 100644
index 0000000000..3bab26216c
--- /dev/null
+++ b/gfx/layers/GLImages.h
@@ -0,0 +1,96 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GFX_GLIMAGES_H
+#define GFX_GLIMAGES_H
+
+#include "GLContextTypes.h"
+#include "GLTypes.h"
+#include "ImageContainer.h" // for Image
+#include "ImageTypes.h" // for ImageFormat::SHARED_GLTEXTURE
+#include "nsCOMPtr.h" // for already_AddRefed
+#include "mozilla/Maybe.h" // for Maybe
+#include "mozilla/gfx/Matrix.h" // for Matrix4x4
+#include "mozilla/gfx/Point.h" // for IntSize
+
+#ifdef MOZ_WIDGET_ANDROID
+# include "AndroidSurfaceTexture.h"
+#endif
+
+namespace mozilla {
+namespace layers {
+
+class GLImage : public Image {
+ public:
+ explicit GLImage(ImageFormat aFormat) : Image(nullptr, aFormat) {}
+
+ already_AddRefed<gfx::SourceSurface> GetAsSourceSurface() override;
+
+ GLImage* AsGLImage() override { return this; }
+};
+
+#ifdef MOZ_WIDGET_ANDROID
+
+class SurfaceTextureImage : public GLImage {
+ public:
+ class SetCurrentCallback {
+ public:
+ virtual void operator()(void) = 0;
+ virtual ~SetCurrentCallback() {}
+ };
+
+ SurfaceTextureImage(AndroidSurfaceTextureHandle aHandle,
+ const gfx::IntSize& aSize, bool aContinuous,
+ gl::OriginPos aOriginPos, bool aHasAlpha,
+ Maybe<gfx::Matrix4x4> aTransformOverride);
+
+ gfx::IntSize GetSize() const override { return mSize; }
+ AndroidSurfaceTextureHandle GetHandle() const { return mHandle; }
+ bool GetContinuous() const { return mContinuous; }
+ gl::OriginPos GetOriginPos() const { return mOriginPos; }
+ bool GetHasAlpha() const { return mHasAlpha; }
+ const Maybe<gfx::Matrix4x4>& GetTransformOverride() const {
+ return mTransformOverride;
+ }
+
+ already_AddRefed<gfx::SourceSurface> GetAsSourceSurface() override {
+ // We can implement this, but currently don't want to because it will cause
+ // the SurfaceTexture to be permanently bound to the snapshot readback
+ // context.
+ return nullptr;
+ }
+
+ SurfaceTextureImage* AsSurfaceTextureImage() override { return this; }
+
+ Maybe<SurfaceDescriptor> GetDesc() override;
+
+ void RegisterSetCurrentCallback(UniquePtr<SetCurrentCallback> aCallback) {
+ mSetCurrentCallback = std::move(aCallback);
+ }
+
+ void OnSetCurrent() {
+ if (mSetCurrentCallback) {
+ (*mSetCurrentCallback)();
+ mSetCurrentCallback.reset();
+ }
+ }
+
+ private:
+ AndroidSurfaceTextureHandle mHandle;
+ gfx::IntSize mSize;
+ bool mContinuous;
+ gl::OriginPos mOriginPos;
+ const bool mHasAlpha;
+ const Maybe<gfx::Matrix4x4> mTransformOverride;
+ UniquePtr<SetCurrentCallback> mSetCurrentCallback;
+};
+
+#endif // MOZ_WIDGET_ANDROID
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // GFX_GLIMAGES_H
diff --git a/gfx/layers/GPUVideoImage.h b/gfx/layers/GPUVideoImage.h
new file mode 100644
index 0000000000..228246e678
--- /dev/null
+++ b/gfx/layers/GPUVideoImage.h
@@ -0,0 +1,104 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GFX_GPU_VIDEO_IMAGE_H
+#define GFX_GPU_VIDEO_IMAGE_H
+
+#include "mozilla/RefPtr.h"
+#include "ImageContainer.h"
+#include "mozilla/layers/GPUVideoTextureClient.h"
+#include "mozilla/layers/CompositableClient.h"
+#include "mozilla/layers/ImageBridgeChild.h"
+
+namespace mozilla {
+namespace gl {
+class GLBlitHelper;
+}
+namespace layers {
+
+class IGPUVideoSurfaceManager {
+ protected:
+ virtual ~IGPUVideoSurfaceManager() = default;
+
+ public:
+ NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING
+
+ virtual already_AddRefed<gfx::SourceSurface> Readback(
+ const SurfaceDescriptorGPUVideo& aSD) = 0;
+ virtual void DeallocateSurfaceDescriptor(
+ const SurfaceDescriptorGPUVideo& aSD) = 0;
+};
+
+// Represents an animated Image that is known to the GPU process.
+class GPUVideoImage final : public Image {
+ friend class gl::GLBlitHelper;
+
+ public:
+ GPUVideoImage(IGPUVideoSurfaceManager* aManager,
+ const SurfaceDescriptorGPUVideo& aSD, const gfx::IntSize& aSize,
+ const gfx::ColorDepth& aColorDepth)
+ : Image(nullptr, ImageFormat::GPU_VIDEO),
+ mSize(aSize),
+ mColorDepth(aColorDepth) {
+ // Create the TextureClient immediately since the GPUVideoTextureData
+ // is responsible for deallocating the SurfaceDescriptor.
+ //
+ // Use the RECYCLE texture flag, since it's likely that our 'real'
+ // TextureData (in the decoder thread of the GPU process) is using
+ // it too, and we want to make sure we don't send the delete message
+ // until we've stopped being used on the compositor.
+ mTextureClient = TextureClient::CreateWithData(
+ new GPUVideoTextureData(aManager, aSD, aSize), TextureFlags::RECYCLE,
+ ImageBridgeChild::GetSingleton().get());
+ }
+
+ virtual ~GPUVideoImage() = default;
+
+ gfx::IntSize GetSize() const override { return mSize; }
+
+ gfx::ColorDepth GetColorDepth() const override { return mColorDepth; }
+
+ Maybe<SurfaceDescriptor> GetDesc() override {
+ return GetDescFromTexClient(mTextureClient);
+ }
+
+ private:
+ GPUVideoTextureData* GetData() const {
+ if (!mTextureClient) {
+ return nullptr;
+ }
+ TextureData* data = mTextureClient->GetInternalData();
+ if (!data) {
+ return nullptr;
+ }
+ return data->AsGPUVideoTextureData();
+ }
+
+ public:
+ already_AddRefed<gfx::SourceSurface> GetAsSourceSurface() override {
+ GPUVideoTextureData* data = GetData();
+ if (!data) {
+ return nullptr;
+ }
+ return data->GetAsSourceSurface();
+ }
+
+ TextureClient* GetTextureClient(KnowsCompositor* aKnowsCompositor) override {
+ MOZ_ASSERT(aKnowsCompositor == ImageBridgeChild::GetSingleton(),
+ "Must only use GPUVideo on ImageBridge");
+ return mTextureClient;
+ }
+
+ private:
+ gfx::IntSize mSize;
+ gfx::ColorDepth mColorDepth;
+ RefPtr<TextureClient> mTextureClient;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // GFX_GPU_VIDEO_IMAGE_H
diff --git a/gfx/layers/IMFYCbCrImage.cpp b/gfx/layers/IMFYCbCrImage.cpp
new file mode 100644
index 0000000000..0534b2126e
--- /dev/null
+++ b/gfx/layers/IMFYCbCrImage.cpp
@@ -0,0 +1,151 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "IMFYCbCrImage.h"
+#include "mozilla/gfx/DeviceManagerDx.h"
+#include "mozilla/gfx/gfxVars.h"
+#include "mozilla/gfx/Types.h"
+#include "mozilla/layers/TextureD3D11.h"
+#include "mozilla/layers/CompositableClient.h"
+#include "mozilla/layers/CompositableForwarder.h"
+#include "mozilla/layers/D3D11YCbCrImage.h"
+#include "mozilla/layers/TextureClient.h"
+#include "d3d9.h"
+
+namespace mozilla {
+namespace layers {
+
+IMFYCbCrImage::IMFYCbCrImage(IMFMediaBuffer* aBuffer, IMF2DBuffer* a2DBuffer,
+ KnowsCompositor* aKnowsCompositor,
+ ImageContainer* aContainer)
+ : RecyclingPlanarYCbCrImage(nullptr),
+ mBuffer(aBuffer),
+ m2DBuffer(a2DBuffer) {
+ mAllocator = aContainer->GetD3D11YCbCrRecycleAllocator(aKnowsCompositor);
+}
+
+IMFYCbCrImage::~IMFYCbCrImage() {
+ if (m2DBuffer) {
+ m2DBuffer->Unlock2D();
+ } else {
+ mBuffer->Unlock();
+ }
+}
+
+/* static */
+bool IMFYCbCrImage::CopyDataToTexture(const Data& aData, ID3D11Device* aDevice,
+ DXGIYCbCrTextureData* aTextureData) {
+ MOZ_ASSERT(aTextureData);
+
+ HRESULT hr;
+ RefPtr<ID3D10Multithread> mt;
+
+ hr = aDevice->QueryInterface((ID3D10Multithread**)getter_AddRefs(mt));
+ if (FAILED(hr)) {
+ return false;
+ }
+
+ if (!mt->GetMultithreadProtected()) {
+ return false;
+ }
+
+ if (!gfx::DeviceManagerDx::Get()->CanInitializeKeyedMutexTextures()) {
+ return false;
+ }
+
+ ID3D11Texture2D* textureY = aTextureData->GetD3D11Texture(0);
+ ID3D11Texture2D* textureCb = aTextureData->GetD3D11Texture(1);
+ ID3D11Texture2D* textureCr = aTextureData->GetD3D11Texture(2);
+
+ D3D11MTAutoEnter mtAutoEnter(mt.forget());
+
+ RefPtr<ID3D11DeviceContext> ctx;
+ aDevice->GetImmediateContext(getter_AddRefs(ctx));
+ if (!ctx) {
+ gfxCriticalError() << "Failed to get immediate context.";
+ return false;
+ }
+
+ // The documentation here seems to suggest using the immediate mode context
+ // on more than one thread is not allowed:
+ // https://msdn.microsoft.com/en-us/library/windows/desktop/ff476891(v=vs.85).aspx
+ // The Debug Layer seems to imply it is though. When the ID3D10Multithread
+ // layer is on. The Enter/Leave of the critical section shouldn't even be
+ // required but were added for extra security.
+
+ {
+ AutoLockD3D11Texture lockY(textureY);
+ AutoLockD3D11Texture lockCr(textureCr);
+ AutoLockD3D11Texture lockCb(textureCb);
+ D3D11MTAutoEnter mtAutoEnter(mt.forget());
+
+ D3D11_BOX box;
+ box.front = box.top = box.left = 0;
+ box.back = 1;
+ box.right = aData.YDataSize().width;
+ box.bottom = aData.YDataSize().height;
+ ctx->UpdateSubresource(textureY, 0, &box, aData.mYChannel, aData.mYStride,
+ 0);
+
+ box.right = aData.CbCrDataSize().width;
+ box.bottom = aData.CbCrDataSize().height;
+ ctx->UpdateSubresource(textureCb, 0, &box, aData.mCbChannel,
+ aData.mCbCrStride, 0);
+ ctx->UpdateSubresource(textureCr, 0, &box, aData.mCrChannel,
+ aData.mCbCrStride, 0);
+ }
+
+ return true;
+}
+
+TextureClient* IMFYCbCrImage::GetD3D11TextureClient(
+ KnowsCompositor* aKnowsCompositor) {
+ if (!mAllocator) {
+ return nullptr;
+ }
+
+ RefPtr<ID3D11Device> device = gfx::DeviceManagerDx::Get()->GetImageDevice();
+ if (!device) {
+ return nullptr;
+ }
+
+ {
+ DXGIYCbCrTextureAllocationHelper helper(mData, TextureFlags::DEFAULT,
+ device);
+ mTextureClient = mAllocator->CreateOrRecycle(helper);
+ }
+
+ if (!mTextureClient) {
+ return nullptr;
+ }
+
+ DXGIYCbCrTextureData* data =
+ mTextureClient->GetInternalData()->AsDXGIYCbCrTextureData();
+
+ if (!CopyDataToTexture(mData, device, data)) {
+ // Failed to copy data
+ mTextureClient = nullptr;
+ return nullptr;
+ }
+
+ return mTextureClient;
+}
+
+TextureClient* IMFYCbCrImage::GetTextureClient(
+ KnowsCompositor* aKnowsCompositor) {
+ if (mTextureClient) {
+ return mTextureClient;
+ }
+
+ RefPtr<ID3D11Device> device = gfx::DeviceManagerDx::Get()->GetImageDevice();
+ if (!device || !aKnowsCompositor->SupportsD3D11()) {
+ return nullptr;
+ }
+ return GetD3D11TextureClient(aKnowsCompositor);
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/IMFYCbCrImage.h b/gfx/layers/IMFYCbCrImage.h
new file mode 100644
index 0000000000..e1df0b2099
--- /dev/null
+++ b/gfx/layers/IMFYCbCrImage.h
@@ -0,0 +1,43 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GFX_IMFYCBCRIMAGE_H
+#define GFX_IMFYCBCRIMAGE_H
+
+#include "mozilla/layers/TextureD3D11.h"
+#include "mozilla/RefPtr.h"
+#include "ImageContainer.h"
+#include "mfidl.h"
+
+namespace mozilla {
+namespace layers {
+
+class IMFYCbCrImage : public RecyclingPlanarYCbCrImage {
+ public:
+ IMFYCbCrImage(IMFMediaBuffer* aBuffer, IMF2DBuffer* a2DBuffer,
+ KnowsCompositor* aKnowsCompositor, ImageContainer* aContainer);
+
+ bool IsValid() const override { return true; }
+
+ TextureClient* GetTextureClient(KnowsCompositor* aKnowsCompositor) override;
+
+ protected:
+ TextureClient* GetD3D11TextureClient(KnowsCompositor* aKnowsCompositor);
+ static bool CopyDataToTexture(const Data& aData, ID3D11Device* aDevice,
+ DXGIYCbCrTextureData* aTextureData);
+
+ virtual ~IMFYCbCrImage();
+
+ RefPtr<IMFMediaBuffer> mBuffer;
+ RefPtr<IMF2DBuffer> m2DBuffer;
+ RefPtr<D3D11YCbCrRecycleAllocator> mAllocator;
+ RefPtr<TextureClient> mTextureClient;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // GFX_D3DSURFACEIMAGE_H
diff --git a/gfx/layers/IPDLActor.h b/gfx/layers/IPDLActor.h
new file mode 100644
index 0000000000..b5eb04d0dc
--- /dev/null
+++ b/gfx/layers/IPDLActor.h
@@ -0,0 +1,59 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_LAYERS_IPDLACTOR_H
+#define MOZILLA_LAYERS_IPDLACTOR_H
+
+#include "mozilla/ipc/ProtocolUtils.h"
+#include "mozilla/layers/CompositableForwarder.h"
+#include "mozilla/Unused.h"
+#include "gfxPlatform.h"
+
+namespace mozilla {
+namespace layers {
+
+/// A base class to facilitate the deallocation of IPDL actors.
+///
+/// Implements the parent side of the simple deallocation handshake.
+/// Override the Destroy method rather than the ActorDestroy method.
+template <typename Protocol>
+class ParentActor : public Protocol {
+ public:
+ ParentActor() : mDestroyed(false) {}
+
+ ~ParentActor() { MOZ_ASSERT(mDestroyed); }
+
+ bool CanSend() const { return !mDestroyed; }
+
+ // Override this rather than ActorDestroy
+ virtual void Destroy() {}
+
+ mozilla::ipc::IPCResult RecvDestroy() final {
+ DestroyIfNeeded();
+ Unused << Protocol::Send__delete__(this);
+ return IPC_OK();
+ }
+
+ typedef ipc::IProtocol::ActorDestroyReason Why;
+
+ void ActorDestroy(Why) override { DestroyIfNeeded(); }
+
+ protected:
+ void DestroyIfNeeded() {
+ if (!mDestroyed) {
+ Destroy();
+ mDestroyed = true;
+ }
+ }
+
+ private:
+ bool mDestroyed;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif
diff --git a/gfx/layers/ImageContainer.cpp b/gfx/layers/ImageContainer.cpp
new file mode 100644
index 0000000000..964ffdba8b
--- /dev/null
+++ b/gfx/layers/ImageContainer.cpp
@@ -0,0 +1,1005 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ImageContainer.h"
+
+#include <string.h> // for memcpy, memset
+
+#include "GLImages.h" // for SurfaceTextureImage
+#include "YCbCrUtils.h" // for YCbCr conversions
+#include "gfx2DGlue.h"
+#include "gfxPlatform.h" // for gfxPlatform
+#include "gfxUtils.h" // for gfxUtils
+#include "libyuv.h"
+#include "mozilla/CheckedInt.h"
+#include "mozilla/ProfilerLabels.h"
+#include "mozilla/RefPtr.h" // for already_AddRefed
+#include "mozilla/StaticPrefs_layers.h"
+#include "mozilla/gfx/2D.h"
+#include "mozilla/gfx/gfxVars.h"
+#include "mozilla/ipc/CrossProcessMutex.h" // for CrossProcessMutex, etc
+#include "mozilla/layers/CompositorTypes.h"
+#include "mozilla/layers/ImageBridgeChild.h" // for ImageBridgeChild
+#include "mozilla/layers/ImageClient.h" // for ImageClient
+#include "mozilla/layers/ImageDataSerializer.h" // for SurfaceDescriptorBuffer
+#include "mozilla/layers/LayersMessages.h"
+#include "mozilla/layers/SharedPlanarYCbCrImage.h"
+#include "mozilla/layers/SharedRGBImage.h"
+#include "mozilla/layers/TextureClientRecycleAllocator.h"
+#include "nsProxyRelease.h"
+#include "nsISupportsUtils.h" // for NS_IF_ADDREF
+
+#ifdef XP_MACOSX
+# include "MacIOSurfaceImage.h"
+# include "mozilla/gfx/QuartzSupport.h"
+#endif
+
+#ifdef XP_WIN
+# include <d3d10_1.h>
+
+# include "gfxWindowsPlatform.h"
+# include "mozilla/gfx/DeviceManagerDx.h"
+# include "mozilla/layers/D3D11ShareHandleImage.h"
+# include "mozilla/layers/D3D11YCbCrImage.h"
+#endif
+
+namespace mozilla::layers {
+
+using namespace mozilla::gfx;
+using namespace mozilla::ipc;
+
+Atomic<int32_t> Image::sSerialCounter(0);
+
+Atomic<uint32_t> ImageContainer::sGenerationCounter(0);
+
+static void CopyPlane(uint8_t* aDst, const uint8_t* aSrc,
+ const gfx::IntSize& aSize, int32_t aStride,
+ int32_t aSkip);
+
+RefPtr<PlanarYCbCrImage> ImageFactory::CreatePlanarYCbCrImage(
+ const gfx::IntSize& aScaleHint, BufferRecycleBin* aRecycleBin) {
+ return new RecyclingPlanarYCbCrImage(aRecycleBin);
+}
+
+BufferRecycleBin::BufferRecycleBin()
+ : mLock("mozilla.layers.BufferRecycleBin.mLock")
+ // This member is only valid when the bin is not empty and will be
+ // properly initialized in RecycleBuffer, but initializing it here avoids
+ // static analysis noise.
+ ,
+ mRecycledBufferSize(0) {}
+
+void BufferRecycleBin::RecycleBuffer(UniquePtr<uint8_t[]> aBuffer,
+ uint32_t aSize) {
+ MutexAutoLock lock(mLock);
+
+ if (!mRecycledBuffers.IsEmpty() && aSize != mRecycledBufferSize) {
+ mRecycledBuffers.Clear();
+ }
+ mRecycledBufferSize = aSize;
+ mRecycledBuffers.AppendElement(std::move(aBuffer));
+}
+
+UniquePtr<uint8_t[]> BufferRecycleBin::GetBuffer(uint32_t aSize) {
+ MutexAutoLock lock(mLock);
+
+ if (mRecycledBuffers.IsEmpty() || mRecycledBufferSize != aSize) {
+ return UniquePtr<uint8_t[]>(new (fallible) uint8_t[aSize]);
+ }
+
+ return mRecycledBuffers.PopLastElement();
+}
+
+void BufferRecycleBin::ClearRecycledBuffers() {
+ MutexAutoLock lock(mLock);
+ if (!mRecycledBuffers.IsEmpty()) {
+ mRecycledBuffers.Clear();
+ }
+ mRecycledBufferSize = 0;
+}
+
+ImageContainerListener::ImageContainerListener(ImageContainer* aImageContainer)
+ : mLock("mozilla.layers.ImageContainerListener.mLock"),
+ mImageContainer(aImageContainer) {}
+
+ImageContainerListener::~ImageContainerListener() = default;
+
+void ImageContainerListener::NotifyComposite(
+ const ImageCompositeNotification& aNotification) {
+ MutexAutoLock lock(mLock);
+ if (mImageContainer) {
+ mImageContainer->NotifyComposite(aNotification);
+ }
+}
+
+void ImageContainerListener::NotifyDropped(uint32_t aDropped) {
+ MutexAutoLock lock(mLock);
+ if (mImageContainer) {
+ mImageContainer->NotifyDropped(aDropped);
+ }
+}
+
+void ImageContainerListener::ClearImageContainer() {
+ MutexAutoLock lock(mLock);
+ mImageContainer = nullptr;
+}
+
+void ImageContainerListener::DropImageClient() {
+ MutexAutoLock lock(mLock);
+ if (mImageContainer) {
+ mImageContainer->DropImageClient();
+ }
+}
+
+already_AddRefed<ImageClient> ImageContainer::GetImageClient() {
+ RecursiveMutexAutoLock mon(mRecursiveMutex);
+ EnsureImageClient();
+ RefPtr<ImageClient> imageClient = mImageClient;
+ return imageClient.forget();
+}
+
+void ImageContainer::DropImageClient() {
+ RecursiveMutexAutoLock mon(mRecursiveMutex);
+ if (mImageClient) {
+ mImageClient->ClearCachedResources();
+ mImageClient = nullptr;
+ }
+}
+
+void ImageContainer::EnsureImageClient() {
+ // If we're not forcing a new ImageClient, then we can skip this if we don't
+ // have an existing ImageClient, or if the existing one belongs to an IPC
+ // actor that is still open.
+ if (!mIsAsync) {
+ return;
+ }
+ if (mImageClient &&
+ mImageClient->GetForwarder()->GetLayersIPCActor()->IPCOpen()) {
+ return;
+ }
+
+ RefPtr<ImageBridgeChild> imageBridge = ImageBridgeChild::GetSingleton();
+ if (imageBridge) {
+ mImageClient =
+ imageBridge->CreateImageClient(CompositableType::IMAGE, this);
+ if (mImageClient) {
+ mAsyncContainerHandle = mImageClient->GetAsyncHandle();
+ } else {
+ // It's okay to drop the async container handle since the ImageBridgeChild
+ // is going to die anyway.
+ mAsyncContainerHandle = CompositableHandle();
+ }
+ }
+}
+
+ImageContainer::ImageContainer(Mode flag)
+ : mRecursiveMutex("ImageContainer.mRecursiveMutex"),
+ mGenerationCounter(++sGenerationCounter),
+ mPaintCount(0),
+ mDroppedImageCount(0),
+ mImageFactory(new ImageFactory()),
+ mRecycleBin(new BufferRecycleBin()),
+ mIsAsync(flag == ASYNCHRONOUS),
+ mCurrentProducerID(-1) {
+ if (flag == ASYNCHRONOUS) {
+ mNotifyCompositeListener = new ImageContainerListener(this);
+ EnsureImageClient();
+ }
+}
+
+ImageContainer::ImageContainer(const CompositableHandle& aHandle)
+ : mRecursiveMutex("ImageContainer.mRecursiveMutex"),
+ mGenerationCounter(++sGenerationCounter),
+ mPaintCount(0),
+ mDroppedImageCount(0),
+ mImageFactory(nullptr),
+ mRecycleBin(nullptr),
+ mIsAsync(true),
+ mAsyncContainerHandle(aHandle),
+ mCurrentProducerID(-1) {
+ MOZ_ASSERT(mAsyncContainerHandle);
+}
+
+ImageContainer::~ImageContainer() {
+ if (mNotifyCompositeListener) {
+ mNotifyCompositeListener->ClearImageContainer();
+ }
+ if (mAsyncContainerHandle) {
+ if (RefPtr<ImageBridgeChild> imageBridge =
+ ImageBridgeChild::GetSingleton()) {
+ imageBridge->ForgetImageContainer(mAsyncContainerHandle);
+ }
+ }
+}
+
+Maybe<SurfaceDescriptor> Image::GetDesc() { return GetDescFromTexClient(); }
+
+Maybe<SurfaceDescriptor> Image::GetDescFromTexClient(
+ TextureClient* const tcOverride) {
+ RefPtr<TextureClient> tc = tcOverride;
+ if (!tcOverride) {
+ tc = GetTextureClient(nullptr);
+ }
+ if (!tc) {
+ return {};
+ }
+
+ const auto& tcd = tc->GetInternalData();
+
+ SurfaceDescriptor ret;
+ if (!tcd->Serialize(ret)) {
+ return {};
+ }
+ return Some(ret);
+}
+
+already_AddRefed<ImageContainerListener>
+ImageContainer::GetImageContainerListener() const {
+ MOZ_ASSERT(InImageBridgeChildThread());
+ return do_AddRef(mNotifyCompositeListener);
+}
+
+RefPtr<PlanarYCbCrImage> ImageContainer::CreatePlanarYCbCrImage() {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ EnsureImageClient();
+ if (mImageClient && mImageClient->AsImageClientSingle()) {
+ return new SharedPlanarYCbCrImage(mImageClient);
+ }
+ if (mRecycleAllocator) {
+ return new SharedPlanarYCbCrImage(mRecycleAllocator);
+ }
+ return mImageFactory->CreatePlanarYCbCrImage(mScaleHint, mRecycleBin);
+}
+
+RefPtr<SharedRGBImage> ImageContainer::CreateSharedRGBImage() {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ EnsureImageClient();
+ if (mImageClient && mImageClient->AsImageClientSingle()) {
+ return new SharedRGBImage(mImageClient);
+ }
+ if (mRecycleAllocator) {
+ return new SharedRGBImage(mRecycleAllocator);
+ }
+ return nullptr;
+}
+
+void ImageContainer::SetCurrentImageInternal(
+ const nsTArray<NonOwningImage>& aImages) {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+
+ mGenerationCounter = ++sGenerationCounter;
+
+ if (!aImages.IsEmpty()) {
+ NS_ASSERTION(mCurrentImages.IsEmpty() ||
+ mCurrentImages[0].mProducerID != aImages[0].mProducerID ||
+ mCurrentImages[0].mFrameID <= aImages[0].mFrameID,
+ "frame IDs shouldn't go backwards");
+ if (aImages[0].mProducerID != mCurrentProducerID) {
+ mCurrentProducerID = aImages[0].mProducerID;
+ }
+ }
+
+ nsTArray<OwningImage> newImages;
+
+ for (uint32_t i = 0; i < aImages.Length(); ++i) {
+ NS_ASSERTION(aImages[i].mImage, "image can't be null");
+ NS_ASSERTION(!aImages[i].mTimeStamp.IsNull() || aImages.Length() == 1,
+ "Multiple images require timestamps");
+ if (i > 0) {
+ NS_ASSERTION(aImages[i].mTimeStamp >= aImages[i - 1].mTimeStamp,
+ "Timestamps must not decrease");
+ NS_ASSERTION(aImages[i].mFrameID > aImages[i - 1].mFrameID,
+ "FrameIDs must increase");
+ NS_ASSERTION(aImages[i].mProducerID == aImages[i - 1].mProducerID,
+ "ProducerIDs must be the same");
+ }
+ OwningImage* img = newImages.AppendElement();
+ img->mImage = aImages[i].mImage;
+ img->mTimeStamp = aImages[i].mTimeStamp;
+ img->mFrameID = aImages[i].mFrameID;
+ img->mProducerID = aImages[i].mProducerID;
+ for (const auto& oldImg : mCurrentImages) {
+ if (oldImg.mFrameID == img->mFrameID &&
+ oldImg.mProducerID == img->mProducerID) {
+ img->mComposited = oldImg.mComposited;
+ break;
+ }
+ }
+ }
+
+ mCurrentImages = std::move(newImages);
+}
+
+void ImageContainer::ClearImagesFromImageBridge() {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ SetCurrentImageInternal(nsTArray<NonOwningImage>());
+}
+
+void ImageContainer::SetCurrentImages(const nsTArray<NonOwningImage>& aImages) {
+ AUTO_PROFILER_LABEL("ImageContainer::SetCurrentImages", GRAPHICS);
+ MOZ_ASSERT(!aImages.IsEmpty());
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ if (mIsAsync) {
+ if (RefPtr<ImageBridgeChild> imageBridge =
+ ImageBridgeChild::GetSingleton()) {
+ imageBridge->UpdateImageClient(this);
+ }
+ }
+ SetCurrentImageInternal(aImages);
+}
+
+void ImageContainer::ClearAllImages() {
+ mRecursiveMutex.Lock();
+ if (mImageClient) {
+ RefPtr<ImageClient> imageClient = mImageClient;
+ mRecursiveMutex.Unlock();
+
+ // Let ImageClient release all TextureClients. This doesn't return
+ // until ImageBridge has called ClearCurrentImageFromImageBridge.
+ if (RefPtr<ImageBridgeChild> imageBridge =
+ ImageBridgeChild::GetSingleton()) {
+ imageBridge->FlushAllImages(imageClient, this);
+ }
+ return;
+ }
+
+ SetCurrentImageInternal(nsTArray<NonOwningImage>());
+ mRecursiveMutex.Unlock();
+}
+
+void ImageContainer::ClearCachedResources() {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ if (mImageClient && mImageClient->AsImageClientSingle()) {
+ if (!mImageClient->HasTextureClientRecycler()) {
+ return;
+ }
+ mImageClient->GetTextureClientRecycler()->ShrinkToMinimumSize();
+ return;
+ }
+ return mRecycleBin->ClearRecycledBuffers();
+}
+
+void ImageContainer::SetCurrentImageInTransaction(Image* aImage) {
+ AutoTArray<NonOwningImage, 1> images;
+ images.AppendElement(NonOwningImage(aImage));
+ SetCurrentImagesInTransaction(images);
+}
+
+void ImageContainer::SetCurrentImagesInTransaction(
+ const nsTArray<NonOwningImage>& aImages) {
+ NS_ASSERTION(NS_IsMainThread(), "Should be on main thread.");
+ NS_ASSERTION(!HasImageClient(),
+ "Should use async image transfer with ImageBridge.");
+
+ SetCurrentImageInternal(aImages);
+}
+
+bool ImageContainer::IsAsync() const { return mIsAsync; }
+
+CompositableHandle ImageContainer::GetAsyncContainerHandle() {
+ NS_ASSERTION(IsAsync(),
+ "Shared image ID is only relevant to async ImageContainers");
+ RecursiveMutexAutoLock mon(mRecursiveMutex);
+ NS_ASSERTION(mAsyncContainerHandle, "Should have a shared image ID");
+ EnsureImageClient();
+ return mAsyncContainerHandle;
+}
+
+bool ImageContainer::HasCurrentImage() {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+
+ return !mCurrentImages.IsEmpty();
+}
+
+void ImageContainer::GetCurrentImages(nsTArray<OwningImage>* aImages,
+ uint32_t* aGenerationCounter) {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+
+ *aImages = mCurrentImages.Clone();
+ if (aGenerationCounter) {
+ *aGenerationCounter = mGenerationCounter;
+ }
+}
+
+gfx::IntSize ImageContainer::GetCurrentSize() {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+
+ if (mCurrentImages.IsEmpty()) {
+ return gfx::IntSize(0, 0);
+ }
+
+ return mCurrentImages[0].mImage->GetSize();
+}
+
+void ImageContainer::NotifyComposite(
+ const ImageCompositeNotification& aNotification) {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+
+ // An image composition notification is sent the first time a particular
+ // image is composited by an ImageHost. Thus, every time we receive such
+ // a notification, a new image has been painted.
+ ++mPaintCount;
+
+ if (aNotification.producerID() == mCurrentProducerID) {
+ for (auto& img : mCurrentImages) {
+ if (img.mFrameID == aNotification.frameID()) {
+ img.mComposited = true;
+ }
+ }
+ }
+
+ if (!aNotification.imageTimeStamp().IsNull()) {
+ mPaintDelay = aNotification.firstCompositeTimeStamp() -
+ aNotification.imageTimeStamp();
+ }
+}
+
+void ImageContainer::NotifyDropped(uint32_t aDropped) {
+ mDroppedImageCount += aDropped;
+}
+
+void ImageContainer::EnsureRecycleAllocatorForRDD(
+ KnowsCompositor* aKnowsCompositor) {
+ MOZ_ASSERT(!mIsAsync);
+ MOZ_ASSERT(XRE_IsRDDProcess());
+
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ MOZ_ASSERT(!mImageClient);
+
+ if (mRecycleAllocator &&
+ aKnowsCompositor == mRecycleAllocator->GetKnowsCompositor()) {
+ return;
+ }
+
+ if (!StaticPrefs::layers_recycle_allocator_rdd_AtStartup()) {
+ return;
+ }
+
+ static const uint32_t MAX_POOLED_VIDEO_COUNT = 5;
+
+ mRecycleAllocator =
+ new layers::TextureClientRecycleAllocator(aKnowsCompositor);
+ mRecycleAllocator->SetMaxPoolSize(MAX_POOLED_VIDEO_COUNT);
+}
+
+#ifdef XP_WIN
+already_AddRefed<D3D11RecycleAllocator>
+ImageContainer::GetD3D11RecycleAllocator(KnowsCompositor* aKnowsCompositor,
+ gfx::SurfaceFormat aPreferredFormat) {
+ MOZ_ASSERT(aKnowsCompositor);
+
+ if (!aKnowsCompositor->SupportsD3D11()) {
+ return nullptr;
+ }
+
+ RefPtr<ID3D11Device> device = gfx::DeviceManagerDx::Get()->GetImageDevice();
+ if (!device) {
+ return nullptr;
+ }
+
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ if (mD3D11RecycleAllocator && mD3D11RecycleAllocator->mDevice == device &&
+ mD3D11RecycleAllocator->GetKnowsCompositor() == aKnowsCompositor) {
+ return do_AddRef(mD3D11RecycleAllocator);
+ }
+
+ mD3D11RecycleAllocator =
+ new D3D11RecycleAllocator(aKnowsCompositor, device, aPreferredFormat);
+
+ if (device != DeviceManagerDx::Get()->GetCompositorDevice()) {
+ RefPtr<SyncObjectClient> syncObject =
+ SyncObjectClient::CreateSyncObjectClient(
+ aKnowsCompositor->GetTextureFactoryIdentifier().mSyncHandle,
+ device);
+ mD3D11RecycleAllocator->SetSyncObject(syncObject);
+ }
+
+ return do_AddRef(mD3D11RecycleAllocator);
+}
+
+already_AddRefed<D3D11YCbCrRecycleAllocator>
+ImageContainer::GetD3D11YCbCrRecycleAllocator(
+ KnowsCompositor* aKnowsCompositor) {
+ if (!aKnowsCompositor->SupportsD3D11() ||
+ !gfx::DeviceManagerDx::Get()->GetImageDevice()) {
+ return nullptr;
+ }
+
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ if (mD3D11YCbCrRecycleAllocator &&
+ aKnowsCompositor == mD3D11YCbCrRecycleAllocator->GetKnowsCompositor()) {
+ return do_AddRef(mD3D11YCbCrRecycleAllocator);
+ }
+
+ mD3D11YCbCrRecycleAllocator =
+ new D3D11YCbCrRecycleAllocator(aKnowsCompositor);
+ return do_AddRef(mD3D11YCbCrRecycleAllocator);
+}
+#endif
+
+#ifdef XP_MACOSX
+already_AddRefed<MacIOSurfaceRecycleAllocator>
+ImageContainer::GetMacIOSurfaceRecycleAllocator() {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ if (!mMacIOSurfaceRecycleAllocator) {
+ mMacIOSurfaceRecycleAllocator = new MacIOSurfaceRecycleAllocator();
+ }
+
+ return do_AddRef(mMacIOSurfaceRecycleAllocator);
+}
+#endif
+
+// -
+// https://searchfox.org/mozilla-central/source/dom/media/ipc/RemoteImageHolder.cpp#46
+
+Maybe<PlanarYCbCrData> PlanarYCbCrData::From(
+ const SurfaceDescriptorBuffer& sdb) {
+ if (sdb.desc().type() != BufferDescriptor::TYCbCrDescriptor) {
+ return {};
+ }
+ const YCbCrDescriptor& yuvDesc = sdb.desc().get_YCbCrDescriptor();
+
+ Maybe<Range<uint8_t>> buffer;
+ const MemoryOrShmem& memOrShmem = sdb.data();
+ switch (memOrShmem.type()) {
+ case MemoryOrShmem::Tuintptr_t:
+ gfxCriticalError() << "PlanarYCbCrData::From SurfaceDescriptorBuffer "
+ "w/uintptr_t unsupported.";
+ break;
+ case MemoryOrShmem::TShmem:
+ buffer.emplace(memOrShmem.get_Shmem().Range<uint8_t>());
+ break;
+ default:
+ MOZ_ASSERT(false, "Unknown MemoryOrShmem type");
+ break;
+ }
+ if (!buffer) {
+ return {};
+ }
+
+ PlanarYCbCrData yuvData;
+ yuvData.mYStride = AssertedCast<int32_t>(yuvDesc.yStride());
+ yuvData.mCbCrStride = AssertedCast<int32_t>(yuvDesc.cbCrStride());
+ // default mYSkip, mCbSkip, mCrSkip because not held in YCbCrDescriptor
+ yuvData.mYSkip = yuvData.mCbSkip = yuvData.mCrSkip = 0;
+ yuvData.mPictureRect = yuvDesc.display();
+ yuvData.mStereoMode = yuvDesc.stereoMode();
+ yuvData.mColorDepth = yuvDesc.colorDepth();
+ yuvData.mYUVColorSpace = yuvDesc.yUVColorSpace();
+ yuvData.mColorRange = yuvDesc.colorRange();
+ yuvData.mChromaSubsampling = yuvDesc.chromaSubsampling();
+
+ const auto GetPlanePtr = [&](const uint32_t beginOffset,
+ const gfx::IntSize size,
+ const int32_t stride) -> uint8_t* {
+ if (size.width > stride) return nullptr;
+ auto bytesNeeded = CheckedInt<uintptr_t>(stride) *
+ size.height; // Don't accept `stride*(h-1)+w`.
+ bytesNeeded += beginOffset;
+ if (!bytesNeeded.isValid() || bytesNeeded.value() > buffer->length()) {
+ gfxCriticalError()
+ << "PlanarYCbCrData::From asked for out-of-bounds plane data.";
+ return nullptr;
+ }
+ return (buffer->begin() + beginOffset).get();
+ };
+ yuvData.mYChannel =
+ GetPlanePtr(yuvDesc.yOffset(), yuvDesc.ySize(), yuvData.mYStride);
+ yuvData.mCbChannel =
+ GetPlanePtr(yuvDesc.cbOffset(), yuvDesc.cbCrSize(), yuvData.mCbCrStride);
+ yuvData.mCrChannel =
+ GetPlanePtr(yuvDesc.crOffset(), yuvDesc.cbCrSize(), yuvData.mCbCrStride);
+
+ if (yuvData.mYSkip || yuvData.mCbSkip || yuvData.mCrSkip ||
+ yuvDesc.ySize().width < 0 || yuvDesc.ySize().height < 0 ||
+ yuvDesc.cbCrSize().width < 0 || yuvDesc.cbCrSize().height < 0 ||
+ yuvData.mYStride < 0 || yuvData.mCbCrStride < 0 || !yuvData.mYChannel ||
+ !yuvData.mCbChannel || !yuvData.mCrChannel) {
+ gfxCriticalError() << "Unusual PlanarYCbCrData: " << yuvData.mYSkip << ","
+ << yuvData.mCbSkip << "," << yuvData.mCrSkip << ", "
+ << yuvDesc.ySize().width << "," << yuvDesc.ySize().height
+ << ", " << yuvDesc.cbCrSize().width << ","
+ << yuvDesc.cbCrSize().height << ", " << yuvData.mYStride
+ << "," << yuvData.mCbCrStride << ", "
+ << yuvData.mYChannel << "," << yuvData.mCbChannel << ","
+ << yuvData.mCrChannel;
+ return {};
+ }
+
+ return Some(yuvData);
+}
+
+// -
+
+PlanarYCbCrImage::PlanarYCbCrImage()
+ : Image(nullptr, ImageFormat::PLANAR_YCBCR),
+ mOffscreenFormat(SurfaceFormat::UNKNOWN),
+ mBufferSize(0) {}
+
+nsresult PlanarYCbCrImage::BuildSurfaceDescriptorBuffer(
+ SurfaceDescriptorBuffer& aSdBuffer,
+ const std::function<MemoryOrShmem(uint32_t)>& aAllocate) {
+ const PlanarYCbCrData* pdata = GetData();
+ MOZ_ASSERT(pdata, "must have PlanarYCbCrData");
+ MOZ_ASSERT(pdata->mYSkip == 0 && pdata->mCbSkip == 0 && pdata->mCrSkip == 0,
+ "YCbCrDescriptor doesn't hold skip values");
+
+ auto ySize = pdata->YDataSize();
+ auto cbcrSize = pdata->CbCrDataSize();
+ uint32_t yOffset;
+ uint32_t cbOffset;
+ uint32_t crOffset;
+ ImageDataSerializer::ComputeYCbCrOffsets(pdata->mYStride, ySize.height,
+ pdata->mCbCrStride, cbcrSize.height,
+ yOffset, cbOffset, crOffset);
+
+ uint32_t bufferSize = ImageDataSerializer::ComputeYCbCrBufferSize(
+ ySize, pdata->mYStride, cbcrSize, pdata->mCbCrStride, yOffset, cbOffset,
+ crOffset);
+
+ aSdBuffer.data() = aAllocate(bufferSize);
+
+ uint8_t* buffer = nullptr;
+ const MemoryOrShmem& memOrShmem = aSdBuffer.data();
+ switch (memOrShmem.type()) {
+ case MemoryOrShmem::Tuintptr_t:
+ buffer = reinterpret_cast<uint8_t*>(memOrShmem.get_uintptr_t());
+ break;
+ case MemoryOrShmem::TShmem:
+ buffer = memOrShmem.get_Shmem().get<uint8_t>();
+ break;
+ default:
+ buffer = nullptr;
+ break;
+ }
+ if (!buffer) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ aSdBuffer.desc() = YCbCrDescriptor(
+ pdata->mPictureRect, ySize, pdata->mYStride, cbcrSize, pdata->mCbCrStride,
+ yOffset, cbOffset, crOffset, pdata->mStereoMode, pdata->mColorDepth,
+ pdata->mYUVColorSpace, pdata->mColorRange, pdata->mChromaSubsampling);
+
+ CopyPlane(buffer + yOffset, pdata->mYChannel, ySize, pdata->mYStride,
+ pdata->mYSkip);
+ CopyPlane(buffer + cbOffset, pdata->mCbChannel, cbcrSize, pdata->mCbCrStride,
+ pdata->mCbSkip);
+ CopyPlane(buffer + crOffset, pdata->mCrChannel, cbcrSize, pdata->mCbCrStride,
+ pdata->mCrSkip);
+ return NS_OK;
+}
+
+RecyclingPlanarYCbCrImage::~RecyclingPlanarYCbCrImage() {
+ if (mBuffer) {
+ mRecycleBin->RecycleBuffer(std::move(mBuffer), mBufferSize);
+ }
+}
+
+size_t RecyclingPlanarYCbCrImage::SizeOfExcludingThis(
+ MallocSizeOf aMallocSizeOf) const {
+ // Ignoring:
+ // - mData - just wraps mBuffer
+ // - Surfaces should be reported under gfx-surfaces-*:
+ // - mSourceSurface
+ // - Base class:
+ // - mImplData is not used
+ // Not owned:
+ // - mRecycleBin
+ size_t size = aMallocSizeOf(mBuffer.get());
+
+ // Could add in the future:
+ // - mBackendData (from base class)
+
+ return size;
+}
+
+UniquePtr<uint8_t[]> RecyclingPlanarYCbCrImage::AllocateBuffer(uint32_t aSize) {
+ return mRecycleBin->GetBuffer(aSize);
+}
+
+static void CopyPlane(uint8_t* aDst, const uint8_t* aSrc,
+ const gfx::IntSize& aSize, int32_t aStride,
+ int32_t aSkip) {
+ int32_t height = aSize.height;
+ int32_t width = aSize.width;
+
+ MOZ_RELEASE_ASSERT(width <= aStride);
+
+ if (!aSkip) {
+ // Fast path: planar input.
+ memcpy(aDst, aSrc, height * aStride);
+ } else {
+ for (int y = 0; y < height; ++y) {
+ const uint8_t* src = aSrc;
+ uint8_t* dst = aDst;
+ // Slow path
+ for (int x = 0; x < width; ++x) {
+ *dst++ = *src++;
+ src += aSkip;
+ }
+ aSrc += aStride;
+ aDst += aStride;
+ }
+ }
+}
+
+bool RecyclingPlanarYCbCrImage::CopyData(const Data& aData) {
+ // update buffer size
+ // Use uint32_t throughout to match AllocateBuffer's param and mBufferSize
+ auto ySize = aData.YDataSize();
+ auto cbcrSize = aData.CbCrDataSize();
+ const auto checkedSize =
+ CheckedInt<uint32_t>(aData.mCbCrStride) * cbcrSize.height * 2 +
+ CheckedInt<uint32_t>(aData.mYStride) * ySize.height *
+ (aData.mAlpha ? 2 : 1);
+
+ if (!checkedSize.isValid()) return false;
+
+ const auto size = checkedSize.value();
+
+ // get new buffer
+ mBuffer = AllocateBuffer(size);
+ if (!mBuffer) return false;
+
+ // update buffer size
+ mBufferSize = size;
+
+ mData = aData; // mAlpha will be set if aData has it
+ mData.mYChannel = mBuffer.get();
+ mData.mCbChannel = mData.mYChannel + mData.mYStride * ySize.height;
+ mData.mCrChannel = mData.mCbChannel + mData.mCbCrStride * cbcrSize.height;
+ mData.mYSkip = mData.mCbSkip = mData.mCrSkip = 0;
+
+ CopyPlane(mData.mYChannel, aData.mYChannel, ySize, aData.mYStride,
+ aData.mYSkip);
+ CopyPlane(mData.mCbChannel, aData.mCbChannel, cbcrSize, aData.mCbCrStride,
+ aData.mCbSkip);
+ CopyPlane(mData.mCrChannel, aData.mCrChannel, cbcrSize, aData.mCbCrStride,
+ aData.mCrSkip);
+ if (aData.mAlpha) {
+ CopyPlane(mData.mAlpha->mChannel, aData.mAlpha->mChannel, ySize,
+ aData.mYStride, aData.mYSkip);
+ }
+
+ mSize = aData.mPictureRect.Size();
+ mOrigin = aData.mPictureRect.TopLeft();
+ return true;
+}
+
+gfxImageFormat PlanarYCbCrImage::GetOffscreenFormat() const {
+ return mOffscreenFormat == SurfaceFormat::UNKNOWN ? gfxVars::OffscreenFormat()
+ : mOffscreenFormat;
+}
+
+bool PlanarYCbCrImage::AdoptData(const Data& aData) {
+ mData = aData;
+ mSize = aData.mPictureRect.Size();
+ mOrigin = aData.mPictureRect.TopLeft();
+ return true;
+}
+
+already_AddRefed<gfx::SourceSurface> PlanarYCbCrImage::GetAsSourceSurface() {
+ if (mSourceSurface) {
+ RefPtr<gfx::SourceSurface> surface(mSourceSurface);
+ return surface.forget();
+ }
+
+ gfx::IntSize size(mSize);
+ gfx::SurfaceFormat format =
+ gfx::ImageFormatToSurfaceFormat(GetOffscreenFormat());
+ gfx::GetYCbCrToRGBDestFormatAndSize(mData, format, size);
+ if (mSize.width > PlanarYCbCrImage::MAX_DIMENSION ||
+ mSize.height > PlanarYCbCrImage::MAX_DIMENSION) {
+ NS_ERROR("Illegal image dest width or height");
+ return nullptr;
+ }
+
+ RefPtr<gfx::DataSourceSurface> surface =
+ gfx::Factory::CreateDataSourceSurface(size, format);
+ if (NS_WARN_IF(!surface)) {
+ return nullptr;
+ }
+
+ DataSourceSurface::ScopedMap mapping(surface, DataSourceSurface::WRITE);
+ if (NS_WARN_IF(!mapping.IsMapped())) {
+ return nullptr;
+ }
+
+ gfx::ConvertYCbCrToRGB(mData, format, size, mapping.GetData(),
+ mapping.GetStride());
+
+ mSourceSurface = surface;
+
+ return surface.forget();
+}
+
+PlanarYCbCrImage::~PlanarYCbCrImage() {
+ NS_ReleaseOnMainThread("PlanarYCbCrImage::mSourceSurface",
+ mSourceSurface.forget());
+}
+
+NVImage::NVImage() : Image(nullptr, ImageFormat::NV_IMAGE), mBufferSize(0) {}
+
+NVImage::~NVImage() {
+ NS_ReleaseOnMainThread("NVImage::mSourceSurface", mSourceSurface.forget());
+}
+
+IntSize NVImage::GetSize() const { return mSize; }
+
+IntRect NVImage::GetPictureRect() const { return mData.mPictureRect; }
+
+already_AddRefed<SourceSurface> NVImage::GetAsSourceSurface() {
+ if (mSourceSurface) {
+ RefPtr<gfx::SourceSurface> surface(mSourceSurface);
+ return surface.forget();
+ }
+
+ // Convert the current NV12 or NV21 data to YUV420P so that we can follow the
+ // logics in PlanarYCbCrImage::GetAsSourceSurface().
+ auto ySize = mData.YDataSize();
+ auto cbcrSize = mData.CbCrDataSize();
+ const int bufferLength =
+ ySize.height * mData.mYStride + cbcrSize.height * cbcrSize.width * 2;
+ UniquePtr<uint8_t[]> buffer(new uint8_t[bufferLength]);
+
+ Data aData = mData;
+ aData.mCbCrStride = cbcrSize.width;
+ aData.mCbSkip = 0;
+ aData.mCrSkip = 0;
+ aData.mYChannel = buffer.get();
+ aData.mCbChannel = aData.mYChannel + ySize.height * aData.mYStride;
+ aData.mCrChannel = aData.mCbChannel + cbcrSize.height * aData.mCbCrStride;
+
+ if (mData.mCbChannel < mData.mCrChannel) { // NV12
+ libyuv::NV12ToI420(mData.mYChannel, mData.mYStride, mData.mCbChannel,
+ mData.mCbCrStride, aData.mYChannel, aData.mYStride,
+ aData.mCbChannel, aData.mCbCrStride, aData.mCrChannel,
+ aData.mCbCrStride, ySize.width, ySize.height);
+ } else { // NV21
+ libyuv::NV21ToI420(mData.mYChannel, mData.mYStride, mData.mCrChannel,
+ mData.mCbCrStride, aData.mYChannel, aData.mYStride,
+ aData.mCbChannel, aData.mCbCrStride, aData.mCrChannel,
+ aData.mCbCrStride, ySize.width, ySize.height);
+ }
+
+ // The logics in PlanarYCbCrImage::GetAsSourceSurface().
+ gfx::IntSize size(mSize);
+ gfx::SurfaceFormat format = gfx::ImageFormatToSurfaceFormat(
+ gfxPlatform::GetPlatform()->GetOffscreenFormat());
+ gfx::GetYCbCrToRGBDestFormatAndSize(aData, format, size);
+ if (mSize.width > PlanarYCbCrImage::MAX_DIMENSION ||
+ mSize.height > PlanarYCbCrImage::MAX_DIMENSION) {
+ NS_ERROR("Illegal image dest width or height");
+ return nullptr;
+ }
+
+ RefPtr<gfx::DataSourceSurface> surface =
+ gfx::Factory::CreateDataSourceSurface(size, format);
+ if (NS_WARN_IF(!surface)) {
+ return nullptr;
+ }
+
+ DataSourceSurface::ScopedMap mapping(surface, DataSourceSurface::WRITE);
+ if (NS_WARN_IF(!mapping.IsMapped())) {
+ return nullptr;
+ }
+
+ gfx::ConvertYCbCrToRGB(aData, format, size, mapping.GetData(),
+ mapping.GetStride());
+
+ mSourceSurface = surface;
+
+ return surface.forget();
+}
+
+bool NVImage::IsValid() const { return !!mBufferSize; }
+
+uint32_t NVImage::GetBufferSize() const { return mBufferSize; }
+
+NVImage* NVImage::AsNVImage() { return this; };
+
+bool NVImage::SetData(const Data& aData) {
+ MOZ_ASSERT(aData.mCbSkip == 1 && aData.mCrSkip == 1);
+ MOZ_ASSERT((int)std::abs(aData.mCbChannel - aData.mCrChannel) == 1);
+
+ // Calculate buffer size
+ // Use uint32_t throughout to match AllocateBuffer's param and mBufferSize
+ const auto checkedSize =
+ CheckedInt<uint32_t>(aData.YDataSize().height) * aData.mYStride +
+ CheckedInt<uint32_t>(aData.CbCrDataSize().height) * aData.mCbCrStride;
+
+ if (!checkedSize.isValid()) return false;
+
+ const auto size = checkedSize.value();
+
+ // Allocate a new buffer.
+ mBuffer = AllocateBuffer(size);
+ if (!mBuffer) {
+ return false;
+ }
+
+ // Update mBufferSize.
+ mBufferSize = size;
+
+ // Update mData.
+ mData = aData;
+ mData.mYChannel = mBuffer.get();
+ mData.mCbChannel = mData.mYChannel + (aData.mCbChannel - aData.mYChannel);
+ mData.mCrChannel = mData.mYChannel + (aData.mCrChannel - aData.mYChannel);
+
+ // Update mSize.
+ mSize = aData.mPictureRect.Size();
+
+ // Copy the input data into mBuffer.
+ // This copies the y-channel and the interleaving CbCr-channel.
+ memcpy(mData.mYChannel, aData.mYChannel, mBufferSize);
+
+ return true;
+}
+
+const NVImage::Data* NVImage::GetData() const { return &mData; }
+
+UniquePtr<uint8_t[]> NVImage::AllocateBuffer(uint32_t aSize) {
+ UniquePtr<uint8_t[]> buffer(new uint8_t[aSize]);
+ return buffer;
+}
+
+SourceSurfaceImage::SourceSurfaceImage(const gfx::IntSize& aSize,
+ gfx::SourceSurface* aSourceSurface)
+ : Image(nullptr, ImageFormat::MOZ2D_SURFACE),
+ mSize(aSize),
+ mSourceSurface(aSourceSurface),
+ mTextureFlags(TextureFlags::DEFAULT) {}
+
+SourceSurfaceImage::SourceSurfaceImage(gfx::SourceSurface* aSourceSurface)
+ : Image(nullptr, ImageFormat::MOZ2D_SURFACE),
+ mSize(aSourceSurface->GetSize()),
+ mSourceSurface(aSourceSurface),
+ mTextureFlags(TextureFlags::DEFAULT) {}
+
+SourceSurfaceImage::~SourceSurfaceImage() {
+ NS_ReleaseOnMainThread("SourceSurfaceImage::mSourceSurface",
+ mSourceSurface.forget());
+}
+
+TextureClient* SourceSurfaceImage::GetTextureClient(
+ KnowsCompositor* aKnowsCompositor) {
+ if (!aKnowsCompositor) {
+ return nullptr;
+ }
+
+ return mTextureClients.WithEntryHandle(
+ aKnowsCompositor->GetSerial(), [&](auto&& entry) -> TextureClient* {
+ if (entry) {
+ return entry->get();
+ }
+
+ RefPtr<TextureClient> textureClient;
+ RefPtr<SourceSurface> surface = GetAsSourceSurface();
+ MOZ_ASSERT(surface);
+ if (surface) {
+ // gfx::BackendType::NONE means default to content backend
+ textureClient = TextureClient::CreateFromSurface(
+ aKnowsCompositor, surface, BackendSelector::Content,
+ mTextureFlags, ALLOC_DEFAULT);
+ }
+ if (textureClient) {
+ textureClient->SyncWithObject(aKnowsCompositor->GetSyncObject());
+ return entry.Insert(std::move(textureClient)).get();
+ }
+
+ return nullptr;
+ });
+}
+
+ImageContainer::ProducerID ImageContainer::AllocateProducerID() {
+ // Callable on all threads.
+ static Atomic<ImageContainer::ProducerID> sProducerID(0u);
+ return ++sProducerID;
+}
+
+} // namespace mozilla::layers
diff --git a/gfx/layers/ImageContainer.h b/gfx/layers/ImageContainer.h
new file mode 100644
index 0000000000..58bfcf6923
--- /dev/null
+++ b/gfx/layers/ImageContainer.h
@@ -0,0 +1,943 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GFX_IMAGECONTAINER_H
+#define GFX_IMAGECONTAINER_H
+
+#include <stdint.h> // for int32_t, uint32_t, uint8_t, uint64_t
+#include "ImageTypes.h" // for ImageFormat, etc
+#include "mozilla/AlreadyAddRefed.h"
+#include "mozilla/Assertions.h" // for MOZ_ASSERT_HELPER2
+#include "mozilla/Mutex.h" // for Mutex
+#include "mozilla/RecursiveMutex.h" // for RecursiveMutex, etc
+#include "mozilla/ThreadSafeWeakPtr.h"
+#include "mozilla/TimeStamp.h" // for TimeStamp
+#include "mozilla/gfx/Point.h" // For IntSize
+#include "mozilla/gfx/Rect.h"
+#include "mozilla/gfx/Types.h" // For ColorDepth
+#include "mozilla/layers/LayersTypes.h" // for LayersBackend, etc
+#include "mozilla/layers/CompositorTypes.h"
+#include "mozilla/mozalloc.h" // for operator delete, etc
+#include "nsDebug.h" // for NS_ASSERTION
+#include "nsISupportsImpl.h" // for Image::Release, etc
+#include "nsTArray.h" // for nsTArray
+#include "nsThreadUtils.h" // for NS_IsMainThread
+#include "mozilla/Atomics.h"
+#include "mozilla/gfx/2D.h"
+#include "mozilla/EnumeratedArray.h"
+#include "mozilla/UniquePtr.h"
+#include "MediaInfo.h"
+#include "nsTHashMap.h"
+
+#ifdef XP_WIN
+struct ID3D10Texture2D;
+struct ID3D10Device;
+struct ID3D10ShaderResourceView;
+#endif
+
+typedef void* HANDLE;
+
+namespace mozilla {
+
+namespace layers {
+
+class ImageClient;
+class ImageCompositeNotification;
+class ImageContainer;
+class ImageContainerChild;
+class SharedPlanarYCbCrImage;
+class SurfaceDescriptor;
+class PlanarYCbCrImage;
+class TextureClient;
+class TextureClientRecycleAllocator;
+class KnowsCompositor;
+class NVImage;
+class MemoryOrShmem;
+#ifdef XP_WIN
+class D3D11RecycleAllocator;
+class D3D11YCbCrRecycleAllocator;
+#endif
+#ifdef XP_MACOSX
+class MacIOSurfaceRecycleAllocator;
+#endif
+class SurfaceDescriptorBuffer;
+
+struct ImageBackendData {
+ virtual ~ImageBackendData() = default;
+
+ protected:
+ ImageBackendData() = default;
+};
+
+/* Forward declarations for Image derivatives. */
+class GLImage;
+class SharedRGBImage;
+#ifdef MOZ_WIDGET_ANDROID
+class SurfaceTextureImage;
+#elif defined(XP_MACOSX)
+class MacIOSurfaceImage;
+#elif MOZ_WAYLAND
+class DMABUFSurfaceImage;
+#endif
+
+/**
+ * A class representing a buffer of pixel data. The data can be in one
+ * of various formats including YCbCr.
+ *
+ * Create an image using an ImageContainer. Fill the image with data, and
+ * then call ImageContainer::SetImage to display it. An image must not be
+ * modified after calling SetImage. Image implementations do not need to
+ * perform locking; when filling an Image, the Image client is responsible
+ * for ensuring only one thread accesses the Image at a time, and after
+ * SetImage the image is immutable.
+ *
+ * When resampling an Image, only pixels within the buffer should be
+ * sampled. For example, cairo images should be sampled in EXTEND_PAD mode.
+ */
+class Image {
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(Image)
+
+ public:
+ ImageFormat GetFormat() const { return mFormat; }
+ void* GetImplData() const { return mImplData; }
+
+ virtual gfx::IntSize GetSize() const = 0;
+ virtual gfx::IntPoint GetOrigin() const { return gfx::IntPoint(0, 0); }
+ virtual gfx::IntRect GetPictureRect() const {
+ return gfx::IntRect(GetOrigin().x, GetOrigin().y, GetSize().width,
+ GetSize().height);
+ }
+ virtual gfx::ColorDepth GetColorDepth() const {
+ return gfx::ColorDepth::COLOR_8;
+ }
+
+ ImageBackendData* GetBackendData(LayersBackend aBackend) {
+ return mBackendData[aBackend].get();
+ }
+ void SetBackendData(LayersBackend aBackend, ImageBackendData* aData) {
+ mBackendData[aBackend] = mozilla::WrapUnique(aData);
+ }
+
+ int32_t GetSerial() const { return mSerial; }
+
+ bool IsDRM() const { return mIsDRM; }
+ void SetIsDRM(bool aIsDRM) { mIsDRM = aIsDRM; }
+
+ virtual already_AddRefed<gfx::SourceSurface> GetAsSourceSurface() = 0;
+
+ virtual bool IsValid() const { return true; }
+
+ /**
+ * For use with the TextureForwarder only (so that the later can
+ * synchronize the TextureClient with the TextureHost).
+ */
+ virtual TextureClient* GetTextureClient(KnowsCompositor* aKnowsCompositor) {
+ return nullptr;
+ }
+
+ /* Access to derived classes. */
+ virtual GLImage* AsGLImage() { return nullptr; }
+#ifdef MOZ_WIDGET_ANDROID
+ virtual SurfaceTextureImage* AsSurfaceTextureImage() { return nullptr; }
+#endif
+#ifdef XP_MACOSX
+ virtual MacIOSurfaceImage* AsMacIOSurfaceImage() { return nullptr; }
+#endif
+ virtual PlanarYCbCrImage* AsPlanarYCbCrImage() { return nullptr; }
+#ifdef MOZ_WAYLAND
+ virtual DMABUFSurfaceImage* AsDMABUFSurfaceImage() { return nullptr; }
+#endif
+
+ virtual NVImage* AsNVImage() { return nullptr; }
+
+ virtual Maybe<SurfaceDescriptor> GetDesc();
+
+ protected:
+ Maybe<SurfaceDescriptor> GetDescFromTexClient(
+ TextureClient* tcOverride = nullptr);
+
+ Image(void* aImplData, ImageFormat aFormat)
+ : mImplData(aImplData),
+ mSerial(++sSerialCounter),
+ mFormat(aFormat),
+ mIsDRM(false) {}
+
+ // Protected destructor, to discourage deletion outside of Release():
+ virtual ~Image() = default;
+
+ mozilla::EnumeratedArray<mozilla::layers::LayersBackend,
+ mozilla::layers::LayersBackend::LAYERS_LAST,
+ UniquePtr<ImageBackendData>>
+ mBackendData;
+
+ void* mImplData;
+ int32_t mSerial;
+ ImageFormat mFormat;
+ bool mIsDRM;
+
+ static mozilla::Atomic<int32_t> sSerialCounter;
+};
+
+/**
+ * A RecycleBin is owned by an ImageContainer. We store buffers in it that we
+ * want to recycle from one image to the next.It's a separate object from
+ * ImageContainer because images need to store a strong ref to their RecycleBin
+ * and we must avoid creating a reference loop between an ImageContainer and
+ * its active image.
+ */
+class BufferRecycleBin final {
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(BufferRecycleBin)
+
+ // typedef mozilla::gl::GLContext GLContext;
+
+ public:
+ BufferRecycleBin();
+
+ void RecycleBuffer(mozilla::UniquePtr<uint8_t[]> aBuffer, uint32_t aSize);
+ // Returns a recycled buffer of the right size, or allocates a new buffer.
+ mozilla::UniquePtr<uint8_t[]> GetBuffer(uint32_t aSize);
+ virtual void ClearRecycledBuffers();
+
+ private:
+ typedef mozilla::Mutex Mutex;
+
+ // Private destructor, to discourage deletion outside of Release():
+ ~BufferRecycleBin() = default;
+
+ // This protects mRecycledBuffers, mRecycledBufferSize, mRecycledTextures
+ // and mRecycledTextureSizes
+ Mutex mLock;
+
+ // We should probably do something to prune this list on a timer so we don't
+ // eat excess memory while video is paused...
+ nsTArray<mozilla::UniquePtr<uint8_t[]>> mRecycledBuffers
+ MOZ_GUARDED_BY(mLock);
+ // This is only valid if mRecycledBuffers is non-empty
+ uint32_t mRecycledBufferSize MOZ_GUARDED_BY(mLock);
+};
+
+/**
+ * A class that manages Image creation for a LayerManager. The only reason
+ * we need a separate class here is that LayerManagers aren't threadsafe
+ * (because layers can only be used on the main thread) and we want to
+ * be able to create images from any thread, to facilitate video playback
+ * without involving the main thread, for example.
+ * Different layer managers can implement child classes of this making it
+ * possible to create layer manager specific images.
+ * This class is not meant to be used directly but rather can be set on an
+ * image container. This is usually done by the layer system internally and
+ * not explicitly by users. For PlanarYCbCr or Cairo images the default
+ * implementation will creates images whose data lives in system memory, for
+ * MacIOSurfaces the default implementation will be a simple MacIOSurface
+ * wrapper.
+ */
+
+class ImageFactory {
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(ImageFactory)
+ protected:
+ friend class ImageContainer;
+
+ ImageFactory() = default;
+ virtual ~ImageFactory() = default;
+
+ virtual RefPtr<PlanarYCbCrImage> CreatePlanarYCbCrImage(
+ const gfx::IntSize& aScaleHint, BufferRecycleBin* aRecycleBin);
+};
+
+// Used to notify ImageContainer::NotifyComposite()
+class ImageContainerListener final {
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(ImageContainerListener)
+
+ public:
+ explicit ImageContainerListener(ImageContainer* aImageContainer);
+
+ void NotifyComposite(const ImageCompositeNotification& aNotification);
+ void NotifyDropped(uint32_t aDropped);
+ void ClearImageContainer();
+ void DropImageClient();
+
+ private:
+ typedef mozilla::Mutex Mutex;
+
+ ~ImageContainerListener();
+
+ Mutex mLock;
+ ImageContainer* mImageContainer MOZ_GUARDED_BY(mLock);
+};
+
+/**
+ * A class that manages Images for an ImageLayer. The only reason
+ * we need a separate class here is that ImageLayers aren't threadsafe
+ * (because layers can only be used on the main thread) and we want to
+ * be able to set the current Image from any thread, to facilitate
+ * video playback without involving the main thread, for example.
+ *
+ * An ImageContainer can operate in one of these modes:
+ * 1) Normal. Triggered by constructing the ImageContainer with
+ * DISABLE_ASYNC or when compositing is happening on the main thread.
+ * SetCurrentImages changes ImageContainer state but nothing is sent to the
+ * compositor until the next layer transaction.
+ * 2) Asynchronous. Initiated by constructing the ImageContainer with
+ * ENABLE_ASYNC when compositing is happening on the main thread.
+ * SetCurrentImages sends a message through the ImageBridge to the compositor
+ * thread to update the image, without going through the main thread or
+ * a layer transaction.
+ * The ImageContainer uses a shared memory block containing a cross-process
+ * mutex to communicate with the compositor thread. SetCurrentImage
+ * synchronously updates the shared state to point to the new image and the old
+ * image is immediately released (not true in Normal or Asynchronous modes).
+ */
+class ImageContainer final : public SupportsThreadSafeWeakPtr<ImageContainer> {
+ friend class ImageContainerChild;
+
+ public:
+ MOZ_DECLARE_REFCOUNTED_TYPENAME(ImageContainer)
+
+ enum Mode { SYNCHRONOUS = 0x0, ASYNCHRONOUS = 0x01 };
+
+ static const uint64_t sInvalidAsyncContainerId = 0;
+
+ explicit ImageContainer(ImageContainer::Mode flag = SYNCHRONOUS);
+
+ /**
+ * Create ImageContainer just to hold another ASYNCHRONOUS ImageContainer's
+ * async container ID.
+ * @param aAsyncContainerID async container ID for which we are a proxy
+ */
+ explicit ImageContainer(const CompositableHandle& aHandle);
+
+ ~ImageContainer();
+
+ typedef ContainerFrameID FrameID;
+ typedef ContainerProducerID ProducerID;
+
+ RefPtr<PlanarYCbCrImage> CreatePlanarYCbCrImage();
+
+ // Factory methods for shared image types.
+ RefPtr<SharedRGBImage> CreateSharedRGBImage();
+
+ struct NonOwningImage {
+ explicit NonOwningImage(Image* aImage = nullptr,
+ TimeStamp aTimeStamp = TimeStamp(),
+ FrameID aFrameID = 0, ProducerID aProducerID = 0)
+ : mImage(aImage),
+ mTimeStamp(aTimeStamp),
+ mFrameID(aFrameID),
+ mProducerID(aProducerID) {}
+ Image* mImage;
+ TimeStamp mTimeStamp;
+ FrameID mFrameID;
+ ProducerID mProducerID;
+ };
+ /**
+ * Set aImages as the list of timestamped to display. The Images must have
+ * been created by this ImageContainer.
+ * Can be called on any thread. This method takes mRecursiveMutex
+ * when accessing thread-shared state.
+ * aImages must be non-empty. The first timestamp in the list may be
+ * null but the others must not be, and the timestamps must increase.
+ * Every element of aImages must have non-null mImage.
+ * mFrameID can be zero, in which case you won't get meaningful
+ * painted/dropped frame counts. Otherwise you should use a unique and
+ * increasing ID for each decoded and submitted frame (but it's OK to
+ * pass the same frame to SetCurrentImages).
+ * mProducerID is a unique ID for the stream of images. A change in the
+ * mProducerID means changing to a new mFrameID namespace. All frames in
+ * aImages must have the same mProducerID.
+ *
+ * The Image data must not be modified after this method is called!
+ * Note that this must not be called if ENABLE_ASYNC has not been set.
+ *
+ * The implementation calls CurrentImageChanged() while holding
+ * mRecursiveMutex.
+ *
+ * If this ImageContainer has an ImageClient for async video:
+ * Schedule a task to send the image to the compositor using the
+ * PImageBridge protcol without using the main thread.
+ */
+ void SetCurrentImages(const nsTArray<NonOwningImage>& aImages);
+
+ /**
+ * Clear all images. Let ImageClient release all TextureClients. Because we
+ * may release the lock after acquiring it in this method, it cannot be called
+ * with the lock held.
+ */
+ void ClearAllImages() MOZ_EXCLUDES(mRecursiveMutex);
+
+ /**
+ * Clear any resources that are not immediately necessary. This may be called
+ * in low-memory conditions.
+ */
+ void ClearCachedResources();
+
+ /**
+ * Clear the current images.
+ * This function is expect to be called only from a CompositableClient
+ * that belongs to ImageBridgeChild. Created to prevent dead lock.
+ * See Bug 901224.
+ */
+ void ClearImagesFromImageBridge();
+
+ /**
+ * Set an Image as the current image to display. The Image must have
+ * been created by this ImageContainer.
+ * Must be called on the main thread, within a layers transaction.
+ *
+ * This method takes mRecursiveMutex
+ * when accessing thread-shared state.
+ * aImage can be null. While it's null, nothing will be painted.
+ *
+ * The Image data must not be modified after this method is called!
+ * Note that this must not be called if ENABLE_ASYNC been set.
+ *
+ * You won't get meaningful painted/dropped counts when using this method.
+ */
+ void SetCurrentImageInTransaction(Image* aImage);
+ void SetCurrentImagesInTransaction(const nsTArray<NonOwningImage>& aImages);
+
+ /**
+ * Returns true if this ImageContainer uses the ImageBridge IPDL protocol.
+ *
+ * Can be called from any thread.
+ */
+ bool IsAsync() const;
+
+ /**
+ * If this ImageContainer uses ImageBridge, returns the ID associated to
+ * this container, for use in the ImageBridge protocol.
+ * Returns 0 if this ImageContainer does not use ImageBridge. Note that
+ * 0 is always an invalid ID for asynchronous image containers.
+ *
+ * Can be called from any thread.
+ */
+ CompositableHandle GetAsyncContainerHandle();
+
+ /**
+ * Returns if the container currently has an image.
+ * Can be called on any thread. This method takes mRecursiveMutex
+ * when accessing thread-shared state.
+ */
+ bool HasCurrentImage();
+
+ struct OwningImage {
+ OwningImage() : mFrameID(0), mProducerID(0), mComposited(false) {}
+ RefPtr<Image> mImage;
+ TimeStamp mTimeStamp;
+ FrameID mFrameID;
+ ProducerID mProducerID;
+ bool mComposited;
+ };
+ /**
+ * Copy the current Image list to aImages.
+ * This has to add references since otherwise there are race conditions
+ * where the current image is destroyed before the caller can add
+ * a reference.
+ * Can be called on any thread.
+ * May return an empty list to indicate there is no current image.
+ * If aGenerationCounter is non-null, sets *aGenerationCounter to a value
+ * that's unique for this ImageContainer state.
+ */
+ void GetCurrentImages(nsTArray<OwningImage>* aImages,
+ uint32_t* aGenerationCounter = nullptr);
+
+ /**
+ * Returns the size of the image in pixels.
+ * Can be called on any thread. This method takes mRecursiveMutex when
+ * accessing thread-shared state.
+ */
+ gfx::IntSize GetCurrentSize();
+
+ /**
+ * Sets a size that the image is expected to be rendered at.
+ * This is a hint for image backends to optimize scaling.
+ * Default implementation in this class is to ignore the hint.
+ * Can be called on any thread. This method takes mRecursiveMutex
+ * when accessing thread-shared state.
+ */
+ void SetScaleHint(const gfx::IntSize& aScaleHint) {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ mScaleHint = aScaleHint;
+ }
+
+ const gfx::IntSize GetScaleHint() const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ return mScaleHint;
+ }
+
+ void SetTransformHint(const gfx::Matrix& aTransformHint) {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ mTransformHint = aTransformHint;
+ }
+
+ const gfx::Matrix GetTransformHint() const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ return mTransformHint;
+ }
+
+ void SetRotation(VideoInfo::Rotation aRotation) {
+ MOZ_ASSERT(NS_IsMainThread());
+ mRotation = aRotation;
+ }
+
+ VideoInfo::Rotation GetRotation() const {
+ MOZ_ASSERT(NS_IsMainThread());
+ return mRotation;
+ }
+
+ void SetImageFactory(ImageFactory* aFactory) {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ mImageFactory = aFactory ? aFactory : new ImageFactory();
+ }
+
+ already_AddRefed<ImageFactory> GetImageFactory() const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ return do_AddRef(mImageFactory);
+ }
+
+ void EnsureRecycleAllocatorForRDD(KnowsCompositor* aKnowsCompositor);
+
+#ifdef XP_WIN
+ already_AddRefed<D3D11RecycleAllocator> GetD3D11RecycleAllocator(
+ KnowsCompositor* aKnowsCompositor, gfx::SurfaceFormat aPreferredFormat);
+ already_AddRefed<D3D11YCbCrRecycleAllocator> GetD3D11YCbCrRecycleAllocator(
+ KnowsCompositor* aKnowsCompositor);
+#endif
+
+#ifdef XP_MACOSX
+ already_AddRefed<MacIOSurfaceRecycleAllocator>
+ GetMacIOSurfaceRecycleAllocator();
+#endif
+
+ /**
+ * Returns the delay between the last composited image's presentation
+ * timestamp and when it was first composited. It's possible for the delay
+ * to be negative if the first image in the list passed to SetCurrentImages
+ * has a presentation timestamp greater than "now".
+ * Returns 0 if the composited image had a null timestamp, or if no
+ * image has been composited yet.
+ */
+ TimeDuration GetPaintDelay() {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ return mPaintDelay;
+ }
+
+ /**
+ * Returns the number of images which have been contained in this container
+ * and painted at least once. Can be called from any thread.
+ */
+ uint32_t GetPaintCount() {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ return mPaintCount;
+ }
+
+ /**
+ * An entry in the current image list "expires" when the entry has an
+ * non-null timestamp, and in a SetCurrentImages call the new image list is
+ * non-empty, the timestamp of the first new image is non-null and greater
+ * than the timestamp associated with the image, and the first new image's
+ * frameID is not the same as the entry's.
+ * Every expired image that is never composited is counted as dropped.
+ */
+ uint32_t GetDroppedImageCount() { return mDroppedImageCount; }
+
+ void NotifyComposite(const ImageCompositeNotification& aNotification);
+ void NotifyDropped(uint32_t aDropped);
+
+ already_AddRefed<ImageContainerListener> GetImageContainerListener() const;
+
+ /**
+ * Get the ImageClient associated with this container. Returns only after
+ * validating, and it will recreate the image client if that fails.
+ * Returns nullptr if not applicable.
+ */
+ already_AddRefed<ImageClient> GetImageClient();
+
+ /**
+ * Main thread only.
+ */
+ static ProducerID AllocateProducerID();
+
+ void DropImageClient();
+
+ private:
+ typedef mozilla::RecursiveMutex RecursiveMutex;
+
+ void SetCurrentImageInternal(const nsTArray<NonOwningImage>& aImages);
+
+ // This is called to ensure we have an active image, this may not be true
+ // when we're storing image information in a RemoteImageData structure.
+ // NOTE: If we have remote data mRemoteDataMutex should be locked when
+ // calling this function!
+ void EnsureActiveImage();
+
+ void EnsureImageClient() MOZ_REQUIRES(mRecursiveMutex);
+
+ bool HasImageClient() const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ return !!mImageClient;
+ }
+
+ // RecursiveMutex to protect thread safe access to the "current
+ // image", and any other state which is shared between threads.
+ mutable RecursiveMutex mRecursiveMutex;
+
+ RefPtr<TextureClientRecycleAllocator> mRecycleAllocator
+ MOZ_GUARDED_BY(mRecursiveMutex);
+
+#ifdef XP_WIN
+ RefPtr<D3D11RecycleAllocator> mD3D11RecycleAllocator
+ MOZ_GUARDED_BY(mRecursiveMutex);
+
+ RefPtr<D3D11YCbCrRecycleAllocator> mD3D11YCbCrRecycleAllocator
+ MOZ_GUARDED_BY(mRecursiveMutex);
+#endif
+#ifdef XP_MACOSX
+ RefPtr<MacIOSurfaceRecycleAllocator> mMacIOSurfaceRecycleAllocator
+ MOZ_GUARDED_BY(mRecursiveMutex);
+#endif
+
+ nsTArray<OwningImage> mCurrentImages MOZ_GUARDED_BY(mRecursiveMutex);
+
+ // Updates every time mActiveImage changes
+ uint32_t mGenerationCounter MOZ_GUARDED_BY(mRecursiveMutex);
+
+ // Number of contained images that have been painted at least once. It's up
+ // to the ImageContainer implementation to ensure accesses to this are
+ // threadsafe.
+ uint32_t mPaintCount MOZ_GUARDED_BY(mRecursiveMutex);
+
+ // See GetPaintDelay. Accessed only with mRecursiveMutex held.
+ TimeDuration mPaintDelay MOZ_GUARDED_BY(mRecursiveMutex);
+
+ // See GetDroppedImageCount.
+ mozilla::Atomic<uint32_t> mDroppedImageCount;
+
+ // This is the image factory used by this container, layer managers using
+ // this container can set an alternative image factory that will be used to
+ // create images for this container.
+ RefPtr<ImageFactory> mImageFactory MOZ_GUARDED_BY(mRecursiveMutex);
+
+ gfx::IntSize mScaleHint MOZ_GUARDED_BY(mRecursiveMutex);
+
+ gfx::Matrix mTransformHint MOZ_GUARDED_BY(mRecursiveMutex);
+
+ // Main thread only.
+ VideoInfo::Rotation mRotation = VideoInfo::Rotation::kDegree_0;
+
+ RefPtr<BufferRecycleBin> mRecycleBin MOZ_GUARDED_BY(mRecursiveMutex);
+
+ // This member points to an ImageClient if this ImageContainer was
+ // sucessfully created with ENABLE_ASYNC, or points to null otherwise.
+ // 'unsuccessful' in this case only means that the ImageClient could not
+ // be created, most likely because off-main-thread compositing is not enabled.
+ // In this case the ImageContainer is perfectly usable, but it will forward
+ // frames to the compositor through transactions in the main thread rather
+ // than asynchronusly using the ImageBridge IPDL protocol.
+ RefPtr<ImageClient> mImageClient MOZ_GUARDED_BY(mRecursiveMutex);
+
+ const bool mIsAsync;
+ CompositableHandle mAsyncContainerHandle MOZ_GUARDED_BY(mRecursiveMutex);
+
+ // ProducerID for last current image(s)
+ ProducerID mCurrentProducerID MOZ_GUARDED_BY(mRecursiveMutex);
+
+ RefPtr<ImageContainerListener> mNotifyCompositeListener;
+
+ static mozilla::Atomic<uint32_t> sGenerationCounter;
+};
+
+class AutoLockImage {
+ public:
+ explicit AutoLockImage(ImageContainer* aContainer) {
+ aContainer->GetCurrentImages(&mImages);
+ }
+
+ bool HasImage() const { return !mImages.IsEmpty(); }
+ Image* GetImage() const {
+ return mImages.IsEmpty() ? nullptr : mImages[0].mImage.get();
+ }
+
+ Image* GetImage(TimeStamp aTimeStamp) const {
+ if (mImages.IsEmpty()) {
+ return nullptr;
+ }
+
+ MOZ_ASSERT(!aTimeStamp.IsNull());
+ uint32_t chosenIndex = 0;
+
+ while (chosenIndex + 1 < mImages.Length() &&
+ mImages[chosenIndex + 1].mTimeStamp <= aTimeStamp) {
+ ++chosenIndex;
+ }
+
+ return mImages[chosenIndex].mImage.get();
+ }
+
+ private:
+ AutoTArray<ImageContainer::OwningImage, 4> mImages;
+};
+
+// This type is currently only used for AVIF and WebCodecs therefore makes some
+// specific assumptions (e.g., Alpha's bpc and stride is equal to Y's one)
+struct PlanarAlphaData {
+ uint8_t* mChannel = nullptr;
+ gfx::IntSize mSize = gfx::IntSize(0, 0);
+ gfx::ColorDepth mDepth = gfx::ColorDepth::COLOR_8;
+ bool mPremultiplied = false;
+};
+struct PlanarYCbCrData {
+ // Luminance buffer
+ uint8_t* mYChannel = nullptr;
+ int32_t mYStride = 0;
+ int32_t mYSkip = 0;
+ // Chroma buffers
+ uint8_t* mCbChannel = nullptr;
+ uint8_t* mCrChannel = nullptr;
+ int32_t mCbCrStride = 0;
+ int32_t mCbSkip = 0;
+ int32_t mCrSkip = 0;
+ // Alpha buffer and its metadata
+ Maybe<PlanarAlphaData> mAlpha = Nothing();
+ // Picture region
+ gfx::IntRect mPictureRect = gfx::IntRect(0, 0, 0, 0);
+ StereoMode mStereoMode = StereoMode::MONO;
+ gfx::ColorDepth mColorDepth = gfx::ColorDepth::COLOR_8;
+ gfx::YUVColorSpace mYUVColorSpace = gfx::YUVColorSpace::Default;
+ gfx::ColorSpace2 mColorPrimaries = gfx::ColorSpace2::UNKNOWN;
+ gfx::TransferFunction mTransferFunction = gfx::TransferFunction::BT709;
+ gfx::ColorRange mColorRange = gfx::ColorRange::LIMITED;
+ gfx::ChromaSubsampling mChromaSubsampling = gfx::ChromaSubsampling::FULL;
+
+ // The cropped picture size of the Y channel.
+ gfx::IntSize YPictureSize() const { return mPictureRect.Size(); }
+
+ // The cropped picture size of the Cb/Cr channels.
+ gfx::IntSize CbCrPictureSize() const {
+ return mCbCrStride > 0 ? gfx::ChromaSize(YPictureSize(), mChromaSubsampling)
+ : gfx::IntSize(0, 0);
+ }
+
+ // The total uncropped size of data in the Y channel.
+ gfx::IntSize YDataSize() const {
+ return gfx::IntSize(mPictureRect.XMost(), mPictureRect.YMost());
+ }
+
+ // The total uncropped size of data in the Cb/Cr channels.
+ gfx::IntSize CbCrDataSize() const {
+ return mCbCrStride > 0 ? gfx::ChromaSize(YDataSize(), mChromaSubsampling)
+ : gfx::IntSize(0, 0);
+ }
+
+ static Maybe<PlanarYCbCrData> From(const SurfaceDescriptorBuffer&);
+};
+
+/****** Image subtypes for the different formats ******/
+
+/**
+ * We assume that the image data is in the REC 470M color space (see
+ * Theora specification, section 4.3.1).
+ *
+ * The YCbCr format can be:
+ *
+ * 4:4:4 - CbCr width/height are the same as Y.
+ * 4:2:2 - CbCr width is half that of Y. Height is the same.
+ * 4:2:0 - CbCr width and height is half that of Y.
+ *
+ * mChromaSubsampling specifies which YCbCr subsampling scheme to use.
+ *
+ * The Image that is rendered is the picture region defined by mPictureRect.
+ *
+ * mYSkip, mCbSkip, mCrSkip are added to support various output
+ * formats from hardware decoder. They are per-pixel skips in the
+ * source image.
+ *
+ * For example when image width is 640, mYStride is 670, mYSkip is 2,
+ * the mYChannel buffer looks like:
+ *
+ * |<----------------------- mYStride ----------------------------->|
+ * |<----------------- YDataSize().width ---------->|
+ * 0 3 6 9 12 15 18 21 639 669
+ * |----------------------------------------------------------------|
+ * |Y___Y___Y___Y___Y___Y___Y___Y... |%%%%%%%%%%%%%%%|
+ * |Y___Y___Y___Y___Y___Y___Y___Y... |%%%%%%%%%%%%%%%|
+ * |Y___Y___Y___Y___Y___Y___Y___Y... |%%%%%%%%%%%%%%%|
+ * | |<->|
+ * mYSkip
+ */
+class PlanarYCbCrImage : public Image {
+ public:
+ typedef PlanarYCbCrData Data;
+
+ enum { MAX_DIMENSION = 16384 };
+
+ virtual ~PlanarYCbCrImage();
+
+ /**
+ * This makes a copy of the data buffers, in order to support functioning
+ * in all different layer managers.
+ */
+ virtual bool CopyData(const Data& aData) = 0;
+
+ /**
+ * This doesn't make a copy of the data buffers.
+ */
+ virtual bool AdoptData(const Data& aData);
+
+ /**
+ * This will create an empty data buffers according to the input data's size.
+ */
+ virtual bool CreateEmptyBuffer(const Data& aData, const gfx::IntSize& aYSize,
+ const gfx::IntSize& aCbCrSize) {
+ return false;
+ }
+ bool CreateEmptyBuffer(const Data& aData) {
+ return CreateEmptyBuffer(aData, aData.YDataSize(), aData.CbCrDataSize());
+ }
+
+ /**
+ * Grab the original YUV data. This is optional.
+ */
+ virtual const Data* GetData() const { return &mData; }
+
+ /**
+ * Return the number of bytes of heap memory used to store this image.
+ */
+ uint32_t GetDataSize() const { return mBufferSize; }
+
+ bool IsValid() const override { return !!mBufferSize; }
+
+ gfx::IntSize GetSize() const override { return mSize; }
+
+ gfx::IntPoint GetOrigin() const override { return mOrigin; }
+
+ PlanarYCbCrImage();
+
+ virtual size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const {
+ return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+ }
+
+ virtual size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const = 0;
+
+ PlanarYCbCrImage* AsPlanarYCbCrImage() override { return this; }
+
+ /**
+ * Build a SurfaceDescriptorBuffer with this image. A function to allocate
+ * a MemoryOrShmem with the given capacity must be provided.
+ */
+ virtual nsresult BuildSurfaceDescriptorBuffer(
+ SurfaceDescriptorBuffer& aSdBuffer,
+ const std::function<MemoryOrShmem(uint32_t)>& aAllocate);
+
+ protected:
+ already_AddRefed<gfx::SourceSurface> GetAsSourceSurface() override;
+
+ void SetOffscreenFormat(gfxImageFormat aFormat) {
+ mOffscreenFormat = aFormat;
+ }
+ gfxImageFormat GetOffscreenFormat() const;
+
+ Data mData;
+ gfx::IntPoint mOrigin;
+ gfx::IntSize mSize;
+ gfxImageFormat mOffscreenFormat;
+ RefPtr<gfx::SourceSurface> mSourceSurface;
+ uint32_t mBufferSize;
+};
+
+class RecyclingPlanarYCbCrImage : public PlanarYCbCrImage {
+ public:
+ explicit RecyclingPlanarYCbCrImage(BufferRecycleBin* aRecycleBin)
+ : mRecycleBin(aRecycleBin) {}
+ virtual ~RecyclingPlanarYCbCrImage();
+ bool CopyData(const Data& aData) override;
+ size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override;
+
+ protected:
+ /**
+ * Return a buffer to store image data in.
+ */
+ mozilla::UniquePtr<uint8_t[]> AllocateBuffer(uint32_t aSize);
+
+ RefPtr<BufferRecycleBin> mRecycleBin;
+ mozilla::UniquePtr<uint8_t[]> mBuffer;
+};
+
+/**
+ * NVImage is used to store YUV420SP_NV12 and YUV420SP_NV21 data natively, which
+ * are not supported by PlanarYCbCrImage. (PlanarYCbCrImage only stores YUV444P,
+ * YUV422P and YUV420P, it converts YUV420SP_NV12 and YUV420SP_NV21 data into
+ * YUV420P in its PlanarYCbCrImage::SetData() method.)
+ *
+ * PlanarYCbCrData is able to express all the YUV family and so we keep use it
+ * in NVImage.
+ */
+class NVImage final : public Image {
+ typedef PlanarYCbCrData Data;
+
+ public:
+ NVImage();
+ virtual ~NVImage();
+
+ // Methods inherited from layers::Image.
+ gfx::IntSize GetSize() const override;
+ gfx::IntRect GetPictureRect() const override;
+ already_AddRefed<gfx::SourceSurface> GetAsSourceSurface() override;
+ bool IsValid() const override;
+ NVImage* AsNVImage() override;
+
+ // Methods mimic layers::PlanarYCbCrImage.
+ bool SetData(const Data& aData);
+ const Data* GetData() const;
+ uint32_t GetBufferSize() const;
+
+ protected:
+ /**
+ * Return a buffer to store image data in.
+ */
+ mozilla::UniquePtr<uint8_t[]> AllocateBuffer(uint32_t aSize);
+
+ mozilla::UniquePtr<uint8_t[]> mBuffer;
+ uint32_t mBufferSize;
+ gfx::IntSize mSize;
+ Data mData;
+ RefPtr<gfx::SourceSurface> mSourceSurface;
+};
+
+/**
+ * Currently, the data in a SourceSurfaceImage surface is treated as being in
+ * the device output color space. This class is very simple as all backends have
+ * to know about how to deal with drawing a cairo image.
+ */
+class SourceSurfaceImage final : public Image {
+ public:
+ already_AddRefed<gfx::SourceSurface> GetAsSourceSurface() override {
+ RefPtr<gfx::SourceSurface> surface(mSourceSurface);
+ return surface.forget();
+ }
+
+ void SetTextureFlags(TextureFlags aTextureFlags) {
+ mTextureFlags = aTextureFlags;
+ }
+ TextureClient* GetTextureClient(KnowsCompositor* aKnowsCompositor) override;
+
+ gfx::IntSize GetSize() const override { return mSize; }
+
+ SourceSurfaceImage(const gfx::IntSize& aSize,
+ gfx::SourceSurface* aSourceSurface);
+ explicit SourceSurfaceImage(gfx::SourceSurface* aSourceSurface);
+ virtual ~SourceSurfaceImage();
+
+ private:
+ gfx::IntSize mSize;
+ RefPtr<gfx::SourceSurface> mSourceSurface;
+ nsTHashMap<uint32_t, RefPtr<TextureClient>> mTextureClients;
+ TextureFlags mTextureFlags;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif
diff --git a/gfx/layers/ImageDataSerializer.cpp b/gfx/layers/ImageDataSerializer.cpp
new file mode 100644
index 0000000000..70d5493185
--- /dev/null
+++ b/gfx/layers/ImageDataSerializer.cpp
@@ -0,0 +1,359 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ImageDataSerializer.h"
+
+#include "YCbCrUtils.h" // for YCbCr conversions
+#include "gfx2DGlue.h" // for SurfaceFormatToImageFormat
+#include "mozilla/Assertions.h" // for MOZ_ASSERT, etc
+#include "mozilla/gfx/2D.h" // for DataSourceSurface, Factory
+#include "mozilla/gfx/Logging.h" // for gfxDebug
+#include "mozilla/gfx/Tools.h" // for GetAlignedStride, etc
+#include "mozilla/gfx/Types.h"
+#include "mozilla/mozalloc.h" // for operator delete, etc
+
+namespace mozilla {
+namespace layers {
+namespace ImageDataSerializer {
+
+using namespace gfx;
+
+int32_t ComputeRGBStride(SurfaceFormat aFormat, int32_t aWidth) {
+#ifdef XP_MACOSX
+ // Some drivers require an alignment of 32 bytes for efficient texture upload.
+ return GetAlignedStride<32>(aWidth, BytesPerPixel(aFormat));
+#else
+ return GetAlignedStride<4>(aWidth, BytesPerPixel(aFormat));
+#endif
+}
+
+int32_t GetRGBStride(const RGBDescriptor& aDescriptor) {
+ return ComputeRGBStride(aDescriptor.format(), aDescriptor.size().width);
+}
+
+uint32_t ComputeRGBBufferSize(IntSize aSize, SurfaceFormat aFormat) {
+ MOZ_ASSERT(aSize.height >= 0 && aSize.width >= 0);
+
+ // This takes care of checking whether there could be overflow
+ // with enough margin for the metadata.
+ if (!gfx::Factory::AllowedSurfaceSize(aSize)) {
+ return 0;
+ }
+
+ // Note we're passing height instad of the bpp parameter, but the end
+ // result is the same - and the bpp was already taken care of in the
+ // ComputeRGBStride function.
+ int32_t bufsize = GetAlignedStride<16>(ComputeRGBStride(aFormat, aSize.width),
+ aSize.height);
+
+ if (bufsize < 0) {
+ // This should not be possible thanks to Factory::AllowedSurfaceSize
+ return 0;
+ }
+
+ return bufsize;
+}
+
+// Minimum required shmem size in bytes
+uint32_t ComputeYCbCrBufferSize(const gfx::IntSize& aYSize, int32_t aYStride,
+ const gfx::IntSize& aCbCrSize,
+ int32_t aCbCrStride) {
+ MOZ_ASSERT(aYSize.height >= 0 && aYSize.width >= 0);
+
+ if (aYSize.height < 0 || aYSize.width < 0 || aCbCrSize.height < 0 ||
+ aCbCrSize.width < 0 ||
+ !gfx::Factory::AllowedSurfaceSize(IntSize(aYStride, aYSize.height)) ||
+ !gfx::Factory::AllowedSurfaceSize(
+ IntSize(aCbCrStride, aCbCrSize.height))) {
+ return 0;
+ }
+
+ // Overflow checks are performed in AllowedSurfaceSize
+ return GetAlignedStride<4>(aYSize.height, aYStride) +
+ 2 * GetAlignedStride<4>(aCbCrSize.height, aCbCrStride);
+}
+
+uint32_t ComputeYCbCrBufferSize(const gfx::IntSize& aYSize, int32_t aYStride,
+ const gfx::IntSize& aCbCrSize,
+ int32_t aCbCrStride, uint32_t aYOffset,
+ uint32_t aCbOffset, uint32_t aCrOffset) {
+ MOZ_ASSERT(aYSize.height >= 0 && aYSize.width >= 0);
+
+ if (aYSize.height < 0 || aYSize.width < 0 || aCbCrSize.height < 0 ||
+ aCbCrSize.width < 0 ||
+ !gfx::Factory::AllowedSurfaceSize(IntSize(aYStride, aYSize.height)) ||
+ !gfx::Factory::AllowedSurfaceSize(
+ IntSize(aCbCrStride, aCbCrSize.height))) {
+ return 0;
+ }
+
+ uint32_t yLength = GetAlignedStride<4>(aYStride, aYSize.height);
+ uint32_t cbCrLength = GetAlignedStride<4>(aCbCrStride, aCbCrSize.height);
+ if (yLength == 0 || cbCrLength == 0) {
+ return 0;
+ }
+
+ CheckedInt<uint32_t> yEnd = aYOffset;
+ yEnd += yLength;
+ CheckedInt<uint32_t> cbEnd = aCbOffset;
+ cbEnd += cbCrLength;
+ CheckedInt<uint32_t> crEnd = aCrOffset;
+ crEnd += cbCrLength;
+
+ if (!yEnd.isValid() || !cbEnd.isValid() || !crEnd.isValid() ||
+ yEnd.value() > aCbOffset || cbEnd.value() > aCrOffset) {
+ return 0;
+ }
+
+ return crEnd.value();
+}
+
+uint32_t ComputeYCbCrBufferSize(uint32_t aBufferSize) {
+ return GetAlignedStride<4>(aBufferSize, 1);
+}
+
+void ComputeYCbCrOffsets(int32_t yStride, int32_t yHeight, int32_t cbCrStride,
+ int32_t cbCrHeight, uint32_t& outYOffset,
+ uint32_t& outCbOffset, uint32_t& outCrOffset) {
+ outYOffset = 0;
+ outCbOffset = outYOffset + GetAlignedStride<4>(yStride, yHeight);
+ outCrOffset = outCbOffset + GetAlignedStride<4>(cbCrStride, cbCrHeight);
+}
+
+gfx::SurfaceFormat FormatFromBufferDescriptor(
+ const BufferDescriptor& aDescriptor) {
+ switch (aDescriptor.type()) {
+ case BufferDescriptor::TRGBDescriptor:
+ return aDescriptor.get_RGBDescriptor().format();
+ case BufferDescriptor::TYCbCrDescriptor:
+ return gfx::SurfaceFormat::YUV;
+ default:
+ MOZ_CRASH("GFX: FormatFromBufferDescriptor");
+ }
+}
+
+gfx::IntSize SizeFromBufferDescriptor(const BufferDescriptor& aDescriptor) {
+ switch (aDescriptor.type()) {
+ case BufferDescriptor::TRGBDescriptor:
+ return aDescriptor.get_RGBDescriptor().size();
+ case BufferDescriptor::TYCbCrDescriptor: {
+ return aDescriptor.get_YCbCrDescriptor().display().Size();
+ }
+ default:
+ MOZ_CRASH("GFX: SizeFromBufferDescriptor");
+ }
+}
+
+gfx::IntRect RectFromBufferDescriptor(const BufferDescriptor& aDescriptor) {
+ switch (aDescriptor.type()) {
+ case BufferDescriptor::TRGBDescriptor: {
+ auto size = aDescriptor.get_RGBDescriptor().size();
+ return gfx::IntRect(0, 0, size.Width(), size.Height());
+ }
+ case BufferDescriptor::TYCbCrDescriptor:
+ return aDescriptor.get_YCbCrDescriptor().display();
+ default:
+ MOZ_CRASH("GFX: RectFromBufferDescriptor");
+ }
+}
+
+Maybe<gfx::IntSize> YSizeFromBufferDescriptor(
+ const BufferDescriptor& aDescriptor) {
+ switch (aDescriptor.type()) {
+ case BufferDescriptor::TRGBDescriptor:
+ return Nothing();
+ case BufferDescriptor::TYCbCrDescriptor:
+ return Some(aDescriptor.get_YCbCrDescriptor().ySize());
+ default:
+ MOZ_CRASH("GFX: YSizeFromBufferDescriptor");
+ }
+}
+
+Maybe<gfx::IntSize> CbCrSizeFromBufferDescriptor(
+ const BufferDescriptor& aDescriptor) {
+ switch (aDescriptor.type()) {
+ case BufferDescriptor::TRGBDescriptor:
+ return Nothing();
+ case BufferDescriptor::TYCbCrDescriptor:
+ return Some(aDescriptor.get_YCbCrDescriptor().cbCrSize());
+ default:
+ MOZ_CRASH("GFX: CbCrSizeFromBufferDescriptor");
+ }
+}
+
+Maybe<int32_t> YStrideFromBufferDescriptor(
+ const BufferDescriptor& aDescriptor) {
+ switch (aDescriptor.type()) {
+ case BufferDescriptor::TRGBDescriptor:
+ return Nothing();
+ case BufferDescriptor::TYCbCrDescriptor:
+ return Some(aDescriptor.get_YCbCrDescriptor().yStride());
+ default:
+ MOZ_CRASH("GFX: YStrideFromBufferDescriptor");
+ }
+}
+
+Maybe<int32_t> CbCrStrideFromBufferDescriptor(
+ const BufferDescriptor& aDescriptor) {
+ switch (aDescriptor.type()) {
+ case BufferDescriptor::TRGBDescriptor:
+ return Nothing();
+ case BufferDescriptor::TYCbCrDescriptor:
+ return Some(aDescriptor.get_YCbCrDescriptor().cbCrStride());
+ default:
+ MOZ_CRASH("GFX: CbCrStrideFromBufferDescriptor");
+ }
+}
+
+Maybe<gfx::YUVColorSpace> YUVColorSpaceFromBufferDescriptor(
+ const BufferDescriptor& aDescriptor) {
+ switch (aDescriptor.type()) {
+ case BufferDescriptor::TRGBDescriptor:
+ return Nothing();
+ case BufferDescriptor::TYCbCrDescriptor:
+ return Some(aDescriptor.get_YCbCrDescriptor().yUVColorSpace());
+ default:
+ MOZ_CRASH("GFX: YUVColorSpaceFromBufferDescriptor");
+ }
+}
+
+Maybe<gfx::ColorDepth> ColorDepthFromBufferDescriptor(
+ const BufferDescriptor& aDescriptor) {
+ switch (aDescriptor.type()) {
+ case BufferDescriptor::TRGBDescriptor:
+ return Nothing();
+ case BufferDescriptor::TYCbCrDescriptor:
+ return Some(aDescriptor.get_YCbCrDescriptor().colorDepth());
+ default:
+ MOZ_CRASH("GFX: ColorDepthFromBufferDescriptor");
+ }
+}
+
+Maybe<gfx::ColorRange> ColorRangeFromBufferDescriptor(
+ const BufferDescriptor& aDescriptor) {
+ switch (aDescriptor.type()) {
+ case BufferDescriptor::TRGBDescriptor:
+ return Nothing();
+ case BufferDescriptor::TYCbCrDescriptor:
+ return Some(aDescriptor.get_YCbCrDescriptor().colorRange());
+ default:
+ MOZ_CRASH("GFX: YUVFullRangeFromBufferDescriptor");
+ }
+}
+
+Maybe<StereoMode> StereoModeFromBufferDescriptor(
+ const BufferDescriptor& aDescriptor) {
+ switch (aDescriptor.type()) {
+ case BufferDescriptor::TRGBDescriptor:
+ return Nothing();
+ case BufferDescriptor::TYCbCrDescriptor:
+ return Some(aDescriptor.get_YCbCrDescriptor().stereoMode());
+ default:
+ MOZ_CRASH("GFX: StereoModeFromBufferDescriptor");
+ }
+}
+
+Maybe<gfx::ChromaSubsampling> ChromaSubsamplingFromBufferDescriptor(
+ const BufferDescriptor& aDescriptor) {
+ switch (aDescriptor.type()) {
+ case BufferDescriptor::TRGBDescriptor:
+ return Nothing();
+ case BufferDescriptor::TYCbCrDescriptor:
+ return Some(aDescriptor.get_YCbCrDescriptor().chromaSubsampling());
+ default:
+ MOZ_CRASH("GFX: ChromaSubsamplingFromBufferDescriptor");
+ }
+}
+
+uint8_t* GetYChannel(uint8_t* aBuffer, const YCbCrDescriptor& aDescriptor) {
+ return aBuffer + aDescriptor.yOffset();
+}
+
+uint8_t* GetCbChannel(uint8_t* aBuffer, const YCbCrDescriptor& aDescriptor) {
+ return aBuffer + aDescriptor.cbOffset();
+}
+
+uint8_t* GetCrChannel(uint8_t* aBuffer, const YCbCrDescriptor& aDescriptor) {
+ return aBuffer + aDescriptor.crOffset();
+}
+
+already_AddRefed<DataSourceSurface> DataSourceSurfaceFromYCbCrDescriptor(
+ uint8_t* aBuffer, const YCbCrDescriptor& aDescriptor,
+ gfx::DataSourceSurface* aSurface) {
+ const gfx::IntRect display = aDescriptor.display();
+ const gfx::IntSize size = display.Size();
+ RefPtr<DataSourceSurface> result;
+ if (aSurface) {
+ MOZ_ASSERT(aSurface->GetSize() == size);
+ MOZ_ASSERT(aSurface->GetFormat() == gfx::SurfaceFormat::B8G8R8X8);
+ if (aSurface->GetSize() == size &&
+ aSurface->GetFormat() == gfx::SurfaceFormat::B8G8R8X8) {
+ result = aSurface;
+ }
+ }
+
+ if (!result) {
+ result =
+ Factory::CreateDataSourceSurface(size, gfx::SurfaceFormat::B8G8R8X8);
+ }
+ if (NS_WARN_IF(!result)) {
+ return nullptr;
+ }
+
+ DataSourceSurface::MappedSurface map;
+ if (NS_WARN_IF(!result->Map(DataSourceSurface::MapType::WRITE, &map))) {
+ return nullptr;
+ }
+
+ layers::PlanarYCbCrData ycbcrData;
+ ycbcrData.mYChannel = GetYChannel(aBuffer, aDescriptor);
+ ycbcrData.mYStride = aDescriptor.yStride();
+ ycbcrData.mCbChannel = GetCbChannel(aBuffer, aDescriptor);
+ ycbcrData.mCrChannel = GetCrChannel(aBuffer, aDescriptor);
+ ycbcrData.mCbCrStride = aDescriptor.cbCrStride();
+ ycbcrData.mPictureRect = aDescriptor.display();
+ ycbcrData.mYUVColorSpace = aDescriptor.yUVColorSpace();
+ ycbcrData.mColorDepth = aDescriptor.colorDepth();
+ ycbcrData.mChromaSubsampling = aDescriptor.chromaSubsampling();
+
+ gfx::ConvertYCbCrToRGB(ycbcrData, gfx::SurfaceFormat::B8G8R8X8, size,
+ map.mData, map.mStride);
+
+ result->Unmap();
+ return result.forget();
+}
+
+void ConvertAndScaleFromYCbCrDescriptor(uint8_t* aBuffer,
+ const YCbCrDescriptor& aDescriptor,
+ const gfx::SurfaceFormat& aDestFormat,
+ const gfx::IntSize& aDestSize,
+ unsigned char* aDestBuffer,
+ int32_t aStride) {
+ MOZ_ASSERT(aBuffer);
+
+ layers::PlanarYCbCrData ycbcrData;
+ ycbcrData.mYChannel = GetYChannel(aBuffer, aDescriptor);
+ ycbcrData.mYStride = aDescriptor.yStride();
+ ycbcrData.mCbChannel = GetCbChannel(aBuffer, aDescriptor);
+ ycbcrData.mCrChannel = GetCrChannel(aBuffer, aDescriptor);
+ ycbcrData.mCbCrStride = aDescriptor.cbCrStride();
+ ycbcrData.mPictureRect = aDescriptor.display();
+ ycbcrData.mYUVColorSpace = aDescriptor.yUVColorSpace();
+ ycbcrData.mColorDepth = aDescriptor.colorDepth();
+ ycbcrData.mChromaSubsampling = aDescriptor.chromaSubsampling();
+
+ gfx::ConvertYCbCrToRGB(ycbcrData, aDestFormat, aDestSize, aDestBuffer,
+ aStride);
+}
+
+gfx::IntSize GetCroppedCbCrSize(const YCbCrDescriptor& aDescriptor) {
+ return ChromaSize(aDescriptor.display().Size(),
+ aDescriptor.chromaSubsampling());
+}
+
+} // namespace ImageDataSerializer
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/ImageDataSerializer.h b/gfx/layers/ImageDataSerializer.h
new file mode 100644
index 0000000000..8584ea1a10
--- /dev/null
+++ b/gfx/layers/ImageDataSerializer.h
@@ -0,0 +1,113 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GFX_LAYERS_BLOBSURFACE_H
+#define GFX_LAYERS_BLOBSURFACE_H
+
+#include <stdint.h> // for uint8_t, uint32_t
+#include "mozilla/Attributes.h" // for MOZ_STACK_CLASS
+#include "mozilla/RefPtr.h" // for already_AddRefed
+#include "mozilla/gfx/Point.h" // for IntSize
+#include "mozilla/gfx/Rect.h" // for IntRect
+#include "mozilla/gfx/Types.h" // for SurfaceFormat
+#include "mozilla/layers/LayersSurfaces.h" // for SurfaceDescriptor
+
+namespace mozilla {
+namespace gfx {
+class DataSourceSurface;
+class DrawTarget;
+} // namespace gfx
+} // namespace mozilla
+
+namespace mozilla {
+namespace layers {
+
+namespace ImageDataSerializer {
+
+// RGB
+
+int32_t ComputeRGBStride(gfx::SurfaceFormat aFormat, int32_t aWidth);
+
+int32_t GetRGBStride(const RGBDescriptor& aDescriptor);
+
+uint32_t ComputeRGBBufferSize(gfx::IntSize aSize, gfx::SurfaceFormat aFormat);
+
+// YCbCr
+
+/// This function is meant as a helper to know how much shared memory we need
+/// to allocate in a shmem in order to place a shared YCbCr image blob of
+/// given dimensions.
+uint32_t ComputeYCbCrBufferSize(const gfx::IntSize& aYSize, int32_t aYStride,
+ const gfx::IntSize& aCbCrSize,
+ int32_t aCbCrStride);
+uint32_t ComputeYCbCrBufferSize(const gfx::IntSize& aYSize, int32_t aYStride,
+ const gfx::IntSize& aCbCrSize,
+ int32_t aCbCrStride, uint32_t aYOffset,
+ uint32_t aCbOffset, uint32_t aCrOffset);
+uint32_t ComputeYCbCrBufferSize(uint32_t aBufferSize);
+
+void ComputeYCbCrOffsets(int32_t yStride, int32_t yHeight, int32_t cbCrStride,
+ int32_t cbCrHeight, uint32_t& outYOffset,
+ uint32_t& outCbOffset, uint32_t& outCrOffset);
+
+gfx::SurfaceFormat FormatFromBufferDescriptor(
+ const BufferDescriptor& aDescriptor);
+
+gfx::IntSize SizeFromBufferDescriptor(const BufferDescriptor& aDescriptor);
+
+gfx::IntRect RectFromBufferDescriptor(const BufferDescriptor& aDescriptor);
+
+Maybe<gfx::IntSize> YSizeFromBufferDescriptor(
+ const BufferDescriptor& aDescriptor);
+
+Maybe<gfx::IntSize> CbCrSizeFromBufferDescriptor(
+ const BufferDescriptor& aDescriptor);
+
+Maybe<int32_t> YStrideFromBufferDescriptor(const BufferDescriptor& aDescriptor);
+
+Maybe<int32_t> CbCrStrideFromBufferDescriptor(
+ const BufferDescriptor& aDescriptor);
+
+Maybe<gfx::YUVColorSpace> YUVColorSpaceFromBufferDescriptor(
+ const BufferDescriptor& aDescriptor);
+
+Maybe<gfx::ColorDepth> ColorDepthFromBufferDescriptor(
+ const BufferDescriptor& aDescriptor);
+
+Maybe<gfx::ColorRange> ColorRangeFromBufferDescriptor(
+ const BufferDescriptor& aDescriptor);
+
+Maybe<StereoMode> StereoModeFromBufferDescriptor(
+ const BufferDescriptor& aDescriptor);
+
+Maybe<gfx::ChromaSubsampling> ChromaSubsamplingFromBufferDescriptor(
+ const BufferDescriptor& aDescriptor);
+
+uint8_t* GetYChannel(uint8_t* aBuffer, const YCbCrDescriptor& aDescriptor);
+
+uint8_t* GetCbChannel(uint8_t* aBuffer, const YCbCrDescriptor& aDescriptor);
+
+uint8_t* GetCrChannel(uint8_t* aBuffer, const YCbCrDescriptor& aDescriptor);
+
+already_AddRefed<gfx::DataSourceSurface> DataSourceSurfaceFromYCbCrDescriptor(
+ uint8_t* aBuffer, const YCbCrDescriptor& aDescriptor,
+ gfx::DataSourceSurface* aSurface = nullptr);
+
+void ConvertAndScaleFromYCbCrDescriptor(uint8_t* aBuffer,
+ const YCbCrDescriptor& aDescriptor,
+ const gfx::SurfaceFormat& aDestFormat,
+ const gfx::IntSize& aDestSize,
+ unsigned char* aDestBuffer,
+ int32_t aStride);
+
+gfx::IntSize GetCroppedCbCrSize(const YCbCrDescriptor& aDescriptor);
+
+} // namespace ImageDataSerializer
+
+} // namespace layers
+} // namespace mozilla
+
+#endif
diff --git a/gfx/layers/ImageTypes.h b/gfx/layers/ImageTypes.h
new file mode 100644
index 0000000000..96021ba9f6
--- /dev/null
+++ b/gfx/layers/ImageTypes.h
@@ -0,0 +1,130 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GFX_IMAGETYPES_H
+#define GFX_IMAGETYPES_H
+
+#include <stdint.h> // for uint32_t
+
+namespace mozilla {
+
+enum class ImageFormat {
+ /**
+ * The PLANAR_YCBCR format creates a PlanarYCbCrImage. All backends should
+ * support this format, because the Ogg video decoder depends on it.
+ * The maximum image width and height is 16384.
+ */
+ PLANAR_YCBCR,
+
+ /**
+ * The NV_IMAGE format creates a NVImage. The PLANAR_YCBCR together with this
+ * complete the YUV format family.
+ */
+ NV_IMAGE,
+
+ /**
+ * The SHARED_RGB format creates a SharedRGBImage, which stores RGB data in
+ * shared memory. Some Android hardware video decoders require this format.
+ * Currently only used on Android.
+ */
+ SHARED_RGB,
+
+ /**
+ * The MOZ2D_SURFACE format creates a SourceSurfaceImage. All backends should
+ * support this format, because video rendering sometimes requires it.
+ *
+ * This format is useful even though a PaintedLayer could be used.
+ * It makes it easy to render a cairo surface when another Image format
+ * could be used. It can also avoid copying the surface data in some
+ * cases.
+ */
+ MOZ2D_SURFACE,
+
+ /**
+ * A MacIOSurface object.
+ */
+ MAC_IOSURFACE,
+
+ /**
+ * An Android SurfaceTexture ID that can be shared across threads and
+ * processes.
+ */
+ SURFACE_TEXTURE,
+
+ /**
+ * The D3D9_RGB32_TEXTURE format creates a D3D9SurfaceImage, and wraps a
+ * IDirect3DTexture9 in RGB32 layout.
+ */
+ D3D9_RGB32_TEXTURE,
+
+ /**
+ * An Image type carries an opaque handle once for each stream.
+ * The opaque handle would be a platform specific identifier.
+ */
+ OVERLAY_IMAGE,
+
+ /**
+ * A share handle to a ID3D11Texture2D.
+ */
+ D3D11_SHARE_HANDLE_TEXTURE,
+
+ /**
+ * A wrapper of ID3D11Texture2D of IMFSample.
+ * Expected to be used in GPU process.
+ */
+ D3D11_TEXTURE_IMF_SAMPLE,
+
+ /**
+ * A wrapper around a drawable TextureClient.
+ */
+ TEXTURE_WRAPPER,
+
+ /**
+ * A D3D11 backed YUV image.
+ */
+ D3D11_YCBCR_IMAGE,
+
+ /**
+ * An opaque handle that refers to an Image stored in the GPU
+ * process.
+ */
+ GPU_VIDEO,
+
+ /**
+ * The DMABUF format creates a SharedDMABUFImage, which stores YUV
+ * data in DMABUF memory. Used by VAAPI decoder on Linux.
+ */
+ DMABUF,
+
+ /**
+ * A Wrapper of Dcomp surface handle, used by the windows media foundation
+ * media engine playback.
+ */
+ DCOMP_SURFACE,
+};
+
+enum class StereoMode {
+ MONO,
+ LEFT_RIGHT,
+ RIGHT_LEFT,
+ BOTTOM_TOP,
+ TOP_BOTTOM,
+ MAX,
+};
+
+namespace layers {
+
+typedef uint32_t ContainerFrameID;
+constexpr ContainerFrameID kContainerFrameID_Invalid = 0;
+
+typedef uint32_t ContainerProducerID;
+constexpr ContainerProducerID kContainerProducerID_Invalid = 0;
+
+} // namespace layers
+
+} // namespace mozilla
+
+#endif
diff --git a/gfx/layers/LayerUserData.h b/gfx/layers/LayerUserData.h
new file mode 100644
index 0000000000..206fefc998
--- /dev/null
+++ b/gfx/layers/LayerUserData.h
@@ -0,0 +1,28 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GFX_LAYERUSERDATA_H
+#define GFX_LAYERUSERDATA_H
+
+namespace mozilla {
+namespace layers {
+
+/**
+ * Base class for userdata objects attached to layers and layer managers.
+ *
+ * We define it here in a separate header so clients only need to include
+ * this header for their class definitions, rather than pulling in Layers.h.
+ * Everything else in Layers.h should be forward-declarable.
+ */
+class LayerUserData {
+ public:
+ virtual ~LayerUserData() = default;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif /* GFX_LAYERUSERDATA_H */
diff --git a/gfx/layers/LayersTypes.cpp b/gfx/layers/LayersTypes.cpp
new file mode 100644
index 0000000000..8d0ddb9d31
--- /dev/null
+++ b/gfx/layers/LayersTypes.cpp
@@ -0,0 +1,84 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "LayersTypes.h"
+
+#include <cinttypes>
+#include "nsPrintfCString.h"
+#include "mozilla/gfx/gfxVars.h"
+
+#ifdef XP_WIN
+# include "gfxConfig.h"
+# include "mozilla/StaticPrefs_gfx.h"
+#endif
+
+namespace mozilla {
+namespace layers {
+
+const char* kCompositionPayloadTypeNames[kCompositionPayloadTypeCount] = {
+ "KeyPress",
+ "APZScroll",
+ "APZPinchZoom",
+ "ContentPaint",
+ "MouseUpFollowedByClick",
+};
+
+const char* GetLayersBackendName(LayersBackend aBackend) {
+ switch (aBackend) {
+ case LayersBackend::LAYERS_NONE:
+ return "none";
+ case LayersBackend::LAYERS_WR:
+ if (gfx::gfxVars::UseSoftwareWebRender()) {
+#ifdef XP_WIN
+ if (gfx::gfxVars::AllowSoftwareWebRenderD3D11() &&
+ gfx::gfxConfig::IsEnabled(gfx::Feature::D3D11_COMPOSITING)) {
+ return "webrender_software_d3d11";
+ }
+#endif
+ return "webrender_software";
+ }
+ return "webrender";
+ default:
+ MOZ_ASSERT_UNREACHABLE("unknown layers backend");
+ return "unknown";
+ }
+}
+
+std::ostream& operator<<(std::ostream& aStream, const LayersId& aId) {
+ return aStream << nsPrintfCString("0x%" PRIx64, aId.mId).get();
+}
+
+/* static */
+CompositableHandle CompositableHandle::GetNext() {
+ static std::atomic<uint64_t> sCounter = 0;
+ return CompositableHandle{++sCounter};
+}
+
+/* static */
+RemoteTextureId RemoteTextureId::GetNext() {
+ static std::atomic<uint64_t> sCounter = 0;
+ return RemoteTextureId{++sCounter};
+}
+
+/* static */
+RemoteTextureOwnerId RemoteTextureOwnerId::GetNext() {
+ static std::atomic<uint64_t> sCounter = 0;
+ return RemoteTextureOwnerId{++sCounter};
+}
+
+/* static */
+GpuProcessTextureId GpuProcessTextureId::GetNext() {
+ if (!XRE_IsGPUProcess()) {
+ MOZ_ASSERT_UNREACHABLE("unexpected to be called");
+ return GpuProcessTextureId{};
+ }
+
+ static std::atomic<uint64_t> sCounter = 0;
+ return GpuProcessTextureId{++sCounter};
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/LayersTypes.h b/gfx/layers/LayersTypes.h
new file mode 100644
index 0000000000..f75953eba0
--- /dev/null
+++ b/gfx/layers/LayersTypes.h
@@ -0,0 +1,523 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GFX_LAYERSTYPES_H
+#define GFX_LAYERSTYPES_H
+
+#include <iosfwd> // for ostream
+#include <stdint.h> // for uint32_t
+#include <stdio.h> // FILE
+#include <tuple>
+
+#include "Units.h"
+#include "mozilla/DefineEnum.h" // for MOZ_DEFINE_ENUM_CLASS_WITH_BASE
+#include "mozilla/Maybe.h"
+#include "mozilla/TimeStamp.h" // for TimeStamp
+#include "nsRegion.h"
+#include "mozilla/EnumSet.h"
+
+#ifndef MOZ_LAYERS_HAVE_LOG
+# define MOZ_LAYERS_HAVE_LOG
+#endif
+#define MOZ_LAYERS_LOG(_args) \
+ MOZ_LOG(LayerManager::GetLog(), LogLevel::Debug, _args)
+#define MOZ_LAYERS_LOG_IF_SHADOWABLE(layer, _args)
+
+#define INVALID_OVERLAY -1
+
+// #define ENABLE_FRAME_LATENCY_LOG
+
+namespace IPC {
+template <typename T>
+struct ParamTraits;
+} // namespace IPC
+
+namespace mozilla {
+
+enum class StyleBorderStyle : uint8_t;
+
+namespace layers {
+
+class TextureHost;
+
+#undef NONE
+#undef OPAQUE
+
+struct LayersId {
+ uint64_t mId = 0;
+
+ bool IsValid() const { return mId != 0; }
+
+ // Allow explicit cast to a uint64_t for now
+ explicit operator uint64_t() const { return mId; }
+
+ // Implement some operators so this class can be used as a key in
+ // stdlib classes.
+ bool operator<(const LayersId& aOther) const { return mId < aOther.mId; }
+
+ bool operator==(const LayersId& aOther) const { return mId == aOther.mId; }
+
+ bool operator!=(const LayersId& aOther) const { return !(*this == aOther); }
+
+ friend std::ostream& operator<<(std::ostream& aStream, const LayersId& aId);
+
+ // Helper struct that allow this class to be used as a key in
+ // std::unordered_map like so:
+ // std::unordered_map<LayersId, ValueType, LayersId::HashFn> myMap;
+ struct HashFn {
+ std::size_t operator()(const LayersId& aKey) const {
+ return std::hash<uint64_t>{}(aKey.mId);
+ }
+ };
+};
+
+template <typename T>
+struct BaseTransactionId {
+ uint64_t mId = 0;
+
+ bool IsValid() const { return mId != 0; }
+
+ [[nodiscard]] BaseTransactionId<T> Next() const {
+ return BaseTransactionId<T>{mId + 1};
+ }
+
+ [[nodiscard]] BaseTransactionId<T> Prev() const {
+ return BaseTransactionId<T>{mId - 1};
+ }
+
+ int64_t operator-(const BaseTransactionId<T>& aOther) const {
+ return mId - aOther.mId;
+ }
+
+ // Allow explicit cast to a uint64_t for now
+ explicit operator uint64_t() const { return mId; }
+
+ bool operator<(const BaseTransactionId<T>& aOther) const {
+ return mId < aOther.mId;
+ }
+
+ bool operator<=(const BaseTransactionId<T>& aOther) const {
+ return mId <= aOther.mId;
+ }
+
+ bool operator>(const BaseTransactionId<T>& aOther) const {
+ return mId > aOther.mId;
+ }
+
+ bool operator>=(const BaseTransactionId<T>& aOther) const {
+ return mId >= aOther.mId;
+ }
+
+ bool operator==(const BaseTransactionId<T>& aOther) const {
+ return mId == aOther.mId;
+ }
+
+ bool operator!=(const BaseTransactionId<T>& aOther) const {
+ return mId != aOther.mId;
+ }
+};
+
+class TransactionIdType {};
+typedef BaseTransactionId<TransactionIdType> TransactionId;
+
+struct LayersObserverEpoch {
+ uint64_t mId;
+
+ [[nodiscard]] LayersObserverEpoch Next() const {
+ return LayersObserverEpoch{mId + 1};
+ }
+
+ bool operator<=(const LayersObserverEpoch& aOther) const {
+ return mId <= aOther.mId;
+ }
+
+ bool operator>=(const LayersObserverEpoch& aOther) const {
+ return mId >= aOther.mId;
+ }
+
+ bool operator==(const LayersObserverEpoch& aOther) const {
+ return mId == aOther.mId;
+ }
+
+ bool operator!=(const LayersObserverEpoch& aOther) const {
+ return mId != aOther.mId;
+ }
+};
+
+// CompositionOpportunityId is a counter that goes up every time we have an
+// opportunity to composite. It increments even on no-op composites (if nothing
+// has changed) and while compositing is paused. It does not skip values if a
+// composite is delayed. It is meaningful per window.
+// This counter is used to differentiate intentionally-skipped video frames from
+// unintentionally-skipped video frames: If CompositionOpportunityIds are
+// observed by the video in +1 increments, then the video was onscreen the
+// entire time and compositing was not paused. But if gaps in
+// CompositionOpportunityIds are observed, that must mean that the video was not
+// considered during some composition opportunities, because compositing was
+// paused or because the video was not part of the on-screen scene.
+class CompositionOpportunityType {};
+typedef BaseTransactionId<CompositionOpportunityType> CompositionOpportunityId;
+
+/// We make different decisions about resource allocation sizes in WebRender
+/// depending on whether we are going to render web pages or simpler
+/// content in the window.
+enum class WindowKind : int8_t { MAIN = 0, SECONDARY, LAST };
+
+enum class LayersBackend : int8_t { LAYERS_NONE = 0, LAYERS_WR, LAYERS_LAST };
+
+enum class WebRenderBackend : int8_t { HARDWARE = 0, SOFTWARE, LAST };
+
+enum class WebRenderCompositor : int8_t {
+ DRAW = 0,
+ DIRECT_COMPOSITION,
+ CORE_ANIMATION,
+ SOFTWARE,
+ D3D11,
+ OPENGL,
+ WAYLAND,
+ LAST
+};
+
+const char* GetLayersBackendName(LayersBackend aBackend);
+
+enum class TextureType : int8_t {
+ Unknown = 0,
+ D3D11,
+ MacIOSurface,
+ AndroidNativeWindow,
+ AndroidHardwareBuffer,
+ DMABUF,
+ EGLImage,
+ Last
+};
+
+enum class BufferMode : int8_t { BUFFER_NONE, BUFFERED };
+
+enum class DrawRegionClip : int8_t { DRAW, NONE };
+
+enum class SurfaceMode : int8_t {
+ SURFACE_NONE = 0,
+ SURFACE_OPAQUE,
+ SURFACE_SINGLE_CHANNEL_ALPHA,
+ SURFACE_COMPONENT_ALPHA
+};
+
+// clang-format off
+MOZ_DEFINE_ENUM_CLASS_WITH_BASE(
+ ScaleMode, int8_t, (
+ SCALE_NONE,
+ STRETCH
+// Unimplemented - PRESERVE_ASPECT_RATIO_CONTAIN
+));
+// clang-format on
+
+// Bit flags that go on a RefLayer and override the
+// event regions in the entire subtree below. This is needed for propagating
+// various flags across processes since the child-process layout code doesn't
+// know about parent-process listeners or CSS rules.
+enum EventRegionsOverride {
+ // The default, no flags set
+ NoOverride = 0,
+ // Treat all hit regions in the subtree as dispatch-to-content
+ ForceDispatchToContent = (1 << 0),
+ // Treat all hit regions in the subtree as empty
+ ForceEmptyHitRegion = (1 << 1),
+ // OR union of all valid bit flags, for use in BitFlagsEnumSerializer
+ ALL_BITS = (1 << 2) - 1
+};
+
+MOZ_ALWAYS_INLINE EventRegionsOverride operator|(EventRegionsOverride a,
+ EventRegionsOverride b) {
+ return (EventRegionsOverride)((int)a | (int)b);
+}
+
+MOZ_ALWAYS_INLINE EventRegionsOverride& operator|=(EventRegionsOverride& a,
+ EventRegionsOverride b) {
+ a = a | b;
+ return a;
+}
+
+// Flags used as an argument to functions that dump textures.
+enum TextureDumpMode {
+ Compress, // dump texture with LZ4 compression
+ DoNotCompress // dump texture uncompressed
+};
+
+// Corresponding bit masks for allowed touch behaviors
+// are defined in AllowedTouchBehavior
+typedef uint32_t TouchBehaviorFlags;
+
+// Some specialized typedefs of Matrix4x4Typed.
+typedef gfx::Matrix4x4Typed<LayerPixel, CSSTransformedLayerPixel>
+ CSSTransformMatrix;
+// Several different async transforms can contribute to a layer's transform
+// (specifically, an async animation can contribute a transform, and each APZC
+// that scrolls a layer can contribute async scroll/zoom and overscroll
+// transforms).
+// To try to model this with typed units, we represent individual async
+// transforms as ParentLayer -> ParentLayer transforms (aliased as
+// AsyncTransformComponentMatrix), and we represent the product of all of them
+// as a CSSTransformLayer -> ParentLayer transform (aliased as
+// AsyncTransformMatrix). To create an AsyncTransformMatrix from component
+// matrices, a ViewAs operation is needed. A MultipleAsyncTransforms
+// PixelCastJustification is provided for this purpose.
+typedef gfx::Matrix4x4Typed<ParentLayerPixel, ParentLayerPixel>
+ AsyncTransformComponentMatrix;
+typedef gfx::Matrix4x4Typed<CSSTransformedLayerPixel, ParentLayerPixel>
+ AsyncTransformMatrix;
+
+typedef Array<gfx::DeviceColor, 4> BorderColors;
+typedef Array<LayerSize, 4> BorderCorners;
+typedef Array<LayerCoord, 4> BorderWidths;
+typedef Array<StyleBorderStyle, 4> BorderStyles;
+
+typedef Maybe<LayerRect> MaybeLayerRect;
+
+// This is used to communicate Layers across IPC channels. The Handle is valid
+// for layers in the same PLayerTransaction. Handles are created by
+// ClientLayerManager, and are cached in LayerTransactionParent on first use.
+class LayerHandle final {
+ friend struct IPC::ParamTraits<mozilla::layers::LayerHandle>;
+
+ public:
+ LayerHandle() : mHandle(0) {}
+ LayerHandle(const LayerHandle& aOther) = default;
+ explicit LayerHandle(uint64_t aHandle) : mHandle(aHandle) {}
+ bool IsValid() const { return mHandle != 0; }
+ explicit operator bool() const { return IsValid(); }
+ bool operator==(const LayerHandle& aOther) const {
+ return mHandle == aOther.mHandle;
+ }
+ uint64_t Value() const { return mHandle; }
+
+ private:
+ uint64_t mHandle;
+};
+
+// This is used to communicate Compositables across IPC channels. The Handle is
+// valid for layers in the same PLayerTransaction or PImageBridge. Handles are
+// created by ClientLayerManager or ImageBridgeChild, and are cached in the
+// parent side on first use.
+class CompositableHandle final {
+ friend struct IPC::ParamTraits<mozilla::layers::CompositableHandle>;
+
+ public:
+ static CompositableHandle GetNext();
+
+ CompositableHandle() : mHandle(0) {}
+ CompositableHandle(const CompositableHandle& aOther) = default;
+ explicit CompositableHandle(uint64_t aHandle) : mHandle(aHandle) {}
+ bool IsValid() const { return mHandle != 0; }
+ explicit operator bool() const { return IsValid(); }
+ explicit operator uint64_t() const { return mHandle; }
+ bool operator==(const CompositableHandle& aOther) const {
+ return mHandle == aOther.mHandle;
+ }
+ bool operator!=(const CompositableHandle& aOther) const {
+ return !(*this == aOther);
+ }
+ uint64_t Value() const { return mHandle; }
+
+ private:
+ uint64_t mHandle;
+};
+
+enum class CompositableHandleOwner : uint8_t {
+ WebRenderBridge,
+ ImageBridge,
+};
+
+struct RemoteTextureId {
+ uint64_t mId = 0;
+
+ auto MutTiedFields() { return std::tie(mId); }
+
+ static RemoteTextureId GetNext();
+
+ static constexpr RemoteTextureId Max() { return RemoteTextureId{UINT64_MAX}; }
+
+ bool IsValid() const { return mId != 0; }
+
+ // Allow explicit cast to a uint64_t for now
+ explicit operator uint64_t() const { return mId; }
+
+ // Implement some operators so this class can be used as a key in
+ // stdlib classes.
+ bool operator<(const RemoteTextureId& aOther) const {
+ return mId < aOther.mId;
+ }
+
+ bool operator>(const RemoteTextureId& aOther) const {
+ return mId > aOther.mId;
+ }
+
+ bool operator==(const RemoteTextureId& aOther) const {
+ return mId == aOther.mId;
+ }
+
+ bool operator!=(const RemoteTextureId& aOther) const {
+ return !(*this == aOther);
+ }
+
+ bool operator>=(const RemoteTextureId& aOther) const {
+ return mId >= aOther.mId;
+ }
+
+ // Helper struct that allow this class to be used as a key in
+ // std::unordered_map like so:
+ // std::unordered_map<RemoteTextureId, ValueType, RemoteTextureId::HashFn>
+ // myMap;
+ struct HashFn {
+ std::size_t operator()(const RemoteTextureId aKey) const {
+ return std::hash<uint64_t>{}(aKey.mId);
+ }
+ };
+};
+
+struct RemoteTextureOwnerId {
+ uint64_t mId = 0;
+
+ auto MutTiedFields() { return std::tie(mId); }
+
+ static RemoteTextureOwnerId GetNext();
+
+ bool IsValid() const { return mId != 0; }
+
+ // Allow explicit cast to a uint64_t for now
+ explicit operator uint64_t() const { return mId; }
+
+ // Implement some operators so this class can be used as a key in
+ // stdlib classes.
+ bool operator<(const RemoteTextureOwnerId& aOther) const {
+ return mId < aOther.mId;
+ }
+
+ bool operator==(const RemoteTextureOwnerId& aOther) const {
+ return mId == aOther.mId;
+ }
+
+ bool operator!=(const RemoteTextureOwnerId& aOther) const {
+ return !(*this == aOther);
+ }
+
+ // Helper struct that allow this class to be used as a key in
+ // std::unordered_map like so:
+ // std::unordered_map<RemoteTextureOwnerId, ValueType,
+ // RemoteTextureOwnerId::HashFn> myMap;
+ struct HashFn {
+ std::size_t operator()(const RemoteTextureOwnerId aKey) const {
+ return std::hash<uint64_t>{}(aKey.mId);
+ }
+ };
+};
+
+// TextureId allocated in GPU process
+struct GpuProcessTextureId {
+ uint64_t mId = 0;
+
+ static GpuProcessTextureId GetNext();
+
+ bool IsValid() const { return mId != 0; }
+
+ // Allow explicit cast to a uint64_t for now
+ explicit operator uint64_t() const { return mId; }
+
+ bool operator==(const GpuProcessTextureId& aOther) const {
+ return mId == aOther.mId;
+ }
+
+ bool operator!=(const GpuProcessTextureId& aOther) const {
+ return !(*this == aOther);
+ }
+
+ // Helper struct that allow this class to be used as a key in
+ // std::unordered_map like so:
+ // std::unordered_map<GpuProcessTextureId, ValueType,
+ // GpuProcessTextureId::HashFn> myMap;
+ struct HashFn {
+ std::size_t operator()(const GpuProcessTextureId aKey) const {
+ return std::hash<uint64_t>{}(aKey.mId);
+ }
+ };
+};
+
+// clang-format off
+MOZ_DEFINE_ENUM_CLASS_WITH_BASE(ScrollDirection, uint8_t, (
+ eVertical,
+ eHorizontal
+));
+
+using ScrollDirections = EnumSet<ScrollDirection, uint8_t>;
+
+constexpr ScrollDirections EitherScrollDirection(ScrollDirection::eVertical,ScrollDirection::eHorizontal);
+constexpr ScrollDirections HorizontalScrollDirection(ScrollDirection::eHorizontal);
+constexpr ScrollDirections VerticalScrollDirection(ScrollDirection::eVertical);
+
+// Return the scroll directions which have a nonzero component in |aDelta|.
+template <typename Point>
+ScrollDirections DirectionsInDelta(const Point& aDelta) {
+ ScrollDirections result;
+ if (aDelta.x != 0) {
+ result += ScrollDirection::eHorizontal;
+ }
+ if (aDelta.y != 0) {
+ result += ScrollDirection::eVertical;
+ }
+ return result;
+}
+
+MOZ_DEFINE_ENUM_CLASS_WITH_BASE(CompositionPayloadType, uint8_t, (
+ /**
+ * A |CompositionPayload| with this type indicates a key press happened
+ * before composition and will be used to determine latency between key press
+ * and presentation in |mozilla::Telemetry::KEYPRESS_PRESENT_LATENCY|
+ */
+ eKeyPress,
+
+ /**
+ * A |CompositionPayload| with this type indicates that an APZ scroll event
+ * occurred that will be included in the composition.
+ */
+ eAPZScroll,
+
+ /**
+ * A |CompositionPayload| with this type indicates that an APZ pinch-to-zoom
+ * event occurred that will be included in the composition.
+ */
+ eAPZPinchZoom,
+
+ /**
+ * A |CompositionPayload| with this type indicates that content was painted
+ * that will be included in the composition.
+ */
+ eContentPaint,
+
+ /**
+ * A |CompositionPayload| with this type indicates a mouse up (which caused
+ * a click to happen) happened before composition and will be used to determine latency
+ * between mouse up and presentation in
+ * |mozilla::Telemetry::MOUSEUP_FOLLOWED_BY_CLICK_PRESENT_LATENCY|
+ */
+ eMouseUpFollowedByClick
+));
+// clang-format on
+
+extern const char* kCompositionPayloadTypeNames[kCompositionPayloadTypeCount];
+
+struct CompositionPayload {
+ bool operator==(const CompositionPayload& aOther) const {
+ return mType == aOther.mType && mTimeStamp == aOther.mTimeStamp;
+ }
+ /* The type of payload that is in this composition */
+ CompositionPayloadType mType;
+ /* When this payload was generated */
+ TimeStamp mTimeStamp;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif /* GFX_LAYERSTYPES_H */
diff --git a/gfx/layers/MacIOSurfaceHelpers.cpp b/gfx/layers/MacIOSurfaceHelpers.cpp
new file mode 100644
index 0000000000..55e85a73d2
--- /dev/null
+++ b/gfx/layers/MacIOSurfaceHelpers.cpp
@@ -0,0 +1,183 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "libyuv.h"
+#include "MacIOSurfaceHelpers.h"
+#include "mozilla/gfx/MacIOSurface.h"
+#include "YCbCrUtils.h"
+
+namespace mozilla {
+
+using namespace gfx;
+
+namespace layers {
+
+#define ALIGNED_32(x) ((x + 31) & ~31)
+#define ALIGNEDPTR_32(x) \
+ reinterpret_cast<uint8_t*>((reinterpret_cast<uintptr_t>(x) + 31) & ~31)
+
+static already_AddRefed<SourceSurface>
+CreateSourceSurfaceFromLockedMacIOSurface(MacIOSurface* aSurface) {
+ size_t bytesPerRow = aSurface->GetBytesPerRow();
+ size_t ioWidth = aSurface->GetDevicePixelWidth();
+ size_t ioHeight = aSurface->GetDevicePixelHeight();
+ IntSize size((int32_t)ioWidth, (int32_t)ioHeight);
+ SurfaceFormat ioFormat = aSurface->GetFormat();
+
+ if ((ioFormat == SurfaceFormat::NV12 || ioFormat == SurfaceFormat::YUV422) &&
+ (ioWidth > PlanarYCbCrImage::MAX_DIMENSION ||
+ ioHeight > PlanarYCbCrImage::MAX_DIMENSION)) {
+ return nullptr;
+ }
+
+ SurfaceFormat format =
+ (ioFormat == SurfaceFormat::NV12 || ioFormat == SurfaceFormat::YUV422)
+ ? SurfaceFormat::B8G8R8X8
+ : SurfaceFormat::B8G8R8A8;
+
+ RefPtr<DataSourceSurface> dataSurface =
+ Factory::CreateDataSourceSurface(size, format);
+ if (NS_WARN_IF(!dataSurface)) {
+ return nullptr;
+ }
+
+ DataSourceSurface::MappedSurface mappedSurface;
+ if (!dataSurface->Map(DataSourceSurface::WRITE, &mappedSurface)) {
+ return nullptr;
+ }
+
+ if (ioFormat == SurfaceFormat::NV12) {
+ /* Extract and separate the CbCr planes */
+ size_t cbCrStride = aSurface->GetBytesPerRow(1);
+ size_t cbCrWidth = aSurface->GetDevicePixelWidth(1);
+ size_t cbCrHeight = aSurface->GetDevicePixelHeight(1);
+
+ auto cbPlane = MakeUnique<uint8_t[]>(cbCrWidth * cbCrHeight);
+ auto crPlane = MakeUnique<uint8_t[]>(cbCrWidth * cbCrHeight);
+
+ uint8_t* src = (uint8_t*)aSurface->GetBaseAddressOfPlane(1);
+ uint8_t* cbDest = cbPlane.get();
+ uint8_t* crDest = crPlane.get();
+
+ for (size_t i = 0; i < cbCrHeight; i++) {
+ uint8_t* rowSrc = src + cbCrStride * i;
+ for (size_t j = 0; j < cbCrWidth; j++) {
+ *cbDest = *rowSrc;
+ cbDest++;
+ rowSrc++;
+ *crDest = *rowSrc;
+ crDest++;
+ rowSrc++;
+ }
+ }
+
+ /* Convert to RGB */
+ PlanarYCbCrData data;
+ data.mYChannel = (uint8_t*)aSurface->GetBaseAddressOfPlane(0);
+ data.mYStride = aSurface->GetBytesPerRow(0);
+ data.mCbChannel = cbPlane.get();
+ data.mCrChannel = crPlane.get();
+ data.mCbCrStride = cbCrWidth;
+ data.mPictureRect = IntRect(IntPoint(0, 0), size);
+ data.mYUVColorSpace = aSurface->GetYUVColorSpace();
+ data.mColorPrimaries = aSurface->mColorPrimaries;
+ data.mColorRange = aSurface->IsFullRange() ? gfx::ColorRange::FULL
+ : gfx::ColorRange::LIMITED;
+ data.mChromaSubsampling = ChromaSubsampling::HALF_WIDTH_AND_HEIGHT;
+
+ ConvertYCbCrToRGB(data, SurfaceFormat::B8G8R8X8, size, mappedSurface.mData,
+ mappedSurface.mStride);
+ } else if (ioFormat == SurfaceFormat::YUV422) {
+ if (ioWidth == ALIGNED_32(ioWidth)) {
+ // Optimization when width is aligned to 32.
+ libyuv::ConvertToARGB((uint8_t*)aSurface->GetBaseAddress(),
+ 0 /* not used */, mappedSurface.mData,
+ mappedSurface.mStride, 0, 0, size.width,
+ size.height, size.width, size.height,
+ libyuv::kRotate0, libyuv::FOURCC_YUYV);
+ } else {
+ /* Convert to YV16 */
+ size_t cbCrWidth = (ioWidth + 1) >> 1;
+ size_t cbCrHeight = ioHeight;
+ // Ensure our stride is a multiple of 32 to allow for memory aligned rows.
+ size_t cbCrStride = ALIGNED_32(cbCrWidth);
+ size_t strideDelta = cbCrStride - cbCrWidth;
+ MOZ_ASSERT(strideDelta <= 31);
+
+ auto yPlane = MakeUnique<uint8_t[]>(cbCrStride * 2 * ioHeight + 31);
+ auto cbPlane = MakeUnique<uint8_t[]>(cbCrStride * cbCrHeight + 31);
+ auto crPlane = MakeUnique<uint8_t[]>(cbCrStride * cbCrHeight + 31);
+
+ uint8_t* src = (uint8_t*)aSurface->GetBaseAddress();
+ uint8_t* yDest = ALIGNEDPTR_32(yPlane.get());
+ uint8_t* cbDest = ALIGNEDPTR_32(cbPlane.get());
+ uint8_t* crDest = ALIGNEDPTR_32(crPlane.get());
+
+ for (size_t i = 0; i < ioHeight; i++) {
+ uint8_t* rowSrc = src + bytesPerRow * i;
+ for (size_t j = 0; j < cbCrWidth; j++) {
+ *yDest = *rowSrc;
+ yDest++;
+ rowSrc++;
+ *cbDest = *rowSrc;
+ cbDest++;
+ rowSrc++;
+ *yDest = *rowSrc;
+ yDest++;
+ rowSrc++;
+ *crDest = *rowSrc;
+ crDest++;
+ rowSrc++;
+ }
+ if (strideDelta) {
+ cbDest += strideDelta;
+ crDest += strideDelta;
+ yDest += strideDelta << 1;
+ }
+ }
+
+ /* Convert to RGB */
+ PlanarYCbCrData data;
+ data.mYChannel = ALIGNEDPTR_32(yPlane.get());
+ data.mYStride = cbCrStride * 2;
+ data.mCbChannel = ALIGNEDPTR_32(cbPlane.get());
+ data.mCrChannel = ALIGNEDPTR_32(crPlane.get());
+ data.mCbCrStride = cbCrStride;
+ data.mPictureRect = IntRect(IntPoint(0, 0), size);
+ data.mYUVColorSpace = aSurface->GetYUVColorSpace();
+ data.mColorPrimaries = aSurface->mColorPrimaries;
+ data.mColorRange = aSurface->IsFullRange() ? gfx::ColorRange::FULL
+ : gfx::ColorRange::LIMITED;
+ data.mChromaSubsampling = ChromaSubsampling::HALF_WIDTH;
+
+ ConvertYCbCrToRGB(data, SurfaceFormat::B8G8R8X8, size,
+ mappedSurface.mData, mappedSurface.mStride);
+ }
+ } else {
+ unsigned char* ioData = (unsigned char*)aSurface->GetBaseAddress();
+
+ for (size_t i = 0; i < ioHeight; ++i) {
+ memcpy(mappedSurface.mData + i * mappedSurface.mStride,
+ ioData + i * bytesPerRow, ioWidth * 4);
+ }
+ }
+
+ dataSurface->Unmap();
+
+ return dataSurface.forget();
+}
+
+already_AddRefed<SourceSurface> CreateSourceSurfaceFromMacIOSurface(
+ MacIOSurface* aSurface) {
+ aSurface->Lock();
+ RefPtr<SourceSurface> result =
+ CreateSourceSurfaceFromLockedMacIOSurface(aSurface);
+ aSurface->Unlock();
+ return result.forget();
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/MacIOSurfaceHelpers.h b/gfx/layers/MacIOSurfaceHelpers.h
new file mode 100644
index 0000000000..9aa0a299b4
--- /dev/null
+++ b/gfx/layers/MacIOSurfaceHelpers.h
@@ -0,0 +1,30 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GFX_MACIOSURFACEHELPERS_H
+#define GFX_MACIOSURFACEHELPERS_H
+
+class MacIOSurface;
+template <class T>
+struct already_AddRefed;
+
+namespace mozilla {
+
+namespace gfx {
+class SourceSurface;
+}
+
+namespace layers {
+
+// Unlike MacIOSurface::GetAsSurface, this also handles IOSurface formats
+// with multiple planes and does YCbCr to RGB conversion, if necessary.
+already_AddRefed<gfx::SourceSurface> CreateSourceSurfaceFromMacIOSurface(
+ MacIOSurface* aSurface);
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // GFX_MACIOSURFACEHELPERS_H
diff --git a/gfx/layers/MacIOSurfaceImage.cpp b/gfx/layers/MacIOSurfaceImage.cpp
new file mode 100644
index 0000000000..08163321b7
--- /dev/null
+++ b/gfx/layers/MacIOSurfaceImage.cpp
@@ -0,0 +1,250 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MacIOSurfaceHelpers.h"
+#include "MacIOSurfaceImage.h"
+#include "gfxPlatform.h"
+#include "mozilla/layers/CompositableClient.h"
+#include "mozilla/layers/CompositableForwarder.h"
+#include "mozilla/layers/MacIOSurfaceTextureClientOGL.h"
+#include "mozilla/layers/TextureForwarder.h"
+#include "mozilla/StaticPrefs_layers.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/Unused.h"
+#include "YCbCrUtils.h"
+
+using namespace mozilla::layers;
+using namespace mozilla::gfx;
+
+TextureClient* MacIOSurfaceImage::GetTextureClient(
+ KnowsCompositor* aKnowsCompositor) {
+ if (!mTextureClient) {
+ BackendType backend = BackendType::NONE;
+ TextureFlags flags =
+ IsDRM() ? TextureFlags::DRM_SOURCE : TextureFlags::DEFAULT;
+ mTextureClient = TextureClient::CreateWithData(
+ MacIOSurfaceTextureData::Create(mSurface, backend), flags,
+ aKnowsCompositor->GetTextureForwarder());
+ }
+ return mTextureClient;
+}
+
+ColorDepth MacIOSurfaceImage::GetColorDepth() const {
+ if (!mSurface) {
+ return gfx::ColorDepth::COLOR_8;
+ }
+ return mSurface->GetColorDepth();
+}
+
+already_AddRefed<SourceSurface> MacIOSurfaceImage::GetAsSourceSurface() {
+ return CreateSourceSurfaceFromMacIOSurface(mSurface);
+}
+
+static inline uint16_t safeShift10BitBy6(const uint16_t& a10BitLSB) {
+ // a10BitLSB is a 10-bit value packed into the least significant bits of
+ // a 16 bit value. This function asserts that the 6 MSBs are zero, then
+ // shifts the 10 LSBs by 6 to become the MSBs.
+ MOZ_ASSERT((a10BitLSB & 0b1111'1100'0000'0000) == 0);
+ return a10BitLSB << 6;
+}
+
+bool MacIOSurfaceImage::SetData(ImageContainer* aContainer,
+ const PlanarYCbCrData& aData) {
+ MOZ_ASSERT(!mSurface);
+
+ if (aData.mYSkip != 0 || aData.mCbSkip != 0 || aData.mCrSkip != 0 ||
+ !(aData.mYUVColorSpace == YUVColorSpace::BT601 ||
+ aData.mYUVColorSpace == YUVColorSpace::BT709 ||
+ aData.mYUVColorSpace == YUVColorSpace::BT2020) ||
+ !(aData.mColorRange == ColorRange::FULL ||
+ aData.mColorRange == ColorRange::LIMITED) ||
+ !(aData.mColorDepth == ColorDepth::COLOR_8 ||
+ aData.mColorDepth == ColorDepth::COLOR_10)) {
+ return false;
+ }
+
+ // We can only support 4:2:2 and 4:2:0 formats currently.
+ switch (aData.mChromaSubsampling) {
+ case ChromaSubsampling::HALF_WIDTH:
+ case ChromaSubsampling::HALF_WIDTH_AND_HEIGHT:
+ break;
+ default:
+ return false;
+ }
+
+ RefPtr<MacIOSurfaceRecycleAllocator> allocator =
+ aContainer->GetMacIOSurfaceRecycleAllocator();
+
+ auto ySize = aData.YDataSize();
+ auto cbcrSize = aData.CbCrDataSize();
+ RefPtr<MacIOSurface> surf = allocator->Allocate(
+ ySize, cbcrSize, aData.mYUVColorSpace, aData.mTransferFunction,
+ aData.mColorRange, aData.mColorDepth);
+
+ surf->Lock(false);
+
+ if (surf->GetFormat() == SurfaceFormat::YUV422) {
+ // If the CbCrSize's height is half of the YSize's height, then we'll
+ // need to duplicate the CbCr data on every second row.
+ size_t heightScale = ySize.height / cbcrSize.height;
+
+ // The underlying IOSurface has format
+ // kCVPixelFormatType_422YpCbCr8FullRange or
+ // kCVPixelFormatType_422YpCbCr8_yuvs, which uses a 4:2:2 Y`0 Cb Y`1 Cr
+ // layout. See CVPixelBuffer.h for the full list of format descriptions.
+ MOZ_ASSERT(ySize.height > 0);
+ uint8_t* dst = (uint8_t*)surf->GetBaseAddressOfPlane(0);
+ size_t stride = surf->GetBytesPerRow(0);
+ for (size_t i = 0; i < (size_t)ySize.height; i++) {
+ // Compute the row addresses. If the input was 4:2:0, then
+ // we divide i by 2, so that each source row of CbCr maps to
+ // two dest rows.
+ uint8_t* rowYSrc = aData.mYChannel + aData.mYStride * i;
+ uint8_t* rowCbSrc =
+ aData.mCbChannel + aData.mCbCrStride * (i / heightScale);
+ uint8_t* rowCrSrc =
+ aData.mCrChannel + aData.mCbCrStride * (i / heightScale);
+ uint8_t* rowDst = dst + stride * i;
+
+ // Iterate across the CbCr width (which we have guaranteed to be half of
+ // the surface width), and write two 16bit pixels each time.
+ for (size_t j = 0; j < (size_t)cbcrSize.width; j++) {
+ *rowDst = *rowYSrc;
+ rowDst++;
+ rowYSrc++;
+
+ *rowDst = *rowCbSrc;
+ rowDst++;
+ rowCbSrc++;
+
+ *rowDst = *rowYSrc;
+ rowDst++;
+ rowYSrc++;
+
+ *rowDst = *rowCrSrc;
+ rowDst++;
+ rowCrSrc++;
+ }
+ }
+ } else if (surf->GetFormat() == SurfaceFormat::NV12) {
+ MOZ_ASSERT(ySize.height > 0);
+ uint8_t* dst = (uint8_t*)surf->GetBaseAddressOfPlane(0);
+ size_t stride = surf->GetBytesPerRow(0);
+ for (size_t i = 0; i < (size_t)ySize.height; i++) {
+ uint8_t* rowSrc = aData.mYChannel + aData.mYStride * i;
+ uint8_t* rowDst = dst + stride * i;
+ memcpy(rowDst, rowSrc, ySize.width);
+ }
+
+ // Copy and interleave the Cb and Cr channels.
+ MOZ_ASSERT(cbcrSize.height > 0);
+ dst = (uint8_t*)surf->GetBaseAddressOfPlane(1);
+ stride = surf->GetBytesPerRow(1);
+ for (size_t i = 0; i < (size_t)cbcrSize.height; i++) {
+ uint8_t* rowCbSrc = aData.mCbChannel + aData.mCbCrStride * i;
+ uint8_t* rowCrSrc = aData.mCrChannel + aData.mCbCrStride * i;
+ uint8_t* rowDst = dst + stride * i;
+
+ for (size_t j = 0; j < (size_t)cbcrSize.width; j++) {
+ *rowDst = *rowCbSrc;
+ rowDst++;
+ rowCbSrc++;
+
+ *rowDst = *rowCrSrc;
+ rowDst++;
+ rowCrSrc++;
+ }
+ }
+ } else if (surf->GetFormat() == SurfaceFormat::P010) {
+ MOZ_ASSERT(ySize.height > 0);
+ auto dst = reinterpret_cast<uint16_t*>(surf->GetBaseAddressOfPlane(0));
+ size_t stride = surf->GetBytesPerRow(0) / 2;
+ for (size_t i = 0; i < (size_t)ySize.height; i++) {
+ auto rowSrc = reinterpret_cast<const uint16_t*>(aData.mYChannel +
+ aData.mYStride * i);
+ auto rowDst = dst + stride * i;
+
+ for (const auto j : IntegerRange(ySize.width)) {
+ Unused << j;
+
+ *rowDst = safeShift10BitBy6(*rowSrc);
+ rowDst++;
+ rowSrc++;
+ }
+ }
+
+ // Copy and interleave the Cb and Cr channels.
+ MOZ_ASSERT(cbcrSize.height > 0);
+ dst = (uint16_t*)surf->GetBaseAddressOfPlane(1);
+ stride = surf->GetBytesPerRow(1) / 2;
+ for (size_t i = 0; i < (size_t)cbcrSize.height; i++) {
+ uint16_t* rowCbSrc =
+ (uint16_t*)(aData.mCbChannel + aData.mCbCrStride * i);
+ uint16_t* rowCrSrc =
+ (uint16_t*)(aData.mCrChannel + aData.mCbCrStride * i);
+ uint16_t* rowDst = dst + stride * i;
+
+ for (const auto j : IntegerRange(cbcrSize.width)) {
+ Unused << j;
+
+ *rowDst = safeShift10BitBy6(*rowCbSrc);
+ rowDst++;
+ rowCbSrc++;
+
+ *rowDst = safeShift10BitBy6(*rowCrSrc);
+ rowDst++;
+ rowCrSrc++;
+ }
+ }
+ }
+
+ surf->Unlock(false);
+ mSurface = surf;
+ mPictureRect = aData.mPictureRect;
+ return true;
+}
+
+already_AddRefed<MacIOSurface> MacIOSurfaceRecycleAllocator::Allocate(
+ const gfx::IntSize aYSize, const gfx::IntSize& aCbCrSize,
+ gfx::YUVColorSpace aYUVColorSpace, gfx::TransferFunction aTransferFunction,
+ gfx::ColorRange aColorRange, gfx::ColorDepth aColorDepth) {
+ nsTArray<CFTypeRefPtr<IOSurfaceRef>> surfaces = std::move(mSurfaces);
+ RefPtr<MacIOSurface> result;
+ for (auto& surf : surfaces) {
+ // If the surface size has changed, then discard any surfaces of the old
+ // size.
+ if (::IOSurfaceGetWidthOfPlane(surf.get(), 0) != (size_t)aYSize.width ||
+ ::IOSurfaceGetHeightOfPlane(surf.get(), 0) != (size_t)aYSize.height) {
+ continue;
+ }
+
+ // Only construct a MacIOSurface object when we find one that isn't
+ // in-use, since the constructor adds a usage ref.
+ if (!result && !::IOSurfaceIsInUse(surf.get())) {
+ result = new MacIOSurface(surf, false, aYUVColorSpace);
+ }
+
+ mSurfaces.AppendElement(surf);
+ }
+
+ if (!result) {
+ if (StaticPrefs::layers_iosurfaceimage_use_nv12_AtStartup()) {
+ result = MacIOSurface::CreateNV12OrP010Surface(
+ aYSize, aCbCrSize, aYUVColorSpace, aTransferFunction, aColorRange,
+ aColorDepth);
+ } else {
+ result = MacIOSurface::CreateYUV422Surface(aYSize, aYUVColorSpace,
+ aColorRange);
+ }
+
+ if (mSurfaces.Length() <
+ StaticPrefs::layers_iosurfaceimage_recycle_limit()) {
+ mSurfaces.AppendElement(result->GetIOSurfaceRef());
+ }
+ }
+
+ return result.forget();
+}
diff --git a/gfx/layers/MacIOSurfaceImage.h b/gfx/layers/MacIOSurfaceImage.h
new file mode 100644
index 0000000000..99c004e424
--- /dev/null
+++ b/gfx/layers/MacIOSurfaceImage.h
@@ -0,0 +1,74 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GFX_MACIOSURFACEIMAGE_H
+#define GFX_MACIOSURFACEIMAGE_H
+
+#include "ImageContainer.h"
+#include "mozilla/gfx/MacIOSurface.h"
+#include "mozilla/gfx/Point.h"
+#include "mozilla/layers/TextureClient.h"
+
+namespace mozilla {
+
+namespace layers {
+
+class MacIOSurfaceImage : public Image {
+ public:
+ explicit MacIOSurfaceImage(MacIOSurface* aSurface)
+ : Image(nullptr, ImageFormat::MAC_IOSURFACE), mSurface(aSurface) {
+ if (aSurface) {
+ mPictureRect = gfx::IntRect(
+ gfx::IntPoint{}, gfx::IntSize(aSurface->GetDevicePixelWidth(0),
+ aSurface->GetDevicePixelHeight(0)));
+ }
+ }
+
+ bool SetData(ImageContainer* aContainer, const PlanarYCbCrData& aData);
+
+ MacIOSurface* GetSurface() { return mSurface; }
+
+ gfx::IntSize GetSize() const override {
+ return gfx::IntSize::Truncate(mSurface->GetDevicePixelWidth(),
+ mSurface->GetDevicePixelHeight());
+ }
+
+ already_AddRefed<gfx::SourceSurface> GetAsSourceSurface() override;
+
+ TextureClient* GetTextureClient(KnowsCompositor* aKnowsCompositor) override;
+
+ MacIOSurfaceImage* AsMacIOSurfaceImage() override { return this; }
+
+ gfx::IntRect GetPictureRect() const override { return mPictureRect; }
+
+ gfx::ColorDepth GetColorDepth() const override;
+
+ private:
+ RefPtr<MacIOSurface> mSurface;
+ RefPtr<TextureClient> mTextureClient;
+ gfx::IntRect mPictureRect;
+};
+
+class MacIOSurfaceRecycleAllocator {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MacIOSurfaceRecycleAllocator)
+
+ already_AddRefed<MacIOSurface> Allocate(
+ const gfx::IntSize aYSize, const gfx::IntSize& aCbCrSize,
+ gfx::YUVColorSpace aYUVColorSpace,
+ gfx::TransferFunction aTransferFunction, gfx::ColorRange aColorRange,
+ gfx::ColorDepth aColorDepth);
+
+ private:
+ ~MacIOSurfaceRecycleAllocator() = default;
+
+ nsTArray<CFTypeRefPtr<IOSurfaceRef>> mSurfaces;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // GFX_SHAREDTEXTUREIMAGE_H
diff --git a/gfx/layers/MemoryPressureObserver.cpp b/gfx/layers/MemoryPressureObserver.cpp
new file mode 100644
index 0000000000..16c9ea4b1e
--- /dev/null
+++ b/gfx/layers/MemoryPressureObserver.cpp
@@ -0,0 +1,77 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MemoryPressureObserver.h"
+
+#include "mozilla/Services.h"
+#include "nsCOMPtr.h"
+#include "nsDependentString.h"
+#include "nsIObserverService.h"
+#include "nsLiteralString.h"
+
+namespace mozilla {
+namespace layers {
+
+MemoryPressureObserver::MemoryPressureObserver(
+ MemoryPressureListener* aListener)
+ : mListener(aListener) {}
+
+MemoryPressureObserver::~MemoryPressureObserver() {
+ // If this assertion is hit we probably forgot to unregister the observer.
+ MOZ_ASSERT(!mListener);
+}
+
+already_AddRefed<MemoryPressureObserver> MemoryPressureObserver::Create(
+ MemoryPressureListener* aListener) {
+ nsCOMPtr<nsIObserverService> service = services::GetObserverService();
+
+ if (!service) {
+ return nullptr;
+ }
+
+ RefPtr<MemoryPressureObserver> observer =
+ new MemoryPressureObserver(aListener);
+
+ bool useWeakRef = false;
+ service->AddObserver(observer, "memory-pressure", useWeakRef);
+
+ return observer.forget();
+}
+
+void MemoryPressureObserver::Unregister() {
+ if (!mListener) {
+ return;
+ }
+
+ nsCOMPtr<nsIObserverService> service = services::GetObserverService();
+ if (service) {
+ service->RemoveObserver(this, "memory-pressure");
+ }
+
+ mListener = nullptr;
+}
+
+NS_IMETHODIMP
+MemoryPressureObserver::Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData) {
+ if (mListener && strcmp(aTopic, "memory-pressure") == 0) {
+ MemoryPressureReason reason = MemoryPressureReason::LOW_MEMORY;
+ auto reason_string = nsDependentString(aData);
+ if (StringBeginsWith(reason_string, u"low-memory-ongoing"_ns)) {
+ reason = MemoryPressureReason::LOW_MEMORY_ONGOING;
+ } else if (StringBeginsWith(reason_string, u"heap-minimize"_ns)) {
+ reason = MemoryPressureReason::HEAP_MINIMIZE;
+ }
+ mListener->OnMemoryPressure(reason);
+ }
+
+ return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS(MemoryPressureObserver, nsIObserver)
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/MemoryPressureObserver.h b/gfx/layers/MemoryPressureObserver.h
new file mode 100644
index 0000000000..27048c99ed
--- /dev/null
+++ b/gfx/layers/MemoryPressureObserver.h
@@ -0,0 +1,59 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: sw=2 ts=8 et :
+ */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_LAYERS_MEMORYPRESSUREOBSERVER_H
+#define MOZILLA_LAYERS_MEMORYPRESSUREOBSERVER_H
+
+#include "nsIObserver.h"
+
+namespace mozilla {
+namespace layers {
+
+// A simple memory pressure observer implementation born out of the realization
+// that almost all of our memory pressure observers do exactly the same thing.
+//
+// The intended way to use it is to have the class that nees to react on memory
+// pressure inherit the MemoryPressureListener interface and own a strong
+// reference to a MemoryPressureListener object.
+// Call Unregister on the listener in the destructor of your class or whenever
+// you do not which to receive the notification anymore, otherwise the listener
+// will be held alive by the observer service (leak) and keep a dangling pointer
+// to your class.
+
+/// See nsIMemory.idl
+enum class MemoryPressureReason {
+ LOW_MEMORY,
+ LOW_MEMORY_ONGOING,
+ HEAP_MINIMIZE,
+};
+
+class MemoryPressureListener {
+ public:
+ virtual void OnMemoryPressure(MemoryPressureReason aWhy) = 0;
+};
+
+class MemoryPressureObserver final : public nsIObserver {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIOBSERVER
+
+ // Returns null if anything goes wrong.
+ static already_AddRefed<MemoryPressureObserver> Create(
+ MemoryPressureListener* aListener);
+
+ void Unregister();
+
+ private:
+ explicit MemoryPressureObserver(MemoryPressureListener* aListener);
+ virtual ~MemoryPressureObserver();
+ MemoryPressureListener* mListener;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif
diff --git a/gfx/layers/NativeLayer.h b/gfx/layers/NativeLayer.h
new file mode 100644
index 0000000000..5cdb51fd35
--- /dev/null
+++ b/gfx/layers/NativeLayer.h
@@ -0,0 +1,245 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_NativeLayer_h
+#define mozilla_layers_NativeLayer_h
+
+#include "mozilla/Maybe.h"
+#include "mozilla/Range.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/gfx/Types.h"
+#include "mozilla/layers/ScreenshotGrabber.h"
+
+#include "GLTypes.h"
+#include "nsISupportsImpl.h"
+#include "nsRegion.h"
+
+namespace mozilla {
+
+namespace gl {
+class GLContext;
+} // namespace gl
+
+namespace wr {
+class RenderTextureHost;
+}
+
+namespace layers {
+
+class NativeLayer;
+class NativeLayerCA;
+class NativeLayerWayland;
+class NativeLayerRootCA;
+class NativeLayerRootWayland;
+class NativeLayerRootSnapshotter;
+class SurfacePoolHandle;
+
+// NativeLayerRoot and NativeLayer allow building up a flat layer "tree" of
+// sibling layers. These layers provide a cross-platform abstraction for the
+// platform's native layers, such as CoreAnimation layers on macOS.
+// Every layer has a rectangle that describes its position and size in the
+// window. The native layer root is usually be created by the window, and then
+// the compositing subsystem uses it to create and place the actual layers.
+class NativeLayerRoot {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(NativeLayerRoot)
+
+ virtual NativeLayerRootCA* AsNativeLayerRootCA() { return nullptr; }
+ virtual NativeLayerRootWayland* AsNativeLayerRootWayland() { return nullptr; }
+
+ virtual already_AddRefed<NativeLayer> CreateLayer(
+ const gfx::IntSize& aSize, bool aIsOpaque,
+ SurfacePoolHandle* aSurfacePoolHandle) = 0;
+ virtual already_AddRefed<NativeLayer> CreateLayerForExternalTexture(
+ bool aIsOpaque) = 0;
+ virtual already_AddRefed<NativeLayer> CreateLayerForColor(
+ gfx::DeviceColor aColor) {
+ return nullptr;
+ }
+
+ virtual void AppendLayer(NativeLayer* aLayer) = 0;
+ virtual void RemoveLayer(NativeLayer* aLayer) = 0;
+ virtual void SetLayers(const nsTArray<RefPtr<NativeLayer>>& aLayers) = 0;
+
+ // Called before any layer content changes
+ virtual void PrepareForCommit() {}
+
+ // Publish the layer changes to the screen. Returns whether the commit was
+ // successful.
+ virtual bool CommitToScreen() = 0;
+
+ // Returns a new NativeLayerRootSnapshotter that can be used to read back the
+ // visual output of this NativeLayerRoot. The snapshotter needs to be
+ // destroyed on the same thread that CreateSnapshotter() was called on. Only
+ // one snapshotter per NativeLayerRoot can be in existence at any given time.
+ // CreateSnapshotter() makes sure of this and crashes if called at a time at
+ // which there still exists a snapshotter for this NativeLayerRoot.
+ virtual UniquePtr<NativeLayerRootSnapshotter> CreateSnapshotter() {
+ return nullptr;
+ }
+
+ protected:
+ virtual ~NativeLayerRoot() = default;
+};
+
+// Allows reading back the visual output of a NativeLayerRoot.
+// Can only be used on a single thread, unlike NativeLayerRoot.
+// Holds a strong reference to the NativeLayerRoot that created it.
+// On Mac, this owns a GLContext, which wants to be created and destroyed on the
+// same thread.
+class NativeLayerRootSnapshotter : public profiler_screenshots::Window {
+ public:
+ virtual ~NativeLayerRootSnapshotter() = default;
+
+ // Reads the composited result of the NativeLayer tree into aReadbackBuffer,
+ // synchronously. Should only be called right after a call to CommitToScreen()
+ // - in that case it is guaranteed to read back exactly the NativeLayer state
+ // that was committed. If called at other times, this API does not define
+ // whether the observed state includes NativeLayer modifications which have
+ // not been committed. (The macOS implementation will include those pending
+ // modifications by doing an offscreen commit.)
+ // The readback buffer's stride is assumed to be aReadbackSize.width * 4. Only
+ // BGRA is supported.
+ virtual bool ReadbackPixels(const gfx::IntSize& aReadbackSize,
+ gfx::SurfaceFormat aReadbackFormat,
+ const Range<uint8_t>& aReadbackBuffer) = 0;
+};
+
+// Represents a native layer. Native layers, such as CoreAnimation layers on
+// macOS, are used to put pixels on the screen and to refresh and manipulate
+// the visual contents of a window efficiently. For example, drawing to a layer
+// once and then displaying the layer for multiple frames while moving it to
+// different positions will be more efficient than drawing into a window (or a
+// non-moving layer) multiple times with different internal offsets.
+// There are two sources of "work" for a given composited frame: 1) Our own
+// drawing (such as OpenGL compositing into a window or layer) and 2) the
+// compositing window manager's work to update the screen. Every pixel we draw
+// needs to be copied to the screen by the window manager. This suggests two
+// avenues for reducing the work load for a given frame: Drawing fewer pixels
+// ourselves, and making the window manager copy fewer pixels to the screen.
+// Smart use of native layers allows reducing both work loads: If a visual
+// change can be expressed purely as a layer attribute change (such as a change
+// in the layer's position), this lets us eliminate our own drawing for that
+// change. And secondly, manipulating a small layer rather than a large layer
+// will reduce the window manager's work for that frame because it'll only copy
+// the pixels of the small layer to the screen.
+class NativeLayer {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(NativeLayer)
+
+ virtual NativeLayerCA* AsNativeLayerCA() { return nullptr; }
+ virtual NativeLayerWayland* AsNativeLayerWayland() { return nullptr; }
+
+ // The size and opaqueness of a layer are supplied during layer creation and
+ // never change.
+ virtual gfx::IntSize GetSize() = 0;
+ virtual bool IsOpaque() = 0;
+
+ // The location of the layer, in integer device pixels.
+ // This is applied to the layer, before the transform is applied.
+ virtual void SetPosition(const gfx::IntPoint& aPosition) = 0;
+ virtual gfx::IntPoint GetPosition() = 0;
+
+ // Sets a transformation to apply to the Layer. This gets applied to
+ // coordinates with the position applied, but before clipping is
+ // applied.
+ virtual void SetTransform(const gfx::Matrix4x4& aTransform) = 0;
+ virtual gfx::Matrix4x4 GetTransform() = 0;
+
+ virtual gfx::IntRect GetRect() = 0;
+
+ // Set an optional clip rect on the layer. The clip rect is in post-transform
+ // coordinate space
+ virtual void SetClipRect(const Maybe<gfx::IntRect>& aClipRect) = 0;
+ virtual Maybe<gfx::IntRect> ClipRect() = 0;
+
+ // Returns the "display rect", in content coordinates, of the current front
+ // surface. This rect acts as an extra clip and prevents invalid content from
+ // getting to the screen. The display rect starts out empty before the first
+ // call to NextSurface*. Note the different coordinate space from the regular
+ // clip rect: the clip rect is "outside" the layer position, the display rect
+ // is "inside" the layer position (moves with the layer).
+ virtual gfx::IntRect CurrentSurfaceDisplayRect() = 0;
+
+ // Whether the surface contents are flipped vertically compared to this
+ // layer's coordinate system. Can be set on any thread at any time.
+ virtual void SetSurfaceIsFlipped(bool aIsFlipped) = 0;
+ virtual bool SurfaceIsFlipped() = 0;
+
+ virtual void SetSamplingFilter(gfx::SamplingFilter aSamplingFilter) = 0;
+
+ // Returns a DrawTarget. The size of the DrawTarget will be the same as the
+ // size of this layer. The caller should draw to that DrawTarget, then drop
+ // its reference to the DrawTarget, and then call NotifySurfaceReady(). It can
+ // limit its drawing to aUpdateRegion (which is in the DrawTarget's device
+ // space). After a call to NextSurface*, NextSurface* must not be called again
+ // until after NotifySurfaceReady has been called. Can be called on any
+ // thread. When used from multiple threads, callers need to make sure that
+ // they still only call NextSurface* and NotifySurfaceReady alternatingly and
+ // not in any other order. aUpdateRegion and aDisplayRect are in "content
+ // coordinates" and must not extend beyond the layer size. If aDisplayRect
+ // contains parts that were not valid before, then those parts must be updated
+ // (must be part of aUpdateRegion), so that the entirety of aDisplayRect is
+ // valid after the update. The display rect determines the parts of the
+ // surface that will be shown; this allows using surfaces with only
+ // partially-valid content, as long as none of the invalid content is included
+ // in the display rect.
+ virtual RefPtr<gfx::DrawTarget> NextSurfaceAsDrawTarget(
+ const gfx::IntRect& aDisplayRect, const gfx::IntRegion& aUpdateRegion,
+ gfx::BackendType aBackendType) = 0;
+
+ // Returns a GLuint for a framebuffer that can be used for drawing to the
+ // surface. The size of the framebuffer will be the same as the size of this
+ // layer. If aNeedsDepth is true, the framebuffer is created with a depth
+ // buffer.
+ // The framebuffer's depth buffer (if present) may be shared with other
+ // framebuffers of the same size, even from entirely different NativeLayer
+ // objects. The caller should not assume anything about the depth buffer's
+ // existing contents (i.e. it should clear it at the beginning of the draw).
+ // Callers should draw to one layer at a time, such that there is no
+ // interleaved drawing to different framebuffers that could be tripped up by
+ // the sharing.
+ // The caller should draw to the framebuffer, unbind it, and then call
+ // NotifySurfaceReady(). It can limit its drawing to aUpdateRegion (which is
+ // in the framebuffer's device space, possibly "upside down" if
+ // SurfaceIsFlipped()).
+ // The framebuffer will be created in the GLContext that this layer's
+ // SurfacePoolHandle was created for.
+ // After a call to NextSurface*, NextSurface* must not be called again until
+ // after NotifySurfaceReady has been called. Can be called on any thread. When
+ // used from multiple threads, callers need to make sure that they still only
+ // call NextSurface and NotifySurfaceReady alternatingly and not in any other
+ // order.
+ // aUpdateRegion and aDisplayRect are in "content coordinates" and must not
+ // extend beyond the layer size. If aDisplayRect contains parts that were not
+ // valid before, then those parts must be updated (must be part of
+ // aUpdateRegion), so that the entirety of aDisplayRect is valid after the
+ // update. The display rect determines the parts of the surface that will be
+ // shown; this allows using surfaces with only partially-valid content, as
+ // long as none of the invalid content is included in the display rect.
+ virtual Maybe<GLuint> NextSurfaceAsFramebuffer(
+ const gfx::IntRect& aDisplayRect, const gfx::IntRegion& aUpdateRegion,
+ bool aNeedsDepth) = 0;
+
+ // Indicates that the surface which has been returned from the most recent
+ // call to NextSurface* is now finished being drawn to and can be displayed on
+ // the screen. Resets the invalid region on the surface to the empty region.
+ virtual void NotifySurfaceReady() = 0;
+
+ // If you know that this layer will likely not draw any more frames, then it's
+ // good to call DiscardBackbuffers in order to save memory and allow other
+ // layer's to pick up the released surfaces from the pool.
+ virtual void DiscardBackbuffers() = 0;
+
+ virtual void AttachExternalImage(wr::RenderTextureHost* aExternalImage) = 0;
+
+ protected:
+ virtual ~NativeLayer() = default;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_NativeLayer_h
diff --git a/gfx/layers/NativeLayerCA.h b/gfx/layers/NativeLayerCA.h
new file mode 100644
index 0000000000..ce4ce6fd93
--- /dev/null
+++ b/gfx/layers/NativeLayerCA.h
@@ -0,0 +1,481 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_NativeLayerCA_h
+#define mozilla_layers_NativeLayerCA_h
+
+#include <IOSurface/IOSurface.h>
+
+#include <deque>
+#include <unordered_map>
+#include <ostream>
+
+#include "mozilla/Mutex.h"
+#include "mozilla/TimeStamp.h"
+
+#include "mozilla/gfx/MacIOSurface.h"
+#include "mozilla/layers/NativeLayer.h"
+#include "CFTypeRefPtr.h"
+#include "nsRegion.h"
+#include "nsISupportsImpl.h"
+
+#ifdef __OBJC__
+@class CALayer;
+#else
+typedef void CALayer;
+#endif
+
+namespace mozilla {
+
+namespace gl {
+class GLContextCGL;
+class MozFramebuffer;
+} // namespace gl
+namespace wr {
+class RenderMacIOSurfaceTextureHost;
+} // namespace wr
+
+namespace layers {
+
+class NativeLayerRootSnapshotterCA;
+class SurfacePoolHandleCA;
+
+enum class VideoLowPowerType {
+ // These must be kept synchronized with the telemetry histogram enums.
+ NotVideo, // Never emitted as telemetry. No video is visible.
+ LowPower, // As best we can tell, we are in the "detached",
+ // low-power compositing mode. We don't use "Success"
+ // because of name collision with telemetry generation.
+ FailMultipleVideo, // There is more than one video visible.
+ FailWindowed, // The window is not fullscreen.
+ FailOverlaid, // Something is on top of the video (likely captions).
+ FailBacking, // The layer behind the video is not full-coverage black.
+ FailMacOSVersion, // macOS version does not meet requirements.
+ FailPref, // Pref is not set.
+ FailSurface, // Surface is not eligible.
+ FailEnqueue, // Enqueueing the video didn't work.
+};
+
+// NativeLayerRootCA is the CoreAnimation implementation of the NativeLayerRoot
+// interface. A NativeLayerRootCA is created by the widget around an existing
+// CALayer with a call to CreateForCALayer - this CALayer is the root of the
+// "onscreen" representation of this layer tree.
+// All methods can be called from any thread, there is internal locking.
+// All effects from mutating methods are buffered locally and don't modify the
+// underlying CoreAnimation layers until CommitToScreen() is called. This
+// ensures that the modifications happen on the right thread.
+//
+// More specifically: During normal operation, screen updates are driven from a
+// compositing thread. On this thread, the layers are created / destroyed, their
+// contents are painted, and the result is committed to the screen. However,
+// there are some scenarios that need to involve the main thread, most notably
+// window resizing: During a window resize, we still need the drawing part to
+// happen on the compositing thread, but the modifications to the underlying
+// CALayers need to happen on the main thread, once compositing is done.
+//
+// NativeLayerRootCA + NativeLayerCA create and maintain *two* CALayer tree
+// representations: An "onscreen" representation and an "offscreen"
+// representation. These representations are updated via calls to
+// CommitToScreen() and CommitOffscreen(), respectively. The reason for having
+// two representations is the following: Our implementation of the snapshotter
+// API uses CARenderer, which lets us render the composited result of our layer
+// tree into a GPU buffer. But CARenderer requires "ownership" of the rendered
+// CALayers in the sense that it associates the CALayers with a local
+// "CAContext". A CALayer can only be associated with one CAContext at any time.
+// If we wanted te render our *onscreen* CALayers with CARenderer, we would need
+// to remove them from the window, reparent them to the CARenderer, render them,
+// and then put them back into the window. This would lead to a visible flashing
+// effect. To solve this problem, we build two CALayer representations, so that
+// one representation can stay inside the window and the other can stay attached
+// to the CARenderer.
+class NativeLayerRootCA : public NativeLayerRoot {
+ public:
+ static already_AddRefed<NativeLayerRootCA> CreateForCALayer(CALayer* aLayer);
+
+ virtual NativeLayerRootCA* AsNativeLayerRootCA() override { return this; }
+
+ // Can be called on any thread at any point. Returns whether comitting was
+ // successful. Will return false if called off the main thread while
+ // off-main-thread commits are suspended.
+ bool CommitToScreen() override;
+
+ void CommitOffscreen();
+ void OnNativeLayerRootSnapshotterDestroyed(
+ NativeLayerRootSnapshotterCA* aNativeLayerRootSnapshotter);
+
+ // Enters a mode during which CommitToScreen(), when called on a non-main
+ // thread, will not apply any updates to the CALayer tree.
+ void SuspendOffMainThreadCommits();
+
+ // Exits the mode entered by SuspendOffMainThreadCommits().
+ // Returns true if the last CommitToScreen() was canceled due to suspension,
+ // indicating that another call to CommitToScreen() is needed.
+ bool UnsuspendOffMainThreadCommits();
+
+ bool AreOffMainThreadCommitsSuspended();
+
+ void DumpLayerTreeToFile(const char* aPath);
+
+ enum class WhichRepresentation : uint8_t { ONSCREEN, OFFSCREEN };
+
+ // Overridden methods
+ already_AddRefed<NativeLayer> CreateLayer(const gfx::IntSize& aSize, bool aIsOpaque,
+ SurfacePoolHandle* aSurfacePoolHandle) override;
+ void AppendLayer(NativeLayer* aLayer) override;
+ void RemoveLayer(NativeLayer* aLayer) override;
+ void SetLayers(const nsTArray<RefPtr<NativeLayer>>& aLayers) override;
+ UniquePtr<NativeLayerRootSnapshotter> CreateSnapshotter() override;
+
+ void SetBackingScale(float aBackingScale);
+ float BackingScale();
+
+ already_AddRefed<NativeLayer> CreateLayerForExternalTexture(bool aIsOpaque) override;
+ already_AddRefed<NativeLayer> CreateLayerForColor(gfx::DeviceColor aColor) override;
+
+ void SetWindowIsFullscreen(bool aFullscreen);
+
+ VideoLowPowerType CheckVideoLowPower();
+
+ protected:
+ explicit NativeLayerRootCA(CALayer* aLayer);
+ ~NativeLayerRootCA() override;
+
+ struct Representation {
+ explicit Representation(CALayer* aRootCALayer);
+ ~Representation();
+ void Commit(WhichRepresentation aRepresentation,
+ const nsTArray<RefPtr<NativeLayerCA>>& aSublayers, bool aWindowIsFullscreen);
+ CALayer* mRootCALayer = nullptr; // strong
+ bool mMutatedLayerStructure = false;
+ };
+
+ template <typename F>
+ void ForAllRepresentations(F aFn);
+
+ Mutex mMutex MOZ_UNANNOTATED; // protects all other fields
+ Representation mOnscreenRepresentation;
+ Representation mOffscreenRepresentation;
+ NativeLayerRootSnapshotterCA* mWeakSnapshotter = nullptr;
+ nsTArray<RefPtr<NativeLayerCA>> mSublayers; // in z-order
+ float mBackingScale = 1.0f;
+ bool mMutated = false;
+
+ // While mOffMainThreadCommitsSuspended is true, no commits
+ // should happen on a non-main thread, because they might race with
+ // main-thread driven updates such as window shape changes, and cause
+ // glitches.
+ bool mOffMainThreadCommitsSuspended = false;
+
+ // Set to true if CommitToScreen() was aborted because of commit suspension.
+ // Set to false when CommitToScreen() completes successfully. When true,
+ // indicates that CommitToScreen() needs to be called at the next available
+ // opportunity.
+ bool mCommitPending = false;
+
+ // Updated by the layer's view's window to match the fullscreen state
+ // of that window.
+ bool mWindowIsFullscreen = false;
+
+ // How many times have we committed since the last time we emitted
+ // telemetry?
+ unsigned int mTelemetryCommitCount = 0;
+};
+
+class RenderSourceNLRS;
+
+class NativeLayerRootSnapshotterCA final : public NativeLayerRootSnapshotter {
+ public:
+ static UniquePtr<NativeLayerRootSnapshotterCA> Create(NativeLayerRootCA* aLayerRoot,
+ CALayer* aRootCALayer);
+ virtual ~NativeLayerRootSnapshotterCA();
+
+ bool ReadbackPixels(const gfx::IntSize& aReadbackSize, gfx::SurfaceFormat aReadbackFormat,
+ const Range<uint8_t>& aReadbackBuffer) override;
+ already_AddRefed<profiler_screenshots::RenderSource> GetWindowContents(
+ const gfx::IntSize& aWindowSize) override;
+ already_AddRefed<profiler_screenshots::DownscaleTarget> CreateDownscaleTarget(
+ const gfx::IntSize& aSize) override;
+ already_AddRefed<profiler_screenshots::AsyncReadbackBuffer> CreateAsyncReadbackBuffer(
+ const gfx::IntSize& aSize) override;
+
+ protected:
+ NativeLayerRootSnapshotterCA(NativeLayerRootCA* aLayerRoot, RefPtr<gl::GLContext>&& aGL,
+ CALayer* aRootCALayer);
+ void UpdateSnapshot(const gfx::IntSize& aSize);
+
+ RefPtr<NativeLayerRootCA> mLayerRoot;
+ RefPtr<gl::GLContext> mGL;
+
+ // Can be null. Created and updated in UpdateSnapshot.
+ RefPtr<RenderSourceNLRS> mSnapshot;
+ CARenderer* mRenderer = nullptr; // strong
+};
+
+// NativeLayerCA wraps a CALayer and lets you draw to it. It ensures that only
+// fully-drawn frames make their way to the screen, by maintaining a swap chain
+// of IOSurfaces.
+// All calls to mutating methods are buffered, and don't take effect on the
+// underlying CoreAnimation layers until ApplyChanges() is called.
+// The two most important methods are NextSurface and NotifySurfaceReady:
+// NextSurface takes an available surface from the swap chain or creates a new
+// surface if necessary. This surface can then be drawn to. Once drawing is
+// finished, NotifySurfaceReady marks the surface as ready. This surface is
+// committed to the layer during the next call to ApplyChanges().
+// The swap chain keeps track of invalid areas within the surfaces.
+class NativeLayerCA : public NativeLayer {
+ public:
+ virtual NativeLayerCA* AsNativeLayerCA() override { return this; }
+
+ // Overridden methods
+ gfx::IntSize GetSize() override;
+ void SetPosition(const gfx::IntPoint& aPosition) override;
+ gfx::IntPoint GetPosition() override;
+ void SetTransform(const gfx::Matrix4x4& aTransform) override;
+ gfx::Matrix4x4 GetTransform() override;
+ gfx::IntRect GetRect() override;
+ void SetSamplingFilter(gfx::SamplingFilter aSamplingFilter) override;
+ RefPtr<gfx::DrawTarget> NextSurfaceAsDrawTarget(const gfx::IntRect& aDisplayRect,
+ const gfx::IntRegion& aUpdateRegion,
+ gfx::BackendType aBackendType) override;
+ Maybe<GLuint> NextSurfaceAsFramebuffer(const gfx::IntRect& aDisplayRect,
+ const gfx::IntRegion& aUpdateRegion,
+ bool aNeedsDepth) override;
+ void NotifySurfaceReady() override;
+ void DiscardBackbuffers() override;
+ bool IsOpaque() override;
+ void SetClipRect(const Maybe<gfx::IntRect>& aClipRect) override;
+ Maybe<gfx::IntRect> ClipRect() override;
+ gfx::IntRect CurrentSurfaceDisplayRect() override;
+ void SetSurfaceIsFlipped(bool aIsFlipped) override;
+ bool SurfaceIsFlipped() override;
+
+ void DumpLayer(std::ostream& aOutputStream);
+
+ void AttachExternalImage(wr::RenderTextureHost* aExternalImage) override;
+
+ void SetRootWindowIsFullscreen(bool aFullscreen);
+
+ protected:
+ friend class NativeLayerRootCA;
+
+ NativeLayerCA(const gfx::IntSize& aSize, bool aIsOpaque, SurfacePoolHandleCA* aSurfacePoolHandle);
+ explicit NativeLayerCA(bool aIsOpaque);
+ explicit NativeLayerCA(gfx::DeviceColor aColor);
+ ~NativeLayerCA() override;
+
+ // Gets the next surface for drawing from our swap chain and stores it in
+ // mInProgressSurface. Returns whether this was successful.
+ // mInProgressSurface is guaranteed to be not in use by the window server.
+ // After a call to NextSurface, NextSurface must not be called again until
+ // after NotifySurfaceReady has been called. Can be called on any thread. When
+ // used from multiple threads, callers need to make sure that they still only
+ // call NextSurface and NotifySurfaceReady alternatingly and not in any other
+ // order.
+ bool NextSurface(const MutexAutoLock& aProofOfLock);
+
+ // To be called by NativeLayerRootCA:
+ typedef NativeLayerRootCA::WhichRepresentation WhichRepresentation;
+ CALayer* UnderlyingCALayer(WhichRepresentation aRepresentation);
+
+ enum class UpdateType {
+ None, // Order is important. Each enum must fully encompass the
+ OnlyVideo, // work implied by the previous enums.
+ All,
+ };
+
+ UpdateType HasUpdate(WhichRepresentation aRepresentation);
+ bool WillUpdateAffectLayers(WhichRepresentation aRepresentation);
+ bool ApplyChanges(WhichRepresentation aRepresentation, UpdateType aUpdate);
+
+ void SetBackingScale(float aBackingScale);
+
+ // Invalidates the specified region in all surfaces that are tracked by this
+ // layer.
+ void InvalidateRegionThroughoutSwapchain(const MutexAutoLock& aProofOfLock,
+ const gfx::IntRegion& aRegion);
+
+ // Invalidate aUpdateRegion and make sure that mInProgressSurface retains any
+ // valid content from the previous surface outside of aUpdateRegion, so that
+ // only aUpdateRegion needs to be drawn. If content needs to be copied,
+ // aCopyFn is called to do the copying.
+ // aCopyFn: Fn(CFTypeRefPtr<IOSurfaceRef> aValidSourceIOSurface,
+ // const gfx::IntRegion& aCopyRegion) -> void
+ template <typename F>
+ void HandlePartialUpdate(const MutexAutoLock& aProofOfLock, const gfx::IntRect& aDisplayRect,
+ const gfx::IntRegion& aUpdateRegion, F&& aCopyFn);
+
+ struct SurfaceWithInvalidRegion {
+ CFTypeRefPtr<IOSurfaceRef> mSurface;
+ gfx::IntRegion mInvalidRegion;
+ };
+
+ struct SurfaceWithInvalidRegionAndCheckCount {
+ SurfaceWithInvalidRegion mEntry;
+ uint32_t mCheckCount; // The number of calls to IOSurfaceIsInUse
+ };
+
+ Maybe<SurfaceWithInvalidRegion> GetUnusedSurfaceAndCleanUp(const MutexAutoLock& aProofOfLock);
+
+ bool IsVideo();
+ bool IsVideoAndLocked(const MutexAutoLock& aProofOfLock);
+ bool ShouldSpecializeVideo(const MutexAutoLock& aProofOfLock);
+ bool HasExtent() const { return mHasExtent; }
+ void SetHasExtent(bool aHasExtent) { mHasExtent = aHasExtent; }
+
+ // This function returns a CGRect if a clip should be applied to the layer.
+ // If set, the CGRect has the scaled position of the clip relative to the
+ // surface origin and the scaled size of the clip rect.
+ static Maybe<CGRect> CalculateClipGeometry(
+ const gfx::IntSize& aSize, const gfx::IntPoint& aPosition, const gfx::Matrix4x4& aTransform,
+ const gfx::IntRect& aDisplayRect, const Maybe<gfx::IntRect>& aClipRect, float aBackingScale);
+
+ // Wraps one CALayer representation of this NativeLayer.
+ struct Representation {
+ Representation();
+ ~Representation();
+
+ CALayer* UnderlyingCALayer() { return mWrappingCALayer; }
+
+ bool EnqueueSurface(IOSurfaceRef aSurfaceRef);
+
+ // Applies buffered changes to the native CALayers. The contract with the
+ // caller is as follows: If any of these values have changed since the last
+ // call to ApplyChanges, mMutated[Field] needs to have been set to true
+ // before the call. If aUpdate is not All, then a partial update will be
+ // applied. In such a case, ApplyChanges may not make any changes that
+ // require a CATransacation, because no transaction will be created. In a
+ // a partial update, the return value will indicate if all the needed
+ // changes were able to be applied under these restrictions. A false return
+ // value indicates an All update is necessary.
+ bool ApplyChanges(UpdateType aUpdate, const gfx::IntSize& aSize, bool aIsOpaque,
+ const gfx::IntPoint& aPosition, const gfx::Matrix4x4& aTransform,
+ const gfx::IntRect& aDisplayRect, const Maybe<gfx::IntRect>& aClipRect,
+ float aBackingScale, bool aSurfaceIsFlipped,
+ gfx::SamplingFilter aSamplingFilter, bool aSpecializeVideo,
+ CFTypeRefPtr<IOSurfaceRef> aFrontSurface, CFTypeRefPtr<CGColorRef> aColor,
+ bool aIsDRM, bool aIsVideo);
+
+ // Return whether any aspects of this layer representation have been mutated
+ // since the last call to ApplyChanges, i.e. whether ApplyChanges needs to
+ // be called.
+ // This is used to optimize away a CATransaction commit if no layers have
+ // changed.
+ UpdateType HasUpdate(bool aIsVideo);
+
+ // Lazily initialized by first call to ApplyChanges. mWrappingLayer is the
+ // layer that applies the intersection of mDisplayRect and mClipRect (if
+ // set), and mContentCALayer is the layer that hosts the IOSurface. We do
+ // not share clip layers between consecutive NativeLayerCA objects with the
+ // same clip rect.
+ CALayer* mWrappingCALayer = nullptr; // strong
+ CALayer* mContentCALayer = nullptr; // strong
+ CALayer* mOpaquenessTintLayer = nullptr; // strong
+
+#ifdef NIGHTLY_BUILD
+ bool mLogNextVideoSurface = false;
+#endif
+
+ bool mMutatedPosition : 1;
+ bool mMutatedTransform : 1;
+ bool mMutatedDisplayRect : 1;
+ bool mMutatedClipRect : 1;
+ bool mMutatedBackingScale : 1;
+ bool mMutatedSize : 1;
+ bool mMutatedSurfaceIsFlipped : 1;
+ bool mMutatedFrontSurface : 1;
+ bool mMutatedSamplingFilter : 1;
+ bool mMutatedSpecializeVideo : 1;
+ bool mMutatedIsDRM : 1;
+ };
+
+ Representation& GetRepresentation(WhichRepresentation aRepresentation);
+ template <typename F>
+ void ForAllRepresentations(F aFn);
+
+ // Controls access to all fields of this class.
+ Mutex mMutex MOZ_UNANNOTATED;
+
+ // Each IOSurface is initially created inside NextSurface.
+ // The surface stays alive until the recycling mechanism in NextSurface
+ // determines it is no longer needed (because the swap chain has grown too
+ // long) or until DiscardBackbuffers() is called or the layer is destroyed.
+ // During the surface's lifetime, it will continuously move through the fields
+ // mInProgressSurface, mFrontSurface, and back to front through the mSurfaces
+ // queue:
+ //
+ // mSurfaces.front()
+ // ------[NextSurface()]-----> mInProgressSurface
+ // --[NotifySurfaceReady()]--> mFrontSurface
+ // --[NotifySurfaceReady()]--> mSurfaces.back() --> .... -->
+ // mSurfaces.front()
+ //
+ // We mark an IOSurface as "in use" as long as it is either in
+ // mInProgressSurface. When it is in mFrontSurface or in the mSurfaces queue,
+ // it is not marked as "in use" by us - but it can be "in use" by the window
+ // server. Consequently, IOSurfaceIsInUse on a surface from mSurfaces reflects
+ // whether the window server is still reading from the surface, and we can use
+ // this indicator to decide when to recycle the surface.
+ //
+ // Users of NativeLayerCA normally proceed in this order:
+ // 1. Begin a frame by calling NextSurface to get the surface.
+ // 2. Draw to the surface.
+ // 3. Mark the surface as done by calling NotifySurfaceReady.
+ // 4. Call NativeLayerRoot::CommitToScreen(), which calls ApplyChanges()
+ // during a CATransaction.
+
+ // The surface we returned from the most recent call to NextSurface, before
+ // the matching call to NotifySurfaceReady.
+ // Will only be Some() between calls to NextSurface and NotifySurfaceReady.
+ Maybe<SurfaceWithInvalidRegion> mInProgressSurface;
+ Maybe<gfx::IntRegion> mInProgressUpdateRegion;
+ Maybe<gfx::IntRect> mInProgressDisplayRect;
+
+ // The surface that the most recent call to NotifySurfaceReady was for.
+ // Will be Some() after the first call to NotifySurfaceReady, for the rest of
+ // the layer's life time.
+ Maybe<SurfaceWithInvalidRegion> mFrontSurface;
+
+ // The queue of surfaces which make up the rest of our "swap chain".
+ // mSurfaces.front() is the next surface we'll attempt to use.
+ // mSurfaces.back() is the one that was used most recently.
+ std::vector<SurfaceWithInvalidRegionAndCheckCount> mSurfaces;
+
+ // Non-null between calls to NextSurfaceAsDrawTarget and NotifySurfaceReady.
+ RefPtr<MacIOSurface> mInProgressLockedIOSurface;
+
+ RefPtr<SurfacePoolHandleCA> mSurfacePoolHandle;
+ RefPtr<wr::RenderMacIOSurfaceTextureHost> mTextureHost;
+
+ Representation mOnscreenRepresentation;
+ Representation mOffscreenRepresentation;
+
+ gfx::IntPoint mPosition;
+ gfx::Matrix4x4 mTransform;
+ gfx::IntRect mDisplayRect;
+ gfx::IntSize mSize;
+ Maybe<gfx::IntRect> mClipRect;
+ gfx::SamplingFilter mSamplingFilter = gfx::SamplingFilter::POINT;
+ float mBackingScale = 1.0f;
+ bool mSurfaceIsFlipped = false;
+ CFTypeRefPtr<CGColorRef> mColor;
+ const bool mIsOpaque = false;
+ bool mRootWindowIsFullscreen = false;
+ bool mSpecializeVideo = false;
+ bool mHasExtent = false;
+ bool mIsDRM = false;
+
+#ifdef NIGHTLY_BUILD
+ // Track the consistency of our caller's API usage. Layers that are drawn
+ // should only ever be called with NotifySurfaceReady. Layers that are
+ // external should only ever be called with AttachExternalImage.
+ bool mHasEverAttachExternalImage = false;
+ bool mHasEverNotifySurfaceReady = false;
+#endif
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_NativeLayerCA_h
diff --git a/gfx/layers/NativeLayerCA.mm b/gfx/layers/NativeLayerCA.mm
new file mode 100644
index 0000000000..34e5cf0ffb
--- /dev/null
+++ b/gfx/layers/NativeLayerCA.mm
@@ -0,0 +1,1990 @@
+/* -*- Mode: C++; tab-width: 20; indent-tabs-mode: nullptr; c-basic-offset: 2 -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/layers/NativeLayerCA.h"
+
+#import <AppKit/NSAnimationContext.h>
+#import <AppKit/NSColor.h>
+#import <AVFoundation/AVFoundation.h>
+#import <OpenGL/gl.h>
+#import <QuartzCore/QuartzCore.h>
+
+#include <algorithm>
+#include <fstream>
+#include <iostream>
+#include <sstream>
+#include <utility>
+
+#include "gfxUtils.h"
+#include "GLBlitHelper.h"
+#include "GLContextCGL.h"
+#include "GLContextProvider.h"
+#include "MozFramebuffer.h"
+#include "mozilla/gfx/Swizzle.h"
+#include "mozilla/layers/ScreenshotGrabber.h"
+#include "mozilla/layers/SurfacePoolCA.h"
+#include "mozilla/StaticPrefs_gfx.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/webrender/RenderMacIOSurfaceTextureHost.h"
+#include "nsCocoaFeatures.h"
+#include "ScopedGLHelpers.h"
+#include "SDKDeclarations.h"
+
+@interface CALayer (PrivateSetContentsOpaque)
+- (void)setContentsOpaque:(BOOL)opaque;
+@end
+
+namespace mozilla {
+namespace layers {
+
+using gfx::DataSourceSurface;
+using gfx::IntPoint;
+using gfx::IntRect;
+using gfx::IntRegion;
+using gfx::IntSize;
+using gfx::Matrix4x4;
+using gfx::SurfaceFormat;
+using gl::GLContext;
+using gl::GLContextCGL;
+
+static Maybe<Telemetry::LABELS_GFX_MACOS_VIDEO_LOW_POWER> VideoLowPowerTypeToTelemetryType(
+ VideoLowPowerType aVideoLowPower) {
+ switch (aVideoLowPower) {
+ case VideoLowPowerType::LowPower:
+ return Some(Telemetry::LABELS_GFX_MACOS_VIDEO_LOW_POWER::LowPower);
+
+ case VideoLowPowerType::FailMultipleVideo:
+ return Some(Telemetry::LABELS_GFX_MACOS_VIDEO_LOW_POWER::FailMultipleVideo);
+
+ case VideoLowPowerType::FailWindowed:
+ return Some(Telemetry::LABELS_GFX_MACOS_VIDEO_LOW_POWER::FailWindowed);
+
+ case VideoLowPowerType::FailOverlaid:
+ return Some(Telemetry::LABELS_GFX_MACOS_VIDEO_LOW_POWER::FailOverlaid);
+
+ case VideoLowPowerType::FailBacking:
+ return Some(Telemetry::LABELS_GFX_MACOS_VIDEO_LOW_POWER::FailBacking);
+
+ case VideoLowPowerType::FailMacOSVersion:
+ return Some(Telemetry::LABELS_GFX_MACOS_VIDEO_LOW_POWER::FailMacOSVersion);
+
+ case VideoLowPowerType::FailPref:
+ return Some(Telemetry::LABELS_GFX_MACOS_VIDEO_LOW_POWER::FailPref);
+
+ case VideoLowPowerType::FailSurface:
+ return Some(Telemetry::LABELS_GFX_MACOS_VIDEO_LOW_POWER::FailSurface);
+
+ case VideoLowPowerType::FailEnqueue:
+ return Some(Telemetry::LABELS_GFX_MACOS_VIDEO_LOW_POWER::FailEnqueue);
+
+ default:
+ return Nothing();
+ }
+}
+
+static void EmitTelemetryForVideoLowPower(VideoLowPowerType aVideoLowPower) {
+ auto telemetryValue = VideoLowPowerTypeToTelemetryType(aVideoLowPower);
+ if (telemetryValue.isSome()) {
+ Telemetry::AccumulateCategorical(telemetryValue.value());
+ }
+}
+
+// Utility classes for NativeLayerRootSnapshotter (NLRS) profiler screenshots.
+
+class RenderSourceNLRS : public profiler_screenshots::RenderSource {
+ public:
+ explicit RenderSourceNLRS(UniquePtr<gl::MozFramebuffer>&& aFramebuffer)
+ : RenderSource(aFramebuffer->mSize), mFramebuffer(std::move(aFramebuffer)) {}
+ auto& FB() { return *mFramebuffer; }
+
+ protected:
+ UniquePtr<gl::MozFramebuffer> mFramebuffer;
+};
+
+class DownscaleTargetNLRS : public profiler_screenshots::DownscaleTarget {
+ public:
+ DownscaleTargetNLRS(gl::GLContext* aGL, UniquePtr<gl::MozFramebuffer>&& aFramebuffer)
+ : profiler_screenshots::DownscaleTarget(aFramebuffer->mSize),
+ mGL(aGL),
+ mRenderSource(new RenderSourceNLRS(std::move(aFramebuffer))) {}
+ already_AddRefed<profiler_screenshots::RenderSource> AsRenderSource() override {
+ return do_AddRef(mRenderSource);
+ };
+ bool DownscaleFrom(profiler_screenshots::RenderSource* aSource, const IntRect& aSourceRect,
+ const IntRect& aDestRect) override;
+
+ protected:
+ RefPtr<gl::GLContext> mGL;
+ RefPtr<RenderSourceNLRS> mRenderSource;
+};
+
+class AsyncReadbackBufferNLRS : public profiler_screenshots::AsyncReadbackBuffer {
+ public:
+ AsyncReadbackBufferNLRS(gl::GLContext* aGL, const IntSize& aSize, GLuint aBufferHandle)
+ : profiler_screenshots::AsyncReadbackBuffer(aSize), mGL(aGL), mBufferHandle(aBufferHandle) {}
+ void CopyFrom(profiler_screenshots::RenderSource* aSource) override;
+ bool MapAndCopyInto(DataSourceSurface* aSurface, const IntSize& aReadSize) override;
+
+ protected:
+ virtual ~AsyncReadbackBufferNLRS();
+ RefPtr<gl::GLContext> mGL;
+ GLuint mBufferHandle = 0;
+};
+
+// Needs to be on the stack whenever CALayer mutations are performed.
+// (Mutating CALayers outside of a transaction can result in permanently stuck rendering, because
+// such mutations create an implicit transaction which never auto-commits if the current thread does
+// not have a native runloop.)
+// Uses NSAnimationContext, which wraps CATransaction with additional off-main-thread protection,
+// see bug 1585523.
+struct MOZ_STACK_CLASS AutoCATransaction final {
+ AutoCATransaction() {
+ [NSAnimationContext beginGrouping];
+ // By default, mutating a CALayer property triggers an animation which smoothly transitions the
+ // property to the new value. We don't need these animations, and this call turns them off:
+ [CATransaction setDisableActions:YES];
+ }
+ ~AutoCATransaction() { [NSAnimationContext endGrouping]; }
+};
+
+/* static */ already_AddRefed<NativeLayerRootCA> NativeLayerRootCA::CreateForCALayer(
+ CALayer* aLayer) {
+ RefPtr<NativeLayerRootCA> layerRoot = new NativeLayerRootCA(aLayer);
+ return layerRoot.forget();
+}
+
+// Returns an autoreleased CALayer* object.
+static CALayer* MakeOffscreenRootCALayer() {
+ // This layer should behave similarly to the backing layer of a flipped NSView.
+ // It will never be rendered on the screen and it will never be attached to an NSView's layer;
+ // instead, it will be the root layer of a "local" CAContext.
+ // Setting geometryFlipped to YES causes the orientation of descendant CALayers' contents (such as
+ // IOSurfaces) to be consistent with what happens in a layer subtree that is attached to a flipped
+ // NSView. Setting it to NO would cause the surfaces in individual leaf layers to render upside
+ // down (rather than just flipping the entire layer tree upside down).
+ AutoCATransaction transaction;
+ CALayer* layer = [CALayer layer];
+ layer.position = NSZeroPoint;
+ layer.bounds = NSZeroRect;
+ layer.anchorPoint = NSZeroPoint;
+ layer.contentsGravity = kCAGravityTopLeft;
+ layer.masksToBounds = YES;
+ layer.geometryFlipped = YES;
+ return layer;
+}
+
+NativeLayerRootCA::NativeLayerRootCA(CALayer* aLayer)
+ : mMutex("NativeLayerRootCA"),
+ mOnscreenRepresentation(aLayer),
+ mOffscreenRepresentation(MakeOffscreenRootCALayer()) {}
+
+NativeLayerRootCA::~NativeLayerRootCA() {
+ MOZ_RELEASE_ASSERT(mSublayers.IsEmpty(),
+ "Please clear all layers before destroying the layer root.");
+}
+
+already_AddRefed<NativeLayer> NativeLayerRootCA::CreateLayer(
+ const IntSize& aSize, bool aIsOpaque, SurfacePoolHandle* aSurfacePoolHandle) {
+ RefPtr<NativeLayer> layer =
+ new NativeLayerCA(aSize, aIsOpaque, aSurfacePoolHandle->AsSurfacePoolHandleCA());
+ return layer.forget();
+}
+
+already_AddRefed<NativeLayer> NativeLayerRootCA::CreateLayerForExternalTexture(bool aIsOpaque) {
+ RefPtr<NativeLayer> layer = new NativeLayerCA(aIsOpaque);
+ return layer.forget();
+}
+
+already_AddRefed<NativeLayer> NativeLayerRootCA::CreateLayerForColor(gfx::DeviceColor aColor) {
+ RefPtr<NativeLayer> layer = new NativeLayerCA(aColor);
+ return layer.forget();
+}
+
+void NativeLayerRootCA::AppendLayer(NativeLayer* aLayer) {
+ MutexAutoLock lock(mMutex);
+
+ RefPtr<NativeLayerCA> layerCA = aLayer->AsNativeLayerCA();
+ MOZ_RELEASE_ASSERT(layerCA);
+
+ mSublayers.AppendElement(layerCA);
+ layerCA->SetBackingScale(mBackingScale);
+ layerCA->SetRootWindowIsFullscreen(mWindowIsFullscreen);
+ ForAllRepresentations([&](Representation& r) { r.mMutatedLayerStructure = true; });
+}
+
+void NativeLayerRootCA::RemoveLayer(NativeLayer* aLayer) {
+ MutexAutoLock lock(mMutex);
+
+ RefPtr<NativeLayerCA> layerCA = aLayer->AsNativeLayerCA();
+ MOZ_RELEASE_ASSERT(layerCA);
+
+ mSublayers.RemoveElement(layerCA);
+ ForAllRepresentations([&](Representation& r) { r.mMutatedLayerStructure = true; });
+}
+
+void NativeLayerRootCA::SetLayers(const nsTArray<RefPtr<NativeLayer>>& aLayers) {
+ MutexAutoLock lock(mMutex);
+
+ // Ideally, we'd just be able to do mSublayers = std::move(aLayers).
+ // However, aLayers has a different type: it carries NativeLayer objects, whereas mSublayers
+ // carries NativeLayerCA objects, so we have to downcast all the elements first. There's one other
+ // reason to look at all the elements in aLayers first: We need to make sure any new layers know
+ // about our current backing scale.
+
+ nsTArray<RefPtr<NativeLayerCA>> layersCA(aLayers.Length());
+ for (auto& layer : aLayers) {
+ RefPtr<NativeLayerCA> layerCA = layer->AsNativeLayerCA();
+ MOZ_RELEASE_ASSERT(layerCA);
+ layerCA->SetBackingScale(mBackingScale);
+ layerCA->SetRootWindowIsFullscreen(mWindowIsFullscreen);
+ layersCA.AppendElement(std::move(layerCA));
+ }
+
+ if (layersCA != mSublayers) {
+ mSublayers = std::move(layersCA);
+ ForAllRepresentations([&](Representation& r) { r.mMutatedLayerStructure = true; });
+ }
+}
+
+void NativeLayerRootCA::SetBackingScale(float aBackingScale) {
+ MutexAutoLock lock(mMutex);
+
+ mBackingScale = aBackingScale;
+ for (auto layer : mSublayers) {
+ layer->SetBackingScale(aBackingScale);
+ }
+}
+
+float NativeLayerRootCA::BackingScale() {
+ MutexAutoLock lock(mMutex);
+ return mBackingScale;
+}
+
+void NativeLayerRootCA::SuspendOffMainThreadCommits() {
+ MutexAutoLock lock(mMutex);
+ mOffMainThreadCommitsSuspended = true;
+}
+
+bool NativeLayerRootCA::UnsuspendOffMainThreadCommits() {
+ MutexAutoLock lock(mMutex);
+ mOffMainThreadCommitsSuspended = false;
+ return mCommitPending;
+}
+
+bool NativeLayerRootCA::AreOffMainThreadCommitsSuspended() {
+ MutexAutoLock lock(mMutex);
+ return mOffMainThreadCommitsSuspended;
+}
+
+bool NativeLayerRootCA::CommitToScreen() {
+ {
+ MutexAutoLock lock(mMutex);
+
+ if (!NS_IsMainThread() && mOffMainThreadCommitsSuspended) {
+ mCommitPending = true;
+ return false;
+ }
+
+ mOnscreenRepresentation.Commit(WhichRepresentation::ONSCREEN, mSublayers, mWindowIsFullscreen);
+
+ mCommitPending = false;
+ }
+
+ if (StaticPrefs::gfx_webrender_debug_dump_native_layer_tree_to_file()) {
+ static uint32_t sFrameID = 0;
+ uint32_t frameID = sFrameID++;
+
+ NSString* dirPath =
+ [NSString stringWithFormat:@"%@/Desktop/nativelayerdumps-%d", NSHomeDirectory(), getpid()];
+ if ([NSFileManager.defaultManager createDirectoryAtPath:dirPath
+ withIntermediateDirectories:YES
+ attributes:nil
+ error:nullptr]) {
+ NSString* filename = [NSString stringWithFormat:@"frame-%d.html", frameID];
+ NSString* filePath = [dirPath stringByAppendingPathComponent:filename];
+ DumpLayerTreeToFile([filePath UTF8String]);
+ } else {
+ NSLog(@"Failed to create directory %@", dirPath);
+ }
+ }
+
+ // Decide if we are going to emit telemetry about video low power on this commit.
+ static const int32_t TELEMETRY_COMMIT_PERIOD =
+ StaticPrefs::gfx_core_animation_low_power_telemetry_frames_AtStartup();
+ mTelemetryCommitCount = (mTelemetryCommitCount + 1) % TELEMETRY_COMMIT_PERIOD;
+ if (mTelemetryCommitCount == 0) {
+ // Figure out if we are hitting video low power mode.
+ VideoLowPowerType videoLowPower = CheckVideoLowPower();
+ EmitTelemetryForVideoLowPower(videoLowPower);
+ }
+
+ return true;
+}
+
+UniquePtr<NativeLayerRootSnapshotter> NativeLayerRootCA::CreateSnapshotter() {
+ MutexAutoLock lock(mMutex);
+ MOZ_RELEASE_ASSERT(
+ !mWeakSnapshotter,
+ "No NativeLayerRootSnapshotter for this NativeLayerRoot should exist when this is called");
+
+ auto cr = NativeLayerRootSnapshotterCA::Create(this, mOffscreenRepresentation.mRootCALayer);
+ if (cr) {
+ mWeakSnapshotter = cr.get();
+ }
+ return cr;
+}
+
+void NativeLayerRootCA::OnNativeLayerRootSnapshotterDestroyed(
+ NativeLayerRootSnapshotterCA* aNativeLayerRootSnapshotter) {
+ MutexAutoLock lock(mMutex);
+ MOZ_RELEASE_ASSERT(mWeakSnapshotter == aNativeLayerRootSnapshotter);
+ mWeakSnapshotter = nullptr;
+}
+
+void NativeLayerRootCA::CommitOffscreen() {
+ MutexAutoLock lock(mMutex);
+ mOffscreenRepresentation.Commit(WhichRepresentation::OFFSCREEN, mSublayers, mWindowIsFullscreen);
+}
+
+template <typename F>
+void NativeLayerRootCA::ForAllRepresentations(F aFn) {
+ aFn(mOnscreenRepresentation);
+ aFn(mOffscreenRepresentation);
+}
+
+NativeLayerRootCA::Representation::Representation(CALayer* aRootCALayer)
+ : mRootCALayer([aRootCALayer retain]) {}
+
+NativeLayerRootCA::Representation::~Representation() {
+ if (mMutatedLayerStructure) {
+ // Clear the root layer's sublayers. At this point the window is usually closed, so this
+ // transaction does not cause any screen updates.
+ AutoCATransaction transaction;
+ mRootCALayer.sublayers = @[];
+ }
+
+ [mRootCALayer release];
+}
+
+void NativeLayerRootCA::Representation::Commit(WhichRepresentation aRepresentation,
+ const nsTArray<RefPtr<NativeLayerCA>>& aSublayers,
+ bool aWindowIsFullscreen) {
+ bool mustRebuild = mMutatedLayerStructure;
+ if (!mustRebuild) {
+ // Check which type of update we need to do, if any.
+ NativeLayerCA::UpdateType updateRequired = NativeLayerCA::UpdateType::None;
+
+ for (auto layer : aSublayers) {
+ // Use the ordering of our UpdateType enums to build a maximal update type.
+ updateRequired = std::max(updateRequired, layer->HasUpdate(aRepresentation));
+ if (updateRequired == NativeLayerCA::UpdateType::All) {
+ break;
+ }
+ }
+
+ if (updateRequired == NativeLayerCA::UpdateType::None) {
+ // Nothing more needed, so early exit.
+ return;
+ }
+
+ if (updateRequired == NativeLayerCA::UpdateType::OnlyVideo) {
+ bool allUpdatesSucceeded = std::all_of(
+ aSublayers.begin(), aSublayers.end(), [=](const RefPtr<NativeLayerCA>& layer) {
+ return layer->ApplyChanges(aRepresentation, NativeLayerCA::UpdateType::OnlyVideo);
+ });
+
+ if (allUpdatesSucceeded) {
+ // Nothing more needed, so early exit;
+ return;
+ }
+ }
+ }
+
+ // We're going to do a full update now, which requires a transaction. Update all of the
+ // sublayers. Afterwards, only continue processing the sublayers which have an extent.
+ AutoCATransaction transaction;
+ nsTArray<NativeLayerCA*> sublayersWithExtent;
+ for (auto layer : aSublayers) {
+ mustRebuild |= layer->WillUpdateAffectLayers(aRepresentation);
+ layer->ApplyChanges(aRepresentation, NativeLayerCA::UpdateType::All);
+ CALayer* caLayer = layer->UnderlyingCALayer(aRepresentation);
+ if (!caLayer.masksToBounds || !NSIsEmptyRect(caLayer.bounds)) {
+ // This layer has an extent. If it didn't before, we need to rebuild.
+ mustRebuild |= !layer->HasExtent();
+ layer->SetHasExtent(true);
+ sublayersWithExtent.AppendElement(layer);
+ } else {
+ // This layer has no extent. If it did before, we need to rebuild.
+ mustRebuild |= layer->HasExtent();
+ layer->SetHasExtent(false);
+ }
+
+ // One other reason we may need to rebuild is if the caLayer is not part of the
+ // root layer's sublayers. This might happen if the caLayer was rebuilt.
+ // We construct this check in a way that maximizes the boolean short-circuit,
+ // because we don't want to call containsObject unless absolutely necessary.
+ mustRebuild = mustRebuild || ![mRootCALayer.sublayers containsObject:caLayer];
+ }
+
+ if (mustRebuild) {
+ uint32_t sublayersCount = sublayersWithExtent.Length();
+ NSMutableArray<CALayer*>* sublayers = [NSMutableArray arrayWithCapacity:sublayersCount];
+ for (auto layer : sublayersWithExtent) {
+ [sublayers addObject:layer->UnderlyingCALayer(aRepresentation)];
+ }
+ mRootCALayer.sublayers = sublayers;
+ }
+
+ mMutatedLayerStructure = false;
+}
+
+/* static */ UniquePtr<NativeLayerRootSnapshotterCA> NativeLayerRootSnapshotterCA::Create(
+ NativeLayerRootCA* aLayerRoot, CALayer* aRootCALayer) {
+ if (NS_IsMainThread()) {
+ // Disallow creating snapshotters on the main thread.
+ // On the main thread, any explicit CATransaction / NSAnimationContext is nested within a global
+ // implicit transaction. This makes it impossible to apply CALayer mutations synchronously such
+ // that they become visible to CARenderer. As a result, the snapshotter would not capture
+ // the right output on the main thread.
+ return nullptr;
+ }
+
+ nsCString failureUnused;
+ RefPtr<gl::GLContext> gl =
+ gl::GLContextProvider::CreateHeadless({gl::CreateContextFlags::ALLOW_OFFLINE_RENDERER |
+ gl::CreateContextFlags::REQUIRE_COMPAT_PROFILE},
+ &failureUnused);
+ if (!gl) {
+ return nullptr;
+ }
+
+ return UniquePtr<NativeLayerRootSnapshotterCA>(
+ new NativeLayerRootSnapshotterCA(aLayerRoot, std::move(gl), aRootCALayer));
+}
+
+void NativeLayerRootCA::DumpLayerTreeToFile(const char* aPath) {
+ MutexAutoLock lock(mMutex);
+ NSLog(@"Dumping NativeLayer contents to %s", aPath);
+ std::ofstream fileOutput(aPath);
+ if (fileOutput.fail()) {
+ NSLog(@"Opening %s for writing failed.", aPath);
+ }
+
+ // Make sure floating point values use a period for the decimal separator.
+ fileOutput.imbue(std::locale("C"));
+
+ fileOutput << "<html>\n";
+ for (const auto& layer : mSublayers) {
+ layer->DumpLayer(fileOutput);
+ }
+ fileOutput << "</html>\n";
+ fileOutput.close();
+}
+
+void NativeLayerRootCA::SetWindowIsFullscreen(bool aFullscreen) {
+ MutexAutoLock lock(mMutex);
+
+ if (mWindowIsFullscreen != aFullscreen) {
+ mWindowIsFullscreen = aFullscreen;
+
+ for (auto layer : mSublayers) {
+ layer->SetRootWindowIsFullscreen(mWindowIsFullscreen);
+ }
+ }
+}
+
+/* static */ bool IsCGColorOpaqueBlack(CGColorRef aColor) {
+ if (CGColorEqualToColor(aColor, CGColorGetConstantColor(kCGColorBlack))) {
+ return true;
+ }
+ size_t componentCount = CGColorGetNumberOfComponents(aColor);
+ if (componentCount == 0) {
+ // This will happen if aColor is kCGColorClear. It's not opaque black.
+ return false;
+ }
+
+ const CGFloat* components = CGColorGetComponents(aColor);
+ for (size_t c = 0; c < componentCount - 1; ++c) {
+ if (components[c] > 0.0f) {
+ return false;
+ }
+ }
+ return components[componentCount - 1] >= 1.0f;
+}
+
+VideoLowPowerType NativeLayerRootCA::CheckVideoLowPower() {
+ // This deteremines whether the current layer contents qualify for the
+ // macOS Core Animation video low power mode. Those requirements are
+ // summarized at
+ // https://developer.apple.com/documentation/webkit/delivering_video_content_for_safari
+ // and we verify them by checking:
+ // 1) There must be exactly one video showing.
+ // 2) The topmost CALayer must be a AVSampleBufferDisplayLayer.
+ // 3) The video layer must be showing a buffer encoded in one of the
+ // kCVPixelFormatType_420YpCbCr pixel formats.
+ // 4) The layer below that must cover the entire screen and have a black
+ // background color.
+ // 5) The window must be fullscreen.
+ // This function checks these requirements empirically. If one of the checks
+ // fail, we either return immediately or do additional processing to
+ // determine more detail.
+
+ uint32_t videoLayerCount = 0;
+ NativeLayerCA* topLayer = nullptr;
+ CALayer* topCALayer = nil;
+ CALayer* secondCALayer = nil;
+ bool topLayerIsVideo = false;
+
+ for (auto layer : mSublayers) {
+ // Only layers with extent are contributing to our sublayers.
+ if (layer->HasExtent()) {
+ topLayer = layer;
+
+ secondCALayer = topCALayer;
+ topCALayer = topLayer->UnderlyingCALayer(WhichRepresentation::ONSCREEN);
+ topLayerIsVideo = topLayer->IsVideo();
+ if (topLayerIsVideo) {
+ ++videoLayerCount;
+ }
+ }
+ }
+
+ if (videoLayerCount == 0) {
+ return VideoLowPowerType::NotVideo;
+ }
+
+ // Most importantly, check if the window is fullscreen. If the user is watching
+ // video in a window, then all of the other enums are irrelevant to achieving
+ // the low power mode.
+ if (!mWindowIsFullscreen) {
+ return VideoLowPowerType::FailWindowed;
+ }
+
+ if (videoLayerCount > 1) {
+ return VideoLowPowerType::FailMultipleVideo;
+ }
+
+ if (!topLayerIsVideo) {
+ return VideoLowPowerType::FailOverlaid;
+ }
+
+ if (!secondCALayer || !IsCGColorOpaqueBlack(secondCALayer.backgroundColor) ||
+ !CGRectContainsRect(secondCALayer.frame, secondCALayer.superlayer.bounds)) {
+ return VideoLowPowerType::FailBacking;
+ }
+
+ CALayer* topContentCALayer = topCALayer.sublayers[0];
+ if (![topContentCALayer isKindOfClass:[AVSampleBufferDisplayLayer class]]) {
+ // We didn't create a AVSampleBufferDisplayLayer for the top video layer.
+ // Try to figure out why by following some of the logic in
+ // NativeLayerCA::ShouldSpecializeVideo.
+ if (!nsCocoaFeatures::OnHighSierraOrLater()) {
+ return VideoLowPowerType::FailMacOSVersion;
+ }
+
+ if (!StaticPrefs::gfx_core_animation_specialize_video()) {
+ return VideoLowPowerType::FailPref;
+ }
+
+ // The only remaining reason is that the surface wasn't eligible. We
+ // assert this instead of if-ing it, to ensure that we always have a
+ // return value from this clause.
+#ifdef DEBUG
+ MOZ_ASSERT(topLayer->mTextureHost);
+ MacIOSurface* macIOSurface = topLayer->mTextureHost->GetSurface();
+ CFTypeRefPtr<IOSurfaceRef> surface = macIOSurface->GetIOSurfaceRef();
+ OSType pixelFormat = IOSurfaceGetPixelFormat(surface.get());
+ MOZ_ASSERT(!(pixelFormat == kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange ||
+ pixelFormat == kCVPixelFormatType_420YpCbCr8BiPlanarFullRange ||
+ pixelFormat == kCVPixelFormatType_420YpCbCr10BiPlanarVideoRange ||
+ pixelFormat == kCVPixelFormatType_420YpCbCr10BiPlanarFullRange));
+#endif
+ return VideoLowPowerType::FailSurface;
+ }
+
+ AVSampleBufferDisplayLayer* topVideoLayer = (AVSampleBufferDisplayLayer*)topContentCALayer;
+ if (topVideoLayer.status != AVQueuedSampleBufferRenderingStatusRendering) {
+ return VideoLowPowerType::FailEnqueue;
+ }
+
+ // As best we can tell, we're eligible for video low power mode. Hurrah!
+ return VideoLowPowerType::LowPower;
+}
+
+NativeLayerRootSnapshotterCA::NativeLayerRootSnapshotterCA(NativeLayerRootCA* aLayerRoot,
+ RefPtr<GLContext>&& aGL,
+ CALayer* aRootCALayer)
+ : mLayerRoot(aLayerRoot), mGL(aGL) {
+ AutoCATransaction transaction;
+ mRenderer = [[CARenderer rendererWithCGLContext:gl::GLContextCGL::Cast(mGL)->GetCGLContext()
+ options:nil] retain];
+ mRenderer.layer = aRootCALayer;
+}
+
+NativeLayerRootSnapshotterCA::~NativeLayerRootSnapshotterCA() {
+ mLayerRoot->OnNativeLayerRootSnapshotterDestroyed(this);
+ [mRenderer release];
+}
+
+already_AddRefed<profiler_screenshots::RenderSource>
+NativeLayerRootSnapshotterCA::GetWindowContents(const IntSize& aWindowSize) {
+ UpdateSnapshot(aWindowSize);
+ return do_AddRef(mSnapshot);
+}
+
+void NativeLayerRootSnapshotterCA::UpdateSnapshot(const IntSize& aSize) {
+ CGRect bounds = CGRectMake(0, 0, aSize.width, aSize.height);
+
+ {
+ // Set the correct bounds and scale on the renderer and its root layer. CARenderer always
+ // renders at unit scale, i.e. the coordinates on the root layer must map 1:1 to render target
+ // pixels. But the coordinates on our content layers are in "points", where 1 point maps to 2
+ // device pixels on HiDPI. So in order to render at the full device pixel resolution, we set a
+ // scale transform on the root offscreen layer.
+ AutoCATransaction transaction;
+ mRenderer.layer.bounds = bounds;
+ float scale = mLayerRoot->BackingScale();
+ mRenderer.layer.sublayerTransform = CATransform3DMakeScale(scale, scale, 1);
+ mRenderer.bounds = bounds;
+ }
+
+ mLayerRoot->CommitOffscreen();
+
+ mGL->MakeCurrent();
+
+ bool needToRedrawEverything = false;
+ if (!mSnapshot || mSnapshot->Size() != aSize) {
+ mSnapshot = nullptr;
+ auto fb = gl::MozFramebuffer::Create(mGL, aSize, 0, false);
+ if (!fb) {
+ return;
+ }
+ mSnapshot = new RenderSourceNLRS(std::move(fb));
+ needToRedrawEverything = true;
+ }
+
+ const gl::ScopedBindFramebuffer bindFB(mGL, mSnapshot->FB().mFB);
+ mGL->fViewport(0.0, 0.0, aSize.width, aSize.height);
+
+ // These legacy OpenGL function calls are part of CARenderer's API contract, see CARenderer.h.
+ // The size passed to glOrtho must be the device pixel size of the render target, otherwise
+ // CARenderer will produce incorrect results.
+ glMatrixMode(GL_PROJECTION);
+ glLoadIdentity();
+ glOrtho(0.0, aSize.width, 0.0, aSize.height, -1, 1);
+
+ float mediaTime = CACurrentMediaTime();
+ [mRenderer beginFrameAtTime:mediaTime timeStamp:nullptr];
+ if (needToRedrawEverything) {
+ [mRenderer addUpdateRect:bounds];
+ }
+ if (!CGRectIsEmpty([mRenderer updateBounds])) {
+ // CARenderer assumes the layer tree is opaque. It only ever paints over existing content, it
+ // never erases anything. However, our layer tree is not necessarily opaque. So we manually
+ // erase the area that's going to be redrawn. This ensures correct rendering in the transparent
+ // areas.
+ //
+ // Since we erase the bounds of the update area, this will erase more than necessary if the
+ // update area is not a single rectangle. Unfortunately we cannot get the precise update region
+ // from CARenderer, we can only get the bounds.
+ CGRect updateBounds = [mRenderer updateBounds];
+ gl::ScopedGLState scopedScissorTestState(mGL, LOCAL_GL_SCISSOR_TEST, true);
+ gl::ScopedScissorRect scissor(mGL, updateBounds.origin.x, updateBounds.origin.y,
+ updateBounds.size.width, updateBounds.size.height);
+ mGL->fClearColor(0.0, 0.0, 0.0, 0.0);
+ mGL->fClear(LOCAL_GL_COLOR_BUFFER_BIT);
+ // We erased the update region's bounds. Make sure the entire update bounds get repainted.
+ [mRenderer addUpdateRect:updateBounds];
+ }
+ [mRenderer render];
+ [mRenderer endFrame];
+}
+
+bool NativeLayerRootSnapshotterCA::ReadbackPixels(const IntSize& aReadbackSize,
+ SurfaceFormat aReadbackFormat,
+ const Range<uint8_t>& aReadbackBuffer) {
+ if (aReadbackFormat != SurfaceFormat::B8G8R8A8) {
+ return false;
+ }
+
+ UpdateSnapshot(aReadbackSize);
+ if (!mSnapshot) {
+ return false;
+ }
+
+ const gl::ScopedBindFramebuffer bindFB(mGL, mSnapshot->FB().mFB);
+ gl::ScopedPackState safePackState(mGL);
+ mGL->fReadPixels(0.0f, 0.0f, aReadbackSize.width, aReadbackSize.height, LOCAL_GL_BGRA,
+ LOCAL_GL_UNSIGNED_BYTE, &aReadbackBuffer[0]);
+
+ return true;
+}
+
+already_AddRefed<profiler_screenshots::DownscaleTarget>
+NativeLayerRootSnapshotterCA::CreateDownscaleTarget(const IntSize& aSize) {
+ auto fb = gl::MozFramebuffer::Create(mGL, aSize, 0, false);
+ if (!fb) {
+ return nullptr;
+ }
+ RefPtr<profiler_screenshots::DownscaleTarget> dt = new DownscaleTargetNLRS(mGL, std::move(fb));
+ return dt.forget();
+}
+
+already_AddRefed<profiler_screenshots::AsyncReadbackBuffer>
+NativeLayerRootSnapshotterCA::CreateAsyncReadbackBuffer(const IntSize& aSize) {
+ size_t bufferByteCount = aSize.width * aSize.height * 4;
+ GLuint bufferHandle = 0;
+ mGL->fGenBuffers(1, &bufferHandle);
+
+ gl::ScopedPackState scopedPackState(mGL);
+ mGL->fBindBuffer(LOCAL_GL_PIXEL_PACK_BUFFER, bufferHandle);
+ mGL->fPixelStorei(LOCAL_GL_PACK_ALIGNMENT, 1);
+ mGL->fBufferData(LOCAL_GL_PIXEL_PACK_BUFFER, bufferByteCount, nullptr, LOCAL_GL_STREAM_READ);
+ return MakeAndAddRef<AsyncReadbackBufferNLRS>(mGL, aSize, bufferHandle);
+}
+
+NativeLayerCA::NativeLayerCA(const IntSize& aSize, bool aIsOpaque,
+ SurfacePoolHandleCA* aSurfacePoolHandle)
+ : mMutex("NativeLayerCA"),
+ mSurfacePoolHandle(aSurfacePoolHandle),
+ mSize(aSize),
+ mIsOpaque(aIsOpaque) {
+ MOZ_RELEASE_ASSERT(mSurfacePoolHandle, "Need a non-null surface pool handle.");
+}
+
+NativeLayerCA::NativeLayerCA(bool aIsOpaque)
+ : mMutex("NativeLayerCA"), mSurfacePoolHandle(nullptr), mIsOpaque(aIsOpaque) {
+#ifdef NIGHTLY_BUILD
+ if (StaticPrefs::gfx_core_animation_specialize_video_log()) {
+ NSLog(@"VIDEO_LOG: NativeLayerCA: %p is being created to host video, which will force a video "
+ @"layer rebuild.",
+ this);
+ }
+#endif
+}
+
+CGColorRef CGColorCreateForDeviceColor(gfx::DeviceColor aColor) {
+ if (StaticPrefs::gfx_color_management_native_srgb()) {
+ // Use CGColorCreateSRGB if it's available, otherwise use older macOS API methods,
+ // which unfortunately allocate additional memory for the colorSpace object.
+ if (@available(macOS 10.15, iOS 13.0, *)) {
+ // Even if it is available, we have to address the function dynamically, to keep
+ // compiler happy when building with earlier versions of the SDK.
+ static auto CGColorCreateSRGBPtr = (CGColorRef(*)(CGFloat, CGFloat, CGFloat, CGFloat))dlsym(
+ RTLD_DEFAULT, "CGColorCreateSRGB");
+ if (CGColorCreateSRGBPtr) {
+ return CGColorCreateSRGBPtr(aColor.r, aColor.g, aColor.b, aColor.a);
+ }
+ }
+
+ CGColorSpaceRef colorSpace = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
+ CGFloat components[] = {aColor.r, aColor.g, aColor.b, aColor.a};
+ CGColorRef color = CGColorCreate(colorSpace, components);
+ CFRelease(colorSpace);
+ return color;
+ }
+
+ return CGColorCreateGenericRGB(aColor.r, aColor.g, aColor.b, aColor.a);
+}
+
+NativeLayerCA::NativeLayerCA(gfx::DeviceColor aColor)
+ : mMutex("NativeLayerCA"), mSurfacePoolHandle(nullptr), mIsOpaque(aColor.a >= 1.0f) {
+ MOZ_ASSERT(aColor.a > 0.0f, "Can't handle a fully transparent backdrop.");
+ mColor.AssignUnderCreateRule(CGColorCreateForDeviceColor(aColor));
+}
+
+NativeLayerCA::~NativeLayerCA() {
+#ifdef NIGHTLY_BUILD
+ if (mHasEverAttachExternalImage && StaticPrefs::gfx_core_animation_specialize_video_log()) {
+ NSLog(@"VIDEO_LOG: ~NativeLayerCA: %p is being destroyed after hosting video.", this);
+ }
+#endif
+ if (mInProgressLockedIOSurface) {
+ mInProgressLockedIOSurface->Unlock(false);
+ mInProgressLockedIOSurface = nullptr;
+ }
+ if (mInProgressSurface) {
+ IOSurfaceDecrementUseCount(mInProgressSurface->mSurface.get());
+ mSurfacePoolHandle->ReturnSurfaceToPool(mInProgressSurface->mSurface);
+ }
+ if (mFrontSurface) {
+ mSurfacePoolHandle->ReturnSurfaceToPool(mFrontSurface->mSurface);
+ }
+ for (const auto& surf : mSurfaces) {
+ mSurfacePoolHandle->ReturnSurfaceToPool(surf.mEntry.mSurface);
+ }
+}
+
+void NativeLayerCA::AttachExternalImage(wr::RenderTextureHost* aExternalImage) {
+ MutexAutoLock lock(mMutex);
+
+#ifdef NIGHTLY_BUILD
+ mHasEverAttachExternalImage = true;
+ MOZ_RELEASE_ASSERT(!mHasEverNotifySurfaceReady, "Shouldn't change layer type to external.");
+#endif
+
+ wr::RenderMacIOSurfaceTextureHost* texture = aExternalImage->AsRenderMacIOSurfaceTextureHost();
+ MOZ_ASSERT(texture || aExternalImage->IsWrappingAsyncRemoteTexture());
+ mTextureHost = texture;
+ if (!mTextureHost) {
+ gfxCriticalNoteOnce << "ExternalImage is not RenderMacIOSurfaceTextureHost";
+ return;
+ }
+
+ gfx::IntSize oldSize = mSize;
+ mSize = texture->GetSize(0);
+ bool changedSizeAndDisplayRect = (mSize != oldSize);
+
+ mDisplayRect = IntRect(IntPoint{}, mSize);
+
+ bool oldSpecializeVideo = mSpecializeVideo;
+ mSpecializeVideo = ShouldSpecializeVideo(lock);
+ bool changedSpecializeVideo = (mSpecializeVideo != oldSpecializeVideo);
+#ifdef NIGHTLY_BUILD
+ if (changedSpecializeVideo && StaticPrefs::gfx_core_animation_specialize_video_log()) {
+ NSLog(@"VIDEO_LOG: AttachExternalImage: %p is forcing a video layer rebuild.", this);
+ }
+#endif
+
+ bool oldIsDRM = mIsDRM;
+ mIsDRM = aExternalImage->IsFromDRMSource();
+ bool changedIsDRM = (mIsDRM != oldIsDRM);
+
+ ForAllRepresentations([&](Representation& r) {
+ r.mMutatedFrontSurface = true;
+ r.mMutatedDisplayRect |= changedSizeAndDisplayRect;
+ r.mMutatedSize |= changedSizeAndDisplayRect;
+ r.mMutatedSpecializeVideo |= changedSpecializeVideo;
+ r.mMutatedIsDRM |= changedIsDRM;
+ });
+}
+
+bool NativeLayerCA::IsVideo() {
+ // Anything with a texture host is considered a video source.
+ return mTextureHost;
+}
+
+bool NativeLayerCA::IsVideoAndLocked(const MutexAutoLock& aProofOfLock) {
+ // Anything with a texture host is considered a video source.
+ return mTextureHost;
+}
+
+bool NativeLayerCA::ShouldSpecializeVideo(const MutexAutoLock& aProofOfLock) {
+ if (!IsVideoAndLocked(aProofOfLock)) {
+ // Only videos are eligible.
+ return false;
+ }
+
+ if (!nsCocoaFeatures::OnHighSierraOrLater()) {
+ // We must be on a modern-enough macOS.
+ return false;
+ }
+
+ MOZ_ASSERT(mTextureHost);
+
+ // DRM video is supported in macOS 10.15 and beyond, and such video must use
+ // a specialized video layer.
+ if (@available(macOS 10.15, iOS 13.0, *)) {
+ if (mTextureHost->IsFromDRMSource()) {
+ return true;
+ }
+ }
+
+ // Beyond this point, we need to know about the format of the video.
+ MacIOSurface* macIOSurface = mTextureHost->GetSurface();
+ if (macIOSurface->GetYUVColorSpace() == gfx::YUVColorSpace::BT2020) {
+ // BT2020 is a signifier of HDR color space, whether or not the bit depth
+ // is expanded to cover that color space. This video needs a specialized
+ // video layer.
+ return true;
+ }
+
+ CFTypeRefPtr<IOSurfaceRef> surface = macIOSurface->GetIOSurfaceRef();
+ OSType pixelFormat = IOSurfaceGetPixelFormat(surface.get());
+ if (pixelFormat == kCVPixelFormatType_420YpCbCr10BiPlanarVideoRange ||
+ pixelFormat == kCVPixelFormatType_420YpCbCr10BiPlanarFullRange) {
+ // HDR videos require specialized video layers.
+ return true;
+ }
+
+ // Beyond this point, we return true if-and-only-if we think we can achieve
+ // the power-saving "detached mode" of the macOS compositor.
+
+ if (!StaticPrefs::gfx_core_animation_specialize_video()) {
+ // Pref must be set.
+ return false;
+ }
+
+ if (pixelFormat != kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange &&
+ pixelFormat != kCVPixelFormatType_420YpCbCr8BiPlanarFullRange) {
+ // The video is not in one of the formats that qualifies for detachment.
+ return false;
+ }
+
+ // It will only detach if we're fullscreen.
+ return mRootWindowIsFullscreen;
+}
+
+void NativeLayerCA::SetRootWindowIsFullscreen(bool aFullscreen) {
+ if (mRootWindowIsFullscreen == aFullscreen) {
+ return;
+ }
+
+ MutexAutoLock lock(mMutex);
+
+ mRootWindowIsFullscreen = aFullscreen;
+
+ bool oldSpecializeVideo = mSpecializeVideo;
+ mSpecializeVideo = ShouldSpecializeVideo(lock);
+ bool changedSpecializeVideo = (mSpecializeVideo != oldSpecializeVideo);
+
+ if (changedSpecializeVideo) {
+#ifdef NIGHTLY_BUILD
+ if (StaticPrefs::gfx_core_animation_specialize_video_log()) {
+ NSLog(@"VIDEO_LOG: SetRootWindowIsFullscreen: %p is forcing a video layer rebuild.", this);
+ }
+#endif
+
+ ForAllRepresentations([&](Representation& r) { r.mMutatedSpecializeVideo = true; });
+ }
+}
+
+void NativeLayerCA::SetSurfaceIsFlipped(bool aIsFlipped) {
+ MutexAutoLock lock(mMutex);
+
+ if (aIsFlipped != mSurfaceIsFlipped) {
+ mSurfaceIsFlipped = aIsFlipped;
+ ForAllRepresentations([&](Representation& r) { r.mMutatedSurfaceIsFlipped = true; });
+ }
+}
+
+bool NativeLayerCA::SurfaceIsFlipped() {
+ MutexAutoLock lock(mMutex);
+ return mSurfaceIsFlipped;
+}
+
+IntSize NativeLayerCA::GetSize() {
+ MutexAutoLock lock(mMutex);
+ return mSize;
+}
+
+void NativeLayerCA::SetPosition(const IntPoint& aPosition) {
+ MutexAutoLock lock(mMutex);
+
+ if (aPosition != mPosition) {
+ mPosition = aPosition;
+ ForAllRepresentations([&](Representation& r) { r.mMutatedPosition = true; });
+ }
+}
+
+IntPoint NativeLayerCA::GetPosition() {
+ MutexAutoLock lock(mMutex);
+ return mPosition;
+}
+
+void NativeLayerCA::SetTransform(const Matrix4x4& aTransform) {
+ MutexAutoLock lock(mMutex);
+ MOZ_ASSERT(aTransform.IsRectilinear());
+
+ if (aTransform != mTransform) {
+ mTransform = aTransform;
+ ForAllRepresentations([&](Representation& r) { r.mMutatedTransform = true; });
+ }
+}
+
+void NativeLayerCA::SetSamplingFilter(gfx::SamplingFilter aSamplingFilter) {
+ MutexAutoLock lock(mMutex);
+
+ if (aSamplingFilter != mSamplingFilter) {
+ mSamplingFilter = aSamplingFilter;
+ ForAllRepresentations([&](Representation& r) { r.mMutatedSamplingFilter = true; });
+ }
+}
+
+Matrix4x4 NativeLayerCA::GetTransform() {
+ MutexAutoLock lock(mMutex);
+ return mTransform;
+}
+
+IntRect NativeLayerCA::GetRect() {
+ MutexAutoLock lock(mMutex);
+ return IntRect(mPosition, mSize);
+}
+
+void NativeLayerCA::SetBackingScale(float aBackingScale) {
+ MutexAutoLock lock(mMutex);
+
+ if (aBackingScale != mBackingScale) {
+ mBackingScale = aBackingScale;
+ ForAllRepresentations([&](Representation& r) { r.mMutatedBackingScale = true; });
+ }
+}
+
+bool NativeLayerCA::IsOpaque() {
+ // mIsOpaque is const, so no need for a lock.
+ return mIsOpaque;
+}
+
+void NativeLayerCA::SetClipRect(const Maybe<gfx::IntRect>& aClipRect) {
+ MutexAutoLock lock(mMutex);
+
+ if (aClipRect != mClipRect) {
+ mClipRect = aClipRect;
+ ForAllRepresentations([&](Representation& r) { r.mMutatedClipRect = true; });
+ }
+}
+
+Maybe<gfx::IntRect> NativeLayerCA::ClipRect() {
+ MutexAutoLock lock(mMutex);
+ return mClipRect;
+}
+
+void NativeLayerCA::DumpLayer(std::ostream& aOutputStream) {
+ MutexAutoLock lock(mMutex);
+
+ Maybe<CGRect> scaledClipRect =
+ CalculateClipGeometry(mSize, mPosition, mTransform, mDisplayRect, mClipRect, mBackingScale);
+
+ CGRect useClipRect;
+ if (scaledClipRect.isSome()) {
+ useClipRect = *scaledClipRect;
+ } else {
+ useClipRect = CGRectZero;
+ }
+
+ aOutputStream << "<div style=\"";
+ aOutputStream << "position: absolute; ";
+ aOutputStream << "left: " << useClipRect.origin.x << "px; ";
+ aOutputStream << "top: " << useClipRect.origin.y << "px; ";
+ aOutputStream << "width: " << useClipRect.size.width << "px; ";
+ aOutputStream << "height: " << useClipRect.size.height << "px; ";
+
+ if (scaledClipRect.isSome()) {
+ aOutputStream << "overflow: hidden; ";
+ }
+
+ if (mColor) {
+ const CGFloat* components = CGColorGetComponents(mColor.get());
+ aOutputStream << "background: rgb(" << components[0] * 255.0f << " " << components[1] * 255.0f
+ << " " << components[2] * 255.0f << "); opacity: " << components[3] << "; ";
+
+ // That's all we need for color layers. We don't need to specify an image.
+ aOutputStream << "\"/></div>\n";
+ return;
+ }
+
+ aOutputStream << "\">";
+
+ auto size = gfx::Size(mSize) / mBackingScale;
+
+ aOutputStream << "<img style=\"";
+ aOutputStream << "width: " << size.width << "px; ";
+ aOutputStream << "height: " << size.height << "px; ";
+
+ if (mSamplingFilter == gfx::SamplingFilter::POINT) {
+ aOutputStream << "image-rendering: crisp-edges; ";
+ }
+
+ Matrix4x4 transform = mTransform;
+ transform.PreTranslate(mPosition.x, mPosition.y, 0);
+ transform.PostTranslate((-useClipRect.origin.x * mBackingScale),
+ (-useClipRect.origin.y * mBackingScale), 0);
+
+ if (mSurfaceIsFlipped) {
+ transform.PreTranslate(0, mSize.height, 0).PreScale(1, -1, 1);
+ }
+
+ if (!transform.IsIdentity()) {
+ const auto& m = transform;
+ aOutputStream << "transform-origin: top left; ";
+ aOutputStream << "transform: matrix3d(";
+ aOutputStream << m._11 << ", " << m._12 << ", " << m._13 << ", " << m._14 << ", ";
+ aOutputStream << m._21 << ", " << m._22 << ", " << m._23 << ", " << m._24 << ", ";
+ aOutputStream << m._31 << ", " << m._32 << ", " << m._33 << ", " << m._34 << ", ";
+ aOutputStream << m._41 / mBackingScale << ", " << m._42 / mBackingScale << ", " << m._43 << ", "
+ << m._44;
+ aOutputStream << "); ";
+ }
+ aOutputStream << "\" ";
+
+ CFTypeRefPtr<IOSurfaceRef> surface;
+ if (mFrontSurface) {
+ surface = mFrontSurface->mSurface;
+ aOutputStream << "alt=\"regular surface 0x" << std::hex << int(IOSurfaceGetID(surface.get()))
+ << "\" ";
+ } else if (mTextureHost) {
+ surface = mTextureHost->GetSurface()->GetIOSurfaceRef();
+ aOutputStream << "alt=\"TextureHost surface 0x" << std::hex
+ << int(IOSurfaceGetID(surface.get())) << "\" ";
+ } else {
+ aOutputStream << "alt=\"no surface 0x\" ";
+ }
+
+ aOutputStream << "src=\"";
+
+ if (surface) {
+ // Attempt to render the surface as a PNG. Skia can do this for RGB surfaces.
+ RefPtr<MacIOSurface> surf = new MacIOSurface(surface);
+ surf->Lock(true);
+ SurfaceFormat format = surf->GetFormat();
+ if (format == SurfaceFormat::B8G8R8A8 || format == SurfaceFormat::B8G8R8X8) {
+ RefPtr<gfx::DrawTarget> dt = surf->GetAsDrawTargetLocked(gfx::BackendType::SKIA);
+ if (dt) {
+ RefPtr<gfx::SourceSurface> sourceSurf = dt->Snapshot();
+ nsCString dataUrl;
+ gfxUtils::EncodeSourceSurface(sourceSurf, ImageType::PNG, u""_ns, gfxUtils::eDataURIEncode,
+ nullptr, &dataUrl);
+ aOutputStream << dataUrl.get();
+ }
+ }
+ surf->Unlock(true);
+ }
+
+ aOutputStream << "\"/></div>\n";
+}
+
+gfx::IntRect NativeLayerCA::CurrentSurfaceDisplayRect() {
+ MutexAutoLock lock(mMutex);
+ return mDisplayRect;
+}
+
+NativeLayerCA::Representation::Representation()
+ : mMutatedPosition(true),
+ mMutatedTransform(true),
+ mMutatedDisplayRect(true),
+ mMutatedClipRect(true),
+ mMutatedBackingScale(true),
+ mMutatedSize(true),
+ mMutatedSurfaceIsFlipped(true),
+ mMutatedFrontSurface(true),
+ mMutatedSamplingFilter(true),
+ mMutatedSpecializeVideo(true),
+ mMutatedIsDRM(true) {}
+
+NativeLayerCA::Representation::~Representation() {
+ [mContentCALayer release];
+ [mOpaquenessTintLayer release];
+ [mWrappingCALayer release];
+}
+
+void NativeLayerCA::InvalidateRegionThroughoutSwapchain(const MutexAutoLock& aProofOfLock,
+ const IntRegion& aRegion) {
+ IntRegion r = aRegion;
+ if (mInProgressSurface) {
+ mInProgressSurface->mInvalidRegion.OrWith(r);
+ }
+ if (mFrontSurface) {
+ mFrontSurface->mInvalidRegion.OrWith(r);
+ }
+ for (auto& surf : mSurfaces) {
+ surf.mEntry.mInvalidRegion.OrWith(r);
+ }
+}
+
+bool NativeLayerCA::NextSurface(const MutexAutoLock& aProofOfLock) {
+ if (mSize.IsEmpty()) {
+ gfxCriticalError() << "NextSurface returning false because of invalid mSize (" << mSize.width
+ << ", " << mSize.height << ").";
+ return false;
+ }
+
+ MOZ_RELEASE_ASSERT(
+ !mInProgressSurface,
+ "ERROR: Do not call NextSurface twice in sequence. Call NotifySurfaceReady before the "
+ "next call to NextSurface.");
+
+ Maybe<SurfaceWithInvalidRegion> surf = GetUnusedSurfaceAndCleanUp(aProofOfLock);
+ if (!surf) {
+ CFTypeRefPtr<IOSurfaceRef> newSurf = mSurfacePoolHandle->ObtainSurfaceFromPool(mSize);
+ MOZ_RELEASE_ASSERT(newSurf, "NextSurface IOSurfaceCreate failed to create the surface.");
+ surf = Some(SurfaceWithInvalidRegion{newSurf, IntRect({}, mSize)});
+ }
+
+ mInProgressSurface = std::move(surf);
+ IOSurfaceIncrementUseCount(mInProgressSurface->mSurface.get());
+ return true;
+}
+
+template <typename F>
+void NativeLayerCA::HandlePartialUpdate(const MutexAutoLock& aProofOfLock,
+ const IntRect& aDisplayRect, const IntRegion& aUpdateRegion,
+ F&& aCopyFn) {
+ MOZ_RELEASE_ASSERT(IntRect({}, mSize).Contains(aUpdateRegion.GetBounds()),
+ "The update region should be within the surface bounds.");
+ MOZ_RELEASE_ASSERT(IntRect({}, mSize).Contains(aDisplayRect),
+ "The display rect should be within the surface bounds.");
+
+ MOZ_RELEASE_ASSERT(!mInProgressUpdateRegion);
+ MOZ_RELEASE_ASSERT(!mInProgressDisplayRect);
+
+ mInProgressUpdateRegion = Some(aUpdateRegion);
+ mInProgressDisplayRect = Some(aDisplayRect);
+
+ if (mFrontSurface) {
+ // Copy not-overwritten valid content from mFrontSurface so that valid content never gets lost.
+ gfx::IntRegion copyRegion;
+ copyRegion.Sub(mInProgressSurface->mInvalidRegion, aUpdateRegion);
+ copyRegion.SubOut(mFrontSurface->mInvalidRegion);
+
+ if (!copyRegion.IsEmpty()) {
+ // Now copy the valid content, using a caller-provided copy function.
+ aCopyFn(mFrontSurface->mSurface, copyRegion);
+ mInProgressSurface->mInvalidRegion.SubOut(copyRegion);
+ }
+ }
+
+ InvalidateRegionThroughoutSwapchain(aProofOfLock, aUpdateRegion);
+}
+
+RefPtr<gfx::DrawTarget> NativeLayerCA::NextSurfaceAsDrawTarget(const IntRect& aDisplayRect,
+ const IntRegion& aUpdateRegion,
+ gfx::BackendType aBackendType) {
+ MutexAutoLock lock(mMutex);
+ if (!NextSurface(lock)) {
+ return nullptr;
+ }
+
+ mInProgressLockedIOSurface = new MacIOSurface(mInProgressSurface->mSurface);
+ mInProgressLockedIOSurface->Lock(false);
+ RefPtr<gfx::DrawTarget> dt = mInProgressLockedIOSurface->GetAsDrawTargetLocked(aBackendType);
+
+ HandlePartialUpdate(
+ lock, aDisplayRect, aUpdateRegion,
+ [&](CFTypeRefPtr<IOSurfaceRef> validSource, const gfx::IntRegion& copyRegion) {
+ RefPtr<MacIOSurface> source = new MacIOSurface(validSource);
+ source->Lock(true);
+ {
+ RefPtr<gfx::DrawTarget> sourceDT = source->GetAsDrawTargetLocked(aBackendType);
+ RefPtr<gfx::SourceSurface> sourceSurface = sourceDT->Snapshot();
+
+ for (auto iter = copyRegion.RectIter(); !iter.Done(); iter.Next()) {
+ const gfx::IntRect& r = iter.Get();
+ dt->CopySurface(sourceSurface, r, r.TopLeft());
+ }
+ }
+ source->Unlock(true);
+ });
+
+ return dt;
+}
+
+Maybe<GLuint> NativeLayerCA::NextSurfaceAsFramebuffer(const IntRect& aDisplayRect,
+ const IntRegion& aUpdateRegion,
+ bool aNeedsDepth) {
+ MutexAutoLock lock(mMutex);
+ MOZ_RELEASE_ASSERT(NextSurface(lock), "NextSurfaceAsFramebuffer needs a surface.");
+
+ Maybe<GLuint> fbo =
+ mSurfacePoolHandle->GetFramebufferForSurface(mInProgressSurface->mSurface, aNeedsDepth);
+ MOZ_RELEASE_ASSERT(fbo, "GetFramebufferForSurface failed.");
+
+ HandlePartialUpdate(
+ lock, aDisplayRect, aUpdateRegion,
+ [&](CFTypeRefPtr<IOSurfaceRef> validSource, const gfx::IntRegion& copyRegion) {
+ // Copy copyRegion from validSource to fbo.
+ MOZ_RELEASE_ASSERT(mSurfacePoolHandle->gl());
+ mSurfacePoolHandle->gl()->MakeCurrent();
+ Maybe<GLuint> sourceFBO = mSurfacePoolHandle->GetFramebufferForSurface(validSource, false);
+ MOZ_RELEASE_ASSERT(sourceFBO,
+ "GetFramebufferForSurface failed during HandlePartialUpdate.");
+ for (auto iter = copyRegion.RectIter(); !iter.Done(); iter.Next()) {
+ gfx::IntRect r = iter.Get();
+ if (mSurfaceIsFlipped) {
+ r.y = mSize.height - r.YMost();
+ }
+ mSurfacePoolHandle->gl()->BlitHelper()->BlitFramebufferToFramebuffer(*sourceFBO, *fbo, r,
+ r, LOCAL_GL_NEAREST);
+ }
+ });
+
+ return fbo;
+}
+
+void NativeLayerCA::NotifySurfaceReady() {
+ MutexAutoLock lock(mMutex);
+
+#ifdef NIGHTLY_BUILD
+ mHasEverNotifySurfaceReady = true;
+ MOZ_RELEASE_ASSERT(!mHasEverAttachExternalImage, "Shouldn't change layer type to drawn.");
+#endif
+
+ MOZ_RELEASE_ASSERT(mInProgressSurface,
+ "NotifySurfaceReady called without preceding call to NextSurface");
+
+ if (mInProgressLockedIOSurface) {
+ mInProgressLockedIOSurface->Unlock(false);
+ mInProgressLockedIOSurface = nullptr;
+ }
+
+ if (mFrontSurface) {
+ mSurfaces.push_back({*mFrontSurface, 0});
+ mFrontSurface = Nothing();
+ }
+
+ MOZ_RELEASE_ASSERT(mInProgressUpdateRegion);
+ IOSurfaceDecrementUseCount(mInProgressSurface->mSurface.get());
+ mFrontSurface = std::move(mInProgressSurface);
+ mFrontSurface->mInvalidRegion.SubOut(mInProgressUpdateRegion.extract());
+
+ ForAllRepresentations([&](Representation& r) { r.mMutatedFrontSurface = true; });
+
+ MOZ_RELEASE_ASSERT(mInProgressDisplayRect);
+ if (!mDisplayRect.IsEqualInterior(*mInProgressDisplayRect)) {
+ mDisplayRect = *mInProgressDisplayRect;
+ ForAllRepresentations([&](Representation& r) { r.mMutatedDisplayRect = true; });
+ }
+ mInProgressDisplayRect = Nothing();
+}
+
+void NativeLayerCA::DiscardBackbuffers() {
+ MutexAutoLock lock(mMutex);
+
+ for (const auto& surf : mSurfaces) {
+ mSurfacePoolHandle->ReturnSurfaceToPool(surf.mEntry.mSurface);
+ }
+ mSurfaces.clear();
+}
+
+NativeLayerCA::Representation& NativeLayerCA::GetRepresentation(
+ WhichRepresentation aRepresentation) {
+ switch (aRepresentation) {
+ case WhichRepresentation::ONSCREEN:
+ return mOnscreenRepresentation;
+ case WhichRepresentation::OFFSCREEN:
+ return mOffscreenRepresentation;
+ }
+}
+
+template <typename F>
+void NativeLayerCA::ForAllRepresentations(F aFn) {
+ aFn(mOnscreenRepresentation);
+ aFn(mOffscreenRepresentation);
+}
+
+NativeLayerCA::UpdateType NativeLayerCA::HasUpdate(WhichRepresentation aRepresentation) {
+ MutexAutoLock lock(mMutex);
+ return GetRepresentation(aRepresentation).HasUpdate(IsVideoAndLocked(lock));
+}
+
+/* static */
+Maybe<CGRect> NativeLayerCA::CalculateClipGeometry(
+ const gfx::IntSize& aSize, const gfx::IntPoint& aPosition, const gfx::Matrix4x4& aTransform,
+ const gfx::IntRect& aDisplayRect, const Maybe<gfx::IntRect>& aClipRect, float aBackingScale) {
+ Maybe<IntRect> clipFromDisplayRect;
+ if (!aDisplayRect.IsEqualInterior(IntRect({}, aSize))) {
+ // When the display rect is a subset of the layer, then we want to guarantee that no
+ // pixels outside that rect are sampled, since they might be uninitialized.
+ // Transforming the display rect into a post-transform clip only maintains this if
+ // it's an integer translation, which is all we support for this case currently.
+ MOZ_ASSERT(aTransform.Is2DIntegerTranslation());
+ clipFromDisplayRect =
+ Some(RoundedToInt(aTransform.TransformBounds(IntRectToRect(aDisplayRect + aPosition))));
+ }
+
+ Maybe<gfx::IntRect> effectiveClip = IntersectMaybeRects(aClipRect, clipFromDisplayRect);
+ if (!effectiveClip) {
+ return Nothing();
+ }
+
+ return Some(CGRectMake(effectiveClip->X() / aBackingScale, effectiveClip->Y() / aBackingScale,
+ effectiveClip->Width() / aBackingScale,
+ effectiveClip->Height() / aBackingScale));
+}
+
+bool NativeLayerCA::ApplyChanges(WhichRepresentation aRepresentation,
+ NativeLayerCA::UpdateType aUpdate) {
+ MutexAutoLock lock(mMutex);
+ CFTypeRefPtr<IOSurfaceRef> surface;
+ if (mFrontSurface) {
+ surface = mFrontSurface->mSurface;
+ } else if (mTextureHost) {
+ surface = mTextureHost->GetSurface()->GetIOSurfaceRef();
+ }
+ return GetRepresentation(aRepresentation)
+ .ApplyChanges(aUpdate, mSize, mIsOpaque, mPosition, mTransform, mDisplayRect, mClipRect,
+ mBackingScale, mSurfaceIsFlipped, mSamplingFilter, mSpecializeVideo, surface,
+ mColor, mIsDRM, IsVideo());
+}
+
+CALayer* NativeLayerCA::UnderlyingCALayer(WhichRepresentation aRepresentation) {
+ MutexAutoLock lock(mMutex);
+ return GetRepresentation(aRepresentation).UnderlyingCALayer();
+}
+
+static NSString* NSStringForOSType(OSType type) {
+ unichar c[4];
+ c[0] = (type >> 24) & 0xFF;
+ c[1] = (type >> 16) & 0xFF;
+ c[2] = (type >> 8) & 0xFF;
+ c[3] = (type >> 0) & 0xFF;
+ NSString* string = [[NSString stringWithCharacters:c length:4] autorelease];
+ return string;
+}
+
+/* static */ void LogSurface(IOSurfaceRef aSurfaceRef, CVPixelBufferRef aBuffer,
+ CMVideoFormatDescriptionRef aFormat) {
+ NSLog(@"VIDEO_LOG: LogSurface...\n");
+
+ CFDictionaryRef surfaceValues = IOSurfaceCopyAllValues(aSurfaceRef);
+ NSLog(@"Surface values are %@.\n", surfaceValues);
+ CFRelease(surfaceValues);
+
+ if (aBuffer) {
+ CGColorSpaceRef colorSpace = CVImageBufferGetColorSpace(aBuffer);
+ NSLog(@"ColorSpace is %@.\n", colorSpace);
+
+ CFDictionaryRef bufferAttachments =
+ CVBufferGetAttachments(aBuffer, kCVAttachmentMode_ShouldPropagate);
+ NSLog(@"Buffer attachments are %@.\n", bufferAttachments);
+ }
+
+ if (aFormat) {
+ OSType codec = CMFormatDescriptionGetMediaSubType(aFormat);
+ NSLog(@"Codec is %@.\n", NSStringForOSType(codec));
+
+ CFDictionaryRef extensions = CMFormatDescriptionGetExtensions(aFormat);
+ NSLog(@"Format extensions are %@.\n", extensions);
+ }
+}
+
+bool NativeLayerCA::Representation::EnqueueSurface(IOSurfaceRef aSurfaceRef) {
+ MOZ_ASSERT([mContentCALayer isKindOfClass:[AVSampleBufferDisplayLayer class]]);
+ AVSampleBufferDisplayLayer* videoLayer = (AVSampleBufferDisplayLayer*)mContentCALayer;
+
+ if (@available(macOS 11.0, iOS 14.0, *)) {
+ if (videoLayer.requiresFlushToResumeDecoding) {
+ [videoLayer flush];
+ }
+ }
+
+ // If the layer can't handle a new sample, early exit.
+ if (!videoLayer.readyForMoreMediaData) {
+#ifdef NIGHTLY_BUILD
+ if (StaticPrefs::gfx_core_animation_specialize_video_log()) {
+ NSLog(@"VIDEO_LOG: EnqueueSurface failed on readyForMoreMediaData.");
+ }
+#endif
+ return false;
+ }
+
+ // Convert the IOSurfaceRef into a CMSampleBuffer, so we can enqueue it in mContentCALayer
+ CVPixelBufferRef pixelBuffer = nullptr;
+ CVReturn cvValue =
+ CVPixelBufferCreateWithIOSurface(kCFAllocatorDefault, aSurfaceRef, nullptr, &pixelBuffer);
+ if (cvValue != kCVReturnSuccess) {
+ MOZ_ASSERT(pixelBuffer == nullptr, "Failed call shouldn't allocate memory.");
+#ifdef NIGHTLY_BUILD
+ if (StaticPrefs::gfx_core_animation_specialize_video_log()) {
+ NSLog(@"VIDEO_LOG: EnqueueSurface failed on allocating pixel buffer.");
+ }
+#endif
+ return false;
+ }
+
+#ifdef NIGHTLY_BUILD
+ if (StaticPrefs::gfx_core_animation_specialize_video_check_color_space()) {
+ // Ensure the resulting pixel buffer has a color space. If it doesn't, then modify
+ // the surface and create the buffer again.
+ CFTypeRefPtr<CGColorSpaceRef> colorSpace =
+ CFTypeRefPtr<CGColorSpaceRef>::WrapUnderGetRule(CVImageBufferGetColorSpace(pixelBuffer));
+ if (!colorSpace) {
+ // Use our main display color space.
+ colorSpace = CFTypeRefPtr<CGColorSpaceRef>::WrapUnderCreateRule(
+ CGDisplayCopyColorSpace(CGMainDisplayID()));
+ auto colorData =
+ CFTypeRefPtr<CFDataRef>::WrapUnderCreateRule(CGColorSpaceCopyICCData(colorSpace.get()));
+ IOSurfaceSetValue(aSurfaceRef, CFSTR("IOSurfaceColorSpace"), colorData.get());
+
+ // Get rid of our old pixel buffer and create a new one.
+ CFRelease(pixelBuffer);
+ cvValue =
+ CVPixelBufferCreateWithIOSurface(kCFAllocatorDefault, aSurfaceRef, nullptr, &pixelBuffer);
+ if (cvValue != kCVReturnSuccess) {
+ MOZ_ASSERT(pixelBuffer == nullptr, "Failed call shouldn't allocate memory.");
+ return false;
+ }
+ }
+ MOZ_ASSERT(CVImageBufferGetColorSpace(pixelBuffer), "Pixel buffer should have a color space.");
+ }
+#endif
+
+ CFTypeRefPtr<CVPixelBufferRef> pixelBufferDeallocator =
+ CFTypeRefPtr<CVPixelBufferRef>::WrapUnderCreateRule(pixelBuffer);
+
+ CMVideoFormatDescriptionRef formatDescription = nullptr;
+ OSStatus osValue = CMVideoFormatDescriptionCreateForImageBuffer(kCFAllocatorDefault, pixelBuffer,
+ &formatDescription);
+ if (osValue != noErr) {
+ MOZ_ASSERT(formatDescription == nullptr, "Failed call shouldn't allocate memory.");
+#ifdef NIGHTLY_BUILD
+ if (StaticPrefs::gfx_core_animation_specialize_video_log()) {
+ NSLog(@"VIDEO_LOG: EnqueueSurface failed on allocating format description.");
+ }
+#endif
+ return false;
+ }
+ CFTypeRefPtr<CMVideoFormatDescriptionRef> formatDescriptionDeallocator =
+ CFTypeRefPtr<CMVideoFormatDescriptionRef>::WrapUnderCreateRule(formatDescription);
+
+#ifdef NIGHTLY_BUILD
+ if (mLogNextVideoSurface && StaticPrefs::gfx_core_animation_specialize_video_log()) {
+ LogSurface(aSurfaceRef, pixelBuffer, formatDescription);
+ mLogNextVideoSurface = false;
+ }
+#endif
+
+ CMSampleTimingInfo timingInfo = kCMTimingInfoInvalid;
+
+ bool spoofTiming = false;
+#ifdef NIGHTLY_BUILD
+ spoofTiming = StaticPrefs::gfx_core_animation_specialize_video_spoof_timing();
+#endif
+ if (spoofTiming) {
+ // Since we don't have timing information for the sample, set the sample to play at the
+ // current timestamp.
+ CMTimebaseRef timebase = [(AVSampleBufferDisplayLayer*)mContentCALayer controlTimebase];
+ CMTime nowTime = CMTimebaseGetTime(timebase);
+ timingInfo = {.presentationTimeStamp = nowTime};
+ }
+
+ CMSampleBufferRef sampleBuffer = nullptr;
+ osValue = CMSampleBufferCreateReadyWithImageBuffer(kCFAllocatorDefault, pixelBuffer,
+ formatDescription, &timingInfo, &sampleBuffer);
+ if (osValue != noErr) {
+ MOZ_ASSERT(sampleBuffer == nullptr, "Failed call shouldn't allocate memory.");
+#ifdef NIGHTLY_BUILD
+ if (StaticPrefs::gfx_core_animation_specialize_video_log()) {
+ NSLog(@"VIDEO_LOG: EnqueueSurface failed on allocating sample buffer.");
+ }
+#endif
+ return false;
+ }
+ CFTypeRefPtr<CMSampleBufferRef> sampleBufferDeallocator =
+ CFTypeRefPtr<CMSampleBufferRef>::WrapUnderCreateRule(sampleBuffer);
+
+ if (!spoofTiming) {
+ // Since we don't have timing information for the sample, before we enqueue it, we
+ // attach an attribute that specifies that the sample should be played immediately.
+ CFArrayRef attachmentsArray = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, YES);
+ if (!attachmentsArray || CFArrayGetCount(attachmentsArray) == 0) {
+ // No dictionary to alter.
+ return false;
+ }
+ CFMutableDictionaryRef sample0Dictionary =
+ (__bridge CFMutableDictionaryRef)CFArrayGetValueAtIndex(attachmentsArray, 0);
+ CFDictionarySetValue(sample0Dictionary, kCMSampleAttachmentKey_DisplayImmediately,
+ kCFBooleanTrue);
+ }
+
+ [videoLayer enqueueSampleBuffer:sampleBuffer];
+
+ return true;
+}
+
+bool NativeLayerCA::Representation::ApplyChanges(
+ NativeLayerCA::UpdateType aUpdate, const IntSize& aSize, bool aIsOpaque,
+ const IntPoint& aPosition, const Matrix4x4& aTransform, const IntRect& aDisplayRect,
+ const Maybe<IntRect>& aClipRect, float aBackingScale, bool aSurfaceIsFlipped,
+ gfx::SamplingFilter aSamplingFilter, bool aSpecializeVideo,
+ CFTypeRefPtr<IOSurfaceRef> aFrontSurface, CFTypeRefPtr<CGColorRef> aColor, bool aIsDRM,
+ bool aIsVideo) {
+ // If we have an OnlyVideo update, handle it and early exit.
+ if (aUpdate == UpdateType::OnlyVideo) {
+ // If we don't have any updates to do, exit early with success. This is
+ // important to do so that the overall OnlyVideo pass will succeed as long
+ // as the video layers are successful.
+ if (HasUpdate(true) == UpdateType::None) {
+ return true;
+ }
+
+ MOZ_ASSERT(!mMutatedSpecializeVideo && mMutatedFrontSurface,
+ "Shouldn't attempt a OnlyVideo update in this case.");
+
+ bool updateSucceeded = false;
+ if (aSpecializeVideo) {
+ IOSurfaceRef surface = aFrontSurface.get();
+ updateSucceeded = EnqueueSurface(surface);
+
+ if (updateSucceeded) {
+ mMutatedFrontSurface = false;
+ } else {
+ // Set mMutatedSpecializeVideo, which will ensure that the next update
+ // will rebuild the video layer.
+ mMutatedSpecializeVideo = true;
+#ifdef NIGHTLY_BUILD
+ if (StaticPrefs::gfx_core_animation_specialize_video_log()) {
+ NSLog(@"VIDEO_LOG: EnqueueSurface failed in OnlyVideo update.");
+ }
+#endif
+ }
+ }
+
+ return updateSucceeded;
+ }
+
+ MOZ_ASSERT(aUpdate == UpdateType::All);
+
+ if (mWrappingCALayer && mMutatedSpecializeVideo) {
+ // Since specialize video changes the way we construct our wrapping and content layers,
+ // we have to scrap them if this value has changed.
+#ifdef NIGHTLY_BUILD
+ if (aIsVideo && StaticPrefs::gfx_core_animation_specialize_video_log()) {
+ NSLog(@"VIDEO_LOG: Scrapping existing video layer.");
+ }
+#endif
+ [mContentCALayer release];
+ mContentCALayer = nil;
+ [mOpaquenessTintLayer release];
+ mOpaquenessTintLayer = nil;
+ [mWrappingCALayer release];
+ mWrappingCALayer = nil;
+ }
+
+ bool layerNeedsInitialization = false;
+ if (!mWrappingCALayer) {
+ layerNeedsInitialization = true;
+ mWrappingCALayer = [[CALayer layer] retain];
+ mWrappingCALayer.position = CGPointZero;
+ mWrappingCALayer.bounds = CGRectZero;
+ mWrappingCALayer.anchorPoint = CGPointZero;
+ mWrappingCALayer.contentsGravity = kCAGravityTopLeft;
+ mWrappingCALayer.edgeAntialiasingMask = 0;
+
+ if (aColor) {
+ // Color layers set a color on the wrapping layer and don't get a content layer.
+ mWrappingCALayer.backgroundColor = aColor.get();
+ } else {
+ if (aSpecializeVideo) {
+#ifdef NIGHTLY_BUILD
+ if (aIsVideo && StaticPrefs::gfx_core_animation_specialize_video_log()) {
+ NSLog(@"VIDEO_LOG: Rebuilding video layer with AVSampleBufferDisplayLayer.");
+ mLogNextVideoSurface = true;
+ }
+#endif
+ mContentCALayer = [[AVSampleBufferDisplayLayer layer] retain];
+ CMTimebaseRef timebase;
+#ifdef CMTIMEBASE_USE_SOURCE_TERMINOLOGY
+ CMTimebaseCreateWithSourceClock(kCFAllocatorDefault, CMClockGetHostTimeClock(), &timebase);
+#else
+ CMTimebaseCreateWithMasterClock(kCFAllocatorDefault, CMClockGetHostTimeClock(), &timebase);
+#endif
+ CMTimebaseSetRate(timebase, 1.0f);
+ [(AVSampleBufferDisplayLayer*)mContentCALayer setControlTimebase:timebase];
+ CFRelease(timebase);
+ } else {
+#ifdef NIGHTLY_BUILD
+ if (aIsVideo && StaticPrefs::gfx_core_animation_specialize_video_log()) {
+ NSLog(@"VIDEO_LOG: Rebuilding video layer with CALayer.");
+ mLogNextVideoSurface = true;
+ }
+#endif
+ mContentCALayer = [[CALayer layer] retain];
+ }
+ mContentCALayer.position = CGPointZero;
+ mContentCALayer.anchorPoint = CGPointZero;
+ mContentCALayer.contentsGravity = kCAGravityTopLeft;
+ mContentCALayer.contentsScale = 1;
+ mContentCALayer.bounds = CGRectMake(0, 0, aSize.width, aSize.height);
+ mContentCALayer.edgeAntialiasingMask = 0;
+ mContentCALayer.opaque = aIsOpaque;
+ if ([mContentCALayer respondsToSelector:@selector(setContentsOpaque:)]) {
+ // The opaque property seems to not be enough when using IOSurface contents.
+ // Additionally, call the private method setContentsOpaque.
+ [mContentCALayer setContentsOpaque:aIsOpaque];
+ }
+
+ [mWrappingCALayer addSublayer:mContentCALayer];
+ }
+ }
+
+ if (@available(macOS 10.15, iOS 13.0, *)) {
+ if (aSpecializeVideo && mMutatedIsDRM) {
+ ((AVSampleBufferDisplayLayer*)mContentCALayer).preventsCapture = aIsDRM;
+ }
+ }
+
+ bool shouldTintOpaqueness = StaticPrefs::gfx_core_animation_tint_opaque();
+ if (shouldTintOpaqueness && !mOpaquenessTintLayer) {
+ mOpaquenessTintLayer = [[CALayer layer] retain];
+ mOpaquenessTintLayer.position = CGPointZero;
+ mOpaquenessTintLayer.bounds = mContentCALayer.bounds;
+ mOpaquenessTintLayer.anchorPoint = CGPointZero;
+ mOpaquenessTintLayer.contentsGravity = kCAGravityTopLeft;
+ if (aIsOpaque) {
+ mOpaquenessTintLayer.backgroundColor =
+ [[[NSColor greenColor] colorWithAlphaComponent:0.5] CGColor];
+ } else {
+ mOpaquenessTintLayer.backgroundColor =
+ [[[NSColor redColor] colorWithAlphaComponent:0.5] CGColor];
+ }
+ [mWrappingCALayer addSublayer:mOpaquenessTintLayer];
+ } else if (!shouldTintOpaqueness && mOpaquenessTintLayer) {
+ [mOpaquenessTintLayer removeFromSuperlayer];
+ [mOpaquenessTintLayer release];
+ mOpaquenessTintLayer = nullptr;
+ }
+
+ // CALayers have a position and a size, specified through the position and the bounds properties.
+ // layer.bounds.origin must always be (0, 0).
+ // A layer's position affects the layer's entire layer subtree. In other words, each layer's
+ // position is relative to its superlayer's position. We implement the clip rect using
+ // masksToBounds on mWrappingCALayer. So mContentCALayer's position is relative to the clip rect
+ // position.
+ // Note: The Core Animation docs on "Positioning and Sizing Sublayers" say:
+ // Important: Always use integral numbers for the width and height of your layer.
+ // We hope that this refers to integral physical pixels, and not to integral logical coordinates.
+
+ if (mContentCALayer && (mMutatedBackingScale || mMutatedSize || layerNeedsInitialization)) {
+ mContentCALayer.bounds =
+ CGRectMake(0, 0, aSize.width / aBackingScale, aSize.height / aBackingScale);
+ if (mOpaquenessTintLayer) {
+ mOpaquenessTintLayer.bounds = mContentCALayer.bounds;
+ }
+ mContentCALayer.contentsScale = aBackingScale;
+ }
+
+ if (mMutatedBackingScale || mMutatedPosition || mMutatedDisplayRect || mMutatedClipRect ||
+ mMutatedTransform || mMutatedSurfaceIsFlipped || mMutatedSize || layerNeedsInitialization) {
+ Maybe<CGRect> scaledClipRect =
+ CalculateClipGeometry(aSize, aPosition, aTransform, aDisplayRect, aClipRect, aBackingScale);
+
+ CGRect useClipRect;
+ if (scaledClipRect.isSome()) {
+ useClipRect = *scaledClipRect;
+ } else {
+ useClipRect = CGRectZero;
+ }
+
+ mWrappingCALayer.position = useClipRect.origin;
+ mWrappingCALayer.bounds = CGRectMake(0, 0, useClipRect.size.width, useClipRect.size.height);
+ mWrappingCALayer.masksToBounds = scaledClipRect.isSome();
+
+ if (mContentCALayer) {
+ Matrix4x4 transform = aTransform;
+ transform.PreTranslate(aPosition.x, aPosition.y, 0);
+ transform.PostTranslate((-useClipRect.origin.x * aBackingScale),
+ (-useClipRect.origin.y * aBackingScale), 0);
+
+ if (aSurfaceIsFlipped) {
+ transform.PreTranslate(0, aSize.height, 0).PreScale(1, -1, 1);
+ }
+
+ CATransform3D transformCA{transform._11,
+ transform._12,
+ transform._13,
+ transform._14,
+ transform._21,
+ transform._22,
+ transform._23,
+ transform._24,
+ transform._31,
+ transform._32,
+ transform._33,
+ transform._34,
+ transform._41 / aBackingScale,
+ transform._42 / aBackingScale,
+ transform._43,
+ transform._44};
+ mContentCALayer.transform = transformCA;
+ if (mOpaquenessTintLayer) {
+ mOpaquenessTintLayer.transform = mContentCALayer.transform;
+ }
+ }
+ }
+
+ if (mContentCALayer && (mMutatedSamplingFilter || layerNeedsInitialization)) {
+ if (aSamplingFilter == gfx::SamplingFilter::POINT) {
+ mContentCALayer.minificationFilter = kCAFilterNearest;
+ mContentCALayer.magnificationFilter = kCAFilterNearest;
+ } else {
+ mContentCALayer.minificationFilter = kCAFilterLinear;
+ mContentCALayer.magnificationFilter = kCAFilterLinear;
+ }
+ }
+
+ if (mMutatedFrontSurface) {
+ // This is handled last because a video update could fail, causing us to
+ // early exit, leaving the mutation bits untouched. We do this so that the
+ // *next* update will clear the video layer and setup a regular layer.
+
+ IOSurfaceRef surface = aFrontSurface.get();
+ if (aSpecializeVideo) {
+ // Attempt to enqueue this as a video frame. If we fail, we'll rebuild
+ // our video layer in the next update.
+ bool isEnqueued = EnqueueSurface(surface);
+ if (!isEnqueued) {
+ // Set mMutatedSpecializeVideo, which will ensure that the next update
+ // will rebuild the video layer.
+ mMutatedSpecializeVideo = true;
+#ifdef NIGHTLY_BUILD
+ if (StaticPrefs::gfx_core_animation_specialize_video_log()) {
+ NSLog(@"VIDEO_LOG: EnqueueSurface failed in All update.");
+ }
+#endif
+ return false;
+ }
+ } else {
+#ifdef NIGHTLY_BUILD
+ if (mLogNextVideoSurface && StaticPrefs::gfx_core_animation_specialize_video_log()) {
+ LogSurface(surface, nullptr, nullptr);
+ mLogNextVideoSurface = false;
+ }
+#endif
+ mContentCALayer.contents = (id)surface;
+ }
+ }
+
+ mMutatedPosition = false;
+ mMutatedTransform = false;
+ mMutatedBackingScale = false;
+ mMutatedSize = false;
+ mMutatedSurfaceIsFlipped = false;
+ mMutatedDisplayRect = false;
+ mMutatedClipRect = false;
+ mMutatedFrontSurface = false;
+ mMutatedSamplingFilter = false;
+ mMutatedSpecializeVideo = false;
+ mMutatedIsDRM = false;
+
+ return true;
+}
+
+NativeLayerCA::UpdateType NativeLayerCA::Representation::HasUpdate(bool aIsVideo) {
+ if (!mWrappingCALayer) {
+ return UpdateType::All;
+ }
+
+ // This check intentionally skips mMutatedFrontSurface. We'll check it later to see
+ // if we can attempt an OnlyVideo update.
+ if (mMutatedPosition || mMutatedTransform || mMutatedDisplayRect || mMutatedClipRect ||
+ mMutatedBackingScale || mMutatedSize || mMutatedSurfaceIsFlipped || mMutatedSamplingFilter ||
+ mMutatedSpecializeVideo || mMutatedIsDRM) {
+ return UpdateType::All;
+ }
+
+ // Check if we should try an OnlyVideo update. We know from the above check that our
+ // specialize video is stable (we don't know what value we'll receive, though), so
+ // we just have to check that we have a surface to display.
+ if (mMutatedFrontSurface) {
+ return (aIsVideo ? UpdateType::OnlyVideo : UpdateType::All);
+ }
+
+ return UpdateType::None;
+}
+
+bool NativeLayerCA::WillUpdateAffectLayers(WhichRepresentation aRepresentation) {
+ MutexAutoLock lock(mMutex);
+ auto& r = GetRepresentation(aRepresentation);
+ return r.mMutatedSpecializeVideo || !r.UnderlyingCALayer();
+}
+
+// Called when mMutex is already being held by the current thread.
+Maybe<NativeLayerCA::SurfaceWithInvalidRegion> NativeLayerCA::GetUnusedSurfaceAndCleanUp(
+ const MutexAutoLock& aProofOfLock) {
+ std::vector<SurfaceWithInvalidRegionAndCheckCount> usedSurfaces;
+ Maybe<SurfaceWithInvalidRegion> unusedSurface;
+
+ // Separate mSurfaces into used and unused surfaces.
+ for (auto& surf : mSurfaces) {
+ if (IOSurfaceIsInUse(surf.mEntry.mSurface.get())) {
+ surf.mCheckCount++;
+ if (surf.mCheckCount < 10) {
+ usedSurfaces.push_back(std::move(surf));
+ } else {
+ // The window server has been holding on to this surface for an unreasonably long time. This
+ // is known to happen sometimes, for example in occluded windows or after a GPU switch. In
+ // that case, release our references to the surface so that it doesn't look like we're
+ // trying to keep it alive.
+ mSurfacePoolHandle->ReturnSurfaceToPool(std::move(surf.mEntry.mSurface));
+ }
+ } else {
+ if (unusedSurface) {
+ // Multiple surfaces are unused. Keep the most recent one and release any earlier ones. The
+ // most recent one requires the least amount of copying during partial repaints.
+ mSurfacePoolHandle->ReturnSurfaceToPool(std::move(unusedSurface->mSurface));
+ }
+ unusedSurface = Some(std::move(surf.mEntry));
+ }
+ }
+
+ // Put the used surfaces back into mSurfaces.
+ mSurfaces = std::move(usedSurfaces);
+
+ return unusedSurface;
+}
+
+bool DownscaleTargetNLRS::DownscaleFrom(profiler_screenshots::RenderSource* aSource,
+ const IntRect& aSourceRect, const IntRect& aDestRect) {
+ mGL->BlitHelper()->BlitFramebufferToFramebuffer(static_cast<RenderSourceNLRS*>(aSource)->FB().mFB,
+ mRenderSource->FB().mFB, aSourceRect, aDestRect,
+ LOCAL_GL_LINEAR);
+
+ return true;
+}
+
+void AsyncReadbackBufferNLRS::CopyFrom(profiler_screenshots::RenderSource* aSource) {
+ IntSize size = aSource->Size();
+ MOZ_RELEASE_ASSERT(Size() == size);
+
+ gl::ScopedPackState scopedPackState(mGL);
+ mGL->fBindBuffer(LOCAL_GL_PIXEL_PACK_BUFFER, mBufferHandle);
+ mGL->fPixelStorei(LOCAL_GL_PACK_ALIGNMENT, 1);
+ const gl::ScopedBindFramebuffer bindFB(mGL, static_cast<RenderSourceNLRS*>(aSource)->FB().mFB);
+ mGL->fReadPixels(0, 0, size.width, size.height, LOCAL_GL_RGBA, LOCAL_GL_UNSIGNED_BYTE, 0);
+}
+
+bool AsyncReadbackBufferNLRS::MapAndCopyInto(DataSourceSurface* aSurface,
+ const IntSize& aReadSize) {
+ MOZ_RELEASE_ASSERT(aReadSize <= aSurface->GetSize());
+
+ if (!mGL || !mGL->MakeCurrent()) {
+ return false;
+ }
+
+ gl::ScopedPackState scopedPackState(mGL);
+ mGL->fBindBuffer(LOCAL_GL_PIXEL_PACK_BUFFER, mBufferHandle);
+ mGL->fPixelStorei(LOCAL_GL_PACK_ALIGNMENT, 1);
+
+ const uint8_t* srcData = nullptr;
+ if (mGL->IsSupported(gl::GLFeature::map_buffer_range)) {
+ srcData = static_cast<uint8_t*>(mGL->fMapBufferRange(LOCAL_GL_PIXEL_PACK_BUFFER, 0,
+ aReadSize.height * aReadSize.width * 4,
+ LOCAL_GL_MAP_READ_BIT));
+ } else {
+ srcData =
+ static_cast<uint8_t*>(mGL->fMapBuffer(LOCAL_GL_PIXEL_PACK_BUFFER, LOCAL_GL_READ_ONLY));
+ }
+
+ if (!srcData) {
+ return false;
+ }
+
+ int32_t srcStride = mSize.width * 4; // Bind() sets an alignment of 1
+ DataSourceSurface::ScopedMap map(aSurface, DataSourceSurface::WRITE);
+ uint8_t* destData = map.GetData();
+ int32_t destStride = map.GetStride();
+ SurfaceFormat destFormat = aSurface->GetFormat();
+ for (int32_t destRow = 0; destRow < aReadSize.height; destRow++) {
+ // Turn srcData upside down during the copy.
+ int32_t srcRow = aReadSize.height - 1 - destRow;
+ const uint8_t* src = &srcData[srcRow * srcStride];
+ uint8_t* dest = &destData[destRow * destStride];
+ SwizzleData(src, srcStride, SurfaceFormat::R8G8B8A8, dest, destStride, destFormat,
+ IntSize(aReadSize.width, 1));
+ }
+
+ mGL->fUnmapBuffer(LOCAL_GL_PIXEL_PACK_BUFFER);
+
+ return true;
+}
+
+AsyncReadbackBufferNLRS::~AsyncReadbackBufferNLRS() {
+ if (mGL && mGL->MakeCurrent()) {
+ mGL->fDeleteBuffers(1, &mBufferHandle);
+ }
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/NativeLayerWayland.cpp b/gfx/layers/NativeLayerWayland.cpp
new file mode 100644
index 0000000000..55250b3de0
--- /dev/null
+++ b/gfx/layers/NativeLayerWayland.cpp
@@ -0,0 +1,760 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/layers/NativeLayerWayland.h"
+
+#include <dlfcn.h>
+#include <utility>
+#include <algorithm>
+
+#include "gfxUtils.h"
+#include "nsGtkUtils.h"
+#include "GLContextProvider.h"
+#include "GLBlitHelper.h"
+#include "mozilla/gfx/DataSurfaceHelpers.h"
+#include "mozilla/gfx/Logging.h"
+#include "mozilla/layers/SurfacePoolWayland.h"
+#include "mozilla/StaticPrefs_widget.h"
+#include "mozilla/webrender/RenderThread.h"
+
+namespace mozilla::layers {
+
+using gfx::BackendType;
+using gfx::DrawTarget;
+using gfx::IntPoint;
+using gfx::IntRect;
+using gfx::IntRegion;
+using gfx::IntSize;
+using gfx::Matrix4x4;
+using gfx::Point;
+using gfx::Rect;
+using gfx::SamplingFilter;
+using gfx::Size;
+
+static const struct wl_callback_listener sFrameListenerNativeLayerWayland = {
+ NativeLayerWayland::FrameCallbackHandler};
+
+CallbackMultiplexHelper::CallbackMultiplexHelper(CallbackFunc aCallbackFunc,
+ void* aCallbackData)
+ : mCallbackFunc(aCallbackFunc), mCallbackData(aCallbackData) {}
+
+void CallbackMultiplexHelper::Callback(uint32_t aTime) {
+ if (!mActive) {
+ return;
+ }
+ mActive = false;
+
+ // This is likely the first of a batch of frame callbacks being processed and
+ // may trigger the setup of a successive one. In order to avoid complexity,
+ // defer calling the callback function until we had a chance to process
+ // all pending frame callbacks.
+
+ AddRef();
+ nsCOMPtr<nsIRunnable> runnable = NewRunnableMethod<uint32_t>(
+ "layers::CallbackMultiplexHelper::RunCallback", this,
+ &CallbackMultiplexHelper::RunCallback, aTime);
+ MOZ_ALWAYS_SUCCEEDS(NS_DispatchToCurrentThreadQueue(
+ runnable.forget(), EventQueuePriority::Vsync));
+}
+
+void CallbackMultiplexHelper::RunCallback(uint32_t aTime) {
+ mCallbackFunc(mCallbackData, aTime);
+ Release();
+}
+
+/* static */
+already_AddRefed<NativeLayerRootWayland>
+NativeLayerRootWayland::CreateForMozContainer(MozContainer* aContainer) {
+ RefPtr<NativeLayerRootWayland> layerRoot =
+ new NativeLayerRootWayland(aContainer);
+ return layerRoot.forget();
+}
+
+NativeLayerRootWayland::NativeLayerRootWayland(MozContainer* aContainer)
+ : mMutex("NativeLayerRootWayland"), mContainer(aContainer) {
+ g_object_ref(mContainer);
+}
+
+NativeLayerRootWayland::~NativeLayerRootWayland() {
+ GdkWindow* gdkWindow = gtk_widget_get_window(GTK_WIDGET(mContainer));
+ if (gdkWindow) {
+ GdkFrameClock* frameClock = gdk_window_get_frame_clock(gdkWindow);
+ g_signal_handlers_disconnect_by_data(frameClock, this);
+ }
+ g_object_unref(mContainer);
+}
+
+already_AddRefed<NativeLayer> NativeLayerRootWayland::CreateLayer(
+ const IntSize& aSize, bool aIsOpaque,
+ SurfacePoolHandle* aSurfacePoolHandle) {
+ RefPtr<NativeLayer> layer = new NativeLayerWayland(
+ aSize, aIsOpaque, aSurfacePoolHandle->AsSurfacePoolHandleWayland());
+ return layer.forget();
+}
+
+already_AddRefed<NativeLayer>
+NativeLayerRootWayland::CreateLayerForExternalTexture(bool aIsOpaque) {
+ RefPtr<NativeLayer> layer = new NativeLayerWayland(aIsOpaque);
+ return layer.forget();
+}
+
+void NativeLayerRootWayland::AppendLayer(NativeLayer* aLayer) {
+ MOZ_RELEASE_ASSERT(false);
+ MutexAutoLock lock(mMutex);
+
+ RefPtr<NativeLayerWayland> layerWayland = aLayer->AsNativeLayerWayland();
+ MOZ_RELEASE_ASSERT(layerWayland);
+
+ mSublayers.AppendElement(layerWayland);
+}
+
+void NativeLayerRootWayland::RemoveLayer(NativeLayer* aLayer) {
+ MOZ_RELEASE_ASSERT(false);
+ MutexAutoLock lock(mMutex);
+
+ RefPtr<NativeLayerWayland> layerWayland = aLayer->AsNativeLayerWayland();
+ MOZ_RELEASE_ASSERT(layerWayland);
+
+ mSublayers.RemoveElement(layerWayland);
+}
+
+void NativeLayerRootWayland::SetLayers(
+ const nsTArray<RefPtr<NativeLayer>>& aLayers) {
+ MutexAutoLock lock(mMutex);
+
+ nsTArray<RefPtr<NativeLayerWayland>> newSublayers(aLayers.Length());
+ for (const RefPtr<NativeLayer>& sublayer : aLayers) {
+ RefPtr<NativeLayerWayland> layer = sublayer->AsNativeLayerWayland();
+ newSublayers.AppendElement(layer);
+ }
+
+ if (newSublayers != mSublayers) {
+ for (const RefPtr<NativeLayerWayland>& layer : mSublayers) {
+ if (!newSublayers.Contains(layer)) {
+ mOldSublayers.AppendElement(layer);
+ }
+ }
+ mSublayers = std::move(newSublayers);
+ mNewLayers = true;
+ }
+}
+
+bool NativeLayerRootWayland::CommitToScreen() {
+ MutexAutoLock lock(mMutex);
+ return CommitToScreen(lock);
+}
+
+bool NativeLayerRootWayland::CommitToScreen(const MutexAutoLock& aProofOfLock) {
+ mFrameInProcess = false;
+
+ MozContainerSurfaceLock lock(mContainer);
+ struct wl_surface* containerSurface = lock.GetSurface();
+ if (!containerSurface) {
+ if (!mCallbackRequested) {
+ RefPtr<NativeLayerRootWayland> self(this);
+ moz_container_wayland_add_initial_draw_callback_locked(
+ mContainer, [self]() -> void {
+ MutexAutoLock lock(self->mMutex);
+ if (!self->mFrameInProcess) {
+ self->CommitToScreen(lock);
+ }
+ self->mCallbackRequested = false;
+ });
+ mCallbackRequested = true;
+ }
+ return true;
+ }
+
+ wl_surface* previousSurface = nullptr;
+ for (RefPtr<NativeLayerWayland>& layer : mSublayers) {
+ layer->EnsureParentSurface(containerSurface);
+
+ if (mNewLayers) {
+ wl_subsurface_place_above(layer->mWlSubsurface, previousSurface
+ ? previousSurface
+ : containerSurface);
+ previousSurface = layer->mWlSurface;
+ }
+
+ MOZ_RELEASE_ASSERT(layer->mTransform.Is2D());
+ auto transform2D = layer->mTransform.As2D();
+
+ Rect surfaceRectClipped =
+ Rect(0, 0, (float)layer->mSize.width, (float)layer->mSize.height);
+ surfaceRectClipped =
+ surfaceRectClipped.Intersect(Rect(layer->mDisplayRect));
+
+ transform2D.PostTranslate((float)layer->mPosition.x,
+ (float)layer->mPosition.y);
+ surfaceRectClipped = transform2D.TransformBounds(surfaceRectClipped);
+
+ if (layer->mClipRect) {
+ surfaceRectClipped =
+ surfaceRectClipped.Intersect(Rect(layer->mClipRect.value()));
+ }
+
+ if (roundf(surfaceRectClipped.width) > 0 &&
+ roundf(surfaceRectClipped.height) > 0) {
+ layer->SetBufferTransformFlipped(transform2D._11 < 0.0,
+ transform2D._22 < 0.0);
+
+ double bufferScale = moz_container_wayland_get_scale(mContainer);
+ layer->SetSubsurfacePosition(floor(surfaceRectClipped.x / bufferScale),
+ floor(surfaceRectClipped.y / bufferScale));
+ layer->SetViewportDestinationSize(
+ ceil(surfaceRectClipped.width / bufferScale),
+ ceil(surfaceRectClipped.height / bufferScale));
+
+ auto transform2DInversed = transform2D.Inverse();
+ Rect bufferClip = transform2DInversed.TransformBounds(surfaceRectClipped);
+ layer->SetViewportSourceRect(bufferClip);
+
+ layer->Commit();
+ } else {
+ layer->Unmap();
+ }
+ }
+
+ if (mNewLayers) {
+ for (RefPtr<NativeLayerWayland>& layer : mOldSublayers) {
+ layer->Unmap();
+ }
+ mOldSublayers.Clear();
+
+ nsCOMPtr<nsIRunnable> updateLayersRunnable = NewRunnableMethod<>(
+ "layers::NativeLayerRootWayland::UpdateLayersOnMainThread", this,
+ &NativeLayerRootWayland::UpdateLayersOnMainThread);
+ MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThreadQueue(
+ updateLayersRunnable.forget(), EventQueuePriority::Normal));
+ mNewLayers = false;
+ }
+
+ if (containerSurface != mWlSurface) {
+ if (!mShmBuffer) {
+ mShmBuffer = widget::WaylandBufferSHM::Create(LayoutDeviceIntSize(1, 1));
+ mShmBuffer->Clear();
+ }
+ mShmBuffer->AttachAndCommit(containerSurface);
+ mWlSurface = containerSurface;
+ } else {
+ wl_surface_commit(containerSurface);
+ }
+
+ wl_display_flush(widget::WaylandDisplayGet()->GetDisplay());
+ return true;
+}
+
+void NativeLayerRootWayland::RequestFrameCallback(CallbackFunc aCallbackFunc,
+ void* aCallbackData) {
+ MutexAutoLock lock(mMutex);
+
+ mCallbackMultiplexHelper =
+ new CallbackMultiplexHelper(aCallbackFunc, aCallbackData);
+
+ for (const RefPtr<NativeLayerWayland>& layer : mSublayersOnMainThread) {
+ layer->RequestFrameCallback(mCallbackMultiplexHelper);
+ }
+
+ MozContainerSurfaceLock lockContainer(mContainer);
+ struct wl_surface* wlSurface = lockContainer.GetSurface();
+ if (wlSurface) {
+ wl_surface_commit(wlSurface);
+ wl_display_flush(widget::WaylandDisplayGet()->GetDisplay());
+ }
+}
+
+static void sAfterFrameClockAfterPaint(
+ GdkFrameClock* aClock, NativeLayerRootWayland* aNativeLayerRoot) {
+ aNativeLayerRoot->AfterFrameClockAfterPaint();
+}
+
+void NativeLayerRootWayland::AfterFrameClockAfterPaint() {
+ MutexAutoLock lock(mMutex);
+ MozContainerSurfaceLock lockContainer(mContainer);
+ struct wl_surface* containerSurface = lockContainer.GetSurface();
+ for (const RefPtr<NativeLayerWayland>& layer : mSublayersOnMainThread) {
+ wl_surface_commit(layer->mWlSurface);
+ }
+ if (containerSurface) {
+ wl_surface_commit(containerSurface);
+ }
+}
+
+void NativeLayerRootWayland::UpdateLayersOnMainThread() {
+ AssertIsOnMainThread();
+ MutexAutoLock lock(mMutex);
+
+ static auto sGdkWaylandWindowAddCallbackSurface =
+ (void (*)(GdkWindow*, struct wl_surface*))dlsym(
+ RTLD_DEFAULT, "gdk_wayland_window_add_frame_callback_surface");
+ static auto sGdkWaylandWindowRemoveCallbackSurface =
+ (void (*)(GdkWindow*, struct wl_surface*))dlsym(
+ RTLD_DEFAULT, "gdk_wayland_window_remove_frame_callback_surface");
+
+ MozContainerSurfaceLock lockContainer(mContainer);
+ struct wl_surface* containerSurface = lockContainer.GetSurface();
+ GdkWindow* gdkWindow = gtk_widget_get_window(GTK_WIDGET(mContainer));
+
+ mSublayersOnMainThread.RemoveElementsBy([&](const auto& layer) {
+ if (!mSublayers.Contains(layer)) {
+ if (layer->IsOpaque() &&
+ StaticPrefs::widget_wayland_opaque_region_enabled_AtStartup() &&
+ sGdkWaylandWindowAddCallbackSurface && gdkWindow) {
+ sGdkWaylandWindowRemoveCallbackSurface(gdkWindow, layer->mWlSurface);
+
+ wl_compositor* compositor =
+ widget::WaylandDisplayGet()->GetCompositor();
+ wl_region* region = wl_compositor_create_region(compositor);
+ wl_surface_set_opaque_region(layer->mWlSurface, region);
+ wl_region_destroy(region);
+ wl_surface_commit(layer->mWlSurface);
+ }
+ return true;
+ }
+ return false;
+ });
+
+ for (const RefPtr<NativeLayerWayland>& layer : mSublayers) {
+ if (!mSublayersOnMainThread.Contains(layer)) {
+ if (layer->IsOpaque() &&
+ StaticPrefs::widget_wayland_opaque_region_enabled_AtStartup() &&
+ sGdkWaylandWindowRemoveCallbackSurface && gdkWindow) {
+ sGdkWaylandWindowAddCallbackSurface(gdkWindow, layer->mWlSurface);
+
+ wl_compositor* compositor =
+ widget::WaylandDisplayGet()->GetCompositor();
+ wl_region* region = wl_compositor_create_region(compositor);
+ wl_region_add(region, 0, 0, INT32_MAX, INT32_MAX);
+ wl_surface_set_opaque_region(layer->mWlSurface, region);
+ wl_region_destroy(region);
+ wl_surface_commit(layer->mWlSurface);
+ }
+ if (mCallbackMultiplexHelper && mCallbackMultiplexHelper->IsActive()) {
+ layer->RequestFrameCallback(mCallbackMultiplexHelper);
+ }
+ mSublayersOnMainThread.AppendElement(layer);
+ }
+ }
+
+ if (containerSurface) {
+ wl_surface_commit(containerSurface);
+ }
+
+ if (!mGdkAfterPaintId && gdkWindow) {
+ GdkFrameClock* frameClock = gdk_window_get_frame_clock(gdkWindow);
+ mGdkAfterPaintId =
+ g_signal_connect_after(frameClock, "after-paint",
+ G_CALLBACK(sAfterFrameClockAfterPaint), this);
+ }
+}
+
+NativeLayerWayland::NativeLayerWayland(
+ const IntSize& aSize, bool aIsOpaque,
+ SurfacePoolHandleWayland* aSurfacePoolHandle)
+ : mMutex("NativeLayerWayland"),
+ mSurfacePoolHandle(aSurfacePoolHandle),
+ mSize(aSize),
+ mIsOpaque(aIsOpaque) {
+ MOZ_RELEASE_ASSERT(mSurfacePoolHandle,
+ "Need a non-null surface pool handle.");
+
+ widget::nsWaylandDisplay* waylandDisplay = widget::WaylandDisplayGet();
+ wl_compositor* compositor = waylandDisplay->GetCompositor();
+ mWlSurface = wl_compositor_create_surface(compositor);
+
+ wl_region* region = wl_compositor_create_region(compositor);
+ wl_surface_set_input_region(mWlSurface, region);
+ wl_region_destroy(region);
+
+ wp_viewporter* viewporter = waylandDisplay->GetViewporter();
+ mViewport = wp_viewporter_get_viewport(viewporter, mWlSurface);
+}
+
+NativeLayerWayland::NativeLayerWayland(bool aIsOpaque)
+ : mMutex("NativeLayerWayland"),
+ mSurfacePoolHandle(nullptr),
+ mIsOpaque(aIsOpaque) {
+ MOZ_RELEASE_ASSERT(false); // external image
+}
+
+NativeLayerWayland::~NativeLayerWayland() {
+ MutexAutoLock lock(mMutex);
+
+ if (mInProgressBuffer) {
+ mSurfacePoolHandle->ReturnBufferToPool(mInProgressBuffer);
+ mInProgressBuffer = nullptr;
+ }
+ if (mFrontBuffer) {
+ mSurfacePoolHandle->ReturnBufferToPool(mFrontBuffer);
+ mFrontBuffer = nullptr;
+ }
+ MozClearPointer(mCallback, wl_callback_destroy);
+ MozClearPointer(mViewport, wp_viewport_destroy);
+ MozClearPointer(mWlSubsurface, wl_subsurface_destroy);
+ MozClearPointer(mWlSurface, wl_surface_destroy);
+}
+
+void NativeLayerWayland::AttachExternalImage(
+ wr::RenderTextureHost* aExternalImage) {
+ MOZ_RELEASE_ASSERT(false);
+}
+
+void NativeLayerWayland::SetSurfaceIsFlipped(bool aIsFlipped) {
+ MutexAutoLock lock(mMutex);
+
+ if (aIsFlipped != mSurfaceIsFlipped) {
+ mSurfaceIsFlipped = aIsFlipped;
+ }
+}
+
+bool NativeLayerWayland::SurfaceIsFlipped() {
+ MutexAutoLock lock(mMutex);
+
+ return mSurfaceIsFlipped;
+}
+
+IntSize NativeLayerWayland::GetSize() {
+ MutexAutoLock lock(mMutex);
+ return mSize;
+}
+
+void NativeLayerWayland::SetPosition(const IntPoint& aPosition) {
+ MutexAutoLock lock(mMutex);
+
+ if (aPosition != mPosition) {
+ mPosition = aPosition;
+ }
+}
+
+IntPoint NativeLayerWayland::GetPosition() {
+ MutexAutoLock lock(mMutex);
+ return mPosition;
+}
+
+void NativeLayerWayland::SetTransform(const Matrix4x4& aTransform) {
+ MutexAutoLock lock(mMutex);
+ MOZ_ASSERT(aTransform.IsRectilinear());
+
+ if (aTransform != mTransform) {
+ mTransform = aTransform;
+ }
+}
+
+void NativeLayerWayland::SetSamplingFilter(SamplingFilter aSamplingFilter) {
+ MutexAutoLock lock(mMutex);
+
+ if (aSamplingFilter != mSamplingFilter) {
+ mSamplingFilter = aSamplingFilter;
+ }
+}
+
+Matrix4x4 NativeLayerWayland::GetTransform() {
+ MutexAutoLock lock(mMutex);
+ return mTransform;
+}
+
+IntRect NativeLayerWayland::GetRect() {
+ MutexAutoLock lock(mMutex);
+ return IntRect(mPosition, mSize);
+}
+
+bool NativeLayerWayland::IsOpaque() {
+ MutexAutoLock lock(mMutex);
+ return mIsOpaque;
+}
+
+void NativeLayerWayland::SetClipRect(const Maybe<IntRect>& aClipRect) {
+ MutexAutoLock lock(mMutex);
+
+ if (aClipRect != mClipRect) {
+ mClipRect = aClipRect;
+ }
+}
+
+Maybe<IntRect> NativeLayerWayland::ClipRect() {
+ MutexAutoLock lock(mMutex);
+ return mClipRect;
+}
+
+IntRect NativeLayerWayland::CurrentSurfaceDisplayRect() {
+ MutexAutoLock lock(mMutex);
+ return mDisplayRect;
+}
+
+RefPtr<DrawTarget> NativeLayerWayland::NextSurfaceAsDrawTarget(
+ const IntRect& aDisplayRect, const IntRegion& aUpdateRegion,
+ BackendType aBackendType) {
+ MutexAutoLock lock(mMutex);
+
+ mDisplayRect = IntRect(aDisplayRect);
+ mDirtyRegion = IntRegion(aUpdateRegion);
+
+ MOZ_ASSERT(!mInProgressBuffer);
+ if (mFrontBuffer && !mFrontBuffer->IsAttached()) {
+ // the Wayland compositor released the buffer early, we can reuse it
+ mInProgressBuffer = std::move(mFrontBuffer);
+ } else {
+ mInProgressBuffer = mSurfacePoolHandle->ObtainBufferFromPool(mSize);
+ if (mFrontBuffer) {
+ HandlePartialUpdate(lock);
+ mSurfacePoolHandle->ReturnBufferToPool(mFrontBuffer);
+ }
+ }
+ mFrontBuffer = nullptr;
+
+ if (!mInProgressBuffer) {
+ gfxCriticalError() << "Failed to obtain buffer";
+ wr::RenderThread::Get()->HandleWebRenderError(
+ wr::WebRenderError::NEW_SURFACE);
+ return nullptr;
+ }
+
+ return mInProgressBuffer->Lock();
+}
+
+Maybe<GLuint> NativeLayerWayland::NextSurfaceAsFramebuffer(
+ const IntRect& aDisplayRect, const IntRegion& aUpdateRegion,
+ bool aNeedsDepth) {
+ MutexAutoLock lock(mMutex);
+
+ mDisplayRect = IntRect(aDisplayRect);
+ mDirtyRegion = IntRegion(aUpdateRegion);
+
+ MOZ_ASSERT(!mInProgressBuffer);
+ if (mFrontBuffer && !mFrontBuffer->IsAttached()) {
+ // the Wayland compositor released the buffer early, we can reuse it
+ mInProgressBuffer = std::move(mFrontBuffer);
+ mFrontBuffer = nullptr;
+ } else {
+ mInProgressBuffer = mSurfacePoolHandle->ObtainBufferFromPool(mSize);
+ }
+
+ if (!mInProgressBuffer) {
+ gfxCriticalError() << "Failed to obtain buffer";
+ wr::RenderThread::Get()->HandleWebRenderError(
+ wr::WebRenderError::NEW_SURFACE);
+ return Nothing();
+ }
+
+ // get the framebuffer before handling partial damage so we don't accidently
+ // create one without depth buffer
+ Maybe<GLuint> fbo = mSurfacePoolHandle->GetFramebufferForBuffer(
+ mInProgressBuffer, aNeedsDepth);
+ MOZ_RELEASE_ASSERT(fbo, "GetFramebufferForBuffer failed.");
+
+ if (mFrontBuffer) {
+ HandlePartialUpdate(lock);
+ mSurfacePoolHandle->ReturnBufferToPool(mFrontBuffer);
+ mFrontBuffer = nullptr;
+ }
+
+ return fbo;
+}
+
+void NativeLayerWayland::HandlePartialUpdate(
+ const MutexAutoLock& aProofOfLock) {
+ IntRegion copyRegion = IntRegion(mDisplayRect);
+ copyRegion.SubOut(mDirtyRegion);
+
+ if (!copyRegion.IsEmpty()) {
+ if (mSurfacePoolHandle->gl()) {
+ mSurfacePoolHandle->gl()->MakeCurrent();
+ for (auto iter = copyRegion.RectIter(); !iter.Done(); iter.Next()) {
+ gfx::IntRect r = iter.Get();
+ Maybe<GLuint> sourceFB =
+ mSurfacePoolHandle->GetFramebufferForBuffer(mFrontBuffer, false);
+ Maybe<GLuint> destFB = mSurfacePoolHandle->GetFramebufferForBuffer(
+ mInProgressBuffer, false);
+ MOZ_RELEASE_ASSERT(sourceFB && destFB);
+ mSurfacePoolHandle->gl()->BlitHelper()->BlitFramebufferToFramebuffer(
+ sourceFB.value(), destFB.value(), r, r, LOCAL_GL_NEAREST);
+ }
+ } else {
+ RefPtr<gfx::DataSourceSurface> dataSourceSurface =
+ gfx::CreateDataSourceSurfaceFromData(
+ mSize, mFrontBuffer->GetSurfaceFormat(),
+ (const uint8_t*)mFrontBuffer->GetImageData(),
+ mSize.width * BytesPerPixel(mFrontBuffer->GetSurfaceFormat()));
+ RefPtr<DrawTarget> dt = mInProgressBuffer->Lock();
+
+ for (auto iter = copyRegion.RectIter(); !iter.Done(); iter.Next()) {
+ IntRect r = iter.Get();
+ dt->CopySurface(dataSourceSurface, r, IntPoint(r.x, r.y));
+ }
+ }
+ }
+}
+
+void NativeLayerWayland::NotifySurfaceReady() {
+ MOZ_ASSERT(!mFrontBuffer);
+ MOZ_ASSERT(mInProgressBuffer);
+ mFrontBuffer = mInProgressBuffer;
+ mInProgressBuffer = nullptr;
+}
+
+void NativeLayerWayland::DiscardBackbuffers() {}
+
+void NativeLayerWayland::Commit() {
+ MutexAutoLock lock(mMutex);
+
+ if (mDirtyRegion.IsEmpty() && mHasBufferAttached) {
+ wl_surface_commit(mWlSurface);
+ return;
+ }
+
+ for (auto iter = mDirtyRegion.RectIter(); !iter.Done(); iter.Next()) {
+ IntRect r = iter.Get();
+ wl_surface_damage_buffer(mWlSurface, r.x, r.y, r.width, r.height);
+ }
+
+ MOZ_ASSERT(mFrontBuffer);
+ mFrontBuffer->AttachAndCommit(mWlSurface);
+ mHasBufferAttached = true;
+ mDirtyRegion.SetEmpty();
+}
+
+void NativeLayerWayland::Unmap() {
+ MutexAutoLock lock(mMutex);
+
+ if (!mHasBufferAttached) {
+ return;
+ }
+
+ wl_surface_attach(mWlSurface, nullptr, 0, 0);
+ wl_surface_commit(mWlSurface);
+ mHasBufferAttached = false;
+}
+
+void NativeLayerWayland::EnsureParentSurface(wl_surface* aParentSurface) {
+ MutexAutoLock lock(mMutex);
+
+ if (aParentSurface != mParentWlSurface) {
+ MozClearPointer(mWlSubsurface, wl_subsurface_destroy);
+ mSubsurfacePosition = IntPoint(0, 0);
+
+ if (aParentSurface) {
+ wl_subcompositor* subcompositor =
+ widget::WaylandDisplayGet()->GetSubcompositor();
+ mWlSubsurface = wl_subcompositor_get_subsurface(subcompositor, mWlSurface,
+ aParentSurface);
+ }
+ mParentWlSurface = aParentSurface;
+ }
+}
+
+void NativeLayerWayland::SetBufferTransformFlipped(bool aFlippedX,
+ bool aFlippedY) {
+ MutexAutoLock lock(mMutex);
+
+ if (aFlippedX == mBufferTransformFlippedX &&
+ aFlippedY == mBufferTransformFlippedY) {
+ return;
+ }
+
+ mBufferTransformFlippedX = aFlippedX;
+ mBufferTransformFlippedY = aFlippedY;
+
+ if (mBufferTransformFlippedY) {
+ if (mBufferTransformFlippedX) {
+ wl_surface_set_buffer_transform(mWlSurface, WL_OUTPUT_TRANSFORM_180);
+ } else {
+ wl_surface_set_buffer_transform(mWlSurface,
+ WL_OUTPUT_TRANSFORM_FLIPPED_180);
+ }
+ } else {
+ if (mBufferTransformFlippedX) {
+ wl_surface_set_buffer_transform(mWlSurface, WL_OUTPUT_TRANSFORM_FLIPPED);
+ } else {
+ wl_surface_set_buffer_transform(mWlSurface, WL_OUTPUT_TRANSFORM_NORMAL);
+ }
+ }
+}
+
+void NativeLayerWayland::SetSubsurfacePosition(int aX, int aY) {
+ MutexAutoLock lock(mMutex);
+
+ if ((aX == mSubsurfacePosition.x && aY == mSubsurfacePosition.y) ||
+ !mWlSubsurface) {
+ return;
+ }
+
+ mSubsurfacePosition.x = aX;
+ mSubsurfacePosition.y = aY;
+ wl_subsurface_set_position(mWlSubsurface, mSubsurfacePosition.x,
+ mSubsurfacePosition.y);
+}
+
+void NativeLayerWayland::SetViewportSourceRect(const Rect aSourceRect) {
+ MutexAutoLock lock(mMutex);
+
+ Rect bufferRect = Rect(0, 0, mSize.width, mSize.height);
+ Rect sourceRect = aSourceRect.Intersect(bufferRect);
+
+ if (mViewportSourceRect == sourceRect) {
+ return;
+ }
+
+ mViewportSourceRect = sourceRect;
+ wp_viewport_set_source(mViewport, wl_fixed_from_double(mViewportSourceRect.x),
+ wl_fixed_from_double(mViewportSourceRect.y),
+ wl_fixed_from_double(mViewportSourceRect.width),
+ wl_fixed_from_double(mViewportSourceRect.height));
+}
+
+void NativeLayerWayland::SetViewportDestinationSize(int aWidth, int aHeight) {
+ MutexAutoLock lock(mMutex);
+
+ if (aWidth == mViewportDestinationSize.width &&
+ aHeight == mViewportDestinationSize.height) {
+ return;
+ }
+
+ mViewportDestinationSize.width = aWidth;
+ mViewportDestinationSize.height = aHeight;
+ wp_viewport_set_destination(mViewport, mViewportDestinationSize.width,
+ mViewportDestinationSize.height);
+}
+
+void NativeLayerWayland::RequestFrameCallback(
+ const RefPtr<CallbackMultiplexHelper>& aMultiplexHelper) {
+ MutexAutoLock lock(mMutex);
+ MOZ_ASSERT(aMultiplexHelper->IsActive());
+
+ // Avoid piling up old helpers if this surface does not receive callbacks
+ // for a longer time
+ mCallbackMultiplexHelpers.RemoveElementsBy(
+ [&](const auto& object) { return !object->IsActive(); });
+
+ mCallbackMultiplexHelpers.AppendElement(aMultiplexHelper);
+ if (!mCallback) {
+ mCallback = wl_surface_frame(mWlSurface);
+ wl_callback_add_listener(mCallback, &sFrameListenerNativeLayerWayland,
+ this);
+ wl_surface_commit(mWlSurface);
+ }
+}
+
+void NativeLayerWayland::FrameCallbackHandler(wl_callback* aCallback,
+ uint32_t aTime) {
+ MutexAutoLock lock(mMutex);
+
+ MOZ_RELEASE_ASSERT(aCallback == mCallback);
+ MozClearPointer(mCallback, wl_callback_destroy);
+
+ for (const RefPtr<CallbackMultiplexHelper>& callbackMultiplexHelper :
+ mCallbackMultiplexHelpers) {
+ callbackMultiplexHelper->Callback(aTime);
+ }
+ mCallbackMultiplexHelpers.Clear();
+}
+
+/* static */
+void NativeLayerWayland::FrameCallbackHandler(void* aData,
+ wl_callback* aCallback,
+ uint32_t aTime) {
+ auto surface = reinterpret_cast<NativeLayerWayland*>(aData);
+ surface->FrameCallbackHandler(aCallback, aTime);
+}
+
+} // namespace mozilla::layers
diff --git a/gfx/layers/NativeLayerWayland.h b/gfx/layers/NativeLayerWayland.h
new file mode 100644
index 0000000000..0c45adc311
--- /dev/null
+++ b/gfx/layers/NativeLayerWayland.h
@@ -0,0 +1,182 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_NativeLayerWayland_h
+#define mozilla_layers_NativeLayerWayland_h
+
+#include <deque>
+#include <unordered_map>
+
+#include "mozilla/Mutex.h"
+#include "mozilla/layers/NativeLayer.h"
+#include "mozilla/layers/SurfacePoolWayland.h"
+#include "mozilla/widget/MozContainerWayland.h"
+#include "nsRegion.h"
+#include "nsTArray.h"
+
+namespace mozilla::layers {
+
+typedef void (*CallbackFunc)(void* aData, uint32_t aTime);
+
+class CallbackMultiplexHelper final {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(CallbackMultiplexHelper);
+
+ explicit CallbackMultiplexHelper(CallbackFunc aCallbackFunc,
+ void* aCallbackData);
+
+ void Callback(uint32_t aTime);
+ bool IsActive() { return mActive; }
+
+ private:
+ ~CallbackMultiplexHelper() = default;
+
+ void RunCallback(uint32_t aTime);
+
+ bool mActive = true;
+ CallbackFunc mCallbackFunc = nullptr;
+ void* mCallbackData = nullptr;
+};
+
+class NativeLayerRootWayland final : public NativeLayerRoot {
+ public:
+ static already_AddRefed<NativeLayerRootWayland> CreateForMozContainer(
+ MozContainer* aContainer);
+
+ virtual NativeLayerRootWayland* AsNativeLayerRootWayland() override {
+ return this;
+ }
+
+ // Overridden methods
+ already_AddRefed<NativeLayer> CreateLayer(
+ const gfx::IntSize& aSize, bool aIsOpaque,
+ SurfacePoolHandle* aSurfacePoolHandle) override;
+ already_AddRefed<NativeLayer> CreateLayerForExternalTexture(
+ bool aIsOpaque) override;
+
+ void AppendLayer(NativeLayer* aLayer) override;
+ void RemoveLayer(NativeLayer* aLayer) override;
+ void SetLayers(const nsTArray<RefPtr<NativeLayer>>& aLayers) override;
+
+ void PrepareForCommit() override { mFrameInProcess = true; };
+ bool CommitToScreen() override;
+
+ void UpdateLayersOnMainThread();
+ void AfterFrameClockAfterPaint();
+ void RequestFrameCallback(CallbackFunc aCallbackFunc, void* aCallbackData);
+
+ private:
+ explicit NativeLayerRootWayland(MozContainer* aContainer);
+ ~NativeLayerRootWayland();
+
+ bool CommitToScreen(const MutexAutoLock& aProofOfLock);
+
+ Mutex mMutex MOZ_UNANNOTATED;
+
+ MozContainer* mContainer = nullptr;
+ wl_surface* mWlSurface = nullptr;
+ RefPtr<widget::WaylandBufferSHM> mShmBuffer;
+
+ nsTArray<RefPtr<NativeLayerWayland>> mSublayers;
+ nsTArray<RefPtr<NativeLayerWayland>> mOldSublayers;
+ nsTArray<RefPtr<NativeLayerWayland>> mSublayersOnMainThread;
+ bool mNewLayers = false;
+
+ bool mFrameInProcess = false;
+ bool mCallbackRequested = false;
+
+ gulong mGdkAfterPaintId = 0;
+ RefPtr<CallbackMultiplexHelper> mCallbackMultiplexHelper;
+};
+
+class NativeLayerWayland final : public NativeLayer {
+ public:
+ virtual NativeLayerWayland* AsNativeLayerWayland() override { return this; }
+
+ // Overridden methods
+ gfx::IntSize GetSize() override;
+ void SetPosition(const gfx::IntPoint& aPosition) override;
+ gfx::IntPoint GetPosition() override;
+ void SetTransform(const gfx::Matrix4x4& aTransform) override;
+ gfx::Matrix4x4 GetTransform() override;
+ gfx::IntRect GetRect() override;
+ void SetSamplingFilter(gfx::SamplingFilter aSamplingFilter) override;
+ RefPtr<gfx::DrawTarget> NextSurfaceAsDrawTarget(
+ const gfx::IntRect& aDisplayRect, const gfx::IntRegion& aUpdateRegion,
+ gfx::BackendType aBackendType) override;
+ Maybe<GLuint> NextSurfaceAsFramebuffer(const gfx::IntRect& aDisplayRect,
+ const gfx::IntRegion& aUpdateRegion,
+ bool aNeedsDepth) override;
+ void NotifySurfaceReady() override;
+ void DiscardBackbuffers() override;
+ bool IsOpaque() override;
+ void SetClipRect(const Maybe<gfx::IntRect>& aClipRect) override;
+ Maybe<gfx::IntRect> ClipRect() override;
+ gfx::IntRect CurrentSurfaceDisplayRect() override;
+ void SetSurfaceIsFlipped(bool aIsFlipped) override;
+ bool SurfaceIsFlipped() override;
+
+ void AttachExternalImage(wr::RenderTextureHost* aExternalImage) override;
+
+ void Commit();
+ void Unmap();
+ void EnsureParentSurface(wl_surface* aParentSurface);
+ const RefPtr<SurfacePoolHandleWayland> GetSurfacePoolHandle() {
+ return mSurfacePoolHandle;
+ };
+ void SetBufferTransformFlipped(bool aFlippedX, bool aFlippedY);
+ void SetSubsurfacePosition(int aX, int aY);
+ void SetViewportSourceRect(const gfx::Rect aSourceRect);
+ void SetViewportDestinationSize(int aWidth, int aHeight);
+
+ void RequestFrameCallback(
+ const RefPtr<CallbackMultiplexHelper>& aMultiplexHelper);
+ static void FrameCallbackHandler(void* aData, wl_callback* aCallback,
+ uint32_t aTime);
+
+ private:
+ friend class NativeLayerRootWayland;
+
+ NativeLayerWayland(const gfx::IntSize& aSize, bool aIsOpaque,
+ SurfacePoolHandleWayland* aSurfacePoolHandle);
+ explicit NativeLayerWayland(bool aIsOpaque);
+ ~NativeLayerWayland() override;
+
+ void HandlePartialUpdate(const MutexAutoLock& aProofOfLock);
+ void FrameCallbackHandler(wl_callback* aCallback, uint32_t aTime);
+
+ Mutex mMutex MOZ_UNANNOTATED;
+
+ const RefPtr<SurfacePoolHandleWayland> mSurfacePoolHandle;
+ const gfx::IntSize mSize;
+ const bool mIsOpaque = false;
+ gfx::IntPoint mPosition;
+ gfx::Matrix4x4 mTransform;
+ gfx::IntRect mDisplayRect;
+ gfx::IntRegion mDirtyRegion;
+ Maybe<gfx::IntRect> mClipRect;
+ gfx::SamplingFilter mSamplingFilter = gfx::SamplingFilter::POINT;
+ bool mSurfaceIsFlipped = false;
+ bool mHasBufferAttached = false;
+
+ wl_surface* mWlSurface = nullptr;
+ wl_surface* mParentWlSurface = nullptr;
+ wl_subsurface* mWlSubsurface = nullptr;
+ wl_callback* mCallback = nullptr;
+ wp_viewport* mViewport = nullptr;
+ bool mBufferTransformFlippedX = false;
+ bool mBufferTransformFlippedY = false;
+ gfx::IntPoint mSubsurfacePosition = gfx::IntPoint(0, 0);
+ gfx::Rect mViewportSourceRect = gfx::Rect(-1, -1, -1, -1);
+ gfx::IntSize mViewportDestinationSize = gfx::IntSize(-1, -1);
+ nsTArray<RefPtr<CallbackMultiplexHelper>> mCallbackMultiplexHelpers;
+
+ RefPtr<widget::WaylandBuffer> mInProgressBuffer;
+ RefPtr<widget::WaylandBuffer> mFrontBuffer;
+};
+
+} // namespace mozilla::layers
+
+#endif // mozilla_layers_NativeLayerWayland_h
diff --git a/gfx/layers/OOPCanvasRenderer.h b/gfx/layers/OOPCanvasRenderer.h
new file mode 100644
index 0000000000..bc56a2a5fa
--- /dev/null
+++ b/gfx/layers/OOPCanvasRenderer.h
@@ -0,0 +1,53 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_LAYERS_OOPCANVASRENDERER_H_
+#define MOZILLA_LAYERS_OOPCANVASRENDERER_H_
+
+#include "nsISupportsImpl.h"
+
+class nsICanvasRenderingContextInternal;
+
+namespace mozilla {
+
+namespace dom {
+class HTMLCanvasElement;
+}
+
+namespace layers {
+class CanvasClient;
+
+/**
+ * This renderer works with WebGL running in the host process. It does
+ * not perform any graphics operations itself -- it is the client-side
+ * representation. It forwards WebGL composition to the remote process.
+ */
+class OOPCanvasRenderer final {
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(OOPCanvasRenderer)
+
+ public:
+ explicit OOPCanvasRenderer(nsICanvasRenderingContextInternal* aContext)
+ : mContext(aContext) {}
+
+ dom::HTMLCanvasElement* mHTMLCanvasElement = nullptr;
+
+ // The ClientWebGLContext that this is for
+ nsICanvasRenderingContextInternal* mContext = nullptr;
+
+ // The lifetime of this pointer is controlled by OffscreenCanvas
+ // Can be accessed in active thread and ImageBridge thread.
+ // But we never accessed it at the same time on both thread. So no
+ // need to protect this member.
+ CanvasClient* mCanvasClient = nullptr;
+
+ private:
+ ~OOPCanvasRenderer() = default;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // MOZILLA_LAYERS_OOPCANVASRENDERER_H_
diff --git a/gfx/layers/PersistentBufferProvider.cpp b/gfx/layers/PersistentBufferProvider.cpp
new file mode 100644
index 0000000000..4ce356bea9
--- /dev/null
+++ b/gfx/layers/PersistentBufferProvider.cpp
@@ -0,0 +1,674 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "PersistentBufferProvider.h"
+
+#include "mozilla/layers/TextureClient.h"
+#include "mozilla/layers/TextureForwarder.h"
+#include "mozilla/gfx/gfxVars.h"
+#include "mozilla/gfx/DrawTargetWebgl.h"
+#include "mozilla/gfx/Logging.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/StaticPrefs_layers.h"
+#include "pratom.h"
+#include "gfxPlatform.h"
+
+namespace mozilla {
+
+using namespace gfx;
+
+namespace layers {
+
+PersistentBufferProviderBasic::PersistentBufferProviderBasic(DrawTarget* aDt)
+ : mDrawTarget(aDt) {
+ MOZ_COUNT_CTOR(PersistentBufferProviderBasic);
+}
+
+PersistentBufferProviderBasic::~PersistentBufferProviderBasic() {
+ MOZ_COUNT_DTOR(PersistentBufferProviderBasic);
+ Destroy();
+}
+
+already_AddRefed<gfx::DrawTarget>
+PersistentBufferProviderBasic::BorrowDrawTarget(
+ const gfx::IntRect& aPersistedRect) {
+ MOZ_ASSERT(!mSnapshot);
+ RefPtr<gfx::DrawTarget> dt(mDrawTarget);
+ return dt.forget();
+}
+
+bool PersistentBufferProviderBasic::ReturnDrawTarget(
+ already_AddRefed<gfx::DrawTarget> aDT) {
+ RefPtr<gfx::DrawTarget> dt(aDT);
+ MOZ_ASSERT(mDrawTarget == dt);
+ if (dt) {
+ // Since SkiaGL default to storing drawing command until flush
+ // we have to flush it before present.
+ dt->Flush();
+ }
+ return true;
+}
+
+already_AddRefed<gfx::SourceSurface>
+PersistentBufferProviderBasic::BorrowSnapshot(gfx::DrawTarget* aTarget) {
+ mSnapshot = mDrawTarget->Snapshot();
+ RefPtr<SourceSurface> snapshot = mSnapshot;
+ return snapshot.forget();
+}
+
+void PersistentBufferProviderBasic::ReturnSnapshot(
+ already_AddRefed<gfx::SourceSurface> aSnapshot) {
+ RefPtr<SourceSurface> snapshot = aSnapshot;
+ MOZ_ASSERT(!snapshot || snapshot == mSnapshot);
+ mSnapshot = nullptr;
+}
+
+void PersistentBufferProviderBasic::Destroy() {
+ mSnapshot = nullptr;
+ mDrawTarget = nullptr;
+}
+
+// static
+already_AddRefed<PersistentBufferProviderBasic>
+PersistentBufferProviderBasic::Create(gfx::IntSize aSize,
+ gfx::SurfaceFormat aFormat,
+ gfx::BackendType aBackend) {
+ RefPtr<DrawTarget> dt =
+ gfxPlatform::GetPlatform()->CreateDrawTargetForBackend(aBackend, aSize,
+ aFormat);
+
+ if (dt) {
+ // This is simply to ensure the DrawTarget gets initialized, and will detect
+ // a device reset, even if we're on the main thread.
+ dt->ClearRect(Rect(0, 0, 0, 0));
+ }
+
+ if (!dt || !dt->IsValid()) {
+ return nullptr;
+ }
+
+ RefPtr<PersistentBufferProviderBasic> provider =
+ new PersistentBufferProviderBasic(dt);
+
+ return provider.forget();
+}
+
+PersistentBufferProviderAccelerated::PersistentBufferProviderAccelerated(
+ DrawTarget* aDt)
+ : PersistentBufferProviderBasic(aDt) {
+ MOZ_COUNT_CTOR(PersistentBufferProviderAccelerated);
+ MOZ_ASSERT(aDt->GetBackendType() == BackendType::WEBGL);
+}
+
+PersistentBufferProviderAccelerated::~PersistentBufferProviderAccelerated() {
+ MOZ_COUNT_DTOR(PersistentBufferProviderAccelerated);
+}
+
+inline gfx::DrawTargetWebgl*
+PersistentBufferProviderAccelerated::GetDrawTargetWebgl() const {
+ return static_cast<gfx::DrawTargetWebgl*>(mDrawTarget.get());
+}
+
+Maybe<layers::SurfaceDescriptor>
+PersistentBufferProviderAccelerated::GetFrontBuffer() {
+ return GetDrawTargetWebgl()->GetFrontBuffer();
+}
+
+already_AddRefed<gfx::DrawTarget>
+PersistentBufferProviderAccelerated::BorrowDrawTarget(
+ const gfx::IntRect& aPersistedRect) {
+ GetDrawTargetWebgl()->BeginFrame(aPersistedRect);
+ return PersistentBufferProviderBasic::BorrowDrawTarget(aPersistedRect);
+}
+
+bool PersistentBufferProviderAccelerated::ReturnDrawTarget(
+ already_AddRefed<gfx::DrawTarget> aDT) {
+ bool result = PersistentBufferProviderBasic::ReturnDrawTarget(std::move(aDT));
+ GetDrawTargetWebgl()->EndFrame();
+ return result;
+}
+
+already_AddRefed<gfx::SourceSurface>
+PersistentBufferProviderAccelerated::BorrowSnapshot(gfx::DrawTarget* aTarget) {
+ mSnapshot = GetDrawTargetWebgl()->GetOptimizedSnapshot(aTarget);
+ return do_AddRef(mSnapshot);
+}
+
+bool PersistentBufferProviderAccelerated::RequiresRefresh() const {
+ return GetDrawTargetWebgl()->RequiresRefresh();
+}
+
+void PersistentBufferProviderAccelerated::OnMemoryPressure() {
+ GetDrawTargetWebgl()->OnMemoryPressure();
+}
+
+static already_AddRefed<TextureClient> CreateTexture(
+ KnowsCompositor* aKnowsCompositor, gfx::SurfaceFormat aFormat,
+ gfx::IntSize aSize, bool aWillReadFrequently) {
+ TextureAllocationFlags flags = ALLOC_DEFAULT;
+ if (aWillReadFrequently) {
+ flags = TextureAllocationFlags(flags | ALLOC_DO_NOT_ACCELERATE);
+ }
+ return TextureClient::CreateForDrawing(
+ aKnowsCompositor, aFormat, aSize, BackendSelector::Canvas,
+ TextureFlags::DEFAULT | TextureFlags::NON_BLOCKING_READ_LOCK, flags);
+}
+
+// static
+already_AddRefed<PersistentBufferProviderShared>
+PersistentBufferProviderShared::Create(gfx::IntSize aSize,
+ gfx::SurfaceFormat aFormat,
+ KnowsCompositor* aKnowsCompositor,
+ bool aWillReadFrequently) {
+ if (!aKnowsCompositor || !aKnowsCompositor->GetTextureForwarder() ||
+ !aKnowsCompositor->GetTextureForwarder()->IPCOpen()) {
+ return nullptr;
+ }
+
+ if (!StaticPrefs::layers_shared_buffer_provider_enabled()) {
+ return nullptr;
+ }
+
+#ifdef XP_WIN
+ // Bug 1285271 - Disable shared buffer provider on Windows with D2D due to
+ // instability, unless we are remoting the canvas drawing to the GPU process.
+ if (gfxPlatform::GetPlatform()->GetPreferredCanvasBackend() ==
+ BackendType::DIRECT2D1_1 &&
+ !TextureData::IsRemote(aKnowsCompositor, BackendSelector::Canvas)) {
+ return nullptr;
+ }
+#endif
+
+ RefPtr<TextureClient> texture =
+ CreateTexture(aKnowsCompositor, aFormat, aSize, aWillReadFrequently);
+ if (!texture) {
+ return nullptr;
+ }
+
+ RefPtr<PersistentBufferProviderShared> provider =
+ new PersistentBufferProviderShared(aSize, aFormat, aKnowsCompositor,
+ texture, aWillReadFrequently);
+ return provider.forget();
+}
+
+PersistentBufferProviderShared::PersistentBufferProviderShared(
+ gfx::IntSize aSize, gfx::SurfaceFormat aFormat,
+ KnowsCompositor* aKnowsCompositor, RefPtr<TextureClient>& aTexture,
+ bool aWillReadFrequently)
+
+ : mSize(aSize),
+ mFormat(aFormat),
+ mKnowsCompositor(aKnowsCompositor),
+ mFront(Nothing()),
+ mWillReadFrequently(aWillReadFrequently) {
+ MOZ_ASSERT(aKnowsCompositor);
+ if (mTextures.append(aTexture)) {
+ mBack = Some<uint32_t>(0);
+ }
+
+ // XXX KnowsCompositor could be used for mMaxAllowedTextures
+ if (gfxVars::UseWebRenderTripleBufferingWin()) {
+ ++mMaxAllowedTextures;
+ }
+
+ MOZ_COUNT_CTOR(PersistentBufferProviderShared);
+}
+
+PersistentBufferProviderShared::~PersistentBufferProviderShared() {
+ MOZ_COUNT_DTOR(PersistentBufferProviderShared);
+
+ if (IsActivityTracked()) {
+ mKnowsCompositor->GetActiveResourceTracker()->RemoveObject(this);
+ }
+
+ Destroy();
+}
+
+bool PersistentBufferProviderShared::SetKnowsCompositor(
+ KnowsCompositor* aKnowsCompositor, bool& aOutLostFrontTexture) {
+ MOZ_ASSERT(aKnowsCompositor);
+ MOZ_ASSERT(!aOutLostFrontTexture);
+ if (!aKnowsCompositor) {
+ return false;
+ }
+
+ if (mKnowsCompositor == aKnowsCompositor) {
+ // The forwarder should not change most of the time.
+ return true;
+ }
+
+ if (IsActivityTracked()) {
+ mKnowsCompositor->GetActiveResourceTracker()->RemoveObject(this);
+ }
+
+ if (mKnowsCompositor->GetTextureForwarder() !=
+ aKnowsCompositor->GetTextureForwarder() ||
+ mKnowsCompositor->GetCompositorBackendType() !=
+ aKnowsCompositor->GetCompositorBackendType()) {
+ // We are going to be used with an different and/or incompatible forwarder.
+ // This should be extremely rare. We have to copy the front buffer into a
+ // texture that is compatible with the new forwarder.
+
+ // Grab the current front buffer.
+ RefPtr<TextureClient> prevTexture = GetTexture(mFront);
+
+ // Get rid of everything else
+ Destroy();
+
+ if (prevTexture && !prevTexture->IsValid()) {
+ aOutLostFrontTexture = true;
+ } else if (prevTexture && prevTexture->IsValid()) {
+ RefPtr<TextureClient> newTexture =
+ CreateTexture(aKnowsCompositor, mFormat, mSize, mWillReadFrequently);
+
+ MOZ_ASSERT(newTexture);
+ if (!newTexture) {
+ return false;
+ }
+
+ // If we early-return in one of the following branches, we will
+ // leave the buffer provider in an empty state, since we called
+ // Destroy. Not ideal but at least we won't try to use it with a
+ // an incompatible ipc channel.
+
+ if (!newTexture->Lock(OpenMode::OPEN_WRITE)) {
+ return false;
+ }
+
+ if (!prevTexture->Lock(OpenMode::OPEN_READ)) {
+ newTexture->Unlock();
+ return false;
+ }
+
+ bool success =
+ prevTexture->CopyToTextureClient(newTexture, nullptr, nullptr);
+
+ prevTexture->Unlock();
+ newTexture->Unlock();
+
+ if (!success) {
+ return false;
+ }
+
+ if (!mTextures.append(newTexture)) {
+ return false;
+ }
+ mFront = Some<uint32_t>(mTextures.length() - 1);
+ mBack = mFront;
+ }
+ }
+
+ mKnowsCompositor = aKnowsCompositor;
+
+ return true;
+}
+
+TextureClient* PersistentBufferProviderShared::GetTexture(
+ const Maybe<uint32_t>& aIndex) {
+ if (aIndex.isNothing() || !CheckIndex(aIndex.value())) {
+ return nullptr;
+ }
+ return mTextures[aIndex.value()];
+}
+
+already_AddRefed<gfx::DrawTarget>
+PersistentBufferProviderShared::BorrowDrawTarget(
+ const gfx::IntRect& aPersistedRect) {
+ if (!mKnowsCompositor->GetTextureForwarder() ||
+ !mKnowsCompositor->GetTextureForwarder()->IPCOpen()) {
+ return nullptr;
+ }
+
+ MOZ_ASSERT(!mSnapshot);
+
+ if (IsActivityTracked()) {
+ mKnowsCompositor->GetActiveResourceTracker()->MarkUsed(this);
+ } else {
+ mKnowsCompositor->GetActiveResourceTracker()->AddObject(this);
+ }
+
+ if (mDrawTarget) {
+ RefPtr<gfx::DrawTarget> dt(mDrawTarget);
+ return dt.forget();
+ }
+
+ auto previousBackBuffer = mBack;
+
+ TextureClient* tex = GetTexture(mBack);
+
+ // First try to reuse the current back buffer. If we can do that it means
+ // we can skip copying its content to the new back buffer.
+ if (tex && tex->IsReadLocked()) {
+ // The back buffer is currently used by the compositor, we can't draw
+ // into it.
+ tex = nullptr;
+ }
+
+ if (!tex) {
+ // Try to grab an already allocated texture if any is available.
+ for (uint32_t i = 0; i < mTextures.length(); ++i) {
+ if (!mTextures[i]->IsReadLocked()) {
+ mBack = Some(i);
+ tex = mTextures[i];
+ break;
+ }
+ }
+ }
+
+ if (!tex) {
+ // We have to allocate a new texture.
+ if (mTextures.length() >= mMaxAllowedTextures) {
+ // We should never need to buffer that many textures, something's wrong.
+ // In theory we throttle the main thread when the compositor can't keep
+ // up, so we shoud never get in a situation where we sent 4 textures to
+ // the compositor and the latter has not released any of them. In
+ // practice, though, the throttling mechanism appears to have some issues,
+ // especially when switching between layer managers (during tab-switch).
+ // To make sure we don't get too far ahead of the compositor, we send a
+ // sync ping to the compositor thread...
+ mKnowsCompositor->SyncWithCompositor();
+ // ...and try again.
+ for (uint32_t i = 0; i < mTextures.length(); ++i) {
+ if (!mTextures[i]->IsReadLocked()) {
+ gfxCriticalNote << "Managed to allocate after flush.";
+ mBack = Some(i);
+ tex = mTextures[i];
+ break;
+ }
+ }
+
+ if (!tex) {
+ gfxCriticalNote << "Unexpected BufferProvider over-production.";
+ // It would be pretty bad to keep piling textures up at this point so we
+ // call NotifyInactive to remove some of our textures.
+ NotifyInactive();
+ // Give up now. The caller can fall-back to a non-shared buffer
+ // provider.
+ return nullptr;
+ }
+ }
+
+ RefPtr<TextureClient> newTexture =
+ CreateTexture(mKnowsCompositor, mFormat, mSize, mWillReadFrequently);
+
+ MOZ_ASSERT(newTexture);
+ if (newTexture) {
+ if (mTextures.append(newTexture)) {
+ tex = newTexture;
+ mBack = Some<uint32_t>(mTextures.length() - 1);
+ }
+ }
+ }
+
+ if (!tex) {
+ return nullptr;
+ }
+
+ if (mPermanentBackBuffer) {
+ // If we have a permanent back buffer lock the selected one and switch to
+ // the permanent one before borrowing the DrawTarget. We will copy back into
+ // the selected one when ReturnDrawTarget is called, before we make it the
+ // new front buffer.
+ if (!tex->Lock(OpenMode::OPEN_WRITE)) {
+ return nullptr;
+ }
+ tex = mPermanentBackBuffer;
+ } else {
+ // Copy from the previous back buffer if required.
+ Maybe<TextureClientAutoLock> autoReadLock;
+ TextureClient* previous = nullptr;
+ if (mBack != previousBackBuffer && !aPersistedRect.IsEmpty()) {
+ if (tex->HasSynchronization()) {
+ // We are about to read lock a texture that is in use by the compositor
+ // and has synchronization. To prevent possible future contention we
+ // switch to using a permanent back buffer.
+ mPermanentBackBuffer = CreateTexture(mKnowsCompositor, mFormat, mSize,
+ mWillReadFrequently);
+ if (!mPermanentBackBuffer) {
+ return nullptr;
+ }
+ if (!tex->Lock(OpenMode::OPEN_WRITE)) {
+ return nullptr;
+ }
+ tex = mPermanentBackBuffer;
+ }
+
+ previous = GetTexture(previousBackBuffer);
+ if (previous) {
+ autoReadLock.emplace(previous, OpenMode::OPEN_READ);
+ }
+ }
+
+ if (!tex->Lock(OpenMode::OPEN_READ_WRITE)) {
+ return nullptr;
+ }
+
+ if (autoReadLock.isSome() && autoReadLock->Succeeded() && previous) {
+ DebugOnly<bool> success =
+ previous->CopyToTextureClient(tex, &aPersistedRect, nullptr);
+ MOZ_ASSERT(success);
+ }
+ }
+
+ mDrawTarget = tex->BorrowDrawTarget();
+ if (mDrawTarget) {
+ // This is simply to ensure the DrawTarget gets initialized, and will detect
+ // a device reset, even if we're on the main thread.
+ mDrawTarget->ClearRect(Rect(0, 0, 0, 0));
+
+ if (!mDrawTarget->IsValid()) {
+ mDrawTarget = nullptr;
+ }
+ }
+
+ RefPtr<gfx::DrawTarget> dt(mDrawTarget);
+ return dt.forget();
+}
+
+bool PersistentBufferProviderShared::ReturnDrawTarget(
+ already_AddRefed<gfx::DrawTarget> aDT) {
+ RefPtr<gfx::DrawTarget> dt(aDT);
+ MOZ_ASSERT(mDrawTarget == dt);
+ // Can't change the current front buffer while its snapshot is borrowed!
+ MOZ_ASSERT(!mSnapshot);
+
+ TextureClient* back = GetTexture(mBack);
+ MOZ_ASSERT(back);
+
+ mDrawTarget = nullptr;
+ dt = nullptr;
+
+ // If we have a permanent back buffer we have actually been drawing to that,
+ // so now we must copy to the shared one.
+ if (mPermanentBackBuffer && back) {
+ DebugOnly<bool> success =
+ mPermanentBackBuffer->CopyToTextureClient(back, nullptr, nullptr);
+ MOZ_ASSERT(success);
+
+ // Let our permanent back buffer know that we have finished drawing.
+ mPermanentBackBuffer->EndDraw();
+ }
+
+ if (back) {
+ back->Unlock();
+ mFront = mBack;
+ }
+
+ return !!back;
+}
+
+TextureClient* PersistentBufferProviderShared::GetTextureClient() {
+ // Can't access the front buffer while drawing.
+ MOZ_ASSERT(!mDrawTarget);
+ TextureClient* texture = GetTexture(mFront);
+ if (!texture) {
+ gfxCriticalNote
+ << "PersistentBufferProviderShared: front buffer unavailable";
+ return nullptr;
+ }
+
+ // Sometimes, for example on tab switch, we re-forward our texture. So if we
+ // are and it is still read locked, then borrow and return our DrawTarget to
+ // force it to be copied to a texture that we will safely read lock.
+ if (texture->IsReadLocked()) {
+ RefPtr<DrawTarget> dt =
+ BorrowDrawTarget(IntRect(0, 0, mSize.width, mSize.height));
+
+ // If we failed to borrow a DrawTarget then all our textures must be read
+ // locked or we failed to create one, so we'll just return the current front
+ // buffer even though that might lead to contention.
+ if (dt) {
+ ReturnDrawTarget(dt.forget());
+ texture = GetTexture(mFront);
+ if (!texture) {
+ gfxCriticalNote
+ << "PersistentBufferProviderShared: front buffer unavailable";
+ return nullptr;
+ }
+ }
+ } else {
+ // If it isn't read locked then make sure it is set as updated, so that we
+ // will always read lock even if we've forwarded the texture before.
+ texture->SetUpdated();
+ }
+
+ return texture;
+}
+
+already_AddRefed<gfx::SourceSurface>
+PersistentBufferProviderShared::BorrowSnapshot(gfx::DrawTarget* aTarget) {
+ // If we have a permanent back buffer we can always use that to snapshot.
+ if (mPermanentBackBuffer) {
+ mSnapshot = mPermanentBackBuffer->BorrowSnapshot();
+ return do_AddRef(mSnapshot);
+ }
+
+ if (mDrawTarget) {
+ auto back = GetTexture(mBack);
+ MOZ_ASSERT(back && back->IsLocked());
+ mSnapshot = back->BorrowSnapshot();
+ return do_AddRef(mSnapshot);
+ }
+
+ auto front = GetTexture(mFront);
+ if (!front || front->IsLocked()) {
+ MOZ_ASSERT(false);
+ return nullptr;
+ }
+
+ if (front->IsReadLocked() && front->HasSynchronization()) {
+ // We are about to read lock a texture that is in use by the compositor and
+ // has synchronization. To prevent possible future contention we switch to
+ // using a permanent back buffer.
+ mPermanentBackBuffer =
+ CreateTexture(mKnowsCompositor, mFormat, mSize, mWillReadFrequently);
+ if (!mPermanentBackBuffer ||
+ !mPermanentBackBuffer->Lock(OpenMode::OPEN_READ_WRITE)) {
+ return nullptr;
+ }
+
+ if (!front->Lock(OpenMode::OPEN_READ)) {
+ return nullptr;
+ }
+
+ DebugOnly<bool> success =
+ front->CopyToTextureClient(mPermanentBackBuffer, nullptr, nullptr);
+ MOZ_ASSERT(success);
+ front->Unlock();
+ mSnapshot = mPermanentBackBuffer->BorrowSnapshot();
+ return do_AddRef(mSnapshot);
+ }
+
+ if (!front->Lock(OpenMode::OPEN_READ)) {
+ return nullptr;
+ }
+
+ mSnapshot = front->BorrowSnapshot();
+
+ return do_AddRef(mSnapshot);
+}
+
+void PersistentBufferProviderShared::ReturnSnapshot(
+ already_AddRefed<gfx::SourceSurface> aSnapshot) {
+ RefPtr<SourceSurface> snapshot = aSnapshot;
+ MOZ_ASSERT(!snapshot || snapshot == mSnapshot);
+
+ mSnapshot = nullptr;
+ snapshot = nullptr;
+
+ if (mDrawTarget || mPermanentBackBuffer) {
+ return;
+ }
+
+ auto front = GetTexture(mFront);
+ if (front) {
+ front->Unlock();
+ }
+}
+
+void PersistentBufferProviderShared::NotifyInactive() {
+ ClearCachedResources();
+}
+
+void PersistentBufferProviderShared::ClearCachedResources() {
+ RefPtr<TextureClient> front = GetTexture(mFront);
+ RefPtr<TextureClient> back = GetTexture(mBack);
+
+ // Clear all textures (except the front and back ones that we just kept).
+ mTextures.clear();
+ mPermanentBackBuffer = nullptr;
+
+ if (back) {
+ if (mTextures.append(back)) {
+ mBack = Some<uint32_t>(0);
+ }
+ if (front == back) {
+ mFront = mBack;
+ }
+ }
+
+ if (front && front != back) {
+ if (mTextures.append(front)) {
+ mFront = Some<uint32_t>(mTextures.length() - 1);
+ }
+ }
+}
+
+void PersistentBufferProviderShared::Destroy() {
+ mSnapshot = nullptr;
+ mDrawTarget = nullptr;
+
+ if (mPermanentBackBuffer) {
+ mPermanentBackBuffer->Unlock();
+ mPermanentBackBuffer = nullptr;
+ }
+
+ for (auto& texture : mTextures) {
+ if (texture && texture->IsLocked()) {
+ MOZ_ASSERT(false);
+ texture->Unlock();
+ }
+ }
+
+ mTextures.clear();
+}
+
+bool PersistentBufferProviderShared::IsAccelerated() const {
+#ifdef XP_WIN
+ // Detect if we're using D2D canvas.
+ if (mWillReadFrequently || mTextures.empty() || !mTextures[0]) {
+ return false;
+ }
+ auto* data = mTextures[0]->GetInternalData();
+ if (data && data->AsD3D11TextureData()) {
+ return true;
+ }
+#endif
+ return false;
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/PersistentBufferProvider.h b/gfx/layers/PersistentBufferProvider.h
new file mode 100644
index 0000000000..25abcc8fcb
--- /dev/null
+++ b/gfx/layers/PersistentBufferProvider.h
@@ -0,0 +1,273 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_GFX_PersistentBUFFERPROVIDER_H
+#define MOZILLA_GFX_PersistentBUFFERPROVIDER_H
+
+#include "mozilla/Assertions.h" // for MOZ_ASSERT, etc
+#include "mozilla/RefPtr.h" // for RefPtr, already_AddRefed, etc
+#include "mozilla/layers/KnowsCompositor.h"
+#include "mozilla/layers/LayersSurfaces.h"
+#include "mozilla/layers/LayersTypes.h"
+#include "mozilla/RefCounted.h"
+#include "mozilla/gfx/Types.h"
+#include "mozilla/Vector.h"
+#include "mozilla/WeakPtr.h"
+
+namespace mozilla {
+
+class ClientWebGLContext;
+
+namespace gfx {
+class SourceSurface;
+class DrawTarget;
+class DrawTargetWebgl;
+} // namespace gfx
+
+namespace layers {
+
+class TextureClient;
+
+/**
+ * A PersistentBufferProvider is for users which require the temporary use of
+ * a DrawTarget to draw into. When they're done drawing they return the
+ * DrawTarget, when they later need to continue drawing they get a DrawTarget
+ * from the provider again, the provider will guarantee the contents of the
+ * previously returned DrawTarget is persisted into the one newly returned.
+ */
+class PersistentBufferProvider : public RefCounted<PersistentBufferProvider>,
+ public SupportsWeakPtr {
+ public:
+ MOZ_DECLARE_REFCOUNTED_VIRTUAL_TYPENAME(PersistentBufferProvider)
+
+ virtual ~PersistentBufferProvider() = default;
+
+ virtual bool IsShared() const { return false; }
+ virtual bool IsAccelerated() const { return false; }
+
+ /**
+ * Get a DrawTarget from the PersistentBufferProvider.
+ *
+ * \param aPersistedRect This indicates the area of the DrawTarget that needs
+ * to have remained the same since the call to
+ * ReturnDrawTarget.
+ */
+ virtual already_AddRefed<gfx::DrawTarget> BorrowDrawTarget(
+ const gfx::IntRect& aPersistedRect) = 0;
+
+ /**
+ * Return a DrawTarget to the PersistentBufferProvider and indicate the
+ * contents of this DrawTarget is to be considered current by the
+ * BufferProvider. The caller should forget any references to the DrawTarget.
+ */
+ virtual bool ReturnDrawTarget(already_AddRefed<gfx::DrawTarget> aDT) = 0;
+
+ /**
+ * Temporarily borrow a snapshot of the provider. If a target is supplied,
+ * the snapshot will be optimized for it, if applicable.
+ */
+ virtual already_AddRefed<gfx::SourceSurface> BorrowSnapshot(
+ gfx::DrawTarget* aTarget = nullptr) = 0;
+
+ virtual void ReturnSnapshot(
+ already_AddRefed<gfx::SourceSurface> aSnapshot) = 0;
+
+ virtual TextureClient* GetTextureClient() { return nullptr; }
+
+ virtual void OnMemoryPressure() {}
+
+ virtual void OnShutdown() {}
+
+ virtual bool SetKnowsCompositor(KnowsCompositor* aKnowsCompositor,
+ bool& aOutLostFrontTexture) {
+ return true;
+ }
+
+ virtual void ClearCachedResources() {}
+
+ /**
+ * Return true if this provider preserves the drawing state (clips,
+ * transforms, etc.) across frames. In practice this means users of the
+ * provider can skip popping all of the clips at the end of the frames and
+ * pushing them back at the beginning of the following frames, which can be
+ * costly (cf. bug 1294351).
+ */
+ virtual bool PreservesDrawingState() const = 0;
+
+ /**
+ * Whether or not the provider should be recreated, such as when profiling
+ * heuristics determine this type of provider is no longer advantageous to
+ * use.
+ */
+ virtual bool RequiresRefresh() const { return false; }
+
+ /**
+ * Provide a WebGL front buffer for compositing, if available.
+ */
+ virtual Maybe<layers::SurfaceDescriptor> GetFrontBuffer() {
+ return Nothing();
+ }
+};
+
+class PersistentBufferProviderBasic : public PersistentBufferProvider {
+ public:
+ MOZ_DECLARE_REFCOUNTED_VIRTUAL_TYPENAME(PersistentBufferProviderBasic,
+ override)
+
+ static already_AddRefed<PersistentBufferProviderBasic> Create(
+ gfx::IntSize aSize, gfx::SurfaceFormat aFormat,
+ gfx::BackendType aBackend);
+
+ explicit PersistentBufferProviderBasic(gfx::DrawTarget* aTarget);
+
+ already_AddRefed<gfx::DrawTarget> BorrowDrawTarget(
+ const gfx::IntRect& aPersistedRect) override;
+
+ bool ReturnDrawTarget(already_AddRefed<gfx::DrawTarget> aDT) override;
+
+ already_AddRefed<gfx::SourceSurface> BorrowSnapshot(
+ gfx::DrawTarget* aTarget) override;
+
+ void ReturnSnapshot(already_AddRefed<gfx::SourceSurface> aSnapshot) override;
+
+ bool PreservesDrawingState() const override { return true; }
+
+ void OnShutdown() override { Destroy(); }
+
+ protected:
+ void Destroy();
+
+ ~PersistentBufferProviderBasic() override;
+
+ RefPtr<gfx::DrawTarget> mDrawTarget;
+ RefPtr<gfx::SourceSurface> mSnapshot;
+};
+
+class PersistentBufferProviderAccelerated
+ : public PersistentBufferProviderBasic {
+ public:
+ MOZ_DECLARE_REFCOUNTED_VIRTUAL_TYPENAME(PersistentBufferProviderAccelerated,
+ override)
+
+ explicit PersistentBufferProviderAccelerated(gfx::DrawTarget* aTarget);
+
+ bool IsAccelerated() const override { return true; }
+
+ Maybe<layers::SurfaceDescriptor> GetFrontBuffer() override;
+
+ already_AddRefed<gfx::DrawTarget> BorrowDrawTarget(
+ const gfx::IntRect& aPersistedRect) override;
+
+ bool ReturnDrawTarget(already_AddRefed<gfx::DrawTarget> aDT) override;
+
+ already_AddRefed<gfx::SourceSurface> BorrowSnapshot(
+ gfx::DrawTarget* aTarget) override;
+
+ bool RequiresRefresh() const override;
+
+ void OnMemoryPressure() override;
+
+ protected:
+ ~PersistentBufferProviderAccelerated() override;
+
+ gfx::DrawTargetWebgl* GetDrawTargetWebgl() const;
+};
+
+/**
+ * Provides access to a buffer which can be sent to the compositor without
+ * requiring a copy.
+ */
+class PersistentBufferProviderShared : public PersistentBufferProvider,
+ public ActiveResource {
+ public:
+ MOZ_DECLARE_REFCOUNTED_VIRTUAL_TYPENAME(PersistentBufferProviderShared,
+ override)
+
+ static already_AddRefed<PersistentBufferProviderShared> Create(
+ gfx::IntSize aSize, gfx::SurfaceFormat aFormat,
+ KnowsCompositor* aKnowsCompositor, bool aWillReadFrequently = false);
+
+ bool IsShared() const override { return true; }
+
+ already_AddRefed<gfx::DrawTarget> BorrowDrawTarget(
+ const gfx::IntRect& aPersistedRect) override;
+
+ bool ReturnDrawTarget(already_AddRefed<gfx::DrawTarget> aDT) override;
+
+ already_AddRefed<gfx::SourceSurface> BorrowSnapshot(
+ gfx::DrawTarget* aTarget) override;
+
+ void ReturnSnapshot(already_AddRefed<gfx::SourceSurface> aSnapshot) override;
+
+ TextureClient* GetTextureClient() override;
+
+ void NotifyInactive() override;
+
+ void OnShutdown() override { Destroy(); }
+
+ bool SetKnowsCompositor(KnowsCompositor* aKnowsCompositor,
+ bool& aOutLostFrontTexture) override;
+
+ void ClearCachedResources() override;
+
+ bool PreservesDrawingState() const override { return false; }
+
+ bool IsAccelerated() const override;
+
+ protected:
+ PersistentBufferProviderShared(gfx::IntSize aSize, gfx::SurfaceFormat aFormat,
+ KnowsCompositor* aKnowsCompositor,
+ RefPtr<TextureClient>& aTexture,
+ bool aWillReadFrequently);
+
+ ~PersistentBufferProviderShared();
+
+ TextureClient* GetTexture(const Maybe<uint32_t>& aIndex);
+ bool CheckIndex(uint32_t aIndex) { return aIndex < mTextures.length(); }
+
+ void Destroy();
+
+ gfx::IntSize mSize;
+ gfx::SurfaceFormat mFormat;
+ RefPtr<KnowsCompositor> mKnowsCompositor;
+ // If the texture has its own synchronization then copying back from the
+ // previous texture can cause contention issues and even deadlocks. So we use
+ // a separate permanent back buffer and copy into the shared back buffer when
+ // the DrawTarget is returned, before making it the new front buffer.
+ RefPtr<TextureClient> mPermanentBackBuffer;
+ static const size_t kMaxTexturesAllowed = 5;
+ Vector<RefPtr<TextureClient>, kMaxTexturesAllowed + 2> mTextures;
+ // Offset of the texture in mTextures that the canvas uses.
+ Maybe<uint32_t> mBack;
+ // Offset of the texture in mTextures that is presented to the compositor.
+ Maybe<uint32_t> mFront;
+ // Whether to avoid acceleration.
+ bool mWillReadFrequently = false;
+
+ RefPtr<gfx::DrawTarget> mDrawTarget;
+ RefPtr<gfx::SourceSurface> mSnapshot;
+ size_t mMaxAllowedTextures = kMaxTexturesAllowed;
+};
+
+struct AutoReturnSnapshot final {
+ PersistentBufferProvider* mBufferProvider;
+ RefPtr<gfx::SourceSurface>* mSnapshot;
+
+ explicit AutoReturnSnapshot(PersistentBufferProvider* aProvider = nullptr)
+ : mBufferProvider(aProvider), mSnapshot(nullptr) {}
+
+ ~AutoReturnSnapshot() {
+ if (mBufferProvider) {
+ mBufferProvider->ReturnSnapshot(mSnapshot ? mSnapshot->forget()
+ : nullptr);
+ }
+ }
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif
diff --git a/gfx/layers/ProfilerScreenshots.cpp b/gfx/layers/ProfilerScreenshots.cpp
new file mode 100644
index 0000000000..44bef9e410
--- /dev/null
+++ b/gfx/layers/ProfilerScreenshots.cpp
@@ -0,0 +1,140 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/layers/ProfilerScreenshots.h"
+
+#include "mozilla/TimeStamp.h"
+
+#include "GeckoProfiler.h"
+#include "gfxUtils.h"
+#include "nsThreadUtils.h"
+
+using namespace mozilla;
+using namespace mozilla::gfx;
+using namespace mozilla::layers;
+
+struct ScreenshotMarker {
+ static constexpr mozilla::Span<const char> MarkerTypeName() {
+ return mozilla::MakeStringSpan("CompositorScreenshot");
+ }
+ static void StreamJSONMarkerData(
+ mozilla::baseprofiler::SpliceableJSONWriter& aWriter,
+ const mozilla::ProfilerString8View& aScreenshotDataURL,
+ const mozilla::gfx::IntSize& aWindowSize, uint32_t aWindowIdentifier) {
+ if (aScreenshotDataURL.Length() != 0) {
+ aWriter.UniqueStringProperty("url", aScreenshotDataURL);
+ }
+
+ aWriter.IntProperty("windowID", aWindowIdentifier);
+
+ if (!aWindowSize.IsEmpty()) {
+ aWriter.DoubleProperty("windowWidth", aWindowSize.width);
+ aWriter.DoubleProperty("windowHeight", aWindowSize.height);
+ }
+ }
+ static mozilla::MarkerSchema MarkerTypeDisplay() {
+ return mozilla::MarkerSchema::SpecialFrontendLocation{};
+ }
+};
+
+uint32_t ProfilerScreenshots::sWindowCounter = 0;
+
+ProfilerScreenshots::ProfilerScreenshots()
+ : mMutex("ProfilerScreenshots::mMutex"),
+ mLiveSurfaceCount(0),
+ mWindowIdentifier(++sWindowCounter) {}
+
+ProfilerScreenshots::~ProfilerScreenshots() {
+ if (mWindowIdentifier) {
+ profiler_add_marker("CompositorScreenshotWindowDestroyed",
+ geckoprofiler::category::GRAPHICS,
+ MarkerThreadId::MainThread(), ScreenshotMarker{},
+ /* aScreenshotDataURL */ "", mozilla::gfx::IntSize{},
+ mWindowIdentifier);
+ }
+}
+
+/* static */
+bool ProfilerScreenshots::IsEnabled() {
+ return profiler_feature_active(ProfilerFeature::Screenshots);
+}
+
+void ProfilerScreenshots::SubmitScreenshot(
+ const gfx::IntSize& aOriginalSize, const IntSize& aScaledSize,
+ const TimeStamp& aTimeStamp,
+ const std::function<bool(DataSourceSurface*)>& aPopulateSurface) {
+ RefPtr<DataSourceSurface> backingSurface = TakeNextSurface();
+ if (!backingSurface) {
+ return;
+ }
+
+ MOZ_RELEASE_ASSERT(aScaledSize <= backingSurface->GetSize());
+
+ bool succeeded = aPopulateSurface(backingSurface);
+
+ if (!succeeded) {
+ PROFILER_MARKER_UNTYPED(
+ "NoCompositorScreenshot because aPopulateSurface callback failed",
+ GRAPHICS);
+ ReturnSurface(backingSurface);
+ return;
+ }
+
+ NS_DispatchBackgroundTask(NS_NewRunnableFunction(
+ "ProfilerScreenshots::SubmitScreenshot",
+ [self = RefPtr<ProfilerScreenshots>{this},
+ backingSurface = std::move(backingSurface),
+ windowIdentifier = mWindowIdentifier, originalSize = aOriginalSize,
+ scaledSize = aScaledSize, timeStamp = aTimeStamp]() {
+ // Create a new surface that wraps backingSurface's data but has the
+ // correct size.
+ DataSourceSurface::ScopedMap scopedMap(backingSurface,
+ DataSourceSurface::READ);
+ RefPtr<DataSourceSurface> surf =
+ Factory::CreateWrappingDataSourceSurface(
+ scopedMap.GetData(), scopedMap.GetStride(), scaledSize,
+ SurfaceFormat::B8G8R8A8);
+
+ // Encode surf to a JPEG data URL.
+ nsCString dataURL;
+ nsresult rv = gfxUtils::EncodeSourceSurface(
+ surf, ImageType::JPEG, u"quality=85"_ns, gfxUtils::eDataURIEncode,
+ nullptr, &dataURL);
+ if (NS_SUCCEEDED(rv)) {
+ // Add a marker with the data URL.
+ profiler_add_marker(
+ "CompositorScreenshot", geckoprofiler::category::GRAPHICS,
+ {MarkerThreadId::MainThread(),
+ MarkerTiming::InstantAt(timeStamp)},
+ ScreenshotMarker{}, dataURL, originalSize, windowIdentifier);
+ }
+
+ // Return backingSurface back to the surface pool.
+ self->ReturnSurface(backingSurface);
+ }));
+}
+
+already_AddRefed<DataSourceSurface> ProfilerScreenshots::TakeNextSurface() {
+ MutexAutoLock mon(mMutex);
+ if (!mAvailableSurfaces.IsEmpty()) {
+ RefPtr<DataSourceSurface> surf = mAvailableSurfaces[0];
+ mAvailableSurfaces.RemoveElementAt(0);
+ return surf.forget();
+ }
+ if (mLiveSurfaceCount >= 8) {
+ NS_WARNING(
+ "already 8 surfaces in flight, skipping capture for this composite");
+ return nullptr;
+ }
+ mLiveSurfaceCount++;
+ return Factory::CreateDataSourceSurface(ScreenshotSize(),
+ SurfaceFormat::B8G8R8A8);
+}
+
+void ProfilerScreenshots::ReturnSurface(DataSourceSurface* aSurface) {
+ MutexAutoLock mon(this->mMutex);
+ mAvailableSurfaces.AppendElement(aSurface);
+}
diff --git a/gfx/layers/ProfilerScreenshots.h b/gfx/layers/ProfilerScreenshots.h
new file mode 100644
index 0000000000..8239c70403
--- /dev/null
+++ b/gfx/layers/ProfilerScreenshots.h
@@ -0,0 +1,120 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_ProfilerScreenshots_h
+#define mozilla_layers_ProfilerScreenshots_h
+
+#include <functional>
+
+#include "mozilla/Mutex.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/TimeStamp.h"
+#include "nsCOMPtr.h"
+#include "nsTArray.h"
+
+#include "mozilla/gfx/Point.h"
+
+class nsIThread;
+
+namespace mozilla {
+
+namespace gfx {
+class DataSourceSurface;
+}
+
+namespace layers {
+
+/**
+ * Can be used to submit screenshots from the compositor to the profiler.
+ * Screenshots have a fixed bounding size. The user of this class will usually
+ * scale down the window contents first, ideally on the GPU, then read back the
+ * small scaled down image into main memory, and then call SubmitScreenshot to
+ * pass the data to the profiler.
+ * This class encodes each screenshot to a JPEG data URL, on a separate thread.
+ * This class manages that thread and recycles memory buffers.
+ * Users of ProfilerScreenshots should have one ProfilerScreenshot instance per
+ * window, as a unique window id is created by the constructor.
+ */
+class ProfilerScreenshots final {
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(ProfilerScreenshots)
+
+ public:
+ ProfilerScreenshots();
+
+ /**
+ * Returns whether the profiler is currently active and is running with the
+ * "screenshots" feature enabled.
+ */
+ static bool IsEnabled();
+
+ /**
+ * Returns a fixed size that all screenshots should be resized to fit into.
+ */
+ static gfx::IntSize ScreenshotSize() { return gfx::IntSize(350, 350); }
+
+ /**
+ * The main functionality provided by this class.
+ * This method will synchronously invoke the supplied aPopulateSurface
+ * callback function with a DataSourceSurface in which the callback should
+ * store the pixel data. This surface may be larger than aScaledSize, but
+ * only the data in the rectangle (0, 0, aScaledSize.width,
+ * aScaledSize.height) will be read.
+ * @param aWindowIdentifier A pointer-sized integer that can be used to match
+ * up multiple screenshots from the same window.
+ * @param aOriginalSize The unscaled size of the snapshotted window.
+ * @param aScaledSize The scaled size, aScaled <= ScreenshotSize(), which the
+ * snapshot has been resized to.
+ * @param aTimeStamp The time at which the snapshot was taken. In
+ * asynchronous readback implementations, this is the time at which the
+ * readback / copy command was put into the command stream to the GPU, not
+ * the time at which the readback data was mapped into main memory.
+ * @param aPopulateSurface A callback that the caller needs to implement,
+ * which needs to copy the screenshot pixel data into the surface that's
+ * supplied to the callback. Called zero or one times, synchronously.
+ */
+ void SubmitScreenshot(
+ const gfx::IntSize& aOriginalSize, const gfx::IntSize& aScaledSize,
+ const TimeStamp& aTimeStamp,
+ const std::function<bool(gfx::DataSourceSurface*)>& aPopulateSurface);
+
+ private:
+ ~ProfilerScreenshots();
+
+ /**
+ * Recycle a surface from mAvailableSurfaces or create a new one if all
+ * surfaces are currently in use, up to some maximum limit.
+ * Returns null if the limit is reached.
+ * Can be called on any thread.
+ */
+ already_AddRefed<gfx::DataSourceSurface> TakeNextSurface();
+
+ /**
+ * Return aSurface back into the mAvailableSurfaces pool. Can be called on
+ * any thread.
+ */
+ void ReturnSurface(gfx::DataSourceSurface* aSurface);
+
+ // An array of surfaces ready to be recycled. Can be accessed from multiple
+ // threads, protected by mMutex.
+ nsTArray<RefPtr<gfx::DataSourceSurface>> mAvailableSurfaces;
+ // Protects mAvailableSurfaces.
+ Mutex mMutex MOZ_UNANNOTATED;
+ // The total number of surfaces created. If encoding is fast enough to happen
+ // entirely in the time between two calls to SubmitScreenshot, this should
+ // never exceed 1.
+ uint32_t mLiveSurfaceCount;
+
+ // Window identifier used to submit screenshots, created in the constructor.
+ uint32_t mWindowIdentifier;
+
+ // Counter incremented each time a new instance is constructed.
+ static uint32_t sWindowCounter;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_ProfilerScreenshots_h
diff --git a/gfx/layers/RecordedCanvasEventImpl.h b/gfx/layers/RecordedCanvasEventImpl.h
new file mode 100644
index 0000000000..ab234ab8ed
--- /dev/null
+++ b/gfx/layers/RecordedCanvasEventImpl.h
@@ -0,0 +1,597 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_RecordedCanvasEventImpl_h
+#define mozilla_layers_RecordedCanvasEventImpl_h
+
+#include "mozilla/gfx/RecordedEvent.h"
+#include "mozilla/gfx/RecordingTypes.h"
+#include "mozilla/layers/CanvasTranslator.h"
+#include "mozilla/layers/CompositorTypes.h"
+#include "mozilla/layers/TextureClient.h"
+
+namespace mozilla {
+namespace layers {
+
+using gfx::DrawTarget;
+using gfx::IntSize;
+using gfx::RecordedEvent;
+using gfx::RecordedEventDerived;
+using EventType = gfx::RecordedEvent::EventType;
+using gfx::ReadElement;
+using gfx::ReferencePtr;
+using gfx::SurfaceFormat;
+using gfx::WriteElement;
+using ipc::SharedMemoryBasic;
+
+const EventType CANVAS_BEGIN_TRANSACTION = EventType::LAST;
+const EventType CANVAS_END_TRANSACTION = EventType(EventType::LAST + 1);
+const EventType CANVAS_FLUSH = EventType(EventType::LAST + 2);
+const EventType TEXTURE_LOCK = EventType(EventType::LAST + 3);
+const EventType TEXTURE_UNLOCK = EventType(EventType::LAST + 4);
+const EventType CACHE_DATA_SURFACE = EventType(EventType::LAST + 5);
+const EventType PREPARE_DATA_FOR_SURFACE = EventType(EventType::LAST + 6);
+const EventType GET_DATA_FOR_SURFACE = EventType(EventType::LAST + 7);
+const EventType ADD_SURFACE_ALIAS = EventType(EventType::LAST + 8);
+const EventType REMOVE_SURFACE_ALIAS = EventType(EventType::LAST + 9);
+const EventType DEVICE_CHANGE_ACKNOWLEDGED = EventType(EventType::LAST + 10);
+const EventType NEXT_TEXTURE_ID = EventType(EventType::LAST + 11);
+const EventType TEXTURE_DESTRUCTION = EventType(EventType::LAST + 12);
+
+class RecordedCanvasBeginTransaction final
+ : public RecordedEventDerived<RecordedCanvasBeginTransaction> {
+ public:
+ RecordedCanvasBeginTransaction()
+ : RecordedEventDerived(CANVAS_BEGIN_TRANSACTION) {}
+
+ template <class S>
+ MOZ_IMPLICIT RecordedCanvasBeginTransaction(S& aStream);
+
+ bool PlayCanvasEvent(CanvasTranslator* aTranslator) const;
+
+ template <class S>
+ void Record(S& aStream) const;
+
+ std::string GetName() const final { return "RecordedCanvasBeginTransaction"; }
+};
+
+inline bool RecordedCanvasBeginTransaction::PlayCanvasEvent(
+ CanvasTranslator* aTranslator) const {
+ aTranslator->BeginTransaction();
+ return true;
+}
+
+template <class S>
+void RecordedCanvasBeginTransaction::Record(S& aStream) const {}
+
+template <class S>
+RecordedCanvasBeginTransaction::RecordedCanvasBeginTransaction(S& aStream)
+ : RecordedEventDerived(CANVAS_BEGIN_TRANSACTION) {}
+
+class RecordedCanvasEndTransaction final
+ : public RecordedEventDerived<RecordedCanvasEndTransaction> {
+ public:
+ RecordedCanvasEndTransaction()
+ : RecordedEventDerived(CANVAS_END_TRANSACTION) {}
+
+ template <class S>
+ MOZ_IMPLICIT RecordedCanvasEndTransaction(S& aStream);
+
+ bool PlayCanvasEvent(CanvasTranslator* aTranslator) const;
+
+ template <class S>
+ void Record(S& aStream) const;
+
+ std::string GetName() const final { return "RecordedCanvasEndTransaction"; }
+};
+
+inline bool RecordedCanvasEndTransaction::PlayCanvasEvent(
+ CanvasTranslator* aTranslator) const {
+ aTranslator->EndTransaction();
+ return true;
+}
+
+template <class S>
+void RecordedCanvasEndTransaction::Record(S& aStream) const {}
+
+template <class S>
+RecordedCanvasEndTransaction::RecordedCanvasEndTransaction(S& aStream)
+ : RecordedEventDerived(CANVAS_END_TRANSACTION) {}
+
+class RecordedCanvasFlush final
+ : public RecordedEventDerived<RecordedCanvasFlush> {
+ public:
+ RecordedCanvasFlush() : RecordedEventDerived(CANVAS_FLUSH) {}
+
+ template <class S>
+ MOZ_IMPLICIT RecordedCanvasFlush(S& aStream);
+
+ bool PlayCanvasEvent(CanvasTranslator* aTranslator) const;
+
+ template <class S>
+ void Record(S& aStream) const;
+
+ std::string GetName() const final { return "RecordedCanvasFlush"; }
+};
+
+inline bool RecordedCanvasFlush::PlayCanvasEvent(
+ CanvasTranslator* aTranslator) const {
+ aTranslator->Flush();
+ return true;
+}
+
+template <class S>
+void RecordedCanvasFlush::Record(S& aStream) const {}
+
+template <class S>
+RecordedCanvasFlush::RecordedCanvasFlush(S& aStream)
+ : RecordedEventDerived(CANVAS_FLUSH) {}
+
+class RecordedTextureLock final
+ : public RecordedEventDerived<RecordedTextureLock> {
+ public:
+ RecordedTextureLock(int64_t aTextureId, const OpenMode aMode)
+ : RecordedEventDerived(TEXTURE_LOCK),
+ mTextureId(aTextureId),
+ mMode(aMode) {}
+
+ template <class S>
+ MOZ_IMPLICIT RecordedTextureLock(S& aStream);
+
+ bool PlayCanvasEvent(CanvasTranslator* aTranslator) const;
+
+ template <class S>
+ void Record(S& aStream) const;
+
+ std::string GetName() const final { return "TextureLock"; }
+
+ private:
+ int64_t mTextureId;
+ OpenMode mMode;
+};
+
+inline bool RecordedTextureLock::PlayCanvasEvent(
+ CanvasTranslator* aTranslator) const {
+ TextureData* textureData = aTranslator->LookupTextureData(mTextureId);
+ if (!textureData) {
+ return false;
+ }
+
+ textureData->Lock(mMode);
+ return true;
+}
+
+template <class S>
+void RecordedTextureLock::Record(S& aStream) const {
+ WriteElement(aStream, mTextureId);
+ WriteElement(aStream, mMode);
+}
+
+template <class S>
+RecordedTextureLock::RecordedTextureLock(S& aStream)
+ : RecordedEventDerived(TEXTURE_LOCK) {
+ ReadElement(aStream, mTextureId);
+ ReadElementConstrained(aStream, mMode, OpenMode::OPEN_NONE,
+ OpenMode::OPEN_READ_WRITE_ASYNC);
+}
+
+class RecordedTextureUnlock final
+ : public RecordedEventDerived<RecordedTextureUnlock> {
+ public:
+ explicit RecordedTextureUnlock(int64_t aTextureId)
+ : RecordedEventDerived(TEXTURE_UNLOCK), mTextureId(aTextureId) {}
+
+ template <class S>
+ MOZ_IMPLICIT RecordedTextureUnlock(S& aStream);
+
+ bool PlayCanvasEvent(CanvasTranslator* aTranslator) const;
+
+ template <class S>
+ void Record(S& aStream) const;
+
+ std::string GetName() const final { return "TextureUnlock"; }
+
+ private:
+ int64_t mTextureId;
+};
+
+inline bool RecordedTextureUnlock::PlayCanvasEvent(
+ CanvasTranslator* aTranslator) const {
+ TextureData* textureData = aTranslator->LookupTextureData(mTextureId);
+ if (!textureData) {
+ return false;
+ }
+
+ textureData->Unlock();
+ return true;
+}
+
+template <class S>
+void RecordedTextureUnlock::Record(S& aStream) const {
+ WriteElement(aStream, mTextureId);
+}
+
+template <class S>
+RecordedTextureUnlock::RecordedTextureUnlock(S& aStream)
+ : RecordedEventDerived(TEXTURE_UNLOCK) {
+ ReadElement(aStream, mTextureId);
+}
+
+class RecordedCacheDataSurface final
+ : public RecordedEventDerived<RecordedCacheDataSurface> {
+ public:
+ explicit RecordedCacheDataSurface(gfx::SourceSurface* aSurface)
+ : RecordedEventDerived(CACHE_DATA_SURFACE), mSurface(aSurface) {}
+
+ template <class S>
+ MOZ_IMPLICIT RecordedCacheDataSurface(S& aStream);
+
+ bool PlayCanvasEvent(CanvasTranslator* aTranslator) const;
+
+ template <class S>
+ void Record(S& aStream) const;
+
+ std::string GetName() const final { return "RecordedCacheDataSurface"; }
+
+ private:
+ ReferencePtr mSurface;
+};
+
+inline bool RecordedCacheDataSurface::PlayCanvasEvent(
+ CanvasTranslator* aTranslator) const {
+ gfx::SourceSurface* surface = aTranslator->LookupSourceSurface(mSurface);
+ if (!surface) {
+ return false;
+ }
+
+ RefPtr<gfx::DataSourceSurface> dataSurface = surface->GetDataSurface();
+
+ aTranslator->AddDataSurface(mSurface, std::move(dataSurface));
+ return true;
+}
+
+template <class S>
+void RecordedCacheDataSurface::Record(S& aStream) const {
+ WriteElement(aStream, mSurface);
+}
+
+template <class S>
+RecordedCacheDataSurface::RecordedCacheDataSurface(S& aStream)
+ : RecordedEventDerived(CACHE_DATA_SURFACE) {
+ ReadElement(aStream, mSurface);
+}
+
+class RecordedPrepareDataForSurface final
+ : public RecordedEventDerived<RecordedPrepareDataForSurface> {
+ public:
+ explicit RecordedPrepareDataForSurface(const gfx::SourceSurface* aSurface)
+ : RecordedEventDerived(PREPARE_DATA_FOR_SURFACE), mSurface(aSurface) {}
+
+ template <class S>
+ MOZ_IMPLICIT RecordedPrepareDataForSurface(S& aStream);
+
+ bool PlayCanvasEvent(CanvasTranslator* aTranslator) const;
+
+ template <class S>
+ void Record(S& aStream) const;
+
+ std::string GetName() const final { return "RecordedPrepareDataForSurface"; }
+
+ private:
+ ReferencePtr mSurface;
+};
+
+inline bool RecordedPrepareDataForSurface::PlayCanvasEvent(
+ CanvasTranslator* aTranslator) const {
+ RefPtr<gfx::DataSourceSurface> dataSurface =
+ aTranslator->LookupDataSurface(mSurface);
+ if (!dataSurface) {
+ gfx::SourceSurface* surface = aTranslator->LookupSourceSurface(mSurface);
+ if (!surface) {
+ return false;
+ }
+
+ dataSurface = surface->GetDataSurface();
+ if (!dataSurface) {
+ return false;
+ }
+ }
+
+ auto preparedMap = MakeUnique<gfx::DataSourceSurface::ScopedMap>(
+ dataSurface, gfx::DataSourceSurface::READ);
+ if (!preparedMap->IsMapped()) {
+ return false;
+ }
+
+ aTranslator->SetPreparedMap(mSurface, std::move(preparedMap));
+
+ return true;
+}
+
+template <class S>
+void RecordedPrepareDataForSurface::Record(S& aStream) const {
+ WriteElement(aStream, mSurface);
+}
+
+template <class S>
+RecordedPrepareDataForSurface::RecordedPrepareDataForSurface(S& aStream)
+ : RecordedEventDerived(PREPARE_DATA_FOR_SURFACE) {
+ ReadElement(aStream, mSurface);
+}
+
+class RecordedGetDataForSurface final
+ : public RecordedEventDerived<RecordedGetDataForSurface> {
+ public:
+ explicit RecordedGetDataForSurface(const gfx::SourceSurface* aSurface)
+ : RecordedEventDerived(GET_DATA_FOR_SURFACE), mSurface(aSurface) {}
+
+ template <class S>
+ MOZ_IMPLICIT RecordedGetDataForSurface(S& aStream);
+
+ bool PlayCanvasEvent(CanvasTranslator* aTranslator) const;
+
+ template <class S>
+ void Record(S& aStream) const;
+
+ std::string GetName() const final { return "RecordedGetDataForSurface"; }
+
+ private:
+ ReferencePtr mSurface;
+};
+
+inline bool RecordedGetDataForSurface::PlayCanvasEvent(
+ CanvasTranslator* aTranslator) const {
+ gfx::SourceSurface* surface = aTranslator->LookupSourceSurface(mSurface);
+ if (!surface) {
+ return false;
+ }
+
+ UniquePtr<gfx::DataSourceSurface::ScopedMap> map =
+ aTranslator->GetPreparedMap(mSurface);
+ if (!map) {
+ return false;
+ }
+
+ int32_t dataFormatWidth =
+ surface->GetSize().width * BytesPerPixel(surface->GetFormat());
+ int32_t srcStride = map->GetStride();
+ if (dataFormatWidth > srcStride) {
+ return false;
+ }
+
+ char* src = reinterpret_cast<char*>(map->GetData());
+ char* endSrc = src + (map->GetSurface()->GetSize().height * srcStride);
+ while (src < endSrc) {
+ aTranslator->ReturnWrite(src, dataFormatWidth);
+ src += srcStride;
+ }
+
+ return true;
+}
+
+template <class S>
+void RecordedGetDataForSurface::Record(S& aStream) const {
+ WriteElement(aStream, mSurface);
+}
+
+template <class S>
+RecordedGetDataForSurface::RecordedGetDataForSurface(S& aStream)
+ : RecordedEventDerived(GET_DATA_FOR_SURFACE) {
+ ReadElement(aStream, mSurface);
+}
+
+class RecordedAddSurfaceAlias final
+ : public RecordedEventDerived<RecordedAddSurfaceAlias> {
+ public:
+ RecordedAddSurfaceAlias(ReferencePtr aSurfaceAlias,
+ const RefPtr<gfx::SourceSurface>& aActualSurface)
+ : RecordedEventDerived(ADD_SURFACE_ALIAS),
+ mSurfaceAlias(aSurfaceAlias),
+ mActualSurface(aActualSurface) {}
+
+ template <class S>
+ MOZ_IMPLICIT RecordedAddSurfaceAlias(S& aStream);
+
+ bool PlayCanvasEvent(CanvasTranslator* aTranslator) const;
+
+ template <class S>
+ void Record(S& aStream) const;
+
+ std::string GetName() const final { return "RecordedAddSurfaceAlias"; }
+
+ private:
+ ReferencePtr mSurfaceAlias;
+ ReferencePtr mActualSurface;
+};
+
+inline bool RecordedAddSurfaceAlias::PlayCanvasEvent(
+ CanvasTranslator* aTranslator) const {
+ RefPtr<gfx::SourceSurface> surface =
+ aTranslator->LookupSourceSurface(mActualSurface);
+ if (!surface) {
+ return false;
+ }
+
+ aTranslator->AddSourceSurface(mSurfaceAlias, surface);
+ return true;
+}
+
+template <class S>
+void RecordedAddSurfaceAlias::Record(S& aStream) const {
+ WriteElement(aStream, mSurfaceAlias);
+ WriteElement(aStream, mActualSurface);
+}
+
+template <class S>
+RecordedAddSurfaceAlias::RecordedAddSurfaceAlias(S& aStream)
+ : RecordedEventDerived(ADD_SURFACE_ALIAS) {
+ ReadElement(aStream, mSurfaceAlias);
+ ReadElement(aStream, mActualSurface);
+}
+
+class RecordedRemoveSurfaceAlias final
+ : public RecordedEventDerived<RecordedRemoveSurfaceAlias> {
+ public:
+ explicit RecordedRemoveSurfaceAlias(ReferencePtr aSurfaceAlias)
+ : RecordedEventDerived(REMOVE_SURFACE_ALIAS),
+ mSurfaceAlias(aSurfaceAlias) {}
+
+ template <class S>
+ MOZ_IMPLICIT RecordedRemoveSurfaceAlias(S& aStream);
+
+ bool PlayCanvasEvent(CanvasTranslator* aTranslator) const;
+
+ template <class S>
+ void Record(S& aStream) const;
+
+ std::string GetName() const final { return "RecordedRemoveSurfaceAlias"; }
+
+ private:
+ ReferencePtr mSurfaceAlias;
+};
+
+inline bool RecordedRemoveSurfaceAlias::PlayCanvasEvent(
+ CanvasTranslator* aTranslator) const {
+ aTranslator->RemoveSourceSurface(mSurfaceAlias);
+ return true;
+}
+
+template <class S>
+void RecordedRemoveSurfaceAlias::Record(S& aStream) const {
+ WriteElement(aStream, mSurfaceAlias);
+}
+
+template <class S>
+RecordedRemoveSurfaceAlias::RecordedRemoveSurfaceAlias(S& aStream)
+ : RecordedEventDerived(REMOVE_SURFACE_ALIAS) {
+ ReadElement(aStream, mSurfaceAlias);
+}
+
+class RecordedDeviceChangeAcknowledged final
+ : public RecordedEventDerived<RecordedDeviceChangeAcknowledged> {
+ public:
+ RecordedDeviceChangeAcknowledged()
+ : RecordedEventDerived(DEVICE_CHANGE_ACKNOWLEDGED) {}
+
+ template <class S>
+ MOZ_IMPLICIT RecordedDeviceChangeAcknowledged(S& aStream);
+
+ bool PlayCanvasEvent(CanvasTranslator* aTranslator) const;
+
+ template <class S>
+ void Record(S& aStream) const;
+
+ std::string GetName() const final {
+ return "RecordedDeviceChangeAcknowledged";
+ }
+};
+
+inline bool RecordedDeviceChangeAcknowledged::PlayCanvasEvent(
+ CanvasTranslator* aTranslator) const {
+ aTranslator->DeviceChangeAcknowledged();
+ return true;
+}
+
+template <class S>
+void RecordedDeviceChangeAcknowledged::Record(S& aStream) const {}
+
+template <class S>
+RecordedDeviceChangeAcknowledged::RecordedDeviceChangeAcknowledged(S& aStream)
+ : RecordedEventDerived(DEVICE_CHANGE_ACKNOWLEDGED) {}
+
+class RecordedNextTextureId final
+ : public RecordedEventDerived<RecordedNextTextureId> {
+ public:
+ explicit RecordedNextTextureId(int64_t aNextTextureId)
+ : RecordedEventDerived(NEXT_TEXTURE_ID), mNextTextureId(aNextTextureId) {}
+
+ template <class S>
+ MOZ_IMPLICIT RecordedNextTextureId(S& aStream);
+
+ bool PlayCanvasEvent(CanvasTranslator* aTranslator) const;
+
+ template <class S>
+ void Record(S& aStream) const;
+
+ std::string GetName() const final { return "RecordedNextTextureId"; }
+
+ private:
+ int64_t mNextTextureId;
+};
+
+inline bool RecordedNextTextureId::PlayCanvasEvent(
+ CanvasTranslator* aTranslator) const {
+ aTranslator->SetNextTextureId(mNextTextureId);
+ return true;
+}
+
+template <class S>
+void RecordedNextTextureId::Record(S& aStream) const {
+ WriteElement(aStream, mNextTextureId);
+}
+
+template <class S>
+RecordedNextTextureId::RecordedNextTextureId(S& aStream)
+ : RecordedEventDerived(NEXT_TEXTURE_ID) {
+ ReadElement(aStream, mNextTextureId);
+}
+
+class RecordedTextureDestruction final
+ : public RecordedEventDerived<RecordedTextureDestruction> {
+ public:
+ explicit RecordedTextureDestruction(int64_t aTextureId)
+ : RecordedEventDerived(TEXTURE_DESTRUCTION), mTextureId(aTextureId) {}
+
+ template <class S>
+ MOZ_IMPLICIT RecordedTextureDestruction(S& aStream);
+
+ bool PlayCanvasEvent(CanvasTranslator* aTranslator) const;
+
+ template <class S>
+ void Record(S& aStream) const;
+
+ std::string GetName() const final { return "RecordedTextureDestruction"; }
+
+ private:
+ int64_t mTextureId;
+};
+
+inline bool RecordedTextureDestruction::PlayCanvasEvent(
+ CanvasTranslator* aTranslator) const {
+ aTranslator->RemoveTexture(mTextureId);
+ return true;
+}
+
+template <class S>
+void RecordedTextureDestruction::Record(S& aStream) const {
+ WriteElement(aStream, mTextureId);
+}
+
+template <class S>
+RecordedTextureDestruction::RecordedTextureDestruction(S& aStream)
+ : RecordedEventDerived(TEXTURE_DESTRUCTION) {
+ ReadElement(aStream, mTextureId);
+}
+
+#define FOR_EACH_CANVAS_EVENT(f) \
+ f(CANVAS_BEGIN_TRANSACTION, RecordedCanvasBeginTransaction); \
+ f(CANVAS_END_TRANSACTION, RecordedCanvasEndTransaction); \
+ f(CANVAS_FLUSH, RecordedCanvasFlush); \
+ f(TEXTURE_LOCK, RecordedTextureLock); \
+ f(TEXTURE_UNLOCK, RecordedTextureUnlock); \
+ f(CACHE_DATA_SURFACE, RecordedCacheDataSurface); \
+ f(PREPARE_DATA_FOR_SURFACE, RecordedPrepareDataForSurface); \
+ f(GET_DATA_FOR_SURFACE, RecordedGetDataForSurface); \
+ f(ADD_SURFACE_ALIAS, RecordedAddSurfaceAlias); \
+ f(REMOVE_SURFACE_ALIAS, RecordedRemoveSurfaceAlias); \
+ f(DEVICE_CHANGE_ACKNOWLEDGED, RecordedDeviceChangeAcknowledged); \
+ f(NEXT_TEXTURE_ID, RecordedNextTextureId); \
+ f(TEXTURE_DESTRUCTION, RecordedTextureDestruction);
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_RecordedCanvasEventImpl_h
diff --git a/gfx/layers/RemoteTextureMap.cpp b/gfx/layers/RemoteTextureMap.cpp
new file mode 100644
index 0000000000..cbaefe692d
--- /dev/null
+++ b/gfx/layers/RemoteTextureMap.cpp
@@ -0,0 +1,1028 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/layers/RemoteTextureMap.h"
+
+#include <vector>
+
+#include "CompositableHost.h"
+#include "mozilla/gfx/gfxVars.h"
+#include "mozilla/layers/AsyncImagePipelineManager.h"
+#include "mozilla/layers/BufferTexture.h"
+#include "mozilla/layers/CompositorThread.h"
+#include "mozilla/layers/ImageDataSerializer.h"
+#include "mozilla/layers/RemoteTextureHostWrapper.h"
+#include "mozilla/layers/WebRenderTextureHost.h"
+#include "mozilla/StaticPrefs_webgl.h"
+#include "mozilla/webrender/RenderThread.h"
+#include "SharedSurface.h"
+
+namespace mozilla::layers {
+
+RemoteTextureOwnerClient::RemoteTextureOwnerClient(
+ const base::ProcessId aForPid)
+ : mForPid(aForPid) {}
+
+RemoteTextureOwnerClient::~RemoteTextureOwnerClient() = default;
+
+bool RemoteTextureOwnerClient::IsRegistered(
+ const RemoteTextureOwnerId aOwnerId) {
+ auto it = mOwnerIds.find(aOwnerId);
+ if (it == mOwnerIds.end()) {
+ return false;
+ }
+ return true;
+}
+
+void RemoteTextureOwnerClient::RegisterTextureOwner(
+ const RemoteTextureOwnerId aOwnerId, bool aIsSyncMode) {
+ MOZ_ASSERT(mOwnerIds.find(aOwnerId) == mOwnerIds.end());
+ mOwnerIds.emplace(aOwnerId);
+ RemoteTextureMap::Get()->RegisterTextureOwner(aOwnerId, mForPid, aIsSyncMode);
+}
+
+void RemoteTextureOwnerClient::UnregisterTextureOwner(
+ const RemoteTextureOwnerId aOwnerId) {
+ auto it = mOwnerIds.find(aOwnerId);
+ if (it == mOwnerIds.end()) {
+ return;
+ }
+ mOwnerIds.erase(it);
+ RemoteTextureMap::Get()->UnregisterTextureOwner(aOwnerId, mForPid);
+}
+
+void RemoteTextureOwnerClient::UnregisterAllTextureOwners() {
+ if (!mOwnerIds.empty()) {
+ RemoteTextureMap::Get()->UnregisterTextureOwners(mOwnerIds, mForPid);
+ mOwnerIds.clear();
+ }
+}
+
+void RemoteTextureOwnerClient::PushTexture(
+ const RemoteTextureId aTextureId, const RemoteTextureOwnerId aOwnerId,
+ UniquePtr<TextureData>&& aTextureData,
+ const std::shared_ptr<gl::SharedSurface>& aSharedSurface) {
+ MOZ_ASSERT(IsRegistered(aOwnerId));
+
+ RefPtr<TextureHost> textureHost = RemoteTextureMap::CreateRemoteTexture(
+ aTextureData.get(), TextureFlags::DEFAULT);
+ if (!textureHost) {
+ MOZ_ASSERT_UNREACHABLE("unexpected to be called");
+ return;
+ }
+
+ RemoteTextureMap::Get()->PushTexture(aTextureId, aOwnerId, mForPid,
+ std::move(aTextureData), textureHost,
+ aSharedSurface);
+}
+
+void RemoteTextureOwnerClient::PushDummyTexture(
+ const RemoteTextureId aTextureId, const RemoteTextureOwnerId aOwnerId) {
+ MOZ_ASSERT(IsRegistered(aOwnerId));
+
+ auto flags = TextureFlags::DEALLOCATE_CLIENT | TextureFlags::REMOTE_TEXTURE |
+ TextureFlags::DUMMY_TEXTURE;
+ auto* rawData = BufferTextureData::Create(
+ gfx::IntSize(1, 1), gfx::SurfaceFormat::B8G8R8A8, gfx::BackendType::SKIA,
+ LayersBackend::LAYERS_WR, flags, ALLOC_DEFAULT, nullptr);
+ if (!rawData) {
+ MOZ_ASSERT_UNREACHABLE("unexpected to be called");
+ return;
+ }
+
+ auto textureData = UniquePtr<TextureData>(rawData);
+
+ RefPtr<TextureHost> textureHost = RemoteTextureMap::CreateRemoteTexture(
+ textureData.get(), TextureFlags::DUMMY_TEXTURE);
+ if (!textureHost) {
+ MOZ_ASSERT_UNREACHABLE("unexpected to be called");
+ return;
+ }
+
+ RemoteTextureMap::Get()->PushTexture(aTextureId, aOwnerId, mForPid,
+ std::move(textureData), textureHost,
+ /* aSharedSurface */ nullptr);
+}
+
+void RemoteTextureOwnerClient::GetLatestBufferSnapshot(
+ const RemoteTextureOwnerId aOwnerId, const mozilla::ipc::Shmem& aDestShmem,
+ const gfx::IntSize& aSize) {
+ MOZ_ASSERT(IsRegistered(aOwnerId));
+ RemoteTextureMap::Get()->GetLatestBufferSnapshot(aOwnerId, mForPid,
+ aDestShmem, aSize);
+}
+
+UniquePtr<TextureData>
+RemoteTextureOwnerClient::CreateOrRecycleBufferTextureData(
+ const RemoteTextureOwnerId aOwnerId, gfx::IntSize aSize,
+ gfx::SurfaceFormat aFormat) {
+ auto texture = RemoteTextureMap::Get()->GetRecycledBufferTextureData(
+ aOwnerId, mForPid, aSize, aFormat);
+ if (texture) {
+ return texture;
+ }
+
+ auto flags = TextureFlags::DEALLOCATE_CLIENT | TextureFlags::REMOTE_TEXTURE;
+ auto* data = BufferTextureData::Create(aSize, aFormat, gfx::BackendType::SKIA,
+ LayersBackend::LAYERS_WR, flags,
+ ALLOC_DEFAULT, nullptr);
+ return UniquePtr<TextureData>(data);
+}
+
+std::shared_ptr<gl::SharedSurface>
+RemoteTextureOwnerClient::GetRecycledSharedSurface(
+ const RemoteTextureOwnerId aOwnerId) {
+ return RemoteTextureMap::Get()->RemoteTextureMap::GetRecycledSharedSurface(
+ aOwnerId, mForPid);
+}
+
+StaticAutoPtr<RemoteTextureMap> RemoteTextureMap::sInstance;
+
+/* static */
+void RemoteTextureMap::Init() {
+ MOZ_ASSERT(!sInstance);
+ sInstance = new RemoteTextureMap();
+}
+
+/* static */
+void RemoteTextureMap::Shutdown() {
+ if (sInstance) {
+ sInstance = nullptr;
+ }
+}
+
+RemoteTextureMap::RemoteTextureMap() : mMonitor("D3D11TextureMap::mMonitor") {}
+
+RemoteTextureMap::~RemoteTextureMap() = default;
+
+void RemoteTextureMap::PushTexture(
+ const RemoteTextureId aTextureId, const RemoteTextureOwnerId aOwnerId,
+ const base::ProcessId aForPid, UniquePtr<TextureData>&& aTextureData,
+ RefPtr<TextureHost>& aTextureHost,
+ const std::shared_ptr<gl::SharedSurface>& aSharedSurface) {
+ MOZ_RELEASE_ASSERT(aTextureHost);
+
+ std::vector<std::function<void(const RemoteTextureInfo&)>>
+ renderingReadyCallbacks; // Call outside the monitor
+ {
+ MonitorAutoLock lock(mMonitor);
+
+ auto* owner = GetTextureOwner(lock, aOwnerId, aForPid);
+ if (!owner) {
+ MOZ_ASSERT_UNREACHABLE("unexpected to be called");
+ return;
+ }
+
+ const auto key = std::pair(aForPid, aOwnerId);
+ auto it = mRemoteTexturePushListeners.find(key);
+ // Notify a new texture if callback is requested
+ if (it != mRemoteTexturePushListeners.end()) {
+ RefPtr<CompositableHost> compositableHost = it->second;
+ RefPtr<Runnable> runnable = NS_NewRunnableFunction(
+ "RemoteTextureMap::PushTexture::Runnable",
+ [compositableHost, aTextureId, aOwnerId, aForPid]() {
+ compositableHost->NotifyPushTexture(aTextureId, aOwnerId, aForPid);
+ });
+ CompositorThread()->Dispatch(runnable.forget());
+ }
+
+ auto textureData = MakeUnique<TextureDataHolder>(
+ aTextureId, aTextureHost, std::move(aTextureData), aSharedSurface);
+
+ MOZ_ASSERT(owner->mLatestTextureId < aTextureId);
+
+ owner->mWaitingTextureDataHolders.push_back(std::move(textureData));
+
+ if (!owner->mIsSyncMode) {
+ renderingReadyCallbacks =
+ GetRenderingReadyCallbacks(lock, owner, aTextureId);
+ // Update mAsyncRemoteTextureHost for async mode.
+ // This happens when PushTexture() with RemoteTextureId is called after
+ // GetRemoteTextureForDisplayList() with the RemoteTextureId.
+ const auto key = std::pair(aForPid, aTextureId);
+ auto it = mRemoteTextureHostWrapperHolders.find(key);
+ if (it != mRemoteTextureHostWrapperHolders.end()) {
+ MOZ_ASSERT(!it->second->mAsyncRemoteTextureHost);
+ it->second->mAsyncRemoteTextureHost = aTextureHost;
+ }
+ }
+
+ mMonitor.Notify();
+
+ // Drop obsoleted remote textures.
+ while (!owner->mUsingTextureDataHolders.empty()) {
+ auto& front = owner->mUsingTextureDataHolders.front();
+ // When compositable ref of TextureHost becomes 0, the TextureHost is not
+ // used by WebRender anymore.
+ if (front->mTextureHost &&
+ front->mTextureHost->NumCompositableRefs() == 0) {
+ // Recycle gl::SharedSurface
+ if (front->mSharedSurface) {
+ owner->mRecycledSharedSurfaces.push(front->mSharedSurface);
+ front->mSharedSurface = nullptr;
+ }
+ // Recycle BufferTextureData
+ if (!(front->mTextureHost->GetFlags() & TextureFlags::DUMMY_TEXTURE) &&
+ (front->mTextureData &&
+ front->mTextureData->AsBufferTextureData())) {
+ owner->mRecycledTextures.push(std::move(front->mTextureData));
+ }
+ owner->mUsingTextureDataHolders.pop_front();
+ } else if (front->mTextureHost &&
+ front->mTextureHost->NumCompositableRefs() >= 0) {
+ // Remote texture is still in use by WebRender.
+ break;
+ } else {
+ MOZ_ASSERT_UNREACHABLE("unexpected to be called");
+ owner->mUsingTextureDataHolders.pop_front();
+ }
+ }
+ }
+
+ const auto info = RemoteTextureInfo(aTextureId, aOwnerId, aForPid);
+ for (auto& callback : renderingReadyCallbacks) {
+ callback(info);
+ }
+}
+
+void RemoteTextureMap::GetLatestBufferSnapshot(
+ const RemoteTextureOwnerId aOwnerId, const base::ProcessId aForPid,
+ const mozilla::ipc::Shmem& aDestShmem, const gfx::IntSize& aSize) {
+ // The compositable ref of remote texture should be updated in mMonitor lock.
+ CompositableTextureHostRef textureHostRef;
+ RefPtr<TextureHost> releasingTexture; // Release outside the monitor
+ {
+ MonitorAutoLock lock(mMonitor);
+
+ auto* owner = GetTextureOwner(lock, aOwnerId, aForPid);
+ if (!owner) {
+ MOZ_ASSERT_UNREACHABLE("unexpected to be called");
+ return;
+ }
+
+ // Get latest TextureHost of remote Texture.
+ if (owner->mWaitingTextureDataHolders.empty() &&
+ !owner->mLatestTextureHost) {
+ return;
+ }
+ TextureHost* textureHost =
+ !owner->mWaitingTextureDataHolders.empty()
+ ? owner->mWaitingTextureDataHolders.back()->mTextureHost
+ : owner->mLatestTextureHost;
+ if (!textureHost->AsBufferTextureHost()) {
+ // Only BufferTextureHost is supported for now.
+ MOZ_ASSERT_UNREACHABLE("unexpected to be called");
+ return;
+ }
+ if (textureHost->GetSize() != aSize) {
+ MOZ_ASSERT_UNREACHABLE("unexpected to be called");
+ return;
+ }
+ if (textureHost->GetFormat() != gfx::SurfaceFormat::R8G8B8A8 &&
+ textureHost->GetFormat() != gfx::SurfaceFormat::B8G8R8A8) {
+ MOZ_ASSERT_UNREACHABLE("unexpected to be called");
+ return;
+ }
+ // Increment compositable ref to prevent that TextureHost is removed during
+ // memcpy.
+ textureHostRef = textureHost;
+ }
+
+ if (!textureHostRef) {
+ return;
+ }
+
+ auto* bufferTextureHost = textureHostRef->AsBufferTextureHost();
+ if (bufferTextureHost) {
+ uint32_t stride = ImageDataSerializer::ComputeRGBStride(
+ bufferTextureHost->GetFormat(), aSize.width);
+ uint32_t bufferSize = stride * aSize.height;
+ uint8_t* dst = aDestShmem.get<uint8_t>();
+ uint8_t* src = bufferTextureHost->GetBuffer();
+
+ MOZ_ASSERT(bufferSize <= aDestShmem.Size<uint8_t>());
+ memcpy(dst, src, bufferSize);
+ }
+
+ {
+ MonitorAutoLock lock(mMonitor);
+ // Release compositable ref in mMonitor lock, but release RefPtr outside the
+ // monitor
+ releasingTexture = textureHostRef;
+ textureHostRef = nullptr;
+ }
+}
+
+void RemoteTextureMap::RegisterTextureOwner(const RemoteTextureOwnerId aOwnerId,
+ const base::ProcessId aForPid,
+ bool aIsSyncMode) {
+ MonitorAutoLock lock(mMonitor);
+
+ const auto key = std::pair(aForPid, aOwnerId);
+ auto it = mTextureOwners.find(key);
+ if (it != mTextureOwners.end()) {
+ MOZ_ASSERT_UNREACHABLE("unexpected to be called");
+ return;
+ }
+ auto owner = MakeUnique<TextureOwner>();
+ owner->mIsSyncMode = aIsSyncMode;
+
+ mTextureOwners.emplace(key, std::move(owner));
+}
+
+void RemoteTextureMap::KeepTextureDataAliveForTextureHostIfNecessary(
+ const MonitorAutoLock& aProofOfLock,
+ std::deque<UniquePtr<TextureDataHolder>>& aHolders) {
+ for (auto& holder : aHolders) {
+ // If remote texture of TextureHost still exist, keep
+ // gl::SharedSurface/TextureData alive while the TextureHost is alive.
+ if (holder->mTextureHost &&
+ holder->mTextureHost->NumCompositableRefs() >= 0) {
+ RefPtr<nsISerialEventTarget> eventTarget =
+ MessageLoop::current()->SerialEventTarget();
+ RefPtr<Runnable> runnable = NS_NewRunnableFunction(
+ "RemoteTextureMap::UnregisterTextureOwner::Runnable",
+ [data = std::move(holder->mTextureData),
+ surface = std::move(holder->mSharedSurface)]() {});
+
+ auto destroyedCallback = [eventTarget = std::move(eventTarget),
+ runnable = std::move(runnable)]() mutable {
+ eventTarget->Dispatch(runnable.forget());
+ };
+
+ holder->mTextureHost->SetDestroyedCallback(destroyedCallback);
+ }
+ }
+}
+
+void RemoteTextureMap::UnregisterTextureOwner(
+ const RemoteTextureOwnerId aOwnerId, const base::ProcessId aForPid) {
+ UniquePtr<TextureOwner> releasingOwner; // Release outside the monitor
+ std::vector<RefPtr<TextureHost>>
+ releasingTextures; // Release outside the monitor
+ std::vector<std::function<void(const RemoteTextureInfo&)>>
+ renderingReadyCallbacks; // Call outside the monitor
+ {
+ MonitorAutoLock lock(mMonitor);
+
+ const auto key = std::pair(aForPid, aOwnerId);
+ auto it = mTextureOwners.find(key);
+ if (it == mTextureOwners.end()) {
+ MOZ_ASSERT_UNREACHABLE("unexpected to be called");
+ return;
+ }
+
+ if (it->second->mLatestTextureHost) {
+ // Release CompositableRef in mMonitor
+ releasingTextures.emplace_back(it->second->mLatestTextureHost);
+ it->second->mLatestTextureHost = nullptr;
+ }
+
+ if (it->second->mLatestRenderedTextureHost) {
+ // Release CompositableRef in mMonitor
+ releasingTextures.emplace_back(it->second->mLatestRenderedTextureHost);
+ it->second->mLatestRenderedTextureHost = nullptr;
+ }
+
+ renderingReadyCallbacks =
+ GetAllRenderingReadyCallbacks(lock, it->second.get());
+
+ KeepTextureDataAliveForTextureHostIfNecessary(
+ lock, it->second->mWaitingTextureDataHolders);
+
+ KeepTextureDataAliveForTextureHostIfNecessary(
+ lock, it->second->mUsingTextureDataHolders);
+
+ releasingOwner = std::move(it->second);
+ mTextureOwners.erase(it);
+
+ mMonitor.Notify();
+ }
+
+ const auto info =
+ RemoteTextureInfo(RemoteTextureId{0}, RemoteTextureOwnerId{0}, 0);
+ for (auto& callback : renderingReadyCallbacks) {
+ callback(info);
+ }
+}
+
+void RemoteTextureMap::UnregisterTextureOwners(
+ const std::unordered_set<RemoteTextureOwnerId,
+ RemoteTextureOwnerId::HashFn>& aOwnerIds,
+ const base::ProcessId aForPid) {
+ std::vector<UniquePtr<TextureOwner>>
+ releasingOwners; // Release outside the monitor
+ std::vector<RefPtr<TextureHost>>
+ releasingTextures; // Release outside the monitor
+ std::vector<std::function<void(const RemoteTextureInfo&)>>
+ renderingReadyCallbacks; // Call outside the monitor
+ {
+ MonitorAutoLock lock(mMonitor);
+
+ for (auto id : aOwnerIds) {
+ const auto key = std::pair(aForPid, id);
+ auto it = mTextureOwners.find(key);
+ if (it == mTextureOwners.end()) {
+ MOZ_ASSERT_UNREACHABLE("unexpected to be called");
+ continue;
+ }
+
+ if (it->second->mLatestTextureHost) {
+ // Release CompositableRef in mMonitor
+ releasingTextures.emplace_back(it->second->mLatestTextureHost);
+ it->second->mLatestTextureHost = nullptr;
+ }
+
+ if (it->second->mLatestRenderedTextureHost) {
+ // Release CompositableRef in mMonitor
+ releasingTextures.emplace_back(it->second->mLatestRenderedTextureHost);
+ it->second->mLatestRenderedTextureHost = nullptr;
+ }
+
+ renderingReadyCallbacks =
+ GetAllRenderingReadyCallbacks(lock, it->second.get());
+
+ KeepTextureDataAliveForTextureHostIfNecessary(
+ lock, it->second->mWaitingTextureDataHolders);
+
+ KeepTextureDataAliveForTextureHostIfNecessary(
+ lock, it->second->mUsingTextureDataHolders);
+
+ releasingOwners.push_back(std::move(it->second));
+ mTextureOwners.erase(it);
+ }
+
+ mMonitor.Notify();
+ }
+
+ const auto info =
+ RemoteTextureInfo(RemoteTextureId{0}, RemoteTextureOwnerId{0}, 0);
+ for (auto& callback : renderingReadyCallbacks) {
+ callback(info);
+ }
+}
+
+/* static */
+RefPtr<TextureHost> RemoteTextureMap::CreateRemoteTexture(
+ TextureData* aTextureData, TextureFlags aTextureFlags) {
+ SurfaceDescriptor desc;
+ DebugOnly<bool> ret = aTextureData->Serialize(desc);
+ MOZ_ASSERT(ret);
+ TextureFlags flags = aTextureFlags | TextureFlags::REMOTE_TEXTURE |
+ TextureFlags::DEALLOCATE_CLIENT;
+
+ Maybe<wr::ExternalImageId> externalImageId = Nothing();
+ RefPtr<TextureHost> textureHost =
+ TextureHost::Create(desc, null_t(), nullptr, LayersBackend::LAYERS_WR,
+ flags, externalImageId);
+ MOZ_ASSERT(textureHost);
+ if (!textureHost) {
+ gfxCriticalNoteOnce << "Failed to create remote texture";
+ return nullptr;
+ }
+
+ textureHost->EnsureRenderTexture(Nothing());
+
+ return textureHost;
+}
+
+void RemoteTextureMap::UpdateTexture(const MonitorAutoLock& aProofOfLock,
+ RemoteTextureMap::TextureOwner* aOwner,
+ const RemoteTextureId aTextureId) {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+ MOZ_ASSERT(aOwner);
+ MOZ_ASSERT(aTextureId >= aOwner->mLatestTextureId);
+
+ if (aTextureId == aOwner->mLatestTextureId) {
+ // No need to update texture.
+ return;
+ }
+
+ // Move remote textures to mUsingTextureDataHolders.
+ while (!aOwner->mWaitingTextureDataHolders.empty()) {
+ auto& front = aOwner->mWaitingTextureDataHolders.front();
+ if (aTextureId < front->mTextureId) {
+ break;
+ }
+ MOZ_RELEASE_ASSERT(front->mTextureHost);
+ aOwner->mLatestTextureHost = front->mTextureHost;
+ aOwner->mLatestTextureId = front->mTextureId;
+
+ UniquePtr<TextureDataHolder> holder = std::move(front);
+ aOwner->mWaitingTextureDataHolders.pop_front();
+ aOwner->mUsingTextureDataHolders.push_back(std::move(holder));
+ }
+}
+
+std::vector<std::function<void(const RemoteTextureInfo&)>>
+RemoteTextureMap::GetRenderingReadyCallbacks(
+ const MonitorAutoLock& aProofOfLock, RemoteTextureMap::TextureOwner* aOwner,
+ const RemoteTextureId aTextureId) {
+ MOZ_ASSERT(aOwner);
+
+ std::vector<std::function<void(const RemoteTextureInfo&)>> functions;
+
+ while (!aOwner->mRenderingReadyCallbackHolders.empty()) {
+ auto& front = aOwner->mRenderingReadyCallbackHolders.front();
+ if (aTextureId < front->mTextureId) {
+ break;
+ }
+ if (front->mCallback) {
+ functions.push_back(std::move(front->mCallback));
+ }
+ aOwner->mRenderingReadyCallbackHolders.pop_front();
+ }
+
+ return functions;
+}
+
+std::vector<std::function<void(const RemoteTextureInfo&)>>
+RemoteTextureMap::GetAllRenderingReadyCallbacks(
+ const MonitorAutoLock& aProofOfLock,
+ RemoteTextureMap::TextureOwner* aOwner) {
+ auto functions =
+ GetRenderingReadyCallbacks(aProofOfLock, aOwner, RemoteTextureId::Max());
+ MOZ_ASSERT(aOwner->mRenderingReadyCallbackHolders.empty());
+
+ return functions;
+}
+
+bool RemoteTextureMap::GetRemoteTextureForDisplayList(
+ RemoteTextureHostWrapper* aTextureHostWrapper,
+ std::function<void(const RemoteTextureInfo&)>&& aReadyCallback) {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+ MOZ_ASSERT(aTextureHostWrapper);
+
+ if (aTextureHostWrapper->IsReadyForRendering()) {
+ return false;
+ }
+
+ const auto& textureId = aTextureHostWrapper->mTextureId;
+ const auto& ownerId = aTextureHostWrapper->mOwnerId;
+ const auto& forPid = aTextureHostWrapper->mForPid;
+ const auto& size = aTextureHostWrapper->mSize;
+
+ RefPtr<TextureHost> textureHost;
+ {
+ MonitorAutoLock lock(mMonitor);
+
+ auto* owner = GetTextureOwner(lock, ownerId, forPid);
+ if (!owner) {
+ return false;
+ }
+
+ UpdateTexture(lock, owner, textureId);
+
+ if (owner->mLatestTextureHost &&
+ (owner->mLatestTextureHost->GetFlags() & TextureFlags::DUMMY_TEXTURE)) {
+ // Remote texture allocation was failed.
+ return false;
+ }
+
+ bool syncMode = owner->mIsSyncMode || bool(aReadyCallback);
+
+ if (syncMode) {
+ // remote texture sync ipc
+ if (textureId == owner->mLatestTextureId) {
+ MOZ_ASSERT(owner->mLatestTextureHost);
+ MOZ_ASSERT(owner->mLatestTextureHost->GetSize() == size);
+ if (owner->mLatestTextureHost->GetSize() != size) {
+ gfxCriticalNoteOnce << "unexpected remote texture size: "
+ << owner->mLatestTextureHost->GetSize()
+ << " expected: " << size;
+ }
+ textureHost = owner->mLatestTextureHost;
+ } else {
+ if (aReadyCallback) {
+ auto callbackHolder = MakeUnique<RenderingReadyCallbackHolder>(
+ textureId, std::move(aReadyCallback));
+ owner->mRenderingReadyCallbackHolders.push_back(
+ std::move(callbackHolder));
+ return true;
+ } else {
+ MOZ_ASSERT_UNREACHABLE("unexpected to be called");
+ }
+ }
+ } else {
+ // remote texture async ipc
+ if (owner->mLatestTextureHost) {
+ if (owner->mLatestTextureHost->GetSize() == size) {
+ textureHost = owner->mLatestTextureHost;
+ } else {
+ gfxCriticalNoteOnce << "unexpected remote texture size: "
+ << owner->mLatestTextureHost->GetSize()
+ << " expected: " << size;
+ MOZ_ASSERT_UNREACHABLE("unexpected to be called");
+ }
+ } else {
+ gfxCriticalNoteOnce << "remote texture does not exist";
+ MOZ_ASSERT_UNREACHABLE("unexpected to be called");
+ }
+
+ // Update mAsyncRemoteTextureHost for async mode
+ if (textureId == owner->mLatestTextureId) {
+ const auto key = std::pair(forPid, textureId);
+ auto it = mRemoteTextureHostWrapperHolders.find(key);
+ if (it != mRemoteTextureHostWrapperHolders.end() &&
+ !it->second->mAsyncRemoteTextureHost) {
+ it->second->mAsyncRemoteTextureHost = owner->mLatestTextureHost;
+ } else {
+ MOZ_ASSERT(it->second->mAsyncRemoteTextureHost ==
+ owner->mLatestTextureHost);
+ }
+ }
+ }
+
+ if (textureHost) {
+ aTextureHostWrapper->SetRemoteTextureHostForDisplayList(lock, textureHost,
+ syncMode);
+ aTextureHostWrapper->ApplyTextureFlagsToRemoteTexture();
+ }
+ }
+
+ return false;
+}
+
+wr::MaybeExternalImageId RemoteTextureMap::GetExternalImageIdOfRemoteTexture(
+ const RemoteTextureId aTextureId, const RemoteTextureOwnerId aOwnerId,
+ const base::ProcessId aForPid) {
+ MOZ_ASSERT(wr::RenderThread::IsInRenderThread());
+ MonitorAutoLock lock(mMonitor);
+
+ const auto key = std::pair(aForPid, aTextureId);
+ auto it = mRemoteTextureHostWrapperHolders.find(key);
+ if (it == mRemoteTextureHostWrapperHolders.end()) {
+ MOZ_ASSERT_UNREACHABLE("unexpected to be called");
+ return Nothing();
+ }
+
+ TextureHost* remoteTexture = it->second->mAsyncRemoteTextureHost;
+
+ auto* owner = GetTextureOwner(lock, aOwnerId, aForPid);
+ if (!owner) {
+ if (!remoteTexture) {
+ // This could happen with IPC abnormal shutdown
+ return Nothing();
+ }
+ return remoteTexture->GetMaybeExternalImageId();
+ }
+
+ if (remoteTexture &&
+ remoteTexture->GetFlags() & TextureFlags::DUMMY_TEXTURE) {
+ // Remote texture allocation was failed.
+ return Nothing();
+ }
+ MOZ_ASSERT(!(remoteTexture &&
+ remoteTexture->GetFlags() & TextureFlags::DUMMY_TEXTURE));
+
+ MOZ_ASSERT(owner);
+
+ if (!remoteTexture) {
+ // Use mLatestRenderedTextureHost for rendering. Remote texture of
+ // aTextureId does not exist.
+ remoteTexture = owner->mLatestRenderedTextureHost;
+ if (!it->second->mReadyCheckSuppressed) {
+ MOZ_ASSERT_UNREACHABLE("unexpected to be called");
+ gfxCriticalNoteOnce << "remote texture for rendering does not exist id:"
+ << uint64_t(aTextureId);
+ }
+ } else {
+ // Update mLatestRenderedTextureHost
+ owner->mLatestRenderedTextureHost = remoteTexture;
+ }
+
+ if (!remoteTexture) {
+ return Nothing();
+ }
+
+ return remoteTexture->GetMaybeExternalImageId();
+}
+
+void RemoteTextureMap::ReleaseRemoteTextureHostForDisplayList(
+ RemoteTextureHostWrapper* aTextureHostWrapper) {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+ MOZ_ASSERT(aTextureHostWrapper);
+
+ RefPtr<TextureHost> releasingTexture; // Release outside the mutex
+ {
+ MonitorAutoLock lock(mMonitor);
+ releasingTexture =
+ aTextureHostWrapper->GetRemoteTextureHostForDisplayList(lock);
+ aTextureHostWrapper->ClearRemoteTextureHostForDisplayList(lock);
+ }
+}
+
+RefPtr<TextureHost> RemoteTextureMap::GetOrCreateRemoteTextureHostWrapper(
+ const RemoteTextureId aTextureId, const RemoteTextureOwnerId aOwnerId,
+ const base::ProcessId aForPid, const gfx::IntSize aSize,
+ const TextureFlags aFlags) {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+ MonitorAutoLock lock(mMonitor);
+
+ const auto key = std::pair(aForPid, aTextureId);
+ auto it = mRemoteTextureHostWrapperHolders.find(key);
+ if (it != mRemoteTextureHostWrapperHolders.end()) {
+ return it->second->mRemoteTextureHostWrapper;
+ }
+
+ auto wrapper = RemoteTextureHostWrapper::Create(aTextureId, aOwnerId, aForPid,
+ aSize, aFlags);
+ auto wrapperHolder = MakeUnique<RemoteTextureHostWrapperHolder>(wrapper);
+
+ mRemoteTextureHostWrapperHolders.emplace(key, std::move(wrapperHolder));
+
+ return wrapper;
+}
+
+void RemoteTextureMap::UnregisterRemoteTextureHostWrapper(
+ const RemoteTextureId aTextureId, const RemoteTextureOwnerId aOwnerId,
+ const base::ProcessId aForPid) {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+
+ std::vector<RefPtr<TextureHost>>
+ releasingTextures; // Release outside the monitor
+ {
+ MonitorAutoLock lock(mMonitor);
+
+ const auto key = std::pair(aForPid, aTextureId);
+ auto it = mRemoteTextureHostWrapperHolders.find(key);
+ if (it == mRemoteTextureHostWrapperHolders.end()) {
+ MOZ_ASSERT_UNREACHABLE("unexpected to be called");
+ return;
+ }
+ releasingTextures.emplace_back(it->second->mRemoteTextureHostWrapper);
+ if (it->second->mAsyncRemoteTextureHost) {
+ releasingTextures.emplace_back(it->second->mAsyncRemoteTextureHost);
+ }
+
+ mRemoteTextureHostWrapperHolders.erase(it);
+ mMonitor.Notify();
+ }
+}
+
+void RemoteTextureMap::RegisterRemoteTexturePushListener(
+ const RemoteTextureOwnerId aOwnerId, const base::ProcessId aForPid,
+ CompositableHost* aListener) {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+
+ RefPtr<CompositableHost>
+ releasingCompositableHost; // Release outside the monitor
+ {
+ MonitorAutoLock lock(mMonitor);
+
+ const auto key = std::pair(aForPid, aOwnerId);
+ auto it = mRemoteTexturePushListeners.find(key);
+ // Remove obsoleted CompositableHost.
+ if (it != mRemoteTexturePushListeners.end()) {
+ releasingCompositableHost = std::move(it->second);
+ mRemoteTexturePushListeners.erase(it);
+ }
+ mRemoteTexturePushListeners.emplace(key, aListener);
+
+ auto* owner = GetTextureOwner(lock, aOwnerId, aForPid);
+ if (!owner) {
+ return;
+ }
+ if (owner->mWaitingTextureDataHolders.empty() &&
+ !owner->mLatestTextureHost) {
+ return;
+ }
+
+ // Get latest RemoteTextureId.
+ auto textureId = !owner->mWaitingTextureDataHolders.empty()
+ ? owner->mWaitingTextureDataHolders.back()->mTextureId
+ : owner->mLatestTextureId;
+
+ // Notify the RemoteTextureId to callback
+ RefPtr<CompositableHost> compositableHost = aListener;
+ RefPtr<Runnable> runnable = NS_NewRunnableFunction(
+ "RemoteTextureMap::RegisterRemoteTexturePushListener::Runnable",
+ [compositableHost, textureId, aOwnerId, aForPid]() {
+ compositableHost->NotifyPushTexture(textureId, aOwnerId, aForPid);
+ });
+ CompositorThread()->Dispatch(runnable.forget());
+ }
+}
+
+void RemoteTextureMap::UnregisterRemoteTexturePushListener(
+ const RemoteTextureOwnerId aOwnerId, const base::ProcessId aForPid,
+ CompositableHost* aListener) {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+
+ RefPtr<CompositableHost>
+ releasingCompositableHost; // Release outside the monitor
+ {
+ MonitorAutoLock lock(mMonitor);
+
+ const auto key = std::pair(aForPid, aOwnerId);
+ auto it = mRemoteTexturePushListeners.find(key);
+ if (it == mRemoteTexturePushListeners.end()) {
+ return;
+ }
+ if (aListener != it->second) {
+ // aListener was alredy obsoleted.
+ return;
+ }
+ releasingCompositableHost = std::move(it->second);
+ mRemoteTexturePushListeners.erase(it);
+ }
+}
+
+bool RemoteTextureMap::CheckRemoteTextureReady(
+ const RemoteTextureInfo& aInfo,
+ std::function<void(const RemoteTextureInfo&)>&& aCallback) {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+
+ MonitorAutoLock lock(mMonitor);
+
+ auto* owner = GetTextureOwner(lock, aInfo.mOwnerId, aInfo.mForPid);
+ if (!owner) {
+ // Owner is already removed.
+ return true;
+ }
+
+ const auto key = std::pair(aInfo.mForPid, aInfo.mTextureId);
+ auto it = mRemoteTextureHostWrapperHolders.find(key);
+ if (it == mRemoteTextureHostWrapperHolders.end()) {
+ MOZ_ASSERT_UNREACHABLE("unexpected to be called");
+ gfxCriticalNoteOnce << "Remote texture does not exist id:"
+ << uint64_t(aInfo.mTextureId);
+ return true;
+ }
+
+ if (it->second->mAsyncRemoteTextureHost) {
+ return true;
+ }
+ MOZ_ASSERT(!it->second->mAsyncRemoteTextureHost);
+
+ // Check if RemoteTextureId is as expected.
+ if (!owner->mRenderingReadyCallbackHolders.empty()) {
+ auto& front = owner->mRenderingReadyCallbackHolders.front();
+ MOZ_RELEASE_ASSERT(aInfo.mTextureId >= front->mTextureId);
+ }
+
+ auto callbackHolder = MakeUnique<RenderingReadyCallbackHolder>(
+ aInfo.mTextureId, std::move(aCallback));
+ owner->mRenderingReadyCallbackHolders.push_back(std::move(callbackHolder));
+
+ return false;
+}
+
+bool RemoteTextureMap::WaitRemoteTextureReady(const RemoteTextureInfo& aInfo) {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+
+ MonitorAutoLock lock(mMonitor);
+
+ auto* owner = GetTextureOwner(lock, aInfo.mOwnerId, aInfo.mForPid);
+ if (!owner) {
+ // Owner is already removed.
+ return false;
+ }
+
+ const auto key = std::pair(aInfo.mForPid, aInfo.mTextureId);
+ auto it = mRemoteTextureHostWrapperHolders.find(key);
+ if (it == mRemoteTextureHostWrapperHolders.end()) {
+ MOZ_ASSERT_UNREACHABLE("unexpected to be called");
+ gfxCriticalNoteOnce << "Remote texture does not exist id:"
+ << uint64_t(aInfo.mTextureId);
+ return false;
+ }
+
+ const TimeDuration timeout = TimeDuration::FromMilliseconds(1000);
+ TextureHost* remoteTexture = it->second->mAsyncRemoteTextureHost;
+
+ while (!remoteTexture) {
+ CVStatus status = mMonitor.Wait(timeout);
+ if (status == CVStatus::Timeout) {
+ MOZ_ASSERT_UNREACHABLE("unexpected to be called");
+ gfxCriticalNoteOnce << "Remote texture wait time out id:"
+ << uint64_t(aInfo.mTextureId);
+ return false;
+ }
+
+ auto it = mRemoteTextureHostWrapperHolders.find(key);
+ if (it == mRemoteTextureHostWrapperHolders.end()) {
+ MOZ_ASSERT_UNREACHABLE("unexpected to be called");
+ return false;
+ }
+
+ remoteTexture = it->second->mAsyncRemoteTextureHost;
+ if (!remoteTexture) {
+ auto* owner = GetTextureOwner(lock, aInfo.mOwnerId, aInfo.mForPid);
+ // When owner is alreay unregistered, remote texture will not be pushed.
+ if (!owner) {
+ // This could happen with IPC abnormal shutdown
+ return false;
+ }
+ }
+ }
+
+ return true;
+}
+
+void RemoteTextureMap::SuppressRemoteTextureReadyCheck(
+ const RemoteTextureId aTextureId, const base::ProcessId aForPid) {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+ MonitorAutoLock lock(mMonitor);
+
+ const auto key = std::pair(aForPid, aTextureId);
+ auto it = mRemoteTextureHostWrapperHolders.find(key);
+ if (it == mRemoteTextureHostWrapperHolders.end()) {
+ MOZ_ASSERT_UNREACHABLE("unexpected to be called");
+ return;
+ }
+ it->second->mReadyCheckSuppressed = true;
+}
+
+UniquePtr<TextureData> RemoteTextureMap::GetRecycledBufferTextureData(
+ const RemoteTextureOwnerId aOwnerId, const base::ProcessId aForPid,
+ gfx::IntSize aSize, gfx::SurfaceFormat aFormat) {
+ std::stack<UniquePtr<TextureData>>
+ releasingTextures; // Release outside the monitor
+ UniquePtr<TextureData> texture;
+ {
+ MonitorAutoLock lock(mMonitor);
+
+ auto* owner = GetTextureOwner(lock, aOwnerId, aForPid);
+ if (!owner) {
+ return nullptr;
+ }
+
+ if (owner->mRecycledTextures.empty()) {
+ return nullptr;
+ }
+
+ if (!owner->mRecycledTextures.empty()) {
+ auto& top = owner->mRecycledTextures.top();
+ auto* bufferTexture = top->AsBufferTextureData();
+
+ if (bufferTexture && bufferTexture->GetSize() == aSize &&
+ bufferTexture->GetFormat() == aFormat) {
+ texture = std::move(top);
+ owner->mRecycledTextures.pop();
+ } else {
+ // If size or format are different, release all textures.
+ owner->mRecycledTextures.swap(releasingTextures);
+ }
+ }
+ }
+ return texture;
+}
+
+std::shared_ptr<gl::SharedSurface> RemoteTextureMap::GetRecycledSharedSurface(
+ const RemoteTextureOwnerId aOwnerId, const base::ProcessId aForPid) {
+ std::shared_ptr<gl::SharedSurface> sharedSurface;
+ {
+ MonitorAutoLock lock(mMonitor);
+
+ auto* owner = GetTextureOwner(lock, aOwnerId, aForPid);
+ if (!owner) {
+ return nullptr;
+ }
+
+ if (owner->mRecycledSharedSurfaces.empty()) {
+ return nullptr;
+ }
+
+ if (!owner->mRecycledSharedSurfaces.empty()) {
+ sharedSurface = owner->mRecycledSharedSurfaces.front();
+ owner->mRecycledSharedSurfaces.pop();
+ }
+ }
+ return sharedSurface;
+}
+
+RemoteTextureMap::TextureOwner* RemoteTextureMap::GetTextureOwner(
+ const MonitorAutoLock& aProofOfLock, const RemoteTextureOwnerId aOwnerId,
+ const base::ProcessId aForPid) {
+ const auto key = std::pair(aForPid, aOwnerId);
+ auto it = mTextureOwners.find(key);
+ if (it == mTextureOwners.end()) {
+ return nullptr;
+ }
+ return it->second.get();
+}
+
+RemoteTextureMap::TextureDataHolder::TextureDataHolder(
+ const RemoteTextureId aTextureId, RefPtr<TextureHost> aTextureHost,
+ UniquePtr<TextureData>&& aTextureData,
+ const std::shared_ptr<gl::SharedSurface>& aSharedSurface)
+ : mTextureId(aTextureId),
+ mTextureHost(aTextureHost),
+ mTextureData(std::move(aTextureData)),
+ mSharedSurface(aSharedSurface) {}
+
+RemoteTextureMap::RenderingReadyCallbackHolder::RenderingReadyCallbackHolder(
+ const RemoteTextureId aTextureId,
+ std::function<void(const RemoteTextureInfo&)>&& aCallback)
+ : mTextureId(aTextureId), mCallback(aCallback) {}
+
+RemoteTextureMap::RemoteTextureHostWrapperHolder::
+ RemoteTextureHostWrapperHolder(
+ RefPtr<TextureHost> aRemoteTextureHostWrapper)
+ : mRemoteTextureHostWrapper(aRemoteTextureHostWrapper) {}
+
+} // namespace mozilla::layers
diff --git a/gfx/layers/RemoteTextureMap.h b/gfx/layers/RemoteTextureMap.h
new file mode 100644
index 0000000000..1153446869
--- /dev/null
+++ b/gfx/layers/RemoteTextureMap.h
@@ -0,0 +1,296 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_GFX_RemoteTextureMap_H
+#define MOZILLA_GFX_RemoteTextureMap_H
+
+#include <deque>
+#include <functional>
+#include <map>
+#include <memory>
+#include <queue>
+#include <stack>
+#include <unordered_set>
+#include <utility>
+
+#include "mozilla/gfx/Point.h" // for IntSize
+#include "mozilla/gfx/Types.h" // for SurfaceFormat
+#include "mozilla/ipc/Shmem.h"
+#include "mozilla/layers/CompositorTypes.h" // for TextureFlags, etc
+#include "mozilla/layers/LayersSurfaces.h" // for SurfaceDescriptor
+#include "mozilla/layers/TextureHost.h"
+#include "mozilla/Monitor.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/ThreadSafeWeakPtr.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/webrender/WebRenderTypes.h"
+
+class nsISerialEventTarget;
+
+namespace mozilla {
+
+namespace gl {
+class SharedSurface;
+}
+
+namespace layers {
+
+class CompositableHost;
+class RemoteTextureHostWrapper;
+class TextureData;
+class TextureHost;
+
+struct RemoteTextureInfo {
+ RemoteTextureInfo(const RemoteTextureId aTextureId,
+ const RemoteTextureOwnerId aOwnerId,
+ const base::ProcessId aForPid)
+ : mTextureId(aTextureId), mOwnerId(aOwnerId), mForPid(aForPid) {}
+
+ const RemoteTextureId mTextureId;
+ const RemoteTextureOwnerId mOwnerId;
+ const base::ProcessId mForPid;
+};
+
+struct RemoteTextureInfoList {
+ std::queue<RemoteTextureInfo> mList;
+};
+
+/**
+ * A class provides API for remote texture owners.
+ */
+class RemoteTextureOwnerClient final {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(RemoteTextureOwnerClient)
+
+ explicit RemoteTextureOwnerClient(const base::ProcessId aForPid);
+
+ bool IsRegistered(const RemoteTextureOwnerId aOwnerId);
+ void RegisterTextureOwner(const RemoteTextureOwnerId aOwnerId,
+ bool aIsSyncMode);
+ void UnregisterTextureOwner(const RemoteTextureOwnerId aOwnerId);
+ void UnregisterAllTextureOwners();
+ void PushTexture(const RemoteTextureId aTextureId,
+ const RemoteTextureOwnerId aOwnerId,
+ UniquePtr<TextureData>&& aTextureData,
+ const std::shared_ptr<gl::SharedSurface>& aSharedSurface);
+ void PushDummyTexture(const RemoteTextureId aTextureId,
+ const RemoteTextureOwnerId aOwnerId);
+ void GetLatestBufferSnapshot(const RemoteTextureOwnerId aOwnerId,
+ const mozilla::ipc::Shmem& aDestShmem,
+ const gfx::IntSize& aSize);
+ UniquePtr<TextureData> CreateOrRecycleBufferTextureData(
+ const RemoteTextureOwnerId aOwnerId, gfx::IntSize aSize,
+ gfx::SurfaceFormat aFormat);
+ std::shared_ptr<gl::SharedSurface> GetRecycledSharedSurface(
+ const RemoteTextureOwnerId aOwnerId);
+
+ const base::ProcessId mForPid;
+
+ protected:
+ ~RemoteTextureOwnerClient();
+
+ std::unordered_set<RemoteTextureOwnerId, RemoteTextureOwnerId::HashFn>
+ mOwnerIds;
+};
+
+/**
+ * A class to map RemoteTextureId to remote texture(TextureHost).
+ * Remote textures are provided by texture owner.
+ */
+class RemoteTextureMap {
+ public:
+ static void Init();
+ static void Shutdown();
+ static RemoteTextureMap* Get() { return sInstance; }
+
+ RemoteTextureMap();
+ ~RemoteTextureMap();
+
+ // Push remote texture data and gl::SharedSurface from texture owner.
+ // The texture data is used for creating TextureHost.
+ // gl::SharedSurface is pushed only when the surface needs to be kept alive
+ // during TextureHost usage. The texture data and the surface might be
+ // recycled when TextureHost is destroyed.
+ void PushTexture(const RemoteTextureId aTextureId,
+ const RemoteTextureOwnerId aOwnerId,
+ const base::ProcessId aForPid,
+ UniquePtr<TextureData>&& aTextureData,
+ RefPtr<TextureHost>& aTextureHost,
+ const std::shared_ptr<gl::SharedSurface>& aSharedSurface);
+
+ void GetLatestBufferSnapshot(const RemoteTextureOwnerId aOwnerId,
+ const base::ProcessId aForPid,
+ const mozilla::ipc::Shmem& aDestShmem,
+ const gfx::IntSize& aSize);
+
+ // aIsSyncMode defines if RemoteTextureMap::GetRemoteTextureForDisplayList()
+ // works synchronously.
+ void RegisterTextureOwner(const RemoteTextureOwnerId aOwnerId,
+ const base::ProcessId aForPid, bool aIsSyncMode);
+ void UnregisterTextureOwner(const RemoteTextureOwnerId aOwnerIds,
+ const base::ProcessId aForPid);
+ void UnregisterTextureOwners(
+ const std::unordered_set<RemoteTextureOwnerId,
+ RemoteTextureOwnerId::HashFn>& aOwnerIds,
+ const base::ProcessId aForPid);
+
+ // Get TextureHost that is used for building wr display list.
+ // In sync mode, mRemoteTextureForDisplayList holds TextureHost of mTextureId.
+ // In async mode, it could be previous remote texture's TextureHost that is
+ // compatible to the mTextureId's TextureHost.
+ //
+ // return true when aReadyCallback will be called.
+ bool GetRemoteTextureForDisplayList(
+ RemoteTextureHostWrapper* aTextureHostWrapper,
+ std::function<void(const RemoteTextureInfo&)>&& aReadyCallback);
+
+ // Get ExternalImageId of remote texture for WebRender rendering.
+ wr::MaybeExternalImageId GetExternalImageIdOfRemoteTexture(
+ const RemoteTextureId aTextureId, const RemoteTextureOwnerId aOwnerId,
+ const base::ProcessId aForPid);
+
+ void ReleaseRemoteTextureHostForDisplayList(
+ RemoteTextureHostWrapper* aTextureHostWrapper);
+
+ RefPtr<TextureHost> GetOrCreateRemoteTextureHostWrapper(
+ const RemoteTextureId aTextureId, const RemoteTextureOwnerId aOwnerId,
+ const base::ProcessId aForPid, const gfx::IntSize aSize,
+ const TextureFlags aFlags);
+
+ void UnregisterRemoteTextureHostWrapper(const RemoteTextureId aTextureId,
+ const RemoteTextureOwnerId aOwnerId,
+ const base::ProcessId aForPid);
+
+ void RegisterRemoteTexturePushListener(const RemoteTextureOwnerId aOwnerId,
+ const base::ProcessId aForPid,
+ CompositableHost* aListener);
+
+ void UnregisterRemoteTexturePushListener(const RemoteTextureOwnerId aOwnerId,
+ const base::ProcessId aForPid,
+ CompositableHost* aListener);
+
+ bool CheckRemoteTextureReady(
+ const RemoteTextureInfo& aInfo,
+ std::function<void(const RemoteTextureInfo&)>&& aCallback);
+
+ bool WaitRemoteTextureReady(const RemoteTextureInfo& aInfo);
+
+ void SuppressRemoteTextureReadyCheck(const RemoteTextureId aTextureId,
+ const base::ProcessId aForPid);
+
+ UniquePtr<TextureData> GetRecycledBufferTextureData(
+ const RemoteTextureOwnerId aOwnerId, const base::ProcessId aForPid,
+ gfx::IntSize aSize, gfx::SurfaceFormat aFormat);
+
+ std::shared_ptr<gl::SharedSurface> GetRecycledSharedSurface(
+ const RemoteTextureOwnerId aOwnerId, const base::ProcessId aForPid);
+
+ static RefPtr<TextureHost> CreateRemoteTexture(TextureData* aTextureData,
+ TextureFlags aTextureFlags);
+
+ protected:
+ // Holds data related to remote texture
+ struct TextureDataHolder {
+ TextureDataHolder(const RemoteTextureId aTextureId,
+ RefPtr<TextureHost> aTextureHost,
+ UniquePtr<TextureData>&& aTextureData,
+ const std::shared_ptr<gl::SharedSurface>& aSharedSurface);
+
+ const RemoteTextureId mTextureId;
+ // TextureHost of remote texture
+ // Compositable ref of the mTextureHost should be updated within mMonitor.
+ // The compositable ref is used to check if TextureHost(remote texture) is
+ // still in use by WebRender.
+ RefPtr<TextureHost> mTextureHost;
+ // Holds BufferTextureData of TextureHost
+ UniquePtr<TextureData> mTextureData;
+ // Holds gl::SharedSurface of TextureHost
+ std::shared_ptr<gl::SharedSurface> mSharedSurface;
+ };
+
+ struct RenderingReadyCallbackHolder {
+ RenderingReadyCallbackHolder(
+ const RemoteTextureId aTextureId,
+ std::function<void(const RemoteTextureInfo&)>&& aCallback);
+
+ const RemoteTextureId mTextureId;
+ // callback of async RemoteTexture ready
+ std::function<void(const RemoteTextureInfo&)> mCallback;
+ };
+
+ struct TextureOwner {
+ bool mIsSyncMode = true;
+ // Holds TextureDataHolders that wait to be used for building wr display
+ // list.
+ std::deque<UniquePtr<TextureDataHolder>> mWaitingTextureDataHolders;
+ // Holds TextureDataHolders that are used for building wr display list.
+ std::deque<UniquePtr<TextureDataHolder>> mUsingTextureDataHolders;
+ // Holds async RemoteTexture ready callbacks.
+ std::deque<UniquePtr<RenderingReadyCallbackHolder>>
+ mRenderingReadyCallbackHolders;
+
+ RemoteTextureId mLatestTextureId = {0};
+ CompositableTextureHostRef mLatestTextureHost;
+ CompositableTextureHostRef mLatestRenderedTextureHost;
+ std::stack<UniquePtr<TextureData>> mRecycledTextures;
+ std::queue<std::shared_ptr<gl::SharedSurface>> mRecycledSharedSurfaces;
+ };
+
+ // Holds data related to remote texture wrapper
+ struct RemoteTextureHostWrapperHolder {
+ explicit RemoteTextureHostWrapperHolder(
+ RefPtr<TextureHost> aRemoteTextureHostWrapper);
+
+ const RefPtr<TextureHost> mRemoteTextureHostWrapper;
+ // Hold compositable ref of remote texture of the RemoteTextureId in async
+ // mode. It is for keeping the texture alive during its rendering by
+ // WebRender.
+ CompositableTextureHostRef mAsyncRemoteTextureHost;
+ bool mReadyCheckSuppressed = false;
+ };
+
+ void UpdateTexture(const MonitorAutoLock& aProofOfLock,
+ RemoteTextureMap::TextureOwner* aOwner,
+ const RemoteTextureId aTextureId);
+
+ std::vector<std::function<void(const RemoteTextureInfo&)>>
+ GetRenderingReadyCallbacks(const MonitorAutoLock& aProofOfLock,
+ RemoteTextureMap::TextureOwner* aOwner,
+ const RemoteTextureId aTextureId);
+
+ std::vector<std::function<void(const RemoteTextureInfo&)>>
+ GetAllRenderingReadyCallbacks(const MonitorAutoLock& aProofOfLock,
+ RemoteTextureMap::TextureOwner* aOwner);
+
+ void KeepTextureDataAliveForTextureHostIfNecessary(
+ const MonitorAutoLock& aProofOfLock,
+ std::deque<UniquePtr<TextureDataHolder>>& aHolders);
+
+ RemoteTextureMap::TextureOwner* GetTextureOwner(
+ const MonitorAutoLock& aProofOfLock, const RemoteTextureOwnerId aOwnerId,
+ const base::ProcessId aForPid);
+
+ Monitor mMonitor MOZ_UNANNOTATED;
+
+ std::map<std::pair<base::ProcessId, RemoteTextureOwnerId>,
+ UniquePtr<TextureOwner>>
+ mTextureOwners;
+
+ std::map<std::pair<base::ProcessId, RemoteTextureId>,
+ UniquePtr<RemoteTextureHostWrapperHolder>>
+ mRemoteTextureHostWrapperHolders;
+
+ std::map<std::pair<base::ProcessId, RemoteTextureOwnerId>,
+ RefPtr<CompositableHost>>
+ mRemoteTexturePushListeners;
+
+ static StaticAutoPtr<RemoteTextureMap> sInstance;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // MOZILLA_GFX_RemoteTextureMap_H
diff --git a/gfx/layers/RepaintRequest.cpp b/gfx/layers/RepaintRequest.cpp
new file mode 100644
index 0000000000..d4c84e75a6
--- /dev/null
+++ b/gfx/layers/RepaintRequest.cpp
@@ -0,0 +1,27 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "RepaintRequest.h"
+
+#include <ostream>
+
+namespace mozilla {
+namespace layers {
+
+std::ostream& operator<<(std::ostream& aOut, const RepaintRequest& aRequest) {
+ aOut << "{ scrollId=" << aRequest.mScrollId
+ << ", scrollOffset=" << aRequest.mScrollOffset
+ << ", zoom=" << aRequest.mZoom
+ << ", viewport=" << aRequest.mLayoutViewport
+ << ", scrollUpdateType=" << (int)aRequest.mScrollUpdateType
+ << ", scrollGeneration=" << aRequest.mScrollGeneration
+ << ", scrollGenerationOnApz=" << aRequest.mScrollGenerationOnApz
+ << ", dpMargins=" << aRequest.mDisplayPortMargins << "}";
+ return aOut;
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/RepaintRequest.h b/gfx/layers/RepaintRequest.h
new file mode 100644
index 0000000000..44fdcb7d10
--- /dev/null
+++ b/gfx/layers/RepaintRequest.h
@@ -0,0 +1,337 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GFX_REPAINTREQUEST_H
+#define GFX_REPAINTREQUEST_H
+
+#include <iosfwd>
+#include <stdint.h> // for uint8_t, uint32_t, uint64_t
+
+#include "FrameMetrics.h" // for FrameMetrics
+#include "mozilla/DefineEnum.h" // for MOZ_DEFINE_ENUM
+#include "mozilla/gfx/BasePoint.h" // for BasePoint
+#include "mozilla/gfx/Rect.h" // for RoundedIn
+#include "mozilla/gfx/ScaleFactor.h" // for ScaleFactor
+#include "mozilla/ScrollSnapTargetId.h" // for ScrollSnapTargetIds
+#include "mozilla/TimeStamp.h" // for TimeStamp
+#include "Units.h" // for CSSRect, CSSPixel, etc
+#include "UnitTransforms.h" // for ViewAs
+
+namespace IPC {
+template <typename T>
+struct ParamTraits;
+} // namespace IPC
+
+namespace mozilla {
+namespace layers {
+
+struct RepaintRequest {
+ friend struct IPC::ParamTraits<mozilla::layers::RepaintRequest>;
+
+ public:
+ // clang-format off
+ MOZ_DEFINE_ENUM_WITH_BASE_AT_CLASS_SCOPE(
+ ScrollOffsetUpdateType, uint8_t, (
+ eNone, // The default; the scroll offset was not updated.
+ eUserAction, // The scroll offset was updated by APZ in response
+ // to user action.
+ eVisualUpdate // The scroll offset was updated by APZ in response
+ // to a visual scroll update request from the
+ // main thread.
+ ));
+ // clang-format on
+
+ RepaintRequest()
+ : mScrollId(ScrollableLayerGuid::NULL_SCROLL_ID),
+ mPresShellResolution(1),
+ mCompositionBounds(0, 0, 0, 0),
+ mCumulativeResolution(),
+ mDevPixelsPerCSSPixel(1),
+ mScrollOffset(0, 0),
+ mZoom(),
+ mDisplayPortMargins(0, 0, 0, 0),
+ mPresShellId(-1),
+ mLayoutViewport(0, 0, 0, 0),
+ mTransformToAncestorScale(),
+ mPaintRequestTime(),
+ mScrollUpdateType(eNone),
+ mScrollAnimationType(APZScrollAnimationType::No),
+ mIsRootContent(false),
+ mIsScrollInfoLayer(false),
+ mIsInScrollingGesture(false) {}
+
+ RepaintRequest(const FrameMetrics& aOther,
+ const ScreenMargin& aDisplayportMargins,
+ const ScrollOffsetUpdateType aScrollUpdateType,
+ APZScrollAnimationType aScrollAnimationType,
+ const APZScrollGeneration& aScrollGenerationOnApz,
+ const ScrollSnapTargetIds& aLastSnapTargetIds,
+ bool aIsInScrollingGesture)
+ : mScrollId(aOther.GetScrollId()),
+ mPresShellResolution(aOther.GetPresShellResolution()),
+ mCompositionBounds(aOther.GetCompositionBounds()),
+ mCumulativeResolution(aOther.GetCumulativeResolution()),
+ mDevPixelsPerCSSPixel(aOther.GetDevPixelsPerCSSPixel()),
+ mScrollOffset(aOther.GetVisualScrollOffset()),
+ mZoom(aOther.GetZoom()),
+ mScrollGeneration(aOther.GetScrollGeneration()),
+ mScrollGenerationOnApz(aScrollGenerationOnApz),
+ mDisplayPortMargins(aDisplayportMargins),
+ mPresShellId(aOther.GetPresShellId()),
+ mLayoutViewport(aOther.GetLayoutViewport()),
+ mTransformToAncestorScale(aOther.GetTransformToAncestorScale()),
+ mPaintRequestTime(aOther.GetPaintRequestTime()),
+ mScrollUpdateType(aScrollUpdateType),
+ mScrollAnimationType(aScrollAnimationType),
+ mLastSnapTargetIds(aLastSnapTargetIds),
+ mIsRootContent(aOther.IsRootContent()),
+ mIsScrollInfoLayer(aOther.IsScrollInfoLayer()),
+ mIsInScrollingGesture(aIsInScrollingGesture) {}
+
+ // Default copy ctor and operator= are fine
+
+ bool operator==(const RepaintRequest& aOther) const {
+ // Put mScrollId at the top since it's the most likely one to fail.
+ return mScrollId == aOther.mScrollId &&
+ mPresShellResolution == aOther.mPresShellResolution &&
+ mCompositionBounds.IsEqualEdges(aOther.mCompositionBounds) &&
+ mCumulativeResolution == aOther.mCumulativeResolution &&
+ mDevPixelsPerCSSPixel == aOther.mDevPixelsPerCSSPixel &&
+ mScrollOffset == aOther.mScrollOffset &&
+ // don't compare mZoom
+ mScrollGeneration == aOther.mScrollGeneration &&
+ mDisplayPortMargins == aOther.mDisplayPortMargins &&
+ mPresShellId == aOther.mPresShellId &&
+ mLayoutViewport.IsEqualEdges(aOther.mLayoutViewport) &&
+ mTransformToAncestorScale == aOther.mTransformToAncestorScale &&
+ mPaintRequestTime == aOther.mPaintRequestTime &&
+ mScrollUpdateType == aOther.mScrollUpdateType &&
+ mScrollAnimationType == aOther.mScrollAnimationType &&
+ mLastSnapTargetIds == aOther.mLastSnapTargetIds &&
+ mIsRootContent == aOther.mIsRootContent &&
+ mIsScrollInfoLayer == aOther.mIsScrollInfoLayer &&
+ mIsInScrollingGesture == aOther.mIsInScrollingGesture;
+ }
+
+ bool operator!=(const RepaintRequest& aOther) const {
+ return !operator==(aOther);
+ }
+
+ friend std::ostream& operator<<(std::ostream& aOut,
+ const RepaintRequest& aRequest);
+
+ CSSToScreenScale2D DisplayportPixelsPerCSSPixel() const {
+ // Refer to FrameMetrics::DisplayportPixelsPerCSSPixel() for explanation.
+ return mZoom * mTransformToAncestorScale;
+ }
+
+ CSSToLayerScale LayersPixelsPerCSSPixel() const {
+ return mDevPixelsPerCSSPixel * mCumulativeResolution;
+ }
+
+ // Get the amount by which this frame has been zoomed since the last repaint.
+ LayerToParentLayerScale GetAsyncZoom() const {
+ return mZoom / LayersPixelsPerCSSPixel();
+ }
+
+ CSSSize CalculateCompositedSizeInCssPixels() const {
+ if (GetZoom() == CSSToParentLayerScale(0)) {
+ return CSSSize(); // avoid division by zero
+ }
+ return mCompositionBounds.Size() / GetZoom();
+ }
+
+ float GetPresShellResolution() const { return mPresShellResolution; }
+
+ const ParentLayerRect& GetCompositionBounds() const {
+ return mCompositionBounds;
+ }
+
+ const LayoutDeviceToLayerScale& GetCumulativeResolution() const {
+ return mCumulativeResolution;
+ }
+
+ const CSSToLayoutDeviceScale& GetDevPixelsPerCSSPixel() const {
+ return mDevPixelsPerCSSPixel;
+ }
+
+ bool IsAnimationInProgress() const {
+ return mScrollAnimationType != APZScrollAnimationType::No;
+ }
+
+ bool IsRootContent() const { return mIsRootContent; }
+
+ CSSPoint GetLayoutScrollOffset() const { return mLayoutViewport.TopLeft(); }
+
+ const CSSPoint& GetVisualScrollOffset() const { return mScrollOffset; }
+
+ const CSSToParentLayerScale& GetZoom() const { return mZoom; }
+
+ ScrollOffsetUpdateType GetScrollUpdateType() const {
+ return mScrollUpdateType;
+ }
+
+ bool GetScrollOffsetUpdated() const { return mScrollUpdateType != eNone; }
+
+ MainThreadScrollGeneration GetScrollGeneration() const {
+ return mScrollGeneration;
+ }
+
+ APZScrollGeneration GetScrollGenerationOnApz() const {
+ return mScrollGenerationOnApz;
+ }
+
+ ScrollableLayerGuid::ViewID GetScrollId() const { return mScrollId; }
+
+ const ScreenMargin& GetDisplayPortMargins() const {
+ return mDisplayPortMargins;
+ }
+
+ uint32_t GetPresShellId() const { return mPresShellId; }
+
+ const CSSRect& GetLayoutViewport() const { return mLayoutViewport; }
+
+ const ParentLayerToScreenScale2D& GetTransformToAncestorScale() const {
+ return mTransformToAncestorScale;
+ }
+
+ const TimeStamp& GetPaintRequestTime() const { return mPaintRequestTime; }
+
+ bool IsScrollInfoLayer() const { return mIsScrollInfoLayer; }
+
+ bool IsInScrollingGesture() const { return mIsInScrollingGesture; }
+
+ APZScrollAnimationType GetScrollAnimationType() const {
+ return mScrollAnimationType;
+ }
+
+ const ScrollSnapTargetIds& GetLastSnapTargetIds() const {
+ return mLastSnapTargetIds;
+ }
+
+ protected:
+ void SetIsRootContent(bool aIsRootContent) {
+ mIsRootContent = aIsRootContent;
+ }
+
+ void SetIsScrollInfoLayer(bool aIsScrollInfoLayer) {
+ mIsScrollInfoLayer = aIsScrollInfoLayer;
+ }
+
+ void SetIsInScrollingGesture(bool aIsInScrollingGesture) {
+ mIsInScrollingGesture = aIsInScrollingGesture;
+ }
+
+ private:
+ // A ID assigned to each scrollable frame, unique within each LayersId..
+ ScrollableLayerGuid::ViewID mScrollId;
+
+ // The pres-shell resolution that has been induced on the document containing
+ // this scroll frame as a result of zooming this scroll frame (whether via
+ // user action, or choosing an initial zoom level on page load). This can
+ // only be different from 1.0 for frames that are zoomable, which currently
+ // is just the root content document's root scroll frame
+ // (mIsRootContent = true).
+ // This is a plain float rather than a ScaleFactor because in and of itself
+ // it does not convert between any coordinate spaces for which we have names.
+ float mPresShellResolution;
+
+ // This is the area within the widget that we're compositing to. It is in the
+ // layer coordinates of the scrollable content's parent layer.
+ //
+ // The size of the composition bounds corresponds to the size of the scroll
+ // frame's scroll port (but in a coordinate system where the size does not
+ // change during zooming).
+ //
+ // The origin of the composition bounds is relative to the layer tree origin.
+ // Unlike the scroll port's origin, it does not change during scrolling of
+ // the scrollable layer to which it is associated. However, it may change due
+ // to scrolling of ancestor layers.
+ //
+ // This value is provided by Gecko at layout/paint time.
+ ParentLayerRect mCompositionBounds;
+
+ // See FrameMetrics::mCumulativeResolution for description.
+ LayoutDeviceToLayerScale mCumulativeResolution;
+
+ // The conversion factor between CSS pixels and device pixels for this frame.
+ // This can vary based on a variety of things, such as reflowing-zoom.
+ CSSToLayoutDeviceScale mDevPixelsPerCSSPixel;
+
+ // The position of the top-left of the scroll frame's scroll port, relative
+ // to the scrollable content's origin.
+ CSSPoint mScrollOffset;
+
+ // The "user zoom". Content is painted by gecko at mCumulativeResolution *
+ // mDevPixelsPerCSSPixel, but will be drawn to the screen at mZoom. In the
+ // steady state, the two will be the same, but during an async zoom action the
+ // two may diverge. This information is initialized in Gecko but updated in
+ // the APZC.
+ CSSToParentLayerScale mZoom;
+
+ // The scroll generation counter used to acknowledge the scroll offset update
+ // on the main-thread.
+ MainThreadScrollGeneration mScrollGeneration;
+
+ // The scroll generation counter stored in each SampledAPZState and the
+ // scrollable frame on the main-thread and used to compare with each other
+ // in the WebRender renderer thread to tell which sampled scroll offset
+ // matches the scroll offset used on the main-thread.
+ APZScrollGeneration mScrollGenerationOnApz;
+
+ // A display port expressed as layer margins that apply to the rect of what
+ // is drawn of the scrollable element.
+ ScreenMargin mDisplayPortMargins;
+
+ uint32_t mPresShellId;
+
+ // For a root scroll frame (RSF), the document's layout viewport
+ // (sometimes called "CSS viewport" in older code).
+ //
+ // Its size is the dimensions we're using to constrain the <html> element
+ // of the document (i.e. the initial containing block (ICB) size).
+ //
+ // Its origin is the RSF's layout scroll position, i.e. the scroll position
+ // exposed to web content via window.scrollX/Y.
+ //
+ // Note that only the root content document's RSF has a layout viewport
+ // that's distinct from the visual viewport. For an iframe RSF, the two
+ // are the same.
+ //
+ // For a scroll frame that is not an RSF, this metric is meaningless and
+ // invalid.
+ CSSRect mLayoutViewport;
+
+ // See FrameMetrics::mTransformToAncestorScale for description.
+ ParentLayerToScreenScale2D mTransformToAncestorScale;
+
+ // The time at which the APZC last requested a repaint for this scroll frame.
+ TimeStamp mPaintRequestTime;
+
+ // The type of repaint request this represents.
+ ScrollOffsetUpdateType mScrollUpdateType;
+
+ APZScrollAnimationType mScrollAnimationType;
+
+ ScrollSnapTargetIds mLastSnapTargetIds;
+
+ // Whether or not this is the root scroll frame for the root content document.
+ bool mIsRootContent : 1;
+
+ // True if this scroll frame is a scroll info layer. A scroll info layer is
+ // not layerized and its content cannot be truly async-scrolled, but its
+ // metrics are still sent to and updated by the compositor, with the updates
+ // being reflected on the next paint rather than the next composite.
+ bool mIsScrollInfoLayer : 1;
+
+ // Whether the APZC is in the middle of processing a gesture.
+ bool mIsInScrollingGesture : 1;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif /* GFX_REPAINTREQUEST_H */
diff --git a/gfx/layers/SampleTime.cpp b/gfx/layers/SampleTime.cpp
new file mode 100644
index 0000000000..fd87b669f0
--- /dev/null
+++ b/gfx/layers/SampleTime.cpp
@@ -0,0 +1,75 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "SampleTime.h"
+
+namespace mozilla {
+namespace layers {
+
+SampleTime::SampleTime() : mType(eNull) {}
+
+/*static*/
+SampleTime SampleTime::FromVsync(const TimeStamp& aTime) {
+ MOZ_ASSERT(!aTime.IsNull());
+ return SampleTime(eVsync, aTime);
+}
+
+/*static*/
+SampleTime SampleTime::FromNow() { return SampleTime(eNow, TimeStamp::Now()); }
+
+/*static*/
+SampleTime SampleTime::FromTest(const TimeStamp& aTime) {
+ MOZ_ASSERT(!aTime.IsNull());
+ return SampleTime(eTest, aTime);
+}
+
+bool SampleTime::IsNull() const { return mType == eNull; }
+
+SampleTime::TimeType SampleTime::Type() const { return mType; }
+
+const TimeStamp& SampleTime::Time() const { return mTime; }
+
+bool SampleTime::operator==(const SampleTime& aOther) const {
+ return mTime == aOther.mTime;
+}
+
+bool SampleTime::operator!=(const SampleTime& aOther) const {
+ return !(*this == aOther);
+}
+
+bool SampleTime::operator<(const SampleTime& aOther) const {
+ return mTime < aOther.mTime;
+}
+
+bool SampleTime::operator<=(const SampleTime& aOther) const {
+ return mTime <= aOther.mTime;
+}
+
+bool SampleTime::operator>(const SampleTime& aOther) const {
+ return mTime > aOther.mTime;
+}
+
+bool SampleTime::operator>=(const SampleTime& aOther) const {
+ return mTime >= aOther.mTime;
+}
+
+SampleTime SampleTime::operator+(const TimeDuration& aDuration) const {
+ return SampleTime(mType, mTime + aDuration);
+}
+
+SampleTime SampleTime::operator-(const TimeDuration& aDuration) const {
+ return SampleTime(mType, mTime - aDuration);
+}
+
+TimeDuration SampleTime::operator-(const SampleTime& aOther) const {
+ return mTime - aOther.mTime;
+}
+
+SampleTime::SampleTime(TimeType aType, const TimeStamp& aTime)
+ : mType(aType), mTime(aTime) {}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/SampleTime.h b/gfx/layers/SampleTime.h
new file mode 100644
index 0000000000..a32f758eb7
--- /dev/null
+++ b/gfx/layers/SampleTime.h
@@ -0,0 +1,69 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_SampleTime_h
+#define mozilla_layers_SampleTime_h
+
+#include "mozilla/TimeStamp.h"
+
+namespace mozilla {
+namespace layers {
+
+/**
+ * When compositing frames, there is usually a "sample time" associated
+ * with the frame, which may be derived from vsync or controlled by a test.
+ * This class encapsulates that and tracks where the sample time comes from,
+ * as the desired behaviour may vary based on the time source.
+ */
+class SampleTime {
+ public:
+ enum TimeType {
+ // Uninitialized sample time.
+ eNull,
+ // Time comes from a vsync tick, possibly adjusted by a vsync interval.
+ eVsync,
+ // Time comes from a "now" timestamp
+ eNow,
+ // Time is set by a test
+ eTest,
+ };
+
+ SampleTime();
+ static SampleTime FromVsync(const TimeStamp& aTime);
+ static SampleTime FromNow();
+ static SampleTime FromTest(const TimeStamp& aTime);
+
+ bool IsNull() const;
+ TimeType Type() const;
+ const TimeStamp& Time() const;
+
+ // These operators just compare the timestamps
+ bool operator==(const SampleTime& aOther) const;
+ bool operator!=(const SampleTime& aOther) const;
+ bool operator<(const SampleTime& aOther) const;
+ bool operator<=(const SampleTime& aOther) const;
+ bool operator>(const SampleTime& aOther) const;
+ bool operator>=(const SampleTime& aOther) const;
+ // These return a new SampleTime with the same type as this one, but the
+ // time adjusted by the provided TimeDuration
+ SampleTime operator+(const TimeDuration& aDuration) const;
+ SampleTime operator-(const TimeDuration& aDuration) const;
+ // This just produces the time difference between the two SampleTimes,
+ // ignoring the type.
+ TimeDuration operator-(const SampleTime& aOther) const;
+
+ private:
+ // Private constructor; use one of the static FromXXX methods instead.
+ SampleTime(TimeType aType, const TimeStamp& aTime);
+
+ TimeType mType;
+ TimeStamp mTime;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_SampleTime_h
diff --git a/gfx/layers/ScreenshotGrabber.cpp b/gfx/layers/ScreenshotGrabber.cpp
new file mode 100644
index 0000000000..3719afa336
--- /dev/null
+++ b/gfx/layers/ScreenshotGrabber.cpp
@@ -0,0 +1,229 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ScreenshotGrabber.h"
+
+#include "mozilla/ProfilerMarkers.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/TimeStamp.h"
+#include "mozilla/UniquePtr.h"
+
+#include "mozilla/layers/Compositor.h"
+#include "mozilla/layers/ProfilerScreenshots.h"
+#include "mozilla/layers/TextureHost.h"
+#include "mozilla/gfx/Point.h"
+#include "nsTArray.h"
+
+namespace mozilla {
+
+using namespace gfx;
+
+namespace layers {
+namespace profiler_screenshots {
+
+/**
+ * The actual implementation of screenshot grabbing.
+ * The ScreenshotGrabberImpl object is destroyed if the profiler is
+ * disabled and MaybeGrabScreenshot notices it.
+ */
+class ScreenshotGrabberImpl final {
+ public:
+ explicit ScreenshotGrabberImpl(const IntSize& aBufferSize);
+ ~ScreenshotGrabberImpl();
+
+ void GrabScreenshot(Window& aWindow, const IntSize& aWindowSize);
+ void ProcessQueue();
+
+ private:
+ struct QueueItem final {
+ mozilla::TimeStamp mTimeStamp;
+ RefPtr<AsyncReadbackBuffer> mScreenshotBuffer;
+ IntSize mScreenshotSize;
+ IntSize mWindowSize;
+ };
+
+ RefPtr<RenderSource> ScaleDownWindowRenderSourceToSize(
+ Window& aWindow, const IntSize& aDestSize,
+ RenderSource* aWindowRenderSource, size_t aLevel);
+
+ already_AddRefed<AsyncReadbackBuffer> TakeNextBuffer(Window& aWindow);
+ void ReturnBuffer(AsyncReadbackBuffer* aBuffer);
+
+ nsTArray<RefPtr<DownscaleTarget>> mCachedLevels;
+ nsTArray<RefPtr<AsyncReadbackBuffer>> mAvailableBuffers;
+ Maybe<QueueItem> mCurrentFrameQueueItem;
+ nsTArray<QueueItem> mQueue;
+ RefPtr<ProfilerScreenshots> mProfilerScreenshots;
+ const IntSize mBufferSize;
+};
+
+} // namespace profiler_screenshots
+
+ScreenshotGrabber::ScreenshotGrabber() = default;
+
+ScreenshotGrabber::~ScreenshotGrabber() = default;
+
+void ScreenshotGrabber::MaybeGrabScreenshot(
+ profiler_screenshots::Window& aWindow, const IntSize& aWindowSize) {
+ if (ProfilerScreenshots::IsEnabled()) {
+ if (!mImpl) {
+ mImpl = MakeUnique<profiler_screenshots::ScreenshotGrabberImpl>(
+ ProfilerScreenshots::ScreenshotSize());
+ }
+ mImpl->GrabScreenshot(aWindow, aWindowSize);
+ } else if (mImpl) {
+ Destroy();
+ }
+}
+
+void ScreenshotGrabber::MaybeProcessQueue() {
+ if (ProfilerScreenshots::IsEnabled()) {
+ if (!mImpl) {
+ mImpl = MakeUnique<profiler_screenshots::ScreenshotGrabberImpl>(
+ ProfilerScreenshots::ScreenshotSize());
+ }
+ mImpl->ProcessQueue();
+ } else if (mImpl) {
+ Destroy();
+ }
+}
+
+void ScreenshotGrabber::NotifyEmptyFrame() {
+ PROFILER_MARKER_UNTYPED("NoCompositorScreenshot because nothing changed",
+ GRAPHICS);
+}
+
+void ScreenshotGrabber::Destroy() { mImpl = nullptr; }
+
+namespace profiler_screenshots {
+
+ScreenshotGrabberImpl::ScreenshotGrabberImpl(const IntSize& aBufferSize)
+ : mBufferSize(aBufferSize) {}
+
+ScreenshotGrabberImpl::~ScreenshotGrabberImpl() {
+ // Any queue items in mQueue or mCurrentFrameQueueItem will be lost.
+ // That's ok: Either the profiler has stopped and we don't care about these
+ // screenshots, or the window is closing and we don't really need the last
+ // few frames from the window.
+}
+
+// Scale down aWindowRenderSource into a RenderSource of size
+// mBufferSize * (1 << aLevel) and return that RenderSource.
+// Don't scale down by more than a factor of 2 with a single scaling operation,
+// because it'll look bad. If higher scales are needed, use another
+// intermediate target by calling this function recursively with aLevel + 1.
+RefPtr<RenderSource> ScreenshotGrabberImpl::ScaleDownWindowRenderSourceToSize(
+ Window& aWindow, const IntSize& aDestSize,
+ RenderSource* aWindowRenderSource, size_t aLevel) {
+ if (aLevel == mCachedLevels.Length()) {
+ mCachedLevels.AppendElement(
+ aWindow.CreateDownscaleTarget(mBufferSize * (1 << aLevel)));
+ }
+ MOZ_RELEASE_ASSERT(aLevel < mCachedLevels.Length());
+
+ RefPtr<RenderSource> renderSource = aWindowRenderSource;
+ IntSize sourceSize = aWindowRenderSource->Size();
+ if (sourceSize.width > aDestSize.width * 2) {
+ sourceSize = aDestSize * 2;
+ renderSource = ScaleDownWindowRenderSourceToSize(
+ aWindow, sourceSize, aWindowRenderSource, aLevel + 1);
+ }
+
+ if (renderSource) {
+ if (mCachedLevels[aLevel]->DownscaleFrom(
+ renderSource, IntRect({}, sourceSize), IntRect({}, aDestSize))) {
+ return mCachedLevels[aLevel]->AsRenderSource();
+ }
+ }
+ return nullptr;
+}
+
+void ScreenshotGrabberImpl::GrabScreenshot(Window& aWindow,
+ const IntSize& aWindowSize) {
+ RefPtr<RenderSource> windowRenderSource =
+ aWindow.GetWindowContents(aWindowSize);
+
+ if (!windowRenderSource) {
+ PROFILER_MARKER_UNTYPED(
+ "NoCompositorScreenshot because of unsupported compositor "
+ "configuration",
+ GRAPHICS);
+ return;
+ }
+
+ Size windowSize(aWindowSize);
+ float scale = std::min(mBufferSize.width / windowSize.width,
+ mBufferSize.height / windowSize.height);
+ IntSize scaledSize = IntSize::Round(windowSize * scale);
+ RefPtr<RenderSource> scaledTarget = ScaleDownWindowRenderSourceToSize(
+ aWindow, scaledSize, windowRenderSource, 0);
+
+ if (!scaledTarget) {
+ PROFILER_MARKER_UNTYPED(
+ "NoCompositorScreenshot because ScaleDownWindowRenderSourceToSize "
+ "failed",
+ GRAPHICS);
+ return;
+ }
+
+ RefPtr<AsyncReadbackBuffer> buffer = TakeNextBuffer(aWindow);
+ if (!buffer) {
+ PROFILER_MARKER_UNTYPED(
+ "NoCompositorScreenshot because AsyncReadbackBuffer creation failed",
+ GRAPHICS);
+ return;
+ }
+
+ buffer->CopyFrom(scaledTarget);
+
+ // This QueueItem will be added to the queue at the end of the next call to
+ // ProcessQueue(). This ensures that the buffer isn't mapped into main memory
+ // until the next frame. If we did it in this frame, we'd block on the GPU.
+ mCurrentFrameQueueItem =
+ Some(QueueItem{TimeStamp::Now(), std::move(buffer), scaledSize,
+ windowRenderSource->Size()});
+}
+
+already_AddRefed<AsyncReadbackBuffer> ScreenshotGrabberImpl::TakeNextBuffer(
+ Window& aWindow) {
+ if (!mAvailableBuffers.IsEmpty()) {
+ RefPtr<AsyncReadbackBuffer> buffer = mAvailableBuffers[0];
+ mAvailableBuffers.RemoveElementAt(0);
+ return buffer.forget();
+ }
+ return aWindow.CreateAsyncReadbackBuffer(mBufferSize);
+}
+
+void ScreenshotGrabberImpl::ReturnBuffer(AsyncReadbackBuffer* aBuffer) {
+ mAvailableBuffers.AppendElement(aBuffer);
+}
+
+void ScreenshotGrabberImpl::ProcessQueue() {
+ if (!mQueue.IsEmpty()) {
+ if (!mProfilerScreenshots) {
+ mProfilerScreenshots = new ProfilerScreenshots();
+ }
+ for (const auto& item : mQueue) {
+ mProfilerScreenshots->SubmitScreenshot(
+ item.mWindowSize, item.mScreenshotSize, item.mTimeStamp,
+ [&item](DataSourceSurface* aTargetSurface) {
+ return item.mScreenshotBuffer->MapAndCopyInto(aTargetSurface,
+ item.mScreenshotSize);
+ });
+ ReturnBuffer(item.mScreenshotBuffer);
+ }
+ }
+ mQueue.Clear();
+
+ if (mCurrentFrameQueueItem) {
+ mQueue.AppendElement(std::move(*mCurrentFrameQueueItem));
+ mCurrentFrameQueueItem = Nothing();
+ }
+}
+
+} // namespace profiler_screenshots
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/ScreenshotGrabber.h b/gfx/layers/ScreenshotGrabber.h
new file mode 100644
index 0000000000..a2dfc2a085
--- /dev/null
+++ b/gfx/layers/ScreenshotGrabber.h
@@ -0,0 +1,139 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_ScreenshotGrabber_h
+#define mozilla_layers_ScreenshotGrabber_h
+
+#include "nsISupportsImpl.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/gfx/2D.h"
+#include "mozilla/gfx/Point.h"
+#include "mozilla/gfx/Rect.h"
+
+namespace mozilla {
+namespace layers {
+
+namespace profiler_screenshots {
+class Window;
+class RenderSource;
+class DownscaleTarget;
+class AsyncReadbackBuffer;
+
+class ScreenshotGrabberImpl;
+} // namespace profiler_screenshots
+
+/**
+ * Used by various renderers / layer managers to grab snapshots from the window
+ * and submit them to the Gecko profiler.
+ * Doesn't do any work if the profiler is not running or the "screenshots"
+ * feature is not enabled.
+ * Screenshots are scaled down to fit within a fixed size, and read back to
+ * main memory using async readback. Scaling is done in multiple scale-by-0.5x
+ * steps using DownscaleTarget::CopyFrom, and readback is done using
+ * AsyncReadbackBuffers.
+ */
+class ScreenshotGrabber final {
+ public:
+ ScreenshotGrabber();
+ ~ScreenshotGrabber();
+
+ // Scale the contents of aWindow's current render target into an
+ // appropriately sized DownscaleTarget and read its contents into an
+ // AsyncReadbackBuffer. The AsyncReadbackBuffer is not mapped into main
+ // memory until the second call to MaybeProcessQueue() after this call to
+ // MaybeGrabScreenshot().
+ void MaybeGrabScreenshot(profiler_screenshots::Window& aWindow,
+ const gfx::IntSize& aWindowSize);
+
+ // Map the contents of any outstanding AsyncReadbackBuffers from previous
+ // composites into main memory and submit each screenshot to the profiler.
+ void MaybeProcessQueue();
+
+ // Insert a special profiler marker for a composite that didn't do any actual
+ // compositing, so that the profiler knows why no screenshot was taken for
+ // this frame.
+ void NotifyEmptyFrame();
+
+ // Destroy all Window-related resources that this class is holding on to.
+ void Destroy();
+
+ private:
+ // non-null while ProfilerScreenshots::IsEnabled() returns true
+ UniquePtr<profiler_screenshots::ScreenshotGrabberImpl> mImpl;
+};
+
+// Interface definitions.
+
+namespace profiler_screenshots {
+
+class Window {
+ public:
+ virtual already_AddRefed<RenderSource> GetWindowContents(
+ const gfx::IntSize& aWindowSize) = 0;
+ virtual already_AddRefed<DownscaleTarget> CreateDownscaleTarget(
+ const gfx::IntSize& aSize) = 0;
+ virtual already_AddRefed<AsyncReadbackBuffer> CreateAsyncReadbackBuffer(
+ const gfx::IntSize& aSize) = 0;
+
+ protected:
+ virtual ~Window() {}
+};
+
+class RenderSource {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(RenderSource)
+
+ const auto& Size() const { return mSize; }
+
+ protected:
+ explicit RenderSource(const gfx::IntSize& aSize) : mSize(aSize) {}
+ virtual ~RenderSource() {}
+
+ const gfx::IntSize mSize;
+};
+
+class DownscaleTarget {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(DownscaleTarget)
+
+ virtual already_AddRefed<RenderSource> AsRenderSource() = 0;
+
+ const auto& Size() const { return mSize; }
+ virtual bool DownscaleFrom(RenderSource* aSource,
+ const gfx::IntRect& aSourceRect,
+ const gfx::IntRect& aDestRect) = 0;
+
+ protected:
+ explicit DownscaleTarget(const gfx::IntSize& aSize) : mSize(aSize) {}
+ virtual ~DownscaleTarget() {}
+
+ const gfx::IntSize mSize;
+};
+
+class AsyncReadbackBuffer {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(
+ mozilla::layers::profiler_screenshots::AsyncReadbackBuffer)
+
+ const auto& Size() const { return mSize; }
+ virtual void CopyFrom(RenderSource* aSource) = 0;
+ virtual bool MapAndCopyInto(gfx::DataSourceSurface* aSurface,
+ const gfx::IntSize& aReadSize) = 0;
+
+ protected:
+ explicit AsyncReadbackBuffer(const gfx::IntSize& aSize) : mSize(aSize) {}
+ virtual ~AsyncReadbackBuffer() {}
+
+ const gfx::IntSize mSize;
+};
+
+} // namespace profiler_screenshots
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_ScreenshotGrabber_h
diff --git a/gfx/layers/ScrollableLayerGuid.cpp b/gfx/layers/ScrollableLayerGuid.cpp
new file mode 100644
index 0000000000..914a8ad124
--- /dev/null
+++ b/gfx/layers/ScrollableLayerGuid.cpp
@@ -0,0 +1,77 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ScrollableLayerGuid.h"
+
+#include <ostream>
+#include "mozilla/HashFunctions.h" // for HashGeneric
+#include "nsPrintfCString.h" // for nsPrintfCString
+
+namespace mozilla {
+namespace layers {
+
+ScrollableLayerGuid::ScrollableLayerGuid()
+ : mLayersId{0}, mPresShellId(0), mScrollId(0) {}
+
+ScrollableLayerGuid::ScrollableLayerGuid(LayersId aLayersId,
+ uint32_t aPresShellId,
+ ViewID aScrollId)
+ : mLayersId(aLayersId), mPresShellId(aPresShellId), mScrollId(aScrollId) {}
+
+bool ScrollableLayerGuid::operator==(const ScrollableLayerGuid& other) const {
+ return mLayersId == other.mLayersId && mPresShellId == other.mPresShellId &&
+ mScrollId == other.mScrollId;
+}
+
+bool ScrollableLayerGuid::operator!=(const ScrollableLayerGuid& other) const {
+ return !(*this == other);
+}
+
+bool ScrollableLayerGuid::operator<(const ScrollableLayerGuid& other) const {
+ if (mLayersId < other.mLayersId) {
+ return true;
+ }
+ if (mLayersId == other.mLayersId) {
+ if (mPresShellId < other.mPresShellId) {
+ return true;
+ }
+ if (mPresShellId == other.mPresShellId) {
+ return mScrollId < other.mScrollId;
+ }
+ }
+ return false;
+}
+
+std::ostream& operator<<(std::ostream& aOut, const ScrollableLayerGuid& aGuid) {
+ return aOut << nsPrintfCString("{ l=0x%" PRIx64 ", p=%u, v=%" PRIu64 " }",
+ uint64_t(aGuid.mLayersId), aGuid.mPresShellId,
+ aGuid.mScrollId)
+ .get();
+}
+
+bool ScrollableLayerGuid::EqualsIgnoringPresShell(
+ const ScrollableLayerGuid& aA, const ScrollableLayerGuid& aB) {
+ return aA.mLayersId == aB.mLayersId && aA.mScrollId == aB.mScrollId;
+}
+
+std::size_t ScrollableLayerGuid::HashFn::operator()(
+ const ScrollableLayerGuid& aGuid) const {
+ return HashGeneric(uint64_t(aGuid.mLayersId), aGuid.mPresShellId,
+ aGuid.mScrollId);
+}
+
+std::size_t ScrollableLayerGuid::HashIgnoringPresShellFn::operator()(
+ const ScrollableLayerGuid& aGuid) const {
+ return HashGeneric(uint64_t(aGuid.mLayersId), aGuid.mScrollId);
+}
+
+bool ScrollableLayerGuid::EqualIgnoringPresShellFn::operator()(
+ const ScrollableLayerGuid& lhs, const ScrollableLayerGuid& rhs) const {
+ return EqualsIgnoringPresShell(lhs, rhs);
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/ScrollableLayerGuid.h b/gfx/layers/ScrollableLayerGuid.h
new file mode 100644
index 0000000000..ef3733eba0
--- /dev/null
+++ b/gfx/layers/ScrollableLayerGuid.h
@@ -0,0 +1,86 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GFX_SCROLLABLELAYERGUID_H
+#define GFX_SCROLLABLELAYERGUID_H
+
+#include <iosfwd> // for ostream
+#include <stdint.h> // for uint8_t, uint32_t, uint64_t
+#include "mozilla/layers/LayersTypes.h" // for LayersId
+#include "nsHashKeys.h" // for nsUint64HashKey
+
+namespace mozilla {
+namespace layers {
+
+/**
+ * This class allows us to uniquely identify a scrollable layer. The
+ * mLayersId identifies the layer tree (corresponding to a child process
+ * and/or tab) that the scrollable layer belongs to. The mPresShellId
+ * is a temporal identifier (corresponding to the document loaded that
+ * contains the scrollable layer, which may change over time). The
+ * mScrollId corresponds to the actual frame that is scrollable.
+ */
+struct ScrollableLayerGuid {
+ // We use IDs to identify frames across processes.
+ typedef uint64_t ViewID;
+ typedef nsUint64HashKey ViewIDHashKey;
+ static const ViewID NULL_SCROLL_ID; // This container layer does not scroll.
+ static const ViewID START_SCROLL_ID = 2; // This is the ID that scrolling
+ // subframes will begin at.
+
+ LayersId mLayersId;
+ uint32_t mPresShellId;
+ ViewID mScrollId;
+
+ ScrollableLayerGuid();
+
+ ScrollableLayerGuid(LayersId aLayersId, uint32_t aPresShellId,
+ ViewID aScrollId);
+
+ ScrollableLayerGuid(const ScrollableLayerGuid& other) = default;
+
+ ~ScrollableLayerGuid() = default;
+
+ bool operator==(const ScrollableLayerGuid& other) const;
+ bool operator!=(const ScrollableLayerGuid& other) const;
+ bool operator<(const ScrollableLayerGuid& other) const;
+
+ friend std::ostream& operator<<(std::ostream& aOut,
+ const ScrollableLayerGuid& aGuid);
+
+ using Comparator = bool (*)(const ScrollableLayerGuid&,
+ const ScrollableLayerGuid&);
+
+ static bool EqualsIgnoringPresShell(const ScrollableLayerGuid& aA,
+ const ScrollableLayerGuid& aB);
+
+ // Helper structs to use as hash/equality functions in std::unordered_map.
+ // e.g. std::unordered_map<ScrollableLayerGuid,
+ // ValueType,
+ // ScrollableLayerGuid::HashFn> myMap;
+ // std::unordered_map<ScrollableLayerGuid,
+ // ValueType,
+ // ScrollableLayerGuid::HashIgnoringPresShellFn,
+ // ScrollableLayerGuid::EqualIgnoringPresShellFn> myMap;
+
+ struct HashFn {
+ std::size_t operator()(const ScrollableLayerGuid& aGuid) const;
+ };
+
+ struct HashIgnoringPresShellFn {
+ std::size_t operator()(const ScrollableLayerGuid& aGuid) const;
+ };
+
+ struct EqualIgnoringPresShellFn {
+ bool operator()(const ScrollableLayerGuid& lhs,
+ const ScrollableLayerGuid& rhs) const;
+ };
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif /* GFX_SCROLLABLELAYERGUID_H */
diff --git a/gfx/layers/ScrollbarData.h b/gfx/layers/ScrollbarData.h
new file mode 100644
index 0000000000..c5811a015d
--- /dev/null
+++ b/gfx/layers/ScrollbarData.h
@@ -0,0 +1,139 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_gfx_layers_ScrollbarData_h
+#define mozilla_gfx_layers_ScrollbarData_h
+
+#include "FrameMetrics.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/gfx/Types.h"
+#include "mozilla/layers/LayersTypes.h"
+#include "mozilla/layers/ScrollableLayerGuid.h"
+
+namespace IPC {
+template <typename T>
+struct ParamTraits;
+} // namespace IPC
+
+namespace mozilla {
+namespace layers {
+
+// clang-format off
+MOZ_DEFINE_ENUM_CLASS_WITH_BASE(ScrollbarLayerType, uint8_t, (
+ None,
+ Thumb,
+ Container
+));
+// clang-format on
+
+/**
+ * It stores data for scroll thumb layer or container layers.
+ */
+struct ScrollbarData {
+ private:
+ /**
+ * This constructor is for Thumb layer type.
+ */
+ ScrollbarData(ScrollDirection aDirection, float aThumbRatio,
+ OuterCSSCoord aThumbStart, OuterCSSCoord aThumbLength,
+ OuterCSSCoord aThumbMinLength, bool aThumbIsAsyncDraggable,
+ OuterCSSCoord aScrollTrackStart,
+ OuterCSSCoord aScrollTrackLength, uint64_t aTargetViewId)
+ : mDirection(Some(aDirection)),
+ mScrollbarLayerType(ScrollbarLayerType::Thumb),
+ mThumbRatio(aThumbRatio),
+ mThumbStart(aThumbStart),
+ mThumbLength(aThumbLength),
+ mThumbMinLength(aThumbMinLength),
+ mThumbIsAsyncDraggable(aThumbIsAsyncDraggable),
+ mScrollTrackStart(aScrollTrackStart),
+ mScrollTrackLength(aScrollTrackLength),
+ mTargetViewId(aTargetViewId) {}
+
+ /**
+ * This constructor is for Container layer type.
+ */
+ ScrollbarData(const Maybe<ScrollDirection>& aDirection,
+ uint64_t aTargetViewId)
+ : mDirection(aDirection),
+ mScrollbarLayerType(ScrollbarLayerType::Container),
+ mTargetViewId(aTargetViewId) {}
+
+ public:
+ ScrollbarData() = default;
+
+ static ScrollbarData CreateForThumb(
+ ScrollDirection aDirection, float aThumbRatio, OuterCSSCoord aThumbStart,
+ OuterCSSCoord aThumbLength, OuterCSSCoord aThumbMinLength,
+ bool aThumbIsAsyncDraggable, OuterCSSCoord aScrollTrackStart,
+ OuterCSSCoord aScrollTrackLength, uint64_t aTargetViewId) {
+ return ScrollbarData(aDirection, aThumbRatio, aThumbStart, aThumbLength,
+ aThumbMinLength, aThumbIsAsyncDraggable,
+ aScrollTrackStart, aScrollTrackLength, aTargetViewId);
+ }
+
+ static ScrollbarData CreateForScrollbarContainer(
+ const Maybe<ScrollDirection>& aDirection, uint64_t aTargetViewId) {
+ return ScrollbarData(aDirection, aTargetViewId);
+ }
+
+ /**
+ * The mDirection contains a direction if mScrollbarLayerType is Thumb
+ * or Container, otherwise it's empty.
+ */
+ Maybe<ScrollDirection> mDirection;
+
+ /**
+ * Indicate what kind of layer this data is for. All possibilities are defined
+ * in enum ScrollbarLayerType
+ */
+ ScrollbarLayerType mScrollbarLayerType = ScrollbarLayerType::None;
+
+ /**
+ * The scrollbar thumb ratio is the ratio of the thumb position (in the CSS
+ * pixels of the scrollframe's parent's space) to the scroll position (in the
+ * CSS pixels of the scrollframe's space).
+ */
+ float mThumbRatio = 0.0f;
+
+ OuterCSSCoord mThumbStart;
+ OuterCSSCoord mThumbLength;
+ OuterCSSCoord mThumbMinLength;
+
+ /**
+ * Whether the scrollbar thumb can be dragged asynchronously.
+ */
+ bool mThumbIsAsyncDraggable = false;
+
+ OuterCSSCoord mScrollTrackStart;
+ OuterCSSCoord mScrollTrackLength;
+ uint64_t mTargetViewId = ScrollableLayerGuid::NULL_SCROLL_ID;
+
+ bool operator==(const ScrollbarData& aOther) const {
+ return mDirection == aOther.mDirection &&
+ mScrollbarLayerType == aOther.mScrollbarLayerType &&
+ mThumbRatio == aOther.mThumbRatio &&
+ mThumbStart == aOther.mThumbStart &&
+ mThumbLength == aOther.mThumbLength &&
+ mThumbMinLength == aOther.mThumbMinLength &&
+ mThumbIsAsyncDraggable == aOther.mThumbIsAsyncDraggable &&
+ mScrollTrackStart == aOther.mScrollTrackStart &&
+ mScrollTrackLength == aOther.mScrollTrackLength &&
+ mTargetViewId == aOther.mTargetViewId;
+ }
+ bool operator!=(const ScrollbarData& aOther) const {
+ return !(*this == aOther);
+ }
+
+ bool IsThumb() const {
+ return mScrollbarLayerType == ScrollbarLayerType::Thumb;
+ }
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_gfx_layers_ScrollbarData_h
diff --git a/gfx/layers/ShareableCanvasRenderer.cpp b/gfx/layers/ShareableCanvasRenderer.cpp
new file mode 100644
index 0000000000..621d8286a6
--- /dev/null
+++ b/gfx/layers/ShareableCanvasRenderer.cpp
@@ -0,0 +1,229 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ShareableCanvasRenderer.h"
+
+#include "mozilla/dom/WebGLTypes.h"
+#include "mozilla/gfx/2D.h"
+#include "mozilla/layers/TextureClientSharedSurface.h"
+#include "mozilla/layers/CompositableForwarder.h"
+#include "mozilla/layers/TextureForwarder.h"
+
+#include "ClientWebGLContext.h"
+#include "gfxUtils.h"
+#include "GLScreenBuffer.h"
+#include "nsICanvasRenderingContextInternal.h"
+#include "SharedSurfaceGL.h"
+
+using namespace mozilla::gfx;
+
+namespace mozilla {
+namespace layers {
+
+ShareableCanvasRenderer::ShareableCanvasRenderer() {
+ MOZ_COUNT_CTOR(ShareableCanvasRenderer);
+}
+
+ShareableCanvasRenderer::~ShareableCanvasRenderer() {
+ MOZ_COUNT_DTOR(ShareableCanvasRenderer);
+
+ mFrontBufferFromDesc = nullptr;
+ DisconnectClient();
+}
+
+void ShareableCanvasRenderer::Initialize(const CanvasRendererData& aData) {
+ CanvasRenderer::Initialize(aData);
+ mCanvasClient = nullptr;
+}
+
+void ShareableCanvasRenderer::ClearCachedResources() {
+ CanvasRenderer::ClearCachedResources();
+
+ if (mCanvasClient) {
+ mCanvasClient->Clear();
+ }
+}
+
+void ShareableCanvasRenderer::DisconnectClient() {
+ if (mCanvasClient) {
+ mCanvasClient->OnDetach();
+ mCanvasClient = nullptr;
+ }
+}
+
+RefPtr<layers::TextureClient> ShareableCanvasRenderer::GetFrontBufferFromDesc(
+ const layers::SurfaceDescriptor& desc, TextureFlags flags) {
+ if (mFrontBufferFromDesc && mFrontBufferDesc == desc)
+ return mFrontBufferFromDesc;
+ mFrontBufferFromDesc = nullptr;
+
+ // Test the validity of aAllocator
+ const auto& compositableForwarder = GetForwarder();
+ if (!compositableForwarder) {
+ return nullptr;
+ }
+ const auto& textureForwarder = compositableForwarder->GetTextureForwarder();
+
+ auto format = gfx::SurfaceFormat::R8G8B8X8;
+ if (!mData.mIsOpaque) {
+ format = gfx::SurfaceFormat::R8G8B8A8;
+
+ if (!mData.mIsAlphaPremult) {
+ flags |= TextureFlags::NON_PREMULTIPLIED;
+ }
+ }
+
+ mFrontBufferFromDesc = SharedSurfaceTextureData::CreateTextureClient(
+ desc, format, mData.mSize, flags, textureForwarder);
+ mFrontBufferDesc = desc;
+ return mFrontBufferFromDesc;
+}
+
+void ShareableCanvasRenderer::UpdateCompositableClient() {
+ if (!CreateCompositable()) {
+ return;
+ }
+
+ if (!IsDirty()) {
+ return;
+ }
+ ResetDirty();
+
+ const auto context = mData.GetContext();
+ if (!context) return;
+ const auto& provider = context->GetBufferProvider();
+ const auto& forwarder = GetForwarder();
+
+ // -
+
+ auto flags = TextureFlags::IMMUTABLE;
+ if (!YIsDown()) {
+ flags |= TextureFlags::ORIGIN_BOTTOM_LEFT;
+ }
+ if (IsOpaque()) {
+ flags |= TextureFlags::IS_OPAQUE;
+ }
+
+ // With remote texture push callback, a new pushed remote texture is notifiled
+ // from RemoteTextureMap to WebRenderImageHost.
+ if (mData.mRemoteTextureOwnerIdOfPushCallback) {
+ GetForwarder()->EnableRemoteTexturePushCallback(
+ mCanvasClient, *mData.mRemoteTextureOwnerIdOfPushCallback, mData.mSize,
+ flags);
+ EnsurePipeline();
+ return;
+ }
+
+ // -
+
+ const auto fnGetExistingTc =
+ [&](const Maybe<SurfaceDescriptor>& aDesc,
+ bool& aOutLostFrontTexture) -> RefPtr<TextureClient> {
+ if (aDesc) {
+ return GetFrontBufferFromDesc(*aDesc, flags);
+ }
+ if (provider) {
+ if (!provider->SetKnowsCompositor(forwarder, aOutLostFrontTexture)) {
+ gfxCriticalNote << "BufferProvider::SetForwarder failed";
+ return nullptr;
+ }
+ if (aOutLostFrontTexture) {
+ return nullptr;
+ }
+
+ return provider->GetTextureClient();
+ }
+ return nullptr;
+ };
+
+ // -
+
+ const auto fnMakeTcFromSnapshot = [&]() -> RefPtr<TextureClient> {
+ const auto& size = mData.mSize;
+
+ auto contentType = gfxContentType::COLOR;
+ if (!mData.mIsOpaque) {
+ contentType = gfxContentType::COLOR_ALPHA;
+ }
+ const auto surfaceFormat =
+ gfxPlatform::GetPlatform()->Optimal2DFormatForContent(contentType);
+
+ const auto tc =
+ mCanvasClient->CreateTextureClientForCanvas(surfaceFormat, size, flags);
+ if (!tc) {
+ return nullptr;
+ }
+
+ {
+ TextureClientAutoLock tcLock(tc, OpenMode::OPEN_WRITE_ONLY);
+ if (!tcLock.Succeeded()) {
+ return nullptr;
+ }
+
+ const RefPtr<DrawTarget> dt = tc->BorrowDrawTarget();
+
+ const bool requireAlphaPremult = false;
+ auto borrowed = BorrowSnapshot(requireAlphaPremult);
+ if (!borrowed) {
+ return nullptr;
+ }
+ dt->CopySurface(borrowed->mSurf, borrowed->mSurf->GetRect(), {0, 0});
+ }
+
+ return tc;
+ };
+
+ // -
+
+ {
+ FirePreTransactionCallback();
+
+ const auto desc = context->GetFrontBuffer(nullptr);
+ if (desc &&
+ desc->type() == SurfaceDescriptor::TSurfaceDescriptorRemoteTexture) {
+ const auto& forwarder = GetForwarder();
+ const auto& textureDesc = desc->get_SurfaceDescriptorRemoteTexture();
+ if (!mData.mIsAlphaPremult) {
+ flags |= TextureFlags::NON_PREMULTIPLIED;
+ }
+ EnsurePipeline();
+ forwarder->UseRemoteTexture(mCanvasClient, textureDesc.textureId(),
+ textureDesc.ownerId(), mData.mSize, flags);
+
+ FireDidTransactionCallback();
+ return;
+ }
+
+ EnsurePipeline();
+
+ // Let's see if we can get a no-copy TextureClient from the canvas.
+ bool lostFrontTexture = false;
+ auto tc = fnGetExistingTc(desc, lostFrontTexture);
+ if (lostFrontTexture) {
+ // Device reset could cause this.
+ return;
+ }
+ if (!tc) {
+ // Otherwise, snapshot the surface and copy into a TexClient.
+ tc = fnMakeTcFromSnapshot();
+ }
+ if (tc != mFrontBufferFromDesc) {
+ mFrontBufferFromDesc = nullptr;
+ }
+
+ if (!tc) {
+ NS_WARNING("Couldn't make TextureClient for CanvasRenderer.");
+ return;
+ }
+
+ mCanvasClient->UseTexture(tc);
+
+ FireDidTransactionCallback();
+ }
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/ShareableCanvasRenderer.h b/gfx/layers/ShareableCanvasRenderer.h
new file mode 100644
index 0000000000..a19cc436f7
--- /dev/null
+++ b/gfx/layers/ShareableCanvasRenderer.h
@@ -0,0 +1,63 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GFX_SHAREABLECANVASRENDERER_H
+#define GFX_SHAREABLECANVASRENDERER_H
+
+#include "CompositorTypes.h"
+#include "CanvasRenderer.h"
+#include "mozilla/layers/CanvasClient.h"
+
+namespace mozilla {
+namespace gl {
+class SurfaceFactory;
+} // namespace gl
+
+namespace gfx {
+class DrawTarget;
+} // namespace gfx
+
+namespace layers {
+
+class ShareableCanvasRenderer : public CanvasRenderer {
+ friend class CanvasClient2D;
+ friend class CanvasClientSharedSurface;
+
+ protected:
+ RefPtr<CanvasClient> mCanvasClient;
+
+ private:
+ layers::SurfaceDescriptor mFrontBufferDesc;
+ RefPtr<TextureClient> mFrontBufferFromDesc;
+
+ public:
+ ShareableCanvasRenderer();
+ virtual ~ShareableCanvasRenderer();
+
+ public:
+ void Initialize(const CanvasRendererData&) override;
+
+ virtual CompositableForwarder* GetForwarder() = 0;
+
+ virtual bool CreateCompositable() = 0;
+ virtual void EnsurePipeline() = 0;
+
+ void ClearCachedResources() override;
+ void DisconnectClient() override;
+
+ void UpdateCompositableClient();
+
+ CanvasClient* GetCanvasClient() { return mCanvasClient; }
+
+ private:
+ RefPtr<TextureClient> GetFrontBufferFromDesc(const layers::SurfaceDescriptor&,
+ TextureFlags);
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif
diff --git a/gfx/layers/SourceSurfaceSharedData.cpp b/gfx/layers/SourceSurfaceSharedData.cpp
new file mode 100644
index 0000000000..622f68855d
--- /dev/null
+++ b/gfx/layers/SourceSurfaceSharedData.cpp
@@ -0,0 +1,277 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "SourceSurfaceSharedData.h"
+
+#include "mozilla/Likely.h"
+#include "mozilla/StaticPrefs_image.h"
+#include "mozilla/Types.h" // for decltype
+#include "mozilla/layers/SharedSurfacesChild.h"
+#include "mozilla/layers/SharedSurfacesParent.h"
+#include "nsDebug.h" // for NS_ABORT_OOM
+
+#include "base/process_util.h"
+
+#ifdef DEBUG
+/**
+ * If defined, this makes SourceSurfaceSharedData::Finalize memory protect the
+ * underlying shared buffer in the producing process (the content or UI
+ * process). Given flushing the page table is expensive, and its utility is
+ * predominantly diagnostic (in case of overrun), turn it off by default.
+ */
+# define SHARED_SURFACE_PROTECT_FINALIZED
+#endif
+
+using namespace mozilla::layers;
+
+namespace mozilla {
+namespace gfx {
+
+void SourceSurfaceSharedDataWrapper::Init(const IntSize& aSize, int32_t aStride,
+ SurfaceFormat aFormat,
+ SharedMemoryBasic::Handle aHandle,
+ base::ProcessId aCreatorPid) {
+ MOZ_ASSERT(!mBuf);
+ mSize = aSize;
+ mStride = aStride;
+ mFormat = aFormat;
+ mCreatorPid = aCreatorPid;
+
+ size_t len = GetAlignedDataLength();
+ mBuf = MakeAndAddRef<SharedMemoryBasic>();
+ if (!mBuf->SetHandle(std::move(aHandle), ipc::SharedMemory::RightsReadOnly)) {
+ MOZ_CRASH("Invalid shared memory handle!");
+ }
+
+ bool mapped = EnsureMapped(len);
+ if ((sizeof(uintptr_t) <= 4 ||
+ StaticPrefs::image_mem_shared_unmap_force_enabled_AtStartup()) &&
+ len / 1024 >
+ StaticPrefs::image_mem_shared_unmap_min_threshold_kb_AtStartup()) {
+ mHandleLock.emplace("SourceSurfaceSharedDataWrapper::mHandleLock");
+
+ if (mapped) {
+ // Tracking at the initial mapping, and not just after the first use of
+ // the surface means we might get unmapped again before the next frame
+ // gets rendered if a low virtual memory condition persists.
+ SharedSurfacesParent::AddTracking(this);
+ }
+ } else if (!mapped) {
+ // We don't support unmapping for this surface, and we failed to map it.
+ NS_ABORT_OOM(len);
+ } else {
+ mBuf->CloseHandle();
+ }
+}
+
+void SourceSurfaceSharedDataWrapper::Init(SourceSurfaceSharedData* aSurface) {
+ MOZ_ASSERT(!mBuf);
+ MOZ_ASSERT(aSurface);
+ mSize = aSurface->mSize;
+ mStride = aSurface->mStride;
+ mFormat = aSurface->mFormat;
+ mCreatorPid = base::GetCurrentProcId();
+ mBuf = aSurface->mBuf;
+}
+
+bool SourceSurfaceSharedDataWrapper::EnsureMapped(size_t aLength) {
+ MOZ_ASSERT(!GetData());
+
+ while (!mBuf->Map(aLength)) {
+ nsTArray<RefPtr<SourceSurfaceSharedDataWrapper>> expired;
+ if (!SharedSurfacesParent::AgeOneGeneration(expired)) {
+ return false;
+ }
+ MOZ_ASSERT(!expired.Contains(this));
+ SharedSurfacesParent::ExpireMap(expired);
+ }
+
+ return true;
+}
+
+bool SourceSurfaceSharedDataWrapper::Map(MapType aMapType,
+ MappedSurface* aMappedSurface) {
+ uint8_t* dataPtr;
+
+ if (aMapType != MapType::READ) {
+ // The data may be write-protected
+ return false;
+ }
+
+ if (mHandleLock) {
+ MutexAutoLock lock(*mHandleLock);
+ dataPtr = GetData();
+ if (mMapCount == 0) {
+ SharedSurfacesParent::RemoveTracking(this);
+ if (!dataPtr) {
+ size_t len = GetAlignedDataLength();
+ if (!EnsureMapped(len)) {
+ NS_ABORT_OOM(len);
+ }
+ dataPtr = GetData();
+ }
+ }
+ ++mMapCount;
+ } else {
+ dataPtr = GetData();
+ ++mMapCount;
+ }
+
+ MOZ_ASSERT(dataPtr);
+ aMappedSurface->mData = dataPtr;
+ aMappedSurface->mStride = mStride;
+ return true;
+}
+
+void SourceSurfaceSharedDataWrapper::Unmap() {
+ if (mHandleLock) {
+ MutexAutoLock lock(*mHandleLock);
+ if (--mMapCount == 0) {
+ SharedSurfacesParent::AddTracking(this);
+ }
+ } else {
+ --mMapCount;
+ }
+ MOZ_ASSERT(mMapCount >= 0);
+}
+
+void SourceSurfaceSharedDataWrapper::ExpireMap() {
+ MutexAutoLock lock(*mHandleLock);
+ if (mMapCount == 0) {
+ mBuf->Unmap();
+ }
+}
+
+bool SourceSurfaceSharedData::Init(const IntSize& aSize, int32_t aStride,
+ SurfaceFormat aFormat,
+ bool aShare /* = true */) {
+ mSize = aSize;
+ mStride = aStride;
+ mFormat = aFormat;
+
+ size_t len = GetAlignedDataLength();
+ mBuf = new SharedMemoryBasic();
+ if (NS_WARN_IF(!mBuf->Create(len)) || NS_WARN_IF(!mBuf->Map(len))) {
+ mBuf = nullptr;
+ return false;
+ }
+
+ if (aShare) {
+ layers::SharedSurfacesChild::Share(this);
+ }
+
+ return true;
+}
+
+void SourceSurfaceSharedData::SizeOfExcludingThis(MallocSizeOf aMallocSizeOf,
+ SizeOfInfo& aInfo) const {
+ MutexAutoLock lock(mMutex);
+ aInfo.AddType(SurfaceType::DATA_SHARED);
+ if (mBuf) {
+ aInfo.mNonHeapBytes = GetAlignedDataLength();
+ }
+ if (!mClosed) {
+ aInfo.mExternalHandles = 1;
+ }
+ Maybe<wr::ExternalImageId> extId = SharedSurfacesChild::GetExternalId(this);
+ if (extId) {
+ aInfo.mExternalId = wr::AsUint64(extId.ref());
+ }
+}
+
+uint8_t* SourceSurfaceSharedData::GetDataInternal() const {
+ mMutex.AssertCurrentThreadOwns();
+
+ // If we have an old buffer lingering, it is because we get reallocated to
+ // get a new handle to share, but there were still active mappings.
+ if (MOZ_UNLIKELY(mOldBuf)) {
+ MOZ_ASSERT(mMapCount > 0);
+ MOZ_ASSERT(mFinalized);
+ return static_cast<uint8_t*>(mOldBuf->memory());
+ }
+ return static_cast<uint8_t*>(mBuf->memory());
+}
+
+nsresult SourceSurfaceSharedData::CloneHandle(
+ SharedMemoryBasic::Handle& aHandle) {
+ MutexAutoLock lock(mMutex);
+ MOZ_ASSERT(mHandleCount > 0);
+
+ if (mClosed) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ aHandle = mBuf->CloneHandle();
+ if (MOZ_UNLIKELY(!aHandle)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ return NS_OK;
+}
+
+void SourceSurfaceSharedData::CloseHandleInternal() {
+ mMutex.AssertCurrentThreadOwns();
+
+ if (mClosed) {
+ MOZ_ASSERT(mHandleCount == 0);
+ MOZ_ASSERT(mShared);
+ return;
+ }
+
+ if (mShared) {
+ mBuf->CloseHandle();
+ mClosed = true;
+ }
+}
+
+bool SourceSurfaceSharedData::ReallocHandle() {
+ MutexAutoLock lock(mMutex);
+ MOZ_ASSERT(mHandleCount > 0);
+ MOZ_ASSERT(mClosed);
+
+ if (NS_WARN_IF(!mFinalized)) {
+ // We haven't finished populating the surface data yet, which means we are
+ // out of luck, as we have no means of synchronizing with the producer to
+ // write new data to a new buffer. This should be fairly rare, caused by a
+ // crash in the GPU process, while we were decoding an image.
+ return false;
+ }
+
+ size_t len = GetAlignedDataLength();
+ RefPtr<SharedMemoryBasic> buf = new SharedMemoryBasic();
+ if (NS_WARN_IF(!buf->Create(len)) || NS_WARN_IF(!buf->Map(len))) {
+ return false;
+ }
+
+ size_t copyLen = GetDataLength();
+ memcpy(buf->memory(), mBuf->memory(), copyLen);
+#ifdef SHARED_SURFACE_PROTECT_FINALIZED
+ buf->Protect(static_cast<char*>(buf->memory()), len, RightsRead);
+#endif
+
+ if (mMapCount > 0 && !mOldBuf) {
+ mOldBuf = std::move(mBuf);
+ }
+ mBuf = std::move(buf);
+ mClosed = false;
+ mShared = false;
+ return true;
+}
+
+void SourceSurfaceSharedData::Finalize() {
+ MutexAutoLock lock(mMutex);
+ MOZ_ASSERT(!mFinalized);
+
+#ifdef SHARED_SURFACE_PROTECT_FINALIZED
+ size_t len = GetAlignedDataLength();
+ mBuf->Protect(static_cast<char*>(mBuf->memory()), len, RightsRead);
+#endif
+
+ mFinalized = true;
+}
+
+} // namespace gfx
+} // namespace mozilla
diff --git a/gfx/layers/SourceSurfaceSharedData.h b/gfx/layers/SourceSurfaceSharedData.h
new file mode 100644
index 0000000000..73743a839d
--- /dev/null
+++ b/gfx/layers/SourceSurfaceSharedData.h
@@ -0,0 +1,345 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_GFX_SOURCESURFACESHAREDDATA_H_
+#define MOZILLA_GFX_SOURCESURFACESHAREDDATA_H_
+
+#include "mozilla/gfx/2D.h"
+#include "mozilla/Mutex.h"
+#include "mozilla/ipc/SharedMemoryBasic.h"
+#include "nsExpirationTracker.h"
+
+namespace mozilla {
+namespace gfx {
+
+class SourceSurfaceSharedData;
+
+/**
+ * This class is used to wrap shared (as in process) data buffers allocated by
+ * a SourceSurfaceSharedData object. It may live in the same process or a
+ * different process from the actual SourceSurfaceSharedData object.
+ *
+ * If it is in the same process, mBuf is the same object as that in the surface.
+ * It is a useful abstraction over just using the surface directly, because it
+ * can have a different lifetime from the surface; if the surface gets freed,
+ * consumers may continue accessing the data in the buffer. Releasing the
+ * original surface is a signal which feeds into SharedSurfacesParent to decide
+ * to release the SourceSurfaceSharedDataWrapper.
+ *
+ * If it is in a different process, mBuf is a new SharedMemoryBasic object which
+ * mapped in the given shared memory handle as read only memory.
+ */
+class SourceSurfaceSharedDataWrapper final : public DataSourceSurface {
+ typedef mozilla::ipc::SharedMemoryBasic SharedMemoryBasic;
+
+ public:
+ MOZ_DECLARE_REFCOUNTED_VIRTUAL_TYPENAME(SourceSurfaceSharedDataWrapper,
+ override)
+
+ SourceSurfaceSharedDataWrapper()
+ : mStride(0),
+ mConsumers(0),
+ mFormat(SurfaceFormat::UNKNOWN),
+ mCreatorPid(0),
+ mCreatorRef(true) {}
+
+ void Init(const IntSize& aSize, int32_t aStride, SurfaceFormat aFormat,
+ SharedMemoryBasic::Handle aHandle, base::ProcessId aCreatorPid);
+
+ void Init(SourceSurfaceSharedData* aSurface);
+
+ base::ProcessId GetCreatorPid() const { return mCreatorPid; }
+
+ int32_t Stride() override { return mStride; }
+
+ SurfaceType GetType() const override {
+ return SurfaceType::DATA_SHARED_WRAPPER;
+ }
+ IntSize GetSize() const override { return mSize; }
+ SurfaceFormat GetFormat() const override { return mFormat; }
+
+ uint8_t* GetData() override { return static_cast<uint8_t*>(mBuf->memory()); }
+
+ bool OnHeap() const override { return false; }
+
+ bool Map(MapType aMapType, MappedSurface* aMappedSurface) final;
+
+ void Unmap() final;
+
+ void ExpireMap();
+
+ bool AddConsumer() { return ++mConsumers == 1; }
+
+ bool RemoveConsumer(bool aForCreator) {
+ MOZ_ASSERT(mConsumers > 0);
+ if (aForCreator) {
+ if (!mCreatorRef) {
+ MOZ_ASSERT_UNREACHABLE("Already released creator reference!");
+ return false;
+ }
+ mCreatorRef = false;
+ }
+ return --mConsumers == 0;
+ }
+
+ uint32_t GetConsumers() const {
+ MOZ_ASSERT(mConsumers > 0);
+ return mConsumers;
+ }
+
+ bool HasCreatorRef() const { return mCreatorRef; }
+
+ nsExpirationState* GetExpirationState() { return &mExpirationState; }
+
+ private:
+ size_t GetDataLength() const {
+ return static_cast<size_t>(mStride) * mSize.height;
+ }
+
+ size_t GetAlignedDataLength() const {
+ return mozilla::ipc::SharedMemory::PageAlignedSize(GetDataLength());
+ }
+
+ bool EnsureMapped(size_t aLength);
+
+ // Protects mapping and unmapping of mBuf.
+ Maybe<Mutex> mHandleLock;
+ nsExpirationState mExpirationState;
+ int32_t mStride;
+ uint32_t mConsumers;
+ IntSize mSize;
+ RefPtr<SharedMemoryBasic> mBuf;
+ SurfaceFormat mFormat;
+ base::ProcessId mCreatorPid;
+ bool mCreatorRef;
+};
+
+/**
+ * This class is used to wrap shared (as in process) data buffers used by a
+ * source surface.
+ */
+class SourceSurfaceSharedData : public DataSourceSurface {
+ typedef mozilla::ipc::SharedMemoryBasic SharedMemoryBasic;
+
+ public:
+ MOZ_DECLARE_REFCOUNTED_VIRTUAL_TYPENAME(SourceSurfaceSharedData, override)
+
+ SourceSurfaceSharedData()
+ : mMutex("SourceSurfaceSharedData"),
+ mStride(0),
+ mHandleCount(0),
+ mFormat(SurfaceFormat::UNKNOWN),
+ mClosed(false),
+ mFinalized(false),
+ mShared(false) {}
+
+ /**
+ * Initialize the surface by creating a shared memory buffer with a size
+ * determined by aSize, aStride and aFormat. If aShare is true, it will also
+ * immediately attempt to share the surface with the GPU process via
+ * SharedSurfacesChild.
+ */
+ bool Init(const IntSize& aSize, int32_t aStride, SurfaceFormat aFormat,
+ bool aShare = true);
+
+ uint8_t* GetData() final {
+ MutexAutoLock lock(mMutex);
+ return GetDataInternal();
+ }
+
+ int32_t Stride() final { return mStride; }
+
+ SurfaceType GetType() const override { return SurfaceType::DATA_SHARED; }
+ IntSize GetSize() const final { return mSize; }
+ SurfaceFormat GetFormat() const final { return mFormat; }
+
+ void SizeOfExcludingThis(MallocSizeOf aMallocSizeOf,
+ SizeOfInfo& aInfo) const final;
+
+ bool OnHeap() const final { return false; }
+
+ /**
+ * Although Map (and Moz2D in general) isn't normally threadsafe,
+ * we want to allow it for SourceSurfaceSharedData since it should
+ * always be fine (for reading at least).
+ *
+ * This is the same as the base class implementation except using
+ * mMapCount instead of mIsMapped since that breaks for multithread.
+ *
+ * Additionally if a reallocation happened while there were active
+ * mappings, then we guarantee that GetData will continue to return
+ * the same data pointer by retaining the old shared buffer until
+ * the last mapping is freed via Unmap.
+ */
+ bool Map(MapType aMapType, MappedSurface* aMappedSurface) final {
+ MutexAutoLock lock(mMutex);
+ if (mFinalized && aMapType != MapType::READ) {
+ // Once finalized the data may be write-protected
+ return false;
+ }
+ ++mMapCount;
+ aMappedSurface->mData = GetDataInternal();
+ aMappedSurface->mStride = mStride;
+ return true;
+ }
+
+ void Unmap() final {
+ MutexAutoLock lock(mMutex);
+ MOZ_ASSERT(mMapCount > 0);
+ if (--mMapCount == 0) {
+ mOldBuf = nullptr;
+ }
+ }
+
+ /**
+ * Get a handle to share to another process for this buffer. Returns:
+ * NS_OK -- success, aHandle is valid.
+ * NS_ERROR_NOT_AVAILABLE -- handle was closed, need to reallocate.
+ * NS_ERROR_FAILURE -- failed to create a handle to share.
+ */
+ nsresult CloneHandle(SharedMemoryBasic::Handle& aHandle);
+
+ /**
+ * Indicates the buffer is not expected to be shared with any more processes.
+ * May release the handle if possible (see CloseHandleInternal).
+ */
+ void FinishedSharing() {
+ MutexAutoLock lock(mMutex);
+ mShared = true;
+ CloseHandleInternal();
+ }
+
+ /**
+ * Indicates whether or not the buffer can be shared with another process
+ * without reallocating. Note that this is racy and should only be used for
+ * informational/reporting purposes.
+ */
+ bool CanShare() const {
+ MutexAutoLock lock(mMutex);
+ return !mClosed;
+ }
+
+ /**
+ * Allocate a new shared memory buffer so that we can get a new handle for
+ * sharing to new processes. CloneHandle must have failed with
+ * NS_ERROR_NOT_AVAILABLE in order for this to be safe to call. Returns true
+ * if the operation succeeds. If it fails, there is no state change.
+ */
+ bool ReallocHandle();
+
+ /**
+ * Signals we have finished writing to the buffer and it may be marked as
+ * read only.
+ */
+ void Finalize();
+
+ /**
+ * Indicates whether or not the buffer can change. If this returns true, it is
+ * guaranteed to continue to do so for the remainder of the surface's life.
+ */
+ bool IsFinalized() const {
+ MutexAutoLock lock(mMutex);
+ return mFinalized;
+ }
+
+ /**
+ * Yields a dirty rect of what has changed since it was last called.
+ */
+ Maybe<IntRect> TakeDirtyRect() final {
+ MutexAutoLock lock(mMutex);
+ if (mDirtyRect) {
+ Maybe<IntRect> ret = std::move(mDirtyRect);
+ return ret;
+ }
+ return Nothing();
+ }
+
+ /**
+ * Increment the invalidation counter.
+ */
+ void Invalidate(const IntRect& aDirtyRect) final {
+ MutexAutoLock lock(mMutex);
+ if (!aDirtyRect.IsEmpty()) {
+ if (mDirtyRect) {
+ mDirtyRect->UnionRect(mDirtyRect.ref(), aDirtyRect);
+ } else {
+ mDirtyRect = Some(aDirtyRect);
+ }
+ } else {
+ mDirtyRect = Some(IntRect(IntPoint(0, 0), mSize));
+ }
+ MOZ_ASSERT_IF(mDirtyRect, !mDirtyRect->IsEmpty());
+ }
+
+ /**
+ * While a HandleLock exists for the given surface, the shared memory handle
+ * cannot be released.
+ */
+ class MOZ_STACK_CLASS HandleLock final {
+ public:
+ explicit HandleLock(SourceSurfaceSharedData* aSurface)
+ : mSurface(aSurface) {
+ mSurface->LockHandle();
+ }
+
+ ~HandleLock() { mSurface->UnlockHandle(); }
+
+ private:
+ RefPtr<SourceSurfaceSharedData> mSurface;
+ };
+
+ protected:
+ virtual ~SourceSurfaceSharedData() = default;
+
+ private:
+ friend class SourceSurfaceSharedDataWrapper;
+
+ void LockHandle() {
+ MutexAutoLock lock(mMutex);
+ ++mHandleCount;
+ }
+
+ void UnlockHandle() {
+ MutexAutoLock lock(mMutex);
+ MOZ_ASSERT(mHandleCount > 0);
+ --mHandleCount;
+ mShared = true;
+ CloseHandleInternal();
+ }
+
+ uint8_t* GetDataInternal() const;
+
+ size_t GetDataLength() const {
+ return static_cast<size_t>(mStride) * mSize.height;
+ }
+
+ size_t GetAlignedDataLength() const {
+ return mozilla::ipc::SharedMemory::PageAlignedSize(GetDataLength());
+ }
+
+ /**
+ * Attempt to close the handle. Only if the buffer has been both finalized
+ * and we have completed sharing will it be released.
+ */
+ void CloseHandleInternal();
+
+ mutable Mutex mMutex MOZ_UNANNOTATED;
+ int32_t mStride;
+ int32_t mHandleCount;
+ Maybe<IntRect> mDirtyRect;
+ IntSize mSize;
+ RefPtr<SharedMemoryBasic> mBuf;
+ RefPtr<SharedMemoryBasic> mOldBuf;
+ SurfaceFormat mFormat;
+ bool mClosed : 1;
+ bool mFinalized : 1;
+ bool mShared : 1;
+};
+
+} // namespace gfx
+} // namespace mozilla
+
+#endif /* MOZILLA_GFX_SOURCESURFACESHAREDDATA_H_ */
diff --git a/gfx/layers/SurfacePool.h b/gfx/layers/SurfacePool.h
new file mode 100644
index 0000000000..eecb398d85
--- /dev/null
+++ b/gfx/layers/SurfacePool.h
@@ -0,0 +1,76 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_SurfacePool_h
+#define mozilla_layers_SurfacePool_h
+
+#include "mozilla/Maybe.h"
+#include "mozilla/ThreadSafeWeakPtr.h"
+
+#include "GLTypes.h"
+#include "nsISupportsImpl.h"
+#include "nsRegion.h"
+
+namespace mozilla {
+
+namespace gl {
+class GLContext;
+} // namespace gl
+
+namespace layers {
+
+class SurfacePoolHandle;
+
+// A pool of surfaces for NativeLayers. Manages GL resources. Since GLContexts
+// are bound to their creator thread, a pool should not be shared across
+// threads. Call Create() to create an instance. Call GetHandleForGL() to obtain
+// a handle that can be passed to NativeLayerRoot::CreateLayer.
+class SurfacePool {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(SurfacePool);
+
+#if defined(XP_MACOSX) || defined(MOZ_WAYLAND)
+ static RefPtr<SurfacePool> Create(size_t aPoolSizeLimit);
+#endif
+
+ // aGL can be nullptr.
+ virtual RefPtr<SurfacePoolHandle> GetHandleForGL(gl::GLContext* aGL) = 0;
+ virtual void DestroyGLResourcesForContext(gl::GLContext* aGL) = 0;
+
+ protected:
+ virtual ~SurfacePool() = default;
+};
+
+class SurfacePoolHandleCA;
+class SurfacePoolHandleWayland;
+
+// A handle to the process-wide surface pool. Users should create one handle per
+// OS window, and call OnBeginFrame() and OnEndFrame() on the handle at
+// appropriate times. OnBeginFrame() and OnEndFrame() should be called on the
+// thread that the surface pool was created on.
+// These handles are stored on NativeLayers that are created with them and keep
+// the SurfacePool alive.
+class SurfacePoolHandle {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(SurfacePoolHandle);
+ virtual SurfacePoolHandleCA* AsSurfacePoolHandleCA() { return nullptr; }
+ virtual SurfacePoolHandleWayland* AsSurfacePoolHandleWayland() {
+ return nullptr;
+ }
+
+ virtual RefPtr<SurfacePool> Pool() = 0;
+
+ // Should be called every frame, in order to do rate-limited cleanup tasks.
+ virtual void OnBeginFrame() = 0;
+ virtual void OnEndFrame() = 0;
+
+ protected:
+ virtual ~SurfacePoolHandle() = default;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_SurfacePool_h
diff --git a/gfx/layers/SurfacePoolCA.h b/gfx/layers/SurfacePoolCA.h
new file mode 100644
index 0000000000..97344f7d06
--- /dev/null
+++ b/gfx/layers/SurfacePoolCA.h
@@ -0,0 +1,288 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_SurfacePoolCA_h
+#define mozilla_layers_SurfacePoolCA_h
+
+#include <IOSurface/IOSurface.h>
+
+#include <deque>
+#include <unordered_map>
+
+#include "mozilla/Atomics.h"
+#include "mozilla/DataMutex.h"
+
+#include "mozilla/layers/SurfacePool.h"
+#include "CFTypeRefPtr.h"
+#include "MozFramebuffer.h"
+#include "nsISupportsImpl.h"
+
+namespace mozilla {
+
+namespace gl {
+class MozFramebuffer;
+} // namespace gl
+
+namespace layers {
+
+class SurfacePoolHandleCA;
+struct SurfacePoolCAWrapperForGL;
+
+// An implementation of SurfacePool for IOSurfaces and GL framebuffers.
+// The goal of having this pool is to avoid creating and destroying IOSurfaces
+// and framebuffers frequently, because doing so is expensive.
+// SurfacePoolCA is threadsafe. All its data is wrapped inside LockedPool, and
+// each access to LockedPool is guarded with a lock through DataMutex.
+//
+// The pool satisfies the following requirements:
+// - It can be shared across windows, even across windows with different
+// GLContexts.
+// - The number of unused surfaces that are available for recycling is capped
+// to a fixed value per pool, regardless of how many windows use that pool.
+// - When all windows are closed (all handles are gone), no surfaces are kept
+// alive (the pool is destroyed).
+// - There is an explicit way of deleting GL resources for a GLContext so that
+// it can happen at a deterministic time on the right thread.
+// - Additionally, once a GLContext is no longer being used in any window
+// (really: any pool handle), all surface-associated GL resources of that
+// context are destroyed.
+// - For every IOSurface, only one set of GL resources is in existence at any
+// given time. We don't want there to be framebuffers in two different
+// GLContexts for one surface.
+// - We do not want to recycle an IOSurface that currently has GL resources of
+// context A for a pool handle that uses context B.
+// - We need to delay IOSurface recycling until the window server is done with
+// the surface (`!IOSurfaceIsInUse(surf)`)
+class SurfacePoolCA final : public SurfacePool {
+ public:
+ // Get a handle for a new window. aGL can be nullptr.
+ RefPtr<SurfacePoolHandle> GetHandleForGL(gl::GLContext* aGL) override;
+
+ // Destroy all GL resources associated with aGL managed by this pool.
+ void DestroyGLResourcesForContext(gl::GLContext* aGL) override;
+
+ private:
+ friend struct SurfacePoolCAWrapperForGL;
+ friend class SurfacePoolHandleCA;
+ friend RefPtr<SurfacePool> SurfacePool::Create(size_t aPoolSizeLimit);
+
+ explicit SurfacePoolCA(size_t aPoolSizeLimit);
+ ~SurfacePoolCA() override;
+
+ // Get an existing surface of aSize from the pool or create a new surface.
+ // The returned surface is guaranteed not to be in use by the window server.
+ CFTypeRefPtr<IOSurfaceRef> ObtainSurfaceFromPool(const gfx::IntSize& aSize,
+ gl::GLContext* aGL);
+
+ // Place a surface that was previously obtained from this pool back into the
+ // pool. aSurface may or may not be in use by the window server.
+ void ReturnSurfaceToPool(CFTypeRefPtr<IOSurfaceRef> aSurface);
+
+ // Re-run checks whether the window server still uses IOSurfaces which are
+ // eligible for recycling. The purpose of the "generation" counter is to
+ // reduce the number of calls to IOSurfaceIsInUse in a scenario where many
+ // windows / handles are calling CollectPendingSurfaces in the same frame
+ // (i.e. multiple simultaneously-animating windows).
+ uint64_t CollectPendingSurfaces(uint64_t aCheckGenerationsUpTo);
+
+ // Enforce the pool size limit by evicting surfaces as necessary. This should
+ // happen at the end of the frame so that we can temporarily exceed the limit
+ // within a frame.
+ void EnforcePoolSizeLimit();
+
+ // Get or create the framebuffer for the given surface and GL context.
+ // The returned framebuffer handle will become invalid once
+ // DestroyGLResourcesForContext or DecrementGLContextHandleCount are called.
+ // The framebuffer's depth buffer (if present) may be shared between multiple
+ // framebuffers! Do not assume anything about the depth buffer's existing
+ // contents (i.e. clear it at the beginning of the draw), and do not
+ // interleave drawing commands to different framebuffers in such a way that
+ // the shared depth buffer could cause trouble.
+ Maybe<GLuint> GetFramebufferForSurface(CFTypeRefPtr<IOSurfaceRef> aSurface,
+ gl::GLContext* aGL,
+ bool aNeedsDepthBuffer);
+
+ // Called by the destructor of SurfacePoolCAWrapperForGL so that we can clear
+ // our weak reference to it and delete GL resources.
+ void OnWrapperDestroyed(gl::GLContext* aGL,
+ SurfacePoolCAWrapperForGL* aWrapper);
+
+ // The actual pool implementation lives in LockedPool, which is accessed in
+ // a thread-safe manner.
+ struct LockedPool {
+ explicit LockedPool(size_t aPoolSizeLimit);
+ LockedPool(LockedPool&&) = default;
+ ~LockedPool();
+
+ RefPtr<SurfacePoolCAWrapperForGL> GetWrapperForGL(SurfacePoolCA* aPool,
+ gl::GLContext* aGL);
+ void DestroyGLResourcesForContext(gl::GLContext* aGL);
+
+ CFTypeRefPtr<IOSurfaceRef> ObtainSurfaceFromPool(const gfx::IntSize& aSize,
+ gl::GLContext* aGL);
+ void ReturnSurfaceToPool(CFTypeRefPtr<IOSurfaceRef> aSurface);
+ uint64_t CollectPendingSurfaces(uint64_t aCheckGenerationsUpTo);
+ void EnforcePoolSizeLimit();
+ Maybe<GLuint> GetFramebufferForSurface(CFTypeRefPtr<IOSurfaceRef> aSurface,
+ gl::GLContext* aGL,
+ bool aNeedsDepthBuffer);
+ void OnWrapperDestroyed(gl::GLContext* aGL,
+ SurfacePoolCAWrapperForGL* aWrapper);
+ uint64_t EstimateTotalMemory();
+
+ uint64_t mCollectionGeneration = 0;
+
+ protected:
+ struct GLResourcesForSurface {
+ RefPtr<gl::GLContext> mGLContext; // non-null
+ UniquePtr<gl::MozFramebuffer> mFramebuffer; // non-null
+ };
+
+ struct SurfacePoolEntry {
+ gfx::IntSize mSize;
+ CFTypeRefPtr<IOSurfaceRef> mIOSurface; // non-null
+ Maybe<GLResourcesForSurface> mGLResources;
+ };
+
+ struct PendingSurfaceEntry {
+ SurfacePoolEntry mEntry;
+ // The value of LockedPool::mCollectionGeneration at the time
+ // IOSurfaceIsInUse was last called for mEntry.mIOSurface.
+ uint64_t mPreviousCheckGeneration;
+ // The number of times an IOSurfaceIsInUse check has been performed.
+ uint64_t mCheckCount;
+ };
+
+ template <typename F>
+ void MutateEntryStorage(const char* aMutationType,
+ const gfx::IntSize& aSize, F aFn);
+
+ template <typename F>
+ void ForEachEntry(F aFn);
+
+ bool CanRecycleSurfaceForRequest(const SurfacePoolEntry& aEntry,
+ const gfx::IntSize& aSize,
+ gl::GLContext* aGL);
+
+ RefPtr<gl::DepthAndStencilBuffer> GetDepthBufferForSharing(
+ gl::GLContext* aGL, const gfx::IntSize& aSize);
+ UniquePtr<gl::MozFramebuffer> CreateFramebufferForTexture(
+ gl::GLContext* aGL, const gfx::IntSize& aSize, GLuint aTexture,
+ bool aNeedsDepthBuffer);
+
+ // Every IOSurface that is managed by the pool is wrapped in a
+ // SurfacePoolEntry object. Every entry is stored in one of three buckets at
+ // any given time: mInUseEntries, mPendingEntries, or mAvailableEntries. All
+ // mutations to these buckets are performed via calls to
+ // MutateEntryStorage(). Entries can move between the buckets in the
+ // following ways:
+ //
+ // [new]
+ // | Create
+ // v
+ // +----------------------------------------------------------------+
+ // | mInUseEntries |
+ // +------+------------------------------+--------------------------+
+ // | ^ | Start waiting for
+ // | | Recycle v
+ // | | +-----------------------------+
+ // | | | mPendingEntries |
+ // | | +--+--------------------+-----+
+ // | Retain | | Stop waiting for |
+ // v | v |
+ // +-------------------+-------------------------+ |
+ // | mAvailableEntries | |
+ // +-----------------------------+---------------+ |
+ // | Evict | Eject
+ // v v
+ // [destroyed] [destroyed]
+ //
+ // Each arrow corresponds to one invocation of MutateEntryStorage() with the
+ // arrow's label passed as the aMutationType string parameter.
+
+ // Stores the entries for surfaces that are in use by NativeLayerCA, i.e. an
+ // entry is inside mInUseEntries between calls to ObtainSurfaceFromPool()
+ // and ReturnSurfaceToPool().
+ std::unordered_map<CFTypeRefPtr<IOSurfaceRef>, SurfacePoolEntry>
+ mInUseEntries;
+
+ // Stores entries which are no longer in use by NativeLayerCA but are still
+ // in use by the window server, i.e. for which
+ // IOSurfaceIsInUse(pendingSurfaceEntry.mEntry.mIOSurface.get()) still
+ // returns true. These entries are checked once per frame inside
+ // CollectPendingSurfaces(), and returned to mAvailableEntries once the
+ // window server is done.
+ nsTArray<PendingSurfaceEntry> mPendingEntries;
+
+ // Stores entries which are available for recycling. These entries are not
+ // in use by a NativeLayerCA or by the window server.
+ nsTArray<SurfacePoolEntry> mAvailableEntries;
+
+ // Keeps weak references to SurfacePoolCAWrapperForGL instances.
+ // For each GLContext* value (including nullptr), only one wrapper can
+ // exist at any time. The wrapper keeps a strong reference to us and
+ // notifies us when it gets destroyed. At that point we can call
+ // DestroyGLResourcesForContext because we know no other SurfaceHandles for
+ // that context exist.
+ std::unordered_map<gl::GLContext*, SurfacePoolCAWrapperForGL*> mWrappers;
+ size_t mPoolSizeLimit = 0;
+
+ struct DepthBufferEntry {
+ RefPtr<gl::GLContext> mGLContext;
+ gfx::IntSize mSize;
+ WeakPtr<gl::DepthAndStencilBuffer> mBuffer;
+ };
+
+ nsTArray<DepthBufferEntry> mDepthBuffers;
+ };
+
+ DataMutex<LockedPool> mPool;
+};
+
+// One process-wide instance per (SurfacePoolCA*, GLContext*) pair.
+// Keeps the SurfacePool alive, and the SurfacePool has a weak reference to the
+// wrapper so that it can ensure that there's only one wrapper for it per
+// GLContext* at any time.
+struct SurfacePoolCAWrapperForGL {
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(SurfacePoolCAWrapperForGL);
+
+ const RefPtr<SurfacePoolCA> mPool; // non-null
+ const RefPtr<gl::GLContext> mGL; // can be null
+
+ SurfacePoolCAWrapperForGL(SurfacePoolCA* aPool, gl::GLContext* aGL)
+ : mPool(aPool), mGL(aGL) {}
+
+ protected:
+ ~SurfacePoolCAWrapperForGL() { mPool->OnWrapperDestroyed(mGL, this); }
+};
+
+// A surface pool handle that is stored on NativeLayerCA and keeps the
+// SurfacePool alive.
+class SurfacePoolHandleCA final : public SurfacePoolHandle {
+ public:
+ SurfacePoolHandleCA* AsSurfacePoolHandleCA() override { return this; }
+ const auto& gl() { return mPoolWrapper->mGL; }
+ CFTypeRefPtr<IOSurfaceRef> ObtainSurfaceFromPool(const gfx::IntSize& aSize);
+ void ReturnSurfaceToPool(CFTypeRefPtr<IOSurfaceRef> aSurface);
+ Maybe<GLuint> GetFramebufferForSurface(CFTypeRefPtr<IOSurfaceRef> aSurface,
+ bool aNeedsDepthBuffer);
+ RefPtr<SurfacePool> Pool() override { return mPoolWrapper->mPool; }
+ void OnBeginFrame() override;
+ void OnEndFrame() override;
+
+ private:
+ friend class SurfacePoolCA;
+ SurfacePoolHandleCA(RefPtr<SurfacePoolCAWrapperForGL>&& aPoolWrapper,
+ uint64_t aCurrentCollectionGeneration);
+ ~SurfacePoolHandleCA() override;
+
+ const RefPtr<SurfacePoolCAWrapperForGL> mPoolWrapper;
+ DataMutex<uint64_t> mPreviousFrameCollectionGeneration;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_SurfacePoolCA_h
diff --git a/gfx/layers/SurfacePoolCA.mm b/gfx/layers/SurfacePoolCA.mm
new file mode 100644
index 0000000000..f5fc0201bf
--- /dev/null
+++ b/gfx/layers/SurfacePoolCA.mm
@@ -0,0 +1,440 @@
+/* -*- Mode: C++; tab-width: 20; indent-tabs-mode: nullptr; c-basic-offset: 2 -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/layers/SurfacePoolCA.h"
+
+#import <CoreVideo/CVPixelBuffer.h>
+
+#include <algorithm>
+#include <unordered_set>
+#include <utility>
+
+#include "mozilla/ProfilerLabels.h"
+#include "mozilla/ProfilerMarkers.h"
+#include "mozilla/StaticMutex.h"
+#include "mozilla/StaticPrefs_gfx.h"
+
+#include "GLContextCGL.h"
+#include "MozFramebuffer.h"
+#include "ScopedGLHelpers.h"
+
+namespace mozilla {
+namespace layers {
+
+using gfx::IntPoint;
+using gfx::IntRect;
+using gfx::IntRegion;
+using gfx::IntSize;
+using gl::GLContext;
+using gl::GLContextCGL;
+
+/* static */ RefPtr<SurfacePool> SurfacePool::Create(size_t aPoolSizeLimit) {
+ return new SurfacePoolCA(aPoolSizeLimit);
+}
+
+// SurfacePoolCA::LockedPool
+
+SurfacePoolCA::LockedPool::LockedPool(size_t aPoolSizeLimit) : mPoolSizeLimit(aPoolSizeLimit) {}
+
+SurfacePoolCA::LockedPool::~LockedPool() {
+ MOZ_RELEASE_ASSERT(mWrappers.empty(),
+ "Any outstanding wrappers should have kept the surface pool alive");
+ MOZ_RELEASE_ASSERT(mInUseEntries.empty(),
+ "Leak! No more surfaces should be in use at this point.");
+ // Remove all entries in mPendingEntries and mAvailableEntries.
+ MutateEntryStorage("Clear", {}, [&]() {
+ mPendingEntries.Clear();
+ mAvailableEntries.Clear();
+ });
+}
+
+RefPtr<SurfacePoolCAWrapperForGL> SurfacePoolCA::LockedPool::GetWrapperForGL(SurfacePoolCA* aPool,
+ GLContext* aGL) {
+ auto& wrapper = mWrappers[aGL];
+ if (!wrapper) {
+ wrapper = new SurfacePoolCAWrapperForGL(aPool, aGL);
+ }
+ return wrapper;
+}
+
+void SurfacePoolCA::LockedPool::DestroyGLResourcesForContext(GLContext* aGL) {
+ ForEachEntry([&](SurfacePoolEntry& entry) {
+ if (entry.mGLResources && entry.mGLResources->mGLContext == aGL) {
+ entry.mGLResources = Nothing();
+ }
+ });
+ mDepthBuffers.RemoveElementsBy(
+ [&](const DepthBufferEntry& entry) { return entry.mGLContext == aGL; });
+}
+
+template <typename F>
+void SurfacePoolCA::LockedPool::MutateEntryStorage(const char* aMutationType,
+ const gfx::IntSize& aSize, F aFn) {
+ [[maybe_unused]] size_t inUseCountBefore = mInUseEntries.size();
+ [[maybe_unused]] size_t pendingCountBefore = mPendingEntries.Length();
+ [[maybe_unused]] size_t availableCountBefore = mAvailableEntries.Length();
+ [[maybe_unused]] TimeStamp before = TimeStamp::Now();
+
+ aFn();
+
+ if (profiler_thread_is_being_profiled_for_markers()) {
+ PROFILER_MARKER_TEXT(
+ "SurfacePool", GRAPHICS, MarkerTiming::IntervalUntilNowFrom(before),
+ nsPrintfCString("%d -> %d in use | %d -> %d waiting for | %d -> %d "
+ "available | %s %dx%d | %dMB total memory",
+ int(inUseCountBefore), int(mInUseEntries.size()), int(pendingCountBefore),
+ int(mPendingEntries.Length()), int(availableCountBefore),
+ int(mAvailableEntries.Length()), aMutationType, aSize.width, aSize.height,
+ int(EstimateTotalMemory() / 1000 / 1000)));
+ }
+}
+
+template <typename F>
+void SurfacePoolCA::LockedPool::ForEachEntry(F aFn) {
+ for (auto& iter : mInUseEntries) {
+ aFn(iter.second);
+ }
+ for (auto& entry : mPendingEntries) {
+ aFn(entry.mEntry);
+ }
+ for (auto& entry : mAvailableEntries) {
+ aFn(entry);
+ }
+}
+
+uint64_t SurfacePoolCA::LockedPool::EstimateTotalMemory() {
+ std::unordered_set<const gl::DepthAndStencilBuffer*> depthAndStencilBuffers;
+ uint64_t memBytes = 0;
+
+ ForEachEntry([&](const SurfacePoolEntry& entry) {
+ auto size = entry.mSize;
+ memBytes += size.width * 4 * size.height;
+ if (entry.mGLResources) {
+ const auto& fb = *entry.mGLResources->mFramebuffer;
+ if (const auto& buffer = fb.GetDepthAndStencilBuffer()) {
+ depthAndStencilBuffers.insert(buffer.get());
+ }
+ }
+ });
+
+ for (const auto& buffer : depthAndStencilBuffers) {
+ memBytes += buffer->EstimateMemory();
+ }
+
+ return memBytes;
+}
+
+bool SurfacePoolCA::LockedPool::CanRecycleSurfaceForRequest(const SurfacePoolEntry& aEntry,
+ const IntSize& aSize, GLContext* aGL) {
+ if (aEntry.mSize != aSize) {
+ return false;
+ }
+ if (aEntry.mGLResources) {
+ return aEntry.mGLResources->mGLContext == aGL;
+ }
+ return true;
+}
+
+CFTypeRefPtr<IOSurfaceRef> SurfacePoolCA::LockedPool::ObtainSurfaceFromPool(const IntSize& aSize,
+ GLContext* aGL) {
+ // Do a linear scan through mAvailableEntries to find an eligible surface, going from oldest to
+ // newest. The size of this array is limited, so the linear scan is fast.
+ auto iterToRecycle = std::find_if(mAvailableEntries.begin(), mAvailableEntries.end(),
+ [&](const SurfacePoolEntry& aEntry) {
+ return CanRecycleSurfaceForRequest(aEntry, aSize, aGL);
+ });
+ if (iterToRecycle != mAvailableEntries.end()) {
+ CFTypeRefPtr<IOSurfaceRef> surface = iterToRecycle->mIOSurface;
+ MOZ_RELEASE_ASSERT(surface.get(), "Available surfaces should be non-null.");
+ // Move the entry from mAvailableEntries to mInUseEntries.
+ MutateEntryStorage("Recycle", aSize, [&]() {
+ mInUseEntries.insert({surface, std::move(*iterToRecycle)});
+ mAvailableEntries.RemoveElementAt(iterToRecycle);
+ });
+ return surface;
+ }
+
+ AUTO_PROFILER_LABEL_DYNAMIC_NSCSTRING("IOSurface creation", GRAPHICS_TileAllocation,
+ nsPrintfCString("%dx%d", aSize.width, aSize.height));
+ CFTypeRefPtr<IOSurfaceRef> surface =
+ CFTypeRefPtr<IOSurfaceRef>::WrapUnderCreateRule(IOSurfaceCreate((__bridge CFDictionaryRef) @{
+ (__bridge NSString*)kIOSurfaceWidth : @(aSize.width),
+ (__bridge NSString*)kIOSurfaceHeight : @(aSize.height),
+ (__bridge NSString*)kIOSurfacePixelFormat : @(kCVPixelFormatType_32BGRA),
+ (__bridge NSString*)kIOSurfaceBytesPerElement : @(4),
+ }));
+ if (surface) {
+ if (StaticPrefs::gfx_color_management_native_srgb()) {
+ IOSurfaceSetValue(surface.get(), CFSTR("IOSurfaceColorSpace"), kCGColorSpaceSRGB);
+ }
+ // Create a new entry in mInUseEntries.
+ MutateEntryStorage("Create", aSize, [&]() {
+ mInUseEntries.insert({surface, SurfacePoolEntry{aSize, surface, {}}});
+ });
+ }
+ return surface;
+}
+
+void SurfacePoolCA::LockedPool::ReturnSurfaceToPool(CFTypeRefPtr<IOSurfaceRef> aSurface) {
+ auto inUseEntryIter = mInUseEntries.find(aSurface);
+ MOZ_RELEASE_ASSERT(inUseEntryIter != mInUseEntries.end());
+ if (IOSurfaceIsInUse(aSurface.get())) {
+ // Move the entry from mInUseEntries to mPendingEntries.
+ MutateEntryStorage("Start waiting for", IntSize(inUseEntryIter->second.mSize), [&]() {
+ mPendingEntries.AppendElement(
+ PendingSurfaceEntry{std::move(inUseEntryIter->second), mCollectionGeneration, 0});
+ mInUseEntries.erase(inUseEntryIter);
+ });
+ } else {
+ // Move the entry from mInUseEntries to mAvailableEntries.
+ MOZ_RELEASE_ASSERT(inUseEntryIter->second.mIOSurface.get(),
+ "In use surfaces should be non-null.");
+ MutateEntryStorage("Retain", IntSize(inUseEntryIter->second.mSize), [&]() {
+ mAvailableEntries.AppendElement(std::move(inUseEntryIter->second));
+ mInUseEntries.erase(inUseEntryIter);
+ });
+ }
+}
+
+void SurfacePoolCA::LockedPool::EnforcePoolSizeLimit() {
+ // Enforce the pool size limit, removing least-recently-used entries as necessary.
+ while (mAvailableEntries.Length() > mPoolSizeLimit) {
+ MutateEntryStorage("Evict", IntSize(mAvailableEntries[0].mSize),
+ [&]() { mAvailableEntries.RemoveElementAt(0); });
+ }
+}
+
+uint64_t SurfacePoolCA::LockedPool::CollectPendingSurfaces(uint64_t aCheckGenerationsUpTo) {
+ mCollectionGeneration++;
+
+ // Loop from back to front, potentially deleting items as we iterate.
+ // mPendingEntries is used as a set; the order of its items is not meaningful.
+ size_t i = mPendingEntries.Length();
+ while (i) {
+ i -= 1;
+ auto& pendingSurf = mPendingEntries[i];
+ if (pendingSurf.mPreviousCheckGeneration > aCheckGenerationsUpTo) {
+ continue;
+ }
+ // Check if the window server is still using the surface. As long as it is doing that, we cannot
+ // move the surface to mAvailableSurfaces because anything we draw to it could reach the screen
+ // in a place where we don't expect it.
+ if (IOSurfaceIsInUse(pendingSurf.mEntry.mIOSurface.get())) {
+ // The surface is still in use. Update mPreviousCheckGeneration and mCheckCount.
+ pendingSurf.mPreviousCheckGeneration = mCollectionGeneration;
+ pendingSurf.mCheckCount++;
+ if (pendingSurf.mCheckCount >= 30) {
+ // The window server has been holding on to this surface for an unreasonably long time. This
+ // is known to happen sometimes, for example in occluded windows or after a GPU switch. In
+ // that case, release our references to the surface so that it's Not Our Problem anymore.
+ // Remove the entry from mPendingEntries.
+ MutateEntryStorage("Eject", IntSize(pendingSurf.mEntry.mSize),
+ [&]() { mPendingEntries.RemoveElementAt(i); });
+ }
+ } else {
+ // The surface has become unused!
+ // Move the entry from mPendingEntries to mAvailableEntries.
+ MOZ_RELEASE_ASSERT(pendingSurf.mEntry.mIOSurface.get(),
+ "Pending surfaces should be non-null.");
+ MutateEntryStorage("Stop waiting for", IntSize(pendingSurf.mEntry.mSize), [&]() {
+ mAvailableEntries.AppendElement(std::move(pendingSurf.mEntry));
+ mPendingEntries.RemoveElementAt(i);
+ });
+ }
+ }
+ return mCollectionGeneration;
+}
+
+void SurfacePoolCA::LockedPool::OnWrapperDestroyed(gl::GLContext* aGL,
+ SurfacePoolCAWrapperForGL* aWrapper) {
+ if (aGL) {
+ DestroyGLResourcesForContext(aGL);
+ }
+
+ auto iter = mWrappers.find(aGL);
+ MOZ_RELEASE_ASSERT(iter != mWrappers.end());
+ MOZ_RELEASE_ASSERT(iter->second == aWrapper, "Only one SurfacePoolCAWrapperForGL object should "
+ "exist for each GLContext* at any time");
+ mWrappers.erase(iter);
+}
+
+Maybe<GLuint> SurfacePoolCA::LockedPool::GetFramebufferForSurface(
+ CFTypeRefPtr<IOSurfaceRef> aSurface, GLContext* aGL, bool aNeedsDepthBuffer) {
+ MOZ_RELEASE_ASSERT(aGL);
+
+ auto inUseEntryIter = mInUseEntries.find(aSurface);
+ MOZ_RELEASE_ASSERT(inUseEntryIter != mInUseEntries.end());
+
+ SurfacePoolEntry& entry = inUseEntryIter->second;
+ if (entry.mGLResources) {
+ // We have an existing framebuffer.
+ MOZ_RELEASE_ASSERT(entry.mGLResources->mGLContext == aGL,
+ "Recycled surface that still had GL resources from a different GL context. "
+ "This shouldn't happen.");
+ if (!aNeedsDepthBuffer || entry.mGLResources->mFramebuffer->HasDepth()) {
+ return Some(entry.mGLResources->mFramebuffer->mFB);
+ }
+ }
+
+ // No usable existing framebuffer, we need to create one.
+
+ AUTO_PROFILER_LABEL_DYNAMIC_NSCSTRING(
+ "Framebuffer creation", GRAPHICS_TileAllocation,
+ nsPrintfCString("%dx%d", entry.mSize.width, entry.mSize.height));
+
+ RefPtr<GLContextCGL> cgl = GLContextCGL::Cast(aGL);
+ MOZ_RELEASE_ASSERT(cgl, "Unexpected GLContext type");
+
+ if (!aGL->MakeCurrent()) {
+ // Context may have been destroyed.
+ return {};
+ }
+
+ GLuint tex = aGL->CreateTexture();
+ {
+ const gl::ScopedBindTexture bindTex(aGL, tex, LOCAL_GL_TEXTURE_RECTANGLE_ARB);
+ CGLTexImageIOSurface2D(cgl->GetCGLContext(), LOCAL_GL_TEXTURE_RECTANGLE_ARB, LOCAL_GL_RGBA,
+ entry.mSize.width, entry.mSize.height, LOCAL_GL_BGRA,
+ LOCAL_GL_UNSIGNED_INT_8_8_8_8_REV, entry.mIOSurface.get(), 0);
+ }
+
+ auto fb = CreateFramebufferForTexture(aGL, entry.mSize, tex, aNeedsDepthBuffer);
+ if (!fb) {
+ // Framebuffer completeness check may have failed.
+ return {};
+ }
+
+ GLuint fbo = fb->mFB;
+ entry.mGLResources = Some(GLResourcesForSurface{aGL, std::move(fb)});
+ return Some(fbo);
+}
+
+RefPtr<gl::DepthAndStencilBuffer> SurfacePoolCA::LockedPool::GetDepthBufferForSharing(
+ GLContext* aGL, const IntSize& aSize) {
+ // Clean out entries for which the weak pointer has become null.
+ mDepthBuffers.RemoveElementsBy([&](const DepthBufferEntry& entry) { return !entry.mBuffer; });
+
+ for (const auto& entry : mDepthBuffers) {
+ if (entry.mGLContext == aGL && entry.mSize == aSize) {
+ return entry.mBuffer.get();
+ }
+ }
+ return nullptr;
+}
+
+UniquePtr<gl::MozFramebuffer> SurfacePoolCA::LockedPool::CreateFramebufferForTexture(
+ GLContext* aGL, const IntSize& aSize, GLuint aTexture, bool aNeedsDepthBuffer) {
+ if (aNeedsDepthBuffer) {
+ // Try to find an existing depth buffer of aSize in aGL and create a framebuffer that shares it.
+ if (auto buffer = GetDepthBufferForSharing(aGL, aSize)) {
+ return gl::MozFramebuffer::CreateForBackingWithSharedDepthAndStencil(
+ aSize, 0, LOCAL_GL_TEXTURE_RECTANGLE_ARB, aTexture, buffer);
+ }
+ }
+
+ // No depth buffer needed or we didn't find one. Create a framebuffer with a new depth buffer and
+ // store a weak pointer to the new depth buffer in mDepthBuffers.
+ UniquePtr<gl::MozFramebuffer> fb = gl::MozFramebuffer::CreateForBacking(
+ aGL, aSize, 0, aNeedsDepthBuffer, LOCAL_GL_TEXTURE_RECTANGLE_ARB, aTexture);
+ if (fb && fb->GetDepthAndStencilBuffer()) {
+ mDepthBuffers.AppendElement(DepthBufferEntry{aGL, aSize, fb->GetDepthAndStencilBuffer().get()});
+ }
+
+ return fb;
+}
+
+// SurfacePoolHandleCA
+
+SurfacePoolHandleCA::SurfacePoolHandleCA(RefPtr<SurfacePoolCAWrapperForGL>&& aPoolWrapper,
+ uint64_t aCurrentCollectionGeneration)
+ : mPoolWrapper(aPoolWrapper),
+ mPreviousFrameCollectionGeneration(
+ "SurfacePoolHandleCA::mPreviousFrameCollectionGeneration") {
+ auto generation = mPreviousFrameCollectionGeneration.Lock();
+ *generation = aCurrentCollectionGeneration;
+}
+
+SurfacePoolHandleCA::~SurfacePoolHandleCA() {}
+
+void SurfacePoolHandleCA::OnBeginFrame() {
+ auto generation = mPreviousFrameCollectionGeneration.Lock();
+ *generation = mPoolWrapper->mPool->CollectPendingSurfaces(*generation);
+}
+
+void SurfacePoolHandleCA::OnEndFrame() { mPoolWrapper->mPool->EnforcePoolSizeLimit(); }
+
+CFTypeRefPtr<IOSurfaceRef> SurfacePoolHandleCA::ObtainSurfaceFromPool(const IntSize& aSize) {
+ return mPoolWrapper->mPool->ObtainSurfaceFromPool(aSize, mPoolWrapper->mGL);
+}
+
+void SurfacePoolHandleCA::ReturnSurfaceToPool(CFTypeRefPtr<IOSurfaceRef> aSurface) {
+ mPoolWrapper->mPool->ReturnSurfaceToPool(aSurface);
+}
+
+Maybe<GLuint> SurfacePoolHandleCA::GetFramebufferForSurface(CFTypeRefPtr<IOSurfaceRef> aSurface,
+ bool aNeedsDepthBuffer) {
+ return mPoolWrapper->mPool->GetFramebufferForSurface(aSurface, mPoolWrapper->mGL,
+ aNeedsDepthBuffer);
+}
+
+// SurfacePoolCA
+
+SurfacePoolCA::SurfacePoolCA(size_t aPoolSizeLimit)
+ : mPool(LockedPool(aPoolSizeLimit), "SurfacePoolCA::mPool") {}
+
+SurfacePoolCA::~SurfacePoolCA() {}
+
+RefPtr<SurfacePoolHandle> SurfacePoolCA::GetHandleForGL(GLContext* aGL) {
+ RefPtr<SurfacePoolCAWrapperForGL> wrapper;
+ uint64_t collectionGeneration = 0;
+ {
+ auto pool = mPool.Lock();
+ wrapper = pool->GetWrapperForGL(this, aGL);
+ collectionGeneration = pool->mCollectionGeneration;
+ }
+
+ // Run the SurfacePoolHandleCA constructor outside of the lock so that the
+ // mPool lock and the handle's lock are always ordered the same way.
+ return new SurfacePoolHandleCA(std::move(wrapper), collectionGeneration);
+}
+
+void SurfacePoolCA::DestroyGLResourcesForContext(GLContext* aGL) {
+ auto pool = mPool.Lock();
+ pool->DestroyGLResourcesForContext(aGL);
+}
+
+CFTypeRefPtr<IOSurfaceRef> SurfacePoolCA::ObtainSurfaceFromPool(const IntSize& aSize,
+ GLContext* aGL) {
+ auto pool = mPool.Lock();
+ return pool->ObtainSurfaceFromPool(aSize, aGL);
+}
+
+void SurfacePoolCA::ReturnSurfaceToPool(CFTypeRefPtr<IOSurfaceRef> aSurface) {
+ auto pool = mPool.Lock();
+ pool->ReturnSurfaceToPool(aSurface);
+}
+
+uint64_t SurfacePoolCA::CollectPendingSurfaces(uint64_t aCheckGenerationsUpTo) {
+ auto pool = mPool.Lock();
+ return pool->CollectPendingSurfaces(aCheckGenerationsUpTo);
+}
+void SurfacePoolCA::EnforcePoolSizeLimit() {
+ auto pool = mPool.Lock();
+ pool->EnforcePoolSizeLimit();
+}
+
+Maybe<GLuint> SurfacePoolCA::GetFramebufferForSurface(CFTypeRefPtr<IOSurfaceRef> aSurface,
+ GLContext* aGL, bool aNeedsDepthBuffer) {
+ auto pool = mPool.Lock();
+ return pool->GetFramebufferForSurface(aSurface, aGL, aNeedsDepthBuffer);
+}
+
+void SurfacePoolCA::OnWrapperDestroyed(gl::GLContext* aGL, SurfacePoolCAWrapperForGL* aWrapper) {
+ auto pool = mPool.Lock();
+ return pool->OnWrapperDestroyed(aGL, aWrapper);
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/SurfacePoolWayland.cpp b/gfx/layers/SurfacePoolWayland.cpp
new file mode 100644
index 0000000000..e1229fcaaa
--- /dev/null
+++ b/gfx/layers/SurfacePoolWayland.cpp
@@ -0,0 +1,246 @@
+/* -*- Mode: C++; tab-width: 20; indent-tabs-mode: nullptr; c-basic-offset: 2
+ * -*- This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/layers/SurfacePoolWayland.h"
+
+#include "GLBlitHelper.h"
+#include "mozilla/gfx/DataSurfaceHelpers.h"
+
+namespace mozilla::layers {
+
+using gfx::IntSize;
+using gl::DepthAndStencilBuffer;
+using gl::GLContext;
+using gl::MozFramebuffer;
+using widget::WaylandBuffer;
+
+/* static */ RefPtr<SurfacePool> SurfacePool::Create(size_t aPoolSizeLimit) {
+ return new SurfacePoolWayland(aPoolSizeLimit);
+}
+
+SurfacePoolWayland::SurfacePoolWayland(size_t aPoolSizeLimit)
+ : mMutex("SurfacePoolWayland"), mPoolSizeLimit(aPoolSizeLimit) {}
+
+RefPtr<SurfacePoolHandle> SurfacePoolWayland::GetHandleForGL(GLContext* aGL) {
+ return new SurfacePoolHandleWayland(this, aGL);
+}
+
+template <typename F>
+void SurfacePoolWayland::ForEachEntry(F aFn) {
+ for (auto& iter : mInUseEntries) {
+ aFn(iter.second);
+ }
+ for (auto& entry : mPendingEntries) {
+ aFn(entry);
+ }
+ for (auto& entry : mAvailableEntries) {
+ aFn(entry);
+ }
+}
+
+void SurfacePoolWayland::DestroyGLResourcesForContext(GLContext* aGL) {
+ MutexAutoLock lock(mMutex);
+
+ ForEachEntry([&](SurfacePoolEntry& entry) {
+ if (entry.mGLResources && entry.mGLResources->mGL == aGL) {
+ entry.mGLResources = Nothing();
+ entry.mWaylandBuffer->DestroyGLResources();
+ }
+ });
+ mDepthBuffers.RemoveElementsBy(
+ [&](const DepthBufferEntry& entry) { return entry.mGL == aGL; });
+}
+
+bool SurfacePoolWayland::CanRecycleSurfaceForRequest(
+ const MutexAutoLock& aProofOfLock, const SurfacePoolEntry& aEntry,
+ const IntSize& aSize, GLContext* aGL) {
+ if (aEntry.mSize != aSize) {
+ return false;
+ }
+ if (aEntry.mGLResources) {
+ return aEntry.mGLResources->mGL == aGL;
+ }
+ return aGL == nullptr;
+}
+
+RefPtr<WaylandBuffer> SurfacePoolWayland::ObtainBufferFromPool(
+ const IntSize& aSize, GLContext* aGL) {
+ MutexAutoLock lock(mMutex);
+
+ auto iterToRecycle = std::find_if(
+ mAvailableEntries.begin(), mAvailableEntries.end(),
+ [&](const SurfacePoolEntry& aEntry) {
+ return CanRecycleSurfaceForRequest(lock, aEntry, aSize, aGL);
+ });
+ if (iterToRecycle != mAvailableEntries.end()) {
+ RefPtr<WaylandBuffer> buffer = iterToRecycle->mWaylandBuffer;
+ mInUseEntries.insert({buffer.get(), std::move(*iterToRecycle)});
+ mAvailableEntries.RemoveElementAt(iterToRecycle);
+ return buffer;
+ }
+
+ RefPtr<WaylandBuffer> buffer;
+ if (aGL) {
+ buffer = widget::WaylandBufferDMABUF::Create(
+ LayoutDeviceIntSize::FromUnknownSize(aSize), aGL);
+ } else {
+ buffer = widget::WaylandBufferSHM::Create(
+ LayoutDeviceIntSize::FromUnknownSize(aSize));
+ }
+ if (buffer) {
+ mInUseEntries.insert({buffer.get(), SurfacePoolEntry{aSize, buffer, {}}});
+ }
+
+ return buffer;
+}
+
+void SurfacePoolWayland::ReturnBufferToPool(
+ const RefPtr<WaylandBuffer>& aBuffer) {
+ MutexAutoLock lock(mMutex);
+
+ auto inUseEntryIter = mInUseEntries.find(aBuffer);
+ MOZ_RELEASE_ASSERT(inUseEntryIter != mInUseEntries.end());
+
+ if (aBuffer->IsAttached()) {
+ mPendingEntries.AppendElement(std::move(inUseEntryIter->second));
+ mInUseEntries.erase(inUseEntryIter);
+ } else {
+ mAvailableEntries.AppendElement(std::move(inUseEntryIter->second));
+ mInUseEntries.erase(inUseEntryIter);
+ }
+}
+
+void SurfacePoolWayland::EnforcePoolSizeLimit() {
+ MutexAutoLock lock(mMutex);
+
+ // Enforce the pool size limit, removing least-recently-used entries as
+ // necessary.
+ while (mAvailableEntries.Length() > mPoolSizeLimit) {
+ mAvailableEntries.RemoveElementAt(0);
+ }
+
+ NS_WARNING_ASSERTION(mPendingEntries.Length() < mPoolSizeLimit * 2,
+ "Are we leaking pending entries?");
+ NS_WARNING_ASSERTION(mInUseEntries.size() < mPoolSizeLimit * 2,
+ "Are we leaking in-use entries?");
+}
+
+void SurfacePoolWayland::CollectPendingSurfaces() {
+ MutexAutoLock lock(mMutex);
+ mPendingEntries.RemoveElementsBy([&](auto& entry) {
+ if (!entry.mWaylandBuffer->IsAttached()) {
+ mAvailableEntries.AppendElement(std::move(entry));
+ return true;
+ }
+ return false;
+ });
+}
+
+Maybe<GLuint> SurfacePoolWayland::GetFramebufferForBuffer(
+ const RefPtr<WaylandBuffer>& aBuffer, GLContext* aGL,
+ bool aNeedsDepthBuffer) {
+ MutexAutoLock lock(mMutex);
+ MOZ_RELEASE_ASSERT(aGL);
+
+ auto inUseEntryIter = mInUseEntries.find(aBuffer);
+ MOZ_RELEASE_ASSERT(inUseEntryIter != mInUseEntries.end());
+
+ SurfacePoolEntry& entry = inUseEntryIter->second;
+ if (entry.mGLResources) {
+ // We have an existing framebuffer.
+ MOZ_RELEASE_ASSERT(entry.mGLResources->mGL == aGL,
+ "Recycled surface that still had GL resources from a "
+ "different GL context. "
+ "This shouldn't happen.");
+ if (!aNeedsDepthBuffer || entry.mGLResources->mFramebuffer->HasDepth()) {
+ return Some(entry.mGLResources->mFramebuffer->mFB);
+ }
+ }
+
+ // No usable existing framebuffer, we need to create one.
+
+ if (!aGL->MakeCurrent()) {
+ // Context may have been destroyed.
+ return {};
+ }
+
+ const GLuint tex = aBuffer->GetTexture();
+ auto fb = CreateFramebufferForTexture(lock, aGL, entry.mSize, tex,
+ aNeedsDepthBuffer);
+ if (!fb) {
+ // Framebuffer completeness check may have failed.
+ return {};
+ }
+
+ GLuint fbo = fb->mFB;
+ entry.mGLResources = Some(GLResourcesForBuffer{aGL, std::move(fb)});
+ return Some(fbo);
+}
+
+RefPtr<gl::DepthAndStencilBuffer> SurfacePoolWayland::GetDepthBufferForSharing(
+ const MutexAutoLock& aProofOfLock, GLContext* aGL, const IntSize& aSize) {
+ // Clean out entries for which the weak pointer has become null.
+ mDepthBuffers.RemoveElementsBy(
+ [&](const DepthBufferEntry& entry) { return !entry.mBuffer; });
+
+ for (const auto& entry : mDepthBuffers) {
+ if (entry.mGL == aGL && entry.mSize == aSize) {
+ return entry.mBuffer.get();
+ }
+ }
+ return nullptr;
+}
+
+UniquePtr<MozFramebuffer> SurfacePoolWayland::CreateFramebufferForTexture(
+ const MutexAutoLock& aProofOfLock, GLContext* aGL, const IntSize& aSize,
+ GLuint aTexture, bool aNeedsDepthBuffer) {
+ if (aNeedsDepthBuffer) {
+ // Try to find an existing depth buffer of aSize in aGL and create a
+ // framebuffer that shares it.
+ if (auto buffer = GetDepthBufferForSharing(aProofOfLock, aGL, aSize)) {
+ return MozFramebuffer::CreateForBackingWithSharedDepthAndStencil(
+ aSize, 0, LOCAL_GL_TEXTURE_2D, aTexture, buffer);
+ }
+ }
+
+ // No depth buffer needed or we didn't find one. Create a framebuffer with a
+ // new depth buffer and store a weak pointer to the new depth buffer in
+ // mDepthBuffers.
+ UniquePtr<MozFramebuffer> fb = MozFramebuffer::CreateForBacking(
+ aGL, aSize, 0, aNeedsDepthBuffer, LOCAL_GL_TEXTURE_2D, aTexture);
+ if (fb && fb->GetDepthAndStencilBuffer()) {
+ mDepthBuffers.AppendElement(
+ DepthBufferEntry{aGL, aSize, fb->GetDepthAndStencilBuffer().get()});
+ }
+
+ return fb;
+}
+
+SurfacePoolHandleWayland::SurfacePoolHandleWayland(
+ RefPtr<SurfacePoolWayland> aPool, GLContext* aGL)
+ : mPool(std::move(aPool)), mGL(aGL) {}
+
+void SurfacePoolHandleWayland::OnBeginFrame() {
+ mPool->CollectPendingSurfaces();
+}
+
+void SurfacePoolHandleWayland::OnEndFrame() { mPool->EnforcePoolSizeLimit(); }
+
+RefPtr<WaylandBuffer> SurfacePoolHandleWayland::ObtainBufferFromPool(
+ const IntSize& aSize) {
+ return mPool->ObtainBufferFromPool(aSize, mGL);
+}
+
+void SurfacePoolHandleWayland::ReturnBufferToPool(
+ const RefPtr<WaylandBuffer>& aBuffer) {
+ mPool->ReturnBufferToPool(aBuffer);
+}
+
+Maybe<GLuint> SurfacePoolHandleWayland::GetFramebufferForBuffer(
+ const RefPtr<WaylandBuffer>& aBuffer, bool aNeedsDepthBuffer) {
+ return mPool->GetFramebufferForBuffer(aBuffer, mGL, aNeedsDepthBuffer);
+}
+
+} // namespace mozilla::layers
diff --git a/gfx/layers/SurfacePoolWayland.h b/gfx/layers/SurfacePoolWayland.h
new file mode 100644
index 0000000000..6a746e732c
--- /dev/null
+++ b/gfx/layers/SurfacePoolWayland.h
@@ -0,0 +1,127 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_SurfacePoolWayland_h
+#define mozilla_layers_SurfacePoolWayland_h
+
+#include <wayland-egl.h>
+
+#include "GLContext.h"
+#include "MozFramebuffer.h"
+#include "mozilla/layers/SurfacePool.h"
+#include "mozilla/widget/WaylandBuffer.h"
+
+#include <unordered_map>
+
+namespace mozilla::layers {
+
+class SurfacePoolWayland final : public SurfacePool {
+ public:
+ // Get a handle for a new window. aGL can be nullptr.
+ RefPtr<SurfacePoolHandle> GetHandleForGL(gl::GLContext* aGL) override;
+
+ // Destroy all GL resources associated with aGL managed by this pool.
+ void DestroyGLResourcesForContext(gl::GLContext* aGL) override;
+
+ private:
+ friend class SurfacePoolHandleWayland;
+ friend RefPtr<SurfacePool> SurfacePool::Create(size_t aPoolSizeLimit);
+
+ explicit SurfacePoolWayland(size_t aPoolSizeLimit);
+
+ RefPtr<widget::WaylandBuffer> ObtainBufferFromPool(const gfx::IntSize& aSize,
+ gl::GLContext* aGL);
+ void ReturnBufferToPool(const RefPtr<widget::WaylandBuffer>& aBuffer);
+ void EnforcePoolSizeLimit();
+ void CollectPendingSurfaces();
+ Maybe<GLuint> GetFramebufferForBuffer(
+ const RefPtr<widget::WaylandBuffer>& aBuffer, gl::GLContext* aGL,
+ bool aNeedsDepthBuffer);
+
+ struct GLResourcesForBuffer final {
+ RefPtr<gl::GLContext> mGL; // non-null
+ UniquePtr<gl::MozFramebuffer> mFramebuffer; // non-null
+ };
+
+ struct SurfacePoolEntry final {
+ const gfx::IntSize mSize;
+ const RefPtr<widget::WaylandBuffer> mWaylandBuffer; // non-null
+ Maybe<GLResourcesForBuffer> mGLResources;
+ };
+
+ bool CanRecycleSurfaceForRequest(const MutexAutoLock& aProofOfLock,
+ const SurfacePoolEntry& aEntry,
+ const gfx::IntSize& aSize,
+ gl::GLContext* aGL);
+
+ RefPtr<gl::DepthAndStencilBuffer> GetDepthBufferForSharing(
+ const MutexAutoLock& aProofOfLock, gl::GLContext* aGL,
+ const gfx::IntSize& aSize);
+ UniquePtr<gl::MozFramebuffer> CreateFramebufferForTexture(
+ const MutexAutoLock& aProofOfLock, gl::GLContext* aGL,
+ const gfx::IntSize& aSize, GLuint aTexture, bool aNeedsDepthBuffer);
+
+ Mutex mMutex MOZ_UNANNOTATED;
+
+ // Stores the entries for surfaces that are in use by NativeLayerWayland, i.e.
+ // an entry is inside mInUseEntries between calls to ObtainSurfaceFromPool()
+ // and ReturnSurfaceToPool().
+ std::unordered_map<widget::WaylandBuffer*, SurfacePoolEntry> mInUseEntries;
+
+ // Stores entries which are no longer in use by NativeLayerWayland but are
+ // still in use by the window server, i.e. for which
+ // WaylandBuffer::IsAttached() still returns true.
+ // These entries are checked once per frame inside
+ // CollectPendingSurfaces(), and returned to mAvailableEntries once the
+ // window server is done.
+ nsTArray<SurfacePoolEntry> mPendingEntries;
+
+ // Stores entries which are available for recycling. These entries are not
+ // in use by a NativeLayerWayland or by the window server.
+ nsTArray<SurfacePoolEntry> mAvailableEntries;
+ size_t mPoolSizeLimit;
+
+ template <typename F>
+ void ForEachEntry(F aFn);
+
+ struct DepthBufferEntry final {
+ RefPtr<gl::GLContext> mGL;
+ gfx::IntSize mSize;
+ WeakPtr<gl::DepthAndStencilBuffer> mBuffer;
+ };
+
+ nsTArray<DepthBufferEntry> mDepthBuffers;
+};
+
+// A surface pool handle that is stored on NativeLayerWayland and keeps the
+// SurfacePool alive.
+class SurfacePoolHandleWayland final : public SurfacePoolHandle {
+ public:
+ SurfacePoolHandleWayland* AsSurfacePoolHandleWayland() override {
+ return this;
+ }
+
+ RefPtr<widget::WaylandBuffer> ObtainBufferFromPool(const gfx::IntSize& aSize);
+ void ReturnBufferToPool(const RefPtr<widget::WaylandBuffer>& aBuffer);
+ Maybe<GLuint> GetFramebufferForBuffer(
+ const RefPtr<widget::WaylandBuffer>& aBuffer, bool aNeedsDepthBuffer);
+ const auto& gl() { return mGL; }
+
+ RefPtr<SurfacePool> Pool() override { return mPool; }
+ void OnBeginFrame() override;
+ void OnEndFrame() override;
+
+ private:
+ friend class SurfacePoolWayland;
+ SurfacePoolHandleWayland(RefPtr<SurfacePoolWayland> aPool,
+ gl::GLContext* aGL);
+
+ const RefPtr<SurfacePoolWayland> mPool;
+ const RefPtr<gl::GLContext> mGL;
+};
+
+} // namespace mozilla::layers
+
+#endif
diff --git a/gfx/layers/SyncObject.cpp b/gfx/layers/SyncObject.cpp
new file mode 100644
index 0000000000..adb85da14f
--- /dev/null
+++ b/gfx/layers/SyncObject.cpp
@@ -0,0 +1,50 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "SyncObject.h"
+
+#ifdef XP_WIN
+# include "mozilla/gfx/DeviceManagerDx.h"
+# include "mozilla/layers/TextureD3D11.h"
+#endif
+#include "nsIXULRuntime.h" // for BrowserTabsRemoteAutostart
+
+namespace mozilla {
+namespace layers {
+
+#ifdef XP_WIN
+/* static */
+already_AddRefed<SyncObjectHost> SyncObjectHost::CreateSyncObjectHost(
+ ID3D11Device* aDevice) {
+ return MakeAndAddRef<SyncObjectD3D11Host>(aDevice);
+}
+
+/* static */
+already_AddRefed<SyncObjectClient> SyncObjectClient::CreateSyncObjectClient(
+ SyncHandle aHandle, ID3D11Device* aDevice) {
+ if (!aHandle) {
+ return nullptr;
+ }
+
+ return MakeAndAddRef<SyncObjectD3D11Client>(aHandle, aDevice);
+}
+#endif
+
+already_AddRefed<SyncObjectClient>
+SyncObjectClient::CreateSyncObjectClientForContentDevice(SyncHandle aHandle) {
+#ifndef XP_WIN
+ return nullptr;
+#else
+ if (!aHandle) {
+ return nullptr;
+ }
+
+ return MakeAndAddRef<SyncObjectD3D11ClientContentDevice>(aHandle);
+#endif
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/SyncObject.h b/gfx/layers/SyncObject.h
new file mode 100644
index 0000000000..03590dd436
--- /dev/null
+++ b/gfx/layers/SyncObject.h
@@ -0,0 +1,75 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_GFX_LAYERS_SYNCOBJECT_H
+#define MOZILLA_GFX_LAYERS_SYNCOBJECT_H
+
+#include "mozilla/RefCounted.h"
+
+struct ID3D11Device;
+
+namespace mozilla {
+namespace layers {
+
+#ifdef XP_WIN
+typedef void* SyncHandle;
+#else
+typedef uintptr_t SyncHandle;
+#endif // XP_WIN
+
+class SyncObjectHost : public RefCounted<SyncObjectHost> {
+ public:
+ MOZ_DECLARE_REFCOUNTED_VIRTUAL_TYPENAME(SyncObjectHost)
+ virtual ~SyncObjectHost() = default;
+
+#ifdef XP_WIN
+ static already_AddRefed<SyncObjectHost> CreateSyncObjectHost(
+ ID3D11Device* aDevice);
+#endif
+ virtual bool Init() = 0;
+
+ virtual SyncHandle GetSyncHandle() = 0;
+
+ // Return false for failed synchronization.
+ virtual bool Synchronize(bool aFallible = false) = 0;
+
+ protected:
+ SyncObjectHost() = default;
+};
+
+class SyncObjectClient : public external::AtomicRefCounted<SyncObjectClient> {
+ public:
+ MOZ_DECLARE_REFCOUNTED_VIRTUAL_TYPENAME(SyncObjectClient)
+ virtual ~SyncObjectClient() = default;
+
+#ifdef XP_WIN
+ static already_AddRefed<SyncObjectClient> CreateSyncObjectClient(
+ SyncHandle aHandle, ID3D11Device* aDevice);
+#endif
+ static already_AddRefed<SyncObjectClient>
+ CreateSyncObjectClientForContentDevice(SyncHandle aHandle);
+
+ enum class SyncType {
+ D3D11,
+ };
+
+ virtual SyncType GetSyncType() = 0;
+
+ // Return false for failed synchronization.
+ virtual bool Synchronize(bool aFallible = false) = 0;
+
+ virtual bool IsSyncObjectValid() = 0;
+
+ virtual void EnsureInitialized() = 0;
+
+ protected:
+ SyncObjectClient() = default;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // MOZILLA_GFX_LAYERS_SYNCOBJECT_H
diff --git a/gfx/layers/TextureSourceProvider.cpp b/gfx/layers/TextureSourceProvider.cpp
new file mode 100644
index 0000000000..78b9931167
--- /dev/null
+++ b/gfx/layers/TextureSourceProvider.cpp
@@ -0,0 +1,19 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/layers/TextureSourceProvider.h"
+#include "mozilla/layers/TextureHost.h"
+#include "mozilla/layers/PTextureParent.h"
+
+namespace mozilla {
+namespace layers {
+
+TextureSourceProvider::~TextureSourceProvider() {}
+
+void TextureSourceProvider::Destroy() {}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/TextureSourceProvider.h b/gfx/layers/TextureSourceProvider.h
new file mode 100644
index 0000000000..50849692f5
--- /dev/null
+++ b/gfx/layers/TextureSourceProvider.h
@@ -0,0 +1,76 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#ifndef mozilla_gfx_layers_TextureSourceProvider_h
+#define mozilla_gfx_layers_TextureSourceProvider_h
+
+#include "nsISupportsImpl.h"
+#include "mozilla/AlreadyAddRefed.h"
+#include "mozilla/TimeStamp.h"
+#include "mozilla/layers/CompositorTypes.h"
+#include "nsTArray.h"
+
+struct ID3D11Device;
+
+namespace mozilla {
+namespace gfx {
+class DataSourceSurface;
+} // namespace gfx
+namespace gl {
+class GLContext;
+} // namespace gl
+namespace layers {
+
+class TextureHost;
+class DataTextureSource;
+class Compositor;
+class CompositorOGL;
+
+// Provided by a HostLayerManager or Compositor for allocating backend-specific
+// texture types.
+class TextureSourceProvider {
+ public:
+ NS_INLINE_DECL_REFCOUNTING(TextureSourceProvider)
+
+ virtual already_AddRefed<DataTextureSource> CreateDataTextureSource(
+ TextureFlags aFlags = TextureFlags::NO_FLAGS) = 0;
+
+ virtual TimeStamp GetLastCompositionEndTime() const = 0;
+
+ virtual void TryUnlockTextures() {}
+
+ // If overridden, make sure to call the base function.
+ virtual void Destroy();
+
+ // If this provider is also a Compositor, return the compositor. Otherwise
+ // return null.
+ virtual Compositor* AsCompositor() { return nullptr; }
+
+ // If this provider is also a CompositorOGL, return the compositor. Otherwise
+ // return nullptr.
+ virtual CompositorOGL* AsCompositorOGL() { return nullptr; }
+
+#ifdef XP_WIN
+ // On Windows, if this provides Direct3D textures, it must expose the device.
+ virtual ID3D11Device* GetD3D11Device() const { return nullptr; }
+#endif
+
+ // If this provides OpenGL textures, it must expose the GLContext.
+ virtual gl::GLContext* GetGLContext() const { return nullptr; }
+
+ virtual int32_t GetMaxTextureSize() const = 0;
+
+ // Return whether or not this provider is still valid (i.e., is still being
+ // used to composite).
+ virtual bool IsValid() const = 0;
+
+ protected:
+ virtual ~TextureSourceProvider();
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_gfx_layers_TextureSourceProvider_h
diff --git a/gfx/layers/TextureWrapperImage.cpp b/gfx/layers/TextureWrapperImage.cpp
new file mode 100644
index 0000000000..324bc31ef9
--- /dev/null
+++ b/gfx/layers/TextureWrapperImage.cpp
@@ -0,0 +1,49 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#include "TextureWrapperImage.h"
+
+namespace mozilla {
+namespace layers {
+
+using namespace mozilla::gfx;
+
+TextureWrapperImage::TextureWrapperImage(TextureClient* aClient,
+ const IntRect& aPictureRect)
+ : Image(nullptr, ImageFormat::TEXTURE_WRAPPER),
+ mPictureRect(aPictureRect),
+ mTextureClient(aClient) {}
+
+TextureWrapperImage::~TextureWrapperImage() = default;
+
+gfx::IntSize TextureWrapperImage::GetSize() const {
+ return mTextureClient->GetSize();
+}
+
+gfx::IntRect TextureWrapperImage::GetPictureRect() const {
+ return mPictureRect;
+}
+
+already_AddRefed<gfx::SourceSurface> TextureWrapperImage::GetAsSourceSurface() {
+ TextureClientAutoLock autoLock(mTextureClient, OpenMode::OPEN_READ);
+ if (!autoLock.Succeeded()) {
+ return nullptr;
+ }
+
+ RefPtr<DrawTarget> dt = mTextureClient->BorrowDrawTarget();
+ if (!dt) {
+ return nullptr;
+ }
+
+ return dt->Snapshot();
+}
+
+TextureClient* TextureWrapperImage::GetTextureClient(
+ KnowsCompositor* aKnowsCompositor) {
+ return mTextureClient;
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/TextureWrapperImage.h b/gfx/layers/TextureWrapperImage.h
new file mode 100644
index 0000000000..8da62626d0
--- /dev/null
+++ b/gfx/layers/TextureWrapperImage.h
@@ -0,0 +1,37 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GFX_LAYERS_TEXTUREWRAPPINGIMAGE_H_
+#define GFX_LAYERS_TEXTUREWRAPPINGIMAGE_H_
+
+#include "mozilla/RefPtr.h"
+#include "ImageContainer.h"
+#include "mozilla/layers/TextureClient.h"
+
+namespace mozilla {
+namespace layers {
+
+// Wraps a TextureClient into an Image. This may only be used on the main
+// thread, and only with TextureClients that support BorrowDrawTarget().
+class TextureWrapperImage final : public Image {
+ public:
+ TextureWrapperImage(TextureClient* aClient, const gfx::IntRect& aPictureRect);
+ virtual ~TextureWrapperImage();
+
+ gfx::IntSize GetSize() const override;
+ gfx::IntRect GetPictureRect() const override;
+ already_AddRefed<gfx::SourceSurface> GetAsSourceSurface() override;
+ TextureClient* GetTextureClient(KnowsCompositor* aKnowsCompositor) override;
+
+ private:
+ gfx::IntRect mPictureRect;
+ RefPtr<TextureClient> mTextureClient;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // GFX_LAYERS_TEXTUREWRAPPINGIMAGE_H_
diff --git a/gfx/layers/TransactionIdAllocator.h b/gfx/layers/TransactionIdAllocator.h
new file mode 100644
index 0000000000..c9f545470f
--- /dev/null
+++ b/gfx/layers/TransactionIdAllocator.h
@@ -0,0 +1,93 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GFX_TRANSACTION_ID_ALLOCATOR_H
+#define GFX_TRANSACTION_ID_ALLOCATOR_H
+
+#include "nsISupportsImpl.h"
+#include "mozilla/layers/LayersTypes.h"
+#include "mozilla/TimeStamp.h"
+#include "mozilla/VsyncDispatcher.h"
+
+namespace mozilla {
+namespace layers {
+
+class TransactionIdAllocator {
+ protected:
+ virtual ~TransactionIdAllocator() = default;
+
+ public:
+ NS_INLINE_DECL_REFCOUNTING(TransactionIdAllocator)
+
+ /**
+ * Allocate a unique id number for the current refresh tick, can
+ * only be called while IsInRefresh().
+ *
+ * If too many id's are allocated without being returned then
+ * the refresh driver will suspend until they catch up. This
+ * "throttling" behaviour can be skipped by passing aThrottle=false.
+ * Otherwise call sites should generally be passing aThrottle=true.
+ */
+ virtual TransactionId GetTransactionId(bool aThrottle) = 0;
+
+ /**
+ * Return the transaction id that for the last non-revoked transaction.
+ * This allows the caller to tell whether a composite was triggered by
+ * a paint that occurred after a call to TransactionId().
+ */
+ virtual TransactionId LastTransactionId() const = 0;
+
+ /**
+ * Notify that all work (including asynchronous composites)
+ * for a given transaction id has been completed.
+ *
+ * If the refresh driver has been suspended because
+ * of having too many outstanding id's, then this may
+ * resume it.
+ */
+ virtual void NotifyTransactionCompleted(TransactionId aTransactionId) = 0;
+
+ /**
+ * Revoke a transaction id that isn't needed to track
+ * completion of asynchronous work. This is similar
+ * to NotifyTransactionCompleted except avoids
+ * return ordering issues.
+ */
+ virtual void RevokeTransactionId(TransactionId aTransactionId) = 0;
+
+ /**
+ * Stop waiting for pending transactions, if any.
+ *
+ * This is used when ClientLayerManager is assigning to another refresh
+ * driver, and the current refresh driver may never receive transaction
+ * completed notifications.
+ */
+ virtual void ClearPendingTransactions() = 0;
+
+ /**
+ * Transaction id is usually initialized as 0, however when ClientLayerManager
+ * switches to another refresh driver, completed transactions of the previous
+ * refresh driver could be delivered and confuse the newly adopted refresh
+ * driver. To prevent this situation, use this function to reset transaction
+ * id to the last transaction id from previous refresh driver, so that all
+ * completed transactions of previous refresh driver will be ignored.
+ */
+ virtual void ResetInitialTransactionId(TransactionId aTransactionId) = 0;
+
+ /**
+ * Get the start time of the current refresh tick.
+ */
+ virtual mozilla::TimeStamp GetTransactionStart() = 0;
+
+ virtual VsyncId GetVsyncId() = 0;
+
+ virtual mozilla::TimeStamp GetVsyncStart() = 0;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif /* GFX_TRANSACTION_ID_ALLOCATOR_H */
diff --git a/gfx/layers/TreeTraversal.h b/gfx/layers/TreeTraversal.h
new file mode 100644
index 0000000000..4c02a4e5b5
--- /dev/null
+++ b/gfx/layers/TreeTraversal.h
@@ -0,0 +1,276 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_TreeTraversal_h
+#define mozilla_layers_TreeTraversal_h
+
+#include <queue>
+#include <type_traits>
+
+namespace mozilla {
+namespace layers {
+
+/*
+ * Returned by |aPostAction| and |aPreAction| in ForEachNode, indicates
+ * the behavior to follow either action:
+ *
+ * TraversalFlag::Skip - the node's children are not traversed. If this
+ * flag is returned by |aPreAction|, |aPostAction| is skipped for the
+ * current node, as well.
+ * TraversalFlag::Continue - traversal continues normally.
+ * TraversalFlag::Abort - traversal stops immediately.
+ */
+enum class TraversalFlag { Skip, Continue, Abort };
+
+/*
+ * Iterator types to be specified in traversal function calls:
+ *
+ * ForwardIterator - for nodes using GetFirstChild() and GetNextSibling()
+ * ReverseIterator - for nodes using GetLastChild() and GetPrevSibling()
+ */
+class ForwardIterator {
+ public:
+ template <typename Node>
+ static Node* FirstChild(Node* n) {
+ return n->GetFirstChild();
+ }
+ template <typename Node>
+ static Node* NextSibling(Node* n) {
+ return n->GetNextSibling();
+ }
+ template <typename Node>
+ static Node FirstChild(Node n) {
+ return n.GetFirstChild();
+ }
+ template <typename Node>
+ static Node NextSibling(Node n) {
+ return n.GetNextSibling();
+ }
+};
+class ReverseIterator {
+ public:
+ template <typename Node>
+ static Node* FirstChild(Node* n) {
+ return n->GetLastChild();
+ }
+ template <typename Node>
+ static Node* NextSibling(Node* n) {
+ return n->GetPrevSibling();
+ }
+ template <typename Node>
+ static Node FirstChild(Node n) {
+ return n.GetLastChild();
+ }
+ template <typename Node>
+ static Node NextSibling(Node n) {
+ return n.GetPrevSibling();
+ }
+};
+
+/*
+ * Do a depth-first traversal of the tree rooted at |aRoot|, performing
+ * |aPreAction| before traversal of children and |aPostAction| after.
+ *
+ * Returns true if traversal aborted, false if continued normally. If
+ * TraversalFlag::Skip is returned in |aPreAction|, then |aPostAction|
+ * is not performed.
+ *
+ * |Iterator| should have static methods named NextSibling() and FirstChild()
+ * that accept an argument of type Node. For convenience, classes
+ * |ForwardIterator| and |ReverseIterator| are provided which implement these
+ * methods as GetNextSibling()/GetFirstChild() and
+ * GetPrevSibling()/GetLastChild(), respectively.
+ */
+template <typename Iterator, typename Node, typename PreAction,
+ typename PostAction>
+static auto ForEachNode(Node aRoot, const PreAction& aPreAction,
+ const PostAction& aPostAction)
+ -> std::enable_if_t<
+ std::is_same_v<decltype(aPreAction(aRoot)), TraversalFlag> &&
+ std::is_same_v<decltype(aPostAction(aRoot)), TraversalFlag>,
+ bool> {
+ if (!aRoot) {
+ return false;
+ }
+
+ TraversalFlag result = aPreAction(aRoot);
+
+ if (result == TraversalFlag::Abort) {
+ return true;
+ }
+
+ if (result == TraversalFlag::Continue) {
+ for (Node child = Iterator::FirstChild(aRoot); child;
+ child = Iterator::NextSibling(child)) {
+ bool abort = ForEachNode<Iterator>(child, aPreAction, aPostAction);
+ if (abort) {
+ return true;
+ }
+ }
+
+ result = aPostAction(aRoot);
+
+ if (result == TraversalFlag::Abort) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+/*
+ * Do a depth-first traversal of the tree rooted at |aRoot|, performing
+ * |aPreAction| before traversal of children and |aPostAction| after.
+ */
+template <typename Iterator, typename Node, typename PreAction,
+ typename PostAction>
+static auto ForEachNode(Node aRoot, const PreAction& aPreAction,
+ const PostAction& aPostAction)
+ -> std::enable_if_t<std::is_same_v<decltype(aPreAction(aRoot)), void> &&
+ std::is_same_v<decltype(aPostAction(aRoot)), void>,
+ void> {
+ if (!aRoot) {
+ return;
+ }
+
+ aPreAction(aRoot);
+
+ for (Node child = Iterator::FirstChild(aRoot); child;
+ child = Iterator::NextSibling(child)) {
+ ForEachNode<Iterator>(child, aPreAction, aPostAction);
+ }
+
+ aPostAction(aRoot);
+}
+
+/*
+ * ForEachNode pre-order traversal, using TraversalFlag.
+ */
+template <typename Iterator, typename Node, typename PreAction>
+auto ForEachNode(Node aRoot, const PreAction& aPreAction) -> std::enable_if_t<
+ std::is_same_v<decltype(aPreAction(aRoot)), TraversalFlag>, bool> {
+ return ForEachNode<Iterator>(
+ aRoot, aPreAction, [](Node aNode) { return TraversalFlag::Continue; });
+}
+
+/*
+ * ForEachNode pre-order, not using TraversalFlag.
+ */
+template <typename Iterator, typename Node, typename PreAction>
+auto ForEachNode(Node aRoot, const PreAction& aPreAction)
+ -> std::enable_if_t<std::is_same_v<decltype(aPreAction(aRoot)), void>,
+ void> {
+ ForEachNode<Iterator>(aRoot, aPreAction, [](Node aNode) {});
+}
+
+/*
+ * ForEachNode post-order traversal, using TraversalFlag.
+ */
+template <typename Iterator, typename Node, typename PostAction>
+auto ForEachNodePostOrder(Node aRoot, const PostAction& aPostAction)
+ -> std::enable_if_t<
+ std::is_same_v<decltype(aPostAction(aRoot)), TraversalFlag>, bool> {
+ return ForEachNode<Iterator>(
+ aRoot, [](Node aNode) { return TraversalFlag::Continue; }, aPostAction);
+}
+
+/*
+ * ForEachNode post-order, not using TraversalFlag.
+ */
+template <typename Iterator, typename Node, typename PostAction>
+auto ForEachNodePostOrder(Node aRoot, const PostAction& aPostAction)
+ -> std::enable_if_t<std::is_same_v<decltype(aPostAction(aRoot)), void>,
+ void> {
+ ForEachNode<Iterator>(
+ aRoot, [](Node aNode) {}, aPostAction);
+}
+
+/*
+ * Do a breadth-first search of the tree rooted at |aRoot|, and return the
+ * first visited node that satisfies |aCondition|, or nullptr if no such node
+ * was found.
+ *
+ * |Iterator| and |Node| have all the same requirements seen in ForEachNode()'s
+ * definition, but in addition to those, |Node| must be able to express a null
+ * value, returned from Node()
+ */
+template <typename Iterator, typename Node, typename Condition>
+Node BreadthFirstSearch(Node aRoot, const Condition& aCondition) {
+ if (!aRoot) {
+ return Node();
+ }
+
+ std::queue<Node> queue;
+ queue.push(aRoot);
+ while (!queue.empty()) {
+ Node node = queue.front();
+ queue.pop();
+
+ if (aCondition(node)) {
+ return node;
+ }
+
+ for (Node child = Iterator::FirstChild(node); child;
+ child = Iterator::NextSibling(child)) {
+ queue.push(child);
+ }
+ }
+
+ return Node();
+}
+
+/*
+ * Do a pre-order, depth-first search of the tree rooted at |aRoot|, and
+ * return the first visited node that satisfies |aCondition|, or nullptr
+ * if no such node was found.
+ *
+ * |Iterator| and |Node| have all the same requirements seen in ForEachNode()'s
+ * definition, but in addition to those, |Node| must be able to express a null
+ * value, returned from Node().
+ */
+template <typename Iterator, typename Node, typename Condition>
+Node DepthFirstSearch(Node aRoot, const Condition& aCondition) {
+ Node result = Node();
+
+ ForEachNode<Iterator>(aRoot, [&aCondition, &result](Node aNode) {
+ if (aCondition(aNode)) {
+ result = aNode;
+ return TraversalFlag::Abort;
+ }
+
+ return TraversalFlag::Continue;
+ });
+
+ return result;
+}
+
+/*
+ * Perform a post-order, depth-first search starting at aRoot.
+ *
+ * |Iterator| and |Node| have all the same requirements seen in ForEachNode()'s
+ * definition, but in addition to those, |Node| must be able to express a null
+ * value, returned from Node().
+ */
+template <typename Iterator, typename Node, typename Condition>
+Node DepthFirstSearchPostOrder(Node aRoot, const Condition& aCondition) {
+ Node result = Node();
+
+ ForEachNodePostOrder<Iterator>(aRoot, [&aCondition, &result](Node aNode) {
+ if (aCondition(aNode)) {
+ result = aNode;
+ return TraversalFlag::Abort;
+ }
+
+ return TraversalFlag::Continue;
+ });
+
+ return result;
+}
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_TreeTraversal_h
diff --git a/gfx/layers/UpdateImageHelper.h b/gfx/layers/UpdateImageHelper.h
new file mode 100644
index 0000000000..57ac237efe
--- /dev/null
+++ b/gfx/layers/UpdateImageHelper.h
@@ -0,0 +1,82 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GFX_UPDATEIMAGEHELPER_H
+#define GFX_UPDATEIMAGEHELPER_H
+
+#include "mozilla/layers/CompositorTypes.h"
+#include "mozilla/layers/ImageClient.h"
+#include "mozilla/layers/TextureClient.h"
+#include "mozilla/layers/TextureClientRecycleAllocator.h"
+#include "mozilla/layers/TextureWrapperImage.h"
+#include "mozilla/gfx/Types.h"
+
+namespace mozilla {
+namespace layers {
+
+class UpdateImageHelper {
+ public:
+ UpdateImageHelper(ImageContainer* aImageContainer, ImageClient* aImageClient,
+ gfx::IntSize aImageSize, gfx::SurfaceFormat aFormat)
+ : mImageContainer(aImageContainer),
+ mImageClient(aImageClient),
+ mImageSize(aImageSize),
+ mIsLocked(false) {
+ mTexture = mImageClient->GetTextureClientRecycler()->CreateOrRecycle(
+ aFormat, mImageSize, BackendSelector::Content, TextureFlags::DEFAULT);
+ if (!mTexture) {
+ return;
+ }
+
+ mIsLocked = mTexture->Lock(OpenMode::OPEN_WRITE_ONLY);
+ if (!mIsLocked) {
+ return;
+ }
+ }
+
+ ~UpdateImageHelper() {
+ if (mIsLocked) {
+ mTexture->Unlock();
+ mIsLocked = false;
+ }
+ }
+
+ already_AddRefed<gfx::DrawTarget> GetDrawTarget() {
+ RefPtr<gfx::DrawTarget> target;
+ if (mTexture) {
+ target = mTexture->BorrowDrawTarget();
+ }
+ return target.forget();
+ }
+
+ bool UpdateImage() {
+ if (!mTexture) {
+ return false;
+ }
+
+ if (mIsLocked) {
+ mTexture->Unlock();
+ mIsLocked = false;
+ }
+
+ RefPtr<TextureWrapperImage> image = new TextureWrapperImage(
+ mTexture, gfx::IntRect(gfx::IntPoint(0, 0), mImageSize));
+ mImageContainer->SetCurrentImageInTransaction(image);
+ return mImageClient->UpdateImage(mImageContainer);
+ }
+
+ private:
+ RefPtr<ImageContainer> mImageContainer;
+ RefPtr<ImageClient> mImageClient;
+ gfx::IntSize mImageSize;
+ RefPtr<TextureClient> mTexture;
+ bool mIsLocked;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // GFX_UPDATEIMAGEHELPER_H
diff --git a/gfx/layers/ZoomConstraints.cpp b/gfx/layers/ZoomConstraints.cpp
new file mode 100644
index 0000000000..97754d1e11
--- /dev/null
+++ b/gfx/layers/ZoomConstraints.cpp
@@ -0,0 +1,23 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ZoomConstraints.h"
+
+#include <ostream>
+#include "nsPrintfCString.h"
+
+namespace mozilla {
+namespace layers {
+
+std::ostream& operator<<(std::ostream& aStream, const ZoomConstraints& z) {
+ return aStream << nsPrintfCString("{ z=%d, dt=%d, min=%f, max=%f }",
+ z.mAllowZoom, z.mAllowDoubleTapZoom,
+ z.mMinZoom.scale, z.mMaxZoom.scale)
+ .get();
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/ZoomConstraints.h b/gfx/layers/ZoomConstraints.h
new file mode 100644
index 0000000000..51f9bfa0ec
--- /dev/null
+++ b/gfx/layers/ZoomConstraints.h
@@ -0,0 +1,66 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GFX_ZOOMCONSTRAINTS_H
+#define GFX_ZOOMCONSTRAINTS_H
+
+#include <iosfwd>
+
+#include "Units.h" // for CSSRect, CSSPixel, etc
+#include "mozilla/Maybe.h"
+#include "mozilla/layers/ScrollableLayerGuid.h" // for ScrollableLayerGuid
+
+namespace mozilla {
+namespace layers {
+
+struct ZoomConstraints {
+ bool mAllowZoom;
+ bool mAllowDoubleTapZoom;
+ CSSToParentLayerScale mMinZoom;
+ CSSToParentLayerScale mMaxZoom;
+
+ ZoomConstraints() : mAllowZoom(true), mAllowDoubleTapZoom(true) {
+ MOZ_COUNT_CTOR(ZoomConstraints);
+ }
+
+ ZoomConstraints(bool aAllowZoom, bool aAllowDoubleTapZoom,
+ const CSSToParentLayerScale& aMinZoom,
+ const CSSToParentLayerScale& aMaxZoom)
+ : mAllowZoom(aAllowZoom),
+ mAllowDoubleTapZoom(aAllowDoubleTapZoom),
+ mMinZoom(aMinZoom),
+ mMaxZoom(aMaxZoom) {
+ MOZ_COUNT_CTOR(ZoomConstraints);
+ }
+
+ ZoomConstraints(const ZoomConstraints& other)
+ : mAllowZoom(other.mAllowZoom),
+ mAllowDoubleTapZoom(other.mAllowDoubleTapZoom),
+ mMinZoom(other.mMinZoom),
+ mMaxZoom(other.mMaxZoom) {
+ MOZ_COUNT_CTOR(ZoomConstraints);
+ }
+
+ MOZ_COUNTED_DTOR(ZoomConstraints)
+
+ bool operator==(const ZoomConstraints& other) const {
+ return mAllowZoom == other.mAllowZoom &&
+ mAllowDoubleTapZoom == other.mAllowDoubleTapZoom &&
+ mMinZoom == other.mMinZoom && mMaxZoom == other.mMaxZoom;
+ }
+
+ bool operator!=(const ZoomConstraints& other) const {
+ return !(*this == other);
+ }
+
+ friend std::ostream& operator<<(std::ostream& aStream,
+ const ZoomConstraints& z);
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif /* GFX_ZOOMCONSTRAINTS_H */
diff --git a/gfx/layers/apz/public/APZInputBridge.h b/gfx/layers/apz/public/APZInputBridge.h
new file mode 100644
index 0000000000..07ed39548d
--- /dev/null
+++ b/gfx/layers/apz/public/APZInputBridge.h
@@ -0,0 +1,297 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_APZInputBridge_h
+#define mozilla_layers_APZInputBridge_h
+
+#include "Units.h" // for LayoutDeviceIntPoint
+#include "mozilla/EventForwards.h" // for WidgetInputEvent, nsEventStatus
+#include "mozilla/layers/APZPublicUtils.h" // for APZWheelAction
+#include "mozilla/layers/LayersTypes.h" // for ScrollDirections
+#include "mozilla/layers/ScrollableLayerGuid.h" // for ScrollableLayerGuid
+
+namespace mozilla {
+
+class InputData;
+
+namespace layers {
+
+class APZInputBridgeParent;
+class AsyncPanZoomController;
+class InputBlockState;
+class TouchBlockState;
+struct ScrollableLayerGuid;
+struct TargetConfirmationFlags;
+struct PointerEventsConsumableFlags;
+
+enum class APZHandledPlace : uint8_t {
+ Unhandled = 0, // we know for sure that the event will not be handled
+ // by either the root APZC or others
+ HandledByRoot = 1, // we know for sure that the event will be handled
+ // by the root content APZC
+ HandledByContent = 2, // we know for sure it will be handled by a non-root
+ // APZC or by an event listener using preventDefault()
+ // in a document
+ Invalid = 3,
+ Last = Invalid
+};
+
+struct APZHandledResult {
+ APZHandledPlace mPlace = APZHandledPlace::Invalid;
+ SideBits mScrollableDirections = SideBits::eNone;
+ ScrollDirections mOverscrollDirections = ScrollDirections();
+
+ APZHandledResult() = default;
+ // A constructor for cases where we have the target of the input block this
+ // event is part of, the target might be adjusted to be the root in the
+ // ScrollingDownWillMoveDynamicToolbar case.
+ //
+ // NOTE: There's a case where |aTarget| is the APZC for the root content but
+ // |aPlace| has to be `HandledByContent`, for example, the root content has
+ // an event handler using preventDefault() in the callback, so call sites of
+ // this function should be responsible to set a proper |aPlace|.
+ APZHandledResult(APZHandledPlace aPlace,
+ const AsyncPanZoomController* aTarget);
+ APZHandledResult(APZHandledPlace aPlace, SideBits aScrollableDirections,
+ ScrollDirections aOverscrollDirections)
+ : mPlace(aPlace),
+ mScrollableDirections(aScrollableDirections),
+ mOverscrollDirections(aOverscrollDirections) {}
+
+ bool IsHandledByContent() const {
+ return mPlace == APZHandledPlace::HandledByContent;
+ }
+ bool IsHandledByRoot() const {
+ return mPlace == APZHandledPlace::HandledByRoot;
+ }
+ bool operator==(const APZHandledResult& aOther) const {
+ return mPlace == aOther.mPlace &&
+ mScrollableDirections == aOther.mScrollableDirections &&
+ mOverscrollDirections == aOther.mOverscrollDirections;
+ }
+};
+
+/**
+ * Represents the outcome of APZ receiving and processing an input event.
+ * This is returned from APZInputBridge::ReceiveInputEvent() and related APIs.
+ */
+struct APZEventResult {
+ /**
+ * Creates a default result with a status of eIgnore, no block ID, and empty
+ * target guid.
+ */
+ APZEventResult();
+
+ /**
+ * Creates a result with a status of eIgnore, no block ID, the guid of the
+ * given initial target, and an APZHandledResult if we are sure the event
+ * is not going to be dispatched to contents.
+ */
+ APZEventResult(const RefPtr<AsyncPanZoomController>& aInitialTarget,
+ TargetConfirmationFlags aFlags);
+
+ void SetStatusAsConsumeNoDefault() {
+ mStatus = nsEventStatus_eConsumeNoDefault;
+ }
+
+ void SetStatusAsIgnore() { mStatus = nsEventStatus_eIgnore; }
+
+ // Set mStatus to nsEventStatus_eConsumeDoDefault and set mHandledResult
+ // depending on |aTarget|.
+ void SetStatusAsConsumeDoDefault(
+ const RefPtr<AsyncPanZoomController>& aTarget);
+
+ // Set mStatus to nsEventStatus_eConsumeDoDefault, unlike above
+ // SetStatusAsConsumeDoDefault(const RefPtr<AsyncPanZoomController>&) this
+ // function doesn't mutate mHandledResult.
+ void SetStatusAsConsumeDoDefault() {
+ mStatus = nsEventStatus_eConsumeDoDefault;
+ }
+
+ // Set mStatus to nsEventStatus_eConsumeDoDefault and set mHandledResult
+ // depending on |aBlock|'s target APZC.
+ void SetStatusAsConsumeDoDefault(const InputBlockState& aBlock);
+ // Set mStatus and mHandledResult for a touch event which is not dropped
+ // altogether (i.e. the status is not eConsumeNoDefault).
+ void SetStatusForTouchEvent(const InputBlockState& aBlock,
+ TargetConfirmationFlags aFlags,
+ PointerEventsConsumableFlags aConsumableFlags,
+ const AsyncPanZoomController* aTarget);
+
+ // Set mStatus and mHandledResult during in a stat of fast fling.
+ void SetStatusForFastFling(const TouchBlockState& aBlock,
+ TargetConfirmationFlags aFlags,
+ PointerEventsConsumableFlags aConsumableFlags,
+ const AsyncPanZoomController* aTarget);
+
+ // DO NOT USE THIS UpdateStatus DIRECTLY. THIS FUNCTION IS ONLY FOR
+ // SERIALIZATION / DESERIALIZATION OF THIS STRUCT IN IPC.
+ void UpdateStatus(nsEventStatus aStatus) { mStatus = aStatus; }
+ nsEventStatus GetStatus() const { return mStatus; };
+
+ // DO NOT USE THIS UpdateHandledResult DIRECTLY. THIS FUNCTION IS ONLY FOR
+ // SERIALIZATION / DESERIALIZATION OF THIS STRUCT IN IPC.
+ void UpdateHandledResult(const Maybe<APZHandledResult>& aHandledResult) {
+ mHandledResult = aHandledResult;
+ }
+ const Maybe<APZHandledResult>& GetHandledResult() const {
+ return mHandledResult;
+ }
+
+ bool WillHaveDelayedResult() const {
+ return GetStatus() != nsEventStatus_eConsumeNoDefault &&
+ !GetHandledResult();
+ }
+
+ private:
+ void UpdateHandledResult(const InputBlockState& aBlock,
+ PointerEventsConsumableFlags aConsumableFlags,
+ const AsyncPanZoomController* aTarget,
+ bool aDispatchToContent);
+
+ /**
+ * A status flag indicated how APZ handled the event.
+ * The interpretation of each value is as follows:
+ *
+ * nsEventStatus_eConsumeNoDefault is returned to indicate the
+ * APZ is consuming this event and the caller should discard the event with
+ * extreme prejudice. The exact scenarios under which this is returned is
+ * implementation-dependent and may vary.
+ * nsEventStatus_eIgnore is returned to indicate that the APZ code didn't
+ * use this event. This might be because it was directed at a point on
+ * the screen where there was no APZ, or because the thing the user was
+ * trying to do was not allowed. (For example, attempting to pan a
+ * non-pannable document).
+ * nsEventStatus_eConsumeDoDefault is returned to indicate that the APZ
+ * code may have used this event to do some user-visible thing. Note that
+ * in some cases CONSUMED is returned even if the event was NOT used. This
+ * is because we cannot always know at the time of event delivery whether
+ * the event will be used or not. So we err on the side of sending
+ * CONSUMED when we are uncertain.
+ */
+ nsEventStatus mStatus;
+
+ /**
+ * This is:
+ * - set to HandledByRoot if we know for sure that the event will be handled
+ * by the root content APZC;
+ * - set to HandledByContent if we know for sure it will not be;
+ * - left empty if we are unsure.
+ */
+ Maybe<APZHandledResult> mHandledResult;
+
+ public:
+ /**
+ * The guid of the APZC initially targeted by this event.
+ * This will usually be the APZC that handles the event, but in cases
+ * where the event is dispatched to content, it may end up being
+ * handled by a different APZC.
+ */
+ ScrollableLayerGuid mTargetGuid;
+ /**
+ * If this event started or was added to an input block, the id of that
+ * input block, otherwise InputBlockState::NO_BLOCK_ID.
+ */
+ uint64_t mInputBlockId;
+};
+
+/**
+ * This class lives in the main process, and is accessed via the controller
+ * thread (which is the process main thread for desktop, and the Java UI
+ * thread for Android). This class exposes a synchronous API to deliver
+ * incoming input events to APZ and modify them in-place to unapply the APZ
+ * async transform. If there is a GPU process, then this class does sync IPC
+ * calls over to the GPU process in order to accomplish this. Otherwise,
+ * APZCTreeManager overrides and implements these methods directly.
+ */
+class APZInputBridge {
+ public:
+ using InputBlockCallback = std::function<void(
+ uint64_t aInputBlockId, const APZHandledResult& aHandledResult)>;
+
+ /**
+ * General handler for incoming input events. Manipulates the frame metrics
+ * based on what type of input it is. For example, a PinchGestureEvent will
+ * cause scaling. This should only be called externally to this class, and
+ * must be called on the controller thread.
+ *
+ * This function transforms |aEvent| to have its coordinates in DOM space.
+ * This is so that the event can be passed through the DOM and content can
+ * handle them. The event may need to be converted to a WidgetInputEvent
+ * by the caller if it wants to do this.
+ *
+ * @param aEvent input event object; is modified in-place
+ * @param aCallback an optional callback to be invoked when the input block is
+ * ready for handling,
+ * @return The result of processing the event. Refer to the documentation of
+ * APZEventResult and its field.
+ */
+ virtual APZEventResult ReceiveInputEvent(
+ InputData& aEvent,
+ InputBlockCallback&& aCallback = InputBlockCallback()) = 0;
+
+ /**
+ * WidgetInputEvent handler. Transforms |aEvent| (which is assumed to be an
+ * already-existing instance of an WidgetInputEvent which may be an
+ * WidgetTouchEvent) to have its coordinates in DOM space. This is so that the
+ * event can be passed through the DOM and content can handle them.
+ *
+ * NOTE: Be careful of invoking the WidgetInputEvent variant. This can only be
+ * called on the main thread. See widget/InputData.h for more information on
+ * why we have InputData and WidgetInputEvent separated. If this function is
+ * used, the controller thread must be the main thread, or undefined behaviour
+ * may occur.
+ * NOTE: On unix, mouse events are treated as touch and are forwarded
+ * to the appropriate apz as such.
+ *
+ * See documentation for other ReceiveInputEvent above.
+ */
+ APZEventResult ReceiveInputEvent(
+ WidgetInputEvent& aEvent,
+ InputBlockCallback&& aCallback = InputBlockCallback());
+
+ // Returns the kind of wheel event action, if any, that will be (or was)
+ // performed by APZ. If this returns true, the event must not perform a
+ // synchronous scroll.
+ //
+ // Even if this returns Nothing(), all wheel events in APZ-aware widgets must
+ // be sent through APZ so they are transformed correctly for BrowserParent.
+ static Maybe<APZWheelAction> ActionForWheelEvent(WidgetWheelEvent* aEvent);
+
+ protected:
+ friend class APZInputBridgeParent;
+
+ // Methods to help process WidgetInputEvents (or manage conversion to/from
+ // InputData)
+
+ virtual void ProcessUnhandledEvent(LayoutDeviceIntPoint* aRefPoint,
+ ScrollableLayerGuid* aOutTargetGuid,
+ uint64_t* aOutFocusSequenceNumber,
+ LayersId* aOutLayersId) = 0;
+
+ virtual void UpdateWheelTransaction(
+ LayoutDeviceIntPoint aRefPoint, EventMessage aEventMessage,
+ const Maybe<ScrollableLayerGuid>& aTargetGuid) = 0;
+
+ virtual ~APZInputBridge() = default;
+};
+
+std::ostream& operator<<(std::ostream& aOut,
+ const APZHandledResult& aHandledResult);
+
+// This enum class is used for communicating between APZ and the browser gesture
+// support code. APZ needs to wait for the browser to send this response just
+// like APZ waits for the content's response if there's an APZ ware event
+// listener in the content process.
+enum class BrowserGestureResponse : bool {
+ NotConsumed = 0, // Representing the browser doesn't consume the gesture
+ Consumed = 1, // Representing the browser has started consuming the gesture.
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_APZInputBridge_h
diff --git a/gfx/layers/apz/public/APZPublicUtils.h b/gfx/layers/apz/public/APZPublicUtils.h
new file mode 100644
index 0000000000..6433008b4c
--- /dev/null
+++ b/gfx/layers/apz/public/APZPublicUtils.h
@@ -0,0 +1,82 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_APZPublicUtils_h
+#define mozilla_layers_APZPublicUtils_h
+
+// This file is for APZ-related utilities that need to be consumed from outside
+// of gfx/layers. For internal utilities, prefer APZUtils.h.
+
+#include <stdint.h>
+#include <utility>
+#include "ScrollAnimationBezierPhysics.h"
+#include "Units.h"
+#include "mozilla/DefineEnum.h"
+#include "mozilla/ScrollOrigin.h"
+#include "mozilla/gfx/Point.h"
+#include "mozilla/ScrollTypes.h"
+
+namespace mozilla {
+
+namespace layers {
+
+struct FrameMetrics;
+
+// clang-format off
+MOZ_DEFINE_ENUM_CLASS_WITH_BASE(APZWheelAction, uint8_t, (
+ Scroll,
+ PinchZoom
+))
+// clang-format on
+
+namespace apz {
+
+/**
+ * Initializes the global state used in AsyncPanZoomController.
+ * This is normally called when it is first needed in the constructor
+ * of APZCTreeManager, but can be called manually to force it to be
+ * initialized earlier.
+ */
+void InitializeGlobalState();
+
+/**
+ * See AsyncPanZoomController::CalculatePendingDisplayPort. This
+ * function simply delegates to that one, so that non-layers code
+ * never needs to include AsyncPanZoomController.h
+ */
+const ScreenMargin CalculatePendingDisplayPort(
+ const FrameMetrics& aFrameMetrics, const ParentLayerPoint& aVelocity);
+
+/**
+ * Returns a width and height multiplier, each of which is a power of two
+ * between 1 and 8 inclusive. The multiplier is chosen based on the provided
+ * base size, such that multiplier is larger when the base size is larger.
+ * The exact details are somewhat arbitrary and tuned by hand.
+ * This function is intended to only be used with WebRender, because that is
+ * the codepath that wants to use a larger displayport alignment, because
+ * moving the displayport is relatively expensive with WebRender.
+ */
+gfx::IntSize GetDisplayportAlignmentMultiplier(const ScreenSize& aBaseSize);
+
+/**
+ * Calculate the physics parameters for smooth scroll animations for the
+ * given origin, based on pref values.
+ */
+ScrollAnimationBezierPhysicsSettings ComputeBezierAnimationSettingsForOrigin(
+ ScrollOrigin aOrigin);
+
+/**
+ * Calculate if the scrolling should be instant or smooth based based on
+ * preferences and the origin
+ */
+ScrollMode GetScrollModeForOrigin(ScrollOrigin origin);
+
+} // namespace apz
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_APZPublicUtils_h
diff --git a/gfx/layers/apz/public/APZSampler.h b/gfx/layers/apz/public/APZSampler.h
new file mode 100644
index 0000000000..a870c2d688
--- /dev/null
+++ b/gfx/layers/apz/public/APZSampler.h
@@ -0,0 +1,154 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_APZSampler_h
+#define mozilla_layers_APZSampler_h
+
+#include <unordered_map>
+
+#include "apz/src/APZCTreeManager.h"
+#include "base/platform_thread.h" // for PlatformThreadId
+#include "mozilla/layers/APZUtils.h"
+#include "mozilla/layers/SampleTime.h"
+#include "mozilla/StaticMutex.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/webrender/WebRenderTypes.h"
+#include "Units.h"
+#include "VsyncSource.h"
+
+namespace mozilla {
+
+class TimeStamp;
+
+namespace wr {
+struct Transaction;
+class TransactionWrapper;
+struct WrWindowId;
+} // namespace wr
+
+namespace layers {
+
+struct ScrollbarData;
+
+/**
+ * This interface exposes APZ methods related to "sampling" (i.e. reading the
+ * async transforms produced by APZ). These methods should all be called on
+ * the sampler thread.
+ */
+class APZSampler {
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(APZSampler)
+
+ public:
+ APZSampler(const RefPtr<APZCTreeManager>& aApz, bool aIsUsingWebRender);
+
+ // Whoever creates this sampler is responsible for calling Destroy() on it
+ // before releasing the owning refptr.
+ void Destroy();
+
+ void SetWebRenderWindowId(const wr::WindowId& aWindowId);
+
+ /**
+ * This function is invoked from rust on the render backend thread when it
+ * is created. It effectively tells the APZSampler "the current thread is
+ * the sampler thread for this window id" and allows APZSampler to remember
+ * which thread it is.
+ */
+ static void SetSamplerThread(const wr::WrWindowId& aWindowId);
+ static void SampleForWebRender(const wr::WrWindowId& aWindowId,
+ const uint64_t* aGeneratedFrameId,
+ wr::Transaction* aTransaction);
+
+ void SetSampleTime(const SampleTime& aSampleTime);
+ void SampleForWebRender(const Maybe<VsyncId>& aGeneratedFrameId,
+ wr::TransactionWrapper& aTxn);
+
+ /**
+ * Similar to above GetCurrentAsyncTransform, but get the current transform
+ * with LayersId and ViewID.
+ * NOTE: This function should NOT be called on the compositor thread.
+ */
+ AsyncTransform GetCurrentAsyncTransform(
+ const LayersId& aLayersId, const ScrollableLayerGuid::ViewID& aScrollId,
+ AsyncTransformComponents aComponents,
+ const MutexAutoLock& aProofOfMapLock) const;
+
+ /**
+ * Returns the composition bounds of the APZC corresponding to the pair of
+ * |aLayersId| and |aScrollId|.
+ */
+ ParentLayerRect GetCompositionBounds(
+ const LayersId& aLayersId, const ScrollableLayerGuid::ViewID& aScrollId,
+ const MutexAutoLock& aProofOfMapLock) const;
+
+ struct ScrollOffsetAndRange {
+ CSSPoint mOffset;
+ CSSRect mRange;
+ };
+ /**
+ * Returns the scroll offset and scroll range of the APZC corresponding to the
+ * pair of |aLayersId| and |aScrollId|
+ *
+ * Note: This is called from OMTA Sampler thread, or Compositor thread for
+ * testing.
+ */
+ Maybe<ScrollOffsetAndRange> GetCurrentScrollOffsetAndRange(
+ const LayersId& aLayersId, const ScrollableLayerGuid::ViewID& aScrollId,
+ const MutexAutoLock& aProofOfMapLock) const;
+
+ /**
+ * This can be used to assert that the current thread is the
+ * sampler thread (which samples the async transform).
+ * This does nothing if thread assertions are disabled.
+ */
+ void AssertOnSamplerThread() const;
+
+ /**
+ * Returns true if currently on the APZSampler's "sampler thread".
+ */
+ bool IsSamplerThread() const;
+
+ template <typename Callback>
+ void CallWithMapLock(Callback& aCallback) {
+ mApz->CallWithMapLock(aCallback);
+ }
+
+ protected:
+ virtual ~APZSampler();
+
+ static already_AddRefed<APZSampler> GetSampler(
+ const wr::WrWindowId& aWindowId);
+
+ private:
+ RefPtr<APZCTreeManager> mApz;
+ bool mIsUsingWebRender;
+
+ // Used to manage the mapping from a WR window id to APZSampler. These are
+ // only used if WebRender is enabled. Both sWindowIdMap and mWindowId should
+ // only be used while holding the sWindowIdLock. Note that we use a
+ // StaticAutoPtr wrapper on sWindowIdMap to avoid a static initializer for the
+ // unordered_map. This also avoids the initializer/memory allocation in cases
+ // where we're not using WebRender.
+ static StaticMutex sWindowIdLock MOZ_UNANNOTATED;
+ static StaticAutoPtr<std::unordered_map<uint64_t, RefPtr<APZSampler>>>
+ sWindowIdMap;
+ Maybe<wr::WrWindowId> mWindowId;
+
+ // Lock used to protected mSamplerThreadId
+ mutable Mutex mThreadIdLock MOZ_UNANNOTATED;
+ // If WebRender is enabled, this holds the thread id of the render backend
+ // thread (which is the sampler thread) for the compositor associated with
+ // this APZSampler instance.
+ Maybe<PlatformThreadId> mSamplerThreadId;
+
+ Mutex mSampleTimeLock MOZ_UNANNOTATED;
+ // Can only be accessed or modified while holding mSampleTimeLock.
+ SampleTime mSampleTime;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_APZSampler_h
diff --git a/gfx/layers/apz/public/APZUpdater.h b/gfx/layers/apz/public/APZUpdater.h
new file mode 100644
index 0000000000..66d040b356
--- /dev/null
+++ b/gfx/layers/apz/public/APZUpdater.h
@@ -0,0 +1,236 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_APZUpdater_h
+#define mozilla_layers_APZUpdater_h
+
+#include <deque>
+#include <unordered_map>
+
+#include "base/platform_thread.h" // for PlatformThreadId
+#include "LayersTypes.h"
+#include "mozilla/layers/APZTestData.h"
+#include "mozilla/layers/WebRenderScrollData.h"
+#include "mozilla/StaticMutex.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/webrender/WebRenderTypes.h"
+#include "nsThreadUtils.h"
+#include "Units.h"
+
+namespace mozilla {
+
+namespace layers {
+
+class APZCTreeManager;
+class FocusTarget;
+class WebRenderScrollData;
+
+/**
+ * This interface is used to send updates or otherwise mutate APZ internal
+ * state. These functions is usually called from the compositor thread in
+ * response to IPC messages. The method implementations internally redispatch
+ * themselves to the updater thread in the case where the compositor thread
+ * is not the updater thread.
+ */
+class APZUpdater {
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(APZUpdater)
+
+ public:
+ APZUpdater(const RefPtr<APZCTreeManager>& aApz, bool aConnectedToWebRender);
+
+ bool HasTreeManager(const RefPtr<APZCTreeManager>& aApz);
+ void SetWebRenderWindowId(const wr::WindowId& aWindowId);
+
+ /**
+ * This function is invoked from rust on the scene builder thread when it
+ * is created. It effectively tells the APZUpdater "the current thread is
+ * the updater thread for this window id" and allows APZUpdater to remember
+ * which thread it is.
+ */
+ static void SetUpdaterThread(const wr::WrWindowId& aWindowId);
+ static void PrepareForSceneSwap(const wr::WrWindowId& aWindowId);
+ static void CompleteSceneSwap(const wr::WrWindowId& aWindowId,
+ const wr::WrPipelineInfo& aInfo);
+ static void ProcessPendingTasks(const wr::WrWindowId& aWindowId);
+
+ void ClearTree(LayersId aRootLayersId);
+ void UpdateFocusState(LayersId aRootLayerTreeId,
+ LayersId aOriginatingLayersId,
+ const FocusTarget& aFocusTarget);
+ /**
+ * This should be called (in the WR-enabled case) when the compositor receives
+ * a new WebRenderScrollData for a layers id. The |aScrollData| parameter is
+ * the scroll data for |aOriginatingLayersId| and |aEpoch| is the
+ * corresponding epoch for the transaction that transferred the scroll data.
+ * This function will store the new scroll data and update the focus state and
+ * hit-testing tree.
+ */
+ void UpdateScrollDataAndTreeState(LayersId aRootLayerTreeId,
+ LayersId aOriginatingLayersId,
+ const wr::Epoch& aEpoch,
+ WebRenderScrollData&& aScrollData);
+ /**
+ * This is called in the WR-enabled case when we get an empty transaction that
+ * has some scroll offset updates (from paint-skipped scrolling on the content
+ * side). This function will update the stored scroll offsets and the
+ * hit-testing tree.
+ */
+ void UpdateScrollOffsets(LayersId aRootLayerTreeId,
+ LayersId aOriginatingLayersId,
+ ScrollUpdatesMap&& aUpdates,
+ uint32_t aPaintSequenceNumber);
+
+ void NotifyLayerTreeAdopted(LayersId aLayersId,
+ const RefPtr<APZUpdater>& aOldUpdater);
+ void NotifyLayerTreeRemoved(LayersId aLayersId);
+
+ bool GetAPZTestData(LayersId aLayersId, APZTestData* aOutData);
+
+ void SetTestAsyncScrollOffset(LayersId aLayersId,
+ const ScrollableLayerGuid::ViewID& aScrollId,
+ const CSSPoint& aOffset);
+ void SetTestAsyncZoom(LayersId aLayersId,
+ const ScrollableLayerGuid::ViewID& aScrollId,
+ const LayerToParentLayerScale& aZoom);
+
+ // This can only be called on the updater thread.
+ const WebRenderScrollData* GetScrollData(LayersId aLayersId) const;
+
+ /**
+ * This can be used to assert that the current thread is the
+ * updater thread (which samples the async transform).
+ * This does nothing if thread assertions are disabled.
+ */
+ void AssertOnUpdaterThread() const;
+
+ /**
+ * Runs the given task on the APZ "updater thread" for this APZUpdater. If
+ * this function is called from the updater thread itself then the task is
+ * run immediately without getting queued.
+ *
+ * The layers id argument should be the id of the layer tree that is
+ * requesting this task to be run. Conceptually each layer tree has a separate
+ * task queue, so that if one layer tree is blocked waiting for a scene build
+ * then tasks for the other layer trees can still be processed.
+ */
+ void RunOnUpdaterThread(LayersId aLayersId, already_AddRefed<Runnable> aTask);
+
+ /**
+ * Returns true if currently on the APZUpdater's "updater thread".
+ */
+ bool IsUpdaterThread() const;
+
+ /**
+ * Dispatches the given task to the APZ "controller thread", but does it
+ * *from* the updater thread. That is, if the thread on which this function is
+ * called is not the updater thread, the task is first dispatched to the
+ * updater thread. When the updater thread runs it (or if this is called
+ * directly on the updater thread), that is when the task gets dispatched to
+ * the controller thread. The controller thread then actually runs the task.
+ *
+ * See the RunOnUpdaterThread method for details on the layers id argument.
+ */
+ void RunOnControllerThread(LayersId aLayersId,
+ already_AddRefed<Runnable> aTask);
+
+ void MarkAsDetached(LayersId aLayersId);
+
+ protected:
+ virtual ~APZUpdater();
+
+ // Return true if the APZUpdater is connected to WebRender and is
+ // using a WebRender scene builder thread as its updater thread.
+ // This is only false during GTests, and a shutdown codepath during
+ // which we create a dummy APZUpdater.
+ bool IsConnectedToWebRender() const;
+
+ static already_AddRefed<APZUpdater> GetUpdater(
+ const wr::WrWindowId& aWindowId);
+
+ void ProcessQueue();
+
+ private:
+ RefPtr<APZCTreeManager> mApz;
+ bool mDestroyed;
+ bool mConnectedToWebRender;
+
+ // Map from layers id to WebRenderScrollData. This can only be touched on
+ // the updater thread.
+ std::unordered_map<LayersId, WebRenderScrollData, LayersId::HashFn>
+ mScrollData;
+
+ // Stores epoch state for a particular layers id. This structure is only
+ // accessed on the updater thread.
+ struct EpochState {
+ // The epoch for the most recent scroll data sent from the content side.
+ wr::Epoch mRequired;
+ // The epoch for the most recent scene built and swapped in on the WR side.
+ Maybe<wr::Epoch> mBuilt;
+ // True if and only if the layers id is the root layers id for the
+ // compositor
+ bool mIsRoot;
+
+ EpochState();
+
+ // Whether or not the state for this layers id is such that it blocks
+ // processing of tasks for the layer tree. This happens if the root layers
+ // id or a "visible" layers id has scroll data for an epoch newer than what
+ // has been built. A "visible" layers id is one that is attached to the full
+ // layer tree (i.e. there is a chain of reflayer items from the root layer
+ // tree to the relevant layer subtree). This is not always the case; for
+ // instance a content process may send the compositor layers for a document
+ // before the chrome has attached the remote iframe to the root document.
+ // Since WR only builds pipelines for "visible" layers ids, |mBuilt| being
+ // populated means that the layers id is "visible".
+ bool IsBlocked() const;
+ };
+
+ // Map from layers id to epoch state.
+ // This data structure can only be touched on the updater thread.
+ std::unordered_map<LayersId, EpochState, LayersId::HashFn> mEpochData;
+
+ // Used to manage the mapping from a WR window id to APZUpdater. These are
+ // only used if WebRender is enabled. Both sWindowIdMap and mWindowId should
+ // only be used while holding the sWindowIdLock. Note that we use a
+ // StaticAutoPtr wrapper on sWindowIdMap to avoid a static initializer for the
+ // unordered_map. This also avoids the initializer/memory allocation in cases
+ // where we're not using WebRender.
+ static StaticMutex sWindowIdLock MOZ_UNANNOTATED;
+ static StaticAutoPtr<std::unordered_map<uint64_t, APZUpdater*>> sWindowIdMap;
+ Maybe<wr::WrWindowId> mWindowId;
+
+ // Lock used to protected mUpdaterThreadId;
+ mutable Mutex mThreadIdLock MOZ_UNANNOTATED;
+ // If WebRender and async scene building are enabled, this holds the thread id
+ // of the scene builder thread (which is the updater thread) for the
+ // compositor associated with this APZUpdater instance. It may be populated
+ // even if async scene building is not enabled, but in that case we don't
+ // care about the contents.
+ Maybe<PlatformThreadId> mUpdaterThreadId;
+
+ // Helper struct that pairs each queued runnable with the layers id that it is
+ // associated with. This allows us to easily implement the conceptual
+ // separation of mUpdaterQueue into independent queues per layers id.
+ struct QueuedTask {
+ LayersId mLayersId;
+ RefPtr<Runnable> mRunnable;
+ };
+
+ // Lock used to protect mUpdaterQueue
+ Mutex mQueueLock MOZ_UNANNOTATED;
+ // Holds a queue of tasks to be run on the updater thread, when the updater
+ // thread is a WebRender thread, since it won't have a message loop we can
+ // dispatch to. Note that although this is a single queue it is conceptually
+ // separated into multiple ones, one per layers id. Tasks for a given layers
+ // id will always run in FIFO order, but there is no guaranteed ordering for
+ // tasks with different layers ids.
+ std::deque<QueuedTask> mUpdaterQueue;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_APZUpdater_h
diff --git a/gfx/layers/apz/public/CompositorController.h b/gfx/layers/apz/public/CompositorController.h
new file mode 100644
index 0000000000..ad71a8faf5
--- /dev/null
+++ b/gfx/layers/apz/public/CompositorController.h
@@ -0,0 +1,33 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_CompositorController_h
+#define mozilla_layers_CompositorController_h
+
+#include "nsISupportsImpl.h" // for NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING
+#include "mozilla/Maybe.h"
+#include "mozilla/webrender/WebRenderTypes.h"
+
+namespace mozilla {
+namespace layers {
+
+class CompositorController {
+ public:
+ NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING
+
+ /**
+ * Ask the compositor to schedule a new composite.
+ */
+ virtual void ScheduleRenderOnCompositorThread(wr::RenderReasons aReasons) = 0;
+
+ protected:
+ virtual ~CompositorController() = default;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_CompositorController_h
diff --git a/gfx/layers/apz/public/GeckoContentController.h b/gfx/layers/apz/public/GeckoContentController.h
new file mode 100644
index 0000000000..abfef68caa
--- /dev/null
+++ b/gfx/layers/apz/public/GeckoContentController.h
@@ -0,0 +1,187 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_GeckoContentController_h
+#define mozilla_layers_GeckoContentController_h
+
+#include "GeckoContentControllerTypes.h"
+#include "InputData.h" // for PinchGestureInput
+#include "LayersTypes.h" // for ScrollDirection
+#include "Units.h" // for CSSPoint, CSSRect, etc
+#include "mozilla/Assertions.h" // for MOZ_ASSERT_HELPER2
+#include "mozilla/Attributes.h" // for MOZ_CAN_RUN_SCRIPT
+#include "mozilla/DefineEnum.h" // for MOZ_DEFINE_ENUM
+#include "mozilla/EventForwards.h" // for Modifiers
+#include "mozilla/layers/APZThreadUtils.h"
+#include "mozilla/layers/MatrixMessage.h" // for MatrixMessage
+#include "mozilla/layers/ScrollableLayerGuid.h" // for ScrollableLayerGuid, etc
+#include "nsISupportsImpl.h"
+
+namespace mozilla {
+
+class Runnable;
+
+namespace layers {
+
+struct RepaintRequest;
+
+class GeckoContentController {
+ public:
+ using APZStateChange = GeckoContentController_APZStateChange;
+ using TapType = GeckoContentController_TapType;
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(GeckoContentController)
+
+ /**
+ * Notifies the content side of the most recently computed transforms for
+ * each layers subtree to the root. The nsTArray will contain one
+ * MatrixMessage for each layers id in the current APZ tree, along with the
+ * corresponding transform.
+ */
+ virtual void NotifyLayerTransforms(nsTArray<MatrixMessage>&& aTransforms) = 0;
+
+ /**
+ * Requests a paint of the given RepaintRequest |aRequest| from Gecko.
+ * Implementations per-platform are responsible for actually handling this.
+ *
+ * This method must always be called on the repaint thread, which depends
+ * on the GeckoContentController. For ChromeProcessController it is the
+ * Gecko main thread, while for RemoteContentController it is the compositor
+ * thread where it can send IPDL messages.
+ */
+ virtual void RequestContentRepaint(const RepaintRequest& aRequest) = 0;
+
+ /**
+ * Requests handling of a tap event. |aPoint| is in LD pixels, relative to the
+ * current scroll offset.
+ */
+ MOZ_CAN_RUN_SCRIPT
+ virtual void HandleTap(TapType aType, const LayoutDevicePoint& aPoint,
+ Modifiers aModifiers, const ScrollableLayerGuid& aGuid,
+ uint64_t aInputBlockId) = 0;
+
+ /**
+ * When the apz.allow_zooming pref is set to false, the APZ will not
+ * translate pinch gestures to actual zooming. Instead, it will call this
+ * method to notify gecko of the pinch gesture, and allow it to deal with it
+ * however it wishes. Note that this function is not called if the pinch is
+ * prevented by content calling preventDefault() on the touch events, or via
+ * use of the touch-action property.
+ * @param aType One of PINCHGESTURE_START, PINCHGESTURE_SCALE,
+ * PINCHGESTURE_FINGERLIFTED, or PINCHGESTURE_END, indicating the phase
+ * of the pinch.
+ * @param aGuid The guid of the APZ that is detecting the pinch. This is
+ * generally the root APZC for the layers id.
+ * @param aFocusPoint The focus point of the pinch event.
+ * @param aSpanChange For the START or END event, this is always 0.
+ * For a SCALE event, this is the difference in span between the
+ * previous state and the new state.
+ * @param aModifiers The keyboard modifiers depressed during the pinch.
+ */
+ virtual void NotifyPinchGesture(PinchGestureInput::PinchGestureType aType,
+ const ScrollableLayerGuid& aGuid,
+ const LayoutDevicePoint& aFocusPoint,
+ LayoutDeviceCoord aSpanChange,
+ Modifiers aModifiers) = 0;
+
+ /**
+ * Schedules a runnable to run on the controller thread at some time
+ * in the future.
+ * This method must always be called on the controller thread.
+ */
+ virtual void PostDelayedTask(already_AddRefed<Runnable> aRunnable,
+ int aDelayMs) {
+ APZThreadUtils::DelayedDispatch(std::move(aRunnable), aDelayMs);
+ }
+
+ /**
+ * Returns true if we are currently on the thread that can send repaint
+ * requests.
+ */
+ virtual bool IsRepaintThread() = 0;
+
+ /**
+ * Runs the given task on the "repaint" thread.
+ */
+ virtual void DispatchToRepaintThread(already_AddRefed<Runnable> aTask) = 0;
+
+ /**
+ * General notices of APZ state changes for consumers.
+ * |aGuid| identifies the APZC originating the state change.
+ * |aChange| identifies the type of state change
+ * |aArg| is used by some state changes to pass extra information (see
+ * the documentation for each state change above)
+ * |aInputBlockId| is populated for the |eStartTouch| and |eEndTouch|
+ * state changes and identifies the input block of the
+ * gesture that triggers the state change.
+ */
+ virtual void NotifyAPZStateChange(const ScrollableLayerGuid& aGuid,
+ APZStateChange aChange, int aArg = 0,
+ Maybe<uint64_t> aInputBlockId = Nothing()) {
+ }
+
+ /**
+ * Notify content of a MozMouseScrollFailed event.
+ */
+ virtual void NotifyMozMouseScrollEvent(
+ const ScrollableLayerGuid::ViewID& aScrollId, const nsString& aEvent) {}
+
+ /**
+ * Notify content that the repaint requests have been flushed.
+ */
+ virtual void NotifyFlushComplete() = 0;
+
+ /**
+ * If the async scrollbar-drag initiation code kicks in on the APZ side, then
+ * we need to let content know that we are dragging the scrollbar. Otherwise,
+ * by the time the mousedown events is handled by content, the scrollthumb
+ * could already have been moved via a RequestContentRepaint message at a
+ * new scroll position, and the mousedown might end up triggering a click-to-
+ * scroll on where the thumb used to be.
+ */
+ virtual void NotifyAsyncScrollbarDragInitiated(
+ uint64_t aDragBlockId, const ScrollableLayerGuid::ViewID& aScrollId,
+ ScrollDirection aDirection) = 0;
+ virtual void NotifyAsyncScrollbarDragRejected(
+ const ScrollableLayerGuid::ViewID& aScrollId) = 0;
+
+ virtual void NotifyAsyncAutoscrollRejected(
+ const ScrollableLayerGuid::ViewID& aScrollId) = 0;
+
+ virtual void CancelAutoscroll(const ScrollableLayerGuid& aGuid) = 0;
+
+ virtual void NotifyScaleGestureComplete(const ScrollableLayerGuid& aGuid,
+ float aScale) = 0;
+
+ virtual void UpdateOverscrollVelocity(const ScrollableLayerGuid& aGuid,
+ float aX, float aY,
+ bool aIsRootContent) {}
+ virtual void UpdateOverscrollOffset(const ScrollableLayerGuid& aGuid,
+ float aX, float aY, bool aIsRootContent) {
+ }
+
+ GeckoContentController() = default;
+
+ /**
+ * Needs to be called on the main thread.
+ */
+ virtual void Destroy() {}
+
+ /**
+ * Whether this is RemoteContentController.
+ */
+ virtual bool IsRemote() { return false; }
+
+ virtual PresShell* GetTopLevelPresShell() const { return nullptr; };
+
+ protected:
+ // Protected destructor, to discourage deletion outside of Release():
+ virtual ~GeckoContentController() = default;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_GeckoContentController_h
diff --git a/gfx/layers/apz/public/GeckoContentControllerTypes.h b/gfx/layers/apz/public/GeckoContentControllerTypes.h
new file mode 100644
index 0000000000..8ab478eab5
--- /dev/null
+++ b/gfx/layers/apz/public/GeckoContentControllerTypes.h
@@ -0,0 +1,68 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_GeckoContentControllerTypes_h
+#define mozilla_layers_GeckoContentControllerTypes_h
+
+#include "mozilla/DefineEnum.h"
+
+namespace mozilla {
+namespace layers {
+
+// clang-format off
+MOZ_DEFINE_ENUM_CLASS(GeckoContentController_APZStateChange, (
+ /**
+ * APZ started modifying the view (including panning, zooming, and fling).
+ */
+ eTransformBegin,
+ /**
+ * APZ finished modifying the view.
+ */
+ eTransformEnd,
+ /**
+ * APZ started a touch.
+ * |aArg| is 1 if touch can be a pan, 0 otherwise.
+ */
+ eStartTouch,
+ /**
+ * APZ started a pan.
+ */
+ eStartPanning,
+ /**
+ * APZ finished processing a touch.
+ * |aArg| is 1 if touch was a click, 0 otherwise.
+ */
+ eEndTouch
+));
+// clang-format on
+
+/**
+ * Different types of tap-related events that can be sent in
+ * the HandleTap function. The names should be relatively self-explanatory.
+ * Note that the eLongTapUp will always be preceded by an eLongTap, but not
+ * all eLongTap notifications will be followed by an eLongTapUp (for instance,
+ * if the user moves their finger after triggering the long-tap but before
+ * lifting it).
+ * The difference between eDoubleTap and eSecondTap is subtle - the eDoubleTap
+ * is for an actual double-tap "gesture" while eSecondTap is for the same user
+ * input but where a double-tap gesture is not allowed. This is used to fire
+ * a click event with detail=2 to web content (similar to what a mouse double-
+ * click would do).
+ */
+// clang-format off
+MOZ_DEFINE_ENUM_CLASS(GeckoContentController_TapType, (
+ eSingleTap,
+ eDoubleTap,
+ eSecondTap,
+ eLongTap,
+ eLongTapUp
+));
+// clang-format on
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_GeckoContentControllerTypes_h
diff --git a/gfx/layers/apz/public/IAPZCTreeManager.h b/gfx/layers/apz/public/IAPZCTreeManager.h
new file mode 100644
index 0000000000..cd0859d34d
--- /dev/null
+++ b/gfx/layers/apz/public/IAPZCTreeManager.h
@@ -0,0 +1,150 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_IAPZCTreeManager_h
+#define mozilla_layers_IAPZCTreeManager_h
+
+#include <stdint.h> // for uint64_t, uint32_t
+
+#include "mozilla/layers/LayersTypes.h" // for TouchBehaviorFlags
+#include "mozilla/layers/ScrollableLayerGuid.h" // for ScrollableLayerGuid, etc
+#include "mozilla/layers/ZoomConstraints.h" // for ZoomConstraints
+#include "nsTArrayForwardDeclare.h" // for nsTArray, nsTArray_Impl, etc
+#include "nsISupportsImpl.h" // for MOZ_COUNT_CTOR, etc
+#include "Units.h" // for CSSRect, etc
+
+namespace mozilla {
+namespace layers {
+
+class APZInputBridge;
+class KeyboardMap;
+struct ZoomTarget;
+
+enum AllowedTouchBehavior {
+ NONE = 0,
+ VERTICAL_PAN = 1 << 0,
+ HORIZONTAL_PAN = 1 << 1,
+ PINCH_ZOOM = 1 << 2,
+ ANIMATING_ZOOM = 1 << 3,
+ UNKNOWN = 1 << 4
+};
+
+enum ZoomToRectBehavior : uint32_t {
+ DEFAULT_BEHAVIOR = 0,
+ DISABLE_ZOOM_OUT = 1 << 0,
+ PAN_INTO_VIEW_ONLY = 1 << 1,
+ ONLY_ZOOM_TO_DEFAULT_SCALE = 1 << 2,
+};
+
+enum class BrowserGestureResponse : bool;
+
+class AsyncDragMetrics;
+struct APZHandledResult;
+
+class IAPZCTreeManager {
+ NS_INLINE_DECL_THREADSAFE_VIRTUAL_REFCOUNTING(IAPZCTreeManager)
+
+ public:
+ /**
+ * Set the keyboard shortcuts to use for translating keyboard events.
+ */
+ virtual void SetKeyboardMap(const KeyboardMap& aKeyboardMap) = 0;
+
+ /**
+ * Kicks an animation to zoom to a rect. This may be either a zoom out or zoom
+ * in. The actual animation is done on the sampler thread after being set
+ * up. |aRect| must be given in CSS pixels, relative to the document.
+ * |aFlags| is a combination of the ZoomToRectBehavior enum values.
+ */
+ virtual void ZoomToRect(const ScrollableLayerGuid& aGuid,
+ const ZoomTarget& aZoomTarget,
+ const uint32_t aFlags = DEFAULT_BEHAVIOR) = 0;
+
+ /**
+ * If we have touch listeners, this should always be called when we know
+ * definitively whether or not content has preventDefaulted any touch events
+ * that have come in. If |aPreventDefault| is true, any touch events in the
+ * queue will be discarded. This function must be called on the controller
+ * thread.
+ */
+ virtual void ContentReceivedInputBlock(uint64_t aInputBlockId,
+ bool aPreventDefault) = 0;
+
+ /**
+ * When the event regions code is enabled, this function should be invoked to
+ * to confirm the target of the input block. This is only needed in cases
+ * where the initial input event of the block hit a dispatch-to-content region
+ * but is safe to call for all input blocks. This function should always be
+ * invoked on the controller thread.
+ * The different elements in the array of targets correspond to the targets
+ * for the different touch points. In the case where the touch point has no
+ * target, or the target is not a scrollable frame, the target's |mScrollId|
+ * should be set to ScrollableLayerGuid::NULL_SCROLL_ID.
+ */
+ virtual void SetTargetAPZC(uint64_t aInputBlockId,
+ const nsTArray<ScrollableLayerGuid>& aTargets) = 0;
+
+ /**
+ * Updates any zoom constraints contained in the <meta name="viewport"> tag.
+ * If the |aConstraints| is Nothing() then previously-provided constraints for
+ * the given |aGuid| are cleared.
+ */
+ virtual void UpdateZoomConstraints(
+ const ScrollableLayerGuid& aGuid,
+ const Maybe<ZoomConstraints>& aConstraints) = 0;
+
+ virtual void SetDPI(float aDpiValue) = 0;
+
+ /**
+ * Sets allowed touch behavior values for current touch-session for specific
+ * input block (determined by aInputBlock).
+ * Should be invoked by the widget. Each value of the aValues arrays
+ * corresponds to the different touch point that is currently active.
+ * Must be called after receiving the TOUCH_START event that starts the
+ * touch-session.
+ * This must be called on the controller thread.
+ */
+ virtual void SetAllowedTouchBehavior(
+ uint64_t aInputBlockId, const nsTArray<TouchBehaviorFlags>& aValues) = 0;
+
+ virtual void SetBrowserGestureResponse(uint64_t aInputBlockId,
+ BrowserGestureResponse aResponse) = 0;
+
+ virtual void StartScrollbarDrag(const ScrollableLayerGuid& aGuid,
+ const AsyncDragMetrics& aDragMetrics) = 0;
+
+ virtual bool StartAutoscroll(const ScrollableLayerGuid& aGuid,
+ const ScreenPoint& aAnchorLocation) = 0;
+
+ virtual void StopAutoscroll(const ScrollableLayerGuid& aGuid) = 0;
+
+ /**
+ * Function used to disable LongTap gestures.
+ *
+ * On slow running tests, drags and touch events can be misinterpreted
+ * as a long tap. This allows tests to disable long tap gesture detection.
+ */
+ virtual void SetLongTapEnabled(bool aTapGestureEnabled) = 0;
+
+ /**
+ * Returns an APZInputBridge interface that can be used to send input
+ * events to APZ in a synchronous manner. This will always be non-null, and
+ * the returned object's lifetime will match the lifetime of this
+ * IAPZCTreeManager implementation.
+ * It is only valid to call this function in the UI process.
+ */
+ virtual APZInputBridge* InputBridge() = 0;
+
+ protected:
+ // Discourage destruction outside of decref
+
+ virtual ~IAPZCTreeManager() = default;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_IAPZCTreeManager_h
diff --git a/gfx/layers/apz/public/MatrixMessage.h b/gfx/layers/apz/public/MatrixMessage.h
new file mode 100644
index 0000000000..45050f7406
--- /dev/null
+++ b/gfx/layers/apz/public/MatrixMessage.h
@@ -0,0 +1,64 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_MatrixMessage_h
+#define mozilla_layers_MatrixMessage_h
+
+#include "mozilla/Maybe.h"
+#include "mozilla/gfx/Matrix.h"
+#include "mozilla/layers/LayersTypes.h"
+#include "Units.h" // for ScreenRect
+#include "UnitTransforms.h"
+
+namespace mozilla {
+namespace layers {
+class MatrixMessage {
+ public:
+ // Don't use this one directly
+ MatrixMessage() = default;
+
+ MatrixMessage(const Maybe<LayerToScreenMatrix4x4>& aMatrix,
+ const ScreenRect& aTopLevelViewportVisibleRectInBrowserCoords,
+ const LayersId& aLayersId)
+ : mMatrix(ToUnknownMatrix(aMatrix)),
+ mTopLevelViewportVisibleRectInBrowserCoords(
+ aTopLevelViewportVisibleRectInBrowserCoords),
+ mLayersId(aLayersId) {}
+
+ inline Maybe<LayerToScreenMatrix4x4> GetMatrix() const {
+ return LayerToScreenMatrix4x4::FromUnknownMatrix(mMatrix);
+ }
+
+ inline ScreenRect GetTopLevelViewportVisibleRectInBrowserCoords() const {
+ return mTopLevelViewportVisibleRectInBrowserCoords;
+ }
+
+ inline const LayersId& GetLayersId() const { return mLayersId; }
+
+ bool operator==(const MatrixMessage& aOther) const {
+ return aOther.mMatrix == mMatrix &&
+ aOther.mTopLevelViewportVisibleRectInBrowserCoords ==
+ mTopLevelViewportVisibleRectInBrowserCoords &&
+ aOther.mLayersId == mLayersId;
+ }
+
+ bool operator!=(const MatrixMessage& aOther) const {
+ return !(*this == aOther);
+ }
+ // Fields are public for IPC. Don't access directly
+ // elsewhere.
+ // Transform matrix to convert this layer to screen coordinate.
+ Maybe<gfx::Matrix4x4> mMatrix; // Untyped for IPC
+ // The remote iframe document rectangle corresponding to this layer.
+ // The rectangle is the result of clipped out by ancestor async scrolling so
+ // that the rectangle will be empty if it's completely scrolled out of view.
+ ScreenRect mTopLevelViewportVisibleRectInBrowserCoords;
+ LayersId mLayersId;
+};
+}; // namespace layers
+}; // namespace mozilla
+
+#endif // mozilla_layers_MatrixMessage_h
diff --git a/gfx/layers/apz/src/APZCTreeManager.cpp b/gfx/layers/apz/src/APZCTreeManager.cpp
new file mode 100644
index 0000000000..9dd296dd97
--- /dev/null
+++ b/gfx/layers/apz/src/APZCTreeManager.cpp
@@ -0,0 +1,3742 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include <stack>
+#include <unordered_set>
+#include "APZCTreeManager.h"
+#include "AsyncPanZoomController.h"
+#include "Compositor.h" // for Compositor
+#include "DragTracker.h" // for DragTracker
+#include "GenericFlingAnimation.h" // for FLING_LOG
+#include "HitTestingTreeNode.h" // for HitTestingTreeNode
+#include "InputBlockState.h" // for InputBlockState
+#include "InputData.h" // for InputData, etc
+#include "WRHitTester.h" // for WRHitTester
+#include "mozilla/RecursiveMutex.h"
+#include "mozilla/dom/MouseEventBinding.h" // for MouseEvent constants
+#include "mozilla/dom/BrowserParent.h" // for AreRecordReplayTabsActive
+#include "mozilla/dom/Touch.h" // for Touch
+#include "mozilla/gfx/CompositorHitTestInfo.h"
+#include "mozilla/gfx/LoggingConstants.h"
+#include "mozilla/gfx/gfxVars.h" // for gfxVars
+#include "mozilla/gfx/GPUParent.h" // for GPUParent
+#include "mozilla/gfx/Logging.h" // for gfx::TreeLog
+#include "mozilla/gfx/Point.h" // for Point
+#include "mozilla/layers/APZSampler.h" // for APZSampler
+#include "mozilla/layers/APZThreadUtils.h" // for AssertOnControllerThread, etc
+#include "mozilla/layers/APZUpdater.h" // for APZUpdater
+#include "mozilla/layers/APZUtils.h" // for AsyncTransform
+#include "mozilla/layers/AsyncDragMetrics.h" // for AsyncDragMetrics
+#include "mozilla/layers/CompositorBridgeParent.h" // for CompositorBridgeParent, etc
+#include "mozilla/layers/DoubleTapToZoom.h" // for ZoomTarget
+#include "mozilla/layers/MatrixMessage.h"
+#include "mozilla/layers/UiCompositorControllerParent.h"
+#include "mozilla/layers/WebRenderScrollDataWrapper.h"
+#include "mozilla/MouseEvents.h"
+#include "mozilla/mozalloc.h" // for operator new
+#include "mozilla/Preferences.h" // for Preferences
+#include "mozilla/StaticPrefs_accessibility.h"
+#include "mozilla/StaticPrefs_apz.h"
+#include "mozilla/StaticPrefs_layout.h"
+#include "mozilla/ToString.h"
+#include "mozilla/TouchEvents.h"
+#include "mozilla/EventStateManager.h" // for WheelPrefs
+#include "mozilla/webrender/WebRenderAPI.h"
+#include "nsDebug.h" // for NS_WARNING
+#include "nsPoint.h" // for nsIntPoint
+#include "nsThreadUtils.h" // for NS_IsMainThread
+#include "ScrollThumbUtils.h" // for ComputeTransformForScrollThumb
+#include "OverscrollHandoffState.h" // for OverscrollHandoffState
+#include "TreeTraversal.h" // for ForEachNode, BreadthFirstSearch, etc
+#include "Units.h" // for ParentlayerPixel
+#include "GestureEventListener.h" // for GestureEventListener::setLongTapEnabled
+#include "UnitTransforms.h" // for ViewAs
+
+mozilla::LazyLogModule mozilla::layers::APZCTreeManager::sLog("apz.manager");
+#define APZCTM_LOG(...) \
+ MOZ_LOG(APZCTreeManager::sLog, LogLevel::Debug, (__VA_ARGS__))
+
+static mozilla::LazyLogModule sApzKeyLog("apz.key");
+#define APZ_KEY_LOG(...) MOZ_LOG(sApzKeyLog, LogLevel::Debug, (__VA_ARGS__))
+
+namespace mozilla {
+namespace layers {
+
+using mozilla::gfx::CompositorHitTestFlags;
+using mozilla::gfx::CompositorHitTestInfo;
+using mozilla::gfx::CompositorHitTestInvisibleToHit;
+using mozilla::gfx::LOG_DEFAULT;
+
+typedef mozilla::gfx::Point Point;
+typedef mozilla::gfx::Point4D Point4D;
+typedef mozilla::gfx::Matrix4x4 Matrix4x4;
+
+typedef CompositorBridgeParent::LayerTreeState LayerTreeState;
+
+struct APZCTreeManager::TreeBuildingState {
+ TreeBuildingState(LayersId aRootLayersId, bool aIsFirstPaint,
+ LayersId aOriginatingLayersId, APZTestData* aTestData,
+ uint32_t aPaintSequence)
+ : mIsFirstPaint(aIsFirstPaint),
+ mOriginatingLayersId(aOriginatingLayersId),
+ mPaintLogger(aTestData, aPaintSequence) {
+ CompositorBridgeParent::CallWithIndirectShadowTree(
+ aRootLayersId, [this](LayerTreeState& aState) -> void {
+ mCompositorController = aState.GetCompositorController();
+ });
+ }
+
+ typedef std::unordered_map<AsyncPanZoomController*, gfx::Matrix4x4>
+ DeferredTransformMap;
+
+ // State that doesn't change as we recurse in the tree building
+ RefPtr<CompositorController> mCompositorController;
+ const bool mIsFirstPaint;
+ const LayersId mOriginatingLayersId;
+ const APZPaintLogHelper mPaintLogger;
+
+ // State that is updated as we perform the tree build
+
+ // A list of nodes that need to be destroyed at the end of the tree building.
+ // This is initialized with all nodes in the old tree, and nodes are removed
+ // from it as we reuse them in the new tree.
+ nsTArray<RefPtr<HitTestingTreeNode>> mNodesToDestroy;
+
+ // This map is populated as we place APZCs into the new tree. Its purpose is
+ // to facilitate re-using the same APZC for different layers that scroll
+ // together (and thus have the same ScrollableLayerGuid). The presShellId
+ // doesn't matter for this purpose, and we move the map to the APZCTreeManager
+ // after we're done building, so it's useful to have the presshell-ignoring
+ // map for that.
+ std::unordered_map<ScrollableLayerGuid, ApzcMapData,
+ ScrollableLayerGuid::HashIgnoringPresShellFn,
+ ScrollableLayerGuid::EqualIgnoringPresShellFn>
+ mApzcMap;
+
+ // This is populated with all the HitTestingTreeNodes that are scroll thumbs
+ // and have a scrollthumb animation id (which indicates that they need to be
+ // sampled for WebRender on the sampler thread).
+ std::vector<HitTestingTreeNode*> mScrollThumbs;
+ // This is populated with all the scroll target nodes. We use in conjunction
+ // with mScrollThumbs to build APZCTreeManager::mScrollThumbInfo.
+ std::unordered_map<ScrollableLayerGuid, HitTestingTreeNode*,
+ ScrollableLayerGuid::HashIgnoringPresShellFn,
+ ScrollableLayerGuid::EqualIgnoringPresShellFn>
+ mScrollTargets;
+
+ // During the tree building process, the perspective transform component
+ // of the ancestor transforms of some APZCs can be "deferred" to their
+ // children, meaning they are added to the children's ancestor transforms
+ // instead. Those deferred transforms are tracked here.
+ DeferredTransformMap mPerspectiveTransformsDeferredToChildren;
+
+ // As we recurse down through the tree, this picks up the zoom animation id
+ // from a node in the layer tree, and propagates it downwards to the nearest
+ // APZC instance that is for an RCD node. Generally it will be set on the
+ // root node of the layers (sub-)tree, which may not be same as the RCD node
+ // for the subtree, and so we need this mechanism to ensure it gets propagated
+ // to the RCD's APZC instance. Once it is set on the APZC instance, the value
+ // is cleared back to Nothing(). Note that this is only used in the WebRender
+ // codepath.
+ Maybe<uint64_t> mZoomAnimationId;
+
+ // See corresponding members of APZCTreeManager. These are the same thing, but
+ // on the tree-walking state. They are populated while walking the tree in
+ // a layers update, and then moved into APZCTreeManager.
+ std::vector<FixedPositionInfo> mFixedPositionInfo;
+ std::vector<RootScrollbarInfo> mRootScrollbarInfo;
+ std::vector<StickyPositionInfo> mStickyPositionInfo;
+
+ // As we recurse down through reflayers in the tree, this picks up the
+ // cumulative EventRegionsOverride flags from the reflayers, and is used to
+ // apply them to descendant layers.
+ std::stack<EventRegionsOverride> mOverrideFlags;
+};
+
+class APZCTreeManager::CheckerboardFlushObserver : public nsIObserver {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIOBSERVER
+
+ explicit CheckerboardFlushObserver(APZCTreeManager* aTreeManager)
+ : mTreeManager(aTreeManager) {
+ MOZ_ASSERT(NS_IsMainThread());
+ nsCOMPtr<nsIObserverService> obsSvc =
+ mozilla::services::GetObserverService();
+ MOZ_ASSERT(obsSvc);
+ if (obsSvc) {
+ obsSvc->AddObserver(this, "APZ:FlushActiveCheckerboard", false);
+ }
+ }
+
+ void Unregister() {
+ MOZ_ASSERT(NS_IsMainThread());
+ nsCOMPtr<nsIObserverService> obsSvc =
+ mozilla::services::GetObserverService();
+ if (obsSvc) {
+ obsSvc->RemoveObserver(this, "APZ:FlushActiveCheckerboard");
+ }
+ mTreeManager = nullptr;
+ }
+
+ protected:
+ virtual ~CheckerboardFlushObserver() = default;
+
+ private:
+ RefPtr<APZCTreeManager> mTreeManager;
+};
+
+NS_IMPL_ISUPPORTS(APZCTreeManager::CheckerboardFlushObserver, nsIObserver)
+
+NS_IMETHODIMP
+APZCTreeManager::CheckerboardFlushObserver::Observe(nsISupports* aSubject,
+ const char* aTopic,
+ const char16_t*) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mTreeManager.get());
+
+ RecursiveMutexAutoLock lock(mTreeManager->mTreeLock);
+ if (mTreeManager->mRootNode) {
+ ForEachNode<ReverseIterator>(
+ mTreeManager->mRootNode.get(), [](HitTestingTreeNode* aNode) {
+ if (aNode->IsPrimaryHolder()) {
+ MOZ_ASSERT(aNode->GetApzc());
+ aNode->GetApzc()->FlushActiveCheckerboardReport();
+ }
+ });
+ }
+ if (XRE_IsGPUProcess()) {
+ if (gfx::GPUParent* gpu = gfx::GPUParent::GetSingleton()) {
+ nsCString topic("APZ:FlushActiveCheckerboard:Done");
+ Unused << gpu->SendNotifyUiObservers(topic);
+ }
+ } else {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ nsCOMPtr<nsIObserverService> obsSvc =
+ mozilla::services::GetObserverService();
+ if (obsSvc) {
+ obsSvc->NotifyObservers(nullptr, "APZ:FlushActiveCheckerboard:Done",
+ nullptr);
+ }
+ }
+ return NS_OK;
+}
+
+/**
+ * A RAII class used for setting the focus sequence number on input events
+ * as they are being processed. Any input event is assumed to be potentially
+ * focus changing unless explicitly marked otherwise.
+ */
+class MOZ_RAII AutoFocusSequenceNumberSetter {
+ public:
+ AutoFocusSequenceNumberSetter(FocusState& aFocusState, InputData& aEvent)
+ : mFocusState(aFocusState), mEvent(aEvent), mMayChangeFocus(true) {}
+
+ void MarkAsNonFocusChanging() { mMayChangeFocus = false; }
+
+ ~AutoFocusSequenceNumberSetter() {
+ if (mMayChangeFocus) {
+ mFocusState.ReceiveFocusChangingEvent();
+
+ APZ_KEY_LOG(
+ "Marking input with type=%d as focus changing with seq=%" PRIu64 "\n",
+ static_cast<int>(mEvent.mInputType),
+ mFocusState.LastAPZProcessedEvent());
+ } else {
+ APZ_KEY_LOG(
+ "Marking input with type=%d as non focus changing with seq=%" PRIu64
+ "\n",
+ static_cast<int>(mEvent.mInputType),
+ mFocusState.LastAPZProcessedEvent());
+ }
+
+ mEvent.mFocusSequenceNumber = mFocusState.LastAPZProcessedEvent();
+ }
+
+ private:
+ FocusState& mFocusState;
+ InputData& mEvent;
+ bool mMayChangeFocus;
+};
+
+APZCTreeManager::APZCTreeManager(LayersId aRootLayersId,
+ UniquePtr<IAPZHitTester> aHitTester)
+ : mTestSampleTime(Nothing(), "APZCTreeManager::mTestSampleTime"),
+ mInputQueue(new InputQueue()),
+ mRootLayersId(aRootLayersId),
+ mSampler(nullptr),
+ mUpdater(nullptr),
+ mTreeLock("APZCTreeLock"),
+ mMapLock("APZCMapLock"),
+ mRetainedTouchIdentifier(-1),
+ mInScrollbarTouchDrag(false),
+ mCurrentMousePosition(ScreenPoint(),
+ "APZCTreeManager::mCurrentMousePosition"),
+ mApzcTreeLog("apzctree"),
+ mTestDataLock("APZTestDataLock"),
+ mDPI(160.0),
+ mHitTester(std::move(aHitTester)),
+ mScrollGenerationLock("APZScrollGenerationLock") {
+ RefPtr<APZCTreeManager> self(this);
+ NS_DispatchToMainThread(NS_NewRunnableFunction(
+ "layers::APZCTreeManager::APZCTreeManager",
+ [self] { self->mFlushObserver = new CheckerboardFlushObserver(self); }));
+ AsyncPanZoomController::InitializeGlobalState();
+ mApzcTreeLog.ConditionOnPrefFunction(StaticPrefs::apz_printtree);
+
+ if (!mHitTester) {
+ mHitTester = MakeUnique<WRHitTester>();
+ }
+ mHitTester->Initialize(this);
+}
+
+APZCTreeManager::~APZCTreeManager() = default;
+
+void APZCTreeManager::SetSampler(APZSampler* aSampler) {
+ // We're either setting the sampler or clearing it
+ MOZ_ASSERT((mSampler == nullptr) != (aSampler == nullptr));
+ mSampler = aSampler;
+}
+
+void APZCTreeManager::SetUpdater(APZUpdater* aUpdater) {
+ // We're either setting the updater or clearing it
+ MOZ_ASSERT((mUpdater == nullptr) != (aUpdater == nullptr));
+ mUpdater = aUpdater;
+}
+
+void APZCTreeManager::NotifyLayerTreeAdopted(
+ LayersId aLayersId, const RefPtr<APZCTreeManager>& aOldApzcTreeManager) {
+ AssertOnUpdaterThread();
+
+ if (aOldApzcTreeManager) {
+ aOldApzcTreeManager->mFocusState.RemoveFocusTarget(aLayersId);
+ // While we could move the focus target information from the old APZC tree
+ // manager into this one, it's safer to not do that, as we'll probably have
+ // that information repopulated soon anyway (on the next layers update).
+ }
+
+ UniquePtr<APZTestData> adoptedData;
+ if (aOldApzcTreeManager) {
+ MutexAutoLock lock(aOldApzcTreeManager->mTestDataLock);
+ auto it = aOldApzcTreeManager->mTestData.find(aLayersId);
+ if (it != aOldApzcTreeManager->mTestData.end()) {
+ adoptedData = std::move(it->second);
+ aOldApzcTreeManager->mTestData.erase(it);
+ }
+ }
+ if (adoptedData) {
+ MutexAutoLock lock(mTestDataLock);
+ mTestData[aLayersId] = std::move(adoptedData);
+ }
+}
+
+void APZCTreeManager::NotifyLayerTreeRemoved(LayersId aLayersId) {
+ AssertOnUpdaterThread();
+
+ mFocusState.RemoveFocusTarget(aLayersId);
+
+ { // scope lock
+ MutexAutoLock lock(mTestDataLock);
+ mTestData.erase(aLayersId);
+ }
+}
+
+AsyncPanZoomController* APZCTreeManager::NewAPZCInstance(
+ LayersId aLayersId, GeckoContentController* aController) {
+ return new AsyncPanZoomController(
+ aLayersId, this, mInputQueue, aController,
+ AsyncPanZoomController::USE_GESTURE_DETECTOR);
+}
+
+void APZCTreeManager::SetTestSampleTime(const Maybe<TimeStamp>& aTime) {
+ auto testSampleTime = mTestSampleTime.Lock();
+ testSampleTime.ref() = aTime;
+}
+
+SampleTime APZCTreeManager::GetFrameTime() {
+ auto testSampleTime = mTestSampleTime.Lock();
+ if (testSampleTime.ref()) {
+ return SampleTime::FromTest(*testSampleTime.ref());
+ }
+ return SampleTime::FromNow();
+}
+
+void APZCTreeManager::SetAllowedTouchBehavior(
+ uint64_t aInputBlockId, const nsTArray<TouchBehaviorFlags>& aValues) {
+ if (!APZThreadUtils::IsControllerThread()) {
+ APZThreadUtils::RunOnControllerThread(
+ NewRunnableMethod<uint64_t,
+ StoreCopyPassByLRef<nsTArray<TouchBehaviorFlags>>>(
+ "layers::APZCTreeManager::SetAllowedTouchBehavior", this,
+ &APZCTreeManager::SetAllowedTouchBehavior, aInputBlockId,
+ aValues.Clone()));
+ return;
+ }
+
+ APZThreadUtils::AssertOnControllerThread();
+
+ mInputQueue->SetAllowedTouchBehavior(aInputBlockId, aValues);
+}
+
+void APZCTreeManager::SetBrowserGestureResponse(
+ uint64_t aInputBlockId, BrowserGestureResponse aResponse) {
+ if (!APZThreadUtils::IsControllerThread()) {
+ APZThreadUtils::RunOnControllerThread(
+ NewRunnableMethod<uint64_t, BrowserGestureResponse>(
+ "layers::APZCTreeManager::SetBrowserGestureResponse", this,
+ &APZCTreeManager::SetBrowserGestureResponse, aInputBlockId,
+ aResponse));
+ return;
+ }
+
+ APZThreadUtils::AssertOnControllerThread();
+
+ mInputQueue->SetBrowserGestureResponse(aInputBlockId, aResponse);
+}
+
+void APZCTreeManager::UpdateHitTestingTree(
+ const WebRenderScrollDataWrapper& aRoot, bool aIsFirstPaint,
+ LayersId aOriginatingLayersId, uint32_t aPaintSequenceNumber) {
+ AssertOnUpdaterThread();
+
+ RecursiveMutexAutoLock lock(mTreeLock);
+
+ // For testing purposes, we log some data to the APZTestData associated with
+ // the layers id that originated this update.
+ APZTestData* testData = nullptr;
+ if (StaticPrefs::apz_test_logging_enabled()) {
+ MutexAutoLock lock(mTestDataLock);
+ UniquePtr<APZTestData> ptr = MakeUnique<APZTestData>();
+ auto result =
+ mTestData.insert(std::make_pair(aOriginatingLayersId, std::move(ptr)));
+ testData = result.first->second.get();
+ testData->StartNewPaint(aPaintSequenceNumber);
+ }
+
+ TreeBuildingState state(mRootLayersId, aIsFirstPaint, aOriginatingLayersId,
+ testData, aPaintSequenceNumber);
+
+ // We do this business with collecting the entire tree into an array because
+ // otherwise it's very hard to determine which APZC instances need to be
+ // destroyed. In the worst case, there are two scenarios: (a) a layer with an
+ // APZC is removed from the layer tree and (b) a layer with an APZC is moved
+ // in the layer tree from one place to a completely different place. In
+ // scenario (a) we would want to destroy the APZC while walking the layer tree
+ // and noticing that the layer/APZC is no longer there. But if we do that then
+ // we run into a problem in scenario (b) because we might encounter that layer
+ // later during the walk. To handle both of these we have to 'remember' that
+ // the layer was not found, and then do the destroy only at the end of the
+ // tree walk after we are sure that the layer was removed and not just
+ // transplanted elsewhere. Doing that as part of a recursive tree walk is hard
+ // and so maintaining a list and removing APZCs that are still alive is much
+ // simpler.
+ ForEachNode<ReverseIterator>(mRootNode.get(),
+ [&state](HitTestingTreeNode* aNode) {
+ state.mNodesToDestroy.AppendElement(aNode);
+ });
+ mRootNode = nullptr;
+ mAsyncZoomContainerSubtree = Nothing();
+ int asyncZoomContainerNestingDepth = 0;
+ bool haveNestedAsyncZoomContainers = false;
+ nsTArray<LayersId> subtreesWithRootContentOutsideAsyncZoomContainer;
+
+ if (aRoot) {
+ std::unordered_set<LayersId, LayersId::HashFn> seenLayersIds;
+ std::stack<gfx::TreeAutoIndent<gfx::LOG_CRITICAL>> indents;
+ std::stack<AncestorTransform> ancestorTransforms;
+ HitTestingTreeNode* parent = nullptr;
+ HitTestingTreeNode* next = nullptr;
+ LayersId layersId = mRootLayersId;
+ seenLayersIds.insert(mRootLayersId);
+ ancestorTransforms.push(AncestorTransform());
+ state.mOverrideFlags.push(EventRegionsOverride::NoOverride);
+ nsTArray<Maybe<ZoomConstraints>> zoomConstraintsStack;
+
+ // push a nothing to be used for anything outside an async zoom container
+ zoomConstraintsStack.AppendElement(Nothing());
+
+ mApzcTreeLog << "[start]\n";
+ mTreeLock.AssertCurrentThreadIn();
+
+ ForEachNode<ReverseIterator>(
+ aRoot,
+ [&](ScrollNode aLayerMetrics) {
+ if (auto asyncZoomContainerId =
+ aLayerMetrics.GetAsyncZoomContainerId()) {
+ if (asyncZoomContainerNestingDepth > 0) {
+ haveNestedAsyncZoomContainers = true;
+ }
+ mAsyncZoomContainerSubtree = Some(layersId);
+ ++asyncZoomContainerNestingDepth;
+
+ auto it = mZoomConstraints.find(
+ ScrollableLayerGuid(layersId, 0, *asyncZoomContainerId));
+ if (it != mZoomConstraints.end()) {
+ zoomConstraintsStack.AppendElement(Some(it->second));
+ } else {
+ zoomConstraintsStack.AppendElement(Nothing());
+ }
+ }
+
+ if (aLayerMetrics.Metrics().IsRootContent()) {
+ MutexAutoLock lock(mMapLock);
+ mGeckoFixedLayerMargins =
+ aLayerMetrics.Metrics().GetFixedLayerMargins();
+ } else {
+ MOZ_ASSERT(aLayerMetrics.Metrics().GetFixedLayerMargins() ==
+ ScreenMargin(),
+ "fixed-layer-margins should be 0 on non-root layer");
+ }
+
+ // Note that this check happens after the potential increment of
+ // asyncZoomContainerNestingDepth, to allow the root content
+ // metadata to be on the same node as the async zoom container.
+ if (aLayerMetrics.Metrics().IsRootContent() &&
+ asyncZoomContainerNestingDepth == 0) {
+ subtreesWithRootContentOutsideAsyncZoomContainer.AppendElement(
+ layersId);
+ }
+
+ HitTestingTreeNode* node = PrepareNodeForLayer(
+ lock, aLayerMetrics, aLayerMetrics.Metrics(), layersId,
+ zoomConstraintsStack.LastElement(), ancestorTransforms.top(),
+ parent, next, state);
+ MOZ_ASSERT(node);
+ AsyncPanZoomController* apzc = node->GetApzc();
+ aLayerMetrics.SetApzc(apzc);
+
+ // GetScrollbarAnimationId is only set when webrender is enabled,
+ // which limits the extra thumb mapping work to the webrender-enabled
+ // case where it is needed.
+ // Note also that when webrender is enabled, a "valid" animation id
+ // is always nonzero, so we don't need to worry about handling the
+ // case where WR is enabled and the animation id is zero.
+ if (node->GetScrollbarAnimationId()) {
+ if (node->IsScrollThumbNode()) {
+ state.mScrollThumbs.push_back(node);
+ } else if (node->IsScrollbarContainerNode()) {
+ // Only scrollbar containers for the root have an animation id.
+ state.mRootScrollbarInfo.emplace_back(
+ *(node->GetScrollbarAnimationId()),
+ node->GetScrollbarDirection());
+ }
+ }
+
+ // GetFixedPositionAnimationId is only set when webrender is enabled.
+ if (node->GetFixedPositionAnimationId().isSome()) {
+ state.mFixedPositionInfo.emplace_back(node);
+ }
+ // GetStickyPositionAnimationId is only set when webrender is enabled.
+ if (node->GetStickyPositionAnimationId().isSome()) {
+ state.mStickyPositionInfo.emplace_back(node);
+ }
+ if (apzc && node->IsPrimaryHolder()) {
+ state.mScrollTargets[apzc->GetGuid()] = node;
+ }
+
+ // Accumulate the CSS transform between layers that have an APZC.
+ // In the terminology of the big comment above
+ // APZCTreeManager::GetScreenToApzcTransform, if we are at layer M,
+ // then aAncestorTransform is NC * OC * PC, and we left-multiply MC
+ // and compute ancestorTransform to be MC * NC * OC * PC. This gets
+ // passed down as the ancestor transform to layer L when we recurse
+ // into the children below. If we are at a layer with an APZC, such as
+ // P, then we reset the ancestorTransform to just PC, to start the new
+ // accumulation as we go down.
+ AncestorTransform currentTransform{
+ aLayerMetrics.GetTransform(),
+ aLayerMetrics.TransformIsPerspective()};
+ if (!apzc) {
+ currentTransform = currentTransform * ancestorTransforms.top();
+ }
+ ancestorTransforms.push(currentTransform);
+
+ // Note that |node| at this point will not have any children,
+ // otherwise we we would have to set next to node->GetFirstChild().
+ MOZ_ASSERT(!node->GetFirstChild());
+ parent = node;
+ next = nullptr;
+
+ // Update the layersId if we have a new one
+ if (Maybe<LayersId> newLayersId = aLayerMetrics.GetReferentId()) {
+ layersId = *newLayersId;
+ seenLayersIds.insert(layersId);
+
+ // Propagate any event region override flags down into all
+ // descendant nodes from the reflayer that has the flag. This is an
+ // optimization to avoid having to walk up the tree to check the
+ // override flags. Note that we don't keep the flags on the reflayer
+ // itself, because the semantics of the flags are that they apply
+ // to all content in the layer subtree being referenced. This
+ // matters with the WR hit-test codepath, because this reflayer may
+ // be just one of many nodes associated with a particular APZC, and
+ // calling GetTargetNode with a guid may return any one of the
+ // nodes. If different nodes have different flags on them that can
+ // make the WR hit-test result incorrect, but being strict about
+ // only putting the flags on descendant layers avoids this problem.
+ state.mOverrideFlags.push(state.mOverrideFlags.top() |
+ aLayerMetrics.GetEventRegionsOverride());
+ }
+
+ indents.push(gfx::TreeAutoIndent<gfx::LOG_CRITICAL>(mApzcTreeLog));
+ },
+ [&](ScrollNode aLayerMetrics) {
+ if (aLayerMetrics.GetAsyncZoomContainerId()) {
+ --asyncZoomContainerNestingDepth;
+ zoomConstraintsStack.RemoveLastElement();
+ }
+ if (aLayerMetrics.GetReferentId()) {
+ state.mOverrideFlags.pop();
+ }
+
+ next = parent;
+ parent = parent->GetParent();
+ layersId = next->GetLayersId();
+ ancestorTransforms.pop();
+ indents.pop();
+ });
+
+ mApzcTreeLog << "[end]\n";
+
+ MOZ_ASSERT(
+ !mAsyncZoomContainerSubtree ||
+ !subtreesWithRootContentOutsideAsyncZoomContainer.Contains(
+ *mAsyncZoomContainerSubtree),
+ "If there is an async zoom container, all scroll nodes with root "
+ "content scroll metadata should be inside it");
+ MOZ_ASSERT(!haveNestedAsyncZoomContainers,
+ "Should not have nested async zoom container");
+
+ // If we have perspective transforms deferred to children, do another
+ // walk of the tree and actually apply them to the children.
+ // We can't do this "as we go" in the previous traversal, because by the
+ // time we realize we need to defer a perspective transform for an APZC,
+ // we may already have processed a previous layer (including children
+ // found in its subtree) that shares that APZC.
+ if (!state.mPerspectiveTransformsDeferredToChildren.empty()) {
+ ForEachNode<ReverseIterator>(
+ mRootNode.get(), [&state](HitTestingTreeNode* aNode) {
+ AsyncPanZoomController* apzc = aNode->GetApzc();
+ if (!apzc) {
+ return;
+ }
+ if (!aNode->IsPrimaryHolder()) {
+ return;
+ }
+
+ AsyncPanZoomController* parent = apzc->GetParent();
+ if (!parent) {
+ return;
+ }
+
+ auto it =
+ state.mPerspectiveTransformsDeferredToChildren.find(parent);
+ if (it != state.mPerspectiveTransformsDeferredToChildren.end()) {
+ apzc->SetAncestorTransform(AncestorTransform{
+ it->second * apzc->GetAncestorTransform(), false});
+ }
+ });
+ }
+
+ // Remove any layers ids for which we no longer have content from
+ // mDetachedLayersIds.
+ for (auto iter = mDetachedLayersIds.begin();
+ iter != mDetachedLayersIds.end();) {
+ // unordered_set::erase() invalidates the iterator pointing to the
+ // element being erased, but returns an iterator to the next element.
+ if (seenLayersIds.find(*iter) == seenLayersIds.end()) {
+ iter = mDetachedLayersIds.erase(iter);
+ } else {
+ ++iter;
+ }
+ }
+ }
+
+ // We do not support tree structures where the root node has siblings.
+ MOZ_ASSERT(!(mRootNode && mRootNode->GetPrevSibling()));
+
+ { // scope lock and update our mApzcMap before we destroy all the unused
+ // APZC instances
+ MutexAutoLock lock(mMapLock);
+ mApzcMap = std::move(state.mApzcMap);
+
+ for (auto& mapping : mApzcMap) {
+ AsyncPanZoomController* parent = mapping.second.apzc->GetParent();
+ mapping.second.parent = parent ? Some(parent->GetGuid()) : Nothing();
+ }
+
+ mScrollThumbInfo.clear();
+ // For non-webrender, state.mScrollThumbs will be empty so this will be a
+ // no-op.
+ for (HitTestingTreeNode* thumb : state.mScrollThumbs) {
+ MOZ_ASSERT(thumb->IsScrollThumbNode());
+ ScrollableLayerGuid targetGuid(thumb->GetLayersId(), 0,
+ thumb->GetScrollTargetId());
+ auto it = state.mScrollTargets.find(targetGuid);
+ if (it == state.mScrollTargets.end()) {
+ // It could be that |thumb| is a scrollthumb for content which didn't
+ // have an APZC, for example if the content isn't layerized. Regardless,
+ // we can't async-scroll it so we don't need to worry about putting it
+ // in mScrollThumbInfo.
+ continue;
+ }
+ HitTestingTreeNode* target = it->second;
+ mScrollThumbInfo.emplace_back(
+ *(thumb->GetScrollbarAnimationId()), thumb->GetTransform(),
+ thumb->GetScrollbarData(), targetGuid, target->GetTransform(),
+ target->IsAncestorOf(thumb));
+ }
+
+ mRootScrollbarInfo = std::move(state.mRootScrollbarInfo);
+ mFixedPositionInfo = std::move(state.mFixedPositionInfo);
+ mStickyPositionInfo = std::move(state.mStickyPositionInfo);
+ }
+
+ for (size_t i = 0; i < state.mNodesToDestroy.Length(); i++) {
+ APZCTM_LOG("Destroying node at %p with APZC %p\n",
+ state.mNodesToDestroy[i].get(),
+ state.mNodesToDestroy[i]->GetApzc());
+ state.mNodesToDestroy[i]->Destroy();
+ }
+
+ APZCTM_LOG("APZCTreeManager (%p)\n", this);
+ if (mRootNode && MOZ_LOG_TEST(sLog, LogLevel::Debug)) {
+ mRootNode->Dump(" ");
+ }
+ SendSubtreeTransformsToChromeMainThread(nullptr);
+}
+
+void APZCTreeManager::UpdateFocusState(LayersId aRootLayerTreeId,
+ LayersId aOriginatingLayersId,
+ const FocusTarget& aFocusTarget) {
+ AssertOnUpdaterThread();
+
+ if (!StaticPrefs::apz_keyboard_enabled_AtStartup()) {
+ return;
+ }
+
+ mFocusState.Update(aRootLayerTreeId, aOriginatingLayersId, aFocusTarget);
+}
+
+void APZCTreeManager::SampleForWebRender(const Maybe<VsyncId>& aVsyncId,
+ wr::TransactionWrapper& aTxn,
+ const SampleTime& aSampleTime) {
+ AssertOnSamplerThread();
+ MutexAutoLock lock(mMapLock);
+
+ RefPtr<WebRenderBridgeParent> wrBridgeParent;
+ RefPtr<CompositorController> controller;
+ CompositorBridgeParent::CallWithIndirectShadowTree(
+ mRootLayersId, [&](LayerTreeState& aState) -> void {
+ controller = aState.GetCompositorController();
+ wrBridgeParent = aState.mWrBridge;
+ });
+
+ bool activeAnimations = AdvanceAnimationsInternal(lock, aSampleTime);
+ if (activeAnimations && controller) {
+ controller->ScheduleRenderOnCompositorThread(
+ wr::RenderReasons::ANIMATED_PROPERTY);
+ }
+
+ nsTArray<wr::WrTransformProperty> transforms;
+
+ // Sample async transforms on scrollable layers.
+ for (const auto& mapping : mApzcMap) {
+ AsyncPanZoomController* apzc = mapping.second.apzc;
+
+ if (Maybe<CompositionPayload> payload = apzc->NotifyScrollSampling()) {
+ if (wrBridgeParent && aVsyncId) {
+ wrBridgeParent->AddPendingScrollPayload(*payload, *aVsyncId);
+ }
+ }
+
+ if (StaticPrefs::apz_test_logging_enabled()) {
+ MutexAutoLock lock(mTestDataLock);
+
+ ScrollableLayerGuid guid = apzc->GetGuid();
+ auto it = mTestData.find(guid.mLayersId);
+ if (it != mTestData.end()) {
+ it->second->RecordSampledResult(
+ apzc->GetCurrentAsyncVisualViewport(
+ AsyncPanZoomController::eForCompositing)
+ .TopLeft(),
+ (aSampleTime.Time() - TimeStamp::ProcessCreation())
+ .ToMicroseconds(),
+ guid.mLayersId, guid.mScrollId);
+ }
+ }
+
+ if (Maybe<uint64_t> zoomAnimationId = apzc->GetZoomAnimationId()) {
+ // for now we only support zooming on root content APZCs
+ MOZ_ASSERT(apzc->IsRootContent());
+
+ LayoutDeviceToParentLayerScale zoom = apzc->GetCurrentPinchZoomScale(
+ AsyncPanZoomController::eForCompositing);
+
+ AsyncTransform asyncVisualTransform = apzc->GetCurrentAsyncTransform(
+ AsyncPanZoomController::eForCompositing,
+ AsyncTransformComponents{AsyncTransformComponent::eVisual});
+
+ transforms.AppendElement(wr::ToWrTransformProperty(
+ *zoomAnimationId, LayoutDeviceToParentLayerMatrix4x4::Scaling(
+ zoom.scale, zoom.scale, 1.0f) *
+ AsyncTransformComponentMatrix::Translation(
+ asyncVisualTransform.mTranslation)));
+
+ aTxn.UpdateIsTransformAsyncZooming(*zoomAnimationId,
+ apzc->IsAsyncZooming());
+ }
+
+ nsTArray<wr::SampledScrollOffset> sampledOffsets =
+ apzc->GetSampledScrollOffsets();
+ aTxn.UpdateScrollPosition(wr::AsPipelineId(apzc->GetGuid().mLayersId),
+ apzc->GetGuid().mScrollId, sampledOffsets);
+
+#if defined(MOZ_WIDGET_ANDROID)
+ // Send the root frame metrics to java through the UIController
+ RefPtr<UiCompositorControllerParent> uiController =
+ UiCompositorControllerParent::GetFromRootLayerTreeId(mRootLayersId);
+ if (uiController &&
+ apzc->UpdateRootFrameMetricsIfChanged(mLastRootMetrics)) {
+ uiController->NotifyUpdateScreenMetrics(mLastRootMetrics);
+ }
+#endif
+ }
+
+ // Now collect all the async transforms needed for the scrollthumbs.
+ for (const ScrollThumbInfo& info : mScrollThumbInfo) {
+ auto it = mApzcMap.find(info.mTargetGuid);
+ if (it == mApzcMap.end()) {
+ // It could be that |info| is a scrollthumb for content which didn't
+ // have an APZC, for example if the content isn't layerized. Regardless,
+ // we can't async-scroll it so we don't need to worry about putting it
+ // in mScrollThumbInfo.
+ continue;
+ }
+ AsyncPanZoomController* scrollTargetApzc = it->second.apzc;
+ MOZ_ASSERT(scrollTargetApzc);
+ LayerToParentLayerMatrix4x4 transform =
+ scrollTargetApzc->CallWithLastContentPaintMetrics(
+ [&](const FrameMetrics& aMetrics) {
+ return ComputeTransformForScrollThumb(
+ info.mThumbTransform * AsyncTransformMatrix(),
+ info.mTargetTransform.ToUnknownMatrix(), scrollTargetApzc,
+ aMetrics, info.mThumbData, info.mTargetIsAncestor);
+ });
+ transforms.AppendElement(
+ wr::ToWrTransformProperty(info.mThumbAnimationId, transform));
+ }
+
+ // Move the root scrollbar in response to the dynamic toolbar transition.
+ for (const RootScrollbarInfo& info : mRootScrollbarInfo) {
+ // We only care about the horizontal scrollbar.
+ if (info.mScrollDirection == ScrollDirection::eHorizontal) {
+ ScreenPoint translation =
+ apz::ComputeFixedMarginsOffset(GetCompositorFixedLayerMargins(lock),
+ SideBits::eBottom, ScreenMargin());
+
+ LayerToParentLayerMatrix4x4 transform =
+ LayerToParentLayerMatrix4x4::Translation(ViewAs<ParentLayerPixel>(
+ translation, PixelCastJustification::ScreenIsParentLayerForRoot));
+
+ transforms.AppendElement(
+ wr::ToWrTransformProperty(info.mScrollbarAnimationId, transform));
+ }
+ }
+
+ for (const FixedPositionInfo& info : mFixedPositionInfo) {
+ MOZ_ASSERT(info.mFixedPositionAnimationId.isSome());
+ if (!IsFixedToRootContent(info, lock)) {
+ continue;
+ }
+
+ ScreenPoint translation = apz::ComputeFixedMarginsOffset(
+ GetCompositorFixedLayerMargins(lock), info.mFixedPosSides,
+ mGeckoFixedLayerMargins);
+
+ LayerToParentLayerMatrix4x4 transform =
+ LayerToParentLayerMatrix4x4::Translation(ViewAs<ParentLayerPixel>(
+ translation, PixelCastJustification::ScreenIsParentLayerForRoot));
+
+ transforms.AppendElement(
+ wr::ToWrTransformProperty(*info.mFixedPositionAnimationId, transform));
+ }
+
+ for (const StickyPositionInfo& info : mStickyPositionInfo) {
+ MOZ_ASSERT(info.mStickyPositionAnimationId.isSome());
+ SideBits sides = SidesStuckToRootContent(info, lock);
+ if (sides == SideBits::eNone) {
+ continue;
+ }
+
+ ScreenPoint translation = apz::ComputeFixedMarginsOffset(
+ GetCompositorFixedLayerMargins(lock), sides,
+ // For sticky layers, we don't need to factor
+ // mGeckoFixedLayerMargins because Gecko doesn't shift the
+ // position of sticky elements for dynamic toolbar movements.
+ ScreenMargin());
+
+ LayerToParentLayerMatrix4x4 transform =
+ LayerToParentLayerMatrix4x4::Translation(ViewAs<ParentLayerPixel>(
+ translation, PixelCastJustification::ScreenIsParentLayerForRoot));
+
+ transforms.AppendElement(
+ wr::ToWrTransformProperty(*info.mStickyPositionAnimationId, transform));
+ }
+
+ aTxn.AppendTransformProperties(transforms);
+}
+
+ParentLayerRect APZCTreeManager::ComputeClippedCompositionBounds(
+ const MutexAutoLock& aProofOfMapLock, ClippedCompositionBoundsMap& aDestMap,
+ ScrollableLayerGuid aGuid) {
+ if (auto iter = aDestMap.find(aGuid); iter != aDestMap.end()) {
+ // We already computed it for this one, early-exit. This might happen
+ // because on a later iteration of mApzcMap we might encounter an ancestor
+ // of an APZC that we processed on an earlier iteration. In this case we
+ // would have computed the ancestor's clipped composition bounds when
+ // recursing up on the earlier iteration.
+ return iter->second;
+ }
+
+ ParentLayerRect bounds = mApzcMap[aGuid].apzc->GetCompositionBounds();
+ const auto& mapEntry = mApzcMap.find(aGuid);
+ MOZ_ASSERT(mapEntry != mApzcMap.end());
+ if (mapEntry->second.parent.isNothing()) {
+ // Recursion base case, where the APZC with guid `aGuid` has no parent.
+ // In this case, we don't need to clip `bounds` any further and can just
+ // early exit.
+ aDestMap.emplace(aGuid, bounds);
+ return bounds;
+ }
+
+ ScrollableLayerGuid parentGuid = mapEntry->second.parent.value();
+ auto parentBoundsEntry = aDestMap.find(parentGuid);
+ // If aDestMap doesn't contain the parent entry yet, we recurse to compute
+ // that one first.
+ ParentLayerRect parentClippedBounds =
+ (parentBoundsEntry == aDestMap.end())
+ ? ComputeClippedCompositionBounds(aProofOfMapLock, aDestMap,
+ parentGuid)
+ : parentBoundsEntry->second;
+
+ // The parent layer's async transform applies to the current layer to take
+ // `bounds` into the same coordinate space as `parentClippedBounds`. However,
+ // we're going to do the inverse operation and unapply this transform to
+ // `parentClippedBounds` to bring it into the same coordinate space as
+ // `bounds`.
+ AsyncTransform appliesToLayer =
+ mApzcMap[parentGuid].apzc->GetCurrentAsyncTransform(
+ AsyncPanZoomController::eForCompositing);
+
+ // Do the unapplication
+ LayerRect parentClippedBoundsInParentLayerSpace =
+ (parentClippedBounds - appliesToLayer.mTranslation) /
+ appliesToLayer.mScale;
+
+ // And then clip `bounds` by the parent's comp bounds in the current space.
+ bounds = bounds.Intersect(
+ ViewAs<ParentLayerPixel>(parentClippedBoundsInParentLayerSpace,
+ PixelCastJustification::MovingDownToChildren));
+
+ // Done!
+ aDestMap.emplace(aGuid, bounds);
+ return bounds;
+}
+
+bool APZCTreeManager::AdvanceAnimationsInternal(
+ const MutexAutoLock& aProofOfMapLock, const SampleTime& aSampleTime) {
+ ClippedCompositionBoundsMap clippedCompBounds;
+ bool activeAnimations = false;
+ for (const auto& mapping : mApzcMap) {
+ AsyncPanZoomController* apzc = mapping.second.apzc;
+ // Note that this call is recursive, but it early-exits if called again
+ // with the same guid. So this loop is still amortized O(n) with respect to
+ // the number of APZCs.
+ ParentLayerRect clippedBounds = ComputeClippedCompositionBounds(
+ aProofOfMapLock, clippedCompBounds, mapping.first);
+
+ apzc->ReportCheckerboard(aSampleTime, clippedBounds);
+ activeAnimations |= apzc->AdvanceAnimations(aSampleTime);
+ }
+ return activeAnimations;
+}
+
+void APZCTreeManager::PrintLayerInfo(const ScrollNode& aLayer) {
+ if (StaticPrefs::apz_printtree() && aLayer.Dump(mApzcTreeLog) > 0) {
+ mApzcTreeLog << "\n";
+ }
+}
+
+// mTreeLock is held, and checked with static analysis
+void APZCTreeManager::AttachNodeToTree(HitTestingTreeNode* aNode,
+ HitTestingTreeNode* aParent,
+ HitTestingTreeNode* aNextSibling) {
+ if (aNextSibling) {
+ aNextSibling->SetPrevSibling(aNode);
+ } else if (aParent) {
+ aParent->SetLastChild(aNode);
+ } else {
+ MOZ_ASSERT(!mRootNode);
+ mRootNode = aNode;
+ aNode->MakeRoot();
+ }
+}
+
+already_AddRefed<HitTestingTreeNode> APZCTreeManager::RecycleOrCreateNode(
+ const RecursiveMutexAutoLock& aProofOfTreeLock, TreeBuildingState& aState,
+ AsyncPanZoomController* aApzc, LayersId aLayersId) {
+ // Find a node without an APZC and return it. Note that unless the layer tree
+ // actually changes, this loop should generally do an early-return on the
+ // first iteration, so it should be cheap in the common case.
+ for (int32_t i = aState.mNodesToDestroy.Length() - 1; i >= 0; i--) {
+ RefPtr<HitTestingTreeNode> node = aState.mNodesToDestroy[i];
+ if (node->IsRecyclable(aProofOfTreeLock)) {
+ aState.mNodesToDestroy.RemoveElementAt(i);
+ node->RecycleWith(aProofOfTreeLock, aApzc, aLayersId);
+ return node.forget();
+ }
+ }
+ RefPtr<HitTestingTreeNode> node =
+ new HitTestingTreeNode(aApzc, false, aLayersId);
+ return node.forget();
+}
+
+void APZCTreeManager::StartScrollbarDrag(const ScrollableLayerGuid& aGuid,
+ const AsyncDragMetrics& aDragMetrics) {
+ if (!APZThreadUtils::IsControllerThread()) {
+ APZThreadUtils::RunOnControllerThread(
+ NewRunnableMethod<ScrollableLayerGuid, AsyncDragMetrics>(
+ "layers::APZCTreeManager::StartScrollbarDrag", this,
+ &APZCTreeManager::StartScrollbarDrag, aGuid, aDragMetrics));
+ return;
+ }
+
+ APZThreadUtils::AssertOnControllerThread();
+
+ RefPtr<AsyncPanZoomController> apzc = GetTargetAPZC(aGuid);
+ if (!apzc) {
+ NotifyScrollbarDragRejected(aGuid);
+ return;
+ }
+
+ uint64_t inputBlockId = aDragMetrics.mDragStartSequenceNumber;
+ mInputQueue->ConfirmDragBlock(inputBlockId, apzc, aDragMetrics);
+}
+
+bool APZCTreeManager::StartAutoscroll(const ScrollableLayerGuid& aGuid,
+ const ScreenPoint& aAnchorLocation) {
+ APZThreadUtils::AssertOnControllerThread();
+
+ RefPtr<AsyncPanZoomController> apzc = GetTargetAPZC(aGuid);
+ if (!apzc) {
+ if (XRE_IsGPUProcess()) {
+ // If we're in the compositor process, the "return false" will be
+ // ignored because the query comes over the PAPZCTreeManager protocol
+ // via an async message. In this case, send an explicit rejection
+ // message to content.
+ NotifyAutoscrollRejected(aGuid);
+ }
+ return false;
+ }
+
+ apzc->StartAutoscroll(aAnchorLocation);
+ return true;
+}
+
+void APZCTreeManager::StopAutoscroll(const ScrollableLayerGuid& aGuid) {
+ APZThreadUtils::AssertOnControllerThread();
+
+ if (RefPtr<AsyncPanZoomController> apzc = GetTargetAPZC(aGuid)) {
+ apzc->StopAutoscroll();
+ }
+}
+
+void APZCTreeManager::NotifyScrollbarDragInitiated(
+ uint64_t aDragBlockId, const ScrollableLayerGuid& aGuid,
+ ScrollDirection aDirection) const {
+ RefPtr<GeckoContentController> controller =
+ GetContentController(aGuid.mLayersId);
+ if (controller) {
+ controller->NotifyAsyncScrollbarDragInitiated(aDragBlockId, aGuid.mScrollId,
+ aDirection);
+ }
+}
+
+void APZCTreeManager::NotifyScrollbarDragRejected(
+ const ScrollableLayerGuid& aGuid) const {
+ RefPtr<GeckoContentController> controller =
+ GetContentController(aGuid.mLayersId);
+ if (controller) {
+ controller->NotifyAsyncScrollbarDragRejected(aGuid.mScrollId);
+ }
+}
+
+void APZCTreeManager::NotifyAutoscrollRejected(
+ const ScrollableLayerGuid& aGuid) const {
+ RefPtr<GeckoContentController> controller =
+ GetContentController(aGuid.mLayersId);
+ MOZ_ASSERT(controller);
+ controller->NotifyAsyncAutoscrollRejected(aGuid.mScrollId);
+}
+
+void SetHitTestData(HitTestingTreeNode* aNode,
+ const WebRenderScrollDataWrapper& aLayer,
+ const EventRegionsOverride& aOverrideFlags) {
+ aNode->SetHitTestData(aLayer.GetVisibleRegion(),
+ aLayer.GetRemoteDocumentSize(),
+ aLayer.GetTransformTyped(), aOverrideFlags,
+ aLayer.GetAsyncZoomContainerId());
+}
+
+HitTestingTreeNode* APZCTreeManager::PrepareNodeForLayer(
+ const RecursiveMutexAutoLock& aProofOfTreeLock, const ScrollNode& aLayer,
+ const FrameMetrics& aMetrics, LayersId aLayersId,
+ const Maybe<ZoomConstraints>& aZoomConstraints,
+ const AncestorTransform& aAncestorTransform, HitTestingTreeNode* aParent,
+ HitTestingTreeNode* aNextSibling, TreeBuildingState& aState) {
+ mTreeLock.AssertCurrentThreadIn(); // for static analysis
+ bool needsApzc = true;
+ if (!aMetrics.IsScrollable()) {
+ needsApzc = false;
+ }
+
+ // XXX: As a future optimization we can probably stick these things on the
+ // TreeBuildingState, and update them as we change layers id during the
+ // traversal
+ RefPtr<GeckoContentController> geckoContentController;
+ CompositorBridgeParent::CallWithIndirectShadowTree(
+ aLayersId, [&](LayerTreeState& lts) -> void {
+ geckoContentController = lts.mController;
+ });
+
+ if (!geckoContentController) {
+ needsApzc = false;
+ }
+
+ if (Maybe<uint64_t> zoomAnimationId = aLayer.GetZoomAnimationId()) {
+ aState.mZoomAnimationId = zoomAnimationId;
+ }
+
+ RefPtr<HitTestingTreeNode> node = nullptr;
+ if (!needsApzc) {
+ // Note: if layer properties must be propagated to nodes, RecvUpdate in
+ // LayerTransactionParent.cpp must ensure that APZ will be notified
+ // when those properties change.
+ node = RecycleOrCreateNode(aProofOfTreeLock, aState, nullptr, aLayersId);
+ AttachNodeToTree(node, aParent, aNextSibling);
+ SetHitTestData(node, aLayer, aState.mOverrideFlags.top());
+ node->SetScrollbarData(aLayer.GetScrollbarAnimationId(),
+ aLayer.GetScrollbarData());
+ node->SetFixedPosData(aLayer.GetFixedPositionScrollContainerId(),
+ aLayer.GetFixedPositionSides(),
+ aLayer.GetFixedPositionAnimationId());
+ node->SetStickyPosData(aLayer.GetStickyScrollContainerId(),
+ aLayer.GetStickyScrollRangeOuter(),
+ aLayer.GetStickyScrollRangeInner(),
+ aLayer.GetStickyPositionAnimationId());
+ PrintLayerInfo(aLayer);
+ return node;
+ }
+
+ AsyncPanZoomController* apzc = nullptr;
+ // If we get here, aLayer is a scrollable layer and somebody
+ // has registered a GeckoContentController for it, so we need to ensure
+ // it has an APZC instance to manage its scrolling.
+
+ // aState.mApzcMap allows reusing the exact same APZC instance for different
+ // layers with the same FrameMetrics data. This is needed because in some
+ // cases content that is supposed to scroll together is split into multiple
+ // layers because of e.g. non-scrolling content interleaved in z-index order.
+ ScrollableLayerGuid guid(aLayersId, aMetrics.GetPresShellId(),
+ aMetrics.GetScrollId());
+ auto insertResult = aState.mApzcMap.insert(std::make_pair(
+ guid,
+ ApzcMapData{static_cast<AsyncPanZoomController*>(nullptr), Nothing()}));
+ if (!insertResult.second) {
+ apzc = insertResult.first->second.apzc;
+ PrintLayerInfo(aLayer);
+ }
+ APZCTM_LOG("Found APZC %p for layer %p with identifiers %" PRIx64 " %" PRId64
+ "\n",
+ apzc, aLayer.GetLayer(), uint64_t(guid.mLayersId), guid.mScrollId);
+
+ // If we haven't encountered a layer already with the same metrics, then we
+ // need to do the full reuse-or-make-an-APZC algorithm, which is contained
+ // inside the block below.
+ if (apzc == nullptr) {
+ apzc = aLayer.GetApzc();
+
+ // If the content represented by the scrollable layer has changed (which may
+ // be possible because of DLBI heuristics) then we don't want to keep using
+ // the same old APZC for the new content. Also, when reparenting a tab into
+ // a new window a layer might get moved to a different layer tree with a
+ // different APZCTreeManager. In these cases we don't want to reuse the same
+ // APZC, so null it out so we run through the code to find another one or
+ // create one.
+ if (apzc && (!apzc->Matches(guid) || !apzc->HasTreeManager(this))) {
+ apzc = nullptr;
+ }
+
+ // See if we can find an APZC from the previous tree that matches the
+ // ScrollableLayerGuid from this layer. If there is one, then we know that
+ // the layout of the page changed causing the layer tree to be rebuilt, but
+ // the underlying content for the APZC is still there somewhere. Therefore,
+ // we want to find the APZC instance and continue using it here.
+ //
+ // We particularly want to find the primary-holder node from the previous
+ // tree that matches, because we don't want that node to get destroyed. If
+ // it does get destroyed, then the APZC will get destroyed along with it by
+ // definition, but we want to keep that APZC around in the new tree.
+ // We leave non-primary-holder nodes in the destroy list because we don't
+ // care about those nodes getting destroyed.
+ for (size_t i = 0; i < aState.mNodesToDestroy.Length(); i++) {
+ RefPtr<HitTestingTreeNode> n = aState.mNodesToDestroy[i];
+ if (n->IsPrimaryHolder() && n->GetApzc() && n->GetApzc()->Matches(guid)) {
+ node = n;
+ if (apzc != nullptr) {
+ // If there is an APZC already then it should match the one from the
+ // old primary-holder node
+ MOZ_ASSERT(apzc == node->GetApzc());
+ }
+ apzc = node->GetApzc();
+ break;
+ }
+ }
+
+ // The APZC we get off the layer may have been destroyed previously if the
+ // layer was inactive or omitted from the layer tree for whatever reason
+ // from a layers update. If it later comes back it will have a reference to
+ // a destroyed APZC and so we need to throw that out and make a new one.
+ bool newApzc = (apzc == nullptr || apzc->IsDestroyed());
+ if (newApzc) {
+ apzc = NewAPZCInstance(aLayersId, geckoContentController);
+ apzc->SetCompositorController(aState.mCompositorController.get());
+ MOZ_ASSERT(node == nullptr);
+ node = new HitTestingTreeNode(apzc, true, aLayersId);
+ } else {
+ // If we are re-using a node for this layer clear the tree pointers
+ // so that it doesn't continue pointing to nodes that might no longer
+ // be in the tree. These pointers will get reset properly as we continue
+ // building the tree. Also remove it from the set of nodes that are going
+ // to be destroyed, because it's going to remain active.
+ aState.mNodesToDestroy.RemoveElement(node);
+ node->SetPrevSibling(nullptr);
+ node->SetLastChild(nullptr);
+ }
+
+ if (aMetrics.IsRootContent()) {
+ apzc->SetZoomAnimationId(aState.mZoomAnimationId);
+ aState.mZoomAnimationId = Nothing();
+ }
+
+ APZCTM_LOG(
+ "Using APZC %p for layer %p with identifiers %" PRIx64 " %" PRId64 "\n",
+ apzc, aLayer.GetLayer(), uint64_t(aLayersId), aMetrics.GetScrollId());
+
+ apzc->NotifyLayersUpdated(aLayer.Metadata(), aState.mIsFirstPaint,
+ aLayersId == aState.mOriginatingLayersId);
+
+ // Since this is the first time we are encountering an APZC with this guid,
+ // the node holding it must be the primary holder. It may be newly-created
+ // or not, depending on whether it went through the newApzc branch above.
+ MOZ_ASSERT(node->IsPrimaryHolder() && node->GetApzc() &&
+ node->GetApzc()->Matches(guid));
+
+ SetHitTestData(node, aLayer, aState.mOverrideFlags.top());
+ apzc->SetAncestorTransform(aAncestorTransform);
+
+ PrintLayerInfo(aLayer);
+
+ // Bind the APZC instance into the tree of APZCs
+ AttachNodeToTree(node, aParent, aNextSibling);
+
+ // For testing, log the parent scroll id of every APZC that has a
+ // parent. This allows test code to reconstruct the APZC tree.
+ // Note that we currently only do this for APZCs in the layer tree
+ // that originated the update, because the only identifying information
+ // we are logging about APZCs is the scroll id, and otherwise we could
+ // confuse APZCs from different layer trees with the same scroll id.
+ if (aLayersId == aState.mOriginatingLayersId) {
+ if (apzc->HasNoParentWithSameLayersId()) {
+ aState.mPaintLogger.LogTestData(aMetrics.GetScrollId(),
+ "hasNoParentWithSameLayersId", true);
+ } else {
+ MOZ_ASSERT(apzc->GetParent());
+ aState.mPaintLogger.LogTestData(aMetrics.GetScrollId(),
+ "parentScrollId",
+ apzc->GetParent()->GetGuid().mScrollId);
+ }
+ if (aMetrics.IsRootContent()) {
+ aState.mPaintLogger.LogTestData(aMetrics.GetScrollId(), "isRootContent",
+ true);
+ }
+ // Note that the async scroll offset is in ParentLayer pixels
+ aState.mPaintLogger.LogTestData(
+ aMetrics.GetScrollId(), "asyncScrollOffset",
+ apzc->GetCurrentAsyncScrollOffset(
+ AsyncPanZoomController::eForHitTesting));
+ aState.mPaintLogger.LogTestData(aMetrics.GetScrollId(),
+ "hasAsyncKeyScrolled",
+ apzc->TestHasAsyncKeyScrolled());
+ }
+
+ // We must update the zoom constraints even if the apzc isn't new because it
+ // might have moved.
+ if (node->IsPrimaryHolder()) {
+ if (aZoomConstraints) {
+ apzc->UpdateZoomConstraints(*aZoomConstraints);
+
+#ifdef DEBUG
+ auto it = mZoomConstraints.find(guid);
+ if (it != mZoomConstraints.end()) {
+ MOZ_ASSERT(it->second == *aZoomConstraints);
+ }
+ } else {
+ // We'd like to assert these things (if the first doesn't hold then at
+ // least the second) but neither are not true because xul root content
+ // gets zoomable zoom constraints, but which is not zoomable because it
+ // doesn't have a root scroll frame.
+ // clang-format off
+ // MOZ_ASSERT(mZoomConstraints.find(guid) == mZoomConstraints.end());
+ // auto it = mZoomConstraints.find(guid);
+ // if (it != mZoomConstraints.end()) {
+ // MOZ_ASSERT(!it->second.mAllowZoom && !it->second.mAllowDoubleTapZoom);
+ // }
+ // clang-format on
+#endif
+ }
+ }
+
+ // Add a guid -> APZC mapping for the newly created APZC.
+ insertResult.first->second.apzc = apzc;
+ } else {
+ // We already built an APZC earlier in this tree walk, but we have another
+ // layer now that will also be using that APZC. The hit-test region on the
+ // APZC needs to be updated to deal with the new layer's hit region.
+
+ node = RecycleOrCreateNode(aProofOfTreeLock, aState, apzc, aLayersId);
+ AttachNodeToTree(node, aParent, aNextSibling);
+
+ // Even though different layers associated with a given APZC may be at
+ // different levels in the layer tree (e.g. one being an uncle of another),
+ // we require from Layout that the CSS transforms up to their common
+ // ancestor be roughly the same. There are cases in which the transforms
+ // are not exactly the same, for example if the parent is container layer
+ // for an opacity, and this container layer has a resolution-induced scale
+ // as its base transform and a prescale that is supposed to undo that scale.
+ // Due to floating point inaccuracies those transforms can end up not quite
+ // canceling each other. That's why we're using a fuzzy comparison here
+ // instead of an exact one.
+ // In addition, two ancestor transforms are allowed to differ if one of
+ // them contains a perspective transform component and the other does not.
+ // This represents situations where some content in a scrollable frame
+ // is subject to a perspective transform and other content does not.
+ // In such cases, go with the one that does not include the perspective
+ // component; the perspective transform is remembered and applied to the
+ // children instead.
+ auto ancestorTransform = aAncestorTransform.CombinedTransform();
+ auto existingAncestorTransform = apzc->GetAncestorTransform();
+ if (!ancestorTransform.FuzzyEqualsMultiplicative(
+ existingAncestorTransform)) {
+ typedef TreeBuildingState::DeferredTransformMap::value_type PairType;
+ if (!aAncestorTransform.ContainsPerspectiveTransform() &&
+ !apzc->AncestorTransformContainsPerspective()) {
+ // If this content is being presented in a paginated fashion (e.g.
+ // print preview), the multiple layers may reflect multiple instances
+ // of the same display item being rendered on different pages. In such
+ // cases, it's expected that different instances can have different
+ // transforms, since each page renders a different part of the item.
+ if (!aLayer.Metadata().IsPaginatedPresentation()) {
+ if (ancestorTransform.IsFinite() &&
+ existingAncestorTransform.IsFinite()) {
+ MOZ_ASSERT(
+ false,
+ "Two layers that scroll together have different ancestor "
+ "transforms");
+ } else {
+ MOZ_ASSERT(ancestorTransform.IsFinite() ==
+ existingAncestorTransform.IsFinite());
+ }
+ }
+ } else if (!aAncestorTransform.ContainsPerspectiveTransform()) {
+ aState.mPerspectiveTransformsDeferredToChildren.insert(
+ PairType{apzc, apzc->GetAncestorTransformPerspective()});
+ apzc->SetAncestorTransform(aAncestorTransform);
+ } else {
+ aState.mPerspectiveTransformsDeferredToChildren.insert(
+ PairType{apzc, aAncestorTransform.GetPerspectiveTransform()});
+ }
+ }
+
+ SetHitTestData(node, aLayer, aState.mOverrideFlags.top());
+ }
+
+ // Note: if layer properties must be propagated to nodes, RecvUpdate in
+ // LayerTransactionParent.cpp must ensure that APZ will be notified
+ // when those properties change.
+ node->SetScrollbarData(aLayer.GetScrollbarAnimationId(),
+ aLayer.GetScrollbarData());
+ node->SetFixedPosData(aLayer.GetFixedPositionScrollContainerId(),
+ aLayer.GetFixedPositionSides(),
+ aLayer.GetFixedPositionAnimationId());
+ node->SetStickyPosData(aLayer.GetStickyScrollContainerId(),
+ aLayer.GetStickyScrollRangeOuter(),
+ aLayer.GetStickyScrollRangeInner(),
+ aLayer.GetStickyPositionAnimationId());
+ return node;
+}
+
+template <typename PanGestureOrScrollWheelInput>
+static bool WillHandleInput(const PanGestureOrScrollWheelInput& aPanInput) {
+ if (!XRE_IsParentProcess() || !NS_IsMainThread()) {
+ return true;
+ }
+
+ WidgetWheelEvent wheelEvent = aPanInput.ToWidgetEvent(nullptr);
+ return APZInputBridge::ActionForWheelEvent(&wheelEvent).isSome();
+}
+
+/*static*/
+void APZCTreeManager::FlushApzRepaints(LayersId aLayersId) {
+ // Previously, paints were throttled and therefore this method was used to
+ // ensure any pending paints were flushed. Now, paints are flushed
+ // immediately, so it is safe to simply send a notification now.
+ APZCTM_LOG("Flushing repaints for layers id 0x%" PRIx64 "\n",
+ uint64_t(aLayersId));
+ RefPtr<GeckoContentController> controller = GetContentController(aLayersId);
+#ifndef MOZ_WIDGET_ANDROID
+ // On Android, this code is run in production and may actually get a nullptr
+ // controller here. On other platforms this code is test-only and should never
+ // get a nullptr.
+ MOZ_ASSERT(controller);
+#endif
+ if (controller) {
+ controller->DispatchToRepaintThread(NewRunnableMethod(
+ "layers::GeckoContentController::NotifyFlushComplete", controller,
+ &GeckoContentController::NotifyFlushComplete));
+ }
+}
+
+void APZCTreeManager::MarkAsDetached(LayersId aLayersId) {
+ RecursiveMutexAutoLock lock(mTreeLock);
+ mDetachedLayersIds.insert(aLayersId);
+}
+
+static bool HasNonLockModifier(Modifiers aModifiers) {
+ return (aModifiers & (MODIFIER_ALT | MODIFIER_ALTGRAPH | MODIFIER_CONTROL |
+ MODIFIER_FN | MODIFIER_META | MODIFIER_SHIFT |
+ MODIFIER_SYMBOL | MODIFIER_OS)) != 0;
+}
+
+APZEventResult APZCTreeManager::ReceiveInputEvent(
+ InputData& aEvent, InputBlockCallback&& aCallback) {
+ APZThreadUtils::AssertOnControllerThread();
+ InputHandlingState state{aEvent};
+
+ // Use a RAII class for updating the focus sequence number of this event
+ AutoFocusSequenceNumberSetter focusSetter(mFocusState, aEvent);
+
+ switch (aEvent.mInputType) {
+ case MULTITOUCH_INPUT: {
+ MultiTouchInput& touchInput = aEvent.AsMultiTouchInput();
+ ProcessTouchInput(state, touchInput);
+ break;
+ }
+ case MOUSE_INPUT: {
+ MouseInput& mouseInput = aEvent.AsMouseInput();
+ mouseInput.mHandledByAPZ = true;
+
+ SetCurrentMousePosition(mouseInput.mOrigin);
+
+ bool startsDrag = DragTracker::StartsDrag(mouseInput);
+ if (startsDrag) {
+ // If this is the start of a drag we need to unambiguously know if it's
+ // going to land on a scrollbar or not. We can't apply an untransform
+ // here without knowing that, so we need to ensure the untransform is
+ // a no-op.
+ FlushRepaintsToClearScreenToGeckoTransform();
+ }
+
+ state.mHit = GetTargetAPZC(mouseInput.mOrigin);
+ bool hitScrollbar = (bool)state.mHit.mScrollbarNode;
+
+ // When the mouse is outside the window we still want to handle dragging
+ // but we won't find an APZC. Fallback to root APZC then.
+ { // scope lock
+ RecursiveMutexAutoLock lock(mTreeLock);
+ if (!state.mHit.mTargetApzc && mRootNode) {
+ state.mHit.mTargetApzc = mRootNode->GetApzc();
+ }
+ }
+
+ if (state.mHit.mTargetApzc) {
+ if (StaticPrefs::apz_test_logging_enabled() &&
+ mouseInput.mType == MouseInput::MOUSE_HITTEST) {
+ ScrollableLayerGuid guid = state.mHit.mTargetApzc->GetGuid();
+
+ MutexAutoLock lock(mTestDataLock);
+ auto it = mTestData.find(guid.mLayersId);
+ MOZ_ASSERT(it != mTestData.end());
+ it->second->RecordHitResult(mouseInput.mOrigin, state.mHit.mHitResult,
+ guid.mLayersId, guid.mScrollId);
+ }
+
+ TargetConfirmationFlags confFlags{state.mHit.mHitResult};
+ state.mResult = mInputQueue->ReceiveInputEvent(state.mHit.mTargetApzc,
+ confFlags, mouseInput);
+
+ // If we're starting an async scrollbar drag
+ bool apzDragEnabled = StaticPrefs::apz_drag_enabled();
+ if (apzDragEnabled && startsDrag && state.mHit.mScrollbarNode &&
+ state.mHit.mScrollbarNode->IsScrollThumbNode() &&
+ state.mHit.mScrollbarNode->GetScrollbarData()
+ .mThumbIsAsyncDraggable) {
+ SetupScrollbarDrag(mouseInput, state.mHit.mScrollbarNode,
+ state.mHit.mTargetApzc.get());
+ }
+
+ if (state.mResult.GetStatus() == nsEventStatus_eConsumeDoDefault) {
+ // This input event is part of a drag block, so whether or not it is
+ // directed at a scrollbar depends on whether the drag block started
+ // on a scrollbar.
+ hitScrollbar = mInputQueue->IsDragOnScrollbar(hitScrollbar);
+ }
+
+ if (!hitScrollbar) {
+ // The input was not targeted at a scrollbar, so we untransform it
+ // like we do for other content. Scrollbars are "special" because they
+ // have special handling in AsyncCompositionManager when resolution is
+ // applied. TODO: we should find a better way to deal with this.
+ ScreenToParentLayerMatrix4x4 transformToApzc =
+ GetScreenToApzcTransform(state.mHit.mTargetApzc);
+ ParentLayerToScreenMatrix4x4 transformToGecko =
+ GetApzcToGeckoTransformForHit(state.mHit);
+ ScreenToScreenMatrix4x4 outTransform =
+ transformToApzc * transformToGecko;
+ Maybe<ScreenPoint> untransformedRefPoint =
+ UntransformBy(outTransform, mouseInput.mOrigin);
+ if (untransformedRefPoint) {
+ mouseInput.mOrigin = *untransformedRefPoint;
+ }
+ } else {
+ // Likewise, if the input was targeted at a scrollbar, we don't want
+ // to apply the callback transform in the main thread, so we remove
+ // the scrollid from the guid. We need to keep the layersId intact so
+ // that the response from the child process doesn't get discarded.
+ state.mResult.mTargetGuid.mScrollId =
+ ScrollableLayerGuid::NULL_SCROLL_ID;
+ }
+ }
+ break;
+ }
+ case SCROLLWHEEL_INPUT: {
+ FlushRepaintsToClearScreenToGeckoTransform();
+
+ // Do this before early return for Fission hit testing.
+ ScrollWheelInput& wheelInput = aEvent.AsScrollWheelInput();
+ state.mHit = GetTargetAPZC(wheelInput.mOrigin);
+
+ wheelInput.mHandledByAPZ = WillHandleInput(wheelInput);
+ if (!wheelInput.mHandledByAPZ) {
+ return state.Finish(*this, std::move(aCallback));
+ }
+
+ if (state.mHit.mTargetApzc) {
+ MOZ_ASSERT(state.mHit.mHitResult != CompositorHitTestInvisibleToHit);
+
+ if (wheelInput.mAPZAction == APZWheelAction::PinchZoom) {
+ // The mousewheel may have hit a subframe, but we want to send the
+ // pinch-zoom events to the root-content APZC.
+ {
+ RecursiveMutexAutoLock lock(mTreeLock);
+ state.mHit.mTargetApzc = FindRootContentApzcForLayersId(
+ state.mHit.mTargetApzc->GetLayersId());
+ }
+ if (state.mHit.mTargetApzc) {
+ SynthesizePinchGestureFromMouseWheel(wheelInput,
+ state.mHit.mTargetApzc);
+ }
+ state.mResult.SetStatusAsConsumeNoDefault();
+ return state.Finish(*this, std::move(aCallback));
+ }
+
+ MOZ_ASSERT(wheelInput.mAPZAction == APZWheelAction::Scroll);
+
+ // For wheel events, the call to ReceiveInputEvent below may result in
+ // scrolling, which changes the async transform. However, the event we
+ // want to pass to gecko should be the pre-scroll event coordinates,
+ // transformed into the gecko space. (pre-scroll because the mouse
+ // cursor is stationary during wheel scrolling, unlike touchmove
+ // events). Since we just flushed the pending repaints the transform to
+ // gecko space should only consist of overscroll-cancelling transforms.
+ ScreenToScreenMatrix4x4 transformToGecko =
+ GetScreenToApzcTransform(state.mHit.mTargetApzc) *
+ GetApzcToGeckoTransformForHit(state.mHit);
+ Maybe<ScreenPoint> untransformedOrigin =
+ UntransformBy(transformToGecko, wheelInput.mOrigin);
+
+ if (!untransformedOrigin) {
+ return state.Finish(*this, std::move(aCallback));
+ }
+
+ state.mResult = mInputQueue->ReceiveInputEvent(
+ state.mHit.mTargetApzc,
+ TargetConfirmationFlags{state.mHit.mHitResult}, wheelInput);
+
+ // Update the out-parameters so they are what the caller expects.
+ wheelInput.mOrigin = *untransformedOrigin;
+ }
+ break;
+ }
+ case PANGESTURE_INPUT: {
+ FlushRepaintsToClearScreenToGeckoTransform();
+
+ // Do this before early return for Fission hit testing.
+ PanGestureInput& panInput = aEvent.AsPanGestureInput();
+ state.mHit = GetTargetAPZC(panInput.mPanStartPoint);
+
+ panInput.mHandledByAPZ = WillHandleInput(panInput);
+ if (!panInput.mHandledByAPZ) {
+ if (mInputQueue->GetCurrentPanGestureBlock()) {
+ if (state.mHit.mTargetApzc &&
+ (panInput.mType == PanGestureInput::PANGESTURE_END ||
+ panInput.mType == PanGestureInput::PANGESTURE_CANCELLED)) {
+ // If we've already been processing a pan gesture in an APZC but
+ // fall into this _if_ branch, which means this pan-end or
+ // pan-cancelled event will not be proccessed in the APZC, send a
+ // pan-interrupted event to stop any on-going work for the pan
+ // gesture, otherwise we will get stuck at an intermidiate state
+ // becasue we might no longer receive any events which will be
+ // handled by the APZC.
+ PanGestureInput panInterrupted(
+ PanGestureInput::PANGESTURE_INTERRUPTED, panInput.mTimeStamp,
+ panInput.mPanStartPoint, panInput.mPanDisplacement,
+ panInput.modifiers);
+ Unused << mInputQueue->ReceiveInputEvent(
+ state.mHit.mTargetApzc,
+ TargetConfirmationFlags{state.mHit.mHitResult}, panInterrupted);
+ }
+ }
+ return state.Finish(*this, std::move(aCallback));
+ }
+
+ // If/when we enable support for pan inputs off-main-thread, we'll need
+ // to duplicate this EventStateManager code or something. See the call to
+ // GetUserPrefsForWheelEvent in APZInputBridge.cpp for why these fields
+ // are stored separately.
+ MOZ_ASSERT(NS_IsMainThread());
+ WidgetWheelEvent wheelEvent = panInput.ToWidgetEvent(nullptr);
+ EventStateManager::GetUserPrefsForWheelEvent(
+ &wheelEvent, &panInput.mUserDeltaMultiplierX,
+ &panInput.mUserDeltaMultiplierY);
+
+ if (state.mHit.mTargetApzc) {
+ MOZ_ASSERT(state.mHit.mHitResult != CompositorHitTestInvisibleToHit);
+
+ // For pan gesture events, the call to ReceiveInputEvent below may
+ // result in scrolling, which changes the async transform. However, the
+ // event we want to pass to gecko should be the pre-scroll event
+ // coordinates, transformed into the gecko space. (pre-scroll because
+ // the mouse cursor is stationary during pan gesture scrolling, unlike
+ // touchmove events). Since we just flushed the pending repaints the
+ // transform to gecko space should only consist of overscroll-cancelling
+ // transforms.
+ ScreenToScreenMatrix4x4 transformToGecko =
+ GetScreenToApzcTransform(state.mHit.mTargetApzc) *
+ GetApzcToGeckoTransformForHit(state.mHit);
+ Maybe<ScreenPoint> untransformedStartPoint =
+ UntransformBy(transformToGecko, panInput.mPanStartPoint);
+ Maybe<ScreenPoint> untransformedDisplacement =
+ UntransformVector(transformToGecko, panInput.mPanDisplacement,
+ panInput.mPanStartPoint);
+
+ if (!untransformedStartPoint || !untransformedDisplacement) {
+ return state.Finish(*this, std::move(aCallback));
+ }
+
+ panInput.mOverscrollBehaviorAllowsSwipe =
+ state.mHit.mTargetApzc->OverscrollBehaviorAllowsSwipe();
+
+ state.mResult = mInputQueue->ReceiveInputEvent(
+ state.mHit.mTargetApzc,
+ TargetConfirmationFlags{state.mHit.mHitResult}, panInput);
+
+ // Update the out-parameters so they are what the caller expects.
+ panInput.mPanStartPoint = *untransformedStartPoint;
+ panInput.mPanDisplacement = *untransformedDisplacement;
+ }
+ break;
+ }
+ case PINCHGESTURE_INPUT: {
+ PinchGestureInput& pinchInput = aEvent.AsPinchGestureInput();
+ if (HasNonLockModifier(pinchInput.modifiers)) {
+ APZCTM_LOG("Discarding pinch input due to modifiers 0x%x\n",
+ pinchInput.modifiers);
+ return state.Finish(*this, std::move(aCallback));
+ }
+
+ state.mHit = GetTargetAPZC(pinchInput.mFocusPoint);
+
+ // We always handle pinch gestures as pinch zooms.
+ pinchInput.mHandledByAPZ = true;
+
+ if (state.mHit.mTargetApzc) {
+ MOZ_ASSERT(state.mHit.mHitResult != CompositorHitTestInvisibleToHit);
+
+ if (!state.mHit.mTargetApzc->IsRootContent()) {
+ state.mHit.mTargetApzc = FindZoomableApzc(state.mHit.mTargetApzc);
+ }
+ }
+
+ if (state.mHit.mTargetApzc) {
+ ScreenToScreenMatrix4x4 outTransform =
+ GetScreenToApzcTransform(state.mHit.mTargetApzc) *
+ GetApzcToGeckoTransformForHit(state.mHit);
+ Maybe<ScreenPoint> untransformedFocusPoint =
+ UntransformBy(outTransform, pinchInput.mFocusPoint);
+
+ if (!untransformedFocusPoint) {
+ return state.Finish(*this, std::move(aCallback));
+ }
+
+ state.mResult = mInputQueue->ReceiveInputEvent(
+ state.mHit.mTargetApzc,
+ TargetConfirmationFlags{state.mHit.mHitResult}, pinchInput);
+
+ // Update the out-parameters so they are what the caller expects.
+ pinchInput.mFocusPoint = *untransformedFocusPoint;
+ }
+ break;
+ }
+ case TAPGESTURE_INPUT: { // note: no one currently sends these
+ TapGestureInput& tapInput = aEvent.AsTapGestureInput();
+ state.mHit = GetTargetAPZC(tapInput.mPoint);
+
+ if (state.mHit.mTargetApzc) {
+ MOZ_ASSERT(state.mHit.mHitResult != CompositorHitTestInvisibleToHit);
+
+ ScreenToScreenMatrix4x4 outTransform =
+ GetScreenToApzcTransform(state.mHit.mTargetApzc) *
+ GetApzcToGeckoTransformForHit(state.mHit);
+ Maybe<ScreenIntPoint> untransformedPoint =
+ UntransformBy(outTransform, tapInput.mPoint);
+
+ if (!untransformedPoint) {
+ return state.Finish(*this, std::move(aCallback));
+ }
+
+ // Tap gesture events are not grouped into input blocks, and they're
+ // never queued in InputQueue, but processed right away. So, we only
+ // need to set |mTapGestureHitResult| for the duration of the
+ // InputQueue::ReceiveInputEvent() call.
+ {
+ RecursiveMutexAutoLock lock(mTreeLock);
+ mTapGestureHitResult =
+ mHitTester->CloneHitTestResult(lock, state.mHit);
+ }
+
+ state.mResult = mInputQueue->ReceiveInputEvent(
+ state.mHit.mTargetApzc,
+ TargetConfirmationFlags{state.mHit.mHitResult}, tapInput);
+
+ mTapGestureHitResult = HitTestResult();
+
+ // Update the out-parameters so they are what the caller expects.
+ tapInput.mPoint = *untransformedPoint;
+ }
+ break;
+ }
+ case KEYBOARD_INPUT: {
+ // Disable async keyboard scrolling when accessibility.browsewithcaret is
+ // enabled
+ if (!StaticPrefs::apz_keyboard_enabled_AtStartup() ||
+ StaticPrefs::accessibility_browsewithcaret()) {
+ APZ_KEY_LOG("Skipping key input from invalid prefs\n");
+ return state.Finish(*this, std::move(aCallback));
+ }
+
+ KeyboardInput& keyInput = aEvent.AsKeyboardInput();
+
+ // Try and find a matching shortcut for this keyboard input
+ Maybe<KeyboardShortcut> shortcut = mKeyboardMap.FindMatch(keyInput);
+
+ if (!shortcut) {
+ APZ_KEY_LOG("Skipping key input with no shortcut\n");
+
+ // If we don't have a shortcut for this key event, then we can keep our
+ // focus only if we know there are no key event listeners for this
+ // target
+ if (mFocusState.CanIgnoreKeyboardShortcutMisses()) {
+ focusSetter.MarkAsNonFocusChanging();
+ }
+ return state.Finish(*this, std::move(aCallback));
+ }
+
+ // Check if this shortcut needs to be dispatched to content. Anything
+ // matching this is assumed to be able to change focus.
+ if (shortcut->mDispatchToContent) {
+ APZ_KEY_LOG("Skipping key input with dispatch-to-content shortcut\n");
+ return state.Finish(*this, std::move(aCallback));
+ }
+
+ // We know we have an action to execute on whatever is the current focus
+ // target
+ const KeyboardScrollAction& action = shortcut->mAction;
+
+ // The current focus target depends on which direction the scroll is to
+ // happen
+ Maybe<ScrollableLayerGuid> targetGuid;
+ switch (action.mType) {
+ case KeyboardScrollAction::eScrollCharacter: {
+ targetGuid = mFocusState.GetHorizontalTarget();
+ break;
+ }
+ case KeyboardScrollAction::eScrollLine:
+ case KeyboardScrollAction::eScrollPage:
+ case KeyboardScrollAction::eScrollComplete: {
+ targetGuid = mFocusState.GetVerticalTarget();
+ break;
+ }
+ }
+
+ // If we don't have a scroll target then either we have a stale focus
+ // target, the focused element has event listeners, or the focused element
+ // doesn't have a layerized scroll frame. In any case we need to dispatch
+ // to content.
+ if (!targetGuid) {
+ APZ_KEY_LOG("Skipping key input with no current focus target\n");
+ return state.Finish(*this, std::move(aCallback));
+ }
+
+ RefPtr<AsyncPanZoomController> targetApzc =
+ GetTargetAPZC(targetGuid->mLayersId, targetGuid->mScrollId);
+
+ if (!targetApzc) {
+ APZ_KEY_LOG("Skipping key input with focus target but no APZC\n");
+ return state.Finish(*this, std::move(aCallback));
+ }
+
+ // Attach the keyboard scroll action to the input event for processing
+ // by the input queue.
+ keyInput.mAction = action;
+
+ APZ_KEY_LOG("Dispatching key input with apzc=%p\n", targetApzc.get());
+
+ // Dispatch the event to the input queue.
+ state.mResult = mInputQueue->ReceiveInputEvent(
+ targetApzc, TargetConfirmationFlags{true}, keyInput);
+
+ // Any keyboard event that is dispatched to the input queue at this point
+ // should have been consumed
+ MOZ_ASSERT(state.mResult.GetStatus() == nsEventStatus_eConsumeDoDefault ||
+ state.mResult.GetStatus() == nsEventStatus_eConsumeNoDefault);
+
+ keyInput.mHandledByAPZ = true;
+ focusSetter.MarkAsNonFocusChanging();
+
+ break;
+ }
+ }
+ return state.Finish(*this, std::move(aCallback));
+}
+
+static TouchBehaviorFlags ConvertToTouchBehavior(
+ const CompositorHitTestInfo& info) {
+ TouchBehaviorFlags result = AllowedTouchBehavior::UNKNOWN;
+ if (info == CompositorHitTestInvisibleToHit) {
+ result = AllowedTouchBehavior::NONE;
+ } else if (info.contains(CompositorHitTestFlags::eIrregularArea)) {
+ // Note that eApzAwareListeners and eInactiveScrollframe are similar
+ // to eIrregularArea in some respects, but are not relevant for the
+ // purposes of this function, which deals specifically with touch-action.
+ result = AllowedTouchBehavior::UNKNOWN;
+ } else {
+ result = AllowedTouchBehavior::VERTICAL_PAN |
+ AllowedTouchBehavior::HORIZONTAL_PAN |
+ AllowedTouchBehavior::PINCH_ZOOM |
+ AllowedTouchBehavior::ANIMATING_ZOOM;
+ if (info.contains(CompositorHitTestFlags::eTouchActionPanXDisabled)) {
+ result &= ~AllowedTouchBehavior::HORIZONTAL_PAN;
+ }
+ if (info.contains(CompositorHitTestFlags::eTouchActionPanYDisabled)) {
+ result &= ~AllowedTouchBehavior::VERTICAL_PAN;
+ }
+ if (info.contains(CompositorHitTestFlags::eTouchActionPinchZoomDisabled)) {
+ result &= ~AllowedTouchBehavior::PINCH_ZOOM;
+ }
+ if (info.contains(
+ CompositorHitTestFlags::eTouchActionAnimatingZoomDisabled)) {
+ result &= ~AllowedTouchBehavior::ANIMATING_ZOOM;
+ }
+ }
+ return result;
+}
+
+APZCTreeManager::HitTestResult APZCTreeManager::GetTouchInputBlockAPZC(
+ const MultiTouchInput& aEvent,
+ nsTArray<TouchBehaviorFlags>* aOutTouchBehaviors) {
+ HitTestResult hit;
+ if (aEvent.mTouches.Length() == 0) {
+ return hit;
+ }
+
+ FlushRepaintsToClearScreenToGeckoTransform();
+
+ hit = GetTargetAPZC(aEvent.mTouches[0].mScreenPoint);
+ // Don't set a layers id on multi-touch events.
+ if (aEvent.mTouches.Length() != 1) {
+ hit.mLayersId = LayersId{0};
+ }
+
+ if (aOutTouchBehaviors) {
+ aOutTouchBehaviors->AppendElement(ConvertToTouchBehavior(hit.mHitResult));
+ }
+ for (size_t i = 1; i < aEvent.mTouches.Length(); i++) {
+ HitTestResult hit2 = GetTargetAPZC(aEvent.mTouches[i].mScreenPoint);
+ if (aOutTouchBehaviors) {
+ aOutTouchBehaviors->AppendElement(
+ ConvertToTouchBehavior(hit2.mHitResult));
+ }
+ hit.mTargetApzc = GetZoomableTarget(hit.mTargetApzc, hit2.mTargetApzc);
+ APZCTM_LOG("Using APZC %p as the root APZC for multi-touch\n",
+ hit.mTargetApzc.get());
+ // A multi-touch gesture will not be a scrollbar drag, even if the
+ // first touch point happened to hit a scrollbar.
+ hit.mScrollbarNode.Clear();
+
+ // XXX we should probably be combining the hit results from the different
+ // touch points somehow, instead of just using the last one.
+ hit.mHitResult = hit2.mHitResult;
+ }
+
+ return hit;
+}
+
+APZEventResult APZCTreeManager::InputHandlingState::Finish(
+ APZCTreeManager& aTreeManager, InputBlockCallback&& aCallback) {
+ // The validity check here handles both the case where mHit was
+ // never populated (because this event did not trigger a hit-test),
+ // and the case where it was populated with an invalid LayersId
+ // (which can happen e.g. for multi-touch events).
+ if (mHit.mLayersId.IsValid()) {
+ mEvent.mLayersId = mHit.mLayersId;
+ }
+
+ // Absorb events that are in targetted at a position in the gutter,
+ // unless they are fixed position elements.
+ if (mHit.mHitOverscrollGutter && mHit.mFixedPosSides == SideBits::eNone) {
+ mResult.SetStatusAsConsumeNoDefault();
+ }
+
+ // If the event will have a delayed result then add the callback to the
+ // APZCTreeManager.
+ if (aCallback && mResult.WillHaveDelayedResult()) {
+ aTreeManager.AddInputBlockCallback(
+ mResult.mInputBlockId, {mResult.GetStatus(), std::move(aCallback)});
+ }
+
+ return mResult;
+}
+
+void APZCTreeManager::ProcessTouchInput(InputHandlingState& aState,
+ MultiTouchInput& aInput) {
+ aInput.mHandledByAPZ = true;
+ nsTArray<TouchBehaviorFlags> touchBehaviors;
+ HitTestingTreeNodeAutoLock hitScrollbarNode;
+ if (aInput.mType == MultiTouchInput::MULTITOUCH_START) {
+ // If we are panned into overscroll and a second finger goes down,
+ // ignore that second touch point completely. The touch-start for it is
+ // dropped completely; subsequent touch events until the touch-end for it
+ // will have this touch point filtered out.
+ // (By contrast, if we're in overscroll but not panning, such as after
+ // putting two fingers down during an overscroll animation, we process the
+ // second touch and proceed to pinch.)
+ if (mTouchBlockHitResult.mTargetApzc &&
+ mTouchBlockHitResult.mTargetApzc->IsInPanningState() &&
+ BuildOverscrollHandoffChain(mTouchBlockHitResult.mTargetApzc)
+ ->HasOverscrolledApzc()) {
+ if (mRetainedTouchIdentifier == -1) {
+ mRetainedTouchIdentifier =
+ mTouchBlockHitResult.mTargetApzc->GetLastTouchIdentifier();
+ }
+
+ aState.mResult.SetStatusAsConsumeNoDefault();
+ return;
+ }
+
+ aState.mHit = GetTouchInputBlockAPZC(aInput, &touchBehaviors);
+ RecursiveMutexAutoLock lock(mTreeLock);
+ // Repopulate mTouchBlockHitResult from the input state.
+ mTouchBlockHitResult = mHitTester->CloneHitTestResult(lock, aState.mHit);
+ hitScrollbarNode = std::move(aState.mHit.mScrollbarNode);
+
+ // Check if this event starts a scrollbar touch-drag. The conditions
+ // checked are similar to the ones we check for MOUSE_INPUT starting
+ // a scrollbar mouse-drag.
+ mInScrollbarTouchDrag =
+ StaticPrefs::apz_drag_enabled() &&
+ StaticPrefs::apz_drag_touch_enabled() && hitScrollbarNode &&
+ hitScrollbarNode->IsScrollThumbNode() &&
+ hitScrollbarNode->GetScrollbarData().mThumbIsAsyncDraggable;
+
+ MOZ_ASSERT(touchBehaviors.Length() == aInput.mTouches.Length());
+ for (size_t i = 0; i < touchBehaviors.Length(); i++) {
+ APZCTM_LOG("Touch point has allowed behaviours 0x%02x\n",
+ touchBehaviors[i]);
+ if (touchBehaviors[i] == AllowedTouchBehavior::UNKNOWN) {
+ // If there's any unknown items in the list, throw it out and we'll
+ // wait for the main thread to send us a notification.
+ touchBehaviors.Clear();
+ break;
+ }
+ }
+ } else if (mTouchBlockHitResult.mTargetApzc) {
+ APZCTM_LOG("Re-using APZC %p as continuation of event block\n",
+ mTouchBlockHitResult.mTargetApzc.get());
+ RecursiveMutexAutoLock lock(mTreeLock);
+ aState.mHit = mHitTester->CloneHitTestResult(lock, mTouchBlockHitResult);
+ }
+
+ if (mInScrollbarTouchDrag) {
+ aState.mResult = ProcessTouchInputForScrollbarDrag(
+ aInput, hitScrollbarNode, mTouchBlockHitResult.mHitResult);
+ } else {
+ // If we receive a touch-cancel, it means all touches are finished, so we
+ // can stop ignoring any that we were ignoring.
+ if (aInput.mType == MultiTouchInput::MULTITOUCH_CANCEL) {
+ mRetainedTouchIdentifier = -1;
+ }
+
+ // If we are currently ignoring any touch points, filter them out from the
+ // set of touch points included in this event. Note that we modify aInput
+ // itself, so that the touch points are also filtered out when the caller
+ // passes the event on to content.
+ if (mRetainedTouchIdentifier != -1) {
+ for (size_t j = 0; j < aInput.mTouches.Length(); ++j) {
+ if (aInput.mTouches[j].mIdentifier != mRetainedTouchIdentifier) {
+ aInput.mTouches.RemoveElementAt(j);
+ if (!touchBehaviors.IsEmpty()) {
+ MOZ_ASSERT(touchBehaviors.Length() > j);
+ touchBehaviors.RemoveElementAt(j);
+ }
+ --j;
+ }
+ }
+ if (aInput.mTouches.IsEmpty()) {
+ aState.mResult.SetStatusAsConsumeNoDefault();
+ return;
+ }
+ }
+
+ if (mTouchBlockHitResult.mTargetApzc) {
+ MOZ_ASSERT(mTouchBlockHitResult.mHitResult !=
+ CompositorHitTestInvisibleToHit);
+
+ aState.mResult = mInputQueue->ReceiveInputEvent(
+ mTouchBlockHitResult.mTargetApzc,
+ TargetConfirmationFlags{mTouchBlockHitResult.mHitResult}, aInput,
+ touchBehaviors.IsEmpty() ? Nothing()
+ : Some(std::move(touchBehaviors)));
+
+ // For computing the event to pass back to Gecko, use up-to-date
+ // transforms (i.e. not anything cached in an input block). This ensures
+ // that transformToApzc and transformToGecko are in sync.
+ // Note: we are not using ConvertToGecko() here, because we don't
+ // want to multiply transformToApzc and transformToGecko once
+ // for each touch point.
+ ScreenToParentLayerMatrix4x4 transformToApzc =
+ GetScreenToApzcTransform(mTouchBlockHitResult.mTargetApzc);
+ ParentLayerToScreenMatrix4x4 transformToGecko =
+ GetApzcToGeckoTransformForHit(mTouchBlockHitResult);
+ ScreenToScreenMatrix4x4 outTransform = transformToApzc * transformToGecko;
+
+ for (size_t i = 0; i < aInput.mTouches.Length(); i++) {
+ SingleTouchData& touchData = aInput.mTouches[i];
+ Maybe<ScreenIntPoint> untransformedScreenPoint =
+ UntransformBy(outTransform, touchData.mScreenPoint);
+ if (!untransformedScreenPoint) {
+ aState.mResult.SetStatusAsIgnore();
+ return;
+ }
+ touchData.mScreenPoint = *untransformedScreenPoint;
+ AdjustEventPointForDynamicToolbar(touchData.mScreenPoint,
+ mTouchBlockHitResult);
+ }
+ }
+ }
+
+ mTouchCounter.Update(aInput);
+
+ // If it's the end of the touch sequence then clear out variables so we
+ // don't keep dangling references and leak things.
+ if (mTouchCounter.GetActiveTouchCount() == 0) {
+ mTouchBlockHitResult = HitTestResult();
+ mRetainedTouchIdentifier = -1;
+ mInScrollbarTouchDrag = false;
+ }
+}
+
+void APZCTreeManager::AdjustEventPointForDynamicToolbar(
+ ScreenIntPoint& aEventPoint, const HitTestResult& aHit) {
+ if (aHit.mFixedPosSides != SideBits::eNone) {
+ MutexAutoLock lock(mMapLock);
+ aEventPoint -= RoundedToInt(apz::ComputeFixedMarginsOffset(
+ GetCompositorFixedLayerMargins(lock), aHit.mFixedPosSides,
+ mGeckoFixedLayerMargins));
+ } else if (aHit.mNode && aHit.mNode->GetStickyPositionAnimationId()) {
+ SideBits sideBits = SideBits::eNone;
+ {
+ RecursiveMutexAutoLock lock(mTreeLock);
+ sideBits = SidesStuckToRootContent(aHit.mNode.Get(lock));
+ }
+ MutexAutoLock lock(mMapLock);
+ aEventPoint -= RoundedToInt(apz::ComputeFixedMarginsOffset(
+ GetCompositorFixedLayerMargins(lock), sideBits, ScreenMargin()));
+ }
+}
+
+static MouseInput::MouseType MultiTouchTypeToMouseType(
+ MultiTouchInput::MultiTouchType aType) {
+ switch (aType) {
+ case MultiTouchInput::MULTITOUCH_START:
+ return MouseInput::MOUSE_DOWN;
+ case MultiTouchInput::MULTITOUCH_MOVE:
+ return MouseInput::MOUSE_MOVE;
+ case MultiTouchInput::MULTITOUCH_END:
+ case MultiTouchInput::MULTITOUCH_CANCEL:
+ return MouseInput::MOUSE_UP;
+ }
+ MOZ_ASSERT_UNREACHABLE("Invalid multi-touch type");
+ return MouseInput::MOUSE_NONE;
+}
+
+APZEventResult APZCTreeManager::ProcessTouchInputForScrollbarDrag(
+ MultiTouchInput& aTouchInput,
+ const HitTestingTreeNodeAutoLock& aScrollThumbNode,
+ const gfx::CompositorHitTestInfo& aHitInfo) {
+ MOZ_ASSERT(mRetainedTouchIdentifier == -1);
+ MOZ_ASSERT(mTouchBlockHitResult.mTargetApzc);
+ MOZ_ASSERT(aTouchInput.mTouches.Length() == 1);
+
+ // Synthesize a mouse event based on the touch event, so that we can
+ // reuse code in InputQueue and APZC for handling scrollbar mouse-drags.
+ MouseInput mouseInput{MultiTouchTypeToMouseType(aTouchInput.mType),
+ MouseInput::PRIMARY_BUTTON,
+ dom::MouseEvent_Binding::MOZ_SOURCE_TOUCH,
+ MouseButtonsFlag::ePrimaryFlag,
+ aTouchInput.mTouches[0].mScreenPoint,
+ aTouchInput.mTimeStamp,
+ aTouchInput.modifiers};
+ mouseInput.mHandledByAPZ = true;
+
+ TargetConfirmationFlags targetConfirmed{aHitInfo};
+ APZEventResult result;
+ result = mInputQueue->ReceiveInputEvent(mTouchBlockHitResult.mTargetApzc,
+ targetConfirmed, mouseInput);
+
+ // |aScrollThumbNode| is non-null iff. this is the event that starts the drag.
+ // If so, set up the drag.
+ if (aScrollThumbNode) {
+ SetupScrollbarDrag(mouseInput, aScrollThumbNode,
+ mTouchBlockHitResult.mTargetApzc.get());
+ }
+
+ // Since the input was targeted at a scrollbar:
+ // - The original touch event (which will be sent on to content) will
+ // not be untransformed.
+ // - We don't want to apply the callback transform in the main thread,
+ // so we remove the scrollid from the guid.
+ // Both of these match the behaviour of mouse events that target a scrollbar;
+ // see the code for handling mouse events in ReceiveInputEvent() for
+ // additional explanation.
+ result.mTargetGuid.mScrollId = ScrollableLayerGuid::NULL_SCROLL_ID;
+
+ return result;
+}
+
+void APZCTreeManager::SetupScrollbarDrag(
+ MouseInput& aMouseInput, const HitTestingTreeNodeAutoLock& aScrollThumbNode,
+ AsyncPanZoomController* aApzc) {
+ DragBlockState* dragBlock = mInputQueue->GetCurrentDragBlock();
+ if (!dragBlock) {
+ return;
+ }
+
+ const ScrollbarData& thumbData = aScrollThumbNode->GetScrollbarData();
+ MOZ_ASSERT(thumbData.mDirection.isSome());
+
+ // Record the thumb's position at the start of the drag.
+ // We snap back to this position if, during the drag, the mouse
+ // gets sufficiently far away from the scrollbar.
+ dragBlock->SetInitialThumbPos(thumbData.mThumbStart);
+
+ // Under some conditions, we can confirm the drag block right away.
+ // Otherwise, we have to wait for a main-thread confirmation.
+ if (StaticPrefs::apz_drag_initial_enabled() &&
+ // check that the scrollbar's target scroll frame is layerized
+ aScrollThumbNode->GetScrollTargetId() == aApzc->GetGuid().mScrollId &&
+ !aApzc->IsScrollInfoLayer()) {
+ uint64_t dragBlockId = dragBlock->GetBlockId();
+ // AsyncPanZoomController::HandleInputEvent() will call
+ // TransformToLocal() on the event, but we need its mLocalOrigin now
+ // to compute a drag start offset for the AsyncDragMetrics.
+ aMouseInput.TransformToLocal(aApzc->GetTransformToThis());
+ OuterCSSCoord dragStart =
+ aApzc->ConvertScrollbarPoint(aMouseInput.mLocalOrigin, thumbData);
+ // ConvertScrollbarPoint() got the drag start offset relative to
+ // the scroll track. Now get it relative to the thumb.
+ // ScrollThumbData::mThumbStart stores the offset of the thumb
+ // relative to the scroll track at the time of the last paint.
+ // Since that paint, the thumb may have acquired an async transform
+ // due to async scrolling, so look that up and apply it.
+ LayerToParentLayerMatrix4x4 thumbTransform;
+ {
+ RecursiveMutexAutoLock lock(mTreeLock);
+ thumbTransform = ComputeTransformForNode(aScrollThumbNode.Get(lock));
+ }
+ // Only consider the translation, since we do not support both
+ // zooming and scrollbar dragging on any platform.
+ OuterCSSCoord thumbStart =
+ thumbData.mThumbStart +
+ ((*thumbData.mDirection == ScrollDirection::eHorizontal)
+ ? thumbTransform._41
+ : thumbTransform._42);
+ dragStart -= thumbStart;
+
+ // Content can't prevent scrollbar dragging with preventDefault(),
+ // so we don't need to wait for a content response. It's important
+ // to do this before calling ConfirmDragBlock() since that can
+ // potentially process and consume the block.
+ dragBlock->SetContentResponse(false);
+
+ NotifyScrollbarDragInitiated(dragBlockId, aApzc->GetGuid(),
+ *thumbData.mDirection);
+
+ mInputQueue->ConfirmDragBlock(
+ dragBlockId, aApzc,
+ AsyncDragMetrics(aApzc->GetGuid().mScrollId,
+ aApzc->GetGuid().mPresShellId, dragBlockId, dragStart,
+ *thumbData.mDirection));
+ }
+}
+
+void APZCTreeManager::SynthesizePinchGestureFromMouseWheel(
+ const ScrollWheelInput& aWheelInput,
+ const RefPtr<AsyncPanZoomController>& aTarget) {
+ MOZ_ASSERT(aTarget);
+
+ ScreenPoint focusPoint = aWheelInput.mOrigin;
+
+ // Compute span values based on the wheel delta.
+ ScreenCoord oldSpan = 100;
+ ScreenCoord newSpan = oldSpan + aWheelInput.mDeltaY;
+
+ // There's no ambiguity as to the target for pinch gesture events.
+ TargetConfirmationFlags confFlags{true};
+
+ PinchGestureInput pinchStart{PinchGestureInput::PINCHGESTURE_START,
+ PinchGestureInput::MOUSEWHEEL,
+ aWheelInput.mTimeStamp,
+ ExternalPoint(0, 0),
+ focusPoint,
+ oldSpan,
+ oldSpan,
+ aWheelInput.modifiers};
+ PinchGestureInput pinchScale1{PinchGestureInput::PINCHGESTURE_SCALE,
+ PinchGestureInput::MOUSEWHEEL,
+ aWheelInput.mTimeStamp,
+ ExternalPoint(0, 0),
+ focusPoint,
+ oldSpan,
+ oldSpan,
+ aWheelInput.modifiers};
+ PinchGestureInput pinchScale2{PinchGestureInput::PINCHGESTURE_SCALE,
+ PinchGestureInput::MOUSEWHEEL,
+ aWheelInput.mTimeStamp,
+ ExternalPoint(0, 0),
+ focusPoint,
+ oldSpan,
+ newSpan,
+ aWheelInput.modifiers};
+ PinchGestureInput pinchEnd{PinchGestureInput::PINCHGESTURE_END,
+ PinchGestureInput::MOUSEWHEEL,
+ aWheelInput.mTimeStamp,
+ ExternalPoint(0, 0),
+ focusPoint,
+ newSpan,
+ newSpan,
+ aWheelInput.modifiers};
+
+ mInputQueue->ReceiveInputEvent(aTarget, confFlags, pinchStart);
+ mInputQueue->ReceiveInputEvent(aTarget, confFlags, pinchScale1);
+ mInputQueue->ReceiveInputEvent(aTarget, confFlags, pinchScale2);
+ mInputQueue->ReceiveInputEvent(aTarget, confFlags, pinchEnd);
+}
+
+void APZCTreeManager::UpdateWheelTransaction(
+ LayoutDeviceIntPoint aRefPoint, EventMessage aEventMessage,
+ const Maybe<ScrollableLayerGuid>& aTargetGuid) {
+ APZThreadUtils::AssertOnControllerThread();
+
+ WheelBlockState* txn = mInputQueue->GetActiveWheelTransaction();
+ if (!txn) {
+ return;
+ }
+
+ // If the transaction has simply timed out, we don't need to do anything
+ // else.
+ if (txn->MaybeTimeout(TimeStamp::Now())) {
+ return;
+ }
+
+ switch (aEventMessage) {
+ case eMouseMove:
+ case eDragOver: {
+ ScreenIntPoint point = ViewAs<ScreenPixel>(
+ aRefPoint,
+ PixelCastJustification::LayoutDeviceIsScreenForUntransformedEvent);
+
+ txn->OnMouseMove(point, aTargetGuid);
+
+ return;
+ }
+ case eKeyPress:
+ case eKeyUp:
+ case eKeyDown:
+ case eMouseUp:
+ case eMouseDown:
+ case eMouseDoubleClick:
+ case eMouseAuxClick:
+ case eMouseClick:
+ case eContextMenu:
+ case eDrop:
+ txn->EndTransaction();
+ return;
+ default:
+ break;
+ }
+}
+
+void APZCTreeManager::ProcessUnhandledEvent(LayoutDeviceIntPoint* aRefPoint,
+ ScrollableLayerGuid* aOutTargetGuid,
+ uint64_t* aOutFocusSequenceNumber,
+ LayersId* aOutLayersId) {
+ APZThreadUtils::AssertOnControllerThread();
+
+ // Transform the aRefPoint.
+ // If the event hits an overscrolled APZC, instruct the caller to ignore it.
+ PixelCastJustification LDIsScreen =
+ PixelCastJustification::LayoutDeviceIsScreenForUntransformedEvent;
+ ScreenIntPoint refPointAsScreen = ViewAs<ScreenPixel>(*aRefPoint, LDIsScreen);
+ HitTestResult hit = GetTargetAPZC(refPointAsScreen);
+ if (aOutLayersId) {
+ *aOutLayersId = hit.mLayersId;
+ }
+ if (hit.mTargetApzc) {
+ MOZ_ASSERT(hit.mHitResult != CompositorHitTestInvisibleToHit);
+ hit.mTargetApzc->GetGuid(aOutTargetGuid);
+ ScreenToParentLayerMatrix4x4 transformToApzc =
+ GetScreenToApzcTransform(hit.mTargetApzc);
+ ParentLayerToScreenMatrix4x4 transformToGecko =
+ GetApzcToGeckoTransformForHit(hit);
+ ScreenToScreenMatrix4x4 outTransform = transformToApzc * transformToGecko;
+ Maybe<ScreenIntPoint> untransformedRefPoint =
+ UntransformBy(outTransform, refPointAsScreen);
+ if (untransformedRefPoint) {
+ *aRefPoint =
+ ViewAs<LayoutDevicePixel>(*untransformedRefPoint, LDIsScreen);
+ }
+ }
+
+ // Update the focus sequence number and attach it to the event
+ mFocusState.ReceiveFocusChangingEvent();
+ *aOutFocusSequenceNumber = mFocusState.LastAPZProcessedEvent();
+}
+
+void APZCTreeManager::SetKeyboardMap(const KeyboardMap& aKeyboardMap) {
+ if (!APZThreadUtils::IsControllerThread()) {
+ APZThreadUtils::RunOnControllerThread(NewRunnableMethod<KeyboardMap>(
+ "layers::APZCTreeManager::SetKeyboardMap", this,
+ &APZCTreeManager::SetKeyboardMap, aKeyboardMap));
+ return;
+ }
+
+ APZThreadUtils::AssertOnControllerThread();
+
+ mKeyboardMap = aKeyboardMap;
+}
+
+void APZCTreeManager::ZoomToRect(const ScrollableLayerGuid& aGuid,
+ const ZoomTarget& aZoomTarget,
+ const uint32_t aFlags) {
+ if (!APZThreadUtils::IsControllerThread()) {
+ APZThreadUtils::RunOnControllerThread(
+ NewRunnableMethod<ScrollableLayerGuid, ZoomTarget, uint32_t>(
+ "layers::APZCTreeManager::ZoomToRect", this,
+ &APZCTreeManager::ZoomToRect, aGuid, aZoomTarget, aFlags));
+ return;
+ }
+
+ // We could probably move this to run on the updater thread if needed, but
+ // either way we should restrict it to a single thread. For now let's use the
+ // controller thread.
+ APZThreadUtils::AssertOnControllerThread();
+
+ RefPtr<AsyncPanZoomController> apzc = GetTargetAPZC(aGuid);
+ if (apzc) {
+ apzc->ZoomToRect(aZoomTarget, aFlags);
+ }
+}
+
+void APZCTreeManager::ContentReceivedInputBlock(uint64_t aInputBlockId,
+ bool aPreventDefault) {
+ if (!APZThreadUtils::IsControllerThread()) {
+ APZThreadUtils::RunOnControllerThread(NewRunnableMethod<uint64_t, bool>(
+ "layers::APZCTreeManager::ContentReceivedInputBlock", this,
+ &APZCTreeManager::ContentReceivedInputBlock, aInputBlockId,
+ aPreventDefault));
+ return;
+ }
+
+ APZThreadUtils::AssertOnControllerThread();
+
+ mInputQueue->ContentReceivedInputBlock(aInputBlockId, aPreventDefault);
+}
+
+void APZCTreeManager::SetTargetAPZC(
+ uint64_t aInputBlockId, const nsTArray<ScrollableLayerGuid>& aTargets) {
+ if (!APZThreadUtils::IsControllerThread()) {
+ APZThreadUtils::RunOnControllerThread(
+ NewRunnableMethod<uint64_t,
+ StoreCopyPassByRRef<nsTArray<ScrollableLayerGuid>>>(
+ "layers::APZCTreeManager::SetTargetAPZC", this,
+ &layers::APZCTreeManager::SetTargetAPZC, aInputBlockId,
+ aTargets.Clone()));
+ return;
+ }
+
+ RefPtr<AsyncPanZoomController> target = nullptr;
+ if (aTargets.Length() > 0) {
+ target = GetTargetAPZC(aTargets[0]);
+ }
+ for (size_t i = 1; i < aTargets.Length(); i++) {
+ RefPtr<AsyncPanZoomController> apzc = GetTargetAPZC(aTargets[i]);
+ target = GetZoomableTarget(target, apzc);
+ }
+ if (InputBlockState* block = mInputQueue->GetBlockForId(aInputBlockId)) {
+ if (block->AsPinchGestureBlock() && aTargets.Length() == 1) {
+ target = FindZoomableApzc(target);
+ }
+ }
+ mInputQueue->SetConfirmedTargetApzc(aInputBlockId, target);
+}
+
+void APZCTreeManager::UpdateZoomConstraints(
+ const ScrollableLayerGuid& aGuid,
+ const Maybe<ZoomConstraints>& aConstraints) {
+ if (!GetUpdater()->IsUpdaterThread()) {
+ // This can happen if we're in the UI process and got a call directly from
+ // nsBaseWidget or from a content process over PAPZCTreeManager. In that
+ // case we get this call on the compositor thread, which may be different
+ // from the updater thread. It can also happen in the GPU process if that is
+ // enabled, since the call will go over PAPZCTreeManager and arrive on the
+ // compositor thread in the GPU process.
+ GetUpdater()->RunOnUpdaterThread(
+ aGuid.mLayersId,
+ NewRunnableMethod<ScrollableLayerGuid, Maybe<ZoomConstraints>>(
+ "APZCTreeManager::UpdateZoomConstraints", this,
+ &APZCTreeManager::UpdateZoomConstraints, aGuid, aConstraints));
+ return;
+ }
+
+ AssertOnUpdaterThread();
+
+ // Propagate the zoom constraints down to the subtree, stopping at APZCs
+ // which have their own zoom constraints or are in a different layers id.
+ if (aConstraints) {
+ APZCTM_LOG("Recording constraints %s for guid %s\n",
+ ToString(aConstraints.value()).c_str(), ToString(aGuid).c_str());
+ mZoomConstraints[aGuid] = aConstraints.ref();
+ } else {
+ APZCTM_LOG("Removing constraints for guid %s\n", ToString(aGuid).c_str());
+ mZoomConstraints.erase(aGuid);
+ }
+
+ RecursiveMutexAutoLock lock(mTreeLock);
+ RefPtr<HitTestingTreeNode> node = DepthFirstSearchPostOrder<ReverseIterator>(
+ mRootNode.get(), [&aGuid](HitTestingTreeNode* aNode) {
+ bool matches = false;
+ if (auto zoomId = aNode->GetAsyncZoomContainerId()) {
+ matches = ScrollableLayerGuid::EqualsIgnoringPresShell(
+ aGuid, ScrollableLayerGuid(aNode->GetLayersId(), 0, *zoomId));
+ }
+ return matches;
+ });
+
+ // This does not hold because we can get zoom constraints updates before the
+ // layer tree update with the async zoom container (I assume).
+ // clang-format off
+ // MOZ_ASSERT(node || aConstraints.isNothing() ||
+ // (!aConstraints->mAllowZoom && !aConstraints->mAllowDoubleTapZoom));
+ // clang-format on
+
+ // If there is no async zoom container then the zoom constraints should not
+ // allow zooming and building the HTT should have handled clearing the zoom
+ // constraints from all nodes so we don't have to handle doing anything in
+ // case there is no async zoom container.
+
+ if (node && aConstraints) {
+ ForEachNode<ReverseIterator>(node.get(), [&aConstraints, &node, &aGuid,
+ this](HitTestingTreeNode* aNode) {
+ if (aNode != node) {
+ // don't go into other async zoom containers
+ if (auto zoomId = aNode->GetAsyncZoomContainerId()) {
+ MOZ_ASSERT(!ScrollableLayerGuid::EqualsIgnoringPresShell(
+ aGuid, ScrollableLayerGuid(aNode->GetLayersId(), 0, *zoomId)));
+ return TraversalFlag::Skip;
+ }
+ if (AsyncPanZoomController* childApzc = aNode->GetApzc()) {
+ if (!ScrollableLayerGuid::EqualsIgnoringPresShell(
+ aGuid, childApzc->GetGuid())) {
+ // We can have subtrees with their own zoom constraints - leave
+ // these alone.
+ if (this->mZoomConstraints.find(childApzc->GetGuid()) !=
+ this->mZoomConstraints.end()) {
+ return TraversalFlag::Skip;
+ }
+ }
+ }
+ }
+ if (aNode->IsPrimaryHolder()) {
+ MOZ_ASSERT(aNode->GetApzc());
+ aNode->GetApzc()->UpdateZoomConstraints(aConstraints.ref());
+ }
+ return TraversalFlag::Continue;
+ });
+ }
+}
+
+void APZCTreeManager::FlushRepaintsToClearScreenToGeckoTransform() {
+ // As the name implies, we flush repaint requests for the entire APZ tree in
+ // order to clear the screen-to-gecko transform (aka the "untransform" applied
+ // to incoming input events before they can be passed on to Gecko).
+ //
+ // The primary reason we do this is to avoid the problem where input events,
+ // after being untransformed, end up hit-testing differently in Gecko. This
+ // might happen in cases where the input event lands on content that is async-
+ // scrolled into view, but Gecko still thinks it is out of view given the
+ // visible area of a scrollframe.
+ //
+ // Another reason we want to clear the untransform is that if our APZ hit-test
+ // hits a dispatch-to-content region then that's an ambiguous result and we
+ // need to ask Gecko what actually got hit. In order to do this we need to
+ // untransform the input event into Gecko space - but to do that we need to
+ // know which APZC got hit! This leads to a circular dependency; the only way
+ // to get out of it is to make sure that the untransform for all the possible
+ // matched APZCs is the same. It is simplest to ensure that by flushing the
+ // pending repaint requests, which makes all of the untransforms empty (and
+ // therefore equal).
+ RecursiveMutexAutoLock lock(mTreeLock);
+
+ ForEachNode<ReverseIterator>(mRootNode.get(), [](HitTestingTreeNode* aNode) {
+ if (aNode->IsPrimaryHolder()) {
+ MOZ_ASSERT(aNode->GetApzc());
+ aNode->GetApzc()->FlushRepaintForNewInputBlock();
+ }
+ });
+}
+
+void APZCTreeManager::ClearTree() {
+ AssertOnUpdaterThread();
+
+ // Ensure that no references to APZCs are alive in any lingering input
+ // blocks. This breaks cycles from InputBlockState::mTargetApzc back to
+ // the InputQueue.
+ APZThreadUtils::RunOnControllerThread(NewRunnableMethod(
+ "layers::InputQueue::Clear", mInputQueue, &InputQueue::Clear));
+
+ RecursiveMutexAutoLock lock(mTreeLock);
+
+ // Collect the nodes into a list, and then destroy each one.
+ // We can't destroy them as we collect them, because ForEachNode()
+ // does a pre-order traversal of the tree, and Destroy() nulls out
+ // the fields needed to reach the children of the node.
+ nsTArray<RefPtr<HitTestingTreeNode>> nodesToDestroy;
+ ForEachNode<ReverseIterator>(mRootNode.get(),
+ [&nodesToDestroy](HitTestingTreeNode* aNode) {
+ nodesToDestroy.AppendElement(aNode);
+ });
+
+ for (size_t i = 0; i < nodesToDestroy.Length(); i++) {
+ nodesToDestroy[i]->Destroy();
+ }
+ mRootNode = nullptr;
+
+ {
+ // Also remove references to APZC instances in the map
+ MutexAutoLock lock(mMapLock);
+ mApzcMap.clear();
+ }
+
+ RefPtr<APZCTreeManager> self(this);
+ NS_DispatchToMainThread(
+ NS_NewRunnableFunction("layers::APZCTreeManager::ClearTree", [self] {
+ self->mFlushObserver->Unregister();
+ self->mFlushObserver = nullptr;
+ }));
+}
+
+RefPtr<HitTestingTreeNode> APZCTreeManager::GetRootNode() const {
+ RecursiveMutexAutoLock lock(mTreeLock);
+ return mRootNode;
+}
+
+/**
+ * Transform a displacement from the ParentLayer coordinates of a source APZC
+ * to the ParentLayer coordinates of a target APZC.
+ * @param aTreeManager the tree manager for the APZC tree containing |aSource|
+ * and |aTarget|
+ * @param aSource the source APZC
+ * @param aTarget the target APZC
+ * @param aStartPoint the start point of the displacement
+ * @param aEndPoint the end point of the displacement
+ * @return true on success, false if aStartPoint or aEndPoint cannot be
+ * transformed into target's coordinate space
+ */
+static bool TransformDisplacement(APZCTreeManager* aTreeManager,
+ AsyncPanZoomController* aSource,
+ AsyncPanZoomController* aTarget,
+ ParentLayerPoint& aStartPoint,
+ ParentLayerPoint& aEndPoint) {
+ if (aSource == aTarget) {
+ return true;
+ }
+
+ // Convert start and end points to Screen coordinates.
+ ParentLayerToScreenMatrix4x4 untransformToApzc =
+ aTreeManager->GetScreenToApzcTransform(aSource).Inverse();
+ ScreenPoint screenStart = TransformBy(untransformToApzc, aStartPoint);
+ ScreenPoint screenEnd = TransformBy(untransformToApzc, aEndPoint);
+
+ // Convert start and end points to aTarget's ParentLayer coordinates.
+ ScreenToParentLayerMatrix4x4 transformToApzc =
+ aTreeManager->GetScreenToApzcTransform(aTarget);
+ Maybe<ParentLayerPoint> startPoint =
+ UntransformBy(transformToApzc, screenStart);
+ Maybe<ParentLayerPoint> endPoint = UntransformBy(transformToApzc, screenEnd);
+ if (!startPoint || !endPoint) {
+ return false;
+ }
+ aEndPoint = *endPoint;
+ aStartPoint = *startPoint;
+
+ return true;
+}
+
+bool APZCTreeManager::DispatchScroll(
+ AsyncPanZoomController* aPrev, ParentLayerPoint& aStartPoint,
+ ParentLayerPoint& aEndPoint,
+ OverscrollHandoffState& aOverscrollHandoffState) {
+ const OverscrollHandoffChain& overscrollHandoffChain =
+ aOverscrollHandoffState.mChain;
+ uint32_t overscrollHandoffChainIndex = aOverscrollHandoffState.mChainIndex;
+ RefPtr<AsyncPanZoomController> next;
+ // If we have reached the end of the overscroll handoff chain, there is
+ // nothing more to scroll, so we ignore the rest of the pan gesture.
+ if (overscrollHandoffChainIndex >= overscrollHandoffChain.Length()) {
+ // Nothing more to scroll - ignore the rest of the pan gesture.
+ return false;
+ }
+
+ next = overscrollHandoffChain.GetApzcAtIndex(overscrollHandoffChainIndex);
+
+ if (next == nullptr || next->IsDestroyed()) {
+ return false;
+ }
+
+ // Convert the start and end points from |aPrev|'s coordinate space to
+ // |next|'s coordinate space.
+ if (!TransformDisplacement(this, aPrev, next, aStartPoint, aEndPoint)) {
+ return false;
+ }
+
+ // Scroll |next|. If this causes overscroll, it will call DispatchScroll()
+ // again with an incremented index.
+ if (!next->AttemptScroll(aStartPoint, aEndPoint, aOverscrollHandoffState)) {
+ // Transform |aStartPoint| and |aEndPoint| (which now represent the
+ // portion of the displacement that wasn't consumed by APZCs later
+ // in the handoff chain) back into |aPrev|'s coordinate space. This
+ // allows the caller (which is |aPrev|) to interpret the unconsumed
+ // displacement in its own coordinate space, and make use of it
+ // (e.g. by going into overscroll).
+ if (!TransformDisplacement(this, next, aPrev, aStartPoint, aEndPoint)) {
+ NS_WARNING("Failed to untransform scroll points during dispatch");
+ }
+ return false;
+ }
+
+ // Return true to indicate the scroll was consumed entirely.
+ return true;
+}
+
+ParentLayerPoint APZCTreeManager::DispatchFling(
+ AsyncPanZoomController* aPrev, const FlingHandoffState& aHandoffState) {
+ // If immediate handoff is disallowed, do not allow handoff beyond the
+ // single APZC that's scrolled by the input block that triggered this fling.
+ if (aHandoffState.mIsHandoff && !StaticPrefs::apz_allow_immediate_handoff() &&
+ aHandoffState.mScrolledApzc == aPrev) {
+ FLING_LOG("APZCTM dropping handoff due to disallowed immediate handoff\n");
+ return aHandoffState.mVelocity;
+ }
+
+ const OverscrollHandoffChain* chain = aHandoffState.mChain;
+ RefPtr<AsyncPanZoomController> current;
+ uint32_t overscrollHandoffChainLength = chain->Length();
+ uint32_t startIndex;
+
+ // The fling's velocity needs to be transformed from the screen coordinates
+ // of |aPrev| to the screen coordinates of |next|. To transform a velocity
+ // correctly, we need to convert it to a displacement. For now, we do this
+ // by anchoring it to a start point of (0, 0).
+ // TODO: For this to be correct in the presence of 3D transforms, we should
+ // use the end point of the touch that started the fling as the start point
+ // rather than (0, 0).
+ ParentLayerPoint startPoint; // (0, 0)
+ ParentLayerPoint endPoint;
+
+ if (aHandoffState.mIsHandoff) {
+ startIndex = chain->IndexOf(aPrev) + 1;
+
+ // IndexOf will return aOverscrollHandoffChain->Length() if
+ // |aPrev| is not found.
+ if (startIndex >= overscrollHandoffChainLength) {
+ return aHandoffState.mVelocity;
+ }
+ } else {
+ startIndex = 0;
+ }
+
+ // This will store any velocity left over after the entire handoff.
+ ParentLayerPoint finalResidualVelocity = aHandoffState.mVelocity;
+
+ ParentLayerPoint currentVelocity = aHandoffState.mVelocity;
+ for (; startIndex < overscrollHandoffChainLength; startIndex++) {
+ current = chain->GetApzcAtIndex(startIndex);
+
+ // Make sure the apzc about to be handled can be handled
+ if (current == nullptr || current->IsDestroyed()) {
+ break;
+ }
+
+ endPoint = startPoint + currentVelocity;
+
+ RefPtr<AsyncPanZoomController> prevApzc =
+ (startIndex > 0) ? chain->GetApzcAtIndex(startIndex - 1) : nullptr;
+
+ // Only transform when current apzc can be transformed with previous
+ if (prevApzc) {
+ if (!TransformDisplacement(this, prevApzc, current, startPoint,
+ endPoint)) {
+ break;
+ }
+ }
+
+ ParentLayerPoint availableVelocity = (endPoint - startPoint);
+ ParentLayerPoint residualVelocity;
+
+ FlingHandoffState transformedHandoffState = aHandoffState;
+ transformedHandoffState.mVelocity = availableVelocity;
+
+ // Obey overscroll-behavior.
+ if (prevApzc) {
+ residualVelocity += prevApzc->AdjustHandoffVelocityForOverscrollBehavior(
+ transformedHandoffState.mVelocity);
+ }
+
+ residualVelocity += current->AttemptFling(transformedHandoffState);
+
+ // If there's no residual velocity, there's nothing more to hand off.
+ if (current->IsZero(residualVelocity)) {
+ return ParentLayerPoint();
+ }
+
+ // If any of the velocity available to be handed off was consumed,
+ // subtract the proportion of consumed velocity from finalResidualVelocity.
+ // Note: it's important to compare |residualVelocity| to |availableVelocity|
+ // here and not to |transformedHandoffState.mVelocity|, since the latter
+ // may have been modified by AdjustHandoffVelocityForOverscrollBehavior().
+ if (!current->IsZero(availableVelocity.x - residualVelocity.x)) {
+ finalResidualVelocity.x *= (residualVelocity.x / availableVelocity.x);
+ }
+ if (!current->IsZero(availableVelocity.y - residualVelocity.y)) {
+ finalResidualVelocity.y *= (residualVelocity.y / availableVelocity.y);
+ }
+
+ currentVelocity = residualVelocity;
+ }
+
+ // Return any residual velocity left over after the entire handoff process.
+ return finalResidualVelocity;
+}
+
+already_AddRefed<AsyncPanZoomController> APZCTreeManager::GetTargetAPZC(
+ const ScrollableLayerGuid& aGuid) {
+ RecursiveMutexAutoLock lock(mTreeLock);
+ RefPtr<HitTestingTreeNode> node = GetTargetNode(aGuid, nullptr);
+ MOZ_ASSERT(!node || node->GetApzc()); // any node returned must have an APZC
+ RefPtr<AsyncPanZoomController> apzc = node ? node->GetApzc() : nullptr;
+ return apzc.forget();
+}
+
+already_AddRefed<AsyncPanZoomController> APZCTreeManager::GetTargetAPZC(
+ const LayersId& aLayersId,
+ const ScrollableLayerGuid::ViewID& aScrollId) const {
+ MutexAutoLock lock(mMapLock);
+ return GetTargetAPZC(aLayersId, aScrollId, lock);
+}
+
+already_AddRefed<AsyncPanZoomController> APZCTreeManager::GetTargetAPZC(
+ const LayersId& aLayersId, const ScrollableLayerGuid::ViewID& aScrollId,
+ const MutexAutoLock& aProofOfMapLock) const {
+ ScrollableLayerGuid guid(aLayersId, 0, aScrollId);
+ auto it = mApzcMap.find(guid);
+ RefPtr<AsyncPanZoomController> apzc =
+ (it != mApzcMap.end() ? it->second.apzc : nullptr);
+ return apzc.forget();
+}
+
+already_AddRefed<HitTestingTreeNode> APZCTreeManager::GetTargetNode(
+ const ScrollableLayerGuid& aGuid, GuidComparator aComparator) const {
+ mTreeLock.AssertCurrentThreadIn();
+ RefPtr<HitTestingTreeNode> target =
+ DepthFirstSearchPostOrder<ReverseIterator>(
+ mRootNode.get(), [&aGuid, &aComparator](HitTestingTreeNode* node) {
+ bool matches = false;
+ if (node->GetApzc()) {
+ if (aComparator) {
+ matches = aComparator(aGuid, node->GetApzc()->GetGuid());
+ } else {
+ matches = node->GetApzc()->Matches(aGuid);
+ }
+ }
+ return matches;
+ });
+ return target.forget();
+}
+
+APZCTreeManager::HitTestResult APZCTreeManager::GetTargetAPZC(
+ const ScreenPoint& aPoint) {
+ RecursiveMutexAutoLock lock(mTreeLock);
+ MOZ_ASSERT(mHitTester);
+ return mHitTester->GetAPZCAtPoint(aPoint, lock);
+}
+
+APZCTreeManager::TargetApzcForNodeResult APZCTreeManager::FindHandoffParent(
+ const AsyncPanZoomController* aApzc) {
+ RefPtr<HitTestingTreeNode> node = GetTargetNode(aApzc->GetGuid(), nullptr);
+ while (node) {
+ auto result = GetTargetApzcForNode(node->GetParent());
+ if (result.mApzc) {
+ // avoid infinite recursion in the overscroll handoff chain.
+ if (result.mApzc != aApzc) {
+ return result;
+ }
+ }
+ node = node->GetParent();
+ }
+
+ return {nullptr, false};
+}
+
+RefPtr<const OverscrollHandoffChain>
+APZCTreeManager::BuildOverscrollHandoffChain(
+ const RefPtr<AsyncPanZoomController>& aInitialTarget) {
+ // Scroll grabbing is a mechanism that allows content to specify that
+ // the initial target of a pan should be not the innermost scrollable
+ // frame at the touch point (which is what GetTargetAPZC finds), but
+ // something higher up in the tree.
+ // It's not sufficient to just find the initial target, however, as
+ // overscroll can be handed off to another APZC. Without scroll grabbing,
+ // handoff just occurs from child to parent. With scroll grabbing, the
+ // handoff order can be different, so we build a chain of APZCs in the
+ // order in which scroll will be handed off to them.
+
+ // Grab tree lock since we'll be walking the APZC tree.
+ RecursiveMutexAutoLock lock(mTreeLock);
+
+ // Build the chain. If there is a scroll parent link, we use that. This is
+ // needed to deal with scroll info layers, because they participate in handoff
+ // but do not follow the expected layer tree structure. If there are no
+ // scroll parent links we just walk up the tree to find the scroll parent.
+ OverscrollHandoffChain* result = new OverscrollHandoffChain;
+ AsyncPanZoomController* apzc = aInitialTarget;
+ while (apzc != nullptr) {
+ result->Add(apzc);
+
+ APZCTreeManager::TargetApzcForNodeResult handoffResult =
+ FindHandoffParent(apzc);
+
+ if (!handoffResult.mIsFixed && !apzc->IsRootForLayersId() &&
+ apzc->GetScrollHandoffParentId() ==
+ ScrollableLayerGuid::NULL_SCROLL_ID) {
+ // This probably indicates a bug or missed case in layout code
+ NS_WARNING("Found a non-root APZ with no handoff parent");
+ }
+
+ // If `apzc` is inside fixed content, we want to hand off to the document's
+ // root APZC next. The scroll parent id wouldn't give us this because it's
+ // based on ASRs.
+ if (handoffResult.mIsFixed || apzc->GetScrollHandoffParentId() ==
+ ScrollableLayerGuid::NULL_SCROLL_ID) {
+ apzc = handoffResult.mApzc;
+ continue;
+ }
+
+ // Guard against a possible infinite-loop condition. If we hit this, the
+ // layout code that generates the handoff parents did something wrong.
+ MOZ_ASSERT(apzc->GetScrollHandoffParentId() != apzc->GetGuid().mScrollId);
+ RefPtr<AsyncPanZoomController> scrollParent = GetTargetAPZC(
+ apzc->GetGuid().mLayersId, apzc->GetScrollHandoffParentId());
+ apzc = scrollParent.get();
+ }
+
+ // Now adjust the chain to account for scroll grabbing. Sorting is a bit
+ // of an overkill here, but scroll grabbing will likely be generalized
+ // to scroll priorities, so we might as well do it this way.
+ result->SortByScrollPriority();
+
+ // Print the overscroll chain for debugging.
+ for (uint32_t i = 0; i < result->Length(); ++i) {
+ APZCTM_LOG("OverscrollHandoffChain[%d] = %p\n", i,
+ result->GetApzcAtIndex(i).get());
+ }
+
+ return result;
+}
+
+void APZCTreeManager::SetLongTapEnabled(bool aLongTapEnabled) {
+ if (!APZThreadUtils::IsControllerThread()) {
+ APZThreadUtils::RunOnControllerThread(NewRunnableMethod<bool>(
+ "layers::APZCTreeManager::SetLongTapEnabled", this,
+ &APZCTreeManager::SetLongTapEnabled, aLongTapEnabled));
+ return;
+ }
+
+ APZThreadUtils::AssertOnControllerThread();
+ GestureEventListener::SetLongTapEnabled(aLongTapEnabled);
+}
+
+void APZCTreeManager::AddInputBlockCallback(
+ uint64_t aInputBlockId, InputBlockCallbackInfo&& aCallbackInfo) {
+ APZThreadUtils::AssertOnControllerThread();
+ mInputQueue->AddInputBlockCallback(aInputBlockId, std::move(aCallbackInfo));
+}
+
+void APZCTreeManager::FindScrollThumbNode(
+ const AsyncDragMetrics& aDragMetrics, LayersId aLayersId,
+ HitTestingTreeNodeAutoLock& aOutThumbNode) {
+ if (!aDragMetrics.mDirection) {
+ // The AsyncDragMetrics has not been initialized yet - there will be
+ // no matching node, so don't bother searching the tree.
+ return;
+ }
+
+ RecursiveMutexAutoLock lock(mTreeLock);
+
+ RefPtr<HitTestingTreeNode> result = DepthFirstSearch<ReverseIterator>(
+ mRootNode.get(), [&aDragMetrics, &aLayersId](HitTestingTreeNode* aNode) {
+ return aNode->MatchesScrollDragMetrics(aDragMetrics, aLayersId);
+ });
+ if (result) {
+ aOutThumbNode.Initialize(lock, result.forget(), mTreeLock);
+ }
+}
+
+APZCTreeManager::TargetApzcForNodeResult APZCTreeManager::GetTargetApzcForNode(
+ const HitTestingTreeNode* aNode) {
+ for (const HitTestingTreeNode* n = aNode;
+ n && n->GetLayersId() == aNode->GetLayersId(); n = n->GetParent()) {
+ // For a fixed node, GetApzc() may return an APZC for content in the
+ // enclosing document, so we need to check GetFixedPosTarget() before
+ // GetApzc().
+ if (n->GetFixedPosTarget() != ScrollableLayerGuid::NULL_SCROLL_ID) {
+ RefPtr<AsyncPanZoomController> fpTarget =
+ GetTargetAPZC(n->GetLayersId(), n->GetFixedPosTarget());
+ APZCTM_LOG("Found target APZC %p using fixed-pos lookup on %" PRIu64 "\n",
+ fpTarget.get(), n->GetFixedPosTarget());
+ return {fpTarget.get(), true};
+ }
+ if (n->GetApzc()) {
+ APZCTM_LOG("Found target %p using ancestor lookup\n", n->GetApzc());
+ return {n->GetApzc(), false};
+ }
+ }
+ return {nullptr, false};
+}
+
+HitTestingTreeNode* APZCTreeManager::FindRootNodeForLayersId(
+ LayersId aLayersId) const {
+ mTreeLock.AssertCurrentThreadIn();
+
+ HitTestingTreeNode* resultNode = BreadthFirstSearch<ReverseIterator>(
+ mRootNode.get(), [aLayersId](HitTestingTreeNode* aNode) {
+ AsyncPanZoomController* apzc = aNode->GetApzc();
+ return apzc && apzc->GetLayersId() == aLayersId &&
+ apzc->IsRootForLayersId();
+ });
+ return resultNode;
+}
+
+already_AddRefed<AsyncPanZoomController> APZCTreeManager::FindZoomableApzc(
+ AsyncPanZoomController* aStart) const {
+ return GetZoomableTarget(aStart, aStart);
+}
+
+ScreenMargin APZCTreeManager::GetCompositorFixedLayerMargins() const {
+ RecursiveMutexAutoLock lock(mTreeLock);
+ return mCompositorFixedLayerMargins;
+}
+
+AsyncPanZoomController* APZCTreeManager::FindRootContentApzcForLayersId(
+ LayersId aLayersId) const {
+ mTreeLock.AssertCurrentThreadIn();
+
+ HitTestingTreeNode* resultNode = BreadthFirstSearch<ReverseIterator>(
+ mRootNode.get(), [aLayersId](HitTestingTreeNode* aNode) {
+ AsyncPanZoomController* apzc = aNode->GetApzc();
+ return apzc && apzc->GetLayersId() == aLayersId &&
+ apzc->IsRootContent();
+ });
+ return resultNode ? resultNode->GetApzc() : nullptr;
+}
+
+// clang-format off
+/* The methods GetScreenToApzcTransform() and GetApzcToGeckoTransform() return
+ some useful transformations that input events may need applied. This is best
+ illustrated with an example. Consider a chain of layers, L, M, N, O, P, Q, R. Layer L
+ is the layer that corresponds to the argument |aApzc|, and layer R is the root
+ of the layer tree. Layer M is the parent of L, N is the parent of M, and so on.
+ When layer L is displayed to the screen by the compositor, the set of transforms that
+ are applied to L are (in order from top to bottom):
+
+ L's CSS transform (hereafter referred to as transform matrix LC)
+ L's nontransient async transform (hereafter referred to as transform matrix LN)
+ L's transient async transform (hereafter referred to as transform matrix LT)
+ M's CSS transform (hereafter referred to as transform matrix MC)
+ M's nontransient async transform (hereafter referred to as transform matrix MN)
+ M's transient async transform (hereafter referred to as transform matrix MT)
+ ...
+ R's CSS transform (hereafter referred to as transform matrix RC)
+ R's nontransient async transform (hereafter referred to as transform matrix RN)
+ R's transient async transform (hereafter referred to as transform matrix RT)
+
+ Also, for any layer, the async transform is the combination of its transient and non-transient
+ parts. That is, for any layer L:
+ LA === LN * LT
+ LA.Inverse() === LT.Inverse() * LN.Inverse()
+
+ If we want user input to modify L's transient async transform, we have to first convert
+ user input from screen space to the coordinate space of L's transient async transform. Doing
+ this involves applying the following transforms (in order from top to bottom):
+ RT.Inverse()
+ RN.Inverse()
+ RC.Inverse()
+ ...
+ MT.Inverse()
+ MN.Inverse()
+ MC.Inverse()
+ This combined transformation is returned by GetScreenToApzcTransform().
+
+ Next, if we want user inputs sent to gecko for event-dispatching, we will need to strip
+ out all of the async transforms that are involved in this chain. This is because async
+ transforms are stored only in the compositor and gecko does not account for them when
+ doing display-list-based hit-testing for event dispatching.
+ Furthermore, because these input events are processed by Gecko in a FIFO queue that
+ includes other things (specifically paint requests), it is possible that by time the
+ input event reaches gecko, it will have painted something else. Therefore, we need to
+ apply another transform to the input events to account for the possible disparity between
+ what we know gecko last painted and the last paint request we sent to gecko. Let this
+ transform be represented by LD, MD, ... RD.
+ Therefore, given a user input in screen space, the following transforms need to be applied
+ (in order from top to bottom):
+ RT.Inverse()
+ RN.Inverse()
+ RC.Inverse()
+ ...
+ MT.Inverse()
+ MN.Inverse()
+ MC.Inverse()
+ LT.Inverse()
+ LN.Inverse()
+ LC.Inverse()
+ LC
+ LD
+ MC
+ MD
+ ...
+ RC
+ RD
+ This sequence can be simplified and refactored to the following:
+ GetScreenToApzcTransform()
+ LA.Inverse()
+ LD
+ MC
+ MD
+ ...
+ RC
+ RD
+ Since GetScreenToApzcTransform() can be obtained by calling that function, GetApzcToGeckoTransform()
+ returns the remaining transforms (LA.Inverse() * LD * ... * RD), so that the caller code can
+ combine it with GetScreenToApzcTransform() to get the final transform required in this case.
+
+ Note that for many of these layers, there will be no AsyncPanZoomController attached, and
+ so the async transform will be the identity transform. So, in the example above, if layers
+ L and P have APZC instances attached, MT, MN, MD, NT, NN, ND, OT, ON, OD, QT, QN, QD, RT,
+ RN and RD will be identity transforms.
+ Additionally, for space-saving purposes, each APZC instance stores its layer's individual
+ CSS transform and the accumulation of CSS transforms to its parent APZC. So the APZC for
+ layer L would store LC and (MC * NC * OC), and the layer P would store PC and (QC * RC).
+ The APZC instances track the last dispatched paint request and so are able to calculate LD and
+ PD using those internally stored values.
+ The APZCs also obviously have LT, LN, PT, and PN, so all of the above transformation combinations
+ required can be generated.
+ */
+// clang-format on
+
+/*
+ * See the long comment above for a detailed explanation of this function.
+ */
+ScreenToParentLayerMatrix4x4 APZCTreeManager::GetScreenToApzcTransform(
+ const AsyncPanZoomController* aApzc) const {
+ Matrix4x4 result;
+ RecursiveMutexAutoLock lock(mTreeLock);
+
+ // The comments below assume there is a chain of layers L..R with L and P
+ // having APZC instances as explained in the comment above. This function is
+ // called with aApzc at L, and the loop below performs one iteration, where
+ // parent is at P. The comments explain what values are stored in the
+ // variables at these two levels. All the comments use standard matrix
+ // notation where the leftmost matrix in a multiplication is applied first.
+
+ // ancestorUntransform is PC.Inverse() * OC.Inverse() * NC.Inverse() *
+ // MC.Inverse()
+ Matrix4x4 ancestorUntransform = aApzc->GetAncestorTransform().Inverse();
+
+ // result is initialized to PC.Inverse() * OC.Inverse() * NC.Inverse() *
+ // MC.Inverse()
+ result = ancestorUntransform;
+
+ for (AsyncPanZoomController* parent = aApzc->GetParent(); parent;
+ parent = parent->GetParent()) {
+ // ancestorUntransform is updated to RC.Inverse() * QC.Inverse() when parent
+ // == P
+ ancestorUntransform = parent->GetAncestorTransform().Inverse();
+ // asyncUntransform is updated to PA.Inverse() when parent == P
+ Matrix4x4 asyncUntransform = parent
+ ->GetCurrentAsyncTransformWithOverscroll(
+ AsyncPanZoomController::eForHitTesting)
+ .Inverse()
+ .ToUnknownMatrix();
+ // untransformSinceLastApzc is RC.Inverse() * QC.Inverse() * PA.Inverse()
+ Matrix4x4 untransformSinceLastApzc = ancestorUntransform * asyncUntransform;
+
+ // result is RC.Inverse() * QC.Inverse() * PA.Inverse() * PC.Inverse() *
+ // OC.Inverse() * NC.Inverse() * MC.Inverse()
+ result = untransformSinceLastApzc * result;
+
+ // The above value for result when parent == P matches the required output
+ // as explained in the comment above this method. Note that any missing
+ // terms are guaranteed to be identity transforms.
+ }
+
+ return ViewAs<ScreenToParentLayerMatrix4x4>(result);
+}
+
+/*
+ * See the long comment above GetScreenToApzcTransform() for a detailed
+ * explanation of this function.
+ */
+ParentLayerToScreenMatrix4x4 APZCTreeManager::GetApzcToGeckoTransform(
+ const AsyncPanZoomController* aApzc,
+ const AsyncTransformComponents& aComponents) const {
+ Matrix4x4 result;
+ RecursiveMutexAutoLock lock(mTreeLock);
+
+ // The comments below assume there is a chain of layers L..R with L and P
+ // having APZC instances as explained in the comment above. This function is
+ // called with aApzc at L, and the loop below performs one iteration, where
+ // parent is at P. The comments explain what values are stored in the
+ // variables at these two levels. All the comments use standard matrix
+ // notation where the leftmost matrix in a multiplication is applied first.
+
+ // asyncUntransform is LA.Inverse()
+ Matrix4x4 asyncUntransform =
+ aApzc
+ ->GetCurrentAsyncTransformWithOverscroll(
+ AsyncPanZoomController::eForHitTesting, aComponents)
+ .Inverse()
+ .ToUnknownMatrix();
+
+ // aTransformToGeckoOut is initialized to LA.Inverse() * LD * MC * NC * OC *
+ // PC
+ result = asyncUntransform *
+ aApzc->GetTransformToLastDispatchedPaint(aComponents) *
+ aApzc->GetAncestorTransform();
+
+ for (AsyncPanZoomController* parent = aApzc->GetParent(); parent;
+ parent = parent->GetParent()) {
+ // aTransformToGeckoOut is LA.Inverse() * LD * MC * NC * OC * PC * PD * QC *
+ // RC
+ //
+ // Note: Do not pass the async transform components for the current target
+ // to the parent.
+ result = result *
+ parent->GetTransformToLastDispatchedPaint(LayoutAndVisual) *
+ parent->GetAncestorTransform();
+
+ // The above value for result when parent == P matches the required output
+ // as explained in the comment above this method. Note that any missing
+ // terms are guaranteed to be identity transforms.
+ }
+
+ return ViewAs<ParentLayerToScreenMatrix4x4>(result);
+}
+
+ParentLayerToScreenMatrix4x4 APZCTreeManager::GetApzcToGeckoTransformForHit(
+ HitTestResult& aHitResult) const {
+ // Fixed content is only subject to the visual component of the async
+ // transform.
+ AsyncTransformComponents components =
+ aHitResult.mFixedPosSides == SideBits::eNone
+ ? LayoutAndVisual
+ : AsyncTransformComponents{AsyncTransformComponent::eVisual};
+ return GetApzcToGeckoTransform(aHitResult.mTargetApzc, components);
+}
+
+ScreenPoint APZCTreeManager::GetCurrentMousePosition() const {
+ auto pos = mCurrentMousePosition.Lock();
+ return pos.ref();
+}
+
+void APZCTreeManager::SetCurrentMousePosition(const ScreenPoint& aNewPos) {
+ auto pos = mCurrentMousePosition.Lock();
+ pos.ref() = aNewPos;
+}
+
+static AsyncPanZoomController* GetApzcWithDifferentLayersIdByWalkingParents(
+ AsyncPanZoomController* aApzc) {
+ if (!aApzc) {
+ return nullptr;
+ }
+ AsyncPanZoomController* parent = aApzc->GetParent();
+ while (parent && (parent->GetLayersId() == aApzc->GetLayersId())) {
+ parent = parent->GetParent();
+ }
+ return parent;
+}
+
+already_AddRefed<AsyncPanZoomController> APZCTreeManager::GetZoomableTarget(
+ AsyncPanZoomController* aApzc1, AsyncPanZoomController* aApzc2) const {
+ RecursiveMutexAutoLock lock(mTreeLock);
+ RefPtr<AsyncPanZoomController> apzc;
+ // For now, we only ever want to do pinching on the root-content APZC for
+ // a given layers id.
+ if (aApzc1 && aApzc2 && aApzc1->GetLayersId() == aApzc2->GetLayersId()) {
+ // If the two APZCs have the same layers id, find the root-content APZC
+ // for that layers id. Don't call CommonAncestor() because there may not
+ // be a common ancestor for the layers id (e.g. if one APZCs is inside a
+ // fixed-position element).
+ apzc = FindRootContentApzcForLayersId(aApzc1->GetLayersId());
+ if (apzc) {
+ return apzc.forget();
+ }
+ }
+
+ // Otherwise, find the common ancestor (to reach a common layers id), and then
+ // walk up the apzc tree until we find a root-content APZC.
+ apzc = CommonAncestor(aApzc1, aApzc2);
+ RefPtr<AsyncPanZoomController> zoomable;
+ while (apzc && !zoomable) {
+ zoomable = FindRootContentApzcForLayersId(apzc->GetLayersId());
+ apzc = GetApzcWithDifferentLayersIdByWalkingParents(apzc);
+ }
+
+ return zoomable.forget();
+}
+
+Maybe<ScreenIntPoint> APZCTreeManager::ConvertToGecko(
+ const ScreenIntPoint& aPoint, AsyncPanZoomController* aApzc) {
+ RecursiveMutexAutoLock lock(mTreeLock);
+ // TODO: The current check assumes that a touch gesture and a touchpad tap
+ // gesture can't both be active at the same time. If we turn on double-tap-
+ // to-zoom on a touchscreen platform like Windows or Linux, this assumption
+ // would no longer be valid, and we'd have to instead have TapGestureInput
+ // track and inform this function whether it was created from touch events.
+ const HitTestResult& hit = mInputQueue->GetCurrentTouchBlock()
+ ? mTouchBlockHitResult
+ : mTapGestureHitResult;
+ AsyncTransformComponents components =
+ hit.mFixedPosSides == SideBits::eNone
+ ? LayoutAndVisual
+ : AsyncTransformComponents{AsyncTransformComponent::eVisual};
+ ScreenToScreenMatrix4x4 transformScreenToGecko =
+ GetScreenToApzcTransform(aApzc) *
+ GetApzcToGeckoTransform(aApzc, components);
+ Maybe<ScreenIntPoint> geckoPoint =
+ UntransformBy(transformScreenToGecko, aPoint);
+ if (geckoPoint) {
+ AdjustEventPointForDynamicToolbar(*geckoPoint, hit);
+ }
+ return geckoPoint;
+}
+
+already_AddRefed<AsyncPanZoomController> APZCTreeManager::CommonAncestor(
+ AsyncPanZoomController* aApzc1, AsyncPanZoomController* aApzc2) const {
+ mTreeLock.AssertCurrentThreadIn();
+ RefPtr<AsyncPanZoomController> ancestor;
+
+ // If either aApzc1 or aApzc2 is null, min(depth1, depth2) will be 0 and this
+ // function will return null.
+
+ // Calculate depth of the APZCs in the tree
+ int depth1 = 0, depth2 = 0;
+ for (AsyncPanZoomController* parent = aApzc1; parent;
+ parent = parent->GetParent()) {
+ depth1++;
+ }
+ for (AsyncPanZoomController* parent = aApzc2; parent;
+ parent = parent->GetParent()) {
+ depth2++;
+ }
+
+ // At most one of the following two loops will be executed; the deeper APZC
+ // pointer will get walked up to the depth of the shallower one.
+ int minDepth = depth1 < depth2 ? depth1 : depth2;
+ while (depth1 > minDepth) {
+ depth1--;
+ aApzc1 = aApzc1->GetParent();
+ }
+ while (depth2 > minDepth) {
+ depth2--;
+ aApzc2 = aApzc2->GetParent();
+ }
+
+ // Walk up the ancestor chains of both APZCs, always staying at the same depth
+ // for either APZC, and return the the first common ancestor encountered.
+ while (true) {
+ if (aApzc1 == aApzc2) {
+ ancestor = aApzc1;
+ break;
+ }
+ if (depth1 <= 0) {
+ break;
+ }
+ aApzc1 = aApzc1->GetParent();
+ aApzc2 = aApzc2->GetParent();
+ }
+ return ancestor.forget();
+}
+
+bool APZCTreeManager::IsFixedToRootContent(
+ const HitTestingTreeNode* aNode) const {
+ MutexAutoLock lock(mMapLock);
+ return IsFixedToRootContent(FixedPositionInfo(aNode), lock);
+}
+
+bool APZCTreeManager::IsFixedToRootContent(
+ const FixedPositionInfo& aFixedInfo,
+ const MutexAutoLock& aProofOfMapLock) const {
+ ScrollableLayerGuid::ViewID fixedTarget = aFixedInfo.mFixedPosTarget;
+ if (fixedTarget == ScrollableLayerGuid::NULL_SCROLL_ID) {
+ return false;
+ }
+ auto it =
+ mApzcMap.find(ScrollableLayerGuid(aFixedInfo.mLayersId, 0, fixedTarget));
+ if (it == mApzcMap.end()) {
+ return false;
+ }
+ RefPtr<AsyncPanZoomController> targetApzc = it->second.apzc;
+ return targetApzc && targetApzc->IsRootContent();
+}
+
+SideBits APZCTreeManager::SidesStuckToRootContent(
+ const HitTestingTreeNode* aNode) const {
+ MutexAutoLock lock(mMapLock);
+ return SidesStuckToRootContent(StickyPositionInfo(aNode), lock);
+}
+
+SideBits APZCTreeManager::SidesStuckToRootContent(
+ const StickyPositionInfo& aStickyInfo,
+ const MutexAutoLock& aProofOfMapLock) const {
+ SideBits result = SideBits::eNone;
+
+ ScrollableLayerGuid::ViewID stickyTarget = aStickyInfo.mStickyPosTarget;
+ if (stickyTarget == ScrollableLayerGuid::NULL_SCROLL_ID) {
+ return result;
+ }
+
+ // We support the dynamic toolbar at top and bottom.
+ if ((aStickyInfo.mFixedPosSides & SideBits::eTopBottom) == SideBits::eNone) {
+ return result;
+ }
+
+ auto it = mApzcMap.find(
+ ScrollableLayerGuid(aStickyInfo.mLayersId, 0, stickyTarget));
+ if (it == mApzcMap.end()) {
+ return result;
+ }
+ RefPtr<AsyncPanZoomController> stickyTargetApzc = it->second.apzc;
+ if (!stickyTargetApzc || !stickyTargetApzc->IsRootContent()) {
+ return result;
+ }
+
+ ParentLayerPoint translation =
+ stickyTargetApzc
+ ->GetCurrentAsyncTransform(
+ AsyncPanZoomController::eForHitTesting,
+ AsyncTransformComponents{AsyncTransformComponent::eLayout})
+ .mTranslation;
+
+ if (apz::IsStuckAtTop(translation.y, aStickyInfo.mStickyScrollRangeInner,
+ aStickyInfo.mStickyScrollRangeOuter)) {
+ result |= SideBits::eTop;
+ }
+ if (apz::IsStuckAtBottom(translation.y, aStickyInfo.mStickyScrollRangeInner,
+ aStickyInfo.mStickyScrollRangeOuter)) {
+ result |= SideBits::eBottom;
+ }
+ return result;
+}
+
+LayerToParentLayerMatrix4x4 APZCTreeManager::ComputeTransformForNode(
+ const HitTestingTreeNode* aNode) const {
+ mTreeLock.AssertCurrentThreadIn();
+ // The async transforms applied here for hit-testing purposes, are intended
+ // to match the ones AsyncCompositionManager (or equivalent WebRender code)
+ // applies for rendering purposes.
+ // Note that with containerless scrolling, the layer structure looks like
+ // this:
+ //
+ // root container layer
+ // async zoom container layer
+ // scrollable content layers (with scroll metadata)
+ // fixed content layers (no scroll metadta, annotated isFixedPosition)
+ // scrollbar layers
+ //
+ // The intended async transforms in this case are:
+ // * On the async zoom container layer, the "visual" portion of the root
+ // content APZC's async transform (which includes the zoom, and async
+ // scrolling of the visual viewport relative to the layout viewport).
+ // * On the scrollable layers bearing the root content APZC's scroll
+ // metadata, the "layout" portion of the root content APZC's async
+ // transform (which includes async scrolling of the layout viewport
+ // relative to the scrollable rect origin).
+ if (AsyncPanZoomController* apzc = aNode->GetApzc()) {
+ // If the node represents scrollable content, apply the async transform
+ // from its APZC.
+ bool visualTransformIsInheritedFromAncestor =
+ /* we're the APZC whose visual transform might be on the async
+ zoom container */
+ apzc->IsRootContent() &&
+ /* there is an async zoom container on this subtree */
+ mAsyncZoomContainerSubtree == Some(aNode->GetLayersId()) &&
+ /* it's not us */
+ !aNode->GetAsyncZoomContainerId();
+ AsyncTransformComponents components =
+ visualTransformIsInheritedFromAncestor
+ ? AsyncTransformComponents{AsyncTransformComponent::eLayout}
+ : LayoutAndVisual;
+ return aNode->GetTransform() *
+ CompleteAsyncTransform(apzc->GetCurrentAsyncTransformWithOverscroll(
+ AsyncPanZoomController::eForHitTesting, components));
+ } else if (aNode->GetAsyncZoomContainerId()) {
+ if (AsyncPanZoomController* rootContent =
+ FindRootContentApzcForLayersId(aNode->GetLayersId())) {
+ return aNode->GetTransform() *
+ CompleteAsyncTransform(
+ rootContent->GetCurrentAsyncTransformWithOverscroll(
+ AsyncPanZoomController::eForHitTesting,
+ {AsyncTransformComponent::eVisual}));
+ }
+ } else if (aNode->IsScrollThumbNode()) {
+ // If the node represents a scrollbar thumb, compute and apply the
+ // transformation that will be applied to the thumb in
+ // AsyncCompositionManager.
+ ScrollableLayerGuid guid{aNode->GetLayersId(), 0,
+ aNode->GetScrollTargetId()};
+ if (RefPtr<HitTestingTreeNode> scrollTargetNode = GetTargetNode(
+ guid, &ScrollableLayerGuid::EqualsIgnoringPresShell)) {
+ AsyncPanZoomController* scrollTargetApzc = scrollTargetNode->GetApzc();
+ MOZ_ASSERT(scrollTargetApzc);
+ return scrollTargetApzc->CallWithLastContentPaintMetrics(
+ [&](const FrameMetrics& aMetrics) {
+ return ComputeTransformForScrollThumb(
+ aNode->GetTransform() * AsyncTransformMatrix(),
+ scrollTargetNode->GetTransform().ToUnknownMatrix(),
+ scrollTargetApzc, aMetrics, aNode->GetScrollbarData(),
+ scrollTargetNode->IsAncestorOf(aNode));
+ });
+ }
+ } else if (IsFixedToRootContent(aNode)) {
+ ParentLayerPoint translation;
+ {
+ MutexAutoLock mapLock(mMapLock);
+ translation = ViewAs<ParentLayerPixel>(
+ apz::ComputeFixedMarginsOffset(
+ GetCompositorFixedLayerMargins(mapLock),
+ aNode->GetFixedPosSides(), mGeckoFixedLayerMargins),
+ PixelCastJustification::ScreenIsParentLayerForRoot);
+ }
+ return aNode->GetTransform() *
+ CompleteAsyncTransform(
+ AsyncTransformComponentMatrix::Translation(translation));
+ }
+ SideBits sides = SidesStuckToRootContent(aNode);
+ if (sides != SideBits::eNone) {
+ ParentLayerPoint translation;
+ {
+ MutexAutoLock mapLock(mMapLock);
+ translation = ViewAs<ParentLayerPixel>(
+ apz::ComputeFixedMarginsOffset(
+ GetCompositorFixedLayerMargins(mapLock), sides,
+ // For sticky layers, we don't need to factor
+ // mGeckoFixedLayerMargins because Gecko doesn't shift the
+ // position of sticky elements for dynamic toolbar movements.
+ ScreenMargin()),
+ PixelCastJustification::ScreenIsParentLayerForRoot);
+ }
+ return aNode->GetTransform() *
+ CompleteAsyncTransform(
+ AsyncTransformComponentMatrix::Translation(translation));
+ }
+ // Otherwise, the node does not have an async transform.
+ return aNode->GetTransform() * AsyncTransformMatrix();
+}
+
+already_AddRefed<wr::WebRenderAPI> APZCTreeManager::GetWebRenderAPI() const {
+ RefPtr<wr::WebRenderAPI> api;
+ CompositorBridgeParent::CallWithIndirectShadowTree(
+ mRootLayersId, [&](LayerTreeState& aState) -> void {
+ if (aState.mWrBridge) {
+ api = aState.mWrBridge->GetWebRenderAPI();
+ }
+ });
+ return api.forget();
+}
+
+/*static*/
+already_AddRefed<GeckoContentController> APZCTreeManager::GetContentController(
+ LayersId aLayersId) {
+ RefPtr<GeckoContentController> controller;
+ CompositorBridgeParent::CallWithIndirectShadowTree(
+ aLayersId,
+ [&](LayerTreeState& aState) -> void { controller = aState.mController; });
+ return controller.forget();
+}
+
+ScreenMargin APZCTreeManager::GetCompositorFixedLayerMargins(
+ const MutexAutoLock& aProofOfMapLock) const {
+ ScreenMargin result = mCompositorFixedLayerMargins;
+ if (StaticPrefs::apz_fixed_margin_override_enabled()) {
+ result.top = StaticPrefs::apz_fixed_margin_override_top();
+ result.bottom = StaticPrefs::apz_fixed_margin_override_bottom();
+ }
+ return result;
+}
+
+bool APZCTreeManager::GetAPZTestData(LayersId aLayersId,
+ APZTestData* aOutData) {
+ AssertOnUpdaterThread();
+
+ { // copy the relevant test data into aOutData while holding the
+ // mTestDataLock
+ MutexAutoLock lock(mTestDataLock);
+ auto it = mTestData.find(aLayersId);
+ if (it == mTestData.end()) {
+ return false;
+ }
+ *aOutData = *(it->second);
+ }
+
+ { // add some additional "current state" into the returned APZTestData
+ MutexAutoLock mapLock(mMapLock);
+
+ ClippedCompositionBoundsMap clippedCompBounds;
+ for (const auto& mapping : mApzcMap) {
+ if (mapping.first.mLayersId != aLayersId) {
+ continue;
+ }
+
+ ParentLayerRect clippedBounds = ComputeClippedCompositionBounds(
+ mapLock, clippedCompBounds, mapping.first);
+ AsyncPanZoomController* apzc = mapping.second.apzc;
+ std::string viewId = std::to_string(mapping.first.mScrollId);
+ std::string apzcState;
+ if (apzc->GetCheckerboardMagnitude(clippedBounds)) {
+ apzcState += "checkerboarding,";
+ }
+ if (apzc->IsOverscrolled()) {
+ apzcState += "overscrolled,";
+ }
+ aOutData->RecordAdditionalData(viewId, apzcState);
+ }
+ }
+ return true;
+}
+
+void APZCTreeManager::SendSubtreeTransformsToChromeMainThread(
+ const AsyncPanZoomController* aAncestor) {
+ RefPtr<GeckoContentController> controller =
+ GetContentController(mRootLayersId);
+ if (!controller) {
+ return;
+ }
+ nsTArray<MatrixMessage> messages;
+ bool underAncestor = (aAncestor == nullptr);
+ bool shouldNotify = false;
+ {
+ RecursiveMutexAutoLock lock(mTreeLock);
+ if (!mRootNode) {
+ // Event dispatched during shutdown, after ClearTree().
+ // Note, mRootNode needs to be checked with mTreeLock held.
+ return;
+ }
+ // This formulation duplicates matrix multiplications closer
+ // to the root of the tree. For now, aiming for separation
+ // of concerns rather than minimum number of multiplications.
+ ForEachNode<ReverseIterator>(
+ mRootNode.get(),
+ [&](HitTestingTreeNode* aNode) {
+ mTreeLock.AssertCurrentThreadIn();
+ bool atAncestor = (aAncestor && aNode->GetApzc() == aAncestor);
+ MOZ_ASSERT(!(underAncestor && atAncestor));
+ underAncestor |= atAncestor;
+ if (!underAncestor) {
+ return;
+ }
+ LayersId layersId = aNode->GetLayersId();
+ HitTestingTreeNode* parent = aNode->GetParent();
+ if (!parent) {
+ messages.AppendElement(MatrixMessage(Some(LayerToScreenMatrix4x4()),
+ ScreenRect(), layersId));
+ } else if (layersId != parent->GetLayersId()) {
+ if (mDetachedLayersIds.find(layersId) != mDetachedLayersIds.end()) {
+ messages.AppendElement(
+ MatrixMessage(Nothing(), ScreenRect(), layersId));
+ } else {
+ messages.AppendElement(MatrixMessage(
+ Some(parent->GetTransformToGecko()),
+ parent->GetRemoteDocumentScreenRect(), layersId));
+ }
+ }
+ },
+ [&](HitTestingTreeNode* aNode) {
+ bool atAncestor = (aAncestor && aNode->GetApzc() == aAncestor);
+ if (atAncestor) {
+ MOZ_ASSERT(underAncestor);
+ underAncestor = false;
+ }
+ });
+ if (messages != mLastMessages) {
+ mLastMessages = messages;
+ shouldNotify = true;
+ }
+ }
+ if (shouldNotify) {
+ controller->NotifyLayerTransforms(std::move(messages));
+ }
+}
+
+void APZCTreeManager::SetFixedLayerMargins(ScreenIntCoord aTop,
+ ScreenIntCoord aBottom) {
+ MutexAutoLock lock(mMapLock);
+ mCompositorFixedLayerMargins.top = aTop;
+ mCompositorFixedLayerMargins.bottom = aBottom;
+}
+
+/*static*/
+LayerToParentLayerMatrix4x4 APZCTreeManager::ComputeTransformForScrollThumb(
+ const LayerToParentLayerMatrix4x4& aCurrentTransform,
+ const Matrix4x4& aScrollableContentTransform, AsyncPanZoomController* aApzc,
+ const FrameMetrics& aMetrics, const ScrollbarData& aScrollbarData,
+ bool aScrollbarIsDescendant) {
+ return apz::ComputeTransformForScrollThumb(
+ aCurrentTransform, aScrollableContentTransform, aApzc, aMetrics,
+ aScrollbarData, aScrollbarIsDescendant);
+}
+
+APZSampler* APZCTreeManager::GetSampler() const {
+ // We should always have a sampler here, since in practice the sampler
+ // is destroyed at the same time that this APZCTreeMAnager instance is.
+ MOZ_ASSERT(mSampler);
+ return mSampler;
+}
+
+void APZCTreeManager::AssertOnSamplerThread() {
+ GetSampler()->AssertOnSamplerThread();
+}
+
+APZUpdater* APZCTreeManager::GetUpdater() const {
+ // We should always have an updater here, since in practice the updater
+ // is destroyed at the same time that this APZCTreeManager instance is.
+ MOZ_ASSERT(mUpdater);
+ return mUpdater;
+}
+
+void APZCTreeManager::AssertOnUpdaterThread() {
+ GetUpdater()->AssertOnUpdaterThread();
+}
+
+MOZ_PUSH_IGNORE_THREAD_SAFETY
+void APZCTreeManager::LockTree() {
+ AssertOnUpdaterThread();
+ mTreeLock.Lock();
+}
+
+void APZCTreeManager::UnlockTree() {
+ AssertOnUpdaterThread();
+ mTreeLock.Unlock();
+}
+MOZ_POP_THREAD_SAFETY
+
+void APZCTreeManager::SetDPI(float aDpiValue) {
+ if (!APZThreadUtils::IsControllerThread()) {
+ APZThreadUtils::RunOnControllerThread(
+ NewRunnableMethod<float>("layers::APZCTreeManager::SetDPI", this,
+ &APZCTreeManager::SetDPI, aDpiValue));
+ return;
+ }
+
+ APZThreadUtils::AssertOnControllerThread();
+ mDPI = aDpiValue;
+}
+
+float APZCTreeManager::GetDPI() const {
+ APZThreadUtils::AssertOnControllerThread();
+ return mDPI;
+}
+
+APZCTreeManager::FixedPositionInfo::FixedPositionInfo(
+ const HitTestingTreeNode* aNode) {
+ mFixedPositionAnimationId = aNode->GetFixedPositionAnimationId();
+ mFixedPosSides = aNode->GetFixedPosSides();
+ mFixedPosTarget = aNode->GetFixedPosTarget();
+ mLayersId = aNode->GetLayersId();
+}
+
+APZCTreeManager::StickyPositionInfo::StickyPositionInfo(
+ const HitTestingTreeNode* aNode) {
+ mStickyPositionAnimationId = aNode->GetStickyPositionAnimationId();
+ mFixedPosSides = aNode->GetFixedPosSides();
+ mStickyPosTarget = aNode->GetStickyPosTarget();
+ mLayersId = aNode->GetLayersId();
+ mStickyScrollRangeInner = aNode->GetStickyScrollRangeInner();
+ mStickyScrollRangeOuter = aNode->GetStickyScrollRangeOuter();
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/APZCTreeManager.h b/gfx/layers/apz/src/APZCTreeManager.h
new file mode 100644
index 0000000000..fb0e98e350
--- /dev/null
+++ b/gfx/layers/apz/src/APZCTreeManager.h
@@ -0,0 +1,1064 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_APZCTreeManager_h
+#define mozilla_layers_APZCTreeManager_h
+
+#include <unordered_map> // for std::unordered_map
+
+#include "FocusState.h" // for FocusState
+#include "HitTestingTreeNode.h" // for HitTestingTreeNodeAutoLock
+#include "IAPZHitTester.h" // for IAPZHitTester::HitTestResult
+#include "gfxPoint.h" // for gfxPoint
+#include "mozilla/Assertions.h" // for MOZ_ASSERT_HELPER2
+#include "mozilla/DataMutex.h" // for DataMutex
+#include "mozilla/gfx/CompositorHitTestInfo.h"
+#include "mozilla/gfx/Logging.h" // for gfx::TreeLog
+#include "mozilla/gfx/Matrix.h" // for Matrix4x4
+#include "mozilla/layers/APZInputBridge.h" // for APZInputBridge
+#include "mozilla/layers/APZTestData.h" // for APZTestData
+#include "mozilla/layers/APZUtils.h" // for GeckoViewMetrics
+#include "mozilla/layers/IAPZCTreeManager.h" // for IAPZCTreeManager
+#include "mozilla/layers/ScrollbarData.h"
+#include "mozilla/layers/LayersTypes.h"
+#include "mozilla/layers/KeyboardMap.h" // for KeyboardMap
+#include "mozilla/layers/TouchCounter.h" // for TouchCounter
+#include "mozilla/layers/ZoomConstraints.h" // for ZoomConstraints
+#include "mozilla/webrender/webrender_ffi.h"
+#include "mozilla/RecursiveMutex.h" // for RecursiveMutex
+#include "mozilla/RefPtr.h" // for RefPtr
+#include "mozilla/TimeStamp.h" // for mozilla::TimeStamp
+#include "mozilla/UniquePtr.h" // for UniquePtr
+#include "nsCOMPtr.h" // for already_AddRefed
+#include "nsTArray.h"
+
+namespace mozilla {
+class MultiTouchInput;
+
+namespace wr {
+class TransactionWrapper;
+class WebRenderAPI;
+} // namespace wr
+
+namespace layers {
+
+class Layer;
+class AsyncPanZoomController;
+class APZCTreeManagerParent;
+class APZSampler;
+class APZUpdater;
+class CompositorBridgeParent;
+class OverscrollHandoffChain;
+struct OverscrollHandoffState;
+class FocusTarget;
+struct FlingHandoffState;
+class InputQueue;
+struct InputBlockCallbackInfo;
+class GeckoContentController;
+class HitTestingTreeNode;
+class SampleTime;
+class WebRenderScrollDataWrapper;
+struct AncestorTransform;
+struct ScrollThumbData;
+struct ZoomTarget;
+
+/**
+ * ****************** NOTE ON LOCK ORDERING IN APZ **************************
+ *
+ * To avoid deadlock, APZ imposes and respects a global ordering on threads
+ * and locks relevant to APZ.
+ *
+ * Please see the "Threading / Locking Overview" section of
+ * gfx/docs/AsyncPanZoom.rst (hosted in rendered form at
+ * https://firefox-source-docs.mozilla.org/gfx/gfx/AsyncPanZoom.html#threading-locking-overview)
+ * for what the ordering is, and what are the rules for respecting it.
+ * **************************************************************************
+ */
+
+/**
+ * This class manages the tree of AsyncPanZoomController instances. There is one
+ * instance of this class owned by each CompositorBridgeParent, and it contains
+ * as many AsyncPanZoomController instances as there are scrollable container
+ * layers. This class generally lives on the updater thread, although some
+ * functions may be called from other threads as noted; thread safety is ensured
+ * internally.
+ *
+ * The bulk of the work of this class happens as part of the
+ * UpdateHitTestingTree function, which is when a layer tree update is received
+ * by the compositor. This function walks through the layer tree and creates a
+ * tree of HitTestingTreeNode instances to match the layer tree and for use in
+ * hit-testing on the controller thread. APZC instances may be preserved across
+ * calls to this function if the corresponding layers are still present in the
+ * layer tree.
+ *
+ * The other functions on this class are used by various pieces of client code
+ * to notify the APZC instances of events relevant to them. This includes, for
+ * example, user input events that drive panning and zooming, changes to the
+ * scroll viewport area, and changes to pan/zoom constraints.
+ *
+ * Note that the ClearTree function MUST be called when this class is no longer
+ * needed; see the method documentation for details.
+ *
+ * Behaviour of APZ is controlled by a number of preferences shown
+ * \ref APZCPrefs "here".
+ */
+class APZCTreeManager : public IAPZCTreeManager, public APZInputBridge {
+ typedef mozilla::layers::AllowedTouchBehavior AllowedTouchBehavior;
+ typedef mozilla::layers::AsyncDragMetrics AsyncDragMetrics;
+ using HitTestResult = IAPZHitTester::HitTestResult;
+
+ /**
+ * A result from APZCTreeManager::FindHandoffParent.
+ */
+ struct TargetApzcForNodeResult {
+ // The APZC to handoff overscroll to.
+ AsyncPanZoomController* mApzc;
+ // Targeting a document's root APZC from content fixed to the document.
+ bool mIsFixed;
+ };
+
+ // Helper struct to hold some state while we build the hit-testing tree. The
+ // sole purpose of this struct is to shorten the argument list to
+ // UpdateHitTestingTree. All the state that we don't need to
+ // push on the stack during recursion and pop on unwind is stored here.
+ struct TreeBuildingState;
+
+ public:
+ explicit APZCTreeManager(LayersId aRootLayersId,
+ UniquePtr<IAPZHitTester> aHitTester = nullptr);
+
+ static mozilla::LazyLogModule sLog;
+
+ void SetSampler(APZSampler* aSampler);
+ void SetUpdater(APZUpdater* aUpdater);
+
+ /**
+ * Notifies this APZCTreeManager that the associated compositor is now
+ * responsible for managing another layers id, which got moved over from
+ * some other compositor. That other compositor's APZCTreeManager is also
+ * provided. This allows APZCTreeManager to transfer any necessary state
+ * from the old APZCTreeManager related to that layers id.
+ * This function must be called on the updater thread.
+ */
+ void NotifyLayerTreeAdopted(LayersId aLayersId,
+ const RefPtr<APZCTreeManager>& aOldTreeManager);
+
+ /**
+ * Notifies this APZCTreeManager that a layer tree being managed by the
+ * associated compositor has been removed/destroyed. Note that this does
+ * NOT get called during shutdown situations, when the root layer tree is
+ * also getting destroyed.
+ * This function must be called on the updater thread.
+ */
+ void NotifyLayerTreeRemoved(LayersId aLayersId);
+
+ /**
+ * Rebuild the focus state based on the focus target from the layer tree
+ * update that just occurred. This must be called on the updater thread.
+ *
+ * @param aRootLayerTreeId The layer tree ID of the root layer corresponding
+ * to this APZCTreeManager
+ * @param aOriginatingLayersId The layer tree ID of the layer corresponding to
+ * this layer tree update.
+ */
+ void UpdateFocusState(LayersId aRootLayerTreeId,
+ LayersId aOriginatingLayersId,
+ const FocusTarget& aFocusTarget);
+
+ /**
+ * Rebuild the hit-testing tree based on an incoming WebRender transaction.
+ * Preserve nodes and APZC instances where possible, but retire those whose
+ * layers are no longer in the layer tree.
+ * (Note: "layer tree" here refers to the tree of WebRenderLayerScrollData
+ * nodes sent as part of a WebRender transaction.)
+ *
+ * This must be called on the updater thread.
+ *
+ * @param aRoot The root of the (full) layer tree
+ * @param aOriginatingLayersId The layers id of the subtree that triggered
+ * this repaint, and to which aIsFirstPaint
+ * applies.
+ * @param aIsFirstPaint True if the transaction that this is called in
+ * response to included a first-paint. If this is true,
+ * the part of the tree that is affected by the
+ * first-paint flag is indicated by the
+ * aOriginatingLayersId parameter.
+ * @param aPaintSequenceNumber The sequence number of the paint that triggered
+ * this layer update. Note that every child
+ * process' layer subtree has its own sequence
+ * numbers.
+ */
+ void UpdateHitTestingTree(const WebRenderScrollDataWrapper& aRoot,
+ bool aIsFirstPaint, LayersId aOriginatingLayersId,
+ uint32_t aPaintSequenceNumber);
+
+ /**
+ * Called when webrender is enabled, from the sampler thread. This function
+ * populates the provided transaction with any async scroll offsets needed.
+ * It also advances APZ animations to the specified sample time, and requests
+ * another composite if there are still active animations.
+ * In effect it is the webrender equivalent of (part of) the code in
+ * AsyncCompositionManager.
+ */
+ void SampleForWebRender(const Maybe<VsyncId>& aVsyncId,
+ wr::TransactionWrapper& aTxn,
+ const SampleTime& aSampleTime);
+
+ /**
+ * Refer to the documentation of APZInputBridge::ReceiveInputEvent() and
+ * APZEventResult.
+ */
+ APZEventResult ReceiveInputEvent(
+ InputData& aEvent,
+ InputBlockCallback&& aCallback = InputBlockCallback()) override;
+
+ /**
+ * Set the keyboard shortcuts to use for translating keyboard events.
+ */
+ void SetKeyboardMap(const KeyboardMap& aKeyboardMap) override;
+
+ /**
+ * Kicks an animation to zoom to a rect. This may be either a zoom out or zoom
+ * in. The actual animation is done on the sampler thread after being set
+ * up. |aRect| must be given in CSS pixels, relative to the document.
+ * |aFlags| is a combination of the ZoomToRectBehavior enum values.
+ */
+ void ZoomToRect(const ScrollableLayerGuid& aGuid,
+ const ZoomTarget& aZoomTarget,
+ const uint32_t aFlags = DEFAULT_BEHAVIOR) override;
+
+ /**
+ * If we have touch listeners, this should always be called when we know
+ * definitively whether or not content has preventDefaulted any touch events
+ * that have come in. If |aPreventDefault| is true, any touch events in the
+ * queue will be discarded. This function must be called on the controller
+ * thread.
+ */
+ void ContentReceivedInputBlock(uint64_t aInputBlockId,
+ bool aPreventDefault) override;
+
+ /**
+ * When the event regions code is enabled, this function should be invoked to
+ * to confirm the target of the input block. This is only needed in cases
+ * where the initial input event of the block hit a dispatch-to-content region
+ * but is safe to call for all input blocks.
+ * The different elements in the array of targets correspond to the targets
+ * for the different touch points. In the case where the touch point has no
+ * target, or the target is not a scrollable frame, the target's |mScrollId|
+ * should be set to ScrollableLayerGuid::NULL_SCROLL_ID.
+ * Note: For mouse events that start a scrollbar drag, both SetTargetAPZC()
+ * and StartScrollbarDrag() will be called, and the calls may happen
+ * in either order. That's fine - whichever arrives first will confirm
+ * the block, and StartScrollbarDrag() will fill in the drag metrics.
+ * If the block is confirmed before we have drag metrics, some events
+ * in the drag block may be handled as no-ops until the drag metrics
+ * arrive.
+ */
+ void SetTargetAPZC(uint64_t aInputBlockId,
+ const nsTArray<ScrollableLayerGuid>& aTargets) override;
+
+ /**
+ * Updates any zoom constraints contained in the <meta name="viewport"> tag.
+ * If the |aConstraints| is Nothing() then previously-provided constraints for
+ * the given |aGuid| are cleared.
+ */
+ void UpdateZoomConstraints(
+ const ScrollableLayerGuid& aGuid,
+ const Maybe<ZoomConstraints>& aConstraints) override;
+
+ /**
+ * Calls Destroy() on all APZC instances attached to the tree, and resets the
+ * tree back to empty. This function must be called exactly once during the
+ * lifetime of this APZCTreeManager, when this APZCTreeManager is no longer
+ * needed. Failing to call this function may prevent objects from being freed
+ * properly.
+ * This must be called on the updater thread.
+ */
+ void ClearTree();
+
+ /**
+ * Sets the dpi value used by all AsyncPanZoomControllers attached to this
+ * tree manager.
+ * DPI defaults to 160 if not set using SetDPI() at any point.
+ */
+ void SetDPI(float aDpiValue) override;
+
+ /**
+ * Returns the current dpi value in use.
+ */
+ float GetDPI() const;
+
+ /**
+ * Find the hit testing node for the scrollbar thumb that matches these
+ * drag metrics. Initializes aOutThumbNode with the node, if there is one.
+ */
+ void FindScrollThumbNode(const AsyncDragMetrics& aDragMetrics,
+ LayersId aLayersId,
+ HitTestingTreeNodeAutoLock& aOutThumbNode);
+
+ /**
+ * Sets allowed touch behavior values for current touch-session for specific
+ * input block (determined by aInputBlock).
+ * Should be invoked by the widget. Each value of the aValues arrays
+ * corresponds to the different touch point that is currently active.
+ * Must be called after receiving the TOUCH_START event that starts the
+ * touch-session.
+ */
+ void SetAllowedTouchBehavior(
+ uint64_t aInputBlockId,
+ const nsTArray<TouchBehaviorFlags>& aValues) override;
+
+ void SetBrowserGestureResponse(uint64_t aInputBlockId,
+ BrowserGestureResponse aResponse) override;
+
+ /**
+ * This is a callback for AsyncPanZoomController to call when it wants to
+ * scroll in response to a touch-move event, or when it needs to hand off
+ * overscroll to the next APZC. Note that because of scroll grabbing, the
+ * first APZC to scroll may not be the one that is receiving the touch events.
+ *
+ * |aPrev| is the APZC that received the touch events triggering the scroll
+ * (in the case of an initial scroll), or the last APZC to scroll (in the
+ * case of overscroll)
+ * |aStartPoint| and |aEndPoint| are in |aPrev|'s transformed screen
+ * coordinates (i.e. the same coordinates in which touch points are given to
+ * APZCs). The amount of (over)scroll is represented by two points rather
+ * than a displacement because with certain 3D transforms, the same
+ * displacement between different points in transformed coordinates can
+ * represent different displacements in untransformed coordinates.
+ * |aOverscrollHandoffChain| is the overscroll handoff chain used for
+ * determining the order in which scroll should be handed off between
+ * APZCs
+ * |aOverscrollHandoffChainIndex| is the next position in the overscroll
+ * handoff chain that should be scrolled.
+ *
+ * aStartPoint and aEndPoint will be modified depending on how much of the
+ * scroll each APZC consumes. This is to allow the sending APZC to go into
+ * an overscrolled state if no APZC further up in the handoff chain accepted
+ * the entire scroll.
+ *
+ * The function will return true if the entire scroll was consumed, and
+ * false otherwise. As this function also modifies aStartPoint and aEndPoint,
+ * when scroll is consumed, it should always the case that this function
+ * returns true if and only if IsZero(aStartPoint - aEndPoint), using the
+ * modified aStartPoint and aEndPoint after the function returns.
+ *
+ * The way this method works is best illustrated with an example.
+ * Consider three nested APZCs, A, B, and C, with C being the innermost one.
+ * Say B is scroll-grabbing.
+ * The touch events go to C because it's the innermost one (so e.g. taps
+ * should go through C), but the overscroll handoff chain is B -> C -> A
+ * because B is scroll-grabbing.
+ * For convenience I'll refer to the three APZC objects as A, B, and C, and
+ * to the tree manager object as TM.
+ * Here's what happens when C receives a touch-move event:
+ * - C.TrackTouch() calls TM.DispatchScroll() with index = 0.
+ * - TM.DispatchScroll() calls B.AttemptScroll() (since B is at index 0 in
+ * the chain).
+ * - B.AttemptScroll() scrolls B. If there is overscroll, it calls
+ * TM.DispatchScroll() with index = 1.
+ * - TM.DispatchScroll() calls C.AttemptScroll() (since C is at index 1 in
+ * the chain)
+ * - C.AttemptScroll() scrolls C. If there is overscroll, it calls
+ * TM.DispatchScroll() with index = 2.
+ * - TM.DispatchScroll() calls A.AttemptScroll() (since A is at index 2 in
+ * the chain)
+ * - A.AttemptScroll() scrolls A. If there is overscroll, it calls
+ * TM.DispatchScroll() with index = 3.
+ * - TM.DispatchScroll() discards the rest of the scroll as there are no
+ * more elements in the chain.
+ *
+ * Note: this should be used for panning only. For handing off overscroll for
+ * a fling, use DispatchFling().
+ */
+ bool DispatchScroll(AsyncPanZoomController* aPrev,
+ ParentLayerPoint& aStartPoint,
+ ParentLayerPoint& aEndPoint,
+ OverscrollHandoffState& aOverscrollHandoffState);
+
+ /**
+ * This is a callback for AsyncPanZoomController to call when it wants to
+ * start a fling in response to a touch-end event, or when it needs to hand
+ * off a fling to the next APZC. Note that because of scroll grabbing, the
+ * first APZC to fling may not be the one that is receiving the touch events.
+ *
+ * @param aApzc the APZC that wants to start or hand off the fling
+ * @param aHandoffState a collection of state about the operation,
+ * which contains the following:
+ *
+ * mVelocity the current velocity of the fling, in |aApzc|'s screen
+ * pixels per millisecond
+ * mChain the chain of APZCs along which the fling
+ * should be handed off
+ * mIsHandoff is true if |aApzc| is handing off an existing fling (in
+ * this case the fling is given to the next APZC in the
+ * handoff chain after |aApzc|), and false is |aApzc| wants
+ * start a fling (in this case the fling is given to the
+ * first APZC in the chain)
+ *
+ * The return value is the "residual velocity", the portion of
+ * |aHandoffState.mVelocity| that was not consumed by APZCs in the
+ * handoff chain doing flings.
+ * The caller can use this value to determine whether it should consume
+ * the excess velocity by going into overscroll.
+ */
+ ParentLayerPoint DispatchFling(AsyncPanZoomController* aApzc,
+ const FlingHandoffState& aHandoffState);
+
+ void StartScrollbarDrag(const ScrollableLayerGuid& aGuid,
+ const AsyncDragMetrics& aDragMetrics) override;
+
+ bool StartAutoscroll(const ScrollableLayerGuid& aGuid,
+ const ScreenPoint& aAnchorLocation) override;
+
+ void StopAutoscroll(const ScrollableLayerGuid& aGuid) override;
+
+ /*
+ * Build the chain of APZCs that will handle overscroll for a pan starting at
+ * |aInitialTarget|.
+ */
+ RefPtr<const OverscrollHandoffChain> BuildOverscrollHandoffChain(
+ const RefPtr<AsyncPanZoomController>& aInitialTarget);
+
+ /**
+ * Function used to disable LongTap gestures.
+ *
+ * On slow running tests, drags and touch events can be misinterpreted
+ * as a long tap. This allows tests to disable long tap gesture detection.
+ */
+ void SetLongTapEnabled(bool aTapGestureEnabled) override;
+
+ APZInputBridge* InputBridge() override { return this; }
+
+ /**
+ * Add a callback to be invoked when |aInputBlockId| is ready for handling.
+ *
+ * Should only be used for input blocks that are not yet ready for handling
+ * at the time this is called. If the input block was already handled,
+ * the callback will never be called.
+ *
+ * Only one callback can be registered for an input block at a time.
+ * Subsequent attempts to register a callback for an input block will be
+ * ignored until the existing callback is triggered.
+ */
+ void AddInputBlockCallback(uint64_t aInputBlockId,
+ InputBlockCallbackInfo&& aCallbackInfo);
+
+ // Methods to help process WidgetInputEvents (or manage conversion to/from
+ // InputData)
+
+ void ProcessUnhandledEvent(LayoutDeviceIntPoint* aRefPoint,
+ ScrollableLayerGuid* aOutTargetGuid,
+ uint64_t* aOutFocusSequenceNumber,
+ LayersId* aOutLayersId) override;
+
+ void UpdateWheelTransaction(
+ LayoutDeviceIntPoint aRefPoint, EventMessage aEventMessage,
+ const Maybe<ScrollableLayerGuid>& aTargetGuid) override;
+
+ bool GetAPZTestData(LayersId aLayersId, APZTestData* aOutData);
+
+ /**
+ * Iterates over the hit testing tree, collects LayersIds and associated
+ * transforms from layer coordinate space to root coordinate space, and
+ * sends these over to the main thread of the chrome process. If the provided
+ * |aAncestor| argument is non-null, then only the transforms for layer
+ * subtrees scrolled by the aAncestor (i.e. descendants of aAncestor) will be
+ * sent.
+ */
+ void SendSubtreeTransformsToChromeMainThread(
+ const AsyncPanZoomController* aAncestor);
+
+ /**
+ * Set fixed layer margins for dynamic toolbar.
+ */
+ void SetFixedLayerMargins(ScreenIntCoord aTop, ScreenIntCoord aBottom);
+
+ /**
+ * Refer to apz::ComputeTransformForScrollThumb() for a description
+ * of the parameters.
+ */
+ static LayerToParentLayerMatrix4x4 ComputeTransformForScrollThumb(
+ const LayerToParentLayerMatrix4x4& aCurrentTransform,
+ const gfx::Matrix4x4& aScrollableContentTransform,
+ AsyncPanZoomController* aApzc, const FrameMetrics& aMetrics,
+ const ScrollbarData& aScrollbarData, bool aScrollbarIsDescendant);
+
+ /**
+ * Dispatch a flush complete notification from the repaint thread of the
+ * content controller for the given layers id.
+ */
+ static void FlushApzRepaints(LayersId aLayersId);
+
+ /**
+ * Mark |aLayersId| as having been moved from the compositor that owns this
+ * tree manager to a compositor that doesn't use APZ.
+ * See |mDetachedLayersIds| for more details.
+ */
+ void MarkAsDetached(LayersId aLayersId);
+
+ // Assert that the current thread is the sampler thread for this APZCTM.
+ void AssertOnSamplerThread();
+ // Assert that the current thread is the updater thread for this APZCTM.
+ void AssertOnUpdaterThread();
+
+ // Returns a pointer to the WebRenderAPI this APZCTreeManager is for.
+ // This might be null (for example, if WebRender is not enabled).
+ already_AddRefed<wr::WebRenderAPI> GetWebRenderAPI() const;
+
+ protected:
+ // Protected destructor, to discourage deletion outside of Release():
+ virtual ~APZCTreeManager();
+
+ APZSampler* GetSampler() const;
+ APZUpdater* GetUpdater() const;
+
+ // We need to allow APZUpdater to lock and unlock this tree during a WR
+ // scene swap. We do this using private helpers to avoid exposing these
+ // functions to the world.
+ private:
+ friend class APZUpdater;
+ void LockTree() MOZ_CAPABILITY_ACQUIRE(mTreeLock);
+ void UnlockTree() MOZ_CAPABILITY_RELEASE(mTreeLock);
+
+ // Protected hooks for gtests subclass
+ virtual AsyncPanZoomController* NewAPZCInstance(
+ LayersId aLayersId, GeckoContentController* aController);
+
+ public:
+ // Public hook for gtests subclass
+ virtual SampleTime GetFrameTime();
+
+ // Also used for controlling time during tests
+ void SetTestSampleTime(const Maybe<TimeStamp>& aTime);
+
+ private:
+ mutable DataMutex<Maybe<TimeStamp>> mTestSampleTime;
+ CopyableTArray<MatrixMessage> mLastMessages;
+
+ public:
+ /* Some helper functions to find an APZC given some identifying input. These
+ functions lock the tree of APZCs while they find the right one, and then
+ return an addref'd pointer to it. This allows caller code to just use the
+ target APZC without worrying about it going away. These are public for
+ testing code and generally should not be used by other production code.
+ */
+ RefPtr<HitTestingTreeNode> GetRootNode() const;
+ HitTestResult GetTargetAPZC(const ScreenPoint& aPoint);
+ already_AddRefed<AsyncPanZoomController> GetTargetAPZC(
+ const LayersId& aLayersId,
+ const ScrollableLayerGuid::ViewID& aScrollId) const;
+ already_AddRefed<AsyncPanZoomController> GetTargetAPZC(
+ const LayersId& aLayersId, const ScrollableLayerGuid::ViewID& aScrollId,
+ const MutexAutoLock& aProofOfMapLock) const;
+ ScreenToParentLayerMatrix4x4 GetScreenToApzcTransform(
+ const AsyncPanZoomController* aApzc) const;
+ ParentLayerToScreenMatrix4x4 GetApzcToGeckoTransformForHit(
+ HitTestResult& aHitResult) const;
+ ParentLayerToScreenMatrix4x4 GetApzcToGeckoTransform(
+ const AsyncPanZoomController* aApzc,
+ const AsyncTransformComponents& aComponents) const;
+ ScreenPoint GetCurrentMousePosition() const;
+ void SetCurrentMousePosition(const ScreenPoint& aNewPos);
+
+ /**
+ * Convert a screen point of an event targeting |aApzc| to Gecko
+ * coordinates.
+ */
+ Maybe<ScreenIntPoint> ConvertToGecko(const ScreenIntPoint& aPoint,
+ AsyncPanZoomController* aApzc);
+
+ /**
+ * Find the zoomable APZC in the same layer subtree (i.e. with the same
+ * layers id) as the given APZC.
+ */
+ already_AddRefed<AsyncPanZoomController> FindZoomableApzc(
+ AsyncPanZoomController* aStart) const;
+
+ ScreenMargin GetCompositorFixedLayerMargins() const;
+
+ void AdjustEventPointForDynamicToolbar(ScreenIntPoint& aEventPoint,
+ const HitTestResult& aHit);
+
+ APZScrollGeneration NewAPZScrollGeneration() {
+ // In the production code this function gets only called from the sampler
+ // thread but in tests using nsIDOMWindowUtils.setAsyncScrollOffset this
+ // function gets called from the controller thread so we need to lock the
+ // mutex for this counter.
+ MutexAutoLock lock(mScrollGenerationLock);
+ return mScrollGenerationCounter.NewAPZGeneration();
+ }
+
+ template <typename Callback>
+ void CallWithMapLock(Callback& aCallback) {
+ MutexAutoLock lock(mMapLock);
+ aCallback(lock);
+ }
+
+ private:
+ using GuidComparator = ScrollableLayerGuid::Comparator;
+ using ScrollNode = WebRenderScrollDataWrapper;
+
+ /* Helpers */
+
+ void AttachNodeToTree(HitTestingTreeNode* aNode, HitTestingTreeNode* aParent,
+ HitTestingTreeNode* aNextSibling)
+ MOZ_REQUIRES(mTreeLock);
+ already_AddRefed<AsyncPanZoomController> GetTargetAPZC(
+ const ScrollableLayerGuid& aGuid);
+ already_AddRefed<HitTestingTreeNode> GetTargetNode(
+ const ScrollableLayerGuid& aGuid, GuidComparator aComparator) const;
+ HitTestingTreeNode* FindTargetNode(HitTestingTreeNode* aNode,
+ const ScrollableLayerGuid& aGuid,
+ GuidComparator aComparator);
+ TargetApzcForNodeResult GetTargetApzcForNode(const HitTestingTreeNode* aNode);
+ TargetApzcForNodeResult FindHandoffParent(
+ const AsyncPanZoomController* aApzc);
+ HitTestingTreeNode* FindRootNodeForLayersId(LayersId aLayersId) const;
+ AsyncPanZoomController* FindRootContentApzcForLayersId(
+ LayersId aLayersId) const;
+ already_AddRefed<AsyncPanZoomController> GetZoomableTarget(
+ AsyncPanZoomController* aApzc1, AsyncPanZoomController* aApzc2) const;
+ already_AddRefed<AsyncPanZoomController> CommonAncestor(
+ AsyncPanZoomController* aApzc1, AsyncPanZoomController* aApzc2) const;
+
+ struct FixedPositionInfo;
+ struct StickyPositionInfo;
+
+ // Returns true if |aNode| is a fixed layer that is fixed to the root content
+ // APZC.
+ // The map lock is required within these functions; if the map lock is already
+ // being held by the caller, the second overload should be used. If the map
+ // lock is not being held at the call site, the first overload should be used.
+ bool IsFixedToRootContent(const HitTestingTreeNode* aNode) const;
+ bool IsFixedToRootContent(const FixedPositionInfo& aFixedInfo,
+ const MutexAutoLock& aProofOfMapLock) const;
+
+ // Returns the vertical sides of |aNode| that are stuck to the root content.
+ // The map lock is required within these functions; if the map lock is already
+ // being held by the caller, the second overload should be used. If the map
+ // lock is not being held at the call site, the first overload should be used.
+ SideBits SidesStuckToRootContent(const HitTestingTreeNode* aNode) const;
+ SideBits SidesStuckToRootContent(const StickyPositionInfo& aStickyInfo,
+ const MutexAutoLock& aProofOfMapLock) const;
+
+ /**
+ * Perform hit testing for a touch-start event.
+ *
+ * @param aEvent The touch-start event.
+ *
+ * The remaining parameters are out-parameter used to communicate additional
+ * return values:
+ *
+ * @param aOutTouchBehaviors
+ * The touch behaviours that should be allowed for this touch block.
+
+ * @return The results of the hit test, including the APZC that was hit.
+ */
+ HitTestResult GetTouchInputBlockAPZC(
+ const MultiTouchInput& aEvent,
+ nsTArray<TouchBehaviorFlags>* aOutTouchBehaviors);
+
+ /**
+ * A helper structure for use by ReceiveInputEvent() and its helpers.
+ */
+ struct InputHandlingState {
+ // A reference to the event being handled.
+ InputData& mEvent;
+
+ // The value that will be returned by ReceiveInputEvent().
+ APZEventResult mResult;
+
+ // If we performed a hit-test while handling this input event, or
+ // reused the result of a previous hit-test in the input block,
+ // this is populated with the result of the hit test.
+ HitTestResult mHit;
+
+ // Called at the end of ReceiveInputEvent() to perform any final
+ // computations, and then return mResult.
+ // If the event will have a delayed result then this takes care of adding
+ // the specified callback to the APZCTreeManager.
+ APZEventResult Finish(APZCTreeManager& aTreeManager,
+ InputBlockCallback&& aCallback);
+ };
+
+ void ProcessTouchInput(InputHandlingState& aState, MultiTouchInput& aInput);
+ /**
+ * Given a mouse-down event that hit a scroll thumb node, set up APZ
+ * dragging of the scroll thumb.
+ *
+ * Must be called after the mouse event has been sent to InputQueue.
+ *
+ * @param aMouseInput The mouse-down event.
+ * @param aScrollThumbNode Tthe scroll thumb node that was hit.
+ * @param aApzc
+ * The APZC for the scroll frame scrolled by the scroll thumb, if that
+ * scroll frame is layerized. (A thumb can be layerized without its
+ * target scroll frame being layerized.) Otherwise, an enclosing APZC.
+ */
+ void SetupScrollbarDrag(MouseInput& aMouseInput,
+ const HitTestingTreeNodeAutoLock& aScrollThumbNode,
+ AsyncPanZoomController* aApzc);
+ /**
+ * Process a touch event that's part of a scrollbar touch-drag gesture.
+ *
+ * @param aInput The touch event.
+ * @param aScrollThumbNode
+ * If this is the touch-start event, the node representing the scroll
+ * thumb we are starting to drag. Otherwise nullptr.
+ * @param aHitInfo
+ * The hit-test flags for the touch input.
+ * @return See ReceiveInputEvent() for what the return value means.
+ */
+ APZEventResult ProcessTouchInputForScrollbarDrag(
+ MultiTouchInput& aInput,
+ const HitTestingTreeNodeAutoLock& aScrollThumbNode,
+ const gfx::CompositorHitTestInfo& aHitInfo);
+ void FlushRepaintsToClearScreenToGeckoTransform();
+
+ void SynthesizePinchGestureFromMouseWheel(
+ const ScrollWheelInput& aWheelInput,
+ const RefPtr<AsyncPanZoomController>& aTarget);
+
+ already_AddRefed<HitTestingTreeNode> RecycleOrCreateNode(
+ const RecursiveMutexAutoLock& aProofOfTreeLock, TreeBuildingState& aState,
+ AsyncPanZoomController* aApzc, LayersId aLayersId);
+ HitTestingTreeNode* PrepareNodeForLayer(
+ const RecursiveMutexAutoLock& aProofOfTreeLock, const ScrollNode& aLayer,
+ const FrameMetrics& aMetrics, LayersId aLayersId,
+ const Maybe<ZoomConstraints>& aZoomConstraints,
+ const AncestorTransform& aAncestorTransform, HitTestingTreeNode* aParent,
+ HitTestingTreeNode* aNextSibling, TreeBuildingState& aState);
+
+ void PrintLayerInfo(const ScrollNode& aLayer);
+
+ void NotifyScrollbarDragInitiated(uint64_t aDragBlockId,
+ const ScrollableLayerGuid& aGuid,
+ ScrollDirection aDirection) const;
+ void NotifyScrollbarDragRejected(const ScrollableLayerGuid& aGuid) const;
+ void NotifyAutoscrollRejected(const ScrollableLayerGuid& aGuid) const;
+
+ // Returns the transform that converts from |aNode|'s coordinates to
+ // the coordinates of |aNode|'s parent in the hit-testing tree.
+ // Requires the caller to hold mTreeLock.
+ LayerToParentLayerMatrix4x4 ComputeTransformForNode(
+ const HitTestingTreeNode* aNode) const MOZ_REQUIRES(mTreeLock);
+
+ // Look up the GeckoContentController for the given layers id.
+ static already_AddRefed<GeckoContentController> GetContentController(
+ LayersId aLayersId);
+
+ bool AdvanceAnimationsInternal(const MutexAutoLock& aProofOfMapLock,
+ const SampleTime& aSampleTime);
+
+ using ClippedCompositionBoundsMap =
+ std::unordered_map<ScrollableLayerGuid, ParentLayerRect,
+ ScrollableLayerGuid::HashIgnoringPresShellFn,
+ ScrollableLayerGuid::EqualIgnoringPresShellFn>;
+ // This is a recursive function that populates `aDestMap` with the clipped
+ // composition bounds for the APZC corresponding to `aGuid` and returns those
+ // bounds as a convenience. It recurses to also populate `aDestMap` with that
+ // APZC's ancestors. In order to do this it needs to access mApzcMap
+ // and therefore requires the caller to hold the map lock.
+ ParentLayerRect ComputeClippedCompositionBounds(
+ const MutexAutoLock& aProofOfMapLock,
+ ClippedCompositionBoundsMap& aDestMap, ScrollableLayerGuid aGuid);
+
+ ScreenMargin GetCompositorFixedLayerMargins(
+ const MutexAutoLock& aProofOfMapLock) const;
+
+ protected:
+ /* The input queue where input events are held until we know enough to
+ * figure out where they're going. Protected so gtests can access it.
+ */
+ RefPtr<InputQueue> mInputQueue;
+
+ private:
+ /* Layers id for the root CompositorBridgeParent that owns this
+ * APZCTreeManager. */
+ LayersId mRootLayersId;
+
+ /* Pointer to the APZSampler instance that is bound to this APZCTreeManager.
+ * The sampler has a RefPtr to this class, and this non-owning raw pointer
+ * back to the APZSampler is nulled out in the sampler's destructor, so this
+ * pointer should always be valid.
+ */
+ APZSampler* MOZ_NON_OWNING_REF mSampler;
+ /* Pointer to the APZUpdater instance that is bound to this APZCTreeManager.
+ * The updater has a RefPtr to this class, and this non-owning raw pointer
+ * back to the APZUpdater is nulled out in the updater's destructor, so this
+ * pointer should always be valid.
+ */
+ APZUpdater* MOZ_NON_OWNING_REF mUpdater;
+
+ /* Whenever walking or mutating the tree rooted at mRootNode, mTreeLock must
+ * be held. This lock does not need to be held while manipulating a single
+ * APZC instance in isolation (that is, if its tree pointers are not being
+ * accessed or mutated). The lock also needs to be held when accessing the
+ * mRootNode instance variable, as that is considered part of the APZC tree
+ * management state.
+ * IMPORTANT: See the note about lock ordering at the top of this file. */
+ mutable mozilla::RecursiveMutex mTreeLock;
+ RefPtr<HitTestingTreeNode> mRootNode MOZ_GUARDED_BY(mTreeLock);
+
+ /*
+ * A set of LayersIds for which APZCTM should only send empty
+ * MatrixMessages via NotifyLayerTransform().
+ * This is used in cases where a tab has been transferred to a non-APZ
+ * compositor (and thus will not receive MatrixMessages reflecting its new
+ * transforms) and we need to make sure it doesn't get stuck with transforms
+ * from its old tree manager (us).
+ * Acquire mTreeLock before accessing this.
+ */
+ std::unordered_set<LayersId, LayersId::HashFn> mDetachedLayersIds
+ MOZ_GUARDED_BY(mTreeLock);
+
+ /* If the current hit-testing tree contains an async zoom container
+ * node, this is set to the layers id of subtree that has the node.
+ */
+ Maybe<LayersId> mAsyncZoomContainerSubtree;
+
+ /** A lock that protects mApzcMap, mScrollThumbInfo, mRootScrollbarInfo,
+ * mFixedPositionInfo, and mStickyPositionInfo.
+ */
+ mutable mozilla::Mutex mMapLock;
+
+ /**
+ * Helper structure to store a bunch of things in mApzcMap so that they can
+ * be used from the sampler thread.
+ */
+ struct ApzcMapData {
+ // A pointer to the APZC itself
+ RefPtr<AsyncPanZoomController> apzc;
+ // The parent APZC's guid, or Nothing() if there is no parent
+ Maybe<ScrollableLayerGuid> parent;
+ };
+
+ /**
+ * A map for quick access to get some APZC data by guid, without having to
+ * acquire the tree lock. mMapLock must be acquired while accessing or
+ * modifying mApzcMap.
+ */
+ std::unordered_map<ScrollableLayerGuid, ApzcMapData,
+ ScrollableLayerGuid::HashIgnoringPresShellFn,
+ ScrollableLayerGuid::EqualIgnoringPresShellFn>
+ mApzcMap;
+ /**
+ * A helper structure to store all the information needed to compute the
+ * async transform for a scrollthumb on the sampler thread.
+ */
+ struct ScrollThumbInfo {
+ uint64_t mThumbAnimationId;
+ CSSTransformMatrix mThumbTransform;
+ ScrollbarData mThumbData;
+ ScrollableLayerGuid mTargetGuid;
+ CSSTransformMatrix mTargetTransform;
+ bool mTargetIsAncestor;
+
+ ScrollThumbInfo(const uint64_t& aThumbAnimationId,
+ const CSSTransformMatrix& aThumbTransform,
+ const ScrollbarData& aThumbData,
+ const ScrollableLayerGuid& aTargetGuid,
+ const CSSTransformMatrix& aTargetTransform,
+ bool aTargetIsAncestor)
+ : mThumbAnimationId(aThumbAnimationId),
+ mThumbTransform(aThumbTransform),
+ mThumbData(aThumbData),
+ mTargetGuid(aTargetGuid),
+ mTargetTransform(aTargetTransform),
+ mTargetIsAncestor(aTargetIsAncestor) {
+ MOZ_ASSERT(mTargetGuid.mScrollId == mThumbData.mTargetViewId);
+ }
+ };
+ /**
+ * If this APZCTreeManager is being used with WebRender, this vector gets
+ * populated during a layers update. It holds a package of information needed
+ * to compute and set the async transforms on scroll thumbs. This information
+ * is extracted from the HitTestingTreeNodes for the WebRender case because
+ * accessing the HitTestingTreeNodes requires holding the tree lock which
+ * we cannot do on the WR sampler thread. mScrollThumbInfo, however, can
+ * be accessed while just holding the mMapLock which is safe to do on the
+ * sampler thread.
+ * mMapLock must be acquired while accessing or modifying mScrollThumbInfo.
+ */
+ std::vector<ScrollThumbInfo> mScrollThumbInfo;
+
+ /**
+ * A helper structure to store all the information needed to compute the
+ * async transform for a scrollthumb on the sampler thread.
+ */
+ struct RootScrollbarInfo {
+ uint64_t mScrollbarAnimationId;
+ ScrollDirection mScrollDirection;
+
+ RootScrollbarInfo(const uint64_t& aScrollbarAnimationId,
+ const ScrollDirection aScrollDirection)
+ : mScrollbarAnimationId(aScrollbarAnimationId),
+ mScrollDirection(aScrollDirection) {}
+ };
+ /**
+ * If this APZCTreeManager is being used with WebRender, this vector gets
+ * populated during a layers update. It holds a package of information needed
+ * to compute and set the async transforms on root scrollbars. This
+ * information is extracted from the HitTestingTreeNodes for the WebRender
+ * case because accessing the HitTestingTreeNodes requires holding the tree
+ * lock which we cannot do on the WR sampler thread. mRootScrollbarInfo,
+ * however, can be accessed while just holding the mMapLock which is safe to
+ * do on the sampler thread.
+ * mMapLock must be acquired while accessing or modifying mRootScrollbarInfo.
+ */
+ std::vector<RootScrollbarInfo> mRootScrollbarInfo;
+
+ /**
+ * A helper structure to store all the information needed to compute the
+ * async transform for a fixed position element on the sampler thread.
+ */
+ struct FixedPositionInfo {
+ Maybe<uint64_t> mFixedPositionAnimationId;
+ SideBits mFixedPosSides;
+ ScrollableLayerGuid::ViewID mFixedPosTarget;
+ LayersId mLayersId;
+
+ explicit FixedPositionInfo(const HitTestingTreeNode* aNode);
+ };
+ /**
+ * If this APZCTreeManager is being used with WebRender, this vector gets
+ * populated during a layers update. It holds a package of information needed
+ * to compute and set the async transforms on fixed position content. This
+ * information is extracted from the HitTestingTreeNodes for the WebRender
+ * case because accessing the HitTestingTreeNodes requires holding the tree
+ * lock which we cannot do on the WR sampler thread. mFixedPositionInfo,
+ * however, can be accessed while just holding the mMapLock which is safe to
+ * do on the sampler thread. mMapLock must be acquired while accessing or
+ * modifying mFixedPositionInfo.
+ */
+ std::vector<FixedPositionInfo> mFixedPositionInfo;
+
+ /**
+ * A helper structure to store all the information needed to compute the
+ * async transform for a sticky position element on the sampler thread.
+ */
+ struct StickyPositionInfo {
+ Maybe<uint64_t> mStickyPositionAnimationId;
+ SideBits mFixedPosSides;
+ ScrollableLayerGuid::ViewID mStickyPosTarget;
+ LayersId mLayersId;
+ LayerRectAbsolute mStickyScrollRangeInner;
+ LayerRectAbsolute mStickyScrollRangeOuter;
+
+ explicit StickyPositionInfo(const HitTestingTreeNode* aNode);
+ };
+ /**
+ * If this APZCTreeManager is being used with WebRender, this vector gets
+ * populated during a layers update. It holds a package of information needed
+ * to compute and set the async transforms on sticky position content. This
+ * information is extracted from the HitTestingTreeNodes for the WebRender
+ * case because accessing the HitTestingTreeNodes requires holding the tree
+ * lock which we cannot do on the WR sampler thread. mStickyPositionInfo,
+ * however, can be accessed while just holding the mMapLock which is safe to
+ * do on the sampler thread. mMapLock must be acquired while accessing or
+ * modifying mStickyPositionInfo.
+ */
+ std::vector<StickyPositionInfo> mStickyPositionInfo;
+
+ /* Holds the zoom constraints for scrollable layers, as determined by the
+ * the main-thread gecko code. This can only be accessed on the updater
+ * thread. */
+ std::unordered_map<ScrollableLayerGuid, ZoomConstraints,
+ ScrollableLayerGuid::HashIgnoringPresShellFn,
+ ScrollableLayerGuid::EqualIgnoringPresShellFn>
+ mZoomConstraints;
+ /* A list of keyboard shortcuts to use for translating keyboard inputs into
+ * keyboard actions. This is gathered on the main thread from XBL bindings.
+ * This must only be accessed on the controller thread.
+ */
+ KeyboardMap mKeyboardMap;
+ /* This tracks the focus targets of chrome and content and whether we have
+ * a current focus target or whether we are waiting for a new confirmation.
+ */
+ FocusState mFocusState;
+ /* This tracks the hit test result info for the current touch input block.
+ * In particular, it tracks the target APZC, the hit test flags, and the
+ * fixed pos sides. This is populated at the start of a touch block based
+ * on the hit-test result, and used for subsequent touch events in the block.
+ * This allows touch points to move outside the thing they started on, but
+ * still have the touch events delivered to the same initial APZC. This will
+ * only ever be touched on the input delivery thread, and so does not require
+ * locking.
+ */
+ HitTestResult mTouchBlockHitResult;
+ /* Sometimes we want to ignore all touches except one. In such cases, this
+ * is set to the identifier of the touch we are not ignoring; in other cases,
+ * this is set to -1.
+ */
+ int32_t mRetainedTouchIdentifier;
+ /* This tracks whether the current input block represents a touch-drag of
+ * a scrollbar. In this state, touch events are forwarded to content as touch
+ * events, but converted to mouse events before going into InputQueue and
+ * being handled by an APZC (to reuse the APZ code for scrollbar dragging
+ * with a mouse).
+ */
+ bool mInScrollbarTouchDrag;
+ /* Tracks the number of touch points we are tracking that are currently on
+ * the screen. */
+ TouchCounter mTouchCounter;
+ /* If a tap gesture event sent directly by widget code (rather than gesture
+ * detected from touch events by APZ) is being processed, this stores the
+ * result of hit testing for that tap gesture event.
+ */
+ HitTestResult mTapGestureHitResult;
+ /* Stores the current mouse position in screen coordinates.
+ */
+ mutable DataMutex<ScreenPoint> mCurrentMousePosition;
+ /* Extra margins that should be applied to content that fixed wrt. the
+ * RCD-RSF, to account for the dynamic toolbar.
+ * Acquire mMapLock before accessing this.
+ */
+ ScreenMargin mCompositorFixedLayerMargins;
+ /* Similar to above |mCompositorFixedLayerMargins|. But this value is the
+ * margins on the main-thread at the last time position:fixed elements were
+ * updated during the dynamic toolbar transitions.
+ * Acquire mMapLock before accessing this.
+ */
+ ScreenMargin mGeckoFixedLayerMargins;
+ /* For logging the APZC tree for debugging (enabled by the apz.printtree
+ * pref). The purpose of using LOG_CRITICAL is so that you don't also need to
+ * change the gfx.logging.level pref to see the output. */
+ gfx::TreeLog<gfx::LOG_CRITICAL> mApzcTreeLog;
+
+ class CheckerboardFlushObserver;
+ friend class CheckerboardFlushObserver;
+ RefPtr<CheckerboardFlushObserver> mFlushObserver;
+
+ // Map from layers id to APZTestData. Accesses and mutations must be
+ // protected by the mTestDataLock.
+ std::unordered_map<LayersId, UniquePtr<APZTestData>, LayersId::HashFn>
+ mTestData;
+ mutable mozilla::Mutex mTestDataLock;
+
+ // This must only be touched on the controller thread.
+ float mDPI;
+
+ friend class IAPZHitTester;
+ UniquePtr<IAPZHitTester> mHitTester;
+
+ // NOTE: This ScrollGenerationCounter needs to be per APZCTreeManager since
+ // the generation is bumped up on the sampler theread which is per
+ // APZCTreeManager.
+ ScrollGenerationCounter mScrollGenerationCounter;
+ mozilla::Mutex mScrollGenerationLock;
+
+#if defined(MOZ_WIDGET_ANDROID)
+ private:
+ // Last Frame metrics sent to java through UIController.
+ GeckoViewMetrics mLastRootMetrics;
+#endif // defined(MOZ_WIDGET_ANDROID)
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_PanZoomController_h
diff --git a/gfx/layers/apz/src/APZInputBridge.cpp b/gfx/layers/apz/src/APZInputBridge.cpp
new file mode 100644
index 0000000000..bce801a6d2
--- /dev/null
+++ b/gfx/layers/apz/src/APZInputBridge.cpp
@@ -0,0 +1,435 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/layers/APZInputBridge.h"
+
+#include "AsyncPanZoomController.h"
+#include "InputData.h" // for MouseInput, etc
+#include "InputBlockState.h" // for InputBlockState
+#include "OverscrollHandoffState.h" // for OverscrollHandoffState
+#include "mozilla/EventForwards.h"
+#include "mozilla/dom/WheelEventBinding.h" // for WheelEvent constants
+#include "mozilla/EventStateManager.h" // for EventStateManager
+#include "mozilla/layers/APZThreadUtils.h" // for AssertOnControllerThread, etc
+#include "mozilla/MouseEvents.h" // for WidgetMouseEvent
+#include "mozilla/StaticPrefs_apz.h"
+#include "mozilla/StaticPrefs_general.h"
+#include "mozilla/StaticPrefs_test.h"
+#include "mozilla/TextEvents.h" // for WidgetKeyboardEvent
+#include "mozilla/TouchEvents.h" // for WidgetTouchEvent
+#include "mozilla/WheelHandlingHelper.h" // for WheelDeltaHorizontalizer,
+ // WheelDeltaAdjustmentStrategy
+
+namespace mozilla {
+namespace layers {
+
+APZEventResult::APZEventResult()
+ : mStatus(nsEventStatus_eIgnore),
+ mInputBlockId(InputBlockState::NO_BLOCK_ID) {}
+
+APZEventResult::APZEventResult(
+ const RefPtr<AsyncPanZoomController>& aInitialTarget,
+ TargetConfirmationFlags aFlags)
+ : APZEventResult() {
+ mHandledResult = [&]() -> Maybe<APZHandledResult> {
+ if (!aInitialTarget->IsRootContent()) {
+ // If the initial target is not the root, this will definitely not be
+ // handled by the root. (The confirmed target is either the initial
+ // target, or a descendant.)
+ return Some(
+ APZHandledResult{APZHandledPlace::HandledByContent, aInitialTarget});
+ }
+
+ if (!aFlags.mDispatchToContent) {
+ // If the initial target is the root and we don't need to dispatch to
+ // content, the event will definitely be handled by the root.
+ return Some(
+ APZHandledResult{APZHandledPlace::HandledByRoot, aInitialTarget});
+ }
+
+ // Otherwise, we're not sure.
+ return Nothing();
+ }();
+ aInitialTarget->GetGuid(&mTargetGuid);
+}
+
+void APZEventResult::SetStatusAsConsumeDoDefault(
+ const InputBlockState& aBlock) {
+ SetStatusAsConsumeDoDefault(aBlock.GetTargetApzc());
+}
+
+void APZEventResult::SetStatusAsConsumeDoDefault(
+ const RefPtr<AsyncPanZoomController>& aTarget) {
+ mStatus = nsEventStatus_eConsumeDoDefault;
+ mHandledResult =
+ Some(aTarget && aTarget->IsRootContent()
+ ? APZHandledResult{APZHandledPlace::HandledByRoot, aTarget}
+ : APZHandledResult{APZHandledPlace::HandledByContent, aTarget});
+}
+
+void APZEventResult::SetStatusForTouchEvent(
+ const InputBlockState& aBlock, TargetConfirmationFlags aFlags,
+ PointerEventsConsumableFlags aConsumableFlags,
+ const AsyncPanZoomController* aTarget) {
+ // Note, we need to continue setting mStatus to eIgnore in the {mHasRoom=true,
+ // mAllowedByTouchAction=false} case because this is the behaviour expected by
+ // APZEventState::ProcessTouchEvent() when it determines when to send a
+ // `pointercancel` event. TODO: Use something more descriptive than
+ // nsEventStatus for this purpose.
+ mStatus = aConsumableFlags.IsConsumable() ? nsEventStatus_eConsumeDoDefault
+ : nsEventStatus_eIgnore;
+
+ UpdateHandledResult(aBlock, aConsumableFlags, aTarget,
+ aFlags.mDispatchToContent);
+}
+
+void APZEventResult::UpdateHandledResult(
+ const InputBlockState& aBlock,
+ PointerEventsConsumableFlags aConsumableFlags,
+ const AsyncPanZoomController* aTarget, bool aDispatchToContent) {
+ // If the touch event's effect is disallowed by touch-action, treat it as if
+ // a touch event listener had preventDefault()-ed it (i.e. return
+ // HandledByContent, except we can do it eagerly rather than having to wait
+ // for the listener to run).
+ if (!aConsumableFlags.mAllowedByTouchAction) {
+ mHandledResult =
+ Some(APZHandledResult{APZHandledPlace::HandledByContent, aTarget});
+ return;
+ }
+
+ if (mHandledResult && !aDispatchToContent && !aConsumableFlags.mHasRoom) {
+ // Set result to Unhandled if we have no room to scroll, unless it
+ // was HandledByContent because we're over a dispatch-to-content region,
+ // in which case it should remain HandledByContent.
+ mHandledResult->mPlace = APZHandledPlace::Unhandled;
+ }
+
+ if (aTarget && !aTarget->IsRootContent()) {
+ auto [result, rootApzc] =
+ aBlock.GetOverscrollHandoffChain()->ScrollingDownWillMoveDynamicToolbar(
+ aTarget);
+ if (result) {
+ MOZ_ASSERT(rootApzc && rootApzc->IsRootContent());
+ // The event is actually consumed by a non-root APZC but scroll
+ // positions in all relevant APZCs are at the bottom edge, so if there's
+ // still contents covered by the dynamic toolbar we need to move the
+ // dynamic toolbar to make the covered contents visible, thus we need
+ // to tell it to GeckoView so we handle it as if it's consumed in the
+ // root APZC.
+ // IMPORTANT NOTE: If the incoming TargetConfirmationFlags has
+ // mDispatchToContent, we need to change it to Nothing() so that
+ // GeckoView can properly wait for results from the content on the
+ // main-thread.
+ mHandledResult =
+ aDispatchToContent
+ ? Nothing()
+ : Some(APZHandledResult{aConsumableFlags.IsConsumable()
+ ? APZHandledPlace::HandledByRoot
+ : APZHandledPlace::Unhandled,
+ rootApzc});
+ }
+ }
+}
+
+void APZEventResult::SetStatusForFastFling(
+ const TouchBlockState& aBlock, TargetConfirmationFlags aFlags,
+ PointerEventsConsumableFlags aConsumableFlags,
+ const AsyncPanZoomController* aTarget) {
+ MOZ_ASSERT(aBlock.IsDuringFastFling());
+
+ // Set eConsumeNoDefault for fast fling since we don't want to send the event
+ // to content at all.
+ mStatus = nsEventStatus_eConsumeNoDefault;
+
+ // In the case of fast fling, the event will never be sent to content, so we
+ // want a result where `aDispatchToContent` is false whatever the original
+ // `aFlags.mDispatchToContent` is.
+ UpdateHandledResult(aBlock, aConsumableFlags, aTarget, false /*
+ aDispatchToContent */);
+}
+
+static bool WillHandleMouseEvent(const WidgetMouseEventBase& aEvent) {
+ return aEvent.mMessage == eMouseMove || aEvent.mMessage == eMouseDown ||
+ aEvent.mMessage == eMouseUp || aEvent.mMessage == eDragEnd ||
+ (StaticPrefs::test_events_async_enabled() &&
+ aEvent.mMessage == eMouseHitTest);
+}
+
+/* static */
+Maybe<APZWheelAction> APZInputBridge::ActionForWheelEvent(
+ WidgetWheelEvent* aEvent) {
+ if (!(aEvent->mDeltaMode == dom::WheelEvent_Binding::DOM_DELTA_LINE ||
+ aEvent->mDeltaMode == dom::WheelEvent_Binding::DOM_DELTA_PIXEL ||
+ aEvent->mDeltaMode == dom::WheelEvent_Binding::DOM_DELTA_PAGE)) {
+ return Nothing();
+ }
+ return EventStateManager::APZWheelActionFor(aEvent);
+}
+
+APZEventResult APZInputBridge::ReceiveInputEvent(
+ WidgetInputEvent& aEvent, InputBlockCallback&& aCallback) {
+ APZThreadUtils::AssertOnControllerThread();
+
+ APZEventResult result;
+
+ switch (aEvent.mClass) {
+ case eMouseEventClass:
+ case eDragEventClass: {
+ WidgetMouseEvent& mouseEvent = *aEvent.AsMouseEvent();
+ if (WillHandleMouseEvent(mouseEvent)) {
+ MouseInput input(mouseEvent);
+ input.mOrigin =
+ ScreenPoint(mouseEvent.mRefPoint.x, mouseEvent.mRefPoint.y);
+
+ result = ReceiveInputEvent(input, std::move(aCallback));
+
+ mouseEvent.mRefPoint = TruncatedToInt(ViewAs<LayoutDevicePixel>(
+ input.mOrigin,
+ PixelCastJustification::LayoutDeviceIsScreenForUntransformedEvent));
+ mouseEvent.mFlags.mHandledByAPZ = input.mHandledByAPZ;
+ mouseEvent.mFocusSequenceNumber = input.mFocusSequenceNumber;
+#ifdef XP_MACOSX
+ // It's not assumed that the click event has already been prevented,
+ // except mousedown event with ctrl key is pressed where we prevent
+ // click event from widget on Mac platform.
+ MOZ_ASSERT_IF(!mouseEvent.IsControl() ||
+ mouseEvent.mMessage != eMouseDown ||
+ mouseEvent.mButton != MouseButton::ePrimary,
+ !mouseEvent.mClickEventPrevented);
+#else
+ MOZ_ASSERT(
+ !mouseEvent.mClickEventPrevented,
+ "It's not assumed that the click event has already been prevented");
+#endif
+ mouseEvent.mClickEventPrevented |= input.mPreventClickEvent;
+ MOZ_ASSERT_IF(mouseEvent.mClickEventPrevented,
+ mouseEvent.mMessage == eMouseDown ||
+ mouseEvent.mMessage == eMouseUp);
+ aEvent.mLayersId = input.mLayersId;
+
+ if (mouseEvent.IsReal()) {
+ UpdateWheelTransaction(mouseEvent.mRefPoint, mouseEvent.mMessage,
+ Some(result.mTargetGuid));
+ }
+
+ return result;
+ }
+
+ if (mouseEvent.IsReal()) {
+ UpdateWheelTransaction(mouseEvent.mRefPoint, mouseEvent.mMessage,
+ Nothing());
+ }
+
+ ProcessUnhandledEvent(&mouseEvent.mRefPoint, &result.mTargetGuid,
+ &aEvent.mFocusSequenceNumber, &aEvent.mLayersId);
+ return result;
+ }
+ case eTouchEventClass: {
+ WidgetTouchEvent& touchEvent = *aEvent.AsTouchEvent();
+ MultiTouchInput touchInput(touchEvent);
+ result = ReceiveInputEvent(touchInput, std::move(aCallback));
+ // touchInput was modified in-place to possibly remove some
+ // touch points (if we are overscrolled), and the coordinates were
+ // modified using the APZ untransform. We need to copy these changes
+ // back into the WidgetInputEvent.
+ touchEvent.mTouches.Clear();
+ touchEvent.mTouches.SetCapacity(touchInput.mTouches.Length());
+ for (size_t i = 0; i < touchInput.mTouches.Length(); i++) {
+ *touchEvent.mTouches.AppendElement() =
+ touchInput.mTouches[i].ToNewDOMTouch();
+ }
+ touchEvent.mFlags.mHandledByAPZ = touchInput.mHandledByAPZ;
+ touchEvent.mFocusSequenceNumber = touchInput.mFocusSequenceNumber;
+ aEvent.mLayersId = touchInput.mLayersId;
+ return result;
+ }
+ case eWheelEventClass: {
+ WidgetWheelEvent& wheelEvent = *aEvent.AsWheelEvent();
+
+ if (Maybe<APZWheelAction> action = ActionForWheelEvent(&wheelEvent)) {
+ ScrollWheelInput::ScrollMode scrollMode =
+ ScrollWheelInput::SCROLLMODE_INSTANT;
+ if (StaticPrefs::general_smoothScroll() &&
+ ((wheelEvent.mDeltaMode ==
+ dom::WheelEvent_Binding::DOM_DELTA_LINE &&
+ StaticPrefs::general_smoothScroll_mouseWheel()) ||
+ (wheelEvent.mDeltaMode ==
+ dom::WheelEvent_Binding::DOM_DELTA_PAGE &&
+ StaticPrefs::general_smoothScroll_pages()))) {
+ scrollMode = ScrollWheelInput::SCROLLMODE_SMOOTH;
+ }
+
+ WheelDeltaAdjustmentStrategy strategy =
+ EventStateManager::GetWheelDeltaAdjustmentStrategy(wheelEvent);
+ // Adjust the delta values of the wheel event if the current default
+ // action is to horizontalize scrolling. I.e., deltaY values are set to
+ // deltaX and deltaY and deltaZ values are set to 0.
+ // If horizontalized, the delta values will be restored and its overflow
+ // deltaX will become 0 when the WheelDeltaHorizontalizer instance is
+ // being destroyed.
+ WheelDeltaHorizontalizer horizontalizer(wheelEvent);
+ if (WheelDeltaAdjustmentStrategy::eHorizontalize == strategy) {
+ horizontalizer.Horizontalize();
+ }
+
+ // If the wheel event becomes no-op event, don't handle it as scroll.
+ if (wheelEvent.mDeltaX || wheelEvent.mDeltaY) {
+ ScreenPoint origin(wheelEvent.mRefPoint.x, wheelEvent.mRefPoint.y);
+ ScrollWheelInput input(
+ wheelEvent.mTimeStamp, 0, scrollMode,
+ ScrollWheelInput::DeltaTypeForDeltaMode(wheelEvent.mDeltaMode),
+ origin, wheelEvent.mDeltaX, wheelEvent.mDeltaY,
+ wheelEvent.mAllowToOverrideSystemScrollSpeed, strategy);
+ input.mAPZAction = action.value();
+
+ // We add the user multiplier as a separate field, rather than
+ // premultiplying it, because if the input is converted back to a
+ // WidgetWheelEvent, then EventStateManager would apply the delta a
+ // second time. We could in theory work around this by asking ESM to
+ // customize the event much sooner, and then save the
+ // "mCustomizedByUserPrefs" bit on ScrollWheelInput - but for now,
+ // this seems easier.
+ EventStateManager::GetUserPrefsForWheelEvent(
+ &wheelEvent, &input.mUserDeltaMultiplierX,
+ &input.mUserDeltaMultiplierY);
+
+ result = ReceiveInputEvent(input, std::move(aCallback));
+ wheelEvent.mRefPoint = TruncatedToInt(ViewAs<LayoutDevicePixel>(
+ input.mOrigin, PixelCastJustification::
+ LayoutDeviceIsScreenForUntransformedEvent));
+ wheelEvent.mFlags.mHandledByAPZ = input.mHandledByAPZ;
+ wheelEvent.mFocusSequenceNumber = input.mFocusSequenceNumber;
+ aEvent.mLayersId = input.mLayersId;
+
+ return result;
+ }
+ }
+
+ UpdateWheelTransaction(aEvent.mRefPoint, aEvent.mMessage, Nothing());
+ ProcessUnhandledEvent(&aEvent.mRefPoint, &result.mTargetGuid,
+ &aEvent.mFocusSequenceNumber, &aEvent.mLayersId);
+ MOZ_ASSERT(result.GetStatus() == nsEventStatus_eIgnore);
+ return result;
+ }
+ case eKeyboardEventClass: {
+ WidgetKeyboardEvent& keyboardEvent = *aEvent.AsKeyboardEvent();
+
+ KeyboardInput input(keyboardEvent);
+
+ result = ReceiveInputEvent(input, std::move(aCallback));
+
+ keyboardEvent.mFlags.mHandledByAPZ = input.mHandledByAPZ;
+ keyboardEvent.mFocusSequenceNumber = input.mFocusSequenceNumber;
+ return result;
+ }
+ default: {
+ UpdateWheelTransaction(aEvent.mRefPoint, aEvent.mMessage, Nothing());
+ ProcessUnhandledEvent(&aEvent.mRefPoint, &result.mTargetGuid,
+ &aEvent.mFocusSequenceNumber, &aEvent.mLayersId);
+ return result;
+ }
+ }
+
+ MOZ_ASSERT_UNREACHABLE("Invalid WidgetInputEvent type.");
+ result.SetStatusAsConsumeNoDefault();
+ return result;
+}
+
+APZHandledResult::APZHandledResult(APZHandledPlace aPlace,
+ const AsyncPanZoomController* aTarget)
+ : mPlace(aPlace) {
+ MOZ_ASSERT(aTarget);
+ switch (aPlace) {
+ case APZHandledPlace::Unhandled:
+ break;
+ case APZHandledPlace::HandledByContent:
+ if (aTarget) {
+ mScrollableDirections = aTarget->ScrollableDirections();
+ mOverscrollDirections = aTarget->GetAllowedHandoffDirections();
+ }
+ break;
+ case APZHandledPlace::HandledByRoot: {
+ MOZ_ASSERT(aTarget->IsRootContent());
+ if (aTarget) {
+ mScrollableDirections = aTarget->ScrollableDirections();
+ mOverscrollDirections = aTarget->GetAllowedHandoffDirections();
+ }
+ break;
+ }
+ default:
+ MOZ_ASSERT_UNREACHABLE("Invalid APZHandledPlace");
+ break;
+ }
+}
+
+std::ostream& operator<<(std::ostream& aOut, const SideBits& aSideBits) {
+ if ((aSideBits & SideBits::eAll) == SideBits::eAll) {
+ aOut << "all";
+ } else {
+ AutoTArray<nsCString, 4> strings;
+ if (aSideBits & SideBits::eTop) {
+ strings.AppendElement("top"_ns);
+ }
+ if (aSideBits & SideBits::eRight) {
+ strings.AppendElement("right"_ns);
+ }
+ if (aSideBits & SideBits::eBottom) {
+ strings.AppendElement("bottom"_ns);
+ }
+ if (aSideBits & SideBits::eLeft) {
+ strings.AppendElement("left"_ns);
+ }
+ aOut << strings;
+ }
+ return aOut;
+}
+
+std::ostream& operator<<(std::ostream& aOut,
+ const ScrollDirections& aScrollDirections) {
+ if (aScrollDirections.contains(EitherScrollDirection)) {
+ aOut << "either";
+ } else if (aScrollDirections.contains(HorizontalScrollDirection)) {
+ aOut << "horizontal";
+ } else if (aScrollDirections.contains(VerticalScrollDirection)) {
+ aOut << "vertical";
+ } else {
+ aOut << "none";
+ }
+ return aOut;
+}
+
+std::ostream& operator<<(std::ostream& aOut,
+ const APZHandledPlace& aHandledPlace) {
+ switch (aHandledPlace) {
+ case APZHandledPlace::Unhandled:
+ aOut << "unhandled";
+ break;
+ case APZHandledPlace::HandledByRoot: {
+ aOut << "handled-by-root";
+ break;
+ }
+ case APZHandledPlace::HandledByContent: {
+ aOut << "handled-by-content";
+ break;
+ }
+ case APZHandledPlace::Invalid: {
+ aOut << "INVALID";
+ break;
+ }
+ }
+ return aOut;
+}
+
+std::ostream& operator<<(std::ostream& aOut,
+ const APZHandledResult& aHandledResult) {
+ aOut << "handled: " << aHandledResult.mPlace << ", ";
+ aOut << "scrollable: " << aHandledResult.mScrollableDirections << ", ";
+ aOut << "overscroll: " << aHandledResult.mOverscrollDirections << std::endl;
+ return aOut;
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/APZPublicUtils.cpp b/gfx/layers/apz/src/APZPublicUtils.cpp
new file mode 100644
index 0000000000..6902e0738c
--- /dev/null
+++ b/gfx/layers/apz/src/APZPublicUtils.cpp
@@ -0,0 +1,111 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/layers/APZPublicUtils.h"
+
+#include "AsyncPanZoomController.h"
+#include "mozilla/HelperMacros.h"
+#include "mozilla/StaticPrefs_general.h"
+
+namespace mozilla {
+namespace layers {
+
+namespace apz {
+
+/*static*/ void InitializeGlobalState() {
+ MOZ_ASSERT(NS_IsMainThread());
+ AsyncPanZoomController::InitializeGlobalState();
+}
+
+/*static*/ const ScreenMargin CalculatePendingDisplayPort(
+ const FrameMetrics& aFrameMetrics, const ParentLayerPoint& aVelocity) {
+ return AsyncPanZoomController::CalculatePendingDisplayPort(
+ aFrameMetrics, aVelocity, AsyncPanZoomController::ZoomInProgress::No);
+}
+
+/*static*/ gfx::IntSize GetDisplayportAlignmentMultiplier(
+ const ScreenSize& aBaseSize) {
+ return AsyncPanZoomController::GetDisplayportAlignmentMultiplier(aBaseSize);
+}
+
+ScrollAnimationBezierPhysicsSettings ComputeBezierAnimationSettingsForOrigin(
+ ScrollOrigin aOrigin) {
+ int32_t minMS = 0;
+ int32_t maxMS = 0;
+ bool isOriginSmoothnessEnabled = false;
+
+#define READ_DURATIONS(prefbase) \
+ isOriginSmoothnessEnabled = StaticPrefs::general_smoothScroll() && \
+ StaticPrefs::general_smoothScroll_##prefbase(); \
+ if (isOriginSmoothnessEnabled) { \
+ minMS = StaticPrefs::general_smoothScroll_##prefbase##_durationMinMS(); \
+ maxMS = StaticPrefs::general_smoothScroll_##prefbase##_durationMaxMS(); \
+ }
+
+ switch (aOrigin) {
+ case ScrollOrigin::Pixels:
+ READ_DURATIONS(pixels)
+ break;
+ case ScrollOrigin::Lines:
+ READ_DURATIONS(lines)
+ break;
+ case ScrollOrigin::Pages:
+ READ_DURATIONS(pages)
+ break;
+ case ScrollOrigin::MouseWheel:
+ READ_DURATIONS(mouseWheel)
+ break;
+ case ScrollOrigin::Scrollbars:
+ READ_DURATIONS(scrollbars)
+ break;
+ default:
+ READ_DURATIONS(other)
+ break;
+ }
+
+#undef READ_DURATIONS
+
+ if (isOriginSmoothnessEnabled) {
+ static const int32_t kSmoothScrollMaxAllowedAnimationDurationMS = 10000;
+ maxMS = clamped(maxMS, 0, kSmoothScrollMaxAllowedAnimationDurationMS);
+ minMS = clamped(minMS, 0, maxMS);
+ }
+
+ // Keep the animation duration longer than the average event intervals
+ // (to "connect" consecutive scroll animations before the scroll comes to a
+ // stop).
+ double intervalRatio =
+ ((double)StaticPrefs::general_smoothScroll_durationToIntervalRatio()) /
+ 100.0;
+
+ // Duration should be at least as long as the intervals -> ratio is at least 1
+ intervalRatio = std::max(1.0, intervalRatio);
+
+ return ScrollAnimationBezierPhysicsSettings{minMS, maxMS, intervalRatio};
+}
+
+ScrollMode GetScrollModeForOrigin(ScrollOrigin origin) {
+ if (!StaticPrefs::general_smoothScroll()) return ScrollMode::Instant;
+ switch (origin) {
+ case ScrollOrigin::Lines:
+ return StaticPrefs::general_smoothScroll_lines() ? ScrollMode::Smooth
+ : ScrollMode::Instant;
+ case ScrollOrigin::Pages:
+ return StaticPrefs::general_smoothScroll_pages() ? ScrollMode::Smooth
+ : ScrollMode::Instant;
+ case ScrollOrigin::Other:
+ return StaticPrefs::general_smoothScroll_other() ? ScrollMode::Smooth
+ : ScrollMode::Instant;
+ default:
+ MOZ_ASSERT(false, "Unknown keyboard scroll origin");
+ return StaticPrefs::general_smoothScroll() ? ScrollMode::Smooth
+ : ScrollMode::Instant;
+ }
+}
+
+} // namespace apz
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/APZSampler.cpp b/gfx/layers/apz/src/APZSampler.cpp
new file mode 100644
index 0000000000..088fb6f7a0
--- /dev/null
+++ b/gfx/layers/apz/src/APZSampler.cpp
@@ -0,0 +1,216 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/layers/APZSampler.h"
+
+#include "AsyncPanZoomController.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/layers/APZThreadUtils.h"
+#include "mozilla/layers/APZUtils.h"
+#include "mozilla/layers/CompositorThread.h"
+#include "mozilla/layers/SynchronousTask.h"
+#include "TreeTraversal.h"
+#include "mozilla/webrender/WebRenderAPI.h"
+
+namespace mozilla {
+namespace layers {
+
+StaticMutex APZSampler::sWindowIdLock;
+StaticAutoPtr<std::unordered_map<uint64_t, RefPtr<APZSampler>>>
+ APZSampler::sWindowIdMap;
+
+APZSampler::APZSampler(const RefPtr<APZCTreeManager>& aApz,
+ bool aIsUsingWebRender)
+ : mApz(aApz),
+ mIsUsingWebRender(aIsUsingWebRender),
+ mThreadIdLock("APZSampler::mThreadIdLock"),
+ mSampleTimeLock("APZSampler::mSampleTimeLock") {
+ MOZ_ASSERT(aApz);
+ mApz->SetSampler(this);
+}
+
+APZSampler::~APZSampler() { mApz->SetSampler(nullptr); }
+
+void APZSampler::Destroy() {
+ StaticMutexAutoLock lock(sWindowIdLock);
+ if (mWindowId) {
+ MOZ_ASSERT(sWindowIdMap);
+ sWindowIdMap->erase(wr::AsUint64(*mWindowId));
+ }
+}
+
+void APZSampler::SetWebRenderWindowId(const wr::WindowId& aWindowId) {
+ StaticMutexAutoLock lock(sWindowIdLock);
+ MOZ_ASSERT(!mWindowId);
+ mWindowId = Some(aWindowId);
+ if (!sWindowIdMap) {
+ sWindowIdMap = new std::unordered_map<uint64_t, RefPtr<APZSampler>>();
+ NS_DispatchToMainThread(NS_NewRunnableFunction(
+ "APZSampler::ClearOnShutdown", [] { ClearOnShutdown(&sWindowIdMap); }));
+ }
+ (*sWindowIdMap)[wr::AsUint64(aWindowId)] = this;
+}
+
+/*static*/
+void APZSampler::SetSamplerThread(const wr::WrWindowId& aWindowId) {
+ if (RefPtr<APZSampler> sampler = GetSampler(aWindowId)) {
+ MutexAutoLock lock(sampler->mThreadIdLock);
+ sampler->mSamplerThreadId = Some(PlatformThread::CurrentId());
+ }
+}
+
+/*static*/
+void APZSampler::SampleForWebRender(const wr::WrWindowId& aWindowId,
+ const uint64_t* aGeneratedFrameId,
+ wr::Transaction* aTransaction) {
+ if (RefPtr<APZSampler> sampler = GetSampler(aWindowId)) {
+ wr::TransactionWrapper txn(aTransaction);
+ Maybe<VsyncId> vsyncId =
+ aGeneratedFrameId ? Some(VsyncId{*aGeneratedFrameId}) : Nothing();
+ sampler->SampleForWebRender(vsyncId, txn);
+ }
+}
+
+void APZSampler::SetSampleTime(const SampleTime& aSampleTime) {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+ MutexAutoLock lock(mSampleTimeLock);
+ // This only gets called with WR, and the time provided is going to be
+ // the time at which the current vsync interval ends. i.e. it is the timestamp
+ // for the next vsync that will occur.
+ mSampleTime = aSampleTime;
+}
+
+void APZSampler::SampleForWebRender(const Maybe<VsyncId>& aVsyncId,
+ wr::TransactionWrapper& aTxn) {
+ AssertOnSamplerThread();
+ SampleTime sampleTime;
+ { // scope lock
+ MutexAutoLock lock(mSampleTimeLock);
+
+ // If mSampleTime is null we're in a startup phase where the
+ // WebRenderBridgeParent hasn't yet provided us with a sample time.
+ // If we're that early there probably aren't any APZ animations happening
+ // anyway, so using Timestamp::Now() should be fine.
+ SampleTime now = SampleTime::FromNow();
+ sampleTime = mSampleTime.IsNull() ? now : mSampleTime;
+ }
+ mApz->SampleForWebRender(aVsyncId, aTxn, sampleTime);
+}
+
+AsyncTransform APZSampler::GetCurrentAsyncTransform(
+ const LayersId& aLayersId, const ScrollableLayerGuid::ViewID& aScrollId,
+ AsyncTransformComponents aComponents,
+ const MutexAutoLock& aProofOfMapLock) const {
+ MOZ_ASSERT(!CompositorThreadHolder::IsInCompositorThread());
+ AssertOnSamplerThread();
+
+ RefPtr<AsyncPanZoomController> apzc =
+ mApz->GetTargetAPZC(aLayersId, aScrollId, aProofOfMapLock);
+ if (!apzc) {
+ // It's possible that this function can get called even after the target
+ // APZC has been already destroyed because destroying the animation which
+ // triggers this function call is basically processed later than the APZC,
+ // i.e. queue mCompositorAnimationsToDelete in WebRenderBridgeParent and
+ // then remove in WebRenderBridgeParent::RemoveEpochDataPriorTo.
+ return AsyncTransform{};
+ }
+
+ return apzc->GetCurrentAsyncTransform(AsyncPanZoomController::eForCompositing,
+ aComponents);
+}
+
+ParentLayerRect APZSampler::GetCompositionBounds(
+ const LayersId& aLayersId, const ScrollableLayerGuid::ViewID& aScrollId,
+ const MutexAutoLock& aProofOfMapLock) const {
+ // This function can get called on the compositor in case of non WebRender
+ // get called on the sampler thread in case of WebRender.
+ AssertOnSamplerThread();
+
+ RefPtr<AsyncPanZoomController> apzc =
+ mApz->GetTargetAPZC(aLayersId, aScrollId, aProofOfMapLock);
+ if (!apzc) {
+ // On WebRender it's possible that this function can get called even after
+ // the target APZC has been already destroyed because destroying the
+ // animation which triggers this function call is basically processed later
+ // than the APZC one, i.e. queue mCompositorAnimationsToDelete in
+ // WebRenderBridgeParent and then remove them in
+ // WebRenderBridgeParent::RemoveEpochDataPriorTo.
+ return ParentLayerRect();
+ }
+
+ return apzc->GetCompositionBounds();
+}
+
+Maybe<APZSampler::ScrollOffsetAndRange>
+APZSampler::GetCurrentScrollOffsetAndRange(
+ const LayersId& aLayersId, const ScrollableLayerGuid::ViewID& aScrollId,
+ const MutexAutoLock& aProofOfMapLock) const {
+ // Note: This is called from OMTA Sampler thread, or Compositor thread for
+ // testing.
+
+ RefPtr<AsyncPanZoomController> apzc =
+ mApz->GetTargetAPZC(aLayersId, aScrollId, aProofOfMapLock);
+ if (!apzc) {
+ return Nothing();
+ }
+
+ return Some(ScrollOffsetAndRange{
+ // FIXME: Use the one-frame delayed offset now. This doesn't take
+ // scroll-linked effets into accounts, so we have to fix this in the
+ // future.
+ apzc->GetCurrentAsyncVisualViewport(
+ AsyncPanZoomController::AsyncTransformConsumer::eForCompositing)
+ .TopLeft(),
+ apzc->GetCurrentScrollRangeInCssPixels()});
+}
+
+void APZSampler::AssertOnSamplerThread() const {
+ if (APZThreadUtils::GetThreadAssertionsEnabled()) {
+ MOZ_ASSERT(IsSamplerThread());
+ }
+}
+
+bool APZSampler::IsSamplerThread() const {
+ if (mIsUsingWebRender) {
+ // If the sampler thread id isn't set yet then we cannot be running on the
+ // sampler thread (because we will have the thread id before we run any
+ // other C++ code on it, and this function is only ever invoked from C++
+ // code), so return false in that scenario.
+ MutexAutoLock lock(mThreadIdLock);
+ return mSamplerThreadId && PlatformThread::CurrentId() == *mSamplerThreadId;
+ }
+ return CompositorThreadHolder::IsInCompositorThread();
+}
+
+/*static*/
+already_AddRefed<APZSampler> APZSampler::GetSampler(
+ const wr::WrWindowId& aWindowId) {
+ RefPtr<APZSampler> sampler;
+ StaticMutexAutoLock lock(sWindowIdLock);
+ if (sWindowIdMap) {
+ auto it = sWindowIdMap->find(wr::AsUint64(aWindowId));
+ if (it != sWindowIdMap->end()) {
+ sampler = it->second;
+ }
+ }
+ return sampler.forget();
+}
+
+} // namespace layers
+} // namespace mozilla
+
+void apz_register_sampler(mozilla::wr::WrWindowId aWindowId) {
+ mozilla::layers::APZSampler::SetSamplerThread(aWindowId);
+}
+
+void apz_sample_transforms(mozilla::wr::WrWindowId aWindowId,
+ const uint64_t* aGeneratedFrameId,
+ mozilla::wr::Transaction* aTransaction) {
+ mozilla::layers::APZSampler::SampleForWebRender(aWindowId, aGeneratedFrameId,
+ aTransaction);
+}
+
+void apz_deregister_sampler(mozilla::wr::WrWindowId aWindowId) {}
diff --git a/gfx/layers/apz/src/APZUpdater.cpp b/gfx/layers/apz/src/APZUpdater.cpp
new file mode 100644
index 0000000000..2bbad6e1a7
--- /dev/null
+++ b/gfx/layers/apz/src/APZUpdater.cpp
@@ -0,0 +1,546 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/layers/APZUpdater.h"
+
+#include "APZCTreeManager.h"
+#include "AsyncPanZoomController.h"
+#include "base/task.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/layers/APZThreadUtils.h"
+#include "mozilla/layers/CompositorThread.h"
+#include "mozilla/layers/SynchronousTask.h"
+#include "mozilla/layers/WebRenderScrollDataWrapper.h"
+#include "mozilla/webrender/WebRenderAPI.h"
+
+namespace mozilla {
+namespace layers {
+
+StaticMutex APZUpdater::sWindowIdLock;
+StaticAutoPtr<std::unordered_map<uint64_t, APZUpdater*>>
+ APZUpdater::sWindowIdMap;
+
+APZUpdater::APZUpdater(const RefPtr<APZCTreeManager>& aApz,
+ bool aConnectedToWebRender)
+ : mApz(aApz),
+ mDestroyed(false),
+ mConnectedToWebRender(aConnectedToWebRender),
+ mThreadIdLock("APZUpdater::ThreadIdLock"),
+ mQueueLock("APZUpdater::QueueLock") {
+ MOZ_ASSERT(aApz);
+ mApz->SetUpdater(this);
+}
+
+APZUpdater::~APZUpdater() {
+ mApz->SetUpdater(nullptr);
+
+ StaticMutexAutoLock lock(sWindowIdLock);
+ if (mWindowId) {
+ MOZ_ASSERT(sWindowIdMap);
+ // Ensure that ClearTree was called and the task got run
+ MOZ_ASSERT(sWindowIdMap->find(wr::AsUint64(*mWindowId)) ==
+ sWindowIdMap->end());
+ }
+}
+
+bool APZUpdater::HasTreeManager(const RefPtr<APZCTreeManager>& aApz) {
+ return aApz.get() == mApz.get();
+}
+
+void APZUpdater::SetWebRenderWindowId(const wr::WindowId& aWindowId) {
+ StaticMutexAutoLock lock(sWindowIdLock);
+ MOZ_ASSERT(!mWindowId);
+ mWindowId = Some(aWindowId);
+ if (!sWindowIdMap) {
+ sWindowIdMap = new std::unordered_map<uint64_t, APZUpdater*>();
+ NS_DispatchToMainThread(NS_NewRunnableFunction(
+ "APZUpdater::ClearOnShutdown", [] { ClearOnShutdown(&sWindowIdMap); }));
+ }
+ (*sWindowIdMap)[wr::AsUint64(aWindowId)] = this;
+}
+
+/*static*/
+void APZUpdater::SetUpdaterThread(const wr::WrWindowId& aWindowId) {
+ if (RefPtr<APZUpdater> updater = GetUpdater(aWindowId)) {
+ MutexAutoLock lock(updater->mThreadIdLock);
+ updater->mUpdaterThreadId = Some(PlatformThread::CurrentId());
+ }
+}
+
+// Takes a conditional lock!
+/*static*/
+void APZUpdater::PrepareForSceneSwap(const wr::WrWindowId& aWindowId)
+ MOZ_NO_THREAD_SAFETY_ANALYSIS {
+ if (RefPtr<APZUpdater> updater = GetUpdater(aWindowId)) {
+ updater->mApz->LockTree();
+ }
+}
+
+// Assumes we took a conditional lock!
+/*static*/
+void APZUpdater::CompleteSceneSwap(const wr::WrWindowId& aWindowId,
+ const wr::WrPipelineInfo& aInfo) {
+ RefPtr<APZUpdater> updater = GetUpdater(aWindowId);
+ if (!updater) {
+ // This should only happen in cases where PrepareForSceneSwap also got a
+ // null updater. No updater-thread tasks get run between PrepareForSceneSwap
+ // and this function, so there is no opportunity for the updater mapping
+ // to have gotten removed from sWindowIdMap in between the two calls.
+ return;
+ }
+ updater->mApz->mTreeLock.AssertCurrentThreadIn();
+
+ for (const auto& removedPipeline : aInfo.removed_pipelines) {
+ LayersId layersId = wr::AsLayersId(removedPipeline.pipeline_id);
+ updater->mEpochData.erase(layersId);
+ }
+ // Reset the built info for all pipelines, then put it back for the ones
+ // that got built in this scene swap.
+ for (auto& i : updater->mEpochData) {
+ i.second.mBuilt = Nothing();
+ }
+ for (const auto& epoch : aInfo.epochs) {
+ LayersId layersId = wr::AsLayersId(epoch.pipeline_id);
+ updater->mEpochData[layersId].mBuilt = Some(epoch.epoch);
+ }
+
+ // Run any tasks that got unblocked, then unlock the tree. The order is
+ // important because we want to run all the tasks up to and including the
+ // UpdateHitTestingTree calls corresponding to the built epochs, and we
+ // want to run those before we release the lock (i.e. atomically with the
+ // scene swap). This ensures that any hit-tests always encounter a consistent
+ // state between the APZ tree and the built scene in WR.
+ //
+ // While we could add additional information to the queued tasks to figure
+ // out the minimal set of tasks we want to run here, it's easier and harmless
+ // to just run all the queued and now-unblocked tasks inside the lock.
+ //
+ // Note that the ProcessQueue here might remove the window id -> APZUpdater
+ // mapping from sWindowIdMap, but we still unlock the tree successfully to
+ // leave things in a good state.
+ updater->ProcessQueue();
+
+ updater->mApz->UnlockTree();
+}
+
+/*static*/
+void APZUpdater::ProcessPendingTasks(const wr::WrWindowId& aWindowId) {
+ if (RefPtr<APZUpdater> updater = GetUpdater(aWindowId)) {
+ updater->ProcessQueue();
+ }
+}
+
+void APZUpdater::ClearTree(LayersId aRootLayersId) {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+ RefPtr<APZUpdater> self = this;
+ RunOnUpdaterThread(aRootLayersId,
+ NS_NewRunnableFunction("APZUpdater::ClearTree", [=]() {
+ self->mApz->ClearTree();
+ self->mDestroyed = true;
+
+ // Once ClearTree is called on the APZCTreeManager, we
+ // are in a shutdown phase. After this point it's ok if
+ // WebRender cannot get a hold of the updater via the
+ // window id, and it's a good point to remove the mapping
+ // and avoid leaving a dangling pointer to this object.
+ StaticMutexAutoLock lock(sWindowIdLock);
+ if (self->mWindowId) {
+ MOZ_ASSERT(sWindowIdMap);
+ sWindowIdMap->erase(wr::AsUint64(*(self->mWindowId)));
+ }
+ }));
+}
+
+void APZUpdater::UpdateFocusState(LayersId aRootLayerTreeId,
+ LayersId aOriginatingLayersId,
+ const FocusTarget& aFocusTarget) {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+ RunOnUpdaterThread(aOriginatingLayersId,
+ NewRunnableMethod<LayersId, LayersId, FocusTarget>(
+ "APZUpdater::UpdateFocusState", mApz,
+ &APZCTreeManager::UpdateFocusState, aRootLayerTreeId,
+ aOriginatingLayersId, aFocusTarget));
+}
+
+void APZUpdater::UpdateScrollDataAndTreeState(
+ LayersId aRootLayerTreeId, LayersId aOriginatingLayersId,
+ const wr::Epoch& aEpoch, WebRenderScrollData&& aScrollData) {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+ RefPtr<APZUpdater> self = this;
+ // Insert an epoch requirement update into the queue, so that
+ // tasks inserted into the queue after this point only get executed
+ // once the epoch requirement is satisfied. In particular, the
+ // UpdateHitTestingTree call below needs to wait until the epoch requirement
+ // is satisfied, which is why it is a separate task in the queue.
+ RunOnUpdaterThread(
+ aOriginatingLayersId,
+ NS_NewRunnableFunction("APZUpdater::UpdateEpochRequirement", [=]() {
+ if (aRootLayerTreeId == aOriginatingLayersId) {
+ self->mEpochData[aOriginatingLayersId].mIsRoot = true;
+ }
+ self->mEpochData[aOriginatingLayersId].mRequired = aEpoch;
+ }));
+ RunOnUpdaterThread(
+ aOriginatingLayersId,
+ NS_NewRunnableFunction(
+ "APZUpdater::UpdateHitTestingTree",
+ [=, aScrollData = std::move(aScrollData)]() mutable {
+ auto isFirstPaint = aScrollData.IsFirstPaint();
+ auto paintSequenceNumber = aScrollData.GetPaintSequenceNumber();
+
+ self->mScrollData[aOriginatingLayersId] = std::move(aScrollData);
+ auto root = self->mScrollData.find(aRootLayerTreeId);
+ if (root == self->mScrollData.end()) {
+ return;
+ }
+ self->mApz->UpdateHitTestingTree(
+ WebRenderScrollDataWrapper(*self, &(root->second)),
+ isFirstPaint, aOriginatingLayersId, paintSequenceNumber);
+ }));
+}
+
+void APZUpdater::UpdateScrollOffsets(LayersId aRootLayerTreeId,
+ LayersId aOriginatingLayersId,
+ ScrollUpdatesMap&& aUpdates,
+ uint32_t aPaintSequenceNumber) {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+ RefPtr<APZUpdater> self = this;
+ RunOnUpdaterThread(
+ aOriginatingLayersId,
+ NS_NewRunnableFunction(
+ "APZUpdater::UpdateScrollOffsets",
+ [=, updates = std::move(aUpdates)]() mutable {
+ self->mScrollData[aOriginatingLayersId].ApplyUpdates(
+ std::move(updates), aPaintSequenceNumber);
+ auto root = self->mScrollData.find(aRootLayerTreeId);
+ if (root == self->mScrollData.end()) {
+ return;
+ }
+ self->mApz->UpdateHitTestingTree(
+ WebRenderScrollDataWrapper(*self, &(root->second)),
+ /*isFirstPaint*/ false, aOriginatingLayersId,
+ aPaintSequenceNumber);
+ }));
+}
+
+void APZUpdater::NotifyLayerTreeAdopted(LayersId aLayersId,
+ const RefPtr<APZUpdater>& aOldUpdater) {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+ RunOnUpdaterThread(aLayersId,
+ NewRunnableMethod<LayersId, RefPtr<APZCTreeManager>>(
+ "APZUpdater::NotifyLayerTreeAdopted", mApz,
+ &APZCTreeManager::NotifyLayerTreeAdopted, aLayersId,
+ aOldUpdater ? aOldUpdater->mApz : nullptr));
+}
+
+void APZUpdater::NotifyLayerTreeRemoved(LayersId aLayersId) {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+ RefPtr<APZUpdater> self = this;
+ RunOnUpdaterThread(
+ aLayersId,
+ NS_NewRunnableFunction("APZUpdater::NotifyLayerTreeRemoved", [=]() {
+ self->mEpochData.erase(aLayersId);
+ self->mScrollData.erase(aLayersId);
+ self->mApz->NotifyLayerTreeRemoved(aLayersId);
+ }));
+}
+
+bool APZUpdater::GetAPZTestData(LayersId aLayersId, APZTestData* aOutData) {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+
+ RefPtr<APZCTreeManager> apz = mApz;
+ bool ret = false;
+ SynchronousTask waiter("APZUpdater::GetAPZTestData");
+ RunOnUpdaterThread(
+ aLayersId, NS_NewRunnableFunction("APZUpdater::GetAPZTestData", [&]() {
+ AutoCompleteTask notifier(&waiter);
+ ret = apz->GetAPZTestData(aLayersId, aOutData);
+ }));
+
+ // Wait until the task posted above has run and populated aOutData and ret
+ waiter.Wait();
+
+ return ret;
+}
+
+void APZUpdater::SetTestAsyncScrollOffset(
+ LayersId aLayersId, const ScrollableLayerGuid::ViewID& aScrollId,
+ const CSSPoint& aOffset) {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+ RefPtr<APZCTreeManager> apz = mApz;
+ RunOnUpdaterThread(
+ aLayersId,
+ NS_NewRunnableFunction("APZUpdater::SetTestAsyncScrollOffset", [=]() {
+ RefPtr<AsyncPanZoomController> apzc =
+ apz->GetTargetAPZC(aLayersId, aScrollId);
+ if (apzc) {
+ apzc->SetTestAsyncScrollOffset(aOffset);
+ } else {
+ NS_WARNING("Unable to find APZC in SetTestAsyncScrollOffset");
+ }
+ }));
+}
+
+void APZUpdater::SetTestAsyncZoom(LayersId aLayersId,
+ const ScrollableLayerGuid::ViewID& aScrollId,
+ const LayerToParentLayerScale& aZoom) {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+ RefPtr<APZCTreeManager> apz = mApz;
+ RunOnUpdaterThread(
+ aLayersId, NS_NewRunnableFunction("APZUpdater::SetTestAsyncZoom", [=]() {
+ RefPtr<AsyncPanZoomController> apzc =
+ apz->GetTargetAPZC(aLayersId, aScrollId);
+ if (apzc) {
+ apzc->SetTestAsyncZoom(aZoom);
+ } else {
+ NS_WARNING("Unable to find APZC in SetTestAsyncZoom");
+ }
+ }));
+}
+
+const WebRenderScrollData* APZUpdater::GetScrollData(LayersId aLayersId) const {
+ AssertOnUpdaterThread();
+ auto it = mScrollData.find(aLayersId);
+ return (it == mScrollData.end() ? nullptr : &(it->second));
+}
+
+void APZUpdater::AssertOnUpdaterThread() const {
+ if (APZThreadUtils::GetThreadAssertionsEnabled()) {
+ MOZ_ASSERT(IsUpdaterThread());
+ }
+}
+
+void APZUpdater::RunOnUpdaterThread(LayersId aLayersId,
+ already_AddRefed<Runnable> aTask) {
+ RefPtr<Runnable> task = aTask;
+
+ // In the scenario where IsConnectedToWebRender() is true, this function
+ // might get called early (before mUpdaterThreadId is set). In that case
+ // IsUpdaterThread() will return false and we'll queue the task onto
+ // mUpdaterQueue. This is fine; the task is still guaranteed to run (barring
+ // catastrophic failure) because the WakeSceneBuilder call will still trigger
+ // the callback to run tasks.
+
+ if (IsUpdaterThread()) {
+ // This function should only be called from the updater thread in test
+ // scenarios where we are not connected to WebRender. If it were called from
+ // the updater thread when we are connected to WebRender, running the task
+ // right away would be incorrect (we'd need to check that |aLayersId|
+ // isn't blocked, and if it is then enqueue the task instead).
+ MOZ_ASSERT(!IsConnectedToWebRender());
+ task->Run();
+ return;
+ }
+
+ if (IsConnectedToWebRender()) {
+ // If the updater thread is a WebRender thread, and we're not on it
+ // right now, save the task in the queue. We will run tasks from the queue
+ // during the callback from the updater thread, which we trigger by the
+ // call to WakeSceneBuilder.
+
+ bool sendWakeMessage = true;
+ { // scope lock
+ MutexAutoLock lock(mQueueLock);
+ for (const auto& queuedTask : mUpdaterQueue) {
+ if (queuedTask.mLayersId == aLayersId) {
+ // If there's already a task in the queue with this layers id, then
+ // we must have previously sent a WakeSceneBuilder message (when
+ // adding the first task with this layers id to the queue). Either
+ // that hasn't been fully processed yet, or the layers id is blocked
+ // waiting for an epoch - in either case there's no point in sending
+ // another WakeSceneBuilder message.
+ sendWakeMessage = false;
+ break;
+ }
+ }
+ mUpdaterQueue.push_back(QueuedTask{aLayersId, task});
+ }
+ if (sendWakeMessage) {
+ RefPtr<wr::WebRenderAPI> api = mApz->GetWebRenderAPI();
+ if (api) {
+ api->WakeSceneBuilder();
+ } else {
+ // Not sure if this can happen, but it might be possible. If it does,
+ // the task is in the queue, but if we didn't get a WebRenderAPI it
+ // might never run, or it might run later if we manage to get a
+ // WebRenderAPI later. For now let's just emit a warning, this can
+ // probably be upgraded to an assert later.
+ NS_WARNING("Possibly dropping task posted to updater thread");
+ }
+ }
+ return;
+ }
+
+ if (CompositorThread()) {
+ CompositorThread()->Dispatch(task.forget());
+ } else {
+ // Could happen during startup
+ NS_WARNING("Dropping task posted to updater thread");
+ }
+}
+
+bool APZUpdater::IsUpdaterThread() const {
+ if (IsConnectedToWebRender()) {
+ // If the updater thread id isn't set yet then we cannot be running on the
+ // updater thread (because we will have the thread id before we run any
+ // C++ code on it, and this function is only ever invoked from C++ code),
+ // so return false in that scenario.
+ MutexAutoLock lock(mThreadIdLock);
+ return mUpdaterThreadId && PlatformThread::CurrentId() == *mUpdaterThreadId;
+ }
+ return CompositorThreadHolder::IsInCompositorThread();
+}
+
+void APZUpdater::RunOnControllerThread(LayersId aLayersId,
+ already_AddRefed<Runnable> aTask) {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+
+ RefPtr<Runnable> task = aTask;
+
+ RunOnUpdaterThread(
+ aLayersId,
+ NewRunnableFunction("APZUpdater::RunOnControllerThread",
+ &APZThreadUtils::RunOnControllerThread,
+ std::move(task), nsIThread::DISPATCH_NORMAL));
+}
+
+bool APZUpdater::IsConnectedToWebRender() const {
+ return mConnectedToWebRender;
+}
+
+/*static*/
+already_AddRefed<APZUpdater> APZUpdater::GetUpdater(
+ const wr::WrWindowId& aWindowId) {
+ RefPtr<APZUpdater> updater;
+ StaticMutexAutoLock lock(sWindowIdLock);
+ if (sWindowIdMap) {
+ auto it = sWindowIdMap->find(wr::AsUint64(aWindowId));
+ if (it != sWindowIdMap->end()) {
+ updater = it->second;
+ }
+ }
+ return updater.forget();
+}
+
+void APZUpdater::ProcessQueue() {
+ MOZ_ASSERT(!mDestroyed);
+
+ { // scope lock to check for emptiness
+ MutexAutoLock lock(mQueueLock);
+ if (mUpdaterQueue.empty()) {
+ return;
+ }
+ }
+
+ std::deque<QueuedTask> blockedTasks;
+ while (true) {
+ QueuedTask task;
+
+ { // scope lock to extract a task
+ MutexAutoLock lock(mQueueLock);
+ if (mUpdaterQueue.empty()) {
+ // If we're done processing mUpdaterQueue, swap the tasks that are
+ // still blocked back in and finish
+ std::swap(mUpdaterQueue, blockedTasks);
+ break;
+ }
+ task = mUpdaterQueue.front();
+ mUpdaterQueue.pop_front();
+ }
+
+ // We check the task to see if it is blocked. Note that while this
+ // ProcessQueue function is executing, a particular layers id cannot go
+ // from blocked to unblocked, because only CompleteSceneSwap can unblock
+ // a layers id, and that also runs on the updater thread. If somehow
+ // a layers id gets unblocked while we're processing the queue, then it
+ // might result in tasks getting executed out of order.
+
+ auto it = mEpochData.find(task.mLayersId);
+ if (it != mEpochData.end() && it->second.IsBlocked()) {
+ // If this task is blocked, put it into the blockedTasks queue that
+ // we will replace mUpdaterQueue with
+ blockedTasks.push_back(task);
+ } else {
+ // Run and discard the task
+ task.mRunnable->Run();
+ }
+ }
+
+ if (mDestroyed) {
+ // If we get here, then we must have just run the ClearTree task for
+ // this updater. There might be tasks in the queue from content subtrees
+ // of this window that are blocked due to stale epochs. This can happen
+ // if the tasks were queued after the root pipeline was removed in
+ // WebRender, which prevents scene builds (and therefore prevents us
+ // from getting updated epochs via CompleteSceneSwap). See bug 1465658
+ // comment 43 for some more context.
+ // To avoid leaking these tasks, we discard the contents of the queue.
+ // This happens during window shutdown so if we don't run the tasks it's
+ // not going to matter much.
+ MutexAutoLock lock(mQueueLock);
+ if (!mUpdaterQueue.empty()) {
+ mUpdaterQueue.clear();
+ }
+ }
+}
+
+void APZUpdater::MarkAsDetached(LayersId aLayersId) {
+ mApz->MarkAsDetached(aLayersId);
+}
+
+APZUpdater::EpochState::EpochState() : mRequired{0}, mIsRoot(false) {}
+
+bool APZUpdater::EpochState::IsBlocked() const {
+ // The root is a special case because we basically assume it is "visible"
+ // even before it is built for the first time. This is because building the
+ // scene automatically makes it visible, and we need to make sure the APZ
+ // scroll data gets applied atomically with that happening.
+ //
+ // Layer subtrees on the other hand do not automatically become visible upon
+ // being built, because there must be a another layer tree update to change
+ // the visibility (i.e. an ancestor layer tree update that adds the necessary
+ // reflayer to complete the chain of reflayers).
+ //
+ // So in the case of non-visible subtrees, we know that no hit-test will
+ // actually end up hitting that subtree either before or after the scene swap,
+ // because the subtree will remain non-visible. That in turns means that we
+ // can apply the APZ scroll data for that subtree epoch before the scene is
+ // built, because it's not going to get used anyway. And that means we don't
+ // need to block the queue for non-visible subtrees. Which is a good thing,
+ // because in practice it seems like we often have non-visible subtrees sent
+ // to the compositor from content.
+ if (mIsRoot && !mBuilt) {
+ return true;
+ }
+ return mBuilt && (*mBuilt < mRequired);
+}
+
+} // namespace layers
+} // namespace mozilla
+
+// Rust callback implementations
+
+void apz_register_updater(mozilla::wr::WrWindowId aWindowId) {
+ mozilla::layers::APZUpdater::SetUpdaterThread(aWindowId);
+}
+
+void apz_pre_scene_swap(mozilla::wr::WrWindowId aWindowId) {
+ mozilla::layers::APZUpdater::PrepareForSceneSwap(aWindowId);
+}
+
+void apz_post_scene_swap(mozilla::wr::WrWindowId aWindowId,
+ const mozilla::wr::WrPipelineInfo* aInfo) {
+ mozilla::layers::APZUpdater::CompleteSceneSwap(aWindowId, *aInfo);
+}
+
+void apz_run_updater(mozilla::wr::WrWindowId aWindowId) {
+ mozilla::layers::APZUpdater::ProcessPendingTasks(aWindowId);
+}
+
+void apz_deregister_updater(mozilla::wr::WrWindowId aWindowId) {
+ // Run anything that's still left.
+ mozilla::layers::APZUpdater::ProcessPendingTasks(aWindowId);
+}
diff --git a/gfx/layers/apz/src/APZUtils.cpp b/gfx/layers/apz/src/APZUtils.cpp
new file mode 100644
index 0000000000..843046c34a
--- /dev/null
+++ b/gfx/layers/apz/src/APZUtils.cpp
@@ -0,0 +1,118 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/layers/APZUtils.h"
+
+#include "mozilla/StaticPrefs_apz.h"
+#include "mozilla/StaticPrefs_layers.h"
+
+namespace mozilla {
+namespace layers {
+
+namespace apz {
+
+bool IsCloseToHorizontal(float aAngle, float aThreshold) {
+ return (aAngle < aThreshold || aAngle > (M_PI - aThreshold));
+}
+
+bool IsCloseToVertical(float aAngle, float aThreshold) {
+ return (fabs(aAngle - (M_PI / 2)) < aThreshold);
+}
+
+bool IsStuckAtBottom(gfxFloat aTranslation,
+ const LayerRectAbsolute& aInnerRange,
+ const LayerRectAbsolute& aOuterRange) {
+ // The item will be stuck at the bottom if the async scroll delta is in
+ // the range [aOuterRange.Y(), aInnerRange.Y()]. Since the translation
+ // is negated with repect to the async scroll delta (i.e. scrolling down
+ // produces a positive scroll delta and negative translation), we invert it
+ // and check to see if it falls in the specified range.
+ return aOuterRange.Y() <= -aTranslation && -aTranslation <= aInnerRange.Y();
+}
+
+bool IsStuckAtTop(gfxFloat aTranslation, const LayerRectAbsolute& aInnerRange,
+ const LayerRectAbsolute& aOuterRange) {
+ // Same as IsStuckAtBottom, except we want to check for the range
+ // [aInnerRange.YMost(), aOuterRange.YMost()].
+ return aInnerRange.YMost() <= -aTranslation &&
+ -aTranslation <= aOuterRange.YMost();
+}
+
+ScreenPoint ComputeFixedMarginsOffset(
+ const ScreenMargin& aCompositorFixedLayerMargins, SideBits aFixedSides,
+ const ScreenMargin& aGeckoFixedLayerMargins) {
+ // Work out the necessary translation, in screen space.
+ ScreenPoint translation;
+
+ ScreenMargin effectiveMargin =
+ aCompositorFixedLayerMargins - aGeckoFixedLayerMargins;
+ if ((aFixedSides & SideBits::eLeftRight) == SideBits::eLeftRight) {
+ translation.x += (effectiveMargin.left - effectiveMargin.right) / 2;
+ } else if (aFixedSides & SideBits::eRight) {
+ translation.x -= effectiveMargin.right;
+ } else if (aFixedSides & SideBits::eLeft) {
+ translation.x += effectiveMargin.left;
+ }
+
+ if ((aFixedSides & SideBits::eTopBottom) == SideBits::eTopBottom) {
+ translation.y += (effectiveMargin.top - effectiveMargin.bottom) / 2;
+ } else if (aFixedSides & SideBits::eBottom) {
+ translation.y -= effectiveMargin.bottom;
+ } else if (aFixedSides & SideBits::eTop) {
+ translation.y += effectiveMargin.top;
+ }
+
+ return translation;
+}
+
+bool AboutToCheckerboard(const FrameMetrics& aPaintedMetrics,
+ const FrameMetrics& aCompositorMetrics) {
+ // The main-thread code to compute the painted area can introduce some
+ // rounding error due to multiple unit conversions, so we inflate the rect by
+ // one app unit to account for that.
+ CSSRect painted = aPaintedMetrics.GetDisplayPort() +
+ aPaintedMetrics.GetLayoutScrollOffset();
+ painted.Inflate(CSSMargin::FromAppUnits(nsMargin(1, 1, 1, 1)));
+
+ // Inflate the rect by the danger zone. See the description of the danger zone
+ // prefs in AsyncPanZoomController.cpp for an explanation of this.
+ CSSRect visible =
+ CSSRect(aCompositorMetrics.GetVisualScrollOffset(),
+ aCompositorMetrics.CalculateBoundedCompositedSizeInCssPixels());
+ visible.Inflate(ScreenSize(StaticPrefs::apz_danger_zone_x(),
+ StaticPrefs::apz_danger_zone_y()) /
+ aCompositorMetrics.DisplayportPixelsPerCSSPixel());
+
+ // Clamp both rects to the scrollable rect, because having either of those
+ // exceed the scrollable rect doesn't make sense, and could lead to false
+ // positives.
+ painted = painted.Intersect(aPaintedMetrics.GetScrollableRect());
+ visible = visible.Intersect(aPaintedMetrics.GetScrollableRect());
+
+ return !painted.Contains(visible);
+}
+
+SideBits GetOverscrollSideBits(const ParentLayerPoint& aOverscrollAmount) {
+ SideBits sides = SideBits::eNone;
+
+ if (aOverscrollAmount.x < 0) {
+ sides |= SideBits::eLeft;
+ } else if (aOverscrollAmount.x > 0) {
+ sides |= SideBits::eRight;
+ }
+
+ if (aOverscrollAmount.y < 0) {
+ sides |= SideBits::eTop;
+ } else if (aOverscrollAmount.y > 0) {
+ sides |= SideBits::eBottom;
+ }
+
+ return sides;
+}
+
+} // namespace apz
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/APZUtils.h b/gfx/layers/apz/src/APZUtils.h
new file mode 100644
index 0000000000..8adaa71339
--- /dev/null
+++ b/gfx/layers/apz/src/APZUtils.h
@@ -0,0 +1,220 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_APZUtils_h
+#define mozilla_layers_APZUtils_h
+
+// This file is for APZ-related utilities that are used by code in gfx/layers
+// only. For APZ-related utilities used by the Rest of the World (widget/,
+// layout/, dom/, IPDL protocols, etc.), use APZPublicUtils.h.
+// Do not include this header from source files outside of gfx/layers.
+
+#include <stdint.h> // for uint32_t
+#include <type_traits>
+#include "gfxTypes.h"
+#include "FrameMetrics.h"
+#include "LayersTypes.h"
+#include "UnitTransforms.h"
+#include "mozilla/gfx/CompositorHitTestInfo.h"
+#include "mozilla/gfx/Point.h"
+#include "mozilla/DefineEnum.h"
+#include "mozilla/EnumSet.h"
+#include "mozilla/FloatingPoint.h"
+
+namespace mozilla {
+
+namespace layers {
+
+enum CancelAnimationFlags : uint32_t {
+ Default = 0x0, /* Cancel all animations */
+ ExcludeOverscroll = 0x1, /* Don't clear overscroll */
+ ScrollSnap = 0x2, /* Snap to snap points */
+ ExcludeWheel = 0x4, /* Don't stop wheel smooth-scroll animations */
+ TriggeredExternally = 0x8, /* Cancellation was not triggered by APZ in
+ response to an input event */
+};
+
+inline CancelAnimationFlags operator|(CancelAnimationFlags a,
+ CancelAnimationFlags b) {
+ return static_cast<CancelAnimationFlags>(static_cast<int>(a) |
+ static_cast<int>(b));
+}
+
+// clang-format off
+enum class ScrollSource {
+ // Touch-screen.
+ Touchscreen,
+
+ // Touchpad with gesture support.
+ Touchpad,
+
+ // Mouse wheel.
+ Wheel,
+
+ // Keyboard
+ Keyboard,
+};
+// clang-format on
+
+inline bool ScrollSourceRespectsDisregardedDirections(ScrollSource aSource) {
+ return aSource == ScrollSource::Wheel || aSource == ScrollSource::Touchpad;
+}
+
+inline bool ScrollSourceAllowsOverscroll(ScrollSource aSource) {
+ return aSource == ScrollSource::Touchpad ||
+ aSource == ScrollSource::Touchscreen;
+}
+
+// Epsilon to be used when comparing 'float' coordinate values
+// with FuzzyEqualsAdditive. The rationale is that 'float' has 7 decimal
+// digits of precision, and coordinate values should be no larger than in the
+// ten thousands. Note also that the smallest legitimate difference in page
+// coordinates is 1 app unit, which is 1/60 of a (CSS pixel), so this epsilon
+// isn't too large.
+const CSSCoord COORDINATE_EPSILON = 0.01f;
+
+inline bool IsZero(const CSSPoint& aPoint) {
+ return FuzzyEqualsAdditive(aPoint.x, CSSCoord(), COORDINATE_EPSILON) &&
+ FuzzyEqualsAdditive(aPoint.y, CSSCoord(), COORDINATE_EPSILON);
+}
+
+// Represents async transforms consisting of a scale and a translation.
+struct AsyncTransform {
+ explicit AsyncTransform(
+ LayerToParentLayerScale aScale = LayerToParentLayerScale(),
+ ParentLayerPoint aTranslation = ParentLayerPoint())
+ : mScale(aScale), mTranslation(aTranslation) {}
+
+ operator AsyncTransformComponentMatrix() const {
+ return AsyncTransformComponentMatrix::Scaling(mScale.scale, mScale.scale, 1)
+ .PostTranslate(mTranslation.x, mTranslation.y, 0);
+ }
+
+ bool operator==(const AsyncTransform& rhs) const {
+ return mTranslation == rhs.mTranslation && mScale == rhs.mScale;
+ }
+
+ bool operator!=(const AsyncTransform& rhs) const { return !(*this == rhs); }
+
+ LayerToParentLayerScale mScale;
+ ParentLayerPoint mTranslation;
+};
+
+// Deem an AsyncTransformComponentMatrix (obtained by multiplying together
+// one or more AsyncTransformComponentMatrix objects) as constituting a
+// complete async transform.
+inline AsyncTransformMatrix CompleteAsyncTransform(
+ const AsyncTransformComponentMatrix& aMatrix) {
+ return ViewAs<AsyncTransformMatrix>(
+ aMatrix, PixelCastJustification::MultipleAsyncTransforms);
+}
+
+struct TargetConfirmationFlags final {
+ explicit TargetConfirmationFlags(bool aTargetConfirmed)
+ : mTargetConfirmed(aTargetConfirmed),
+ mRequiresTargetConfirmation(false),
+ mHitScrollbar(false),
+ mHitScrollThumb(false),
+ mDispatchToContent(false) {}
+
+ explicit TargetConfirmationFlags(
+ const gfx::CompositorHitTestInfo& aHitTestInfo)
+ : mTargetConfirmed(
+ (aHitTestInfo != gfx::CompositorHitTestInvisibleToHit) &&
+ (aHitTestInfo & gfx::CompositorHitTestDispatchToContent).isEmpty()),
+ mRequiresTargetConfirmation(aHitTestInfo.contains(
+ gfx::CompositorHitTestFlags::eRequiresTargetConfirmation)),
+ mHitScrollbar(
+ aHitTestInfo.contains(gfx::CompositorHitTestFlags::eScrollbar)),
+ mHitScrollThumb(aHitTestInfo.contains(
+ gfx::CompositorHitTestFlags::eScrollbarThumb)),
+ mDispatchToContent(
+ !(aHitTestInfo & gfx::CompositorHitTestDispatchToContent)
+ .isEmpty()) {}
+
+ bool mTargetConfirmed : 1;
+ bool mRequiresTargetConfirmation : 1;
+ bool mHitScrollbar : 1;
+ bool mHitScrollThumb : 1;
+ bool mDispatchToContent : 1;
+};
+
+enum class AsyncTransformComponent { eLayout, eVisual };
+
+using AsyncTransformComponents = EnumSet<AsyncTransformComponent>;
+
+constexpr AsyncTransformComponents LayoutAndVisual(
+ AsyncTransformComponent::eLayout, AsyncTransformComponent::eVisual);
+
+/**
+ * Metrics that GeckoView wants to know at every composite.
+ * These are the effective visual scroll offset and zoom level of
+ * the root content APZC at composition time.
+ */
+struct GeckoViewMetrics {
+ CSSPoint mVisualScrollOffset;
+ CSSToParentLayerScale mZoom;
+};
+
+namespace apz {
+
+/**
+ * Is aAngle within the given threshold of the horizontal axis?
+ * @param aAngle an angle in radians in the range [0, pi]
+ * @param aThreshold an angle in radians in the range [0, pi/2]
+ */
+bool IsCloseToHorizontal(float aAngle, float aThreshold);
+
+// As above, but for the vertical axis.
+bool IsCloseToVertical(float aAngle, float aThreshold);
+
+// Returns true if a sticky layer with async translation |aTranslation| is
+// stuck with a bottom margin. The inner/outer ranges are produced by the main
+// thread at the last paint, and so |aTranslation| only needs to be the
+// async translation from the last paint.
+bool IsStuckAtBottom(gfxFloat aTranslation,
+ const LayerRectAbsolute& aInnerRange,
+ const LayerRectAbsolute& aOuterRange);
+
+// Returns true if a sticky layer with async translation |aTranslation| is
+// stuck with a top margin.
+bool IsStuckAtTop(gfxFloat aTranslation, const LayerRectAbsolute& aInnerRange,
+ const LayerRectAbsolute& aOuterRange);
+
+/**
+ * Compute the translation that should be applied to a layer that's fixed
+ * at |eFixedSides|, to respect the fixed layer margins |aFixedMargins|.
+ */
+ScreenPoint ComputeFixedMarginsOffset(
+ const ScreenMargin& aCompositorFixedLayerMargins, SideBits aFixedSides,
+ const ScreenMargin& aGeckoFixedLayerMargins);
+
+/**
+ * Takes the visible rect from the compositor metrics, adds a pref-based
+ * margin around it, and checks to see if it is contained inside the painted
+ * rect from the painted metrics. Returns true if it is contained, or false
+ * if not. Returning false means that a (relatively) small amount of async
+ * scrolling/zooming can result in the visible area going outside the painted
+ * area and resulting in visual checkerboarding.
+ * Note that this may return false positives for cases where the scrollframe
+ * in question is nested inside other scrollframes, as the composition bounds
+ * used to determine the visible rect may in fact be clipped by enclosing
+ * scrollframes, but that is not accounted for in this function.
+ */
+bool AboutToCheckerboard(const FrameMetrics& aPaintedMetrics,
+ const FrameMetrics& aCompositorMetrics);
+
+/**
+ * Returns SideBits where the given |aOverscrollAmount| overscrolls.
+ */
+SideBits GetOverscrollSideBits(const ParentLayerPoint& aOverscrollAmount);
+
+} // namespace apz
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_APZUtils_h
diff --git a/gfx/layers/apz/src/AndroidAPZ.cpp b/gfx/layers/apz/src/AndroidAPZ.cpp
new file mode 100644
index 0000000000..4895c893de
--- /dev/null
+++ b/gfx/layers/apz/src/AndroidAPZ.cpp
@@ -0,0 +1,36 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "AndroidAPZ.h"
+
+#include "AndroidFlingPhysics.h"
+#include "AndroidVelocityTracker.h"
+#include "AsyncPanZoomController.h"
+#include "GenericFlingAnimation.h"
+#include "OverscrollHandoffState.h"
+
+namespace mozilla {
+namespace layers {
+
+AsyncPanZoomAnimation* AndroidSpecificState::CreateFlingAnimation(
+ AsyncPanZoomController& aApzc, const FlingHandoffState& aHandoffState,
+ float aPLPPI) {
+ return new GenericFlingAnimation<AndroidFlingPhysics>(aApzc, aHandoffState,
+ aPLPPI);
+}
+
+UniquePtr<VelocityTracker> AndroidSpecificState::CreateVelocityTracker(
+ Axis* aAxis) {
+ return MakeUnique<AndroidVelocityTracker>();
+}
+
+/* static */
+void AndroidSpecificState::InitializeGlobalState() {
+ AndroidFlingPhysics::InitializeGlobalState();
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/AndroidAPZ.h b/gfx/layers/apz/src/AndroidAPZ.h
new file mode 100644
index 0000000000..ab30b4e612
--- /dev/null
+++ b/gfx/layers/apz/src/AndroidAPZ.h
@@ -0,0 +1,34 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_AndroidAPZ_h_
+#define mozilla_layers_AndroidAPZ_h_
+
+#include "AsyncPanZoomAnimation.h"
+#include "AsyncPanZoomController.h"
+
+namespace mozilla {
+namespace layers {
+
+class AndroidSpecificState : public PlatformSpecificStateBase {
+ public:
+ virtual AndroidSpecificState* AsAndroidSpecificState() override {
+ return this;
+ }
+
+ virtual AsyncPanZoomAnimation* CreateFlingAnimation(
+ AsyncPanZoomController& aApzc, const FlingHandoffState& aHandoffState,
+ float aPLPPI) override;
+ virtual UniquePtr<VelocityTracker> CreateVelocityTracker(
+ Axis* aAxis) override;
+
+ static void InitializeGlobalState();
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_AndroidAPZ_h_
diff --git a/gfx/layers/apz/src/AndroidFlingPhysics.cpp b/gfx/layers/apz/src/AndroidFlingPhysics.cpp
new file mode 100644
index 0000000000..d18f4be4d4
--- /dev/null
+++ b/gfx/layers/apz/src/AndroidFlingPhysics.cpp
@@ -0,0 +1,218 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "AndroidFlingPhysics.h"
+
+#include <cmath>
+
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/StaticPrefs_apz.h"
+#include "mozilla/StaticPtr.h"
+
+namespace mozilla {
+namespace layers {
+
+// The fling physics calculations implemented here are adapted from
+// Chrome's implementation of fling physics on Android:
+// https://cs.chromium.org/chromium/src/ui/events/android/scroller.cc?rcl=3ae3aaff927038a5c644926842cb0c31dea60c79
+
+static double ComputeDeceleration(float aDPI) {
+ const float kFriction = 0.84f;
+ const float kGravityEarth = 9.80665f;
+ return kGravityEarth // g (m/s^2)
+ * 39.37f // inch/meter
+ * aDPI // pixels/inch
+ * kFriction;
+}
+
+// == std::log(0.78f) / std::log(0.9f)
+const float kDecelerationRate = 2.3582018f;
+
+// Default friction constant in android.view.ViewConfiguration.
+static float GetFlingFriction() {
+ return StaticPrefs::apz_android_chrome_fling_physics_friction();
+}
+
+// Tension lines cross at (GetInflexion(), 1).
+static float GetInflexion() {
+ // Clamp the inflexion to the range [0,1]. Values outside of this range
+ // do not make sense in the physics model, and for negative values the
+ // approximation used to compute the spline curve does not converge.
+ const float inflexion =
+ StaticPrefs::apz_android_chrome_fling_physics_inflexion();
+ if (inflexion < 0.0f) {
+ return 0.0f;
+ }
+ if (inflexion > 1.0f) {
+ return 1.0f;
+ }
+ return inflexion;
+}
+
+// Fling scroll is stopped when the scroll position is |kThresholdForFlingEnd|
+// pixels or closer from the end.
+static float GetThresholdForFlingEnd() {
+ return StaticPrefs::apz_android_chrome_fling_physics_stop_threshold();
+}
+
+static double ComputeSplineDeceleration(ParentLayerCoord aVelocity,
+ double aTuningCoeff) {
+ float velocityPerSec = aVelocity * 1000.0f;
+ return std::log(GetInflexion() * velocityPerSec /
+ (GetFlingFriction() * aTuningCoeff));
+}
+
+static TimeDuration ComputeFlingDuration(ParentLayerCoord aVelocity,
+ double aTuningCoeff) {
+ const double splineDecel = ComputeSplineDeceleration(aVelocity, aTuningCoeff);
+ const double timeSeconds = std::exp(splineDecel / (kDecelerationRate - 1.0));
+ return TimeDuration::FromSeconds(timeSeconds);
+}
+
+static ParentLayerCoord ComputeFlingDistance(ParentLayerCoord aVelocity,
+ double aTuningCoeff) {
+ const double splineDecel = ComputeSplineDeceleration(aVelocity, aTuningCoeff);
+ return GetFlingFriction() * aTuningCoeff *
+ std::exp(kDecelerationRate / (kDecelerationRate - 1.0) * splineDecel);
+}
+
+struct SplineConstants {
+ public:
+ SplineConstants() {
+ const float kStartTension = 0.5f;
+ const float kEndTension = 1.0f;
+ const float kP1 = kStartTension * GetInflexion();
+ const float kP2 = 1.0f - kEndTension * (1.0f - GetInflexion());
+
+ float xMin = 0.0f;
+ for (int i = 0; i < kNumSamples; i++) {
+ const float alpha = static_cast<float>(i) / kNumSamples;
+
+ float xMax = 1.0f;
+ float x, tx, coef;
+ // While the inflexion can be overridden by the user, it's clamped to
+ // [0,1]. For values in this range, the approximation algorithm below
+ // should converge in < 20 iterations. For good measure, we impose an
+ // iteration limit as well.
+ static const int sIterationLimit = 100;
+ int iterations = 0;
+ while (iterations++ < sIterationLimit) {
+ x = xMin + (xMax - xMin) / 2.0f;
+ coef = 3.0f * x * (1.0f - x);
+ tx = coef * ((1.0f - x) * kP1 + x * kP2) + x * x * x;
+ if (FuzzyEqualsAdditive(tx, alpha)) {
+ break;
+ }
+ if (tx > alpha) {
+ xMax = x;
+ } else {
+ xMin = x;
+ }
+ }
+ mSplinePositions[i] = coef * ((1.0f - x) * kStartTension + x) + x * x * x;
+ }
+ mSplinePositions[kNumSamples] = 1.0f;
+ }
+
+ void CalculateCoefficients(float aTime, float* aOutDistanceCoef,
+ float* aOutVelocityCoef) {
+ *aOutDistanceCoef = 1.0f;
+ *aOutVelocityCoef = 0.0f;
+ const int index = static_cast<int>(kNumSamples * aTime);
+ if (index < kNumSamples) {
+ const float tInf = static_cast<float>(index) / kNumSamples;
+ const float dInf = mSplinePositions[index];
+ const float tSup = static_cast<float>(index + 1) / kNumSamples;
+ const float dSup = mSplinePositions[index + 1];
+ *aOutVelocityCoef = (dSup - dInf) / (tSup - tInf);
+ *aOutDistanceCoef = dInf + (aTime - tInf) * *aOutVelocityCoef;
+ }
+ }
+
+ private:
+ static const int kNumSamples = 100;
+ float mSplinePositions[kNumSamples + 1];
+};
+
+StaticAutoPtr<SplineConstants> gSplineConstants;
+
+/* static */
+void AndroidFlingPhysics::InitializeGlobalState() {
+ gSplineConstants = new SplineConstants();
+ ClearOnShutdown(&gSplineConstants);
+}
+
+void AndroidFlingPhysics::Init(const ParentLayerPoint& aStartingVelocity,
+ float aPLPPI) {
+ mVelocity = aStartingVelocity.Length();
+ // We should not have created a fling animation if there is no velocity.
+ MOZ_ASSERT(mVelocity != 0.0f);
+ const double tuningCoeff = ComputeDeceleration(aPLPPI);
+ mTargetDuration = ComputeFlingDuration(mVelocity, tuningCoeff);
+ MOZ_ASSERT(!mTargetDuration.IsZero());
+ mDurationSoFar = TimeDuration();
+ mLastPos = ParentLayerPoint();
+ mCurrentPos = ParentLayerPoint();
+ float coeffX =
+ mVelocity == 0 ? 1.0f : aStartingVelocity.x.value / mVelocity.value;
+ float coeffY =
+ mVelocity == 0 ? 1.0f : aStartingVelocity.y.value / mVelocity.value;
+ mTargetDistance = ComputeFlingDistance(mVelocity, tuningCoeff);
+ mTargetPos =
+ ParentLayerPoint(mTargetDistance * coeffX, mTargetDistance * coeffY);
+ const float hyp = mTargetPos.Length();
+ if (FuzzyEqualsAdditive(hyp, 0.0f)) {
+ mDeltaNorm = ParentLayerPoint(1, 1);
+ } else {
+ mDeltaNorm = ParentLayerPoint(mTargetPos.x / hyp, mTargetPos.y / hyp);
+ }
+}
+void AndroidFlingPhysics::Sample(const TimeDuration& aDelta,
+ ParentLayerPoint* aOutVelocity,
+ ParentLayerPoint* aOutOffset) {
+ float newVelocity;
+ if (SampleImpl(aDelta, &newVelocity)) {
+ *aOutOffset = (mCurrentPos - mLastPos);
+ *aOutVelocity = ParentLayerPoint(mDeltaNorm.x * newVelocity,
+ mDeltaNorm.y * newVelocity);
+ mLastPos = mCurrentPos;
+ } else {
+ *aOutOffset = (mTargetPos - mLastPos);
+ *aOutVelocity = ParentLayerPoint();
+ }
+}
+
+bool AndroidFlingPhysics::SampleImpl(const TimeDuration& aDelta,
+ float* aOutVelocity) {
+ mDurationSoFar += aDelta;
+ if (mDurationSoFar >= mTargetDuration) {
+ return false;
+ }
+
+ const float timeRatio =
+ mDurationSoFar.ToSeconds() / mTargetDuration.ToSeconds();
+ float distanceCoef = 1.0f;
+ float velocityCoef = 0.0f;
+ gSplineConstants->CalculateCoefficients(timeRatio, &distanceCoef,
+ &velocityCoef);
+
+ // The caller expects the velocity in pixels per _millisecond_.
+ *aOutVelocity =
+ velocityCoef * mTargetDistance / mTargetDuration.ToMilliseconds();
+
+ mCurrentPos = mTargetPos * distanceCoef;
+
+ ParentLayerPoint remainder = mTargetPos - mCurrentPos;
+ const float threshold = GetThresholdForFlingEnd();
+ if (fabsf(remainder.x) < threshold && fabsf(remainder.y) < threshold) {
+ return false;
+ }
+
+ return true;
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/AndroidFlingPhysics.h b/gfx/layers/apz/src/AndroidFlingPhysics.h
new file mode 100644
index 0000000000..68fb53e804
--- /dev/null
+++ b/gfx/layers/apz/src/AndroidFlingPhysics.h
@@ -0,0 +1,45 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_AndroidFlingPhysics_h_
+#define mozilla_layers_AndroidFlingPhysics_h_
+
+#include "AsyncPanZoomController.h"
+#include "Units.h"
+#include "mozilla/Assertions.h"
+
+namespace mozilla {
+namespace layers {
+
+class AndroidFlingPhysics {
+ public:
+ void Init(const ParentLayerPoint& aVelocity, float aPLPPI);
+ void Sample(const TimeDuration& aDelta, ParentLayerPoint* aOutVelocity,
+ ParentLayerPoint* aOutOffset);
+
+ static void InitializeGlobalState();
+
+ private:
+ // Returns false if the animation should end.
+ bool SampleImpl(const TimeDuration& aDelta, float* aOutVelocity);
+
+ // Information pertaining to the current fling.
+ // This is initialized on each call to Init().
+ ParentLayerCoord mVelocity; // diagonal velocity (length of velocity vector)
+ TimeDuration mTargetDuration;
+ TimeDuration mDurationSoFar;
+ ParentLayerPoint mLastPos;
+ ParentLayerPoint mCurrentPos;
+ ParentLayerCoord mTargetDistance; // diagonal distance
+ ParentLayerPoint mTargetPos; // really a target *offset* relative to the
+ // start position, which we don't track
+ ParentLayerPoint mDeltaNorm; // mTargetPos with length normalized to 1
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_AndroidFlingPhysics_h_
diff --git a/gfx/layers/apz/src/AndroidVelocityTracker.cpp b/gfx/layers/apz/src/AndroidVelocityTracker.cpp
new file mode 100644
index 0000000000..a355811a00
--- /dev/null
+++ b/gfx/layers/apz/src/AndroidVelocityTracker.cpp
@@ -0,0 +1,288 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "AndroidVelocityTracker.h"
+
+#include "mozilla/StaticPrefs_apz.h"
+
+namespace mozilla {
+namespace layers {
+
+// This velocity tracker implementation was adapted from Chromium's
+// second-order unweighted least-squares velocity tracker strategy
+// (https://cs.chromium.org/chromium/src/ui/events/gesture_detection/velocity_tracker.cc?l=101&rcl=9ea9a086d4f54c702ec9a38e55fb3eb8bbc2401b).
+
+// Threshold between position updates for determining that a pointer has
+// stopped moving. Some input devices do not send move events in the
+// case where a pointer has stopped. We need to detect this case so that we can
+// accurately predict the velocity after the pointer starts moving again.
+static const TimeDuration kAssumePointerMoveStoppedTime =
+ TimeDuration::FromMilliseconds(40);
+
+// The degree of the approximation.
+static const uint8_t kDegree = 2;
+
+// The degree of the polynomial used in SolveLeastSquares().
+// This should be the degree of the approximation plus one.
+static const uint8_t kPolyDegree = kDegree + 1;
+
+// Maximum size of position history.
+static const uint8_t kHistorySize = 20;
+
+AndroidVelocityTracker::AndroidVelocityTracker() {}
+
+void AndroidVelocityTracker::StartTracking(ParentLayerCoord aPos,
+ TimeStamp aTimestamp) {
+ Clear();
+ mHistory.AppendElement(std::make_pair(aTimestamp, aPos));
+ mLastEventTime = aTimestamp;
+}
+
+Maybe<float> AndroidVelocityTracker::AddPosition(ParentLayerCoord aPos,
+ TimeStamp aTimestamp) {
+ if ((aTimestamp - mLastEventTime) >= kAssumePointerMoveStoppedTime) {
+ Clear();
+ }
+
+ if ((aTimestamp - mLastEventTime).ToMilliseconds() < 1.0) {
+ // If we get a sample within a millisecond of the previous one,
+ // just update its position. Two samples in the history with the
+ // same timestamp can lead to things like infinite velocities.
+ if (mHistory.Length() > 0) {
+ mHistory[mHistory.Length() - 1].second = aPos;
+ }
+ } else {
+ mHistory.AppendElement(std::make_pair(aTimestamp, aPos));
+ if (mHistory.Length() > kHistorySize) {
+ mHistory.RemoveElementAt(0);
+ }
+ }
+
+ mLastEventTime = aTimestamp;
+
+ if (mHistory.Length() < 2) {
+ return Nothing();
+ }
+
+ auto start = mHistory[mHistory.Length() - 2];
+ auto end = mHistory[mHistory.Length() - 1];
+ auto velocity =
+ (end.second - start.second) / (end.first - start.first).ToMilliseconds();
+ // The velocity needs to be negated because the positions represent
+ // touch positions, and the direction of scrolling is opposite to the
+ // direction of the finger's movement.
+ return Some(-velocity);
+}
+
+static float VectorDot(const float* a, const float* b, uint32_t m) {
+ float r = 0;
+ while (m--) {
+ r += *(a++) * *(b++);
+ }
+ return r;
+}
+
+static float VectorNorm(const float* a, uint32_t m) {
+ float r = 0;
+ while (m--) {
+ float t = *(a++);
+ r += t * t;
+ }
+ return sqrtf(r);
+}
+
+/**
+ * Solves a linear least squares problem to obtain a N degree polynomial that
+ * fits the specified input data as nearly as possible.
+ *
+ * Returns true if a solution is found, false otherwise.
+ *
+ * The input consists of two vectors of data points X and Y with indices 0..m-1
+ * along with a weight vector W of the same size.
+ *
+ * The output is a vector B with indices 0..n that describes a polynomial
+ * that fits the data, such the sum of W[i] * W[i] * abs(Y[i] - (B[0] + B[1]
+ * X[i] * + B[2] X[i]^2 ... B[n] X[i]^n)) for all i between 0 and m-1 is
+ * minimized.
+ *
+ * Accordingly, the weight vector W should be initialized by the caller with the
+ * reciprocal square root of the variance of the error in each input data point.
+ * In other words, an ideal choice for W would be W[i] = 1 / var(Y[i]) = 1 /
+ * stddev(Y[i]).
+ * The weights express the relative importance of each data point. If the
+ * weights are* all 1, then the data points are considered to be of equal
+ * importance when fitting the polynomial. It is a good idea to choose weights
+ * that diminish the importance of data points that may have higher than usual
+ * error margins.
+ *
+ * Errors among data points are assumed to be independent. W is represented
+ * here as a vector although in the literature it is typically taken to be a
+ * diagonal matrix.
+ *
+ * That is to say, the function that generated the input data can be
+ * approximated by y(x) ~= B[0] + B[1] x + B[2] x^2 + ... + B[n] x^n.
+ *
+ * The coefficient of determination (R^2) is also returned to describe the
+ * goodness of fit of the model for the given data. It is a value between 0
+ * and 1, where 1 indicates perfect correspondence.
+ *
+ * This function first expands the X vector to a m by n matrix A such that
+ * A[i][0] = 1, A[i][1] = X[i], A[i][2] = X[i]^2, ..., A[i][n] = X[i]^n, then
+ * multiplies it by w[i].
+ *
+ * Then it calculates the QR decomposition of A yielding an m by m orthonormal
+ * matrix Q and an m by n upper triangular matrix R. Because R is upper
+ * triangular (lower part is all zeroes), we can simplify the decomposition into
+ * an m by n matrix Q1 and a n by n matrix R1 such that A = Q1 R1.
+ *
+ * Finally we solve the system of linear equations given by
+ * R1 B = (Qtranspose W Y) to find B.
+ *
+ * For efficiency, we lay out A and Q column-wise in memory because we
+ * frequently operate on the column vectors. Conversely, we lay out R row-wise.
+ *
+ * http://en.wikipedia.org/wiki/Numerical_methods_for_linear_least_squares
+ * http://en.wikipedia.org/wiki/Gram-Schmidt
+ */
+static bool SolveLeastSquares(const float* x, const float* y, const float* w,
+ uint32_t m, uint32_t n, float* out_b) {
+ // MSVC does not support variable-length arrays (used by the original Android
+ // implementation of this function).
+#if defined(COMPILER_MSVC)
+ const uint32_t M_ARRAY_LENGTH = VelocityTracker::kHistorySize;
+ const uint32_t N_ARRAY_LENGTH = VelocityTracker::kPolyDegree;
+ DCHECK_LE(m, M_ARRAY_LENGTH);
+ DCHECK_LE(n, N_ARRAY_LENGTH);
+#else
+ const uint32_t M_ARRAY_LENGTH = m;
+ const uint32_t N_ARRAY_LENGTH = n;
+#endif
+
+ // Expand the X vector to a matrix A, pre-multiplied by the weights.
+ float a[N_ARRAY_LENGTH][M_ARRAY_LENGTH]; // column-major order
+ for (uint32_t h = 0; h < m; h++) {
+ a[0][h] = w[h];
+ for (uint32_t i = 1; i < n; i++) {
+ a[i][h] = a[i - 1][h] * x[h];
+ }
+ }
+
+ // Apply the Gram-Schmidt process to A to obtain its QR decomposition.
+
+ // Orthonormal basis, column-major order.
+ float q[N_ARRAY_LENGTH][M_ARRAY_LENGTH];
+ // Upper triangular matrix, row-major order.
+ float r[N_ARRAY_LENGTH][N_ARRAY_LENGTH];
+ for (uint32_t j = 0; j < n; j++) {
+ for (uint32_t h = 0; h < m; h++) {
+ q[j][h] = a[j][h];
+ }
+ for (uint32_t i = 0; i < j; i++) {
+ float dot = VectorDot(&q[j][0], &q[i][0], m);
+ for (uint32_t h = 0; h < m; h++) {
+ q[j][h] -= dot * q[i][h];
+ }
+ }
+
+ float norm = VectorNorm(&q[j][0], m);
+ if (norm < 0.000001f) {
+ // vectors are linearly dependent or zero so no solution
+ return false;
+ }
+
+ float invNorm = 1.0f / norm;
+ for (uint32_t h = 0; h < m; h++) {
+ q[j][h] *= invNorm;
+ }
+ for (uint32_t i = 0; i < n; i++) {
+ r[j][i] = i < j ? 0 : VectorDot(&q[j][0], &a[i][0], m);
+ }
+ }
+
+ // Solve R B = Qt W Y to find B. This is easy because R is upper triangular.
+ // We just work from bottom-right to top-left calculating B's coefficients.
+ float wy[M_ARRAY_LENGTH];
+ for (uint32_t h = 0; h < m; h++) {
+ wy[h] = y[h] * w[h];
+ }
+ for (uint32_t i = n; i-- != 0;) {
+ out_b[i] = VectorDot(&q[i][0], wy, m);
+ for (uint32_t j = n - 1; j > i; j--) {
+ out_b[i] -= r[i][j] * out_b[j];
+ }
+ out_b[i] /= r[i][i];
+ }
+
+ return true;
+}
+
+Maybe<float> AndroidVelocityTracker::ComputeVelocity(TimeStamp aTimestamp) {
+ if (mHistory.IsEmpty()) {
+ return Nothing{};
+ }
+
+ // Polynomial coefficients describing motion along the axis.
+ float xcoeff[kPolyDegree + 1];
+ for (size_t i = 0; i <= kPolyDegree; i++) {
+ xcoeff[i] = 0;
+ }
+
+ // Iterate over movement samples in reverse time order and collect samples.
+ float pos[kHistorySize];
+ float w[kHistorySize];
+ float time[kHistorySize];
+ uint32_t m = 0;
+ int index = mHistory.Length() - 1;
+ const TimeDuration horizon = TimeDuration::FromMilliseconds(
+ StaticPrefs::apz_velocity_relevance_time_ms());
+ const auto& newest_movement = mHistory[index];
+
+ do {
+ const auto& movement = mHistory[index];
+ TimeDuration age = newest_movement.first - movement.first;
+ if (age > horizon) break;
+
+ ParentLayerCoord position = movement.second;
+ pos[m] = position;
+ w[m] = 1.0f;
+ time[m] =
+ -static_cast<float>(age.ToMilliseconds()) / 1000.0f; // in seconds
+ index--;
+ m++;
+ } while (index >= 0);
+
+ if (m == 0) {
+ return Nothing{}; // no data
+ }
+
+ // Calculate a least squares polynomial fit.
+
+ // Polynomial degree (number of coefficients), or zero if no information is
+ // available.
+ uint32_t degree = kDegree;
+ if (degree > m - 1) {
+ degree = m - 1;
+ }
+
+ if (degree >= 1) { // otherwise, no velocity data available
+ uint32_t n = degree + 1;
+ if (SolveLeastSquares(time, pos, w, m, n, xcoeff)) {
+ float velocity = xcoeff[1];
+
+ // The velocity needs to be negated because the positions represent
+ // touch positions, and the direction of scrolling is opposite to the
+ // direction of the finger's movement.
+ return Some(-velocity / 1000.0f); // convert to pixels per millisecond
+ }
+ }
+
+ return Nothing{};
+}
+
+void AndroidVelocityTracker::Clear() { mHistory.Clear(); }
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/AndroidVelocityTracker.h b/gfx/layers/apz/src/AndroidVelocityTracker.h
new file mode 100644
index 0000000000..40e346a9ea
--- /dev/null
+++ b/gfx/layers/apz/src/AndroidVelocityTracker.h
@@ -0,0 +1,42 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_AndroidVelocityTracker_h
+#define mozilla_layers_AndroidVelocityTracker_h
+
+#include <utility>
+#include <cstdint>
+
+#include "Axis.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/Maybe.h"
+#include "nsTArray.h"
+
+namespace mozilla {
+namespace layers {
+
+class AndroidVelocityTracker : public VelocityTracker {
+ public:
+ explicit AndroidVelocityTracker();
+ void StartTracking(ParentLayerCoord aPos, TimeStamp aTimestamp) override;
+ Maybe<float> AddPosition(ParentLayerCoord aPos,
+ TimeStamp aTimestamp) override;
+ Maybe<float> ComputeVelocity(TimeStamp aTimestamp) override;
+ void Clear() override;
+
+ private:
+ // A queue of (timestamp, position) pairs; these are the historical
+ // positions at the given timestamps.
+ nsTArray<std::pair<TimeStamp, ParentLayerCoord>> mHistory;
+ // The last time an event was added to the tracker, or the null moment if no
+ // events have been added.
+ TimeStamp mLastEventTime;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif
diff --git a/gfx/layers/apz/src/AsyncDragMetrics.h b/gfx/layers/apz/src/AsyncDragMetrics.h
new file mode 100644
index 0000000000..5374818a06
--- /dev/null
+++ b/gfx/layers/apz/src/AsyncDragMetrics.h
@@ -0,0 +1,54 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_DragMetrics_h
+#define mozilla_layers_DragMetrics_h
+
+#include "mozilla/layers/ScrollableLayerGuid.h"
+#include "LayersTypes.h"
+#include "mozilla/Maybe.h"
+
+namespace IPC {
+template <typename T>
+struct ParamTraits;
+} // namespace IPC
+
+namespace mozilla {
+
+namespace layers {
+
+class AsyncDragMetrics {
+ friend struct IPC::ParamTraits<mozilla::layers::AsyncDragMetrics>;
+
+ public:
+ // IPC constructor
+ AsyncDragMetrics()
+ : mViewId(0),
+ mPresShellId(0),
+ mDragStartSequenceNumber(0),
+ mScrollbarDragOffset(0) {}
+
+ AsyncDragMetrics(const ScrollableLayerGuid::ViewID& aViewId,
+ uint32_t aPresShellId, uint64_t aDragStartSequenceNumber,
+ OuterCSSCoord aScrollbarDragOffset,
+ ScrollDirection aDirection)
+ : mViewId(aViewId),
+ mPresShellId(aPresShellId),
+ mDragStartSequenceNumber(aDragStartSequenceNumber),
+ mScrollbarDragOffset(aScrollbarDragOffset),
+ mDirection(Some(aDirection)) {}
+
+ ScrollableLayerGuid::ViewID mViewId;
+ uint32_t mPresShellId;
+ uint64_t mDragStartSequenceNumber;
+ OuterCSSCoord mScrollbarDragOffset; // relative to the thumb's start offset
+ Maybe<ScrollDirection> mDirection;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif
diff --git a/gfx/layers/apz/src/AsyncPanZoomAnimation.h b/gfx/layers/apz/src/AsyncPanZoomAnimation.h
new file mode 100644
index 0000000000..127667afd9
--- /dev/null
+++ b/gfx/layers/apz/src/AsyncPanZoomAnimation.h
@@ -0,0 +1,101 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_AsyncPanZoomAnimation_h_
+#define mozilla_layers_AsyncPanZoomAnimation_h_
+
+#include "APZUtils.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/TimeStamp.h"
+#include "nsISupportsImpl.h"
+#include "nsTArray.h"
+#include "nsThreadUtils.h"
+
+namespace mozilla {
+namespace layers {
+
+struct FrameMetrics;
+
+class WheelScrollAnimation;
+class OverscrollAnimation;
+class SmoothMsdScrollAnimation;
+class SmoothScrollAnimation;
+
+class AsyncPanZoomAnimation {
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(AsyncPanZoomAnimation)
+
+ public:
+ explicit AsyncPanZoomAnimation() = default;
+
+ virtual bool DoSample(FrameMetrics& aFrameMetrics,
+ const TimeDuration& aDelta) = 0;
+
+ /**
+ * Attempt to handle a main-thread scroll offset update without cancelling
+ * the animation. This may or may not make sense depending on the type of
+ * the animation and whether the scroll update is relative or absolute.
+ *
+ * If the scroll update is relative, |aRelativeDelta| will contain the
+ * delta of the relative update. If the scroll update is absolute,
+ * |aRelativeDelta| will be Nothing() (the animation can check the APZC's
+ * FrameMetrics for the new absolute scroll offset if it wants to handle
+ * and absolute update).
+ *
+ * Returns whether the animation could handle the scroll update. If the
+ * return value is false, the animation will be cancelled.
+ */
+ virtual bool HandleScrollOffsetUpdate(const Maybe<CSSPoint>& aRelativeDelta) {
+ return false;
+ }
+
+ bool Sample(FrameMetrics& aFrameMetrics, const TimeDuration& aDelta) {
+ // In some situations, particularly when handoff is involved, it's possible
+ // for |aDelta| to be negative on the first call to sample. Ignore such a
+ // sample here, to avoid each derived class having to deal with this case.
+ if (aDelta.ToMilliseconds() <= 0) {
+ return true;
+ }
+
+ return DoSample(aFrameMetrics, aDelta);
+ }
+
+ /**
+ * Get the deferred tasks in |mDeferredTasks| and place them in |aTasks|. See
+ * |mDeferredTasks| for more information. Clears |mDeferredTasks|.
+ */
+ nsTArray<RefPtr<Runnable>> TakeDeferredTasks() {
+ return std::move(mDeferredTasks);
+ }
+
+ virtual WheelScrollAnimation* AsWheelScrollAnimation() { return nullptr; }
+ virtual SmoothMsdScrollAnimation* AsSmoothMsdScrollAnimation() {
+ return nullptr;
+ }
+ virtual SmoothScrollAnimation* AsSmoothScrollAnimation() { return nullptr; }
+ virtual OverscrollAnimation* AsOverscrollAnimation() { return nullptr; }
+
+ virtual bool WantsRepaints() { return true; }
+
+ virtual void Cancel(CancelAnimationFlags aFlags) {}
+
+ virtual bool WasTriggeredByScript() const { return false; }
+
+ protected:
+ // Protected destructor, to discourage deletion outside of Release():
+ virtual ~AsyncPanZoomAnimation() = default;
+
+ /**
+ * Tasks scheduled for execution after the APZC's mMonitor is released.
+ * Derived classes can add tasks here in Sample(), and the APZC can call
+ * ExecuteDeferredTasks() to execute them.
+ */
+ nsTArray<RefPtr<Runnable>> mDeferredTasks;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_AsyncPanZoomAnimation_h_
diff --git a/gfx/layers/apz/src/AsyncPanZoomController.cpp b/gfx/layers/apz/src/AsyncPanZoomController.cpp
new file mode 100644
index 0000000000..802d5b9dbf
--- /dev/null
+++ b/gfx/layers/apz/src/AsyncPanZoomController.cpp
@@ -0,0 +1,6654 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "AsyncPanZoomController.h" // for AsyncPanZoomController, etc
+
+#include <math.h> // for fabsf, fabs, atan2
+#include <stdint.h> // for uint32_t, uint64_t
+#include <sys/types.h> // for int32_t
+#include <algorithm> // for max, min
+#include <utility> // for std::make_pair
+
+#include "APZCTreeManager.h" // for APZCTreeManager
+#include "AsyncPanZoomAnimation.h" // for AsyncPanZoomAnimation
+#include "AutoDirWheelDeltaAdjuster.h" // for APZAutoDirWheelDeltaAdjuster
+#include "AutoscrollAnimation.h" // for AutoscrollAnimation
+#include "Axis.h" // for AxisX, AxisY, Axis, etc
+#include "CheckerboardEvent.h" // for CheckerboardEvent
+#include "Compositor.h" // for Compositor
+#include "DesktopFlingPhysics.h" // for DesktopFlingPhysics
+#include "FrameMetrics.h" // for FrameMetrics, etc
+#include "GenericFlingAnimation.h" // for GenericFlingAnimation
+#include "GestureEventListener.h" // for GestureEventListener
+#include "HitTestingTreeNode.h" // for HitTestingTreeNode
+#include "InputData.h" // for MultiTouchInput, etc
+#include "InputBlockState.h" // for InputBlockState, TouchBlockState
+#include "InputQueue.h" // for InputQueue
+#include "Overscroll.h" // for OverscrollAnimation
+#include "OverscrollHandoffState.h" // for OverscrollHandoffState
+#include "SimpleVelocityTracker.h" // for SimpleVelocityTracker
+#include "Units.h" // for CSSRect, CSSPoint, etc
+#include "UnitTransforms.h" // for TransformTo
+#include "base/message_loop.h" // for MessageLoop
+#include "base/task.h" // for NewRunnableMethod, etc
+#include "gfxTypes.h" // for gfxFloat
+#include "mozilla/Assertions.h" // for MOZ_ASSERT, etc
+#include "mozilla/BasicEvents.h" // for Modifiers, MODIFIER_*
+#include "mozilla/ClearOnShutdown.h" // for ClearOnShutdown
+#include "mozilla/ServoStyleConsts.h" // for StyleComputedTimingFunction
+#include "mozilla/EventForwards.h" // for nsEventStatus_*
+#include "mozilla/EventStateManager.h" // for EventStateManager
+#include "mozilla/MouseEvents.h" // for WidgetWheelEvent
+#include "mozilla/Preferences.h" // for Preferences
+#include "mozilla/RecursiveMutex.h" // for RecursiveMutexAutoLock, etc
+#include "mozilla/RefPtr.h" // for RefPtr
+#include "mozilla/ScrollTypes.h"
+#include "mozilla/StaticPrefs_apz.h"
+#include "mozilla/StaticPrefs_general.h"
+#include "mozilla/StaticPrefs_gfx.h"
+#include "mozilla/StaticPrefs_mousewheel.h"
+#include "mozilla/StaticPrefs_layers.h"
+#include "mozilla/StaticPrefs_layout.h"
+#include "mozilla/StaticPrefs_slider.h"
+#include "mozilla/StaticPrefs_test.h"
+#include "mozilla/StaticPrefs_toolkit.h"
+#include "mozilla/Telemetry.h" // for Telemetry
+#include "mozilla/TimeStamp.h" // for TimeDuration, TimeStamp
+#include "mozilla/dom/CheckerboardReportService.h" // for CheckerboardEventStorage
+// note: CheckerboardReportService.h actually lives in gfx/layers/apz/util/
+#include "mozilla/dom/Touch.h" // for Touch
+#include "mozilla/gfx/gfxVars.h" // for gfxVars
+#include "mozilla/gfx/BasePoint.h" // for BasePoint
+#include "mozilla/gfx/BaseRect.h" // for BaseRect
+#include "mozilla/gfx/Point.h" // for Point, RoundedToInt, etc
+#include "mozilla/gfx/Rect.h" // for RoundedIn
+#include "mozilla/gfx/ScaleFactor.h" // for ScaleFactor
+#include "mozilla/layers/APZThreadUtils.h" // for AssertOnControllerThread, etc
+#include "mozilla/layers/APZUtils.h" // for AsyncTransform
+#include "mozilla/layers/CompositorController.h" // for CompositorController
+#include "mozilla/layers/DirectionUtils.h" // for GetAxis{Start,End,Length,Scale}
+#include "mozilla/layers/APZPublicUtils.h" // for GetScrollMode
+#include "mozilla/mozalloc.h" // for operator new, etc
+#include "mozilla/Unused.h" // for unused
+#include "nsAlgorithm.h" // for clamped
+#include "nsCOMPtr.h" // for already_AddRefed
+#include "nsDebug.h" // for NS_WARNING
+#include "nsLayoutUtils.h"
+#include "nsMathUtils.h" // for NS_hypot
+#include "nsPoint.h" // for nsIntPoint
+#include "nsStyleConsts.h"
+#include "nsTArray.h" // for nsTArray, nsTArray_Impl, etc
+#include "nsThreadUtils.h" // for NS_IsMainThread
+#include "nsViewportInfo.h" // for ViewportMinScale(), ViewportMaxScale()
+#include "prsystem.h" // for PR_GetPhysicalMemorySize
+#include "mozilla/ipc/SharedMemoryBasic.h" // for SharedMemoryBasic
+#include "ScrollSnap.h" // for ScrollSnapUtils
+#include "ScrollAnimationPhysics.h" // for ComputeAcceleratedWheelDelta
+#include "SmoothMsdScrollAnimation.h"
+#include "SmoothScrollAnimation.h"
+#include "WheelScrollAnimation.h"
+#if defined(MOZ_WIDGET_ANDROID)
+# include "AndroidAPZ.h"
+#endif // defined(MOZ_WIDGET_ANDROID)
+
+static mozilla::LazyLogModule sApzCtlLog("apz.controller");
+#define APZC_LOG(...) MOZ_LOG(sApzCtlLog, LogLevel::Debug, (__VA_ARGS__))
+#define APZC_LOGV(...) MOZ_LOG(sApzCtlLog, LogLevel::Verbose, (__VA_ARGS__))
+
+// Log to the apz.controller log with additional info from the APZC
+#define APZC_LOG_DETAIL(fmt, apzc, ...) \
+ APZC_LOG("%p(%s scrollId=%" PRIu64 "): " fmt, (apzc), \
+ (apzc)->IsRootContent() ? "root" : "subframe", \
+ (apzc)->GetScrollId(), ##__VA_ARGS__)
+
+#define APZC_LOG_FM_COMMON(fm, prefix, level, ...) \
+ if (MOZ_LOG_TEST(sApzCtlLog, level)) { \
+ std::stringstream ss; \
+ ss << nsPrintfCString(prefix, __VA_ARGS__).get() << ":" << fm; \
+ MOZ_LOG(sApzCtlLog, level, ("%s\n", ss.str().c_str())); \
+ }
+#define APZC_LOG_FM(fm, prefix, ...) \
+ APZC_LOG_FM_COMMON(fm, prefix, LogLevel::Debug, __VA_ARGS__)
+#define APZC_LOGV_FM(fm, prefix, ...) \
+ APZC_LOG_FM_COMMON(fm, prefix, LogLevel::Verbose, __VA_ARGS__)
+
+namespace mozilla {
+namespace layers {
+
+typedef mozilla::layers::AllowedTouchBehavior AllowedTouchBehavior;
+typedef GeckoContentController::APZStateChange APZStateChange;
+typedef GeckoContentController::TapType TapType;
+typedef mozilla::gfx::Point Point;
+typedef mozilla::gfx::Matrix4x4 Matrix4x4;
+
+// Choose between platform-specific implementations.
+#ifdef MOZ_WIDGET_ANDROID
+typedef WidgetOverscrollEffect OverscrollEffect;
+typedef AndroidSpecificState PlatformSpecificState;
+#else
+typedef GenericOverscrollEffect OverscrollEffect;
+typedef PlatformSpecificStateBase
+ PlatformSpecificState; // no extra state, just use the base class
+#endif
+
+/**
+ * \page APZCPrefs APZ preferences
+ *
+ * The following prefs are used to control the behaviour of the APZC.
+ * The default values are provided in StaticPrefList.yaml.
+ *
+ * \li\b apz.allow_double_tap_zooming
+ * Pref that allows or disallows double tap to zoom
+ *
+ * \li\b apz.allow_immediate_handoff
+ * If set to true, scroll can be handed off from one APZC to another within
+ * a single input block. If set to false, a single input block can only
+ * scroll one APZC.
+ *
+ * \li\b apz.allow_zooming_out
+ * If set to true, APZ will allow zooming out past the initial scale on
+ * desktop. This is false by default to match Chrome's behaviour.
+ *
+ * \li\b apz.android.chrome_fling_physics.friction
+ * A tunable parameter for Chrome fling physics on Android that governs
+ * how quickly a fling animation slows down due to friction (and therefore
+ * also how far it reaches). Should be in the range [0-1].
+ *
+ * \li\b apz.android.chrome_fling_physics.inflexion
+ * A tunable parameter for Chrome fling physics on Android that governs
+ * the shape of the fling curve. Should be in the range [0-1].
+ *
+ * \li\b apz.android.chrome_fling_physics.stop_threshold
+ * A tunable parameter for Chrome fling physics on Android that governs
+ * how close the fling animation has to get to its target destination
+ * before it stops.
+ * Units: ParentLayer pixels
+ *
+ * \li\b apz.autoscroll.enabled
+ * If set to true, autoscrolling is driven by APZ rather than the content
+ * process main thread.
+ *
+ * \li\b apz.axis_lock.mode
+ * The preferred axis locking style. See AxisLockMode for possible values.
+ *
+ * \li\b apz.axis_lock.lock_angle
+ * Angle from axis within which we stay axis-locked.\n
+ * Units: radians
+ *
+ * \li\b apz.axis_lock.breakout_threshold
+ * Distance in inches the user must pan before axis lock can be broken.\n
+ * Units: (real-world, i.e. screen) inches
+ *
+ * \li\b apz.axis_lock.breakout_angle
+ * Angle at which axis lock can be broken.\n
+ * Units: radians
+ *
+ * \li\b apz.axis_lock.direct_pan_angle
+ * If the angle from an axis to the line drawn by a pan move is less than
+ * this value, we can assume that panning can be done in the allowed direction
+ * (horizontal or vertical).\n
+ * Currently used only for touch-action css property stuff and was addded to
+ * keep behaviour consistent with IE.\n
+ * Units: radians
+ *
+ * \li\b apz.content_response_timeout
+ * Amount of time before we timeout response from content. For example, if
+ * content is being unruly/slow and we don't get a response back within this
+ * time, we will just pretend that content did not preventDefault any touch
+ * events we dispatched to it.\n
+ * Units: milliseconds
+ *
+ * \li\b apz.danger_zone_x
+ * \li\b apz.danger_zone_y
+ * When drawing high-res tiles, we drop down to drawing low-res tiles
+ * when we know we can't keep up with the scrolling. The way we determine
+ * this is by checking if we are entering the "danger zone", which is the
+ * boundary of the painted content. For example, if the painted content
+ * goes from y=0...1000 and the visible portion is y=250...750 then
+ * we're far from checkerboarding. If we get to y=490...990 though then we're
+ * only 10 pixels away from showing checkerboarding so we are probably in
+ * a state where we can't keep up with scrolling. The danger zone prefs specify
+ * how wide this margin is; in the above example a y-axis danger zone of 10
+ * pixels would make us drop to low-res at y=490...990.\n
+ * This value is in screen pixels.
+ *
+ * \li\b apz.disable_for_scroll_linked_effects
+ * Setting this pref to true will disable APZ scrolling on documents where
+ * scroll-linked effects are detected. A scroll linked effect is detected if
+ * positioning or transform properties are updated inside a scroll event
+ * dispatch; we assume that such an update is in response to the scroll event
+ * and is therefore a scroll-linked effect which will be laggy with APZ
+ * scrolling.
+ *
+ * \li\b apz.displayport_expiry_ms
+ * While a scrollable frame is scrolling async, we set a displayport on it
+ * to make sure it is layerized. However this takes up memory, so once the
+ * scrolling stops we want to remove the displayport. This pref controls how
+ * long after scrolling stops the displayport is removed. A value of 0 will
+ * disable the expiry behavior entirely.
+ * Units: milliseconds
+ *
+ * \li\b apz.drag.enabled
+ * Setting this pref to true will cause APZ to handle mouse-dragging of
+ * scrollbar thumbs.
+ *
+ * \li\b apz.drag.initial.enabled
+ * Setting this pref to true will cause APZ to try to handle mouse-dragging
+ * of scrollbar thumbs without an initial round-trip to content to start it
+ * if possible. Only has an effect if apz.drag.enabled is also true.
+ *
+ * \li\b apz.drag.touch.enabled
+ * Setting this pref to true will cause APZ to handle touch-dragging of
+ * scrollbar thumbs. Only has an effect if apz.drag.enabled is also true.
+ *
+ * \li\b apz.enlarge_displayport_when_clipped
+ * Pref that enables enlarging of the displayport along one axis when the
+ * generated displayport's size is beyond that of the scrollable rect on the
+ * opposite axis.
+ *
+ * \li\b apz.fling_accel_min_fling_velocity
+ * The minimum velocity of the second fling, and the minimum velocity of the
+ * previous fling animation at the point of interruption, for the new fling to
+ * be considered for fling acceleration.
+ * Units: screen pixels per milliseconds
+ *
+ * \li\b apz.fling_accel_min_pan_velocity
+ * The minimum velocity during the pan gesture that causes a fling for that
+ * fling to be considered for fling acceleration.
+ * Units: screen pixels per milliseconds
+ *
+ * \li\b apz.fling_accel_max_pause_interval_ms
+ * The maximum time that is allowed to elapse between the touch start event that
+ * interrupts the previous fling, and the touch move that initiates panning for
+ * the current fling, for that fling to be considered for fling acceleration.
+ * Units: milliseconds
+ *
+ * \li\b apz.fling_accel_base_mult
+ * \li\b apz.fling_accel_supplemental_mult
+ * When applying an acceleration on a fling, the new computed velocity is
+ * (new_fling_velocity * base_mult) + (old_velocity * supplemental_mult).
+ * The base_mult and supplemental_mult multiplier values are controlled by
+ * these prefs. Note that "old_velocity" here is the initial velocity of the
+ * previous fling _after_ acceleration was applied to it (if applicable).
+ *
+ * \li\b apz.fling_curve_function_x1
+ * \li\b apz.fling_curve_function_y1
+ * \li\b apz.fling_curve_function_x2
+ * \li\b apz.fling_curve_function_y2
+ * \li\b apz.fling_curve_threshold_inches_per_ms
+ * These five parameters define a Bezier curve function and threshold used to
+ * increase the actual velocity relative to the user's finger velocity. When the
+ * finger velocity is below the threshold (or if the threshold is not positive),
+ * the velocity is used as-is. If the finger velocity exceeds the threshold
+ * velocity, then the function defined by the curve is applied on the part of
+ * the velocity that exceeds the threshold. Note that the upper bound of the
+ * velocity is still specified by the \b apz.max_velocity_inches_per_ms pref,
+ * and the function will smoothly curve the velocity from the threshold to the
+ * max. In general the function parameters chosen should define an ease-out
+ * curve in order to increase the velocity in this range, or an ease-in curve to
+ * decrease the velocity. A straight-line curve is equivalent to disabling the
+ * curve entirely by setting the threshold to -1. The max velocity pref must
+ * also be set in order for the curving to take effect, as it defines the upper
+ * bound of the velocity curve.\n
+ * The points (x1, y1) and (x2, y2) used as the two intermediate control points
+ * in the cubic bezier curve; the first and last points are (0,0) and (1,1).\n
+ * Some example values for these prefs can be found at\n
+ * https://searchfox.org/mozilla-central/rev/f82d5c549f046cb64ce5602bfd894b7ae807c8f8/dom/animation/ComputedTimingFunction.cpp#27-33
+ *
+ * \li\b apz.fling_friction
+ * Amount of friction applied during flings. This is used in the following
+ * formula: v(t1) = v(t0) * (1 - f)^(t1 - t0), where v(t1) is the velocity
+ * for a new sample, v(t0) is the velocity at the previous sample, f is the
+ * value of this pref, and (t1 - t0) is the amount of time, in milliseconds,
+ * that has elapsed between the two samples.\n
+ * NOTE: Not currently used in Android fling calculations.
+ *
+ * \li\b apz.fling_min_velocity_threshold
+ * Minimum velocity for a fling to actually kick off. If the user pans and lifts
+ * their finger such that the velocity is smaller than or equal to this amount,
+ * no fling is initiated.\n
+ * Units: screen pixels per millisecond
+ *
+ * \li\b apz.fling_stop_on_tap_threshold
+ * When flinging, if the velocity is above this number, then a tap on the
+ * screen will stop the fling without dispatching a tap to content. If the
+ * velocity is below this threshold a tap will also be dispatched.
+ * Note: when modifying this pref be sure to run the APZC gtests as some of
+ * them depend on the value of this pref.\n
+ * Units: screen pixels per millisecond
+ *
+ * \li\b apz.fling_stopped_threshold
+ * When flinging, if the velocity goes below this number, we just stop the
+ * animation completely. This is to prevent asymptotically approaching 0
+ * velocity and rerendering unnecessarily.\n
+ * Units: screen pixels per millisecond.\n
+ * NOTE: Should not be set to anything
+ * other than 0.0 for Android except for tests to disable flings.
+ *
+ * \li\b apz.keyboard.enabled
+ * Determines whether scrolling with the keyboard will be allowed to be handled
+ * by APZ.
+ *
+ * \li\b apz.keyboard.passive-listeners
+ * When enabled, APZ will interpret the passive event listener flag to mean
+ * that the event listener won't change the focused element or selection of
+ * the page. With this, web content can use passive key listeners and not have
+ * keyboard APZ disabled.
+ *
+ * \li\b apz.max_tap_time
+ * Maximum time for a touch on the screen and corresponding lift of the finger
+ * to be considered a tap. This also applies to double taps, except that it is
+ * used both for the interval between the first touchdown and first touchup,
+ * and for the interval between the first touchup and the second touchdown.\n
+ * Units: milliseconds.
+ *
+ * \li\b apz.max_velocity_inches_per_ms
+ * Maximum velocity. Velocity will be capped at this value if a faster fling
+ * occurs. Negative values indicate unlimited velocity.\n
+ * Units: (real-world, i.e. screen) inches per millisecond
+ *
+ * \li\b apz.max_velocity_queue_size
+ * Maximum size of velocity queue. The queue contains last N velocity records.
+ * On touch end we calculate the average velocity in order to compensate
+ * touch/mouse drivers misbehaviour.
+ *
+ * \li\b apz.min_skate_speed
+ * Minimum amount of speed along an axis before we switch to "skate" multipliers
+ * rather than using the "stationary" multipliers.\n
+ * Units: CSS pixels per millisecond
+ *
+ * \li\b apz.one_touch_pinch.enabled
+ * Whether or not the "one-touch-pinch" gesture (for zooming with one finger)
+ * is enabled or not.
+ *
+ * \li\b apz.overscroll.enabled
+ * Pref that enables overscrolling. If this is disabled, excess scroll that
+ * cannot be handed off is discarded.
+ *
+ * \li\b apz.overscroll.min_pan_distance_ratio
+ * The minimum ratio of the pan distance along one axis to the pan distance
+ * along the other axis needed to initiate overscroll along the first axis
+ * during panning.
+ *
+ * \li\b apz.overscroll.stretch_factor
+ * How much overscrolling can stretch content along an axis.
+ * The maximum stretch along an axis is a factor of (1 + kStretchFactor).
+ * (So if kStretchFactor is 0, you can't stretch at all; if kStretchFactor
+ * is 1, you can stretch at most by a factor of 2).
+ *
+ * \li\b apz.overscroll.stop_distance_threshold
+ * \li\b apz.overscroll.stop_velocity_threshold
+ * Thresholds for stopping the overscroll animation. When both the distance
+ * and the velocity fall below their thresholds, we stop oscillating.\n
+ * Units: screen pixels (for distance)
+ * screen pixels per millisecond (for velocity)
+ *
+ * \li\b apz.overscroll.spring_stiffness
+ * The spring stiffness constant for the overscroll mass-spring-damper model.
+ *
+ * \li\b apz.overscroll.damping
+ * The damping constant for the overscroll mass-spring-damper model.
+ *
+ * \li\b apz.overscroll.max_velocity
+ * The maximum velocity (in ParentLayerPixels per millisecond) allowed when
+ * initiating the overscroll snap-back animation.
+ *
+ * \li\b apz.paint_skipping.enabled
+ * When APZ is scrolling and sending repaint requests to the main thread, often
+ * the main thread doesn't actually need to do a repaint. This pref allows the
+ * main thread to skip doing those repaints in cases where it doesn't need to.
+ *
+ * \li\b apz.pinch_lock.mode
+ * The preferred pinch locking style. See PinchLockMode for possible values.
+ *
+ * \li\b apz.pinch_lock.scroll_lock_threshold
+ * Pinch locking is triggered if the user scrolls more than this distance
+ * and pinches less than apz.pinch_lock.span_lock_threshold.\n
+ * Units: (real-world, i.e. screen) inches
+ *
+ * \li\b apz.pinch_lock.span_breakout_threshold
+ * Distance in inches the user must pinch before lock can be broken.\n
+ * Units: (real-world, i.e. screen) inches measured between two touch points
+ *
+ * \li\b apz.pinch_lock.span_lock_threshold
+ * Pinch locking is triggered if the user pinches less than this distance
+ * and scrolls more than apz.pinch_lock.scroll_lock_threshold.\n
+ * Units: (real-world, i.e. screen) inches measured between two touch points
+ *
+ * \li\b apz.pinch_lock.buffer_max_age
+ * To ensure that pinch locking threshold calculations are not affected by
+ * variations in touch screen sensitivity, calculations draw from a buffer of
+ * recent events. This preference specifies the maximum time that events are
+ * held in this buffer.
+ * Units: milliseconds
+ *
+ * \li\b apz.popups.enabled
+ * Determines whether APZ is used for XUL popup widgets with remote content.
+ * Ideally, this should always be true, but it is currently not well tested, and
+ * has known issues, so needs to be prefable.
+ *
+ * \li\b apz.record_checkerboarding
+ * Whether or not to record detailed info on checkerboarding events.
+ *
+ * \li\b apz.second_tap_tolerance
+ * Constant describing the tolerance in distance we use, multiplied by the
+ * device DPI, within which a second tap is counted as part of a gesture
+ * continuing from the first tap. Making this larger allows the user more
+ * distance between the first and second taps in a "double tap" or "one touch
+ * pinch" gesture.\n
+ * Units: (real-world, i.e. screen) inches
+ *
+ * \li\b apz.test.logging_enabled
+ * Enable logging of APZ test data (see bug 961289).
+ *
+ * \li\b apz.touch_move_tolerance
+ * See the description for apz.touch_start_tolerance below. This is a similar
+ * threshold, except it is used to suppress touchmove events from being
+ * delivered to content for NON-scrollable frames (or more precisely, for APZCs
+ * where ArePointerEventsConsumable returns false).\n Units: (real-world, i.e.
+ * screen) inches
+ *
+ * \li\b apz.touch_start_tolerance
+ * Constant describing the tolerance in distance we use, multiplied by the
+ * device DPI, before we start panning the screen. This is to prevent us from
+ * accidentally processing taps as touch moves, and from very short/accidental
+ * touches moving the screen. touchmove events are also not delivered to content
+ * within this distance on scrollable frames.\n
+ * Units: (real-world, i.e. screen) inches
+ *
+ * \li\b apz.velocity_bias
+ * How much to adjust the displayport in the direction of scrolling. This value
+ * is multiplied by the velocity and added to the displayport offset.
+ *
+ * \li\b apz.velocity_relevance_time_ms
+ * When computing a fling velocity from the most recently stored velocity
+ * information, only velocities within the most X milliseconds are used.
+ * This pref controls the value of X.\n
+ * Units: ms
+ *
+ * \li\b apz.x_skate_size_multiplier
+ * \li\b apz.y_skate_size_multiplier
+ * The multiplier we apply to the displayport size if it is skating (current
+ * velocity is above \b apz.min_skate_speed). We prefer to increase the size of
+ * the Y axis because it is more natural in the case that a user is reading a
+ * page page that scrolls up/down. Note that one, both or neither of these may
+ * be used at any instant.\n In general we want \b
+ * apz.[xy]_skate_size_multiplier to be smaller than the corresponding
+ * stationary size multiplier because when panning fast we would like to paint
+ * less and get faster, more predictable paint times. When panning slowly we
+ * can afford to paint more even though it's slower.
+ *
+ * \li\b apz.x_stationary_size_multiplier
+ * \li\b apz.y_stationary_size_multiplier
+ * The multiplier we apply to the displayport size if it is not skating (see
+ * documentation for the skate size multipliers above).
+ *
+ * \li\b apz.x_skate_highmem_adjust
+ * \li\b apz.y_skate_highmem_adjust
+ * On high memory systems, we adjust the displayport during skating
+ * to be larger so we can reduce checkerboarding.
+ *
+ * \li\b apz.zoom_animation_duration_ms
+ * This controls how long the zoom-to-rect animation takes.\n
+ * Units: ms
+ *
+ * \li\b apz.scale_repaint_delay_ms
+ * How long to delay between repaint requests during a scale.
+ * A negative number prevents repaint requests during a scale.\n
+ * Units: ms
+ */
+
+/**
+ * Computed time function used for sampling frames of a zoom to animation.
+ */
+StaticAutoPtr<StyleComputedTimingFunction> gZoomAnimationFunction;
+
+/**
+ * Computed time function used for curving up velocity when it gets high.
+ */
+StaticAutoPtr<StyleComputedTimingFunction> gVelocityCurveFunction;
+
+/**
+ * The estimated duration of a paint for the purposes of calculating a new
+ * displayport, in milliseconds.
+ */
+static const double kDefaultEstimatedPaintDurationMs = 50;
+
+/**
+ * Returns true if this is a high memory system and we can use
+ * extra memory for a larger displayport to reduce checkerboarding.
+ */
+static bool gIsHighMemSystem = false;
+static bool IsHighMemSystem() { return gIsHighMemSystem; }
+
+AsyncPanZoomAnimation* PlatformSpecificStateBase::CreateFlingAnimation(
+ AsyncPanZoomController& aApzc, const FlingHandoffState& aHandoffState,
+ float aPLPPI) {
+ return new GenericFlingAnimation<DesktopFlingPhysics>(aApzc, aHandoffState,
+ aPLPPI);
+}
+
+UniquePtr<VelocityTracker> PlatformSpecificStateBase::CreateVelocityTracker(
+ Axis* aAxis) {
+ return MakeUnique<SimpleVelocityTracker>(aAxis);
+}
+
+SampleTime AsyncPanZoomController::GetFrameTime() const {
+ APZCTreeManager* treeManagerLocal = GetApzcTreeManager();
+ return treeManagerLocal ? treeManagerLocal->GetFrameTime()
+ : SampleTime::FromNow();
+}
+
+bool AsyncPanZoomController::IsZero(const ParentLayerPoint& aPoint) const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+
+ const auto zoom = Metrics().GetZoom();
+
+ if (zoom == CSSToParentLayerScale(0)) {
+ return true;
+ }
+
+ return layers::IsZero(aPoint / zoom);
+}
+
+bool AsyncPanZoomController::IsZero(ParentLayerCoord aCoord) const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+
+ const auto zoom = Metrics().GetZoom();
+
+ if (zoom == CSSToParentLayerScale(0)) {
+ return true;
+ }
+
+ return FuzzyEqualsAdditive((aCoord / zoom), CSSCoord(), COORDINATE_EPSILON);
+}
+
+bool AsyncPanZoomController::FuzzyGreater(ParentLayerCoord aCoord1,
+ ParentLayerCoord aCoord2) const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+
+ const auto zoom = Metrics().GetZoom();
+
+ if (zoom == CSSToParentLayerScale(0)) {
+ return false;
+ }
+
+ return (aCoord1 - aCoord2) / zoom > COORDINATE_EPSILON;
+}
+
+class MOZ_STACK_CLASS StateChangeNotificationBlocker final {
+ public:
+ explicit StateChangeNotificationBlocker(AsyncPanZoomController* aApzc)
+ : mApzc(aApzc) {
+ RecursiveMutexAutoLock lock(mApzc->mRecursiveMutex);
+ mInitialState = mApzc->mState;
+ mApzc->mNotificationBlockers++;
+ }
+
+ ~StateChangeNotificationBlocker() {
+ AsyncPanZoomController::PanZoomState newState;
+ {
+ RecursiveMutexAutoLock lock(mApzc->mRecursiveMutex);
+ mApzc->mNotificationBlockers--;
+ newState = mApzc->mState;
+ }
+ mApzc->DispatchStateChangeNotification(mInitialState, newState);
+ }
+
+ private:
+ AsyncPanZoomController* mApzc;
+ AsyncPanZoomController::PanZoomState mInitialState;
+};
+
+/**
+ * An RAII class to temporarily apply async test attributes to the provided
+ * AsyncPanZoomController.
+ *
+ * This class should be used in the implementation of any AsyncPanZoomController
+ * method that queries the async scroll offset or async zoom (this includes
+ * the async layout viewport offset, since modifying the async scroll offset
+ * may result in the layout viewport moving as well).
+ */
+class MOZ_RAII AutoApplyAsyncTestAttributes final {
+ public:
+ explicit AutoApplyAsyncTestAttributes(
+ const AsyncPanZoomController*,
+ const RecursiveMutexAutoLock& aProofOfLock);
+ ~AutoApplyAsyncTestAttributes();
+
+ private:
+ AsyncPanZoomController* mApzc;
+ FrameMetrics mPrevFrameMetrics;
+ ParentLayerPoint mPrevOverscroll;
+ const RecursiveMutexAutoLock& mProofOfLock;
+};
+
+AutoApplyAsyncTestAttributes::AutoApplyAsyncTestAttributes(
+ const AsyncPanZoomController* aApzc,
+ const RecursiveMutexAutoLock& aProofOfLock)
+ // Having to use const_cast here seems less ugly than the alternatives
+ // of making several members of AsyncPanZoomController that
+ // ApplyAsyncTestAttributes() modifies |mutable|, or several methods that
+ // query the async transforms non-const.
+ : mApzc(const_cast<AsyncPanZoomController*>(aApzc)),
+ mPrevFrameMetrics(aApzc->Metrics()),
+ mPrevOverscroll(aApzc->GetOverscrollAmountInternal()),
+ mProofOfLock(aProofOfLock) {
+ mApzc->ApplyAsyncTestAttributes(aProofOfLock);
+}
+
+AutoApplyAsyncTestAttributes::~AutoApplyAsyncTestAttributes() {
+ mApzc->UnapplyAsyncTestAttributes(mProofOfLock, mPrevFrameMetrics,
+ mPrevOverscroll);
+}
+
+class ZoomAnimation : public AsyncPanZoomAnimation {
+ public:
+ ZoomAnimation(AsyncPanZoomController& aApzc, const CSSPoint& aStartOffset,
+ const CSSToParentLayerScale& aStartZoom,
+ const CSSPoint& aEndOffset,
+ const CSSToParentLayerScale& aEndZoom)
+ : mApzc(aApzc),
+ mTotalDuration(TimeDuration::FromMilliseconds(
+ StaticPrefs::apz_zoom_animation_duration_ms())),
+ mStartOffset(aStartOffset),
+ mStartZoom(aStartZoom),
+ mEndOffset(aEndOffset),
+ mEndZoom(aEndZoom) {}
+
+ virtual bool DoSample(FrameMetrics& aFrameMetrics,
+ const TimeDuration& aDelta) override {
+ mDuration += aDelta;
+ double animPosition = mDuration / mTotalDuration;
+
+ if (animPosition >= 1.0) {
+ aFrameMetrics.SetZoom(mEndZoom);
+ mApzc.SetVisualScrollOffset(mEndOffset);
+ return false;
+ }
+
+ // Sample the zoom at the current time point. The sampled zoom
+ // will affect the final computed resolution.
+ float sampledPosition =
+ gZoomAnimationFunction->At(animPosition, /* aBeforeFlag = */ false);
+
+ // We scale the scrollOffset linearly with sampledPosition, so the zoom
+ // needs to scale inversely to match.
+ if (mStartZoom == CSSToParentLayerScale(0) ||
+ mEndZoom == CSSToParentLayerScale(0)) {
+ return false;
+ }
+
+ aFrameMetrics.SetZoom(
+ CSSToParentLayerScale(1 / (sampledPosition / mEndZoom.scale +
+ (1 - sampledPosition) / mStartZoom.scale)));
+
+ mApzc.SetVisualScrollOffset(CSSPoint::FromUnknownPoint(gfx::Point(
+ mEndOffset.x * sampledPosition + mStartOffset.x * (1 - sampledPosition),
+ mEndOffset.y * sampledPosition +
+ mStartOffset.y * (1 - sampledPosition))));
+ return true;
+ }
+
+ virtual bool WantsRepaints() override { return true; }
+
+ private:
+ AsyncPanZoomController& mApzc;
+
+ TimeDuration mDuration;
+ const TimeDuration mTotalDuration;
+
+ // Old metrics from before we started a zoom animation. This is only valid
+ // when we are in the "ANIMATED_ZOOM" state. This is used so that we can
+ // interpolate between the start and end frames. We only use the
+ // |mViewportScrollOffset| and |mResolution| fields on this.
+ CSSPoint mStartOffset;
+ CSSToParentLayerScale mStartZoom;
+
+ // Target metrics for a zoom to animation. This is only valid when we are in
+ // the "ANIMATED_ZOOM" state. We only use the |mViewportScrollOffset| and
+ // |mResolution| fields on this.
+ CSSPoint mEndOffset;
+ CSSToParentLayerScale mEndZoom;
+};
+
+/*static*/
+void AsyncPanZoomController::InitializeGlobalState() {
+ static bool sInitialized = false;
+ if (sInitialized) return;
+ sInitialized = true;
+
+ MOZ_ASSERT(NS_IsMainThread());
+
+ gZoomAnimationFunction = new StyleComputedTimingFunction(
+ StyleComputedTimingFunction::Keyword(StyleTimingKeyword::Ease));
+ ClearOnShutdown(&gZoomAnimationFunction);
+ gVelocityCurveFunction =
+ new StyleComputedTimingFunction(StyleComputedTimingFunction::CubicBezier(
+ StaticPrefs::apz_fling_curve_function_x1_AtStartup(),
+ StaticPrefs::apz_fling_curve_function_y1_AtStartup(),
+ StaticPrefs::apz_fling_curve_function_x2_AtStartup(),
+ StaticPrefs::apz_fling_curve_function_y2_AtStartup()));
+ ClearOnShutdown(&gVelocityCurveFunction);
+
+ uint64_t sysmem = PR_GetPhysicalMemorySize();
+ uint64_t threshold = 1LL << 32; // 4 GB in bytes
+ gIsHighMemSystem = sysmem >= threshold;
+
+ PlatformSpecificState::InitializeGlobalState();
+}
+
+AsyncPanZoomController::AsyncPanZoomController(
+ LayersId aLayersId, APZCTreeManager* aTreeManager,
+ const RefPtr<InputQueue>& aInputQueue,
+ GeckoContentController* aGeckoContentController, GestureBehavior aGestures)
+ : mLayersId(aLayersId),
+ mGeckoContentController(aGeckoContentController),
+ mRefPtrMonitor("RefPtrMonitor"),
+ // mTreeManager must be initialized before GetFrameTime() is called
+ mTreeManager(aTreeManager),
+ mRecursiveMutex("AsyncPanZoomController"),
+ mLastContentPaintMetrics(mLastContentPaintMetadata.GetMetrics()),
+ mPanDirRestricted(false),
+ mPinchLocked(false),
+ mPinchEventBuffer(TimeDuration::FromMilliseconds(
+ StaticPrefs::apz_pinch_lock_buffer_max_age_AtStartup())),
+ mZoomConstraints(false, false,
+ mScrollMetadata.GetMetrics().GetDevPixelsPerCSSPixel() *
+ ViewportMinScale() / ParentLayerToScreenScale(1),
+ mScrollMetadata.GetMetrics().GetDevPixelsPerCSSPixel() *
+ ViewportMaxScale() / ParentLayerToScreenScale(1)),
+ mLastSampleTime(GetFrameTime()),
+ mLastCheckerboardReport(GetFrameTime()),
+ mLastNotifiedZoom(),
+ mOverscrollEffect(MakeUnique<OverscrollEffect>(*this)),
+ mState(NOTHING),
+ mX(this),
+ mY(this),
+ mNotificationBlockers(0),
+ mInputQueue(aInputQueue),
+ mPinchPaintTimerSet(false),
+ mDelayedTransformEnd(false),
+ mTestAttributeAppliers(0),
+ mTestHasAsyncKeyScrolled(false),
+ mCheckerboardEventLock("APZCBELock") {
+ if (aGestures == USE_GESTURE_DETECTOR) {
+ mGestureEventListener = new GestureEventListener(this);
+ }
+ // Put one default-constructed sampled state in the queue.
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ mSampledState.emplace_back();
+}
+
+AsyncPanZoomController::~AsyncPanZoomController() { MOZ_ASSERT(IsDestroyed()); }
+
+PlatformSpecificStateBase* AsyncPanZoomController::GetPlatformSpecificState() {
+ if (!mPlatformSpecificState) {
+ mPlatformSpecificState = MakeUnique<PlatformSpecificState>();
+ }
+ return mPlatformSpecificState.get();
+}
+
+already_AddRefed<GeckoContentController>
+AsyncPanZoomController::GetGeckoContentController() const {
+ MonitorAutoLock lock(mRefPtrMonitor);
+ RefPtr<GeckoContentController> controller = mGeckoContentController;
+ return controller.forget();
+}
+
+already_AddRefed<GestureEventListener>
+AsyncPanZoomController::GetGestureEventListener() const {
+ MonitorAutoLock lock(mRefPtrMonitor);
+ RefPtr<GestureEventListener> listener = mGestureEventListener;
+ return listener.forget();
+}
+
+const RefPtr<InputQueue>& AsyncPanZoomController::GetInputQueue() const {
+ return mInputQueue;
+}
+
+void AsyncPanZoomController::Destroy() {
+ AssertOnUpdaterThread();
+
+ CancelAnimation(CancelAnimationFlags::ScrollSnap);
+
+ { // scope the lock
+ MonitorAutoLock lock(mRefPtrMonitor);
+ mGeckoContentController = nullptr;
+ mGestureEventListener = nullptr;
+ }
+ mParent = nullptr;
+ mTreeManager = nullptr;
+}
+
+bool AsyncPanZoomController::IsDestroyed() const {
+ return mTreeManager == nullptr;
+}
+
+float AsyncPanZoomController::GetDPI() const {
+ if (APZCTreeManager* localPtr = mTreeManager) {
+ return localPtr->GetDPI();
+ }
+ // If this APZC has been destroyed then this value is not going to be
+ // used for anything that the user will end up seeing, so we can just
+ // return 0.
+ return 0.0;
+}
+
+ScreenCoord AsyncPanZoomController::GetTouchStartTolerance() const {
+ return (StaticPrefs::apz_touch_start_tolerance() * GetDPI());
+}
+
+ScreenCoord AsyncPanZoomController::GetTouchMoveTolerance() const {
+ return (StaticPrefs::apz_touch_move_tolerance() * GetDPI());
+}
+
+ScreenCoord AsyncPanZoomController::GetSecondTapTolerance() const {
+ return (StaticPrefs::apz_second_tap_tolerance() * GetDPI());
+}
+
+/* static */ AsyncPanZoomController::AxisLockMode
+AsyncPanZoomController::GetAxisLockMode() {
+ return static_cast<AxisLockMode>(StaticPrefs::apz_axis_lock_mode());
+}
+
+bool AsyncPanZoomController::UsingStatefulAxisLock() const {
+ return (GetAxisLockMode() == STANDARD || GetAxisLockMode() == STICKY);
+}
+
+/* static */ AsyncPanZoomController::PinchLockMode
+AsyncPanZoomController::GetPinchLockMode() {
+ return static_cast<PinchLockMode>(StaticPrefs::apz_pinch_lock_mode());
+}
+
+PointerEventsConsumableFlags AsyncPanZoomController::ArePointerEventsConsumable(
+ TouchBlockState* aBlock, const MultiTouchInput& aInput) {
+ uint32_t touchPoints = aInput.mTouches.Length();
+ if (touchPoints == 0) {
+ // Cant' do anything with zero touch points
+ return {false, false};
+ }
+
+ // This logic is simplified, erring on the side of returning true if we're
+ // not sure. It's safer to pretend that we can consume the event and then
+ // not be able to than vice-versa. But at the same time, we should try hard
+ // to return an accurate result, because returning true can trigger a
+ // pointercancel event to web content, which can break certain features
+ // that are using touch-action and handling the pointermove events.
+ //
+ // Note that in particular this function can return true if APZ is waiting on
+ // the main thread for touch-action information. In this scenario, the
+ // APZEventState::MainThreadAgreesEventsAreConsumableByAPZ() function tries
+ // to use the main-thread touch-action information to filter out false
+ // positives.
+ //
+ // We could probably enhance this logic to determine things like "we're
+ // not pannable, so we can only zoom in, and the zoom is already maxed
+ // out, so we're not zoomable either" but no need for that at this point.
+
+ bool pannableX = aBlock->GetOverscrollHandoffChain()->CanScrollInDirection(
+ this, ScrollDirection::eHorizontal);
+ bool touchActionAllowsX = aBlock->TouchActionAllowsPanningX();
+ bool pannableY = (aBlock->GetOverscrollHandoffChain()->CanScrollInDirection(
+ this, ScrollDirection::eVertical) ||
+ // In the case of the root APZC with any dynamic toolbar, it
+ // shoule be pannable if there is room moving the dynamic
+ // toolbar.
+ (IsRootContent() && CanVerticalScrollWithDynamicToolbar()));
+ bool touchActionAllowsY = aBlock->TouchActionAllowsPanningY();
+
+ bool pannable;
+ bool touchActionAllowsPanning;
+
+ Maybe<ScrollDirection> panDirection =
+ aBlock->GetBestGuessPanDirection(aInput);
+ if (panDirection == Some(ScrollDirection::eVertical)) {
+ pannable = pannableY;
+ touchActionAllowsPanning = touchActionAllowsY;
+ } else if (panDirection == Some(ScrollDirection::eHorizontal)) {
+ pannable = pannableX;
+ touchActionAllowsPanning = touchActionAllowsX;
+ } else {
+ // If we don't have a guessed pan direction, err on the side of returning
+ // true.
+ pannable = pannableX || pannableY;
+ touchActionAllowsPanning = touchActionAllowsX || touchActionAllowsY;
+ }
+
+ if (touchPoints == 1) {
+ return {pannable, touchActionAllowsPanning};
+ }
+
+ bool zoomable = ZoomConstraintsAllowZoom();
+ bool touchActionAllowsZoom = aBlock->TouchActionAllowsPinchZoom();
+
+ return {pannable || zoomable,
+ touchActionAllowsPanning || touchActionAllowsZoom};
+}
+
+nsEventStatus AsyncPanZoomController::HandleDragEvent(
+ const MouseInput& aEvent, const AsyncDragMetrics& aDragMetrics,
+ OuterCSSCoord aInitialThumbPos) {
+ // RDM is a special case where touch events will be synthesized in response
+ // to mouse events, and APZ will receive both even though RDM prevent-defaults
+ // the mouse events. This is because mouse events don't opt into APZ waiting
+ // to check if the event has been prevent-defaulted and are still processed
+ // as a result. To handle this, have APZ ignore mouse events when RDM and
+ // touch simulation are active.
+ bool isRDMTouchSimulationActive = false;
+ {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ isRDMTouchSimulationActive =
+ mScrollMetadata.GetIsRDMTouchSimulationActive();
+ }
+
+ if (!StaticPrefs::apz_drag_enabled() || isRDMTouchSimulationActive) {
+ return nsEventStatus_eIgnore;
+ }
+
+ if (!GetApzcTreeManager()) {
+ return nsEventStatus_eConsumeNoDefault;
+ }
+
+ {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+
+ if (aEvent.mType == MouseInput::MouseType::MOUSE_UP) {
+ if (mState == SCROLLBAR_DRAG) {
+ APZC_LOG("%p ending drag\n", this);
+ SetState(NOTHING);
+ }
+
+ SnapBackIfOverscrolled();
+
+ return nsEventStatus_eConsumeNoDefault;
+ }
+ }
+
+ HitTestingTreeNodeAutoLock node;
+ GetApzcTreeManager()->FindScrollThumbNode(aDragMetrics, mLayersId, node);
+ if (!node) {
+ APZC_LOG("%p unable to find scrollthumb node with viewid %" PRIu64 "\n",
+ this, aDragMetrics.mViewId);
+ return nsEventStatus_eConsumeNoDefault;
+ }
+
+ if (aEvent.mType == MouseInput::MouseType::MOUSE_DOWN) {
+ APZC_LOG("%p starting scrollbar drag\n", this);
+ SetState(SCROLLBAR_DRAG);
+ }
+
+ if (aEvent.mType != MouseInput::MouseType::MOUSE_MOVE) {
+ APZC_LOG("%p discarding event of type %d\n", this, aEvent.mType);
+ return nsEventStatus_eConsumeNoDefault;
+ }
+
+ const ScrollbarData& scrollbarData = node->GetScrollbarData();
+ MOZ_ASSERT(scrollbarData.mScrollbarLayerType ==
+ layers::ScrollbarLayerType::Thumb);
+ MOZ_ASSERT(scrollbarData.mDirection.isSome());
+ ScrollDirection direction = *scrollbarData.mDirection;
+
+ bool isMouseAwayFromThumb = false;
+ if (int snapMultiplier = StaticPrefs::slider_snapMultiplier_AtStartup()) {
+ // It's fine to ignore the async component of the thumb's transform,
+ // because any async transform of the thumb will be in the direction of
+ // scrolling, but here we're interested in the other direction.
+ ParentLayerRect thumbRect =
+ (node->GetTransform() * AsyncTransformMatrix())
+ .TransformBounds(LayerRect(node->GetVisibleRegion().GetBounds()));
+ ScrollDirection otherDirection = GetPerpendicularDirection(direction);
+ ParentLayerCoord distance =
+ GetAxisStart(otherDirection, thumbRect.DistanceTo(aEvent.mLocalOrigin));
+ ParentLayerCoord thumbWidth = GetAxisLength(otherDirection, thumbRect);
+ // Avoid triggering this condition spuriously when the thumb is
+ // offscreen and its visible region is therefore empty.
+ if (thumbWidth > 0 && thumbWidth * snapMultiplier < distance) {
+ isMouseAwayFromThumb = true;
+ APZC_LOG("%p determined mouse is away from thumb, will snap\n", this);
+ }
+ }
+
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ OuterCSSCoord thumbPosition;
+ if (isMouseAwayFromThumb) {
+ thumbPosition = aInitialThumbPos;
+ } else {
+ thumbPosition = ConvertScrollbarPoint(aEvent.mLocalOrigin, scrollbarData) -
+ aDragMetrics.mScrollbarDragOffset;
+ }
+
+ OuterCSSCoord maxThumbPos = scrollbarData.mScrollTrackLength;
+ maxThumbPos -= scrollbarData.mThumbLength;
+
+ float scrollPercent =
+ maxThumbPos.value == 0.0f ? 0.0f : (float)(thumbPosition / maxThumbPos);
+ APZC_LOG("%p scrollbar dragged to %f percent\n", this, scrollPercent);
+
+ CSSCoord minScrollPosition =
+ GetAxisStart(direction, Metrics().GetScrollableRect().TopLeft());
+ CSSCoord maxScrollPosition =
+ GetAxisStart(direction, Metrics().GetScrollableRect().BottomRight()) -
+ GetAxisLength(direction, Metrics().CalculateCompositedSizeInCssPixels());
+ CSSCoord scrollPosition =
+ minScrollPosition +
+ (scrollPercent * (maxScrollPosition - minScrollPosition));
+
+ scrollPosition = std::max(scrollPosition, minScrollPosition);
+ scrollPosition = std::min(scrollPosition, maxScrollPosition);
+
+ CSSPoint scrollOffset = Metrics().GetVisualScrollOffset();
+ if (direction == ScrollDirection::eHorizontal) {
+ scrollOffset.x = scrollPosition;
+ } else {
+ scrollOffset.y = scrollPosition;
+ }
+ APZC_LOG("%p set scroll offset to %s from scrollbar drag\n", this,
+ ToString(scrollOffset).c_str());
+ SetVisualScrollOffset(scrollOffset);
+ ScheduleCompositeAndMaybeRepaint();
+
+ return nsEventStatus_eConsumeNoDefault;
+}
+
+nsEventStatus AsyncPanZoomController::HandleInputEvent(
+ const InputData& aEvent,
+ const ScreenToParentLayerMatrix4x4& aTransformToApzc) {
+ APZThreadUtils::AssertOnControllerThread();
+
+ nsEventStatus rv = nsEventStatus_eIgnore;
+
+ switch (aEvent.mInputType) {
+ case MULTITOUCH_INPUT: {
+ MultiTouchInput multiTouchInput = aEvent.AsMultiTouchInput();
+ RefPtr<GestureEventListener> listener = GetGestureEventListener();
+ if (listener) {
+ // We only care about screen coordinates in the gesture listener,
+ // so we don't bother transforming the event to parent layer coordinates
+ rv = listener->HandleInputEvent(multiTouchInput);
+ if (rv == nsEventStatus_eConsumeNoDefault) {
+ return rv;
+ }
+ }
+
+ if (!multiTouchInput.TransformToLocal(aTransformToApzc)) {
+ return rv;
+ }
+
+ switch (multiTouchInput.mType) {
+ case MultiTouchInput::MULTITOUCH_START:
+ rv = OnTouchStart(multiTouchInput);
+ break;
+ case MultiTouchInput::MULTITOUCH_MOVE:
+ rv = OnTouchMove(multiTouchInput);
+ break;
+ case MultiTouchInput::MULTITOUCH_END:
+ rv = OnTouchEnd(multiTouchInput);
+ break;
+ case MultiTouchInput::MULTITOUCH_CANCEL:
+ rv = OnTouchCancel(multiTouchInput);
+ break;
+ }
+ break;
+ }
+ case PANGESTURE_INPUT: {
+ PanGestureInput panGestureInput = aEvent.AsPanGestureInput();
+ if (!panGestureInput.TransformToLocal(aTransformToApzc)) {
+ return rv;
+ }
+
+ switch (panGestureInput.mType) {
+ case PanGestureInput::PANGESTURE_MAYSTART:
+ rv = OnPanMayBegin(panGestureInput);
+ break;
+ case PanGestureInput::PANGESTURE_CANCELLED:
+ rv = OnPanCancelled(panGestureInput);
+ break;
+ case PanGestureInput::PANGESTURE_START:
+ rv = OnPanBegin(panGestureInput);
+ break;
+ case PanGestureInput::PANGESTURE_PAN:
+ rv = OnPan(panGestureInput, FingersOnTouchpad::Yes);
+ break;
+ case PanGestureInput::PANGESTURE_END:
+ rv = OnPanEnd(panGestureInput);
+ break;
+ case PanGestureInput::PANGESTURE_MOMENTUMSTART:
+ rv = OnPanMomentumStart(panGestureInput);
+ break;
+ case PanGestureInput::PANGESTURE_MOMENTUMPAN:
+ rv = OnPan(panGestureInput, FingersOnTouchpad::No);
+ break;
+ case PanGestureInput::PANGESTURE_MOMENTUMEND:
+ rv = OnPanMomentumEnd(panGestureInput);
+ break;
+ case PanGestureInput::PANGESTURE_INTERRUPTED:
+ rv = OnPanInterrupted(panGestureInput);
+ break;
+ }
+ break;
+ }
+ case MOUSE_INPUT: {
+ MouseInput mouseInput = aEvent.AsMouseInput();
+ if (!mouseInput.TransformToLocal(aTransformToApzc)) {
+ return rv;
+ }
+ break;
+ }
+ case SCROLLWHEEL_INPUT: {
+ ScrollWheelInput scrollInput = aEvent.AsScrollWheelInput();
+ if (!scrollInput.TransformToLocal(aTransformToApzc)) {
+ return rv;
+ }
+
+ rv = OnScrollWheel(scrollInput);
+ break;
+ }
+ case PINCHGESTURE_INPUT: {
+ // The APZCTreeManager should take care of ensuring that only root-content
+ // APZCs get pinch inputs.
+ MOZ_ASSERT(IsRootContent());
+ PinchGestureInput pinchInput = aEvent.AsPinchGestureInput();
+ if (!pinchInput.TransformToLocal(aTransformToApzc)) {
+ return rv;
+ }
+
+ rv = HandleGestureEvent(pinchInput);
+ break;
+ }
+ case TAPGESTURE_INPUT: {
+ TapGestureInput tapInput = aEvent.AsTapGestureInput();
+ if (!tapInput.TransformToLocal(aTransformToApzc)) {
+ return rv;
+ }
+
+ rv = HandleGestureEvent(tapInput);
+ break;
+ }
+ case KEYBOARD_INPUT: {
+ const KeyboardInput& keyInput = aEvent.AsKeyboardInput();
+ rv = OnKeyboard(keyInput);
+ break;
+ }
+ }
+
+ return rv;
+}
+
+nsEventStatus AsyncPanZoomController::HandleGestureEvent(
+ const InputData& aEvent) {
+ APZThreadUtils::AssertOnControllerThread();
+
+ nsEventStatus rv = nsEventStatus_eIgnore;
+
+ switch (aEvent.mInputType) {
+ case PINCHGESTURE_INPUT: {
+ // This may be invoked via a one-touch-pinch gesture from
+ // GestureEventListener. In that case we want redirect it to the enclosing
+ // root-content APZC.
+ if (!IsRootContent()) {
+ if (APZCTreeManager* treeManagerLocal = GetApzcTreeManager()) {
+ if (RefPtr<AsyncPanZoomController> root =
+ treeManagerLocal->FindZoomableApzc(this)) {
+ rv = root->HandleGestureEvent(aEvent);
+ }
+ }
+ break;
+ }
+ PinchGestureInput pinchGestureInput = aEvent.AsPinchGestureInput();
+ pinchGestureInput.TransformToLocal(GetTransformToThis());
+ switch (pinchGestureInput.mType) {
+ case PinchGestureInput::PINCHGESTURE_START:
+ rv = OnScaleBegin(pinchGestureInput);
+ break;
+ case PinchGestureInput::PINCHGESTURE_SCALE:
+ rv = OnScale(pinchGestureInput);
+ break;
+ case PinchGestureInput::PINCHGESTURE_FINGERLIFTED:
+ case PinchGestureInput::PINCHGESTURE_END:
+ rv = OnScaleEnd(pinchGestureInput);
+ break;
+ }
+ break;
+ }
+ case TAPGESTURE_INPUT: {
+ TapGestureInput tapGestureInput = aEvent.AsTapGestureInput();
+ tapGestureInput.TransformToLocal(GetTransformToThis());
+ switch (tapGestureInput.mType) {
+ case TapGestureInput::TAPGESTURE_LONG:
+ rv = OnLongPress(tapGestureInput);
+ break;
+ case TapGestureInput::TAPGESTURE_LONG_UP:
+ rv = OnLongPressUp(tapGestureInput);
+ break;
+ case TapGestureInput::TAPGESTURE_UP:
+ rv = OnSingleTapUp(tapGestureInput);
+ break;
+ case TapGestureInput::TAPGESTURE_CONFIRMED:
+ rv = OnSingleTapConfirmed(tapGestureInput);
+ break;
+ case TapGestureInput::TAPGESTURE_DOUBLE:
+ // This means that double tapping on an oop iframe "works" in that we
+ // don't try (and fail) to zoom the oop iframe. But it also means it
+ // is impossible to zoom to some content inside that oop iframe.
+ // Instead the best we can do is zoom to the oop iframe itself. This
+ // is consistent with what Chrome and Safari currently do. Allowing
+ // zooming to content inside an oop iframe would be decently
+ // complicated and it doesn't seem worth it. Bug 1715179 is on file
+ // for this.
+ if (!IsRootContent()) {
+ if (APZCTreeManager* treeManagerLocal = GetApzcTreeManager()) {
+ if (RefPtr<AsyncPanZoomController> root =
+ treeManagerLocal->FindZoomableApzc(this)) {
+ rv = root->OnDoubleTap(tapGestureInput);
+ }
+ }
+ break;
+ }
+ rv = OnDoubleTap(tapGestureInput);
+ break;
+ case TapGestureInput::TAPGESTURE_SECOND:
+ rv = OnSecondTap(tapGestureInput);
+ break;
+ case TapGestureInput::TAPGESTURE_CANCEL:
+ rv = OnCancelTap(tapGestureInput);
+ break;
+ }
+ break;
+ }
+ default:
+ MOZ_ASSERT_UNREACHABLE("Unhandled input event");
+ break;
+ }
+
+ return rv;
+}
+
+void AsyncPanZoomController::StartAutoscroll(const ScreenPoint& aPoint) {
+ // Cancel any existing animation.
+ CancelAnimation();
+
+ SetState(AUTOSCROLL);
+ StartAnimation(new AutoscrollAnimation(*this, aPoint));
+}
+
+void AsyncPanZoomController::StopAutoscroll() {
+ if (mState == AUTOSCROLL) {
+ CancelAnimation(TriggeredExternally);
+ }
+}
+
+nsEventStatus AsyncPanZoomController::OnTouchStart(
+ const MultiTouchInput& aEvent) {
+ APZC_LOG_DETAIL("got a touch-start in state %s\n", this,
+ ToString(mState).c_str());
+ mPanDirRestricted = false;
+
+ switch (mState) {
+ case FLING:
+ case ANIMATING_ZOOM:
+ case SMOOTH_SCROLL:
+ case SMOOTHMSD_SCROLL:
+ case OVERSCROLL_ANIMATION:
+ case WHEEL_SCROLL:
+ case KEYBOARD_SCROLL:
+ case PAN_MOMENTUM:
+ case AUTOSCROLL:
+ MOZ_ASSERT(GetCurrentTouchBlock());
+ GetCurrentTouchBlock()->GetOverscrollHandoffChain()->CancelAnimations(
+ ExcludeOverscroll);
+ [[fallthrough]];
+ case SCROLLBAR_DRAG:
+ case NOTHING: {
+ ParentLayerPoint point = GetFirstTouchPoint(aEvent);
+ mLastTouch.mPosition = mStartTouch = GetFirstExternalTouchPoint(aEvent);
+ StartTouch(point, aEvent.mTimeStamp);
+ if (RefPtr<GeckoContentController> controller =
+ GetGeckoContentController()) {
+ MOZ_ASSERT(GetCurrentTouchBlock());
+ controller->NotifyAPZStateChange(
+ GetGuid(), APZStateChange::eStartTouch,
+ GetCurrentTouchBlock()->GetOverscrollHandoffChain()->CanBePanned(
+ this),
+ Some(GetCurrentTouchBlock()->GetBlockId()));
+ }
+ mLastTouch.mTimeStamp = mTouchStartTime = aEvent.mTimeStamp;
+ SetState(TOUCHING);
+ break;
+ }
+ case TOUCHING:
+ case PANNING:
+ case PANNING_LOCKED_X:
+ case PANNING_LOCKED_Y:
+ case PINCHING:
+ NS_WARNING("Received impossible touch in OnTouchStart");
+ break;
+ }
+
+ return nsEventStatus_eConsumeNoDefault;
+}
+
+nsEventStatus AsyncPanZoomController::OnTouchMove(
+ const MultiTouchInput& aEvent) {
+ APZC_LOG_DETAIL("got a touch-move in state %s\n", this,
+ ToString(mState).c_str());
+ switch (mState) {
+ case FLING:
+ case SMOOTHMSD_SCROLL:
+ case NOTHING:
+ case ANIMATING_ZOOM:
+ // May happen if the user double-taps and drags without lifting after the
+ // second tap. Ignore the move if this happens.
+ return nsEventStatus_eIgnore;
+
+ case TOUCHING: {
+ ScreenCoord panThreshold = GetTouchStartTolerance();
+ ExternalPoint extPoint = GetFirstExternalTouchPoint(aEvent);
+ Maybe<std::pair<MultiTouchInput, MultiTouchInput>> splitEvent;
+
+ // We intentionally skip the UpdateWithTouchAtDevicePoint call when the
+ // panThreshold is zero. This ensures more deterministic behaviour during
+ // testing. If we call that, Axis::mPos gets updated to the point of this
+ // touchmove event, but we "consume" the move to overcome the
+ // panThreshold, so it's hard to pan a specific amount reliably from a
+ // mochitest.
+ if (panThreshold > 0.0f) {
+ const float vectorLength = PanVector(extPoint).Length();
+
+ if (vectorLength < panThreshold) {
+ UpdateWithTouchAtDevicePoint(aEvent);
+ mLastTouch = {extPoint, aEvent.mTimeStamp};
+
+ return nsEventStatus_eIgnore;
+ }
+
+ splitEvent = MaybeSplitTouchMoveEvent(aEvent, panThreshold,
+ vectorLength, extPoint);
+
+ UpdateWithTouchAtDevicePoint(splitEvent ? splitEvent->first : aEvent);
+ }
+
+ nsEventStatus result;
+ const MultiTouchInput& firstEvent =
+ splitEvent ? splitEvent->first : aEvent;
+
+ MOZ_ASSERT(GetCurrentTouchBlock());
+ if (GetCurrentTouchBlock()->TouchActionAllowsPanningXY()) {
+ // In the calls to StartPanning() below, the first argument needs to be
+ // the External position of |firstEvent|.
+ // However, instead of computing that using
+ // GetFirstExternalTouchPoint(firstEvent), we pass |extPoint| which
+ // has been modified by MaybeSplitTouchMoveEvent() to the desired
+ // value. This is a workaround for the fact that recomputing the
+ // External point would require a round-trip through |mScreenPoint|
+ // which is an integer.
+
+ // User tries to trigger a touch behavior. If allowed touch behavior is
+ // vertical pan + horizontal pan (touch-action value is equal to AUTO)
+ // we can return ConsumeNoDefault status immediately to trigger cancel
+ // event further.
+ // It should happen independent of the parent type (whether it is
+ // scrolling or not).
+ StartPanning(extPoint, firstEvent.mTimeStamp);
+ result = nsEventStatus_eConsumeNoDefault;
+ } else {
+ result = StartPanning(extPoint, firstEvent.mTimeStamp);
+ }
+
+ if (splitEvent && IsInPanningState()) {
+ TrackTouch(splitEvent->second);
+ return nsEventStatus_eConsumeNoDefault;
+ }
+
+ return result;
+ }
+
+ case PANNING:
+ case PANNING_LOCKED_X:
+ case PANNING_LOCKED_Y:
+ case PAN_MOMENTUM:
+ TrackTouch(aEvent);
+ return nsEventStatus_eConsumeNoDefault;
+
+ case PINCHING:
+ // The scale gesture listener should have handled this.
+ NS_WARNING(
+ "Gesture listener should have handled pinching in OnTouchMove.");
+ return nsEventStatus_eIgnore;
+
+ case SMOOTH_SCROLL:
+ case WHEEL_SCROLL:
+ case KEYBOARD_SCROLL:
+ case OVERSCROLL_ANIMATION:
+ case AUTOSCROLL:
+ case SCROLLBAR_DRAG:
+ // Should not receive a touch-move in the OVERSCROLL_ANIMATION state
+ // as touch blocks that begin in an overscrolled state cancel the
+ // animation. The same is true for wheel scroll animations.
+ NS_WARNING("Received impossible touch in OnTouchMove");
+ break;
+ }
+
+ return nsEventStatus_eConsumeNoDefault;
+}
+
+nsEventStatus AsyncPanZoomController::OnTouchEnd(
+ const MultiTouchInput& aEvent) {
+ APZC_LOG_DETAIL("got a touch-end in state %s\n", this,
+ ToString(mState).c_str());
+ OnTouchEndOrCancel();
+
+ // In case no touch behavior triggered previously we can avoid sending
+ // scroll events or requesting content repaint. This condition is added
+ // to make tests consistent - in case touch-action is NONE (and therefore
+ // no pans/zooms can be performed) we expected neither scroll or repaint
+ // events.
+ if (mState != NOTHING) {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ }
+
+ switch (mState) {
+ case FLING:
+ // Should never happen.
+ NS_WARNING("Received impossible touch end in OnTouchEnd.");
+ [[fallthrough]];
+ case ANIMATING_ZOOM:
+ case SMOOTHMSD_SCROLL:
+ case NOTHING:
+ // May happen if the user double-taps and drags without lifting after the
+ // second tap. Ignore if this happens.
+ return nsEventStatus_eIgnore;
+
+ case TOUCHING:
+ // We may have some velocity stored on the axis from move events
+ // that were not big enough to trigger scrolling. Clear that out.
+ SetVelocityVector(ParentLayerPoint(0, 0));
+ MOZ_ASSERT(GetCurrentTouchBlock());
+ APZC_LOG("%p still has %u touch points active\n", this,
+ GetCurrentTouchBlock()->GetActiveTouchCount());
+ // In cases where the user is panning, then taps the second finger without
+ // entering a pinch, we will arrive here when the second finger is lifted.
+ // However the first finger is still down so we want to remain in state
+ // TOUCHING.
+ if (GetCurrentTouchBlock()->GetActiveTouchCount() == 0) {
+ // It's possible we may be overscrolled if the user tapped during a
+ // previous overscroll pan. Make sure to snap back in this situation.
+ // An ancestor APZC could be overscrolled instead of this APZC, so
+ // walk the handoff chain as well.
+ GetCurrentTouchBlock()
+ ->GetOverscrollHandoffChain()
+ ->SnapBackOverscrolledApzc(this);
+ mFlingAccelerator.Reset();
+ // SnapBackOverscrolledApzc() will put any APZC it causes to snap back
+ // into the OVERSCROLL_ANIMATION state. If that's not us, since we're
+ // done TOUCHING enter the NOTHING state.
+ if (mState != OVERSCROLL_ANIMATION) {
+ SetState(NOTHING);
+ }
+ }
+ return nsEventStatus_eIgnore;
+
+ case PANNING:
+ case PANNING_LOCKED_X:
+ case PANNING_LOCKED_Y:
+ case PAN_MOMENTUM: {
+ MOZ_ASSERT(GetCurrentTouchBlock());
+ EndTouch(aEvent.mTimeStamp, Axis::ClearAxisLock::Yes);
+ return HandleEndOfPan();
+ }
+ case PINCHING:
+ SetState(NOTHING);
+ // Scale gesture listener should have handled this.
+ NS_WARNING(
+ "Gesture listener should have handled pinching in OnTouchEnd.");
+ return nsEventStatus_eIgnore;
+
+ case SMOOTH_SCROLL:
+ case WHEEL_SCROLL:
+ case KEYBOARD_SCROLL:
+ case OVERSCROLL_ANIMATION:
+ case AUTOSCROLL:
+ case SCROLLBAR_DRAG:
+ // Should not receive a touch-end in the OVERSCROLL_ANIMATION state
+ // as touch blocks that begin in an overscrolled state cancel the
+ // animation. The same is true for WHEEL_SCROLL.
+ NS_WARNING("Received impossible touch in OnTouchEnd");
+ break;
+ }
+
+ return nsEventStatus_eConsumeNoDefault;
+}
+
+nsEventStatus AsyncPanZoomController::OnTouchCancel(
+ const MultiTouchInput& aEvent) {
+ APZC_LOG_DETAIL("got a touch-cancel in state %s\n", this,
+ ToString(mState).c_str());
+ OnTouchEndOrCancel();
+ CancelAnimationAndGestureState();
+ return nsEventStatus_eConsumeNoDefault;
+}
+
+nsEventStatus AsyncPanZoomController::OnScaleBegin(
+ const PinchGestureInput& aEvent) {
+ APZC_LOG_DETAIL("got a scale-begin in state %s\n", this,
+ ToString(mState).c_str());
+
+ mPinchLocked = false;
+ mPinchPaintTimerSet = false;
+ // Note that there may not be a touch block at this point, if we received the
+ // PinchGestureEvent directly from widget code without any touch events.
+ if (HasReadyTouchBlock() &&
+ !GetCurrentTouchBlock()->TouchActionAllowsPinchZoom()) {
+ return nsEventStatus_eIgnore;
+ }
+
+ // For platforms that don't support APZ zooming, dispatch a message to the
+ // content controller, it may want to do something else with this gesture.
+ // FIXME: bug 1525793 -- this may need to handle zooming or not on a
+ // per-document basis.
+ if (!StaticPrefs::apz_allow_zooming()) {
+ if (RefPtr<GeckoContentController> controller =
+ GetGeckoContentController()) {
+ APZC_LOG("%p notifying controller of pinch gesture start\n", this);
+ controller->NotifyPinchGesture(
+ aEvent.mType, GetGuid(),
+ ViewAs<LayoutDevicePixel>(
+ aEvent.mFocusPoint,
+ PixelCastJustification::
+ LayoutDeviceIsScreenForUntransformedEvent),
+ 0, aEvent.modifiers);
+ }
+ }
+
+ SetState(PINCHING);
+ Telemetry::Accumulate(Telemetry::APZ_ZOOM_PINCHSOURCE, (int)aEvent.mSource);
+ SetVelocityVector(ParentLayerPoint(0, 0));
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ mLastZoomFocus =
+ aEvent.mLocalFocusPoint - Metrics().GetCompositionBounds().TopLeft();
+
+ mPinchEventBuffer.push(aEvent);
+
+ return nsEventStatus_eConsumeNoDefault;
+}
+
+nsEventStatus AsyncPanZoomController::OnScale(const PinchGestureInput& aEvent) {
+ APZC_LOG_DETAIL("got a scale in state %s\n", this, ToString(mState).c_str());
+
+ if (HasReadyTouchBlock() &&
+ !GetCurrentTouchBlock()->TouchActionAllowsPinchZoom()) {
+ return nsEventStatus_eIgnore;
+ }
+
+ if (mState != PINCHING) {
+ return nsEventStatus_eConsumeNoDefault;
+ }
+
+ mPinchEventBuffer.push(aEvent);
+ HandlePinchLocking(aEvent);
+ bool allowZoom = ZoomConstraintsAllowZoom() && !mPinchLocked;
+
+ // If we are pinch-locked, this is a two-finger pan.
+ // Tracking panning distance and velocity.
+ // UpdateWithTouchAtDevicePoint() acquires the tree lock, so
+ // it cannot be called while the mRecursiveMutex lock is held.
+ if (mPinchLocked) {
+ mX.UpdateWithTouchAtDevicePoint(aEvent.mLocalFocusPoint.x,
+ aEvent.mTimeStamp);
+ mY.UpdateWithTouchAtDevicePoint(aEvent.mLocalFocusPoint.y,
+ aEvent.mTimeStamp);
+ }
+
+ // FIXME: bug 1525793 -- this may need to handle zooming or not on a
+ // per-document basis.
+ if (!StaticPrefs::apz_allow_zooming()) {
+ if (RefPtr<GeckoContentController> controller =
+ GetGeckoContentController()) {
+ APZC_LOG("%p notifying controller of pinch gesture\n", this);
+ controller->NotifyPinchGesture(
+ aEvent.mType, GetGuid(),
+ ViewAs<LayoutDevicePixel>(
+ aEvent.mFocusPoint,
+ PixelCastJustification::
+ LayoutDeviceIsScreenForUntransformedEvent),
+ ViewAs<LayoutDevicePixel>(
+ aEvent.mCurrentSpan - aEvent.mPreviousSpan,
+ PixelCastJustification::
+ LayoutDeviceIsScreenForUntransformedEvent),
+ aEvent.modifiers);
+ }
+ }
+
+ {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ // Only the root APZC is zoomable, and the root APZC is not allowed to have
+ // different x and y scales. If it did, the calculations in this function
+ // would have to be adjusted (as e.g. it would no longer be valid to take
+ // the minimum or maximum of the ratios of the widths and heights of the
+ // page rect and the composition bounds).
+ MOZ_ASSERT(Metrics().IsRootContent());
+
+ CSSToParentLayerScale userZoom = Metrics().GetZoom();
+ ParentLayerPoint focusPoint =
+ aEvent.mLocalFocusPoint - Metrics().GetCompositionBounds().TopLeft();
+ CSSPoint cssFocusPoint;
+ if (Metrics().GetZoom() != CSSToParentLayerScale(0)) {
+ cssFocusPoint = focusPoint / Metrics().GetZoom();
+ }
+
+ ParentLayerPoint focusChange = mLastZoomFocus - focusPoint;
+ mLastZoomFocus = focusPoint;
+ // If displacing by the change in focus point will take us off page bounds,
+ // then reduce the displacement such that it doesn't.
+ focusChange.x -= mX.DisplacementWillOverscrollAmount(focusChange.x);
+ focusChange.y -= mY.DisplacementWillOverscrollAmount(focusChange.y);
+ if (userZoom != CSSToParentLayerScale(0)) {
+ ScrollBy(focusChange / userZoom);
+ }
+
+ // If the span is zero or close to it, we don't want to process this zoom
+ // change because we're going to get wonky numbers for the spanRatio. So
+ // let's bail out here. Note that we do this after the focus-change-scroll
+ // above, so that if we have a pinch with zero span but changing focus,
+ // such as generated by some Synaptics touchpads on Windows, we still
+ // scroll properly.
+ float prevSpan = aEvent.mPreviousSpan;
+ if (fabsf(prevSpan) <= EPSILON || fabsf(aEvent.mCurrentSpan) <= EPSILON) {
+ // We might have done a nonzero ScrollBy above, so update metrics and
+ // repaint/recomposite
+ ScheduleCompositeAndMaybeRepaint();
+ return nsEventStatus_eConsumeNoDefault;
+ }
+ float spanRatio = aEvent.mCurrentSpan / aEvent.mPreviousSpan;
+
+ // When we zoom in with focus, we can zoom too much towards the boundaries
+ // that we actually go over them. These are the needed displacements along
+ // either axis such that we don't overscroll the boundaries when zooming.
+ CSSPoint neededDisplacement;
+
+ CSSToParentLayerScale realMinZoom = mZoomConstraints.mMinZoom;
+ CSSToParentLayerScale realMaxZoom = mZoomConstraints.mMaxZoom;
+ realMinZoom.scale =
+ std::max(realMinZoom.scale, Metrics().GetCompositionBounds().Width() /
+ Metrics().GetScrollableRect().Width());
+ realMinZoom.scale =
+ std::max(realMinZoom.scale, Metrics().GetCompositionBounds().Height() /
+ Metrics().GetScrollableRect().Height());
+ if (realMaxZoom < realMinZoom) {
+ realMaxZoom = realMinZoom;
+ }
+
+ bool doScale = allowZoom && ((spanRatio > 1.0 && userZoom < realMaxZoom) ||
+ (spanRatio < 1.0 && userZoom > realMinZoom));
+
+ if (doScale) {
+ spanRatio = clamped(spanRatio, realMinZoom.scale / userZoom.scale,
+ realMaxZoom.scale / userZoom.scale);
+
+ // Note that the spanRatio here should never put us into OVERSCROLL_BOTH
+ // because up above we clamped it.
+ neededDisplacement.x =
+ -mX.ScaleWillOverscrollAmount(spanRatio, cssFocusPoint.x);
+ neededDisplacement.y =
+ -mY.ScaleWillOverscrollAmount(spanRatio, cssFocusPoint.y);
+
+ ScaleWithFocus(spanRatio, cssFocusPoint);
+
+ if (neededDisplacement != CSSPoint()) {
+ ScrollBy(neededDisplacement);
+ }
+
+ // We don't want to redraw on every scale, so throttle it.
+ if (!mPinchPaintTimerSet) {
+ const int delay = StaticPrefs::apz_scale_repaint_delay_ms();
+ if (delay >= 0) {
+ if (RefPtr<GeckoContentController> controller =
+ GetGeckoContentController()) {
+ mPinchPaintTimerSet = true;
+ controller->PostDelayedTask(
+ NewRunnableMethod(
+ "layers::AsyncPanZoomController::"
+ "DoDelayedRequestContentRepaint",
+ this,
+ &AsyncPanZoomController::DoDelayedRequestContentRepaint),
+ delay);
+ }
+ }
+ } else if (apz::AboutToCheckerboard(mLastContentPaintMetrics,
+ Metrics())) {
+ // If we already scheduled a throttled repaint request but are also
+ // in danger of checkerboarding soon, trigger the repaint request to
+ // go out immediately. This should reduce the amount of time we spend
+ // checkerboarding.
+ //
+ // Note that if we remain in this "about to
+ // checkerboard" state over a period of time with multiple pinch input
+ // events (which is quite likely), then we will flip-flop between taking
+ // the above branch (!mPinchPaintTimerSet) and this branch (which will
+ // flush the repaint request and reset mPinchPaintTimerSet to false).
+ // This is sort of desirable because it halves the number of repaint
+ // requests we send, and therefore reduces IPC traffic.
+ // Keep in mind that many of these repaint requests will be ignored on
+ // the main-thread anyway due to the resolution mismatch - the first
+ // repaint request will be honored because APZ's notion of the painted
+ // resolution matches the actual main thread resolution, but that first
+ // repaint request will change the resolution on the main thread.
+ // Subsequent repaint requests will be ignored in APZCCallbackHelper, at
+ // https://searchfox.org/mozilla-central/rev/e0eb861a187f0bb6d994228f2e0e49b2c9ee455e/gfx/layers/apz/util/APZCCallbackHelper.cpp#331-338,
+ // until we receive a NotifyLayersUpdated call that re-syncs APZ's
+ // notion of the painted resolution to the main thread. These ignored
+ // repaint requests are contributing to IPC traffic needlessly, and so
+ // halving the number of repaint requests (as mentioned above) seems
+ // desirable.
+ DoDelayedRequestContentRepaint();
+ }
+ } else {
+ // Trigger a repaint request after scrolling.
+ RequestContentRepaint();
+ }
+
+ // We did a ScrollBy call above even if we didn't do a scale, so we
+ // should composite for that.
+ ScheduleComposite();
+ }
+
+ return nsEventStatus_eConsumeNoDefault;
+}
+
+nsEventStatus AsyncPanZoomController::OnScaleEnd(
+ const PinchGestureInput& aEvent) {
+ APZC_LOG_DETAIL("got a scale-end in state %s\n", this,
+ ToString(mState).c_str());
+
+ mPinchPaintTimerSet = false;
+
+ if (HasReadyTouchBlock() &&
+ !GetCurrentTouchBlock()->TouchActionAllowsPinchZoom()) {
+ return nsEventStatus_eIgnore;
+ }
+
+ // FIXME: bug 1525793 -- this may need to handle zooming or not on a
+ // per-document basis.
+ if (!StaticPrefs::apz_allow_zooming()) {
+ if (RefPtr<GeckoContentController> controller =
+ GetGeckoContentController()) {
+ controller->NotifyPinchGesture(
+ aEvent.mType, GetGuid(),
+ ViewAs<LayoutDevicePixel>(
+ aEvent.mFocusPoint,
+ PixelCastJustification::
+ LayoutDeviceIsScreenForUntransformedEvent),
+ 0, aEvent.modifiers);
+ }
+ }
+
+ {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ ScheduleComposite();
+ RequestContentRepaint();
+ }
+
+ mPinchEventBuffer.clear();
+
+ if (aEvent.mType == PinchGestureInput::PINCHGESTURE_FINGERLIFTED) {
+ // One finger is still down, so transition to a TOUCHING state
+ if (!mPinchLocked) {
+ mPanDirRestricted = false;
+ mLastTouch.mPosition = mStartTouch =
+ ToExternalPoint(aEvent.mScreenOffset, aEvent.mFocusPoint);
+ mLastTouch.mTimeStamp = mTouchStartTime = aEvent.mTimeStamp;
+ StartTouch(aEvent.mLocalFocusPoint, aEvent.mTimeStamp);
+ SetState(TOUCHING);
+ } else {
+ // If we are pinch locked, StartTouch() was already called
+ // when we entered the pinch lock.
+ StartPanning(ToExternalPoint(aEvent.mScreenOffset, aEvent.mFocusPoint),
+ aEvent.mTimeStamp);
+ }
+ } else {
+ // Otherwise, handle the gesture being completely done.
+
+ // Some of the code paths below, like ScrollSnap() or HandleEndOfPan(),
+ // may start an animation, but otherwise we want to end up in the NOTHING
+ // state. To avoid state change notification churn, we use a
+ // notification blocker.
+ bool stateWasPinching = (mState == PINCHING);
+ StateChangeNotificationBlocker blocker(this);
+ SetState(NOTHING);
+
+ if (ZoomConstraintsAllowZoom()) {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+
+ // We can get into a situation where we are overscrolled at the end of a
+ // pinch if we go into overscroll with a two-finger pan, and then turn
+ // that into a pinch by increasing the span sufficiently. In such a case,
+ // there is no snap-back animation to get us out of overscroll, so we need
+ // to get out of it somehow.
+ // Moreover, in cases of scroll handoff, the overscroll can be on an APZC
+ // further up in the handoff chain rather than on the current APZC, so
+ // we need to clear overscroll along the entire handoff chain.
+ if (HasReadyTouchBlock()) {
+ GetCurrentTouchBlock()->GetOverscrollHandoffChain()->ClearOverscroll();
+ } else {
+ ClearOverscroll();
+ }
+ // Along with clearing the overscroll, we also want to snap to the nearest
+ // snap point as appropriate.
+ ScrollSnap(ScrollSnapFlags::IntendedEndPosition);
+ } else {
+ // when zoom is not allowed
+ EndTouch(aEvent.mTimeStamp, Axis::ClearAxisLock::Yes);
+ if (stateWasPinching) {
+ // still pinching
+ if (HasReadyTouchBlock()) {
+ return HandleEndOfPan();
+ }
+ }
+ }
+ }
+ return nsEventStatus_eConsumeNoDefault;
+}
+
+nsEventStatus AsyncPanZoomController::HandleEndOfPan() {
+ MOZ_ASSERT(!mAnimation);
+ MOZ_ASSERT(GetCurrentTouchBlock() || GetCurrentPanGestureBlock());
+ GetCurrentInputBlock()->GetOverscrollHandoffChain()->FlushRepaints();
+ ParentLayerPoint flingVelocity = GetVelocityVector();
+
+ // Clear our velocities; if DispatchFling() gives the fling to us,
+ // the fling velocity gets *added* to our existing velocity in
+ // AcceptFling().
+ SetVelocityVector(ParentLayerPoint(0, 0));
+ // Clear our state so that we don't stay in the PANNING state
+ // if DispatchFling() gives the fling to somone else. However,
+ // don't send the state change notification until we've determined
+ // what our final state is to avoid notification churn.
+ StateChangeNotificationBlocker blocker(this);
+ SetState(NOTHING);
+
+ APZC_LOG("%p starting a fling animation if %f > %f\n", this,
+ flingVelocity.Length().value,
+ StaticPrefs::apz_fling_min_velocity_threshold());
+
+ if (flingVelocity.Length() <=
+ StaticPrefs::apz_fling_min_velocity_threshold()) {
+ // Relieve overscroll now if needed, since we will not transition to a fling
+ // animation and then an overscroll animation, and relieve it then.
+ GetCurrentInputBlock()
+ ->GetOverscrollHandoffChain()
+ ->SnapBackOverscrolledApzc(this);
+ mFlingAccelerator.Reset();
+ return nsEventStatus_eConsumeNoDefault;
+ }
+
+ // Make a local copy of the tree manager pointer and check that it's not
+ // null before calling DispatchFling(). This is necessary because Destroy(),
+ // which nulls out mTreeManager, could be called concurrently.
+ if (APZCTreeManager* treeManagerLocal = GetApzcTreeManager()) {
+ const FlingHandoffState handoffState{
+ flingVelocity,
+ GetCurrentInputBlock()->GetOverscrollHandoffChain(),
+ Some(mTouchStartRestingTimeBeforePan),
+ mMinimumVelocityDuringPan.valueOr(0),
+ false /* not handoff */,
+ GetCurrentInputBlock()->GetScrolledApzc()};
+ treeManagerLocal->DispatchFling(this, handoffState);
+ }
+ return nsEventStatus_eConsumeNoDefault;
+}
+
+Maybe<LayoutDevicePoint> AsyncPanZoomController::ConvertToGecko(
+ const ScreenIntPoint& aPoint) {
+ if (APZCTreeManager* treeManagerLocal = GetApzcTreeManager()) {
+ if (Maybe<ScreenIntPoint> layoutPoint =
+ treeManagerLocal->ConvertToGecko(aPoint, this)) {
+ return Some(LayoutDevicePoint(ViewAs<LayoutDevicePixel>(
+ *layoutPoint,
+ PixelCastJustification::LayoutDeviceIsScreenForUntransformedEvent)));
+ }
+ }
+ return Nothing();
+}
+
+OuterCSSCoord AsyncPanZoomController::ConvertScrollbarPoint(
+ const ParentLayerPoint& aScrollbarPoint,
+ const ScrollbarData& aThumbData) const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+
+ CSSPoint scrollbarPoint;
+ if (Metrics().GetZoom() != CSSToParentLayerScale(0)) {
+ // First, get it into the right coordinate space.
+ scrollbarPoint = aScrollbarPoint / Metrics().GetZoom();
+ }
+
+ // The scrollbar can be transformed with the frame but the pres shell
+ // resolution is only applied to the scroll frame.
+ OuterCSSPoint outerScrollbarPoint =
+ scrollbarPoint * Metrics().GetCSSToOuterCSSScale();
+
+ // Now, get it to be relative to the beginning of the scroll track.
+ OuterCSSRect cssCompositionBound =
+ Metrics().CalculateCompositionBoundsInOuterCssPixels();
+ return GetAxisStart(*aThumbData.mDirection, outerScrollbarPoint) -
+ GetAxisStart(*aThumbData.mDirection, cssCompositionBound) -
+ aThumbData.mScrollTrackStart;
+}
+
+static bool AllowsScrollingMoreThanOnePage(double aMultiplier) {
+ return Abs(aMultiplier) >=
+ EventStateManager::MIN_MULTIPLIER_VALUE_ALLOWING_OVER_ONE_PAGE_SCROLL;
+}
+
+ParentLayerPoint AsyncPanZoomController::GetScrollWheelDelta(
+ const ScrollWheelInput& aEvent) const {
+ return GetScrollWheelDelta(aEvent, aEvent.mDeltaX, aEvent.mDeltaY,
+ aEvent.mUserDeltaMultiplierX,
+ aEvent.mUserDeltaMultiplierY);
+}
+
+ParentLayerPoint AsyncPanZoomController::GetScrollWheelDelta(
+ const ScrollWheelInput& aEvent, double aDeltaX, double aDeltaY,
+ double aMultiplierX, double aMultiplierY) const {
+ ParentLayerSize scrollAmount;
+ ParentLayerSize pageScrollSize;
+
+ {
+ // Grab the lock to access the frame metrics.
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ LayoutDeviceIntSize scrollAmountLD = mScrollMetadata.GetLineScrollAmount();
+ LayoutDeviceIntSize pageScrollSizeLD =
+ mScrollMetadata.GetPageScrollAmount();
+ scrollAmount = scrollAmountLD / Metrics().GetDevPixelsPerCSSPixel() *
+ Metrics().GetZoom();
+ pageScrollSize = pageScrollSizeLD / Metrics().GetDevPixelsPerCSSPixel() *
+ Metrics().GetZoom();
+ }
+
+ ParentLayerPoint delta;
+ switch (aEvent.mDeltaType) {
+ case ScrollWheelInput::SCROLLDELTA_LINE: {
+ delta.x = aDeltaX * scrollAmount.width;
+ delta.y = aDeltaY * scrollAmount.height;
+ break;
+ }
+ case ScrollWheelInput::SCROLLDELTA_PAGE: {
+ delta.x = aDeltaX * pageScrollSize.width;
+ delta.y = aDeltaY * pageScrollSize.height;
+ break;
+ }
+ case ScrollWheelInput::SCROLLDELTA_PIXEL: {
+ delta = ToParentLayerCoordinates(ScreenPoint(aDeltaX, aDeltaY),
+ aEvent.mOrigin);
+ break;
+ }
+ }
+
+ // Apply user-set multipliers.
+ delta.x *= aMultiplierX;
+ delta.y *= aMultiplierY;
+
+ // For the conditions under which we allow system scroll overrides, see
+ // WidgetWheelEvent::OverriddenDelta{X,Y}.
+ // Note that we do *not* restrict this to the root content, see bug 1217715
+ // for discussion on this.
+ if (StaticPrefs::mousewheel_system_scroll_override_enabled() &&
+ !aEvent.IsCustomizedByUserPrefs() &&
+ aEvent.mDeltaType == ScrollWheelInput::SCROLLDELTA_LINE &&
+ aEvent.mAllowToOverrideSystemScrollSpeed) {
+ delta.x = WidgetWheelEvent::ComputeOverriddenDelta(delta.x, false);
+ delta.y = WidgetWheelEvent::ComputeOverriddenDelta(delta.y, true);
+ }
+
+ // If this is a line scroll, and this event was part of a scroll series, then
+ // it might need extra acceleration. See WheelHandlingHelper.cpp.
+ if (aEvent.mDeltaType == ScrollWheelInput::SCROLLDELTA_LINE &&
+ aEvent.mScrollSeriesNumber > 0) {
+ int32_t start = StaticPrefs::mousewheel_acceleration_start();
+ if (start >= 0 && aEvent.mScrollSeriesNumber >= uint32_t(start)) {
+ int32_t factor = StaticPrefs::mousewheel_acceleration_factor();
+ if (factor > 0) {
+ delta.x = ComputeAcceleratedWheelDelta(
+ delta.x, aEvent.mScrollSeriesNumber, factor);
+ delta.y = ComputeAcceleratedWheelDelta(
+ delta.y, aEvent.mScrollSeriesNumber, factor);
+ }
+ }
+ }
+
+ // We shouldn't scroll more than one page at once except when the
+ // user preference is large.
+ if (!AllowsScrollingMoreThanOnePage(aMultiplierX) &&
+ Abs(delta.x) > pageScrollSize.width) {
+ delta.x = (delta.x >= 0) ? pageScrollSize.width : -pageScrollSize.width;
+ }
+ if (!AllowsScrollingMoreThanOnePage(aMultiplierY) &&
+ Abs(delta.y) > pageScrollSize.height) {
+ delta.y = (delta.y >= 0) ? pageScrollSize.height : -pageScrollSize.height;
+ }
+
+ return delta;
+}
+
+nsEventStatus AsyncPanZoomController::OnKeyboard(const KeyboardInput& aEvent) {
+ // Mark that this APZC has async key scrolled
+ mTestHasAsyncKeyScrolled = true;
+
+ // Calculate the destination for this keyboard scroll action
+ CSSPoint destination = GetKeyboardDestination(aEvent.mAction);
+ ScrollOrigin scrollOrigin =
+ SmoothScrollAnimation::GetScrollOriginForAction(aEvent.mAction.mType);
+ Maybe<CSSSnapTarget> snapTarget = MaybeAdjustDestinationForScrollSnapping(
+ aEvent, destination, GetScrollSnapFlagsForKeyboardAction(aEvent.mAction));
+ ScrollMode scrollMode = apz::GetScrollModeForOrigin(scrollOrigin);
+
+ RecordScrollPayload(aEvent.mTimeStamp);
+ // If the scrolling is instant, then scroll immediately to the destination
+ if (scrollMode == ScrollMode::Instant) {
+ CancelAnimation();
+
+ ParentLayerPoint startPoint, endPoint;
+
+ {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+
+ // CallDispatchScroll interprets the start and end points as the start and
+ // end of a touch scroll so they need to be reversed.
+ startPoint = destination * Metrics().GetZoom();
+ endPoint = Metrics().GetVisualScrollOffset() * Metrics().GetZoom();
+ }
+
+ ParentLayerPoint delta = endPoint - startPoint;
+
+ ScreenPoint distance = ToScreenCoordinates(
+ ParentLayerPoint(fabs(delta.x), fabs(delta.y)), startPoint);
+
+ OverscrollHandoffState handoffState(
+ *mInputQueue->GetCurrentKeyboardBlock()->GetOverscrollHandoffChain(),
+ distance, ScrollSource::Keyboard);
+
+ CallDispatchScroll(startPoint, endPoint, handoffState);
+ ParentLayerPoint remainingDelta = endPoint - startPoint;
+ if (remainingDelta != delta) {
+ // If any scrolling happened, set KEYBOARD_SCROLL explicitly so that it
+ // will trigger a TransformEnd notification.
+ SetState(KEYBOARD_SCROLL);
+ }
+
+ if (snapTarget) {
+ {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ mLastSnapTargetIds = std::move(snapTarget->mTargetIds);
+ }
+ }
+ SetState(NOTHING);
+
+ return nsEventStatus_eConsumeDoDefault;
+ }
+
+ // The lock must be held across the entire update operation, so the
+ // compositor doesn't end the animation before we get a chance to
+ // update it.
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+
+ if (snapTarget) {
+ // If we're scroll snapping, use a smooth scroll animation to get
+ // the desired physics. Note that SmoothMsdScrollTo() will re-use an
+ // existing smooth scroll animation if there is one.
+ APZC_LOG("%p keyboard scrolling to snap point %s\n", this,
+ ToString(destination).c_str());
+ SmoothMsdScrollTo(std::move(*snapTarget), ScrollTriggeredByScript::No);
+ return nsEventStatus_eConsumeDoDefault;
+ }
+
+ // Use a keyboard scroll animation to scroll, reusing an existing one if it
+ // exists
+ if (mState != KEYBOARD_SCROLL) {
+ CancelAnimation();
+ SetState(KEYBOARD_SCROLL);
+
+ nsPoint initialPosition =
+ CSSPoint::ToAppUnits(Metrics().GetVisualScrollOffset());
+ StartAnimation(
+ new SmoothScrollAnimation(*this, initialPosition, scrollOrigin));
+ }
+
+ // Convert velocity from ParentLayerPoints/ms to ParentLayerPoints/s and then
+ // to appunits/second.
+ nsPoint velocity;
+ if (Metrics().GetZoom() != CSSToParentLayerScale(0)) {
+ velocity =
+ CSSPoint::ToAppUnits(ParentLayerPoint(mX.GetVelocity() * 1000.0f,
+ mY.GetVelocity() * 1000.0f) /
+ Metrics().GetZoom());
+ }
+
+ SmoothScrollAnimation* animation = mAnimation->AsSmoothScrollAnimation();
+ MOZ_ASSERT(animation);
+
+ animation->UpdateDestination(aEvent.mTimeStamp,
+ CSSPixel::ToAppUnits(destination),
+ nsSize(velocity.x, velocity.y));
+
+ return nsEventStatus_eConsumeDoDefault;
+}
+
+CSSPoint AsyncPanZoomController::GetKeyboardDestination(
+ const KeyboardScrollAction& aAction) const {
+ CSSSize lineScrollSize;
+ CSSSize pageScrollSize;
+ CSSPoint scrollOffset;
+ CSSRect scrollRect;
+ ParentLayerRect compositionBounds;
+
+ {
+ // Grab the lock to access the frame metrics.
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+
+ lineScrollSize = mScrollMetadata.GetLineScrollAmount() /
+ Metrics().GetDevPixelsPerCSSPixel();
+ pageScrollSize = mScrollMetadata.GetPageScrollAmount() /
+ Metrics().GetDevPixelsPerCSSPixel();
+
+ scrollOffset = GetCurrentAnimationDestination(lock).valueOr(
+ Metrics().GetVisualScrollOffset());
+
+ scrollRect = Metrics().GetScrollableRect();
+ compositionBounds = Metrics().GetCompositionBounds();
+ }
+
+ // Calculate the scroll destination based off of the scroll type and direction
+ CSSPoint scrollDestination = scrollOffset;
+
+ switch (aAction.mType) {
+ case KeyboardScrollAction::eScrollCharacter: {
+ int32_t scrollDistance =
+ StaticPrefs::toolkit_scrollbox_horizontalScrollDistance();
+
+ if (aAction.mForward) {
+ scrollDestination.x += scrollDistance * lineScrollSize.width;
+ } else {
+ scrollDestination.x -= scrollDistance * lineScrollSize.width;
+ }
+ break;
+ }
+ case KeyboardScrollAction::eScrollLine: {
+ int32_t scrollDistance =
+ StaticPrefs::toolkit_scrollbox_verticalScrollDistance();
+ if (scrollDistance * lineScrollSize.height <=
+ compositionBounds.Height()) {
+ if (aAction.mForward) {
+ scrollDestination.y += scrollDistance * lineScrollSize.height;
+ } else {
+ scrollDestination.y -= scrollDistance * lineScrollSize.height;
+ }
+ break;
+ }
+ [[fallthrough]];
+ }
+ case KeyboardScrollAction::eScrollPage: {
+ if (aAction.mForward) {
+ scrollDestination.y += pageScrollSize.height;
+ } else {
+ scrollDestination.y -= pageScrollSize.height;
+ }
+ break;
+ }
+ case KeyboardScrollAction::eScrollComplete: {
+ if (aAction.mForward) {
+ scrollDestination.y = scrollRect.YMost();
+ } else {
+ scrollDestination.y = scrollRect.Y();
+ }
+ break;
+ }
+ }
+
+ return scrollDestination;
+}
+
+ScrollSnapFlags AsyncPanZoomController::GetScrollSnapFlagsForKeyboardAction(
+ const KeyboardScrollAction& aAction) const {
+ switch (aAction.mType) {
+ case KeyboardScrollAction::eScrollCharacter:
+ case KeyboardScrollAction::eScrollLine:
+ return ScrollSnapFlags::IntendedDirection;
+ case KeyboardScrollAction::eScrollPage:
+ return ScrollSnapFlags::IntendedDirection |
+ ScrollSnapFlags::IntendedEndPosition;
+ case KeyboardScrollAction::eScrollComplete:
+ return ScrollSnapFlags::IntendedEndPosition;
+ }
+ return ScrollSnapFlags::Disabled;
+}
+
+ParentLayerPoint AsyncPanZoomController::GetDeltaForEvent(
+ const InputData& aEvent) const {
+ ParentLayerPoint delta;
+ if (aEvent.mInputType == SCROLLWHEEL_INPUT) {
+ delta = GetScrollWheelDelta(aEvent.AsScrollWheelInput());
+ } else if (aEvent.mInputType == PANGESTURE_INPUT) {
+ const PanGestureInput& panInput = aEvent.AsPanGestureInput();
+ delta = ToParentLayerCoordinates(panInput.UserMultipliedPanDisplacement(),
+ panInput.mPanStartPoint);
+ }
+ return delta;
+}
+
+CSSRect AsyncPanZoomController::GetCurrentScrollRangeInCssPixels() const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ return Metrics().CalculateScrollRange();
+}
+
+// Return whether or not the underlying layer can be scrolled on either axis.
+bool AsyncPanZoomController::CanScroll(const InputData& aEvent) const {
+ ParentLayerPoint delta = GetDeltaForEvent(aEvent);
+ if (!delta.x && !delta.y) {
+ return false;
+ }
+
+ if (SCROLLWHEEL_INPUT == aEvent.mInputType) {
+ const ScrollWheelInput& scrollWheelInput = aEvent.AsScrollWheelInput();
+ // If it's a wheel scroll, we first check if it is an auto-dir scroll.
+ // 1. For an auto-dir scroll, check if it's delta should be adjusted, if it
+ // is, then we can conclude it must be scrollable; otherwise, fall back
+ // to checking if it is scrollable without adjusting its delta.
+ // 2. For a non-auto-dir scroll, simply check if it is scrollable without
+ // adjusting its delta.
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ if (scrollWheelInput.IsAutoDir(mScrollMetadata.ForceMousewheelAutodir())) {
+ auto deltaX = scrollWheelInput.mDeltaX;
+ auto deltaY = scrollWheelInput.mDeltaY;
+ bool isRTL =
+ IsContentOfHonouredTargetRightToLeft(scrollWheelInput.HonoursRoot(
+ mScrollMetadata.ForceMousewheelAutodirHonourRoot()));
+ APZAutoDirWheelDeltaAdjuster adjuster(deltaX, deltaY, mX, mY, isRTL);
+ if (adjuster.ShouldBeAdjusted()) {
+ // If we detect that the delta values should be adjusted for an auto-dir
+ // wheel scroll, then it is impossible to be an unscrollable scroll.
+ return true;
+ }
+ }
+ return CanScrollWithWheel(delta);
+ }
+ return CanScroll(delta);
+}
+
+ScrollDirections AsyncPanZoomController::GetAllowedHandoffDirections() const {
+ ScrollDirections result;
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+
+ // In Fission there can be non-scrollable APZCs. It's unclear whether
+ // overscroll-behavior should be respected for these
+ // (see https://github.com/w3c/csswg-drafts/issues/6523) but
+ // we currently don't, to match existing practice.
+ const bool isScrollable = mX.CanScroll() || mY.CanScroll();
+ const bool isRoot = IsRootContent();
+ if ((!isScrollable && !isRoot) || mX.OverscrollBehaviorAllowsHandoff()) {
+ result += ScrollDirection::eHorizontal;
+ }
+ if ((!isScrollable && !isRoot) || mY.OverscrollBehaviorAllowsHandoff()) {
+ result += ScrollDirection::eVertical;
+ }
+ return result;
+}
+
+bool AsyncPanZoomController::CanScroll(const ParentLayerPoint& aDelta) const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ return mX.CanScroll(ParentLayerCoord(aDelta.x)) ||
+ mY.CanScroll(ParentLayerCoord(aDelta.y));
+}
+
+bool AsyncPanZoomController::CanScrollWithWheel(
+ const ParentLayerPoint& aDelta) const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+
+ // For more details about the concept of a disregarded direction, refer to the
+ // code in struct ScrollMetadata which defines mDisregardedDirection.
+ Maybe<ScrollDirection> disregardedDirection =
+ mScrollMetadata.GetDisregardedDirection();
+ if (mX.CanScroll(ParentLayerCoord(aDelta.x)) &&
+ disregardedDirection != Some(ScrollDirection::eHorizontal)) {
+ return true;
+ }
+ if (mY.CanScroll(ParentLayerCoord(aDelta.y)) &&
+ disregardedDirection != Some(ScrollDirection::eVertical)) {
+ return true;
+ }
+ return false;
+}
+
+bool AsyncPanZoomController::CanScroll(ScrollDirection aDirection) const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ switch (aDirection) {
+ case ScrollDirection::eHorizontal:
+ return mX.CanScroll();
+ case ScrollDirection::eVertical:
+ return mY.CanScroll();
+ }
+ MOZ_ASSERT_UNREACHABLE("Invalid value");
+ return false;
+}
+
+bool AsyncPanZoomController::CanVerticalScrollWithDynamicToolbar() const {
+ MOZ_ASSERT(IsRootContent());
+
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ return mY.CanVerticalScrollWithDynamicToolbar();
+}
+
+bool AsyncPanZoomController::CanScrollDownwards() const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ return mY.CanScrollTo(eSideBottom);
+}
+
+SideBits AsyncPanZoomController::ScrollableDirections() const {
+ SideBits result;
+ { // scope lock to respect lock ordering with APZCTreeManager::mTreeLock
+ // which will be acquired in the `GetCompositorFixedLayerMargins` below.
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ result = mX.ScrollableDirections() | mY.ScrollableDirections();
+ }
+
+ if (IsRootContent()) {
+ if (APZCTreeManager* treeManagerLocal = GetApzcTreeManager()) {
+ ScreenMargin fixedLayerMargins =
+ treeManagerLocal->GetCompositorFixedLayerMargins();
+ {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ result |= mY.ScrollableDirectionsWithDynamicToolbar(fixedLayerMargins);
+ }
+ }
+ }
+
+ return result;
+}
+
+bool AsyncPanZoomController::IsContentOfHonouredTargetRightToLeft(
+ bool aHonoursRoot) const {
+ if (aHonoursRoot) {
+ return mScrollMetadata.IsAutoDirRootContentRTL();
+ }
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ return Metrics().IsHorizontalContentRightToLeft();
+}
+
+bool AsyncPanZoomController::AllowScrollHandoffInCurrentBlock() const {
+ bool result = mInputQueue->AllowScrollHandoff();
+ if (!StaticPrefs::apz_allow_immediate_handoff()) {
+ if (InputBlockState* currentBlock = GetCurrentInputBlock()) {
+ // Do not allow handoff beyond the first APZC to scroll.
+ if (currentBlock->GetScrolledApzc() == this) {
+ result = false;
+ APZC_LOG("%p dropping handoff; AllowImmediateHandoff=false\n", this);
+ }
+ }
+ }
+ return result;
+}
+
+void AsyncPanZoomController::DoDelayedRequestContentRepaint() {
+ if (!IsDestroyed() && mPinchPaintTimerSet) {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ RequestContentRepaint();
+ }
+ mPinchPaintTimerSet = false;
+}
+
+void AsyncPanZoomController::DoDelayedTransformEndNotification(
+ PanZoomState aOldState) {
+ if (!IsDestroyed() && IsDelayedTransformEndSet()) {
+ DispatchStateChangeNotification(aOldState, NOTHING);
+ }
+ SetDelayedTransformEnd(false);
+}
+
+static void AdjustDeltaForAllowedScrollDirections(
+ ParentLayerPoint& aDelta,
+ const ScrollDirections& aAllowedScrollDirections) {
+ if (!aAllowedScrollDirections.contains(ScrollDirection::eHorizontal)) {
+ aDelta.x = 0;
+ }
+ if (!aAllowedScrollDirections.contains(ScrollDirection::eVertical)) {
+ aDelta.y = 0;
+ }
+}
+
+nsEventStatus AsyncPanZoomController::OnScrollWheel(
+ const ScrollWheelInput& aEvent) {
+ // Get the scroll wheel's delta values in parent-layer pixels. But before
+ // getting the values, we need to check if it is an auto-dir scroll and if it
+ // should be adjusted, if both answers are yes, let's adjust X and Y values
+ // first, and then get the delta values in parent-layer pixels based on the
+ // adjusted values.
+ bool adjustedByAutoDir = false;
+ auto deltaX = aEvent.mDeltaX;
+ auto deltaY = aEvent.mDeltaY;
+ ParentLayerPoint delta;
+ {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ if (aEvent.IsAutoDir(mScrollMetadata.ForceMousewheelAutodir())) {
+ // It's an auto-dir scroll, so check if its delta should be adjusted, if
+ // so, adjust it.
+ bool isRTL = IsContentOfHonouredTargetRightToLeft(aEvent.HonoursRoot(
+ mScrollMetadata.ForceMousewheelAutodirHonourRoot()));
+ APZAutoDirWheelDeltaAdjuster adjuster(deltaX, deltaY, mX, mY, isRTL);
+ if (adjuster.ShouldBeAdjusted()) {
+ adjuster.Adjust();
+ adjustedByAutoDir = true;
+ }
+ }
+ }
+ // Ensure the calls to GetScrollWheelDelta are outside the mRecursiveMutex
+ // lock since these calls may acquire the APZ tree lock. Holding
+ // mRecursiveMutex while acquiring the APZ tree lock is lock ordering
+ // violation.
+ if (adjustedByAutoDir) {
+ // If the original delta values have been adjusted, we pass them to
+ // replace the original delta values in |aEvent| so that the delta values
+ // in parent-layer pixels are caculated based on the adjusted values, not
+ // the original ones.
+ // Pay special attention to the last two parameters. They are in a swaped
+ // order so that they still correspond to their delta after adjustment.
+ delta = GetScrollWheelDelta(aEvent, deltaX, deltaY,
+ aEvent.mUserDeltaMultiplierY,
+ aEvent.mUserDeltaMultiplierX);
+ } else {
+ // If the original delta values haven't been adjusted by auto-dir, just pass
+ // the |aEvent| and caculate the delta values in parent-layer pixels based
+ // on the original delta values from |aEvent|.
+ delta = GetScrollWheelDelta(aEvent);
+ }
+
+ APZC_LOG("%p got a scroll-wheel with delta in parent-layer pixels: %s\n",
+ this, ToString(delta).c_str());
+
+ if (adjustedByAutoDir) {
+ MOZ_ASSERT(delta.x || delta.y,
+ "Adjusted auto-dir delta values can never be all-zero.");
+ APZC_LOG("%p got a scroll-wheel with adjusted auto-dir delta values\n",
+ this);
+ } else if ((delta.x || delta.y) && !CanScrollWithWheel(delta)) {
+ // We can't scroll this apz anymore, so we simply drop the event.
+ if (mInputQueue->GetActiveWheelTransaction() &&
+ StaticPrefs::test_mousescroll()) {
+ if (RefPtr<GeckoContentController> controller =
+ GetGeckoContentController()) {
+ controller->NotifyMozMouseScrollEvent(GetScrollId(),
+ u"MozMouseScrollFailed"_ns);
+ }
+ }
+ return nsEventStatus_eConsumeNoDefault;
+ }
+
+ MOZ_ASSERT(mInputQueue->GetCurrentWheelBlock());
+ AdjustDeltaForAllowedScrollDirections(
+ delta, mInputQueue->GetCurrentWheelBlock()->GetAllowedScrollDirections());
+
+ if (delta.x == 0 && delta.y == 0) {
+ // Avoid spurious state changes and unnecessary work
+ return nsEventStatus_eIgnore;
+ }
+
+ switch (aEvent.mScrollMode) {
+ case ScrollWheelInput::SCROLLMODE_INSTANT: {
+ // Wheel events from "clicky" mouse wheels trigger scroll snapping to the
+ // next snap point. Check for this, and adjust the delta to take into
+ // account the snap point.
+ CSSPoint startPosition;
+ {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ startPosition = Metrics().GetVisualScrollOffset();
+ }
+ Maybe<CSSSnapTarget> snapTarget =
+ MaybeAdjustDeltaForScrollSnappingOnWheelInput(aEvent, delta,
+ startPosition);
+
+ ScreenPoint distance = ToScreenCoordinates(
+ ParentLayerPoint(fabs(delta.x), fabs(delta.y)), aEvent.mLocalOrigin);
+
+ CancelAnimation();
+
+ OverscrollHandoffState handoffState(
+ *mInputQueue->GetCurrentWheelBlock()->GetOverscrollHandoffChain(),
+ distance, ScrollSource::Wheel);
+ ParentLayerPoint startPoint = aEvent.mLocalOrigin;
+ ParentLayerPoint endPoint = aEvent.mLocalOrigin - delta;
+ RecordScrollPayload(aEvent.mTimeStamp);
+
+ CallDispatchScroll(startPoint, endPoint, handoffState);
+ ParentLayerPoint remainingDelta = endPoint - startPoint;
+ if (remainingDelta != delta) {
+ // If any scrolling happened, set WHEEL_SCROLL explicitly so that it
+ // will trigger a TransformEnd notification.
+ SetState(WHEEL_SCROLL);
+ }
+
+ if (snapTarget) {
+ {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ mLastSnapTargetIds = std::move(snapTarget->mTargetIds);
+ }
+ }
+ SetState(NOTHING);
+
+ // The calls above handle their own locking; moreover,
+ // ToScreenCoordinates() and CallDispatchScroll() can grab the tree lock.
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ RequestContentRepaint();
+
+ break;
+ }
+
+ case ScrollWheelInput::SCROLLMODE_SMOOTH: {
+ // The lock must be held across the entire update operation, so the
+ // compositor doesn't end the animation before we get a chance to
+ // update it.
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+
+ RecordScrollPayload(aEvent.mTimeStamp);
+ // Perform scroll snapping if appropriate.
+ // If we're already in a wheel scroll or smooth scroll animation,
+ // the delta is applied to its destination, not to the current
+ // scroll position. Take this into account when finding a snap point.
+ CSSPoint startPosition = GetCurrentAnimationDestination(lock).valueOr(
+ Metrics().GetVisualScrollOffset());
+
+ if (Maybe<CSSSnapTarget> snapTarget =
+ MaybeAdjustDeltaForScrollSnappingOnWheelInput(aEvent, delta,
+ startPosition)) {
+ // If we're scroll snapping, use a smooth scroll animation to get
+ // the desired physics. Note that SmoothMsdScrollTo() will re-use an
+ // existing smooth scroll animation if there is one.
+ APZC_LOG("%p wheel scrolling to snap point %s\n", this,
+ ToString(startPosition).c_str());
+ SmoothMsdScrollTo(std::move(*snapTarget), ScrollTriggeredByScript::No);
+ break;
+ }
+
+ // Otherwise, use a wheel scroll animation, also reusing one if possible.
+ if (mState != WHEEL_SCROLL) {
+ CancelAnimation();
+ SetState(WHEEL_SCROLL);
+
+ nsPoint initialPosition =
+ CSSPoint::ToAppUnits(Metrics().GetVisualScrollOffset());
+ StartAnimation(new WheelScrollAnimation(*this, initialPosition,
+ aEvent.mDeltaType));
+ }
+ // Convert velocity from ParentLayerPoints/ms to ParentLayerPoints/s and
+ // then to appunits/second.
+
+ nsPoint deltaInAppUnits;
+ nsPoint velocity;
+ if (Metrics().GetZoom() != CSSToParentLayerScale(0)) {
+ deltaInAppUnits = CSSPoint::ToAppUnits(delta / Metrics().GetZoom());
+ velocity =
+ CSSPoint::ToAppUnits(ParentLayerPoint(mX.GetVelocity() * 1000.0f,
+ mY.GetVelocity() * 1000.0f) /
+ Metrics().GetZoom());
+ }
+
+ WheelScrollAnimation* animation = mAnimation->AsWheelScrollAnimation();
+ animation->UpdateDelta(aEvent.mTimeStamp, deltaInAppUnits,
+ nsSize(velocity.x, velocity.y));
+ break;
+ }
+ }
+
+ return nsEventStatus_eConsumeNoDefault;
+}
+
+void AsyncPanZoomController::NotifyMozMouseScrollEvent(
+ const nsString& aString) const {
+ RefPtr<GeckoContentController> controller = GetGeckoContentController();
+ if (!controller) {
+ return;
+ }
+ controller->NotifyMozMouseScrollEvent(GetScrollId(), aString);
+}
+
+nsEventStatus AsyncPanZoomController::OnPanMayBegin(
+ const PanGestureInput& aEvent) {
+ APZC_LOG_DETAIL("got a pan-maybegin in state %s\n", this,
+ ToString(mState).c_str());
+
+ StartTouch(aEvent.mLocalPanStartPoint, aEvent.mTimeStamp);
+ MOZ_ASSERT(GetCurrentPanGestureBlock());
+ GetCurrentPanGestureBlock()->GetOverscrollHandoffChain()->CancelAnimations();
+
+ return nsEventStatus_eConsumeNoDefault;
+}
+
+nsEventStatus AsyncPanZoomController::OnPanCancelled(
+ const PanGestureInput& aEvent) {
+ APZC_LOG_DETAIL("got a pan-cancelled in state %s\n", this,
+ ToString(mState).c_str());
+
+ mX.CancelGesture();
+ mY.CancelGesture();
+
+ return nsEventStatus_eConsumeNoDefault;
+}
+
+nsEventStatus AsyncPanZoomController::OnPanBegin(
+ const PanGestureInput& aEvent) {
+ APZC_LOG_DETAIL("got a pan-begin in state %s\n", this,
+ ToString(mState).c_str());
+
+ if (mState == SMOOTHMSD_SCROLL) {
+ // SMOOTHMSD_SCROLL scrolls are cancelled by pan gestures.
+ CancelAnimation();
+ }
+
+ StartTouch(aEvent.mLocalPanStartPoint, aEvent.mTimeStamp);
+
+ if (!UsingStatefulAxisLock()) {
+ SetState(PANNING);
+ } else {
+ float dx = aEvent.mPanDisplacement.x, dy = aEvent.mPanDisplacement.y;
+
+ if (dx != 0.0f || dy != 0.0f) {
+ double angle = atan2(dy, dx); // range [-pi, pi]
+ angle = fabs(angle); // range [0, pi]
+ HandlePanning(angle);
+ } else {
+ SetState(PANNING);
+ }
+ }
+
+ // Call into OnPan in order to process any delta included in this event.
+ OnPan(aEvent, FingersOnTouchpad::Yes);
+
+ return nsEventStatus_eConsumeNoDefault;
+}
+
+std::tuple<ParentLayerPoint, ScreenPoint>
+AsyncPanZoomController::GetDisplacementsForPanGesture(
+ const PanGestureInput& aEvent) {
+ // Note that there is a multiplier that applies onto the "physical" pan
+ // displacement (how much the user's fingers moved) that produces the
+ // "logical" pan displacement (how much the page should move). For some of the
+ // code below it makes more sense to use the physical displacement rather than
+ // the logical displacement, and vice-versa.
+ ScreenPoint physicalPanDisplacement = aEvent.mPanDisplacement;
+ ParentLayerPoint logicalPanDisplacement =
+ aEvent.UserMultipliedLocalPanDisplacement();
+ if (aEvent.mDeltaType == PanGestureInput::PANDELTA_PAGE) {
+ // Pan events with page units are used by Gtk, so this replicates Gtk:
+ // https://gitlab.gnome.org/GNOME/gtk/blob/c734c7e9188b56f56c3a504abee05fa40c5475ac/gtk/gtkrange.c#L3065-3073
+ CSSSize pageScrollSize;
+ CSSToParentLayerScale zoom;
+ {
+ // Grab the lock to access the frame metrics.
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ pageScrollSize = mScrollMetadata.GetPageScrollAmount() /
+ Metrics().GetDevPixelsPerCSSPixel();
+ zoom = Metrics().GetZoom();
+ }
+ // scrollUnit* is in units of "ParentLayer pixels per page proportion"...
+ auto scrollUnitWidth = std::min(std::pow(pageScrollSize.width, 2.0 / 3.0),
+ pageScrollSize.width / 2.0) *
+ zoom.scale;
+ auto scrollUnitHeight = std::min(std::pow(pageScrollSize.height, 2.0 / 3.0),
+ pageScrollSize.height / 2.0) *
+ zoom.scale;
+ // ... and pan displacements are in units of "page proportion count"
+ // here, so the products of them and scrollUnit* are in ParentLayer pixels
+ ParentLayerPoint physicalPanDisplacementPL(
+ physicalPanDisplacement.x * scrollUnitWidth,
+ physicalPanDisplacement.y * scrollUnitHeight);
+ physicalPanDisplacement = ToScreenCoordinates(physicalPanDisplacementPL,
+ aEvent.mLocalPanStartPoint);
+ logicalPanDisplacement.x *= scrollUnitWidth;
+ logicalPanDisplacement.y *= scrollUnitHeight;
+
+ // Accelerate (decelerate) any pans by raising it to a user configurable
+ // power (apz.touch_acceleration_factor_x, apz.touch_acceleration_factor_y)
+ //
+ // Confine input for pow() to greater than or equal to 0 to avoid domain
+ // errors with non-integer exponents
+ if (mX.GetVelocity() != 0) {
+ float absVelocity = std::abs(mX.GetVelocity());
+ logicalPanDisplacement.x *=
+ std::pow(absVelocity,
+ StaticPrefs::apz_touch_acceleration_factor_x()) /
+ absVelocity;
+ }
+
+ if (mY.GetVelocity() != 0) {
+ float absVelocity = std::abs(mY.GetVelocity());
+ logicalPanDisplacement.y *=
+ std::pow(absVelocity,
+ StaticPrefs::apz_touch_acceleration_factor_y()) /
+ absVelocity;
+ }
+ }
+
+ MOZ_ASSERT(GetCurrentPanGestureBlock());
+ AdjustDeltaForAllowedScrollDirections(
+ logicalPanDisplacement,
+ GetCurrentPanGestureBlock()->GetAllowedScrollDirections());
+
+ if (GetAxisLockMode() == DOMINANT_AXIS) {
+ // Given a pan gesture and both directions have a delta, implement
+ // dominant axis scrolling and only use the delta for the larger
+ // axis.
+ if (logicalPanDisplacement.y != 0 && logicalPanDisplacement.x != 0) {
+ if (fabs(logicalPanDisplacement.y) >= fabs(logicalPanDisplacement.x)) {
+ logicalPanDisplacement.x = 0;
+ physicalPanDisplacement.x = 0;
+ } else {
+ logicalPanDisplacement.y = 0;
+ physicalPanDisplacement.y = 0;
+ }
+ }
+ }
+
+ return {logicalPanDisplacement, physicalPanDisplacement};
+}
+
+nsEventStatus AsyncPanZoomController::OnPan(
+ const PanGestureInput& aEvent, FingersOnTouchpad aFingersOnTouchpad) {
+ APZC_LOG_DETAIL("got a pan-pan in state %s\n", this,
+ ToString(GetState()).c_str());
+
+ if (GetState() == SMOOTHMSD_SCROLL) {
+ if (aFingersOnTouchpad == FingersOnTouchpad::No) {
+ // When a SMOOTHMSD_SCROLL scroll is being processed on a frame, mouse
+ // wheel and trackpad momentum scroll position updates will not cancel the
+ // SMOOTHMSD_SCROLL scroll animations, enabling scripts that depend on
+ // them to be responsive without forcing the user to wait for the momentum
+ // scrolling to completely stop.
+ return nsEventStatus_eConsumeNoDefault;
+ }
+
+ // SMOOTHMSD_SCROLL scrolls are cancelled by pan gestures.
+ CancelAnimation();
+ }
+
+ if (GetState() == NOTHING) {
+ // This event block was interrupted by something else. If the user's fingers
+ // are still on on the touchpad we want to resume scrolling, otherwise we
+ // ignore the rest of the scroll gesture.
+ if (aFingersOnTouchpad == FingersOnTouchpad::No) {
+ return nsEventStatus_eConsumeNoDefault;
+ }
+ // Resume / restart the pan.
+ // PanBegin will call back into this function with mState == PANNING.
+ return OnPanBegin(aEvent);
+ }
+
+ auto [logicalPanDisplacement, physicalPanDisplacement] =
+ GetDisplacementsForPanGesture(aEvent);
+
+ {
+ // Grab the lock to protect the animation from being canceled on the updater
+ // thread.
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ MOZ_ASSERT_IF(GetState() == OVERSCROLL_ANIMATION, mAnimation);
+
+ if (GetState() == OVERSCROLL_ANIMATION && mAnimation &&
+ aFingersOnTouchpad == FingersOnTouchpad::No) {
+ // If there is an on-going overscroll animation, we tell the animation
+ // whether the displacements should be handled by the animation or not.
+ MOZ_ASSERT(mAnimation->AsOverscrollAnimation());
+ if (RefPtr<OverscrollAnimation> overscrollAnimation =
+ mAnimation->AsOverscrollAnimation()) {
+ overscrollAnimation->HandlePanMomentum(logicalPanDisplacement);
+ // And then as a result of the above call, if the animation is currently
+ // affecting on the axis, drop the displacement value on the axis so
+ // that we stop further oversrolling on the axis.
+ if (overscrollAnimation->IsManagingXAxis()) {
+ logicalPanDisplacement.x = 0;
+ physicalPanDisplacement.x = 0;
+ }
+ if (overscrollAnimation->IsManagingYAxis()) {
+ logicalPanDisplacement.y = 0;
+ physicalPanDisplacement.y = 0;
+ }
+ }
+ }
+ }
+
+ HandlePanningUpdate(physicalPanDisplacement);
+
+ MOZ_ASSERT(GetCurrentPanGestureBlock());
+ ScreenPoint panDistance(fabs(physicalPanDisplacement.x),
+ fabs(physicalPanDisplacement.y));
+ OverscrollHandoffState handoffState(
+ *GetCurrentPanGestureBlock()->GetOverscrollHandoffChain(), panDistance,
+ ScrollSource::Touchpad);
+
+ // Create fake "touch" positions that will result in the desired scroll
+ // motion. Note that the pan displacement describes the change in scroll
+ // position: positive displacement values mean that the scroll position
+ // increases. However, an increase in scroll position means that the scrolled
+ // contents are moved to the left / upwards. Since our simulated "touches"
+ // determine the motion of the scrolled contents, not of the scroll position,
+ // they need to move in the opposite direction of the pan displacement.
+ ParentLayerPoint startPoint = aEvent.mLocalPanStartPoint;
+ ParentLayerPoint endPoint =
+ aEvent.mLocalPanStartPoint - logicalPanDisplacement;
+ if (logicalPanDisplacement != ParentLayerPoint()) {
+ // Don't expect a composite to be triggered if the displacement is zero
+ RecordScrollPayload(aEvent.mTimeStamp);
+ }
+
+ const ParentLayerPoint velocity = GetVelocityVector();
+ bool consumed = CallDispatchScroll(startPoint, endPoint, handoffState);
+
+ const ParentLayerPoint visualDisplacement = ToParentLayerCoordinates(
+ handoffState.mTotalMovement, aEvent.mPanStartPoint);
+ // We need to update the axis velocity in order to get a useful display port
+ // size and position. We need to do so even if this is a momentum pan (i.e.
+ // aFingersOnTouchpad == No); in that case the "with touch" part is not
+ // really appropriate, so we may want to rethink this at some point.
+ // Note that we have to make all simulated positions relative to
+ // Axis::GetPos(), because the current position is an invented position, and
+ // because resetting the position to the mouse position (e.g.
+ // aEvent.mLocalStartPoint) would mess up velocity calculation. (This is
+ // the only caller of UpdateWithTouchAtDevicePoint() for pan events, so
+ // there is no risk of other calls resetting the position.)
+ // Also note that if there is an on-going overscroll animation in the axis,
+ // we shouldn't call UpdateWithTouchAtDevicePoint because the call changes
+ // the velocity which should be managed by the overscroll animation.
+ // Finally, note that we do this *after* CallDispatchScroll(), so that the
+ // position we use reflects the actual amount of movement that occurred
+ // (in particular, if we're in overscroll, if reflects the amount of movement
+ // *after* applying resistance). This is important because we want the axis
+ // velocity to track the visual movement speed of the page.
+ if (visualDisplacement.x != 0) {
+ mX.UpdateWithTouchAtDevicePoint(mX.GetPos() - visualDisplacement.x,
+ aEvent.mTimeStamp);
+ }
+ if (visualDisplacement.y != 0) {
+ mY.UpdateWithTouchAtDevicePoint(mY.GetPos() - visualDisplacement.y,
+ aEvent.mTimeStamp);
+ }
+
+ if (aFingersOnTouchpad == FingersOnTouchpad::No) {
+ if (IsOverscrolled() && GetState() != OVERSCROLL_ANIMATION) {
+ StartOverscrollAnimation(velocity, GetOverscrollSideBits());
+ } else if (!consumed) {
+ // If there is unconsumed scroll and we're in the momentum part of the
+ // pan gesture, terminate the momentum scroll. This prevents momentum
+ // scroll events from unexpectedly causing scrolling later if somehow
+ // the APZC becomes scrollable again in this direction (e.g. if the user
+ // uses some other input method to scroll in the opposite direction).
+ SetState(NOTHING);
+ }
+ }
+
+ return nsEventStatus_eConsumeNoDefault;
+}
+
+nsEventStatus AsyncPanZoomController::OnPanEnd(const PanGestureInput& aEvent) {
+ APZC_LOG_DETAIL("got a pan-end in state %s\n", this,
+ ToString(mState).c_str());
+
+ // This can happen if the OS sends a second pan-end event after the first one
+ // has already started an overscroll animation or entered a fling state.
+ // This has been observed on some Wayland versions.
+ PanZoomState currentState = GetState();
+ if (currentState == OVERSCROLL_ANIMATION || currentState == NOTHING ||
+ currentState == FLING) {
+ return nsEventStatus_eIgnore;
+ }
+
+ if (aEvent.mPanDisplacement != ScreenPoint{}) {
+ // Call into OnPan in order to process the delta included in this event.
+ OnPan(aEvent, FingersOnTouchpad::Yes);
+ }
+
+ // Do not unlock the axis lock at the end of a pan gesture. The axis lock
+ // should extend into the momentum scroll.
+ EndTouch(aEvent.mTimeStamp, Axis::ClearAxisLock::No);
+
+ // Use HandleEndOfPan for fling on platforms that don't
+ // emit momentum events (Gtk).
+ if (aEvent.mSimulateMomentum) {
+ return HandleEndOfPan();
+ }
+
+ MOZ_ASSERT(GetCurrentPanGestureBlock());
+ RefPtr<const OverscrollHandoffChain> overscrollHandoffChain =
+ GetCurrentPanGestureBlock()->GetOverscrollHandoffChain();
+
+ // Call SnapBackOverscrolledApzcForMomentum regardless whether this APZC is
+ // overscrolled or not since overscroll animations for ancestor APZCs in this
+ // overscroll handoff chain might have been cancelled by the current pan
+ // gesture block.
+ overscrollHandoffChain->SnapBackOverscrolledApzcForMomentum(
+ this, GetVelocityVector());
+ // If this APZC is overscrolled, the above SnapBackOverscrolledApzcForMomentum
+ // triggers an overscroll animation. When we're finished with the overscroll
+ // animation, the state will be reset and a TransformEnd will be sent to the
+ // main thread.
+ currentState = GetState();
+ if (currentState != OVERSCROLL_ANIMATION) {
+ // Do not send a state change notification to the content controller here.
+ // Instead queue a delayed task to dispatch the notification if no
+ // momentum pan or scroll snap follows the pan-end.
+ RefPtr<GeckoContentController> controller = GetGeckoContentController();
+ if (controller) {
+ SetDelayedTransformEnd(true);
+ controller->PostDelayedTask(
+ NewRunnableMethod<PanZoomState>(
+ "layers::AsyncPanZoomController::"
+ "DoDelayedTransformEndNotification",
+ this, &AsyncPanZoomController::DoDelayedTransformEndNotification,
+ currentState),
+ StaticPrefs::apz_scrollend_event_content_delay_ms());
+ SetStateNoContentControllerDispatch(NOTHING);
+ } else {
+ SetState(NOTHING);
+ }
+ }
+
+ // Drop any velocity on axes where we don't have room to scroll anyways
+ // (in this APZC, or an APZC further in the handoff chain).
+ // This ensures that we don't enlarge the display port unnecessarily.
+ {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ if (!overscrollHandoffChain->CanScrollInDirection(
+ this, ScrollDirection::eHorizontal)) {
+ mX.SetVelocity(0);
+ }
+ if (!overscrollHandoffChain->CanScrollInDirection(
+ this, ScrollDirection::eVertical)) {
+ mY.SetVelocity(0);
+ }
+ }
+
+ RequestContentRepaint();
+ ScrollSnapToDestination();
+
+ return nsEventStatus_eConsumeNoDefault;
+}
+
+nsEventStatus AsyncPanZoomController::OnPanMomentumStart(
+ const PanGestureInput& aEvent) {
+ APZC_LOG_DETAIL("got a pan-momentumstart in state %s\n", this,
+ ToString(mState).c_str());
+
+ if (mState == SMOOTHMSD_SCROLL || mState == OVERSCROLL_ANIMATION) {
+ return nsEventStatus_eConsumeNoDefault;
+ }
+
+ if (IsDelayedTransformEndSet()) {
+ // Do not send another TransformBegin notification if we have not
+ // delivered a corresponding TransformEnd. Also ensure that any
+ // queued transform-end due to a pan-end is not sent. Instead rely
+ // on the transform-end sent due to the momentum pan.
+ SetDelayedTransformEnd(false);
+ SetStateNoContentControllerDispatch(PAN_MOMENTUM);
+ } else {
+ SetState(PAN_MOMENTUM);
+ }
+
+ // Call into OnPan in order to process any delta included in this event.
+ OnPan(aEvent, FingersOnTouchpad::No);
+
+ return nsEventStatus_eConsumeNoDefault;
+}
+
+nsEventStatus AsyncPanZoomController::OnPanMomentumEnd(
+ const PanGestureInput& aEvent) {
+ APZC_LOG_DETAIL("got a pan-momentumend in state %s\n", this,
+ ToString(mState).c_str());
+
+ if (mState == OVERSCROLL_ANIMATION) {
+ return nsEventStatus_eConsumeNoDefault;
+ }
+
+ // Call into OnPan in order to process any delta included in this event.
+ OnPan(aEvent, FingersOnTouchpad::No);
+
+ // We need to reset the velocity to zero. We don't really have a "touch"
+ // here because the touch has already ended long before the momentum
+ // animation started, but I guess it doesn't really matter for now.
+ mX.CancelGesture();
+ mY.CancelGesture();
+ SetState(NOTHING);
+
+ RequestContentRepaint();
+
+ return nsEventStatus_eConsumeNoDefault;
+}
+
+nsEventStatus AsyncPanZoomController::OnPanInterrupted(
+ const PanGestureInput& aEvent) {
+ APZC_LOG_DETAIL("got a pan-interrupted in state %s\n", this,
+ ToString(mState).c_str());
+
+ CancelAnimation();
+
+ return nsEventStatus_eIgnore;
+}
+
+nsEventStatus AsyncPanZoomController::OnLongPress(
+ const TapGestureInput& aEvent) {
+ APZC_LOG_DETAIL("got a long-press in state %s\n", this,
+ ToString(mState).c_str());
+ RefPtr<GeckoContentController> controller = GetGeckoContentController();
+ if (controller) {
+ if (Maybe<LayoutDevicePoint> geckoScreenPoint =
+ ConvertToGecko(aEvent.mPoint)) {
+ TouchBlockState* touch = GetCurrentTouchBlock();
+ if (!touch) {
+ APZC_LOG(
+ "%p dropping long-press because some non-touch block interrupted "
+ "it\n",
+ this);
+ return nsEventStatus_eIgnore;
+ }
+ if (touch->IsDuringFastFling()) {
+ APZC_LOG("%p dropping long-press because of fast fling\n", this);
+ return nsEventStatus_eIgnore;
+ }
+ uint64_t blockId = GetInputQueue()->InjectNewTouchBlock(this);
+ controller->HandleTap(TapType::eLongTap, *geckoScreenPoint,
+ aEvent.modifiers, GetGuid(), blockId);
+ return nsEventStatus_eConsumeNoDefault;
+ }
+ }
+ return nsEventStatus_eIgnore;
+}
+
+nsEventStatus AsyncPanZoomController::OnLongPressUp(
+ const TapGestureInput& aEvent) {
+ APZC_LOG_DETAIL("got a long-tap-up in state %s\n", this,
+ ToString(mState).c_str());
+ return GenerateSingleTap(TapType::eLongTapUp, aEvent.mPoint,
+ aEvent.modifiers);
+}
+
+nsEventStatus AsyncPanZoomController::GenerateSingleTap(
+ TapType aType, const ScreenIntPoint& aPoint,
+ mozilla::Modifiers aModifiers) {
+ RefPtr<GeckoContentController> controller = GetGeckoContentController();
+ if (controller) {
+ if (Maybe<LayoutDevicePoint> geckoScreenPoint = ConvertToGecko(aPoint)) {
+ TouchBlockState* touch = GetCurrentTouchBlock();
+ // |touch| may be null in the case where this function is
+ // invoked by GestureEventListener on a timeout. In that case we already
+ // verified that the single tap is allowed so we let it through.
+ // XXX there is a bug here that in such a case the touch block that
+ // generated this tap will not get its mSingleTapOccurred flag set.
+ // See https://bugzilla.mozilla.org/show_bug.cgi?id=1256344#c6
+ if (touch) {
+ if (touch->IsDuringFastFling()) {
+ APZC_LOG(
+ "%p dropping single-tap because it was during a fast-fling\n",
+ this);
+ return nsEventStatus_eIgnore;
+ }
+ touch->SetSingleTapOccurred();
+ }
+ // Because this may be being running as part of
+ // APZCTreeManager::ReceiveInputEvent, calling controller->HandleTap
+ // directly might mean that content receives the single tap message before
+ // the corresponding touch-up. To avoid that we schedule the singletap
+ // message to run on the next spin of the event loop. See bug 965381 for
+ // the issue this was causing.
+ APZC_LOG("posting runnable for HandleTap from GenerateSingleTap");
+ RefPtr<Runnable> runnable =
+ NewRunnableMethod<TapType, LayoutDevicePoint, mozilla::Modifiers,
+ ScrollableLayerGuid, uint64_t>(
+ "layers::GeckoContentController::HandleTap", controller,
+ &GeckoContentController::HandleTap, aType, *geckoScreenPoint,
+ aModifiers, GetGuid(), touch ? touch->GetBlockId() : 0);
+
+ controller->PostDelayedTask(runnable.forget(), 0);
+ return nsEventStatus_eConsumeNoDefault;
+ }
+ }
+ return nsEventStatus_eIgnore;
+}
+
+void AsyncPanZoomController::OnTouchEndOrCancel() {
+ if (RefPtr<GeckoContentController> controller = GetGeckoContentController()) {
+ MOZ_ASSERT(GetCurrentTouchBlock());
+ controller->NotifyAPZStateChange(
+ GetGuid(), APZStateChange::eEndTouch,
+ GetCurrentTouchBlock()->SingleTapOccurred(),
+ Some(GetCurrentTouchBlock()->GetBlockId()));
+ }
+}
+
+nsEventStatus AsyncPanZoomController::OnSingleTapUp(
+ const TapGestureInput& aEvent) {
+ APZC_LOG_DETAIL("got a single-tap-up in state %s\n", this,
+ ToString(mState).c_str());
+ // If mZoomConstraints.mAllowDoubleTapZoom is true we wait for a call to
+ // OnSingleTapConfirmed before sending event to content
+ MOZ_ASSERT(GetCurrentTouchBlock());
+ if (!(ZoomConstraintsAllowDoubleTapZoom() &&
+ GetCurrentTouchBlock()->TouchActionAllowsDoubleTapZoom())) {
+ return GenerateSingleTap(TapType::eSingleTap, aEvent.mPoint,
+ aEvent.modifiers);
+ }
+ return nsEventStatus_eIgnore;
+}
+
+nsEventStatus AsyncPanZoomController::OnSingleTapConfirmed(
+ const TapGestureInput& aEvent) {
+ APZC_LOG_DETAIL("got a single-tap-confirmed in state %s\n", this,
+ ToString(mState).c_str());
+ return GenerateSingleTap(TapType::eSingleTap, aEvent.mPoint,
+ aEvent.modifiers);
+}
+
+nsEventStatus AsyncPanZoomController::OnDoubleTap(
+ const TapGestureInput& aEvent) {
+ APZC_LOG_DETAIL("got a double-tap in state %s\n", this,
+ ToString(mState).c_str());
+ RefPtr<GeckoContentController> controller = GetGeckoContentController();
+ if (controller) {
+ if (ZoomConstraintsAllowDoubleTapZoom() &&
+ (!GetCurrentTouchBlock() ||
+ GetCurrentTouchBlock()->TouchActionAllowsDoubleTapZoom())) {
+ if (Maybe<LayoutDevicePoint> geckoScreenPoint =
+ ConvertToGecko(aEvent.mPoint)) {
+ controller->HandleTap(
+ TapType::eDoubleTap, *geckoScreenPoint, aEvent.modifiers, GetGuid(),
+ GetCurrentTouchBlock() ? GetCurrentTouchBlock()->GetBlockId() : 0);
+ }
+ }
+ return nsEventStatus_eConsumeNoDefault;
+ }
+ return nsEventStatus_eIgnore;
+}
+
+nsEventStatus AsyncPanZoomController::OnSecondTap(
+ const TapGestureInput& aEvent) {
+ APZC_LOG_DETAIL("got a second-tap in state %s\n", this,
+ ToString(mState).c_str());
+ return GenerateSingleTap(TapType::eSecondTap, aEvent.mPoint,
+ aEvent.modifiers);
+}
+
+nsEventStatus AsyncPanZoomController::OnCancelTap(
+ const TapGestureInput& aEvent) {
+ APZC_LOG_DETAIL("got a cancel-tap in state %s\n", this,
+ ToString(mState).c_str());
+ // XXX: Implement this.
+ return nsEventStatus_eIgnore;
+}
+
+ScreenToParentLayerMatrix4x4 AsyncPanZoomController::GetTransformToThis()
+ const {
+ if (APZCTreeManager* treeManagerLocal = GetApzcTreeManager()) {
+ return treeManagerLocal->GetScreenToApzcTransform(this);
+ }
+ return ScreenToParentLayerMatrix4x4();
+}
+
+ScreenPoint AsyncPanZoomController::ToScreenCoordinates(
+ const ParentLayerPoint& aVector, const ParentLayerPoint& aAnchor) const {
+ return TransformVector(GetTransformToThis().Inverse(), aVector, aAnchor);
+}
+
+// TODO: figure out a good way to check the w-coordinate is positive and return
+// the result
+ParentLayerPoint AsyncPanZoomController::ToParentLayerCoordinates(
+ const ScreenPoint& aVector, const ScreenPoint& aAnchor) const {
+ return TransformVector(GetTransformToThis(), aVector, aAnchor);
+}
+
+ParentLayerPoint AsyncPanZoomController::ToParentLayerCoordinates(
+ const ScreenPoint& aVector, const ExternalPoint& aAnchor) const {
+ return ToParentLayerCoordinates(
+ aVector,
+ ViewAs<ScreenPixel>(aAnchor, PixelCastJustification::ExternalIsScreen));
+}
+
+ExternalPoint AsyncPanZoomController::ToExternalPoint(
+ const ExternalPoint& aScreenOffset, const ScreenPoint& aScreenPoint) {
+ return aScreenOffset +
+ ViewAs<ExternalPixel>(aScreenPoint,
+ PixelCastJustification::ExternalIsScreen);
+}
+
+ScreenPoint AsyncPanZoomController::PanVector(const ExternalPoint& aPos) const {
+ return ScreenPoint(fabs(aPos.x - mStartTouch.x),
+ fabs(aPos.y - mStartTouch.y));
+}
+
+bool AsyncPanZoomController::Contains(const ScreenIntPoint& aPoint) const {
+ ScreenToParentLayerMatrix4x4 transformToThis = GetTransformToThis();
+ Maybe<ParentLayerIntPoint> point = UntransformBy(transformToThis, aPoint);
+ if (!point) {
+ return false;
+ }
+
+ ParentLayerIntRect cb;
+ {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ GetFrameMetrics().GetCompositionBounds().ToIntRect(&cb);
+ }
+ return cb.Contains(*point);
+}
+
+bool AsyncPanZoomController::IsInOverscrollGutter(
+ const ScreenPoint& aHitTestPoint) const {
+ if (!IsPhysicallyOverscrolled()) {
+ return false;
+ }
+
+ Maybe<ParentLayerPoint> apzcPoint =
+ UntransformBy(GetTransformToThis(), aHitTestPoint);
+ if (!apzcPoint) return false;
+ return IsInOverscrollGutter(*apzcPoint);
+}
+
+bool AsyncPanZoomController::IsInOverscrollGutter(
+ const ParentLayerPoint& aHitTestPoint) const {
+ ParentLayerRect compositionBounds;
+ {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ compositionBounds = GetFrameMetrics().GetCompositionBounds();
+ }
+ if (!compositionBounds.Contains(aHitTestPoint)) {
+ // Point is outside of scrollable element's bounds altogether.
+ return false;
+ }
+ auto overscrollTransform = GetOverscrollTransform(eForHitTesting);
+ ParentLayerPoint overscrollUntransformed =
+ overscrollTransform.Inverse().TransformPoint(aHitTestPoint);
+
+ if (compositionBounds.Contains(overscrollUntransformed)) {
+ // Point is over scrollable content.
+ return false;
+ }
+
+ // Point is in gutter.
+ return true;
+}
+
+bool AsyncPanZoomController::IsOverscrolled() const {
+ return mOverscrollEffect->IsOverscrolled();
+}
+
+bool AsyncPanZoomController::IsPhysicallyOverscrolled() const {
+ // As an optimization, avoid calling Apply/UnapplyAsyncTestAttributes
+ // unless we're in a test environment where we need it.
+ if (StaticPrefs::apz_overscroll_test_async_scroll_offset_enabled()) {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ AutoApplyAsyncTestAttributes testAttributeApplier(this, lock);
+ return mX.IsOverscrolled() || mY.IsOverscrolled();
+ }
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ return mX.IsOverscrolled() || mY.IsOverscrolled();
+}
+
+bool AsyncPanZoomController::IsInInvalidOverscroll() const {
+ return mX.IsInInvalidOverscroll() || mY.IsInInvalidOverscroll();
+}
+
+ParentLayerPoint AsyncPanZoomController::PanStart() const {
+ return ParentLayerPoint(mX.PanStart(), mY.PanStart());
+}
+
+const ParentLayerPoint AsyncPanZoomController::GetVelocityVector() const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ return ParentLayerPoint(mX.GetVelocity(), mY.GetVelocity());
+}
+
+void AsyncPanZoomController::SetVelocityVector(
+ const ParentLayerPoint& aVelocityVector) {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ mX.SetVelocity(aVelocityVector.x);
+ mY.SetVelocity(aVelocityVector.y);
+}
+
+void AsyncPanZoomController::HandlePanningWithTouchAction(double aAngle) {
+ // Handling of cross sliding will need to be added in this method after
+ // touch-action released enabled by default.
+ MOZ_ASSERT(GetCurrentTouchBlock());
+ RefPtr<const OverscrollHandoffChain> overscrollHandoffChain =
+ GetCurrentInputBlock()->GetOverscrollHandoffChain();
+ bool canScrollHorizontal =
+ !mX.IsAxisLocked() && overscrollHandoffChain->CanScrollInDirection(
+ this, ScrollDirection::eHorizontal);
+ bool canScrollVertical =
+ !mY.IsAxisLocked() && overscrollHandoffChain->CanScrollInDirection(
+ this, ScrollDirection::eVertical);
+ if (GetCurrentTouchBlock()->TouchActionAllowsPanningXY()) {
+ if (canScrollHorizontal && canScrollVertical) {
+ if (apz::IsCloseToHorizontal(aAngle,
+ StaticPrefs::apz_axis_lock_lock_angle())) {
+ mY.SetAxisLocked(true);
+ SetState(PANNING_LOCKED_X);
+ } else if (apz::IsCloseToVertical(
+ aAngle, StaticPrefs::apz_axis_lock_lock_angle())) {
+ mX.SetAxisLocked(true);
+ SetState(PANNING_LOCKED_Y);
+ } else {
+ SetState(PANNING);
+ }
+ } else if (canScrollHorizontal || canScrollVertical) {
+ SetState(PANNING);
+ } else {
+ SetState(NOTHING);
+ }
+ } else if (GetCurrentTouchBlock()->TouchActionAllowsPanningX()) {
+ // Using bigger angle for panning to keep behavior consistent
+ // with IE.
+ if (apz::IsCloseToHorizontal(
+ aAngle, StaticPrefs::apz_axis_lock_direct_pan_angle())) {
+ mY.SetAxisLocked(true);
+ SetState(PANNING_LOCKED_X);
+ mPanDirRestricted = true;
+ } else {
+ // Don't treat these touches as pan/zoom movements since 'touch-action'
+ // value requires it.
+ SetState(NOTHING);
+ }
+ } else if (GetCurrentTouchBlock()->TouchActionAllowsPanningY()) {
+ if (apz::IsCloseToVertical(aAngle,
+ StaticPrefs::apz_axis_lock_direct_pan_angle())) {
+ mX.SetAxisLocked(true);
+ SetState(PANNING_LOCKED_Y);
+ mPanDirRestricted = true;
+ } else {
+ SetState(NOTHING);
+ }
+ } else {
+ SetState(NOTHING);
+ }
+ if (!IsInPanningState()) {
+ // If we didn't enter a panning state because touch-action disallowed it,
+ // make sure to clear any leftover velocity from the pre-threshold
+ // touchmoves.
+ mX.SetVelocity(0);
+ mY.SetVelocity(0);
+ }
+}
+
+void AsyncPanZoomController::HandlePanning(double aAngle) {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ MOZ_ASSERT(GetCurrentInputBlock());
+ RefPtr<const OverscrollHandoffChain> overscrollHandoffChain =
+ GetCurrentInputBlock()->GetOverscrollHandoffChain();
+ bool canScrollHorizontal =
+ !mX.IsAxisLocked() && overscrollHandoffChain->CanScrollInDirection(
+ this, ScrollDirection::eHorizontal);
+ bool canScrollVertical =
+ !mY.IsAxisLocked() && overscrollHandoffChain->CanScrollInDirection(
+ this, ScrollDirection::eVertical);
+
+ MOZ_ASSERT(UsingStatefulAxisLock());
+
+ if (!canScrollHorizontal || !canScrollVertical) {
+ SetState(PANNING);
+ } else if (apz::IsCloseToHorizontal(
+ aAngle, StaticPrefs::apz_axis_lock_lock_angle())) {
+ mY.SetAxisLocked(true);
+ if (canScrollHorizontal) {
+ SetState(PANNING_LOCKED_X);
+ }
+ } else if (apz::IsCloseToVertical(aAngle,
+ StaticPrefs::apz_axis_lock_lock_angle())) {
+ mX.SetAxisLocked(true);
+ if (canScrollVertical) {
+ SetState(PANNING_LOCKED_Y);
+ }
+ } else {
+ SetState(PANNING);
+ }
+}
+
+void AsyncPanZoomController::HandlePanningUpdate(
+ const ScreenPoint& aPanDistance) {
+ // If we're axis-locked, check if the user is trying to break the lock
+ if (GetAxisLockMode() == STICKY && !mPanDirRestricted) {
+ ParentLayerPoint vector =
+ ToParentLayerCoordinates(aPanDistance, mStartTouch);
+
+ float angle = atan2f(vector.y, vector.x); // range [-pi, pi]
+ angle = fabsf(angle); // range [0, pi]
+
+ float breakThreshold =
+ StaticPrefs::apz_axis_lock_breakout_threshold() * GetDPI();
+
+ if (fabs(aPanDistance.x) > breakThreshold ||
+ fabs(aPanDistance.y) > breakThreshold) {
+ switch (mState) {
+ case PANNING_LOCKED_X:
+ if (!apz::IsCloseToHorizontal(
+ angle, StaticPrefs::apz_axis_lock_breakout_angle())) {
+ mY.SetAxisLocked(false);
+ // If we are within the lock angle from the Y axis, lock
+ // onto the Y axis.
+ if (apz::IsCloseToVertical(
+ angle, StaticPrefs::apz_axis_lock_lock_angle())) {
+ mX.SetAxisLocked(true);
+ SetState(PANNING_LOCKED_Y);
+ } else {
+ SetState(PANNING);
+ }
+ }
+ break;
+
+ case PANNING_LOCKED_Y:
+ if (!apz::IsCloseToVertical(
+ angle, StaticPrefs::apz_axis_lock_breakout_angle())) {
+ mX.SetAxisLocked(false);
+ // If we are within the lock angle from the X axis, lock
+ // onto the X axis.
+ if (apz::IsCloseToHorizontal(
+ angle, StaticPrefs::apz_axis_lock_lock_angle())) {
+ mY.SetAxisLocked(true);
+ SetState(PANNING_LOCKED_X);
+ } else {
+ SetState(PANNING);
+ }
+ }
+ break;
+
+ case PANNING:
+ HandlePanning(angle);
+ break;
+
+ default:
+ break;
+ }
+ }
+ }
+}
+
+void AsyncPanZoomController::HandlePinchLocking(
+ const PinchGestureInput& aEvent) {
+ // Focus change and span distance calculated from an event buffer
+ // Used to handle pinch locking irrespective of touch screen sensitivity
+ // Note: both values fall back to the same value as
+ // their un-buffered counterparts if there is only one (the latest)
+ // event in the buffer. ie: when the touch screen is dispatching
+ // events slower than the lifetime of the buffer
+ ParentLayerCoord bufferedSpanDistance;
+ ParentLayerPoint focusPoint, bufferedFocusChange;
+ {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+
+ focusPoint = mPinchEventBuffer.back().mLocalFocusPoint -
+ Metrics().GetCompositionBounds().TopLeft();
+ ParentLayerPoint bufferedLastZoomFocus =
+ (mPinchEventBuffer.size() > 1)
+ ? mPinchEventBuffer.front().mLocalFocusPoint -
+ Metrics().GetCompositionBounds().TopLeft()
+ : mLastZoomFocus;
+
+ bufferedFocusChange = bufferedLastZoomFocus - focusPoint;
+ bufferedSpanDistance = fabsf(mPinchEventBuffer.front().mPreviousSpan -
+ mPinchEventBuffer.back().mCurrentSpan);
+ }
+
+ // Convert to screen coordinates
+ ScreenCoord spanDistance =
+ ToScreenCoordinates(ParentLayerPoint(0, bufferedSpanDistance), focusPoint)
+ .Length();
+ ScreenPoint focusChange =
+ ToScreenCoordinates(bufferedFocusChange, focusPoint);
+
+ if (mPinchLocked) {
+ if (GetPinchLockMode() == PINCH_STICKY) {
+ ScreenCoord spanBreakoutThreshold =
+ StaticPrefs::apz_pinch_lock_span_breakout_threshold() * GetDPI();
+ mPinchLocked = !(spanDistance > spanBreakoutThreshold);
+ }
+ } else {
+ if (GetPinchLockMode() != PINCH_FREE) {
+ ScreenCoord spanLockThreshold =
+ StaticPrefs::apz_pinch_lock_span_lock_threshold() * GetDPI();
+ ScreenCoord scrollLockThreshold =
+ StaticPrefs::apz_pinch_lock_scroll_lock_threshold() * GetDPI();
+
+ if (spanDistance < spanLockThreshold &&
+ focusChange.Length() > scrollLockThreshold) {
+ mPinchLocked = true;
+
+ // We are transitioning to a two-finger pan that could trigger
+ // a fling at its end, so start tracking velocity.
+ StartTouch(aEvent.mLocalFocusPoint, aEvent.mTimeStamp);
+ }
+ }
+ }
+}
+
+nsEventStatus AsyncPanZoomController::StartPanning(
+ const ExternalPoint& aStartPoint, const TimeStamp& aEventTime) {
+ ParentLayerPoint vector =
+ ToParentLayerCoordinates(PanVector(aStartPoint), mStartTouch);
+ double angle = atan2(vector.y, vector.x); // range [-pi, pi]
+ angle = fabs(angle); // range [0, pi]
+
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ HandlePanningWithTouchAction(angle);
+
+ if (IsInPanningState()) {
+ mTouchStartRestingTimeBeforePan = aEventTime - mTouchStartTime;
+ mMinimumVelocityDuringPan = Nothing();
+
+ if (RefPtr<GeckoContentController> controller =
+ GetGeckoContentController()) {
+ controller->NotifyAPZStateChange(GetGuid(),
+ APZStateChange::eStartPanning);
+ }
+ return nsEventStatus_eConsumeNoDefault;
+ }
+ // Don't consume an event that didn't trigger a panning.
+ return nsEventStatus_eIgnore;
+}
+
+void AsyncPanZoomController::UpdateWithTouchAtDevicePoint(
+ const MultiTouchInput& aEvent) {
+ const SingleTouchData& touchData = aEvent.mTouches[0];
+ // Take historical touch data into account in order to improve the accuracy
+ // of the velocity estimate. On many Android devices, the touch screen samples
+ // at a higher rate than vsync (e.g. 100Hz vs 60Hz), and the historical data
+ // lets us take advantage of those high-rate samples.
+ for (const auto& historicalData : touchData.mHistoricalData) {
+ ParentLayerPoint historicalPoint = historicalData.mLocalScreenPoint;
+ mX.UpdateWithTouchAtDevicePoint(historicalPoint.x,
+ historicalData.mTimeStamp);
+ mY.UpdateWithTouchAtDevicePoint(historicalPoint.y,
+ historicalData.mTimeStamp);
+ }
+ ParentLayerPoint point = touchData.mLocalScreenPoint;
+ mX.UpdateWithTouchAtDevicePoint(point.x, aEvent.mTimeStamp);
+ mY.UpdateWithTouchAtDevicePoint(point.y, aEvent.mTimeStamp);
+}
+
+Maybe<CompositionPayload> AsyncPanZoomController::NotifyScrollSampling() {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ return mSampledState.front().TakeScrollPayload();
+}
+
+bool AsyncPanZoomController::AttemptScroll(
+ ParentLayerPoint& aStartPoint, ParentLayerPoint& aEndPoint,
+ OverscrollHandoffState& aOverscrollHandoffState) {
+ // "start - end" rather than "end - start" because e.g. moving your finger
+ // down (*positive* direction along y axis) causes the vertical scroll offset
+ // to *decrease* as the page follows your finger.
+ ParentLayerPoint displacement = aStartPoint - aEndPoint;
+
+ ParentLayerPoint overscroll; // will be used outside monitor block
+
+ // If the direction of panning is reversed within the same input block,
+ // a later event in the block could potentially scroll an APZC earlier
+ // in the handoff chain, than an earlier event in the block (because
+ // the earlier APZC was scrolled to its extent in the original direction).
+ // We want to disallow this.
+ bool scrollThisApzc = false;
+ if (InputBlockState* block = GetCurrentInputBlock()) {
+ scrollThisApzc =
+ !block->GetScrolledApzc() || block->IsDownchainOfScrolledApzc(this);
+ }
+
+ ParentLayerPoint adjustedDisplacement;
+ if (scrollThisApzc) {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ bool respectDisregardedDirections =
+ ScrollSourceRespectsDisregardedDirections(
+ aOverscrollHandoffState.mScrollSource);
+ bool forcesVerticalOverscroll = respectDisregardedDirections &&
+ mScrollMetadata.GetDisregardedDirection() ==
+ Some(ScrollDirection::eVertical);
+ bool forcesHorizontalOverscroll =
+ respectDisregardedDirections &&
+ mScrollMetadata.GetDisregardedDirection() ==
+ Some(ScrollDirection::eHorizontal);
+
+ bool yChanged =
+ mY.AdjustDisplacement(displacement.y, adjustedDisplacement.y,
+ overscroll.y, forcesVerticalOverscroll);
+ bool xChanged =
+ mX.AdjustDisplacement(displacement.x, adjustedDisplacement.x,
+ overscroll.x, forcesHorizontalOverscroll);
+ if (xChanged || yChanged) {
+ ScheduleComposite();
+ }
+
+ if (!IsZero(adjustedDisplacement) &&
+ Metrics().GetZoom() != CSSToParentLayerScale(0)) {
+ ScrollBy(adjustedDisplacement / Metrics().GetZoom());
+ if (InputBlockState* block = GetCurrentInputBlock()) {
+ bool displacementIsUserVisible = true;
+
+ { // Release the APZC lock before calling ToScreenCoordinates which
+ // acquires the APZ tree lock. Note that this just unlocks the mutex
+ // once, so if we're locking it multiple times on the callstack then
+ // this will be insufficient.
+ RecursiveMutexAutoUnlock unlock(mRecursiveMutex);
+
+ ScreenIntPoint screenDisplacement = RoundedToInt(
+ ToScreenCoordinates(adjustedDisplacement, aStartPoint));
+ // If the displacement we just applied rounds to zero in screen space,
+ // then it's probably not going to be visible to the user. In that
+ // case let's not mark this APZC as scrolled, so that even if the
+ // immediate handoff pref is disabled, we'll allow doing the handoff
+ // to the next APZC.
+ if (screenDisplacement == ScreenIntPoint()) {
+ displacementIsUserVisible = false;
+ }
+ }
+ if (displacementIsUserVisible) {
+ block->SetScrolledApzc(this);
+ }
+ }
+ // Note that in the case of instant scrolling, the last snap target ids
+ // will be set after AttemptScroll call so that we can clobber them
+ // unconditionally here.
+ mLastSnapTargetIds = ScrollSnapTargetIds{};
+ ScheduleCompositeAndMaybeRepaint();
+ }
+
+ // Adjust the start point to reflect the consumed portion of the scroll.
+ aStartPoint = aEndPoint + overscroll;
+ } else {
+ overscroll = displacement;
+ }
+
+ // Accumulate the amount of actual scrolling that occurred into the handoff
+ // state. Note that ToScreenCoordinates() needs to be called outside the
+ // mutex.
+ if (!IsZero(adjustedDisplacement)) {
+ aOverscrollHandoffState.mTotalMovement +=
+ ToScreenCoordinates(adjustedDisplacement, aEndPoint);
+ }
+
+ // If we consumed the entire displacement as a normal scroll, great.
+ if (IsZero(overscroll)) {
+ return true;
+ }
+
+ if (AllowScrollHandoffInCurrentBlock()) {
+ // If there is overscroll, first try to hand it off to an APZC later
+ // in the handoff chain to consume (either as a normal scroll or as
+ // overscroll).
+ // Note: "+ overscroll" rather than "- overscroll" because "overscroll"
+ // is what's left of "displacement", and "displacement" is "start - end".
+ ++aOverscrollHandoffState.mChainIndex;
+ bool consumed =
+ CallDispatchScroll(aStartPoint, aEndPoint, aOverscrollHandoffState);
+ if (consumed) {
+ return true;
+ }
+
+ overscroll = aStartPoint - aEndPoint;
+ MOZ_ASSERT(!IsZero(overscroll));
+ }
+
+ // If there is no APZC later in the handoff chain that accepted the
+ // overscroll, try to accept it ourselves. We only accept it if we
+ // are pannable.
+ if (ScrollSourceAllowsOverscroll(aOverscrollHandoffState.mScrollSource)) {
+ APZC_LOG("%p taking overscroll during panning\n", this);
+
+ ParentLayerPoint prevVisualOverscroll = GetOverscrollAmount();
+
+ OverscrollForPanning(overscroll, aOverscrollHandoffState.mPanDistance);
+
+ // Accumulate the amount of change to the overscroll that occurred into the
+ // handoff state. Note that the input amount, |overscroll|, is turned into
+ // some smaller visual overscroll amount (queried via GetOverscrollAmount())
+ // by applying resistance (Axis::ApplyResistance()), and it's the latter we
+ // want to count towards OverscrollHandoffState::mTotalMovement.
+ ParentLayerPoint visualOverscrollChange =
+ GetOverscrollAmount() - prevVisualOverscroll;
+ if (!IsZero(visualOverscrollChange)) {
+ aOverscrollHandoffState.mTotalMovement +=
+ ToScreenCoordinates(visualOverscrollChange, aEndPoint);
+ }
+ }
+
+ aStartPoint = aEndPoint + overscroll;
+
+ return IsZero(overscroll);
+}
+
+void AsyncPanZoomController::OverscrollForPanning(
+ ParentLayerPoint& aOverscroll, const ScreenPoint& aPanDistance) {
+ // Only allow entering overscroll along an axis if the pan distance along
+ // that axis is greater than the pan distance along the other axis by a
+ // configurable factor. If we are already overscrolled, don't check this.
+ if (!IsOverscrolled()) {
+ if (aPanDistance.x <
+ StaticPrefs::apz_overscroll_min_pan_distance_ratio() * aPanDistance.y) {
+ aOverscroll.x = 0;
+ }
+ if (aPanDistance.y <
+ StaticPrefs::apz_overscroll_min_pan_distance_ratio() * aPanDistance.x) {
+ aOverscroll.y = 0;
+ }
+ }
+
+ OverscrollBy(aOverscroll);
+}
+
+ScrollDirections AsyncPanZoomController::GetOverscrollableDirections() const {
+ ScrollDirections result;
+
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+
+ // If the target has the disregarded direction, it means it's single line
+ // text control, thus we don't want to overscroll in both directions.
+ if (mScrollMetadata.GetDisregardedDirection()) {
+ return result;
+ }
+
+ if (mX.CanScroll() && mX.OverscrollBehaviorAllowsOverscrollEffect()) {
+ result += ScrollDirection::eHorizontal;
+ }
+
+ if (mY.CanScroll() && mY.OverscrollBehaviorAllowsOverscrollEffect()) {
+ result += ScrollDirection::eVertical;
+ }
+
+ return result;
+}
+
+void AsyncPanZoomController::OverscrollBy(ParentLayerPoint& aOverscroll) {
+ if (!StaticPrefs::apz_overscroll_enabled()) {
+ return;
+ }
+
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ // Do not go into overscroll in a direction in which we have no room to
+ // scroll to begin with.
+ ScrollDirections overscrollableDirections = GetOverscrollableDirections();
+ if (IsZero(aOverscroll.x)) {
+ overscrollableDirections -= ScrollDirection::eHorizontal;
+ }
+ if (IsZero(aOverscroll.y)) {
+ overscrollableDirections -= ScrollDirection::eVertical;
+ }
+
+ mOverscrollEffect->ConsumeOverscroll(aOverscroll, overscrollableDirections);
+}
+
+RefPtr<const OverscrollHandoffChain>
+AsyncPanZoomController::BuildOverscrollHandoffChain() {
+ if (APZCTreeManager* treeManagerLocal = GetApzcTreeManager()) {
+ return treeManagerLocal->BuildOverscrollHandoffChain(this);
+ }
+
+ // This APZC IsDestroyed(). To avoid callers having to special-case this
+ // scenario, just build a 1-element chain containing ourselves.
+ OverscrollHandoffChain* result = new OverscrollHandoffChain;
+ result->Add(this);
+ return result;
+}
+
+ParentLayerPoint AsyncPanZoomController::AttemptFling(
+ const FlingHandoffState& aHandoffState) {
+ // The PLPPI computation acquires the tree lock, so it needs to be performed
+ // on the controller thread, and before the APZC lock is acquired.
+ APZThreadUtils::AssertOnControllerThread();
+ float PLPPI = ComputePLPPI(PanStart(), aHandoffState.mVelocity);
+
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+
+ if (!IsPannable()) {
+ return aHandoffState.mVelocity;
+ }
+
+ // We may have a pre-existing velocity for whatever reason (for example,
+ // a previously handed off fling). We don't want to clobber that.
+ APZC_LOG("%p accepting fling with velocity %s\n", this,
+ ToString(aHandoffState.mVelocity).c_str());
+ ParentLayerPoint residualVelocity = aHandoffState.mVelocity;
+ if (mX.CanScroll()) {
+ mX.SetVelocity(mX.GetVelocity() + aHandoffState.mVelocity.x);
+ residualVelocity.x = 0;
+ }
+ if (mY.CanScroll()) {
+ mY.SetVelocity(mY.GetVelocity() + aHandoffState.mVelocity.y);
+ residualVelocity.y = 0;
+ }
+
+ // If we're not scrollable in at least one of the directions in which we
+ // were handed velocity, don't start a fling animation.
+ // The |IsFinite()| condition should only fail when running some tests
+ // that generate events faster than the clock resolution.
+ ParentLayerPoint velocity = GetVelocityVector();
+ if (!velocity.IsFinite() ||
+ velocity.Length() <= StaticPrefs::apz_fling_min_velocity_threshold()) {
+ // Relieve overscroll now if needed, since we will not transition to a fling
+ // animation and then an overscroll animation, and relieve it then.
+ aHandoffState.mChain->SnapBackOverscrolledApzc(this);
+ return residualVelocity;
+ }
+
+ // If there's a scroll snap point near the predicted fling destination,
+ // scroll there using a smooth scroll animation. Otherwise, start a
+ // fling animation.
+ ScrollSnapToDestination();
+ if (mState != SMOOTHMSD_SCROLL) {
+ SetState(FLING);
+ AsyncPanZoomAnimation* fling =
+ GetPlatformSpecificState()->CreateFlingAnimation(*this, aHandoffState,
+ PLPPI);
+ StartAnimation(fling);
+ }
+
+ return residualVelocity;
+}
+
+float AsyncPanZoomController::ComputePLPPI(ParentLayerPoint aPoint,
+ ParentLayerPoint aDirection) const {
+ // Avoid division-by-zero.
+ if (aDirection == ParentLayerPoint()) {
+ return GetDPI();
+ }
+
+ // Convert |aDirection| into a unit vector.
+ aDirection = aDirection / aDirection.Length();
+
+ // Place the vector at |aPoint| and convert to screen coordinates.
+ // The length of the resulting vector is the number of Screen coordinates
+ // that equal 1 ParentLayer coordinate in the given direction.
+ float screenPerParent = ToScreenCoordinates(aDirection, aPoint).Length();
+
+ // Finally, factor in the DPI scale.
+ return GetDPI() / screenPerParent;
+}
+
+Maybe<CSSPoint> AsyncPanZoomController::GetCurrentAnimationDestination(
+ const RecursiveMutexAutoLock& aProofOfLock) const {
+ if (mState == WHEEL_SCROLL) {
+ return Some(mAnimation->AsWheelScrollAnimation()->GetDestination());
+ }
+ if (mState == SMOOTH_SCROLL) {
+ return Some(mAnimation->AsSmoothScrollAnimation()->GetDestination());
+ }
+ if (mState == SMOOTHMSD_SCROLL) {
+ return Some(mAnimation->AsSmoothMsdScrollAnimation()->GetDestination());
+ }
+ if (mState == KEYBOARD_SCROLL) {
+ return Some(mAnimation->AsSmoothScrollAnimation()->GetDestination());
+ }
+
+ return Nothing();
+}
+
+ParentLayerPoint
+AsyncPanZoomController::AdjustHandoffVelocityForOverscrollBehavior(
+ ParentLayerPoint& aHandoffVelocity) const {
+ ParentLayerPoint residualVelocity;
+ ScrollDirections handoffDirections = GetAllowedHandoffDirections();
+ if (!handoffDirections.contains(ScrollDirection::eHorizontal)) {
+ residualVelocity.x = aHandoffVelocity.x;
+ aHandoffVelocity.x = 0;
+ }
+ if (!handoffDirections.contains(ScrollDirection::eVertical)) {
+ residualVelocity.y = aHandoffVelocity.y;
+ aHandoffVelocity.y = 0;
+ }
+ return residualVelocity;
+}
+
+bool AsyncPanZoomController::OverscrollBehaviorAllowsSwipe() const {
+ // Swipe navigation is a "non-local" overscroll behavior like handoff.
+ return GetAllowedHandoffDirections().contains(ScrollDirection::eHorizontal);
+}
+
+void AsyncPanZoomController::HandleFlingOverscroll(
+ const ParentLayerPoint& aVelocity, SideBits aOverscrollSideBits,
+ const RefPtr<const OverscrollHandoffChain>& aOverscrollHandoffChain,
+ const RefPtr<const AsyncPanZoomController>& aScrolledApzc) {
+ APZCTreeManager* treeManagerLocal = GetApzcTreeManager();
+ if (treeManagerLocal) {
+ const FlingHandoffState handoffState{
+ aVelocity, aOverscrollHandoffChain, Nothing(),
+ 0, true /* handoff */, aScrolledApzc};
+ ParentLayerPoint residualVelocity =
+ treeManagerLocal->DispatchFling(this, handoffState);
+ FLING_LOG("APZC %p left with residual velocity %s\n", this,
+ ToString(residualVelocity).c_str());
+ if (!IsZero(residualVelocity) && IsPannable() &&
+ StaticPrefs::apz_overscroll_enabled()) {
+ // Obey overscroll-behavior.
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ if (!mX.OverscrollBehaviorAllowsOverscrollEffect()) {
+ residualVelocity.x = 0;
+ }
+ if (!mY.OverscrollBehaviorAllowsOverscrollEffect()) {
+ residualVelocity.y = 0;
+ }
+
+ if (!IsZero(residualVelocity)) {
+ mOverscrollEffect->RelieveOverscroll(residualVelocity,
+ aOverscrollSideBits);
+ }
+ }
+ }
+}
+
+void AsyncPanZoomController::HandleSmoothScrollOverscroll(
+ const ParentLayerPoint& aVelocity, SideBits aOverscrollSideBits) {
+ // We must call BuildOverscrollHandoffChain from this deferred callback
+ // function in order to avoid a deadlock when acquiring the tree lock.
+ HandleFlingOverscroll(aVelocity, aOverscrollSideBits,
+ BuildOverscrollHandoffChain(), nullptr);
+}
+
+void AsyncPanZoomController::SmoothScrollTo(const CSSPoint& aDestination,
+ const ScrollOrigin& aOrigin) {
+ // Convert velocity from ParentLayerPoints/ms to ParentLayerPoints/s and then
+ // to appunits/second.
+ nsPoint destination = CSSPoint::ToAppUnits(aDestination);
+ nsSize velocity;
+ if (Metrics().GetZoom() != CSSToParentLayerScale(0)) {
+ velocity = CSSSize::ToAppUnits(ParentLayerSize(mX.GetVelocity() * 1000.0f,
+ mY.GetVelocity() * 1000.0f) /
+ Metrics().GetZoom());
+ }
+
+ if (mState == SMOOTH_SCROLL && mAnimation) {
+ RefPtr<SmoothScrollAnimation> animation(
+ mAnimation->AsSmoothScrollAnimation());
+ if (animation->GetScrollOrigin() == aOrigin) {
+ APZC_LOG("%p updating destination on existing animation\n", this);
+ animation->UpdateDestination(GetFrameTime().Time(), destination,
+ velocity);
+ return;
+ }
+ }
+
+ CancelAnimation();
+ SetState(SMOOTH_SCROLL);
+ nsPoint initialPosition =
+ CSSPoint::ToAppUnits(Metrics().GetVisualScrollOffset());
+ RefPtr<SmoothScrollAnimation> animation =
+ new SmoothScrollAnimation(*this, initialPosition, aOrigin);
+ animation->UpdateDestination(GetFrameTime().Time(), destination, velocity);
+ StartAnimation(animation.get());
+}
+
+void AsyncPanZoomController::SmoothMsdScrollTo(
+ CSSSnapTarget&& aDestination, ScrollTriggeredByScript aTriggeredByScript) {
+ if (mState == SMOOTHMSD_SCROLL && mAnimation) {
+ APZC_LOG("%p updating destination on existing animation\n", this);
+ RefPtr<SmoothMsdScrollAnimation> animation(
+ static_cast<SmoothMsdScrollAnimation*>(mAnimation.get()));
+ animation->SetDestination(aDestination.mPosition,
+ std::move(aDestination.mTargetIds),
+ aTriggeredByScript);
+ } else {
+ CancelAnimation();
+ SetState(SMOOTHMSD_SCROLL);
+ // Convert velocity from ParentLayerPoints/ms to ParentLayerPoints/s.
+ CSSPoint initialVelocity;
+ if (Metrics().GetZoom() != CSSToParentLayerScale(0)) {
+ initialVelocity = ParentLayerPoint(mX.GetVelocity() * 1000.0f,
+ mY.GetVelocity() * 1000.0f) /
+ Metrics().GetZoom();
+ }
+
+ StartAnimation(new SmoothMsdScrollAnimation(
+ *this, Metrics().GetVisualScrollOffset(), initialVelocity,
+ aDestination.mPosition,
+ StaticPrefs::layout_css_scroll_behavior_spring_constant(),
+ StaticPrefs::layout_css_scroll_behavior_damping_ratio(),
+ std::move(aDestination.mTargetIds), aTriggeredByScript));
+ }
+}
+
+void AsyncPanZoomController::StartOverscrollAnimation(
+ const ParentLayerPoint& aVelocity, SideBits aOverscrollSideBits) {
+ MOZ_ASSERT(mState != OVERSCROLL_ANIMATION);
+
+ SetState(OVERSCROLL_ANIMATION);
+
+ ParentLayerPoint velocity = aVelocity;
+ AdjustDeltaForAllowedScrollDirections(velocity,
+ GetOverscrollableDirections());
+ StartAnimation(new OverscrollAnimation(*this, velocity, aOverscrollSideBits));
+}
+
+bool AsyncPanZoomController::CallDispatchScroll(
+ ParentLayerPoint& aStartPoint, ParentLayerPoint& aEndPoint,
+ OverscrollHandoffState& aOverscrollHandoffState) {
+ // Make a local copy of the tree manager pointer and check if it's not
+ // null before calling DispatchScroll(). This is necessary because
+ // Destroy(), which nulls out mTreeManager, could be called concurrently.
+ APZCTreeManager* treeManagerLocal = GetApzcTreeManager();
+ if (!treeManagerLocal) {
+ return false;
+ }
+
+ // Obey overscroll-behavior.
+ ParentLayerPoint endPoint = aEndPoint;
+ if (aOverscrollHandoffState.mChainIndex > 0) {
+ ScrollDirections handoffDirections = GetAllowedHandoffDirections();
+ if (!handoffDirections.contains(ScrollDirection::eHorizontal)) {
+ endPoint.x = aStartPoint.x;
+ }
+ if (!handoffDirections.contains(ScrollDirection::eVertical)) {
+ endPoint.y = aStartPoint.y;
+ }
+ if (aStartPoint == endPoint) {
+ // Handoff not allowed in either direction - don't even bother.
+ return false;
+ }
+ }
+
+ return treeManagerLocal->DispatchScroll(this, aStartPoint, endPoint,
+ aOverscrollHandoffState);
+}
+
+void AsyncPanZoomController::RecordScrollPayload(const TimeStamp& aTimeStamp) {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ if (!mScrollPayload) {
+ mScrollPayload = Some(
+ CompositionPayload{CompositionPayloadType::eAPZScroll, aTimeStamp});
+ }
+}
+
+void AsyncPanZoomController::StartTouch(const ParentLayerPoint& aPoint,
+ TimeStamp aTimestamp) {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ mX.StartTouch(aPoint.x, aTimestamp);
+ mY.StartTouch(aPoint.y, aTimestamp);
+}
+
+void AsyncPanZoomController::EndTouch(TimeStamp aTimestamp,
+ Axis::ClearAxisLock aClearAxisLock) {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ mX.EndTouch(aTimestamp, aClearAxisLock);
+ mY.EndTouch(aTimestamp, aClearAxisLock);
+}
+
+void AsyncPanZoomController::TrackTouch(const MultiTouchInput& aEvent) {
+ ExternalPoint extPoint = GetFirstExternalTouchPoint(aEvent);
+ ScreenPoint panVector = PanVector(extPoint);
+ HandlePanningUpdate(panVector);
+
+ ParentLayerPoint prevTouchPoint(mX.GetPos(), mY.GetPos());
+ ParentLayerPoint touchPoint = GetFirstTouchPoint(aEvent);
+
+ UpdateWithTouchAtDevicePoint(aEvent);
+
+ auto velocity = GetVelocityVector().Length();
+ if (mMinimumVelocityDuringPan) {
+ mMinimumVelocityDuringPan =
+ Some(std::min(*mMinimumVelocityDuringPan, velocity));
+ } else {
+ mMinimumVelocityDuringPan = Some(velocity);
+ }
+
+ if (prevTouchPoint != touchPoint) {
+ MOZ_ASSERT(GetCurrentTouchBlock());
+ OverscrollHandoffState handoffState(
+ *GetCurrentTouchBlock()->GetOverscrollHandoffChain(), panVector,
+ ScrollSource::Touchscreen);
+ RecordScrollPayload(aEvent.mTimeStamp);
+ CallDispatchScroll(prevTouchPoint, touchPoint, handoffState);
+ }
+}
+
+ParentLayerPoint AsyncPanZoomController::GetFirstTouchPoint(
+ const MultiTouchInput& aEvent) {
+ return ((SingleTouchData&)aEvent.mTouches[0]).mLocalScreenPoint;
+}
+
+ExternalPoint AsyncPanZoomController::GetFirstExternalTouchPoint(
+ const MultiTouchInput& aEvent) {
+ return ToExternalPoint(aEvent.mScreenOffset,
+ ((SingleTouchData&)aEvent.mTouches[0]).mScreenPoint);
+}
+
+ParentLayerPoint AsyncPanZoomController::GetOverscrollAmount() const {
+ if (StaticPrefs::apz_overscroll_test_async_scroll_offset_enabled()) {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ AutoApplyAsyncTestAttributes testAttributeApplier(this, lock);
+ return GetOverscrollAmountInternal();
+ }
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ return GetOverscrollAmountInternal();
+}
+
+ParentLayerPoint AsyncPanZoomController::GetOverscrollAmountInternal() const {
+ return {mX.GetOverscroll(), mY.GetOverscroll()};
+}
+
+SideBits AsyncPanZoomController::GetOverscrollSideBits() const {
+ return apz::GetOverscrollSideBits({mX.GetOverscroll(), mY.GetOverscroll()});
+}
+
+void AsyncPanZoomController::RestoreOverscrollAmount(
+ const ParentLayerPoint& aOverscroll) {
+ mX.RestoreOverscroll(aOverscroll.x);
+ mY.RestoreOverscroll(aOverscroll.y);
+}
+
+void AsyncPanZoomController::StartAnimation(AsyncPanZoomAnimation* aAnimation) {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ mAnimation = aAnimation;
+ mLastSampleTime = GetFrameTime();
+ ScheduleComposite();
+}
+
+void AsyncPanZoomController::CancelAnimation(CancelAnimationFlags aFlags) {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ APZC_LOG_DETAIL("running CancelAnimation(0x%x) in state %s\n", this, aFlags,
+ ToString(mState).c_str());
+
+ if ((aFlags & ExcludeWheel) && mState == WHEEL_SCROLL) {
+ return;
+ }
+
+ if (mAnimation) {
+ mAnimation->Cancel(aFlags);
+ }
+
+ SetState(NOTHING);
+ mLastSnapTargetIds = ScrollSnapTargetIds{};
+ mAnimation = nullptr;
+ // Since there is no animation in progress now the axes should
+ // have no velocity either. If we are dropping the velocity from a non-zero
+ // value we should trigger a repaint as the displayport margins are dependent
+ // on the velocity and the last repaint request might not have good margins
+ // any more.
+ bool repaint = !IsZero(GetVelocityVector());
+ mX.SetVelocity(0);
+ mY.SetVelocity(0);
+ mX.SetAxisLocked(false);
+ mY.SetAxisLocked(false);
+ // Setting the state to nothing and cancelling the animation can
+ // preempt normal mechanisms for relieving overscroll, so we need to clear
+ // overscroll here.
+ if (!(aFlags & ExcludeOverscroll) && IsOverscrolled()) {
+ ClearOverscroll();
+ repaint = true;
+ }
+ // Similar to relieving overscroll, we also need to snap to any snap points
+ // if appropriate.
+ if (aFlags & CancelAnimationFlags::ScrollSnap) {
+ ScrollSnap(ScrollSnapFlags::IntendedEndPosition);
+ }
+ if (repaint) {
+ RequestContentRepaint();
+ ScheduleComposite();
+ }
+}
+
+void AsyncPanZoomController::ClearOverscroll() {
+ mOverscrollEffect->ClearOverscroll();
+}
+
+void AsyncPanZoomController::ClearPhysicalOverscroll() {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ mX.ClearOverscroll();
+ mY.ClearOverscroll();
+}
+
+void AsyncPanZoomController::SetCompositorController(
+ CompositorController* aCompositorController) {
+ mCompositorController = aCompositorController;
+}
+
+void AsyncPanZoomController::SetVisualScrollOffset(const CSSPoint& aOffset) {
+ Metrics().SetVisualScrollOffset(aOffset);
+ Metrics().RecalculateLayoutViewportOffset();
+}
+
+void AsyncPanZoomController::ClampAndSetVisualScrollOffset(
+ const CSSPoint& aOffset) {
+ Metrics().ClampAndSetVisualScrollOffset(aOffset);
+ Metrics().RecalculateLayoutViewportOffset();
+}
+
+void AsyncPanZoomController::ScrollBy(const CSSPoint& aOffset) {
+ SetVisualScrollOffset(Metrics().GetVisualScrollOffset() + aOffset);
+}
+
+void AsyncPanZoomController::ScrollByAndClamp(const CSSPoint& aOffset) {
+ ClampAndSetVisualScrollOffset(Metrics().GetVisualScrollOffset() + aOffset);
+}
+
+void AsyncPanZoomController::ScaleWithFocus(float aScale,
+ const CSSPoint& aFocus) {
+ Metrics().ZoomBy(aScale);
+ // We want to adjust the scroll offset such that the CSS point represented by
+ // aFocus remains at the same position on the screen before and after the
+ // change in zoom. The below code accomplishes this; see
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=923431#c6 for an in-depth
+ // explanation of how.
+ SetVisualScrollOffset((Metrics().GetVisualScrollOffset() + aFocus) -
+ (aFocus / aScale));
+}
+
+/*static*/
+gfx::IntSize AsyncPanZoomController::GetDisplayportAlignmentMultiplier(
+ const ScreenSize& aBaseSize) {
+ gfx::IntSize multiplier(1, 1);
+ float baseWidth = aBaseSize.width;
+ while (baseWidth > 500) {
+ baseWidth /= 2;
+ multiplier.width *= 2;
+ if (multiplier.width >= 8) {
+ break;
+ }
+ }
+ float baseHeight = aBaseSize.height;
+ while (baseHeight > 500) {
+ baseHeight /= 2;
+ multiplier.height *= 2;
+ if (multiplier.height >= 8) {
+ break;
+ }
+ }
+ return multiplier;
+}
+
+/**
+ * Enlarges the displayport along both axes based on the velocity.
+ */
+static CSSSize CalculateDisplayPortSize(
+ const CSSSize& aCompositionSize, const CSSPoint& aVelocity,
+ AsyncPanZoomController::ZoomInProgress aZoomInProgress,
+ const CSSToScreenScale2D& aDpPerCSS) {
+ bool xIsStationarySpeed =
+ fabsf(aVelocity.x) < StaticPrefs::apz_min_skate_speed();
+ bool yIsStationarySpeed =
+ fabsf(aVelocity.y) < StaticPrefs::apz_min_skate_speed();
+ float xMultiplier = xIsStationarySpeed
+ ? StaticPrefs::apz_x_stationary_size_multiplier()
+ : StaticPrefs::apz_x_skate_size_multiplier();
+ float yMultiplier = yIsStationarySpeed
+ ? StaticPrefs::apz_y_stationary_size_multiplier()
+ : StaticPrefs::apz_y_skate_size_multiplier();
+
+ if (IsHighMemSystem() && !xIsStationarySpeed) {
+ xMultiplier += StaticPrefs::apz_x_skate_highmem_adjust();
+ }
+
+ if (IsHighMemSystem() && !yIsStationarySpeed) {
+ yMultiplier += StaticPrefs::apz_y_skate_highmem_adjust();
+ }
+
+ if (aZoomInProgress == AsyncPanZoomController::ZoomInProgress::Yes) {
+ // If a zoom is in progress, we will be making content visible on the
+ // x and y axes in equal proportion, because the zoom operation scales
+ // equally on the x and y axes. The default multipliers computed above are
+ // biased towards the y-axis since that's where most scrolling occurs, but
+ // in the case of zooming, we should really use equal multipliers on both
+ // axes. This does that while preserving the total displayport area
+ // quantity (aCompositionSize.Area() * xMultiplier * yMultiplier).
+ // Note that normally changing the shape of the displayport is expensive
+ // and should be avoided, but if a zoom is in progress the displayport
+ // is likely going to be fully repainted anyway due to changes in resolution
+ // so there should be no marginal cost to also changing the shape of it.
+ float areaMultiplier = xMultiplier * yMultiplier;
+ xMultiplier = sqrt(areaMultiplier);
+ yMultiplier = xMultiplier;
+ }
+
+ // Scale down the margin multipliers by the alignment multiplier because
+ // the alignment code will expand the displayport outward to the multiplied
+ // alignment. This is not necessary for correctness, but for performance;
+ // if we don't do this the displayport can end up much larger. The math here
+ // is actually just scaling the part of the multipler that is > 1, so that
+ // we never end up with xMultiplier or yMultiplier being less than 1 (that
+ // would result in a guaranteed checkerboarding situation). Note that the
+ // calculation doesn't cancel exactly the increased margin from applying
+ // the alignment multiplier, but this is simple and should provide
+ // reasonable behaviour in most cases.
+ gfx::IntSize alignmentMultipler =
+ AsyncPanZoomController::GetDisplayportAlignmentMultiplier(
+ aCompositionSize * aDpPerCSS);
+ if (xMultiplier > 1) {
+ xMultiplier = ((xMultiplier - 1) / alignmentMultipler.width) + 1;
+ }
+ if (yMultiplier > 1) {
+ yMultiplier = ((yMultiplier - 1) / alignmentMultipler.height) + 1;
+ }
+
+ return aCompositionSize * CSSSize(xMultiplier, yMultiplier);
+}
+
+/**
+ * Ensures that the displayport is at least as large as the visible area
+ * inflated by the danger zone. If this is not the case then the
+ * "AboutToCheckerboard" function in TiledContentClient.cpp will return true
+ * even in the stable state.
+ */
+static CSSSize ExpandDisplayPortToDangerZone(
+ const CSSSize& aDisplayPortSize, const FrameMetrics& aFrameMetrics) {
+ CSSSize dangerZone(0.0f, 0.0f);
+ if (aFrameMetrics.DisplayportPixelsPerCSSPixel().xScale != 0 &&
+ aFrameMetrics.DisplayportPixelsPerCSSPixel().yScale != 0) {
+ dangerZone = ScreenSize(StaticPrefs::apz_danger_zone_x(),
+ StaticPrefs::apz_danger_zone_y()) /
+ aFrameMetrics.DisplayportPixelsPerCSSPixel();
+ }
+ const CSSSize compositionSize =
+ aFrameMetrics.CalculateBoundedCompositedSizeInCssPixels();
+
+ const float xSize = std::max(aDisplayPortSize.width,
+ compositionSize.width + (2 * dangerZone.width));
+
+ const float ySize =
+ std::max(aDisplayPortSize.height,
+ compositionSize.height + (2 * dangerZone.height));
+
+ return CSSSize(xSize, ySize);
+}
+
+/**
+ * Attempts to redistribute any area in the displayport that would get clipped
+ * by the scrollable rect, or be inaccessible due to disabled scrolling, to the
+ * other axis, while maintaining total displayport area.
+ */
+static void RedistributeDisplayPortExcess(CSSSize& aDisplayPortSize,
+ const CSSRect& aScrollableRect) {
+ // As aDisplayPortSize.height * aDisplayPortSize.width does not change,
+ // we are just scaling by the ratio and its inverse.
+ if (aDisplayPortSize.height > aScrollableRect.Height()) {
+ aDisplayPortSize.width *=
+ (aDisplayPortSize.height / aScrollableRect.Height());
+ aDisplayPortSize.height = aScrollableRect.Height();
+ } else if (aDisplayPortSize.width > aScrollableRect.Width()) {
+ aDisplayPortSize.height *=
+ (aDisplayPortSize.width / aScrollableRect.Width());
+ aDisplayPortSize.width = aScrollableRect.Width();
+ }
+}
+
+/* static */
+const ScreenMargin AsyncPanZoomController::CalculatePendingDisplayPort(
+ const FrameMetrics& aFrameMetrics, const ParentLayerPoint& aVelocity,
+ ZoomInProgress aZoomInProgress) {
+ if (aFrameMetrics.IsScrollInfoLayer()) {
+ // Don't compute margins. Since we can't asynchronously scroll this frame,
+ // we don't want to paint anything more than the composition bounds.
+ return ScreenMargin();
+ }
+
+ CSSSize compositionSize =
+ aFrameMetrics.CalculateBoundedCompositedSizeInCssPixels();
+ CSSPoint velocity;
+ if (aFrameMetrics.GetZoom() != CSSToParentLayerScale(0)) {
+ velocity = aVelocity / aFrameMetrics.GetZoom(); // avoid division by zero
+ }
+ CSSRect scrollableRect = aFrameMetrics.GetExpandedScrollableRect();
+
+ // Calculate the displayport size based on how fast we're moving along each
+ // axis.
+ CSSSize displayPortSize =
+ CalculateDisplayPortSize(compositionSize, velocity, aZoomInProgress,
+ aFrameMetrics.DisplayportPixelsPerCSSPixel());
+
+ displayPortSize =
+ ExpandDisplayPortToDangerZone(displayPortSize, aFrameMetrics);
+
+ if (StaticPrefs::apz_enlarge_displayport_when_clipped()) {
+ RedistributeDisplayPortExcess(displayPortSize, scrollableRect);
+ }
+
+ // We calculate a "displayport" here which is relative to the scroll offset.
+ // Note that the scroll offset we have here in the APZ code may not be the
+ // same as the base rect that gets used on the layout side when the
+ // displayport margins are actually applied, so it is important to only
+ // consider the displayport as margins relative to a scroll offset rather than
+ // relative to something more unchanging like the scrollable rect origin.
+
+ // Center the displayport based on its expansion over the composition size.
+ CSSRect displayPort((compositionSize.width - displayPortSize.width) / 2.0f,
+ (compositionSize.height - displayPortSize.height) / 2.0f,
+ displayPortSize.width, displayPortSize.height);
+
+ // Offset the displayport, depending on how fast we're moving and the
+ // estimated time it takes to paint, to try to minimise checkerboarding.
+ float paintFactor = kDefaultEstimatedPaintDurationMs;
+ displayPort.MoveBy(velocity * paintFactor * StaticPrefs::apz_velocity_bias());
+
+ APZC_LOGV_FM(aFrameMetrics,
+ "Calculated displayport as %s from velocity %s zooming %d paint "
+ "time %f metrics",
+ ToString(displayPort).c_str(), ToString(aVelocity).c_str(),
+ (int)aZoomInProgress, paintFactor);
+
+ CSSMargin cssMargins;
+ cssMargins.left = -displayPort.X();
+ cssMargins.top = -displayPort.Y();
+ cssMargins.right =
+ displayPort.Width() - compositionSize.width - cssMargins.left;
+ cssMargins.bottom =
+ displayPort.Height() - compositionSize.height - cssMargins.top;
+
+ return cssMargins * aFrameMetrics.DisplayportPixelsPerCSSPixel();
+}
+
+void AsyncPanZoomController::ScheduleComposite() {
+ if (mCompositorController) {
+ mCompositorController->ScheduleRenderOnCompositorThread(
+ wr::RenderReasons::APZ);
+ }
+}
+
+void AsyncPanZoomController::ScheduleCompositeAndMaybeRepaint() {
+ ScheduleComposite();
+ RequestContentRepaint();
+}
+
+void AsyncPanZoomController::FlushRepaintForOverscrollHandoff() {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ RequestContentRepaint();
+}
+
+void AsyncPanZoomController::FlushRepaintForNewInputBlock() {
+ APZC_LOG("%p flushing repaint for new input block\n", this);
+
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ RequestContentRepaint();
+}
+
+bool AsyncPanZoomController::SnapBackIfOverscrolled() {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ if (SnapBackIfOverscrolledForMomentum(ParentLayerPoint(0, 0))) {
+ return true;
+ }
+ // If we don't kick off an overscroll animation, we still need to snap to any
+ // nearby snap points, assuming we haven't already done so when we started
+ // this fling
+ if (mState != FLING) {
+ ScrollSnap(ScrollSnapFlags::IntendedEndPosition);
+ }
+ return false;
+}
+
+bool AsyncPanZoomController::SnapBackIfOverscrolledForMomentum(
+ const ParentLayerPoint& aVelocity) {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ // It's possible that we're already in the middle of an overscroll
+ // animation - if so, don't start a new one.
+ if (IsOverscrolled() && mState != OVERSCROLL_ANIMATION) {
+ APZC_LOG("%p is overscrolled, starting snap-back\n", this);
+ mOverscrollEffect->RelieveOverscroll(aVelocity, GetOverscrollSideBits());
+ return true;
+ }
+ return false;
+}
+
+bool AsyncPanZoomController::IsFlingingFast() const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ if (mState == FLING && GetVelocityVector().Length() >
+ StaticPrefs::apz_fling_stop_on_tap_threshold()) {
+ APZC_LOG("%p is moving fast\n", this);
+ return true;
+ }
+ return false;
+}
+
+bool AsyncPanZoomController::IsPannable() const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ return mX.CanScroll() || mY.CanScroll();
+}
+
+bool AsyncPanZoomController::IsScrollInfoLayer() const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ return Metrics().IsScrollInfoLayer();
+}
+
+int32_t AsyncPanZoomController::GetLastTouchIdentifier() const {
+ RefPtr<GestureEventListener> listener = GetGestureEventListener();
+ return listener ? listener->GetLastTouchIdentifier() : -1;
+}
+
+void AsyncPanZoomController::RequestContentRepaint(
+ RepaintUpdateType aUpdateType) {
+ // Reinvoke this method on the repaint thread if it's not there already. It's
+ // important to do this before the call to CalculatePendingDisplayPort, so
+ // that CalculatePendingDisplayPort uses the most recent available version of
+ // Metrics(). just before the paint request is dispatched to content.
+ RefPtr<GeckoContentController> controller = GetGeckoContentController();
+ if (!controller) {
+ return;
+ }
+ if (!controller->IsRepaintThread()) {
+ // Even though we want to do the actual repaint request on the repaint
+ // thread, we want to update the expected gecko metrics synchronously.
+ // Otherwise we introduce a race condition where we might read from the
+ // expected gecko metrics on the controller thread before or after it gets
+ // updated on the repaint thread, when in fact we always want the updated
+ // version when reading.
+ { // scope lock
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ mExpectedGeckoMetrics.UpdateFrom(Metrics());
+ }
+
+ // use the local variable to resolve the function overload.
+ auto func =
+ static_cast<void (AsyncPanZoomController::*)(RepaintUpdateType)>(
+ &AsyncPanZoomController::RequestContentRepaint);
+ controller->DispatchToRepaintThread(NewRunnableMethod<RepaintUpdateType>(
+ "layers::AsyncPanZoomController::RequestContentRepaint", this, func,
+ aUpdateType));
+ return;
+ }
+
+ MOZ_ASSERT(controller->IsRepaintThread());
+
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ ParentLayerPoint velocity = GetVelocityVector();
+ ScreenMargin displayportMargins = CalculatePendingDisplayPort(
+ Metrics(), velocity,
+ (mState == PINCHING || mState == ANIMATING_ZOOM) ? ZoomInProgress::Yes
+ : ZoomInProgress::No);
+ Metrics().SetPaintRequestTime(TimeStamp::Now());
+ RequestContentRepaint(velocity, displayportMargins, aUpdateType);
+}
+
+static CSSRect GetDisplayPortRect(const FrameMetrics& aFrameMetrics,
+ const ScreenMargin& aDisplayportMargins) {
+ // This computation is based on what happens in CalculatePendingDisplayPort.
+ // If that changes then this might need to change too.
+ // Note that the display port rect APZ computes is relative to the visual
+ // scroll offset. It's adjusted to be relative to the layout scroll offset
+ // when the main thread processes a repaint request (in
+ // APZCCallbackHelper::AdjustDisplayPortForScrollDelta()) and ultimately
+ // applied (in DisplayPortUtils::GetDisplayPort()) in this adjusted form.
+ CSSRect baseRect(aFrameMetrics.GetVisualScrollOffset(),
+ aFrameMetrics.CalculateBoundedCompositedSizeInCssPixels());
+ baseRect.Inflate(aDisplayportMargins /
+ aFrameMetrics.DisplayportPixelsPerCSSPixel());
+ return baseRect;
+}
+
+void AsyncPanZoomController::RequestContentRepaint(
+ const ParentLayerPoint& aVelocity, const ScreenMargin& aDisplayportMargins,
+ RepaintUpdateType aUpdateType) {
+ mRecursiveMutex.AssertCurrentThreadIn();
+
+ RefPtr<GeckoContentController> controller = GetGeckoContentController();
+ if (!controller) {
+ return;
+ }
+ MOZ_ASSERT(controller->IsRepaintThread());
+
+ APZScrollAnimationType animationType = APZScrollAnimationType::No;
+ if (mAnimation) {
+ animationType = mAnimation->WasTriggeredByScript()
+ ? APZScrollAnimationType::TriggeredByScript
+ : APZScrollAnimationType::TriggeredByUserInput;
+ }
+ RepaintRequest request(Metrics(), aDisplayportMargins, aUpdateType,
+ animationType, mScrollGeneration, mLastSnapTargetIds,
+ IsInScrollingGesture());
+
+ if (request.IsRootContent() && request.GetZoom() != mLastNotifiedZoom &&
+ mState != PINCHING && mState != ANIMATING_ZOOM) {
+ controller->NotifyScaleGestureComplete(
+ GetGuid(),
+ (request.GetZoom() / request.GetDevPixelsPerCSSPixel()).scale);
+ mLastNotifiedZoom = request.GetZoom();
+ }
+
+ // If we're trying to paint what we already think is painted, discard this
+ // request since it's a pointless paint.
+ if (request.GetDisplayPortMargins().WithinEpsilonOf(
+ mLastPaintRequestMetrics.GetDisplayPortMargins(), EPSILON) &&
+ request.GetVisualScrollOffset().WithinEpsilonOf(
+ mLastPaintRequestMetrics.GetVisualScrollOffset(), EPSILON) &&
+ request.GetPresShellResolution() ==
+ mLastPaintRequestMetrics.GetPresShellResolution() &&
+ request.GetZoom() == mLastPaintRequestMetrics.GetZoom() &&
+ request.GetLayoutViewport().WithinEpsilonOf(
+ mLastPaintRequestMetrics.GetLayoutViewport(), EPSILON) &&
+ request.GetScrollGeneration() ==
+ mLastPaintRequestMetrics.GetScrollGeneration() &&
+ request.GetScrollUpdateType() ==
+ mLastPaintRequestMetrics.GetScrollUpdateType() &&
+ request.GetScrollAnimationType() ==
+ mLastPaintRequestMetrics.GetScrollAnimationType() &&
+ request.GetLastSnapTargetIds() ==
+ mLastPaintRequestMetrics.GetLastSnapTargetIds()) {
+ return;
+ }
+
+ APZC_LOGV("%p requesting content repaint %s", this,
+ ToString(request).c_str());
+ { // scope lock
+ MutexAutoLock lock(mCheckerboardEventLock);
+ if (mCheckerboardEvent && mCheckerboardEvent->IsRecordingTrace()) {
+ std::stringstream info;
+ info << " velocity " << aVelocity;
+ std::string str = info.str();
+ mCheckerboardEvent->UpdateRendertraceProperty(
+ CheckerboardEvent::RequestedDisplayPort,
+ GetDisplayPortRect(Metrics(), aDisplayportMargins), str);
+ }
+ }
+
+ controller->RequestContentRepaint(request);
+ mExpectedGeckoMetrics.UpdateFrom(Metrics());
+ mLastPaintRequestMetrics = request;
+
+ // We're holding the APZC lock here, so redispatch this so we can get
+ // the tree lock without the APZC lock.
+ controller->DispatchToRepaintThread(
+ NewRunnableMethod<AsyncPanZoomController*>(
+ "layers::APZCTreeManager::SendSubtreeTransformsToChromeMainThread",
+ GetApzcTreeManager(),
+ &APZCTreeManager::SendSubtreeTransformsToChromeMainThread, this));
+}
+
+bool AsyncPanZoomController::UpdateAnimation(
+ const RecursiveMutexAutoLock& aProofOfLock, const SampleTime& aSampleTime,
+ nsTArray<RefPtr<Runnable>>* aOutDeferredTasks) {
+ AssertOnSamplerThread();
+
+ // This function may get called multiple with the same sample time, if we
+ // composite multiple times at the same timestamp.
+ // However we only want to do one animation step per composition so we need
+ // to deduplicate these calls first.
+ if (mLastSampleTime == aSampleTime) {
+ return !!mAnimation;
+ }
+
+ // We're at a new timestamp, so advance to the next sample in the deque, if
+ // there is one. That one will be used for all the code that reads the
+ // eForCompositing transforms in this vsync interval.
+ AdvanceToNextSample();
+
+ // And then create a new sample, which will be used in the *next* vsync
+ // interval. We do the sample at this point and not later in order to try
+ // and enforce one frame delay between computing the async transform and
+ // compositing it to the screen. This one-frame delay gives code running on
+ // the main thread a chance to try and respond to the scroll position change,
+ // so that e.g. a main-thread animation can stay in sync with user-driven
+ // scrolling or a compositor animation.
+ bool needComposite = SampleCompositedAsyncTransform(aProofOfLock);
+
+ TimeDuration sampleTimeDelta = aSampleTime - mLastSampleTime;
+ mLastSampleTime = aSampleTime;
+
+ if (needComposite || mAnimation) {
+ // Bump the scroll generation before we call RequestContentRepaint below
+ // so that the RequestContentRepaint call will surely use the new
+ // generation.
+ if (APZCTreeManager* treeManagerLocal = GetApzcTreeManager()) {
+ mScrollGeneration = treeManagerLocal->NewAPZScrollGeneration();
+ }
+ }
+
+ if (mAnimation) {
+ bool continueAnimation = mAnimation->Sample(Metrics(), sampleTimeDelta);
+ bool wantsRepaints = mAnimation->WantsRepaints();
+ *aOutDeferredTasks = mAnimation->TakeDeferredTasks();
+ if (!continueAnimation) {
+ SetState(NOTHING);
+ if (mAnimation->AsSmoothMsdScrollAnimation()) {
+ {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ mLastSnapTargetIds =
+ mAnimation->AsSmoothMsdScrollAnimation()->TakeSnapTargetIds();
+ }
+ }
+ mAnimation = nullptr;
+ }
+ // Request a repaint at the end of the animation in case something such as a
+ // call to NotifyLayersUpdated was invoked during the animation and Gecko's
+ // current state is some intermediate point of the animation.
+ if (!continueAnimation || wantsRepaints) {
+ RequestContentRepaint();
+ }
+ needComposite = true;
+ }
+ return needComposite;
+}
+
+AsyncTransformComponentMatrix AsyncPanZoomController::GetOverscrollTransform(
+ AsyncTransformConsumer aMode) const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ AutoApplyAsyncTestAttributes testAttributeApplier(this, lock);
+
+ if (aMode == eForCompositing && mScrollMetadata.IsApzForceDisabled()) {
+ return AsyncTransformComponentMatrix();
+ }
+
+ if (!IsPhysicallyOverscrolled()) {
+ return AsyncTransformComponentMatrix();
+ }
+
+ // The overscroll effect is a simple translation by the overscroll offset.
+ ParentLayerPoint overscrollOffset(-mX.GetOverscroll(), -mY.GetOverscroll());
+ return AsyncTransformComponentMatrix().PostTranslate(overscrollOffset.x,
+ overscrollOffset.y, 0);
+}
+
+bool AsyncPanZoomController::AdvanceAnimations(const SampleTime& aSampleTime) {
+ AssertOnSamplerThread();
+
+ // Don't send any state-change notifications until the end of the function,
+ // because we may go through some intermediate states while we finish
+ // animations and start new ones.
+ StateChangeNotificationBlocker blocker(this);
+
+ // The eventual return value of this function. The compositor needs to know
+ // whether or not to advance by a frame as soon as it can. For example, if a
+ // fling is happening, it has to keep compositing so that the animation is
+ // smooth. If an animation frame is requested, it is the compositor's
+ // responsibility to schedule a composite.
+ bool requestAnimationFrame = false;
+ nsTArray<RefPtr<Runnable>> deferredTasks;
+
+ {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ { // scope lock
+ CSSRect visibleRect = GetVisibleRect(lock);
+ MutexAutoLock lock2(mCheckerboardEventLock);
+ // Update RendertraceProperty before UpdateAnimation() call, since
+ // the UpdateAnimation() updates effective ScrollOffset for next frame
+ // if APZFrameDelay is enabled.
+ if (mCheckerboardEvent) {
+ mCheckerboardEvent->UpdateRendertraceProperty(
+ CheckerboardEvent::UserVisible, visibleRect);
+ }
+ }
+
+ requestAnimationFrame = UpdateAnimation(lock, aSampleTime, &deferredTasks);
+ }
+ // Execute any deferred tasks queued up by mAnimation's Sample() (called by
+ // UpdateAnimation()). This needs to be done after the monitor is released
+ // since the tasks are allowed to call APZCTreeManager methods which can grab
+ // the tree lock.
+ for (uint32_t i = 0; i < deferredTasks.Length(); ++i) {
+ APZThreadUtils::RunOnControllerThread(std::move(deferredTasks[i]));
+ }
+
+ // If any of the deferred tasks starts a new animation, it will request a
+ // new composite directly, so we can just return requestAnimationFrame here.
+ return requestAnimationFrame;
+}
+
+ParentLayerPoint AsyncPanZoomController::GetCurrentAsyncScrollOffset(
+ AsyncTransformConsumer aMode) const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ AutoApplyAsyncTestAttributes testAttributeApplier(this, lock);
+
+ return GetEffectiveScrollOffset(aMode, lock) * GetEffectiveZoom(aMode, lock);
+}
+
+CSSRect AsyncPanZoomController::GetCurrentAsyncVisualViewport(
+ AsyncTransformConsumer aMode) const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ AutoApplyAsyncTestAttributes testAttributeApplier(this, lock);
+
+ return CSSRect(
+ GetEffectiveScrollOffset(aMode, lock),
+ FrameMetrics::CalculateCompositedSizeInCssPixels(
+ Metrics().GetCompositionBounds(), GetEffectiveZoom(aMode, lock)));
+}
+
+AsyncTransform AsyncPanZoomController::GetCurrentAsyncTransform(
+ AsyncTransformConsumer aMode, AsyncTransformComponents aComponents,
+ std::size_t aSampleIndex) const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ AutoApplyAsyncTestAttributes testAttributeApplier(this, lock);
+
+ CSSToParentLayerScale effectiveZoom;
+ if (aComponents.contains(AsyncTransformComponent::eVisual)) {
+ effectiveZoom = GetEffectiveZoom(aMode, lock, aSampleIndex);
+ } else {
+ effectiveZoom =
+ Metrics().LayersPixelsPerCSSPixel() * LayerToParentLayerScale(1.0f);
+ }
+
+ LayerToParentLayerScale compositedAsyncZoom =
+ effectiveZoom / Metrics().LayersPixelsPerCSSPixel();
+
+ ParentLayerPoint translation;
+ if (aComponents.contains(AsyncTransformComponent::eVisual)) {
+ // There is no "lastPaintVisualOffset" to subtract here; the visual offset
+ // is entirely async.
+
+ CSSPoint currentVisualOffset =
+ GetEffectiveScrollOffset(aMode, lock, aSampleIndex) -
+ GetEffectiveLayoutViewport(aMode, lock, aSampleIndex).TopLeft();
+
+ translation += currentVisualOffset * effectiveZoom;
+ }
+ if (aComponents.contains(AsyncTransformComponent::eLayout)) {
+ CSSPoint lastPaintLayoutOffset;
+ if (mLastContentPaintMetrics.IsScrollable()) {
+ lastPaintLayoutOffset = mLastContentPaintMetrics.GetLayoutScrollOffset();
+ }
+
+ CSSPoint currentLayoutOffset =
+ GetEffectiveLayoutViewport(aMode, lock, aSampleIndex).TopLeft();
+
+ translation +=
+ (currentLayoutOffset - lastPaintLayoutOffset) * effectiveZoom;
+ }
+
+ return AsyncTransform(compositedAsyncZoom, -translation);
+}
+
+AsyncTransformComponentMatrix
+AsyncPanZoomController::GetCurrentAsyncTransformWithOverscroll(
+ AsyncTransformConsumer aMode, AsyncTransformComponents aComponents,
+ std::size_t aSampleIndex) const {
+ AsyncTransformComponentMatrix asyncTransform =
+ GetCurrentAsyncTransform(aMode, aComponents, aSampleIndex);
+ // The overscroll transform is considered part of the layout component of
+ // the async transform, because it should not apply to fixed content.
+ if (aComponents.contains(AsyncTransformComponent::eLayout)) {
+ return asyncTransform * GetOverscrollTransform(aMode);
+ }
+ return asyncTransform;
+}
+
+LayoutDeviceToParentLayerScale AsyncPanZoomController::GetCurrentPinchZoomScale(
+ AsyncTransformConsumer aMode) const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ AutoApplyAsyncTestAttributes testAttributeApplier(this, lock);
+ CSSToParentLayerScale scale = GetEffectiveZoom(aMode, lock);
+ return scale / Metrics().GetDevPixelsPerCSSPixel();
+}
+
+AutoTArray<wr::SampledScrollOffset, 2>
+AsyncPanZoomController::GetSampledScrollOffsets() const {
+ AssertOnSamplerThread();
+
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+
+ const AsyncTransformComponents asyncTransformComponents =
+ GetZoomAnimationId()
+ ? AsyncTransformComponents{AsyncTransformComponent::eLayout}
+ : LayoutAndVisual;
+
+ // If layerTranslation includes only the layout component of the async
+ // transform then it has not been scaled by the async zoom, so we want to
+ // divide it by the resolution. If layerTranslation includes the visual
+ // component, then we should use the pinch zoom scale, which includes the
+ // async zoom. However, we only use LayoutAndVisual for non-zoomable APZCs,
+ // so it makes no difference.
+ LayoutDeviceToParentLayerScale resolution =
+ GetCumulativeResolution() * LayerToParentLayerScale(1.0f);
+
+ AutoTArray<wr::SampledScrollOffset, 2> sampledOffsets;
+
+ for (std::deque<SampledAPZCState>::size_type index = 0;
+ index < mSampledState.size(); index++) {
+ ParentLayerPoint layerTranslation =
+ GetCurrentAsyncTransform(AsyncPanZoomController::eForCompositing,
+ asyncTransformComponents, index)
+ .mTranslation;
+
+ // Include the overscroll transform here in scroll offsets transform
+ // to ensure that we do not overscroll fixed content.
+ layerTranslation =
+ GetOverscrollTransform(AsyncPanZoomController::eForCompositing)
+ .TransformPoint(layerTranslation);
+ // The positive translation means the painted content is supposed to
+ // move down (or to the right), and that corresponds to a reduction in
+ // the scroll offset. Since we are effectively giving WR the async
+ // scroll delta here, we want to negate the translation.
+ LayoutDevicePoint asyncScrollDelta = -layerTranslation / resolution;
+ sampledOffsets.AppendElement(wr::SampledScrollOffset{
+ wr::ToLayoutVector2D(asyncScrollDelta),
+ wr::ToWrAPZScrollGeneration(mSampledState[index].Generation())});
+ }
+
+ return sampledOffsets;
+}
+
+bool AsyncPanZoomController::SuppressAsyncScrollOffset() const {
+ return mScrollMetadata.IsApzForceDisabled() ||
+ (Metrics().IsMinimalDisplayPort() &&
+ StaticPrefs::apz_prefer_jank_minimal_displayports());
+}
+
+CSSRect AsyncPanZoomController::GetEffectiveLayoutViewport(
+ AsyncTransformConsumer aMode, const RecursiveMutexAutoLock& aProofOfLock,
+ std::size_t aSampleIndex) const {
+ if (aMode == eForCompositing && SuppressAsyncScrollOffset()) {
+ return mLastContentPaintMetrics.GetLayoutViewport();
+ }
+ if (aMode == eForCompositing) {
+ return mSampledState[aSampleIndex].GetLayoutViewport();
+ }
+ return Metrics().GetLayoutViewport();
+}
+
+CSSPoint AsyncPanZoomController::GetEffectiveScrollOffset(
+ AsyncTransformConsumer aMode, const RecursiveMutexAutoLock& aProofOfLock,
+ std::size_t aSampleIndex) const {
+ if (aMode == eForCompositing && SuppressAsyncScrollOffset()) {
+ return mLastContentPaintMetrics.GetVisualScrollOffset();
+ }
+ if (aMode == eForCompositing) {
+ return mSampledState[aSampleIndex].GetVisualScrollOffset();
+ }
+ return Metrics().GetVisualScrollOffset();
+}
+
+CSSToParentLayerScale AsyncPanZoomController::GetEffectiveZoom(
+ AsyncTransformConsumer aMode, const RecursiveMutexAutoLock& aProofOfLock,
+ std::size_t aSampleIndex) const {
+ if (aMode == eForCompositing && SuppressAsyncScrollOffset()) {
+ return mLastContentPaintMetrics.GetZoom();
+ }
+ if (aMode == eForCompositing) {
+ return mSampledState[aSampleIndex].GetZoom();
+ }
+ return Metrics().GetZoom();
+}
+
+void AsyncPanZoomController::AdvanceToNextSample() {
+ AssertOnSamplerThread();
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ // Always keep at least one state in mSampledState.
+ if (mSampledState.size() > 1) {
+ mSampledState.pop_front();
+ }
+}
+
+bool AsyncPanZoomController::SampleCompositedAsyncTransform(
+ const RecursiveMutexAutoLock& aProofOfLock) {
+ MOZ_ASSERT(mSampledState.size() <= 2);
+ bool sampleChanged = (mSampledState.back() != SampledAPZCState(Metrics()));
+ mSampledState.emplace_back(Metrics(), std::move(mScrollPayload),
+ mScrollGeneration);
+ return sampleChanged;
+}
+
+void AsyncPanZoomController::ResampleCompositedAsyncTransform(
+ const RecursiveMutexAutoLock& aProofOfLock) {
+ // This only gets called during testing situations, so the fact that this
+ // drops the scroll payload from mSampledState.front() is not really a
+ // problem.
+ if (APZCTreeManager* treeManagerLocal = GetApzcTreeManager()) {
+ mScrollGeneration = treeManagerLocal->NewAPZScrollGeneration();
+ }
+ mSampledState.front() = SampledAPZCState(Metrics(), {}, mScrollGeneration);
+}
+
+void AsyncPanZoomController::ApplyAsyncTestAttributes(
+ const RecursiveMutexAutoLock& aProofOfLock) {
+ if (mTestAttributeAppliers == 0) {
+ if (mTestAsyncScrollOffset != CSSPoint() ||
+ mTestAsyncZoom != LayerToParentLayerScale()) {
+ // TODO Currently we update Metrics() and resample, which will cause
+ // the very latest user input to get immediately captured in the sample,
+ // and may defeat our attempt at "frame delay" (i.e. delaying the user
+ // input from affecting composition by one frame).
+ // Instead, maybe we should just apply the mTest* stuff directly to
+ // mSampledState.front(). We can even save/restore that SampledAPZCState
+ // instance in the AutoApplyAsyncTestAttributes instead of Metrics().
+ Metrics().ZoomBy(mTestAsyncZoom.scale);
+ CSSPoint asyncScrollPosition = Metrics().GetVisualScrollOffset();
+ CSSPoint requestedPoint =
+ asyncScrollPosition + this->mTestAsyncScrollOffset;
+ CSSPoint clampedPoint =
+ Metrics().CalculateScrollRange().ClampPoint(requestedPoint);
+ CSSPoint difference = mTestAsyncScrollOffset - clampedPoint;
+
+ ScrollByAndClamp(mTestAsyncScrollOffset);
+
+ if (StaticPrefs::apz_overscroll_test_async_scroll_offset_enabled()) {
+ ParentLayerPoint overscroll = difference * Metrics().GetZoom();
+ OverscrollBy(overscroll);
+ }
+ ResampleCompositedAsyncTransform(aProofOfLock);
+ }
+ }
+ ++mTestAttributeAppliers;
+}
+
+void AsyncPanZoomController::UnapplyAsyncTestAttributes(
+ const RecursiveMutexAutoLock& aProofOfLock,
+ const FrameMetrics& aPrevFrameMetrics,
+ const ParentLayerPoint& aPrevOverscroll) {
+ MOZ_ASSERT(mTestAttributeAppliers >= 1);
+ --mTestAttributeAppliers;
+ if (mTestAttributeAppliers == 0) {
+ if (mTestAsyncScrollOffset != CSSPoint() ||
+ mTestAsyncZoom != LayerToParentLayerScale()) {
+ Metrics() = aPrevFrameMetrics;
+ RestoreOverscrollAmount(aPrevOverscroll);
+ ResampleCompositedAsyncTransform(aProofOfLock);
+ }
+ }
+}
+
+Matrix4x4 AsyncPanZoomController::GetTransformToLastDispatchedPaint(
+ const AsyncTransformComponents& aComponents) const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ CSSPoint componentOffset;
+
+ // The computation of the componentOffset should roughly be the negation
+ // of the translation in GetCurrentAsyncTransform() with the expected
+ // gecko metrics substituted for the effective scroll offsets.
+ if (aComponents.contains(AsyncTransformComponent::eVisual)) {
+ componentOffset += mExpectedGeckoMetrics.GetLayoutScrollOffset() -
+ mExpectedGeckoMetrics.GetVisualScrollOffset();
+ }
+
+ if (aComponents.contains(AsyncTransformComponent::eLayout)) {
+ CSSPoint lastPaintLayoutOffset;
+
+ if (mLastContentPaintMetrics.IsScrollable()) {
+ lastPaintLayoutOffset = mLastContentPaintMetrics.GetLayoutScrollOffset();
+ }
+
+ componentOffset +=
+ lastPaintLayoutOffset - mExpectedGeckoMetrics.GetLayoutScrollOffset();
+ }
+
+ LayerPoint scrollChange = componentOffset *
+ mLastContentPaintMetrics.GetDevPixelsPerCSSPixel() *
+ mLastContentPaintMetrics.GetCumulativeResolution();
+
+ // We're interested in the async zoom change. Factor out the content scale
+ // that may change when dragging the window to a monitor with a different
+ // content scale.
+ LayoutDeviceToParentLayerScale lastContentZoom =
+ mLastContentPaintMetrics.GetZoom() /
+ mLastContentPaintMetrics.GetDevPixelsPerCSSPixel();
+ LayoutDeviceToParentLayerScale lastDispatchedZoom =
+ mExpectedGeckoMetrics.GetZoom() /
+ mExpectedGeckoMetrics.GetDevPixelsPerCSSPixel();
+ float zoomChange = 1.0;
+ if (aComponents.contains(AsyncTransformComponent::eVisual) &&
+ lastDispatchedZoom != LayoutDeviceToParentLayerScale(0)) {
+ zoomChange = lastContentZoom.scale / lastDispatchedZoom.scale;
+ }
+ return Matrix4x4::Translation(scrollChange.x, scrollChange.y, 0)
+ .PostScale(zoomChange, zoomChange, 1);
+}
+
+CSSRect AsyncPanZoomController::GetVisibleRect(
+ const RecursiveMutexAutoLock& aProofOfLock) const {
+ AutoApplyAsyncTestAttributes testAttributeApplier(this, aProofOfLock);
+ CSSPoint currentScrollOffset = GetEffectiveScrollOffset(
+ AsyncPanZoomController::eForCompositing, aProofOfLock);
+ CSSRect visible = CSSRect(currentScrollOffset,
+ Metrics().CalculateCompositedSizeInCssPixels());
+ return visible;
+}
+
+static CSSRect GetPaintedRect(const FrameMetrics& aFrameMetrics) {
+ CSSRect displayPort = aFrameMetrics.GetDisplayPort();
+ if (displayPort.IsEmpty()) {
+ // Fallback to use the viewport if the diplayport hasn't been set.
+ // This situation often happens non-scrollable iframe's root scroller in
+ // Fission.
+ return aFrameMetrics.GetVisualViewport();
+ }
+
+ return displayPort + aFrameMetrics.GetLayoutScrollOffset();
+}
+
+uint32_t AsyncPanZoomController::GetCheckerboardMagnitude(
+ const ParentLayerRect& aClippedCompositionBounds) const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+
+ CSSRect painted = GetPaintedRect(mLastContentPaintMetrics);
+ painted.Inflate(CSSMargin::FromAppUnits(
+ nsMargin(1, 1, 1, 1))); // fuzz for rounding error
+
+ CSSRect visible = GetVisibleRect(lock); // relative to scrolled frame origin
+ if (visible.IsEmpty() || painted.Contains(visible)) {
+ // early-exit if we're definitely not checkerboarding
+ return 0;
+ }
+
+ // aClippedCompositionBounds and Metrics().GetCompositionBounds() are both
+ // relative to the layer tree origin.
+ // The "*RelativeToItself*" variables are relative to the comp bounds origin
+ ParentLayerRect visiblePartOfCompBoundsRelativeToItself =
+ aClippedCompositionBounds - Metrics().GetCompositionBounds().TopLeft();
+ CSSRect visiblePartOfCompBoundsRelativeToItselfInCssSpace;
+ if (Metrics().GetZoom() != CSSToParentLayerScale(0)) {
+ visiblePartOfCompBoundsRelativeToItselfInCssSpace =
+ (visiblePartOfCompBoundsRelativeToItself / Metrics().GetZoom());
+ }
+
+ // This one is relative to the scrolled frame origin, same as `visible`
+ CSSRect visiblePartOfCompBoundsInCssSpace =
+ visiblePartOfCompBoundsRelativeToItselfInCssSpace + visible.TopLeft();
+
+ visible = visible.Intersect(visiblePartOfCompBoundsInCssSpace);
+
+ CSSIntRegion checkerboard;
+ // Round so as to minimize checkerboarding; if we're only showing fractional
+ // pixels of checkerboarding it's not really worth counting
+ checkerboard.Sub(RoundedIn(visible), RoundedOut(painted));
+ uint32_t area = checkerboard.Area();
+ if (area) {
+ APZC_LOG_FM(Metrics(),
+ "%p is currently checkerboarding (painted %s visible %s)", this,
+ ToString(painted).c_str(), ToString(visible).c_str());
+ }
+ return area;
+}
+
+void AsyncPanZoomController::ReportCheckerboard(
+ const SampleTime& aSampleTime,
+ const ParentLayerRect& aClippedCompositionBounds) {
+ if (mLastCheckerboardReport == aSampleTime) {
+ // This function will get called multiple times for each APZC on a single
+ // composite (once for each layer it is attached to). Only report the
+ // checkerboard once per composite though.
+ return;
+ }
+ mLastCheckerboardReport = aSampleTime;
+
+ bool recordTrace = StaticPrefs::apz_record_checkerboarding();
+ bool forTelemetry = Telemetry::CanRecordBase();
+ uint32_t magnitude = GetCheckerboardMagnitude(aClippedCompositionBounds);
+
+ // IsInTransformingState() acquires the APZC lock and thus needs to
+ // be called before acquiring mCheckerboardEventLock.
+ bool inTransformingState = IsInTransformingState();
+
+ MutexAutoLock lock(mCheckerboardEventLock);
+ if (!mCheckerboardEvent && (recordTrace || forTelemetry)) {
+ mCheckerboardEvent = MakeUnique<CheckerboardEvent>(recordTrace);
+ }
+ mPotentialCheckerboardTracker.InTransform(inTransformingState,
+ recordTrace || forTelemetry);
+ if (magnitude) {
+ mPotentialCheckerboardTracker.CheckerboardSeen();
+ }
+ UpdateCheckerboardEvent(lock, magnitude);
+}
+
+void AsyncPanZoomController::UpdateCheckerboardEvent(
+ const MutexAutoLock& aProofOfLock, uint32_t aMagnitude) {
+ if (mCheckerboardEvent && mCheckerboardEvent->RecordFrameInfo(aMagnitude)) {
+ // This checkerboard event is done. Report some metrics to telemetry.
+ mozilla::Telemetry::Accumulate(mozilla::Telemetry::CHECKERBOARD_SEVERITY,
+ mCheckerboardEvent->GetSeverity());
+ mozilla::Telemetry::Accumulate(mozilla::Telemetry::CHECKERBOARD_PEAK,
+ mCheckerboardEvent->GetPeak());
+ mozilla::Telemetry::Accumulate(
+ mozilla::Telemetry::CHECKERBOARD_DURATION,
+ (uint32_t)mCheckerboardEvent->GetDuration().ToMilliseconds());
+
+ // mCheckerboardEvent only gets created if we are supposed to record
+ // telemetry so we always pass true for aRecordTelemetry.
+ mPotentialCheckerboardTracker.CheckerboardDone(
+ /* aRecordTelemetry = */ true);
+
+ if (StaticPrefs::apz_record_checkerboarding()) {
+ // if the pref is enabled, also send it to the storage class. it may be
+ // chosen for public display on about:checkerboard, the hall of fame for
+ // checkerboard events.
+ uint32_t severity = mCheckerboardEvent->GetSeverity();
+ std::string log = mCheckerboardEvent->GetLog();
+ CheckerboardEventStorage::Report(severity, log);
+ }
+ mCheckerboardEvent = nullptr;
+ }
+}
+
+void AsyncPanZoomController::FlushActiveCheckerboardReport() {
+ MutexAutoLock lock(mCheckerboardEventLock);
+ // Pretend like we got a frame with 0 pixels checkerboarded. This will
+ // terminate the checkerboard event and flush it out
+ UpdateCheckerboardEvent(lock, 0);
+}
+
+void AsyncPanZoomController::NotifyLayersUpdated(
+ const ScrollMetadata& aScrollMetadata, bool aIsFirstPaint,
+ bool aThisLayerTreeUpdated) {
+ AssertOnUpdaterThread();
+
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ bool isDefault = mScrollMetadata.IsDefault();
+
+ const FrameMetrics& aLayerMetrics = aScrollMetadata.GetMetrics();
+
+ if ((aScrollMetadata == mLastContentPaintMetadata) && !isDefault) {
+ // No new information here, skip it.
+ APZC_LOGV("%p NotifyLayersUpdated short-circuit\n", this);
+ return;
+ }
+
+ // If the Metrics scroll offset is different from the last scroll offset
+ // that the main-thread sent us, then we know that the user has been doing
+ // something that triggers a scroll. This check is the APZ equivalent of the
+ // check on the main-thread at
+ // https://hg.mozilla.org/mozilla-central/file/97a52326b06a/layout/generic/nsGfxScrollFrame.cpp#l4050
+ // There is code below (the use site of userScrolled) that prevents a
+ // restored- scroll-position update from overwriting a user scroll, again
+ // equivalent to how the main thread code does the same thing.
+ // XXX Suspicious comparison between layout and visual scroll offsets.
+ // This may not do the right thing when we're zoomed in.
+ CSSPoint lastScrollOffset = mLastContentPaintMetrics.GetLayoutScrollOffset();
+ bool userScrolled = !FuzzyEqualsAdditive(Metrics().GetVisualScrollOffset().x,
+ lastScrollOffset.x) ||
+ !FuzzyEqualsAdditive(Metrics().GetVisualScrollOffset().y,
+ lastScrollOffset.y);
+
+ if (aScrollMetadata.DidContentGetPainted()) {
+ mLastContentPaintMetadata = aScrollMetadata;
+ }
+
+ mScrollMetadata.SetScrollParentId(aScrollMetadata.GetScrollParentId());
+ APZC_LOGV_FM(aLayerMetrics,
+ "%p got a NotifyLayersUpdated with aIsFirstPaint=%d, "
+ "aThisLayerTreeUpdated=%d",
+ this, aIsFirstPaint, aThisLayerTreeUpdated);
+
+ { // scope lock
+ MutexAutoLock lock(mCheckerboardEventLock);
+ if (mCheckerboardEvent && mCheckerboardEvent->IsRecordingTrace()) {
+ std::string str;
+ if (aThisLayerTreeUpdated) {
+ if (!aLayerMetrics.GetPaintRequestTime().IsNull()) {
+ // Note that we might get the paint request time as non-null, but with
+ // aThisLayerTreeUpdated false. That can happen if we get a layer
+ // transaction from a different process right after we get the layer
+ // transaction with aThisLayerTreeUpdated == true. In this case we
+ // want to ignore the paint request time because it was already dumped
+ // in the previous layer transaction.
+ TimeDuration paintTime =
+ TimeStamp::Now() - aLayerMetrics.GetPaintRequestTime();
+ std::stringstream info;
+ info << " painttime " << paintTime.ToMilliseconds();
+ str = info.str();
+ } else {
+ // This might be indicative of a wasted paint particularly if it
+ // happens during a checkerboard event.
+ str = " (this layertree updated)";
+ }
+ }
+ mCheckerboardEvent->UpdateRendertraceProperty(
+ CheckerboardEvent::Page, aLayerMetrics.GetScrollableRect());
+ mCheckerboardEvent->UpdateRendertraceProperty(
+ CheckerboardEvent::PaintedDisplayPort, GetPaintedRect(aLayerMetrics),
+ str);
+ }
+ }
+
+ // The main thread may send us a visual scroll offset update. This is
+ // different from a layout viewport offset update in that the layout viewport
+ // offset is limited to the layout scroll range, while the visual viewport
+ // offset is not.
+ // However, there are some conditions in which the layout update will clobber
+ // the visual update, and we want to ignore the visual update in those cases.
+ // This variable tracks that.
+ bool ignoreVisualUpdate = false;
+
+ // TODO if we're in a drag and scrollOffsetUpdated is set then we want to
+ // ignore it
+
+ bool needContentRepaint = false;
+ RepaintUpdateType contentRepaintType = RepaintUpdateType::eNone;
+ bool viewportSizeUpdated = false;
+ bool needToReclampScroll = false;
+
+ if ((aIsFirstPaint && aThisLayerTreeUpdated) || isDefault ||
+ Metrics().IsRootContent() != aLayerMetrics.IsRootContent()) {
+ if (Metrics().IsRootContent() && !aLayerMetrics.IsRootContent()) {
+ // We only support zooming on root content APZCs
+ SetZoomAnimationId(Nothing());
+ }
+
+ // Initialize our internal state to something sane when the content
+ // that was just painted is something we knew nothing about previously
+ CancelAnimation();
+
+ // Keep our existing scroll generation, if there are scroll updates. In this
+ // case we'll update our scroll generation when processing the scroll update
+ // array below. If there are no scroll updates, take the generation from the
+ // incoming metrics. Bug 1662019 will simplify this later.
+ ScrollGeneration oldScrollGeneration = Metrics().GetScrollGeneration();
+ mScrollMetadata = aScrollMetadata;
+ if (!aScrollMetadata.GetScrollUpdates().IsEmpty()) {
+ Metrics().SetScrollGeneration(oldScrollGeneration);
+ }
+
+ mExpectedGeckoMetrics.UpdateFrom(aLayerMetrics);
+
+ for (auto& sampledState : mSampledState) {
+ sampledState.UpdateScrollProperties(Metrics());
+ sampledState.UpdateZoomProperties(Metrics());
+ }
+
+ if (aLayerMetrics.HasNonZeroDisplayPortMargins()) {
+ // A non-zero display port margin here indicates a displayport has
+ // been set by a previous APZC for the content at this guid. The
+ // scrollable rect may have changed since then, making the margins
+ // wrong, so we need to calculate a new display port.
+ // It is important that we request a repaint here only when we need to
+ // otherwise we will end up setting a display port on every frame that
+ // gets a view id.
+ APZC_LOG("%p detected non-empty margins which probably need updating\n",
+ this);
+ needContentRepaint = true;
+ }
+ } else {
+ // If we're not taking the aLayerMetrics wholesale we still need to pull
+ // in some things into our local Metrics() because these things are
+ // determined by Gecko and our copy in Metrics() may be stale.
+
+ if (Metrics().GetLayoutViewport().Size() !=
+ aLayerMetrics.GetLayoutViewport().Size()) {
+ CSSRect layoutViewport = Metrics().GetLayoutViewport();
+ // The offset will be updated if necessary via
+ // RecalculateLayoutViewportOffset().
+ layoutViewport.SizeTo(aLayerMetrics.GetLayoutViewport().Size());
+ Metrics().SetLayoutViewport(layoutViewport);
+
+ needContentRepaint = true;
+ viewportSizeUpdated = true;
+ }
+
+ // TODO: Rely entirely on |aScrollMetadata.IsResolutionUpdated()| to
+ // determine which branch to take, and drop the other conditions.
+ CSSToParentLayerScale oldZoom = Metrics().GetZoom();
+ if (FuzzyEqualsAdditive(
+ Metrics().GetCompositionBoundsWidthIgnoringScrollbars(),
+ aLayerMetrics.GetCompositionBoundsWidthIgnoringScrollbars()) &&
+ Metrics().GetDevPixelsPerCSSPixel() ==
+ aLayerMetrics.GetDevPixelsPerCSSPixel() &&
+ !viewportSizeUpdated && !aScrollMetadata.IsResolutionUpdated()) {
+ // Any change to the pres shell resolution was requested by APZ and is
+ // already included in our zoom; however, other components of the
+ // cumulative resolution (a parent document's pres-shell resolution, or
+ // the css-driven resolution) may have changed, and we need to update
+ // our zoom to reflect that. Note that we can't just take
+ // aLayerMetrics.mZoom because the APZ may have additional async zoom
+ // since the repaint request.
+ float totalResolutionChange = 1.0;
+
+ if (Metrics().GetCumulativeResolution() != LayoutDeviceToLayerScale(0)) {
+ totalResolutionChange = aLayerMetrics.GetCumulativeResolution().scale /
+ Metrics().GetCumulativeResolution().scale;
+ }
+
+ float presShellResolutionChange = aLayerMetrics.GetPresShellResolution() /
+ Metrics().GetPresShellResolution();
+ if (presShellResolutionChange != 1.0f) {
+ needContentRepaint = true;
+ }
+ Metrics().ZoomBy(totalResolutionChange / presShellResolutionChange);
+ for (auto& sampledState : mSampledState) {
+ sampledState.ZoomBy(totalResolutionChange / presShellResolutionChange);
+ }
+ } else {
+ // Take the new zoom as either device scale or composition width or
+ // viewport size got changed (e.g. due to orientation change, or content
+ // changing the meta-viewport tag), or the main thread originated a
+ // resolution change for another reason (e.g. Ctrl+0 was pressed to
+ // reset the zoom).
+ Metrics().SetZoom(aLayerMetrics.GetZoom());
+ for (auto& sampledState : mSampledState) {
+ sampledState.UpdateZoomProperties(aLayerMetrics);
+ }
+ Metrics().SetDevPixelsPerCSSPixel(
+ aLayerMetrics.GetDevPixelsPerCSSPixel());
+ }
+
+ if (Metrics().GetZoom() != oldZoom) {
+ // If the zoom changed, the scroll range in CSS pixels may have changed
+ // even if the composition bounds didn't.
+ needToReclampScroll = true;
+ }
+
+ mExpectedGeckoMetrics.UpdateZoomFrom(aLayerMetrics);
+
+ if (!Metrics().GetScrollableRect().IsEqualEdges(
+ aLayerMetrics.GetScrollableRect())) {
+ Metrics().SetScrollableRect(aLayerMetrics.GetScrollableRect());
+ needContentRepaint = true;
+ needToReclampScroll = true;
+ }
+ if (!Metrics().GetCompositionBounds().IsEqualEdges(
+ aLayerMetrics.GetCompositionBounds())) {
+ Metrics().SetCompositionBounds(aLayerMetrics.GetCompositionBounds());
+ needToReclampScroll = true;
+ }
+ Metrics().SetCompositionBoundsWidthIgnoringScrollbars(
+ aLayerMetrics.GetCompositionBoundsWidthIgnoringScrollbars());
+
+ if (Metrics().IsRootContent() &&
+ Metrics().GetCompositionSizeWithoutDynamicToolbar() !=
+ aLayerMetrics.GetCompositionSizeWithoutDynamicToolbar()) {
+ Metrics().SetCompositionSizeWithoutDynamicToolbar(
+ aLayerMetrics.GetCompositionSizeWithoutDynamicToolbar());
+ needToReclampScroll = true;
+ }
+ Metrics().SetBoundingCompositionSize(
+ aLayerMetrics.GetBoundingCompositionSize());
+ Metrics().SetPresShellResolution(aLayerMetrics.GetPresShellResolution());
+ Metrics().SetCumulativeResolution(aLayerMetrics.GetCumulativeResolution());
+ Metrics().SetTransformToAncestorScale(
+ aLayerMetrics.GetTransformToAncestorScale());
+ mScrollMetadata.SetHasScrollgrab(aScrollMetadata.GetHasScrollgrab());
+ mScrollMetadata.SetLineScrollAmount(aScrollMetadata.GetLineScrollAmount());
+ mScrollMetadata.SetPageScrollAmount(aScrollMetadata.GetPageScrollAmount());
+ mScrollMetadata.SetSnapInfo(ScrollSnapInfo(aScrollMetadata.GetSnapInfo()));
+ mScrollMetadata.SetIsLayersIdRoot(aScrollMetadata.IsLayersIdRoot());
+ mScrollMetadata.SetIsAutoDirRootContentRTL(
+ aScrollMetadata.IsAutoDirRootContentRTL());
+ Metrics().SetIsScrollInfoLayer(aLayerMetrics.IsScrollInfoLayer());
+ Metrics().SetHasNonZeroDisplayPortMargins(
+ aLayerMetrics.HasNonZeroDisplayPortMargins());
+ Metrics().SetMinimalDisplayPort(aLayerMetrics.IsMinimalDisplayPort());
+ mScrollMetadata.SetForceDisableApz(aScrollMetadata.IsApzForceDisabled());
+ mScrollMetadata.SetIsRDMTouchSimulationActive(
+ aScrollMetadata.GetIsRDMTouchSimulationActive());
+ mScrollMetadata.SetForceMousewheelAutodir(
+ aScrollMetadata.ForceMousewheelAutodir());
+ mScrollMetadata.SetForceMousewheelAutodirHonourRoot(
+ aScrollMetadata.ForceMousewheelAutodirHonourRoot());
+ mScrollMetadata.SetIsPaginatedPresentation(
+ aScrollMetadata.IsPaginatedPresentation());
+ mScrollMetadata.SetDisregardedDirection(
+ aScrollMetadata.GetDisregardedDirection());
+ mScrollMetadata.SetOverscrollBehavior(
+ aScrollMetadata.GetOverscrollBehavior());
+ }
+
+ bool scrollOffsetUpdated = false;
+ bool smoothScrollRequested = false;
+ bool didCancelAnimation = false;
+ Maybe<CSSPoint> cumulativeRelativeDelta;
+ for (const auto& scrollUpdate : aScrollMetadata.GetScrollUpdates()) {
+ APZC_LOG("%p processing scroll update %s\n", this,
+ ToString(scrollUpdate).c_str());
+ if (!(Metrics().GetScrollGeneration() < scrollUpdate.GetGeneration())) {
+ // This is stale, let's ignore it
+ APZC_LOG("%p scrollupdate generation stale, dropping\n", this);
+ continue;
+ }
+ Metrics().SetScrollGeneration(scrollUpdate.GetGeneration());
+
+ MOZ_ASSERT(scrollUpdate.GetOrigin() != ScrollOrigin::Apz);
+ if (userScrolled &&
+ !nsLayoutUtils::CanScrollOriginClobberApz(scrollUpdate.GetOrigin())) {
+ APZC_LOG("%p scrollupdate cannot clobber APZ userScrolled\n", this);
+ continue;
+ }
+ // XXX: if we get here, |scrollUpdate| is clobbering APZ, so we may want
+ // to reset |userScrolled| back to false so that subsequent scrollUpdates
+ // in this loop don't get dropped by the check above. Need to add a test
+ // that exercises this scenario, as we don't currently have one.
+
+ if (scrollUpdate.GetMode() == ScrollMode::Smooth ||
+ scrollUpdate.GetMode() == ScrollMode::SmoothMsd) {
+ smoothScrollRequested = true;
+
+ // Requests to animate the visual scroll position override requests to
+ // simply update the visual scroll offset to a particular point. Since
+ // we have an animation request, we set ignoreVisualUpdate to true to
+ // indicate we don't need to apply the visual scroll update in
+ // aLayerMetrics.
+ ignoreVisualUpdate = true;
+
+ // For relative updates we want to add the relative offset to any existing
+ // destination, or the current visual offset if there is no existing
+ // destination.
+ CSSPoint base = GetCurrentAnimationDestination(lock).valueOr(
+ Metrics().GetVisualScrollOffset());
+
+ CSSPoint destination;
+ if (scrollUpdate.GetType() == ScrollUpdateType::Relative) {
+ CSSPoint delta =
+ scrollUpdate.GetDestination() - scrollUpdate.GetSource();
+ APZC_LOG("%p relative smooth scrolling from %s by %s\n", this,
+ ToString(base).c_str(), ToString(delta).c_str());
+ destination = Metrics().CalculateScrollRange().ClampPoint(base + delta);
+ } else if (scrollUpdate.GetType() == ScrollUpdateType::PureRelative) {
+ CSSPoint delta = scrollUpdate.GetDelta();
+ APZC_LOG("%p pure-relative smooth scrolling from %s by %s\n", this,
+ ToString(base).c_str(), ToString(delta).c_str());
+ destination = Metrics().CalculateScrollRange().ClampPoint(base + delta);
+ } else {
+ APZC_LOG("%p smooth scrolling to %s\n", this,
+ ToString(scrollUpdate.GetDestination()).c_str());
+ destination = scrollUpdate.GetDestination();
+ }
+
+ if (scrollUpdate.GetMode() == ScrollMode::SmoothMsd) {
+ SmoothMsdScrollTo(
+ CSSSnapTarget{destination, scrollUpdate.GetSnapTargetIds()},
+ scrollUpdate.GetScrollTriggeredByScript());
+ } else {
+ MOZ_ASSERT(scrollUpdate.GetMode() == ScrollMode::Smooth);
+ MOZ_ASSERT(!scrollUpdate.WasTriggeredByScript());
+ SmoothScrollTo(destination, scrollUpdate.GetOrigin());
+ }
+ continue;
+ }
+
+ MOZ_ASSERT(scrollUpdate.GetMode() == ScrollMode::Instant ||
+ scrollUpdate.GetMode() == ScrollMode::Normal);
+
+ // If the layout update is of a higher priority than the visual update, then
+ // we don't want to apply the visual update.
+ // If the layout update is of a clobbering type (or a smooth scroll request,
+ // which is handled above) then it takes precedence over an eRestore visual
+ // update. But we also allow the possibility for the main thread to ask us
+ // to scroll both the layout and visual viewports to distinct (but
+ // compatible) locations (via e.g. both updates being of a non-clobbering/
+ // eRestore type).
+ if (nsLayoutUtils::CanScrollOriginClobberApz(scrollUpdate.GetOrigin()) &&
+ aLayerMetrics.GetVisualScrollUpdateType() !=
+ FrameMetrics::eMainThread) {
+ ignoreVisualUpdate = true;
+ }
+
+ Maybe<CSSPoint> relativeDelta;
+ if (scrollUpdate.GetType() == ScrollUpdateType::Relative) {
+ APZC_LOG(
+ "%p relative updating scroll offset from %s by %s\n", this,
+ ToString(Metrics().GetVisualScrollOffset()).c_str(),
+ ToString(scrollUpdate.GetDestination() - scrollUpdate.GetSource())
+ .c_str());
+
+ scrollOffsetUpdated = true;
+
+ // It's possible that the main thread has ignored an APZ scroll offset
+ // update for the pending relative scroll that we have just received.
+ // When this happens, we need to send a new scroll offset update with
+ // the combined scroll offset or else the main thread may have an
+ // incorrect scroll offset for a period of time.
+ if (Metrics().HasPendingScroll(aLayerMetrics)) {
+ needContentRepaint = true;
+ contentRepaintType = RepaintUpdateType::eUserAction;
+ }
+
+ relativeDelta =
+ Some(Metrics().ApplyRelativeScrollUpdateFrom(scrollUpdate));
+ Metrics().RecalculateLayoutViewportOffset();
+ } else if (scrollUpdate.GetType() == ScrollUpdateType::PureRelative) {
+ APZC_LOG("%p pure-relative updating scroll offset from %s by %s\n", this,
+ ToString(Metrics().GetVisualScrollOffset()).c_str(),
+ ToString(scrollUpdate.GetDelta()).c_str());
+
+ scrollOffsetUpdated = true;
+
+ // Always need a repaint request with a repaint type for pure relative
+ // scrolls because apz is doing the scroll at the main thread's request.
+ // The main thread has not updated it's scroll offset yet, it is depending
+ // on apz to tell it where to scroll.
+ needContentRepaint = true;
+ contentRepaintType = RepaintUpdateType::eVisualUpdate;
+
+ // We have to ignore a visual scroll offset update otherwise it will
+ // clobber the relative scrolling we are about to do. We perform
+ // visualScrollOffset = visualScrollOffset + delta. Then the
+ // visualScrollOffsetUpdated block below will do visualScrollOffset =
+ // aLayerMetrics.GetVisualDestination(). We need visual scroll offset
+ // updates to be incorporated into this scroll update loop to properly fix
+ // this.
+ ignoreVisualUpdate = true;
+
+ relativeDelta =
+ Some(Metrics().ApplyPureRelativeScrollUpdateFrom(scrollUpdate));
+ Metrics().RecalculateLayoutViewportOffset();
+ } else {
+ APZC_LOG("%p updating scroll offset from %s to %s\n", this,
+ ToString(Metrics().GetVisualScrollOffset()).c_str(),
+ ToString(scrollUpdate.GetDestination()).c_str());
+ bool offsetChanged = Metrics().ApplyScrollUpdateFrom(scrollUpdate);
+ Metrics().RecalculateLayoutViewportOffset();
+
+ if (offsetChanged || scrollUpdate.GetMode() != ScrollMode::Instant ||
+ scrollUpdate.GetType() != ScrollUpdateType::Absolute ||
+ scrollUpdate.GetOrigin() != ScrollOrigin::None) {
+ // We get a NewScrollFrame update for newly created scroll frames. Only
+ // if this was not a NewScrollFrame update or the offset changed do we
+ // request repaint. This is important so that we don't request repaint
+ // for every new content and set a full display port on it.
+ scrollOffsetUpdated = true;
+ }
+ }
+
+ if (relativeDelta) {
+ cumulativeRelativeDelta =
+ !cumulativeRelativeDelta
+ ? relativeDelta
+ : Some(*cumulativeRelativeDelta + *relativeDelta);
+ } else {
+ // If the scroll update is not relative, clobber the cumulative delta,
+ // i.e. later updates win.
+ cumulativeRelativeDelta.reset();
+ }
+
+ // If an animation is underway, tell it about the scroll offset update.
+ // Some animations can handle some scroll offset updates and continue
+ // running. Those that can't will return false, and we cancel them.
+ if (ShouldCancelAnimationForScrollUpdate(relativeDelta)) {
+ // Cancel the animation (which might also trigger a repaint request)
+ // after we update the scroll offset above. Otherwise we can be left
+ // in a state where things are out of sync.
+ CancelAnimation();
+ didCancelAnimation = true;
+ }
+ }
+
+ if (scrollOffsetUpdated) {
+ for (auto& sampledState : mSampledState) {
+ if (!didCancelAnimation && cumulativeRelativeDelta.isSome()) {
+ sampledState.UpdateScrollPropertiesWithRelativeDelta(
+ Metrics(), *cumulativeRelativeDelta);
+ } else {
+ sampledState.UpdateScrollProperties(Metrics());
+ }
+ }
+
+ // Because of the scroll generation update, any inflight paint requests
+ // are going to be ignored by layout, and so mExpectedGeckoMetrics becomes
+ // incorrect for the purposes of calculating the LD transform. To correct
+ // this we need to update mExpectedGeckoMetrics to be the last thing we
+ // know was painted by Gecko.
+ mExpectedGeckoMetrics.UpdateFrom(aLayerMetrics);
+
+ // Since the scroll offset has changed, we need to recompute the
+ // displayport margins and send them to layout. Otherwise there might be
+ // scenarios where for example we scroll from the top of a page (where the
+ // top displayport margin is zero) to the bottom of a page, which will
+ // result in a displayport that doesn't extend upwards at all.
+ // Note that even if the CancelAnimation call above requested a repaint
+ // this is fine because we already have repaint request deduplication.
+ needContentRepaint = true;
+ // Since the main-thread scroll offset changed we should trigger a
+ // recomposite to make sure it becomes user-visible.
+ ScheduleComposite();
+ } else if (needToReclampScroll) {
+ // Even if we didn't accept a new scroll offset from content, the
+ // scrollable rect or composition bounds may have changed in a way that
+ // makes our local scroll offset out of bounds, so re-clamp it.
+ ClampAndSetVisualScrollOffset(Metrics().GetVisualScrollOffset());
+ for (auto& sampledState : mSampledState) {
+ sampledState.ClampVisualScrollOffset(Metrics());
+ }
+ }
+
+ // If our scroll range changed (for example, because the page dynamically
+ // loaded new content, thereby increasing the size of the scrollable rect),
+ // and we're overscrolled, being overscrolled may no longer be a valid
+ // state (for example, we may no longer be at the edge of our scroll range),
+ // so clear overscroll and discontinue any overscroll animation.
+ // Ideas for improvements here:
+ // - Instead of collapsing the overscroll gutter, try to "fill it"
+ // with newly loaded content. This would basically entail checking
+ // if (GetVisualScrollOffset() + GetOverscrollAmount()) is a valid
+ // visual scroll offset in our new scroll range, and if so, scrolling
+ // there.
+ if (needToReclampScroll) {
+ if (IsInInvalidOverscroll()) {
+ if (mState == OVERSCROLL_ANIMATION) {
+ CancelAnimation();
+ } else if (IsOverscrolled()) {
+ ClearOverscroll();
+ }
+ }
+ }
+
+ if (smoothScrollRequested && !scrollOffsetUpdated) {
+ mExpectedGeckoMetrics.UpdateFrom(aLayerMetrics);
+ // Need to acknowledge the request.
+ needContentRepaint = true;
+ }
+
+ // If `isDefault` is true, this APZC is a "new" one (this is the first time
+ // it's getting a NotifyLayersUpdated call). In this case we want to apply the
+ // visual scroll offset from the main thread to our scroll offset.
+ // The main thread may also ask us to scroll the visual viewport to a
+ // particular location. However, in all cases, we want to ignore the visual
+ // offset update if ignoreVisualUpdate is true, because we're clobbering
+ // the visual update with a layout update.
+ bool visualScrollOffsetUpdated =
+ !ignoreVisualUpdate &&
+ (isDefault ||
+ aLayerMetrics.GetVisualScrollUpdateType() != FrameMetrics::eNone);
+
+ if (visualScrollOffsetUpdated) {
+ APZC_LOG("%p updating visual scroll offset from %s to %s (updateType %d)\n",
+ this, ToString(Metrics().GetVisualScrollOffset()).c_str(),
+ ToString(aLayerMetrics.GetVisualDestination()).c_str(),
+ (int)aLayerMetrics.GetVisualScrollUpdateType());
+ bool offsetChanged = Metrics().ClampAndSetVisualScrollOffset(
+ aLayerMetrics.GetVisualDestination());
+
+ // If this is the first time we got metrics for this content (isDefault) and
+ // the update type was none and the offset didn't change then we don't have
+ // to do anything. This is important because we don't want to request
+ // repaint on the initial NotifyLayersUpdated for every content and thus set
+ // a full display port.
+ if (aLayerMetrics.GetVisualScrollUpdateType() == FrameMetrics::eNone &&
+ !offsetChanged) {
+ visualScrollOffsetUpdated = false;
+ }
+ }
+ if (visualScrollOffsetUpdated) {
+ // The rest of this branch largely follows the code in the
+ // |if (scrollOffsetUpdated)| branch above. Eventually it should get
+ // merged into that branch.
+ Metrics().RecalculateLayoutViewportOffset();
+ for (auto& sampledState : mSampledState) {
+ sampledState.UpdateScrollProperties(Metrics());
+ }
+ mExpectedGeckoMetrics.UpdateFrom(aLayerMetrics);
+ if (ShouldCancelAnimationForScrollUpdate(Nothing())) {
+ CancelAnimation();
+ }
+ // The main thread did not actually paint a displayport at the target
+ // visual offset, so we need to ask it to repaint. We need to set the
+ // contentRepaintType to something other than eNone, otherwise the main
+ // thread will short-circuit the repaint request.
+ // Don't do this for eRestore visual updates as a repaint coming from APZ
+ // breaks the scroll offset restoration mechanism.
+ needContentRepaint = true;
+ if (aLayerMetrics.GetVisualScrollUpdateType() ==
+ FrameMetrics::eMainThread) {
+ contentRepaintType = RepaintUpdateType::eVisualUpdate;
+ }
+ ScheduleComposite();
+ }
+
+ if (viewportSizeUpdated) {
+ // While we want to accept the main thread's layout viewport _size_,
+ // its position may be out of date in light of async scrolling, to
+ // adjust it if necessary to make sure it continues to enclose the
+ // visual viewport.
+ // Note: it's important to do this _after_ we've accepted any
+ // updated composition bounds.
+ Metrics().RecalculateLayoutViewportOffset();
+ }
+
+ if (needContentRepaint) {
+ // This repaint request could be driven by a user action if we accept a
+ // relative scroll offset update
+ RequestContentRepaint(contentRepaintType);
+ }
+}
+
+FrameMetrics& AsyncPanZoomController::Metrics() {
+ mRecursiveMutex.AssertCurrentThreadIn();
+ return mScrollMetadata.GetMetrics();
+}
+
+const FrameMetrics& AsyncPanZoomController::Metrics() const {
+ mRecursiveMutex.AssertCurrentThreadIn();
+ return mScrollMetadata.GetMetrics();
+}
+
+GeckoViewMetrics AsyncPanZoomController::GetGeckoViewMetrics() const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ return GeckoViewMetrics{GetEffectiveScrollOffset(eForCompositing, lock),
+ GetEffectiveZoom(eForCompositing, lock)};
+}
+
+bool AsyncPanZoomController::UpdateRootFrameMetricsIfChanged(
+ GeckoViewMetrics& aMetrics) {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+
+ if (!Metrics().IsRootContent()) {
+ return false;
+ }
+
+ GeckoViewMetrics newMetrics = GetGeckoViewMetrics();
+ bool hasChanged = RoundedToInt(aMetrics.mVisualScrollOffset) !=
+ RoundedToInt(newMetrics.mVisualScrollOffset) ||
+ aMetrics.mZoom != newMetrics.mZoom;
+
+ if (hasChanged) {
+ aMetrics = newMetrics;
+ }
+
+ return hasChanged;
+}
+
+const FrameMetrics& AsyncPanZoomController::GetFrameMetrics() const {
+ return Metrics();
+}
+
+const ScrollMetadata& AsyncPanZoomController::GetScrollMetadata() const {
+ mRecursiveMutex.AssertCurrentThreadIn();
+ return mScrollMetadata;
+}
+
+void AsyncPanZoomController::AssertOnSamplerThread() const {
+ if (APZCTreeManager* treeManagerLocal = GetApzcTreeManager()) {
+ treeManagerLocal->AssertOnSamplerThread();
+ }
+}
+
+void AsyncPanZoomController::AssertOnUpdaterThread() const {
+ if (APZCTreeManager* treeManagerLocal = GetApzcTreeManager()) {
+ treeManagerLocal->AssertOnUpdaterThread();
+ }
+}
+
+APZCTreeManager* AsyncPanZoomController::GetApzcTreeManager() const {
+ mRecursiveMutex.AssertNotCurrentThreadIn();
+ return mTreeManager;
+}
+
+void AsyncPanZoomController::ZoomToRect(const ZoomTarget& aZoomTarget,
+ const uint32_t aFlags) {
+ CSSRect rect = aZoomTarget.targetRect;
+ if (!rect.IsFinite()) {
+ NS_WARNING("ZoomToRect got called with a non-finite rect; ignoring...");
+ return;
+ }
+
+ if (rect.IsEmpty() && (aFlags & DISABLE_ZOOM_OUT)) {
+ // Double-tap-to-zooming uses an empty rect to mean "zoom out".
+ // If zooming out is disabled, an empty rect is nonsensical
+ // and will produce undesirable scrolling.
+ NS_WARNING(
+ "ZoomToRect got called with an empty rect and zoom out disabled; "
+ "ignoring...");
+ return;
+ }
+
+ SetState(ANIMATING_ZOOM);
+
+ {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+
+ MOZ_ASSERT(Metrics().IsRootContent());
+
+ const float defaultZoomInAmount =
+ StaticPrefs::apz_doubletapzoom_defaultzoomin();
+
+ ParentLayerRect compositionBounds = Metrics().GetCompositionBounds();
+ CSSRect cssPageRect = Metrics().GetScrollableRect();
+ CSSPoint scrollOffset = Metrics().GetVisualScrollOffset();
+ CSSSize sizeBeforeZoom = Metrics().CalculateCompositedSizeInCssPixels();
+ CSSToParentLayerScale currentZoom = Metrics().GetZoom();
+ CSSToParentLayerScale targetZoom;
+
+ // The minimum zoom to prevent over-zoom-out.
+ // If the zoom factor is lower than this (i.e. we are zoomed more into the
+ // page), then the CSS content rect, in layers pixels, will be smaller than
+ // the composition bounds. If this happens, we can't fill the target
+ // composited area with this frame.
+ CSSToParentLayerScale localMinZoom(
+ std::max(compositionBounds.Width() / cssPageRect.Width(),
+ compositionBounds.Height() / cssPageRect.Height()));
+
+ localMinZoom.scale =
+ clamped(localMinZoom.scale, mZoomConstraints.mMinZoom.scale,
+ mZoomConstraints.mMaxZoom.scale);
+
+ localMinZoom = std::max(mZoomConstraints.mMinZoom, localMinZoom);
+ CSSToParentLayerScale localMaxZoom =
+ std::max(localMinZoom, mZoomConstraints.mMaxZoom);
+
+ if (!rect.IsEmpty()) {
+ // Intersect the zoom-to-rect to the CSS rect to make sure it fits.
+ rect = rect.Intersect(cssPageRect);
+ targetZoom = CSSToParentLayerScale(
+ std::min(compositionBounds.Width() / rect.Width(),
+ compositionBounds.Height() / rect.Height()));
+ if (aFlags & DISABLE_ZOOM_OUT) {
+ targetZoom = std::max(targetZoom, currentZoom);
+ }
+ }
+
+ // 1. If the rect is empty, the content-side logic for handling a double-tap
+ // requested that we zoom out.
+ // 2. currentZoom is equal to mZoomConstraints.mMaxZoom and user still
+ // double-tapping it
+ // Treat these cases as a request to zoom out as much as possible
+ // unless cantZoomOutBehavior == ZoomIn and currentZoom
+ // is equal to localMinZoom and user still double-tapping it, then try to
+ // zoom in a small amount to provide feedback to the user.
+ bool zoomOut = false;
+ // True if we are already zoomed out and we are asked to either stay there
+ // or zoom out more and cantZoomOutBehavior == ZoomIn.
+ bool zoomInDefaultAmount = false;
+ if (aFlags & DISABLE_ZOOM_OUT) {
+ zoomOut = false;
+ } else {
+ if (rect.IsEmpty()) {
+ if (currentZoom == localMinZoom &&
+ aZoomTarget.cantZoomOutBehavior == CantZoomOutBehavior::ZoomIn &&
+ (defaultZoomInAmount != 1.f)) {
+ zoomInDefaultAmount = true;
+ } else {
+ zoomOut = true;
+ }
+ } else if (currentZoom == localMaxZoom && targetZoom >= localMaxZoom) {
+ zoomOut = true;
+ }
+ }
+
+ // already at min zoom and asked to zoom out further
+ if (!zoomOut && currentZoom == localMinZoom && targetZoom <= localMinZoom &&
+ aZoomTarget.cantZoomOutBehavior == CantZoomOutBehavior::ZoomIn &&
+ (defaultZoomInAmount != 1.f)) {
+ zoomInDefaultAmount = true;
+ }
+ MOZ_ASSERT(!(zoomInDefaultAmount && zoomOut));
+
+ if (zoomInDefaultAmount) {
+ targetZoom =
+ CSSToParentLayerScale(currentZoom.scale * defaultZoomInAmount);
+ }
+
+ if (zoomOut) {
+ targetZoom = localMinZoom;
+ }
+
+ if (aFlags & PAN_INTO_VIEW_ONLY) {
+ targetZoom = currentZoom;
+ } else if (aFlags & ONLY_ZOOM_TO_DEFAULT_SCALE) {
+ CSSToParentLayerScale zoomAtDefaultScale =
+ Metrics().GetDevPixelsPerCSSPixel() *
+ LayoutDeviceToParentLayerScale(1.0);
+ if (targetZoom.scale > zoomAtDefaultScale.scale) {
+ // Only change the zoom if we are less than the default zoom
+ if (currentZoom.scale < zoomAtDefaultScale.scale) {
+ targetZoom = zoomAtDefaultScale;
+ } else {
+ targetZoom = currentZoom;
+ }
+ }
+ }
+
+ targetZoom.scale =
+ clamped(targetZoom.scale, localMinZoom.scale, localMaxZoom.scale);
+
+ FrameMetrics endZoomToMetrics = Metrics();
+ endZoomToMetrics.SetZoom(CSSToParentLayerScale(targetZoom));
+ CSSSize sizeAfterZoom =
+ endZoomToMetrics.CalculateCompositedSizeInCssPixels();
+
+ if (zoomInDefaultAmount || zoomOut) {
+ // For the zoom out case we should always center what was visible
+ // otherwise it feels like we are scrolling as well as zooming out. For
+ // the non-zoomOut case, if we've been provided a pointer location, zoom
+ // around that, otherwise just zoom in to the center of what's currently
+ // visible.
+ if (!zoomOut && aZoomTarget.documentRelativePointerPosition.isSome()) {
+ rect = CSSRect(aZoomTarget.documentRelativePointerPosition->x -
+ sizeAfterZoom.width / 2,
+ aZoomTarget.documentRelativePointerPosition->y -
+ sizeAfterZoom.height / 2,
+ sizeAfterZoom.Width(), sizeAfterZoom.Height());
+ } else {
+ rect = CSSRect(
+ scrollOffset.x + (sizeBeforeZoom.width - sizeAfterZoom.width) / 2,
+ scrollOffset.y + (sizeBeforeZoom.height - sizeAfterZoom.height) / 2,
+ sizeAfterZoom.Width(), sizeAfterZoom.Height());
+ }
+
+ rect = rect.Intersect(cssPageRect);
+ }
+
+ // Check if we can fit the full elementBoundingRect.
+ if (!aZoomTarget.targetRect.IsEmpty() && !zoomOut &&
+ aZoomTarget.elementBoundingRect.isSome()) {
+ MOZ_ASSERT(aZoomTarget.elementBoundingRect->Contains(rect));
+ CSSRect elementBoundingRect =
+ aZoomTarget.elementBoundingRect->Intersect(cssPageRect);
+ if (elementBoundingRect.width <= sizeAfterZoom.width &&
+ elementBoundingRect.height <= sizeAfterZoom.height) {
+ rect = elementBoundingRect;
+ }
+ }
+
+ // Vertically center the zoomed element in the screen.
+ if (!zoomOut && (sizeAfterZoom.height > rect.Height())) {
+ rect.MoveByY(-(sizeAfterZoom.height - rect.Height()) * 0.5f);
+ if (rect.Y() < 0.0f) {
+ rect.MoveToY(0.0f);
+ }
+ }
+
+ // Horizontally center the zoomed element in the screen.
+ if (!zoomOut && (sizeAfterZoom.width > rect.Width())) {
+ rect.MoveByX(-(sizeAfterZoom.width - rect.Width()) * 0.5f);
+ if (rect.X() < 0.0f) {
+ rect.MoveToX(0.0f);
+ }
+ }
+
+ bool intersectRectAgain = false;
+ // If we can't zoom out enough to show the full rect then shift the rect we
+ // are able to show to center what was visible.
+ // Note that this calculation works no matter the relation of sizeBeforeZoom
+ // to sizeAfterZoom, ie whether we are increasing or decreasing zoom.
+ if (!zoomOut && (sizeAfterZoom.height < rect.Height())) {
+ rect.y =
+ scrollOffset.y + (sizeBeforeZoom.height - sizeAfterZoom.height) / 2;
+ rect.height = sizeAfterZoom.Height();
+
+ intersectRectAgain = true;
+ }
+
+ if (!zoomOut && (sizeAfterZoom.width < rect.Width())) {
+ rect.x =
+ scrollOffset.x + (sizeBeforeZoom.width - sizeAfterZoom.width) / 2;
+ rect.width = sizeAfterZoom.Width();
+
+ intersectRectAgain = true;
+ }
+ if (intersectRectAgain) {
+ rect = rect.Intersect(cssPageRect);
+ }
+
+ // If any of these conditions are met, the page will be overscrolled after
+ // zoomed. Attempting to scroll outside of the valid scroll range will cause
+ // problems.
+ if (rect.Y() + sizeAfterZoom.height > cssPageRect.YMost()) {
+ rect.MoveToY(std::max(cssPageRect.Y(),
+ cssPageRect.YMost() - sizeAfterZoom.height));
+ }
+ if (rect.Y() < cssPageRect.Y()) {
+ rect.MoveToY(cssPageRect.Y());
+ }
+ if (rect.X() + sizeAfterZoom.width > cssPageRect.XMost()) {
+ rect.MoveToX(
+ std::max(cssPageRect.X(), cssPageRect.XMost() - sizeAfterZoom.width));
+ }
+ if (rect.X() < cssPageRect.X()) {
+ rect.MoveToY(cssPageRect.X());
+ }
+
+ endZoomToMetrics.SetVisualScrollOffset(rect.TopLeft());
+ endZoomToMetrics.RecalculateLayoutViewportOffset();
+
+ StartAnimation(new ZoomAnimation(
+ *this, Metrics().GetVisualScrollOffset(), Metrics().GetZoom(),
+ endZoomToMetrics.GetVisualScrollOffset(), endZoomToMetrics.GetZoom()));
+
+ RequestContentRepaint(RepaintUpdateType::eUserAction);
+ }
+}
+
+InputBlockState* AsyncPanZoomController::GetCurrentInputBlock() const {
+ return GetInputQueue()->GetCurrentBlock();
+}
+
+TouchBlockState* AsyncPanZoomController::GetCurrentTouchBlock() const {
+ return GetInputQueue()->GetCurrentTouchBlock();
+}
+
+PanGestureBlockState* AsyncPanZoomController::GetCurrentPanGestureBlock()
+ const {
+ return GetInputQueue()->GetCurrentPanGestureBlock();
+}
+
+PinchGestureBlockState* AsyncPanZoomController::GetCurrentPinchGestureBlock()
+ const {
+ return GetInputQueue()->GetCurrentPinchGestureBlock();
+}
+
+void AsyncPanZoomController::ResetTouchInputState() {
+ MultiTouchInput cancel(MultiTouchInput::MULTITOUCH_CANCEL, 0,
+ TimeStamp::Now(), 0);
+ RefPtr<GestureEventListener> listener = GetGestureEventListener();
+ if (listener) {
+ listener->HandleInputEvent(cancel);
+ }
+ CancelAnimationAndGestureState();
+ // Clear overscroll along the entire handoff chain, in case an APZC
+ // later in the chain is overscrolled.
+ if (TouchBlockState* block = GetCurrentTouchBlock()) {
+ block->GetOverscrollHandoffChain()->ClearOverscroll();
+ }
+}
+
+void AsyncPanZoomController::ResetPanGestureInputState() {
+ // No point sending a PANGESTURE_INTERRUPTED as all it does is
+ // call CancelAnimation(), which we also do here.
+ CancelAnimationAndGestureState();
+ // Clear overscroll along the entire handoff chain, in case an APZC
+ // later in the chain is overscrolled.
+ if (PanGestureBlockState* block = GetCurrentPanGestureBlock()) {
+ block->GetOverscrollHandoffChain()->ClearOverscroll();
+ }
+}
+
+void AsyncPanZoomController::CancelAnimationAndGestureState() {
+ mX.CancelGesture();
+ mY.CancelGesture();
+ CancelAnimation(CancelAnimationFlags::ScrollSnap);
+}
+
+bool AsyncPanZoomController::HasReadyTouchBlock() const {
+ return GetInputQueue()->HasReadyTouchBlock();
+}
+
+bool AsyncPanZoomController::CanHandleScrollOffsetUpdate(PanZoomState aState) {
+ return aState == PAN_MOMENTUM || aState == TOUCHING || IsPanningState(aState);
+}
+
+bool AsyncPanZoomController::ShouldCancelAnimationForScrollUpdate(
+ const Maybe<CSSPoint>& aRelativeDelta) {
+ // Never call CancelAnimation() for a no-op relative update.
+ if (aRelativeDelta == Some(CSSPoint())) {
+ return false;
+ }
+
+ if (mAnimation) {
+ return !mAnimation->HandleScrollOffsetUpdate(aRelativeDelta);
+ }
+
+ return !CanHandleScrollOffsetUpdate(mState);
+}
+
+AsyncPanZoomController::PanZoomState
+AsyncPanZoomController::SetStateNoContentControllerDispatch(
+ PanZoomState aNewState) {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ APZC_LOG_DETAIL("changing from state %s to %s\n", this,
+ ToString(mState).c_str(), ToString(aNewState).c_str());
+ PanZoomState oldState = mState;
+ mState = aNewState;
+ return oldState;
+}
+
+void AsyncPanZoomController::SetState(PanZoomState aNewState) {
+ // When a state transition to a transforming state is occuring and a delayed
+ // transform end notification exists, send the TransformEnd notification
+ // before the TransformBegin notification is sent for the input state change.
+ if (IsTransformingState(aNewState) && IsDelayedTransformEndSet()) {
+ MOZ_ASSERT(!IsTransformingState(mState));
+ SetDelayedTransformEnd(false);
+ DispatchStateChangeNotification(PANNING, NOTHING);
+ }
+
+ PanZoomState oldState = SetStateNoContentControllerDispatch(aNewState);
+
+ DispatchStateChangeNotification(oldState, aNewState);
+}
+
+auto AsyncPanZoomController::GetState() const -> PanZoomState {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ return mState;
+}
+
+void AsyncPanZoomController::DispatchStateChangeNotification(
+ PanZoomState aOldState, PanZoomState aNewState) {
+ { // scope the lock
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ if (mNotificationBlockers > 0) {
+ return;
+ }
+ }
+
+ if (RefPtr<GeckoContentController> controller = GetGeckoContentController()) {
+ if (!IsTransformingState(aOldState) && IsTransformingState(aNewState)) {
+ controller->NotifyAPZStateChange(GetGuid(),
+ APZStateChange::eTransformBegin);
+ } else if (IsTransformingState(aOldState) &&
+ !IsTransformingState(aNewState)) {
+ controller->NotifyAPZStateChange(GetGuid(),
+ APZStateChange::eTransformEnd);
+ }
+ }
+}
+
+bool AsyncPanZoomController::IsInTransformingState() const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ return IsTransformingState(mState);
+}
+
+bool AsyncPanZoomController::IsTransformingState(PanZoomState aState) {
+ return !(aState == NOTHING || aState == TOUCHING);
+}
+
+bool AsyncPanZoomController::IsPanningState(PanZoomState aState) {
+ return (aState == PANNING || aState == PANNING_LOCKED_X ||
+ aState == PANNING_LOCKED_Y);
+}
+
+bool AsyncPanZoomController::IsInPanningState() const {
+ return IsPanningState(mState);
+}
+
+bool AsyncPanZoomController::IsInScrollingGesture() const {
+ return IsPanningState(mState) || mState == SCROLLBAR_DRAG ||
+ mState == TOUCHING || mState == PINCHING;
+}
+
+bool AsyncPanZoomController::IsDelayedTransformEndSet() {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ return mDelayedTransformEnd;
+}
+
+void AsyncPanZoomController::SetDelayedTransformEnd(bool aDelayedTransformEnd) {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ mDelayedTransformEnd = aDelayedTransformEnd;
+}
+
+void AsyncPanZoomController::UpdateZoomConstraints(
+ const ZoomConstraints& aConstraints) {
+ if ((MOZ_LOG_TEST(sApzCtlLog, LogLevel::Debug) &&
+ (aConstraints != mZoomConstraints)) ||
+ MOZ_LOG_TEST(sApzCtlLog, LogLevel::Verbose)) {
+ APZC_LOG("%p updating zoom constraints to %d %d %f %f\n", this,
+ aConstraints.mAllowZoom, aConstraints.mAllowDoubleTapZoom,
+ aConstraints.mMinZoom.scale, aConstraints.mMaxZoom.scale);
+ }
+
+ if (std::isnan(aConstraints.mMinZoom.scale) ||
+ std::isnan(aConstraints.mMaxZoom.scale)) {
+ NS_WARNING("APZC received zoom constraints with NaN values; dropping...");
+ return;
+ }
+
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ CSSToParentLayerScale min = Metrics().GetDevPixelsPerCSSPixel() *
+ ViewportMinScale() / ParentLayerToScreenScale(1);
+ CSSToParentLayerScale max = Metrics().GetDevPixelsPerCSSPixel() *
+ ViewportMaxScale() / ParentLayerToScreenScale(1);
+
+ // inf float values and other bad cases should be sanitized by the code below.
+ mZoomConstraints.mAllowZoom = aConstraints.mAllowZoom;
+ mZoomConstraints.mAllowDoubleTapZoom = aConstraints.mAllowDoubleTapZoom;
+ mZoomConstraints.mMinZoom =
+ (min > aConstraints.mMinZoom ? min : aConstraints.mMinZoom);
+ mZoomConstraints.mMaxZoom =
+ (max > aConstraints.mMaxZoom ? aConstraints.mMaxZoom : max);
+ if (mZoomConstraints.mMaxZoom < mZoomConstraints.mMinZoom) {
+ mZoomConstraints.mMaxZoom = mZoomConstraints.mMinZoom;
+ }
+}
+
+bool AsyncPanZoomController::ZoomConstraintsAllowZoom() const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ return mZoomConstraints.mAllowZoom;
+}
+
+bool AsyncPanZoomController::ZoomConstraintsAllowDoubleTapZoom() const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ return mZoomConstraints.mAllowDoubleTapZoom;
+}
+
+void AsyncPanZoomController::PostDelayedTask(already_AddRefed<Runnable> aTask,
+ int aDelayMs) {
+ APZThreadUtils::AssertOnControllerThread();
+ RefPtr<Runnable> task = aTask;
+ RefPtr<GeckoContentController> controller = GetGeckoContentController();
+ if (controller) {
+ controller->PostDelayedTask(task.forget(), aDelayMs);
+ }
+ // If there is no controller, that means this APZC has been destroyed, and
+ // we probably don't need to run the task. It will get destroyed when the
+ // RefPtr goes out of scope.
+}
+
+bool AsyncPanZoomController::Matches(const ScrollableLayerGuid& aGuid) {
+ return aGuid == GetGuid();
+}
+
+bool AsyncPanZoomController::HasTreeManager(
+ const APZCTreeManager* aTreeManager) const {
+ return GetApzcTreeManager() == aTreeManager;
+}
+
+void AsyncPanZoomController::GetGuid(ScrollableLayerGuid* aGuidOut) const {
+ if (aGuidOut) {
+ *aGuidOut = GetGuid();
+ }
+}
+
+ScrollableLayerGuid AsyncPanZoomController::GetGuid() const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ return ScrollableLayerGuid(mLayersId, Metrics().GetPresShellId(),
+ Metrics().GetScrollId());
+}
+
+void AsyncPanZoomController::SetTestAsyncScrollOffset(const CSSPoint& aPoint) {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ mTestAsyncScrollOffset = aPoint;
+ ScheduleComposite();
+}
+
+void AsyncPanZoomController::SetTestAsyncZoom(
+ const LayerToParentLayerScale& aZoom) {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ mTestAsyncZoom = aZoom;
+ ScheduleComposite();
+}
+
+Maybe<CSSSnapTarget> AsyncPanZoomController::FindSnapPointNear(
+ const CSSPoint& aDestination, ScrollUnit aUnit,
+ ScrollSnapFlags aSnapFlags) {
+ mRecursiveMutex.AssertCurrentThreadIn();
+ APZC_LOG("%p scroll snapping near %s\n", this,
+ ToString(aDestination).c_str());
+ CSSRect scrollRange = Metrics().CalculateScrollRange();
+ if (auto snapTarget = ScrollSnapUtils::GetSnapPointForDestination(
+ mScrollMetadata.GetSnapInfo(), aUnit, aSnapFlags,
+ CSSRect::ToAppUnits(scrollRange),
+ CSSPoint::ToAppUnits(Metrics().GetVisualScrollOffset()),
+ CSSPoint::ToAppUnits(aDestination))) {
+ CSSPoint cssSnapPoint = CSSPoint::FromAppUnits(snapTarget->mPosition);
+ // GetSnapPointForDestination() can produce a destination that's outside
+ // of the scroll frame's scroll range. Clamp it here (this matches the
+ // behaviour of the main-thread code path, which clamps it in
+ // nsGfxScrollFrame::ScrollTo()).
+ return Some(CSSSnapTarget{scrollRange.ClampPoint(cssSnapPoint),
+ snapTarget->mTargetIds});
+ }
+ return Nothing();
+}
+
+Maybe<std::pair<MultiTouchInput, MultiTouchInput>>
+AsyncPanZoomController::MaybeSplitTouchMoveEvent(
+ const MultiTouchInput& aOriginalEvent, ScreenCoord aPanThreshold,
+ float aVectorLength, ExternalPoint& aExtPoint) {
+ if (aVectorLength <= aPanThreshold) {
+ return Nothing();
+ }
+
+ auto splitEvent = std::make_pair(aOriginalEvent, aOriginalEvent);
+
+ SingleTouchData& firstTouchData = splitEvent.first.mTouches[0];
+ SingleTouchData& secondTouchData = splitEvent.second.mTouches[0];
+
+ firstTouchData.mHistoricalData.Clear();
+ secondTouchData.mHistoricalData.Clear();
+
+ ExternalPoint destination = aExtPoint;
+ ExternalPoint thresholdPosition;
+
+ const float ratio = aPanThreshold / aVectorLength;
+ thresholdPosition.x = mStartTouch.x + ratio * (destination.x - mStartTouch.x);
+ thresholdPosition.y = mStartTouch.y + ratio * (destination.y - mStartTouch.y);
+
+ TouchSample start{mLastTouch};
+ // To compute the timestamp of the first event (which is at the threshold),
+ // use linear interpolation with the starting point |start| being the last
+ // event that's before the threshold, and the end point |end| being the first
+ // event after the threshold.
+
+ // The initial choice for |start| is the last touch event before
+ // |aOriginalEvent|, and the initial choice for |end| is |aOriginalEvent|.
+
+ // However, the historical data points stored in |aOriginalEvent| may contain
+ // intermediate positions that can serve as tighter bounds for the
+ // interpolation.
+ TouchSample end{destination, aOriginalEvent.mTimeStamp};
+
+ for (const auto& historicalData :
+ aOriginalEvent.mTouches[0].mHistoricalData) {
+ ExternalPoint histExtPoint = ToExternalPoint(aOriginalEvent.mScreenOffset,
+ historicalData.mScreenPoint);
+
+ if (PanVector(histExtPoint).Length() <
+ PanVector(thresholdPosition).Length()) {
+ start = {histExtPoint, historicalData.mTimeStamp};
+ } else {
+ break;
+ }
+ }
+
+ for (const SingleTouchData::HistoricalTouchData& histData :
+ Reversed(aOriginalEvent.mTouches[0].mHistoricalData)) {
+ ExternalPoint histExtPoint =
+ ToExternalPoint(aOriginalEvent.mScreenOffset, histData.mScreenPoint);
+
+ if (PanVector(histExtPoint).Length() >
+ PanVector(thresholdPosition).Length()) {
+ end = {histExtPoint, histData.mTimeStamp};
+ } else {
+ break;
+ }
+ }
+
+ const float totalLength =
+ ScreenPoint(fabs(end.mPosition.x - start.mPosition.x),
+ fabs(end.mPosition.y - start.mPosition.y))
+ .Length();
+ const float thresholdLength =
+ ScreenPoint(fabs(thresholdPosition.x - start.mPosition.x),
+ fabs(thresholdPosition.y - start.mPosition.y))
+ .Length();
+ const float splitRatio = thresholdLength / totalLength;
+
+ splitEvent.first.mTimeStamp =
+ start.mTimeStamp +
+ (end.mTimeStamp - start.mTimeStamp).MultDouble(splitRatio);
+
+ for (const auto& historicalData :
+ aOriginalEvent.mTouches[0].mHistoricalData) {
+ if (historicalData.mTimeStamp > splitEvent.first.mTimeStamp) {
+ secondTouchData.mHistoricalData.AppendElement(historicalData);
+ } else {
+ firstTouchData.mHistoricalData.AppendElement(historicalData);
+ }
+ }
+
+ firstTouchData.mScreenPoint = RoundedToInt(
+ ViewAs<ScreenPixel>(thresholdPosition - splitEvent.first.mScreenOffset,
+ PixelCastJustification::ExternalIsScreen));
+
+ // Recompute firstTouchData.mLocalScreenPoint.
+ splitEvent.first.TransformToLocal(GetTransformToThis());
+
+ // Pass |thresholdPosition| back out to the caller via |aExtPoint|
+ aExtPoint = thresholdPosition;
+
+ return Some(splitEvent);
+}
+
+void AsyncPanZoomController::ScrollSnapNear(const CSSPoint& aDestination,
+ ScrollSnapFlags aSnapFlags) {
+ if (Maybe<CSSSnapTarget> snapTarget = FindSnapPointNear(
+ aDestination, ScrollUnit::DEVICE_PIXELS, aSnapFlags)) {
+ if (snapTarget->mPosition != Metrics().GetVisualScrollOffset()) {
+ APZC_LOG("%p smooth scrolling to snap point %s\n", this,
+ ToString(snapTarget->mPosition).c_str());
+ SmoothMsdScrollTo(std::move(*snapTarget), ScrollTriggeredByScript::No);
+ }
+ }
+}
+
+void AsyncPanZoomController::ScrollSnap(ScrollSnapFlags aSnapFlags) {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ ScrollSnapNear(Metrics().GetVisualScrollOffset(), aSnapFlags);
+}
+
+void AsyncPanZoomController::ScrollSnapToDestination() {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+
+ float friction = StaticPrefs::apz_fling_friction();
+ ParentLayerPoint velocity(mX.GetVelocity(), mY.GetVelocity());
+ ParentLayerPoint predictedDelta;
+ // "-velocity / log(1.0 - friction)" is the integral of the deceleration
+ // curve modeled for flings in the "Axis" class.
+ if (velocity.x != 0.0f && friction != 0.0f) {
+ predictedDelta.x = -velocity.x / log(1.0 - friction);
+ }
+ if (velocity.y != 0.0f && friction != 0.0f) {
+ predictedDelta.y = -velocity.y / log(1.0 - friction);
+ }
+
+ // If the fling will overscroll, don't scroll snap, because then the user
+ // user would not see any overscroll animation.
+ bool flingWillOverscroll =
+ IsOverscrolled() && ((velocity.x.value * mX.GetOverscroll() >= 0) ||
+ (velocity.y.value * mY.GetOverscroll() >= 0));
+ if (flingWillOverscroll) {
+ return;
+ }
+
+ CSSPoint startPosition = Metrics().GetVisualScrollOffset();
+ ScrollSnapFlags snapFlags = ScrollSnapFlags::IntendedEndPosition;
+ if (predictedDelta != ParentLayerPoint()) {
+ snapFlags |= ScrollSnapFlags::IntendedDirection;
+ }
+ if (Maybe<CSSSnapTarget> snapTarget = MaybeAdjustDeltaForScrollSnapping(
+ ScrollUnit::DEVICE_PIXELS, snapFlags, predictedDelta,
+ startPosition)) {
+ APZC_LOG(
+ "%p fling snapping. friction: %f velocity: %f, %f "
+ "predictedDelta: %f, %f position: %f, %f "
+ "snapDestination: %f, %f\n",
+ this, friction, velocity.x.value, velocity.y.value,
+ predictedDelta.x.value, predictedDelta.y.value,
+ Metrics().GetVisualScrollOffset().x.value,
+ Metrics().GetVisualScrollOffset().y.value, startPosition.x.value,
+ startPosition.y.value);
+
+ // Ensure that any queued transform-end due to a pan-end is not
+ // sent. Instead rely on the transform-end sent due to the
+ // scroll snap animation.
+ SetDelayedTransformEnd(false);
+
+ SmoothMsdScrollTo(std::move(*snapTarget), ScrollTriggeredByScript::No);
+ }
+}
+
+Maybe<CSSSnapTarget> AsyncPanZoomController::MaybeAdjustDeltaForScrollSnapping(
+ ScrollUnit aUnit, ScrollSnapFlags aSnapFlags, ParentLayerPoint& aDelta,
+ CSSPoint& aStartPosition) {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ CSSToParentLayerScale zoom = Metrics().GetZoom();
+ if (zoom == CSSToParentLayerScale(0)) {
+ return Nothing();
+ }
+ CSSPoint destination = Metrics().CalculateScrollRange().ClampPoint(
+ aStartPosition + (aDelta / zoom));
+
+ if (Maybe<CSSSnapTarget> snapTarget =
+ FindSnapPointNear(destination, aUnit, aSnapFlags)) {
+ aDelta = (snapTarget->mPosition - aStartPosition) * zoom;
+ aStartPosition = snapTarget->mPosition;
+ return snapTarget;
+ }
+ return Nothing();
+}
+
+Maybe<CSSSnapTarget>
+AsyncPanZoomController::MaybeAdjustDeltaForScrollSnappingOnWheelInput(
+ const ScrollWheelInput& aEvent, ParentLayerPoint& aDelta,
+ CSSPoint& aStartPosition) {
+ // Don't scroll snap for pixel scrolls. This matches the main thread
+ // behaviour in EventStateManager::DoScrollText().
+ if (aEvent.mDeltaType == ScrollWheelInput::SCROLLDELTA_PIXEL) {
+ return Nothing();
+ }
+
+ // Note that this MaybeAdjustDeltaForScrollSnappingOnWheelInput also gets
+ // called for pan gestures at least on older Mac and Windows. In such cases
+ // `aEvent.mDeltaType` is `SCROLLDELTA_PIXEL` which should be filtered out by
+ // the above `if` block, so we assume all incoming `aEvent` are purely wheel
+ // events, thus we basically use `IntendedDirection` here.
+ // If we want to change the behavior, i.e. we want to do scroll snap for
+ // such cases as well, we need to use `IntendedEndPoint`.
+ ScrollSnapFlags snapFlags = ScrollSnapFlags::IntendedDirection;
+ if (aEvent.mDeltaType == ScrollWheelInput::SCROLLDELTA_PAGE) {
+ // On Windows there are a couple of cases where scroll events happen with
+ // SCROLLDELTA_PAGE, in such case we consider it's a page scroll.
+ snapFlags |= ScrollSnapFlags::IntendedEndPosition;
+ }
+ return MaybeAdjustDeltaForScrollSnapping(
+ ScrollWheelInput::ScrollUnitForDeltaType(aEvent.mDeltaType),
+ ScrollSnapFlags::IntendedDirection, aDelta, aStartPosition);
+}
+
+Maybe<CSSSnapTarget>
+AsyncPanZoomController::MaybeAdjustDestinationForScrollSnapping(
+ const KeyboardInput& aEvent, CSSPoint& aDestination,
+ ScrollSnapFlags aSnapFlags) {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ ScrollUnit unit = KeyboardScrollAction::GetScrollUnit(aEvent.mAction.mType);
+
+ if (Maybe<CSSSnapTarget> snapPoint =
+ FindSnapPointNear(aDestination, unit, aSnapFlags)) {
+ aDestination = snapPoint->mPosition;
+ return snapPoint;
+ }
+ return Nothing();
+}
+
+void AsyncPanZoomController::SetZoomAnimationId(
+ const Maybe<uint64_t>& aZoomAnimationId) {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ mZoomAnimationId = aZoomAnimationId;
+}
+
+Maybe<uint64_t> AsyncPanZoomController::GetZoomAnimationId() const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ return mZoomAnimationId;
+}
+
+std::ostream& operator<<(std::ostream& aOut,
+ const AsyncPanZoomController::PanZoomState& aState) {
+ switch (aState) {
+ case AsyncPanZoomController::PanZoomState::NOTHING:
+ aOut << "NOTHING";
+ break;
+ case AsyncPanZoomController::PanZoomState::FLING:
+ aOut << "FLING";
+ break;
+ case AsyncPanZoomController::PanZoomState::TOUCHING:
+ aOut << "TOUCHING";
+ break;
+ case AsyncPanZoomController::PanZoomState::PANNING:
+ aOut << "PANNING";
+ break;
+ case AsyncPanZoomController::PanZoomState::PANNING_LOCKED_X:
+ aOut << "PANNING_LOCKED_X";
+ break;
+ case AsyncPanZoomController::PanZoomState::PANNING_LOCKED_Y:
+ aOut << "PANNING_LOCKED_Y";
+ break;
+ case AsyncPanZoomController::PanZoomState::PAN_MOMENTUM:
+ aOut << "PAN_MOMENTUM";
+ break;
+ case AsyncPanZoomController::PanZoomState::PINCHING:
+ aOut << "PINCHING";
+ break;
+ case AsyncPanZoomController::PanZoomState::ANIMATING_ZOOM:
+ aOut << "ANIMATING_ZOOM";
+ break;
+ case AsyncPanZoomController::PanZoomState::OVERSCROLL_ANIMATION:
+ aOut << "OVERSCROLL_ANIMATION";
+ break;
+ case AsyncPanZoomController::PanZoomState::SMOOTH_SCROLL:
+ aOut << "SMOOTH_SCROLL";
+ break;
+ case AsyncPanZoomController::PanZoomState::SMOOTHMSD_SCROLL:
+ aOut << "SMOOTHMSD_SCROLL";
+ break;
+ case AsyncPanZoomController::PanZoomState::WHEEL_SCROLL:
+ aOut << "WHEEL_SCROLL";
+ break;
+ case AsyncPanZoomController::PanZoomState::KEYBOARD_SCROLL:
+ aOut << "KEYBOARD_SCROLL";
+ break;
+ case AsyncPanZoomController::PanZoomState::AUTOSCROLL:
+ aOut << "AUTOSCROLL";
+ break;
+ case AsyncPanZoomController::PanZoomState::SCROLLBAR_DRAG:
+ aOut << "SCROLLBAR_DRAG";
+ break;
+ default:
+ aOut << "UNKNOWN_STATE";
+ break;
+ }
+ return aOut;
+}
+
+bool operator==(const PointerEventsConsumableFlags& aLhs,
+ const PointerEventsConsumableFlags& aRhs) {
+ return (aLhs.mHasRoom == aRhs.mHasRoom) &&
+ (aLhs.mAllowedByTouchAction == aRhs.mAllowedByTouchAction);
+}
+
+std::ostream& operator<<(std::ostream& aOut,
+ const PointerEventsConsumableFlags& aFlags) {
+ aOut << std::boolalpha << "{ hasRoom: " << aFlags.mHasRoom
+ << ", allowedByTouchAction: " << aFlags.mAllowedByTouchAction << "}";
+ return aOut;
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/AsyncPanZoomController.h b/gfx/layers/apz/src/AsyncPanZoomController.h
new file mode 100644
index 0000000000..1f589df826
--- /dev/null
+++ b/gfx/layers/apz/src/AsyncPanZoomController.h
@@ -0,0 +1,1943 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_AsyncPanZoomController_h
+#define mozilla_layers_AsyncPanZoomController_h
+
+#include "mozilla/layers/GeckoContentController.h"
+#include "mozilla/layers/RepaintRequest.h"
+#include "mozilla/layers/SampleTime.h"
+#include "mozilla/layers/ScrollbarData.h"
+#include "mozilla/layers/ZoomConstraints.h"
+#include "mozilla/Atomics.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/EventForwards.h"
+#include "mozilla/Monitor.h"
+#include "mozilla/RecursiveMutex.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/ScrollTypes.h"
+#include "mozilla/StaticPrefs_apz.h"
+#include "mozilla/UniquePtr.h"
+#include "InputData.h"
+#include "Axis.h" // for Axis, Side, etc.
+#include "ExpectedGeckoMetrics.h"
+#include "FlingAccelerator.h"
+#include "InputQueue.h"
+#include "APZUtils.h"
+#include "LayersTypes.h"
+#include "mozilla/gfx/Matrix.h"
+#include "nsRegion.h"
+#include "nsTArray.h"
+#include "PotentialCheckerboardDurationTracker.h"
+#include "RecentEventsBuffer.h" // for RecentEventsBuffer
+#include "SampledAPZCState.h"
+
+#include <iosfwd>
+
+namespace mozilla {
+
+namespace ipc {
+
+class SharedMemoryBasic;
+
+} // namespace ipc
+
+namespace wr {
+struct SampledScrollOffset;
+} // namespace wr
+
+namespace layers {
+
+class AsyncDragMetrics;
+class APZCTreeManager;
+struct ScrollableLayerGuid;
+class CompositorController;
+class GestureEventListener;
+struct AsyncTransform;
+class AsyncPanZoomAnimation;
+class StackScrollerFlingAnimation;
+template <typename FlingPhysics>
+class GenericFlingAnimation;
+class AndroidFlingPhysics;
+class DesktopFlingPhysics;
+class InputBlockState;
+struct FlingHandoffState;
+class TouchBlockState;
+class PanGestureBlockState;
+class OverscrollHandoffChain;
+struct OverscrollHandoffState;
+class StateChangeNotificationBlocker;
+class CheckerboardEvent;
+class OverscrollEffectBase;
+class WidgetOverscrollEffect;
+class GenericOverscrollEffect;
+class AndroidSpecificState;
+struct KeyboardScrollAction;
+struct ZoomTarget;
+
+namespace apz {
+struct AsyncScrollThumbTransformer;
+}
+
+// Base class for grouping platform-specific APZC state variables.
+class PlatformSpecificStateBase {
+ public:
+ virtual ~PlatformSpecificStateBase() = default;
+ virtual AndroidSpecificState* AsAndroidSpecificState() { return nullptr; }
+ // PLPPI = "ParentLayer pixels per (Screen) inch"
+ virtual AsyncPanZoomAnimation* CreateFlingAnimation(
+ AsyncPanZoomController& aApzc, const FlingHandoffState& aHandoffState,
+ float aPLPPI);
+ virtual UniquePtr<VelocityTracker> CreateVelocityTracker(Axis* aAxis);
+
+ static void InitializeGlobalState() {}
+};
+
+/*
+ * Represents a transform from the ParentLayer coordinate space of an APZC
+ * to the ParentLayer coordinate space of its parent APZC.
+ * Each layer along the way contributes to the transform. We track
+ * contributions that are perspective transforms separately, as sometimes
+ * these require special handling.
+ */
+struct AncestorTransform {
+ gfx::Matrix4x4 mTransform;
+ gfx::Matrix4x4 mPerspectiveTransform;
+
+ AncestorTransform() = default;
+
+ AncestorTransform(const gfx::Matrix4x4& aTransform,
+ bool aTransformIsPerspective) {
+ (aTransformIsPerspective ? mPerspectiveTransform : mTransform) = aTransform;
+ }
+
+ AncestorTransform(const gfx::Matrix4x4& aTransform,
+ const gfx::Matrix4x4& aPerspectiveTransform)
+ : mTransform(aTransform), mPerspectiveTransform(aPerspectiveTransform) {}
+
+ gfx::Matrix4x4 CombinedTransform() const {
+ return mTransform * mPerspectiveTransform;
+ }
+
+ bool ContainsPerspectiveTransform() const {
+ return !mPerspectiveTransform.IsIdentity();
+ }
+
+ gfx::Matrix4x4 GetPerspectiveTransform() const {
+ return mPerspectiveTransform;
+ }
+
+ friend AncestorTransform operator*(const AncestorTransform& aA,
+ const AncestorTransform& aB) {
+ return AncestorTransform{
+ aA.mTransform * aB.mTransform,
+ aA.mPerspectiveTransform * aB.mPerspectiveTransform};
+ }
+};
+
+// Flags returned by AsyncPanZoomController::ArePointerEventsConsumable().
+// See the function for more details.
+struct PointerEventsConsumableFlags {
+ // The APZC has room to pan or zoom in response to the touch event.
+ bool mHasRoom = false;
+
+ // The panning or zooming is allowed by the touch-action property.
+ bool mAllowedByTouchAction = false;
+
+ bool IsConsumable() const { return mHasRoom && mAllowedByTouchAction; }
+ friend bool operator==(const PointerEventsConsumableFlags& aLhs,
+ const PointerEventsConsumableFlags& aRhs);
+ friend std::ostream& operator<<(std::ostream& aOut,
+ const PointerEventsConsumableFlags& aFlags);
+};
+
+/**
+ * Controller for all panning and zooming logic. Any time a user input is
+ * detected and it must be processed in some way to affect what the user sees,
+ * it goes through here. Listens for any input event from InputData and can
+ * optionally handle WidgetGUIEvent-derived touch events, but this must be done
+ * on the main thread. Note that this class completely cross-platform.
+ *
+ * Input events originate on the UI thread of the platform that this runs on,
+ * and are then sent to this class. This class processes the event in some way;
+ * for example, a touch move will usually lead to a panning of content (though
+ * of course there are exceptions, such as if content preventDefaults the event,
+ * or if the target frame is not scrollable). The compositor interacts with this
+ * class by locking it and querying it for the current transform matrix based on
+ * the panning and zooming logic that was invoked on the UI thread.
+ *
+ * Currently, each outer DOM window (i.e. a website in a tab, but not any
+ * subframes) has its own AsyncPanZoomController. In the future, to support
+ * asynchronously scrolled subframes, we want to have one AsyncPanZoomController
+ * per frame.
+ */
+class AsyncPanZoomController {
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(AsyncPanZoomController)
+
+ typedef mozilla::MonitorAutoLock MonitorAutoLock;
+ typedef mozilla::gfx::Matrix4x4 Matrix4x4;
+ typedef mozilla::layers::RepaintRequest::ScrollOffsetUpdateType
+ RepaintUpdateType;
+
+ public:
+ enum GestureBehavior {
+ // The platform code is responsible for forwarding gesture events here. We
+ // will not attempt to generate gesture events from MultiTouchInputs.
+ DEFAULT_GESTURES,
+ // An instance of GestureEventListener is used to detect gestures. This is
+ // handled completely internally within this class.
+ USE_GESTURE_DETECTOR
+ };
+
+ /**
+ * Gets the DPI from the tree manager.
+ */
+ float GetDPI() const;
+
+ /**
+ * Constant describing the tolerance in distance we use, multiplied by the
+ * device DPI, before we start panning the screen. This is to prevent us from
+ * accidentally processing taps as touch moves, and from very short/accidental
+ * touches moving the screen.
+ * Note: It's an abuse of the 'Coord' class to use it to represent a 2D
+ * distance, but it's the closest thing we currently have.
+ */
+ ScreenCoord GetTouchStartTolerance() const;
+ /**
+ * Same as GetTouchStartTolerance, but the tolerance for how far the touch
+ * has to move before it starts allowing touchmove events to be dispatched
+ * to content, for non-scrollable content.
+ */
+ ScreenCoord GetTouchMoveTolerance() const;
+ /**
+ * Same as GetTouchStartTolerance, but the tolerance for how close the second
+ * tap has to be to the first tap in order to be counted as part of a
+ * multi-tap gesture (double-tap or one-touch-pinch).
+ */
+ ScreenCoord GetSecondTapTolerance() const;
+
+ AsyncPanZoomController(LayersId aLayersId, APZCTreeManager* aTreeManager,
+ const RefPtr<InputQueue>& aInputQueue,
+ GeckoContentController* aController,
+ GestureBehavior aGestures = DEFAULT_GESTURES);
+
+ // --------------------------------------------------------------------------
+ // These methods must only be called on the gecko thread.
+ //
+
+ /**
+ * Read the various prefs and do any global initialization for all APZC
+ * instances. This must be run on the gecko thread before any APZC instances
+ * are actually used for anything meaningful.
+ */
+ static void InitializeGlobalState();
+
+ // --------------------------------------------------------------------------
+ // These methods must only be called on the controller/UI thread.
+ //
+
+ /**
+ * Kicks an animation to zoom to a rect. This may be either a zoom out or zoom
+ * in. The actual animation is done on the sampler thread after being set
+ * up.
+ */
+ void ZoomToRect(const ZoomTarget& aZoomTarget, const uint32_t aFlags);
+
+ /**
+ * Updates any zoom constraints contained in the <meta name="viewport"> tag.
+ */
+ void UpdateZoomConstraints(const ZoomConstraints& aConstraints);
+
+ /**
+ * Schedules a runnable to run on the controller/UI thread at some time
+ * in the future.
+ */
+ void PostDelayedTask(already_AddRefed<Runnable> aTask, int aDelayMs);
+
+ // --------------------------------------------------------------------------
+ // These methods must only be called on the sampler thread.
+ //
+
+ /**
+ * Advances any animations currently running to the given timestamp.
+ * This may be called multiple times with the same timestamp.
+ *
+ * The return value indicates whether or not any currently running animation
+ * should continue. If true, the compositor should schedule another composite.
+ */
+ bool AdvanceAnimations(const SampleTime& aSampleTime);
+
+ bool UpdateAnimation(const RecursiveMutexAutoLock& aProofOfLock,
+ const SampleTime& aSampleTime,
+ nsTArray<RefPtr<Runnable>>* aOutDeferredTasks);
+
+ // --------------------------------------------------------------------------
+ // These methods must only be called on the updater thread.
+ //
+
+ /**
+ * A shadow layer update has arrived. |aScrollMetdata| is the new
+ * ScrollMetadata for the container layer corresponding to this APZC.
+ * |aIsFirstPaint| is a flag passed from the shadow
+ * layers code indicating that the scroll metadata being sent with this call
+ * are the initial metadata and the initial paint of the frame has just
+ * happened.
+ */
+ void NotifyLayersUpdated(const ScrollMetadata& aScrollMetadata,
+ bool aIsFirstPaint, bool aThisLayerTreeUpdated);
+
+ /**
+ * The platform implementation must set the compositor controller so that we
+ * can request composites.
+ */
+ void SetCompositorController(CompositorController* aCompositorController);
+
+ // --------------------------------------------------------------------------
+ // These methods can be called from any thread.
+ //
+
+ /**
+ * Shut down the controller/UI thread state and prepare to be
+ * deleted (which may happen from any thread).
+ */
+ void Destroy();
+
+ /**
+ * Returns true if Destroy() has already been called on this APZC instance.
+ */
+ bool IsDestroyed() const;
+
+ /**
+ * Returns the transform to take something from the coordinate space of the
+ * last thing we know gecko painted, to the coordinate space of the last thing
+ * we asked gecko to paint. In cases where that last request has not yet been
+ * processed, this is needed to transform input events properly into a space
+ * gecko will understand.
+ */
+ Matrix4x4 GetTransformToLastDispatchedPaint(
+ const AsyncTransformComponents& aComponents = LayoutAndVisual) const;
+
+ /**
+ * Returns the number of CSS pixels of checkerboard according to the metrics
+ * in this APZC. The argument provided by the caller is the composition bounds
+ * of this APZC, additionally clipped by the composition bounds of any
+ * ancestor APZCs, accounting for all the async transforms.
+ */
+ uint32_t GetCheckerboardMagnitude(
+ const ParentLayerRect& aClippedCompositionBounds) const;
+
+ /**
+ * Report the number of CSSPixel-milliseconds of checkerboard to telemetry.
+ * See GetCheckerboardMagnitude for documentation of the
+ * aClippedCompositionBounds argument that needs to be provided by the caller.
+ */
+ void ReportCheckerboard(const SampleTime& aSampleTime,
+ const ParentLayerRect& aClippedCompositionBounds);
+
+ /**
+ * Flush any active checkerboard report that's in progress. This basically
+ * pretends like any in-progress checkerboard event has terminated, and pushes
+ * out the report to the checkerboard reporting service and telemetry. If the
+ * checkerboard event has not really finished, it will start a new event
+ * on the next composite.
+ */
+ void FlushActiveCheckerboardReport();
+
+ /**
+ * See documentation on corresponding method in APZPublicUtils.h
+ */
+ static gfx::IntSize GetDisplayportAlignmentMultiplier(
+ const ScreenSize& aBaseSize);
+
+ enum class ZoomInProgress {
+ No,
+ Yes,
+ };
+
+ /**
+ * Recalculates the displayport. Ideally, this should paint an area bigger
+ * than the composite-to dimensions so that when you scroll down, you don't
+ * checkerboard immediately. This includes a bunch of logic, including
+ * algorithms to bias painting in the direction of the velocity and other
+ * such things.
+ */
+ static const ScreenMargin CalculatePendingDisplayPort(
+ const FrameMetrics& aFrameMetrics, const ParentLayerPoint& aVelocity,
+ ZoomInProgress aZoomInProgress);
+
+ nsEventStatus HandleDragEvent(const MouseInput& aEvent,
+ const AsyncDragMetrics& aDragMetrics,
+ OuterCSSCoord aInitialThumbPos);
+
+ /**
+ * Handler for events which should not be intercepted by the touch listener.
+ */
+ nsEventStatus HandleInputEvent(
+ const InputData& aEvent,
+ const ScreenToParentLayerMatrix4x4& aTransformToApzc);
+
+ /**
+ * Handler for gesture events.
+ * Currently some gestures are detected in GestureEventListener that calls
+ * APZC back through this handler in order to avoid recursive calls to
+ * APZC::HandleInputEvent() which is supposed to do the work for
+ * ReceiveInputEvent().
+ */
+ nsEventStatus HandleGestureEvent(const InputData& aEvent);
+
+ /**
+ * Start autoscrolling this APZC, anchored at the provided location.
+ */
+ void StartAutoscroll(const ScreenPoint& aAnchorLocation);
+
+ /**
+ * Stop autoscrolling this APZC.
+ */
+ void StopAutoscroll();
+
+ /**
+ * Populates the provided object (if non-null) with the scrollable guid of
+ * this apzc.
+ */
+ void GetGuid(ScrollableLayerGuid* aGuidOut) const;
+
+ /**
+ * Returns the scrollable guid of this apzc.
+ */
+ ScrollableLayerGuid GetGuid() const;
+
+ /**
+ * Returns true if this APZC instance is for the layer identified by the guid.
+ */
+ bool Matches(const ScrollableLayerGuid& aGuid);
+
+ /**
+ * Returns true if the tree manager of this APZC is the same as the one
+ * passed in.
+ */
+ bool HasTreeManager(const APZCTreeManager* aTreeManager) const;
+
+ void StartAnimation(AsyncPanZoomAnimation* aAnimation);
+
+ /**
+ * Cancels any currently running animation.
+ * aFlags is a bit-field to provide specifics of how to cancel the animation.
+ * See CancelAnimationFlags.
+ */
+ void CancelAnimation(CancelAnimationFlags aFlags = Default);
+
+ /**
+ * Clear any overscroll on this APZC.
+ */
+ void ClearOverscroll();
+ void ClearPhysicalOverscroll();
+
+ /**
+ * Returns whether this APZC is for an element marked with the 'scrollgrab'
+ * attribute.
+ */
+ bool HasScrollgrab() const { return mScrollMetadata.GetHasScrollgrab(); }
+
+ /**
+ * Returns whether this APZC has scroll snap points.
+ */
+ bool HasScrollSnapping() const {
+ return mScrollMetadata.GetSnapInfo().HasScrollSnapping();
+ }
+
+ /**
+ * Returns whether this APZC has room to be panned (in any direction).
+ */
+ bool IsPannable() const;
+
+ /**
+ * Returns whether this APZC represents a scroll info layer.
+ */
+ bool IsScrollInfoLayer() const;
+
+ /**
+ * Returns true if the APZC has been flung with a velocity greater than the
+ * stop-on-tap fling velocity threshold (which is pref-controlled).
+ */
+ bool IsFlingingFast() const;
+
+ /**
+ * Returns whether this APZC is currently autoscrolling.
+ */
+ bool IsAutoscroll() const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ return mState == AUTOSCROLL;
+ }
+
+ /**
+ * Returns the identifier of the touch in the last touch event processed by
+ * this APZC. This should only be called when the last touch event contained
+ * only one touch.
+ */
+ int32_t GetLastTouchIdentifier() const;
+
+ /**
+ * Returns the matrix that transforms points from global screen space into
+ * this APZC's ParentLayer space.
+ * To respect the lock ordering, mRecursiveMutex must NOT be held when calling
+ * this function (since this function acquires the tree lock).
+ */
+ ScreenToParentLayerMatrix4x4 GetTransformToThis() const;
+
+ /**
+ * Convert the vector |aVector|, rooted at the point |aAnchor|, from
+ * this APZC's ParentLayer coordinates into screen coordinates.
+ * The anchor is necessary because with 3D tranforms, the location of the
+ * vector can affect the result of the transform.
+ * To respect the lock ordering, mRecursiveMutex must NOT be held when calling
+ * this function (since this function acquires the tree lock).
+ */
+ ScreenPoint ToScreenCoordinates(const ParentLayerPoint& aVector,
+ const ParentLayerPoint& aAnchor) const;
+
+ /**
+ * Convert the vector |aVector|, rooted at the point |aAnchor|, from
+ * screen coordinates into this APZC's ParentLayer coordinates.
+ * The anchor is necessary because with 3D tranforms, the location of the
+ * vector can affect the result of the transform.
+ * To respect the lock ordering, mRecursiveMutex must NOT be held when calling
+ * this function (since this function acquires the tree lock).
+ */
+ ParentLayerPoint ToParentLayerCoordinates(const ScreenPoint& aVector,
+ const ScreenPoint& aAnchor) const;
+
+ /**
+ * Same as above, but uses an ExternalPoint as the anchor.
+ */
+ ParentLayerPoint ToParentLayerCoordinates(const ScreenPoint& aVector,
+ const ExternalPoint& aAnchor) const;
+
+ /**
+ * Combines an offset defined as an external point, with a window-relative
+ * offset to give an absolute external point.
+ */
+ static ExternalPoint ToExternalPoint(const ExternalPoint& aScreenOffset,
+ const ScreenPoint& aScreenPoint);
+
+ /**
+ * Gets a vector where the head is the given point, and the tail is
+ * the touch start position.
+ */
+ ScreenPoint PanVector(const ExternalPoint& aPos) const;
+
+ // Return whether or not a wheel event will be able to scroll in either
+ // direction.
+ bool CanScroll(const InputData& aEvent) const;
+
+ // Return the directions in which this APZC allows handoff (as governed by
+ // overscroll-behavior).
+ ScrollDirections GetAllowedHandoffDirections() const;
+
+ // Return the directions in which this APZC allows overscrolling.
+ ScrollDirections GetOverscrollableDirections() const;
+
+ // Return whether or not a scroll delta will be able to scroll in either
+ // direction.
+ bool CanScroll(const ParentLayerPoint& aDelta) const;
+
+ // Return whether or not a scroll delta will be able to scroll in either
+ // direction with wheel.
+ bool CanScrollWithWheel(const ParentLayerPoint& aDelta) const;
+
+ // Return whether or not there is room to scroll this APZC
+ // in the given direction.
+ bool CanScroll(ScrollDirection aDirection) const;
+
+ // Return the directions in which this APZC is able to scroll.
+ SideBits ScrollableDirections() const;
+
+ // Return true if there is room to scroll along with moving the dynamic
+ // toolbar.
+ //
+ // NOTE: This function should be used only for the root content APZC.
+ bool CanVerticalScrollWithDynamicToolbar() const;
+
+ // Return true if there is room to scroll downwards.
+ bool CanScrollDownwards() const;
+
+ /**
+ * Convert a point on the scrollbar from this APZC's ParentLayer coordinates
+ * to OuterCSS coordinates relative to the beginning of the scroll track.
+ * Only the component in the direction of scrolling is returned.
+ */
+ OuterCSSCoord ConvertScrollbarPoint(const ParentLayerPoint& aScrollbarPoint,
+ const ScrollbarData& aThumbData) const;
+
+ void NotifyMozMouseScrollEvent(const nsString& aString) const;
+
+ bool OverscrollBehaviorAllowsSwipe() const;
+
+ //|Metrics()| and |Metrics() const| are getter functions that both return
+ // mScrollMetadata.mMetrics
+
+ const FrameMetrics& Metrics() const;
+ FrameMetrics& Metrics();
+
+ /**
+ * Get the GeckoViewMetrics to be sent to Gecko for the current composite.
+ */
+ GeckoViewMetrics GetGeckoViewMetrics() const;
+
+ // Helper function to compare root frame metrics and update them
+ // Returns true when the metrics have changed and were updated.
+ bool UpdateRootFrameMetricsIfChanged(GeckoViewMetrics& aMetrics);
+
+ // Returns the cached current frame time.
+ SampleTime GetFrameTime() const;
+
+ bool IsZero(const ParentLayerPoint& aPoint) const;
+ bool IsZero(ParentLayerCoord aCoord) const;
+
+ bool FuzzyGreater(ParentLayerCoord aCoord1, ParentLayerCoord aCoord2) const;
+
+ private:
+ // Get whether the horizontal content of the honoured target of auto-dir
+ // scrolling starts from right to left. If you don't know of auto-dir
+ // scrolling or what a honoured target means,
+ // @see mozilla::WheelDeltaAdjustmentStrategy
+ bool IsContentOfHonouredTargetRightToLeft(bool aHonoursRoot) const;
+
+ protected:
+ // Protected destructor, to discourage deletion outside of Release():
+ virtual ~AsyncPanZoomController();
+
+ /**
+ * Helper method for touches beginning. Sets everything up for panning and any
+ * multitouch gestures.
+ */
+ nsEventStatus OnTouchStart(const MultiTouchInput& aEvent);
+
+ /**
+ * Helper method for touches moving. Does any transforms needed when panning.
+ */
+ nsEventStatus OnTouchMove(const MultiTouchInput& aEvent);
+
+ /**
+ * Helper method for touches ending. Redraws the screen if necessary and does
+ * any cleanup after a touch has ended.
+ */
+ nsEventStatus OnTouchEnd(const MultiTouchInput& aEvent);
+
+ /**
+ * Helper method for touches being cancelled. Treated roughly the same as a
+ * touch ending (OnTouchEnd()).
+ */
+ nsEventStatus OnTouchCancel(const MultiTouchInput& aEvent);
+
+ /**
+ * Helper method for scales beginning. Distinct from the OnTouch* handlers in
+ * that this implies some outside implementation has determined that the user
+ * is pinching.
+ */
+ nsEventStatus OnScaleBegin(const PinchGestureInput& aEvent);
+
+ /**
+ * Helper method for scaling. As the user moves their fingers when pinching,
+ * this changes the scale of the page.
+ */
+ nsEventStatus OnScale(const PinchGestureInput& aEvent);
+
+ /**
+ * Helper method for scales ending. Redraws the screen if necessary and does
+ * any cleanup after a scale has ended.
+ */
+ nsEventStatus OnScaleEnd(const PinchGestureInput& aEvent);
+
+ /**
+ * Helper methods for handling pan events.
+ */
+ nsEventStatus OnPanMayBegin(const PanGestureInput& aEvent);
+ nsEventStatus OnPanCancelled(const PanGestureInput& aEvent);
+ nsEventStatus OnPanBegin(const PanGestureInput& aEvent);
+ enum class FingersOnTouchpad {
+ Yes,
+ No,
+ };
+ nsEventStatus OnPan(const PanGestureInput& aEvent,
+ FingersOnTouchpad aFingersOnTouchpad);
+ nsEventStatus OnPanEnd(const PanGestureInput& aEvent);
+ nsEventStatus OnPanMomentumStart(const PanGestureInput& aEvent);
+ nsEventStatus OnPanMomentumEnd(const PanGestureInput& aEvent);
+ nsEventStatus HandleEndOfPan();
+ nsEventStatus OnPanInterrupted(const PanGestureInput& aEvent);
+
+ /**
+ * Helper methods for handling scroll wheel events.
+ */
+ nsEventStatus OnScrollWheel(const ScrollWheelInput& aEvent);
+
+ /**
+ * Gets the scroll wheel delta's values in parent-layer pixels from the
+ * original delta's values of a wheel input.
+ */
+ ParentLayerPoint GetScrollWheelDelta(const ScrollWheelInput& aEvent) const;
+
+ /**
+ * This function is like GetScrollWheelDelta(aEvent).
+ * The difference is the four added parameters provide values as alternatives
+ * to the original wheel input's delta values, so |aEvent|'s delta values are
+ * ignored in this function, we only use some other member variables and
+ * functions of |aEvent|.
+ */
+ ParentLayerPoint GetScrollWheelDelta(const ScrollWheelInput& aEvent,
+ double aDeltaX, double aDeltaY,
+ double aMultiplierX,
+ double aMultiplierY) const;
+
+ /**
+ * This deleted function is used for:
+ * 1. avoiding accidental implicit value type conversions of input delta
+ * values when callers intend to call the above function;
+ * 2. decoupling the manual relationship between the delta value type and the
+ * above function. If by any chance the defined delta value type in
+ * ScrollWheelInput has changed, this will automatically result in build
+ * time failure, so we can learn of it the first time and accordingly
+ * redefine those parameters' value types in the above function.
+ */
+ template <typename T>
+ ParentLayerPoint GetScrollWheelDelta(ScrollWheelInput&, T, T, T, T) = delete;
+
+ /**
+ * Helper methods for handling keyboard events.
+ */
+ nsEventStatus OnKeyboard(const KeyboardInput& aEvent);
+
+ CSSPoint GetKeyboardDestination(const KeyboardScrollAction& aAction) const;
+
+ // Returns the corresponding ScrollSnapFlags for the given |aAction|.
+ // See https://drafts.csswg.org/css-scroll-snap/#scroll-types
+ ScrollSnapFlags GetScrollSnapFlagsForKeyboardAction(
+ const KeyboardScrollAction& aAction) const;
+
+ /**
+ * Helper methods for long press gestures.
+ */
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY
+ nsEventStatus OnLongPress(const TapGestureInput& aEvent);
+ nsEventStatus OnLongPressUp(const TapGestureInput& aEvent);
+
+ /**
+ * Helper method for single tap gestures.
+ */
+ nsEventStatus OnSingleTapUp(const TapGestureInput& aEvent);
+
+ /**
+ * Helper method for a single tap confirmed.
+ */
+ nsEventStatus OnSingleTapConfirmed(const TapGestureInput& aEvent);
+
+ /**
+ * Helper method for double taps.
+ */
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY
+ nsEventStatus OnDoubleTap(const TapGestureInput& aEvent);
+
+ /**
+ * Helper method for double taps where the double-tap gesture is disabled.
+ */
+ nsEventStatus OnSecondTap(const TapGestureInput& aEvent);
+
+ /**
+ * Helper method to cancel any gesture currently going to Gecko. Used
+ * primarily when a user taps the screen over some clickable content but then
+ * pans down instead of letting go (i.e. to cancel a previous touch so that a
+ * new one can properly take effect.
+ */
+ nsEventStatus OnCancelTap(const TapGestureInput& aEvent);
+
+ /**
+ * The following five methods modify the scroll offset. For the APZC
+ * representing the RCD-RSF, they also recalculate the offset of the layout
+ * viewport.
+ */
+
+ /**
+ * Scroll the scroll frame to an X,Y offset.
+ */
+ void SetVisualScrollOffset(const CSSPoint& aOffset);
+
+ /**
+ * Scroll the scroll frame to an X,Y offset, clamping the resulting scroll
+ * offset to the scroll range.
+ */
+ void ClampAndSetVisualScrollOffset(const CSSPoint& aOffset);
+
+ /**
+ * Scroll the scroll frame by an X,Y offset.
+ * The resulting scroll offset is not clamped to the scrollable rect;
+ * the caller must ensure it stays within range.
+ */
+ void ScrollBy(const CSSPoint& aOffset);
+
+ /**
+ * Scroll the scroll frame by an X,Y offset, clamping the resulting
+ * scroll offset to the scroll range.
+ */
+ void ScrollByAndClamp(const CSSPoint& aOffset);
+
+ /**
+ * Scales the viewport by an amount (note that it multiplies this scale in to
+ * the current scale, it doesn't set it to |aScale|). Also considers a focus
+ * point so that the page zooms inward/outward from that point.
+ */
+ void ScaleWithFocus(float aScale, const CSSPoint& aFocus);
+
+ /**
+ * Schedules a composite on the compositor thread.
+ */
+ void ScheduleComposite();
+
+ /**
+ * Schedules a composite, and if enough time has elapsed since the last
+ * paint, a paint.
+ */
+ void ScheduleCompositeAndMaybeRepaint();
+
+ /**
+ * Gets the start point of the current touch.
+ * This only makes sense if a touch is currently happening and OnTouchMove()
+ * or the equivalent for pan gestures is being invoked.
+ */
+ ParentLayerPoint PanStart() const;
+
+ /**
+ * Gets a vector of the velocities of each axis.
+ */
+ const ParentLayerPoint GetVelocityVector() const;
+
+ /**
+ * Sets the velocities of each axis.
+ */
+ void SetVelocityVector(const ParentLayerPoint& aVelocityVector);
+
+ /**
+ * Gets the first touch point from a MultiTouchInput. This gets only
+ * the first one and assumes the rest are either missing or not relevant.
+ */
+ ParentLayerPoint GetFirstTouchPoint(const MultiTouchInput& aEvent);
+
+ /**
+ * Gets the relevant point in the event
+ * (eg. first touch, or pinch focus point) of the given InputData.
+ */
+ ExternalPoint GetExternalPoint(const InputData& aEvent);
+
+ /**
+ * Gets the relevant point in the event, in external screen coordinates.
+ */
+ ExternalPoint GetFirstExternalTouchPoint(const MultiTouchInput& aEvent);
+
+ /**
+ * Gets the amount by which this APZC is overscrolled along both axes.
+ */
+ ParentLayerPoint GetOverscrollAmount() const;
+
+ private:
+ // Internal version of GetOverscrollAmount() which does not set
+ // the test async properties.
+ ParentLayerPoint GetOverscrollAmountInternal() const;
+
+ protected:
+ /**
+ * Returns SideBits where this APZC is overscrolled.
+ */
+ SideBits GetOverscrollSideBits() const;
+
+ /**
+ * Restore the amount by which this APZC is overscrolled along both axes
+ * to the specified amount. This is for test-related use; overscrolling
+ * as a result of user input should happen via OverscrollBy().
+ */
+ void RestoreOverscrollAmount(const ParentLayerPoint& aOverscroll);
+
+ /**
+ * Sets the panning state basing on the pan direction angle and current
+ * touch-action value.
+ */
+ void HandlePanningWithTouchAction(double angle);
+
+ /**
+ * Sets the panning state ignoring the touch action value.
+ */
+ void HandlePanning(double angle);
+
+ /**
+ * Update the panning state and axis locks.
+ */
+ void HandlePanningUpdate(const ScreenPoint& aDelta);
+
+ /**
+ * Set and update the pinch lock
+ */
+ void HandlePinchLocking(const PinchGestureInput& aEvent);
+
+ /**
+ * Sets up anything needed for panning. This takes us out of the "TOUCHING"
+ * state and starts actually panning us. We provide the physical pixel
+ * position of the start point so that the pan gesture is calculated
+ * regardless of if the window/GeckoView moved during the pan.
+ */
+ nsEventStatus StartPanning(const ExternalPoint& aStartPoint,
+ const TimeStamp& aEventTime);
+
+ /**
+ * Wrapper for Axis::UpdateWithTouchAtDevicePoint(). Calls this function for
+ * both axes and factors in the time delta from the last update.
+ */
+ void UpdateWithTouchAtDevicePoint(const MultiTouchInput& aEvent);
+
+ /**
+ * Does any panning required due to a new touch event.
+ */
+ void TrackTouch(const MultiTouchInput& aEvent);
+
+ /**
+ * Register the start of a touch or pan gesture at the given position and
+ * time.
+ */
+ void StartTouch(const ParentLayerPoint& aPoint, TimeStamp aTimestamp);
+
+ /**
+ * Register the end of a touch or pan gesture at the given time.
+ */
+ void EndTouch(TimeStamp aTimestamp, Axis::ClearAxisLock aClearAxisLock);
+
+ /**
+ * Utility function to send updated FrameMetrics to Gecko so that it can paint
+ * the displayport area. Calls into GeckoContentController to do the actual
+ * work. This call will use the current metrics. If this function is called
+ * from a non-main thread, it will redispatch itself to the main thread, and
+ * use the latest metrics during the redispatch.
+ */
+ void RequestContentRepaint(
+ RepaintUpdateType aUpdateType = RepaintUpdateType::eUserAction);
+
+ /**
+ * Send Metrics() to Gecko to trigger a repaint. This function may filter
+ * duplicate calls with the same metrics. This function must be called on the
+ * main thread.
+ */
+ void RequestContentRepaint(const ParentLayerPoint& aVelocity,
+ const ScreenMargin& aDisplayportMargins,
+ RepaintUpdateType aUpdateType);
+
+ /**
+ * Gets the current frame metrics. This is *not* the Gecko copy stored in the
+ * layers code.
+ */
+ const FrameMetrics& GetFrameMetrics() const;
+
+ /**
+ * Gets the current scroll metadata. This is *not* the Gecko copy stored in
+ * the layers code/
+ */
+ const ScrollMetadata& GetScrollMetadata() const;
+
+ /**
+ * Gets the pointer to the apzc tree manager. All the access to tree manager
+ * should be made via this method and not via private variable since this
+ * method ensures that no lock is set.
+ */
+ APZCTreeManager* GetApzcTreeManager() const;
+
+ void AssertOnSamplerThread() const;
+ void AssertOnUpdaterThread() const;
+
+ /**
+ * Convert ScreenPoint relative to the screen to LayoutDevicePoint relative
+ * to the parent document. This excludes the transient compositor transform.
+ * NOTE: This must be converted to LayoutDevicePoint relative to the child
+ * document before sending over IPC to a child process.
+ */
+ Maybe<LayoutDevicePoint> ConvertToGecko(const ScreenIntPoint& aPoint);
+
+ enum AxisLockMode {
+ FREE, /* No locking at all */
+ STANDARD, /* Default axis locking mode that remains locked until pan ends */
+ STICKY, /* Allow lock to be broken, with hysteresis */
+ DOMINANT_AXIS, /* Only allow movement on one axis */
+ };
+
+ static AxisLockMode GetAxisLockMode();
+
+ bool UsingStatefulAxisLock() const;
+
+ enum PinchLockMode {
+ PINCH_FREE, /* No locking at all */
+ PINCH_STANDARD, /* Default pinch locking mode that remains locked until
+ pinch gesture ends*/
+ PINCH_STICKY, /* Allow lock to be broken, with hysteresis */
+ };
+
+ static PinchLockMode GetPinchLockMode();
+
+ // Helper function for OnSingleTapUp(), OnSingleTapConfirmed(), and
+ // OnLongPressUp().
+ nsEventStatus GenerateSingleTap(GeckoContentController::TapType aType,
+ const ScreenIntPoint& aPoint,
+ mozilla::Modifiers aModifiers);
+
+ // Common processing at the end of a touch block.
+ void OnTouchEndOrCancel();
+
+ LayersId mLayersId;
+ RefPtr<CompositorController> mCompositorController;
+
+ /* Access to the following two fields is protected by the mRefPtrMonitor,
+ since they are accessed on the UI thread but can be cleared on the
+ updater thread. */
+ RefPtr<GeckoContentController> mGeckoContentController;
+ RefPtr<GestureEventListener> mGestureEventListener;
+ mutable Monitor mRefPtrMonitor MOZ_UNANNOTATED;
+
+ // This is a raw pointer to avoid introducing a reference cycle between
+ // AsyncPanZoomController and APZCTreeManager. Since these objects don't
+ // live on the main thread, we can't use the cycle collector with them.
+ // The APZCTreeManager owns the lifetime of the APZCs, so nulling this
+ // pointer out in Destroy() will prevent accessing deleted memory.
+ Atomic<APZCTreeManager*> mTreeManager;
+
+ /* Utility functions that return a addrefed pointer to the corresponding
+ * fields. */
+ already_AddRefed<GeckoContentController> GetGeckoContentController() const;
+ already_AddRefed<GestureEventListener> GetGestureEventListener() const;
+
+ PlatformSpecificStateBase* GetPlatformSpecificState();
+
+ /**
+ * Convenience functions to get the corresponding fields of mZoomContraints
+ * while holding mRecursiveMutex.
+ */
+ bool ZoomConstraintsAllowZoom() const;
+ bool ZoomConstraintsAllowDoubleTapZoom() const;
+
+ protected:
+ // Both |mScrollMetadata| and |mLastContentPaintMetrics| are protected by the
+ // monitor. Do not read from or modify them without locking.
+ ScrollMetadata mScrollMetadata;
+
+ // Protects |mScrollMetadata|, |mLastContentPaintMetrics|, |mState| and
+ // |mLastSnapTargetIds|. Before manipulating |mScrollMetadata|,
+ // |mLastContentPaintMetrics| or |mLastSnapTargetIds| the monitor should be
+ // held. When setting |mState|, either the SetState() function can be used, or
+ // the monitor can be held and then |mState| updated.
+ // IMPORTANT: See the note about lock ordering at the top of
+ // APZCTreeManager.h. This is mutable to allow entering it from 'const'
+ // methods; doing otherwise would significantly limit what methods could be
+ // 'const'.
+ // FIXME: Please keep in mind that due to some existing coupled relationships
+ // among the class members, we should be aware of indirect usage of the
+ // monitor-protected members. That is, although this monitor isn't required to
+ // be held before manipulating non-protected class members, some functions on
+ // those members might indirectly manipulate the protected members; in such
+ // cases, the monitor should still be held. Let's take mX.CanScroll for
+ // example:
+ // Axis::CanScroll(ParentLayerCoord) calls Axis::CanScroll() which calls
+ // Axis::GetPageLength() which calls Axis::GetFrameMetrics() which calls
+ // AsyncPanZoomController::GetFrameMetrics(), therefore, this monitor should
+ // be held before calling the CanScroll function of |mX| and |mY|. These
+ // coupled relationships bring us the burden of taking care of when the
+ // monitor should be held, so they should be decoupled in the future.
+ mutable RecursiveMutex mRecursiveMutex MOZ_UNANNOTATED;
+
+ private:
+ // Metadata of the container layer corresponding to this APZC. This is
+ // stored here so that it is accessible from the UI/controller thread.
+ // These are the metrics at last content paint, the most recent
+ // values we were notified of in NotifyLayersUpdate(). Since it represents
+ // the Gecko state, it should be used as a basis for untransformation when
+ // sending messages back to Gecko.
+ ScrollMetadata mLastContentPaintMetadata;
+ FrameMetrics& mLastContentPaintMetrics; // for convenience, refers to
+ // mLastContentPaintMetadata.mMetrics
+ // The last content repaint request.
+ RepaintRequest mLastPaintRequestMetrics;
+ // The metrics that we expect content to have. This is updated when we
+ // request a content repaint, and when we receive a shadow layers update.
+ // This allows us to transform events into Gecko's coordinate space.
+ ExpectedGeckoMetrics mExpectedGeckoMetrics;
+
+ // This holds important state from the Metrics() at previous times
+ // SampleCompositedAsyncTransform() was called. This will always have at least
+ // one item. mRecursiveMutex must be held when using or modifying this member.
+ // Samples should be inserted to the "back" of the deque and extracted from
+ // the "front".
+ std::deque<SampledAPZCState> mSampledState;
+
+ // Groups state variables that are specific to a platform.
+ // Initialized on first use.
+ UniquePtr<PlatformSpecificStateBase> mPlatformSpecificState;
+
+ // This flag is set to true when we are in a axis-locked pan as a result of
+ // the touch-action CSS property.
+ bool mPanDirRestricted;
+
+ // This flag is set to true when we are in a pinch-locked state. ie: user
+ // is performing a two-finger pan rather than a pinch gesture
+ bool mPinchLocked;
+
+ // Stores the pinch events that occured within a given timeframe. Used to
+ // calculate the focusChange and spanDistance within a fixed timeframe.
+ // RecentEventsBuffer is not threadsafe. Should only be accessed on the
+ // controller thread.
+ RecentEventsBuffer<PinchGestureInput> mPinchEventBuffer;
+
+ // Most up-to-date constraints on zooming. These should always be reasonable
+ // values; for example, allowing a min zoom of 0.0 can cause very bad things
+ // to happen. Hold mRecursiveMutex when accessing this.
+ ZoomConstraints mZoomConstraints;
+
+ // The last time the compositor has sampled the content transform for this
+ // frame.
+ SampleTime mLastSampleTime;
+
+ // The last sample time at which we submitted a checkerboarding report.
+ SampleTime mLastCheckerboardReport;
+
+ // Stores the previous focus point if there is a pinch gesture happening. Used
+ // to allow panning by moving multiple fingers (thus moving the focus point).
+ ParentLayerPoint mLastZoomFocus;
+
+ // Stores the previous zoom level at which we last sent a ScaleGestureComplete
+ // notification.
+ CSSToParentLayerScale mLastNotifiedZoom;
+
+ // Accessing mAnimation needs to be protected by mRecursiveMutex
+ RefPtr<AsyncPanZoomAnimation> mAnimation;
+
+ UniquePtr<OverscrollEffectBase> mOverscrollEffect;
+
+ // Zoom animation id, used for zooming in WebRender. This should only be
+ // set on the APZC instance for the root content document (i.e. the one we
+ // support zooming on), and is only used if WebRender is enabled. The
+ // animation id itself refers to the transform animation id that was set on
+ // the stacking context in the WR display list. By changing the transform
+ // associated with this id, we can adjust the scaling that WebRender applies,
+ // thereby controlling the zoom.
+ Maybe<uint64_t> mZoomAnimationId;
+
+ // Position on screen where user first put their finger down.
+ ExternalPoint mStartTouch;
+
+ // Accessing mScrollPayload needs to be protected by mRecursiveMutex
+ Maybe<CompositionPayload> mScrollPayload;
+
+ // Representing sampled scroll offset generation, this value is bumped up
+ // every time this APZC sampled new scroll offset.
+ APZScrollGeneration mScrollGeneration;
+
+ friend class Axis;
+
+ public:
+ Maybe<CompositionPayload> NotifyScrollSampling();
+
+ /**
+ * Invoke |callable|, passing |mLastContentPaintMetrics| as argument,
+ * while holding the APZC lock required to access |mLastContentPaintMetrics|.
+ * This allows code outside of an AsyncPanZoomController method implementation
+ * to access |mLastContentPaintMetrics| without having to make a copy of it.
+ * Passes through the return value of |callable|.
+ */
+ template <typename Callable>
+ auto CallWithLastContentPaintMetrics(const Callable& callable) const
+ -> decltype(callable(mLastContentPaintMetrics)) {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ return callable(mLastContentPaintMetrics);
+ }
+
+ void SetZoomAnimationId(const Maybe<uint64_t>& aZoomAnimationId);
+ Maybe<uint64_t> GetZoomAnimationId() const;
+
+ /* ===================================================================
+ * The functions and members in this section are used to expose
+ * the current async transform state to callers.
+ */
+ public:
+ /**
+ * Allows consumers of async transforms to specify for what purpose they are
+ * using the async transform:
+ *
+ * |eForHitTesting| is intended for hit-testing and other uses that need
+ * the most up-to-date transform, reflecting all events
+ * that have been processed so far, even if the transform
+ * is not yet reflected visually.
+ * |eForCompositing| is intended for the transform that should be reflected
+ * visually.
+ *
+ * For example, if an APZC has metrics with the mForceDisableApz flag set,
+ * then the |eForCompositing| async transform will be empty, while the
+ * |eForHitTesting| async transform will reflect processed input events
+ * regardless of mForceDisableApz.
+ */
+ enum AsyncTransformConsumer {
+ eForHitTesting,
+ eForCompositing,
+ };
+
+ /**
+ * Get the current scroll offset of the scrollable frame corresponding
+ * to this APZC, including the effects of any asynchronous panning and
+ * zooming, in ParentLayer pixels.
+ */
+ ParentLayerPoint GetCurrentAsyncScrollOffset(
+ AsyncTransformConsumer aMode) const;
+
+ /**
+ * Get the current visual viewport of the scrollable frame corresponding
+ * to this APZC, including the effects of any asynchronous panning and
+ * zooming, in CSS pixels.
+ */
+ CSSRect GetCurrentAsyncVisualViewport(AsyncTransformConsumer aMode) const;
+
+ /**
+ * Return a visual effect that reflects this apzc's
+ * overscrolled state, if any.
+ */
+ AsyncTransformComponentMatrix GetOverscrollTransform(
+ AsyncTransformConsumer aMode) const;
+
+ /**
+ * Returns the incremental transformation corresponding to the async pan/zoom
+ * in progress. That is, when this transform is multiplied with the layer's
+ * existing transform, it will make the layer appear with the desired pan/zoom
+ * amount.
+ * The transform can have both scroll and zoom components; the caller can
+ * request just one or the other, or both, via the |aComponents| parameter.
+ * When only the eLayout component is requested, the returned translation
+ * should really be a LayerPoint, rather than a ParentLayerPoint, as it will
+ * not be scaled by the asynchronous zoom.
+ * |aMode| specifies whether the async transform is queried for the purpose of
+ * hit testing (eHitTesting) in which case the latest values from |Metrics()|
+ * are used, or for compositing (eCompositing) in which case a sampled value
+ * from |mSampledState| is used.
+ * |aSampleIndex| specifies which sample in |mSampledState| to use.
+ */
+ AsyncTransform GetCurrentAsyncTransform(
+ AsyncTransformConsumer aMode,
+ AsyncTransformComponents aComponents = LayoutAndVisual,
+ std::size_t aSampleIndex = 0) const;
+
+ /**
+ * Returns the same transform as GetCurrentAsyncTransform(), but includes
+ * any transform due to axis over-scroll.
+ */
+ AsyncTransformComponentMatrix GetCurrentAsyncTransformWithOverscroll(
+ AsyncTransformConsumer aMode,
+ AsyncTransformComponents aComponents = LayoutAndVisual,
+ std::size_t aSampleIndex = 0) const;
+
+ AutoTArray<wr::SampledScrollOffset, 2> GetSampledScrollOffsets() const;
+
+ /**
+ * Returns the "zoom" bits of the transform. This includes both the rasterized
+ * (layout device to layer scale) and async (layer scale to parent layer
+ * scale) components of the zoom.
+ */
+ LayoutDeviceToParentLayerScale GetCurrentPinchZoomScale(
+ AsyncTransformConsumer aMode) const;
+
+ ParentLayerRect GetCompositionBounds() const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ return mScrollMetadata.GetMetrics().GetCompositionBounds();
+ }
+
+ LayoutDeviceToLayerScale GetCumulativeResolution() const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ return mScrollMetadata.GetMetrics().GetCumulativeResolution();
+ }
+
+ // Returns the delta for the given InputData.
+ ParentLayerPoint GetDeltaForEvent(const InputData& aEvent) const;
+
+ /**
+ * Get the current scroll range of the scrollable frame coreesponding to this
+ * APZC.
+ */
+ CSSRect GetCurrentScrollRangeInCssPixels() const;
+
+ private:
+ /**
+ * Advances to the next sample, if there is one, the list of sampled states
+ * stored in mSampledState. This will make the result of
+ * |GetCurrentAsyncTransform(eForCompositing)| and similar functions reflect
+ * the async scroll offset and zoom of the next sample. See also
+ * SampleCompositedAsyncTransform which creates the samples.
+ */
+ void AdvanceToNextSample();
+
+ /**
+ * Samples the composited async transform, storing the result into
+ * mSampledState. This will make the result of
+ * |GetCurrentAsyncTransform(eForCompositing)| and similar functions reflect
+ * the async scroll offset and zoom stored in |Metrics()| when the sample
+ * is activated via some future call to |AdvanceToNextSample|.
+ *
+ * Returns true if the newly sampled value is different from the last
+ * sampled value.
+ */
+ bool SampleCompositedAsyncTransform(
+ const RecursiveMutexAutoLock& aProofOfLock);
+
+ /**
+ * Updates the sample at the front of mSampledState with the latest
+ * metrics. This makes the result of
+ * |GetCurrentAsyncTransform(eForCompositing)| reflect the current Metrics().
+ */
+ void ResampleCompositedAsyncTransform(
+ const RecursiveMutexAutoLock& aProofOfLock);
+
+ /*
+ * Helper functions to query the async layout viewport, scroll offset, and
+ * zoom either directly from |Metrics()|, or from cached variables that
+ * store the required value from the last time it was sampled by calling
+ * SampleCompositedAsyncTransform(), depending on who is asking.
+ */
+ CSSRect GetEffectiveLayoutViewport(AsyncTransformConsumer aMode,
+ const RecursiveMutexAutoLock& aProofOfLock,
+ std::size_t aSampleIndex = 0) const;
+ CSSPoint GetEffectiveScrollOffset(AsyncTransformConsumer aMode,
+ const RecursiveMutexAutoLock& aProofOfLock,
+ std::size_t aSampleIndex = 0) const;
+ CSSToParentLayerScale GetEffectiveZoom(
+ AsyncTransformConsumer aMode, const RecursiveMutexAutoLock& aProofOfLock,
+ std::size_t aSampleIndex = 0) const;
+
+ /**
+ * Returns the visible portion of the content scrolled by this APZC, in
+ * CSS pixels. The caller must have acquired the mRecursiveMutex lock.
+ */
+ CSSRect GetVisibleRect(const RecursiveMutexAutoLock& aProofOfLock) const;
+
+ /**
+ * Returns a pair of displacements both in logical/physical units for
+ * |aEvent|.
+ */
+ std::tuple<ParentLayerPoint, ScreenPoint> GetDisplacementsForPanGesture(
+ const PanGestureInput& aEvent);
+
+ private:
+ friend class AutoApplyAsyncTestAttributes;
+
+ bool SuppressAsyncScrollOffset() const;
+
+ /**
+ * Applies |mTestAsyncScrollOffset| and |mTestAsyncZoom| to this
+ * AsyncPanZoomController. Calls |SampleCompositedAsyncTransform| to ensure
+ * that the GetCurrentAsync* functions consider the test offset and zoom in
+ * their computations.
+ */
+ void ApplyAsyncTestAttributes(const RecursiveMutexAutoLock& aProofOfLock);
+
+ /**
+ * Sets this AsyncPanZoomController's FrameMetrics to |aPrevFrameMetrics| and
+ * calls |SampleCompositedAsyncTransform| to unapply any test values applied
+ * by |ApplyAsyncTestAttributes|.
+ */
+ void UnapplyAsyncTestAttributes(const RecursiveMutexAutoLock& aProofOfLock,
+ const FrameMetrics& aPrevFrameMetrics,
+ const ParentLayerPoint& aPrevOverscroll);
+
+ /* ===================================================================
+ * The functions and members in this section are used to manage
+ * the state that tracks what this APZC is doing with the input events.
+ */
+ protected:
+ enum PanZoomState {
+ NOTHING, /* no touch-start events received */
+ FLING, /* all touches removed, but we're still scrolling page */
+ TOUCHING, /* one touch-start event received */
+
+ PANNING, /* panning the frame */
+ PANNING_LOCKED_X, /* touch-start followed by move (i.e. panning with axis
+ lock) X axis */
+ PANNING_LOCKED_Y, /* as above for Y axis */
+
+ PAN_MOMENTUM, /* like PANNING, but controlled by momentum PanGestureInput
+ events */
+
+ PINCHING, /* nth touch-start, where n > 1. this mode allows pan and zoom */
+ ANIMATING_ZOOM, /* animated zoom to a new rect */
+ OVERSCROLL_ANIMATION, /* Spring-based animation used to relieve overscroll
+ once the finger is lifted. */
+ SMOOTH_SCROLL, /* Smooth scrolling to destination, with physics
+ controlled by prefs specific to the scroll origin. */
+ SMOOTHMSD_SCROLL, /* SmoothMSD scrolling to destination. Used by
+ CSSOM-View smooth scroll-behavior */
+ WHEEL_SCROLL, /* Smooth scrolling to a destination for a wheel event. */
+ KEYBOARD_SCROLL, /* Smooth scrolling to a destination for a keyboard event.
+ */
+ AUTOSCROLL, /* Autoscroll animation. */
+ SCROLLBAR_DRAG /* Async scrollbar drag. */
+ };
+ // This is in theory protected by |mRecursiveMutex|; that is, it should be
+ // held whenever this is updated. In practice though... see bug 897017.
+ PanZoomState mState;
+
+ AxisX mX;
+ AxisY mY;
+
+ /**
+ * Returns wheter the given input state is a user pan-gesture.
+ *
+ * Note: momentum pan gesture states are not considered a panning state.
+ */
+ static bool IsPanningState(PanZoomState aState);
+
+ /**
+ * Returns wheter a delayed transform end is queued.
+ */
+ bool IsDelayedTransformEndSet();
+
+ /**
+ * Returns wheter a delayed transform end is queued.
+ */
+ void SetDelayedTransformEnd(bool aDelayedTransformEnd);
+
+ /**
+ * Returns whether the specified PanZoomState does not need to be reset when
+ * a scroll offset update is processed.
+ */
+ static bool CanHandleScrollOffsetUpdate(PanZoomState aState);
+
+ /**
+ * Determine whether a main-thread scroll offset update should result in
+ * a call to CancelAnimation() (which interrupts in-progress animations and
+ * gestures).
+ *
+ * If the update is a relative update, |aRelativeDelta| contains its amount.
+ * If the update is not a relative update, GetMetrics() should already reflect
+ * the new offset at the time of the call.
+ */
+ bool ShouldCancelAnimationForScrollUpdate(
+ const Maybe<CSSPoint>& aRelativeDelta);
+
+ private:
+ friend class StateChangeNotificationBlocker;
+ /**
+ * A counter of how many StateChangeNotificationBlockers are active.
+ * A non-zero count will prevent state change notifications from
+ * being dispatched. Only code that holds mRecursiveMutex should touch this.
+ */
+ int mNotificationBlockers;
+
+ /**
+ * Helper to set the current state, without content controller events
+ * for the state change. This is useful in cases where the content
+ * controller events may need to be delayed.
+ */
+ PanZoomState SetStateNoContentControllerDispatch(PanZoomState aNewState);
+
+ /**
+ * Helper to set the current state. Holds mRecursiveMutex before actually
+ * setting it and fires content controller events based on state changes.
+ * Always set the state using this call, do not set it directly.
+ */
+ void SetState(PanZoomState aNewState);
+ /**
+ * Helper for getting the current state which acquires mRecursiveMutex
+ * before accessing the field.
+ */
+ PanZoomState GetState() const;
+ /**
+ * Fire content controller notifications about state changes, assuming no
+ * StateChangeNotificationBlocker has been activated.
+ */
+ void DispatchStateChangeNotification(PanZoomState aOldState,
+ PanZoomState aNewState);
+ /**
+ * Internal helpers for checking general state of this apzc.
+ */
+ bool IsInTransformingState() const;
+ static bool IsTransformingState(PanZoomState aState);
+
+ /* ===================================================================
+ * The functions and members in this section are used to manage
+ * blocks of touch events and the state needed to deal with content
+ * listeners.
+ */
+ public:
+ /**
+ * Flush a repaint request if one is needed, without throttling it with the
+ * paint throttler.
+ */
+ void FlushRepaintForNewInputBlock();
+
+ /**
+ * Given an input event and the touch block it belongs to, check if the
+ * event can lead to a panning/zooming behavior.
+ * This is used for logic related to the pointer events spec (figuring out
+ * when to dispatch the pointercancel event), as well as an input to the
+ * computation of the APZHandledResult for the event (used on Android to
+ * govern dynamic toolbar and pull-to-refresh behaviour).
+ */
+ PointerEventsConsumableFlags ArePointerEventsConsumable(
+ TouchBlockState* aBlock, const MultiTouchInput& aInput);
+
+ /**
+ * Clear internal state relating to touch input handling.
+ */
+ void ResetTouchInputState();
+
+ /**
+ Clear internal state relating to pan gesture input handling.
+ */
+ void ResetPanGestureInputState();
+
+ /**
+ * Gets a ref to the input queue that is shared across the entire tree
+ * manager.
+ */
+ const RefPtr<InputQueue>& GetInputQueue() const;
+
+ private:
+ void CancelAnimationAndGestureState();
+
+ RefPtr<InputQueue> mInputQueue;
+ InputBlockState* GetCurrentInputBlock() const;
+ TouchBlockState* GetCurrentTouchBlock() const;
+ bool HasReadyTouchBlock() const;
+
+ PanGestureBlockState* GetCurrentPanGestureBlock() const;
+ PinchGestureBlockState* GetCurrentPinchGestureBlock() const;
+
+ private:
+ /* ===================================================================
+ * The functions and members in this section are used to manage
+ * fling animations, smooth scroll animations, and overscroll
+ * during a fling or smooth scroll.
+ */
+ public:
+ /**
+ * Attempt a fling with the velocity specified in |aHandoffState|.
+ * |aHandoffState.mIsHandoff| should be true iff. the fling was handed off
+ * from a previous APZC, and determines whether acceleration is applied
+ * to the fling.
+ * We only accept the fling in the direction(s) in which we are pannable.
+ * Returns the "residual velocity", i.e. the portion of
+ * |aHandoffState.mVelocity| that this APZC did not consume.
+ */
+ ParentLayerPoint AttemptFling(const FlingHandoffState& aHandoffState);
+
+ ParentLayerPoint AdjustHandoffVelocityForOverscrollBehavior(
+ ParentLayerPoint& aHandoffVelocity) const;
+
+ private:
+ friend class StackScrollerFlingAnimation;
+ friend class AutoscrollAnimation;
+ template <typename FlingPhysics>
+ friend class GenericFlingAnimation;
+ friend class AndroidFlingPhysics;
+ friend class DesktopFlingPhysics;
+ friend class OverscrollAnimation;
+ friend class SmoothMsdScrollAnimation;
+ friend class GenericScrollAnimation;
+ friend class WheelScrollAnimation;
+ friend class ZoomAnimation;
+
+ friend class GenericOverscrollEffect;
+ friend class WidgetOverscrollEffect;
+ friend struct apz::AsyncScrollThumbTransformer;
+
+ FlingAccelerator mFlingAccelerator;
+
+ // Indicates if the repaint-during-pinch timer is currently set
+ bool mPinchPaintTimerSet;
+
+ // Indicates a delayed transform end notification is queued, and the
+ // transform-end timer is currently set. mRecursiveMutex must be held
+ // when using or modifying this member.
+ bool mDelayedTransformEnd;
+
+ // Deal with overscroll resulting from a fling animation. This is only ever
+ // called on APZC instances that were actually performing a fling.
+ // The overscroll is handled by trying to hand the fling off to an APZC
+ // later in the handoff chain, or if there are no takers, continuing the
+ // fling and entering an overscrolled state.
+ void HandleFlingOverscroll(
+ const ParentLayerPoint& aVelocity, SideBits aOverscrollSideBits,
+ const RefPtr<const OverscrollHandoffChain>& aOverscrollHandoffChain,
+ const RefPtr<const AsyncPanZoomController>& aScrolledApzc);
+
+ void HandleSmoothScrollOverscroll(const ParentLayerPoint& aVelocity,
+ SideBits aOverscrollSideBits);
+
+ // Start an overscroll animation with the given initial velocity.
+ void StartOverscrollAnimation(const ParentLayerPoint& aVelocity,
+ SideBits aOverscrollSideBits);
+
+ // Start a smooth-scrolling animation to the given destination, with physics
+ // based on the prefs for the indicated origin.
+ void SmoothScrollTo(const CSSPoint& aDestination,
+ const ScrollOrigin& aOrigin);
+
+ // Start a smooth-scrolling animation to the given destination, with MSD
+ // physics that is suited for scroll-snapping.
+ void SmoothMsdScrollTo(CSSSnapTarget&& aDestination,
+ ScrollTriggeredByScript aTriggeredByScript);
+
+ // Returns whether overscroll is allowed during an event.
+ bool AllowScrollHandoffInCurrentBlock() const;
+
+ // Invoked by the pinch repaint timer.
+ void DoDelayedRequestContentRepaint();
+
+ // Invoked by the on pan-end handler to ensure that scrollend is only
+ // fired once when a momentum pan or scroll snap is triggered as a
+ // result of the pan gesture.
+ void DoDelayedTransformEndNotification(PanZoomState aOldState);
+
+ // Compute the number of ParentLayer pixels per (Screen) inch at the given
+ // point and in the given direction.
+ float ComputePLPPI(ParentLayerPoint aPoint,
+ ParentLayerPoint aDirection) const;
+
+ Maybe<CSSPoint> GetCurrentAnimationDestination(
+ const RecursiveMutexAutoLock& aProofOfLock) const;
+
+ /* ===================================================================
+ * The functions and members in this section are used to make ancestor chains
+ * out of APZC instances. These chains can only be walked or manipulated
+ * while holding the lock in the associated APZCTreeManager instance.
+ */
+ public:
+ void SetParent(AsyncPanZoomController* aParent) { mParent = aParent; }
+
+ AsyncPanZoomController* GetParent() const { return mParent; }
+
+ /* Returns true if there is no APZC higher in the tree with the same
+ * layers id.
+ */
+ bool HasNoParentWithSameLayersId() const {
+ return !mParent || (mParent->mLayersId != mLayersId);
+ }
+
+ bool IsRootForLayersId() const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ return mScrollMetadata.IsLayersIdRoot();
+ }
+
+ bool IsRootContent() const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ return Metrics().IsRootContent();
+ }
+
+ private:
+ // |mTreeManager| belongs in this section but it's declaration is a bit
+ // further above due to initialization-order constraints.
+
+ RefPtr<AsyncPanZoomController> mParent;
+
+ /* ===================================================================
+ * The functions and members in this section are used for scrolling,
+ * including handing off scroll to another APZC, and overscrolling.
+ */
+
+ ScrollableLayerGuid::ViewID GetScrollId() const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ return Metrics().GetScrollId();
+ }
+
+ public:
+ ScrollableLayerGuid::ViewID GetScrollHandoffParentId() const {
+ return mScrollMetadata.GetScrollParentId();
+ }
+
+ /**
+ * Attempt to scroll in response to a touch-move from |aStartPoint| to
+ * |aEndPoint|, which are in our (transformed) screen coordinates.
+ * Due to overscroll handling, there may not actually have been a touch-move
+ * at these points, but this function will scroll as if there had been.
+ * If this attempt causes overscroll (i.e. the layer cannot be scrolled
+ * by the entire amount requested), the overscroll is passed back to the
+ * tree manager via APZCTreeManager::DispatchScroll(). If the tree manager
+ * does not find an APZC further in the handoff chain to accept the
+ * overscroll, and this APZC is pannable, this APZC enters an overscrolled
+ * state.
+ * |aOverscrollHandoffChain| and |aOverscrollHandoffChainIndex| are used by
+ * the tree manager to keep track of which APZC to hand off the overscroll
+ * to; this function increments the chain and the index and passes it on to
+ * APZCTreeManager::DispatchScroll() in the event of overscroll.
+ * Returns true iff. this APZC, or an APZC further down the
+ * handoff chain, accepted the scroll (possibly entering an overscrolled
+ * state). If this returns false, the caller APZC knows that it should enter
+ * an overscrolled state itself if it can.
+ * aStartPoint and aEndPoint are modified depending on how much of the
+ * scroll gesture was consumed by APZCs in the handoff chain.
+ */
+ bool AttemptScroll(ParentLayerPoint& aStartPoint, ParentLayerPoint& aEndPoint,
+ OverscrollHandoffState& aOverscrollHandoffState);
+
+ void FlushRepaintForOverscrollHandoff();
+
+ /**
+ * If overscrolled, start a snap-back animation and return true. Even if not
+ * overscrolled, this function tries to snap back to if there's an applicable
+ * scroll snap point.
+ * Otherwise return false.
+ */
+ bool SnapBackIfOverscrolled();
+
+ /**
+ * NOTE: Similar to above but this function doesn't snap back to the scroll
+ * snap point.
+ */
+ bool SnapBackIfOverscrolledForMomentum(const ParentLayerPoint& aVelocity);
+
+ /**
+ * Build the chain of APZCs along which scroll will be handed off when
+ * this APZC receives input events.
+ *
+ * Notes on lifetime and const-correctness:
+ * - The returned handoff chain is |const|, to indicate that it cannot be
+ * changed after being built.
+ * - When passing the chain to a function that uses it without storing it,
+ * pass it by reference-to-const (as in |const OverscrollHandoffChain&|).
+ * - When storing the chain, store it by RefPtr-to-const (as in
+ * |RefPtr<const OverscrollHandoffChain>|). This ensures the chain is
+ * kept alive. Note that queueing a task that uses the chain as an
+ * argument constitutes storing, as the task may outlive its queuer.
+ * - When passing the chain to a function that will store it, pass it as
+ * |const RefPtr<const OverscrollHandoffChain>&|. This allows the
+ * function to copy it into the |RefPtr<const OverscrollHandoffChain>|
+ * that will store it, while avoiding an unnecessary copy (and thus
+ * AddRef() and Release()) when passing it.
+ */
+ RefPtr<const OverscrollHandoffChain> BuildOverscrollHandoffChain();
+
+ private:
+ /**
+ * A helper function for calling APZCTreeManager::DispatchScroll().
+ * Guards against the case where the APZC is being concurrently destroyed
+ * (and thus mTreeManager is being nulled out).
+ */
+ bool CallDispatchScroll(ParentLayerPoint& aStartPoint,
+ ParentLayerPoint& aEndPoint,
+ OverscrollHandoffState& aOverscrollHandoffState);
+
+ void RecordScrollPayload(const TimeStamp& aTimeStamp);
+
+ /**
+ * A helper function for overscrolling during panning. This is a wrapper
+ * around OverscrollBy() that also implements restrictions on entering
+ * overscroll based on the pan angle.
+ */
+ void OverscrollForPanning(ParentLayerPoint& aOverscroll,
+ const ScreenPoint& aPanDistance);
+
+ /**
+ * Try to overscroll by 'aOverscroll'.
+ * If we are pannable on a particular axis, that component of 'aOverscroll'
+ * is transferred to any existing overscroll.
+ */
+ void OverscrollBy(ParentLayerPoint& aOverscroll);
+
+ /* ===================================================================
+ * The functions and members in this section are used to maintain the
+ * area that this APZC instance is responsible for. This is used when
+ * hit-testing to see which APZC instance should handle touch events.
+ */
+ public:
+ void SetAncestorTransform(const AncestorTransform& aAncestorTransform) {
+ mAncestorTransform = aAncestorTransform;
+ }
+
+ Matrix4x4 GetAncestorTransform() const {
+ return mAncestorTransform.CombinedTransform();
+ }
+
+ bool AncestorTransformContainsPerspective() const {
+ return mAncestorTransform.ContainsPerspectiveTransform();
+ }
+
+ // Return the perspective transform component of the ancestor transform.
+ Matrix4x4 GetAncestorTransformPerspective() const {
+ return mAncestorTransform.GetPerspectiveTransform();
+ }
+
+ // Returns whether or not this apzc contains the given screen point within
+ // its composition bounds.
+ bool Contains(const ScreenIntPoint& aPoint) const;
+
+ bool IsInOverscrollGutter(const ScreenPoint& aHitTestPoint) const;
+ bool IsInOverscrollGutter(const ParentLayerPoint& aHitTestPoint) const;
+
+ bool IsOverscrolled() const;
+
+ bool IsOverscrollAnimationRunning() const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ return mState == OVERSCROLL_ANIMATION;
+ }
+
+ // IsPhysicallyOverscrolled() checks whether the APZC is overscrolled
+ // by an overscroll effect which applies a transform to the APZC's contents.
+ bool IsPhysicallyOverscrolled() const;
+
+ private:
+ bool IsInInvalidOverscroll() const;
+
+ public:
+ bool IsInPanningState() const;
+
+ // Returns whether being in the middle of a gesture. E.g., this APZC has
+ // started handling a pan gesture but hasn't yet received pan-end, etc.
+ bool IsInScrollingGesture() const;
+
+ private:
+ /* This is the cumulative CSS transform for all the layers from (and
+ * including) the parent APZC down to (but excluding) this one, and excluding
+ * any perspective transforms. */
+ AncestorTransform mAncestorTransform;
+
+ /* ===================================================================
+ * The functions and members in this section are used for testing
+ * and assertion purposes only.
+ */
+ public:
+ /**
+ * Gets whether this APZC has performed async key scrolling.
+ */
+ bool TestHasAsyncKeyScrolled() const { return mTestHasAsyncKeyScrolled; }
+
+ /**
+ * Set an extra offset for testing async scrolling.
+ */
+ void SetTestAsyncScrollOffset(const CSSPoint& aPoint);
+ /**
+ * Set an extra offset for testing async scrolling.
+ */
+ void SetTestAsyncZoom(const LayerToParentLayerScale& aZoom);
+
+ LayersId GetLayersId() const { return mLayersId; }
+
+ bool IsAsyncZooming() const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ return mState == PINCHING || mState == ANIMATING_ZOOM;
+ }
+
+ private:
+ // The timestamp of the latest touch start event.
+ TimeStamp mTouchStartTime;
+ // Used for interpolating touch events that cross the touch-start
+ // tolerance threshold.
+ struct TouchSample {
+ ExternalPoint mPosition;
+ TimeStamp mTimeStamp;
+ };
+ // Information about the latest touch event.
+ // This is only populated when we're in the TOUCHING state
+ // (and thus the last touch event has only one touch point).
+ TouchSample mLastTouch;
+ // The time duration between mTouchStartTime and the touchmove event that
+ // started the pan (the touchmove event that transitioned this APZC from the
+ // TOUCHING state to one of the PANNING* states). Only valid while this APZC
+ // is in a panning state.
+ TimeDuration mTouchStartRestingTimeBeforePan;
+ Maybe<ParentLayerCoord> mMinimumVelocityDuringPan;
+ // This variable needs to be protected by |mRecursiveMutex|.
+ ScrollSnapTargetIds mLastSnapTargetIds;
+ // Extra offset to add to the async scroll position for testing
+ CSSPoint mTestAsyncScrollOffset;
+ // Extra zoom to include in the aync zoom for testing
+ LayerToParentLayerScale mTestAsyncZoom;
+ uint8_t mTestAttributeAppliers;
+ // Flag to track whether or not this APZC has ever async key scrolled.
+ bool mTestHasAsyncKeyScrolled;
+
+ /* ===================================================================
+ * The functions and members in this section are used for checkerboard
+ * recording.
+ */
+ private:
+ // Helper function to update the in-progress checkerboard event, if any.
+ void UpdateCheckerboardEvent(const MutexAutoLock& aProofOfLock,
+ uint32_t aMagnitude);
+
+ // Mutex protecting mCheckerboardEvent
+ Mutex mCheckerboardEventLock MOZ_UNANNOTATED;
+ // This is created when this APZC instance is first included as part of a
+ // composite. If a checkerboard event takes place, this is destroyed at the
+ // end of the event, and a new one is created on the next composite.
+ UniquePtr<CheckerboardEvent> mCheckerboardEvent;
+ // This is used to track the total amount of time that we could reasonably
+ // be checkerboarding. Combined with other info, this allows us to
+ // meaningfully say how frequently users actually encounter checkerboarding.
+ PotentialCheckerboardDurationTracker mPotentialCheckerboardTracker;
+
+ /* ===================================================================
+ * The functions in this section are used for CSS scroll snapping.
+ */
+
+ // If moving |aStartPosition| by |aDelta| should trigger scroll snapping,
+ // adjust |aDelta| to reflect the snapping (that is, make it a delta that will
+ // take us to the desired snap point). The delta is interpreted as being
+ // relative to |aStartPosition|, and if a target snap point is found,
+ // |aStartPosition| is also updated, to the value of the snap point.
+ // |aUnit| affects the snapping behaviour (see ScrollSnapUtils::
+ // GetSnapPointForDestination).
+ // Returns true iff. a target snap point was found.
+ Maybe<CSSSnapTarget> MaybeAdjustDeltaForScrollSnapping(
+ ScrollUnit aUnit, ScrollSnapFlags aSnapFlags, ParentLayerPoint& aDelta,
+ CSSPoint& aStartPosition);
+
+ // A wrapper function of MaybeAdjustDeltaForScrollSnapping for
+ // ScrollWheelInput.
+ Maybe<CSSSnapTarget> MaybeAdjustDeltaForScrollSnappingOnWheelInput(
+ const ScrollWheelInput& aEvent, ParentLayerPoint& aDelta,
+ CSSPoint& aStartPosition);
+
+ Maybe<CSSSnapTarget> MaybeAdjustDestinationForScrollSnapping(
+ const KeyboardInput& aEvent, CSSPoint& aDestination,
+ ScrollSnapFlags aSnapFlags);
+
+ // Snap to a snap position nearby the current scroll position, if appropriate.
+ void ScrollSnap(ScrollSnapFlags aSnapFlags);
+
+ // Snap to a snap position nearby the destination predicted based on the
+ // current velocity, if appropriate.
+ void ScrollSnapToDestination();
+
+ // Snap to a snap position nearby the provided destination, if appropriate.
+ void ScrollSnapNear(const CSSPoint& aDestination, ScrollSnapFlags aSnapFlags);
+
+ // Find a snap point near |aDestination| that we should snap to.
+ // Returns the snap point if one was found, or an empty Maybe otherwise.
+ // |aUnit| affects the snapping behaviour (see ScrollSnapUtils::
+ // GetSnapPointForDestination). It should generally be determined by the
+ // type of event that's triggering the scroll.
+ Maybe<CSSSnapTarget> FindSnapPointNear(const CSSPoint& aDestination,
+ ScrollUnit aUnit,
+ ScrollSnapFlags aSnapFlags);
+
+ // If |aOriginalEvent| crosses the touch-start tolerance threshold, split it
+ // into two events: one that just reaches the threshold, and the remainder.
+ //
+ // |aPanThreshold| is the touch-start tolerance, and |aVectorLength| is
+ // the length of the vector from the touch-start position to |aOriginalEvent|.
+ // These values could be computed from |aOriginalEvent| but they are
+ // passed in for convenience since the caller also needs to compute them.
+ //
+ // |aExtPoint| is the position of |aOriginalEvent| in External coordinates,
+ // and in case of a split is modified by the function to reflect the position
+ // of of the first event. This is a workaround for the fact that recomputing
+ // the External position from the returned event would require a round-trip
+ // through |mScreenPoint| which is an integer.
+ Maybe<std::pair<MultiTouchInput, MultiTouchInput>> MaybeSplitTouchMoveEvent(
+ const MultiTouchInput& aOriginalEvent, ScreenCoord aPanThreshold,
+ float aVectorLength, ExternalPoint& aExtPoint);
+
+ friend std::ostream& operator<<(
+ std::ostream& aOut, const AsyncPanZoomController::PanZoomState& aState);
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_PanZoomController_h
diff --git a/gfx/layers/apz/src/AutoDirWheelDeltaAdjuster.h b/gfx/layers/apz/src/AutoDirWheelDeltaAdjuster.h
new file mode 100644
index 0000000000..187641514a
--- /dev/null
+++ b/gfx/layers/apz/src/AutoDirWheelDeltaAdjuster.h
@@ -0,0 +1,89 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef __mozilla_layers_AutoDirWheelDeltaAdjuster_h__
+#define __mozilla_layers_AutoDirWheelDeltaAdjuster_h__
+
+#include "Axis.h" // for AxisX, AxisY, Side
+#include "mozilla/WheelHandlingHelper.h" // for AutoDirWheelDeltaAdjuster
+
+namespace mozilla {
+namespace layers {
+
+/**
+ * About AutoDirWheelDeltaAdjuster:
+ * For an AutoDir wheel scroll, there's some situations where we should adjust a
+ * wheel event's delta values. AutoDirWheelDeltaAdjuster converts delta values
+ * for AutoDir scrolling. An AutoDir wheel scroll lets the user scroll a frame
+ * with only one scrollbar, using either a vertical or a horzizontal wheel.
+ * For more detail about the concept of AutoDir scrolling, see the comments in
+ * AutoDirWheelDeltaAdjuster.
+ *
+ * This is the APZ implementation of AutoDirWheelDeltaAdjuster.
+ */
+class MOZ_STACK_CLASS APZAutoDirWheelDeltaAdjuster final
+ : public AutoDirWheelDeltaAdjuster {
+ public:
+ /**
+ * @param aDeltaX DeltaX for a wheel event whose delta values will
+ * be adjusted upon calling adjust() when
+ * ShouldBeAdjusted() returns true.
+ * @param aDeltaY DeltaY for a wheel event, like DeltaX.
+ * @param aAxisX The X axis information provider for the current
+ * frame, such as whether the frame can be scrolled
+ * horizontally, leftwards or rightwards.
+ * @param aAxisY The Y axis information provider for the current
+ * frame, such as whether the frame can be scrolled
+ * vertically, upwards or downwards.
+ * @param aIsHorizontalContentRightToLeft
+ * Indicates whether the horizontal content starts
+ * at rightside. This value will decide which edge
+ * the adjusted scroll goes towards, in other words,
+ * it will decide the sign of the adjusted delta
+ * values). For detailed information, see
+ * IsHorizontalContentRightToLeft() in
+ * the base class AutoDirWheelDeltaAdjuster.
+ */
+ APZAutoDirWheelDeltaAdjuster(double& aDeltaX, double& aDeltaY,
+ const AxisX& aAxisX, const AxisY& aAxisY,
+ bool aIsHorizontalContentRightToLeft)
+ : AutoDirWheelDeltaAdjuster(aDeltaX, aDeltaY),
+ mAxisX(aAxisX),
+ mAxisY(aAxisY),
+ mIsHorizontalContentRightToLeft(aIsHorizontalContentRightToLeft) {}
+
+ private:
+ virtual bool CanScrollAlongXAxis() const override {
+ return mAxisX.CanScroll();
+ }
+ virtual bool CanScrollAlongYAxis() const override {
+ return mAxisY.CanScroll();
+ }
+ virtual bool CanScrollUpwards() const override {
+ return mAxisY.CanScrollTo(eSideTop);
+ }
+ virtual bool CanScrollDownwards() const override {
+ return mAxisY.CanScrollTo(eSideBottom);
+ }
+ virtual bool CanScrollLeftwards() const override {
+ return mAxisX.CanScrollTo(eSideLeft);
+ }
+ virtual bool CanScrollRightwards() const override {
+ return mAxisX.CanScrollTo(eSideRight);
+ }
+ virtual bool IsHorizontalContentRightToLeft() const override {
+ return mIsHorizontalContentRightToLeft;
+ }
+
+ const AxisX& mAxisX;
+ const AxisY& mAxisY;
+ bool mIsHorizontalContentRightToLeft;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // __mozilla_layers_AutoDirWheelDeltaAdjuster_h__
diff --git a/gfx/layers/apz/src/AutoscrollAnimation.cpp b/gfx/layers/apz/src/AutoscrollAnimation.cpp
new file mode 100644
index 0000000000..8d4b8fca10
--- /dev/null
+++ b/gfx/layers/apz/src/AutoscrollAnimation.cpp
@@ -0,0 +1,93 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "AutoscrollAnimation.h"
+
+#include <cmath> // for sqrtf()
+
+#include "AsyncPanZoomController.h"
+#include "APZCTreeManager.h"
+#include "FrameMetrics.h"
+#include "mozilla/Telemetry.h" // for Telemetry
+
+namespace mozilla {
+namespace layers {
+
+// Helper function for AutoscrollAnimation::DoSample().
+// Basically copied as-is from toolkit/actors/AutoScrollChild.jsm.
+static float Accelerate(ScreenCoord curr, ScreenCoord start) {
+ static const int speed = 12;
+ float val = (curr - start) / speed;
+ if (val > 1) {
+ return val * sqrtf(val) - 1;
+ }
+ if (val < -1) {
+ return val * sqrtf(-val) + 1;
+ }
+ return 0;
+}
+
+AutoscrollAnimation::AutoscrollAnimation(AsyncPanZoomController& aApzc,
+ const ScreenPoint& aAnchorLocation)
+ : mApzc(aApzc), mAnchorLocation(aAnchorLocation) {}
+
+bool AutoscrollAnimation::DoSample(FrameMetrics& aFrameMetrics,
+ const TimeDuration& aDelta) {
+ APZCTreeManager* treeManager = mApzc.GetApzcTreeManager();
+ if (!treeManager) {
+ return false;
+ }
+
+ ScreenPoint mouseLocation = treeManager->GetCurrentMousePosition();
+
+ // The implementation of this function closely mirrors that of its main-
+ // thread equivalent, the autoscrollLoop() function in
+ // toolkit/actors/AutoScrollChild.jsm.
+
+ // Avoid long jumps when the browser hangs for more than |maxTimeDelta| ms.
+ static const TimeDuration maxTimeDelta = TimeDuration::FromMilliseconds(100);
+ TimeDuration timeDelta = TimeDuration::Min(aDelta, maxTimeDelta);
+
+ float timeCompensation = timeDelta.ToMilliseconds() / 20;
+
+ // Notes:
+ // - The main-thread implementation rounds the scroll delta to an integer,
+ // and keeps track of the fractional part as an "error". It does this
+ // because it uses Window.scrollBy() or Element.scrollBy() to perform
+ // the scrolling, and those functions truncate the fractional part of
+ // the offset. APZ does no such truncation, so there's no need to keep
+ // track of the fractional part separately.
+ // - The Accelerate() function takes Screen coordinates as inputs, but
+ // its output is interpreted as CSS coordinates. This is intentional,
+ // insofar as autoscrollLoop() does the same thing.
+ CSSPoint scrollDelta{
+ Accelerate(mouseLocation.x, mAnchorLocation.x) * timeCompensation,
+ Accelerate(mouseLocation.y, mAnchorLocation.y) * timeCompensation};
+
+ mApzc.ScrollByAndClamp(scrollDelta);
+
+ // An autoscroll animation never ends of its own accord.
+ // It can be stopped in response to various input events, in which case
+ // AsyncPanZoomController::StopAutoscroll() will stop it via
+ // CancelAnimation().
+ return true;
+}
+
+void AutoscrollAnimation::Cancel(CancelAnimationFlags aFlags) {
+ // The cancellation was initiated by browser.js, so there's no need to
+ // notify it.
+ if (aFlags & TriggeredExternally) {
+ return;
+ }
+
+ if (RefPtr<GeckoContentController> controller =
+ mApzc.GetGeckoContentController()) {
+ controller->CancelAutoscroll(mApzc.GetGuid());
+ }
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/AutoscrollAnimation.h b/gfx/layers/apz/src/AutoscrollAnimation.h
new file mode 100644
index 0000000000..a37f6d473a
--- /dev/null
+++ b/gfx/layers/apz/src/AutoscrollAnimation.h
@@ -0,0 +1,42 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_AutocrollAnimation_h_
+#define mozilla_layers_AutocrollAnimation_h_
+
+#include "AsyncPanZoomAnimation.h"
+
+namespace mozilla {
+namespace layers {
+
+class AsyncPanZoomController;
+
+class AutoscrollAnimation : public AsyncPanZoomAnimation {
+ public:
+ AutoscrollAnimation(AsyncPanZoomController& aApzc,
+ const ScreenPoint& aAnchorLocation);
+
+ bool DoSample(FrameMetrics& aFrameMetrics,
+ const TimeDuration& aDelta) override;
+
+ bool HandleScrollOffsetUpdate(
+ const Maybe<CSSPoint>& aRelativeDelta) override {
+ // Autoscroll works using screen space coordinates, so there's no work we
+ // need to do to handle either a relative or an absolute scroll update.
+ return true;
+ }
+
+ void Cancel(CancelAnimationFlags aFlags) override;
+
+ private:
+ AsyncPanZoomController& mApzc;
+ ScreenPoint mAnchorLocation;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_AutoscrollAnimation_h_
diff --git a/gfx/layers/apz/src/Axis.cpp b/gfx/layers/apz/src/Axis.cpp
new file mode 100644
index 0000000000..f0842864d8
--- /dev/null
+++ b/gfx/layers/apz/src/Axis.cpp
@@ -0,0 +1,733 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "Axis.h"
+
+#include <math.h> // for fabsf, pow, powf
+#include <algorithm> // for max
+
+#include "APZCTreeManager.h" // for APZCTreeManager
+#include "AsyncPanZoomController.h" // for AsyncPanZoomController
+#include "FrameMetrics.h" // for FrameMetrics
+#include "SimpleVelocityTracker.h" // for FrameMetrics
+#include "mozilla/Attributes.h" // for final
+#include "mozilla/Preferences.h" // for Preferences
+#include "mozilla/gfx/Rect.h" // for RoundedIn
+#include "mozilla/layers/APZThreadUtils.h" // for AssertOnControllerThread
+#include "mozilla/mozalloc.h" // for operator new
+#include "nsMathUtils.h" // for NS_lround
+#include "nsPrintfCString.h" // for nsPrintfCString
+#include "nsThreadUtils.h" // for NS_DispatchToMainThread, etc
+#include "nscore.h" // for NS_IMETHOD
+
+static mozilla::LazyLogModule sApzAxsLog("apz.axis");
+#define AXIS_LOG(...) MOZ_LOG(sApzAxsLog, LogLevel::Debug, (__VA_ARGS__))
+
+namespace mozilla {
+namespace layers {
+
+bool FuzzyEqualsCoordinate(CSSCoord aValue1, CSSCoord aValue2) {
+ return FuzzyEqualsAdditive(aValue1, aValue2, COORDINATE_EPSILON) ||
+ FuzzyEqualsMultiplicative(aValue1, aValue2);
+}
+
+Axis::Axis(AsyncPanZoomController* aAsyncPanZoomController)
+ : mPos(0),
+ mVelocity(0.0f, "Axis::mVelocity"),
+ mAxisLocked(false),
+ mAsyncPanZoomController(aAsyncPanZoomController),
+ mOverscroll(0),
+ mMSDModel(0.0, 0.0, 0.0, StaticPrefs::apz_overscroll_spring_stiffness(),
+ StaticPrefs::apz_overscroll_damping()),
+ mVelocityTracker(mAsyncPanZoomController->GetPlatformSpecificState()
+ ->CreateVelocityTracker(this)) {}
+
+float Axis::ToLocalVelocity(float aVelocityInchesPerMs) const {
+ ScreenPoint velocity =
+ MakePoint(aVelocityInchesPerMs * mAsyncPanZoomController->GetDPI());
+ // Use ToScreenCoordinates() to convert a point rather than a vector by
+ // treating the point as a vector, and using (0, 0) as the anchor.
+ ScreenPoint panStart = mAsyncPanZoomController->ToScreenCoordinates(
+ mAsyncPanZoomController->PanStart(), ParentLayerPoint());
+ ParentLayerPoint localVelocity =
+ mAsyncPanZoomController->ToParentLayerCoordinates(velocity, panStart);
+ return localVelocity.Length();
+}
+
+void Axis::UpdateWithTouchAtDevicePoint(ParentLayerCoord aPos,
+ TimeStamp aTimestamp) {
+ // mVelocityTracker is controller-thread only
+ APZThreadUtils::AssertOnControllerThread();
+
+ mPos = aPos;
+
+ AXIS_LOG("%p|%s got position %f\n", mAsyncPanZoomController, Name(),
+ mPos.value);
+ if (Maybe<float> newVelocity =
+ mVelocityTracker->AddPosition(aPos, aTimestamp)) {
+ DoSetVelocity(mAxisLocked ? 0 : *newVelocity);
+ AXIS_LOG("%p|%s velocity from tracker is %f%s\n", mAsyncPanZoomController,
+ Name(), *newVelocity,
+ mAxisLocked ? ", but we are axis locked" : "");
+ }
+}
+
+void Axis::StartTouch(ParentLayerCoord aPos, TimeStamp aTimestamp) {
+ mStartPos = aPos;
+ mPos = aPos;
+ mVelocityTracker->StartTracking(aPos, aTimestamp);
+ mAxisLocked = false;
+}
+
+bool Axis::AdjustDisplacement(ParentLayerCoord aDisplacement,
+ ParentLayerCoord& aDisplacementOut,
+ ParentLayerCoord& aOverscrollAmountOut,
+ bool aForceOverscroll /* = false */) {
+ if (mAxisLocked) {
+ aOverscrollAmountOut = 0;
+ aDisplacementOut = 0;
+ return false;
+ }
+ if (aForceOverscroll) {
+ aOverscrollAmountOut = aDisplacement;
+ aDisplacementOut = 0;
+ return false;
+ }
+
+ ParentLayerCoord displacement = aDisplacement;
+
+ // First consume any overscroll in the opposite direction along this axis.
+ ParentLayerCoord consumedOverscroll = 0;
+ if (mOverscroll > 0 && aDisplacement < 0) {
+ consumedOverscroll = std::min(mOverscroll, -aDisplacement);
+ } else if (mOverscroll < 0 && aDisplacement > 0) {
+ consumedOverscroll = 0.f - std::min(-mOverscroll, aDisplacement);
+ }
+ mOverscroll -= consumedOverscroll;
+ displacement += consumedOverscroll;
+
+ if (consumedOverscroll != 0.0f) {
+ AXIS_LOG("%p|%s changed overscroll amount to %f\n", mAsyncPanZoomController,
+ Name(), mOverscroll.value);
+ }
+
+ // Split the requested displacement into an allowed displacement that does
+ // not overscroll, and an overscroll amount.
+ aOverscrollAmountOut = DisplacementWillOverscrollAmount(displacement);
+ if (aOverscrollAmountOut != 0.0f) {
+ // No need to have a velocity along this axis anymore; it won't take us
+ // anywhere, so we're just spinning needlessly.
+ AXIS_LOG("%p|%s has overscrolled, clearing velocity\n",
+ mAsyncPanZoomController, Name());
+ DoSetVelocity(0.0f);
+ displacement -= aOverscrollAmountOut;
+ }
+ aDisplacementOut = displacement;
+ return fabsf(consumedOverscroll) > EPSILON;
+}
+
+ParentLayerCoord Axis::ApplyResistance(
+ ParentLayerCoord aRequestedOverscroll) const {
+ // 'resistanceFactor' is a value between 0 and 1/16, which:
+ // - tends to 1/16 as the existing overscroll tends to 0
+ // - tends to 0 as the existing overscroll tends to the composition length
+ // The actual overscroll is the requested overscroll multiplied by this
+ // factor.
+ float resistanceFactor =
+ (1 - fabsf(GetOverscroll()) / GetCompositionLength()) / 16;
+ float result = resistanceFactor < 0 ? ParentLayerCoord(0)
+ : aRequestedOverscroll * resistanceFactor;
+ result = clamped(result, -8.0f, 8.0f);
+ return result;
+}
+
+void Axis::OverscrollBy(ParentLayerCoord aOverscroll) {
+ MOZ_ASSERT(CanScroll());
+ // We can get some spurious calls to OverscrollBy() with near-zero values
+ // due to rounding error. Ignore those (they might trip the asserts below.)
+ if (mAsyncPanZoomController->IsZero(aOverscroll)) {
+ return;
+ }
+ EndOverscrollAnimation();
+ aOverscroll = ApplyResistance(aOverscroll);
+ if (aOverscroll > 0) {
+#ifdef DEBUG
+ if (!IsScrolledToEnd()) {
+ nsPrintfCString message(
+ "composition end (%f) is not equal (within error) to page end (%f)\n",
+ GetCompositionEnd().value, GetPageEnd().value);
+ NS_ASSERTION(false, message.get());
+ MOZ_CRASH("GFX: Overscroll issue > 0");
+ }
+#endif
+ MOZ_ASSERT(mOverscroll >= 0);
+ } else if (aOverscroll < 0) {
+#ifdef DEBUG
+ if (!IsScrolledToStart()) {
+ nsPrintfCString message(
+ "composition origin (%f) is not equal (within error) to page origin "
+ "(%f)\n",
+ GetOrigin().value, GetPageStart().value);
+ NS_ASSERTION(false, message.get());
+ MOZ_CRASH("GFX: Overscroll issue < 0");
+ }
+#endif
+ MOZ_ASSERT(mOverscroll <= 0);
+ }
+ mOverscroll += aOverscroll;
+
+ AXIS_LOG("%p|%s changed overscroll amount to %f\n", mAsyncPanZoomController,
+ Name(), mOverscroll.value);
+}
+
+ParentLayerCoord Axis::GetOverscroll() const { return mOverscroll; }
+
+void Axis::RestoreOverscroll(ParentLayerCoord aOverscroll) {
+ mOverscroll = aOverscroll;
+}
+
+void Axis::StartOverscrollAnimation(float aVelocity) {
+ const float maxVelocity = StaticPrefs::apz_overscroll_max_velocity();
+ aVelocity = clamped(aVelocity / 2.0f, -maxVelocity, maxVelocity);
+ SetVelocity(aVelocity);
+ mMSDModel.SetPosition(mOverscroll);
+ // Convert velocity from ParentLayerCoords/millisecond to
+ // ParentLayerCoords/second.
+ mMSDModel.SetVelocity(DoGetVelocity() * 1000.0);
+
+ AXIS_LOG(
+ "%p|%s beginning overscroll animation with amount %f and velocity %f\n",
+ mAsyncPanZoomController, Name(), mOverscroll.value, DoGetVelocity());
+}
+
+void Axis::EndOverscrollAnimation() {
+ mMSDModel.SetPosition(0.0);
+ mMSDModel.SetVelocity(0.0);
+}
+
+bool Axis::SampleOverscrollAnimation(const TimeDuration& aDelta,
+ SideBits aOverscrollSideBits) {
+ mMSDModel.Simulate(aDelta);
+ mOverscroll = mMSDModel.GetPosition();
+
+ if (((aOverscrollSideBits & (SideBits::eTop | SideBits::eLeft)) &&
+ mOverscroll > 0) ||
+ ((aOverscrollSideBits & (SideBits::eBottom | SideBits::eRight)) &&
+ mOverscroll < 0)) {
+ // Stop the overscroll model immediately if it's going to get across the
+ // boundary.
+ mMSDModel.SetPosition(0.0);
+ mMSDModel.SetVelocity(0.0);
+ }
+
+ AXIS_LOG("%p|%s changed overscroll amount to %f\n", mAsyncPanZoomController,
+ Name(), mOverscroll.value);
+
+ if (mMSDModel.IsFinished(1.0)) {
+ // "Jump" to the at-rest state. The jump shouldn't be noticeable as the
+ // velocity and overscroll are already low.
+ AXIS_LOG("%p|%s oscillation dropped below threshold, going to rest\n",
+ mAsyncPanZoomController, Name());
+ ClearOverscroll();
+ DoSetVelocity(0);
+ return false;
+ }
+
+ // Otherwise, continue the animation.
+ return true;
+}
+
+bool Axis::IsOverscrollAnimationRunning() const {
+ return !mMSDModel.IsFinished(1.0);
+}
+
+bool Axis::IsOverscrollAnimationAlive() const {
+ // Unlike IsOverscrollAnimationRunning, check the position and the velocity to
+ // be sure that the animation has started but hasn't yet finished.
+ return mMSDModel.GetPosition() != 0.0 || mMSDModel.GetVelocity() != 0.0;
+}
+
+bool Axis::IsOverscrolled() const { return mOverscroll != 0.f; }
+
+bool Axis::IsScrolledToStart() const {
+ const auto zoom = GetFrameMetrics().GetZoom();
+
+ if (zoom == CSSToParentLayerScale(0)) {
+ return true;
+ }
+
+ return FuzzyEqualsCoordinate(GetOrigin() / zoom, GetPageStart() / zoom);
+}
+
+bool Axis::IsScrolledToEnd() const {
+ const auto zoom = GetFrameMetrics().GetZoom();
+
+ if (zoom == CSSToParentLayerScale(0)) {
+ return true;
+ }
+
+ return FuzzyEqualsCoordinate(GetCompositionEnd() / zoom, GetPageEnd() / zoom);
+}
+
+bool Axis::IsInInvalidOverscroll() const {
+ if (mOverscroll > 0) {
+ return !IsScrolledToEnd();
+ } else if (mOverscroll < 0) {
+ return !IsScrolledToStart();
+ }
+ return false;
+}
+
+void Axis::ClearOverscroll() {
+ EndOverscrollAnimation();
+ mOverscroll = 0;
+}
+
+ParentLayerCoord Axis::PanStart() const { return mStartPos; }
+
+ParentLayerCoord Axis::PanDistance() const { return fabs(mPos - mStartPos); }
+
+ParentLayerCoord Axis::PanDistance(ParentLayerCoord aPos) const {
+ return fabs(aPos - mStartPos);
+}
+
+void Axis::EndTouch(TimeStamp aTimestamp, ClearAxisLock aClearAxisLock) {
+ // mVelocityQueue is controller-thread only
+ APZThreadUtils::AssertOnControllerThread();
+
+ // If the velocity tracker wasn't able to compute a velocity, zero out
+ // the velocity to make sure we don't get a fling based on some old and
+ // no-longer-relevant value of mVelocity. Also if the axis is locked then
+ // just reset the velocity to 0 since we don't need any velocity to carry
+ // into the fling.
+ if (mAxisLocked) {
+ DoSetVelocity(0);
+ } else if (Maybe<float> velocity =
+ mVelocityTracker->ComputeVelocity(aTimestamp)) {
+ DoSetVelocity(*velocity);
+ } else {
+ DoSetVelocity(0);
+ }
+ if (aClearAxisLock == ClearAxisLock::Yes) {
+ mAxisLocked = false;
+ }
+ AXIS_LOG("%p|%s ending touch, computed velocity %f\n",
+ mAsyncPanZoomController, Name(), DoGetVelocity());
+}
+
+void Axis::CancelGesture() {
+ // mVelocityQueue is controller-thread only
+ APZThreadUtils::AssertOnControllerThread();
+
+ AXIS_LOG("%p|%s cancelling touch, clearing velocity queue\n",
+ mAsyncPanZoomController, Name());
+ DoSetVelocity(0.0f);
+ mVelocityTracker->Clear();
+ SetAxisLocked(false);
+}
+
+bool Axis::CanScroll() const {
+ return mAsyncPanZoomController->FuzzyGreater(GetPageLength(),
+ GetCompositionLength());
+}
+
+bool Axis::CanScroll(CSSCoord aDelta) const {
+ return CanScroll(aDelta * GetFrameMetrics().GetZoom());
+}
+
+bool Axis::CanScroll(ParentLayerCoord aDelta) const {
+ if (!CanScroll()) {
+ return false;
+ }
+
+ const auto zoom = GetFrameMetrics().GetZoom();
+ CSSCoord availableToScroll = 0;
+
+ if (zoom != CSSToParentLayerScale(0)) {
+ availableToScroll =
+ ParentLayerCoord(
+ fabs(DisplacementWillOverscrollAmount(aDelta) - aDelta)) /
+ zoom;
+ }
+
+ return availableToScroll > COORDINATE_EPSILON;
+}
+
+CSSCoord Axis::ClampOriginToScrollableRect(CSSCoord aOrigin) const {
+ CSSToParentLayerScale zoom = GetFrameMetrics().GetZoom();
+ ParentLayerCoord origin = aOrigin * zoom;
+ ParentLayerCoord result;
+ if (origin < GetPageStart()) {
+ result = GetPageStart();
+ } else if (origin + GetCompositionLength() > GetPageEnd()) {
+ result = GetPageEnd() - GetCompositionLength();
+ } else {
+ return aOrigin;
+ }
+ if (zoom == CSSToParentLayerScale(0)) {
+ return aOrigin;
+ }
+ return result / zoom;
+}
+
+bool Axis::CanScrollNow() const { return !mAxisLocked && CanScroll(); }
+
+ParentLayerCoord Axis::DisplacementWillOverscrollAmount(
+ ParentLayerCoord aDisplacement) const {
+ ParentLayerCoord newOrigin = GetOrigin() + aDisplacement;
+ ParentLayerCoord newCompositionEnd = GetCompositionEnd() + aDisplacement;
+ // If the current pan plus a displacement takes the window to the left of or
+ // above the current page rect.
+ bool minus = newOrigin < GetPageStart();
+ // If the current pan plus a displacement takes the window to the right of or
+ // below the current page rect.
+ bool plus = newCompositionEnd > GetPageEnd();
+ if (minus && plus) {
+ // Don't handle overscrolled in both directions; a displacement can't cause
+ // this, it must have already been zoomed out too far.
+ return 0;
+ }
+ if (minus) {
+ return newOrigin - GetPageStart();
+ }
+ if (plus) {
+ return newCompositionEnd - GetPageEnd();
+ }
+ return 0;
+}
+
+CSSCoord Axis::ScaleWillOverscrollAmount(float aScale, CSSCoord aFocus) const {
+ // Internally, do computations in ParentLayer coordinates *before* the scale
+ // is applied.
+ CSSToParentLayerScale zoom = GetFrameMetrics().GetZoom();
+ ParentLayerCoord focus = aFocus * zoom;
+ ParentLayerCoord originAfterScale = (GetOrigin() + focus) - (focus / aScale);
+
+ bool both = ScaleWillOverscrollBothSides(aScale);
+ bool minus = GetPageStart() - originAfterScale > COORDINATE_EPSILON;
+ bool plus =
+ (originAfterScale + (GetCompositionLength() / aScale)) - GetPageEnd() >
+ COORDINATE_EPSILON;
+
+ if ((minus && plus) || both) {
+ // If we ever reach here it's a bug in the client code.
+ MOZ_ASSERT(false,
+ "In an OVERSCROLL_BOTH condition in ScaleWillOverscrollAmount");
+ return 0;
+ }
+ if (minus && zoom != CSSToParentLayerScale(0)) {
+ return (originAfterScale - GetPageStart()) / zoom;
+ }
+ if (plus && zoom != CSSToParentLayerScale(0)) {
+ return (originAfterScale + (GetCompositionLength() / aScale) -
+ GetPageEnd()) /
+ zoom;
+ }
+ return 0;
+}
+
+bool Axis::IsAxisLocked() const { return mAxisLocked; }
+
+float Axis::GetVelocity() const { return mAxisLocked ? 0 : DoGetVelocity(); }
+
+void Axis::SetVelocity(float aVelocity) {
+ AXIS_LOG("%p|%s direct-setting velocity to %f\n", mAsyncPanZoomController,
+ Name(), aVelocity);
+ DoSetVelocity(aVelocity);
+}
+
+ParentLayerCoord Axis::GetCompositionEnd() const {
+ return GetOrigin() + GetCompositionLength();
+}
+
+ParentLayerCoord Axis::GetPageEnd() const {
+ return GetPageStart() + GetPageLength();
+}
+
+ParentLayerCoord Axis::GetScrollRangeEnd() const {
+ return GetPageEnd() - GetCompositionLength();
+}
+
+ParentLayerCoord Axis::GetOrigin() const {
+ ParentLayerPoint origin =
+ GetFrameMetrics().GetVisualScrollOffset() * GetFrameMetrics().GetZoom();
+ return GetPointOffset(origin);
+}
+
+ParentLayerCoord Axis::GetCompositionLength() const {
+ return GetRectLength(GetFrameMetrics().GetCompositionBounds());
+}
+
+ParentLayerCoord Axis::GetPageStart() const {
+ ParentLayerRect pageRect = GetFrameMetrics().GetExpandedScrollableRect() *
+ GetFrameMetrics().GetZoom();
+ return GetRectOffset(pageRect);
+}
+
+ParentLayerCoord Axis::GetPageLength() const {
+ ParentLayerRect pageRect = GetFrameMetrics().GetExpandedScrollableRect() *
+ GetFrameMetrics().GetZoom();
+ return GetRectLength(pageRect);
+}
+
+bool Axis::ScaleWillOverscrollBothSides(float aScale) const {
+ const FrameMetrics& metrics = GetFrameMetrics();
+ ParentLayerRect screenCompositionBounds =
+ metrics.GetCompositionBounds() / ParentLayerToParentLayerScale(aScale);
+ return GetRectLength(screenCompositionBounds) - GetPageLength() >
+ COORDINATE_EPSILON;
+}
+
+float Axis::DoGetVelocity() const {
+ auto velocity = mVelocity.Lock();
+ return velocity.ref();
+}
+void Axis::DoSetVelocity(float aVelocity) {
+ auto velocity = mVelocity.Lock();
+ velocity.ref() = aVelocity;
+}
+
+const FrameMetrics& Axis::GetFrameMetrics() const {
+ return mAsyncPanZoomController->GetFrameMetrics();
+}
+
+const ScrollMetadata& Axis::GetScrollMetadata() const {
+ return mAsyncPanZoomController->GetScrollMetadata();
+}
+
+bool Axis::OverscrollBehaviorAllowsHandoff() const {
+ // Scroll handoff is a "non-local" overscroll behavior, so it's allowed
+ // with "auto" and disallowed with "contain" and "none".
+ return GetOverscrollBehavior() == OverscrollBehavior::Auto;
+}
+
+bool Axis::OverscrollBehaviorAllowsOverscrollEffect() const {
+ // An overscroll effect is a "local" overscroll behavior, so it's allowed
+ // with "auto" and "contain" and disallowed with "none".
+ return GetOverscrollBehavior() != OverscrollBehavior::None;
+}
+
+AxisX::AxisX(AsyncPanZoomController* aAsyncPanZoomController)
+ : Axis(aAsyncPanZoomController) {}
+
+CSSCoord AxisX::GetPointOffset(const CSSPoint& aPoint) const {
+ return aPoint.x;
+}
+
+OuterCSSCoord AxisX::GetPointOffset(const OuterCSSPoint& aPoint) const {
+ return aPoint.x;
+}
+
+ParentLayerCoord AxisX::GetPointOffset(const ParentLayerPoint& aPoint) const {
+ return aPoint.x;
+}
+
+CSSToParentLayerScale AxisX::GetAxisScale(
+ const CSSToParentLayerScale2D& aScale) const {
+ return CSSToParentLayerScale(aScale.xScale);
+}
+
+ParentLayerCoord AxisX::GetRectLength(const ParentLayerRect& aRect) const {
+ return aRect.Width();
+}
+
+CSSCoord AxisX::GetRectLength(const CSSRect& aRect) const {
+ return aRect.Width();
+}
+
+ParentLayerCoord AxisX::GetRectOffset(const ParentLayerRect& aRect) const {
+ return aRect.X();
+}
+
+CSSCoord AxisX::GetRectOffset(const CSSRect& aRect) const { return aRect.X(); }
+
+float AxisX::GetTransformScale(
+ const AsyncTransformComponentMatrix& aMatrix) const {
+ return aMatrix._11;
+}
+
+ParentLayerCoord AxisX::GetTransformTranslation(
+ const AsyncTransformComponentMatrix& aMatrix) const {
+ return aMatrix._41;
+}
+
+void AxisX::PostScale(AsyncTransformComponentMatrix& aMatrix,
+ float aScale) const {
+ aMatrix.PostScale(aScale, 1.f, 1.f);
+}
+
+void AxisX::PostTranslate(AsyncTransformComponentMatrix& aMatrix,
+ ParentLayerCoord aTranslation) const {
+ aMatrix.PostTranslate(aTranslation, 0, 0);
+}
+
+ScreenPoint AxisX::MakePoint(ScreenCoord aCoord) const {
+ return ScreenPoint(aCoord, 0);
+}
+
+const char* AxisX::Name() const { return "X"; }
+
+bool AxisX::CanScrollTo(Side aSide) const {
+ switch (aSide) {
+ case eSideLeft:
+ return CanScroll(CSSCoord(-COORDINATE_EPSILON * 2));
+ case eSideRight:
+ return CanScroll(CSSCoord(COORDINATE_EPSILON * 2));
+ default:
+ MOZ_ASSERT_UNREACHABLE("aSide is out of valid values");
+ return false;
+ }
+}
+
+SideBits AxisX::ScrollableDirections() const {
+ SideBits directions = SideBits::eNone;
+
+ if (CanScrollTo(eSideLeft)) {
+ directions |= SideBits::eLeft;
+ }
+ if (CanScrollTo(eSideRight)) {
+ directions |= SideBits::eRight;
+ }
+
+ return directions;
+}
+
+OverscrollBehavior AxisX::GetOverscrollBehavior() const {
+ return GetScrollMetadata().GetOverscrollBehavior().mBehaviorX;
+}
+
+AxisY::AxisY(AsyncPanZoomController* aAsyncPanZoomController)
+ : Axis(aAsyncPanZoomController) {}
+
+CSSCoord AxisY::GetPointOffset(const CSSPoint& aPoint) const {
+ return aPoint.y;
+}
+
+OuterCSSCoord AxisY::GetPointOffset(const OuterCSSPoint& aPoint) const {
+ return aPoint.y;
+}
+
+ParentLayerCoord AxisY::GetPointOffset(const ParentLayerPoint& aPoint) const {
+ return aPoint.y;
+}
+
+CSSToParentLayerScale AxisY::GetAxisScale(
+ const CSSToParentLayerScale2D& aScale) const {
+ return CSSToParentLayerScale(aScale.yScale);
+}
+
+ParentLayerCoord AxisY::GetRectLength(const ParentLayerRect& aRect) const {
+ return aRect.Height();
+}
+
+CSSCoord AxisY::GetRectLength(const CSSRect& aRect) const {
+ return aRect.Height();
+}
+
+ParentLayerCoord AxisY::GetRectOffset(const ParentLayerRect& aRect) const {
+ return aRect.Y();
+}
+
+CSSCoord AxisY::GetRectOffset(const CSSRect& aRect) const { return aRect.Y(); }
+
+float AxisY::GetTransformScale(
+ const AsyncTransformComponentMatrix& aMatrix) const {
+ return aMatrix._22;
+}
+
+ParentLayerCoord AxisY::GetTransformTranslation(
+ const AsyncTransformComponentMatrix& aMatrix) const {
+ return aMatrix._42;
+}
+
+void AxisY::PostScale(AsyncTransformComponentMatrix& aMatrix,
+ float aScale) const {
+ aMatrix.PostScale(1.f, aScale, 1.f);
+}
+
+void AxisY::PostTranslate(AsyncTransformComponentMatrix& aMatrix,
+ ParentLayerCoord aTranslation) const {
+ aMatrix.PostTranslate(0, aTranslation, 0);
+}
+
+ScreenPoint AxisY::MakePoint(ScreenCoord aCoord) const {
+ return ScreenPoint(0, aCoord);
+}
+
+const char* AxisY::Name() const { return "Y"; }
+
+bool AxisY::CanScrollTo(Side aSide) const {
+ switch (aSide) {
+ case eSideTop:
+ return CanScroll(CSSCoord(-COORDINATE_EPSILON * 2));
+ case eSideBottom:
+ return CanScroll(CSSCoord(COORDINATE_EPSILON * 2));
+ default:
+ MOZ_ASSERT_UNREACHABLE("aSide is out of valid values");
+ return false;
+ }
+}
+
+SideBits AxisY::ScrollableDirections() const {
+ SideBits directions = SideBits::eNone;
+
+ if (CanScrollTo(eSideTop)) {
+ directions |= SideBits::eTop;
+ }
+ if (CanScrollTo(eSideBottom)) {
+ directions |= SideBits::eBottom;
+ }
+
+ return directions;
+}
+
+bool AxisY::HasDynamicToolbar() const {
+ return GetCompositionLengthWithoutDynamicToolbar() != ParentLayerCoord(0);
+}
+
+SideBits AxisY::ScrollableDirectionsWithDynamicToolbar(
+ const ScreenMargin& aFixedLayerMargins) const {
+ MOZ_ASSERT(mAsyncPanZoomController->IsRootContent());
+
+ SideBits directions = ScrollableDirections();
+
+ if (HasDynamicToolbar()) {
+ ParentLayerCoord toolbarHeight =
+ GetCompositionLength() - GetCompositionLengthWithoutDynamicToolbar();
+
+ ParentLayerMargin fixedLayerMargins = ViewAs<ParentLayerPixel>(
+ aFixedLayerMargins, PixelCastJustification::ScreenIsParentLayerForRoot);
+
+ if (!mAsyncPanZoomController->IsZero(fixedLayerMargins.bottom)) {
+ directions |= SideBits::eTop;
+ }
+ if (mAsyncPanZoomController->FuzzyGreater(
+ aFixedLayerMargins.bottom + toolbarHeight, 0)) {
+ directions |= SideBits::eBottom;
+ }
+ }
+
+ return directions;
+}
+
+bool AxisY::CanVerticalScrollWithDynamicToolbar() const {
+ return !HasDynamicToolbar()
+ ? CanScroll()
+ : mAsyncPanZoomController->FuzzyGreater(
+ GetPageLength(),
+ GetCompositionLengthWithoutDynamicToolbar());
+}
+
+OverscrollBehavior AxisY::GetOverscrollBehavior() const {
+ return GetScrollMetadata().GetOverscrollBehavior().mBehaviorY;
+}
+
+ParentLayerCoord AxisY::GetCompositionLengthWithoutDynamicToolbar() const {
+ return GetFrameMetrics().GetCompositionSizeWithoutDynamicToolbar().Height();
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/Axis.h b/gfx/layers/apz/src/Axis.h
new file mode 100644
index 0000000000..072c0a297b
--- /dev/null
+++ b/gfx/layers/apz/src/Axis.h
@@ -0,0 +1,462 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_Axis_h
+#define mozilla_layers_Axis_h
+
+#include <sys/types.h> // for int32_t
+
+#include "APZUtils.h"
+#include "AxisPhysicsMSDModel.h"
+#include "mozilla/DataMutex.h" // for DataMutex
+#include "mozilla/gfx/Types.h" // for Side
+#include "mozilla/TimeStamp.h" // for TimeDuration
+#include "nsTArray.h" // for nsTArray
+#include "Units.h"
+
+namespace mozilla {
+namespace layers {
+
+const float EPSILON = 0.0001f;
+
+/**
+ * Compare two coordinates for equality, accounting for rounding error.
+ * Use both FuzzyEqualsAdditive() with COORDINATE_EPISLON, which accounts for
+ * things like the error introduced by rounding during a round-trip to app
+ * units, and FuzzyEqualsMultiplicative(), which accounts for accumulated error
+ * due to floating-point operations (which can be larger than COORDINATE_EPISLON
+ * for sufficiently large coordinate values).
+ */
+bool FuzzyEqualsCoordinate(CSSCoord aValue1, CSSCoord aValue2);
+
+struct FrameMetrics;
+class AsyncPanZoomController;
+
+/**
+ * Interface for computing velocities along the axis based on
+ * position samples.
+ */
+class VelocityTracker {
+ public:
+ virtual ~VelocityTracker() = default;
+
+ /**
+ * Start tracking velocity along this axis, starting with the given
+ * initial position and corresponding timestamp.
+ */
+ virtual void StartTracking(ParentLayerCoord aPos, TimeStamp aTimestamp) = 0;
+ /**
+ * Record a new position along this axis, at the given timestamp.
+ * Returns the average velocity between the last sample and this one, or
+ * or Nothing() if a reasonable average cannot be computed.
+ */
+ virtual Maybe<float> AddPosition(ParentLayerCoord aPos,
+ TimeStamp aTimestamp) = 0;
+ /**
+ * Compute an estimate of the axis's current velocity, based on recent
+ * position samples. It's up to implementation how many samples to consider
+ * and how to perform the computation.
+ * If the tracker doesn't have enough samples to compute a result, it
+ * may return Nothing{}.
+ */
+ virtual Maybe<float> ComputeVelocity(TimeStamp aTimestamp) = 0;
+ /**
+ * Clear all state in the velocity tracker.
+ */
+ virtual void Clear() = 0;
+};
+
+/**
+ * Helper class to maintain each axis of movement (X,Y) for panning and zooming.
+ * Note that everything here is specific to one axis; that is, the X axis knows
+ * nothing about the Y axis and vice versa.
+ */
+class Axis {
+ public:
+ explicit Axis(AsyncPanZoomController* aAsyncPanZoomController);
+
+ /**
+ * Notify this Axis that a new touch has been received, including a timestamp
+ * for when the touch was received. This triggers a recalculation of velocity.
+ * This can also used for pan gesture events. For those events, |aPos| is
+ * an invented position corresponding to the mouse position plus any
+ * accumulated displacements over the course of the pan gesture.
+ */
+ void UpdateWithTouchAtDevicePoint(ParentLayerCoord aPos,
+ TimeStamp aTimestamp);
+
+ public:
+ /**
+ * Notify this Axis that a touch has begun, i.e. the user has put their finger
+ * on the screen but has not yet tried to pan.
+ */
+ void StartTouch(ParentLayerCoord aPos, TimeStamp aTimestamp);
+
+ /**
+ * Helper enum class for specifying if EndTouch() should clear the axis lock.
+ */
+ enum class ClearAxisLock { Yes, No };
+
+ /**
+ * Notify this Axis that a touch has ended gracefully. This may perform
+ * recalculations of the axis velocity.
+ */
+ void EndTouch(TimeStamp aTimestamp, ClearAxisLock aClearAxisLock);
+
+ /**
+ * Notify this Axis that the gesture has ended forcefully. Useful for stopping
+ * flings when a user puts their finger down in the middle of one (i.e. to
+ * stop a previous touch including its fling so that a new one can take its
+ * place).
+ */
+ void CancelGesture();
+
+ /**
+ * Takes a requested displacement to the position of this axis, and adjusts it
+ * to account for overscroll (which might decrease the displacement; this is
+ * to prevent the viewport from overscrolling the page rect), and axis locking
+ * (which might prevent any displacement from happening). If overscroll
+ * ocurred, its amount is written to |aOverscrollAmountOut|.
+ * The |aDisplacementOut| parameter is set to the adjusted displacement, and
+ * the function returns true if and only if internal overscroll amounts were
+ * changed.
+ */
+ bool AdjustDisplacement(ParentLayerCoord aDisplacement,
+ ParentLayerCoord& aDisplacementOut,
+ ParentLayerCoord& aOverscrollAmountOut,
+ bool aForceOverscroll = false);
+
+ /**
+ * Overscrolls this axis by the requested amount in the requested direction.
+ * The axis must be at the end of its scroll range in this direction.
+ */
+ void OverscrollBy(ParentLayerCoord aOverscroll);
+
+ /**
+ * Return the amount of overscroll on this axis, in ParentLayer pixels.
+ *
+ * If this amount is nonzero, the relevant component of
+ * mAsyncPanZoomController->Metrics().mScrollOffset must be at its
+ * extreme allowed value in the relevant direction (that is, it must be at
+ * its maximum value if we are overscrolled at our composition length, and
+ * at its minimum value if we are overscrolled at the origin).
+ */
+ ParentLayerCoord GetOverscroll() const;
+
+ /**
+ * Restore the amount by which this axis is overscrolled to the specified
+ * amount. This is for test-related use; overscrolling as a result of user
+ * input should happen via OverscrollBy().
+ */
+ void RestoreOverscroll(ParentLayerCoord aOverscroll);
+
+ /**
+ * Start an overscroll animation with the given initial velocity.
+ */
+ void StartOverscrollAnimation(float aVelocity);
+
+ /**
+ * Sample the snap-back animation to relieve overscroll.
+ * |aDelta| is the time since the last sample, |aOverscrollSideBits| is
+ * the direction where the overscroll happens on this axis.
+ */
+ bool SampleOverscrollAnimation(const TimeDuration& aDelta,
+ SideBits aOverscrollSideBits);
+
+ /**
+ * Stop an overscroll animation.
+ */
+ void EndOverscrollAnimation();
+
+ /**
+ * Return whether this axis is overscrolled in either direction.
+ */
+ bool IsOverscrolled() const;
+
+ /**
+ * Return true if this axis is overscrolled but its scroll offset
+ * has changed in a way that makes the oversrolled state no longer
+ * valid (for example, it is overscrolled at the top but the
+ * scroll offset is no longer zero).
+ */
+ bool IsInInvalidOverscroll() const;
+
+ /**
+ * Clear any overscroll amount on this axis.
+ */
+ void ClearOverscroll();
+
+ /**
+ * Returns whether the overscroll animation is alive.
+ */
+ bool IsOverscrollAnimationAlive() const;
+
+ /**
+ * Returns whether the overscroll animation is running.
+ * Note that unlike the above IsOverscrollAnimationAlive, this function
+ * returns false even if the animation is still there but is very close to
+ * the destination position and its velocity is quite low, i.e. it's time to
+ * finish.
+ */
+ bool IsOverscrollAnimationRunning() const;
+
+ /**
+ * Gets the starting position of the touch supplied in StartTouch().
+ */
+ ParentLayerCoord PanStart() const;
+
+ /**
+ * Gets the distance between the starting position of the touch supplied in
+ * StartTouch() and the current touch from the last
+ * UpdateWithTouchAtDevicePoint().
+ */
+ ParentLayerCoord PanDistance() const;
+
+ /**
+ * Gets the distance between the starting position of the touch supplied in
+ * StartTouch() and the supplied position.
+ */
+ ParentLayerCoord PanDistance(ParentLayerCoord aPos) const;
+
+ /**
+ * Returns true if the page has room to be scrolled along this axis.
+ */
+ bool CanScroll() const;
+
+ /**
+ * Returns whether this axis can scroll any more in a particular direction.
+ */
+ bool CanScroll(CSSCoord aDelta) const;
+ bool CanScroll(ParentLayerCoord aDelta) const;
+
+ /**
+ * Returns true if the page has room to be scrolled along this axis
+ * and this axis is not scroll-locked.
+ */
+ bool CanScrollNow() const;
+
+ /**
+ * Clamp a point to the page's scrollable bounds. That is, a scroll
+ * destination to the returned point will not contain any overscroll.
+ */
+ CSSCoord ClampOriginToScrollableRect(CSSCoord aOrigin) const;
+
+ void SetAxisLocked(bool aAxisLocked) { mAxisLocked = aAxisLocked; }
+
+ /**
+ * Gets the raw velocity of this axis at this moment.
+ */
+ float GetVelocity() const;
+
+ /**
+ * Sets the raw velocity of this axis at this moment.
+ * Intended to be called only when the axis "takes over" a velocity from
+ * another APZC, in which case there are no touch points available to call
+ * UpdateWithTouchAtDevicePoint. In other circumstances,
+ * UpdateWithTouchAtDevicePoint should be used and the velocity calculated
+ * there.
+ */
+ void SetVelocity(float aVelocity);
+
+ /**
+ * If a displacement will overscroll the axis, this returns the amount and in
+ * what direction.
+ */
+ ParentLayerCoord DisplacementWillOverscrollAmount(
+ ParentLayerCoord aDisplacement) const;
+
+ /**
+ * If a scale will overscroll the axis, this returns the amount and in what
+ * direction.
+ *
+ * |aFocus| is the point at which the scale is focused at. We will offset the
+ * scroll offset in such a way that it remains in the same place on the page
+ * relative.
+ *
+ * Note: Unlike most other functions in Axis, this functions operates in
+ * CSS coordinates so there is no confusion as to whether the
+ * ParentLayer coordinates it operates in are before or after the scale
+ * is applied.
+ */
+ CSSCoord ScaleWillOverscrollAmount(float aScale, CSSCoord aFocus) const;
+
+ /**
+ * Checks if an axis will overscroll in both directions by computing the
+ * content rect and checking that its height/width (depending on the axis)
+ * does not overextend past the viewport.
+ *
+ * This gets called by ScaleWillOverscroll().
+ */
+ bool ScaleWillOverscrollBothSides(float aScale) const;
+
+ /**
+ * Returns true if movement on this axis is locked.
+ */
+ bool IsAxisLocked() const;
+
+ ParentLayerCoord GetOrigin() const;
+ ParentLayerCoord GetCompositionLength() const;
+ ParentLayerCoord GetPageStart() const;
+ ParentLayerCoord GetPageLength() const;
+ ParentLayerCoord GetCompositionEnd() const;
+ ParentLayerCoord GetPageEnd() const;
+ ParentLayerCoord GetScrollRangeEnd() const;
+
+ bool IsScrolledToStart() const;
+ bool IsScrolledToEnd() const;
+
+ ParentLayerCoord GetPos() const { return mPos; }
+
+ bool OverscrollBehaviorAllowsHandoff() const;
+ bool OverscrollBehaviorAllowsOverscrollEffect() const;
+
+ virtual CSSToParentLayerScale GetAxisScale(
+ const CSSToParentLayerScale2D& aScale) const = 0;
+ virtual CSSCoord GetPointOffset(const CSSPoint& aPoint) const = 0;
+ virtual OuterCSSCoord GetPointOffset(const OuterCSSPoint& aPoint) const = 0;
+ virtual ParentLayerCoord GetPointOffset(
+ const ParentLayerPoint& aPoint) const = 0;
+ virtual ParentLayerCoord GetRectLength(
+ const ParentLayerRect& aRect) const = 0;
+ virtual CSSCoord GetRectLength(const CSSRect& aRect) const = 0;
+ virtual ParentLayerCoord GetRectOffset(
+ const ParentLayerRect& aRect) const = 0;
+ virtual CSSCoord GetRectOffset(const CSSRect& aRect) const = 0;
+ virtual float GetTransformScale(
+ const AsyncTransformComponentMatrix& aMatrix) const = 0;
+ virtual ParentLayerCoord GetTransformTranslation(
+ const AsyncTransformComponentMatrix& aMatrix) const = 0;
+ virtual void PostScale(AsyncTransformComponentMatrix& aMatrix,
+ float aScale) const = 0;
+ virtual void PostTranslate(AsyncTransformComponentMatrix& aMatrix,
+ ParentLayerCoord aTranslation) const = 0;
+
+ virtual ScreenPoint MakePoint(ScreenCoord aCoord) const = 0;
+
+ const void* OpaqueApzcPointer() const { return mAsyncPanZoomController; }
+
+ virtual const char* Name() const = 0;
+
+ // Convert a velocity from global inches/ms into ParentLayerCoords/ms.
+ float ToLocalVelocity(float aVelocityInchesPerMs) const;
+
+ protected:
+ // A position along the axis, used during input event processing to
+ // track velocities (and for touch gestures, to track the length of
+ // the gesture). For touch events, this represents the position of
+ // the finger (or in the case of two-finger scrolling, the midpoint
+ // of the two fingers). For pan gesture events, this represents an
+ // invented position corresponding to the mouse position at the start
+ // of the pan, plus deltas representing the displacement of the pan.
+ ParentLayerCoord mPos;
+
+ ParentLayerCoord mStartPos;
+ // The velocity can be accessed from multiple threads (e.g. APZ
+ // controller thread and APZ sampler thread), so needs to be
+ // protected by a mutex.
+ // Units: ParentLayerCoords per millisecond
+ mutable DataMutex<float> mVelocity;
+ bool mAxisLocked; // Whether movement on this axis is locked.
+ AsyncPanZoomController* mAsyncPanZoomController;
+
+ // The amount by which we are overscrolled; see GetOverscroll().
+ ParentLayerCoord mOverscroll;
+
+ // The mass-spring-damper model for overscroll physics.
+ AxisPhysicsMSDModel mMSDModel;
+
+ // Used to track velocity over a series of input events and compute
+ // a resulting velocity to use for e.g. starting a fling animation.
+ // This member can only be accessed on the controller/UI thread.
+ UniquePtr<VelocityTracker> mVelocityTracker;
+
+ float DoGetVelocity() const;
+ void DoSetVelocity(float aVelocity);
+
+ const FrameMetrics& GetFrameMetrics() const;
+ const ScrollMetadata& GetScrollMetadata() const;
+
+ // Do not use this function directly, use
+ // AsyncPanZoomController::GetAllowedHandoffDirections instead.
+ virtual OverscrollBehavior GetOverscrollBehavior() const = 0;
+
+ // Adjust a requested overscroll amount for resistance, yielding a smaller
+ // actual overscroll amount.
+ ParentLayerCoord ApplyResistance(ParentLayerCoord aOverscroll) const;
+
+ // Helper function for SampleOverscrollAnimation().
+ void StepOverscrollAnimation(double aStepDurationMilliseconds);
+};
+
+class AxisX : public Axis {
+ public:
+ explicit AxisX(AsyncPanZoomController* mAsyncPanZoomController);
+ CSSToParentLayerScale GetAxisScale(
+ const CSSToParentLayerScale2D& aScale) const override;
+ CSSCoord GetPointOffset(const CSSPoint& aPoint) const override;
+ OuterCSSCoord GetPointOffset(const OuterCSSPoint& aPoint) const override;
+ ParentLayerCoord GetPointOffset(
+ const ParentLayerPoint& aPoint) const override;
+ ParentLayerCoord GetRectLength(const ParentLayerRect& aRect) const override;
+ CSSCoord GetRectLength(const CSSRect& aRect) const override;
+ ParentLayerCoord GetRectOffset(const ParentLayerRect& aRect) const override;
+ CSSCoord GetRectOffset(const CSSRect& aRect) const override;
+ float GetTransformScale(
+ const AsyncTransformComponentMatrix& aMatrix) const override;
+ ParentLayerCoord GetTransformTranslation(
+ const AsyncTransformComponentMatrix& aMatrix) const override;
+ void PostScale(AsyncTransformComponentMatrix& aMatrix,
+ float aScale) const override;
+ void PostTranslate(AsyncTransformComponentMatrix& aMatrix,
+ ParentLayerCoord aTranslation) const override;
+ ScreenPoint MakePoint(ScreenCoord aCoord) const override;
+ const char* Name() const override;
+ bool CanScrollTo(Side aSide) const;
+ SideBits ScrollableDirections() const;
+
+ private:
+ OverscrollBehavior GetOverscrollBehavior() const override;
+};
+
+class AxisY : public Axis {
+ public:
+ explicit AxisY(AsyncPanZoomController* mAsyncPanZoomController);
+ CSSCoord GetPointOffset(const CSSPoint& aPoint) const override;
+ OuterCSSCoord GetPointOffset(const OuterCSSPoint& aPoint) const override;
+ ParentLayerCoord GetPointOffset(
+ const ParentLayerPoint& aPoint) const override;
+ CSSToParentLayerScale GetAxisScale(
+ const CSSToParentLayerScale2D& aScale) const override;
+ ParentLayerCoord GetRectLength(const ParentLayerRect& aRect) const override;
+ CSSCoord GetRectLength(const CSSRect& aRect) const override;
+ ParentLayerCoord GetRectOffset(const ParentLayerRect& aRect) const override;
+ CSSCoord GetRectOffset(const CSSRect& aRect) const override;
+ float GetTransformScale(
+ const AsyncTransformComponentMatrix& aMatrix) const override;
+ ParentLayerCoord GetTransformTranslation(
+ const AsyncTransformComponentMatrix& aMatrix) const override;
+ void PostScale(AsyncTransformComponentMatrix& aMatrix,
+ float aScale) const override;
+ void PostTranslate(AsyncTransformComponentMatrix& aMatrix,
+ ParentLayerCoord aTranslation) const override;
+ ScreenPoint MakePoint(ScreenCoord aCoord) const override;
+ const char* Name() const override;
+ bool CanScrollTo(Side aSide) const;
+ bool CanVerticalScrollWithDynamicToolbar() const;
+ SideBits ScrollableDirections() const;
+ SideBits ScrollableDirectionsWithDynamicToolbar(
+ const ScreenMargin& aFixedLayerMargins) const;
+
+ private:
+ OverscrollBehavior GetOverscrollBehavior() const override;
+ ParentLayerCoord GetCompositionLengthWithoutDynamicToolbar() const;
+ bool HasDynamicToolbar() const;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif
diff --git a/gfx/layers/apz/src/CheckerboardEvent.cpp b/gfx/layers/apz/src/CheckerboardEvent.cpp
new file mode 100644
index 0000000000..8f518c7383
--- /dev/null
+++ b/gfx/layers/apz/src/CheckerboardEvent.cpp
@@ -0,0 +1,195 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "CheckerboardEvent.h"
+#include "mozilla/Logging.h"
+
+#include <algorithm> // for std::sort
+
+static mozilla::LazyLogModule sApzCheckLog("apz.checkerboard");
+
+namespace mozilla {
+namespace layers {
+
+// Relatively arbitrary limit to prevent a perma-checkerboard event from
+// eating up gobs of memory. Ideally we shouldn't have perma-checkerboarding
+// but better to guard against it.
+#define LOG_LENGTH_LIMIT (50 * 1024)
+
+const char* CheckerboardEvent::sDescriptions[] = {
+ "page",
+ "painted displayport",
+ "requested displayport",
+ "viewport",
+};
+
+const char* CheckerboardEvent::sColors[] = {
+ "brown",
+ "lightgreen",
+ "yellow",
+ "red",
+};
+
+CheckerboardEvent::CheckerboardEvent(bool aRecordTrace)
+ : mRecordTrace(aRecordTrace),
+ mOriginTime(TimeStamp::Now()),
+ mCheckerboardingActive(false),
+ mLastSampleTime(mOriginTime),
+ mFrameCount(0),
+ mTotalPixelMs(0),
+ mPeakPixels(0),
+ mRendertraceLock("Rendertrace") {}
+
+uint32_t CheckerboardEvent::GetSeverity() {
+ // Scale the total into a 32-bit value
+ return (uint32_t)sqrt((double)mTotalPixelMs);
+}
+
+uint32_t CheckerboardEvent::GetPeak() { return mPeakPixels; }
+
+TimeDuration CheckerboardEvent::GetDuration() { return mEndTime - mStartTime; }
+
+std::string CheckerboardEvent::GetLog() {
+ MonitorAutoLock lock(mRendertraceLock);
+ return mRendertraceInfo.str();
+}
+
+bool CheckerboardEvent::IsRecordingTrace() { return mRecordTrace; }
+
+void CheckerboardEvent::UpdateRendertraceProperty(
+ RendertraceProperty aProperty, const CSSRect& aRect,
+ const std::string& aExtraInfo) {
+ if (!mRecordTrace) {
+ return;
+ }
+ MonitorAutoLock lock(mRendertraceLock);
+ if (!mCheckerboardingActive) {
+ mBufferedProperties[aProperty].Update(aProperty, aRect, aExtraInfo, lock);
+ } else {
+ LogInfo(aProperty, TimeStamp::Now(), aRect, aExtraInfo, lock);
+ }
+}
+
+void CheckerboardEvent::LogInfo(RendertraceProperty aProperty,
+ const TimeStamp& aTimestamp,
+ const CSSRect& aRect,
+ const std::string& aExtraInfo,
+ const MonitorAutoLock& aProofOfLock) {
+ MOZ_ASSERT(mRecordTrace);
+ if (mRendertraceInfo.tellp() >= LOG_LENGTH_LIMIT) {
+ // The log is already long enough, don't put more things into it. We'll
+ // append a truncation message when this event ends.
+ return;
+ }
+ // The log is consumed by the page at about:checkerboard. The format is not
+ // formally specced, but an informal description can be found at
+ // https://searchfox.org/mozilla-central/rev/d866b96d74ec2a63f09ee418f048d23f4fd379a2/toolkit/components/aboutcheckerboard/content/aboutCheckerboard.js#86
+ mRendertraceInfo << "RENDERTRACE "
+ << (aTimestamp - mOriginTime).ToMilliseconds() << " rect "
+ << sColors[aProperty] << " " << aRect.X() << " " << aRect.Y()
+ << " " << aRect.Width() << " " << aRect.Height() << " "
+ << "// " << sDescriptions[aProperty] << aExtraInfo
+ << std::endl;
+}
+
+bool CheckerboardEvent::RecordFrameInfo(uint32_t aCssPixelsCheckerboarded) {
+ TimeStamp sampleTime = TimeStamp::Now();
+ bool eventEnding = false;
+ if (aCssPixelsCheckerboarded > 0) {
+ if (!mCheckerboardingActive) {
+ StartEvent();
+ }
+ MOZ_ASSERT(mCheckerboardingActive);
+ MOZ_ASSERT(sampleTime >= mLastSampleTime);
+ mTotalPixelMs +=
+ (uint64_t)((sampleTime - mLastSampleTime).ToMilliseconds() *
+ aCssPixelsCheckerboarded);
+ if (aCssPixelsCheckerboarded > mPeakPixels) {
+ mPeakPixels = aCssPixelsCheckerboarded;
+ }
+ mFrameCount++;
+ } else {
+ if (mCheckerboardingActive) {
+ StopEvent();
+ eventEnding = true;
+ }
+ MOZ_ASSERT(!mCheckerboardingActive);
+ }
+ mLastSampleTime = sampleTime;
+ return eventEnding;
+}
+
+void CheckerboardEvent::StartEvent() {
+ MOZ_LOG(sApzCheckLog, LogLevel::Debug, ("Starting checkerboard event"));
+ MOZ_ASSERT(!mCheckerboardingActive);
+ mCheckerboardingActive = true;
+ mStartTime = TimeStamp::Now();
+
+ if (!mRecordTrace) {
+ return;
+ }
+ MonitorAutoLock lock(mRendertraceLock);
+ std::vector<PropertyValue> history;
+ for (PropertyBuffer& bufferedProperty : mBufferedProperties) {
+ bufferedProperty.Flush(history, lock);
+ }
+ std::sort(history.begin(), history.end());
+ for (const PropertyValue& p : history) {
+ LogInfo(p.mProperty, p.mTimeStamp, p.mRect, p.mExtraInfo, lock);
+ }
+ mRendertraceInfo << " -- checkerboarding starts below --" << std::endl;
+}
+
+void CheckerboardEvent::StopEvent() {
+ MOZ_LOG(sApzCheckLog, LogLevel::Debug, ("Stopping checkerboard event"));
+ mCheckerboardingActive = false;
+ mEndTime = TimeStamp::Now();
+
+ if (!mRecordTrace) {
+ return;
+ }
+ MonitorAutoLock lock(mRendertraceLock);
+ if (mRendertraceInfo.tellp() >= LOG_LENGTH_LIMIT) {
+ mRendertraceInfo << "[logging aborted due to length limitations]\n";
+ }
+ mRendertraceInfo << "Checkerboarded for " << mFrameCount << " frames ("
+ << (mEndTime - mStartTime).ToMilliseconds() << " ms), "
+ << mPeakPixels << " peak, " << GetSeverity() << " severity."
+ << std::endl;
+}
+
+bool CheckerboardEvent::PropertyValue::operator<(
+ const PropertyValue& aOther) const {
+ if (mTimeStamp < aOther.mTimeStamp) {
+ return true;
+ } else if (mTimeStamp > aOther.mTimeStamp) {
+ return false;
+ }
+ return mProperty < aOther.mProperty;
+}
+
+CheckerboardEvent::PropertyBuffer::PropertyBuffer() : mIndex(0) {}
+
+void CheckerboardEvent::PropertyBuffer::Update(
+ RendertraceProperty aProperty, const CSSRect& aRect,
+ const std::string& aExtraInfo, const MonitorAutoLock& aProofOfLock) {
+ mValues[mIndex] = {aProperty, TimeStamp::Now(), aRect, aExtraInfo};
+ mIndex = (mIndex + 1) % BUFFER_SIZE;
+}
+
+void CheckerboardEvent::PropertyBuffer::Flush(
+ std::vector<PropertyValue>& aOut, const MonitorAutoLock& aProofOfLock) {
+ for (uint32_t i = 0; i < BUFFER_SIZE; i++) {
+ uint32_t ix = (mIndex + i) % BUFFER_SIZE;
+ if (!mValues[ix].mTimeStamp.IsNull()) {
+ aOut.push_back(mValues[ix]);
+ mValues[ix].mTimeStamp = TimeStamp();
+ }
+ }
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/CheckerboardEvent.h b/gfx/layers/apz/src/CheckerboardEvent.h
new file mode 100644
index 0000000000..ad7fa83b2b
--- /dev/null
+++ b/gfx/layers/apz/src/CheckerboardEvent.h
@@ -0,0 +1,218 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_CheckerboardEvent_h
+#define mozilla_layers_CheckerboardEvent_h
+
+#include "mozilla/DefineEnum.h"
+#include "mozilla/Monitor.h"
+#include "mozilla/TimeStamp.h"
+#include <sstream>
+#include "Units.h"
+#include <vector>
+
+namespace mozilla {
+namespace layers {
+
+/**
+ * This class records information relevant to one "checkerboard event", which is
+ * a contiguous set of frames where a given APZC was checkerboarding. The intent
+ * of this class is to record enough information that it can provide actionable
+ * steps to reduce the occurrence of checkerboarding. Furthermore, it records
+ * information about the severity of the checkerboarding so as to allow
+ * prioritizing the debugging of some checkerboarding events over others.
+ */
+class CheckerboardEvent final {
+ public:
+ // clang-format off
+ MOZ_DEFINE_ENUM_AT_CLASS_SCOPE(
+ RendertraceProperty, (
+ Page,
+ PaintedDisplayPort,
+ RequestedDisplayPort,
+ UserVisible
+ ));
+ // clang-format on
+
+ static const char* sDescriptions[sRendertracePropertyCount];
+ static const char* sColors[sRendertracePropertyCount];
+
+ public:
+ explicit CheckerboardEvent(bool aRecordTrace);
+
+ /**
+ * Gets the "severity" of the checkerboard event. This doesn't have units,
+ * it's just useful for comparing two checkerboard events to see which one
+ * is worse, for some implementation-specific definition of "worse".
+ */
+ uint32_t GetSeverity();
+
+ /**
+ * Gets the number of CSS pixels that were checkerboarded at the peak of the
+ * checkerboard event.
+ */
+ uint32_t GetPeak();
+
+ /**
+ * Gets the length of the checkerboard event.
+ */
+ TimeDuration GetDuration();
+
+ /**
+ * Gets the raw log of the checkerboard event. This can be called any time,
+ * although it really only makes sense to pull once the event is done, after
+ * RecordFrameInfo returns true.
+ */
+ std::string GetLog();
+
+ /**
+ * Returns true iff this event is recording a detailed trace of the event.
+ * This is the argument passed in to the constructor.
+ */
+ bool IsRecordingTrace();
+
+ /**
+ * Provide a new value for one of the rects that is tracked for
+ * checkerboard events.
+ */
+ void UpdateRendertraceProperty(RendertraceProperty aProperty,
+ const CSSRect& aRect,
+ const std::string& aExtraInfo = std::string());
+
+ /**
+ * Provide the number of CSS pixels that are checkerboarded in a composite
+ * at the current time.
+ * @return true if the checkerboard event has completed. The caller should
+ * stop updating this object once this happens.
+ */
+ bool RecordFrameInfo(uint32_t aCssPixelsCheckerboarded);
+
+ private:
+ /**
+ * Helper method to do stuff when checkeboarding starts.
+ */
+ void StartEvent();
+ /**
+ * Helper method to do stuff when checkerboarding stops.
+ */
+ void StopEvent();
+
+ /**
+ * Helper method to log a rendertrace property and its value to the
+ * rendertrace info buffer (mRendertraceInfo).
+ */
+ void LogInfo(RendertraceProperty aProperty, const TimeStamp& aTimestamp,
+ const CSSRect& aRect, const std::string& aExtraInfo,
+ const MonitorAutoLock& aProofOfLock);
+
+ /**
+ * Helper struct that holds a single rendertrace property value.
+ */
+ struct PropertyValue {
+ RendertraceProperty mProperty;
+ TimeStamp mTimeStamp;
+ CSSRect mRect;
+ std::string mExtraInfo;
+
+ bool operator<(const PropertyValue& aOther) const;
+ };
+
+ /**
+ * A circular buffer that stores the most recent BUFFER_SIZE values of a
+ * given property.
+ */
+ class PropertyBuffer {
+ public:
+ PropertyBuffer();
+ /**
+ * Add a new value to the buffer, overwriting the oldest one if needed.
+ */
+ void Update(RendertraceProperty aProperty, const CSSRect& aRect,
+ const std::string& aExtraInfo,
+ const MonitorAutoLock& aProofOfLock);
+ /**
+ * Dump the recorded values, oldest to newest, to the given vector, and
+ * remove them from this buffer.
+ */
+ void Flush(std::vector<PropertyValue>& aOut,
+ const MonitorAutoLock& aProofOfLock);
+
+ private:
+ static const uint32_t BUFFER_SIZE = 5;
+
+ /**
+ * The index of the oldest value in the buffer. This is the next index
+ * that will be written to.
+ */
+ uint32_t mIndex;
+ PropertyValue mValues[BUFFER_SIZE];
+ };
+
+ private:
+ /**
+ * If true, we should log the various properties during the checkerboard
+ * event. If false, we only need to record things we need for telemetry
+ * measures.
+ */
+ const bool mRecordTrace;
+ /**
+ * A base time so that the other timestamps can be turned into durations.
+ */
+ const TimeStamp mOriginTime;
+ /**
+ * Whether or not a checkerboard event is currently occurring.
+ */
+ bool mCheckerboardingActive;
+
+ /**
+ * The start time of the checkerboard event.
+ */
+ TimeStamp mStartTime;
+ /**
+ * The end time of the checkerboard event.
+ */
+ TimeStamp mEndTime;
+ /**
+ * The sample time of the last frame recorded.
+ */
+ TimeStamp mLastSampleTime;
+ /**
+ * The number of contiguous frames with checkerboard.
+ */
+ uint32_t mFrameCount;
+ /**
+ * The total number of pixel-milliseconds of checkerboarding visible to
+ * the user during the checkerboarding event.
+ */
+ uint64_t mTotalPixelMs;
+ /**
+ * The largest number of pixels of checkerboarding visible to the user
+ * during any one frame, during this checkerboarding event.
+ */
+ uint32_t mPeakPixels;
+
+ /**
+ * Monitor that needs to be acquired before touching mBufferedProperties
+ * or mRendertraceInfo.
+ */
+ mutable Monitor mRendertraceLock MOZ_UNANNOTATED;
+ /**
+ * A circular buffer to store some properties. This is used before the
+ * checkerboarding actually starts, so that we have some data on what
+ * was happening before the checkerboarding started.
+ */
+ PropertyBuffer mBufferedProperties[sRendertracePropertyCount];
+ /**
+ * The rendertrace info buffer that gives us info on what was happening
+ * during the checkerboard event.
+ */
+ std::ostringstream mRendertraceInfo;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_CheckerboardEvent_h
diff --git a/gfx/layers/apz/src/DesktopFlingPhysics.h b/gfx/layers/apz/src/DesktopFlingPhysics.h
new file mode 100644
index 0000000000..e93cc07a23
--- /dev/null
+++ b/gfx/layers/apz/src/DesktopFlingPhysics.h
@@ -0,0 +1,67 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_DesktopFlingPhysics_h_
+#define mozilla_layers_DesktopFlingPhysics_h_
+
+#include "AsyncPanZoomController.h"
+#include "Units.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/StaticPrefs_apz.h"
+
+namespace mozilla {
+namespace layers {
+
+class DesktopFlingPhysics {
+ public:
+ void Init(const ParentLayerPoint& aStartingVelocity,
+ float aPLPPI /* unused */) {
+ mVelocity = aStartingVelocity;
+ }
+ void Sample(const TimeDuration& aDelta, ParentLayerPoint* aOutVelocity,
+ ParentLayerPoint* aOutOffset) {
+ float friction = StaticPrefs::apz_fling_friction();
+ float threshold = StaticPrefs::apz_fling_stopped_threshold();
+
+ mVelocity = ParentLayerPoint(
+ ApplyFrictionOrCancel(mVelocity.x, aDelta, friction, threshold),
+ ApplyFrictionOrCancel(mVelocity.y, aDelta, friction, threshold));
+
+ *aOutVelocity = mVelocity;
+ *aOutOffset = mVelocity * aDelta.ToMilliseconds();
+ }
+
+ private:
+ /**
+ * Applies friction to the given velocity and returns the result, or
+ * returns zero if the velocity is too low.
+ * |aVelocity| is the incoming velocity.
+ * |aDelta| is the amount of time that has passed since the last time
+ * friction was applied.
+ * |aFriction| is the amount of friction to apply.
+ * |aThreshold| is the velocity below which the fling is cancelled.
+ */
+ static float ApplyFrictionOrCancel(float aVelocity,
+ const TimeDuration& aDelta,
+ float aFriction, float aThreshold) {
+ if (fabsf(aVelocity) <= aThreshold) {
+ // If the velocity is very low, just set it to 0 and stop the fling,
+ // otherwise we'll just asymptotically approach 0 and the user won't
+ // actually see any changes.
+ return 0.0f;
+ }
+
+ aVelocity *= pow(1.0f - aFriction, float(aDelta.ToMilliseconds()));
+ return aVelocity;
+ }
+
+ ParentLayerPoint mVelocity;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_DesktopFlingPhysics_h_
diff --git a/gfx/layers/apz/src/DragTracker.cpp b/gfx/layers/apz/src/DragTracker.cpp
new file mode 100644
index 0000000000..aa3b2a34f7
--- /dev/null
+++ b/gfx/layers/apz/src/DragTracker.cpp
@@ -0,0 +1,59 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "DragTracker.h"
+
+#include "InputData.h"
+#include "mozilla/Logging.h"
+
+static mozilla::LazyLogModule sApzDrgLog("apz.drag");
+#define DRAG_LOG(...) MOZ_LOG(sApzDrgLog, LogLevel::Debug, (__VA_ARGS__))
+
+namespace mozilla {
+namespace layers {
+
+DragTracker::DragTracker() : mInDrag(false) {}
+
+/*static*/
+bool DragTracker::StartsDrag(const MouseInput& aInput) {
+ return aInput.IsLeftButton() && aInput.mType == MouseInput::MOUSE_DOWN;
+}
+
+/*static*/
+bool DragTracker::EndsDrag(const MouseInput& aInput) {
+ // On Windows, we don't receive a MOUSE_UP at the end of a drag if an
+ // actual drag session took place. As a backup, we detect the end of the
+ // drag using the MOUSE_DRAG_END event, which normally is routed directly
+ // to content, but we're specially routing to APZ for this purpose. Bug
+ // 1265105 tracks a solution to this at the Windows widget layer; once
+ // that is implemented, this workaround can be removed.
+ return (aInput.IsLeftButton() && aInput.mType == MouseInput::MOUSE_UP) ||
+ aInput.mType == MouseInput::MOUSE_DRAG_END;
+}
+
+void DragTracker::Update(const MouseInput& aInput) {
+ if (StartsDrag(aInput)) {
+ DRAG_LOG("Starting drag\n");
+ mInDrag = true;
+ } else if (EndsDrag(aInput)) {
+ DRAG_LOG("Ending drag\n");
+ mInDrag = false;
+ mOnScrollbar = Nothing();
+ }
+}
+
+bool DragTracker::InDrag() const { return mInDrag; }
+
+bool DragTracker::IsOnScrollbar(bool aOnScrollbar) {
+ if (!mOnScrollbar) {
+ DRAG_LOG("Setting hitscrollbar %d\n", aOnScrollbar);
+ mOnScrollbar = Some(aOnScrollbar);
+ }
+ return mOnScrollbar.value();
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/DragTracker.h b/gfx/layers/apz/src/DragTracker.h
new file mode 100644
index 0000000000..92678d53c1
--- /dev/null
+++ b/gfx/layers/apz/src/DragTracker.h
@@ -0,0 +1,39 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_DragTracker_h
+#define mozilla_layers_DragTracker_h
+
+#include "mozilla/EventForwards.h"
+#include "mozilla/Maybe.h"
+
+namespace mozilla {
+
+class MouseInput;
+
+namespace layers {
+
+// DragTracker simply tracks a sequence of mouse inputs and allows us to tell
+// if we are in a drag or not (i.e. the left mouse button went down and hasn't
+// gone up yet).
+class DragTracker {
+ public:
+ DragTracker();
+ static bool StartsDrag(const MouseInput& aInput);
+ static bool EndsDrag(const MouseInput& aInput);
+ void Update(const MouseInput& aInput);
+ bool InDrag() const;
+ bool IsOnScrollbar(bool aOnScrollbar);
+
+ private:
+ Maybe<bool> mOnScrollbar;
+ bool mInDrag;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif /* mozilla_layers_DragTracker_h */
diff --git a/gfx/layers/apz/src/ExpectedGeckoMetrics.cpp b/gfx/layers/apz/src/ExpectedGeckoMetrics.cpp
new file mode 100644
index 0000000000..26b94e94d4
--- /dev/null
+++ b/gfx/layers/apz/src/ExpectedGeckoMetrics.cpp
@@ -0,0 +1,26 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ExpectedGeckoMetrics.h"
+#include "FrameMetrics.h"
+
+namespace mozilla {
+namespace layers {
+
+void ExpectedGeckoMetrics::UpdateFrom(const FrameMetrics& aMetrics) {
+ mVisualScrollOffset = aMetrics.GetVisualScrollOffset();
+ mLayoutScrollOffset = aMetrics.GetLayoutScrollOffset();
+ mZoom = aMetrics.GetZoom();
+ mDevPixelsPerCSSPixel = aMetrics.GetDevPixelsPerCSSPixel();
+}
+
+void ExpectedGeckoMetrics::UpdateZoomFrom(const FrameMetrics& aMetrics) {
+ mZoom = aMetrics.GetZoom();
+ mDevPixelsPerCSSPixel = aMetrics.GetDevPixelsPerCSSPixel();
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/ExpectedGeckoMetrics.h b/gfx/layers/apz/src/ExpectedGeckoMetrics.h
new file mode 100644
index 0000000000..ca5aa76eba
--- /dev/null
+++ b/gfx/layers/apz/src/ExpectedGeckoMetrics.h
@@ -0,0 +1,44 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_ExpectedGeckoMetrics_h
+#define mozilla_layers_ExpectedGeckoMetrics_h
+
+#include "Units.h"
+
+namespace mozilla {
+namespace layers {
+
+struct FrameMetrics;
+
+// A class that stores a subset of the FrameMetrics information
+// than an APZC instance expects Gecko to have (either the
+// metrics that were most recently sent to Gecko, or the ones
+// most recently received from Gecko).
+class ExpectedGeckoMetrics {
+ public:
+ ExpectedGeckoMetrics() = default;
+ void UpdateFrom(const FrameMetrics& aMetrics);
+ void UpdateZoomFrom(const FrameMetrics& aMetrics);
+
+ const CSSPoint& GetVisualScrollOffset() const { return mVisualScrollOffset; }
+ const CSSPoint& GetLayoutScrollOffset() const { return mLayoutScrollOffset; }
+ const CSSToParentLayerScale& GetZoom() const { return mZoom; }
+ const CSSToLayoutDeviceScale& GetDevPixelsPerCSSPixel() const {
+ return mDevPixelsPerCSSPixel;
+ }
+
+ private:
+ CSSPoint mVisualScrollOffset;
+ CSSPoint mLayoutScrollOffset;
+ CSSToParentLayerScale mZoom;
+ CSSToLayoutDeviceScale mDevPixelsPerCSSPixel;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif
diff --git a/gfx/layers/apz/src/FlingAccelerator.cpp b/gfx/layers/apz/src/FlingAccelerator.cpp
new file mode 100644
index 0000000000..f37d04c77b
--- /dev/null
+++ b/gfx/layers/apz/src/FlingAccelerator.cpp
@@ -0,0 +1,128 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "FlingAccelerator.h"
+
+#include "mozilla/StaticPrefs_apz.h"
+
+#include "GenericFlingAnimation.h" // for FLING_LOG and FlingHandoffState
+
+namespace mozilla {
+namespace layers {
+
+void FlingAccelerator::Reset() {
+ mPreviousFlingStartingVelocity = ParentLayerPoint{};
+ mPreviousFlingCancelVelocity = ParentLayerPoint{};
+ mIsTracking = false;
+}
+
+static bool SameDirection(float aVelocity1, float aVelocity2) {
+ return (aVelocity1 == 0.0f) || (aVelocity2 == 0.0f) ||
+ (IsNegative(aVelocity1) == IsNegative(aVelocity2));
+}
+
+static float Accelerate(float aBase, float aSupplemental) {
+ return (aBase * StaticPrefs::apz_fling_accel_base_mult()) +
+ (aSupplemental * StaticPrefs::apz_fling_accel_supplemental_mult());
+}
+
+ParentLayerPoint FlingAccelerator::GetFlingStartingVelocity(
+ const SampleTime& aNow, const ParentLayerPoint& aVelocity,
+ const FlingHandoffState& aHandoffState) {
+ // If the fling should be accelerated and is in the same direction as the
+ // previous fling, boost the velocity to be the sum of the two. Check separate
+ // axes separately because we could have two vertical flings with small
+ // horizontal components on the opposite side of zero, and we still want the
+ // y-fling to get accelerated.
+ ParentLayerPoint velocity = aVelocity;
+ if (ShouldAccelerate(aNow, aVelocity, aHandoffState)) {
+ if (velocity.x != 0 &&
+ SameDirection(velocity.x, mPreviousFlingStartingVelocity.x)) {
+ velocity.x = Accelerate(velocity.x, mPreviousFlingStartingVelocity.x);
+ FLING_LOG("%p Applying fling x-acceleration from %f to %f (delta %f)\n",
+ this, aVelocity.x.value, velocity.x.value,
+ mPreviousFlingStartingVelocity.x.value);
+ }
+ if (velocity.y != 0 &&
+ SameDirection(velocity.y, mPreviousFlingStartingVelocity.y)) {
+ velocity.y = Accelerate(velocity.y, mPreviousFlingStartingVelocity.y);
+ FLING_LOG("%p Applying fling y-acceleration from %f to %f (delta %f)\n",
+ this, aVelocity.y.value, velocity.y.value,
+ mPreviousFlingStartingVelocity.y.value);
+ }
+ }
+
+ Reset();
+
+ mPreviousFlingStartingVelocity = velocity;
+ mIsTracking = true;
+
+ return velocity;
+}
+
+bool FlingAccelerator::ShouldAccelerate(
+ const SampleTime& aNow, const ParentLayerPoint& aVelocity,
+ const FlingHandoffState& aHandoffState) const {
+ if (!IsTracking()) {
+ FLING_LOG("%p Fling accelerator was reset, not accelerating.\n", this);
+ return false;
+ }
+
+ if (!aHandoffState.mTouchStartRestingTime) {
+ FLING_LOG("%p Don't have a touch start resting time, not accelerating.\n",
+ this);
+ return false;
+ }
+
+ double msBetweenTouchStartAndPanStart =
+ aHandoffState.mTouchStartRestingTime->ToMilliseconds();
+ FLING_LOG(
+ "%p ShouldAccelerate with pan velocity %f pixels/ms, min pan velocity %f "
+ "pixels/ms, previous fling cancel velocity %f pixels/ms, time elapsed "
+ "since starting previous time between touch start and pan "
+ "start %fms.\n",
+ this, float(aVelocity.Length()), float(aHandoffState.mMinPanVelocity),
+ float(mPreviousFlingCancelVelocity.Length()),
+ float(msBetweenTouchStartAndPanStart));
+
+ if (aVelocity.Length() < StaticPrefs::apz_fling_accel_min_fling_velocity()) {
+ FLING_LOG("%p Fling velocity too low (%f), not accelerating.\n", this,
+ float(aVelocity.Length()));
+ return false;
+ }
+
+ if (aHandoffState.mMinPanVelocity <
+ StaticPrefs::apz_fling_accel_min_pan_velocity()) {
+ FLING_LOG(
+ "%p Panning velocity was too slow at some point during the pan (%f), "
+ "not accelerating.\n",
+ this, float(aHandoffState.mMinPanVelocity));
+ return false;
+ }
+
+ if (mPreviousFlingCancelVelocity.Length() <
+ StaticPrefs::apz_fling_accel_min_fling_velocity()) {
+ FLING_LOG(
+ "%p The previous fling animation had slowed down too much when it was "
+ "interrupted (%f), not accelerating.\n",
+ this, float(mPreviousFlingCancelVelocity.Length()));
+ return false;
+ }
+
+ if (msBetweenTouchStartAndPanStart >=
+ StaticPrefs::apz_fling_accel_max_pause_interval_ms()) {
+ FLING_LOG(
+ "%p Too much time (%fms) elapsed between touch start and pan start, "
+ "not accelerating.\n",
+ this, msBetweenTouchStartAndPanStart);
+ return false;
+ }
+
+ return true;
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/FlingAccelerator.h b/gfx/layers/apz/src/FlingAccelerator.h
new file mode 100644
index 0000000000..49e9a7b257
--- /dev/null
+++ b/gfx/layers/apz/src/FlingAccelerator.h
@@ -0,0 +1,59 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_FlingAccelerator_h
+#define mozilla_layers_FlingAccelerator_h
+
+#include "mozilla/layers/SampleTime.h"
+#include "Units.h"
+
+namespace mozilla {
+namespace layers {
+
+struct FlingHandoffState;
+
+/**
+ * This class is used to track state that is used when determining whether a
+ * fling should be accelerated.
+ */
+class FlingAccelerator final {
+ public:
+ FlingAccelerator() {}
+
+ // Resets state so that the next fling will not be accelerated.
+ void Reset();
+
+ // Returns false after a reset or before the first fling.
+ bool IsTracking() const { return mIsTracking; }
+
+ // Starts a new fling, and returns the (potentially accelerated) velocity that
+ // should be used for that fling.
+ ParentLayerPoint GetFlingStartingVelocity(
+ const SampleTime& aNow, const ParentLayerPoint& aVelocity,
+ const FlingHandoffState& aHandoffState);
+
+ void ObserveFlingCanceled(const ParentLayerPoint& aVelocity) {
+ mPreviousFlingCancelVelocity = aVelocity;
+ }
+
+ protected:
+ bool ShouldAccelerate(const SampleTime& aNow,
+ const ParentLayerPoint& aVelocity,
+ const FlingHandoffState& aHandoffState) const;
+
+ // The initial velocity of the most recent fling.
+ ParentLayerPoint mPreviousFlingStartingVelocity;
+ // The velocity that the previous fling animation had at the point it was
+ // interrupted.
+ ParentLayerPoint mPreviousFlingCancelVelocity;
+ // Whether the upcoming fling is eligible for acceleration.
+ bool mIsTracking = false;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_FlingAccelerator_h
diff --git a/gfx/layers/apz/src/FocusState.cpp b/gfx/layers/apz/src/FocusState.cpp
new file mode 100644
index 0000000000..b7510bcef4
--- /dev/null
+++ b/gfx/layers/apz/src/FocusState.cpp
@@ -0,0 +1,225 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "FocusState.h"
+
+#include "mozilla/Logging.h"
+#include "mozilla/layers/APZThreadUtils.h"
+
+static mozilla::LazyLogModule sApzFstLog("apz.focusstate");
+#define FS_LOG(...) MOZ_LOG(sApzFstLog, LogLevel::Debug, (__VA_ARGS__))
+
+namespace mozilla {
+namespace layers {
+
+FocusState::FocusState()
+ : mMutex("FocusStateMutex"),
+ mLastAPZProcessedEvent(1),
+ mLastContentProcessedEvent(0),
+ mFocusHasKeyEventListeners(false),
+ mReceivedUpdate(false),
+ mFocusLayersId{0},
+ mFocusHorizontalTarget(ScrollableLayerGuid::NULL_SCROLL_ID),
+ mFocusVerticalTarget(ScrollableLayerGuid::NULL_SCROLL_ID) {}
+
+uint64_t FocusState::LastAPZProcessedEvent() const {
+ APZThreadUtils::AssertOnControllerThread();
+ MutexAutoLock lock(mMutex);
+
+ return mLastAPZProcessedEvent;
+}
+
+bool FocusState::IsCurrent(const MutexAutoLock& aProofOfLock) const {
+ FS_LOG("Checking IsCurrent() with cseq=%" PRIu64 ", aseq=%" PRIu64 "\n",
+ mLastContentProcessedEvent, mLastAPZProcessedEvent);
+
+ MOZ_ASSERT(mLastContentProcessedEvent <= mLastAPZProcessedEvent);
+ return mLastContentProcessedEvent == mLastAPZProcessedEvent;
+}
+
+void FocusState::ReceiveFocusChangingEvent() {
+ APZThreadUtils::AssertOnControllerThread();
+ MutexAutoLock lock(mMutex);
+
+ if (!mReceivedUpdate) {
+ // In the initial state don't advance mLastAPZProcessedEvent because we
+ // might blow away the information that we're in a freshly-restarted GPU
+ // process. This information (i.e. that mLastAPZProcessedEvent == 1) needs
+ // to be preserved until the first call to Update() which will then advance
+ // mLastAPZProcessedEvent to match the content-side sequence number.
+ return;
+ }
+ mLastAPZProcessedEvent += 1;
+ FS_LOG("Focus changing event incremented aseq to %" PRIu64 "\n",
+ mLastAPZProcessedEvent);
+}
+
+void FocusState::Update(LayersId aRootLayerTreeId,
+ LayersId aOriginatingLayersId,
+ const FocusTarget& aState) {
+ // This runs on the updater thread, it's not worth passing around extra raw
+ // pointers just to assert it.
+
+ MutexAutoLock lock(mMutex);
+
+ FS_LOG("Update with rlt=%" PRIu64 ", olt=%" PRIu64 ", ft=(%s, %" PRIu64 ")\n",
+ aRootLayerTreeId.mId, aOriginatingLayersId.mId, aState.Type(),
+ aState.mSequenceNumber);
+ mReceivedUpdate = true;
+
+ // Update the focus tree with the latest target
+ mFocusTree[aOriginatingLayersId] = aState;
+
+ // Reset our internal state so we can recalculate it
+ mFocusHasKeyEventListeners = false;
+ mFocusLayersId = aRootLayerTreeId;
+ mFocusHorizontalTarget = ScrollableLayerGuid::NULL_SCROLL_ID;
+ mFocusVerticalTarget = ScrollableLayerGuid::NULL_SCROLL_ID;
+
+ // To update the focus state for the entire APZCTreeManager, we need
+ // to traverse the focus tree to find the current leaf which is the global
+ // focus target we can use for async keyboard scrolling
+ while (true) {
+ auto currentNode = mFocusTree.find(mFocusLayersId);
+ if (currentNode == mFocusTree.end()) {
+ FS_LOG("Setting target to nil (cannot find lt=%" PRIu64 ")\n",
+ mFocusLayersId.mId);
+ return;
+ }
+
+ const FocusTarget& target = currentNode->second;
+
+ // Accumulate event listener flags on the path to the focus target
+ mFocusHasKeyEventListeners |= target.mFocusHasKeyEventListeners;
+
+ // Match on the data stored in mData
+ // The match functions return true or false depending on whether the
+ // enclosing method, FocusState::Update, should return or continue to the
+ // next iteration of the while loop, respectively.
+ struct FocusTargetDataMatcher {
+ FocusState& mFocusState;
+ const uint64_t mSequenceNumber;
+
+ bool operator()(const FocusTarget::NoFocusTarget& aNoFocusTarget) {
+ FS_LOG("Setting target to nil (reached a nil target) with seq=%" PRIu64
+ "\n",
+ mSequenceNumber);
+
+ // Mark what sequence number this target has for debugging purposes so
+ // we can always accurately report on whether we are stale or not
+ mFocusState.mLastContentProcessedEvent = mSequenceNumber;
+
+ // If this focus state was just created and content has experienced more
+ // events then us, then assume we were recreated and sync focus sequence
+ // numbers.
+ if (mFocusState.mLastAPZProcessedEvent == 1 &&
+ mFocusState.mLastContentProcessedEvent >
+ mFocusState.mLastAPZProcessedEvent) {
+ mFocusState.mLastAPZProcessedEvent =
+ mFocusState.mLastContentProcessedEvent;
+ }
+ return true;
+ }
+
+ bool operator()(const LayersId& aRefLayerId) {
+ // Guard against infinite loops
+ MOZ_ASSERT(mFocusState.mFocusLayersId != aRefLayerId);
+ if (mFocusState.mFocusLayersId == aRefLayerId) {
+ FS_LOG(
+ "Setting target to nil (bailing out of infinite loop, lt=%" PRIu64
+ ")\n",
+ mFocusState.mFocusLayersId.mId);
+ return true;
+ }
+
+ FS_LOG("Looking for target in lt=%" PRIu64 "\n", aRefLayerId.mId);
+
+ // The focus target is in a child layer tree
+ mFocusState.mFocusLayersId = aRefLayerId;
+ return false;
+ }
+
+ bool operator()(const FocusTarget::ScrollTargets& aScrollTargets) {
+ FS_LOG("Setting target to h=%" PRIu64 ", v=%" PRIu64
+ ", and seq=%" PRIu64 "\n",
+ aScrollTargets.mHorizontal, aScrollTargets.mVertical,
+ mSequenceNumber);
+
+ // This is the global focus target
+ mFocusState.mFocusHorizontalTarget = aScrollTargets.mHorizontal;
+ mFocusState.mFocusVerticalTarget = aScrollTargets.mVertical;
+
+ // Mark what sequence number this target has so we can determine whether
+ // it is stale or not
+ mFocusState.mLastContentProcessedEvent = mSequenceNumber;
+
+ // If this focus state was just created and content has experienced more
+ // events then us, then assume we were recreated and sync focus sequence
+ // numbers.
+ if (mFocusState.mLastAPZProcessedEvent == 1 &&
+ mFocusState.mLastContentProcessedEvent >
+ mFocusState.mLastAPZProcessedEvent) {
+ mFocusState.mLastAPZProcessedEvent =
+ mFocusState.mLastContentProcessedEvent;
+ }
+ return true;
+ }
+ }; // struct FocusTargetDataMatcher
+
+ if (target.mData.match(
+ FocusTargetDataMatcher{*this, target.mSequenceNumber})) {
+ return;
+ }
+ }
+}
+
+void FocusState::RemoveFocusTarget(LayersId aLayersId) {
+ // This runs on the updater thread, it's not worth passing around extra raw
+ // pointers just to assert it.
+ MutexAutoLock lock(mMutex);
+
+ mFocusTree.erase(aLayersId);
+}
+
+Maybe<ScrollableLayerGuid> FocusState::GetHorizontalTarget() const {
+ APZThreadUtils::AssertOnControllerThread();
+ MutexAutoLock lock(mMutex);
+
+ // There is not a scrollable layer to async scroll if
+ // 1. We aren't current
+ // 2. There are event listeners that could change the focus
+ // 3. The target has not been layerized
+ if (!IsCurrent(lock) || mFocusHasKeyEventListeners ||
+ mFocusHorizontalTarget == ScrollableLayerGuid::NULL_SCROLL_ID) {
+ return Nothing();
+ }
+ return Some(ScrollableLayerGuid(mFocusLayersId, 0, mFocusHorizontalTarget));
+}
+
+Maybe<ScrollableLayerGuid> FocusState::GetVerticalTarget() const {
+ APZThreadUtils::AssertOnControllerThread();
+ MutexAutoLock lock(mMutex);
+
+ // There is not a scrollable layer to async scroll if:
+ // 1. We aren't current
+ // 2. There are event listeners that could change the focus
+ // 3. The target has not been layerized
+ if (!IsCurrent(lock) || mFocusHasKeyEventListeners ||
+ mFocusVerticalTarget == ScrollableLayerGuid::NULL_SCROLL_ID) {
+ return Nothing();
+ }
+ return Some(ScrollableLayerGuid(mFocusLayersId, 0, mFocusVerticalTarget));
+}
+
+bool FocusState::CanIgnoreKeyboardShortcutMisses() const {
+ APZThreadUtils::AssertOnControllerThread();
+ MutexAutoLock lock(mMutex);
+
+ return IsCurrent(lock) && !mFocusHasKeyEventListeners;
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/FocusState.h b/gfx/layers/apz/src/FocusState.h
new file mode 100644
index 0000000000..14d536be3e
--- /dev/null
+++ b/gfx/layers/apz/src/FocusState.h
@@ -0,0 +1,175 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_FocusState_h
+#define mozilla_layers_FocusState_h
+
+#include <unordered_map> // for std::unordered_map
+#include <unordered_set> // for std::unordered_set
+
+#include "mozilla/layers/FocusTarget.h" // for FocusTarget
+#include "mozilla/layers/ScrollableLayerGuid.h" // for ViewID
+#include "mozilla/Mutex.h" // for Mutex
+
+namespace mozilla {
+namespace layers {
+
+/**
+ * This class is used for tracking chrome and content focus targets and
+ * calculating global focus information from them for use by APZCTreeManager
+ * for async keyboard scrolling.
+ *
+ * # Calculating the element to scroll
+ *
+ * Chrome and content processes have independently focused elements. This makes
+ * it difficult to calculate the global focused element and its scrollable
+ * frame from the chrome or content side. So instead we send the local focus
+ * information from each process to here and then calculate the global focus
+ * information. This local information resides in a `focus target`.
+ *
+ * A focus target indicates that either:
+ * 1. The focused element is a remote browser along with its layer tree ID
+ * 2. The focused element is not scrollable
+ * 3. The focused element is scrollable along with the ViewID's of its
+ scrollable layers
+ *
+ * Using this information we can determine the global focus information by
+ * starting at the focus target of the root layer tree ID and following remote
+ * browsers until we reach a scrollable or non-scrollable focus target.
+ *
+ * # Determinism and sequence numbers
+ *
+ * The focused element in content can be changed within any javascript code. And
+ * javascript can run in response to an event or at any moment from `setTimeout`
+ * and others. This makes it impossible to always have the current focus
+ * information in APZ as it can be changed asynchronously at any moment. If we
+ * don't have the latest focus information, we may incorrectly scroll a target
+ * when we shouldn't.
+ *
+ * A tradeoff is designed here whereby we will maintain deterministic focus
+ * changes for user input, but not for other javascript code. The reasoning
+ * here is that `setTimeout` and others are already non-deterministic and so it
+ * might not be as breaking to web content.
+ *
+ * To maintain deterministic focus changes for a given stream of user inputs,
+ * we invalidate our focus state whenever we receive a user input that may
+ * trigger event listeners. We then attach a new sequence number to these
+ * events and dispatch them to content. Content will then include the latest
+ * sequence number it has processed to every focus update. Using this we can
+ * determine whether any potentially focus changing events have yet to be
+ * handled by content.
+ *
+ * Once we have received the latest focus sequence number from content, we know
+ * that all event listeners triggered by user inputs, and their resulting focus
+ * changes, have been processed and so we have a current target that we can use
+ * again.
+ */
+class FocusState final {
+ public:
+ FocusState();
+
+ /**
+ * The sequence number of the last potentially focus changing event processed
+ * by APZ. This number starts at one and increases monotonically. This number
+ * will never be zero as that is used to catch uninitialized focus sequence
+ * numbers on input events.
+ */
+ uint64_t LastAPZProcessedEvent() const;
+
+ /**
+ * Notify focus state of a potentially focus changing event. This will
+ * increment the current focus sequence number. The new value can be gotten
+ * from LastAPZProcessedEvent().
+ */
+ void ReceiveFocusChangingEvent();
+
+ /**
+ * Update the internal focus tree and recalculate the global focus target for
+ * a focus target update received from chrome or content.
+ *
+ * @param aRootLayerTreeId the layer tree ID of the root layer for the
+ parent APZCTreeManager
+ * @param aOriginatingLayersId the layer tree ID that this focus target
+ belongs to
+ */
+ void Update(LayersId aRootLayerTreeId, LayersId aOriginatingLayersId,
+ const FocusTarget& aTarget);
+
+ /**
+ * Removes a focus target by its layer tree ID.
+ */
+ void RemoveFocusTarget(LayersId aLayersId);
+
+ /**
+ * Gets the scrollable layer that should be horizontally scrolled for a key
+ * event, if any. The returned ScrollableLayerGuid doesn't contain a
+ * presShellId, and so it should not be used in comparisons.
+ *
+ * No scrollable layer is returned if any of the following are true:
+ * 1. We don't have a current focus target
+ * 2. There are event listeners that could change the focus
+ * 3. The target has not been layerized
+ */
+ Maybe<ScrollableLayerGuid> GetHorizontalTarget() const;
+ /**
+ * The same as GetHorizontalTarget() but for vertical scrolling.
+ */
+ Maybe<ScrollableLayerGuid> GetVerticalTarget() const;
+
+ /**
+ * Gets whether it is safe to not increment the focus sequence number for an
+ * unmatched keyboard event.
+ */
+ bool CanIgnoreKeyboardShortcutMisses() const;
+
+ private:
+ /**
+ * Whether the current focus state is known to be current or else if an event
+ * has been processed that could change the focus but we have not received an
+ * update with a new confirmed target.
+ * This can only be called by methods that have already acquired mMutex; they
+ * have to pass their lock as compile-time proof.
+ */
+ bool IsCurrent(const MutexAutoLock& aLock) const;
+
+ private:
+ // All methods should hold this lock, since this class is accessed via both
+ // the updater and controller threads.
+ mutable Mutex mMutex MOZ_UNANNOTATED;
+
+ // The set of focus targets received indexed by their layer tree ID
+ std::unordered_map<LayersId, FocusTarget, LayersId::HashFn> mFocusTree;
+
+ // The focus sequence number of the last potentially focus changing event
+ // processed by APZ. This number starts at one and increases monotonically.
+ // We don't worry about wrap around here because at a pace of 100
+ // increments/sec, it would take 5.85*10^9 years before we would wrap around.
+ // This number will never be zero as that is used to catch uninitialized focus
+ // sequence numbers on input events.
+ uint64_t mLastAPZProcessedEvent;
+ // The focus sequence number last received in a focus update.
+ uint64_t mLastContentProcessedEvent;
+
+ // A flag whether there is a key listener on the event target chain for the
+ // focused element
+ bool mFocusHasKeyEventListeners;
+ // A flag that is false until the first call to Update().
+ bool mReceivedUpdate;
+
+ // The layer tree ID which contains the scrollable frame of the focused
+ // element
+ LayersId mFocusLayersId;
+ // The scrollable layer corresponding to the scrollable frame that is used to
+ // scroll the focused element. This depends on the direction the user is
+ // scrolling.
+ ScrollableLayerGuid::ViewID mFocusHorizontalTarget;
+ ScrollableLayerGuid::ViewID mFocusVerticalTarget;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_FocusState_h
diff --git a/gfx/layers/apz/src/FocusTarget.cpp b/gfx/layers/apz/src/FocusTarget.cpp
new file mode 100644
index 0000000000..f1f6463e5c
--- /dev/null
+++ b/gfx/layers/apz/src/FocusTarget.cpp
@@ -0,0 +1,233 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/layers/FocusTarget.h"
+#include "mozilla/dom/BrowserBridgeChild.h" // for BrowserBridgeChild
+#include "mozilla/dom/EventTarget.h" // for EventTarget
+#include "mozilla/dom/RemoteBrowser.h" // For RemoteBrowser
+#include "mozilla/EventDispatcher.h" // for EventDispatcher
+#include "mozilla/PresShell.h" // For PresShell
+#include "mozilla/StaticPrefs_apz.h"
+#include "nsIContentInlines.h" // for nsINode::IsEditable()
+#include "nsLayoutUtils.h" // for nsLayoutUtils
+
+static mozilla::LazyLogModule sApzFtgLog("apz.focustarget");
+#define FT_LOG(...) MOZ_LOG(sApzFtgLog, LogLevel::Debug, (__VA_ARGS__))
+
+using namespace mozilla::dom;
+using namespace mozilla::layout;
+
+namespace mozilla {
+namespace layers {
+
+static PresShell* GetRetargetEventPresShell(PresShell* aRootPresShell) {
+ MOZ_ASSERT(aRootPresShell);
+
+ // Use the last focused window in this PresShell and its
+ // associated PresShell
+ nsCOMPtr<nsPIDOMWindowOuter> window =
+ aRootPresShell->GetFocusedDOMWindowInOurWindow();
+ if (!window) {
+ return nullptr;
+ }
+
+ RefPtr<Document> retargetEventDoc = window->GetExtantDoc();
+ if (!retargetEventDoc) {
+ return nullptr;
+ }
+
+ return retargetEventDoc->GetPresShell();
+}
+
+// _BOUNDARY because Dispatch() with `targets` must not handle the event.
+MOZ_CAN_RUN_SCRIPT_BOUNDARY static bool HasListenersForKeyEvents(
+ nsIContent* aContent) {
+ if (!aContent) {
+ return false;
+ }
+
+ WidgetEvent event(true, eVoidEvent);
+ nsTArray<EventTarget*> targets;
+ nsresult rv = EventDispatcher::Dispatch(aContent, nullptr, &event, nullptr,
+ nullptr, nullptr, &targets);
+ NS_ENSURE_SUCCESS(rv, false);
+ for (size_t i = 0; i < targets.Length(); i++) {
+ if (targets[i]->HasNonSystemGroupListenersForUntrustedKeyEvents()) {
+ return true;
+ }
+ }
+ return false;
+}
+
+// _BOUNDARY because Dispatch() with `targets` must not handle the event.
+MOZ_CAN_RUN_SCRIPT_BOUNDARY static bool HasListenersForNonPassiveKeyEvents(
+ nsIContent* aContent) {
+ if (!aContent) {
+ return false;
+ }
+
+ WidgetEvent event(true, eVoidEvent);
+ nsTArray<EventTarget*> targets;
+ nsresult rv = EventDispatcher::Dispatch(aContent, nullptr, &event, nullptr,
+ nullptr, nullptr, &targets);
+ NS_ENSURE_SUCCESS(rv, false);
+ for (size_t i = 0; i < targets.Length(); i++) {
+ if (targets[i]
+ ->HasNonPassiveNonSystemGroupListenersForUntrustedKeyEvents()) {
+ return true;
+ }
+ }
+ return false;
+}
+
+static bool IsEditableNode(nsINode* aNode) {
+ return aNode && aNode->IsEditable();
+}
+
+FocusTarget::FocusTarget()
+ : mSequenceNumber(0),
+ mFocusHasKeyEventListeners(false),
+ mData(AsVariant(NoFocusTarget())) {}
+
+FocusTarget::FocusTarget(PresShell* aRootPresShell,
+ uint64_t aFocusSequenceNumber)
+ : mSequenceNumber(aFocusSequenceNumber),
+ mFocusHasKeyEventListeners(false),
+ mData(AsVariant(NoFocusTarget())) {
+ MOZ_ASSERT(aRootPresShell);
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // Key events can be retargeted to a child PresShell when there is an iframe
+ RefPtr<PresShell> presShell = GetRetargetEventPresShell(aRootPresShell);
+
+ if (!presShell) {
+ FT_LOG("Creating nil target with seq=%" PRIu64
+ " (can't find retargeted presshell)\n",
+ aFocusSequenceNumber);
+
+ return;
+ }
+
+ RefPtr<Document> document = presShell->GetDocument();
+ if (!document) {
+ FT_LOG("Creating nil target with seq=%" PRIu64 " (no document)\n",
+ aFocusSequenceNumber);
+
+ return;
+ }
+
+ // Find the focused content and use it to determine whether there are key
+ // event listeners or whether key events will be targeted at a different
+ // process through a remote browser.
+ nsCOMPtr<nsIContent> focusedContent =
+ presShell->GetFocusedContentInOurWindow();
+ nsCOMPtr<nsIContent> keyEventTarget = focusedContent;
+
+ // If there is no focused element then event dispatch goes to the body of
+ // the page if it exists or the root element.
+ if (!keyEventTarget) {
+ keyEventTarget = document->GetUnfocusedKeyEventTarget();
+ }
+
+ // Check if there are key event listeners that could prevent default or change
+ // the focus or selection of the page.
+ if (StaticPrefs::apz_keyboard_passive_listeners()) {
+ mFocusHasKeyEventListeners =
+ HasListenersForNonPassiveKeyEvents(keyEventTarget.get());
+ } else {
+ mFocusHasKeyEventListeners = HasListenersForKeyEvents(keyEventTarget.get());
+ }
+
+ // Check if the key event target is content editable or if the document
+ // is in design mode.
+ if (IsEditableNode(keyEventTarget) || IsEditableNode(document)) {
+ FT_LOG("Creating nil target with seq=%" PRIu64
+ ", kl=%d (disabling for editable node)\n",
+ aFocusSequenceNumber, static_cast<int>(mFocusHasKeyEventListeners));
+
+ return;
+ }
+
+ // Check if the key event target is a remote browser
+ if (RemoteBrowser* remoteBrowser = RemoteBrowser::GetFrom(keyEventTarget)) {
+ LayersId layersId = remoteBrowser->GetLayersId();
+
+ // The globally focused element for scrolling is in a remote layer tree
+ if (layersId.IsValid()) {
+ FT_LOG("Creating reflayer target with seq=%" PRIu64 ", kl=%d, lt=%" PRIu64
+ "\n",
+ aFocusSequenceNumber, mFocusHasKeyEventListeners, layersId.mId);
+
+ mData = AsVariant<LayersId>(std::move(layersId));
+ return;
+ }
+
+ FT_LOG("Creating nil target with seq=%" PRIu64
+ ", kl=%d (remote browser missing layers id)\n",
+ aFocusSequenceNumber, mFocusHasKeyEventListeners);
+
+ return;
+ }
+
+ // The content to scroll is either the focused element or the focus node of
+ // the selection. It's difficult to determine if an element is an interactive
+ // element requiring async keyboard scrolling to be disabled. So we only
+ // allow async key scrolling based on the selection, which doesn't have
+ // this problem and is more common.
+ if (focusedContent) {
+ FT_LOG("Creating nil target with seq=%" PRIu64
+ ", kl=%d (disabling for focusing an element)\n",
+ aFocusSequenceNumber, mFocusHasKeyEventListeners);
+
+ return;
+ }
+
+ nsCOMPtr<nsIContent> selectedContent =
+ presShell->GetSelectedContentForScrolling();
+
+ // Gather the scrollable frames that would be scrolled in each direction
+ // for this scroll target
+ nsIScrollableFrame* horizontal =
+ presShell->GetScrollableFrameToScrollForContent(
+ selectedContent.get(), HorizontalScrollDirection);
+ nsIScrollableFrame* vertical =
+ presShell->GetScrollableFrameToScrollForContent(selectedContent.get(),
+ VerticalScrollDirection);
+
+ // We might have the globally focused element for scrolling. Gather a ViewID
+ // for the horizontal and vertical scroll targets of this element.
+ ScrollTargets target;
+ target.mHorizontal = nsLayoutUtils::FindIDForScrollableFrame(horizontal);
+ target.mVertical = nsLayoutUtils::FindIDForScrollableFrame(vertical);
+ mData = AsVariant(target);
+
+ FT_LOG("Creating scroll target with seq=%" PRIu64 ", kl=%d, h=%" PRIu64
+ ", v=%" PRIu64 "\n",
+ aFocusSequenceNumber, mFocusHasKeyEventListeners, target.mHorizontal,
+ target.mVertical);
+}
+
+bool FocusTarget::operator==(const FocusTarget& aRhs) const {
+ return mSequenceNumber == aRhs.mSequenceNumber &&
+ mFocusHasKeyEventListeners == aRhs.mFocusHasKeyEventListeners &&
+ mData == aRhs.mData;
+}
+
+const char* FocusTarget::Type() const {
+ if (mData.is<LayersId>()) {
+ return "LayersId";
+ }
+ if (mData.is<ScrollTargets>()) {
+ return "ScrollTargets";
+ }
+ if (mData.is<NoFocusTarget>()) {
+ return "NoFocusTarget";
+ }
+ return "<unknown>";
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/FocusTarget.h b/gfx/layers/apz/src/FocusTarget.h
new file mode 100644
index 0000000000..f4caa5d070
--- /dev/null
+++ b/gfx/layers/apz/src/FocusTarget.h
@@ -0,0 +1,71 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_FocusTarget_h
+#define mozilla_layers_FocusTarget_h
+
+#include <stdint.h> // for int32_t, uint32_t
+
+#include "mozilla/DefineEnum.h" // for MOZ_DEFINE_ENUM
+#include "mozilla/layers/ScrollableLayerGuid.h" // for ViewID
+#include "mozilla/Variant.h" // for Variant
+#include "mozilla/Maybe.h" // for Maybe
+
+namespace mozilla {
+
+class PresShell;
+
+namespace layers {
+
+/**
+ * This class is used for communicating information about the currently focused
+ * element of a document and the scrollable frames to use when keyboard
+ * scrolling it. It is created on the main thread at paint-time, but is then
+ * passed over IPC to the compositor/APZ code.
+ */
+class FocusTarget final {
+ public:
+ struct ScrollTargets {
+ ScrollableLayerGuid::ViewID mHorizontal;
+ ScrollableLayerGuid::ViewID mVertical;
+
+ bool operator==(const ScrollTargets& aRhs) const {
+ return (mHorizontal == aRhs.mHorizontal && mVertical == aRhs.mVertical);
+ }
+ };
+
+ // We need this to represent the case where mData has no focus target data
+ // because we can't have an empty variant
+ struct NoFocusTarget {
+ bool operator==(const NoFocusTarget& aRhs) const { return true; }
+ };
+
+ FocusTarget();
+
+ /**
+ * Construct a focus target for the specified top level PresShell
+ */
+ FocusTarget(PresShell* aRootPresShell, uint64_t aFocusSequenceNumber);
+
+ bool operator==(const FocusTarget& aRhs) const;
+
+ const char* Type() const;
+
+ public:
+ // The content sequence number recorded at the time of this class's creation
+ uint64_t mSequenceNumber;
+
+ // Whether there are keydown, keypress, or keyup event listeners
+ // in the event target chain of the focused element
+ bool mFocusHasKeyEventListeners;
+
+ mozilla::Variant<LayersId, ScrollTargets, NoFocusTarget> mData;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_FocusTarget_h
diff --git a/gfx/layers/apz/src/GenericFlingAnimation.h b/gfx/layers/apz/src/GenericFlingAnimation.h
new file mode 100644
index 0000000000..82f981ae0a
--- /dev/null
+++ b/gfx/layers/apz/src/GenericFlingAnimation.h
@@ -0,0 +1,207 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_GenericFlingAnimation_h_
+#define mozilla_layers_GenericFlingAnimation_h_
+
+#include "APZUtils.h"
+#include "AsyncPanZoomAnimation.h"
+#include "AsyncPanZoomController.h"
+#include "FrameMetrics.h"
+#include "Units.h"
+#include "OverscrollHandoffState.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/Monitor.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/StaticPrefs_apz.h"
+#include "mozilla/TimeStamp.h"
+#include "mozilla/ToString.h"
+#include "nsThreadUtils.h"
+
+static mozilla::LazyLogModule sApzFlgLog("apz.fling");
+#define FLING_LOG(...) MOZ_LOG(sApzFlgLog, LogLevel::Debug, (__VA_ARGS__))
+
+namespace mozilla {
+namespace layers {
+
+/**
+ * The FlingPhysics template parameter determines the physics model
+ * that the fling animation follows. It must have the following methods:
+ *
+ * - Default constructor.
+ *
+ * - Init(const ParentLayerPoint& aStartingVelocity, float aPLPPI).
+ * Called at the beginning of the fling, with the fling's starting velocity,
+ * and the number of ParentLayer pixels per (Screen) inch at the point of
+ * the fling's start in the fling's direction.
+ *
+ * - Sample(const TimeDuration& aDelta,
+ * ParentLayerPoint* aOutVelocity,
+ * ParentLayerPoint* aOutOffset);
+ * Called on each sample of the fling.
+ * |aDelta| is the time elapsed since the last sample.
+ * |aOutVelocity| should be the desired velocity after the current sample,
+ * in ParentLayer pixels per millisecond.
+ * |aOutOffset| should be the desired _delta_ to the scroll offset after
+ * the current sample. |aOutOffset| should _not_ be clamped to the APZC's
+ * scrollable bounds; the caller will do the clamping, and it needs to
+ * know the unclamped value to handle handoff/overscroll correctly.
+ */
+template <typename FlingPhysics>
+class GenericFlingAnimation : public AsyncPanZoomAnimation,
+ public FlingPhysics {
+ public:
+ GenericFlingAnimation(AsyncPanZoomController& aApzc,
+ const FlingHandoffState& aHandoffState, float aPLPPI)
+ : mApzc(aApzc),
+ mOverscrollHandoffChain(aHandoffState.mChain),
+ mScrolledApzc(aHandoffState.mScrolledApzc) {
+ MOZ_ASSERT(mOverscrollHandoffChain);
+
+ // Drop any velocity on axes where we don't have room to scroll anyways
+ // (in this APZC, or an APZC further in the handoff chain).
+ // This ensures that we don't take the 'overscroll' path in Sample()
+ // on account of one axis which can't scroll having a velocity.
+ if (!mOverscrollHandoffChain->CanScrollInDirection(
+ &mApzc, ScrollDirection::eHorizontal)) {
+ RecursiveMutexAutoLock lock(mApzc.mRecursiveMutex);
+ mApzc.mX.SetVelocity(0);
+ }
+ if (!mOverscrollHandoffChain->CanScrollInDirection(
+ &mApzc, ScrollDirection::eVertical)) {
+ RecursiveMutexAutoLock lock(mApzc.mRecursiveMutex);
+ mApzc.mY.SetVelocity(0);
+ }
+
+ if (aHandoffState.mIsHandoff) {
+ // Only apply acceleration in the APZC that originated the fling, not in
+ // APZCs further down the handoff chain during handoff.
+ mApzc.mFlingAccelerator.Reset();
+ }
+
+ ParentLayerPoint velocity =
+ mApzc.mFlingAccelerator.GetFlingStartingVelocity(
+ aApzc.GetFrameTime(), mApzc.GetVelocityVector(), aHandoffState);
+
+ mApzc.SetVelocityVector(velocity);
+
+ FlingPhysics::Init(mApzc.GetVelocityVector(), aPLPPI);
+ }
+
+ /**
+ * Advances a fling by an interpolated amount based on the passed in |aDelta|.
+ * This should be called whenever sampling the content transform for this
+ * frame. Returns true if the fling animation should be advanced by one frame,
+ * or false if there is no fling or the fling has ended.
+ */
+ virtual bool DoSample(FrameMetrics& aFrameMetrics,
+ const TimeDuration& aDelta) override {
+ CSSToParentLayerScale zoom(aFrameMetrics.GetZoom());
+ if (zoom == CSSToParentLayerScale(0)) {
+ return false;
+ }
+
+ ParentLayerPoint velocity;
+ ParentLayerPoint offset;
+ FlingPhysics::Sample(aDelta, &velocity, &offset);
+
+ mApzc.SetVelocityVector(velocity);
+
+ // If we shouldn't continue the fling, let's just stop and repaint.
+ if (IsZero(velocity / zoom)) {
+ FLING_LOG("%p ending fling animation. overscrolled=%d\n", &mApzc,
+ mApzc.IsOverscrolled());
+ // This APZC or an APZC further down the handoff chain may be be
+ // overscrolled. Start a snap-back animation on the overscrolled APZC.
+ // Note:
+ // This needs to be a deferred task even though it can safely run
+ // while holding mRecursiveMutex, because otherwise, if the overscrolled
+ // APZC is this one, then the SetState(NOTHING) in UpdateAnimation will
+ // stomp on the SetState(SNAP_BACK) it does.
+ mDeferredTasks.AppendElement(NewRunnableMethod<AsyncPanZoomController*>(
+ "layers::OverscrollHandoffChain::SnapBackOverscrolledApzc",
+ mOverscrollHandoffChain.get(),
+ &OverscrollHandoffChain::SnapBackOverscrolledApzc, &mApzc));
+ return false;
+ }
+
+ // Ordinarily we might need to do a ScheduleComposite if either of
+ // the following AdjustDisplacement calls returns true, but this
+ // is already running as part of a FlingAnimation, so we'll be compositing
+ // per frame of animation anyway.
+ ParentLayerPoint overscroll;
+ ParentLayerPoint adjustedOffset;
+ mApzc.mX.AdjustDisplacement(offset.x, adjustedOffset.x, overscroll.x);
+ mApzc.mY.AdjustDisplacement(offset.y, adjustedOffset.y, overscroll.y);
+ if (aFrameMetrics.GetZoom() != CSSToParentLayerScale(0)) {
+ mApzc.ScrollBy(adjustedOffset / aFrameMetrics.GetZoom());
+ }
+
+ // The fling may have caused us to reach the end of our scroll range.
+ if (!IsZero(overscroll / zoom)) {
+ // Hand off the fling to the next APZC in the overscroll handoff chain.
+
+ // We may have reached the end of the scroll range along one axis but
+ // not the other. In such a case we only want to hand off the relevant
+ // component of the fling.
+ if (mApzc.IsZero(overscroll.x)) {
+ velocity.x = 0;
+ } else if (mApzc.IsZero(overscroll.y)) {
+ velocity.y = 0;
+ }
+
+ // To hand off the fling, we attempt to find a target APZC and start a new
+ // fling with the same velocity on that APZC. For simplicity, the actual
+ // overscroll of the current sample is discarded rather than being handed
+ // off. The compositor should sample animations sufficiently frequently
+ // that this is not noticeable. The target APZC is chosen by seeing if
+ // there is an APZC further in the handoff chain which is pannable; if
+ // there isn't, we take the new fling ourselves, entering an overscrolled
+ // state.
+ // Note: APZC is holding mRecursiveMutex, so directly calling
+ // HandleFlingOverscroll() (which acquires the tree lock) would violate
+ // the lock ordering. Instead we schedule HandleFlingOverscroll() to be
+ // called after mRecursiveMutex is released.
+ FLING_LOG("%p fling went into overscroll, handing off with velocity %s\n",
+ &mApzc, ToString(velocity).c_str());
+ mDeferredTasks.AppendElement(
+ NewRunnableMethod<ParentLayerPoint, SideBits,
+ RefPtr<const OverscrollHandoffChain>,
+ RefPtr<const AsyncPanZoomController>>(
+ "layers::AsyncPanZoomController::HandleFlingOverscroll", &mApzc,
+ &AsyncPanZoomController::HandleFlingOverscroll, velocity,
+ apz::GetOverscrollSideBits(overscroll), mOverscrollHandoffChain,
+ mScrolledApzc));
+
+ // If there is a remaining velocity on this APZC, continue this fling
+ // as well. (This fling and the handed-off fling will run concurrently.)
+ // Note that AdjustDisplacement() will have zeroed out the velocity
+ // along the axes where we're overscrolled.
+ return !IsZero(mApzc.GetVelocityVector() / zoom);
+ }
+
+ return true;
+ }
+
+ void Cancel(CancelAnimationFlags aFlags) override {
+ mApzc.mFlingAccelerator.ObserveFlingCanceled(mApzc.GetVelocityVector());
+ }
+
+ virtual bool HandleScrollOffsetUpdate(
+ const Maybe<CSSPoint>& aRelativeDelta) override {
+ return true;
+ }
+
+ private:
+ AsyncPanZoomController& mApzc;
+ RefPtr<const OverscrollHandoffChain> mOverscrollHandoffChain;
+ RefPtr<const AsyncPanZoomController> mScrolledApzc;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_GenericFlingAnimation_h_
diff --git a/gfx/layers/apz/src/GenericScrollAnimation.cpp b/gfx/layers/apz/src/GenericScrollAnimation.cpp
new file mode 100644
index 0000000000..9320482295
--- /dev/null
+++ b/gfx/layers/apz/src/GenericScrollAnimation.cpp
@@ -0,0 +1,120 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "GenericScrollAnimation.h"
+
+#include "AsyncPanZoomController.h"
+#include "FrameMetrics.h"
+#include "nsPoint.h"
+#include "ScrollAnimationPhysics.h"
+#include "ScrollAnimationBezierPhysics.h"
+#include "ScrollAnimationMSDPhysics.h"
+#include "mozilla/StaticPrefs_general.h"
+
+namespace mozilla {
+namespace layers {
+
+GenericScrollAnimation::GenericScrollAnimation(
+ AsyncPanZoomController& aApzc, const nsPoint& aInitialPosition,
+ const ScrollAnimationBezierPhysicsSettings& aSettings)
+ : mApzc(aApzc), mFinalDestination(aInitialPosition) {
+ // ScrollAnimationBezierPhysics (despite it's name) handles the case of
+ // general.smoothScroll being disabled whereas ScrollAnimationMSDPhysics does
+ // not (ie it scrolls smoothly).
+ if (StaticPrefs::general_smoothScroll() &&
+ StaticPrefs::general_smoothScroll_msdPhysics_enabled()) {
+ mAnimationPhysics = MakeUnique<ScrollAnimationMSDPhysics>(aInitialPosition);
+ } else {
+ mAnimationPhysics =
+ MakeUnique<ScrollAnimationBezierPhysics>(aInitialPosition, aSettings);
+ }
+}
+
+void GenericScrollAnimation::UpdateDelta(TimeStamp aTime, const nsPoint& aDelta,
+ const nsSize& aCurrentVelocity) {
+ mFinalDestination += aDelta;
+
+ Update(aTime, aCurrentVelocity);
+}
+
+void GenericScrollAnimation::UpdateDestination(TimeStamp aTime,
+ const nsPoint& aDestination,
+ const nsSize& aCurrentVelocity) {
+ mFinalDestination = aDestination;
+
+ Update(aTime, aCurrentVelocity);
+}
+
+void GenericScrollAnimation::Update(TimeStamp aTime,
+ const nsSize& aCurrentVelocity) {
+ // Clamp the final destination to the scrollable area.
+ CSSPoint clamped = CSSPoint::FromAppUnits(mFinalDestination);
+ clamped.x = mApzc.mX.ClampOriginToScrollableRect(clamped.x);
+ clamped.y = mApzc.mY.ClampOriginToScrollableRect(clamped.y);
+ mFinalDestination = CSSPoint::ToAppUnits(clamped);
+
+ mAnimationPhysics->Update(aTime, mFinalDestination, aCurrentVelocity);
+}
+
+bool GenericScrollAnimation::DoSample(FrameMetrics& aFrameMetrics,
+ const TimeDuration& aDelta) {
+ TimeStamp now = mApzc.GetFrameTime().Time();
+ CSSToParentLayerScale zoom(aFrameMetrics.GetZoom());
+ if (zoom == CSSToParentLayerScale(0)) {
+ return false;
+ }
+
+ // If the animation is finished, make sure the final position is correct by
+ // using one last displacement. Otherwise, compute the delta via the timing
+ // function as normal.
+ bool finished = mAnimationPhysics->IsFinished(now);
+ nsPoint sampledDest = mAnimationPhysics->PositionAt(now);
+ ParentLayerPoint displacement = (CSSPoint::FromAppUnits(sampledDest) -
+ aFrameMetrics.GetVisualScrollOffset()) *
+ zoom;
+
+ if (finished) {
+ mApzc.mX.SetVelocity(0);
+ mApzc.mY.SetVelocity(0);
+ } else if (!IsZero(displacement / zoom)) {
+ // Convert velocity from AppUnits/Seconds to ParentLayerCoords/Milliseconds
+ nsSize velocity = mAnimationPhysics->VelocityAt(now);
+ ParentLayerPoint velocityPL =
+ CSSPoint::FromAppUnits(nsPoint(velocity.width, velocity.height)) * zoom;
+ mApzc.mX.SetVelocity(velocityPL.x / 1000.0);
+ mApzc.mY.SetVelocity(velocityPL.y / 1000.0);
+ }
+ // Note: we ignore overscroll for generic animations.
+ ParentLayerPoint adjustedOffset, overscroll;
+ mApzc.mX.AdjustDisplacement(
+ displacement.x, adjustedOffset.x, overscroll.x,
+ mDirectionForcedToOverscroll == Some(ScrollDirection::eHorizontal));
+ mApzc.mY.AdjustDisplacement(
+ displacement.y, adjustedOffset.y, overscroll.y,
+ mDirectionForcedToOverscroll == Some(ScrollDirection::eVertical));
+ // If we expected to scroll, but there's no more scroll range on either axis,
+ // then end the animation early. Note that the initial displacement could be 0
+ // if the compositor ran very quickly (<1ms) after the animation was created.
+ // When that happens we want to make sure the animation continues.
+ if (!IsZero(displacement / zoom) && IsZero(adjustedOffset / zoom)) {
+ // Nothing more to do - end the animation.
+ return false;
+ }
+ mApzc.ScrollBy(adjustedOffset / zoom);
+ return !finished;
+}
+
+bool GenericScrollAnimation::HandleScrollOffsetUpdate(
+ const Maybe<CSSPoint>& aRelativeDelta) {
+ if (aRelativeDelta) {
+ mAnimationPhysics->ApplyContentShift(*aRelativeDelta);
+ return true;
+ }
+ return false;
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/GenericScrollAnimation.h b/gfx/layers/apz/src/GenericScrollAnimation.h
new file mode 100644
index 0000000000..56a64dc5ec
--- /dev/null
+++ b/gfx/layers/apz/src/GenericScrollAnimation.h
@@ -0,0 +1,59 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_GenericScrollAnimation_h_
+#define mozilla_layers_GenericScrollAnimation_h_
+
+#include "AsyncPanZoomAnimation.h"
+
+namespace mozilla {
+
+struct ScrollAnimationBezierPhysicsSettings;
+class ScrollAnimationPhysics;
+
+namespace layers {
+
+class AsyncPanZoomController;
+
+class GenericScrollAnimation : public AsyncPanZoomAnimation {
+ public:
+ GenericScrollAnimation(AsyncPanZoomController& aApzc,
+ const nsPoint& aInitialPosition,
+ const ScrollAnimationBezierPhysicsSettings& aSettings);
+
+ bool DoSample(FrameMetrics& aFrameMetrics,
+ const TimeDuration& aDelta) override;
+
+ bool HandleScrollOffsetUpdate(const Maybe<CSSPoint>& aRelativeDelta) override;
+
+ void UpdateDelta(TimeStamp aTime, const nsPoint& aDelta,
+ const nsSize& aCurrentVelocity);
+ void UpdateDestination(TimeStamp aTime, const nsPoint& aDestination,
+ const nsSize& aCurrentVelocity);
+
+ CSSPoint GetDestination() const {
+ return CSSPoint::FromAppUnits(mFinalDestination);
+ }
+
+ private:
+ void Update(TimeStamp aTime, const nsSize& aCurrentVelocity);
+
+ protected:
+ AsyncPanZoomController& mApzc;
+ UniquePtr<ScrollAnimationPhysics> mAnimationPhysics;
+ nsPoint mFinalDestination;
+ // If a direction is forced to overscroll, it means it's axis in that
+ // direction is locked, and scroll in that direction is treated as overscroll
+ // of an equal amount, which, for example, may then bubble up a scroll action
+ // to its parent, or may behave as whatever an overscroll occurence requires
+ // to behave
+ Maybe<ScrollDirection> mDirectionForcedToOverscroll;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_GenericScrollAnimation_h_
diff --git a/gfx/layers/apz/src/GestureEventListener.cpp b/gfx/layers/apz/src/GestureEventListener.cpp
new file mode 100644
index 0000000000..b54674b593
--- /dev/null
+++ b/gfx/layers/apz/src/GestureEventListener.cpp
@@ -0,0 +1,663 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "GestureEventListener.h"
+#include <algorithm> // for max
+#include <math.h> // for fabsf
+#include <stddef.h> // for size_t
+#include "AsyncPanZoomController.h" // for AsyncPanZoomController
+#include "InputBlockState.h" // for TouchBlockState
+#include "base/task.h" // for CancelableTask, etc
+#include "InputBlockState.h" // for TouchBlockState
+#include "mozilla/StaticPrefs_apz.h"
+#include "mozilla/StaticPrefs_ui.h"
+#include "nsDebug.h" // for NS_WARNING
+#include "nsMathUtils.h" // for NS_hypot
+
+static mozilla::LazyLogModule sApzGelLog("apz.gesture");
+#define GEL_LOG(...) MOZ_LOG(sApzGelLog, LogLevel::Debug, (__VA_ARGS__))
+
+namespace mozilla {
+namespace layers {
+
+/**
+ * Amount of span or focus change needed to take us from the
+ * GESTURE_WAITING_PINCH state to the GESTURE_PINCH state. This is measured as
+ * either a change in distance between the fingers used to compute the span
+ * ratio, or the a change in position of the focus point between the two
+ * fingers.
+ */
+static const float PINCH_START_THRESHOLD = 35.0f;
+
+/**
+ * Determines how fast a one touch pinch zooms in and out. The greater the
+ * value, the faster it zooms.
+ */
+static const float ONE_TOUCH_PINCH_SPEED = 0.005f;
+
+static bool sLongTapEnabled = true;
+
+static ScreenPoint GetCurrentFocus(const MultiTouchInput& aEvent) {
+ const ScreenPoint& firstTouch = aEvent.mTouches[0].mScreenPoint;
+ const ScreenPoint& secondTouch = aEvent.mTouches[1].mScreenPoint;
+ return (firstTouch + secondTouch) / 2;
+}
+
+static ScreenCoord GetCurrentSpan(const MultiTouchInput& aEvent) {
+ const ScreenPoint& firstTouch = aEvent.mTouches[0].mScreenPoint;
+ const ScreenPoint& secondTouch = aEvent.mTouches[1].mScreenPoint;
+ ScreenPoint delta = secondTouch - firstTouch;
+ return delta.Length();
+}
+
+ScreenCoord GestureEventListener::GetYSpanFromGestureStartPoint() {
+ // use the position that began the one-touch-pinch gesture rather
+ // mTouchStartPosition
+ const ScreenPoint start = mOneTouchPinchStartPosition;
+ const ScreenPoint& current = mTouches[0].mScreenPoint;
+ return current.y - start.y;
+}
+
+static TapGestureInput CreateTapEvent(const MultiTouchInput& aTouch,
+ TapGestureInput::TapGestureType aType) {
+ return TapGestureInput(aType, aTouch.mTimeStamp,
+ aTouch.mTouches[0].mScreenPoint, aTouch.modifiers);
+}
+
+GestureEventListener::GestureEventListener(
+ AsyncPanZoomController* aAsyncPanZoomController)
+ : mAsyncPanZoomController(aAsyncPanZoomController),
+ mState(GESTURE_NONE),
+ mSpanChange(0.0f),
+ mPreviousSpan(0.0f),
+ mFocusChange(0.0f),
+ mLastTouchInput(MultiTouchInput::MULTITOUCH_START, 0, TimeStamp(), 0),
+ mLastTapInput(MultiTouchInput::MULTITOUCH_START, 0, TimeStamp(), 0),
+ mLongTapTimeoutTask(nullptr),
+ mMaxTapTimeoutTask(nullptr) {}
+
+GestureEventListener::~GestureEventListener() = default;
+
+nsEventStatus GestureEventListener::HandleInputEvent(
+ const MultiTouchInput& aEvent) {
+ GEL_LOG("Receiving event type %d with %zu touches in state %d\n",
+ aEvent.mType, aEvent.mTouches.Length(), mState);
+
+ nsEventStatus rv = nsEventStatus_eIgnore;
+
+ // Cache the current event since it may become the single or long tap that we
+ // send.
+ mLastTouchInput = aEvent;
+
+ switch (aEvent.mType) {
+ case MultiTouchInput::MULTITOUCH_START:
+ mTouches.Clear();
+ // Cache every touch.
+ for (size_t i = 0; i < aEvent.mTouches.Length(); i++) {
+ mTouches.AppendElement(aEvent.mTouches[i]);
+ }
+
+ if (aEvent.mTouches.Length() == 1) {
+ rv = HandleInputTouchSingleStart();
+ } else {
+ rv = HandleInputTouchMultiStart();
+ }
+ break;
+ case MultiTouchInput::MULTITOUCH_MOVE:
+ // Update the screen points of the cached touches.
+ for (size_t i = 0; i < aEvent.mTouches.Length(); i++) {
+ for (size_t j = 0; j < mTouches.Length(); j++) {
+ if (aEvent.mTouches[i].mIdentifier == mTouches[j].mIdentifier) {
+ mTouches[j].mScreenPoint = aEvent.mTouches[i].mScreenPoint;
+ mTouches[j].mLocalScreenPoint =
+ aEvent.mTouches[i].mLocalScreenPoint;
+ }
+ }
+ }
+ rv = HandleInputTouchMove();
+ break;
+ case MultiTouchInput::MULTITOUCH_END:
+ // Remove the cache of the touch that ended.
+ for (size_t i = 0; i < aEvent.mTouches.Length(); i++) {
+ for (size_t j = 0; j < mTouches.Length(); j++) {
+ if (aEvent.mTouches[i].mIdentifier == mTouches[j].mIdentifier) {
+ mTouches.RemoveElementAt(j);
+ break;
+ }
+ }
+ }
+
+ rv = HandleInputTouchEnd();
+ break;
+ case MultiTouchInput::MULTITOUCH_CANCEL:
+ mTouches.Clear();
+ rv = HandleInputTouchCancel();
+ break;
+ }
+
+ return rv;
+}
+
+int32_t GestureEventListener::GetLastTouchIdentifier() const {
+ if (mTouches.Length() != 1) {
+ NS_WARNING(
+ "GetLastTouchIdentifier() called when last touch event "
+ "did not have one touch");
+ }
+ return mTouches.IsEmpty() ? -1 : mTouches[0].mIdentifier;
+}
+
+/* static */
+void GestureEventListener::SetLongTapEnabled(bool aLongTapEnabled) {
+ sLongTapEnabled = aLongTapEnabled;
+}
+
+/* static */
+bool GestureEventListener::IsLongTapEnabled() { return sLongTapEnabled; }
+
+void GestureEventListener::EnterFirstSingleTouchDown() {
+ SetState(GESTURE_FIRST_SINGLE_TOUCH_DOWN);
+ mTouchStartPosition = mLastTouchInput.mTouches[0].mScreenPoint;
+ mTouchStartOffset = mLastTouchInput.mScreenOffset;
+
+ if (sLongTapEnabled) {
+ CreateLongTapTimeoutTask();
+ }
+ CreateMaxTapTimeoutTask();
+}
+
+nsEventStatus GestureEventListener::HandleInputTouchSingleStart() {
+ switch (mState) {
+ case GESTURE_NONE:
+ EnterFirstSingleTouchDown();
+ break;
+ case GESTURE_FIRST_SINGLE_TOUCH_UP:
+ if (SecondTapIsFar()) {
+ // If the second tap goes down far away from the first, then bail out
+ // of any gesture that includes the first tap.
+ CancelLongTapTimeoutTask();
+ CancelMaxTapTimeoutTask();
+ mSingleTapSent = Nothing();
+
+ // But still allow the second tap to participate in a gesture
+ // (e.g. lead to a single tap, or a double tap if an additional
+ // tap occurs near the same location).
+ EnterFirstSingleTouchDown();
+ } else {
+ // Otherwise, reset the touch start position so that, if this turns into
+ // a one-touch-pinch gesture, it uses the second tap's down position as
+ // the focus, rather than the first tap's.
+ mTouchStartPosition = mLastTouchInput.mTouches[0].mScreenPoint;
+ mTouchStartOffset = mLastTouchInput.mScreenOffset;
+ SetState(GESTURE_SECOND_SINGLE_TOUCH_DOWN);
+ }
+ break;
+ default:
+ NS_WARNING("Unhandled state upon single touch start");
+ SetState(GESTURE_NONE);
+ break;
+ }
+
+ return nsEventStatus_eIgnore;
+}
+
+nsEventStatus GestureEventListener::HandleInputTouchMultiStart() {
+ nsEventStatus rv = nsEventStatus_eIgnore;
+
+ switch (mState) {
+ case GESTURE_NONE:
+ SetState(GESTURE_MULTI_TOUCH_DOWN);
+ break;
+ case GESTURE_FIRST_SINGLE_TOUCH_DOWN:
+ CancelLongTapTimeoutTask();
+ CancelMaxTapTimeoutTask();
+ SetState(GESTURE_MULTI_TOUCH_DOWN);
+ // Prevent APZC::OnTouchStart() from handling MULTITOUCH_START event
+ rv = nsEventStatus_eConsumeNoDefault;
+ break;
+ case GESTURE_FIRST_SINGLE_TOUCH_MAX_TAP_DOWN:
+ CancelLongTapTimeoutTask();
+ SetState(GESTURE_MULTI_TOUCH_DOWN);
+ // Prevent APZC::OnTouchStart() from handling MULTITOUCH_START event
+ rv = nsEventStatus_eConsumeNoDefault;
+ break;
+ case GESTURE_FIRST_SINGLE_TOUCH_UP:
+ case GESTURE_SECOND_SINGLE_TOUCH_DOWN:
+ // Cancel wait for double tap
+ CancelMaxTapTimeoutTask();
+ MOZ_ASSERT(mSingleTapSent.isSome());
+ if (!mSingleTapSent.value()) {
+ TriggerSingleTapConfirmedEvent();
+ }
+ mSingleTapSent = Nothing();
+ SetState(GESTURE_MULTI_TOUCH_DOWN);
+ // Prevent APZC::OnTouchStart() from handling MULTITOUCH_START event
+ rv = nsEventStatus_eConsumeNoDefault;
+ break;
+ case GESTURE_LONG_TOUCH_DOWN:
+ SetState(GESTURE_MULTI_TOUCH_DOWN);
+ break;
+ case GESTURE_MULTI_TOUCH_DOWN:
+ case GESTURE_PINCH:
+ // Prevent APZC::OnTouchStart() from handling MULTITOUCH_START event
+ rv = nsEventStatus_eConsumeNoDefault;
+ break;
+ default:
+ NS_WARNING("Unhandled state upon multitouch start");
+ SetState(GESTURE_NONE);
+ break;
+ }
+
+ return rv;
+}
+
+bool GestureEventListener::MoveDistanceExceeds(ScreenCoord aThreshold) const {
+ ExternalPoint start = AsyncPanZoomController::ToExternalPoint(
+ mTouchStartOffset, mTouchStartPosition);
+ ExternalPoint end = AsyncPanZoomController::ToExternalPoint(
+ mLastTouchInput.mScreenOffset, mLastTouchInput.mTouches[0].mScreenPoint);
+ return (start - end).Length() > aThreshold;
+}
+
+bool GestureEventListener::MoveDistanceIsLarge() const {
+ return MoveDistanceExceeds(mAsyncPanZoomController->GetTouchStartTolerance());
+}
+
+bool GestureEventListener::SecondTapIsFar() const {
+ // Allow a little more room here, because the is actually lifting their finger
+ // off the screen before replacing it, and that tends to have more error than
+ // wiggling the finger while on the screen.
+ return MoveDistanceExceeds(mAsyncPanZoomController->GetSecondTapTolerance());
+}
+
+nsEventStatus GestureEventListener::HandleInputTouchMove() {
+ nsEventStatus rv = nsEventStatus_eIgnore;
+
+ switch (mState) {
+ case GESTURE_NONE:
+ // Ignore this input signal as the corresponding events get handled by
+ // APZC
+ break;
+
+ case GESTURE_LONG_TOUCH_DOWN:
+ if (MoveDistanceIsLarge()) {
+ // So that we don't fire a long-tap-up if the user moves around after a
+ // long-tap
+ SetState(GESTURE_NONE);
+ }
+ break;
+
+ case GESTURE_FIRST_SINGLE_TOUCH_DOWN:
+ case GESTURE_FIRST_SINGLE_TOUCH_MAX_TAP_DOWN: {
+ // If we move too much, bail out of the tap.
+ if (MoveDistanceIsLarge()) {
+ CancelLongTapTimeoutTask();
+ CancelMaxTapTimeoutTask();
+ mSingleTapSent = Nothing();
+ SetState(GESTURE_NONE);
+ }
+ break;
+ }
+
+ // The user has performed a double tap, but not lifted her finger.
+ case GESTURE_SECOND_SINGLE_TOUCH_DOWN: {
+ // If touch has moved noticeably (within StaticPrefs::apz_max_tap_time()),
+ // change state.
+ if (MoveDistanceIsLarge()) {
+ CancelLongTapTimeoutTask();
+ CancelMaxTapTimeoutTask();
+ mSingleTapSent = Nothing();
+ if (!StaticPrefs::apz_one_touch_pinch_enabled()) {
+ // If the one-touch-pinch feature is disabled, bail out of the double-
+ // tap gesture instead.
+ SetState(GESTURE_NONE);
+ break;
+ }
+
+ SetState(GESTURE_ONE_TOUCH_PINCH);
+
+ ScreenCoord currentSpan = 1.0f;
+ ScreenPoint currentFocus = mTouchStartPosition;
+
+ // save the position that the one-touch-pinch gesture actually begins
+ mOneTouchPinchStartPosition = mLastTouchInput.mTouches[0].mScreenPoint;
+
+ PinchGestureInput pinchEvent(
+ PinchGestureInput::PINCHGESTURE_START, PinchGestureInput::ONE_TOUCH,
+ mLastTouchInput.mTimeStamp, mLastTouchInput.mScreenOffset,
+ currentFocus, currentSpan, currentSpan, mLastTouchInput.modifiers);
+
+ rv = mAsyncPanZoomController->HandleGestureEvent(pinchEvent);
+
+ mPreviousSpan = currentSpan;
+ mPreviousFocus = currentFocus;
+ }
+ break;
+ }
+
+ case GESTURE_MULTI_TOUCH_DOWN: {
+ if (mLastTouchInput.mTouches.Length() < 2) {
+ NS_WARNING(
+ "Wrong input: less than 2 moving points in "
+ "GESTURE_MULTI_TOUCH_DOWN state");
+ break;
+ }
+
+ ScreenCoord currentSpan = GetCurrentSpan(mLastTouchInput);
+ ScreenPoint currentFocus = GetCurrentFocus(mLastTouchInput);
+
+ mSpanChange += fabsf(currentSpan - mPreviousSpan);
+ mFocusChange += (currentFocus - mPreviousFocus).Length();
+ if (mSpanChange > PINCH_START_THRESHOLD ||
+ mFocusChange > PINCH_START_THRESHOLD) {
+ SetState(GESTURE_PINCH);
+ PinchGestureInput pinchEvent(
+ PinchGestureInput::PINCHGESTURE_START, PinchGestureInput::TOUCH,
+ mLastTouchInput.mTimeStamp, mLastTouchInput.mScreenOffset,
+ currentFocus, currentSpan, currentSpan, mLastTouchInput.modifiers);
+
+ rv = mAsyncPanZoomController->HandleGestureEvent(pinchEvent);
+ } else {
+ // Prevent APZC::OnTouchMove from processing a move event when two
+ // touches are active
+ rv = nsEventStatus_eConsumeNoDefault;
+ }
+
+ mPreviousSpan = currentSpan;
+ mPreviousFocus = currentFocus;
+ break;
+ }
+
+ case GESTURE_PINCH: {
+ if (mLastTouchInput.mTouches.Length() < 2) {
+ NS_WARNING(
+ "Wrong input: less than 2 moving points in GESTURE_PINCH state");
+ // Prevent APZC::OnTouchMove() from handling this wrong input
+ rv = nsEventStatus_eConsumeNoDefault;
+ break;
+ }
+
+ ScreenCoord currentSpan = GetCurrentSpan(mLastTouchInput);
+
+ PinchGestureInput pinchEvent(
+ PinchGestureInput::PINCHGESTURE_SCALE, PinchGestureInput::TOUCH,
+ mLastTouchInput.mTimeStamp, mLastTouchInput.mScreenOffset,
+ GetCurrentFocus(mLastTouchInput), currentSpan, mPreviousSpan,
+ mLastTouchInput.modifiers);
+
+ rv = mAsyncPanZoomController->HandleGestureEvent(pinchEvent);
+ mPreviousSpan = currentSpan;
+
+ break;
+ }
+
+ case GESTURE_ONE_TOUCH_PINCH: {
+ ScreenCoord currentSpan = GetYSpanFromGestureStartPoint();
+ float effectiveSpan =
+ 1.0f + (fabsf(currentSpan.value) * ONE_TOUCH_PINCH_SPEED);
+ ScreenPoint currentFocus = mTouchStartPosition;
+
+ // Invert zoom.
+ if (currentSpan.value < 0) {
+ effectiveSpan = 1.0f / effectiveSpan;
+ }
+
+ PinchGestureInput pinchEvent(
+ PinchGestureInput::PINCHGESTURE_SCALE, PinchGestureInput::ONE_TOUCH,
+ mLastTouchInput.mTimeStamp, mLastTouchInput.mScreenOffset,
+ currentFocus, effectiveSpan, mPreviousSpan,
+ mLastTouchInput.modifiers);
+
+ rv = mAsyncPanZoomController->HandleGestureEvent(pinchEvent);
+ mPreviousSpan = effectiveSpan;
+
+ break;
+ }
+
+ default:
+ NS_WARNING("Unhandled state upon touch move");
+ SetState(GESTURE_NONE);
+ break;
+ }
+
+ return rv;
+}
+
+nsEventStatus GestureEventListener::HandleInputTouchEnd() {
+ // We intentionally do not pass apzc return statuses up since
+ // it may cause apzc stay in the touching state even after
+ // gestures are completed (please see Bug 1013378 for reference).
+
+ nsEventStatus rv = nsEventStatus_eIgnore;
+
+ switch (mState) {
+ case GESTURE_NONE:
+ // GEL doesn't have a dedicated state for PANNING handled in APZC thus
+ // ignore.
+ break;
+
+ case GESTURE_FIRST_SINGLE_TOUCH_DOWN: {
+ CancelLongTapTimeoutTask();
+ CancelMaxTapTimeoutTask();
+ nsEventStatus tapupStatus = mAsyncPanZoomController->HandleGestureEvent(
+ CreateTapEvent(mLastTouchInput, TapGestureInput::TAPGESTURE_UP));
+ mSingleTapSent = Some(tapupStatus != nsEventStatus_eIgnore);
+ SetState(GESTURE_FIRST_SINGLE_TOUCH_UP);
+ CreateMaxTapTimeoutTask();
+ break;
+ }
+
+ case GESTURE_SECOND_SINGLE_TOUCH_DOWN: {
+ CancelMaxTapTimeoutTask();
+ MOZ_ASSERT(mSingleTapSent.isSome());
+ mAsyncPanZoomController->HandleGestureEvent(CreateTapEvent(
+ mLastTouchInput, mSingleTapSent.value()
+ ? TapGestureInput::TAPGESTURE_SECOND
+ : TapGestureInput::TAPGESTURE_DOUBLE));
+ mSingleTapSent = Nothing();
+ SetState(GESTURE_NONE);
+ break;
+ }
+
+ case GESTURE_FIRST_SINGLE_TOUCH_MAX_TAP_DOWN:
+ CancelLongTapTimeoutTask();
+ SetState(GESTURE_NONE);
+ TriggerSingleTapConfirmedEvent();
+ break;
+
+ case GESTURE_LONG_TOUCH_DOWN: {
+ SetState(GESTURE_NONE);
+ mAsyncPanZoomController->HandleGestureEvent(
+ CreateTapEvent(mLastTouchInput, TapGestureInput::TAPGESTURE_LONG_UP));
+ break;
+ }
+
+ case GESTURE_MULTI_TOUCH_DOWN:
+ if (mTouches.Length() < 2) {
+ SetState(GESTURE_NONE);
+ }
+ break;
+
+ case GESTURE_PINCH:
+ if (mTouches.Length() < 2) {
+ SetState(GESTURE_NONE);
+ PinchGestureInput::PinchGestureType type =
+ PinchGestureInput::PINCHGESTURE_END;
+ ScreenPoint point;
+ if (mTouches.Length() == 1) {
+ // As user still keeps one finger down the event's focus point should
+ // contain meaningful data.
+ type = PinchGestureInput::PINCHGESTURE_FINGERLIFTED;
+ point = mTouches[0].mScreenPoint;
+ }
+ PinchGestureInput pinchEvent(type, PinchGestureInput::TOUCH,
+ mLastTouchInput.mTimeStamp,
+ mLastTouchInput.mScreenOffset, point, 1.0f,
+ 1.0f, mLastTouchInput.modifiers);
+ mAsyncPanZoomController->HandleGestureEvent(pinchEvent);
+ }
+
+ rv = nsEventStatus_eConsumeNoDefault;
+
+ break;
+
+ case GESTURE_ONE_TOUCH_PINCH: {
+ SetState(GESTURE_NONE);
+ PinchGestureInput pinchEvent(
+ PinchGestureInput::PINCHGESTURE_END, PinchGestureInput::ONE_TOUCH,
+ mLastTouchInput.mTimeStamp, mLastTouchInput.mScreenOffset,
+ ScreenPoint(), 1.0f, 1.0f, mLastTouchInput.modifiers);
+ mAsyncPanZoomController->HandleGestureEvent(pinchEvent);
+
+ rv = nsEventStatus_eConsumeNoDefault;
+
+ break;
+ }
+
+ default:
+ NS_WARNING("Unhandled state upon touch end");
+ SetState(GESTURE_NONE);
+ break;
+ }
+
+ return rv;
+}
+
+nsEventStatus GestureEventListener::HandleInputTouchCancel() {
+ mSingleTapSent = Nothing();
+ SetState(GESTURE_NONE);
+ CancelMaxTapTimeoutTask();
+ CancelLongTapTimeoutTask();
+ return nsEventStatus_eIgnore;
+}
+
+void GestureEventListener::HandleInputTimeoutLongTap() {
+ GEL_LOG("Running long-tap timeout task in state %d\n", mState);
+
+ mLongTapTimeoutTask = nullptr;
+
+ switch (mState) {
+ case GESTURE_FIRST_SINGLE_TOUCH_DOWN:
+ // just in case MaxTapTime > ContextMenuDelay cancel MaxTap timer
+ // and fall through
+ CancelMaxTapTimeoutTask();
+ [[fallthrough]];
+ case GESTURE_FIRST_SINGLE_TOUCH_MAX_TAP_DOWN: {
+ SetState(GESTURE_LONG_TOUCH_DOWN);
+ mAsyncPanZoomController->HandleGestureEvent(
+ CreateTapEvent(mLastTouchInput, TapGestureInput::TAPGESTURE_LONG));
+ break;
+ }
+ default:
+ NS_WARNING("Unhandled state upon long tap timeout");
+ SetState(GESTURE_NONE);
+ break;
+ }
+}
+
+void GestureEventListener::HandleInputTimeoutMaxTap(bool aDuringFastFling) {
+ GEL_LOG("Running max-tap timeout task in state %d\n", mState);
+
+ mMaxTapTimeoutTask = nullptr;
+
+ if (mState == GESTURE_FIRST_SINGLE_TOUCH_DOWN) {
+ SetState(GESTURE_FIRST_SINGLE_TOUCH_MAX_TAP_DOWN);
+ } else if (mState == GESTURE_FIRST_SINGLE_TOUCH_UP ||
+ mState == GESTURE_SECOND_SINGLE_TOUCH_DOWN) {
+ MOZ_ASSERT(mSingleTapSent.isSome());
+ if (!aDuringFastFling && !mSingleTapSent.value()) {
+ TriggerSingleTapConfirmedEvent();
+ }
+ mSingleTapSent = Nothing();
+ SetState(GESTURE_NONE);
+ } else {
+ NS_WARNING("Unhandled state upon MAX_TAP timeout");
+ SetState(GESTURE_NONE);
+ }
+}
+
+void GestureEventListener::TriggerSingleTapConfirmedEvent() {
+ mAsyncPanZoomController->HandleGestureEvent(
+ CreateTapEvent(mLastTapInput, TapGestureInput::TAPGESTURE_CONFIRMED));
+}
+
+void GestureEventListener::SetState(GestureState aState) {
+ mState = aState;
+
+ if (mState == GESTURE_NONE) {
+ mSpanChange = 0.0f;
+ mPreviousSpan = 0.0f;
+ mFocusChange = 0.0f;
+ } else if (mState == GESTURE_MULTI_TOUCH_DOWN) {
+ mPreviousSpan = GetCurrentSpan(mLastTouchInput);
+ mPreviousFocus = GetCurrentFocus(mLastTouchInput);
+ }
+}
+
+void GestureEventListener::CancelLongTapTimeoutTask() {
+ if (mState == GESTURE_SECOND_SINGLE_TOUCH_DOWN) {
+ // being in this state means the task has been canceled already
+ return;
+ }
+
+ if (mLongTapTimeoutTask) {
+ mLongTapTimeoutTask->Cancel();
+ mLongTapTimeoutTask = nullptr;
+ }
+}
+
+void GestureEventListener::CreateLongTapTimeoutTask() {
+ RefPtr<CancelableRunnable> task = NewCancelableRunnableMethod(
+ "layers::GestureEventListener::HandleInputTimeoutLongTap", this,
+ &GestureEventListener::HandleInputTimeoutLongTap);
+
+ mLongTapTimeoutTask = task;
+
+ TouchBlockState* block =
+ mAsyncPanZoomController->GetInputQueue()->GetCurrentTouchBlock();
+ MOZ_ASSERT(block);
+ long alreadyElapsed =
+ static_cast<long>(block->GetTimeSinceBlockStart().ToMilliseconds());
+ long remainingDelay =
+ StaticPrefs::ui_click_hold_context_menus_delay() - alreadyElapsed;
+ mAsyncPanZoomController->PostDelayedTask(task.forget(),
+ std::max(0L, remainingDelay));
+}
+
+void GestureEventListener::CancelMaxTapTimeoutTask() {
+ if (mState == GESTURE_FIRST_SINGLE_TOUCH_MAX_TAP_DOWN) {
+ // being in this state means the timer has just been triggered
+ return;
+ }
+
+ if (mMaxTapTimeoutTask) {
+ mMaxTapTimeoutTask->Cancel();
+ mMaxTapTimeoutTask = nullptr;
+ }
+}
+
+void GestureEventListener::CreateMaxTapTimeoutTask() {
+ mLastTapInput = mLastTouchInput;
+
+ TouchBlockState* block =
+ mAsyncPanZoomController->GetInputQueue()->GetCurrentTouchBlock();
+ MOZ_ASSERT(block);
+ RefPtr<CancelableRunnable> task = NewCancelableRunnableMethod<bool>(
+ "layers::GestureEventListener::HandleInputTimeoutMaxTap", this,
+ &GestureEventListener::HandleInputTimeoutMaxTap,
+ block->IsDuringFastFling());
+
+ mMaxTapTimeoutTask = task;
+
+ long alreadyElapsed =
+ static_cast<long>(block->GetTimeSinceBlockStart().ToMilliseconds());
+ long remainingDelay = StaticPrefs::apz_max_tap_time() - alreadyElapsed;
+ mAsyncPanZoomController->PostDelayedTask(task.forget(),
+ std::max(0L, remainingDelay));
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/GestureEventListener.h b/gfx/layers/apz/src/GestureEventListener.h
new file mode 100644
index 0000000000..aa51889fdd
--- /dev/null
+++ b/gfx/layers/apz/src/GestureEventListener.h
@@ -0,0 +1,285 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_GestureEventListener_h
+#define mozilla_layers_GestureEventListener_h
+
+#include "InputData.h" // for MultiTouchInput, etc
+#include "Units.h"
+#include "mozilla/EventForwards.h" // for nsEventStatus
+#include "mozilla/RefPtr.h" // for RefPtr
+#include "nsISupportsImpl.h"
+#include "nsTArray.h" // for nsTArray
+
+namespace mozilla {
+
+class CancelableRunnable;
+
+namespace layers {
+
+class AsyncPanZoomController;
+
+/**
+ * Platform-non-specific, generalized gesture event listener. This class
+ * intercepts all touches events on their way to AsyncPanZoomController and
+ * determines whether or not they are part of a gesture.
+ *
+ * For example, seeing that two fingers are on the screen means that the user
+ * wants to do a pinch gesture, so we don't forward the touches along to
+ * AsyncPanZoomController since it will think that they are just trying to pan
+ * the screen. Instead, we generate a PinchGestureInput and send that. If the
+ * touch event is not part of a gesture, we just return nsEventStatus_eIgnore
+ * and AsyncPanZoomController is expected to handle it.
+ */
+class GestureEventListener final {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(GestureEventListener)
+
+ explicit GestureEventListener(
+ AsyncPanZoomController* aAsyncPanZoomController);
+
+ // --------------------------------------------------------------------------
+ // These methods must only be called on the controller/UI thread.
+ //
+
+ /**
+ * General input handler for a touch event. If the touch event is not a part
+ * of a gesture, then we pass it along to AsyncPanZoomController. Otherwise,
+ * it gets consumed here and never forwarded along.
+ */
+ nsEventStatus HandleInputEvent(const MultiTouchInput& aEvent);
+
+ /**
+ * Returns the identifier of the touch in the last touch event processed by
+ * this GestureEventListener. This should only be called when the last touch
+ * event contained only one touch.
+ */
+ int32_t GetLastTouchIdentifier() const;
+
+ /**
+ * Function used to disable long tap gestures.
+ *
+ * On slow running tests, drags and touch events can be misinterpreted
+ * as a long tap. This allows tests to disable long tap gesture detection.
+ */
+ static void SetLongTapEnabled(bool aLongTapEnabled);
+ static bool IsLongTapEnabled();
+
+ private:
+ // Private destructor, to discourage deletion outside of Release():
+ ~GestureEventListener();
+
+ /**
+ * States of GEL finite-state machine.
+ */
+ enum GestureState {
+ // This is the initial and final state of any gesture.
+ // In this state there's no gesture going on, and we don't think we're
+ // about to enter one.
+ // Allowed next states: GESTURE_FIRST_SINGLE_TOUCH_DOWN,
+ // GESTURE_MULTI_TOUCH_DOWN.
+ GESTURE_NONE,
+
+ // A touch start with a single touch point has just happened.
+ // After having gotten into this state we start timers for MAX_TAP_TIME and
+ // StaticPrefs::ui_click_hold_context_menus_delay().
+ // Allowed next states: GESTURE_MULTI_TOUCH_DOWN, GESTURE_NONE,
+ // GESTURE_FIRST_SINGLE_TOUCH_UP,
+ // GESTURE_LONG_TOUCH_DOWN,
+ // GESTURE_FIRST_SINGLE_TOUCH_MAX_TAP_DOWN.
+ GESTURE_FIRST_SINGLE_TOUCH_DOWN,
+
+ // While in GESTURE_FIRST_SINGLE_TOUCH_DOWN state a MAX_TAP_TIME timer got
+ // triggered. Now we'll trigger either a single tap if a user lifts her
+ // finger or a long tap if StaticPrefs::ui_click_hold_context_menus_delay()
+ // happens first.
+ // Allowed next states: GESTURE_MULTI_TOUCH_DOWN, GESTURE_NONE,
+ // GESTURE_LONG_TOUCH_DOWN.
+ GESTURE_FIRST_SINGLE_TOUCH_MAX_TAP_DOWN,
+
+ // A user put her finger down and lifted it up quickly enough.
+ // After having gotten into this state we clear the timer for MAX_TAP_TIME.
+ // Allowed next states: GESTURE_SECOND_SINGLE_TOUCH_DOWN, GESTURE_NONE,
+ // GESTURE_MULTI_TOUCH_DOWN.
+ GESTURE_FIRST_SINGLE_TOUCH_UP,
+
+ // A user put down her finger again right after a single tap thus the
+ // gesture can't be a single tap, but rather a double tap. But we're
+ // still not sure about that until the user lifts her finger again.
+ // Allowed next states: GESTURE_MULTI_TOUCH_DOWN, GESTURE_ONE_TOUCH_PINCH,
+ // GESTURE_NONE.
+ GESTURE_SECOND_SINGLE_TOUCH_DOWN,
+
+ // A long touch has happened, but the user still keeps her finger down.
+ // We'll trigger a "long tap up" event when the finger is up.
+ // Allowed next states: GESTURE_NONE, GESTURE_MULTI_TOUCH_DOWN.
+ GESTURE_LONG_TOUCH_DOWN,
+
+ // We have detected that two or more fingers are on the screen, but there
+ // hasn't been enough movement yet to make us start actually zooming the
+ // screen.
+ // Allowed next states: GESTURE_PINCH, GESTURE_NONE
+ GESTURE_MULTI_TOUCH_DOWN,
+
+ // There are two or more fingers on the screen, and the user has already
+ // pinched enough for us to start zooming the screen.
+ // Allowed next states: GESTURE_NONE
+ GESTURE_PINCH,
+
+ // The user has double tapped, but not lifted her finger, and moved her
+ // finger more than PINCH_START_THRESHOLD.
+ // Allowed next states: GESTURE_NONE.
+ GESTURE_ONE_TOUCH_PINCH
+ };
+
+ /**
+ * These HandleInput* functions comprise input alphabet of the GEL
+ * finite-state machine triggering state transitions.
+ */
+ nsEventStatus HandleInputTouchSingleStart();
+ nsEventStatus HandleInputTouchMultiStart();
+ nsEventStatus HandleInputTouchEnd();
+ nsEventStatus HandleInputTouchMove();
+ nsEventStatus HandleInputTouchCancel();
+ void HandleInputTimeoutLongTap();
+ void HandleInputTimeoutMaxTap(bool aDuringFastFling);
+
+ void EnterFirstSingleTouchDown();
+
+ void TriggerSingleTapConfirmedEvent();
+
+ bool MoveDistanceExceeds(ScreenCoord aThreshold) const;
+ bool MoveDistanceIsLarge() const;
+ bool SecondTapIsFar() const;
+
+ /**
+ * Returns current vertical span, counting from the where the gesture first
+ * began (after a brief delay detecting the gesture from first touch).
+ */
+ ScreenCoord GetYSpanFromGestureStartPoint();
+
+ /**
+ * Do actual state transition and reset substates.
+ */
+ void SetState(GestureState aState);
+
+ RefPtr<AsyncPanZoomController> mAsyncPanZoomController;
+
+ /**
+ * Array containing all active touches. When a touch happens it, gets added to
+ * this array, even if we choose not to handle it. When it ends, we remove it.
+ * We need to maintain this array in order to detect the end of the
+ * "multitouch" states because touch start events contain all current touches,
+ * but touch end events contain only those touches that have gone.
+ */
+ nsTArray<SingleTouchData> mTouches;
+
+ /**
+ * Current state we're dealing with.
+ */
+ GestureState mState;
+
+ /**
+ * Total change in span since we detected a pinch gesture. Only used when we
+ * are in the |GESTURE_WAITING_PINCH| state and need to know how far zoomed
+ * out we are compared to our original pinch span. Note that this does _not_
+ * continue to be updated once we jump into the |GESTURE_PINCH| state.
+ */
+ ScreenCoord mSpanChange;
+
+ /**
+ * Previous span calculated for the purposes of setting inside a
+ * PinchGestureInput.
+ */
+ ScreenCoord mPreviousSpan;
+
+ /* Properties similar to mSpanChange and mPreviousSpan, but for the focus */
+ ScreenCoord mFocusChange;
+ ScreenPoint mPreviousFocus;
+
+ /**
+ * Cached copy of the last touch input.
+ */
+ MultiTouchInput mLastTouchInput;
+
+ /**
+ * Cached copy of the last tap gesture input.
+ * In the situation when we have a tap followed by a pinch we lose info
+ * about tap since we keep only last input and to dispatch it correctly
+ * we save last tap copy into this variable.
+ * For more info see bug 947892.
+ */
+ MultiTouchInput mLastTapInput;
+
+ /**
+ * Position of the last touch that exceeds the GetTouchStartTolerance when
+ * performing a one-touch-pinch gesture; using the mTouchStartPosition is
+ * slightly inaccurate because by the time the touch position has passed
+ * the threshold for the gesture, there is already a span that the zoom
+ * is calculated from, instead of starting at 1.0 when the threshold gets
+ * passed.
+ */
+ ScreenPoint mOneTouchPinchStartPosition;
+
+ /**
+ * Position of the last touch starting. This is only valid during an attempt
+ * to determine if a touch is a tap. If a touch point moves away from
+ * mTouchStartPosition to the distance greater than
+ * AsyncPanZoomController::GetTouchStartTolerance() while in
+ * GESTURE_FIRST_SINGLE_TOUCH_DOWN, GESTURE_FIRST_SINGLE_TOUCH_MAX_TAP_DOWN
+ * or GESTURE_SECOND_SINGLE_TOUCH_DOWN then we're certain the gesture is
+ * not tap.
+ */
+ ScreenPoint mTouchStartPosition;
+
+ /**
+ * We store the window/GeckoView's display offset as well, so we can
+ * track the user's physical touch movements in absolute display coordinates.
+ */
+ ExternalPoint mTouchStartOffset;
+
+ /**
+ * Task used to timeout a long tap. This gets posted to the UI thread such
+ * that it runs a time when a single tap happens. We cache it so that
+ * we can cancel it if any other touch event happens.
+ *
+ * The task is supposed to be non-null if in GESTURE_FIRST_SINGLE_TOUCH_DOWN
+ * and GESTURE_FIRST_SINGLE_TOUCH_MAX_TAP_DOWN states.
+ *
+ * CancelLongTapTimeoutTask: Cancel the mLongTapTimeoutTask and also set
+ * it to null.
+ */
+ RefPtr<CancelableRunnable> mLongTapTimeoutTask;
+ void CancelLongTapTimeoutTask();
+ void CreateLongTapTimeoutTask();
+
+ /**
+ * Task used to timeout a single tap or a double tap.
+ *
+ * The task is supposed to be non-null if in GESTURE_FIRST_SINGLE_TOUCH_DOWN,
+ * GESTURE_FIRST_SINGLE_TOUCH_UP and GESTURE_SECOND_SINGLE_TOUCH_DOWN states.
+ *
+ * CancelMaxTapTimeoutTask: Cancel the mMaxTapTimeoutTask and also set
+ * it to null.
+ */
+ RefPtr<CancelableRunnable> mMaxTapTimeoutTask;
+ void CancelMaxTapTimeoutTask();
+ void CreateMaxTapTimeoutTask();
+
+ /**
+ * Tracks whether the single-tap event was already sent to content. This is
+ * needed because it affects how the double-tap gesture, if detected, is
+ * handled. The value is only valid in states GESTURE_FIRST_SINGLE_TOUCH_UP
+ * and GESTURE_SECOND_SINGLE_TOUCH_DOWN; to more easily catch violations it is
+ * stored in a Maybe which is set to Nothing() at all other times.
+ */
+ Maybe<bool> mSingleTapSent;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif
diff --git a/gfx/layers/apz/src/HitTestingTreeNode.cpp b/gfx/layers/apz/src/HitTestingTreeNode.cpp
new file mode 100644
index 0000000000..d25a9c1d5f
--- /dev/null
+++ b/gfx/layers/apz/src/HitTestingTreeNode.cpp
@@ -0,0 +1,419 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "HitTestingTreeNode.h"
+#include <stack>
+
+#include "AsyncPanZoomController.h" // for AsyncPanZoomController
+#include "mozilla/StaticPrefs_layout.h"
+#include "mozilla/gfx/Point.h" // for Point4D
+#include "mozilla/layers/APZUtils.h" // for AsyncTransform, CompleteAsyncTransform
+#include "mozilla/layers/AsyncDragMetrics.h" // for AsyncDragMetrics
+#include "mozilla/ToString.h" // for ToString
+#include "nsPrintfCString.h" // for nsPrintfCString
+#include "UnitTransforms.h" // for ViewAs
+
+static mozilla::LazyLogModule sApzMgrLog("apz.manager");
+
+namespace mozilla {
+namespace layers {
+
+using gfx::CompositorHitTestInfo;
+
+HitTestingTreeNode::HitTestingTreeNode(AsyncPanZoomController* aApzc,
+ bool aIsPrimaryHolder,
+ LayersId aLayersId)
+ : mApzc(aApzc),
+ mIsPrimaryApzcHolder(aIsPrimaryHolder),
+ mLockCount(0),
+ mLayersId(aLayersId),
+ mFixedPosTarget(ScrollableLayerGuid::NULL_SCROLL_ID),
+ mStickyPosTarget(ScrollableLayerGuid::NULL_SCROLL_ID),
+ mOverride(EventRegionsOverride::NoOverride) {
+ if (mIsPrimaryApzcHolder) {
+ MOZ_ASSERT(mApzc);
+ }
+ MOZ_ASSERT(!mApzc || mApzc->GetLayersId() == mLayersId);
+}
+
+void HitTestingTreeNode::RecycleWith(
+ const RecursiveMutexAutoLock& aProofOfTreeLock,
+ AsyncPanZoomController* aApzc, LayersId aLayersId) {
+ MOZ_ASSERT(IsRecyclable(aProofOfTreeLock));
+ Destroy(); // clear out tree pointers
+ mApzc = aApzc;
+ mLayersId = aLayersId;
+ MOZ_ASSERT(!mApzc || mApzc->GetLayersId() == mLayersId);
+ // The caller is expected to call appropriate setters (SetHitTestData,
+ // SetScrollbarData, SetFixedPosData, SetStickyPosData, etc.) to repopulate
+ // all the data fields before using this node for "real work". Otherwise
+ // those data fields may contain stale information from the previous use
+ // of this node object.
+}
+
+HitTestingTreeNode::~HitTestingTreeNode() = default;
+
+void HitTestingTreeNode::Destroy() {
+ // This runs on the updater thread, it's not worth passing around extra raw
+ // pointers just to assert it.
+
+ mPrevSibling = nullptr;
+ mLastChild = nullptr;
+ mParent = nullptr;
+
+ if (mApzc) {
+ if (mIsPrimaryApzcHolder) {
+ mApzc->Destroy();
+ }
+ mApzc = nullptr;
+ }
+}
+
+bool HitTestingTreeNode::IsRecyclable(
+ const RecursiveMutexAutoLock& aProofOfTreeLock) {
+ return !(IsPrimaryHolder() || (mLockCount > 0));
+}
+
+void HitTestingTreeNode::SetLastChild(HitTestingTreeNode* aChild) {
+ mLastChild = aChild;
+ if (aChild) {
+ aChild->mParent = this;
+
+ if (aChild->GetApzc()) {
+ AsyncPanZoomController* parent = GetNearestContainingApzc();
+ // We assume that HitTestingTreeNodes with an ancestor/descendant
+ // relationship cannot both point to the same APZC instance. This
+ // assertion only covers a subset of cases in which that might occur,
+ // but it's better than nothing.
+ MOZ_ASSERT(aChild->GetApzc() != parent);
+ aChild->SetApzcParent(parent);
+ }
+ }
+}
+
+void HitTestingTreeNode::SetScrollbarData(
+ const Maybe<uint64_t>& aScrollbarAnimationId,
+ const ScrollbarData& aScrollbarData) {
+ mScrollbarAnimationId = aScrollbarAnimationId;
+ mScrollbarData = aScrollbarData;
+}
+
+bool HitTestingTreeNode::MatchesScrollDragMetrics(
+ const AsyncDragMetrics& aDragMetrics, LayersId aLayersId) const {
+ return IsScrollThumbNode() && mLayersId == aLayersId &&
+ mScrollbarData.mDirection == aDragMetrics.mDirection &&
+ mScrollbarData.mTargetViewId == aDragMetrics.mViewId;
+}
+
+bool HitTestingTreeNode::IsScrollThumbNode() const {
+ return mScrollbarData.mScrollbarLayerType ==
+ layers::ScrollbarLayerType::Thumb;
+}
+
+bool HitTestingTreeNode::IsScrollbarNode() const {
+ return mScrollbarData.mScrollbarLayerType != layers::ScrollbarLayerType::None;
+}
+
+bool HitTestingTreeNode::IsScrollbarContainerNode() const {
+ return mScrollbarData.mScrollbarLayerType ==
+ layers::ScrollbarLayerType::Container;
+}
+
+ScrollDirection HitTestingTreeNode::GetScrollbarDirection() const {
+ MOZ_ASSERT(IsScrollbarNode());
+ MOZ_ASSERT(mScrollbarData.mDirection.isSome());
+ return *mScrollbarData.mDirection;
+}
+
+ScrollableLayerGuid::ViewID HitTestingTreeNode::GetScrollTargetId() const {
+ return mScrollbarData.mTargetViewId;
+}
+
+Maybe<uint64_t> HitTestingTreeNode::GetScrollbarAnimationId() const {
+ return mScrollbarAnimationId;
+}
+
+const ScrollbarData& HitTestingTreeNode::GetScrollbarData() const {
+ return mScrollbarData;
+}
+
+void HitTestingTreeNode::SetFixedPosData(
+ ScrollableLayerGuid::ViewID aFixedPosTarget, SideBits aFixedPosSides,
+ const Maybe<uint64_t>& aFixedPositionAnimationId) {
+ mFixedPosTarget = aFixedPosTarget;
+ mFixedPosSides = aFixedPosSides;
+ mFixedPositionAnimationId = aFixedPositionAnimationId;
+}
+
+ScrollableLayerGuid::ViewID HitTestingTreeNode::GetFixedPosTarget() const {
+ return mFixedPosTarget;
+}
+
+SideBits HitTestingTreeNode::GetFixedPosSides() const { return mFixedPosSides; }
+
+Maybe<uint64_t> HitTestingTreeNode::GetFixedPositionAnimationId() const {
+ return mFixedPositionAnimationId;
+}
+
+void HitTestingTreeNode::SetPrevSibling(HitTestingTreeNode* aSibling) {
+ mPrevSibling = aSibling;
+ if (aSibling) {
+ aSibling->mParent = mParent;
+
+ if (aSibling->GetApzc()) {
+ AsyncPanZoomController* parent =
+ mParent ? mParent->GetNearestContainingApzc() : nullptr;
+ aSibling->SetApzcParent(parent);
+ }
+ }
+}
+
+void HitTestingTreeNode::SetStickyPosData(
+ ScrollableLayerGuid::ViewID aStickyPosTarget,
+ const LayerRectAbsolute& aScrollRangeOuter,
+ const LayerRectAbsolute& aScrollRangeInner,
+ const Maybe<uint64_t>& aStickyPositionAnimationId) {
+ mStickyPosTarget = aStickyPosTarget;
+ mStickyScrollRangeOuter = aScrollRangeOuter;
+ mStickyScrollRangeInner = aScrollRangeInner;
+ mStickyPositionAnimationId = aStickyPositionAnimationId;
+}
+
+ScrollableLayerGuid::ViewID HitTestingTreeNode::GetStickyPosTarget() const {
+ return mStickyPosTarget;
+}
+
+const LayerRectAbsolute& HitTestingTreeNode::GetStickyScrollRangeOuter() const {
+ return mStickyScrollRangeOuter;
+}
+
+const LayerRectAbsolute& HitTestingTreeNode::GetStickyScrollRangeInner() const {
+ return mStickyScrollRangeInner;
+}
+
+Maybe<uint64_t> HitTestingTreeNode::GetStickyPositionAnimationId() const {
+ return mStickyPositionAnimationId;
+}
+
+void HitTestingTreeNode::MakeRoot() {
+ mParent = nullptr;
+
+ if (GetApzc()) {
+ SetApzcParent(nullptr);
+ }
+}
+
+HitTestingTreeNode* HitTestingTreeNode::GetFirstChild() const {
+ HitTestingTreeNode* child = GetLastChild();
+ while (child && child->GetPrevSibling()) {
+ child = child->GetPrevSibling();
+ }
+ return child;
+}
+
+HitTestingTreeNode* HitTestingTreeNode::GetLastChild() const {
+ return mLastChild;
+}
+
+HitTestingTreeNode* HitTestingTreeNode::GetPrevSibling() const {
+ return mPrevSibling;
+}
+
+HitTestingTreeNode* HitTestingTreeNode::GetParent() const { return mParent; }
+
+bool HitTestingTreeNode::IsAncestorOf(const HitTestingTreeNode* aOther) const {
+ for (const HitTestingTreeNode* cur = aOther; cur; cur = cur->GetParent()) {
+ if (cur == this) {
+ return true;
+ }
+ }
+ return false;
+}
+
+AsyncPanZoomController* HitTestingTreeNode::GetApzc() const { return mApzc; }
+
+AsyncPanZoomController* HitTestingTreeNode::GetNearestContainingApzc() const {
+ for (const HitTestingTreeNode* n = this; n; n = n->GetParent()) {
+ if (n->GetApzc()) {
+ return n->GetApzc();
+ }
+ }
+ return nullptr;
+}
+
+bool HitTestingTreeNode::IsPrimaryHolder() const {
+ return mIsPrimaryApzcHolder;
+}
+
+LayersId HitTestingTreeNode::GetLayersId() const { return mLayersId; }
+
+void HitTestingTreeNode::SetHitTestData(
+ const LayerIntRegion& aVisibleRegion,
+ const LayerIntSize& aRemoteDocumentSize,
+ const CSSTransformMatrix& aTransform, const EventRegionsOverride& aOverride,
+ const Maybe<ScrollableLayerGuid::ViewID>& aAsyncZoomContainerId) {
+ mVisibleRegion = aVisibleRegion;
+ mRemoteDocumentSize = aRemoteDocumentSize;
+ mTransform = aTransform;
+ mOverride = aOverride;
+ mAsyncZoomContainerId = aAsyncZoomContainerId;
+}
+
+EventRegionsOverride HitTestingTreeNode::GetEventRegionsOverride() const {
+ return mOverride;
+}
+
+const CSSTransformMatrix& HitTestingTreeNode::GetTransform() const {
+ return mTransform;
+}
+
+LayerToScreenMatrix4x4 HitTestingTreeNode::GetTransformToGecko() const {
+ if (mParent) {
+ LayerToParentLayerMatrix4x4 thisToParent =
+ mTransform * AsyncTransformMatrix();
+ if (mApzc) {
+ thisToParent =
+ thisToParent * ViewAs<ParentLayerToParentLayerMatrix4x4>(
+ mApzc->GetTransformToLastDispatchedPaint());
+ }
+ ParentLayerToScreenMatrix4x4 parentToRoot =
+ ViewAs<ParentLayerToScreenMatrix4x4>(
+ mParent->GetTransformToGecko(),
+ PixelCastJustification::MovingDownToChildren);
+ return thisToParent * parentToRoot;
+ }
+
+ return ViewAs<LayerToScreenMatrix4x4>(
+ mTransform * AsyncTransformMatrix(),
+ PixelCastJustification::ScreenIsParentLayerForRoot);
+}
+
+const LayerIntRegion& HitTestingTreeNode::GetVisibleRegion() const {
+ return mVisibleRegion;
+}
+
+ScreenRect HitTestingTreeNode::GetRemoteDocumentScreenRect() const {
+ ScreenRect result = TransformBy(
+ GetTransformToGecko(),
+ IntRectToRect(LayerIntRect(LayerIntPoint(), mRemoteDocumentSize)));
+
+ for (const HitTestingTreeNode* node = this; node; node = node->GetParent()) {
+ if (!node->GetApzc()) {
+ continue;
+ }
+
+ ParentLayerRect compositionBounds = node->GetApzc()->GetCompositionBounds();
+ if (compositionBounds.IsEmpty()) {
+ return ScreenRect();
+ }
+
+ ScreenRect scrollPortOnScreenCoordinate = TransformBy(
+ node->GetParent() ? node->GetParent()->GetTransformToGecko()
+ : LayerToScreenMatrix4x4(),
+ ViewAs<LayerPixel>(compositionBounds,
+ PixelCastJustification::MovingDownToChildren));
+ if (scrollPortOnScreenCoordinate.IsEmpty()) {
+ return ScreenRect();
+ }
+
+ result = result.Intersect(scrollPortOnScreenCoordinate);
+ if (result.IsEmpty()) {
+ return ScreenRect();
+ }
+ }
+ return result;
+}
+
+Maybe<ScrollableLayerGuid::ViewID> HitTestingTreeNode::GetAsyncZoomContainerId()
+ const {
+ return mAsyncZoomContainerId;
+}
+
+void HitTestingTreeNode::Dump(const char* aPrefix) const {
+ MOZ_LOG(
+ sApzMgrLog, LogLevel::Debug,
+ ("%sHitTestingTreeNode (%p) APZC (%p) g=(%s) %s%s%s t=(%s) "
+ "%s%s\n",
+ aPrefix, this, mApzc.get(),
+ mApzc ? ToString(mApzc->GetGuid()).c_str()
+ : nsPrintfCString("l=0x%" PRIx64, uint64_t(mLayersId)).get(),
+ (mOverride & EventRegionsOverride::ForceDispatchToContent) ? "fdtc "
+ : "",
+ (mOverride & EventRegionsOverride::ForceEmptyHitRegion) ? "fehr " : "",
+ (mFixedPosTarget != ScrollableLayerGuid::NULL_SCROLL_ID)
+ ? nsPrintfCString("fixed=%" PRIu64 " ", mFixedPosTarget).get()
+ : "",
+ ToString(mTransform).c_str(),
+ mScrollbarData.mDirection.isSome() ? " scrollbar" : "",
+ IsScrollThumbNode() ? " scrollthumb" : ""));
+
+ if (!mLastChild) {
+ return;
+ }
+
+ // Dump the children in order from first child to last child
+ std::stack<HitTestingTreeNode*> children;
+ for (HitTestingTreeNode* child = mLastChild.get(); child;
+ child = child->mPrevSibling) {
+ children.push(child);
+ }
+ nsPrintfCString childPrefix("%s ", aPrefix);
+ while (!children.empty()) {
+ children.top()->Dump(childPrefix.get());
+ children.pop();
+ }
+}
+
+void HitTestingTreeNode::SetApzcParent(AsyncPanZoomController* aParent) {
+ // precondition: GetApzc() is non-null
+ MOZ_ASSERT(GetApzc() != nullptr);
+ if (IsPrimaryHolder()) {
+ GetApzc()->SetParent(aParent);
+ } else {
+ MOZ_ASSERT(GetApzc()->GetParent() == aParent);
+ }
+}
+
+void HitTestingTreeNode::Lock(const RecursiveMutexAutoLock& aProofOfTreeLock) {
+ mLockCount++;
+}
+
+void HitTestingTreeNode::Unlock(
+ const RecursiveMutexAutoLock& aProofOfTreeLock) {
+ MOZ_ASSERT(mLockCount > 0);
+ mLockCount--;
+}
+
+HitTestingTreeNodeAutoLock::HitTestingTreeNodeAutoLock()
+ : mTreeMutex(nullptr) {}
+
+HitTestingTreeNodeAutoLock::~HitTestingTreeNodeAutoLock() { Clear(); }
+
+void HitTestingTreeNodeAutoLock::Initialize(
+ const RecursiveMutexAutoLock& aProofOfTreeLock,
+ already_AddRefed<HitTestingTreeNode> aNode, RecursiveMutex& aTreeMutex) {
+ MOZ_ASSERT(!mNode);
+
+ mNode = aNode;
+ mTreeMutex = &aTreeMutex;
+
+ mNode->Lock(aProofOfTreeLock);
+}
+
+void HitTestingTreeNodeAutoLock::Clear() {
+ if (!mNode) {
+ return;
+ }
+ MOZ_ASSERT(mTreeMutex);
+
+ { // scope lock
+ RecursiveMutexAutoLock lock(*mTreeMutex);
+ mNode->Unlock(lock);
+ }
+ mNode = nullptr;
+ mTreeMutex = nullptr;
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/HitTestingTreeNode.h b/gfx/layers/apz/src/HitTestingTreeNode.h
new file mode 100644
index 0000000000..ed20f9f561
--- /dev/null
+++ b/gfx/layers/apz/src/HitTestingTreeNode.h
@@ -0,0 +1,270 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_HitTestingTreeNode_h
+#define mozilla_layers_HitTestingTreeNode_h
+
+#include "mozilla/gfx/CompositorHitTestInfo.h"
+#include "mozilla/gfx/Matrix.h" // for Matrix4x4
+#include "mozilla/layers/LayersTypes.h" // for EventRegions
+#include "mozilla/layers/ScrollableLayerGuid.h" // for ScrollableLayerGuid
+#include "mozilla/layers/ScrollbarData.h" // for ScrollbarData
+#include "mozilla/Maybe.h" // for Maybe
+#include "mozilla/RecursiveMutex.h" // for RecursiveMutexAutoLock
+#include "mozilla/RefPtr.h" // for nsRefPtr
+namespace mozilla {
+namespace layers {
+
+class AsyncDragMetrics;
+class AsyncPanZoomController;
+
+/**
+ * This class represents a node in a tree that is used by the APZCTreeManager
+ * to do hit testing. The tree is roughly a copy of the layer tree, but will
+ * contain multiple nodes in cases where the layer has multiple FrameMetrics.
+ * In other words, the structure of this tree should be identical to the
+ * WebRenderScrollDataWrapper tree (see documentation in
+ * WebRenderScrollDataWrapper.h).
+ *
+ * Not all HitTestingTreeNode instances will have an APZC associated with them;
+ * only HitTestingTreeNodes that correspond to layers with scrollable metrics
+ * have APZCs.
+ * Multiple HitTestingTreeNode instances may share the same underlying APZC
+ * instance if the layers they represent share the same scrollable metrics (i.e.
+ * are part of the same animated geometry root). If this happens, exactly one of
+ * the HitTestingTreeNode instances will be designated as the "primary holder"
+ * of the APZC. When this primary holder is destroyed, it will destroy the APZC
+ * along with it; in contrast, destroying non-primary-holder nodes will not
+ * destroy the APZC.
+ * Code should not make assumptions about which of the nodes will be the
+ * primary holder, only that that there will be exactly one for each APZC in
+ * the tree.
+ *
+ * The reason this tree exists at all is so that we can do hit-testing on the
+ * thread that we receive input on (referred to the as the controller thread in
+ * APZ terminology), which may be different from the compositor thread.
+ * Accessing the compositor layer tree can only be done on the compositor
+ * thread, and so it is simpler to make a copy of the hit-testing related
+ * properties into a separate tree.
+ *
+ * The tree pointers on the node (mLastChild, etc.) can only be manipulated
+ * while holding the APZ tree lock. Any code that wishes to use a
+ * HitTestingTreeNode outside of holding the tree lock should do so by using
+ * the HitTestingTreeNodeAutoLock wrapper, which prevents the node from
+ * being recycled (and also holds a RefPtr to the node to prevent it from
+ * getting freed).
+ */
+class HitTestingTreeNode {
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(HitTestingTreeNode);
+
+ private:
+ ~HitTestingTreeNode();
+
+ public:
+ HitTestingTreeNode(AsyncPanZoomController* aApzc, bool aIsPrimaryHolder,
+ LayersId aLayersId);
+ void RecycleWith(const RecursiveMutexAutoLock& aProofOfTreeLock,
+ AsyncPanZoomController* aApzc, LayersId aLayersId);
+ // Clears the tree pointers on the node, thereby breaking RefPtr cycles. This
+ // can trigger free'ing of this and other HitTestingTreeNode instances.
+ void Destroy();
+
+ // Returns true if and only if the node is available for recycling as part
+ // of a hit-testing tree update. Note that this node can have Destroy() called
+ // on it whether or not it is recyclable.
+ bool IsRecyclable(const RecursiveMutexAutoLock& aProofOfTreeLock);
+
+ /* Tree construction methods */
+
+ void SetLastChild(HitTestingTreeNode* aChild);
+ void SetPrevSibling(HitTestingTreeNode* aSibling);
+ void MakeRoot();
+
+ /* Tree walking methods. GetFirstChild is O(n) in the number of children. The
+ * other tree walking methods are all O(1). */
+
+ HitTestingTreeNode* GetFirstChild() const;
+ HitTestingTreeNode* GetLastChild() const;
+ HitTestingTreeNode* GetPrevSibling() const;
+ HitTestingTreeNode* GetParent() const;
+
+ bool IsAncestorOf(const HitTestingTreeNode* aOther) const;
+
+ /* APZC related methods */
+
+ AsyncPanZoomController* GetApzc() const;
+ AsyncPanZoomController* GetNearestContainingApzc() const;
+ bool IsPrimaryHolder() const;
+ LayersId GetLayersId() const;
+
+ /* Hit test related methods */
+
+ void SetHitTestData(
+ const LayerIntRegion& aVisibleRegion,
+ const LayerIntSize& aRemoteDocumentSize,
+ const CSSTransformMatrix& aTransform,
+ const EventRegionsOverride& aOverride,
+ const Maybe<ScrollableLayerGuid::ViewID>& aAsyncZoomContainerId);
+
+ /* Scrollbar info */
+
+ void SetScrollbarData(const Maybe<uint64_t>& aScrollbarAnimationId,
+ const ScrollbarData& aScrollbarData);
+ bool MatchesScrollDragMetrics(const AsyncDragMetrics& aDragMetrics,
+ LayersId aLayersId) const;
+ bool IsScrollbarNode() const; // Scroll thumb or scrollbar container layer.
+ bool IsScrollbarContainerNode() const; // Scrollbar container layer.
+ // This can only be called if IsScrollbarNode() is true
+ ScrollDirection GetScrollbarDirection() const;
+ bool IsScrollThumbNode() const; // Scroll thumb container layer.
+ ScrollableLayerGuid::ViewID GetScrollTargetId() const;
+ const ScrollbarData& GetScrollbarData() const;
+ Maybe<uint64_t> GetScrollbarAnimationId() const;
+
+ /* Fixed pos info */
+
+ void SetFixedPosData(ScrollableLayerGuid::ViewID aFixedPosTarget,
+ SideBits aFixedPosSides,
+ const Maybe<uint64_t>& aFixedPositionAnimationId);
+ ScrollableLayerGuid::ViewID GetFixedPosTarget() const;
+ SideBits GetFixedPosSides() const;
+ Maybe<uint64_t> GetFixedPositionAnimationId() const;
+
+ /* Sticky pos info */
+ void SetStickyPosData(ScrollableLayerGuid::ViewID aStickyPosTarget,
+ const LayerRectAbsolute& aScrollRangeOuter,
+ const LayerRectAbsolute& aScrollRangeInner,
+ const Maybe<uint64_t>& aStickyPositionAnimationId);
+ ScrollableLayerGuid::ViewID GetStickyPosTarget() const;
+ const LayerRectAbsolute& GetStickyScrollRangeOuter() const;
+ const LayerRectAbsolute& GetStickyScrollRangeInner() const;
+ Maybe<uint64_t> GetStickyPositionAnimationId() const;
+
+ /* Returns the mOverride flag. */
+ EventRegionsOverride GetEventRegionsOverride() const;
+ const CSSTransformMatrix& GetTransform() const;
+ /* This is similar to APZCTreeManager::GetApzcToGeckoTransform but without
+ * the async bits. It's used on the main-thread for transforming coordinates
+ * across a BrowserParent/BrowserChild interface.*/
+ LayerToScreenMatrix4x4 GetTransformToGecko() const;
+ const LayerIntRegion& GetVisibleRegion() const;
+
+ /* Returns the screen coordinate rectangle of remote iframe corresponding to
+ * this node. The rectangle is the result of clipped by ancestor async
+ * scrolling. */
+ ScreenRect GetRemoteDocumentScreenRect() const;
+
+ Maybe<ScrollableLayerGuid::ViewID> GetAsyncZoomContainerId() const;
+
+ /* Debug helpers */
+ void Dump(const char* aPrefix = "") const;
+
+ private:
+ friend class HitTestingTreeNodeAutoLock;
+ // Functions that are private but called from HitTestingTreeNodeAutoLock
+ void Lock(const RecursiveMutexAutoLock& aProofOfTreeLock);
+ void Unlock(const RecursiveMutexAutoLock& aProofOfTreeLock);
+
+ void SetApzcParent(AsyncPanZoomController* aApzc);
+
+ RefPtr<HitTestingTreeNode> mLastChild;
+ RefPtr<HitTestingTreeNode> mPrevSibling;
+ RefPtr<HitTestingTreeNode> mParent;
+
+ RefPtr<AsyncPanZoomController> mApzc;
+ bool mIsPrimaryApzcHolder;
+ int mLockCount;
+
+ LayersId mLayersId;
+
+ // This is only set if WebRender is enabled, and only for HTTNs
+ // where IsScrollThumbNode() returns true. It holds the animation id that we
+ // use to move the thumb node to reflect async scrolling.
+ Maybe<uint64_t> mScrollbarAnimationId;
+
+ // This is set for scrollbar Container and Thumb layers.
+ ScrollbarData mScrollbarData;
+
+ // This is only set if WebRender is enabled. It holds the animation id that
+ // we use to adjust fixed position content for the toolbar.
+ Maybe<uint64_t> mFixedPositionAnimationId;
+
+ ScrollableLayerGuid::ViewID mFixedPosTarget;
+ SideBits mFixedPosSides;
+
+ ScrollableLayerGuid::ViewID mStickyPosTarget;
+ LayerRectAbsolute mStickyScrollRangeOuter;
+ LayerRectAbsolute mStickyScrollRangeInner;
+ // This is only set if WebRender is enabled. It holds the animation id that
+ // we use to adjust sticky position content for the toolbar.
+ Maybe<uint64_t> mStickyPositionAnimationId;
+
+ LayerIntRegion mVisibleRegion;
+
+ /* The size of remote iframe on the corresponding layer coordinate.
+ * It's empty if this node is not for remote iframe. */
+ LayerIntSize mRemoteDocumentSize;
+
+ /* This is the transform from layer L. This does NOT include any async
+ * transforms. */
+ CSSTransformMatrix mTransform;
+
+ /* If the layer is the async zoom container layer then this will hold the id.
+ */
+ Maybe<ScrollableLayerGuid::ViewID> mAsyncZoomContainerId;
+
+ /* Indicates whether or not the event regions on this node need to be
+ * overridden in a certain way. */
+ EventRegionsOverride mOverride;
+};
+
+/**
+ * A class that allows safe usage of a HitTestingTreeNode outside of the APZ
+ * tree lock. In general, this class should be Initialize()'d inside the tree
+ * lock (enforced by the proof-of-lock to Initialize), and then can be returned
+ * to a scope outside the tree lock and used safely. Upon destruction or
+ * Clear() being called, it unlocks the underlying node at which point it can
+ * be recycled or freed.
+ */
+class HitTestingTreeNodeAutoLock final {
+ public:
+ HitTestingTreeNodeAutoLock();
+ ~HitTestingTreeNodeAutoLock();
+ // Make it move-only. Note that the default implementations of the move
+ // constructor and assignment operator are correct: they'll call the
+ // move constructor of mNode, which will null out mNode on the moved-from
+ // object, and Clear() will early-exit when the moved-from object's
+ // destructor is called.
+ HitTestingTreeNodeAutoLock(HitTestingTreeNodeAutoLock&&) = default;
+ HitTestingTreeNodeAutoLock& operator=(HitTestingTreeNodeAutoLock&&) = default;
+
+ void Initialize(const RecursiveMutexAutoLock& aProofOfTreeLock,
+ already_AddRefed<HitTestingTreeNode> aNode,
+ RecursiveMutex& aTreeMutex);
+ void Clear();
+
+ // Convenience operators to simplify the using code.
+ explicit operator bool() const { return !!mNode; }
+ bool operator!() const { return !mNode; }
+ HitTestingTreeNode* operator->() const { return mNode.get(); }
+
+ // Allow getting back a raw pointer to the node, but only inside the scope
+ // of the tree lock. The caller is responsible for ensuring that they do not
+ // use the raw pointer outside that scope.
+ HitTestingTreeNode* Get(
+ mozilla::RecursiveMutexAutoLock& aProofOfTreeLock) const {
+ return mNode.get();
+ }
+
+ private:
+ RefPtr<HitTestingTreeNode> mNode;
+ RecursiveMutex* mTreeMutex;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_HitTestingTreeNode_h
diff --git a/gfx/layers/apz/src/IAPZHitTester.cpp b/gfx/layers/apz/src/IAPZHitTester.cpp
new file mode 100644
index 0000000000..884efe97e8
--- /dev/null
+++ b/gfx/layers/apz/src/IAPZHitTester.cpp
@@ -0,0 +1,78 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "IAPZHitTester.h"
+#include "APZCTreeManager.h"
+#include "AsyncPanZoomController.h"
+
+namespace mozilla {
+namespace layers {
+
+IAPZHitTester::HitTestResult IAPZHitTester::CloneHitTestResult(
+ RecursiveMutexAutoLock& aProofOfTreeLock,
+ const IAPZHitTester::HitTestResult& aHitTestResult) const {
+ HitTestResult result;
+
+ result.mTargetApzc = aHitTestResult.mTargetApzc;
+ result.mHitResult = aHitTestResult.mHitResult;
+ result.mLayersId = aHitTestResult.mLayersId;
+ result.mFixedPosSides = aHitTestResult.mFixedPosSides;
+ result.mHitOverscrollGutter = aHitTestResult.mHitOverscrollGutter;
+
+ RefPtr<HitTestingTreeNode> scrollbarNode =
+ aHitTestResult.mScrollbarNode.Get(aProofOfTreeLock);
+ RefPtr<HitTestingTreeNode> node = aHitTestResult.mNode.Get(aProofOfTreeLock);
+
+ if (aHitTestResult.mScrollbarNode) {
+ InitializeHitTestingTreeNodeAutoLock(result.mScrollbarNode,
+ aProofOfTreeLock, scrollbarNode);
+ }
+ if (aHitTestResult.mNode) {
+ InitializeHitTestingTreeNodeAutoLock(result.mNode, aProofOfTreeLock, node);
+ }
+
+ return result;
+}
+
+LayersId IAPZHitTester::GetRootLayersId() const {
+ return mTreeManager->mRootLayersId;
+}
+
+HitTestingTreeNode* IAPZHitTester::GetRootNode() const {
+ mTreeManager->mTreeLock.AssertCurrentThreadIn();
+ return mTreeManager->mRootNode;
+}
+
+HitTestingTreeNode* IAPZHitTester::FindRootNodeForLayersId(
+ LayersId aLayersId) const {
+ return mTreeManager->FindRootNodeForLayersId(aLayersId);
+}
+
+AsyncPanZoomController* IAPZHitTester::FindRootApzcForLayersId(
+ LayersId aLayersId) const {
+ HitTestingTreeNode* resultNode = FindRootNodeForLayersId(aLayersId);
+ return resultNode ? resultNode->GetApzc() : nullptr;
+}
+
+already_AddRefed<HitTestingTreeNode> IAPZHitTester::GetTargetNode(
+ const ScrollableLayerGuid& aGuid,
+ ScrollableLayerGuid::Comparator aComparator) {
+ // Acquire the tree lock so that derived classes can call this from
+ // methods other than GetAPZCAtPoint().
+ RecursiveMutexAutoLock lock(mTreeManager->mTreeLock);
+ return mTreeManager->GetTargetNode(aGuid, aComparator);
+}
+
+void IAPZHitTester::InitializeHitTestingTreeNodeAutoLock(
+ HitTestingTreeNodeAutoLock& aAutoLock,
+ const RecursiveMutexAutoLock& aProofOfTreeLock,
+ RefPtr<HitTestingTreeNode>& aNode) const {
+ aAutoLock.Initialize(aProofOfTreeLock, aNode.forget(),
+ mTreeManager->mTreeLock);
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/IAPZHitTester.h b/gfx/layers/apz/src/IAPZHitTester.h
new file mode 100644
index 0000000000..8822b40ea8
--- /dev/null
+++ b/gfx/layers/apz/src/IAPZHitTester.h
@@ -0,0 +1,91 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_IAPZHitTester_h
+#define mozilla_layers_IAPZHitTester_h
+
+#include "HitTestingTreeNode.h" // for HitTestingTreeNodeAutoLock
+#include "mozilla/RefPtr.h"
+#include "mozilla/gfx/CompositorHitTestInfo.h"
+#include "mozilla/layers/LayersTypes.h"
+
+namespace mozilla {
+namespace layers {
+
+class AsyncPanZoomController;
+class APZCTreeManager;
+
+class IAPZHitTester {
+ public:
+ virtual ~IAPZHitTester() = default;
+
+ // Not a constructor because we want external code to be able to pass a hit
+ // tester to the APZCTreeManager constructor, which will then initialize it.
+ void Initialize(APZCTreeManager* aTreeManager) {
+ mTreeManager = aTreeManager;
+ }
+
+ // Represents the results of an APZ hit test.
+ struct HitTestResult {
+ // The APZC targeted by the hit test.
+ RefPtr<AsyncPanZoomController> mTargetApzc;
+ // The applicable hit test flags.
+ gfx::CompositorHitTestInfo mHitResult;
+ // The layers id of the content that was hit.
+ // This effectively identifiers the process that was hit for
+ // Fission purposes.
+ LayersId mLayersId;
+ // If a scrollbar was hit, this will be populated with the
+ // scrollbar node. The AutoLock allows accessing the scrollbar
+ // node without having to hold the tree lock.
+ HitTestingTreeNodeAutoLock mScrollbarNode;
+ // If content that is fixed to the root-content APZC was hit,
+ // the sides of the viewport to which the content is fixed.
+ SideBits mFixedPosSides = SideBits::eNone;
+ // If a fixed/sticky position element was hit, this will be populated with
+ // the hit-testing tree node. The AutoLock allows accessing the node
+ // without having to hold the tree lock.
+ HitTestingTreeNodeAutoLock mNode;
+ // This is set to true If mTargetApzc is overscrolled and the
+ // event targeted the gap space ("gutter") created by the overscroll.
+ bool mHitOverscrollGutter = false;
+
+ HitTestResult() = default;
+ // Make it move-only.
+ HitTestResult(HitTestResult&&) = default;
+ HitTestResult& operator=(HitTestResult&&) = default;
+ };
+
+ virtual HitTestResult GetAPZCAtPoint(
+ const ScreenPoint& aHitTestPoint,
+ const RecursiveMutexAutoLock& aProofOfTreeLock) = 0;
+
+ HitTestResult CloneHitTestResult(RecursiveMutexAutoLock& aProofOfTreeLock,
+ const HitTestResult& aHitTestResult) const;
+
+ protected:
+ APZCTreeManager* mTreeManager = nullptr;
+
+ // We are a friend of APZCTreeManager but our derived classes
+ // are not. Wrap a few private members of APZCTreeManager for
+ // use by derived classes.
+ LayersId GetRootLayersId() const;
+ HitTestingTreeNode* GetRootNode() const;
+ HitTestingTreeNode* FindRootNodeForLayersId(LayersId aLayersId) const;
+ AsyncPanZoomController* FindRootApzcForLayersId(LayersId aLayersId) const;
+ already_AddRefed<HitTestingTreeNode> GetTargetNode(
+ const ScrollableLayerGuid& aGuid,
+ ScrollableLayerGuid::Comparator aComparator);
+ void InitializeHitTestingTreeNodeAutoLock(
+ HitTestingTreeNodeAutoLock& aAutoLock,
+ const RecursiveMutexAutoLock& aProofOfTreeLock,
+ RefPtr<HitTestingTreeNode>& aNode) const;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_IAPZHitTester_h
diff --git a/gfx/layers/apz/src/InputBlockState.cpp b/gfx/layers/apz/src/InputBlockState.cpp
new file mode 100644
index 0000000000..b492af0215
--- /dev/null
+++ b/gfx/layers/apz/src/InputBlockState.cpp
@@ -0,0 +1,840 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "InputBlockState.h"
+
+#include "APZUtils.h"
+#include "AsyncPanZoomController.h" // for AsyncPanZoomController
+
+#include "mozilla/MouseEvents.h"
+#include "mozilla/StaticPrefs_apz.h"
+#include "mozilla/StaticPrefs_layout.h"
+#include "mozilla/StaticPrefs_mousewheel.h"
+#include "mozilla/StaticPrefs_test.h"
+#include "mozilla/Telemetry.h" // for Telemetry
+#include "mozilla/ToString.h"
+#include "mozilla/layers/IAPZCTreeManager.h" // for AllowedTouchBehavior
+#include "OverscrollHandoffState.h"
+#include "QueuedInput.h"
+
+static mozilla::LazyLogModule sApzIbsLog("apz.inputstate");
+#define TBS_LOG(...) MOZ_LOG(sApzIbsLog, LogLevel::Debug, (__VA_ARGS__))
+
+namespace mozilla {
+namespace layers {
+
+static uint64_t sBlockCounter = InputBlockState::NO_BLOCK_ID + 1;
+
+InputBlockState::InputBlockState(
+ const RefPtr<AsyncPanZoomController>& aTargetApzc,
+ TargetConfirmationFlags aFlags)
+ : mTargetApzc(aTargetApzc),
+ mRequiresTargetConfirmation(aFlags.mRequiresTargetConfirmation),
+ mBlockId(sBlockCounter++),
+ mTransformToApzc(aTargetApzc->GetTransformToThis()) {
+ // We should never be constructed with a nullptr target.
+ MOZ_ASSERT(mTargetApzc);
+ mOverscrollHandoffChain = mTargetApzc->BuildOverscrollHandoffChain();
+ // If a new block starts on a scrollthumb and we have APZ scrollbar
+ // dragging enabled, defer confirmation until we get the drag metrics
+ // for the thumb.
+ bool startingDrag = StaticPrefs::apz_drag_enabled() && aFlags.mHitScrollThumb;
+ mTargetConfirmed = aFlags.mTargetConfirmed && !startingDrag
+ ? TargetConfirmationState::eConfirmed
+ : TargetConfirmationState::eUnconfirmed;
+}
+
+bool InputBlockState::SetConfirmedTargetApzc(
+ const RefPtr<AsyncPanZoomController>& aTargetApzc,
+ TargetConfirmationState aState, InputData* aFirstInput,
+ bool aForScrollbarDrag) {
+ MOZ_ASSERT(aState == TargetConfirmationState::eConfirmed ||
+ aState == TargetConfirmationState::eTimedOut);
+
+ if (mTargetConfirmed == TargetConfirmationState::eTimedOut &&
+ aState == TargetConfirmationState::eConfirmed) {
+ // The main thread finally responded. We had already timed out the
+ // confirmation, but we want to update the state internally so that we
+ // can record the time for telemetry purposes.
+ mTargetConfirmed = TargetConfirmationState::eTimedOutAndMainThreadResponded;
+ }
+ // Sometimes, bugs in compositor hit testing can lead to APZ confirming
+ // a different target than the main thread. If this happens for a drag
+ // block created for a scrollbar drag, the consequences can be fairly
+ // user-unfriendly, such as the scrollbar not being draggable at all,
+ // or it scrolling the contents of the wrong scrollframe. In debug
+ // builds, we assert in this situation, so that the
+ // underlying compositor hit testing bug can be fixed. In release builds,
+ // however, we just silently accept the main thread's confirmed target,
+ // which will produce the expected behaviour (apart from drag events
+ // received so far being dropped).
+ if (AsDragBlock() && aForScrollbarDrag &&
+ mTargetConfirmed == TargetConfirmationState::eConfirmed &&
+ aState == TargetConfirmationState::eConfirmed && mTargetApzc &&
+ aTargetApzc && mTargetApzc->GetGuid() != aTargetApzc->GetGuid()) {
+ MOZ_ASSERT(false,
+ "APZ and main thread confirmed scrollbar drag block with "
+ "different targets");
+ UpdateTargetApzc(aTargetApzc);
+ return true;
+ }
+
+ if (mTargetConfirmed != TargetConfirmationState::eUnconfirmed) {
+ return false;
+ }
+ mTargetConfirmed = aState;
+
+ TBS_LOG("%p got confirmed target APZC %p\n", this, mTargetApzc.get());
+ if (mTargetApzc == aTargetApzc) {
+ // The confirmed target is the same as the tentative one, so we're done.
+ return true;
+ }
+
+ TBS_LOG("%p replacing unconfirmed target %p with real target %p\n", this,
+ mTargetApzc.get(), aTargetApzc.get());
+
+ UpdateTargetApzc(aTargetApzc);
+ return true;
+}
+
+void InputBlockState::UpdateTargetApzc(
+ const RefPtr<AsyncPanZoomController>& aTargetApzc) {
+ if (mTargetApzc == aTargetApzc) {
+ MOZ_ASSERT_UNREACHABLE(
+ "The new target APZC should be different from the old one");
+ return;
+ }
+
+ if (mTargetApzc) {
+ // Restore overscroll state on the previous target APZC and ancestor APZCs
+ // in the scroll handoff chain other than the new one.
+ mTargetApzc->SnapBackIfOverscrolled();
+
+ uint32_t i = mOverscrollHandoffChain->IndexOf(mTargetApzc) + 1;
+ for (; i < mOverscrollHandoffChain->Length(); i++) {
+ AsyncPanZoomController* apzc = mOverscrollHandoffChain->GetApzcAtIndex(i);
+ if (apzc != aTargetApzc) {
+ MOZ_ASSERT(!apzc->IsOverscrolled() ||
+ apzc->IsOverscrollAnimationRunning());
+ apzc->SnapBackIfOverscrolled();
+ }
+ }
+ }
+
+ // note that aTargetApzc MAY be null here.
+ mTargetApzc = aTargetApzc;
+ mTransformToApzc = aTargetApzc ? aTargetApzc->GetTransformToThis()
+ : ScreenToParentLayerMatrix4x4();
+ mOverscrollHandoffChain =
+ (mTargetApzc ? mTargetApzc->BuildOverscrollHandoffChain() : nullptr);
+}
+
+const RefPtr<AsyncPanZoomController>& InputBlockState::GetTargetApzc() const {
+ return mTargetApzc;
+}
+
+const RefPtr<const OverscrollHandoffChain>&
+InputBlockState::GetOverscrollHandoffChain() const {
+ return mOverscrollHandoffChain;
+}
+
+uint64_t InputBlockState::GetBlockId() const { return mBlockId; }
+
+bool InputBlockState::IsTargetConfirmed() const {
+ return mTargetConfirmed != TargetConfirmationState::eUnconfirmed;
+}
+
+bool InputBlockState::HasReceivedRealConfirmedTarget() const {
+ return mTargetConfirmed == TargetConfirmationState::eConfirmed ||
+ mTargetConfirmed ==
+ TargetConfirmationState::eTimedOutAndMainThreadResponded;
+}
+
+bool InputBlockState::ShouldDropEvents() const {
+ return mRequiresTargetConfirmation &&
+ (mTargetConfirmed != TargetConfirmationState::eConfirmed);
+}
+
+bool InputBlockState::IsDownchainOf(AsyncPanZoomController* aA,
+ AsyncPanZoomController* aB) const {
+ if (aA == aB) {
+ return true;
+ }
+
+ bool seenA = false;
+ for (size_t i = 0; i < mOverscrollHandoffChain->Length(); ++i) {
+ AsyncPanZoomController* apzc = mOverscrollHandoffChain->GetApzcAtIndex(i);
+ if (apzc == aB) {
+ return seenA;
+ }
+ if (apzc == aA) {
+ seenA = true;
+ }
+ }
+ return false;
+}
+
+void InputBlockState::SetScrolledApzc(AsyncPanZoomController* aApzc) {
+ // An input block should only have one scrolled APZC.
+ MOZ_ASSERT(!mScrolledApzc || (StaticPrefs::apz_allow_immediate_handoff()
+ ? IsDownchainOf(mScrolledApzc, aApzc)
+ : mScrolledApzc == aApzc));
+
+ mScrolledApzc = aApzc;
+}
+
+AsyncPanZoomController* InputBlockState::GetScrolledApzc() const {
+ return mScrolledApzc;
+}
+
+bool InputBlockState::IsDownchainOfScrolledApzc(
+ AsyncPanZoomController* aApzc) const {
+ MOZ_ASSERT(aApzc && mScrolledApzc);
+
+ return IsDownchainOf(mScrolledApzc, aApzc);
+}
+
+void InputBlockState::DispatchEvent(const InputData& aEvent) const {
+ GetTargetApzc()->HandleInputEvent(aEvent, mTransformToApzc);
+}
+
+CancelableBlockState::CancelableBlockState(
+ const RefPtr<AsyncPanZoomController>& aTargetApzc,
+ TargetConfirmationFlags aFlags)
+ : InputBlockState(aTargetApzc, aFlags),
+ mPreventDefault(false),
+ mContentResponded(false),
+ mContentResponseTimerExpired(false) {}
+
+bool CancelableBlockState::SetContentResponse(bool aPreventDefault) {
+ if (mContentResponded) {
+ return false;
+ }
+ TBS_LOG("%p got content response %d with timer expired %d\n", this,
+ aPreventDefault, mContentResponseTimerExpired);
+ mPreventDefault = aPreventDefault;
+ mContentResponded = true;
+ return true;
+}
+
+bool CancelableBlockState::TimeoutContentResponse() {
+ if (mContentResponseTimerExpired) {
+ return false;
+ }
+ TBS_LOG("%p got content timer expired with response received %d\n", this,
+ mContentResponded);
+ if (!mContentResponded) {
+ mPreventDefault = false;
+ }
+ mContentResponseTimerExpired = true;
+ return true;
+}
+
+bool CancelableBlockState::IsContentResponseTimerExpired() const {
+ return mContentResponseTimerExpired;
+}
+
+bool CancelableBlockState::IsDefaultPrevented() const {
+ MOZ_ASSERT(mContentResponded || mContentResponseTimerExpired);
+ return mPreventDefault;
+}
+
+bool CancelableBlockState::IsReadyForHandling() const {
+ if (!IsTargetConfirmed()) {
+ return false;
+ }
+ return mContentResponded || mContentResponseTimerExpired;
+}
+
+bool CancelableBlockState::ShouldDropEvents() const {
+ return InputBlockState::ShouldDropEvents() || IsDefaultPrevented();
+}
+
+DragBlockState::DragBlockState(
+ const RefPtr<AsyncPanZoomController>& aTargetApzc,
+ TargetConfirmationFlags aFlags, const MouseInput& aInitialEvent)
+ : CancelableBlockState(aTargetApzc, aFlags), mReceivedMouseUp(false) {}
+
+bool DragBlockState::HasReceivedMouseUp() { return mReceivedMouseUp; }
+
+void DragBlockState::MarkMouseUpReceived() { mReceivedMouseUp = true; }
+
+void DragBlockState::SetInitialThumbPos(OuterCSSCoord aThumbPos) {
+ mInitialThumbPos = aThumbPos;
+}
+
+void DragBlockState::SetDragMetrics(const AsyncDragMetrics& aDragMetrics) {
+ mDragMetrics = aDragMetrics;
+}
+
+void DragBlockState::DispatchEvent(const InputData& aEvent) const {
+ MouseInput mouseInput = aEvent.AsMouseInput();
+ if (!mouseInput.TransformToLocal(mTransformToApzc)) {
+ return;
+ }
+
+ GetTargetApzc()->HandleDragEvent(mouseInput, mDragMetrics, mInitialThumbPos);
+}
+
+bool DragBlockState::MustStayActive() { return !mReceivedMouseUp; }
+
+const char* DragBlockState::Type() { return "drag"; }
+// This is used to track the current wheel transaction.
+static uint64_t sLastWheelBlockId = InputBlockState::NO_BLOCK_ID;
+
+WheelBlockState::WheelBlockState(
+ const RefPtr<AsyncPanZoomController>& aTargetApzc,
+ TargetConfirmationFlags aFlags, const ScrollWheelInput& aInitialEvent)
+ : CancelableBlockState(aTargetApzc, aFlags),
+ mScrollSeriesCounter(0),
+ mTransactionEnded(false) {
+ sLastWheelBlockId = GetBlockId();
+
+ if (aFlags.mTargetConfirmed) {
+ // Find the nearest APZC in the overscroll handoff chain that is scrollable.
+ // If we get a content confirmation later that the apzc is different, then
+ // content should have found a scrollable apzc, so we don't need to handle
+ // that case.
+ RefPtr<AsyncPanZoomController> apzc =
+ mOverscrollHandoffChain->FindFirstScrollable(aInitialEvent,
+ &mAllowedScrollDirections);
+
+ if (apzc) {
+ if (apzc != GetTargetApzc()) {
+ UpdateTargetApzc(apzc);
+ }
+ } else if (!mOverscrollHandoffChain->CanBePanned(
+ mOverscrollHandoffChain->GetApzcAtIndex(0))) {
+ // If there's absolutely nothing scrollable start a transaction and mark
+ // this as such to we know to store our EventTime.
+ mIsScrollable = false;
+ } else {
+ // Scrollable, but not in this direction.
+ EndTransaction();
+ }
+ }
+}
+
+bool WheelBlockState::SetContentResponse(bool aPreventDefault) {
+ if (aPreventDefault) {
+ EndTransaction();
+ }
+ return CancelableBlockState::SetContentResponse(aPreventDefault);
+}
+
+bool WheelBlockState::SetConfirmedTargetApzc(
+ const RefPtr<AsyncPanZoomController>& aTargetApzc,
+ TargetConfirmationState aState, InputData* aFirstInput,
+ bool aForScrollbarDrag) {
+ // The APZC that we find via APZCCallbackHelpers may not be the same APZC
+ // ESM or OverscrollHandoff would have computed. Make sure we get the right
+ // one by looking for the first apzc the next pending event can scroll.
+ RefPtr<AsyncPanZoomController> apzc = aTargetApzc;
+ if (apzc && aFirstInput) {
+ apzc = apzc->BuildOverscrollHandoffChain()->FindFirstScrollable(
+ *aFirstInput, &mAllowedScrollDirections);
+ }
+
+ InputBlockState::SetConfirmedTargetApzc(apzc, aState, aFirstInput,
+ aForScrollbarDrag);
+ return true;
+}
+
+void WheelBlockState::Update(ScrollWheelInput& aEvent) {
+ // We might not be in a transaction if the block never started in a
+ // transaction - for example, if nothing was scrollable.
+ if (!InTransaction()) {
+ return;
+ }
+
+ // The current "scroll series" is a like a sub-transaction. It has a separate
+ // timeout of 80ms. Since we need to compute wheel deltas at different phases
+ // of a transaction (for example, when it is updated, and later when the
+ // event action is taken), we affix the scroll series counter to the event.
+ // This makes GetScrollWheelDelta() consistent.
+ if (!mLastEventTime.IsNull() &&
+ (aEvent.mTimeStamp - mLastEventTime).ToMilliseconds() >
+ StaticPrefs::mousewheel_scroll_series_timeout()) {
+ mScrollSeriesCounter = 0;
+ }
+ aEvent.mScrollSeriesNumber = ++mScrollSeriesCounter;
+
+ // If we can't scroll in the direction of the wheel event, we don't update
+ // the last move time. This allows us to timeout a transaction even if the
+ // mouse isn't moving.
+ //
+ // We skip this check if the target is not yet confirmed, so that when it is
+ // confirmed, we don't timeout the transaction.
+ RefPtr<AsyncPanZoomController> apzc = GetTargetApzc();
+ if (mIsScrollable && IsTargetConfirmed() && !apzc->CanScroll(aEvent)) {
+ return;
+ }
+
+ // Update the time of the last known good event, and reset the mouse move
+ // time to null. This will reset the delays on both the general transaction
+ // timeout and the mouse-move-in-frame timeout.
+ mLastEventTime = aEvent.mTimeStamp;
+ mLastMouseMove = TimeStamp();
+}
+
+bool WheelBlockState::MustStayActive() { return !mTransactionEnded; }
+
+const char* WheelBlockState::Type() { return "scroll wheel"; }
+
+bool WheelBlockState::ShouldAcceptNewEvent() const {
+ if (!InTransaction()) {
+ // If we're not in a transaction, start a new one.
+ return false;
+ }
+
+ RefPtr<AsyncPanZoomController> apzc = GetTargetApzc();
+ if (apzc->IsDestroyed()) {
+ return false;
+ }
+
+ return true;
+}
+
+bool WheelBlockState::MaybeTimeout(const ScrollWheelInput& aEvent) {
+ MOZ_ASSERT(InTransaction());
+
+ if (MaybeTimeout(aEvent.mTimeStamp)) {
+ return true;
+ }
+
+ if (!mLastMouseMove.IsNull()) {
+ // If there's a recent mouse movement, we can time out the transaction
+ // early.
+ TimeDuration duration = TimeStamp::Now() - mLastMouseMove;
+ if (duration.ToMilliseconds() >=
+ StaticPrefs::mousewheel_transaction_ignoremovedelay()) {
+ TBS_LOG("%p wheel transaction timed out after mouse move\n", this);
+ EndTransaction();
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool WheelBlockState::MaybeTimeout(const TimeStamp& aTimeStamp) {
+ MOZ_ASSERT(InTransaction());
+
+ // End the transaction if the event occurred > 1.5s after the most recently
+ // seen wheel event.
+ TimeDuration duration = aTimeStamp - mLastEventTime;
+ if (duration.ToMilliseconds() <
+ StaticPrefs::mousewheel_transaction_timeout()) {
+ return false;
+ }
+
+ TBS_LOG("%p wheel transaction timed out\n", this);
+
+ if (StaticPrefs::test_mousescroll()) {
+ RefPtr<AsyncPanZoomController> apzc = GetTargetApzc();
+ apzc->NotifyMozMouseScrollEvent(u"MozMouseScrollTransactionTimeout"_ns);
+ }
+
+ EndTransaction();
+ return true;
+}
+
+void WheelBlockState::OnMouseMove(
+ const ScreenIntPoint& aPoint,
+ const Maybe<ScrollableLayerGuid>& aTargetGuid) {
+ MOZ_ASSERT(InTransaction());
+
+ if (!GetTargetApzc()->Contains(aPoint) ||
+ // If the mouse moved over to a different APZC, `mIsScrollable`
+ // may no longer be false and needs to be recomputed.
+ (!mIsScrollable && aTargetGuid.isSome() &&
+ aTargetGuid.value() != GetTargetApzc()->GetGuid())) {
+ EndTransaction();
+ return;
+ }
+
+ if (mLastMouseMove.IsNull()) {
+ // If the cursor is moving inside the frame, and it is more than the
+ // ignoremovedelay time since the last scroll operation, we record
+ // this as the most recent mouse movement.
+ TimeStamp now = TimeStamp::Now();
+ TimeDuration duration = now - mLastEventTime;
+ if (duration.ToMilliseconds() >=
+ StaticPrefs::mousewheel_transaction_ignoremovedelay()) {
+ mLastMouseMove = now;
+ }
+ }
+}
+
+void WheelBlockState::UpdateTargetApzc(
+ const RefPtr<AsyncPanZoomController>& aTargetApzc) {
+ InputBlockState::UpdateTargetApzc(aTargetApzc);
+
+ // If we found there was no target apzc, then we end the transaction.
+ if (!GetTargetApzc()) {
+ EndTransaction();
+ }
+}
+
+bool WheelBlockState::InTransaction() const {
+ // We consider a wheel block to be in a transaction if it has a confirmed
+ // target and is the most recent wheel input block to be created.
+ if (GetBlockId() != sLastWheelBlockId) {
+ return false;
+ }
+
+ if (mTransactionEnded) {
+ return false;
+ }
+
+ MOZ_ASSERT(GetTargetApzc());
+ return true;
+}
+
+bool WheelBlockState::AllowScrollHandoff() const {
+ // If we're in a wheel transaction, we do not allow overscroll handoff until
+ // a new event ends the wheel transaction.
+ return !IsTargetConfirmed() || !InTransaction();
+}
+
+void WheelBlockState::EndTransaction() {
+ TBS_LOG("%p ending wheel transaction\n", this);
+ mTransactionEnded = true;
+}
+
+PanGestureBlockState::PanGestureBlockState(
+ const RefPtr<AsyncPanZoomController>& aTargetApzc,
+ TargetConfirmationFlags aFlags, const PanGestureInput& aInitialEvent)
+ : CancelableBlockState(aTargetApzc, aFlags),
+ mInterrupted(false),
+ mWaitingForContentResponse(false),
+ mWaitingForBrowserGestureResponse(false),
+ mStartedBrowserGesture(false) {
+ if (aFlags.mTargetConfirmed) {
+ // Find the nearest APZC in the overscroll handoff chain that is scrollable.
+ // If we get a content confirmation later that the apzc is different, then
+ // content should have found a scrollable apzc, so we don't need to handle
+ // that case.
+ RefPtr<AsyncPanZoomController> apzc =
+ mOverscrollHandoffChain->FindFirstScrollable(aInitialEvent,
+ &mAllowedScrollDirections);
+
+ if (apzc && apzc != GetTargetApzc()) {
+ UpdateTargetApzc(apzc);
+ }
+ }
+}
+
+bool PanGestureBlockState::SetConfirmedTargetApzc(
+ const RefPtr<AsyncPanZoomController>& aTargetApzc,
+ TargetConfirmationState aState, InputData* aFirstInput,
+ bool aForScrollbarDrag) {
+ // The APZC that we find via APZCCallbackHelpers may not be the same APZC
+ // ESM or OverscrollHandoff would have computed. Make sure we get the right
+ // one by looking for the first apzc the next pending event can scroll.
+ RefPtr<AsyncPanZoomController> apzc = aTargetApzc;
+ if (apzc && aFirstInput) {
+ RefPtr<AsyncPanZoomController> scrollableApzc =
+ apzc->BuildOverscrollHandoffChain()->FindFirstScrollable(
+ *aFirstInput, &mAllowedScrollDirections);
+ if (scrollableApzc) {
+ apzc = scrollableApzc;
+ }
+ }
+
+ InputBlockState::SetConfirmedTargetApzc(apzc, aState, aFirstInput,
+ aForScrollbarDrag);
+ return true;
+}
+
+bool PanGestureBlockState::MustStayActive() { return !mInterrupted; }
+
+const char* PanGestureBlockState::Type() { return "pan gesture"; }
+
+bool PanGestureBlockState::SetContentResponse(bool aPreventDefault) {
+ if (aPreventDefault) {
+ TBS_LOG("%p setting interrupted flag\n", this);
+ mInterrupted = true;
+ }
+ bool stateChanged = CancelableBlockState::SetContentResponse(aPreventDefault);
+ if (mWaitingForContentResponse) {
+ mWaitingForContentResponse = false;
+ stateChanged = true;
+ }
+ return stateChanged;
+}
+
+bool PanGestureBlockState::IsReadyForHandling() const {
+ if (!CancelableBlockState::IsReadyForHandling()) {
+ return false;
+ }
+ return !mWaitingForBrowserGestureResponse &&
+ (!mWaitingForContentResponse || IsContentResponseTimerExpired());
+}
+
+bool PanGestureBlockState::ShouldDropEvents() const {
+ return CancelableBlockState::ShouldDropEvents() || mStartedBrowserGesture;
+}
+
+bool PanGestureBlockState::TimeoutContentResponse() {
+ // Reset mWaitingForBrowserGestureResponse here so that we will not wait for
+ // the response forever.
+ mWaitingForBrowserGestureResponse = false;
+ return CancelableBlockState::TimeoutContentResponse();
+}
+
+bool PanGestureBlockState::AllowScrollHandoff() const { return false; }
+
+void PanGestureBlockState::SetNeedsToWaitForContentResponse(
+ bool aWaitForContentResponse) {
+ mWaitingForContentResponse = aWaitForContentResponse;
+}
+
+void PanGestureBlockState::SetNeedsToWaitForBrowserGestureResponse(
+ bool aWaitForBrowserGestureResponse) {
+ mWaitingForBrowserGestureResponse = aWaitForBrowserGestureResponse;
+}
+
+void PanGestureBlockState::SetBrowserGestureResponse(
+ BrowserGestureResponse aResponse) {
+ mWaitingForBrowserGestureResponse = false;
+ mStartedBrowserGesture = bool(aResponse);
+}
+
+PinchGestureBlockState::PinchGestureBlockState(
+ const RefPtr<AsyncPanZoomController>& aTargetApzc,
+ TargetConfirmationFlags aFlags)
+ : CancelableBlockState(aTargetApzc, aFlags),
+ mInterrupted(false),
+ mWaitingForContentResponse(false) {}
+
+bool PinchGestureBlockState::MustStayActive() { return true; }
+
+const char* PinchGestureBlockState::Type() { return "pinch gesture"; }
+
+bool PinchGestureBlockState::SetContentResponse(bool aPreventDefault) {
+ if (aPreventDefault) {
+ TBS_LOG("%p setting interrupted flag\n", this);
+ mInterrupted = true;
+ }
+ bool stateChanged = CancelableBlockState::SetContentResponse(aPreventDefault);
+ if (mWaitingForContentResponse) {
+ mWaitingForContentResponse = false;
+ stateChanged = true;
+ }
+ return stateChanged;
+}
+
+bool PinchGestureBlockState::IsReadyForHandling() const {
+ if (!CancelableBlockState::IsReadyForHandling()) {
+ return false;
+ }
+ return !mWaitingForContentResponse || IsContentResponseTimerExpired();
+}
+
+void PinchGestureBlockState::SetNeedsToWaitForContentResponse(
+ bool aWaitForContentResponse) {
+ mWaitingForContentResponse = aWaitForContentResponse;
+}
+
+TouchBlockState::TouchBlockState(
+ const RefPtr<AsyncPanZoomController>& aTargetApzc,
+ TargetConfirmationFlags aFlags, TouchCounter& aCounter)
+ : CancelableBlockState(aTargetApzc, aFlags),
+ mAllowedTouchBehaviorSet(false),
+ mDuringFastFling(false),
+ mSingleTapOccurred(false),
+ mInSlop(false),
+ mTouchCounter(aCounter),
+ mStartTime(GetTargetApzc()->GetFrameTime().Time()) {
+ TBS_LOG("Creating %p\n", this);
+}
+
+bool TouchBlockState::SetAllowedTouchBehaviors(
+ const nsTArray<TouchBehaviorFlags>& aBehaviors) {
+ if (mAllowedTouchBehaviorSet) {
+ return false;
+ }
+ TBS_LOG("%p got allowed touch behaviours for %zu points\n", this,
+ aBehaviors.Length());
+ mAllowedTouchBehaviors.AppendElements(aBehaviors);
+ mAllowedTouchBehaviorSet = true;
+ return true;
+}
+
+bool TouchBlockState::GetAllowedTouchBehaviors(
+ nsTArray<TouchBehaviorFlags>& aOutBehaviors) const {
+ if (!mAllowedTouchBehaviorSet) {
+ return false;
+ }
+ aOutBehaviors.AppendElements(mAllowedTouchBehaviors);
+ return true;
+}
+
+bool TouchBlockState::HasAllowedTouchBehaviors() const {
+ return mAllowedTouchBehaviorSet;
+}
+
+void TouchBlockState::CopyPropertiesFrom(const TouchBlockState& aOther) {
+ TBS_LOG("%p copying properties from %p\n", this, &aOther);
+ MOZ_ASSERT(aOther.mAllowedTouchBehaviorSet ||
+ aOther.IsContentResponseTimerExpired());
+ SetAllowedTouchBehaviors(aOther.mAllowedTouchBehaviors);
+ mTransformToApzc = aOther.mTransformToApzc;
+}
+
+bool TouchBlockState::IsReadyForHandling() const {
+ if (!CancelableBlockState::IsReadyForHandling()) {
+ return false;
+ }
+
+ return mAllowedTouchBehaviorSet || IsContentResponseTimerExpired();
+}
+
+void TouchBlockState::SetDuringFastFling() {
+ TBS_LOG("%p setting fast-motion flag\n", this);
+ mDuringFastFling = true;
+}
+
+bool TouchBlockState::IsDuringFastFling() const { return mDuringFastFling; }
+
+void TouchBlockState::SetSingleTapOccurred() {
+ TBS_LOG("%p setting single-tap-occurred flag\n", this);
+ mSingleTapOccurred = true;
+}
+
+bool TouchBlockState::SingleTapOccurred() const { return mSingleTapOccurred; }
+
+bool TouchBlockState::MustStayActive() { return true; }
+
+const char* TouchBlockState::Type() { return "touch"; }
+
+TimeDuration TouchBlockState::GetTimeSinceBlockStart() const {
+ return GetTargetApzc()->GetFrameTime().Time() - mStartTime;
+}
+
+void TouchBlockState::DispatchEvent(const InputData& aEvent) const {
+ MOZ_ASSERT(aEvent.mInputType == MULTITOUCH_INPUT);
+ mTouchCounter.Update(aEvent.AsMultiTouchInput());
+ CancelableBlockState::DispatchEvent(aEvent);
+}
+
+bool TouchBlockState::TouchActionAllowsPinchZoom() const {
+ // Pointer events specification requires that all touch points allow zoom.
+ for (auto& behavior : mAllowedTouchBehaviors) {
+ if (!(behavior & AllowedTouchBehavior::PINCH_ZOOM)) {
+ return false;
+ }
+ }
+ return true;
+}
+
+bool TouchBlockState::TouchActionAllowsDoubleTapZoom() const {
+ for (auto& behavior : mAllowedTouchBehaviors) {
+ if (!(behavior & AllowedTouchBehavior::ANIMATING_ZOOM)) {
+ return false;
+ }
+ }
+ return true;
+}
+
+bool TouchBlockState::TouchActionAllowsPanningX() const {
+ if (mAllowedTouchBehaviors.IsEmpty()) {
+ // Default to allowed
+ return true;
+ }
+ TouchBehaviorFlags flags = mAllowedTouchBehaviors[0];
+ return (flags & AllowedTouchBehavior::HORIZONTAL_PAN);
+}
+
+bool TouchBlockState::TouchActionAllowsPanningY() const {
+ if (mAllowedTouchBehaviors.IsEmpty()) {
+ // Default to allowed
+ return true;
+ }
+ TouchBehaviorFlags flags = mAllowedTouchBehaviors[0];
+ return (flags & AllowedTouchBehavior::VERTICAL_PAN);
+}
+
+bool TouchBlockState::TouchActionAllowsPanningXY() const {
+ if (mAllowedTouchBehaviors.IsEmpty()) {
+ // Default to allowed
+ return true;
+ }
+ TouchBehaviorFlags flags = mAllowedTouchBehaviors[0];
+ return (flags & AllowedTouchBehavior::HORIZONTAL_PAN) &&
+ (flags & AllowedTouchBehavior::VERTICAL_PAN);
+}
+
+bool TouchBlockState::UpdateSlopState(const MultiTouchInput& aInput,
+ bool aApzcCanConsumeEvents) {
+ if (aInput.mType == MultiTouchInput::MULTITOUCH_START) {
+ // this is by definition the first event in this block. If it's the first
+ // touch, then we enter a slop state.
+ mInSlop = (aInput.mTouches.Length() == 1);
+ if (mInSlop) {
+ mSlopOrigin = aInput.mTouches[0].mScreenPoint;
+ TBS_LOG("%p entering slop with origin %s\n", this,
+ ToString(mSlopOrigin).c_str());
+ }
+ return false;
+ }
+ if (mInSlop) {
+ ScreenCoord threshold = 0;
+ // If the target was confirmed to null then the threshold doesn't
+ // matter anyway since the events will never be processed.
+ if (const RefPtr<AsyncPanZoomController>& apzc = GetTargetApzc()) {
+ threshold = aApzcCanConsumeEvents ? apzc->GetTouchStartTolerance()
+ : apzc->GetTouchMoveTolerance();
+ }
+ bool stayInSlop =
+ (aInput.mType == MultiTouchInput::MULTITOUCH_MOVE) &&
+ (aInput.mTouches.Length() == 1) &&
+ ((aInput.mTouches[0].mScreenPoint - mSlopOrigin).Length() < threshold);
+ if (!stayInSlop) {
+ // we're out of the slop zone, and will stay out for the remainder of
+ // this block
+ TBS_LOG("%p exiting slop\n", this);
+ mInSlop = false;
+ }
+ }
+ return mInSlop;
+}
+
+bool TouchBlockState::IsInSlop() const { return mInSlop; }
+
+Maybe<ScrollDirection> TouchBlockState::GetBestGuessPanDirection(
+ const MultiTouchInput& aInput) {
+ if (aInput.mType != MultiTouchInput::MULTITOUCH_MOVE ||
+ aInput.mTouches.Length() != 1) {
+ return Nothing();
+ }
+ ScreenPoint vector = aInput.mTouches[0].mScreenPoint - mSlopOrigin;
+ double angle = atan2(vector.y, vector.x); // range [-pi, pi]
+ angle = fabs(angle); // range [0, pi]
+
+ double angleThreshold = TouchActionAllowsPanningXY()
+ ? StaticPrefs::apz_axis_lock_lock_angle()
+ : StaticPrefs::apz_axis_lock_direct_pan_angle();
+ if (apz::IsCloseToHorizontal(angle, angleThreshold)) {
+ return Some(ScrollDirection::eHorizontal);
+ }
+ if (apz::IsCloseToVertical(angle, angleThreshold)) {
+ return Some(ScrollDirection::eVertical);
+ }
+ return Nothing();
+}
+
+uint32_t TouchBlockState::GetActiveTouchCount() const {
+ return mTouchCounter.GetActiveTouchCount();
+}
+
+KeyboardBlockState::KeyboardBlockState(
+ const RefPtr<AsyncPanZoomController>& aTargetApzc)
+ : InputBlockState(aTargetApzc, TargetConfirmationFlags{true}) {}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/InputBlockState.h b/gfx/layers/apz/src/InputBlockState.h
new file mode 100644
index 0000000000..6320eda6d6
--- /dev/null
+++ b/gfx/layers/apz/src/InputBlockState.h
@@ -0,0 +1,544 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_InputBlockState_h
+#define mozilla_layers_InputBlockState_h
+
+#include "InputData.h" // for MultiTouchInput
+#include "mozilla/RefCounted.h" // for RefCounted
+#include "mozilla/RefPtr.h" // for RefPtr
+#include "mozilla/StaticPrefs_apz.h"
+#include "mozilla/gfx/Matrix.h" // for Matrix4x4
+#include "mozilla/layers/APZUtils.h"
+#include "mozilla/layers/LayersTypes.h" // for TouchBehaviorFlags
+#include "mozilla/layers/AsyncDragMetrics.h"
+#include "mozilla/layers/TouchCounter.h"
+#include "mozilla/TimeStamp.h" // for TimeStamp
+#include "nsTArray.h" // for nsTArray
+
+namespace mozilla {
+namespace layers {
+
+class AsyncPanZoomController;
+class OverscrollHandoffChain;
+class CancelableBlockState;
+class TouchBlockState;
+class WheelBlockState;
+class DragBlockState;
+class PanGestureBlockState;
+class PinchGestureBlockState;
+class KeyboardBlockState;
+enum class BrowserGestureResponse : bool;
+
+/**
+ * A base class that stores state common to various input blocks.
+ * Note that the InputBlockState constructor acquires the tree lock, so callers
+ * from inside AsyncPanZoomController should ensure that the APZC lock is not
+ * held.
+ */
+class InputBlockState : public RefCounted<InputBlockState> {
+ public:
+ MOZ_DECLARE_REFCOUNTED_TYPENAME(InputBlockState)
+
+ static const uint64_t NO_BLOCK_ID = 0;
+
+ enum class TargetConfirmationState : uint8_t {
+ eUnconfirmed,
+ eTimedOut,
+ eTimedOutAndMainThreadResponded,
+ eConfirmed
+ };
+
+ InputBlockState(const RefPtr<AsyncPanZoomController>& aTargetApzc,
+ TargetConfirmationFlags aFlags);
+ virtual ~InputBlockState() = default;
+
+ virtual CancelableBlockState* AsCancelableBlock() { return nullptr; }
+ virtual TouchBlockState* AsTouchBlock() { return nullptr; }
+ virtual WheelBlockState* AsWheelBlock() { return nullptr; }
+ virtual DragBlockState* AsDragBlock() { return nullptr; }
+ virtual PanGestureBlockState* AsPanGestureBlock() { return nullptr; }
+ virtual PinchGestureBlockState* AsPinchGestureBlock() { return nullptr; }
+ virtual KeyboardBlockState* AsKeyboardBlock() { return nullptr; }
+
+ virtual bool SetConfirmedTargetApzc(
+ const RefPtr<AsyncPanZoomController>& aTargetApzc,
+ TargetConfirmationState aState, InputData* aFirstInput,
+ bool aForScrollbarDrag);
+ const RefPtr<AsyncPanZoomController>& GetTargetApzc() const;
+ const RefPtr<const OverscrollHandoffChain>& GetOverscrollHandoffChain() const;
+ uint64_t GetBlockId() const;
+
+ bool IsTargetConfirmed() const;
+ bool HasReceivedRealConfirmedTarget() const;
+
+ virtual bool ShouldDropEvents() const;
+
+ void SetScrolledApzc(AsyncPanZoomController* aApzc);
+ AsyncPanZoomController* GetScrolledApzc() const;
+ bool IsDownchainOfScrolledApzc(AsyncPanZoomController* aApzc) const;
+
+ /**
+ * Dispatch the event to the target APZC. Mostly this is a hook for
+ * subclasses to do any per-event processing they need to.
+ */
+ virtual void DispatchEvent(const InputData& aEvent) const;
+
+ /**
+ * Return true if this input block must stay active if it would otherwise
+ * be removed as the last item in the pending queue.
+ */
+ virtual bool MustStayActive() = 0;
+
+ protected:
+ virtual void UpdateTargetApzc(
+ const RefPtr<AsyncPanZoomController>& aTargetApzc);
+
+ private:
+ // Checks whether |aA| is an ancestor of |aB| (or the same as |aB|) in
+ // |mOverscrollHandoffChain|.
+ bool IsDownchainOf(AsyncPanZoomController* aA,
+ AsyncPanZoomController* aB) const;
+
+ private:
+ RefPtr<AsyncPanZoomController> mTargetApzc;
+ TargetConfirmationState mTargetConfirmed;
+ bool mRequiresTargetConfirmation;
+ const uint64_t mBlockId;
+
+ // The APZC that was actually scrolled by events in this input block.
+ // This is used in configurations where a single input block is only
+ // allowed to scroll a single APZC (configurations where
+ // StaticPrefs::apz_allow_immediate_handoff() is false). Set the first time an
+ // input event in this block scrolls an APZC.
+ RefPtr<AsyncPanZoomController> mScrolledApzc;
+
+ protected:
+ RefPtr<const OverscrollHandoffChain> mOverscrollHandoffChain;
+
+ // Used to transform events from global screen space to |mTargetApzc|'s
+ // screen space. It's cached at the beginning of the input block so that
+ // all events in the block are in the same coordinate space.
+ ScreenToParentLayerMatrix4x4 mTransformToApzc;
+};
+
+/**
+ * This class represents a set of events that can be cancelled by web content
+ * via event listeners.
+ *
+ * Each cancelable input block can be cancelled by web content, and
+ * this information is stored in the mPreventDefault flag. Because web
+ * content runs on the Gecko main thread, we cannot always wait for web
+ * content's response. Instead, there is a timeout that sets this flag in the
+ * case where web content doesn't respond in time. The mContentResponded and
+ * mContentResponseTimerExpired flags indicate which of these scenarios
+ * occurred.
+ */
+class CancelableBlockState : public InputBlockState {
+ public:
+ CancelableBlockState(const RefPtr<AsyncPanZoomController>& aTargetApzc,
+ TargetConfirmationFlags aFlags);
+
+ CancelableBlockState* AsCancelableBlock() override { return this; }
+
+ /**
+ * Record whether or not content cancelled this block of events.
+ * @param aPreventDefault true iff the block is cancelled.
+ * @return false if this block has already received a response from
+ * web content, true if not.
+ */
+ virtual bool SetContentResponse(bool aPreventDefault);
+
+ /**
+ * Record that content didn't respond in time.
+ * @return false if this block already timed out, true if not.
+ */
+ virtual bool TimeoutContentResponse();
+
+ /**
+ * Checks if the content response timer has already expired.
+ */
+ bool IsContentResponseTimerExpired() const;
+
+ /**
+ * @return true iff web content cancelled this block of events.
+ */
+ bool IsDefaultPrevented() const;
+
+ /**
+ * @return true iff this block has received all the information needed
+ * to properly dispatch the events in the block.
+ */
+ virtual bool IsReadyForHandling() const;
+
+ /**
+ * Return a descriptive name for the block kind.
+ */
+ virtual const char* Type() = 0;
+
+ bool ShouldDropEvents() const override;
+
+ private:
+ bool mPreventDefault;
+ bool mContentResponded;
+ bool mContentResponseTimerExpired;
+};
+
+/**
+ * A single block of wheel events.
+ */
+class WheelBlockState : public CancelableBlockState {
+ public:
+ WheelBlockState(const RefPtr<AsyncPanZoomController>& aTargetApzc,
+ TargetConfirmationFlags aFlags,
+ const ScrollWheelInput& aEvent);
+
+ bool SetContentResponse(bool aPreventDefault) override;
+ bool MustStayActive() override;
+ const char* Type() override;
+ bool SetConfirmedTargetApzc(const RefPtr<AsyncPanZoomController>& aTargetApzc,
+ TargetConfirmationState aState,
+ InputData* aFirstInput,
+ bool aForScrollbarDrag) override;
+
+ WheelBlockState* AsWheelBlock() override { return this; }
+
+ /**
+ * Determine whether this wheel block is accepting new events.
+ */
+ bool ShouldAcceptNewEvent() const;
+
+ /**
+ * Call to check whether a wheel event will cause the current transaction to
+ * timeout.
+ */
+ bool MaybeTimeout(const ScrollWheelInput& aEvent);
+
+ /**
+ * Called from APZCTM when a mouse move or drag+drop event occurs, before
+ * the event has been processed.
+ */
+ void OnMouseMove(const ScreenIntPoint& aPoint,
+ const Maybe<ScrollableLayerGuid>& aTargetGuid);
+
+ /**
+ * Returns whether or not the block is participating in a wheel transaction.
+ * This means that the block is the most recent input block to be created,
+ * and no events have occurred that would require scrolling a different
+ * frame.
+ *
+ * @return True if in a transaction, false otherwise.
+ */
+ bool InTransaction() const;
+
+ /**
+ * Mark the block as no longer participating in a wheel transaction. This
+ * will force future wheel events to begin a new input block.
+ */
+ void EndTransaction();
+
+ /**
+ * @return Whether or not overscrolling is prevented for this wheel block.
+ */
+ bool AllowScrollHandoff() const;
+
+ /**
+ * Called to check and possibly end the transaction due to a timeout.
+ *
+ * @return True if the transaction ended, false otherwise.
+ */
+ bool MaybeTimeout(const TimeStamp& aTimeStamp);
+
+ /**
+ * Update the wheel transaction state for a new event.
+ */
+ void Update(ScrollWheelInput& aEvent);
+
+ ScrollDirections GetAllowedScrollDirections() const {
+ return mAllowedScrollDirections;
+ }
+
+ protected:
+ void UpdateTargetApzc(
+ const RefPtr<AsyncPanZoomController>& aTargetApzc) override;
+
+ private:
+ TimeStamp mLastEventTime;
+ TimeStamp mLastMouseMove;
+ uint32_t mScrollSeriesCounter;
+ bool mTransactionEnded;
+ bool mIsScrollable = true;
+ ScrollDirections mAllowedScrollDirections;
+};
+
+/**
+ * A block of mouse events that are part of a drag
+ */
+class DragBlockState : public CancelableBlockState {
+ public:
+ DragBlockState(const RefPtr<AsyncPanZoomController>& aTargetApzc,
+ TargetConfirmationFlags aFlags, const MouseInput& aEvent);
+
+ bool MustStayActive() override;
+ const char* Type() override;
+
+ bool HasReceivedMouseUp();
+ void MarkMouseUpReceived();
+
+ DragBlockState* AsDragBlock() override { return this; }
+
+ void SetInitialThumbPos(OuterCSSCoord aThumbPos);
+ void SetDragMetrics(const AsyncDragMetrics& aDragMetrics);
+
+ void DispatchEvent(const InputData& aEvent) const override;
+
+ private:
+ AsyncDragMetrics mDragMetrics;
+ OuterCSSCoord mInitialThumbPos;
+ bool mReceivedMouseUp;
+};
+
+/**
+ * A single block of pan gesture events.
+ */
+class PanGestureBlockState : public CancelableBlockState {
+ public:
+ PanGestureBlockState(const RefPtr<AsyncPanZoomController>& aTargetApzc,
+ TargetConfirmationFlags aFlags,
+ const PanGestureInput& aEvent);
+
+ bool SetContentResponse(bool aPreventDefault) override;
+ bool IsReadyForHandling() const override;
+ bool MustStayActive() override;
+ const char* Type() override;
+ bool SetConfirmedTargetApzc(const RefPtr<AsyncPanZoomController>& aTargetApzc,
+ TargetConfirmationState aState,
+ InputData* aFirstInput,
+ bool aForScrollbarDrag) override;
+
+ PanGestureBlockState* AsPanGestureBlock() override { return this; }
+
+ bool ShouldDropEvents() const override;
+
+ bool TimeoutContentResponse() override;
+
+ /**
+ * @return Whether or not overscrolling is prevented for this block.
+ */
+ bool AllowScrollHandoff() const;
+
+ bool WasInterrupted() const { return mInterrupted; }
+
+ void SetNeedsToWaitForContentResponse(bool aWaitForContentResponse);
+ void SetNeedsToWaitForBrowserGestureResponse(
+ bool aWaitForBrowserGestureResponse);
+ void SetBrowserGestureResponse(BrowserGestureResponse aResponse);
+
+ ScrollDirections GetAllowedScrollDirections() const {
+ return mAllowedScrollDirections;
+ }
+
+ private:
+ bool mInterrupted;
+ bool mWaitingForContentResponse;
+ // A pan gesture may be used for browser's swipe gestures so APZ needs to wait
+ // for the response from the browser whether the gesture has been used for
+ // swipe or not. This `mWaitingForBrowserGestureResponse` flag represents the
+ // waiting state. And below `mStartedBrowserGesture` represents the response
+ // from the browser.
+ bool mWaitingForBrowserGestureResponse;
+ bool mStartedBrowserGesture;
+ ScrollDirections mAllowedScrollDirections;
+};
+
+/**
+ * A single block of pinch gesture events.
+ */
+class PinchGestureBlockState : public CancelableBlockState {
+ public:
+ PinchGestureBlockState(const RefPtr<AsyncPanZoomController>& aTargetApzc,
+ TargetConfirmationFlags aFlags);
+
+ bool SetContentResponse(bool aPreventDefault) override;
+ bool IsReadyForHandling() const override;
+ bool MustStayActive() override;
+ const char* Type() override;
+
+ PinchGestureBlockState* AsPinchGestureBlock() override { return this; }
+
+ bool WasInterrupted() const { return mInterrupted; }
+
+ void SetNeedsToWaitForContentResponse(bool aWaitForContentResponse);
+
+ private:
+ bool mInterrupted;
+ bool mWaitingForContentResponse;
+};
+
+/**
+ * This class represents a single touch block. A touch block is
+ * a set of touch events that can be cancelled by web content via
+ * touch event listeners.
+ *
+ * Every touch-start event creates a new touch block. In this case, the
+ * touch block consists of the touch-start, followed by all touch events
+ * up to but not including the next touch-start (except in the case where
+ * a long-tap happens, see below). Note that in particular we cannot know
+ * when a touch block ends until the next one is started. Most touch
+ * blocks are created by receipt of a touch-start event.
+ *
+ * Every long-tap event also creates a new touch block, since it can also
+ * be consumed by web content. In this case, when the long-tap event is
+ * dispatched to web content, a new touch block is started to hold the remaining
+ * touch events, up to but not including the next touch start (or long-tap).
+ *
+ * Additionally, if touch-action is enabled, each touch block should
+ * have a set of allowed touch behavior flags; one for each touch point.
+ * This also requires running code on the Gecko main thread, and so may
+ * be populated with some latency. The mAllowedTouchBehaviorSet and
+ * mAllowedTouchBehaviors variables track this information.
+ */
+class TouchBlockState : public CancelableBlockState {
+ public:
+ explicit TouchBlockState(const RefPtr<AsyncPanZoomController>& aTargetApzc,
+ TargetConfirmationFlags aFlags,
+ TouchCounter& aTouchCounter);
+
+ TouchBlockState* AsTouchBlock() override { return this; }
+
+ /**
+ * Set the allowed touch behavior flags for this block.
+ * @return false if this block already has these flags set, true if not.
+ */
+ bool SetAllowedTouchBehaviors(const nsTArray<TouchBehaviorFlags>& aBehaviors);
+ /**
+ * If the allowed touch behaviors have been set, populate them into
+ * |aOutBehaviors| and return true. Else, return false.
+ */
+ bool GetAllowedTouchBehaviors(
+ nsTArray<TouchBehaviorFlags>& aOutBehaviors) const;
+
+ /**
+ * Returns true if the allowed touch behaviours have been set, or if touch
+ * action is disabled.
+ */
+ bool HasAllowedTouchBehaviors() const;
+
+ /**
+ * Copy various properties from another block.
+ */
+ void CopyPropertiesFrom(const TouchBlockState& aOther);
+
+ /**
+ * @return true iff this block has received all the information needed
+ * to properly dispatch the events in the block.
+ */
+ bool IsReadyForHandling() const override;
+
+ /**
+ * Sets a flag that indicates this input block occurred while the APZ was
+ * in a state of fast flinging. This affects gestures that may be produced
+ * from input events in this block.
+ */
+ void SetDuringFastFling();
+ /**
+ * @return true iff SetDuringFastFling was called on this block.
+ */
+ bool IsDuringFastFling() const;
+ /**
+ * Set the single-tap-occurred flag that indicates that this touch block
+ * triggered a single tap event.
+ */
+ void SetSingleTapOccurred();
+ /**
+ * @return true iff the single-tap-occurred flag is set on this block.
+ */
+ bool SingleTapOccurred() const;
+
+ /**
+ * @return false iff touch-action is enabled and the allowed touch behaviors
+ * for this touch block do not allow pinch-zooming.
+ */
+ bool TouchActionAllowsPinchZoom() const;
+ /**
+ * @return false iff touch-action is enabled and the allowed touch behaviors
+ * for this touch block do not allow double-tap zooming.
+ */
+ bool TouchActionAllowsDoubleTapZoom() const;
+ /**
+ * @return false iff touch-action is enabled and the allowed touch behaviors
+ * for the first touch point do not allow panning in the specified
+ * direction(s).
+ */
+ bool TouchActionAllowsPanningX() const;
+ bool TouchActionAllowsPanningY() const;
+ bool TouchActionAllowsPanningXY() const;
+
+ /**
+ * Notifies the input block of an incoming touch event so that the block can
+ * update its internal slop state. "Slop" refers to the area around the
+ * initial touchstart where we drop touchmove events so that content doesn't
+ * see them. The |aApzcCanConsumeEvents| parameter is factored into how large
+ * the slop area is - if this is true the slop area is larger.
+ * @return true iff the provided event is a touchmove in the slop area and
+ * so should not be sent to content.
+ */
+ bool UpdateSlopState(const MultiTouchInput& aInput,
+ bool aApzcCanConsumeEvents);
+ bool IsInSlop() const;
+
+ /**
+ * Based on the slop origin and the given input event, return a best guess
+ * as to the pan direction of this touch block. Returns Nothing() if no guess
+ * can be made.
+ */
+ Maybe<ScrollDirection> GetBestGuessPanDirection(
+ const MultiTouchInput& aInput);
+
+ /**
+ * Returns the number of touch points currently active.
+ */
+ uint32_t GetActiveTouchCount() const;
+
+ void DispatchEvent(const InputData& aEvent) const override;
+ bool MustStayActive() override;
+ const char* Type() override;
+ TimeDuration GetTimeSinceBlockStart() const;
+
+ private:
+ nsTArray<TouchBehaviorFlags> mAllowedTouchBehaviors;
+ bool mAllowedTouchBehaviorSet;
+ bool mDuringFastFling;
+ bool mSingleTapOccurred;
+ bool mInSlop;
+ ScreenIntPoint mSlopOrigin;
+ // A reference to the InputQueue's touch counter
+ TouchCounter& mTouchCounter;
+ TimeStamp mStartTime;
+};
+
+/**
+ * This class represents a set of keyboard inputs targeted at the same Apzc.
+ */
+class KeyboardBlockState : public InputBlockState {
+ public:
+ explicit KeyboardBlockState(
+ const RefPtr<AsyncPanZoomController>& aTargetApzc);
+
+ KeyboardBlockState* AsKeyboardBlock() override { return this; }
+
+ bool MustStayActive() override { return false; }
+
+ /**
+ * @return Whether or not overscrolling is prevented for this keyboard block.
+ */
+ bool AllowScrollHandoff() const { return false; }
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_InputBlockState_h
diff --git a/gfx/layers/apz/src/InputQueue.cpp b/gfx/layers/apz/src/InputQueue.cpp
new file mode 100644
index 0000000000..b6c7a05830
--- /dev/null
+++ b/gfx/layers/apz/src/InputQueue.cpp
@@ -0,0 +1,1090 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "InputQueue.h"
+
+#include "AsyncPanZoomController.h"
+
+#include "GestureEventListener.h"
+#include "InputBlockState.h"
+#include "mozilla/EventForwards.h"
+#include "mozilla/layers/APZInputBridge.h"
+#include "mozilla/layers/APZThreadUtils.h"
+#include "mozilla/ToString.h"
+#include "OverscrollHandoffState.h"
+#include "QueuedInput.h"
+#include "mozilla/StaticPrefs_apz.h"
+#include "mozilla/StaticPrefs_layout.h"
+#include "mozilla/StaticPrefs_ui.h"
+
+static mozilla::LazyLogModule sApzInpLog("apz.inputqueue");
+#define INPQ_LOG(...) MOZ_LOG(sApzInpLog, LogLevel::Debug, (__VA_ARGS__))
+
+namespace mozilla {
+namespace layers {
+
+InputQueue::InputQueue() = default;
+
+InputQueue::~InputQueue() { mQueuedInputs.Clear(); }
+
+APZEventResult InputQueue::ReceiveInputEvent(
+ const RefPtr<AsyncPanZoomController>& aTarget,
+ TargetConfirmationFlags aFlags, InputData& aEvent,
+ const Maybe<nsTArray<TouchBehaviorFlags>>& aTouchBehaviors) {
+ APZThreadUtils::AssertOnControllerThread();
+
+ AutoRunImmediateTimeout timeoutRunner{this};
+
+ switch (aEvent.mInputType) {
+ case MULTITOUCH_INPUT: {
+ const MultiTouchInput& event = aEvent.AsMultiTouchInput();
+ return ReceiveTouchInput(aTarget, aFlags, event, aTouchBehaviors);
+ }
+
+ case SCROLLWHEEL_INPUT: {
+ const ScrollWheelInput& event = aEvent.AsScrollWheelInput();
+ return ReceiveScrollWheelInput(aTarget, aFlags, event);
+ }
+
+ case PANGESTURE_INPUT: {
+ const PanGestureInput& event = aEvent.AsPanGestureInput();
+ return ReceivePanGestureInput(aTarget, aFlags, event);
+ }
+
+ case PINCHGESTURE_INPUT: {
+ const PinchGestureInput& event = aEvent.AsPinchGestureInput();
+ return ReceivePinchGestureInput(aTarget, aFlags, event);
+ }
+
+ case MOUSE_INPUT: {
+ MouseInput& event = aEvent.AsMouseInput();
+ return ReceiveMouseInput(aTarget, aFlags, event);
+ }
+
+ case KEYBOARD_INPUT: {
+ // Every keyboard input must have a confirmed target
+ MOZ_ASSERT(aTarget && aFlags.mTargetConfirmed);
+
+ const KeyboardInput& event = aEvent.AsKeyboardInput();
+ return ReceiveKeyboardInput(aTarget, aFlags, event);
+ }
+
+ default: {
+ // The `mStatus` for other input type is only used by tests, so just
+ // pass through the return value of HandleInputEvent() for now.
+ APZEventResult result(aTarget, aFlags);
+ nsEventStatus status =
+ aTarget->HandleInputEvent(aEvent, aTarget->GetTransformToThis());
+ switch (status) {
+ case nsEventStatus_eIgnore:
+ result.SetStatusAsIgnore();
+ break;
+ case nsEventStatus_eConsumeNoDefault:
+ result.SetStatusAsConsumeNoDefault();
+ break;
+ case nsEventStatus_eConsumeDoDefault:
+ result.SetStatusAsConsumeDoDefault(aTarget);
+ break;
+ default:
+ MOZ_ASSERT_UNREACHABLE("An invalid status");
+ break;
+ }
+ return result;
+ }
+ }
+}
+
+APZEventResult InputQueue::ReceiveTouchInput(
+ const RefPtr<AsyncPanZoomController>& aTarget,
+ TargetConfirmationFlags aFlags, const MultiTouchInput& aEvent,
+ const Maybe<nsTArray<TouchBehaviorFlags>>& aTouchBehaviors) {
+ APZEventResult result(aTarget, aFlags);
+
+ RefPtr<TouchBlockState> block;
+ bool waitingForContentResponse = false;
+ if (aEvent.mType == MultiTouchInput::MULTITOUCH_START) {
+ nsTArray<TouchBehaviorFlags> currentBehaviors;
+ bool haveBehaviors = false;
+ if (mActiveTouchBlock) {
+ haveBehaviors =
+ mActiveTouchBlock->GetAllowedTouchBehaviors(currentBehaviors);
+ // If the behaviours aren't set, but the main-thread response timer on
+ // the block is expired we still treat it as though it has behaviors,
+ // because in that case we still want to interrupt the fast-fling and
+ // use the default behaviours.
+ haveBehaviors |= mActiveTouchBlock->IsContentResponseTimerExpired();
+ }
+
+ block = StartNewTouchBlock(aTarget, aFlags, false);
+ INPQ_LOG("started new touch block %p id %" PRIu64 " for target %p\n",
+ block.get(), block->GetBlockId(), aTarget.get());
+
+ // XXX using the chain from |block| here may be wrong in cases where the
+ // target isn't confirmed and the real target turns out to be something
+ // else. For now assume this is rare enough that it's not an issue.
+ if (mQueuedInputs.IsEmpty() && aEvent.mTouches.Length() == 1 &&
+ block->GetOverscrollHandoffChain()->HasFastFlungApzc() &&
+ haveBehaviors) {
+ // If we're already in a fast fling, and a single finger goes down, then
+ // we want special handling for the touch event, because it shouldn't get
+ // delivered to content. Note that we don't set this flag when going
+ // from a fast fling to a pinch state (i.e. second finger goes down while
+ // the first finger is moving).
+ block->SetDuringFastFling();
+ block->SetConfirmedTargetApzc(
+ aTarget, InputBlockState::TargetConfirmationState::eConfirmed,
+ nullptr /* the block was just created so it has no events */,
+ false /* not a scrollbar drag */);
+ block->SetAllowedTouchBehaviors(currentBehaviors);
+ INPQ_LOG("block %p tagged as fast-motion\n", block.get());
+ } else if (aTouchBehaviors) {
+ // If this block isn't started during a fast-fling, and APZCTM has
+ // provided touch behavior information, then put it on the block so
+ // that the ArePointerEventsConsumable call below can use it.
+ block->SetAllowedTouchBehaviors(*aTouchBehaviors);
+ }
+
+ CancelAnimationsForNewBlock(block);
+
+ waitingForContentResponse = MaybeRequestContentResponse(aTarget, block);
+ } else {
+ // for touch inputs that don't start a block, APZCTM shouldn't be giving
+ // us any touch behaviors.
+ MOZ_ASSERT(aTouchBehaviors.isNothing());
+
+ block = mActiveTouchBlock.get();
+ if (!block) {
+ NS_WARNING(
+ "Received a non-start touch event while no touch blocks active!");
+ return result;
+ }
+
+ INPQ_LOG("received new touch event (type=%d) in block %p\n", aEvent.mType,
+ block.get());
+ }
+
+ result.mInputBlockId = block->GetBlockId();
+
+ // Note that the |aTarget| the APZCTM sent us may contradict the confirmed
+ // target set on the block. In this case the confirmed target (which may be
+ // null) should take priority. This is equivalent to just always using the
+ // target (confirmed or not) from the block.
+ RefPtr<AsyncPanZoomController> target = block->GetTargetApzc();
+
+ // XXX calling ArePointerEventsConsumable on |target| may be wrong here if
+ // the target isn't confirmed and the real target turns out to be something
+ // else. For now assume this is rare enough that it's not an issue.
+ PointerEventsConsumableFlags consumableFlags;
+ if (target) {
+ consumableFlags = target->ArePointerEventsConsumable(block, aEvent);
+ }
+ if (block->IsDuringFastFling()) {
+ INPQ_LOG("dropping event due to block %p being in fast motion\n",
+ block.get());
+ result.SetStatusForFastFling(*block, aFlags, consumableFlags, target);
+ } else { // handling depends on ArePointerEventsConsumable()
+ bool consumable = consumableFlags.IsConsumable();
+ if (block->UpdateSlopState(aEvent, consumable)) {
+ INPQ_LOG("dropping event due to block %p being in %sslop\n", block.get(),
+ consumable ? "" : "mini-");
+ result.SetStatusAsConsumeNoDefault();
+ } else {
+ result.SetStatusForTouchEvent(*block, aFlags, consumableFlags, target);
+ }
+ }
+ mQueuedInputs.AppendElement(MakeUnique<QueuedInput>(aEvent, *block));
+ ProcessQueue();
+
+ // If this block just started and is waiting for a content response, but
+ // also in a slop state (i.e. touchstart gets delivered to content but
+ // not any touchmoves), then we might end up in a situation where we don't
+ // get the content response until the timeout is hit because we never exit
+ // the slop state. But if that timeout is longer than the long-press timeout,
+ // then the long-press gets delayed too. Avoid that by scheduling a callback
+ // with the long-press timeout that will force the block to get processed.
+ int32_t longTapTimeout = StaticPrefs::ui_click_hold_context_menus_delay();
+ int32_t contentTimeout = StaticPrefs::apz_content_response_timeout();
+ if (waitingForContentResponse && longTapTimeout < contentTimeout &&
+ block->IsInSlop() && GestureEventListener::IsLongTapEnabled()) {
+ MOZ_ASSERT(aEvent.mType == MultiTouchInput::MULTITOUCH_START);
+ MOZ_ASSERT(!block->IsDuringFastFling());
+ RefPtr<Runnable> maybeLongTap = NewRunnableMethod<uint64_t>(
+ "layers::InputQueue::MaybeLongTapTimeout", this,
+ &InputQueue::MaybeLongTapTimeout, block->GetBlockId());
+ INPQ_LOG("scheduling maybe-long-tap timeout for target %p\n",
+ aTarget.get());
+ aTarget->PostDelayedTask(maybeLongTap.forget(), longTapTimeout);
+ }
+
+ return result;
+}
+
+APZEventResult InputQueue::ReceiveMouseInput(
+ const RefPtr<AsyncPanZoomController>& aTarget,
+ TargetConfirmationFlags aFlags, MouseInput& aEvent) {
+ APZEventResult result(aTarget, aFlags);
+
+ // On a new mouse down we can have a new target so we must force a new block
+ // with a new target.
+ bool newBlock = DragTracker::StartsDrag(aEvent);
+
+ RefPtr<DragBlockState> block = newBlock ? nullptr : mActiveDragBlock.get();
+ if (block && block->HasReceivedMouseUp()) {
+ block = nullptr;
+ }
+
+ if (!block && mDragTracker.InDrag()) {
+ // If there's no current drag block, but we're getting a move with a button
+ // down, we need to start a new drag block because we're obviously already
+ // in the middle of a drag (it probably got interrupted by something else).
+ INPQ_LOG(
+ "got a drag event outside a drag block, need to create a block to hold "
+ "it\n");
+ newBlock = true;
+ }
+
+ mDragTracker.Update(aEvent);
+
+ if (!newBlock && !block) {
+ // This input event is not in a drag block, so we're not doing anything
+ // with it, return eIgnore.
+ return result;
+ }
+
+ if (!block) {
+ MOZ_ASSERT(newBlock);
+ block = new DragBlockState(aTarget, aFlags, aEvent);
+
+ INPQ_LOG(
+ "started new drag block %p id %" PRIu64
+ "for %sconfirmed target %p; on scrollbar: %d; on scrollthumb: %d\n",
+ block.get(), block->GetBlockId(), aFlags.mTargetConfirmed ? "" : "un",
+ aTarget.get(), aFlags.mHitScrollbar, aFlags.mHitScrollThumb);
+
+ mActiveDragBlock = block;
+
+ if (aFlags.mHitScrollThumb || !aFlags.mHitScrollbar) {
+ // If we're running autoscroll, we'll always cancel it during the
+ // following call of CancelAnimationsForNewBlock. At this time,
+ // we don't want to fire `click` event on the web content for web-compat
+ // with Chrome. Therefore, we notify widget of it with the flag.
+ if ((aEvent.mType == MouseInput::MOUSE_DOWN ||
+ aEvent.mType == MouseInput::MOUSE_UP) &&
+ block->GetOverscrollHandoffChain()->HasAutoscrollApzc()) {
+ aEvent.mPreventClickEvent = true;
+ }
+ CancelAnimationsForNewBlock(block);
+ }
+ MaybeRequestContentResponse(aTarget, block);
+ }
+
+ result.mInputBlockId = block->GetBlockId();
+
+ mQueuedInputs.AppendElement(MakeUnique<QueuedInput>(aEvent, *block));
+ ProcessQueue();
+
+ if (DragTracker::EndsDrag(aEvent)) {
+ block->MarkMouseUpReceived();
+ }
+
+ // The event is part of a drag block and could potentially cause
+ // scrolling, so return DoDefault.
+ result.SetStatusAsConsumeDoDefault(*block);
+ return result;
+}
+
+APZEventResult InputQueue::ReceiveScrollWheelInput(
+ const RefPtr<AsyncPanZoomController>& aTarget,
+ TargetConfirmationFlags aFlags, const ScrollWheelInput& aEvent) {
+ APZEventResult result(aTarget, aFlags);
+
+ RefPtr<WheelBlockState> block = mActiveWheelBlock.get();
+ // If the block is not accepting new events we'll create a new input block
+ // (and therefore a new wheel transaction).
+ if (block &&
+ (!block->ShouldAcceptNewEvent() || block->MaybeTimeout(aEvent))) {
+ block = nullptr;
+ }
+
+ MOZ_ASSERT(!block || block->InTransaction());
+
+ if (!block) {
+ block = new WheelBlockState(aTarget, aFlags, aEvent);
+ INPQ_LOG("started new scroll wheel block %p id %" PRIu64
+ " for %starget %p\n",
+ block.get(), block->GetBlockId(),
+ aFlags.mTargetConfirmed ? "confirmed " : "", aTarget.get());
+
+ mActiveWheelBlock = block;
+
+ CancelAnimationsForNewBlock(block, ExcludeWheel);
+ MaybeRequestContentResponse(aTarget, block);
+ } else {
+ INPQ_LOG("received new wheel event in block %p\n", block.get());
+ }
+
+ result.mInputBlockId = block->GetBlockId();
+
+ // Note that the |aTarget| the APZCTM sent us may contradict the confirmed
+ // target set on the block. In this case the confirmed target (which may be
+ // null) should take priority. This is equivalent to just always using the
+ // target (confirmed or not) from the block, which is what
+ // ProcessQueue() does.
+ mQueuedInputs.AppendElement(MakeUnique<QueuedInput>(aEvent, *block));
+
+ // The WheelBlockState needs to affix a counter to the event before we process
+ // it. Note that the counter is affixed to the copy in the queue rather than
+ // |aEvent|.
+ block->Update(mQueuedInputs.LastElement()->Input()->AsScrollWheelInput());
+
+ ProcessQueue();
+
+ result.SetStatusAsConsumeDoDefault(*block);
+ return result;
+}
+
+APZEventResult InputQueue::ReceiveKeyboardInput(
+ const RefPtr<AsyncPanZoomController>& aTarget,
+ TargetConfirmationFlags aFlags, const KeyboardInput& aEvent) {
+ APZEventResult result(aTarget, aFlags);
+
+ RefPtr<KeyboardBlockState> block = mActiveKeyboardBlock.get();
+
+ // If the block is targeting a different Apzc than this keyboard event then
+ // we'll create a new input block
+ if (block && block->GetTargetApzc() != aTarget) {
+ block = nullptr;
+ }
+
+ if (!block) {
+ block = new KeyboardBlockState(aTarget);
+ INPQ_LOG("started new keyboard block %p id %" PRIu64 " for target %p\n",
+ block.get(), block->GetBlockId(), aTarget.get());
+
+ mActiveKeyboardBlock = block;
+ } else {
+ INPQ_LOG("received new keyboard event in block %p\n", block.get());
+ }
+
+ result.mInputBlockId = block->GetBlockId();
+
+ mQueuedInputs.AppendElement(MakeUnique<QueuedInput>(aEvent, *block));
+
+ ProcessQueue();
+
+ // If APZ is allowing passive listeners then we must dispatch the event to
+ // content, otherwise we can consume the event.
+ if (StaticPrefs::apz_keyboard_passive_listeners()) {
+ result.SetStatusAsConsumeDoDefault(*block);
+ } else {
+ result.SetStatusAsConsumeNoDefault();
+ }
+ return result;
+}
+
+static bool CanScrollTargetHorizontally(const PanGestureInput& aInitialEvent,
+ PanGestureBlockState* aBlock) {
+ PanGestureInput horizontalComponent = aInitialEvent;
+ horizontalComponent.mPanDisplacement.y = 0;
+ ScrollDirections allowedScrollDirections;
+ RefPtr<AsyncPanZoomController> horizontallyScrollableAPZC =
+ aBlock->GetOverscrollHandoffChain()->FindFirstScrollable(
+ horizontalComponent, &allowedScrollDirections,
+ OverscrollHandoffChain::IncludeOverscroll::No);
+ return horizontallyScrollableAPZC &&
+ horizontallyScrollableAPZC == aBlock->GetTargetApzc() &&
+ allowedScrollDirections.contains(ScrollDirection::eHorizontal);
+}
+
+APZEventResult InputQueue::ReceivePanGestureInput(
+ const RefPtr<AsyncPanZoomController>& aTarget,
+ TargetConfirmationFlags aFlags, const PanGestureInput& aEvent) {
+ APZEventResult result(aTarget, aFlags);
+
+ if (aEvent.mType == PanGestureInput::PANGESTURE_MAYSTART ||
+ aEvent.mType == PanGestureInput::PANGESTURE_CANCELLED) {
+ // Ignore these events for now.
+ result.SetStatusAsConsumeDoDefault(aTarget);
+ return result;
+ }
+
+ if (aEvent.mType == PanGestureInput::PANGESTURE_INTERRUPTED) {
+ if (RefPtr<PanGestureBlockState> block = mActivePanGestureBlock.get()) {
+ mQueuedInputs.AppendElement(MakeUnique<QueuedInput>(aEvent, *block));
+ ProcessQueue();
+ }
+ result.SetStatusAsIgnore();
+ return result;
+ }
+
+ RefPtr<PanGestureBlockState> block;
+ if (aEvent.mType != PanGestureInput::PANGESTURE_START) {
+ block = mActivePanGestureBlock.get();
+ }
+
+ PanGestureInput event = aEvent;
+
+ // Below `SetStatusAsConsumeDoDefault()` preserves `mHandledResult` of
+ // `result` which was set in the ctor of APZEventResult at the top of this
+ // function based on `aFlag` so that the `mHandledResult` value is reliable to
+ // tell whether the event will be handled by the root content APZC at least
+ // for swipe-navigation stuff. E.g. if a pan-start event scrolled the root
+ // scroll container, we don't need to anything for swipe-navigation.
+ result.SetStatusAsConsumeDoDefault();
+
+ if (!block || block->WasInterrupted()) {
+ if (event.mType == PanGestureInput::PANGESTURE_MOMENTUMSTART ||
+ event.mType == PanGestureInput::PANGESTURE_MOMENTUMPAN ||
+ event.mType == PanGestureInput::PANGESTURE_MOMENTUMEND) {
+ // If there are momentum events after an interruption, discard them.
+ // However, if there is a non-momentum event (indicating the user
+ // continued scrolling on the touchpad), a new input block is started
+ // by turning the event into a pan-start below.
+ return result;
+ }
+ if (event.mType != PanGestureInput::PANGESTURE_START) {
+ // Only PANGESTURE_START events are allowed to start a new pan gesture
+ // block, but we really want to start a new block here, so we magically
+ // turn this input into a PANGESTURE_START.
+ INPQ_LOG(
+ "transmogrifying pan input %d to PANGESTURE_START for new block\n",
+ event.mType);
+ event.mType = PanGestureInput::PANGESTURE_START;
+ }
+ block = new PanGestureBlockState(aTarget, aFlags, event);
+ INPQ_LOG("started new pan gesture block %p id %" PRIu64 " for target %p\n",
+ block.get(), block->GetBlockId(), aTarget.get());
+
+ mActivePanGestureBlock = block;
+
+ CancelAnimationsForNewBlock(block);
+ const bool waitingForContentResponse =
+ MaybeRequestContentResponse(aTarget, block);
+
+ if (event.AllowsSwipe() && !CanScrollTargetHorizontally(event, block)) {
+ // We will ask the browser whether this pan event is going to be used for
+ // swipe or not, so we need to wait the response.
+ block->SetNeedsToWaitForBrowserGestureResponse(true);
+ if (!waitingForContentResponse) {
+ ScheduleMainThreadTimeout(aTarget, block);
+ }
+ if (aFlags.mTargetConfirmed) {
+ // This event may trigger a swipe gesture, depending on what our caller
+ // wants to do it. We need to suspend handling of this block until we
+ // get a content response which will tell us whether to proceed or abort
+ // the block.
+ block->SetNeedsToWaitForContentResponse(true);
+
+ // Inform our caller that we haven't scrolled in response to the event
+ // and that a swipe can be started from this event if desired.
+ result.SetStatusAsIgnore();
+ }
+ }
+ } else {
+ INPQ_LOG("received new pan event (type=%d) in block %p\n", aEvent.mType,
+ block.get());
+ }
+
+ result.mInputBlockId = block->GetBlockId();
+
+ // Note that the |aTarget| the APZCTM sent us may contradict the confirmed
+ // target set on the block. In this case the confirmed target (which may be
+ // null) should take priority. This is equivalent to just always using the
+ // target (confirmed or not) from the block, which is what
+ // ProcessQueue() does.
+ mQueuedInputs.AppendElement(MakeUnique<QueuedInput>(event, *block));
+ ProcessQueue();
+
+ return result;
+}
+
+APZEventResult InputQueue::ReceivePinchGestureInput(
+ const RefPtr<AsyncPanZoomController>& aTarget,
+ TargetConfirmationFlags aFlags, const PinchGestureInput& aEvent) {
+ APZEventResult result(aTarget, aFlags);
+
+ RefPtr<PinchGestureBlockState> block;
+ if (aEvent.mType != PinchGestureInput::PINCHGESTURE_START) {
+ block = mActivePinchGestureBlock.get();
+ }
+
+ result.SetStatusAsConsumeDoDefault(aTarget);
+
+ if (!block || block->WasInterrupted()) {
+ if (aEvent.mType != PinchGestureInput::PINCHGESTURE_START) {
+ // Only PINCHGESTURE_START events are allowed to start a new pinch gesture
+ // block.
+ INPQ_LOG("pinchgesture block %p was interrupted %d\n", block.get(),
+ block ? block->WasInterrupted() : 0);
+ return result;
+ }
+ block = new PinchGestureBlockState(aTarget, aFlags);
+ INPQ_LOG("started new pinch gesture block %p id %" PRIu64
+ " for target %p\n",
+ block.get(), block->GetBlockId(), aTarget.get());
+
+ mActivePinchGestureBlock = block;
+ block->SetNeedsToWaitForContentResponse(true);
+
+ CancelAnimationsForNewBlock(block);
+ MaybeRequestContentResponse(aTarget, block);
+ } else {
+ INPQ_LOG("received new pinch event (type=%d) in block %p\n", aEvent.mType,
+ block.get());
+ }
+
+ result.mInputBlockId = block->GetBlockId();
+
+ // Note that the |aTarget| the APZCTM sent us may contradict the confirmed
+ // target set on the block. In this case the confirmed target (which may be
+ // null) should take priority. This is equivalent to just always using the
+ // target (confirmed or not) from the block, which is what
+ // ProcessQueue() does.
+ mQueuedInputs.AppendElement(MakeUnique<QueuedInput>(aEvent, *block));
+ ProcessQueue();
+
+ return result;
+}
+
+void InputQueue::CancelAnimationsForNewBlock(InputBlockState* aBlock,
+ CancelAnimationFlags aExtraFlags) {
+ // We want to cancel animations here as soon as possible (i.e. without waiting
+ // for content responses) because a finger has gone down and we don't want to
+ // keep moving the content under the finger. However, to prevent "future"
+ // touchstart events from interfering with "past" animations (i.e. from a
+ // previous touch block that is still being processed) we only do this
+ // animation-cancellation if there are no older touch blocks still in the
+ // queue.
+ if (mQueuedInputs.IsEmpty()) {
+ aBlock->GetOverscrollHandoffChain()->CancelAnimations(
+ aExtraFlags | ExcludeOverscroll | ScrollSnap);
+ }
+}
+
+bool InputQueue::MaybeRequestContentResponse(
+ const RefPtr<AsyncPanZoomController>& aTarget,
+ CancelableBlockState* aBlock) {
+ bool waitForMainThread = false;
+ if (aBlock->IsTargetConfirmed()) {
+ // Content won't prevent-default this, so we can just set the flag directly.
+ INPQ_LOG("not waiting for content response on block %p\n", aBlock);
+ aBlock->SetContentResponse(false);
+ } else {
+ waitForMainThread = true;
+ }
+ if (aBlock->AsTouchBlock() &&
+ !aBlock->AsTouchBlock()->HasAllowedTouchBehaviors()) {
+ INPQ_LOG("waiting for main thread touch-action info on block %p\n", aBlock);
+ waitForMainThread = true;
+ }
+ if (waitForMainThread) {
+ // We either don't know for sure if aTarget is the right APZC, or we may
+ // need to wait to give content the opportunity to prevent-default the
+ // touch events. Either way we schedule a timeout so the main thread stuff
+ // can run.
+ ScheduleMainThreadTimeout(aTarget, aBlock);
+ }
+ return waitForMainThread;
+}
+
+uint64_t InputQueue::InjectNewTouchBlock(AsyncPanZoomController* aTarget) {
+ AutoRunImmediateTimeout timeoutRunner{this};
+ TouchBlockState* block =
+ StartNewTouchBlock(aTarget, TargetConfirmationFlags{true},
+ /* aCopyPropertiesFromCurrent = */ true);
+ INPQ_LOG("injecting new touch block %p with id %" PRIu64 " and target %p\n",
+ block, block->GetBlockId(), aTarget);
+ ScheduleMainThreadTimeout(aTarget, block);
+ return block->GetBlockId();
+}
+
+TouchBlockState* InputQueue::StartNewTouchBlock(
+ const RefPtr<AsyncPanZoomController>& aTarget,
+ TargetConfirmationFlags aFlags, bool aCopyPropertiesFromCurrent) {
+ TouchBlockState* newBlock =
+ new TouchBlockState(aTarget, aFlags, mTouchCounter);
+ if (aCopyPropertiesFromCurrent) {
+ // We should never enter here without a current touch block, because this
+ // codepath is invoked from the OnLongPress handler in
+ // AsyncPanZoomController, which should bail out if there is no current
+ // touch block.
+ MOZ_ASSERT(GetCurrentTouchBlock());
+ newBlock->CopyPropertiesFrom(*GetCurrentTouchBlock());
+ }
+
+ mActiveTouchBlock = newBlock;
+ return newBlock;
+}
+
+InputBlockState* InputQueue::GetCurrentBlock() const {
+ APZThreadUtils::AssertOnControllerThread();
+ return mQueuedInputs.IsEmpty() ? nullptr : mQueuedInputs[0]->Block();
+}
+
+TouchBlockState* InputQueue::GetCurrentTouchBlock() const {
+ InputBlockState* block = GetCurrentBlock();
+ return block ? block->AsTouchBlock() : mActiveTouchBlock.get();
+}
+
+WheelBlockState* InputQueue::GetCurrentWheelBlock() const {
+ InputBlockState* block = GetCurrentBlock();
+ return block ? block->AsWheelBlock() : mActiveWheelBlock.get();
+}
+
+DragBlockState* InputQueue::GetCurrentDragBlock() const {
+ InputBlockState* block = GetCurrentBlock();
+ return block ? block->AsDragBlock() : mActiveDragBlock.get();
+}
+
+PanGestureBlockState* InputQueue::GetCurrentPanGestureBlock() const {
+ InputBlockState* block = GetCurrentBlock();
+ return block ? block->AsPanGestureBlock() : mActivePanGestureBlock.get();
+}
+
+PinchGestureBlockState* InputQueue::GetCurrentPinchGestureBlock() const {
+ InputBlockState* block = GetCurrentBlock();
+ return block ? block->AsPinchGestureBlock() : mActivePinchGestureBlock.get();
+}
+
+KeyboardBlockState* InputQueue::GetCurrentKeyboardBlock() const {
+ InputBlockState* block = GetCurrentBlock();
+ return block ? block->AsKeyboardBlock() : mActiveKeyboardBlock.get();
+}
+
+WheelBlockState* InputQueue::GetActiveWheelTransaction() const {
+ WheelBlockState* block = mActiveWheelBlock.get();
+ if (!block || !block->InTransaction()) {
+ return nullptr;
+ }
+ return block;
+}
+
+bool InputQueue::HasReadyTouchBlock() const {
+ return !mQueuedInputs.IsEmpty() &&
+ mQueuedInputs[0]->Block()->AsTouchBlock() &&
+ mQueuedInputs[0]->Block()->AsTouchBlock()->IsReadyForHandling();
+}
+
+bool InputQueue::AllowScrollHandoff() const {
+ if (GetCurrentWheelBlock()) {
+ return GetCurrentWheelBlock()->AllowScrollHandoff();
+ }
+ if (GetCurrentPanGestureBlock()) {
+ return GetCurrentPanGestureBlock()->AllowScrollHandoff();
+ }
+ if (GetCurrentKeyboardBlock()) {
+ return GetCurrentKeyboardBlock()->AllowScrollHandoff();
+ }
+ return true;
+}
+
+bool InputQueue::IsDragOnScrollbar(bool aHitScrollbar) {
+ if (!mDragTracker.InDrag()) {
+ return false;
+ }
+ // Now that we know we are in a drag, get the info from the drag tracker.
+ // We keep it in the tracker rather than the block because the block can get
+ // interrupted by something else (like a wheel event) and then a new block
+ // will get created without the info we want. The tracker will persist though.
+ return mDragTracker.IsOnScrollbar(aHitScrollbar);
+}
+
+void InputQueue::ScheduleMainThreadTimeout(
+ const RefPtr<AsyncPanZoomController>& aTarget,
+ CancelableBlockState* aBlock) {
+ INPQ_LOG("scheduling main thread timeout for target %p\n", aTarget.get());
+ RefPtr<Runnable> timeoutTask = NewRunnableMethod<uint64_t>(
+ "layers::InputQueue::MainThreadTimeout", this,
+ &InputQueue::MainThreadTimeout, aBlock->GetBlockId());
+ int32_t timeout = StaticPrefs::apz_content_response_timeout();
+ if (timeout == 0) {
+ // If the timeout is zero, treat it as a request to ignore any main
+ // thread confirmation and unconditionally use fallback behaviour for
+ // when a timeout is reached. This codepath is used by tests that
+ // want to exercise the fallback behaviour.
+ // To ensure the fallback behaviour is used unconditionally, the timeout
+ // is run right away instead of using PostDelayedTask(). However,
+ // we can't run it right here, because MainThreadTimeout() expects that
+ // the input block has at least one input event in mQueuedInputs, and
+ // the event that triggered this call may not have been added to
+ // mQueuedInputs yet.
+ mImmediateTimeout = std::move(timeoutTask);
+ } else {
+ aTarget->PostDelayedTask(timeoutTask.forget(), timeout);
+ }
+}
+
+InputBlockState* InputQueue::GetBlockForId(uint64_t aInputBlockId) {
+ return FindBlockForId(aInputBlockId, nullptr);
+}
+
+void InputQueue::AddInputBlockCallback(uint64_t aInputBlockId,
+ InputBlockCallbackInfo&& aCallbackInfo) {
+ mInputBlockCallbacks.insert(InputBlockCallbackMap::value_type(
+ aInputBlockId, std::move(aCallbackInfo)));
+}
+
+InputBlockState* InputQueue::FindBlockForId(uint64_t aInputBlockId,
+ InputData** aOutFirstInput) {
+ for (const auto& queuedInput : mQueuedInputs) {
+ if (queuedInput->Block()->GetBlockId() == aInputBlockId) {
+ if (aOutFirstInput) {
+ *aOutFirstInput = queuedInput->Input();
+ }
+ return queuedInput->Block();
+ }
+ }
+
+ InputBlockState* block = nullptr;
+ if (mActiveTouchBlock && mActiveTouchBlock->GetBlockId() == aInputBlockId) {
+ block = mActiveTouchBlock.get();
+ } else if (mActiveWheelBlock &&
+ mActiveWheelBlock->GetBlockId() == aInputBlockId) {
+ block = mActiveWheelBlock.get();
+ } else if (mActiveDragBlock &&
+ mActiveDragBlock->GetBlockId() == aInputBlockId) {
+ block = mActiveDragBlock.get();
+ } else if (mActivePanGestureBlock &&
+ mActivePanGestureBlock->GetBlockId() == aInputBlockId) {
+ block = mActivePanGestureBlock.get();
+ } else if (mActivePinchGestureBlock &&
+ mActivePinchGestureBlock->GetBlockId() == aInputBlockId) {
+ block = mActivePinchGestureBlock.get();
+ } else if (mActiveKeyboardBlock &&
+ mActiveKeyboardBlock->GetBlockId() == aInputBlockId) {
+ block = mActiveKeyboardBlock.get();
+ }
+ // Since we didn't encounter this block while iterating through mQueuedInputs,
+ // it must have no events associated with it at the moment.
+ if (aOutFirstInput) {
+ *aOutFirstInput = nullptr;
+ }
+ return block;
+}
+
+void InputQueue::MainThreadTimeout(uint64_t aInputBlockId) {
+ // It's possible that this function gets called after the controller thread
+ // was discarded during shutdown.
+ if (!APZThreadUtils::IsControllerThreadAlive()) {
+ return;
+ }
+ APZThreadUtils::AssertOnControllerThread();
+
+ INPQ_LOG("got a main thread timeout; block=%" PRIu64 "\n", aInputBlockId);
+ bool success = false;
+ InputData* firstInput = nullptr;
+ InputBlockState* inputBlock = FindBlockForId(aInputBlockId, &firstInput);
+ if (inputBlock && inputBlock->AsCancelableBlock()) {
+ CancelableBlockState* block = inputBlock->AsCancelableBlock();
+ // time out the touch-listener response and also confirm the existing
+ // target apzc in the case where the main thread doesn't get back to us
+ // fast enough.
+ success = block->TimeoutContentResponse();
+ success |= block->SetConfirmedTargetApzc(
+ block->GetTargetApzc(),
+ InputBlockState::TargetConfirmationState::eTimedOut, firstInput,
+ // This actually could be a scrollbar drag, but we pass
+ // aForScrollbarDrag=false because for scrollbar drags,
+ // SetConfirmedTargetApzc() will also be called by ConfirmDragBlock(),
+ // and we pass aForScrollbarDrag=true there.
+ false);
+ } else if (inputBlock) {
+ NS_WARNING("input block is not a cancelable block");
+ }
+ if (success) {
+ ProcessQueue();
+ }
+}
+
+void InputQueue::MaybeLongTapTimeout(uint64_t aInputBlockId) {
+ // It's possible that this function gets called after the controller thread
+ // was discarded during shutdown.
+ if (!APZThreadUtils::IsControllerThreadAlive()) {
+ return;
+ }
+ APZThreadUtils::AssertOnControllerThread();
+
+ INPQ_LOG("got a maybe-long-tap timeout; block=%" PRIu64 "\n", aInputBlockId);
+
+ InputBlockState* inputBlock = FindBlockForId(aInputBlockId, nullptr);
+ MOZ_ASSERT(!inputBlock || inputBlock->AsTouchBlock());
+ if (inputBlock && inputBlock->AsTouchBlock()->IsInSlop()) {
+ // If the block is still in slop, it won't have sent a touchmove to content
+ // and so content will not have sent a content response. But also it means
+ // the touchstart should trigger a long-press gesture so let's force the
+ // block to get processed now.
+ MainThreadTimeout(aInputBlockId);
+ }
+}
+
+void InputQueue::ContentReceivedInputBlock(uint64_t aInputBlockId,
+ bool aPreventDefault) {
+ APZThreadUtils::AssertOnControllerThread();
+
+ INPQ_LOG("got a content response; block=%" PRIu64 " preventDefault=%d\n",
+ aInputBlockId, aPreventDefault);
+ bool success = false;
+ InputBlockState* inputBlock = FindBlockForId(aInputBlockId, nullptr);
+ if (inputBlock && inputBlock->AsCancelableBlock()) {
+ CancelableBlockState* block = inputBlock->AsCancelableBlock();
+ success = block->SetContentResponse(aPreventDefault);
+ } else if (inputBlock) {
+ NS_WARNING("input block is not a cancelable block");
+ }
+ if (success) {
+ ProcessQueue();
+ }
+}
+
+void InputQueue::SetConfirmedTargetApzc(
+ uint64_t aInputBlockId, const RefPtr<AsyncPanZoomController>& aTargetApzc) {
+ APZThreadUtils::AssertOnControllerThread();
+
+ INPQ_LOG("got a target apzc; block=%" PRIu64 " guid=%s\n", aInputBlockId,
+ aTargetApzc ? ToString(aTargetApzc->GetGuid()).c_str() : "");
+ bool success = false;
+ InputData* firstInput = nullptr;
+ InputBlockState* inputBlock = FindBlockForId(aInputBlockId, &firstInput);
+ if (inputBlock && inputBlock->AsCancelableBlock()) {
+ CancelableBlockState* block = inputBlock->AsCancelableBlock();
+ success = block->SetConfirmedTargetApzc(
+ aTargetApzc, InputBlockState::TargetConfirmationState::eConfirmed,
+ firstInput,
+ // This actually could be a scrollbar drag, but we pass
+ // aForScrollbarDrag=false because for scrollbar drags,
+ // SetConfirmedTargetApzc() will also be called by ConfirmDragBlock(),
+ // and we pass aForScrollbarDrag=true there.
+ false);
+ } else if (inputBlock) {
+ NS_WARNING("input block is not a cancelable block");
+ }
+ if (success) {
+ ProcessQueue();
+ }
+}
+
+void InputQueue::ConfirmDragBlock(
+ uint64_t aInputBlockId, const RefPtr<AsyncPanZoomController>& aTargetApzc,
+ const AsyncDragMetrics& aDragMetrics) {
+ APZThreadUtils::AssertOnControllerThread();
+
+ INPQ_LOG("got a target apzc; block=%" PRIu64 " guid=%s dragtarget=%" PRIu64
+ "\n",
+ aInputBlockId,
+ aTargetApzc ? ToString(aTargetApzc->GetGuid()).c_str() : "",
+ aDragMetrics.mViewId);
+ bool success = false;
+ InputData* firstInput = nullptr;
+ InputBlockState* inputBlock = FindBlockForId(aInputBlockId, &firstInput);
+ if (inputBlock && inputBlock->AsDragBlock()) {
+ DragBlockState* block = inputBlock->AsDragBlock();
+ block->SetDragMetrics(aDragMetrics);
+ success = block->SetConfirmedTargetApzc(
+ aTargetApzc, InputBlockState::TargetConfirmationState::eConfirmed,
+ firstInput,
+ /* aForScrollbarDrag = */ true);
+ }
+ if (success) {
+ ProcessQueue();
+ }
+}
+
+void InputQueue::SetAllowedTouchBehavior(
+ uint64_t aInputBlockId, const nsTArray<TouchBehaviorFlags>& aBehaviors) {
+ APZThreadUtils::AssertOnControllerThread();
+
+ INPQ_LOG("got allowed touch behaviours; block=%" PRIu64 "\n", aInputBlockId);
+ bool success = false;
+ InputBlockState* inputBlock = FindBlockForId(aInputBlockId, nullptr);
+ if (inputBlock && inputBlock->AsTouchBlock()) {
+ TouchBlockState* block = inputBlock->AsTouchBlock();
+ success = block->SetAllowedTouchBehaviors(aBehaviors);
+ } else if (inputBlock) {
+ NS_WARNING("input block is not a touch block");
+ }
+ if (success) {
+ ProcessQueue();
+ }
+}
+
+void InputQueue::SetBrowserGestureResponse(uint64_t aInputBlockId,
+ BrowserGestureResponse aResponse) {
+ InputBlockState* inputBlock = FindBlockForId(aInputBlockId, nullptr);
+
+ if (inputBlock && inputBlock->AsPanGestureBlock()) {
+ PanGestureBlockState* block = inputBlock->AsPanGestureBlock();
+ block->SetBrowserGestureResponse(aResponse);
+ } else if (inputBlock) {
+ NS_WARNING("input block is not a pan gesture block");
+ }
+ ProcessQueue();
+}
+
+static APZHandledResult GetHandledResultFor(
+ const AsyncPanZoomController* aApzc,
+ const InputBlockState& aCurrentInputBlock, nsEventStatus aEagerStatus) {
+ if (aCurrentInputBlock.ShouldDropEvents()) {
+ return APZHandledResult{APZHandledPlace::HandledByContent, aApzc};
+ }
+
+ if (!aApzc) {
+ return APZHandledResult{APZHandledPlace::HandledByContent, aApzc};
+ }
+
+ if (aApzc->IsRootContent()) {
+ // If the eager status was eIgnore, we would have returned an eager result
+ // of Unhandled if there had been no event handler. Now that we know the
+ // event handler did not preventDefault() the input block, return Unhandled
+ // as the delayed result.
+ // FIXME: A more accurate implementation would be to re-do the entire
+ // computation that determines the status (i.e. calling
+ // ArePointerEventsConsumable()) with the confirmed target APZC.
+ return (aEagerStatus == nsEventStatus_eConsumeDoDefault &&
+ aApzc->CanVerticalScrollWithDynamicToolbar())
+ ? APZHandledResult{APZHandledPlace::HandledByRoot, aApzc}
+ : APZHandledResult{APZHandledPlace::Unhandled, aApzc};
+ }
+
+ auto [result, rootApzc] = aCurrentInputBlock.GetOverscrollHandoffChain()
+ ->ScrollingDownWillMoveDynamicToolbar(aApzc);
+ if (!result) {
+ return APZHandledResult{APZHandledPlace::HandledByContent, aApzc};
+ }
+
+ // Return `HandledByRoot` if scroll positions in all relevant APZC are at the
+ // bottom edge and if there are contents covered by the dynamic toolbar.
+ MOZ_ASSERT(rootApzc && rootApzc->IsRootContent());
+ return APZHandledResult{APZHandledPlace::HandledByRoot, rootApzc};
+}
+
+void InputQueue::ProcessQueue() {
+ APZThreadUtils::AssertOnControllerThread();
+
+ while (!mQueuedInputs.IsEmpty()) {
+ InputBlockState* curBlock = mQueuedInputs[0]->Block();
+ CancelableBlockState* cancelable = curBlock->AsCancelableBlock();
+ if (cancelable && !cancelable->IsReadyForHandling()) {
+ break;
+ }
+
+ INPQ_LOG(
+ "processing input from block %p; preventDefault %d shouldDropEvents %d "
+ "target %p\n",
+ curBlock, cancelable && cancelable->IsDefaultPrevented(),
+ curBlock->ShouldDropEvents(), curBlock->GetTargetApzc().get());
+ RefPtr<AsyncPanZoomController> target = curBlock->GetTargetApzc();
+
+ // If there is an input block callback registered for this
+ // input block, invoke it.
+ auto it = mInputBlockCallbacks.find(curBlock->GetBlockId());
+ if (it != mInputBlockCallbacks.end()) {
+ APZHandledResult handledResult =
+ GetHandledResultFor(target, *curBlock, it->second.mEagerStatus);
+ it->second.mCallback(curBlock->GetBlockId(), handledResult);
+ // The callback is one-shot; discard it after calling it.
+ mInputBlockCallbacks.erase(it);
+ }
+
+ // target may be null here if the initial target was unconfirmed and then
+ // we later got a confirmed null target. in that case drop the events.
+ if (target) {
+ // If the event is targeting a different APZC than the previous one,
+ // we want to clear the previous APZC's gesture state regardless of
+ // whether we're actually dispatching the event or not.
+ if (mLastActiveApzc && mLastActiveApzc != target &&
+ mTouchCounter.GetActiveTouchCount() > 0) {
+ mLastActiveApzc->ResetTouchInputState();
+ }
+ if (curBlock->ShouldDropEvents()) {
+ if (curBlock->AsTouchBlock()) {
+ target->ResetTouchInputState();
+ } else if (curBlock->AsPanGestureBlock()) {
+ target->ResetPanGestureInputState();
+ }
+ } else {
+ UpdateActiveApzc(target);
+ curBlock->DispatchEvent(*(mQueuedInputs[0]->Input()));
+ }
+ }
+ mQueuedInputs.RemoveElementAt(0);
+ }
+
+ if (CanDiscardBlock(mActiveTouchBlock)) {
+ mActiveTouchBlock = nullptr;
+ }
+ if (CanDiscardBlock(mActiveWheelBlock)) {
+ mActiveWheelBlock = nullptr;
+ }
+ if (CanDiscardBlock(mActiveDragBlock)) {
+ mActiveDragBlock = nullptr;
+ }
+ if (CanDiscardBlock(mActivePanGestureBlock)) {
+ mActivePanGestureBlock = nullptr;
+ }
+ if (CanDiscardBlock(mActivePinchGestureBlock)) {
+ mActivePinchGestureBlock = nullptr;
+ }
+ if (CanDiscardBlock(mActiveKeyboardBlock)) {
+ mActiveKeyboardBlock = nullptr;
+ }
+}
+
+bool InputQueue::CanDiscardBlock(InputBlockState* aBlock) {
+ if (!aBlock ||
+ (aBlock->AsCancelableBlock() &&
+ !aBlock->AsCancelableBlock()->IsReadyForHandling()) ||
+ aBlock->MustStayActive()) {
+ return false;
+ }
+ InputData* firstInput = nullptr;
+ FindBlockForId(aBlock->GetBlockId(), &firstInput);
+ if (firstInput) {
+ // The block has at least one input event still in the queue, so it's
+ // not depleted
+ return false;
+ }
+ return true;
+}
+
+void InputQueue::UpdateActiveApzc(
+ const RefPtr<AsyncPanZoomController>& aNewActive) {
+ mLastActiveApzc = aNewActive;
+}
+
+void InputQueue::Clear() {
+ // On Android, where the controller thread is the Android UI thread,
+ // it's possible for this to be called after the main thread has
+ // already run the shutdown task that clears the state used to
+ // implement APZThreadUtils::AssertOnControllerThread().
+ // In such cases, we still want to perform the cleanup.
+ if (APZThreadUtils::IsControllerThreadAlive()) {
+ APZThreadUtils::AssertOnControllerThread();
+ }
+
+ mQueuedInputs.Clear();
+ mActiveTouchBlock = nullptr;
+ mActiveWheelBlock = nullptr;
+ mActiveDragBlock = nullptr;
+ mActivePanGestureBlock = nullptr;
+ mActivePinchGestureBlock = nullptr;
+ mActiveKeyboardBlock = nullptr;
+ mLastActiveApzc = nullptr;
+}
+
+InputQueue::AutoRunImmediateTimeout::AutoRunImmediateTimeout(InputQueue* aQueue)
+ : mQueue(aQueue) {
+ MOZ_ASSERT(!mQueue->mImmediateTimeout);
+}
+
+InputQueue::AutoRunImmediateTimeout::~AutoRunImmediateTimeout() {
+ if (mQueue->mImmediateTimeout) {
+ mQueue->mImmediateTimeout->Run();
+ mQueue->mImmediateTimeout = nullptr;
+ }
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/InputQueue.h b/gfx/layers/apz/src/InputQueue.h
new file mode 100644
index 0000000000..8a015c24f3
--- /dev/null
+++ b/gfx/layers/apz/src/InputQueue.h
@@ -0,0 +1,277 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_InputQueue_h
+#define mozilla_layers_InputQueue_h
+
+#include "APZUtils.h"
+#include "DragTracker.h"
+#include "InputData.h"
+#include "mozilla/EventForwards.h"
+#include "mozilla/layers/TouchCounter.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/UniquePtr.h"
+#include "nsTArray.h"
+
+#include <unordered_map>
+
+namespace mozilla {
+
+class InputData;
+class MultiTouchInput;
+class ScrollWheelInput;
+
+namespace layers {
+
+class AsyncPanZoomController;
+class InputBlockState;
+class CancelableBlockState;
+class TouchBlockState;
+class WheelBlockState;
+class DragBlockState;
+class PanGestureBlockState;
+class PinchGestureBlockState;
+class KeyboardBlockState;
+class AsyncDragMetrics;
+class QueuedInput;
+struct APZEventResult;
+struct APZHandledResult;
+enum class BrowserGestureResponse : bool;
+
+using InputBlockCallback = std::function<void(uint64_t aInputBlockId,
+ APZHandledResult aHandledResult)>;
+
+struct InputBlockCallbackInfo {
+ nsEventStatus mEagerStatus;
+ InputBlockCallback mCallback;
+};
+
+/**
+ * This class stores incoming input events, associated with "input blocks",
+ * until they are ready for handling.
+ */
+class InputQueue {
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(InputQueue)
+
+ public:
+ InputQueue();
+
+ /**
+ * Notifies the InputQueue of a new incoming input event. The APZC that the
+ * input event was targeted to should be provided in the |aTarget| parameter.
+ * See the documentation on APZCTreeManager::ReceiveInputEvent for info on
+ * return values from this function.
+ */
+ APZEventResult ReceiveInputEvent(
+ const RefPtr<AsyncPanZoomController>& aTarget,
+ TargetConfirmationFlags aFlags, InputData& aEvent,
+ const Maybe<nsTArray<TouchBehaviorFlags>>& aTouchBehaviors = Nothing());
+ /**
+ * This function should be invoked to notify the InputQueue when web content
+ * decides whether or not it wants to cancel a block of events. The block
+ * id to which this applies should be provided in |aInputBlockId|.
+ */
+ void ContentReceivedInputBlock(uint64_t aInputBlockId, bool aPreventDefault);
+ /**
+ * This function should be invoked to notify the InputQueue once the target
+ * APZC to handle an input block has been confirmed. In practice this should
+ * generally be decidable upon receipt of the input event, but in some cases
+ * we may need to query the layout engine to know for sure. The input block
+ * this applies to should be specified via the |aInputBlockId| parameter.
+ */
+ void SetConfirmedTargetApzc(
+ uint64_t aInputBlockId,
+ const RefPtr<AsyncPanZoomController>& aTargetApzc);
+ /**
+ * This function is invoked to confirm that the drag block should be handled
+ * by the APZ.
+ */
+ void ConfirmDragBlock(uint64_t aInputBlockId,
+ const RefPtr<AsyncPanZoomController>& aTargetApzc,
+ const AsyncDragMetrics& aDragMetrics);
+ /**
+ * This function should be invoked to notify the InputQueue of the touch-
+ * action properties for the different touch points in an input block. The
+ * input block this applies to should be specified by the |aInputBlockId|
+ * parameter. If touch-action is not enabled on the platform, this function
+ * does nothing and need not be called.
+ */
+ void SetAllowedTouchBehavior(uint64_t aInputBlockId,
+ const nsTArray<TouchBehaviorFlags>& aBehaviors);
+ /**
+ * Adds a new touch block at the end of the input queue that has the same
+ * allowed touch behaviour flags as the the touch block currently being
+ * processed. This should only be called when processing of a touch block
+ * triggers the creation of a new touch block. Returns the input block id
+ * of the the newly-created block.
+ */
+ uint64_t InjectNewTouchBlock(AsyncPanZoomController* aTarget);
+ /**
+ * Returns the pending input block at the head of the queue, if there is one.
+ * This may return null if there all input events have been processed.
+ */
+ InputBlockState* GetCurrentBlock() const;
+ /*
+ * Returns the current pending input block as a specific kind of block. If
+ * GetCurrentBlock() returns null, these functions additionally check the
+ * mActiveXXXBlock field of the corresponding input type to see if there is
+ * a depleted but still active input block, and returns that if found. These
+ * functions may return null if no block is found.
+ */
+ TouchBlockState* GetCurrentTouchBlock() const;
+ WheelBlockState* GetCurrentWheelBlock() const;
+ DragBlockState* GetCurrentDragBlock() const;
+ PanGestureBlockState* GetCurrentPanGestureBlock() const;
+ PinchGestureBlockState* GetCurrentPinchGestureBlock() const;
+ KeyboardBlockState* GetCurrentKeyboardBlock() const;
+ /**
+ * Returns true iff the pending block at the head of the queue is a touch
+ * block and is ready for handling.
+ */
+ bool HasReadyTouchBlock() const;
+ /**
+ * If there is an active wheel transaction, returns the WheelBlockState
+ * representing the transaction. Otherwise, returns null. "Active" in this
+ * function name is the same kind of "active" as in mActiveWheelBlock - that
+ * is, new incoming wheel events will go into the "active" block.
+ */
+ WheelBlockState* GetActiveWheelTransaction() const;
+ /**
+ * Remove all input blocks from the input queue.
+ */
+ void Clear();
+ /**
+ * Whether the current pending block allows scroll handoff.
+ */
+ bool AllowScrollHandoff() const;
+ /**
+ * If there is currently a drag in progress, return whether or not it was
+ * targeted at a scrollbar. If the drag was newly-created and doesn't know,
+ * use the provided |aOnScrollbar| to populate that information.
+ */
+ bool IsDragOnScrollbar(bool aOnScrollbar);
+
+ InputBlockState* GetBlockForId(uint64_t aInputBlockId);
+
+ void AddInputBlockCallback(uint64_t aInputBlockId,
+ InputBlockCallbackInfo&& aCallback);
+
+ void SetBrowserGestureResponse(uint64_t aInputBlockId,
+ BrowserGestureResponse aResponse);
+
+ private:
+ ~InputQueue();
+
+ // RAII class for automatically running a timeout task that may
+ // need to be run immediately after an event has been queued.
+ class AutoRunImmediateTimeout final {
+ public:
+ explicit AutoRunImmediateTimeout(InputQueue* aQueue);
+ ~AutoRunImmediateTimeout();
+
+ private:
+ InputQueue* mQueue;
+ };
+
+ TouchBlockState* StartNewTouchBlock(
+ const RefPtr<AsyncPanZoomController>& aTarget,
+ TargetConfirmationFlags aFlags, bool aCopyPropertiesFromCurrent);
+
+ /**
+ * If animations are present for the current pending input block, cancel
+ * them as soon as possible.
+ */
+ void CancelAnimationsForNewBlock(InputBlockState* aBlock,
+ CancelAnimationFlags aExtraFlags = Default);
+
+ /**
+ * If we need to wait for a content response, schedule that now. Returns true
+ * if the timeout was scheduled, false otherwise.
+ */
+ bool MaybeRequestContentResponse(
+ const RefPtr<AsyncPanZoomController>& aTarget,
+ CancelableBlockState* aBlock);
+
+ APZEventResult ReceiveTouchInput(
+ const RefPtr<AsyncPanZoomController>& aTarget,
+ TargetConfirmationFlags aFlags, const MultiTouchInput& aEvent,
+ const Maybe<nsTArray<TouchBehaviorFlags>>& aTouchBehaviors);
+ APZEventResult ReceiveMouseInput(
+ const RefPtr<AsyncPanZoomController>& aTarget,
+ TargetConfirmationFlags aFlags, MouseInput& aEvent);
+ APZEventResult ReceiveScrollWheelInput(
+ const RefPtr<AsyncPanZoomController>& aTarget,
+ TargetConfirmationFlags aFlags, const ScrollWheelInput& aEvent);
+ APZEventResult ReceivePanGestureInput(
+ const RefPtr<AsyncPanZoomController>& aTarget,
+ TargetConfirmationFlags aFlags, const PanGestureInput& aEvent);
+ APZEventResult ReceivePinchGestureInput(
+ const RefPtr<AsyncPanZoomController>& aTarget,
+ TargetConfirmationFlags aFlags, const PinchGestureInput& aEvent);
+ APZEventResult ReceiveKeyboardInput(
+ const RefPtr<AsyncPanZoomController>& aTarget,
+ TargetConfirmationFlags aFlags, const KeyboardInput& aEvent);
+
+ /**
+ * Helper function that searches mQueuedInputs for the first block matching
+ * the given id, and returns it. If |aOutFirstInput| is non-null, it is
+ * populated with a pointer to the first input in mQueuedInputs that
+ * corresponds to the block, or null if no such input was found. Note that
+ * even if there are no inputs in mQueuedInputs, this function can return
+ * non-null if the block id provided matches one of the depleted-but-still-
+ * active blocks (mActiveTouchBlock, mActiveWheelBlock, etc.).
+ */
+ InputBlockState* FindBlockForId(uint64_t aInputBlockId,
+ InputData** aOutFirstInput);
+ void ScheduleMainThreadTimeout(const RefPtr<AsyncPanZoomController>& aTarget,
+ CancelableBlockState* aBlock);
+ void MainThreadTimeout(uint64_t aInputBlockId);
+ void MaybeLongTapTimeout(uint64_t aInputBlockId);
+ void ProcessQueue();
+ bool CanDiscardBlock(InputBlockState* aBlock);
+ void UpdateActiveApzc(const RefPtr<AsyncPanZoomController>& aNewActive);
+
+ private:
+ // The queue of input events that have not yet been fully processed.
+ // This member must only be accessed on the controller/UI thread.
+ nsTArray<UniquePtr<QueuedInput>> mQueuedInputs;
+
+ // These are the most recently created blocks of each input type. They are
+ // "active" in the sense that new inputs of that type are associated with
+ // them. Note that these pointers may be null if no inputs of the type have
+ // arrived, or if the inputs for the type formed a complete block that was
+ // then discarded.
+ RefPtr<TouchBlockState> mActiveTouchBlock;
+ RefPtr<WheelBlockState> mActiveWheelBlock;
+ RefPtr<DragBlockState> mActiveDragBlock;
+ RefPtr<PanGestureBlockState> mActivePanGestureBlock;
+ RefPtr<PinchGestureBlockState> mActivePinchGestureBlock;
+ RefPtr<KeyboardBlockState> mActiveKeyboardBlock;
+
+ // The APZC to which the last event was delivered
+ RefPtr<AsyncPanZoomController> mLastActiveApzc;
+
+ // Track touches so we know when to clear mLastActiveApzc
+ TouchCounter mTouchCounter;
+
+ // Track mouse inputs so we know if we're in a drag or not
+ DragTracker mDragTracker;
+
+ // Temporarily stores a timeout task that needs to be run as soon as
+ // as the event that triggered it has been queued.
+ RefPtr<Runnable> mImmediateTimeout;
+
+ // Maps input block ids to callbacks that will be invoked when the input block
+ // is ready for handling.
+ using InputBlockCallbackMap =
+ std::unordered_map<uint64_t, InputBlockCallbackInfo>;
+ InputBlockCallbackMap mInputBlockCallbacks;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_InputQueue_h
diff --git a/gfx/layers/apz/src/KeyboardMap.cpp b/gfx/layers/apz/src/KeyboardMap.cpp
new file mode 100644
index 0000000000..9444037be6
--- /dev/null
+++ b/gfx/layers/apz/src/KeyboardMap.cpp
@@ -0,0 +1,170 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/layers/KeyboardMap.h"
+
+#include "mozilla/TextEvents.h" // for IgnoreModifierState, ShortcutKeyCandidate
+
+namespace mozilla {
+namespace layers {
+
+KeyboardShortcut::KeyboardShortcut()
+ : mKeyCode(0),
+ mCharCode(0),
+ mModifiers(0),
+ mModifiersMask(0),
+ mEventType(KeyboardInput::KeyboardEventType::KEY_OTHER),
+ mDispatchToContent(false) {}
+
+KeyboardShortcut::KeyboardShortcut(KeyboardInput::KeyboardEventType aEventType,
+ uint32_t aKeyCode, uint32_t aCharCode,
+ Modifiers aModifiers,
+ Modifiers aModifiersMask,
+ const KeyboardScrollAction& aAction)
+ : mAction(aAction),
+ mKeyCode(aKeyCode),
+ mCharCode(aCharCode),
+ mModifiers(aModifiers),
+ mModifiersMask(aModifiersMask),
+ mEventType(aEventType),
+ mDispatchToContent(false) {}
+
+KeyboardShortcut::KeyboardShortcut(KeyboardInput::KeyboardEventType aEventType,
+ uint32_t aKeyCode, uint32_t aCharCode,
+ Modifiers aModifiers,
+ Modifiers aModifiersMask)
+ : mKeyCode(aKeyCode),
+ mCharCode(aCharCode),
+ mModifiers(aModifiers),
+ mModifiersMask(aModifiersMask),
+ mEventType(aEventType),
+ mDispatchToContent(true) {}
+
+/* static */
+void KeyboardShortcut::AppendHardcodedShortcuts(
+ nsTArray<KeyboardShortcut>& aShortcuts) {
+ // Tab
+ KeyboardShortcut tab1;
+ tab1.mDispatchToContent = true;
+ tab1.mKeyCode = NS_VK_TAB;
+ tab1.mCharCode = 0;
+ tab1.mModifiers = 0;
+ tab1.mModifiersMask = 0;
+ tab1.mEventType = KeyboardInput::KEY_PRESS;
+ aShortcuts.AppendElement(tab1);
+
+ // F6
+ KeyboardShortcut tab2;
+ tab2.mDispatchToContent = true;
+ tab2.mKeyCode = NS_VK_F6;
+ tab2.mCharCode = 0;
+ tab2.mModifiers = 0;
+ tab2.mModifiersMask = 0;
+ tab2.mEventType = KeyboardInput::KEY_PRESS;
+ aShortcuts.AppendElement(tab2);
+}
+
+bool KeyboardShortcut::Matches(const KeyboardInput& aInput,
+ const IgnoreModifierState& aIgnore,
+ uint32_t aOverrideCharCode) const {
+ return mEventType == aInput.mType && MatchesKey(aInput, aOverrideCharCode) &&
+ MatchesModifiers(aInput, aIgnore);
+}
+
+bool KeyboardShortcut::MatchesKey(const KeyboardInput& aInput,
+ uint32_t aOverrideCharCode) const {
+ // Compare by the key code if we have one
+ if (!mCharCode) {
+ return mKeyCode == aInput.mKeyCode;
+ }
+
+ // We are comparing by char code
+ uint32_t charCode;
+
+ // If we are comparing against a shortcut candidate then we might
+ // have an override char code
+ if (aOverrideCharCode) {
+ charCode = aOverrideCharCode;
+ } else {
+ charCode = aInput.mCharCode;
+ }
+
+ // Both char codes must be in lowercase to compare correctly
+ if (IS_IN_BMP(charCode)) {
+ charCode = ToLowerCase(static_cast<char16_t>(charCode));
+ }
+
+ return mCharCode == charCode;
+}
+
+bool KeyboardShortcut::MatchesModifiers(
+ const KeyboardInput& aInput, const IgnoreModifierState& aIgnore) const {
+ Modifiers modifiersMask = mModifiersMask;
+
+ // If we are ignoring Shift or OS, then unset that part of the mask
+ if (aIgnore.mOS) {
+ modifiersMask &= ~MODIFIER_OS;
+ }
+ if (aIgnore.mShift) {
+ modifiersMask &= ~MODIFIER_SHIFT;
+ }
+
+ // Mask off the modifiers we are ignoring from the keyboard input
+ return (aInput.modifiers & modifiersMask) == mModifiers;
+}
+
+KeyboardMap::KeyboardMap(nsTArray<KeyboardShortcut>&& aShortcuts)
+ : mShortcuts(aShortcuts) {}
+
+KeyboardMap::KeyboardMap() = default;
+
+Maybe<KeyboardShortcut> KeyboardMap::FindMatch(
+ const KeyboardInput& aEvent) const {
+ // If there are no shortcut candidates, then just search with with the
+ // keyboard input
+ if (aEvent.mShortcutCandidates.IsEmpty()) {
+ return FindMatchInternal(aEvent, IgnoreModifierState());
+ }
+
+ // Otherwise do a search with each shortcut candidate in order
+ for (auto& key : aEvent.mShortcutCandidates) {
+ IgnoreModifierState ignoreModifierState;
+ ignoreModifierState.mShift = key.mIgnoreShift;
+
+ auto match = FindMatchInternal(aEvent, ignoreModifierState, key.mCharCode);
+ if (match) {
+ return match;
+ }
+ }
+ return Nothing();
+}
+
+Maybe<KeyboardShortcut> KeyboardMap::FindMatchInternal(
+ const KeyboardInput& aEvent, const IgnoreModifierState& aIgnore,
+ uint32_t aOverrideCharCode) const {
+ for (auto& shortcut : mShortcuts) {
+ if (shortcut.Matches(aEvent, aIgnore, aOverrideCharCode)) {
+ return Some(shortcut);
+ }
+ }
+
+#ifdef XP_WIN
+ // Windows native applications ignore Windows-Logo key state when checking
+ // shortcut keys even if the key is pressed. Therefore, if there is no
+ // shortcut key which exactly matches current modifier state, we should
+ // retry to look for a shortcut key without the Windows-Logo key press.
+ if (!aIgnore.mOS && (aEvent.modifiers & MODIFIER_OS)) {
+ IgnoreModifierState ignoreModifierState(aIgnore);
+ ignoreModifierState.mOS = true;
+ return FindMatchInternal(aEvent, ignoreModifierState, aOverrideCharCode);
+ }
+#endif
+
+ return Nothing();
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/KeyboardMap.h b/gfx/layers/apz/src/KeyboardMap.h
new file mode 100644
index 0000000000..32ec8ea61d
--- /dev/null
+++ b/gfx/layers/apz/src/KeyboardMap.h
@@ -0,0 +1,118 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_KeyboardMap_h
+#define mozilla_layers_KeyboardMap_h
+
+#include <stdint.h> // for uint32_t
+
+#include "InputData.h" // for KeyboardInput
+#include "nsIScrollableFrame.h" // for nsIScrollableFrame::ScrollUnit
+#include "nsTArray.h" // for nsTArray
+#include "mozilla/Maybe.h" // for mozilla::Maybe
+#include "KeyboardScrollAction.h" // for KeyboardScrollAction
+
+namespace mozilla {
+
+struct IgnoreModifierState;
+
+namespace layers {
+
+class KeyboardMap;
+
+/**
+ * This class is an off main-thread <xul:handler> for scrolling commands.
+ */
+class KeyboardShortcut final {
+ public:
+ KeyboardShortcut();
+
+ /**
+ * Create a keyboard shortcut that when matched can be handled by executing
+ * the specified keyboard action.
+ */
+ KeyboardShortcut(KeyboardInput::KeyboardEventType aEventType,
+ uint32_t aKeyCode, uint32_t aCharCode, Modifiers aModifiers,
+ Modifiers aModifiersMask,
+ const KeyboardScrollAction& aAction);
+
+ /**
+ * Create a keyboard shortcut that when matched should be handled by ignoring
+ * the keyboard event and dispatching it to content.
+ */
+ KeyboardShortcut(KeyboardInput::KeyboardEventType aEventType,
+ uint32_t aKeyCode, uint32_t aCharCode, Modifiers aModifiers,
+ Modifiers aModifiersMask);
+
+ /**
+ * There are some default actions for keyboard inputs that are hardcoded in
+ * EventStateManager instead of being represented as XBL handlers. This adds
+ * keyboard shortcuts to match these inputs and dispatch them to content.
+ */
+ static void AppendHardcodedShortcuts(nsTArray<KeyboardShortcut>& aShortcuts);
+
+ protected:
+ friend mozilla::layers::KeyboardMap;
+
+ bool Matches(const KeyboardInput& aInput, const IgnoreModifierState& aIgnore,
+ uint32_t aOverrideCharCode = 0) const;
+
+ private:
+ bool MatchesKey(const KeyboardInput& aInput,
+ uint32_t aOverrideCharCode) const;
+ bool MatchesModifiers(const KeyboardInput& aInput,
+ const IgnoreModifierState& aIgnore) const;
+
+ public:
+ // The action to perform when this shortcut is matched,
+ // and not flagged to be dispatched to content
+ KeyboardScrollAction mAction;
+
+ // Only one of mKeyCode or mCharCode may be non-zero
+ // whichever one is non-zero is the one to compare when matching
+ uint32_t mKeyCode;
+ uint32_t mCharCode;
+
+ // The modifiers that must be active for this shortcut
+ Modifiers mModifiers;
+ // The modifiers to compare when matching this shortcut
+ Modifiers mModifiersMask;
+
+ // The type of keyboard event to match against
+ KeyboardInput::KeyboardEventType mEventType;
+
+ // Whether events matched by this must be dispatched to content
+ bool mDispatchToContent;
+};
+
+/**
+ * A keyboard map is an off main-thread <xul:binding> for scrolling commands.
+ */
+class KeyboardMap final {
+ public:
+ KeyboardMap();
+ explicit KeyboardMap(nsTArray<KeyboardShortcut>&& aShortcuts);
+
+ const nsTArray<KeyboardShortcut>& Shortcuts() const { return mShortcuts; }
+
+ /**
+ * Search through the internal list of shortcuts for a match for the input
+ * event
+ */
+ Maybe<KeyboardShortcut> FindMatch(const KeyboardInput& aEvent) const;
+
+ private:
+ Maybe<KeyboardShortcut> FindMatchInternal(
+ const KeyboardInput& aEvent, const IgnoreModifierState& aIgnore,
+ uint32_t aOverrideCharCode = 0) const;
+
+ CopyableTArray<KeyboardShortcut> mShortcuts;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_KeyboardMap_h
diff --git a/gfx/layers/apz/src/KeyboardScrollAction.cpp b/gfx/layers/apz/src/KeyboardScrollAction.cpp
new file mode 100644
index 0000000000..42d9a8bff2
--- /dev/null
+++ b/gfx/layers/apz/src/KeyboardScrollAction.cpp
@@ -0,0 +1,37 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/layers/KeyboardScrollAction.h"
+
+namespace mozilla {
+namespace layers {
+
+/* static */ ScrollUnit KeyboardScrollAction::GetScrollUnit(
+ KeyboardScrollAction::KeyboardScrollActionType aDeltaType) {
+ switch (aDeltaType) {
+ case KeyboardScrollAction::eScrollCharacter:
+ return ScrollUnit::LINES;
+ case KeyboardScrollAction::eScrollLine:
+ return ScrollUnit::LINES;
+ case KeyboardScrollAction::eScrollPage:
+ return ScrollUnit::PAGES;
+ case KeyboardScrollAction::eScrollComplete:
+ return ScrollUnit::WHOLE;
+ }
+
+ // Silence an overzealous warning
+ return ScrollUnit::WHOLE;
+}
+
+KeyboardScrollAction::KeyboardScrollAction()
+ : mType(KeyboardScrollAction::eScrollCharacter), mForward(false) {}
+
+KeyboardScrollAction::KeyboardScrollAction(KeyboardScrollActionType aType,
+ bool aForward)
+ : mType(aType), mForward(aForward) {}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/KeyboardScrollAction.h b/gfx/layers/apz/src/KeyboardScrollAction.h
new file mode 100644
index 0000000000..780006c1b3
--- /dev/null
+++ b/gfx/layers/apz/src/KeyboardScrollAction.h
@@ -0,0 +1,48 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_KeyboardScrollAction_h
+#define mozilla_layers_KeyboardScrollAction_h
+
+#include <cstdint> // for uint8_t
+
+#include "mozilla/ScrollTypes.h"
+#include "mozilla/DefineEnum.h" // for MOZ_DEFINE_ENUM
+
+namespace mozilla {
+namespace layers {
+
+/**
+ * This class represents a scrolling action to be performed on a scrollable
+ * layer.
+ */
+struct KeyboardScrollAction final {
+ public:
+ // clang-format off
+ MOZ_DEFINE_ENUM_WITH_BASE_AT_CLASS_SCOPE(
+ KeyboardScrollActionType, uint8_t, (
+ eScrollCharacter,
+ eScrollLine,
+ eScrollPage,
+ eScrollComplete
+ ));
+ // clang-format on
+
+ static ScrollUnit GetScrollUnit(KeyboardScrollActionType aDeltaType);
+
+ KeyboardScrollAction();
+ KeyboardScrollAction(KeyboardScrollActionType aType, bool aForward);
+
+ // The type of scroll to perform for this action
+ KeyboardScrollActionType mType;
+ // Whether to scroll forward or backward along the axis of this action type
+ bool mForward;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_KeyboardScrollAction_h
diff --git a/gfx/layers/apz/src/Overscroll.h b/gfx/layers/apz/src/Overscroll.h
new file mode 100644
index 0000000000..1fb7c3e487
--- /dev/null
+++ b/gfx/layers/apz/src/Overscroll.h
@@ -0,0 +1,250 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_Overscroll_h
+#define mozilla_layers_Overscroll_h
+
+#include "AsyncPanZoomAnimation.h"
+#include "AsyncPanZoomController.h"
+#include "mozilla/TimeStamp.h"
+#include "nsThreadUtils.h"
+
+namespace mozilla {
+namespace layers {
+
+// Animation used by GenericOverscrollEffect.
+class OverscrollAnimation : public AsyncPanZoomAnimation {
+ public:
+ OverscrollAnimation(AsyncPanZoomController& aApzc,
+ const ParentLayerPoint& aVelocity,
+ SideBits aOverscrollSideBits)
+ : mApzc(aApzc), mOverscrollSideBits(aOverscrollSideBits) {
+ MOZ_ASSERT(
+ (mOverscrollSideBits & SideBits::eTopBottom) != SideBits::eTopBottom &&
+ (mOverscrollSideBits & SideBits::eLeftRight) !=
+ SideBits::eLeftRight,
+ "Don't allow overscrolling on both sides at the same time");
+ if ((aOverscrollSideBits & SideBits::eLeftRight) != SideBits::eNone) {
+ mApzc.mX.StartOverscrollAnimation(aVelocity.x);
+ }
+ if ((aOverscrollSideBits & SideBits::eTopBottom) != SideBits::eNone) {
+ mApzc.mY.StartOverscrollAnimation(aVelocity.y);
+ }
+ }
+ virtual ~OverscrollAnimation() {
+ mApzc.mX.EndOverscrollAnimation();
+ mApzc.mY.EndOverscrollAnimation();
+ }
+
+ virtual bool DoSample(FrameMetrics& aFrameMetrics,
+ const TimeDuration& aDelta) override {
+ // Can't inline these variables due to short-circuit evaluation.
+ bool continueX = mApzc.mX.IsOverscrollAnimationAlive() &&
+ mApzc.mX.SampleOverscrollAnimation(
+ aDelta, mOverscrollSideBits & SideBits::eLeftRight);
+ bool continueY = mApzc.mY.IsOverscrollAnimationAlive() &&
+ mApzc.mY.SampleOverscrollAnimation(
+ aDelta, mOverscrollSideBits & SideBits::eTopBottom);
+ if (!continueX && !continueY) {
+ // If we got into overscroll from a fling, that fling did not request a
+ // fling snap to avoid a resulting scrollTo from cancelling the overscroll
+ // animation too early. We do still want to request a fling snap, though,
+ // in case the end of the axis at which we're overscrolled is not a valid
+ // snap point, so we request one now. If there are no snap points, this
+ // will do nothing. If there are snap points, we'll get a scrollTo that
+ // snaps us back to the nearest valid snap point. The scroll snapping is
+ // done in a deferred task, otherwise the state change to NOTHING caused
+ // by the overscroll animation ending would clobber a possible state
+ // change to SMOOTH_SCROLL in ScrollSnap().
+ mDeferredTasks.AppendElement(NewRunnableMethod<ScrollSnapFlags>(
+ "layers::AsyncPanZoomController::ScrollSnap", &mApzc,
+ &AsyncPanZoomController::ScrollSnap,
+ ScrollSnapFlags::IntendedDirection |
+ ScrollSnapFlags::IntendedEndPosition));
+ return false;
+ }
+ return true;
+ }
+
+ virtual bool WantsRepaints() override { return false; }
+
+ // Tell the overscroll animation about the pan momentum event. For each axis,
+ // the overscroll animation may start, stop, or continue managing that axis in
+ // response to the pan momentum event
+ void HandlePanMomentum(const ParentLayerPoint& aDisplacement) {
+ float xOverscroll = mApzc.mX.GetOverscroll();
+ if ((xOverscroll > 0 && aDisplacement.x > 0) ||
+ (xOverscroll < 0 && aDisplacement.x < 0)) {
+ if (!mApzc.mX.IsOverscrollAnimationRunning()) {
+ // Start a new overscroll animation on this axis, if there is no
+ // overscroll animation running and if the pan momentum displacement
+ // the pan momentum displacement is the same direction of the current
+ // overscroll.
+ mApzc.mX.StartOverscrollAnimation(mApzc.mX.GetVelocity());
+ mOverscrollSideBits |=
+ xOverscroll > 0 ? SideBits::eRight : SideBits::eLeft;
+ }
+ } else if ((xOverscroll > 0 && aDisplacement.x < 0) ||
+ (xOverscroll < 0 && aDisplacement.x > 0)) {
+ // Otherwise, stop the animation in the direction so that it won't clobber
+ // subsequent pan momentum scrolling.
+ mApzc.mX.EndOverscrollAnimation();
+ }
+
+ // Same as above but for Y axis.
+ float yOverscroll = mApzc.mY.GetOverscroll();
+ if ((yOverscroll > 0 && aDisplacement.y > 0) ||
+ (yOverscroll < 0 && aDisplacement.y < 0)) {
+ if (!mApzc.mY.IsOverscrollAnimationRunning()) {
+ mApzc.mY.StartOverscrollAnimation(mApzc.mY.GetVelocity());
+ mOverscrollSideBits |=
+ yOverscroll > 0 ? SideBits::eBottom : SideBits::eTop;
+ }
+ } else if ((yOverscroll > 0 && aDisplacement.y < 0) ||
+ (yOverscroll < 0 && aDisplacement.y > 0)) {
+ mApzc.mY.EndOverscrollAnimation();
+ }
+ }
+
+ ScrollDirections GetDirections() const {
+ ScrollDirections directions;
+ if (mApzc.mX.IsOverscrollAnimationRunning()) {
+ directions += ScrollDirection::eHorizontal;
+ }
+ if (mApzc.mY.IsOverscrollAnimationRunning()) {
+ directions += ScrollDirection::eVertical;
+ }
+ return directions;
+ };
+
+ OverscrollAnimation* AsOverscrollAnimation() override { return this; }
+
+ bool IsManagingXAxis() const {
+ return mApzc.mX.IsOverscrollAnimationRunning();
+ }
+ bool IsManagingYAxis() const {
+ return mApzc.mY.IsOverscrollAnimationRunning();
+ }
+
+ private:
+ AsyncPanZoomController& mApzc;
+ SideBits mOverscrollSideBits;
+};
+
+// Base class for different overscroll effects;
+class OverscrollEffectBase {
+ public:
+ virtual ~OverscrollEffectBase() = default;
+
+ // Try to increase the amount of overscroll by |aOverscroll|. Limited to
+ // directions contained in |aOverscrollableDirections|. Components of
+ // |aOverscroll| in directions that are successfully consumed are dropped.
+ virtual void ConsumeOverscroll(
+ ParentLayerPoint& aOverscroll,
+ ScrollDirections aOverscrollableDirections) = 0;
+
+ // Relieve overscroll. Depending on the implementation, the relief may
+ // be immediate, or gradual (e.g. after an animation) but this starts
+ // the process. |aVelocity| is the current velocity of the APZC, and
+ // |aOverscrollSideBits| contains the side(s) at which the APZC is
+ // overscrolled.
+ virtual void RelieveOverscroll(const ParentLayerPoint& aVelocity,
+ SideBits aOverscrollSideBits) = 0;
+
+ virtual bool IsOverscrolled() const = 0;
+
+ // Similarly to RelieveOverscroll(), but has immediate effect
+ // (no animation).
+ virtual void ClearOverscroll() = 0;
+};
+
+// A generic overscroll effect, implemented by AsyncPanZoomController itself.
+class GenericOverscrollEffect : public OverscrollEffectBase {
+ public:
+ explicit GenericOverscrollEffect(AsyncPanZoomController& aApzc)
+ : mApzc(aApzc) {}
+
+ void ConsumeOverscroll(ParentLayerPoint& aOverscroll,
+ ScrollDirections aOverscrollableDirections) override {
+ if (aOverscrollableDirections.contains(ScrollDirection::eHorizontal)) {
+ mApzc.mX.OverscrollBy(aOverscroll.x);
+ aOverscroll.x = 0;
+ }
+
+ if (aOverscrollableDirections.contains(ScrollDirection::eVertical)) {
+ mApzc.mY.OverscrollBy(aOverscroll.y);
+ aOverscroll.y = 0;
+ }
+
+ if (!aOverscrollableDirections.isEmpty()) {
+ mApzc.ScheduleComposite();
+ }
+ }
+
+ void RelieveOverscroll(const ParentLayerPoint& aVelocity,
+ SideBits aOverscrollSideBits) override {
+ mApzc.StartOverscrollAnimation(aVelocity, aOverscrollSideBits);
+ }
+
+ bool IsOverscrolled() const override {
+ return mApzc.IsPhysicallyOverscrolled();
+ }
+
+ void ClearOverscroll() override { mApzc.ClearPhysicalOverscroll(); }
+
+ private:
+ AsyncPanZoomController& mApzc;
+};
+
+// A widget-specific overscroll effect, implemented by the widget via
+// GeckoContentController.
+class WidgetOverscrollEffect : public OverscrollEffectBase {
+ public:
+ explicit WidgetOverscrollEffect(AsyncPanZoomController& aApzc)
+ : mApzc(aApzc), mIsOverscrolled(false) {}
+
+ void ConsumeOverscroll(ParentLayerPoint& aOverscroll,
+ ScrollDirections aOverscrollableDirections) override {
+ RefPtr<GeckoContentController> controller =
+ mApzc.GetGeckoContentController();
+ if (controller && !aOverscrollableDirections.isEmpty()) {
+ mIsOverscrolled = true;
+ controller->UpdateOverscrollOffset(mApzc.GetGuid(), aOverscroll.x,
+ aOverscroll.y, mApzc.IsRootContent());
+ aOverscroll = ParentLayerPoint();
+ }
+ }
+
+ void RelieveOverscroll(const ParentLayerPoint& aVelocity,
+ SideBits aOverscrollSideBits) override {
+ RefPtr<GeckoContentController> controller =
+ mApzc.GetGeckoContentController();
+ // From APZC's point of view, consider it to no longer be overscrolled
+ // as soon as RelieveOverscroll() is called. The widget may use a
+ // delay or animation until the relieving of the overscroll is complete,
+ // but we don't have any insight into that.
+ mIsOverscrolled = false;
+ if (controller) {
+ controller->UpdateOverscrollVelocity(mApzc.GetGuid(), aVelocity.x,
+ aVelocity.y, mApzc.IsRootContent());
+ }
+ }
+
+ bool IsOverscrolled() const override { return mIsOverscrolled; }
+
+ void ClearOverscroll() override {
+ RelieveOverscroll(ParentLayerPoint(), SideBits() /* ignored */);
+ }
+
+ private:
+ AsyncPanZoomController& mApzc;
+ bool mIsOverscrolled;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_Overscroll_h
diff --git a/gfx/layers/apz/src/OverscrollHandoffState.cpp b/gfx/layers/apz/src/OverscrollHandoffState.cpp
new file mode 100644
index 0000000000..ed38f6fa7a
--- /dev/null
+++ b/gfx/layers/apz/src/OverscrollHandoffState.cpp
@@ -0,0 +1,228 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "OverscrollHandoffState.h"
+
+#include <algorithm> // for std::stable_sort
+#include "mozilla/Assertions.h"
+#include "mozilla/FloatingPoint.h"
+#include "AsyncPanZoomController.h"
+
+namespace mozilla {
+namespace layers {
+
+OverscrollHandoffChain::~OverscrollHandoffChain() = default;
+
+void OverscrollHandoffChain::Add(AsyncPanZoomController* aApzc) {
+ mChain.push_back(aApzc);
+}
+
+struct CompareByScrollPriority {
+ bool operator()(const RefPtr<AsyncPanZoomController>& a,
+ const RefPtr<AsyncPanZoomController>& b) const {
+ return a->HasScrollgrab() && !b->HasScrollgrab();
+ }
+};
+
+void OverscrollHandoffChain::SortByScrollPriority() {
+ // The sorting being stable ensures that the relative order between
+ // non-scrollgrabbing APZCs remains child -> parent.
+ // (The relative order between scrollgrabbing APZCs will also remain
+ // child -> parent, though that's just an artefact of the implementation
+ // and users of 'scrollgrab' should not rely on this.)
+ std::stable_sort(mChain.begin(), mChain.end(), CompareByScrollPriority());
+}
+
+const RefPtr<AsyncPanZoomController>& OverscrollHandoffChain::GetApzcAtIndex(
+ uint32_t aIndex) const {
+ MOZ_ASSERT(aIndex < Length());
+ return mChain[aIndex];
+}
+
+uint32_t OverscrollHandoffChain::IndexOf(
+ const AsyncPanZoomController* aApzc) const {
+ uint32_t i;
+ for (i = 0; i < Length(); ++i) {
+ if (mChain[i] == aApzc) {
+ break;
+ }
+ }
+ return i;
+}
+
+void OverscrollHandoffChain::ForEachApzc(APZCMethod aMethod) const {
+ for (uint32_t i = 0; i < Length(); ++i) {
+ (mChain[i]->*aMethod)();
+ }
+}
+
+bool OverscrollHandoffChain::AnyApzc(APZCPredicate aPredicate) const {
+ MOZ_ASSERT(Length() > 0);
+ for (uint32_t i = 0; i < Length(); ++i) {
+ if ((mChain[i]->*aPredicate)()) {
+ return true;
+ }
+ }
+ return false;
+}
+
+void OverscrollHandoffChain::FlushRepaints() const {
+ ForEachApzc(&AsyncPanZoomController::FlushRepaintForOverscrollHandoff);
+}
+
+void OverscrollHandoffChain::CancelAnimations(
+ CancelAnimationFlags aFlags) const {
+ MOZ_ASSERT(Length() > 0);
+ for (uint32_t i = 0; i < Length(); ++i) {
+ mChain[i]->CancelAnimation(aFlags);
+ }
+}
+
+void OverscrollHandoffChain::ClearOverscroll() const {
+ ForEachApzc(&AsyncPanZoomController::ClearOverscroll);
+}
+
+void OverscrollHandoffChain::SnapBackOverscrolledApzc(
+ const AsyncPanZoomController* aStart) const {
+ uint32_t i = IndexOf(aStart);
+ for (; i < Length(); ++i) {
+ AsyncPanZoomController* apzc = mChain[i];
+ if (!apzc->IsDestroyed()) {
+ apzc->SnapBackIfOverscrolled();
+ }
+ }
+}
+
+void OverscrollHandoffChain::SnapBackOverscrolledApzcForMomentum(
+ const AsyncPanZoomController* aStart,
+ const ParentLayerPoint& aVelocity) const {
+ uint32_t i = IndexOf(aStart);
+ for (; i < Length(); ++i) {
+ AsyncPanZoomController* apzc = mChain[i];
+ if (!apzc->IsDestroyed()) {
+ apzc->SnapBackIfOverscrolledForMomentum(aVelocity);
+ }
+ }
+}
+
+bool OverscrollHandoffChain::CanBePanned(
+ const AsyncPanZoomController* aApzc) const {
+ // Find |aApzc| in the handoff chain.
+ uint32_t i = IndexOf(aApzc);
+
+ // See whether any APZC in the handoff chain starting from |aApzc|
+ // has room to be panned.
+ for (uint32_t j = i; j < Length(); ++j) {
+ if (mChain[j]->IsPannable()) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool OverscrollHandoffChain::CanScrollInDirection(
+ const AsyncPanZoomController* aApzc, ScrollDirection aDirection) const {
+ // Find |aApzc| in the handoff chain.
+ uint32_t i = IndexOf(aApzc);
+
+ // See whether any APZC in the handoff chain starting from |aApzc|
+ // has room to scroll in the given direction.
+ for (uint32_t j = i; j < Length(); ++j) {
+ if (mChain[j]->CanScroll(aDirection)) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool OverscrollHandoffChain::HasOverscrolledApzc() const {
+ return AnyApzc(&AsyncPanZoomController::IsOverscrolled);
+}
+
+bool OverscrollHandoffChain::HasFastFlungApzc() const {
+ return AnyApzc(&AsyncPanZoomController::IsFlingingFast);
+}
+
+bool OverscrollHandoffChain::HasAutoscrollApzc() const {
+ return AnyApzc(&AsyncPanZoomController::IsAutoscroll);
+}
+
+RefPtr<AsyncPanZoomController> OverscrollHandoffChain::FindFirstScrollable(
+ const InputData& aInput, ScrollDirections* aOutAllowedScrollDirections,
+ IncludeOverscroll aIncludeOverscroll) const {
+ // Start by allowing scrolling in both directions. As we do handoff
+ // overscroll-behavior may restrict one or both of the directions.
+ *aOutAllowedScrollDirections += ScrollDirection::eVertical;
+ *aOutAllowedScrollDirections += ScrollDirection::eHorizontal;
+
+ for (size_t i = 0; i < Length(); i++) {
+ if (mChain[i]->CanScroll(aInput)) {
+ return mChain[i];
+ }
+
+ // If there is any directions we allow overscroll effects on the root
+ // content APZC (i.e. the overscroll-behavior of the root one is not
+ // `none`), we consider the APZC can be scrollable in terms of pan gestures
+ // because it causes overscrolling even if it's not able to scroll to the
+ // direction.
+ if (StaticPrefs::apz_overscroll_enabled() && bool(aIncludeOverscroll) &&
+ // FIXME: Bug 1707491: Drop this pan gesture input check.
+ aInput.mInputType == PANGESTURE_INPUT && mChain[i]->IsRootContent()) {
+ // Check whether the root content APZC is also overscrollable governed by
+ // overscroll-behavior in the same directions where we allow scrolling
+ // handoff and where we are going to scroll, if it matches we do handoff
+ // to the root content APZC.
+ // In other words, if the root content is not scrollable, we don't
+ // handoff.
+ ScrollDirections allowedOverscrollDirections =
+ mChain[i]->GetOverscrollableDirections();
+ ParentLayerPoint delta = mChain[i]->GetDeltaForEvent(aInput);
+ if (mChain[i]->IsZero(delta.x)) {
+ allowedOverscrollDirections -= ScrollDirection::eHorizontal;
+ }
+ if (mChain[i]->IsZero(delta.y)) {
+ allowedOverscrollDirections -= ScrollDirection::eVertical;
+ }
+
+ allowedOverscrollDirections &= *aOutAllowedScrollDirections;
+ if (!allowedOverscrollDirections.isEmpty()) {
+ *aOutAllowedScrollDirections = allowedOverscrollDirections;
+ return mChain[i];
+ }
+ }
+
+ *aOutAllowedScrollDirections &= mChain[i]->GetAllowedHandoffDirections();
+ if (aOutAllowedScrollDirections->isEmpty()) {
+ return nullptr;
+ }
+ }
+ return nullptr;
+}
+
+std::tuple<bool, const AsyncPanZoomController*>
+OverscrollHandoffChain::ScrollingDownWillMoveDynamicToolbar(
+ const AsyncPanZoomController* aApzc) const {
+ MOZ_ASSERT(aApzc && !aApzc->IsRootContent(),
+ "Should be used for non-root APZC");
+
+ for (uint32_t i = IndexOf(aApzc); i < Length(); i++) {
+ if (mChain[i]->IsRootContent()) {
+ bool scrollable = mChain[i]->CanVerticalScrollWithDynamicToolbar();
+ return {scrollable, scrollable ? mChain[i].get() : nullptr};
+ }
+
+ if (mChain[i]->CanScrollDownwards()) {
+ return {false, nullptr};
+ }
+ }
+
+ return {false, nullptr};
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/OverscrollHandoffState.h b/gfx/layers/apz/src/OverscrollHandoffState.h
new file mode 100644
index 0000000000..90a22f259c
--- /dev/null
+++ b/gfx/layers/apz/src/OverscrollHandoffState.h
@@ -0,0 +1,203 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_OverscrollHandoffChain_h
+#define mozilla_layers_OverscrollHandoffChain_h
+
+#include <vector>
+#include "mozilla/RefPtr.h" // for RefPtr
+#include "nsISupportsImpl.h" // for NS_INLINE_DECL_THREADSAFE_REFCOUNTING
+#include "APZUtils.h" // for CancelAnimationFlags
+#include "mozilla/layers/LayersTypes.h" // for Layer::ScrollDirection
+#include "Units.h" // for ScreenPoint
+
+namespace mozilla {
+
+class InputData;
+
+namespace layers {
+
+class AsyncPanZoomController;
+
+/**
+ * This class represents the chain of APZCs along which overscroll is handed
+ * off. It is created by APZCTreeManager by starting from an initial APZC which
+ * is the target for input events, and following the scroll parent ID links
+ * (often but not always corresponding to parent pointers in the APZC tree),
+ * then adjusting for scrollgrab.
+ */
+class OverscrollHandoffChain {
+ protected:
+ // Reference-counted classes cannot have public destructors.
+ ~OverscrollHandoffChain();
+
+ public:
+ // Threadsafe so that the controller and sampler threads can both maintain
+ // nsRefPtrs to the same handoff chain.
+ // Mutable so that we can pass around the class by
+ // RefPtr<const OverscrollHandoffChain> and thus enforce that, once built,
+ // the chain is not modified.
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(OverscrollHandoffChain)
+
+ /*
+ * Methods for building the handoff chain.
+ * These should be used only by
+ * AsyncPanZoomController::BuildOverscrollHandoffChain().
+ */
+ void Add(AsyncPanZoomController* aApzc);
+ void SortByScrollPriority();
+
+ /*
+ * Methods for accessing the handoff chain.
+ */
+ uint32_t Length() const { return mChain.size(); }
+ const RefPtr<AsyncPanZoomController>& GetApzcAtIndex(uint32_t aIndex) const;
+ // Returns Length() if |aApzc| is not on this chain.
+ uint32_t IndexOf(const AsyncPanZoomController* aApzc) const;
+
+ /*
+ * Convenience methods for performing operations on APZCs in the chain.
+ */
+
+ // Flush repaints all the way up the chain.
+ void FlushRepaints() const;
+
+ // Cancel animations all the way up the chain.
+ void CancelAnimations(CancelAnimationFlags aFlags = Default) const;
+
+ // Clear overscroll all the way up the chain.
+ void ClearOverscroll() const;
+
+ // Snap back the APZC that is overscrolled on the subset of the chain from
+ // |aStart| onwards, if any.
+ void SnapBackOverscrolledApzc(const AsyncPanZoomController* aStart) const;
+
+ // Similar to above SnapbackOverscrolledApzc but for pan gestures with
+ // momentum events, this function doesn't end up calling each APZC's
+ // ScrollSnap.
+ // |aVelocity| is the initial velocity of |aStart|.
+ void SnapBackOverscrolledApzcForMomentum(
+ const AsyncPanZoomController* aStart,
+ const ParentLayerPoint& aVelocity) const;
+
+ // Determine whether the given APZC, or any APZC further in the chain,
+ // has room to be panned.
+ bool CanBePanned(const AsyncPanZoomController* aApzc) const;
+
+ // Determine whether the given APZC, or any APZC further in the chain,
+ // can scroll in the given direction.
+ bool CanScrollInDirection(const AsyncPanZoomController* aApzc,
+ ScrollDirection aDirection) const;
+
+ // Determine whether any APZC along this handoff chain is overscrolled.
+ bool HasOverscrolledApzc() const;
+
+ // Determine whether any APZC along this handoff chain has been flung fast.
+ bool HasFastFlungApzc() const;
+
+ // Determine whether any APZC along this handoff chain is autoscroll.
+ bool HasAutoscrollApzc() const;
+
+ // Find the first APZC in this handoff chain that can be scrolled by |aInput|.
+ // Since overscroll-behavior can restrict handoff in some directions,
+ // |aOutAllowedScrollDirections| is populated with the scroll directions
+ // in which scrolling of the returned APZC is allowed.
+ // |aIncludeOverscroll| is an optional flag whether to consider overscrollable
+ // as scrollable or not.
+ enum class IncludeOverscroll : bool { No, Yes };
+ RefPtr<AsyncPanZoomController> FindFirstScrollable(
+ const InputData& aInput, ScrollDirections* aOutAllowedScrollDirections,
+ IncludeOverscroll aIncludeOverscroll = IncludeOverscroll::Yes) const;
+
+ // Return a pair of true and the root content APZC if all non-root APZCs in
+ // this handoff chain starting from |aApzc| are not able to scroll downwards
+ // (i.e. there is no room to scroll downwards in each APZC respectively) and
+ // there is any contents covered by the dynamic toolbar, otherwise return a
+ // pair of false and nullptr.
+ std::tuple<bool, const AsyncPanZoomController*>
+ ScrollingDownWillMoveDynamicToolbar(
+ const AsyncPanZoomController* aApzc) const;
+
+ private:
+ std::vector<RefPtr<AsyncPanZoomController>> mChain;
+
+ typedef void (AsyncPanZoomController::*APZCMethod)();
+ typedef bool (AsyncPanZoomController::*APZCPredicate)() const;
+ void ForEachApzc(APZCMethod aMethod) const;
+ bool AnyApzc(APZCPredicate aPredicate) const;
+};
+
+/**
+ * This class groups the state maintained during overscroll handoff.
+ */
+struct OverscrollHandoffState {
+ OverscrollHandoffState(const OverscrollHandoffChain& aChain,
+ const ScreenPoint& aPanDistance,
+ ScrollSource aScrollSource)
+ : mChain(aChain),
+ mChainIndex(0),
+ mPanDistance(aPanDistance),
+ mScrollSource(aScrollSource) {}
+
+ // The chain of APZCs along which we hand off scroll.
+ // This is const to indicate that the chain does not change over the
+ // course of handoff.
+ const OverscrollHandoffChain& mChain;
+
+ // The index of the APZC in the chain that we are currently giving scroll to.
+ // This is non-const to indicate that this changes over the course of handoff.
+ uint32_t mChainIndex;
+
+ // The total distance since touch-start of the pan that triggered the
+ // handoff. This is const to indicate that it does not change over the
+ // course of handoff.
+ // The x/y components of this are non-negative.
+ const ScreenPoint mPanDistance;
+
+ ScrollSource mScrollSource;
+
+ // The total amount of actual movement that this scroll caused, including
+ // scrolling and changes to overscroll. This starts at zero and is accumulated
+ // over the course of the handoff.
+ ScreenPoint mTotalMovement;
+};
+
+/*
+ * This class groups the state maintained during fling handoff.
+ */
+struct FlingHandoffState {
+ // The velocity of the fling being handed off.
+ ParentLayerPoint mVelocity;
+
+ // The chain of APZCs along which we hand off the fling.
+ // Unlike in OverscrollHandoffState, this is stored by RefPtr because
+ // otherwise it may not stay alive for the entire handoff.
+ RefPtr<const OverscrollHandoffChain> mChain;
+
+ // The time duration between the touch start and the touch move that started
+ // the pan gesture which triggered this fling. In other words, the time it
+ // took for the finger to move enough to cross the touch slop threshold.
+ // Nothing if this fling was not immediately caused by a touch pan.
+ Maybe<TimeDuration> mTouchStartRestingTime;
+
+ // The slowest panning velocity encountered during the pan that triggered this
+ // fling.
+ ParentLayerCoord mMinPanVelocity;
+
+ // Whether handoff has happened by this point, or we're still process
+ // the original fling.
+ bool mIsHandoff;
+
+ // The single APZC that was scrolled by the pan that started this fling.
+ // The fling is only allowed to scroll this APZC, too.
+ // Used only if immediate scroll handoff is disallowed.
+ RefPtr<const AsyncPanZoomController> mScrolledApzc;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif /* mozilla_layers_OverscrollHandoffChain_h */
diff --git a/gfx/layers/apz/src/PotentialCheckerboardDurationTracker.cpp b/gfx/layers/apz/src/PotentialCheckerboardDurationTracker.cpp
new file mode 100644
index 0000000000..f3d5538ba0
--- /dev/null
+++ b/gfx/layers/apz/src/PotentialCheckerboardDurationTracker.cpp
@@ -0,0 +1,74 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "PotentialCheckerboardDurationTracker.h"
+
+#include "mozilla/Telemetry.h" // for Telemetry
+
+namespace mozilla {
+namespace layers {
+
+PotentialCheckerboardDurationTracker::PotentialCheckerboardDurationTracker()
+ : mInCheckerboard(false), mInTransform(false) {}
+
+void PotentialCheckerboardDurationTracker::CheckerboardSeen() {
+ // This might get called while mInCheckerboard is already true
+ if (!Tracking()) {
+ mCurrentPeriodStart = TimeStamp::Now();
+ }
+ mInCheckerboard = true;
+}
+
+void PotentialCheckerboardDurationTracker::CheckerboardDone(
+ bool aRecordTelemetry) {
+ MOZ_ASSERT(Tracking());
+ mInCheckerboard = false;
+ if (!Tracking()) {
+ if (aRecordTelemetry) {
+ mozilla::Telemetry::AccumulateTimeDelta(
+ mozilla::Telemetry::CHECKERBOARD_POTENTIAL_DURATION,
+ mCurrentPeriodStart);
+ }
+ }
+}
+
+void PotentialCheckerboardDurationTracker::InTransform(bool aInTransform,
+ bool aRecordTelemetry) {
+ if (aInTransform == mInTransform) {
+ // no-op
+ return;
+ }
+
+ if (!Tracking()) {
+ // Because !Tracking(), mInTransform must be false, and so aInTransform
+ // must be true (or we would have early-exited this function already).
+ // Therefore, we are starting a potential checkerboard period.
+ mInTransform = aInTransform;
+ mCurrentPeriodStart = TimeStamp::Now();
+ return;
+ }
+
+ mInTransform = aInTransform;
+
+ if (!Tracking()) {
+ // Tracking() must have been true at the start of this function, or we
+ // would have taken the other !Tracking branch above. If it's false now,
+ // it means we just stopped tracking, so we are ending a potential
+ // checkerboard period.
+ if (aRecordTelemetry) {
+ mozilla::Telemetry::AccumulateTimeDelta(
+ mozilla::Telemetry::CHECKERBOARD_POTENTIAL_DURATION,
+ mCurrentPeriodStart);
+ }
+ }
+}
+
+bool PotentialCheckerboardDurationTracker::Tracking() const {
+ return mInTransform || mInCheckerboard;
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/PotentialCheckerboardDurationTracker.h b/gfx/layers/apz/src/PotentialCheckerboardDurationTracker.h
new file mode 100644
index 0000000000..58786f32af
--- /dev/null
+++ b/gfx/layers/apz/src/PotentialCheckerboardDurationTracker.h
@@ -0,0 +1,61 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_PotentialCheckerboardDurationTracker_h
+#define mozilla_layers_PotentialCheckerboardDurationTracker_h
+
+#include "mozilla/TimeStamp.h"
+
+namespace mozilla {
+namespace layers {
+
+/**
+ * This class allows the owner to track the duration of time considered
+ * "potentially checkerboarding". This is the union of two possibly-intersecting
+ * sets of time periods. The first set is that in which checkerboarding was
+ * actually happening, since by definition it could potentially be happening.
+ * The second set is that in which the APZC is actively transforming content
+ * in the compositor, since it could potentially transform it so as to display
+ * checkerboarding to the user.
+ * The caller of this class calls the appropriate methods to indicate the start
+ * and stop of these two sets, and this class manages accumulating the union
+ * of the various durations.
+ */
+class PotentialCheckerboardDurationTracker {
+ public:
+ PotentialCheckerboardDurationTracker();
+
+ /**
+ * This should be called if checkerboarding is encountered. It can be called
+ * multiple times during a checkerboard event.
+ */
+ void CheckerboardSeen();
+ /**
+ * This should be called when checkerboarding is done. It must have been
+ * preceded by one or more calls to CheckerboardSeen().
+ */
+ void CheckerboardDone(bool aRecordTelemetry);
+
+ /**
+ * This should be called at composition time, to indicate if the APZC is in
+ * a transforming state or not.
+ */
+ void InTransform(bool aInTransform, bool aRecordTelemetry);
+
+ private:
+ bool Tracking() const;
+
+ private:
+ bool mInCheckerboard;
+ bool mInTransform;
+
+ TimeStamp mCurrentPeriodStart;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_PotentialCheckerboardDurationTracker_h
diff --git a/gfx/layers/apz/src/QueuedInput.cpp b/gfx/layers/apz/src/QueuedInput.cpp
new file mode 100644
index 0000000000..87ffe7250e
--- /dev/null
+++ b/gfx/layers/apz/src/QueuedInput.cpp
@@ -0,0 +1,44 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "QueuedInput.h"
+
+#include "AsyncPanZoomController.h"
+#include "InputBlockState.h"
+#include "InputData.h"
+#include "OverscrollHandoffState.h"
+
+namespace mozilla {
+namespace layers {
+
+QueuedInput::QueuedInput(const MultiTouchInput& aInput, TouchBlockState& aBlock)
+ : mInput(MakeUnique<MultiTouchInput>(aInput)), mBlock(&aBlock) {}
+
+QueuedInput::QueuedInput(const ScrollWheelInput& aInput,
+ WheelBlockState& aBlock)
+ : mInput(MakeUnique<ScrollWheelInput>(aInput)), mBlock(&aBlock) {}
+
+QueuedInput::QueuedInput(const MouseInput& aInput, DragBlockState& aBlock)
+ : mInput(MakeUnique<MouseInput>(aInput)), mBlock(&aBlock) {}
+
+QueuedInput::QueuedInput(const PanGestureInput& aInput,
+ PanGestureBlockState& aBlock)
+ : mInput(MakeUnique<PanGestureInput>(aInput)), mBlock(&aBlock) {}
+
+QueuedInput::QueuedInput(const PinchGestureInput& aInput,
+ PinchGestureBlockState& aBlock)
+ : mInput(MakeUnique<PinchGestureInput>(aInput)), mBlock(&aBlock) {}
+
+QueuedInput::QueuedInput(const KeyboardInput& aInput,
+ KeyboardBlockState& aBlock)
+ : mInput(MakeUnique<KeyboardInput>(aInput)), mBlock(&aBlock) {}
+
+InputData* QueuedInput::Input() { return mInput.get(); }
+
+InputBlockState* QueuedInput::Block() { return mBlock.get(); }
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/QueuedInput.h b/gfx/layers/apz/src/QueuedInput.h
new file mode 100644
index 0000000000..fcc2f2090a
--- /dev/null
+++ b/gfx/layers/apz/src/QueuedInput.h
@@ -0,0 +1,63 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_QueuedInput_h
+#define mozilla_layers_QueuedInput_h
+
+#include "mozilla/RefPtr.h"
+#include "mozilla/UniquePtr.h"
+
+namespace mozilla {
+
+class InputData;
+class MultiTouchInput;
+class ScrollWheelInput;
+class MouseInput;
+class PanGestureInput;
+class PinchGestureInput;
+class KeyboardInput;
+
+namespace layers {
+
+class InputBlockState;
+class TouchBlockState;
+class WheelBlockState;
+class DragBlockState;
+class PanGestureBlockState;
+class PinchGestureBlockState;
+class KeyboardBlockState;
+
+/**
+ * This lightweight class holds a pointer to an input event that has not yet
+ * been completely processed, along with the input block that the input event
+ * is associated with.
+ */
+class QueuedInput {
+ public:
+ QueuedInput(const MultiTouchInput& aInput, TouchBlockState& aBlock);
+ QueuedInput(const ScrollWheelInput& aInput, WheelBlockState& aBlock);
+ QueuedInput(const MouseInput& aInput, DragBlockState& aBlock);
+ QueuedInput(const PanGestureInput& aInput, PanGestureBlockState& aBlock);
+ QueuedInput(const PinchGestureInput& aInput, PinchGestureBlockState& aBlock);
+ QueuedInput(const KeyboardInput& aInput, KeyboardBlockState& aBlock);
+
+ InputData* Input();
+ InputBlockState* Block();
+
+ private:
+ // A copy of the input event that is provided to the constructor. This must
+ // be non-null, and is owned by this QueuedInput instance (hence the
+ // UniquePtr).
+ UniquePtr<InputData> mInput;
+ // A pointer to the block that the input event is associated with. This must
+ // be non-null.
+ RefPtr<InputBlockState> mBlock;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_QueuedInput_h
diff --git a/gfx/layers/apz/src/RecentEventsBuffer.h b/gfx/layers/apz/src/RecentEventsBuffer.h
new file mode 100644
index 0000000000..d1ae5797af
--- /dev/null
+++ b/gfx/layers/apz/src/RecentEventsBuffer.h
@@ -0,0 +1,83 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_RecentEventsBuffer_h
+#define mozilla_layers_RecentEventsBuffer_h
+
+#include <deque>
+
+#include "mozilla/TimeStamp.h"
+
+namespace mozilla {
+namespace layers {
+/**
+ * RecentEventsBuffer: maintains an age constrained buffer of events
+ *
+ * Intended for use with elements of type InputData, but the only requirement
+ * is a member "mTimeStamp" of type TimeStamp
+ */
+template <typename Event>
+class RecentEventsBuffer {
+ public:
+ explicit RecentEventsBuffer(TimeDuration maxAge);
+
+ void push(Event event);
+ void clear();
+
+ typedef typename std::deque<Event>::size_type size_type;
+ size_type size() { return mBuffer.size(); }
+
+ // Delegate to container for iterators
+ typedef typename std::deque<Event>::iterator iterator;
+ typedef typename std::deque<Event>::const_iterator const_iterator;
+ iterator begin() { return mBuffer.begin(); }
+ iterator end() { return mBuffer.end(); }
+ const_iterator cbegin() const { return mBuffer.cbegin(); }
+ const_iterator cend() const { return mBuffer.cend(); }
+
+ // Also delegate for front/back
+ typedef typename std::deque<Event>::reference reference;
+ typedef typename std::deque<Event>::const_reference const_reference;
+ reference front() { return mBuffer.front(); }
+ reference back() { return mBuffer.back(); }
+ const_reference front() const { return mBuffer.front(); }
+ const_reference back() const { return mBuffer.back(); }
+
+ private:
+ TimeDuration mMaxAge;
+ std::deque<Event> mBuffer;
+};
+
+template <typename Event>
+RecentEventsBuffer<Event>::RecentEventsBuffer(TimeDuration maxAge)
+ : mMaxAge(maxAge), mBuffer() {}
+
+template <typename Event>
+void RecentEventsBuffer<Event>::push(Event event) {
+ // Events must be pushed in chronological order
+ MOZ_ASSERT(mBuffer.empty() || mBuffer.back().mTimeStamp <= event.mTimeStamp);
+
+ mBuffer.push_back(event);
+
+ // Flush all events older than the given lifetime
+ TimeStamp bound = event.mTimeStamp - mMaxAge;
+ while (!mBuffer.empty()) {
+ if (mBuffer.front().mTimeStamp >= bound) {
+ break;
+ }
+ mBuffer.pop_front();
+ }
+}
+
+template <typename Event>
+void RecentEventsBuffer<Event>::clear() {
+ mBuffer.clear();
+}
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_RecentEventsBuffer_h
diff --git a/gfx/layers/apz/src/SampledAPZCState.cpp b/gfx/layers/apz/src/SampledAPZCState.cpp
new file mode 100644
index 0000000000..712a46a3b1
--- /dev/null
+++ b/gfx/layers/apz/src/SampledAPZCState.cpp
@@ -0,0 +1,111 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "SampledAPZCState.h"
+#include "APZUtils.h"
+
+namespace mozilla {
+namespace layers {
+
+SampledAPZCState::SampledAPZCState() {}
+
+SampledAPZCState::SampledAPZCState(const FrameMetrics& aMetrics)
+ : mLayoutViewport(aMetrics.GetLayoutViewport()),
+ mVisualScrollOffset(aMetrics.GetVisualScrollOffset()),
+ mZoom(aMetrics.GetZoom()) {
+ RemoveFractionalAsyncDelta();
+}
+
+SampledAPZCState::SampledAPZCState(const FrameMetrics& aMetrics,
+ Maybe<CompositionPayload>&& aPayload,
+ APZScrollGeneration aGeneration)
+ : mLayoutViewport(aMetrics.GetLayoutViewport()),
+ mVisualScrollOffset(aMetrics.GetVisualScrollOffset()),
+ mZoom(aMetrics.GetZoom()),
+ mScrollPayload(std::move(aPayload)),
+ mGeneration(aGeneration) {
+ RemoveFractionalAsyncDelta();
+}
+
+bool SampledAPZCState::operator==(const SampledAPZCState& aOther) const {
+ // The payload doesn't factor into equality, that just comes along for
+ // the ride.
+ return mLayoutViewport.IsEqualEdges(aOther.mLayoutViewport) &&
+ mVisualScrollOffset == aOther.mVisualScrollOffset &&
+ mZoom == aOther.mZoom;
+}
+
+bool SampledAPZCState::operator!=(const SampledAPZCState& aOther) const {
+ return !(*this == aOther);
+}
+
+Maybe<CompositionPayload> SampledAPZCState::TakeScrollPayload() {
+ return std::move(mScrollPayload);
+}
+
+void SampledAPZCState::UpdateScrollProperties(const FrameMetrics& aMetrics) {
+ mLayoutViewport = aMetrics.GetLayoutViewport();
+ mVisualScrollOffset = aMetrics.GetVisualScrollOffset();
+}
+
+void SampledAPZCState::UpdateScrollPropertiesWithRelativeDelta(
+ const FrameMetrics& aMetrics, const CSSPoint& aRelativeDelta) {
+ mVisualScrollOffset += aRelativeDelta;
+ KeepLayoutViewportEnclosingVisualViewport(aMetrics);
+}
+
+void SampledAPZCState::UpdateZoomProperties(const FrameMetrics& aMetrics) {
+ mZoom = aMetrics.GetZoom();
+}
+
+void SampledAPZCState::ClampVisualScrollOffset(const FrameMetrics& aMetrics) {
+ // Make sure that we use the local mZoom to do these calculations, because the
+ // one on aMetrics might be newer.
+ CSSRect scrollRange = FrameMetrics::CalculateScrollRange(
+ aMetrics.GetScrollableRect(), aMetrics.GetCompositionBounds(), mZoom);
+ mVisualScrollOffset = scrollRange.ClampPoint(mVisualScrollOffset);
+
+ KeepLayoutViewportEnclosingVisualViewport(aMetrics);
+}
+
+void SampledAPZCState::ZoomBy(float aScale) { mZoom.scale *= aScale; }
+
+void SampledAPZCState::RemoveFractionalAsyncDelta() {
+ // This function is a performance hack. With non-WebRender, having small
+ // fractional deltas between the layout offset and scroll offset on
+ // container layers can trigger the creation of a temporary surface during
+ // composition, because it produces a non-integer translation that doesn't
+ // play well with layer clips. So we detect the case where the delta is
+ // uselessly small (0.01 parentlayer pixels or less) and tweak the sampled
+ // scroll offset to eliminate it. By doing this here at sample time rather
+ // than elsewhere in the pipeline we are least likely to break assumptions
+ // and invariants elsewhere in the code, since sampling effectively takes
+ // a snapshot of APZ state (decoupling it from APZ assumptions) and provides
+ // it as an input to the compositor (so all compositor state should be
+ // internally consistent based on this input).
+ if (mLayoutViewport.TopLeft() == mVisualScrollOffset) {
+ return;
+ }
+ const ParentLayerCoord EPSILON = 0.01;
+ ParentLayerPoint paintedOffset = mLayoutViewport.TopLeft() * mZoom;
+ ParentLayerPoint asyncOffset = mVisualScrollOffset * mZoom;
+ if (FuzzyEqualsAdditive(paintedOffset.x, asyncOffset.x, EPSILON) &&
+ FuzzyEqualsAdditive(paintedOffset.y, asyncOffset.y, EPSILON)) {
+ mVisualScrollOffset = mLayoutViewport.TopLeft();
+ }
+}
+
+void SampledAPZCState::KeepLayoutViewportEnclosingVisualViewport(
+ const FrameMetrics& aMetrics) {
+ FrameMetrics::KeepLayoutViewportEnclosingVisualViewport(
+ CSSRect(mVisualScrollOffset,
+ FrameMetrics::CalculateCompositedSizeInCssPixels(
+ aMetrics.GetCompositionBounds(), mZoom)),
+ aMetrics.GetScrollableRect(), mLayoutViewport);
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/SampledAPZCState.h b/gfx/layers/apz/src/SampledAPZCState.h
new file mode 100644
index 0000000000..a521eeaf87
--- /dev/null
+++ b/gfx/layers/apz/src/SampledAPZCState.h
@@ -0,0 +1,72 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_SampledAPZCState_h
+#define mozilla_layers_SampledAPZCState_h
+
+#include "FrameMetrics.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/ScrollGeneration.h"
+
+namespace mozilla {
+namespace layers {
+
+class SampledAPZCState {
+ public:
+ SampledAPZCState();
+ explicit SampledAPZCState(const FrameMetrics& aMetrics);
+ SampledAPZCState(const FrameMetrics& aMetrics,
+ Maybe<CompositionPayload>&& aPayload,
+ APZScrollGeneration aGeneration);
+
+ bool operator==(const SampledAPZCState& aOther) const;
+ bool operator!=(const SampledAPZCState& aOther) const;
+
+ CSSRect GetLayoutViewport() const { return mLayoutViewport; }
+ CSSPoint GetVisualScrollOffset() const { return mVisualScrollOffset; }
+ CSSToParentLayerScale GetZoom() const { return mZoom; }
+ Maybe<CompositionPayload> TakeScrollPayload();
+ const APZScrollGeneration& Generation() const { return mGeneration; }
+
+ void UpdateScrollProperties(const FrameMetrics& aMetrics);
+ void UpdateScrollPropertiesWithRelativeDelta(const FrameMetrics& aMetrics,
+ const CSSPoint& aRelativeDelta);
+
+ void UpdateZoomProperties(const FrameMetrics& aMetrics);
+
+ /**
+ * Re-clamp mVisualScrollOffset to the scroll range specified by the provided
+ * metrics. This only needs to be called if the scroll offset changes
+ * outside of AsyncPanZoomController::SampleCompositedAsyncTransform().
+ * It also recalculates mLayoutViewport so that it continues to enclose
+ * the visual viewport. This only needs to be called if the
+ * layout viewport changes outside of SampleCompositedAsyncTransform().
+ */
+ void ClampVisualScrollOffset(const FrameMetrics& aMetrics);
+
+ void ZoomBy(float aScale);
+
+ private:
+ // These variables cache the layout viewport, scroll offset, and zoom stored
+ // in |Metrics()| at the time this class was constructed.
+ CSSRect mLayoutViewport;
+ CSSPoint mVisualScrollOffset;
+ CSSToParentLayerScale mZoom;
+ // An optional payload that rides along with the sampled state.
+ Maybe<CompositionPayload> mScrollPayload;
+ APZScrollGeneration mGeneration;
+
+ void RemoveFractionalAsyncDelta();
+ // A handy wrapper to call
+ // FrameMetrics::KeepLayoutViewportEnclosingVisualViewport with this
+ // SampledAPZCState and the given |aMetrics|.
+ void KeepLayoutViewportEnclosingVisualViewport(const FrameMetrics& aMetrics);
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_SampledAPZCState_h
diff --git a/gfx/layers/apz/src/ScrollThumbUtils.cpp b/gfx/layers/apz/src/ScrollThumbUtils.cpp
new file mode 100644
index 0000000000..814fa59759
--- /dev/null
+++ b/gfx/layers/apz/src/ScrollThumbUtils.cpp
@@ -0,0 +1,341 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ScrollThumbUtils.h"
+#include "AsyncPanZoomController.h"
+#include "FrameMetrics.h"
+#include "UnitTransforms.h"
+#include "Units.h"
+#include "gfxPlatform.h"
+#include "mozilla/gfx/Matrix.h"
+#include "mozilla/StaticPrefs_toolkit.h"
+
+namespace mozilla {
+namespace layers {
+namespace apz {
+
+struct AsyncScrollThumbTransformer {
+ // Inputs
+ const LayerToParentLayerMatrix4x4& mCurrentTransform;
+ const gfx::Matrix4x4& mScrollableContentTransform;
+ AsyncPanZoomController* mApzc;
+ const FrameMetrics& mMetrics;
+ const ScrollbarData& mScrollbarData;
+ bool mScrollbarIsDescendant;
+
+ // Intermediate results
+ AsyncTransformComponentMatrix mAsyncTransform;
+ AsyncTransformComponentMatrix mScrollbarTransform;
+
+ LayerToParentLayerMatrix4x4 ComputeTransform();
+
+ private:
+ // Helper functions for ComputeTransform().
+
+ // If the thumb's orientation is along |aAxis|, add transformations
+ // of the thumb into |mScrollbarTransform|.
+ void ApplyTransformForAxis(const Axis& aAxis);
+
+ enum class ScrollThumbExtent { Start, End };
+
+ // Scale the thumb by |aScale| along |aAxis|, while keeping constant the
+ // position of the top denoted by |aExtent|.
+ void ScaleThumbBy(const Axis& aAxis, float aScale, ScrollThumbExtent aExtent);
+
+ // Translate the thumb along |aAxis| by |aTranslation| in "scrollbar space"
+ // (CSS pixels along the scrollbar track, similar to e.g.
+ // |mScrollbarData.mThumbStart|).
+ void TranslateThumb(const Axis& aAxis, OuterCSSCoord aTranslation);
+};
+
+void AsyncScrollThumbTransformer::TranslateThumb(const Axis& aAxis,
+ OuterCSSCoord aTranslation) {
+ aAxis.PostTranslate(
+ mScrollbarTransform,
+ ViewAs<CSSPixel>(aTranslation,
+ PixelCastJustification::CSSPixelsOfSurroundingContent) *
+ mMetrics.GetDevPixelsPerCSSPixel() *
+ LayoutDeviceToParentLayerScale(1.0));
+}
+
+void AsyncScrollThumbTransformer::ScaleThumbBy(const Axis& aAxis, float aScale,
+ ScrollThumbExtent aExtent) {
+ // To keep the position of the top of the thumb constant, the thumb needs to
+ // translated to compensate for the scale applied. The origin with respect to
+ // which the scale is applied is the origin of the layer tree, rather than
+ // the origin of the scroll thumb. This means that the space between the
+ // origin and the top of thumb (including the part of the scrollbar track
+ // above the thumb, the part of the scrollbar above the track (i.e. a
+ // scrollbar button if present), plus whatever content is above the scroll
+ // frame) is scaled too, effectively translating the thumb. We undo that
+ // translation here. (One can think of the adjustment being done to the
+ // translation here as a change of basis. We have a method to help with that,
+ // Matrix4x4::ChangeBasis(), but it wouldn't necessarily make the code cleaner
+ // in this case).
+ const OuterCSSCoord scrollTrackOrigin =
+ aAxis.GetPointOffset(
+ mMetrics.CalculateCompositionBoundsInOuterCssPixels().TopLeft()) +
+ mScrollbarData.mScrollTrackStart;
+ OuterCSSCoord thumbExtent = scrollTrackOrigin + mScrollbarData.mThumbStart;
+ if (aExtent == ScrollThumbExtent::End) {
+ thumbExtent += mScrollbarData.mThumbLength;
+ }
+ const OuterCSSCoord thumbExtentScaled = thumbExtent * aScale;
+ const OuterCSSCoord thumbExtentDelta = thumbExtentScaled - thumbExtent;
+
+ aAxis.PostScale(mScrollbarTransform, aScale);
+ TranslateThumb(aAxis, -thumbExtentDelta);
+}
+
+void AsyncScrollThumbTransformer::ApplyTransformForAxis(const Axis& aAxis) {
+ ParentLayerCoord asyncScroll = aAxis.GetTransformTranslation(mAsyncTransform);
+ const float asyncZoom = aAxis.GetTransformScale(mAsyncTransform);
+ const ParentLayerCoord overscroll =
+ aAxis.GetPointOffset(mApzc->GetOverscrollAmount());
+
+ bool haveAsyncZoom = !FuzzyEqualsAdditive(asyncZoom, 1.f);
+ if (!haveAsyncZoom && mApzc->IsZero(asyncScroll) &&
+ mApzc->IsZero(overscroll)) {
+ return;
+ }
+
+ OuterCSSCoord translation;
+ float scale = 1.0;
+
+ bool recalcMode = StaticPrefs::apz_scrollthumb_recalc();
+ if (recalcMode) {
+ // In this branch (taken when apz.scrollthumb.recalc=true), |translation|
+ // and |scale| are computed using the approach implemented in bug 1554795
+ // of fully recalculating the desired position and size using the logic
+ // that attempts to closely match the main-thread calculation.
+
+ const CSSRect visualViewportRect = mApzc->GetCurrentAsyncVisualViewport(
+ AsyncPanZoomController::eForCompositing);
+ const CSSCoord visualViewportLength =
+ aAxis.GetRectLength(visualViewportRect);
+
+ const CSSCoord maxMinPosDifference =
+ CSSCoord(
+ aAxis.GetRectLength(mMetrics.GetScrollableRect()).Truncated()) -
+ visualViewportLength;
+
+ OuterCSSCoord effectiveThumbLength = mScrollbarData.mThumbLength;
+
+ if (haveAsyncZoom) {
+ // The calculations here closely follow the main thread calculations at
+ // https://searchfox.org/mozilla-central/rev/0bf957f909ae1f3d19b43fd4edfc277342554836/layout/generic/nsGfxScrollFrame.cpp#6902-6927
+ // and
+ // https://searchfox.org/mozilla-central/rev/0bf957f909ae1f3d19b43fd4edfc277342554836/layout/xul/nsSliderFrame.cpp#587-614
+ // Any modifications there should be reflected here as well.
+ const CSSCoord pageIncrementMin =
+ static_cast<int>(visualViewportLength * 0.8);
+ CSSCoord pageIncrement;
+
+ CSSToLayoutDeviceScale deviceScale = mMetrics.GetDevPixelsPerCSSPixel();
+ if (*mScrollbarData.mDirection == ScrollDirection::eVertical) {
+ const CSSCoord lineScrollAmount =
+ (mApzc->GetScrollMetadata().GetLineScrollAmount() / deviceScale)
+ .height;
+ const double kScrollMultiplier =
+ StaticPrefs::toolkit_scrollbox_verticalScrollDistance();
+ CSSCoord increment = lineScrollAmount * kScrollMultiplier;
+
+ pageIncrement =
+ std::max(visualViewportLength - increment, pageIncrementMin);
+ } else {
+ pageIncrement = pageIncrementMin;
+ }
+
+ float ratio = pageIncrement / (maxMinPosDifference + pageIncrement);
+
+ OuterCSSCoord desiredThumbLength{
+ std::max(mScrollbarData.mThumbMinLength,
+ mScrollbarData.mScrollTrackLength * ratio)};
+
+ // Round the thumb length to an integer number of LayoutDevice pixels, to
+ // match the main-thread behaviour.
+ auto outerDeviceScale = ViewAs<OuterCSSToLayoutDeviceScale>(
+ deviceScale, PixelCastJustification::CSSPixelsOfSurroundingContent);
+ desiredThumbLength =
+ LayoutDeviceCoord((desiredThumbLength * outerDeviceScale).Rounded()) /
+ outerDeviceScale;
+
+ effectiveThumbLength = desiredThumbLength;
+
+ scale = desiredThumbLength / mScrollbarData.mThumbLength;
+ }
+
+ // Subtracting the offset of the scrollable rect is needed for right-to-left
+ // pages.
+ const CSSCoord curPos = aAxis.GetRectOffset(visualViewportRect) -
+ aAxis.GetRectOffset(mMetrics.GetScrollableRect());
+
+ const CSSToOuterCSSScale thumbPosRatio(
+ (maxMinPosDifference != 0)
+ ? float((mScrollbarData.mScrollTrackLength - effectiveThumbLength) /
+ maxMinPosDifference)
+ : 1.f);
+
+ const OuterCSSCoord desiredThumbPos = curPos * thumbPosRatio;
+
+ translation = desiredThumbPos - mScrollbarData.mThumbStart;
+ } else {
+ // In this branch (taken when apz.scrollthumb.recalc=false), |translation|
+ // and |scale| are computed using the pre-bug1554795 approach of turning
+ // the async scroll and zoom deltas into transforms to apply to the
+ // main-thread thumb position and size.
+
+ // The scroll thumb needs to be scaled in the direction of scrolling by the
+ // inverse of the async zoom. This is because zooming in decreases the
+ // fraction of the whole srollable rect that is in view.
+ scale = 1.f / asyncZoom;
+
+ // Note: |metrics.GetZoom()| doesn't yet include the async zoom.
+ CSSToParentLayerScale effectiveZoom =
+ CSSToParentLayerScale(mMetrics.GetZoom().scale * asyncZoom);
+
+ if (gfxPlatform::UseDesktopZoomingScrollbars()) {
+ // As computed by GetCurrentAsyncTransform, asyncScrollY is
+ // asyncScrollY = -(GetEffectiveScrollOffset -
+ // mLastContentPaintMetrics.GetLayoutScrollOffset()) *
+ // effectiveZoom
+ // where GetEffectiveScrollOffset includes the visual viewport offset that
+ // the main thread knows about plus any async scrolling to the visual
+ // viewport offset that the main thread does not (yet) know about. We want
+ // asyncScrollY to be
+ // asyncScrollY = -(GetEffectiveScrollOffset -
+ // mLastContentPaintMetrics.GetVisualScrollOffset()) * effectiveZoom
+ // because the main thread positions the scrollbars at the visual viewport
+ // offset that it knows about. (aMetrics is mLastContentPaintMetrics)
+
+ asyncScroll -= aAxis.GetPointOffset((mMetrics.GetLayoutScrollOffset() -
+ mMetrics.GetVisualScrollOffset()) *
+ effectiveZoom);
+ }
+
+ // Here we convert the scrollbar thumb ratio into a true unitless ratio by
+ // dividing out the conversion factor from the scrollframe's parent's space
+ // to the scrollframe's space.
+ float unitlessThumbRatio = mScrollbarData.mThumbRatio /
+ (mMetrics.GetPresShellResolution() * asyncZoom);
+
+ // The scroll thumb needs to be translated in opposite direction of the
+ // async scroll. This is because scrolling down, which translates the layer
+ // content up, should result in moving the scroll thumb down.
+ ParentLayerCoord translationPL = -asyncScroll * unitlessThumbRatio;
+
+ // The translation we computed is in the scroll frame's ParentLayer space.
+ // This includes the full cumulative resolution, even if we are a subframe.
+ // However, the resulting transform is used in a context where the scrollbar
+ // is already subject to the resolutions of enclosing scroll frames. To
+ // avoid double application of these enclosing resolutions, divide them out,
+ // leaving only the local resolution if any.
+ translationPL /= (mMetrics.GetCumulativeResolution().scale /
+ mMetrics.GetPresShellResolution());
+
+ // Convert translation to CSS pixels as this is what TranslateThumb expects.
+ translation = ViewAs<OuterCSSPixel>(
+ translationPL / (mMetrics.GetDevPixelsPerCSSPixel() *
+ LayoutDeviceToParentLayerScale(1.0)),
+ PixelCastJustification::CSSPixelsOfSurroundingContent);
+ }
+
+ // When scaling the thumb to account for the async zoom, keep the position
+ // of the start of the thumb (which corresponds to the scroll offset)
+ // constant.
+ if (haveAsyncZoom) {
+ ScaleThumbBy(aAxis, scale, ScrollThumbExtent::Start);
+ }
+
+ // If the page is overscrolled, additionally squish the thumb in accordance
+ // with the overscroll amount.
+ if (overscroll != 0) {
+ float overscrollScale =
+ 1.0f - (std::abs(overscroll.value) /
+ aAxis.GetRectLength(mMetrics.GetCompositionBounds()));
+ MOZ_ASSERT(overscrollScale > 0.0f && overscrollScale <= 1.0f);
+ // If we're overscrolled at the top, keep the top of the thumb in place
+ // as we squish it. If we're overscrolled at the bottom, keep the bottom of
+ // the thumb in place.
+ ScaleThumbBy(
+ aAxis, overscrollScale,
+ overscroll < 0 ? ScrollThumbExtent::Start : ScrollThumbExtent::End);
+ }
+
+ TranslateThumb(aAxis, translation);
+}
+
+LayerToParentLayerMatrix4x4 AsyncScrollThumbTransformer::ComputeTransform() {
+ // We only apply the transform if the scroll-target layer has non-container
+ // children (i.e. when it has some possibly-visible content). This is to
+ // avoid moving scroll-bars in the situation that only a scroll information
+ // layer has been built for a scroll frame, as this would result in a
+ // disparity between scrollbars and visible content.
+ if (mMetrics.IsScrollInfoLayer()) {
+ return LayerToParentLayerMatrix4x4{};
+ }
+
+ MOZ_RELEASE_ASSERT(mApzc);
+
+ mAsyncTransform =
+ mApzc->GetCurrentAsyncTransform(AsyncPanZoomController::eForCompositing);
+
+ // |mAsyncTransform| represents the amount by which we have scrolled and
+ // zoomed since the last paint. Because the scrollbar was sized and positioned
+ // based on the painted content, we need to adjust it based on asyncTransform
+ // so that it reflects what the user is actually seeing now.
+ if (*mScrollbarData.mDirection == ScrollDirection::eVertical) {
+ ApplyTransformForAxis(mApzc->mY);
+ }
+ if (*mScrollbarData.mDirection == ScrollDirection::eHorizontal) {
+ ApplyTransformForAxis(mApzc->mX);
+ }
+
+ LayerToParentLayerMatrix4x4 transform =
+ mCurrentTransform * mScrollbarTransform;
+
+ AsyncTransformComponentMatrix compensation;
+ // If the scrollbar layer is a child of the content it is a scrollbar for,
+ // then we need to adjust for any async transform (including an overscroll
+ // transform) on the content. This needs to be cancelled out because layout
+ // positions and sizes the scrollbar on the assumption that there is no async
+ // transform, and without this adjustment the scrollbar will end up in the
+ // wrong place.
+ //
+ // Note that since the async transform is applied on top of the content's
+ // regular transform, we need to make sure to unapply the async transform in
+ // the same coordinate space. This requires applying the content transform
+ // and then unapplying it after unapplying the async transform.
+ if (mScrollbarIsDescendant) {
+ AsyncTransformComponentMatrix overscroll =
+ mApzc->GetOverscrollTransform(AsyncPanZoomController::eForCompositing);
+ gfx::Matrix4x4 asyncUntransform =
+ (mAsyncTransform * overscroll).Inverse().ToUnknownMatrix();
+ const gfx::Matrix4x4& contentTransform = mScrollableContentTransform;
+ gfx::Matrix4x4 contentUntransform = contentTransform.Inverse();
+
+ compensation *= ViewAs<AsyncTransformComponentMatrix>(
+ contentTransform * asyncUntransform * contentUntransform);
+ }
+ transform = transform * compensation;
+
+ return transform;
+}
+
+LayerToParentLayerMatrix4x4 ComputeTransformForScrollThumb(
+ const LayerToParentLayerMatrix4x4& aCurrentTransform,
+ const gfx::Matrix4x4& aScrollableContentTransform,
+ AsyncPanZoomController* aApzc, const FrameMetrics& aMetrics,
+ const ScrollbarData& aScrollbarData, bool aScrollbarIsDescendant) {
+ return AsyncScrollThumbTransformer{
+ aCurrentTransform, aScrollableContentTransform, aApzc, aMetrics,
+ aScrollbarData, aScrollbarIsDescendant}
+ .ComputeTransform();
+}
+
+} // namespace apz
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/ScrollThumbUtils.h b/gfx/layers/apz/src/ScrollThumbUtils.h
new file mode 100644
index 0000000000..49a45de3a7
--- /dev/null
+++ b/gfx/layers/apz/src/ScrollThumbUtils.h
@@ -0,0 +1,51 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_ScrollThumbUtils_h
+#define mozilla_layers_ScrollThumbUtils_h
+
+#include "LayersTypes.h"
+#include "Units.h"
+
+namespace mozilla {
+namespace layers {
+
+class AsyncPanZoomController;
+
+struct FrameMetrics;
+struct ScrollbarData;
+
+namespace apz {
+/**
+ * Compute the updated shadow transform for a scroll thumb layer that
+ * reflects async scrolling of the associated scroll frame.
+ *
+ * @param aCurrentTransform The current shadow transform on the scroll thumb
+ * layer, as returned by Layer::GetLocalTransform() or similar.
+ * @param aScrollableContentTransform The current content transform on the
+ * scrollable content, as returned by Layer::GetTransform().
+ * @param aApzc The APZC that scrolls the scroll frame.
+ * @param aMetrics The metrics associated with the scroll frame, reflecting
+ * the last paint of the associated content. Note: this metrics should
+ * NOT reflect async scrolling or zooming, i.e. they should be the layer
+ * tree's copy of the metrics, or APZC's last-content-paint metrics.
+ * @param aScrollbarData The scrollbar data for the the scroll thumb layer.
+ * @param aScrollbarIsDescendant True iff. the scroll thumb layer is a
+ * descendant of the layer bearing the scroll frame's metrics.
+ * @return The new shadow transform for the scroll thumb layer, including
+ * any pre- or post-scales.
+ */
+LayerToParentLayerMatrix4x4 ComputeTransformForScrollThumb(
+ const LayerToParentLayerMatrix4x4& aCurrentTransform,
+ const gfx::Matrix4x4& aScrollableContentTransform,
+ AsyncPanZoomController* aApzc, const FrameMetrics& aMetrics,
+ const ScrollbarData& aScrollbarData, bool aScrollbarIsDescendant);
+
+} // namespace apz
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_ScrollThumbUtils_h
diff --git a/gfx/layers/apz/src/SimpleVelocityTracker.cpp b/gfx/layers/apz/src/SimpleVelocityTracker.cpp
new file mode 100644
index 0000000000..87cae10d51
--- /dev/null
+++ b/gfx/layers/apz/src/SimpleVelocityTracker.cpp
@@ -0,0 +1,135 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "SimpleVelocityTracker.h"
+
+#include "mozilla/ServoStyleConsts.h" // for StyleComputedTimingFunction
+#include "mozilla/StaticPrefs_apz.h"
+#include "mozilla/StaticPtr.h" // for StaticAutoPtr
+
+static mozilla::LazyLogModule sApzSvtLog("apz.simplevelocitytracker");
+#define SVT_LOG(...) MOZ_LOG(sApzSvtLog, LogLevel::Debug, (__VA_ARGS__))
+
+namespace mozilla {
+namespace layers {
+
+// When we compute the velocity we do so by taking two input events and
+// dividing the distance delta over the time delta. In some cases the time
+// delta can be really small, which can make the velocity computation very
+// volatile. To avoid this we impose a minimum time delta below which we do
+// not recompute the velocity.
+const TimeDuration MIN_VELOCITY_SAMPLE_TIME = TimeDuration::FromMilliseconds(5);
+
+extern StaticAutoPtr<StyleComputedTimingFunction> gVelocityCurveFunction;
+
+SimpleVelocityTracker::SimpleVelocityTracker(Axis* aAxis)
+ : mAxis(aAxis), mVelocitySamplePos(0) {}
+
+void SimpleVelocityTracker::StartTracking(ParentLayerCoord aPos,
+ TimeStamp aTimestamp) {
+ Clear();
+ mVelocitySampleTime = aTimestamp;
+ mVelocitySamplePos = aPos;
+}
+
+Maybe<float> SimpleVelocityTracker::AddPosition(ParentLayerCoord aPos,
+ TimeStamp aTimestamp) {
+ if (aTimestamp <= mVelocitySampleTime + MIN_VELOCITY_SAMPLE_TIME) {
+ // See also the comment on MIN_VELOCITY_SAMPLE_TIME.
+ // We don't update either mVelocitySampleTime or mVelocitySamplePos so that
+ // eventually when we do get an event with the required time delta we use
+ // the corresponding distance delta as well.
+ SVT_LOG("%p|%s skipping velocity computation for small time delta %f ms\n",
+ mAxis->OpaqueApzcPointer(), mAxis->Name(),
+ (aTimestamp - mVelocitySampleTime).ToMilliseconds());
+ return Nothing();
+ }
+
+ float newVelocity =
+ (float)(mVelocitySamplePos - aPos) /
+ (float)(aTimestamp - mVelocitySampleTime).ToMilliseconds();
+
+ newVelocity = ApplyFlingCurveToVelocity(newVelocity);
+
+ SVT_LOG("%p|%s updating velocity to %f with touch\n",
+ mAxis->OpaqueApzcPointer(), mAxis->Name(), newVelocity);
+ mVelocitySampleTime = aTimestamp;
+ mVelocitySamplePos = aPos;
+
+ AddVelocityToQueue(aTimestamp, newVelocity);
+
+ return Some(newVelocity);
+}
+
+Maybe<float> SimpleVelocityTracker::ComputeVelocity(TimeStamp aTimestamp) {
+ float velocity = 0;
+ int count = 0;
+ for (const auto& e : mVelocityQueue) {
+ TimeDuration timeDelta = (aTimestamp - e.first);
+ if (timeDelta < TimeDuration::FromMilliseconds(
+ StaticPrefs::apz_velocity_relevance_time_ms())) {
+ count++;
+ velocity += e.second;
+ }
+ }
+ mVelocityQueue.Clear();
+ if (count > 1) {
+ velocity /= count;
+ }
+ return Some(velocity);
+}
+
+void SimpleVelocityTracker::Clear() { mVelocityQueue.Clear(); }
+
+void SimpleVelocityTracker::AddVelocityToQueue(TimeStamp aTimestamp,
+ float aVelocity) {
+ mVelocityQueue.AppendElement(std::make_pair(aTimestamp, aVelocity));
+ if (mVelocityQueue.Length() >
+ StaticPrefs::apz_max_velocity_queue_size_AtStartup()) {
+ mVelocityQueue.RemoveElementAt(0);
+ }
+}
+
+float SimpleVelocityTracker::ApplyFlingCurveToVelocity(float aVelocity) const {
+ float newVelocity = aVelocity;
+ if (StaticPrefs::apz_max_velocity_inches_per_ms() > 0.0f) {
+ bool velocityIsNegative = (newVelocity < 0);
+ newVelocity = fabs(newVelocity);
+
+ float maxVelocity =
+ mAxis->ToLocalVelocity(StaticPrefs::apz_max_velocity_inches_per_ms());
+ newVelocity = std::min(newVelocity, maxVelocity);
+
+ if (StaticPrefs::apz_fling_curve_threshold_inches_per_ms() > 0.0f &&
+ StaticPrefs::apz_fling_curve_threshold_inches_per_ms() <
+ StaticPrefs::apz_max_velocity_inches_per_ms()) {
+ float curveThreshold = mAxis->ToLocalVelocity(
+ StaticPrefs::apz_fling_curve_threshold_inches_per_ms());
+ if (newVelocity > curveThreshold) {
+ // here, 0 < curveThreshold < newVelocity <= maxVelocity, so we apply
+ // the curve
+ float scale = maxVelocity - curveThreshold;
+ float funcInput = (newVelocity - curveThreshold) / scale;
+ float funcOutput =
+ gVelocityCurveFunction->At(funcInput, /* aBeforeFlag = */ false);
+ float curvedVelocity = (funcOutput * scale) + curveThreshold;
+ SVT_LOG("%p|%s curving up velocity from %f to %f\n",
+ mAxis->OpaqueApzcPointer(), mAxis->Name(), newVelocity,
+ curvedVelocity);
+ newVelocity = curvedVelocity;
+ }
+ }
+
+ if (velocityIsNegative) {
+ newVelocity = -newVelocity;
+ }
+ }
+
+ return newVelocity;
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/SimpleVelocityTracker.h b/gfx/layers/apz/src/SimpleVelocityTracker.h
new file mode 100644
index 0000000000..1778dee065
--- /dev/null
+++ b/gfx/layers/apz/src/SimpleVelocityTracker.h
@@ -0,0 +1,54 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_VelocityTracker_h
+#define mozilla_layers_VelocityTracker_h
+
+#include <utility>
+#include <cstdint>
+
+#include "Axis.h"
+#include "mozilla/Attributes.h"
+#include "nsTArray.h"
+
+namespace mozilla {
+namespace layers {
+
+class SimpleVelocityTracker : public VelocityTracker {
+ public:
+ explicit SimpleVelocityTracker(Axis* aAxis);
+ void StartTracking(ParentLayerCoord aPos, TimeStamp aTimestamp) override;
+ Maybe<float> AddPosition(ParentLayerCoord aPos,
+ TimeStamp aTimestamp) override;
+ Maybe<float> ComputeVelocity(TimeStamp aTimestamp) override;
+ void Clear() override;
+
+ private:
+ void AddVelocityToQueue(TimeStamp aTimestamp, float aVelocity);
+ float ApplyFlingCurveToVelocity(float aVelocity) const;
+
+ // The Axis that uses this velocity tracker.
+ // This is a raw pointer because the Axis owns the velocity tracker
+ // by UniquePtr, so the velocity tracker cannot outlive the Axis.
+ Axis* MOZ_NON_OWNING_REF mAxis;
+
+ // A queue of (timestamp, velocity) pairs; these are the historical
+ // velocities at the given timestamps. Velocities are in screen pixels per ms.
+ // This member can only be accessed on the controller/UI thread.
+ nsTArray<std::pair<TimeStamp, float>> mVelocityQueue;
+
+ // mVelocitySampleTime and mVelocitySamplePos are the time and position
+ // used in the last velocity sampling. They get updated when a new sample is
+ // taken (which may not happen on every input event, if the time delta is too
+ // small).
+ TimeStamp mVelocitySampleTime;
+ ParentLayerCoord mVelocitySamplePos;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif
diff --git a/gfx/layers/apz/src/SmoothMsdScrollAnimation.cpp b/gfx/layers/apz/src/SmoothMsdScrollAnimation.cpp
new file mode 100644
index 0000000000..8342dc157f
--- /dev/null
+++ b/gfx/layers/apz/src/SmoothMsdScrollAnimation.cpp
@@ -0,0 +1,139 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#include "SmoothMsdScrollAnimation.h"
+#include "AsyncPanZoomController.h"
+
+namespace mozilla {
+namespace layers {
+
+SmoothMsdScrollAnimation::SmoothMsdScrollAnimation(
+ AsyncPanZoomController& aApzc, const CSSPoint& aInitialPosition,
+ const CSSPoint& aInitialVelocity, const CSSPoint& aDestination,
+ double aSpringConstant, double aDampingRatio,
+ ScrollSnapTargetIds&& aSnapTargetIds,
+ ScrollTriggeredByScript aTriggeredByScript)
+ : mApzc(aApzc),
+ mXAxisModel(aInitialPosition.x, aDestination.x, aInitialVelocity.x,
+ aSpringConstant, aDampingRatio),
+ mYAxisModel(aInitialPosition.y, aDestination.y, aInitialVelocity.y,
+ aSpringConstant, aDampingRatio),
+ mSnapTargetIds(std::move(aSnapTargetIds)),
+ mTriggeredByScript(aTriggeredByScript) {}
+
+bool SmoothMsdScrollAnimation::DoSample(FrameMetrics& aFrameMetrics,
+ const TimeDuration& aDelta) {
+ CSSToParentLayerScale zoom(aFrameMetrics.GetZoom());
+ if (zoom == CSSToParentLayerScale(0)) {
+ return false;
+ }
+ CSSPoint oneParentLayerPixel =
+ ParentLayerPoint(1, 1) / aFrameMetrics.GetZoom();
+ if (mXAxisModel.IsFinished(oneParentLayerPixel.x) &&
+ mYAxisModel.IsFinished(oneParentLayerPixel.y)) {
+ // Set the scroll offset to the exact destination. If we allow the scroll
+ // offset to end up being a bit off from the destination, we can get
+ // artefacts like "scroll to the next snap point in this direction"
+ // scrolling to the snap point we're already supposed to be at.
+ mApzc.ClampAndSetVisualScrollOffset(
+ CSSPoint(mXAxisModel.GetDestination(), mYAxisModel.GetDestination()));
+ return false;
+ }
+
+ mXAxisModel.Simulate(aDelta);
+ mYAxisModel.Simulate(aDelta);
+
+ CSSPoint position =
+ CSSPoint(mXAxisModel.GetPosition(), mYAxisModel.GetPosition());
+ CSSPoint css_velocity =
+ CSSPoint(mXAxisModel.GetVelocity(), mYAxisModel.GetVelocity());
+
+ // Convert from pixels/second to pixels/ms
+ ParentLayerPoint velocity =
+ ParentLayerPoint(css_velocity.x, css_velocity.y) / 1000.0f;
+
+ // Keep the velocity updated for the Axis class so that any animations
+ // chained off of the smooth scroll will inherit it.
+ if (mXAxisModel.IsFinished(oneParentLayerPixel.x)) {
+ mApzc.mX.SetVelocity(0);
+ } else {
+ mApzc.mX.SetVelocity(velocity.x);
+ }
+ if (mYAxisModel.IsFinished(oneParentLayerPixel.y)) {
+ mApzc.mY.SetVelocity(0);
+ } else {
+ mApzc.mY.SetVelocity(velocity.y);
+ }
+ // If we overscroll, hand off to a fling animation that will complete the
+ // spring back.
+ ParentLayerPoint displacement =
+ (position - aFrameMetrics.GetVisualScrollOffset()) * zoom;
+
+ ParentLayerPoint overscroll;
+ ParentLayerPoint adjustedOffset;
+ mApzc.mX.AdjustDisplacement(displacement.x, adjustedOffset.x, overscroll.x);
+ mApzc.mY.AdjustDisplacement(displacement.y, adjustedOffset.y, overscroll.y);
+ mApzc.ScrollBy(adjustedOffset / zoom);
+ // The smooth scroll may have caused us to reach the end of our scroll
+ // range. This can happen if either the
+ // layout.css.scroll-behavior.damping-ratio preference is set to less than 1
+ // (underdamped) or if a smooth scroll inherits velocity from a fling
+ // gesture.
+ if (!IsZero(overscroll / zoom)) {
+ // Hand off a fling with the remaining momentum to the next APZC in the
+ // overscroll handoff chain.
+
+ // We may have reached the end of the scroll range along one axis but
+ // not the other. In such a case we only want to hand off the relevant
+ // component of the fling.
+ if (mApzc.IsZero(overscroll.x)) {
+ velocity.x = 0;
+ } else if (mApzc.IsZero(overscroll.y)) {
+ velocity.y = 0;
+ }
+
+ // To hand off the fling, we attempt to find a target APZC and start a new
+ // fling with the same velocity on that APZC. For simplicity, the actual
+ // overscroll of the current sample is discarded rather than being handed
+ // off. The compositor should sample animations sufficiently frequently
+ // that this is not noticeable. The target APZC is chosen by seeing if
+ // there is an APZC further in the handoff chain which is pannable; if
+ // there isn't, we take the new fling ourselves, entering an overscrolled
+ // state.
+ // Note: APZC is holding mRecursiveMutex, so directly calling
+ // HandleSmoothScrollOverscroll() (which acquires the tree lock) would
+ // violate the lock ordering. Instead we schedule
+ // HandleSmoothScrollOverscroll() to be called after mRecursiveMutex is
+ // released.
+ mDeferredTasks.AppendElement(NewRunnableMethod<ParentLayerPoint, SideBits>(
+ "layers::AsyncPanZoomController::HandleSmoothScrollOverscroll", &mApzc,
+ &AsyncPanZoomController::HandleSmoothScrollOverscroll, velocity,
+ apz::GetOverscrollSideBits(overscroll)));
+ return false;
+ }
+
+ return true;
+}
+
+void SmoothMsdScrollAnimation::SetDestination(
+ const CSSPoint& aNewDestination, ScrollSnapTargetIds&& aSnapTargetIds,
+ ScrollTriggeredByScript aTriggeredByScript) {
+ mXAxisModel.SetDestination(aNewDestination.x);
+ mYAxisModel.SetDestination(aNewDestination.y);
+ mSnapTargetIds = std::move(aSnapTargetIds);
+ mTriggeredByScript = aTriggeredByScript;
+}
+
+CSSPoint SmoothMsdScrollAnimation::GetDestination() const {
+ return CSSPoint(mXAxisModel.GetDestination(), mYAxisModel.GetDestination());
+}
+
+SmoothMsdScrollAnimation*
+SmoothMsdScrollAnimation::AsSmoothMsdScrollAnimation() {
+ return this;
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/SmoothMsdScrollAnimation.h b/gfx/layers/apz/src/SmoothMsdScrollAnimation.h
new file mode 100644
index 0000000000..1f2247c473
--- /dev/null
+++ b/gfx/layers/apz/src/SmoothMsdScrollAnimation.h
@@ -0,0 +1,61 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_SmoothMsdScrollAnimation_h_
+#define mozilla_layers_SmoothMsdScrollAnimation_h_
+
+#include "AsyncPanZoomAnimation.h"
+#include "mozilla/layers/AxisPhysicsMSDModel.h"
+#include "mozilla/ScrollPositionUpdate.h"
+
+namespace mozilla {
+namespace layers {
+
+class AsyncPanZoomController;
+
+class SmoothMsdScrollAnimation final : public AsyncPanZoomAnimation {
+ public:
+ SmoothMsdScrollAnimation(AsyncPanZoomController& aApzc,
+ const CSSPoint& aInitialPosition,
+ const CSSPoint& aInitialVelocity,
+ const CSSPoint& aDestination, double aSpringConstant,
+ double aDampingRatio,
+ ScrollSnapTargetIds&& aSnapTargetIds,
+ ScrollTriggeredByScript aTriggeredByScript);
+
+ /**
+ * Advances a smooth scroll simulation based on the time passed in |aDelta|.
+ * This should be called whenever sampling the content transform for this
+ * frame. Returns true if the smooth scroll should be advanced by one frame,
+ * or false if the smooth scroll has ended.
+ */
+ bool DoSample(FrameMetrics& aFrameMetrics,
+ const TimeDuration& aDelta) override;
+
+ void SetDestination(const CSSPoint& aNewDestination,
+ ScrollSnapTargetIds&& aSnapTargetIds,
+ ScrollTriggeredByScript aTriggeredByScript);
+ CSSPoint GetDestination() const;
+ SmoothMsdScrollAnimation* AsSmoothMsdScrollAnimation() override;
+
+ bool WasTriggeredByScript() const override {
+ return mTriggeredByScript == ScrollTriggeredByScript::Yes;
+ }
+
+ ScrollSnapTargetIds TakeSnapTargetIds() { return std::move(mSnapTargetIds); }
+
+ private:
+ AsyncPanZoomController& mApzc;
+ AxisPhysicsMSDModel mXAxisModel;
+ AxisPhysicsMSDModel mYAxisModel;
+ ScrollSnapTargetIds mSnapTargetIds;
+ ScrollTriggeredByScript mTriggeredByScript;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif
diff --git a/gfx/layers/apz/src/SmoothScrollAnimation.cpp b/gfx/layers/apz/src/SmoothScrollAnimation.cpp
new file mode 100644
index 0000000000..266c027a55
--- /dev/null
+++ b/gfx/layers/apz/src/SmoothScrollAnimation.cpp
@@ -0,0 +1,46 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "SmoothScrollAnimation.h"
+#include "ScrollAnimationBezierPhysics.h"
+#include "mozilla/layers/APZPublicUtils.h"
+
+namespace mozilla {
+namespace layers {
+
+SmoothScrollAnimation::SmoothScrollAnimation(AsyncPanZoomController& aApzc,
+ const nsPoint& aInitialPosition,
+ ScrollOrigin aOrigin)
+ : GenericScrollAnimation(
+ aApzc, aInitialPosition,
+ apz::ComputeBezierAnimationSettingsForOrigin(aOrigin)),
+ mOrigin(aOrigin) {}
+
+SmoothScrollAnimation* SmoothScrollAnimation::AsSmoothScrollAnimation() {
+ return this;
+}
+
+ScrollOrigin SmoothScrollAnimation::GetScrollOrigin() const { return mOrigin; }
+
+ScrollOrigin SmoothScrollAnimation::GetScrollOriginForAction(
+ KeyboardScrollAction::KeyboardScrollActionType aAction) {
+ switch (aAction) {
+ case KeyboardScrollAction::eScrollCharacter:
+ case KeyboardScrollAction::eScrollLine: {
+ return ScrollOrigin::Lines;
+ }
+ case KeyboardScrollAction::eScrollPage:
+ return ScrollOrigin::Pages;
+ case KeyboardScrollAction::eScrollComplete:
+ return ScrollOrigin::Other;
+ default:
+ MOZ_ASSERT(false, "Unknown keyboard scroll action type");
+ return ScrollOrigin::Other;
+ }
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/SmoothScrollAnimation.h b/gfx/layers/apz/src/SmoothScrollAnimation.h
new file mode 100644
index 0000000000..1143744cc1
--- /dev/null
+++ b/gfx/layers/apz/src/SmoothScrollAnimation.h
@@ -0,0 +1,37 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_SmoothScrollAnimation_h_
+#define mozilla_layers_SmoothScrollAnimation_h_
+
+#include "GenericScrollAnimation.h"
+#include "mozilla/ScrollOrigin.h"
+#include "mozilla/layers/KeyboardScrollAction.h"
+
+namespace mozilla {
+namespace layers {
+
+class AsyncPanZoomController;
+
+class SmoothScrollAnimation : public GenericScrollAnimation {
+ public:
+ SmoothScrollAnimation(AsyncPanZoomController& aApzc,
+ const nsPoint& aInitialPosition,
+ ScrollOrigin aScrollOrigin);
+
+ SmoothScrollAnimation* AsSmoothScrollAnimation() override;
+ ScrollOrigin GetScrollOrigin() const;
+ static ScrollOrigin GetScrollOriginForAction(
+ KeyboardScrollAction::KeyboardScrollActionType aAction);
+
+ private:
+ ScrollOrigin mOrigin;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_SmoothScrollAnimation_h_
diff --git a/gfx/layers/apz/src/WRHitTester.cpp b/gfx/layers/apz/src/WRHitTester.cpp
new file mode 100644
index 0000000000..873400976f
--- /dev/null
+++ b/gfx/layers/apz/src/WRHitTester.cpp
@@ -0,0 +1,247 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "WRHitTester.h"
+#include "AsyncPanZoomController.h"
+#include "APZCTreeManager.h"
+#include "TreeTraversal.h" // for BreadthFirstSearch
+#include "mozilla/gfx/CompositorHitTestInfo.h"
+#include "mozilla/webrender/WebRenderAPI.h"
+#include "nsDebug.h" // for NS_ASSERTION
+#include "nsIXULRuntime.h" // for FissionAutostart
+#include "mozilla/gfx/Matrix.h"
+
+#define APZCTM_LOG(...) \
+ MOZ_LOG(APZCTreeManager::sLog, LogLevel::Debug, (__VA_ARGS__))
+
+namespace mozilla {
+namespace layers {
+
+using mozilla::gfx::CompositorHitTestFlags;
+using mozilla::gfx::CompositorHitTestInvisibleToHit;
+
+static bool CheckCloseToIdentity(const gfx::Matrix4x4& aMatrix) {
+ // We allow a factor of 1/2048 in the multiply part of the matrix, so that if
+ // we multiply by a point on a screen of size 2048 we would be off by at most
+ // 1 pixel approximately.
+ const float multiplyEps = 1 / 2048.f;
+ // We allow 1 pixel in the translate part of the matrix.
+ const float translateEps = 1.f;
+
+ if (!FuzzyEqualsAdditive(aMatrix._11, 1.f, multiplyEps) ||
+ !FuzzyEqualsAdditive(aMatrix._12, 0.f, multiplyEps) ||
+ !FuzzyEqualsAdditive(aMatrix._13, 0.f, multiplyEps) ||
+ !FuzzyEqualsAdditive(aMatrix._14, 0.f, multiplyEps) ||
+ !FuzzyEqualsAdditive(aMatrix._21, 0.f, multiplyEps) ||
+ !FuzzyEqualsAdditive(aMatrix._22, 1.f, multiplyEps) ||
+ !FuzzyEqualsAdditive(aMatrix._23, 0.f, multiplyEps) ||
+ !FuzzyEqualsAdditive(aMatrix._24, 0.f, multiplyEps) ||
+ !FuzzyEqualsAdditive(aMatrix._31, 0.f, multiplyEps) ||
+ !FuzzyEqualsAdditive(aMatrix._32, 0.f, multiplyEps) ||
+ !FuzzyEqualsAdditive(aMatrix._33, 1.f, multiplyEps) ||
+ !FuzzyEqualsAdditive(aMatrix._34, 0.f, multiplyEps) ||
+ !FuzzyEqualsAdditive(aMatrix._41, 0.f, translateEps) ||
+ !FuzzyEqualsAdditive(aMatrix._42, 0.f, translateEps) ||
+ !FuzzyEqualsAdditive(aMatrix._43, 0.f, translateEps) ||
+ !FuzzyEqualsAdditive(aMatrix._44, 1.f, multiplyEps)) {
+ return false;
+ }
+ return true;
+}
+
+// Checks that within the constraints of floating point math we can invert it
+// reasonably enough that multiplying by the computed inverse is close to the
+// identity.
+static bool CheckInvertibleWithFinitePrecision(const gfx::Matrix4x4& aMatrix) {
+ auto inverse = aMatrix.MaybeInverse();
+ if (inverse.isNothing()) {
+ // Should we return false?
+ return true;
+ }
+ if (!CheckCloseToIdentity(aMatrix * *inverse)) {
+ return false;
+ }
+ if (!CheckCloseToIdentity(*inverse * aMatrix)) {
+ return false;
+ }
+ return true;
+}
+
+IAPZHitTester::HitTestResult WRHitTester::GetAPZCAtPoint(
+ const ScreenPoint& aHitTestPoint,
+ const RecursiveMutexAutoLock& aProofOfTreeLock) {
+ HitTestResult hit;
+ RefPtr<wr::WebRenderAPI> wr = mTreeManager->GetWebRenderAPI();
+ if (!wr) {
+ // If WebRender isn't running, fall back to the root APZC.
+ // This is mostly for the benefit of GTests which do not
+ // run a WebRender instance, but gracefully falling back
+ // here allows those tests which are not specifically
+ // testing the hit-test algorithm to still work.
+ hit.mTargetApzc = FindRootApzcForLayersId(GetRootLayersId());
+ hit.mHitResult = CompositorHitTestFlags::eVisibleToHitTest;
+ return hit;
+ }
+
+ APZCTM_LOG("Hit-testing point %s with WR\n", ToString(aHitTestPoint).c_str());
+ std::vector<wr::WrHitResult> results =
+ wr->HitTest(wr::ToWorldPoint(aHitTestPoint));
+
+ Maybe<wr::WrHitResult> chosenResult;
+ for (const wr::WrHitResult& result : results) {
+ ScrollableLayerGuid guid{result.mLayersId, 0, result.mScrollId};
+ APZCTM_LOG("Examining result with guid %s hit info 0x%x... ",
+ ToString(guid).c_str(), result.mHitInfo.serialize());
+ if (result.mHitInfo == CompositorHitTestInvisibleToHit) {
+ APZCTM_LOG("skipping due to invisibility.\n");
+ continue;
+ }
+ RefPtr<HitTestingTreeNode> node =
+ GetTargetNode(guid, &ScrollableLayerGuid::EqualsIgnoringPresShell);
+ if (!node) {
+ APZCTM_LOG("no corresponding node found, falling back to root.\n");
+
+#ifdef DEBUG
+ // We can enter here during normal codepaths for cases where the
+ // nsDisplayCompositorHitTestInfo item emitted a scrollId of
+ // NULL_SCROLL_ID to the webrender display list. The semantics of that
+ // is to fall back to the root APZC for the layers id, so that's what
+ // we do here.
+ // If we enter this codepath and scrollId is not NULL_SCROLL_ID, then
+ // that's more likely to be due to a race condition between rebuilding
+ // the APZ tree and updating the WR scene/hit-test information, resulting
+ // in WR giving us a hit result for a scene that is not active in APZ.
+ // Such a scenario would need debugging and fixing.
+ // In non-Fission mode, make this assertion non-fatal because there is
+ // a known issue related to inactive scroll frames that can cause this
+ // to fire (see bug 1634763), which is fixed in Fission mode and not
+ // worth fixing in non-Fission mode.
+ if (FissionAutostart()) {
+ MOZ_ASSERT(result.mScrollId == ScrollableLayerGuid::NULL_SCROLL_ID);
+ } else {
+ NS_ASSERTION(
+ result.mScrollId == ScrollableLayerGuid::NULL_SCROLL_ID,
+ "Inconsistency between WebRender display list and APZ scroll data");
+ }
+#endif
+ node = FindRootNodeForLayersId(result.mLayersId);
+ if (!node) {
+ // Should never happen, but handle gracefully in release builds just
+ // in case.
+ MOZ_ASSERT(false);
+ chosenResult = Some(result);
+ break;
+ }
+ }
+ MOZ_ASSERT(node->GetApzc()); // any node returned must have an APZC
+ EventRegionsOverride flags = node->GetEventRegionsOverride();
+ if (flags & EventRegionsOverride::ForceEmptyHitRegion) {
+ // This result is inside a subtree that is invisible to hit-testing.
+ APZCTM_LOG("skipping due to FEHR subtree.\n");
+ continue;
+ }
+
+ if (!CheckInvertibleWithFinitePrecision(
+ mTreeManager->GetScreenToApzcTransform(node->GetApzc())
+ .ToUnknownMatrix())) {
+ APZCTM_LOG("skipping due to check inverse accuracy\n");
+ continue;
+ }
+
+ APZCTM_LOG("selecting as chosen result.\n");
+ chosenResult = Some(result);
+ hit.mTargetApzc = node->GetApzc();
+ if (flags & EventRegionsOverride::ForceDispatchToContent) {
+ chosenResult->mHitInfo += CompositorHitTestFlags::eApzAwareListeners;
+ }
+ break;
+ }
+ if (!chosenResult) {
+ return hit;
+ }
+
+ MOZ_ASSERT(hit.mTargetApzc);
+ hit.mLayersId = chosenResult->mLayersId;
+ ScrollableLayerGuid::ViewID scrollId = chosenResult->mScrollId;
+ gfx::CompositorHitTestInfo hitInfo = chosenResult->mHitInfo;
+ Maybe<uint64_t> animationId = chosenResult->mAnimationId;
+ SideBits sideBits = chosenResult->mSideBits;
+
+ APZCTM_LOG("Successfully matched APZC %p (hit result 0x%x)\n",
+ hit.mTargetApzc.get(), hitInfo.serialize());
+
+ const bool isScrollbar =
+ hitInfo.contains(gfx::CompositorHitTestFlags::eScrollbar);
+ const bool isScrollbarThumb =
+ hitInfo.contains(gfx::CompositorHitTestFlags::eScrollbarThumb);
+ const ScrollDirection direction =
+ hitInfo.contains(gfx::CompositorHitTestFlags::eScrollbarVertical)
+ ? ScrollDirection::eVertical
+ : ScrollDirection::eHorizontal;
+ HitTestingTreeNode* scrollbarNode = nullptr;
+ if (isScrollbar || isScrollbarThumb) {
+ scrollbarNode = BreadthFirstSearch<ReverseIterator>(
+ GetRootNode(), [&](HitTestingTreeNode* aNode) {
+ return (aNode->GetLayersId() == hit.mLayersId) &&
+ (aNode->IsScrollbarNode() == isScrollbar) &&
+ (aNode->IsScrollThumbNode() == isScrollbarThumb) &&
+ (aNode->GetScrollbarDirection() == direction) &&
+ (aNode->GetScrollTargetId() == scrollId);
+ });
+ }
+
+ hit.mHitResult = hitInfo;
+
+ if (scrollbarNode) {
+ RefPtr<HitTestingTreeNode> scrollbarRef = scrollbarNode;
+ InitializeHitTestingTreeNodeAutoLock(hit.mScrollbarNode, aProofOfTreeLock,
+ scrollbarRef);
+ }
+
+ hit.mFixedPosSides = sideBits;
+ if (animationId.isSome()) {
+ RefPtr<HitTestingTreeNode> positionedNode = nullptr;
+
+ positionedNode = BreadthFirstSearch<ReverseIterator>(
+ GetRootNode(), [&](HitTestingTreeNode* aNode) {
+ return (aNode->GetFixedPositionAnimationId() == animationId ||
+ aNode->GetStickyPositionAnimationId() == animationId);
+ });
+
+ if (positionedNode) {
+ MOZ_ASSERT(positionedNode->GetLayersId() == chosenResult->mLayersId,
+ "Found node layers id does not match the hit result");
+ MOZ_ASSERT((positionedNode->GetFixedPositionAnimationId().isSome() ||
+ positionedNode->GetStickyPositionAnimationId().isSome()),
+ "A a matching fixed/sticky position node should be found");
+ InitializeHitTestingTreeNodeAutoLock(hit.mNode, aProofOfTreeLock,
+ positionedNode);
+ }
+
+#if defined(MOZ_WIDGET_ANDROID)
+ if (hit.mNode && hit.mNode->GetFixedPositionAnimationId().isSome()) {
+ // If the hit element is a fixed position element, the side bits from
+ // the hit-result item tag are used. For now just ensure that these
+ // match what is found in the hit-testing tree node.
+ MOZ_ASSERT(sideBits == hit.mNode->GetFixedPosSides(),
+ "Fixed position side bits do not match");
+ } else if (hit.mTargetApzc && hit.mTargetApzc->IsRootContent()) {
+ // If the hit element is not a fixed position element, then the hit test
+ // result item's side bits should not be populated.
+ MOZ_ASSERT(sideBits == SideBits::eNone,
+ "Hit test results have side bits only for pos:fixed");
+ }
+#endif
+ }
+
+ hit.mHitOverscrollGutter =
+ hit.mTargetApzc && hit.mTargetApzc->IsInOverscrollGutter(aHitTestPoint);
+
+ return hit;
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/WRHitTester.h b/gfx/layers/apz/src/WRHitTester.h
new file mode 100644
index 0000000000..abb9de1a66
--- /dev/null
+++ b/gfx/layers/apz/src/WRHitTester.h
@@ -0,0 +1,26 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_WRHitTester_h
+#define mozilla_layers_WRHitTester_h
+
+#include "IAPZHitTester.h"
+
+namespace mozilla {
+namespace layers {
+
+// IAPZHitTester implementation for WebRender.
+class WRHitTester : public IAPZHitTester {
+ public:
+ virtual HitTestResult GetAPZCAtPoint(
+ const ScreenPoint& aHitTestPoint,
+ const RecursiveMutexAutoLock& aProofOfTreeLock) override;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // define mozilla_layers_WRHitTester_h
diff --git a/gfx/layers/apz/src/WheelScrollAnimation.cpp b/gfx/layers/apz/src/WheelScrollAnimation.cpp
new file mode 100644
index 0000000000..6203bcb8fa
--- /dev/null
+++ b/gfx/layers/apz/src/WheelScrollAnimation.cpp
@@ -0,0 +1,64 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "WheelScrollAnimation.h"
+
+#include <tuple>
+#include "AsyncPanZoomController.h"
+#include "mozilla/StaticPrefs_general.h"
+#include "nsPoint.h"
+#include "ScrollAnimationBezierPhysics.h"
+
+namespace mozilla {
+namespace layers {
+
+static ScrollAnimationBezierPhysicsSettings SettingsForDeltaType(
+ ScrollWheelInput::ScrollDeltaType aDeltaType) {
+ int32_t minMS = 0;
+ int32_t maxMS = 0;
+
+ switch (aDeltaType) {
+ case ScrollWheelInput::SCROLLDELTA_PAGE:
+ maxMS = clamped(StaticPrefs::general_smoothScroll_pages_durationMaxMS(),
+ 0, 10000);
+ minMS = clamped(StaticPrefs::general_smoothScroll_pages_durationMinMS(),
+ 0, maxMS);
+ break;
+ case ScrollWheelInput::SCROLLDELTA_PIXEL:
+ maxMS = clamped(StaticPrefs::general_smoothScroll_pixels_durationMaxMS(),
+ 0, 10000);
+ minMS = clamped(StaticPrefs::general_smoothScroll_pixels_durationMinMS(),
+ 0, maxMS);
+ break;
+ case ScrollWheelInput::SCROLLDELTA_LINE:
+ maxMS =
+ clamped(StaticPrefs::general_smoothScroll_mouseWheel_durationMaxMS(),
+ 0, 10000);
+ minMS =
+ clamped(StaticPrefs::general_smoothScroll_mouseWheel_durationMinMS(),
+ 0, maxMS);
+ break;
+ }
+
+ // The pref is 100-based int percentage, while mIntervalRatio is 1-based ratio
+ double intervalRatio =
+ ((double)StaticPrefs::general_smoothScroll_durationToIntervalRatio()) /
+ 100.0;
+ intervalRatio = std::max(1.0, intervalRatio);
+ return ScrollAnimationBezierPhysicsSettings{minMS, maxMS, intervalRatio};
+}
+
+WheelScrollAnimation::WheelScrollAnimation(
+ AsyncPanZoomController& aApzc, const nsPoint& aInitialPosition,
+ ScrollWheelInput::ScrollDeltaType aDeltaType)
+ : GenericScrollAnimation(aApzc, aInitialPosition,
+ SettingsForDeltaType(aDeltaType)) {
+ mDirectionForcedToOverscroll =
+ mApzc.mScrollMetadata.GetDisregardedDirection();
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/WheelScrollAnimation.h b/gfx/layers/apz/src/WheelScrollAnimation.h
new file mode 100644
index 0000000000..7c039ef3fd
--- /dev/null
+++ b/gfx/layers/apz/src/WheelScrollAnimation.h
@@ -0,0 +1,30 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_WheelScrollAnimation_h_
+#define mozilla_layers_WheelScrollAnimation_h_
+
+#include "GenericScrollAnimation.h"
+#include "InputData.h"
+
+namespace mozilla {
+namespace layers {
+
+class AsyncPanZoomController;
+
+class WheelScrollAnimation : public GenericScrollAnimation {
+ public:
+ WheelScrollAnimation(AsyncPanZoomController& aApzc,
+ const nsPoint& aInitialPosition,
+ ScrollWheelInput::ScrollDeltaType aDeltaType);
+
+ WheelScrollAnimation* AsWheelScrollAnimation() override { return this; }
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_WheelScrollAnimation_h_
diff --git a/gfx/layers/apz/test/gtest/APZCBasicTester.h b/gfx/layers/apz/test/gtest/APZCBasicTester.h
new file mode 100644
index 0000000000..621a3d37be
--- /dev/null
+++ b/gfx/layers/apz/test/gtest/APZCBasicTester.h
@@ -0,0 +1,102 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_APZCBasicTester_h
+#define mozilla_layers_APZCBasicTester_h
+
+/**
+ * Defines a test fixture used for testing a single APZC.
+ */
+
+#include "APZTestCommon.h"
+
+#include "mozilla/layers/APZSampler.h"
+#include "mozilla/layers/APZUpdater.h"
+
+class APZCBasicTester : public APZCTesterBase {
+ public:
+ explicit APZCBasicTester(
+ AsyncPanZoomController::GestureBehavior aGestureBehavior =
+ AsyncPanZoomController::DEFAULT_GESTURES)
+ : mGestureBehavior(aGestureBehavior) {}
+
+ protected:
+ virtual void SetUp() {
+ APZCTesterBase::SetUp();
+ APZThreadUtils::SetThreadAssertionsEnabled(false);
+ APZThreadUtils::SetControllerThread(NS_GetCurrentThread());
+
+ tm = new TestAPZCTreeManager(mcc);
+ updater = new APZUpdater(tm, false);
+ sampler = new APZSampler(tm, false);
+ apzc =
+ new TestAsyncPanZoomController(LayersId{0}, mcc, tm, mGestureBehavior);
+ apzc->SetFrameMetrics(TestFrameMetrics());
+ apzc->GetScrollMetadata().SetIsLayersIdRoot(true);
+ }
+
+ /**
+ * Get the APZC's scroll range in CSS pixels.
+ */
+ CSSRect GetScrollRange() const {
+ const FrameMetrics& metrics = apzc->GetFrameMetrics();
+ return CSSRect(metrics.GetScrollableRect().TopLeft(),
+ metrics.GetScrollableRect().Size() -
+ metrics.CalculateCompositedSizeInCssPixels());
+ }
+
+ virtual void TearDown() {
+ while (mcc->RunThroughDelayedTasks())
+ ;
+ apzc->Destroy();
+ tm->ClearTree();
+ tm->ClearContentController();
+
+ APZCTesterBase::TearDown();
+ }
+
+ void MakeApzcWaitForMainThread() { apzc->SetWaitForMainThread(); }
+
+ void MakeApzcZoomable() {
+ apzc->UpdateZoomConstraints(ZoomConstraints(
+ true, true, CSSToParentLayerScale(0.25f), CSSToParentLayerScale(4.0f)));
+ }
+
+ void MakeApzcUnzoomable() {
+ apzc->UpdateZoomConstraints(ZoomConstraints(false, false,
+ CSSToParentLayerScale(1.0f),
+ CSSToParentLayerScale(1.0f)));
+ }
+
+ /**
+ * Sample animations once, 1 ms later than the last sample.
+ */
+ void SampleAnimationOnce() {
+ const TimeDuration increment = TimeDuration::FromMilliseconds(1);
+ ParentLayerPoint pointOut;
+ AsyncTransform viewTransformOut;
+ mcc->AdvanceBy(increment);
+ apzc->SampleContentTransformForFrame(&viewTransformOut, pointOut);
+ }
+ /**
+ * Sample animations one frame, 17 ms later than the last sample.
+ */
+ void SampleAnimationOneFrame() {
+ const TimeDuration increment = TimeDuration::FromMilliseconds(17);
+ ParentLayerPoint pointOut;
+ AsyncTransform viewTransformOut;
+ mcc->AdvanceBy(increment);
+ apzc->SampleContentTransformForFrame(&viewTransformOut, pointOut);
+ }
+
+ AsyncPanZoomController::GestureBehavior mGestureBehavior;
+ RefPtr<TestAPZCTreeManager> tm;
+ RefPtr<APZSampler> sampler;
+ RefPtr<APZUpdater> updater;
+ RefPtr<TestAsyncPanZoomController> apzc;
+};
+
+#endif // mozilla_layers_APZCBasicTester_h
diff --git a/gfx/layers/apz/test/gtest/APZCTreeManagerTester.h b/gfx/layers/apz/test/gtest/APZCTreeManagerTester.h
new file mode 100644
index 0000000000..8a2f9d749b
--- /dev/null
+++ b/gfx/layers/apz/test/gtest/APZCTreeManagerTester.h
@@ -0,0 +1,223 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_APZCTreeManagerTester_h
+#define mozilla_layers_APZCTreeManagerTester_h
+
+/**
+ * Defines a test fixture used for testing multiple APZCs interacting in
+ * an APZCTreeManager.
+ */
+
+#include "APZTestAccess.h"
+#include "APZTestCommon.h"
+#include "gfxPlatform.h"
+#include "MockHitTester.h"
+#include "apz/src/WRHitTester.h"
+
+#include "mozilla/layers/APZSampler.h"
+#include "mozilla/layers/APZUpdater.h"
+#include "mozilla/layers/WebRenderScrollDataWrapper.h"
+
+class APZCTreeManagerTester : public APZCTesterBase {
+ protected:
+ APZCTreeManagerTester() : mHitTester(MakeUnique<WRHitTester>()) {}
+
+ virtual void SetUp() {
+ APZCTesterBase::SetUp();
+
+ APZThreadUtils::SetThreadAssertionsEnabled(false);
+ APZThreadUtils::SetControllerThread(NS_GetCurrentThread());
+
+ manager = new TestAPZCTreeManager(mcc, std::move(mHitTester));
+ updater = new APZUpdater(manager, false);
+ sampler = new APZSampler(manager, false);
+ }
+
+ virtual void TearDown() {
+ while (mcc->RunThroughDelayedTasks())
+ ;
+ manager->ClearTree();
+ manager->ClearContentController();
+
+ APZCTesterBase::TearDown();
+ }
+
+ /**
+ * Sample animations once for all APZCs, 1 ms later than the last sample and
+ * return whether there is still any active animations or not.
+ */
+ bool SampleAnimationsOnce() {
+ const TimeDuration increment = TimeDuration::FromMilliseconds(1);
+ ParentLayerPoint pointOut;
+ AsyncTransform viewTransformOut;
+ mcc->AdvanceBy(increment);
+
+ bool activeAnimations = false;
+
+ for (size_t i = 0; i < layers.GetLayerCount(); ++i) {
+ if (TestAsyncPanZoomController* apzc = ApzcOf(layers[i])) {
+ activeAnimations |=
+ apzc->SampleContentTransformForFrame(&viewTransformOut, pointOut);
+ }
+ }
+
+ return activeAnimations;
+ }
+
+ // A convenience function for letting a test modify the frame metrics
+ // stored on a particular layer.
+ template <typename Callback>
+ void ModifyFrameMetrics(WebRenderLayerScrollData* aLayer,
+ Callback aCallback) {
+ MOZ_ASSERT(aLayer->GetScrollMetadataCount() == 1);
+ ScrollMetadata& metadataRef =
+ APZTestAccess::GetScrollMetadataMut(*aLayer, layers, 0);
+ aCallback(metadataRef, metadataRef.GetMetrics());
+ }
+
+ // A convenience wrapper for manager->UpdateHitTestingTree().
+ void UpdateHitTestingTree(uint32_t aPaintSequenceNumber = 0) {
+ manager->UpdateHitTestingTree(WebRenderScrollDataWrapper{*updater, &layers},
+ /* is first paint = */ false, LayersId{0},
+ aPaintSequenceNumber);
+ }
+
+ void CreateScrollData(const char* aTreeShape,
+ const LayerIntRegion* aVisibleRegions = nullptr,
+ const gfx::Matrix4x4* aTransforms = nullptr) {
+ layers = TestWRScrollData::Create(aTreeShape, *updater, aVisibleRegions,
+ aTransforms);
+ root = layers[0];
+ }
+
+ void CreateMockHitTester() {
+ mHitTester = MakeUnique<MockHitTester>();
+ // Save a pointer in a separate variable, because SetUp() will
+ // move the value out of mHitTester.
+ mMockHitTester = static_cast<MockHitTester*>(mHitTester.get());
+ }
+ void QueueMockHitResult(ScrollableLayerGuid::ViewID aScrollId,
+ gfx::CompositorHitTestInfo aHitInfo =
+ gfx::CompositorHitTestFlags::eVisibleToHitTest) {
+ MOZ_ASSERT(mMockHitTester);
+ mMockHitTester->QueueHitResult(aScrollId, aHitInfo);
+ }
+
+ RefPtr<TestAPZCTreeManager> manager;
+ RefPtr<APZSampler> sampler;
+ RefPtr<APZUpdater> updater;
+ TestWRScrollData layers;
+ WebRenderLayerScrollData* root = nullptr;
+
+ UniquePtr<IAPZHitTester> mHitTester;
+ MockHitTester* mMockHitTester = nullptr;
+
+ protected:
+ static ScrollMetadata BuildScrollMetadata(
+ ScrollableLayerGuid::ViewID aScrollId, const CSSRect& aScrollableRect,
+ const ParentLayerRect& aCompositionBounds) {
+ ScrollMetadata metadata;
+ FrameMetrics& metrics = metadata.GetMetrics();
+ metrics.SetScrollId(aScrollId);
+ // By convention in this test file, START_SCROLL_ID is the root, so mark it
+ // as such.
+ if (aScrollId == ScrollableLayerGuid::START_SCROLL_ID) {
+ metadata.SetIsLayersIdRoot(true);
+ }
+ metrics.SetCompositionBounds(aCompositionBounds);
+ metrics.SetScrollableRect(aScrollableRect);
+ metrics.SetLayoutScrollOffset(CSSPoint(0, 0));
+ metadata.SetPageScrollAmount(LayoutDeviceIntSize(50, 100));
+ metadata.SetLineScrollAmount(LayoutDeviceIntSize(5, 10));
+ return metadata;
+ }
+
+ void SetScrollMetadata(WebRenderLayerScrollData* aLayer,
+ const ScrollMetadata& aMetadata) {
+ MOZ_ASSERT(aLayer->GetScrollMetadataCount() <= 1,
+ "This function does not support multiple ScrollMetadata on a "
+ "single layer");
+ if (aLayer->GetScrollMetadataCount() == 0) {
+ // Add new metrics
+ aLayer->AppendScrollMetadata(layers, aMetadata);
+ } else {
+ // Overwrite existing metrics
+ ModifyFrameMetrics(
+ aLayer, [&](ScrollMetadata& aSm, FrameMetrics&) { aSm = aMetadata; });
+ }
+ }
+
+ void SetScrollMetadata(WebRenderLayerScrollData* aLayer,
+ const nsTArray<ScrollMetadata>& aMetadata) {
+ // The reason for this restriction is that WebRenderLayerScrollData does not
+ // have an API to *remove* previous metadata.
+ MOZ_ASSERT(aLayer->GetScrollMetadataCount() == 0,
+ "This function can only be used on layers which do not yet have "
+ "scroll metadata");
+ for (const ScrollMetadata& metadata : aMetadata) {
+ aLayer->AppendScrollMetadata(layers, metadata);
+ }
+ }
+
+ void SetScrollableFrameMetrics(WebRenderLayerScrollData* aLayer,
+ ScrollableLayerGuid::ViewID aScrollId,
+ CSSRect aScrollableRect = CSSRect(-1, -1, -1,
+ -1)) {
+ auto localTransform = aLayer->GetTransformTyped() * AsyncTransformMatrix();
+ ParentLayerIntRect compositionBounds =
+ RoundedToInt(localTransform.TransformBounds(
+ LayerRect(aLayer->GetVisibleRegion().GetBounds())));
+ ScrollMetadata metadata = BuildScrollMetadata(
+ aScrollId, aScrollableRect, ParentLayerRect(compositionBounds));
+ SetScrollMetadata(aLayer, metadata);
+ }
+
+ bool HasScrollableFrameMetrics(const WebRenderLayerScrollData* aLayer) const {
+ for (uint32_t i = 0; i < aLayer->GetScrollMetadataCount(); i++) {
+ if (aLayer->GetScrollMetadata(layers, i).GetMetrics().IsScrollable()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ void SetScrollHandoff(WebRenderLayerScrollData* aChild,
+ WebRenderLayerScrollData* aParent) {
+ ModifyFrameMetrics(aChild, [&](ScrollMetadata& aSm, FrameMetrics&) {
+ aSm.SetScrollParentId(
+ aParent->GetScrollMetadata(layers, 0).GetMetrics().GetScrollId());
+ });
+ }
+
+ TestAsyncPanZoomController* ApzcOf(WebRenderLayerScrollData* aLayer) {
+ EXPECT_EQ(1u, aLayer->GetScrollMetadataCount());
+ return ApzcOf(aLayer, 0);
+ }
+
+ TestAsyncPanZoomController* ApzcOf(WebRenderLayerScrollData* aLayer,
+ uint32_t aIndex) {
+ EXPECT_LT(aIndex, aLayer->GetScrollMetadataCount());
+ // Unlike Layer, WebRenderLayerScrollData does not store the associated
+ // APZCs, so look it up using the tree manager instead.
+ RefPtr<AsyncPanZoomController> apzc = manager->GetTargetAPZC(
+ LayersId{0},
+ aLayer->GetScrollMetadata(layers, aIndex).GetMetrics().GetScrollId());
+ return (TestAsyncPanZoomController*)apzc.get();
+ }
+
+ void CreateSimpleScrollingLayer() {
+ const char* treeShape = "x";
+ LayerIntRegion layerVisibleRegion[] = {
+ LayerIntRect(0, 0, 200, 200),
+ };
+ CreateScrollData(treeShape, layerVisibleRegion);
+ SetScrollableFrameMetrics(layers[0], ScrollableLayerGuid::START_SCROLL_ID,
+ CSSRect(0, 0, 500, 500));
+ }
+};
+
+#endif // mozilla_layers_APZCTreeManagerTester_h
diff --git a/gfx/layers/apz/test/gtest/APZTestAccess.cpp b/gfx/layers/apz/test/gtest/APZTestAccess.cpp
new file mode 100644
index 0000000000..d55d7711f8
--- /dev/null
+++ b/gfx/layers/apz/test/gtest/APZTestAccess.cpp
@@ -0,0 +1,27 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "APZTestAccess.h"
+#include "mozilla/layers/WebRenderScrollData.h"
+
+namespace mozilla {
+namespace layers {
+
+/*static*/
+void APZTestAccess::InitializeForTest(WebRenderLayerScrollData& aLayer,
+ int32_t aDescendantCount) {
+ aLayer.InitializeForTest(aDescendantCount);
+}
+
+/*static*/
+ScrollMetadata& APZTestAccess::GetScrollMetadataMut(
+ WebRenderLayerScrollData& aLayer, WebRenderScrollData& aOwner,
+ size_t aIndex) {
+ return aLayer.GetScrollMetadataMut(aOwner, aIndex);
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/test/gtest/APZTestAccess.h b/gfx/layers/apz/test/gtest/APZTestAccess.h
new file mode 100644
index 0000000000..a56fb10a1a
--- /dev/null
+++ b/gfx/layers/apz/test/gtest/APZTestAccess.h
@@ -0,0 +1,36 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_APZTestAccess_h
+#define mozilla_layers_APZTestAccess_h
+
+#include <cstddef> // for size_t
+#include <cstdint> // for int32_t
+
+namespace mozilla {
+namespace layers {
+
+struct ScrollMetadata;
+class WebRenderLayerScrollData;
+class WebRenderScrollData;
+
+// The only purpose of this class is to serve as a single type that can be
+// the target of a "friend class" declaration in APZ classes that want to
+// give APZ test code access to their private members.
+// APZ test code can then access those members via this class.
+class APZTestAccess {
+ public:
+ static void InitializeForTest(WebRenderLayerScrollData& aLayer,
+ int32_t aDescendantCount);
+ static ScrollMetadata& GetScrollMetadataMut(WebRenderLayerScrollData& aLayer,
+ WebRenderScrollData& aOwner,
+ size_t aIndex);
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif
diff --git a/gfx/layers/apz/test/gtest/APZTestCommon.cpp b/gfx/layers/apz/test/gtest/APZTestCommon.cpp
new file mode 100644
index 0000000000..5276531a26
--- /dev/null
+++ b/gfx/layers/apz/test/gtest/APZTestCommon.cpp
@@ -0,0 +1,15 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "APZTestCommon.h"
+
+AsyncPanZoomController* TestAPZCTreeManager::NewAPZCInstance(
+ LayersId aLayersId, GeckoContentController* aController) {
+ MockContentControllerDelayed* mcc =
+ static_cast<MockContentControllerDelayed*>(aController);
+ return new TestAsyncPanZoomController(
+ aLayersId, mcc, this, AsyncPanZoomController::USE_GESTURE_DETECTOR);
+}
diff --git a/gfx/layers/apz/test/gtest/APZTestCommon.h b/gfx/layers/apz/test/gtest/APZTestCommon.h
new file mode 100644
index 0000000000..e4552bdd11
--- /dev/null
+++ b/gfx/layers/apz/test/gtest/APZTestCommon.h
@@ -0,0 +1,1051 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_APZTestCommon_h
+#define mozilla_layers_APZTestCommon_h
+
+/**
+ * Defines a set of mock classes and utility functions/classes for
+ * writing APZ gtests.
+ */
+
+#include "gtest/gtest.h"
+#include "gmock/gmock.h"
+
+#include "mozilla/Attributes.h"
+#include "mozilla/layers/GeckoContentController.h"
+#include "mozilla/layers/CompositorBridgeParent.h"
+#include "mozilla/layers/APZThreadUtils.h"
+#include "mozilla/layers/MatrixMessage.h"
+#include "mozilla/StaticPrefs_layout.h"
+#include "mozilla/TypedEnumBits.h"
+#include "mozilla/UniquePtr.h"
+#include "apz/src/APZCTreeManager.h"
+#include "apz/src/AsyncPanZoomController.h"
+#include "apz/src/HitTestingTreeNode.h"
+#include "base/task.h"
+#include "gfxPlatform.h"
+#include "TestWRScrollData.h"
+#include "UnitTransforms.h"
+
+using namespace mozilla;
+using namespace mozilla::gfx;
+using namespace mozilla::layers;
+using ::testing::_;
+using ::testing::AtLeast;
+using ::testing::AtMost;
+using ::testing::InSequence;
+using ::testing::MockFunction;
+using ::testing::NiceMock;
+typedef mozilla::layers::GeckoContentController::TapType TapType;
+
+inline TimeStamp GetStartupTime() {
+ static TimeStamp sStartupTime = TimeStamp::Now();
+ return sStartupTime;
+}
+
+inline uint32_t MillisecondsSinceStartup(TimeStamp aTime) {
+ return (aTime - GetStartupTime()).ToMilliseconds();
+}
+
+// Some helper functions for constructing input event objects suitable to be
+// passed either to an APZC (which expects an transformed point), or to an APZTM
+// (which expects an untransformed point). We handle both cases by setting both
+// the transformed and untransformed fields to the same value.
+inline SingleTouchData CreateSingleTouchData(int32_t aIdentifier,
+ const ScreenIntPoint& aPoint) {
+ SingleTouchData touch(aIdentifier, aPoint, ScreenSize(0, 0), 0, 0);
+ touch.mLocalScreenPoint = ParentLayerPoint(aPoint.x, aPoint.y);
+ return touch;
+}
+
+// Convenience wrapper for CreateSingleTouchData() that takes loose coordinates.
+inline SingleTouchData CreateSingleTouchData(int32_t aIdentifier,
+ ScreenIntCoord aX,
+ ScreenIntCoord aY) {
+ return CreateSingleTouchData(aIdentifier, ScreenIntPoint(aX, aY));
+}
+
+inline PinchGestureInput CreatePinchGestureInput(
+ PinchGestureInput::PinchGestureType aType, const ScreenPoint& aFocus,
+ float aCurrentSpan, float aPreviousSpan, TimeStamp timestamp) {
+ ParentLayerPoint localFocus(aFocus.x, aFocus.y);
+ PinchGestureInput result(aType, PinchGestureInput::UNKNOWN, timestamp,
+ ExternalPoint(0, 0), aFocus, aCurrentSpan,
+ aPreviousSpan, 0);
+ return result;
+}
+
+template <class SetArg, class Storage>
+class ScopedGfxSetting {
+ public:
+ ScopedGfxSetting(const std::function<SetArg(void)>& aGetPrefFunc,
+ const std::function<void(SetArg)>& aSetPrefFunc, SetArg aVal)
+ : mSetPrefFunc(aSetPrefFunc) {
+ mOldVal = aGetPrefFunc();
+ aSetPrefFunc(aVal);
+ }
+
+ ~ScopedGfxSetting() { mSetPrefFunc(mOldVal); }
+
+ private:
+ std::function<void(SetArg)> mSetPrefFunc;
+ Storage mOldVal;
+};
+
+static inline constexpr auto kDefaultTouchBehavior =
+ AllowedTouchBehavior::VERTICAL_PAN | AllowedTouchBehavior::HORIZONTAL_PAN |
+ AllowedTouchBehavior::PINCH_ZOOM | AllowedTouchBehavior::ANIMATING_ZOOM;
+
+#define FRESH_PREF_VAR_PASTE(id, line) id##line
+#define FRESH_PREF_VAR_EXPAND(id, line) FRESH_PREF_VAR_PASTE(id, line)
+#define FRESH_PREF_VAR FRESH_PREF_VAR_EXPAND(pref, __LINE__)
+
+#define SCOPED_GFX_PREF_BOOL(prefName, prefValue) \
+ ScopedGfxSetting<bool, bool> FRESH_PREF_VAR( \
+ [=]() { return Preferences::GetBool(prefName); }, \
+ [=](bool aPrefValue) { Preferences::SetBool(prefName, aPrefValue); }, \
+ prefValue)
+
+#define SCOPED_GFX_PREF_INT(prefName, prefValue) \
+ ScopedGfxSetting<int32_t, int32_t> FRESH_PREF_VAR( \
+ [=]() { return Preferences::GetInt(prefName); }, \
+ [=](int32_t aPrefValue) { Preferences::SetInt(prefName, aPrefValue); }, \
+ prefValue)
+
+#define SCOPED_GFX_PREF_FLOAT(prefName, prefValue) \
+ ScopedGfxSetting<float, float> FRESH_PREF_VAR( \
+ [=]() { return Preferences::GetFloat(prefName); }, \
+ [=](float aPrefValue) { Preferences::SetFloat(prefName, aPrefValue); }, \
+ prefValue)
+
+class MockContentController : public GeckoContentController {
+ public:
+ MOCK_METHOD1(NotifyLayerTransforms, void(nsTArray<MatrixMessage>&&));
+ MOCK_METHOD1(RequestContentRepaint, void(const RepaintRequest&));
+ MOCK_METHOD5(HandleTap, void(TapType, const LayoutDevicePoint&, Modifiers,
+ const ScrollableLayerGuid&, uint64_t));
+ MOCK_METHOD5(NotifyPinchGesture,
+ void(PinchGestureInput::PinchGestureType,
+ const ScrollableLayerGuid&, const LayoutDevicePoint&,
+ LayoutDeviceCoord, Modifiers));
+ // Can't use the macros with already_AddRefed :(
+ void PostDelayedTask(already_AddRefed<Runnable> aTask, int aDelayMs) {
+ RefPtr<Runnable> task = aTask;
+ }
+ bool IsRepaintThread() { return NS_IsMainThread(); }
+ void DispatchToRepaintThread(already_AddRefed<Runnable> aTask) {
+ NS_DispatchToMainThread(std::move(aTask));
+ }
+ MOCK_METHOD4(NotifyAPZStateChange,
+ void(const ScrollableLayerGuid& aGuid, APZStateChange aChange,
+ int aArg, Maybe<uint64_t> aInputBlockId));
+ MOCK_METHOD0(NotifyFlushComplete, void());
+ MOCK_METHOD3(NotifyAsyncScrollbarDragInitiated,
+ void(uint64_t, const ScrollableLayerGuid::ViewID&,
+ ScrollDirection aDirection));
+ MOCK_METHOD1(NotifyAsyncScrollbarDragRejected,
+ void(const ScrollableLayerGuid::ViewID&));
+ MOCK_METHOD1(NotifyAsyncAutoscrollRejected,
+ void(const ScrollableLayerGuid::ViewID&));
+ MOCK_METHOD1(CancelAutoscroll, void(const ScrollableLayerGuid&));
+ MOCK_METHOD2(NotifyScaleGestureComplete,
+ void(const ScrollableLayerGuid&, float aScale));
+ MOCK_METHOD4(UpdateOverscrollVelocity,
+ void(const ScrollableLayerGuid&, float, float, bool));
+ MOCK_METHOD4(UpdateOverscrollOffset,
+ void(const ScrollableLayerGuid&, float, float, bool));
+};
+
+class MockContentControllerDelayed : public MockContentController {
+ public:
+ MockContentControllerDelayed()
+ : mTime(SampleTime::FromTest(GetStartupTime())) {}
+
+ const TimeStamp& Time() { return mTime.Time(); }
+ const SampleTime& GetSampleTime() { return mTime; }
+
+ void AdvanceByMillis(int aMillis) {
+ AdvanceBy(TimeDuration::FromMilliseconds(aMillis));
+ }
+
+ void AdvanceBy(const TimeDuration& aIncrement) {
+ SampleTime target = mTime + aIncrement;
+ while (mTaskQueue.Length() > 0 && mTaskQueue[0].second <= target) {
+ RunNextDelayedTask();
+ }
+ mTime = target;
+ }
+
+ void PostDelayedTask(already_AddRefed<Runnable> aTask, int aDelayMs) {
+ RefPtr<Runnable> task = aTask;
+ SampleTime runAtTime = mTime + TimeDuration::FromMilliseconds(aDelayMs);
+ int insIndex = mTaskQueue.Length();
+ while (insIndex > 0) {
+ if (mTaskQueue[insIndex - 1].second <= runAtTime) {
+ break;
+ }
+ insIndex--;
+ }
+ mTaskQueue.InsertElementAt(insIndex, std::make_pair(task, runAtTime));
+ }
+
+ // Run all the tasks in the queue, returning the number of tasks
+ // run. Note that if a task queues another task while running, that
+ // new task will not be run. Therefore, there may be still be tasks
+ // in the queue after this function is called. Only when the return
+ // value is 0 is the queue guaranteed to be empty.
+ int RunThroughDelayedTasks() {
+ nsTArray<std::pair<RefPtr<Runnable>, SampleTime>> runQueue =
+ std::move(mTaskQueue);
+ int numTasks = runQueue.Length();
+ for (int i = 0; i < numTasks; i++) {
+ mTime = runQueue[i].second;
+ runQueue[i].first->Run();
+
+ // Deleting the task is important in order to release the reference to
+ // the callee object.
+ runQueue[i].first = nullptr;
+ }
+ return numTasks;
+ }
+
+ private:
+ void RunNextDelayedTask() {
+ std::pair<RefPtr<Runnable>, SampleTime> next = mTaskQueue[0];
+ mTaskQueue.RemoveElementAt(0);
+ mTime = next.second;
+ next.first->Run();
+ // Deleting the task is important in order to release the reference to
+ // the callee object.
+ next.first = nullptr;
+ }
+
+ // The following array is sorted by timestamp (tasks are inserted in order by
+ // timestamp).
+ nsTArray<std::pair<RefPtr<Runnable>, SampleTime>> mTaskQueue;
+ SampleTime mTime;
+};
+
+class TestAPZCTreeManager : public APZCTreeManager {
+ public:
+ explicit TestAPZCTreeManager(MockContentControllerDelayed* aMcc,
+ UniquePtr<IAPZHitTester> aHitTester = nullptr)
+ : APZCTreeManager(LayersId{0}, std::move(aHitTester)), mcc(aMcc) {}
+
+ RefPtr<InputQueue> GetInputQueue() const { return mInputQueue; }
+
+ void ClearContentController() { mcc = nullptr; }
+
+ /**
+ * This function is not currently implemented.
+ * See bug 1468804 for more information.
+ **/
+ void CancelAnimation() { EXPECT_TRUE(false); }
+
+ APZEventResult ReceiveInputEvent(
+ InputData& aEvent,
+ InputBlockCallback&& aCallback = InputBlockCallback()) override {
+ APZEventResult result =
+ APZCTreeManager::ReceiveInputEvent(aEvent, std::move(aCallback));
+ if (aEvent.mInputType == PANGESTURE_INPUT &&
+ // In the APZCTreeManager::ReceiveInputEvent some type of pan gesture
+ // events are marked as `mHandledByAPZ = false` (e.g. with Ctrl key
+ // modifier which causes reflow zoom), in such cases the events will
+ // never be processed by InputQueue so we shouldn't try to invoke
+ // AllowsSwipe() here.
+ aEvent.AsPanGestureInput().mHandledByAPZ &&
+ aEvent.AsPanGestureInput().AllowsSwipe()) {
+ SetBrowserGestureResponse(result.mInputBlockId,
+ BrowserGestureResponse::NotConsumed);
+ }
+ return result;
+ }
+
+ protected:
+ AsyncPanZoomController* NewAPZCInstance(
+ LayersId aLayersId, GeckoContentController* aController) override;
+
+ SampleTime GetFrameTime() override { return mcc->GetSampleTime(); }
+
+ private:
+ RefPtr<MockContentControllerDelayed> mcc;
+};
+
+class TestAsyncPanZoomController : public AsyncPanZoomController {
+ public:
+ TestAsyncPanZoomController(LayersId aLayersId,
+ MockContentControllerDelayed* aMcc,
+ TestAPZCTreeManager* aTreeManager,
+ GestureBehavior aBehavior = DEFAULT_GESTURES)
+ : AsyncPanZoomController(aLayersId, aTreeManager,
+ aTreeManager->GetInputQueue(), aMcc, aBehavior),
+ mWaitForMainThread(false),
+ mcc(aMcc) {}
+
+ APZEventResult ReceiveInputEvent(
+ InputData& aEvent,
+ const Maybe<nsTArray<uint32_t>>& aTouchBehaviors = Nothing()) {
+ // This is a function whose signature matches exactly the ReceiveInputEvent
+ // on APZCTreeManager. This allows us to templates for functions like
+ // TouchDown, TouchUp, etc so that we can reuse the code for dispatching
+ // events into both APZC and APZCTM.
+ APZEventResult result = GetInputQueue()->ReceiveInputEvent(
+ this, TargetConfirmationFlags{!mWaitForMainThread}, aEvent,
+ aTouchBehaviors);
+
+ if (aEvent.mInputType == PANGESTURE_INPUT &&
+ aEvent.AsPanGestureInput().AllowsSwipe()) {
+ GetInputQueue()->SetBrowserGestureResponse(
+ result.mInputBlockId, BrowserGestureResponse::NotConsumed);
+ }
+ return result;
+ }
+
+ void ContentReceivedInputBlock(uint64_t aInputBlockId, bool aPreventDefault) {
+ GetInputQueue()->ContentReceivedInputBlock(aInputBlockId, aPreventDefault);
+ }
+
+ void ConfirmTarget(uint64_t aInputBlockId) {
+ RefPtr<AsyncPanZoomController> target = this;
+ GetInputQueue()->SetConfirmedTargetApzc(aInputBlockId, target);
+ }
+
+ void SetAllowedTouchBehavior(uint64_t aInputBlockId,
+ const nsTArray<TouchBehaviorFlags>& aBehaviors) {
+ GetInputQueue()->SetAllowedTouchBehavior(aInputBlockId, aBehaviors);
+ }
+
+ void SetFrameMetrics(const FrameMetrics& metrics) {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ Metrics() = metrics;
+ }
+
+ void SetScrollMetadata(const ScrollMetadata& aMetadata) {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ mScrollMetadata = aMetadata;
+ }
+
+ FrameMetrics& GetFrameMetrics() {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ return mScrollMetadata.GetMetrics();
+ }
+
+ ScrollMetadata& GetScrollMetadata() {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ return mScrollMetadata;
+ }
+
+ const FrameMetrics& GetFrameMetrics() const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ return mScrollMetadata.GetMetrics();
+ }
+
+ using AsyncPanZoomController::GetOverscrollAmount;
+ using AsyncPanZoomController::GetVelocityVector;
+
+ void AssertStateIsReset() const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ EXPECT_EQ(NOTHING, mState);
+ }
+
+ void AssertStateIsFling() const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ EXPECT_EQ(FLING, mState);
+ }
+
+ void AssertStateIsSmoothScroll() const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ EXPECT_EQ(SMOOTH_SCROLL, mState);
+ }
+
+ void AssertStateIsSmoothMsdScroll() const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ EXPECT_EQ(SMOOTHMSD_SCROLL, mState);
+ }
+
+ void AssertStateIsPanningLockedY() {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ EXPECT_EQ(PANNING_LOCKED_Y, mState);
+ }
+
+ void AssertStateIsPanningLockedX() {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ EXPECT_EQ(PANNING_LOCKED_X, mState);
+ }
+
+ void AssertStateIsPanning() {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ EXPECT_EQ(PANNING, mState);
+ }
+
+ void AssertStateIsPanMomentum() {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ EXPECT_EQ(PAN_MOMENTUM, mState);
+ }
+
+ void SetAxisLocked(ScrollDirections aDirections, bool aLockValue) {
+ if (aDirections.contains(ScrollDirection::eVertical)) {
+ mY.SetAxisLocked(aLockValue);
+ }
+ if (aDirections.contains(ScrollDirection::eHorizontal)) {
+ mX.SetAxisLocked(aLockValue);
+ }
+ }
+
+ void AssertNotAxisLocked() const {
+ EXPECT_FALSE(mY.IsAxisLocked());
+ EXPECT_FALSE(mX.IsAxisLocked());
+ }
+
+ void AssertAxisLocked(ScrollDirection aDirection) const {
+ switch (aDirection) {
+ case ScrollDirection::eHorizontal:
+ EXPECT_TRUE(mY.IsAxisLocked());
+ EXPECT_FALSE(mX.IsAxisLocked());
+ break;
+ case ScrollDirection::eVertical:
+ EXPECT_TRUE(mX.IsAxisLocked());
+ EXPECT_FALSE(mY.IsAxisLocked());
+ break;
+ default:
+ FAIL() << "input direction must be either vertical or horizontal";
+ }
+ }
+
+ void AdvanceAnimationsUntilEnd(
+ const TimeDuration& aIncrement = TimeDuration::FromMilliseconds(10)) {
+ while (AdvanceAnimations(mcc->GetSampleTime())) {
+ mcc->AdvanceBy(aIncrement);
+ }
+ }
+
+ bool SampleContentTransformForFrame(
+ AsyncTransform* aOutTransform, ParentLayerPoint& aScrollOffset,
+ const TimeDuration& aIncrement = TimeDuration::FromMilliseconds(0)) {
+ mcc->AdvanceBy(aIncrement);
+ bool ret = AdvanceAnimations(mcc->GetSampleTime());
+ if (aOutTransform) {
+ *aOutTransform =
+ GetCurrentAsyncTransform(AsyncPanZoomController::eForHitTesting);
+ }
+ aScrollOffset =
+ GetCurrentAsyncScrollOffset(AsyncPanZoomController::eForHitTesting);
+ return ret;
+ }
+
+ CSSPoint GetCompositedScrollOffset() const {
+ return GetCurrentAsyncScrollOffset(
+ AsyncPanZoomController::eForCompositing) /
+ GetFrameMetrics().GetZoom();
+ }
+
+ void SetWaitForMainThread() { mWaitForMainThread = true; }
+
+ bool IsOverscrollAnimationRunning() const {
+ return mState == PanZoomState::OVERSCROLL_ANIMATION;
+ }
+
+ private:
+ bool mWaitForMainThread;
+ MockContentControllerDelayed* mcc;
+};
+
+class APZCTesterBase : public ::testing::Test {
+ public:
+ APZCTesterBase() { mcc = new NiceMock<MockContentControllerDelayed>(); }
+
+ void SetUp() override {
+ gfxPlatform::GetPlatform();
+ // This pref is changed in Pan() without using SCOPED_GFX_PREF
+ // because the modified value needs to be in place until the touch
+ // events are processed, which may not happen until the input queue
+ // is flushed in TearDown(). So, we save and restore its value here.
+ mTouchStartTolerance = StaticPrefs::apz_touch_start_tolerance();
+ }
+
+ void TearDown() override {
+ Preferences::SetFloat("apz.touch_start_tolerance", mTouchStartTolerance);
+ }
+
+ enum class PanOptions {
+ None = 0,
+ KeepFingerDown = 0x1,
+ /*
+ * Do not adjust the touch-start coordinates to overcome the touch-start
+ * tolerance threshold. If this option is passed, it's up to the caller
+ * to pass in coordinates that are sufficient to overcome the touch-start
+ * tolerance *and* cause the desired amount of scrolling.
+ */
+ ExactCoordinates = 0x2,
+ NoFling = 0x4
+ };
+
+ enum class PinchOptions {
+ None = 0,
+ LiftFinger1 = 0x1,
+ LiftFinger2 = 0x2,
+ /*
+ * The bitwise OR result of (LiftFinger1 | LiftFinger2).
+ * Defined explicitly here because it is used as the default
+ * argument for PinchWithTouchInput which is defined BEFORE the
+ * definition of operator| for this class.
+ */
+ LiftBothFingers = 0x3
+ };
+
+ template <class InputReceiver>
+ APZEventResult Tap(const RefPtr<InputReceiver>& aTarget,
+ const ScreenIntPoint& aPoint, TimeDuration aTapLength,
+ nsEventStatus (*aOutEventStatuses)[2] = nullptr,
+ uint64_t* aOutInputBlockId = nullptr);
+
+ template <class InputReceiver>
+ void TapAndCheckStatus(const RefPtr<InputReceiver>& aTarget,
+ const ScreenIntPoint& aPoint, TimeDuration aTapLength);
+
+ template <class InputReceiver>
+ void Pan(const RefPtr<InputReceiver>& aTarget,
+ const ScreenIntPoint& aTouchStart, const ScreenIntPoint& aTouchEnd,
+ PanOptions aOptions = PanOptions::None,
+ nsTArray<uint32_t>* aAllowedTouchBehaviors = nullptr,
+ nsEventStatus (*aOutEventStatuses)[4] = nullptr,
+ uint64_t* aOutInputBlockId = nullptr);
+
+ /*
+ * A version of Pan() that only takes y coordinates rather than (x, y) points
+ * for the touch start and end points, and uses 10 for the x coordinates.
+ * This is for convenience, as most tests only need to pan in one direction.
+ */
+ template <class InputReceiver>
+ void Pan(const RefPtr<InputReceiver>& aTarget, int aTouchStartY,
+ int aTouchEndY, PanOptions aOptions = PanOptions::None,
+ nsTArray<uint32_t>* aAllowedTouchBehaviors = nullptr,
+ nsEventStatus (*aOutEventStatuses)[4] = nullptr,
+ uint64_t* aOutInputBlockId = nullptr);
+
+ /*
+ * Dispatches mock touch events to the apzc and checks whether apzc properly
+ * consumed them and triggered scrolling behavior.
+ */
+ template <class InputReceiver>
+ void PanAndCheckStatus(const RefPtr<InputReceiver>& aTarget, int aTouchStartY,
+ int aTouchEndY, bool aExpectConsumed,
+ nsTArray<uint32_t>* aAllowedTouchBehaviors,
+ uint64_t* aOutInputBlockId = nullptr);
+
+ template <class InputReceiver>
+ void DoubleTap(const RefPtr<InputReceiver>& aTarget,
+ const ScreenIntPoint& aPoint,
+ nsEventStatus (*aOutEventStatuses)[4] = nullptr,
+ uint64_t (*aOutInputBlockIds)[2] = nullptr);
+
+ template <class InputReceiver>
+ void DoubleTapAndCheckStatus(const RefPtr<InputReceiver>& aTarget,
+ const ScreenIntPoint& aPoint,
+ uint64_t (*aOutInputBlockIds)[2] = nullptr);
+
+ template <class InputReceiver>
+ void PinchWithTouchInput(
+ const RefPtr<InputReceiver>& aTarget, const ScreenIntPoint& aFocus,
+ const ScreenIntPoint& aSecondFocus, float aScale, int& inputId,
+ nsTArray<uint32_t>* aAllowedTouchBehaviors = nullptr,
+ nsEventStatus (*aOutEventStatuses)[4] = nullptr,
+ uint64_t* aOutInputBlockId = nullptr,
+ PinchOptions aOptions = PinchOptions::LiftBothFingers,
+ bool aVertical = false);
+
+ // Pinch with one focus point. Zooms in place with no panning
+ template <class InputReceiver>
+ void PinchWithTouchInput(
+ const RefPtr<InputReceiver>& aTarget, const ScreenIntPoint& aFocus,
+ float aScale, int& inputId,
+ nsTArray<uint32_t>* aAllowedTouchBehaviors = nullptr,
+ nsEventStatus (*aOutEventStatuses)[4] = nullptr,
+ uint64_t* aOutInputBlockId = nullptr,
+ PinchOptions aOptions = PinchOptions::LiftBothFingers,
+ bool aVertical = false);
+
+ template <class InputReceiver>
+ void PinchWithTouchInputAndCheckStatus(
+ const RefPtr<InputReceiver>& aTarget, const ScreenIntPoint& aFocus,
+ float aScale, int& inputId, bool aShouldTriggerPinch,
+ nsTArray<uint32_t>* aAllowedTouchBehaviors);
+
+ template <class InputReceiver>
+ void PinchWithPinchInput(const RefPtr<InputReceiver>& aTarget,
+ const ScreenIntPoint& aFocus,
+ const ScreenIntPoint& aSecondFocus, float aScale,
+ nsEventStatus (*aOutEventStatuses)[3] = nullptr);
+
+ template <class InputReceiver>
+ void PinchWithPinchInputAndCheckStatus(const RefPtr<InputReceiver>& aTarget,
+ const ScreenIntPoint& aFocus,
+ float aScale,
+ bool aShouldTriggerPinch);
+
+ protected:
+ RefPtr<MockContentControllerDelayed> mcc;
+
+ private:
+ float mTouchStartTolerance;
+};
+
+MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(APZCTesterBase::PanOptions)
+MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(APZCTesterBase::PinchOptions)
+
+template <class InputReceiver>
+APZEventResult APZCTesterBase::Tap(const RefPtr<InputReceiver>& aTarget,
+ const ScreenIntPoint& aPoint,
+ TimeDuration aTapLength,
+ nsEventStatus (*aOutEventStatuses)[2],
+ uint64_t* aOutInputBlockId) {
+ APZEventResult touchDownResult = TouchDown(aTarget, aPoint, mcc->Time());
+ if (aOutEventStatuses) {
+ (*aOutEventStatuses)[0] = touchDownResult.GetStatus();
+ }
+ if (aOutInputBlockId) {
+ *aOutInputBlockId = touchDownResult.mInputBlockId;
+ }
+ mcc->AdvanceBy(aTapLength);
+
+ // If touch-action is enabled then simulate the allowed touch behaviour
+ // notification that the main thread is supposed to deliver.
+ if (touchDownResult.GetStatus() != nsEventStatus_eConsumeNoDefault) {
+ SetDefaultAllowedTouchBehavior(aTarget, touchDownResult.mInputBlockId);
+ }
+
+ APZEventResult touchUpResult = TouchUp(aTarget, aPoint, mcc->Time());
+ if (aOutEventStatuses) {
+ (*aOutEventStatuses)[1] = touchUpResult.GetStatus();
+ }
+ return touchDownResult;
+}
+
+template <class InputReceiver>
+void APZCTesterBase::TapAndCheckStatus(const RefPtr<InputReceiver>& aTarget,
+ const ScreenIntPoint& aPoint,
+ TimeDuration aTapLength) {
+ nsEventStatus statuses[2];
+ Tap(aTarget, aPoint, aTapLength, &statuses);
+ EXPECT_EQ(nsEventStatus_eConsumeDoDefault, statuses[0]);
+ EXPECT_EQ(nsEventStatus_eConsumeDoDefault, statuses[1]);
+}
+
+template <class InputReceiver>
+void APZCTesterBase::Pan(const RefPtr<InputReceiver>& aTarget,
+ const ScreenIntPoint& aTouchStart,
+ const ScreenIntPoint& aTouchEnd, PanOptions aOptions,
+ nsTArray<uint32_t>* aAllowedTouchBehaviors,
+ nsEventStatus (*aOutEventStatuses)[4],
+ uint64_t* aOutInputBlockId) {
+ // Reduce the move tolerance to a tiny value.
+ // We can't use a scoped pref because this value might be read at some later
+ // time when the events are actually processed, rather than when we deliver
+ // them.
+ const float touchStartTolerance = 0.1f;
+ const float panThreshold = touchStartTolerance * aTarget->GetDPI();
+ Preferences::SetFloat("apz.touch_start_tolerance", touchStartTolerance);
+ Preferences::SetFloat("apz.touch_move_tolerance", 0.0f);
+ int overcomeTouchToleranceX = 0;
+ int overcomeTouchToleranceY = 0;
+ if (!(aOptions & PanOptions::ExactCoordinates)) {
+ // Have the direction of the adjustment to overcome the touch tolerance
+ // match the direction of the entire gesture, otherwise we run into
+ // trouble such as accidentally activating the axis lock.
+ if (aTouchStart.x != aTouchEnd.x && aTouchStart.y != aTouchEnd.y) {
+ // Tests that need to avoid rounding error here can arrange for
+ // panThreshold to be 10 (by setting the DPI to 100), which makes sure
+ // that these are the legs in a Pythagorean triple where panThreshold is
+ // the hypotenuse. Watch out for changes of APZCPinchTester::mDPI.
+ overcomeTouchToleranceX = panThreshold / 10 * 6;
+ overcomeTouchToleranceY = panThreshold / 10 * 8;
+ } else if (aTouchStart.x != aTouchEnd.x) {
+ overcomeTouchToleranceX = panThreshold;
+ } else if (aTouchStart.y != aTouchEnd.y) {
+ overcomeTouchToleranceY = panThreshold;
+ }
+ }
+
+ const TimeDuration TIME_BETWEEN_TOUCH_EVENT =
+ TimeDuration::FromMilliseconds(20);
+
+ // Even if the caller doesn't care about the block id, we need it to set the
+ // allowed touch behaviour below, so make sure aOutInputBlockId is non-null.
+ uint64_t blockId;
+ if (!aOutInputBlockId) {
+ aOutInputBlockId = &blockId;
+ }
+
+ // Make sure the move is large enough to not be handled as a tap
+ APZEventResult result =
+ TouchDown(aTarget,
+ ScreenIntPoint(aTouchStart.x + overcomeTouchToleranceX,
+ aTouchStart.y + overcomeTouchToleranceY),
+ mcc->Time());
+ if (aOutInputBlockId) {
+ *aOutInputBlockId = result.mInputBlockId;
+ }
+ if (aOutEventStatuses) {
+ (*aOutEventStatuses)[0] = result.GetStatus();
+ }
+
+ mcc->AdvanceBy(TIME_BETWEEN_TOUCH_EVENT);
+
+ // Allowed touch behaviours must be set after sending touch-start.
+ if (result.GetStatus() != nsEventStatus_eConsumeNoDefault) {
+ if (aAllowedTouchBehaviors) {
+ EXPECT_EQ(1UL, aAllowedTouchBehaviors->Length());
+ aTarget->SetAllowedTouchBehavior(*aOutInputBlockId,
+ *aAllowedTouchBehaviors);
+ } else {
+ SetDefaultAllowedTouchBehavior(aTarget, *aOutInputBlockId);
+ }
+ }
+
+ result = TouchMove(aTarget, aTouchStart, mcc->Time());
+ if (aOutEventStatuses) {
+ (*aOutEventStatuses)[1] = result.GetStatus();
+ }
+
+ mcc->AdvanceBy(TIME_BETWEEN_TOUCH_EVENT);
+
+ const int numSteps = 3;
+ auto stepVector = (aTouchEnd - aTouchStart) / numSteps;
+ for (int k = 1; k < numSteps; k++) {
+ auto stepPoint = aTouchStart + stepVector * k;
+ Unused << TouchMove(aTarget, stepPoint, mcc->Time());
+
+ mcc->AdvanceBy(TIME_BETWEEN_TOUCH_EVENT);
+ }
+
+ result = TouchMove(aTarget, aTouchEnd, mcc->Time());
+ if (aOutEventStatuses) {
+ (*aOutEventStatuses)[2] = result.GetStatus();
+ }
+
+ mcc->AdvanceBy(TIME_BETWEEN_TOUCH_EVENT);
+
+ if (!(aOptions & PanOptions::KeepFingerDown)) {
+ result = TouchUp(aTarget, aTouchEnd, mcc->Time());
+ } else {
+ result.SetStatusAsIgnore();
+ }
+ if (aOutEventStatuses) {
+ (*aOutEventStatuses)[3] = result.GetStatus();
+ }
+
+ if ((aOptions & PanOptions::NoFling)) {
+ aTarget->CancelAnimation();
+ }
+
+ // Don't increment the time here. Animations started on touch-up, such as
+ // flings, are affected by elapsed time, and we want to be able to sample
+ // them immediately after they start, without time having elapsed.
+}
+
+template <class InputReceiver>
+void APZCTesterBase::Pan(const RefPtr<InputReceiver>& aTarget, int aTouchStartY,
+ int aTouchEndY, PanOptions aOptions,
+ nsTArray<uint32_t>* aAllowedTouchBehaviors,
+ nsEventStatus (*aOutEventStatuses)[4],
+ uint64_t* aOutInputBlockId) {
+ Pan(aTarget, ScreenIntPoint(10, aTouchStartY), ScreenIntPoint(10, aTouchEndY),
+ aOptions, aAllowedTouchBehaviors, aOutEventStatuses, aOutInputBlockId);
+}
+
+template <class InputReceiver>
+void APZCTesterBase::PanAndCheckStatus(
+ const RefPtr<InputReceiver>& aTarget, int aTouchStartY, int aTouchEndY,
+ bool aExpectConsumed, nsTArray<uint32_t>* aAllowedTouchBehaviors,
+ uint64_t* aOutInputBlockId) {
+ nsEventStatus statuses[4]; // down, move, move, up
+ Pan(aTarget, aTouchStartY, aTouchEndY, PanOptions::None,
+ aAllowedTouchBehaviors, &statuses, aOutInputBlockId);
+
+ EXPECT_EQ(nsEventStatus_eConsumeDoDefault, statuses[0]);
+
+ nsEventStatus touchMoveStatus;
+ if (aExpectConsumed) {
+ touchMoveStatus = nsEventStatus_eConsumeDoDefault;
+ } else {
+ touchMoveStatus = nsEventStatus_eIgnore;
+ }
+ EXPECT_EQ(touchMoveStatus, statuses[1]);
+ EXPECT_EQ(touchMoveStatus, statuses[2]);
+}
+
+template <class InputReceiver>
+void APZCTesterBase::DoubleTap(const RefPtr<InputReceiver>& aTarget,
+ const ScreenIntPoint& aPoint,
+ nsEventStatus (*aOutEventStatuses)[4],
+ uint64_t (*aOutInputBlockIds)[2]) {
+ APZEventResult result = TouchDown(aTarget, aPoint, mcc->Time());
+ if (aOutEventStatuses) {
+ (*aOutEventStatuses)[0] = result.GetStatus();
+ }
+ if (aOutInputBlockIds) {
+ (*aOutInputBlockIds)[0] = result.mInputBlockId;
+ }
+ mcc->AdvanceByMillis(10);
+
+ // If touch-action is enabled then simulate the allowed touch behaviour
+ // notification that the main thread is supposed to deliver.
+ if (result.GetStatus() != nsEventStatus_eConsumeNoDefault) {
+ SetDefaultAllowedTouchBehavior(aTarget, result.mInputBlockId);
+ }
+
+ result = TouchUp(aTarget, aPoint, mcc->Time());
+ if (aOutEventStatuses) {
+ (*aOutEventStatuses)[1] = result.GetStatus();
+ }
+ mcc->AdvanceByMillis(10);
+ result = TouchDown(aTarget, aPoint, mcc->Time());
+ if (aOutEventStatuses) {
+ (*aOutEventStatuses)[2] = result.GetStatus();
+ }
+ if (aOutInputBlockIds) {
+ (*aOutInputBlockIds)[1] = result.mInputBlockId;
+ }
+ mcc->AdvanceByMillis(10);
+
+ if (result.GetStatus() != nsEventStatus_eConsumeNoDefault) {
+ SetDefaultAllowedTouchBehavior(aTarget, result.mInputBlockId);
+ }
+
+ result = TouchUp(aTarget, aPoint, mcc->Time());
+ if (aOutEventStatuses) {
+ (*aOutEventStatuses)[3] = result.GetStatus();
+ }
+}
+
+template <class InputReceiver>
+void APZCTesterBase::DoubleTapAndCheckStatus(
+ const RefPtr<InputReceiver>& aTarget, const ScreenIntPoint& aPoint,
+ uint64_t (*aOutInputBlockIds)[2]) {
+ nsEventStatus statuses[4];
+ DoubleTap(aTarget, aPoint, &statuses, aOutInputBlockIds);
+ EXPECT_EQ(nsEventStatus_eConsumeDoDefault, statuses[0]);
+ EXPECT_EQ(nsEventStatus_eConsumeDoDefault, statuses[1]);
+ EXPECT_EQ(nsEventStatus_eConsumeDoDefault, statuses[2]);
+ EXPECT_EQ(nsEventStatus_eConsumeDoDefault, statuses[3]);
+}
+
+template <class InputReceiver>
+void APZCTesterBase::PinchWithTouchInput(
+ const RefPtr<InputReceiver>& aTarget, const ScreenIntPoint& aFocus,
+ float aScale, int& inputId, nsTArray<uint32_t>* aAllowedTouchBehaviors,
+ nsEventStatus (*aOutEventStatuses)[4], uint64_t* aOutInputBlockId,
+ PinchOptions aOptions, bool aVertical) {
+ // Perform a pinch gesture with the same start & end focus point
+ PinchWithTouchInput(aTarget, aFocus, aFocus, aScale, inputId,
+ aAllowedTouchBehaviors, aOutEventStatuses,
+ aOutInputBlockId, aOptions, aVertical);
+}
+
+template <class InputReceiver>
+void APZCTesterBase::PinchWithTouchInput(
+ const RefPtr<InputReceiver>& aTarget, const ScreenIntPoint& aFocus,
+ const ScreenIntPoint& aSecondFocus, float aScale, int& inputId,
+ nsTArray<uint32_t>* aAllowedTouchBehaviors,
+ nsEventStatus (*aOutEventStatuses)[4], uint64_t* aOutInputBlockId,
+ PinchOptions aOptions, bool aVertical) {
+ // Having pinch coordinates in float type may cause problems with
+ // high-precision scale values since SingleTouchData accepts integer value.
+ // But for trivial tests it should be ok.
+ const float pinchLength = 100.0;
+ const float pinchLengthScaled = pinchLength * aScale;
+
+ const float pinchLengthX = aVertical ? 0 : pinchLength;
+ const float pinchLengthScaledX = aVertical ? 0 : pinchLengthScaled;
+ const float pinchLengthY = aVertical ? pinchLength : 0;
+ const float pinchLengthScaledY = aVertical ? pinchLengthScaled : 0;
+
+ // Even if the caller doesn't care about the block id, we need it to set the
+ // allowed touch behaviour below, so make sure aOutInputBlockId is non-null.
+ uint64_t blockId;
+ if (!aOutInputBlockId) {
+ aOutInputBlockId = &blockId;
+ }
+
+ const TimeDuration TIME_BETWEEN_TOUCH_EVENT =
+ TimeDuration::FromMilliseconds(20);
+
+ MultiTouchInput mtiStart =
+ MultiTouchInput(MultiTouchInput::MULTITOUCH_START, 0, mcc->Time(), 0);
+ mtiStart.mTouches.AppendElement(CreateSingleTouchData(inputId, aFocus));
+ mtiStart.mTouches.AppendElement(CreateSingleTouchData(inputId + 1, aFocus));
+ APZEventResult result;
+ result = aTarget->ReceiveInputEvent(mtiStart);
+ if (aOutInputBlockId) {
+ *aOutInputBlockId = result.mInputBlockId;
+ }
+ if (aOutEventStatuses) {
+ (*aOutEventStatuses)[0] = result.GetStatus();
+ }
+
+ mcc->AdvanceBy(TIME_BETWEEN_TOUCH_EVENT);
+
+ if (aAllowedTouchBehaviors) {
+ EXPECT_EQ(2UL, aAllowedTouchBehaviors->Length());
+ aTarget->SetAllowedTouchBehavior(*aOutInputBlockId,
+ *aAllowedTouchBehaviors);
+ } else {
+ SetDefaultAllowedTouchBehavior(aTarget, *aOutInputBlockId, 2);
+ }
+
+ ScreenIntPoint pinchStartPoint1(aFocus.x - int32_t(pinchLengthX),
+ aFocus.y - int32_t(pinchLengthY));
+ ScreenIntPoint pinchStartPoint2(aFocus.x + int32_t(pinchLengthX),
+ aFocus.y + int32_t(pinchLengthY));
+
+ MultiTouchInput mtiMove1 =
+ MultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, 0, mcc->Time(), 0);
+ mtiMove1.mTouches.AppendElement(
+ CreateSingleTouchData(inputId, pinchStartPoint1));
+ mtiMove1.mTouches.AppendElement(
+ CreateSingleTouchData(inputId + 1, pinchStartPoint2));
+ result = aTarget->ReceiveInputEvent(mtiMove1);
+ if (aOutEventStatuses) {
+ (*aOutEventStatuses)[1] = result.GetStatus();
+ }
+
+ mcc->AdvanceBy(TIME_BETWEEN_TOUCH_EVENT);
+
+ // Pinch instantly but move in steps.
+ const int numSteps = 3;
+ auto stepVector = (aSecondFocus - aFocus) / numSteps;
+ for (int k = 1; k < numSteps; k++) {
+ ScreenIntPoint stepFocus = aFocus + stepVector * k;
+ ScreenIntPoint stepPoint1(stepFocus.x - int32_t(pinchLengthScaledX),
+ stepFocus.y - int32_t(pinchLengthScaledY));
+ ScreenIntPoint stepPoint2(stepFocus.x + int32_t(pinchLengthScaledX),
+ stepFocus.y + int32_t(pinchLengthScaledY));
+ MultiTouchInput mtiMoveStep =
+ MultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, 0, mcc->Time(), 0);
+ mtiMoveStep.mTouches.AppendElement(
+ CreateSingleTouchData(inputId, stepPoint1));
+ mtiMoveStep.mTouches.AppendElement(
+ CreateSingleTouchData(inputId + 1, stepPoint2));
+ Unused << aTarget->ReceiveInputEvent(mtiMoveStep);
+
+ mcc->AdvanceBy(TIME_BETWEEN_TOUCH_EVENT);
+ }
+
+ ScreenIntPoint pinchEndPoint1(aSecondFocus.x - int32_t(pinchLengthScaledX),
+ aSecondFocus.y - int32_t(pinchLengthScaledY));
+ ScreenIntPoint pinchEndPoint2(aSecondFocus.x + int32_t(pinchLengthScaledX),
+ aSecondFocus.y + int32_t(pinchLengthScaledY));
+
+ MultiTouchInput mtiMove2 =
+ MultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, 0, mcc->Time(), 0);
+ mtiMove2.mTouches.AppendElement(
+ CreateSingleTouchData(inputId, pinchEndPoint1));
+ mtiMove2.mTouches.AppendElement(
+ CreateSingleTouchData(inputId + 1, pinchEndPoint2));
+ result = aTarget->ReceiveInputEvent(mtiMove2);
+ if (aOutEventStatuses) {
+ (*aOutEventStatuses)[2] = result.GetStatus();
+ }
+
+ if (aOptions & (PinchOptions::LiftFinger1 | PinchOptions::LiftFinger2)) {
+ mcc->AdvanceBy(TIME_BETWEEN_TOUCH_EVENT);
+
+ MultiTouchInput mtiEnd =
+ MultiTouchInput(MultiTouchInput::MULTITOUCH_END, 0, mcc->Time(), 0);
+ if (aOptions & PinchOptions::LiftFinger1) {
+ mtiEnd.mTouches.AppendElement(
+ CreateSingleTouchData(inputId, pinchEndPoint1));
+ }
+ if (aOptions & PinchOptions::LiftFinger2) {
+ mtiEnd.mTouches.AppendElement(
+ CreateSingleTouchData(inputId + 1, pinchEndPoint2));
+ }
+ result = aTarget->ReceiveInputEvent(mtiEnd);
+ if (aOutEventStatuses) {
+ (*aOutEventStatuses)[3] = result.GetStatus();
+ }
+ }
+
+ inputId += 2;
+}
+
+template <class InputReceiver>
+void APZCTesterBase::PinchWithTouchInputAndCheckStatus(
+ const RefPtr<InputReceiver>& aTarget, const ScreenIntPoint& aFocus,
+ float aScale, int& inputId, bool aShouldTriggerPinch,
+ nsTArray<uint32_t>* aAllowedTouchBehaviors) {
+ nsEventStatus statuses[4]; // down, move, move, up
+ PinchWithTouchInput(aTarget, aFocus, aScale, inputId, aAllowedTouchBehaviors,
+ &statuses);
+
+ nsEventStatus expectedMoveStatus = aShouldTriggerPinch
+ ? nsEventStatus_eConsumeDoDefault
+ : nsEventStatus_eIgnore;
+ EXPECT_EQ(nsEventStatus_eConsumeDoDefault, statuses[0]);
+ EXPECT_EQ(expectedMoveStatus, statuses[1]);
+ EXPECT_EQ(expectedMoveStatus, statuses[2]);
+}
+
+template <class InputReceiver>
+void APZCTesterBase::PinchWithPinchInput(
+ const RefPtr<InputReceiver>& aTarget, const ScreenIntPoint& aFocus,
+ const ScreenIntPoint& aSecondFocus, float aScale,
+ nsEventStatus (*aOutEventStatuses)[3]) {
+ const TimeDuration TIME_BETWEEN_PINCH_INPUT =
+ TimeDuration::FromMilliseconds(50);
+
+ auto event = CreatePinchGestureInput(PinchGestureInput::PINCHGESTURE_START,
+ aFocus, 10.0, 10.0, mcc->Time());
+ APZEventResult actual = aTarget->ReceiveInputEvent(event);
+ if (aOutEventStatuses) {
+ (*aOutEventStatuses)[0] = actual.GetStatus();
+ }
+ mcc->AdvanceBy(TIME_BETWEEN_PINCH_INPUT);
+
+ event =
+ CreatePinchGestureInput(PinchGestureInput::PINCHGESTURE_SCALE,
+ aSecondFocus, 10.0 * aScale, 10.0, mcc->Time());
+ actual = aTarget->ReceiveInputEvent(event);
+ if (aOutEventStatuses) {
+ (*aOutEventStatuses)[1] = actual.GetStatus();
+ }
+ mcc->AdvanceBy(TIME_BETWEEN_PINCH_INPUT);
+
+ event =
+ CreatePinchGestureInput(PinchGestureInput::PINCHGESTURE_END, aSecondFocus,
+ 10.0 * aScale, 10.0 * aScale, mcc->Time());
+ actual = aTarget->ReceiveInputEvent(event);
+ if (aOutEventStatuses) {
+ (*aOutEventStatuses)[2] = actual.GetStatus();
+ }
+}
+
+template <class InputReceiver>
+void APZCTesterBase::PinchWithPinchInputAndCheckStatus(
+ const RefPtr<InputReceiver>& aTarget, const ScreenIntPoint& aFocus,
+ float aScale, bool aShouldTriggerPinch) {
+ nsEventStatus statuses[3]; // scalebegin, scale, scaleend
+ PinchWithPinchInput(aTarget, aFocus, aFocus, aScale, &statuses);
+
+ nsEventStatus expectedStatus = aShouldTriggerPinch
+ ? nsEventStatus_eConsumeDoDefault
+ : nsEventStatus_eIgnore;
+ EXPECT_EQ(expectedStatus, statuses[0]);
+ EXPECT_EQ(expectedStatus, statuses[1]);
+}
+
+inline FrameMetrics TestFrameMetrics() {
+ FrameMetrics fm;
+
+ fm.SetDisplayPort(CSSRect(0, 0, 10, 10));
+ fm.SetCompositionBounds(ParentLayerRect(0, 0, 10, 10));
+ fm.SetScrollableRect(CSSRect(0, 0, 100, 100));
+
+ return fm;
+}
+
+#endif // mozilla_layers_APZTestCommon_h
diff --git a/gfx/layers/apz/test/gtest/InputUtils.h b/gfx/layers/apz/test/gtest/InputUtils.h
new file mode 100644
index 0000000000..74f4b640b3
--- /dev/null
+++ b/gfx/layers/apz/test/gtest/InputUtils.h
@@ -0,0 +1,149 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_InputUtils_h
+#define mozilla_layers_InputUtils_h
+
+/**
+ * Defines a set of utility functions for generating input events
+ * to an APZC/APZCTM during APZ gtests.
+ */
+
+#include "APZTestCommon.h"
+
+/* The InputReceiver template parameter used in the helper functions below needs
+ * to be a class that implements functions with the signatures:
+ * APZEventResult ReceiveInputEvent(const InputData& aEvent);
+ * void SetAllowedTouchBehavior(uint64_t aInputBlockId,
+ * const nsTArray<uint32_t>& aBehaviours);
+ * The classes that currently implement these are APZCTreeManager and
+ * TestAsyncPanZoomController. Using this template allows us to test individual
+ * APZC instances in isolation and also an entire APZ tree, while using the same
+ * code to dispatch input events.
+ */
+
+template <class InputReceiver>
+void SetDefaultAllowedTouchBehavior(const RefPtr<InputReceiver>& aTarget,
+ uint64_t aInputBlockId,
+ int touchPoints = 1) {
+ nsTArray<uint32_t> defaultBehaviors;
+ // use the default value where everything is allowed
+ for (int i = 0; i < touchPoints; i++) {
+ defaultBehaviors.AppendElement(
+ mozilla::layers::AllowedTouchBehavior::HORIZONTAL_PAN |
+ mozilla::layers::AllowedTouchBehavior::VERTICAL_PAN |
+ mozilla::layers::AllowedTouchBehavior::PINCH_ZOOM |
+ mozilla::layers::AllowedTouchBehavior::ANIMATING_ZOOM);
+ }
+ aTarget->SetAllowedTouchBehavior(aInputBlockId, defaultBehaviors);
+}
+
+inline MultiTouchInput CreateMultiTouchInput(
+ MultiTouchInput::MultiTouchType aType, TimeStamp aTime) {
+ return MultiTouchInput(aType, MillisecondsSinceStartup(aTime), aTime, 0);
+}
+
+template <class InputReceiver>
+APZEventResult TouchDown(const RefPtr<InputReceiver>& aTarget,
+ const ScreenIntPoint& aPoint, TimeStamp aTime) {
+ MultiTouchInput mti =
+ CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, aTime);
+ mti.mTouches.AppendElement(CreateSingleTouchData(0, aPoint));
+ return aTarget->ReceiveInputEvent(mti);
+}
+
+template <class InputReceiver>
+APZEventResult TouchMove(const RefPtr<InputReceiver>& aTarget,
+ const ScreenIntPoint& aPoint, TimeStamp aTime) {
+ MultiTouchInput mti =
+ CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, aTime);
+ mti.mTouches.AppendElement(CreateSingleTouchData(0, aPoint));
+ return aTarget->ReceiveInputEvent(mti);
+}
+
+template <class InputReceiver>
+APZEventResult TouchUp(const RefPtr<InputReceiver>& aTarget,
+ const ScreenIntPoint& aPoint, TimeStamp aTime) {
+ MultiTouchInput mti =
+ CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_END, aTime);
+ mti.mTouches.AppendElement(CreateSingleTouchData(0, aPoint));
+ return aTarget->ReceiveInputEvent(mti);
+}
+
+template <class InputReceiver>
+APZEventResult Wheel(const RefPtr<InputReceiver>& aTarget,
+ const ScreenIntPoint& aPoint, const ScreenPoint& aDelta,
+ TimeStamp aTime) {
+ ScrollWheelInput input(aTime, 0, ScrollWheelInput::SCROLLMODE_INSTANT,
+ ScrollWheelInput::SCROLLDELTA_PIXEL, aPoint, aDelta.x,
+ aDelta.y, false, WheelDeltaAdjustmentStrategy::eNone);
+ return aTarget->ReceiveInputEvent(input);
+}
+
+template <class InputReceiver>
+APZEventResult SmoothWheel(const RefPtr<InputReceiver>& aTarget,
+ const ScreenIntPoint& aPoint,
+ const ScreenPoint& aDelta, TimeStamp aTime) {
+ ScrollWheelInput input(aTime, 0, ScrollWheelInput::SCROLLMODE_SMOOTH,
+ ScrollWheelInput::SCROLLDELTA_LINE, aPoint, aDelta.x,
+ aDelta.y, false, WheelDeltaAdjustmentStrategy::eNone);
+ return aTarget->ReceiveInputEvent(input);
+}
+
+template <class InputReceiver>
+APZEventResult MouseDown(const RefPtr<InputReceiver>& aTarget,
+ const ScreenIntPoint& aPoint, TimeStamp aTime) {
+ MouseInput input(MouseInput::MOUSE_DOWN,
+ MouseInput::ButtonType::PRIMARY_BUTTON, 0, 0, aPoint, aTime,
+ 0);
+ return aTarget->ReceiveInputEvent(input);
+}
+
+template <class InputReceiver>
+APZEventResult MouseMove(const RefPtr<InputReceiver>& aTarget,
+ const ScreenIntPoint& aPoint, TimeStamp aTime) {
+ MouseInput input(MouseInput::MOUSE_MOVE,
+ MouseInput::ButtonType::PRIMARY_BUTTON, 0, 0, aPoint, aTime,
+ 0);
+ return aTarget->ReceiveInputEvent(input);
+}
+
+template <class InputReceiver>
+APZEventResult MouseUp(const RefPtr<InputReceiver>& aTarget,
+ const ScreenIntPoint& aPoint, TimeStamp aTime) {
+ MouseInput input(MouseInput::MOUSE_UP, MouseInput::ButtonType::PRIMARY_BUTTON,
+ 0, 0, aPoint, aTime, 0);
+ return aTarget->ReceiveInputEvent(input);
+}
+
+template <class InputReceiver>
+APZEventResult PanGesture(PanGestureInput::PanGestureType aType,
+ const RefPtr<InputReceiver>& aTarget,
+ const ScreenIntPoint& aPoint,
+ const ScreenPoint& aDelta, TimeStamp aTime,
+ Modifiers aModifiers = MODIFIER_NONE,
+ bool aSimulateMomentum = false) {
+ PanGestureInput input(aType, aTime, aPoint, aDelta, aModifiers);
+ input.mSimulateMomentum = aSimulateMomentum;
+ if constexpr (std::is_same_v<InputReceiver, TestAsyncPanZoomController>) {
+ // In the case of TestAsyncPanZoomController we know for sure that the
+ // event will be handled by APZ so set it explicitly.
+ input.mHandledByAPZ = true;
+ }
+ return aTarget->ReceiveInputEvent(input);
+}
+
+template <class InputReceiver>
+APZEventResult PanGestureWithModifiers(PanGestureInput::PanGestureType aType,
+ Modifiers aModifiers,
+ const RefPtr<InputReceiver>& aTarget,
+ const ScreenIntPoint& aPoint,
+ const ScreenPoint& aDelta,
+ TimeStamp aTime) {
+ return PanGesture(aType, aTarget, aPoint, aDelta, aTime, aModifiers);
+}
+
+#endif // mozilla_layers_InputUtils_h
diff --git a/gfx/layers/apz/test/gtest/MockHitTester.cpp b/gfx/layers/apz/test/gtest/MockHitTester.cpp
new file mode 100644
index 0000000000..025866ca65
--- /dev/null
+++ b/gfx/layers/apz/test/gtest/MockHitTester.cpp
@@ -0,0 +1,38 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MockHitTester.h"
+#include "apz/src/AsyncPanZoomController.h"
+#include "mozilla/layers/ScrollableLayerGuid.h"
+
+namespace mozilla::layers {
+
+IAPZHitTester::HitTestResult MockHitTester::GetAPZCAtPoint(
+ const ScreenPoint& aHitTestPoint,
+ const RecursiveMutexAutoLock& aProofOfTreeLock) {
+ MOZ_ASSERT(!mQueuedResults.empty());
+ HitTestResult result = std::move(mQueuedResults.front());
+ mQueuedResults.pop();
+ return result;
+}
+
+void MockHitTester::QueueHitResult(ScrollableLayerGuid::ViewID aScrollId,
+ gfx::CompositorHitTestInfo aHitInfo) {
+ LayersId layersId = GetRootLayersId(); // currently this is all the tests use
+ RefPtr<HitTestingTreeNode> node =
+ GetTargetNode(ScrollableLayerGuid(layersId, 0, aScrollId),
+ ScrollableLayerGuid::EqualsIgnoringPresShell);
+ MOZ_ASSERT(node);
+ AsyncPanZoomController* apzc = node->GetApzc();
+ MOZ_ASSERT(apzc);
+ HitTestResult result;
+ result.mTargetApzc = apzc;
+ result.mHitResult = aHitInfo;
+ result.mLayersId = layersId;
+ mQueuedResults.push(std::move(result));
+}
+
+} // namespace mozilla::layers
diff --git a/gfx/layers/apz/test/gtest/MockHitTester.h b/gfx/layers/apz/test/gtest/MockHitTester.h
new file mode 100644
index 0000000000..9c91b31152
--- /dev/null
+++ b/gfx/layers/apz/test/gtest/MockHitTester.h
@@ -0,0 +1,37 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_MockHitTester_h
+#define mozilla_layers_MockHitTester_h
+
+#include "apz/src/IAPZHitTester.h"
+#include "mozilla/gfx/CompositorHitTestInfo.h"
+
+#include <queue>
+
+namespace mozilla::layers {
+
+// IAPZHitTester implementation for APZ gtests.
+// This does not actually perform hit-testing, it just allows
+// the test code to specify the expected hit test results.
+class MockHitTester final : public IAPZHitTester {
+ public:
+ HitTestResult GetAPZCAtPoint(
+ const ScreenPoint& aHitTestPoint,
+ const RecursiveMutexAutoLock& aProofOfTreeLock) override;
+
+ // Queue a hit test result whose target APZC is the APZC
+ // with scroll id |aScrollId|, and the provided hit test flags.
+ void QueueHitResult(ScrollableLayerGuid::ViewID aScrollId,
+ gfx::CompositorHitTestInfo aHitInfo);
+
+ private:
+ std::queue<HitTestResult> mQueuedResults;
+};
+
+} // namespace mozilla::layers
+
+#endif // define mozilla_layers_MockHitTester_h
diff --git a/gfx/layers/apz/test/gtest/TestAxisLock.cpp b/gfx/layers/apz/test/gtest/TestAxisLock.cpp
new file mode 100644
index 0000000000..8b0df3e8a2
--- /dev/null
+++ b/gfx/layers/apz/test/gtest/TestAxisLock.cpp
@@ -0,0 +1,645 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "APZCTreeManagerTester.h"
+#include "APZTestCommon.h"
+
+#include "InputUtils.h"
+#include "gtest/gtest.h"
+
+#include <cmath>
+
+class APZCAxisLockCompatTester : public APZCTreeManagerTester,
+ public testing::WithParamInterface<int> {
+ public:
+ APZCAxisLockCompatTester() : oldAxisLockMode(0) { CreateMockHitTester(); }
+
+ int oldAxisLockMode;
+
+ UniquePtr<ScopedLayerTreeRegistration> registration;
+
+ RefPtr<TestAsyncPanZoomController> apzc;
+
+ void SetUp() {
+ APZCTreeManagerTester::SetUp();
+
+ oldAxisLockMode = Preferences::GetInt("apz.axis_lock.mode");
+
+ Preferences::SetInt("apz.axis_lock.mode", GetParam());
+ }
+
+ void TearDown() {
+ APZCTreeManagerTester::TearDown();
+
+ Preferences::SetInt("apz.axis_lock.mode", oldAxisLockMode);
+ }
+
+ static std::string PrintFromParam(const testing::TestParamInfo<int>& info) {
+ switch (info.param) {
+ case 0:
+ return "FREE";
+ case 1:
+ return "STANDARD";
+ case 2:
+ return "STICKY";
+ case 3:
+ return "DOMINANT_AXIS";
+ default:
+ return "UNKNOWN";
+ }
+ }
+};
+
+class APZCAxisLockTester : public APZCTreeManagerTester {
+ public:
+ APZCAxisLockTester() { CreateMockHitTester(); }
+
+ UniquePtr<ScopedLayerTreeRegistration> registration;
+
+ RefPtr<TestAsyncPanZoomController> apzc;
+
+ void SetupBasicTest() {
+ const char* treeShape = "x";
+ LayerIntRegion layerVisibleRegion[] = {
+ LayerIntRect(0, 0, 100, 100),
+ };
+ CreateScrollData(treeShape, layerVisibleRegion);
+ SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID,
+ CSSRect(0, 0, 500, 500));
+
+ registration = MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc);
+
+ UpdateHitTestingTree();
+ }
+
+ void BreakStickyAxisLockTestGesture(const ScrollDirections& aDirections) {
+ float panX = 0;
+ float panY = 0;
+
+ if (aDirections.contains(ScrollDirection::eVertical)) {
+ panY = 30;
+ }
+ if (aDirections.contains(ScrollDirection::eHorizontal)) {
+ panX = 30;
+ }
+
+ // Kick off the gesture that may lock onto an axis
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 50),
+ ScreenPoint(panX, panY), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 50),
+ ScreenPoint(panX, panY), mcc->Time());
+ }
+
+ void BreakStickyAxisLockTest(const ScrollDirections& aDirections) {
+ // Create the gesture for the test.
+ BreakStickyAxisLockTestGesture(aDirections);
+
+ // Based on the scroll direction(s) ensure the state is what we expect.
+ if (aDirections == ScrollDirection::eVertical) {
+ apzc->AssertStateIsPanningLockedY();
+ apzc->AssertAxisLocked(ScrollDirection::eVertical);
+ EXPECT_GT(apzc->GetVelocityVector().y, 0);
+ EXPECT_EQ(apzc->GetVelocityVector().x, 0);
+ } else if (aDirections == ScrollDirection::eHorizontal) {
+ apzc->AssertStateIsPanningLockedX();
+ apzc->AssertAxisLocked(ScrollDirection::eHorizontal);
+ EXPECT_GT(apzc->GetVelocityVector().x, 0);
+ EXPECT_EQ(apzc->GetVelocityVector().y, 0);
+ } else {
+ apzc->AssertStateIsPanning();
+ apzc->AssertNotAxisLocked();
+ EXPECT_GT(apzc->GetVelocityVector().x, 0);
+ EXPECT_GT(apzc->GetVelocityVector().y, 0);
+ }
+
+ // Cleanup for next test.
+ apzc->AdvanceAnimationsUntilEnd();
+ }
+};
+
+TEST_F(APZCAxisLockTester, BasicDominantAxisUse) {
+ SCOPED_GFX_PREF_INT("apz.axis_lock.mode", 1);
+ SCOPED_GFX_PREF_FLOAT("apz.axis_lock.lock_angle", M_PI / 4.0f);
+
+ SetupBasicTest();
+
+ apzc = ApzcOf(root);
+
+ // Kick off the initial gesture that triggers the momentum scroll.
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_START, manager, ScreenIntPoint(50, 50),
+ ScreenIntPoint(1, 2), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 50),
+ ScreenPoint(15, 30), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 50),
+ ScreenPoint(15, 30), mcc->Time());
+
+ // Should be in a PANNING_LOCKED_Y state with no horizontal velocity.
+ apzc->AssertStateIsPanningLockedY();
+ apzc->AssertAxisLocked(ScrollDirection::eVertical);
+ EXPECT_GT(apzc->GetVelocityVector().y, 0);
+ EXPECT_EQ(apzc->GetVelocityVector().x, 0);
+
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_END, manager, ScreenIntPoint(50, 50),
+ ScreenPoint(0, 0), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ // Ensure that we have not panned on the horizontal axis.
+ ParentLayerPoint panEndOffset = apzc->GetCurrentAsyncScrollOffset(
+ AsyncPanZoomController::AsyncTransformConsumer::eForHitTesting);
+ EXPECT_EQ(panEndOffset.x, 0);
+
+ // The lock onto the Y axis extends into momentum scroll.
+ apzc->AssertAxisLocked(ScrollDirection::eVertical);
+
+ // Start the momentum scroll.
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMSTART, manager,
+ ScreenIntPoint(50, 50), ScreenPoint(30, 90), mcc->Time());
+ mcc->AdvanceByMillis(10);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, manager,
+ ScreenIntPoint(50, 50), ScreenPoint(10, 30), mcc->Time());
+ mcc->AdvanceByMillis(10);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, manager,
+ ScreenIntPoint(50, 50), ScreenPoint(10, 30), mcc->Time());
+ mcc->AdvanceByMillis(10);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ // In momentum locking mode, we should still be locked onto the Y axis.
+ apzc->AssertStateIsPanMomentum();
+ apzc->AssertAxisLocked(ScrollDirection::eVertical);
+ EXPECT_GT(apzc->GetVelocityVector().y, 0);
+ EXPECT_EQ(apzc->GetVelocityVector().x, 0);
+
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMEND, manager,
+ ScreenIntPoint(50, 50), ScreenPoint(0, 0), mcc->Time());
+
+ // After momentum scroll end, ensure we are no longer locked onto an axis.
+ apzc->AssertNotAxisLocked();
+
+ // Wait until the end of the animation and ensure the final state is
+ // reasonable.
+ apzc->AdvanceAnimationsUntilEnd();
+ ParentLayerPoint finalOffset = apzc->GetCurrentAsyncScrollOffset(
+ AsyncPanZoomController::AsyncTransformConsumer::eForHitTesting);
+
+ // Ensure we have scrolled some amount on the Y axis in momentum scroll.
+ EXPECT_GT(finalOffset.y, panEndOffset.y);
+ EXPECT_EQ(finalOffset.x, 0.0f);
+}
+
+TEST_F(APZCAxisLockTester, NewGestureBreaksMomentumAxisLock) {
+ SCOPED_GFX_PREF_INT("apz.axis_lock.mode", 1);
+ SCOPED_GFX_PREF_FLOAT("apz.axis_lock.lock_angle", M_PI / 4.0f);
+
+ SetupBasicTest();
+
+ apzc = ApzcOf(root);
+
+ // Kick off the initial gesture that triggers the momentum scroll.
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_START, manager, ScreenIntPoint(50, 50),
+ ScreenIntPoint(2, 1), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 50),
+ ScreenPoint(30, 15), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 50),
+ ScreenPoint(30, 15), mcc->Time());
+
+ // Should be in a PANNING_LOCKED_X state with no vertical velocity.
+ apzc->AssertStateIsPanningLockedX();
+ apzc->AssertAxisLocked(ScrollDirection::eHorizontal);
+ EXPECT_GT(apzc->GetVelocityVector().x, 0);
+ EXPECT_EQ(apzc->GetVelocityVector().y, 0);
+
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_END, manager, ScreenIntPoint(50, 50),
+ ScreenPoint(0, 0), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ // Double check that we have not panned on the vertical axis.
+ ParentLayerPoint panEndOffset = apzc->GetCurrentAsyncScrollOffset(
+ AsyncPanZoomController::AsyncTransformConsumer::eForHitTesting);
+ EXPECT_EQ(panEndOffset.y, 0);
+
+ // Ensure that the axis locks extends into momentum scroll.
+ apzc->AssertAxisLocked(ScrollDirection::eHorizontal);
+
+ // Start the momentum scroll.
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMSTART, manager,
+ ScreenIntPoint(50, 50), ScreenPoint(80, 40), mcc->Time());
+ mcc->AdvanceByMillis(10);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, manager,
+ ScreenIntPoint(50, 50), ScreenPoint(20, 10), mcc->Time());
+ mcc->AdvanceByMillis(10);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, manager,
+ ScreenIntPoint(50, 50), ScreenPoint(20, 10), mcc->Time());
+ mcc->AdvanceByMillis(10);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ // In momentum locking mode, we should still be locked onto the X axis.
+ apzc->AssertStateIsPanMomentum();
+ apzc->AssertAxisLocked(ScrollDirection::eHorizontal);
+ EXPECT_GT(apzc->GetVelocityVector().x, 0);
+ EXPECT_EQ(apzc->GetVelocityVector().y, 0);
+
+ ParentLayerPoint beforeBreakOffset = apzc->GetCurrentAsyncScrollOffset(
+ AsyncPanZoomController::AsyncTransformConsumer::eForHitTesting);
+ EXPECT_EQ(beforeBreakOffset.y, 0);
+ // Ensure we have scrolled some amount on the X axis in momentum scroll.
+ EXPECT_GT(beforeBreakOffset.x, panEndOffset.x);
+
+ // Kick off the gesture that breaks the lock onto the X axis.
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_START, manager, ScreenIntPoint(50, 50),
+ ScreenIntPoint(1, 2), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ ParentLayerPoint afterBreakOffset = apzc->GetCurrentAsyncScrollOffset(
+ AsyncPanZoomController::AsyncTransformConsumer::eForHitTesting);
+
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 50),
+ ScreenPoint(15, 30), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 50),
+ ScreenPoint(15, 30), mcc->Time());
+
+ // The lock onto the X axis should be broken and we now should be locked
+ // onto the Y axis.
+ apzc->AssertStateIsPanningLockedY();
+ apzc->AssertAxisLocked(ScrollDirection::eVertical);
+ EXPECT_GT(apzc->GetVelocityVector().y, 0);
+ EXPECT_EQ(apzc->GetVelocityVector().x, 0);
+
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_END, manager, ScreenIntPoint(50, 50),
+ ScreenPoint(0, 0), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ // The lock onto the Y axis extends into momentum scroll.
+ apzc->AssertAxisLocked(ScrollDirection::eVertical);
+
+ // Wait until the end of the animation and ensure the final state is
+ // reasonable.
+ apzc->AdvanceAnimationsUntilEnd();
+ ParentLayerPoint finalOffset = apzc->GetCurrentAsyncScrollOffset(
+ AsyncPanZoomController::AsyncTransformConsumer::eForHitTesting);
+
+ EXPECT_GT(finalOffset.y, 0);
+ // Ensure that we did not scroll on the X axis after the vertical scroll
+ // started.
+ EXPECT_EQ(finalOffset.x, afterBreakOffset.x);
+}
+
+TEST_F(APZCAxisLockTester, BreakStickyAxisLock) {
+ SCOPED_GFX_PREF_INT("apz.axis_lock.mode", 2);
+ SCOPED_GFX_PREF_FLOAT("apz.axis_lock.lock_angle", M_PI / 6.0f);
+ SCOPED_GFX_PREF_FLOAT("apz.axis_lock.breakout_angle", M_PI / 6.0f);
+
+ SetupBasicTest();
+
+ apzc = ApzcOf(root);
+
+ // Start a gesture to get us locked onto the Y axis.
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_START, manager, ScreenIntPoint(50, 50),
+ ScreenIntPoint(0, 2), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ // Ensure that we have locked onto the Y axis.
+ apzc->AssertStateIsPanningLockedY();
+
+ // Test switch to locking onto the X axis.
+ BreakStickyAxisLockTest(ScrollDirection::eHorizontal);
+
+ // Test switch back to locking onto the Y axis.
+ BreakStickyAxisLockTest(ScrollDirection::eVertical);
+
+ // Test breaking all axis locks from a Y axis lock.
+ BreakStickyAxisLockTest(ScrollDirections(ScrollDirection::eHorizontal,
+ ScrollDirection::eVertical));
+
+ // We should be in a panning state.
+ apzc->AssertStateIsPanning();
+ apzc->AssertNotAxisLocked();
+
+ // Lock back to the X axis.
+ BreakStickyAxisLockTestGesture(ScrollDirection::eHorizontal);
+
+ // End the gesture.
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_END, manager, ScreenIntPoint(50, 50),
+ ScreenPoint(0, 0), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ // Start a gesture to get us locked onto the X axis.
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_START, manager, ScreenIntPoint(50, 50),
+ ScreenIntPoint(2, 0), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ // Ensure that we have locked onto the X axis.
+ apzc->AssertStateIsPanningLockedX();
+
+ // Test breaking all axis locks from a X axis lock.
+ BreakStickyAxisLockTest(ScrollDirections(ScrollDirection::eHorizontal,
+ ScrollDirection::eVertical));
+
+ // We should be in a panning state.
+ apzc->AssertStateIsPanning();
+ apzc->AssertNotAxisLocked();
+
+ // Test switch back to locking onto the Y axis.
+ BreakStickyAxisLockTest(ScrollDirection::eVertical);
+}
+
+TEST_F(APZCAxisLockTester, BreakAxisLockByLockAngle) {
+ SCOPED_GFX_PREF_INT("apz.axis_lock.mode", 2);
+ SCOPED_GFX_PREF_FLOAT("apz.axis_lock.lock_angle", M_PI / 4.0f);
+ SCOPED_GFX_PREF_FLOAT("apz.axis_lock.breakout_angle", M_PI / 8.0f);
+
+ SetupBasicTest();
+
+ apzc = ApzcOf(root);
+
+ // Start a gesture to get us locked onto the Y axis.
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_START, manager, ScreenIntPoint(50, 50),
+ ScreenIntPoint(1, 10), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ // Ensure that we have locked onto the Y axis.
+ apzc->AssertStateIsPanningLockedY();
+
+ // Stay within 45 degrees from the X axis, and more than 22.5 degrees from
+ // the Y axis. This should break the Y lock and lock us to the X axis.
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_PAN, manager, ScreenIntPoint(50, 50),
+ ScreenIntPoint(12, 10), mcc->Time());
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ // Ensure that we have locked onto the X axis.
+ apzc->AssertStateIsPanningLockedX();
+
+ // End the gesture.
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_END, manager, ScreenIntPoint(50, 50),
+ ScreenPoint(0, 0), mcc->Time());
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+}
+
+TEST_F(APZCAxisLockTester, TestDominantAxisScrolling) {
+ SCOPED_GFX_PREF_INT("apz.axis_lock.mode", 3);
+
+ int panY;
+ int panX;
+
+ SetupBasicTest();
+
+ apzc = ApzcOf(root);
+
+ ParentLayerPoint lastOffset =
+ apzc->GetCurrentAsyncScrollOffset(AsyncPanZoomController::eForHitTesting);
+
+ // In dominant axis mode, test pan gesture events with varying gesture
+ // angles and ensure that we only pan on one axis.
+ for (panX = 0, panY = 50; panY >= 0; panY -= 10, panX += 5) {
+ // Gesture that should be locked onto one axis
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_START, manager,
+ ScreenIntPoint(50, 50), ScreenIntPoint(panX, panY), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 50),
+ ScreenPoint(static_cast<float>(panX), static_cast<float>(panY)),
+ mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_END, manager, ScreenIntPoint(50, 50),
+ ScreenPoint(0, 0), mcc->Time());
+ apzc->AdvanceAnimationsUntilEnd();
+
+ ParentLayerPoint scrollOffset = apzc->GetCurrentAsyncScrollOffset(
+ AsyncPanZoomController::eForHitTesting);
+
+ if (panX > panY) {
+ // If we're closer to the X axis ensure that we moved on the horizontal
+ // axis and there was no movement on the vertical axis.
+ EXPECT_GT(scrollOffset.x, lastOffset.x);
+ EXPECT_EQ(scrollOffset.y, lastOffset.y);
+ } else {
+ // If we're closer to the Y axis ensure that we moved on the vertical
+ // axis and there was no movement on the horizontal axis.
+ EXPECT_GT(scrollOffset.y, lastOffset.y);
+ EXPECT_EQ(scrollOffset.x, lastOffset.x);
+ }
+
+ lastOffset = scrollOffset;
+ }
+}
+
+TEST_F(APZCAxisLockTester, TestCanScrollWithAxisLock) {
+ SCOPED_GFX_PREF_INT("apz.axis_lock.mode", 2);
+
+ SetupBasicTest();
+
+ apzc = ApzcOf(root);
+
+ // The axis locks do not impact CanScroll()
+ apzc->SetAxisLocked(ScrollDirection::eHorizontal, true);
+ EXPECT_EQ(apzc->CanScroll(ParentLayerPoint(10, 0)), true);
+
+ apzc->SetAxisLocked(ScrollDirection::eHorizontal, false);
+ apzc->SetAxisLocked(ScrollDirection::eVertical, true);
+ EXPECT_EQ(apzc->CanScroll(ParentLayerPoint(0, 10)), true);
+}
+
+TEST_F(APZCAxisLockTester, TestScrollHandoffAxisLockConflict) {
+ SCOPED_GFX_PREF_INT("apz.axis_lock.mode", 2);
+
+ // Create two scrollable frames. One parent frame with one child.
+ const char* treeShape = "x(x)";
+ LayerIntRegion layerVisibleRegion[] = {
+ LayerIntRect(0, 0, 100, 100),
+ LayerIntRect(0, 0, 100, 100),
+ };
+ CreateScrollData(treeShape, layerVisibleRegion);
+ SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID,
+ CSSRect(0, 0, 500, 500));
+ SetScrollableFrameMetrics(layers[1], ScrollableLayerGuid::START_SCROLL_ID + 1,
+ CSSRect(0, 0, 500, 500));
+ SetScrollHandoff(layers[1], root);
+
+ registration = MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc);
+
+ UpdateHitTestingTree();
+
+ RefPtr<TestAsyncPanZoomController> rootApzc = ApzcOf(root);
+ apzc = ApzcOf(layers[1]);
+
+ // Create a gesture on the y-axis that should lock the x axis.
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
+ PanGesture(PanGestureInput::PANGESTURE_START, manager, ScreenIntPoint(50, 50),
+ ScreenIntPoint(0, 2), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 50),
+ ScreenPoint(0, 15), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
+ PanGesture(PanGestureInput::PANGESTURE_END, manager, ScreenIntPoint(50, 50),
+ ScreenPoint(0, 0), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimationsUntilEnd();
+
+ // We are locked onto the y-axis.
+ apzc->AssertAxisLocked(ScrollDirection::eVertical);
+
+ // There should be movement in the child.
+ ParentLayerPoint childCurrentOffset = apzc->GetCurrentAsyncScrollOffset(
+ AsyncPanZoomController::AsyncTransformConsumer::eForHitTesting);
+ EXPECT_GT(childCurrentOffset.y, 0);
+ EXPECT_EQ(childCurrentOffset.x, 0);
+
+ // There should be no movement in the parent.
+ ParentLayerPoint parentCurrentOffset = rootApzc->GetCurrentAsyncScrollOffset(
+ AsyncPanZoomController::AsyncTransformConsumer::eForHitTesting);
+ EXPECT_EQ(parentCurrentOffset.y, 0);
+ EXPECT_EQ(parentCurrentOffset.x, 0);
+
+ // Create a gesture on the x-axis, that should be directed
+ // at the child, even if the x-axis is locked.
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
+ PanGesture(PanGestureInput::PANGESTURE_START, manager, ScreenIntPoint(50, 50),
+ ScreenIntPoint(2, 0), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 50),
+ ScreenPoint(15, 0), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
+ PanGesture(PanGestureInput::PANGESTURE_END, manager, ScreenIntPoint(50, 50),
+ ScreenPoint(0, 0), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimationsUntilEnd();
+
+ // We broke the y-axis lock and are now locked onto the x-axis.
+ apzc->AssertAxisLocked(ScrollDirection::eHorizontal);
+
+ // There should be some movement in the child on the x-axis.
+ ParentLayerPoint childFinalOffset = apzc->GetCurrentAsyncScrollOffset(
+ AsyncPanZoomController::AsyncTransformConsumer::eForHitTesting);
+ EXPECT_GT(childFinalOffset.x, 0);
+
+ // There should still be no movement in the parent.
+ ParentLayerPoint parentFinalOffset = rootApzc->GetCurrentAsyncScrollOffset(
+ AsyncPanZoomController::AsyncTransformConsumer::eForHitTesting);
+ EXPECT_EQ(parentFinalOffset.y, 0);
+ EXPECT_EQ(parentFinalOffset.x, 0);
+}
+
+// The delta from the initial pan gesture should be reflected in the
+// current offset for all axis locking modes.
+TEST_P(APZCAxisLockCompatTester, TestPanGestureStart) {
+ const char* treeShape = "x";
+ LayerIntRegion layerVisibleRegion[] = {
+ LayerIntRect(0, 0, 100, 100),
+ };
+ CreateScrollData(treeShape, layerVisibleRegion);
+ SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID,
+ CSSRect(0, 0, 500, 500));
+
+ registration = MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc);
+
+ UpdateHitTestingTree();
+
+ apzc = ApzcOf(root);
+
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_START, manager, ScreenIntPoint(50, 50),
+ ScreenIntPoint(0, 10), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimationsUntilEnd();
+ ParentLayerPoint currentOffset = apzc->GetCurrentAsyncScrollOffset(
+ AsyncPanZoomController::AsyncTransformConsumer::eForHitTesting);
+
+ EXPECT_EQ(currentOffset.x, 0);
+ EXPECT_EQ(currentOffset.y, 10);
+}
+
+// All APZCAxisLockCompatTester tests should be run for each apz.axis_lock.mode.
+// If another mode is added, the value should be added to this list.
+INSTANTIATE_TEST_SUITE_P(APZCAxisLockCompat, APZCAxisLockCompatTester,
+ testing::Values(0, 1, 2, 3),
+ APZCAxisLockCompatTester::PrintFromParam);
diff --git a/gfx/layers/apz/test/gtest/TestBasic.cpp b/gfx/layers/apz/test/gtest/TestBasic.cpp
new file mode 100644
index 0000000000..52cebfccd4
--- /dev/null
+++ b/gfx/layers/apz/test/gtest/TestBasic.cpp
@@ -0,0 +1,639 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "APZCBasicTester.h"
+#include "APZTestCommon.h"
+
+#include "InputUtils.h"
+
+static ScrollGenerationCounter sGenerationCounter;
+
+TEST_F(APZCBasicTester, Overzoom) {
+ // the visible area of the document in CSS pixels is x=10 y=0 w=100 h=100
+ FrameMetrics fm;
+ fm.SetCompositionBounds(ParentLayerRect(0, 0, 100, 100));
+ fm.SetScrollableRect(CSSRect(0, 0, 125, 150));
+ fm.SetVisualScrollOffset(CSSPoint(10, 0));
+ fm.SetZoom(CSSToParentLayerScale(1.0));
+ fm.SetIsRootContent(true);
+ apzc->SetFrameMetrics(fm);
+
+ MakeApzcZoomable();
+
+ EXPECT_CALL(*mcc, RequestContentRepaint(_)).Times(1);
+
+ PinchWithPinchInputAndCheckStatus(apzc, ScreenIntPoint(50, 50), 0.5, true);
+
+ fm = apzc->GetFrameMetrics();
+ EXPECT_EQ(0.8f, fm.GetZoom().scale);
+ // bug 936721 - PGO builds introduce rounding error so
+ // use a fuzzy match instead
+ EXPECT_LT(std::abs(fm.GetVisualScrollOffset().x), 1e-5);
+ EXPECT_LT(std::abs(fm.GetVisualScrollOffset().y), 1e-5);
+}
+
+TEST_F(APZCBasicTester, ZoomLimits) {
+ SCOPED_GFX_PREF_FLOAT("apz.min_zoom", 0.9f);
+ SCOPED_GFX_PREF_FLOAT("apz.max_zoom", 2.0f);
+
+ // the visible area of the document in CSS pixels is x=10 y=0 w=100 h=100
+ FrameMetrics fm;
+ fm.SetCompositionBounds(ParentLayerRect(0, 0, 100, 100));
+ fm.SetScrollableRect(CSSRect(0, 0, 125, 150));
+ fm.SetZoom(CSSToParentLayerScale(1.0));
+ fm.SetIsRootContent(true);
+ apzc->SetFrameMetrics(fm);
+
+ MakeApzcZoomable();
+
+ // This should take the zoom scale to 0.8, but we've capped it at 0.9.
+ PinchWithPinchInputAndCheckStatus(apzc, ScreenIntPoint(50, 50), 0.5, true);
+
+ fm = apzc->GetFrameMetrics();
+ EXPECT_EQ(0.9f, fm.GetZoom().scale);
+
+ // This should take the zoom scale to 2.7, but we've capped it at 2.
+ PinchWithPinchInputAndCheckStatus(apzc, ScreenIntPoint(50, 50), 3, true);
+
+ fm = apzc->GetFrameMetrics();
+ EXPECT_EQ(2.0f, fm.GetZoom().scale);
+}
+
+TEST_F(APZCBasicTester, SimpleTransform) {
+ ParentLayerPoint pointOut;
+ AsyncTransform viewTransformOut;
+ apzc->SampleContentTransformForFrame(&viewTransformOut, pointOut);
+
+ EXPECT_EQ(ParentLayerPoint(), pointOut);
+ EXPECT_EQ(AsyncTransform(), viewTransformOut);
+}
+
+TEST_F(APZCBasicTester, ComplexTransform) {
+ // This test assumes there is a page that gets rendered to
+ // two layers. In CSS pixels, the first layer is 50x50 and
+ // the second layer is 25x50. The widget scale factor is 3.0
+ // and the presShell resolution is 2.0. Therefore, these layers
+ // end up being 300x300 and 150x300 in layer pixels.
+ //
+ // The second (child) layer has an additional CSS transform that
+ // stretches it by 2.0 on the x-axis. Therefore, after applying
+ // CSS transforms, the two layers are the same size in screen
+ // pixels.
+ //
+ // The screen itself is 24x24 in screen pixels (therefore 4x4 in
+ // CSS pixels). The displayport is 1 extra CSS pixel on all
+ // sides.
+
+ RefPtr<TestAsyncPanZoomController> childApzc =
+ new TestAsyncPanZoomController(LayersId{0}, mcc, tm);
+
+ const char* treeShape = "x(x)";
+ // LayerID 0 1
+ LayerIntRegion layerVisibleRegion[] = {
+ LayerIntRect(0, 0, 300, 300),
+ LayerIntRect(0, 0, 150, 300),
+ };
+ Matrix4x4 transforms[] = {
+ Matrix4x4(),
+ Matrix4x4(),
+ };
+ transforms[0].PostScale(
+ 0.5f, 0.5f,
+ 1.0f); // this results from the 2.0 resolution on the root layer
+ transforms[1].PostScale(
+ 2.0f, 1.0f,
+ 1.0f); // this is the 2.0 x-axis CSS transform on the child layer
+
+ auto layers = TestWRScrollData::Create(treeShape, *updater,
+ layerVisibleRegion, transforms);
+
+ ScrollMetadata metadata;
+ FrameMetrics& metrics = metadata.GetMetrics();
+ metrics.SetCompositionBounds(ParentLayerRect(0, 0, 24, 24));
+ metrics.SetDisplayPort(CSSRect(-1, -1, 6, 6));
+ metrics.SetVisualScrollOffset(CSSPoint(10, 10));
+ metrics.SetLayoutViewport(CSSRect(10, 10, 8, 8));
+ metrics.SetScrollableRect(CSSRect(0, 0, 50, 50));
+ metrics.SetCumulativeResolution(LayoutDeviceToLayerScale(2));
+ metrics.SetPresShellResolution(2.0f);
+ metrics.SetZoom(CSSToParentLayerScale(6));
+ metrics.SetDevPixelsPerCSSPixel(CSSToLayoutDeviceScale(3));
+ metrics.SetScrollId(ScrollableLayerGuid::START_SCROLL_ID);
+
+ ScrollMetadata childMetadata = metadata;
+ FrameMetrics& childMetrics = childMetadata.GetMetrics();
+ childMetrics.SetScrollId(ScrollableLayerGuid::START_SCROLL_ID + 1);
+
+ layers[0]->AppendScrollMetadata(layers, metadata);
+ layers[1]->AppendScrollMetadata(layers, childMetadata);
+
+ ParentLayerPoint pointOut;
+ AsyncTransform viewTransformOut;
+
+ // Both the parent and child layer should behave exactly the same here,
+ // because the CSS transform on the child layer does not affect the
+ // SampleContentTransformForFrame code
+
+ // initial transform
+ apzc->SetFrameMetrics(metrics);
+ apzc->NotifyLayersUpdated(metadata, true, true);
+ apzc->SampleContentTransformForFrame(&viewTransformOut, pointOut);
+ EXPECT_EQ(AsyncTransform(LayerToParentLayerScale(1), ParentLayerPoint()),
+ viewTransformOut);
+ EXPECT_EQ(ParentLayerPoint(60, 60), pointOut);
+
+ childApzc->SetFrameMetrics(childMetrics);
+ childApzc->NotifyLayersUpdated(childMetadata, true, true);
+ childApzc->SampleContentTransformForFrame(&viewTransformOut, pointOut);
+ EXPECT_EQ(AsyncTransform(LayerToParentLayerScale(1), ParentLayerPoint()),
+ viewTransformOut);
+ EXPECT_EQ(ParentLayerPoint(60, 60), pointOut);
+
+ // do an async scroll by 5 pixels and check the transform
+ metrics.ScrollBy(CSSPoint(5, 0));
+ apzc->SetFrameMetrics(metrics);
+ apzc->SampleContentTransformForFrame(&viewTransformOut, pointOut);
+ EXPECT_EQ(
+ AsyncTransform(LayerToParentLayerScale(1), ParentLayerPoint(-30, 0)),
+ viewTransformOut);
+ EXPECT_EQ(ParentLayerPoint(90, 60), pointOut);
+
+ childMetrics.ScrollBy(CSSPoint(5, 0));
+ childApzc->SetFrameMetrics(childMetrics);
+ childApzc->SampleContentTransformForFrame(&viewTransformOut, pointOut);
+ EXPECT_EQ(
+ AsyncTransform(LayerToParentLayerScale(1), ParentLayerPoint(-30, 0)),
+ viewTransformOut);
+ EXPECT_EQ(ParentLayerPoint(90, 60), pointOut);
+
+ // do an async zoom of 1.5x and check the transform
+ metrics.ZoomBy(1.5f);
+ apzc->SetFrameMetrics(metrics);
+ apzc->SampleContentTransformForFrame(&viewTransformOut, pointOut);
+ EXPECT_EQ(
+ AsyncTransform(LayerToParentLayerScale(1.5), ParentLayerPoint(-45, 0)),
+ viewTransformOut);
+ EXPECT_EQ(ParentLayerPoint(135, 90), pointOut);
+
+ childMetrics.ZoomBy(1.5f);
+ childApzc->SetFrameMetrics(childMetrics);
+ childApzc->SampleContentTransformForFrame(&viewTransformOut, pointOut);
+ EXPECT_EQ(
+ AsyncTransform(LayerToParentLayerScale(1.5), ParentLayerPoint(-45, 0)),
+ viewTransformOut);
+ EXPECT_EQ(ParentLayerPoint(135, 90), pointOut);
+
+ childApzc->Destroy();
+}
+
+TEST_F(APZCBasicTester, Fling) {
+ SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0f);
+ int touchStart = 50;
+ int touchEnd = 10;
+ ParentLayerPoint pointOut;
+ AsyncTransform viewTransformOut;
+
+ // Fling down. Each step scroll further down
+ Pan(apzc, touchStart, touchEnd);
+ ParentLayerPoint lastPoint;
+ for (int i = 1; i < 50; i += 1) {
+ apzc->SampleContentTransformForFrame(&viewTransformOut, pointOut,
+ TimeDuration::FromMilliseconds(1));
+ EXPECT_GT(pointOut.y, lastPoint.y);
+ lastPoint = pointOut;
+ }
+}
+
+#ifndef MOZ_WIDGET_ANDROID // Maybe fails on Android
+TEST_F(APZCBasicTester, ResumeInterruptedTouchDrag_Bug1592435) {
+ // Start a touch-drag and scroll some amount, not lifting the finger.
+ SCOPED_GFX_PREF_FLOAT("apz.touch_start_tolerance", 1.0f / 1000.0f);
+ ScreenIntPoint touchPos(10, 50);
+ uint64_t touchBlock = TouchDown(apzc, touchPos, mcc->Time()).mInputBlockId;
+ SetDefaultAllowedTouchBehavior(apzc, touchBlock);
+ for (int i = 0; i < 20; ++i) {
+ touchPos.y -= 1;
+ mcc->AdvanceByMillis(1);
+ TouchMove(apzc, touchPos, mcc->Time());
+ }
+
+ // Take note of the scroll offset before the interruption.
+ CSSPoint scrollOffsetBeforeInterruption =
+ apzc->GetFrameMetrics().GetVisualScrollOffset();
+
+ // Have the main thread interrupt the touch-drag by sending
+ // a main thread scroll update to a nearby location.
+ CSSPoint mainThreadOffset = scrollOffsetBeforeInterruption;
+ mainThreadOffset.y -= 5;
+ ScrollMetadata metadata = apzc->GetScrollMetadata();
+ metadata.GetMetrics().SetLayoutScrollOffset(mainThreadOffset);
+ nsTArray<ScrollPositionUpdate> scrollUpdates;
+ scrollUpdates.AppendElement(ScrollPositionUpdate::NewScroll(
+ ScrollOrigin::Other, CSSPoint::ToAppUnits(mainThreadOffset)));
+ metadata.SetScrollUpdates(scrollUpdates);
+ metadata.GetMetrics().SetScrollGeneration(
+ scrollUpdates.LastElement().GetGeneration());
+ apzc->NotifyLayersUpdated(metadata, false, true);
+
+ // Continue and finish the touch-drag gesture.
+ for (int i = 0; i < 20; ++i) {
+ touchPos.y -= 1;
+ mcc->AdvanceByMillis(1);
+ TouchMove(apzc, touchPos, mcc->Time());
+ }
+
+ // Check that the portion of the touch-drag that occurred after
+ // the interruption caused additional scrolling.
+ CSSPoint finalScrollOffset = apzc->GetFrameMetrics().GetVisualScrollOffset();
+ EXPECT_GT(finalScrollOffset.y, scrollOffsetBeforeInterruption.y);
+
+ // Now do the same thing, but for a visual scroll update.
+ scrollOffsetBeforeInterruption =
+ apzc->GetFrameMetrics().GetVisualScrollOffset();
+ mainThreadOffset = scrollOffsetBeforeInterruption;
+ mainThreadOffset.y -= 5;
+ metadata = apzc->GetScrollMetadata();
+ metadata.GetMetrics().SetVisualDestination(mainThreadOffset);
+ metadata.GetMetrics().SetScrollGeneration(
+ sGenerationCounter.NewMainThreadGeneration());
+ metadata.GetMetrics().SetVisualScrollUpdateType(FrameMetrics::eMainThread);
+ scrollUpdates.Clear();
+ metadata.SetScrollUpdates(scrollUpdates);
+ apzc->NotifyLayersUpdated(metadata, false, true);
+ for (int i = 0; i < 20; ++i) {
+ touchPos.y -= 1;
+ mcc->AdvanceByMillis(1);
+ TouchMove(apzc, touchPos, mcc->Time());
+ }
+ finalScrollOffset = apzc->GetFrameMetrics().GetVisualScrollOffset();
+ EXPECT_GT(finalScrollOffset.y, scrollOffsetBeforeInterruption.y);
+
+ // Clean up by ending the touch gesture.
+ mcc->AdvanceByMillis(1);
+ TouchUp(apzc, touchPos, mcc->Time());
+}
+#endif
+
+TEST_F(APZCBasicTester, RelativeScrollOffset) {
+ // Set up initial conditions: zoomed in, layout offset at (100, 100),
+ // visual offset at (120, 120); the relative offset is therefore (20, 20).
+ ScrollMetadata metadata;
+ FrameMetrics& metrics = metadata.GetMetrics();
+ metrics.SetScrollableRect(CSSRect(0, 0, 1000, 1000));
+ metrics.SetLayoutViewport(CSSRect(100, 100, 100, 100));
+ metrics.SetZoom(CSSToParentLayerScale(2.0));
+ metrics.SetCompositionBounds(ParentLayerRect(0, 0, 100, 100));
+ metrics.SetVisualScrollOffset(CSSPoint(120, 120));
+ metrics.SetIsRootContent(true);
+ apzc->SetFrameMetrics(metrics);
+
+ // Scroll the layout viewport to (200, 200).
+ ScrollMetadata mainThreadMetadata = metadata;
+ FrameMetrics& mainThreadMetrics = mainThreadMetadata.GetMetrics();
+ mainThreadMetrics.SetLayoutScrollOffset(CSSPoint(200, 200));
+ nsTArray<ScrollPositionUpdate> scrollUpdates;
+ scrollUpdates.AppendElement(ScrollPositionUpdate::NewScroll(
+ ScrollOrigin::Other, CSSPoint::ToAppUnits(CSSPoint(200, 200))));
+ mainThreadMetadata.SetScrollUpdates(scrollUpdates);
+ mainThreadMetrics.SetScrollGeneration(
+ scrollUpdates.LastElement().GetGeneration());
+ apzc->NotifyLayersUpdated(mainThreadMetadata, /*isFirstPaint=*/false,
+ /*thisLayerTreeUpdated=*/true);
+
+ // Check that the relative offset has been preserved.
+ metrics = apzc->GetFrameMetrics();
+ EXPECT_EQ(metrics.GetLayoutScrollOffset(), CSSPoint(200, 200));
+ EXPECT_EQ(metrics.GetVisualScrollOffset(), CSSPoint(220, 220));
+}
+
+TEST_F(APZCBasicTester, MultipleSmoothScrollsSmooth) {
+ SCOPED_GFX_PREF_BOOL("general.smoothScroll", true);
+ // We want to test that if we send multiple smooth scroll requests that we
+ // still smoothly animate, ie that we get non-zero change every frame while
+ // the animation is running.
+
+ ScrollMetadata metadata;
+ FrameMetrics& metrics = metadata.GetMetrics();
+ metrics.SetScrollableRect(CSSRect(0, 0, 100, 10000));
+ metrics.SetLayoutViewport(CSSRect(0, 0, 100, 100));
+ metrics.SetZoom(CSSToParentLayerScale(1.0));
+ metrics.SetCompositionBounds(ParentLayerRect(0, 0, 100, 100));
+ metrics.SetVisualScrollOffset(CSSPoint(0, 0));
+ metrics.SetIsRootContent(true);
+ apzc->SetFrameMetrics(metrics);
+
+ // Structure of this test.
+ // -send a pure relative smooth scroll request via NotifyLayersUpdated
+ // -advance animations a few times, check that scroll offset is increasing
+ // after the first few advances
+ // -send a pure relative smooth scroll request via NotifyLayersUpdated
+ // -advance animations a few times, check that scroll offset is increasing
+ // -send a pure relative smooth scroll request via NotifyLayersUpdated
+ // -advance animations a few times, check that scroll offset is increasing
+
+ ScrollMetadata metadata2 = metadata;
+ nsTArray<ScrollPositionUpdate> scrollUpdates2;
+ scrollUpdates2.AppendElement(ScrollPositionUpdate::NewPureRelativeScroll(
+ ScrollOrigin::Other, ScrollMode::Smooth,
+ CSSPoint::ToAppUnits(CSSPoint(0, 200))));
+ metadata2.SetScrollUpdates(scrollUpdates2);
+ metadata2.GetMetrics().SetScrollGeneration(
+ scrollUpdates2.LastElement().GetGeneration());
+ apzc->NotifyLayersUpdated(metadata2, /*isFirstPaint=*/false,
+ /*thisLayerTreeUpdated=*/true);
+
+ // Get the animation going
+ for (uint32_t i = 0; i < 3; i++) {
+ SampleAnimationOneFrame();
+ }
+
+ float offset =
+ apzc->GetCurrentAsyncScrollOffset(AsyncPanZoomController::eForCompositing)
+ .y;
+ ASSERT_GT(offset, 0);
+ float lastOffset = offset;
+
+ for (uint32_t i = 0; i < 2; i++) {
+ for (uint32_t j = 0; j < 3; j++) {
+ SampleAnimationOneFrame();
+ offset = apzc->GetCurrentAsyncScrollOffset(
+ AsyncPanZoomController::eForCompositing)
+ .y;
+ ASSERT_GT(offset, lastOffset);
+ lastOffset = offset;
+ }
+
+ ScrollMetadata metadata3 = metadata;
+ nsTArray<ScrollPositionUpdate> scrollUpdates3;
+ scrollUpdates3.AppendElement(ScrollPositionUpdate::NewPureRelativeScroll(
+ ScrollOrigin::Other, ScrollMode::Smooth,
+ CSSPoint::ToAppUnits(CSSPoint(0, 200))));
+ metadata3.SetScrollUpdates(scrollUpdates3);
+ metadata3.GetMetrics().SetScrollGeneration(
+ scrollUpdates3.LastElement().GetGeneration());
+ apzc->NotifyLayersUpdated(metadata3, /*isFirstPaint=*/false,
+ /*thisLayerTreeUpdated=*/true);
+ }
+
+ for (uint32_t j = 0; j < 7; j++) {
+ SampleAnimationOneFrame();
+ offset = apzc->GetCurrentAsyncScrollOffset(
+ AsyncPanZoomController::eForCompositing)
+ .y;
+ ASSERT_GT(offset, lastOffset);
+ lastOffset = offset;
+ }
+}
+
+class APZCSmoothScrollTester : public APZCBasicTester {
+ public:
+ // Test that a smooth scroll animation correctly handles its destination
+ // being updated by a relative scroll delta.
+ void TestSmoothScrollDestinationUpdate() {
+ // Set up scroll frame. Starting scroll position is (0, 0).
+ ScrollMetadata metadata;
+ FrameMetrics& metrics = metadata.GetMetrics();
+ metrics.SetScrollableRect(CSSRect(0, 0, 100, 10000));
+ metrics.SetLayoutViewport(CSSRect(0, 0, 100, 100));
+ metrics.SetZoom(CSSToParentLayerScale(1.0));
+ metrics.SetCompositionBounds(ParentLayerRect(0, 0, 100, 100));
+ metrics.SetVisualScrollOffset(CSSPoint(0, 0));
+ metrics.SetIsRootContent(true);
+ apzc->SetFrameMetrics(metrics);
+
+ // Start smooth scroll via main-thread request.
+ nsTArray<ScrollPositionUpdate> scrollUpdates;
+ scrollUpdates.AppendElement(ScrollPositionUpdate::NewPureRelativeScroll(
+ ScrollOrigin::Other, ScrollMode::Smooth,
+ CSSPoint::ToAppUnits(CSSPoint(0, 1000))));
+ metadata.SetScrollUpdates(scrollUpdates);
+ metrics.SetScrollGeneration(scrollUpdates.LastElement().GetGeneration());
+ apzc->NotifyLayersUpdated(metadata, false, true);
+
+ // Sample the smooth scroll animation until we get past y=500.
+ apzc->AssertStateIsSmoothScroll();
+ float y = 0;
+ while (y < 500) {
+ SampleAnimationOneFrame();
+ y = apzc->GetFrameMetrics().GetVisualScrollOffset().y;
+ }
+
+ // Send a relative scroll of y = -400.
+ scrollUpdates.Clear();
+ scrollUpdates.AppendElement(ScrollPositionUpdate::NewRelativeScroll(
+ CSSPoint::ToAppUnits(CSSPoint(0, 500)),
+ CSSPoint::ToAppUnits(CSSPoint(0, 100))));
+ metadata.SetScrollUpdates(scrollUpdates);
+ metrics.SetScrollGeneration(scrollUpdates.LastElement().GetGeneration());
+ apzc->NotifyLayersUpdated(metadata, false, false);
+
+ // Verify the relative scroll was applied but didn't cancel the animation.
+ float y2 = apzc->GetFrameMetrics().GetVisualScrollOffset().y;
+ ASSERT_EQ(y2, y - 400);
+ apzc->AssertStateIsSmoothScroll();
+
+ // Sample the animation again and check that it respected the relative
+ // scroll.
+ SampleAnimationOneFrame();
+ float y3 = apzc->GetFrameMetrics().GetVisualScrollOffset().y;
+ ASSERT_GT(y3, y2);
+ ASSERT_LT(y3, 500);
+
+ // Continue animation until done and check that it ended up at a correctly
+ // adjusted destination.
+ apzc->AdvanceAnimationsUntilEnd();
+ float y4 = apzc->GetFrameMetrics().GetVisualScrollOffset().y;
+ ASSERT_EQ(y4, 600); // 1000 (initial destination) - 400 (relative scroll)
+ }
+};
+
+TEST_F(APZCSmoothScrollTester, SmoothScrollDestinationUpdateBezier) {
+ SCOPED_GFX_PREF_BOOL("general.smoothScroll", true);
+ SCOPED_GFX_PREF_BOOL("general.smoothScroll.msdPhysics.enabled", false);
+ TestSmoothScrollDestinationUpdate();
+}
+
+TEST_F(APZCSmoothScrollTester, SmoothScrollDestinationUpdateMsd) {
+ SCOPED_GFX_PREF_BOOL("general.smoothScroll", true);
+ SCOPED_GFX_PREF_BOOL("general.smoothScroll.msdPhysics.enabled", true);
+ TestSmoothScrollDestinationUpdate();
+}
+
+TEST_F(APZCBasicTester, ZoomAndScrollableRectChangeAfterZoomChange) {
+ // We want to check that a small scrollable rect change (which causes us to
+ // reclamp our scroll position, including in the sampled state) does not move
+ // the scroll offset in the sample state based the zoom in the apzc, only
+ // based on the zoom in the sampled state.
+
+ // First we zoom in to the right hand side. Then start zooming out, then send
+ // a scrollable rect change and check that it doesn't change the sampled state
+ // scroll offset.
+
+ ScrollMetadata metadata;
+ FrameMetrics& metrics = metadata.GetMetrics();
+ metrics.SetCompositionBounds(ParentLayerRect(0, 0, 100, 100));
+ metrics.SetScrollableRect(CSSRect(0, 0, 100, 1000));
+ metrics.SetLayoutViewport(CSSRect(0, 0, 100, 100));
+ metrics.SetVisualScrollOffset(CSSPoint(0, 0));
+ metrics.SetZoom(CSSToParentLayerScale(1.0));
+ metrics.SetIsRootContent(true);
+ apzc->SetFrameMetrics(metrics);
+
+ MakeApzcZoomable();
+
+ // Zoom to right side.
+ ZoomTarget zoomTarget{CSSRect(75, 25, 25, 25)};
+ apzc->ZoomToRect(zoomTarget, 0);
+
+ // Run the animation to completion, should take 250ms/16.67ms = 15 frames, but
+ // do extra to make sure.
+ for (uint32_t i = 0; i < 30; i++) {
+ SampleAnimationOneFrame();
+ }
+
+ EXPECT_FALSE(apzc->IsAsyncZooming());
+
+ // Zoom out.
+ ZoomTarget zoomTarget2{CSSRect(0, 0, 100, 100)};
+ apzc->ZoomToRect(zoomTarget2, 0);
+
+ // Run the animation a few times to get it going.
+ for (uint32_t i = 0; i < 2; i++) {
+ SampleAnimationOneFrame();
+ }
+
+ // Check that it is decreasing in scale.
+ float prevScale =
+ apzc->GetCurrentPinchZoomScale(AsyncPanZoomController::eForCompositing)
+ .scale;
+ for (uint32_t i = 0; i < 2; i++) {
+ SampleAnimationOneFrame();
+ float scale =
+ apzc->GetCurrentPinchZoomScale(AsyncPanZoomController::eForCompositing)
+ .scale;
+ ASSERT_GT(prevScale, scale);
+ prevScale = scale;
+ }
+
+ float offset =
+ apzc->GetCurrentAsyncScrollOffset(AsyncPanZoomController::eForCompositing)
+ .x;
+
+ // Change the scrollable rect slightly to trigger a reclamp.
+ ScrollMetadata metadata2 = metadata;
+ metadata2.GetMetrics().SetScrollableRect(CSSRect(0, 0, 100, 1000.2));
+ apzc->NotifyLayersUpdated(metadata2, /*isFirstPaint=*/false,
+ /*thisLayerTreeUpdated=*/true);
+
+ float newOffset =
+ apzc->GetCurrentAsyncScrollOffset(AsyncPanZoomController::eForCompositing)
+ .x;
+
+ ASSERT_EQ(newOffset, offset);
+}
+
+TEST_F(APZCBasicTester, ZoomToRectAndCompositionBoundsChange) {
+ // We want to check that content sending a composition bounds change (due to
+ // addition of scrollbars) during a zoom animation does not cause us to take
+ // the out of date content resolution.
+
+ ScrollMetadata metadata;
+ FrameMetrics& metrics = metadata.GetMetrics();
+ metrics.SetCompositionBounds(ParentLayerRect(0, 0, 100, 100));
+ metrics.SetCompositionBoundsWidthIgnoringScrollbars(ParentLayerCoord{100});
+ metrics.SetScrollableRect(CSSRect(0, 0, 100, 1000));
+ metrics.SetLayoutViewport(CSSRect(0, 0, 100, 100));
+ metrics.SetVisualScrollOffset(CSSPoint(0, 0));
+ metrics.SetZoom(CSSToParentLayerScale(1.0));
+ metrics.SetIsRootContent(true);
+ apzc->SetFrameMetrics(metrics);
+
+ MakeApzcZoomable();
+
+ // Start a zoom to a rect.
+ ZoomTarget zoomTarget{CSSRect(25, 25, 25, 25)};
+ apzc->ZoomToRect(zoomTarget, 0);
+
+ // Run the animation a few times to get it going.
+ // Check that it is increasing in scale.
+ float prevScale =
+ apzc->GetCurrentPinchZoomScale(AsyncPanZoomController::eForCompositing)
+ .scale;
+ for (uint32_t i = 0; i < 3; i++) {
+ SampleAnimationOneFrame();
+ float scale =
+ apzc->GetCurrentPinchZoomScale(AsyncPanZoomController::eForCompositing)
+ .scale;
+ ASSERT_GE(scale, prevScale);
+ prevScale = scale;
+ }
+
+ EXPECT_TRUE(apzc->IsAsyncZooming());
+
+ // Simulate the appearance of a scrollbar by reducing the width of
+ // the composition bounds, while keeping
+ // mCompositionBoundsWidthIgnoringScrollbars unchanged.
+ ScrollMetadata metadata2 = metadata;
+ metadata2.GetMetrics().SetCompositionBounds(ParentLayerRect(0, 0, 90, 100));
+ apzc->NotifyLayersUpdated(metadata2, /*isFirstPaint=*/false,
+ /*thisLayerTreeUpdated=*/true);
+
+ float scale =
+ apzc->GetCurrentPinchZoomScale(AsyncPanZoomController::eForCompositing)
+ .scale;
+
+ ASSERT_EQ(scale, prevScale);
+
+ // Run the rest of the animation to completion, should take 250ms/16.67ms = 15
+ // frames total, but do extra to make sure.
+ for (uint32_t i = 0; i < 30; i++) {
+ SampleAnimationOneFrame();
+ scale =
+ apzc->GetCurrentPinchZoomScale(AsyncPanZoomController::eForCompositing)
+ .scale;
+ ASSERT_GE(scale, prevScale);
+ prevScale = scale;
+ }
+
+ EXPECT_FALSE(apzc->IsAsyncZooming());
+}
+
+TEST_F(APZCBasicTester, StartTolerance) {
+ SCOPED_GFX_PREF_FLOAT("apz.touch_start_tolerance", 10 / tm->GetDPI());
+
+ FrameMetrics fm;
+ fm.SetCompositionBounds(ParentLayerRect(0, 0, 100, 100));
+ fm.SetScrollableRect(CSSRect(0, 0, 100, 300));
+ fm.SetVisualScrollOffset(CSSPoint(0, 50));
+ fm.SetIsRootContent(true);
+ apzc->SetFrameMetrics(fm);
+
+ uint64_t touchBlock = TouchDown(apzc, {50, 50}, mcc->Time()).mInputBlockId;
+ SetDefaultAllowedTouchBehavior(apzc, touchBlock);
+
+ CSSPoint initialScrollOffset =
+ apzc->GetFrameMetrics().GetVisualScrollOffset();
+
+ mcc->AdvanceByMillis(1);
+ TouchMove(apzc, {50, 70}, mcc->Time());
+
+ // Expect 10 pixels of scrolling: the distance from (50,50) to (50,70)
+ // minus the 10-pixel touch start tolerance.
+ ASSERT_EQ(initialScrollOffset.y - 10,
+ apzc->GetFrameMetrics().GetVisualScrollOffset().y);
+
+ mcc->AdvanceByMillis(1);
+ TouchMove(apzc, {50, 90}, mcc->Time());
+
+ // Expect 30 pixels of scrolling: the distance from (50,50) to (50,90)
+ // minus the 10-pixel touch start tolerance.
+ ASSERT_EQ(initialScrollOffset.y - 30,
+ apzc->GetFrameMetrics().GetVisualScrollOffset().y);
+
+ // Clean up by ending the touch gesture.
+ mcc->AdvanceByMillis(1);
+ TouchUp(apzc, {50, 90}, mcc->Time());
+}
diff --git a/gfx/layers/apz/test/gtest/TestEventRegions.cpp b/gfx/layers/apz/test/gtest/TestEventRegions.cpp
new file mode 100644
index 0000000000..0b4564b49f
--- /dev/null
+++ b/gfx/layers/apz/test/gtest/TestEventRegions.cpp
@@ -0,0 +1,199 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "APZCTreeManagerTester.h"
+#include "APZTestCommon.h"
+#include "InputUtils.h"
+#include "mozilla/layers/LayersTypes.h"
+
+class APZEventRegionsTester : public APZCTreeManagerTester {
+ protected:
+ UniquePtr<ScopedLayerTreeRegistration> registration;
+ TestAsyncPanZoomController* rootApzc;
+
+ void CreateEventRegionsLayerTree1() {
+ const char* treeShape = "x(xx)";
+ LayerIntRegion layerVisibleRegions[] = {
+ LayerIntRect(0, 0, 200, 200), // root
+ LayerIntRect(0, 0, 100, 200), // left half
+ LayerIntRect(0, 100, 200, 100), // bottom half
+ };
+ CreateScrollData(treeShape, layerVisibleRegions);
+ SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID);
+ SetScrollableFrameMetrics(layers[1],
+ ScrollableLayerGuid::START_SCROLL_ID + 1);
+ SetScrollableFrameMetrics(layers[2],
+ ScrollableLayerGuid::START_SCROLL_ID + 2);
+ SetScrollHandoff(layers[1], root);
+ SetScrollHandoff(layers[2], root);
+
+ registration = MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc);
+ UpdateHitTestingTree();
+ rootApzc = ApzcOf(root);
+ }
+
+ void CreateEventRegionsLayerTree2() {
+ const char* treeShape = "x(x)";
+ LayerIntRegion layerVisibleRegions[] = {
+ LayerIntRect(0, 0, 100, 500),
+ LayerIntRect(0, 150, 100, 100),
+ };
+ CreateScrollData(treeShape, layerVisibleRegions);
+ SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID);
+
+ registration = MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc);
+ UpdateHitTestingTree();
+ rootApzc = ApzcOf(root);
+ }
+
+ void CreateBug1117712LayerTree() {
+ const char* treeShape = "x(x(x)x)";
+ // LayerID 0 1 2 3
+ // 0 is the root
+ // 1 is a container layer whose sole purpose to make a non-empty ancestor
+ // transform for 2, so that 2's screen-to-apzc and apzc-to-gecko
+ // transforms are different from 3's.
+ // 2 is a small layer that is the actual target
+ // 3 is a big layer obscuring 2 with a dispatch-to-content region
+ LayerIntRegion layerVisibleRegions[] = {
+ LayerIntRect(0, 0, 100, 100),
+ LayerIntRect(0, 0, 0, 0),
+ LayerIntRect(0, 0, 10, 10),
+ LayerIntRect(0, 0, 100, 100),
+ };
+ Matrix4x4 layerTransforms[] = {
+ Matrix4x4(),
+ Matrix4x4::Translation(50, 0, 0),
+ Matrix4x4(),
+ Matrix4x4(),
+ };
+ CreateScrollData(treeShape, layerVisibleRegions, layerTransforms);
+
+ SetScrollableFrameMetrics(layers[2], ScrollableLayerGuid::START_SCROLL_ID,
+ CSSRect(0, 0, 10, 10));
+ SetScrollableFrameMetrics(layers[3],
+ ScrollableLayerGuid::START_SCROLL_ID + 1,
+ CSSRect(0, 0, 100, 100));
+ SetScrollHandoff(layers[3], layers[2]);
+
+ registration = MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc);
+ UpdateHitTestingTree();
+ }
+};
+
+class APZEventRegionsTesterMock : public APZEventRegionsTester {
+ public:
+ APZEventRegionsTesterMock() { CreateMockHitTester(); }
+};
+
+TEST_F(APZEventRegionsTesterMock, HitRegionImmediateResponse) {
+ CreateEventRegionsLayerTree1();
+
+ TestAsyncPanZoomController* root = ApzcOf(layers[0]);
+ TestAsyncPanZoomController* left = ApzcOf(layers[1]);
+ TestAsyncPanZoomController* bottom = ApzcOf(layers[2]);
+
+ MockFunction<void(std::string checkPointName)> check;
+ {
+ InSequence s;
+ EXPECT_CALL(*mcc, HandleTap(TapType::eSingleTap, _, _, left->GetGuid(), _))
+ .Times(1);
+ EXPECT_CALL(check, Call("Tapped on left"));
+ EXPECT_CALL(*mcc,
+ HandleTap(TapType::eSingleTap, _, _, bottom->GetGuid(), _))
+ .Times(1);
+ EXPECT_CALL(check, Call("Tapped on bottom"));
+ EXPECT_CALL(*mcc, HandleTap(TapType::eSingleTap, _, _, root->GetGuid(), _))
+ .Times(1);
+ EXPECT_CALL(check, Call("Tapped on root"));
+ EXPECT_CALL(check, Call("Tap pending on d-t-c region"));
+ EXPECT_CALL(*mcc,
+ HandleTap(TapType::eSingleTap, _, _, bottom->GetGuid(), _))
+ .Times(1);
+ EXPECT_CALL(check, Call("Tapped on bottom again"));
+ EXPECT_CALL(*mcc, HandleTap(TapType::eSingleTap, _, _, left->GetGuid(), _))
+ .Times(1);
+ EXPECT_CALL(check, Call("Tapped on left this time"));
+ }
+
+ TimeDuration tapDuration = TimeDuration::FromMilliseconds(100);
+
+ // Tap in the exposed hit regions of each of the layers once and ensure
+ // the clicks are dispatched right away
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
+ Tap(manager, ScreenIntPoint(10, 10), tapDuration);
+ mcc->RunThroughDelayedTasks(); // this runs the tap event
+ check.Call("Tapped on left");
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 2);
+ Tap(manager, ScreenIntPoint(110, 110), tapDuration);
+ mcc->RunThroughDelayedTasks(); // this runs the tap event
+ check.Call("Tapped on bottom");
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ Tap(manager, ScreenIntPoint(110, 10), tapDuration);
+ mcc->RunThroughDelayedTasks(); // this runs the tap event
+ check.Call("Tapped on root");
+
+ // Now tap on the dispatch-to-content region where the layers overlap
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 2,
+ {CompositorHitTestFlags::eVisibleToHitTest,
+ CompositorHitTestFlags::eIrregularArea});
+ Tap(manager, ScreenIntPoint(10, 110), tapDuration);
+ mcc->RunThroughDelayedTasks(); // this runs the main-thread timeout
+ check.Call("Tap pending on d-t-c region");
+ mcc->RunThroughDelayedTasks(); // this runs the tap event
+ check.Call("Tapped on bottom again");
+
+ // Now let's do that again, but simulate a main-thread response
+ uint64_t inputBlockId = 0;
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 2,
+ {CompositorHitTestFlags::eVisibleToHitTest,
+ CompositorHitTestFlags::eIrregularArea});
+ Tap(manager, ScreenIntPoint(10, 110), tapDuration, nullptr, &inputBlockId);
+ nsTArray<ScrollableLayerGuid> targets;
+ targets.AppendElement(left->GetGuid());
+ manager->SetTargetAPZC(inputBlockId, targets);
+ while (mcc->RunThroughDelayedTasks())
+ ; // this runs the tap event
+ check.Call("Tapped on left this time");
+}
+
+TEST_F(APZEventRegionsTesterMock, HitRegionAccumulatesChildren) {
+ CreateEventRegionsLayerTree2();
+
+ // Tap in the area of the child layer that's not directly included in the
+ // parent layer's hit region. Verify that it comes out of the APZC's
+ // content controller, which indicates the input events got routed correctly
+ // to the APZC.
+ EXPECT_CALL(*mcc,
+ HandleTap(TapType::eSingleTap, _, _, rootApzc->GetGuid(), _))
+ .Times(1);
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ Tap(manager, ScreenIntPoint(10, 160), TimeDuration::FromMilliseconds(100));
+}
+
+TEST_F(APZEventRegionsTesterMock, Bug1117712) {
+ CreateBug1117712LayerTree();
+
+ TestAsyncPanZoomController* apzc2 = ApzcOf(layers[2]);
+
+ // These touch events should hit the dispatch-to-content region of layers[3]
+ // and so get queued with that APZC as the tentative target.
+ uint64_t inputBlockId = 0;
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1,
+ {CompositorHitTestFlags::eVisibleToHitTest,
+ CompositorHitTestFlags::eIrregularArea});
+ Tap(manager, ScreenIntPoint(55, 5), TimeDuration::FromMilliseconds(100),
+ nullptr, &inputBlockId);
+ // But now we tell the APZ that really it hit layers[2], and expect the tap
+ // to be delivered at the correct coordinates.
+ EXPECT_CALL(*mcc, HandleTap(TapType::eSingleTap, LayoutDevicePoint(55, 5), 0,
+ apzc2->GetGuid(), _))
+ .Times(1);
+
+ nsTArray<ScrollableLayerGuid> targets;
+ targets.AppendElement(apzc2->GetGuid());
+ manager->SetTargetAPZC(inputBlockId, targets);
+}
diff --git a/gfx/layers/apz/test/gtest/TestEventResult.cpp b/gfx/layers/apz/test/gtest/TestEventResult.cpp
new file mode 100644
index 0000000000..90d17ee511
--- /dev/null
+++ b/gfx/layers/apz/test/gtest/TestEventResult.cpp
@@ -0,0 +1,476 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "APZCTreeManagerTester.h"
+#include "APZTestCommon.h"
+#include "InputUtils.h"
+#include "mozilla/EventForwards.h"
+#include "mozilla/layers/LayersTypes.h"
+#include <tuple>
+
+class APZEventResultTester : public APZCTreeManagerTester {
+ protected:
+ UniquePtr<ScopedLayerTreeRegistration> registration;
+
+ void UpdateOverscrollBehavior(OverscrollBehavior aX, OverscrollBehavior aY) {
+ ModifyFrameMetrics(root, [aX, aY](ScrollMetadata& sm, FrameMetrics& _) {
+ OverscrollBehaviorInfo overscroll;
+ overscroll.mBehaviorX = aX;
+ overscroll.mBehaviorY = aY;
+ sm.SetOverscrollBehavior(overscroll);
+ });
+ UpdateHitTestingTree();
+ }
+
+ void SetScrollOffsetOnMainThread(const CSSPoint& aPoint) {
+ RefPtr<TestAsyncPanZoomController> apzc = ApzcOf(root);
+
+ ScrollMetadata metadata = apzc->GetScrollMetadata();
+ metadata.GetMetrics().SetLayoutScrollOffset(aPoint);
+ nsTArray<ScrollPositionUpdate> scrollUpdates;
+ scrollUpdates.AppendElement(ScrollPositionUpdate::NewScroll(
+ ScrollOrigin::Other, CSSPoint::ToAppUnits(aPoint)));
+ metadata.SetScrollUpdates(scrollUpdates);
+ metadata.GetMetrics().SetScrollGeneration(
+ scrollUpdates.LastElement().GetGeneration());
+ apzc->NotifyLayersUpdated(metadata, /*aIsFirstPaint=*/false,
+ /*aThisLayerTreeUpdated=*/true);
+ }
+
+ void CreateScrollableRootLayer() {
+ const char* treeShape = "x";
+ LayerIntRegion layerVisibleRegions[] = {
+ LayerIntRect(0, 0, 100, 100),
+ };
+ CreateScrollData(treeShape, layerVisibleRegions);
+ SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID,
+ CSSRect(0, 0, 200, 200));
+ ModifyFrameMetrics(root, [](ScrollMetadata& sm, FrameMetrics& metrics) {
+ metrics.SetIsRootContent(true);
+ });
+ registration = MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc);
+ UpdateHitTestingTree();
+ }
+
+ enum class PreventDefaultFlag { No, Yes };
+ std::tuple<APZEventResult, APZHandledResult> TapDispatchToContent(
+ const ScreenIntPoint& aPoint, PreventDefaultFlag aPreventDefaultFlag) {
+ APZEventResult result =
+ Tap(manager, aPoint, TimeDuration::FromMilliseconds(100));
+
+ APZHandledResult delayedAnswer{APZHandledPlace::Invalid, SideBits::eNone,
+ ScrollDirections()};
+ manager->AddInputBlockCallback(
+ result.mInputBlockId,
+ {result.GetStatus(), [&](uint64_t id, const APZHandledResult& answer) {
+ EXPECT_EQ(id, result.mInputBlockId);
+ delayedAnswer = answer;
+ }});
+ manager->SetAllowedTouchBehavior(result.mInputBlockId,
+ {AllowedTouchBehavior::VERTICAL_PAN});
+ manager->SetTargetAPZC(result.mInputBlockId, {result.mTargetGuid});
+ manager->ContentReceivedInputBlock(
+ result.mInputBlockId, aPreventDefaultFlag == PreventDefaultFlag::Yes);
+ return {result, delayedAnswer};
+ }
+
+ void OverscrollDirectionsWithEventHandlerTest(
+ PreventDefaultFlag aPreventDefaultFlag) {
+ UpdateHitTestingTree();
+
+ APZHandledPlace expectedPlace =
+ aPreventDefaultFlag == PreventDefaultFlag::No
+ ? APZHandledPlace::HandledByRoot
+ : APZHandledPlace::HandledByContent;
+ {
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID,
+ {CompositorHitTestFlags::eVisibleToHitTest,
+ CompositorHitTestFlags::eIrregularArea});
+ auto [result, delayedHandledResult] =
+ TapDispatchToContent(ScreenIntPoint(50, 50), aPreventDefaultFlag);
+ EXPECT_EQ(result.GetHandledResult(), Nothing());
+ EXPECT_EQ(
+ delayedHandledResult,
+ (APZHandledResult{expectedPlace, SideBits::eBottom | SideBits::eRight,
+ EitherScrollDirection}));
+ }
+
+ // overscroll-behavior: contain, contain.
+ UpdateOverscrollBehavior(OverscrollBehavior::Contain,
+ OverscrollBehavior::Contain);
+ {
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID,
+ {CompositorHitTestFlags::eVisibleToHitTest,
+ CompositorHitTestFlags::eIrregularArea});
+ auto [result, delayedHandledResult] =
+ TapDispatchToContent(ScreenIntPoint(50, 50), aPreventDefaultFlag);
+ EXPECT_EQ(result.GetHandledResult(), Nothing());
+ EXPECT_EQ(
+ delayedHandledResult,
+ (APZHandledResult{expectedPlace, SideBits::eBottom | SideBits::eRight,
+ ScrollDirections()}));
+ }
+
+ // overscroll-behavior: none, none.
+ UpdateOverscrollBehavior(OverscrollBehavior::None,
+ OverscrollBehavior::None);
+ {
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID,
+ {CompositorHitTestFlags::eVisibleToHitTest,
+ CompositorHitTestFlags::eIrregularArea});
+ auto [result, delayedHandledResult] =
+ TapDispatchToContent(ScreenIntPoint(50, 50), aPreventDefaultFlag);
+ EXPECT_EQ(result.GetHandledResult(), Nothing());
+ EXPECT_EQ(
+ delayedHandledResult,
+ (APZHandledResult{expectedPlace, SideBits::eBottom | SideBits::eRight,
+ ScrollDirections()}));
+ }
+
+ // overscroll-behavior: auto, none.
+ UpdateOverscrollBehavior(OverscrollBehavior::Auto,
+ OverscrollBehavior::None);
+ {
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID,
+ {CompositorHitTestFlags::eVisibleToHitTest,
+ CompositorHitTestFlags::eIrregularArea});
+ auto [result, delayedHandledResult] =
+ TapDispatchToContent(ScreenIntPoint(50, 50), aPreventDefaultFlag);
+ EXPECT_EQ(result.GetHandledResult(), Nothing());
+ EXPECT_EQ(
+ delayedHandledResult,
+ (APZHandledResult{expectedPlace, SideBits::eBottom | SideBits::eRight,
+ HorizontalScrollDirection}));
+ }
+
+ // overscroll-behavior: none, auto.
+ UpdateOverscrollBehavior(OverscrollBehavior::None,
+ OverscrollBehavior::Auto);
+ {
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID,
+ {CompositorHitTestFlags::eVisibleToHitTest,
+ CompositorHitTestFlags::eIrregularArea});
+ auto [result, delayedHandledResult] =
+ TapDispatchToContent(ScreenIntPoint(50, 50), aPreventDefaultFlag);
+ EXPECT_EQ(result.GetHandledResult(), Nothing());
+ EXPECT_EQ(
+ delayedHandledResult,
+ (APZHandledResult{expectedPlace, SideBits::eBottom | SideBits::eRight,
+ VerticalScrollDirection}));
+ }
+ }
+
+ void ScrollableDirectionsWithEventHandlerTest(
+ PreventDefaultFlag aPreventDefaultFlag) {
+ UpdateHitTestingTree();
+
+ APZHandledPlace expectedPlace =
+ aPreventDefaultFlag == PreventDefaultFlag::No
+ ? APZHandledPlace::HandledByRoot
+ : APZHandledPlace::HandledByContent;
+ {
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID,
+ {CompositorHitTestFlags::eVisibleToHitTest,
+ CompositorHitTestFlags::eIrregularArea});
+ auto [result, delayedHandledResult] =
+ TapDispatchToContent(ScreenIntPoint(50, 50), aPreventDefaultFlag);
+ EXPECT_EQ(result.GetHandledResult(), Nothing());
+ EXPECT_EQ(
+ delayedHandledResult,
+ (APZHandledResult{expectedPlace, SideBits::eBottom | SideBits::eRight,
+ EitherScrollDirection}));
+ }
+
+ // scroll down a bit.
+ SetScrollOffsetOnMainThread(CSSPoint(0, 10));
+ {
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID,
+ {CompositorHitTestFlags::eVisibleToHitTest,
+ CompositorHitTestFlags::eIrregularArea});
+ auto [result, delayedHandledResult] =
+ TapDispatchToContent(ScreenIntPoint(50, 50), aPreventDefaultFlag);
+ EXPECT_EQ(result.GetHandledResult(), Nothing());
+ EXPECT_EQ(delayedHandledResult,
+ (APZHandledResult{
+ expectedPlace,
+ SideBits::eTop | SideBits::eBottom | SideBits::eRight,
+ EitherScrollDirection}));
+ }
+
+ // scroll to the bottom edge
+ SetScrollOffsetOnMainThread(CSSPoint(0, 100));
+ {
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID,
+ {CompositorHitTestFlags::eVisibleToHitTest,
+ CompositorHitTestFlags::eIrregularArea});
+ auto [result, delayedHandledResult] =
+ TapDispatchToContent(ScreenIntPoint(50, 50), aPreventDefaultFlag);
+ EXPECT_EQ(result.GetHandledResult(), Nothing());
+ EXPECT_EQ(
+ delayedHandledResult,
+ (APZHandledResult{expectedPlace, SideBits::eRight | SideBits::eTop,
+ EitherScrollDirection}));
+ }
+
+ // scroll to right a bit.
+ SetScrollOffsetOnMainThread(CSSPoint(10, 100));
+ {
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID,
+ {CompositorHitTestFlags::eVisibleToHitTest,
+ CompositorHitTestFlags::eIrregularArea});
+ auto [result, delayedHandledResult] =
+ TapDispatchToContent(ScreenIntPoint(50, 50), aPreventDefaultFlag);
+ EXPECT_EQ(result.GetHandledResult(), Nothing());
+ EXPECT_EQ(
+ delayedHandledResult,
+ (APZHandledResult{expectedPlace,
+ SideBits::eLeft | SideBits::eRight | SideBits::eTop,
+ EitherScrollDirection}));
+ }
+
+ // scroll to the right edge.
+ SetScrollOffsetOnMainThread(CSSPoint(100, 100));
+ {
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID,
+ {CompositorHitTestFlags::eVisibleToHitTest,
+ CompositorHitTestFlags::eIrregularArea});
+ auto [result, delayedHandledResult] =
+ TapDispatchToContent(ScreenIntPoint(50, 50), aPreventDefaultFlag);
+ EXPECT_EQ(result.GetHandledResult(), Nothing());
+ EXPECT_EQ(
+ delayedHandledResult,
+ (APZHandledResult{expectedPlace, SideBits::eTop | SideBits::eLeft,
+ EitherScrollDirection}));
+ }
+ }
+};
+
+TEST_F(APZEventResultTester, OverscrollDirections) {
+ CreateScrollableRootLayer();
+
+ TimeDuration tapDuration = TimeDuration::FromMilliseconds(100);
+
+ // The default value of overscroll-behavior is auto.
+ APZEventResult result = Tap(manager, ScreenIntPoint(50, 50), tapDuration);
+ EXPECT_EQ(result.GetHandledResult()->mOverscrollDirections,
+ EitherScrollDirection);
+
+ // overscroll-behavior: contain, contain.
+ UpdateOverscrollBehavior(OverscrollBehavior::Contain,
+ OverscrollBehavior::Contain);
+ result = Tap(manager, ScreenIntPoint(50, 50), tapDuration);
+ EXPECT_EQ(result.GetHandledResult()->mOverscrollDirections,
+ ScrollDirections());
+
+ // overscroll-behavior: none, none.
+ UpdateOverscrollBehavior(OverscrollBehavior::None, OverscrollBehavior::None);
+ result = Tap(manager, ScreenIntPoint(50, 50), tapDuration);
+ EXPECT_EQ(result.GetHandledResult()->mOverscrollDirections,
+ ScrollDirections());
+
+ // overscroll-behavior: auto, none.
+ UpdateOverscrollBehavior(OverscrollBehavior::Auto, OverscrollBehavior::None);
+ result = Tap(manager, ScreenIntPoint(50, 50), tapDuration);
+ EXPECT_EQ(result.GetHandledResult()->mOverscrollDirections,
+ HorizontalScrollDirection);
+
+ // overscroll-behavior: none, auto.
+ UpdateOverscrollBehavior(OverscrollBehavior::None, OverscrollBehavior::Auto);
+ result = Tap(manager, ScreenIntPoint(50, 50), tapDuration);
+ EXPECT_EQ(result.GetHandledResult()->mOverscrollDirections,
+ VerticalScrollDirection);
+}
+
+TEST_F(APZEventResultTester, ScrollableDirections) {
+ CreateScrollableRootLayer();
+
+ TimeDuration tapDuration = TimeDuration::FromMilliseconds(100);
+
+ APZEventResult result = Tap(manager, ScreenIntPoint(50, 50), tapDuration);
+ // scrollable to down/right.
+ EXPECT_EQ(result.GetHandledResult()->mScrollableDirections,
+ SideBits::eBottom | SideBits::eRight);
+
+ // scroll down a bit.
+ SetScrollOffsetOnMainThread(CSSPoint(0, 10));
+ result = Tap(manager, ScreenIntPoint(50, 50), tapDuration);
+ // also scrollable toward top.
+ EXPECT_EQ(result.GetHandledResult()->mScrollableDirections,
+ SideBits::eTop | SideBits::eBottom | SideBits::eRight);
+
+ // scroll to the bottom edge
+ SetScrollOffsetOnMainThread(CSSPoint(0, 100));
+ result = Tap(manager, ScreenIntPoint(50, 50), tapDuration);
+ EXPECT_EQ(result.GetHandledResult()->mScrollableDirections,
+ SideBits::eRight | SideBits::eTop);
+
+ // scroll to right a bit.
+ SetScrollOffsetOnMainThread(CSSPoint(10, 100));
+ result = Tap(manager, ScreenIntPoint(50, 50), tapDuration);
+ EXPECT_EQ(result.GetHandledResult()->mScrollableDirections,
+ SideBits::eLeft | SideBits::eRight | SideBits::eTop);
+
+ // scroll to the right edge.
+ SetScrollOffsetOnMainThread(CSSPoint(100, 100));
+ result = Tap(manager, ScreenIntPoint(50, 50), tapDuration);
+ EXPECT_EQ(result.GetHandledResult()->mScrollableDirections,
+ SideBits::eLeft | SideBits::eTop);
+}
+
+class APZEventResultTesterMock : public APZEventResultTester {
+ public:
+ APZEventResultTesterMock() { CreateMockHitTester(); }
+};
+
+TEST_F(APZEventResultTesterMock, OverscrollDirectionsWithEventHandler) {
+ CreateScrollableRootLayer();
+
+ OverscrollDirectionsWithEventHandlerTest(PreventDefaultFlag::No);
+}
+
+TEST_F(APZEventResultTesterMock,
+ OverscrollDirectionsWithPreventDefaultEventHandler) {
+ CreateScrollableRootLayer();
+
+ OverscrollDirectionsWithEventHandlerTest(PreventDefaultFlag::Yes);
+}
+
+TEST_F(APZEventResultTesterMock, ScrollableDirectionsWithEventHandler) {
+ CreateScrollableRootLayer();
+
+ ScrollableDirectionsWithEventHandlerTest(PreventDefaultFlag::No);
+}
+
+TEST_F(APZEventResultTesterMock,
+ ScrollableDirectionsWithPreventDefaultEventHandler) {
+ CreateScrollableRootLayer();
+
+ ScrollableDirectionsWithEventHandlerTest(PreventDefaultFlag::Yes);
+}
+
+// Test that APZEventResult::GetHandledResult() is correctly
+// populated.
+TEST_F(APZEventResultTesterMock, HandledByRootApzcFlag) {
+ // Create simple layer tree containing a dispatch-to-content region
+ // that covers part but not all of its area.
+ const char* treeShape = "x";
+ LayerIntRegion layerVisibleRegions[] = {
+ LayerIntRect(0, 0, 100, 100),
+ };
+ CreateScrollData(treeShape, layerVisibleRegions);
+ SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID,
+ CSSRect(0, 0, 100, 200));
+ ModifyFrameMetrics(root, [](ScrollMetadata& sm, FrameMetrics& metrics) {
+ metrics.SetIsRootContent(true);
+ });
+ // away from the scrolling container layer.
+ registration = MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc);
+ UpdateHitTestingTree();
+
+ // Tap the top half and check that we report that the event was
+ // handled by the root APZC.
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ APZEventResult result =
+ TouchDown(manager, ScreenIntPoint(50, 25), mcc->Time());
+ TouchUp(manager, ScreenIntPoint(50, 25), mcc->Time());
+ EXPECT_EQ(result.GetHandledResult(),
+ Some(APZHandledResult{APZHandledPlace::HandledByRoot,
+ SideBits::eBottom, EitherScrollDirection}));
+
+ // Tap the bottom half and check that we report that we're not
+ // sure whether the event was handled by the root APZC.
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID,
+ {CompositorHitTestFlags::eVisibleToHitTest,
+ CompositorHitTestFlags::eIrregularArea});
+ result = TouchDown(manager, ScreenIntPoint(50, 75), mcc->Time());
+ TouchUp(manager, ScreenIntPoint(50, 75), mcc->Time());
+ EXPECT_EQ(result.GetHandledResult(), Nothing());
+
+ // Register an input block callback that will tell us the
+ // delayed answer.
+ APZHandledResult delayedAnswer{APZHandledPlace::Invalid, SideBits::eNone,
+ ScrollDirections()};
+ manager->AddInputBlockCallback(
+ result.mInputBlockId,
+ {result.GetStatus(), [&](uint64_t id, const APZHandledResult& answer) {
+ EXPECT_EQ(id, result.mInputBlockId);
+ delayedAnswer = answer;
+ }});
+
+ // Send APZ the relevant notifications to allow it to process the
+ // input block.
+ manager->SetAllowedTouchBehavior(result.mInputBlockId,
+ {AllowedTouchBehavior::VERTICAL_PAN});
+ manager->SetTargetAPZC(result.mInputBlockId, {result.mTargetGuid});
+ manager->ContentReceivedInputBlock(result.mInputBlockId,
+ /*aPreventDefault=*/false);
+
+ // Check that we received the delayed answer and it is what we expect.
+ EXPECT_EQ(delayedAnswer,
+ (APZHandledResult{APZHandledPlace::HandledByRoot, SideBits::eBottom,
+ EitherScrollDirection}));
+
+ // Now repeat the tap on the bottom half, but simulate a prevent-default.
+ // This time, we expect a delayed answer of `HandledByContent`.
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID,
+ {CompositorHitTestFlags::eVisibleToHitTest,
+ CompositorHitTestFlags::eIrregularArea});
+ result = TouchDown(manager, ScreenIntPoint(50, 75), mcc->Time());
+ TouchUp(manager, ScreenIntPoint(50, 75), mcc->Time());
+ EXPECT_EQ(result.GetHandledResult(), Nothing());
+ manager->AddInputBlockCallback(
+ result.mInputBlockId,
+ {result.GetStatus(), [&](uint64_t id, const APZHandledResult& answer) {
+ EXPECT_EQ(id, result.mInputBlockId);
+ delayedAnswer = answer;
+ }});
+ manager->SetAllowedTouchBehavior(result.mInputBlockId,
+ {AllowedTouchBehavior::VERTICAL_PAN});
+ manager->SetTargetAPZC(result.mInputBlockId, {result.mTargetGuid});
+ manager->ContentReceivedInputBlock(result.mInputBlockId,
+ /*aPreventDefault=*/true);
+ EXPECT_EQ(delayedAnswer,
+ (APZHandledResult{APZHandledPlace::HandledByContent,
+ SideBits::eBottom, EitherScrollDirection}));
+
+ // Shrink the scrollable area, now it's no longer scrollable.
+ ModifyFrameMetrics(root, [](ScrollMetadata& sm, FrameMetrics& metrics) {
+ metrics.SetScrollableRect(CSSRect(0, 0, 100, 100));
+ });
+ UpdateHitTestingTree();
+ // Now repeat the tap on the bottom half with an event handler.
+ // This time, we expect a delayed answer of `Unhandled`.
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID,
+ {CompositorHitTestFlags::eVisibleToHitTest,
+ CompositorHitTestFlags::eIrregularArea});
+ result = TouchDown(manager, ScreenIntPoint(50, 75), mcc->Time());
+ TouchUp(manager, ScreenIntPoint(50, 75), mcc->Time());
+ EXPECT_EQ(result.GetHandledResult(), Nothing());
+ manager->AddInputBlockCallback(
+ result.mInputBlockId,
+ {result.GetStatus(), [&](uint64_t id, const APZHandledResult& answer) {
+ EXPECT_EQ(id, result.mInputBlockId);
+ delayedAnswer = answer;
+ }});
+ manager->SetAllowedTouchBehavior(result.mInputBlockId,
+ {AllowedTouchBehavior::VERTICAL_PAN});
+ manager->SetTargetAPZC(result.mInputBlockId, {result.mTargetGuid});
+ manager->ContentReceivedInputBlock(result.mInputBlockId,
+ /*aPreventDefault=*/false);
+ EXPECT_EQ(delayedAnswer,
+ (APZHandledResult{APZHandledPlace::Unhandled, SideBits::eNone,
+ ScrollDirections()}));
+
+ // Repeat the tap on the bottom half, with no event handler.
+ // Make sure we get an eager answer of `Unhandled`.
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ result = TouchDown(manager, ScreenIntPoint(50, 75), mcc->Time());
+ TouchUp(manager, ScreenIntPoint(50, 75), mcc->Time());
+ EXPECT_EQ(result.GetStatus(), nsEventStatus_eIgnore);
+ EXPECT_EQ(result.GetHandledResult(),
+ Some(APZHandledResult{APZHandledPlace::Unhandled, SideBits::eNone,
+ EitherScrollDirection}));
+}
diff --git a/gfx/layers/apz/test/gtest/TestFlingAcceleration.cpp b/gfx/layers/apz/test/gtest/TestFlingAcceleration.cpp
new file mode 100644
index 0000000000..986025bddc
--- /dev/null
+++ b/gfx/layers/apz/test/gtest/TestFlingAcceleration.cpp
@@ -0,0 +1,252 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include <initializer_list>
+#include "APZCTreeManagerTester.h"
+#include "APZTestCommon.h"
+#include "InputUtils.h"
+
+class APZCFlingAccelerationTester : public APZCTreeManagerTester {
+ protected:
+ void SetUp() {
+ APZCTreeManagerTester::SetUp();
+ const char* treeShape = "x";
+ LayerIntRegion layerVisibleRegion[] = {
+ LayerIntRect(0, 0, 800, 1000),
+ };
+ CreateScrollData(treeShape, layerVisibleRegion);
+ SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID,
+ CSSRect(0, 0, 800, 50000));
+ // Scroll somewhere into the middle of the scroll range, so that we have
+ // lots of space to scroll in both directions.
+ ModifyFrameMetrics(root, [](ScrollMetadata& aSm, FrameMetrics& aMetrics) {
+ aMetrics.SetVisualScrollUpdateType(
+ FrameMetrics::ScrollOffsetUpdateType::eMainThread);
+ aMetrics.SetVisualDestination(CSSPoint(0, 25000));
+ });
+
+ registration = MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc);
+ UpdateHitTestingTree();
+
+ apzc = ApzcOf(root);
+ }
+
+ void ExecutePanGesture100Hz(const ScreenIntPoint& aStartPoint,
+ std::initializer_list<int32_t> aYDeltas) {
+ APZEventResult result = TouchDown(apzc, aStartPoint, mcc->Time());
+
+ // Allowed touch behaviours must be set after sending touch-start.
+ if (result.GetStatus() != nsEventStatus_eConsumeNoDefault) {
+ SetDefaultAllowedTouchBehavior(apzc, result.mInputBlockId);
+ }
+
+ const TimeDuration kTouchTimeDelta100Hz =
+ TimeDuration::FromMilliseconds(10);
+
+ ScreenIntPoint currentLocation = aStartPoint;
+ for (int32_t delta : aYDeltas) {
+ mcc->AdvanceBy(kTouchTimeDelta100Hz);
+ if (delta != 0) {
+ currentLocation.y += delta;
+ Unused << TouchMove(apzc, currentLocation, mcc->Time());
+ }
+ }
+
+ Unused << TouchUp(apzc, currentLocation, mcc->Time());
+ }
+
+ void ExecuteWait(const TimeDuration& aDuration) {
+ TimeDuration remaining = aDuration;
+ const TimeDuration TIME_BETWEEN_FRAMES =
+ TimeDuration::FromSeconds(1) / int64_t(60);
+ while (remaining.ToMilliseconds() > 0) {
+ mcc->AdvanceBy(TIME_BETWEEN_FRAMES);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ remaining -= TIME_BETWEEN_FRAMES;
+ }
+ }
+
+ RefPtr<TestAsyncPanZoomController> apzc;
+ UniquePtr<ScopedLayerTreeRegistration> registration;
+};
+
+enum class UpOrDown : uint8_t { Up, Down };
+
+// This is a macro so that the assertions print useful line numbers.
+#define CHECK_VELOCITY(aUpOrDown, aLowerBound, aUpperBound) \
+ do { \
+ auto vel = apzc->GetVelocityVector(); \
+ if (UpOrDown::aUpOrDown == UpOrDown::Up) { \
+ EXPECT_LT(vel.y, 0.0); \
+ } else { \
+ EXPECT_GT(vel.y, 0.0); \
+ } \
+ EXPECT_GE(vel.Length(), aLowerBound); \
+ EXPECT_LE(vel.Length(), aUpperBound); \
+ } while (0)
+
+// These tests have the following pattern: Two flings are executed, with a bit
+// of wait time in between. The deltas in each pan gesture have been captured
+// from a real phone, from touch events triggered by real fingers.
+// We check the velocity at the end to detect whether the fling was accelerated
+// or not. As an additional safety precaution, we also check the velocities for
+// the first fling, so that changes in behavior are easier to analyze.
+// One added challenge of this test is the fact that it has to work with on
+// multiple platforms, and we use different velocity estimation strategies and
+// different fling physics depending on the platform.
+// The upper and lower bounds for the velocities were chosen in such a way that
+// the test passes on all platforms. At the time of writing, we usually end up
+// with higher velocities on Android than on Desktop, so the observed velocities
+// on Android became the upper bounds and the observed velocities on Desktop
+// becaume the lower bounds, each rounded out to a multiple of 0.1.
+
+TEST_F(APZCFlingAccelerationTester, TwoNormalFlingsShouldAccelerate) {
+ ExecutePanGesture100Hz(ScreenIntPoint{665, 1244},
+ {0, 0, -21, -44, -52, -55, -53, -49, -46, -47});
+ CHECK_VELOCITY(Down, 4.5, 6.8);
+
+ ExecuteWait(TimeDuration::FromMilliseconds(375));
+ CHECK_VELOCITY(Down, 2.2, 5.1);
+
+ ExecutePanGesture100Hz(ScreenIntPoint{623, 1211},
+ {-6, -51, -55, 0, -53, -57, -60, -60, -56});
+ CHECK_VELOCITY(Down, 9.0, 14.0);
+}
+
+TEST_F(APZCFlingAccelerationTester, TwoFastFlingsShouldAccelerate) {
+ ExecutePanGesture100Hz(ScreenIntPoint{764, 714},
+ {9, 30, 49, 60, 64, 64, 62, 59, 51});
+ CHECK_VELOCITY(Up, 5.0, 7.5);
+
+ ExecuteWait(TimeDuration::FromMilliseconds(447));
+ CHECK_VELOCITY(Up, 2.3, 5.2);
+
+ ExecutePanGesture100Hz(ScreenIntPoint{743, 739},
+ {7, 0, 38, 66, 75, 146, 0, 119});
+ CHECK_VELOCITY(Up, 13.0, 20.0);
+}
+
+TEST_F(APZCFlingAccelerationTester,
+ FlingsInOppositeDirectionShouldNotAccelerate) {
+ ExecutePanGesture100Hz(ScreenIntPoint{728, 1381},
+ {0, 0, 0, -12, -24, -32, -43, -46, 0});
+ CHECK_VELOCITY(Down, 2.9, 5.3);
+
+ ExecuteWait(TimeDuration::FromMilliseconds(153));
+ CHECK_VELOCITY(Down, 2.1, 4.8);
+
+ ExecutePanGesture100Hz(ScreenIntPoint{698, 1059},
+ {0, 0, 14, 61, 41, 0, 45, 35});
+ CHECK_VELOCITY(Up, 3.2, 4.3);
+}
+
+TEST_F(APZCFlingAccelerationTester,
+ ShouldNotAccelerateWhenPreviousFlingHasSlowedDown) {
+ ExecutePanGesture100Hz(ScreenIntPoint{748, 1046},
+ {0, 9, 15, 23, 31, 30, 0, 34, 31, 29, 28, 24, 24, 11});
+ CHECK_VELOCITY(Up, 2.2, 3.0);
+ ExecuteWait(TimeDuration::FromMilliseconds(498));
+ CHECK_VELOCITY(Up, 0.5, 1.0);
+ ExecutePanGesture100Hz(ScreenIntPoint{745, 1056},
+ {0, 10, 17, 29, 29, 33, 33, 0, 31, 27, 13});
+ CHECK_VELOCITY(Up, 1.8, 2.7);
+}
+
+TEST_F(APZCFlingAccelerationTester, ShouldNotAccelerateWhenPausedAtStartOfPan) {
+ ExecutePanGesture100Hz(
+ ScreenIntPoint{711, 1468},
+ {0, 0, 0, 0, -8, 0, -18, -32, -50, -57, -66, -68, -63, -60});
+ CHECK_VELOCITY(Down, 6.2, 8.6);
+
+ ExecuteWait(TimeDuration::FromMilliseconds(285));
+ CHECK_VELOCITY(Down, 3.4, 7.4);
+
+ ExecutePanGesture100Hz(
+ ScreenIntPoint{658, 1352},
+ {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, -8, -18, -34, -53, -70, -75, -75, -64});
+ CHECK_VELOCITY(Down, 6.7, 9.1);
+}
+
+TEST_F(APZCFlingAccelerationTester, ShouldNotAccelerateWhenPausedDuringPan) {
+ ExecutePanGesture100Hz(
+ ScreenIntPoint{732, 1423},
+ {0, 0, 0, -5, 0, -15, -41, -71, -90, -93, -85, -64, -44});
+ CHECK_VELOCITY(Down, 7.5, 10.1);
+
+ ExecuteWait(TimeDuration::FromMilliseconds(204));
+ CHECK_VELOCITY(Down, 4.8, 9.4);
+
+ ExecutePanGesture100Hz(
+ ScreenIntPoint{651, 1372},
+ {0, 0, 0, -6, 0, -16, -26, -41, -49, -65, -66, -61, -50, -35, -24,
+ -17, -11, -8, -6, -5, -4, -3, -2, -2, -2, -2, -2, -2, -2, -2,
+ -3, -4, -5, -7, -9, -10, -10, -12, -18, -25, -23, -28, -30, -24});
+ CHECK_VELOCITY(Down, 2.5, 3.4);
+}
+
+TEST_F(APZCFlingAccelerationTester,
+ ShouldNotAccelerateWhenOppositeDirectionDuringPan) {
+ ExecutePanGesture100Hz(ScreenIntPoint{663, 1371},
+ {0, 0, 0, -5, -18, -31, -49, -56, -61, -54, -55});
+ CHECK_VELOCITY(Down, 5.4, 7.1);
+
+ ExecuteWait(TimeDuration::FromMilliseconds(255));
+ CHECK_VELOCITY(Down, 3.1, 6.0);
+
+ ExecutePanGesture100Hz(
+ ScreenIntPoint{726, 930},
+ {0, 0, 0, 0, 30, 0, 19, 24, 32, 30, 37, 33,
+ 33, 32, 25, 23, 23, 18, 13, 9, 5, 3, 1, 0,
+ -7, -19, -38, -53, -68, -79, -85, -73, -64, -54});
+ CHECK_VELOCITY(Down, 7.0, 10.0);
+}
+
+TEST_F(APZCFlingAccelerationTester,
+ ShouldAccelerateAfterLongWaitIfVelocityStillHigh) {
+ // Reduce friction with the "Desktop" fling physics a little, so that it
+ // behaves more similarly to the Android fling physics, and has enough
+ // velocity after the wait time to allow for acceleration.
+ SCOPED_GFX_PREF_FLOAT("apz.fling_friction", 0.0012);
+
+ ExecutePanGesture100Hz(ScreenIntPoint{739, 1424},
+ {0, 0, -5, -10, -20, 0, -110, -86, 0, -102, -105});
+ CHECK_VELOCITY(Down, 6.3, 9.4);
+
+ ExecuteWait(TimeDuration::FromMilliseconds(1117));
+ CHECK_VELOCITY(Down, 1.6, 3.3);
+
+ ExecutePanGesture100Hz(ScreenIntPoint{726, 1380},
+ {0, -8, 0, -30, -60, -87, -104, -111});
+ CHECK_VELOCITY(Down, 13.0, 23.0);
+}
+
+TEST_F(APZCFlingAccelerationTester, ShouldNotAccelerateAfterCanceledWithTap) {
+ // First, build up a lot of speed.
+ ExecutePanGesture100Hz(ScreenIntPoint{569, 710},
+ {11, 2, 107, 18, 148, 57, 133, 159, 21});
+ ExecuteWait(TimeDuration::FromMilliseconds(154));
+ ExecutePanGesture100Hz(ScreenIntPoint{581, 650},
+ {12, 68, 0, 162, 78, 140, 167});
+ ExecuteWait(TimeDuration::FromMilliseconds(123));
+ ExecutePanGesture100Hz(ScreenIntPoint{568, 723}, {11, 0, 79, 91, 131, 171});
+ ExecuteWait(TimeDuration::FromMilliseconds(123));
+ ExecutePanGesture100Hz(ScreenIntPoint{598, 678},
+ {8, 55, 22, 87, 117, 220, 54});
+ ExecuteWait(TimeDuration::FromMilliseconds(134));
+ ExecutePanGesture100Hz(ScreenIntPoint{585, 854}, {45, 137, 107, 102, 79});
+ ExecuteWait(TimeDuration::FromMilliseconds(246));
+
+ // Then, interrupt with a tap.
+ ExecutePanGesture100Hz(ScreenIntPoint{566, 812}, {0, 0, 0, 0});
+ ExecuteWait(TimeDuration::FromMilliseconds(869));
+
+ // Then do a regular fling.
+ ExecutePanGesture100Hz(ScreenIntPoint{599, 819},
+ {0, 0, 8, 35, 8, 38, 29, 37});
+
+ CHECK_VELOCITY(Up, 2.8, 4.2);
+}
diff --git a/gfx/layers/apz/test/gtest/TestGestureDetector.cpp b/gfx/layers/apz/test/gtest/TestGestureDetector.cpp
new file mode 100644
index 0000000000..ad5f379ba8
--- /dev/null
+++ b/gfx/layers/apz/test/gtest/TestGestureDetector.cpp
@@ -0,0 +1,849 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "APZCBasicTester.h"
+#include "APZTestCommon.h"
+#include "InputUtils.h"
+#include "mozilla/StaticPrefs_apz.h"
+
+// Note: There are additional tests that test gesture detection behaviour
+// with multiple APZCs in TestTreeManager.cpp.
+
+class APZCGestureDetectorTester : public APZCBasicTester {
+ public:
+ APZCGestureDetectorTester()
+ : APZCBasicTester(AsyncPanZoomController::USE_GESTURE_DETECTOR) {}
+
+ protected:
+ FrameMetrics GetPinchableFrameMetrics() {
+ FrameMetrics fm;
+ fm.SetCompositionBounds(ParentLayerRect(200, 200, 100, 200));
+ fm.SetScrollableRect(CSSRect(0, 0, 980, 1000));
+ fm.SetVisualScrollOffset(CSSPoint(300, 300));
+ fm.SetZoom(CSSToParentLayerScale(2.0));
+ // APZC only allows zooming on the root scrollable frame.
+ fm.SetIsRootContent(true);
+ // the visible area of the document in CSS pixels is x=300 y=300 w=50 h=100
+ return fm;
+ }
+};
+
+#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android
+TEST_F(APZCGestureDetectorTester, Pan_After_Pinch) {
+ SCOPED_GFX_PREF_INT("apz.axis_lock.mode", 2);
+ SCOPED_GFX_PREF_FLOAT("apz.axis_lock.lock_angle", M_PI / 6.0f);
+ SCOPED_GFX_PREF_FLOAT("apz.axis_lock.breakout_angle", M_PI / 8.0f);
+
+ FrameMetrics originalMetrics = GetPinchableFrameMetrics();
+ apzc->SetFrameMetrics(originalMetrics);
+
+ MakeApzcZoomable();
+
+ // Test parameters
+ float zoomAmount = 1.25;
+ float pinchLength = 100.0;
+ float pinchLengthScaled = pinchLength * zoomAmount;
+ int focusX = 250;
+ int focusY = 300;
+ int panDistance = 20;
+ const TimeDuration TIME_BETWEEN_TOUCH_EVENT =
+ TimeDuration::FromMilliseconds(50);
+
+ int firstFingerId = 0;
+ int secondFingerId = firstFingerId + 1;
+
+ // Put fingers down
+ MultiTouchInput mti =
+ MultiTouchInput(MultiTouchInput::MULTITOUCH_START, 0, mcc->Time(), 0);
+ mti.mTouches.AppendElement(
+ CreateSingleTouchData(firstFingerId, focusX, focusY));
+ mti.mTouches.AppendElement(
+ CreateSingleTouchData(secondFingerId, focusX, focusY));
+ apzc->ReceiveInputEvent(mti, Some(nsTArray<uint32_t>{kDefaultTouchBehavior}));
+ mcc->AdvanceBy(TIME_BETWEEN_TOUCH_EVENT);
+
+ // Spread fingers out to enter the pinch state
+ mti = MultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, 0, mcc->Time(), 0);
+ mti.mTouches.AppendElement(
+ CreateSingleTouchData(firstFingerId, focusX - pinchLength, focusY));
+ mti.mTouches.AppendElement(
+ CreateSingleTouchData(secondFingerId, focusX + pinchLength, focusY));
+ apzc->ReceiveInputEvent(mti);
+ mcc->AdvanceBy(TIME_BETWEEN_TOUCH_EVENT);
+
+ // Do the actual pinch of 1.25x
+ mti = MultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, 0, mcc->Time(), 0);
+ mti.mTouches.AppendElement(
+ CreateSingleTouchData(firstFingerId, focusX - pinchLengthScaled, focusY));
+ mti.mTouches.AppendElement(CreateSingleTouchData(
+ secondFingerId, focusX + pinchLengthScaled, focusY));
+ apzc->ReceiveInputEvent(mti);
+ mcc->AdvanceBy(TIME_BETWEEN_TOUCH_EVENT);
+
+ // Verify that the zoom changed, just to make sure our code above did what it
+ // was supposed to.
+ FrameMetrics zoomedMetrics = apzc->GetFrameMetrics();
+ float newZoom = zoomedMetrics.GetZoom().scale;
+ EXPECT_EQ(originalMetrics.GetZoom().scale * zoomAmount, newZoom);
+
+ // Now we lift one finger...
+ mti = MultiTouchInput(MultiTouchInput::MULTITOUCH_END, 0, mcc->Time(), 0);
+ mti.mTouches.AppendElement(CreateSingleTouchData(
+ secondFingerId, focusX + pinchLengthScaled, focusY));
+ apzc->ReceiveInputEvent(mti);
+ mcc->AdvanceBy(TIME_BETWEEN_TOUCH_EVENT);
+
+ // ... and pan with the remaining finger. This pan just breaks through the
+ // distance threshold.
+ focusY += StaticPrefs::apz_touch_start_tolerance() * tm->GetDPI();
+ mti = MultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, 0, mcc->Time(), 0);
+ mti.mTouches.AppendElement(
+ CreateSingleTouchData(firstFingerId, focusX - pinchLengthScaled, focusY));
+ apzc->ReceiveInputEvent(mti);
+ mcc->AdvanceBy(TIME_BETWEEN_TOUCH_EVENT);
+
+ // This one does an actual pan of 20 pixels
+ focusY += panDistance;
+ mti = MultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, 0, mcc->Time(), 0);
+ mti.mTouches.AppendElement(
+ CreateSingleTouchData(firstFingerId, focusX - pinchLengthScaled, focusY));
+ apzc->ReceiveInputEvent(mti);
+ mcc->AdvanceBy(TIME_BETWEEN_TOUCH_EVENT);
+
+ // Lift the remaining finger
+ mti = MultiTouchInput(MultiTouchInput::MULTITOUCH_END, 0, mcc->Time(), 0);
+ mti.mTouches.AppendElement(
+ CreateSingleTouchData(firstFingerId, focusX - pinchLengthScaled, focusY));
+ apzc->ReceiveInputEvent(mti);
+ mcc->AdvanceBy(TIME_BETWEEN_TOUCH_EVENT);
+
+ // Verify that we scrolled
+ FrameMetrics finalMetrics = apzc->GetFrameMetrics();
+ EXPECT_EQ(zoomedMetrics.GetVisualScrollOffset().y - (panDistance / newZoom),
+ finalMetrics.GetVisualScrollOffset().y);
+
+ // Clear out any remaining fling animation and pending tasks
+ apzc->AdvanceAnimationsUntilEnd();
+ while (mcc->RunThroughDelayedTasks())
+ ;
+ apzc->AssertStateIsReset();
+}
+#endif
+
+TEST_F(APZCGestureDetectorTester, Pan_With_Tap) {
+ SCOPED_GFX_PREF_FLOAT("apz.touch_start_tolerance", 0.1);
+
+ FrameMetrics originalMetrics = GetPinchableFrameMetrics();
+ apzc->SetFrameMetrics(originalMetrics);
+
+ // Making the APZC zoomable isn't really needed for the correct operation of
+ // this test, but it could help catch regressions where we accidentally enter
+ // a pinch state.
+ MakeApzcZoomable();
+
+ // Test parameters
+ int touchX = 250;
+ int touchY = 300;
+ int panDistance = 20;
+
+ int firstFingerId = 0;
+ int secondFingerId = firstFingerId + 1;
+
+ const float panThreshold =
+ StaticPrefs::apz_touch_start_tolerance() * tm->GetDPI();
+
+ // Put finger down
+ MultiTouchInput mti =
+ CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time());
+ mti.mTouches.AppendElement(
+ CreateSingleTouchData(firstFingerId, touchX, touchY));
+ apzc->ReceiveInputEvent(mti, Some(nsTArray<uint32_t>{kDefaultTouchBehavior}));
+
+ // Start a pan, break through the threshold
+ touchY += panThreshold;
+ mti = CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, mcc->Time());
+ mti.mTouches.AppendElement(
+ CreateSingleTouchData(firstFingerId, touchX, touchY));
+ apzc->ReceiveInputEvent(mti);
+
+ // Do an actual pan for a bit
+ touchY += panDistance;
+ mti = CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, mcc->Time());
+ mti.mTouches.AppendElement(
+ CreateSingleTouchData(firstFingerId, touchX, touchY));
+ apzc->ReceiveInputEvent(mti);
+
+ // Put a second finger down
+ mti = CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time());
+ mti.mTouches.AppendElement(
+ CreateSingleTouchData(firstFingerId, touchX, touchY));
+ mti.mTouches.AppendElement(
+ CreateSingleTouchData(secondFingerId, touchX + 10, touchY));
+ apzc->ReceiveInputEvent(mti, Some(nsTArray<uint32_t>{kDefaultTouchBehavior}));
+
+ // Lift the second finger
+ mti = CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_END, mcc->Time());
+ mti.mTouches.AppendElement(
+ CreateSingleTouchData(secondFingerId, touchX + 10, touchY));
+ apzc->ReceiveInputEvent(mti);
+
+ // Bust through the threshold again
+ touchY += panThreshold;
+ mti = CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, mcc->Time());
+ mti.mTouches.AppendElement(
+ CreateSingleTouchData(firstFingerId, touchX, touchY));
+ apzc->ReceiveInputEvent(mti);
+
+ // Do some more actual panning
+ touchY += panDistance;
+ mti = CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, mcc->Time());
+ mti.mTouches.AppendElement(
+ CreateSingleTouchData(firstFingerId, touchX, touchY));
+ apzc->ReceiveInputEvent(mti);
+
+ // Lift the first finger
+ mti = CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_END, mcc->Time());
+ mti.mTouches.AppendElement(
+ CreateSingleTouchData(firstFingerId, touchX, touchY));
+ apzc->ReceiveInputEvent(mti);
+
+ // Verify that we scrolled
+ FrameMetrics finalMetrics = apzc->GetFrameMetrics();
+ float zoom = finalMetrics.GetZoom().scale;
+ EXPECT_EQ(
+ originalMetrics.GetVisualScrollOffset().y - (panDistance * 2 / zoom),
+ finalMetrics.GetVisualScrollOffset().y);
+
+ // Clear out any remaining fling animation and pending tasks
+ apzc->AdvanceAnimationsUntilEnd();
+ while (mcc->RunThroughDelayedTasks())
+ ;
+ apzc->AssertStateIsReset();
+}
+
+TEST_F(APZCGestureDetectorTester, SecondTapIsFar_Bug1586496) {
+ // Test that we receive two single-tap events when two tap gestures are
+ // close in time but far in distance.
+ EXPECT_CALL(*mcc, HandleTap(TapType::eSingleTap, _, 0, apzc->GetGuid(), _))
+ .Times(2);
+
+ TimeDuration brief =
+ TimeDuration::FromMilliseconds(StaticPrefs::apz_max_tap_time() / 10.0);
+
+ ScreenIntPoint point(10, 10);
+ Tap(apzc, point, brief);
+
+ mcc->AdvanceBy(brief);
+
+ point.x += apzc->GetSecondTapTolerance() * 2;
+ point.y += apzc->GetSecondTapTolerance() * 2;
+
+ Tap(apzc, point, brief);
+}
+
+class APZCFlingStopTester : public APZCGestureDetectorTester {
+ protected:
+ // Start a fling, and then tap while the fling is ongoing. When
+ // aSlow is false, the tap will happen while the fling is at a
+ // high velocity, and we check that the tap doesn't trigger sending a tap
+ // to content. If aSlow is true, the tap will happen while the fling
+ // is at a slow velocity, and we check that the tap does trigger sending
+ // a tap to content. See bug 1022956.
+ void DoFlingStopTest(bool aSlow) {
+ int touchStart = 50;
+ int touchEnd = 10;
+
+ // Start the fling down.
+ Pan(apzc, touchStart, touchEnd);
+ // The touchstart from the pan will leave some cancelled tasks in the queue,
+ // clear them out
+
+ // If we want to tap while the fling is fast, let the fling advance for 10ms
+ // only. If we want the fling to slow down more, advance to 2000ms. These
+ // numbers may need adjusting if our friction and threshold values change,
+ // but they should be deterministic at least.
+ int timeDelta = aSlow ? 2000 : 10;
+ int tapCallsExpected = aSlow ? 2 : 1;
+
+ // Advance the fling animation by timeDelta milliseconds.
+ ParentLayerPoint pointOut;
+ AsyncTransform viewTransformOut;
+ apzc->SampleContentTransformForFrame(
+ &viewTransformOut, pointOut, TimeDuration::FromMilliseconds(timeDelta));
+
+ // Deliver a tap to abort the fling. Ensure that we get a SingleTap
+ // call out of it if and only if the fling is slow.
+ EXPECT_CALL(*mcc, HandleTap(TapType::eSingleTap, _, 0, apzc->GetGuid(), _))
+ .Times(tapCallsExpected);
+ Tap(apzc, ScreenIntPoint(10, 10), 0);
+ while (mcc->RunThroughDelayedTasks())
+ ;
+
+ // Deliver another tap, to make sure that taps are flowing properly once
+ // the fling is aborted.
+ Tap(apzc, ScreenIntPoint(100, 100), 0);
+ while (mcc->RunThroughDelayedTasks())
+ ;
+
+ // Verify that we didn't advance any further after the fling was aborted, in
+ // either case.
+ ParentLayerPoint finalPointOut;
+ apzc->SampleContentTransformForFrame(&viewTransformOut, finalPointOut);
+ EXPECT_EQ(pointOut.x, finalPointOut.x);
+ EXPECT_EQ(pointOut.y, finalPointOut.y);
+
+ apzc->AssertStateIsReset();
+ }
+
+ void DoFlingStopWithSlowListener(bool aPreventDefault) {
+ MakeApzcWaitForMainThread();
+
+ int touchStart = 50;
+ int touchEnd = 10;
+ uint64_t blockId = 0;
+
+ // Start the fling down.
+ Pan(apzc, touchStart, touchEnd, PanOptions::None, nullptr, nullptr,
+ &blockId);
+ apzc->ConfirmTarget(blockId);
+ apzc->ContentReceivedInputBlock(blockId, false);
+
+ // Sample the fling a couple of times to ensure it's going.
+ ParentLayerPoint point, finalPoint;
+ AsyncTransform viewTransform;
+ apzc->SampleContentTransformForFrame(&viewTransform, point,
+ TimeDuration::FromMilliseconds(10));
+ apzc->SampleContentTransformForFrame(&viewTransform, finalPoint,
+ TimeDuration::FromMilliseconds(10));
+ EXPECT_GT(finalPoint.y, point.y);
+
+ // Now we put our finger down to stop the fling
+ blockId =
+ TouchDown(apzc, ScreenIntPoint(10, 10), mcc->Time()).mInputBlockId;
+
+ // Re-sample to make sure it hasn't moved
+ apzc->SampleContentTransformForFrame(&viewTransform, point,
+ TimeDuration::FromMilliseconds(10));
+ EXPECT_EQ(finalPoint.x, point.x);
+ EXPECT_EQ(finalPoint.y, point.y);
+
+ // respond to the touchdown that stopped the fling.
+ // even if we do a prevent-default on it, the animation should remain
+ // stopped.
+ apzc->ContentReceivedInputBlock(blockId, aPreventDefault);
+
+ // Verify the page hasn't moved
+ apzc->SampleContentTransformForFrame(&viewTransform, point,
+ TimeDuration::FromMilliseconds(70));
+ EXPECT_EQ(finalPoint.x, point.x);
+ EXPECT_EQ(finalPoint.y, point.y);
+
+ // clean up
+ TouchUp(apzc, ScreenIntPoint(10, 10), mcc->Time());
+
+ apzc->AssertStateIsReset();
+ }
+};
+
+TEST_F(APZCFlingStopTester, FlingStop) {
+ SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0f);
+ DoFlingStopTest(false);
+}
+
+TEST_F(APZCFlingStopTester, FlingStopTap) {
+ SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0f);
+ DoFlingStopTest(true);
+}
+
+TEST_F(APZCFlingStopTester, FlingStopSlowListener) {
+ SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0f);
+ DoFlingStopWithSlowListener(false);
+}
+
+TEST_F(APZCFlingStopTester, FlingStopPreventDefault) {
+ SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0f);
+ DoFlingStopWithSlowListener(true);
+}
+
+TEST_F(APZCGestureDetectorTester, ShortPress) {
+ MakeApzcUnzoomable();
+
+ MockFunction<void(std::string checkPointName)> check;
+ {
+ InSequence s;
+ // This verifies that the single tap notification is sent after the
+ // touchup is fully processed. The ordering here is important.
+ EXPECT_CALL(check, Call("pre-tap"));
+ EXPECT_CALL(check, Call("post-tap"));
+ EXPECT_CALL(*mcc, HandleTap(TapType::eSingleTap, LayoutDevicePoint(10, 10),
+ 0, apzc->GetGuid(), _))
+ .Times(1);
+ }
+
+ check.Call("pre-tap");
+ TapAndCheckStatus(apzc, ScreenIntPoint(10, 10),
+ TimeDuration::FromMilliseconds(100));
+ check.Call("post-tap");
+
+ apzc->AssertStateIsReset();
+}
+
+TEST_F(APZCGestureDetectorTester, MediumPress) {
+ MakeApzcUnzoomable();
+
+ MockFunction<void(std::string checkPointName)> check;
+ {
+ InSequence s;
+ // This verifies that the single tap notification is sent after the
+ // touchup is fully processed. The ordering here is important.
+ EXPECT_CALL(check, Call("pre-tap"));
+ EXPECT_CALL(check, Call("post-tap"));
+ EXPECT_CALL(*mcc, HandleTap(TapType::eSingleTap, LayoutDevicePoint(10, 10),
+ 0, apzc->GetGuid(), _))
+ .Times(1);
+ }
+
+ check.Call("pre-tap");
+ TapAndCheckStatus(apzc, ScreenIntPoint(10, 10),
+ TimeDuration::FromMilliseconds(400));
+ check.Call("post-tap");
+
+ apzc->AssertStateIsReset();
+}
+
+class APZCLongPressTester : public APZCGestureDetectorTester {
+ protected:
+ void DoLongPressTest(uint32_t aBehavior) {
+ MakeApzcUnzoomable();
+
+ APZEventResult result =
+ TouchDown(apzc, ScreenIntPoint(10, 10), mcc->Time());
+ EXPECT_EQ(nsEventStatus_eConsumeDoDefault, result.GetStatus());
+ uint64_t blockId = result.mInputBlockId;
+
+ if (result.GetStatus() != nsEventStatus_eConsumeNoDefault) {
+ // SetAllowedTouchBehavior() must be called after sending touch-start.
+ nsTArray<uint32_t> allowedTouchBehaviors;
+ allowedTouchBehaviors.AppendElement(aBehavior);
+ apzc->SetAllowedTouchBehavior(blockId, allowedTouchBehaviors);
+ }
+ // Have content "respond" to the touchstart
+ apzc->ContentReceivedInputBlock(blockId, false);
+
+ MockFunction<void(std::string checkPointName)> check;
+
+ {
+ InSequence s;
+
+ EXPECT_CALL(check, Call("preHandleLongTap"));
+ blockId++;
+ EXPECT_CALL(*mcc, HandleTap(TapType::eLongTap, LayoutDevicePoint(10, 10),
+ 0, apzc->GetGuid(), blockId))
+ .Times(1);
+ EXPECT_CALL(check, Call("postHandleLongTap"));
+
+ EXPECT_CALL(check, Call("preHandleLongTapUp"));
+ EXPECT_CALL(*mcc,
+ HandleTap(TapType::eLongTapUp, LayoutDevicePoint(10, 10), 0,
+ apzc->GetGuid(), _))
+ .Times(1);
+ EXPECT_CALL(check, Call("postHandleLongTapUp"));
+ }
+
+ // Manually invoke the longpress while the touch is currently down.
+ check.Call("preHandleLongTap");
+ mcc->RunThroughDelayedTasks();
+ check.Call("postHandleLongTap");
+
+ // Dispatching the longpress event starts a new touch block, which
+ // needs a new content response and also has a pending timeout task
+ // in the queue. Deal with those here. We do the content response first
+ // with preventDefault=false, and then we run the timeout task which
+ // "loses the race" and does nothing.
+ apzc->ContentReceivedInputBlock(blockId, false);
+ mcc->AdvanceByMillis(1000);
+
+ // Finally, simulate lifting the finger. Since the long-press wasn't
+ // prevent-defaulted, we should get a long-tap-up event.
+ check.Call("preHandleLongTapUp");
+ result = TouchUp(apzc, ScreenIntPoint(10, 10), mcc->Time());
+ mcc->RunThroughDelayedTasks();
+ EXPECT_EQ(nsEventStatus_eConsumeDoDefault, result.GetStatus());
+ check.Call("postHandleLongTapUp");
+
+ apzc->AssertStateIsReset();
+ }
+
+ void DoLongPressPreventDefaultTest(uint32_t aBehavior) {
+ MakeApzcUnzoomable();
+
+ EXPECT_CALL(*mcc, RequestContentRepaint(_)).Times(0);
+
+ int touchX = 10, touchStartY = 10, touchEndY = 50;
+
+ APZEventResult result =
+ TouchDown(apzc, ScreenIntPoint(touchX, touchStartY), mcc->Time());
+ EXPECT_EQ(nsEventStatus_eConsumeDoDefault, result.GetStatus());
+ uint64_t blockId = result.mInputBlockId;
+
+ if (result.GetStatus() != nsEventStatus_eConsumeNoDefault) {
+ // SetAllowedTouchBehavior() must be called after sending touch-start.
+ nsTArray<uint32_t> allowedTouchBehaviors;
+ allowedTouchBehaviors.AppendElement(aBehavior);
+ apzc->SetAllowedTouchBehavior(blockId, allowedTouchBehaviors);
+ }
+ // Have content "respond" to the touchstart
+ apzc->ContentReceivedInputBlock(blockId, false);
+
+ MockFunction<void(std::string checkPointName)> check;
+
+ {
+ InSequence s;
+
+ EXPECT_CALL(check, Call("preHandleLongTap"));
+ blockId++;
+ EXPECT_CALL(*mcc, HandleTap(TapType::eLongTap,
+ LayoutDevicePoint(touchX, touchStartY), 0,
+ apzc->GetGuid(), blockId))
+ .Times(1);
+ EXPECT_CALL(check, Call("postHandleLongTap"));
+ }
+
+ // Manually invoke the longpress while the touch is currently down.
+ check.Call("preHandleLongTap");
+ mcc->RunThroughDelayedTasks();
+ check.Call("postHandleLongTap");
+
+ // There should be a TimeoutContentResponse task in the queue still,
+ // waiting for the response from the longtap event dispatched above.
+ // Send the signal that content has handled the long-tap, and then run
+ // the timeout task (it will be a no-op because the content "wins" the
+ // race. This takes the place of the "contextmenu" event.
+ apzc->ContentReceivedInputBlock(blockId, true);
+ mcc->AdvanceByMillis(1000);
+
+ MultiTouchInput mti =
+ CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, mcc->Time());
+ mti.mTouches.AppendElement(SingleTouchData(
+ 0, ParentLayerPoint(touchX, touchEndY), ScreenSize(0, 0), 0, 0));
+ result = apzc->ReceiveInputEvent(mti);
+ EXPECT_EQ(nsEventStatus_eConsumeDoDefault, result.GetStatus());
+
+ EXPECT_CALL(*mcc, HandleTap(TapType::eSingleTap,
+ LayoutDevicePoint(touchX, touchEndY), 0,
+ apzc->GetGuid(), _))
+ .Times(0);
+ result = TouchUp(apzc, ScreenIntPoint(touchX, touchEndY), mcc->Time());
+ EXPECT_EQ(nsEventStatus_eConsumeDoDefault, result.GetStatus());
+
+ ParentLayerPoint pointOut;
+ AsyncTransform viewTransformOut;
+ apzc->SampleContentTransformForFrame(&viewTransformOut, pointOut);
+
+ EXPECT_EQ(ParentLayerPoint(), pointOut);
+ EXPECT_EQ(AsyncTransform(), viewTransformOut);
+
+ apzc->AssertStateIsReset();
+ }
+};
+
+TEST_F(APZCLongPressTester, LongPress) {
+ DoLongPressTest(kDefaultTouchBehavior);
+}
+
+TEST_F(APZCLongPressTester, LongPressPreventDefault) {
+ DoLongPressPreventDefaultTest(kDefaultTouchBehavior);
+}
+
+TEST_F(APZCGestureDetectorTester, DoubleTap) {
+ MakeApzcWaitForMainThread();
+ MakeApzcZoomable();
+
+ apzc->GetFrameMetrics().SetIsRootContent(true);
+
+ EXPECT_CALL(*mcc, HandleTap(TapType::eSingleTap, LayoutDevicePoint(10, 10), 0,
+ apzc->GetGuid(), _))
+ .Times(0);
+ EXPECT_CALL(*mcc, HandleTap(TapType::eDoubleTap, LayoutDevicePoint(10, 10), 0,
+ apzc->GetGuid(), _))
+ .Times(1);
+
+ uint64_t blockIds[2];
+ DoubleTapAndCheckStatus(apzc, ScreenIntPoint(10, 10), &blockIds);
+
+ // responses to the two touchstarts
+ apzc->ContentReceivedInputBlock(blockIds[0], false);
+ apzc->ContentReceivedInputBlock(blockIds[1], false);
+
+ apzc->AssertStateIsReset();
+}
+
+TEST_F(APZCGestureDetectorTester, DoubleTapNotZoomable) {
+ MakeApzcWaitForMainThread();
+ MakeApzcUnzoomable();
+
+ EXPECT_CALL(*mcc, HandleTap(TapType::eSingleTap, LayoutDevicePoint(10, 10), 0,
+ apzc->GetGuid(), _))
+ .Times(1);
+ EXPECT_CALL(*mcc, HandleTap(TapType::eSecondTap, LayoutDevicePoint(10, 10), 0,
+ apzc->GetGuid(), _))
+ .Times(1);
+ EXPECT_CALL(*mcc, HandleTap(TapType::eDoubleTap, LayoutDevicePoint(10, 10), 0,
+ apzc->GetGuid(), _))
+ .Times(0);
+
+ uint64_t blockIds[2];
+ DoubleTapAndCheckStatus(apzc, ScreenIntPoint(10, 10), &blockIds);
+
+ // responses to the two touchstarts
+ apzc->ContentReceivedInputBlock(blockIds[0], false);
+ apzc->ContentReceivedInputBlock(blockIds[1], false);
+
+ apzc->AssertStateIsReset();
+}
+
+TEST_F(APZCGestureDetectorTester, DoubleTapPreventDefaultFirstOnly) {
+ MakeApzcWaitForMainThread();
+ MakeApzcZoomable();
+
+ EXPECT_CALL(*mcc, HandleTap(TapType::eSingleTap, LayoutDevicePoint(10, 10), 0,
+ apzc->GetGuid(), _))
+ .Times(1);
+ EXPECT_CALL(*mcc, HandleTap(TapType::eDoubleTap, LayoutDevicePoint(10, 10), 0,
+ apzc->GetGuid(), _))
+ .Times(0);
+
+ uint64_t blockIds[2];
+ DoubleTapAndCheckStatus(apzc, ScreenIntPoint(10, 10), &blockIds);
+
+ // responses to the two touchstarts
+ apzc->ContentReceivedInputBlock(blockIds[0], true);
+ apzc->ContentReceivedInputBlock(blockIds[1], false);
+
+ apzc->AssertStateIsReset();
+}
+
+TEST_F(APZCGestureDetectorTester, DoubleTapPreventDefaultBoth) {
+ MakeApzcWaitForMainThread();
+ MakeApzcZoomable();
+
+ EXPECT_CALL(*mcc, HandleTap(TapType::eSingleTap, LayoutDevicePoint(10, 10), 0,
+ apzc->GetGuid(), _))
+ .Times(0);
+ EXPECT_CALL(*mcc, HandleTap(TapType::eDoubleTap, LayoutDevicePoint(10, 10), 0,
+ apzc->GetGuid(), _))
+ .Times(0);
+
+ uint64_t blockIds[2];
+ DoubleTapAndCheckStatus(apzc, ScreenIntPoint(10, 10), &blockIds);
+
+ // responses to the two touchstarts
+ apzc->ContentReceivedInputBlock(blockIds[0], true);
+ apzc->ContentReceivedInputBlock(blockIds[1], true);
+
+ apzc->AssertStateIsReset();
+}
+
+// Test for bug 947892
+// We test whether we dispatch tap event when the tap is followed by pinch.
+TEST_F(APZCGestureDetectorTester, TapFollowedByPinch) {
+ MakeApzcZoomable();
+
+ EXPECT_CALL(*mcc, HandleTap(TapType::eSingleTap, LayoutDevicePoint(10, 10), 0,
+ apzc->GetGuid(), _))
+ .Times(1);
+
+ Tap(apzc, ScreenIntPoint(10, 10), TimeDuration::FromMilliseconds(100));
+
+ int inputId = 0;
+ MultiTouchInput mti;
+ mti = CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time());
+ mti.mTouches.AppendElement(SingleTouchData(inputId, ParentLayerPoint(20, 20),
+ ScreenSize(0, 0), 0, 0));
+ mti.mTouches.AppendElement(SingleTouchData(
+ inputId + 1, ParentLayerPoint(10, 10), ScreenSize(0, 0), 0, 0));
+ apzc->ReceiveInputEvent(mti);
+
+ mti = CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_END, mcc->Time());
+ mti.mTouches.AppendElement(SingleTouchData(inputId, ParentLayerPoint(20, 20),
+ ScreenSize(0, 0), 0, 0));
+ mti.mTouches.AppendElement(SingleTouchData(
+ inputId + 1, ParentLayerPoint(10, 10), ScreenSize(0, 0), 0, 0));
+ apzc->ReceiveInputEvent(mti);
+
+ apzc->AssertStateIsReset();
+}
+
+TEST_F(APZCGestureDetectorTester, TapFollowedByMultipleTouches) {
+ MakeApzcZoomable();
+
+ EXPECT_CALL(*mcc, HandleTap(TapType::eSingleTap, LayoutDevicePoint(10, 10), 0,
+ apzc->GetGuid(), _))
+ .Times(1);
+
+ Tap(apzc, ScreenIntPoint(10, 10), TimeDuration::FromMilliseconds(100));
+
+ int inputId = 0;
+ MultiTouchInput mti;
+ mti = CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time());
+ mti.mTouches.AppendElement(SingleTouchData(inputId, ParentLayerPoint(20, 20),
+ ScreenSize(0, 0), 0, 0));
+ apzc->ReceiveInputEvent(mti, Some(nsTArray<uint32_t>{kDefaultTouchBehavior}));
+
+ mti = CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time());
+ mti.mTouches.AppendElement(SingleTouchData(inputId, ParentLayerPoint(20, 20),
+ ScreenSize(0, 0), 0, 0));
+ mti.mTouches.AppendElement(SingleTouchData(
+ inputId + 1, ParentLayerPoint(10, 10), ScreenSize(0, 0), 0, 0));
+ apzc->ReceiveInputEvent(mti, Some(nsTArray<uint32_t>{kDefaultTouchBehavior}));
+
+ mti = CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_END, mcc->Time());
+ mti.mTouches.AppendElement(SingleTouchData(inputId, ParentLayerPoint(20, 20),
+ ScreenSize(0, 0), 0, 0));
+ mti.mTouches.AppendElement(SingleTouchData(
+ inputId + 1, ParentLayerPoint(10, 10), ScreenSize(0, 0), 0, 0));
+ apzc->ReceiveInputEvent(mti);
+
+ apzc->AssertStateIsReset();
+}
+
+TEST_F(APZCGestureDetectorTester, LongPressInterruptedByWheel) {
+ // Since we try to allow concurrent input blocks of different types to
+ // co-exist, the wheel block shouldn't interrupt the long-press detection.
+ // But more importantly, this shouldn't crash, which is what it did at one
+ // point in time.
+ EXPECT_CALL(*mcc, HandleTap(TapType::eLongTap, _, _, _, _)).Times(1);
+
+ APZEventResult result = TouchDown(apzc, ScreenIntPoint(10, 10), mcc->Time());
+ uint64_t touchBlockId = result.mInputBlockId;
+ if (result.GetStatus() != nsEventStatus_eConsumeNoDefault) {
+ SetDefaultAllowedTouchBehavior(apzc, touchBlockId);
+ }
+ mcc->AdvanceByMillis(10);
+ uint64_t wheelBlockId =
+ Wheel(apzc, ScreenIntPoint(10, 10), ScreenPoint(0, -10), mcc->Time())
+ .mInputBlockId;
+ EXPECT_NE(touchBlockId, wheelBlockId);
+ mcc->AdvanceByMillis(1000);
+}
+
+TEST_F(APZCGestureDetectorTester, TapTimeoutInterruptedByWheel) {
+ // In this test, even though the wheel block comes right after the tap, the
+ // tap should still be dispatched because it completes fully before the wheel
+ // block arrived.
+ EXPECT_CALL(*mcc, HandleTap(TapType::eSingleTap, LayoutDevicePoint(10, 10), 0,
+ apzc->GetGuid(), _))
+ .Times(1);
+
+ // We make the APZC zoomable so the gesture detector needs to wait to
+ // distinguish between tap and double-tap. During that timeout is when we
+ // insert the wheel event.
+ MakeApzcZoomable();
+
+ uint64_t touchBlockId = 0;
+ Tap(apzc, ScreenIntPoint(10, 10), TimeDuration::FromMilliseconds(100),
+ nullptr, &touchBlockId);
+ mcc->AdvanceByMillis(10);
+ uint64_t wheelBlockId =
+ Wheel(apzc, ScreenIntPoint(10, 10), ScreenPoint(0, -10), mcc->Time())
+ .mInputBlockId;
+ EXPECT_NE(touchBlockId, wheelBlockId);
+ while (mcc->RunThroughDelayedTasks())
+ ;
+}
+
+TEST_F(APZCGestureDetectorTester, LongPressWithInputQueueDelay) {
+ // In this test, we ensure that any time spent waiting in the input queue for
+ // the content response is subtracted from the long-press timeout in the
+ // GestureEventListener. In this test the content response timeout is longer
+ // than the long-press timeout.
+ SCOPED_GFX_PREF_INT("apz.content_response_timeout", 60);
+ SCOPED_GFX_PREF_INT("ui.click_hold_context_menus.delay", 30);
+
+ MakeApzcWaitForMainThread();
+
+ MockFunction<void(std::string checkPointName)> check;
+
+ {
+ InSequence s;
+ EXPECT_CALL(check, Call("pre long-tap dispatch"));
+ EXPECT_CALL(*mcc, HandleTap(TapType::eLongTap, LayoutDevicePoint(10, 10), 0,
+ apzc->GetGuid(), _))
+ .Times(1);
+ EXPECT_CALL(check, Call("post long-tap dispatch"));
+ }
+
+ // Touch down
+ APZEventResult result = TouchDown(apzc, ScreenIntPoint(10, 10), mcc->Time());
+ uint64_t touchBlockId = result.mInputBlockId;
+ // Simulate content response after 10ms
+ mcc->AdvanceByMillis(10);
+ apzc->ContentReceivedInputBlock(touchBlockId, false);
+ apzc->SetAllowedTouchBehavior(touchBlockId, {kDefaultTouchBehavior});
+ apzc->ConfirmTarget(touchBlockId);
+ // Ensure long-tap event happens within 20ms after that
+ check.Call("pre long-tap dispatch");
+ mcc->AdvanceByMillis(20);
+ check.Call("post long-tap dispatch");
+}
+
+TEST_F(APZCGestureDetectorTester, LongPressWithInputQueueDelay2) {
+ // Similar to the previous test, except this time we don't simulate the
+ // content response at all, and still expect the long-press to happen on
+ // schedule.
+ SCOPED_GFX_PREF_INT("apz.content_response_timeout", 60);
+ SCOPED_GFX_PREF_INT("ui.click_hold_context_menus.delay", 30);
+
+ MakeApzcWaitForMainThread();
+
+ MockFunction<void(std::string checkPointName)> check;
+
+ {
+ InSequence s;
+ EXPECT_CALL(check, Call("pre long-tap dispatch"));
+ EXPECT_CALL(*mcc, HandleTap(TapType::eLongTap, LayoutDevicePoint(10, 10), 0,
+ apzc->GetGuid(), _))
+ .Times(1);
+ EXPECT_CALL(check, Call("post long-tap dispatch"));
+ }
+
+ // Touch down
+ TouchDown(apzc, ScreenIntPoint(10, 10), mcc->Time());
+ // Ensure the long-tap happens within 30ms even though there's no content
+ // response.
+ check.Call("pre long-tap dispatch");
+ mcc->AdvanceByMillis(30);
+ check.Call("post long-tap dispatch");
+}
+
+TEST_F(APZCGestureDetectorTester, LongPressWithInputQueueDelay3) {
+ // Similar to the previous test, except now we have the long-press delay
+ // being longer than the content response timeout.
+ SCOPED_GFX_PREF_INT("apz.content_response_timeout", 30);
+ SCOPED_GFX_PREF_INT("ui.click_hold_context_menus.delay", 60);
+
+ MakeApzcWaitForMainThread();
+
+ MockFunction<void(std::string checkPointName)> check;
+
+ {
+ InSequence s;
+ EXPECT_CALL(check, Call("pre long-tap dispatch"));
+ EXPECT_CALL(*mcc, HandleTap(TapType::eLongTap, LayoutDevicePoint(10, 10), 0,
+ apzc->GetGuid(), _))
+ .Times(1);
+ EXPECT_CALL(check, Call("post long-tap dispatch"));
+ }
+
+ // Touch down
+ TouchDown(apzc, ScreenIntPoint(10, 10), mcc->Time());
+ // Ensure the long-tap happens at the 60ms mark even though the input event
+ // waits in the input queue for the full content response timeout of 30ms
+ mcc->AdvanceByMillis(59);
+ check.Call("pre long-tap dispatch");
+ mcc->AdvanceByMillis(1);
+ check.Call("post long-tap dispatch");
+}
diff --git a/gfx/layers/apz/test/gtest/TestHitTesting.cpp b/gfx/layers/apz/test/gtest/TestHitTesting.cpp
new file mode 100644
index 0000000000..04f1e40f5d
--- /dev/null
+++ b/gfx/layers/apz/test/gtest/TestHitTesting.cpp
@@ -0,0 +1,352 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "APZCTreeManagerTester.h"
+#include "APZTestCommon.h"
+
+#include "InputUtils.h"
+
+class APZHitTestingTester : public APZCTreeManagerTester {
+ protected:
+ ScreenToParentLayerMatrix4x4 transformToApzc;
+ ParentLayerToScreenMatrix4x4 transformToGecko;
+
+ already_AddRefed<AsyncPanZoomController> GetTargetAPZC(
+ const ScreenPoint& aPoint) {
+ RefPtr<AsyncPanZoomController> hit =
+ manager->GetTargetAPZC(aPoint).mTargetApzc;
+ if (hit) {
+ transformToApzc = manager->GetScreenToApzcTransform(hit.get());
+ transformToGecko =
+ manager->GetApzcToGeckoTransform(hit.get(), LayoutAndVisual);
+ }
+ return hit.forget();
+ }
+
+ protected:
+ void DisableApzOn(WebRenderLayerScrollData* aLayer) {
+ ModifyFrameMetrics(aLayer, [](ScrollMetadata& aSm, FrameMetrics&) {
+ aSm.SetForceDisableApz(true);
+ });
+ }
+
+ void CreateComplexMultiLayerTree() {
+ const char* treeShape = "x(xx(x)xx(x(x)xx))";
+ // LayerID 0 12 3 45 6 7 89
+ LayerIntRegion layerVisibleRegion[] = {
+ LayerIntRect(0, 0, 300, 400), // root(0)
+ LayerIntRect(0, 0, 100, 100), // layer(1) in top-left
+ LayerIntRect(50, 50, 200, 300), // layer(2) centered in root(0)
+ LayerIntRect(50, 50, 200,
+ 300), // layer(3) fully occupying parent layer(2)
+ LayerIntRect(0, 200, 100, 100), // layer(4) in bottom-left
+ LayerIntRect(200, 0, 100,
+ 400), // layer(5) along the right 100px of root(0)
+ LayerIntRect(200, 0, 100, 200), // layer(6) taking up the top
+ // half of parent layer(5)
+ LayerIntRect(200, 0, 100,
+ 200), // layer(7) fully occupying parent layer(6)
+ LayerIntRect(200, 200, 100,
+ 100), // layer(8) in bottom-right (below (6))
+ LayerIntRect(200, 300, 100,
+ 100), // layer(9) in bottom-right (below (8))
+ };
+ CreateScrollData(treeShape, layerVisibleRegion);
+ SetScrollableFrameMetrics(layers[1], ScrollableLayerGuid::START_SCROLL_ID);
+ SetScrollableFrameMetrics(layers[2], ScrollableLayerGuid::START_SCROLL_ID);
+ SetScrollableFrameMetrics(layers[4],
+ ScrollableLayerGuid::START_SCROLL_ID + 1);
+ SetScrollableFrameMetrics(layers[6],
+ ScrollableLayerGuid::START_SCROLL_ID + 1);
+ SetScrollableFrameMetrics(layers[7],
+ ScrollableLayerGuid::START_SCROLL_ID + 2);
+ SetScrollableFrameMetrics(layers[8],
+ ScrollableLayerGuid::START_SCROLL_ID + 1);
+ SetScrollableFrameMetrics(layers[9],
+ ScrollableLayerGuid::START_SCROLL_ID + 3);
+ }
+
+ void CreateBug1148350LayerTree() {
+ const char* treeShape = "x(x)";
+ // LayerID 0 1
+ LayerIntRegion layerVisibleRegion[] = {
+ LayerIntRect(0, 0, 200, 200),
+ LayerIntRect(0, 0, 200, 200),
+ };
+ CreateScrollData(treeShape, layerVisibleRegion);
+ SetScrollableFrameMetrics(layers[1], ScrollableLayerGuid::START_SCROLL_ID);
+ }
+};
+
+TEST_F(APZHitTestingTester, ComplexMultiLayerTree) {
+ CreateComplexMultiLayerTree();
+ ScopedLayerTreeRegistration registration(LayersId{0}, mcc);
+ UpdateHitTestingTree();
+
+ /* The layer tree looks like this:
+
+ 0
+ |----|--+--|----|
+ 1 2 4 5
+ | /|\
+ 3 6 8 9
+ |
+ 7
+
+ Layers 1,2 have the same APZC
+ Layers 4,6,8 have the same APZC
+ Layer 7 has an APZC
+ Layer 9 has an APZC
+ */
+
+ TestAsyncPanZoomController* nullAPZC = nullptr;
+ // Ensure all the scrollable layers have an APZC
+
+ EXPECT_FALSE(HasScrollableFrameMetrics(layers[0]));
+ EXPECT_NE(nullAPZC, ApzcOf(layers[1]));
+ EXPECT_NE(nullAPZC, ApzcOf(layers[2]));
+ EXPECT_FALSE(HasScrollableFrameMetrics(layers[3]));
+ EXPECT_NE(nullAPZC, ApzcOf(layers[4]));
+ EXPECT_FALSE(HasScrollableFrameMetrics(layers[5]));
+ EXPECT_NE(nullAPZC, ApzcOf(layers[6]));
+ EXPECT_NE(nullAPZC, ApzcOf(layers[7]));
+ EXPECT_NE(nullAPZC, ApzcOf(layers[8]));
+ EXPECT_NE(nullAPZC, ApzcOf(layers[9]));
+ // Ensure those that scroll together have the same APZCs
+ EXPECT_EQ(ApzcOf(layers[1]), ApzcOf(layers[2]));
+ EXPECT_EQ(ApzcOf(layers[4]), ApzcOf(layers[6]));
+ EXPECT_EQ(ApzcOf(layers[8]), ApzcOf(layers[6]));
+ // Ensure those that don't scroll together have different APZCs
+ EXPECT_NE(ApzcOf(layers[1]), ApzcOf(layers[4]));
+ EXPECT_NE(ApzcOf(layers[1]), ApzcOf(layers[7]));
+ EXPECT_NE(ApzcOf(layers[1]), ApzcOf(layers[9]));
+ EXPECT_NE(ApzcOf(layers[4]), ApzcOf(layers[7]));
+ EXPECT_NE(ApzcOf(layers[4]), ApzcOf(layers[9]));
+ EXPECT_NE(ApzcOf(layers[7]), ApzcOf(layers[9]));
+ // Ensure the APZC parent chains are set up correctly
+ TestAsyncPanZoomController* layers1_2 = ApzcOf(layers[1]);
+ TestAsyncPanZoomController* layers4_6_8 = ApzcOf(layers[4]);
+ TestAsyncPanZoomController* layer7 = ApzcOf(layers[7]);
+ TestAsyncPanZoomController* layer9 = ApzcOf(layers[9]);
+ EXPECT_EQ(nullptr, layers1_2->GetParent());
+ EXPECT_EQ(nullptr, layers4_6_8->GetParent());
+ EXPECT_EQ(layers4_6_8, layer7->GetParent());
+ EXPECT_EQ(nullptr, layer9->GetParent());
+ // Ensure the hit-testing tree looks like the layer tree
+ RefPtr<HitTestingTreeNode> root = manager->GetRootNode();
+ RefPtr<HitTestingTreeNode> node5 = root->GetLastChild();
+ RefPtr<HitTestingTreeNode> node4 = node5->GetPrevSibling();
+ RefPtr<HitTestingTreeNode> node2 = node4->GetPrevSibling();
+ RefPtr<HitTestingTreeNode> node1 = node2->GetPrevSibling();
+ RefPtr<HitTestingTreeNode> node3 = node2->GetLastChild();
+ RefPtr<HitTestingTreeNode> node9 = node5->GetLastChild();
+ RefPtr<HitTestingTreeNode> node8 = node9->GetPrevSibling();
+ RefPtr<HitTestingTreeNode> node6 = node8->GetPrevSibling();
+ RefPtr<HitTestingTreeNode> node7 = node6->GetLastChild();
+ EXPECT_EQ(nullptr, node1->GetPrevSibling());
+ EXPECT_EQ(nullptr, node3->GetPrevSibling());
+ EXPECT_EQ(nullptr, node6->GetPrevSibling());
+ EXPECT_EQ(nullptr, node7->GetPrevSibling());
+ EXPECT_EQ(nullptr, node1->GetLastChild());
+ EXPECT_EQ(nullptr, node3->GetLastChild());
+ EXPECT_EQ(nullptr, node4->GetLastChild());
+ EXPECT_EQ(nullptr, node7->GetLastChild());
+ EXPECT_EQ(nullptr, node8->GetLastChild());
+ EXPECT_EQ(nullptr, node9->GetLastChild());
+
+ // Assertions about hit-testing have been ported to mochitest,
+ // in helper_hittest_bug1730606-4.html.
+}
+
+TEST_F(APZHitTestingTester, TestRepaintFlushOnNewInputBlock) {
+ // The main purpose of this test is to verify that touch-start events (or
+ // anything that starts a new input block) don't ever get untransformed. This
+ // should always hold because the APZ code should flush repaints when we start
+ // a new input block and the transform to gecko space should be empty.
+
+ CreateSimpleScrollingLayer();
+ ScopedLayerTreeRegistration registration(LayersId{0}, mcc);
+ UpdateHitTestingTree();
+ RefPtr<TestAsyncPanZoomController> apzcroot = ApzcOf(root);
+
+ // At this point, the following holds (all coordinates in screen pixels):
+ // layers[0] has content from (0,0)-(500,500), clipped by composition bounds
+ // (0,0)-(200,200)
+
+ MockFunction<void(std::string checkPointName)> check;
+
+ {
+ InSequence s;
+
+ EXPECT_CALL(*mcc, RequestContentRepaint(_)).Times(AtLeast(1));
+ EXPECT_CALL(check, Call("post-first-touch-start"));
+ EXPECT_CALL(*mcc, RequestContentRepaint(_)).Times(AtLeast(1));
+ EXPECT_CALL(check, Call("post-second-fling"));
+ EXPECT_CALL(*mcc, RequestContentRepaint(_)).Times(AtLeast(1));
+ EXPECT_CALL(check, Call("post-second-touch-start"));
+ }
+
+ // This first pan will move the APZC by 50 pixels, and dispatch a paint
+ // request.
+ Pan(apzcroot, 100, 50, PanOptions::NoFling);
+
+ // Verify that a touch start doesn't get untransformed
+ ScreenIntPoint touchPoint(50, 50);
+ MultiTouchInput mti =
+ CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time());
+ mti.mTouches.AppendElement(
+ SingleTouchData(0, touchPoint, ScreenSize(0, 0), 0, 0));
+
+ EXPECT_EQ(nsEventStatus_eConsumeDoDefault,
+ manager->ReceiveInputEvent(mti).GetStatus());
+ EXPECT_EQ(touchPoint, mti.mTouches[0].mScreenPoint);
+ check.Call("post-first-touch-start");
+
+ // Send a touchend to clear state
+ mti.mType = MultiTouchInput::MULTITOUCH_END;
+ manager->ReceiveInputEvent(mti);
+
+ mcc->AdvanceByMillis(1000);
+
+ // Now do two pans. The first of these will dispatch a repaint request, as
+ // above. The second will get stuck in the paint throttler because the first
+ // one doesn't get marked as "completed", so this will result in a non-empty
+ // LD transform. (Note that any outstanding repaint requests from the first
+ // half of this test don't impact this half because we advance the time by 1
+ // second, which will trigger the max-wait-exceeded codepath in the paint
+ // throttler).
+ Pan(apzcroot, 100, 50, PanOptions::NoFling);
+ check.Call("post-second-fling");
+ Pan(apzcroot, 100, 50, PanOptions::NoFling);
+
+ // Ensure that a touch start again doesn't get untransformed by flushing
+ // a repaint
+ mti.mType = MultiTouchInput::MULTITOUCH_START;
+ EXPECT_EQ(nsEventStatus_eConsumeDoDefault,
+ manager->ReceiveInputEvent(mti).GetStatus());
+ EXPECT_EQ(touchPoint, mti.mTouches[0].mScreenPoint);
+ check.Call("post-second-touch-start");
+
+ mti.mType = MultiTouchInput::MULTITOUCH_END;
+ EXPECT_EQ(nsEventStatus_eConsumeDoDefault,
+ manager->ReceiveInputEvent(mti).GetStatus());
+ EXPECT_EQ(touchPoint, mti.mTouches[0].mScreenPoint);
+}
+
+TEST_F(APZHitTestingTester, TestRepaintFlushOnWheelEvents) {
+ // The purpose of this test is to ensure that wheel events trigger a repaint
+ // flush as per bug 1166871, and that the wheel event untransform is a no-op.
+
+ CreateSimpleScrollingLayer();
+ ScopedLayerTreeRegistration registration(LayersId{0}, mcc);
+ UpdateHitTestingTree();
+ TestAsyncPanZoomController* apzcroot = ApzcOf(root);
+
+ EXPECT_CALL(*mcc, RequestContentRepaint(_)).Times(AtLeast(3));
+ ScreenPoint origin(100, 50);
+ for (int i = 0; i < 3; i++) {
+ ScrollWheelInput swi(mcc->Time(), 0, ScrollWheelInput::SCROLLMODE_INSTANT,
+ ScrollWheelInput::SCROLLDELTA_PIXEL, origin, 0, 10,
+ false, WheelDeltaAdjustmentStrategy::eNone);
+ EXPECT_EQ(nsEventStatus_eConsumeDoDefault,
+ manager->ReceiveInputEvent(swi).GetStatus());
+ EXPECT_EQ(origin, swi.mOrigin);
+
+ AsyncTransform viewTransform;
+ ParentLayerPoint point;
+ apzcroot->SampleContentTransformForFrame(&viewTransform, point);
+ EXPECT_EQ(0, point.x);
+ EXPECT_EQ((i + 1) * 10, point.y);
+ EXPECT_EQ(0, viewTransform.mTranslation.x);
+ EXPECT_EQ((i + 1) * -10, viewTransform.mTranslation.y);
+
+ mcc->AdvanceByMillis(5);
+ }
+}
+
+TEST_F(APZHitTestingTester, TestForceDisableApz) {
+ CreateSimpleScrollingLayer();
+ ScopedLayerTreeRegistration registration(LayersId{0}, mcc);
+ UpdateHitTestingTree();
+ DisableApzOn(root);
+ TestAsyncPanZoomController* apzcroot = ApzcOf(root);
+
+ ScreenPoint origin(100, 50);
+ ScrollWheelInput swi(mcc->Time(), 0, ScrollWheelInput::SCROLLMODE_INSTANT,
+ ScrollWheelInput::SCROLLDELTA_PIXEL, origin, 0, 10,
+ false, WheelDeltaAdjustmentStrategy::eNone);
+ EXPECT_EQ(nsEventStatus_eConsumeDoDefault,
+ manager->ReceiveInputEvent(swi).GetStatus());
+ EXPECT_EQ(origin, swi.mOrigin);
+
+ AsyncTransform viewTransform;
+ ParentLayerPoint point;
+ apzcroot->SampleContentTransformForFrame(&viewTransform, point);
+ // Since APZ is force-disabled, we expect to see the async transform via
+ // the NORMAL AsyncMode, but not via the RESPECT_FORCE_DISABLE AsyncMode.
+ EXPECT_EQ(0, point.x);
+ EXPECT_EQ(10, point.y);
+ EXPECT_EQ(0, viewTransform.mTranslation.x);
+ EXPECT_EQ(-10, viewTransform.mTranslation.y);
+ viewTransform = apzcroot->GetCurrentAsyncTransform(
+ AsyncPanZoomController::eForCompositing);
+ point = apzcroot->GetCurrentAsyncScrollOffset(
+ AsyncPanZoomController::eForCompositing);
+ EXPECT_EQ(0, point.x);
+ EXPECT_EQ(0, point.y);
+ EXPECT_EQ(0, viewTransform.mTranslation.x);
+ EXPECT_EQ(0, viewTransform.mTranslation.y);
+
+ mcc->AdvanceByMillis(10);
+
+ // With untransforming events we should get normal behaviour (in this case,
+ // no noticeable untransform, because the repaint request already got
+ // flushed).
+ swi = ScrollWheelInput(mcc->Time(), 0, ScrollWheelInput::SCROLLMODE_INSTANT,
+ ScrollWheelInput::SCROLLDELTA_PIXEL, origin, 0, 0,
+ false, WheelDeltaAdjustmentStrategy::eNone);
+ EXPECT_EQ(nsEventStatus_eConsumeDoDefault,
+ manager->ReceiveInputEvent(swi).GetStatus());
+ EXPECT_EQ(origin, swi.mOrigin);
+}
+
+TEST_F(APZHitTestingTester, Bug1148350) {
+ CreateBug1148350LayerTree();
+ ScopedLayerTreeRegistration registration(LayersId{0}, mcc);
+ UpdateHitTestingTree();
+
+ MockFunction<void(std::string checkPointName)> check;
+ {
+ InSequence s;
+ EXPECT_CALL(*mcc,
+ HandleTap(TapType::eSingleTap, LayoutDevicePoint(100, 100), 0,
+ ApzcOf(layers[1])->GetGuid(), _))
+ .Times(1);
+ EXPECT_CALL(check, Call("Tapped without transform"));
+ EXPECT_CALL(*mcc,
+ HandleTap(TapType::eSingleTap, LayoutDevicePoint(100, 100), 0,
+ ApzcOf(layers[1])->GetGuid(), _))
+ .Times(1);
+ EXPECT_CALL(check, Call("Tapped with interleaved transform"));
+ }
+
+ Tap(manager, ScreenIntPoint(100, 100), TimeDuration::FromMilliseconds(100));
+ mcc->RunThroughDelayedTasks();
+ check.Call("Tapped without transform");
+
+ uint64_t blockId =
+ TouchDown(manager, ScreenIntPoint(100, 100), mcc->Time()).mInputBlockId;
+ SetDefaultAllowedTouchBehavior(manager, blockId);
+ mcc->AdvanceByMillis(100);
+
+ layers[0]->SetVisibleRegion(LayerIntRegion(LayerIntRect(0, 50, 200, 150)));
+ layers[0]->SetTransform(Matrix4x4::Translation(0, 50, 0));
+ UpdateHitTestingTree();
+
+ TouchUp(manager, ScreenIntPoint(100, 100), mcc->Time());
+ mcc->RunThroughDelayedTasks();
+ check.Call("Tapped with interleaved transform");
+}
diff --git a/gfx/layers/apz/test/gtest/TestInputQueue.cpp b/gfx/layers/apz/test/gtest/TestInputQueue.cpp
new file mode 100644
index 0000000000..6e47340da5
--- /dev/null
+++ b/gfx/layers/apz/test/gtest/TestInputQueue.cpp
@@ -0,0 +1,45 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "APZCTreeManagerTester.h"
+#include "APZTestCommon.h"
+#include "InputUtils.h"
+
+// Test of scenario described in bug 1269067 - that a continuing mouse drag
+// doesn't interrupt a wheel scrolling animation
+TEST_F(APZCTreeManagerTester, WheelInterruptedByMouseDrag) {
+ // Set up a scrollable layer
+ CreateSimpleScrollingLayer();
+ ScopedLayerTreeRegistration registration(LayersId{0}, mcc);
+ UpdateHitTestingTree();
+ RefPtr<TestAsyncPanZoomController> apzc = ApzcOf(root);
+
+ // First start the mouse drag
+ uint64_t dragBlockId =
+ MouseDown(apzc, ScreenIntPoint(5, 5), mcc->Time()).mInputBlockId;
+ uint64_t tmpBlockId =
+ MouseMove(apzc, ScreenIntPoint(6, 6), mcc->Time()).mInputBlockId;
+ EXPECT_EQ(dragBlockId, tmpBlockId);
+
+ // Insert the wheel event, check that it has a new block id
+ uint64_t wheelBlockId =
+ SmoothWheel(apzc, ScreenIntPoint(6, 6), ScreenPoint(0, 1), mcc->Time())
+ .mInputBlockId;
+ EXPECT_NE(dragBlockId, wheelBlockId);
+
+ // Continue the drag, check that the block id is the same as before
+ tmpBlockId = MouseMove(apzc, ScreenIntPoint(7, 5), mcc->Time()).mInputBlockId;
+ EXPECT_EQ(dragBlockId, tmpBlockId);
+
+ // Finish the wheel animation
+ apzc->AdvanceAnimationsUntilEnd();
+
+ // Check that it scrolled
+ ParentLayerPoint scroll =
+ apzc->GetCurrentAsyncScrollOffset(AsyncPanZoomController::eForHitTesting);
+ EXPECT_EQ(scroll.x, 0);
+ EXPECT_EQ(scroll.y, 10); // We scrolled 1 "line" or 10 pixels
+}
diff --git a/gfx/layers/apz/test/gtest/TestOverscroll.cpp b/gfx/layers/apz/test/gtest/TestOverscroll.cpp
new file mode 100644
index 0000000000..10327871fb
--- /dev/null
+++ b/gfx/layers/apz/test/gtest/TestOverscroll.cpp
@@ -0,0 +1,1991 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "APZCBasicTester.h"
+#include "APZCTreeManagerTester.h"
+#include "APZTestCommon.h"
+#include "mozilla/layers/WebRenderScrollDataWrapper.h"
+
+#include "InputUtils.h"
+
+class APZCOverscrollTester : public APZCBasicTester {
+ public:
+ explicit APZCOverscrollTester(
+ AsyncPanZoomController::GestureBehavior aGestureBehavior =
+ AsyncPanZoomController::DEFAULT_GESTURES)
+ : APZCBasicTester(aGestureBehavior) {}
+
+ protected:
+ UniquePtr<ScopedLayerTreeRegistration> registration;
+
+ void TestOverscroll() {
+ // Pan sufficiently to hit overscroll behavior
+ PanIntoOverscroll();
+
+ // Check that we recover from overscroll via an animation.
+ ParentLayerPoint expectedScrollOffset(0, GetScrollRange().YMost());
+ SampleAnimationUntilRecoveredFromOverscroll(expectedScrollOffset);
+ }
+
+ void PanIntoOverscroll() {
+ int touchStart = 500;
+ int touchEnd = 10;
+ Pan(apzc, touchStart, touchEnd);
+ EXPECT_TRUE(apzc->IsOverscrolled());
+ }
+
+ /**
+ * Sample animations until we recover from overscroll.
+ * @param aExpectedScrollOffset the expected reported scroll offset
+ * throughout the animation
+ */
+ void SampleAnimationUntilRecoveredFromOverscroll(
+ const ParentLayerPoint& aExpectedScrollOffset) {
+ const TimeDuration increment = TimeDuration::FromMilliseconds(1);
+ bool recoveredFromOverscroll = false;
+ ParentLayerPoint pointOut;
+ AsyncTransform viewTransformOut;
+ while (apzc->SampleContentTransformForFrame(&viewTransformOut, pointOut)) {
+ // The reported scroll offset should be the same throughout.
+ EXPECT_EQ(aExpectedScrollOffset, pointOut);
+
+ // Trigger computation of the overscroll tranform, to make sure
+ // no assetions fire during the calculation.
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting);
+
+ if (!apzc->IsOverscrolled()) {
+ recoveredFromOverscroll = true;
+ }
+
+ mcc->AdvanceBy(increment);
+ }
+ EXPECT_TRUE(recoveredFromOverscroll);
+ apzc->AssertStateIsReset();
+ }
+
+ ScrollableLayerGuid CreateSimpleRootScrollableForWebRender() {
+ ScrollableLayerGuid guid;
+ guid.mScrollId = ScrollableLayerGuid::START_SCROLL_ID;
+ guid.mLayersId = LayersId{0};
+
+ ScrollMetadata metadata;
+ FrameMetrics& metrics = metadata.GetMetrics();
+ metrics.SetCompositionBounds(ParentLayerRect(0, 0, 100, 100));
+ metrics.SetScrollableRect(CSSRect(0, 0, 100, 1000));
+ metrics.SetScrollId(guid.mScrollId);
+ metadata.SetIsLayersIdRoot(true);
+
+ WebRenderLayerScrollData rootLayerScrollData;
+ rootLayerScrollData.InitializeRoot(0);
+ WebRenderScrollData scrollData;
+ rootLayerScrollData.AppendScrollMetadata(scrollData, metadata);
+ scrollData.AddLayerData(std::move(rootLayerScrollData));
+
+ registration = MakeUnique<ScopedLayerTreeRegistration>(guid.mLayersId, mcc);
+ tm->UpdateHitTestingTree(WebRenderScrollDataWrapper(*updater, &scrollData),
+ false, guid.mLayersId, 0);
+ return guid;
+ }
+};
+
+#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android
+TEST_F(APZCOverscrollTester, FlingIntoOverscroll) {
+ // Enable overscrolling.
+ SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
+ SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0f);
+
+ // Scroll down by 25 px. Don't fling for simplicity.
+ Pan(apzc, 50, 25, PanOptions::NoFling);
+
+ // Now scroll back up by 20px, this time flinging after.
+ // The fling should cover the remaining 5 px of room to scroll, then
+ // go into overscroll, and finally snap-back to recover from overscroll.
+ Pan(apzc, 25, 45);
+ const TimeDuration increment = TimeDuration::FromMilliseconds(1);
+ bool reachedOverscroll = false;
+ bool recoveredFromOverscroll = false;
+ while (apzc->AdvanceAnimations(mcc->GetSampleTime())) {
+ if (!reachedOverscroll && apzc->IsOverscrolled()) {
+ reachedOverscroll = true;
+ }
+ if (reachedOverscroll && !apzc->IsOverscrolled()) {
+ recoveredFromOverscroll = true;
+ }
+ mcc->AdvanceBy(increment);
+ }
+ EXPECT_TRUE(reachedOverscroll);
+ EXPECT_TRUE(recoveredFromOverscroll);
+}
+#endif
+
+#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android
+TEST_F(APZCOverscrollTester, OverScrollPanning) {
+ SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
+
+ TestOverscroll();
+}
+#endif
+
+#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android
+// Tests that an overscroll animation doesn't trigger an assertion failure
+// in the case where a sample has a velocity of zero.
+TEST_F(APZCOverscrollTester, OverScroll_Bug1152051a) {
+ SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
+
+ // Doctor the prefs to make the velocity zero at the end of the first sample.
+
+ // This ensures our incoming velocity to the overscroll animation is
+ // a round(ish) number, 4.9 (that being the distance of the pan before
+ // overscroll, which is 500 - 10 = 490 pixels, divided by the duration of
+ // the pan, which is 100 ms).
+ SCOPED_GFX_PREF_FLOAT("apz.fling_friction", 0);
+
+ TestOverscroll();
+}
+#endif
+
+#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android
+// Tests that ending an overscroll animation doesn't leave around state that
+// confuses the next overscroll animation.
+TEST_F(APZCOverscrollTester, OverScroll_Bug1152051b) {
+ SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
+ SCOPED_GFX_PREF_FLOAT("apz.overscroll.stop_distance_threshold", 0.1f);
+
+ // Pan sufficiently to hit overscroll behavior
+ PanIntoOverscroll();
+
+ // Sample animations once, to give the fling animation started on touch-up
+ // a chance to realize it's overscrolled, and schedule a call to
+ // HandleFlingOverscroll().
+ SampleAnimationOnce();
+
+ // This advances the time and runs the HandleFlingOverscroll task scheduled in
+ // the previous call, which starts an overscroll animation. It then samples
+ // the overscroll animation once, to get it to initialize the first overscroll
+ // sample.
+ SampleAnimationOnce();
+
+ // Do a touch-down to cancel the overscroll animation, and then a touch-up
+ // to schedule a new one since we're still overscrolled. We don't pan because
+ // panning can trigger functions that clear the overscroll animation state
+ // in other ways.
+ APZEventResult result = TouchDown(apzc, ScreenIntPoint(10, 10), mcc->Time());
+ if (result.GetStatus() != nsEventStatus_eConsumeNoDefault) {
+ SetDefaultAllowedTouchBehavior(apzc, result.mInputBlockId);
+ }
+ TouchUp(apzc, ScreenIntPoint(10, 10), mcc->Time());
+
+ // Sample the second overscroll animation to its end.
+ // If the ending of the first overscroll animation fails to clear state
+ // properly, this will assert.
+ ParentLayerPoint expectedScrollOffset(0, GetScrollRange().YMost());
+ SampleAnimationUntilRecoveredFromOverscroll(expectedScrollOffset);
+}
+#endif
+
+#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android
+// Tests that the page doesn't get stuck in an
+// overscroll animation after a low-velocity pan.
+TEST_F(APZCOverscrollTester, OverScrollAfterLowVelocityPan_Bug1343775) {
+ SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
+
+ // Pan into overscroll with a velocity less than the
+ // apz.fling_min_velocity_threshold preference.
+ Pan(apzc, 10, 30);
+
+ EXPECT_TRUE(apzc->IsOverscrolled());
+
+ apzc->AdvanceAnimationsUntilEnd();
+
+ // Check that we recovered from overscroll.
+ EXPECT_FALSE(apzc->IsOverscrolled());
+}
+#endif
+
+#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android
+TEST_F(APZCOverscrollTester, OverScrollAbort) {
+ SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
+
+ // Pan sufficiently to hit overscroll behavior
+ int touchStart = 500;
+ int touchEnd = 10;
+ Pan(apzc, touchStart, touchEnd);
+ EXPECT_TRUE(apzc->IsOverscrolled());
+
+ ParentLayerPoint pointOut;
+ AsyncTransform viewTransformOut;
+
+ // This sample call will run to the end of the fling animation
+ // and will schedule the overscroll animation.
+ apzc->SampleContentTransformForFrame(&viewTransformOut, pointOut,
+ TimeDuration::FromMilliseconds(10000));
+ EXPECT_TRUE(apzc->IsOverscrolled());
+
+ // At this point, we have an active overscroll animation.
+ // Check that cancelling the animation clears the overscroll.
+ apzc->CancelAnimation();
+ EXPECT_FALSE(apzc->IsOverscrolled());
+ apzc->AssertStateIsReset();
+}
+#endif
+
+#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android
+TEST_F(APZCOverscrollTester, OverScrollPanningAbort) {
+ SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
+
+ // Pan sufficiently to hit overscroll behaviour. Keep the finger down so
+ // the pan does not end.
+ int touchStart = 500;
+ int touchEnd = 10;
+ Pan(apzc, touchStart, touchEnd, PanOptions::KeepFingerDown);
+ EXPECT_TRUE(apzc->IsOverscrolled());
+
+ // Check that calling CancelAnimation() while the user is still panning
+ // (and thus no fling or snap-back animation has had a chance to start)
+ // clears the overscroll.
+ apzc->CancelAnimation();
+ EXPECT_FALSE(apzc->IsOverscrolled());
+ apzc->AssertStateIsReset();
+}
+#endif
+
+#ifndef MOZ_WIDGET_ANDROID // Maybe fails on Android
+TEST_F(APZCOverscrollTester, OverscrollByVerticalPanGestures) {
+ SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
+
+ PanGesture(PanGestureInput::PANGESTURE_START, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, -2), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, -10), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, -2), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_END, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, 0), mcc->Time());
+
+ EXPECT_TRUE(apzc->IsOverscrolled());
+
+ // Check that we recover from overscroll via an animation.
+ ParentLayerPoint expectedScrollOffset(0, 0);
+ SampleAnimationUntilRecoveredFromOverscroll(expectedScrollOffset);
+}
+#endif
+
+#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android
+TEST_F(APZCOverscrollTester, StuckInOverscroll_Bug1767337) {
+ SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
+
+ PanGesture(PanGestureInput::PANGESTURE_START, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, -2), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, -10), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, -10), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, -10), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, -10), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, -2), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ // Send two PANGESTURE_END in a row, to see if the second one gets us
+ // stuck in overscroll.
+ PanGesture(PanGestureInput::PANGESTURE_END, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, 0), mcc->Time(), MODIFIER_NONE, true);
+ SampleAnimationOnce();
+ PanGesture(PanGestureInput::PANGESTURE_END, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, 0), mcc->Time(), MODIFIER_NONE, true);
+
+ EXPECT_TRUE(apzc->IsOverscrolled());
+
+ // Check that we recover from overscroll via an animation.
+ ParentLayerPoint expectedScrollOffset(0, 0);
+ SampleAnimationUntilRecoveredFromOverscroll(expectedScrollOffset);
+}
+#endif
+
+#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android
+TEST_F(APZCOverscrollTester, OverscrollByVerticalAndHorizontalPanGestures) {
+ SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
+
+ PanGesture(PanGestureInput::PANGESTURE_START, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, -2), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, -10), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, -2), mcc->Time());
+
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(-10, 0), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(-2, 0), mcc->Time());
+
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_END, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, 0), mcc->Time());
+
+ EXPECT_TRUE(apzc->IsOverscrolled());
+
+ // Check that we recover from overscroll via an animation.
+ ParentLayerPoint expectedScrollOffset(0, 0);
+ SampleAnimationUntilRecoveredFromOverscroll(expectedScrollOffset);
+}
+#endif
+
+#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android
+TEST_F(APZCOverscrollTester, OverscrollByPanMomentumGestures) {
+ SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
+
+ PanGesture(PanGestureInput::PANGESTURE_START, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, 2), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, 10), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, 2), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_END, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, 0), mcc->Time());
+
+ // Make sure we are not yet in overscrolled region.
+ EXPECT_TRUE(!apzc->IsOverscrolled());
+
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMSTART, apzc,
+ ScreenIntPoint(50, 80), ScreenPoint(0, 0), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc,
+ ScreenIntPoint(50, 80), ScreenPoint(0, 200), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc,
+ ScreenIntPoint(50, 80), ScreenPoint(0, 100), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc,
+ ScreenIntPoint(50, 80), ScreenPoint(0, 2), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMEND, apzc,
+ ScreenIntPoint(50, 80), ScreenPoint(0, 0), mcc->Time());
+
+ EXPECT_TRUE(apzc->IsOverscrolled());
+
+ // Check that we recover from overscroll via an animation.
+ ParentLayerPoint expectedScrollOffset(0, GetScrollRange().YMost());
+ SampleAnimationUntilRecoveredFromOverscroll(expectedScrollOffset);
+}
+#endif
+
+#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android
+TEST_F(APZCOverscrollTester, IgnoreMomemtumDuringOverscroll) {
+ SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
+
+ float yMost = GetScrollRange().YMost();
+ PanGesture(PanGestureInput::PANGESTURE_START, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, yMost / 10), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, yMost), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, yMost / 10), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_END, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, 0), mcc->Time());
+
+ // Make sure we've started an overscroll animation.
+ EXPECT_TRUE(apzc->IsOverscrolled());
+ EXPECT_TRUE(apzc->IsOverscrollAnimationRunning());
+
+ // And check the overscrolled transform value before/after calling PanGesture
+ // to make sure the overscroll amount isn't affected by momentum events.
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ AsyncTransformComponentMatrix overscrolledTransform =
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting);
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMSTART, apzc,
+ ScreenIntPoint(50, 80), ScreenPoint(0, 0), mcc->Time());
+ EXPECT_EQ(overscrolledTransform, apzc->GetOverscrollTransform(
+ AsyncPanZoomController::eForHitTesting));
+
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ overscrolledTransform =
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting);
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc,
+ ScreenIntPoint(50, 80), ScreenPoint(0, 200), mcc->Time());
+ EXPECT_EQ(overscrolledTransform, apzc->GetOverscrollTransform(
+ AsyncPanZoomController::eForHitTesting));
+
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ overscrolledTransform =
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting);
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc,
+ ScreenIntPoint(50, 80), ScreenPoint(0, 100), mcc->Time());
+ EXPECT_EQ(overscrolledTransform, apzc->GetOverscrollTransform(
+ AsyncPanZoomController::eForHitTesting));
+
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ overscrolledTransform =
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting);
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc,
+ ScreenIntPoint(50, 80), ScreenPoint(0, 2), mcc->Time());
+ EXPECT_EQ(overscrolledTransform, apzc->GetOverscrollTransform(
+ AsyncPanZoomController::eForHitTesting));
+
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ overscrolledTransform =
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting);
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMEND, apzc,
+ ScreenIntPoint(50, 80), ScreenPoint(0, 0), mcc->Time());
+ EXPECT_EQ(overscrolledTransform, apzc->GetOverscrollTransform(
+ AsyncPanZoomController::eForHitTesting));
+
+ // Check that we've recovered from overscroll via an animation.
+ ParentLayerPoint expectedScrollOffset(0, GetScrollRange().YMost());
+ SampleAnimationUntilRecoveredFromOverscroll(expectedScrollOffset);
+}
+#endif
+
+#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android
+TEST_F(APZCOverscrollTester, VerticalOnlyOverscroll) {
+ SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
+
+ // Make the content scrollable only vertically.
+ ScrollMetadata metadata;
+ FrameMetrics& metrics = metadata.GetMetrics();
+ metrics.SetCompositionBounds(ParentLayerRect(0, 0, 100, 100));
+ metrics.SetScrollableRect(CSSRect(0, 0, 100, 1000));
+ apzc->SetFrameMetrics(metrics);
+
+ // Scroll up into overscroll a bit.
+ PanGesture(PanGestureInput::PANGESTURE_START, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(-2, -2), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(-10, -10), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(-2, -2), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_END, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, 0), mcc->Time());
+ // Now it's overscrolled.
+ EXPECT_TRUE(apzc->IsOverscrolled());
+ AsyncTransformComponentMatrix overscrolledTransform =
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting);
+ // The overscroll shouldn't happen horizontally.
+ EXPECT_TRUE(overscrolledTransform._41 == 0);
+ // Happens only vertically.
+ EXPECT_TRUE(overscrolledTransform._42 != 0);
+
+ // Send pan momentum events including horizontal bits.
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMSTART, apzc,
+ ScreenIntPoint(50, 80), ScreenPoint(0, 0), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc,
+ ScreenIntPoint(50, 80), ScreenPoint(-10, -100), mcc->Time());
+ overscrolledTransform =
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting);
+ // The overscroll shouldn't happen horizontally.
+ EXPECT_TRUE(overscrolledTransform._41 == 0);
+ EXPECT_TRUE(overscrolledTransform._42 != 0);
+
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc,
+ ScreenIntPoint(50, 80), ScreenPoint(-5, -50), mcc->Time());
+ overscrolledTransform =
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting);
+ EXPECT_TRUE(overscrolledTransform._41 == 0);
+ EXPECT_TRUE(overscrolledTransform._42 != 0);
+
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc,
+ ScreenIntPoint(50, 80), ScreenPoint(0, -2), mcc->Time());
+ overscrolledTransform =
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting);
+ EXPECT_TRUE(overscrolledTransform._41 == 0);
+ EXPECT_TRUE(overscrolledTransform._42 != 0);
+
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMEND, apzc,
+ ScreenIntPoint(50, 80), ScreenPoint(0, 0), mcc->Time());
+ overscrolledTransform =
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting);
+ EXPECT_TRUE(overscrolledTransform._41 == 0);
+ EXPECT_TRUE(overscrolledTransform._42 != 0);
+
+ // Check that we recover from overscroll via an animation.
+ ParentLayerPoint expectedScrollOffset(0, 0);
+ SampleAnimationUntilRecoveredFromOverscroll(expectedScrollOffset);
+}
+#endif
+
+#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android
+TEST_F(APZCOverscrollTester, VerticalOnlyOverscrollByPanMomentum) {
+ SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
+
+ // Make the content scrollable only vertically.
+ ScrollMetadata metadata;
+ FrameMetrics& metrics = metadata.GetMetrics();
+ metrics.SetCompositionBounds(ParentLayerRect(0, 0, 100, 100));
+ metrics.SetScrollableRect(CSSRect(0, 0, 100, 1000));
+ // Scrolls the content down a bit.
+ metrics.SetVisualScrollOffset(CSSPoint(0, 50));
+ apzc->SetFrameMetrics(metrics);
+
+ // Scroll up a bit where overscroll will not happen.
+ PanGesture(PanGestureInput::PANGESTURE_START, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, -2), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, -10), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, -2), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_END, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, 0), mcc->Time());
+
+ // Make sure it's not yet overscrolled.
+ EXPECT_TRUE(!apzc->IsOverscrolled());
+
+ // Send pan momentum events including horizontal bits.
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMSTART, apzc,
+ ScreenIntPoint(50, 80), ScreenPoint(0, 0), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc,
+ ScreenIntPoint(50, 80), ScreenPoint(-10, -100), mcc->Time());
+ // Now it's overscrolled.
+ EXPECT_TRUE(apzc->IsOverscrolled());
+
+ AsyncTransformComponentMatrix overscrolledTransform =
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting);
+ // But the overscroll shouldn't happen horizontally.
+ EXPECT_TRUE(overscrolledTransform._41 == 0);
+ // Happens only vertically.
+ EXPECT_TRUE(overscrolledTransform._42 != 0);
+
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc,
+ ScreenIntPoint(50, 80), ScreenPoint(-5, -50), mcc->Time());
+ overscrolledTransform =
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting);
+ EXPECT_TRUE(overscrolledTransform._41 == 0);
+ EXPECT_TRUE(overscrolledTransform._42 != 0);
+
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc,
+ ScreenIntPoint(50, 80), ScreenPoint(0, -2), mcc->Time());
+ overscrolledTransform =
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting);
+ EXPECT_TRUE(overscrolledTransform._41 == 0);
+ EXPECT_TRUE(overscrolledTransform._42 != 0);
+
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMEND, apzc,
+ ScreenIntPoint(50, 80), ScreenPoint(0, 0), mcc->Time());
+ overscrolledTransform =
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting);
+ EXPECT_TRUE(overscrolledTransform._41 == 0);
+ EXPECT_TRUE(overscrolledTransform._42 != 0);
+
+ // Check that we recover from overscroll via an animation.
+ ParentLayerPoint expectedScrollOffset(0, 0);
+ SampleAnimationUntilRecoveredFromOverscroll(expectedScrollOffset);
+}
+#endif
+
+#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android
+TEST_F(APZCOverscrollTester, DisallowOverscrollInSingleLineTextControl) {
+ SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
+
+ // Create a horizontal scrollable frame with `vertical disregarded direction`.
+ ScrollMetadata metadata;
+ FrameMetrics& metrics = metadata.GetMetrics();
+ metrics.SetCompositionBounds(ParentLayerRect(0, 0, 100, 10));
+ metrics.SetScrollableRect(CSSRect(0, 0, 1000, 10));
+ apzc->SetFrameMetrics(metrics);
+ metadata.SetDisregardedDirection(Some(ScrollDirection::eVertical));
+ apzc->NotifyLayersUpdated(metadata, /*aIsFirstPaint=*/false,
+ /*aThisLayerTreeUpdated=*/true);
+
+ // Try to overscroll up and left with pan gestures.
+ PanGesture(PanGestureInput::PANGESTURE_START, apzc, ScreenIntPoint(50, 5),
+ ScreenPoint(-2, -2), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 5),
+ ScreenPoint(-10, -10), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 5),
+ ScreenPoint(-2, -2), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_END, apzc, ScreenIntPoint(50, 5),
+ ScreenPoint(0, 0), mcc->Time());
+
+ // No overscrolling should happen.
+ EXPECT_TRUE(!apzc->IsOverscrolled());
+
+ // Send pan momentum events too.
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMSTART, apzc,
+ ScreenIntPoint(50, 5), ScreenPoint(0, 0), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc,
+ ScreenIntPoint(50, 5), ScreenPoint(-100, -100), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc,
+ ScreenIntPoint(50, 5), ScreenPoint(-50, -50), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc,
+ ScreenIntPoint(50, 5), ScreenPoint(-2, -2), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMEND, apzc,
+ ScreenIntPoint(50, 5), ScreenPoint(0, 0), mcc->Time());
+ // No overscrolling should happen either.
+ EXPECT_TRUE(!apzc->IsOverscrolled());
+}
+#endif
+
+#ifndef MOZ_WIDGET_ANDROID // Maybe fails on Android
+// Tests that horizontal overscroll animation keeps running with vertical
+// pan momentum scrolling.
+TEST_F(APZCOverscrollTester,
+ HorizontalOverscrollAnimationWithVerticalPanMomentumScrolling) {
+ SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
+
+ ScrollMetadata metadata;
+ FrameMetrics& metrics = metadata.GetMetrics();
+ metrics.SetCompositionBounds(ParentLayerRect(0, 0, 100, 100));
+ metrics.SetScrollableRect(CSSRect(0, 0, 1000, 5000));
+ apzc->SetFrameMetrics(metrics);
+
+ // Try to overscroll left with pan gestures.
+ PanGesture(PanGestureInput::PANGESTURE_START, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(-2, 0), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(-10, 0), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(-2, 0), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_END, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, 0), mcc->Time());
+
+ // Make sure we've started an overscroll animation.
+ EXPECT_TRUE(apzc->IsOverscrolled());
+ EXPECT_TRUE(apzc->IsOverscrollAnimationRunning());
+ AsyncTransformComponentMatrix initialOverscrolledTransform =
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting);
+
+ // Send lengthy downward momentums to make sure the overscroll animation
+ // doesn't clobber the momentums scrolling.
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ // The overscroll amount on X axis has started being managed by the overscroll
+ // animation.
+ AsyncTransformComponentMatrix currentOverscrolledTransform =
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting);
+ EXPECT_NE(initialOverscrolledTransform._41, currentOverscrolledTransform._41);
+ // There is no overscroll on Y axis.
+ EXPECT_EQ(currentOverscrolledTransform._42, 0);
+ ParentLayerPoint scrollOffset =
+ apzc->GetCurrentAsyncScrollOffset(AsyncPanZoomController::eForHitTesting);
+ // The scroll offset shouldn't be changed by the overscroll animation.
+ EXPECT_EQ(scrollOffset.y, 0);
+
+ // Simple gesture on the Y axis to ensure that we can send a vertical
+ // momentum scroll
+ PanGesture(PanGestureInput::PANGESTURE_START, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, -2), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, 2), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_END, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, 0), mcc->Time());
+
+ ParentLayerPoint offsetAfterPan =
+ apzc->GetCurrentAsyncScrollOffset(AsyncPanZoomController::eForHitTesting);
+
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMSTART, apzc,
+ ScreenIntPoint(50, 80), ScreenPoint(0, 0), mcc->Time());
+ EXPECT_TRUE(apzc->IsOverscrolled());
+ EXPECT_TRUE(apzc->IsOverscrollAnimationRunning());
+ // The overscroll amount on both axes shouldn't be changed by this pan
+ // momentum start event since the displacement is zero.
+ EXPECT_EQ(
+ currentOverscrolledTransform._41,
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting)._41);
+ EXPECT_EQ(
+ currentOverscrolledTransform._42,
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting)._42);
+
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ // The overscroll amount should be managed by the overscroll animation.
+ EXPECT_NE(
+ currentOverscrolledTransform._41,
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting)._41);
+ scrollOffset =
+ apzc->GetCurrentAsyncScrollOffset(AsyncPanZoomController::eForHitTesting);
+ // Not yet started scrolling.
+ EXPECT_EQ(scrollOffset.y, offsetAfterPan.y);
+ EXPECT_EQ(scrollOffset.x, 0);
+
+ currentOverscrolledTransform =
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting);
+
+ // Send a long pan momentum.
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc,
+ ScreenIntPoint(50, 80), ScreenPoint(0, 200), mcc->Time());
+ EXPECT_TRUE(apzc->IsOverscrolled());
+ EXPECT_TRUE(apzc->IsOverscrollAnimationRunning());
+ // The overscroll amount on X axis shouldn't be changed by this momentum pan.
+ EXPECT_EQ(
+ currentOverscrolledTransform._41,
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting)._41);
+ // Now it started scrolling vertically.
+ scrollOffset =
+ apzc->GetCurrentAsyncScrollOffset(AsyncPanZoomController::eForHitTesting);
+ EXPECT_GT(scrollOffset.y, 0);
+ EXPECT_EQ(scrollOffset.x, 0);
+
+ currentOverscrolledTransform =
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting);
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ // The overscroll on X axis keeps being managed by the overscroll animation.
+ EXPECT_NE(
+ currentOverscrolledTransform._41,
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting)._41);
+ // The scroll offset on Y axis shouldn't be changed by the overscroll
+ // animation.
+ EXPECT_EQ(scrollOffset.y, apzc->GetCurrentAsyncScrollOffset(
+ AsyncPanZoomController::eForHitTesting)
+ .y);
+
+ currentOverscrolledTransform =
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting);
+ scrollOffset =
+ apzc->GetCurrentAsyncScrollOffset(AsyncPanZoomController::eForHitTesting);
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc,
+ ScreenIntPoint(50, 80), ScreenPoint(0, 100), mcc->Time());
+ EXPECT_TRUE(apzc->IsOverscrolled());
+ EXPECT_TRUE(apzc->IsOverscrollAnimationRunning());
+ // The overscroll amount on X axis shouldn't be changed by this momentum pan.
+ EXPECT_EQ(
+ currentOverscrolledTransform._41,
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting)._41);
+ // Scrolling keeps going by momentum.
+ EXPECT_GT(
+ apzc->GetCurrentAsyncScrollOffset(AsyncPanZoomController::eForHitTesting)
+ .y,
+ scrollOffset.y);
+
+ scrollOffset =
+ apzc->GetCurrentAsyncScrollOffset(AsyncPanZoomController::eForHitTesting);
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc,
+ ScreenIntPoint(50, 80), ScreenPoint(0, 10), mcc->Time());
+ EXPECT_TRUE(apzc->IsOverscrolled());
+ EXPECT_TRUE(apzc->IsOverscrollAnimationRunning());
+ // Scrolling keeps going by momentum.
+ EXPECT_GT(
+ apzc->GetCurrentAsyncScrollOffset(AsyncPanZoomController::eForHitTesting)
+ .y,
+ scrollOffset.y);
+
+ currentOverscrolledTransform =
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting);
+ scrollOffset =
+ apzc->GetCurrentAsyncScrollOffset(AsyncPanZoomController::eForHitTesting);
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMEND, apzc,
+ ScreenIntPoint(50, 80), ScreenPoint(0, 0), mcc->Time());
+ EXPECT_TRUE(apzc->IsOverscrolled());
+ EXPECT_TRUE(apzc->IsOverscrollAnimationRunning());
+ // This momentum event doesn't change the scroll offset since its
+ // displacement is zero.
+ EXPECT_EQ(
+ apzc->GetCurrentAsyncScrollOffset(AsyncPanZoomController::eForHitTesting)
+ .y,
+ scrollOffset.y);
+
+ // Check that we recover from the horizontal overscroll via the animation.
+ ParentLayerPoint expectedScrollOffset(0, scrollOffset.y);
+ SampleAnimationUntilRecoveredFromOverscroll(expectedScrollOffset);
+}
+#endif
+
+#ifndef MOZ_WIDGET_ANDROID // Maybe fails on Android
+// Similar to above
+// HorizontalOverscrollAnimationWithVerticalPanMomentumScrolling,
+// but having OverscrollAnimation on both axes initially.
+TEST_F(APZCOverscrollTester,
+ BothAxesOverscrollAnimationWithPanMomentumScrolling) {
+ // TODO: This test currently requires gestures that cause movement on both
+ // axis, which excludes DOMINANT_AXIS locking mode. The gestures should be
+ // broken up into multiple gestures to cause the overscroll.
+ SCOPED_GFX_PREF_INT("apz.axis_lock.mode", 2);
+ SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
+
+ ScrollMetadata metadata;
+ FrameMetrics& metrics = metadata.GetMetrics();
+ metrics.SetCompositionBounds(ParentLayerRect(0, 0, 100, 100));
+ metrics.SetScrollableRect(CSSRect(0, 0, 1000, 5000));
+ apzc->SetFrameMetrics(metrics);
+
+ // Try to overscroll up and left with pan gestures.
+ PanGesture(PanGestureInput::PANGESTURE_START, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(-2, -2), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(-10, -10), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(-2, -2), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_END, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, 0), mcc->Time());
+
+ // Make sure we've started an overscroll animation.
+ EXPECT_TRUE(apzc->IsOverscrolled());
+ EXPECT_TRUE(apzc->IsOverscrollAnimationRunning());
+ AsyncTransformComponentMatrix initialOverscrolledTransform =
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting);
+
+ // Send lengthy downward momentums to make sure the overscroll animation
+ // doesn't clobber the momentums scrolling.
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ // The overscroll amount has started being managed by the overscroll
+ // animation.
+ AsyncTransformComponentMatrix currentOverscrolledTransform =
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting);
+ EXPECT_NE(initialOverscrolledTransform._41, currentOverscrolledTransform._41);
+ EXPECT_NE(initialOverscrolledTransform._42, currentOverscrolledTransform._42);
+
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMSTART, apzc,
+ ScreenIntPoint(50, 80), ScreenPoint(0, 0), mcc->Time());
+ EXPECT_TRUE(apzc->IsOverscrolled());
+ EXPECT_TRUE(apzc->IsOverscrollAnimationRunning());
+ // The overscroll amount on both axes shouldn't be changed by this pan
+ // momentum start event since the displacement is zero.
+ EXPECT_EQ(
+ currentOverscrolledTransform._41,
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting)._41);
+ EXPECT_EQ(
+ currentOverscrolledTransform._42,
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting)._42);
+
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ // Still being managed by the overscroll animation.
+ EXPECT_NE(
+ currentOverscrolledTransform._41,
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting)._41);
+ EXPECT_NE(
+ currentOverscrolledTransform._42,
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting)._42);
+
+ currentOverscrolledTransform =
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting);
+ // Send a long pan momentum.
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc,
+ ScreenIntPoint(50, 80), ScreenPoint(0, 200), mcc->Time());
+ EXPECT_TRUE(apzc->IsOverscrolled());
+ EXPECT_TRUE(apzc->IsOverscrollAnimationRunning());
+ // The overscroll amount on X axis shouldn't be changed by this momentum pan.
+ EXPECT_EQ(
+ currentOverscrolledTransform._41,
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting)._41);
+ // But now the overscroll amount on Y axis should be changed by this momentum
+ // pan.
+ EXPECT_NE(
+ currentOverscrolledTransform._42,
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting)._42);
+ // Actually it's no longer overscrolled.
+ EXPECT_EQ(
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting)._42,
+ 0);
+
+ ParentLayerPoint currentScrollOffset =
+ apzc->GetCurrentAsyncScrollOffset(AsyncPanZoomController::eForHitTesting);
+ // Now it started scrolling.
+ EXPECT_GT(currentScrollOffset.y, 0);
+
+ currentOverscrolledTransform =
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting);
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ // The overscroll on X axis keeps being managed by the overscroll animation.
+ EXPECT_NE(
+ currentOverscrolledTransform._41,
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting)._41);
+ // But the overscroll on Y axis is no longer affected by the overscroll
+ // animation.
+ EXPECT_EQ(
+ currentOverscrolledTransform._42,
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting)._42);
+ // The scroll offset on Y axis shouldn't be changed by the overscroll
+ // animation.
+ EXPECT_EQ(
+ currentScrollOffset.y,
+ apzc->GetCurrentAsyncScrollOffset(AsyncPanZoomController::eForHitTesting)
+ .y);
+
+ currentOverscrolledTransform =
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting);
+ currentScrollOffset =
+ apzc->GetCurrentAsyncScrollOffset(AsyncPanZoomController::eForHitTesting);
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc,
+ ScreenIntPoint(50, 80), ScreenPoint(0, 100), mcc->Time());
+ EXPECT_TRUE(apzc->IsOverscrolled());
+ EXPECT_TRUE(apzc->IsOverscrollAnimationRunning());
+ // The overscroll amount on X axis shouldn't be changed by this momentum pan.
+ EXPECT_EQ(
+ currentOverscrolledTransform._41,
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting)._41);
+ // Keeping no overscrolling on Y axis.
+ EXPECT_EQ(
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting)._42,
+ 0);
+ // Scrolling keeps going by momentum.
+ EXPECT_GT(
+ apzc->GetCurrentAsyncScrollOffset(AsyncPanZoomController::eForHitTesting)
+ .y,
+ currentScrollOffset.y);
+
+ currentScrollOffset =
+ apzc->GetCurrentAsyncScrollOffset(AsyncPanZoomController::eForHitTesting);
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc,
+ ScreenIntPoint(50, 80), ScreenPoint(0, 10), mcc->Time());
+ EXPECT_TRUE(apzc->IsOverscrolled());
+ EXPECT_TRUE(apzc->IsOverscrollAnimationRunning());
+ // Keeping no overscrolling on Y axis.
+ EXPECT_EQ(
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting)._42,
+ 0);
+ // Scrolling keeps going by momentum.
+ EXPECT_GT(
+ apzc->GetCurrentAsyncScrollOffset(AsyncPanZoomController::eForHitTesting)
+ .y,
+ currentScrollOffset.y);
+
+ currentOverscrolledTransform =
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting);
+ currentScrollOffset =
+ apzc->GetCurrentAsyncScrollOffset(AsyncPanZoomController::eForHitTesting);
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMEND, apzc,
+ ScreenIntPoint(50, 80), ScreenPoint(0, 0), mcc->Time());
+ EXPECT_TRUE(apzc->IsOverscrolled());
+ EXPECT_TRUE(apzc->IsOverscrollAnimationRunning());
+ // Keeping no overscrolling on Y axis.
+ EXPECT_EQ(
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting)._42,
+ 0);
+ // This momentum event doesn't change the scroll offset since its
+ // displacement is zero.
+ EXPECT_EQ(
+ apzc->GetCurrentAsyncScrollOffset(AsyncPanZoomController::eForHitTesting)
+ .y,
+ currentScrollOffset.y);
+
+ // Check that we recover from the horizontal overscroll via the animation.
+ ParentLayerPoint expectedScrollOffset(0, currentScrollOffset.y);
+ SampleAnimationUntilRecoveredFromOverscroll(expectedScrollOffset);
+}
+#endif
+
+#ifndef MOZ_WIDGET_ANDROID // Maybe fails on Android
+// This is another variant of
+// HorizontalOverscrollAnimationWithVerticalPanMomentumScrolling. In this test,
+// after a horizontal overscroll animation started, upwards pan moments happen,
+// thus there should be a new vertical overscroll animation in addition to
+// the horizontal one.
+TEST_F(
+ APZCOverscrollTester,
+ VerticalOverscrollAnimationInAdditionToExistingHorizontalOverscrollAnimation) {
+ SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
+
+ ScrollMetadata metadata;
+ FrameMetrics& metrics = metadata.GetMetrics();
+ metrics.SetCompositionBounds(ParentLayerRect(0, 0, 100, 100));
+ metrics.SetScrollableRect(CSSRect(0, 0, 1000, 5000));
+ // Scrolls the content 50px down.
+ metrics.SetVisualScrollOffset(CSSPoint(0, 50));
+ apzc->SetFrameMetrics(metrics);
+
+ // Try to overscroll left with pan gestures.
+ PanGesture(PanGestureInput::PANGESTURE_START, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(-2, 0), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(-10, 0), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(-2, 0), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_END, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, 0), mcc->Time());
+
+ // Make sure we've started an overscroll animation.
+ EXPECT_TRUE(apzc->IsOverscrolled());
+ EXPECT_TRUE(apzc->IsOverscrollAnimationRunning());
+ AsyncTransformComponentMatrix initialOverscrolledTransform =
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting);
+
+ // Send lengthy __upward__ momentums to make sure the overscroll animation
+ // doesn't clobber the momentums scrolling.
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ // The overscroll amount on X axis has started being managed by the overscroll
+ // animation.
+ AsyncTransformComponentMatrix currentOverscrolledTransform =
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting);
+ EXPECT_NE(initialOverscrolledTransform._41, currentOverscrolledTransform._41);
+ // There is no overscroll on Y axis.
+ EXPECT_EQ(
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting)._42,
+ 0);
+ ParentLayerPoint scrollOffset =
+ apzc->GetCurrentAsyncScrollOffset(AsyncPanZoomController::eForHitTesting);
+ // The scroll offset shouldn't be changed by the overscroll animation.
+ EXPECT_EQ(scrollOffset.y, 50);
+
+ // Simple gesture on the Y axis to ensure that we can send a vertical
+ // momentum scroll
+ PanGesture(PanGestureInput::PANGESTURE_START, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, -2), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, 2), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_END, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, 0), mcc->Time());
+
+ ParentLayerPoint offsetAfterPan =
+ apzc->GetCurrentAsyncScrollOffset(AsyncPanZoomController::eForHitTesting);
+
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMSTART, apzc,
+ ScreenIntPoint(50, 80), ScreenPoint(0, 0), mcc->Time());
+ EXPECT_TRUE(apzc->IsOverscrolled());
+ EXPECT_TRUE(apzc->IsOverscrollAnimationRunning());
+ // The overscroll amount on both axes shouldn't be changed by this pan
+ // momentum start event since the displacement is zero.
+ EXPECT_EQ(
+ currentOverscrolledTransform._41,
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting)._41);
+ EXPECT_EQ(
+ currentOverscrolledTransform._42,
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting)._42);
+
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ // The overscroll amount should be managed by the overscroll animation.
+ EXPECT_NE(
+ currentOverscrolledTransform._41,
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting)._41);
+ scrollOffset =
+ apzc->GetCurrentAsyncScrollOffset(AsyncPanZoomController::eForHitTesting);
+ // Not yet started scrolling.
+ EXPECT_EQ(scrollOffset.y, offsetAfterPan.y);
+ EXPECT_EQ(scrollOffset.x, 0);
+
+ currentOverscrolledTransform =
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting);
+
+ // Send a long pan momentum.
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc,
+ ScreenIntPoint(50, 80), ScreenPoint(0, -200), mcc->Time());
+ EXPECT_TRUE(apzc->IsOverscrolled());
+ EXPECT_TRUE(apzc->IsOverscrollAnimationRunning());
+ // The overscroll amount on X axis shouldn't be changed by this momentum pan.
+ EXPECT_EQ(
+ currentOverscrolledTransform._41,
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting)._41);
+ // Now it started scrolling vertically.
+ scrollOffset =
+ apzc->GetCurrentAsyncScrollOffset(AsyncPanZoomController::eForHitTesting);
+ EXPECT_EQ(scrollOffset.y, 0);
+ EXPECT_EQ(scrollOffset.x, 0);
+ // Actually it's also vertically overscrolled.
+ EXPECT_GT(
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting)._42,
+ 0);
+
+ currentOverscrolledTransform =
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting);
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ // The overscroll on X axis keeps being managed by the overscroll animation.
+ EXPECT_NE(
+ currentOverscrolledTransform._41,
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting)._41);
+ // The overscroll on Y Axis hasn't been changed by the overscroll animation at
+ // this moment, sine the last displacement was consumed in the last pan
+ // momentum.
+ EXPECT_EQ(
+ currentOverscrolledTransform._42,
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting)._42);
+
+ currentOverscrolledTransform =
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting);
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc,
+ ScreenIntPoint(50, 80), ScreenPoint(0, -100), mcc->Time());
+ EXPECT_TRUE(apzc->IsOverscrolled());
+ EXPECT_TRUE(apzc->IsOverscrollAnimationRunning());
+ // The overscroll amount on X axis shouldn't be changed by this momentum pan.
+ EXPECT_EQ(
+ currentOverscrolledTransform._41,
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting)._41);
+ // Now the overscroll amount on Y axis shouldn't be changed by this momentum
+ // pan either.
+ EXPECT_EQ(
+ currentOverscrolledTransform._42,
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting)._42);
+
+ currentOverscrolledTransform =
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting);
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ EXPECT_NE(
+ currentOverscrolledTransform._41,
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting)._41);
+ // And now the overscroll on Y Axis should be also managed by the overscroll
+ // animation.
+ EXPECT_NE(
+ currentOverscrolledTransform._42,
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting)._42);
+
+ currentOverscrolledTransform =
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting);
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc,
+ ScreenIntPoint(50, 80), ScreenPoint(0, -10), mcc->Time());
+ EXPECT_TRUE(apzc->IsOverscrolled());
+ EXPECT_TRUE(apzc->IsOverscrollAnimationRunning());
+ // The overscroll amount on both axes shouldn't be changed by momentum event.
+ EXPECT_EQ(
+ currentOverscrolledTransform._41,
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting)._41);
+ EXPECT_EQ(
+ currentOverscrolledTransform._42,
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting)._42);
+
+ currentOverscrolledTransform =
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting);
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMEND, apzc,
+ ScreenIntPoint(50, 80), ScreenPoint(0, 0), mcc->Time());
+ EXPECT_TRUE(apzc->IsOverscrolled());
+ EXPECT_TRUE(apzc->IsOverscrollAnimationRunning());
+
+ // Check that we recover from the horizontal overscroll via the animation.
+ ParentLayerPoint expectedScrollOffset(0, 0);
+ SampleAnimationUntilRecoveredFromOverscroll(expectedScrollOffset);
+}
+#endif
+
+#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android
+TEST_F(APZCOverscrollTester, OverscrollByPanGesturesInterruptedByReflowZoom) {
+ SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
+ SCOPED_GFX_PREF_INT("mousewheel.with_control.action", 3); // reflow zoom.
+
+ // A sanity check that pan gestures with ctrl modifier will not be handled by
+ // APZ.
+ PanGestureInput panInput(PanGestureInput::PANGESTURE_START, mcc->Time(),
+ ScreenIntPoint(5, 5), ScreenPoint(0, -2),
+ MODIFIER_CONTROL);
+ WidgetWheelEvent wheelEvent = panInput.ToWidgetEvent(nullptr);
+ EXPECT_FALSE(APZInputBridge::ActionForWheelEvent(&wheelEvent).isSome());
+
+ ScrollableLayerGuid rootGuid = CreateSimpleRootScrollableForWebRender();
+ RefPtr<AsyncPanZoomController> apzc =
+ tm->GetTargetAPZC(rootGuid.mLayersId, rootGuid.mScrollId);
+
+ PanGesture(PanGestureInput::PANGESTURE_START, tm, ScreenIntPoint(50, 80),
+ ScreenPoint(0, -2), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_PAN, tm, ScreenIntPoint(50, 80),
+ ScreenPoint(0, -10), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ // Make sure overscrolling has started.
+ EXPECT_TRUE(apzc->IsOverscrolled());
+
+ // Press ctrl until PANGESTURE_END.
+ PanGestureWithModifiers(PanGestureInput::PANGESTURE_PAN, MODIFIER_CONTROL, tm,
+ ScreenIntPoint(50, 80), ScreenPoint(0, -2),
+ mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ // At this moment (i.e. PANGESTURE_PAN), still in overscrolling state.
+ EXPECT_TRUE(apzc->IsOverscrolled());
+
+ PanGestureWithModifiers(PanGestureInput::PANGESTURE_END, MODIFIER_CONTROL, tm,
+ ScreenIntPoint(50, 80), ScreenPoint(0, 0),
+ mcc->Time());
+ // The overscrolling state should have been restored.
+ EXPECT_TRUE(!apzc->IsOverscrolled());
+}
+#endif
+
+#ifndef MOZ_WIDGET_ANDROID // Only applies to GenericOverscrollEffect
+TEST_F(APZCOverscrollTester, SmoothTransitionFromPanToAnimation) {
+ SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
+
+ ScrollMetadata metadata;
+ FrameMetrics& metrics = metadata.GetMetrics();
+ metrics.SetCompositionBounds(ParentLayerRect(0, 0, 100, 100));
+ metrics.SetScrollableRect(CSSRect(0, 0, 100, 1000));
+ // Start scrolled down to y=500px.
+ metrics.SetVisualScrollOffset(CSSPoint(0, 500));
+ apzc->SetFrameMetrics(metrics);
+
+ int frameLength = 10; // milliseconds; 10 to keep the math simple
+ float panVelocity = 10; // pixels per millisecond
+ int panPixelsPerFrame = frameLength * panVelocity; // 100 pixels per frame
+
+ ScreenIntPoint panPoint(50, 50);
+ PanGesture(PanGestureInput::PANGESTURE_START, apzc, panPoint,
+ ScreenPoint(0, -1), mcc->Time());
+ // Pan up for 6 frames at 100 pixels per frame. This should reduce
+ // the vertical scroll offset from 500 to 0, and get us into overscroll.
+ for (int i = 0; i < 6; ++i) {
+ mcc->AdvanceByMillis(frameLength);
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, panPoint,
+ ScreenPoint(0, -panPixelsPerFrame), mcc->Time());
+ }
+ EXPECT_TRUE(apzc->IsOverscrolled());
+
+ // Pan further into overscroll at the same input velocity, enough
+ // for the frames while we are in overscroll to dominate the computation
+ // in the velocity tracker.
+ // Importantly, while the input velocity is still 100 pixels per frame,
+ // in the overscrolled state the page only visual moves by at most 8 pixels
+ // per frame.
+ int frames = StaticPrefs::apz_velocity_relevance_time_ms() / frameLength;
+ for (int i = 0; i < frames; ++i) {
+ mcc->AdvanceByMillis(frameLength);
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, panPoint,
+ ScreenPoint(0, -panPixelsPerFrame), mcc->Time());
+ }
+ EXPECT_TRUE(apzc->IsOverscrolled());
+
+ // End the pan, allowing an overscroll animation to start.
+ mcc->AdvanceByMillis(frameLength);
+ PanGesture(PanGestureInput::PANGESTURE_END, apzc, panPoint, ScreenPoint(0, 0),
+ mcc->Time());
+ EXPECT_TRUE(apzc->IsOverscrollAnimationRunning());
+
+ // Check that the velocity reflects the actual movement (no more than 8
+ // pixels/frame ==> 0.8 pixels per millisecond), not the input velocity
+ // (100 pixels/frame ==> 10 pixels per millisecond). This ensures that
+ // the transition from the pan to the animation appears smooth.
+ // (Note: velocities are negative since they are upwards.)
+ EXPECT_LT(apzc->GetVelocityVector().y, 0);
+ EXPECT_GT(apzc->GetVelocityVector().y, -0.8);
+}
+#endif
+
+#ifndef MOZ_WIDGET_ANDROID // Only applies to GenericOverscrollEffect
+TEST_F(APZCOverscrollTester, NoOverscrollForMousewheel) {
+ SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
+
+ ScrollMetadata metadata;
+ FrameMetrics& metrics = metadata.GetMetrics();
+ metrics.SetCompositionBounds(ParentLayerRect(0, 0, 100, 100));
+ metrics.SetScrollableRect(CSSRect(0, 0, 100, 1000));
+ // Start scrolled down just a few pixels from the top.
+ metrics.SetVisualScrollOffset(CSSPoint(0, 3));
+ // Set line and page scroll amounts. Otherwise, even though Wheel() uses
+ // SCROLLDELTA_PIXEL, the wheel handling code will get confused by things
+ // like the "don't scroll more than one page" check.
+ metadata.SetPageScrollAmount(LayoutDeviceIntSize(50, 100));
+ metadata.SetLineScrollAmount(LayoutDeviceIntSize(5, 10));
+ apzc->SetScrollMetadata(metadata);
+
+ // Send a wheel with enough delta to scrollto y=0 *and* overscroll.
+ Wheel(apzc, ScreenIntPoint(10, 10), ScreenPoint(0, -10), mcc->Time());
+
+ // Check that we did not actually go into overscroll.
+ EXPECT_FALSE(apzc->IsOverscrolled());
+}
+#endif
+
+#ifndef MOZ_WIDGET_ANDROID // Only applies to GenericOverscrollEffect
+TEST_F(APZCOverscrollTester, ClickWhileOverscrolled) {
+ SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
+
+ ScrollMetadata metadata;
+ FrameMetrics& metrics = metadata.GetMetrics();
+ metrics.SetCompositionBounds(ParentLayerRect(0, 0, 100, 100));
+ metrics.SetScrollableRect(CSSRect(0, 0, 100, 1000));
+ metrics.SetVisualScrollOffset(CSSPoint(0, 0));
+ apzc->SetFrameMetrics(metrics);
+
+ // Pan into overscroll at the top.
+ ScreenIntPoint panPoint(50, 50);
+ PanGesture(PanGestureInput::PANGESTURE_START, apzc, panPoint,
+ ScreenPoint(0, -1), mcc->Time());
+ mcc->AdvanceByMillis(10);
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, panPoint,
+ ScreenPoint(0, -100), mcc->Time());
+ EXPECT_TRUE(apzc->IsOverscrolled());
+ EXPECT_TRUE(apzc->GetOverscrollAmount().y < 0); // overscrolled at top
+
+ // End the pan. This should start an overscroll animation.
+ mcc->AdvanceByMillis(10);
+ PanGesture(PanGestureInput::PANGESTURE_END, apzc, panPoint, ScreenPoint(0, 0),
+ mcc->Time());
+ EXPECT_TRUE(apzc->GetOverscrollAmount().y < 0); // overscrolled at top
+ EXPECT_TRUE(apzc->IsOverscrollAnimationRunning());
+
+ // Send a mouse-down. This should interrupt the animation but not relieve
+ // overscroll yet.
+ ParentLayerPoint overscrollBefore = apzc->GetOverscrollAmount();
+ MouseDown(apzc, panPoint, mcc->Time());
+ EXPECT_FALSE(apzc->IsOverscrollAnimationRunning());
+ EXPECT_EQ(overscrollBefore, apzc->GetOverscrollAmount());
+
+ // Send a mouse-up. This should start an overscroll animation again.
+ MouseUp(apzc, panPoint, mcc->Time());
+ EXPECT_TRUE(apzc->IsOverscrollAnimationRunning());
+
+ SampleAnimationUntilRecoveredFromOverscroll(ParentLayerPoint(0, 0));
+}
+#endif
+
+#ifndef MOZ_WIDGET_ANDROID // Only applies to GenericOverscrollEffect
+TEST_F(APZCOverscrollTester, DynamicallyLoadingContent) {
+ SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
+
+ ScrollMetadata metadata;
+ FrameMetrics& metrics = metadata.GetMetrics();
+ metrics.SetCompositionBounds(ParentLayerRect(0, 0, 100, 100));
+ metrics.SetScrollableRect(CSSRect(0, 0, 100, 1000));
+ metrics.SetVisualScrollOffset(CSSPoint(0, 0));
+ apzc->SetFrameMetrics(metrics);
+
+ // Pan to the bottom of the page, and further, into overscroll.
+ ScreenIntPoint panPoint(50, 50);
+ PanGesture(PanGestureInput::PANGESTURE_START, apzc, panPoint,
+ ScreenPoint(0, 1), mcc->Time());
+ for (int i = 0; i < 12; ++i) {
+ mcc->AdvanceByMillis(10);
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, panPoint,
+ ScreenPoint(0, 100), mcc->Time());
+ }
+ EXPECT_TRUE(apzc->IsOverscrolled());
+ EXPECT_TRUE(apzc->GetOverscrollAmount().y > 0); // overscrolled at bottom
+
+ // Grow the scrollable rect at the bottom, simulating the page loading content
+ // dynamically.
+ CSSRect scrollableRect = metrics.GetScrollableRect();
+ scrollableRect.height += 500;
+ metrics.SetScrollableRect(scrollableRect);
+ apzc->NotifyLayersUpdated(metadata, false, true);
+
+ // Check that the modified scrollable rect cleared the overscroll.
+ EXPECT_FALSE(apzc->IsOverscrolled());
+
+ // Pan back up to the top, and further, into overscroll.
+ PanGesture(PanGestureInput::PANGESTURE_START, apzc, panPoint,
+ ScreenPoint(0, -1), mcc->Time());
+ for (int i = 0; i < 12; ++i) {
+ mcc->AdvanceByMillis(10);
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, panPoint,
+ ScreenPoint(0, -100), mcc->Time());
+ }
+ EXPECT_TRUE(apzc->IsOverscrolled());
+ ParentLayerPoint overscrollAmount = apzc->GetOverscrollAmount();
+ EXPECT_TRUE(overscrollAmount.y < 0); // overscrolled at top
+
+ // Grow the scrollable rect at the bottom again.
+ scrollableRect = metrics.GetScrollableRect();
+ scrollableRect.height += 500;
+ metrics.SetScrollableRect(scrollableRect);
+ apzc->NotifyLayersUpdated(metadata, false, true);
+
+ // Check that the modified scrollable rect did NOT clear overscroll at the
+ // top.
+ EXPECT_TRUE(apzc->IsOverscrolled());
+ EXPECT_EQ(overscrollAmount,
+ apzc->GetOverscrollAmount()); // overscroll did not change at all
+}
+#endif
+
+#ifndef MOZ_WIDGET_ANDROID // Only applies to GenericOverscrollEffect
+TEST_F(APZCOverscrollTester, SmallAmountOfOverscroll) {
+ SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
+
+ ScrollMetadata metadata;
+ FrameMetrics& metrics = metadata.GetMetrics();
+ metrics.SetCompositionBounds(ParentLayerRect(0, 0, 100, 100));
+ metrics.SetScrollableRect(CSSRect(0, 0, 100, 1000));
+
+ // Do vertical overscroll first.
+ ScreenIntPoint panPoint(50, 50);
+ PanGesture(PanGestureInput::PANGESTURE_START, apzc, panPoint,
+ ScreenPoint(0, -10), mcc->Time());
+ mcc->AdvanceByMillis(10);
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, panPoint,
+ ScreenPoint(0, -10), mcc->Time());
+ mcc->AdvanceByMillis(10);
+ PanGesture(PanGestureInput::PANGESTURE_END, apzc, panPoint, ScreenPoint(0, 0),
+ mcc->Time());
+ mcc->AdvanceByMillis(10);
+
+ // Then do small horizontal overscroll which will be considered as "finished"
+ // by our overscroll animation physics model.
+ PanGesture(PanGestureInput::PANGESTURE_START, apzc, panPoint,
+ ScreenPoint(-0.1, 0), mcc->Time());
+ mcc->AdvanceByMillis(10);
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, panPoint,
+ ScreenPoint(-0.2, 0), mcc->Time());
+ mcc->AdvanceByMillis(10);
+ PanGesture(PanGestureInput::PANGESTURE_END, apzc, panPoint, ScreenPoint(0, 0),
+ mcc->Time());
+ mcc->AdvanceByMillis(10);
+
+ EXPECT_TRUE(apzc->IsOverscrolled());
+ EXPECT_TRUE(apzc->GetOverscrollAmount().y < 0); // overscrolled at top
+ EXPECT_TRUE(apzc->GetOverscrollAmount().x < 0); // and overscrolled at left
+
+ // Then do vertical scroll.
+ PanGesture(PanGestureInput::PANGESTURE_START, apzc, panPoint,
+ ScreenPoint(0, 10), mcc->Time());
+ mcc->AdvanceByMillis(10);
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, panPoint,
+ ScreenPoint(0, 100), mcc->Time());
+ mcc->AdvanceByMillis(10);
+ PanGesture(PanGestureInput::PANGESTURE_END, apzc, panPoint, ScreenPoint(0, 0),
+ mcc->Time());
+
+ ParentLayerPoint scrollOffset =
+ apzc->GetCurrentAsyncScrollOffset(AsyncPanZoomController::eForHitTesting);
+ EXPECT_GT(scrollOffset.y, 0); // Make sure the vertical scroll offset is
+ // greater than zero.
+
+ // The small horizontal overscroll amount should be restored to zero.
+ ParentLayerPoint expectedScrollOffset(0, scrollOffset.y);
+ SampleAnimationUntilRecoveredFromOverscroll(expectedScrollOffset);
+}
+#endif
+
+#ifdef MOZ_WIDGET_ANDROID // Only applies to WidgetOverscrollEffect
+TEST_F(APZCOverscrollTester, StuckInOverscroll_Bug1786452) {
+ SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
+
+ ScrollMetadata metadata;
+ FrameMetrics& metrics = metadata.GetMetrics();
+ metrics.SetCompositionBounds(ParentLayerRect(0, 0, 100, 100));
+ metrics.SetScrollableRect(CSSRect(0, 0, 100, 1000));
+
+ // Over the course of the test, expect one or more calls to
+ // UpdateOverscrollOffset(), followed by a call to UpdateOverscrollVelocity().
+ // The latter ensures the widget has a chance to end its overscroll effect.
+ InSequence s;
+ EXPECT_CALL(*mcc, UpdateOverscrollOffset(_, _, _, _)).Times(AtLeast(1));
+ EXPECT_CALL(*mcc, UpdateOverscrollVelocity(_, _, _, _)).Times(1);
+
+ // Pan into overscroll, keeping the finger down
+ ScreenIntPoint startPoint(10, 500);
+ ScreenIntPoint endPoint(10, 10);
+ Pan(apzc, startPoint, endPoint, PanOptions::KeepFingerDown);
+ EXPECT_TRUE(apzc->IsOverscrolled());
+
+ // Linger a while to cause the velocity to drop to very low or zero
+ mcc->AdvanceByMillis(100);
+ TouchMove(apzc, endPoint, mcc->Time());
+ EXPECT_LT(apzc->GetVelocityVector().Length(),
+ StaticPrefs::apz_fling_min_velocity_threshold());
+ EXPECT_TRUE(apzc->IsOverscrolled());
+
+ // Lift the finger
+ mcc->AdvanceByMillis(20);
+ TouchUp(apzc, endPoint, mcc->Time());
+ EXPECT_FALSE(apzc->IsOverscrolled());
+}
+#endif
+
+class APZCOverscrollTesterMock : public APZCTreeManagerTester {
+ public:
+ APZCOverscrollTesterMock() { CreateMockHitTester(); }
+
+ UniquePtr<ScopedLayerTreeRegistration> registration;
+ TestAsyncPanZoomController* rootApzc;
+};
+
+#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android
+TEST_F(APZCOverscrollTesterMock, OverscrollHandoff) {
+ SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
+
+ const char* treeShape = "x(x)";
+ LayerIntRegion layerVisibleRegion[] = {LayerIntRect(0, 0, 100, 100),
+ LayerIntRect(0, 0, 100, 50)};
+ CreateScrollData(treeShape, layerVisibleRegion);
+ SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID,
+ CSSRect(0, 0, 200, 200));
+ SetScrollableFrameMetrics(layers[1], ScrollableLayerGuid::START_SCROLL_ID + 1,
+ // same size as the visible region so that
+ // the container is not scrollable in any directions
+ // actually. This is simulating overflow: hidden
+ // iframe document in Fission, though we don't set
+ // a different layers id.
+ CSSRect(0, 0, 100, 50));
+
+ SetScrollHandoff(layers[1], root);
+
+ registration = MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc);
+ UpdateHitTestingTree();
+ rootApzc = ApzcOf(root);
+ rootApzc->GetFrameMetrics().SetIsRootContent(true);
+
+ // A pan gesture on the child scroller (which is not scrollable though).
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
+ PanGesture(PanGestureInput::PANGESTURE_START, manager, ScreenIntPoint(50, 20),
+ ScreenPoint(0, -2), mcc->Time());
+ EXPECT_TRUE(rootApzc->IsOverscrolled());
+}
+#endif
+
+#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android
+TEST_F(APZCOverscrollTesterMock, VerticalOverscrollHandoffToScrollableRoot) {
+ SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
+
+ // Create a layer tree having two vertical scrollable layers.
+ const char* treeShape = "x(x)";
+ LayerIntRegion layerVisibleRegion[] = {LayerIntRect(0, 0, 100, 100),
+ LayerIntRect(0, 0, 100, 50)};
+ CreateScrollData(treeShape, layerVisibleRegion);
+ SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID,
+ CSSRect(0, 0, 100, 200));
+ SetScrollableFrameMetrics(layers[1], ScrollableLayerGuid::START_SCROLL_ID + 1,
+ CSSRect(0, 0, 100, 200));
+
+ SetScrollHandoff(layers[1], root);
+
+ registration = MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc);
+ UpdateHitTestingTree();
+ rootApzc = ApzcOf(root);
+ rootApzc->GetFrameMetrics().SetIsRootContent(true);
+
+ // A vertical pan gesture on the child scroller which will be handed off to
+ // the root APZC.
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
+ PanGesture(PanGestureInput::PANGESTURE_START, manager, ScreenIntPoint(50, 20),
+ ScreenPoint(0, -2), mcc->Time());
+ EXPECT_TRUE(rootApzc->IsOverscrolled());
+ EXPECT_FALSE(ApzcOf(layers[1])->IsOverscrolled());
+}
+#endif
+
+#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android
+TEST_F(APZCOverscrollTesterMock, NoOverscrollHandoffToNonScrollableRoot) {
+ SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
+
+ // Create a layer tree having non-scrollable root and a vertical scrollable
+ // child.
+ const char* treeShape = "x(x)";
+ LayerIntRegion layerVisibleRegion[] = {LayerIntRect(0, 0, 100, 100),
+ LayerIntRect(0, 0, 100, 50)};
+ CreateScrollData(treeShape, layerVisibleRegion);
+ SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID,
+ CSSRect(0, 0, 100, 100));
+ SetScrollableFrameMetrics(layers[1], ScrollableLayerGuid::START_SCROLL_ID + 1,
+ CSSRect(0, 0, 100, 200));
+
+ SetScrollHandoff(layers[1], root);
+
+ registration = MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc);
+ UpdateHitTestingTree();
+ rootApzc = ApzcOf(root);
+ rootApzc->GetFrameMetrics().SetIsRootContent(true);
+
+ // A vertical pan gesture on the child scroller which should not be handed
+ // off the root APZC.
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
+ PanGesture(PanGestureInput::PANGESTURE_START, manager, ScreenIntPoint(50, 20),
+ ScreenPoint(0, -2), mcc->Time());
+ EXPECT_FALSE(rootApzc->IsOverscrolled());
+ EXPECT_TRUE(ApzcOf(layers[1])->IsOverscrolled());
+}
+#endif
+
+#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android
+TEST_F(APZCOverscrollTesterMock, NoOverscrollHandoffOrthogonalPanGesture) {
+ SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
+
+ // Create a layer tree having horizontal scrollable root and a vertical
+ // scrollable child.
+ const char* treeShape = "x(x)";
+ LayerIntRegion layerVisibleRegion[] = {LayerIntRect(0, 0, 100, 100),
+ LayerIntRect(0, 0, 100, 50)};
+ CreateScrollData(treeShape, layerVisibleRegion);
+ SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID,
+ CSSRect(0, 0, 200, 100));
+ SetScrollableFrameMetrics(layers[1], ScrollableLayerGuid::START_SCROLL_ID + 1,
+ CSSRect(0, 0, 100, 200));
+
+ SetScrollHandoff(layers[1], root);
+
+ registration = MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc);
+ UpdateHitTestingTree();
+ rootApzc = ApzcOf(root);
+ rootApzc->GetFrameMetrics().SetIsRootContent(true);
+
+ // A vertical pan gesture on the child scroller which should not be handed
+ // off the root APZC because the root APZC is not scrollable vertically.
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
+ PanGesture(PanGestureInput::PANGESTURE_START, manager, ScreenIntPoint(50, 20),
+ ScreenPoint(0, -2), mcc->Time());
+ EXPECT_FALSE(rootApzc->IsOverscrolled());
+ EXPECT_TRUE(ApzcOf(layers[1])->IsOverscrolled());
+}
+#endif
+
+#ifndef MOZ_WIDGET_ANDROID // Only applies to GenericOverscrollEffect
+TEST_F(APZCOverscrollTesterMock,
+ RetriggerCancelledOverscrollAnimationByNewPanGesture) {
+ SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
+
+ // Create a layer tree having vertical scrollable root and a horizontal
+ // scrollable child.
+ const char* treeShape = "x(x)";
+ LayerIntRegion layerVisibleRegion[] = {LayerIntRect(0, 0, 100, 100),
+ LayerIntRect(0, 0, 100, 50)};
+ CreateScrollData(treeShape, layerVisibleRegion);
+ SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID,
+ CSSRect(0, 0, 100, 200));
+ SetScrollableFrameMetrics(layers[1], ScrollableLayerGuid::START_SCROLL_ID + 1,
+ CSSRect(0, 0, 200, 50));
+
+ SetScrollHandoff(layers[1], root);
+
+ registration = MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc);
+ UpdateHitTestingTree();
+ rootApzc = ApzcOf(root);
+ rootApzc->GetFrameMetrics().SetIsRootContent(true);
+
+ ScreenIntPoint panPoint(50, 20);
+ // A vertical pan gesture on the child scroller which should be handed off the
+ // root APZC.
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
+ PanGesture(PanGestureInput::PANGESTURE_START, manager, panPoint,
+ ScreenPoint(0, -2), mcc->Time());
+ mcc->AdvanceByMillis(10);
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
+ PanGesture(PanGestureInput::PANGESTURE_PAN, manager, panPoint,
+ ScreenPoint(0, -10), mcc->Time());
+ mcc->AdvanceByMillis(10);
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
+ PanGesture(PanGestureInput::PANGESTURE_END, manager, panPoint,
+ ScreenPoint(0, 0), mcc->Time());
+
+ // The root APZC should be overscrolled and the child APZC should not be.
+ EXPECT_TRUE(rootApzc->IsOverscrolled());
+ EXPECT_FALSE(ApzcOf(layers[1])->IsOverscrolled());
+
+ mcc->AdvanceByMillis(10);
+
+ // Make sure the root APZC is still overscrolled.
+ EXPECT_TRUE(rootApzc->IsOverscrolled());
+
+ // Start a new horizontal pan gesture on the child scroller which should be
+ // handled by the child APZC now.
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
+ APZEventResult result = PanGesture(PanGestureInput::PANGESTURE_START, manager,
+ panPoint, ScreenPoint(-2, 0), mcc->Time());
+ // The above horizontal pan start event was flagged as "this event may trigger
+ // swipe" and either the root scrollable frame or the horizontal child
+ // scrollable frame is not scrollable in the pan start direction, thus the pan
+ // start event run into the short circuit path for swipe-to-navigation in
+ // InputQueue::ReceivePanGestureInput, which means it's waiting for the
+ // content response, so we need to respond explicitly here.
+ manager->ContentReceivedInputBlock(result.mInputBlockId, false);
+ mcc->AdvanceByMillis(10);
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
+ PanGesture(PanGestureInput::PANGESTURE_PAN, manager, panPoint,
+ ScreenPoint(-10, 0), mcc->Time());
+ mcc->AdvanceByMillis(10);
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
+ PanGesture(PanGestureInput::PANGESTURE_END, manager, panPoint,
+ ScreenPoint(0, 0), mcc->Time());
+
+ // Now both APZCs should be overscrolled.
+ EXPECT_TRUE(rootApzc->IsOverscrolled());
+ EXPECT_TRUE(ApzcOf(layers[1])->IsOverscrolled());
+
+ // Sample all animations until all of them have been finished.
+ while (SampleAnimationsOnce())
+ ;
+
+ // After the animations finished, all overscrolled states should have been
+ // restored.
+ EXPECT_FALSE(rootApzc->IsOverscrolled());
+ EXPECT_FALSE(ApzcOf(layers[1])->IsOverscrolled());
+}
+#endif
+
+#ifndef MOZ_WIDGET_ANDROID // Only applies to GenericOverscrollEffect
+TEST_F(APZCOverscrollTesterMock, RetriggeredOverscrollAnimationVelocity) {
+ SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
+
+ // Setup two nested vertical scrollable frames.
+ const char* treeShape = "x(x)";
+ LayerIntRegion layerVisibleRegion[] = {LayerIntRect(0, 0, 100, 100),
+ LayerIntRect(0, 0, 100, 50)};
+ CreateScrollData(treeShape, layerVisibleRegion);
+ SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID,
+ CSSRect(0, 0, 100, 200));
+ SetScrollableFrameMetrics(layers[1], ScrollableLayerGuid::START_SCROLL_ID + 1,
+ CSSRect(0, 0, 100, 200));
+
+ SetScrollHandoff(layers[1], root);
+
+ registration = MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc);
+ UpdateHitTestingTree();
+ rootApzc = ApzcOf(root);
+ rootApzc->GetFrameMetrics().SetIsRootContent(true);
+
+ ScreenIntPoint panPoint(50, 20);
+ // A vertical upward pan gesture on the child scroller which should be handed
+ // off the root APZC.
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
+ PanGesture(PanGestureInput::PANGESTURE_START, manager, panPoint,
+ ScreenPoint(0, -2), mcc->Time());
+ mcc->AdvanceByMillis(10);
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
+ PanGesture(PanGestureInput::PANGESTURE_PAN, manager, panPoint,
+ ScreenPoint(0, -10), mcc->Time());
+ mcc->AdvanceByMillis(10);
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
+ PanGesture(PanGestureInput::PANGESTURE_END, manager, panPoint,
+ ScreenPoint(0, 0), mcc->Time());
+
+ // The root APZC should be overscrolled and the child APZC should not be.
+ EXPECT_TRUE(rootApzc->IsOverscrolled());
+ EXPECT_FALSE(ApzcOf(layers[1])->IsOverscrolled());
+
+ mcc->AdvanceByMillis(10);
+
+ // Make sure the root APZC is still overscrolled and there's an overscroll
+ // animation.
+ EXPECT_TRUE(rootApzc->IsOverscrolled());
+ EXPECT_TRUE(rootApzc->IsOverscrollAnimationRunning());
+
+ // And make sure the overscroll animation's velocity is a certain amount in
+ // the upward direction.
+ EXPECT_LT(rootApzc->GetVelocityVector().y, 0);
+
+ // Start a new downward pan gesture on the child scroller which
+ // should be handled by the child APZC now.
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
+ PanGesture(PanGestureInput::PANGESTURE_START, manager, panPoint,
+ ScreenPoint(0, 2), mcc->Time());
+ mcc->AdvanceByMillis(10);
+ // The new pan-start gesture stops the overscroll animation at this moment.
+ EXPECT_TRUE(!rootApzc->IsOverscrollAnimationRunning());
+
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
+ PanGesture(PanGestureInput::PANGESTURE_PAN, manager, panPoint,
+ ScreenPoint(0, 10), mcc->Time());
+ mcc->AdvanceByMillis(10);
+ // There's no overscroll animation yet even if the root APZC is still
+ // overscrolled.
+ EXPECT_TRUE(!rootApzc->IsOverscrollAnimationRunning());
+ EXPECT_TRUE(rootApzc->IsOverscrolled());
+
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
+ PanGesture(PanGestureInput::PANGESTURE_END, manager, panPoint,
+ ScreenPoint(0, 10), mcc->Time());
+
+ // Now an overscroll animation should have been triggered by the pan-end
+ // gesture.
+ EXPECT_TRUE(rootApzc->IsOverscrollAnimationRunning());
+ EXPECT_TRUE(rootApzc->IsOverscrolled());
+ // And the newly created overscroll animation's positions should never exceed
+ // 0.
+ while (SampleAnimationsOnce()) {
+ EXPECT_LE(rootApzc->GetOverscrollAmount().y, 0);
+ }
+}
+#endif
+
+#ifndef MOZ_WIDGET_ANDROID // Only applies to GenericOverscrollEffect
+TEST_F(APZCOverscrollTesterMock, OverscrollIntoPreventDefault) {
+ SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
+
+ const char* treeShape = "x";
+ LayerIntRegion layerVisibleRegions[] = {LayerIntRect(0, 0, 100, 100)};
+ CreateScrollData(treeShape, layerVisibleRegions);
+ SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID,
+ CSSRect(0, 0, 100, 200));
+
+ registration = MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc);
+ UpdateHitTestingTree();
+ rootApzc = ApzcOf(root);
+
+ // Start a pan gesture a few pixels below the 20px DTC region.
+ ScreenIntPoint cursorLocation(10, 25);
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ APZEventResult result =
+ PanGesture(PanGestureInput::PANGESTURE_START, manager, cursorLocation,
+ ScreenPoint(0, -2), mcc->Time());
+
+ // At this point, we should be overscrolled.
+ EXPECT_TRUE(rootApzc->IsOverscrolled());
+
+ // Pan further, until the DTC region is under the cursor.
+ // Note that, due to ApplyResistance(), we need a large input delta to cause a
+ // visual transform enough to bridge the 5px to the DTC region.
+ mcc->AdvanceByMillis(10);
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_PAN, manager, cursorLocation,
+ ScreenPoint(0, -100), mcc->Time());
+
+ // At this point, we are still overscrolled. Record the overscroll amount.
+ EXPECT_TRUE(rootApzc->IsOverscrolled());
+ float overscrollY = rootApzc->GetOverscrollAmount().y;
+
+ // Send a content response with preventDefault = true.
+ manager->SetAllowedTouchBehavior(result.mInputBlockId,
+ {AllowedTouchBehavior::VERTICAL_PAN});
+ manager->SetTargetAPZC(result.mInputBlockId, {result.mTargetGuid});
+ manager->ContentReceivedInputBlock(result.mInputBlockId,
+ /*aPreventDefault=*/true);
+
+ // The content response has the effect of interrupting the input block
+ // but no processing happens yet (as there are no events in the block).
+ EXPECT_TRUE(rootApzc->IsOverscrolled());
+ EXPECT_EQ(overscrollY, rootApzc->GetOverscrollAmount().y);
+
+ // Send one more pan event. This starts a new, *unconfirmed* input block
+ // (via the "transmogrify" codepath).
+ mcc->AdvanceByMillis(10);
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID,
+ {CompositorHitTestFlags::eVisibleToHitTest,
+ CompositorHitTestFlags::eIrregularArea});
+ result = PanGesture(PanGestureInput::PANGESTURE_PAN, manager, cursorLocation,
+ ScreenPoint(0, -10), mcc->Time());
+
+ // No overscroll occurs (the event is waiting in the queue for confirmation).
+ EXPECT_TRUE(rootApzc->IsOverscrolled());
+ EXPECT_EQ(overscrollY, rootApzc->GetOverscrollAmount().y);
+
+ // preventDefault the new event as well
+ manager->SetAllowedTouchBehavior(result.mInputBlockId,
+ {AllowedTouchBehavior::VERTICAL_PAN});
+ manager->SetTargetAPZC(result.mInputBlockId, {result.mTargetGuid});
+ manager->ContentReceivedInputBlock(result.mInputBlockId,
+ /*aPreventDefault=*/true);
+
+ // This should trigger clearing the overscrolling and resetting the state.
+ EXPECT_FALSE(rootApzc->IsOverscrolled());
+ rootApzc->AssertStateIsReset();
+
+ // If there are momentum events after this point, they should not cause
+ // further scrolling or overscorll.
+ mcc->AdvanceByMillis(10);
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ result = PanGesture(PanGestureInput::PANGESTURE_MOMENTUMSTART, manager,
+ cursorLocation, ScreenPoint(0, -100), mcc->Time());
+ mcc->AdvanceByMillis(10);
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ result = PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, manager,
+ cursorLocation, ScreenPoint(0, -100), mcc->Time());
+ EXPECT_FALSE(rootApzc->IsOverscrolled());
+ EXPECT_EQ(rootApzc->GetFrameMetrics().GetVisualScrollOffset(),
+ CSSPoint(0, 0));
+}
+#endif
diff --git a/gfx/layers/apz/test/gtest/TestPanning.cpp b/gfx/layers/apz/test/gtest/TestPanning.cpp
new file mode 100644
index 0000000000..886b0fec99
--- /dev/null
+++ b/gfx/layers/apz/test/gtest/TestPanning.cpp
@@ -0,0 +1,251 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "APZCBasicTester.h"
+#include "APZTestCommon.h"
+#include "InputUtils.h"
+#include "gtest/gtest.h"
+
+class APZCPanningTester : public APZCBasicTester {
+ protected:
+ void DoPanTest(bool aShouldTriggerScroll, bool aShouldBeConsumed,
+ uint32_t aBehavior) {
+ if (aShouldTriggerScroll) {
+ // Three repaint request for each pan.
+ EXPECT_CALL(*mcc, RequestContentRepaint(_)).Times(6);
+ } else {
+ EXPECT_CALL(*mcc, RequestContentRepaint(_)).Times(0);
+ }
+
+ int touchStart = 50;
+ int touchEnd = 10;
+ ParentLayerPoint pointOut;
+ AsyncTransform viewTransformOut;
+
+ nsTArray<uint32_t> allowedTouchBehaviors;
+ allowedTouchBehaviors.AppendElement(aBehavior);
+
+ // Pan down
+ PanAndCheckStatus(apzc, touchStart, touchEnd, aShouldBeConsumed,
+ &allowedTouchBehaviors);
+ apzc->SampleContentTransformForFrame(&viewTransformOut, pointOut);
+
+ if (aShouldTriggerScroll) {
+ EXPECT_EQ(ParentLayerPoint(0, -(touchEnd - touchStart)), pointOut);
+ EXPECT_NE(AsyncTransform(), viewTransformOut);
+ } else {
+ EXPECT_EQ(ParentLayerPoint(), pointOut);
+ EXPECT_EQ(AsyncTransform(), viewTransformOut);
+ }
+
+ // Clear the fling from the previous pan, or stopping it will
+ // consume the next touchstart
+ apzc->CancelAnimation();
+
+ // Pan back
+ PanAndCheckStatus(apzc, touchEnd, touchStart, aShouldBeConsumed,
+ &allowedTouchBehaviors);
+ apzc->SampleContentTransformForFrame(&viewTransformOut, pointOut);
+
+ EXPECT_EQ(ParentLayerPoint(), pointOut);
+ EXPECT_EQ(AsyncTransform(), viewTransformOut);
+ }
+
+ void DoPanWithPreventDefaultTest() {
+ MakeApzcWaitForMainThread();
+
+ int touchStart = 50;
+ int touchEnd = 10;
+ ParentLayerPoint pointOut;
+ AsyncTransform viewTransformOut;
+ uint64_t blockId = 0;
+
+ // Pan down
+ nsTArray<uint32_t> allowedTouchBehaviors;
+ allowedTouchBehaviors.AppendElement(
+ mozilla::layers::AllowedTouchBehavior::VERTICAL_PAN);
+ PanAndCheckStatus(apzc, touchStart, touchEnd, true, &allowedTouchBehaviors,
+ &blockId);
+
+ // Send the signal that content has handled and preventDefaulted the touch
+ // events. This flushes the event queue.
+ apzc->ContentReceivedInputBlock(blockId, true);
+
+ apzc->SampleContentTransformForFrame(&viewTransformOut, pointOut);
+ EXPECT_EQ(ParentLayerPoint(), pointOut);
+ EXPECT_EQ(AsyncTransform(), viewTransformOut);
+
+ apzc->AssertStateIsReset();
+ }
+};
+
+// In the each of the following 4 pan tests we are performing two pan gestures:
+// vertical pan from top to bottom and back - from bottom to top. According to
+// the pointer-events/touch-action spec AUTO and PAN_Y touch-action values allow
+// vertical scrolling while NONE and PAN_X forbid it. The first parameter of
+// DoPanTest method specifies this behavior. However, the events will be marked
+// as consumed even if the behavior in PAN_X, because the user could move their
+// finger horizontally too - APZ has no way of knowing beforehand and so must
+// consume the events.
+TEST_F(APZCPanningTester, PanWithTouchActionAuto) {
+ // Velocity bias can cause extra repaint requests.
+ SCOPED_GFX_PREF_FLOAT("apz.velocity_bias", 0.0);
+ DoPanTest(true, true,
+ mozilla::layers::AllowedTouchBehavior::HORIZONTAL_PAN |
+ mozilla::layers::AllowedTouchBehavior::VERTICAL_PAN);
+}
+
+TEST_F(APZCPanningTester, PanWithTouchActionNone) {
+ // Velocity bias can cause extra repaint requests.
+ SCOPED_GFX_PREF_FLOAT("apz.velocity_bias", 0.0);
+ DoPanTest(false, false, 0);
+}
+
+TEST_F(APZCPanningTester, PanWithTouchActionPanX) {
+ // Velocity bias can cause extra repaint requests.
+ SCOPED_GFX_PREF_FLOAT("apz.velocity_bias", 0.0);
+ DoPanTest(false, false,
+ mozilla::layers::AllowedTouchBehavior::HORIZONTAL_PAN);
+}
+
+TEST_F(APZCPanningTester, PanWithTouchActionPanY) {
+ // Velocity bias can cause extra repaint requests.
+ SCOPED_GFX_PREF_FLOAT("apz.velocity_bias", 0.0);
+ DoPanTest(true, true, mozilla::layers::AllowedTouchBehavior::VERTICAL_PAN);
+}
+
+TEST_F(APZCPanningTester, PanWithPreventDefault) {
+ DoPanWithPreventDefaultTest();
+}
+
+TEST_F(APZCPanningTester, PanWithHistoricalTouchData) {
+ SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0);
+
+ // Simulate the same pan gesture, in three different ways.
+ // We start at y=50, with a 50ms resting period at the start of the pan.
+ // Then we accelerate the finger upwards towards y=10, reaching a 10px/10ms
+ // velocity towards the end of the panning motion.
+ //
+ // The first simulation fires touch move events with 10ms gaps.
+ // The second simulation skips two of the touch move events, simulating
+ // "jank". The third simulation also skips those two events, but reports the
+ // missed positions in the following event's historical coordinates.
+ //
+ // Consequently, the first and third simulation should estimate the same
+ // velocities, whereas the second simulation should estimate a different
+ // velocity because it is missing data.
+
+ // First simulation: full data
+
+ APZEventResult result = TouchDown(apzc, ScreenIntPoint(0, 50), mcc->Time());
+ if (result.GetStatus() != nsEventStatus_eConsumeNoDefault) {
+ SetDefaultAllowedTouchBehavior(apzc, result.mInputBlockId);
+ }
+
+ mcc->AdvanceByMillis(50);
+ result = TouchMove(apzc, ScreenIntPoint(0, 45), mcc->Time());
+ mcc->AdvanceByMillis(10);
+ result = TouchMove(apzc, ScreenIntPoint(0, 40), mcc->Time());
+ mcc->AdvanceByMillis(10);
+ result = TouchMove(apzc, ScreenIntPoint(0, 30), mcc->Time());
+ mcc->AdvanceByMillis(10);
+ result = TouchMove(apzc, ScreenIntPoint(0, 20), mcc->Time());
+ result = TouchUp(apzc, ScreenIntPoint(0, 20), mcc->Time());
+ auto velocityFromFullDataAsSeparateEvents = apzc->GetVelocityVector();
+ apzc->CancelAnimation();
+
+ mcc->AdvanceByMillis(100);
+
+ // Second simulation: partial data
+
+ result = TouchDown(apzc, ScreenIntPoint(0, 50), mcc->Time());
+ if (result.GetStatus() != nsEventStatus_eConsumeNoDefault) {
+ SetDefaultAllowedTouchBehavior(apzc, result.mInputBlockId);
+ }
+
+ mcc->AdvanceByMillis(50);
+ result = TouchMove(apzc, ScreenIntPoint(0, 45), mcc->Time());
+ mcc->AdvanceByMillis(30);
+ result = TouchMove(apzc, ScreenIntPoint(0, 20), mcc->Time());
+ result = TouchUp(apzc, ScreenIntPoint(0, 20), mcc->Time());
+ auto velocityFromPartialData = apzc->GetVelocityVector();
+ apzc->CancelAnimation();
+
+ mcc->AdvanceByMillis(100);
+
+ // Third simulation: full data via historical data
+
+ result = TouchDown(apzc, ScreenIntPoint(0, 50), mcc->Time());
+ if (result.GetStatus() != nsEventStatus_eConsumeNoDefault) {
+ SetDefaultAllowedTouchBehavior(apzc, result.mInputBlockId);
+ }
+
+ mcc->AdvanceByMillis(50);
+ result = TouchMove(apzc, ScreenIntPoint(0, 45), mcc->Time());
+ mcc->AdvanceByMillis(30);
+
+ MultiTouchInput mti =
+ CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, mcc->Time());
+ auto singleTouchData = CreateSingleTouchData(0, ScreenIntPoint(0, 20));
+ singleTouchData.mHistoricalData.AppendElement(
+ SingleTouchData::HistoricalTouchData{
+ mcc->Time() - TimeDuration::FromMilliseconds(20),
+ ScreenIntPoint(0, 40),
+ {},
+ {},
+ 0.0f,
+ 0.0f});
+ singleTouchData.mHistoricalData.AppendElement(
+ SingleTouchData::HistoricalTouchData{
+ mcc->Time() - TimeDuration::FromMilliseconds(10),
+ ScreenIntPoint(0, 30),
+ {},
+ {},
+ 0.0f,
+ 0.0f});
+ mti.mTouches.AppendElement(singleTouchData);
+ result = apzc->ReceiveInputEvent(mti);
+
+ result = TouchUp(apzc, ScreenIntPoint(0, 20), mcc->Time());
+ auto velocityFromFullDataViaHistory = apzc->GetVelocityVector();
+ apzc->CancelAnimation();
+
+ EXPECT_EQ(velocityFromFullDataAsSeparateEvents,
+ velocityFromFullDataViaHistory);
+ EXPECT_NE(velocityFromPartialData, velocityFromFullDataViaHistory);
+}
+
+TEST_F(APZCPanningTester, DuplicatePanEndEvents_Bug1833950) {
+ // Send a pan gesture that triggers a fling animation at the end.
+ // Note that we need at least two _PAN events to have enough samples
+ // in the velocity tracker to compute a fling velocity.
+ PanGesture(PanGestureInput::PANGESTURE_START, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, 2), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, 10), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, 10), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_END, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, 0), mcc->Time(), MODIFIER_NONE,
+ /*aSimulateMomentum=*/true);
+
+ // Give the fling animation a chance to start.
+ SampleAnimationOnce();
+ apzc->AssertStateIsFling();
+
+ // Send a duplicate pan-end event.
+ // This test is just intended to check that doing this doesn't
+ // trigger an assertion failure in debug mode.
+ PanGesture(PanGestureInput::PANGESTURE_END, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, 0), mcc->Time(), MODIFIER_NONE,
+ /*aSimulateMomentum=*/true);
+}
diff --git a/gfx/layers/apz/test/gtest/TestPinching.cpp b/gfx/layers/apz/test/gtest/TestPinching.cpp
new file mode 100644
index 0000000000..f6d1280bf3
--- /dev/null
+++ b/gfx/layers/apz/test/gtest/TestPinching.cpp
@@ -0,0 +1,675 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "APZCBasicTester.h"
+#include "APZTestCommon.h"
+#include "InputUtils.h"
+#include "mozilla/StaticPrefs_apz.h"
+
+class APZCPinchTester : public APZCBasicTester {
+ private:
+ // This (multiplied by apz.touch_start_tolerance) needs to be the hypotenuse
+ // in a Pythagorean triple, along with overcomeTouchToleranceX and
+ // overcomeTouchToleranceY from APZCTesterBase::Pan().
+ // This is because APZCTesterBase::Pan(), when run without the
+ // PanOptions::ExactCoordinates option, will need to first overcome the
+ // touch start tolerance by performing a move of exactly
+ // (apz.touch_start_tolerance * DPI) length.
+ // When moving on both axes at once, we need to use integers for both legs
+ // (overcomeTouchToleranceX and overcomeTouchToleranceY) while making sure
+ // that the hypotenuse is also a round integer number (hence Pythagorean
+ // triples). (The hypotenuse is the length of the movement in this case.)
+ static const int mDPI = 100;
+
+ public:
+ explicit APZCPinchTester(
+ AsyncPanZoomController::GestureBehavior aGestureBehavior =
+ AsyncPanZoomController::DEFAULT_GESTURES)
+ : APZCBasicTester(aGestureBehavior) {}
+
+ void SetUp() override {
+ APZCBasicTester::SetUp();
+ tm->SetDPI(mDPI);
+ }
+
+ protected:
+ FrameMetrics GetPinchableFrameMetrics() {
+ FrameMetrics fm;
+ fm.SetCompositionBounds(ParentLayerRect(0, 0, 100, 200));
+ fm.SetScrollableRect(CSSRect(0, 0, 980, 1000));
+ fm.SetVisualScrollOffset(CSSPoint(300, 300));
+ fm.SetLayoutViewport(CSSRect(300, 300, 100, 200));
+ fm.SetZoom(CSSToParentLayerScale(2.0));
+ // APZC only allows zooming on the root scrollable frame.
+ fm.SetIsRootContent(true);
+ // the visible area of the document in CSS pixels is x=300 y=300 w=50 h=100
+ return fm;
+ }
+
+ void DoPinchTest(bool aShouldTriggerPinch,
+ nsTArray<uint32_t>* aAllowedTouchBehaviors = nullptr) {
+ apzc->SetFrameMetrics(GetPinchableFrameMetrics());
+ MakeApzcZoomable();
+
+ if (aShouldTriggerPinch) {
+ // One repaint request for each gesture.
+ EXPECT_CALL(*mcc, RequestContentRepaint(_)).Times(2);
+ } else {
+ EXPECT_CALL(*mcc, RequestContentRepaint(_)).Times(0);
+ }
+
+ int touchInputId = 0;
+ if (mGestureBehavior == AsyncPanZoomController::USE_GESTURE_DETECTOR) {
+ PinchWithTouchInputAndCheckStatus(apzc, ScreenIntPoint(250, 300), 1.25,
+ touchInputId, aShouldTriggerPinch,
+ aAllowedTouchBehaviors);
+ } else {
+ PinchWithPinchInputAndCheckStatus(apzc, ScreenIntPoint(250, 300), 1.25,
+ aShouldTriggerPinch);
+ }
+
+ apzc->AssertStateIsReset();
+
+ FrameMetrics fm = apzc->GetFrameMetrics();
+
+ if (aShouldTriggerPinch) {
+ // the visible area of the document in CSS pixels is now x=325 y=330 w=40
+ // h=80
+ EXPECT_EQ(2.5f, fm.GetZoom().scale);
+ EXPECT_EQ(325, fm.GetVisualScrollOffset().x);
+ EXPECT_EQ(330, fm.GetVisualScrollOffset().y);
+ } else {
+ // The frame metrics should stay the same since touch-action:none makes
+ // apzc ignore pinch gestures.
+ EXPECT_EQ(2.0f, fm.GetZoom().scale);
+ EXPECT_EQ(300, fm.GetVisualScrollOffset().x);
+ EXPECT_EQ(300, fm.GetVisualScrollOffset().y);
+ }
+
+ // part 2 of the test, move to the top-right corner of the page and pinch
+ // and make sure we stay in the correct spot
+ fm.SetZoom(CSSToParentLayerScale(2.0));
+ fm.SetVisualScrollOffset(CSSPoint(930, 5));
+ apzc->SetFrameMetrics(fm);
+ // the visible area of the document in CSS pixels is x=930 y=5 w=50 h=100
+
+ if (mGestureBehavior == AsyncPanZoomController::USE_GESTURE_DETECTOR) {
+ PinchWithTouchInputAndCheckStatus(apzc, ScreenIntPoint(250, 300), 0.5,
+ touchInputId, aShouldTriggerPinch,
+ aAllowedTouchBehaviors);
+ } else {
+ PinchWithPinchInputAndCheckStatus(apzc, ScreenIntPoint(250, 300), 0.5,
+ aShouldTriggerPinch);
+ }
+
+ apzc->AssertStateIsReset();
+
+ fm = apzc->GetFrameMetrics();
+
+ if (aShouldTriggerPinch) {
+ // the visible area of the document in CSS pixels is now x=805 y=0 w=100
+ // h=200
+ EXPECT_EQ(1.0f, fm.GetZoom().scale);
+ EXPECT_EQ(805, fm.GetVisualScrollOffset().x);
+ EXPECT_EQ(0, fm.GetVisualScrollOffset().y);
+ } else {
+ EXPECT_EQ(2.0f, fm.GetZoom().scale);
+ EXPECT_EQ(930, fm.GetVisualScrollOffset().x);
+ EXPECT_EQ(5, fm.GetVisualScrollOffset().y);
+ }
+ }
+};
+
+class APZCPinchGestureDetectorTester : public APZCPinchTester {
+ public:
+ APZCPinchGestureDetectorTester()
+ : APZCPinchTester(AsyncPanZoomController::USE_GESTURE_DETECTOR) {}
+
+ void DoPinchWithPreventDefaultTest() {
+ FrameMetrics originalMetrics = GetPinchableFrameMetrics();
+ apzc->SetFrameMetrics(originalMetrics);
+
+ MakeApzcWaitForMainThread();
+ MakeApzcZoomable();
+
+ int touchInputId = 0;
+ uint64_t blockId = 0;
+ PinchWithTouchInput(apzc, ScreenIntPoint(250, 300), 1.25, touchInputId,
+ nullptr, nullptr, &blockId);
+
+ // Send the prevent-default notification for the touch block
+ apzc->ContentReceivedInputBlock(blockId, true);
+
+ // verify the metrics didn't change (i.e. the pinch was ignored)
+ FrameMetrics fm = apzc->GetFrameMetrics();
+ EXPECT_EQ(originalMetrics.GetZoom(), fm.GetZoom());
+ EXPECT_EQ(originalMetrics.GetVisualScrollOffset().x,
+ fm.GetVisualScrollOffset().x);
+ EXPECT_EQ(originalMetrics.GetVisualScrollOffset().y,
+ fm.GetVisualScrollOffset().y);
+
+ apzc->AssertStateIsReset();
+ }
+};
+
+class APZCPinchLockingTester : public APZCPinchTester {
+ private:
+ ScreenIntPoint mFocus;
+ float mSpan;
+ int mPinchLockBufferMaxAge;
+
+ public:
+ APZCPinchLockingTester()
+ : APZCPinchTester(AsyncPanZoomController::USE_GESTURE_DETECTOR),
+ mFocus(ScreenIntPoint(200, 300)),
+ mSpan(10.0) {}
+
+ virtual void SetUp() {
+ mPinchLockBufferMaxAge =
+ StaticPrefs::apz_pinch_lock_buffer_max_age_AtStartup();
+
+ APZCPinchTester::SetUp();
+ apzc->SetFrameMetrics(GetPinchableFrameMetrics());
+ MakeApzcZoomable();
+
+ auto event = CreatePinchGestureInput(PinchGestureInput::PINCHGESTURE_START,
+ mFocus, mSpan, mSpan, mcc->Time());
+ apzc->ReceiveInputEvent(event);
+ mcc->AdvanceBy(TimeDuration::FromMilliseconds(mPinchLockBufferMaxAge + 1));
+ }
+
+ void twoFingerPan() {
+ ScreenCoord panDistance =
+ StaticPrefs::apz_pinch_lock_scroll_lock_threshold() * 1.2 *
+ tm->GetDPI();
+
+ mFocus = ScreenIntPoint((int)(mFocus.x.value + panDistance),
+ (int)(mFocus.y.value));
+
+ auto event = CreatePinchGestureInput(PinchGestureInput::PINCHGESTURE_SCALE,
+ mFocus, mSpan, mSpan, mcc->Time());
+ apzc->ReceiveInputEvent(event);
+ mcc->AdvanceBy(TimeDuration::FromMilliseconds(mPinchLockBufferMaxAge + 1));
+ }
+
+ void twoFingerZoom() {
+ float pinchDistance =
+ StaticPrefs::apz_pinch_lock_span_breakout_threshold() * 1.2 *
+ tm->GetDPI();
+
+ float newSpan = mSpan + pinchDistance;
+
+ auto event = CreatePinchGestureInput(PinchGestureInput::PINCHGESTURE_SCALE,
+ mFocus, newSpan, mSpan, mcc->Time());
+ apzc->ReceiveInputEvent(event);
+ mcc->AdvanceBy(TimeDuration::FromMilliseconds(mPinchLockBufferMaxAge + 1));
+ mSpan = newSpan;
+ }
+
+ bool isPinchLockActive() {
+ FrameMetrics originalMetrics = apzc->GetFrameMetrics();
+
+ // Send a small scale input to the APZC
+ float pinchDistance =
+ StaticPrefs::apz_pinch_lock_span_breakout_threshold() * 0.8 *
+ tm->GetDPI();
+ auto event =
+ CreatePinchGestureInput(PinchGestureInput::PINCHGESTURE_SCALE, mFocus,
+ mSpan + pinchDistance, mSpan, mcc->Time());
+ apzc->ReceiveInputEvent(event);
+
+ FrameMetrics result = apzc->GetFrameMetrics();
+ bool lockActive = originalMetrics.GetZoom() == result.GetZoom() &&
+ originalMetrics.GetVisualScrollOffset().x ==
+ result.GetVisualScrollOffset().x &&
+ originalMetrics.GetVisualScrollOffset().y ==
+ result.GetVisualScrollOffset().y;
+
+ // Avoid side effects, reset to original frame metrics
+ apzc->SetFrameMetrics(originalMetrics);
+ return lockActive;
+ }
+};
+
+TEST_F(APZCPinchGestureDetectorTester,
+ Pinch_UseGestureDetector_TouchActionNone) {
+ nsTArray<uint32_t> behaviors = {mozilla::layers::AllowedTouchBehavior::NONE,
+ mozilla::layers::AllowedTouchBehavior::NONE};
+ DoPinchTest(false, &behaviors);
+}
+
+TEST_F(APZCPinchGestureDetectorTester,
+ Pinch_UseGestureDetector_TouchActionZoom) {
+ nsTArray<uint32_t> behaviors;
+ behaviors.AppendElement(mozilla::layers::AllowedTouchBehavior::PINCH_ZOOM);
+ behaviors.AppendElement(mozilla::layers::AllowedTouchBehavior::PINCH_ZOOM);
+ DoPinchTest(true, &behaviors);
+}
+
+TEST_F(APZCPinchGestureDetectorTester,
+ Pinch_UseGestureDetector_TouchActionNotAllowZoom) {
+ nsTArray<uint32_t> behaviors;
+ behaviors.AppendElement(mozilla::layers::AllowedTouchBehavior::NONE);
+ behaviors.AppendElement(mozilla::layers::AllowedTouchBehavior::PINCH_ZOOM);
+ DoPinchTest(false, &behaviors);
+}
+
+TEST_F(APZCPinchGestureDetectorTester,
+ Pinch_UseGestureDetector_TouchActionNone_NoAPZZoom) {
+ SCOPED_GFX_PREF_BOOL("apz.allow_zooming", false);
+
+ // Since we are preventing the pinch action via touch-action we should not be
+ // sending the pinch gesture notifications that would normally be sent when
+ // apz_allow_zooming is false.
+ EXPECT_CALL(*mcc, NotifyPinchGesture(_, _, _, _, _)).Times(0);
+ nsTArray<uint32_t> behaviors = {mozilla::layers::AllowedTouchBehavior::NONE,
+ mozilla::layers::AllowedTouchBehavior::NONE};
+ DoPinchTest(false, &behaviors);
+}
+
+TEST_F(APZCPinchGestureDetectorTester, Pinch_PreventDefault) {
+ DoPinchWithPreventDefaultTest();
+}
+
+TEST_F(APZCPinchGestureDetectorTester, Pinch_PreventDefault_NoAPZZoom) {
+ SCOPED_GFX_PREF_BOOL("apz.allow_zooming", false);
+
+ // Since we are preventing the pinch action we should not be sending the pinch
+ // gesture notifications that would normally be sent when apz_allow_zooming is
+ // false.
+ EXPECT_CALL(*mcc, NotifyPinchGesture(_, _, _, _, _)).Times(0);
+
+ DoPinchWithPreventDefaultTest();
+}
+
+TEST_F(APZCPinchGestureDetectorTester, Panning_TwoFingerFling_ZoomDisabled) {
+ SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0f);
+
+ apzc->SetFrameMetrics(GetPinchableFrameMetrics());
+ MakeApzcUnzoomable();
+
+ // Perform a two finger pan
+ int touchInputId = 0;
+ uint64_t blockId = 0;
+ PinchWithTouchInput(apzc, ScreenIntPoint(100, 200), ScreenIntPoint(100, 100),
+ 1, touchInputId, nullptr, nullptr, &blockId);
+
+ // Expect to be in a flinging state
+ apzc->AssertStateIsFling();
+}
+
+TEST_F(APZCPinchGestureDetectorTester, Pinch_DoesntFling_ZoomDisabled) {
+ SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0f);
+
+ apzc->SetFrameMetrics(GetPinchableFrameMetrics());
+ MakeApzcUnzoomable();
+
+ // Perform a pinch
+ int touchInputId = 0;
+ uint64_t blockId = 0;
+
+ PinchWithTouchInput(apzc, ScreenIntPoint(100, 200), ScreenIntPoint(100, 100),
+ 2, touchInputId, nullptr, nullptr, &blockId,
+ PinchOptions::LiftFinger2, true);
+
+ // Lift second finger after a pause
+ mcc->AdvanceBy(TimeDuration::FromMilliseconds(50));
+ TouchUp(apzc, ScreenIntPoint(100, 100), mcc->Time());
+
+ // Pinch should not trigger a fling
+ EXPECT_EQ(apzc->GetVelocityVector().y, 0);
+}
+
+TEST_F(APZCPinchGestureDetectorTester, Panning_TwoFingerFling_ZoomEnabled) {
+ SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0f);
+
+ apzc->SetFrameMetrics(GetPinchableFrameMetrics());
+ MakeApzcZoomable();
+
+ // Perform a two finger pan
+ int touchInputId = 0;
+ uint64_t blockId = 0;
+ PinchWithTouchInput(apzc, ScreenIntPoint(100, 200), ScreenIntPoint(100, 100),
+ 1, touchInputId, nullptr, nullptr, &blockId);
+
+ // Expect to NOT be in flinging state
+ apzc->AssertStateIsReset();
+}
+
+TEST_F(APZCPinchGestureDetectorTester,
+ Panning_TwoThenOneFingerFling_ZoomEnabled) {
+ SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0f);
+
+ apzc->SetFrameMetrics(GetPinchableFrameMetrics());
+ MakeApzcZoomable();
+
+ // Perform a two finger pan lifting only the first finger
+ int touchInputId = 0;
+ uint64_t blockId = 0;
+ PinchWithTouchInput(apzc, ScreenIntPoint(100, 200), ScreenIntPoint(100, 100),
+ 1, touchInputId, nullptr, nullptr, &blockId,
+ PinchOptions::LiftFinger2);
+
+ // Lift second finger after a pause
+ mcc->AdvanceBy(TimeDuration::FromMilliseconds(50));
+ TouchUp(apzc, ScreenIntPoint(100, 100), mcc->Time());
+
+ // This gesture should activate the pinch lock, and result
+ // in a fling even if the page is zoomable.
+ apzc->AssertStateIsFling();
+}
+
+TEST_F(APZCPinchGestureDetectorTester,
+ Panning_TwoThenOneFingerFling_ZoomDisabled) {
+ SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0f);
+
+ apzc->SetFrameMetrics(GetPinchableFrameMetrics());
+ MakeApzcUnzoomable();
+
+ // Perform a two finger pan lifting only the first finger
+ int touchInputId = 0;
+ uint64_t blockId = 0;
+ PinchWithTouchInput(apzc, ScreenIntPoint(100, 200), ScreenIntPoint(100, 100),
+ 1, touchInputId, nullptr, nullptr, &blockId,
+ PinchOptions::LiftFinger2);
+
+ // Lift second finger after a pause
+ mcc->AdvanceBy(TimeDuration::FromMilliseconds(50));
+ TouchUp(apzc, ScreenIntPoint(100, 100), mcc->Time());
+
+ // This gesture should activate the pinch lock and result in a fling
+ apzc->AssertStateIsFling();
+}
+
+TEST_F(APZCPinchTester, Panning_TwoFinger_ZoomDisabled) {
+ // set up APZ
+ apzc->SetFrameMetrics(GetPinchableFrameMetrics());
+ MakeApzcUnzoomable();
+
+ nsEventStatus statuses[3]; // scalebegin, scale, scaleend
+ PinchWithPinchInput(apzc, ScreenIntPoint(250, 350), ScreenIntPoint(200, 300),
+ 10, &statuses);
+
+ FrameMetrics fm = apzc->GetFrameMetrics();
+
+ // It starts from (300, 300), then moves the focus point from (250, 350) to
+ // (200, 300) pans by (50, 50) screen pixels, but there is a 2x zoom, which
+ // causes the scroll offset to change by half of that (25, 25) pixels.
+ EXPECT_EQ(325, fm.GetVisualScrollOffset().x);
+ EXPECT_EQ(325, fm.GetVisualScrollOffset().y);
+ EXPECT_EQ(2.0, fm.GetZoom().scale);
+}
+
+TEST_F(APZCPinchTester, Panning_Beyond_LayoutViewport) {
+ SCOPED_GFX_PREF_INT("apz.axis_lock.mode", 0);
+
+ apzc->SetFrameMetrics(GetPinchableFrameMetrics());
+ MakeApzcZoomable();
+
+ // Case 1 - visual viewport is still inside layout viewport.
+ Pan(apzc, 350, 300, PanOptions::NoFling);
+ FrameMetrics fm = apzc->GetFrameMetrics();
+ // It starts from (300, 300) pans by (0, 50) screen pixels, but there is a
+ // 2x zoom, which causes the scroll offset to change by half of that (0, 25).
+ // But the visual viewport is still inside the layout viewport.
+ EXPECT_EQ(300, fm.GetVisualScrollOffset().x);
+ EXPECT_EQ(325, fm.GetVisualScrollOffset().y);
+ EXPECT_EQ(300, fm.GetLayoutViewport().X());
+ EXPECT_EQ(300, fm.GetLayoutViewport().Y());
+
+ // Case 2 - visual viewport crosses the bottom boundary of the layout
+ // viewport.
+ Pan(apzc, 525, 325, PanOptions::NoFling);
+ fm = apzc->GetFrameMetrics();
+ // It starts from (300, 325) pans by (0, 200) screen pixels, but there is a
+ // 2x zoom, which causes the scroll offset to change by half of that
+ // (0, 100). The visual viewport crossed the bottom boundary of the layout
+ // viewport by 25px.
+ EXPECT_EQ(300, fm.GetVisualScrollOffset().x);
+ EXPECT_EQ(425, fm.GetVisualScrollOffset().y);
+ EXPECT_EQ(300, fm.GetLayoutViewport().X());
+ EXPECT_EQ(325, fm.GetLayoutViewport().Y());
+
+ // Case 3 - visual viewport crosses the top boundary of the layout viewport.
+ Pan(apzc, 425, 775, PanOptions::NoFling);
+ fm = apzc->GetFrameMetrics();
+ // It starts from (300, 425) pans by (0, -350) screen pixels, but there is a
+ // 2x zoom, which causes the scroll offset to change by half of that
+ // (0, -175). The visual viewport crossed the top of the layout viewport by
+ // 75px.
+ EXPECT_EQ(300, fm.GetVisualScrollOffset().x);
+ EXPECT_EQ(250, fm.GetVisualScrollOffset().y);
+ EXPECT_EQ(300, fm.GetLayoutViewport().X());
+ EXPECT_EQ(250, fm.GetLayoutViewport().Y());
+
+ // Case 4 - visual viewport crosses the left boundary of the layout viewport.
+ Pan(apzc, ScreenIntPoint(150, 10), ScreenIntPoint(350, 10),
+ PanOptions::NoFling);
+ fm = apzc->GetFrameMetrics();
+ // It starts from (300, 250) pans by (-200, 0) screen pixels, but there is a
+ // 2x zoom, which causes the scroll offset to change by half of that
+ // (-100, 0). The visual viewport crossed the left boundary of the layout
+ // viewport by 100px.
+ EXPECT_EQ(200, fm.GetVisualScrollOffset().x);
+ EXPECT_EQ(250, fm.GetVisualScrollOffset().y);
+ EXPECT_EQ(200, fm.GetLayoutViewport().X());
+ EXPECT_EQ(250, fm.GetLayoutViewport().Y());
+
+ // Case 5 - visual viewport crosses the right boundary of the layout viewport.
+ Pan(apzc, ScreenIntPoint(350, 10), ScreenIntPoint(150, 10),
+ PanOptions::NoFling);
+ fm = apzc->GetFrameMetrics();
+ // It starts from (200, 250) pans by (200, 0) screen pixels, but there is a
+ // 2x zoom, which causes the scroll offset to change by half of that
+ // (100, 0). The visual viewport crossed the right boundary of the layout
+ // viewport by 50px.
+ EXPECT_EQ(300, fm.GetVisualScrollOffset().x);
+ EXPECT_EQ(250, fm.GetVisualScrollOffset().y);
+ EXPECT_EQ(250, fm.GetLayoutViewport().X());
+ EXPECT_EQ(250, fm.GetLayoutViewport().Y());
+
+ // Case 6 - visual viewport crosses both the vertical and horizontal
+ // boundaries of the layout viewport by moving diagonally towards the
+ // top-right corner.
+ Pan(apzc, ScreenIntPoint(350, 200), ScreenIntPoint(150, 400),
+ PanOptions::NoFling);
+ fm = apzc->GetFrameMetrics();
+ // It starts from (300, 250) pans by (200, -200) screen pixels, but there is
+ // a 2x zoom, which causes the scroll offset to change by half of that
+ // (100, -100). The visual viewport moved by (100, -100) outside the
+ // boundary of the layout viewport.
+ EXPECT_EQ(400, fm.GetVisualScrollOffset().x);
+ EXPECT_EQ(150, fm.GetVisualScrollOffset().y);
+ EXPECT_EQ(350, fm.GetLayoutViewport().X());
+ EXPECT_EQ(150, fm.GetLayoutViewport().Y());
+}
+
+TEST_F(APZCPinchGestureDetectorTester, Pinch_APZZoom_Disabled) {
+ SCOPED_GFX_PREF_BOOL("apz.allow_zooming", false);
+
+ FrameMetrics originalMetrics = GetPinchableFrameMetrics();
+ apzc->SetFrameMetrics(originalMetrics);
+
+ // When apz_allow_zooming is false, the ZoomConstraintsClient produces
+ // ZoomConstraints with mAllowZoom set to false.
+ MakeApzcUnzoomable();
+
+ // With apz_allow_zooming false, we expect the NotifyPinchGesture function to
+ // get called as the pinch progresses, but the metrics shouldn't change.
+ EXPECT_CALL(*mcc,
+ NotifyPinchGesture(PinchGestureInput::PINCHGESTURE_START,
+ apzc->GetGuid(), _, LayoutDeviceCoord(0), _))
+ .Times(1);
+ EXPECT_CALL(*mcc, NotifyPinchGesture(PinchGestureInput::PINCHGESTURE_SCALE,
+ apzc->GetGuid(), _, _, _))
+ .Times(AtLeast(1));
+ EXPECT_CALL(*mcc,
+ NotifyPinchGesture(PinchGestureInput::PINCHGESTURE_END,
+ apzc->GetGuid(), _, LayoutDeviceCoord(0), _))
+ .Times(1);
+
+ int touchInputId = 0;
+ uint64_t blockId = 0;
+ PinchWithTouchInput(apzc, ScreenIntPoint(250, 300), 1.25, touchInputId,
+ nullptr, nullptr, &blockId);
+
+ // verify the metrics didn't change (i.e. the pinch was ignored inside APZ)
+ FrameMetrics fm = apzc->GetFrameMetrics();
+ EXPECT_EQ(originalMetrics.GetZoom(), fm.GetZoom());
+ EXPECT_EQ(originalMetrics.GetVisualScrollOffset().x,
+ fm.GetVisualScrollOffset().x);
+ EXPECT_EQ(originalMetrics.GetVisualScrollOffset().y,
+ fm.GetVisualScrollOffset().y);
+
+ apzc->AssertStateIsReset();
+}
+
+TEST_F(APZCPinchGestureDetectorTester, Pinch_NoSpan) {
+ SCOPED_GFX_PREF_BOOL("apz.allow_zooming", false);
+
+ FrameMetrics originalMetrics = GetPinchableFrameMetrics();
+ apzc->SetFrameMetrics(originalMetrics);
+
+ // When apz_allow_zooming is false, the ZoomConstraintsClient produces
+ // ZoomConstraints with mAllowZoom set to false.
+ MakeApzcUnzoomable();
+
+ // With apz_allow_zooming false, we expect the NotifyPinchGesture function to
+ // get called as the pinch progresses, but the metrics shouldn't change.
+ EXPECT_CALL(*mcc,
+ NotifyPinchGesture(PinchGestureInput::PINCHGESTURE_START,
+ apzc->GetGuid(), _, LayoutDeviceCoord(0), _))
+ .Times(1);
+ EXPECT_CALL(*mcc, NotifyPinchGesture(PinchGestureInput::PINCHGESTURE_SCALE,
+ apzc->GetGuid(), _, _, _))
+ .Times(AtLeast(1));
+ EXPECT_CALL(*mcc,
+ NotifyPinchGesture(PinchGestureInput::PINCHGESTURE_END,
+ apzc->GetGuid(), _, LayoutDeviceCoord(0), _))
+ .Times(1);
+
+ int inputId = 0;
+ ScreenIntPoint focus(250, 300);
+
+ // Do a pinch holding a zero span and moving the focus by y=100
+
+ const TimeDuration TIME_BETWEEN_TOUCH_EVENT =
+ TimeDuration::FromMilliseconds(50);
+ const auto touchBehaviors = Some(nsTArray<uint32_t>{kDefaultTouchBehavior});
+
+ MultiTouchInput mtiStart =
+ MultiTouchInput(MultiTouchInput::MULTITOUCH_START, 0, mcc->Time(), 0);
+ mtiStart.mTouches.AppendElement(CreateSingleTouchData(inputId, focus));
+ mtiStart.mTouches.AppendElement(CreateSingleTouchData(inputId + 1, focus));
+ apzc->ReceiveInputEvent(mtiStart, touchBehaviors);
+ mcc->AdvanceBy(TIME_BETWEEN_TOUCH_EVENT);
+
+ focus.y -= 35 + 1; // this is to get over the PINCH_START_THRESHOLD in
+ // GestureEventListener.cpp
+ MultiTouchInput mtiMove1 =
+ MultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, 0, mcc->Time(), 0);
+ mtiMove1.mTouches.AppendElement(CreateSingleTouchData(inputId, focus));
+ mtiMove1.mTouches.AppendElement(CreateSingleTouchData(inputId + 1, focus));
+ apzc->ReceiveInputEvent(mtiMove1);
+ mcc->AdvanceBy(TIME_BETWEEN_TOUCH_EVENT);
+
+ focus.y -= 100; // do a two-finger scroll of 100 screen pixels
+ MultiTouchInput mtiMove2 =
+ MultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, 0, mcc->Time(), 0);
+ mtiMove2.mTouches.AppendElement(CreateSingleTouchData(inputId, focus));
+ mtiMove2.mTouches.AppendElement(CreateSingleTouchData(inputId + 1, focus));
+ apzc->ReceiveInputEvent(mtiMove2);
+ mcc->AdvanceBy(TIME_BETWEEN_TOUCH_EVENT);
+
+ MultiTouchInput mtiEnd =
+ MultiTouchInput(MultiTouchInput::MULTITOUCH_END, 0, mcc->Time(), 0);
+ mtiEnd.mTouches.AppendElement(CreateSingleTouchData(inputId, focus));
+ mtiEnd.mTouches.AppendElement(CreateSingleTouchData(inputId + 1, focus));
+ apzc->ReceiveInputEvent(mtiEnd);
+
+ // Done, check the metrics to make sure we scrolled by 100 screen pixels,
+ // which is 50 CSS pixels for the pinchable frame metrics.
+
+ FrameMetrics fm = apzc->GetFrameMetrics();
+ EXPECT_EQ(originalMetrics.GetZoom(), fm.GetZoom());
+ EXPECT_EQ(originalMetrics.GetVisualScrollOffset().x,
+ fm.GetVisualScrollOffset().x);
+ EXPECT_EQ(originalMetrics.GetVisualScrollOffset().y + 50,
+ fm.GetVisualScrollOffset().y);
+
+ apzc->AssertStateIsReset();
+}
+
+TEST_F(APZCPinchTester, Pinch_TwoFinger_APZZoom_Disabled_Bug1354185) {
+ // Set up APZ such that mZoomConstraints.mAllowZoom is false.
+ SCOPED_GFX_PREF_BOOL("apz.allow_zooming", false);
+ apzc->SetFrameMetrics(GetPinchableFrameMetrics());
+ MakeApzcUnzoomable();
+
+ // We expect a repaint request for scrolling.
+ EXPECT_CALL(*mcc, RequestContentRepaint(_)).Times(1);
+
+ // Send only the PINCHGESTURE_START and PINCHGESTURE_SCALE events,
+ // in order to trigger a call to AsyncPanZoomController::OnScale
+ // but not to AsyncPanZoomController::OnScaleEnd.
+ ScreenIntPoint aFocus(250, 350);
+ ScreenIntPoint aSecondFocus(200, 300);
+ float aScale = 10;
+ auto event = CreatePinchGestureInput(PinchGestureInput::PINCHGESTURE_START,
+ aFocus, 10.0, 10.0, mcc->Time());
+ apzc->ReceiveInputEvent(event);
+
+ event =
+ CreatePinchGestureInput(PinchGestureInput::PINCHGESTURE_SCALE,
+ aSecondFocus, 10.0f * aScale, 10.0, mcc->Time());
+ apzc->ReceiveInputEvent(event);
+}
+
+TEST_F(APZCPinchLockingTester, Pinch_Locking_Free) {
+ SCOPED_GFX_PREF_INT("apz.pinch_lock.mode", 0); // PINCH_FREE
+
+ twoFingerPan();
+ EXPECT_FALSE(isPinchLockActive());
+}
+
+TEST_F(APZCPinchLockingTester, Pinch_Locking_Normal_Lock) {
+ SCOPED_GFX_PREF_INT("apz.pinch_lock.mode", 1); // PINCH_NORMAL
+
+ twoFingerPan();
+ EXPECT_TRUE(isPinchLockActive());
+}
+
+TEST_F(APZCPinchLockingTester, Pinch_Locking_Normal_Lock_Break) {
+ SCOPED_GFX_PREF_INT("apz.pinch_lock.mode", 1); // PINCH_NORMAL
+
+ twoFingerPan();
+ twoFingerZoom();
+ EXPECT_TRUE(isPinchLockActive());
+}
+
+TEST_F(APZCPinchLockingTester, Pinch_Locking_Sticky_Lock) {
+ SCOPED_GFX_PREF_INT("apz.pinch_lock.mode", 2); // PINCH_STICKY
+
+ twoFingerPan();
+ EXPECT_TRUE(isPinchLockActive());
+}
+
+TEST_F(APZCPinchLockingTester, Pinch_Locking_Sticky_Lock_Break) {
+ SCOPED_GFX_PREF_INT("apz.pinch_lock.mode", 2); // PINCH_STICKY
+
+ twoFingerPan();
+ twoFingerZoom();
+ EXPECT_FALSE(isPinchLockActive());
+}
+
+TEST_F(APZCPinchLockingTester, Pinch_Locking_Sticky_Lock_Break_Lock) {
+ SCOPED_GFX_PREF_INT("apz.pinch_lock.mode", 2); // PINCH_STICKY
+
+ twoFingerPan();
+ twoFingerZoom();
+ twoFingerPan();
+ EXPECT_TRUE(isPinchLockActive());
+}
diff --git a/gfx/layers/apz/test/gtest/TestPointerEventsConsumable.cpp b/gfx/layers/apz/test/gtest/TestPointerEventsConsumable.cpp
new file mode 100644
index 0000000000..1946baafe6
--- /dev/null
+++ b/gfx/layers/apz/test/gtest/TestPointerEventsConsumable.cpp
@@ -0,0 +1,500 @@
+/* -*- Mode: C+; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "gtest/gtest.h"
+
+#include "APZCTreeManagerTester.h"
+#include "APZTestCommon.h"
+#include "InputUtils.h"
+#include "apz/src/AsyncPanZoomController.h"
+#include "apz/src/InputBlockState.h"
+#include "apz/src/OverscrollHandoffState.h"
+#include "mozilla/layers/IAPZCTreeManager.h"
+
+class APZCArePointerEventsConsumable : public APZCTreeManagerTester {
+ public:
+ APZCArePointerEventsConsumable() { CreateMockHitTester(); }
+
+ void CreateSingleElementTree() {
+ const char* treeShape = "x";
+ LayerIntRegion layerVisibleRegion[] = {
+ LayerIntRect(0, 0, 100, 100),
+ };
+ CreateScrollData(treeShape, layerVisibleRegion);
+ SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID,
+ CSSRect(0, 0, 500, 500));
+
+ registration = MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc);
+
+ UpdateHitTestingTree();
+
+ ApzcOf(root)->GetFrameMetrics().SetIsRootContent(true);
+ }
+
+ void CreateScrollHandoffTree() {
+ const char* treeShape = "x(x)";
+ LayerIntRegion layerVisibleRegion[] = {LayerIntRect(0, 0, 200, 200),
+ LayerIntRect(50, 50, 100, 100)};
+ CreateScrollData(treeShape, layerVisibleRegion);
+ SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID,
+ CSSRect(0, 0, 300, 300));
+ SetScrollableFrameMetrics(layers[1],
+ ScrollableLayerGuid::START_SCROLL_ID + 1,
+ CSSRect(0, 0, 200, 200));
+ SetScrollHandoff(layers[1], root);
+ registration = MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc);
+ UpdateHitTestingTree();
+
+ ApzcOf(root)->GetFrameMetrics().SetIsRootContent(true);
+ }
+
+ RefPtr<TouchBlockState> CreateTouchBlockStateForApzc(
+ const RefPtr<TestAsyncPanZoomController>& aApzc) {
+ TouchCounter counter{};
+ TargetConfirmationFlags flags{true};
+
+ return new TouchBlockState(aApzc, flags, counter);
+ }
+
+ void UpdateOverscrollBehavior(ScrollableLayerGuid::ViewID aScrollId,
+ OverscrollBehavior aX, OverscrollBehavior aY) {
+ auto* layer = layers[aScrollId - ScrollableLayerGuid::START_SCROLL_ID];
+ ModifyFrameMetrics(layer, [aX, aY](ScrollMetadata& sm, FrameMetrics& _) {
+ OverscrollBehaviorInfo overscroll;
+ overscroll.mBehaviorX = aX;
+ overscroll.mBehaviorY = aY;
+ sm.SetOverscrollBehavior(overscroll);
+ });
+ UpdateHitTestingTree();
+ }
+
+ UniquePtr<ScopedLayerTreeRegistration> registration;
+};
+
+TEST_F(APZCArePointerEventsConsumable, EmptyInput) {
+ CreateSingleElementTree();
+
+ RefPtr<TestAsyncPanZoomController> apzc = ApzcOf(root);
+ RefPtr<TouchBlockState> blockState = CreateTouchBlockStateForApzc(apzc);
+
+ MultiTouchInput touchInput =
+ CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time());
+
+ const PointerEventsConsumableFlags expected{false, false};
+ const PointerEventsConsumableFlags actual =
+ apzc->ArePointerEventsConsumable(blockState, touchInput);
+ EXPECT_EQ(expected, actual);
+}
+
+TEST_F(APZCArePointerEventsConsumable, ScrollHorizontally) {
+ CreateSingleElementTree();
+
+ RefPtr<TestAsyncPanZoomController> apzc = ApzcOf(root);
+ RefPtr<TouchBlockState> blockState = CreateTouchBlockStateForApzc(apzc);
+
+ // Create touch with horizontal 20 unit scroll
+ MultiTouchInput touchStart =
+ CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time());
+ touchStart.mTouches.AppendElement(
+ SingleTouchData(0, ScreenIntPoint(10, 10), ScreenSize(0, 0), 0, 0));
+
+ MultiTouchInput touchMove =
+ CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, mcc->Time());
+ touchMove.mTouches.AppendElement(
+ SingleTouchData(0, ScreenIntPoint(30, 10), ScreenSize(0, 0), 0, 0));
+
+ blockState->UpdateSlopState(touchStart, false);
+
+ PointerEventsConsumableFlags actual{};
+ PointerEventsConsumableFlags expected{};
+
+ // Scroll area 500x500, room to pan x, room to pan y
+ expected = {true, true};
+ actual = apzc->ArePointerEventsConsumable(blockState, touchMove);
+ EXPECT_EQ(expected, actual);
+
+ // Scroll area 100x100, no room to pan x, no room to pan y
+ apzc->GetFrameMetrics().SetScrollableRect(CSSRect{0, 0, 100, 100});
+ expected = {false, true};
+ actual = apzc->ArePointerEventsConsumable(blockState, touchMove);
+ EXPECT_EQ(expected, actual);
+
+ // Scroll area 500x100, room to pan x, no room to pan y
+ apzc->GetFrameMetrics().SetScrollableRect(CSSRect{0, 0, 500, 100});
+ expected = {true, true};
+ actual = apzc->ArePointerEventsConsumable(blockState, touchMove);
+ EXPECT_EQ(expected, actual);
+
+ // Scroll area 100x500, no room to pan x, room to pan y
+ apzc->GetFrameMetrics().SetScrollableRect(CSSRect{0, 0, 100, 500});
+ expected = {false, true};
+ actual = apzc->ArePointerEventsConsumable(blockState, touchMove);
+ EXPECT_EQ(expected, actual);
+}
+
+TEST_F(APZCArePointerEventsConsumable, ScrollVertically) {
+ CreateSingleElementTree();
+
+ RefPtr<TestAsyncPanZoomController> apzc = ApzcOf(root);
+ RefPtr<TouchBlockState> blockState = CreateTouchBlockStateForApzc(apzc);
+
+ // Create touch with vertical 20 unit scroll
+ MultiTouchInput touchStart =
+ CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time());
+ touchStart.mTouches.AppendElement(
+ SingleTouchData(0, ScreenIntPoint(10, 10), ScreenSize(0, 0), 0, 0));
+
+ MultiTouchInput touchMove =
+ CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, mcc->Time());
+ touchMove.mTouches.AppendElement(
+ SingleTouchData(0, ScreenIntPoint(10, 30), ScreenSize(0, 0), 0, 0));
+
+ blockState->UpdateSlopState(touchStart, false);
+
+ PointerEventsConsumableFlags actual{};
+ PointerEventsConsumableFlags expected{};
+
+ // Scroll area 500x500, room to pan x, room to pan y
+ expected = {true, true};
+ actual = apzc->ArePointerEventsConsumable(blockState, touchMove);
+ EXPECT_EQ(expected, actual);
+
+ // Scroll area 100x100, no room to pan x, no room to pan y
+ apzc->GetFrameMetrics().SetScrollableRect(CSSRect{0, 0, 100, 100});
+ expected = {false, true};
+ actual = apzc->ArePointerEventsConsumable(blockState, touchMove);
+ EXPECT_EQ(expected, actual);
+
+ // Scroll area 500x100, room to pan x, no room to pan y
+ apzc->GetFrameMetrics().SetScrollableRect(CSSRect{0, 0, 500, 100});
+ expected = {false, true};
+ actual = apzc->ArePointerEventsConsumable(blockState, touchMove);
+ EXPECT_EQ(expected, actual);
+
+ // Scroll area 100x500, no room to pan x, room to pan y
+ apzc->GetFrameMetrics().SetScrollableRect(CSSRect{0, 0, 100, 500});
+ expected = {true, true};
+ actual = apzc->ArePointerEventsConsumable(blockState, touchMove);
+ EXPECT_EQ(expected, actual);
+}
+
+TEST_F(APZCArePointerEventsConsumable, NestedElementCanScroll) {
+ CreateScrollHandoffTree();
+
+ RefPtr<TestAsyncPanZoomController> apzc = ApzcOf(layers[1]);
+ RefPtr<TouchBlockState> blockState = CreateTouchBlockStateForApzc(apzc);
+
+ // Create touch with vertical 20 unit scroll
+ MultiTouchInput touchStart =
+ CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time());
+ touchStart.mTouches.AppendElement(
+ SingleTouchData(0, ScreenIntPoint(60, 60), ScreenSize(0, 0), 0, 0));
+
+ MultiTouchInput touchMove =
+ CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, mcc->Time());
+ touchMove.mTouches.AppendElement(
+ SingleTouchData(0, ScreenIntPoint(60, 80), ScreenSize(0, 0), 0, 0));
+
+ blockState->UpdateSlopState(touchStart, false);
+
+ const PointerEventsConsumableFlags expected{true, true};
+ const PointerEventsConsumableFlags actual =
+ apzc->ArePointerEventsConsumable(blockState, touchMove);
+ EXPECT_EQ(expected, actual);
+}
+
+TEST_F(APZCArePointerEventsConsumable, NestedElementCannotScroll) {
+ CreateScrollHandoffTree();
+
+ RefPtr<TestAsyncPanZoomController> apzc = ApzcOf(layers[1]);
+ RefPtr<TouchBlockState> blockState = CreateTouchBlockStateForApzc(apzc);
+
+ // Create touch with vertical 20 unit scroll
+ MultiTouchInput touchStart =
+ CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time());
+ touchStart.mTouches.AppendElement(
+ SingleTouchData(0, ScreenIntPoint(60, 60), ScreenSize(0, 0), 0, 0));
+
+ MultiTouchInput touchMove =
+ CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, mcc->Time());
+ touchMove.mTouches.AppendElement(
+ SingleTouchData(0, ScreenIntPoint(60, 80), ScreenSize(0, 0), 0, 0));
+
+ blockState->UpdateSlopState(touchStart, false);
+
+ PointerEventsConsumableFlags actual{};
+ PointerEventsConsumableFlags expected{};
+
+ // Set the nested element to have no room to scroll.
+ // Because of the overscroll handoff, we still have room to scroll
+ // in the parent element.
+ apzc->GetFrameMetrics().SetScrollableRect(CSSRect{0, 0, 100, 100});
+ expected = {true, true};
+ actual = apzc->ArePointerEventsConsumable(blockState, touchMove);
+ EXPECT_EQ(expected, actual);
+
+ // Set overscroll handoff for the nested element to none.
+ // Because no handoff will happen, we are not able to use the parent's
+ // room to scroll.
+ // Bug 1814886: Once fixed, change expected value to {false, true}.
+ UpdateOverscrollBehavior(ScrollableLayerGuid::START_SCROLL_ID + 1,
+ OverscrollBehavior::None, OverscrollBehavior::None);
+ expected = {true, true};
+ actual = apzc->ArePointerEventsConsumable(blockState, touchMove);
+ EXPECT_EQ(expected, actual);
+}
+
+TEST_F(APZCArePointerEventsConsumable, NotScrollableButZoomable) {
+ CreateSingleElementTree();
+
+ RefPtr<TestAsyncPanZoomController> apzc = ApzcOf(root);
+ RefPtr<TouchBlockState> blockState = CreateTouchBlockStateForApzc(apzc);
+
+ // Create touch with vertical 20 unit scroll
+ MultiTouchInput touchStart =
+ CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time());
+ touchStart.mTouches.AppendElement(
+ SingleTouchData(0, ScreenIntPoint(60, 60), ScreenSize(0, 0), 0, 0));
+
+ MultiTouchInput touchMove =
+ CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, mcc->Time());
+ touchMove.mTouches.AppendElement(
+ SingleTouchData(0, ScreenIntPoint(60, 80), ScreenSize(0, 0), 0, 0));
+
+ blockState->UpdateSlopState(touchStart, false);
+
+ // Make the root have no room to scroll
+ apzc->GetFrameMetrics().SetScrollableRect(CSSRect{0, 0, 100, 100});
+
+ // Make zoomable
+ apzc->UpdateZoomConstraints(ZoomConstraints(
+ true, true, CSSToParentLayerScale(0.25f), CSSToParentLayerScale(4.0f)));
+
+ PointerEventsConsumableFlags actual{};
+ PointerEventsConsumableFlags expected{};
+
+ expected = {false, true};
+ actual = apzc->ArePointerEventsConsumable(blockState, touchMove);
+ EXPECT_EQ(expected, actual);
+
+ // Add a second touch point and therefore make the APZC consider
+ // zoom use cases as well.
+ touchMove.mTouches.AppendElement(
+ SingleTouchData(0, ScreenIntPoint(60, 90), ScreenSize(0, 0), 0, 0));
+
+ expected = {true, true};
+ actual = apzc->ArePointerEventsConsumable(blockState, touchMove);
+ EXPECT_EQ(expected, actual);
+}
+
+TEST_F(APZCArePointerEventsConsumable, TouchActionsProhibitAll) {
+ CreateSingleElementTree();
+
+ RefPtr<TestAsyncPanZoomController> apzc = ApzcOf(root);
+
+ // Create touch with vertical 20 unit scroll
+ MultiTouchInput touchStart =
+ CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time());
+ touchStart.mTouches.AppendElement(
+ SingleTouchData(0, ScreenIntPoint(60, 60), ScreenSize(0, 0), 0, 0));
+
+ MultiTouchInput touchMove =
+ CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, mcc->Time());
+ touchMove.mTouches.AppendElement(
+ SingleTouchData(0, ScreenIntPoint(60, 80), ScreenSize(0, 0), 0, 0));
+
+ PointerEventsConsumableFlags expected{};
+ PointerEventsConsumableFlags actual{};
+
+ {
+ RefPtr<TouchBlockState> blockState = CreateTouchBlockStateForApzc(apzc);
+ blockState->UpdateSlopState(touchStart, false);
+
+ blockState->SetAllowedTouchBehaviors({AllowedTouchBehavior::NONE});
+ expected = {true, false};
+ actual = apzc->ArePointerEventsConsumable(blockState, touchMove);
+ EXPECT_EQ(expected, actual);
+ }
+
+ // Convert touch input to two-finger pinch
+ touchStart.mTouches.AppendElement(
+ SingleTouchData(1, ScreenIntPoint(80, 80), ScreenSize(0, 0), 0, 0));
+ touchMove.mTouches.AppendElement(
+ SingleTouchData(1, ScreenIntPoint(90, 90), ScreenSize(0, 0), 0, 0));
+
+ {
+ RefPtr<TouchBlockState> blockState = CreateTouchBlockStateForApzc(apzc);
+ blockState->UpdateSlopState(touchStart, false);
+
+ blockState->SetAllowedTouchBehaviors({AllowedTouchBehavior::NONE});
+ expected = {true, false};
+ actual = apzc->ArePointerEventsConsumable(blockState, touchMove);
+ EXPECT_EQ(expected, actual);
+ }
+}
+
+TEST_F(APZCArePointerEventsConsumable, TouchActionsAllowVerticalScrolling) {
+ CreateSingleElementTree();
+
+ RefPtr<TestAsyncPanZoomController> apzc = ApzcOf(root);
+
+ // Create touch with vertical 20 unit scroll
+ MultiTouchInput touchStart =
+ CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time());
+ touchStart.mTouches.AppendElement(
+ SingleTouchData(0, ScreenIntPoint(60, 60), ScreenSize(0, 0), 0, 0));
+
+ MultiTouchInput touchMove =
+ CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, mcc->Time());
+ touchMove.mTouches.AppendElement(
+ SingleTouchData(0, ScreenIntPoint(60, 80), ScreenSize(0, 0), 0, 0));
+
+ PointerEventsConsumableFlags expected{};
+ PointerEventsConsumableFlags actual{};
+
+ {
+ RefPtr<TouchBlockState> blockState = CreateTouchBlockStateForApzc(apzc);
+ blockState->UpdateSlopState(touchStart, false);
+
+ blockState->SetAllowedTouchBehaviors({AllowedTouchBehavior::NONE});
+ expected = {true, false};
+ actual = apzc->ArePointerEventsConsumable(blockState, touchMove);
+ EXPECT_EQ(expected, actual);
+ }
+
+ {
+ RefPtr<TouchBlockState> blockState = CreateTouchBlockStateForApzc(apzc);
+ blockState->UpdateSlopState(touchStart, false);
+
+ blockState->SetAllowedTouchBehaviors({AllowedTouchBehavior::VERTICAL_PAN});
+ expected = {true, true};
+ actual = apzc->ArePointerEventsConsumable(blockState, touchMove);
+ EXPECT_EQ(expected, actual);
+ }
+}
+
+TEST_F(APZCArePointerEventsConsumable, TouchActionsAllowHorizontalScrolling) {
+ CreateSingleElementTree();
+
+ RefPtr<TestAsyncPanZoomController> apzc = ApzcOf(root);
+
+ // Create touch with horizontal 20 unit scroll
+ MultiTouchInput touchStart =
+ CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time());
+ touchStart.mTouches.AppendElement(
+ SingleTouchData(0, ScreenIntPoint(60, 60), ScreenSize(0, 0), 0, 0));
+
+ MultiTouchInput touchMove =
+ CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, mcc->Time());
+ touchMove.mTouches.AppendElement(
+ SingleTouchData(0, ScreenIntPoint(80, 60), ScreenSize(0, 0), 0, 0));
+
+ PointerEventsConsumableFlags expected{};
+ PointerEventsConsumableFlags actual{};
+
+ {
+ RefPtr<TouchBlockState> blockState = CreateTouchBlockStateForApzc(apzc);
+ blockState->UpdateSlopState(touchStart, false);
+
+ blockState->SetAllowedTouchBehaviors({AllowedTouchBehavior::NONE});
+ expected = {true, false};
+ actual = apzc->ArePointerEventsConsumable(blockState, touchMove);
+ EXPECT_EQ(expected, actual);
+ }
+
+ {
+ RefPtr<TouchBlockState> blockState = CreateTouchBlockStateForApzc(apzc);
+ blockState->UpdateSlopState(touchStart, false);
+
+ blockState->SetAllowedTouchBehaviors(
+ {AllowedTouchBehavior::HORIZONTAL_PAN});
+ expected = {true, true};
+ actual = apzc->ArePointerEventsConsumable(blockState, touchMove);
+ EXPECT_EQ(expected, actual);
+ }
+}
+
+TEST_F(APZCArePointerEventsConsumable, TouchActionsAllowPinchZoom) {
+ CreateSingleElementTree();
+
+ RefPtr<TestAsyncPanZoomController> apzc = ApzcOf(root);
+
+ // Create two-finger pinch
+ MultiTouchInput touchStart =
+ CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time());
+ touchStart.mTouches.AppendElement(
+ SingleTouchData(0, ScreenIntPoint(60, 60), ScreenSize(0, 0), 0, 0));
+ touchStart.mTouches.AppendElement(
+ SingleTouchData(1, ScreenIntPoint(80, 80), ScreenSize(0, 0), 0, 0));
+
+ MultiTouchInput touchMove =
+ CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, mcc->Time());
+ touchMove.mTouches.AppendElement(
+ SingleTouchData(0, ScreenIntPoint(50, 50), ScreenSize(0, 0), 0, 0));
+ touchMove.mTouches.AppendElement(
+ SingleTouchData(1, ScreenIntPoint(90, 90), ScreenSize(0, 0), 0, 0));
+
+ PointerEventsConsumableFlags expected{};
+ PointerEventsConsumableFlags actual{};
+
+ {
+ RefPtr<TouchBlockState> blockState = CreateTouchBlockStateForApzc(apzc);
+ blockState->UpdateSlopState(touchStart, false);
+
+ blockState->SetAllowedTouchBehaviors({AllowedTouchBehavior::NONE});
+ expected = {true, false};
+ actual = apzc->ArePointerEventsConsumable(blockState, touchMove);
+ EXPECT_EQ(expected, actual);
+ }
+
+ {
+ RefPtr<TouchBlockState> blockState = CreateTouchBlockStateForApzc(apzc);
+ blockState->UpdateSlopState(touchStart, false);
+
+ blockState->SetAllowedTouchBehaviors({AllowedTouchBehavior::PINCH_ZOOM});
+ expected = {true, true};
+ actual = apzc->ArePointerEventsConsumable(blockState, touchMove);
+ EXPECT_EQ(expected, actual);
+ }
+}
+
+TEST_F(APZCArePointerEventsConsumable, DynamicToolbar) {
+ CreateSingleElementTree();
+
+ RefPtr<TestAsyncPanZoomController> apzc = ApzcOf(root);
+ RefPtr<TouchBlockState> blockState = CreateTouchBlockStateForApzc(apzc);
+
+ // Create touch with vertical 20 unit scroll
+ MultiTouchInput touchStart =
+ CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time());
+ touchStart.mTouches.AppendElement(
+ SingleTouchData(0, ScreenIntPoint(60, 30), ScreenSize(0, 0), 0, 0));
+
+ MultiTouchInput touchMove =
+ CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, mcc->Time());
+ touchMove.mTouches.AppendElement(
+ SingleTouchData(0, ScreenIntPoint(60, 40), ScreenSize(0, 0), 0, 0));
+
+ blockState->UpdateSlopState(touchStart, false);
+
+ // Restrict size of scrollable area: No room to pan X, no room to pan Y
+ apzc->GetFrameMetrics().SetScrollableRect(CSSRect{0, 0, 100, 100});
+
+ PointerEventsConsumableFlags actual{};
+ PointerEventsConsumableFlags expected{};
+
+ expected = {false, true};
+ actual = apzc->ArePointerEventsConsumable(blockState, touchMove);
+ EXPECT_EQ(expected, actual);
+
+ apzc->GetFrameMetrics().SetCompositionSizeWithoutDynamicToolbar(
+ ParentLayerSize{100, 90});
+ UpdateHitTestingTree();
+
+ expected = {true, true};
+ actual = apzc->ArePointerEventsConsumable(blockState, touchMove);
+ EXPECT_EQ(expected, actual);
+}
diff --git a/gfx/layers/apz/test/gtest/TestScrollHandoff.cpp b/gfx/layers/apz/test/gtest/TestScrollHandoff.cpp
new file mode 100644
index 0000000000..8f497dabe6
--- /dev/null
+++ b/gfx/layers/apz/test/gtest/TestScrollHandoff.cpp
@@ -0,0 +1,809 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "APZCTreeManagerTester.h"
+#include "APZTestCommon.h"
+#include "InputUtils.h"
+
+class APZScrollHandoffTester : public APZCTreeManagerTester {
+ protected:
+ UniquePtr<ScopedLayerTreeRegistration> registration;
+ TestAsyncPanZoomController* rootApzc;
+
+ void CreateScrollHandoffLayerTree1() {
+ const char* treeShape = "x(x)";
+ LayerIntRegion layerVisibleRegion[] = {LayerIntRect(0, 0, 100, 100),
+ LayerIntRect(0, 50, 100, 50)};
+ CreateScrollData(treeShape, layerVisibleRegion);
+ SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID,
+ CSSRect(0, 0, 200, 200));
+ SetScrollableFrameMetrics(layers[1],
+ ScrollableLayerGuid::START_SCROLL_ID + 1,
+ CSSRect(0, 0, 100, 100));
+ SetScrollHandoff(layers[1], root);
+ registration = MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc);
+ UpdateHitTestingTree();
+ rootApzc = ApzcOf(root);
+ rootApzc->GetFrameMetrics().SetIsRootContent(
+ true); // make root APZC zoomable
+ }
+
+ void CreateScrollHandoffLayerTree2() {
+ const char* treeShape = "x(x(x))";
+ LayerIntRegion layerVisibleRegion[] = {LayerIntRect(0, 0, 100, 100),
+ LayerIntRect(0, 0, 100, 100),
+ LayerIntRect(0, 50, 100, 50)};
+ CreateScrollData(treeShape, layerVisibleRegion);
+ SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID,
+ CSSRect(0, 0, 200, 200));
+ SetScrollableFrameMetrics(layers[1],
+ ScrollableLayerGuid::START_SCROLL_ID + 2,
+ CSSRect(-100, -100, 200, 200));
+ SetScrollableFrameMetrics(layers[2],
+ ScrollableLayerGuid::START_SCROLL_ID + 1,
+ CSSRect(0, 0, 100, 100));
+ SetScrollHandoff(layers[1], root);
+ SetScrollHandoff(layers[2], layers[1]);
+ // No ScopedLayerTreeRegistration as that just needs to be done once per
+ // test and this is the second layer tree for a particular test.
+ MOZ_ASSERT(registration);
+ UpdateHitTestingTree();
+ rootApzc = ApzcOf(root);
+ }
+
+ void CreateScrollHandoffLayerTree3() {
+ const char* treeShape = "x(x(x)x(x))";
+ LayerIntRegion layerVisibleRegion[] = {
+ LayerIntRect(0, 0, 100, 100), // root
+ LayerIntRect(0, 0, 100, 50), // scrolling parent 1
+ LayerIntRect(0, 0, 100, 50), // scrolling child 1
+ LayerIntRect(0, 50, 100, 50), // scrolling parent 2
+ LayerIntRect(0, 50, 100, 50) // scrolling child 2
+ };
+ CreateScrollData(treeShape, layerVisibleRegion);
+ SetScrollableFrameMetrics(layers[0], ScrollableLayerGuid::START_SCROLL_ID,
+ CSSRect(0, 0, 100, 100));
+ SetScrollableFrameMetrics(layers[1],
+ ScrollableLayerGuid::START_SCROLL_ID + 1,
+ CSSRect(0, 0, 100, 100));
+ SetScrollableFrameMetrics(layers[2],
+ ScrollableLayerGuid::START_SCROLL_ID + 2,
+ CSSRect(0, 0, 100, 100));
+ SetScrollableFrameMetrics(layers[3],
+ ScrollableLayerGuid::START_SCROLL_ID + 3,
+ CSSRect(0, 50, 100, 100));
+ SetScrollableFrameMetrics(layers[4],
+ ScrollableLayerGuid::START_SCROLL_ID + 4,
+ CSSRect(0, 50, 100, 100));
+ SetScrollHandoff(layers[1], layers[0]);
+ SetScrollHandoff(layers[3], layers[0]);
+ SetScrollHandoff(layers[2], layers[1]);
+ SetScrollHandoff(layers[4], layers[3]);
+ registration = MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc);
+ UpdateHitTestingTree();
+ }
+
+ // Creates a layer tree with a parent layer that is only scrollable
+ // horizontally, and a child layer that is only scrollable vertically.
+ void CreateScrollHandoffLayerTree4() {
+ const char* treeShape = "x(x)";
+ LayerIntRegion layerVisibleRegion[] = {LayerIntRect(0, 0, 100, 100),
+ LayerIntRect(0, 0, 100, 100)};
+ CreateScrollData(treeShape, layerVisibleRegion);
+ SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID,
+ CSSRect(0, 0, 200, 100));
+ SetScrollableFrameMetrics(layers[1],
+ ScrollableLayerGuid::START_SCROLL_ID + 1,
+ CSSRect(0, 0, 100, 200));
+ SetScrollHandoff(layers[1], root);
+ registration = MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc);
+ UpdateHitTestingTree();
+ rootApzc = ApzcOf(root);
+ }
+
+ // Creates a layer tree with a parent layer that is not scrollable, and a
+ // child layer that is only scrollable vertically.
+ void CreateScrollHandoffLayerTree5() {
+ const char* treeShape = "x(x)";
+ LayerIntRegion layerVisibleRegion[] = {
+ LayerIntRect(0, 0, 100, 100), // scrolling parent
+ LayerIntRect(0, 50, 100, 50) // scrolling child
+ };
+ CreateScrollData(treeShape, layerVisibleRegion);
+ SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID,
+ CSSRect(0, 0, 100, 100));
+ SetScrollableFrameMetrics(layers[1],
+ ScrollableLayerGuid::START_SCROLL_ID + 1,
+ CSSRect(0, 0, 100, 200));
+ SetScrollHandoff(layers[1], root);
+ registration = MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc);
+ UpdateHitTestingTree();
+ rootApzc = ApzcOf(root);
+ }
+
+ void CreateScrollgrabLayerTree(bool makeParentScrollable = true) {
+ const char* treeShape = "x(x)";
+ LayerIntRegion layerVisibleRegion[] = {
+ LayerIntRect(0, 0, 100, 100), // scroll-grabbing parent
+ LayerIntRect(0, 20, 100, 80) // child
+ };
+ CreateScrollData(treeShape, layerVisibleRegion);
+ float parentHeight = makeParentScrollable ? 120 : 100;
+ SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID,
+ CSSRect(0, 0, 100, parentHeight));
+ SetScrollableFrameMetrics(layers[1],
+ ScrollableLayerGuid::START_SCROLL_ID + 1,
+ CSSRect(0, 0, 100, 800));
+ SetScrollHandoff(layers[1], root);
+ registration = MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc);
+ UpdateHitTestingTree();
+ rootApzc = ApzcOf(root);
+ rootApzc->GetScrollMetadata().SetHasScrollgrab(true);
+ }
+
+ void TestFlingAcceleration() {
+ // Jack up the fling acceleration multiplier so we can easily determine
+ // whether acceleration occured.
+ const float kAcceleration = 100.0f;
+ SCOPED_GFX_PREF_FLOAT("apz.fling_accel_base_mult", kAcceleration);
+ SCOPED_GFX_PREF_FLOAT("apz.fling_accel_min_fling_velocity", 0.0);
+ SCOPED_GFX_PREF_FLOAT("apz.fling_accel_min_pan_velocity", 0.0);
+
+ RefPtr<TestAsyncPanZoomController> childApzc = ApzcOf(layers[1]);
+
+ // Pan once, enough to fully scroll the scrollgrab parent and then scroll
+ // and fling the child.
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
+ Pan(manager, 70, 40);
+
+ // Give the fling animation a chance to start.
+ SampleAnimationsOnce();
+
+ float childVelocityAfterFling1 = childApzc->GetVelocityVector().y;
+
+ // Pan again.
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
+ Pan(manager, 70, 40);
+
+ // Give the fling animation a chance to start.
+ // This time it should be accelerated.
+ SampleAnimationsOnce();
+
+ float childVelocityAfterFling2 = childApzc->GetVelocityVector().y;
+
+ // We should have accelerated once.
+ // The division by 2 is to account for friction.
+ EXPECT_GT(childVelocityAfterFling2,
+ childVelocityAfterFling1 * kAcceleration / 2);
+
+ // We should not have accelerated twice.
+ // The division by 4 is to account for friction.
+ EXPECT_LE(childVelocityAfterFling2,
+ childVelocityAfterFling1 * kAcceleration * kAcceleration / 4);
+ }
+
+ void TestCrossApzcAxisLock() {
+ SCOPED_GFX_PREF_INT("apz.axis_lock.mode", 1);
+
+ CreateScrollHandoffLayerTree1();
+
+ RefPtr<TestAsyncPanZoomController> childApzc = ApzcOf(layers[1]);
+ Pan(childApzc, ScreenIntPoint(10, 60), ScreenIntPoint(15, 90),
+ PanOptions::KeepFingerDown | PanOptions::ExactCoordinates);
+
+ childApzc->AssertAxisLocked(ScrollDirection::eVertical);
+ childApzc->AssertStateIsPanningLockedY();
+ }
+};
+
+class APZScrollHandoffTesterMock : public APZScrollHandoffTester {
+ public:
+ APZScrollHandoffTesterMock() { CreateMockHitTester(); }
+};
+
+#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android
+// Here we test that if the processing of a touch block is deferred while we
+// wait for content to send a prevent-default message, overscroll is still
+// handed off correctly when the block is processed.
+TEST_F(APZScrollHandoffTester, DeferredInputEventProcessing) {
+ SCOPED_GFX_PREF_BOOL("apz.allow_immediate_handoff", true);
+
+ // Set up the APZC tree.
+ CreateScrollHandoffLayerTree1();
+
+ RefPtr<TestAsyncPanZoomController> childApzc = ApzcOf(layers[1]);
+
+ // Enable touch-listeners so that we can separate the queueing of input
+ // events from them being processed.
+ childApzc->SetWaitForMainThread();
+
+ // Queue input events for a pan.
+ uint64_t blockId = 0;
+ Pan(childApzc, 90, 30, PanOptions::NoFling, nullptr, nullptr, &blockId);
+
+ // Allow the pan to be processed.
+ childApzc->ContentReceivedInputBlock(blockId, false);
+ childApzc->ConfirmTarget(blockId);
+
+ // Make sure overscroll was handed off correctly.
+ EXPECT_EQ(50, childApzc->GetFrameMetrics().GetVisualScrollOffset().y);
+ EXPECT_EQ(10, rootApzc->GetFrameMetrics().GetVisualScrollOffset().y);
+}
+#endif
+
+#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android
+// Here we test that if the layer structure changes in between two input
+// blocks being queued, and the first block is only processed after the second
+// one has been queued, overscroll handoff for the first block follows
+// the original layer structure while overscroll handoff for the second block
+// follows the new layer structure.
+TEST_F(APZScrollHandoffTester, LayerStructureChangesWhileEventsArePending) {
+ SCOPED_GFX_PREF_BOOL("apz.allow_immediate_handoff", true);
+
+ // Set up an initial APZC tree.
+ CreateScrollHandoffLayerTree1();
+
+ RefPtr<TestAsyncPanZoomController> childApzc = ApzcOf(layers[1]);
+
+ // Enable touch-listeners so that we can separate the queueing of input
+ // events from them being processed.
+ childApzc->SetWaitForMainThread();
+
+ // Queue input events for a pan.
+ uint64_t blockId = 0;
+ Pan(childApzc, 90, 30, PanOptions::NoFling, nullptr, nullptr, &blockId);
+
+ // Modify the APZC tree to insert a new APZC 'middle' into the handoff chain
+ // between the child and the root.
+ CreateScrollHandoffLayerTree2();
+ WebRenderLayerScrollData* middle = layers[1];
+ childApzc->SetWaitForMainThread();
+ TestAsyncPanZoomController* middleApzc = ApzcOf(middle);
+
+ // Queue input events for another pan.
+ uint64_t secondBlockId = 0;
+ Pan(childApzc, 30, 90, PanOptions::NoFling, nullptr, nullptr, &secondBlockId);
+
+ // Allow the first pan to be processed.
+ childApzc->ContentReceivedInputBlock(blockId, false);
+ childApzc->ConfirmTarget(blockId);
+
+ // Make sure things have scrolled according to the handoff chain in
+ // place at the time the touch-start of the first pan was queued.
+ EXPECT_EQ(50, childApzc->GetFrameMetrics().GetVisualScrollOffset().y);
+ EXPECT_EQ(10, rootApzc->GetFrameMetrics().GetVisualScrollOffset().y);
+ EXPECT_EQ(0, middleApzc->GetFrameMetrics().GetVisualScrollOffset().y);
+
+ // Allow the second pan to be processed.
+ childApzc->ContentReceivedInputBlock(secondBlockId, false);
+ childApzc->ConfirmTarget(secondBlockId);
+
+ // Make sure things have scrolled according to the handoff chain in
+ // place at the time the touch-start of the second pan was queued.
+ EXPECT_EQ(0, childApzc->GetFrameMetrics().GetVisualScrollOffset().y);
+ EXPECT_EQ(10, rootApzc->GetFrameMetrics().GetVisualScrollOffset().y);
+ EXPECT_EQ(-10, middleApzc->GetFrameMetrics().GetVisualScrollOffset().y);
+}
+#endif
+
+#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android
+// Test that putting a second finger down on an APZC while a down-chain APZC
+// is overscrolled doesn't result in being stuck in overscroll.
+TEST_F(APZScrollHandoffTesterMock, StuckInOverscroll_Bug1073250) {
+ // Enable overscrolling.
+ SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
+ SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0f);
+
+ CreateScrollHandoffLayerTree1();
+
+ TestAsyncPanZoomController* child = ApzcOf(layers[1]);
+
+ // Pan, causing the parent APZC to overscroll.
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ Pan(manager, 10, 40, PanOptions::KeepFingerDown);
+ EXPECT_FALSE(child->IsOverscrolled());
+ EXPECT_TRUE(rootApzc->IsOverscrolled());
+
+ // Put a second finger down.
+ MultiTouchInput secondFingerDown =
+ CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time());
+ // Use the same touch identifier for the first touch (0) as Pan(). (A bit
+ // hacky.)
+ secondFingerDown.mTouches.AppendElement(
+ SingleTouchData(0, ScreenIntPoint(10, 40), ScreenSize(0, 0), 0, 0));
+ secondFingerDown.mTouches.AppendElement(
+ SingleTouchData(1, ScreenIntPoint(30, 20), ScreenSize(0, 0), 0, 0));
+ manager->ReceiveInputEvent(secondFingerDown);
+
+ // Release the fingers.
+ MultiTouchInput fingersUp = secondFingerDown;
+ fingersUp.mType = MultiTouchInput::MULTITOUCH_END;
+ manager->ReceiveInputEvent(fingersUp);
+
+ // Allow any animations to run their course.
+ child->AdvanceAnimationsUntilEnd();
+ rootApzc->AdvanceAnimationsUntilEnd();
+
+ // Make sure nothing is overscrolled.
+ EXPECT_FALSE(child->IsOverscrolled());
+ EXPECT_FALSE(rootApzc->IsOverscrolled());
+}
+#endif
+
+#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android
+// This is almost exactly like StuckInOverscroll_Bug1073250, except the
+// APZC receiving the input events for the first touch block is the child
+// (and thus not the same APZC that overscrolls, which is the parent).
+TEST_F(APZScrollHandoffTesterMock, StuckInOverscroll_Bug1231228) {
+ // Enable overscrolling.
+ SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
+ SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0f);
+
+ CreateScrollHandoffLayerTree1();
+
+ TestAsyncPanZoomController* child = ApzcOf(layers[1]);
+
+ // Pan, causing the parent APZC to overscroll.
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
+ Pan(manager, 60, 90, PanOptions::KeepFingerDown);
+ EXPECT_FALSE(child->IsOverscrolled());
+ EXPECT_TRUE(rootApzc->IsOverscrolled());
+
+ // Put a second finger down.
+ MultiTouchInput secondFingerDown =
+ CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time());
+ // Use the same touch identifier for the first touch (0) as Pan(). (A bit
+ // hacky.)
+ secondFingerDown.mTouches.AppendElement(
+ SingleTouchData(0, ScreenIntPoint(10, 40), ScreenSize(0, 0), 0, 0));
+ secondFingerDown.mTouches.AppendElement(
+ SingleTouchData(1, ScreenIntPoint(30, 20), ScreenSize(0, 0), 0, 0));
+ manager->ReceiveInputEvent(secondFingerDown);
+
+ // Release the fingers.
+ MultiTouchInput fingersUp = secondFingerDown;
+ fingersUp.mType = MultiTouchInput::MULTITOUCH_END;
+ manager->ReceiveInputEvent(fingersUp);
+
+ // Allow any animations to run their course.
+ child->AdvanceAnimationsUntilEnd();
+ rootApzc->AdvanceAnimationsUntilEnd();
+
+ // Make sure nothing is overscrolled.
+ EXPECT_FALSE(child->IsOverscrolled());
+ EXPECT_FALSE(rootApzc->IsOverscrolled());
+}
+#endif
+
+#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android
+TEST_F(APZScrollHandoffTester, StuckInOverscroll_Bug1240202a) {
+ // Enable overscrolling.
+ SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
+
+ CreateScrollHandoffLayerTree1();
+
+ TestAsyncPanZoomController* child = ApzcOf(layers[1]);
+
+ // Pan, causing the parent APZC to overscroll.
+ Pan(manager, 60, 90, PanOptions::KeepFingerDown);
+ EXPECT_FALSE(child->IsOverscrolled());
+ EXPECT_TRUE(rootApzc->IsOverscrolled());
+
+ // Lift the finger, triggering an overscroll animation
+ // (but don't allow it to run).
+ TouchUp(manager, ScreenIntPoint(10, 90), mcc->Time());
+
+ // Put the finger down again, interrupting the animation
+ // and entering the TOUCHING state.
+ TouchDown(manager, ScreenIntPoint(10, 90), mcc->Time());
+
+ // Lift the finger once again.
+ TouchUp(manager, ScreenIntPoint(10, 90), mcc->Time());
+
+ // Allow any animations to run their course.
+ child->AdvanceAnimationsUntilEnd();
+ rootApzc->AdvanceAnimationsUntilEnd();
+
+ // Make sure nothing is overscrolled.
+ EXPECT_FALSE(child->IsOverscrolled());
+ EXPECT_FALSE(rootApzc->IsOverscrolled());
+}
+#endif
+
+#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android
+TEST_F(APZScrollHandoffTesterMock, StuckInOverscroll_Bug1240202b) {
+ // Enable overscrolling.
+ SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
+
+ CreateScrollHandoffLayerTree1();
+
+ TestAsyncPanZoomController* child = ApzcOf(layers[1]);
+
+ // Pan, causing the parent APZC to overscroll.
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
+ Pan(manager, 60, 90, PanOptions::KeepFingerDown);
+ EXPECT_FALSE(child->IsOverscrolled());
+ EXPECT_TRUE(rootApzc->IsOverscrolled());
+
+ // Lift the finger, triggering an overscroll animation
+ // (but don't allow it to run).
+ TouchUp(manager, ScreenIntPoint(10, 90), mcc->Time());
+
+ // Put the finger down again, interrupting the animation
+ // and entering the TOUCHING state.
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
+ TouchDown(manager, ScreenIntPoint(10, 90), mcc->Time());
+
+ // Put a second finger down. Since we're in the TOUCHING state,
+ // the "are we panned into overscroll" check will fail and we
+ // will not ignore the second finger, instead entering the
+ // PINCHING state.
+ MultiTouchInput secondFingerDown(MultiTouchInput::MULTITOUCH_START, 0,
+ TimeStamp(), 0);
+ // Use the same touch identifier for the first touch (0) as TouchDown(). (A
+ // bit hacky.)
+ secondFingerDown.mTouches.AppendElement(
+ SingleTouchData(0, ScreenIntPoint(10, 90), ScreenSize(0, 0), 0, 0));
+ secondFingerDown.mTouches.AppendElement(
+ SingleTouchData(1, ScreenIntPoint(10, 80), ScreenSize(0, 0), 0, 0));
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
+ manager->ReceiveInputEvent(secondFingerDown);
+
+ // Release the fingers.
+ MultiTouchInput fingersUp = secondFingerDown;
+ fingersUp.mType = MultiTouchInput::MULTITOUCH_END;
+ manager->ReceiveInputEvent(fingersUp);
+
+ // Allow any animations to run their course.
+ child->AdvanceAnimationsUntilEnd();
+ rootApzc->AdvanceAnimationsUntilEnd();
+
+ // Make sure nothing is overscrolled.
+ EXPECT_FALSE(child->IsOverscrolled());
+ EXPECT_FALSE(rootApzc->IsOverscrolled());
+}
+#endif
+
+#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android
+TEST_F(APZScrollHandoffTester, OpposingConstrainedAxes_Bug1201098) {
+ // Enable overscrolling.
+ SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
+
+ CreateScrollHandoffLayerTree4();
+
+ RefPtr<TestAsyncPanZoomController> childApzc = ApzcOf(layers[1]);
+
+ // Pan, causing the child APZC to overscroll.
+ Pan(childApzc, 50, 60);
+
+ // Make sure only the child is overscrolled.
+ EXPECT_TRUE(childApzc->IsOverscrolled());
+ EXPECT_FALSE(rootApzc->IsOverscrolled());
+}
+#endif
+
+// Test that flinging in a direction where one component of the fling goes into
+// overscroll but the other doesn't, results in just the one component being
+// handed off to the parent, while the original APZC continues flinging in the
+// other direction.
+TEST_F(APZScrollHandoffTesterMock, PartialFlingHandoff) {
+ SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0f);
+
+ CreateScrollHandoffLayerTree1();
+
+ // Fling up and to the left. The child APZC has room to scroll up, but not
+ // to the left, so the horizontal component of the fling should be handed
+ // off to the parent APZC.
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
+ Pan(manager, ScreenIntPoint(90, 90), ScreenIntPoint(55, 55));
+
+ RefPtr<TestAsyncPanZoomController> parent = ApzcOf(layers[0]);
+ RefPtr<TestAsyncPanZoomController> child = ApzcOf(layers[1]);
+
+ // Advance the child's fling animation once to give the partial handoff
+ // a chance to occur.
+ mcc->AdvanceByMillis(10);
+ child->AdvanceAnimations(mcc->GetSampleTime());
+
+ // Assert that partial handoff has occurred.
+ child->AssertStateIsFling();
+ parent->AssertStateIsFling();
+}
+
+// Here we test that if two flings are happening simultaneously, overscroll
+// is handed off correctly for each.
+TEST_F(APZScrollHandoffTester, SimultaneousFlings) {
+ SCOPED_GFX_PREF_BOOL("apz.allow_immediate_handoff", true);
+ SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0f);
+
+ // Set up an initial APZC tree.
+ CreateScrollHandoffLayerTree3();
+
+ RefPtr<TestAsyncPanZoomController> parent1 = ApzcOf(layers[1]);
+ RefPtr<TestAsyncPanZoomController> child1 = ApzcOf(layers[2]);
+ RefPtr<TestAsyncPanZoomController> parent2 = ApzcOf(layers[3]);
+ RefPtr<TestAsyncPanZoomController> child2 = ApzcOf(layers[4]);
+
+ // Pan on the lower child.
+ Pan(child2, 45, 5);
+
+ // Pan on the upper child.
+ Pan(child1, 95, 55);
+
+ // Check that child1 and child2 are in a FLING state.
+ child1->AssertStateIsFling();
+ child2->AssertStateIsFling();
+
+ // Advance the animations on child1 and child2 until their end.
+ child1->AdvanceAnimationsUntilEnd();
+ child2->AdvanceAnimationsUntilEnd();
+
+ // Check that the flings have been handed off to the parents.
+ child1->AssertStateIsReset();
+ parent1->AssertStateIsFling();
+ child2->AssertStateIsReset();
+ parent2->AssertStateIsFling();
+}
+
+#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android
+TEST_F(APZScrollHandoffTester, Scrollgrab) {
+ SCOPED_GFX_PREF_BOOL("apz.allow_immediate_handoff", true);
+
+ // Set up the layer tree
+ CreateScrollgrabLayerTree();
+
+ RefPtr<TestAsyncPanZoomController> childApzc = ApzcOf(layers[1]);
+
+ // Pan on the child, enough to fully scroll the scrollgrab parent (20 px)
+ // and leave some more (another 15 px) for the child.
+ Pan(childApzc, 80, 45);
+
+ // Check that the parent and child have scrolled as much as we expect.
+ EXPECT_EQ(20, rootApzc->GetFrameMetrics().GetVisualScrollOffset().y);
+ EXPECT_EQ(15, childApzc->GetFrameMetrics().GetVisualScrollOffset().y);
+}
+#endif
+
+TEST_F(APZScrollHandoffTester, ScrollgrabFling) {
+ SCOPED_GFX_PREF_BOOL("apz.allow_immediate_handoff", true);
+ SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0f);
+
+ // Set up the layer tree
+ CreateScrollgrabLayerTree();
+
+ RefPtr<TestAsyncPanZoomController> childApzc = ApzcOf(layers[1]);
+
+ // Pan on the child, not enough to fully scroll the scrollgrab parent.
+ Pan(childApzc, 80, 70);
+
+ // Check that it is the scrollgrab parent that's in a fling, not the child.
+ rootApzc->AssertStateIsFling();
+ childApzc->AssertStateIsReset();
+}
+
+TEST_F(APZScrollHandoffTesterMock, ScrollgrabFlingAcceleration1) {
+ SCOPED_GFX_PREF_BOOL("apz.allow_immediate_handoff", true);
+ SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0f);
+ CreateScrollgrabLayerTree(true /* make parent scrollable */);
+
+ // Note: Usually, fling acceleration does not work across handoff, because our
+ // fling acceleration code does not propagate the "fling cancel velocity"
+ // across handoff. However, this test sets apz.fling_min_velocity_threshold to
+ // zero, so the "fling cancel velocity" is allowed to be zero, and fling
+ // acceleration succeeds, almost by accident.
+ TestFlingAcceleration();
+}
+
+TEST_F(APZScrollHandoffTesterMock, ScrollgrabFlingAcceleration2) {
+ SCOPED_GFX_PREF_BOOL("apz.allow_immediate_handoff", true);
+ SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0f);
+ CreateScrollgrabLayerTree(false /* do not make parent scrollable */);
+ TestFlingAcceleration();
+}
+
+TEST_F(APZScrollHandoffTester, ImmediateHandoffDisallowed_Pan) {
+ SCOPED_GFX_PREF_BOOL("apz.allow_immediate_handoff", false);
+
+ CreateScrollHandoffLayerTree1();
+
+ RefPtr<TestAsyncPanZoomController> parentApzc = ApzcOf(layers[0]);
+ RefPtr<TestAsyncPanZoomController> childApzc = ApzcOf(layers[1]);
+
+ // Pan on the child, enough to scroll it to its end and have scroll
+ // left to hand off. Since immediate handoff is disallowed, we expect
+ // the leftover scroll not to be handed off.
+ Pan(childApzc, 60, 5);
+
+ // Verify that the parent has not scrolled.
+ EXPECT_EQ(50, childApzc->GetFrameMetrics().GetVisualScrollOffset().y);
+ EXPECT_EQ(0, parentApzc->GetFrameMetrics().GetVisualScrollOffset().y);
+
+ // Pan again on the child. This time, since the child was scrolled to
+ // its end when the gesture began, we expect the scroll to be handed off.
+ Pan(childApzc, 60, 50);
+
+ // Verify that the parent scrolled.
+ EXPECT_EQ(10, parentApzc->GetFrameMetrics().GetVisualScrollOffset().y);
+}
+
+TEST_F(APZScrollHandoffTester, ImmediateHandoffDisallowed_Fling) {
+ SCOPED_GFX_PREF_BOOL("apz.allow_immediate_handoff", false);
+ SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0f);
+
+ CreateScrollHandoffLayerTree1();
+
+ RefPtr<TestAsyncPanZoomController> parentApzc = ApzcOf(layers[0]);
+ RefPtr<TestAsyncPanZoomController> childApzc = ApzcOf(layers[1]);
+
+ // Pan on the child, enough to get very close to the end, so that the
+ // subsequent fling reaches the end and has leftover velocity to hand off.
+ Pan(childApzc, 60, 2);
+
+ // Allow the fling to run its course.
+ childApzc->AdvanceAnimationsUntilEnd();
+ parentApzc->AdvanceAnimationsUntilEnd();
+
+ // Verify that the parent has not scrolled.
+ // The first comparison needs to be an ASSERT_NEAR because the fling
+ // computations are such that the final scroll position can be within
+ // COORDINATE_EPSILON of the end rather than right at the end.
+ ASSERT_NEAR(50, childApzc->GetFrameMetrics().GetVisualScrollOffset().y,
+ COORDINATE_EPSILON);
+ EXPECT_EQ(0, parentApzc->GetFrameMetrics().GetVisualScrollOffset().y);
+
+ // Pan again on the child. This time, since the child was scrolled to
+ // its end when the gesture began, we expect the scroll to be handed off.
+ Pan(childApzc, 60, 40);
+
+ // Allow the fling to run its course. The fling should also be handed off.
+ childApzc->AdvanceAnimationsUntilEnd();
+ parentApzc->AdvanceAnimationsUntilEnd();
+
+ // Verify that the parent scrolled from the fling.
+ EXPECT_GT(parentApzc->GetFrameMetrics().GetVisualScrollOffset().y, 10);
+}
+
+TEST_F(APZScrollHandoffTester, CrossApzcAxisLock_TouchAction) {
+ TestCrossApzcAxisLock();
+}
+
+TEST_F(APZScrollHandoffTesterMock, WheelHandoffAfterDirectionReversal) {
+ // Explicitly set the wheel transaction timeout pref because the test relies
+ // on its value.
+ SCOPED_GFX_PREF_INT("mousewheel.transaction.timeout", 1500);
+
+ // Set up a basic scroll handoff layer tree.
+ CreateScrollHandoffLayerTree1();
+
+ rootApzc = ApzcOf(layers[0]);
+ RefPtr<TestAsyncPanZoomController> childApzc = ApzcOf(layers[1]);
+ FrameMetrics& rootMetrics = rootApzc->GetFrameMetrics();
+ FrameMetrics& childMetrics = childApzc->GetFrameMetrics();
+ CSSRect childScrollRange = childMetrics.CalculateScrollRange();
+
+ EXPECT_EQ(0, rootMetrics.GetVisualScrollOffset().y);
+ EXPECT_EQ(0, childMetrics.GetVisualScrollOffset().y);
+
+ ScreenIntPoint cursorLocation(10, 60); // positioned to hit the subframe
+ ScreenPoint upwardDelta(0, -10);
+ ScreenPoint downwardDelta(0, 10);
+
+ // First wheel upwards. This will have no effect because we're already
+ // scrolled to the top.
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
+ Wheel(manager, cursorLocation, upwardDelta, mcc->Time());
+ EXPECT_EQ(0, rootMetrics.GetVisualScrollOffset().y);
+ EXPECT_EQ(0, childMetrics.GetVisualScrollOffset().y);
+
+ // Now wheel downwards 6 times. This should scroll the child, and get it
+ // to the bottom of its 50px scroll range.
+ for (size_t i = 0; i < 6; ++i) {
+ mcc->AdvanceByMillis(100);
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
+ Wheel(manager, cursorLocation, downwardDelta, mcc->Time());
+ }
+ EXPECT_EQ(0, rootMetrics.GetVisualScrollOffset().y);
+ EXPECT_EQ(childScrollRange.YMost(), childMetrics.GetVisualScrollOffset().y);
+
+ // Wheel downwards an additional 16 times, with 100ms increments.
+ // This should be enough to overcome the 1500ms wheel transaction timeout
+ // and start scrolling the root.
+ for (size_t i = 0; i < 16; ++i) {
+ mcc->AdvanceByMillis(100);
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
+ Wheel(manager, cursorLocation, downwardDelta, mcc->Time());
+ }
+ EXPECT_EQ(childScrollRange.YMost(), childMetrics.GetVisualScrollOffset().y);
+ EXPECT_GT(rootMetrics.GetVisualScrollOffset().y, 0);
+}
+
+TEST_F(APZScrollHandoffTesterMock, WheelHandoffNonscrollable) {
+ // Set up a basic scroll layer tree.
+ CreateScrollHandoffLayerTree5();
+
+ RefPtr<TestAsyncPanZoomController> childApzc = ApzcOf(layers[1]);
+ FrameMetrics& childMetrics = childApzc->GetFrameMetrics();
+
+ EXPECT_EQ(0, childMetrics.GetVisualScrollOffset().y);
+
+ ScreenPoint downwardDelta(0, 10);
+ // Positioned to hit the nonscrollable parent frame
+ ScreenIntPoint nonscrollableLocation(40, 10);
+ // Positioned to hit the scrollable subframe
+ ScreenIntPoint scrollableLocation(40, 60);
+
+ // Start the wheel transaction on a nonscrollable parent frame.
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ Wheel(manager, nonscrollableLocation, downwardDelta, mcc->Time());
+ EXPECT_EQ(0, childMetrics.GetVisualScrollOffset().y);
+
+ // Mouse moves to a scrollable subframe. This should end the transaction.
+ mcc->AdvanceByMillis(100);
+ MouseInput mouseInput(MouseInput::MOUSE_MOVE,
+ MouseInput::ButtonType::PRIMARY_BUTTON, 0, 0,
+ scrollableLocation, mcc->Time(), 0);
+ WidgetMouseEvent mouseEvent = mouseInput.ToWidgetEvent(nullptr);
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
+ ((APZInputBridge*)manager.get())->ReceiveInputEvent(mouseEvent);
+
+ // Wheel downward should scroll the subframe.
+ mcc->AdvanceByMillis(100);
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
+ Wheel(manager, scrollableLocation, downwardDelta, mcc->Time());
+ EXPECT_GT(childMetrics.GetVisualScrollOffset().y, 0);
+}
+
+TEST_F(APZScrollHandoffTesterMock, ChildCloseToEndOfScrollRange) {
+ SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
+
+ CreateScrollHandoffLayerTree1();
+
+ RefPtr<TestAsyncPanZoomController> childApzc = ApzcOf(layers[1]);
+
+ FrameMetrics& rootMetrics = rootApzc->GetFrameMetrics();
+ FrameMetrics& childMetrics = childApzc->GetFrameMetrics();
+
+ // Zoom the page in by 3x. This needs to be reflected in the zoom level
+ // and composition bounds of both APZCs.
+ rootMetrics.SetZoom(CSSToParentLayerScale(3.0));
+ rootMetrics.SetCompositionBounds(ParentLayerRect(0, 0, 300, 300));
+ childMetrics.SetZoom(CSSToParentLayerScale(3.0));
+ childMetrics.SetCompositionBounds(ParentLayerRect(0, 150, 300, 150));
+
+ // Scroll the child APZC very close to the end of the scroll range.
+ // The scroll offset is chosen such that in CSS pixels it has 0.01 pixels
+ // room to scroll (less than COORDINATE_EPSILON = 0.02), but in ParentLayer
+ // pixels it has 0.03 pixels room (greater than COORDINATE_EPSILON).
+ childMetrics.SetVisualScrollOffset(CSSPoint(0, 49.99));
+
+ EXPECT_FALSE(childApzc->IsOverscrolled());
+
+ CSSPoint childBefore = childApzc->GetFrameMetrics().GetVisualScrollOffset();
+ CSSPoint parentBefore = rootApzc->GetFrameMetrics().GetVisualScrollOffset();
+
+ // Synthesize a pan gesture that tries to scroll the child further down.
+ PanGesture(PanGestureInput::PANGESTURE_START, childApzc,
+ ScreenIntPoint(10, 20), ScreenPoint(0, 40), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ childApzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ PanGesture(PanGestureInput::PANGESTURE_END, childApzc, ScreenIntPoint(10, 21),
+ ScreenPoint(0, 0), mcc->Time());
+
+ CSSPoint childAfter = childApzc->GetFrameMetrics().GetVisualScrollOffset();
+ CSSPoint parentAfter = rootApzc->GetFrameMetrics().GetVisualScrollOffset();
+
+ bool childScrolled = (childBefore != childAfter);
+ bool parentScrolled = (parentBefore != parentAfter);
+
+ // Check that either the child or the parent scrolled.
+ // (With the current implementation of comparing quantities to
+ // COORDINATE_EPSILON in CSS units, it will be the parent, but the important
+ // thing is that at least one of the child or parent scroll, i.e. we're not
+ // stuck in a situation where no scroll offset is changing).
+ EXPECT_TRUE(childScrolled || parentScrolled);
+}
diff --git a/gfx/layers/apz/test/gtest/TestSnapping.cpp b/gfx/layers/apz/test/gtest/TestSnapping.cpp
new file mode 100644
index 0000000000..13bb1e5591
--- /dev/null
+++ b/gfx/layers/apz/test/gtest/TestSnapping.cpp
@@ -0,0 +1,305 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "APZCTreeManagerTester.h"
+#include "APZTestCommon.h"
+
+#include "InputUtils.h"
+#include "mozilla/StaticPrefs_layout.h"
+#include "mozilla/StaticPrefs_mousewheel.h"
+
+class APZCSnappingTesterMock : public APZCTreeManagerTester {
+ public:
+ APZCSnappingTesterMock() { CreateMockHitTester(); }
+};
+
+TEST_F(APZCSnappingTesterMock, Bug1265510) {
+ const char* treeShape = "x(x)";
+ LayerIntRegion layerVisibleRegion[] = {LayerIntRect(0, 0, 100, 100),
+ LayerIntRect(0, 100, 100, 100)};
+ CreateScrollData(treeShape, layerVisibleRegion);
+ SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID,
+ CSSRect(0, 0, 100, 200));
+ SetScrollableFrameMetrics(layers[1], ScrollableLayerGuid::START_SCROLL_ID + 1,
+ CSSRect(0, 0, 100, 200));
+ SetScrollHandoff(layers[1], root);
+
+ ScrollSnapInfo snap;
+ snap.mScrollSnapStrictnessY = StyleScrollSnapStrictness::Mandatory;
+ snap.mSnapportSize = CSSSize::ToAppUnits(
+ layerVisibleRegion[0].GetBounds().Size() * LayerToCSSScale(1.0));
+
+ snap.mSnapTargets.AppendElement(ScrollSnapInfo::SnapTarget(
+ Nothing(), Some(0 * AppUnitsPerCSSPixel()),
+ CSSRect::ToAppUnits(CSSRect(0, 0, 10, 10)), StyleScrollSnapStop::Normal,
+ ScrollSnapTargetId{1}));
+ snap.mSnapTargets.AppendElement(ScrollSnapInfo::SnapTarget(
+ Nothing(), Some(100 * AppUnitsPerCSSPixel()),
+ CSSRect::ToAppUnits(CSSRect(0, 100, 10, 10)), StyleScrollSnapStop::Normal,
+ ScrollSnapTargetId{2}));
+
+ ModifyFrameMetrics(root, [&](ScrollMetadata& aSm, FrameMetrics&) {
+ aSm.SetSnapInfo(ScrollSnapInfo(snap));
+ });
+
+ UniquePtr<ScopedLayerTreeRegistration> registration =
+ MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc);
+ UpdateHitTestingTree();
+
+ TestAsyncPanZoomController* outer = ApzcOf(layers[0]);
+ TestAsyncPanZoomController* inner = ApzcOf(layers[1]);
+
+ // Position the mouse near the bottom of the outer frame and scroll by 60px.
+ // (6 lines of 10px each). APZC will actually scroll to y=100 because of the
+ // mandatory snap coordinate there.
+ TimeStamp now = mcc->Time();
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ SmoothWheel(manager, ScreenIntPoint(50, 80), ScreenPoint(0, 6), now);
+ // Advance in 5ms increments until we've scrolled by 70px. At this point, the
+ // closest snap point is y=100, and the inner frame should be under the mouse
+ // cursor.
+ while (outer
+ ->GetCurrentAsyncScrollOffset(
+ AsyncPanZoomController::AsyncTransformConsumer::eForHitTesting)
+ .y < 70) {
+ mcc->AdvanceByMillis(5);
+ outer->AdvanceAnimations(mcc->GetSampleTime());
+ }
+ // Now do another wheel in a new transaction. This should start scrolling the
+ // inner frame; we verify that it does by checking the inner scroll position.
+ TimeStamp newTransactionTime =
+ now + TimeDuration::FromMilliseconds(
+ StaticPrefs::mousewheel_transaction_timeout() + 100);
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
+ SmoothWheel(manager, ScreenIntPoint(50, 80), ScreenPoint(0, 6),
+ newTransactionTime);
+ inner->AdvanceAnimationsUntilEnd();
+ EXPECT_LT(
+ 0.0f,
+ inner
+ ->GetCurrentAsyncScrollOffset(
+ AsyncPanZoomController::AsyncTransformConsumer::eForHitTesting)
+ .y);
+
+ // However, the outer frame should also continue to the snap point, otherwise
+ // it is demonstrating incorrect behaviour by violating the mandatory
+ // snapping.
+ outer->AdvanceAnimationsUntilEnd();
+ EXPECT_EQ(
+ 100.0f,
+ outer
+ ->GetCurrentAsyncScrollOffset(
+ AsyncPanZoomController::AsyncTransformConsumer::eForHitTesting)
+ .y);
+}
+
+TEST_F(APZCSnappingTesterMock, Snap_After_Pinch) {
+ const char* treeShape = "x";
+ LayerIntRegion layerVisibleRegion[] = {
+ LayerIntRect(0, 0, 100, 100),
+ };
+ CreateScrollData(treeShape, layerVisibleRegion);
+ SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID,
+ CSSRect(0, 0, 100, 200));
+
+ // Set up some basic scroll snapping
+ ScrollSnapInfo snap;
+ snap.mScrollSnapStrictnessY = StyleScrollSnapStrictness::Mandatory;
+ snap.mSnapportSize = CSSSize::ToAppUnits(
+ layerVisibleRegion[0].GetBounds().Size() * LayerToCSSScale(1.0));
+
+ snap.mSnapTargets.AppendElement(ScrollSnapInfo::SnapTarget(
+ Nothing(), Some(0 * AppUnitsPerCSSPixel()),
+ CSSRect::ToAppUnits(CSSRect(0, 0, 10, 10)), StyleScrollSnapStop::Normal,
+ ScrollSnapTargetId{1}));
+ snap.mSnapTargets.AppendElement(ScrollSnapInfo::SnapTarget(
+ Nothing(), Some(100 * AppUnitsPerCSSPixel()),
+ CSSRect::ToAppUnits(CSSRect(0, 100, 10, 10)), StyleScrollSnapStop::Normal,
+ ScrollSnapTargetId{2}));
+
+ // Save the scroll snap info on the root APZC.
+ // Also mark the root APZC as "root content", since APZC only allows
+ // zooming on the root content APZC.
+ ModifyFrameMetrics(root, [&](ScrollMetadata& aSm, FrameMetrics& aMetrics) {
+ aSm.SetSnapInfo(ScrollSnapInfo(snap));
+ aMetrics.SetIsRootContent(true);
+ });
+
+ UniquePtr<ScopedLayerTreeRegistration> registration =
+ MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc);
+ UpdateHitTestingTree();
+
+ RefPtr<TestAsyncPanZoomController> apzc = ApzcOf(root);
+
+ // Allow zooming
+ apzc->UpdateZoomConstraints(ZoomConstraints(
+ true, true, CSSToParentLayerScale(0.25f), CSSToParentLayerScale(4.0f)));
+
+ PinchWithPinchInput(apzc, ScreenIntPoint(50, 50), ScreenIntPoint(50, 50),
+ 1.2f);
+
+ apzc->AssertStateIsSmoothMsdScroll();
+}
+
+// Currently fails on Android because on the platform we have a different
+// VelocityTracker.
+#ifndef MOZ_WIDGET_ANDROID
+TEST_F(APZCSnappingTesterMock, SnapOnPanEndWithZeroVelocity) {
+ // Use pref values for desktop everywhere.
+ SCOPED_GFX_PREF_FLOAT("apz.fling_friction", 0.002);
+ SCOPED_GFX_PREF_FLOAT("apz.fling_stopped_threshold", 0.01);
+ SCOPED_GFX_PREF_FLOAT("apz.fling_curve_function_x1", 0.0);
+ SCOPED_GFX_PREF_FLOAT("apz.fling_curve_function_x2", 1.0);
+ SCOPED_GFX_PREF_FLOAT("apz.fling_curve_function_y1", 0.0);
+ SCOPED_GFX_PREF_FLOAT("apz.fling_curve_function_y2", 1.0);
+ SCOPED_GFX_PREF_INT("apz.velocity_relevance_time_ms", 100);
+
+ const char* treeShape = "x";
+ LayerIntRegion layerVisibleRegion[] = {
+ LayerIntRect(0, 0, 100, 100),
+ };
+ CreateScrollData(treeShape, layerVisibleRegion);
+ SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID,
+ CSSRect(0, 0, 100, 400));
+
+ // Set up two snap points, 30 and 100.
+ ScrollSnapInfo snap;
+ snap.mScrollSnapStrictnessY = StyleScrollSnapStrictness::Mandatory;
+ snap.mSnapportSize = CSSSize::ToAppUnits(
+ layerVisibleRegion[0].GetBounds().Size() * LayerToCSSScale(1.0));
+ snap.mSnapTargets.AppendElement(ScrollSnapInfo::SnapTarget(
+ Nothing(), Some(30 * AppUnitsPerCSSPixel()),
+ CSSRect::ToAppUnits(CSSRect(0, 30, 10, 10)), StyleScrollSnapStop::Normal,
+ ScrollSnapTargetId{1}));
+ snap.mSnapTargets.AppendElement(ScrollSnapInfo::SnapTarget(
+ Nothing(), Some(100 * AppUnitsPerCSSPixel()),
+ CSSRect::ToAppUnits(CSSRect(0, 100, 10, 10)), StyleScrollSnapStop::Normal,
+ ScrollSnapTargetId{2}));
+
+ // Save the scroll snap info on the root APZC.
+ ModifyFrameMetrics(root, [&](ScrollMetadata& aSm, FrameMetrics& aMetrics) {
+ aSm.SetSnapInfo(ScrollSnapInfo(snap));
+ });
+
+ UniquePtr<ScopedLayerTreeRegistration> registration =
+ MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc);
+ UpdateHitTestingTree();
+
+ RefPtr<TestAsyncPanZoomController> apzc = ApzcOf(root);
+
+ // Send a series of pan gestures to scroll to position at 50.
+ const ScreenIntPoint position = ScreenIntPoint(50, 30);
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_START, manager, position,
+ ScreenPoint(0, 10), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_PAN, manager, position,
+ ScreenPoint(0, 40), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ // Make sure the velocity just before sending a pan-end is zero.
+ EXPECT_EQ(apzc->GetVelocityVector().y, 0);
+
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_END, manager, position,
+ ScreenPoint(0, 0), mcc->Time());
+
+ // Now a smooth animation has been triggered for snapping to 30.
+ apzc->AssertStateIsSmoothMsdScroll();
+
+ apzc->AdvanceAnimationsUntilEnd();
+ // The snapped position should be 30 rather than 100 because it's the nearest
+ // snap point.
+ EXPECT_EQ(
+ apzc->GetCurrentAsyncScrollOffset(AsyncPanZoomController::eForHitTesting)
+ .y,
+ 30);
+}
+
+// Smililar to above SnapOnPanEndWithZeroVelocity but with positive velocity so
+// that the snap position would be the one in the scrolling direction.
+TEST_F(APZCSnappingTesterMock, SnapOnPanEndWithPositiveVelocity) {
+ // Use pref values for desktop everywhere.
+ SCOPED_GFX_PREF_FLOAT("apz.fling_friction", 0.002);
+ SCOPED_GFX_PREF_FLOAT("apz.fling_stopped_threshold", 0.01);
+ SCOPED_GFX_PREF_FLOAT("apz.fling_curve_function_x1", 0.0);
+ SCOPED_GFX_PREF_FLOAT("apz.fling_curve_function_x2", 1.0);
+ SCOPED_GFX_PREF_FLOAT("apz.fling_curve_function_y1", 0.0);
+ SCOPED_GFX_PREF_FLOAT("apz.fling_curve_function_y2", 1.0);
+ SCOPED_GFX_PREF_INT("apz.velocity_relevance_time_ms", 100);
+
+ const char* treeShape = "x";
+ LayerIntRegion layerVisibleRegion[] = {
+ LayerIntRect(0, 0, 100, 100),
+ };
+ CreateScrollData(treeShape, layerVisibleRegion);
+ SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID,
+ CSSRect(0, 0, 100, 400));
+
+ // Set up two snap points, 30 and 100.
+ ScrollSnapInfo snap;
+ snap.mScrollSnapStrictnessY = StyleScrollSnapStrictness::Mandatory;
+ snap.mSnapportSize = CSSSize::ToAppUnits(
+ layerVisibleRegion[0].GetBounds().Size() * LayerToCSSScale(1.0));
+ snap.mSnapTargets.AppendElement(ScrollSnapInfo::SnapTarget(
+ Nothing(), Some(30 * AppUnitsPerCSSPixel()),
+ CSSRect::ToAppUnits(CSSRect(0, 30, 10, 10)), StyleScrollSnapStop::Normal,
+ ScrollSnapTargetId{1}));
+ snap.mSnapTargets.AppendElement(ScrollSnapInfo::SnapTarget(
+ Nothing(), Some(100 * AppUnitsPerCSSPixel()),
+ CSSRect::ToAppUnits(CSSRect(0, 100, 10, 10)), StyleScrollSnapStop::Normal,
+ ScrollSnapTargetId{2}));
+
+ // Save the scroll snap info on the root APZC.
+ ModifyFrameMetrics(root, [&](ScrollMetadata& aSm, FrameMetrics& aMetrics) {
+ aSm.SetSnapInfo(ScrollSnapInfo(snap));
+ });
+
+ UniquePtr<ScopedLayerTreeRegistration> registration =
+ MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc);
+ UpdateHitTestingTree();
+
+ RefPtr<TestAsyncPanZoomController> apzc = ApzcOf(root);
+
+ // Send a series of pan gestures that a pan-end event happens at 65
+ const ScreenIntPoint position = ScreenIntPoint(50, 30);
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_START, manager, position,
+ ScreenPoint(0, 10), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_PAN, manager, position,
+ ScreenPoint(0, 35), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_PAN, manager, position,
+ ScreenPoint(0, 20), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ // There should be positive velocity in this case.
+ EXPECT_GT(apzc->GetVelocityVector().y, 0);
+
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_END, manager, position,
+ ScreenPoint(0, 0), mcc->Time());
+ mcc->AdvanceByMillis(5);
+
+ // A smooth animation has been triggered by the pan-end event above.
+ apzc->AssertStateIsSmoothMsdScroll();
+
+ apzc->AdvanceAnimationsUntilEnd();
+ EXPECT_EQ(
+ apzc->GetCurrentAsyncScrollOffset(AsyncPanZoomController::eForHitTesting)
+ .y,
+ 100);
+}
+#endif
diff --git a/gfx/layers/apz/test/gtest/TestSnappingOnMomentum.cpp b/gfx/layers/apz/test/gtest/TestSnappingOnMomentum.cpp
new file mode 100644
index 0000000000..02b1d6798c
--- /dev/null
+++ b/gfx/layers/apz/test/gtest/TestSnappingOnMomentum.cpp
@@ -0,0 +1,104 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "APZCTreeManagerTester.h"
+#include "APZTestCommon.h"
+
+#include "InputUtils.h"
+#include "mozilla/StaticPrefs_layout.h"
+
+class APZCSnappingOnMomentumTesterMock : public APZCTreeManagerTester {
+ public:
+ APZCSnappingOnMomentumTesterMock() { CreateMockHitTester(); }
+};
+
+TEST_F(APZCSnappingOnMomentumTesterMock, Snap_On_Momentum) {
+ const char* treeShape = "x";
+ LayerIntRegion layerVisibleRegion[] = {
+ LayerIntRect(0, 0, 100, 100),
+ };
+ CreateScrollData(treeShape, layerVisibleRegion);
+ SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID,
+ CSSRect(0, 0, 100, 500));
+
+ // Set up some basic scroll snapping
+ ScrollSnapInfo snap;
+ snap.mScrollSnapStrictnessY = StyleScrollSnapStrictness::Mandatory;
+ snap.mSnapportSize = CSSSize::ToAppUnits(
+ layerVisibleRegion[0].GetBounds().Size() * LayerToCSSScale(1.0));
+ snap.mSnapTargets.AppendElement(ScrollSnapInfo::SnapTarget(
+ Nothing(), Some(0 * AppUnitsPerCSSPixel()),
+ CSSRect::ToAppUnits(CSSRect(0, 0, 10, 10)), StyleScrollSnapStop::Normal,
+ ScrollSnapTargetId{1}));
+ snap.mSnapTargets.AppendElement(ScrollSnapInfo::SnapTarget(
+ Nothing(), Some(100 * AppUnitsPerCSSPixel()),
+ CSSRect::ToAppUnits(CSSRect(0, 100, 10, 10)), StyleScrollSnapStop::Normal,
+ ScrollSnapTargetId{2}));
+
+ ModifyFrameMetrics(root, [&](ScrollMetadata& aSm, FrameMetrics&) {
+ aSm.SetSnapInfo(ScrollSnapInfo(snap));
+ });
+
+ UniquePtr<ScopedLayerTreeRegistration> registration =
+ MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc);
+ UpdateHitTestingTree();
+
+ RefPtr<TestAsyncPanZoomController> apzc = ApzcOf(root);
+
+ TimeStamp now = mcc->Time();
+
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_START, manager, ScreenIntPoint(50, 80),
+ ScreenPoint(0, 2), now);
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_PAN, manager, ScreenIntPoint(50, 80),
+ ScreenPoint(0, 25), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_PAN, manager, ScreenIntPoint(50, 80),
+ ScreenPoint(0, 25), mcc->Time());
+
+ // The velocity should be positive when panning with positive displacement.
+ EXPECT_GT(apzc->GetVelocityVector().y, 3.0);
+
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_END, manager, ScreenIntPoint(50, 80),
+ ScreenPoint(0, 0), mcc->Time());
+
+ // After lifting the fingers, the velocity should be zero and a smooth
+ // animation should have been triggered for scroll snap.
+ EXPECT_EQ(apzc->GetVelocityVector().y, 0);
+ apzc->AssertStateIsSmoothMsdScroll();
+
+ mcc->AdvanceByMillis(5);
+
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMSTART, manager,
+ ScreenIntPoint(50, 80), ScreenPoint(0, 200), mcc->Time());
+ mcc->AdvanceByMillis(10);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, manager,
+ ScreenIntPoint(50, 80), ScreenPoint(0, 50), mcc->Time());
+ mcc->AdvanceByMillis(10);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMEND, manager,
+ ScreenIntPoint(50, 80), ScreenPoint(0, 0), mcc->Time());
+
+ apzc->AdvanceAnimationsUntilEnd();
+ EXPECT_EQ(
+ 100.0f,
+ apzc->GetCurrentAsyncScrollOffset(
+ AsyncPanZoomController::AsyncTransformConsumer::eForHitTesting)
+ .y);
+}
diff --git a/gfx/layers/apz/test/gtest/TestTransformNotifications.cpp b/gfx/layers/apz/test/gtest/TestTransformNotifications.cpp
new file mode 100644
index 0000000000..53b7aa297f
--- /dev/null
+++ b/gfx/layers/apz/test/gtest/TestTransformNotifications.cpp
@@ -0,0 +1,567 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "APZCBasicTester.h"
+#include "APZCTreeManagerTester.h"
+#include "APZTestCommon.h"
+#include "mozilla/layers/WebRenderScrollDataWrapper.h"
+#include "apz/util/APZEventState.h"
+
+#include "InputUtils.h"
+
+class APZCTransformNotificationTester : public APZCTreeManagerTester {
+ public:
+ explicit APZCTransformNotificationTester() { CreateMockHitTester(); }
+
+ UniquePtr<ScopedLayerTreeRegistration> mRegistration;
+
+ RefPtr<TestAsyncPanZoomController> mRootApzc;
+
+ void SetupBasicTest() {
+ const char* treeShape = "x";
+ LayerIntRegion layerVisibleRegion[] = {
+ LayerIntRect(0, 0, 100, 100),
+ };
+ CreateScrollData(treeShape, layerVisibleRegion);
+ SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID,
+ CSSRect(0, 0, 500, 500));
+
+ mRegistration = MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc);
+
+ UpdateHitTestingTree();
+
+ mRootApzc = ApzcOf(root);
+ }
+
+ void SetupNonScrollableTest() {
+ const char* treeShape = "x";
+ LayerIntRegion layerVisibleRegion[] = {
+ LayerIntRect(0, 0, 100, 100),
+ };
+ CreateScrollData(treeShape, layerVisibleRegion);
+ SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID,
+ CSSRect(0, 0, 100, 100));
+
+ mRegistration = MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc);
+
+ UpdateHitTestingTree();
+
+ mRootApzc = ApzcOf(root);
+
+ mRootApzc->GetFrameMetrics().SetIsRootContent(true);
+ }
+};
+
+TEST_F(APZCTransformNotificationTester, PanningTransformNotifications) {
+ SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
+
+ SetupBasicTest();
+
+ // Scroll down by 25 px. Ensure we only get one set of
+ // state change notifications.
+ //
+ // Then, scroll back up by 20px, this time flinging after.
+ // The fling should cover the remaining 5 px of room to scroll, then
+ // go into overscroll, and finally snap-back to recover from overscroll.
+ // Again, ensure we only get one set of state change notifications for
+ // this entire procedure.
+
+ MockFunction<void(std::string checkPointName)> check;
+ {
+ InSequence s;
+ EXPECT_CALL(check, Call("Simple pan"));
+ EXPECT_CALL(
+ *mcc, NotifyAPZStateChange(
+ _, GeckoContentController::APZStateChange::eStartTouch, _, _))
+ .Times(1);
+ EXPECT_CALL(
+ *mcc,
+ NotifyAPZStateChange(
+ _, GeckoContentController::APZStateChange::eTransformBegin, _, _))
+ .Times(1);
+ EXPECT_CALL(
+ *mcc,
+ NotifyAPZStateChange(
+ _, GeckoContentController::APZStateChange::eStartPanning, _, _))
+ .Times(1);
+ EXPECT_CALL(*mcc,
+ NotifyAPZStateChange(
+ _, GeckoContentController::APZStateChange::eEndTouch, _, _))
+ .Times(1);
+ EXPECT_CALL(
+ *mcc,
+ NotifyAPZStateChange(
+ _, GeckoContentController::APZStateChange::eTransformEnd, _, _))
+ .Times(1);
+ EXPECT_CALL(check, Call("Complex pan"));
+ EXPECT_CALL(
+ *mcc, NotifyAPZStateChange(
+ _, GeckoContentController::APZStateChange::eStartTouch, _, _))
+ .Times(1);
+ EXPECT_CALL(
+ *mcc,
+ NotifyAPZStateChange(
+ _, GeckoContentController::APZStateChange::eTransformBegin, _, _))
+ .Times(1);
+ EXPECT_CALL(
+ *mcc,
+ NotifyAPZStateChange(
+ _, GeckoContentController::APZStateChange::eStartPanning, _, _))
+ .Times(1);
+ EXPECT_CALL(*mcc,
+ NotifyAPZStateChange(
+ _, GeckoContentController::APZStateChange::eEndTouch, _, _))
+ .Times(1);
+ EXPECT_CALL(
+ *mcc,
+ NotifyAPZStateChange(
+ _, GeckoContentController::APZStateChange::eTransformEnd, _, _))
+ .Times(1);
+ EXPECT_CALL(check, Call("Done"));
+ }
+
+ check.Call("Simple pan");
+ Pan(mRootApzc, 50, 25, PanOptions::NoFling);
+ check.Call("Complex pan");
+ Pan(mRootApzc, 25, 45);
+ mRootApzc->AdvanceAnimationsUntilEnd();
+ check.Call("Done");
+}
+
+TEST_F(APZCTransformNotificationTester, PanWithMomentumTransformNotifications) {
+ SetupBasicTest();
+
+ MockFunction<void(std::string checkPointName)> check;
+ {
+ InSequence s;
+ EXPECT_CALL(check, Call("Pan Start"));
+ EXPECT_CALL(
+ *mcc,
+ NotifyAPZStateChange(
+ _, GeckoContentController::APZStateChange::eTransformBegin, _, _))
+ .Times(1);
+
+ EXPECT_CALL(check, Call("Panning"));
+ EXPECT_CALL(check, Call("Pan End"));
+ EXPECT_CALL(check, Call("Momentum Start"));
+
+ EXPECT_CALL(check, Call("Momentum Pan"));
+ EXPECT_CALL(check, Call("Momentum End"));
+ // The TransformEnd should only be sent after the momentum pan.
+ EXPECT_CALL(
+ *mcc,
+ NotifyAPZStateChange(
+ _, GeckoContentController::APZStateChange::eTransformEnd, _, _))
+ .Times(1);
+
+ EXPECT_CALL(check, Call("Done"));
+ }
+
+ check.Call("Pan Start");
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_START, manager, ScreenIntPoint(50, 50),
+ ScreenIntPoint(1, 2), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ mRootApzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ check.Call("Panning");
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_PAN, mRootApzc, ScreenIntPoint(50, 50),
+ ScreenPoint(15, 30), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ mRootApzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ check.Call("Pan End");
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_END, manager, ScreenIntPoint(50, 50),
+ ScreenPoint(0, 0), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ mRootApzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ check.Call("Momentum Start");
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMSTART, manager,
+ ScreenIntPoint(50, 50), ScreenPoint(30, 90), mcc->Time());
+ mcc->AdvanceByMillis(10);
+ mRootApzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ check.Call("Momentum Pan");
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, manager,
+ ScreenIntPoint(50, 50), ScreenPoint(10, 30), mcc->Time());
+ mcc->AdvanceByMillis(10);
+ mRootApzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ check.Call("Momentum End");
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMEND, manager,
+ ScreenIntPoint(50, 50), ScreenPoint(0, 0), mcc->Time());
+ mcc->AdvanceByMillis(10);
+ mRootApzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ check.Call("Done");
+}
+
+TEST_F(APZCTransformNotificationTester,
+ PanWithoutMomentumTransformNotifications) {
+ // Ensure that the TransformEnd delay is 100ms.
+ SCOPED_GFX_PREF_INT("apz.scrollend-event.content.delay_ms", 100);
+
+ SetupBasicTest();
+
+ MockFunction<void(std::string checkPointName)> check;
+ {
+ InSequence s;
+ EXPECT_CALL(check, Call("Pan Start"));
+ EXPECT_CALL(
+ *mcc,
+ NotifyAPZStateChange(
+ _, GeckoContentController::APZStateChange::eTransformBegin, _, _))
+ .Times(1);
+
+ EXPECT_CALL(check, Call("Panning"));
+ EXPECT_CALL(check, Call("Pan End"));
+ EXPECT_CALL(check, Call("TransformEnd delay"));
+ // The TransformEnd should only be sent after the pan gesture and 100ms
+ // timer fire.
+ EXPECT_CALL(
+ *mcc,
+ NotifyAPZStateChange(
+ _, GeckoContentController::APZStateChange::eTransformEnd, _, _))
+ .Times(1);
+
+ EXPECT_CALL(check, Call("Done"));
+ }
+
+ check.Call("Pan Start");
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_START, manager, ScreenIntPoint(50, 50),
+ ScreenIntPoint(1, 2), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ mRootApzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ check.Call("Panning");
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_PAN, mRootApzc, ScreenIntPoint(50, 50),
+ ScreenPoint(15, 30), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ mRootApzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ check.Call("Pan End");
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_END, manager, ScreenIntPoint(50, 50),
+ ScreenPoint(0, 0), mcc->Time());
+ mcc->AdvanceByMillis(55);
+ mRootApzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ check.Call("TransformEnd delay");
+ mcc->AdvanceByMillis(55);
+ mRootApzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ check.Call("Done");
+}
+
+TEST_F(APZCTransformNotificationTester,
+ PanFollowedByNewPanTransformNotifications) {
+ // Ensure that the TransformEnd delay is 100ms.
+ SCOPED_GFX_PREF_INT("apz.scrollend-event.content.delay_ms", 100);
+
+ SetupBasicTest();
+
+ MockFunction<void(std::string checkPointName)> check;
+ {
+ InSequence s;
+ EXPECT_CALL(check, Call("Pan Start"));
+ EXPECT_CALL(
+ *mcc,
+ NotifyAPZStateChange(
+ _, GeckoContentController::APZStateChange::eTransformBegin, _, _))
+ .Times(1);
+
+ EXPECT_CALL(check, Call("Panning"));
+ EXPECT_CALL(check, Call("Pan End"));
+ // The TransformEnd delay should be cut short and delivered before the
+ // new pan gesture begins.
+ EXPECT_CALL(check, Call("New Pan Start"));
+ EXPECT_CALL(
+ *mcc,
+ NotifyAPZStateChange(
+ _, GeckoContentController::APZStateChange::eTransformEnd, _, _))
+ .Times(1);
+ EXPECT_CALL(
+ *mcc,
+ NotifyAPZStateChange(
+ _, GeckoContentController::APZStateChange::eTransformBegin, _, _))
+ .Times(1);
+ EXPECT_CALL(check, Call("New Pan End"));
+ EXPECT_CALL(
+ *mcc,
+ NotifyAPZStateChange(
+ _, GeckoContentController::APZStateChange::eTransformEnd, _, _))
+ .Times(1);
+
+ EXPECT_CALL(check, Call("Done"));
+ }
+
+ check.Call("Pan Start");
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_START, manager, ScreenIntPoint(50, 50),
+ ScreenIntPoint(1, 2), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ mRootApzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ check.Call("Panning");
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_PAN, mRootApzc, ScreenIntPoint(50, 50),
+ ScreenPoint(15, 30), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ mRootApzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ check.Call("Pan End");
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_END, manager, ScreenIntPoint(50, 50),
+ ScreenPoint(0, 0), mcc->Time());
+ mcc->AdvanceByMillis(55);
+ mRootApzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ check.Call("New Pan Start");
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_START, manager, ScreenIntPoint(50, 50),
+ ScreenIntPoint(1, 2), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ mRootApzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_PAN, mRootApzc, ScreenIntPoint(50, 50),
+ ScreenPoint(15, 30), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ mRootApzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ check.Call("New Pan End");
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_END, manager, ScreenIntPoint(50, 50),
+ ScreenPoint(0, 0), mcc->Time());
+ mcc->AdvanceByMillis(105);
+ mRootApzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ check.Call("Done");
+}
+
+TEST_F(APZCTransformNotificationTester,
+ PanFollowedByWheelTransformNotifications) {
+ // Ensure that the TransformEnd delay is 100ms.
+ SCOPED_GFX_PREF_INT("apz.scrollend-event.content.delay_ms", 100);
+
+ SetupBasicTest();
+
+ MockFunction<void(std::string checkPointName)> check;
+ {
+ InSequence s;
+ EXPECT_CALL(check, Call("Pan Start"));
+ EXPECT_CALL(
+ *mcc,
+ NotifyAPZStateChange(
+ _, GeckoContentController::APZStateChange::eTransformBegin, _, _))
+ .Times(1);
+
+ EXPECT_CALL(check, Call("Panning"));
+ EXPECT_CALL(check, Call("Pan End"));
+ // The TransformEnd delay should be cut short and delivered before the
+ // new wheel event begins.
+ EXPECT_CALL(check, Call("Wheel Start"));
+ EXPECT_CALL(
+ *mcc,
+ NotifyAPZStateChange(
+ _, GeckoContentController::APZStateChange::eTransformEnd, _, _))
+ .Times(1);
+ EXPECT_CALL(
+ *mcc,
+ NotifyAPZStateChange(
+ _, GeckoContentController::APZStateChange::eTransformBegin, _, _))
+ .Times(1);
+ EXPECT_CALL(check, Call("Wheel End"));
+ EXPECT_CALL(
+ *mcc,
+ NotifyAPZStateChange(
+ _, GeckoContentController::APZStateChange::eTransformEnd, _, _))
+ .Times(1);
+ EXPECT_CALL(check, Call("Done"));
+ }
+
+ check.Call("Pan Start");
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_START, manager, ScreenIntPoint(50, 50),
+ ScreenIntPoint(1, 2), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ mRootApzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ check.Call("Panning");
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_PAN, mRootApzc, ScreenIntPoint(50, 50),
+ ScreenPoint(15, 30), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ mRootApzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ check.Call("Pan End");
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_END, manager, ScreenIntPoint(50, 50),
+ ScreenPoint(0, 0), mcc->Time());
+ mcc->AdvanceByMillis(55);
+ mRootApzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ check.Call("Wheel Start");
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ SmoothWheel(manager, ScreenIntPoint(50, 50), ScreenPoint(10, 10),
+ mcc->Time());
+ mcc->AdvanceByMillis(10);
+ mRootApzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ check.Call("Wheel End");
+
+ mRootApzc->AdvanceAnimationsUntilEnd();
+
+ check.Call("Done");
+}
+
+#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android
+TEST_F(APZCTransformNotificationTester, PanOverscrollTransformNotifications) {
+ SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
+
+ SetupBasicTest();
+
+ MockFunction<void(std::string checkPointName)> check;
+ {
+ InSequence s;
+ EXPECT_CALL(check, Call("Pan Start"));
+ EXPECT_CALL(
+ *mcc,
+ NotifyAPZStateChange(
+ _, GeckoContentController::APZStateChange::eTransformBegin, _, _))
+ .Times(1);
+
+ EXPECT_CALL(check, Call("Panning Into Overscroll"));
+ EXPECT_CALL(check, Call("Pan End"));
+ EXPECT_CALL(check, Call("Overscroll Animation End"));
+ // The TransformEnd should only be sent after the overscroll animation
+ // completes.
+ EXPECT_CALL(
+ *mcc,
+ NotifyAPZStateChange(
+ _, GeckoContentController::APZStateChange::eTransformEnd, _, _))
+ .Times(1);
+ EXPECT_CALL(check, Call("Done"));
+ }
+
+ check.Call("Pan Start");
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_START, manager, ScreenIntPoint(50, 50),
+ ScreenIntPoint(1, 2), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ mRootApzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ check.Call("Panning Into Overscroll");
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_PAN, mRootApzc, ScreenIntPoint(50, 50),
+ ScreenPoint(15, -30), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ mRootApzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ // Ensure that we have overscrolled.
+ EXPECT_TRUE(mRootApzc->IsOverscrolled());
+
+ check.Call("Pan End");
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_END, manager, ScreenIntPoint(50, 50),
+ ScreenPoint(0, 0), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ mRootApzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ // Wait for the overscroll animation to complete and the TransformEnd
+ // notification to be sent.
+ check.Call("Overscroll Animation End");
+ mcc->AdvanceByMillis(5);
+ mRootApzc->AdvanceAnimationsUntilEnd();
+ EXPECT_FALSE(mRootApzc->IsOverscrolled());
+
+ check.Call("Done");
+}
+#endif
+
+TEST_F(APZCTransformNotificationTester, ScrollableTouchStateChange) {
+ // Create a scroll frame with available space for a scroll.
+ SetupBasicTest();
+
+ MockFunction<void(std::string checkPointName)> check;
+ {
+ EXPECT_CALL(check, Call("Start"));
+ // We receive a touch-start with the flag indicating that the
+ // touch-start occurred over a scrollable element.
+ EXPECT_CALL(
+ *mcc, NotifyAPZStateChange(
+ _, GeckoContentController::APZStateChange::eStartTouch, 1, _))
+ .Times(1);
+
+ EXPECT_CALL(*mcc,
+ NotifyAPZStateChange(
+ _, GeckoContentController::APZStateChange::eEndTouch, 1, _))
+ .Times(1);
+ EXPECT_CALL(check, Call("Done"));
+ }
+
+ check.Call("Start");
+
+ // Conduct a touch down and touch up in the scrollable element,
+ // and ensure the correct state change notifications are sent.
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ TouchDown(mRootApzc, ScreenIntPoint(10, 10), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ mRootApzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ TouchUp(mRootApzc, ScreenIntPoint(10, 10), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ mRootApzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ check.Call("Done");
+}
+
+TEST_F(APZCTransformNotificationTester, NonScrollableTouchStateChange) {
+ // Create a non-scrollable frame with no space to scroll.
+ SetupNonScrollableTest();
+
+ MockFunction<void(std::string checkPointName)> check;
+ {
+ EXPECT_CALL(check, Call("Start"));
+ // We receive a touch-start with the flag indicating that the
+ // touch-start occurred over a non-scrollable element.
+ EXPECT_CALL(
+ *mcc, NotifyAPZStateChange(
+ _, GeckoContentController::APZStateChange::eStartTouch, 0, _))
+ .Times(1);
+
+ EXPECT_CALL(*mcc,
+ NotifyAPZStateChange(
+ _, GeckoContentController::APZStateChange::eEndTouch, 1, _))
+ .Times(1);
+ EXPECT_CALL(check, Call("Done"));
+ }
+
+ check.Call("Start");
+
+ // Conduct a touch down and touch up in the non-scrollable element,
+ // and ensure the correct state change notifications are sent.
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ TouchDown(mRootApzc, ScreenIntPoint(10, 10), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ mRootApzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ TouchUp(mRootApzc, ScreenIntPoint(10, 10), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ mRootApzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ check.Call("Done");
+}
diff --git a/gfx/layers/apz/test/gtest/TestTreeManager.cpp b/gfx/layers/apz/test/gtest/TestTreeManager.cpp
new file mode 100644
index 0000000000..963a400cb8
--- /dev/null
+++ b/gfx/layers/apz/test/gtest/TestTreeManager.cpp
@@ -0,0 +1,347 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "APZCTreeManagerTester.h"
+#include "APZTestCommon.h"
+#include "InputUtils.h"
+#include "Units.h"
+
+class APZCTreeManagerGenericTester : public APZCTreeManagerTester {
+ protected:
+ void CreateSimpleScrollingLayer() {
+ const char* treeShape = "x";
+ LayerIntRegion layerVisibleRegion[] = {
+ LayerIntRect(0, 0, 200, 200),
+ };
+ CreateScrollData(treeShape, layerVisibleRegion);
+ SetScrollableFrameMetrics(layers[0], ScrollableLayerGuid::START_SCROLL_ID,
+ CSSRect(0, 0, 500, 500));
+ }
+
+ void CreateSimpleMultiLayerTree() {
+ const char* treeShape = "x(xx)";
+ // LayerID 0 12
+ LayerIntRegion layerVisibleRegion[] = {
+ LayerIntRect(0, 0, 100, 100),
+ LayerIntRect(0, 0, 100, 50),
+ LayerIntRect(0, 50, 100, 50),
+ };
+ CreateScrollData(treeShape, layerVisibleRegion);
+ }
+
+ void CreatePotentiallyLeakingTree() {
+ const char* treeShape = "x(x(x(x))x(x(x)))";
+ // LayerID 0 1 2 3 4 5 6
+ CreateScrollData(treeShape);
+ SetScrollableFrameMetrics(layers[0], ScrollableLayerGuid::START_SCROLL_ID);
+ SetScrollableFrameMetrics(layers[2],
+ ScrollableLayerGuid::START_SCROLL_ID + 1);
+ SetScrollableFrameMetrics(layers[5],
+ ScrollableLayerGuid::START_SCROLL_ID + 1);
+ SetScrollableFrameMetrics(layers[3],
+ ScrollableLayerGuid::START_SCROLL_ID + 2);
+ SetScrollableFrameMetrics(layers[6],
+ ScrollableLayerGuid::START_SCROLL_ID + 3);
+ }
+
+ void CreateTwoLayerTree(int32_t aRootContentLayerIndex) {
+ const char* treeShape = "x(x)";
+ // LayerID 0 1
+ LayerIntRegion layerVisibleRegion[] = {
+ LayerIntRect(0, 0, 100, 100),
+ LayerIntRect(0, 0, 100, 100),
+ };
+ CreateScrollData(treeShape, layerVisibleRegion);
+ SetScrollableFrameMetrics(layers[0], ScrollableLayerGuid::START_SCROLL_ID);
+ SetScrollableFrameMetrics(layers[1],
+ ScrollableLayerGuid::START_SCROLL_ID + 1);
+ SetScrollHandoff(layers[1], layers[0]);
+
+ // Make layers[aRootContentLayerIndex] the root content
+ ModifyFrameMetrics(layers[aRootContentLayerIndex],
+ [](ScrollMetadata& sm, FrameMetrics& fm) {
+ fm.SetIsRootContent(true);
+ });
+ }
+};
+
+TEST_F(APZCTreeManagerGenericTester, ScrollablePaintedLayers) {
+ CreateSimpleMultiLayerTree();
+ ScopedLayerTreeRegistration registration(LayersId{0}, mcc);
+
+ // both layers have the same scrollId
+ SetScrollableFrameMetrics(layers[1], ScrollableLayerGuid::START_SCROLL_ID);
+ SetScrollableFrameMetrics(layers[2], ScrollableLayerGuid::START_SCROLL_ID);
+ UpdateHitTestingTree();
+
+ TestAsyncPanZoomController* nullAPZC = nullptr;
+ // so they should have the same APZC
+ EXPECT_FALSE(HasScrollableFrameMetrics(layers[0]));
+ EXPECT_NE(nullAPZC, ApzcOf(layers[1]));
+ EXPECT_NE(nullAPZC, ApzcOf(layers[2]));
+ EXPECT_EQ(ApzcOf(layers[1]), ApzcOf(layers[2]));
+}
+
+TEST_F(APZCTreeManagerGenericTester, Bug1068268) {
+ CreatePotentiallyLeakingTree();
+ ScopedLayerTreeRegistration registration(LayersId{0}, mcc);
+
+ UpdateHitTestingTree();
+ RefPtr<HitTestingTreeNode> root = manager->GetRootNode();
+ RefPtr<HitTestingTreeNode> node2 = root->GetFirstChild()->GetFirstChild();
+ RefPtr<HitTestingTreeNode> node5 = root->GetLastChild()->GetLastChild();
+
+ EXPECT_EQ(ApzcOf(layers[2]), node5->GetApzc());
+ EXPECT_EQ(ApzcOf(layers[2]), node2->GetApzc());
+ EXPECT_EQ(ApzcOf(layers[0]), ApzcOf(layers[2])->GetParent());
+ EXPECT_EQ(ApzcOf(layers[2]), ApzcOf(layers[5]));
+
+ EXPECT_EQ(node2->GetFirstChild(), node2->GetLastChild());
+ EXPECT_EQ(ApzcOf(layers[3]), node2->GetLastChild()->GetApzc());
+ EXPECT_EQ(node5->GetFirstChild(), node5->GetLastChild());
+ EXPECT_EQ(ApzcOf(layers[6]), node5->GetLastChild()->GetApzc());
+ EXPECT_EQ(ApzcOf(layers[2]), ApzcOf(layers[3])->GetParent());
+ EXPECT_EQ(ApzcOf(layers[5]), ApzcOf(layers[6])->GetParent());
+}
+
+class APZCTreeManagerGenericTesterMock : public APZCTreeManagerGenericTester {
+ public:
+ APZCTreeManagerGenericTesterMock() { CreateMockHitTester(); }
+};
+
+TEST_F(APZCTreeManagerGenericTesterMock, Bug1194876) {
+ // Create a layer tree with parent and child scrollable layers, with the
+ // child being the root content.
+ CreateTwoLayerTree(1);
+ ScopedLayerTreeRegistration registration(LayersId{0}, mcc);
+ UpdateHitTestingTree();
+
+ uint64_t blockId;
+ nsTArray<ScrollableLayerGuid> targets;
+
+ // First touch goes down, APZCTM will hit layers[1] because it is on top of
+ // layers[0], but we tell it the real target APZC is layers[0].
+ MultiTouchInput mti;
+ mti = CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time());
+ mti.mTouches.AppendElement(
+ SingleTouchData(0, ScreenIntPoint(25, 50), ScreenSize(0, 0), 0, 0));
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1,
+ {CompositorHitTestFlags::eVisibleToHitTest,
+ CompositorHitTestFlags::eIrregularArea});
+ blockId = manager->ReceiveInputEvent(mti).mInputBlockId;
+ manager->ContentReceivedInputBlock(blockId, false);
+ targets.AppendElement(ApzcOf(layers[0])->GetGuid());
+ manager->SetTargetAPZC(blockId, targets);
+
+ // Around here, the above touch will get processed by ApzcOf(layers[0])
+
+ // Second touch goes down (first touch remains down), APZCTM will again hit
+ // layers[1]. Again we tell it both touches landed on layers[0], but because
+ // layers[1] is the RCD layer, it will end up being the multitouch target.
+ mti.mTouches.AppendElement(
+ SingleTouchData(1, ScreenIntPoint(75, 50), ScreenSize(0, 0), 0, 0));
+ // Each touch will get hit-tested, so queue two hit-test results.
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1,
+ {CompositorHitTestFlags::eVisibleToHitTest,
+ CompositorHitTestFlags::eIrregularArea});
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1,
+ {CompositorHitTestFlags::eVisibleToHitTest,
+ CompositorHitTestFlags::eIrregularArea});
+ blockId = manager->ReceiveInputEvent(mti).mInputBlockId;
+ manager->ContentReceivedInputBlock(blockId, false);
+ targets.AppendElement(ApzcOf(layers[0])->GetGuid());
+ manager->SetTargetAPZC(blockId, targets);
+
+ // Around here, the above multi-touch will get processed by ApzcOf(layers[1]).
+ // We want to ensure that ApzcOf(layers[0]) has had its state cleared, because
+ // otherwise it will do things like dispatch spurious long-tap events.
+
+ EXPECT_CALL(*mcc, HandleTap(TapType::eLongTap, _, _, _, _)).Times(0);
+}
+
+TEST_F(APZCTreeManagerGenericTesterMock, TargetChangesMidGesture_Bug1570559) {
+ // Create a layer tree with parent and child scrollable layers, with the
+ // parent being the root content.
+ CreateTwoLayerTree(0);
+ ScopedLayerTreeRegistration registration(LayersId{0}, mcc);
+ UpdateHitTestingTree();
+
+ uint64_t blockId;
+ nsTArray<ScrollableLayerGuid> targets;
+
+ // First touch goes down. APZCTM hits the child layer because it is on top
+ // (and we confirm this target), but do not prevent-default the event, causing
+ // the child APZC's gesture detector to start a long-tap timeout task.
+ MultiTouchInput mti =
+ CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time());
+ mti.mTouches.AppendElement(
+ SingleTouchData(0, ScreenIntPoint(25, 50), ScreenSize(0, 0), 0, 0));
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1,
+ {CompositorHitTestFlags::eVisibleToHitTest,
+ CompositorHitTestFlags::eIrregularArea});
+ blockId = manager->ReceiveInputEvent(mti).mInputBlockId;
+ manager->ContentReceivedInputBlock(blockId, /* default prevented = */ false);
+ targets.AppendElement(ApzcOf(layers[1])->GetGuid());
+ manager->SetTargetAPZC(blockId, targets);
+
+ // Second touch goes down (first touch remains down). APZCTM again hits the
+ // child and we confirm this, but multi-touch events are routed to the root
+ // content APZC which is the parent. This event is prevent-defaulted, so we
+ // clear the parent's gesture state. The bug is that we fail to clear the
+ // child's gesture state.
+ mti.mTouches.AppendElement(
+ SingleTouchData(1, ScreenIntPoint(75, 50), ScreenSize(0, 0), 0, 0));
+ // Each touch will get hit-tested, so queue two hit-test results.
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1,
+ {CompositorHitTestFlags::eVisibleToHitTest,
+ CompositorHitTestFlags::eIrregularArea});
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1,
+ {CompositorHitTestFlags::eVisibleToHitTest,
+ CompositorHitTestFlags::eIrregularArea});
+ blockId = manager->ReceiveInputEvent(mti).mInputBlockId;
+ manager->ContentReceivedInputBlock(blockId, /* default prevented = */ true);
+ targets.AppendElement(ApzcOf(layers[1])->GetGuid());
+ manager->SetTargetAPZC(blockId, targets);
+
+ // If we've failed to clear the child's gesture state, then the long tap
+ // timeout task will fire in TearDown() and a long-tap will be dispatched.
+ EXPECT_CALL(*mcc, HandleTap(TapType::eLongTap, _, _, _, _)).Times(0);
+}
+
+TEST_F(APZCTreeManagerGenericTesterMock, Bug1198900) {
+ // This is just a test that cancels a wheel event to make sure it doesn't
+ // crash.
+ CreateSimpleScrollingLayer();
+ ScopedLayerTreeRegistration registration(LayersId{0}, mcc);
+ UpdateHitTestingTree();
+
+ ScreenPoint origin(100, 50);
+ ScrollWheelInput swi(mcc->Time(), 0, ScrollWheelInput::SCROLLMODE_INSTANT,
+ ScrollWheelInput::SCROLLDELTA_PIXEL, origin, 0, 10,
+ false, WheelDeltaAdjustmentStrategy::eNone);
+ uint64_t blockId;
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID,
+ {CompositorHitTestFlags::eVisibleToHitTest,
+ CompositorHitTestFlags::eIrregularArea});
+ blockId = manager->ReceiveInputEvent(swi).mInputBlockId;
+ manager->ContentReceivedInputBlock(blockId, /* preventDefault= */ true);
+}
+
+// The next two tests check that APZ clamps the scroll offset it composites even
+// if the main thread fails to do so. (The main thread will always clamp its
+// scroll offset internally, but it may not send APZ the clamped version for
+// scroll offset synchronization reasons.)
+TEST_F(APZCTreeManagerTester, Bug1551582) {
+ // The simple layer tree has a scrollable rect of 500x500 and a composition
+ // bounds of 200x200, leading to a scroll range of (0,0,300,300).
+ CreateSimpleScrollingLayer();
+ ScopedLayerTreeRegistration registration(LayersId{0}, mcc);
+ UpdateHitTestingTree();
+
+ // Simulate the main thread scrolling to the end of the scroll range.
+ ModifyFrameMetrics(root, [](ScrollMetadata& aSm, FrameMetrics& aMetrics) {
+ aMetrics.SetLayoutScrollOffset(CSSPoint(300, 300));
+ nsTArray<ScrollPositionUpdate> scrollUpdates;
+ scrollUpdates.AppendElement(ScrollPositionUpdate::NewScroll(
+ ScrollOrigin::Other, CSSPoint::ToAppUnits(CSSPoint(300, 300))));
+ aSm.SetScrollUpdates(scrollUpdates);
+ aMetrics.SetScrollGeneration(scrollUpdates.LastElement().GetGeneration());
+ });
+ UpdateHitTestingTree();
+
+ // Sanity check.
+ RefPtr<TestAsyncPanZoomController> apzc = ApzcOf(root);
+ CSSPoint compositedScrollOffset = apzc->GetCompositedScrollOffset();
+ EXPECT_EQ(CSSPoint(300, 300), compositedScrollOffset);
+
+ // Simulate the main thread shrinking the scrollable rect to 400x400 (and
+ // thereby the scroll range to (0,0,200,200) without sending a new scroll
+ // offset update for the clamped scroll position (200,200).
+ ModifyFrameMetrics(root, [](ScrollMetadata& aSm, FrameMetrics& aMetrics) {
+ aMetrics.SetScrollableRect(CSSRect(0, 0, 400, 400));
+ });
+ UpdateHitTestingTree();
+
+ // Check that APZ has clamped the scroll offset to (200,200) for us.
+ compositedScrollOffset = apzc->GetCompositedScrollOffset();
+ EXPECT_EQ(CSSPoint(200, 200), compositedScrollOffset);
+}
+TEST_F(APZCTreeManagerTester, Bug1557424) {
+ // The simple layer tree has a scrollable rect of 500x500 and a composition
+ // bounds of 200x200, leading to a scroll range of (0,0,300,300).
+ CreateSimpleScrollingLayer();
+ ScopedLayerTreeRegistration registration(LayersId{0}, mcc);
+ UpdateHitTestingTree();
+
+ // Simulate the main thread scrolling to the end of the scroll range.
+ ModifyFrameMetrics(root, [](ScrollMetadata& aSm, FrameMetrics& aMetrics) {
+ aMetrics.SetLayoutScrollOffset(CSSPoint(300, 300));
+ nsTArray<ScrollPositionUpdate> scrollUpdates;
+ scrollUpdates.AppendElement(ScrollPositionUpdate::NewScroll(
+ ScrollOrigin::Other, CSSPoint::ToAppUnits(CSSPoint(300, 300))));
+ aSm.SetScrollUpdates(scrollUpdates);
+ aMetrics.SetScrollGeneration(scrollUpdates.LastElement().GetGeneration());
+ });
+ UpdateHitTestingTree();
+
+ // Sanity check.
+ RefPtr<TestAsyncPanZoomController> apzc = ApzcOf(root);
+ CSSPoint compositedScrollOffset = apzc->GetCompositedScrollOffset();
+ EXPECT_EQ(CSSPoint(300, 300), compositedScrollOffset);
+
+ // Simulate the main thread expanding the composition bounds to 300x300 (and
+ // thereby shrinking the scroll range to (0,0,200,200) without sending a new
+ // scroll offset update for the clamped scroll position (200,200).
+ ModifyFrameMetrics(root, [](ScrollMetadata& aSm, FrameMetrics& aMetrics) {
+ aMetrics.SetCompositionBounds(ParentLayerRect(0, 0, 300, 300));
+ });
+ UpdateHitTestingTree();
+
+ // Check that APZ has clamped the scroll offset to (200,200) for us.
+ compositedScrollOffset = apzc->GetCompositedScrollOffset();
+ EXPECT_EQ(CSSPoint(200, 200), compositedScrollOffset);
+}
+
+TEST_F(APZCTreeManagerTester, Bug1805601) {
+ // The simple layer tree has a scrollable rect of 500x500 and a composition
+ // bounds of 200x200, leading to a scroll range of (0,0,300,300) at unit zoom.
+ CreateSimpleScrollingLayer();
+ ScopedLayerTreeRegistration registration(LayersId{0}, mcc);
+ UpdateHitTestingTree();
+ RefPtr<TestAsyncPanZoomController> apzc = ApzcOf(root);
+ FrameMetrics& compositorMetrics = apzc->GetFrameMetrics();
+ EXPECT_EQ(CSSRect(0, 0, 300, 300), compositorMetrics.CalculateScrollRange());
+
+ // Zoom the page in by 2x. This needs to be reflected in each of the pres
+ // shell resolution, cumulative resolution, and zoom. This makes the scroll
+ // range (0,0,400,400).
+ compositorMetrics.SetZoom(CSSToParentLayerScale(2.0));
+ EXPECT_EQ(CSSRect(0, 0, 400, 400), compositorMetrics.CalculateScrollRange());
+
+ // Scroll to an area inside the 2x scroll range but outside the original one.
+ compositorMetrics.ClampAndSetVisualScrollOffset(CSSPoint(350, 350));
+ EXPECT_EQ(CSSPoint(350, 350), compositorMetrics.GetVisualScrollOffset());
+
+ // Simulate a main-thread update where the zoom is reset to 1x but the visual
+ // scroll offset is unmodified.
+ ModifyFrameMetrics(root, [](ScrollMetadata& aSm, FrameMetrics& aMetrics) {
+ // Changes to |compositorMetrics| are not reflected in |aMetrics|, which
+ // is the "layer tree" copy, so we don't need to explicitly set the zoom to
+ // 1.0 (it still has that as the initial value), but we do need to set
+ // the visual scroll offset to the same value the APZ copy has.
+ aMetrics.SetVisualScrollOffset(CSSPoint(350, 350));
+
+ // Needed to get APZ to accept the 1.0 zoom in |aMetrics|, otherwise
+ // it will act as though its zoom is newer (e.g. an async zoom that hasn't
+ // been repainted yet) and ignore ours.
+ aSm.SetResolutionUpdated(true);
+ });
+ UpdateHitTestingTree();
+
+ // Check that APZ clamped the scroll offset.
+ EXPECT_EQ(CSSRect(0, 0, 300, 300), compositorMetrics.CalculateScrollRange());
+ EXPECT_EQ(CSSPoint(300, 300), compositorMetrics.GetVisualScrollOffset());
+}
diff --git a/gfx/layers/apz/test/gtest/TestWRScrollData.cpp b/gfx/layers/apz/test/gtest/TestWRScrollData.cpp
new file mode 100644
index 0000000000..e267e58e90
--- /dev/null
+++ b/gfx/layers/apz/test/gtest/TestWRScrollData.cpp
@@ -0,0 +1,273 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "TestWRScrollData.h"
+#include "APZTestAccess.h"
+#include "gtest/gtest.h"
+#include "FrameMetrics.h"
+#include "gfxPlatform.h"
+#include "mozilla/layers/APZUpdater.h"
+#include "mozilla/layers/LayersTypes.h"
+#include "mozilla/layers/ScrollableLayerGuid.h"
+#include "mozilla/layers/WebRenderScrollDataWrapper.h"
+#include "mozilla/UniquePtr.h"
+#include "apz/src/APZCTreeManager.h"
+
+using mozilla::layers::APZCTreeManager;
+using mozilla::layers::APZUpdater;
+using mozilla::layers::LayersId;
+using mozilla::layers::ScrollableLayerGuid;
+using mozilla::layers::ScrollMetadata;
+using mozilla::layers::TestWRScrollData;
+using mozilla::layers::WebRenderLayerScrollData;
+using mozilla::layers::WebRenderScrollDataWrapper;
+
+/* static */
+TestWRScrollData TestWRScrollData::Create(const char* aTreeShape,
+ const APZUpdater& aUpdater,
+ const LayerIntRegion* aVisibleRegions,
+ const gfx::Matrix4x4* aTransforms) {
+ // The WebRenderLayerScrollData tree needs to be created in a fairly
+ // particular way (for example, each node needs to know the number of
+ // descendants it has), so this function takes care to create the nodes
+ // in the same order as WebRenderCommandBuilder would.
+ TestWRScrollData result;
+ const size_t len = strlen(aTreeShape);
+ // "Layer index" in this function refers to the index by which a layer will
+ // be accessible via TestWRScrollData::GetLayer(), and matches the order
+ // in which the layer appears in |aTreeShape|.
+ size_t currentLayerIndex = 0;
+ struct LayerEntry {
+ size_t mLayerIndex;
+ int32_t mDescendantCount = 0;
+ };
+ // Layers we have encountered in |aTreeShape|, but have not built a
+ // WebRenderLayerScrollData for. (It can only be built after its
+ // descendants have been encountered and counted.)
+ std::stack<LayerEntry> pendingLayers;
+ std::vector<WebRenderLayerScrollData> finishedLayers;
+ // Tracks the level of nesting of '(' characters. Starts at 1 to account
+ // for the root layer.
+ size_t depth = 1;
+ // Helper function for finishing a layer once all its descendants have been
+ // encountered.
+ auto finishLayer = [&] {
+ MOZ_ASSERT(!pendingLayers.empty());
+ LayerEntry entry = pendingLayers.top();
+
+ WebRenderLayerScrollData layer;
+ APZTestAccess::InitializeForTest(layer, entry.mDescendantCount);
+ if (aVisibleRegions) {
+ layer.SetVisibleRegion(aVisibleRegions[entry.mLayerIndex]);
+ }
+ if (aTransforms) {
+ layer.SetTransform(aTransforms[entry.mLayerIndex]);
+ }
+ finishedLayers.push_back(std::move(layer));
+
+ // |finishedLayers| stores the layers in a different order than they
+ // appeared in |aTreeShape|. To be able to access layers by their layer
+ // index, keep a mapping from layer index to index in |finishedLayers|.
+ result.mIndexMap.emplace(entry.mLayerIndex, finishedLayers.size() - 1);
+
+ pendingLayers.pop();
+
+ // Keep track of descendant counts. The +1 is for the layer just finished.
+ if (!pendingLayers.empty()) {
+ pendingLayers.top().mDescendantCount += (entry.mDescendantCount + 1);
+ }
+ };
+ for (size_t i = 0; i < len; ++i) {
+ if (aTreeShape[i] == '(') {
+ ++depth;
+ } else if (aTreeShape[i] == ')') {
+ if (pendingLayers.size() <= 1) {
+ printf("Invalid tree shape: too many ')'\n");
+ MOZ_CRASH();
+ }
+ finishLayer(); // finish last layer at current depth
+ --depth;
+ } else {
+ if (aTreeShape[i] != 'x') {
+ printf("The only allowed character to represent a layer is 'x'\n");
+ MOZ_CRASH();
+ }
+ if (depth == pendingLayers.size()) {
+ // We have a previous layer at this same depth to finish.
+ if (depth <= 1) {
+ printf("The tree is only allowed to have one root\n");
+ MOZ_CRASH();
+ }
+ finishLayer();
+ }
+ MOZ_ASSERT(depth == pendingLayers.size() + 1);
+ pendingLayers.push({currentLayerIndex});
+ ++currentLayerIndex;
+ }
+ }
+ if (pendingLayers.size() != 1) {
+ printf("Invalid tree shape: '(' and ')' not balanced\n");
+ MOZ_CRASH();
+ }
+ finishLayer(); // finish root layer
+
+ // As in WebRenderCommandBuilder, the layers need to be added to the
+ // WebRenderScrollData in reverse of the order in which they were built.
+ for (auto it = finishedLayers.rbegin(); it != finishedLayers.rend(); ++it) {
+ result.AddLayerData(std::move(*it));
+ }
+ // mIndexMap also needs to be adjusted to accout for the reversal above.
+ for (auto& [layerIndex, storedIndex] : result.mIndexMap) {
+ (void)layerIndex; // suppress -Werror=unused-variable
+ storedIndex = result.GetLayerCount() - storedIndex - 1;
+ }
+
+ return result;
+}
+
+const WebRenderLayerScrollData* TestWRScrollData::operator[](
+ size_t aLayerIndex) const {
+ auto it = mIndexMap.find(aLayerIndex);
+ if (it == mIndexMap.end()) {
+ return nullptr;
+ }
+ return GetLayerData(it->second);
+}
+
+WebRenderLayerScrollData* TestWRScrollData::operator[](size_t aLayerIndex) {
+ auto it = mIndexMap.find(aLayerIndex);
+ if (it == mIndexMap.end()) {
+ return nullptr;
+ }
+ return GetLayerData(it->second);
+}
+
+void TestWRScrollData::SetScrollMetadata(
+ size_t aLayerIndex, const nsTArray<ScrollMetadata>& aMetadata) {
+ WebRenderLayerScrollData* layer = operator[](aLayerIndex);
+ MOZ_ASSERT(layer);
+ for (const ScrollMetadata& metadata : aMetadata) {
+ layer->AppendScrollMetadata(*this, metadata);
+ }
+}
+
+class WebRenderScrollDataWrapperTester : public ::testing::Test {
+ protected:
+ virtual void SetUp() {
+ // This ensures ScrollMetadata::sNullMetadata is initialized.
+ gfxPlatform::GetPlatform();
+
+ mManager = new APZCTreeManager(LayersId{0});
+ mUpdater = new APZUpdater(mManager, false);
+ }
+
+ RefPtr<APZCTreeManager> mManager;
+ RefPtr<APZUpdater> mUpdater;
+};
+
+TEST_F(WebRenderScrollDataWrapperTester, SimpleTree) {
+ auto layers = TestWRScrollData::Create("x(x(x(xx)x(x)))", *mUpdater);
+ WebRenderScrollDataWrapper w0(*mUpdater, &layers);
+
+ ASSERT_EQ(layers[0], w0.GetLayer());
+ WebRenderScrollDataWrapper w1 = w0.GetLastChild();
+ ASSERT_EQ(layers[1], w1.GetLayer());
+ ASSERT_FALSE(w1.GetPrevSibling().IsValid());
+ WebRenderScrollDataWrapper w5 = w1.GetLastChild();
+ ASSERT_EQ(layers[5], w5.GetLayer());
+ WebRenderScrollDataWrapper w6 = w5.GetLastChild();
+ ASSERT_EQ(layers[6], w6.GetLayer());
+ ASSERT_FALSE(w6.GetLastChild().IsValid());
+ WebRenderScrollDataWrapper w2 = w5.GetPrevSibling();
+ ASSERT_EQ(layers[2], w2.GetLayer());
+ ASSERT_FALSE(w2.GetPrevSibling().IsValid());
+ WebRenderScrollDataWrapper w4 = w2.GetLastChild();
+ ASSERT_EQ(layers[4], w4.GetLayer());
+ ASSERT_FALSE(w4.GetLastChild().IsValid());
+ WebRenderScrollDataWrapper w3 = w4.GetPrevSibling();
+ ASSERT_EQ(layers[3], w3.GetLayer());
+ ASSERT_FALSE(w3.GetLastChild().IsValid());
+ ASSERT_FALSE(w3.GetPrevSibling().IsValid());
+}
+
+static ScrollMetadata MakeMetadata(ScrollableLayerGuid::ViewID aId) {
+ ScrollMetadata metadata;
+ metadata.GetMetrics().SetScrollId(aId);
+ return metadata;
+}
+
+TEST_F(WebRenderScrollDataWrapperTester, MultiFramemetricsTree) {
+ auto layers = TestWRScrollData::Create("x(x(x(xx)x(x)))", *mUpdater);
+
+ nsTArray<ScrollMetadata> metadata;
+ metadata.InsertElementAt(0,
+ MakeMetadata(ScrollableLayerGuid::START_SCROLL_ID +
+ 0)); // topmost of root layer
+ metadata.InsertElementAt(0,
+ MakeMetadata(ScrollableLayerGuid::NULL_SCROLL_ID));
+ metadata.InsertElementAt(
+ 0, MakeMetadata(ScrollableLayerGuid::START_SCROLL_ID + 1));
+ metadata.InsertElementAt(
+ 0, MakeMetadata(ScrollableLayerGuid::START_SCROLL_ID + 2));
+ metadata.InsertElementAt(0,
+ MakeMetadata(ScrollableLayerGuid::NULL_SCROLL_ID));
+ metadata.InsertElementAt(
+ 0, MakeMetadata(
+ ScrollableLayerGuid::NULL_SCROLL_ID)); // bottom of root layer
+ layers.SetScrollMetadata(0, metadata);
+
+ metadata.Clear();
+ metadata.InsertElementAt(
+ 0, MakeMetadata(ScrollableLayerGuid::START_SCROLL_ID + 3));
+ layers.SetScrollMetadata(1, metadata);
+
+ metadata.Clear();
+ metadata.InsertElementAt(
+ 0, MakeMetadata(ScrollableLayerGuid::START_SCROLL_ID + 4));
+ layers.SetScrollMetadata(2, metadata);
+
+ metadata.Clear();
+ metadata.InsertElementAt(
+ 0, MakeMetadata(ScrollableLayerGuid::START_SCROLL_ID + 5));
+ layers.SetScrollMetadata(4, metadata);
+
+ metadata.Clear();
+ metadata.InsertElementAt(0,
+ MakeMetadata(ScrollableLayerGuid::NULL_SCROLL_ID));
+ metadata.InsertElementAt(
+ 0, MakeMetadata(ScrollableLayerGuid::START_SCROLL_ID + 6));
+ layers.SetScrollMetadata(5, metadata);
+
+ WebRenderScrollDataWrapper wrapper(*mUpdater, &layers);
+ nsTArray<WebRenderLayerScrollData*> expectedLayers;
+ expectedLayers.AppendElement(layers[0]);
+ expectedLayers.AppendElement(layers[0]);
+ expectedLayers.AppendElement(layers[0]);
+ expectedLayers.AppendElement(layers[0]);
+ expectedLayers.AppendElement(layers[0]);
+ expectedLayers.AppendElement(layers[0]);
+ expectedLayers.AppendElement(layers[1]);
+ expectedLayers.AppendElement(layers[5]);
+ expectedLayers.AppendElement(layers[5]);
+ expectedLayers.AppendElement(layers[6]);
+ nsTArray<ScrollableLayerGuid::ViewID> expectedIds;
+ expectedIds.AppendElement(ScrollableLayerGuid::START_SCROLL_ID + 0);
+ expectedIds.AppendElement(ScrollableLayerGuid::NULL_SCROLL_ID);
+ expectedIds.AppendElement(ScrollableLayerGuid::START_SCROLL_ID + 1);
+ expectedIds.AppendElement(ScrollableLayerGuid::START_SCROLL_ID + 2);
+ expectedIds.AppendElement(ScrollableLayerGuid::NULL_SCROLL_ID);
+ expectedIds.AppendElement(ScrollableLayerGuid::NULL_SCROLL_ID);
+ expectedIds.AppendElement(ScrollableLayerGuid::START_SCROLL_ID + 3);
+ expectedIds.AppendElement(ScrollableLayerGuid::NULL_SCROLL_ID);
+ expectedIds.AppendElement(ScrollableLayerGuid::START_SCROLL_ID + 6);
+ expectedIds.AppendElement(ScrollableLayerGuid::NULL_SCROLL_ID);
+ for (int i = 0; i < 10; i++) {
+ ASSERT_EQ(expectedLayers[i], wrapper.GetLayer());
+ ASSERT_EQ(expectedIds[i], wrapper.Metrics().GetScrollId());
+ wrapper = wrapper.GetLastChild();
+ }
+ ASSERT_FALSE(wrapper.IsValid());
+}
diff --git a/gfx/layers/apz/test/gtest/TestWRScrollData.h b/gfx/layers/apz/test/gtest/TestWRScrollData.h
new file mode 100644
index 0000000000..92e79aaee0
--- /dev/null
+++ b/gfx/layers/apz/test/gtest/TestWRScrollData.h
@@ -0,0 +1,63 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_TestWRScrollData_h
+#define mozilla_layers_TestWRScrollData_h
+
+#include "mozilla/gfx/MatrixFwd.h"
+#include "mozilla/layers/WebRenderScrollData.h"
+
+namespace mozilla {
+namespace layers {
+
+class APZUpdater;
+
+// Extends WebRenderScrollData with some methods useful for gtests.
+class TestWRScrollData : public WebRenderScrollData {
+ public:
+ TestWRScrollData() = default;
+ TestWRScrollData(TestWRScrollData&& aOther) = default;
+ TestWRScrollData& operator=(TestWRScrollData&& aOther) = default;
+
+ /*
+ * Create a WebRenderLayerScrollData tree described by |aTreeShape|.
+ * |aTreeShape| is expected to be a string where each character is
+ * either 'x' to indicate a node in the tree, or a '(' or ')' to indicate
+ * the start/end of a subtree.
+ *
+ * Example "x(x(x(xx)x))" would yield:
+ * x
+ * |
+ * x
+ * / \
+ * x x
+ * / \
+ * x x
+ *
+ * The caller may optionally provide visible regions and/or transforms
+ * for the nodes. If provided, the array should contain one element
+ * for each node, in the same order as in |aTreeShape|.
+ */
+ static TestWRScrollData Create(
+ const char* aTreeShape, const APZUpdater& aUpdater,
+ const LayerIntRegion* aVisibleRegions = nullptr,
+ const gfx::Matrix4x4* aTransforms = nullptr);
+
+ // These methods allow accessing and manipulating layers based on an index
+ // representing the order in which they appear in |aTreeShape|.
+ WebRenderLayerScrollData* operator[](size_t aLayerIndex);
+ const WebRenderLayerScrollData* operator[](size_t aLayerIndex) const;
+ void SetScrollMetadata(size_t aLayerIndex,
+ const nsTArray<ScrollMetadata>& aMetadata);
+
+ private:
+ std::map<size_t, size_t> mIndexMap; // Used to implement GetLayer()
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif
diff --git a/gfx/layers/apz/test/gtest/moz.build b/gfx/layers/apz/test/gtest/moz.build
new file mode 100644
index 0000000000..e6fb799008
--- /dev/null
+++ b/gfx/layers/apz/test/gtest/moz.build
@@ -0,0 +1,39 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+UNIFIED_SOURCES += [
+ "APZTestAccess.cpp",
+ "APZTestCommon.cpp",
+ "MockHitTester.cpp",
+ "TestAxisLock.cpp",
+ "TestBasic.cpp",
+ "TestEventRegions.cpp",
+ "TestEventResult.cpp",
+ "TestFlingAcceleration.cpp",
+ "TestGestureDetector.cpp",
+ "TestHitTesting.cpp",
+ "TestInputQueue.cpp",
+ "TestOverscroll.cpp",
+ "TestPanning.cpp",
+ "TestPinching.cpp",
+ "TestPointerEventsConsumable.cpp",
+ "TestScrollHandoff.cpp",
+ "TestSnapping.cpp",
+ "TestSnappingOnMomentum.cpp",
+ "TestTransformNotifications.cpp",
+ "TestTreeManager.cpp",
+ "TestWRScrollData.cpp",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+LOCAL_INCLUDES += [
+ "/gfx/2d",
+ "/gfx/cairo/cairo/src",
+ "/gfx/layers",
+]
+
+FINAL_LIBRARY = "xul-gtest"
diff --git a/gfx/layers/apz/test/gtest/mvm/TestMobileViewportManager.cpp b/gfx/layers/apz/test/gtest/mvm/TestMobileViewportManager.cpp
new file mode 100644
index 0000000000..cd7e0a7d0d
--- /dev/null
+++ b/gfx/layers/apz/test/gtest/mvm/TestMobileViewportManager.cpp
@@ -0,0 +1,220 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "gtest/gtest.h"
+#include "gmock/gmock.h"
+
+#include <functional>
+
+#include "MobileViewportManager.h"
+#include "mozilla/MVMContext.h"
+#include "mozilla/dom/Event.h"
+
+using namespace mozilla;
+
+class MockMVMContext : public MVMContext {
+ using AutoSizeFlag = nsViewportInfo::AutoSizeFlag;
+ using AutoScaleFlag = nsViewportInfo::AutoScaleFlag;
+ using ZoomFlag = nsViewportInfo::ZoomFlag;
+
+ // A "layout function" is a function that computes the content size
+ // as a function of the ICB size.
+ using LayoutFunction = std::function<CSSSize(CSSSize aICBSize)>;
+
+ public:
+ // MVMContext methods we don't care to implement.
+ MOCK_METHOD3(AddEventListener,
+ void(const nsAString& aType, nsIDOMEventListener* aListener,
+ bool aUseCapture));
+ MOCK_METHOD3(RemoveEventListener,
+ void(const nsAString& aType, nsIDOMEventListener* aListener,
+ bool aUseCapture));
+ MOCK_METHOD3(AddObserver, void(nsIObserver* aObserver, const char* aTopic,
+ bool aOwnsWeak));
+ MOCK_METHOD2(RemoveObserver,
+ void(nsIObserver* aObserver, const char* aTopic));
+ MOCK_METHOD0(Destroy, void());
+
+ MOCK_METHOD1(SetVisualViewportSize, void(const CSSSize& aSize));
+ MOCK_METHOD0(PostVisualViewportResizeEventByDynamicToolbar, void());
+ MOCK_METHOD0(UpdateDisplayPortMargins, void());
+
+ void SetMVM(MobileViewportManager* aMVM) { mMVM = aMVM; }
+
+ // MVMContext method implementations.
+ nsViewportInfo GetViewportInfo(const ScreenIntSize& aDisplaySize) const {
+ // This is a very basic approximation of what Document::GetViewportInfo()
+ // does in the most common cases.
+ // Ideally, we would invoke the algorithm in Document::GetViewportInfo()
+ // itself, but that would require refactoring it a bit to remove
+ // dependencies on the actual Document which we don't have available in
+ // this test harness.
+ CSSSize viewportSize = mDisplaySize / mDeviceScale;
+ if (mAutoSizeFlag == AutoSizeFlag::FixedSize) {
+ viewportSize = CSSSize(mFixedViewportWidth,
+ mFixedViewportWidth * (float(mDisplaySize.height) /
+ mDisplaySize.width));
+ }
+ return nsViewportInfo(mDefaultScale, mMinScale, mMaxScale, viewportSize,
+ mAutoSizeFlag, mAutoScaleFlag, mZoomFlag,
+ dom::ViewportFitType::Auto);
+ }
+ CSSToLayoutDeviceScale CSSToDevPixelScale() const { return mDeviceScale; }
+ float GetResolution() const { return mResolution; }
+ bool SubjectMatchesDocument(nsISupports* aSubject) const { return true; }
+ Maybe<CSSRect> CalculateScrollableRectForRSF() const {
+ return Some(CSSRect(CSSPoint(), mContentSize));
+ }
+ bool IsResolutionUpdatedByApz() const { return false; }
+ LayoutDeviceMargin ScrollbarAreaToExcludeFromCompositionBounds() const {
+ return LayoutDeviceMargin();
+ }
+ Maybe<LayoutDeviceIntSize> GetContentViewerSize() const {
+ return Some(mDisplaySize);
+ }
+ bool AllowZoomingForDocument() const { return true; }
+ bool IsInReaderMode() const { return false; }
+ bool IsDocumentLoading() const { return false; }
+
+ void SetResolutionAndScaleTo(float aResolution,
+ ResolutionChangeOrigin aOrigin) {
+ mResolution = aResolution;
+ mMVM->ResolutionUpdated(aOrigin);
+ }
+ void Reflow(const CSSSize& aNewSize) {
+ mICBSize = aNewSize;
+ mContentSize = mLayoutFunction(mICBSize);
+ }
+
+ // Allow test code to modify the input metrics.
+ void SetMinScale(CSSToScreenScale aMinScale) { mMinScale = aMinScale; }
+ void SetMaxScale(CSSToScreenScale aMaxScale) { mMaxScale = aMaxScale; }
+ void SetInitialScale(CSSToScreenScale aInitialScale) {
+ mDefaultScale = aInitialScale;
+ mAutoScaleFlag = AutoScaleFlag::FixedScale;
+ }
+ void SetFixedViewportWidth(CSSCoord aWidth) {
+ mFixedViewportWidth = aWidth;
+ mAutoSizeFlag = AutoSizeFlag::FixedSize;
+ }
+ void SetDisplaySize(const LayoutDeviceIntSize& aNewDisplaySize) {
+ mDisplaySize = aNewDisplaySize;
+ }
+ void SetLayoutFunction(const LayoutFunction& aLayoutFunction) {
+ mLayoutFunction = aLayoutFunction;
+ }
+
+ // Allow test code to query the output metrics.
+ CSSSize GetICBSize() const { return mICBSize; }
+ CSSSize GetContentSize() const { return mContentSize; }
+
+ private:
+ // Input metrics, with some sensible defaults.
+ LayoutDeviceIntSize mDisplaySize{300, 600};
+ CSSToScreenScale mDefaultScale{1.0f};
+ CSSToScreenScale mMinScale{0.25f};
+ CSSToScreenScale mMaxScale{10.0f};
+ CSSToLayoutDeviceScale mDeviceScale{1.0f};
+ CSSCoord mFixedViewportWidth;
+ AutoSizeFlag mAutoSizeFlag = AutoSizeFlag::AutoSize;
+ AutoScaleFlag mAutoScaleFlag = AutoScaleFlag::AutoScale;
+ ZoomFlag mZoomFlag = ZoomFlag::AllowZoom;
+ // As a default layout function, just set the content size to the ICB size.
+ LayoutFunction mLayoutFunction = [](CSSSize aICBSize) { return aICBSize; };
+
+ // Output metrics.
+ float mResolution = 1.0f;
+ CSSSize mICBSize;
+ CSSSize mContentSize;
+
+ MobileViewportManager* mMVM = nullptr;
+};
+
+class MVMTester : public ::testing::Test {
+ public:
+ MVMTester()
+ : mMVMContext(new MockMVMContext()),
+ mMVM(new MobileViewportManager(
+ mMVMContext,
+ MobileViewportManager::ManagerType::VisualAndMetaViewport)) {
+ mMVMContext->SetMVM(mMVM.get());
+ }
+
+ void Resize(const LayoutDeviceIntSize& aNewDisplaySize) {
+ mMVMContext->SetDisplaySize(aNewDisplaySize);
+ mMVM->RequestReflow(false);
+ }
+
+ protected:
+ RefPtr<MockMVMContext> mMVMContext;
+ RefPtr<MobileViewportManager> mMVM;
+};
+
+TEST_F(MVMTester, ZoomBoundsRespectedAfterRotation_Bug1536755) {
+ // Set up initial conditions.
+ mMVMContext->SetDisplaySize(LayoutDeviceIntSize(600, 300));
+ mMVMContext->SetInitialScale(CSSToScreenScale(1.0f));
+ mMVMContext->SetMinScale(CSSToScreenScale(1.0f));
+ mMVMContext->SetMaxScale(CSSToScreenScale(1.0f));
+ // Set a layout function that simulates a page which is twice
+ // as tall as it is wide.
+ mMVMContext->SetLayoutFunction([](CSSSize aICBSize) {
+ return CSSSize(aICBSize.width, aICBSize.width * 2);
+ });
+
+ // Perform an initial viewport computation and reflow, and
+ // sanity-check the results.
+ mMVM->SetInitialViewport();
+ EXPECT_EQ(CSSSize(600, 300), mMVMContext->GetICBSize());
+ EXPECT_EQ(CSSSize(600, 1200), mMVMContext->GetContentSize());
+ EXPECT_EQ(1.0f, mMVMContext->GetResolution());
+
+ // Now rotate the screen, and check that the minimum and maximum
+ // scales are still respected after the rotation.
+ Resize(LayoutDeviceIntSize(300, 600));
+ EXPECT_EQ(CSSSize(300, 600), mMVMContext->GetICBSize());
+ EXPECT_EQ(CSSSize(300, 600), mMVMContext->GetContentSize());
+ EXPECT_EQ(1.0f, mMVMContext->GetResolution());
+}
+
+TEST_F(MVMTester, LandscapeToPortraitRotation_Bug1523844) {
+ // Set up initial conditions.
+ mMVMContext->SetDisplaySize(LayoutDeviceIntSize(300, 600));
+ // Set a layout function that simulates a page with a fixed
+ // content size that's as wide as the screen in one orientation
+ // (and wider in the other).
+ mMVMContext->SetLayoutFunction(
+ [](CSSSize aICBSize) { return CSSSize(600, 1200); });
+
+ // Simulate a "DOMMetaAdded" event being fired before calling
+ // SetInitialViewport(). This matches what typically happens
+ // during real usage (the MVM receives the "DOMMetaAdded"
+ // before the "load", and it's the "load" that calls
+ // SetInitialViewport()), and is important to trigger this
+ // bug, because it causes the MVM to be stuck with an
+ // "mRestoreResolution" (prior to the fix).
+ mMVM->HandleDOMMetaAdded();
+
+ // Perform an initial viewport computation and reflow, and
+ // sanity-check the results.
+ mMVM->SetInitialViewport();
+ EXPECT_EQ(CSSSize(300, 600), mMVMContext->GetICBSize());
+ EXPECT_EQ(CSSSize(600, 1200), mMVMContext->GetContentSize());
+ EXPECT_EQ(0.5f, mMVMContext->GetResolution());
+
+ // Rotate to landscape.
+ Resize(LayoutDeviceIntSize(600, 300));
+ EXPECT_EQ(CSSSize(600, 300), mMVMContext->GetICBSize());
+ EXPECT_EQ(CSSSize(600, 1200), mMVMContext->GetContentSize());
+ EXPECT_EQ(1.0f, mMVMContext->GetResolution());
+
+ // Rotate back to portrait and check that we have returned
+ // to the portrait resolution.
+ Resize(LayoutDeviceIntSize(300, 600));
+ EXPECT_EQ(CSSSize(300, 600), mMVMContext->GetICBSize());
+ EXPECT_EQ(CSSSize(600, 1200), mMVMContext->GetContentSize());
+ EXPECT_EQ(0.5f, mMVMContext->GetResolution());
+}
diff --git a/gfx/layers/apz/test/gtest/mvm/moz.build b/gfx/layers/apz/test/gtest/mvm/moz.build
new file mode 100644
index 0000000000..0fa985307b
--- /dev/null
+++ b/gfx/layers/apz/test/gtest/mvm/moz.build
@@ -0,0 +1,13 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+UNIFIED_SOURCES += [
+ "TestMobileViewportManager.cpp",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul-gtest"
diff --git a/gfx/layers/apz/test/mochitest/FissionTestHelperChild.sys.mjs b/gfx/layers/apz/test/mochitest/FissionTestHelperChild.sys.mjs
new file mode 100644
index 0000000000..c0695b7abb
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/FissionTestHelperChild.sys.mjs
@@ -0,0 +1,157 @@
+// This code runs in the content process that holds the window to which
+// this actor is attached. There is one instance of this class for each
+// "inner window" (i.e. one per content document, including iframes/nested
+// iframes).
+// There is a 1:1 relationship between instances of this class and
+// FissionTestHelperParent instances, and the pair are entangled such
+// that they can communicate with each other regardless of which process
+// they live in.
+
+export class FissionTestHelperChild extends JSWindowActorChild {
+ constructor() {
+ super();
+ this._msgCounter = 0;
+ this._oopifResponsePromiseResolvers = [];
+ }
+
+ cw() {
+ return this.contentWindow.wrappedJSObject;
+ }
+
+ initialize() {
+ // This exports a bunch of things into the content window so that
+ // the test can access them. Most things are scoped inside the
+ // FissionTestHelper object on the window to avoid polluting the global
+ // namespace.
+
+ let cw = this.cw();
+ Cu.exportFunction(
+ (cond, msg) => this.sendAsyncMessage("ok", { cond, msg }),
+ cw,
+ { defineAs: "ok" }
+ );
+ Cu.exportFunction(
+ (a, b, msg) => this.sendAsyncMessage("is", { a, b, msg }),
+ cw,
+ { defineAs: "is" }
+ );
+
+ let FissionTestHelper = Cu.createObjectIn(cw, {
+ defineAs: "FissionTestHelper",
+ });
+ FissionTestHelper.startTestPromise = new cw.Promise(
+ Cu.exportFunction(resolve => {
+ this._startTestPromiseResolver = resolve;
+ }, cw)
+ );
+
+ Cu.exportFunction(this.subtestDone.bind(this), FissionTestHelper, {
+ defineAs: "subtestDone",
+ });
+
+ Cu.exportFunction(this.subtestFailed.bind(this), FissionTestHelper, {
+ defineAs: "subtestFailed",
+ });
+
+ Cu.exportFunction(this.sendToOopif.bind(this), FissionTestHelper, {
+ defineAs: "sendToOopif",
+ });
+ Cu.exportFunction(this.fireEventInEmbedder.bind(this), FissionTestHelper, {
+ defineAs: "fireEventInEmbedder",
+ });
+ }
+
+ // Called by the subtest to indicate completion to the top-level browser-chrome
+ // mochitest.
+ subtestDone() {
+ let cw = this.cw();
+ if (cw.ApzCleanup) {
+ cw.ApzCleanup.execute();
+ }
+ this.sendAsyncMessage("Test:Complete", {});
+ }
+
+ // Called by the subtest to indicate subtest failure. Only one of subtestDone
+ // or subtestFailed should be called.
+ subtestFailed(msg) {
+ this.sendAsyncMessage("ok", { cond: false, msg });
+ this.subtestDone();
+ }
+
+ // Called by the subtest to eval some code in the OOP iframe. This returns
+ // a promise that resolves to the return value from the eval.
+ sendToOopif(iframeElement, stringToEval) {
+ let browsingContextId = iframeElement.browsingContext.id;
+ let msgId = ++this._msgCounter;
+ let cw = this.cw();
+ let responsePromise = new cw.Promise(
+ Cu.exportFunction(resolve => {
+ this._oopifResponsePromiseResolvers[msgId] = resolve;
+ }, cw)
+ );
+ this.sendAsyncMessage("EmbedderToOopif", {
+ browsingContextId,
+ msgId,
+ stringToEval,
+ });
+ return responsePromise;
+ }
+
+ // Called by OOP iframes to dispatch an event in the embedder window. This
+ // can be used by the OOP iframe to asynchronously notify the embedder of
+ // things that happen. The embedder can use promiseOneEvent from
+ // helper_fission_utils.js to listen for these events.
+ fireEventInEmbedder(eventType, data) {
+ this.sendAsyncMessage("OopifToEmbedder", { eventType, data });
+ }
+
+ handleEvent(evt) {
+ switch (evt.type) {
+ case "FissionTestHelper:Init":
+ this.initialize();
+ break;
+ }
+ }
+
+ receiveMessage(msg) {
+ switch (msg.name) {
+ case "Test:Start":
+ this._startTestPromiseResolver();
+ delete this._startTestPromiseResolver;
+ break;
+ case "FromEmbedder":
+ let evalResult = this.contentWindow.eval(msg.data.stringToEval);
+ this.sendAsyncMessage("OopifToEmbedder", {
+ msgId: msg.data.msgId,
+ evalResult,
+ });
+ break;
+ case "FromOopif":
+ if (typeof msg.data.msgId == "number") {
+ if (!(msg.data.msgId in this._oopifResponsePromiseResolvers)) {
+ dump(
+ "Error: FromOopif got a message with unknown numeric msgId in " +
+ this.contentWindow.location.href +
+ "\n"
+ );
+ }
+ this._oopifResponsePromiseResolvers[msg.data.msgId](
+ msg.data.evalResult
+ );
+ delete this._oopifResponsePromiseResolvers[msg.data.msgId];
+ } else if (typeof msg.data.eventType == "string") {
+ let cw = this.cw();
+ let event = new cw.Event(msg.data.eventType);
+ event.data = Cu.cloneInto(msg.data.data, cw);
+ this.contentWindow.dispatchEvent(event);
+ } else {
+ dump(
+ "Warning: Unrecognized FromOopif message received in " +
+ this.contentWindow.location.href +
+ "\n"
+ );
+ }
+ break;
+ }
+ }
+}
diff --git a/gfx/layers/apz/test/mochitest/FissionTestHelperParent.sys.mjs b/gfx/layers/apz/test/mochitest/FissionTestHelperParent.sys.mjs
new file mode 100644
index 0000000000..d71de3b2ad
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/FissionTestHelperParent.sys.mjs
@@ -0,0 +1,103 @@
+// This code always runs in the parent process. There is one instance of
+// this class for each "inner window" (should be one per content document,
+// including iframes/nested iframes).
+// There is a 1:1 relationship between instances of this class and
+// FissionTestHelperChild instances, and the pair are entangled such
+// that they can communicate with each other regardless of which process
+// they live in.
+
+export class FissionTestHelperParent extends JSWindowActorParent {
+ constructor() {
+ super();
+ this._testCompletePromise = new Promise(resolve => {
+ this._testCompletePromiseResolver = resolve;
+ });
+ }
+
+ embedderWindow() {
+ let embedder = this.manager.browsingContext.embedderWindowGlobal;
+ // embedder is of type WindowGlobalParent, defined in WindowGlobalActors.webidl
+ if (!embedder) {
+ dump("ERROR: no embedder found in FissionTestHelperParent\n");
+ }
+ return embedder;
+ }
+
+ docURI() {
+ return this.manager.documentURI.spec;
+ }
+
+ // Returns a promise that is resolved when this parent actor receives a
+ // "Test:Complete" message from the child.
+ getTestCompletePromise() {
+ return this._testCompletePromise;
+ }
+
+ startTest() {
+ this.sendAsyncMessage("Test:Start", {});
+ }
+
+ receiveMessage(msg) {
+ switch (msg.name) {
+ case "ok":
+ FissionTestHelperParent.SimpleTest.ok(
+ msg.data.cond,
+ this.docURI() + " | " + msg.data.msg
+ );
+ break;
+
+ case "is":
+ FissionTestHelperParent.SimpleTest.is(
+ msg.data.a,
+ msg.data.b,
+ this.docURI() + " | " + msg.data.msg
+ );
+ break;
+
+ case "Test:Complete":
+ this._testCompletePromiseResolver();
+ break;
+
+ case "EmbedderToOopif":
+ // This relays messages from the embedder to an OOP-iframe. The browsing
+ // context id in the message data identifies the OOP-iframe.
+ let oopifBrowsingContext = BrowsingContext.get(
+ msg.data.browsingContextId
+ );
+ if (oopifBrowsingContext == null) {
+ FissionTestHelperParent.SimpleTest.ok(
+ false,
+ "EmbedderToOopif couldn't find oopif"
+ );
+ break;
+ }
+ let oopifActor =
+ oopifBrowsingContext.currentWindowGlobal.getActor(
+ "FissionTestHelper"
+ );
+ if (!oopifActor) {
+ FissionTestHelperParent.SimpleTest.ok(
+ false,
+ "EmbedderToOopif couldn't find oopif actor"
+ );
+ break;
+ }
+ oopifActor.sendAsyncMessage("FromEmbedder", msg.data);
+ break;
+
+ case "OopifToEmbedder":
+ // This relays messages from the OOP-iframe to the top-level content
+ // window which is embedding it.
+ let embedderActor = this.embedderWindow().getActor("FissionTestHelper");
+ if (!embedderActor) {
+ FissionTestHelperParent.SimpleTest.ok(
+ false,
+ "OopifToEmbedder couldn't find embedder"
+ );
+ break;
+ }
+ embedderActor.sendAsyncMessage("FromOopif", msg.data);
+ break;
+ }
+ }
+}
diff --git a/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js b/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js
new file mode 100644
index 0000000000..1b1ae8db26
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js
@@ -0,0 +1,1881 @@
+// ownerGlobal isn't defined in content privileged windows.
+/* eslint-disable mozilla/use-ownerGlobal */
+
+// Utilities for synthesizing of native events.
+
+async function getResolution() {
+ let resolution = -1; // bogus value in case DWU fails us
+ // Use window.top to get the root content window which is what has
+ // the resolution.
+ resolution = await SpecialPowers.spawn(window.top, [], () => {
+ return SpecialPowers.getDOMWindowUtils(content.window).getResolution();
+ });
+ return resolution;
+}
+
+function getPlatform() {
+ if (navigator.platform.indexOf("Win") == 0) {
+ return "windows";
+ }
+ if (navigator.platform.indexOf("Mac") == 0) {
+ return "mac";
+ }
+ // Check for Android before Linux
+ if (navigator.appVersion.includes("Android")) {
+ return "android";
+ }
+ if (navigator.platform.indexOf("Linux") == 0) {
+ return "linux";
+ }
+ return "unknown";
+}
+
+function nativeVerticalWheelEventMsg() {
+ switch (getPlatform()) {
+ case "windows":
+ return 0x020a; // WM_MOUSEWHEEL
+ case "mac":
+ var useWheelCodepath = SpecialPowers.getBoolPref(
+ "apz.test.mac.synth_wheel_input",
+ false
+ );
+ // Default to 1 (kCGScrollPhaseBegan) to trigger PanGestureInput events
+ // from widget code. Allow setting a pref to override this behaviour and
+ // trigger ScrollWheelInput events instead.
+ return useWheelCodepath ? 0 : 1;
+ case "linux":
+ return 4; // value is unused, pass GDK_SCROLL_SMOOTH anyway
+ }
+ throw new Error(
+ "Native wheel events not supported on platform " + getPlatform()
+ );
+}
+
+function nativeHorizontalWheelEventMsg() {
+ switch (getPlatform()) {
+ case "windows":
+ return 0x020e; // WM_MOUSEHWHEEL
+ case "mac":
+ return 0; // value is unused, can be anything
+ case "linux":
+ return 4; // value is unused, pass GDK_SCROLL_SMOOTH anyway
+ }
+ throw new Error(
+ "Native wheel events not supported on platform " + getPlatform()
+ );
+}
+
+function nativeArrowDownKey() {
+ switch (getPlatform()) {
+ case "windows":
+ return WIN_VK_DOWN;
+ case "mac":
+ return MAC_VK_DownArrow;
+ }
+ throw new Error(
+ "Native key events not supported on platform " + getPlatform()
+ );
+}
+
+function nativeArrowUpKey() {
+ switch (getPlatform()) {
+ case "windows":
+ return WIN_VK_UP;
+ case "mac":
+ return MAC_VK_UpArrow;
+ }
+ throw new Error(
+ "Native key events not supported on platform " + getPlatform()
+ );
+}
+
+function targetIsWindow(aTarget) {
+ return aTarget.Window && aTarget instanceof aTarget.Window;
+}
+
+function targetIsTopWindow(aTarget) {
+ if (!targetIsWindow(aTarget)) {
+ return false;
+ }
+ return aTarget == aTarget.top;
+}
+
+// Given an event target which may be a window or an element, get the associated window.
+function windowForTarget(aTarget) {
+ if (targetIsWindow(aTarget)) {
+ return aTarget;
+ }
+ return aTarget.ownerDocument.defaultView;
+}
+
+// Given an event target which may be a window or an element, get the associated element.
+function elementForTarget(aTarget) {
+ if (targetIsWindow(aTarget)) {
+ return aTarget.document.documentElement;
+ }
+ return aTarget;
+}
+
+// Given an event target which may be a window or an element, get the associatd nsIDOMWindowUtils.
+function utilsForTarget(aTarget) {
+ return SpecialPowers.getDOMWindowUtils(windowForTarget(aTarget));
+}
+
+// Given a pixel scrolling delta, converts it to the platform's native units.
+function nativeScrollUnits(aTarget, aDimen) {
+ switch (getPlatform()) {
+ case "linux": {
+ // GTK deltas are treated as line height divided by 3 by gecko.
+ var targetWindow = windowForTarget(aTarget);
+ var targetElement = elementForTarget(aTarget);
+ var lineHeight =
+ targetWindow.getComputedStyle(targetElement)["font-size"];
+ return aDimen / (parseInt(lineHeight) * 3);
+ }
+ }
+ return aDimen;
+}
+
+function parseNativeModifiers(aModifiers, aWindow = window) {
+ let modifiers = 0;
+ if (aModifiers.capsLockKey) {
+ modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_CAPS_LOCK;
+ }
+ if (aModifiers.numLockKey) {
+ modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_NUM_LOCK;
+ }
+ if (aModifiers.shiftKey) {
+ modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_SHIFT_LEFT;
+ }
+ if (aModifiers.shiftRightKey) {
+ modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_SHIFT_RIGHT;
+ }
+ if (aModifiers.ctrlKey) {
+ modifiers |=
+ SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_CONTROL_LEFT;
+ }
+ if (aModifiers.ctrlRightKey) {
+ modifiers |=
+ SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_CONTROL_RIGHT;
+ }
+ if (aModifiers.altKey) {
+ modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_ALT_LEFT;
+ }
+ if (aModifiers.altRightKey) {
+ modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_ALT_RIGHT;
+ }
+ if (aModifiers.metaKey) {
+ modifiers |=
+ SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_COMMAND_LEFT;
+ }
+ if (aModifiers.metaRightKey) {
+ modifiers |=
+ SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_COMMAND_RIGHT;
+ }
+ if (aModifiers.helpKey) {
+ modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_HELP;
+ }
+ if (aModifiers.fnKey) {
+ modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_FUNCTION;
+ }
+ if (aModifiers.numericKeyPadKey) {
+ modifiers |=
+ SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_NUMERIC_KEY_PAD;
+ }
+
+ if (aModifiers.accelKey) {
+ modifiers |= _EU_isMac(aWindow)
+ ? SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_COMMAND_LEFT
+ : SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_CONTROL_LEFT;
+ }
+ if (aModifiers.accelRightKey) {
+ modifiers |= _EU_isMac(aWindow)
+ ? SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_COMMAND_RIGHT
+ : SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_CONTROL_RIGHT;
+ }
+ if (aModifiers.altGrKey) {
+ modifiers |= _EU_isMac(aWindow)
+ ? SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_ALT_LEFT
+ : SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_ALT_GRAPH;
+ }
+ return modifiers;
+}
+
+// Several event sythesization functions below (and their helpers) take a "target"
+// parameter which may be either an element or a window. For such functions,
+// the target's "bounding rect" refers to the bounding client rect for an element,
+// and the window's origin for a window.
+// Not all functions have been "upgraded" to allow a window argument yet; feel
+// free to upgrade others as necessary.
+
+// Get the origin of |aTarget| relative to the root content document's
+// visual viewport in CSS coordinates.
+// |aTarget| may be an element (contained in the root content document or
+// a subdocument) or, as a special case, the root content window.
+// FIXME: Support iframe windows as targets.
+function _getTargetRect(aTarget) {
+ let rect = { left: 0, top: 0, width: 0, height: 0 };
+
+ // If the target is the root content window, its origin relative
+ // to the visual viewport is (0, 0).
+ if (aTarget instanceof Window) {
+ return rect;
+ }
+ if (aTarget.Window && aTarget instanceof aTarget.Window) {
+ // iframe window
+ // FIXME: Compute proper rect against the root content window
+ return rect;
+ }
+
+ // Otherwise, we have an element. Start with the origin of
+ // its bounding client rect which is relative to the enclosing
+ // document's layout viewport. Note that for iframes, the
+ // layout viewport is also the visual viewport.
+ const boundingClientRect = aTarget.getBoundingClientRect();
+ rect.left = boundingClientRect.left;
+ rect.top = boundingClientRect.top;
+ rect.width = boundingClientRect.width;
+ rect.height = boundingClientRect.height;
+
+ // Iterate up the window hierarchy until we reach the root
+ // content window, adding the offsets of any iframe windows
+ // relative to their parent window.
+ while (aTarget.ownerDocument.defaultView.frameElement) {
+ const iframe = aTarget.ownerDocument.defaultView.frameElement;
+ // The offset of the iframe window relative to the parent window
+ // includes the iframe's border, and the iframe's origin in its
+ // containing document.
+ const style = iframe.ownerDocument.defaultView.getComputedStyle(iframe);
+ const borderLeft = parseFloat(style.borderLeftWidth) || 0;
+ const borderTop = parseFloat(style.borderTopWidth) || 0;
+ const borderRight = parseFloat(style.borderRightWidth) || 0;
+ const borderBottom = parseFloat(style.borderBottomWidth) || 0;
+ const paddingLeft = parseFloat(style.paddingLeft) || 0;
+ const paddingTop = parseFloat(style.paddingTop) || 0;
+ const paddingRight = parseFloat(style.paddingRight) || 0;
+ const paddingBottom = parseFloat(style.paddingBottom) || 0;
+ const iframeRect = iframe.getBoundingClientRect();
+ rect.left += iframeRect.left + borderLeft + paddingLeft;
+ rect.top += iframeRect.top + borderTop + paddingTop;
+ if (
+ rect.left + rect.width >
+ iframeRect.right - borderRight - paddingRight
+ ) {
+ rect.width = Math.max(
+ iframeRect.right - borderRight - paddingRight - rect.left,
+ 0
+ );
+ }
+ if (
+ rect.top + rect.height >
+ iframeRect.bottom - borderBottom - paddingBottom
+ ) {
+ rect.height = Math.max(
+ iframeRect.bottom - borderBottom - paddingBottom - rect.top,
+ 0
+ );
+ }
+ aTarget = iframe;
+ }
+
+ return rect;
+}
+
+// Returns the in-process root window for the given |aWindow|.
+function getInProcessRootWindow(aWindow) {
+ let window = aWindow;
+ while (window.frameElement) {
+ window = window.frameElement.ownerDocument.defaultView;
+ }
+ return window;
+}
+
+// Convert (offsetX, offsetY) of target or center of it, in CSS pixels to device
+// pixels relative to the screen.
+// TODO: this function currently does not incorporate some CSS transforms on
+// elements enclosing target, e.g. scale transforms.
+async function coordinatesRelativeToScreen(aParams) {
+ const {
+ target, // The target element or window
+ offsetX, // X offset relative to `target`
+ offsetY, // Y offset relative to `target`
+ atCenter, // Instead of offsetX/offsetY, return center of `target`
+ } = aParams;
+ // Note that |window| might not be the root content window, for two
+ // possible reasons:
+ // 1. The mochitest that's calling into this function is not using a mechanism
+ // like runSubtestsSeriallyInFreshWindows() to load the test page in
+ // a top-level context, so it's loaded into an iframe by the mochitest
+ // harness.
+ // 2. The mochitest itself creates an iframe and calls this function from
+ // script running in the context of the iframe.
+ // Since the resolution applies to the top level content document, below we
+ // use the mozInnerScreen{X,Y} of the top level content window (window.top)
+ // only for the case where this function gets called in the top level content
+ // document. In other cases we use nsIDOMWindowUtils.toScreenRect().
+
+ // We do often specify `window` as the target, if it's the top level window,
+ // `nsIDOMWindowUtils.toScreenRect` isn't suitable because the function is
+ // supposed to be called with values in the document coords, so for example
+ // if desktop zoom is being applied, (0, 0) in the document coords might be
+ // outside of the visual viewport, i.e. it's going to be negative with the
+ // `toScreenRect` conversion, whereas the call sites with `window` of this
+ // function expect (0, 0) position should be the visual viport's offset. So
+ // in such cases we simply use mozInnerScreen{X,Y} to convert the given value
+ // to the screen coords.
+ if (target instanceof Window && window.parent == window) {
+ const resolution = await getResolution();
+ const deviceScale = window.devicePixelRatio;
+ return {
+ x:
+ window.mozInnerScreenX * deviceScale +
+ (atCenter ? 0 : offsetX) * resolution * deviceScale,
+ y:
+ window.mozInnerScreenY * deviceScale +
+ (atCenter ? 0 : offsetY) * resolution * deviceScale,
+ };
+ }
+
+ const rect = _getTargetRect(target);
+
+ const utils = SpecialPowers.getDOMWindowUtils(getInProcessRootWindow(window));
+ const positionInScreenCoords = utils.toScreenRect(
+ rect.left + (atCenter ? rect.width / 2 : offsetX),
+ rect.top + (atCenter ? rect.height / 2 : offsetY),
+ 0,
+ 0
+ );
+
+ return {
+ x: positionInScreenCoords.x,
+ y: positionInScreenCoords.y,
+ };
+}
+
+// Get the bounding box of aElement, and return it in device pixels
+// relative to the screen.
+// TODO: This function should probably take into account the resolution and
+// the relative viewport rect like coordinatesRelativeToScreen() does.
+function rectRelativeToScreen(aElement) {
+ var targetWindow = aElement.ownerDocument.defaultView;
+ var scale = targetWindow.devicePixelRatio;
+ var rect = aElement.getBoundingClientRect();
+ return {
+ x: (targetWindow.mozInnerScreenX + rect.left) * scale,
+ y: (targetWindow.mozInnerScreenY + rect.top) * scale,
+ width: rect.width * scale,
+ height: rect.height * scale,
+ };
+}
+
+// Synthesizes a native mousewheel event and returns immediately. This does not
+// guarantee anything; you probably want to use one of the other functions below
+// which actually wait for results.
+// aX and aY are relative to the top-left of |aTarget|'s bounding rect.
+// aDeltaX and aDeltaY are pixel deltas, and aObserver can be left undefined
+// if not needed.
+async function synthesizeNativeWheel(
+ aTarget,
+ aX,
+ aY,
+ aDeltaX,
+ aDeltaY,
+ aObserver
+) {
+ var pt = await coordinatesRelativeToScreen({
+ offsetX: aX,
+ offsetY: aY,
+ target: aTarget,
+ });
+ if (aDeltaX && aDeltaY) {
+ throw new Error(
+ "Simultaneous wheeling of horizontal and vertical is not supported on all platforms."
+ );
+ }
+ aDeltaX = nativeScrollUnits(aTarget, aDeltaX);
+ aDeltaY = nativeScrollUnits(aTarget, aDeltaY);
+ var msg = aDeltaX
+ ? nativeHorizontalWheelEventMsg()
+ : nativeVerticalWheelEventMsg();
+ var utils = utilsForTarget(aTarget);
+ var element = elementForTarget(aTarget);
+ utils.sendNativeMouseScrollEvent(
+ pt.x,
+ pt.y,
+ msg,
+ aDeltaX,
+ aDeltaY,
+ 0,
+ 0,
+ // Specify MOUSESCROLL_SCROLL_LINES if the test wants to run through wheel
+ // input code path on Mac since it's normal mouse wheel inputs.
+ SpecialPowers.getBoolPref("apz.test.mac.synth_wheel_input", false)
+ ? SpecialPowers.DOMWindowUtils.MOUSESCROLL_SCROLL_LINES
+ : 0,
+ element,
+ aObserver
+ );
+ return true;
+}
+
+// Synthesizes a native pan gesture event and returns immediately.
+// NOTE: This works only on Mac.
+// You can specify kCGScrollPhaseBegan = 1, kCGScrollPhaseChanged = 2 and
+// kCGScrollPhaseEnded = 4 for |aPhase|.
+async function synthesizeNativePanGestureEvent(
+ aTarget,
+ aX,
+ aY,
+ aDeltaX,
+ aDeltaY,
+ aPhase,
+ aObserver
+) {
+ if (getPlatform() != "mac") {
+ throw new Error(
+ `synthesizeNativePanGestureEvent doesn't work on ${getPlatform()}`
+ );
+ }
+
+ var pt = await coordinatesRelativeToScreen({
+ offsetX: aX,
+ offsetY: aY,
+ target: aTarget,
+ });
+ if (aDeltaX && aDeltaY) {
+ throw new Error(
+ "Simultaneous panning of horizontal and vertical is not supported."
+ );
+ }
+
+ aDeltaX = nativeScrollUnits(aTarget, aDeltaX);
+ aDeltaY = nativeScrollUnits(aTarget, aDeltaY);
+
+ var element = elementForTarget(aTarget);
+ var utils = utilsForTarget(aTarget);
+ utils.sendNativeMouseScrollEvent(
+ pt.x,
+ pt.y,
+ aPhase,
+ aDeltaX,
+ aDeltaY,
+ 0 /* deltaZ */,
+ 0 /* modifiers */,
+ 0 /* scroll event unit pixel */,
+ element,
+ aObserver
+ );
+
+ return true;
+}
+
+// Sends a native touchpad pan event and resolve the returned promise once the
+// request has been successfully made to the OS.
+// NOTE: This works only on Windows and Linux.
+// You can specify nsIDOMWindowUtils.PHASE_BEGIN, PHASE_UPDATE and PHASE_END
+// for |aPhase|.
+async function promiseNativeTouchpadPanEventAndWaitForObserver(
+ aTarget,
+ aX,
+ aY,
+ aDeltaX,
+ aDeltaY,
+ aPhase
+) {
+ if (getPlatform() != "windows" && getPlatform() != "linux") {
+ throw new Error(
+ `promiseNativeTouchpadPanEventAndWaitForObserver doesn't work on ${getPlatform()}`
+ );
+ }
+
+ let pt = await coordinatesRelativeToScreen({
+ offsetX: aX,
+ offsetY: aY,
+ target: aTarget,
+ });
+
+ const utils = utilsForTarget(aTarget);
+
+ return new Promise(resolve => {
+ var observer = {
+ observe(aSubject, aTopic, aData) {
+ if (aTopic == "touchpadpanevent") {
+ resolve();
+ }
+ },
+ };
+
+ utils.sendNativeTouchpadPan(
+ aPhase,
+ pt.x,
+ pt.y,
+ aDeltaX,
+ aDeltaY,
+ 0,
+ observer
+ );
+ });
+}
+
+async function synthesizeSimpleGestureEvent(
+ aElement,
+ aType,
+ aX,
+ aY,
+ aDirection,
+ aDelta,
+ aModifiers,
+ aClickCount
+) {
+ let pt = await coordinatesRelativeToScreen({
+ offsetX: aX,
+ offsetY: aY,
+ target: aElement,
+ });
+
+ let utils = utilsForTarget(aElement);
+ utils.sendSimpleGestureEvent(
+ aType,
+ pt.x,
+ pt.y,
+ aDirection,
+ aDelta,
+ aModifiers,
+ aClickCount
+ );
+}
+
+// Synthesizes a native pan gesture event and resolve the returned promise once the
+// request has been successfully made to the OS.
+function promiseNativePanGestureEventAndWaitForObserver(
+ aElement,
+ aX,
+ aY,
+ aDeltaX,
+ aDeltaY,
+ aPhase
+) {
+ return new Promise(resolve => {
+ var observer = {
+ observe(aSubject, aTopic, aData) {
+ if (aTopic == "mousescrollevent") {
+ resolve();
+ }
+ },
+ };
+ synthesizeNativePanGestureEvent(
+ aElement,
+ aX,
+ aY,
+ aDeltaX,
+ aDeltaY,
+ aPhase,
+ observer
+ );
+ });
+}
+
+// Synthesizes a native mousewheel event and resolve the returned promise once the
+// request has been successfully made to the OS. This does not necessarily
+// guarantee that the OS generates the event we requested. See
+// synthesizeNativeWheel for details on the parameters.
+function promiseNativeWheelAndWaitForObserver(
+ aElement,
+ aX,
+ aY,
+ aDeltaX,
+ aDeltaY
+) {
+ return new Promise(resolve => {
+ var observer = {
+ observe(aSubject, aTopic, aData) {
+ if (aTopic == "mousescrollevent") {
+ resolve();
+ }
+ },
+ };
+ synthesizeNativeWheel(aElement, aX, aY, aDeltaX, aDeltaY, observer);
+ });
+}
+
+// Synthesizes a native mousewheel event and resolve the returned promise once the
+// wheel event is dispatched to |aTarget|'s containing window. If the event
+// targets content in a subdocument, |aTarget| should be inside the
+// subdocument (or the subdocument's window). See synthesizeNativeWheel for
+// details on the other parameters.
+function promiseNativeWheelAndWaitForWheelEvent(
+ aTarget,
+ aX,
+ aY,
+ aDeltaX,
+ aDeltaY
+) {
+ return new Promise((resolve, reject) => {
+ var targetWindow = windowForTarget(aTarget);
+ targetWindow.addEventListener(
+ "wheel",
+ function (e) {
+ setTimeout(resolve, 0);
+ },
+ { once: true }
+ );
+ try {
+ synthesizeNativeWheel(aTarget, aX, aY, aDeltaX, aDeltaY);
+ } catch (e) {
+ reject(e);
+ }
+ });
+}
+
+// Synthesizes a native mousewheel event and resolves the returned promise once the
+// first resulting scroll event is dispatched to |aTarget|'s containing window.
+// If the event targets content in a subdocument, |aTarget| should be inside
+// the subdocument (or the subdocument's window). See synthesizeNativeWheel
+// for details on the other parameters.
+function promiseNativeWheelAndWaitForScrollEvent(
+ aTarget,
+ aX,
+ aY,
+ aDeltaX,
+ aDeltaY
+) {
+ return new Promise((resolve, reject) => {
+ var targetWindow = windowForTarget(aTarget);
+ targetWindow.addEventListener(
+ "scroll",
+ function () {
+ setTimeout(resolve, 0);
+ },
+ { capture: true, once: true }
+ ); // scroll events don't always bubble
+ try {
+ synthesizeNativeWheel(aTarget, aX, aY, aDeltaX, aDeltaY);
+ } catch (e) {
+ reject(e);
+ }
+ });
+}
+
+async function synthesizeTouchpadPinch(scales, focusX, focusY, options) {
+ var scalesAndFoci = [];
+
+ for (let i = 0; i < scales.length; i++) {
+ scalesAndFoci.push([scales[i], focusX, focusY]);
+ }
+
+ await synthesizeTouchpadGesture(scalesAndFoci, options);
+}
+
+// scalesAndFoci is an array of [scale, focusX, focuxY] tuples.
+async function synthesizeTouchpadGesture(scalesAndFoci, options) {
+ // Check for options, fill in defaults if appropriate.
+ let waitForTransformEnd =
+ options.waitForTransformEnd !== undefined
+ ? options.waitForTransformEnd
+ : true;
+ let waitForFrames =
+ options.waitForFrames !== undefined ? options.waitForFrames : false;
+
+ // Register the listener for the TransformEnd observer topic
+ let transformEndPromise = promiseTransformEnd();
+
+ var modifierFlags = 0;
+ var utils = utilsForTarget(document.body);
+ for (let i = 0; i < scalesAndFoci.length; i++) {
+ var pt = await coordinatesRelativeToScreen({
+ offsetX: scalesAndFoci[i][1],
+ offsetY: scalesAndFoci[i][2],
+ target: document.body,
+ });
+ var phase;
+ if (i === 0) {
+ phase = SpecialPowers.DOMWindowUtils.PHASE_BEGIN;
+ } else if (i === scalesAndFoci.length - 1) {
+ phase = SpecialPowers.DOMWindowUtils.PHASE_END;
+ } else {
+ phase = SpecialPowers.DOMWindowUtils.PHASE_UPDATE;
+ }
+ utils.sendNativeTouchpadPinch(
+ phase,
+ scalesAndFoci[i][0],
+ pt.x,
+ pt.y,
+ modifierFlags
+ );
+ if (waitForFrames) {
+ await promiseFrame();
+ }
+ }
+
+ // Wait for TransformEnd to fire.
+ if (waitForTransformEnd) {
+ await transformEndPromise;
+ }
+}
+
+async function synthesizeTouchpadPan(
+ focusX,
+ focusY,
+ deltaXs,
+ deltaYs,
+ options
+) {
+ // Check for options, fill in defaults if appropriate.
+ let waitForTransformEnd =
+ options.waitForTransformEnd !== undefined
+ ? options.waitForTransformEnd
+ : true;
+ let waitForFrames =
+ options.waitForFrames !== undefined ? options.waitForFrames : false;
+
+ // Register the listener for the TransformEnd observer topic
+ let transformEndPromise = promiseTransformEnd();
+
+ var modifierFlags = 0;
+ var pt = await coordinatesRelativeToScreen({
+ offsetX: focusX,
+ offsetY: focusY,
+ target: document.body,
+ });
+ var utils = utilsForTarget(document.body);
+ for (let i = 0; i < deltaXs.length; i++) {
+ var phase;
+ if (i === 0) {
+ phase = SpecialPowers.DOMWindowUtils.PHASE_BEGIN;
+ } else if (i === deltaXs.length - 1) {
+ phase = SpecialPowers.DOMWindowUtils.PHASE_END;
+ } else {
+ phase = SpecialPowers.DOMWindowUtils.PHASE_UPDATE;
+ }
+ utils.sendNativeTouchpadPan(
+ phase,
+ pt.x,
+ pt.y,
+ deltaXs[i],
+ deltaYs[i],
+ modifierFlags
+ );
+ if (waitForFrames) {
+ await promiseFrame();
+ }
+ }
+
+ // Wait for TransformEnd to fire.
+ if (waitForTransformEnd) {
+ await transformEndPromise;
+ }
+}
+
+// Synthesizes a native touch event and dispatches it. aX and aY in CSS pixels
+// relative to the top-left of |aTarget|'s bounding rect.
+async function synthesizeNativeTouch(
+ aTarget,
+ aX,
+ aY,
+ aType,
+ aObserver = null,
+ aTouchId = 0
+) {
+ var pt = await coordinatesRelativeToScreen({
+ offsetX: aX,
+ offsetY: aY,
+ target: aTarget,
+ });
+ var utils = utilsForTarget(aTarget);
+ utils.sendNativeTouchPoint(aTouchId, aType, pt.x, pt.y, 1, 90, aObserver);
+ return true;
+}
+
+function sendBasicNativePointerInput(
+ utils,
+ aId,
+ aPointerType,
+ aState,
+ aX,
+ aY,
+ aObserver,
+ { pressure = 1, twist = 0, tiltX = 0, tiltY = 0, button = 0 } = {}
+) {
+ switch (aPointerType) {
+ case "touch":
+ utils.sendNativeTouchPoint(aId, aState, aX, aY, pressure, 90, aObserver);
+ break;
+ case "pen":
+ utils.sendNativePenInput(
+ aId,
+ aState,
+ aX,
+ aY,
+ pressure,
+ twist,
+ tiltX,
+ tiltY,
+ button,
+ aObserver
+ );
+ break;
+ default:
+ throw new Error(`Not supported: ${aPointerType}`);
+ }
+}
+
+async function promiseNativePointerInput(
+ aTarget,
+ aPointerType,
+ aState,
+ aX,
+ aY,
+ options
+) {
+ const pt = await coordinatesRelativeToScreen({
+ offsetX: aX,
+ offsetY: aY,
+ target: aTarget,
+ });
+ const utils = utilsForTarget(aTarget);
+ return new Promise(resolve => {
+ sendBasicNativePointerInput(
+ utils,
+ options?.pointerId ?? 0,
+ aPointerType,
+ aState,
+ pt.x,
+ pt.y,
+ resolve,
+ options
+ );
+ });
+}
+
+/**
+ * Function to generate native pointer events as a sequence.
+ * @param aTarget is the element or window whose bounding rect the coordinates are
+ * relative to.
+ * @param aPointerType "touch" or "pen".
+ * @param aPositions is a 2D array of position data. It is indexed as [row][column],
+ * where advancing the row counter moves forward in time, and each column
+ * represents a single pointer. Each row must have exactly
+ * the same number of columns, and the number of columns must match the length
+ * of the aPointerIds parameter.
+ * For each row, each entry is either an object with x and y fields,
+ * or a null. A null value indicates that the pointer should be "lifted"
+ * (i.e. send a touchend for that touch input). A non-null value therefore
+ * indicates the position of the pointer input.
+ * This function takes care of the state tracking necessary to send
+ * pointerup/pointerdown inputs as necessary as the pointers go up and down.
+ * @param aObserver is the observer that will get registered on the very last
+ * native pointer synthesis call this function makes.
+ * @param aPointerIds is an array holding the pointer ID values.
+ */
+async function synthesizeNativePointerSequences(
+ aTarget,
+ aPointerType,
+ aPositions,
+ aObserver = null,
+ aPointerIds = [0],
+ options
+) {
+ // We use lastNonNullValue to figure out which synthesizeNativeTouch call
+ // will be the last one we make, so that we can register aObserver on it.
+ var lastNonNullValue = -1;
+ for (let i = 0; i < aPositions.length; i++) {
+ if (aPositions[i] == null) {
+ throw new Error(`aPositions[${i}] was unexpectedly null`);
+ }
+ if (aPositions[i].length != aPointerIds.length) {
+ throw new Error(
+ `aPositions[${i}] did not have the expected number of positions; ` +
+ `expected ${aPointerIds.length} pointers but found ${aPositions[i].length}`
+ );
+ }
+ for (let j = 0; j < aPointerIds.length; j++) {
+ if (aPositions[i][j] != null) {
+ lastNonNullValue = i * aPointerIds.length + j;
+ // Do the conversion to screen space before actually synthesizing
+ // the events, otherwise the screen space may change as a result of
+ // the touch inputs and the conversion may not work as intended.
+ aPositions[i][j] = await coordinatesRelativeToScreen({
+ offsetX: aPositions[i][j].x,
+ offsetY: aPositions[i][j].y,
+ target: aTarget,
+ });
+ }
+ }
+ }
+ if (lastNonNullValue < 0) {
+ throw new Error("All values in positions array were null!");
+ }
+
+ // Insert a row of nulls at the end of aPositions, to ensure that all
+ // touches get removed. If the touches have already been removed this will
+ // just add an extra no-op iteration in the aPositions loop below.
+ var allNullRow = new Array(aPointerIds.length);
+ allNullRow.fill(null);
+ aPositions.push(allNullRow);
+
+ // The last sendNativeTouchPoint call will be the TOUCH_REMOVE which happens
+ // one iteration of aPosition after the last non-null value.
+ var lastSynthesizeCall = lastNonNullValue + aPointerIds.length;
+
+ // track which touches are down and which are up. start with all up
+ var currentPositions = new Array(aPointerIds.length);
+ currentPositions.fill(null);
+
+ var utils = utilsForTarget(aTarget);
+ // Iterate over the position data now, and generate the touches requested
+ for (let i = 0; i < aPositions.length; i++) {
+ for (let j = 0; j < aPointerIds.length; j++) {
+ if (aPositions[i][j] == null) {
+ // null means lift the finger
+ if (currentPositions[j] == null) {
+ // it's already lifted, do nothing
+ } else {
+ // synthesize the touch-up. If this is the last call we're going to
+ // make, pass the observer as well
+ var thisIndex = i * aPointerIds.length + j;
+ var observer = lastSynthesizeCall == thisIndex ? aObserver : null;
+ sendBasicNativePointerInput(
+ utils,
+ aPointerIds[j],
+ aPointerType,
+ SpecialPowers.DOMWindowUtils.TOUCH_REMOVE,
+ currentPositions[j].x,
+ currentPositions[j].y,
+ observer,
+ options
+ );
+ currentPositions[j] = null;
+ }
+ } else {
+ sendBasicNativePointerInput(
+ utils,
+ aPointerIds[j],
+ aPointerType,
+ SpecialPowers.DOMWindowUtils.TOUCH_CONTACT,
+ aPositions[i][j].x,
+ aPositions[i][j].y,
+ null,
+ options
+ );
+ currentPositions[j] = aPositions[i][j];
+ }
+ }
+ }
+ return true;
+}
+
+async function synthesizeNativeTouchSequences(
+ aTarget,
+ aPositions,
+ aObserver = null,
+ aTouchIds = [0]
+) {
+ await synthesizeNativePointerSequences(
+ aTarget,
+ "touch",
+ aPositions,
+ aObserver,
+ aTouchIds
+ );
+}
+
+async function synthesizeNativePointerDrag(
+ aTarget,
+ aPointerType,
+ aX,
+ aY,
+ aDeltaX,
+ aDeltaY,
+ aObserver = null,
+ aPointerId = 0,
+ options
+) {
+ var steps = Math.max(Math.abs(aDeltaX), Math.abs(aDeltaY));
+ var positions = [[{ x: aX, y: aY }]];
+ for (var i = 1; i < steps; i++) {
+ var dx = i * (aDeltaX / steps);
+ var dy = i * (aDeltaY / steps);
+ var pos = { x: aX + dx, y: aY + dy };
+ positions.push([pos]);
+ }
+ positions.push([{ x: aX + aDeltaX, y: aY + aDeltaY }]);
+ return synthesizeNativePointerSequences(
+ aTarget,
+ aPointerType,
+ positions,
+ aObserver,
+ [aPointerId],
+ options
+ );
+}
+
+// Note that when calling this function you'll want to make sure that the pref
+// "apz.touch_start_tolerance" is set to 0, or some of the touchmove will get
+// consumed to overcome the panning threshold.
+async function synthesizeNativeTouchDrag(
+ aTarget,
+ aX,
+ aY,
+ aDeltaX,
+ aDeltaY,
+ aObserver = null,
+ aTouchId = 0
+) {
+ return synthesizeNativePointerDrag(
+ aTarget,
+ "touch",
+ aX,
+ aY,
+ aDeltaX,
+ aDeltaY,
+ aObserver,
+ aTouchId
+ );
+}
+
+function promiseNativePointerDrag(
+ aTarget,
+ aPointerType,
+ aX,
+ aY,
+ aDeltaX,
+ aDeltaY,
+ aPointerId = 0,
+ options
+) {
+ return new Promise(resolve => {
+ synthesizeNativePointerDrag(
+ aTarget,
+ aPointerType,
+ aX,
+ aY,
+ aDeltaX,
+ aDeltaY,
+ resolve,
+ aPointerId,
+ options
+ );
+ });
+}
+
+// Promise-returning variant of synthesizeNativeTouchDrag
+function promiseNativeTouchDrag(
+ aTarget,
+ aX,
+ aY,
+ aDeltaX,
+ aDeltaY,
+ aTouchId = 0
+) {
+ return new Promise(resolve => {
+ synthesizeNativeTouchDrag(
+ aTarget,
+ aX,
+ aY,
+ aDeltaX,
+ aDeltaY,
+ resolve,
+ aTouchId
+ );
+ });
+}
+
+// Tapping is essentially a dragging with no move
+function promiseNativePointerTap(aTarget, aPointerType, aX, aY, options) {
+ return promiseNativePointerDrag(
+ aTarget,
+ aPointerType,
+ aX,
+ aY,
+ 0,
+ 0,
+ options?.pointerId ?? 0,
+ options
+ );
+}
+
+async function synthesizeNativeTap(aTarget, aX, aY, aObserver = null) {
+ var pt = await coordinatesRelativeToScreen({
+ offsetX: aX,
+ offsetY: aY,
+ target: aTarget,
+ });
+ let utils = utilsForTarget(aTarget);
+ utils.sendNativeTouchTap(pt.x, pt.y, false, aObserver);
+ return true;
+}
+
+// only currently implemented on macOS
+async function synthesizeNativeTouchpadDoubleTap(aTarget, aX, aY) {
+ ok(
+ getPlatform() == "mac",
+ "only implemented on mac. implement sendNativeTouchpadDoubleTap for this platform," +
+ " see bug 1696802 for how it was done on macOS"
+ );
+ let pt = await coordinatesRelativeToScreen({
+ offsetX: aX,
+ offsetY: aY,
+ target: aTarget,
+ });
+ let utils = utilsForTarget(aTarget);
+ utils.sendNativeTouchpadDoubleTap(pt.x, pt.y, 0);
+ return true;
+}
+
+// If the event targets content in a subdocument, |aTarget| should be inside the
+// subdocument (or the subdocument window).
+async function synthesizeNativeMouseEventWithAPZ(aParams, aObserver = null) {
+ if (aParams.win !== undefined) {
+ throw Error(
+ "Are you trying to use EventUtils' API? `win` won't be used with synthesizeNativeMouseClickWithAPZ."
+ );
+ }
+ if (aParams.scale !== undefined) {
+ throw Error(
+ "Are you trying to use EventUtils' API? `scale` won't be used with synthesizeNativeMouseClickWithAPZ."
+ );
+ }
+ if (aParams.elementOnWidget !== undefined) {
+ throw Error(
+ "Are you trying to use EventUtils' API? `elementOnWidget` won't be used with synthesizeNativeMouseClickWithAPZ."
+ );
+ }
+ const {
+ type, // "click", "mousedown", "mouseup" or "mousemove"
+ target, // Origin of offsetX and offsetY, must be an element
+ offsetX, // X offset in `target` in CSS Pixels
+ offsetY, // Y offset in `target` in CSS pixels
+ atCenter, // Instead of offsetX/Y, synthesize the event at center of `target`
+ screenX, // X offset in screen in device pixels, offsetX/Y nor atCenter must not be set if this is set
+ screenY, // Y offset in screen in device pixels, offsetX/Y nor atCenter must not be set if this is set
+ button = 0, // if "click", "mousedown", "mouseup", set same value as DOM MouseEvent.button
+ modifiers = {}, // Active modifiers, see `parseNativeModifiers`
+ } = aParams;
+ if (atCenter) {
+ if (offsetX != undefined || offsetY != undefined) {
+ throw Error(
+ `atCenter is specified, but offsetX (${offsetX}) and/or offsetY (${offsetY}) are also specified`
+ );
+ }
+ if (screenX != undefined || screenY != undefined) {
+ throw Error(
+ `atCenter is specified, but screenX (${screenX}) and/or screenY (${screenY}) are also specified`
+ );
+ }
+ } else if (offsetX != undefined && offsetY != undefined) {
+ if (screenX != undefined || screenY != undefined) {
+ throw Error(
+ `offsetX/Y are specified, but screenX (${screenX}) and/or screenY (${screenY}) are also specified`
+ );
+ }
+ } else if (screenX != undefined && screenY != undefined) {
+ if (offsetX != undefined || offsetY != undefined) {
+ throw Error(
+ `screenX/Y are specified, but offsetX (${offsetX}) and/or offsetY (${offsetY}) are also specified`
+ );
+ }
+ }
+ const pt = await (async () => {
+ if (screenX != undefined) {
+ return { x: screenX, y: screenY };
+ }
+ return coordinatesRelativeToScreen({
+ offsetX,
+ offsetY,
+ atCenter,
+ target,
+ });
+ })();
+ const utils = utilsForTarget(target);
+ const element = elementForTarget(target);
+ const modifierFlags = parseNativeModifiers(modifiers);
+ if (type === "click") {
+ utils.sendNativeMouseEvent(
+ pt.x,
+ pt.y,
+ utils.NATIVE_MOUSE_MESSAGE_BUTTON_DOWN,
+ button,
+ modifierFlags,
+ element,
+ function () {
+ utils.sendNativeMouseEvent(
+ pt.x,
+ pt.y,
+ utils.NATIVE_MOUSE_MESSAGE_BUTTON_UP,
+ button,
+ modifierFlags,
+ element,
+ aObserver
+ );
+ }
+ );
+ return;
+ }
+
+ utils.sendNativeMouseEvent(
+ pt.x,
+ pt.y,
+ (() => {
+ switch (type) {
+ case "mousedown":
+ return utils.NATIVE_MOUSE_MESSAGE_BUTTON_DOWN;
+ case "mouseup":
+ return utils.NATIVE_MOUSE_MESSAGE_BUTTON_UP;
+ case "mousemove":
+ return utils.NATIVE_MOUSE_MESSAGE_MOVE;
+ default:
+ throw Error(`Invalid type is specified: ${type}`);
+ }
+ })(),
+ button,
+ modifierFlags,
+ element,
+ aObserver
+ );
+}
+
+function promiseNativeMouseEventWithAPZ(aParams) {
+ return new Promise(resolve =>
+ synthesizeNativeMouseEventWithAPZ(aParams, resolve)
+ );
+}
+
+// See synthesizeNativeMouseEventWithAPZ for the detail of aParams.
+function promiseNativeMouseEventWithAPZAndWaitForEvent(aParams) {
+ return new Promise(resolve => {
+ const targetWindow = windowForTarget(aParams.target);
+ const eventType = aParams.eventTypeToWait || aParams.type;
+ targetWindow.addEventListener(eventType, resolve, {
+ once: true,
+ });
+ synthesizeNativeMouseEventWithAPZ(aParams);
+ });
+}
+
+// Move the mouse to (dx, dy) relative to |target|, and scroll the wheel
+// at that location.
+// Moving the mouse is necessary to avoid wheel events from two consecutive
+// promiseMoveMouseAndScrollWheelOver() calls on different elements being incorrectly
+// considered as part of the same wheel transaction.
+// We also wait for the mouse move event to be processed before sending the
+// wheel event, otherwise there is a chance they might get reordered, and
+// we have the transaction problem again.
+// This function returns a promise that is resolved when the resulting wheel
+// (if waitForScroll = false) or scroll (if waitForScroll = true) event is
+// received.
+function promiseMoveMouseAndScrollWheelOver(
+ target,
+ dx,
+ dy,
+ waitForScroll = true,
+ scrollDelta = 10
+) {
+ let p = promiseNativeMouseEventWithAPZAndWaitForEvent({
+ type: "mousemove",
+ target,
+ offsetX: dx,
+ offsetY: dy,
+ });
+ if (waitForScroll) {
+ p = p.then(() => {
+ return promiseNativeWheelAndWaitForScrollEvent(
+ target,
+ dx,
+ dy,
+ 0,
+ -scrollDelta
+ );
+ });
+ } else {
+ p = p.then(() => {
+ return promiseNativeWheelAndWaitForWheelEvent(
+ target,
+ dx,
+ dy,
+ 0,
+ -scrollDelta
+ );
+ });
+ }
+ return p;
+}
+
+async function scrollbarDragStart(aTarget, aScaleFactor) {
+ var targetElement = elementForTarget(aTarget);
+ var w = {},
+ h = {};
+ utilsForTarget(aTarget).getScrollbarSizes(targetElement, w, h);
+ var verticalScrollbarWidth = w.value;
+ if (verticalScrollbarWidth == 0) {
+ return null;
+ }
+
+ var upArrowHeight = verticalScrollbarWidth; // assume square scrollbar buttons
+ var startX = targetElement.clientWidth + verticalScrollbarWidth / 2;
+ var startY = upArrowHeight + 5; // start dragging somewhere in the thumb
+ startX *= aScaleFactor;
+ startY *= aScaleFactor;
+
+ // targetElement.clientWidth is unaffected by the zoom, but if the target
+ // is the root content window, the distance from the window origin to the
+ // scrollbar in CSS pixels does decrease proportionally to the zoom,
+ // so the CSS coordinates we return need to be scaled accordingly.
+ if (targetIsTopWindow(aTarget)) {
+ var resolution = await getResolution();
+ startX /= resolution;
+ startY /= resolution;
+ }
+
+ return { x: startX, y: startY };
+}
+
+// Synthesizes events to drag |target|'s vertical scrollbar by the distance
+// specified, synthesizing a mousemove for each increment as specified.
+// Returns null if the element doesn't have a vertical scrollbar. Otherwise,
+// returns an async function that should be invoked after the mousemoves have been
+// processed by the widget code, to end the scrollbar drag. Mousemoves being
+// processed by the widget code can be detected by listening for the mousemove
+// events in the caller, or for some other event that is triggered by the
+// mousemove, such as the scroll event resulting from the scrollbar drag.
+// The aScaleFactor argument should be provided if the scrollframe has been
+// scaled by an enclosing CSS transform. (TODO: this is a workaround for the
+// fact that coordinatesRelativeToScreen is supposed to do this automatically
+// but it currently does not).
+// Note: helper_scrollbar_snap_bug1501062.html contains a copy of this code
+// with modifications. Fixes here should be copied there if appropriate.
+// |target| can be an element (for subframes) or a window (for root frames).
+async function promiseVerticalScrollbarDrag(
+ aTarget,
+ aDistance = 20,
+ aIncrement = 5,
+ aScaleFactor = 1
+) {
+ var startPoint = await scrollbarDragStart(aTarget, aScaleFactor);
+ var targetElement = elementForTarget(aTarget);
+ if (startPoint == null) {
+ return null;
+ }
+
+ dump(
+ "Starting drag at " +
+ startPoint.x +
+ ", " +
+ startPoint.y +
+ " from top-left of #" +
+ targetElement.id +
+ "\n"
+ );
+
+ // Move the mouse to the scrollbar thumb and drag it down
+ await promiseNativeMouseEventWithAPZ({
+ target: aTarget,
+ offsetX: startPoint.x,
+ offsetY: startPoint.y,
+ type: "mousemove",
+ });
+ // mouse down
+ await promiseNativeMouseEventWithAPZ({
+ target: aTarget,
+ offsetX: startPoint.x,
+ offsetY: startPoint.y,
+ type: "mousedown",
+ });
+ // drag vertically by |aIncrement| until we reach the specified distance
+ for (var y = aIncrement; y < aDistance; y += aIncrement) {
+ await promiseNativeMouseEventWithAPZ({
+ target: aTarget,
+ offsetX: startPoint.x,
+ offsetY: startPoint.y + y,
+ type: "mousemove",
+ });
+ }
+ await promiseNativeMouseEventWithAPZ({
+ target: aTarget,
+ offsetX: startPoint.x,
+ offsetY: startPoint.y + aDistance,
+ type: "mousemove",
+ });
+
+ // and return an async function to call afterwards to finish up the drag
+ return async function () {
+ dump("Finishing drag of #" + targetElement.id + "\n");
+ await promiseNativeMouseEventWithAPZ({
+ target: aTarget,
+ offsetX: startPoint.x,
+ offsetY: startPoint.y + aDistance,
+ type: "mouseup",
+ });
+ };
+}
+
+// This is similar to promiseVerticalScrollbarDrag except this triggers
+// the vertical scrollbar drag with a touch drag input. This function
+// returns true if a scrollbar was present and false if no scrollbar
+// was found for the given element.
+async function promiseVerticalScrollbarTouchDrag(
+ aTarget,
+ aDistance = 20,
+ aScaleFactor = 1
+) {
+ var startPoint = await scrollbarDragStart(aTarget, aScaleFactor);
+ var targetElement = elementForTarget(aTarget);
+ if (startPoint == null) {
+ return false;
+ }
+
+ dump(
+ "Starting touch drag at " +
+ startPoint.x +
+ ", " +
+ startPoint.y +
+ " from top-left of #" +
+ targetElement.id +
+ "\n"
+ );
+
+ await promiseNativeTouchDrag(
+ aTarget,
+ startPoint.x,
+ startPoint.y,
+ 0,
+ aDistance
+ );
+
+ return true;
+}
+
+// Synthesizes a native mouse drag, starting at offset (mouseX, mouseY) from
+// the given target. The drag occurs in the given number of steps, to a final
+// destination of (mouseX + distanceX, mouseY + distanceY) from the target.
+// Returns a promise (wrapped in a function, so it doesn't execute immediately)
+// that should be awaited after the mousemoves have been processed by the widget
+// code, to end the drag. This is important otherwise the OS can sometimes
+// reorder the events and the drag doesn't have the intended effect (see
+// bug 1368603).
+// Example usage:
+// let dragFinisher = await promiseNativeMouseDrag(myElement, 0, 0);
+// await myIndicationThatDragHadAnEffect;
+// await dragFinisher();
+async function promiseNativeMouseDrag(
+ target,
+ mouseX,
+ mouseY,
+ distanceX = 20,
+ distanceY = 20,
+ steps = 20
+) {
+ var targetElement = elementForTarget(target);
+ dump(
+ "Starting drag at " +
+ mouseX +
+ ", " +
+ mouseY +
+ " from top-left of #" +
+ targetElement.id +
+ "\n"
+ );
+
+ // Move the mouse to the target position
+ await promiseNativeMouseEventWithAPZ({
+ target,
+ offsetX: mouseX,
+ offsetY: mouseY,
+ type: "mousemove",
+ });
+ // mouse down
+ await promiseNativeMouseEventWithAPZ({
+ target,
+ offsetX: mouseX,
+ offsetY: mouseY,
+ type: "mousedown",
+ });
+ // drag vertically by |increment| until we reach the specified distance
+ for (var s = 1; s <= steps; s++) {
+ let dx = distanceX * (s / steps);
+ let dy = distanceY * (s / steps);
+ dump(`Dragging to ${mouseX + dx}, ${mouseY + dy} from target\n`);
+ await promiseNativeMouseEventWithAPZ({
+ target,
+ offsetX: mouseX + dx,
+ offsetY: mouseY + dy,
+ type: "mousemove",
+ });
+ }
+
+ // and return a function-wrapped promise to call afterwards to finish the drag
+ return function () {
+ return promiseNativeMouseEventWithAPZ({
+ target,
+ offsetX: mouseX + distanceX,
+ offsetY: mouseY + distanceY,
+ type: "mouseup",
+ });
+ };
+}
+
+// Synthesizes a native touch sequence of events corresponding to a pinch-zoom-in
+// at the given focus point. The focus point must be specified in CSS coordinates
+// relative to the document body.
+async function pinchZoomInTouchSequence(focusX, focusY) {
+ // prettier-ignore
+ var zoom_in = [
+ [ { x: focusX - 25, y: focusY - 50 }, { x: focusX + 25, y: focusY + 50 } ],
+ [ { x: focusX - 30, y: focusY - 80 }, { x: focusX + 30, y: focusY + 80 } ],
+ [ { x: focusX - 35, y: focusY - 110 }, { x: focusX + 40, y: focusY + 110 } ],
+ [ { x: focusX - 40, y: focusY - 140 }, { x: focusX + 45, y: focusY + 140 } ],
+ [ { x: focusX - 45, y: focusY - 170 }, { x: focusX + 50, y: focusY + 170 } ],
+ [ { x: focusX - 50, y: focusY - 200 }, { x: focusX + 55, y: focusY + 200 } ],
+ ];
+
+ var touchIds = [0, 1];
+ return synthesizeNativeTouchSequences(document.body, zoom_in, null, touchIds);
+}
+
+// Returns a promise that is resolved when the observer service dispatches a
+// message with the given topic.
+function promiseTopic(aTopic) {
+ return new Promise((resolve, reject) => {
+ SpecialPowers.Services.obs.addObserver(function observer(
+ subject,
+ topic,
+ data
+ ) {
+ try {
+ SpecialPowers.Services.obs.removeObserver(observer, topic);
+ resolve([subject, data]);
+ } catch (ex) {
+ SpecialPowers.Services.obs.removeObserver(observer, topic);
+ reject(ex);
+ }
+ },
+ aTopic);
+ });
+}
+
+// Returns a promise that is resolved when a APZ transform ends.
+function promiseTransformEnd() {
+ return promiseTopic("APZ:TransformEnd");
+}
+
+function promiseScrollend(aTarget = window) {
+ return promiseOneEvent(aTarget, "scrollend");
+}
+
+// Returns a promise that resolves after the indicated number
+// of touchend events have fired on the given target element.
+function promiseTouchEnd(element, count = 1) {
+ return new Promise(resolve => {
+ var eventCount = 0;
+ var counterFunction = function (e) {
+ eventCount++;
+ if (eventCount == count) {
+ element.removeEventListener("touchend", counterFunction, {
+ passive: true,
+ });
+ resolve();
+ }
+ };
+ element.addEventListener("touchend", counterFunction, { passive: true });
+ });
+}
+
+// This generates a touch-based pinch zoom-in gesture that is expected
+// to succeed. It returns after APZ has completed the zoom and reaches the end
+// of the transform. The focus point is expected to be in CSS coordinates
+// relative to the document body.
+async function pinchZoomInWithTouch(focusX, focusY) {
+ // Register the listener for the TransformEnd observer topic
+ let transformEndPromise = promiseTopic("APZ:TransformEnd");
+
+ // Dispatch all the touch events
+ await pinchZoomInTouchSequence(focusX, focusY);
+
+ // Wait for TransformEnd to fire.
+ await transformEndPromise;
+}
+// This generates a touchpad pinch zoom-in gesture that is expected
+// to succeed. It returns after APZ has completed the zoom and reaches the end
+// of the transform. The focus point is expected to be in CSS coordinates
+// relative to the document body.
+async function pinchZoomInWithTouchpad(focusX, focusY, options = {}) {
+ var zoomIn = [
+ 1.0, 1.019531, 1.035156, 1.037156, 1.039156, 1.054688, 1.056688, 1.070312,
+ 1.072312, 1.089844, 1.091844, 1.109375, 1.128906, 1.144531, 1.160156,
+ 1.175781, 1.191406, 1.207031, 1.222656, 1.234375, 1.246094, 1.261719,
+ 1.273438, 1.285156, 1.296875, 1.3125, 1.328125, 1.347656, 1.363281,
+ 1.382812, 1.402344, 1.421875, 1.0,
+ ];
+ await synthesizeTouchpadPinch(zoomIn, focusX, focusY, options);
+}
+
+async function pinchZoomInAndPanWithTouchpad(options = {}) {
+ var x = 584;
+ var y = 347;
+ var scalesAndFoci = [];
+ // Zoom
+ for (var scale = 1.0; scale <= 2.0; scale += 0.2) {
+ scalesAndFoci.push([scale, x, y]);
+ }
+ // Pan (due to a limitation of the current implementation, events
+ // for which the scale doesn't change are dropped, so vary the
+ // scale slightly as well).
+ for (var i = 1; i <= 20; i++) {
+ x -= 4;
+ y -= 5;
+ scalesAndFoci.push([scale + 0.01 * i, x, y]);
+ }
+ await synthesizeTouchpadGesture(scalesAndFoci, options);
+}
+
+async function pinchZoomOutWithTouchpad(focusX, focusY, options = {}) {
+ // The last item equal one to indicate scale end
+ var zoomOut = [
+ 1.0, 1.375, 1.359375, 1.339844, 1.316406, 1.296875, 1.277344, 1.257812,
+ 1.238281, 1.21875, 1.199219, 1.175781, 1.15625, 1.132812, 1.101562,
+ 1.078125, 1.054688, 1.03125, 1.011719, 0.992188, 0.972656, 0.953125,
+ 0.933594, 1.0,
+ ];
+ await synthesizeTouchpadPinch(zoomOut, focusX, focusY, options);
+}
+
+async function pinchZoomInOutWithTouchpad(focusX, focusY, options = {}) {
+ // Use the same scale for two events in a row to make sure the code handles this properly.
+ var zoomInOut = [
+ 1.0, 1.082031, 1.089844, 1.097656, 1.101562, 1.109375, 1.121094, 1.128906,
+ 1.128906, 1.125, 1.097656, 1.074219, 1.054688, 1.035156, 1.015625, 1.0, 1.0,
+ ];
+ await synthesizeTouchpadPinch(zoomInOut, focusX, focusY, options);
+}
+// This generates a touch-based pinch gesture that is expected to succeed
+// and trigger an APZ:TransformEnd observer notification.
+// It returns after that notification has been dispatched.
+// The coordinates of touch events in `touchSequence` are expected to be
+// in CSS coordinates relative to the document body.
+async function synthesizeNativeTouchAndWaitForTransformEnd(
+ touchSequence,
+ touchIds
+) {
+ // Register the listener for the TransformEnd observer topic
+ let transformEndPromise = promiseTopic("APZ:TransformEnd");
+
+ // Dispatch all the touch events
+ await synthesizeNativeTouchSequences(
+ document.body,
+ touchSequence,
+ null,
+ touchIds
+ );
+
+ // Wait for TransformEnd to fire.
+ await transformEndPromise;
+}
+
+// Returns a touch sequence for a pinch-zoom-out operation in the center
+// of the visual viewport. The touch sequence returned is in CSS coordinates
+// relative to the document body.
+function pinchZoomOutTouchSequenceAtCenter() {
+ // Divide the half of visual viewport size by 8, then cause touch events
+ // starting from the 7th furthest away from the center towards the center.
+ const deltaX = window.visualViewport.width / 16;
+ const deltaY = window.visualViewport.height / 16;
+ const centerX =
+ window.visualViewport.pageLeft + window.visualViewport.width / 2;
+ const centerY =
+ window.visualViewport.pageTop + window.visualViewport.height / 2;
+ // prettier-ignore
+ var zoom_out = [
+ [ { x: centerX - (deltaX * 6), y: centerY - (deltaY * 6) },
+ { x: centerX + (deltaX * 6), y: centerY + (deltaY * 6) } ],
+ [ { x: centerX - (deltaX * 5), y: centerY - (deltaY * 5) },
+ { x: centerX + (deltaX * 5), y: centerY + (deltaY * 5) } ],
+ [ { x: centerX - (deltaX * 4), y: centerY - (deltaY * 4) },
+ { x: centerX + (deltaX * 4), y: centerY + (deltaY * 4) } ],
+ [ { x: centerX - (deltaX * 3), y: centerY - (deltaY * 3) },
+ { x: centerX + (deltaX * 3), y: centerY + (deltaY * 3) } ],
+ [ { x: centerX - (deltaX * 2), y: centerY - (deltaY * 2) },
+ { x: centerX + (deltaX * 2), y: centerY + (deltaY * 2) } ],
+ [ { x: centerX - (deltaX * 1), y: centerY - (deltaY * 1) },
+ { x: centerX + (deltaX * 1), y: centerY + (deltaY * 1) } ],
+ ];
+ return zoom_out;
+}
+
+// This generates a touch-based pinch zoom-out gesture that is expected
+// to succeed. It returns after APZ has completed the zoom and reaches the end
+// of the transform. The touch inputs are directed to the center of the
+// current visual viewport.
+async function pinchZoomOutWithTouchAtCenter() {
+ var zoom_out = pinchZoomOutTouchSequenceAtCenter();
+ var touchIds = [0, 1];
+ await synthesizeNativeTouchAndWaitForTransformEnd(zoom_out, touchIds);
+}
+
+// useTouchpad is only currently implemented on macOS
+async function synthesizeDoubleTap(element, x, y, useTouchpad) {
+ if (useTouchpad) {
+ await synthesizeNativeTouchpadDoubleTap(element, x, y);
+ } else {
+ await synthesizeNativeTap(element, x, y);
+ await synthesizeNativeTap(element, x, y);
+ }
+}
+// useTouchpad is only currently implemented on macOS
+async function doubleTapOn(element, x, y, useTouchpad) {
+ let transformEndPromise = promiseTransformEnd();
+
+ await synthesizeDoubleTap(element, x, y, useTouchpad);
+
+ // Wait for the APZ:TransformEnd to fire
+ await transformEndPromise;
+
+ // Flush state so we can query an accurate resolution
+ await promiseApzFlushedRepaints();
+}
+
+const NativePanHandlerForLinux = {
+ beginPhase: SpecialPowers.DOMWindowUtils.PHASE_BEGIN,
+ updatePhase: SpecialPowers.DOMWindowUtils.PHASE_UPDATE,
+ endPhase: SpecialPowers.DOMWindowUtils.PHASE_END,
+ promiseNativePanEvent: promiseNativeTouchpadPanEventAndWaitForObserver,
+ delta: -50,
+};
+
+const NativePanHandlerForWindows = {
+ beginPhase: SpecialPowers.DOMWindowUtils.PHASE_BEGIN,
+ updatePhase: SpecialPowers.DOMWindowUtils.PHASE_UPDATE,
+ endPhase: SpecialPowers.DOMWindowUtils.PHASE_END,
+ promiseNativePanEvent: promiseNativeTouchpadPanEventAndWaitForObserver,
+ delta: 50,
+};
+
+const NativePanHandlerForMac = {
+ // From https://developer.apple.com/documentation/coregraphics/cgscrollphase/kcgscrollphasebegan?language=occ , etc.
+ beginPhase: 1, // kCGScrollPhaseBegan
+ updatePhase: 2, // kCGScrollPhaseChanged
+ endPhase: 4, // kCGScrollPhaseEnded
+ promiseNativePanEvent: promiseNativePanGestureEventAndWaitForObserver,
+ delta: -50,
+};
+
+const NativePanHandlerForHeadless = {
+ beginPhase: SpecialPowers.DOMWindowUtils.PHASE_BEGIN,
+ updatePhase: SpecialPowers.DOMWindowUtils.PHASE_UPDATE,
+ endPhase: SpecialPowers.DOMWindowUtils.PHASE_END,
+ promiseNativePanEvent: promiseNativeTouchpadPanEventAndWaitForObserver,
+ delta: 50,
+};
+
+function getPanHandler() {
+ if (SpecialPowers.isHeadless) {
+ return NativePanHandlerForHeadless;
+ }
+
+ switch (getPlatform()) {
+ case "linux":
+ return NativePanHandlerForLinux;
+ case "windows":
+ return NativePanHandlerForWindows;
+ case "mac":
+ return NativePanHandlerForMac;
+ default:
+ throw new Error(
+ "There's no native pan handler on platform " + getPlatform()
+ );
+ }
+}
+
+// Lazily get `NativePanHandler` to avoid an exception where we don't support
+// native pan events (e.g. Android).
+if (!window.hasOwnProperty("NativePanHandler")) {
+ Object.defineProperty(window, "NativePanHandler", {
+ get() {
+ return getPanHandler();
+ },
+ });
+}
+
+async function panRightToLeftBegin(aElement, aX, aY, aMultiplier) {
+ await NativePanHandler.promiseNativePanEvent(
+ aElement,
+ aX,
+ aY,
+ NativePanHandler.delta * aMultiplier,
+ 0,
+ NativePanHandler.beginPhase
+ );
+}
+
+async function panRightToLeftUpdate(aElement, aX, aY, aMultiplier) {
+ await NativePanHandler.promiseNativePanEvent(
+ aElement,
+ aX,
+ aY,
+ NativePanHandler.delta * aMultiplier,
+ 0,
+ NativePanHandler.updatePhase
+ );
+}
+
+async function panRightToLeftEnd(aElement, aX, aY, aMultiplier) {
+ await NativePanHandler.promiseNativePanEvent(
+ aElement,
+ aX,
+ aY,
+ 0,
+ 0,
+ NativePanHandler.endPhase
+ );
+}
+
+async function panRightToLeft(aElement, aX, aY, aMultiplier) {
+ await panRightToLeftBegin(aElement, aX, aY, aMultiplier);
+ await panRightToLeftUpdate(aElement, aX, aY, aMultiplier);
+ await panRightToLeftEnd(aElement, aX, aY, aMultiplier);
+}
+
+async function panLeftToRight(aElement, aX, aY, aMultiplier) {
+ await panLeftToRightBegin(aElement, aX, aY, aMultiplier);
+ await panLeftToRightUpdate(aElement, aX, aY, aMultiplier);
+ await panLeftToRightEnd(aElement, aX, aY, aMultiplier);
+}
+
+async function panLeftToRightBegin(aElement, aX, aY, aMultiplier) {
+ await NativePanHandler.promiseNativePanEvent(
+ aElement,
+ aX,
+ aY,
+ -NativePanHandler.delta * aMultiplier,
+ 0,
+ NativePanHandler.beginPhase
+ );
+}
+
+async function panLeftToRightUpdate(aElement, aX, aY, aMultiplier) {
+ await NativePanHandler.promiseNativePanEvent(
+ aElement,
+ aX,
+ aY,
+ -NativePanHandler.delta * aMultiplier,
+ 0,
+ NativePanHandler.updatePhase
+ );
+ await NativePanHandler.promiseNativePanEvent(
+ aElement,
+ aX,
+ aY,
+ -NativePanHandler.delta * aMultiplier,
+ 0,
+ NativePanHandler.updatePhase
+ );
+}
+
+async function panLeftToRightEnd(aElement, aX, aY, aMultiplier) {
+ await NativePanHandler.promiseNativePanEvent(
+ aElement,
+ aX,
+ aY,
+ 0,
+ 0,
+ NativePanHandler.endPhase
+ );
+}
diff --git a/gfx/layers/apz/test/mochitest/apz_test_utils.js b/gfx/layers/apz/test/mochitest/apz_test_utils.js
new file mode 100644
index 0000000000..1004f8a3d5
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/apz_test_utils.js
@@ -0,0 +1,1287 @@
+// Utilities for writing APZ tests using the framework added in bug 961289
+
+// ----------------------------------------------------------------------
+// Functions that convert the APZ test data into a more usable form.
+// Every place we have a WebIDL sequence whose elements are dictionaries
+// with two elements, a key, and a value, we convert this into a JS
+// object with a property for each key/value pair. (This is the structure
+// we really want, but we can't express in directly in WebIDL.)
+// ----------------------------------------------------------------------
+
+// getHitTestConfig() expects apz_test_native_event_utils.js to be loaded as well.
+/* import-globals-from apz_test_native_event_utils.js */
+
+function convertEntries(entries) {
+ var result = {};
+ for (var i = 0; i < entries.length; ++i) {
+ result[entries[i].key] = entries[i].value;
+ }
+ return result;
+}
+
+function parsePoint(str) {
+ var pieces = str.replace(/[()\s]+/g, "").split(",");
+ SimpleTest.is(pieces.length, 2, "expected string of form (x,y)");
+ for (var i = 0; i < 2; i++) {
+ var eq = pieces[i].indexOf("=");
+ if (eq >= 0) {
+ pieces[i] = pieces[i].substring(eq + 1);
+ }
+ }
+ return {
+ x: parseInt(pieces[0]),
+ y: parseInt(pieces[1]),
+ };
+}
+
+// Given a VisualViewport object, return the visual viewport
+// rect relative to the page.
+function getVisualViewportRect(vv) {
+ return {
+ x: vv.pageLeft,
+ y: vv.pageTop,
+ width: vv.width,
+ height: vv.height,
+ };
+}
+
+// Return the offset of the visual viewport relative to the layout viewport.
+function getRelativeViewportOffset(window) {
+ const offsetX = {};
+ const offsetY = {};
+ const utils = SpecialPowers.getDOMWindowUtils(window);
+ utils.getVisualViewportOffsetRelativeToLayoutViewport(offsetX, offsetY);
+ return {
+ x: offsetX.value,
+ y: offsetY.value,
+ };
+}
+
+function parseRect(str) {
+ var pieces = str.replace(/[()\s]+/g, "").split(",");
+ SimpleTest.is(pieces.length, 4, "expected string of form (x,y,w,h)");
+ for (var i = 0; i < 4; i++) {
+ var eq = pieces[i].indexOf("=");
+ if (eq >= 0) {
+ pieces[i] = pieces[i].substring(eq + 1);
+ }
+ }
+ return {
+ x: parseInt(pieces[0]),
+ y: parseInt(pieces[1]),
+ width: parseInt(pieces[2]),
+ height: parseInt(pieces[3]),
+ };
+}
+
+// These functions expect rects with fields named x/y/width/height, such as
+// that returned by parseRect().
+function rectContains(haystack, needle) {
+ return (
+ haystack.x <= needle.x &&
+ haystack.y <= needle.y &&
+ haystack.x + haystack.width >= needle.x + needle.width &&
+ haystack.y + haystack.height >= needle.y + needle.height
+ );
+}
+function rectToString(rect) {
+ return (
+ "(" + rect.x + "," + rect.y + "," + rect.width + "," + rect.height + ")"
+ );
+}
+function assertRectContainment(
+ haystackRect,
+ haystackDesc,
+ needleRect,
+ needleDesc
+) {
+ SimpleTest.ok(
+ rectContains(haystackRect, needleRect),
+ haystackDesc +
+ " " +
+ rectToString(haystackRect) +
+ " should contain " +
+ needleDesc +
+ " " +
+ rectToString(needleRect)
+ );
+}
+
+function getPropertyAsRect(scrollFrames, scrollId, prop) {
+ SimpleTest.ok(
+ scrollId in scrollFrames,
+ "expected scroll frame data for scroll id " + scrollId
+ );
+ var scrollFrameData = scrollFrames[scrollId];
+ SimpleTest.ok(
+ "displayport" in scrollFrameData,
+ "expected a " + prop + " for scroll id " + scrollId
+ );
+ var value = scrollFrameData[prop];
+ return parseRect(value);
+}
+
+function convertScrollFrameData(scrollFrames) {
+ var result = {};
+ for (var i = 0; i < scrollFrames.length; ++i) {
+ result[scrollFrames[i].scrollId] = convertEntries(scrollFrames[i].entries);
+ }
+ return result;
+}
+
+function convertBuckets(buckets) {
+ var result = {};
+ for (var i = 0; i < buckets.length; ++i) {
+ result[buckets[i].sequenceNumber] = convertScrollFrameData(
+ buckets[i].scrollFrames
+ );
+ }
+ return result;
+}
+
+function convertTestData(testData) {
+ var result = {};
+ result.paints = convertBuckets(testData.paints);
+ result.repaintRequests = convertBuckets(testData.repaintRequests);
+ return result;
+}
+
+// Returns the last bucket that has at least one scrollframe. This
+// is useful for skipping over buckets that are from empty transactions,
+// because those don't contain any useful data.
+function getLastNonemptyBucket(buckets) {
+ for (var i = buckets.length - 1; i >= 0; --i) {
+ if (buckets[i].scrollFrames.length) {
+ return buckets[i];
+ }
+ }
+ return null;
+}
+
+// Takes something like "matrix(1, 0, 0, 1, 234.024, 528.29023)"" and returns a number array
+function parseTransform(transform) {
+ return /matrix\((.*),(.*),(.*),(.*),(.*),(.*)\)/
+ .exec(transform)
+ .slice(1)
+ .map(parseFloat);
+}
+
+function isTransformClose(a, b, name) {
+ is(
+ a.length,
+ b.length,
+ `expected transforms ${a} and ${b} to be the same length`
+ );
+ for (let i = 0; i < a.length; i++) {
+ ok(Math.abs(a[i] - b[i]) < 0.01, name);
+ }
+}
+
+// Given APZ test data for a single paint on the compositor side,
+// reconstruct the APZC tree structure from the 'parentScrollId'
+// entries that were logged. More specifically, the subset of the
+// APZC tree structure corresponding to the layer subtree for the
+// content process that triggered the paint, is reconstructed (as
+// the APZ test data only contains information abot this subtree).
+function buildApzcTree(paint) {
+ // The APZC tree can potentially have multiple root nodes,
+ // so we invent a node that is the parent of all roots.
+ // This 'root' does not correspond to an APZC.
+ var root = { scrollId: -1, children: [] };
+ for (let scrollId in paint) {
+ paint[scrollId].children = [];
+ paint[scrollId].scrollId = scrollId;
+ }
+ for (let scrollId in paint) {
+ var parentNode = null;
+ if ("hasNoParentWithSameLayersId" in paint[scrollId]) {
+ parentNode = root;
+ } else if ("parentScrollId" in paint[scrollId]) {
+ parentNode = paint[paint[scrollId].parentScrollId];
+ }
+ parentNode.children.push(paint[scrollId]);
+ }
+ return root;
+}
+
+// Given an APZC tree produced by buildApzcTree, return the RCD node in
+// the tree, or null if there was none.
+function findRcdNode(apzcTree) {
+ // isRootContent will be undefined or "1"
+ if (apzcTree.isRootContent) {
+ return apzcTree;
+ }
+ for (var i = 0; i < apzcTree.children.length; i++) {
+ var rcd = findRcdNode(apzcTree.children[i]);
+ if (rcd != null) {
+ return rcd;
+ }
+ }
+ return null;
+}
+
+// Return whether an element whose id includes |elementId| has been layerized.
+// Assumes |elementId| will be present in the content description for the
+// element, and not in the content descriptions of other elements.
+function isLayerized(elementId) {
+ var contentTestData =
+ SpecialPowers.getDOMWindowUtils(window).getContentAPZTestData();
+ var nonEmptyBucket = getLastNonemptyBucket(contentTestData.paints);
+ ok(nonEmptyBucket != null, "expected at least one nonempty paint");
+ var seqno = nonEmptyBucket.sequenceNumber;
+ contentTestData = convertTestData(contentTestData);
+ var paint = contentTestData.paints[seqno];
+ for (var scrollId in paint) {
+ if ("contentDescription" in paint[scrollId]) {
+ if (paint[scrollId].contentDescription.includes(elementId)) {
+ return true;
+ }
+ }
+ }
+ return false;
+}
+
+// Return a rect (or null) that holds the last known content-side displayport
+// for a given element. (The element selection works the same way, and with
+// the same assumptions as the isLayerized function above).
+function getLastContentDisplayportFor(elementId, expectPainted = true) {
+ var contentTestData =
+ SpecialPowers.getDOMWindowUtils(window).getContentAPZTestData();
+ if (contentTestData == undefined) {
+ ok(!expectPainted, "expected to have apz test data (1)");
+ return null;
+ }
+ var nonEmptyBucket = getLastNonemptyBucket(contentTestData.paints);
+ if (nonEmptyBucket == null) {
+ ok(!expectPainted, "expected to have apz test data (2)");
+ return null;
+ }
+ var seqno = nonEmptyBucket.sequenceNumber;
+ contentTestData = convertTestData(contentTestData);
+ var paint = contentTestData.paints[seqno];
+ for (var scrollId in paint) {
+ if ("contentDescription" in paint[scrollId]) {
+ if (paint[scrollId].contentDescription.includes(elementId)) {
+ if ("displayport" in paint[scrollId]) {
+ return parseRect(paint[scrollId].displayport);
+ }
+ }
+ }
+ }
+ return null;
+}
+
+// Return the APZC tree (as produced by buildApzcTree) for the last
+// non-empty paint received by the compositor.
+function getLastApzcTree() {
+ let data = SpecialPowers.getDOMWindowUtils(window).getCompositorAPZTestData();
+ if (data == undefined) {
+ ok(false, "expected to have compositor apz test data");
+ return null;
+ }
+ if (!data.paints.length) {
+ ok(false, "expected to have at least one compositor paint bucket");
+ return null;
+ }
+ var seqno = data.paints[data.paints.length - 1].sequenceNumber;
+ data = convertTestData(data);
+ return buildApzcTree(data.paints[seqno]);
+}
+
+// Return a promise that is resolved on the next rAF callback
+function promiseFrame(aWindow = window) {
+ return new Promise(resolve => {
+ aWindow.requestAnimationFrame(resolve);
+ });
+}
+
+// Return a promise that is resolved on the next MozAfterPaint event
+function promiseAfterPaint() {
+ return new Promise(resolve => {
+ window.addEventListener("MozAfterPaint", resolve, { once: true });
+ });
+}
+
+// This waits until any pending events on the APZ controller thread are
+// processed, and any resulting repaint requests are received by the main
+// thread. Note that while the repaint requests do get processed by the
+// APZ handler on the main thread, the repaints themselves may not have
+// occurred by the the returned promise resolves. If you want to wait
+// for those repaints, consider using promiseApzFlushedRepaints instead.
+function promiseOnlyApzControllerFlushedWithoutSetTimeout(aWindow = window) {
+ return new Promise(function (resolve, reject) {
+ var repaintDone = function () {
+ dump("PromiseApzRepaintsFlushed: APZ flush done\n");
+ SpecialPowers.Services.obs.removeObserver(
+ repaintDone,
+ "apz-repaints-flushed"
+ );
+ resolve();
+ };
+ SpecialPowers.Services.obs.addObserver(repaintDone, "apz-repaints-flushed");
+ if (SpecialPowers.getDOMWindowUtils(aWindow).flushApzRepaints()) {
+ dump(
+ "PromiseApzRepaintsFlushed: Flushed APZ repaints, waiting for callback...\n"
+ );
+ } else {
+ dump(
+ "PromiseApzRepaintsFlushed: Flushing APZ repaints was a no-op, triggering callback directly...\n"
+ );
+ repaintDone();
+ }
+ });
+}
+
+// Another variant of the above promiseOnlyApzControllerFlushedWithoutSetTimeout
+// but with a setTimeout(0) callback.
+function promiseOnlyApzControllerFlushed(aWindow = window) {
+ return new Promise(resolve => {
+ promiseOnlyApzControllerFlushedWithoutSetTimeout(aWindow).then(() => {
+ setTimeout(resolve, 0);
+ });
+ });
+}
+
+// Flush repaints, APZ pending repaints, and any repaints resulting from that
+// flush. This is particularly useful if the test needs to reach some sort of
+// "idle" state in terms of repaints. Usually just waiting for all paints
+// followed by flushApzRepaints is sufficient to flush all APZ state back to
+// the main thread, but it can leave a paint scheduled which will get triggered
+// at some later time. For tests that specifically test for painting at
+// specific times, this method is the way to go. Even if in doubt, this is the
+// preferred method as the extra step is "safe" and shouldn't interfere with
+// most tests.
+async function promiseApzFlushedRepaints() {
+ await promiseAllPaintsDone();
+ await promiseOnlyApzControllerFlushed();
+ await promiseAllPaintsDone();
+}
+
+// This function takes a set of subtests to run one at a time in new top-level
+// windows, and returns a Promise that is resolved once all the subtests are
+// done running.
+//
+// The aSubtests array is an array of objects with the following keys:
+// file: required, the filename of the subtest.
+// prefs: optional, an array of arrays containing key-value prefs to set.
+// dp_suppression: optional, a boolean on whether or not to respect displayport
+// suppression during the test.
+// onload: optional, a function that will be registered as a load event listener
+// for the child window that will hold the subtest. the function will be
+// passed exactly one argument, which will be the child window.
+// An example of an array is:
+// aSubtests = [
+// { 'file': 'test_file_name.html' },
+// { 'file': 'test_file_2.html', 'prefs': [['pref.name', true], ['other.pref', 1000]], 'dp_suppression': false }
+// { 'file': 'file_3.html', 'onload': function(w) { w.subtestDone(); } }
+// ];
+//
+// Each subtest should call one of the subtestDone() or subtestFailed()
+// functions when it is done, to indicate that the window should be torn
+// down and the next test should run.
+// These functions are injected into the subtest's window by this
+// function prior to loading the subtest. For convenience, the |is| and |ok|
+// functions provided by SimpleTest are also mapped into the subtest's window.
+// For other things from the parent, the subtest can use window.opener.<whatever>
+// to access objects.
+function runSubtestsSeriallyInFreshWindows(aSubtests) {
+ return new Promise(function (resolve, reject) {
+ var testIndex = -1;
+ var w = null;
+
+ // If the "apz.subtest" pref has been set, only a single subtest whose name matches
+ // the pref's value (if any) will be run.
+ var onlyOneSubtest = SpecialPowers.getCharPref(
+ "apz.subtest",
+ /* default = */ ""
+ );
+
+ function advanceSubtestExecutionWithFailure(msg) {
+ SimpleTest.ok(false, msg);
+ advanceSubtestExecution();
+ }
+
+ async function advanceSubtestExecution() {
+ var test = aSubtests[testIndex];
+ if (w) {
+ // Run any cleanup functions registered in the subtest
+ // Guard against the subtest not loading apz_test_utils.js
+ if (w.ApzCleanup) {
+ w.ApzCleanup.execute();
+ }
+ if (typeof test.dp_suppression != "undefined") {
+ // We modified the suppression when starting the test, so now undo that.
+ SpecialPowers.getDOMWindowUtils(window).respectDisplayPortSuppression(
+ !test.dp_suppression
+ );
+ }
+
+ if (test.prefs) {
+ // We pushed some prefs for this test, pop them, and re-invoke
+ // advanceSubtestExecution() after that's been processed
+ SpecialPowers.popPrefEnv(function () {
+ w.close();
+ w = null;
+ advanceSubtestExecution();
+ });
+ return;
+ }
+
+ w.close();
+ }
+
+ testIndex++;
+ if (testIndex >= aSubtests.length) {
+ resolve();
+ return;
+ }
+
+ await SimpleTest.promiseFocus(window);
+
+ test = aSubtests[testIndex];
+
+ let recognizedProps = ["file", "prefs", "dp_suppression", "onload"];
+ for (let prop in test) {
+ if (!recognizedProps.includes(prop)) {
+ SimpleTest.ok(
+ false,
+ "Subtest " + test.file + " has unrecognized property '" + prop + "'"
+ );
+ setTimeout(function () {
+ advanceSubtestExecution();
+ }, 0);
+ return;
+ }
+ }
+
+ if (onlyOneSubtest && onlyOneSubtest != test.file) {
+ SimpleTest.ok(
+ true,
+ "Skipping " +
+ test.file +
+ " because only " +
+ onlyOneSubtest +
+ " is being run"
+ );
+ setTimeout(function () {
+ advanceSubtestExecution();
+ }, 0);
+ return;
+ }
+
+ SimpleTest.ok(true, "Starting subtest " + test.file);
+
+ if (typeof test.dp_suppression != "undefined") {
+ // Normally during a test, the displayport will get suppressed during page
+ // load, and unsuppressed at a non-deterministic time during the test. The
+ // unsuppression can trigger a repaint which interferes with the test, so
+ // to avoid that we can force the displayport to be unsuppressed for the
+ // entire test which is more deterministic.
+ SpecialPowers.getDOMWindowUtils(window).respectDisplayPortSuppression(
+ test.dp_suppression
+ );
+ }
+
+ function spawnTest(aFile) {
+ w = window.open("", "_blank");
+ w.subtestDone = advanceSubtestExecution;
+ w.subtestFailed = advanceSubtestExecutionWithFailure;
+ w.isApzSubtest = true;
+ w.SimpleTest = SimpleTest;
+ w.dump = function (msg) {
+ return dump(aFile + " | " + msg);
+ };
+ w.info = function (msg) {
+ return info(aFile + " | " + msg);
+ };
+ w.is = function (a, b, msg) {
+ return is(a, b, aFile + " | " + msg);
+ };
+ w.isnot = function (a, b, msg) {
+ return isnot(a, b, aFile + " | " + msg);
+ };
+ w.isfuzzy = function (a, b, eps, msg) {
+ return isfuzzy(a, b, eps, aFile + " | " + msg);
+ };
+ w.ok = function (cond, msg) {
+ arguments[1] = aFile + " | " + msg;
+ // Forward all arguments to SimpleTest.ok where we will check that ok() was
+ // called with at most 2 arguments.
+ return SimpleTest.ok.apply(SimpleTest, arguments);
+ };
+ w.todo_is = function (a, b, msg) {
+ return todo_is(a, b, aFile + " | " + msg);
+ };
+ w.todo = function (cond, msg) {
+ return todo(cond, aFile + " | " + msg);
+ };
+ if (test.onload) {
+ w.addEventListener(
+ "load",
+ function (e) {
+ test.onload(w);
+ },
+ { once: true }
+ );
+ }
+ var subtestUrl =
+ location.href.substring(0, location.href.lastIndexOf("/") + 1) +
+ aFile;
+ function urlResolves(url) {
+ var request = new XMLHttpRequest();
+ request.open("GET", url, false);
+ request.send();
+ return request.status !== 404;
+ }
+ if (!urlResolves(subtestUrl)) {
+ SimpleTest.ok(
+ false,
+ "Subtest URL " +
+ subtestUrl +
+ " does not resolve. " +
+ "Be sure it's present in the support-files section of mochitest.ini."
+ );
+ reject();
+ return undefined;
+ }
+ w.location = subtestUrl;
+ return w;
+ }
+
+ if (test.prefs) {
+ // Got some prefs for this subtest, push them
+ await SpecialPowers.pushPrefEnv({ set: test.prefs });
+ }
+ w = spawnTest(test.file);
+ }
+
+ advanceSubtestExecution();
+ }).catch(function (e) {
+ SimpleTest.ok(false, "Error occurred while running subtests: " + e);
+ });
+}
+
+function pushPrefs(prefs) {
+ return SpecialPowers.pushPrefEnv({ set: prefs });
+}
+
+async function waitUntilApzStable() {
+ if (!SpecialPowers.isMainProcess()) {
+ // We use this waitUntilApzStable function during test initialization
+ // and for those scenarios we want to flush the parent-process layer
+ // tree to the compositor and wait for that as well. That way we know
+ // that not only is the content-process layer tree ready in the compositor,
+ // the parent-process layer tree in the compositor has the appropriate
+ // RefLayer pointing to the content-process layer tree.
+
+ // Sadly this helper function cannot reuse any code from other places because
+ // it must be totally self-contained to be shipped over to the parent process.
+ function parentProcessFlush() {
+ /* eslint-env mozilla/chrome-script */
+ function apzFlush() {
+ var topWin = Services.wm.getMostRecentWindow("navigator:browser");
+ if (!topWin) {
+ topWin = Services.wm.getMostRecentWindow("navigator:geckoview");
+ }
+ var topUtils = topWin.windowUtils;
+
+ var repaintDone = function () {
+ dump("WaitUntilApzStable: APZ flush done in parent proc\n");
+ Services.obs.removeObserver(repaintDone, "apz-repaints-flushed");
+ // send message back to content process
+ sendAsyncMessage("apz-flush-done", null);
+ };
+ var flushRepaint = function () {
+ if (topUtils.isMozAfterPaintPending) {
+ topWin.addEventListener("MozAfterPaint", flushRepaint, {
+ once: true,
+ });
+ return;
+ }
+
+ Services.obs.addObserver(repaintDone, "apz-repaints-flushed");
+ if (topUtils.flushApzRepaints()) {
+ dump(
+ "WaitUntilApzStable: flushed APZ repaints in parent proc, waiting for callback...\n"
+ );
+ } else {
+ dump(
+ "WaitUntilApzStable: flushing APZ repaints in parent proc was a no-op, triggering callback directly...\n"
+ );
+ repaintDone();
+ }
+ };
+
+ // Flush APZ repaints, but wait until all the pending paints have been
+ // sent.
+ flushRepaint();
+ }
+ function cleanup() {
+ removeMessageListener("apz-flush", apzFlush);
+ removeMessageListener("cleanup", cleanup);
+ }
+ addMessageListener("apz-flush", apzFlush);
+ addMessageListener("cleanup", cleanup);
+ }
+
+ // This is the first time waitUntilApzStable is being called, do initialization
+ if (typeof waitUntilApzStable.chromeHelper == "undefined") {
+ waitUntilApzStable.chromeHelper =
+ SpecialPowers.loadChromeScript(parentProcessFlush);
+ ApzCleanup.register(() => {
+ waitUntilApzStable.chromeHelper.sendAsyncMessage("cleanup", null);
+ waitUntilApzStable.chromeHelper.destroy();
+ delete waitUntilApzStable.chromeHelper;
+ });
+ }
+
+ // Actually trigger the parent-process flush and wait for it to finish
+ waitUntilApzStable.chromeHelper.sendAsyncMessage("apz-flush", null);
+ await waitUntilApzStable.chromeHelper.promiseOneMessage("apz-flush-done");
+ dump("WaitUntilApzStable: got apz-flush-done in child proc\n");
+ }
+
+ await SimpleTest.promiseFocus(window);
+ dump("WaitUntilApzStable: done promiseFocus\n");
+ await promiseAllPaintsDone();
+ dump("WaitUntilApzStable: done promiseAllPaintsDone\n");
+ await promiseOnlyApzControllerFlushed();
+ dump("WaitUntilApzStable: all done\n");
+}
+
+// This function returns a promise that is resolved after at least one paint
+// has been sent and processed by the compositor. This function can force
+// such a paint to happen if none are pending. This is useful to run after
+// the waitUntilApzStable() but before reading the compositor-side APZ test
+// data, because the test data for the content layers id only gets populated
+// on content layer tree updates *after* the root layer tree has a RefLayer
+// pointing to the contnet layer tree. waitUntilApzStable itself guarantees
+// that the root layer tree is pointing to the content layer tree, but does
+// not guarantee the subsequent paint; this function does that job.
+async function forceLayerTreeToCompositor() {
+ // Modify a style property to force a layout flush
+ document.body.style.boxSizing = "border-box";
+ var utils = SpecialPowers.getDOMWindowUtils(window);
+ if (!utils.isMozAfterPaintPending) {
+ dump("Forcing a paint since none was pending already...\n");
+ var testMode = utils.isTestControllingRefreshes;
+ utils.advanceTimeAndRefresh(0);
+ if (!testMode) {
+ utils.restoreNormalRefresh();
+ }
+ }
+ await promiseAllPaintsDone(null, true);
+ await promiseOnlyApzControllerFlushed();
+}
+
+function isApzEnabled() {
+ var enabled = SpecialPowers.getDOMWindowUtils(window).asyncPanZoomEnabled;
+ if (!enabled) {
+ // All tests are required to have at least one assertion. Since APZ is
+ // disabled, and the main test is presumably not going to run, we stick in
+ // a dummy assertion here to keep the test passing.
+ SimpleTest.ok(true, "APZ is not enabled; this test will be skipped");
+ }
+ return enabled;
+}
+
+function isKeyApzEnabled() {
+ return isApzEnabled() && SpecialPowers.getBoolPref("apz.keyboard.enabled");
+}
+
+// Take a snapshot of the given rect, *including compositor transforms* (i.e.
+// includes async scroll transforms applied by APZ). If you don't need the
+// compositor transforms, you can probably get away with using
+// SpecialPowers.snapshotWindowWithOptions or one of the friendlier wrappers.
+// The rect provided is expected to be relative to the screen, for example as
+// returned by rectRelativeToScreen in apz_test_native_event_utils.js.
+// Example usage:
+// var snapshot = getSnapshot(rectRelativeToScreen(myDiv));
+// which will take a snapshot of the 'myDiv' element. Note that if part of the
+// element is obscured by other things on top, the snapshot will include those
+// things. If it is clipped by a scroll container, the snapshot will include
+// that area anyway, so you will probably get parts of the scroll container in
+// the snapshot. If the rect extends outside the browser window then the
+// results are undefined.
+// The snapshot is returned in the form of a data URL.
+function getSnapshot(rect) {
+ function parentProcessSnapshot() {
+ /* eslint-env mozilla/chrome-script */
+ addMessageListener("snapshot", function (parentRect) {
+ var topWin = Services.wm.getMostRecentWindow("navigator:browser");
+ if (!topWin) {
+ topWin = Services.wm.getMostRecentWindow("navigator:geckoview");
+ }
+
+ // reposition the rect relative to the top-level browser window
+ parentRect = JSON.parse(parentRect);
+ parentRect.x -= topWin.mozInnerScreenX;
+ parentRect.y -= topWin.mozInnerScreenY;
+
+ // take the snapshot
+ var canvas = topWin.document.createElementNS(
+ "http://www.w3.org/1999/xhtml",
+ "canvas"
+ );
+ canvas.width = parentRect.width;
+ canvas.height = parentRect.height;
+ var ctx = canvas.getContext("2d");
+ ctx.drawWindow(
+ topWin,
+ parentRect.x,
+ parentRect.y,
+ parentRect.width,
+ parentRect.height,
+ "rgb(255,255,255)",
+ ctx.DRAWWINDOW_DRAW_VIEW |
+ ctx.DRAWWINDOW_USE_WIDGET_LAYERS |
+ ctx.DRAWWINDOW_DRAW_CARET
+ );
+ return canvas.toDataURL();
+ });
+ }
+
+ if (typeof getSnapshot.chromeHelper == "undefined") {
+ // This is the first time getSnapshot is being called; do initialization
+ getSnapshot.chromeHelper = SpecialPowers.loadChromeScript(
+ parentProcessSnapshot
+ );
+ ApzCleanup.register(function () {
+ getSnapshot.chromeHelper.destroy();
+ });
+ }
+
+ return getSnapshot.chromeHelper.sendQuery("snapshot", JSON.stringify(rect));
+}
+
+// Takes the document's query string and parses it, assuming the query string
+// is composed of key-value pairs where the value is in JSON format. The object
+// returned contains the various values indexed by their respective keys. In
+// case of duplicate keys, the last value be used.
+// Examples:
+// ?key="value"&key2=false&key3=500
+// produces { "key": "value", "key2": false, "key3": 500 }
+// ?key={"x":0,"y":50}&key2=[1,2,true]
+// produces { "key": { "x": 0, "y": 0 }, "key2": [1, 2, true] }
+function getQueryArgs() {
+ var args = {};
+ if (location.search.length) {
+ var params = location.search.substr(1).split("&");
+ for (var p of params) {
+ var [k, v] = p.split("=");
+ args[k] = JSON.parse(v);
+ }
+ }
+ return args;
+}
+
+// An async function that inserts a script element with the given URI into
+// the head of the document of the given window. This function returns when
+// the load or error event fires on the script element, indicating completion.
+async function injectScript(aScript, aWindow = window) {
+ var e = aWindow.document.createElement("script");
+ e.type = "text/javascript";
+ let loadPromise = new Promise((resolve, reject) => {
+ e.onload = function () {
+ resolve();
+ };
+ e.onerror = function () {
+ dump("Script [" + aScript + "] errored out\n");
+ reject();
+ };
+ });
+ e.src = aScript;
+ aWindow.document.getElementsByTagName("head")[0].appendChild(e);
+ await loadPromise;
+}
+
+// Compute some configuration information used for hit testing.
+// The computed information is cached to avoid recomputing it
+// each time this function is called.
+// The computed information is an object with three fields:
+// utils: the nsIDOMWindowUtils instance for this window
+// isWindow: true if the platform is Windows
+// activateAllScrollFrames: true if prefs indicate all scroll frames are
+// activated with at least a minimal display port
+function getHitTestConfig() {
+ if (!("hitTestConfig" in window)) {
+ var utils = SpecialPowers.getDOMWindowUtils(window);
+ var isWindows = getPlatform() == "windows";
+ let activateAllScrollFrames =
+ SpecialPowers.getBoolPref("apz.wr.activate_all_scroll_frames") ||
+ (SpecialPowers.getBoolPref(
+ "apz.wr.activate_all_scroll_frames_when_fission"
+ ) &&
+ SpecialPowers.Services.appinfo.fissionAutostart);
+
+ window.hitTestConfig = {
+ utils,
+ isWindows,
+ activateAllScrollFrames,
+ };
+ }
+ return window.hitTestConfig;
+}
+
+// Compute the coordinates of the center of the given element. The argument
+// can either be a string (the id of the element desired) or the element
+// itself.
+function centerOf(element) {
+ if (typeof element === "string") {
+ element = document.getElementById(element);
+ }
+ var bounds = element.getBoundingClientRect();
+ return { x: bounds.x + bounds.width / 2, y: bounds.y + bounds.height / 2 };
+}
+
+// Peform a compositor hit test at the given point and return the result.
+// |point| is expected to be in CSS coordinates relative to the layout
+// viewport, since this is what sendMouseEvent() expects. (Note that this
+// is different from sendNativeMouseEvent() which expects screen coordinates
+// relative to the screen.)
+// The returned object has two fields:
+// hitInfo: a combination of APZHitResultFlags
+// scrollId: the view-id of the scroll frame that was hit
+function hitTest(point) {
+ var utils = getHitTestConfig().utils;
+ dump("Hit-testing point (" + point.x + ", " + point.y + ")\n");
+ utils.sendMouseEvent(
+ "MozMouseHittest",
+ point.x,
+ point.y,
+ 0,
+ 0,
+ 0,
+ true,
+ 0,
+ 0,
+ true,
+ true
+ );
+ var data = utils.getCompositorAPZTestData();
+ ok(
+ data.hitResults.length >= 1,
+ "Expected at least one hit result in the APZTestData"
+ );
+ var result = data.hitResults[data.hitResults.length - 1];
+ return {
+ hitInfo: result.hitResult,
+ scrollId: result.scrollId,
+ layersId: result.layersId,
+ };
+}
+
+// Returns a canonical stringification of the hitInfo bitfield.
+function hitInfoToString(hitInfo) {
+ var strs = [];
+ for (var flag in APZHitResultFlags) {
+ if ((hitInfo & APZHitResultFlags[flag]) != 0) {
+ strs.push(flag);
+ }
+ }
+ if (!strs.length) {
+ return "INVISIBLE";
+ }
+ strs.sort(function (a, b) {
+ return APZHitResultFlags[a] - APZHitResultFlags[b];
+ });
+ return strs.join(" | ");
+}
+
+// Takes an object returned by hitTest, along with the expected values, and
+// asserts that they match. Notably, it uses hitInfoToString to provide a
+// more useful message for the case that the hit info doesn't match
+function checkHitResult(
+ hitResult,
+ expectedHitInfo,
+ expectedScrollId,
+ expectedLayersId,
+ desc
+) {
+ is(
+ hitInfoToString(hitResult.hitInfo),
+ hitInfoToString(expectedHitInfo),
+ desc + " hit info"
+ );
+ is(hitResult.scrollId, expectedScrollId, desc + " scrollid");
+ is(hitResult.layersId, expectedLayersId, desc + " layersid");
+}
+
+// Symbolic constants used by hitTestScrollbar().
+var ScrollbarTrackLocation = {
+ START: 1,
+ END: 2,
+};
+var LayerState = {
+ ACTIVE: 1,
+ INACTIVE: 2,
+};
+
+// Perform a hit test on the scrollbar(s) of a scroll frame.
+// This function takes a single argument which is expected to be
+// an object with the following fields:
+// element: The scroll frame to perform the hit test on.
+// directions: The direction(s) of scrollbars to test.
+// If directions.vertical is true, the vertical scrollbar will be tested.
+// If directions.horizontal is true, the horizontal scrollbar will be tested.
+// Both may be true in a single call (in which case two tests are performed).
+// expectedScrollId: The scroll id that is expected to be hit, if activateAllScrollFrames is false.
+// expectedLayersId: The layers id that is expected to be hit.
+// trackLocation: One of ScrollbarTrackLocation.{START, END}.
+// Determines which end of the scrollbar track is targeted.
+// expectThumb: Whether the scrollbar thumb is expected to be present
+// at the targeted end of the scrollbar track.
+// layerState: Whether the scroll frame is active or inactive.
+// The function performs the hit tests and asserts that the returned
+// hit test information is consistent with the passed parameters.
+// There is no return value.
+// Tests that use this function must set the pref
+// "layout.scrollbars.always-layerize-track".
+function hitTestScrollbar(params) {
+ var config = getHitTestConfig();
+
+ var elem = params.element;
+
+ var boundingClientRect = elem.getBoundingClientRect();
+
+ var verticalScrollbarWidth = boundingClientRect.width - elem.clientWidth;
+ var horizontalScrollbarHeight = boundingClientRect.height - elem.clientHeight;
+
+ // On windows, the scrollbar tracks have buttons on the end. When computing
+ // coordinates for hit-testing we need to account for this. We assume the
+ // buttons are square, and so can use the scrollbar width/height to estimate
+ // the size of the buttons
+ var scrollbarArrowButtonHeight = config.isWindows
+ ? verticalScrollbarWidth
+ : 0;
+ var scrollbarArrowButtonWidth = config.isWindows
+ ? horizontalScrollbarHeight
+ : 0;
+
+ // Compute the expected hit result flags.
+ // The direction flag (APZHitResultFlags.SCROLLBAR_VERTICAL) is added in
+ // later, for the vertical test only.
+ // The APZHitResultFlags.SCROLLBAR flag will be present regardless of whether
+ // the layer is active or inactive because we force layerization of scrollbar
+ // tracks. Unfortunately not forcing the layerization results in different
+ // behaviour on different platforms which makes testing harder.
+ var expectedHitInfo = APZHitResultFlags.VISIBLE | APZHitResultFlags.SCROLLBAR;
+ if (params.expectThumb) {
+ // The thumb has listeners which are APZ-aware.
+ expectedHitInfo |= APZHitResultFlags.APZ_AWARE_LISTENERS;
+ var expectActive =
+ config.activateAllScrollFrames || params.layerState == LayerState.ACTIVE;
+ if (!expectActive) {
+ expectedHitInfo |= APZHitResultFlags.INACTIVE_SCROLLFRAME;
+ }
+ // We do not generate the layers for thumbs on inactive scrollframes.
+ if (expectActive) {
+ expectedHitInfo |= APZHitResultFlags.SCROLLBAR_THUMB;
+ }
+ }
+
+ var expectedScrollId = params.expectedScrollId;
+ if (config.activateAllScrollFrames) {
+ expectedScrollId = config.utils.getViewId(params.element);
+ if (params.layerState == LayerState.ACTIVE) {
+ is(
+ expectedScrollId,
+ params.expectedScrollId,
+ "Expected scrollId for active scrollframe should match"
+ );
+ }
+ }
+
+ var scrollframeMsg =
+ params.layerState == LayerState.ACTIVE
+ ? "active scrollframe"
+ : "inactive scrollframe";
+
+ // Hit-test the targeted areas, assuming we don't have overlay scrollbars
+ // with zero dimensions.
+ if (params.directions.vertical && verticalScrollbarWidth > 0) {
+ var verticalScrollbarPoint = {
+ x: boundingClientRect.right - verticalScrollbarWidth / 2,
+ y:
+ params.trackLocation == ScrollbarTrackLocation.START
+ ? boundingClientRect.y + scrollbarArrowButtonHeight + 5
+ : boundingClientRect.bottom -
+ horizontalScrollbarHeight -
+ scrollbarArrowButtonHeight -
+ 5,
+ };
+ checkHitResult(
+ hitTest(verticalScrollbarPoint),
+ expectedHitInfo | APZHitResultFlags.SCROLLBAR_VERTICAL,
+ expectedScrollId,
+ params.expectedLayersId,
+ scrollframeMsg + " - vertical scrollbar"
+ );
+ }
+
+ if (params.directions.horizontal && horizontalScrollbarHeight > 0) {
+ var horizontalScrollbarPoint = {
+ x:
+ params.trackLocation == ScrollbarTrackLocation.START
+ ? boundingClientRect.x + scrollbarArrowButtonWidth + 5
+ : boundingClientRect.right -
+ verticalScrollbarWidth -
+ scrollbarArrowButtonWidth -
+ 5,
+ y: boundingClientRect.bottom - horizontalScrollbarHeight / 2,
+ };
+ checkHitResult(
+ hitTest(horizontalScrollbarPoint),
+ expectedHitInfo,
+ expectedScrollId,
+ params.expectedLayersId,
+ scrollframeMsg + " - horizontal scrollbar"
+ );
+ }
+}
+
+// Return a list of prefs for the given test identifier.
+function getPrefs(ident) {
+ switch (ident) {
+ case "TOUCH_EVENTS:PAN":
+ return [
+ // Dropping the touch slop to 0 makes the tests easier to write because
+ // we can just do a one-pixel drag to get over the pan threshold rather
+ // than having to hard-code some larger value.
+ ["apz.touch_start_tolerance", "0.0"],
+ // The touchstart from the drag can turn into a long-tap if the touch-move
+ // events get held up. Try to prevent that by making long-taps require
+ // a 10 second hold. Note that we also cannot enable chaos mode on this
+ // test for this reason, since chaos mode can cause the long-press timer
+ // to fire sooner than the pref dictates.
+ ["ui.click_hold_context_menus.delay", 10000],
+ // The subtests in this test do touch-drags to pan the page, but we don't
+ // want those pans to turn into fling animations, so we increase the
+ // fling min velocity requirement absurdly high.
+ ["apz.fling_min_velocity_threshold", "10000"],
+ // The helper_div_pan's div gets a displayport on scroll, but if the
+ // test takes too long the displayport can expire before the new scroll
+ // position is synced back to the main thread. So we disable displayport
+ // expiry for these tests.
+ ["apz.displayport_expiry_ms", 0],
+ // We need to disable touch resampling during these tests because we
+ // rely on touch move events being processed without delay. Touch
+ // resampling only processes them once vsync fires.
+ ["android.touch_resampling.enabled", false],
+ ];
+ case "TOUCH_ACTION":
+ return [
+ ...getPrefs("TOUCH_EVENTS:PAN"),
+ ["apz.test.fails_with_native_injection", getPlatform() == "windows"],
+ ];
+ default:
+ return [];
+ }
+}
+
+var ApzCleanup = {
+ _cleanups: [],
+
+ register(func) {
+ if (!this._cleanups.length) {
+ if (!window.isApzSubtest) {
+ SimpleTest.registerCleanupFunction(this.execute.bind(this));
+ } // else ApzCleanup.execute is called from runSubtestsSeriallyInFreshWindows
+ }
+ this._cleanups.push(func);
+ },
+
+ execute() {
+ while (this._cleanups.length) {
+ var func = this._cleanups.pop();
+ try {
+ func();
+ } catch (ex) {
+ SimpleTest.ok(
+ false,
+ "Subtest cleanup function [" +
+ func.toString() +
+ "] threw exception [" +
+ ex +
+ "] on page [" +
+ location.href +
+ "]"
+ );
+ }
+ }
+ },
+};
+
+/**
+ * Returns a promise that will resolve if `eventTarget` receives an event of the
+ * given type that passes the given filter. Only the first matching message is
+ * used. The filter must be a function (or null); it is called with the event
+ * object and the call must return true to resolve the promise.
+ */
+function promiseOneEvent(eventTarget, eventType, filter) {
+ return new Promise((resolve, reject) => {
+ eventTarget.addEventListener(eventType, function listener(e) {
+ let success = false;
+ if (filter == null) {
+ success = true;
+ } else if (typeof filter == "function") {
+ try {
+ success = filter(e);
+ } catch (ex) {
+ dump(
+ `ERROR: Filter passed to promiseOneEvent threw exception: ${ex}\n`
+ );
+ reject();
+ return;
+ }
+ } else {
+ dump(
+ "ERROR: Filter passed to promiseOneEvent was neither null nor a function\n"
+ );
+ reject();
+ return;
+ }
+ if (success) {
+ eventTarget.removeEventListener(eventType, listener);
+ resolve(e);
+ }
+ });
+ });
+}
+
+function visualViewportAsZoomedRect() {
+ let vv = window.visualViewport;
+ return {
+ x: vv.pageLeft,
+ y: vv.pageTop,
+ w: vv.width,
+ h: vv.height,
+ z: vv.scale,
+ };
+}
+
+// Pulls the latest compositor APZ test data and checks to see if the
+// scroller with id `scrollerId` was checkerboarding. It also ensures that
+// a scroller with id `scrollerId` was actually found in the test data.
+// This function requires that "apz.test.logging_enabled" be set to true,
+// in order for the test data to be logged.
+function assertNotCheckerboarded(utils, scrollerId, msgPrefix) {
+ utils.advanceTimeAndRefresh(0);
+ var data = utils.getCompositorAPZTestData();
+ //dump(JSON.stringify(data, null, 4));
+ var found = false;
+ for (apzcData of data.additionalData) {
+ if (apzcData.key == scrollerId) {
+ var checkerboarding = apzcData.value
+ .split(",")
+ .includes("checkerboarding");
+ ok(!checkerboarding, `${msgPrefix}: scroller is not checkerboarding`);
+ found = true;
+ }
+ }
+ ok(found, `${msgPrefix}: Found the scroller in the APZ data`);
+ utils.restoreNormalRefresh();
+}
+
+async function waitToClearOutAnyPotentialScrolls(aWindow) {
+ await promiseFrame(aWindow);
+ await promiseFrame(aWindow);
+ await promiseOnlyApzControllerFlushed(aWindow);
+ await promiseFrame(aWindow);
+ await promiseFrame(aWindow);
+}
+
+function waitForScrollEvent(target) {
+ return new Promise(resolve => {
+ target.addEventListener("scroll", resolve, { once: true });
+ });
+}
+
+// This is another variant of promiseApzFlushedRepaints.
+// We need this function because, unfortunately, there is no easy way to use
+// paint_listeners.js' functions and apz_test_utils.js' functions in popup
+// contents opened by extensions either as scripts in the popup contents or
+// scripts inside SpecialPowers.spawn because we can't use privileged functions
+// in the popup contents' script, we can't use functions basically as it as in
+// the sandboxed context either.
+async function promiseApzFlushedRepaintsInPopup(popup) {
+ // Flush APZ repaints and waits for MozAfterPaint.
+ await SpecialPowers.spawn(popup, [], async () => {
+ const utils = SpecialPowers.getDOMWindowUtils(content.window);
+
+ async function promiseAllPaintsDone() {
+ return new Promise(resolve => {
+ function waitForPaints() {
+ if (utils.isMozAfterPaintPending) {
+ dump("Waits for a MozAfterPaint event\n");
+ content.window.addEventListener(
+ "MozAfterPaint",
+ () => {
+ dump("Got a MozAfterPaint event\n");
+ waitForPaints();
+ },
+ { once: true }
+ );
+ } else {
+ dump("No more pending MozAfterPaint\n");
+ content.window.setTimeout(resolve, 0);
+ }
+ }
+ waitForPaints();
+ });
+ }
+ await promiseAllPaintsDone();
+
+ await new Promise(resolve => {
+ var repaintDone = function () {
+ dump("APZ flush done\n");
+ SpecialPowers.Services.obs.removeObserver(
+ repaintDone,
+ "apz-repaints-flushed"
+ );
+ content.window.setTimeout(resolve, 0);
+ };
+ SpecialPowers.Services.obs.addObserver(
+ repaintDone,
+ "apz-repaints-flushed"
+ );
+ if (utils.flushApzRepaints()) {
+ dump("Flushed APZ repaints, waiting for callback...\n");
+ } else {
+ dump(
+ "Flushing APZ repaints was a no-op, triggering callback directly...\n"
+ );
+ repaintDone();
+ }
+ });
+
+ await promiseAllPaintsDone();
+ });
+}
+
+// A utility function to make sure there's no scroll animation on the given
+// |aElement|.
+async function cancelScrollAnimation(aElement, aWindow = window) {
+ // In fact there's no good way to directly cancel the active animation on the
+ // element, so we destroy the corresponding scrollable frame then reconstruct
+ // a new scrollable frame so that it clobbers the animation.
+ const originalStyle = aElement.style.display;
+ aElement.style.display = "none";
+ await aWindow.promiseApzFlushedRepaints();
+ aElement.style.display = originalStyle;
+ await aWindow.promiseApzFlushedRepaints();
+}
+
+function collectSampledScrollOffsets(aElement) {
+ let data = SpecialPowers.DOMWindowUtils.getCompositorAPZTestData();
+ let sampledResults = data.sampledResults;
+
+ const layersId = SpecialPowers.DOMWindowUtils.getLayersId();
+ const scrollId = SpecialPowers.DOMWindowUtils.getViewId(aElement);
+
+ return sampledResults.filter(
+ result =>
+ SpecialPowers.wrap(result).layersId == layersId &&
+ SpecialPowers.wrap(result).scrollId == scrollId
+ );
+}
diff --git a/gfx/layers/apz/test/mochitest/browser.ini b/gfx/layers/apz/test/mochitest/browser.ini
new file mode 100644
index 0000000000..577c663fab
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/browser.ini
@@ -0,0 +1,64 @@
+[DEFAULT]
+support-files =
+ apz_test_native_event_utils.js
+ apz_test_utils.js
+ helper_browser_test_utils.js
+ !/browser/base/content/test/forms/head.js
+ !/browser/components/extensions/test/browser/head.js
+ !/browser/components/extensions/test/browser/head_browserAction.js
+
+[browser_test_group_fission.js]
+skip-if =
+ (os == 'win' && bits == 32) # Some subtests fail intermittently on Win7.
+ (os == 'linux' && bits == 64) # Bug 1773830
+support-files =
+ FissionTestHelperParent.sys.mjs
+ FissionTestHelperChild.sys.mjs
+ helper_fission_*.*
+ !/dom/animation/test/testcommon.js
+[browser_test_select_zoom.js]
+skip-if = (os == 'win') # bug 1495580
+support-files =
+ helper_test_select_zoom.html
+[browser_test_select_popup_position.js]
+support-files =
+ helper_test_select_popup_position.html
+ helper_test_select_popup_position_transformed_in_parent.html
+ helper_test_select_popup_position_zoomed.html
+[browser_test_background_tab_load_scroll.js]
+support-files =
+ helper_background_tab_load_scroll.html
+[browser_test_background_tab_scroll.js]
+skip-if = (toolkit == 'android') # wheel events not supported on mobile
+support-files =
+ helper_background_tab_scroll.html
+[browser_test_reset_scaling_zoom.js]
+support-files =
+ helper_test_reset_scaling_zoom.html
+[browser_test_scrollbar_in_extension_popup_window.js]
+skip-if =
+ verify || os == 'linux' # Bug 1713052
+[browser_test_scrolling_in_extension_popup_window.js]
+skip-if =
+ os == "mac" # Bug 1784759
+[browser_test_scrolling_on_inactive_scroller_in_extension_popup_window.js]
+run-if = (os == 'mac') # bug 1700805
+[browser_test_scroll_thumb_dragging.js]
+support-files =
+ helper_scroll_thumb_dragging.html
+[browser_test_autoscrolling_in_extension_popup_window.js]
+[browser_test_autoscrolling_in_oop_frame.js]
+skip-if = !fission
+support-files =
+ helper_test_autoscrolling_in_oop_frame.html
+[browser_test_animations_without_apz_sampler.js]
+[browser_test_position_sticky.js]
+support-files =
+ helper_position_sticky_flicker.html
+[browser_test_tab_drag_zoom.js]
+skip-if = (os == 'win') # Our Windows touch injection test code doesn't support pinch gestures (bug 1495580)
+support-files =
+ helper_test_tab_drag_zoom.html
+[browser_test_content_response_timeout.js]
+support-files =
+ helper_content_response_timeout.html
diff --git a/gfx/layers/apz/test/mochitest/browser_test_animations_without_apz_sampler.js b/gfx/layers/apz/test/mochitest/browser_test_animations_without_apz_sampler.js
new file mode 100644
index 0000000000..8fdd20887a
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/browser_test_animations_without_apz_sampler.js
@@ -0,0 +1,134 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/browser/components/extensions/test/browser/head.js",
+ this
+);
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/browser/components/extensions/test/browser/head_browserAction.js",
+ this
+);
+
+add_task(async () => {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_action: {
+ default_popup: "popup.html",
+ browser_style: true,
+ },
+ },
+
+ files: {
+ "popup.html": `
+ <html>
+ <head>
+ <meta charset="utf-8">
+ <style>
+ #target {
+ width: 100px;
+ height: 50px;
+ background: green;
+ }
+ #target2 {
+ width: 100px;
+ height: 50px;
+ background: green;
+ }
+ </style>
+ </head>
+ <body>
+ <div id="target"></div>
+ <div id="target2"></div>
+ </body>
+ </html>`,
+ },
+ });
+
+ await extension.startup();
+
+ async function takeSnapshot(browserWin, callback) {
+ let browser = await openBrowserActionPanel(extension, browserWin, true);
+
+ if (callback) {
+ await SpecialPowers.spawn(browser, [], callback);
+ }
+
+ // Ensure there's no pending paint requests.
+ // The below code is a simplified version of promiseAllPaintsDone in
+ // paint_listener.js.
+ await SpecialPowers.spawn(browser, [], async () => {
+ return new Promise(resolve => {
+ function waitForPaints() {
+ // Wait until paint suppression has ended
+ if (SpecialPowers.DOMWindowUtils.paintingSuppressed) {
+ dump`waiting for paint suppression to end...`;
+ content.window.setTimeout(waitForPaints, 0);
+ return;
+ }
+
+ if (SpecialPowers.DOMWindowUtils.isMozAfterPaintPending) {
+ dump`waiting for paint...`;
+ content.window.addEventListener("MozAfterPaint", waitForPaints, {
+ once: true,
+ });
+ return;
+ }
+ resolve();
+ }
+ waitForPaints();
+ });
+ });
+
+ const snapshot = await SpecialPowers.spawn(browser, [], async () => {
+ return SpecialPowers.snapshotWindowWithOptions(
+ content.window,
+ undefined /* use the default rect */,
+ undefined /* use the default bgcolor */,
+ { DRAWWINDOW_DRAW_VIEW: true } /* to capture scrollbars */
+ )
+ .toDataURL()
+ .toString();
+ });
+
+ const popup = getBrowserActionPopup(extension, browserWin);
+ await closeBrowserAction(extension, browserWin);
+ is(popup.state, "closed", "browserAction popup has been closed");
+
+ return snapshot;
+ }
+
+ // Test without apz sampler.
+ await SpecialPowers.pushPrefEnv({ set: [["apz.popups.enabled", false]] });
+
+ // Reference
+ const newWin = await BrowserTestUtils.openNewBrowserWindow();
+ const reference = await takeSnapshot(newWin);
+ await BrowserTestUtils.closeWindow(newWin);
+
+ // Test target
+ const testWin = await BrowserTestUtils.openNewBrowserWindow();
+ const result = await takeSnapshot(testWin, async () => {
+ let div = content.window.document.getElementById("target");
+ const anim = div.animate({ opacity: [1, 0.5] }, 10);
+ await anim.finished;
+ const anim2 = div.animate(
+ { transform: ["translateX(10px)", "translateX(20px)"] },
+ 10
+ );
+ await anim2.finished;
+
+ let div2 = content.window.document.getElementById("target2");
+ const anim3 = div2.animate(
+ { transform: ["translateX(10px)", "translateX(20px)"] },
+ 10
+ );
+ await anim3.finished;
+ });
+ await BrowserTestUtils.closeWindow(testWin);
+
+ is(result, reference, "The omta property value should be reset");
+
+ await extension.unload();
+});
diff --git a/gfx/layers/apz/test/mochitest/browser_test_autoscrolling_in_extension_popup_window.js b/gfx/layers/apz/test/mochitest/browser_test_autoscrolling_in_extension_popup_window.js
new file mode 100644
index 0000000000..911af7548d
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/browser_test_autoscrolling_in_extension_popup_window.js
@@ -0,0 +1,189 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/browser/components/extensions/test/browser/head.js",
+ this
+);
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/browser/components/extensions/test/browser/head_browserAction.js",
+ this
+);
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/gfx/layers/apz/test/mochitest/apz_test_utils.js",
+ this
+);
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js",
+ this
+);
+
+add_task(async () => {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_action: {
+ default_popup: "popup.html",
+ browser_style: true,
+ },
+ },
+
+ files: {
+ "popup.html": `
+ <html>
+ <head>
+ <meta charset="utf-8">
+ <script src="popup.js"></script>
+ <style>
+ * {
+ padding: 0;
+ margin: 0;
+ }
+ body {
+ height: 400px;
+ width: 200px;
+ overflow-y: auto;
+ overflow-x: hidden;
+ }
+ li {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 30vh;
+ font-size: 200%;
+ }
+ li:nth-child(even){
+ background-color: #ccc;
+ }
+ </style>
+ </head>
+ <body>
+ <ul>
+ <li>1</li>
+ <li>2</li>
+ <li>3</li>
+ <li>4</li>
+ <li>5</li>
+ <li>6</li>
+ <li>7</li>
+ <li>8</li>
+ <li>9</li>
+ <li>10</li>
+ </ul>
+ </body>
+ </html>`,
+ "popup.js": function () {
+ window.addEventListener(
+ "mousemove",
+ () => {
+ dump("Got a mousemove event in the popup content document\n");
+ browser.test.sendMessage("received-mousemove");
+ },
+ { once: true }
+ );
+ window.addEventListener(
+ "scroll",
+ () => {
+ dump("Got a scroll event in the popup content document\n");
+ browser.test.sendMessage("received-scroll");
+ },
+ { once: true }
+ );
+ },
+ },
+ });
+
+ await extension.startup();
+
+ await SpecialPowers.pushPrefEnv({ set: [["apz.popups.enabled", true]] });
+
+ // Open the popup window of the extension.
+ const browserForPopup = await openBrowserActionPanel(
+ extension,
+ undefined,
+ true
+ );
+
+ if (!browserForPopup.isRemoteBrowser) {
+ await closeBrowserAction(extension);
+ await extension.unload();
+ ok(
+ true,
+ "Skipping this test since the popup window doesn't have remote contents"
+ );
+ return;
+ }
+
+ // Flush APZ repaints and waits for MozAfterPaint to make sure APZ state is
+ // stable.
+ await promiseApzFlushedRepaintsInPopup(browserForPopup);
+
+ const { screenX, screenY, viewId, presShellId } = await SpecialPowers.spawn(
+ browserForPopup,
+ [],
+ () => {
+ const winUtils = SpecialPowers.getDOMWindowUtils(content.window);
+ return {
+ screenX: content.window.mozInnerScreenX * content.devicePixelRatio,
+ screenY: content.window.mozInnerScreenY * content.devicePixelRatio,
+ viewId: winUtils.getViewId(content.document.documentElement),
+ presShellId: winUtils.getPresShellId(),
+ };
+ }
+ );
+
+ // Before starting autoscroll we need to make sure a mousemove event has been
+ // processed in the popup content so that subsequent mousemoves for autoscroll
+ // will be properly processed in autoscroll animation.
+ const mousemoveEventPromise = extension.awaitMessage("received-mousemove");
+
+ const nativeMouseEventPromise = promiseNativeMouseEventWithAPZ({
+ type: "mousemove",
+ target: browserForPopup,
+ offsetX: 100,
+ offsetY: 50,
+ });
+
+ await Promise.all([nativeMouseEventPromise, mousemoveEventPromise]);
+
+ const scrollEventPromise = extension.awaitMessage("received-scroll");
+
+ // Start autoscrolling.
+ ok(
+ browserForPopup.browsingContext.startApzAutoscroll(
+ screenX + 100,
+ screenY + 50,
+ viewId,
+ presShellId
+ )
+ );
+
+ // Send sequential mousemove events to cause autoscrolling.
+ for (let i = 0; i < 10; i++) {
+ await promiseNativeMouseEventWithAPZ({
+ type: "mousemove",
+ target: browserForPopup,
+ offsetX: 100,
+ offsetY: 50 + i * 10,
+ });
+ }
+
+ // Flush APZ repaints and waits for MozAfterPaint to make sure the scroll has
+ // been reflected on the main thread.
+ const apzPromise = promiseApzFlushedRepaintsInPopup(browserForPopup);
+
+ await Promise.all([apzPromise, scrollEventPromise]);
+
+ const scrollY = await SpecialPowers.spawn(browserForPopup, [], () => {
+ return content.window.scrollY;
+ });
+ ok(scrollY > 0, "Autoscrolling works in the popup window");
+
+ browserForPopup.browsingContext.stopApzAutoscroll(viewId, presShellId);
+
+ await closeBrowserAction(extension);
+
+ await extension.unload();
+});
diff --git a/gfx/layers/apz/test/mochitest/browser_test_autoscrolling_in_oop_frame.js b/gfx/layers/apz/test/mochitest/browser_test_autoscrolling_in_oop_frame.js
new file mode 100644
index 0000000000..26d0ff6109
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/browser_test_autoscrolling_in_oop_frame.js
@@ -0,0 +1,120 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/gfx/layers/apz/test/mochitest/apz_test_utils.js",
+ this
+);
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js",
+ this
+);
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["general.autoScroll", true],
+ ["middlemouse.contentLoadURL", false],
+ ["test.events.async.enabled", true],
+ ],
+ });
+});
+
+async function doTest() {
+ function httpURL(filename) {
+ const chromeURL = getRootDirectory(gTestPath) + filename;
+ return chromeURL.replace(
+ "chrome://mochitests/content/",
+ "http://mochi.test:8888/"
+ );
+ }
+
+ function getScrollY(context) {
+ return SpecialPowers.spawn(context, [], () => content.scrollY);
+ }
+
+ const pageUrl = httpURL("helper_test_autoscrolling_in_oop_frame.html");
+
+ await BrowserTestUtils.withNewTab(pageUrl, async function (browser) {
+ await promiseApzFlushedRepaintsInPopup(browser);
+
+ const iframeContext = browser.browsingContext.children[0];
+ await promiseApzFlushedRepaintsInPopup(iframeContext);
+
+ const { screenX, screenY, viewId, presShellId } = await SpecialPowers.spawn(
+ iframeContext,
+ [],
+ () => {
+ const winUtils = SpecialPowers.getDOMWindowUtils(content);
+ return {
+ screenX: content.mozInnerScreenX * content.devicePixelRatio,
+ screenY: content.mozInnerScreenY * content.devicePixelRatio,
+ viewId: winUtils.getViewId(content.document.documentElement),
+ presShellId: winUtils.getPresShellId(),
+ };
+ }
+ );
+
+ ok(
+ iframeContext.startApzAutoscroll(
+ screenX + 100,
+ screenY + 50,
+ viewId,
+ presShellId
+ ),
+ "Started autscroll"
+ );
+
+ const scrollEventPromise = SpecialPowers.spawn(
+ iframeContext,
+ [],
+ async () => {
+ return new Promise(resolve => {
+ content.addEventListener(
+ "scroll",
+ event => {
+ dump("Got a scroll event in the iframe\n");
+ resolve();
+ },
+ { once: true }
+ );
+ });
+ }
+ );
+
+ // Send sequential mousemove events to cause autoscrolling.
+ for (let i = 0; i < 10; i++) {
+ await promiseNativeMouseEventWithAPZ({
+ type: "mousemove",
+ target: browser,
+ offsetX: 100,
+ offsetY: 50 + i * 10,
+ });
+ }
+
+ // Flush APZ repaints and waits for MozAfterPaint to make sure the scroll has
+ // been reflected on the main thread.
+ const apzPromise = promiseApzFlushedRepaintsInPopup(browser);
+
+ await Promise.all([apzPromise, scrollEventPromise]);
+
+ const frameScrollY = await getScrollY(iframeContext);
+ ok(frameScrollY > 0, "Autoscrolled the iframe");
+
+ const rootScrollY = await getScrollY(browser);
+ ok(rootScrollY == 0, "Didn't scroll the root document");
+
+ iframeContext.stopApzAutoscroll(viewId, presShellId);
+ });
+}
+
+add_task(async function test_autoscroll_in_oop_iframe() {
+ await doTest();
+});
+
+add_task(async function test_autoscroll_in_oop_iframe_with_os_zoom() {
+ await SpecialPowers.pushPrefEnv({ set: [["ui.textScaleFactor", 200]] });
+ await doTest();
+});
diff --git a/gfx/layers/apz/test/mochitest/browser_test_background_tab_load_scroll.js b/gfx/layers/apz/test/mochitest/browser_test_background_tab_load_scroll.js
new file mode 100644
index 0000000000..9878907603
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/browser_test_background_tab_load_scroll.js
@@ -0,0 +1,117 @@
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/gfx/layers/apz/test/mochitest/apz_test_utils.js",
+ this
+);
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js",
+ this
+);
+
+add_task(async function test_main() {
+ // Open a specific page in a background tab, then switch to the tab, check if
+ // the visual and layout scroll offsets have diverged.
+ // Then change to another tab so it's background again. Then reload it. Then
+ // change back to it and check again if the visual and layout scroll offsets
+ // have diverged.
+ // The page has a couple important properties to trigger the bug. We need to
+ // be restoring a non-zero scroll position so that we call ScrollToImpl with
+ // origin restore so that we do not set the visual viewport offset. We then
+ // need to call ScrollToImpl with a origin that does not get clobber by apz
+ // so that we (wrongly) set the visual viewport offset.
+
+ requestLongerTimeout(2);
+
+ async function twoRafsInContent(browser) {
+ await SpecialPowers.spawn(browser, [], async function () {
+ await new Promise(r =>
+ content.requestAnimationFrame(() => content.requestAnimationFrame(r))
+ );
+ });
+ }
+
+ async function waitForApzInContent(browser) {
+ await SpecialPowers.spawn(browser, [], async () => {
+ await content.wrappedJSObject.waitUntilApzStable();
+ await content.wrappedJSObject.promiseApzFlushedRepaints();
+ });
+ }
+
+ async function checkScrollPosInContent(browser, iter, num) {
+ let visualScrollPos = await SpecialPowers.spawn(browser, [], function () {
+ const offsetX = {};
+ const offsetY = {};
+ SpecialPowers.getDOMWindowUtils(content).getVisualViewportOffset(
+ offsetX,
+ offsetY
+ );
+ return offsetY.value;
+ });
+
+ let scrollPos = await SpecialPowers.spawn(browser, [], function () {
+ return content.window.scrollY;
+ });
+
+ // When this fails the difference is at least 10000.
+ ok(
+ Math.abs(scrollPos - visualScrollPos) < 2,
+ "expect scroll position and visual scroll position to be the same: visual " +
+ visualScrollPos +
+ " scroll " +
+ scrollPos +
+ " (" +
+ iter +
+ "," +
+ num +
+ ")"
+ );
+ }
+
+ for (let i = 0; i < 5; i++) {
+ let blankurl = "about:blank";
+ let blankTab = BrowserTestUtils.addTab(gBrowser, blankurl);
+ let blankbrowser = blankTab.linkedBrowser;
+ await BrowserTestUtils.browserLoaded(blankbrowser, false, blankurl);
+
+ let url =
+ "http://mochi.test:8888/browser/gfx/layers/apz/test/mochitest/helper_background_tab_load_scroll.html";
+ let backgroundTab = BrowserTestUtils.addTab(gBrowser, url);
+ let browser = backgroundTab.linkedBrowser;
+ await BrowserTestUtils.browserLoaded(browser, false, url);
+ dump("Done loading background tab\n");
+
+ await twoRafsInContent(browser);
+
+ // Switch to the foreground.
+ await BrowserTestUtils.switchTab(gBrowser, backgroundTab);
+ dump("Switched background tab to foreground\n");
+
+ await waitForApzInContent(browser);
+
+ await checkScrollPosInContent(browser, i, 1);
+
+ await BrowserTestUtils.switchTab(gBrowser, blankTab);
+
+ browser.reload();
+ await BrowserTestUtils.browserLoaded(browser, false, url);
+
+ await twoRafsInContent(browser);
+
+ // Switch to the foreground.
+ await BrowserTestUtils.switchTab(gBrowser, backgroundTab);
+ dump("Switched background tab to foreground\n");
+
+ await waitForApzInContent(browser);
+
+ await checkScrollPosInContent(browser, i, 2);
+
+ // Cleanup
+ let tabClosed = BrowserTestUtils.waitForTabClosing(backgroundTab);
+ BrowserTestUtils.removeTab(backgroundTab);
+ await tabClosed;
+
+ let blanktabClosed = BrowserTestUtils.waitForTabClosing(blankTab);
+ BrowserTestUtils.removeTab(blankTab);
+ await blanktabClosed;
+ }
+});
diff --git a/gfx/layers/apz/test/mochitest/browser_test_background_tab_scroll.js b/gfx/layers/apz/test/mochitest/browser_test_background_tab_scroll.js
new file mode 100644
index 0000000000..4ce8200199
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/browser_test_background_tab_scroll.js
@@ -0,0 +1,66 @@
+add_task(async function test_main() {
+ // Load page in the background. This will cause the first-paint of the
+ // tab (which has ScrollPositionUpdate instances) to get sent to the
+ // compositor, but the parent process RefLayer won't be pointing to this
+ // tab so APZ never sees the ScrollPositionUpdate instances.
+
+ let url =
+ "http://mochi.test:8888/browser/gfx/layers/apz/test/mochitest/helper_background_tab_scroll.html#scrolltarget";
+ let backgroundTab = BrowserTestUtils.addTab(gBrowser, url);
+ let browser = backgroundTab.linkedBrowser;
+ await BrowserTestUtils.browserLoaded(browser, false, url);
+ dump("Done loading background tab\n");
+
+ // Switch to the foreground, to let the APZ tree get built.
+ await BrowserTestUtils.switchTab(gBrowser, backgroundTab);
+ dump("Switched background tab to foreground\n");
+
+ // Verify main-thread scroll position is where we expect
+ let scrollPos = await ContentTask.spawn(browser, null, function () {
+ return content.window.scrollY;
+ });
+ is(scrollPos, 5000, "Expected background tab to be at scroll pos 5000");
+
+ // Trigger an APZ-side scroll via native wheel event, followed by some code
+ // to ensure APZ's repaint requests to arrive at the main-thread. If
+ // things are working properly, the main thread will accept the repaint
+ // requests and update the main-thread scroll position. If the APZ side
+ // is sending incorrect scroll generations in the repaint request, then
+ // the main thread will fail to clear the main-thread scroll origin (which
+ // was set by the scroll to the #scrolltarget anchor), and so will not
+ // accept APZ's scroll position updates.
+ let contentScrollFunction = async function () {
+ await content.window.wrappedJSObject.promiseNativeWheelAndWaitForWheelEvent(
+ content.window,
+ 100,
+ 100,
+ 0,
+ 200
+ );
+
+ // Advance some/all frames of the APZ wheel animation
+ let utils = content.window.SpecialPowers.getDOMWindowUtils(content.window);
+ for (var i = 0; i < 10; i++) {
+ utils.advanceTimeAndRefresh(16);
+ }
+ utils.restoreNormalRefresh();
+ // Flush pending APZ repaints, then read the main-thread scroll
+ // position
+ await content.window.wrappedJSObject.promiseOnlyApzControllerFlushed(
+ content.window
+ );
+ return content.window.scrollY;
+ };
+ scrollPos = await ContentTask.spawn(browser, null, contentScrollFunction);
+
+ // Verify main-thread scroll position has changed
+ ok(
+ scrollPos < 5000,
+ `Expected background tab to have scrolled up, is at ${scrollPos}`
+ );
+
+ // Cleanup
+ let tabClosed = BrowserTestUtils.waitForTabClosing(backgroundTab);
+ BrowserTestUtils.removeTab(backgroundTab);
+ await tabClosed;
+});
diff --git a/gfx/layers/apz/test/mochitest/browser_test_content_response_timeout.js b/gfx/layers/apz/test/mochitest/browser_test_content_response_timeout.js
new file mode 100644
index 0000000000..a80fd77c17
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/browser_test_content_response_timeout.js
@@ -0,0 +1,88 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+
+"use strict";
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/gfx/layers/apz/test/mochitest/apz_test_utils.js",
+ this
+);
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js",
+ this
+);
+
+add_task(async () => {
+ // Use pan gesture events for Mac.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // Set a relatively shorter timeout value.
+ ["apz.content_response_timeout", 100],
+ ],
+ });
+
+ const URL_ROOT = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content/",
+ "http://mochi.test:8888/"
+ );
+ // Load a content having an APZ-aware listener causing 500ms busy state and
+ // a scroll event listener changing the background color of an element.
+ // The reason why we change the background color in a scroll listener rather
+ // than setting up a Promise resolved in a scroll event handler and waiting
+ // for the Promise is SpecialPowers.spawn doesn't allow it (bug 1743857).
+ const tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ URL_ROOT + "helper_content_response_timeout.html"
+ );
+
+ let scrollPromise = BrowserTestUtils.waitForContentEvent(
+ tab.linkedBrowser,
+ "scroll"
+ );
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ await content.wrappedJSObject.promiseApzFlushedRepaints();
+ await content.wrappedJSObject.waitUntilApzStable();
+ });
+
+ // Note that below function uses `WaitForObserver` version of sending a
+ // pan-start event function so that the notification can be sent in the parent
+ // process, thus we can get the notification even if the content process is
+ // busy.
+ await NativePanHandler.promiseNativePanEvent(
+ tab.linkedBrowser,
+ 100,
+ 100,
+ 0,
+ NativePanHandler.delta,
+ NativePanHandler.beginPhase
+ );
+
+ await new Promise(resolve => {
+ setTimeout(resolve, 200);
+ });
+
+ await NativePanHandler.promiseNativePanEvent(
+ tab.linkedBrowser,
+ 100,
+ 100,
+ 0,
+ NativePanHandler.delta,
+ NativePanHandler.updatePhase
+ );
+ await NativePanHandler.promiseNativePanEvent(
+ tab.linkedBrowser,
+ 100,
+ 100,
+ 0,
+ 0,
+ NativePanHandler.endPhase
+ );
+
+ await scrollPromise;
+ ok(true, "We got at least one scroll event");
+
+ BrowserTestUtils.removeTab(tab);
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/gfx/layers/apz/test/mochitest/browser_test_group_fission.js b/gfx/layers/apz/test/mochitest/browser_test_group_fission.js
new file mode 100644
index 0000000000..43bbcbe444
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/browser_test_group_fission.js
@@ -0,0 +1,150 @@
+add_task(async function setup_pref() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // To avoid throttling requestAnimationFrame callbacks in invisible
+ // iframes
+ ["layout.throttled_frame_rate", 60],
+ ["dom.animations-api.getAnimations.enabled", true],
+ ["dom.animations-api.timelines.enabled", true],
+ // Next two prefs are needed for hit-testing to work
+ ["test.events.async.enabled", true],
+ ["apz.test.logging_enabled", true],
+ ],
+ });
+});
+
+add_task(async function test_main() {
+ function httpURL(filename) {
+ let chromeURL = getRootDirectory(gTestPath) + filename;
+ return chromeURL.replace(
+ "chrome://mochitests/content/",
+ "http://mochi.test:8888/"
+ );
+ }
+
+ // Each of these subtests is a dictionary that contains:
+ // file (required): filename of the subtest that will get opened in a new tab
+ // in the top-level fission-enabled browser window.
+ // setup (optional): function that takes the top-level fission window and is
+ // run once after the subtest is loaded but before it is started.
+ var subtests = [
+ { file: "helper_fission_basic.html" },
+ { file: "helper_fission_transforms.html" },
+ { file: "helper_fission_scroll_oopif.html" },
+ {
+ file: "helper_fission_event_region_override.html",
+ setup(win) {
+ win.document.addEventListener("wheel", e => e.preventDefault(), {
+ once: true,
+ passive: false,
+ });
+ },
+ },
+ { file: "helper_fission_animation_styling_in_oopif.html" },
+ { file: "helper_fission_force_empty_hit_region.html" },
+ { file: "helper_fission_touch.html" },
+ {
+ file: "helper_fission_tap.html",
+ prefs: [["apz.max_tap_time", 10000]],
+ },
+ { file: "helper_fission_inactivescroller_under_oopif.html" },
+ {
+ file: "helper_fission_tap_on_zoomed.html",
+ prefs: [["apz.max_tap_time", 10000]],
+ },
+ {
+ file: "helper_fission_tap_in_nested_iframe_on_zoomed.html",
+ prefs: [["apz.max_tap_time", 10000]],
+ },
+ { file: "helper_fission_scroll_handoff.html" },
+ { file: "helper_fission_large_subframe.html" },
+ { file: "helper_fission_initial_displayport.html" },
+ { file: "helper_fission_checkerboard_severity.html" },
+ { file: "helper_fission_setResolution.html" },
+ { file: "helper_fission_inactivescroller_positionedcontent.html" },
+ { file: "helper_fission_irregular_areas.html" },
+ { file: "helper_fission_animation_styling_in_transformed_oopif.html" },
+ // add additional tests here
+ ];
+
+ // ccov builds run slower and need longer, so let's scale up the timeout
+ // by the number of tests we're running.
+ requestLongerTimeout(subtests.length);
+
+ let fissionWindow = await BrowserTestUtils.openNewBrowserWindow({
+ fission: true,
+ });
+
+ // We import the ESM here so that we can install functions on the class
+ // below.
+ const { FissionTestHelperParent } = ChromeUtils.importESModule(
+ getRootDirectory(gTestPath) + "FissionTestHelperParent.sys.mjs"
+ );
+ FissionTestHelperParent.SimpleTest = SimpleTest;
+
+ ChromeUtils.registerWindowActor("FissionTestHelper", {
+ parent: {
+ esModuleURI:
+ getRootDirectory(gTestPath) + "FissionTestHelperParent.sys.mjs",
+ },
+ child: {
+ esModuleURI:
+ getRootDirectory(gTestPath) + "FissionTestHelperChild.sys.mjs",
+ events: {
+ "FissionTestHelper:Init": { capture: true, wantUntrusted: true },
+ },
+ },
+ allFrames: true,
+ });
+
+ try {
+ var onlyOneSubtest = SpecialPowers.getCharPref(
+ "apz.subtest",
+ /*default = */ ""
+ );
+
+ for (var subtest of subtests) {
+ if (onlyOneSubtest && onlyOneSubtest != subtest.file) {
+ SimpleTest.ok(
+ true,
+ "Skipping " +
+ subtest.file +
+ " because only " +
+ onlyOneSubtest +
+ " is being run"
+ );
+ continue;
+ }
+ let url = httpURL(subtest.file);
+ dump(`Starting test ${url}\n`);
+
+ // Load the test URL and tell it to get started, and wait until it reports
+ // completion.
+ await BrowserTestUtils.withNewTab(
+ { gBrowser: fissionWindow.gBrowser, url },
+ async browser => {
+ let tabActor =
+ browser.browsingContext.currentWindowGlobal.getActor(
+ "FissionTestHelper"
+ );
+ let donePromise = tabActor.getTestCompletePromise();
+ if (subtest.setup) {
+ subtest.setup(fissionWindow);
+ }
+ tabActor.startTest();
+ await donePromise;
+ }
+ );
+
+ dump(`Finished test ${url}\n`);
+ }
+ } finally {
+ // Delete stuff we added to FissionTestHelperParent, beacuse the object will
+ // outlive this test, and leaving stuff on it may leak the things reachable
+ // from it.
+ delete FissionTestHelperParent.SimpleTest;
+ // Teardown
+ ChromeUtils.unregisterWindowActor("FissionTestHelper");
+ await BrowserTestUtils.closeWindow(fissionWindow);
+ }
+});
diff --git a/gfx/layers/apz/test/mochitest/browser_test_position_sticky.js b/gfx/layers/apz/test/mochitest/browser_test_position_sticky.js
new file mode 100644
index 0000000000..ce6b093a80
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/browser_test_position_sticky.js
@@ -0,0 +1,105 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/gfx/layers/apz/test/mochitest/apz_test_utils.js",
+ this
+);
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js",
+ this
+);
+
+add_task(async () => {
+ function httpURL(filename) {
+ let chromeURL = getRootDirectory(gTestPath) + filename;
+ return chromeURL.replace(
+ "chrome://mochitests/content/",
+ "http://mochi.test:8888/"
+ );
+ }
+
+ const url = httpURL("helper_position_sticky_flicker.html");
+ const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+
+ const { rect, scrollbarWidth } = await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [],
+ async () => {
+ const sticky = content.document.getElementById("sticky");
+
+ // Get the area in the screen coords where the position:sticky element is.
+ let stickyRect = sticky.getBoundingClientRect();
+ stickyRect.x += content.window.mozInnerScreenX;
+ stickyRect.y += content.window.mozInnerScreenY;
+
+ // generate some DIVs to make the page complex enough.
+ for (let i = 1; i <= 120000; i++) {
+ const div = content.document.createElement("div");
+ div.innerText = `${i}`;
+ content.document.body.appendChild(div);
+ }
+
+ await content.wrappedJSObject.promiseApzFlushedRepaints();
+ await content.wrappedJSObject.waitUntilApzStable();
+
+ let w = {},
+ h = {};
+ SpecialPowers.DOMWindowUtils.getScrollbarSizes(
+ content.document.documentElement,
+ w,
+ h
+ );
+
+ // Reduce the scrollbar width from the sticky area.
+ stickyRect.width -= w.value;
+ return {
+ rect: stickyRect,
+ scrollbarWidth: w.value,
+ };
+ }
+ );
+
+ // Take a snapshot where the position:sticky element is initially painted.
+ const reference = await getSnapshot(rect);
+
+ let mouseX = window.innerWidth - scrollbarWidth / 2;
+ let mouseY = tab.linkedBrowser.getBoundingClientRect().y + 5;
+
+ // Scroll fast to cause checkerboarding multiple times.
+ const dragFinisher = await promiseNativeMouseDrag(
+ window,
+ mouseX,
+ mouseY,
+ 0,
+ window.innerHeight,
+ 100
+ );
+
+ // On debug builds there seems to be no chance that the content process gets
+ // painted during above promiseNativeMouseDrag call, wait two frames to make
+ // sure it happens so that this test is likely able to fail without proper
+ // fix.
+ if (AppConstants.DEBUG) {
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ await content.wrappedJSObject.promiseFrame(content.window);
+ await content.wrappedJSObject.promiseFrame(content.window);
+ });
+ }
+
+ // Take a snapshot again where the position:sticky element should be painted.
+ const snapshot = await getSnapshot(rect);
+
+ await dragFinisher();
+
+ is(
+ snapshot,
+ reference,
+ "The position:sticky element should stay at the " +
+ "same place after scrolling on heavy load"
+ );
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/gfx/layers/apz/test/mochitest/browser_test_reset_scaling_zoom.js b/gfx/layers/apz/test/mochitest/browser_test_reset_scaling_zoom.js
new file mode 100644
index 0000000000..168d358fcb
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/browser_test_reset_scaling_zoom.js
@@ -0,0 +1,44 @@
+add_task(async function setup_pref() {
+ let isWindows = navigator.platform.indexOf("Win") == 0;
+ await SpecialPowers.pushPrefEnv({
+ set: [["apz.test.fails_with_native_injection", isWindows]],
+ });
+});
+
+add_task(async function test_main() {
+ function httpURL(filename) {
+ let chromeURL = getRootDirectory(gTestPath) + filename;
+ return chromeURL.replace(
+ "chrome://mochitests/content/",
+ "http://mochi.test:8888/"
+ );
+ }
+
+ const pageUrl = httpURL("helper_test_reset_scaling_zoom.html");
+
+ await BrowserTestUtils.withNewTab(pageUrl, async function (browser) {
+ let getResolution = function () {
+ return content.window.SpecialPowers.getDOMWindowUtils(
+ content.window
+ ).getResolution();
+ };
+
+ let doZoomIn = async function () {
+ await content.window.wrappedJSObject.doZoomIn();
+ };
+
+ let resolution = await ContentTask.spawn(browser, null, getResolution);
+ is(resolution, 1.0, "Initial page resolution should be 1.0");
+
+ await ContentTask.spawn(browser, null, doZoomIn);
+ resolution = await ContentTask.spawn(browser, null, getResolution);
+ isnot(resolution, 1.0, "Expected resolution to be bigger than 1.0");
+
+ document.getElementById("cmd_fullZoomReset").doCommand();
+ // Spin the event loop once just to make sure the message gets through
+ await new Promise(resolve => setTimeout(resolve, 0));
+
+ resolution = await ContentTask.spawn(browser, null, getResolution);
+ is(resolution, 1.0, "Expected resolution to be reset to 1.0");
+ });
+});
diff --git a/gfx/layers/apz/test/mochitest/browser_test_scroll_thumb_dragging.js b/gfx/layers/apz/test/mochitest/browser_test_scroll_thumb_dragging.js
new file mode 100644
index 0000000000..bd2d733a32
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/browser_test_scroll_thumb_dragging.js
@@ -0,0 +1,77 @@
+add_task(async function () {
+ function httpURL(filename) {
+ let chromeURL = getRootDirectory(gTestPath) + filename;
+ return chromeURL.replace(
+ "chrome://mochitests/content/",
+ "http://mochi.test:8888/"
+ );
+ }
+
+ const newWin = await BrowserTestUtils.openNewBrowserWindow();
+
+ const pageUrl = httpURL("helper_scroll_thumb_dragging.html");
+ const tab = await BrowserTestUtils.openNewForegroundTab(
+ newWin.gBrowser,
+ pageUrl
+ );
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ await content.wrappedJSObject.promiseApzFlushedRepaints();
+ await content.wrappedJSObject.waitUntilApzStable();
+ });
+
+ // Send an explicit click event to make sure the new window accidentally
+ // doesn't get an "enter-notify-event" on Linux during dragging, the event
+ // forcibly cancels the dragging state.
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ // Creating an object in this content privilege so that the object
+ // properties can be accessed in below
+ // promiseNativeMouseEventWithAPZAndWaitForEvent function.
+ const moveParams = content.window.eval(`({
+ target: window,
+ type: "mousemove",
+ offsetX: 10,
+ offsetY: 10
+ })`);
+ const clickParams = content.window.eval(`({
+ target: window,
+ type: "click",
+ offsetX: 10,
+ offsetY: 10
+ })`);
+ // Send a mouse move event first to make sure the "enter-notify-event"
+ // happens.
+ await content.wrappedJSObject.promiseNativeMouseEventWithAPZAndWaitForEvent(
+ moveParams
+ );
+ await content.wrappedJSObject.promiseNativeMouseEventWithAPZAndWaitForEvent(
+ clickParams
+ );
+ });
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ const scrollPromise = new Promise(resolve => {
+ content.window.addEventListener("scroll", resolve, { once: true });
+ });
+ const dragFinisher =
+ await content.wrappedJSObject.promiseVerticalScrollbarDrag(
+ content.window,
+ 10,
+ 10
+ );
+
+ await scrollPromise;
+ await dragFinisher();
+
+ await content.wrappedJSObject.promiseApzFlushedRepaints();
+ });
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ ok(
+ content.window.scrollY < 100,
+ "The root scrollable content shouldn't be scrolled too much"
+ );
+ });
+
+ await BrowserTestUtils.closeWindow(newWin);
+});
diff --git a/gfx/layers/apz/test/mochitest/browser_test_scrollbar_in_extension_popup_window.js b/gfx/layers/apz/test/mochitest/browser_test_scrollbar_in_extension_popup_window.js
new file mode 100644
index 0000000000..6e18129845
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/browser_test_scrollbar_in_extension_popup_window.js
@@ -0,0 +1,138 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/browser/components/extensions/test/browser/head.js",
+ this
+);
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/browser/components/extensions/test/browser/head_browserAction.js",
+ this
+);
+
+add_task(async () => {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_action: {
+ default_popup: "popup.html",
+ browser_style: true,
+ },
+ },
+
+ files: {
+ "popup.html": `
+ <html>
+ <head>
+ <meta charset="utf-8">
+ <style>
+ * {
+ padding: 0;
+ margin: 0;
+ }
+ body {
+ height: 400px;
+ width: 200px;
+ overflow-y: auto;
+ overflow-x: hidden;
+ }
+ li {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 30vh;
+ font-size: 200%;
+ }
+ li:nth-child(even){
+ background-color: #ccc;
+ }
+ </style>
+ </head>
+ <body>
+ <ul>
+ <li>1</li>
+ <li>2</li>
+ <li>3</li>
+ <li>4</li>
+ <li>5</li>
+ <li>6</li>
+ <li>7</li>
+ <li>8</li>
+ <li>9</li>
+ <li>10</li>
+ </ul>
+ </body>
+ </html>`,
+ },
+ });
+
+ await extension.startup();
+
+ async function takeSnapshot(browserWin) {
+ let browser = await openBrowserActionPanel(extension, browserWin, true);
+
+ // Ensure there's no pending paint requests.
+ // The below code is a simplified version of promiseAllPaintsDone in
+ // paint_listener.js.
+ await SpecialPowers.spawn(browser, [], async () => {
+ return new Promise(resolve => {
+ function waitForPaints() {
+ // Wait until paint suppression has ended
+ if (SpecialPowers.DOMWindowUtils.paintingSuppressed) {
+ dump`waiting for paint suppression to end...`;
+ content.window.setTimeout(waitForPaints, 0);
+ return;
+ }
+
+ if (SpecialPowers.DOMWindowUtils.isMozAfterPaintPending) {
+ dump`waiting for paint...`;
+ content.window.addEventListener("MozAfterPaint", waitForPaints, {
+ once: true,
+ });
+ return;
+ }
+ resolve();
+ }
+ waitForPaints();
+ });
+ });
+
+ const snapshot = await SpecialPowers.spawn(browser, [], async () => {
+ return SpecialPowers.snapshotWindowWithOptions(
+ content.window,
+ undefined /* use the default rect */,
+ undefined /* use the default bgcolor */,
+ { DRAWWINDOW_DRAW_VIEW: true } /* to capture scrollbars */
+ )
+ .toDataURL()
+ .toString();
+ });
+
+ const popup = getBrowserActionPopup(extension, browserWin);
+ await closeBrowserAction(extension, browserWin);
+ is(popup.state, "closed", "browserAction popup has been closed");
+
+ return snapshot;
+ }
+
+ // First, take a snapshot with disabling APZ in the popup window, we assume
+ // scrollbars are rendered properly there.
+ await SpecialPowers.pushPrefEnv({ set: [["apz.popups.enabled", false]] });
+ const newWin = await BrowserTestUtils.openNewBrowserWindow();
+ const reference = await takeSnapshot(newWin);
+ await BrowserTestUtils.closeWindow(newWin);
+
+ // Then take a snapshot with enabling APZ.
+ await SpecialPowers.pushPrefEnv({ set: [["apz.popups.enabled", true]] });
+ const anotherWin = await BrowserTestUtils.openNewBrowserWindow();
+ const test = await takeSnapshot(anotherWin);
+ await BrowserTestUtils.closeWindow(anotherWin);
+
+ is(
+ test,
+ reference,
+ "Contents in popup window opened by extension should be same regardless of the APZ state in the window"
+ );
+
+ await extension.unload();
+});
diff --git a/gfx/layers/apz/test/mochitest/browser_test_scrolling_in_extension_popup_window.js b/gfx/layers/apz/test/mochitest/browser_test_scrolling_in_extension_popup_window.js
new file mode 100644
index 0000000000..6da3f3311b
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/browser_test_scrolling_in_extension_popup_window.js
@@ -0,0 +1,128 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/browser/components/extensions/test/browser/head.js",
+ this
+);
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/browser/components/extensions/test/browser/head_browserAction.js",
+ this
+);
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/gfx/layers/apz/test/mochitest/apz_test_utils.js",
+ this
+);
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js",
+ this
+);
+
+add_task(async () => {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_action: {
+ default_popup: "popup.html",
+ browser_style: true,
+ },
+ },
+
+ files: {
+ "popup.html": `
+ <html>
+ <head>
+ <meta charset="utf-8">
+ <style>
+ * {
+ padding: 0;
+ margin: 0;
+ }
+ body {
+ height: 400px;
+ width: 200px;
+ overflow-y: auto;
+ overflow-x: hidden;
+ }
+ li {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 30vh;
+ font-size: 200%;
+ }
+ li:nth-child(even){
+ background-color: #ccc;
+ }
+ </style>
+ </head>
+ <body>
+ <ul>
+ <li>1</li>
+ <li>2</li>
+ <li>3</li>
+ <li>4</li>
+ <li>5</li>
+ <li>6</li>
+ <li>7</li>
+ <li>8</li>
+ <li>9</li>
+ <li>10</li>
+ </ul>
+ </body>
+ </html>`,
+ },
+ });
+
+ await extension.startup();
+
+ await SpecialPowers.pushPrefEnv({ set: [["apz.popups.enabled", true]] });
+
+ // Open the popup window of the extension.
+ const browserForPopup = await openBrowserActionPanel(
+ extension,
+ undefined,
+ true
+ );
+
+ // Flush APZ repaints and waits for MozAfterPaint to make sure APZ state is
+ // stable.
+ await promiseApzFlushedRepaintsInPopup(browserForPopup);
+
+ const scrollEventPromise = SpecialPowers.spawn(
+ browserForPopup,
+ [],
+ async () => {
+ return new Promise(resolve => {
+ content.window.addEventListener(
+ "scroll",
+ event => {
+ dump("Got a scroll event in the popup content document\n");
+ resolve();
+ },
+ { once: true }
+ );
+ });
+ }
+ );
+
+ // Send native mouse wheel to scroll the content in the popup.
+ await promiseNativeWheelAndWaitForObserver(browserForPopup, 50, 50, 0, -100);
+
+ // Flush APZ repaints and waits for MozAfterPaint to make sure the scroll has
+ // been reflected on the main thread.
+ const apzPromise = promiseApzFlushedRepaintsInPopup(browserForPopup);
+
+ await Promise.all([apzPromise, scrollEventPromise]);
+
+ const scrollY = await SpecialPowers.spawn(browserForPopup, [], () => {
+ return content.window.scrollY;
+ });
+ ok(scrollY > 0, "Mouse wheel scrolling works in the popup window");
+
+ await closeBrowserAction(extension);
+
+ await extension.unload();
+});
diff --git a/gfx/layers/apz/test/mochitest/browser_test_scrolling_on_inactive_scroller_in_extension_popup_window.js b/gfx/layers/apz/test/mochitest/browser_test_scrolling_on_inactive_scroller_in_extension_popup_window.js
new file mode 100644
index 0000000000..99dccd458c
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/browser_test_scrolling_on_inactive_scroller_in_extension_popup_window.js
@@ -0,0 +1,137 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/browser/components/extensions/test/browser/head.js",
+ this
+);
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/browser/components/extensions/test/browser/head_browserAction.js",
+ this
+);
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/gfx/layers/apz/test/mochitest/apz_test_utils.js",
+ this
+);
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js",
+ this
+);
+
+add_task(async () => {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_action: {
+ default_popup: "popup.html",
+ browser_style: true,
+ },
+ },
+
+ files: {
+ "popup.html": `
+ <html>
+ <head>
+ <meta charset="utf-8">
+ <style>
+ * {
+ padding: 0;
+ margin: 0;
+ }
+ body {
+ height: 400px;
+ width: 200px;
+ }
+ .flex {
+ width: 100vw;
+ display: flex;
+ flex-direction: row;
+ height: 100vh;
+ }
+ .overflow {
+ flex: 1;
+ overflow: auto;
+ height: 100vh;
+ }
+ .overflow div {
+ height: 400vh;
+ }
+ </style>
+ </head>
+ <body>
+ <div class="flex">
+ <div class="overflow"><div>123</div></div>
+ <div class="overflow"><div>123</div></div>
+ </div>
+ </body>
+ </html>`,
+ },
+ });
+
+ await extension.startup();
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["apz.popups.enabled", true],
+ ["apz.wr.activate_all_scroll_frames", false],
+ ["apz.wr.activate_all_scroll_frames_when_fission", false],
+ ],
+ });
+
+ // Open the popup window of the extension.
+ const browserForPopup = await openBrowserActionPanel(
+ extension,
+ undefined,
+ true
+ );
+
+ // Flush APZ repaints and waits for MozAfterPaint to make sure APZ state is
+ // stable.
+ await promiseApzFlushedRepaintsInPopup(browserForPopup);
+
+ // A Promise to wait for one scroll event for each scrollable element.
+ const scrollEventsPromise = SpecialPowers.spawn(browserForPopup, [], () => {
+ let promises = [];
+ content.document.querySelectorAll(".overflow").forEach(element => {
+ let promise = new Promise(resolve => {
+ element.addEventListener(
+ "scroll",
+ () => {
+ resolve();
+ },
+ { once: true }
+ );
+ });
+ promises.push(promise);
+ });
+ return Promise.all(promises);
+ });
+
+ // Send two native mouse wheel events to scroll each scrollable element in the
+ // popup.
+ await promiseNativeWheelAndWaitForObserver(browserForPopup, 50, 50, 0, -100);
+ await promiseNativeWheelAndWaitForObserver(browserForPopup, 150, 50, 0, -100);
+
+ // Flush APZ repaints and waits for MozAfterPaint to make sure the scroll has
+ // been reflected on the main thread.
+ const apzPromise = promiseApzFlushedRepaintsInPopup(browserForPopup);
+
+ await Promise.all([apzPromise, scrollEventsPromise]);
+
+ const scrollTops = await SpecialPowers.spawn(browserForPopup, [], () => {
+ let result = [];
+ content.document.querySelectorAll(".overflow").forEach(element => {
+ result.push(element.scrollTop);
+ });
+ return result;
+ });
+
+ ok(scrollTops[0] > 0, "Mouse wheel scrolling works in the popup window");
+ ok(scrollTops[1] > 0, "Mouse wheel scrolling works in the popup window");
+
+ await closeBrowserAction(extension);
+
+ await extension.unload();
+});
diff --git a/gfx/layers/apz/test/mochitest/browser_test_select_popup_position.js b/gfx/layers/apz/test/mochitest/browser_test_select_popup_position.js
new file mode 100644
index 0000000000..08a6ec9b93
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/browser_test_select_popup_position.js
@@ -0,0 +1,130 @@
+/* This test is a a mash up of
+ https://searchfox.org/mozilla-central/rev/559b25eb41c1cbffcb90a34e008b8288312fcd25/gfx/layers/apz/test/mochitest/browser_test_group_fission.js
+ https://searchfox.org/mozilla-central/rev/559b25eb41c1cbffcb90a34e008b8288312fcd25/gfx/layers/apz/test/mochitest/helper_basic_zoom.html
+ https://searchfox.org/mozilla-central/rev/559b25eb41c1cbffcb90a34e008b8288312fcd25/browser/base/content/test/forms/browser_selectpopup.js
+*/
+
+/* import-globals-from helper_browser_test_utils.js */
+Services.scriptloader.loadSubScript(
+ new URL("helper_browser_test_utils.js", gTestPath).href,
+ this
+);
+
+async function runPopupPositionTest(parentDocumentFileName) {
+ function httpURL(filename) {
+ let chromeURL = getRootDirectory(gTestPath) + filename;
+ return chromeURL.replace(
+ "chrome://mochitests/content/",
+ "http://mochi.test:8888/"
+ );
+ }
+
+ function httpCrossOriginURL(filename) {
+ let chromeURL = getRootDirectory(gTestPath) + filename;
+ return chromeURL.replace(
+ "chrome://mochitests/content/",
+ "http://example.com/"
+ );
+ }
+
+ const pageUrl = httpURL(parentDocumentFileName);
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl);
+
+ // Load the OOP iframe.
+ const iframeUrl = httpCrossOriginURL(
+ "helper_test_select_popup_position.html"
+ );
+ const iframe = await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [iframeUrl],
+ async url => {
+ const target = content.document.querySelector("iframe");
+ target.src = url;
+ await new Promise(resolve => {
+ target.addEventListener("load", resolve, { once: true });
+ });
+ return target.browsingContext;
+ }
+ );
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ await content.wrappedJSObject.promiseApzFlushedRepaints();
+ await content.wrappedJSObject.waitUntilApzStable();
+ });
+
+ const selectRect = await SpecialPowers.spawn(iframe, [], () => {
+ return content.document.querySelector("select").getBoundingClientRect();
+ });
+
+ // Get focus on the select element.
+ await SpecialPowers.spawn(iframe, [], async () => {
+ const select = content.document.querySelector("select");
+ const focusPromise = new Promise(resolve => {
+ select.addEventListener("focus", resolve, { once: true });
+ });
+ select.focus();
+ await focusPromise;
+ });
+
+ const selectPopup = await openSelectPopup();
+
+ const popupRect = selectPopup.getBoundingClientRect();
+ const popupMarginTop = parseFloat(getComputedStyle(selectPopup).marginTop);
+ const popupMarginLeft = parseFloat(getComputedStyle(selectPopup).marginLeft);
+
+ info(
+ `popup rect: (${popupRect.x}, ${popupRect.y}) ${popupRect.width}x${popupRect.height}`
+ );
+ info(`popup margins: ${popupMarginTop} / ${popupMarginLeft}`);
+ info(
+ `select rect: (${selectRect.x}, ${selectRect.y}) ${selectRect.width}x${selectRect.height}`
+ );
+
+ is(
+ popupRect.left - popupMarginLeft,
+ selectRect.x * 2.0,
+ "select popup position x should be scaled by the desktop zoom"
+ );
+
+ // On platforms other than MaxOSX the popup menu is positioned below the
+ // option element.
+ if (!navigator.platform.includes("Mac")) {
+ is(
+ popupRect.top - popupMarginTop,
+ tab.linkedBrowser.getBoundingClientRect().top +
+ (selectRect.y + selectRect.height) * 2.0,
+ "select popup position y should be scaled by the desktop zoom"
+ );
+ } else {
+ // On mac it's aligned to the selected menulist option.
+ const offsetToSelectedItem =
+ selectPopup.querySelector("menuitem[selected]").getBoundingClientRect()
+ .top - popupRect.top;
+ is(
+ popupRect.top - popupMarginTop + offsetToSelectedItem,
+ tab.linkedBrowser.getBoundingClientRect().top + selectRect.y * 2.0,
+ "select popup position y should be scaled by the desktop zoom"
+ );
+ }
+
+ await hideSelectPopup();
+
+ BrowserTestUtils.removeTab(tab);
+}
+
+add_task(async function () {
+ if (!SpecialPowers.useRemoteSubframes) {
+ ok(
+ true,
+ "popup window position in non OOP iframe will be fixed by bug 1691346"
+ );
+ return;
+ }
+ await runPopupPositionTest(
+ "helper_test_select_popup_position_transformed_in_parent.html"
+ );
+});
+
+add_task(async function () {
+ await runPopupPositionTest("helper_test_select_popup_position_zoomed.html");
+});
diff --git a/gfx/layers/apz/test/mochitest/browser_test_select_zoom.js b/gfx/layers/apz/test/mochitest/browser_test_select_zoom.js
new file mode 100644
index 0000000000..84baf8e5ac
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/browser_test_select_zoom.js
@@ -0,0 +1,195 @@
+/* This test is a a mash up of
+ https://searchfox.org/mozilla-central/rev/559b25eb41c1cbffcb90a34e008b8288312fcd25/gfx/layers/apz/test/mochitest/browser_test_group_fission.js
+ https://searchfox.org/mozilla-central/rev/559b25eb41c1cbffcb90a34e008b8288312fcd25/gfx/layers/apz/test/mochitest/helper_basic_zoom.html
+ https://searchfox.org/mozilla-central/rev/559b25eb41c1cbffcb90a34e008b8288312fcd25/browser/base/content/test/forms/browser_selectpopup.js
+*/
+
+/* import-globals-from helper_browser_test_utils.js */
+Services.scriptloader.loadSubScript(
+ new URL("helper_browser_test_utils.js", gTestPath).href,
+ this
+);
+
+add_task(async function setup_pref() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // Dropping the touch slop to 0 makes the tests easier to write because
+ // we can just do a one-pixel drag to get over the pan threshold rather
+ // than having to hard-code some larger value.
+ ["apz.touch_start_tolerance", "0.0"],
+ // The subtests in this test do touch-drags to pan the page, but we don't
+ // want those pans to turn into fling animations, so we increase the
+ // fling-min threshold velocity to an arbitrarily large value.
+ ["apz.fling_min_velocity_threshold", "10000"],
+ ],
+ });
+});
+
+// This test opens a select popup after pinch (apz) zooming has happened.
+add_task(async function () {
+ function httpURL(filename) {
+ let chromeURL = getRootDirectory(gTestPath) + filename;
+ //return chromeURL;
+ return chromeURL.replace(
+ "chrome://mochitests/content/",
+ "http://mochi.test:8888/"
+ );
+ }
+
+ const pageUrl = httpURL("helper_test_select_zoom.html");
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl);
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ const input = content.document.getElementById("select");
+ const focusPromise = new Promise(resolve => {
+ input.addEventListener("focus", resolve, { once: true });
+ });
+ input.focus();
+ await focusPromise;
+ });
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ await content.wrappedJSObject.waitUntilApzStable();
+ });
+
+ const initial_resolution = await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [],
+ () => {
+ return content.window.windowUtils.getResolution();
+ }
+ );
+
+ const initial_rect = await SpecialPowers.spawn(tab.linkedBrowser, [], () => {
+ return content.wrappedJSObject.getSelectRect();
+ });
+
+ ok(
+ initial_resolution > 0,
+ "The initial_resolution is " +
+ initial_resolution +
+ ", which is some sane value"
+ );
+
+ // First, get the position of the select popup when no translations have been applied.
+ const selectPopup = await openSelectPopup();
+
+ let popup_initial_rect = selectPopup.getBoundingClientRect();
+ let popupInitialX = popup_initial_rect.left;
+ let popupInitialY = popup_initial_rect.top;
+
+ await hideSelectPopup();
+
+ ok(popupInitialX > 0, "select position before zooming (x) " + popupInitialX);
+ ok(popupInitialY > 0, "select position before zooming (y) " + popupInitialY);
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ await content.wrappedJSObject.pinchZoomInWithTouch(150, 300);
+ });
+
+ // Flush state and get the resolution we're at now
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ await content.wrappedJSObject.promiseApzFlushedRepaints();
+ });
+
+ const final_resolution = await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [],
+ () => {
+ return content.window.windowUtils.getResolution();
+ }
+ );
+
+ ok(
+ final_resolution > initial_resolution,
+ "The final resolution (" +
+ final_resolution +
+ ") is greater after zooming in"
+ );
+
+ const final_rect = await SpecialPowers.spawn(tab.linkedBrowser, [], () => {
+ return content.wrappedJSObject.getSelectRect();
+ });
+
+ await openSelectPopup();
+
+ let popupRect = selectPopup.getBoundingClientRect();
+ ok(
+ Math.abs(popupRect.left - popupInitialX) > 1,
+ "popup should have moved by more than one pixel (x) " +
+ popupRect.left +
+ " " +
+ popupInitialX
+ );
+ ok(
+ Math.abs(popupRect.top - popupInitialY) > 1,
+ "popup should have moved by more than one pixel (y) " +
+ popupRect.top +
+ " " +
+ popupInitialY
+ );
+
+ ok(
+ Math.abs(
+ final_rect.left - initial_rect.left - (popupRect.left - popupInitialX)
+ ) < 1,
+ "popup should have moved approximately the same as the element (x)"
+ );
+ let tolerance = navigator.platform.includes("Linux") ? final_rect.height : 1;
+ ok(
+ Math.abs(
+ final_rect.top - initial_rect.top - (popupRect.top - popupInitialY)
+ ) < tolerance,
+ "popup should have moved approximately the same as the element (y)"
+ );
+
+ ok(
+ true,
+ "initial " +
+ initial_rect.left +
+ " " +
+ initial_rect.top +
+ " " +
+ initial_rect.width +
+ " " +
+ initial_rect.height
+ );
+ ok(
+ true,
+ "final " +
+ final_rect.left +
+ " " +
+ final_rect.top +
+ " " +
+ final_rect.width +
+ " " +
+ final_rect.height
+ );
+
+ ok(
+ true,
+ "initial popup " +
+ popup_initial_rect.left +
+ " " +
+ popup_initial_rect.top +
+ " " +
+ popup_initial_rect.width +
+ " " +
+ popup_initial_rect.height
+ );
+ ok(
+ true,
+ "final popup " +
+ popupRect.left +
+ " " +
+ popupRect.top +
+ " " +
+ popupRect.width +
+ " " +
+ popupRect.height
+ );
+
+ await hideSelectPopup();
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/gfx/layers/apz/test/mochitest/browser_test_tab_drag_zoom.js b/gfx/layers/apz/test/mochitest/browser_test_tab_drag_zoom.js
new file mode 100644
index 0000000000..e421e7bd3c
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/browser_test_tab_drag_zoom.js
@@ -0,0 +1,103 @@
+/* This test is a a mash up of
+ https://searchfox.org/mozilla-central/rev/016925857e2f81a9425de9e03021dcf4251cafcc/gfx/layers/apz/test/mochitest/browser_test_select_zoom.js
+ https://searchfox.org/mozilla-central/rev/016925857e2f81a9425de9e03021dcf4251cafcc/browser/base/content/test/general/browser_tab_drag_drop_perwindow.js
+*/
+
+const EVENTUTILS_URL =
+ "chrome://mochikit/content/tests/SimpleTest/EventUtils.js";
+var EventUtils = {};
+
+Services.scriptloader.loadSubScript(EVENTUTILS_URL, EventUtils);
+
+add_task(async function test_dragging_zoom_handling() {
+ function httpURL(filename) {
+ let chromeURL = getRootDirectory(gTestPath) + filename;
+ //return chromeURL;
+ return chromeURL.replace(
+ "chrome://mochitests/content/",
+ "http://mochi.test:8888/"
+ );
+ }
+
+ const pageUrl = httpURL("helper_test_tab_drag_zoom.html");
+
+ let win1 = await BrowserTestUtils.openNewBrowserWindow();
+ let win2 = await BrowserTestUtils.openNewBrowserWindow();
+
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(win1.gBrowser);
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(
+ win2.gBrowser,
+ pageUrl
+ );
+
+ await SpecialPowers.spawn(tab2.linkedBrowser, [], async () => {
+ await content.wrappedJSObject.waitUntilApzStable();
+ });
+
+ const initial_resolution = await SpecialPowers.spawn(
+ tab2.linkedBrowser,
+ [],
+ () => {
+ return content.window.windowUtils.getResolution();
+ }
+ );
+
+ ok(
+ initial_resolution > 0,
+ "The initial_resolution is " +
+ initial_resolution +
+ ", which is some sane value"
+ );
+
+ let effect = EventUtils.synthesizeDrop(
+ tab2,
+ tab1,
+ [[{ type: TAB_DROP_TYPE, data: tab2 }]],
+ null,
+ win2,
+ win1
+ );
+ is(effect, "move", "Tab should be moved from win2 to win1.");
+
+ await SpecialPowers.spawn(win1.gBrowser.selectedBrowser, [], async () => {
+ await content.wrappedJSObject.waitUntilApzStable();
+ });
+
+ let resolution = await SpecialPowers.spawn(
+ win1.gBrowser.selectedBrowser,
+ [],
+ () => {
+ return content.window.windowUtils.getResolution();
+ }
+ );
+
+ ok(
+ resolution == initial_resolution,
+ "The resolution (" + resolution + ") is the same after tab dragging"
+ );
+
+ await SpecialPowers.spawn(win1.gBrowser.selectedBrowser, [], async () => {
+ await content.wrappedJSObject.pinchZoomInWithTouch(150, 300);
+ });
+
+ // Flush state and get the resolution we're at now
+ await SpecialPowers.spawn(win1.gBrowser.selectedBrowser, [], async () => {
+ await content.wrappedJSObject.promiseApzFlushedRepaints();
+ });
+
+ resolution = await SpecialPowers.spawn(
+ win1.gBrowser.selectedBrowser,
+ [],
+ () => {
+ return content.window.windowUtils.getResolution();
+ }
+ );
+
+ ok(
+ resolution > initial_resolution,
+ "The resolution (" + resolution + ") is greater after zooming in"
+ );
+
+ await BrowserTestUtils.closeWindow(win1);
+ await BrowserTestUtils.closeWindow(win2);
+});
diff --git a/gfx/layers/apz/test/mochitest/green100x100.png b/gfx/layers/apz/test/mochitest/green100x100.png
new file mode 100644
index 0000000000..7df25f33bd
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/green100x100.png
Binary files differ
diff --git a/gfx/layers/apz/test/mochitest/helper_background_tab_load_scroll.html b/gfx/layers/apz/test/mochitest/helper_background_tab_load_scroll.html
new file mode 100644
index 0000000000..4769861b2a
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_background_tab_load_scroll.html
@@ -0,0 +1,147 @@
+<!DOCTYPE html>
+<html>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/paint_listener.js"></script>
+<script src="apz_test_utils.js"></script>
+<script src="apz_test_native_event_utils.js"></script>
+
+<script>
+function addTextToLastDiv() {
+ let alldivs = document.getElementsByTagName('div');
+ let lastdiv = alldivs[alldivs.length-1];
+ for (let i = 0; i < 225; i++) {
+ lastdiv.appendChild(document.createTextNode("Text text text text text text text text text text text text text text text text "));
+ }
+}
+
+function doload() {
+ window.scrollBy(0,10000);
+ document.documentElement.offsetLeft;
+}
+</script>
+<body onload="doload()">
+<div>
+</div>
+<script>
+addTextToLastDiv();
+</script>
+<!-- We use display none and then toggle to regular display in an inline script
+ at the end of our content to try to make sure we generate some reflows
+ which will generate some ScrollToImpl calls with origin restore which are
+ necessary to reproduce the bug. -->
+<div style="display: none;">
+</div>
+<script>
+addTextToLastDiv();
+</script>
+<div>
+</div>
+<script>
+addTextToLastDiv();
+</script>
+<div style="display: none;">
+</div>
+<script>
+addTextToLastDiv();
+</script>
+<div>
+</div>
+<script>
+addTextToLastDiv();
+</script>
+<div style="display: none;">
+</div>
+<script>
+addTextToLastDiv();
+</script>
+<div>
+</div>
+<script>
+addTextToLastDiv();
+</script>
+<div style="display: none;">
+</div>
+<script>
+addTextToLastDiv();
+</script>
+<div>
+</div>
+<script>
+addTextToLastDiv();
+</script>
+<div style="display: none;">
+</div>
+<script>
+addTextToLastDiv();
+</script>
+<div>
+</div>
+<script>
+addTextToLastDiv();
+</script>
+<div style="display: none;">
+</div>
+<script>
+addTextToLastDiv();
+</script>
+<div>
+</div>
+<script>
+addTextToLastDiv();
+</script>
+<div style="display: none;">
+</div>
+<script>
+addTextToLastDiv();
+</script>
+<div>
+</div>
+<script>
+addTextToLastDiv();
+</script>
+<div style="display: none;">
+</div>
+<script>
+addTextToLastDiv();
+</script>
+<div>
+</div>
+<script>
+addTextToLastDiv();
+</script>
+<div style="display: none;">
+</div>
+<script>
+addTextToLastDiv();
+</script>
+<div>
+</div>
+<script>
+addTextToLastDiv();
+</script>
+<div style="display: none;">
+</div>
+<script>
+addTextToLastDiv();
+</script>
+<div>
+</div>
+<script>
+addTextToLastDiv();
+</script>
+<div style="display: none;">
+</div>
+<script>
+addTextToLastDiv();
+</script>
+
+
+<script>
+let alldivs = document.getElementsByTagName('div');
+for (let i = 0 ; i < alldivs.length; i++) {
+ alldivs[i].style.display = "";
+ document.documentElement.offsetLeft;
+}
+</script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_background_tab_scroll.html b/gfx/layers/apz/test/mochitest/helper_background_tab_scroll.html
new file mode 100644
index 0000000000..f55a55f0fc
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_background_tab_scroll.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<script src="apz_test_native_event_utils.js"></script>
+<script src="apz_test_utils.js"></script>
+<style>
+body, html {
+ margin: 0;
+}
+</style>
+<div id="scrolltarget" style="margin-top: 5000px; height: 5000px">#scrolltarget</div>
diff --git a/gfx/layers/apz/test/mochitest/helper_basic_onetouchpinch.html b/gfx/layers/apz/test/mochitest/helper_basic_onetouchpinch.html
new file mode 100644
index 0000000000..1eb1d3dd03
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_basic_onetouchpinch.html
@@ -0,0 +1,90 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width">
+ <title>Sanity check for one-touch pinch zooming</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="application/javascript">
+
+async function test() {
+ let visResEvt = new EventCounter(window.visualViewport, "resize");
+ let visScrEvt = new EventCounter(window.visualViewport, "scroll");
+ // Our internal visual viewport events aren't restricted to the visual view-
+ // port itself, so we can listen on the window itself, however the event
+ // listener needs to be in the system group.
+ let visResEvtInternal = new EventCounter(window, "mozvisualresize",
+ { mozSystemGroup: true });
+ let visScrEvtInternal = new EventCounter(window, "mozvisualscroll",
+ { mozSystemGroup: true });
+ let visResEvtContent = new EventCounter(window, "mozvisualresize");
+ let visScrEvtContent = new EventCounter(window, "mozvisualscroll");
+
+ var initial_resolution = await getResolution();
+ ok(initial_resolution > 0,
+ "The initial_resolution is " + initial_resolution + ", which is some sane value");
+
+ // This listener will trigger the test to continue once APZ is done with
+ // processing the scroll.
+ let transformEndPromise = promiseTransformEnd();
+
+ var zoom_in = [
+ [ { x: 150, y: 300 } ],
+ [ null ],
+ [ { x: 150, y: 300 } ],
+ [ { x: 150, y: 305 } ],
+ [ { x: 150, y: 310 } ],
+ [ { x: 150, y: 315 } ],
+ [ { x: 150, y: 320 } ],
+ [ { x: 150, y: 325 } ],
+ ];
+
+ var touchIds = [0];
+ await synthesizeNativeTouchSequences(document.body, zoom_in, null, touchIds);
+
+ // Wait for the APZ:TransformEnd to be fired after touch events are processed.
+ await transformEndPromise;
+
+ // Flush state and get the resolution we're at now
+ await promiseApzFlushedRepaints();
+ let final_resolution = await getResolution();
+ ok(final_resolution > initial_resolution, "The final resolution (" + final_resolution + ") is greater after zooming in");
+
+ // Check we've got the expected events.
+ // Zooming the page should fire visual viewport resize events:
+ visResEvt.unregister();
+ ok(visResEvt.count > 0, "Got some visual viewport resize events");
+ visResEvtInternal.unregister();
+ ok(visResEvtInternal.count > 0, "Got some mozvisualresize events");
+
+ // We're zooming somewhere in the middle of the page, so the visual
+ // viewport's coordinates change, too.
+ // This is true both relative to the page (mozvisualscroll), as well as
+ // relative to the layout viewport (visual viewport "scroll" event).
+ visScrEvt.unregister();
+ ok(visScrEvt.count > 0, "Got some visual viewport scroll events");
+ visScrEvtInternal.unregister();
+ ok(visScrEvtInternal.count > 0, "Got some mozvisualscroll events");
+
+ // Our internal events shouldn't leak to normal content.
+ visResEvtContent.unregister();
+ is(visResEvtContent.count, 0, "Got no mozvisualresize events in content");
+ visScrEvtContent.unregister();
+ is(visScrEvtContent.count, 0, "Got no mozvisualscroll events in content");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</head>
+<body>
+ Here is some text to stare at as the test runs. It serves no functional
+ purpose, but gives you an idea of the zoom level. It's harder to tell what
+ the zoom level is when the page is just solid white.
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_basic_pan.html b/gfx/layers/apz/test/mochitest/helper_basic_pan.html
new file mode 100644
index 0000000000..db96e34a70
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_basic_pan.html
@@ -0,0 +1,73 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Sanity panning test</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="application/javascript">
+
+async function test() {
+ let scrEvt = new EventCounter(window, "scroll");
+ let visScrEvt = new EventCounter(window.visualViewport, "scroll");
+ // Our internal visual viewport events aren't restricted to the visual view-
+ // port itself, so we can listen on the window itself, however the event
+ // listener needs to be in the system group.
+ let visScrEvtInternal = new EventCounter(window, "mozvisualscroll",
+ { mozSystemGroup: true });
+
+ // This listener will trigger the test to continue once APZ is done with
+ // processing the scroll.
+ let transformEndPromise = promiseTransformEnd();
+
+ await synthesizeNativeTouchDrag(document.body, 10, 100, 0, -50);
+ dump("Finished native drag, waiting for transform-end observer...\n");
+
+ // Wait for the APZ:TransformEnd to be fired after touch events are processed.
+ await transformEndPromise;
+
+ // Flush state.
+ await promiseApzFlushedRepaints();
+
+ is(window.scrollY, 50, "check that the window scrolled");
+
+ // Check we've got the expected events.
+ // This page is using "width=device-width; initial-scale=1.0" and we haven't
+ // pinch-zoomed any further, so layout and visual viewports have the same
+ // size and will scroll together. Therefore we should be getting layout
+ // viewport "scroll" events as well.
+ scrEvt.unregister();
+ ok(scrEvt.count > 0, "Got some layout viewport scroll events");
+ // This one is a bit tricky: Visual viewport "scroll" events are supposed to
+ // fire only when the relative offset between layout and visual viewport
+ // changes. Even when they're both scrolling together, we may update their
+ // positions independently, though, leading to some jitter in the offset and
+ // triggering the event after all.
+ // At least for the case here, where both viewports are the same size and we
+ // have a freshly loaded page, we should however be able to keep the offset at
+ // a constant zero and therefore not cause any visual viewport scroll events
+ // to fire.
+ visScrEvt.unregister();
+ is(visScrEvt.count, 0, "Got no visual viewport scroll events");
+ visScrEvtInternal.unregister();
+ // Our internal visual viewport scroll event on the other hand only cares
+ // about the absolute offset of the visual viewport and should therefore
+ // definitively fire.
+ ok(visScrEvtInternal.count > 0, "Got some mozvisualscroll events");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</head>
+<body>
+ <div style="height: 5000px; background-color: lightgreen;">
+ This div makes the page scrollable.
+ </div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_basic_scrollend.html b/gfx/layers/apz/test/mochitest/helper_basic_scrollend.html
new file mode 100644
index 0000000000..9d71fe6251
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_basic_scrollend.html
@@ -0,0 +1,92 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <script src="apz_test_utils.js"></script>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+ html, body { margin: 0; }
+
+ body {
+ height: 10000px;
+ }
+ </style>
+ <script>
+const searchParams = new URLSearchParams(location.search);
+
+async function test() {
+ var scrollendCount = 0;
+
+ function onScrollend(e) {
+ scrollendCount += 1;
+ }
+
+ switch (searchParams.get("chrome-only")) {
+ case "true":
+ // Add chrome-only event listener.
+ SpecialPowers.addChromeEventListener("scrollend", onScrollend, true);
+ break;
+ case "false":
+ // Add document event listener.
+ document.addEventListener("scrollend", onScrollend);
+ break;
+ default:
+ ok(false, "Unsupported chrome-only value: " + searchParams.get("chrome-only"));
+ break;
+ }
+
+ is(scrollendCount, 0, "A scrollend event should not be triggered yet");
+
+ await promiseFrame();
+
+ let wheelScrollTransformEndPromise = promiseTransformEnd();
+
+ await promiseMoveMouseAndScrollWheelOver(document.scrollingElement, 100, 100);
+
+ await wheelScrollTransformEndPromise;
+
+ await promiseFrame();
+
+ is(scrollendCount, 1, "A scrollend event should be triggered after user scroll");
+
+ scrollendCount = 0;
+
+ // Call the scrollTo function without behavior: smooth to trigger an instant
+ // programatic scroll.
+ scrollTo({ top: 500, left: 0 });
+
+ // Ensure the refresh driver has ticked.
+ await promiseFrame();
+
+ // A scrollend event should be posted after the refresh driver has ticked.
+ is(scrollendCount, 1, "A scrollend event should be triggered after instant scroll");
+
+ // If smooth scrolls are enabled, repeat the test with a smooth scroll.
+ if (SpecialPowers.getBoolPref("general.smoothScroll")) {
+
+ scrollendCount = 0;
+
+ let smoothScrollTransformEndPromise = promiseTransformEnd();
+
+ // Call the scrollTo function with behavior: smooth to trigger a programmatic
+ // scroll that should require some form of async transform.
+ scrollTo({ top: 1000, left: 0, behavior: "smooth" });
+
+ // Ensure the smooth scroll transform has finished.
+ await smoothScrollTransformEndPromise;
+
+ await promiseFrame();
+
+ is(scrollendCount, 1, "A scrollend event should be triggered after smooth scroll");
+ }
+}
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_basic_zoom.html b/gfx/layers/apz/test/mochitest/helper_basic_zoom.html
new file mode 100644
index 0000000000..55717b31a4
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_basic_zoom.html
@@ -0,0 +1,71 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width">
+ <title>Sanity check for zooming</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="application/javascript">
+
+async function test() {
+ let visResEvt = new EventCounter(window.visualViewport, "resize");
+ let visScrEvt = new EventCounter(window.visualViewport, "scroll");
+ // Our internal visual viewport events aren't restricted to the visual view-
+ // port itself, so we can listen on the window itself, however the event
+ // listener needs to be in the system group.
+ let visResEvtInternal = new EventCounter(window, "mozvisualresize",
+ { mozSystemGroup: true });
+ let visScrEvtInternal = new EventCounter(window, "mozvisualscroll",
+ { mozSystemGroup: true });
+ let visResEvtContent = new EventCounter(window, "mozvisualresize");
+ let visScrEvtContent = new EventCounter(window, "mozvisualscroll");
+
+ var initial_resolution = await getResolution();
+ ok(initial_resolution > 0,
+ "The initial_resolution is " + initial_resolution + ", which is some sane value");
+
+ await pinchZoomInWithTouch(150, 300);
+
+ // Flush state and get the resolution we're at now
+ await promiseApzFlushedRepaints();
+ let final_resolution = await getResolution();
+ ok(final_resolution > initial_resolution, "The final resolution (" + final_resolution + ") is greater after zooming in");
+
+ // Check we've got the expected events.
+ // Pinch-zooming the page should fire visual viewport resize events:
+ visResEvt.unregister();
+ ok(visResEvt.count > 0, "Got some visual viewport resize events");
+ visResEvtInternal.unregister();
+ ok(visResEvtInternal.count > 0, "Got some mozvisualresize events");
+
+ // We're pinch-zooming somewhere in the middle of the page, so the visual
+ // viewport's coordinates change, too.
+ // This is true both relative to the page (mozvisualscroll), as well as
+ // relative to the layout viewport (visual viewport "scroll" event).
+ visScrEvt.unregister();
+ ok(visScrEvt.count > 0, "Got some visual viewport scroll events");
+ visScrEvtInternal.unregister();
+ ok(visScrEvtInternal.count > 0, "Got some mozvisualscroll events");
+
+ // Our internal events shouldn't leak to normal content.
+ visResEvtContent.unregister();
+ is(visResEvtContent.count, 0, "Got no mozvisualresize events in content");
+ visScrEvtContent.unregister();
+ is(visScrEvtContent.count, 0, "Got no mozvisualscroll events in content");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</head>
+<body>
+ Here is some text to stare at as the test runs. It serves no functional
+ purpose, but gives you an idea of the zoom level. It's harder to tell what
+ the zoom level is when the page is just solid white.
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_browser_test_utils.js b/gfx/layers/apz/test/mochitest/helper_browser_test_utils.js
new file mode 100644
index 0000000000..ac68c9b1d4
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_browser_test_utils.js
@@ -0,0 +1,11 @@
+// For hideSelectPopup.
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/browser/base/content/test/forms/head.js",
+ this
+);
+
+function openSelectPopup(selector = "select", win = window) {
+ let popupShownPromise = BrowserTestUtils.waitForSelectPopupShown(win);
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }, win);
+ return popupShownPromise;
+}
diff --git a/gfx/layers/apz/test/mochitest/helper_bug1162771.html b/gfx/layers/apz/test/mochitest/helper_bug1162771.html
new file mode 100644
index 0000000000..3503341d41
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1162771.html
@@ -0,0 +1,107 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Test for touchend on media elements</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+async function handleTouchStart() {
+ let v = document.getElementById("video");
+ let d = document.getElementById("div");
+
+ let e = await new Promise(resolve => {
+ document.body.addEventListener("touchstart", resolve, {once: true});
+ });
+
+ if (e.target === v || e.target === d) {
+ e.target.style.display = "none";
+ ok(true, "Set display to none on #" + e.target.id);
+ } else {
+ ok(false, "Got unexpected touchstart on " + e.target);
+ }
+ await promiseAllPaintsDone();
+}
+
+async function handleTouchEnd() {
+ let v = document.getElementById("video");
+ let d = document.getElementById("div");
+
+ let e = await new Promise(resolve => {
+ document.body.addEventListener("touchend", resolve, {once: true});
+ });
+
+ if (e.target === v || e.target === d) {
+ e.target._gotTouchend = true;
+ ok(true, "Got touchend event on #" + e.target.id);
+ }
+}
+
+async function test() {
+ var v = document.getElementById("video");
+ var d = document.getElementById("div");
+
+ var utils = SpecialPowers.getDOMWindowUtils(window);
+
+ let startHandledPromise = handleTouchStart();
+ let endHandledPromise = handleTouchEnd();
+ var pt = await coordinatesRelativeToScreen({ offsetX: 25, offsetY: 5, target: v });
+ utils.sendNativeTouchPoint(0, SpecialPowers.DOMWindowUtils.TOUCH_CONTACT, pt.x, pt.y, 1, 90, null);
+ await startHandledPromise;
+ utils.sendNativeTouchPoint(0, SpecialPowers.DOMWindowUtils.TOUCH_REMOVE, pt.x, pt.y, 1, 90, null);
+ await endHandledPromise;
+ ok(v._gotTouchend, "Touchend was received on video element");
+
+ startHandledPromise = handleTouchStart();
+ endHandledPromise = handleTouchEnd();
+ pt = await coordinatesRelativeToScreen({ offsetX: 25, offsetY: 5, target: d });
+ utils.sendNativeTouchPoint(0, SpecialPowers.DOMWindowUtils.TOUCH_CONTACT, pt.x, pt.y, 1, 90, null);
+ await startHandledPromise;
+ utils.sendNativeTouchPoint(0, SpecialPowers.DOMWindowUtils.TOUCH_REMOVE, pt.x, pt.y, 1, 90, null);
+ await endHandledPromise;
+ ok(d._gotTouchend, "Touchend was received on div element");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+ <style>
+ * {
+ font-size: 24px;
+ box-sizing: border-box;
+ }
+
+ #video {
+ display:block;
+ position:absolute;
+ top: 100px;
+ left:0;
+ width: 50%;
+ height: 100px;
+ border:solid black 1px;
+ background-color: #8a8;
+ }
+
+ #div {
+ display:block;
+ position:absolute;
+ top: 100px;
+ left: 50%;
+ width: 50%;
+ height: 100px;
+ border:solid black 1px;
+ background-color: #88a;
+ }
+ </style>
+</head>
+<body>
+ <p>Tap on the colored boxes to hide them.</p>
+ <video id="video"></video>
+ <div id="div"></div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_bug1271432.html b/gfx/layers/apz/test/mochitest/helper_bug1271432.html
new file mode 100644
index 0000000000..1e40421a8f
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1271432.html
@@ -0,0 +1,573 @@
+<head>
+ <title>Ensure that the hit region doesn't get unexpectedly expanded</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+<script type="application/javascript">
+async function test() {
+ var scroller = document.getElementById("scroller");
+ var scrollerPos = scroller.scrollTop;
+ var dx = 100, dy = 50;
+
+ is(window.scrollY, 0, "Initial page scroll position should be 0");
+ is(scrollerPos, 0, "Initial scroller position should be 0");
+
+ await promiseMoveMouseAndScrollWheelOver(scroller, dx, dy);
+
+ is(window.scrollY, 0, "Page scroll position should still be 0");
+ ok(scroller.scrollTop > scrollerPos, "Scroller should have scrolled");
+
+ // wait for it to layerize fully and then try again
+ await promiseAllPaintsDone();
+ await promiseOnlyApzControllerFlushed();
+ scrollerPos = scroller.scrollTop;
+
+ await promiseMoveMouseAndScrollWheelOver(scroller, dx, dy);
+ is(window.scrollY, 0, "Page scroll position should still be 0 after layerization");
+ ok(scroller.scrollTop > scrollerPos, "Scroller should have continued scrolling");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+</script>
+<style>
+a#with_after_content {
+ background-color: #F16725;
+ opacity: 0.8;
+ display: inline-block;
+ margin-top: 40px;
+ margin-left: 40px;
+}
+a#with_after_content::after {
+ content: " ";
+ position: absolute;
+ width: 0px;
+ height: 0px;
+ bottom: 40px;
+ z-index: -1;
+ right: 40px;
+ background-color: transparent;
+ border-style: solid;
+ border-width: 15px 15px 15px 0;
+ border-color: #d54e0e transparent transparent transparent;
+ box-shadow: none;
+ box-sizing: border-box;
+}
+div#scroller {
+ overflow-y: scroll;
+ width: 50%;
+ height: 50%;
+}
+</style>
+</head>
+<body>
+<a id="with_after_content">Some text</a>
+
+<div id="scroller">
+Scrolling on the very left edge of this div will work.
+Scrolling on the right side of this div (starting with the left edge of the orange box above) should work, but doesn't.<br/>
+0<br>
+1<br>
+2<br>
+3<br>
+4<br>
+5<br>
+6<br>
+7<br>
+8<br>
+9<br>
+10<br>
+11<br>
+12<br>
+13<br>
+14<br>
+15<br>
+16<br>
+17<br>
+18<br>
+19<br>
+20<br>
+21<br>
+22<br>
+23<br>
+24<br>
+25<br>
+26<br>
+27<br>
+28<br>
+29<br>
+30<br>
+31<br>
+32<br>
+33<br>
+34<br>
+35<br>
+36<br>
+37<br>
+38<br>
+39<br>
+40<br>
+41<br>
+42<br>
+43<br>
+44<br>
+45<br>
+46<br>
+47<br>
+48<br>
+49<br>
+50<br>
+51<br>
+52<br>
+53<br>
+54<br>
+55<br>
+56<br>
+57<br>
+58<br>
+59<br>
+60<br>
+61<br>
+62<br>
+63<br>
+64<br>
+65<br>
+66<br>
+67<br>
+68<br>
+69<br>
+70<br>
+71<br>
+72<br>
+73<br>
+74<br>
+75<br>
+76<br>
+77<br>
+78<br>
+79<br>
+80<br>
+81<br>
+82<br>
+83<br>
+84<br>
+85<br>
+86<br>
+87<br>
+88<br>
+89<br>
+90<br>
+91<br>
+92<br>
+93<br>
+94<br>
+95<br>
+96<br>
+97<br>
+98<br>
+99<br>
+100<br>
+101<br>
+102<br>
+103<br>
+104<br>
+105<br>
+106<br>
+107<br>
+108<br>
+109<br>
+110<br>
+111<br>
+112<br>
+113<br>
+114<br>
+115<br>
+116<br>
+117<br>
+118<br>
+119<br>
+120<br>
+121<br>
+122<br>
+123<br>
+124<br>
+125<br>
+126<br>
+127<br>
+128<br>
+129<br>
+130<br>
+131<br>
+132<br>
+133<br>
+134<br>
+135<br>
+136<br>
+137<br>
+138<br>
+139<br>
+140<br>
+141<br>
+142<br>
+143<br>
+144<br>
+145<br>
+146<br>
+147<br>
+148<br>
+149<br>
+150<br>
+151<br>
+152<br>
+153<br>
+154<br>
+155<br>
+156<br>
+157<br>
+158<br>
+159<br>
+160<br>
+161<br>
+162<br>
+163<br>
+164<br>
+165<br>
+166<br>
+167<br>
+168<br>
+169<br>
+170<br>
+171<br>
+172<br>
+173<br>
+174<br>
+175<br>
+176<br>
+177<br>
+178<br>
+179<br>
+180<br>
+181<br>
+182<br>
+183<br>
+184<br>
+185<br>
+186<br>
+187<br>
+188<br>
+189<br>
+190<br>
+191<br>
+192<br>
+193<br>
+194<br>
+195<br>
+196<br>
+197<br>
+198<br>
+199<br>
+200<br>
+201<br>
+202<br>
+203<br>
+204<br>
+205<br>
+206<br>
+207<br>
+208<br>
+209<br>
+210<br>
+211<br>
+212<br>
+213<br>
+214<br>
+215<br>
+216<br>
+217<br>
+218<br>
+219<br>
+220<br>
+221<br>
+222<br>
+223<br>
+224<br>
+225<br>
+226<br>
+227<br>
+228<br>
+229<br>
+230<br>
+231<br>
+232<br>
+233<br>
+234<br>
+235<br>
+236<br>
+237<br>
+238<br>
+239<br>
+240<br>
+241<br>
+242<br>
+243<br>
+244<br>
+245<br>
+246<br>
+247<br>
+248<br>
+249<br>
+250<br>
+251<br>
+252<br>
+253<br>
+254<br>
+255<br>
+256<br>
+257<br>
+258<br>
+259<br>
+260<br>
+261<br>
+262<br>
+263<br>
+264<br>
+265<br>
+266<br>
+267<br>
+268<br>
+269<br>
+270<br>
+271<br>
+272<br>
+273<br>
+274<br>
+275<br>
+276<br>
+277<br>
+278<br>
+279<br>
+280<br>
+281<br>
+282<br>
+283<br>
+284<br>
+285<br>
+286<br>
+287<br>
+288<br>
+289<br>
+290<br>
+291<br>
+292<br>
+293<br>
+294<br>
+295<br>
+296<br>
+297<br>
+298<br>
+299<br>
+300<br>
+301<br>
+302<br>
+303<br>
+304<br>
+305<br>
+306<br>
+307<br>
+308<br>
+309<br>
+310<br>
+311<br>
+312<br>
+313<br>
+314<br>
+315<br>
+316<br>
+317<br>
+318<br>
+319<br>
+320<br>
+321<br>
+322<br>
+323<br>
+324<br>
+325<br>
+326<br>
+327<br>
+328<br>
+329<br>
+330<br>
+331<br>
+332<br>
+333<br>
+334<br>
+335<br>
+336<br>
+337<br>
+338<br>
+339<br>
+340<br>
+341<br>
+342<br>
+343<br>
+344<br>
+345<br>
+346<br>
+347<br>
+348<br>
+349<br>
+350<br>
+351<br>
+352<br>
+353<br>
+354<br>
+355<br>
+356<br>
+357<br>
+358<br>
+359<br>
+360<br>
+361<br>
+362<br>
+363<br>
+364<br>
+365<br>
+366<br>
+367<br>
+368<br>
+369<br>
+370<br>
+371<br>
+372<br>
+373<br>
+374<br>
+375<br>
+376<br>
+377<br>
+378<br>
+379<br>
+380<br>
+381<br>
+382<br>
+383<br>
+384<br>
+385<br>
+386<br>
+387<br>
+388<br>
+389<br>
+390<br>
+391<br>
+392<br>
+393<br>
+394<br>
+395<br>
+396<br>
+397<br>
+398<br>
+399<br>
+400<br>
+401<br>
+402<br>
+403<br>
+404<br>
+405<br>
+406<br>
+407<br>
+408<br>
+409<br>
+410<br>
+411<br>
+412<br>
+413<br>
+414<br>
+415<br>
+416<br>
+417<br>
+418<br>
+419<br>
+420<br>
+421<br>
+422<br>
+423<br>
+424<br>
+425<br>
+426<br>
+427<br>
+428<br>
+429<br>
+430<br>
+431<br>
+432<br>
+433<br>
+434<br>
+435<br>
+436<br>
+437<br>
+438<br>
+439<br>
+440<br>
+441<br>
+442<br>
+443<br>
+444<br>
+445<br>
+446<br>
+447<br>
+448<br>
+449<br>
+450<br>
+451<br>
+452<br>
+453<br>
+454<br>
+455<br>
+456<br>
+457<br>
+458<br>
+459<br>
+460<br>
+461<br>
+462<br>
+463<br>
+464<br>
+465<br>
+466<br>
+467<br>
+468<br>
+469<br>
+470<br>
+471<br>
+472<br>
+473<br>
+474<br>
+475<br>
+476<br>
+477<br>
+478<br>
+479<br>
+480<br>
+481<br>
+482<br>
+483<br>
+484<br>
+485<br>
+486<br>
+487<br>
+488<br>
+489<br>
+490<br>
+491<br>
+492<br>
+493<br>
+494<br>
+495<br>
+496<br>
+497<br>
+498<br>
+499<br>
+</div>
+<div style="height: 1000px">this div makes the page scrollable</div>
+</body>
diff --git a/gfx/layers/apz/test/mochitest/helper_bug1280013.html b/gfx/layers/apz/test/mochitest/helper_bug1280013.html
new file mode 100644
index 0000000000..6b7d5cf4c3
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1280013.html
@@ -0,0 +1,73 @@
+<!DOCTYPE HTML>
+<html style="overflow:hidden">
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=width-device; initial-scale=1.0">
+ <title>Test for bug 1280013</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+async function test() {
+ ok(screen.height > 500, "Screen height must be at least 500 pixels for this test to work");
+
+ // Scroll down to the iframe. Do it in two drags instead of one in case the
+ // device screen is short.
+ let transformEnd = promiseTransformEnd();
+ await synthesizeNativeTouchDrag(window, 10, 200, 0, -175);
+ await transformEnd;
+
+ transformEnd = promiseTransformEnd();
+ await synthesizeNativeTouchDrag(window, 10, 200, 0, -175);
+ await transformEnd;
+
+ // Now the top of the visible area should be at y=350 of the top-level page,
+ // so if the screen is >= 500px tall, the entire iframe should be visible, at
+ // least vertically.
+
+ // However, because of the overflow:hidden on the root elements, all this
+ // scrolling is happening in APZ and is not reflected in the main-thread
+ // scroll position (it is stored in the callback transform instead). We check
+ // this by checking the scroll offset.
+ await promiseOnlyApzControllerFlushed();
+ is(window.scrollY, 0, "Main-thread scroll position is still at 0");
+
+ // Scroll the iframe by 150px.
+ var subframe = document.getElementById("subframe");
+ transformEnd = promiseTransformEnd();
+ await synthesizeNativeTouchDrag(subframe, 10, 100, 0, -150);
+ await transformEnd;
+
+ // Flush any pending paints on the APZ side, and wait for the main thread
+ // to process them all so that we get the correct test data
+ await promiseApzFlushedRepaints();
+
+ // get the displayport for the subframe
+ var utils = SpecialPowers.getDOMWindowUtils(window);
+ var contentPaints = utils.getContentAPZTestData().paints;
+ var lastPaint = convertScrollFrameData(getLastNonemptyBucket(contentPaints).scrollFrames);
+ var foundIt = 0;
+ for (var scrollId in lastPaint) {
+ if (("contentDescription" in lastPaint[scrollId]) &&
+ (lastPaint[scrollId].contentDescription.includes("tall_html"))) {
+ var dp = getPropertyAsRect(lastPaint, scrollId, "displayport");
+ ok(dp.y <= 0, "The displayport top should be less than or equal to zero to cover the visible part of the subframe; it is " + dp.y);
+ ok(dp.y + dp.height >= subframe.clientHeight, "The displayport bottom should be greater than the clientHeight; it is " + (dp.y + dp.height));
+ foundIt++;
+ }
+ }
+ is(foundIt, 1, "Found exactly one displayport for the subframe we were interested in.");
+}
+
+SpecialPowers.getDOMWindowUtils(window).setResolutionAndScaleTo(2.0);
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</head>
+<body style="overflow:hidden">
+ The iframe below is at (0, 400). Scroll it into view, and then scroll the contents. The content should be fully rendered in high-resolution.
+ <iframe id="subframe" style="position:absolute; left: 0px; top: 400px; width: 300px; height: 175px" src="helper_tall.html"></iframe>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_bug1285070.html b/gfx/layers/apz/test/mochitest/helper_bug1285070.html
new file mode 100644
index 0000000000..0df3a77f4a
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1285070.html
@@ -0,0 +1,44 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Test pointer events are dispatched once for touch tap</title>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript">
+ async function test() {
+ let eventsList = ["pointerover", "pointerenter", "pointerdown",
+ "pointerup", "pointerleave", "pointerout",
+ "mousedown", "mouseup",
+ "touchstart", "touchend", "click"];
+ let eventsCount = {};
+
+ eventsList.forEach((eventName) => {
+ eventsCount[eventName] = 0;
+ document.getElementById("div1").addEventListener(eventName, (event) => {
+ ++eventsCount[event.type];
+ ok(true, "Received event " + event.type);
+ });
+ });
+
+ document.addEventListener("click", (event) => {
+ is(event.target, document.getElementById("div1"), "Clicked on div (at " + event.clientX + "," + event.clientY + ")");
+ for (var key in eventsCount) {
+ is(eventsCount[key], 1, "Event " + key + " should be generated once");
+ }
+ subtestDone();
+ });
+
+ await synthesizeNativeTap(document.getElementById("div1"), 100, 100);
+ }
+
+ waitUntilApzStable().then(test);
+
+ </script>
+</head>
+<body>
+ <div id="div1" style="width: 200px; height: 200px; background: black"></div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_bug1299195.html b/gfx/layers/apz/test/mochitest/helper_bug1299195.html
new file mode 100644
index 0000000000..b1aad7ef11
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1299195.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0; user-scalable=no">
+ <title>Test pointer events are dispatched once for touch tap</title>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript">
+ /** Test for Bug 1299195 */
+ async function runTests() {
+ let target0 = document.getElementById("target0");
+ let mouseup_count = 0;
+ let mousedown_count = 0;
+ let pointerup_count = 0;
+ let pointerdown_count = 0;
+
+ target0.addEventListener("mouseup", () => {
+ ++mouseup_count;
+ if (mouseup_count == 2) {
+ is(mousedown_count, 2, "Double tap with touch should fire 2 mousedown events");
+ is(mouseup_count, 2, "Double tap with touch should fire 2 mouseup events");
+ is(pointerdown_count, 2, "Double tap with touch should fire 2 pointerdown events");
+ is(pointerup_count, 2, "Double tap with touch should fire 2 pointerup events");
+ subtestDone();
+ }
+ });
+ target0.addEventListener("mousedown", () => {
+ ++mousedown_count;
+ });
+ target0.addEventListener("pointerup", () => {
+ ++pointerup_count;
+ });
+ target0.addEventListener("pointerdown", () => {
+ ++pointerdown_count;
+ });
+ await synthesizeNativeTap(document.getElementById("target0"), 100, 100);
+ await synthesizeNativeTap(document.getElementById("target0"), 100, 100);
+ }
+ waitUntilApzStable().then(runTests);
+ </script>
+</head>
+<body>
+ <div id="target0" style="width: 200px; height: 200px; background: green"></div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_bug1326290.html b/gfx/layers/apz/test/mochitest/helper_bug1326290.html
new file mode 100644
index 0000000000..17b5a36eaa
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1326290.html
@@ -0,0 +1,63 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Dragging the mouse on a inactive scrollframe's scrollbar</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+ #scrollable {
+ overflow: scroll;
+ height: 200px;
+ width: 200px;
+ }
+ .content {
+ width: 1000px;
+ height: 2000px;
+ }
+ </style>
+ <script type="text/javascript">
+
+async function test() {
+ var scrollableDiv = document.getElementById("scrollable");
+ let scrollPromise = new Promise(resolve => {
+ scrollableDiv.addEventListener("scroll", resolve, {once: true});
+ });
+
+ var dragFinisher = await promiseVerticalScrollbarDrag(scrollableDiv);
+ if (!dragFinisher) {
+ ok(true, "No scrollbar, can't do this test");
+ return;
+ }
+
+ // the events above might be stuck in APZ input queue for a bit until the
+ // layer is activated, so we wait here until the scroll event listener is
+ // triggered.
+ await scrollPromise;
+
+ await dragFinisher();
+
+ // Flush everything just to be safe
+ await promiseOnlyApzControllerFlushed();
+
+ // After dragging the scrollbar 20px on a 200px-high scrollable div, we should
+ // have scrolled approx 10% of the 2000px high content. There might have been
+ // scroll arrows and such so let's just have a minimum bound of 50px to be safe.
+ ok(scrollableDiv.scrollTop > 50, "Scrollbar drag resulted in a scroll position of " + scrollableDiv.scrollTop);
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</head>
+<body>
+ <div id="scrollable">
+ <div class="content">Some content inside the inactive scrollframe</div>
+ </div>
+ <div class="content">Some content to ensure the root scrollframe is scrollable and the overflow:scroll div remains inactive</div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_bug1331693.html b/gfx/layers/apz/test/mochitest/helper_bug1331693.html
new file mode 100644
index 0000000000..6dd0de13cb
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1331693.html
@@ -0,0 +1,71 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Dragging the mouse on a scrollframe inside an SVGEffects</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="text/javascript">
+
+async function test() {
+ var scrollableDiv = document.getElementById("scrollable");
+ let scrollPromise = new Promise(resolve => {
+ scrollableDiv.addEventListener("scroll", resolve, {once: true});
+ });
+
+ var dragFinisher = await promiseVerticalScrollbarDrag(scrollableDiv);
+ if (!dragFinisher) {
+ ok(true, "No scrollbar, can't do this test");
+ return;
+ }
+
+ // the events above might be stuck in APZ input queue for a bit until the
+ // layer is activated, so we wait here until the scroll event listener is
+ // triggered.
+ await scrollPromise;
+
+ await dragFinisher();
+
+ // Flush everything just to be safe
+ await promiseOnlyApzControllerFlushed();
+
+ // After dragging the scrollbar 20px on a 200px-high scrollable div, we should
+ // have scrolled approx 10% of the 2000px high content. There might have been
+ // scroll arrows and such so let's just have a minimum bound of 50px to be safe.
+ ok(scrollableDiv.scrollTop > 50, "Scrollbar drag resulted in a scroll position of " + scrollableDiv.scrollTop);
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+ <style>
+ #svgeffects {
+ background-color: lightgreen;
+ width: 300px;
+ height: 300px;
+ clip-path: circle(200px at 100% 0); /* ensure scrollthumb is in the clip */
+ }
+ #scrollable {
+ overflow: scroll;
+ height: 200px;
+ width: 200px;
+ }
+ #content {
+ width: 1000px;
+ height: 2000px;
+ background-image: linear-gradient(red,blue);
+ }
+ </style>
+</head>
+<body>
+ <div id="svgeffects">A div that generate an svg effects display item
+ <div id="scrollable">
+ <div id="content">Some content inside the scrollframe</div>
+ </div>
+ </div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_bug1346632.html b/gfx/layers/apz/test/mochitest/helper_bug1346632.html
new file mode 100644
index 0000000000..f91f8159b5
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1346632.html
@@ -0,0 +1,89 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Dragging the scrollbar on a page with a fixed-positioned element just past the right edge of the content</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+ body {
+ height: 2000px;
+ }
+ #fixed {
+ width: 240px;
+ height: 100%;
+ position: fixed;
+ top: 0px;
+ right: -240px;
+ z-index: 1000;
+ overflow-y: scroll;
+ }
+ #fixed-content {
+ height: 2000px;
+ }
+ </style>
+ <script type="text/javascript">
+async function test() {
+ var root = document.scrollingElement;
+ var scrollPos = root.scrollTop;
+ var scrollPromise = new Promise((resolve, reject) => {
+ document.addEventListener("scroll", () => {
+ ok(root.scrollTop > scrollPos, "document scrolled after dragging scrollbar");
+ resolve();
+ }, {once: true});
+ });
+
+ if (window.innerWidth == root.clientWidth) {
+ // No scrollbar, abort the test. This can happen e.g. on local macOS runs
+ // with OS settings to only show scrollbars on trackpad/mouse activity.
+ ok(false, "No scrollbars found, cannot run this test!");
+ return;
+ }
+
+ var scrollbarX = (window.innerWidth + root.clientWidth) / 2;
+ // Move the mouse to the scrollbar
+ await promiseNativeMouseEventWithAPZ({
+ target: root,
+ offsetX: scrollbarX,
+ offsetY: 100,
+ type: "mousemove",
+ });
+ // mouse down
+ await promiseNativeMouseEventWithAPZ({
+ target: root,
+ offsetX: scrollbarX,
+ offsetY: 100,
+ type: "mousedown",
+ });
+ // drag vertically
+ await promiseNativeMouseEventWithAPZ({
+ target: root,
+ offsetX: scrollbarX,
+ offsetY: 150,
+ type: "mousemove",
+ });
+ // wait for the scroll listener to fire
+ await scrollPromise;
+ // and release
+ await promiseNativeMouseEventWithAPZ({
+ target: root,
+ offsetX: scrollbarX,
+ offsetY: 150,
+ type: "mouseup",
+ });
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</head>
+<body>
+ <div id="fixed">
+ <p id="fixed-content"></p>
+ </div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_bug1414336.html b/gfx/layers/apz/test/mochitest/helper_bug1414336.html
new file mode 100644
index 0000000000..636328b7e4
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1414336.html
@@ -0,0 +1,97 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1414336
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1414336</title>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="text/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <style>
+ #target0 {
+ width: 200px;
+ height: 400px;
+ touch-action: auto;
+ }
+ </style>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1414336">Mozilla Bug 1414336</a>
+<p id="display"></p>
+<div id="target0">
+ <p>Test bug1414336</p>
+ <p>Test bug1414336</p>
+ <p>Test bug1414336</p>
+ <p>Test bug1414336</p>
+ <p>Test bug1414336</p>
+ <p>Test bug1414336</p>
+ <p>Test bug1414336</p>
+ <p>Test bug1414336</p>
+ <p>Test bug1414336</p>
+ <p>Test bug1414336</p>
+ <p>Test bug1414336</p>
+ <p>Test bug1414336</p>
+ <p>Test bug1414336</p>
+ <p>Test bug1414336</p>
+ <p>Test bug1414336</p>
+ <p>Test bug1414336</p>
+ <p>Test bug1414336</p>
+ <p>Test bug1414336</p>
+ <p>Test bug1414336</p>
+ <p>Test bug1414336</p>
+ <p>Test bug1414336</p>
+ <p>Test bug1414336</p>
+ <p>Test bug1414336</p>
+ <p>Test bug1414336</p>
+ <p>Test bug1414336</p>
+ <p>Test bug1414336</p>
+ <p>Test bug1414336</p>
+ <p>Test bug1414336</p>
+ <p>Test bug1414336</p>
+ <p>Test bug1414336</p>
+ <p>Test bug1414336</p>
+ <p>Test bug1414336</p>
+</div>
+<script type="text/javascript">
+/** Test for Bug 1414336 */
+waitUntilApzStable().then(async () => {
+ let target0 = window.document.getElementById("target0");
+ let target0_events = ["pointerdown", "pointermove"];
+
+ target0_events.forEach((elem, index, arr) => {
+ target0.addEventListener(elem, (event) => {
+ is(event.type, target0_events[0], "receive " + event.type + " on target0");
+ target0_events.shift();
+ }, { once: true });
+ });
+
+ target0.addEventListener("pointercancel", (event) => {
+ ok(false, "Shouldn't receive pointercancel when content prevents default on touchstart");
+ // Wait until the event is done processing before we end the subtest,
+ // otherwise on Android the pointer events pref is flipped back to false
+ // and debug builds will assert.
+ setTimeout(subtestDone, 0);
+ }, { once: true });
+
+ target0.addEventListener("touchstart", (event) => {
+ event.preventDefault();
+ }, { once: true });
+
+ target0.addEventListener("pointerup", (event) => {
+ ok(!target0_events.length, " should receive " + target0_events + " on target0");
+ // Wait until the event is done processing before we end the subtest,
+ // otherwise on Android the pointer events pref is flipped back to false
+ // and debug builds will assert.
+ setTimeout(subtestDone, 0);
+ }, { once: true });
+
+ await synthesizeNativeTouchDrag(target0, 2, 2, 0, 80);
+});
+
+</script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_bug1462961.html b/gfx/layers/apz/test/mochitest/helper_bug1462961.html
new file mode 100644
index 0000000000..d37d041800
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1462961.html
@@ -0,0 +1,74 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Dragging the mouse on a transformed scrollframe inside a fixed-pos element</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="text/javascript">
+
+async function test() {
+ var scrollableDiv = document.getElementById("scrollable");
+ let scrollPromise = new Promise(resolve => {
+ scrollableDiv.addEventListener("scroll", resolve, {once: true});
+ });
+
+ // Scroll down a small amount (10px). The bug in this case is that the
+ // scrollthumb remains a little "above" where it's supposed to be, so if the
+ // bug manifests here, then the thumb will remain at the top of the track
+ // and the scroll position will remain at 0.
+ var dragFinisher = await promiseVerticalScrollbarDrag(scrollableDiv, 10, 10);
+ if (!dragFinisher) {
+ ok(true, "No scrollbar, can't do this test");
+ return;
+ }
+
+ // the events above might be stuck in APZ input queue for a bit until the
+ // layer is activated, so we wait here until the scroll event listener is
+ // triggered.
+ await scrollPromise;
+
+ await dragFinisher();
+
+ // Flush everything just to be safe
+ await promiseOnlyApzControllerFlushed();
+
+ // In this case we just want to make sure the scroll position moved from 0
+ // which indicates the thumb dragging worked properly.
+ ok(scrollableDiv.scrollTop > 0, "Scrollbar drag resulted in a scroll position of " + scrollableDiv.scrollTop);
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+ <style>
+ #fixed {
+ position: fixed;
+ left: 0;
+ top: 0;
+ width: 300px;
+ height: 100%;
+ }
+ #scrollable {
+ transform: translateY(100px);
+ overflow: scroll;
+ height: 100%;
+ }
+ #content {
+ height: 5000px;
+ background-image: linear-gradient(red,blue);
+ }
+ </style>
+</head>
+<body>
+<div id="fixed">
+ <div id="scrollable">
+ <div id="content"></div>
+ </div>
+</div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_bug1473108.html b/gfx/layers/apz/test/mochitest/helper_bug1473108.html
new file mode 100644
index 0000000000..118ac3fc54
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1473108.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1473108
+-->
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Test for Bug 1473108</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+ .a {
+ background: green;
+ height: 64px;
+ width: 32px;
+ display: block;
+ }
+ span::before {
+ content: "";
+ background: red;
+ height: 32px;
+ width: 32px;
+ display: block;
+ }
+ span:active::after {
+ content: "";
+ }
+</style>
+</head>
+
+<body>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1473108">Mozilla Bug 1473108</a>
+ <a class="a" id="event"><span id="target"></span></a>
+
+ <script type="application/javascript">
+
+ waitUntilApzStable().then(async () => {
+ let target = document.getElementById("target");
+ target.addEventListener("click", function(e) {
+ is(e.target, target, `Clicked on at (${e.clientX}, ${e.clientY})`);
+ subtestDone();
+ });
+ await synthesizeNativeTap(target, 5, 5);
+ });
+
+</script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_bug1490393-2.html b/gfx/layers/apz/test/mochitest/helper_bug1490393-2.html
new file mode 100644
index 0000000000..749110449e
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1490393-2.html
@@ -0,0 +1,65 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Dragging the mouse on a scrollbar for a scrollframe inside nested transforms</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="text/javascript">
+
+async function test() {
+ var scrollableDiv = document.getElementById("scrollable");
+ let scrollPromise = new Promise(resolve => {
+ scrollableDiv.addEventListener("scroll", resolve, {once: true});
+ });
+
+ // Scroll down a small amount (10px). The bug in this case is that the
+ // scrollthumb "jumps" by an additional 40 pixels (height of the "gap" div)
+ // and the scrollframe scrolls by a corresponding amount. So after doing this
+ // drag we check the scroll position to make sure it hasn't scrolled by
+ // too much.
+ // Given the scrollable height of 2000px and scrollframe height of 400px,
+ // the scrollthumb should be approximately 80px tall, and dragging it 10px
+ // should scroll approximately 50 pixels. If the bug manifests, it will get
+ // dragged 50px and scroll approximately 250px.
+ var dragFinisher = await promiseVerticalScrollbarDrag(scrollableDiv, 10, 10);
+ if (!dragFinisher) {
+ ok(true, "No scrollbar, can't do this test");
+ return;
+ }
+
+ // the events above might be stuck in APZ input queue for a bit until the
+ // layer is activated, so we wait here until the scroll event listener is
+ // triggered.
+ await scrollPromise;
+
+ await dragFinisher();
+
+ // Flush everything just to be safe
+ await promiseOnlyApzControllerFlushed();
+
+ // In this case we just want to make sure the scroll position moved from 0
+ // which indicates the thumb dragging worked properly.
+ ok(scrollableDiv.scrollTop < 100, "Scrollbar drag resulted in a scroll position of " + scrollableDiv.scrollTop);
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</head>
+<body>
+ <div id="gap" style="min-height: 40px"></div>
+ <div style="height: 400px; transform: translateZ(0)">
+ <div style="height: 100%; overflow-x: auto; overflow-y: hidden; transform: translateZ(0)">
+ <div id="scrollable" style="display: inline-block; height: 100%; overflow-y: auto; transform: translateZ(0)">
+ <div style="min-height: 2000px">Yay text</div>
+ </div>
+ <div style="display: inline-block; width: 2000px; height: 100%;"></div>
+ </div>
+ </div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_bug1490393.html b/gfx/layers/apz/test/mochitest/helper_bug1490393.html
new file mode 100644
index 0000000000..6c18a2d24e
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1490393.html
@@ -0,0 +1,64 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Dragging the mouse on a scrollbar for a scrollframe inside nested transforms</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="text/javascript">
+
+async function test() {
+ var scrollableDiv = document.getElementById("scrollable");
+ let scrollPromise = new Promise(resolve => {
+ scrollableDiv.addEventListener("scroll", resolve, {once: true});
+ });
+
+ // Scroll down a small amount (10px). The bug in this case is that the
+ // scrollthumb "jumps" by an additional 40 pixels (height of the "gap" div)
+ // and the scrollframe scrolls by a corresponding amount. So after doing this
+ // drag we check the scroll position to make sure it hasn't scrolled by
+ // too much.
+ // Given the scrollable height of 2000px and scrollframe height of 400px,
+ // the scrollthumb should be approximately 80px tall, and dragging it 10px
+ // should scroll approximately 50 pixels. If the bug manifests, it will get
+ // dragged 50px and scroll approximately 250px.
+ var dragFinisher = await promiseVerticalScrollbarDrag(scrollableDiv, 10, 10);
+ if (!dragFinisher) {
+ ok(true, "No scrollbar, can't do this test");
+ return;
+ }
+
+ // the events above might be stuck in APZ input queue for a bit until the
+ // layer is activated, so we wait here until the scroll event listener is
+ // triggered.
+ await scrollPromise;
+
+ await dragFinisher();
+
+ // Flush everything just to be safe
+ await promiseOnlyApzControllerFlushed();
+
+ // In this case we just want to make sure the scroll position moved from 0
+ // which indicates the thumb dragging worked properly.
+ ok(scrollableDiv.scrollTop < 100, "Scrollbar drag resulted in a scroll position of " + scrollableDiv.scrollTop);
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</head>
+<body>
+ <div id="gap" style="min-height: 40px"></div>
+ <div style="height: 400px; transform: translateZ(0)">
+ <div style="height: 100%; opacity: 0.9; will-change: opacity">
+ <div id="scrollable" style="height: 100%; overflow-y: auto; transform: translateZ(0)">
+ <div style="min-height: 2000px">Yay text</div>
+ </div>
+ </div>
+ </div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_bug1502010_unconsumed_pan.html b/gfx/layers/apz/test/mochitest/helper_bug1502010_unconsumed_pan.html
new file mode 100644
index 0000000000..73badf4bc7
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1502010_unconsumed_pan.html
@@ -0,0 +1,76 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Test pointercancel doesn't get sent for horizontal panning on a pan-y element</title>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript">
+ var pointerMoveCount = 0;
+ var lastPointerCoord = -1;
+ var apzFlushed = false;
+ var endEventReceived = false;
+ var testEndResolveFunc = null;
+ var testEndPromise = new Promise(resolve => {
+ testEndResolveFunc = resolve;
+ });
+
+ function checkForTestEnd() {
+ if (apzFlushed && endEventReceived) {
+ var target = document.getElementById("carousel");
+ target.removeEventListener("pointermove", moveListener);
+
+ ok(pointerMoveCount > 0, "Got " + pointerMoveCount + " pointermove events");
+ is(document.scrollingElement.scrollTop, 0, "Document didn't y-scroll");
+ is(document.scrollingElement.scrollLeft, 0, "Document didn't x-scroll");
+
+ testEndResolveFunc();
+ }
+ }
+
+ function moveListener(event) {
+ ok(event.clientX >= lastPointerCoord, "Got nondecreasing pointermove to " + event.clientX + "," + event.clientY);
+ lastPointerCoord = event.clientX;
+ pointerMoveCount++;
+ }
+
+ async function test() {
+ var target = document.getElementById("carousel");
+ target.addEventListener("pointercancel", (event) => {
+ ok(false, "Received pointercancel, uh-oh!");
+ endEventReceived = true;
+ setTimeout(checkForTestEnd, 0);
+ }, {once: true});
+ target.addEventListener("pointerup", () => {
+ ok(true, "Received pointerup");
+ endEventReceived = true;
+ setTimeout(checkForTestEnd, 0);
+ }, {once: true});
+
+ target.addEventListener("pointermove", moveListener);
+
+ // Drag mostly horizontally but also slightly vertically. If the
+ // touch-action were not respected due to a bug this might result
+ // in vertical scrolling instead of pointermove events.
+ await new Promise(resolve => {
+ synthesizeNativeTouchDrag(target, 10, 10, 200, -10, resolve);
+ });
+ await promiseOnlyApzControllerFlushed();
+ apzFlushed = true;
+
+ setTimeout(checkForTestEnd, 0);
+
+ await testEndPromise;
+ }
+
+ waitUntilApzStable().then(test).then(subtestDone, subtestFailed);
+
+ </script>
+</head>
+<body>
+ <div id="carousel" style="height: 50px; touch-action: pan-y; background-color: blue"></div>
+ <div id="spacer" style="height: 2000px"></div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_bug1506497_touch_action_fixed_on_fixed.html b/gfx/layers/apz/test/mochitest/helper_bug1506497_touch_action_fixed_on_fixed.html
new file mode 100644
index 0000000000..cc73fe99ea
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1506497_touch_action_fixed_on_fixed.html
@@ -0,0 +1,96 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Test for Bug 1506497</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="application/javascript">
+
+async function test() {
+ document.getElementById("overlay").addEventListener("touchstart", function(e) {
+ // no need to do anything here. Just having a non-passive touchstart
+ // listener will force APZ to wait for the main thread to handle the
+ // touch event. The bug is that the touch-action:none property on the
+ // overlay gets ignored in this case and the body gets scrolled.
+ }, {passive: false});
+
+ // Ensure that APZ gets updated hit-test info
+ await promiseAllPaintsDone();
+
+ // Register a listener that fails the test if the APZ:TransformEnd event fires,
+ // because this test shouldn't actually be triggering any transforms
+ SpecialPowers.Services.obs.addObserver(function() {
+ ok(false, "The test fired an unexpected APZ:TransformEnd");
+ }, "APZ:TransformEnd");
+
+ // Listen for changes to the visual viewport offset.
+ let visScrEvtInternal = new EventCounter(window, "mozvisualscroll",
+ { mozSystemGroup: true });
+
+ // This promise will resolve after the main thread has processed
+ // all the synthesized touch events.
+ let promiseTouchEnd = new Promise(resolve => {
+ var waitForTouchEnd = function(e) {
+ dump("touchend listener hit\n");
+ resolve();
+ };
+ document.documentElement.addEventListener(
+ "touchend",
+ waitForTouchEnd,
+ {passive: true, once: true}
+ );
+ });
+
+ await synthesizeNativeTouchDrag(document.getElementById("boxOnTop"), 5, 5, 0, -50);
+ dump("finished drag, waiting for touchend listener...");
+ await promiseTouchEnd;
+
+ // Flush state.
+ await promiseApzFlushedRepaints();
+
+ // Check that the touch was prevented, per the touch-action
+ is(window.scrollY, 0, "window didn't scroll");
+ is(document.scrollingElement.scrollTop, 0, "scrollingElement didn't scroll");
+ visScrEvtInternal.unregister();
+ is(visScrEvtInternal.count, 0, "visual viewport didn't scroll");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+ <style>
+ #filler {
+ height: 3000px;
+ background-image: linear-gradient(red, blue, green);
+ }
+ #overlay {
+ position: fixed;
+ width: 100%;
+ height: 100%;
+ left: 0;
+ top: 0;
+ touch-action: none;
+ }
+ #boxOnTop {
+ position: fixed;
+ background-color: coral;
+ width: 20vw;
+ height: 20vh;
+ left: 40%;
+ top: 40%;
+ }
+ </style>
+</head>
+<body>
+ <div id="filler"></div>
+ <div id="overlay">
+ <div id="boxOnTop">Touch here and drag up</div>
+ </div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_bug1509575.html b/gfx/layers/apz/test/mochitest/helper_bug1509575.html
new file mode 100644
index 0000000000..4c85d1db42
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1509575.html
@@ -0,0 +1,71 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1509575
+-->
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Test for Bug 1509575</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+<body>
+ <div id="expand" style="background-color: paleturquoise ;">
+ Now you're scrolled, now you're not?
+ </div>
+ <script type="application/javascript">
+
+async function test() {
+ let transformEndPromise = promiseTransformEnd();
+ await synthesizeNativeTouchDrag(document.body, 10, 100, -100, 0);
+ dump("Finished native drag, waiting for transform-end observer...\n");
+
+ // Wait for the APZ:TransformEnd to be fired after touch events are processed.
+ await transformEndPromise;
+
+ // Flush state.
+ await promiseApzFlushedRepaints();
+
+ is(window.scrollX, 0, "layout viewport didn't scroll");
+ let visualX = window.visualViewport.pageLeft;
+ ok(visualX > 0, "visual viewport did scroll");
+
+ let topWinUtils;
+ const isE10s = SpecialPowers.Services.appinfo.browserTabsRemoteAutostart;
+ // We need to reset the first paint flag on the root document in the process
+ // this test is loaded in.
+ if (!isE10s) {
+ // For non-e10s, such as in Fennec, this means we need the *chrome* window
+ // as the topmost entitiy in this process.
+ topWinUtils = SpecialPowers.getDOMWindowUtils(
+ SpecialPowers._getTopChromeWindow(window));
+ } else {
+ topWinUtils = SpecialPowers.getDOMWindowUtils(window);
+ }
+ let afterPaintPromise = promiseAfterPaint();
+ ok(topWinUtils.isFirstPaint === false, "first paint not set");
+ topWinUtils.isFirstPaint = true;
+ // do something that forces a paint *and* an APZ update.
+ document.getElementById("expand").style.width = "6000px";
+
+ // Wait for the event listener to fire.
+ await afterPaintPromise;
+ ok(true, "MozAfterPaint fired");
+
+ // Flush state just to be sure.
+ await promiseApzFlushedRepaints();
+
+ is(window.visualViewport.pageLeft, visualX, "visual viewport remains unchanged");
+}
+
+SpecialPowers.getDOMWindowUtils(window).setResolutionAndScaleTo(2.0);
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_bug1519339_hidden_smoothscroll.html b/gfx/layers/apz/test/mochitest/helper_bug1519339_hidden_smoothscroll.html
new file mode 100644
index 0000000000..225182f749
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1519339_hidden_smoothscroll.html
@@ -0,0 +1,61 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Test for bug 1519339</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+ /* To exercise this bug, the page needs to be overflow:hidden in
+ * only one direction, and have actual room to scroll in the other.
+ * Otherwise we wouldn't try to hand the scroll off to APZ even
+ * before the fix.
+ */
+ html {
+ overflow-y: hidden;
+ }
+ div {
+ width: 200vw;
+ height: 10000px;
+ background-color: lightblue;
+ }
+ </style>
+</head>
+<body>
+ <div></div>
+ <script>
+ async function test() {
+ info("Testing scrollTo() in overflow:hidden direction");
+ let scrollEndPromise = promiseScrollend();
+ window.scrollTo({ top: 2000, behavior: 'smooth' });
+ await scrollEndPromise;
+ await promiseApzFlushedRepaints();
+ is(window.scrollY, 2000,
+ "scrollTo() in overflow:hidden direction scrolled to destination");
+
+ info("Testing scrollBy() in overflow:hidden direction");
+ scrollEndPromise = promiseScrollend();
+ window.scrollBy({ top: 2000, behavior: 'smooth'});
+ await scrollEndPromise;
+ await promiseApzFlushedRepaints();
+ is(window.scrollY, 4000,
+ "scrollBy() in overflow:hidden direction scrolled to destination");
+
+ info("Testing scrollByLines() in overflow:hidden direction");
+ scrollEndPromise = promiseScrollend();
+ window.scrollByLines(5, { behavior: 'smooth' });
+ await scrollEndPromise;
+ await promiseApzFlushedRepaints();
+ // Don't try to predict the exact scroll distance, just check we've
+ // scrolled at all.
+ ok(window.scrollY > 4000,
+ "scrollByLines() in overflow:hidden direction performed scrolling");
+
+ }
+ waitUntilApzStable()
+ .then(test)
+ .then(subtestDone, subtestFailed);
+ </script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_bug1544966_zoom_on_touch_action_none.html b/gfx/layers/apz/test/mochitest/helper_bug1544966_zoom_on_touch_action_none.html
new file mode 100644
index 0000000000..7adfd5ba0f
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1544966_zoom_on_touch_action_none.html
@@ -0,0 +1,89 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Test for Bug 1544966</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="application/javascript">
+
+async function test() {
+ var target = document.getElementById("target");
+
+ var pointersDown = 0;
+ var pointersUp = 0;
+ var pointerMoveCount = 0;
+
+ target.addEventListener("pointerdown", function(e) {
+ dump(`Got pointerdown, pointer id ${e.pointerId}\n`);
+ pointersDown++;
+ });
+ target.addEventListener("pointermove", function(e) {
+ dump(`Got pointermove, pointer id ${e.pointerId}, at ${e.clientX}, ${e.clientY}\n`);
+ pointerMoveCount++;
+ });
+ let pointersUpPromise = new Promise(resolve => {
+ target.addEventListener("pointercancel", function(e) {
+ dump(`Got pointercancel, pointer id ${e.pointerId}\n`);
+ ok(false, "Should not have gotten pointercancel");
+ pointersUp++;
+ if (pointersDown == pointersUp) {
+ // All pointers lifted, let's continue the test
+ resolve();
+ }
+ });
+ target.addEventListener("pointerup", function(e) {
+ dump(`Got pointerup, pointer id ${e.pointerId}\n`);
+ pointersUp++;
+ if (pointersDown == pointersUp) {
+ // All pointers lifted, let's continue the test
+ resolve();
+ }
+ });
+ });
+
+ var zoom_in = [
+ [ { x: 125, y: 175 }, { x: 175, y: 225 } ],
+ [ { x: 120, y: 150 }, { x: 180, y: 250 } ],
+ [ { x: 115, y: 125 }, { x: 185, y: 275 } ],
+ [ { x: 110, y: 100 }, { x: 190, y: 300 } ],
+ [ { x: 105, y: 75 }, { x: 195, y: 325 } ],
+ [ { x: 100, y: 50 }, { x: 200, y: 350 } ],
+ ];
+
+ var touchIds = [0, 1];
+ await synthesizeNativeTouchSequences(document.getElementById("target"), zoom_in, null, touchIds);
+
+ dump("All touch events synthesized, waiting for final pointerup...\n");
+ await pointersUpPromise;
+
+ // Should get at least one pointermove per pointer, even if the events
+ // get coalesced somewhere.
+ is(pointersDown, 2, "Got expected numbers of pointers recorded");
+ ok(pointerMoveCount >= 2, "Got " + pointerMoveCount + " pointermove events");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+ <style>
+ body {
+ height: 5000px;
+ }
+ #target {
+ touch-action: none;
+ height: 400px
+ }
+ </style>
+</head>
+<body>
+ <div id="target">
+ Put down two fingers at the same time and do a pinch action.
+ </div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_bug1550510.html b/gfx/layers/apz/test/mochitest/helper_bug1550510.html
new file mode 100644
index 0000000000..2f3be5dd2c
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1550510.html
@@ -0,0 +1,66 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Dragging the mouse on a scrollbar for a transformed, filtered scrollframe</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="text/javascript">
+
+async function test() {
+ var scrollableDiv = document.getElementById("scrollable");
+ let scrollPromise = new Promise(resolve => {
+ scrollableDiv.addEventListener("scroll", resolve, {once: true});
+ });
+
+ // Scroll down a small amount (10px). The bug in this case is that the
+ // scrollthumb "jumps" most of the way down the scroll track because with
+ // WR enabled the filter and transform display items combine to generate an
+ // incorrect APZC tree, and the mouse position gets untransformed incorrectly.
+ // Given the scrollable height of 2000px and scrollframe height of 400px,
+ // the scrollthumb should be approximately 80px tall, and dragging it 10px
+ // should scroll approximately 50 pixels. If the bug manifests, it will get
+ // dragged an extra ~150px and scroll to approximately 1250px.
+ var dragFinisher = await promiseVerticalScrollbarDrag(scrollableDiv, 10, 10);
+ if (!dragFinisher) {
+ ok(true, "No scrollbar, can't do this test");
+ return;
+ }
+
+ // the events above might be stuck in APZ input queue for a bit until the
+ // layer is activated, so we wait here until the scroll event listener is
+ // triggered.
+ await scrollPromise;
+
+ await dragFinisher();
+
+ // Flush everything just to be safe
+ await promiseOnlyApzControllerFlushed();
+
+ // In this case we just want to make sure the scroll position moved from 0
+ // which indicates the thumb dragging worked properly.
+ ok(scrollableDiv.scrollTop < 100, "Scrollbar drag resulted in a scroll position of " + scrollableDiv.scrollTop);
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</head>
+<body>
+ <div style="position: fixed; left: 100px; top: 100px; width: 400px; height: 600px">
+ <div style="transform: translateY(150px); will-change: transform">
+ <div style="filter: grayscale(80%)">
+ <div id="scrollable" style="height: 400px; overflow-y: auto">
+ <div style="min-height: 2000px">
+ yay text
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_bug1637113_main_thread_hit_test.html b/gfx/layers/apz/test/mochitest/helper_bug1637113_main_thread_hit_test.html
new file mode 100644
index 0000000000..c58c7e40b6
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1637113_main_thread_hit_test.html
@@ -0,0 +1,70 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1637113
+-->
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Test for Bug 1637113</title>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <style>
+ iframe {
+ margin-top: 1000px;
+ }
+ </style>
+</head>
+<body>
+ <iframe id="subframe" srcdoc="<div id='target' style='width:100px;height:100px;'>" width="100px" height="100px"></iframe>
+ <script type="application/javascript">
+
+async function test() {
+ let utils = SpecialPowers.getDOMWindowUtils(window);
+
+ // Reproducing the bug requires three ingredients:
+ // 1. A large layout viewport offset.
+ // 2. A large visual viewport offset relative to the layout viewport.
+ // 3. An event that's dispatched in the iframe's document.
+ // We make the first two happen by doing a large visual scroll that will
+ // also drag the layout viewport with it part of the way.
+ let visualScrollPromise = new Promise(resolve => {
+ window.visualViewport.addEventListener("scroll", resolve, { once: true });
+ });
+ utils.scrollToVisual(0, 900, utils.UPDATE_TYPE_MAIN_THREAD,
+ utils.SCROLL_MODE_INSTANT);
+ await visualScrollPromise;
+ await promiseApzFlushedRepaints();
+
+ let target = subframe.contentWindow.document.getElementById("target");
+ // To get an event that's dispatched in the iframe's document,
+ // synthesize a native tap. This will synthesize three events:
+ // a mouse-move, a mouse-down, and a mouse-up. The mouse-move
+ // and mouse-down are dispatched in the root content document.
+ // The mouse-down causes the iframe to "capture" the mouse, which
+ // leads the mouse-up to be dispatched in the iframe's document
+ // instead. We listen for the mouse-up.
+ let mouseUpEvent = null;
+ let mouseUpPromise = new Promise(resolve => {
+ target.addEventListener("mouseup", function(e) {
+ mouseUpEvent = e;
+ resolve();
+ });
+ });
+
+ await synthesizeNativeTap(target, 10, 10);
+ await mouseUpPromise;
+
+ is(mouseUpEvent.target, target, "mouseup event targeted the correct element");
+}
+
+SpecialPowers.getDOMWindowUtils(window).setResolutionAndScaleTo(2.0);
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_bug1637135_narrow_viewport.html b/gfx/layers/apz/test/mochitest/helper_bug1637135_narrow_viewport.html
new file mode 100644
index 0000000000..7b57416c04
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1637135_narrow_viewport.html
@@ -0,0 +1,60 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1637135
+-->
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=400px">
+ <title>Test for Bug 1637135</title>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <style>
+ #target {
+ margin-left: 450px;
+ width: 100px;
+ height: 100px;
+ }
+ </style>
+</head>
+<body>
+ <div id="target">
+ <script type="application/javascript">
+
+async function test() {
+ // Tap the target element, which is located beyond x=400.
+ // The bug occurs when we cannot hit it because the viewport
+ // width of x=400 causes us to be unable to hit elements
+ // beyond that point.
+ let target = document.getElementById("target");
+ let mouseDownEvent = null;
+ let mouseDownPromise = new Promise(resolve => {
+ target.addEventListener("mousedown", function(e) {
+ mouseDownEvent = e;
+ resolve();
+ });
+ });
+
+ await synthesizeNativeTap(target, 10, 10);
+ await mouseDownPromise;
+
+ is(mouseDownEvent.target, target, "mousedown event targeted the correct element");
+}
+
+if (getPlatform() == "android") {
+ waitUntilApzStable()
+ .then(test)
+ .then(subtestDone, subtestFailed);
+} else {
+ // The fix for bug 1637135 is limited to Android, because
+ // it breaks the ability to target scrollbars, so we can
+ // only run this test on Android.
+ ok(true, "This subtest is only run on Android");
+ subtestDone();
+}
+
+ </script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_bug1638441_fixed_pos_hit_test.html b/gfx/layers/apz/test/mochitest/helper_bug1638441_fixed_pos_hit_test.html
new file mode 100644
index 0000000000..3665ef5a31
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1638441_fixed_pos_hit_test.html
@@ -0,0 +1,67 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1638441
+-->
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Test for Bug 1638441</title>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <style>
+ #target {
+ position: fixed;
+ bottom: 50px;
+ width: 100px;
+ height: 100px;
+ }
+ </style>
+</head>
+<body>
+ <div id="target">
+ <script type="application/javascript">
+
+async function test() {
+ let utils = SpecialPowers.getDOMWindowUtils(window);
+
+ // Do a large visual scroll to scroll the visual viewport to the bottom
+ // of the layout viewport.
+ let visualScrollPromise = new Promise(resolve => {
+ window.visualViewport.addEventListener("scroll", resolve, { once: true });
+ });
+ utils.scrollToVisual(0, 900, utils.UPDATE_TYPE_MAIN_THREAD,
+ utils.SCROLL_MODE_INSTANT);
+ await visualScrollPromise;
+ await promiseApzFlushedRepaints();
+
+ // Tap the position-fixed element which is near the bottom of the
+ // layout viewport (and therefore visible now that the visual
+ // viewport is scrolled to the bottom of the layout viewport).
+ // The intention is to test that the visual-to-layout transform
+ // is applied correctly during the hit test.
+ let target = document.getElementById("target");
+ let mouseDownEvent = null;
+ let mouseDownPromise = new Promise(resolve => {
+ target.addEventListener("mousedown", function(e) {
+ mouseDownEvent = e;
+ resolve();
+ });
+ });
+
+ await synthesizeNativeTap(target, 10, 10);
+ await mouseDownPromise;
+
+ is(mouseDownEvent.target, target, "mousedown event targeted the correct element");
+}
+
+SpecialPowers.getDOMWindowUtils(window).setResolutionAndScaleTo(2.0);
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_bug1638458_contextmenu.html b/gfx/layers/apz/test/mochitest/helper_bug1638458_contextmenu.html
new file mode 100644
index 0000000000..a5a43f7ca1
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1638458_contextmenu.html
@@ -0,0 +1,82 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1638458
+-->
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Test for Bug 1638458</title>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <style>
+ #target {
+ margin-top: 1000px;
+ width: 100px;
+ height: 100px;
+ }
+ </style>
+</head>
+<body>
+ <div id="target">
+ <script type="application/javascript">
+
+async function test() {
+ let utils = SpecialPowers.getDOMWindowUtils(window);
+
+ // Do a large visual scroll to scroll the visual viewport to the bottom
+ // of the layout viewport.
+ let visualScrollPromise = new Promise(resolve => {
+ window.visualViewport.addEventListener("scroll", resolve, { once: true });
+ });
+ utils.scrollToVisual(0, 900, utils.UPDATE_TYPE_MAIN_THREAD,
+ utils.SCROLL_MODE_INSTANT);
+ await visualScrollPromise;
+ await promiseApzFlushedRepaints();
+
+ // Simulate a long-tap on the target. We do this by simply synthesizing
+ // a touch-start event; eventually, the long-tap timeout will be triggered
+ // and the "contextmenu" will be fired (on non-Windows platforms).
+ let target = document.getElementById("target");
+ let contextmenuEvent = null;
+ let contextmenuPromise = new Promise(resolve => {
+ window.addEventListener("contextmenu", function(e) {
+ contextmenuEvent = e;
+ // Don't actually open a context menu; it messes up subsequent
+ // tests unless we take additional action to close it.
+ e.preventDefault();
+ resolve();
+ });
+ });
+ await synthesizeNativeTouch(target, 10, 10, SpecialPowers.DOMWindowUtils.TOUCH_CONTACT);
+ await contextmenuPromise;
+
+ // Check that the "contextmenu" event targets the correct element.
+ is(contextmenuEvent.target, target, "contextmenu event targeted the correct element");
+
+ // Clean up by firing a touch-end to clear the APZ gesture state.
+ await new Promise(resolve => {
+ synthesizeNativeTouch(target, 10, 10, SpecialPowers.DOMWindowUtils.TOUCH_REMOVE,
+ resolve);
+ });
+}
+
+if (getPlatform() == "windows") {
+ // On Windows, contextmenu events work differently (e.g. they are fired
+ // after the touch-end) which makes them more involved to synthesize.
+ // We don't gain much value in terms of extra test coverage from running
+ // this subtest on windows, so just skip it.
+ ok(true, "Skipping this subtest on windows");
+ subtestDone();
+} else {
+ SpecialPowers.getDOMWindowUtils(window).setResolutionAndScaleTo(2.0);
+ waitUntilApzStable()
+ .then(test)
+ .then(subtestDone, subtestFailed);
+}
+
+ </script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_bug1648491_no_pointercancel_with_dtc.html b/gfx/layers/apz/test/mochitest/helper_bug1648491_no_pointercancel_with_dtc.html
new file mode 100644
index 0000000000..3d8fdcf76e
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1648491_no_pointercancel_with_dtc.html
@@ -0,0 +1,89 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Test for Bug 1648491</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="application/javascript">
+
+async function test() {
+ var body = document.body;
+
+ var pointersDown = 0;
+ var pointersUp = 0;
+ var pointerMoveCount = 0;
+
+ body.addEventListener("pointerdown", function(e) {
+ dump(`Got pointerdown, pointer id ${e.pointerId}\n`);
+ pointersDown++;
+ });
+ body.addEventListener("pointermove", function(e) {
+ dump(`Got pointermove, pointer id ${e.pointerId}, at ${e.clientX}, ${e.clientY}\n`);
+ pointerMoveCount++;
+ });
+ let pointersUpPromise = new Promise(resolve => {
+ body.addEventListener("pointercancel", function(e) {
+ dump(`Got pointercancel, pointer id ${e.pointerId}\n`);
+ ok(false, "Should not have gotten pointercancel");
+ pointersUp++;
+ if (pointersDown == pointersUp) {
+ // All pointers lifted, let's continue the test
+ resolve();
+ }
+ });
+ body.addEventListener("pointerup", function(e) {
+ dump(`Got pointerup, pointer id ${e.pointerId}\n`);
+ pointersUp++;
+ if (pointersDown == pointersUp) {
+ // All pointers lifted, let's continue the test
+ resolve();
+ }
+ });
+ });
+
+ var zoom_in = [
+ [ { x: 125, y: 175 }, { x: 175, y: 225 } ],
+ [ { x: 120, y: 150 }, { x: 180, y: 250 } ],
+ [ { x: 115, y: 125 }, { x: 185, y: 275 } ],
+ [ { x: 110, y: 100 }, { x: 190, y: 300 } ],
+ [ { x: 105, y: 75 }, { x: 195, y: 325 } ],
+ [ { x: 100, y: 50 }, { x: 200, y: 350 } ],
+ ];
+
+ var touchIds = [0, 1];
+ await synthesizeNativeTouchSequences(document.getElementById("target"), zoom_in, null, touchIds);
+
+ dump("All touch events synthesized, waiting for final pointerup...\n");
+ await pointersUpPromise;
+
+ // Should get at least one pointermove per pointer, even if the events
+ // get coalesced somewhere.
+ is(pointersDown, 2, "Got expected numbers of pointers recorded");
+ ok(pointerMoveCount >= 2, "Got " + pointerMoveCount + " pointermove events");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+ <style>
+ body {
+ height: 5000px;
+ }
+ #target {
+ touch-action: pan-x pan-y;
+ height: 400px;
+ }
+ </style>
+</head>
+<body>
+ <div id="target" onwheel="return false;">
+ A two-finger pinch action here should send pointer events to content.
+ </div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_bug1662800.html b/gfx/layers/apz/test/mochitest/helper_bug1662800.html
new file mode 100644
index 0000000000..eb804a40f6
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1662800.html
@@ -0,0 +1,61 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Dragging the mouse on a scrollbar for a scrollframe inside nested transforms with a scale component</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="text/javascript">
+
+async function test() {
+ var scrollableDiv = document.getElementById("scrollable");
+ let scrollPromise = new Promise(resolve => {
+ scrollableDiv.addEventListener("scroll", resolve, {once: true});
+ });
+
+ // Scroll down a small amount (7px). The bug in this case is that the
+ // scrollthumb "jumps" most of the way down the scroll track because with
+ // the bug, the code was incorrectly combining the transforms.
+ // Given the scrollable height of 0.7*2000px and scrollframe height of 0.7*400px,
+ // the scrollthumb should be approximately 0.7*80px = 56px tall. Dragging it 7px
+ // should scroll approximately 50 (unscaled) pixels. If the bug manifests, it will get
+ // dragged by a lot more and scroll to approximately 1300px.
+ var dragFinisher = await promiseVerticalScrollbarDrag(scrollableDiv, 7, 7, 0.7);
+ if (!dragFinisher) {
+ ok(true, "No scrollbar, can't do this test");
+ return;
+ }
+
+ // the events above might be stuck in APZ input queue for a bit until the
+ // layer is activated, so we wait here until the scroll event listener is
+ // triggered.
+ await scrollPromise;
+
+ await dragFinisher();
+
+ // Flush everything just to be safe
+ await promiseOnlyApzControllerFlushed();
+
+ // Ensure the scroll position ended up roughly where we wanted it (around
+ // 50px, but definitely less than 1300px).
+ ok(scrollableDiv.scrollTop < 100, "Scrollbar drag resulted in a scroll position of " + scrollableDiv.scrollTop);
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</head>
+<body>
+ <div style="width: 500px; height: 300px; transform: translate(500px, 500px) scale(0.7)">
+ <div id="scrollable" style="transform: translate(-600px, -600px); overflow: scroll">
+ <div style="width: 600px; height: 400px">
+ <div style="width: 600px; height: 2000px; background-image: linear-gradient(red,blue)"></div>
+ </div>
+ </div>
+ </div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_bug1663731_no_pointercancel_on_second_touchstart.html b/gfx/layers/apz/test/mochitest/helper_bug1663731_no_pointercancel_on_second_touchstart.html
new file mode 100644
index 0000000000..e0690c12c6
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1663731_no_pointercancel_on_second_touchstart.html
@@ -0,0 +1,82 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Test for Bug 1663731</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="application/javascript">
+
+async function test() {
+ var body = document.body;
+
+ var cancelledTouchMove = false;
+
+ // Event listeners just for logging/debugging purposes
+ body.addEventListener("pointerdown", function(e) {
+ dump(`Got pointerdown, pointer id ${e.pointerId}\n`);
+ });
+ body.addEventListener("touchstart", function(e) {
+ dump(`Got touchstart with ${e.touches.length} touches\n`);
+ }, {passive: true});
+
+
+ // Event listeners relevant to the test. We want to make sure that even
+ // though APZ can zoom the page, it does NOT dispatch pointercancel events in
+ // the scenario where the page calls preventDefault() on the first touchmove
+ // with two touch points. In other words, if the page chooses to disable
+ // browser pinch-zooming by preventDefault()'ing the first touchmove for
+ // the second touch point, then the browser should not dispatch pointercancel
+ // at all, but keep sending the pointerevents to the content. This is
+ // similar to what the browser does when zooming is disallowed by
+ // touch-action:none, for example.
+ body.addEventListener("pointercancel", function(e) {
+ dump(`Got pointercancel, pointer id ${e.pointerId}\n`);
+ ok(false, "Should not get any pointercancel events");
+ });
+ body.addEventListener("touchmove", function(e) {
+ dump(`Got touchmove with ${e.touches.length} touches\n`);
+ if (e.touches.length > 1) {
+ dump(`Preventing...\n`);
+ e.preventDefault();
+ cancelledTouchMove = true;
+ }
+ }, {passive: false});
+
+ let touchEndPromise = new Promise(resolve => {
+ // This listener is just to catch the end of the touch sequence so we can
+ // end the test at the right time.
+ body.addEventListener("touchend", function(e) {
+ dump(`Got touchend with ${e.touches.length} touches\n`);
+ if (!e.touches.length) {
+ resolve();
+ }
+ });
+ });
+
+ // We can't await this call, because this pinch action doesn't generate a
+ // APZ:TransformEnd. Instead we await the touchend.
+ pinchZoomOutWithTouchAtCenter();
+ await touchEndPromise;
+
+ ok(cancelledTouchMove, "Checking that we definitely cancelled the touchmove");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+ <style>
+ body {
+ height: 5000px;
+ }
+ </style>
+</head>
+<body>
+ A two-finger pinch action here should send pointer events to content and not do browser zooming.
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_bug1669625.html b/gfx/layers/apz/test/mochitest/helper_bug1669625.html
new file mode 100644
index 0000000000..95d2a4bc2c
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1669625.html
@@ -0,0 +1,79 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Scrolling doesn't cause extra SchedulePaint calls</title>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+async function test() {
+ if (SpecialPowers.getBoolPref("apz.force_disable_desktop_zooming_scrollbars") ||
+ getPlatform() == "android") {
+ return;
+ }
+
+ while (window.scrollY == 0) {
+ // the scrollframe is not yet marked as APZ-scrollable. Mark it so before
+ // continuing.
+ window.scrollTo(0, 1000);
+ await promiseApzFlushedRepaints();
+ }
+
+ window.synthesizeKey("KEY_ArrowDown");
+ // This is really tricky. We want to check that during the main part of
+ // scrolling after this we don't get any SchedulePaint calls. The way that we
+ // test that is to use checkAndClearDisplayListState on the document element
+ // to make sure it didn't have display list building ran for it. The
+ // synthesizeKey calls above will end up in ScrollFrameHelper::ScrollBy,
+ // which calls SchedulePaint in order to pass the scroll to the compositor to
+ // perform. That SchedulePaint will result in display list building for the
+ // document element, and that's okay, but we want to call
+ // checkAndClearDisplayListState (to clear the display list building state)
+ // right after that display list building, so that we can observe if any
+ // display list building happens after it. That way that we do that is a rAF,
+ // which runs immediately before painting, and then a setTimeout from the
+ // rAF, which should run almost immediately after painting. Then we wait for
+ // a scroll event, this scroll event is triggered by the compositor updating
+ // the main thread scroll position. And here is where we finally get to what
+ // we want to actually test. The original bug came about when the main
+ // thread, while processing the repaint request from the compositor, called
+ // SchedulePaint, and hence caused display list building. So we want to check
+ // that the refresh driver tick after the scroll event does not do any
+ // display list building. We again use a setTimeout from a rAF to run right
+ // after the paint and check that there was no display list building.
+ await new Promise(resolve => {
+ window.requestAnimationFrame(() => {
+ setTimeout(checkNoDisplayListRebuild, 0, resolve);
+ });
+ });
+}
+
+function checkNoDisplayListRebuild(resolve) {
+ var utils = window.opener.SpecialPowers.getDOMWindowUtils(window);
+ var elem = document.documentElement;
+ utils.checkAndClearDisplayListState(elem);
+ window.addEventListener("scroll", function () {
+ window.requestAnimationFrame(() => {
+ setTimeout(function() {
+ is(utils.checkAndClearDisplayListState(elem), false, "Document element didn't get display list");
+ resolve();
+ },0);
+ });
+ }, {once: true});
+}
+
+waitUntilApzStable()
+ .then(test)
+ .then(subtestDone, subtestFailed);
+
+ </script>
+</head>
+<body style="height: 5000px">
+ <div style="height: 50px">spacer</div>
+ <button id="b" style="width: 10px; height: 10px"></button>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_bug1674935.html b/gfx/layers/apz/test/mochitest/helper_bug1674935.html
new file mode 100644
index 0000000000..f5efa16d5f
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1674935.html
@@ -0,0 +1,76 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Tests that keyboard arrow keys scroll a very specific page</title>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script>
+function start() {
+ document.documentElement.addEventListener("keyup", function() { console.log("keyup"); });
+ document.documentElement.addEventListener("keydown", function() { console.log("keydown"); });
+ document.documentElement.addEventListener("keypress", function() { console.log("keypress"); });
+}
+ </script>
+ <style>
+ .z1asCe {
+ display: inline-block;
+ width: 24px
+ }
+ .kno-ecr-pt {
+ position: relative;
+ }
+ .rsir2d {
+ opacity: 0.54
+ }
+ .bErdLd {
+ position: fixed;
+ right: 0;
+ bottom: 0;
+ top: 0;
+ left: 0;
+ }
+ </style>
+</head>
+<body onload="start();">
+ <div style="height: 4000px;">
+ <div class="rsir2d">
+ <span class=" z1asCe ">
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
+ <path d="M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81 1.66 0 3-1.34 3-3s-1.34-3-3-3-3 1.34-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9c-1.66 0-3 1.34-3 3s1.34 3 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.16c-.05.21-.08.43-.08.65 0 1.61 1.31 2.92 2.92 2.92 1.61 0 2.92-1.31 2.92-2.92s-1.31-2.92-2.92-2.92z"></path>
+ </svg>
+ </span>
+ <div class="bErdLd">
+ </div>
+ </div>
+ <h2 class="kno-ecr-pt"><span>Firefox</span></h2>
+ </div>
+
+ <script type="application/javascript">
+
+ function waitForScrollEvent(target) {
+ return new Promise(resolve => {
+ target.addEventListener("scroll", resolve, { once: true });
+ });
+ }
+
+ async function test() {
+ is(window.scrollX, 0, "shouldn't have scrolled (1)");
+ is(window.scrollY, 0, "shouldn't have scrolled (2)");
+
+ let waitForScroll = waitForScrollEvent(window);
+
+ window.synthesizeKey("KEY_ArrowDown");
+
+ await waitForScroll;
+
+ is(window.scrollX, 0, "shouldn't have scrolled (3)");
+ isnot(window.scrollY, 0, "should have scrolled (4)");
+ }
+
+ waitUntilApzStable().then(test).then(subtestDone, subtestFailed);
+ </script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_bug1682170_pointercancel_on_touchaction_pinchzoom.html b/gfx/layers/apz/test/mochitest/helper_bug1682170_pointercancel_on_touchaction_pinchzoom.html
new file mode 100644
index 0000000000..b9c31dfe89
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1682170_pointercancel_on_touchaction_pinchzoom.html
@@ -0,0 +1,75 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="application/javascript">
+
+async function test() {
+ var body = document.body;
+
+ // Event listeners just for logging/debugging purposes
+ body.addEventListener("pointerdown", function(e) {
+ dump(`Got pointerdown, pointer id ${e.pointerId}\n`);
+ });
+ body.addEventListener("touchstart", function(e) {
+ dump(`Got touchstart with ${e.touches.length} touches\n`);
+ }, {passive: true});
+
+
+ // Event listeners relevant to the test. We want to make sure that a
+ // pointercancel event is dispatched to web content, so we listen for that.
+ // Also we want to ensure the main thread TouchActionHelper code is run and
+ // used, so we add a non-passive touchmove listener that ensures the body has
+ // a d-t-c region.
+ var gotPointerCancel = false;
+ body.addEventListener("pointercancel", function(e) {
+ dump(`Got pointercancel, pointer id ${e.pointerId}\n`);
+ gotPointerCancel = true;
+ });
+ body.addEventListener("touchmove", function(e) {
+ dump(`Got touchmove with ${e.touches.length} touches\n`);
+ }, {passive: false});
+
+ let touchEndPromise = new Promise(resolve => {
+ // This listener is just to catch the end of the touch sequence so we can
+ // end the test at the right time.
+ body.addEventListener("touchend", function(e) {
+ dump(`Got touchend with ${e.touches.length} touches\n`);
+ if (!e.touches.length) {
+ resolve();
+ }
+ });
+ });
+
+ // We can't await this call, because this pinch action doesn't generate a
+ // APZ:TransformEnd. Instead we await the touchend.
+ pinchZoomOutWithTouchAtCenter();
+ await touchEndPromise;
+
+ ok(gotPointerCancel, "Checking that we definitely cancelled the pointerevents");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+ <style>
+ body {
+ height: 5000px;
+ touch-action: pinch-zoom;
+ }
+ </style>
+</head>
+<body>
+ A two-finger pinch action here should trigger browser zoom and trigger a pointercancel to content.
+ Note that the code does a zoom-out and the page is already at min zoom, so
+ the zoom doesn't produce any visual effect. But the DOM events should be the
+ same either way.
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_bug1695598.html b/gfx/layers/apz/test/mochitest/helper_bug1695598.html
new file mode 100644
index 0000000000..fb6102e33d
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1695598.html
@@ -0,0 +1,123 @@
+<html>
+<head>
+ <title>Test for bug 1695598</title>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="text/javascript">
+ let scrollEvents = 100;
+ let i = 0;
+
+ // Scroll points
+ let numscrolls = 0;
+ let last_numscrolls = 0;
+ let start_scrolly = 0;
+ let missed_events = 0;
+ let missed_scroll_updates = 0;
+
+ let utils = SpecialPowers.getDOMWindowUtils(window);
+
+ let timeStamp = document.timeline.currentTime;
+ async function sendScrollEvent(aRafTimestamp) {
+ if (i < scrollEvents) {
+ if (timeStamp == document.timeline.currentTime) {
+ // If we are in a rAF callback at the same time stamp we've already
+ // sent a key event, skip it, otherwise we will not get the
+ // corresponding scroll event for the key event since it will be
+ // coalesced into a single scroll event.
+ window.requestAnimationFrame(sendScrollEvent);
+ return;
+ }
+ timeStamp = document.timeline.currentTime;
+ // Sent a key event in a setTimeout callback so that it will be
+ // processed in between nsRefreshDriver::Tick calls, thus the async
+ // scroll triggered by the key event is going to be processed on the
+ // main-thread before RepaintRequests are processed inside
+ // nsRefreshDriver::Tick, which results there's an async scroll when
+ // RepaintRequests are processed.
+ setTimeout(async () => {
+ window.synthesizeKey("KEY_ArrowDown");
+ i++;
+ // "apz-repaints-flush" is notified in an early runner of
+ // nsRefreshDriver, which means it will be delivered inside
+ // a nsRefreshDriver::Tick call before rAF callbacks.
+ await promiseOnlyApzControllerFlushedWithoutSetTimeout();
+
+ // Wait an animationiteration event since animation events are fired
+ // before rAF callbacks and after scroll events so that it's a good
+ // place to tell whether the expected scroll event got fired or not.
+ await promiseOneEvent(document.getElementById("animation"),
+ "animationiteration", null);
+ if (numscrolls == last_numscrolls) {
+ missed_events++;
+ }
+ if (window.scrollY <= start_scrolly) {
+ missed_scroll_updates++;
+ }
+ last_numscrolls = numscrolls;
+ start_scrolly = window.scrollY;
+ window.requestAnimationFrame(sendScrollEvent);
+ }, 0);
+ } else {
+ // There's a race condition even if we got an "apz-repaints-flush"
+ // notification but any scroll event isn't fired and scroll position
+ // isn't updated since the notification was corresponding to a layers
+ // update triggered by the key event above, which means there was no
+ // repaint request corresponding to APZ animation sample in the time
+ // frame. We allow the case here in the half of key events.
+ ok(missed_events < scrollEvents / 2, `missed event firing ${missed_events} times`);
+ ok(missed_scroll_updates < scrollEvents / 2, `missed scroll update ${missed_scroll_updates} times`);
+ endTest();
+ }
+ }
+
+ async function endTest() {
+ document.removeEventListener("scroll", gotScroll);
+ subtestDone();
+ }
+
+ function gotScroll() {
+ numscrolls++;
+ }
+
+ function startTest() {
+ document.addEventListener("scroll", gotScroll);
+ window.requestAnimationFrame(sendScrollEvent);
+ }
+
+ if (!isApzEnabled()) {
+ ok(true, "APZ not enabled, skipping test");
+ subtestDone();
+ }
+
+ waitUntilApzStable()
+ .then(forceLayerTreeToCompositor)
+ .then(startTest);
+ </script>
+ <style>
+ #content {
+ height: 10000vh;
+ background: repeating-linear-gradient(#EEE, #EEE 100px, #DDD 100px, #DDD 200px);
+ }
+ @keyframes anim {
+ from { opacity: 0; }; /* To avoid churning this scroll test */
+ to { opacity: 0; };
+ }
+ #animation {
+ position: absolute;
+ width: 100px;
+ height: 100px;
+ visibility: hidden; /* for skipping restyles on the main-thread */
+ animation-name: anim;
+ animation-iteration-count: infinite;
+ animation-duration: 1ms; /* to get an animationiteration event in each tick */
+ }
+ </style>
+</head>
+<body>
+ <div id="animation"></div>
+ <div id="content">
+ </div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_bug1714934_mouseevent_buttons.html b/gfx/layers/apz/test/mochitest/helper_bug1714934_mouseevent_buttons.html
new file mode 100644
index 0000000000..35cea7de4f
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1714934_mouseevent_buttons.html
@@ -0,0 +1,40 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1714934
+-->
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Test for Bug 1714934</title>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <style>
+ iframe {
+ margin-top: 1000px;
+ }
+ </style>
+</head>
+<body>
+ <script type="application/javascript">
+
+async function test() {
+ let mousedownPromise = new Promise(resolve => {
+ document.addEventListener("mousedown", e => {
+ is(e.buttons, 1, "Mousedown event reports 1 button pressed")
+ resolve();
+ }, { once: true })
+ });
+ await synthesizeNativeTap(window, 100, 100);
+ await mousedownPromise;
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_bug1719330.html b/gfx/layers/apz/test/mochitest/helper_bug1719330.html
new file mode 100644
index 0000000000..c99b6e1012
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1719330.html
@@ -0,0 +1,65 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Tests that the arrow down key does not scroll by more than 1 element</title>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+ .time-column {
+ width: 68px;
+ height: 28px;
+
+ overflow-y: scroll;
+ scroll-snap-type: y mandatory;
+
+ border-radius: 4px;
+ border: 1px solid red;
+ }
+
+ .time-item {
+ scroll-snap-align: center;
+ height: 100%;
+ }
+ </style>
+</head>
+<body>
+ <div class="time-column"></div>
+
+ <script type="application/javascript">
+
+ function waitForScrollEvent(target) {
+ return new Promise(resolve => {
+ target.addEventListener("scroll", resolve, { once: true });
+ });
+ }
+
+ async function test() {
+ const timeCol = document.querySelector('.time-column');
+
+ for (let i = 0; i < 60; i++) {
+ let item = document.createElement('div');
+ item.classList.add('time-item');
+ item.textContent = i;
+ timeCol.appendChild(item);
+ }
+
+ is(timeCol.scrollTop, 0, "should begin with no scroll (1)");
+
+ let waitForScroll = waitForScrollEvent(timeCol);
+
+ timeCol.focus();
+ window.synthesizeKey("KEY_ArrowDown");
+
+ await waitForScroll;
+
+ ok(timeCol.scrollTop > 0, "should have scrolled (2)");
+ ok(timeCol.scrollTop < 30, "should have not scrolled too far (3)");
+ }
+
+ waitUntilApzStable().then(test).then(subtestDone, subtestFailed);
+ </script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_bug1756529.html b/gfx/layers/apz/test/mochitest/helper_bug1756529.html
new file mode 100644
index 0000000000..e1767f4f57
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1756529.html
@@ -0,0 +1,226 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1756529
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Page scrolling bug test, helper page</title>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+ // --------------------------------------------------------------------
+ // Page scrolling not smooth test
+ //
+ // This test checks that a page scroll respects general_smoothScroll_pages preference.
+ //
+ // The page contains a <div> that is large enough to make the page
+ // scrollable.
+ //
+ // We trigger the page scroll and then we wait to reach destination
+ // Expecting an instant scroll, we check that the scroll event is called once
+ // --------------------------------------------------------------------
+ const testData = [
+ {scrollOrigin: "page", smooth: false,
+ prefs: [["general.smoothScroll", true], ["general.smoothScroll.pages", false],
+ ["general.smoothScroll.msdPhysics.enabled", true]]},
+ {scrollOrigin: "page", smooth: false,
+ prefs: [["general.smoothScroll", false], ["general.smoothScroll.pages", true],
+ ["general.smoothScroll.msdPhysics.enabled", true]]},
+ {scrollOrigin: "page", smooth: true,
+ prefs: [["general.smoothScroll", true], ["general.smoothScroll.pages", true],
+ ["general.smoothScroll.msdPhysics.enabled", true]]},
+ {scrollOrigin: "page", smooth: false,
+ prefs: [["general.smoothScroll", true], ["general.smoothScroll.pages", false],
+ ["general.smoothScroll.msdPhysics.enabled", false]]},
+ {scrollOrigin: "page", smooth: false,
+ prefs: [["general.smoothScroll", false], ["general.smoothScroll.pages", true],
+ ["general.smoothScroll.msdPhysics.enabled", false]]},
+ {scrollOrigin: "page", smooth: true,
+ prefs: [["general.smoothScroll", true], ["general.smoothScroll.pages", true],
+ ["general.smoothScroll.msdPhysics.enabled", false]]},
+ // Origin:Line Scrolling tests
+ {scrollOrigin: "line", smooth: false,
+ prefs: [["general.smoothScroll", true], ["general.smoothScroll.lines", false],
+ ["general.smoothScroll.msdPhysics.enabled", true]]},
+ {scrollOrigin: "line", smooth: false,
+ prefs: [["general.smoothScroll", false], ["general.smoothScroll.lines", true],
+ ["general.smoothScroll.msdPhysics.enabled", true]]},
+ {scrollOrigin: "line", smooth: true,
+ prefs: [["general.smoothScroll", true], ["general.smoothScroll.lines", true],
+ ["general.smoothScroll.msdPhysics.enabled", true]]},
+ {scrollOrigin: "line", smooth: false,
+ prefs: [["general.smoothScroll", true], ["general.smoothScroll.lines", false],
+ ["general.smoothScroll.msdPhysics.enabled", false]]},
+ {scrollOrigin: "line", smooth: false,
+ prefs: [["general.smoothScroll", false], ["general.smoothScroll.lines", true],
+ ["general.smoothScroll.msdPhysics.enabled", false]]},
+ {scrollOrigin: "line", smooth: true,
+ prefs: [["general.smoothScroll", true], ["general.smoothScroll.lines", true],
+ ["general.smoothScroll.msdPhysics.enabled", false]]},
+ // Origin:Other Scrolling test
+ {scrollOrigin: "other", smooth: false,
+ prefs: [["general.smoothScroll", true], ["general.smoothScroll.other", false],
+ ["general.smoothScroll.msdPhysics.enabled", true]]},
+ {scrollOrigin: "other", smooth: false,
+ prefs: [["general.smoothScroll", false], ["general.smoothScroll.other", true],
+ ["general.smoothScroll.msdPhysics.enabled", true]]},
+ {scrollOrigin: "other", smooth: true,
+ prefs: [["general.smoothScroll", true], ["general.smoothScroll.other", true],
+ ["general.smoothScroll.msdPhysics.enabled", true]]},
+ {scrollOrigin: "other", smooth: false,
+ prefs: [["general.smoothScroll", true], ["general.smoothScroll.other", false],
+ ["general.smoothScroll.msdPhysics.enabled", false]]},
+ {scrollOrigin: "other", smooth: false,
+ prefs: [["general.smoothScroll", false], ["general.smoothScroll.other", true],
+ ["general.smoothScroll.msdPhysics.enabled", false]]},
+ {scrollOrigin: "other", smooth: true,
+ prefs: [["general.smoothScroll", true], ["general.smoothScroll.other", true],
+ ["general.smoothScroll.msdPhysics.enabled", false]]}];
+
+ async function test(data) {
+ /*
+ Test Data:
+ {
+ scrollOrigin: "page"|"other"|"line",
+ smooth: bool,
+ prefs: prefences
+ }
+ */
+ const scrollOrigin = data.scrollOrigin;
+ const smooth = data.smooth;
+ const msdPhysics = data.prefs[2][1];
+ let destination = 0;
+ let key = "";
+ switch (scrollOrigin){
+ case "page":
+ destination = document.scrollingElement.clientHeight * 0.8;
+ key = "KEY_PageDown";
+ break;
+ case "other":
+ destination = 40000; // Div is 50k
+ key = "KEY_End";
+ break;
+ case "line":
+ default:
+ destination = 50; // pref set to scroll by 5 lines
+ // line scroll amounts vary by platform but are
+ // in the 16-19px range
+ key = "KEY_ArrowDown";
+ }
+ await SpecialPowers.pushPrefEnv({ set: data.prefs });
+ info(`Testing Scrolling preferences. [origin: ${scrollOrigin}; smooth: ${smooth}; msdPhysics: ${msdPhysics}; ${destination}]`);
+
+ // Send the synthesized key event, and wait until it arrives in the
+ // content process.
+ let keyPromise = promiseOneEvent(window, "keydown", null);
+ window.synthesizeKey(key);
+ await keyPromise;
+
+ // Take control of the refresh driver. It's important to do this
+ // as soon as the key event has arrived, to ensure that any compositor
+ // animation hasn't started yet. Otherwise, the compositor animation
+ // could start and get in multiple samples (potentially the entire
+ // animation) before the content process gets a chance to observe it,
+ // preventing us from distinguishing smooth scrolls from instant scrolls.
+ let utils = SpecialPowers.DOMWindowUtils;
+ utils.advanceTimeAndRefresh(0);
+
+ // Flush any pending paints. This gives a chance for any handoff of
+ // the scroll to APZ to occur.
+ await promiseAllPaintsDone();
+
+ // Tick the refresh driver manually until we detect that scrolling has
+ // started (scrollY > 0) and then stopped (scroll offset the same in
+ // two subsequent ticks).
+ let startedScroll = false;
+ let stoppedScroll = false;
+ let scrollCount = 0;
+ let prevScrollPos = window.scrollY;
+ while (!stoppedScroll) {
+ // Tick the refresh driver. This triggers a composite, so any
+ // compositor animation will be sampled. (Main thread animations
+ // will also be sampled.)
+ utils.advanceTimeAndRefresh(16);
+
+ // Flush APZ repaints to ensure that scroll offset changes from
+ // a compositor sample reach the content process.
+ await promiseApzFlushedRepaints();
+
+ // Track the number of ticks in which the scroll offset changed.
+ let scrollPos = window.scrollY;
+ if (startedScroll && scrollPos == prevScrollPos) {
+ stoppedScroll = true;
+ break;
+ }
+ if (!startedScroll && scrollPos > 0) {
+ startedScroll = true;
+ }
+ if (startedScroll) {
+ scrollCount++;
+ }
+ prevScrollPos = scrollPos;
+ }
+
+ info(`Scrolled to ${window.scrollY}`);
+
+ // Relinquish control of the refresh driver.
+ utils.restoreNormalRefresh();
+
+ ok(window.scrollY >= destination, `The page did not scroll [origin: ${scrollOrigin}, smooth: ${smooth}]`);
+ if (smooth)
+ ok(scrollCount > 1,
+ `Scrolled only once, but expecting a smooth transtion [origin: ${scrollOrigin}; msdPhysics: ${msdPhysics}]`);
+ else
+ is(scrollCount, 1,
+ `Scrolled more than once, but expecting an instant scroll [origin: ${scrollOrigin}; msdPhysics: ${msdPhysics}]`);
+
+ // Synthesize a touch tap to cancel the animation if it's still in-progress.
+ // (scrollTo() does not do this as of bug 1692708, it adjusts the destination
+ // of the animation by a relative delta).
+ let touchStartPromise = promiseOneEvent(window, "touchstart", null);
+ await synthesizeNativeTap(window, 50, 50);
+ // Wait until the tap is actually processed by APZ.
+ await touchStartPromise;
+ await promiseApzFlushedRepaints();
+
+ // Reset scroll position for next case.
+ window.scrollTo(0, 0);
+ await promiseApzFlushedRepaints();
+ is(0, window.scrollY, `Expected to be scrolled to origin, actually scrolled to ${window.scrollY}`)
+ }
+
+ async function runTests() {
+ for (i = 0; i < testData.length; i++){
+ await test(testData[i]);
+ }
+ }
+
+ if (getPlatform() == "linux" || getPlatform() == "mac") {
+ // FIXME(bug 1760731): On Linux, this test frequently hangs at
+ // "await touchStartPromise", so we skip it.
+ // For Mac, the test is disabled due to a high intermittent failure
+ // rate reported in bug 1771836.
+ ok(true, "Test is disabled on Linux and Mac, skipping");
+ subtestDone();
+ } else {
+ waitUntilApzStable()
+ .then(runTests)
+ .then(subtestDone, subtestFailed);
+ }
+ </script>
+</head>
+<body style="height: 10000px; overflow: scroll;">
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1756529">SmoothScrollPage not honored with MSD physics bug.</a>
+ <!-- Put enough content into the page to make it have a nonzero scroll range -->
+ <div style="height: 50000px;">
+ <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Tellus in metus vulputate eu. Vestibulum morbi blandit cursus risus at ultrices mi tempus imperdiet. Congue quisque egestas diam in. Pretium vulputate sapien nec sagittis aliquam malesuada bibendum arcu. Eleifend mi in nulla posuere. Proin libero nunc consequat interdum varius. Risus pretium quam vulputate dignissim suspendisse in est. Lacus vel facilisis volutpat est. Donec pretium vulputate sapien nec. Feugiat sed lectus vestibulum mattis. Platea dictumst quisque sagittis purus. Vulputate eu scelerisque felis imperdiet proin fermentum leo vel. Enim facilisis gravida neque convallis a cras semper auctor. Placerat orci nulla pellentesque dignissim enim sit.</p>
+ <p>Augue neque gravida in fermentum et sollicitudin ac. Mattis enim ut tellus elementum sagittis vitae et. Malesuada nunc vel risus commodo viverra maecenas accumsan. Viverra nibh cras pulvinar mattis nunc sed. Lectus nulla at volutpat diam ut venenatis tellus in. Non tellus orci ac auctor. Magna etiam tempor orci eu lobortis. Malesuada nunc vel risus commodo viverra maecenas accumsan lacus vel. Sagittis orci a scelerisque purus. Tellus pellentesque eu tincidunt tortor. Vulputate dignissim suspendisse in est ante in. Tristique et egestas quis ipsum suspendisse. Quisque egestas diam in arcu cursus. Massa massa ultricies mi quis hendrerit dolor magna eget. Mattis nunc sed blandit libero volutpat sed. Consectetur purus ut faucibus pulvinar elementum integer enim.</p>
+ <p>Vestibulum lorem sed risus ultricies tristique nulla. Imperdiet nulla malesuada pellentesque elit eget gravida. Feugiat nisl pretium fusce id velit ut tortor pretium. Commodo ullamcorper a lacus vestibulum sed arcu non odio. Id nibh tortor id aliquet lectus proin nibh nisl condimentum. Amet volutpat consequat mauris nunc congue nisi vitae suscipit tellus. Neque ornare aenean euismod elementum. Semper quis lectus nulla at. Massa sed elementum tempus egestas. Praesent elementum facilisis leo vel fringilla est ullamcorper eget nulla. Pellentesque elit eget gravida cum sociis natoque penatibus et. Massa enim nec dui nunc mattis enim. Laoreet suspendisse interdum consectetur libero id faucibus nisl. Fusce ut placerat orci nulla.</p>
+ <p>Vitae tempus quam pellentesque nec nam aliquam. Vestibulum mattis ullamcorper velit sed ullamcorper morbi tincidunt ornare. Nam libero justo laoreet sit amet. Arcu non sodales neque sodales. Nec ultrices dui sapien eget mi proin sed. Parturient montes nascetur ridiculus mus mauris vitae ultricies. Lacus sed viverra tellus in hac habitasse. Orci phasellus egestas tellus rutrum. Leo a diam sollicitudin tempor id eu nisl. Diam phasellus vestibulum lorem sed risus ultricies tristique nulla. Lectus nulla at volutpat diam ut venenatis tellus in. Cursus metus aliquam eleifend mi in nulla. Et ultrices neque ornare aenean euismod. Sit amet aliquam id diam maecenas ultricies mi. Volutpat diam ut venenatis tellus in metus vulputate eu.</p>
+ <p>Pellentesque elit ullamcorper dignissim cras tincidunt. Morbi tincidunt augue interdum velit euismod. Diam vel quam elementum pulvinar etiam non quam. Eget duis at tellus at urna. Posuere ac ut consequat semper viverra nam libero justo laoreet. Ac turpis egestas maecenas pharetra convallis posuere. Ultrices tincidunt arcu non sodales neque sodales ut etiam sit. In eu mi bibendum neque egestas. Pellentesque sit amet porttitor eget dolor morbi. Ac tortor dignissim convallis aenean et tortor at. Elementum tempus egestas sed sed risus pretium quam. Nisi scelerisque eu ultrices vitae auctor eu augue. Urna duis convallis convallis tellus id interdum velit laoreet id. Auctor eu augue ut lectus arcu bibendum at varius vel.</p>
+ </div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_bug1780701.html b/gfx/layers/apz/test/mochitest/helper_bug1780701.html
new file mode 100644
index 0000000000..f445f9abe6
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1780701.html
@@ -0,0 +1,70 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>Test that scroll snap wont't happen on zoomed content</title>
+ <script src="apz_test_utils.js"></script>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+ body {
+ margin: 0;
+ }
+ html {
+ overflow-y: scroll;
+ scroll-snap-type: y proximity;
+ }
+ .snap {
+ width: 100vw;
+ height: 100vh;
+ background-color: blue;
+ position: absolute;
+ top: 200px;
+ scroll-snap-align: start;
+ }
+ </style>
+</head>
+<body>
+ <div class="snap"></div>
+ <div style="width: 100%; height: 500vh;"></div>
+ <script type="application/javascript">
+ async function test() {
+ let transformEndPromise = promiseTransformEnd();
+
+ // Use scrollToVisual() to scroll visual viewport.
+ SpecialPowers.DOMWindowUtils.scrollToVisual(
+ 100, 400,
+ SpecialPowers.DOMWindowUtils.UPDATE_TYPE_MAIN_THREAD,
+ SpecialPowers.DOMWindowUtils.SCROLL_MODE_SMOOTH);
+
+ // Wait for the end of the scroll.
+ await transformEndPromise;
+ await waitToClearOutAnyPotentialScrolls();
+
+ const pageTop = visualViewport.pageTop;
+ const pageLeft = visualViewport.pageLeft;
+
+ let eventFired = false;
+ window.visualViewport.addEventListener("scroll", () => {
+ eventFired = true;
+ });
+
+ // Trigger a scroll snap, it should nothing.
+ SpecialPowers.wrap(document.documentElement).mozScrollSnap();
+
+ await waitToClearOutAnyPotentialScrolls();
+ ok(!eventFired, "No visual scroll should happen");
+
+ // Sanity checks to see whether the visual viewport hasn't been changed.
+ is(visualViewport.pageTop, pageTop);
+ is(visualViewport.pageLeft, pageLeft);
+ }
+
+ SpecialPowers.DOMWindowUtils.setResolutionAndScaleTo(10.0);
+ waitUntilApzStable()
+ .then(test)
+ .then(subtestDone, subtestFailed);
+ </script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_bug1783936.html b/gfx/layers/apz/test/mochitest/helper_bug1783936.html
new file mode 100644
index 0000000000..8aec5eafef
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1783936.html
@@ -0,0 +1,74 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>Test that scroll snap happens on pan end without fling</title>
+ <script src="apz_test_utils.js"></script>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+ body {
+ margin: 0;
+ padding: 0;
+ }
+ html {
+ overflow-y: scroll;
+ scroll-snap-type: y mandatory;
+ scroll-behavior: auto;
+ }
+ .snap {
+ width: 100%;
+ height: 100vh;
+ background-color: blue;
+ position: absolute;
+ top: 200px;
+ scroll-snap-align: start;
+ }
+ </style>
+</head>
+<body>
+ <div class="snap"></div>
+ <div style="width: 100%; height: 500vh;"></div>
+ <script type="application/javascript">
+ async function test() {
+ is(window.scrollY, 200, "The initial layout should result snapping to 200px");
+
+ // Start scrolling back by a pan gesture and wait its' scroll end.
+ let transformEndPromise = promiseTransformEnd();
+ await promiseNativeTouchpadPanEventAndWaitForObserver(
+ window,
+ 100,
+ 100,
+ 0, -100,
+ SpecialPowers.DOMWindowUtils.PHASE_BEGIN);
+
+ // Finish the pan gesture.
+ await promiseNativeTouchpadPanEventAndWaitForObserver(
+ window,
+ 100,
+ 100,
+ 0, 0,
+ SpecialPowers.DOMWindowUtils.PHASE_END);
+ await transformEndPromise;
+
+ // Make sure the new scroll positions have reached to the main-thread.
+ await promiseOnlyApzControllerFlushed();
+
+ is(window.scrollY, 200, "The pan-end should result snapping to 200px");
+ }
+
+ // This test is supposed to run on environments where
+ // PanGestureInput.mSimulateMomentum for pan gestures is true, which means
+ // as of now it's only on Linux.
+ if (getPlatform() == "linux") {
+ waitUntilApzStable()
+ .then(test)
+ .then(subtestDone, subtestFailed);
+ } else {
+ ok(true, "Skipping test because this test isn't supposed to work on " + getPlatform());
+ subtestDone();
+ }
+ </script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_bug982141.html b/gfx/layers/apz/test/mochitest/helper_bug982141.html
new file mode 100644
index 0000000000..8ffda2dd2f
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug982141.html
@@ -0,0 +1,130 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=982141
+-->
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=980, user-scalable=no">
+ <title>Test for Bug 982141, helper page</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+ // In this test we have a simple page with a scrollable <div> which has
+ // enough content to make it scrollable. We test that this <div> got
+ // a displayport.
+
+ function test() {
+ // Get the content- and compositor-side test data from nsIDOMWindowUtils.
+ var utils = SpecialPowers.getDOMWindowUtils(window);
+ var contentTestData = utils.getContentAPZTestData();
+ var compositorTestData = utils.getCompositorAPZTestData();
+
+ // Get the sequence number of the last paint on the compositor side.
+ // We do this before converting the APZ test data because the conversion
+ // loses the order of the paints.
+ ok(!!compositorTestData.paints.length,
+ "expected at least one paint in compositor test data");
+ var lastCompositorPaint = compositorTestData.paints[compositorTestData.paints.length - 1];
+ var lastCompositorPaintSeqNo = lastCompositorPaint.sequenceNumber;
+
+ // Convert the test data into a representation that's easier to navigate.
+ contentTestData = convertTestData(contentTestData);
+ compositorTestData = convertTestData(compositorTestData);
+
+ // Reconstruct the APZC tree structure in the last paint.
+ var apzcTree = buildApzcTree(compositorTestData.paints[lastCompositorPaintSeqNo]);
+
+ // The apzc tree for this page should consist of a single child APZC on
+ // the RCD node (the child is for scrollable <div>). Note that in e10s/B2G
+ // cases the RCD will be the root of the tree but on Fennec it will not.
+ var rcd = findRcdNode(apzcTree);
+ ok(rcd != null, "found the RCD node");
+ is(rcd.children.length, 1, "expected a single child APZC");
+ var childScrollId = rcd.children[0].scrollId;
+
+ // We should have content-side data for the same paint.
+ ok(lastCompositorPaintSeqNo in contentTestData.paints,
+ "expected a content paint with sequence number" + lastCompositorPaintSeqNo);
+ var correspondingContentPaint = contentTestData.paints[lastCompositorPaintSeqNo];
+
+ var dp = getPropertyAsRect(correspondingContentPaint, childScrollId, "displayport");
+ var subframe = document.getElementById("subframe");
+ // The clientWidth and clientHeight may be less than 50 if there are scrollbars showing.
+ // In general they will be (50 - <scrollbarwidth>, 50 - <scrollbarheight>).
+ ok(subframe.clientWidth > 0, "Expected a non-zero clientWidth, got: " + subframe.clientWidth);
+ ok(subframe.clientHeight > 0, "Expected a non-zero clientHeight, got: " + subframe.clientHeight);
+ ok(dp.width >= subframe.clientWidth && dp.height >= subframe.clientHeight,
+ "expected a displayport at least as large as the scrollable element, got " + JSON.stringify(dp));
+ }
+
+ waitUntilApzStable()
+ .then(forceLayerTreeToCompositor)
+ .then(test)
+ .then(subtestDone, subtestFailed);
+
+ </script>
+</head>
+<body style="overflow: hidden;"><!-- This combined with the user-scalable=no ensures the root frame is not scrollable -->
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=982141">Mozilla Bug 982141</a>
+ <!-- A scrollable subframe, with enough content to make it have a nonzero scroll range -->
+ <div id="subframe" style="height: 50px; width: 50px; overflow: scroll">
+ <div style="width: 100px">
+ Wide content so that the vertical scrollbar for the parent div
+ doesn't eat into the 50px width and reduce the width of the
+ displayport.
+ </div>
+ Line 1<br>
+ Line 2<br>
+ Line 3<br>
+ Line 4<br>
+ Line 5<br>
+ Line 6<br>
+ Line 7<br>
+ Line 8<br>
+ Line 9<br>
+ Line 10<br>
+ Line 11<br>
+ Line 12<br>
+ Line 13<br>
+ Line 14<br>
+ Line 15<br>
+ Line 16<br>
+ Line 17<br>
+ Line 18<br>
+ Line 19<br>
+ Line 20<br>
+ Line 21<br>
+ Line 22<br>
+ Line 23<br>
+ Line 24<br>
+ Line 25<br>
+ Line 26<br>
+ Line 27<br>
+ Line 28<br>
+ Line 29<br>
+ Line 30<br>
+ Line 31<br>
+ Line 32<br>
+ Line 33<br>
+ Line 34<br>
+ Line 35<br>
+ Line 36<br>
+ Line 37<br>
+ Line 38<br>
+ Line 39<br>
+ Line 40<br>
+ Line 41<br>
+ Line 42<br>
+ Line 43<br>
+ Line 44<br>
+ Line 45<br>
+ Line 46<br>
+ Line 40<br>
+ Line 48<br>
+ Line 49<br>
+ Line 50<br>
+ </div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_check_dp_size.html b/gfx/layers/apz/test/mochitest/helper_check_dp_size.html
new file mode 100644
index 0000000000..0a81a69958
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_check_dp_size.html
@@ -0,0 +1,124 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1689492
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1689492, helper page</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+ // -------------------------------------------------------------------
+ // Infrastructure to get the test assertions to run at the right time.
+ // -------------------------------------------------------------------
+ var SimpleTest = window.opener.SimpleTest;
+
+ // --------------------------------------------------------------------
+ // In this test we have a scrollable root scroll frame (not needed, but
+ // more representative), a scrollable outer div, and a scrollable inner
+ // div. We scroll the inner div, and test that it gets a non-zero
+ // display port (not the main reason for the test, that should already
+ // work, but it's a good sanity check), and then check that the outer
+ // div gets a display port and (here's the important part of the test)
+ // that that display port has zero margins, ie it's relatively close to the
+ // dimensions of the outer div (it can't be exact because we align display
+ // ports). This tests a regression where the outer div would get non-zero
+ // margin display port even though it had never been scrolled (it still
+ // needs a display port because it has a scrollable child). We run the
+ // test several times with different sized outerdiv.
+ // --------------------------------------------------------------------
+
+ function createDivs(outerwidth, outerheight) {
+ let outerdiv = document.createElement("div");
+ outerdiv.id = "outerdiv";
+ outerdiv.style.width = outerwidth + "px";
+ outerdiv.style.height = outerheight + "px";
+ outerdiv.style.overflow = "scroll";
+
+ let innerdiv = document.createElement("div");
+ innerdiv.id = "innerdiv";
+ innerdiv.style.width = "25px";
+ innerdiv.style.height = "25px";
+ innerdiv.style.overflow = "scroll";
+ outerdiv.appendChild(innerdiv);
+
+ let innerspacer = document.createElement("div");
+ innerspacer.style.width = "25px";
+ innerspacer.style.height = "100px";
+ innerdiv.appendChild(innerspacer);
+
+ let outerspacer = document.createElement("div");
+ outerspacer.style.width = "50px";
+ outerspacer.style.height = "10000px";
+ outerdiv.appendChild(outerspacer);
+
+
+ let theplace = document.getElementById("theplace");
+ theplace.parentNode.insertBefore(outerdiv, theplace.nextSibling);
+ }
+
+ async function testOne(theheight, allowedscalefactor, outputprefix) {
+ createDivs(50, theheight);
+ // flush layout
+ document.documentElement.getBoundingClientRect();
+ await promiseApzFlushedRepaints();
+
+ document.getElementById("innerdiv").scrollTop = "10px";
+
+ // Activate the inner div.
+ await promiseMoveMouseAndScrollWheelOver(document.getElementById("innerdiv"), 0, 10);
+
+ await promiseApzFlushedRepaints();
+
+ let innerdp = getLastContentDisplayportFor("innerdiv");
+ ok(innerdp.height > 30, outputprefix + " innerdiv display port should be larger than innerdiv");
+
+ let outerdp = getLastContentDisplayportFor("outerdiv");
+ is(outerdp.x, 0, outputprefix + " outerdiv display port should be relatively bounded x");
+ is(outerdp.y, 0, outputprefix + " outerdiv display port should be relatively bounded y");
+ ok(outerdp.width <= 50, outputprefix + " outerdiv display port should relatively bounded w");
+ ok(outerdp.height < theheight * allowedscalefactor, outputprefix + " outerdiv display port should be relatively bounded h");
+
+ ok(true, "innerdp " + JSON.stringify(innerdp));
+ ok(true, "outerdp " + JSON.stringify(outerdp));
+
+ document.getElementById("outerdiv").remove();
+ }
+
+ async function test() {
+ // We test a variety of scroll frame heights.
+ // The first argument of testOne is the scroll frame height.
+ // The second argument is the allowed scale factor of scroll frame height
+ // to display port height.
+ // In the comment following each line we record the values of the display
+ // port height at the time of writing the test in both the good (ie with
+ // the bug this test is testing fixed), and bad (before the bug this
+ // test is testing fixed) cases. These values can obviously be different,
+ // but it gives a good idea that the good and bad values are far apart so
+ // this test should be robust, and provides good context in the future if
+ // this test starts failing.
+ await testOne( 50, 5.2, "(height 50)"); // good 256, bad 256
+ await testOne(128, 2.1, "(height128)"); // good 256, bad 512
+ await testOne(200, 2.0, "(height200)"); // good 384, bad 768
+ await testOne(256, 1.6, "(height256)"); // good 384, bad 768
+ await testOne(329, 1.6, "(height329)"); // good 512, bad 896
+ await testOne(500, 1.3, "(height500)"); // good 640, bad 280
+ await testOne(640, 1.7, "(height640)"); // good 1024, bad 1536
+
+ }
+
+ waitUntilApzStable()
+ .then(forceLayerTreeToCompositor)
+ .then(test)
+ .then(subtestDone, subtestFailed);
+ </script>
+</head>
+<body>
+ <a id="theplace" target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1689492">Mozilla Bug 1689492</a>
+ <!-- Put enough content into the page to make it have a nonzero scroll range, not needed -->
+ <div style="height: 5000px"></div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_checkerboard_apzforcedisabled.html b/gfx/layers/apz/test/mochitest/helper_checkerboard_apzforcedisabled.html
new file mode 100644
index 0000000000..404368a803
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_checkerboard_apzforcedisabled.html
@@ -0,0 +1,93 @@
+<!DOCTYPE HTML>
+<html id="root-element">
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Checkerboarding while root scrollframe async-scrolls and a
+ subframe has APZ force disabled</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+async function test() {
+ var utils = SpecialPowers.getDOMWindowUtils(window);
+ var subframe = document.getElementById('subframe');
+
+ // layerize subframe
+ await promiseNativeMouseEventWithAPZAndWaitForEvent({
+ type: "click",
+ target: subframe,
+ offsetX: 10,
+ offsetY: 10,
+ });
+
+ // verify layerization
+ await promiseAllPaintsDone();
+ ok(isLayerized("subframe"), "subframe should be layerized at this point");
+ var subframeScrollId = utils.getViewId(subframe);
+ ok(subframeScrollId > 0, "subframe should have a scroll id");
+
+ // then disable APZ for it
+ utils.disableApzForElement(subframe);
+
+ // wait for the dust to settle
+ await promiseAllPaintsDone();
+
+ // Check that the root element's displayport has at least 500px of vertical
+ // displayport margin on either side. This will ensure that we can scroll
+ // by 500px without causing the displayport to move, which in turn means that
+ // the scroll will not trigger repaints (due to paint-skipping).
+ var rootElem = document.documentElement;
+ var rootDisplayport = getLastContentDisplayportFor(rootElem.id);
+ ok(rootDisplayport != null, "root element should have a displayport");
+ dump("root dp: " + JSON.stringify(rootDisplayport) +
+ ", height: " + rootElem.clientHeight);
+ var rootDpVerticalMargin = (rootDisplayport.height - rootElem.clientHeight) / 2;
+ ok(rootDpVerticalMargin > 500,
+ "root element should have at least 500px of vertical displayport margin");
+
+ // Scroll enough that we reveal new parts of the subframe, but not so much
+ // that the root displayport starts moving. If the root displayport moves,
+ // the main-thread will trigger a repaint of the subframe, but if the root
+ // displayport doesn't move, we get a paint-skipped scroll which is where the
+ // bug manifests. (The bug being that the subframe ends in a visual perma-
+ // checkerboarding state). Note that we do an 'auto' behavior scroll so
+ // that it's "instant" rather than an animation. Animations would demonstrate
+ // the bug too but are more complicated to wait for.
+ window.scrollBy({top: 500, left: 0, behavior: 'auto'});
+ is(window.scrollY, 500, "document got scrolled instantly");
+
+ // Note that at this point we must NOT call promiseOnlyApzControllerFlushed, because
+ // otherwise APZCCallbackHelper::NotifyFlushComplete will trigger a repaint
+ // (for unrelated reasons), and the repaint will clear the checkerboard
+ // state. We do, however, want to wait for a "steady state" here that
+ // includes all pending paints from the main thread and a composite that
+ // samples the APZ state. In order to accomplish this we wait for all the main
+ // thread paints, and then force a composite via advanceTimeAndRefresh. The
+ // advanceTimeAndRefresh has the additional advantage of freezing the refresh
+ // driver which avoids any additional externally-triggered repaints from
+ // erasing the symptoms of the bug.
+ await promiseAllPaintsDone();
+ assertNotCheckerboarded(utils, subframeScrollId, "subframe");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+ <style>
+ #subframe {
+ overflow-x: auto;
+ margin-left: 100px; /* makes APZ minimap easier to see */
+ }
+ </style>
+</head>
+<body>
+ <div id="subframe">
+ <div style="width: 10000px; height: 10000px; background-image: linear-gradient(green, red)">
+ </div>
+ </div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_checkerboard_no_multiplier.html b/gfx/layers/apz/test/mochitest/helper_checkerboard_no_multiplier.html
new file mode 100644
index 0000000000..149ef9fbba
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_checkerboard_no_multiplier.html
@@ -0,0 +1,57 @@
+<!DOCTYPE html>
+<html lang="en"><head>
+<meta http-equiv="content-type" content="text/html; charset=UTF-8"><meta charset="utf-8">
+<title>Testcase for checkerboarding with displayport multipliers dropped to zero</title>
+<script type="application/javascript" src="apz_test_utils.js"></script>
+<script src="/tests/SimpleTest/paint_listener.js"></script>
+<meta name="viewport" content="width=device-width"/>
+<style>
+body, html {
+ margin: 0;
+}
+</style>
+<body>
+ <div style="height: 5000px; background-color: green"></div>
+</body>
+<script type="application/javascript">
+async function test() {
+ var utils = SpecialPowers.getDOMWindowUtils(window);
+ var scrollerId = utils.getViewId(document.scrollingElement);
+
+ // Zoom in a bunch
+ const scale = 3.0;
+ utils.setResolutionAndScaleTo(scale);
+
+ // And now we scroll the visual viewport to cover the range it has inside
+ // the layout viewport, plus a bit more so that we also cover the boundary
+ // case where the layout viewport has to move.
+ // At each scroll position, we make sure there's no checkerboarding.
+ // We advance the scroll position on each axis by 43 CSS pixels at a time,
+ // because 43 is a non-power-of-two/prime number and should give us reasonable
+ // coverage of different displayport tile alignment values. Making the
+ // increment too small increases runtime and too large might miss some
+ // alignment values so this seems like a good number.
+
+ async function scrollAndCheck(x, y) {
+ dump(`Scrolling visual viewport to ${x}, ${y}\n`);
+ utils.scrollToVisual(x, y, utils.UPDATE_TYPE_MAIN_THREAD, utils.SCROLL_MODE_INSTANT);
+ await promiseApzFlushedRepaints();
+ assertNotCheckerboarded(utils, scrollerId, `At ${x}, ${y}`);
+ }
+
+ let vv_scrollable_x = window.innerWidth - (window.innerWidth / scale);
+ for (var x = 0; x < vv_scrollable_x + 100; x += 43) {
+ await scrollAndCheck(x, 0);
+ }
+ ok(window.scrollX == 0, "Layout viewport couldn't move on the x-axis, page not scrollable that way");
+ let vv_scrollable_y = window.innerHeight - (window.innerHeight / scale);
+ for (var y = 0; y < vv_scrollable_y + 100; y += 43) {
+ await scrollAndCheck(0, y);
+ }
+ ok(window.scrollY > 0, `Layout viewport moved down to ${window.scrollY} on the y-axis`);
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed)
+</script>
diff --git a/gfx/layers/apz/test/mochitest/helper_checkerboard_scrollinfo.html b/gfx/layers/apz/test/mochitest/helper_checkerboard_scrollinfo.html
new file mode 100644
index 0000000000..2e718ad44b
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_checkerboard_scrollinfo.html
@@ -0,0 +1,91 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Scrolling a scrollinfo layer and making sure it doesn't checkerboard</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <meta name="viewport" content="width=device-width"/>
+<style>
+ #withfilter {
+ filter: url(#menushadow);
+ }
+
+ #scroller {
+ width: 300px;
+ height: 1038px;
+ overflow: scroll;
+ }
+
+ .spacer {
+ height: 1878px;
+ background-image: linear-gradient(red, blue);
+ }
+</style>
+</head>
+<body>
+ <div id="withfilter">
+ <div id="scroller">
+ <div class="spacer"></div>
+ </div>
+ </div>
+<!-- the SVG below copied directly from the Gecko Profiler code that
+ demonstrated the original bug. It basically generates a bit of a "drop
+ shadow" effect on the div it's applied to. Original SVG can be found at
+ https://github.com/firefox-devtools/profiler/blame/624f71bce5469cf4f8b2be720e929ba69fa6bfdc/res/img/svg/shadowfilter.svg -->
+ <svg xmlns="http://www.w3.org/2000/svg">
+ <defs>
+ <filter id="menushadow" color-interpolation-filters="sRGB" x="-10" y="-10" width="30" height="30">
+ <feComponentTransfer in="SourceAlpha">
+ <feFuncA type="linear" slope="0.3"/>
+ </feComponentTransfer>
+ <feGaussianBlur stdDeviation="5"/>
+ <feOffset dy="10" result="shadow"/>
+ <feComponentTransfer in="SourceAlpha">
+ <feFuncA type="linear" slope="0.1"/>
+ </feComponentTransfer>
+ <feMorphology operator="dilate" radius="0.5" result="rim"/>
+ <feMerge><feMergeNode in="shadow"/><feMergeNode in="rim"/></feMerge>
+ <feComposite operator="arithmetic" in2="SourceAlpha" k2="1" k3="-0.1"/>
+ <feMerge><feMergeNode/><feMergeNode in="SourceGraphic"/></feMerge>
+ </filter>
+ </defs>
+ </svg>
+</body>
+<script type="application/javascript">
+async function test() {
+ var scroller = document.querySelector("#scroller");
+ var utils = SpecialPowers.getDOMWindowUtils(window);
+ var scrollerId = utils.getViewId(scroller);
+
+ // Scroll to the bottom of the page, so that the bottom of #scroller is
+ // visible; that's where the checkerboarding happens.
+ document.scrollingElement.scrollTop = document.scrollingElement.scrollTopMax;
+
+ // After the first call to promiseApzFlushedRepaints, the scroller will have
+ // zero displayport margins (because it's inside an SVG filter, and so takes
+ // the "scroll info layer" codepath in APZ's CalculatePendingDisplayPort
+ // function. The main-thread then computes a displayport using those zero
+ // margins and alignment heuristics. If those heuristics are buggy, then the
+ // scroller may end up checkerboarding. That's what we check for on each
+ // scroll increment.
+
+ // The scroll values here just need to be "thorough" enough to exercise the
+ // code at different alignments, so using a non-power-of-two or prime number
+ // for the increment seems like a good idea. The smaller the increment, the
+ // longer the test takes to run (because more iterations) so we don't want it
+ // too small either.
+ for (var y = 3; y <= scroller.scrollTopMax; y += 17) {
+ dump(`Scrolling scroller to ${y}\n`);
+ scroller.scrollTo(0, y);
+ await promiseApzFlushedRepaints();
+ assertNotCheckerboarded(utils, scrollerId, `At y=${y}`);
+ }
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+</script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_checkerboard_zoom_during_load.html b/gfx/layers/apz/test/mochitest/helper_checkerboard_zoom_during_load.html
new file mode 100644
index 0000000000..650b1c265d
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_checkerboard_zoom_during_load.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<html lang="en"><head>
+<meta http-equiv="content-type" content="text/html; charset=UTF-8"><meta charset="utf-8">
+<title>Testcase for checkerboarding after zooming during page load</title>
+<script type="application/javascript" src="apz_test_utils.js"></script>
+<script src="/tests/SimpleTest/paint_listener.js"></script>
+<meta name="viewport" content="width=device-width"/>
+<style>
+body, html {
+ margin: 0;
+}
+</style>
+<body>
+ <div style="height: 5000px; background-color: green"></div>
+</body>
+<script type="application/javascript">
+
+// This function runs after page load, but simulates what might happen if
+// the user does a zoom during page load. It's hard to actually do this
+// during page load, because the specific behaviour depends on interleaving
+// between paints and the load event which is hard to control from a test.
+// So instead, we do the zoom after page load, and then trigger a MVM reset
+// which simulates what happens during the pageload process.
+async function test() {
+ var utils = SpecialPowers.getDOMWindowUtils(window);
+
+ // Make it so that the layout and visual viewports diverge. We do this by
+ // zooming and then moving the visual viewport.
+ utils.setResolutionAndScaleTo(2);
+ var x = window.innerWidth / 2;
+ var y = window.innerHeight / 2;
+ utils.scrollToVisual(x, y, utils.UPDATE_TYPE_MAIN_THREAD, utils.SCROLL_MODE_INSTANT);
+ dump("Done scrollToVisual\n");
+
+ // Next, kick off a paint transaction to APZ, so that it sets appropriate
+ // displayport margins with visual/layout adjustment factors.
+ await promiseApzFlushedRepaints();
+
+ // Once that's done, we want to trigger the MobileViewportManager to update
+ // the displayport margins.
+ dump("Resetting MVM...\n");
+ utils.resetMobileViewportManager();
+
+ // The bug is that at this point, paints end up checkerboarding because the
+ // MVM code to update the displayport margins doesn't preserve the layout
+ // adjustment factor needed.
+ utils.advanceTimeAndRefresh(0);
+ assertNotCheckerboarded(utils, utils.getViewId(document.scrollingElement), `Should not see checkerboarding`);
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+</script>
diff --git a/gfx/layers/apz/test/mochitest/helper_checkerboard_zoomoverflowhidden.html b/gfx/layers/apz/test/mochitest/helper_checkerboard_zoomoverflowhidden.html
new file mode 100644
index 0000000000..63a07ebe9b
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_checkerboard_zoomoverflowhidden.html
@@ -0,0 +1,150 @@
+<!DOCTYPE HTML>
+<html id="root-element">
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Checkerboarding in while scrolling a subframe when root scrollframe has
+ overflow hidden and pinch zoomed in</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+async function test() {
+ var utils = SpecialPowers.getDOMWindowUtils(window);
+
+ var initial_resolution = await getResolution();
+ ok(initial_resolution > 0,
+ "The initial_resolution is " + initial_resolution + ", which is some sane value");
+
+ var subframe = document.getElementById('bugzilla-body');
+
+ // layerize subframe
+ await promiseNativeMouseEventWithAPZAndWaitForEvent({
+ type: "click",
+ target: subframe,
+ offsetX: 10,
+ offsetY: 10,
+ });
+
+ // verify layerization
+ await promiseAllPaintsDone();
+ ok(isLayerized("bugzilla-body"), "subframe should be layerized at this point");
+ var subframeScrollId = utils.getViewId(subframe);
+ ok(subframeScrollId > 0, "subframe should have a scroll id");
+
+ // wait for the dust to settle
+ await promiseAllPaintsDone();
+
+ let touchEndPromise = promiseTouchEnd(document.documentElement, 2);
+
+ // Ensure that APZ gets updated hit-test info
+ await promiseAllPaintsDone();
+
+ var zoom_in = [
+ [ { x: 130, y: 280 }, { x: 150, y: 300 } ],
+ [ { x: 120, y: 250 }, { x: 160, y: 380 } ],
+ [ { x: 115, y: 200 }, { x: 180, y: 410 } ],
+ [ { x: 110, y: 150 }, { x: 200, y: 440 } ],
+ [ { x: 105, y: 120 }, { x: 210, y: 470 } ],
+ [ { x: 100, y: 100 }, { x: 230, y: 500 } ],
+ ];
+
+ var touchIds = [0, 1];
+ await synthesizeNativeTouchSequences(document.body, zoom_in, null, touchIds);
+
+ await touchEndPromise;
+ // Flush state and get the resolution we're at now
+ await promiseApzFlushedRepaints();
+ let final_resolution = await getResolution();
+ ok(final_resolution > initial_resolution, "The final resolution (" + final_resolution + ") is greater than the initial resolution");
+
+ touchEndPromise = promiseTouchEnd(document.documentElement);
+
+ // pan back up to the top left
+ await promiseNativeTouchDrag(window,
+ 5,
+ 5,
+ 500,
+ 500,
+ 2);
+
+ await touchEndPromise; // wait for the touchend listener to fire
+ await promiseApzFlushedRepaints();
+ await promiseAllPaintsDone();
+
+ touchEndPromise = promiseTouchEnd(document.documentElement);
+
+ // pan right to expose the bug
+ await promiseNativeTouchDrag(window,
+ 100,
+ 20,
+ -180,
+ 0,
+ 3);
+
+ await touchEndPromise; // wait for the touchend listener to fire
+ await promiseApzFlushedRepaints();
+
+ assertNotCheckerboarded(utils, subframeScrollId, "Subframe");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+ <style>
+html,
+body {
+ overflow-y: hidden;
+ height: 100%;
+}
+
+body {
+ position: absolute;
+ margin: 0;
+ width: 100%;
+ height: 100%;
+}
+
+#wrapper {
+ position: initial !important;
+ display: flex;
+ flex-direction: column;
+ position: absolute;
+ overflow: hidden;
+ width: 100%;
+ height: 100%;
+}
+
+#bugzilla-body {
+ flex: auto;
+ position: relative;
+ outline: none;
+ padding: 0 15px;
+ overflow-x: auto;
+ overflow-y: scroll;
+ will-change: transform;
+}
+ </style>
+</head>
+<body>
+ <div id="wrapper">
+ <main id="bugzilla-body">
+ <p>STR:</p>
+ <ol>
+ <li>set <code>apz.allow_zoom</code> to <code>true</code></li>
+ <li>visit any bugzilla site (like this one)</li>
+ <li>zoom into the page and observe the left edge of the viewport</li>
+ </ol>
+ <p>ER: content should be shown<br>
+ AR: foreground content seems to disappear, looks like it's being cut off
+ </p>
+ <p>I attached a video of the STR to show the problem a little bit better. So far, I could only reproduce this on bugzilla. Words words words words words words words words words words words words words words words words words words words words words words.</p>
+
+ <div style="height: 10000px;"></div>
+ </main>
+ </div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_click.html b/gfx/layers/apz/test/mochitest/helper_click.html
new file mode 100644
index 0000000000..7c4501cb46
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_click.html
@@ -0,0 +1,42 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Sanity mouse-clicking test</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+async function clickButton() {
+ let clickPromise = new Promise(resolve => {
+ document.addEventListener("click", resolve);
+ });
+
+ if (getQueryArgs().dtc) {
+ // force a dispatch-to-content region on the document
+ document.addEventListener("wheel", function() { /* no-op */ }, { passive: false });
+ await promiseAllPaintsDone();
+ await promiseOnlyApzControllerFlushed();
+ }
+
+ await synthesizeNativeMouseEventWithAPZ(
+ { type: "click", target: document.getElementById("b"), offsetX: 5, offsetY: 5 },
+ () => dump("Finished synthesizing click, waiting for button to be clicked...\n")
+ );
+
+ let e = await clickPromise;
+ is(e.target, document.getElementById("b"), "Clicked on button, yay! (at " + e.clientX + "," + e.clientY + ")");
+}
+
+waitUntilApzStable()
+.then(clickButton)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</head>
+<body>
+ <button id="b" style="width: 10px; height: 10px"></button>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_click_interrupt_animation.html b/gfx/layers/apz/test/mochitest/helper_click_interrupt_animation.html
new file mode 100644
index 0000000000..adba0d90ea
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_click_interrupt_animation.html
@@ -0,0 +1,96 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width">
+ <title>Clicking on the content (not scrollbar) should interrupt animations</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="application/javascript">
+
+async function test() {
+ var scroller = document.documentElement;
+ var verticalScrollbarWidth = window.innerWidth - scroller.clientWidth;
+
+ if (verticalScrollbarWidth == 0) {
+ ok(true, "Scrollbar width is zero on this platform, test is useless here");
+ return;
+ }
+
+ // The anchor is the fixed-pos div that we use to calculate coordinates to
+ // click on the scrollbar. That way we don't have to recompute coordinates
+ // as the page scrolls. The anchor is at the bottom-right corner of the
+ // content area.
+ var anchor = document.getElementById('anchor');
+
+ var xoffset = (verticalScrollbarWidth / 2);
+ // Get a y-coord near the bottom of the vertical scrollbar track. Assume the
+ // vertical thumb is near the top of the scrollback track (since scroll
+ // position starts off at zero) and won't get in the way. Also assume the
+ // down arrow button, if there is one, is square.
+ var yoffset = 0 - verticalScrollbarWidth - 5;
+
+ // Take control of the refresh driver
+ let utils = SpecialPowers.getDOMWindowUtils(window);
+ utils.advanceTimeAndRefresh(0);
+
+ // Click at the bottom of the scrollbar track to trigger a page-down kind of
+ // scroll. This should use "desktop zooming" scrollbar code which should
+ // trigger an APZ scroll animation.
+ await promiseNativeMouseEventWithAPZAndWaitForEvent({
+ type: "click",
+ target: anchor,
+ offsetX: xoffset,
+ offsetY: yoffset,
+ eventTypeToWait: "mouseup"
+ });
+
+ // Run a few frames, that should be enough to let the scroll animation
+ // start. We check to make sure the scroll position has changed.
+ for (let i = 0; i < 5; i++) {
+ utils.advanceTimeAndRefresh(16);
+ }
+ await promiseOnlyApzControllerFlushed();
+
+ let curPos = scroller.scrollTop;
+ ok(curPos > 0,
+ `Scroll offset has moved down some, to ${curPos}`);
+
+ // Now we click on the content, which should cancel the animation. Run
+ // everything to reach a stable state.
+ await promiseNativeMouseEventWithAPZAndWaitForEvent({
+ type: "click",
+ target: anchor,
+ offsetX: -5,
+ offsetY: -5,
+ });
+ for (let i = 0; i < 1000; i++) {
+ utils.advanceTimeAndRefresh(16);
+ }
+ await promiseOnlyApzControllerFlushed();
+
+ // Ensure the scroll position hasn't changed since the last time we checked,
+ // which indicates the animation got interrupted.
+ is(scroller.scrollTop, curPos, `Scroll position hasn't changed again`);
+
+ utils.restoreNormalRefresh();
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</head>
+<body>
+ <div style="position:fixed; bottom: 0; right: 0; width: 1px; height: 1px" id="anchor"></div>
+ <div style="height: 300vh; margin-bottom: 10000px; background-image: linear-gradient(red,blue)"></div>
+ The above div is sized to 3x screen height so the linear gradient is more steep in terms of
+ color/pixel. We only scroll a few pages worth so we don't need the gradient all the way down.
+ And then we use a bottom-margin to make the page really big so the scrollthumb is
+ relatively small, giving us lots of space to click on the scrolltrack.
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_content_response_timeout.html b/gfx/layers/apz/test/mochitest/helper_content_response_timeout.html
new file mode 100644
index 0000000000..41a0319699
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_content_response_timeout.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/paint_listener.js"></script>
+<script src="apz_test_utils.js"></script>
+<script src="apz_test_native_event_utils.js"></script>
+<style>
+html { overflow-y: scroll; }
+body { margin: 0; }
+div { height: 1000vh; }
+</style>
+<div id='target'></div>
+<script>
+window.addEventListener('wheel', (e) => {
+ const timeAtStart = window.performance.now();
+ while (window.performance.now() - timeAtStart < 200) {
+ // Make a 200ms busy state.
+ }
+}, { passive: false});
+// Silence SimpleTest warning about missing assertions by having it wait
+// indefinitely. We don't need to give it an explicit finish because the
+// entire window this test runs in will be closed after subtestDone is called.
+SimpleTest.waitForExplicitFinish();
+</script>
+</script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_disallow_doubletap_zoom_inside_oopif.html b/gfx/layers/apz/test/mochitest/helper_disallow_doubletap_zoom_inside_oopif.html
new file mode 100644
index 0000000000..2c83cb1a1f
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_disallow_doubletap_zoom_inside_oopif.html
@@ -0,0 +1,58 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="user-scalable=no"/>
+ <title>Check that double tapping inside an oop iframe doesn't work if the top
+ level content document doesn't allow zooming</title>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script>
+
+async function test() {
+ let useTouchpad = (location.search == "?touchpad");
+
+ let resolution = await getResolution();
+ ok(resolution > 0,
+ "The initial_resolution is " + resolution + ", which is some sane value");
+
+ // Set up a Promise waiting for a TransformEnd which should never happen.
+ promiseTransformEnd().then(() => {
+ ok(false, "No TransformEnd should happen!");
+ });
+
+ // A double tap inside the OOP iframe.
+ await synthesizeDoubleTap(document.getElementById("target"), 20, 20, useTouchpad);
+
+ for (let i = 0; i < 10; i++) {
+ await promiseFrame();
+ }
+
+ // Flush state just in case.
+ await promiseApzFlushedRepaints();
+
+ let prev_resolution = resolution;
+ resolution = await getResolution();
+ is(resolution, prev_resolution, "No zoom should happen");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+<style>
+iframe {
+ margin: 0;
+ padding: 0;
+ border: 1px solid black;
+}
+</style>
+</head>
+<body>
+
+<iframe id="target" width="100" height="100" src="http://example.org/"></iframe>
+
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_displayport_expiry.html b/gfx/layers/apz/test/mochitest/helper_displayport_expiry.html
new file mode 100644
index 0000000000..023786a270
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_displayport_expiry.html
@@ -0,0 +1,77 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Test for DisplayPort Expiry</title>
+ <meta charset="utf-8">
+ <script src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+<style>
+#first {
+ height: 40vh;
+ width: 100%;
+ background: green;
+ overflow: scroll;
+}
+
+#second {
+ height: 40vh;
+ width: 100%;
+ background: yellow;
+ overflow: scroll;
+}
+
+#inner {
+ height: 20vh;
+ width: 50%;
+ background: red;
+}
+
+.big {
+ height: 100vh;
+ width: 100vw;
+}
+</style>
+</head>
+<body>
+ <div id="first">
+ <div class="big">
+ </div>
+ </div>
+ <br>
+ <div id="second">
+ <div id="inner">
+ <div class="big">
+ </div>
+ </div>
+ <div class="big">
+ </div>
+ </div>
+</body>
+ <script>
+async function test() {
+ await promiseFrame();
+
+ let paintCount = 0;
+ function countPaints(e) {
+ paintCount += 1;
+ }
+
+ window.addEventListener("MozAfterPaint", countPaints);
+
+ await SpecialPowers.promiseTimeout(200);
+
+ window.removeEventListener("MozAfterPaint", countPaints);
+
+ info("paint count: " + paintCount);
+
+ ok(paintCount < 5, "Paint count is within the expect range");
+}
+
+SimpleTest.waitForExplicitFinish();
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+ </script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_div_pan.html b/gfx/layers/apz/test/mochitest/helper_div_pan.html
new file mode 100644
index 0000000000..b740b2f16a
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_div_pan.html
@@ -0,0 +1,43 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Sanity panning test for scrollable div</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+async function test() {
+ let transformEndPromise = promiseTransformEnd();
+
+ await synthesizeNativeTouchDrag(document.getElementById("outer"), 10, 100, 0, -50);
+ dump("Finished native drag, waiting for transform-end observer...\n");
+
+ await transformEndPromise;
+
+ dump("Transform complete; flushing repaints...\n");
+ await promiseOnlyApzControllerFlushed();
+
+ var outerScroll = document.getElementById("outer").scrollTop;
+ is(outerScroll, 50, "check that the div scrolled");
+}
+
+waitUntilApzStable()
+ .then(test)
+ .then(subtestDone);
+
+ </script>
+</head>
+<body>
+ <div id="outer" style="height: 250px; border: solid 1px black; overflow:scroll">
+ <div style="height: 5000px; background-color: lightblue">
+ This div makes the |outer| div scrollable.
+ </div>
+ </div>
+ <div style="height: 5000px; background-color: lightgreen;">
+ This div makes the top-level page scrollable.
+ </div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_dommousescroll.html b/gfx/layers/apz/test/mochitest/helper_dommousescroll.html
new file mode 100644
index 0000000000..390db367f5
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_dommousescroll.html
@@ -0,0 +1,33 @@
+<head>
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Test that Direct Manipulation generated pan gesture events generate DOMMouseScroll events with reasonable line scroll amounts</title>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+</head>
+<body>
+ <script type="application/javascript">
+async function test() {
+ let numLines = 0;
+ window.addEventListener("DOMMouseScroll", function (event) { numLines += event.detail; }, { passive: false });
+ let promise = new Promise(resolve => {
+ window.addEventListener("DOMMouseScroll", resolve, { passive: false });
+ });
+ await promiseApzFlushedRepaints();
+
+ await synthesizeTouchpadPan(20, 100, [0,0,0], [10,100,0], {});
+
+ await waitToClearOutAnyPotentialScrolls(window);
+ await promise;
+
+ info(numLines + " numLines");
+ ok(numLines < 10, "not too many lines");
+ ok(numLines > 0, "some lines");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</body>
diff --git a/gfx/layers/apz/test/mochitest/helper_doubletap_zoom.html b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom.html
new file mode 100644
index 0000000000..4cd613edbb
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=2100"/>
+ <title>Sanity check for double-tap zooming</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+async function test() {
+ let useTouchpad = (location.search == "?touchpad");
+
+ var resolution = await getResolution();
+ ok(resolution > 0,
+ "The initial_resolution is " + resolution + ", which is some sane value");
+
+ // Check that double-tapping once zooms in
+ await doubleTapOn(document.getElementById("target"), 10, 10, useTouchpad);
+ var prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution > prev_resolution, "The first double-tap has increased the resolution to " + resolution);
+
+ // Check that double-tapping again on the same spot zooms out
+ await doubleTapOn(document.getElementById("target"), 10, 10, useTouchpad);
+ prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution < prev_resolution, "The second double-tap has decreased the resolution to " + resolution);
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+ <style type="text/css">
+ .box {
+ width: 800px;
+ height: 500px;
+ margin: 0 auto;
+ }
+</style>
+</head>
+<body>
+<div class="box">Text before the div.</div>
+<div id="target" style="margin-left: 100px; width:900px; height: 400px; background-image: linear-gradient(blue,red)"></div>
+<div class="box">Text after the div.</div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_bug1702464.html b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_bug1702464.html
new file mode 100644
index 0000000000..34d06fc039
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_bug1702464.html
@@ -0,0 +1,90 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=2100"/>
+ <title>Check that double tapping internal calculations correctly convert the tap point</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+async function test() {
+ let useTouchpad = (location.search == "?touchpad");
+
+ const deviceScale = window.devicePixelRatio;
+ let target2 = document.getElementById("target2")
+
+ info("deviceScale " + deviceScale);
+ info("window.innerwh " + window.innerWidth + " " + window.innerHeight);
+ info("vv " + visualViewport.offsetLeft + " " + visualViewport.offsetTop + " " + visualViewport.width + " " + visualViewport.height);
+
+ let resolution = await getResolution();
+ let vvHeightAtUnitZoom = visualViewport.height * resolution;
+
+ // The max amount of zoom is 10 (as defined in dom/base/nsViewportInfo.h),
+ // but that includes the deviceScale (bug 1700865 is filed about this), so
+ // this is the max amount of zoom that double tap can increase by.
+ let maxPinchZoom = 10/deviceScale;
+
+ // Compute the visual viewport size at that max zoom.
+ let minVVHeight = vvHeightAtUnitZoom / maxPinchZoom;
+
+ // Make the element height to just fit inside the minimum visual viewport
+ // height, minus the margins that get added for the zoom target rect (15 on
+ // each side) and a little wiggle room just in case (rounding, etc).
+ let elementHeight = Math.floor(minVVHeight) - 2*15 - 4;
+
+ ok(elementHeight > 10, "tall enough element");
+
+ // And then make the element skinnier than the window size so it triggers
+ // the bug. (half the aspect ratio minus 5 just to be sure)
+ let elementWidth = Math.max(12, Math.floor(elementHeight * window.innerWidth / (2 * window.innerHeight)) - 5);
+
+ info("element size " + elementWidth + " " + elementHeight);
+
+ target2.style.width = elementWidth + "px";
+ target2.style.height = elementHeight + "px";
+
+ await promiseApzFlushedRepaints();
+
+ resolution = await getResolution();
+ ok(resolution > 0,
+ "The initial_resolution is " + resolution + ", which is some sane value");
+
+ // Check that double-tapping once zooms in
+ await doubleTapOn(document.getElementById("target1"), 10, 10, useTouchpad);
+ var prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution > prev_resolution, "The first double-tap has increased the resolution to " + resolution);
+
+ // Check that double-tapping the smaller element zooms in more
+ await doubleTapOn(target2, 8, elementHeight-8, useTouchpad);
+ prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution > prev_resolution, "The second double-tap has increased the resolution to " + resolution);
+
+ let rect = target2.getBoundingClientRect();
+ info("rect " + rect.x + " " + rect.y + " " + rect.width + " " + rect.height);
+ info("vv " + visualViewport.offsetLeft + " " + visualViewport.offsetTop + " " + visualViewport.width + " " + visualViewport.height);
+
+ ok(visualViewport.offsetLeft < rect.x, "visual viewport contains zoom element left");
+ ok(visualViewport.offsetTop < rect.y, "visual viewport contains zoom element top");
+ ok(visualViewport.offsetLeft + visualViewport.width > rect.x + rect.width, "visual viewport contains zoom element right");
+ ok(visualViewport.offsetTop + visualViewport.height > rect.y + rect.height, "visual viewport contains zoom element bottom");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</head>
+<body>
+
+<div id="target1" style="background: blue; width: 50vw; height: 200px; position: absolute; top: 50vh;">
+ <div id="target2" style="background: green; width: 50px; height: 135px; position: absolute; right: 0;"></div>
+</div>
+
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_fixedpos.html b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_fixedpos.html
new file mode 100644
index 0000000000..1da5607d7e
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_fixedpos.html
@@ -0,0 +1,88 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=2100"/>
+ <title>Check that double tapping active scrollable elements in fixed pos work</title>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script>
+
+async function makeActive(x, y, targetId) {
+ let theTarget = document.getElementById(targetId);
+ await promiseNativeMouseEventWithAPZAndWaitForEvent({
+ type: "click",
+ target: theTarget,
+ offsetX: x,
+ offsetY: y,
+ });
+
+ await promiseApzFlushedRepaints();
+
+ ok(isLayerized(targetId), "target should be layerized at this point");
+ let utils = SpecialPowers.getDOMWindowUtils(window);
+ let targetScrollId = utils.getViewId(theTarget);
+ ok(targetScrollId > 0, "target should have a scroll id");
+}
+
+async function test() {
+ let useTouchpad = (location.search == "?touchpad");
+
+ let resolution = await getResolution();
+ ok(resolution > 0,
+ "The initial_resolution is " + resolution + ", which is some sane value");
+
+ await makeActive(100, 50, "target");
+
+ let target = document.getElementById("target");
+
+ // Check that double-tapping once zooms in
+ await doubleTapOn(target, 100, 50, useTouchpad);
+ let prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution > prev_resolution, "The first double-tap has increased the resolution to " + resolution);
+
+ // Check that double-tapping again on the same spot zooms out
+ await doubleTapOn(target, 100, 50, useTouchpad);
+ prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution < prev_resolution, "The second double-tap has decreased the resolution to " + resolution);
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+<style>
+.fixed {
+ top: 100px;
+ width: 500px;
+ height: 300px;
+ background: blue;
+ position: fixed;
+}
+.abox {
+ width: 200px;
+ height: 100px;
+ background: yellow;
+ overflow: auto;
+}
+.spacer {
+ height: 400vh;
+ background: lightgrey;
+}
+</style>
+</head>
+<body>
+
+<div class="fixed">
+ <div class="abox" id="target">
+ <div class="spacer" style="width: 50px;"></div>
+ </div>
+</div>
+<div class="spacer" style="width: 100px;"></div>
+
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_fixedpos_overflow.html b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_fixedpos_overflow.html
new file mode 100644
index 0000000000..0658c562c1
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_fixedpos_overflow.html
@@ -0,0 +1,113 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=2100"/>
+ <title>Check that double tapping elements with large overflow inside active scrollable elements in fixed pos work</title>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script>
+
+async function makeActive(x, y, targetId) {
+ let theTarget = document.getElementById(targetId);
+ await promiseNativeMouseEventWithAPZAndWaitForEvent({
+ type: "click",
+ target: theTarget,
+ offsetX: x,
+ offsetY: y,
+ });
+
+ await promiseApzFlushedRepaints();
+
+ ok(isLayerized(targetId), "target should be layerized at this point");
+ let utils = SpecialPowers.getDOMWindowUtils(window);
+ let targetScrollId = utils.getViewId(theTarget);
+ ok(targetScrollId > 0, "target should have a scroll id");
+}
+
+async function test() {
+ let useTouchpad = (location.search == "?touchpad");
+
+ let resolution = await getResolution();
+ ok(resolution > 0,
+ "The initial_resolution is " + resolution + ", which is some sane value");
+
+ await makeActive(25, 25, "scrollertarget");
+
+ let target = document.getElementById("target");
+
+ // Check that double-tapping once zooms in
+ // Coords outside of the main rect but inside the overflow to trigger the
+ // bug we are testing.
+ await doubleTapOn(target, 25, 120, useTouchpad);
+ let prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution > prev_resolution, "The first double-tap has increased the resolution to " + resolution);
+
+ // Check that double-tapping again on the same spot zooms out
+ await doubleTapOn(target, 25, 120, useTouchpad);
+ prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution < prev_resolution, "The second double-tap has decreased the resolution to " + resolution);
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+<style>
+.fixed {
+ top: 100px;
+ width: 500px;
+ height: 300px;
+ background: blue;
+ position: fixed;
+}
+.abox {
+ width: 200px;
+ height: 200px;
+ background: yellow;
+ overflow: auto;
+}
+.spacer {
+ height: 400vh;
+ background: lightgrey;
+}
+</style>
+</head>
+<body>
+
+<div class="fixed">
+ <div id="scrollertarget" class="abox">
+ <div class="spacer" style="width: 150px;">
+ <div id="target" style="background-color: #eee; width: 145px; height: 50px; border: 1px dotted black; overflow: visible;">
+ Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+ Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
+ Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
+ Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+ Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+ Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
+ Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
+ Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+ Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+ Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
+ Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
+ Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+ Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+ Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
+ Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
+ Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+ Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+ Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
+ Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
+ Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+ </div>
+ </div>
+ </div>
+</div>
+<div class="spacer" style="width: 100px;"></div>
+
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_gencon.html b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_gencon.html
new file mode 100644
index 0000000000..01b1f060d8
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_gencon.html
@@ -0,0 +1,101 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=2100"/>
+ <title>Check that on generated content works</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+async function test() {
+ let useTouchpad = (location.search == "?touchpad");
+
+ let resolution = await getResolution();
+ let initial_resolution = resolution;
+ ok(resolution > 0,
+ "The initial_resolution is " + resolution + ", which is some sane value");
+
+ let target = document.getElementById("target");
+
+ info("tar " + target.getBoundingClientRect().width);
+
+ // Check that first double tap zooms in
+ info("sending first double tap");
+ await doubleTapOn(target, 10, 10, useTouchpad);
+ let prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution > prev_resolution, "After double-tap the resolution has increased to " + resolution);
+
+ // Check that second double tap zooms out
+ info("sending second double tap");
+ await doubleTapOn(target, 10, 10, useTouchpad);
+ prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution < prev_resolution, "After double-tap the resolution has decreased to " + resolution);
+ ok(resolution == initial_resolution, "After double-tap the resolution has decreased to initial_resolution");
+
+ info(" window.innerWidth " + window.innerWidth);
+
+ // Check that third double tap zooms in
+ info("sending third double tap");
+ await doubleTapOn(document.getElementById("placeholder"), 10, 10, useTouchpad);
+ prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution > prev_resolution, "After double-tap the resolution has increased to " + resolution);
+
+ info(" window.innerWidth " + window.innerWidth);
+
+ // Check that fourth double tap zooms out
+ info("sending forth double tap");
+ await doubleTapOn(document.getElementById("placeholder"), 10, 10, useTouchpad);
+ prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution < prev_resolution, "After double-tap the resolution has decreased to " + resolution);
+ ok(resolution == initial_resolution, "After double-tap the resolution has decreased to initial_resolution");
+
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+ <style>
+body, html {
+ margin: 0;
+}
+.withafter {
+ width: 200px;
+ height: 200px;
+ left: 0;
+ background: green;
+ position: relative;
+}
+.withafter::after {
+ width: 20vw;
+ height: 100px;
+ background: blue;
+ position: absolute;
+ left: 80vw;
+ content: 'after';
+}
+.placeholder {
+ width: 20vw;
+ height: 100px;
+ background: blue;
+ position: absolute;
+ left: 80vw;
+ top:0;
+ z-index: -10;
+}
+</style>
+</head>
+<body>
+
+<div id="target" class="withafter">some text</div>
+<div id="placeholder" class="placeholder"></div>
+
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_horizontal_center.html b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_horizontal_center.html
new file mode 100644
index 0000000000..ab945fab1f
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_horizontal_center.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=2100"/>
+ <title>Check that double tapping an element that doesn't fill the width of the viewport as maximum zoom centers it</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+async function test() {
+ let useTouchpad = (location.search == "?touchpad");
+
+ var resolution = await getResolution();
+ ok(resolution > 0,
+ "The initial_resolution is " + resolution + ", which is some sane value");
+
+ // Check that double-tapping once zooms in
+ await doubleTapOn(document.getElementById("target"), 10, 10, useTouchpad);
+ var prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution > 2*prev_resolution, "The first double-tap has increased the resolution to " + resolution);
+
+ info("window.innerWidth " + window.innerWidth); // value when writing this test: 1280
+ info("visualViewport.offsetLeft " + visualViewport.offsetLeft); //value when writing this test: 625 with bug, 537 with fix
+ info("visualViewport.width " + visualViewport.width); // value when writing this test: 253
+ // the left hand edge of the div is at window.innerWidth/2
+ // we want approximately half of the visual viewport width to be to the left of that point.
+ // we have to remove the 50 pixels for the width of the div from that first though.
+ // we multiply that by 80% to factor in the 15 pixel margin we give the zoomed-to element
+ // (we don't hard code 15 though since we might want to tweak that)
+ ok(visualViewport.offsetLeft < window.innerWidth/2 - 0.8*(visualViewport.width-50)/2, "moved over far enough");
+ // and then have a sanity check that it's not too small.
+ // using the same 20% factor as before but in the other direction.
+ ok(visualViewport.offsetLeft > window.innerWidth/2 - 1.2*(visualViewport.width-50)/2, "but not too far");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</head>
+<body>
+
+<div id="target" style="background: blue; width: 50px; height: 50px; position: absolute; left: 50vw;"></div>
+
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_hscrollable.html b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_hscrollable.html
new file mode 100644
index 0000000000..21c3fb7e70
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_hscrollable.html
@@ -0,0 +1,85 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=2100"/>
+ <title>Check that tall element wider than the viewport doesn't scroll to the top</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+// Although this test has hscrollable in the name, it does not test any
+// horizontal scrolling. Rather it is the mere presence of horizontally
+// scrollable content that triggers this bug (it confused the code that
+// picked a rect to zoom to).
+
+async function test() {
+ let useTouchpad = (location.search == "?touchpad");
+
+ let resolution = await getResolution();
+ ok(resolution > 0,
+ "The initial_resolution is " + resolution + ", which is some sane value");
+
+ // instant scroll down
+ window.scrollTo({
+ top: window.innerHeight * 2,
+ left: 0,
+ behavior: 'auto'
+ });
+
+ await promiseApzFlushedRepaints();
+
+ let scrollPos = window.scrollY;
+ ok(scrollPos > window.innerHeight * 2 - 50, "window scrolled down");
+
+ info("window.scrollY " + window.scrollY);
+
+ info("window.innerHeight " + window.innerHeight);
+
+ info("document.documentElement.scrollHeight " + document.documentElement.scrollHeight);
+
+ let target = document.getElementById("target");
+
+ let x = 20;
+ let y = scrollPos + window.innerHeight / 2;
+
+ // Check that second double tap does not scroll up
+ info("sending second double tap");
+ await doubleTapOn(target, x, y, useTouchpad);
+ prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution == prev_resolution, "After double-tap the resolution is the same: " + resolution);
+
+ ok(window.scrollY > window.innerHeight * 2 - 50, "window is still scrolled down");
+ ok(Math.abs(window.scrollY - scrollPos) < 1, "window didnt scroll");
+ info("window.scrollY " + window.scrollY);
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+ <style>
+ .spacer {
+ background-color: #eee;
+ height: 800vh;
+ width: 200vw;
+ }
+ .rect {
+ width: 90vw;
+ height: 30px;
+ background-color: #aaa;
+ }
+</style>
+</head>
+<body>
+<div id="firsttarget" class="rect">
+</div>
+
+<div id="target" class="spacer">
+</div>
+
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_hscrollable2.html b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_hscrollable2.html
new file mode 100644
index 0000000000..fc74ff1c89
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_hscrollable2.html
@@ -0,0 +1,109 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=2100"/>
+ <title>Check that tall element wider than the viewport after zooming in doesn't scroll up</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+async function test() {
+ let useTouchpad = (location.search == "?touchpad");
+
+ let resolution = await getResolution();
+ ok(resolution > 0,
+ "The initial_resolution is " + resolution + ", which is some sane value");
+
+ // instant scroll down so the rect is roughly halfway down
+ window.scrollTo({
+ top: window.innerHeight * 3.5,
+ left: 0,
+ behavior: 'auto'
+ });
+
+ await promiseApzFlushedRepaints();
+
+ let scrollPos = window.scrollY;
+ ok(scrollPos > window.innerHeight * 3.5 - 50, "window scrolled down");
+
+ info("window.scrollY " + window.scrollY);
+
+ info("window.innerHeight " + window.innerHeight);
+
+ info("document.documentElement.scrollHeight " + document.documentElement.scrollHeight);
+
+ let target = document.getElementById("target");
+
+ // Check that first double tap does not scroll up
+ info("sending first double tap");
+ await doubleTapOn(target, 15, 15, useTouchpad);
+ let prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution > prev_resolution, "After double-tap the resolution increased to " + resolution);
+
+ // These values were determined experimentally, have not investigated the
+ // reason for the difference between platforms or the large tolerance needed.
+ let tolerance = 2;
+ if (getPlatform() == "mac") {
+ tolerance = 24;
+ }
+
+ ok(window.scrollY > window.innerHeight * 3.5 - tolerance, "window is still scrolled down");
+ ok(Math.abs(window.scrollY - scrollPos) < tolerance, "window didnt scroll: " + Math.abs(window.scrollY - scrollPos));
+ info("window.scrollY " + window.scrollY);
+
+ // Check that second double tap does not scroll up
+ // Intentionally miss the target and hit the large spacer div, which
+ // should cause us to zoom out, but not scroll up (too much).
+ info("sending second double tap");
+ await doubleTapOn(target, -10, 15, useTouchpad);
+ prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution < prev_resolution, "After double-tap the resolution decreased to " + resolution);
+
+ // These values were determined experimentally, have not investigated the
+ // reason for the difference between platforms or the large tolerance needed.
+ let amountToExpectScrollUp = 0;
+ if (getPlatform() == "android") {
+ amountToExpectScrollUp = 767;
+ tolerance = 4.6;
+ }
+ scrollPos -= amountToExpectScrollUp;
+
+ ok(window.scrollY > window.innerHeight * 3.5 - tolerance - amountToExpectScrollUp, "window is still scrolled down");
+ ok(Math.abs(window.scrollY - scrollPos) < tolerance, "window didnt scroll: " + Math.abs(window.scrollY - scrollPos));
+ info("window.scrollY " + window.scrollY);
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+ <style>
+ .spacer {
+ background-color: #eee;
+ height: 800vh;
+ width: 1600vw;
+ }
+ .rect {
+ position: absolute;
+ width: 30px;
+ height: 30px;
+ background-color: #aaa;
+ top: 400vh;
+ right: 0;
+ }
+</style>
+</head>
+<body>
+<div id="target" class="rect">
+</div>
+
+<div class="spacer">
+</div>
+
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_htmlelement.html b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_htmlelement.html
new file mode 100644
index 0000000000..8fadc4eb3e
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_htmlelement.html
@@ -0,0 +1,76 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=2100"/>
+ <title>Check that double tapping on a scrollbar does not scroll to top</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+async function test() {
+ let useTouchpad = (location.search == "?touchpad");
+
+ var resolution = await getResolution();
+ var start_resolution = resolution;
+ ok(resolution > 0,
+ "The initial_resolution is " + resolution + ", which is some sane value");
+
+ // Check that double-tapping once zooms in
+ await doubleTapOn(document.getElementById("target"), 10, 10, useTouchpad);
+ var prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution > prev_resolution, "The first double-tap has increased the resolution to " + resolution);
+
+ // instant scroll to the middle of the page
+ window.scrollTo({
+ top: 4 * window.innerHeight,
+ left: 0,
+ behavior: 'auto'
+ });
+
+ await promiseApzFlushedRepaints();
+
+ let prevScrollX = window.scrollX;
+ let prevScrollY = window.scrollY;
+
+ ok(3.9 * window.innerHeight < window.scrollY && window.scrollY < 4.1 * window.innerHeight,
+ "scrollY looks good");
+
+ // Check that double-tapping on the bottom scrollbar does not scroll us to the top
+ // Need to divide by resolution because the coords are assumed to be inside the resolution
+ await doubleTapOn(window, (window.innerWidth/2)/resolution, (window.innerHeight - 5)/resolution, useTouchpad);
+ prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution < prev_resolution, "The second double-tap has decreased the resolution to " + resolution);
+ ok(resolution == start_resolution, "The second double-tap has decreased the resolution to the start to " + resolution);
+
+ info("prevscroll " + prevScrollX + " " + prevScrollY + "\n");
+ info("window.scroll " + window.scrollX + " " + window.scrollY + "\n");
+
+ ok(0.88*prevScrollY < window.scrollY && window.scrollY < prevScrollY*1.12, "scroll y didn't change by much");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+ <style type="text/css">
+ .spacer {
+ background-color: #eee;
+ width: 10px;
+ height: 800vh;
+ }
+</style>
+</head>
+<body>
+
+<div id="target" style="width: 100px; height: 100px; background: red;">
+</div>
+<div class="spacer">
+</div>
+
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_img.html b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_img.html
new file mode 100644
index 0000000000..2559a3dd23
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_img.html
@@ -0,0 +1,43 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=2100"/>
+ <title>Check that double tapping img works</title>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script>
+
+async function test() {
+ let useTouchpad = (location.search == "?touchpad");
+
+ var resolution = await getResolution();
+ ok(resolution > 0,
+ "The initial_resolution is " + resolution + ", which is some sane value");
+
+ // Check that double-tapping once zooms in
+ await doubleTapOn(document.getElementById("target"), 10, 10, useTouchpad);
+ var prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution > prev_resolution, "The first double-tap has increased the resolution to " + resolution);
+
+ // Check that double-tapping again on the same spot zooms out
+ await doubleTapOn(document.getElementById("target"), 10, 10, useTouchpad);
+ prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution < prev_resolution, "The second double-tap has decreased the resolution to " + resolution);
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</head>
+<body>
+
+<img id="target" width="100" height="100" src="green100x100.png">
+
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_large_overflow.html b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_large_overflow.html
new file mode 100644
index 0000000000..02c4ca52f8
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_large_overflow.html
@@ -0,0 +1,300 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=2100"/>
+ <title>Check that double tapping on overflow centers the zoom where we double tap</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+async function test() {
+ let useTouchpad = (location.search == "?touchpad");
+
+ var resolution = await getResolution();
+ ok(resolution > 0,
+ "The initial_resolution is " + resolution + ", which is some sane value");
+
+ // instant scroll to the bottom of the page
+ window.scrollTo({
+ top: 10000,
+ left: 0,
+ behavior: 'auto'
+ });
+
+ await promiseApzFlushedRepaints();
+
+ let scrollPos = window.scrollY;
+ ok(scrollPos > 1500, "window scrolled down");
+
+ info("window.scrollY " + window.scrollY);
+
+ info("window.innerHeight " + window.innerHeight);
+
+ info("document.documentElement.scrollHeight " + document.documentElement.scrollHeight);
+
+ let target = document.getElementById("target");
+
+ let x = 10;
+ let y = document.documentElement.scrollHeight - 60;
+
+ // Check that double-tapping once zooms in
+ await doubleTapOn(target, x, y, useTouchpad);
+ var prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution > prev_resolution, "The first double-tap has increased the resolution to " + resolution);
+
+ ok(window.scrollY > 1500, "window is still scrolled down");
+ ok(window.scrollY >= scrollPos-2, "window is still scrolled down");
+ info("window.scrollY " + window.scrollY);
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+ <style>
+ .container {
+ background-color: #eee;
+ width: 200px;
+ height: 50px;
+ border: 1px dotted black;
+ overflow: visible;
+ }
+</style>
+</head>
+<body>
+
+<div id="target" class="container">
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+Text text text text text text text text text text text text text text text text
+
+</div>
+
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_noscroll.html b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_noscroll.html
new file mode 100644
index 0000000000..00e3638ba7
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_noscroll.html
@@ -0,0 +1,59 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=2100"/>
+ <title>Check that double tapping something tall that we are already zoomed to doesn't scroll (it zooms out)</title>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script>
+
+function within(a, b, tolerance) {
+ return (Math.abs(a-b) < tolerance);
+}
+
+async function test() {
+ let useTouchpad = (location.search == "?touchpad");
+
+ let resolution = await getResolution();
+ let initial_resolution = resolution;
+ ok(resolution > 0,
+ "The initial_resolution is " + resolution + ", which is some sane value");
+
+ ok(window.scrollY == 0, "window not scrolled");
+ info("window.scrollY " + window.scrollY);
+
+ // Check that double-tapping once zooms in
+ await doubleTapOn(document.getElementById("target"), 10, 10, useTouchpad);
+ let prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution > prev_resolution, "The first double-tap has increased the resolution to " + resolution);
+ ok(window.scrollY == 0, "window not scrolled");
+ info("window.scrollY " + window.scrollY);
+
+ let x = document.getElementById("target").getBoundingClientRect().width/2;
+ let y = window.visualViewport.height - 20;
+ // Check that near the bottom doesn't scroll but zooms out
+ await doubleTapOn(document.getElementById("target"), x, y, useTouchpad);
+ prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution < prev_resolution, "The second double-tap has decreased the resolution to " + resolution);
+ // slight float inaccuracy, not sure why
+ ok(within(resolution, initial_resolution, 0.0002), "The second double-tap has restored the resolution to " + resolution);
+ ok(window.scrollY == 0, "window not scrolled");
+ info("window.scrollY " + window.scrollY);
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</head>
+<body>
+
+<div id="target" style="background: grey; width: 50vw; height: 300vh;">
+
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_nothing.html b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_nothing.html
new file mode 100644
index 0000000000..12005b3552
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_nothing.html
@@ -0,0 +1,46 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=2100"/>
+ <title>Check that double tapping when zoomed out and there is nothing to zoom to zooms in</title>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script>
+
+async function test() {
+ let useTouchpad = (location.search == "?touchpad");
+
+ let resolution = await getResolution();
+ let initial_resolution = resolution;
+ ok(resolution > 0,
+ "The initial_resolution is " + resolution + ", which is some sane value");
+
+ // Check that double-tapping once zooms in
+ await doubleTapOn(document.getElementById("thebody"), 20, 20, useTouchpad);
+ let prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution > prev_resolution, "The first double-tap has increased the resolution to " + resolution);
+
+ // Check that double-tapping again on the same spot zooms out
+ await doubleTapOn(document.getElementById("thebody"), 20, 20, useTouchpad);
+ prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution < prev_resolution, "The second double-tap has decreased the resolution to " + resolution);
+ ok(resolution == initial_resolution, "The second double-tap has decreased the resolution to the start resolution " + resolution);
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+<style>
+ body, html {margin: 0; width: 100%; height: 100%;}
+</style>
+</head>
+<body id="thebody">
+
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_nothing_listener.html b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_nothing_listener.html
new file mode 100644
index 0000000000..82805fe321
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_nothing_listener.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=2100"/>
+ <title>Check that double tapping when zoomed out and there is nothing to zoom to does not zoom in if this is a non-passive wheel listener</title>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script>
+
+function aListener() {
+ info("aListener called");
+ preventDefault();
+}
+
+async function test() {
+ let useTouchpad = (location.search == "?touchpad");
+
+ let resolution = await getResolution();
+ ok(resolution > 0,
+ "The initial_resolution is " + resolution + ", which is some sane value");
+
+ document.getElementById("thebody").addEventListener('wheel', aListener, {passive: false});
+ await promiseApzFlushedRepaints();
+
+ // Check that double-tapping does not zoom in
+ info("sending first double tap");
+ await doubleTapOn(document.getElementById("thebody"), 20, 20, useTouchpad);
+ let prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution == prev_resolution, "The first double-tap did not change the resolution: " + resolution);
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+<style>
+ body, html {margin: 0; width: 100%; height: 100%;}
+</style>
+</head>
+<body id="thebody">
+
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_oopif.html b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_oopif.html
new file mode 100644
index 0000000000..b6a51e8229
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_oopif.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=2100"/>
+ <title>Check that double tapping inside an oop iframe works</title>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script>
+
+async function test() {
+ let useTouchpad = (location.search == "?touchpad");
+
+ let resolution = await getResolution();
+ ok(resolution > 0,
+ "The initial_resolution is " + resolution + ", which is some sane value");
+
+ // Check that double-tapping once zooms in
+ await doubleTapOn(document.getElementById("target"), 20, 20, useTouchpad);
+ let prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution > prev_resolution, "The first double-tap has increased the resolution to " + resolution);
+
+ // Check that double-tapping again on the same spot zooms out
+ await doubleTapOn(document.getElementById("target"), 20, 20, useTouchpad);
+ prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution < prev_resolution, "The second double-tap has decreased the resolution to " + resolution);
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+<style>
+iframe {
+ margin: 0;
+ padding: 0;
+ border: 1px solid black;
+}
+</style>
+</head>
+<body>
+
+<iframe id="target" width="100" height="100" src="http://example.org/"></iframe>
+
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_scrolled_overflowhidden.html b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_scrolled_overflowhidden.html
new file mode 100644
index 0000000000..bba8eeec08
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_scrolled_overflowhidden.html
@@ -0,0 +1,82 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=2100"/>
+ <title>Check that double tapping when the page is overflow hidden and has been scrolled down by js works</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+async function test() {
+ let useTouchpad = (location.search == "?touchpad");
+
+ var resolution = await getResolution();
+ ok(resolution > 0,
+ "The initial_resolution is " + resolution + ", which is some sane value");
+
+ // instant scroll down
+ window.scrollTo({
+ top: window.innerHeight * 2 - 50,
+ left: 0,
+ behavior: 'auto'
+ });
+
+ await promiseApzFlushedRepaints();
+
+ let scrollPos = window.scrollY;
+ ok(scrollPos > window.innerHeight + 100, "window scrolled down");
+
+ info("window.scrollY " + window.scrollY);
+
+ info("window.innerHeight " + window.innerHeight);
+
+ info("document.documentElement.scrollHeight " + document.documentElement.scrollHeight);
+
+ let target = document.getElementById("target");
+
+ // Check that double-tapping once zooms in
+ info("sending double tap");
+ await doubleTapOn(target, 10, 10, useTouchpad);
+ var prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution > prev_resolution, "The first double-tap has increased the resolution to " + resolution);
+
+ ok(window.scrollY > window.innerHeight + 100, "window is still scrolled down");
+ ok(Math.abs(window.scrollY - scrollPos) < 2, "window didnt scroll");
+ info("window.scrollY " + window.scrollY);
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+ <style>
+ html, body {
+ overflow: hidden;
+ }
+ .spacer {
+ background-color: #eee;
+
+ height: 200vh;
+ }
+ .rect {
+ background-color: #aaa;
+ width: 100px;
+ height: 100px;
+ }
+</style>
+</head>
+<body>
+
+<div class="spacer">
+</div>
+<div id="target" class="rect">
+</div>
+<div class="spacer">
+</div>
+
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_shadowdom.html b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_shadowdom.html
new file mode 100644
index 0000000000..eed45028e2
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_shadowdom.html
@@ -0,0 +1,69 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=2100"/>
+ <title>Check that double tapping shadow dom works</title>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script>
+
+async function attach() {
+ let attachpoint = document.getElementById('attachpoint');
+ attachpoint.attachShadow({mode: 'open'}).innerHTML = "<span>some text</span>";
+
+ // flush layout
+ attachpoint.getBoundingClientRect();
+ await promiseApzFlushedRepaints();
+}
+
+async function test() {
+ let useTouchpad = (location.search == "?touchpad");
+
+ await attach();
+
+ var resolution = await getResolution();
+ ok(resolution > 0,
+ "The initial_resolution is " + resolution + ", which is some sane value");
+
+ // Check that double-tapping once zooms in
+ // This will hit the span inside the shadow dom, inline elements are not
+ // suitable zoom targets, so we will walk up, if you fail to walk out of
+ // the shadow tree we won't get an element and fail to zoom. If we succeed
+ // we'll hit the div with id target and zoom.
+ info("sending first double tap");
+ await doubleTapOn(document.getElementById("target"), 10, 10, useTouchpad);
+ var prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution > prev_resolution, "The first double-tap has increased the resolution to " + resolution);
+
+ // Check that double-tapping again on the same spot zooms out
+ info("sending second double tap");
+ await doubleTapOn(document.getElementById("target"), 10, 10, useTouchpad);
+ prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution < prev_resolution, "The second double-tap has decreased the resolution to " + resolution);
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+ <style>
+ .outer {
+ width: 200px;
+ height: 200px;
+ background: yellow;
+ }
+ </style>
+</head>
+<body>
+
+<div id="target" class="outer">
+ <div id="attachpoint"></div>
+</div>
+
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_small.html b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_small.html
new file mode 100644
index 0000000000..1a2a52aff8
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_small.html
@@ -0,0 +1,43 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=2100"/>
+ <title>Check that double tapping a small element works</title>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script>
+
+async function test() {
+ let useTouchpad = (location.search == "?touchpad");
+
+ var resolution = await getResolution();
+ ok(resolution > 0,
+ "The initial_resolution is " + resolution + ", which is some sane value");
+
+ // Check that double-tapping once zooms in
+ await doubleTapOn(document.getElementById("target"), 1, 1, useTouchpad);
+ var prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution > prev_resolution, "The first double-tap has increased the resolution to " + resolution);
+
+ // Check that double-tapping again on the same spot zooms out
+ await doubleTapOn(document.getElementById("target"), 1, 1, useTouchpad);
+ prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution < prev_resolution, "The second double-tap has decreased the resolution to " + resolution);
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</head>
+<body>
+
+<div id="target" style="background: blue; width: 3px; height: 3px;"></div>
+
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_smooth.html b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_smooth.html
new file mode 100644
index 0000000000..65b4c6698f
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_smooth.html
@@ -0,0 +1,161 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=2100"/>
+ <title>Check that double tapping zoom out animation is smooth</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+let hasPrev = false;
+let zoomingIn = true;
+let lastVVLeft = 0, lastVVTop = 0, lastVVWidth = 0, lastVVHeight = 0;
+let lastScrollX = 0, lastScrollY = 0;
+let lastResolution = 0;
+
+function within(a, b, tolerance) {
+ return (Math.abs(a-b) < tolerance);
+}
+
+async function afterpaint() {
+ info("vv pos " + visualViewport.pageLeft + "," + visualViewport.pageTop);
+ info("vv size " + visualViewport.width + "," + visualViewport.height);
+ info("win scroll " + window.scrollX + "," + window.scrollY);
+ info("res " + await getResolution());
+ if (hasPrev) {
+ ok(zoomingIn ?
+ lastVVLeft <= visualViewport.pageLeft :
+ lastVVLeft >= visualViewport.pageLeft,
+ "vvleft monotonic");
+ // When zooming in pageTop stays 0, when zooming out (at least on mac)
+ // the final value of pageTop is 2.5. Hence why the direction of these
+ // inequalities.
+ ok(zoomingIn ?
+ lastVVTop >= visualViewport.pageTop :
+ lastVVTop <= visualViewport.pageTop,
+ "vvtop monotonic");
+ ok(zoomingIn ?
+ lastVVWidth >= visualViewport.width :
+ lastVVWidth <= visualViewport.width,
+ "vvwidth monotonic");
+ ok(zoomingIn ?
+ lastVVHeight >= visualViewport.height :
+ lastVVHeight <= visualViewport.height,
+ "vvheight monotonic");
+ ok(zoomingIn ?
+ lastScrollX <= window.scrollX :
+ lastScrollX >= window.scrollX,
+ "scrollx monotonic");
+ if (!within(lastScrollY, window.scrollY, 2)) {
+ ok(zoomingIn ?
+ lastScrollY >= window.scrollY :
+ lastScrollY <= window.scrollY,
+ "scrolly monotonic");
+ }
+
+ // At the upper and lower limits of zoom constraints we can briefly go over
+ // the limit and then back because of floating point inaccuracies.
+ // In apzc callback helper we set the new content resolution to
+ // (last presshell resolution that apz knows about) * (apz async zoom)
+ // and (apz async zoom) is calculated as (apz zoom) / (last content resolution that apz knows about)
+ // and (last content resolution that apz knows about) = (dev pixels per css pixel) * (ps resolution) * (resolution induced by transforms)
+ // For simplicity we can assume that (dev pixels per css pixel) == (resolution induced by transforms) == 1
+ // and (ps resolution) == (last presshell resolution that apz knows about).
+ // The calculation then boils down to
+ // (ps resolution) * ((apz zoom) / (ps resolution))
+ // The fact that we divide by (ps resolution) first, and then multiply by
+ // it means the result is not quite equal to (apz zoom).
+ const deviceScale = window.devicePixelRatio;
+ const maxZoom = 10.0;
+ if (!within(lastResolution, maxZoom/deviceScale, 0.0001) &&
+ !within(await getResolution(), maxZoom/deviceScale, 0.0001) &&
+ !within(lastResolution, 1, 0.0001) &&
+ !within(await getResolution(), 1, 0.0001)) {
+ ok(zoomingIn ?
+ lastResolution <= await getResolution() :
+ lastResolution >= await getResolution(),
+ "resolution monotonic");
+ }
+
+ } else {
+ hasPrev = true;
+ }
+ lastVVLeft = visualViewport.pageLeft;
+ lastVVTop = visualViewport.pageTop;
+ lastVVWidth = visualViewport.width;
+ lastVVHeight = visualViewport.height;
+ lastScrollX = window.scrollX;
+ if (!within(lastScrollY, window.scrollY, 2)) {
+ lastScrollY = window.scrollY;
+ }
+ lastResolution = await getResolution();
+}
+
+async function test() {
+ let useTouchpad = (location.search == "?touchpad");
+
+ info("dpi: " + window.devicePixelRatio);
+
+ window.addEventListener("MozAfterPaint", afterpaint);
+ let intervalID = setInterval(afterpaint, 16);
+
+ let resolution = await getResolution();
+ ok(resolution > 0,
+ "The initial_resolution is " + resolution + ", which is some sane value");
+
+ // Check that double-tapping once on a small element zooms in
+ info("sending first double tap");
+ await doubleTapOn(document.getElementById("target2"), 10, 10, useTouchpad);
+ let prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution > prev_resolution, "The first double-tap has increased the resolution to " + resolution);
+
+ await promiseApzFlushedRepaints();
+
+ hasPrev = false;
+ zoomingIn = false;
+
+ // Double tap, this won't hit anything but the body/html, and so will zoom us out.
+ info("sending second double tap");
+ await doubleTapOn(document.getElementById("target2"), 5, 65, useTouchpad);
+ await promiseApzFlushedRepaints();
+ prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution < prev_resolution, "The second double-tap has decreased the resolution to " + resolution);
+
+ clearInterval(intervalID);
+ window.removeEventListener("MozAfterPaint", afterpaint);
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+ <style>
+ .container {
+ background-color: #eee;
+ width: 95vw;
+ height: 400vh;
+ }
+ .smallcontainer {
+ background-color: #aaa;
+ width: 40px;
+ height: 40px;
+ position: absolute;
+ right: 0;
+ top: 20px;
+ }
+</style>
+</head>
+<body>
+
+<div id="target" class="container">
+</div>
+<div id="target2" class="smallcontainer">
+</div>
+
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_square.html b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_square.html
new file mode 100644
index 0000000000..c8278093af
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_square.html
@@ -0,0 +1,61 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=2100"/>
+ <title>Check that double tapping on a square img doesn't cut off parts of the image</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+async function test() {
+ let useTouchpad = (location.search == "?touchpad");
+
+ let resolution = await getResolution();
+ let initial_resolution = resolution;
+ ok(resolution > 0,
+ "The initial_resolution is " + resolution + ", which is some sane value");
+
+ let target = document.getElementById("target");
+
+ // Check that double-tapping once on the element zooms in
+ info("sending first double tap");
+ await doubleTapOn(target, 20, 20, useTouchpad);
+ let prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution > prev_resolution, "The first double-tap has increased the resolution to " + resolution);
+
+ let rect = target.getBoundingClientRect();
+ ok(visualViewport.pageLeft < rect.x, "left");
+ ok(visualViewport.pageTop < rect.y, "top");
+ ok(visualViewport.pageLeft + visualViewport.width > rect.x + rect.width, "right");
+ ok(visualViewport.pageTop + visualViewport.height > rect.y + rect.height, "bottom");
+
+ // Check that double-tapping the second time on the element zooms out
+ info("sending second double tap");
+ await doubleTapOn(target, 20, 20, useTouchpad);
+ prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution < prev_resolution, "The second double-tap has decreased the resolution to " + resolution);
+ ok(resolution == initial_resolution, "The second double-tap has restored the resolution to " + resolution);
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+ <style>
+ .bigsquare {
+ width: 40vh;
+ height: 40vh;
+ }
+</style>
+</head>
+<body>
+
+<img id="target" class="bigsquare" src="green100x100.png">
+
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_tablecell.html b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_tablecell.html
new file mode 100644
index 0000000000..f578ccc592
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_tablecell.html
@@ -0,0 +1,110 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=2100"/>
+ <title>Check that double tapping small table cells does not zoom</title>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script>
+
+async function test() {
+ let useTouchpad = (location.search == "?touchpad");
+
+ let resolution = await getResolution();
+ ok(resolution > 0,
+ "The initial_resolution is " + resolution + ", which is some sane value");
+
+ // Check that double-tapping does not zoom in
+ info("sending first double tap");
+ await doubleTapOn(document.getElementById("target1"), 10, 10, useTouchpad);
+ let prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution == prev_resolution, "The first double-tap did not change the resolution: " + resolution);
+
+ // Check that double-tapping does not zoom in
+ info("sending second double tap");
+ await doubleTapOn(document.getElementById("target2"), 10, 10, useTouchpad);
+ prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution == prev_resolution, "The second double-tap did not change the resolution: " + resolution);
+
+ // Check that double-tapping does zoom in
+ info("sending third double tap");
+ await doubleTapOn(document.getElementById("target3"), 10, 10, useTouchpad);
+ prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution > prev_resolution, "The third double-tap has increased the resolution to " + resolution);
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+ <style>
+ table {
+ width: 100%;
+ }
+ table,
+ td {
+ border: 1px solid #333;
+ }
+ td {
+ height: 25px;
+ }
+ .small {
+ width: 15vw;
+ }
+ .big {
+ width: 50vw;
+ }
+ </style>
+</head>
+<body>
+<table>
+ <thead>
+ <tr>
+ <th colspan="2">The table header</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td class="small"><div id="target1" class="small">The table body</div></td>
+ <td>with two columns</td>
+ </tr>
+ </tbody>
+</table>
+
+<table>
+ <thead>
+ <tr>
+ <th colspan="2">The table header</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td class="small"><div class="small"><div id="target2" class="small">The table body</div></div></td>
+ <td>with two columns</td>
+ </tr>
+ </tbody>
+</table>
+
+<table>
+ <thead>
+ <tr>
+ <th colspan="2">The table header</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td id="target3" class="big">The table body</td>
+ <td>with two columns</td>
+ </tr>
+ </tbody>
+</table>
+
+
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_tallwide.html b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_tallwide.html
new file mode 100644
index 0000000000..438c63b0b0
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_tallwide.html
@@ -0,0 +1,85 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=2100"/>
+ <title>Check that double tapping on a tall element that is >90% width of viewport doesn't scroll to the top of it when scrolled down</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+async function test() {
+ let useTouchpad = (location.search == "?touchpad");
+
+ var resolution = await getResolution();
+ ok(resolution > 0,
+ "The initial_resolution is " + resolution + ", which is some sane value");
+
+ // Check that double-tapping once on a small element zooms in
+ await doubleTapOn(document.getElementById("target2"), 10, 10, useTouchpad);
+ let prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution > prev_resolution, "The first double-tap has increased the resolution to " + resolution);
+
+ // instant scroll to the bottom of the page
+ window.scrollTo({
+ top: 40000,
+ left: 0,
+ behavior: 'auto'
+ });
+
+ await promiseApzFlushedRepaints();
+
+ let scrollPos = window.scrollY;
+ ok(scrollPos > 1500, "window scrolled down (1)");
+ ok(scrollPos > window.innerHeight * 2, "window scrolled down (2)")
+
+ info("window.scrollY " + window.scrollY);
+ info("window.innerHeight " + window.innerHeight);
+ info("visualViewport.pageTop " + visualViewport.pageTop);
+
+ let target = document.getElementById("target");
+
+ let x = 20;
+ let y = visualViewport.pageTop + 20;
+
+ // Check that double-tapping on the big element zooms out
+ await doubleTapOn(target, x, y, useTouchpad);
+ prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution < prev_resolution, "The second double-tap has decreased the resolution to " + resolution);
+
+ info("scrollPos " + scrollPos);
+ info("window.scrollY " + window.scrollY);
+ ok(window.scrollY > 1500, "window is still scrolled down (1)");
+ ok(window.scrollY >= scrollPos - window.innerHeight, "window is still scrolled down (2)");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+ <style>
+ .container {
+ background-color: #eee;
+ width: 95vw;
+ height: 400vh;
+ }
+ .smallcontainer {
+ background-color: #aaa;
+ width: 40px;
+ height: 40px;
+ }
+</style>
+</head>
+<body>
+
+<div id="target" class="container">
+ <div id="target2" class="smallcontainer">
+ </div>
+</div>
+
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_textarea.html b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_textarea.html
new file mode 100644
index 0000000000..99616d9834
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_textarea.html
@@ -0,0 +1,43 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=2100"/>
+ <title>Check that double tapping textarea works</title>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script>
+
+async function test() {
+ let useTouchpad = (location.search == "?touchpad");
+
+ var resolution = await getResolution();
+ ok(resolution > 0,
+ "The initial_resolution is " + resolution + ", which is some sane value");
+
+ // Check that double-tapping once zooms in
+ await doubleTapOn(document.getElementById("target"), 10, 10, useTouchpad);
+ var prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution > prev_resolution, "The first double-tap has increased the resolution to " + resolution);
+
+ // Check that double-tapping again on the same spot zooms out
+ await doubleTapOn(document.getElementById("target"), 10, 10, useTouchpad);
+ prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution < prev_resolution, "The second double-tap has decreased the resolution to " + resolution);
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</head>
+<body>
+
+<textarea id="target" rows="10" cols="30">The cat was playing in the garden.</textarea>
+
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_drag_bug1719913.html b/gfx/layers/apz/test/mochitest/helper_drag_bug1719913.html
new file mode 100644
index 0000000000..36192aed6d
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_drag_bug1719913.html
@@ -0,0 +1,91 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Test for bug 1719913</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="text/javascript">
+
+async function test() {
+ var subframe = document.getElementById("scroller");
+ let scrollPromise = new Promise(resolve => {
+ subframe.addEventListener("scroll", resolve, {once: true});
+ });
+
+ // Scroll down a small amount (5px). The bug in this case is that the
+ // scrollthumb "jumps" further down the scroll track because with the bug,
+ // incorrect location of a transform in the WebRender scroll nodes results
+ // in a miscalculation of the scroll position corresponding to a mouse event
+ // position during dragging.
+ var dragFinisher = await promiseVerticalScrollbarDrag(subframe, 5, 5);
+ if (!dragFinisher) {
+ ok(true, "No scrollbar, can't do this test");
+ return;
+ }
+
+ // the events above might be stuck in APZ input queue for a bit until the
+ // layer is activated, so we wait here until the scroll event listener is
+ // triggered.
+ await scrollPromise;
+
+ await dragFinisher();
+
+ // Flush everything just to be safe
+ await promiseOnlyApzControllerFlushed();
+
+ // The expected scroll position from the 5px of dragging, based on local
+ // testing, is 49px. With the bug, it's 1038px. We check that it's < 100
+ // which should rule out the bug while allowing for minor variations in
+ // scrollbar sizing etc.
+ ok(subframe.scrollTop < 100, "Scrollbar drag resulted in a scroll position of " + subframe.scrollTop);
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+ <style>
+ .columns {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ overflow: auto;
+ }
+ .column {
+ position: relative;
+ width: 50%;
+ height: 500px;
+ }
+ .header {
+ height: 100px;
+ }
+ .scroller {
+ overflow: auto;
+ will-change: transform;
+ height: 100%;
+ }
+ .content {
+ height: 5000px;
+ width: 100%;
+ background: linear-gradient(green, blue);
+ }
+ </style>
+</head>
+<body>
+ <div class="columns">
+ <div class="column">
+ <div class="header"></div>
+ <div class="scroller" id="scroller">
+ <div class="content"></div>
+ </div>
+ </div>
+ <div class="column"></div>
+ </div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_drag_click.html b/gfx/layers/apz/test/mochitest/helper_drag_click.html
new file mode 100644
index 0000000000..01722c798d
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_drag_click.html
@@ -0,0 +1,69 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Sanity mouse-drag click test</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+async function test() {
+ let clickPromise = new Promise(resolve => {
+ document.addEventListener("click", resolve);
+ });
+
+ // Ensure the pointer is inside the window
+ await promiseNativeMouseEventWithAPZ({
+ target: document.getElementById("b"),
+ offsetX: 5,
+ offsetY: 5,
+ type: "mousemove",
+ });
+ // mouse down, move it around, and release it near where it went down. this
+ // should generate a click at the release point
+ await promiseNativeMouseEventWithAPZ({
+ target: document.getElementById("b"),
+ offsetX: 5,
+ offsetY: 5,
+ type: "mousedown",
+ });
+ await promiseNativeMouseEventWithAPZ({
+ target: document.getElementById("b"),
+ offsetX: 100,
+ offsetY: 100,
+ type: "mousemove",
+ });
+ await promiseNativeMouseEventWithAPZ({
+ target: document.getElementById("b"),
+ offsetX: 10,
+ offsetY: 10,
+ type: "mousemove",
+ });
+ await promiseNativeMouseEventWithAPZ({
+ target: document.getElementById("b"),
+ offsetX: 8,
+ offsetY: 8,
+ type: "mouseup",
+ });
+ dump("Finished synthesizing click with a drag in the middle\n");
+
+ let e = await clickPromise;
+ // The mouse down at (5, 5) should not have generated a click, but the up
+ // at (8, 8) should have.
+ is(e.target, document.getElementById("b"), "Clicked on button, yay! (at " + e.clientX + "," + e.clientY + ")");
+ is(e.clientX, 8 + Math.floor(document.getElementById("b").getBoundingClientRect().left), "x-coord of click event looks sane");
+ is(e.clientY, 8 + Math.floor(document.getElementById("b").getBoundingClientRect().top), "y-coord of click event looks sane");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</head>
+<body>
+ <button id="b" style="width: 10px; height: 10px; padding: 0;"></button>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_drag_root_scrollbar.html b/gfx/layers/apz/test/mochitest/helper_drag_root_scrollbar.html
new file mode 100644
index 0000000000..1665e168cb
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_drag_root_scrollbar.html
@@ -0,0 +1,61 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Dragging the mouse on the viewport's scrollbar</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+ .content {
+ width: 1000px;
+ height: 5000px;
+ }
+ </style>
+ <script type="text/javascript">
+
+async function test() {
+ let scrollPromise = new Promise(resolve => {
+ window.addEventListener("scroll", resolve, {once: true});
+ });
+
+ // Do the scroll in one increment so that when the scroll event fires
+ // we're done all the scrolling we're going to do.
+ var dragFinisher = await promiseVerticalScrollbarDrag(window, 20, 20);
+ if (!dragFinisher) {
+ ok(true, "No scrollbar, can't do this test");
+ return;
+ }
+
+ // the events above might be stuck in APZ input queue for a bit until the
+ // layer is activated, so we wait here until the scroll event listener is
+ // triggered.
+ await scrollPromise;
+
+ await dragFinisher();
+
+ // Flush everything just to be safe
+ await promiseOnlyApzControllerFlushed();
+
+ // After dragging the scrollbar 20px on a 1000px-high viewport, we should
+ // have scrolled approx 2% of the 5000px high content. There might have been
+ // scroll arrows and such so let's just have a minimum bound of 50px to be safe.
+ ok(window.scrollY > 50, "Scrollbar drag resulted in a vertical scroll position of " + window.scrollY);
+
+ // Check that we did not get spurious horizontal scrolling, as we might if the
+ // drag gesture is mishandled by content as a select-drag rather than a scrollbar
+ // drag.
+ is(window.scrollX, 0, "Scrollbar drag resulted in a horizontal scroll position of " + window.scrollX);
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</head>
+<body>
+ <div class="content">Some content to ensure the root scrollframe is scrollable</div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_drag_scroll.html b/gfx/layers/apz/test/mochitest/helper_drag_scroll.html
new file mode 100644
index 0000000000..15ca680ad6
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_drag_scroll.html
@@ -0,0 +1,653 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Dragging the mouse on a content-implemented scrollbar</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+ body {
+ background: linear-gradient(135deg, red, blue);
+ }
+ #scrollbar {
+ position:fixed;
+ top: 0;
+ right: 10px;
+ height: 100%;
+ width: 150px;
+ background-color: gray;
+ }
+ </style>
+ <script type="text/javascript">
+var bar = null;
+var mouseDown = false;
+var mouseDownY = -1;
+
+async function moveTo(mouseY) {
+ var fraction = (mouseY - bar.getBoundingClientRect().top) / bar.getBoundingClientRect().height;
+ fraction = Math.max(0, fraction);
+ fraction = Math.min(1, fraction);
+ var oldScrollPos = document.scrollingElement.scrollTop;
+ var newScrollPos = fraction * window.scrollMaxY;
+ ok(newScrollPos > oldScrollPos, "Scroll position strictly increased");
+ // split the scroll in two with a paint in between, just to increase the
+ // complexity of the simulated web content, and to ensure this works as well.
+ document.scrollingElement.scrollTop = (oldScrollPos + newScrollPos) / 2;
+ await promiseAllPaintsDone();
+ document.scrollingElement.scrollTop = newScrollPos;
+}
+
+async function downMouseAndHandleEvent(x, y) {
+ let mouseDownHandledPromise = new Promise(resolve => {
+ bar.addEventListener("mousedown", async function(e) {
+ dump("Got mousedown clientY " + e.clientY + "\n");
+ mouseDown = true;
+ mouseDownY = e.clientY;
+ await moveTo(e.clientY);
+ resolve();
+ }, {capture: true, once: true});
+ });
+ await synthesizeNativeMouseEventWithAPZ({
+ target: bar,
+ offsetX: x,
+ offsetY: y,
+ type: "mousedown",
+ });
+ await mouseDownHandledPromise;
+}
+
+async function moveMouseAndHandleEvent(x, y) {
+ let mouseMoveHandledPromise = new Promise(resolve => {
+ async function mouseOnTarget(e) {
+ if (!mouseDown) {
+ return;
+ }
+ dump("Got mousemove clientY " + e.clientY + "\n");
+ e.stopPropagation();
+ if (e.clientY == mouseDownY) {
+ dump("Discarding spurious mousemove\n");
+ return;
+ }
+ await moveTo(e.clientY);
+ handled();
+ }
+
+ function mouseOffTarget(e) {
+ if (!mouseDown) {
+ return;
+ }
+ ok(false, "The mousemove at " + e.clientY + " was not stopped by the bar listener, and is a glitchy event!");
+ handled();
+ }
+
+ function handled() {
+ bar.removeEventListener("mousemove", mouseOnTarget, true);
+ window.removeEventListener("mousemove", mouseOffTarget);
+ resolve();
+ }
+
+ bar.addEventListener("mousemove", mouseOnTarget, true);
+ window.addEventListener("mousemove", mouseOffTarget);
+ });
+ await synthesizeNativeMouseEventWithAPZ({
+ target: bar,
+ offsetX: x,
+ offsetY: y,
+ type: "mousemove",
+ });
+ await mouseMoveHandledPromise;
+}
+
+async function test() {
+ bar = document.getElementById("scrollbar");
+ mouseDown = false;
+ mouseDownY = -1;
+
+ bar.addEventListener("mouseup", function(e) {
+ mouseDown = false;
+ dump("Got mouseup clientY " + e.clientY + "\n");
+ }, true);
+
+ // Move the mouse to the "scrollbar" (the div upon which dragging changes scroll position)
+ await promiseNativeMouseEventWithAPZ({
+ target: bar,
+ offsetX: 10,
+ offsetY: 10,
+ type: "mousemove",
+ });
+
+ // mouse down
+ await downMouseAndHandleEvent(10, 10);
+
+ // drag vertically by 400px, in 50px increments
+ await moveMouseAndHandleEvent(10, 60);
+ await moveMouseAndHandleEvent(10, 110);
+ await moveMouseAndHandleEvent(10, 160);
+ await moveMouseAndHandleEvent(10, 210);
+ await moveMouseAndHandleEvent(10, 260);
+ await moveMouseAndHandleEvent(10, 310);
+ await moveMouseAndHandleEvent(10, 360);
+ await moveMouseAndHandleEvent(10, 410);
+ // and release
+ await promiseNativeMouseEventWithAPZ({
+ target: bar,
+ offsetX: 10,
+ offsetY: 410,
+ type: "mouseup",
+ });
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</head>
+<body>
+
+<div id="scrollbar">Drag up and down on this bar. The background/scrollbar shouldn't glitch</div>
+This is a tall page<br/>
+1<br/>
+2<br/>
+3<br/>
+4<br/>
+5<br/>
+6<br/>
+7<br/>
+8<br/>
+9<br/>
+10<br/>
+11<br/>
+12<br/>
+13<br/>
+14<br/>
+15<br/>
+16<br/>
+17<br/>
+18<br/>
+19<br/>
+20<br/>
+21<br/>
+22<br/>
+23<br/>
+24<br/>
+25<br/>
+26<br/>
+27<br/>
+28<br/>
+29<br/>
+30<br/>
+31<br/>
+32<br/>
+33<br/>
+34<br/>
+35<br/>
+36<br/>
+37<br/>
+38<br/>
+39<br/>
+40<br/>
+41<br/>
+42<br/>
+43<br/>
+44<br/>
+45<br/>
+46<br/>
+47<br/>
+48<br/>
+49<br/>
+50<br/>
+51<br/>
+52<br/>
+53<br/>
+54<br/>
+55<br/>
+56<br/>
+57<br/>
+58<br/>
+59<br/>
+60<br/>
+61<br/>
+62<br/>
+63<br/>
+64<br/>
+65<br/>
+66<br/>
+67<br/>
+68<br/>
+69<br/>
+70<br/>
+71<br/>
+72<br/>
+73<br/>
+74<br/>
+75<br/>
+76<br/>
+77<br/>
+78<br/>
+79<br/>
+80<br/>
+81<br/>
+82<br/>
+83<br/>
+84<br/>
+85<br/>
+86<br/>
+87<br/>
+88<br/>
+89<br/>
+90<br/>
+91<br/>
+92<br/>
+93<br/>
+94<br/>
+95<br/>
+96<br/>
+97<br/>
+98<br/>
+99<br/>
+100<br/>
+101<br/>
+102<br/>
+103<br/>
+104<br/>
+105<br/>
+106<br/>
+107<br/>
+108<br/>
+109<br/>
+110<br/>
+111<br/>
+112<br/>
+113<br/>
+114<br/>
+115<br/>
+116<br/>
+117<br/>
+118<br/>
+119<br/>
+120<br/>
+121<br/>
+122<br/>
+123<br/>
+124<br/>
+125<br/>
+126<br/>
+127<br/>
+128<br/>
+129<br/>
+130<br/>
+131<br/>
+132<br/>
+133<br/>
+134<br/>
+135<br/>
+136<br/>
+137<br/>
+138<br/>
+139<br/>
+140<br/>
+141<br/>
+142<br/>
+143<br/>
+144<br/>
+145<br/>
+146<br/>
+147<br/>
+148<br/>
+149<br/>
+150<br/>
+151<br/>
+152<br/>
+153<br/>
+154<br/>
+155<br/>
+156<br/>
+157<br/>
+158<br/>
+159<br/>
+160<br/>
+161<br/>
+162<br/>
+163<br/>
+164<br/>
+165<br/>
+166<br/>
+167<br/>
+168<br/>
+169<br/>
+170<br/>
+171<br/>
+172<br/>
+173<br/>
+174<br/>
+175<br/>
+176<br/>
+177<br/>
+178<br/>
+179<br/>
+180<br/>
+181<br/>
+182<br/>
+183<br/>
+184<br/>
+185<br/>
+186<br/>
+187<br/>
+188<br/>
+189<br/>
+190<br/>
+191<br/>
+192<br/>
+193<br/>
+194<br/>
+195<br/>
+196<br/>
+197<br/>
+198<br/>
+199<br/>
+200<br/>
+201<br/>
+202<br/>
+203<br/>
+204<br/>
+205<br/>
+206<br/>
+207<br/>
+208<br/>
+209<br/>
+210<br/>
+211<br/>
+212<br/>
+213<br/>
+214<br/>
+215<br/>
+216<br/>
+217<br/>
+218<br/>
+219<br/>
+220<br/>
+221<br/>
+222<br/>
+223<br/>
+224<br/>
+225<br/>
+226<br/>
+227<br/>
+228<br/>
+229<br/>
+230<br/>
+231<br/>
+232<br/>
+233<br/>
+234<br/>
+235<br/>
+236<br/>
+237<br/>
+238<br/>
+239<br/>
+240<br/>
+241<br/>
+242<br/>
+243<br/>
+244<br/>
+245<br/>
+246<br/>
+247<br/>
+248<br/>
+249<br/>
+250<br/>
+251<br/>
+252<br/>
+253<br/>
+254<br/>
+255<br/>
+256<br/>
+257<br/>
+258<br/>
+259<br/>
+260<br/>
+261<br/>
+262<br/>
+263<br/>
+264<br/>
+265<br/>
+266<br/>
+267<br/>
+268<br/>
+269<br/>
+270<br/>
+271<br/>
+272<br/>
+273<br/>
+274<br/>
+275<br/>
+276<br/>
+277<br/>
+278<br/>
+279<br/>
+280<br/>
+281<br/>
+282<br/>
+283<br/>
+284<br/>
+285<br/>
+286<br/>
+287<br/>
+288<br/>
+289<br/>
+290<br/>
+291<br/>
+292<br/>
+293<br/>
+294<br/>
+295<br/>
+296<br/>
+297<br/>
+298<br/>
+299<br/>
+300<br/>
+301<br/>
+302<br/>
+303<br/>
+304<br/>
+305<br/>
+306<br/>
+307<br/>
+308<br/>
+309<br/>
+310<br/>
+311<br/>
+312<br/>
+313<br/>
+314<br/>
+315<br/>
+316<br/>
+317<br/>
+318<br/>
+319<br/>
+320<br/>
+321<br/>
+322<br/>
+323<br/>
+324<br/>
+325<br/>
+326<br/>
+327<br/>
+328<br/>
+329<br/>
+330<br/>
+331<br/>
+332<br/>
+333<br/>
+334<br/>
+335<br/>
+336<br/>
+337<br/>
+338<br/>
+339<br/>
+340<br/>
+341<br/>
+342<br/>
+343<br/>
+344<br/>
+345<br/>
+346<br/>
+347<br/>
+348<br/>
+349<br/>
+350<br/>
+351<br/>
+352<br/>
+353<br/>
+354<br/>
+355<br/>
+356<br/>
+357<br/>
+358<br/>
+359<br/>
+360<br/>
+361<br/>
+362<br/>
+363<br/>
+364<br/>
+365<br/>
+366<br/>
+367<br/>
+368<br/>
+369<br/>
+370<br/>
+371<br/>
+372<br/>
+373<br/>
+374<br/>
+375<br/>
+376<br/>
+377<br/>
+378<br/>
+379<br/>
+380<br/>
+381<br/>
+382<br/>
+383<br/>
+384<br/>
+385<br/>
+386<br/>
+387<br/>
+388<br/>
+389<br/>
+390<br/>
+391<br/>
+392<br/>
+393<br/>
+394<br/>
+395<br/>
+396<br/>
+397<br/>
+398<br/>
+399<br/>
+400<br/>
+401<br/>
+402<br/>
+403<br/>
+404<br/>
+405<br/>
+406<br/>
+407<br/>
+408<br/>
+409<br/>
+410<br/>
+411<br/>
+412<br/>
+413<br/>
+414<br/>
+415<br/>
+416<br/>
+417<br/>
+418<br/>
+419<br/>
+420<br/>
+421<br/>
+422<br/>
+423<br/>
+424<br/>
+425<br/>
+426<br/>
+427<br/>
+428<br/>
+429<br/>
+430<br/>
+431<br/>
+432<br/>
+433<br/>
+434<br/>
+435<br/>
+436<br/>
+437<br/>
+438<br/>
+439<br/>
+440<br/>
+441<br/>
+442<br/>
+443<br/>
+444<br/>
+445<br/>
+446<br/>
+447<br/>
+448<br/>
+449<br/>
+450<br/>
+451<br/>
+452<br/>
+453<br/>
+454<br/>
+455<br/>
+456<br/>
+457<br/>
+458<br/>
+459<br/>
+460<br/>
+461<br/>
+462<br/>
+463<br/>
+464<br/>
+465<br/>
+466<br/>
+467<br/>
+468<br/>
+469<br/>
+470<br/>
+471<br/>
+472<br/>
+473<br/>
+474<br/>
+475<br/>
+476<br/>
+477<br/>
+478<br/>
+479<br/>
+480<br/>
+481<br/>
+482<br/>
+483<br/>
+484<br/>
+485<br/>
+486<br/>
+487<br/>
+488<br/>
+489<br/>
+490<br/>
+491<br/>
+492<br/>
+493<br/>
+494<br/>
+495<br/>
+496<br/>
+497<br/>
+498<br/>
+499<br/>
+
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_drag_scrollbar_hittest.html b/gfx/layers/apz/test/mochitest/helper_drag_scrollbar_hittest.html
new file mode 100644
index 0000000000..7a1cab6dba
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_drag_scrollbar_hittest.html
@@ -0,0 +1,100 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Test that the scrollbar thumb remains under the cursor during scrollbar dragging</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="text/javascript">
+
+const utils = SpecialPowers.getDOMWindowUtils(window);
+
+async function test() {
+ // This test largely attempts to replicate the STR from bug 1826947
+ // (which demonstrates the same issue as bug 1818721 but with a more
+ // reduced test case).
+
+ is(await getResolution(), 1.0, "should not be zoomed");
+
+ // Zoom in. This is part of the bug 1826947 STR.
+ let resolution = 5.0;
+ utils.setResolutionAndScaleTo(resolution);
+ await promiseApzFlushedRepaints();
+
+ // Scroll horizontally to the middle of the visual viewport. This was
+ // determined empirically to be needed to reproduce the bug (and also
+ // is what happens automatically if you pinch-zoom in using an actual
+ // gesture rather than setResolutionAndScaleTo()).
+ utils.scrollToVisual(document.scrollingElement.clientWidth / 2,
+ 0,
+ utils.UPDATE_TYPE_MAIN_THREAD,
+ utils.SCROLL_MODE_INSTANT);
+ await promiseApzFlushedRepaints();
+
+ // Install a scroll event listener. This is part of the usage of
+ // promiseVerticalScrollbarDrag(), we need to wait for a scroll event
+ // before awaiting dragFinisher().
+ // In this test, since the scroll range comes from zooming in, we need
+ // to listen for a *visual viewport* scroll event.
+ let visualScrollPromise = new Promise(resolve => {
+ window.visualViewport.addEventListener("scroll", resolve, {once: true});
+ });
+
+ // Start the animation in the SVG document. This is needed to reproduce
+ // the bug. (See below for why we do it dynamically.)
+ document.getElementsByTagName("animateTransform")[0].setAttribute("repeatCount", "indefinite");
+ document.getElementsByTagName("animateTransform")[0].beginElement();
+
+ // Drag the vertical scrollbar thumb downward.
+ // Do the scroll in one increment so that when the scroll event fires
+ // we're done all the scrolling we're going to do.
+ let distance = 20;
+ let increment = distance;
+ var dragFinisher = await promiseVerticalScrollbarDrag(window, distance, increment);
+ await visualScrollPromise;
+ await dragFinisher();
+
+ // Check that at the end of the drag, the thumb is still under the cursor.
+ // This is done using hitTest(). To compute the point to pass to hitTest(),
+ // use scrollbarDragStart() to compute the ending mouse position of the
+ // drag the way promiseVerticalScrollbarDrag() does.
+ // However, since we are passing the point to hitTest() which expects
+ // coordinates relative to the layout viewport (whereas scrollbarDragStart()
+ // returns coordinates relative to the visual viewport), we need to translate
+ // by the relative offset.
+ let dragStartPoint = await scrollbarDragStart(window, 1);
+ let hitTestPoint = {x: dragStartPoint.x, y: dragStartPoint.y + distance};
+ const relativeOffset = getRelativeViewportOffset(window);
+ hitTestPoint.x += relativeOffset.x;
+ hitTestPoint.y += relativeOffset.y;
+ let result = hitTest(hitTestPoint);
+ ok((result.hitInfo & APZHitResultFlags.SCROLLBAR_THUMB) != 0,
+ "Thumb should be under the cursor");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</head>
+<body>
+ <!--
+ This is the testcase from bug 1826947 comment 0, except that the animation's
+ repeatCount is initially set to 0, and only changed to "indefinite" dynamically
+ during the test. This is to prevent an issue where the promiseAllPaintsDone()
+ call in waitUntilApzStable() can get into an infinite loop if we schedule
+ new frames of the animation faster than we paint them.
+ -->
+ <svg xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMin meet" viewBox="0 0 100 100">
+ <filter id="THE_FILTER" x="0" y="0" width="10" height="100">
+ <feTurbulence id="turbulence" baseFrequency="10" numOctaves="5"/>
+ </filter>
+ <rect x="0" y="0" width="1" height="1" style="filter: url(#THE_FILTER);">
+ <animateTransform attributeName="transform" type="rotate" from="360" to="340" dur="5s" repeatCount="0"/>
+ </rect>
+ </svg>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_empty.html b/gfx/layers/apz/test/mochitest/helper_empty.html
new file mode 100644
index 0000000000..68cd9179f5
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_empty.html
@@ -0,0 +1,4 @@
+<!DOCTYPE html>
+<script src="apz_test_utils.js"></script>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/paint_listener.js"></script>
diff --git a/gfx/layers/apz/test/mochitest/helper_fission_animation_styling_in_oopif.html b/gfx/layers/apz/test/mochitest/helper_fission_animation_styling_in_oopif.html
new file mode 100644
index 0000000000..12221d0703
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_fission_animation_styling_in_oopif.html
@@ -0,0 +1,166 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for scrolled out of view animation optimization in an OOPIF</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="helper_fission_utils.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script>
+
+fission_subtest_init();
+
+FissionTestHelper.startTestPromise
+ .then(waitUntilApzStable)
+ .then(loadOOPIFrame("testframe", "helper_fission_empty.html"))
+ .then(waitUntilApzStable)
+ .then(test)
+ .then(FissionTestHelper.subtestDone, FissionTestHelper.subtestFailed);
+
+async function setup_in_oopif() {
+ const setup = function() {
+ // Load utility functions for animation stuff.
+ const script = document.createElement("script");
+ script.setAttribute("src", "/tests/dom/animation/test/testcommon.js");
+ document.head.appendChild(script);
+
+ const extraStyle = document.createElement("style");
+ document.head.appendChild(extraStyle);
+ // an animation doesn't cause any geometric changes and doesn't run on the
+ // compositor either
+ extraStyle.sheet.insertRule("@keyframes anim { from { color: red; } to { color: blue; } }", 0);
+
+ const div = document.createElement("div");
+ // Position an element for animation at top: 20px.
+ div.style = "position: absolute; top: 40px; animation: anim 1s infinite; box-shadow: 0px -20px red;";
+ div.setAttribute("id", "target");
+ div.innerHTML = "hello";
+ document.body.appendChild(div);
+ script.onload = () => {
+ // Force to flush the first style to avoid the first style is observed.
+ target.getAnimations()[0];
+ // FIXME: Bug 1578309 use anim.ready instead.
+ promiseFrame().then(() => {
+ FissionTestHelper.fireEventInEmbedder("OOPIF:SetupDone", true);
+ });
+ }
+ return true;
+ }
+
+ const iframePromise = promiseOneEvent(window, "OOPIF:SetupDone", null);
+
+ await FissionTestHelper.sendToOopif(testframe, `(${setup})()`);
+ await iframePromise;
+}
+
+async function observe_styling_in_oopif(aFrameCount) {
+ const observe_styling = function(frameCount) {
+ // Start in a rAF callback.
+ waitForAnimationFrames(1).then(() => {
+ observeStyling(frameCount).then(markers => {
+ FissionTestHelper.fireEventInEmbedder("OOPIF:StyleCount", markers.length);
+ });
+ });
+
+ return true;
+ }
+
+ const iframePromise = promiseOneEvent(window, "OOPIF:StyleCount", null);
+ await FissionTestHelper.sendToOopif(testframe, `(${observe_styling})(${aFrameCount})`);
+
+ const styleCountData = await iframePromise;
+ return styleCountData.data;
+}
+
+// The actual test
+
+async function test() {
+ // Generate an infinite animation which is initially clipped out by
+ // overflow: hidden style in the out-of-process iframe.
+ await setup_in_oopif();
+
+ let styleCount = await observe_styling_in_oopif(5);
+ is(styleCount, 0,
+ "Animation in an out-of-process iframe which is initially clipped out " +
+ "due to 'overflow: hidden' should be throttled");
+
+ // Scroll synchronously to a position where the iframe gets visible.
+ scroller.scrollTo(0, 1000);
+ await new Promise(resolve => {
+ scroller.addEventListener("scroll", resolve, { once: true });
+ });
+
+ // Wait for a frame to make sure the notification of the last scroll position
+ // from APZC reaches the iframe process
+ await observe_styling_in_oopif(1);
+
+ styleCount = await observe_styling_in_oopif(5);
+ is(styleCount, 5,
+ "Animation in an out-of-process iframe which is no longer clipped out " +
+ "should NOT be throttled");
+
+ // Scroll synchronously to a position where the iframe is invisible again.
+ scroller.scrollTo(0, 0);
+ await new Promise(resolve => {
+ scroller.addEventListener("scroll", resolve, { once: true });
+ });
+
+ // Wait for a frame to make sure the notification of the last scroll position
+ // from APZC reaches the iframe process
+ await observe_styling_in_oopif(1);
+
+ styleCount = await observe_styling_in_oopif(5);
+ is(styleCount, 0,
+ "Animation in an out-of-process iframe which is clipped out again " +
+ "should be throttled again");
+
+ // ===== Asyncronous scrolling tests =====
+ scroller.style.overflow = "scroll";
+ // Scroll asynchronously to a position where the animating element gets
+ // visible.
+ scroller.scrollTo({ left: 0, top: 750, behavior: "smooth"});
+
+ // Wait for the asyncronous scroll finish. `60` frames is the same number in
+ // helper_fission_scroll_oopif.html
+ await observe_styling_in_oopif(60);
+
+ styleCount = await observe_styling_in_oopif(5);
+ is(styleCount, 5,
+ "Animation in an out-of-process iframe which is now visible by " +
+ "asynchronous scrolling should NOT be throttled");
+
+ // Scroll asynchronously to a position where the iframe is still visible but
+ // the animating element gets invisible.
+ scroller.scrollTo({ left: 0, top: 720, behavior: "smooth"});
+
+ // Wait for the asyncronous scroll finish.
+ await observe_styling_in_oopif(60);
+
+ styleCount = await observe_styling_in_oopif(5);
+ is(styleCount, 0,
+ "Animation in an out-of-process iframe which is scrolled out of view by " +
+ "asynchronous scrolling should be throttled");
+
+ // Scroll asynchronously to a position where the animating element gets
+ // visible again.
+ scroller.scrollTo({ left: 0, top: 750, behavior: "smooth"});
+
+ // Wait for the asyncronous scroll finish.
+ await observe_styling_in_oopif(60);
+
+ styleCount = await observe_styling_in_oopif(5);
+ is(styleCount, 5,
+ "Animation in an out-of-process iframe appeared by the asynchronous " +
+ "scrolling should be NOT throttled");
+}
+
+ </script>
+</head>
+<div style="width: 300px; height: 300px; overflow: hidden;" id="scroller">
+ <div style="width: 100%; height: 1000px;"></div>
+ <!-- I am not sure it's worth setting scrolling="no" and pointer-events: none. -->
+ <!-- I just want to make sure that HitTestingTreeNode is generated even with these properties. -->
+ <iframe scrolling="no" style="pointer-events: none;" id="testframe"></iframe>
+</div>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_fission_animation_styling_in_transformed_oopif.html b/gfx/layers/apz/test/mochitest/helper_fission_animation_styling_in_transformed_oopif.html
new file mode 100644
index 0000000000..d85d77f552
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_fission_animation_styling_in_transformed_oopif.html
@@ -0,0 +1,130 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for scrolled out of view animation optimization in an OOPIF transformed by rotate(45deg)</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="helper_fission_utils.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script>
+
+fission_subtest_init();
+
+FissionTestHelper.startTestPromise
+ .then(waitUntilApzStable)
+ .then(loadOOPIFrame("testframe", "helper_fission_empty.html"))
+ .then(waitUntilApzStable)
+ .then(test)
+ .then(FissionTestHelper.subtestDone, FissionTestHelper.subtestFailed);
+
+async function setup_in_oopif() {
+ const setup = function() {
+ // Load utility functions for animation stuff.
+ const script = document.createElement("script");
+ script.setAttribute("src", "/tests/dom/animation/test/testcommon.js");
+ document.head.appendChild(script);
+
+ const extraStyle = document.createElement("style");
+ document.head.appendChild(extraStyle);
+ // an animation doesn't affect any geometric changes and doesn't run on the
+ // compositor either
+ extraStyle.sheet.insertRule("@keyframes anim { from { color: red; } to { color: blue; } }", 0);
+
+ const animation = document.createElement("div");
+ animation.style = "animation: anim 1s infinite;";
+ animation.innerHTML = "hello";
+ document.body.appendChild(animation);
+ script.onload = () => {
+ const rect = animation.getBoundingClientRect();
+
+ FissionTestHelper.fireEventInEmbedder("OOPIF:SetupDone",
+ [rect.right, rect.bottom]);
+ }
+ return true;
+ }
+
+ const iframePromise = promiseOneEvent(window, "OOPIF:SetupDone", null);
+
+ await FissionTestHelper.sendToOopif(testframe, `(${setup})()`);
+ const rectData = await iframePromise;
+ return rectData.data;
+}
+
+async function observe_styling_in_oopif(aFrameCount) {
+ const observe_styling = function(frameCount) {
+ // Start in a rAF callback.
+ waitForAnimationFrames(1).then(() => {
+ observeStyling(frameCount).then(markers => {
+ FissionTestHelper.fireEventInEmbedder("OOPIF:StyleCount", markers.length);
+ });
+ });
+
+ return true;
+ }
+
+ const iframePromise = promiseOneEvent(window, "OOPIF:StyleCount", null);
+ await FissionTestHelper.sendToOopif(testframe, `(${observe_styling})(${aFrameCount})`);
+
+ const styleCountData = await iframePromise;
+ return styleCountData.data;
+}
+
+// The actual test
+
+async function test() {
+ // Generate an infinite animation which is initially scrolled out of view.
+ // setup_in_oopif() returns the right bottom position of the animating element
+ // on the iframe coodinate system.
+ const [right, bottom] = await setup_in_oopif();
+
+ let styleCount = await observe_styling_in_oopif(5);
+ is(styleCount, 0,
+ "Animation in an out-of-process iframe which is initially scrolled out " +
+ "of view should be throttled");
+
+ const topPositionOfIFrame = testframe.getBoundingClientRect().top -
+ scroller.clientHeight;
+ // Scroll asynchronously to a position where the animating element gets
+ // visible.
+ scroller.scrollTo({ left: 0, top: topPositionOfIFrame + 1, behavior: "smooth"});
+
+ // Wait for the asyncronous scroll finish. `60` frames is the same number in
+ // helper_fission_scroll_oopif.html
+ await observe_styling_in_oopif(60);
+
+ styleCount = await observe_styling_in_oopif(5);
+ is(styleCount, 5,
+ "Animation in an out-of-process iframe which is no longer scrolled out " +
+ "of view should NOT be throttled");
+
+ // Calculate the right bottom position of the animation which is in an iframe
+ // rotated by `rotate(45deg)`
+ const rightBottomPositionOfAnimation =
+ right / Math.sqrt(2) + bottom / Math.sqrt(2);
+
+ // Scroll asynchronously to a position where the animating element gets
+ // invisible again.
+ scroller.scrollTo({ left: 0,
+ top: topPositionOfIFrame + scroller.clientHeight + rightBottomPositionOfAnimation,
+ behavior: "smooth"});
+
+ // Wait for the asyncronous scroll finish.
+ await observe_styling_in_oopif(60);
+
+ styleCount = await observe_styling_in_oopif(5);
+ is(styleCount, 0,
+ "Animation in an out-of-process iframe which is scrolled out of view " +
+ "again should be throttled");
+}
+
+ </script>
+</head>
+<div style="width: 300px; height: 300px; overflow: scroll;" id="scroller">
+ <div style="width: 100%; height: 1000px;"></div>
+ <div style="transform: rotate(45deg);">
+ <iframe scrolling="no" style="pointer-events: none;" id="testframe" frameborder="0"></iframe>
+ </div>
+ <div style="width: 100%; height: 1000px;"></div>
+</div>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_fission_basic.html b/gfx/layers/apz/test/mochitest/helper_fission_basic.html
new file mode 100644
index 0000000000..dbc41477b9
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_fission_basic.html
@@ -0,0 +1,40 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Basic sanity test that runs inside a fission-enabled window</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="helper_fission_utils.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script>
+
+fission_subtest_init();
+
+FissionTestHelper.startTestPromise
+ .then(waitUntilApzStable)
+ .then(loadOOPIFrame("testframe", "helper_fission_empty.html"))
+ .then(waitUntilApzStable)
+ .then(test)
+ .then(FissionTestHelper.subtestDone, FissionTestHelper.subtestFailed);
+
+
+// The actual test
+
+async function test() {
+ let iframeElement = document.getElementById("testframe");
+ ok(SpecialPowers.wrap(window)
+ .docShell
+ .QueryInterface(SpecialPowers.Ci.nsILoadContext)
+ .useRemoteSubframes,
+ "OOP iframe is actually OOP");
+ let iframeResult = await FissionTestHelper.sendToOopif(iframeElement, "20 + 22");
+ is(iframeResult, 42, "Basic content fission test works");
+}
+
+ </script>
+</head>
+<body>
+<iframe id="testframe"></iframe>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_fission_checkerboard_severity.html b/gfx/layers/apz/test/mochitest/helper_fission_checkerboard_severity.html
new file mode 100644
index 0000000000..ef7943b29f
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_fission_checkerboard_severity.html
@@ -0,0 +1,138 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>
+ A test to make sure checkerboard severity isn't reported for non-scrollable
+ OOP iframe
+ </title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="helper_fission_utils.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script>
+
+fission_subtest_init();
+
+FissionTestHelper.startTestPromise
+ .then(waitUntilApzStable)
+ .then(loadOOPIFrame("testframe", "helper_fission_empty.html"))
+ .then(waitUntilApzStable)
+ .then(test)
+ .then(FissionTestHelper.subtestDone, FissionTestHelper.subtestFailed);
+
+
+// The actual test
+
+let code_for_oopif_to_run = function() {
+ document.addEventListener("click", function(e) {
+ dump(`OOPIF got click at ${e.clientX},${e.clientY}\n`);
+ let result = { x: e.clientX, y: e.clientY };
+ FissionTestHelper.fireEventInEmbedder("OOPIF:ClickData", result);
+ });
+ dump("OOPIF registered click listener\n");
+ return true;
+};
+
+async function getIframeDisplayport(iframe) {
+ let oopif_displayport = function() {
+ let result = getLastContentDisplayportFor("fission_empty_docelement", false);
+ FissionTestHelper.fireEventInEmbedder("OOPIF:Displayport", result);
+ return true;
+ };
+
+ let iframePromise = promiseOneEvent(window, "OOPIF:Displayport", null);
+ ok(await FissionTestHelper.sendToOopif(iframe, `(${oopif_displayport})()`));
+ let iframeResponse = await iframePromise;
+ dump("OOPIF response for Displayport: " +
+ JSON.stringify(iframeResponse.data) + "\n");
+
+ return iframeResponse.data;
+}
+
+async function getIframeScrollMax(iframe) {
+ let oopif_scroll_max = function() {
+ let result = {
+ scrollMaxX: window.scrollMaxX,
+ scrollMaxY: window.scrollMaxY
+ };
+ FissionTestHelper.fireEventInEmbedder("OOPIF:ScrollMax", result);
+ return true;
+ };
+
+ let iframePromise = promiseOneEvent(window, "OOPIF:ScrollMax", null);
+ ok(await FissionTestHelper.sendToOopif(iframe, `(${oopif_scroll_max})()`));
+ let iframeResponse = await iframePromise;
+ dump("OOPIF response for ScrollMax: " +
+ JSON.stringify(iframeResponse.data) + "\n");
+
+ return iframeResponse.data;
+}
+
+async function test() {
+ await SpecialPowers.spawnChrome([], () => {
+ Services.telemetry.getHistogramById("CHECKERBOARD_SEVERITY").clear();
+ });
+
+ const iframe = document.getElementById("testframe");
+
+ // Make sure the iframe content is not scrollable.
+ const { scrollMaxX, scrollMaxY } = await getIframeScrollMax(iframe);
+ is(scrollMaxX, 0, "The iframe content should not be scrollable");
+ is(scrollMaxY, 0, "The iframe content should not be scrollable");
+
+ // Since bug 1709460 any visible OOP iframe initially has set the displayport.
+ let displayport = await getIframeDisplayport(iframe);
+ is(displayport.width, 400);
+ is(displayport.height, 300);
+
+ let iframeResponse =
+ await FissionTestHelper.sendToOopif(iframe, `(${code_for_oopif_to_run})()`);
+ dump("OOPIF response: " + JSON.stringify(iframeResponse) + "\n");
+ ok(iframeResponse, "code_for_oopif_to_run successfully installed");
+
+ // Click on the iframe via APZ so that it triggers a RequestContentRepaint
+ // call then it sets zero display port margins for the iframe's root scroller,
+ // thus as a result it will report a checkerboard event if there had been
+ // checkerboarding.
+ iframePromise = promiseOneEvent(window, "OOPIF:ClickData", null);
+ await synthesizeNativeMouseEventWithAPZ(
+ { type: "click", target: iframe, offsetX: 10, offsetY: 10 },
+ () => dump("Finished synthesizing click, waiting for OOPIF message...\n")
+ );
+ iframeResponse = await iframePromise;
+ dump("OOPIF response: " + JSON.stringify(iframeResponse.data) + "\n");
+
+ // Now the displayport size should have been set.
+ displayport = await getIframeDisplayport(iframe);
+ is(displayport.width, 400, "The displayport size should be same as the iframe size");
+ is(displayport.height, 300, "The displayport size should be same as the iframe size");
+
+ // Wait 100ms to give a chance to deliver the checkerboard event.
+ await new Promise(resolve => {
+ setTimeout(resolve, 100);
+ });
+
+ const hasCheckerboardSeverity = await SpecialPowers.spawnChrome([], () => {
+ const histograms = Services.telemetry.getSnapshotForHistograms(
+ "main",
+ true /* clear the histograms after taking this snapshot*/).parent;
+
+ return histograms.hasOwnProperty("CHECKERBOARD_SEVERITY");
+ });
+ ok(!hasCheckerboardSeverity, "there should be no checkerboard severity data");
+}
+ </script>
+ <style>
+ iframe {
+ width: 400px;
+ height: 300px;
+ border: none;
+ }
+ </style>
+</head>
+<body>
+<iframe id="testframe"></iframe>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_fission_empty.html b/gfx/layers/apz/test/mochitest/helper_fission_empty.html
new file mode 100644
index 0000000000..6a95f76339
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_fission_empty.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<html id="fission_empty_docelement">
+ <meta charset="utf-8">
+ <style>
+ html,body {
+ /* Convenient for calculation of element positions */
+ margin: 0;
+ padding: 0;
+ }
+ </style>
+ <script src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script>
+// This is an empty document that serves as a OOPIF content document that be
+// reused by different fission subtests. The subtest can eval stuff in this
+// document using the sendToOopif helper and thereby populate this document
+// with whatever is needed. This allows the subtest to more "contained" in a
+// single file and avoids having to create new dummy files for each subtest.
+async function loaded() {
+ window.dispatchEvent(new Event("FissionTestHelper:Init"));
+ // Wait a couple of animation frames before sending the load, to ensure that
+ // this OOPIF's layer tree has been sent to the compositor. We use this
+ // instead of things like promiseOnlyApzControllerFlushed and/or promiseAllPaintsDone because
+ // this page is running without SpecialPowers and I couldn't figure out a good
+ // way to get a hold of a things like Services.obs or DOMWindowUtils easily.
+ await promiseFrame();
+ await promiseFrame();
+ FissionTestHelper.fireEventInEmbedder("OOPIF:Load", {content: window.location.href});
+}
+ </script>
+ <body onload="loaded()">
+ </body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_fission_event_region_override.html b/gfx/layers/apz/test/mochitest/helper_fission_event_region_override.html
new file mode 100644
index 0000000000..82f529bebd
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_fission_event_region_override.html
@@ -0,0 +1,84 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Ensure the event region override flags work properly</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="helper_fission_utils.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script>
+
+fission_subtest_init();
+
+FissionTestHelper.startTestPromise
+ .then(waitUntilApzStable)
+ .then(loadOOPIFrame("testframe", "helper_fission_empty.html"))
+ .then(waitUntilApzStable)
+ .then(test)
+ .then(FissionTestHelper.subtestDone, FissionTestHelper.subtestFailed);
+
+
+// The actual test
+
+let code_for_oopif_to_run = function() {
+ document.body.innerHTML = '<div style="height: 5000px">scrollable content</div>';
+ document.addEventListener("wheel", function(e) {
+ dump(`OOPIF got wheel at ${e.clientX},${e.clientY}\n`);
+ let result = { x: e.clientX, y: e.clientY };
+ FissionTestHelper.fireEventInEmbedder("OOPIF:WheelData", result);
+ }, { passive: true });
+ document.addEventListener("scroll", function(e) {
+ dump(`OOPIF got scroll to ${window.scrollX},${window.scrollY}\n`);
+ let result = { x: window.scrollX, y: window.scrollY };
+ FissionTestHelper.fireEventInEmbedder("OOPIF:Scrolled", result);
+ });
+ dump("OOPIF registered wheel and scroll listeners\n");
+ return true;
+};
+
+async function test() {
+ let iframeElement = document.getElementById("testframe");
+
+ let iframeResponse = await FissionTestHelper.sendToOopif(iframeElement, `(${code_for_oopif_to_run})()`);
+ dump("OOPIF response: " + JSON.stringify(iframeResponse) + "\n");
+ ok(iframeResponse, "code_for_oopif_to_run successfully installed");
+
+ let wheeled = false;
+ let scrolled = false;
+ window.addEventListener("OOIF:WheelData", function listener(e) {
+ dump("OOPIF:WheelData received with data: " + JSON.stringify(e.data) + "\n");
+ wheeled = true;
+ });
+ window.addEventListener("OOPIF:Scrolled", function listener(e) {
+ dump("OOPIF:Scrolled received with data: " + JSON.stringify(e.data) + "\n");
+ scrolled = true;
+ });
+
+ await synthesizeNativeWheel(iframeElement, 10, 10, 0, -50);
+
+ // Advance a bunch of frames. The only goal here is to ensure enough time
+ // passes so that if the OOPIF does scroll, we find out about it via the
+ // OOPIF:Scrolled messaging.
+ // If we don't wait long enough we might end up finishing the test before
+ // that scroll message gets received here, and so we might wrongly pass the
+ // test.
+ await SpecialPowers.promiseTimeout(0);
+ var utils = SpecialPowers.getDOMWindowUtils(window);
+ for (var i = 0; i < 5; i++) {
+ utils.advanceTimeAndRefresh(16);
+ }
+ utils.restoreNormalRefresh();
+ await promiseOnlyApzControllerFlushed();
+
+ ok(!wheeled, "OOPIF correctly did not get wheel event");
+ ok(!scrolled, "OOPIF correctly did not scroll");
+}
+
+ </script>
+</head>
+<body>
+<iframe id="testframe"></iframe>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_fission_force_empty_hit_region.html b/gfx/layers/apz/test/mochitest/helper_fission_force_empty_hit_region.html
new file mode 100644
index 0000000000..7baa552c37
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_fission_force_empty_hit_region.html
@@ -0,0 +1,82 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Ensure the ForceEmptyHitRegion flag works properly</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="helper_fission_utils.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script>
+
+fission_subtest_init();
+
+FissionTestHelper.startTestPromise
+ .then(waitUntilApzStable)
+ .then(loadOOPIFrame("testframe1", "helper_fission_empty.html"))
+ .then(loadOOPIFrame("testframe2", "helper_fission_empty.html"))
+ .then(waitUntilApzStable)
+ .then(test)
+ .then(FissionTestHelper.subtestDone, FissionTestHelper.subtestFailed);
+
+
+// The actual test
+
+let code_for_oopif_to_run = function() {
+ document.body.style.backgroundColor = 'green'; // To ensure opaqueness
+ let utils = SpecialPowers.getDOMWindowUtils(window);
+ dump("OOPIF got layersId: " + utils.getLayersId() +
+ ", scrollId: " + utils.getViewId(document.scrollingElement) + "\n");
+ return JSON.stringify({
+ layersId: utils.getLayersId(),
+ viewId: utils.getViewId(document.scrollingElement)
+ });
+};
+
+let iframe_compositor_test_data = function() {
+ let utils = SpecialPowers.getDOMWindowUtils(window);
+ let result = JSON.stringify(utils.getCompositorAPZTestData());
+ dump("OOPIF got compositor APZ data: " + result + "\n");
+ return result;
+};
+
+async function test() {
+ let iframe1 = document.getElementById("testframe1");
+ let iframe2 = document.getElementById("testframe2");
+
+ let iframeResponse = await FissionTestHelper.sendToOopif(iframe1, `(${code_for_oopif_to_run})()`);
+ dump("OOPIF response: " + iframeResponse + "\n");
+ ok(iframeResponse, "code_for_oopif_to_run successfully installed in frame1");
+
+ iframeResponse = await FissionTestHelper.sendToOopif(iframe2, `(${code_for_oopif_to_run})()`);
+ dump("OOPIF response: " + iframeResponse + "\n");
+ ok(iframeResponse, "code_for_oopif_to_run successfully installed in frame2");
+ let iframe2Expected = JSON.parse(iframeResponse);
+
+ let utils = SpecialPowers.getDOMWindowUtils(window);
+
+ // Hit-testing the iframe with pointer-events:none should end up hitting the
+ // document containing the iframe instead (i.e. this document).
+ checkHitResult(await fissionHitTest(centerOf(iframe1), iframe1),
+ APZHitResultFlags.VISIBLE,
+ utils.getViewId(document.scrollingElement),
+ utils.getLayersId(),
+ "center of pointer-events:none iframe should hit parent doc");
+
+ // Hit-testing the iframe that doesn't have pointer-events:none should end up
+ // hitting that iframe.
+ checkHitResult(await fissionHitTest(centerOf(iframe2), iframe2),
+ APZHitResultFlags.VISIBLE,
+ iframe2Expected.viewId,
+ iframe2Expected.layersId,
+ "center of regular iframe should hit iframe doc");
+}
+
+ </script>
+</head>
+<body>
+<iframe id="testframe1" style="pointer-events:none"></iframe>
+<iframe id="testframe2"></iframe>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_fission_inactivescroller_positionedcontent.html b/gfx/layers/apz/test/mochitest/helper_fission_inactivescroller_positionedcontent.html
new file mode 100644
index 0000000000..fc43ace0f1
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_fission_inactivescroller_positionedcontent.html
@@ -0,0 +1,120 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Ensure positioned content inside inactive scollframes but on top of OOPIFs hit-test properly</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="helper_fission_utils.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script>
+
+fission_subtest_init();
+
+FissionTestHelper.startTestPromise
+ .then(waitUntilApzStable)
+ .then(loadOOPIFrame("testframe", "helper_fission_empty.html"))
+ .then(waitUntilApzStable)
+ .then(test)
+ .then(FissionTestHelper.subtestDone, FissionTestHelper.subtestFailed);
+
+let make_oopif_scrollable = function() {
+ // ensure the oopif is scrollable, and wait for the paint so that the
+ // compositor also knows it's scrollable.
+ document.body.style.height = "200vh";
+ promiseApzFlushedRepaints().then(() => {
+ let utils = SpecialPowers.getDOMWindowUtils(window);
+ let result = {
+ layersId: utils.getLayersId(),
+ viewId: utils.getViewId(document.scrollingElement)
+ };
+ dump(`OOPIF computed IDs ${JSON.stringify(result)}\n`);
+ FissionTestHelper.fireEventInEmbedder("OOPIF:Scrollable", result);
+ });
+ return true;
+};
+
+async function test() {
+ let iframe = document.getElementById("testframe");
+
+ let oopifScrollerIdsPromise = promiseOneEvent(window, "OOPIF:Scrollable", null);
+ ok(await FissionTestHelper.sendToOopif(iframe, `(${make_oopif_scrollable})()`),
+ "Ran code to make OOPIF scrollable");
+ let oopifScrollerIds = (await oopifScrollerIdsPromise).data;
+
+ let config = getHitTestConfig();
+ let utils = config.utils;
+
+ // The #scroller div is (a) inactive, and (b) under the OOPIF. However, it
+ // also contains a positioned element with a high z-index (#abspos). #abspos
+ // therefore sits on top of the OOPIF. Hit-testing on #abspos should hit
+ // #scroller, but anywhere else within the OOPIF box should hit the OOPIF.
+
+ checkHitResult(await fissionHitTest(centerOf("abspos"), iframe),
+ APZHitResultFlags.VISIBLE |
+ (config.activateAllScrollFrames ? 0 : APZHitResultFlags.INACTIVE_SCROLLFRAME),
+ config.activateAllScrollFrames ?
+ utils.getViewId(document.getElementById("scroller")) :
+ utils.getViewId(document.scrollingElement),
+ utils.getLayersId(),
+ "abspos element on top of OOPIF should hit parent doc hosting the OOPIF");
+
+ // If the fix for the bug this test is for is not active (as indicated by
+ // config.activateAllScrollFrames) then we just accept the wrong answer. As
+ // of writing this comment the fix will only be active if fission is pref'ed
+ // on, not just enabled for this window, ie the test suite is run in fission
+ // mode.
+ checkHitResult(await fissionHitTest(centerOf("scroller"), iframe),
+ APZHitResultFlags.VISIBLE |
+ (config.activateAllScrollFrames ? 0 : APZHitResultFlags.INACTIVE_SCROLLFRAME),
+ config.activateAllScrollFrames ?
+ oopifScrollerIds.viewId :
+ utils.getViewId(document.scrollingElement),
+ config.activateAllScrollFrames ?
+ oopifScrollerIds.layersId :
+ utils.getLayersId(),
+ "Part of OOPIF sitting on top of the inactive scrollframe should hit OOPIF");
+
+ checkHitResult(await fissionHitTest({x: 250, y: 100}, iframe),
+ APZHitResultFlags.VISIBLE,
+ oopifScrollerIds.viewId,
+ oopifScrollerIds.layersId,
+ "part of OOPIF outside the inactive scfollframe rect should hit the OOPIF");
+}
+
+ </script>
+</head>
+<body>
+<style>
+html, body {
+ margin: 0;
+}
+body {
+ /* Ensure root document is scrollable so that #scroller is inactive by
+ default */
+ height: 200vh;
+}
+iframe {
+ position: absolute;
+ width: 300px;
+ height: 200px;
+}
+
+#scroller {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 200px;
+ height: 200px;
+ background-color: transparent;
+ overflow-y: scroll;
+}
+</style>
+<div id="scroller">
+ <div style="height:500px">inside scroller</div>
+ <div id="abspos" style="position: absolute; z-index: 5; left: 0; width: 80px; top: 20px; height: 80px; background-color: green">abspos inside scroller</div>
+</div>
+<iframe id="testframe"></iframe>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_fission_inactivescroller_under_oopif.html b/gfx/layers/apz/test/mochitest/helper_fission_inactivescroller_under_oopif.html
new file mode 100644
index 0000000000..c3099f52ef
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_fission_inactivescroller_under_oopif.html
@@ -0,0 +1,88 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Ensure inactive scollframes under OOPIFs hit-test properly</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="helper_fission_utils.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script>
+
+fission_subtest_init();
+
+FissionTestHelper.startTestPromise
+ .then(waitUntilApzStable)
+ .then(loadOOPIFrame("testframe", "helper_fission_empty.html"))
+ .then(waitUntilApzStable)
+ .then(test)
+ .then(FissionTestHelper.subtestDone, FissionTestHelper.subtestFailed);
+
+let make_oopif_scrollable = function() {
+ // ensure the oopif is scrollable, and wait for the paint so that the
+ // compositor also knows it's scrollable.
+ document.body.style.height = "200vh";
+ promiseApzFlushedRepaints().then(() => {
+ let utils = SpecialPowers.getDOMWindowUtils(window);
+ let result = {
+ layersId: utils.getLayersId(),
+ viewId: utils.getViewId(document.scrollingElement)
+ };
+ dump(`OOPIF computed IDs ${JSON.stringify(result)}\n`);
+ FissionTestHelper.fireEventInEmbedder("OOPIF:Scrollable", result);
+ });
+ return true;
+};
+
+async function test() {
+ let iframe = document.getElementById("testframe");
+
+ let letScrollerIdPromise = promiseOneEvent(window, "OOPIF:Scrollable", null);
+ ok(await FissionTestHelper.sendToOopif(iframe, `(${make_oopif_scrollable})()`),
+ "Ran code to make OOPIF scrollable");
+ let oopifScrollerIds = (await letScrollerIdPromise).data;
+
+ // The #scroller div is (a) inactive, and (b) under the OOPIF. Hit-testing
+ // against it should hit the OOPIF.
+
+ checkHitResult(await fissionHitTest(centerOf("scroller"), iframe),
+ APZHitResultFlags.VISIBLE,
+ oopifScrollerIds.viewId,
+ oopifScrollerIds.layersId,
+ "Part of OOPIF sitting on top of the inactive scrollframe should hit OOPIF");
+}
+
+ </script>
+</head>
+<body>
+<style>
+html, body {
+ margin: 0;
+}
+body {
+ /* Ensure root document is scrollable so that #scroller is inactive by
+ default */
+ height: 200vh;
+}
+iframe {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 300px;
+ height: 200px;
+}
+
+#scroller {
+ width: 200px;
+ height: 200px;
+ background-color: transparent;
+ overflow-y: scroll;
+}
+</style>
+<div id="scroller">
+ <div style="height:500px">inside scroller</div>
+</div>
+<iframe id="testframe"></iframe>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_fission_initial_displayport.html b/gfx/layers/apz/test/mochitest/helper_fission_initial_displayport.html
new file mode 100644
index 0000000000..bb03ad4eb7
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_fission_initial_displayport.html
@@ -0,0 +1,105 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test that OOP iframe's displayport is initially set</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="helper_fission_utils.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script>
+
+fission_subtest_init();
+
+FissionTestHelper.startTestPromise
+ .then(waitUntilApzStable)
+ .then(loadOOPIFrame("visible-testframe", "helper_fission_empty.html"))
+ .then(loadOOPIFrame("invisible-testframe", "helper_fission_empty.html"))
+ .then(loadOOPIFrame("scrolled-out-testframe", "helper_fission_empty.html"))
+ .then(loadOOPIFrame("clipped-out-testframe", "helper_fission_empty.html"))
+ .then(loadOOPIFrame("partially-scrolled-out-testframe", "helper_fission_empty.html"))
+ .then(waitUntilApzStable)
+ .then(test)
+ .then(FissionTestHelper.subtestDone, FissionTestHelper.subtestFailed);
+
+
+// The actual test
+
+async function getIframeDisplayport(iframeElement) {
+ let oopif_displayport = function() {
+ let result = getLastContentDisplayportFor("fission_empty_docelement", false);
+ FissionTestHelper.fireEventInEmbedder("OOPIF:Displayport", result);
+ return true;
+ };
+
+ let iframePromise = promiseOneEvent(window, "OOPIF:Displayport", null);
+ ok(await FissionTestHelper.sendToOopif(iframeElement, `(${oopif_displayport})()`));
+ let iframeResponse = await iframePromise;
+ dump("OOPIF response for Displayport: " +
+ JSON.stringify(iframeResponse.data) + "\n");
+
+ return iframeResponse.data;
+}
+
+async function test() {
+ const visibleIframeElement = document.getElementById("visible-testframe");
+
+ // Fully visible iframe.
+ let displayport = await getIframeDisplayport(visibleIframeElement);
+ is(displayport.width, 400, "The displayport size should be same as the iframe size");
+ is(displayport.height, 300, "The displayport size should be same as the iframe size");
+
+ // Fully invisible iframe (inside `overflow: hidden` parent element)
+ const invisibleIframeElement = document.getElementById("invisible-testframe");
+ displayport = await getIframeDisplayport(invisibleIframeElement);
+ ok(!displayport, "The displayport shouldn't have set for invisible iframe");
+
+ // Scrolled out iframe.
+ const scrolledOutIframeElement = document.getElementById("scrolled-out-testframe");
+ displayport = await getIframeDisplayport(scrolledOutIframeElement);
+ ok(!displayport,
+ "The displayport shouldn't have set for iframe far away from the parent displayport");
+
+ // Partially invisible iframe (inside `overflow: hidden` parent element)
+ const clippedOutIframeElement = document.getElementById("clipped-out-testframe");
+ displayport = await getIframeDisplayport(clippedOutIframeElement);
+ is(displayport.width, 400, "The displayport width should be same as the iframe width");
+ ok(displayport.height > 0, "The displayport height should be greater than zero");
+ ok(displayport.height < 300, "The displayport height should be less than the iframe height");
+
+ const partiallyScrolledOutIframeElement = document.getElementById("partially-scrolled-out-testframe");
+ displayport = await getIframeDisplayport(partiallyScrolledOutIframeElement);
+ is(displayport.width, 400, "The displayport width should be same as the iframe width");
+ ok(displayport.height > 0, "The displayport height should be greater than zero");
+ ok(displayport.height < 300, "The displayport height should be less than the iframe height");
+}
+
+ </script>
+ <style>
+ iframe {
+ width: 400px;
+ height: 300px;
+ border: none;
+ }
+ </style>
+</head>
+<body>
+<iframe id="visible-testframe"></iframe>
+<div style="width: 300px; height: 300px; overflow: hidden;">
+ <div style="width: 100%; height: 1000px;"></div>
+ <iframe id="invisible-testframe"></iframe>
+</div>
+<div style="width: 300px; height: 300px; overflow: scroll;">
+ <div style="width: 100%; height: 10000px;"></div>
+ <iframe id="scrolled-out-testframe"></iframe>
+</div>
+<div style="width: 300px; height: 300px; overflow: hidden;">
+ <div style="width: 100%; height: 200px;"></div>
+ <iframe id="clipped-out-testframe"></iframe>
+</div>
+<div style="width: 300px; height: 300px; overflow: scroll;">
+ <div style="width: 100%; height: 200px;"></div>
+ <iframe id="partially-scrolled-out-testframe"></iframe>
+</div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_fission_irregular_areas.html b/gfx/layers/apz/test/mochitest/helper_fission_irregular_areas.html
new file mode 100644
index 0000000000..8ef3367c06
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_fission_irregular_areas.html
@@ -0,0 +1,101 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Ensure irregular areas on top of OOPIFs hit-test properly</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="helper_fission_utils.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script>
+
+fission_subtest_init();
+
+FissionTestHelper.startTestPromise
+ .then(waitUntilApzStable)
+ .then(loadOOPIFrame("testframe", "helper_fission_empty.html"))
+ .then(waitUntilApzStable)
+ .then(test)
+ .then(FissionTestHelper.subtestDone, FissionTestHelper.subtestFailed);
+
+let make_oopif_scrollable = function() {
+ // ensure the oopif is scrollable, and wait for the paint so that the
+ // compositor also knows it's scrollable.
+ document.body.style.height = "200vh";
+ promiseApzFlushedRepaints().then(() => {
+ let utils = SpecialPowers.getDOMWindowUtils(window);
+ let result = {
+ layersId: utils.getLayersId(),
+ viewId: utils.getViewId(document.scrollingElement)
+ };
+ dump(`OOPIF computed IDs ${JSON.stringify(result)}\n`);
+ FissionTestHelper.fireEventInEmbedder("OOPIF:Scrollable", result);
+ });
+ return true;
+};
+
+async function test() {
+ let iframe = document.getElementById("testframe");
+
+ let oopifScrollerIds = promiseOneEvent(window, "OOPIF:Scrollable", null);
+ ok(await FissionTestHelper.sendToOopif(iframe, `(${make_oopif_scrollable})()`),
+ "Ran code to make OOPIF scrollable");
+ oopifScrollerIds = (await oopifScrollerIds).data;
+
+ let utils = SpecialPowers.getDOMWindowUtils(window);
+
+ // The triangle_overlay div overlays a part of the iframe. We do 3 hit-tests:
+ // - one that hits the opaque part of the overlay
+ // - one that hits the clipped-away part of the overlay div but is still
+ // inside the bounding box
+ // - one that is not on the overlay at all, but on the part of the iframe not
+ // covered by the overlay.
+ // For the latter two, we expect the hit-test to hit the OOPIF.
+
+ checkHitResult(await fissionHitTest({x: 20, y: 100}, iframe),
+ APZHitResultFlags.VISIBLE | APZHitResultFlags.IRREGULAR_AREA,
+ utils.getViewId(document.scrollingElement),
+ utils.getLayersId(),
+ "opaque part of overlay should hit parent doc hosting the OOPIF");
+
+ checkHitResult(await fissionHitTest({x: 180, y: 100}, iframe),
+ APZHitResultFlags.VISIBLE,
+ oopifScrollerIds.viewId,
+ oopifScrollerIds.layersId,
+ "clipped-away part of overlay should hit OOPIF");
+
+ checkHitResult(await fissionHitTest({x: 250, y: 100}, iframe),
+ APZHitResultFlags.VISIBLE,
+ oopifScrollerIds.viewId,
+ oopifScrollerIds.layersId,
+ "part of OOPIF outside the overlay bounding rect should hit the OOPIF");
+}
+
+ </script>
+</head>
+<body>
+<style>
+html, body {
+ margin: 0;
+}
+iframe {
+ position: absolute;
+ width: 300px;
+ height: 200px;
+}
+
+#triangle_overlay {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 200px;
+ height: 200px;
+ background-color: green;
+ clip-path: polygon(0% 0%, 100% 100%, 0% 100%);
+}
+</style>
+<iframe id="testframe"></iframe>
+<div id="triangle_overlay"></div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_fission_large_subframe.html b/gfx/layers/apz/test/mochitest/helper_fission_large_subframe.html
new file mode 100644
index 0000000000..3d5595f48e
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_fission_large_subframe.html
@@ -0,0 +1,67 @@
+<!DOCTYPE HTML>
+<html id="rcd_docelement">
+<head>
+ <meta charset="utf-8">
+ <title>Test that large OOPIF does not get a too-large displayport</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="helper_fission_utils.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script>
+
+fission_subtest_init();
+
+FissionTestHelper.startTestPromise
+ .then(waitUntilApzStable)
+ .then(loadOOPIFrame("testframe", "helper_fission_empty.html"))
+ .then(waitUntilApzStable)
+ .then(test)
+ .then(FissionTestHelper.subtestDone, FissionTestHelper.subtestFailed);
+
+// Code that will run inside the iframe.
+let get_iframe_displayport = function() {
+ // Give the page a scroll range. This will make the unclamped displayport
+ // even taller.
+ document.body.style.height = "200vh";
+ // Do a round-trip to APZ to make sure the scroll range is reflected
+ // in the displayport it sets.
+ promiseApzFlushedRepaints().then(() => {
+ // Query the composed displayport and report it to the embedder.
+ let result = getLastContentDisplayportFor("fission_empty_docelement");
+ FissionTestHelper.fireEventInEmbedder("OOPIF:Displayport", result);
+ });
+ return true;
+};
+
+async function test() {
+ let iframeElement = document.getElementById("testframe");
+
+ // Give the page a scroll range and make sure APZ sets non-empty displayport margins.
+ document.body.style.height = "500vh";
+ await promiseApzFlushedRepaints();
+
+ // Query the iframe's displayport.
+ let displayportPromise = promiseOneEvent(window, "OOPIF:Displayport", null);
+ ok(await FissionTestHelper.sendToOopif(iframeElement, `(${get_iframe_displayport})()`), "Gotten iframe displayport");
+ let iframeDp = (await displayportPromise).data;
+ dump("iframe displayport is " + JSON.stringify(iframeDp) + "\n");
+
+ // Query the page's displayport.
+ let dp = getLastContentDisplayportFor("rcd_docelement");
+ dump("page displayport is " + JSON.stringify(dp) + "\n");
+
+ // Check that the iframe's displayport is no more than twice as tall as
+ // the page's displayport. The reason it can be up to twice as tall is
+ // described in bug 1690697; we may be able to assert a tighter bound
+ // after making improvements in that bug.
+ ok(iframeDp.height <= (dp.height * 2), "iframe displayport should be no more than twice as tall as page displayport");
+}
+
+ </script>
+</head>
+<body>
+<!-- Make the iframe's viewport very tall -->
+<iframe style="margin-top: 200px" id="testframe" height="10000px"></iframe>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_fission_scroll_handoff.html b/gfx/layers/apz/test/mochitest/helper_fission_scroll_handoff.html
new file mode 100644
index 0000000000..3f75706778
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_fission_scroll_handoff.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>scroll handoff</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="helper_fission_utils.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script>
+
+fission_subtest_init();
+
+FissionTestHelper.startTestPromise
+ .then(waitUntilApzStable)
+ .then(loadOOPIFrame("testframe", "helper_fission_empty.html"))
+ .then(waitUntilApzStable)
+ .then(test)
+ .then(FissionTestHelper.subtestDone, FissionTestHelper.subtestFailed);
+
+async function test() {
+ let scrollEventPromise = new Promise(resolve => {
+ window.addEventListener("scroll", resolve, { once: true });
+ });
+
+ let iframe = document.getElementById("testframe");
+
+ await synthesizeNativeWheel(iframe, 100, 100, 0, -50);
+ await scrollEventPromise;
+
+ ok(window.scrollY > 0,
+ "Mouse wheel scrolling on OOP iframes in position:fixed subtree " +
+ "should be handed off to the parent");
+}
+
+ </script>
+</head>
+<style>
+iframe {
+ position: fixed;
+ width: 500px;
+ height: 500px;
+}
+</style>
+<body>
+<iframe id="testframe"></iframe>
+<div style="height:1000vh"></div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_fission_scroll_oopif.html b/gfx/layers/apz/test/mochitest/helper_fission_scroll_oopif.html
new file mode 100644
index 0000000000..2911b1eaf0
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_fission_scroll_oopif.html
@@ -0,0 +1,158 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for async-scrolling an OOPIF and ensuring hit-testing still works</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="helper_fission_utils.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script>
+
+fission_subtest_init();
+
+FissionTestHelper.startTestPromise
+ .then(waitUntilApzStable)
+ .then(loadOOPIFrame("testframe", "helper_fission_empty.html"))
+ .then(waitUntilApzStable)
+ .then(test)
+ .then(FissionTestHelper.subtestDone, FissionTestHelper.subtestFailed);
+
+
+let code_for_oopif_to_run = function() {
+ document.addEventListener("click", function(e) {
+ dump(`OOPIF got click at ${e.clientX},${e.clientY}\n`);
+ let result = { x: e.clientX, y: e.clientY };
+ FissionTestHelper.fireEventInEmbedder("OOPIF:ClickData", result);
+ });
+ dump("OOPIF registered click listener\n");
+ return true;
+};
+
+async function clickOnIframe(x, y) {
+ let iframePromise = promiseOneEvent(window, "OOPIF:ClickData", null);
+ await synthesizeNativeMouseEventWithAPZ(
+ { type: "click", target: document.body, offsetX: x, offsetY: y },
+ () => dump("Finished synthesizing click, waiting for OOPIF message...\n")
+ );
+ let iframeResponse = await iframePromise;
+ dump("OOPIF response: " + JSON.stringify(iframeResponse.data) + "\n");
+ return iframeResponse.data;
+}
+
+let oopif_scroll_pos = function() {
+ dump(`OOPIF scroll position is y=${window.scrollY}\n`);
+ let result = { y: window.scrollY };
+ FissionTestHelper.fireEventInEmbedder("OOPIF:ScrollPos", result);
+ return true;
+};
+
+async function getIframeScrollY() {
+ let iframeElement = document.getElementById("testframe");
+ let iframePromise = promiseOneEvent(window, "OOPIF:ScrollPos", null);
+ ok(await FissionTestHelper.sendToOopif(iframeElement, `(${oopif_scroll_pos})()`), "Sent scrollY request");
+ let iframeResponse = await iframePromise;
+ dump("OOPIF response for scrollPos: " + JSON.stringify(iframeResponse.data) + "\n");
+ return iframeResponse.data.y;
+}
+
+let make_oopif_scrollable = function() {
+ // ensure the oopif is scrollable, and wait for the paint so that the
+ // compositor also knows it's scrollable.
+ document.body.style.height = "200vh";
+ promiseApzFlushedRepaints().then(() => {
+ let result = { y: window.scrollMaxY };
+ FissionTestHelper.fireEventInEmbedder("OOPIF:Scrollable", result);
+ });
+ // Also register a scroll listener for when it actually gets scrolled.
+ window.addEventListener("scroll", function(e) {
+ dump(`OOPIF got scroll event, now at ${window.scrollY}\n`);
+ let result = { y: window.scrollY };
+ FissionTestHelper.fireEventInEmbedder("OOPIF:Scrolled", result);
+ }, {once: true});
+ return true;
+};
+
+function failsafe(eventType) {
+ // Catch and fail faster on the case where the event ends up not going to
+ // the iframe like it should. Otherwise the test hangs until timeout which
+ // is more painful.
+ document.addEventListener(eventType, function(e) {
+ dump(`${location.href} got ${e.type} at ${e.clientX},${e.clientY}\n`);
+ ok(false, `The OOPIF hosting page should not have gotten the ${eventType}`);
+ setTimeout(FissionTestHelper.subtestFailed, 0);
+ }, {once: true});
+}
+
+// The actual test
+
+async function test() {
+ ok(SpecialPowers.getBoolPref("apz.paint_skipping.enabled"),
+ "paint-skipping is expected to be enabled for this test to be meaningful");
+
+ let iframeElement = document.getElementById("testframe");
+
+ let iframeResponse = await FissionTestHelper.sendToOopif(iframeElement, `(${code_for_oopif_to_run})()`);
+ dump("OOPIF response: " + JSON.stringify(iframeResponse) + "\n");
+ ok(iframeResponse, "code_for_oopif_to_run successfully installed");
+
+ is(window.scrollY, 0, "window is at 0 scroll position");
+
+ // hit-test into the iframe before scrolling
+ let oldClickPoint = await clickOnIframe(50, 250);
+
+ // do an APZ scroll and wait for the main-thread to get the repaint request,
+ // and queue up a paint-skip scroll notification back to APZ.
+ await promiseMoveMouseAndScrollWheelOver(document.body, 10, 10);
+
+ // The wheel scroll might have started an APZ animation, so run that to the end
+ var utils = SpecialPowers.getDOMWindowUtils(window);
+ for (var i = 0; i < 60; i++) {
+ utils.advanceTimeAndRefresh(16);
+ }
+ utils.restoreNormalRefresh();
+ // Let the repaint requests get processed
+ await promiseOnlyApzControllerFlushed();
+ await promiseAllPaintsDone();
+
+ ok(window.scrollY > 5, "window has scrolled by " + window.scrollY + " pixels");
+
+ // hit-test into the iframe after scrolling. The coordinates here are the
+ // same relative to the body as before, but get computed to be different
+ // relative to the window/screen.
+ let newClickPoint = await clickOnIframe(50, 250);
+
+ is(newClickPoint.x, oldClickPoint.x, "x-coord of old and new match");
+ is(newClickPoint.y, oldClickPoint.y, "y-coord of old and new match");
+
+ // Also check that we can send scroll events to the OOPIF. Any wheel events
+ // delivered to this page after this point should result in a failure.
+ failsafe("wheel");
+
+ let iframeY = await getIframeScrollY();
+ is(iframeY, 0, "scrollY of iframe should be 0 initially");
+
+ // Ensure the OOPIF is scrollable.
+ let scrollablePromise = promiseOneEvent(window, "OOPIF:Scrollable", null);
+ ok(await FissionTestHelper.sendToOopif(iframeElement, `(${make_oopif_scrollable})()`), "Made OOPIF scrollable");
+ let oopifScrollMaxY = (await scrollablePromise).data.y;
+ ok(oopifScrollMaxY > 0, "Confirmed that oopif is scrollable");
+
+ // Now scroll over the OOP-iframe (we know it must be under the 50,250 point
+ // because we just checked that above). Note that listening for wheel/scroll
+ // events is trickier because they will fire in the OOPIF, so we can't just
+ // use promiseMoveMouseAndScrollWheelOver directly.
+ let scrolledPromise = promiseOneEvent(window, "OOPIF:Scrolled", null);
+ await synthesizeNativeWheel(document.body, 50, 250, 0, -10);
+ iframeY = (await scrolledPromise).data.y;
+ ok(iframeY > 0, "scrollY of iframe should be >0 after scrolling");
+}
+
+ </script>
+</head>
+<body onload="failsafe('click')">
+<iframe style="margin-top: 200px" id="testframe"></iframe>
+<div style="height: 5000px">tall div to make the page scrollable</div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_fission_setResolution.html b/gfx/layers/apz/test/mochitest/helper_fission_setResolution.html
new file mode 100644
index 0000000000..6bcf3fa2ce
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_fission_setResolution.html
@@ -0,0 +1,59 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>setResolutionAndScaleTo is properly delivered to OOP iframes</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="helper_fission_utils.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script>
+
+fission_subtest_init();
+
+FissionTestHelper.startTestPromise
+ .then(waitUntilApzStable)
+ .then(loadOOPIFrame("testframe", "helper_fission_empty.html"))
+ .then(waitUntilApzStable)
+ .then(test)
+ .then(FissionTestHelper.subtestDone, FissionTestHelper.subtestFailed);
+
+async function test() {
+ let iframeElement = document.getElementById("testframe");
+
+ const scale = 2.0;
+
+ SpecialPowers.getDOMWindowUtils(window).setResolutionAndScaleTo(scale);
+ await promiseApzFlushedRepaints(window);
+ await waitUntilApzStable();
+
+ const originalWidth = originalHeight = 100;
+ // eslint-disable-next-line no-unused-vars
+ let { _x, _y, width, height } = await SpecialPowers.spawn(
+ iframeElement,
+ [originalWidth, originalHeight],
+ // eslint-disable-next-line no-shadow
+ (width, height) => {
+ // nsIDOMWindowUtils.toScreenRect uses the iframe's transform which should
+ // have been informed from APZ.
+ return SpecialPowers.DOMWindowUtils.toScreenRect(0, 0, width, height);
+ }
+ );
+
+ is(width, scale * originalWidth,
+ "The resolution value should be properly delivered into OOP iframes");
+ is(height, scale * originalHeight,
+ "The resolution value should be properly delivered into OOP iframes");
+}
+
+ </script>
+ <style>
+ body, html {
+ margin: 0;
+ }
+ </style>
+</head>
+<body>
+<iframe id="testframe"></iframe>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_fission_tap.html b/gfx/layers/apz/test/mochitest/helper_fission_tap.html
new file mode 100644
index 0000000000..fab04bab26
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_fission_tap.html
@@ -0,0 +1,87 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test to ensure events get untransformed properly for OOP iframes</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="helper_fission_utils.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script>
+
+// Copied from helper_fission_transforms.html, except for the
+// synthesis function.
+
+fission_subtest_init();
+
+FissionTestHelper.startTestPromise
+ .then(waitUntilApzStable)
+ .then(loadOOPIFrame("testframe", "helper_fission_empty.html"))
+ .then(waitUntilApzStable)
+ .then(test)
+ .then(FissionTestHelper.subtestDone, FissionTestHelper.subtestFailed);
+
+let code_for_oopif_to_run = function() {
+ document.addEventListener("click", function(e) {
+ dump(`OOPIF got click at ${e.clientX},${e.clientY}\n`);
+ let result = { x: e.clientX, y: e.clientY };
+ FissionTestHelper.fireEventInEmbedder("OOPIF:ClickData", result);
+ }, {once: true});
+ dump("OOPIF registered click listener\n");
+ return true;
+};
+
+function failsafe() {
+ // Catch and fail faster on the case where the click ends up not going to
+ // the iframe like it should. Otherwise the test hangs until timeout which
+ // is more painful.
+ document.addEventListener("click", function(e) {
+ dump(`${location.href} got click at ${e.clientX},${e.clientY}\n`);
+ ok(false, "The OOPIF hosting page should not have gotten the click");
+ setTimeout(FissionTestHelper.subtestDone, 0);
+ }, {once: true});
+}
+
+async function test() {
+ let iframeElement = document.getElementById("testframe");
+
+ let iframeResponse = await FissionTestHelper.sendToOopif(iframeElement, `(${code_for_oopif_to_run})()`)
+ dump("OOPIF response: " + JSON.stringify(iframeResponse) + "\n");
+ ok(iframeResponse, "code_for_oopif_to_run successfully installed");
+
+ iframePromise = promiseOneEvent(window, "OOPIF:ClickData", null);
+ await synthesizeNativeTap(document.body, 400, 400, function() {
+ dump("Finished synthesizing click, waiting for OOPIF message...\n");
+ });
+ iframeResponse = await iframePromise;
+ dump("OOPIF response: " + JSON.stringify(iframeResponse.data) + "\n");
+
+ let expected_coord = 200 / Math.sqrt(2); // because the iframe is rotated 45 deg
+ ok(Math.abs(iframeResponse.data.x - expected_coord) < 3,
+ `x-coord ${iframeResponse.data.x} landed near expected value ${expected_coord}`);
+ ok(Math.abs(iframeResponse.data.y - expected_coord) < 3,
+ `y-coord ${iframeResponse.data.y} landed near expected value ${expected_coord}`);
+}
+
+ </script>
+ <style>
+ body, html {
+ margin: 0;
+ }
+ div {
+ transform-origin: top left;
+ transform: translateX(400px) scale(2) rotate(45deg);
+ width: 500px;
+ }
+ iframe {
+ width: 400px;
+ height: 300px;
+ border: solid 1px black;
+ }
+ </style>
+</head>
+<body onload="failsafe()">
+<div><iframe id="testframe"></iframe></div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_fission_tap_in_nested_iframe_on_zoomed.html b/gfx/layers/apz/test/mochitest/helper_fission_tap_in_nested_iframe_on_zoomed.html
new file mode 100644
index 0000000000..47a9c4bf8e
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_fission_tap_in_nested_iframe_on_zoomed.html
@@ -0,0 +1,106 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test to ensure events get delivered properly for a nested OOP iframe</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="helper_fission_utils.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script>
+
+// Copied from helper_fission_tap_on_zoomed.html. In this test
+// SpecialPowers.spawn is used instead of FissionTestHelper.sendToOopif to
+// handle scripts in a nested OOP iframe.
+
+fission_subtest_init();
+
+SpecialPowers.getDOMWindowUtils(window).setResolutionAndScaleTo(2.0);
+
+FissionTestHelper.startTestPromise
+ .then(waitUntilApzStable)
+ .then(loadOOPIFrame("testframe", "helper_fission_empty.html"))
+ .then(waitUntilApzStable)
+ .then(test)
+ .then(FissionTestHelper.subtestDone, FissionTestHelper.subtestFailed);
+
+function failsafe() {
+ // Catch and fail faster on the case where the click ends up not going to
+ // the iframe like it should. Otherwise the test hangs until timeout which
+ // is more painful.
+ document.addEventListener("click", function(e) {
+ dump(`${location.href} got click at ${e.clientX},${e.clientY}\n`);
+ ok(false, "The OOPIF hosting page should not have gotten the click");
+ setTimeout(FissionTestHelper.subtestDone, 0);
+ }, {once: true});
+}
+
+async function test() {
+ let iframeElement = document.getElementById("testframe");
+
+ // Load another OOP document in the parent OOP iframe.
+ await SpecialPowers.spawn(iframeElement, [], async () => {
+ const iframe = content.document.createElement("iframe");
+ iframe.src =
+ "https://example.org/browser/gfx/layers/apz/test/mochitest/helper_fission_empty.html";
+ iframe.style.width = "400px";
+ iframe.style.height = "300px";
+ iframe.style.border = "none";
+ content.document.body.appendChild(iframe);
+ await new Promise(resolve => {
+ iframe.addEventListener("load", resolve, {once: true});
+ });
+ await SpecialPowers.spawn(iframe, [], async () => {
+ await content.wrappedJSObject.promiseApzFlushedRepaints(content.window);
+ });
+ });
+
+ // Set a click event listener in the nested OOP document.
+ const iframePromise = SpecialPowers.spawn(iframeElement, [], async () => {
+ const iframe = content.document.querySelector("iframe");
+ const result = await SpecialPowers.spawn(iframe, [], async () => {
+ return new Promise(resolve => {
+ content.document.addEventListener("click", e => {
+ dump(`OOPIF got click at ${e.clientX},${e.clientY}\n`);
+ resolve({ x: e.clientX, y: e.clientY });
+ }, {once: true});
+ });
+ });
+ return result;
+ });
+
+ await synthesizeNativeTap(document.documentElement, 200, 200, function() {
+ dump("Finished synthesizing click, waiting for OOPIF message...\n");
+ });
+ let iframeResponse = await iframePromise;
+ dump("OOPIF response: " + JSON.stringify(iframeResponse) + "\n");
+
+ let expected_coord = 100; // because the parent iframe is offseted by (100, 100).
+ ok(Math.abs(iframeResponse.x - expected_coord) < 3,
+ `x-coord ${iframeResponse.x} landed near expected value ${expected_coord}`);
+ ok(Math.abs(iframeResponse.y - expected_coord) < 3,
+ `y-coord ${iframeResponse.y} landed near expected value ${expected_coord}`);
+}
+
+ </script>
+ <style>
+ body, html {
+ margin: 0;
+ }
+ div {
+ margin-left: 100px;
+ margin-top: 100px;
+ width: 500px;
+ }
+ iframe {
+ width: 400px;
+ height: 300px;
+ border: solid 1px black;
+ }
+ </style>
+</head>
+<body onload="failsafe()">
+<div><iframe id="testframe"></iframe></div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_fission_tap_on_zoomed.html b/gfx/layers/apz/test/mochitest/helper_fission_tap_on_zoomed.html
new file mode 100644
index 0000000000..5d99b972c2
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_fission_tap_on_zoomed.html
@@ -0,0 +1,93 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test to ensure events get delivered properly for an OOP iframe</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="helper_fission_utils.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script>
+
+// Copied from helper_fission_touch.html, differences are 1) the iframe is not
+// transformed instead it's offseted by margin values, 2) the top level document
+// is zoomed by 2.0, 3) using documentElement instead of body to query
+// getBoundingClientRect() because margin collapsing happens between the body
+// and the offseted div (i.e. getBoundingClientRect() for body returns 100px top
+// value).
+
+fission_subtest_init();
+
+SpecialPowers.getDOMWindowUtils(window).setResolutionAndScaleTo(2.0);
+
+FissionTestHelper.startTestPromise
+ .then(waitUntilApzStable)
+ .then(loadOOPIFrame("testframe", "helper_fission_empty.html"))
+ .then(waitUntilApzStable)
+ .then(test)
+ .then(FissionTestHelper.subtestDone, FissionTestHelper.subtestFailed);
+
+let code_for_oopif_to_run = function() {
+ document.addEventListener("click", function(e) {
+ dump(`OOPIF got click at ${e.clientX},${e.clientY}\n`);
+ let result = { x: e.clientX, y: e.clientY };
+ FissionTestHelper.fireEventInEmbedder("OOPIF:ClickData", result);
+ }, {once: true});
+ dump("OOPIF registered click listener\n");
+ return true;
+};
+
+function failsafe() {
+ // Catch and fail faster on the case where the click ends up not going to
+ // the iframe like it should. Otherwise the test hangs until timeout which
+ // is more painful.
+ document.addEventListener("click", function(e) {
+ dump(`${location.href} got click at ${e.clientX},${e.clientY}\n`);
+ ok(false, "The OOPIF hosting page should not have gotten the click");
+ setTimeout(FissionTestHelper.subtestDone, 0);
+ }, {once: true});
+}
+
+async function test() {
+ let iframeElement = document.getElementById("testframe");
+
+ let iframeResponse = await FissionTestHelper.sendToOopif(iframeElement, `(${code_for_oopif_to_run})()`)
+ dump("OOPIF response: " + JSON.stringify(iframeResponse) + "\n");
+ ok(iframeResponse, "code_for_oopif_to_run successfully installed");
+
+ iframePromise = promiseOneEvent(window, "OOPIF:ClickData", null);
+ await synthesizeNativeTap(document.documentElement, 200, 200, function() {
+ dump("Finished synthesizing click, waiting for OOPIF message...\n");
+ });
+ iframeResponse = await iframePromise;
+ dump("OOPIF response: " + JSON.stringify(iframeResponse.data) + "\n");
+
+ let expected_coord = 100; // because the iframe is offseted by (100, 100).
+ ok(Math.abs(iframeResponse.data.x - expected_coord) < 3,
+ `x-coord ${iframeResponse.data.x} landed near expected value ${expected_coord}`);
+ ok(Math.abs(iframeResponse.data.y - expected_coord) < 3,
+ `y-coord ${iframeResponse.data.y} landed near expected value ${expected_coord}`);
+}
+
+ </script>
+ <style>
+ body, html {
+ margin: 0;
+ }
+ div {
+ margin-left: 100px;
+ margin-top: 100px;
+ width: 500px;
+ }
+ iframe {
+ width: 400px;
+ height: 300px;
+ border: solid 1px black;
+ }
+ </style>
+</head>
+<body onload="failsafe()">
+<div><iframe id="testframe"></iframe></div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_fission_touch.html b/gfx/layers/apz/test/mochitest/helper_fission_touch.html
new file mode 100644
index 0000000000..fa317b9f1f
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_fission_touch.html
@@ -0,0 +1,99 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test to ensure touch events for OOP iframes</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="helper_fission_utils.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script>
+
+fission_subtest_init();
+
+FissionTestHelper.startTestPromise
+ .then(waitUntilApzStable)
+ .then(loadOOPIFrame("testframe", "helper_fission_empty.html"))
+ .then(waitUntilApzStable)
+ .then(test)
+ .then(FissionTestHelper.subtestDone, FissionTestHelper.subtestFailed);
+
+
+let code_for_oopif_to_run = function() {
+ let listener = function(e) {
+ let result = { type: e.type, touches: [] };
+ dump(`OOPIF got ${e.type}\n`);
+ for (let touch of e.touches) {
+ result.touches.push({
+ identifier: touch.identifier,
+ clientX: touch.clientX,
+ clientY: touch.clientY
+ });
+ dump(` identifier ${touch.identifier} at ${touch.clientX},${touch.clientY}\n`);
+ }
+ FissionTestHelper.fireEventInEmbedder("OOPIF:TouchEvent", result);
+ };
+ document.addEventListener("touchstart", listener, {once: true});
+ document.addEventListener("touchmove", listener, {once: true});
+ document.addEventListener("touchend", listener, {once: true});
+ dump("OOPIF registered touch listener\n");
+ return true;
+};
+
+function failsafe() {
+ let failListener = function(e) {
+ dump(`${location.href} got ${e.type}\n`);
+ ok(false, `The OOPIF hosting page should not have gotten the ${e.type}`);
+ setTimeout(FissionTestHelper.subtestDone, 0);
+ };
+ // Catch and fail faster on the case where the touch event ends up not going
+ // to the iframe like it should. Otherwise the test hangs until timeout which
+ // is more painful.
+ document.addEventListener("touchstart", failListener, {once: true});
+ document.addEventListener("touchmove", failListener, {once: true});
+ document.addEventListener("touchend", failListener, {once: true});
+}
+
+function waitForTouchEvent(aType) {
+ return promiseOneEvent(window, "OOPIF:TouchEvent", function(e) {
+ return e.data.type === aType;
+ });
+}
+
+async function test() {
+ let iframeElement = document.getElementById("testframe");
+
+ let iframeResponse = await FissionTestHelper.sendToOopif(iframeElement, `(${code_for_oopif_to_run})()`);
+ dump("OOPIF response: " + JSON.stringify(iframeResponse) + "\n");
+ ok(iframeResponse, "code_for_oopif_to_run successfully installed");
+
+ iframePromise = Promise.all([waitForTouchEvent("touchstart"),
+ waitForTouchEvent("touchmove"),
+ waitForTouchEvent("touchend")]);
+ await synthesizeNativeTouchSequences(document.body,
+ [[{x: 100, y: 100}], [{x: 150, y: 150}], [{x: 150, y: 150}]], function() {
+ dump("Finished synthesizing touch tap, waiting for OOPIF message...\n");
+ });
+ await iframePromise;
+}
+
+ </script>
+ <style>
+ body, html {
+ margin: 0;
+ }
+ div {
+ width: 500px;
+ }
+ iframe {
+ width: 400px;
+ height: 300px;
+ border: solid 1px black;
+ }
+ </style>
+</head>
+<body onload="failsafe()">
+<div><iframe id="testframe"></iframe></div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_fission_transforms.html b/gfx/layers/apz/test/mochitest/helper_fission_transforms.html
new file mode 100644
index 0000000000..193b5650fd
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_fission_transforms.html
@@ -0,0 +1,89 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test to ensure events get untransformed properly for OOP iframes</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="helper_fission_utils.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script>
+
+fission_subtest_init();
+
+FissionTestHelper.startTestPromise
+ .then(waitUntilApzStable)
+ .then(loadOOPIFrame("testframe", "helper_fission_empty.html"))
+ .then(waitUntilApzStable)
+ .then(test)
+ .then(FissionTestHelper.subtestDone, FissionTestHelper.subtestFailed);
+
+
+let code_for_oopif_to_run = function() {
+ document.addEventListener("click", function(e) {
+ dump(`OOPIF got click at ${e.clientX},${e.clientY}\n`);
+ let result = { x: e.clientX, y: e.clientY };
+ FissionTestHelper.fireEventInEmbedder("OOPIF:ClickData", result);
+ }, {once: true});
+ dump("OOPIF registered click listener\n");
+ return true;
+};
+
+function failsafe() {
+ // Catch and fail faster on the case where the click ends up not going to
+ // the iframe like it should. Otherwise the test hangs until timeout which
+ // is more painful.
+ document.addEventListener("click", function(e) {
+ dump(`${location.href} got click at ${e.clientX},${e.clientY}\n`);
+ ok(false, "The OOPIF hosting page should not have gotten the click");
+ setTimeout(FissionTestHelper.subtestDone, 0);
+ }, {once: true});
+}
+
+async function test() {
+ let iframeElement = document.getElementById("testframe");
+
+ let iframeResponse = await FissionTestHelper.sendToOopif(iframeElement, `(${code_for_oopif_to_run})()`);
+ dump("OOPIF response: " + JSON.stringify(iframeResponse) + "\n");
+ ok(iframeResponse, "code_for_oopif_to_run successfully installed");
+
+ iframePromise = promiseOneEvent(window, "OOPIF:ClickData", null);
+ await synthesizeNativeMouseEventWithAPZ(
+ { type: "click", target: document.body, offsetX: 400, offsetY: 400 },
+ () => dump("Finished synthesizing click, waiting for OOPIF message...\n")
+ );
+ iframeResponse = await iframePromise;
+ dump("OOPIF response: " + JSON.stringify(iframeResponse.data) + "\n");
+
+ let expected_coord = 200 / Math.sqrt(2); // because the iframe is rotated 45 deg
+ ok(Math.abs(iframeResponse.data.x - expected_coord) < 3,
+ `x-coord ${iframeResponse.data.x} landed near expected value ${expected_coord}`);
+ ok(Math.abs(iframeResponse.data.y - expected_coord) < 3,
+ `y-coord ${iframeResponse.data.y} landed near expected value ${expected_coord}`);
+}
+
+ </script>
+ <style>
+ body, html {
+ margin: 0;
+ }
+ div {
+ transform-origin: top left;
+ transform: translateX(400px) scale(2) rotate(45deg) translate(-50px, -50px);
+ width: 500px;
+ }
+ iframe {
+ width: 400px;
+ height: 300px;
+ position: absolute;
+ top: 50px;
+ left: 50px;
+ border: solid 1px black;
+ }
+ </style>
+</head>
+<body onload="failsafe()">
+<div><iframe id="testframe"></iframe></div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_fission_utils.js b/gfx/layers/apz/test/mochitest/helper_fission_utils.js
new file mode 100644
index 0000000000..ddcab62253
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_fission_utils.js
@@ -0,0 +1,130 @@
+// loadOOPIFrame expects apz_test_utils.js to be loaded as well, for promiseOneEvent.
+/* import-globals-from apz_test_utils.js */
+
+function fission_subtest_init() {
+ // Silence SimpleTest warning about missing assertions by having it wait
+ // indefinitely. We don't need to give it an explicit finish because the
+ // entire window this test runs in will be closed after subtestDone is called.
+ SimpleTest.waitForExplicitFinish();
+
+ // This is the point at which we inject the ok, is, subtestDone, etc. functions
+ // into this window. In particular this function should run after SimpleTest.js
+ // is imported, otherwise SimpleTest.js will clobber the functions with its
+ // own versions. This is implicitly enforced because if we call this function
+ // before SimpleTest.js is imported, the above line will throw an exception.
+ window.dispatchEvent(new Event("FissionTestHelper:Init"));
+}
+
+/**
+ * Starts loading the given `iframePage` in the iframe element with the given
+ * id, and waits for it to load.
+ * Note that calling this function doesn't do the load directly; instead it
+ * returns an async function which can be added to a thenable chain.
+ */
+function loadOOPIFrame(iframeElementId, iframePage) {
+ return async function () {
+ if (window.location.href.startsWith("https://example.com/")) {
+ dump(
+ `WARNING: Calling loadOOPIFrame from ${window.location.href} so the iframe may not be OOP\n`
+ );
+ ok(false, "Current origin is not example.com:443");
+ }
+
+ let url =
+ "https://example.com/browser/gfx/layers/apz/test/mochitest/" + iframePage;
+ let loadPromise = promiseOneEvent(window, "OOPIF:Load", function (e) {
+ return typeof e.data.content == "string" && e.data.content == url;
+ });
+ let elem = document.getElementById(iframeElementId);
+ elem.src = url;
+ await loadPromise;
+ };
+}
+
+/**
+ * This is similar to the hitTest function in apz_test_utils.js, in that it
+ * does a hit-test for a point and returns the result. The difference is that
+ * in the fission world, the hit-test may land on an OOPIF, which means the
+ * result information will be in the APZ test data for the OOPIF process. This
+ * function checks both the current process and OOPIF process to see which one
+ * got a hit result, and returns the result regardless of which process got it.
+ * The caller is expected to check the layers id which will allow distinguishing
+ * the two cases.
+ */
+async function fissionHitTest(point, iframeElement) {
+ let get_iframe_compositor_test_data = function () {
+ let utils = SpecialPowers.getDOMWindowUtils(window);
+ return JSON.stringify(utils.getCompositorAPZTestData());
+ };
+
+ let utils = SpecialPowers.getDOMWindowUtils(window);
+
+ // Get the test data before doing the actual hit-test, to get a baseline
+ // of what we can ignore.
+ let oldParentTestData = utils.getCompositorAPZTestData();
+ let oldIframeTestData = JSON.parse(
+ await FissionTestHelper.sendToOopif(
+ iframeElement,
+ `(${get_iframe_compositor_test_data})()`
+ )
+ );
+
+ // Now do the hit-test
+ dump(`Hit-testing point (${point.x}, ${point.y}) in fission context\n`);
+ utils.sendMouseEvent(
+ "MozMouseHittest",
+ point.x,
+ point.y,
+ 0,
+ 0,
+ 0,
+ true,
+ 0,
+ 0,
+ true,
+ true
+ );
+
+ // Collect the new test data
+ let newParentTestData = utils.getCompositorAPZTestData();
+ let newIframeTestData = JSON.parse(
+ await FissionTestHelper.sendToOopif(
+ iframeElement,
+ `(${get_iframe_compositor_test_data})()`
+ )
+ );
+
+ // See which test data has new hit results
+ let hitResultCount = function (testData) {
+ return Object.keys(testData.hitResults).length;
+ };
+
+ let hitIframe =
+ hitResultCount(newIframeTestData) > hitResultCount(oldIframeTestData);
+ let hitParent =
+ hitResultCount(newParentTestData) > hitResultCount(oldParentTestData);
+
+ // Extract the results from the appropriate test data
+ let lastHitResult = function (testData) {
+ let lastHit =
+ testData.hitResults[Object.keys(testData.hitResults).length - 1];
+ return {
+ hitInfo: lastHit.hitResult,
+ scrollId: lastHit.scrollId,
+ layersId: lastHit.layersId,
+ };
+ };
+ if (hitIframe && hitParent) {
+ throw new Error(
+ "Both iframe and parent got hit-results, that is unexpected!"
+ );
+ } else if (hitIframe) {
+ return lastHitResult(newIframeTestData);
+ } else if (hitParent) {
+ return lastHitResult(newParentTestData);
+ } else {
+ throw new Error(
+ "Neither iframe nor parent got the hit-result, that is unexpected!"
+ );
+ }
+}
diff --git a/gfx/layers/apz/test/mochitest/helper_fixed_html_hittest.html b/gfx/layers/apz/test/mochitest/helper_fixed_html_hittest.html
new file mode 100644
index 0000000000..27add6debb
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_fixed_html_hittest.html
@@ -0,0 +1,61 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width">
+ <title>Hittest position:fixed zoomed scroll</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+ html {
+ position: fixed;
+ }
+ body {
+ margin: 0;
+ }
+ #fixed {
+ position: fixed;
+ top: 100px;
+ left: 100px;
+ }
+ </style>
+</head>
+<body>
+ <div id="fixed"><input type="button" value="Button" /></div>
+ <script>
+ async function test() {
+ // Create an offset between the visual and layout viewports.
+ // The offset is 50 CSS pixels = 100 screen pixels at 2x zoom
+ // in either direction.
+ let transformEndPromise = promiseTransformEnd();
+ await synthesizeNativeTouchDrag(document.body, 10, 10, -50, -50);
+ await transformEndPromise;
+
+ await promiseApzFlushedRepaints();
+
+ let clickPromise = new Promise(resolve => {
+ window.addEventListener("click", resolve);
+ });
+ let input = document.querySelector("input");
+ // Provide the input in window-relative coordinates,
+ // otherwise coordinatesRelativeToScreen() will run into the
+ // same bug as the hit-test, and the two bugs cancel out.
+ await synthesizeNativeMouseEventWithAPZ({
+ type: "click",
+ target: window,
+ // The visual viewport is already offset (50, 50) CSS pixels
+ // into the layout viewport. An additional (50, 50) CSS pixels
+ // gives us (100, 100), the offset of #fixed, in total.
+ offsetX: 55,
+ offsetY: 55
+ });
+ let e = await clickPromise;
+ is(e.target, input, "got click");
+ }
+
+ SpecialPowers.getDOMWindowUtils(window).setResolutionAndScaleTo(2.0);
+ waitUntilApzStable().then(test).then(subtestDone, subtestFailed);
+ </script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_fixed_pos_displayport.html b/gfx/layers/apz/test/mochitest/helper_fixed_pos_displayport.html
new file mode 100644
index 0000000000..adae691096
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_fixed_pos_displayport.html
@@ -0,0 +1,101 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; minimum-scale=1.0">
+ <title>position:fixed display port sizing</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+ html, body {
+ margin: 0;
+ /* This makes sure the `height: 1000%` on #scrolled actually has an effect. */
+ height: 100%;
+ }
+ #fixed {
+ position: fixed;
+ left: 0;
+ height: 100%;
+ width: 300px;
+ background: linear-gradient(135deg, white, black);
+ }
+ /* This makes sure we have a layout scroll range. */
+ #scrolled {
+ width: 300px;
+ height: 1000%;
+ }
+ </style>
+</head>
+<body>
+ <div id="fixed"></div>
+ <div id="scrolled"></div>
+ <script>
+ let utils = SpecialPowers.getDOMWindowUtils(window);
+ let vv = window.visualViewport;
+
+ // Get the displayport of the fixed-position element as of the last paint.
+ function getCurrentFixedPosDisplayport() {
+ let data = convertEntries(utils.getContentAPZTestData().additionalData);
+ let key = "fixedPosDisplayport";
+ ok(key in data, "should have computed a fixed-pos display port");
+ return parseRect(data[key]);
+ }
+
+ async function scrollToVisual(targetX, targetY) {
+ let scrollPromise = new Promise(resolve => {
+ vv.addEventListener("scroll", resolve, { once: true });
+ });
+ utils.scrollToVisual(targetX, targetY, utils.UPDATE_TYPE_MAIN_THREAD,
+ utils.SCROLL_MODE_INSTANT);
+ await scrollPromise;
+ await promiseApzFlushedRepaints();
+ // Allow up to 1 pixel discrepancy due to floating-point error.
+ isfuzzy(vv.pageLeft, targetX, 1, "visual-scrolled horizontally as expected");
+ isfuzzy(vv.pageTop, targetY, 1, "visual-scrolled vertically as expected");
+ }
+
+ // Check that the size and position of the fixed-pos displayport matches
+ // our expectations.
+ function checkFixedPosDisplayport() {
+ let fixedPosDisplayport = getCurrentFixedPosDisplayport();
+
+ // First, check check that we don't expand the displayport to the entire layout viewport
+ // even if we are zoomed in a lot.
+ ok(fixedPosDisplayport.width < window.innerWidth, "fixed-pos displayport is too wide");
+ ok(fixedPosDisplayport.height < window.innerHeight, "fixed-pos displayport is too tall");
+
+ // Now, check the position. We want it to track the visual scroll position
+ // relative to the layout viewport (but not relative to the page), since
+ // fixed-position elements are attached to the layout viewport.
+ // This is accomplished by checking the fixed-pos display port contains
+ // the visual viewport rect as expressed relative to the layout viewport.
+ let vvRect = { x: vv.offsetLeft, // offsets relative to layout viewport
+ y: vv.offsetTop,
+ width: vv.width,
+ height: vv.height };
+ assertRectContainment(fixedPosDisplayport, "fixed-pos displayport",
+ vvRect, "visual viewport");
+ }
+
+ async function test() {
+ // First, check size and position on page load.
+ checkFixedPosDisplayport();
+
+ // Scroll the visual viewport within the layout viewport, without
+ // scrolling the layout viewport itself, and check the size and
+ // position again.
+ await scrollToVisual(vv.width * 3, vv.height * 3);
+ checkFixedPosDisplayport();
+
+ // Finally, scroll the visual viewport farther so as to cause the
+ // layout viewport to scroll as well, and check the size and position
+ // once more.
+ await scrollToVisual(vv.width * 3, vv.height * 30);
+ checkFixedPosDisplayport();
+ }
+ SpecialPowers.getDOMWindowUtils(window).setResolutionAndScaleTo(8.0);
+ waitUntilApzStable().then(test).then(subtestDone, subtestFailed);
+ </script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_fixed_position_scroll_hittest.html b/gfx/layers/apz/test/mochitest/helper_fixed_position_scroll_hittest.html
new file mode 100644
index 0000000000..05cc2d0262
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_fixed_position_scroll_hittest.html
@@ -0,0 +1,51 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width">
+ <title>Hittest position:fixed zoomed scroll</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+ body {
+ margin: 0;
+ }
+ #fixed {
+ position: fixed;
+ height: 30px;
+ width: 100%;
+ background: linear-gradient(135deg, white, black);
+ }
+ #fixed > input {
+ position: absolute;
+ top: 0;
+ right: 0;
+ height: 100%;
+ }
+ </style>
+</head>
+<body>
+ <div id="fixed"><input type="button" value="Button" /></div>
+ <script>
+ async function test() {
+ let transformEndPromise = promiseTransformEnd();
+ await synthesizeNativeTouchDrag(document.body, 10, 10, -2000, 0);
+ await transformEndPromise;
+
+ await promiseApzFlushedRepaints();
+
+ let clickPromise = new Promise(resolve => {
+ window.addEventListener("click", resolve);
+ });
+ let input = document.querySelector("input");
+ await synthesizeNativeMouseEventWithAPZ({ type: "click", target: input, offsetX: 10, offsetY: 10 });
+ let e = await clickPromise;
+ is(e.target, input, "got click");
+ }
+
+ SpecialPowers.getDOMWindowUtils(window).setResolutionAndScaleTo(2.0);
+ waitUntilApzStable().then(test).then(subtestDone, subtestFailed);
+ </script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_fullscreen.html b/gfx/layers/apz/test/mochitest/helper_fullscreen.html
new file mode 100644
index 0000000000..32de4979f2
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_fullscreen.html
@@ -0,0 +1,53 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Tests that layout viewport is not larger than visual viewport on fullscreen</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+ body {
+ margin: 0;
+ padding: 0;
+ overflow-x: hidden;
+ }
+ </style>
+</head>
+<body>
+ <div style="background: blue; width: 100%; height: 100%;"></div>
+ <div style="background: red; width: 200%; height: 100px;">overflowed element</div>
+ <div id="target" style="background: green; width: 100px; height: 100px;"></div>
+ <script type="application/javascript">
+ const utils = SpecialPowers.getDOMWindowUtils(window);
+
+ function waitForFullscreenChange() {
+ return new Promise(resolve => {
+ document.addEventListener("fullscreenchange", resolve);
+ });
+ }
+
+ async function test(testDriver) {
+ target.requestFullscreen();
+
+ await waitForFullscreenChange();
+
+ is(document.fullscreenElement, target,
+ "The target element should have been fullscreen-ed");
+
+ // Try to move rightward, but it should NOT happen.
+ utils.scrollToVisual(200, 0, utils.UPDATE_TYPE_MAIN_THREAD,
+ utils.SCROLL_MODE_INSTANT);
+
+ await waitUntilApzStable();
+
+ is(visualViewport.offsetLeft, 0,
+ "The visual viewport offset should never be moved");
+
+ document.exitFullscreen();
+ }
+
+ waitUntilApzStable().then(test).then(subtestDone, subtestFailed);
+ </script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_backface_hidden.html b/gfx/layers/apz/test/mochitest/helper_hittest_backface_hidden.html
new file mode 100644
index 0000000000..0e84282e54
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_hittest_backface_hidden.html
@@ -0,0 +1,67 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>APZ hit-testing with backface-visibility:hidden</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <meta name="viewport" content="width=device-width"/>
+ <style>
+ body,html{
+ height: 100%;
+ }
+ body{
+ margin: 0;
+ transform-style: preserve-3d;
+ }
+ #back, #front{
+ backface-visibility: hidden;
+ position: absolute;
+ width: 100%;
+ height: 100%
+ }
+ #front{
+ overflow-y:auto;
+ }
+ #content{
+ width: 100%;
+ height: 200%;
+ background: linear-gradient(blue, green);
+ }
+ #back{
+ transform: rotateY(180deg);
+ }
+ </style>
+</head>
+<body>
+ <div id="front">
+ <div id="content"></div>
+ </div>
+ <div id="back"></div></body>
+<script type="application/javascript">
+
+async function test() {
+ var config = getHitTestConfig();
+
+ var subframe = document.getElementById("front");
+
+ // Set a displayport to ensure the subframe is layerized.
+ // This is not required for exercising the behavior we want to test,
+ // but it's needed to be able to assert the results reliably.
+ config.utils.setDisplayPortForElement(0, 0, 1000, 1000, subframe, 1);
+ await promiseApzFlushedRepaints();
+
+ var subframeViewId = config.utils.getViewId(subframe);
+
+ var {scrollId} = hitTest(centerOf(subframe));
+
+ is(scrollId, subframeViewId,
+ "hit the scroll frame behind the backface-visibility:hidden element");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+</script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_basic.html b/gfx/layers/apz/test/mochitest/helper_hittest_basic.html
new file mode 100644
index 0000000000..a9e9f0c07f
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_hittest_basic.html
@@ -0,0 +1,141 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Various tests to exercise the APZ hit-testing codepaths</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <meta name="viewport" content="width=device-width"/>
+</head>
+<body>
+ <div id="scroller" style="width: 300px; height: 300px; overflow:scroll; margin-top: 100px; margin-left: 50px">
+ <div id="contents" style="width: 500px; height: 500px; background-image: linear-gradient(blue,red)">
+ <div id="apzaware" style="position: relative; width: 100px; height: 100px; top: 300px; background-color: red" onwheel="return false;"></div>
+ </div>
+ </div>
+ <div id="make_root_scrollable" style="height: 5000px"></div>
+</body>
+<script type="application/javascript">
+
+async function test() {
+ var config = getHitTestConfig();
+ var utils = config.utils;
+
+ var scroller = document.getElementById("scroller");
+ var apzaware = document.getElementById("apzaware");
+
+ let expectedHitInfo = APZHitResultFlags.VISIBLE;
+ if (!config.activateAllScrollFrames) {
+ expectedHitInfo |= APZHitResultFlags.INACTIVE_SCROLLFRAME;
+ }
+ checkHitResult(hitTest(centerOf(scroller)),
+ expectedHitInfo,
+ (config.activateAllScrollFrames ? utils.getViewId(scroller)
+ : utils.getViewId(document.scrollingElement)),
+ utils.getLayersId(),
+ "inactive scrollframe");
+
+ // The apz-aware div (which has a non-passive wheel listener) is not visible
+ // and so the hit-test should just return the root scrollframe area that's
+ // covering it
+ checkHitResult(hitTest(centerOf(apzaware)),
+ APZHitResultFlags.VISIBLE,
+ utils.getViewId(document.scrollingElement),
+ utils.getLayersId(),
+ "inactive scrollframe - apzaware block");
+
+ // Hit test where the scroll thumbs should be.
+ hitTestScrollbar({
+ element: scroller,
+ directions: { vertical: true, horizontal: true },
+ expectedScrollId: utils.getViewId(document.scrollingElement),
+ expectedLayersId: utils.getLayersId(),
+ trackLocation: ScrollbarTrackLocation.START,
+ expectThumb: true,
+ layerState: LayerState.INACTIVE,
+ });
+
+ // activate the scrollframe but keep the main-thread scroll position at 0.
+ // also apply a async scroll offset in the y-direction such that the
+ // scrollframe scrolls to the bottom of its range.
+ utils.setDisplayPortForElement(0, 0, 500, 500, scroller, 1);
+ await promiseApzFlushedRepaints();
+ var scrollY = scroller.scrollTopMax;
+ utils.setAsyncScrollOffset(scroller, 0, scrollY);
+ // Tick the refresh driver once to make sure the compositor has applied the
+ // async scroll offset (for WebRender hit-testing we need to make sure WR has
+ // the latest info).
+ utils.advanceTimeAndRefresh(16);
+ utils.restoreNormalRefresh();
+
+ var scrollerViewId = utils.getViewId(scroller);
+
+ // Now we again test the middle of the scrollframe, which is now active
+ checkHitResult(hitTest(centerOf(scroller)),
+ APZHitResultFlags.VISIBLE,
+ scrollerViewId,
+ utils.getLayersId(),
+ "active scrollframe");
+
+ // Test the apz-aware block
+ var apzawarePosition = centerOf(apzaware); // main thread position
+ apzawarePosition.y -= scrollY; // APZ position
+ checkHitResult(hitTest(apzawarePosition),
+ APZHitResultFlags.VISIBLE |
+ APZHitResultFlags.APZ_AWARE_LISTENERS,
+ scrollerViewId,
+ utils.getLayersId(),
+ "active scrollframe - apzaware block");
+
+ // Test the scrollbars. Note that this time the vertical scrollthumb is
+ // going to be at the bottom of the track. We'll test both the top and the
+ // bottom.
+
+ // top of scrollbar track
+ hitTestScrollbar({
+ element: scroller,
+ directions: { vertical: true },
+ expectedScrollId: scrollerViewId,
+ expectedLayersId: utils.getLayersId(),
+ trackLocation: ScrollbarTrackLocation.START,
+ expectThumb: false,
+ layerState: LayerState.ACTIVE,
+ });
+ // bottom of scrollbar track (scrollthumb)
+ hitTestScrollbar({
+ element: scroller,
+ directions: { vertical: true },
+ expectedScrollId: scrollerViewId,
+ expectedLayersId: utils.getLayersId(),
+ trackLocation: ScrollbarTrackLocation.END,
+ expectThumb: true,
+ layerState: LayerState.ACTIVE,
+ });
+ // left part of scrollbar track (has scrollthumb)
+ hitTestScrollbar({
+ element: scroller,
+ directions: { horizontal: true },
+ expectedScrollId: scrollerViewId,
+ expectedLayersId: utils.getLayersId(),
+ trackLocation: ScrollbarTrackLocation.START,
+ expectThumb: true,
+ layerState: LayerState.ACTIVE,
+ });
+ // right part of scrollbar track
+ hitTestScrollbar({
+ element: scroller,
+ directions: { horizontal: true },
+ expectedScrollId: scrollerViewId,
+ expectedLayersId: utils.getLayersId(),
+ trackLocation: ScrollbarTrackLocation.END,
+ expectThumb: false,
+ layerState: LayerState.ACTIVE,
+ });
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+</script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_bug1119497.html b/gfx/layers/apz/test/mochitest/helper_hittest_bug1119497.html
new file mode 100644
index 0000000000..1d1e0a922f
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_hittest_bug1119497.html
@@ -0,0 +1,54 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>A hit testing test for the scenario in bug 1119497</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <meta name="viewport" content="width=device-width"/>
+ <style>
+ #scrollbox {
+ width: 200px;
+ height: 200px;
+ overflow: auto;
+ }
+ #scrolled {
+ width: 400px;
+ height: 400px;
+ }
+ #cover {
+ width: 300px;
+ height: 300px;
+ position: fixed;
+ left: 0px;
+ top: 0px;
+ }
+ </style>
+</head>
+<body>
+ <div id="scrollbox">
+ <div id="scrolled"></div>
+ </div>
+ <div id="cover"></div>
+</body>
+<script type="application/javascript">
+
+async function test() {
+ var config = getHitTestConfig();
+ var utils = config.utils;
+
+ // Check that hit-testing on an element that's not scrolled by
+ // anything ("cover") hits the root scroller.
+ checkHitResult(hitTest({x: 50, y: 50}),
+ APZHitResultFlags.VISIBLE,
+ utils.getViewId(document.scrollingElement),
+ utils.getLayersId(),
+ "root scroller");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+</script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_bug1257288.html b/gfx/layers/apz/test/mochitest/helper_hittest_bug1257288.html
new file mode 100644
index 0000000000..d9b813dddf
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_hittest_bug1257288.html
@@ -0,0 +1,74 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>A hit testing test for the scenario in bug 1257288</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <meta name="viewport" content="width=device-width"/>
+ <style>
+ html {
+ background: radial-gradient(circle at 80px 450px, blue 5px, transparent 0) gray no-repeat;
+ height: 100%;
+ }
+
+ #scrollbox {
+ border: 1px solid black;
+ width: 400px;
+ height: 400px;
+ margin: 20px;
+ overflow: auto;
+ background-color: white;
+ }
+
+ #scrolled {
+ padding-top: 300px;
+ padding-bottom: 300px;
+ }
+
+ #clip {
+ overflow: hidden;
+ margin: 10px;
+ }
+
+ #transform {
+ background-color: red;
+ width: 200px;
+ height: 200px;
+ will-change: transform;
+ transform: rotate(45deg);
+ position: relative;
+ left: -100px;
+ }
+ </style>
+</head>
+<body>
+ <div id="scrollbox">
+ <div id="scrolled">
+ <div id="clip">
+ <div id="transform"></div>
+ </div>
+ </div>
+ </div>
+</body>
+<script type="application/javascript">
+
+async function test() {
+ var config = getHitTestConfig();
+ var utils = config.utils;
+
+ // Check that hit-testing on the blue circle (located at (80, 450))
+ // hits the root, not the subframe.
+ checkHitResult(hitTest({x: 80, y: 450}),
+ APZHitResultFlags.VISIBLE,
+ utils.getViewId(document.scrollingElement),
+ utils.getLayersId(),
+ "root scroller should be hit, not subframe");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+</script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_bug1715187.html b/gfx/layers/apz/test/mochitest/helper_hittest_bug1715187.html
new file mode 100644
index 0000000000..6cb58b6413
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_hittest_bug1715187.html
@@ -0,0 +1,69 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=2100"/>
+ <title>Check hittesting fission oop iframe with transform and pinch zoom works bug 1715187</title>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script>
+
+async function test() {
+ let initial_resolution = await getResolution();
+ ok(initial_resolution > 0,
+ "The initial_resolution is " + initial_resolution + ", which is some sane value");
+
+ // Zoom in
+ SpecialPowers.getDOMWindowUtils(window).setResolutionAndScaleTo(2.0*initial_resolution);
+ await promiseApzFlushedRepaints();
+
+ let resolution = await getResolution();
+ ok(resolution > 1.5*initial_resolution,
+ "The resolution is " + resolution + ", after zooming in");
+
+
+ let clickPromise = new Promise(resolve => {
+ window.addEventListener("message", event => {
+ if (event.data == "gotclick") {
+ ok(true, "got click");
+ resolve();
+ }
+ })
+ });
+
+
+ let thetarget = document.getElementById("theiframe");
+ await synthesizeNativeMouseEventWithAPZ({ type: "click", target: thetarget, offsetX: 5, offsetY: 5 });
+ info("sent click");
+
+ await clickPromise;
+
+ ok(true, "must have got click");
+
+ // Restore
+ SpecialPowers.getDOMWindowUtils(window).setResolutionAndScaleTo(initial_resolution);
+ await promiseApzFlushedRepaints();
+
+ resolution = await getResolution();
+ ok(resolution == initial_resolution,
+ "The resolution is " + resolution + ", after restoring");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+<style>
+body {
+ padding-left: 200px;
+}
+</style>
+</head>
+<body>
+<div style="position: absolute; left: 350px; width: 400px; height: 400px; transform: scale(2,1)">
+ <iframe id="theiframe" style="border: 1px;" frameborder="1" src="http://example.org/tests/gfx/layers/apz/test/mochitest/helper_hittest_bug1715187_oopif.html"></iframe>
+</div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_bug1715187_oopif.html b/gfx/layers/apz/test/mochitest/helper_hittest_bug1715187_oopif.html
new file mode 100644
index 0000000000..c0f5baf467
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_hittest_bug1715187_oopif.html
@@ -0,0 +1,13 @@
+<style>
+body, html { margin: 0; }
+</style>
+<div id="thediv" style="position: absolute; top: 0; left: 0; width: 10px; height: 10px; background: blue;">
+</div>
+<div style="width:10px; height: 500vh;"></div>
+<script>
+let target = document.getElementById("thediv");
+target.addEventListener("click", listener);
+function listener() {
+ parent.postMessage("gotclick", "*");
+}
+</script>
diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_bug1715369.html b/gfx/layers/apz/test/mochitest/helper_hittest_bug1715369.html
new file mode 100644
index 0000000000..d1128fa946
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_hittest_bug1715369.html
@@ -0,0 +1,74 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=2100"/>
+ <title>Check hittesting fission oop iframe with transform works bug 1715369</title>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script>
+
+async function makeActive(x, y, targetId) {
+ let theTarget = document.getElementById(targetId);
+ await promiseNativeMouseEventWithAPZAndWaitForEvent({
+ type: "click",
+ target: theTarget,
+ offsetX: x,
+ offsetY: y,
+ });
+
+ await promiseApzFlushedRepaints();
+
+ ok(isLayerized(targetId), "target should be layerized at this point");
+ let utils = SpecialPowers.getDOMWindowUtils(window);
+ let targetScrollId = utils.getViewId(theTarget);
+ ok(targetScrollId > 0, "target should have a scroll id");
+}
+
+async function test() {
+ await makeActive(20, 20, "scrollable");
+
+ let clickPromise = new Promise(resolve => {
+ window.addEventListener("message", event => {
+ if (event.data == "gotclick") {
+ ok(true, "got click");
+ resolve();
+ }
+ })
+ });
+
+
+ let thetarget = document.getElementById("theiframe");
+ await synthesizeNativeMouseEventWithAPZ({ type: "click", target: thetarget, offsetX: 25, offsetY: 25 });
+ info("sent click");
+
+ await clickPromise;
+
+ ok(true, "must have got click");
+
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+<style>
+</style>
+</head>
+<body>
+
+<!--transform
+asr change
+in process iframe/stackingcontext
+oopif-->
+<div style="height: 100px; width: 100px; transform: scale(3); transform-origin: top left;">
+ <div id="scrollable" style="overflow: scroll; height: 200px;">
+ <div id="topspacer" style="height: 50px;"></div>
+ <iframe id="theiframe" style="border: 1px;" frameborder="1" src="helper_hittest_bug1715369_iframe.html"></iframe>
+ <div style="height: 200vh;"></div>
+ </div>
+</div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_bug1715369_iframe.html b/gfx/layers/apz/test/mochitest/helper_hittest_bug1715369_iframe.html
new file mode 100644
index 0000000000..034eb0429f
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_hittest_bug1715369_iframe.html
@@ -0,0 +1,13 @@
+<style>
+body, html { margin: 0; }
+</style>
+<script>
+ window.addEventListener("message", event => {
+ if (event.data == "gotclick") {
+ // forward to parent
+ parent.postMessage("gotclick", "*");
+ }
+ });
+</script>
+<iframe style="border: 1px;" frameborder="1" src="http://example.org/tests/gfx/layers/apz/test/mochitest/helper_hittest_bug1715369_oopif.html"></iframe>
+<div style="width:10px; height: 500vh;"></div>
diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_bug1715369_oopif.html b/gfx/layers/apz/test/mochitest/helper_hittest_bug1715369_oopif.html
new file mode 100644
index 0000000000..1a6582ce60
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_hittest_bug1715369_oopif.html
@@ -0,0 +1,13 @@
+<style>
+body, html { margin: 0; }
+</style>
+<div id="thediv" style="position: absolute; top: 0; left: 0; width: 40px; height: 40px; background: blue;">
+</div>
+<div style="width:10px; height: 500vh;"></div>
+<script>
+let target = document.getElementById("thediv");
+target.addEventListener("click", listener);
+function listener() {
+ parent.postMessage("gotclick", "*");
+}
+</script>
diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_bug1730606-1.html b/gfx/layers/apz/test/mochitest/helper_hittest_bug1730606-1.html
new file mode 100644
index 0000000000..b84cb6b634
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_hittest_bug1730606-1.html
@@ -0,0 +1,124 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>A simple hit testing test that doesn't involve any transforms</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <meta name="viewport" content="width=device-width"/>
+ <style>
+ body, html {
+ margin: 0;
+ padding: 0;
+ width: 100%;
+ height: 100%;
+ }
+ .scrollable-content {
+ width: 200%;
+ height: 200%;
+ }
+ #layer1 {
+ position: absolute;
+ width: 100vw;
+ height: 100vh;
+ background: blue;
+ }
+ #layer2 {
+ position: absolute;
+ top: 100px;
+ left: 100px;
+ width: 100px;
+ height: 100px;
+ background: red;
+ }
+ #layer3 {
+ position: absolute;
+ top: 100px;
+ left: 100px;
+ width: 100px;
+ height: 100px;
+ background: green;
+ overflow: hidden;
+ }
+ #layer4 {
+ position: absolute;
+ top: 50px;
+ left: 50px;
+ width: 100px;
+ height: 100px;
+ background: yellow;
+ overflow: hidden;
+ }
+ </style>
+</head>
+<body>
+ <div id="layer1"></div>
+ <div id="layer2"></div>
+ <div id="layer3">
+ <div class="scrollable-content"></div>
+ </div>
+ <div id="layer4">
+ <div class="scrollable-content"></div>
+ </div>
+ <div class="scrollable-content"></div>
+</body>
+<script type="application/javascript">
+
+async function test() {
+ var config = getHitTestConfig();
+ var utils = config.utils;
+
+ let subframeHitInfo = APZHitResultFlags.VISIBLE;
+ if (!config.activateAllScrollFrames) {
+ subframeHitInfo |= APZHitResultFlags.INACTIVE_SCROLLFRAME;
+ }
+
+ // Initially, the only thing scrollable is the root.
+ checkHitResult(hitTest({x: 175, y: 175}),
+ APZHitResultFlags.VISIBLE,
+ utils.getViewId(document.scrollingElement),
+ utils.getLayersId(),
+ "root scroller");
+
+ // Make layer3 scrollable.
+ layer3.style.overflow = "auto";
+ await promiseApzFlushedRepaints();
+ checkHitResult(hitTest({x: 175, y: 175}),
+ subframeHitInfo,
+ (config.activateAllScrollFrames ? utils.getViewId(layer3)
+ : utils.getViewId(document.scrollingElement)),
+ utils.getLayersId(),
+ "subframe layer3");
+
+ // At (125, 125), layer4 obscures layer3. layer4 is not scrollable yet,
+ // so we hit the root.
+ checkHitResult(hitTest({x: 125, y: 125}),
+ APZHitResultFlags.VISIBLE,
+ utils.getViewId(document.scrollingElement),
+ utils.getLayersId(),
+ "root scroller");
+
+ // Make layer4 scrollable as well. Now (125, 125) should hit it.
+ layer4.style.overflow = "auto";
+ await promiseApzFlushedRepaints();
+ checkHitResult(hitTest({x: 125, y: 125}),
+ subframeHitInfo,
+ (config.activateAllScrollFrames ? utils.getViewId(layer4)
+ : utils.getViewId(document.scrollingElement)),
+ utils.getLayersId(),
+ "subframe layer4");
+
+ // Hit-test outside the reach of layer[3,4] but inside root.
+ checkHitResult(hitTest({x: 225, y: 225}),
+ APZHitResultFlags.VISIBLE,
+ utils.getViewId(document.scrollingElement),
+ utils.getLayersId(),
+ "root scroller");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+</script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_bug1730606-2.html b/gfx/layers/apz/test/mochitest/helper_hittest_bug1730606-2.html
new file mode 100644
index 0000000000..8cd3388534
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_hittest_bug1730606-2.html
@@ -0,0 +1,157 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>A more involved hit testing test that involves css and async transforms</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <meta name="viewport" content="width=device-width"/>
+ <style>
+ body, html {
+ margin: 0;
+ padding: 0;
+ width: 100%;
+ height: 100%;
+ }
+ .scrollable-content {
+ width: 200%;
+ height: 200%;
+ }
+ #layer1 {
+ position: absolute;
+ top: 20px;
+ left: 20px;
+ width: 100px;
+ height: 100px;
+ background: red;
+ overflow: auto;
+ }
+ #layer2 {
+ position: absolute;
+ top: 140px;
+ left: 40px;
+ width: 100px;
+ height: 100px;
+ background: green;
+ transform: scale(2.0, 1.0);
+ transform-origin: 0 0;
+ }
+ #layer3 {
+ position: absolute;
+ top: 0px;
+ left: 0px;
+ width: 100px;
+ height: 100px;
+ background: yellow;
+ overflow: auto;
+ }
+ </style>
+</head>
+<body>
+ <div id="layer1">
+ <div class="scrollable-content"></div>
+ </div>
+ <div id="layer2">
+ <div id="layer3">
+ <div class="scrollable-content"></div>
+ </div>
+ </div>
+ <div class="scrollable-content"></div>
+</body>
+<script type="application/javascript">
+
+async function test() {
+ var config = getHitTestConfig();
+ var utils = config.utils;
+
+ let subframeHitInfo = APZHitResultFlags.VISIBLE;
+ if (!config.activateAllScrollFrames) {
+ subframeHitInfo |= APZHitResultFlags.INACTIVE_SCROLLFRAME;
+ }
+
+ // Hit an area that's clearly on the root and not any of the child layers.
+ checkHitResult(hitTest({x: 150, y: 70}),
+ APZHitResultFlags.VISIBLE,
+ utils.getViewId(document.scrollingElement),
+ utils.getLayersId(),
+ "root scroller");
+
+ // Hit an area on the root that would be on layer3 if layer2 weren't transformed.
+ checkHitResult(hitTest({x: 30, y: 190}),
+ APZHitResultFlags.VISIBLE,
+ utils.getViewId(document.scrollingElement),
+ utils.getLayersId(),
+ "root scroller (area revealed by transform)");
+
+ // Hit an area on layer1.
+ checkHitResult(hitTest({x: 70, y: 70}),
+ subframeHitInfo,
+ (config.activateAllScrollFrames ? utils.getViewId(layer1)
+ : utils.getViewId(document.scrollingElement)),
+ utils.getLayersId(),
+ "layer1");
+
+ // Hit an area on layer3.
+ checkHitResult(hitTest({x: 60, y: 190}),
+ subframeHitInfo,
+ (config.activateAllScrollFrames ? utils.getViewId(layer3)
+ : utils.getViewId(document.scrollingElement)),
+ utils.getLayersId(),
+ "layer3");
+
+ // Hit an area on layer3 that would be on the root if layer2 weren't transformed.
+ checkHitResult(hitTest({x: 150, y: 190}),
+ subframeHitInfo,
+ (config.activateAllScrollFrames ? utils.getViewId(layer3)
+ : utils.getViewId(document.scrollingElement)),
+ utils.getLayersId(),
+ "layer3");
+
+ // Scroll the root upwards by 120 pixels. This scrolls layer1 out of view.
+ utils.setAsyncScrollOffset(document.scrollingElement, 0, 120);
+ // Give WebRender a chance to sample the test async scroll offset.
+ utils.advanceTimeAndRefresh(16);
+ utils.restoreNormalRefresh();
+
+ // Hit where layers3 used to be. It should now hit the root.
+ checkHitResult(hitTest({x: 60, y: 190}),
+ APZHitResultFlags.VISIBLE,
+ utils.getViewId(document.scrollingElement),
+ utils.getLayersId(),
+ "root scroller (after async scroll)");
+
+ // Hit where layers1 used to be and where layers3 should now be.
+ checkHitResult(hitTest({x: 70, y: 70}),
+ subframeHitInfo,
+ (config.activateAllScrollFrames ? utils.getViewId(layer3)
+ : utils.getViewId(document.scrollingElement)),
+ utils.getLayersId(),
+ "layer3 (after async scroll)");
+
+ // Scroll the root upwards by an additional 120 pixels.
+ utils.setAsyncScrollOffset(document.scrollingElement, 0, 240);
+ // Give WebRender a chance to sample the test async scroll offset.
+ utils.advanceTimeAndRefresh(16);
+ utils.restoreNormalRefresh();
+
+ // Hit where layers3 used to be. It should now hit the root.
+ checkHitResult(hitTest({x: 60, y: 190}),
+ APZHitResultFlags.VISIBLE,
+ utils.getViewId(document.scrollingElement),
+ utils.getLayersId(),
+ "root scroller (after second async scroll)");
+
+ // Hit where layers2 used to be. It should now hit the root.
+ checkHitResult(hitTest({x: 70, y: 70}),
+ APZHitResultFlags.VISIBLE,
+ utils.getViewId(document.scrollingElement),
+ utils.getLayersId(),
+ "root scroller (after second async scroll)");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+</script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_bug1730606-3.html b/gfx/layers/apz/test/mochitest/helper_hittest_bug1730606-3.html
new file mode 100644
index 0000000000..0ae01864a5
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_hittest_bug1730606-3.html
@@ -0,0 +1,56 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>A hit testing test involving a scenario with a scale transform</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <meta name="viewport" content="width=device-width"/>
+ <style>
+ body, html {
+ margin: 0;
+ padding: 0;
+ width: 100%;
+ height: 100%;
+ }
+ #layer1 {
+ position: absolute;
+ top: 0px;
+ left: 0px;
+ width: 100px;
+ height: 100px;
+ background: red;
+ overflow: scroll;
+ transform: scale(2.0);
+ transform-origin: 0 0;
+ }
+ </style>
+</head>
+<body>
+ <!-- Neither the root nor layer1 actually have room to scroll -->
+ <div id="layer1"></div>
+</body>
+<script type="application/javascript">
+
+async function test() {
+ var config = getHitTestConfig();
+ var utils = config.utils;
+
+ // Force an APZC for layer1 in spite of it having no scroll range.
+ utils.setDisplayPortForElement(0, 0, 100, 100, layer1, 1);
+ await promiseApzFlushedRepaints();
+
+ // Hit an area on layer1 that would be on the root if layer1 weren't transformed.
+ checkHitResult(hitTest({x: 150, y: 150}),
+ APZHitResultFlags.VISIBLE,
+ utils.getViewId(layer1),
+ utils.getLayersId(),
+ "layer1");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+</script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_bug1730606-4.html b/gfx/layers/apz/test/mochitest/helper_hittest_bug1730606-4.html
new file mode 100644
index 0000000000..26ec487b3e
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_hittest_bug1730606-4.html
@@ -0,0 +1,194 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>A hit testing test involving a scenario with a scale transform</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <meta name="viewport" content="width=device-width"/>
+ <style>
+ /*
+ * This test tries to approximate the layer structure of
+ * APZHitTestingTester.ComplexMultiLayerTree from TestHitTesting.cpp.
+ *
+ * Notes:
+ * - The elements are named after the layers in the testcase
+ * (e.g. "layer1"), but where multiple elements share an APZC,
+ * new elements named "scroller1" etc. are introduced to be
+ * the scroll containers.
+ * - overflow: hidden is used to avoid spurious scrollbar layers.
+ * To trigger APZC creation, the test code explicitly sets display
+ * port margins.
+ * - Perspective transforms are used to force an element to be
+ * layerized if it otherwise wouldn't.
+ * - One difference is that the entire contents of the APZC that
+ * scrolls layer{4,6,8} is shifted to the right by 100px.
+ * Otherwise, the dimensions of the layers make it such that
+ * this APZC's composition bounds covers up layers{1,2,3}
+ * and those cannot be hit.
+ */
+ body, html {
+ margin: 0;
+ padding: 0;
+ width: 100%;
+ height: 100%;
+ }
+ #scroller1 {
+ position: absolute;
+ left: 0;
+ top: 0;
+ width: 250px;
+ height: 350px;
+ overflow: hidden;
+ }
+ #layer1 {
+ position: absolute;
+ left: 0px;
+ top: 0px;
+ width: 100px;
+ height: 100px;
+ background: red;
+ }
+ #layer2 {
+ position: absolute;
+ left: 50px;
+ top: 50px;
+ width: 200px;
+ height: 300px;
+ background: yellow;
+ transform: scale(1.0);
+ perspective: 10px;
+ }
+ #layer3 {
+ position: absolute;
+ left: 10px;
+ top: 10px;
+ width: 10px;
+ height: 10px;
+ background: green;
+ transform: scale(1.0);
+ perspective: 10px;
+ }
+ #scroller2 {
+ position: absolute;
+ left: 100px;
+ top: 0px;
+ width: 300px;
+ height: 400px;
+ overflow: hidden;
+ }
+ #layer4 {
+ position: absolute;
+ left: 0px;
+ top: 200px;
+ width: 100px;
+ height: 100px;
+ background: purple;
+ }
+ #layer5 {
+ position: absolute;
+ left: 200px;
+ top: 0px;
+ width: 100px;
+ height: 400px;
+ background: pink;
+ transform: scale(1.0);
+ perspective: 10px;
+ }
+ #layer6 {
+ position: absolute;
+ left: 0px;
+ top: 0px;
+ width: 100px;
+ height: 200px;
+ background: orange;
+ }
+ #layer7 {
+ position: absolute;
+ left: 0px;
+ top: 0px;
+ width: 100px;
+ height: 200px;
+ background: navy;
+ overflow: hidden;
+ }
+ #layer8 {
+ position: absolute;
+ left: 0px;
+ top: 200px;
+ width: 100px;
+ height: 100px;
+ background: lightgreen;
+ transform: scale(1.0);
+ perspective: 10px;
+ }
+ #layer9 {
+ position: absolute;
+ left: 0px;
+ top: 300px;
+ width: 100px;
+ height: 100px;
+ background: turquoise;
+ overflow: hidden;
+ }
+ </style>
+</head>
+<body>
+ <div id="scroller1">
+ <div id="layer1"></div>
+ <div id="layer2">
+ <div id="layer3"></div>
+ </div>
+ </div>
+ <div id="scroller2">
+ <div id="layer4"></div>
+ <div id="layer5">
+ <div id="layer6">
+ <div id="layer7"></div>
+ </div>
+ <div id="layer8"></div>
+ <div id="layer9"></div>
+ </div>
+ </div>
+</body>
+<script type="application/javascript">
+
+async function test() {
+ var config = getHitTestConfig();
+ var utils = config.utils;
+
+ // Trigger APZC creation.
+ utils.setDisplayPortForElement(0, 0, 250, 350, scroller1, 1);
+ await promiseApzFlushedRepaints();
+ utils.setDisplayPortForElement(0, 0, 300, 400, scroller2, 1);
+ await promiseApzFlushedRepaints();
+ utils.setDisplayPortForElement(0, 0, 100, 200, layer7, 1);
+ await promiseApzFlushedRepaints();
+ utils.setDisplayPortForElement(0, 0, 100, 200, layer9, 1);
+ await promiseApzFlushedRepaints();
+
+ checkHitResult(hitTest({x: 25, y: 25}),
+ APZHitResultFlags.VISIBLE,
+ utils.getViewId(scroller1),
+ utils.getLayersId(),
+ "scroller1");
+
+ checkHitResult(hitTest({x: 350, y: 100}),
+ APZHitResultFlags.VISIBLE,
+ utils.getViewId(layer7),
+ utils.getLayersId(),
+ "layer7");
+
+ checkHitResult(hitTest({x: 375, y: 375}),
+ APZHitResultFlags.VISIBLE,
+ utils.getViewId(layer9),
+ utils.getLayersId(),
+ "layer7");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+</script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_checkerboard.html b/gfx/layers/apz/test/mochitest/helper_hittest_checkerboard.html
new file mode 100644
index 0000000000..eb2d583276
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_hittest_checkerboard.html
@@ -0,0 +1,57 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>APZ hit-testing over a checkerboarded area</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <meta name="viewport" content="width=device-width"/>
+</head>
+<body>
+ <div id="scroller" style="width: 300px; height: 300px; overflow:scroll; margin-top: 100px; margin-left: 50px">
+ <!-- Make the contents tall enough to be sure we can checkerboard -->
+ <div id="contents" style="width: 100%; height: 5000px; background-image: linear-gradient(blue,red)">
+ </div>
+ </div>
+ <div id="make_root_scrollable" style="height: 5000px"></div>
+</body>
+<script type="application/javascript">
+
+async function test() {
+ var config = getHitTestConfig();
+ var utils = config.utils;
+
+ var scroller = document.getElementById("scroller");
+
+ // Activate the scrollframe but keep the main-thread scroll position at 0.
+ // Also apply an async scroll offset in the y-direction such that the
+ // scrollframe scrolls all the way to the bottom of its range, where it's
+ // sure to checkerboard.
+ utils.setDisplayPortForElement(0, 0, 300, 1000, scroller, 1);
+ await promiseApzFlushedRepaints();
+ var scrollY = scroller.scrollTopMax;
+ utils.setAsyncScrollOffset(scroller, 0, scrollY);
+ // Tick the refresh driver once to make sure the compositor has applied the
+ // async scroll offset (for WebRender hit-testing we need to make sure WR has
+ // the latest info).
+ utils.advanceTimeAndRefresh(16);
+ utils.restoreNormalRefresh();
+
+ var scrollerViewId = utils.getViewId(scroller);
+
+ // Hit-test the middle of the scrollframe, which is now inside the
+ // checkerboarded region, and check that we hit the scrollframe and
+ // not its parent.
+ checkHitResult(hitTest(centerOf(scroller)),
+ APZHitResultFlags.VISIBLE,
+ scrollerViewId,
+ utils.getLayersId(),
+ "active scrollframe");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+</script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_clippath.html b/gfx/layers/apz/test/mochitest/helper_hittest_clippath.html
new file mode 100644
index 0000000000..0f8cfca519
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_hittest_clippath.html
@@ -0,0 +1,118 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Hit-testing an iframe covered by an element with a clip-path</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/paint_listener.js"></script>
+ <meta name="viewport" content="width=device-width"/>
+ <meta http-equiv="content-type" content="text/html; charset=UTF-8">
+<style>
+ html, body { margin: 0; }
+ #clipped {
+ width: 400px;
+ height: 400px;
+ background-color: green;
+ position: absolute;
+ top: 100px;
+ left: 100px;
+ clip-path: circle(150px);
+ }
+ iframe {
+ width: 400px;
+ height: 300px;
+ border: 0px solid black;
+ }
+</style>
+</head>
+<body style="height: 5000px">
+<iframe id="sub" srcdoc="<!DOCTYPE html><body style='height: 5000px'><div style='position: absolute; top: 150px; left: 150px; width: 300px; height: 300px; background-color: blue;'></div>
+when this page loads, the blue rect should be behind the green circle. mousing over the area with the blue rect and scrolling with the wheel or trackpad should cause the iframe to scroll."></iframe>
+<div id="clipped"></div>
+<script>
+
+async function test() {
+ var config = getHitTestConfig();
+ var utils = config.utils;
+
+ // layerize the iframe
+ var subwindow = document.getElementById("sub").contentWindow;
+ var subscroller = subwindow.document.scrollingElement;
+ var subutils = SpecialPowers.getDOMWindowUtils(subwindow);
+ subutils.setDisplayPortForElement(0, 0, 400, 1000, subscroller, 1);
+ await promiseApzFlushedRepaints();
+
+ var rootViewId = utils.getViewId(document.scrollingElement);
+ var iframeViewId = subutils.getViewId(subscroller);
+ var layersId = utils.getLayersId();
+ is(subutils.getLayersId(), layersId, "iframe is not OOP");
+
+ checkHitResult(hitTest({ x: 10, y: 10 }),
+ APZHitResultFlags.VISIBLE,
+ iframeViewId,
+ layersId,
+ `(simple) uninteresting point inside the iframe`);
+ checkHitResult(hitTest({ x: 500, y: 10 }),
+ APZHitResultFlags.VISIBLE,
+ rootViewId,
+ layersId,
+ `(simple) uninteresting point in the root scroller`);
+ checkHitResult(hitTest({ x: 110, y: 110 }),
+ APZHitResultFlags.VISIBLE,
+ iframeViewId,
+ layersId,
+ `(simple) point in the iframe behind overlaying div, but outside the bounding box of the clip path`);
+ checkHitResult(hitTest({ x: 160, y: 160 }),
+ APZHitResultFlags.VISIBLE,
+ iframeViewId,
+ layersId,
+ `(simple) point in the iframe behind overlaying div, inside the bounding box of the clip path, but outside the actual clip shape`);
+ checkHitResult(hitTest({ x: 300, y: 200 }),
+ APZHitResultFlags.VISIBLE,
+ rootViewId,
+ layersId,
+ `(simple) point inside the clip shape of the overlaying div`);
+
+ // Now we turn the "simple" clip-path that WR can handle into a more complex
+ // one that needs a mask. Then run the checks again; the expected results for
+ // WR are slightly different
+ document.getElementById("clipped").style.clipPath = "polygon(" +
+ "50px 200px, 75px 75px, 200px 50px, 205px 55px, 210px 50px, " +
+ "215px 55px, 220px 50px, 225px 55px, 230px 50px, 235px 55px, " +
+ "240px 50px, 245px 55px, 250px 50px, 255px 55px, 260px 50px, " +
+ "265px 55px, 270px 50px, 275px 55px, 280px 50px, 350px 200px, " +
+ "200px 350px)";
+ await promiseApzFlushedRepaints();
+
+ checkHitResult(hitTest({ x: 10, y: 10 }),
+ APZHitResultFlags.VISIBLE,
+ iframeViewId,
+ layersId,
+ `(complex) uninteresting point inside the iframe`);
+ checkHitResult(hitTest({ x: 500, y: 10 }),
+ APZHitResultFlags.VISIBLE,
+ rootViewId,
+ layersId,
+ `(complex) uninteresting point in the root scroller`);
+ checkHitResult(hitTest({ x: 110, y: 110 }),
+ APZHitResultFlags.VISIBLE,
+ iframeViewId,
+ layersId,
+ `(complex) point in the iframe behind overlaying div, but outside the bounding box of the clip path`);
+ checkHitResult(hitTest({ x: 160, y: 160 }),
+ APZHitResultFlags.VISIBLE,
+ iframeViewId,
+ layersId,
+ `(complex) point in the iframe behind overlaying div, inside the bounding box of the clip path, but outside the actual clip shape`);
+ checkHitResult(hitTest({ x: 300, y: 200 }),
+ APZHitResultFlags.VISIBLE,
+ iframeViewId,
+ layersId,
+ `(complex) point inside the clip shape of the overlaying div`);
+}
+
+waitUntilApzStable()
+ .then(test)
+ .then(subtestDone, subtestFailed);
+</script>
+</body></html>
diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_clipped_fixed_modal.html b/gfx/layers/apz/test/mochitest/helper_hittest_clipped_fixed_modal.html
new file mode 100644
index 0000000000..122863967a
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_hittest_clipped_fixed_modal.html
@@ -0,0 +1,85 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Hit-testing on content covered by a fullscreen fixed-position item clipped away</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <meta name="viewport" content="width=device-width"/>
+<style>
+.modal
+{
+ position:fixed;
+ z-index:10;
+ width:100%;
+ height:100%;
+ left:0;
+ top:0;
+ clip:rect(1px,1px,1px,1px);
+}
+.modal__content
+{
+ overflow:auto;
+ position:fixed;
+ top:0;
+ left:0;
+ width:100%;
+ height:100%;
+}
+.modal__body
+{
+ position:absolute;
+ top:0;
+ left:0;
+ width:100%;
+ height:100%;
+}
+.content
+{
+ position:fixed;
+ top:0;
+ left:0;
+ width:100%;
+ height:100%;
+ overflow-y:auto
+}
+</style>
+</head>
+<body>
+<div class="content">
+ <div style="height: 5000px; background-image: linear-gradient(red,blue)">
+ Filler to make the content div scrollable
+ </div>
+</div>
+<div class="modal">
+ <div class="modal__content">
+ <div class="modal__body">
+ </div>
+ </div>
+</div>
+</body>
+<script type="application/javascript">
+
+async function test() {
+ var config = getHitTestConfig();
+ var utils = config.utils;
+
+ // layerize the scrollable frame
+ var subframe = document.querySelector(".content");
+ utils.setDisplayPortForElement(0, 0, 800, 2000, subframe, 1);
+ await promiseApzFlushedRepaints();
+
+ var target = document.querySelector(".content");
+ checkHitResult(hitTest(centerOf(target)),
+ APZHitResultFlags.VISIBLE,
+ utils.getViewId(subframe),
+ utils.getLayersId(),
+ "content covered by a clipped fixed div");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+</script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_deep_scene_stack.html b/gfx/layers/apz/test/mochitest/helper_hittest_deep_scene_stack.html
new file mode 100644
index 0000000000..a04b1d3e83
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_hittest_deep_scene_stack.html
@@ -0,0 +1,57 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Exercising the APZ/WR hit-test with a deep scene that produces many results</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <meta name="viewport" content="width=device-width"/>
+<style>
+body {
+ transform-style: preserve-3d;
+}
+
+div {
+ height: 100px;
+ background-color: rgba(0, 255, 0, 0.1);
+ transform: translateX(1px);
+}
+</style>
+</head>
+<body>
+<script>
+
+// Create a 1000-deep set of nested divs with some transparency and transforms.
+// This ensures that the WR hit-test will return all of the divs at the tested
+// point, rather than just the topmost one. We set a touch-action property on
+// this div so that we can ensure we're hit-testing at the right spot.
+var div = document.createElement('div');
+div.id = "innermost";
+div.style.touchAction = "pan-x pan-y";
+div.style.width = "2px";
+
+for (var i = 3; i < 1000; i++) {
+ var container = document.createElement('div');
+ container.style.width = i + "px";
+ container.appendChild(div);
+ div = container;
+}
+document.body.appendChild(div);
+
+async function test(testDriver) {
+ var config = getHitTestConfig();
+ var utils = config.utils;
+
+ // Hit-test at the deepest point of divs.
+ checkHitResult(hitTest(centerOf(document.getElementById("innermost"))),
+ APZHitResultFlags.VISIBLE | APZHitResultFlags.PINCH_ZOOM_DISABLED | APZHitResultFlags.DOUBLE_TAP_ZOOM_DISABLED,
+ utils.getViewId(document.scrollingElement),
+ utils.getLayersId(),
+ "innermost div");
+}
+
+
+waitUntilApzStable().then(test).then(subtestDone, subtestFailed);
+
+</script>
+</body>
diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_fixed-2.html b/gfx/layers/apz/test/mochitest/helper_hittest_fixed-2.html
new file mode 100644
index 0000000000..0f20719d46
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_hittest_fixed-2.html
@@ -0,0 +1,74 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>APZ hit-testing of fixed content when async-scrolled</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <meta name="viewport" content="width=device-width"/>
+ <style>
+ html, body {
+ margin: 0;
+ }
+ #fixed {
+ position: fixed;
+ height: 300px;
+ width: 100%;
+ top: 0;
+ background: blue;
+ }
+ #target {
+ margin-top: 100px;
+ margin-left: 100px;
+ height: 20px;
+ width: 20px;
+ background: red;
+ }
+ </style>
+</head>
+<body>
+ <div id="fixed">
+ <div id="target"></div>
+ </div>
+ <div id="make_root_scrollable" style="height: 5000px"></div>
+</body>
+<script type="application/javascript">
+
+async function test() {
+ // Async scroll the page by 50 pixels using the mouse-wheel.
+ await promiseMoveMouseAndScrollWheelOver(document.body, 10, 10,
+ /* waitForScroll = */ false, /* scrollDelta = */ 50);
+
+ let clickPromise = new Promise(resolve => {
+ target.addEventListener("click", e => {
+ ok(true, "Target was hit");
+ e.stopPropagation(); // do not propagate event to |fixed| ancestor
+ resolve();
+ });
+ fixed.addEventListener("click", e => {
+ // Since target's listener calls stopPropagation(), if we get here
+ // then the coordinates of the click event did not correspond to
+ // |target|, but somewhere else on |fixed|.
+ ok(false, "Fixed ancestor should not be hit");
+ resolve();
+ });
+ });
+
+ // Synthesize a click at (110, 110), which should hit |target| (a
+ // descendant of |fixed|) regardless of the async scroll.
+ await synthesizeNativeMouseEventWithAPZ({
+ type: "click",
+ target: window,
+ offsetX: 110,
+ offsetY: 110
+ });
+
+ await clickPromise;
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+</script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_fixed-3.html b/gfx/layers/apz/test/mochitest/helper_hittest_fixed-3.html
new file mode 100644
index 0000000000..2004ea9ae4
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_hittest_fixed-3.html
@@ -0,0 +1,113 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>APZ hit-testing of fixed content when async-scrolled</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <meta name="viewport" content="width=device-width"/>
+ <style>
+ html, body {
+ margin: 0;
+ }
+ iframe {
+ width: 100%;
+ height: 400px;
+ margin-top: 100px;
+ padding: 0px;
+ border: 0px;
+ display: block;
+ }
+ </style>
+</head>
+<body>
+ <iframe id="subdoc" srcdoc="<!DOCTYPE HTML>
+ <html>
+ <style>
+ html, body {
+ margin: 0;
+ }
+ #fixed {
+ position: fixed;
+ height: 300px;
+ width: 100%;
+ top: 0;
+ background: blue;
+ }
+ #target {
+ margin-top: 100px;
+ margin-left: 100px;
+ height: 20px;
+ width: 20px;
+ background: red;
+ }
+ </style>
+ <div id='fixed'>
+ <div id='target'></div>
+ </div>
+ <div id='make_scrollable' style='height: 5000px'></div>
+ </html>
+ "></iframe>
+ <div id="make_root_scrollable" style="height: 5000px"></div>
+</body>
+<script type="application/javascript">
+
+async function test() {
+ // Async scroll the root content document by 50 pixels.
+ // This test uses a touch-drag (with relevant prefs set in
+ // test_group_hittest-2.html to e.g. disable flings)
+ // rather than mousewheel so that we have control over the
+ // precise amount scrolled.
+ let transformEndPromise = promiseTransformEnd();
+ await synthesizeNativeTouchDrag(document.documentElement, 10, 10, 0, -50);
+ await transformEndPromise;
+
+ // Set up listeners that pass the test if we correctly hit |target|
+ // but fail it if we hit something else.
+ let target = subdoc.contentWindow.target;
+ let fixed = subdoc.contentWindow.fixed;
+ let clickPromise = new Promise(resolve => {
+ target.addEventListener("click", e => {
+ ok(true, "Target was hit");
+ e.stopPropagation(); // do not propagate event to ancestors
+ resolve();
+ });
+ fixed.addEventListener("click", e => {
+ // Since target's listener calls stopPropagation(), if we get here
+ // then the coordinates of the click event did not correspond to
+ // |target|, but somewhere else on |fixed|.
+ //
+ // TODO(bug 1786369): Ensure the parent is not hit once this is
+ // no longer an intermittent failure.
+ todo(false, "Fixed ancestor should not be hit");
+ resolve();
+ });
+ window.addEventListener("click", e => {
+ // Similarly, the root content document's window should not be hit.
+ ok(false, "Root document should not be hit");
+ resolve();
+ });
+ });
+
+ // Synthesize a click which should hit |target|.
+ // The y-coordinate relative to the window is:
+ // 100 pixel margin of iframe from top of root content doc
+ // + 100 pixel margin of target from top of iframe
+ // - 50 pixels async scrolled
+ // + 10 pixels to get us to the middle of the 20px-width target
+ await synthesizeNativeMouseEventWithAPZ({
+ type: "click",
+ target: window,
+ offsetX: 110,
+ offsetY: 160
+ });
+
+ await clickPromise;
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+</script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_fixed.html b/gfx/layers/apz/test/mochitest/helper_hittest_fixed.html
new file mode 100644
index 0000000000..530c53fd7a
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_hittest_fixed.html
@@ -0,0 +1,82 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>APZ hit-testing of fixed content when async-scrolled</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <meta name="viewport" content="width=device-width"/>
+ <style>
+ html, body {
+ margin: 0;
+ }
+ #fixed {
+ position: fixed;
+ height: 300px;
+ width: 100%;
+ top: 0;
+ background: blue;
+ }
+ #target {
+ margin-top: 100px;
+ margin-left: 100px;
+ height: 20px;
+ width: 20px;
+ background: red;
+ }
+ </style>
+</head>
+<body>
+ <div id="fixed">
+ <div id="target"></div>
+ </div>
+ <div id="make_root_scrollable" style="height: 5000px"></div>
+</body>
+<script type="application/javascript">
+
+async function test() {
+ var config = getHitTestConfig();
+ var utils = config.utils;
+
+ // Async scroll the page by 50 pixels. The scroll does not move
+ // the fixed element.
+ utils.setAsyncScrollOffset(document.documentElement, 0, 50);
+ // Tick the refresh driver once to make sure the compositor has applied the
+ // async scroll offset (for WebRender hit-testing we need to make sure WR has
+ // the latest info).
+ utils.advanceTimeAndRefresh(16);
+ utils.restoreNormalRefresh();
+
+ let clickPromise = new Promise(resolve => {
+ target.addEventListener("click", e => {
+ ok(true, "Target was hit");
+ e.stopPropagation(); // do not propagate event to |fixed| ancestor
+ resolve();
+ });
+ fixed.addEventListener("click", e => {
+ // Since target's listener calls stopPropagation(), if we get here
+ // then the coordinates of the click event did not correspond to
+ // |target|, but somewhere else on |fixed|.
+ ok(false, "Fixed ancestor should not be hit");
+ resolve();
+ });
+ });
+
+ // Synthesize a click at (110, 110), which should hit |target| (a
+ // descendant of |fixed|) regardless of the async scroll.
+ await synthesizeNativeMouseEventWithAPZ({
+ type: "click",
+ target: window,
+ offsetX: 110,
+ offsetY: 110
+ });
+
+ await clickPromise;
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+</script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_fixed_bg.html b/gfx/layers/apz/test/mochitest/helper_hittest_fixed_bg.html
new file mode 100644
index 0000000000..1eae84305d
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_hittest_fixed_bg.html
@@ -0,0 +1,53 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Hit-testing of fixed background image</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+ html,
+ body {
+ height: 100vh;
+ margin: 0px;
+ padding: 0px;
+ overflow-x: hidden;
+ }
+ .bg-gradient {
+ background: linear-gradient(white, green) fixed;
+ height: 1000px;
+ width: 100%;
+ }
+ </style>
+ </head>
+ <body>
+ <div class="bg-gradient"></div>
+ <div style="height: 1000px; background-color: green;"></div>
+ </body>
+<script type="application/javascript">
+
+async function test() {
+ var config = getHitTestConfig();
+ var utils = config.utils;
+
+ var body = document.querySelector("body");
+ utils.setDisplayPortForElement(0, 0, 800, 2000, body, 1);
+ await promiseApzFlushedRepaints();
+
+ var target = document.querySelector(".bg-gradient");
+ checkHitResult(hitTest(centerOf(target)),
+ APZHitResultFlags.VISIBLE,
+ utils.getViewId(body),
+ utils.getLayersId(),
+ "fixed bg image");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+</script>
+</html>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_fixed_in_scrolled_transform.html b/gfx/layers/apz/test/mochitest/helper_hittest_fixed_in_scrolled_transform.html
new file mode 100644
index 0000000000..93d1e6064d
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_hittest_fixed_in_scrolled_transform.html
@@ -0,0 +1,91 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Hit-testing on the special setup from fixed-pos-scrolled-clip-3.html</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <meta name="viewport" content="width=device-width"/>
+<style>
+body {
+ margin: 0;
+ height: 4000px;
+}
+
+.transform {
+ transform: translate(10px, 10px);
+ width: 500px;
+}
+
+.subframe {
+ height: 600px;
+ overflow: auto;
+ box-shadow: 0 0 0 2px black;
+}
+
+.scrolled {
+ height: 4000px;
+ position: relative;
+}
+
+.absoluteClip {
+ position: absolute;
+ top: 300px;
+ left: 100px;
+ width: 200px;
+ height: 200px;
+ background: red;
+ clip: rect(auto auto auto auto);
+}
+
+.fixed {
+ position: fixed;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ background: linear-gradient(lime, lime) black 0 100px no-repeat;
+ background-size: 100% 200px;
+}
+</style>
+</head>
+<body>
+<!-- This is lifted from layout/reftests/async-scrolling/fixed-pos-scrolled-clip-3.html -->
+<div class="transform">
+ <div class="subframe">
+ <div class="scrolled">
+ <div class="absoluteClip">
+ <div class="fixed"></div>
+ </div>
+ </div>
+ </div>
+</div>
+</body>
+<script type="application/javascript">
+
+async function test() {
+ var config = getHitTestConfig();
+ var utils = config.utils;
+
+ // layerize the scrollable frame
+ var subframe = document.querySelector(".subframe");
+ utils.setDisplayPortForElement(0, 0, 800, 2000, subframe, 1);
+ await promiseApzFlushedRepaints();
+
+ var target = document.querySelector(".absoluteClip");
+ // The fixed item is fixed with respect to the transform, which is
+ // outside the subframe, so we should scroll the root scroll frame,
+ // not the subframe.
+ checkHitResult(hitTest(centerOf(target)),
+ APZHitResultFlags.VISIBLE,
+ utils.getViewId(document.scrollingElement),
+ utils.getLayersId(),
+ "fixed item inside a scrolling transform");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+</script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_fixed_item_over_oop_iframe.html b/gfx/layers/apz/test/mochitest/helper_hittest_fixed_item_over_oop_iframe.html
new file mode 100644
index 0000000000..a8b66cb835
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_hittest_fixed_item_over_oop_iframe.html
@@ -0,0 +1,61 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>Hit-testing of positioned item on top of oop iframe</title>
+<script src="apz_test_utils.js"></script>
+<script src="apz_test_native_event_utils.js"></script>
+<script src="/tests/SimpleTest/paint_listener.js"></script>
+<!-- https://bugzilla.mozilla.org/show_bug.cgi?id=1747409 -->
+<style>
+body, html {
+ width:100%;
+ height:100%;
+ margin:0;
+}
+
+iframe {
+ width:100%;
+ height:100%;
+ position:fixed;
+ z-index:1;
+}
+
+.over {
+ position: fixed;
+ right: 50px;
+ bottom: 50px;
+ z-index: 9999;
+}
+</style>
+<div class="over">
+ <a id="target" href="#">Link</a>
+</div>
+<iframe src="https://example.com"></iframe>
+<script>
+async function test() {
+ let config = getHitTestConfig();
+ let utils = config.utils;
+ let iframe = document.querySelector("iframe");
+ let target = document.getElementById("target");
+
+ try {
+ iframe.contentWindow.document;
+ ok(false, "Should be a cross-origin iframe");
+ } catch (ex) {}
+
+ await promiseApzFlushedRepaints();
+
+ checkHitResult(hitTest(centerOf(target)),
+ APZHitResultFlags.VISIBLE,
+ utils.getViewId(document.scrollingElement),
+ utils.getLayersId(),
+ "fixed element with z-index");
+}
+
+
+onload = function() {
+ waitUntilApzStable()
+ .then(test)
+ .then(subtestDone, subtestFailed);
+};
+</script>
diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_float_bug1434846.html b/gfx/layers/apz/test/mochitest/helper_hittest_float_bug1434846.html
new file mode 100644
index 0000000000..986c7b8477
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_hittest_float_bug1434846.html
@@ -0,0 +1,56 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>APZ hit-testing with floated subframe</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <meta name="viewport" content="width=device-width"/>
+ <style>
+ #float {
+ float: left;
+ }
+ #subframe {
+ overflow: scroll;
+ height: 300px;
+ }
+ #subframe-content {
+ width: 300px;
+ height: 2000px;
+ background: cyan;
+ }
+ #make-root-scrollable {
+ height: 5000px;
+ }
+ </style>
+</head>
+<body>
+ <div id="float">
+ <div id="subframe">
+ <div id="subframe-content"></div>
+ </div>
+ </div>
+ <div id="make-root-scrollable"></div>
+</body>
+<script type="application/javascript">
+
+async function test() {
+ var utils = getHitTestConfig().utils;
+
+ hitTestScrollbar({
+ element: document.getElementById("subframe"),
+ directions: { vertical: true },
+ expectedScrollId: utils.getViewId(document.scrollingElement),
+ expectedLayersId: utils.getLayersId(),
+ trackLocation: ScrollbarTrackLocation.START,
+ expectThumb: true,
+ layerState: LayerState.INACTIVE,
+ });
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+</script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_float_bug1443518.html b/gfx/layers/apz/test/mochitest/helper_hittest_float_bug1443518.html
new file mode 100644
index 0000000000..0e2e754375
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_hittest_float_bug1443518.html
@@ -0,0 +1,56 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>APZ hit-testing with floated subframe</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <meta name="viewport" content="width=device-width"/>
+ <style>
+ #div1 {
+ position: relative;
+ }
+ #div2 {
+ width: 300px;
+ float: left;
+ }
+ #subframe {
+ overflow: auto;
+ }
+ #make-root-scrollable {
+ height: 5000px;
+ }
+ </style>
+</head>
+<body>
+ <div id="div1">
+ <div id="div2">
+ <div id="subframe">
+ <pre>A line of text that overflows because it's sufficiently long</pre>
+ </div>
+ </div>
+ </div>
+ <div id="make-root-scrollable"></div>
+</body>
+<script type="application/javascript">
+
+async function test() {
+ var utils = getHitTestConfig().utils;
+
+ hitTestScrollbar({
+ element: document.getElementById("subframe"),
+ directions: { horizontal: true },
+ expectedScrollId: utils.getViewId(document.scrollingElement),
+ expectedLayersId: utils.getLayersId(),
+ trackLocation: ScrollbarTrackLocation.START,
+ expectThumb: true,
+ layerState: LayerState.INACTIVE,
+ });
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+</script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_hidden_inactive_scrollframe.html b/gfx/layers/apz/test/mochitest/helper_hittest_hidden_inactive_scrollframe.html
new file mode 100644
index 0000000000..0abed82156
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_hittest_hidden_inactive_scrollframe.html
@@ -0,0 +1,55 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>APZ hit-testing with an inactive scrollframe that is visibility:hidden (bug 1673505)</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <meta name="viewport" content="width=device-width"/>
+</head>
+<body style="height: 110vh">
+ <div style="position:fixed; top:0px; bottom:0px; left:0px; right:0px; visibility:hidden">
+ <div style="overflow-y: scroll; height: 100vh" id="nested">
+ <div style="height: 200vh; background-color: red">
+ The body of this document is scrollable and is the main scrollable
+ element. On top of that we have a hidden fixed-pos item containing another
+ scrollframe, but this nested scrollframe is inactive.
+ Since the fixed-pos item is hidden, the nested scrollframe is hidden
+ too and shouldn't be the target of hit-testing. However, because it is
+ an inactive scrollframe, code to generate the "this is an inactive
+ scrollframe" area was marking it as hit-testable. This bug led to hit-
+ tests being mis-targeted to the nested scrollframe's layers id instead
+ of whatever was underneath.
+ </div>
+ </div>
+ </div>
+</body>
+<script type="application/javascript">
+
+function test(testDriver) {
+ var config = getHitTestConfig();
+ var utils = config.utils;
+
+ let hasViewId;
+ try {
+ utils.getViewId(document.getElementById("nested"));
+ hasViewId = true;
+ } catch (e) {
+ hasViewId = false;
+ }
+ if (!config.activateAllScrollFrames) {
+ ok(!hasViewId, "The nested scroller should be inactive and not have a view id");
+ }
+
+ checkHitResult(
+ hitTest(centerOf(document.body)),
+ APZHitResultFlags.VISIBLE,
+ utils.getViewId(document.scrollingElement),
+ utils.getLayersId(),
+ "hit went through the hidden scrollframe");
+}
+
+waitUntilApzStable().then(test).then(subtestDone, subtestFailed);
+
+</script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_hoisted_scrollinfo.html b/gfx/layers/apz/test/mochitest/helper_hittest_hoisted_scrollinfo.html
new file mode 100644
index 0000000000..3427c8da47
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_hittest_hoisted_scrollinfo.html
@@ -0,0 +1,81 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Hit-testing on a scrollframe forced to be inactive by being inside a filter</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <meta name="viewport" content="width=device-width"/>
+<style>
+ #withfilter {
+ filter: url(#menushadow);
+ }
+
+ #scroller {
+ width: 300px;
+ height: 500px;
+ overflow: scroll;
+ }
+
+ .spacer {
+ height: 1000px;
+ background-image: linear-gradient(red, blue);
+ }
+</style>
+</head>
+<body>
+ <div id="withfilter">
+ <div id="scroller">
+ <div class="spacer"></div>
+ </div>
+ </div>
+<!-- the SVG below copied directly from the Gecko Profiler code that
+ demonstrated the original bug. It basically generates a bit of a "drop
+ shadow" effect on the div it's applied to. Original SVG can be found at
+ https://github.com/firefox-devtools/profiler/blame/624f71bce5469cf4f8b2be720e929ba69fa6bfdc/res/img/svg/shadowfilter.svg -->
+ <svg xmlns="http://www.w3.org/2000/svg">
+ <defs>
+ <filter id="menushadow" color-interpolation-filters="sRGB" x="-10" y="-10" width="30" height="30">
+ <feComponentTransfer in="SourceAlpha">
+ <feFuncA type="linear" slope="0.3"/>
+ </feComponentTransfer>
+ <feGaussianBlur stdDeviation="5"/>
+ <feOffset dy="10" result="shadow"/>
+ <feComponentTransfer in="SourceAlpha">
+ <feFuncA type="linear" slope="0.1"/>
+ </feComponentTransfer>
+ <feMorphology operator="dilate" radius="0.5" result="rim"/>
+ <feMerge><feMergeNode in="shadow"/><feMergeNode in="rim"/></feMerge>
+ <feComposite operator="arithmetic" in2="SourceAlpha" k2="1" k3="-0.1"/>
+ <feMerge><feMergeNode/><feMergeNode in="SourceGraphic"/></feMerge>
+ </filter>
+ </defs>
+ </svg>
+</body>
+<script type="application/javascript">
+async function test() {
+ var config = getHitTestConfig();
+ var utils = config.utils;
+
+ // layerize the scrollable frame. It's inside the filter so this
+ // shouldn't actually change the fact that it will still be main-thread
+ // scrolled.
+ var scroller = document.querySelector("#scroller");
+ utils.setDisplayPortForElement(0, 0, 300, 500, scroller, 1);
+ await promiseApzFlushedRepaints();
+
+ var expectedHitFlags =
+ APZHitResultFlags.VISIBLE | APZHitResultFlags.INACTIVE_SCROLLFRAME;
+ checkHitResult(hitTest(centerOf(scroller)),
+ expectedHitFlags,
+ utils.getViewId(scroller),
+ utils.getLayersId(),
+ "scrollable content inside a filter");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+</script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_iframe_perspective-2.html b/gfx/layers/apz/test/mochitest/helper_hittest_iframe_perspective-2.html
new file mode 100644
index 0000000000..9838a02aa9
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_hittest_iframe_perspective-2.html
@@ -0,0 +1,69 @@
+<head>
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Test that events are delivered to the correct document near an iframe inide a perspective transform</title>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+ div {
+ position: absolute;
+ }
+ .outer {
+ left: 300px;
+ top: 300px;
+ transform: translate3d(0px, 0px, -500px) perspective(500px);
+ }
+ .inner {
+ left: -100px;
+ top: -100px;
+ }
+ iframe {
+ border: 0;
+ }
+ </style>
+</head>
+<body>
+ <div class="outer">
+ <div class="inner">
+ <iframe id="iframe" width="300px" height="200px" src="https://example.com/tests/gfx/layers/apz/test/mochitest/helper_hittest_iframe_perspective_child.html"></iframe>
+ </div>
+ </div>
+ <script type="application/javascript">
+async function test() {
+ // Wait for iframe to receive content transforms.
+ await SpecialPowers.spawn(iframe, [], async () => {
+ await SpecialPowers.contentTransformsReceived(content.window);
+ });
+
+ let eventPromise = new Promise(resolve => {
+ window.addEventListener("message", event => {
+ let data = JSON.parse(event.data);
+ if ("type" in data && data.type == "got-mouse-down") {
+ ok(false, "Child document should not have received mouse-down");
+ resolve();
+ }
+ });
+
+ window.addEventListener("mousedown", event => {
+ ok(true, "Parent document should have received mouse-down");
+ resolve();
+ });
+ });
+
+ // Click a bit above the iframe, and check the event is delivered
+ // to the parent document, not the iframe.
+ await synthesizeNativeMouseEventWithAPZ({
+ type: "click",
+ target: document.documentElement,
+ offsetX: 350,
+ offsetY: 175
+ });
+ await eventPromise;
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</body>
diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_iframe_perspective-3.html b/gfx/layers/apz/test/mochitest/helper_hittest_iframe_perspective-3.html
new file mode 100644
index 0000000000..7fb423ca1c
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_hittest_iframe_perspective-3.html
@@ -0,0 +1,70 @@
+<head>
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Test that events are delivered with correct coordinates to an iframe inide a perspective transform</title>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+ html, body {
+ margin: 0;
+ padding: 0;
+ }
+ #outer {
+ margin-top: 50px;
+ margin-left: 50px;
+ perspective: 500px;
+ }
+ #inner {
+ transform: translate3d(0,0,-100px);
+ transform-style: preserve-3d;
+ }
+ iframe {
+ border: 0;
+ background-color: blue;
+ transform: translate3d(0,0,100px);
+ }
+ </style>
+</head>
+<body>
+ <div id="outer">
+ <div id="inner">
+ <iframe id="iframe" src="https://example.com/tests/gfx/layers/apz/test/mochitest/helper_hittest_iframe_perspective_child.html"></iframe>
+ </div>
+ </div>
+ <script type="application/javascript">
+async function test() {
+ // Wait for iframe to receive content transforms.
+ await SpecialPowers.spawn(iframe, [], async () => {
+ await SpecialPowers.contentTransformsReceived(content.window);
+ });
+
+ let clickCoordsInChild = {
+ offsetX: 0,
+ offsetY: 0
+ };
+ let childMessagePromise = new Promise(resolve => {
+ window.addEventListener("message", event => {
+ let data = JSON.parse(event.data);
+ if ("type" in data && data.type == "got-mouse-down") {
+ clickCoordsInChild = data.coords;
+ resolve();
+ }
+ })
+ });
+ await synthesizeNativeMouseEventWithAPZ({
+ type: "click",
+ target: outer,
+ offsetX: 100,
+ offsetY: 100
+ });
+ await childMessagePromise;
+ is(clickCoordsInChild.offsetX, 100, "x coordinate is correct");
+ is(clickCoordsInChild.offsetY, 100, "y coordinate is correct");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</body>
diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_iframe_perspective.html b/gfx/layers/apz/test/mochitest/helper_hittest_iframe_perspective.html
new file mode 100644
index 0000000000..d7858d4b00
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_hittest_iframe_perspective.html
@@ -0,0 +1,60 @@
+<head>
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Test that events are delivered with correct coordinates to an iframe inide a perspective transform</title>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+ #container {
+ transform: translate3d(0px, 0px, -1000px) perspective(496.281px);
+ width: min-content;
+ }
+ iframe {
+ border: 0;
+ }
+ </style>
+</head>
+<body>
+ <div id="container">
+ <iframe id="iframe" src="https://example.com/tests/gfx/layers/apz/test/mochitest/helper_hittest_iframe_perspective_child.html"></iframe>
+ </div>
+ <script type="application/javascript">
+async function test() {
+ // Wait for iframe to receive content transforms.
+ await SpecialPowers.spawn(iframe, [], async () => {
+ await SpecialPowers.contentTransformsReceived(content.window);
+ });
+
+ let clickCoordsInChild = {
+ offsetX: 0,
+ offsetY: 0
+ };
+ let childMessagePromise = new Promise(resolve => {
+ window.addEventListener("message", event => {
+ let data = JSON.parse(event.data);
+ if ("type" in data && data.type == "got-mouse-down") {
+ clickCoordsInChild = data.coords;
+ resolve();
+ }
+ })
+ });
+ await synthesizeNativeMouseEventWithAPZ({
+ type: "click",
+ target: container,
+ offsetX: 100,
+ offsetY: 100
+ });
+ await childMessagePromise;
+ // Need to use fuzzy comparisons because the combination of floating-point
+ // matrix multiplication and rounding to integer coordinates can result in
+ // small discrepancies.
+ isfuzzy(clickCoordsInChild.offsetX, 100, 1, "x coordinate is correct");
+ isfuzzy(clickCoordsInChild.offsetY, 100, 1, "y coordinate is correct");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</body>
diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_iframe_perspective_child.html b/gfx/layers/apz/test/mochitest/helper_hittest_iframe_perspective_child.html
new file mode 100644
index 0000000000..37f20ad725
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_hittest_iframe_perspective_child.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<script>
+ window.addEventListener("mousedown", event => {
+ let data = JSON.stringify({
+ type: "got-mouse-down",
+ coords: {
+ offsetX: event.clientX,
+ offsetY: event.clientY
+ }
+ });
+ window.parent.postMessage(data, "*");
+ });
+</script>
diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_nested_transforms_bug1459696.html b/gfx/layers/apz/test/mochitest/helper_hittest_nested_transforms_bug1459696.html
new file mode 100644
index 0000000000..9d0a9fa50c
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_hittest_nested_transforms_bug1459696.html
@@ -0,0 +1,80 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>APZ hit-testing with nested inactive transforms (bug 1459696)</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <meta name="viewport" content="width=device-width"/>
+ <style>
+ .pane {
+ position: fixed;
+ top: 0;
+ bottom: 0;
+ }
+ .left {
+ left: 0;
+ right: 66vw;
+ overflow: auto;
+ }
+ .content {
+ width: 100%;
+ height: 200%;
+ background-image: linear-gradient(blue, green);
+ }
+ .right {
+ left: 34vw;
+ right: 0;
+ }
+ .list {
+ overflow: hidden;
+ transform: translate3d(0, 0, 0);
+ height: 100%;
+ }
+ .track {
+ height: 100%;
+ width: 2000px;
+ transform: translate3d(-856px, 0px, 0px);
+ }
+ .slide {
+ float: left;
+ height: 100%;
+ width: 856px;
+ background-image: linear-gradient(red, yellow);
+ }
+ </style>
+</head>
+<body>
+ <div class="left pane" id="left-pane">
+ <div class="content"></div>
+ </div>
+ <div class="right pane">
+ <div class="list">
+ <div class="track">
+ <div class="slide"></div>
+ <div class="slide"></div>
+ </div>
+ </div>
+ </div>
+</body>
+<script type="application/javascript">
+
+async function test() {
+ var utils = getHitTestConfig().utils;
+
+ var leftPane = document.getElementById("left-pane");
+
+ checkHitResult(
+ hitTest(centerOf(leftPane)),
+ APZHitResultFlags.VISIBLE,
+ utils.getViewId(leftPane),
+ utils.getLayersId(),
+ "left pane was successfully hit");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+</script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_obscuration.html b/gfx/layers/apz/test/mochitest/helper_hittest_obscuration.html
new file mode 100644
index 0000000000..377b191359
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_hittest_obscuration.html
@@ -0,0 +1,77 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test hit-testing on content which is revealed by async scrolling</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <meta name="viewport" content="width=device-width"/>
+ <style>
+ #parent {
+ width: 400px;
+ height: 400px;
+ overflow: auto;
+ }
+ #child {
+ margin-top: 200px;
+ width: 100%;
+ height: 100px;
+ overflow: auto;
+ }
+ #obscurer {
+ position: absolute;
+ top: 200px;
+ width: 400px;
+ height: 200px;
+ background: blue;
+ }
+ .spacer {
+ width: 100%;
+ height: 1000px;
+ background: green;
+ }
+ </style>
+</head>
+<body>
+ <div id="parent">
+ <div id="child">
+ <div class="spacer"></div>
+ </div>
+ <div class="spacer"></div>
+ </div>
+ <div id="obscurer"></div>
+</body>
+<script type="application/javascript">
+
+async function test() {
+ var config = getHitTestConfig();
+ var utils = config.utils;
+
+ // Create APZCs for parent and child scrollers.
+ let parent = document.getElementById("parent"); // otherwise parent refers to window.parent
+ utils.setDisplayPortForElement(0, 0, 400, 1000, parent, 1);
+ utils.setDisplayPortForElement(0, 0, 400, 1000, child, 1);
+ await promiseApzFlushedRepaints();
+
+ // Async-scroll the parent scroller by 100 pixels, thereby revealing
+ // the child which was previous covered by "obscurer".
+ utils.setAsyncScrollOffset(parent, 0, 100);
+ // Give WebRender a chance to sample the test async scroll offset.
+ utils.advanceTimeAndRefresh(16);
+ utils.restoreNormalRefresh();
+
+ // Check that hit-testing on the region where the child scroller's
+ // contents are now revealed, successfully hits the scroller.
+ checkHitResult(hitTest({x: 200, y: 150}),
+ APZHitResultFlags.VISIBLE,
+ utils.getViewId(child),
+ utils.getLayersId(),
+ "child scroller");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+</script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_overscroll.html b/gfx/layers/apz/test/mochitest/helper_hittest_overscroll.html
new file mode 100644
index 0000000000..c245258b68
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_hittest_overscroll.html
@@ -0,0 +1,249 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test APZ hit-testing while overscrolled</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <meta name="viewport" content="width=device-width"/>
+ <style>
+ html, body {
+ margin: 0;
+ padding: 0;
+ }
+ .spacer {
+ height: 5000px;
+ }
+ #target {
+ margin-left: 100px;
+ margin-top: 2px;
+ height: 4px;
+ width: 4px;
+ background: red;
+ }
+ #fixedtarget {
+ left: 300px;
+ height: 20px;
+ width: 5px;
+ top: 2px;
+ background: green;
+ position: fixed;
+ }
+ #fixedtargetbutton {
+ height: 10px;
+ width: 5px;
+ padding: 0;
+ margin-top: 10;
+ margin-left: 0;
+ border: 0;
+ }
+ </style>
+</head>
+<body>
+ <div id="target"></div>
+ <div id="fixedtarget">
+ <button id="fixedtargetbutton">
+ </button>
+ </div>
+ <div id="spacer" class="spacer"></div>
+</body>
+<script type="application/javascript">
+
+// Some helper functions for listening for click events in the browser chrome.
+
+// A handle used to interact with the chrome script used to implement
+// [start|stop]ListeningFOrClickEventsInChrome().
+let chromeScriptHandle = null;
+
+function startListeningForClickEventsInChrome() {
+ function chromeScript() {
+ /* eslint-env mozilla/chrome-script */
+ let topWin = Services.wm.getMostRecentWindow("navigator:browser");
+ if (!topWin) {
+ topWin = Services.wm.getMostRecentWindow("navigator:geckoview");
+ }
+ let chromeReceivedClick = false;
+ function chromeListener(e) {
+ chromeReceivedClick = true;
+ }
+ topWin.addEventListener("click", chromeListener);
+ function queryClicked() {
+ sendAsyncMessage("query-clicked-response", { chromeReceivedClick });
+ }
+ function cleanup() {
+ topWin.removeEventListener("click", chromeListener);
+ removeMessageListener("query-clicked", queryClicked);
+ removeMessageListener("cleanup", cleanup);
+ }
+ addMessageListener("query-clicked", queryClicked);
+ addMessageListener("cleanup", cleanup);
+ }
+ chromeScriptHandle = SpecialPowers.loadChromeScript(chromeScript);
+}
+
+async function didChromeReceiveClick() {
+ chromeScriptHandle.sendAsyncMessage("query-clicked", null);
+ let response = await chromeScriptHandle.promiseOneMessage("query-clicked-response");
+ ok(response && ("chromeReceivedClick" in response),
+ "Received a well-formed response from chrome script");
+ return response.chromeReceivedClick;
+}
+
+function stopListeningForClickEventsInChrome() {
+ chromeScriptHandle.sendAsyncMessage("cleanup", null);
+ chromeScriptHandle.destroy();
+}
+
+async function test() {
+ var config = getHitTestConfig();
+ var utils = config.utils;
+
+ // Overscroll the root scroll frame at the top, creating a gutter.
+ // Note that the size of the gutter will only be 8px, because
+ // setAsyncScrollOffset() applies the overscroll as a single delta,
+ // and current APZ logic that transforms a delta into an overscroll
+ // amount limits each delta to at most 8px.
+ utils.setAsyncScrollOffset(document.documentElement, 0, -200);
+
+ // Check that the event hits the root scroll frame in APZ.
+ // This is important because additional pan-gesture events in the gutter
+ // should continue to be handled and cause further overscroll (or
+ // relieving overscroll, depending on their direction).
+ let hitResult = hitTest({x: 100, y: 4});
+ let rootViewId = utils.getViewId(document.documentElement);
+ checkHitResult(hitResult,
+ APZHitResultFlags.VISIBLE,
+ rootViewId,
+ utils.getLayersId(),
+ "APZ hit-test in the root gutter");
+
+ // Now, perform a click in the gutter and check that APZ prevents
+ // the event from reaching Gecko.
+ // To be sure that no event was dispatched to Gecko, install listeners
+ // on both the browser chrome window and the content window.
+ // This makes sure we catch the case where the overscroll transform causes
+ // the event to incorrectly target the browser chrome.
+
+ //// Util function to perform mouse events in the gutter. Used to ensure these
+ //// events are not dispatched to the content.
+ async function clickInGutter(xOffset, yOffset) {
+ startListeningForClickEventsInChrome();
+ let contentReceivedClick = false;
+ let contentListener = function(e) {
+ info("event clientX = " + e.clientX);
+ info("event clientY = " + e.clientY);
+ info("event target id: " + e.target.id);
+ contentReceivedClick = true;
+ };
+ document.addEventListener("click", contentListener);
+ await synthesizeNativeMouseEventWithAPZ({
+ type: "click",
+ target: window,
+ offsetX: xOffset,
+ offsetY: yOffset,
+ });
+ // Wait 10 frames for the event to maybe arrive, and if it
+ // hasn't, assume it won't.
+ for (let i = 0; i < 10; i++) {
+ await promiseFrame();
+ }
+ info("Finished waiting around for click event");
+ let chromeReceivedClick = await didChromeReceiveClick();
+ ok(!chromeReceivedClick,
+ "Gecko received click event in browser chrome when it shouldn't have");
+ ok(!contentReceivedClick,
+ "Gecko received click event targeting web content when it shouldn't have");
+ stopListeningForClickEventsInChrome();
+ document.removeEventListener("click", contentListener);
+ }
+
+ // Perform the test
+ await clickInGutter(100, 4);
+
+ // Finally, while still overscrolled, perform a click not in the gutter.
+ // This event should successfully go through to the web content, and
+ // be untransformed by the overscroll transform (such that it hits the
+ // content that is visually under the cursor).
+ let clickPromise = new Promise(resolve => {
+ document.addEventListener("click", function(e) {
+ info("event clientX = " + e.clientX);
+ info("event clientY = " + e.clientY);
+ is(e.target, target, "Click while overscrolled hit intended target");
+ resolve();
+ }, { once: true });
+ });
+ await synthesizeNativeMouseEventWithAPZ({
+ type: "click",
+ target: window,
+ offsetX: 102,
+ offsetY: 12,
+ });
+ await clickPromise;
+
+ // Test that mouse events targetting the fixed content are dispatched
+ // to the content.
+ async function clickFixed(yOffset, expectedTarget) {
+ let clickFixedPromise = new Promise(resolve => {
+ document.addEventListener("click", function(e) {
+ info("event clientX = " + e.clientX);
+ info("event clientY = " + e.clientY);
+ info("event target id: " + e.target.id);
+ is(e.target, expectedTarget, "Click while overscrolled hit intended target");
+ resolve();
+ }, { once: true });
+ });
+ await synthesizeNativeMouseEventWithAPZ({
+ type: "click",
+ target: window,
+ offsetX: 302,
+ offsetY: yOffset,
+ });
+ await clickFixedPromise;
+ }
+
+ // This hit is technically in the gutter created by the overscroll, but we
+ // should still dispatch to gecko due to the fixed element extending into
+ // the gutter.
+ await clickFixed(4, fixedtarget, false);
+ // Perform various mouse events to ensure the fixed element has not moved
+ await clickFixed(10, fixedtarget, false);
+ await clickFixed(14, fixedtargetbutton, false);
+ await clickFixed(18, fixedtargetbutton, false);
+
+ let clickFixedPromise = new Promise(resolve => {
+ document.addEventListener("click", function(e) {
+ info("event clientX = " + e.clientX);
+ info("event clientY = " + e.clientY);
+ info("event target id: " + e.target.id);
+ // TODO(dlrobertson): What exists directly below the fixed element?
+ // Should there be a gutter below the fixed element? Or should events
+ // directed below the fixed element be dispatched normally. In the
+ // transform of the mouse event, the hit will not have any side bits
+ // set and we will transform the hit result. As a result, 25 will be
+ // transformed to ~17, and the event will be dispatched to the fixed
+ // element.
+ todo(false,
+ "Click while overscrolled hit intended target below fixed content");
+ resolve();
+ }, { once: true });
+ });
+ await synthesizeNativeMouseEventWithAPZ({
+ type: "click",
+ target: window,
+ offsetX: 302,
+ offsetY: 25,
+ });
+ await clickFixedPromise;
+
+ // Click above the fixed element, but in the gutter.
+ await clickInGutter(302, 1);
+ // Click left of the the fixed element, but in the gutter.
+ await clickInGutter(298, 4);
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+</script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_overscroll_contextmenu.html b/gfx/layers/apz/test/mochitest/helper_hittest_overscroll_contextmenu.html
new file mode 100644
index 0000000000..8aff3103dd
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_hittest_overscroll_contextmenu.html
@@ -0,0 +1,129 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test APZ hit-testing while overscrolled</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <meta name="viewport" content="width=device-width"/>
+ <style>
+ html, body {
+ margin: 0;
+ padding: 0;
+ }
+ .spacer {
+ height: 5000px;
+ }
+ #target {
+ margin-left: 100px;
+ margin-top: 2px;
+ height: 4px;
+ width: 4px;
+ background: red;
+ }
+ </style>
+</head>
+<body>
+ <div id="target"></div>
+ <div class="spacer"></div>
+</body>
+<script type="application/javascript">
+
+// Some helper functions for listening for contextmenu events in the browser chrome.
+
+// A handle used to interact with the chrome script used to implement
+// [start|stop]ListeningForContextmenuEventsInChrome().
+let chromeScriptHandle = null;
+
+function startListeningForContextmenuEventsInChrome() {
+ function chromeScript() {
+ /* eslint-env mozilla/chrome-script */
+ let topWin = Services.wm.getMostRecentWindow("navigator:browser");
+ if (!topWin) {
+ topWin = Services.wm.getMostRecentWindow("navigator:geckoview");
+ }
+ let chromeReceivedContextmenu = false;
+ function chromeListener(e) {
+ chromeReceivedContextmenu = true;
+ }
+ topWin.addEventListener("contextmenu", chromeListener);
+ function queryContextmenu() {
+ sendAsyncMessage("query-contextmenu-response", { chromeReceivedContextmenu });
+ }
+ function cleanup() {
+ topWin.removeEventListener("contextmenu", chromeListener);
+ removeMessageListener("query-contextmenu", queryContextmenu);
+ removeMessageListener("cleanup", cleanup);
+ }
+ addMessageListener("query-contextmenu", queryContextmenu);
+ addMessageListener("cleanup", cleanup);
+ }
+ chromeScriptHandle = SpecialPowers.loadChromeScript(chromeScript);
+}
+
+async function didChromeReceiveContextmenu() {
+ chromeScriptHandle.sendAsyncMessage("query-contextmenu", null);
+ let response = await chromeScriptHandle.promiseOneMessage("query-contextmenu-response");
+ ok(response && ("chromeReceivedContextmenu" in response),
+ "Received a well-formed response from chrome script");
+ return response.chromeReceivedContextmenu;
+}
+
+function stopListeningForContextmenuEventsInChrome() {
+ chromeScriptHandle.sendAsyncMessage("cleanup", null);
+ chromeScriptHandle.destroy();
+}
+
+async function test() {
+ var config = getHitTestConfig();
+ var utils = config.utils;
+
+ // Overscroll the root scroll frame at the top, creating a gutter.
+ // Note that the size of the gutter will only be 8px, because
+ // setAsyncScrollOffset() applies the overscroll as a single delta,
+ // and current APZ logic that transforms a delta into an overscroll
+ // amount limits each delta to at most 8px.
+ utils.setAsyncScrollOffset(document.documentElement, 0, -200);
+
+ // Now, perform a right-click in the gutter and check that APZ prevents
+ // the contextevent from reaching Gecko.
+ // To be sure that no event was dispatched to Gecko, install listeners
+ // on both the browser chrome window and the content window.
+ // This makes sure we catch the case where the overscroll transform causes
+ // the event to incorrectly target the browser chrome.
+ let deviceScale = window.devicePixelRatio;
+ let midGutter = 4 / deviceScale; // gutter is 8 *screen* pixels
+ startListeningForContextmenuEventsInChrome();
+ let contentReceivedContextmenu = false;
+ let contentListener = function(e) {
+ contentReceivedContextmenu = true;
+ };
+ document.addEventListener("contextmenu", contentListener);
+ await synthesizeNativeMouseEventWithAPZ({
+ type: "click",
+ button: 2, // eSecondary (= "right mouse button")
+ target: window,
+ offsetX: 100,
+ offsetY: midGutter
+ });
+ // Wait 10 frames for the event to maybe arrive, and if it
+ // hasn't, assume it won't.
+ for (let i = 0; i < 10; i++) {
+ await promiseFrame();
+ }
+ info("Finished waiting around for contextmenu event");
+ let chromeReceivedContextmenu = await didChromeReceiveContextmenu();
+ ok(!chromeReceivedContextmenu,
+ "Gecko received contextmenu event in browser chrome when it shouldn't have");
+ ok(!contentReceivedContextmenu,
+ "Gecko received contextmenu event targeting web content when it shouldn't have");
+ stopListeningForContextmenuEventsInChrome();
+ document.removeEventListener("contextmenu", contentListener);
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+</script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_overscroll_subframe.html b/gfx/layers/apz/test/mochitest/helper_hittest_overscroll_subframe.html
new file mode 100644
index 0000000000..36918b3682
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_hittest_overscroll_subframe.html
@@ -0,0 +1,132 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test APZ hit-testing while overscrolled</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <meta name="viewport" content="width=device-width"/>
+ <style>
+ html, body {
+ margin: 0;
+ padding: 0;
+ }
+ #subframe {
+ width: 100px;
+ height: 100px;
+ overflow: scroll;
+ }
+ .spacer {
+ height: 5000px;
+ }
+ #target {
+ margin-left: 20px;
+ margin-top: 2px;
+ height: 4px;
+ width: 4px;
+ background: red;
+ }
+ </style>
+</head>
+<body>
+ <div id="subframe">
+ <div id="target"></div>
+ <div class="spacer"></div>
+ </div>
+ <div class="spacer"></div>
+</body>
+<script type="application/javascript">
+
+async function test() {
+ var config = getHitTestConfig();
+ var utils = config.utils;
+
+ // Subframe hit testing of overscrolled APZCs does not yet work with WebRender
+ // (bug 1701831), so bail out early.
+ if (true) {
+ todo(false, "This test does not currently pass with WebRender");
+ return;
+ }
+
+ // Activate the subframe. This both matches reality (if you're
+ // scrolling the subframe, it's active), and makes it easier
+ // to check for expected hit test outcomes.
+ utils.setDisplayPortForElement(0, 0, 500, 500, subframe, 1);
+ await promiseApzFlushedRepaints();
+
+ // Overscroll the subframe at the top, creating a gutter.
+ // Note that the size of the gutter will only be 8px, because
+ // setAsyncScrollOffset() applies the overscroll as a single delta,
+ // and current APZ logic that transforms a delta into an overscroll
+ // amount limits each delta to at most 8px.
+ utils.setAsyncScrollOffset(subframe, 0, -200);
+
+ // Check that the event hits the subframe frame in APZ.
+ // This is important because additional pan-gesture events in the gutter
+ // should continue to be handled and cause further overscroll (or
+ // relieving overscroll, depending on their direction).
+ let subframeBounds = subframe.getBoundingClientRect();
+ hitResult = hitTest({
+ x: subframeBounds.x + 50,
+ y: subframeBounds.y + 4
+ });
+ let subframeViewId = utils.getViewId(subframe);
+ checkHitResult(hitResult,
+ APZHitResultFlags.VISIBLE,
+ subframeViewId,
+ utils.getLayersId(),
+ "APZ hit-test in the subframe gutter");
+
+ // Now, perform a click in the gutter and check that APZ prevents
+ // the event from reaching Gecko.
+ // To be sure that no event was dispatched to Gecko, install the listener
+ // on the document, not the subframe.
+ // This makes sure we catch the case where the overscroll transform causes
+ // the event to incorrectly target the document.
+ let receivedClick = false;
+ let listener = function(e) {
+ receivedClick = true;
+ };
+ document.addEventListener("click", listener);
+ await synthesizeNativeMouseEventWithAPZ({
+ type: "click",
+ target: subframe,
+ offsetX: 50,
+ offsetY: 4
+ });
+ // Wait 10 frames for the event to maybe arrive, and if it
+ // hasn't, assume it won't.
+ for (let i = 0; i < 10; i++) {
+ await promiseFrame();
+ }
+ info("Finished waiting around for click event");
+ ok(!receivedClick, "Gecko received click event when it shouldn't have");
+ document.removeEventListener("click", listener);
+
+ // Finally, while still overscrolled, perform a click not in the gutter.
+ // This event should successfully go through to the web content, and
+ // be untransformed by the overscroll transform (such that it hits the
+ // content that is visually under the cursor).
+ let clickPromise = new Promise(resolve => {
+ document.addEventListener("click", function(e) {
+ info("event clientX = " + e.clientX);
+ info("event clientY = " + e.clientY);
+ is(e.target, target, "Click while overscrolled hit intended target");
+ resolve();
+ }, { once: true });
+ });
+ await synthesizeNativeMouseEventWithAPZ({
+ type: "click",
+ target: window,
+ offsetX: 22,
+ offsetY: 12
+ });
+ await clickPromise;
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+</script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_pointerevents_svg.html b/gfx/layers/apz/test/mochitest/helper_hittest_pointerevents_svg.html
new file mode 100644
index 0000000000..22b880736d
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_hittest_pointerevents_svg.html
@@ -0,0 +1,177 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Hit-testing a scrollframe covered by nonrectangular and pointer-events:none things</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <meta name="viewport" content="width=device-width"/>
+ <meta http-equiv="content-type" content="text/html; charset=UTF-8">
+<style>
+ .scroller {
+ overflow: scroll;
+ width: 100px;
+ height: 100px;
+ }
+
+ .scroller > div {
+ height: 200px;
+ background-image: linear-gradient(#fff,#000);
+ }
+</style>
+</head>
+<body>
+<div id="testcase1">
+ <div style="width: 100px;height: 50px;display: inline-block;">
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" style="overflow: visible;background-color: #aa6666;">
+ <circle cx="80" cy="50" r="50"></circle>
+ </svg>
+ </div>
+ <div class="scroller" style="display: inline-block;"><div></div></div>
+ <div style="width: 100px; height: 100px; display: inline-block; position:relative;">
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" style="overflow: visible;background-color: #aa6666;">
+ <circle cx="20" cy="50" r="50"></circle>
+ </svg>
+ </div>
+</div>
+
+<div id="testcase2" style="position:relative; height: 110px;">
+ <div style="width: 100px;height: 100px;position:absolute;pointer-events:none;left: 25px;">
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" style="background-color: #aa6666;pointer-events: none;">
+ <circle cx="75" cy="50" r="50" style="pointer-events: auto;"></circle>
+ </svg>
+ </div>
+ <div class="scroller" style="position: absolute; left: 100px;"><div></div></div>
+ <div style="width: 100px;height: 100px; position:absolute;pointer-events:none;left: 175px;">
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" style="background-color: #aa6666;pointer-events: none;">
+ <circle cx="45" cy="50" r="50" style="pointer-events: auto;"></circle>
+ </svg>
+ </div>
+</div>
+
+<div id="testcase3">
+ <div style="width: 100px;height: 50px;display: inline-block;">
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" style="overflow: visible;background-color: #aa6666;">
+ <rect x="25" y="25" width="100" height="50"></rect>
+ </svg>
+ </div>
+ <div class="scroller" style="display: inline-block;"><div></div></div>
+ <div style="width: 100px; height: 100px; display: inline-block; position:relative;">
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" style="overflow: visible;background-color: #aa6666;">
+ <rect x="-30" y="25" width="100" height="50"></rect>
+ </svg>
+ </div>
+</div>
+
+<div id="testcase4" style="position:relative; height: 110px;">
+ <div style="width: 100px;height: 100px;position:absolute;pointer-events:none;left: 25px;">
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" style="background-color: #aa6666;pointer-events: none;">
+ <rect x="25" y="25" width="100" height="50" style="pointer-events: auto;"></rect>
+ </svg>
+ </div>
+ <div class="scroller" style="position: absolute; left: 100px;"><div></div></div>
+ <div style="width: 100px;height: 100px; position:absolute;pointer-events:none;left: 175px;">
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" style="background-color: #aa6666;pointer-events: none;">
+ <rect x="-25" y="25" width="100" height="50" style="pointer-events: auto;"></rect>
+ </svg>
+ </div>
+</div>
+
+<div style="width: 40em;">
+ Each of the gradients should be scrollable, except where the black stuff on the right cover them.
+ The brown square should not prevent scrolling. Similarly, the content on the left (which goes
+ underneath the scroller) shouldn't matter.
+</div>
+<script>
+
+async function test() {
+ var config = getHitTestConfig();
+ var utils = config.utils;
+
+ // layerize the scrollable frames
+ for (var scroller of document.querySelectorAll(".scroller")) {
+ utils.setDisplayPortForElement(0, 0, 100, 200, scroller, 1);
+ }
+ await promiseApzFlushedRepaints();
+
+ var rootViewId = utils.getViewId(document.scrollingElement);
+ for (var testId = 1; testId <= 4; testId++) {
+ var target = document.querySelector(`#testcase${testId} .scroller`);
+ var scrollerViewId = utils.getViewId(target);
+ checkHitResult(hitTest(centerOf(target)),
+ APZHitResultFlags.VISIBLE,
+ scrollerViewId,
+ utils.getLayersId(),
+ `center of scroller in testcase ${testId}`);
+
+ var bounds = target.getBoundingClientRect();
+ var verticalScrollbarWidth = bounds.width - target.clientWidth;
+ var horizontalScrollbarHeight = bounds.height - target.clientHeight;
+
+ // these points should all hit the target scroller
+ checkHitResult(hitTest({x: bounds.x + 1, y: bounds.y + 1}),
+ APZHitResultFlags.VISIBLE,
+ scrollerViewId,
+ utils.getLayersId(),
+ `top left of scroller in testcase ${testId}`);
+ checkHitResult(hitTest({x: bounds.x + 1, y: bounds.y + (bounds.height / 2)}),
+ APZHitResultFlags.VISIBLE,
+ scrollerViewId,
+ utils.getLayersId(),
+ `middle left of scroller in testcase ${testId}`);
+ checkHitResult(hitTest({x: bounds.x + 1, y: bounds.bottom - horizontalScrollbarHeight - 1}),
+ APZHitResultFlags.VISIBLE,
+ scrollerViewId,
+ utils.getLayersId(),
+ `bottom left (excluding scrollbar) of scroller in testcase ${testId}`);
+ if (horizontalScrollbarHeight > 0) {
+ checkHitResult(hitTest({x: bounds.x + 1, y: bounds.bottom - 1}),
+ APZHitResultFlags.VISIBLE | APZHitResultFlags.SCROLLBAR,
+ scrollerViewId,
+ utils.getLayersId(),
+ `bottom left of scroller in testcase ${testId}`);
+ }
+
+ // With the first two cases (circle masks) both WebRender and non-WebRender
+ // emit dispatch-to-content regions for the right side, so for now we just
+ // test for that. Eventually WebRender should be able to stop emitting DTC
+ // and we can update this test to be more precise in that case.
+ // For the two rectangular test cases we get precise results rather than
+ // dispatch-to-content.
+ if (testId == 1 || testId == 2) {
+ checkHitResult(hitTest({x: bounds.right - verticalScrollbarWidth - 1, y: bounds.y + 1}),
+ APZHitResultFlags.VISIBLE | APZHitResultFlags.IRREGULAR_AREA,
+ rootViewId,
+ utils.getLayersId(),
+ `top right of scroller in testcase ${testId}`);
+ checkHitResult(hitTest({x: bounds.right - verticalScrollbarWidth - 1, y: bounds.bottom - horizontalScrollbarHeight - 1}),
+ APZHitResultFlags.VISIBLE | APZHitResultFlags.IRREGULAR_AREA,
+ rootViewId,
+ utils.getLayersId(),
+ `bottom right of scroller in testcase ${testId}`);
+ } else {
+ checkHitResult(hitTest({x: bounds.right - verticalScrollbarWidth - 1, y: bounds.y + 1}),
+ APZHitResultFlags.VISIBLE,
+ scrollerViewId,
+ utils.getLayersId(),
+ `top right of scroller in testcase ${testId}`);
+ checkHitResult(hitTest({x: bounds.right - verticalScrollbarWidth - 1, y: bounds.bottom - horizontalScrollbarHeight - 1}),
+ APZHitResultFlags.VISIBLE,
+ scrollerViewId,
+ utils.getLayersId(),
+ `bottom right of scroller in testcase ${testId}`);
+ }
+
+ checkHitResult(hitTest({x: bounds.right - 1, y: bounds.y + (bounds.height / 2)}),
+ APZHitResultFlags.VISIBLE | APZHitResultFlags.IRREGULAR_AREA,
+ rootViewId,
+ utils.getLayersId(),
+ `middle right of scroller in testcase ${testId}`);
+ }
+}
+
+waitUntilApzStable()
+ .then(test)
+ .then(subtestDone, subtestFailed);
+</script>
+</body></html>
diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_spam.html b/gfx/layers/apz/test/mochitest/helper_hittest_spam.html
new file mode 100644
index 0000000000..cace5491a5
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_hittest_spam.html
@@ -0,0 +1,100 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test doing lots of hit-testing on a rapidly changing page</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <meta name="viewport" content="width=device-width"/>
+</head>
+<style>
+#spamdiv {
+ overflow: scroll;
+ width: 400px;
+ height: 400px;
+}
+#spamdiv div {
+ width: 1000px;
+ height: 1000px;
+}
+</style>
+<body>
+<script type="application/javascript">
+
+var SPAM_LIMIT = 200; // bigger numbers make the test run longer
+
+// This function adds and removes a scrollable div very rapidly (via
+// setTimeout(0) self-scheduling). This causes very frequent layer
+// transactions with a new APZ hit-testing tree from the main thread to APZ.
+// The div is created afresh every time so that the scroll identifier in
+// Gecko is continually increasing, and hit results from a stale tree will
+// not be valid on the new tree.
+var spamCount = 0;
+var spamPoint = null;
+function divSpammer() {
+ spamCount++;
+ if (spamCount >= SPAM_LIMIT) {
+ return;
+ }
+ setTimeout(divSpammer, 0);
+
+ // Remove the div if it exists...
+ var spamdiv = document.getElementById('spamdiv');
+ if (spamdiv) {
+ spamdiv.remove();
+ return;
+ }
+ // ... and add it if it doesn't exist.
+ spamdiv = document.createElement('div');
+ spamdiv.id = 'spamdiv';
+ spamdiv.appendChild(document.createElement('div'));
+ document.body.appendChild(spamdiv);
+ if (spamPoint == null) {
+ spamPoint = centerOf(spamdiv);
+ }
+}
+
+// This function does continuous hit-testing by scheduling itself over and
+// over with setTimeout(0). It hit-tests the same spot and expects to hit
+// either the root scrollframe (if the spamdiv is not present in that
+// instant) or the spamdiv (if it is present). If the spamdiv is hit, it
+// expects the scrollid to be non-decreasing.
+var rootScrollId = null;
+var lastScrollId = -1;
+function hitTestSpammer() {
+ if (spamCount >= SPAM_LIMIT) {
+ subtestDone();
+ return;
+ }
+ setTimeout(hitTestSpammer, 0);
+
+ if (spamPoint == null) {
+ // The very first invocation of this function will have spamPoint as null,
+ // and we use that to pick up the rootScrollId.
+ ok(rootScrollId == null, "This codepath shouldn't get hit twice");
+ rootScrollId = hitTest(centerOf(document.body)).scrollId;
+ ok(true, "Root scroll id detected as " + rootScrollId);
+ return;
+ }
+
+ var scrollId = hitTest(spamPoint).scrollId;
+ if (scrollId == rootScrollId) {
+ ok(true, "Hit test hit the root scroller, spamdiv is not in compositor");
+ } else {
+ is(scrollId >= lastScrollId, true, "spamdiv's scroll id is now " + scrollId);
+ lastScrollId = scrollId;
+ }
+}
+
+function startTest() {
+ // Make sure to run hitTestSpammer first so the first iteration is while
+ // spamPoint is still null.
+ setTimeout(hitTestSpammer, 0);
+ setTimeout(divSpammer, 0);
+}
+
+waitUntilApzStable().then(startTest);
+
+</script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_sticky_bug1478304.html b/gfx/layers/apz/test/mochitest/helper_hittest_sticky_bug1478304.html
new file mode 100644
index 0000000000..395c688a12
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_hittest_sticky_bug1478304.html
@@ -0,0 +1,58 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>APZ hit-testing with sticky element inside a transform (bug 1478304)</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <meta name="viewport" content="width=device-width"/>
+ <style>
+ #subframe {
+ width: 500px;
+ height: 200px;
+ overflow-y: auto;
+ }
+ #transform {
+ transform: translate(0);
+ }
+ #sticky {
+ background-color: white;
+ position: sticky;
+ top: 0;
+ }
+ #spacer {
+ width: 100px;
+ height: 1000px;
+ }
+ </style>
+</head>
+<body>
+ <div id="subframe">
+ <div id="transform">
+ <div id="sticky">sticky with transformed parent (click me or hover me and try a scroll)</div>
+ <div id="spacer"></div>
+ </div>
+ </div>
+</body>
+<script type="application/javascript">
+
+async function test() {
+ var utils = getHitTestConfig().utils;
+
+ var subframe = document.getElementById("subframe");
+ var sticky = document.getElementById("sticky");
+
+ checkHitResult(
+ hitTest(centerOf(sticky)),
+ APZHitResultFlags.VISIBLE,
+ utils.getViewId(subframe),
+ utils.getLayersId(),
+ "subframe was successfully hit");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+</script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_touchaction.html b/gfx/layers/apz/test/mochitest/helper_hittest_touchaction.html
new file mode 100644
index 0000000000..acabfcc07b
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_hittest_touchaction.html
@@ -0,0 +1,353 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Testing APZ hit-test with touch-action boxes</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <meta name="viewport" content="width=device-width"/>
+ <style>
+.taBox {
+ width: 20px;
+ height: 20px;
+ background-color: green;
+ display: inline-block;
+}
+.taBox > div {
+ /* make sure this doesn't obscure the center of the enclosing taBox */
+ width: 5px;
+ height: 5px;
+ background-color: blue;
+}
+
+.taBigBox {
+ width: 150px;
+ height: 150px;
+ background-color: green;
+ display: inline-block;
+}
+.taBigBox > div {
+ width: 40px;
+ height: 40px;
+ overflow: auto;
+}
+.taBigBox > div > div {
+ width: 100px;
+ height: 100px;
+}
+ </style>
+</head>
+<body>
+<!-- Create a bunch of divs for hit-testing. Some of the divs also
+ contain nested divs to test inheritance/combination of touch-action
+ properties. This is not an exhaustive list of all the possible
+ combinations but it's assorted enough to provide good coverage. -->
+ <div id="taNone" class="taBox" style="touch-action: none">
+ <div id="taInnerNonePanX" style="touch-action: pan-x"></div>
+ <div id="taInnerNoneManip" style="touch-action: manipulation"></div>
+ </div>
+ <div id="taPanX" class="taBox" style="touch-action: pan-x">
+ <div id="taInnerPanXY" style="touch-action: pan-y"></div>
+ <div id="taInnerPanXManip" style="touch-action: manipulation"></div>
+ </div>
+ <div id="taPanY" class="taBox" style="touch-action: pan-y">
+ <div id="taInnerPanYX" style="touch-action: pan-x"></div>
+ <div id="taInnerPanYY" style="touch-action: pan-y"></div>
+ </div>
+ <div id="taPanXY" class="taBox" style="touch-action: pan-x pan-y">
+ <div id="taInnerPanXYNone" style="touch-action: none"></div>
+ </div>
+ <div id="taManip" class="taBox" style="touch-action: manipulation">
+ <div id="taInnerManipPanX" style="touch-action: pan-x"></div>
+ <div id="taInnerManipNone" style="touch-action: none"></div>
+ <div id="taInnerManipListener" ontouchstart="return false;"></div>
+ </div>
+ <div id="taListener" class="taBox" ontouchstart="return false;">
+ <div id="taInnerListenerPanX" style="touch-action: pan-x"></div>
+ </div>
+ <div id="taPinchZoom" class="taBox" style="touch-action: pinch-zoom">
+ </div>
+
+ <br/>
+
+ <!-- More divs for hit-testing. These comprise one big outer div with
+ a touch-action property, then inside is a scrollable div, possibly with
+ a touch-action of its own, and inside that is another div to make the
+ scrollable div scrollable. Note that the touch-action for an element
+ includes the restrictions from all ancestor elements up to and including
+ the element that has the default action for that touch-action property.
+ Panning actions therefore get inherited from the nearest scrollframe
+ downwards, while zooming actions get inherited from the root content
+ element (because that's the only one that has zooming as the default action)
+ downwards. In effect, any pan restrictions on the outer div should not
+ apply to the inner div, but zooming restrictions should. Also, the
+ touch-action on the scrollable div itself should apply to user interaction
+ inside the scrollable div. -->
+ <div id="taOuterPanX" class="taBigBox" style="touch-action: pan-x">
+ <div id="taScrollerPanY" style="touch-action: pan-y">
+ <div></div>
+ </div>
+ <div id="taScroller">
+ <div style="touch-action: pan-y"></div>
+ </div>
+ <div id="taScroller2">
+ <div></div>
+ </div>
+ </div>
+ <div id="taOuterManipulation" class="taBigBox" style="touch-action: manipulation">
+ <div id="taScrollerPanX" style="touch-action: pan-x">
+ <div></div>
+ </div>
+ <div id="taScroller3">
+ <div></div>
+ </div>
+ <div id="taScroller4" style="touch-action: pan-y">
+ <div style="overflow:hidden"></div>
+ </div>
+ </div>
+</body>
+<script type="application/javascript">
+
+var config = getHitTestConfig();
+
+async function test() {
+ for (var scroller of document.querySelectorAll(".taBigBox > div")) {
+ // layerize all the scrollable divs
+ config.utils.setDisplayPortForElement(0, 0, 100, 100, scroller, 1);
+ }
+ await promiseApzFlushedRepaints();
+
+ var scrollId = config.utils.getViewId(document.scrollingElement);
+ var layersId = config.utils.getLayersId();
+
+ checkHitResult(
+ hitTest(centerOf("taNone")),
+ APZHitResultFlags.VISIBLE |
+ APZHitResultFlags.PAN_X_DISABLED |
+ APZHitResultFlags.PAN_Y_DISABLED |
+ APZHitResultFlags.PINCH_ZOOM_DISABLED |
+ APZHitResultFlags.DOUBLE_TAP_ZOOM_DISABLED,
+ scrollId,
+ layersId,
+ "touch-action: none");
+ checkHitResult(
+ hitTest(centerOf("taInnerNonePanX")),
+ APZHitResultFlags.VISIBLE |
+ APZHitResultFlags.PAN_X_DISABLED |
+ APZHitResultFlags.PAN_Y_DISABLED |
+ APZHitResultFlags.PINCH_ZOOM_DISABLED |
+ APZHitResultFlags.DOUBLE_TAP_ZOOM_DISABLED,
+ scrollId,
+ layersId,
+ "touch-action: pan-x inside touch-action: none");
+ checkHitResult(
+ hitTest(centerOf("taInnerNoneManip")),
+ APZHitResultFlags.VISIBLE |
+ APZHitResultFlags.PAN_X_DISABLED |
+ APZHitResultFlags.PAN_Y_DISABLED |
+ APZHitResultFlags.PINCH_ZOOM_DISABLED |
+ APZHitResultFlags.DOUBLE_TAP_ZOOM_DISABLED,
+ scrollId,
+ layersId,
+ "touch-action: manipulation inside touch-action: none");
+
+ checkHitResult(
+ hitTest(centerOf("taPanX")),
+ APZHitResultFlags.VISIBLE |
+ APZHitResultFlags.PAN_Y_DISABLED |
+ APZHitResultFlags.PINCH_ZOOM_DISABLED |
+ APZHitResultFlags.DOUBLE_TAP_ZOOM_DISABLED,
+ scrollId,
+ layersId,
+ "touch-action: pan-x");
+ checkHitResult(
+ hitTest(centerOf("taInnerPanXY")),
+ APZHitResultFlags.VISIBLE |
+ APZHitResultFlags.PAN_X_DISABLED |
+ APZHitResultFlags.PAN_Y_DISABLED |
+ APZHitResultFlags.PINCH_ZOOM_DISABLED |
+ APZHitResultFlags.DOUBLE_TAP_ZOOM_DISABLED,
+ scrollId,
+ layersId,
+ "touch-action: pan-y inside touch-action: pan-x");
+ checkHitResult(
+ hitTest(centerOf("taInnerPanXManip")),
+ APZHitResultFlags.VISIBLE |
+ APZHitResultFlags.PAN_Y_DISABLED |
+ APZHitResultFlags.PINCH_ZOOM_DISABLED |
+ APZHitResultFlags.DOUBLE_TAP_ZOOM_DISABLED,
+ scrollId,
+ layersId,
+ "touch-action: manipulation inside touch-action: pan-x");
+
+ checkHitResult(
+ hitTest(centerOf("taPanY")),
+ APZHitResultFlags.VISIBLE |
+ APZHitResultFlags.PAN_X_DISABLED |
+ APZHitResultFlags.PINCH_ZOOM_DISABLED |
+ APZHitResultFlags.DOUBLE_TAP_ZOOM_DISABLED,
+ scrollId,
+ layersId,
+ "touch-action: pan-y");
+ checkHitResult(
+ hitTest(centerOf("taInnerPanYX")),
+ APZHitResultFlags.VISIBLE |
+ APZHitResultFlags.PAN_X_DISABLED |
+ APZHitResultFlags.PAN_Y_DISABLED |
+ APZHitResultFlags.PINCH_ZOOM_DISABLED |
+ APZHitResultFlags.DOUBLE_TAP_ZOOM_DISABLED,
+ scrollId,
+ layersId,
+ "touch-action: pan-x inside touch-action: pan-y");
+ checkHitResult(
+ hitTest(centerOf("taInnerPanYY")),
+ APZHitResultFlags.VISIBLE |
+ APZHitResultFlags.PAN_X_DISABLED |
+ APZHitResultFlags.PINCH_ZOOM_DISABLED |
+ APZHitResultFlags.DOUBLE_TAP_ZOOM_DISABLED,
+ scrollId,
+ layersId,
+ "touch-action: pan-y inside touch-action: pan-y");
+
+ checkHitResult(
+ hitTest(centerOf("taPanXY")),
+ APZHitResultFlags.VISIBLE |
+ APZHitResultFlags.PINCH_ZOOM_DISABLED |
+ APZHitResultFlags.DOUBLE_TAP_ZOOM_DISABLED,
+ scrollId,
+ layersId,
+ "touch-action: pan-x pan-y");
+ checkHitResult(
+ hitTest(centerOf("taInnerPanXYNone")),
+ APZHitResultFlags.VISIBLE |
+ APZHitResultFlags.PAN_X_DISABLED |
+ APZHitResultFlags.PAN_Y_DISABLED |
+ APZHitResultFlags.PINCH_ZOOM_DISABLED |
+ APZHitResultFlags.DOUBLE_TAP_ZOOM_DISABLED,
+ scrollId,
+ layersId,
+ "touch-action: none inside touch-action: pan-x pan-y");
+
+ checkHitResult(
+ hitTest(centerOf("taManip")),
+ APZHitResultFlags.VISIBLE |
+ APZHitResultFlags.DOUBLE_TAP_ZOOM_DISABLED,
+ scrollId,
+ layersId,
+ "touch-action: manipulation");
+ checkHitResult(
+ hitTest(centerOf("taInnerManipPanX")),
+ APZHitResultFlags.VISIBLE |
+ APZHitResultFlags.PAN_Y_DISABLED |
+ APZHitResultFlags.PINCH_ZOOM_DISABLED |
+ APZHitResultFlags.DOUBLE_TAP_ZOOM_DISABLED,
+ scrollId,
+ layersId,
+ "touch-action: pan-x inside touch-action: manipulation");
+ checkHitResult(
+ hitTest(centerOf("taInnerManipNone")),
+ APZHitResultFlags.VISIBLE |
+ APZHitResultFlags.PAN_X_DISABLED |
+ APZHitResultFlags.PAN_Y_DISABLED |
+ APZHitResultFlags.PINCH_ZOOM_DISABLED |
+ APZHitResultFlags.DOUBLE_TAP_ZOOM_DISABLED,
+ scrollId,
+ layersId,
+ "touch-action: none inside touch-action: manipulation");
+ checkHitResult(
+ hitTest(centerOf("taInnerManipListener")),
+ APZHitResultFlags.VISIBLE |
+ APZHitResultFlags.APZ_AWARE_LISTENERS |
+ APZHitResultFlags.DOUBLE_TAP_ZOOM_DISABLED,
+ scrollId,
+ layersId,
+ "div with touch listener inside touch-action: manipulation");
+
+ checkHitResult(
+ hitTest(centerOf("taListener")),
+ APZHitResultFlags.VISIBLE |
+ APZHitResultFlags.APZ_AWARE_LISTENERS,
+ scrollId,
+ layersId,
+ "div with touch listener");
+ checkHitResult(
+ hitTest(centerOf("taInnerListenerPanX")),
+ APZHitResultFlags.VISIBLE |
+ APZHitResultFlags.APZ_AWARE_LISTENERS |
+ APZHitResultFlags.PAN_Y_DISABLED |
+ APZHitResultFlags.PINCH_ZOOM_DISABLED |
+ APZHitResultFlags.DOUBLE_TAP_ZOOM_DISABLED,
+ scrollId,
+ layersId,
+ "touch-action: pan-x inside div with touch listener");
+
+ checkHitResult(
+ hitTest(centerOf("taPinchZoom")),
+ APZHitResultFlags.VISIBLE |
+ APZHitResultFlags.PAN_Y_DISABLED |
+ APZHitResultFlags.PAN_X_DISABLED |
+ APZHitResultFlags.DOUBLE_TAP_ZOOM_DISABLED,
+ scrollId,
+ layersId,
+ "touch-action: pinch-zoom inside div with touch listener");
+
+ checkHitResult(
+ hitTest(centerOf("taScrollerPanY")),
+ APZHitResultFlags.VISIBLE |
+ APZHitResultFlags.PAN_X_DISABLED |
+ APZHitResultFlags.PINCH_ZOOM_DISABLED |
+ APZHitResultFlags.DOUBLE_TAP_ZOOM_DISABLED,
+ config.utils.getViewId(document.getElementById("taScrollerPanY")),
+ layersId,
+ "touch-action: pan-y on scroller");
+ checkHitResult(
+ hitTest(centerOf("taScroller")),
+ APZHitResultFlags.VISIBLE |
+ APZHitResultFlags.PAN_X_DISABLED |
+ APZHitResultFlags.PINCH_ZOOM_DISABLED |
+ APZHitResultFlags.DOUBLE_TAP_ZOOM_DISABLED,
+ config.utils.getViewId(document.getElementById("taScroller")),
+ layersId,
+ "touch-action: pan-y on div inside scroller");
+ checkHitResult(
+ hitTest(centerOf("taScroller2")),
+ APZHitResultFlags.VISIBLE |
+ APZHitResultFlags.PINCH_ZOOM_DISABLED |
+ APZHitResultFlags.DOUBLE_TAP_ZOOM_DISABLED,
+ config.utils.getViewId(document.getElementById("taScroller2")),
+ layersId,
+ "zooming restrictions from pan-x outside scroller get inherited in");
+
+ checkHitResult(
+ hitTest(centerOf("taScrollerPanX")),
+ APZHitResultFlags.VISIBLE |
+ APZHitResultFlags.PAN_Y_DISABLED |
+ APZHitResultFlags.PINCH_ZOOM_DISABLED |
+ APZHitResultFlags.DOUBLE_TAP_ZOOM_DISABLED,
+ config.utils.getViewId(document.getElementById("taScrollerPanX")),
+ layersId,
+ "touch-action: pan-x on scroller inside manipulation");
+ checkHitResult(
+ hitTest(centerOf("taScroller3")),
+ APZHitResultFlags.VISIBLE |
+ APZHitResultFlags.DOUBLE_TAP_ZOOM_DISABLED,
+ config.utils.getViewId(document.getElementById("taScroller3")),
+ layersId,
+ "touch-action: manipulation outside scroller gets inherited in");
+ checkHitResult(
+ hitTest(centerOf("taScroller4")),
+ APZHitResultFlags.VISIBLE |
+ APZHitResultFlags.PAN_X_DISABLED |
+ APZHitResultFlags.PINCH_ZOOM_DISABLED |
+ APZHitResultFlags.DOUBLE_TAP_ZOOM_DISABLED,
+ config.utils.getViewId(document.getElementById("taScroller4")),
+ layersId,
+ "overflow:hidden div doesn't reset pan-x/pan-y from enclosing scroller");
+}
+
+waitUntilApzStable()
+ .then(test)
+ .then(subtestDone, subtestFailed);
+
+</script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_horizontal_checkerboard.html b/gfx/layers/apz/test/mochitest/helper_horizontal_checkerboard.html
new file mode 100644
index 0000000000..0355243738
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_horizontal_checkerboard.html
@@ -0,0 +1,65 @@
+<!DOCTYPE html>
+<html lang="en"><head>
+<meta http-equiv="content-type" content="text/html; charset=UTF-8"><meta charset="utf-8">
+<title>Testcase for checkerboarding during horizontal scrolling</title>
+<script type="application/javascript" src="apz_test_utils.js"></script>
+<script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+<script src="/tests/SimpleTest/paint_listener.js"></script>
+<style>
+
+.scrollbox {
+ margin: 50px;
+ border: 2px solid black;
+ background: red;
+ width: 1120px;
+ height: 200px;
+ overflow: auto;
+}
+
+.scrolled {
+ width: 20000px;
+ height: 200px;
+ background: lime;
+}
+
+</style>
+
+</head><body>
+ <div class="scrollbox"><div class="scrolled"></div></div>
+</body>
+
+<script type="application/javascript">
+async function test() {
+ var scroller = document.querySelector(".scrollbox");
+ var utils = SpecialPowers.getDOMWindowUtils(window);
+ var scrollerId = utils.getViewId(scroller);
+
+ // This test contains a wide horizontal scroll box and scrolls it horizontally
+ // from right to left. The size of the box is chosen so that the displayport
+ // snapping logic in nsLayoutUtils.cpp would tries an horizontal alignment larger
+ // than the margins. In such a situation we want to make sure the displayport
+ // alignment is adjusted so we don't snap too far which would cause content to
+ // be missed on the right side.
+
+ // The scroll values here just need to be "thorough" enough to exercise the
+ // code at different alignments, so using a non-power-of-two or prime number
+ // for the increment seems like a good idea. The smaller the increment, the
+ // longer the test takes to run (because more iterations) so we don't want it
+ // too small either.
+ // The scroll box is rather wide so we only scroll a portion of it so that the
+ // test doesn't run for too long.
+ var maxX = scroller.scrollLeftMax / 6;
+ for (var x = maxX; x > 0; x -= 71) {
+ dump(`Scrolling scroller to ${x}\n`);
+ scroller.scrollTo(x, 0);
+ await promiseApzFlushedRepaints();
+ assertNotCheckerboarded(utils, scrollerId, `At x=${x}`);
+ }
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+</script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_iframe1.html b/gfx/layers/apz/test/mochitest/helper_iframe1.html
new file mode 100644
index 0000000000..047da96bd4
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_iframe1.html
@@ -0,0 +1,14 @@
+<!DOCTYPE HTML>
+<!-- The purpose of the 'id' on the HTML element is to get something
+ identifiable to show up in the root scroll frame's content description,
+ so we can check it for layerization. -->
+<html id="outer3">
+ <head>
+ <link rel="stylesheet" type="text/css" href="helper_subframe_style.css"/>
+ </head>
+ <body>
+ <div id="inner3" class="inner-frame">
+ <div class="inner-content"></div>
+ </div>
+ </body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_iframe2.html b/gfx/layers/apz/test/mochitest/helper_iframe2.html
new file mode 100644
index 0000000000..fee3883e95
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_iframe2.html
@@ -0,0 +1,14 @@
+<!DOCTYPE HTML>
+<!-- The purpose of the 'id' on the HTML element is to get something
+ identifiable to show up in the root scroll frame's content description,
+ so we can check it for layerization. -->
+<html id="outer4">
+ <head>
+ <link rel="stylesheet" type="text/css" href="helper_subframe_style.css"/>
+ </head>
+ <body>
+ <div id="inner4" class="inner-frame">
+ <div class="inner-content"></div>
+ </div>
+ </body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_iframe_pan.html b/gfx/layers/apz/test/mochitest/helper_iframe_pan.html
new file mode 100644
index 0000000000..032133c255
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_iframe_pan.html
@@ -0,0 +1,49 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Sanity panning test for scrollable div</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+async function test() {
+ var outer = document.getElementById("outer");
+ var transformEndPromise = promiseTransformEnd();
+
+ await synthesizeNativeTouchDrag(outer.contentDocument.body, 10, 100, 0, -50);
+ dump("Finished native drag, waiting for transform-end observer...\n");
+
+ await transformEndPromise;
+
+ dump("Transform complete; flushing repaints...\n");
+ await promiseOnlyApzControllerFlushed(outer.contentWindow);
+
+ var outerScroll = outer.contentWindow.scrollY;
+ if (getPlatform() == "windows") {
+ // On windows, because we run this test with native event synthesization,
+ // Windows can end up eating the first touchmove which can result in the
+ // scroll amount being slightly smaller than 50px. See bug 1388955.
+ dump("iframe scrolled " + outerScroll + "px");
+ ok(outerScroll > 45, "iframe scrolled at least 45 px");
+ ok(outerScroll <= 50, "iframe scrolled at most 50 px");
+ } else {
+ is(outerScroll, 50, "check that the iframe scrolled");
+ }
+}
+
+waitUntilApzStable()
+ .then(test)
+ .then(subtestDone);
+
+ </script>
+</head>
+<body>
+ <iframe id="outer" style="height: 250px; border: solid 1px black" srcdoc="<body style='height:5000px'>"></iframe>
+ <div style="height: 5000px; background-color: lightgreen;">
+ This div makes the top-level page scrollable.
+ </div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_iframe_textarea.html b/gfx/layers/apz/test/mochitest/helper_iframe_textarea.html
new file mode 100644
index 0000000000..f4a934db74
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_iframe_textarea.html
@@ -0,0 +1,12 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <link rel="stylesheet" type="text/css" href="helper_subframe_style.css"/>
+ </head>
+ <body>
+ <div style="height: 8000px;">ABC</div>
+ <textarea rows="20"></textarea>
+ <!-- Leave additional room below the element so it can be scrolled to the center -->
+ <div style="height: 1000px;">ABC</div>
+ </body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_interrupted_reflow.html b/gfx/layers/apz/test/mochitest/helper_interrupted_reflow.html
new file mode 100644
index 0000000000..0dcaf1d67d
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_interrupted_reflow.html
@@ -0,0 +1,712 @@
+<!DOCTYPE html>
+<html>
+ <!--
+ https://bugzilla.mozilla.org/show_bug.cgi?id=1292781
+ -->
+ <head>
+ <title>Test for bug 1292781</title>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <style>
+ .outer {
+ height: 400px;
+ width: 415px;
+ overflow: hidden;
+ position: relative;
+ }
+ .inner {
+ height: 100%;
+ outline: none;
+ overflow-x: hidden;
+ overflow-y: scroll;
+ position: relative;
+ }
+ .inner div:nth-child(even) {
+ background-color: lightblue;
+ }
+ .inner div:nth-child(odd) {
+ background-color: lightgreen;
+ }
+ .outer.contentBefore::before {
+ top: 0;
+ content: '';
+ display: block;
+ height: 2px;
+ position: absolute;
+ width: 100%;
+ z-index: 99;
+ }
+ </style>
+ </head>
+ <body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1292781">Mozilla Bug 1292781</a>
+<p id="display"></p>
+<div id="content">
+ <p>The frame reconstruction should not leave this scrollframe in a bad state</p>
+ <div class="outer">
+ <div class="inner">
+ this is the top of the scrollframe.
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ this is near the top of the scrollframe.
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ this is near the bottom of the scrollframe.
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ this is the bottom of the scrollframe.
+ </div>
+ </div>
+</div>
+
+<pre id="test">
+<script type="text/javascript">
+
+const is = window.opener.is;
+const ok = window.opener.ok;
+const SimpleTest = window.opener.SimpleTest;
+
+// Returns a list of async scroll offsets that the |inner| element had, one for
+// each paint.
+function getAsyncScrollOffsets(aPaintsToIgnore) {
+ var offsets = [];
+ var compositorTestData = SpecialPowers.getDOMWindowUtils(window).getCompositorAPZTestData();
+ var buckets = compositorTestData.paints.slice(aPaintsToIgnore);
+ ok(buckets.length >= 3, "Expected at least three paints in the compositor test data");
+ var childIsLayerized = false;
+ for (var i = 0; i < buckets.length; ++i) {
+ var apzcTree = buildApzcTree(convertScrollFrameData(buckets[i].scrollFrames));
+ var rcd = findRcdNode(apzcTree);
+ if (rcd == null) {
+ continue;
+ }
+ if (rcd.children.length) {
+ // The child may not be layerized in the first few paints, but once it is
+ // layerized, it should stay layerized.
+ childIsLayerized = true;
+ }
+ if (!childIsLayerized) {
+ continue;
+ }
+
+ ok(rcd.children.length == 1, "Root content APZC has exactly one child");
+ offsets.push(parsePoint(rcd.children[0].asyncScrollOffset));
+ }
+ return offsets;
+}
+
+async function test() {
+ var utils = SpecialPowers.DOMWindowUtils;
+
+ // The APZ test data accumulates whenever a test turns it on. We just want
+ // the data for this test, so we check how many frames are already recorded
+ // and discard those later.
+ var framesToSkip = SpecialPowers.getDOMWindowUtils(window).getCompositorAPZTestData().paints.length;
+
+ var elm = document.getElementsByClassName("inner")[0];
+ // Set a zero-margin displayport to ensure that the element is async-scrollable
+ // otherwise on Fennec it is not
+ utils.setDisplayPortMarginsForElement(0, 0, 0, 0, elm, 0);
+
+ var maxScroll = elm.scrollTopMax;
+ elm.scrollTop = maxScroll;
+ await promiseAllPaintsDone();
+ await promiseOnlyApzControllerFlushed();
+
+ // Take control of the refresh driver
+ utils.advanceTimeAndRefresh(0);
+
+ // Force the next reflow to get interrupted
+ utils.forceReflowInterrupt();
+
+ // Make a change that triggers frame reconstruction, and then tick the refresh
+ // driver so that layout processes the pending restyles and then runs an
+ // interruptible reflow. That reflow *will* be interrupted (because of the flag
+ // we set above), and we should end up with a transient 0,0 scroll offset
+ // being sent to the compositor.
+ elm.parentNode.classList.add("contentBefore");
+ utils.advanceTimeAndRefresh(0);
+ // On android, and maybe non-e10s platforms generally, we need to manually
+ // kick the paint to send the layer transaction to the compositor.
+ await promiseAllPaintsDone();
+
+ // Read the main-thread scroll offset; although this is temporarily 0,0 that
+ // temporary value is never exposed to content - instead reading this value
+ // will finish doing the interrupted reflow from above and then report the
+ // correct scroll offset.
+ is(elm.scrollTop, maxScroll, "Main-thread scroll position was restored");
+
+ // .. and now flush everything to make sure the state gets pushed over to the
+ // compositor and APZ as well.
+ utils.restoreNormalRefresh();
+ await promiseApzFlushedRepaints();
+
+ // Now we pull the compositor data and check it. What we expect to see is that
+ // the scroll position goes to maxScroll, then drops to 0 and then goes back
+ // to maxScroll. This test is specifically testing that last bit - that it
+ // properly gets restored from 0 to maxScroll.
+ // The one hitch is that on Android this page is loaded with some amount of
+ // zoom, and the async scroll is in ParentLayerPixel coordinates, so it will
+ // not match maxScroll exactly. Since we can't reliably compute what that
+ // ParentLayer scroll will be, we just make sure the async scroll is nonzero
+ // and use the first value we encounter to verify that it got restored properly.
+ // The other alternative is to spawn this test into a new window with 1.0 zoom
+ // but I'm tired of doing that for pretty much every test.
+ var state = 0;
+ var asyncScrollOffsets = getAsyncScrollOffsets(framesToSkip);
+ dump("Got scroll offsets: " + JSON.stringify(asyncScrollOffsets) + "\n");
+ var maxScrollParentLayerPixels = maxScroll;
+ while (asyncScrollOffsets.length) {
+ let offset = asyncScrollOffsets.shift();
+ switch (state) {
+ // 0 is the initial state, the scroll offset might be zero but should
+ // become non-zero from when we set scrollTop to scrollTopMax
+ case 0:
+ if (offset.y == 0) {
+ break;
+ }
+ if (getPlatform() == "android") {
+ ok(offset.y > 0, "Async scroll y of scrollframe is " + offset.y);
+ maxScrollParentLayerPixels = offset.y;
+ } else {
+ is(offset.y, maxScrollParentLayerPixels, "Async scroll y of scrollframe is " + offset.y);
+ }
+ state = 1;
+ break;
+
+ // state 1 starts out at maxScrollParentLayerPixels, should drop to 0
+ // because of the interrupted reflow putting the scroll into a transient
+ // zero state
+ case 1:
+ if (offset.y == maxScrollParentLayerPixels) {
+ break;
+ }
+ is(offset.y, 0, "Async scroll position was temporarily 0");
+ state = 2;
+ break;
+
+ // state 2 starts out the transient 0 scroll offset, and we expect the
+ // scroll position to get restored back to maxScrollParentLayerPixels
+ case 2:
+ if (offset.y == 0) {
+ break;
+ }
+ is(offset.y, maxScrollParentLayerPixels, "Async scroll y of scrollframe restored to " + offset.y);
+ state = 3;
+ break;
+
+ // Terminal state. The scroll position should stay at maxScrollParentLayerPixels
+ case 3:
+ is(offset.y, maxScrollParentLayerPixels, "Scroll position maintained");
+ break;
+ }
+ }
+ is(state, 3, "The scroll position did drop to 0 and then get restored properly");
+
+ window.opener.finishTest();
+}
+
+waitUntilApzStable()
+.then(async () => test());
+
+</script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_key_scroll.html b/gfx/layers/apz/test/mochitest/helper_key_scroll.html
new file mode 100644
index 0000000000..021e2803b7
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_key_scroll.html
@@ -0,0 +1,109 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1383365
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Async key scrolling test, helper page</title>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript">
+ // --------------------------------------------------------------------
+ // Async key scrolling test
+ //
+ // This test checks that a key scroll occurs asynchronously.
+ //
+ // The page contains a <div> that is large enough to make the page
+ // scrollable. We first synthesize a page down to scroll to the bottom
+ // of the page. Once we have reached the bottom of the page, we synthesize
+ // a page up to get us back to the top of the page.
+ //
+ // Once at the top, we request test data from APZ, rebuild the APZC tree
+ // structure, and use it to check that async key scrolling happened.
+ // --------------------------------------------------------------------
+
+ async function test() {
+ // Sanity check
+ is(checkHasAsyncKeyScrolled(), false, "expected no async key scrolling before test");
+
+ // Send a key to initiate a page scroll to take us to the bottom of the
+ // page. This scroll is done synchronously because APZ doesn't have
+ // current focus state at page load.
+ let scrollBottomPromise = new Promise(resolve => {
+ let checkBottom = function(e) {
+ if (window.scrollY < window.scrollMaxY) {
+ return;
+ }
+ info("Reached final scroll position of sync KEY_End scroll");
+ window.removeEventListener("scroll", checkBottom);
+ resolve();
+ };
+ window.addEventListener("scroll", checkBottom);
+ });
+
+ window.synthesizeKey("KEY_End");
+ await scrollBottomPromise;
+
+ // Spin the refresh driver a few times, so that the AsyncScroll instance
+ // that was running the main-thread scroll animation finishes up and
+ // triggers any repaints that it needs to.
+ var utils = SpecialPowers.DOMWindowUtils;
+ for (var i = 0; i < 10; i++) {
+ utils.advanceTimeAndRefresh(50);
+ }
+ utils.restoreNormalRefresh();
+
+ // Wait for the APZ to reach a stable state as well, before dispatching
+ // the next key input or the default action won't occur.
+ await promiseApzFlushedRepaints();
+
+ is(checkHasAsyncKeyScrolled(), false, "expected no async key scrolling before KEY_Home dispatch");
+
+ // This scroll should be asynchronous now that the focus state is up to date.
+ let scrollTopPromise = new Promise(resolve => {
+ let checkTop = function(e) {
+ if (window.scrollY > 0) {
+ return;
+ }
+ info("Reached final scroll position of async KEY_Home scroll");
+ window.removeEventListener("scroll", checkTop);
+ resolve();
+ };
+ window.addEventListener("scroll", checkTop);
+ });
+
+ window.synthesizeKey("KEY_Home");
+ await scrollTopPromise;
+
+ // Wait for APZ to settle and then check that async scrolling happened.
+ await promiseApzFlushedRepaints();
+ is(checkHasAsyncKeyScrolled(), true, "expected async key scrolling after test");
+ }
+
+ function checkHasAsyncKeyScrolled() {
+ // Reconstruct the APZC tree structure in the last paint.
+ var apzcTree = getLastApzcTree();
+ var rcd = findRcdNode(apzcTree);
+
+ if (rcd) {
+ return rcd.hasAsyncKeyScrolled === "1";
+ }
+
+ info("Last paint rcd is null");
+ return false;
+ }
+
+ waitUntilApzStable()
+ .then(forceLayerTreeToCompositor)
+ .then(test)
+ .then(subtestDone, subtestFailed);
+ </script>
+</head>
+<body style="height: 500px; overflow: scroll">
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1383365">Async key scrolling test</a>
+ <!-- Put enough content into the page to make it have a nonzero scroll range -->
+ <div style="height: 5000px"></div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_long_tap.html b/gfx/layers/apz/test/mochitest/helper_long_tap.html
new file mode 100644
index 0000000000..2fdb2472ec
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_long_tap.html
@@ -0,0 +1,166 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Ensure we get a touch-cancel after a contextmenu comes up</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+function addMouseEventListeners(aTarget) {
+ aTarget.addEventListener("mousemove", recordEvent, true);
+ aTarget.addEventListener("mouseover", recordEvent, true);
+ aTarget.addEventListener("mouseenter", recordEvent, true);
+ aTarget.addEventListener("mouseout", recordEvent, true);
+ aTarget.addEventListener("mouseleave", recordEvent, true);
+}
+
+function removeMouseEventListeners(aTarget) {
+ aTarget.removeEventListener("mousemove", recordEvent, true);
+ aTarget.removeEventListener("mouseover", recordEvent, true);
+ aTarget.removeEventListener("mouseenter", recordEvent, true);
+ aTarget.removeEventListener("mouseout", recordEvent, true);
+ aTarget.removeEventListener("mouseleave", recordEvent, true);
+}
+
+async function longPressLink() {
+ let target = document.getElementById("b");
+ addMouseEventListeners(target);
+ await synthesizeNativeTouch(target, 5, 5, SpecialPowers.DOMWindowUtils.TOUCH_CONTACT, function() {
+ dump("Finished synthesizing touch-start, waiting for events...\n");
+ });
+}
+
+var eventsFired = 0;
+function recordEvent(e) {
+ let target = document.getElementById("b");
+ const platform = getPlatform();
+ if (platform == "windows") {
+ // On Windows we get a mouselongtap event once the long-tap has been detected
+ // by APZ, and that's what we use as the trigger to lift the finger. That then
+ // triggers the contextmenu. This matches the platform convention.
+ switch (eventsFired) {
+ case 0: is(e.type, "touchstart", "Got a touchstart"); break;
+ case 1:
+ is(e.type, "mouselongtap", "Got a mouselongtap");
+ setTimeout(async () => {
+ await synthesizeNativeTouch(document.getElementById("b"), 5, 5, SpecialPowers.DOMWindowUtils.TOUCH_REMOVE);
+ }, 0);
+ break;
+ case 2: is(e.type, "touchend", "Got a touchend"); break;
+ case 3: is(e.type, "mouseover", "Got a mouseover"); break;
+ case 4: is(e.type, "mouseenter", "Got a mouseenter"); break;
+ case 5: is(e.type, "mousemove", "Got a mousemove"); break;
+ case 6: is(e.type, "contextmenu", "Got a contextmenu"); e.preventDefault(); break;
+ default: ok(false, "Got an unexpected event of type " + e.type); break;
+ }
+ eventsFired++;
+
+ if (eventsFired == 7) {
+ removeMouseEventListeners(target);
+ dump("Finished waiting for events, doing an APZ flush to see if any more unexpected events come through...\n");
+ promiseOnlyApzControllerFlushed().then(function() {
+ dump("Done APZ flush, ending test...\n");
+ subtestDone();
+ });
+ }
+ } else if (platform != "android") {
+ // On non-Windows desktop platforms we get a contextmenu event once the
+ // long-tap has been detected. Since we prevent-default that, we don't get
+ // a mouselongtap event at all, and instead get a touchcancel.
+ switch (eventsFired) {
+ case 0: is(e.type, "touchstart", "Got a touchstart"); break;
+ case 1: is(e.type, "mouseover", "Got a mouseover"); break;
+ case 2: is(e.type, "mouseenter", "Got a mouseenter"); break;
+ case 3: is(e.type, "mousemove", "Got a mousemove"); break;
+ case 4: is(e.type, "contextmenu", "Got a contextmenu"); e.preventDefault(); break;
+ case 5: is(e.type, "touchcancel", "Got a touchcancel"); break;
+ default: ok(false, "Got an unexpected event of type " + e.type); break;
+ }
+ eventsFired++;
+
+ if (eventsFired == 6) {
+ removeMouseEventListeners(target);
+ setTimeout(async () => {
+ await synthesizeNativeTouch(target, 5, 5, SpecialPowers.DOMWindowUtils.TOUCH_REMOVE, function() {
+ dump("Finished synthesizing touch-end, doing an APZ flush to see if any more unexpected events come through...\n");
+ promiseOnlyApzControllerFlushed().then(function() {
+ dump("Done APZ flush, ending test...\n");
+ subtestDone();
+ });
+ });
+ }, 0)
+ }
+ } else {
+ // On Android we get a contextmenu event once the long-tap has been
+ // detected. If contextmenu opens we get a touchcancel event, and if
+ // contextmenu didn't open because of preventDefault() in the content,
+ // we will not get the touchcancel event.
+ switch (eventsFired) {
+ case 0: is(e.type, "touchstart", "Got a touchstart"); break;
+ case 1: is(e.type, "mouseover", "Got a mouseover"); break;
+ case 2: is(e.type, "mouseenter", "Got a mouseenter"); break;
+ case 3: is(e.type, "mousemove", "Got a mousemove"); break;
+ case 4: is(e.type, "contextmenu", "Got a contextmenu");
+ // Do preventDefault() in this content, thus we will not get any
+ // touchcancel event.
+ e.preventDefault();
+ setTimeout(async () => {
+ await synthesizeNativeTouch(target, 5, 5, SpecialPowers.DOMWindowUtils.TOUCH_REMOVE, function() {
+ dump("Finished synthesizing touch-end, waiting for a touchend event...\n");
+ });
+ }, 0);
+ break;
+ case 5: is(e.type, "touchend", "Got a touchend");
+ // Send another long press.
+ setTimeout(async () => {
+ await synthesizeNativeTouch(target, 5, 5, SpecialPowers.DOMWindowUtils.TOUCH_CONTACT, function() {
+ dump("Finished synthesizing touch-start, waiting for events...\n");
+ });
+ }, 0);
+ break;
+ case 6: is(e.type, "touchstart", "Got another touchstart"); break;
+ // NOTE: In this another event case, we don't get mouseover or moveenter
+ // event either since the target element hasn't been changed.
+ case 7: is(e.type, "mousemove", "Got another mousemove"); break;
+ case 8: is(e.type, "contextmenu", "Got another contextmenu");
+ // DON'T DO preventDefault() this time, thus we should get a touchcancel
+ // event.
+ break;
+ case 9: is(e.type, "touchcancel", "Got a touchcancel"); break;
+ default: ok(false, "Got an unexpected event of type " + e.type); break;
+ }
+ eventsFired++;
+
+ if (eventsFired == 10) {
+ removeMouseEventListeners(target);
+ setTimeout(async () => {
+ await synthesizeNativeTouch(target, 5, 5, SpecialPowers.DOMWindowUtils.TOUCH_REMOVE, function() {
+ dump("Finished synthesizing touch-end, doing an APZ flush to see if any more unexpected events come through...\n");
+ promiseOnlyApzControllerFlushed().then(function() {
+ dump("Done APZ flush, ending test...\n");
+ subtestDone();
+ });
+ });
+ }, 0);
+ }
+ }
+}
+
+window.addEventListener("touchstart", recordEvent, { passive: true, capture: true });
+window.addEventListener("touchend", recordEvent, { passive: true, capture: true });
+window.addEventListener("touchcancel", recordEvent, true);
+window.addEventListener("contextmenu", recordEvent, true);
+SpecialPowers.addChromeEventListener("mouselongtap", recordEvent, true);
+
+waitUntilApzStable()
+.then(longPressLink);
+
+ </script>
+</head>
+<body>
+ <a id="b" href="#">Link to nowhere</a>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_main_thread_smooth_scroll_scrollend.html b/gfx/layers/apz/test/mochitest/helper_main_thread_smooth_scroll_scrollend.html
new file mode 100644
index 0000000000..4f07db516e
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_main_thread_smooth_scroll_scrollend.html
@@ -0,0 +1,47 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <script src="apz_test_utils.js"></script>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+ html {
+ overflow: hidden;
+ }
+ #spacer {
+ height: 200vh;
+ width: 200vw;
+ position: absolute;
+ background: linear-gradient(green, blue);
+ }
+ </style>
+</head>
+<body>
+<div id="spacer"></div>
+</body>
+<script>
+async function test() {
+ let scrollendCount = 0;
+
+ window.addEventListener("scrollend", e => {
+ scrollendCount += 1;
+ });
+
+ window.scrollTo({ top: window.scrollY, behavior: 'smooth' });
+
+ await promiseFrame();
+
+ is(scrollendCount, 0, "Scrollend is not fired for a main thread no-op smooth scroll");
+
+ window.scrollTo({ top: window.scrollY + 200, behavior: 'smooth' });
+
+ await promiseOneEvent(window, "scrollend");
+
+ is(scrollendCount, 1, "Scrollend is fired for a main thread smooth scroll");
+}
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+</script>
diff --git a/gfx/layers/apz/test/mochitest/helper_mainthread_scroll_bug1662379.html b/gfx/layers/apz/test/mochitest/helper_mainthread_scroll_bug1662379.html
new file mode 100644
index 0000000000..bef8f05f17
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_mainthread_scroll_bug1662379.html
@@ -0,0 +1,168 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, minimum-scale=1.0">
+<script src="apz_test_utils.js"></script>
+<script src="apz_test_native_event_utils.js"></script>
+<script src="/tests/SimpleTest/paint_listener.js"></script>
+<div id="content">
+ <div id="lhs">
+ <ul>
+ <li>Test item 1</li>
+ <li>Test item 2</li>
+ <li>Test item 3</li>
+ <li>Test item 4</li>
+ <li>Test item 5</li>
+ <li>Test item 6</li>
+ <li>Test item 7</li>
+ <li>Test item 8</li>
+ <li>Test item 9</li>
+ <li>Test item 10</li>
+ <li>Test item 11</li>
+ <li>Test item 13</li>
+ <li>Test item 14</li>
+ <li>Test item 15</li>
+ <li>Test item 16</li>
+ <li>Test item 17</li>
+ <li>Test item 18</li>
+ <li>Test item 19</li>
+ </ul>
+ </div>
+ <div id="center">
+ <div>
+ Steps to reproduce:
+ <ol>
+ <li>Scroll the list of "test items" all the way to the bottom
+ <li>Click on the reparent button below
+ <li>Click on one of the test items
+ <li>The `clickTarget` JS variable should match the thing you clicked on
+ </ol>
+ </div>
+ <button onclick="reparent()"> Click here to reparent </button>
+ </div>
+</div>
+<style>
+#content {
+ display: flex;
+ height: 300px;
+ background-color: pink;
+ border: 3px solid green;
+}
+
+#lhs, #rhs {
+ width: 250px;
+ overflow: scroll;
+ flex: 0 0 250px
+}
+
+#center {
+ padding: 20px;
+}
+
+ul {
+ margin: 16px 0px;
+}
+
+/* Each element has a border-height of 20 + (2 * 5) + (2 * 1) = 32px */
+ul li {
+ background-color: aqua;
+ border: 1px solid blue;
+ padding: 5px;
+ cursor: pointer;
+ height: 20px;
+}
+</style>
+<script>
+var clickTarget = null;
+
+for (var el of document.querySelectorAll("ul li")) {
+ el.addEventListener("click", function(e) {
+ clickTarget = e.target;
+ });
+}
+
+function reparent() {
+ var content = document.getElementById("content");
+ var lhs = document.getElementById("lhs");
+ content.appendChild(lhs);
+ lhs.id = "rhs";
+}
+
+function getAsyncScrollOffsetForUniqueRcdChild() {
+ let apzcTree = getLastApzcTree();
+ let rcd = findRcdNode(apzcTree);
+ // We assume a unique child of the RCD. If this is not the case, bail out.
+ if (rcd == null || rcd.children.length != 1) {
+ info("Did not find unique child on the RCD: rcd=" + JSON.stringify(rcd));
+ return {x: -1, y: -1};
+ }
+ let child = rcd.children[0];
+ return parsePoint(child.asyncScrollOffset);
+}
+
+async function test() {
+ if (getPlatform() == "android") {
+ ok(true, "Mousewheel is not supported on android, skipping test...");
+ return;
+ }
+
+ // Simulate user mouse-wheel scrolling the lhs pane down to the bottom.
+ let lhs = document.getElementById("lhs");
+ while (lhs.scrollTop < lhs.scrollTopMax) {
+ await promiseNativeWheelAndWaitForScrollEvent(
+ lhs,
+ 50, 50,
+ 0, -50);
+ info("Did scroll, pos is now " + lhs.scrollTop + "/" + lhs.scrollTopMax);
+ }
+ await promiseApzFlushedRepaints();
+
+ // Click at 50,50 from the top-left corner of the lhs pane. If lhs were
+ // not scrolled, this would hit "Test item 2" but since lhs is scrolled
+ // it should hit something else. So let's check that it doesn't hit
+ // "Test item 2".
+ await promiseNativeMouseEventWithAPZAndWaitForEvent({
+ type: "click",
+ target: lhs,
+ offsetX: 50,
+ offsetY: 50,
+ });
+ isnot(clickTarget, null, "Click target got set");
+ info("Hit " + clickTarget.textContent);
+ isnot(clickTarget.textContent, "Test item 2", "Item two didn't get hit");
+ clickTarget = null;
+
+ // Do the reparenting
+ reparent();
+ await promiseApzFlushedRepaints();
+ info("Done reparent + wait, about to fire click...");
+
+ // Now fire the click on the reparented element (which is now called "rhs")
+ // at the same 50,50 offset from the top-left. This time it *should* hit
+ // "Test item 2" because the reparenting should reset the scroll offset
+ // back to zero.
+ await promiseNativeMouseEventWithAPZAndWaitForEvent({
+ type: "click",
+ target: document.getElementById("rhs"),
+ offsetX: 50,
+ offsetY: 50,
+ });
+
+ // Check that the visual scroll position (as determined by the compositor
+ // scroll offset) is consistent with the main-thread scroll position (as
+ // determined by the click target). In both cases the scroll position
+ // should have gotten reset back to zero with the reparenting step. In
+ // bug 1662379 the visual scroll position was *not* getting reset, and
+ // so even though the main-thread click target was "Test item 2", the
+ // compositor scroll offset was non-zero, so to the user the click
+ // appeared to trigger something different from what they clicked on.
+ isnot(clickTarget, null, "Click target got set");
+ is(clickTarget.textContent, "Test item 2", "Item two got hit correctly");
+ let rhsCompositorScrollOffset = getAsyncScrollOffsetForUniqueRcdChild();
+ is(rhsCompositorScrollOffset.x, 0, "rhs compositor x-offset is zero");
+ is(rhsCompositorScrollOffset.y, 0, "rhs compositor y-offset is zero");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+</script>
diff --git a/gfx/layers/apz/test/mochitest/helper_minimum_scale_1_0.html b/gfx/layers/apz/test/mochitest/helper_minimum_scale_1_0.html
new file mode 100644
index 0000000000..17ccb3a54d
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_minimum_scale_1_0.html
@@ -0,0 +1,46 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=200, minimum-scale=1.0, initial-scale=2.0">
+ <title>Tests that the layout viewport is expanted to the minimum scale size (minimim-scale >= 1.0)</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+ html,body {
+ overflow-x: hidden;
+ margin: 0;
+ }
+ div {
+ position: absolute;
+ }
+ </style>
+</head>
+<body>
+ <div style="width: 200%; height: 200%; background-color: green"></div>
+ <div style="width: 100%; height: 100%; background-color: blue"></div>
+ <script type="application/javascript">
+ const utils = SpecialPowers.getDOMWindowUtils(window);
+
+ async function test(testDriver) {
+ utils.scrollToVisual(100, 0, utils.UPDATE_TYPE_MAIN_THREAD,
+ utils.SCROLL_MODE_INSTANT);
+
+ const promiseForVisualViewportScroll = new Promise(resolve => {
+ window.visualViewport.addEventListener("scroll", () => {
+ resolve();
+ }, { once: true });
+ });
+
+ await waitUntilApzStable();
+
+ await promiseForVisualViewportScroll;
+
+ is(visualViewport.offsetLeft, 100,
+ "The visual viewport offset should be moved");
+ }
+
+ waitUntilApzStable().then(test).then(subtestDone, subtestFailed);
+ </script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_no_scalable_with_initial_scale.html b/gfx/layers/apz/test/mochitest/helper_no_scalable_with_initial_scale.html
new file mode 100644
index 0000000000..7280a26006
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_no_scalable_with_initial_scale.html
@@ -0,0 +1,48 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, minimum-scale=0.25, initial-scale=0.5, user-scalable=no">
+ <title>Tests that the layout viewport is not expanted to the minimum scale size if user-scalable=no is specified</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+ html,body {
+ overflow: hidden;
+ margin: 0;
+ }
+ div {
+ position: absolute;
+ }
+ </style>
+</head>
+<body>
+ <div style="width: 400%; height: 400%; background: red;"></div>
+ <div style="width: 100%; height: 100%; background-color: blue"></div>
+ <script type="application/javascript">
+ const utils = SpecialPowers.getDOMWindowUtils(window);
+
+ async function test(testDriver) {
+ utils.scrollToVisual(100, 0, utils.UPDATE_TYPE_MAIN_THREAD,
+ utils.SCROLL_MODE_INSTANT);
+
+ let receivedScrollEvent = false;
+ window.visualViewport.addEventListener("scroll", () => {
+ receivedScrollEvent = true;
+ }, { once: true });
+
+ await waitUntilApzStable();
+
+ // Waits two frames to get a chance to deliver scroll events.
+ await promiseFrame();
+ await promiseFrame();
+
+ ok(!receivedScrollEvent, "Scroll should never happen");
+ is(visualViewport.offsetLeft, 0,
+ "The visual viewport offset should not be moved");
+ }
+
+ waitUntilApzStable().then(test).then(subtestDone, subtestFailed);
+ </script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_onetouchpinch_nested.html b/gfx/layers/apz/test/mochitest/helper_onetouchpinch_nested.html
new file mode 100644
index 0000000000..54b578b496
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_onetouchpinch_nested.html
@@ -0,0 +1,103 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width">
+ <title>One-touch pinch zooming while on a non-root scroller</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="application/javascript">
+
+async function test_onetouchpinch() {
+ // layerize the scroller so it gets an APZC and GestureEventListener
+ var scroller = document.getElementById("scroller");
+ SpecialPowers.getDOMWindowUtils(window).setDisplayPortForElement(0, 0, 400, 1000, scroller, 1);
+ await promiseApzFlushedRepaints();
+
+ ok(isLayerized("scroller"), "scroller has been successfully layerized");
+
+ var initial_resolution = await getResolution();
+ ok(initial_resolution > 0,
+ "The initial_resolution is " + initial_resolution + ", which is some sane value");
+
+ let transformEndPromise = promiseTransformEnd();
+
+ function translateY(point, dy) {
+ return {x: point.x, y: point.y + dy};
+ }
+
+ var zoom_point = centerOf(scroller);
+ var zoom_in = [
+ [ zoom_point ],
+ [ null ],
+ [ zoom_point ],
+ [ translateY(zoom_point, 5) ],
+ [ translateY(zoom_point, 10) ],
+ [ translateY(zoom_point, 15) ],
+ [ translateY(zoom_point, 20) ],
+ [ translateY(zoom_point, 25) ],
+ ];
+
+ var touchIds = [0];
+ await synthesizeNativeTouchSequences(scroller, zoom_in, null, touchIds);
+
+ // Wait for the APZ:TransformEnd to be fired after touch events are processed.
+ await transformEndPromise;
+
+ // Flush state and get the resolution we're at now
+ await promiseApzFlushedRepaints();
+ let final_resolution = await getResolution();
+ ok(final_resolution > initial_resolution, "The final resolution (" + final_resolution + ") is greater after zooming in");
+
+ // Also check that the scroller didn't get scrolled.
+ is(scroller.scrollTop, 0, "scroller didn't y-scroll");
+ is(scroller.scrollLeft, 0, "scroller didn't x-scroll");
+}
+
+async function test() {
+ // Run the test with the scrollable div
+ await test_onetouchpinch();
+ dump("Wrapping scroller in fixed-pos div...\n");
+ // Now wrap the scrollable div inside a fixed-pos div
+ var fixedElement = document.createElement("div");
+ fixedElement.id = "fixed";
+ document.body.appendChild(fixedElement);
+ fixedElement.appendChild(document.getElementById("scroller"));
+ dump("Done wrapping scroller in fixed-pos div.\n");
+ // Now run the test again, with the scrollable div inside a fixed-pos div
+ await test_onetouchpinch();
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+ <style>
+ #scroller {
+ width: 300px;
+ height: 300px;
+ overflow: scroll;
+ }
+
+ #fixed {
+ background-color: green;
+ position: fixed;
+ width: 300px;
+ height: 300px;
+ left: 100px;
+ top: 100px;
+ }
+ </style>
+</head>
+<body>
+ Here is some text outside the scrollable div.
+ <div id="scroller">
+ Here is some text inside the scrollable div.
+ <div style="height: 2000px">This div actually makes it overflow.</div>
+ </div>
+ <div style="height: 2000px">This div makes the body scrollable.</div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_overflowhidden_zoom.html b/gfx/layers/apz/test/mochitest/helper_overflowhidden_zoom.html
new file mode 100644
index 0000000000..6c12008e4b
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_overflowhidden_zoom.html
@@ -0,0 +1,83 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, minimum-scale=1.0">
+ <title>Tests that zooming in and out doesn't change the scroll position on an overflow hidden document</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+ html,body {
+ overflow: hidden;
+ }
+ </style>
+</head>
+<body>
+ <div style="height: 20000px; background-color: green"></div>
+ <script>
+ const utils = SpecialPowers.getDOMWindowUtils(window);
+
+ async function test() {
+ is(await getResolution(), 1.0, "should not be zoomed (1)");
+
+ is(window.scrollX, 0, "shouldn't have scrolled (2)");
+ is(window.scrollY, 0, "shouldn't have scrolled (3)");
+ is(visualViewport.pageTop, 0, "shouldn't have scrolled (4)");
+ is(visualViewport.pageLeft, 0, "shouldn't have scrolled (5)");
+
+ // Force reconstruction of the root scroll frame to trigger bug 1665332.
+ document.documentElement.style.display = "flex";
+ document.documentElement.offsetLeft;
+ document.documentElement.style.display = "";
+ document.documentElement.offsetLeft;
+
+ is(await getResolution(), 1.0, "should not be zoomed (6)");
+
+ is(window.scrollX, 0, "shouldn't have scrolled (7)");
+ is(window.scrollY, 0, "shouldn't have scrolled (8)");
+ is(visualViewport.pageTop, 0, "shouldn't have scrolled (9)");
+ is(visualViewport.pageLeft, 0, "shouldn't have scrolled (10)");
+
+ // Zoom in
+ SpecialPowers.getDOMWindowUtils(window).setResolutionAndScaleTo(4.0);
+ await promiseApzFlushedRepaints();
+
+ is(await getResolution(), 4.0, "should be zoomed (11)");
+
+ is(window.scrollX, 0, "shouldn't have scrolled (12)");
+ is(window.scrollY, 0, "shouldn't have scrolled (13)");
+ is(visualViewport.pageTop, 0, "shouldn't have scrolled (14)");
+ is(visualViewport.pageLeft, 0, "shouldn't have scrolled (15)");
+
+ // Scroll so the visual viewport offset is non-zero
+ utils.scrollToVisual(20000, 20000, utils.UPDATE_TYPE_MAIN_THREAD,
+ utils.SCROLL_MODE_INSTANT);
+
+ await promiseApzFlushedRepaints();
+
+ is(await getResolution(), 4.0, "should be zoomed (16)");
+
+ is(window.scrollX, 0, "shouldn't have scrolled (17)");
+ is(window.scrollY, 0, "shouldn't have scrolled (18)");
+ isnot(visualViewport.pageTop, 0, "should have scrolled (19)");
+ isnot(visualViewport.pageLeft, 0, "should have scrolled (20)");
+
+ // Zoom back out
+ SpecialPowers.getDOMWindowUtils(window).setResolutionAndScaleTo(1.0);
+ await promiseApzFlushedRepaints();
+
+ is(await getResolution(), 1.0, "should not be zoomed (21)");
+
+ is(window.scrollX, 0, "shouldn't have scrolled (22)");
+ is(window.scrollY, 0, "shouldn't have scrolled (23)");
+ is(visualViewport.pageTop, 0, "shouldn't have scrolled (24)");
+ is(visualViewport.pageLeft, 0, "shouldn't have scrolled (25)");
+ }
+
+ waitUntilApzStable()
+ .then(test)
+ .then(subtestDone, subtestFailed);
+ </script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_override_root.html b/gfx/layers/apz/test/mochitest/helper_override_root.html
new file mode 100644
index 0000000000..81c1b34938
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_override_root.html
@@ -0,0 +1,62 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Simple wheel scroll cancellation</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+async function test() {
+ let wheelEventPromise = new Promise(resolve => {
+ // Add a non-passive listener on the document, so that we have a document-level
+ // APZ-aware listener, and the entire document is put in the dispatch-to-content
+ // region
+ document.addEventListener("wheel", function(e) {
+ // spin for 2 seconds to give APZ time to scroll, if the event region override
+ // is broken and it decides not to wait for the main thread. Note that it's
+ // possible the APZ controller thread is busy for whatever reason so APZ
+ // may not scroll. That might cause this test to only fail intermittently
+ // instead of consistently if the behaviour being tested regresses.
+ var now = Date.now();
+ while (Date.now() - now < 2000);
+
+ // Cancel the scroll. If this works then we know APZ waited for this listener
+ // to run.
+ e.preventDefault();
+ resolve()
+ }, { passive: false });
+ });
+
+ // Ensure APZ gets a paint with the d-t-c region
+ await promiseApzFlushedRepaints();
+
+ await synthesizeNativeWheel(document.body, 100, 100, 0, -50);
+ dump("Finished native wheel, waiting for listener to run...\n");
+
+ await wheelEventPromise;
+ await promiseOnlyApzControllerFlushed();
+
+ is(window.scrollY, 0, "check that the window didn't scroll");
+}
+
+if (window.top != window) {
+ dump("Running inside an iframe! stealing functions from window.top...\n");
+ window.subtestDone = window.top.subtestDone;
+ window.SimpleTest = window.top.SimpleTest;
+ window.is = window.top.is;
+ window.ok = window.top.ok;
+}
+
+waitUntilApzStable()
+ .then(test)
+ .then(subtestDone);
+
+ </script>
+</head>
+<body style="height: 5000px; background-image: linear-gradient(green,red);">
+ This page should not be wheel-scrollable.
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_override_subdoc.html b/gfx/layers/apz/test/mochitest/helper_override_subdoc.html
new file mode 100644
index 0000000000..910f36ddc3
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_override_subdoc.html
@@ -0,0 +1,15 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Wheel scroll cancellation inside iframe</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+</head>
+<body>
+ This just loads helper_override_root in an iframe, so that we test event
+ regions overriding on in-process subdocuments.
+ <iframe id="ifr" src="helper_override_root.html" onload="document.getElementById('ifr').focus()"></iframe>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_overscroll_behavior_bug1425573.html b/gfx/layers/apz/test/mochitest/helper_overscroll_behavior_bug1425573.html
new file mode 100644
index 0000000000..817522f9c3
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_overscroll_behavior_bug1425573.html
@@ -0,0 +1,44 @@
+<head>
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Wheel-scrolling over inactive subframe with overscroll-behavior</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+async function test() {
+ var subframe = document.getElementById("scroll");
+
+ // scroll over the middle of the subframe, and make sure that the page
+ // does not scroll.
+ var waitForScroll = false; // don't wait for a scroll event, it will never come
+ await promiseMoveMouseAndScrollWheelOver(subframe, 100, 100, waitForScroll);
+ ok(window.scrollY == 0, "overscroll-behavior was respected");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+ <style>
+ #scroll {
+ width: 200px;
+ height: 200px;
+ overflow: scroll;
+ overscroll-behavior: contain;
+ }
+ #scrolled {
+ width: 200px;
+ height: 1000px; /* so the subframe has room to scroll */
+ background: linear-gradient(red, blue); /* so you can see it scroll */
+ }
+ </style>
+</head>
+<body>
+ <div id="scroll">
+ <div id="scrolled"></div>
+ </div>
+ <div style="height: 5000px;"></div><!-- So the page is scrollable as well -->
+</body>
+</head>
diff --git a/gfx/layers/apz/test/mochitest/helper_overscroll_behavior_bug1425603.html b/gfx/layers/apz/test/mochitest/helper_overscroll_behavior_bug1425603.html
new file mode 100644
index 0000000000..8324530c95
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_overscroll_behavior_bug1425603.html
@@ -0,0 +1,76 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Scrolling over checkerboarded area respects overscroll-behavior</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <meta name="viewport" content="width=device-width"/>
+ <style>
+ #subframe {
+ width: 100px;
+ height: 100px;
+ overflow: scroll;
+ margin-top: 10px;
+ margin-left: 10px;
+ overscroll-behavior: contain;
+ }
+ #contents {
+ width: 100%;
+ height: 1000px;
+ background-image: linear-gradient(red, blue);
+ }
+ </style>
+</head>
+<body>
+ <div id="subframe">
+ <div id="contents"></div>
+ </div>
+ <div id="make_root_scrollable" style="height: 5000px"></div>
+</body>
+<script type="application/javascript">
+
+async function test() {
+ var config = getHitTestConfig();
+ var utils = config.utils;
+
+ var subframe = document.getElementById("subframe");
+
+ // Activate the scrollframe but keep the main-thread scroll position at 0.
+ // Also apply an async scroll offset in the y-direction large enough
+ // to make the scrollframe checkerboard.
+ // Note: We have to be careful with the numbers here.
+ // promiseMoveMouseAndScrollWheelOver() relies on the main thread receiving
+ // the synthesized mouse-move and wheel events. However, the async
+ // transform created by setAsyncScrollOffset() will cause an untransform
+ // to be applied to the synthesized events' coordinates before they're
+ // passed to the main thread. We have to make sure the transform is
+ // large enough to cause the scroll frame to checkerboard, but not so
+ // large that the untransformed coordinates hit-test out of bounds for
+ // the browser's content area. This is why we make the scroll frame
+ // small (100x100), and give it a display port that's also just 100x100,
+ // so we can keep the async scroll offset small enough (300 in this case)
+ // that the untransformed coordinates are still in-bounds for the window.
+ utils.setDisplayPortForElement(0, 0, 100, 100, subframe, 1);
+ await promiseAllPaintsDone();
+ var scrollY = 300;
+ utils.setAsyncScrollOffset(subframe, 0, scrollY);
+ // Tick the refresh driver once to make sure the compositor has applied the
+ // async scroll offset (for WebRender hit-testing we need to make sure WR has
+ // the latest info).
+ utils.advanceTimeAndRefresh(16);
+ utils.restoreNormalRefresh();
+
+ // Scroll over the subframe, and make sure that the page does not scroll,
+ // i.e. overscroll-behavior is respected.
+ var waitForScroll = false; // don't wait for a scroll event, it will never come
+ await promiseMoveMouseAndScrollWheelOver(subframe, 50, 50, waitForScroll);
+ is(window.scrollY, 0, "overscroll-behavior was respected");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+</script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_overscroll_behavior_bug1494440.html b/gfx/layers/apz/test/mochitest/helper_overscroll_behavior_bug1494440.html
new file mode 100644
index 0000000000..3f12e36102
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_overscroll_behavior_bug1494440.html
@@ -0,0 +1,50 @@
+<head>
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Inactive iframe with overscroll-behavior</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+</head>
+<body>
+ <iframe id="scroll" srcdoc="<!doctype html><html style='overscroll-behavior:none; overflow: auto;'><div style='width:100px;height:2000px;'>">
+ </iframe>
+ <div style="height: 5000px;"></div><!-- So the page is scrollable as well -->
+
+ <script type="application/javascript">
+
+async function test() {
+ var iframe = document.getElementById("scroll");
+ var iframeWindow = iframe.contentWindow;
+
+ // scroll the iframe to the bottom, such that a subsequent scroll on it
+ // _would_ hand off to the page if overscroll-behavior allowed it
+ iframeWindow.scrollTo(0, iframeWindow.scrollMaxY);
+ await promiseApzFlushedRepaints();
+ is(iframeWindow.scrollY, iframeWindow.scrollMaxY, "iframe has scrolled to the bottom");
+
+ // Scroll over the iframe, and make sure that the page
+ // does not scroll.
+ // We can't wait for a "scroll" event unconditionally, since if the platform
+ // behaviour we are testing is correct (overscroll-behavior is respected),
+ // one will never arrive.
+ var waitForScroll = false;
+ await promiseMoveMouseAndScrollWheelOver(iframeWindow, 100, 100, waitForScroll);
+ // However, we need to give a potential "scroll" event a chance to be dispatched,
+ // so that if the platform behaviour we are testing is incorrect (overscroll-behavior)
+ // is not respected, we catch it.
+ await promiseApzFlushedRepaints();
+ is(window.scrollY, 0, "overscroll-behavior was respected");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+ <style>
+ #scroll {
+ width: 200px;
+ height: 500px;
+ }
+ </style>
+</body>
diff --git a/gfx/layers/apz/test/mochitest/helper_overscroll_in_apz_test_data.html b/gfx/layers/apz/test/mochitest/helper_overscroll_in_apz_test_data.html
new file mode 100644
index 0000000000..ed05f25819
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_overscroll_in_apz_test_data.html
@@ -0,0 +1,29 @@
+<!DOCTYPE HTML>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, minimum-scale=1.0">
+<title>A simple test checks "overscrolled" info in APZTestData</title>
+<title>Tests scroll anchoring updates in-progress wheel scrolling __relatively__</title>
+<script src="apz_test_utils.js"></script>
+<script src="apz_test_native_event_utils.js"></script>
+<script src="/tests/SimpleTest/paint_listener.js"></script>
+<div style="height: 300vh; background-color: blue;"></div><!-- Make the root scrollable -->
+<script>
+ async function test() {
+ // Try to overscroll by using setAsyncScrollOffset to avoid race conditions
+ // that APZTestData haven't been arrived on the main-thread.
+ SpecialPowers.DOMWindowUtils.setAsyncScrollOffset(document.scrollingElement, 0, -100);
+
+ const scrollId =
+ SpecialPowers.DOMWindowUtils.getViewId(document.scrollingElement);
+ const data = SpecialPowers.DOMWindowUtils.getCompositorAPZTestData();
+ for (apzcData of data.additionalData) {
+ if (apzcData.key == scrollId) {
+ ok(apzcData.value.split(",").includes("overscrolled"));
+ }
+ }
+ }
+
+ waitUntilApzStable()
+ .then(test)
+ .then(subtestDone, subtestFailed);
+</script>
diff --git a/gfx/layers/apz/test/mochitest/helper_overscroll_in_subscroller.html b/gfx/layers/apz/test/mochitest/helper_overscroll_in_subscroller.html
new file mode 100644
index 0000000000..5936de97f7
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_overscroll_in_subscroller.html
@@ -0,0 +1,165 @@
+<!DOCTYPE HTML>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, minimum-scale=1.0">
+<title>
+ Tests that the overscroll gutter in a sub scroll container is restored if it's
+ no longer target scroll container
+</title>
+<script src="apz_test_utils.js"></script>
+<script src="apz_test_native_event_utils.js"></script>
+<script src="/tests/SimpleTest/paint_listener.js"></script>
+<style>
+ html {
+ overflow: scroll;
+ }
+
+ .content {
+ height: 500px;
+ width: 300px;
+ overflow-y: scroll;
+ background-color: red;
+ }
+</style>
+<!-- a sub scroll container -->
+<div class="content">
+ <div style="height:100vh; background-color: white;"></div>
+</div>
+<div style="height:200vh"></div>
+<script>
+ document.documentElement.addEventListener(
+ "wheel",
+ e => {
+ if (!e.target.closest(`div[class="content"]`)) {
+ e.preventDefault();
+ }
+ },
+ {
+ passive: false,
+ }
+ );
+
+ const subScroller = document.querySelector(`div[class="content"]`);
+ // Make the sub scroll container overscrollable at the top edge.
+ // A `waitUntilApzStable()` call below ensures that this scroll position
+ // has been informed into APZ before starting this test.
+ subScroller.scrollTop = 1;
+
+ // A utility function to collect overscrolled scroll container information.
+ function collectOverscrolledData() {
+ const apzData = SpecialPowers.DOMWindowUtils.getCompositorAPZTestData().additionalData;
+ return apzData.filter(data => {
+ return SpecialPowers.wrap(data).value.split(",").includes("overscrolled");
+ });
+ }
+
+ async function test() {
+ const oneScrollPromise = new Promise(resolve => {
+ subScroller.addEventListener("scroll", () => {
+ resolve();
+ }, { once: true });
+ });
+
+ // Start a pan upward gesture to try oversrolling on the sub scroll
+ // container.
+ await NativePanHandler.promiseNativePanEvent(
+ subScroller,
+ 100,
+ 100,
+ 0,
+ -NativePanHandler.delta,
+ NativePanHandler.beginPhase
+ );
+
+ const rootScrollId =
+ SpecialPowers.DOMWindowUtils.getViewId(document.scrollingElement);
+ const subScrollId =
+ SpecialPowers.DOMWindowUtils.getViewId(subScroller);
+
+ await promiseApzFlushedRepaints();
+ await oneScrollPromise;
+
+ let overscrolledData = collectOverscrolledData();
+ ok(overscrolledData.length >= 1,
+ "There should be at least one overscrolled scroll container");
+ ok(overscrolledData.every(data => SpecialPowers.wrap(data).key == subScrollId),
+ "The overscrolled scroll container should be the sub scroll container");
+
+ let twoScrollEndPromise = new Promise(resolve => {
+ let count = 0;
+ subScroller.addEventListener("scrollend", () => {
+ count++;
+ ok(count <= 2, "There should never be more than two scrollend events");
+ if (count == 2) {
+ resolve();
+ }
+ });
+ });
+
+ // Finish the pan upward gesture.
+ await NativePanHandler.promiseNativePanEvent(
+ subScroller,
+ 100,
+ 100,
+ 0,
+ 0,
+ NativePanHandler.endPhase
+ );
+
+ await promiseApzFlushedRepaints();
+
+ // Now do another pan upward gesture again.
+ await NativePanHandler.promiseNativePanEvent(
+ subScroller,
+ 100,
+ 100,
+ 0,
+ -NativePanHandler.delta,
+ NativePanHandler.beginPhase
+ );
+
+ // Wait two `apz-repaints-flushed`s to give a chance to overscroll the root
+ // scroll container.
+ await promiseApzFlushedRepaints();
+ await promiseApzFlushedRepaints();
+
+ overscrolledData = collectOverscrolledData();
+ ok(overscrolledData.length >= 2,
+ "There should be at least two overscrolled scroll containers");
+ ok(overscrolledData.some(data => SpecialPowers.wrap(data).key == rootScrollId),
+ "The root scroll container should be overscrolled");
+ ok(overscrolledData.some(data => SpecialPowers.wrap(data).key == subScrollId),
+ "The sub scroll container should also be overscrolled");
+
+ // While the root scroll container is still being overscrolled because the
+ // new pan gesture is still on-going, the sub scroll container should be
+ // restored.
+ // Note that this test relies on the fact that two scrollend events get
+ // fired when overscrolling happens, one gets fired when the scroll position
+ // reached to the edge of the scrollport (i.e. just about to start
+ // overscrolling), the other one gets fired when overscrolling ends.
+ await twoScrollEndPromise;
+ info("Got two scroll end events on the sub scroll container");
+
+ await promiseApzFlushedRepaints();
+
+ overscrolledData = collectOverscrolledData();
+ ok(overscrolledData.length >= 1,
+ "There should be at least one overscrolled scroll container");
+ ok(overscrolledData.every(data => SpecialPowers.wrap(data).key == rootScrollId),
+ "The root scroll container should still be overscrolled");
+
+ // Finish the pan upward gesture.
+ await NativePanHandler.promiseNativePanEvent(
+ subScroller,
+ 100,
+ 100,
+ 0,
+ 0,
+ NativePanHandler.endPhase
+ );
+ }
+
+ waitUntilApzStable()
+ .then(test)
+ .then(subtestDone, subtestFailed);
+</script>
diff --git a/gfx/layers/apz/test/mochitest/helper_position_fixed_scroll_handoff-1.html b/gfx/layers/apz/test/mochitest/helper_position_fixed_scroll_handoff-1.html
new file mode 100644
index 0000000000..6c986b0ca1
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_position_fixed_scroll_handoff-1.html
@@ -0,0 +1,88 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>APZ overscroll handoff for fixed elements</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <meta name="viewport" content="width=device-width"/>
+ <style>
+html, body {
+ height: 100%;
+ overflow: hidden;
+}
+
+#main {
+ height: 100%;
+ overflow: auto;
+}
+
+#spacer {
+ height: 5000px;
+}
+
+#fixed {
+ position: fixed;
+ top: 50%;
+ left: 0;
+ width: 100%;
+ height: 100px;
+ background: red;
+ overflow: auto;
+}
+
+#long {
+ height: 250px;
+ width: 50%;
+ position: absolute;
+ background: green;
+ top: 0;
+ left: 25%;
+}
+ </style>
+</head>
+<body>
+ <div id="main">
+ <div id="spacer">
+ </div>
+ <div id="fixed">
+ <div id="long">
+ </div>
+ </div>
+ </div>
+</body>
+<script type="application/javascript">
+
+async function test() {
+ // Scroll to the bottom of the fixed position element that should not
+ // allow overscroll handoff.
+ fixed.scrollTop = fixed.scrollHeight;
+
+ // After scrolling to bottom tick the refresh driver.
+ await promiseFrame();
+
+ info("Start: fixed=" + fixed.scrollTop + " main=" + main.scrollTop);
+
+ // Async scroll the fixed element by 200 pixels using the mouse-wheel.
+ // This should not handoff the overscroll.
+ await promiseMoveMouseAndScrollWheelOver(fixed, 50, 50, false, 200);
+
+ // Make sure scrolling that has happened is propagated to the main thread.
+ await promiseApzFlushedRepaints();
+
+ // Try another gesture to ensure the overscroll handoff runs.
+ await promiseMoveMouseAndScrollWheelOver(fixed, 50, 50, false, 200);
+ await promiseApzFlushedRepaints();
+
+ info("After scroll: fixed=" + fixed.scrollTop + " main=" + main.scrollTop);
+
+ // Ensure that the main element has not scrolled.
+ is(main.scrollTop, 0, "The overscroll should not handoff");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+</script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_position_fixed_scroll_handoff-2.html b/gfx/layers/apz/test/mochitest/helper_position_fixed_scroll_handoff-2.html
new file mode 100644
index 0000000000..29b11072ca
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_position_fixed_scroll_handoff-2.html
@@ -0,0 +1,65 @@
+<!DOCTYPE HTML>
+<head>
+ <title>APZ overscroll handoff for fixed elements</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <meta name="viewport" content="width=device-width"/>
+<style>
+html, body {
+ margin: 0;
+}
+html {
+ overflow: auto;
+ background: blue;
+}
+.spacer {
+ height: 2000px;
+}
+#fixed {
+ position: fixed;
+ overflow: auto;
+ background: red;
+ width: 200px;
+ height: 200px;
+ top: 0;
+ left: 0;
+}
+</style>
+</head>
+<div id="fixed">
+ <div class="spacer"></div>
+</div>
+<div class="spacer"></div>
+<script type="application/javascript">
+
+async function test() {
+ // Scroll to the bottom of the fixed position element that should
+ // allow overscroll handoff.
+ fixed.scrollTop = fixed.scrollHeight;
+
+ // After scrolling to bottom tick the refresh driver.
+ await promiseFrame();
+
+ info("Start: fixed=" + fixed.scrollTop + " window=" + window.scrollY);
+
+ // Async scroll the fixed element by 200 pixels using the mouse-wheel.
+ // This should handoff the overscroll to the window.
+ await promiseMoveMouseAndScrollWheelOver(fixed, 50, 50, false, 200);
+
+ // Make sure scrolling that has happened is propagated to the main thread.
+ await promiseApzFlushedRepaints();
+
+ // Try another gesture to ensure the overscroll handoff runs.
+ await promiseMoveMouseAndScrollWheelOver(fixed, 50, 50, false, 200);
+ await promiseApzFlushedRepaints();
+
+ // Ensure that the window has scrolled.
+ isnot(window.scrollY, 0, "The overscroll should not handoff");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+</script>
diff --git a/gfx/layers/apz/test/mochitest/helper_position_fixed_scroll_handoff-3.html b/gfx/layers/apz/test/mochitest/helper_position_fixed_scroll_handoff-3.html
new file mode 100644
index 0000000000..4a0687ba20
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_position_fixed_scroll_handoff-3.html
@@ -0,0 +1,77 @@
+<!DOCTYPE HTML>
+<head>
+ <title>APZ overscroll handoff for fixed elements</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <meta name="viewport" content="width=device-width"/>
+<style>
+html, body {
+ margin: 0;
+}
+#scrolled {
+ overflow: auto;
+ background: blue;
+ width: 400px;
+ height: 400px;
+}
+.spacer {
+ height: 2000px;
+}
+#fixed {
+ position: fixed;
+ background: red;
+ top: 0;
+ left: 0;
+}
+#subframe {
+ overflow: auto;
+ width: 200px;
+ height: 200px;
+}
+</style>
+</head>
+<div id="scrolled">
+ <div id="fixed">
+ <div id="subframe">
+ <div id="firstspacer" class="spacer"></div>
+ </div>
+ </div>
+ <div id="secondspacer" class="spacer"></div>
+</div>
+<script type="application/javascript">
+
+async function test() {
+ // Scroll to the bottom of the fixed position element that should not
+ // allow overscroll handoff.
+ subframe.scrollTop = subframe.scrollHeight;
+
+ // After scrolling to bottom tick the refresh driver.
+ await promiseFrame();
+
+ info("Before scroll: subframe=" + subframe.scrollTop + " scrolled=" +
+ scrolled.scrollTop);
+
+ // Async scroll the fixed element by 200 pixels using the mouse-wheel.
+ // This should not handoff the overscroll.
+ await promiseMoveMouseAndScrollWheelOver(subframe, 50, 50, false, 200);
+
+ // Make sure scrolling that has happened is propagated to the main thread.
+ await promiseApzFlushedRepaints();
+
+ // Try another gesture to ensure the overscroll handoff runs.
+ await promiseMoveMouseAndScrollWheelOver(subframe, 50, 50, false, 200);
+ await promiseApzFlushedRepaints();
+
+ info("After scroll: subframe=" + subframe.scrollTop + " scrolled=" +
+ scrolled.scrollTop);
+
+ // Ensure that the scrolled element has not scrolled.
+ is(scrolled.scrollTop, 0, "scrolled: The overscroll should not handoff");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+</script>
diff --git a/gfx/layers/apz/test/mochitest/helper_position_fixed_scroll_handoff-4.html b/gfx/layers/apz/test/mochitest/helper_position_fixed_scroll_handoff-4.html
new file mode 100644
index 0000000000..7394984ce3
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_position_fixed_scroll_handoff-4.html
@@ -0,0 +1,79 @@
+<!DOCTYPE HTML>
+<head>
+ <title>APZ overscroll handoff for fixed elements</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <meta name="viewport" content="width=device-width"/>
+<style>
+html, body {
+ margin: 0;
+}
+#scrolled {
+ overflow: auto;
+ background: blue;
+ width: 400px;
+ height: 400px;
+}
+.spacer {
+ height: 2000px;
+}
+#fixed {
+ position: fixed;
+ background: red;
+ top: 0;
+ left: 0;
+}
+#subframe {
+ overflow: auto;
+ width: 200px;
+ height: 200px;
+}
+</style>
+</head>
+<div id="scrolled">
+ <div id="fixed">
+ <div>
+ <div id="subframe">
+ <div id="firstspacer" class="spacer"></div>
+ </div>
+ </div>
+ </div>
+ <div id="secondspacer" class="spacer"></div>
+</div>
+<script type="application/javascript">
+
+async function test() {
+ // Scroll to the bottom of the fixed position element that should not
+ // allow overscroll handoff.
+ subframe.scrollTop = subframe.scrollHeight;
+
+ // After scrolling to bottom tick the refresh driver.
+ await promiseFrame();
+
+ info("Before scroll: subframe=" + subframe.scrollTop + " scrolled=" +
+ scrolled.scrollTop);
+
+ // Async scroll the fixed element by 200 pixels using the mouse-wheel.
+ // This should not handoff the overscroll.
+ await promiseMoveMouseAndScrollWheelOver(subframe, 50, 50, false, 200);
+
+ // Make sure scrolling that has happened is propagated to the main thread.
+ await promiseApzFlushedRepaints();
+
+ // Try another gesture to ensure the overscroll handoff runs.
+ await promiseMoveMouseAndScrollWheelOver(subframe, 50, 50, false, 200);
+ await promiseApzFlushedRepaints();
+
+ info("After scroll: subframe=" + subframe.scrollTop + " scrolled=" +
+ scrolled.scrollTop);
+
+ // Ensure that the scrolled element has not scrolled.
+ is(scrolled.scrollTop, 0, "scrolled: The overscroll should not handoff");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+</script>
diff --git a/gfx/layers/apz/test/mochitest/helper_position_fixed_scroll_handoff-5.html b/gfx/layers/apz/test/mochitest/helper_position_fixed_scroll_handoff-5.html
new file mode 100644
index 0000000000..3d62287c7c
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_position_fixed_scroll_handoff-5.html
@@ -0,0 +1,110 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>APZ overscroll handoff for fixed elements in a subdoc</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <meta name="viewport" content="width=device-width"/>
+ <style>
+ iframe {
+ width: 400px;
+ height: 400px;
+ border: solid 2px black;
+ }
+ #rootcontent {
+ height: 200vh;
+ background: yellow;
+ }
+ </style>
+ </head>
+ <body>
+ <iframe id="subdoc" srcdoc="
+ <!DOCTYPE html>
+ <html>
+ <head>
+ <style>
+ #fixed {
+ position: fixed;
+ top: 0;
+ height: 100px;
+ width: 80%;
+ overflow: scroll;
+ }
+ #fixed-content {
+ background: red;
+ }
+ #rootcontent {
+ background: green;
+ }
+ .spacer {
+ height: 200vh;
+ width: 100%;
+ }
+ </style>
+ </head>
+ <body>
+ <div id='fixed'>
+ <div id='fixed-content' class='spacer'></div>
+ </div>
+ <div id='rootcontent' class='spacer'></div>
+ </body>
+ </html>
+ "></iframe>
+ <div id="rootcontent"></div>
+ </body>
+ <script>
+async function test() {
+ // Scroll to the bottom of the fixed position element to ensure that the following
+ // scroll does trigger overscroll handoff to the subdoc root scrollable element.
+ subdoc.contentWindow.fixed.scrollTop = subdoc.contentWindow.fixed.scrollHeight;
+
+ // After scrolling to bottom tick the refresh driver.
+ await promiseFrame();
+
+ let firstTransformEnd = promiseTransformEnd();
+
+ info("start scroll #1");
+
+ // Async scroll the fixed element by 200 pixels using the mouse-wheel. This should
+ // handoff the overscroll to the subdoc's root scrollable element.
+ await promiseMoveMouseAndScrollWheelOver(subdoc.contentWindow.fixed, 50, 50, false, 200);
+
+ info("After scroll #1: fixed=" + subdoc.contentWindow.fixed.scrollTop +
+ " subdoc window=" + subdoc.contentWindow.scrollY + " window=" + window.scrollY);
+
+ info("wait scroll #1");
+ await firstTransformEnd;
+
+ // Do not attempt the second scroll if we did scroll the root document.
+ // A scroll in this case would likely cause the test to timeout. The assertions at the
+ // end of the test will catch this.
+
+ // If we triggered a scroll handoff to the _root_ document from the subframe, do not
+ // make another attempt at a second scroll. The test has already failed.
+ if (window.scrollY == 0) {
+ let secondTransformEnd = promiseTransformEnd();
+
+ info("start scroll #2");
+
+ await promiseMoveMouseAndScrollWheelOver(subdoc.contentWindow.fixed, 50, 50, false, 200);
+
+ info("After scroll #2: fixed=" + subdoc.contentWindow.fixed.scrollTop +
+ " subdoc window=" + subdoc.contentWindow.scrollY + " window=" + window.scrollY);
+
+ info("wait scroll #2");
+ await secondTransformEnd;
+ }
+
+ // Ensure that the main element has not scrolled and overscroll was handed off to
+ // the subdocument root scrollable element.
+ is(window.scrollY, 0, "The overscroll should not handoff to the root window");
+ isnot(subdoc.contentWindow.scrollY, 0,
+ "The overscroll should handoff to the subdocument's root scrollable element");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+ </script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_position_sticky_flicker.html b/gfx/layers/apz/test/mochitest/helper_position_sticky_flicker.html
new file mode 100644
index 0000000000..28495a7122
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_position_sticky_flicker.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/paint_listener.js"></script>
+<script src="apz_test_utils.js"></script>
+<script src="apz_test_native_event_utils.js"></script>
+<style>
+body {
+ margin: 0;
+}
+#sticky {
+ position: sticky;
+ top: 0;
+ height: 50vh;
+ background-color: green;
+}
+</style>
+<div id="sticky"></div>
+<!-- Content to make the page scrollable will be added dynamically -->
+<script>
+ // Silence SimpleTest warning about missing assertions by having it wait
+ // indefinitely. We don't need to give it an explicit finish because the
+ // entire window this test runs in will be closed after the main browser test
+ // finished.
+ SimpleTest.waitForExplicitFinish();
+</script>
diff --git a/gfx/layers/apz/test/mochitest/helper_position_sticky_scroll_handoff.html b/gfx/layers/apz/test/mochitest/helper_position_sticky_scroll_handoff.html
new file mode 100644
index 0000000000..ae7815a2fc
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_position_sticky_scroll_handoff.html
@@ -0,0 +1,88 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>APZ overscroll handoff for sticky elements</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <meta name="viewport" content="width=device-width"/>
+ <style>
+html, body {
+ height: 100%;
+ overflow: hidden;
+ margin: 0;
+}
+
+#main {
+ height: 100%;
+ overflow: auto;
+}
+
+#spacer {
+ height: 5000px;
+}
+
+#sticky {
+ position: sticky;
+ top: 50%;
+ left: 0;
+ width: 100%;
+ height: 100px;
+ background: red;
+ overflow: auto;
+}
+
+#long {
+ height: 250px;
+ width: 50%;
+ position: absolute;
+ background: green;
+ top: 0;
+ left: 25%;
+}
+ </style>
+</head>
+<body>
+ <div id="main">
+ <div id="sticky">
+ <div id="long">
+ </div>
+ </div>
+ <div id="spacer">
+ </div>
+ </div>
+</body>
+<script type="application/javascript">
+
+async function test() {
+ // Scroll to the bottom of the sticky position element that should not
+ // allow overscroll handoff.
+ sticky.scrollTop = sticky.scrollHeight;
+
+ // After scrolling to bottom tick the refresh driver.
+ await promiseFrame();
+
+ info("Start: sticky=" + sticky.scrollTop + " main=" + main.scrollTop);
+
+ let transformEnd = promiseTransformEnd();
+
+ // Async scroll the sticky element by 200 pixels using the mouse-wheel.
+ // This should handoff the overscroll to the parent element.
+ await promiseMoveMouseAndScrollWheelOver(sticky, 25, 25, false, 200);
+
+ // Wait for the trasform triggered by the gesture to complete.
+ await transformEnd;
+ await promiseApzFlushedRepaints();
+
+ info("After scroll: sticky=" + sticky.scrollTop + " main=" + main.scrollTop);
+
+ // Ensure that the main element has scrolled.
+ isnot(main.scrollTop, 0, "The overscroll should handoff");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+</script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_programmatic_scroll_behavior.html b/gfx/layers/apz/test/mochitest/helper_programmatic_scroll_behavior.html
new file mode 100644
index 0000000000..721ce7e538
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_programmatic_scroll_behavior.html
@@ -0,0 +1,81 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <script src="apz_test_utils.js"></script>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+ html, body { margin: 0; }
+
+ #big {
+ height: 250vh;
+ width: 100%;
+ }
+
+ #target {
+ height: 500px;
+ width: 100%;
+ background: red;
+ }
+ </style>
+</head>
+<body>
+ <div id="big">
+ </div>
+ <div id="target">
+ </div>
+</body>
+<script>
+const searchParams = new URLSearchParams(location.search);
+
+async function test() {
+ // Count the number of scroll events that occur. Instant scrolls should only
+ // trigger one scroll event, so a scroll event count of 1 indicates that a
+ // instant scroll was conducted.
+ let scrollCount = 0;
+ window.addEventListener("scroll", (e) => {
+ scrollCount += 1;
+ });
+
+ let scrollendPromise = promiseScrollend();
+
+ // Call the given programmatic scroll with behavior: smooth.
+ switch (searchParams.get("action")) {
+ case "scrollIntoView":
+ target.scrollIntoView({behavior: "smooth"});
+ break;
+ case "scrollBy":
+ document.scrollingElement.scrollBy({top: 500, behavior: "smooth"});
+ break;
+ case "scrollTo":
+ document.scrollingElement.scrollTo({top: 500, behavior: "smooth"});
+ break;
+ case "scroll":
+ document.scrollingElement.scroll({top: 500, behavior: "smooth"});
+ break;
+ default:
+ ok(false, "Unsupported action: " + searchParams.get("action"));
+ break;
+ }
+
+ await scrollendPromise;
+
+ // If general.smoothScroll is set, the behavior of the scroll should be
+ // "smooth". If general.smoothScroll is disabled, we should respect it and
+ // the scrolls should instant regardless of the specified behavior.
+ if (SpecialPowers.getBoolPref("general.smoothScroll")) {
+ info("final enabled scroll count: " + scrollCount);
+ ok(scrollCount > 1, "The programmatic scroll should create more than one scroll event");
+ } else {
+ info("final disabled scroll count: " + scrollCount);
+ ok(scrollCount == 1, "The programmatic scroll should be instant with one scroll event");
+ }
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+</script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_relative_scroll_smoothness.html b/gfx/layers/apz/test/mochitest/helper_relative_scroll_smoothness.html
new file mode 100644
index 0000000000..c8907c6d5d
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_relative_scroll_smoothness.html
@@ -0,0 +1,141 @@
+<!DOCTYPE html>
+<html>
+<meta charset="utf-8">
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<script src="/tests/SimpleTest/NativeKeyCodes.js"></script>
+<script src="/tests/SimpleTest/paint_listener.js"></script>
+<script src="apz_test_utils.js"></script>
+<script src="apz_test_native_event_utils.js"></script>
+<title>What happens if main thread scrolls?</title>
+<style>
+html, body { margin: 0; }
+
+html {
+ background:
+ repeating-linear-gradient(45deg, transparent 0, transparent 100px, rgba(0,0,0,0.1) 0, rgba(0,0,0,0.1) 200px),
+ repeating-linear-gradient(-45deg, transparent 0, transparent 100px, rgba(0,0,0,0.1) 0, rgba(0,0,0,0.1) 200px),
+ repeating-linear-gradient(to bottom, transparent 0, transparent 500px, rgba(0,0,0,0.4) 0, rgba(0,0,0,0.4) 1000px),
+ repeating-linear-gradient(to bottom, hsl(0, 60%, 80%), hsl(0, 60%, 80%) 200px, hsl(70, 60%, 80%) 0, hsl(70, 60%, 80%) 400px, hsl(140, 60%, 80%) 0, hsl(140, 60%, 80%) 600px, hsl(210, 60%, 80%) 0, hsl(210, 60%, 80%) 800px),
+ white;
+ background-size:
+ 283px 283px,
+ 283px 283px,
+ 100px 1000px,
+ 100px 800px;
+}
+
+body {
+ height: 10000px;
+}
+</style>
+
+<script>
+const searchParams = new URLSearchParams(location.search);
+var intervalId;
+// Start periodic content expansions after we get a scroll event triggered by
+// a key press in test() function below, otherwise we may have same scroll
+// offsets caused by this script before we start scrolling.
+window.addEventListener("scroll", () => {
+ var offset = 0;
+ var initialBodyHeight = 10000;
+ intervalId = setInterval(() => {
+ // "Add content" at the top. We do this by making the body longer and adjusting the background position.
+ offset += 10;
+ document.documentElement.style.backgroundPosition = `0px ${offset}px`;
+ document.body.style.height = `${initialBodyHeight + offset}px`;
+
+ switch (searchParams.get("scroll-method")) {
+ case "scrollBy":
+ window.scrollBy(0, 10);
+ break;
+ case "scrollTop":
+ document.scrollingElement.scrollTop += 10;
+ break;
+ case "scrollTo":
+ window.scrollTo(0, window.scrollY + 10);
+ break;
+ default:
+ ok(false, "Unsupported scroll method: " + searchParams.get("scroll-method"));
+ break;
+ }
+
+ // Simulate some jank.
+ var freezeDurationInMilliseconds = 100;
+ var startTime = Date.now();
+ while (Date.now() - startTime < freezeDurationInMilliseconds) {} // eslint-disable-line no-empty
+ }, 300);
+}, { once: true });
+
+
+async function test() {
+ // Once this content starts scrolling, it triggers a 100ms jank every 300ms so
+ // sending arrow down keys for 1500ms will cause some jank.
+ const timeAtStart = performance.now();
+ while (performance.now() - timeAtStart < 1500) {
+ switch (searchParams.get("input-type")) {
+ case "key":
+ synthesizeKey("KEY_ArrowDown");
+ break;
+ case "native-key":
+ const DownArrowKeyCode = nativeArrowDownKey();
+ ok(synthesizeNativeKey(KEYBOARD_LAYOUT_EN_US,
+ DownArrowKeyCode, {} /* no modifier */,
+ "", ""),
+ "Dispatched an down arrow key event");
+ break;
+ case "wheel":
+ await synthesizeNativeWheel(window, 50, 50, 0, -50);
+ break;
+ default:
+ ok(false, "Unsupported input type: " + searchParams.get("input-type"));
+ break;
+ }
+ await promiseFrame(window);
+ }
+
+ // Stop the periodic expansions.
+ clearInterval(intervalId);
+
+ const records = collectSampledScrollOffsets(document.scrollingElement);
+
+ let previousRecord = { scrollOffsetY: 0, sampledTimeStamp: 0 };
+ for (const record of records) {
+ // Ignore offsets before scrolling.
+ if (record.scrollOffsetY == 0) {
+ continue;
+ }
+ ok(
+ record.scrollOffsetY > previousRecord.scrollOffsetY,
+ "scroll offset should be strictly monotonically increasing " +
+ "previous offset: " + previousRecord.scrollOffsetY +
+ ", offset: " + record.scrollOffsetY
+ );
+ ok(
+ record.sampledTimeStamp > previousRecord.sampledTimeStamp,
+ "sampled time stamp should be strictly monotonically increasing " +
+ "previous timestamp: " + previousRecord.sampledTimeStamp +
+ ", timestamp: " + record.sampledTimeStamp
+ );
+ previousRecord = record;
+ }
+}
+
+function isOnChaosMode() {
+ return SpecialPowers.Services.env.get("MOZ_CHAOSMODE");
+}
+
+if (searchParams.get("input-type") == "native-key" &&
+ getPlatform() != "mac" && getPlatform() != "windows") {
+ ok(true, "Skipping test because native key events are not supported on " +
+ getPlatform());
+ subtestDone();
+} else if (searchParams.get("input-type") == "native-key" &&
+ getPlatform() == "mac" && isOnChaosMode()) {
+ ok(true, "Skipping native-key tests on verify runs on Mac");
+ subtestDone();
+} else {
+ waitUntilApzStable()
+ .then(test)
+ .then(subtestDone, subtestFailed);
+}
+</script>
diff --git a/gfx/layers/apz/test/mochitest/helper_reset_zoom_bug1818967.html b/gfx/layers/apz/test/mochitest/helper_reset_zoom_bug1818967.html
new file mode 100644
index 0000000000..4a4d7e34ca
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_reset_zoom_bug1818967.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<html lang="en"><head>
+<meta http-equiv="content-type" content="text/html; charset=UTF-8">
+<meta charset="utf-8">
+<title>Test that we do not checkerboard after resetting the pinch-zoom scale</title>
+<script type="application/javascript" src="apz_test_utils.js"></script>
+<script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+<script src="/tests/SimpleTest/paint_listener.js"></script>
+<style>
+.scrolled {
+ width: 10000px;
+ height: 10000px;
+ background: linear-gradient(lime, cyan);
+}
+</style>
+</head><body>
+ <div class="scrolled"></div>
+</body>
+
+<script type="application/javascript">
+async function test() {
+ var utils = SpecialPowers.getDOMWindowUtils(window);
+ var scrollerId = utils.getViewId(document.documentElement);
+
+ // Zoom in to the maximum level
+ utils.setResolutionAndScaleTo(10.0);
+ await promiseApzFlushedRepaints();
+
+ // Scroll the layout viewport to around middle of the page
+ window.scrollTo(window.scrollMaxX / 2, window.scrollMaxY / 2);
+ await promiseApzFlushedRepaints();
+
+ // Scroll the visual viewport to the bottom of the layout viewport.
+ // This creates an offset between the visual and layout viewport
+ // offsets which is needed to trigger the bug.
+ utils.scrollToVisual(window.scrollX,
+ window.scrollY + (0.9 * document.documentElement.clientHeight),
+ utils.UPDATE_TYPE_MAIN_THREAD,
+ utils.SCROLL_MODE_INSTANT);
+ await promiseApzFlushedRepaints();
+
+ // Reset the zoom level to 1.0x
+ utils.setResolutionAndScaleTo(1.0);
+ await promiseApzFlushedRepaints();
+
+ // Assert that we're not checkerboarded
+ assertNotCheckerboarded(utils, scrollerId, "After resetting zoom level");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+</script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_scroll_anchoring_on_wheel.html b/gfx/layers/apz/test/mochitest/helper_scroll_anchoring_on_wheel.html
new file mode 100644
index 0000000000..12d307fe57
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_scroll_anchoring_on_wheel.html
@@ -0,0 +1,59 @@
+<!DOCTYPE HTML>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, minimum-scale=1.0">
+<title>Tests scroll anchoring updates in-progress wheel scrolling __relatively__</title>
+<script src="apz_test_utils.js"></script>
+<script src="apz_test_native_event_utils.js"></script>
+<script src="/tests/SimpleTest/paint_listener.js"></script>
+<style>
+ body { margin: 0 }
+ #target > div {
+ height: 500px;
+ }
+</style>
+<div id="target"></div>
+<div class="spacer" style="height: 1000vh"></div>
+<script>
+ const targetElement = document.getElementById("target");
+
+ async function test() {
+ // Fistly send a wheel event to measure the scroll amount of one event.
+ let transformEndPromise = promiseTransformEnd();
+ await synthesizeNativeWheel(window, 50, 50, 0, -50);
+ await transformEndPromise;
+
+ ok(window.scrollY > 0, "Should be scrolled to some amount");
+
+ const firstScrollAmount = window.scrollY;
+
+ // Now send a wheel event again.
+ transformEndPromise = promiseTransformEnd();
+ await synthesizeNativeWheel(window, 50, 50, 0, -50);
+ await promiseApzFlushedRepaints();
+
+ // And insert an element during the wheel scrolling is still in progress.
+ targetElement.appendChild(document.createElement("div"));
+
+ await transformEndPromise;
+
+ // Give scroll offsets a chance to sync.
+ await promiseApzFlushedRepaints();
+
+ // Though in an ideal environment, the expected total scroll amount should
+ // be `firstScrollAmount * 2 + 500`, we don't expect it on our CIs, so we
+ // assume here;
+ // 1) it's greater than double of the first scroll amount since it should
+ // include the height of the inserted element.
+ // 2) it's greater than the first scroll amount + the height of the inserted
+ // element.
+ ok(window.scrollY > firstScrollAmount * 2,
+ "the scroll amount should be greater than double of the first scroll amount");
+ ok(window.scrollY > firstScrollAmount + 500,
+ "the scroll amount should also be greater than the first scroll amount + " +
+ "the inserted element height");
+ }
+
+ waitUntilApzStable()
+ .then(test)
+ .then(subtestDone, subtestFailed);
+</script>
diff --git a/gfx/layers/apz/test/mochitest/helper_scroll_anchoring_smooth_scroll.html b/gfx/layers/apz/test/mochitest/helper_scroll_anchoring_smooth_scroll.html
new file mode 100644
index 0000000000..7bdfa83f24
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_scroll_anchoring_smooth_scroll.html
@@ -0,0 +1,54 @@
+<!DOCTYPE HTML>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, minimum-scale=1.0">
+<title>Tests scroll anchoring interaction with smooth visual scrolling.</title>
+<script src="apz_test_utils.js"></script>
+<script src="apz_test_native_event_utils.js"></script>
+<script src="/tests/SimpleTest/paint_listener.js"></script>
+<style>
+ body { margin: 0 }
+ #target > div {
+ height: 500px;
+ }
+</style>
+<div id="target"></div>
+<div class="spacer" style="height: 200vh"></div>
+<script>
+ const utils = SpecialPowers.DOMWindowUtils;
+ const targetElement = document.getElementById("target");
+
+ async function test() {
+ const destY = window.scrollMaxY;
+ ok(destY > 0, "Should have some scroll range");
+
+ // Scroll to the bottom of the page.
+ window.scrollTo(0, destY);
+
+ is(window.scrollY, window.scrollMaxY, "Should be at the bottom");
+
+ // Register a TransformEnd observer so we can tell when the smooth scroll
+ // animation stops.
+ let transformEndPromise = promiseTransformEnd();
+
+ // Trigger smooth scrolling, and quickly insert an element which takes
+ // space into the DOM.
+ //
+ // It is important that it actually takes space so as to trigger scroll
+ // anchoring.
+ targetElement.scrollIntoView({ behavior: "smooth" });
+ targetElement.appendChild(document.createElement("div"));
+
+ // Wait for the TransformEnd.
+ await transformEndPromise;
+
+ // Give scroll offsets a chance to sync.
+ await promiseApzFlushedRepaints();
+
+ // Check that the async smooth scroll finished.
+ is(window.scrollY, 0, "Should've completed the smooth scroll without getting interrupted by scroll anchoring");
+ }
+
+ waitUntilApzStable()
+ .then(test)
+ .then(subtestDone, subtestFailed);
+</script>
diff --git a/gfx/layers/apz/test/mochitest/helper_scroll_anchoring_smooth_scroll_with_set_timeout.html b/gfx/layers/apz/test/mochitest/helper_scroll_anchoring_smooth_scroll_with_set_timeout.html
new file mode 100644
index 0000000000..07ba816c36
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_scroll_anchoring_smooth_scroll_with_set_timeout.html
@@ -0,0 +1,56 @@
+<!DOCTYPE HTML>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, minimum-scale=1.0">
+<title>Tests scroll anchoring interaction with smooth visual scrolling with set timeout.</title>
+<script src="apz_test_utils.js"></script>
+<script src="apz_test_native_event_utils.js"></script>
+<script src="/tests/SimpleTest/paint_listener.js"></script>
+<style>
+ body { margin: 0 }
+ #target > div {
+ height: 500px;
+ }
+</style>
+<div id="target"></div>
+<div class="spacer" style="height: 200vh"></div>
+<script>
+ const utils = SpecialPowers.DOMWindowUtils;
+ const targetElement = document.getElementById("target");
+
+ async function test() {
+ const destY = window.scrollMaxY;
+ ok(destY > 0, "Should have some scroll range");
+
+ // Scroll to the bottom of the page.
+ window.scrollTo(0, destY);
+
+ is(window.scrollY, window.scrollMaxY, "Should be at the bottom");
+
+ // Register a TransformEnd observer so we can tell when the smooth scroll
+ // animation stops.
+ let transformEndPromise = promiseTransformEnd();
+
+ // Trigger smooth scrolling, and insert an element which takes space into
+ // the DOM in a 20ms setTimeout callback.
+ //
+ // It is important that it actually takes space so as to trigger scroll
+ // anchoring.
+ targetElement.scrollIntoView({ behavior: "smooth" });
+ setTimeout(() => {
+ targetElement.appendChild(document.createElement("div"));
+ }, 20);
+
+ // Wait for the TransformEnd.
+ await transformEndPromise;
+
+ // Give scroll offsets a chance to sync.
+ await promiseApzFlushedRepaints();
+
+ // Check that the async smooth scroll finished.
+ is(window.scrollY, 0, "Should've completed the smooth scroll without getting interrupted by scroll anchoring");
+ }
+
+ waitUntilApzStable()
+ .then(test)
+ .then(subtestDone, subtestFailed);
+</script>
diff --git a/gfx/layers/apz/test/mochitest/helper_scroll_inactive_perspective.html b/gfx/layers/apz/test/mochitest/helper_scroll_inactive_perspective.html
new file mode 100644
index 0000000000..727d0e4fd1
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_scroll_inactive_perspective.html
@@ -0,0 +1,45 @@
+<head>
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Wheel-scrolling over inactive subframe with perspective</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+async function test() {
+ var subframe = document.getElementById("scroll");
+
+ // scroll over the middle of the subframe, to make sure it scrolls,
+ // not the page
+ var scrollPos = subframe.scrollTop;
+ await promiseMoveMouseAndScrollWheelOver(subframe, 100, 100);
+ dump("after scroll, subframe.scrollTop = " + subframe.scrollTop + "\n");
+ ok(subframe.scrollTop > scrollPos, "subframe scrolled after wheeling over it");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+ <style>
+ #scroll {
+ width: 200px;
+ height: 200px;
+ overflow: scroll;
+ perspective: 400px;
+ }
+ #scrolled {
+ width: 200px;
+ height: 1000px; /* so the subframe has room to scroll */
+ background: linear-gradient(red, blue); /* so you can see it scroll */
+ transform: translateZ(0px); /* so the perspective makes it to the display list */
+ }
+ </style>
+</head>
+<body>
+ <div id="scroll">
+ <div id="scrolled"></div>
+ </div>
+ <div style="height: 5000px;"></div><!-- So the page is scrollable as well -->
+</body>
diff --git a/gfx/layers/apz/test/mochitest/helper_scroll_inactive_zindex.html b/gfx/layers/apz/test/mochitest/helper_scroll_inactive_zindex.html
new file mode 100644
index 0000000000..44c3cf3217
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_scroll_inactive_zindex.html
@@ -0,0 +1,46 @@
+<head>
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Wheel-scrolling over inactive subframe with z-index</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+async function test() {
+ var subframe = document.getElementById("scroll");
+
+ // scroll over the middle of the subframe, and make sure that it scrolls,
+ // not the page
+ var scrollPos = subframe.scrollTop;
+ await promiseMoveMouseAndScrollWheelOver(subframe, 100, 100);
+ dump("after scroll, subframe.scrollTop = " + subframe.scrollTop + "\n");
+ ok(subframe.scrollTop > scrollPos, "subframe scrolled after wheeling over it");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+ <style>
+ #scroll {
+ width: 200px;
+ height: 200px;
+ overflow: scroll;
+ }
+ #scrolled {
+ width: 200px;
+ height: 1000px; /* so the subframe has room to scroll */
+ z-index: 2;
+ background: linear-gradient(red, blue); /* so you can see it scroll */
+ transform: translateZ(0px); /* to force active layers */
+ will-change: transform; /* to force active layers */
+ }
+ </style>
+</head>
+<body>
+ <div id="scroll">
+ <div id="scrolled"></div>
+ </div>
+ <div style="height: 5000px;"></div><!-- So the page is scrollable as well -->
+</body>
diff --git a/gfx/layers/apz/test/mochitest/helper_scroll_into_view_bug1516056.html b/gfx/layers/apz/test/mochitest/helper_scroll_into_view_bug1516056.html
new file mode 100644
index 0000000000..99952dddd4
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_scroll_into_view_bug1516056.html
@@ -0,0 +1,62 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width">
+ <title>Test for bug 1516056: "scroll into view" respects bounds on layout scroll position</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+ #target {
+ width: 100px;
+ height: 100px;
+ margin-left: 50%;
+ margin-right: 50%;
+ background: cyan;
+ }
+ </style>
+</head>
+<body>
+ <div id="target"></div>
+ <script>
+ let vv = window.visualViewport;
+ function getVisualScrollRange() {
+ let rootScroller = document.scrollingElement;
+ return {
+ width: rootScroller.scrollWidth - vv.width,
+ height: rootScroller.scrollHeight - vv.height,
+ };
+ }
+ async function test() {
+ is(window.scrollMaxX, 0, "page should have a zero horizontal layout scroll range");
+ is(window.scrollMaxY, 0, "page should have a zero vertical layout scroll range");
+ let visualScrollRange = getVisualScrollRange();
+ ok(visualScrollRange.width > 0, "page should have a nonzero horizontal visual scroll range");
+ ok(visualScrollRange.height > 0, "page should have a nonzero vertical visual scroll range");
+ let target = document.getElementById("target");
+
+ // Scroll target element into view. Wait until any visual scrolling is done before doing checks.
+ let scrollPromise = new Promise(resolve => {
+ vv.addEventListener("scroll", resolve, { once: true });
+ });
+ target.scrollIntoView();
+ await scrollPromise; // wait for visual viewport "scroll" event
+ await promiseApzFlushedRepaints();
+
+ // Test that scrollIntoView() respected the layout scroll range.
+ is(window.scrollX, 0, "page should not layout-scroll with a zero layout scroll range");
+ is(window.scrollY, 0, "page should not layout-scroll with a zero layout scroll range");
+
+ // Test that scrollIntoView() did perform visual scrolling.
+ let vvRect = getVisualViewportRect(vv);
+ let targetBounds = target.getBoundingClientRect();
+ assertRectContainment(vvRect, "visual viewport", targetBounds, "target element bounding rect");
+ }
+ SpecialPowers.getDOMWindowUtils(window).setResolutionAndScaleTo(2.0);
+
+ waitUntilApzStable()
+ .then(test)
+ .then(subtestDone, subtestFailed);
+ </script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_scroll_into_view_bug1562757.html b/gfx/layers/apz/test/mochitest/helper_scroll_into_view_bug1562757.html
new file mode 100644
index 0000000000..4aff2901d7
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_scroll_into_view_bug1562757.html
@@ -0,0 +1,64 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width">
+ <title>Test for bug 1562757: "scroll into view" in iframe respects bounds on layout scroll position</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+ #iframe {
+ width: 100px;
+ height: 100px;
+ margin-left: 50%;
+ margin-right: 50%;
+ background: cyan;
+ display: block;
+ }
+ </style>
+</head>
+<body>
+ <iframe id="iframe" scrolling="no" frameborder="no" srcdoc="<div id='target' style='width:100px;height:100px;'></div>"></iframe>
+
+ <script>
+ let vv = window.visualViewport;
+ function getVisualScrollRange() {
+ let rootScroller = document.scrollingElement;
+ return {
+ width: rootScroller.scrollWidth - vv.width,
+ height: rootScroller.scrollHeight - vv.height,
+ };
+ }
+ async function test() {
+ is(window.scrollMaxX, 0, "page should have a zero horizontal layout scroll range");
+ is(window.scrollMaxY, 0, "page should have a zero vertical layout scroll range");
+ let visualScrollRange = getVisualScrollRange();
+ ok(visualScrollRange.width > 0, "page should have a nonzero horizontal visual scroll range");
+ ok(visualScrollRange.height > 0, "page should have a nonzero vertical visual scroll range");
+ let target = iframe.contentDocument.getElementById("target");
+
+ // Scroll target element into view. Wait until any visual scrolling is done before doing checks.
+ let scrollPromise = new Promise(resolve => {
+ vv.addEventListener("scroll", resolve, { once: true });
+ });
+ target.scrollIntoView();
+ await scrollPromise; // wait for visual viewport "scroll" event
+ await promiseApzFlushedRepaints();
+
+ // Test that scrollIntoView() respected the layout scroll range.
+ is(window.scrollX, 0, "page should not layout-scroll with a zero layout scroll range");
+ is(window.scrollY, 0, "page should not layout-scroll with a zero layout scroll range");
+
+ // Test that scrollIntoView() did perform visual scrolling.
+ let vvRect = getVisualViewportRect(vv);
+ let targetBounds = iframe.getBoundingClientRect();
+ assertRectContainment(vvRect, "visual viewport", targetBounds, "iframe having the target element bounding rect");
+ }
+ SpecialPowers.getDOMWindowUtils(window).setResolutionAndScaleTo(2.0);
+
+ waitUntilApzStable()
+ .then(test)
+ .then(subtestDone, subtestFailed);
+ </script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_scroll_linked_effect_by_wheel.html b/gfx/layers/apz/test/mochitest/helper_scroll_linked_effect_by_wheel.html
new file mode 100644
index 0000000000..f9303253b8
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_scroll_linked_effect_by_wheel.html
@@ -0,0 +1,65 @@
+<!DOCTYPE html>
+<html>
+<meta charset="utf-8">
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<script src="/tests/SimpleTest/paint_listener.js"></script>
+<script src="apz_test_utils.js"></script>
+<script src="apz_test_native_event_utils.js"></script>
+<title>A scroll linked effect scrolled by wheel events</title>
+<style>
+html, body { margin: 0; }
+body {
+ height: 1000vh;
+}
+#target {
+ position: absolute;
+ height: 800px;
+ background-color: #cc00cc;
+ top: 0;
+ left: 0;
+ right: 0;
+}
+</style>
+<div id="target"></div>
+<script>
+// Set up a scroll linked effect element.
+window.addEventListener("scroll", () => {
+ target.style.top = window.scrollY + "px";
+});
+
+async function test() {
+ let rect = rectRelativeToScreen(target);
+ // We only care the top 10px here since the scroll linked effect on the
+ // main-thread can't keep up with the async scrolling by wheel events so that
+ // the position absolute element is often partially scrolled out.
+ rect.height = 10.0;
+ const initialSnapshot = await getSnapshot(rect);
+
+ let snapshots = [];
+ for (let i = 0; i < 10; i++) {
+ await synthesizeNativeWheel(window, 50, 50, 0, -10);
+ snapshots.push(await getSnapshot(rect));
+ }
+
+ const sampledData = collectSampledScrollOffsets(document.scrollingElement);
+ const hasPoint5FractionalOffset = sampledData.some(data => {
+ return SpecialPowers.wrap(data).scrollOffsetY.toString().split(".")?.[1] === "5"
+ });
+
+ if (hasPoint5FractionalOffset) {
+ todo(false, "Bug 1752789: There's at least one sampled scroll offset " +
+ "having .5 fractional part in such cases scroll linked effects can " +
+ "not be rendered at the same position of the async scroll offset");
+ return;
+ }
+
+ snapshots.forEach(snapshot => {
+ is(initialSnapshot, snapshot);
+ });
+}
+
+pushPrefs([["apz.test.logging_enabled", true]])
+.then(waitUntilApzStable)
+.then(test)
+.then(subtestDone, subtestFailed);
+</script>
diff --git a/gfx/layers/apz/test/mochitest/helper_scroll_linked_effect_detector.html b/gfx/layers/apz/test/mochitest/helper_scroll_linked_effect_detector.html
new file mode 100644
index 0000000000..aaa4b43829
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_scroll_linked_effect_detector.html
@@ -0,0 +1,108 @@
+<!DOCTYPE html>
+<html>
+<meta charset="utf-8">
+<script src="/tests/SimpleTest/paint_listener.js"></script>
+<script src="apz_test_utils.js"></script>
+<script src="apz_test_native_event_utils.js"></script>
+<title>ScrollLinkedEffectDetector tests</title>
+<style>
+html, body { margin: 0; }
+body {
+ height: 1000vh;
+}
+#target {
+ position: absolute;
+ height: 800px;
+ background-color: #cc00cc;
+ top: 0;
+ left: 0;
+ right: 0;
+}
+</style>
+<div id="target"></div>
+<script>
+async function test() {
+ let eventTimeStamp;
+ // Utility function to synthesize a scroll and call the given function
+ // in the event listener.
+ async function promiseScrollAndEvent(fn) {
+ let scrollEventPromise = new Promise(resolve => {
+ window.addEventListener("scroll", () => {
+ fn();
+ resolve();
+ }, { once: true });
+ });
+ await scrollEventPromise;
+ // Wait a rAF to make sure we are outside of the micro tasks for the scroll
+ // event callback so that we can ensure our stack based
+ // ScrollLinkedEffectDetector has been scoped out from the function firing
+ // scroll events.
+ await promiseFrame();
+ }
+
+ let intervalId = setInterval(async () => {
+ await synthesizeNativeWheel(window, 50, 50, 0, -10);
+ }, 0);
+ await promiseScrollAndEvent(() => {
+ eventTimeStamp = document.timeline.currentTime;
+ });
+ is(eventTimeStamp, document.timeline.currentTime,
+ `We are in same time frame where we got a scroll event at ${eventTimeStamp}`);
+ ok(!SpecialPowers.DOMWindowUtils.hasScrollLinkedEffect,
+ "No scroll-linked effect found yet");
+
+ // Setup a scroll-linked effect callback.
+ await promiseScrollAndEvent(() => {
+ isnot(window.scrollY, 0, "we've already scrolled some amount");
+ target.style.top = window.scrollY + "px";
+ eventTimeStamp = document.timeline.currentTime;
+ });
+ is(eventTimeStamp, document.timeline.currentTime,
+ `We are in same time frame where we got a scroll event at ${eventTimeStamp}`);
+ ok(SpecialPowers.DOMWindowUtils.hasScrollLinkedEffect,
+ "Scroll-linked effect found");
+
+ // A no-op again.
+ await promiseScrollAndEvent(() => {
+ eventTimeStamp = document.timeline.currentTime;
+ });
+ is(eventTimeStamp, document.timeline.currentTime,
+ `We are in same time frame where we got a scroll event at ${eventTimeStamp}`);
+ ok(!SpecialPowers.DOMWindowUtils.hasScrollLinkedEffect,
+ "No scroll-linked effect found");
+
+ // Setup a non-effective scroll-linked effect callback.
+ await promiseScrollAndEvent(() => {
+ target.style.top = getComputedStyle(target).top;
+ eventTimeStamp = document.timeline.currentTime;
+ });
+ is(eventTimeStamp, document.timeline.currentTime,
+ `We are in same time frame where we got a scroll event at ${eventTimeStamp}`);
+ ok(!SpecialPowers.DOMWindowUtils.hasScrollLinkedEffect,
+ "No scroll-linked effect found");
+
+ // Setup a callback to remove the style.
+ await promiseScrollAndEvent(() => {
+ target.style.top = "";
+ eventTimeStamp = document.timeline.currentTime;
+ });
+ is(eventTimeStamp, document.timeline.currentTime,
+ `We are in same time frame where we got a scroll event at ${eventTimeStamp}`);
+ ok(SpecialPowers.DOMWindowUtils.hasScrollLinkedEffect,
+ "Scroll-linked effect found");
+
+ // Setup a no-op callback.
+ await promiseScrollAndEvent(() => {
+ eventTimeStamp = document.timeline.currentTime;
+ });
+ is(eventTimeStamp, document.timeline.currentTime,
+ `We are in same time frame where we got a scroll event at ${eventTimeStamp}`);
+ ok(!SpecialPowers.DOMWindowUtils.hasScrollLinkedEffect,
+ "No scroll-linked effect found this time");
+ clearInterval(intervalId);
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+</script>
diff --git a/gfx/layers/apz/test/mochitest/helper_scroll_on_position_fixed.html b/gfx/layers/apz/test/mochitest/helper_scroll_on_position_fixed.html
new file mode 100644
index 0000000000..5fbbc1437f
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_scroll_on_position_fixed.html
@@ -0,0 +1,60 @@
+<head>
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Wheel-scrolling over position:fixed and position:sticky elements, in the top-level document as well as iframes</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+async function test() {
+ var iframeWin = document.getElementById("iframe").contentWindow;
+
+ // scroll over the middle of the iframe's position:sticky element, check
+ // that it scrolls the iframe
+ var scrollPos = iframeWin.scrollY;
+ await promiseMoveMouseAndScrollWheelOver(iframeWin.document.body, 50, 150);
+ ok(iframeWin.scrollY > scrollPos, "iframe scrolled after wheeling over the position:sticky element");
+
+ // same, but using the iframe's position:fixed element
+ scrollPos = iframeWin.scrollY;
+ await promiseMoveMouseAndScrollWheelOver(iframeWin.document.body, 250, 150);
+ ok(iframeWin.scrollY > scrollPos, "iframe scrolled after wheeling over the position:fixed element");
+
+ // same, but scrolling the scrollable frame *inside* the position:fixed item
+ var fpos = document.getElementById("fpos_scrollable");
+ scrollPos = fpos.scrollTop;
+ await promiseMoveMouseAndScrollWheelOver(fpos, 50, 150);
+ ok(fpos.scrollTop > scrollPos, "scrollable item inside fixed-pos element scrolled");
+ // wait for it to layerize fully and then try again
+ await promiseAllPaintsDone();
+ await promiseOnlyApzControllerFlushed();
+ scrollPos = fpos.scrollTop;
+ await promiseMoveMouseAndScrollWheelOver(fpos, 50, 150);
+ ok(fpos.scrollTop > scrollPos, "scrollable item inside fixed-pos element scrolled after layerization");
+
+ // same, but using the top-level window's position:sticky element
+ scrollPos = window.scrollY;
+ await promiseMoveMouseAndScrollWheelOver(document.body, 50, 150);
+ ok(window.scrollY > scrollPos, "top-level document scrolled after wheeling over the position:sticky element");
+
+ // same, but using the top-level window's position:fixed element
+ scrollPos = window.scrollY;
+ await promiseMoveMouseAndScrollWheelOver(document.body, 250, 150);
+ ok(window.scrollY > scrollPos, "top-level document scrolled after wheeling over the position:fixed element");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</head>
+<body style="height:5000px; margin:0">
+ <div style="position:sticky; width: 100px; height: 300px; top: 0; background-color:red">sticky</div>
+ <div style="position:fixed; width: 100px; height: 300px; top: 0; left: 200px; background-color: green">fixed</div>
+ <iframe id='iframe' width="300" height="400" srcdoc="<body style='height:5000px; margin:0'><div style='position:sticky; width:100px; height:300px; top: 0; background-color:red'>sticky</div><div style='position:fixed; right:0; top: 0; width:100px; height:300px; background-color:green'>fixed</div>"></iframe>
+
+ <div id="fpos_scrollable" style="position:fixed; width: 100px; height: 300px; top: 0; left: 400px; background-color: red; overflow:scroll">
+ <div style="background-color: blue; height: 1000px; margin: 3px">scrollable content inside a fixed-pos item</div>
+ </div>
+</body>
diff --git a/gfx/layers/apz/test/mochitest/helper_scroll_over_scrollbar.html b/gfx/layers/apz/test/mochitest/helper_scroll_over_scrollbar.html
new file mode 100644
index 0000000000..b7a8698cf8
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_scroll_over_scrollbar.html
@@ -0,0 +1,48 @@
+<head>
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Wheel-scrolling over scrollbar</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+async function test() {
+ var subframe = document.getElementById("scroll");
+
+ // scroll over the scrollbar, and make sure the subframe scrolls
+ var scrollPos = subframe.scrollTop;
+ if (subframe.clientWidth == 200) {
+ // No scrollbar, abort the test. This can happen e.g. on local macOS runs
+ // with OS settings to only show scrollbars on trackpad/mouse activity.
+ ok(false, "No scrollbars found, cannot run this test!");
+ return;
+ }
+ var scrollbarX = (200 + subframe.clientWidth) / 2;
+ await promiseNativeWheelAndWaitForScrollEvent(subframe, scrollbarX, 100,
+ 0, -10);
+ ok(subframe.scrollTop > scrollPos, "subframe scrolled after wheeling over scrollbar");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+ <style>
+ #scroll {
+ width: 200px;
+ height: 200px;
+ overflow: scroll;
+ }
+ #scrolled {
+ width: 200px;
+ height: 1000px; /* so the subframe has room to scroll */
+ will-change: transform; /* to force active layers */
+ }
+ </style>
+</head>
+<body>
+ <div id="scroll">
+ <div id="scrolled"></div>
+ </div>
+</body>
diff --git a/gfx/layers/apz/test/mochitest/helper_scroll_snap_no_valid_snap_position.html b/gfx/layers/apz/test/mochitest/helper_scroll_snap_no_valid_snap_position.html
new file mode 100644
index 0000000000..00f5e7d344
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_scroll_snap_no_valid_snap_position.html
@@ -0,0 +1,45 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>No snapping occurs if there is no valid snap position</title>
+ <script src="apz_test_utils.js"></script>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+ div {
+ position: absolute;
+ }
+ #scroller {
+ width: 100%;
+ height: 500px;
+ overflow-y: scroll;
+ scroll-snap-type: y mandatory;
+ }
+ .child {
+ width: 100%;
+ height: 100px;
+ background-color: blue;
+ }
+ </style>
+</head>
+<body>
+ <div id="scroller">
+ <div class="child" style="top: 0px;"></div>
+ <div style="width: 100%; height: 2000px;"></div>
+ <div class="child" style="top: 1000px;"></div>
+ </div>
+ <script type="application/javascript">
+ async function test() {
+ await promiseMoveMouseAndScrollWheelOver(scroller, 100, 100);
+
+ ok(scroller.scrollTop > 0, "Scroll should happen some amount");
+ }
+
+ waitUntilApzStable()
+ .then(test)
+ .then(subtestDone, subtestFailed);
+ </script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_scroll_snap_not_resnap_during_panning.html b/gfx/layers/apz/test/mochitest/helper_scroll_snap_not_resnap_during_panning.html
new file mode 100644
index 0000000000..f28a2f9396
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_scroll_snap_not_resnap_during_panning.html
@@ -0,0 +1,93 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>Skip re-snapping during pan gesture</title>
+ <script src="apz_test_utils.js"></script>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+ body {
+ margin: 0;
+ }
+ div {
+ position: absolute;
+ }
+ #scroller {
+ width: 100%;
+ height: 500px;
+ overflow-y: scroll;
+ scroll-snap-type: y mandatory;
+ }
+ .child {
+ width: 100%;
+ height: 100px;
+ background-color: blue;
+ scroll-snap-align: start;
+ }
+ </style>
+</head>
+<body>
+ <div id="scroller">
+ <div class="child" style="top: 0px;"></div>
+ <div id="spacer" style="width: 100%; height: 2000px;"></div>
+ </div>
+ <script type="application/javascript">
+ async function test() {
+ is(scroller.scrollTop, 0, "The initial snap point is at 0px");
+
+ let scrollEventPromise = waitForScrollEvent(scroller);
+
+ // Start a pan gesture downward.
+ await promiseNativePanGestureEventAndWaitForObserver(
+ scroller,
+ 100,
+ 100,
+ 0,
+ -10,
+ 1 /* kCGScrollPhaseBegan */);
+
+ await scrollEventPromise;
+
+ ok(scroller.scrollTop > 0, "The pan-start should scroll");
+
+ // Expand the scrollable region during the panning.
+ spacer.style.height = "2200px";
+
+ isnot(scroller.scrollTop, 0, "Do not re-snap to the original 0px");
+ let previousScrollPosition = scroller.scrollTop;
+
+ scrollEventPromise = waitForScrollEvent(scroller);
+ // Finish the pan gesture now.
+ await promiseNativePanGestureEventAndWaitForObserver(
+ scroller,
+ 100,
+ 100,
+ 0,
+ 0 /* 0 velocity to avoid further scrolling by this event */,
+ 4 /* kCGScrollPhaseEnd */);
+
+ await scrollEventPromise;
+
+ // Make sure the new scroll positions have reached to the main-thread.
+ await promiseApzFlushedRepaints();
+
+ // There's no good way to tell whether the snapping has finished, has
+ // reached to the snap destination, so we just check whether the current
+ // scroll position is a bit scrolled back toward the snap destination.
+ ok(scroller.scrollTop < previousScrollPosition,
+ "The pan-end should trigger snapping toward 0px");
+ }
+
+ if (getPlatform() == "mac") {
+ waitUntilApzStable()
+ .then(test)
+ .then(subtestDone, subtestFailed);
+ } else {
+ ok(true, "Skipping test because this test works only on mac");
+ subtestDone();
+ }
+ </script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_scroll_snap_not_resnap_during_scrollbar_dragging.html b/gfx/layers/apz/test/mochitest/helper_scroll_snap_not_resnap_during_scrollbar_dragging.html
new file mode 100644
index 0000000000..ca2be3916f
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_scroll_snap_not_resnap_during_scrollbar_dragging.html
@@ -0,0 +1,105 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>Skip re-snapping during scrollbar dragging</title>
+ <script src="apz_test_utils.js"></script>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+ body {
+ margin: 0;
+ }
+ div {
+ position: absolute;
+ }
+ #scroller {
+ width: 100%;
+ height: 500px;
+ overflow-y: scroll;
+ scroll-snap-type: y mandatory;
+ }
+ .child {
+ width: 100%;
+ height: 100px;
+ background-color: blue;
+ scroll-snap-align: start;
+ }
+ </style>
+</head>
+<body>
+ <div id="scroller">
+ <div class="child" style="top: 0px;"></div>
+ <div id="spacer" style="width: 100%; height: 2000px;"></div>
+ </div>
+ <script type="application/javascript">
+ async function test() {
+ is(scroller.scrollTop, 0, "The initial snap point is at 0px");
+
+ // Move onto the scroll thumb for the scroller element.
+ let w = {}, h = {};
+ SpecialPowers.DOMWindowUtils.getScrollbarSizes(scroller, w, h);
+ if (w.value == 0) {
+ ok(true, "No scrollbar, can't do this test");
+ return;
+ }
+
+ const x = scroller.clientWidth + w.value / 2;
+ await promiseNativeMouseEventWithAPZ({
+ target: scroller,
+ offsetX: x,
+ offsetY: w.value + 5,
+ type: "mousemove"});
+
+ // Start dragging the thumb.
+ await promiseNativeMouseEventWithAPZ({
+ target: scroller,
+ offsetX: x,
+ offsetY: w.value + 5,
+ type: "mousedown",
+ });
+
+ // Move down the thumb to scroll.
+ let scrollEventPromise = waitForScrollEvent(scroller);
+ await promiseNativeMouseEventWithAPZ({
+ target: scroller,
+ offsetX: x,
+ offsetY: w.value + 10,
+ type: "mousemove"});
+ await scrollEventPromise;
+
+ ok(scroller.scrollTop > 0, "Dragging the scroll thumb should scroll");
+
+ // Expand the scrollable region during the dragging.
+ spacer.style.height = "2200px";
+
+ isnot(scroller.scrollTop, 0, "Do not re-snap to the original 0px");
+ let previousScrollPosition = scroller.scrollTop;
+
+ scrollEventPromise = waitForScrollEvent(scroller);
+ // Release the mouse button, it will trigger snapping.
+ await promiseNativeMouseEventWithAPZ({
+ target: scroller,
+ offsetX: x,
+ offsetY: 10,
+ type: "mouseup"});
+
+ await scrollEventPromise;
+
+ // Make sure the new scroll positions have reached to the main-thread.
+ await promiseApzFlushedRepaints();
+
+ // There's no good way to tell whether the snapping has finished, has
+ // reached to the snap destination, so we just check whether the current
+ // scroll position is a bit scrolled back toward the snap destination.
+ ok(scroller.scrollTop < previousScrollPosition,
+ "Releasing the mouse button should trigger snapping toward 0px");
+ }
+
+ waitUntilApzStable()
+ .then(test)
+ .then(subtestDone, subtestFailed);
+ </script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_scroll_snap_on_page_down_scroll.html b/gfx/layers/apz/test/mochitest/helper_scroll_snap_on_page_down_scroll.html
new file mode 100644
index 0000000000..d0779cdb2d
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_scroll_snap_on_page_down_scroll.html
@@ -0,0 +1,84 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>Page scroll snaps a snap point in the same page rather than the one in the next page</title>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+ div {
+ position: absolute;
+ margin: 0px;
+ }
+ body {
+ margin: 0px;
+ }
+ html {
+ overflow-y: scroll;
+ scroll-snap-type: y mandatory;
+ }
+ .snap {
+ scroll-snap-align: start;
+ background: green;
+ }
+ </style>
+</head>
+<body>
+ <div class="snap" style="top: 0vh; width: 100%; height: 20px;">1</div>
+ <div class="snap" style="top: 50vh; width: 100%; height: 20px;">2</div>
+ <div class="snap" style="top: 102vh; width: 100%; height: 20px;">3</div>
+ <div class="snap" style="top: 300vh; width: 100%; height: 20px;">4</div>
+ <div class="snap" style="top: 400vh; width: 100%; height: 20px;">5</div>
+
+ <script type="application/javascript">
+ async function test() {
+ const expectedPosition =
+ document.querySelectorAll(".snap")[1].getBoundingClientRect().top;
+
+ const keyPromise = promiseOneEvent(window, "keydown", null);
+ window.synthesizeKey("KEY_PageDown");
+ await keyPromise;
+
+ let startedScroll = false;
+ let sameScrollPosition = false;
+ let prevScrollPos = window.scrollY;
+ while (true) {
+ // Flush APZ repaints to ensure that scroll offset changes from
+ // a compositor sample reach the content process.
+ await promiseApzFlushedRepaints();
+
+ let scrollPos = window.scrollY;
+ if (scrollPos == prevScrollPos) {
+ if (startedScroll) {
+ // If we had the same scroll position in two frames consecutively,
+ // we consider scroll has finished.
+ if (sameScrollPosition) {
+ break;
+ }
+ sameScrollPosition = true;
+ }
+ } else {
+ sameScrollPosition = false;
+ }
+
+ if (!startedScroll && scrollPos > 0) {
+ startedScroll = true;
+ }
+ prevScrollPos = scrollPos;
+ }
+
+ is(window.scrollY, expectedPosition,
+ "Snaps to the second snap point rather than the third snap point " +
+ "which was initially out of the scrollport even if it's closer to " +
+ "the top of the next page than the second snap point");
+ }
+
+ waitUntilApzStable()
+ .then(test)
+ .then(subtestDone, subtestFailed);
+ </script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_scroll_snap_resnap_after_async_scroll.html b/gfx/layers/apz/test/mochitest/helper_scroll_snap_resnap_after_async_scroll.html
new file mode 100644
index 0000000000..ff1c78bef5
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_scroll_snap_resnap_after_async_scroll.html
@@ -0,0 +1,81 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>Re-snapping to the last snapped element on APZ</title>
+ <script src="apz_test_utils.js"></script>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+ body {
+ margin: 0;
+ }
+ div {
+ position: absolute;
+ }
+ #scroller {
+ width: 100%;
+ height: 500px;
+ overflow-y: scroll;
+ scroll-snap-type: y proximity; /* to escape from the last snap point */
+ }
+ .child {
+ width: 100%;
+ height: 100px;
+ background-color: blue;
+ scroll-snap-align: start;
+ }
+ </style>
+</head>
+<body>
+ <div id="scroller">
+ <div class="child" style="top: 0px;"></div>
+ <div class="child" style="top: 200px;"></div>
+ <div id="spacer" style="width: 100%; height: 2000px;"></div>
+ </div>
+ <script type="application/javascript">
+ async function test() {
+ const proximityThreshold =
+ SpecialPowers.getIntPref("layout.css.scroll-snap.proximity-threshold");
+
+ let transformEndPromise = promiseTransformEnd();
+ await promiseMoveMouseAndScrollWheelOver(scroller, 100, 100);
+
+ await transformEndPromise;
+ await promiseApzFlushedRepaints();
+
+ is(scroller.scrollTop, 200, "snap to 200px");
+
+ document.querySelectorAll(".child")[1].style.top = "400px";
+ is(scroller.scrollTop, 400, "re-snap to 400px");
+
+ // Make sure the new snap position has been reflected in APZ side.
+ await promiseApzFlushedRepaints();
+
+ // Now trigger another wheel scroll to escape from the last snap point.
+ transformEndPromise = promiseTransformEnd();
+ // With a small `layout.css.scroll-behavior.spring-constant` preference
+ // value (it's set in test_group_scroll_snap.html), an async scroll
+ // operation, e.g. a wheel scroll operation, fires multiple scroll events
+ // so that here we wait the first scroll event here by specifying
+ // `waitForScroll=true`, there should be other scroll events after the
+ // first one.
+ await promiseMoveMouseAndScrollWheelOver(scroller, 100, 100,
+ true /* waitForScroll */, proximityThreshold + 1000);
+
+ // Expand the scrollable region during the wheel scroll.
+ spacer.style.height = "2200px";
+
+ await transformEndPromise;
+ await promiseApzFlushedRepaints();
+
+ isnot(scroller.scrollTop, 400, "Do not re-snap to the original 400px point");
+ }
+
+ waitUntilApzStable()
+ .then(test)
+ .then(subtestDone, subtestFailed);
+ </script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_scroll_snap_resnap_after_async_scrollBy.html b/gfx/layers/apz/test/mochitest/helper_scroll_snap_resnap_after_async_scrollBy.html
new file mode 100644
index 0000000000..2b034da933
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_scroll_snap_resnap_after_async_scrollBy.html
@@ -0,0 +1,72 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>Re-snapping to the last snapped element on APZ</title>
+ <script src="apz_test_utils.js"></script>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+ body {
+ margin: 0;
+ }
+ div {
+ position: absolute;
+ }
+ #scroller {
+ width: 100%;
+ height: 500px;
+ overflow-y: scroll;
+ scroll-snap-type: y proximity; /* to escape from the last snap point */
+ }
+ .child {
+ width: 100%;
+ height: 100px;
+ background-color: blue;
+ scroll-snap-align: start;
+ }
+ </style>
+</head>
+<body>
+ <div id="scroller">
+ <div class="child" style="top: 0px;"></div>
+ <div class="child" style="top: 200px;"></div>
+ <div id="spacer" style="width: 100%; height: 2000px;"></div>
+ </div>
+ <script type="application/javascript">
+ async function test() {
+ const proximityThreshold =
+ SpecialPowers.getIntPref("layout.css.scroll-snap.proximity-threshold");
+
+ let transformEndPromise = promiseTransformEnd();
+ scroller.scrollBy({left: 0, top: 100, behavior: "smooth"});
+
+ await transformEndPromise;
+ await promiseApzFlushedRepaints();
+
+ is(scroller.scrollTop, 200, "snap to 200px");
+
+ document.querySelectorAll(".child")[1].style.top = "400px";
+ is(scroller.scrollTop, 400, "re-snap to 400px");
+
+ // Now trigger another async scroll to escape from the last snap point.
+ transformEndPromise = promiseTransformEnd();
+ scroller.scrollBy({left: 0, top: proximityThreshold + 100,
+ behavior: "smooth"});
+
+ // Expand the scrollable region during the async scroll.
+ spacer.style.height = "2200px";
+
+ await transformEndPromise;
+ await promiseApzFlushedRepaints();
+
+ isnot(scroller.scrollTop, 400, "Do not re-snap to the original 400px point");
+ }
+
+ waitUntilApzStable()
+ .then(test)
+ .then(subtestDone, subtestFailed);
+ </script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_scroll_tables_perspective.html b/gfx/layers/apz/test/mochitest/helper_scroll_tables_perspective.html
new file mode 100644
index 0000000000..404274d3f4
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_scroll_tables_perspective.html
@@ -0,0 +1,66 @@
+<!DOCTYPE html>
+<head>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+async function test() {
+ var subframe = document.getElementById("content-wrapper");
+
+ // scroll over the middle of the subframe, to make sure it scrolls,
+ // not the page
+ var scrollPos = subframe.scrollTop;
+ await promiseMoveMouseAndScrollWheelOver(subframe, 100, 100);
+ ok(subframe.scrollTop > scrollPos, "subframe scrolled after wheeling over it");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+ <style>
+ html {
+ perspective:1000px;
+ overflow: hidden;
+ }
+ #fullscreen-wrapper {
+ display:table;
+ visibility:hidden;
+ width:100%;
+ height:100%;
+ position:fixed;
+ top:0;
+ left:0;
+ overflow:hidden;
+ z-index:9999;
+ perspective:1000px;
+ }
+ #content-wrapper {
+ overflow-y:auto;
+ height: 100vh;
+ }
+ #content-content {
+ min-height: 10000px;
+ }
+ </style>
+</head>
+<body>
+ <div id="fullscreen-wrapper">
+ <div></div>
+ </div>
+ <div id="content-wrapper">
+ <div id="content-content">
+ A<br>
+ B<br>
+ C<br>
+ D<br>
+ E<br>
+ f<br>
+ g<br>
+ h<br>
+ i<br>
+ j<br>
+ </div>
+ </div>
+</body>
diff --git a/gfx/layers/apz/test/mochitest/helper_scroll_thumb_dragging.html b/gfx/layers/apz/test/mochitest/helper_scroll_thumb_dragging.html
new file mode 100644
index 0000000000..821cf00be7
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_scroll_thumb_dragging.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/paint_listener.js"></script>
+<script src="apz_test_utils.js"></script>
+<script src="apz_test_native_event_utils.js"></script>
+<style>
+iframe {
+ width: 500px;
+ height: 50px; /* lower height relative to the root scrollable height */
+}
+</style>
+<div style="height: 200vh;"></div>
+<iframe src="http://example.org/"></iframe>
+<div style="height: 200vh;"></div>
+<script>
+ // Silence SimpleTest warning about missing assertions by having it wait
+ // indefinitely. We don't need to give it an explicit finish because the
+ // entire window this test runs in will be closed after subtestDone is called.
+ SimpleTest.waitForExplicitFinish();
+</script>
diff --git a/gfx/layers/apz/test/mochitest/helper_scrollbar_snap_bug1501062.html b/gfx/layers/apz/test/mochitest/helper_scrollbar_snap_bug1501062.html
new file mode 100644
index 0000000000..ec18fd856d
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_scrollbar_snap_bug1501062.html
@@ -0,0 +1,135 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Exercising the slider.snapMultiplier code</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+</head>
+<body>
+ <div id="scrollable" style="width: 300px; height: 300px; overflow: auto">
+ <div id="filler" style="height: 2000px; background-image: linear-gradient(red,blue)"></div>
+ </div>
+</body>
+<script type="text/javascript">
+async function test() {
+ // Note that this pref is a read-once-on-startup pref so we can't change it
+ // and have the change take effect. Instead we just use the value to determine
+ // what the expected behaviour is.
+ var snapMultiplier = SpecialPowers.getIntPref("slider.snapMultiplier");
+
+ // Much of the code below is "inlined" from promiseVerticalScrollbarDrag. Reusing
+ // that code was nontrivial given the modifications we needed to make, and
+ // would have increased the complexity of that helper function more than I'd
+ // like. However if any bugfixes are made to that function this code might
+ // need to be updated as well.
+
+ var scrollableDiv = document.getElementById("scrollable");
+ var boundingClientRect = scrollableDiv.getBoundingClientRect();
+ var verticalScrollbarWidth = boundingClientRect.width - scrollableDiv.clientWidth;
+ if (verticalScrollbarWidth == 0) {
+ ok(true, "No scrollbar, can't do this test");
+ return;
+ }
+
+ // register a scroll listener for the initial drag
+ let scrollPromise = new Promise(resolve => {
+ scrollableDiv.addEventListener("scroll", resolve, {once: true});
+ });
+
+ var upArrowHeight = verticalScrollbarWidth; // assume square scrollbar buttons
+ var mouseX = scrollableDiv.clientWidth + (verticalScrollbarWidth / 2);
+ var mouseY = upArrowHeight + 5; // start dragging somewhere in the thumb
+
+ dump("Starting drag at " + mouseX + ", " + mouseY + " from top-left of #" + scrollableDiv.id + "\n");
+
+ // Move the mouse to the scrollbar thumb and drag it down
+ await promiseNativeMouseEventWithAPZ({
+ target: scrollableDiv,
+ offsetX: mouseX,
+ offsetY: mouseY,
+ type: "mousemove",
+ });
+ await promiseNativeMouseEventWithAPZ({
+ target: scrollableDiv,
+ offsetX: mouseX,
+ offsetY: mouseY,
+ type: "mousedown",
+ });
+ // drag down by 100 pixels
+ mouseY += 100;
+ await promiseNativeMouseEventWithAPZ({
+ target: scrollableDiv,
+ offsetX: mouseX,
+ offsetY: mouseY,
+ type: "mousemove",
+ });
+
+ // wait here until the scroll event listener is triggered.
+ await scrollPromise;
+ var savedScrollPos = scrollableDiv.scrollTop;
+ ok(savedScrollPos > 0, "Scrolled to " + savedScrollPos);
+
+ // register a new scroll event listener. The next mousemove below will either
+ // trigger the snapback behaviour (if snapMultiplier > 0) or trigger a vertical
+ // scroll (if snapMultiplier == 0) because of the x- and y-coordinates we move
+ // the mouse to. This allows us to wait for a scroll event in either case.
+ // If we only triggered the snapback case then waiting for the scroll to
+ // "not happen" in the other case would be more error-prone.
+ scrollPromise = new Promise(resolve => {
+ scrollableDiv.addEventListener("scroll", resolve, {once: true});
+ });
+ // Add 2 to snapMultipler just to make sure we get far enough away from the scrollbar
+ var snapBackDistance = (snapMultiplier + 2) * verticalScrollbarWidth;
+ await promiseNativeMouseEventWithAPZ({
+ target: scrollableDiv,
+ offsetX: mouseX + snapBackDistance,
+ offsetY: mouseY + 10,
+ type: "mousemove",
+ });
+
+ // wait here until the scroll happens
+ await scrollPromise;
+ if (snapMultiplier > 0) {
+ ok(scrollableDiv.scrollTop == 0, "Scroll position snapped back to " + scrollableDiv.scrollTop);
+ } else {
+ ok(scrollableDiv.scrollTop > savedScrollPos, "Scroll position increased to " + scrollableDiv.scrollTop);
+ }
+
+ // Now we move the mouse back to the old position to ensure the scroll position
+ // gets restored properly
+ scrollPromise = new Promise(resolve => {
+ scrollableDiv.addEventListener("scroll", resolve, {once: true});
+ });
+ await promiseNativeMouseEventWithAPZ({
+ target: scrollableDiv,
+ offsetX: mouseX,
+ offsetY: mouseY,
+ type: "mousemove",
+ });
+
+ // wait here until the scroll happens
+ await scrollPromise;
+ ok(scrollableDiv.scrollTop == savedScrollPos, "Scroll position was restored to " + scrollableDiv.scrollTop);
+
+ // Release mouse and ensure the scroll position stuck
+ await promiseNativeMouseEventWithAPZ({
+ target: scrollableDiv,
+ offsetX: mouseX,
+ offsetY: mouseY,
+ type: "mouseup",
+ });
+ // Flush everything just to be safe
+ await promiseOnlyApzControllerFlushed();
+
+ ok(scrollableDiv.scrollTop == savedScrollPos, "Final scroll position was " + scrollableDiv.scrollTop);
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+</script>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_scrollbarbutton_repeat.html b/gfx/layers/apz/test/mochitest/helper_scrollbarbutton_repeat.html
new file mode 100644
index 0000000000..723b250bd8
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_scrollbarbutton_repeat.html
@@ -0,0 +1,101 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Basic test that click and hold on a scrollbar button works as expected</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="text/javascript">
+
+
+async function test() {
+ let utils = SpecialPowers.getDOMWindowUtils(window);
+ let scroller = document.getElementById('scroller');
+ let w = {}, h = {};
+ utils.getScrollbarSizes(scroller, w, h);
+ let verticalScrollbarWidth = w.value;
+ if (verticalScrollbarWidth == 0) {
+ ok(false, "No scrollbar, test will fail");
+ }
+ let downArrowHeight = verticalScrollbarWidth; // assume square scrollbar buttons
+ var mouseX = scroller.clientWidth + verticalScrollbarWidth / 2;
+ let mouseY = scroller.clientHeight - (downArrowHeight / 2);
+
+ let waitForScroll = waitForScrollEvent(scroller);
+
+ info("Synthesizing click at (" + mouseX + ", " + mouseY);
+ await promiseNativeMouseEventWithAPZ({
+ type: "click",
+ target: scroller,
+ offsetX: mouseX,
+ offsetY: mouseY,
+ });
+
+ await waitForScroll;
+
+ // Sanity-check: we should have scrolled.
+ ok(scroller.scrollTop > 0, "Should have scrolled by clicking the scrollbar button");
+
+ let startPos = scroller.scrollTop;
+
+ info("scroller.scrollTop " + scroller.scrollTop);
+
+ // mouse move over the scrollbar button
+ await promiseNativeMouseEventWithAPZ({
+ type: "mousemove",
+ target: scroller,
+ offsetX: mouseX,
+ offsetY: mouseY,
+ });
+ // mouse down
+ await promiseNativeMouseEventWithAPZ({
+ type: "mousedown",
+ target: scroller,
+ offsetX: mouseX,
+ offsetY: mouseY,
+ });
+
+ info("sent mouse down");
+
+ info("scroller.scrollTop " + scroller.scrollTop);
+
+ // mouse down on the scrollbar button and then wait until
+ // we scroll more 2x the distance of one click.
+ while ((scroller.scrollTop - startPos) < 2*startPos) {
+ let waitForScroll2 = waitForScrollEvent(scroller);
+ // Wait a bit
+ await SpecialPowers.promiseTimeout(50);
+ await waitForScroll2;
+ info("loop scroller.scrollTop " + scroller.scrollTop);
+ }
+
+ await promiseNativeMouseEventWithAPZ({
+ type: "mouseup",
+ target: scroller,
+ offsetX: mouseX,
+ offsetY: mouseY,
+ });
+
+ ok(true, "got enough scroll");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+ <style>
+ .spacer {
+ background-color: #212121;
+ height: 9000vh;
+ }
+ </style>
+</head>
+<body>
+ <div id="scroller" style="overflow: auto; width: 200px; height: 200px;">
+ <div class="spacer"></div>
+ </div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_scrollbarbuttonclick_checkerboard.html b/gfx/layers/apz/test/mochitest/helper_scrollbarbuttonclick_checkerboard.html
new file mode 100644
index 0000000000..e7b7895966
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_scrollbarbuttonclick_checkerboard.html
@@ -0,0 +1,75 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Test that repeated scrollbar button clicks do not cause checkerboarding</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="text/javascript">
+
+async function test() {
+ let utils = SpecialPowers.getDOMWindowUtils(window);
+ let scrollId = utils.getViewId(document.documentElement);
+ let w = {}, h = {};
+ utils.getScrollbarSizes(document.documentElement, w, h);
+ let verticalScrollbarWidth = w.value;
+ if (verticalScrollbarWidth == 0) {
+ ok(true, "No scrollbar, can't do this test");
+ }
+ let downArrowHeight = verticalScrollbarWidth; // assume square scrollbar buttons
+ var mouseX = document.documentElement.clientWidth + verticalScrollbarWidth / 2;
+ let mouseY = document.documentElement.clientHeight - (downArrowHeight / 2);
+
+ // Hold the mouse down on the scrollbar button and
+ // keep it down to trigger the "button repeat" codepath.
+ await promiseNativeMouseEventWithAPZ({
+ type: "mousedown",
+ target: window,
+ offsetX: mouseX,
+ offsetY: mouseY,
+ });
+
+ const repetitions = 20;
+ const repeat_delay = 50; // milliseconds
+ for (i = 0; i < repetitions; i++) {
+ // Wait for the results of the click (or, on subsequent iterations
+ // the repeat) to be painted.
+ await promiseFrame();
+
+ assertNotCheckerboarded(utils, scrollId, "after scrollbar button click-hold", true);
+
+ // Wait enough time to trigger the repeat timer.
+ await SpecialPowers.promiseTimeout(repeat_delay);
+ }
+
+ // Release mouse button to clean up.
+ await promiseNativeMouseEventWithAPZ({
+ type: "mouseup",
+ target: window,
+ offsetX: mouseX,
+ offsetY: mouseY,
+ });
+
+ // Sanity-check: we should have scrolled.
+ ok(window.scrollY > 0, "Should have scrolled by clicking the scrollbar button");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+ <style>
+ .page-footer {
+ background-color: #212121;
+ color: #fff;
+ height: 3000px;
+ }
+ </style>
+</head>
+<body>
+ <footer class="page-footer"></footer>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_scrollby_bug1531796.html b/gfx/layers/apz/test/mochitest/helper_scrollby_bug1531796.html
new file mode 100644
index 0000000000..5e6f0e1833
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_scrollby_bug1531796.html
@@ -0,0 +1,36 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Test that scrollBy() doesn't scroll more than it should</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+async function test() {
+ const maxSteps = 20;
+ let scrollPerStep = 40;
+ for (let step = 0; step < maxSteps; step++) {
+ window.scrollBy(0, scrollPerStep);
+ await promiseFrame();
+ }
+ is(window.scrollY, maxSteps * scrollPerStep, "Scrolled by the expected amount");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+ <style>
+ body {
+ height: 5000px;
+ background: linear-gradient(red, black);
+ }
+ </style>
+</head>
+<body>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_scrollend_bubbles.html b/gfx/layers/apz/test/mochitest/helper_scrollend_bubbles.html
new file mode 100644
index 0000000000..d0d763b474
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_scrollend_bubbles.html
@@ -0,0 +1,99 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <script src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+ html, body { margin: 0; }
+
+ body {
+ height: 10000px;
+ }
+
+ #container {
+ height: 500px;
+ width: 500px;
+ overflow: scroll;
+ }
+
+ .spacer {
+ height: 5000px;
+ width: 100%;
+ }
+ </style>
+ <script>
+const searchParams = new URLSearchParams(location.search);
+
+async function test() {
+ let scrollendCount = 0;
+
+ // When scrollend is fired at the document, the document and
+ // window event listeners should be fired.
+ let expectedScrollendCount = 2;
+ let scrollTarget = document.scrollingElement;
+
+ // The scrollend event should not bubble if the target is not Document.
+ function onElementScrollend(e) {
+ scrollendCount += 1;
+ is(e.bubbles, false, "Event bubbles should be false for Element");
+ }
+
+ // The scrollend event should bubble if the target is Document.
+ function onDocumentScrollend(e) {
+ scrollendCount += 1;
+ is(e.bubbles, true, "Event bubbles should be true for Document");
+ }
+
+ function failOnScrollend(e) {
+ ok(false, e.target + ": should not receive a scrollend event");
+ }
+
+ switch (searchParams.get("scroll-target")) {
+ case "document":
+ // The window and the document event listeners should be triggered.
+ document.addEventListener("scrollend", onDocumentScrollend);
+ window.addEventListener("scrollend", onDocumentScrollend);
+
+ // Fail if the element receives a scrollend event.
+ container.addEventListener("scrollend", failOnScrollend);
+ break;
+ case "element":
+ scrollTarget = document.getElementById("container");
+ expectedScrollendCount = 1;
+
+ // Only the the element event listener should be triggered.
+ container.addEventListener("scrollend", onElementScrollend);
+
+ // Fail if the document or window receive a scrollend event.
+ document.addEventListener("scrollend", failOnScrollend);
+ window.addEventListener("scrollend", failOnScrollend);
+ break;
+ default:
+ ok(false, "Unsupported scroll-target: " + searchParams.get("scroll-target"));
+ break;
+ }
+
+ // Call the scrollTo function on the target to trigger the scrollend.
+ scrollTarget.scrollBy({ top: 500, left: 0 });
+
+ // Ensure the refresh driver has ticked.
+ await promiseFrame();
+
+ // A scrollend event should be posted after the refresh driver has ticked.
+ is(scrollendCount, expectedScrollendCount,
+ "Trigger the expected number of scrollend events");
+}
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+ </script>
+</head>
+<body>
+ <div id="container">
+ <div class="spacer">
+ </div>
+ </div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_scrollframe_activation_on_load.html b/gfx/layers/apz/test/mochitest/helper_scrollframe_activation_on_load.html
new file mode 100644
index 0000000000..1947a89a8f
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_scrollframe_activation_on_load.html
@@ -0,0 +1,89 @@
+<!DOCTYPE HTML>
+<html id="root-element">
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1151663
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1151663, helper page</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+ // In this test we have a simple page which is scrollable, with a
+ // scrollable <div> which is also scrollable. We test that the
+ // <div> does not get an initial APZC, since primary scrollable
+ // frame is the page's root scroll frame.
+
+ function test() {
+ // Reconstruct the APZC tree structure in the last paint.
+ var apzcTree = getLastApzcTree();
+
+ // The apzc tree for this page should consist of a single root APZC,
+ // which either is the RCD with no child APZCs (e10s/B2G case) or has a
+ // single child APZC which is for the RCD (fennec case). If we activate
+ // all scrollframes then we expect there to be a child.
+ var rcd = findRcdNode(apzcTree);
+ ok(rcd != null, "found the RCD node");
+
+ // For reference when I run this test locally when I modified it to
+ // handle activateAllScrollFrames I got the following values:
+ // rootElement.clientWidth 1280
+ // rootElement.clientHeight 863
+ // rootDisplayPort: {"x":0,"y":0,"w":1280,"h":3200} non wr
+ // rootDisplayPort: {"x":0,"y":0,"w":1280,"h":1792} wr
+ // At this time activateAllScrollFrames is only active with wr fission:
+ // div displayPort: {"x":0,"y":0,"w":50,"h":50} wr fission
+ // theDiv.clientWidth: 50 wr fission
+ // theDiv.clientHeight: 50 wr fission
+
+ let config = getHitTestConfig();
+
+ let heightMultiplier = SpecialPowers.getCharPref("apz.y_stationary_size_multiplier");
+ // With WebRender, the effective height multiplier can be reduced
+ // for alignment reasons. The reduction should be no more than a
+ // factor of two.
+ heightMultiplier /= 2;
+ info("effective displayport height multipler is " + heightMultiplier);
+
+ let rootDisplayPort = getLastContentDisplayportFor('root-element');
+ let rootElement = document.getElementById('root-element');
+ info("rootDisplayPort is " + JSON.stringify(rootDisplayPort));
+ info("rootElement.clientWidth " + rootElement.clientWidth +
+ " rootElement.clientHeight " + rootElement.clientHeight);
+ ok(rootDisplayPort.width >= rootElement.clientWidth, "rootDisplayPort should be at least as wide as page");
+ ok(rootDisplayPort.height >= heightMultiplier * rootElement.clientHeight,
+ "rootDisplayPort should be at least as tall as page times heightMultiplier");
+
+ let displayPort = getLastContentDisplayportFor('thediv');
+ if (config.activateAllScrollFrames) {
+ is(rcd.children.length, 1, "expected one child on the RCD");
+ let theDiv = document.getElementById("thediv");
+ ok(displayPort != null, "should have displayPort");
+ info("div displayPort: " + JSON.stringify(displayPort));
+ info("div width: " + theDiv.clientWidth);
+ info("div height: " + theDiv.clientHeight);
+ ok(displayPort.width <= theDiv.clientWidth + 1, "displayPort w should have empty margins");
+ ok(displayPort.height <= theDiv.clientHeight + 1, "displayPort h should have empty margins");
+ } else {
+ is(rcd.children.length, 0, "expected no children on the RCD");
+ ok(displayPort == null, "should not have displayPort");
+ }
+ }
+ waitUntilApzStable()
+ .then(forceLayerTreeToCompositor)
+ .then(test)
+ .then(subtestDone, subtestFailed);
+ </script>
+</head>
+<body>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1151663">Mozilla Bug 1151663</a>
+ <div id="thediv" style="height: 50px; width: 50px; overflow: scroll">
+ <!-- Put enough content into the subframe to make it have a nonzero scroll range -->
+ <div style="height: 100px; width: 50px"></div>
+ </div>
+ <!-- Put enough content into the page to make it have a nonzero scroll range -->
+ <div style="height: 5000px"></div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_scrollto_tap.html b/gfx/layers/apz/test/mochitest/helper_scrollto_tap.html
new file mode 100644
index 0000000000..a1fb3f67a6
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_scrollto_tap.html
@@ -0,0 +1,59 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Sanity touch-tapping test</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+async function test() {
+ while (window.scrollY == 0) {
+ // the scrollframe is not yet marked as APZ-scrollable. Mark it so
+ // before continuing.
+ window.scrollTo(0, 1);
+ await promiseApzFlushedRepaints();
+ }
+
+ // This is a scroll by 20px that should use paint-skipping if possible.
+ // If paint-skipping is enabled, this should not trigger a paint, but go
+ // directly to the compositor using an empty transaction. We check for this
+ // by ensuring the document element did not get painted.
+ var utils = window.opener.SpecialPowers.getDOMWindowUtils(window);
+ var elem = document.documentElement;
+ var skipping = location.search == "?true";
+ utils.checkAndClearPaintedState(elem);
+ window.scrollTo(0, 20);
+ await promiseAllPaintsDone();
+
+ if (skipping) {
+ is(utils.checkAndClearPaintedState(elem), false, "Document element didn't get painted");
+ }
+
+ // After that's done, we click on the button to make sure the
+ // skipped-paint codepath still has working APZ event transformations.
+ let clickPromise = new Promise(resolve => {
+ document.addEventListener("click", resolve);
+ });
+
+ await synthesizeNativeTap(document.getElementById("b"), 5, 5, function() {
+ dump("Finished synthesizing tap, waiting for button to be clicked...\n");
+ });
+
+ let clickEvent = await clickPromise;
+ is(clickEvent.target, document.getElementById("b"), "Clicked on button, yay! (at " + clickEvent.clientX + "," + clickEvent.clientY + ")");
+}
+
+waitUntilApzStable()
+ .then(test)
+ .then(subtestDone, subtestFailed);
+
+ </script>
+</head>
+<body style="height: 5000px">
+ <div style="height: 50px">spacer</div>
+ <button id="b" style="width: 10px; height: 10px"></button>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_self_closer.html b/gfx/layers/apz/test/mochitest/helper_self_closer.html
new file mode 100644
index 0000000000..09a9286c06
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_self_closer.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="apz_test_utils.js"></script>
+<script src="/tests/SimpleTest/paint_listener.js"></script>
+<script>
+waitUntilApzStable().then(() => {
+ dump("Bye!\n");
+ window.close();
+});
+</script>
+
+See ya! (This window will close itself)
diff --git a/gfx/layers/apz/test/mochitest/helper_smoothscroll_spam.html b/gfx/layers/apz/test/mochitest/helper_smoothscroll_spam.html
new file mode 100644
index 0000000000..154ed3cafe
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_smoothscroll_spam.html
@@ -0,0 +1,51 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Test for scenario in bug 1228407</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+async function test() {
+ let utils = SpecialPowers.getDOMWindowUtils(window);
+ utils.advanceTimeAndRefresh(0);
+
+ // Part of the problem in bug 1228407 was that the main-thread scroll
+ // generation counter was continually increasing (due to scrollBy calls in
+ // quick succession), and so repaint requests from APZ would get ignored (due
+ // to stale scroll generation), and so the main thread scroll position would
+ // never actually get updated. This loop exercises that case. The expected
+ // behaviour (pre-APZ) was that the scrollBy call would actually start the
+ // scroll animation and advance the scroll position a little bit, so the next
+ // scrollBy call would move the animation destination a little bit, and so
+ // the loop would continue advancing the scroll position. The bug resulted
+ // in the scroll position not advancing at all.
+ for (let i = 0; i < 100; i++) {
+ document.scrollingElement.scrollBy({top:60, behavior: "smooth"});
+ await promiseOnlyApzControllerFlushed();
+ utils.advanceTimeAndRefresh(16);
+ }
+
+ utils.restoreNormalRefresh();
+ await promiseOnlyApzControllerFlushed();
+
+ let scrollPos = document.scrollingElement.scrollTop;
+ ok(scrollPos > 60, `Scrolled ${scrollPos}px, should be more than 60`);
+}
+
+waitUntilApzStable().then(test).then(subtestDone, subtestFailed);
+
+ </script>
+ <style>
+ body {
+ height: 5000px;
+ background: linear-gradient(red, black);
+ }
+ </style>
+</head>
+<body>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_smoothscroll_spam_interleaved.html b/gfx/layers/apz/test/mochitest/helper_smoothscroll_spam_interleaved.html
new file mode 100644
index 0000000000..003ae49ea5
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_smoothscroll_spam_interleaved.html
@@ -0,0 +1,57 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Test for scenario in bug 1228407 with two scrollframes</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+async function test() {
+ let utils = SpecialPowers.getDOMWindowUtils(window);
+ utils.advanceTimeAndRefresh(0);
+
+ // Basically the same setup as in helper_smoothscroll_spam.html, but
+ // with two scrollframes that get scrolled in an interleaved manner.
+ // The original fix for bug 1228407 left this scenario unhandled, with
+ // bug 1231177 tracking the problem. This test exercises the scenario.
+ let s1 = document.getElementById('s1');
+ let s2 = document.getElementById('s2');
+ for (let i = 0; i < 100; i++) {
+ s1.scrollBy({top:60, behavior: "smooth"});
+ s2.scrollBy({top:60, behavior: "smooth"});
+ await promiseOnlyApzControllerFlushed();
+ utils.advanceTimeAndRefresh(16);
+ }
+
+ utils.restoreNormalRefresh();
+ await promiseOnlyApzControllerFlushed();
+
+ let s1pos = s1.scrollTop;
+ let s2pos = s2.scrollTop;
+ ok(s1pos > 60, `s1 scrolled ${s1pos}px, should be more than 60`);
+ ok(s2pos > 60, `s2 scrolled ${s2pos}px, should be more than 60`);
+}
+
+waitUntilApzStable().then(test).then(subtestDone, subtestFailed);
+
+ </script>
+ <style>
+ .scrollable {
+ overflow: scroll;
+ height: 300px;
+ }
+
+ .content {
+ height: 1000px;
+ background-image: linear-gradient(green, blue);
+ }
+ </style>
+</head>
+<body>
+ <div id="s1" class="scrollable"><div class="content"></div></div>
+ <div id="s2" class="scrollable"><div class="content"></div></div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_subframe_style.css b/gfx/layers/apz/test/mochitest/helper_subframe_style.css
new file mode 100644
index 0000000000..5af9640802
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_subframe_style.css
@@ -0,0 +1,15 @@
+body {
+ height: 500px;
+}
+
+.inner-frame {
+ margin-top: 50px; /* this should be at least 30px */
+ height: 200%;
+ width: 75%;
+ overflow: scroll;
+}
+.inner-content {
+ height: 200%;
+ width: 200%;
+ background: repeating-linear-gradient(#EEE, #EEE 100px, #DDD 100px, #DDD 200px);
+}
diff --git a/gfx/layers/apz/test/mochitest/helper_tall.html b/gfx/layers/apz/test/mochitest/helper_tall.html
new file mode 100644
index 0000000000..7fde795fdc
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_tall.html
@@ -0,0 +1,504 @@
+<html id="tall_html">
+<body>
+This is a tall page<br/>
+1<br/>
+2<br/>
+3<br/>
+4<br/>
+5<br/>
+6<br/>
+7<br/>
+8<br/>
+9<br/>
+10<br/>
+11<br/>
+12<br/>
+13<br/>
+14<br/>
+15<br/>
+16<br/>
+17<br/>
+18<br/>
+19<br/>
+20<br/>
+21<br/>
+22<br/>
+23<br/>
+24<br/>
+25<br/>
+26<br/>
+27<br/>
+28<br/>
+29<br/>
+30<br/>
+31<br/>
+32<br/>
+33<br/>
+34<br/>
+35<br/>
+36<br/>
+37<br/>
+38<br/>
+39<br/>
+40<br/>
+41<br/>
+42<br/>
+43<br/>
+44<br/>
+45<br/>
+46<br/>
+47<br/>
+48<br/>
+49<br/>
+50<br/>
+51<br/>
+52<br/>
+53<br/>
+54<br/>
+55<br/>
+56<br/>
+57<br/>
+58<br/>
+59<br/>
+60<br/>
+61<br/>
+62<br/>
+63<br/>
+64<br/>
+65<br/>
+66<br/>
+67<br/>
+68<br/>
+69<br/>
+70<br/>
+71<br/>
+72<br/>
+73<br/>
+74<br/>
+75<br/>
+76<br/>
+77<br/>
+78<br/>
+79<br/>
+80<br/>
+81<br/>
+82<br/>
+83<br/>
+84<br/>
+85<br/>
+86<br/>
+87<br/>
+88<br/>
+89<br/>
+90<br/>
+91<br/>
+92<br/>
+93<br/>
+94<br/>
+95<br/>
+96<br/>
+97<br/>
+98<br/>
+99<br/>
+100<br/>
+101<br/>
+102<br/>
+103<br/>
+104<br/>
+105<br/>
+106<br/>
+107<br/>
+108<br/>
+109<br/>
+110<br/>
+111<br/>
+112<br/>
+113<br/>
+114<br/>
+115<br/>
+116<br/>
+117<br/>
+118<br/>
+119<br/>
+120<br/>
+121<br/>
+122<br/>
+123<br/>
+124<br/>
+125<br/>
+126<br/>
+127<br/>
+128<br/>
+129<br/>
+130<br/>
+131<br/>
+132<br/>
+133<br/>
+134<br/>
+135<br/>
+136<br/>
+137<br/>
+138<br/>
+139<br/>
+140<br/>
+141<br/>
+142<br/>
+143<br/>
+144<br/>
+145<br/>
+146<br/>
+147<br/>
+148<br/>
+149<br/>
+150<br/>
+151<br/>
+152<br/>
+153<br/>
+154<br/>
+155<br/>
+156<br/>
+157<br/>
+158<br/>
+159<br/>
+160<br/>
+161<br/>
+162<br/>
+163<br/>
+164<br/>
+165<br/>
+166<br/>
+167<br/>
+168<br/>
+169<br/>
+170<br/>
+171<br/>
+172<br/>
+173<br/>
+174<br/>
+175<br/>
+176<br/>
+177<br/>
+178<br/>
+179<br/>
+180<br/>
+181<br/>
+182<br/>
+183<br/>
+184<br/>
+185<br/>
+186<br/>
+187<br/>
+188<br/>
+189<br/>
+190<br/>
+191<br/>
+192<br/>
+193<br/>
+194<br/>
+195<br/>
+196<br/>
+197<br/>
+198<br/>
+199<br/>
+200<br/>
+201<br/>
+202<br/>
+203<br/>
+204<br/>
+205<br/>
+206<br/>
+207<br/>
+208<br/>
+209<br/>
+210<br/>
+211<br/>
+212<br/>
+213<br/>
+214<br/>
+215<br/>
+216<br/>
+217<br/>
+218<br/>
+219<br/>
+220<br/>
+221<br/>
+222<br/>
+223<br/>
+224<br/>
+225<br/>
+226<br/>
+227<br/>
+228<br/>
+229<br/>
+230<br/>
+231<br/>
+232<br/>
+233<br/>
+234<br/>
+235<br/>
+236<br/>
+237<br/>
+238<br/>
+239<br/>
+240<br/>
+241<br/>
+242<br/>
+243<br/>
+244<br/>
+245<br/>
+246<br/>
+247<br/>
+248<br/>
+249<br/>
+250<br/>
+251<br/>
+252<br/>
+253<br/>
+254<br/>
+255<br/>
+256<br/>
+257<br/>
+258<br/>
+259<br/>
+260<br/>
+261<br/>
+262<br/>
+263<br/>
+264<br/>
+265<br/>
+266<br/>
+267<br/>
+268<br/>
+269<br/>
+270<br/>
+271<br/>
+272<br/>
+273<br/>
+274<br/>
+275<br/>
+276<br/>
+277<br/>
+278<br/>
+279<br/>
+280<br/>
+281<br/>
+282<br/>
+283<br/>
+284<br/>
+285<br/>
+286<br/>
+287<br/>
+288<br/>
+289<br/>
+290<br/>
+291<br/>
+292<br/>
+293<br/>
+294<br/>
+295<br/>
+296<br/>
+297<br/>
+298<br/>
+299<br/>
+300<br/>
+301<br/>
+302<br/>
+303<br/>
+304<br/>
+305<br/>
+306<br/>
+307<br/>
+308<br/>
+309<br/>
+310<br/>
+311<br/>
+312<br/>
+313<br/>
+314<br/>
+315<br/>
+316<br/>
+317<br/>
+318<br/>
+319<br/>
+320<br/>
+321<br/>
+322<br/>
+323<br/>
+324<br/>
+325<br/>
+326<br/>
+327<br/>
+328<br/>
+329<br/>
+330<br/>
+331<br/>
+332<br/>
+333<br/>
+334<br/>
+335<br/>
+336<br/>
+337<br/>
+338<br/>
+339<br/>
+340<br/>
+341<br/>
+342<br/>
+343<br/>
+344<br/>
+345<br/>
+346<br/>
+347<br/>
+348<br/>
+349<br/>
+350<br/>
+351<br/>
+352<br/>
+353<br/>
+354<br/>
+355<br/>
+356<br/>
+357<br/>
+358<br/>
+359<br/>
+360<br/>
+361<br/>
+362<br/>
+363<br/>
+364<br/>
+365<br/>
+366<br/>
+367<br/>
+368<br/>
+369<br/>
+370<br/>
+371<br/>
+372<br/>
+373<br/>
+374<br/>
+375<br/>
+376<br/>
+377<br/>
+378<br/>
+379<br/>
+380<br/>
+381<br/>
+382<br/>
+383<br/>
+384<br/>
+385<br/>
+386<br/>
+387<br/>
+388<br/>
+389<br/>
+390<br/>
+391<br/>
+392<br/>
+393<br/>
+394<br/>
+395<br/>
+396<br/>
+397<br/>
+398<br/>
+399<br/>
+400<br/>
+401<br/>
+402<br/>
+403<br/>
+404<br/>
+405<br/>
+406<br/>
+407<br/>
+408<br/>
+409<br/>
+410<br/>
+411<br/>
+412<br/>
+413<br/>
+414<br/>
+415<br/>
+416<br/>
+417<br/>
+418<br/>
+419<br/>
+420<br/>
+421<br/>
+422<br/>
+423<br/>
+424<br/>
+425<br/>
+426<br/>
+427<br/>
+428<br/>
+429<br/>
+430<br/>
+431<br/>
+432<br/>
+433<br/>
+434<br/>
+435<br/>
+436<br/>
+437<br/>
+438<br/>
+439<br/>
+440<br/>
+441<br/>
+442<br/>
+443<br/>
+444<br/>
+445<br/>
+446<br/>
+447<br/>
+448<br/>
+449<br/>
+450<br/>
+451<br/>
+452<br/>
+453<br/>
+454<br/>
+455<br/>
+456<br/>
+457<br/>
+458<br/>
+459<br/>
+460<br/>
+461<br/>
+462<br/>
+463<br/>
+464<br/>
+465<br/>
+466<br/>
+467<br/>
+468<br/>
+469<br/>
+470<br/>
+471<br/>
+472<br/>
+473<br/>
+474<br/>
+475<br/>
+476<br/>
+477<br/>
+478<br/>
+479<br/>
+480<br/>
+481<br/>
+482<br/>
+483<br/>
+484<br/>
+485<br/>
+486<br/>
+487<br/>
+488<br/>
+489<br/>
+490<br/>
+491<br/>
+492<br/>
+493<br/>
+494<br/>
+495<br/>
+496<br/>
+497<br/>
+498<br/>
+499<br/>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_tap.html b/gfx/layers/apz/test/mochitest/helper_tap.html
new file mode 100644
index 0000000000..f987299447
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_tap.html
@@ -0,0 +1,32 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Sanity touch-tapping test</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+async function clickButton() {
+ document.addEventListener("click", clicked);
+
+ await synthesizeNativeTap(document.getElementById("b"), 5, 5, function() {
+ dump("Finished synthesizing tap, waiting for button to be clicked...\n");
+ });
+}
+
+function clicked(e) {
+ is(e.target, document.getElementById("b"), "Clicked on button, yay! (at " + e.clientX + "," + e.clientY + ")");
+ subtestDone();
+}
+
+waitUntilApzStable().then(clickButton);
+
+ </script>
+</head>
+<body>
+ <button id="b" style="width: 10px; height: 10px"></button>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_tap_default_passive.html b/gfx/layers/apz/test/mochitest/helper_tap_default_passive.html
new file mode 100644
index 0000000000..a1f276224b
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_tap_default_passive.html
@@ -0,0 +1,81 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Ensure APZ doesn't wait for passive listeners</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+var touchdownTime;
+
+async function longPressLink() {
+ await synthesizeNativeTouch(document.getElementById("b"), 5, 5, SpecialPowers.DOMWindowUtils.TOUCH_CONTACT, function() {
+ dump("Finished synthesizing touch-start, waiting for events...\n");
+ });
+}
+
+var touchstartReceived = false;
+function recordEvent(e) {
+ if (!touchstartReceived) {
+ touchstartReceived = true;
+ is(e.type, "touchstart", "Got a touchstart");
+ e.preventDefault(); // should be a no-op because it's a passive listener
+ return;
+ }
+
+ // If APZ decides to wait for the content response on a particular input block,
+ // it needs to wait until both the touchstart and touchmove event are handled
+ // by the main thread. In this case there is no touchmove at all, so APZ would
+ // end up waiting indefinitely and time out the test. The fact that we get this
+ // contextmenu event (mouselongtap on Windows) at all means that APZ decided
+ // not to wait for the content response, which is the desired behaviour, since
+ // the touchstart listener was registered as a passive listener.
+ if (getPlatform() == "windows") {
+ is(e.type, "mouselongtap", "Got a mouselongtap");
+ } else {
+ is(e.type, "contextmenu", "Got a contextmenu");
+ }
+ e.preventDefault();
+
+ setTimeout(async () => {
+ await synthesizeNativeTouch(document.getElementById("b"), 5, 5, SpecialPowers.DOMWindowUtils.TOUCH_REMOVE, function() {
+ dump("Finished synthesizing touch-end to clear state; finishing test...\n");
+ subtestDone();
+ });
+ }, 0);
+}
+
+function preventDefaultListener(e) {
+ e.preventDefault();
+}
+
+// Note, not passing 'passive'.
+window.addEventListener("touchstart", recordEvent, { capture: true });
+window.ontouchstart = preventDefaultListener;
+if (getPlatform() == "windows") {
+ SpecialPowers.addChromeEventListener("mouselongtap", recordEvent, true);
+} else {
+ window.addEventListener("contextmenu", recordEvent, true);
+}
+
+waitUntilApzStable()
+.then(longPressLink);
+
+ </script>
+</head>
+<body>
+ <a id="b" href="#">Link to nowhere</a>
+ <script>
+ document.addEventListener("touchstart", preventDefaultListener, { capture: true });
+ document.ontouchstart = preventDefaultListener;
+ document.documentElement.addEventListener("touchstart", preventDefaultListener, { capture: true });
+ document.documentElement.ontouchstart = preventDefaultListener;
+ document.body.addEventListener("touchstart", preventDefaultListener, { capture: true });
+ document.body.ontouchstart = preventDefaultListener;
+ document.body.setAttribute("ontouchstart", "event.preventDefault()");
+ </script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_tap_fullzoom.html b/gfx/layers/apz/test/mochitest/helper_tap_fullzoom.html
new file mode 100644
index 0000000000..7d739924f0
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_tap_fullzoom.html
@@ -0,0 +1,33 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Sanity touch-tapping test with fullzoom</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+async function clickButton() {
+ document.addEventListener("click", clicked);
+
+ await synthesizeNativeTap(document.getElementById("b"), 5, 5, function() {
+ dump("Finished synthesizing tap, waiting for button to be clicked...\n");
+ });
+}
+
+function clicked(e) {
+ is(e.target, document.getElementById("b"), "Clicked on button, yay! (at " + e.clientX + "," + e.clientY + ")");
+ subtestDone();
+}
+
+SpecialPowers.setFullZoom(window, 2.0);
+waitUntilApzStable().then(clickButton);
+
+ </script>
+</head>
+<body>
+ <button id="b" style="width: 10px; height: 10px; position: relative; top: 100px"></button>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_tap_passive.html b/gfx/layers/apz/test/mochitest/helper_tap_passive.html
new file mode 100644
index 0000000000..647564c08a
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_tap_passive.html
@@ -0,0 +1,66 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Ensure APZ doesn't wait for passive listeners</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+var touchdownTime;
+
+async function longPressLink() {
+ await synthesizeNativeTouch(document.getElementById("b"), 5, 5, SpecialPowers.DOMWindowUtils.TOUCH_CONTACT, function() {
+ dump("Finished synthesizing touch-start, waiting for events...\n");
+ });
+}
+
+var touchstartReceived = false;
+function recordEvent(e) {
+ if (!touchstartReceived) {
+ touchstartReceived = true;
+ is(e.type, "touchstart", "Got a touchstart");
+ e.preventDefault(); // should be a no-op because it's a passive listener
+ return;
+ }
+
+ // If APZ decides to wait for the content response on a particular input block,
+ // it needs to wait until both the touchstart and touchmove event are handled
+ // by the main thread. In this case there is no touchmove at all, so APZ would
+ // end up waiting indefinitely and time out the test. The fact that we get this
+ // contextmenu event (mouselongtap on Windows) at all means that APZ decided
+ // not to wait for the content response, which is the desired behaviour, since
+ // the touchstart listener was registered as a passive listener.
+ if (getPlatform() == "windows") {
+ is(e.type, "mouselongtap", "Got a mouselongtap");
+ } else {
+ is(e.type, "contextmenu", "Got a contextmenu");
+ }
+ e.preventDefault();
+
+ setTimeout(async () => {
+ await synthesizeNativeTouch(document.getElementById("b"), 5, 5, SpecialPowers.DOMWindowUtils.TOUCH_REMOVE, function() {
+ dump("Finished synthesizing touch-end to clear state; finishing test...\n");
+ subtestDone();
+ });
+ }, 0);
+}
+
+window.addEventListener("touchstart", recordEvent, { passive: true, capture: true });
+if (getPlatform() == "windows") {
+ SpecialPowers.addChromeEventListener("mouselongtap", recordEvent, true);
+} else {
+ window.addEventListener("contextmenu", recordEvent, true);
+}
+
+waitUntilApzStable()
+.then(longPressLink);
+
+ </script>
+</head>
+<body>
+ <a id="b" href="#">Link to nowhere</a>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_test_autoscrolling_in_oop_frame.html b/gfx/layers/apz/test/mochitest/helper_test_autoscrolling_in_oop_frame.html
new file mode 100644
index 0000000000..c1826f583f
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_test_autoscrolling_in_oop_frame.html
@@ -0,0 +1,9 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+</head>
+<body>
+ <iframe src="http://example.net" style="height: 90vh; width: 90vw;"></iframe>
+ <div style="height: 200px"></div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_test_reset_scaling_zoom.html b/gfx/layers/apz/test/mochitest/helper_test_reset_scaling_zoom.html
new file mode 100644
index 0000000000..31779410da
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_test_reset_scaling_zoom.html
@@ -0,0 +1,23 @@
+<html><head>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src='/tests/SimpleTest/paint_listener.js'></script>
+<script src='apz_test_utils.js'></script>
+<script src='apz_test_native_event_utils.js'></script>
+<script>
+async function doZoomIn() {
+ await waitUntilApzStable();
+ await pinchZoomInWithTouch(100, 100);
+ await promiseOnlyApzControllerFlushed();
+}
+
+// Silence SimpleTest warning about missing assertions by having it wait
+// indefinitely. We don't need to give it an explicit finish because the
+// entire window this test runs in will be closed after subtestDone is called.
+SimpleTest.waitForExplicitFinish();
+</script>
+</head>
+<body>
+Here is some text to stare at as the test runs. It serves no functional
+purpose, but gives you an idea of the zoom level. It's harder to tell what
+the zoom level is when the page is just solid white.
+</body></html>
diff --git a/gfx/layers/apz/test/mochitest/helper_test_select_popup_position.html b/gfx/layers/apz/test/mochitest/helper_test_select_popup_position.html
new file mode 100644
index 0000000000..c810751b2f
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_test_select_popup_position.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src='/tests/SimpleTest/paint_listener.js'></script>
+<script src='apz_test_utils.js'></script>
+<style>
+html, body {
+ margin: 0;
+ padding: 0;
+}
+select {
+ position: absolute;
+ top: 150px;
+ left: 150px;
+ height: 30px;
+}
+</style>
+<select><option>he he he</option><option>boo boo</option><option>baz baz</option></select>
+<script>
+ // Silence SimpleTest warning about missing assertions by having it wait
+ // indefinitely. We don't need to give it an explicit finish because the
+ // entire window this test runs in will be closed after subtestDone is called.
+ SimpleTest.waitForExplicitFinish();
+</script>
diff --git a/gfx/layers/apz/test/mochitest/helper_test_select_popup_position_transformed_in_parent.html b/gfx/layers/apz/test/mochitest/helper_test_select_popup_position_transformed_in_parent.html
new file mode 100644
index 0000000000..860ca079de
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_test_select_popup_position_transformed_in_parent.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src='/tests/SimpleTest/paint_listener.js'></script>
+<script src='apz_test_utils.js'></script>
+<style>
+html, body {
+ /* To calculate the popup window position easily. */
+ margin: 0;
+ padding: 0;
+}
+iframe {
+ width: 300px;
+ height: 300px;
+ /* To calculate the popup window position easily. */
+ border: none;
+}
+</style>
+<div style="transform: scale(2); transform-origin: top left;">
+ <iframe></iframe>
+</div>
+<script>
+ // Silence SimpleTest warning about missing assertions by having it wait
+ // indefinitely.
+ SimpleTest.waitForExplicitFinish();
+</script>
diff --git a/gfx/layers/apz/test/mochitest/helper_test_select_popup_position_zoomed.html b/gfx/layers/apz/test/mochitest/helper_test_select_popup_position_zoomed.html
new file mode 100644
index 0000000000..2ec079369e
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_test_select_popup_position_zoomed.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src='/tests/SimpleTest/paint_listener.js'></script>
+<script src='apz_test_utils.js'></script>
+<style>
+html, body {
+ /* To calculate the popup window position easily. */
+ margin: 0;
+ padding: 0;
+}
+iframe {
+ width: 300px;
+ height: 300px;
+ /* To calculate the popup window position easily. */
+ border: none;
+}
+</style>
+<iframe></iframe>
+<script>
+ // Call setResolutionAndScaleTo here to avoid bug 1691358.
+ SpecialPowers.getDOMWindowUtils(window).setResolutionAndScaleTo(2.0);
+ // Silence SimpleTest warning about missing assertions by having it wait
+ // indefinitely.
+ SimpleTest.waitForExplicitFinish();
+</script>
diff --git a/gfx/layers/apz/test/mochitest/helper_test_select_zoom.html b/gfx/layers/apz/test/mochitest/helper_test_select_zoom.html
new file mode 100644
index 0000000000..d3fc5fcb67
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_test_select_zoom.html
@@ -0,0 +1,43 @@
+<html><head>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src='/tests/SimpleTest/paint_listener.js'></script>
+<script src='apz_test_utils.js'></script>
+<script src='apz_test_native_event_utils.js'></script>
+<script>
+function getSelectRect() {
+ const input = document.getElementById("select");
+ let rect = input.getBoundingClientRect();
+ let x = rect.left;
+ let y = rect.top;
+
+ const relativeOffset = getRelativeViewportOffset(window);
+
+ let resolution = SpecialPowers.getDOMWindowUtils(window).getResolution();
+ x = resolution * (x - relativeOffset.x);
+ y = resolution * (y - relativeOffset.y);
+
+ let fullZoom = SpecialPowers.getFullZoom(window);
+ rect = {
+ left: x * fullZoom,
+ top: y * fullZoom,
+ width: rect.width * fullZoom * resolution,
+ height: rect.height * fullZoom * resolution,
+ };
+
+ return rect;
+}
+</script>
+</head>
+<body>
+Here is some text to stare at as the test runs. It serves no functional
+purpose, but gives you an idea of the zoom level. It's harder to tell what
+the zoom level is when the page is just solid white.
+<select id='select' style="position: absolute; left:150px; top:300px;"><option>he he he</option><option>boo boo</option><option>baz baz</option></select>
+
+<script>
+ // Silence SimpleTest warning about missing assertions by having it wait
+ // indefinitely. We don't need to give it an explicit finish because the
+ // entire window this test runs in will be closed after subtestDone is called.
+ SimpleTest.waitForExplicitFinish();
+</script>
+</body></html>
diff --git a/gfx/layers/apz/test/mochitest/helper_test_tab_drag_zoom.html b/gfx/layers/apz/test/mochitest/helper_test_tab_drag_zoom.html
new file mode 100644
index 0000000000..9f0175468c
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_test_tab_drag_zoom.html
@@ -0,0 +1,18 @@
+<html><head>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src='/tests/SimpleTest/paint_listener.js'></script>
+<script src='apz_test_utils.js'></script>
+<script src='apz_test_native_event_utils.js'></script>
+</head>
+<body>
+Here is some text to stare at as the test runs. It serves no functional
+purpose, but gives you an idea of the zoom level. It's harder to tell what
+the zoom level is when the page is just solid white.
+
+<script>
+ // Silence SimpleTest warning about missing assertions by having it wait
+ // indefinitely. We don't need to give it an explicit finish because the
+ // entire window this test runs in will be closed after subtestDone is called.
+ SimpleTest.waitForExplicitFinish();
+</script>
+</body></html>
diff --git a/gfx/layers/apz/test/mochitest/helper_touch_action.html b/gfx/layers/apz/test/mochitest/helper_touch_action.html
new file mode 100644
index 0000000000..10038de29f
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_touch_action.html
@@ -0,0 +1,123 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0,minimum-scale=1.0">
+ <title>Sanity touch-action test</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+function checkScroll(x, y, desc) {
+ is(window.scrollX, x, desc + " - x axis");
+ is(window.scrollY, y, desc + " - y axis");
+}
+
+async function test() {
+ var target = document.getElementById("target");
+
+ // drag the page up to scroll down by 50px
+ let touchEndPromise = promiseTouchEnd(document.body);
+ ok(await synthesizeNativeTouchDrag(target, 10, 100, 0, -50),
+ "Synthesized native vertical drag (1), waiting for touch-end event...");
+ await touchEndPromise;
+ await promiseOnlyApzControllerFlushed();
+ checkScroll(0, 50, "After first vertical drag, with pan-y" );
+
+ // switch style to pan-x
+ document.body.style.touchAction = "pan-x";
+ ok(true, "Waiting for pan-x to propagate...");
+ await promiseAllPaintsDone(null, true);
+ await promiseOnlyApzControllerFlushed();
+
+ // drag the page up to scroll down by 50px, but it won't happen because pan-x
+ touchEndPromise = promiseTouchEnd(document.body);
+ ok(await synthesizeNativeTouchDrag(target, 10, 100, 0, -50),
+ "Synthesized native vertical drag (2), waiting for touch-end event...");
+ await touchEndPromise;
+ await promiseOnlyApzControllerFlushed();
+ checkScroll(0, 50, "After second vertical drag, with pan-x");
+
+ // drag the page left to scroll right by 50px
+ touchEndPromise = promiseTouchEnd(document.body);
+ ok(await synthesizeNativeTouchDrag(target, 100, 10, -50, 0),
+ "Synthesized horizontal drag (1), waiting for touch-end event...");
+ await touchEndPromise;
+ await promiseOnlyApzControllerFlushed();
+ checkScroll(50, 50, "After first horizontal drag, with pan-x");
+
+ // drag the page diagonally right/down to scroll up/left by 40px each axis;
+ // only the x-axis will actually scroll because pan-x
+ touchEndPromise = promiseTouchEnd(document.body);
+ ok(await synthesizeNativeTouchDrag(target, 10, 10, 40, 40),
+ "Synthesized diagonal drag (1), waiting for touch-end event...");
+ await touchEndPromise;
+ await promiseOnlyApzControllerFlushed();
+ checkScroll(10, 50, "After first diagonal drag, with pan-x");
+
+ // switch style back to pan-y
+ document.body.style.touchAction = "pan-y";
+ ok(true, "Waiting for pan-y to propagate...");
+ await promiseAllPaintsDone(null, true);
+ await promiseOnlyApzControllerFlushed();
+
+ // drag the page diagonally right/down to scroll up/left by 40px each axis;
+ // only the y-axis will actually scroll because pan-y
+ touchEndPromise = promiseTouchEnd(document.body);
+ ok(await synthesizeNativeTouchDrag(target, 10, 10, 40, 40),
+ "Synthesized diagonal drag (2), waiting for touch-end event...");
+ await touchEndPromise;
+ await promiseOnlyApzControllerFlushed();
+ checkScroll(10, 10, "After second diagonal drag, with pan-y");
+
+ // switch style to none
+ document.body.style.touchAction = "none";
+ ok(true, "Waiting for none to propagate...");
+ await promiseAllPaintsDone(null, true);
+ await promiseOnlyApzControllerFlushed();
+
+ // drag the page diagonally up/left to scroll down/right by 40px each axis;
+ // neither will scroll because of touch-action
+ touchEndPromise = promiseTouchEnd(document.body);
+ ok(await synthesizeNativeTouchDrag(target, 100, 100, -40, -40),
+ "Synthesized diagonal drag (3), waiting for touch-end event...");
+ await touchEndPromise;
+ await promiseOnlyApzControllerFlushed();
+ checkScroll(10, 10, "After third diagonal drag, with none");
+
+ document.body.style.touchAction = "manipulation";
+ ok(true, "Waiting for manipulation to propagate...");
+ await promiseAllPaintsDone(null, true);
+ await promiseOnlyApzControllerFlushed();
+
+ // drag the page diagonally up/left to scroll down/right by 40px each axis;
+ // both will scroll because of touch-action
+ touchEndPromise = promiseTouchEnd(document.body);
+ ok(await synthesizeNativeTouchDrag(target, 100, 100, -40, -40),
+ "Synthesized diagonal drag (4), waiting for touch-end event...");
+ await touchEndPromise;
+ await promiseOnlyApzControllerFlushed();
+ checkScroll(50, 50, "After fourth diagonal drag, with manipulation");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</head>
+<body style="touch-action: pan-y">
+ <div style="width: 5000px; height: 5000px; background-color: lightgreen;">
+ This div makes the page scrollable on both axes.<br>
+ This is the second line of text.<br>
+ This is the third line of text.<br>
+ This is the fourth line of text.
+ </div>
+ <!-- This fixed-position div remains in the same place relative to the browser chrome, so we
+ can use it as a targeting device for synthetic touch events. The body will move around
+ as we scroll, so we'd have to be constantly adjusting the synthetic drag coordinates
+ if we used that as the target element. -->
+ <div style="position:fixed; left: 10px; top: 10px; width: 1px; height: 1px" id="target"></div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_touch_action_complex.html b/gfx/layers/apz/test/mochitest/helper_touch_action_complex.html
new file mode 100644
index 0000000000..b8df34bfca
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_touch_action_complex.html
@@ -0,0 +1,137 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Complex touch-action test</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+function checkScroll(target, x, y, desc) {
+ is(target.scrollLeft, x, desc + " - x axis");
+ is(target.scrollTop, y, desc + " - y axis");
+}
+
+async function resetConfiguration(config) {
+ // Cycle through all the configuration_X elements, setting them to display:none
+ // except for when X == config, in which case set it to display:block
+ var i = 0;
+ while (true) {
+ i++;
+ var element = document.getElementById("configuration_" + i);
+ if (element == null) {
+ if (i <= config) {
+ ok(false, "The configuration requested was not encountered!");
+ }
+ break;
+ }
+
+ if (i == config) {
+ element.style.display = "block";
+ } else {
+ element.style.display = "none";
+ }
+ }
+
+ // Also reset the scroll position on the scrollframe
+ var s = document.getElementById("scrollframe");
+ s.scrollLeft = 0;
+ s.scrollTop = 0;
+
+ await promiseAllPaintsDone();
+ await promiseApzFlushedRepaints();
+}
+
+async function test() {
+ var scrollframe = document.getElementById("scrollframe");
+
+ // Helper function for the tests below.
+ // Touch-pan configuration |configuration| towards scroll offset (dx, dy) with
+ // the pan touching down at (x, y). Check that the final scroll offset is
+ // (ex, ey). |desc| is some description string.
+ async function scrollAndCheck(configuration, x, y, dx, dy, ex, ey, desc) {
+ // Start with a clean slate
+ await resetConfiguration(configuration);
+ // Reverse the touch delta in order to scroll in the desired direction
+ dx = -dx;
+ dy = -dy;
+ // Do the pan
+ let touchEndPromise = promiseTouchEnd(document.body);
+ ok(await synthesizeNativeTouchDrag(scrollframe, x, y, dx, dy),
+ "Synthesized drag of (" + dx + ", " + dy + ") on configuration " + configuration);
+ await touchEndPromise;
+ await promiseAllPaintsDone();
+ await promiseOnlyApzControllerFlushed();
+ // Check for expected scroll position
+ checkScroll(scrollframe, ex, ey, "configuration " + configuration + " " + desc);
+ }
+
+ // Test configuration_1, which contains two sibling elements that are
+ // overlapping. The touch-action from the second sibling (which is on top)
+ // should be used for the overlapping area.
+ await scrollAndCheck(1, 25, 75, 20, 0, 20, 0, "first element horizontal scroll");
+ await scrollAndCheck(1, 25, 75, 0, 50, 0, 0, "first element vertical scroll");
+ await scrollAndCheck(1, 75, 75, 50, 0, 0, 0, "overlap horizontal scroll");
+ await scrollAndCheck(1, 75, 75, 0, 50, 0, 50, "overlap vertical scroll");
+ await scrollAndCheck(1, 125, 75, 20, 0, 0, 0, "second element horizontal scroll");
+ await scrollAndCheck(1, 125, 75, 0, 50, 0, 50, "second element vertical scroll");
+
+ // Test configuration_2, which contains two overlapping elements with a
+ // parent/child relationship. The parent has pan-x and the child has pan-y,
+ // which means that panning on the parent should work horizontally only, and
+ // on the child no panning should occur at all.
+ await scrollAndCheck(2, 125, 125, 50, 50, 0, 0, "child scroll");
+ await scrollAndCheck(2, 75, 75, 50, 50, 0, 0, "overlap scroll");
+ await scrollAndCheck(2, 25, 75, 0, 50, 0, 0, "parent vertical scroll");
+ await scrollAndCheck(2, 75, 25, 50, 0, 50, 0, "parent horizontal scroll");
+
+ // Test configuration_3, which is the same as configuration_2, except the child
+ // has a rotation transform applied. This forces the event regions on the two
+ // elements to be built separately and then get merged.
+ await scrollAndCheck(3, 125, 125, 50, 50, 0, 0, "child scroll");
+ await scrollAndCheck(3, 75, 75, 50, 50, 0, 0, "overlap scroll");
+ await scrollAndCheck(3, 25, 75, 0, 50, 0, 0, "parent vertical scroll");
+ await scrollAndCheck(3, 75, 25, 50, 0, 50, 0, "parent horizontal scroll");
+
+ // Test configuration_4 has two elements, one above the other, not overlapping,
+ // and the second element is a child of the first. The parent has pan-x, the
+ // child has pan-y, but that means panning horizontally on the parent should
+ // work and panning in any direction on the child should not do anything.
+ await scrollAndCheck(4, 75, 75, 50, 50, 50, 0, "parent diagonal scroll");
+ await scrollAndCheck(4, 75, 150, 50, 50, 0, 0, "child diagonal scroll");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</head>
+<body>
+ <div id="scrollframe" style="width: 300px; height: 300px; overflow:scroll">
+ <div id="scrolled_content" style="width: 1000px; height: 1000px; background-color: green">
+ </div>
+ <div id="configuration_1" style="display:none; position: relative; top: -1000px">
+ <div style="touch-action: pan-x; width: 100px; height: 100px; background-color: blue"></div>
+ <div style="touch-action: pan-y; width: 100px; height: 100px; position: relative; top: -100px; left: 50px; background-color: yellow"></div>
+ </div>
+ <div id="configuration_2" style="display:none; position: relative; top: -1000px">
+ <div style="touch-action: pan-x; width: 100px; height: 100px; background-color: blue">
+ <div style="touch-action: pan-y; width: 100px; height: 100px; position: relative; top: 50px; left: 50px; background-color: yellow"></div>
+ </div>
+ </div>
+ <div id="configuration_3" style="display:none; position: relative; top: -1000px">
+ <div style="touch-action: pan-x; width: 100px; height: 100px; background-color: blue">
+ <div style="touch-action: pan-y; width: 100px; height: 100px; position: relative; top: 50px; left: 50px; background-color: yellow; transform: rotate(90deg)"></div>
+ </div>
+ </div>
+ <div id="configuration_4" style="display:none; position: relative; top: -1000px">
+ <div style="touch-action: pan-x; width: 100px; height: 100px; background-color: blue">
+ <div style="touch-action: pan-y; width: 100px; height: 100px; position: relative; top: 125px; background-color: yellow"></div>
+ </div>
+ </div>
+ </div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_touch_action_ordering_block.html b/gfx/layers/apz/test/mochitest/helper_touch_action_ordering_block.html
new file mode 100644
index 0000000000..ca593c0db5
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_touch_action_ordering_block.html
@@ -0,0 +1,39 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Touch-action with sorted element</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+ async function test() {
+ var target = document.getElementById("target");
+ let touchEndPromise = promiseTouchEnd(document.body);
+
+ // drag the page up to scroll down by 50px
+ ok(await synthesizeNativeTouchDrag(target, 10, 100, 0, -50),
+ "Synthesized native vertical drag, waiting for touch-end event...");
+ await touchEndPromise;
+
+ await promiseOnlyApzControllerFlushed();
+
+ is(window.scrollX, 0, "X scroll offset didn't change");
+ is(window.scrollY, 50, "Y scroll offset changed");
+ }
+
+ waitUntilApzStable()
+ .then(test)
+ .then(subtestDone);
+ </script>
+</head>
+<body style="border: solid 1px green">
+ <div id="spacer" style="height: 2000px">
+ <div style="width:200px; height:200px; background-color:blue">
+ <span id="target" style="display:inline-block; width:200px; height:200px; background-color:red;"></span>
+ </div>
+ <div style="width:200px; height:200px; background-color:orange; touch-action:none; margin-top:-200px;"></div>
+ </div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_touch_action_ordering_zindex.html b/gfx/layers/apz/test/mochitest/helper_touch_action_ordering_zindex.html
new file mode 100644
index 0000000000..5b0d57a18d
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_touch_action_ordering_zindex.html
@@ -0,0 +1,37 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Touch-action with sorted element</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+ async function test() {
+ var target = document.getElementById("target");
+ let touchEndPromise = promiseTouchEnd(document.body);
+
+ // drag the page up to scroll down by 50px
+ ok(await synthesizeNativeTouchDrag(target, 10, 100, 0, -50),
+ "Synthesized native vertical drag, waiting for touch-end event...");
+ await touchEndPromise;
+
+ await promiseOnlyApzControllerFlushed();
+
+ is(window.scrollX, 0, "X scroll offset didn't change");
+ is(window.scrollY, 50, "Y scroll offset changed");
+ }
+
+ waitUntilApzStable()
+ .then(test)
+ .then(subtestDone);
+ </script>
+</head>
+<body style="border: solid 1px green">
+ <div id="spacer" style="height:2000px">
+ <div id="target" style="width:200px; height:200px; background-color:blue;"></div>
+ <div style="position: relative; width:200px; height:200px; background-color:red; touch-action:none; margin-top:-200px; z-index: -1;"></div>
+ </div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_touch_action_regions.html b/gfx/layers/apz/test/mochitest/helper_touch_action_regions.html
new file mode 100644
index 0000000000..6a8a09e55a
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_touch_action_regions.html
@@ -0,0 +1,345 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Test to ensure APZ doesn't always wait for touch-action</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+function failure(e) {
+ ok(false, "This event listener should not have triggered: " + e.type);
+}
+
+function listener(callback) {
+ return function(e) {
+ ok(e.type == "touchstart", "The touchstart event handler was triggered after snapshotting completed");
+ setTimeout(callback, 0);
+ };
+}
+
+// This helper function provides a way for the child process to synchronously
+// check how many touch events the chrome process main-thread has processed. This
+// function can be called with three values: 'start', 'report', and 'end'.
+// The 'start' invocation sets up the listeners, and should be invoked before
+// the touch events of interest are generated. This should only be called once.
+// This returns true on success, and false on failure.
+// The 'report' invocation can be invoked multiple times, and returns an object
+// (in JSON string format) containing the counters.
+// The 'end' invocation tears down the listeners, and should be invoked once
+// at the end to clean up. Returns true on success, false on failure.
+function chromeTouchEventCounter(operation) {
+ function chromeProcessCounter() {
+ /* eslint-env mozilla/chrome-script */
+ const PREFIX = "apz:ctec:";
+
+ const LISTENERS = {
+ "start": function() {
+ var topWin = Services.wm.getMostRecentWindow("navigator:browser");
+ if (!topWin) {
+ topWin = Services.wm.getMostRecentWindow("navigator:geckoview");
+ }
+ if (typeof topWin.eventCounts != "undefined") {
+ dump("Found pre-existing eventCounts object on the top window!\n");
+ return false;
+ }
+ topWin.eventCounts = { "touchstart": 0, "touchmove": 0, "touchend": 0 };
+ topWin.counter = function(e) {
+ topWin.eventCounts[e.type]++;
+ };
+
+ topWin.addEventListener("touchstart", topWin.counter, { passive: true });
+ topWin.addEventListener("touchmove", topWin.counter, { passive: true });
+ topWin.addEventListener("touchend", topWin.counter, { passive: true });
+
+ return true;
+ },
+
+ "report": function() {
+ var topWin = Services.wm.getMostRecentWindow("navigator:browser");
+ if (!topWin) {
+ topWin = Services.wm.getMostRecentWindow("navigator:geckoview");
+ }
+ return JSON.stringify(topWin.eventCounts);
+ },
+
+ "end": function() {
+ for (let [msg, func] of Object.entries(LISTENERS)) {
+ Services.ppmm.removeMessageListener(PREFIX + msg, func);
+ }
+
+ var topWin = Services.wm.getMostRecentWindow("navigator:browser");
+ if (!topWin) {
+ topWin = Services.wm.getMostRecentWindow("navigator:geckoview");
+ }
+ if (typeof topWin.eventCounts == "undefined") {
+ dump("The eventCounts object was not found on the top window!\n");
+ return false;
+ }
+ topWin.removeEventListener("touchstart", topWin.counter);
+ topWin.removeEventListener("touchmove", topWin.counter);
+ topWin.removeEventListener("touchend", topWin.counter);
+ delete topWin.counter;
+ delete topWin.eventCounts;
+ return true;
+ },
+ };
+
+ for (let [msg, func] of Object.entries(LISTENERS)) {
+ Services.ppmm.addMessageListener(PREFIX + msg, func);
+ }
+ }
+
+ if (typeof chromeTouchEventCounter.chromeHelper == "undefined") {
+ // This is the first time chromeTouchEventCounter is being called; do initialization
+ chromeTouchEventCounter.chromeHelper = SpecialPowers.loadChromeScript(chromeProcessCounter);
+ ApzCleanup.register(function() { chromeTouchEventCounter.chromeHelper.destroy(); });
+ }
+
+ return SpecialPowers.Services.cpmm.sendSyncMessage(`apz:ctec:${operation}`, "")[0];
+}
+
+// Simple wrapper that waits until the chrome process has seen |count| instances
+// of the |eventType| event. Returns true on success, and false if 10 seconds
+// go by without the condition being satisfied.
+function waitFor(eventType, count) {
+ var start = Date.now();
+ while (JSON.parse(chromeTouchEventCounter("report"))[eventType] != count) {
+ if (Date.now() - start > 10000) {
+ // It's taking too long, let's abort
+ return false;
+ }
+ }
+ return true;
+}
+
+function RunAfterProcessedQueuedInputEvents(aCallback) {
+ let tm = SpecialPowers.Services.tm;
+ tm.dispatchToMainThread(aCallback, SpecialPowers.Ci.nsIRunnablePriority.PRIORITY_INPUT_HIGH);
+}
+
+var scrollerPosition;
+async function getScrollerPosition() {
+ const scroller = document.getElementById("scroller");
+ scrollerPosition = await coordinatesRelativeToScreen({
+ offsetX: 0,
+ offsetY: 0,
+ target: scroller,
+ });
+}
+
+function* test(testDriver) {
+ // The main part of this test should run completely before the child process'
+ // main-thread deals with the touch event, so check to make sure that happens.
+ document.body.addEventListener("touchstart", failure, { passive: true });
+
+ // What we want here is to synthesize all of the touch events (from this code in
+ // the child process), and have the chrome process generate and process them,
+ // but not allow the events to be dispatched back into the child process until
+ // later. This allows us to ensure that the APZ in the chrome process is not
+ // waiting for the child process to send notifications upon processing the
+ // events. If it were doing so, the APZ would block and this test would fail.
+
+ // In order to actually implement this, we call the synthesize functions with
+ // a async callback in between. The synthesize functions just queue up a
+ // runnable on the child process main thread and return immediately, so with
+ // the async callbacks, the child process main thread queue looks like
+ // this after we're done setting it up:
+ // synthesizeTouchStart
+ // callback testDriver
+ // synthesizeTouchMove
+ // callback testDriver
+ // ...
+ // synthesizeTouchEnd
+ // callback testDriver
+ //
+ // If, after setting up this queue, we yield once, the first synthesization and
+ // callback will run - this will send a synthesization message to the chrome
+ // process, and return control back to us right away. When the chrome process
+ // processes with the synthesized event, it will dispatch the DOM touch event
+ // back to the child process over IPC, which will go into the end of the child
+ // process main thread queue, like so:
+ // synthesizeTouchStart (done)
+ // invoke testDriver (done)
+ // synthesizeTouchMove
+ // invoke testDriver
+ // ...
+ // synthesizeTouchEnd
+ // invoke testDriver
+ // handle DOM touchstart <-- touchstart goes at end of queue
+ //
+ // As we continue yielding one at a time, the synthesizations run, and the
+ // touch events get added to the end of the queue. As we yield, we take
+ // snapshots in the chrome process, to make sure that the APZ has started
+ // scrolling even though we know we haven't yet processed the DOM touch events
+ // in the child process yet.
+ //
+ // Note that the "async callback" we use here is SpecialPowers.tm.dispatchToMainThread
+ // with priority = input, because nothing else does exactly what we want:
+ // - setTimeout(..., 0) does not maintain ordering, because it respects the
+ // time delta provided (i.e. the callback can jump the queue to meet its
+ // deadline).
+ // - SpecialPowers.spinEventLoop and SpecialPowers.executeAfterFlushingMessageQueue
+ // are not e10s friendly, and can get arbitrarily delayed due to IPC
+ // round-trip time.
+ // - SimpleTest.executeSoon has a codepath that delegates to setTimeout, so
+ // is less reliable if it ever decides to switch to that codepath.
+ // - SpecialPowers.executeSoon dispatches a task to main thread. However,
+ // normal runnables may be preempted by input events and be executed in an
+ // unexpected order.
+
+ // Also note that this test is intentionally kept as a yield-style test using
+ // the runContinuation helper, even though all other similar tests have since
+ // been migrated to using async/await and Promise-based architectures. This is
+ // because yield and async/await have different semantics with respect to
+ // timing, and this test requires very specific timing behaviour (as described
+ // above).
+
+ // The other problem we need to deal with is the asynchronicity in the chrome
+ // process. That is, we might request a snapshot before the chrome process has
+ // actually synthesized the event and processed it. To guard against this, we
+ // register a thing in the chrome process that counts the touch events that
+ // have been dispatched, and poll that thing synchronously in order to make
+ // sure we only snapshot after the event in question has been processed.
+ // That's what the chromeTouchEventCounter business is all about. The sync
+ // polling looks bad but in practice only ends up needing to poll once or
+ // twice before the condition is satisfied, and as an extra precaution we add
+ // a time guard so it fails after 10s of polling.
+
+ // So, here we go...
+
+ // Set up the chrome process touch listener
+ ok(chromeTouchEventCounter("start"), "Chrome touch counter registered");
+
+ // Set up the child process events and callbacks
+ var scroller = document.getElementById("scroller");
+ var utils = utilsForTarget(window);
+ utils.sendNativeTouchPoint(0, SpecialPowers.DOMWindowUtils.TOUCH_CONTACT,
+ scrollerPosition.x + 10, scrollerPosition.y + 110,
+ 1, 90, null);
+ RunAfterProcessedQueuedInputEvents(testDriver);
+ for (let i = 1; i < 10; i++) {
+ utils.sendNativeTouchPoint(0, SpecialPowers.DOMWindowUtils.TOUCH_CONTACT,
+ scrollerPosition.x + 10,
+ scrollerPosition.y + 110 - (i * 10),
+ 1, 90, null);
+ RunAfterProcessedQueuedInputEvents(testDriver);
+ }
+ utils.sendNativeTouchPoint(0, SpecialPowers.DOMWindowUtils.TOUCH_REMOVE,
+ scrollerPosition.x + 10,
+ scrollerPosition.y + 10,
+ 1, 90, null);
+ RunAfterProcessedQueuedInputEvents(testDriver);
+ ok(true, "Finished setting up event queue");
+
+ // Get our baseline snapshot
+ var rect = rectRelativeToScreen(scroller);
+ var lastSnapshot = getSnapshot(rect);
+ ok(true, "Got baseline snapshot");
+ var numDifferentSnapshotPairs = 0;
+
+ yield; // this will tell the chrome process to synthesize the touchstart event
+ // and then we wait to make sure it got processed:
+ ok(waitFor("touchstart", 1), "Touchstart processed in chrome process");
+
+ // Loop through the touchmove events
+ for (let i = 1; i < 10; i++) {
+ yield;
+ ok(waitFor("touchmove", i), "Touchmove processed in chrome process");
+
+ // Take a snapshot after each touch move event. This forces
+ // a composite each time, even we don't get a vsync in this
+ // interval.
+ var snapshot = getSnapshot(rect);
+ if (lastSnapshot != snapshot) {
+ numDifferentSnapshotPairs += 1;
+ }
+ lastSnapshot = snapshot;
+ }
+
+ // Check that the snapshot has changed since the baseline, indicating
+ // that the touch events caused async scrolling. Note that, since we
+ // orce a composite after each touch event, even if there is a frame
+ // of delay between APZ processing a touch event and the compositor
+ // applying the async scroll (bug 1375949), by the end of the gesture
+ // the snapshot should have changed.
+ ok(numDifferentSnapshotPairs > 0,
+ "The number of different snapshot pairs was " + numDifferentSnapshotPairs);
+
+ // Wait for the touchend as well, to clear all pending testDriver resumes
+ yield;
+ ok(waitFor("touchend", 1), "Touchend processed in chrome process");
+
+ // Clean up the chrome process hooks
+ chromeTouchEventCounter("end");
+
+ // Now we are going to release our grip on the child process main thread,
+ // so that all the DOM events that were queued up can be processed. We
+ // register a touchstart listener to make sure this happens.
+ document.body.removeEventListener("touchstart", failure);
+ var listenerFunc = listener(testDriver);
+ document.body.addEventListener("touchstart", listenerFunc, { passive: true });
+ dump("done registering listener, going to yield\n");
+ yield;
+ document.body.removeEventListener("touchstart", listenerFunc);
+}
+
+// Despite what this function name says, this does not *directly* run the
+// provided continuation testFunction. Instead, it returns a function that
+// can be used to run the continuation. The extra level of indirection allows
+// it to be more easily added to a promise chain, like so:
+// waitUntilApzStable().then(runContinuation(myTest));
+function runContinuation(testFunction) {
+ return function() {
+ return new Promise(function(resolve, reject) {
+ var testContinuation = null;
+
+ function driveTest() {
+ if (!testContinuation) {
+ testContinuation = testFunction(driveTest);
+ }
+ var ret = testContinuation.next();
+ if (ret.done) {
+ resolve();
+ }
+ }
+
+ try {
+ driveTest();
+ } catch (ex) {
+ ok(
+ false,
+ "APZ test continuation failed with exception: " + ex
+ );
+ }
+ });
+ };
+}
+
+if (SpecialPowers.isMainProcess()) {
+ // This is probably android, where everything is single-process. The
+ // test structure depends on e10s, so the test won't run properly on
+ // this platform. Skip it
+ ok(true, "Skipping test because it is designed to run from the content process");
+ subtestDone();
+} else {
+ waitUntilApzStable()
+ .then(async () => { await getScrollerPosition(); })
+ .then(runContinuation(test))
+ .then(subtestDone, subtestFailed);
+}
+
+ </script>
+</head>
+<body>
+ <div id="scroller" style="width: 400px; height: 400px; overflow: scroll; touch-action: pan-y">
+ <div style="width: 200px; height: 200px; background-color: lightgreen;">
+ This is a colored div that will move on the screen as the scroller scrolls.
+ </div>
+ <div style="width: 1000px; height: 1000px; background-color: lightblue">
+ This is a large div to make the scroller scrollable.
+ </div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_touch_action_zero_opacity_bug1500864.html b/gfx/layers/apz/test/mochitest/helper_touch_action_zero_opacity_bug1500864.html
new file mode 100644
index 0000000000..d31483f4f6
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_touch_action_zero_opacity_bug1500864.html
@@ -0,0 +1,45 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Touch-action on a zero-opacity element</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+async function test() {
+ var target = document.getElementById("target");
+
+ let touchEndPromise = promiseTouchEnd(document.body);
+
+ // drag the page up to scroll down by 50px
+ ok(await synthesizeNativeTouchDrag(target, 10, 100, 0, -50),
+ "Synthesized native vertical drag, waiting for touch-end event...");
+ await touchEndPromise;
+
+ await promiseOnlyApzControllerFlushed();
+
+ is(window.scrollX, 0, "X scroll offset didn't change");
+ is(window.scrollY, 0, "Y scroll offset didn't change");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</head>
+<body style="border: solid 1px green">
+ <div id="spacer" style="height: 2000px">
+ Inside the black border is a zero-opacity touch-action none.
+ <div id="border" style="border: solid 1px black">
+ <div style="opacity: 0; height: 300px;">
+ <div style="transform:translate(0px)">
+ <div id="target" style="height: 300px; touch-action: none">this text shouldn't be visible</div>
+ </div>
+ </div>
+ </div>
+ </div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_touch_drag_root_scrollbar.html b/gfx/layers/apz/test/mochitest/helper_touch_drag_root_scrollbar.html
new file mode 100644
index 0000000000..018ef78087
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_touch_drag_root_scrollbar.html
@@ -0,0 +1,51 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Touch Drag on the viewport's scrollbar</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+ .content {
+ width: 1000px;
+ height: 5000px;
+ }
+ </style>
+ <script type="text/javascript">
+
+async function test() {
+ let transformEndPromise = promiseTransformEnd();
+ let scrollbarPresent = await promiseVerticalScrollbarTouchDrag(window, 20);
+ if (!scrollbarPresent) {
+ ok(true, "No scrollbar, can't do this test");
+ return;
+ }
+
+ await transformEndPromise;
+
+ // Flush everything just to be safe
+ await promiseOnlyApzControllerFlushed();
+
+ // After dragging the scrollbar 20px on a 1000px-high viewport, we should
+ // have scrolled approx 2% of the 5000px high content. There might have been
+ // scroll arrows and such so let's just have a minimum bound of 50px to be safe.
+ ok(window.scrollY > 50, "Scrollbar drag resulted in a vertical scroll position of " + window.scrollY);
+
+ // Check that we did not get spurious horizontal scrolling, as we might if the
+ // drag gesture is mishandled by content as a select-drag rather than a scrollbar
+ // drag.
+ is(window.scrollX, 0, "Scrollbar drag resulted in a horizontal scroll position of " + window.scrollX);
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</head>
+<body>
+ <div class="content">Some content to ensure the root scrollframe is scrollable</div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_touchpad_pinch_and_pan.html b/gfx/layers/apz/test/mochitest/helper_touchpad_pinch_and_pan.html
new file mode 100644
index 0000000000..24eb920a74
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_touchpad_pinch_and_pan.html
@@ -0,0 +1,49 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width">
+ <title>Sanity check for Touchpad pinch zooming and panning</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="application/javascript">
+
+async function test() {
+ var initial_resolution = await getResolution();
+ var initial_page_left = window.visualViewport.pageLeft;
+ var initial_page_top = window.visualViewport.pageTop;
+ ok(initial_resolution > 0,
+ "The initial_resolution is " + initial_resolution + ", which is some sane value");
+ is(initial_page_left, 0,
+ "The initial_page_left is " + initial_page_left + ", which is correct");
+ is(initial_page_top, 0,
+ "The initial_page_top is " + initial_page_top + ", which is correct");
+ await pinchZoomInAndPanWithTouchpad();
+ // Flush state and get the resolution we're at now
+ await promiseApzFlushedRepaints();
+ let final_resolution = await getResolution();
+
+ let final_page_left = window.visualViewport.pageLeft;
+ let final_page_top = window.visualViewport.pageTop;
+ ok(final_resolution > initial_resolution,
+ "The final resolution (" + final_resolution + ") is greater after zooming in");
+ ok(final_page_left > 300,
+ "The final pageLeft (" + final_page_left + ") is greater than 300 after panning");
+ ok(final_page_top > 200,
+ "The final pageTop (" + final_page_top + ") is greater than 200 after panning");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</head>
+<body>
+ Here is some text to stare at as the test runs. It serves no functional
+ purpose, but gives you an idea of the zoom level. It's harder to tell what
+ the zoom level is when the page is just solid white.
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_transform_end_on_keyboard_scroll.html b/gfx/layers/apz/test/mochitest/helper_transform_end_on_keyboard_scroll.html
new file mode 100644
index 0000000000..20bee3cefd
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_transform_end_on_keyboard_scroll.html
@@ -0,0 +1,58 @@
+<!DOCTYPE html>
+<html>
+<meta charset="utf-8">
+<script src="apz_test_utils.js"></script>
+<script src="apz_test_native_event_utils.js"></script>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/NativeKeyCodes.js"></script>
+<script src="/tests/SimpleTest/paint_listener.js"></script>
+<style>
+html, body { margin: 0; }
+
+body {
+ height: 10000px;
+}
+</style>
+
+<script>
+async function test() {
+ // Send a native key event which doesn't cause any scroll, so that now
+ // subsequent native key events will be able to be handled by APZ. See bug
+ // 1774519 about what happens without this event.
+ const UpArrowKeyCode = nativeArrowUpKey();
+ await new Promise(resolve => {
+ synthesizeNativeKey(KEYBOARD_LAYOUT_EN_US,
+ UpArrowKeyCode, {} /* no modifier */,
+ "", "", resolve); });
+
+ // On test verify runs there's still a race condition where the next key event
+ // isn't handled by APZ since the focus sequence number hasn't yet been
+ // reflected to APZ, so we explicitly flush APZ state here.
+ await promiseApzFlushedRepaints();
+
+ const transformEndPromise = promiseTransformEnd();
+ const DownArrowKeyCode = nativeArrowDownKey();
+ await new Promise(resolve => {
+ synthesizeNativeKey(KEYBOARD_LAYOUT_EN_US,
+ DownArrowKeyCode, {} /* no modifier */,
+ "", "", resolve); });
+ await transformEndPromise;
+ ok(true, "Got an APZ:TransformEnd ");
+}
+
+function isOnChaosMode() {
+ return SpecialPowers.Services.env.get("MOZ_CHAOSMODE");
+}
+
+if ((getPlatform() == "mac" || getPlatform() == "windows") &&
+ !isOnChaosMode()) {
+ waitUntilApzStable()
+ .then(test)
+ .then(subtestDone, subtestFailed);
+} else {
+ ok(true, "Skipping test because native key events are not supported on " +
+ getPlatform());
+ subtestDone();
+}
+</script>
diff --git a/gfx/layers/apz/test/mochitest/helper_transform_end_on_wheel_scroll.html b/gfx/layers/apz/test/mochitest/helper_transform_end_on_wheel_scroll.html
new file mode 100644
index 0000000000..af4f72cf44
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_transform_end_on_wheel_scroll.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<html>
+<meta charset="utf-8">
+<script src="apz_test_utils.js"></script>
+<script src="apz_test_native_event_utils.js"></script>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/paint_listener.js"></script>
+<style>
+html, body { margin: 0; }
+
+body {
+ height: 10000px;
+}
+</style>
+
+<script>
+async function test() {
+ let transformEndPromise = promiseTransformEnd();
+ await promiseMoveMouseAndScrollWheelOver(document.documentElement, 100, 100);
+
+ await transformEndPromise;
+ ok(true, "Got an APZ:TransformEnd ");
+}
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+</script>
diff --git a/gfx/layers/apz/test/mochitest/helper_visual_scrollbars_pagescroll.html b/gfx/layers/apz/test/mochitest/helper_visual_scrollbars_pagescroll.html
new file mode 100644
index 0000000000..ae3025930f
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_visual_scrollbars_pagescroll.html
@@ -0,0 +1,119 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width">
+ <title>Clicking on the scrollbar track in quick succession should scroll the right amount</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="application/javascript">
+
+// A helper to synthesize a native mouse click on a scrollbar track,
+// and wait long enough such that subsequent ticking of the refresh
+// driver will progress any resulting scroll animation.
+// In particular, just `await promiseNativeMouseEventWithApz(...)`
+// is not enough, it waits for the synthesization messages to arrive
+// in the parent process, but the native events may still be in the
+// OS event queue. Instead, we need to wait for a synthesized event
+// to arrive at content. While we're synthesizing a "click", if the
+// target is a scrollbar the window only gets the "mousedown" and
+// "mouseup", not a "click". Waiting for the "mousedown" is not enough
+// (the "mouseup" can still be stuck in the event queue), so we wait
+// for "mouseup".
+async function promiseNativeMouseClickOnScrollbarTrack(anchor, xOffset, yOffset) {
+ await promiseNativeMouseEventWithAPZAndWaitForEvent({
+ type: "click",
+ target: anchor,
+ offsetX: xOffset,
+ offsetY: yOffset,
+ eventTypeToWait: "mouseup"
+ });
+}
+
+async function test() {
+ var scroller = document.documentElement;
+ var verticalScrollbarWidth = window.innerWidth - scroller.clientWidth;
+
+ if (verticalScrollbarWidth == 0) {
+ ok(true, "Scrollbar width is zero on this platform, test is useless here");
+ return;
+ }
+
+ // The anchor is the fixed-pos div that we use to calculate coordinates to
+ // click on the scrollbar. That way we don't have to recompute coordinates
+ // as the page scrolls. The anchor is at the bottom-right corner of the
+ // content area.
+ var anchor = document.getElementById('anchor');
+
+ var xoffset = (verticalScrollbarWidth / 2);
+ // Get a y-coord near the bottom of the vertical scrollbar track. Assume the
+ // vertical thumb is near the top of the scrollback track (since scroll
+ // position starts off at zero) and won't get in the way. Also assume the
+ // down arrow button, if there is one, is square.
+ var yoffset = 0 - verticalScrollbarWidth - 5;
+
+ // Take control of the refresh driver
+ let utils = SpecialPowers.getDOMWindowUtils(window);
+ utils.advanceTimeAndRefresh(0);
+
+ // Click at the bottom of the scrollbar track to trigger a page-down kind of
+ // scroll. This should use "desktop zooming" scrollbar code which should
+ // trigger an APZ scroll animation.
+ await promiseNativeMouseClickOnScrollbarTrack(anchor, xoffset, yoffset);
+
+ // Run 1000 frames, that should be enough to let the scroll animation start
+ // and run to completion. We check that it scrolled at least half the visible
+ // height, since we expect about a full screen height minus a few lines.
+ for (let i = 0; i < 1000; i++) {
+ utils.advanceTimeAndRefresh(16);
+ }
+ await promiseOnlyApzControllerFlushed();
+
+ let pageScrollAmount = scroller.scrollTop;
+ ok(pageScrollAmount > scroller.clientHeight / 2,
+ `Scroll offset is ${pageScrollAmount}, should be near clientHeight ${scroller.clientHeight}`);
+
+ // Now we do two clicks in quick succession, but with a few frames in between
+ // to verify the scroll animation from the first click is active before the
+ // second click happens.
+ await promiseNativeMouseClickOnScrollbarTrack(anchor, xoffset, yoffset);
+ for (let i = 0; i < 5; i++) {
+ utils.advanceTimeAndRefresh(16);
+ }
+ await promiseOnlyApzControllerFlushed();
+ let curPos = scroller.scrollTop;
+ ok(curPos > pageScrollAmount, `Scroll offset has increased to ${curPos}`);
+ ok(curPos < pageScrollAmount * 2, "Second page-scroll is not yet complete");
+ await promiseNativeMouseClickOnScrollbarTrack(anchor, xoffset, yoffset);
+
+ // Run to completion and check that we are around 3x pageScrollAmount, with
+ // some allowance for fractional rounding.
+ for (let i = 0; i < 1000; i++) {
+ utils.advanceTimeAndRefresh(16);
+ }
+ await promiseOnlyApzControllerFlushed();
+ curPos = scroller.scrollTop;
+ ok(Math.abs(curPos - (pageScrollAmount * 3)) < 3,
+ `Final scroll offset ${curPos} is close to 3x${pageScrollAmount}`);
+
+ utils.restoreNormalRefresh();
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</head>
+<body>
+ <div style="position:fixed; bottom: 0; right: 0; width: 1px; height: 1px" id="anchor"></div>
+ <div style="height: 300vh; margin-bottom: 10000px; background-image: linear-gradient(red,blue)"></div>
+ The above div is sized to 3x screen height so the linear gradient is more steep in terms of
+ color/pixel. We only scroll a few pages worth so we don't need the gradient all the way down.
+ And then we use a bottom-margin to make the page really big so the scrollthumb is
+ relatively small, giving us lots of space to click on the scrolltrack.
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_visual_smooth_scroll.html b/gfx/layers/apz/test/mochitest/helper_visual_smooth_scroll.html
new file mode 100644
index 0000000000..9d278ee23c
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_visual_smooth_scroll.html
@@ -0,0 +1,53 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, minimum-scale=1.0">
+ <title>Tests that the (internal) visual smooth scrolling API is not restricted to the layout scroll range</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+ div {
+ position: absolute;
+ }
+ </style>
+</head>
+<body>
+ <div style="width: 100%; height: 200%; background-color: green"></div>
+ <div style="width: 100%; height: 100%; background-color: blue"></div>
+ <script type="application/javascript">
+ const utils = SpecialPowers.getDOMWindowUtils(window);
+
+ async function test() {
+ // Pick a destination to scroll to that's outside the layout scroll range
+ // but within the visual scroll range.
+ const destY = window.scrollMaxY + 100;
+
+ // Register a TransformEnd observer so we can tell when the smooth scroll
+ // animation stops.
+ let transformEndPromise = promiseTransformEnd();
+
+ // Use scrollToVisual() to smooth-scroll to the destination.
+ utils.scrollToVisual(0, destY, utils.UPDATE_TYPE_MAIN_THREAD,
+ utils.SCROLL_MODE_SMOOTH);
+
+ // Wait for the TransformEnd.
+ await transformEndPromise;
+
+ // Give scroll offsets a chance to sync.
+ await promiseApzFlushedRepaints();
+
+ // Check that the visual viewport scrolled to the desired destination.
+ is(visualViewport.pageTop, destY,
+ "The visual viewport should have scrolled past the layout scroll range");
+ }
+
+ SpecialPowers.getDOMWindowUtils(window).setResolutionAndScaleTo(2.0);
+
+ waitUntilApzStable()
+ .then(test)
+ .then(subtestDone, subtestFailed);
+ </script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_visualscroll_clamp_restore.html b/gfx/layers/apz/test/mochitest/helper_visualscroll_clamp_restore.html
new file mode 100644
index 0000000000..69c0590a56
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_visualscroll_clamp_restore.html
@@ -0,0 +1,63 @@
+<!DOCTYPE HTML>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, minimum-scale=1.0">
+<title>Tests scroll position is properly synchronized when visual position is temporarily clamped on the main thread</title>
+<script src="apz_test_utils.js"></script>
+<script src="/tests/SimpleTest/paint_listener.js"></script>
+<style>
+.hoverthingy, button {
+ width: 100%;
+ height: 200px;
+ text-align: center;
+ border: solid 1px black;
+ background-color: white;
+}
+
+.hoverthingy:hover {
+ background-color: lightgray;
+}
+</style>
+<div id="filler" style="height: 5000px">This test runs automatically in automation. To run manually, follow the steps: 1. scroll all the way down</div>
+<div class="hoverthingy">3. move the mouse. this div should have a hover effect exactly when the mouse is on top of it</div>
+<button onclick="clampRestore()">2. click this button</div>
+<script>
+/* eslint-disable no-unused-vars */
+function clampRestore() {
+ // Shorten doc to clamp scroll position
+ let filler = document.getElementById('filler');
+ filler.style.height = '4800px';
+ // Force scroll position update
+ let scrollPos = document.scrollingElement.scrollTop;
+ // Restore height
+ filler.style.height = '5000px';
+}
+
+function getAsyncScrollOffset() {
+ let apzcTree = getLastApzcTree();
+ let rcd = findRcdNode(apzcTree);
+ if (rcd == null) {
+ return {x: -1, y: -1};
+ }
+ return parsePoint(rcd.asyncScrollOffset);
+}
+
+async function test() {
+ document.scrollingElement.scrollTop = document.scrollingElement.scrollTopMax;
+ await promiseApzFlushedRepaints();
+ clampRestore();
+ await promiseApzFlushedRepaints();
+ let apzScrollOffset = getAsyncScrollOffset();
+ dump(`Got apzScrollOffset ${JSON.stringify(apzScrollOffset)}\n`);
+ // The bug this test is exercising resulted in a situation where the
+ // main-thread scroll offset and the APZ scroll offset remained out of sync
+ // while in the steady state. This resulted mouse hover effects and clicks
+ // being offset from where the user visually saw the content/mouse. We
+ // check to make sure the scroll offset is in sync to ensure the bug is fixed.
+ is(apzScrollOffset.y, document.scrollingElement.scrollTop,
+ "RCD y-scroll should match between APZ and main thread");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+</script>
diff --git a/gfx/layers/apz/test/mochitest/helper_visualscroll_nonrcd_rsf.html b/gfx/layers/apz/test/mochitest/helper_visualscroll_nonrcd_rsf.html
new file mode 100644
index 0000000000..9955e49e2a
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_visualscroll_nonrcd_rsf.html
@@ -0,0 +1,88 @@
+<!DOCTYPE HTML>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, minimum-scale=1.0">
+<title>Tests that pending visual scroll positions on RSFs of non-RCDs get cleared properly</title>
+<script src="apz_test_utils.js"></script>
+<script src="apz_test_native_event_utils.js"></script>
+<script src="/tests/SimpleTest/paint_listener.js"></script>
+<body>
+<iframe style="width: 300px; height: 300px" id="scroller"></iframe>
+<script>
+function populateScroller() {
+ let text = '<div id="line0">line 0</div><br>';
+ for (let i = 1; i < 100; i++) {
+ text += 'line ' + i + '<br>';
+ }
+ /* eslint-disable no-unsanitized/property */
+ document.querySelector('#scroller').contentDocument.body.innerHTML = text;
+}
+
+function reconstructScroller() {
+ let scroller = document.querySelector('#scroller');
+ scroller.style.display = 'none';
+ /* eslint-disable no-unused-vars */
+ let dummyToForceFlush = scroller.scrollTop;
+ scroller.style.display = '';
+ dummyToForceFlush = scroller.scrollTop;
+}
+
+async function test() {
+ let scroller = document.querySelector('#scroller');
+ let subwin = scroller.contentWindow;
+
+ populateScroller();
+ subwin.scrollTo(0, 100);
+ is(subwin.scrollY, 100, 'Scroller scrolled down to y=100');
+
+ // let the visual scroll position round-trip through APZ
+ await promiseApzFlushedRepaints();
+
+ // frame reconstruction does a ScrollToVisual. The bug is that the pending
+ // visual scroll offset update never gets cleared even though the paint
+ // transaction should clear it.
+ reconstructScroller();
+ await promiseApzFlushedRepaints();
+
+ // Scroll back up to the top using APZ-side scrolling, and wait for the APZ
+ // wheel animation to complete and the final scroll position to get synced
+ // back to the main thread. The large -250 scroll delta required here is due
+ // to bug 1662487.
+ await promiseMoveMouseAndScrollWheelOver(subwin, 10, 10, true, -250);
+ let utils = SpecialPowers.getDOMWindowUtils(window);
+ for (let i = 0; i < 60; i++) {
+ utils.advanceTimeAndRefresh(16);
+ }
+ utils.restoreNormalRefresh();
+ await promiseApzFlushedRepaints();
+ is(subwin.scrollY, 0, 'Scroller scrolled up to y=0');
+
+ // Do a mouse-drag-selection. I couldn't find any simpler way to reproduce
+ // the problem.
+ const kMouseMovePixels = 10;
+ let promiseMouseMovesDone = new Promise((resolve) => {
+ let mouseDownX = 0;
+ subwin.document.documentElement.addEventListener('mousedown', (e) => {
+ dump(`Got mousedown at ${e.screenX}\n`);
+ mouseDownX = e.screenX;
+ });
+ subwin.document.documentElement.addEventListener('mousemove', (e) => {
+ // Mousemove events can get squashed together so we check the coord
+ // instead.
+ dump(`Got mousemove at ${e.screenX}\n`);
+ if (e.screenX - mouseDownX >= kMouseMovePixels) {
+ resolve();
+ }
+ });
+ });
+ let line0 = subwin.document.querySelector('#line0');
+ let dragFinisher = await promiseNativeMouseDrag(line0, 1, 5, kMouseMovePixels, 0, kMouseMovePixels);
+ await promiseMouseMovesDone;
+ await dragFinisher();
+
+ is(subwin.scrollY, 0, 'Scroller should remain at y=0');
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+</script>
diff --git a/gfx/layers/apz/test/mochitest/helper_wheelevents_handoff_on_iframe.html b/gfx/layers/apz/test/mochitest/helper_wheelevents_handoff_on_iframe.html
new file mode 100644
index 0000000000..79101b2ca5
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_wheelevents_handoff_on_iframe.html
@@ -0,0 +1,52 @@
+<head>
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Test that wheel events on an unscrollable OOP iframe are handoff-ed</title>
+ <script src="apz_test_native_event_utils.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+ iframe {
+ height: 201px;
+ border: none;
+ }
+ </style>
+</head>
+<body>
+ <div id="iframe-container" style="overflow-y: scroll; height: 200px;">
+ <iframe src="https://example.com/tests/gfx/layers/apz/test/mochitest/helper_wheelevents_handoff_on_iframe_child.html"></iframe>
+ </div>
+ <div style="height: 200vh;"></div>
+ <script type="application/javascript">
+async function test() {
+ const scrollContainer = document.querySelector("#iframe-container");
+ let scrollEventPromise = waitForScrollEvent(scrollContainer);
+
+ // Send a wheel event on the iframe.
+ const iframe = document.querySelector("iframe");
+ await synthesizeNativeWheel(iframe, 50, 50, 0, -50);
+ await scrollEventPromise;
+
+ // The wheel event should be handoff-ed to the parent scroll container.
+ is(scrollContainer.scrollTop, scrollContainer.scrollTopMax,
+ "The scroll position in the parent scroll container should be at the bottom");
+
+ // Make sure the wheel event wasn't handoff-ed to the root scroller.
+ is(window.scrollY, 0, "The root scroll position should be 0");
+
+ await promiseFrame();
+ scrollEventPromise = waitForScrollEvent(window);
+ // Send a wheel event on the iframe again.
+ await synthesizeNativeWheel(iframe, 50, 50, 0, -50);
+ await scrollEventPromise;
+
+ // Now it should be handoff-ed to the root scroller.
+ ok(window.scrollY > 0,
+ "The wheel event should have been handoff-ed to the root scroller");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</body>
diff --git a/gfx/layers/apz/test/mochitest/helper_wheelevents_handoff_on_iframe_child.html b/gfx/layers/apz/test/mochitest/helper_wheelevents_handoff_on_iframe_child.html
new file mode 100644
index 0000000000..aca9dfdbd4
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_wheelevents_handoff_on_iframe_child.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<style>
+html {
+ overflow: hidden;
+}
+body {
+ margin: 0px;
+ padding: 0px;
+}
+</style>
+<div>overflow: hidden on html</div>
diff --git a/gfx/layers/apz/test/mochitest/helper_wheelevents_handoff_on_non_scrollable_iframe.html b/gfx/layers/apz/test/mochitest/helper_wheelevents_handoff_on_non_scrollable_iframe.html
new file mode 100644
index 0000000000..22963459a0
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_wheelevents_handoff_on_non_scrollable_iframe.html
@@ -0,0 +1,113 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>scroll handoff on non scrollable iframe document with overscroll-behavior: none</title>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script src="apz_test_native_event_utils.js"></script>
+</head>
+<style>
+iframe {
+ width: 500px;
+ height: 500px;
+}
+</style>
+<body>
+<iframe></iframe>
+<div style="height:1000vh"></div>
+<script>
+
+// This test runs twice, with a same origin iframe first then with a cross
+// origin iframe second.
+async function test(targetOrigin) {
+ const iframe = document.querySelector("iframe");
+ const targetURL =
+ SimpleTest.getTestFileURL("helper_empty.html")
+ .replace(window.location.origin, targetOrigin);
+ iframe.src = targetURL;
+
+ await new Promise(resolve => {
+ iframe.onload = resolve;
+ });
+
+ await SpecialPowers.spawn(iframe, [], async () => {
+ content.document.documentElement.style =
+ "overscroll-behavior-y: none; overflow-y: scroll;";
+
+ // Flush the style change.
+ content.document.documentElement.getBoundingClientRect();
+ // Make sure the style change reaches to APZ.
+ await content.window.wrappedJSObject.promiseApzFlushedRepaints();
+ });
+
+ let scrollEventPromise = new Promise(resolve => {
+ window.addEventListener("scroll", resolve, { once: true });
+ });
+
+ await synthesizeNativeWheel(iframe, 100, 100, 0, -10);
+ await scrollEventPromise;
+ await waitToClearOutAnyPotentialScrolls(window);
+
+ ok(window.scrollY > 0,
+ "Mouse wheel scrolling on an OOP iframe where the iframe document is " +
+ "not scrollable but has overscroll-behavior: none property should be " +
+ "handed off to the parent");
+
+ // Make sure the wheel scrolling has finished.
+ await waitToClearOutAnyPotentialScrolls(window);
+
+ // Make the iframe document scrollable and try to scroll up on the iframe
+ // document.
+ await SpecialPowers.spawn(iframe, [], async () => {
+ content.document.body.style = "height: 500vh;";
+
+ // Flush the style change.
+ content.document.documentElement.getBoundingClientRect();
+ // Make sure the style change reaches to APZ.
+ await content.window.wrappedJSObject.promiseApzFlushedRepaints();
+ });
+
+ const mousemoveEventPromise = SpecialPowers.spawn(iframe, [], async () => {
+ await new Promise(resolve => {
+ content.window.addEventListener("mousemove", resolve, { once: true });
+ });
+ });
+
+ // Make sure the above event listener is registered.
+ await SpecialPowers.spawn(iframe, [], async () => {
+ await content.window.wrappedJSObject.promiseApzFlushedRepaints();
+ });
+
+ const scrollPos = window.scrollY;
+
+ // Send a mousemove event on the iframe to finish the last wheel event block.
+ await synthesizeNativeMouseEventWithAPZ({
+ type: "mousemove",
+ target: iframe,
+ offsetX: 100,
+ offsetY: 100 + scrollPos,
+ });
+ await mousemoveEventPromise;
+
+ // Try to scroll up by a new wheel event on the iframe.
+ await synthesizeNativeWheel(iframe, 100, 100 + scrollPos, 0, 10);
+ await waitToClearOutAnyPotentialScrolls(window);
+
+ // The wheel event should not be handed off to the root scroller since the
+ // iframe document has `overscroll-behavior-y: none`.
+ is(window.scrollY, scrollPos,
+ "The root scroller's position should never be changed");
+
+ // Restore the root scroll position for the next test case.
+ window.scrollTo(0, 0);
+}
+
+waitUntilApzStable()
+.then(async () => test(window.location.origin))
+.then(async () => test("http://example.com/"))
+.then(subtestDone, subtestFailed);
+
+</script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_wide_crossorigin_iframe.html b/gfx/layers/apz/test/mochitest/helper_wide_crossorigin_iframe.html
new file mode 100644
index 0000000000..bdcef59229
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_wide_crossorigin_iframe.html
@@ -0,0 +1,33 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<meta charset="utf-8">
+<title>Test cross origin fission iframes get displayport that covers whole width</title>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<script src="/tests/SimpleTest/paint_listener.js"></script>
+<script src="apz_test_utils.js"></script>
+<script src="apz_test_native_event_utils.js"></script>
+</head>
+<body>
+ <iframe id="iframe" style="width:700px; height:150px; border:2px solid blue;"></iframe>
+<script>
+
+ window.addEventListener("message", event => {
+ if (event.data == "wereDone") {
+ subtestDone();
+ } else if (event.data.x != undefined && event.data.y != undefined && event.data.width != undefined && event.data.height != undefined) {
+ info("event.data " + event.data.x + " " + event.data.y + " " + event.data.width + " " + event.data.height);
+ // 683 = 700 width of iframe minus maximum width of scrollbars seen on try
+ ok(event.data.width >= 683, "dp is wide enough");
+ }
+ });
+
+ function test() {
+ document.getElementById("iframe").setAttribute("src", "http://example.org/tests/gfx/layers/apz/test/mochitest/helper_wide_crossorigin_iframe_child.html");
+ }
+
+ waitUntilApzStable().then(test);
+
+</script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_wide_crossorigin_iframe_child.html b/gfx/layers/apz/test/mochitest/helper_wide_crossorigin_iframe_child.html
new file mode 100644
index 0000000000..edf3ad4728
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_wide_crossorigin_iframe_child.html
@@ -0,0 +1,71 @@
+<!DOCTYPE html>
+<html id="helper_wide_crossorigin_iframe_child_docelement">
+<meta charset=utf-8>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <script src="apz_test_native_event_utils.js"></script>
+<style>
+html {
+ border: 5px solid lime;
+ background: yellow;
+ box-sizing: border-box;
+ overflow-y: scroll;
+}
+</style>
+<script>
+ // negative means keep sending forever
+ // we flip this to 10 when we hit onload, so that we send several
+ // before load and some after.
+ let numMoreTimesToSend = -1;
+ function sendDpToParent() {
+ if (numMoreTimesToSend > 0) {
+ numMoreTimesToSend--;
+ }
+ if (numMoreTimesToSend == 0) {
+ clearInterval(intervalId);
+ parent.postMessage("wereDone", "*");
+ return;
+ }
+ let dp = getLastContentDisplayportFor("helper_wide_crossorigin_iframe_child_docelement", /* expectPainted = */ false);
+ if (dp != null) {
+ info("result " + dp.x + " " + dp.y + " " + dp.width + " " + dp.height);
+
+ parent.postMessage(dp, "*");
+ } else {
+ info("no dp yet");
+ }
+ }
+
+ sendDpToParent();
+ setTimeout(sendDpToParent,0);
+
+ let intervalId = setInterval(sendDpToParent, 100);
+
+ addEventListener("MozAfterPaint", sendAndSetTimeout);
+ function sendAndSetTimeout() {
+ sendDpToParent();
+ setTimeout(sendDpToParent,0);
+ }
+
+ window.requestAnimationFrame(checkAndSendRaf);
+ function checkAndSendRaf() {
+ if (numMoreTimesToSend != 0) {
+ window.requestAnimationFrame(checkAndSendRaf);
+ }
+ sendDpToParent();
+ setTimeout(sendDpToParent,0);
+ }
+
+ window.onload = onloaded;
+ window.onDOMContentLoaded = sendDpToParent;
+ document.onreadystatechange = sendDpToParent;
+ document.onafterscriptexecute = sendDpToParent;
+ document.onbeforescriptexecute = sendDpToParent;
+ document.onvisibilitychange = sendDpToParent;
+ function onloaded() {
+ numMoreTimesToSend = 10;
+ sendDpToParent();
+ }
+
+</script>
+<div style="background: blue; height: 400vh;"></div>
diff --git a/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_fixed_bug1673511.html b/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_fixed_bug1673511.html
new file mode 100644
index 0000000000..c63794fdb1
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_fixed_bug1673511.html
@@ -0,0 +1,42 @@
+<!DOCTYPE>
+<html>
+ <head>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Checking zoomToFocusedInput scrolls on position: fixed</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ </head>
+<body>
+<div>
+ <div style="z-index: 100; position: fixed; width: 300px; height: 300px; overflow-y: scroll;" id="container">
+ <div style="height: 1000px;">ABC</div>
+ <input type="text">
+ </div>
+ <!-- Leave additional room below the element so it can be scrolled to the center -->
+ <div style="height: 3000px;">ABC</div>
+</div>
+<script type="application/javascript">
+async function test() {
+ let utils = SpecialPowers.getDOMWindowUtils(window);
+
+ let input = document.querySelector("input");
+ let container = document.querySelector("#container");
+ let originalScrollTop = container.scrollTop;
+
+ input.focus({ preventScroll: true });
+ await waitToClearOutAnyPotentialScrolls(window);
+ ok(container.scrollTop == originalScrollTop, "scroll position keeps top");
+
+ let waitForScroll = waitForScrollEvent(container);
+ utils.zoomToFocusedInput();
+ await waitForScroll;
+ await promiseApzFlushedRepaints();
+
+ ok(container.scrollTop > originalScrollTop, "scroll position isn't top");
+}
+
+waitUntilApzStable().then(test).then(subtestDone, subtestFailed);
+</script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_iframe.html b/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_iframe.html
new file mode 100644
index 0000000000..d916c35efb
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_iframe.html
@@ -0,0 +1,68 @@
+<!DOCTYPE>
+<html>
+ <head>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Checking zoomToFocusedInput scrolls that focused element is into iframe</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ </head>
+<body>
+<div style="height: 8000px;">ABC</div>
+<iframe style="height: 30em;" src="helper_iframe_textarea.html"></iframe>
+</div>
+<!-- Leave additional room below the element so it can be scrolled to the center -->
+<div style="height: 1000px;">ABC</div>
+<script type="application/javascript">
+async function test() {
+ let utils = SpecialPowers.getDOMWindowUtils(window);
+
+ let iframe = document.querySelector("iframe");
+ let textarea = iframe.contentDocument.querySelector("textarea");
+ for (let i = 0; i < 20; i++) {
+ textarea.value += "foo\n";
+ }
+
+ let win = iframe.contentWindow;
+
+ iframe.focus();
+ textarea.focus();
+
+ await waitToClearOutAnyPotentialScrolls(win);
+
+ textarea.setSelectionRange(0, 0);
+ window.scrollTo(0, 0);
+ await waitToClearOutAnyPotentialScrolls(win);
+ is(0, window.scrollY, "scroll position is reset");
+
+ let transformEndPromise = promiseTransformEnd();
+ utils.zoomToFocusedInput();
+ await promiseApzFlushedRepaints();
+
+ ok(window.scrollY > 0, "scroll position isn't top");
+ ok(iframe.contentWindow.scrollY > 0, "scroll position into iframe isn't top");
+ let prevPosY = window.scrollY;
+
+ await transformEndPromise;
+ await promiseApzFlushedRepaints();
+
+ window.scrollTo(0, 0);
+ await waitToClearOutAnyPotentialScrolls(win);
+ is(0, window.scrollY, "scroll position is reset");
+
+ textarea.setSelectionRange(textarea.value.length, textarea.value.length);
+ transformEndPromise = promiseTransformEnd();
+ utils.zoomToFocusedInput();
+ await transformEndPromise;
+ await promiseApzFlushedRepaints();
+
+ ok(window.scrollY > 0, "scroll position isn't top");
+ ok(iframe.contentWindow.scrollY > 0, "scroll position into iframe isn't top");
+ ok(prevPosY < window.scrollY,
+ "scroll position is different from first line of textarea");
+}
+
+waitUntilApzStable().then(test).then(subtestDone, subtestFailed);
+</script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_multiline.html b/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_multiline.html
new file mode 100644
index 0000000000..ff5912dcee
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_multiline.html
@@ -0,0 +1,94 @@
+<!DOCTYPE>
+<html>
+ <head>
+ <title>Checking zoomToFocusedInput scrolls that focused non-input element is visible position</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ </head>
+<body>
+<div style="height: 8000px;">ABC</div>
+<div id="content">
+</div>
+<!-- Leave additional room below the element so it can be scrolled to the center -->
+<div style="height: 1000px;">ABC</div>
+<script type="application/javascript">
+async function test() {
+ let utils = SpecialPowers.getDOMWindowUtils(window);
+
+ // contenteditable
+ let div = document.createElement("div");
+ div.setAttribute("contenteditable", "true");
+ for (let i = 0; i < 200; i++) {
+ div.innerHTML += "foo<br>";
+ }
+ div.innerHTML += "<span id=last>bar</span>";
+ document.getElementById("content").appendChild(div);
+
+ let selection = window.getSelection();
+ selection.collapse(div.firstChild, 0);
+ window.scrollTo(0, 0);
+ await waitToClearOutAnyPotentialScrolls(window);
+ is(0, window.scrollY, "scroll position is reset");
+ let transformEndPromise = promiseTransformEnd();
+ utils.zoomToFocusedInput();
+ await transformEndPromise;
+ await promiseApzFlushedRepaints();
+ ok(window.scrollY > 0, "scroll position isn't top");
+
+ let prevY = window.scrollY;
+
+ selection.collapse(document.getElementById("last").firstChild, 0);
+ window.scrollTo(0, 0);
+ await waitToClearOutAnyPotentialScrolls(window);
+ is(0, window.scrollY, "scroll position is reset");
+ transformEndPromise = promiseTransformEnd();
+ utils.zoomToFocusedInput();
+ await promiseApzFlushedRepaints();
+ ok(prevY < window.scrollY, "scroll position is visibile position of caret");
+
+ await transformEndPromise;
+
+ document.getElementById("content").removeChild(div);
+
+ // <textarea> element
+ let textarea = document.createElement("textarea");
+ textarea.rows = 200;
+ for (let i = 0; i < 200; i++) {
+ textarea.value += "foo\n";
+ }
+ document.getElementById("content").appendChild(textarea);
+ textarea.focus();
+
+ await waitToClearOutAnyPotentialScrolls(window);
+
+ textarea.setSelectionRange(0, 0);
+ window.scrollTo(0, 0);
+ await waitToClearOutAnyPotentialScrolls(window);
+ is(0, window.scrollY, "scroll position is reset");
+ transformEndPromise = promiseTransformEnd();
+ utils.zoomToFocusedInput();
+ await promiseApzFlushedRepaints();
+ ok(window.scrollY > 0, "scroll position isn't top");
+ prevY = window.scrollY;
+
+ await transformEndPromise;
+
+ textarea.setSelectionRange(textarea.value.length, textarea.value.length);
+ window.scrollTo(0, 0);
+ await waitToClearOutAnyPotentialScrolls(window);
+ is(0, window.scrollY, "scroll position is reset");
+ transformEndPromise = promiseTransformEnd();
+ utils.zoomToFocusedInput();
+ await promiseApzFlushedRepaints();
+ ok(prevY < window.scrollY, "scroll position is visibile position of caret");
+
+ await transformEndPromise;
+
+ document.getElementById("content").removeChild(textarea);
+}
+
+waitUntilApzStable().then(test).then(subtestDone, subtestFailed);
+</script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_nozoom.html b/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_nozoom.html
new file mode 100644
index 0000000000..5edd181a2d
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_nozoom.html
@@ -0,0 +1,39 @@
+<!DOCTYPE>
+<html>
+ <head>
+ <title>Checking zoomToFocusedInput does not zoom if meta viewport does not allow it</title>
+ <meta name="viewport" content="width=device-width, height=device-height, initial-scale=0.5 minimum-scale=0.5, maximum-scale=1, user-scalable=no" />
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ </head>
+<body>
+<input id="input1" type="text" style="border: 5px solid black"/>
+<script type="application/javascript">
+async function test() {
+ let utils = SpecialPowers.getDOMWindowUtils(window);
+
+ let resolution = await getResolution();
+ ok(resolution > 0,
+ "The initial_resolution is " + resolution + ", which is some sane value");
+
+ document.getElementById('input1').focus();
+ await waitToClearOutAnyPotentialScrolls(window);
+ await promiseApzFlushedRepaints();
+ let prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution == prev_resolution, "focusing input did not change resolution " + resolution);
+
+ let transformEndPromise = promiseTransformEnd();
+ utils.zoomToFocusedInput();
+ await waitToClearOutAnyPotentialScrolls(window);
+ await transformEndPromise;
+ await promiseApzFlushedRepaints();
+ resolution = await getResolution();
+ ok(resolution == prev_resolution, "zoomToFocusedInput input did not change resolution " + resolution);
+}
+
+waitUntilApzStable().then(test).then(subtestDone, subtestFailed);
+</script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_nozoom_bug1738696.html b/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_nozoom_bug1738696.html
new file mode 100644
index 0000000000..4320e391b7
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_nozoom_bug1738696.html
@@ -0,0 +1,51 @@
+<!DOCTYPE>
+<html>
+ <head>
+ <title>Checking zoomToFocusedInput does not zoom is meta viewport does not allow it</title>
+ <meta name="viewport" content="width=device-width, height=device-height, initial-scale=1.0, minimum-scale=1, maximum-scale=1, user-scalable=no" />
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ </head>
+<body>
+<input id="input1" type="text" style="width:100%; height: 200px; border: 5px solid black"/>
+<script type="application/javascript">
+/* This test does not always exercise the bug it was written for, in the sense
+ that the test would fail without the corresponding patch. It seems as though
+ when hitting bug 1743602 the test does not exercise the bug. If bug 1743602
+ gets fixed hopefully this test will end up exercising the bug. A dev pixel
+ ratio of 1 seems to make it hard (but not impossible) for the test to
+ exercise the bug. This is what you get when running the emulator locally or
+ in our CI infrastructure. A dev pixel ratio of ~2.6 (23 appunits = 1 dev
+ pixel) seems to make it easier (but not 100%) for the test to exercise the
+ bug. This is what I got when running the test on a real phone locally (pixel 2).
+*/
+async function test() {
+ let utils = SpecialPowers.getDOMWindowUtils(window);
+
+ let resolution = await getResolution();
+ ok(resolution > 0,
+ "The initial_resolution is " + resolution + ", which is some sane value");
+
+ document.getElementById('input1').focus();
+ await waitToClearOutAnyPotentialScrolls(window);
+ await promiseApzFlushedRepaints();
+ let prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution == prev_resolution, "focusing input did not change resolution " + resolution);
+
+ let transformEndPromise = promiseTransformEnd();
+ utils.zoomToFocusedInput();
+ await waitToClearOutAnyPotentialScrolls(window);
+ await transformEndPromise;
+ await promiseApzFlushedRepaints();
+ resolution = await getResolution();
+ ok(resolution == prev_resolution, "zoomToFocusedInput input did not change resolution " + resolution);
+}
+
+SpecialPowers.getDOMWindowUtils(window).setDynamicToolbarMaxHeight(300);
+
+waitUntilApzStable().then(test).then(subtestDone, subtestFailed);
+</script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_scroll.html b/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_scroll.html
new file mode 100644
index 0000000000..4054b51657
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_scroll.html
@@ -0,0 +1,51 @@
+<!DOCTYPE>
+<html>
+ <head>
+ <title>Checking zoomToFocusedInput scrolls that focused input element is visible position</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ </head>
+<body>
+<div style="height: 8000px;">ABC</div>
+<input id="input1">
+<!-- Leave additional room below the element so it can be scrolled to the center -->
+<div style="height: 1000px;">ABC</div>
+<script type="application/javascript">
+async function test() {
+ is(0, window.scrollY, "scroll position starts at zero");
+ input1.focus();
+ await waitToClearOutAnyPotentialScrolls(window);
+ isnot(0, window.scrollY, "scroll position isn't top");
+ window.scrollTo(0, 0);
+ await waitToClearOutAnyPotentialScrolls(window);
+ is(0, window.scrollY, "scroll position is top");
+
+ let utils = SpecialPowers.getDOMWindowUtils(window);
+ let transformEndPromise = promiseTransformEnd();
+ utils.zoomToFocusedInput();
+ isnot(0, window.scrollY, "scroll position isn't top");
+
+ // Test for bug 1669588: check that the zoom animation did not get
+ // cancelled by a main thread scroll position update triggered by
+ // the ScrollContentIntoView() operation performed by zoomToFocusedInput().
+
+ await transformEndPromise;
+ await promiseApzFlushedRepaints();
+
+ // Check that the zoom animation performed additional scrolling
+ // beyond the ScrollContentIntoView(). The ScrollContentIntoView()
+ // just scrolls enough to bring `input1` into the viewport, while
+ // the zoom animation will scroll further to center it. To
+ // distinguish the two cases, check that we scrolled enough that
+ // the element's top is above the middle of the visual viewport.
+ let inputTop = input1.getBoundingClientRect().top;
+ inputTop -= window.visualViewport.offsetTop;
+ ok(inputTop < (window.visualViewport.height / 2),
+ "input was scrolled at least as far as the middle of the viewport");
+}
+
+waitUntilApzStable().then(test).then(subtestDone, subtestFailed);
+</script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_touch-action.html b/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_touch-action.html
new file mode 100644
index 0000000000..bdb49f4b84
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_touch-action.html
@@ -0,0 +1,67 @@
+<!DOCTYPE>
+<html>
+ <head>
+ <title>Checking zoomToFocusedInput zooms if touch-action allows it</title>
+ <meta name="viewport" content="width=device-width, height=device-height, initial-scale=0.5 minimum-scale=0.5, maximum-scale=1" />
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ </head>
+ <style type="text/css">
+ .touch-none {
+ touch-action: none;
+ }
+ .touch-auto {
+ touch-action: auto;
+ }
+ </style>
+<body>
+ <div class="touch-none">
+ <input id="input1" type="text" style="border: 5px solid black">
+ </div>
+ <br>
+ <div class="touch-auto">
+ <input id="input2" type="text" style="border: 5px solid black">
+ </div>
+<script type="application/javascript">
+async function test() {
+ let utils = SpecialPowers.getDOMWindowUtils(window);
+
+ let resolution = await getResolution();
+ ok(resolution > 0,
+ "The initial_resolution is " + resolution + ", which is some sane value");
+
+ document.getElementById('input1').focus();
+ await waitToClearOutAnyPotentialScrolls(window);
+ await promiseApzFlushedRepaints();
+ let prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution == prev_resolution, "focusing input1 did not change resolution " + resolution);
+
+ let transformEndPromise = promiseTransformEnd();
+ utils.zoomToFocusedInput();
+ await waitToClearOutAnyPotentialScrolls(window);
+ await transformEndPromise;
+ await promiseApzFlushedRepaints();
+ resolution = await getResolution();
+ ok(resolution == prev_resolution, "zoomToFocusedInput input1 did not change resolution " + resolution);
+
+ document.getElementById('input2').focus();
+ await waitToClearOutAnyPotentialScrolls(window);
+ await promiseApzFlushedRepaints();
+ resolution = await getResolution();
+ ok(resolution == prev_resolution, "focusing input2 did not change resolution " + resolution);
+
+ transformEndPromise = promiseTransformEnd();
+ utils.zoomToFocusedInput();
+ await waitToClearOutAnyPotentialScrolls(window);
+ await transformEndPromise;
+ await promiseApzFlushedRepaints();
+ resolution = await getResolution();
+ ok(resolution != prev_resolution, "zoomToFocusedInput input2 changed resolution " + resolution);
+}
+
+waitUntilApzStable().then(test).then(subtestDone, subtestFailed);
+</script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_zoom.html b/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_zoom.html
new file mode 100644
index 0000000000..c74fe521b4
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_zoom.html
@@ -0,0 +1,39 @@
+<!DOCTYPE>
+<html>
+ <head>
+ <title>Checking zoomToFocusedInput zooms if meta viewport allows it</title>
+ <meta name="viewport" content="width=device-width, height=device-height, initial-scale=0.5 minimum-scale=0.5, maximum-scale=1" />
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ </head>
+<body>
+<input id="input1" type="text" style="border: 5px solid black"/>
+<script type="application/javascript">
+async function test() {
+ let utils = SpecialPowers.getDOMWindowUtils(window);
+
+ let resolution = await getResolution();
+ ok(resolution > 0,
+ "The initial_resolution is " + resolution + ", which is some sane value");
+
+ document.getElementById('input1').focus();
+ await waitToClearOutAnyPotentialScrolls(window);
+ await promiseApzFlushedRepaints();
+ let prev_resolution = resolution;
+ resolution = await getResolution();
+ ok(resolution == prev_resolution, "focusing input did not change resolution " + resolution);
+
+ let transformEndPromise = promiseTransformEnd();
+ utils.zoomToFocusedInput();
+ await waitToClearOutAnyPotentialScrolls(window);
+ await transformEndPromise;
+ await promiseApzFlushedRepaints();
+ resolution = await getResolution();
+ ok(resolution != prev_resolution, "zoomToFocusedInput input changed resolution " + resolution);
+}
+
+waitUntilApzStable().then(test).then(subtestDone, subtestFailed);
+</script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_zoom_after_gpu_process_restart.html b/gfx/layers/apz/test/mochitest/helper_zoom_after_gpu_process_restart.html
new file mode 100644
index 0000000000..0ce6113710
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_zoom_after_gpu_process_restart.html
@@ -0,0 +1,63 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width">
+ <title>Sanity check for pinch zooming after GPU process restart</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+async function test() {
+ let initial_resolution = await getResolution();
+ ok(initial_resolution > 0,
+ "The initial_resolution is " + initial_resolution + ", which is some sane value");
+
+ // Kill the GPU process
+ await SpecialPowers.spawnChrome([], async () => {
+ const gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo);
+
+ if (gfxInfo.usingGPUProcess) {
+ const { TestUtils } = ChromeUtils.import(
+ "resource://testing-common/TestUtils.jsm"
+ );
+ let promise = TestUtils.topicObserved("compositor-reinitialized");
+
+ gfxInfo.killGPUProcessForTests();
+ await promise;
+ }
+ });
+
+ // Ensure resolution is unchanged by GPU process restart
+ await waitUntilApzStable();
+ let resolution = await getResolution();
+ ok(
+ resolution == initial_resolution,
+ "The resolution (" + resolution + ") is the same after GPU process restart"
+ );
+
+ // Perform the zoom
+ await pinchZoomInWithTouch(150, 300);
+
+ // Flush state and get the resolution we're at now
+ await promiseApzFlushedRepaints();
+ let final_resolution = await getResolution();
+ ok(
+ final_resolution > initial_resolution,
+ "The final resolution (" + final_resolution + ") is greater after zooming in"
+ );
+}
+
+waitUntilApzStable()
+ .then(test)
+ .then(subtestDone, subtestFailed);
+
+ </script>
+</head>
+<body>
+ Here is some text to stare at as the test runs. It serves no functional
+ purpose, but gives you an idea of the zoom level. It's harder to tell what
+ the zoom level is when the page is just solid white.
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_zoom_keyboardscroll.html b/gfx/layers/apz/test/mochitest/helper_zoom_keyboardscroll.html
new file mode 100644
index 0000000000..1a7b38fabd
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_zoom_keyboardscroll.html
@@ -0,0 +1,74 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, minimum-scale=1.0">
+ <title>Tests that keyboard arrow keys scroll after zooming in when there was no scrollable overflow before zooming</title>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+</head>
+<body>
+ <div style="height: 20000px; background-color: green"></div>
+ <script type="application/javascript">
+ const utils = SpecialPowers.getDOMWindowUtils(window);
+
+ async function test() {
+ is(await getResolution(), 1.0, "should not be zoomed (1)");
+
+ is(window.scrollX, 0, "shouldn't have scrolled (2)");
+ is(window.scrollY, 0, "shouldn't have scrolled (3)");
+ is(visualViewport.pageTop, 0, "shouldn't have scrolled (4)");
+ is(visualViewport.pageLeft, 0, "shouldn't have scrolled (5)");
+
+ // Zoom in
+ SpecialPowers.getDOMWindowUtils(window).setResolutionAndScaleTo(2.0);
+ await promiseApzFlushedRepaints();
+ await promiseFrame();
+
+ is(await getResolution(), 2.0, "should have zoomed (6)");
+
+ is(window.scrollX, 0, "shouldn't have scrolled (7)");
+ is(window.scrollY, 0, "shouldn't have scrolled (8)");
+ is(visualViewport.pageTop, 0, "shouldn't have scrolled (9)");
+ is(visualViewport.pageLeft, 0, "shouldn't have scrolled (10)");
+
+ window.synthesizeKey("KEY_ArrowRight");
+
+ await promiseApzFlushedRepaints();
+ await promiseFrame();
+
+ is(await getResolution(), 2.0, "should be zoomed (11)");
+
+ is(window.scrollX, 0, "shouldn't have scrolled (12)");
+ is(window.scrollY, 0, "shouldn't have scrolled (13)");
+ is(visualViewport.pageTop, 0, "shouldn't have scrolled (14)");
+ isnot(visualViewport.pageLeft, 0, "should have scrolled (15)");
+
+ window.synthesizeKey("KEY_ArrowDown");
+
+ await promiseApzFlushedRepaints();
+ await promiseFrame();
+
+ is(await getResolution(), 2.0, "should be zoomed (16)");
+
+ is(window.scrollX, 0, "shouldn't have scrolled (17)");
+ is(window.scrollY, 0, "shouldn't have scrolled (18)");
+ isnot(visualViewport.pageTop, 0, "should have scrolled (19)");
+ isnot(visualViewport.pageLeft, 0, "should have scrolled (20)");
+
+ // Zoom back out
+ SpecialPowers.getDOMWindowUtils(window).setResolutionAndScaleTo(1.0);
+ await promiseApzFlushedRepaints();
+ await promiseFrame();
+
+ is(await getResolution(), 1.0, "should not be zoomed (21)");
+ }
+
+ waitUntilApzStable()
+ .then(test)
+ .then(subtestDone, subtestFailed);
+ </script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_zoom_oopif.html b/gfx/layers/apz/test/mochitest/helper_zoom_oopif.html
new file mode 100644
index 0000000000..788fa31bbb
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_zoom_oopif.html
@@ -0,0 +1,54 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width">
+ <title>Sanity check for pinch zooming oop iframe</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="application/javascript">
+
+async function test() {
+ let useTouchpad = (location.search == "?touchpad");
+
+ let thetarget = document.getElementById("target");
+ let r = thetarget.getBoundingClientRect();
+ let x = r.x + r.width/2;
+ let y = r.y + r.height/2;
+
+ let initial_resolution = await getResolution();
+ ok(initial_resolution > 0,
+ "The initial_resolution is " + initial_resolution + ", which is some sane value");
+ if (useTouchpad) {
+ await pinchZoomInWithTouchpad(x, y);
+ } else {
+ await pinchZoomInWithTouch(x, y);
+ }
+ // Flush state and get the resolution we're at now
+ await promiseApzFlushedRepaints();
+ let final_resolution = await getResolution();
+ ok(final_resolution > initial_resolution, "The final resolution (" + final_resolution + ") is greater after zooming in");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+<style>
+iframe {
+ margin: 0;
+ padding: 0;
+ border: 1px solid black;
+}
+</style>
+
+</head>
+<body>
+
+<iframe id="target" width="100" height="100" src="http://example.org/"></iframe>
+
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_zoom_out_clamped_scrollpos.html b/gfx/layers/apz/test/mochitest/helper_zoom_out_clamped_scrollpos.html
new file mode 100644
index 0000000000..2948df9ae3
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_zoom_out_clamped_scrollpos.html
@@ -0,0 +1,85 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, minimum-scale=1.0">
+ <title>Tests that zooming out with an unchanging scroll pos still works properly</title>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+</head>
+<body>
+ <div style="height: 2000px; background-color: linear-gradient(green,blue)"></div>
+ <script type="application/javascript">
+ const utils = SpecialPowers.getDOMWindowUtils(window);
+
+ async function test() {
+ // Initial state
+ is(await getResolution(), 1.0, "should not be zoomed");
+
+ // Zoom in
+ utils.setResolutionAndScaleTo(2.0);
+ await promiseApzFlushedRepaints();
+ // Check that we're still at 0,0 in both layout and visual viewport
+ is(await getResolution(), 2.0, "should be zoomed to 2.0");
+ is(window.scrollX, 0, "shouldn't have scrolled (1)");
+ is(window.scrollY, 0, "shouldn't have scrolled (2)");
+ is(visualViewport.pageLeft, 0, "shouldn't have scrolled (3)");
+ is(visualViewport.pageTop, 0, "shouldn't have scrolled (4)");
+
+ // Freeze the main-thread refresh driver to stop it from processing
+ // paint requests
+ utils.advanceTimeAndRefresh(0);
+
+ // Zoom out. This will send a series of paint requests to the main
+ // thread with zooms that go down from 2.0 to 1.0.
+ // Use a similar touch sequence to what pinchZoomOutWithTouchAtCenter()
+ // does, except keep the first touch point anchored and only move the
+ // second touch point. In particular, we drag the second touch point
+ // from the top-left quadrant of the screen to the bottom-right, so that
+ // the scroll position never changes from 0,0. If we move either finger
+ // upwards at all, the synthesization can generate intermediate touch
+ // events with just that change which can cause the page to scroll down
+ // which we don't want here.
+ // The key here is that each of the repaint requests keeps the scroll
+ // position at 0,0, which in terms of the bug, means that only the first
+ // repaint request actually takes effect and the rest are discarded.
+ // The first repaint request has a zoom somewhere between 1.0 and 2.0,
+ // and therefore after the pinch is done, the zoom ends up stuck there
+ // instead of going all the way back to 1.0 like we would expect.
+ const deltaX = window.visualViewport.width / 16;
+ const deltaY = window.visualViewport.height / 16;
+ const centerX =
+ window.visualViewport.pageLeft + window.visualViewport.width / 2;
+ const centerY =
+ window.visualViewport.pageTop + window.visualViewport.height / 2;
+ const anchorFinger = { x: centerX + (deltaX * 6), y: centerY + (deltaY * 6) };
+ var zoom_out = [];
+ for (var i = -6; i < 6; i++) {
+ var movingFinger = { x: centerX + (deltaX * i), y: centerY + (deltaY * i) };
+ zoom_out.push([anchorFinger, movingFinger]);
+ }
+ var touchIds = [0, 1];
+ await synthesizeNativeTouchAndWaitForTransformEnd(zoom_out, touchIds);
+
+ // Release the refresh driver
+ utils.restoreNormalRefresh();
+
+ // Flush all the things, reach stable state
+ await promiseApzFlushedRepaints();
+
+ // Check that we're back at 1.0 resolution
+ is(await getResolution(), 1.0, "should be back at initial resolution");
+
+ // More sanity checks
+ is(window.scrollX, 0, "shouldn't have scrolled (5)");
+ is(window.scrollY, 0, "shouldn't have scrolled (6)");
+ is(visualViewport.pageLeft, 0, "shouldn't have scrolled (7)");
+ is(visualViewport.pageTop, 0, "shouldn't have scrolled (8)");
+ }
+
+ waitUntilApzStable().then(test).then(subtestDone, subtestFailed);
+ </script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_zoom_out_with_mainthread_clamping.html b/gfx/layers/apz/test/mochitest/helper_zoom_out_with_mainthread_clamping.html
new file mode 100644
index 0000000000..c15622872a
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_zoom_out_with_mainthread_clamping.html
@@ -0,0 +1,110 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, minimum-scale=1.0">
+ <title>Tests that zooming out in a way that triggers main-thread scroll re-clamping works properly</title>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+</head>
+<body>
+ <div style="width: 200vw; height: 2000px; background-color: linear-gradient(green,blue)"></div>
+ <script type="application/javascript">
+ const utils = SpecialPowers.getDOMWindowUtils(window);
+
+ async function test() {
+ // Initial state
+ is(await getResolution(), 1.0, "should not be zoomed");
+
+ // Zoom in and go to the bottom-right corner. This ensures the layout
+ // and visual scroll offsets are nonzero, which increases the chances
+ // that the scroll position layer alignment code will mutate the scroll
+ // position (see comment below).
+ utils.setResolutionAndScaleTo(5.0);
+ await promiseApzFlushedRepaints();
+ utils.scrollToVisual(document.scrollingElement.clientWidth * 5,
+ document.scrollingElement.clientHeight * 5,
+ utils.UPDATE_TYPE_MAIN_THREAD,
+ utils.SCROLL_MODE_INSTANT);
+ await promiseApzFlushedRepaints();
+
+ // Check that we're at the right place
+ is(await getResolution(), 5.0, "should be zoomed to 5.0");
+ is(window.scrollX, window.scrollMaxX, "layout x-coord should be maxed");
+ is(window.scrollY, window.scrollMaxY, "layout y-coord should be maxed");
+ ok(visualViewport.offsetLeft > 0, "visual x-coord should be even further");
+ ok(visualViewport.offsetTop > 0, "visual y-coord should be even further");
+
+ // Zoom out. This will trigger repaint requests to the main thread,
+ // at various intermediate resolutions. The repaint requests will
+ // trigger reflows, which will trigger the root scrollframe to re-clamp
+ // and layer-align the scroll position as part of the post-reflow action.
+ // The test is checking that these mutations don't end up sending a scroll
+ // position update to APZ that interrupts the zoom action (see bug 1671284
+ // comment 9 for the exact mechanism). In order to maximize the chances of
+ // catching the bug, we wait for the main thread repaint after each of the
+ // pinch inputs.
+
+ let zoom_out = pinchZoomOutTouchSequenceAtCenter();
+ // Do coordinate conversion up-front using the current resolution and
+ // visual viewport.
+ for (let entry of zoom_out) {
+ for (let i = 0; i < entry.length; i++) {
+ entry[i] = await coordinatesRelativeToScreen({
+ offsetX: entry[i].x,
+ offsetY: entry[i].y,
+ target: document.body,
+ });
+ }
+ }
+ // Dispatch the touch events, waiting for paints after each row in
+ // zoom_out.
+ let touchIds = [0, 1];
+ for (let i = 0; i < zoom_out.length; i++) {
+ let entry = zoom_out[i];
+ for (let j = 0; j < entry.length; j++) {
+ await new Promise(resolve => {
+ utils.sendNativeTouchPoint(
+ touchIds[j],
+ utils.TOUCH_CONTACT,
+ entry[j].x,
+ entry[j].y,
+ 1,
+ 90,
+ resolve
+ );
+ });
+ }
+ await promiseAllPaintsDone();
+
+ // On the last row also do the touch-up events
+ if (i == zoom_out.length - 1) {
+ for (let j = 0; j < entry.length; j++) {
+ await new Promise(resolve => {
+ utils.sendNativeTouchPoint(
+ touchIds[j],
+ utils.TOUCH_REMOVE,
+ entry[j].x,
+ entry[j].y,
+ 1,
+ 90,
+ resolve
+ );
+ });
+ }
+ }
+ }
+
+ // Wait for everything to stabilize
+ await promiseApzFlushedRepaints();
+
+ // Verify that the zoom completed and we're back at 1.0 resolution
+ isfuzzy(await getResolution(), 1.0, 0.0001, "should be back at initial resolution");
+ }
+
+ waitUntilApzStable().then(test).then(subtestDone, subtestFailed);
+ </script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_zoom_prevented.html b/gfx/layers/apz/test/mochitest/helper_zoom_prevented.html
new file mode 100644
index 0000000000..b756c873f2
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_zoom_prevented.html
@@ -0,0 +1,75 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width">
+ <title>Checking prevent-default for zooming</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+async function testPreventDefault(aTouchStartToCancel) {
+ var initial_resolution = await getResolution();
+ ok(initial_resolution > 0,
+ "The initial_resolution is " + initial_resolution + ", which is some sane value");
+
+ // preventDefault exactly one touchstart based on the value of aTouchStartToCancel
+ var touchStartCount = 0;
+ var canceller = function(e) {
+ dump("touchstart listener hit, count: " + touchStartCount + "\n");
+ touchStartCount++;
+ if (touchStartCount == aTouchStartToCancel) {
+ dump("calling preventDefault on touchstart\n");
+ e.preventDefault();
+ document.documentElement.removeEventListener("touchstart", canceller, {passive: false});
+ }
+ };
+ document.documentElement.addEventListener("touchstart", canceller, {passive: false});
+
+ let touchEndPromise = new Promise(resolve => {
+ document.documentElement.addEventListener("touchend", resolve, {passive: true, once: true});
+ });
+
+ // Ensure that APZ gets updated hit-test info
+ await promiseAllPaintsDone();
+
+ await pinchZoomInTouchSequence(150, 300);
+ await touchEndPromise; // wait for the touchend listener to fire
+
+ // Flush state and get the resolution we're at now
+ await promiseApzFlushedRepaints();
+ let final_resolution = await getResolution();
+ is(final_resolution, initial_resolution, "The final resolution (" + final_resolution + ") matches the initial resolution");
+}
+
+function transformFailer() {
+ ok(false, "The test fired an unexpected APZ:TransformEnd");
+}
+
+async function test() {
+ // Register a listener that fails the test if the APZ:TransformEnd event fires,
+ // because this test shouldn't actually be triggering any transforms
+ SpecialPowers.Services.obs.addObserver(transformFailer, "APZ:TransformEnd");
+
+ await testPreventDefault(1);
+ await testPreventDefault(2);
+}
+
+function cleanup() {
+ SpecialPowers.Services.obs.removeObserver(transformFailer, "APZ:TransformEnd");
+}
+
+waitUntilApzStable()
+.then(test)
+.finally(cleanup)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</head>
+<body>
+ Here is some text to stare at as the test runs. It serves no functional
+ purpose, but gives you an idea of the zoom level. It's harder to tell what
+ the zoom level is when the page is just solid white.
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_zoom_restore_position_tabswitch.html b/gfx/layers/apz/test/mochitest/helper_zoom_restore_position_tabswitch.html
new file mode 100644
index 0000000000..0373b321da
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_zoom_restore_position_tabswitch.html
@@ -0,0 +1,74 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width">
+ <title>Switching tabs back to a zoomed page should restore visual offset</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="application/javascript">
+
+async function test() {
+ let visResEvt = new EventCounter(window.visualViewport, "resize");
+
+ // Do a pinch-zoom in, wait for everything to settle on the APZ side.
+ await pinchZoomInWithTouch(400, 300);
+ await promiseApzFlushedRepaints();
+
+ // Force a new layer tree to the compositor, because otherwise tab-switching
+ // away and back can end up reusing the last layer tree sent to the compositor,
+ // and that may not have the latest visual viewport offset sent from APZ
+ // to the main thread. This should be removed once bug 1640730 is fixed.
+ await forceLayerTreeToCompositor();
+
+ // Wait until we get a visual viewport resized event, if we haven't yet gotten
+ // one.
+ if (visResEvt.count == 0) {
+ await promiseOneEvent(window.visualViewport, "resize", null);
+ }
+ let resizeCount = visResEvt.count;
+ ok(resizeCount > 0, `Visual viewport got resized ${resizeCount} times`);
+
+ // Record the current visual viewport and ensure it reflects a zoomed state.
+ let zoomedViewport = visualViewportAsZoomedRect();
+ ok(visualViewport.offsetLeft > 0, "Visual viewport should not be same as layout viewport (left)");
+ ok(visualViewport.offsetTop > 0, "Visual viewport should not be same as layout viewport (top)");
+ ok(zoomedViewport.x > 0, "Sanity check to ensure visual viewport is not at 0,0 (left)");
+ ok(zoomedViewport.y > 0, "Sanity check to ensure visual viewport is not at 0,0 (top)");
+ ok(zoomedViewport.z > 1, "Sanity check to ensure visual viewport scale is > 1");
+
+ // Open a new foreground tab and wait until it closes itself. The tab itself
+ // waits for APZ stability before closing, so we know that the APZ state
+ // was updated to the other document and back to this window.
+ let focusPromise = promiseOneEvent(window, "focus", null);
+ window.open("helper_self_closer.html", "_blank");
+ await focusPromise;
+ ok(true, "Got focus back after self-closer closed");
+
+ // Wait for the dust to settle.
+ await promiseApzFlushedRepaints();
+
+ // Ensure the visual viewport is just as we left it.
+ let restoredViewport = visualViewportAsZoomedRect();
+ for (field in zoomedViewport) {
+ is(restoredViewport[field], zoomedViewport[field], `Field ${field} of the zoomed viewport restored`);
+ }
+
+ // Just for good measure. This might help with debugging unexpected failures.
+ is(visResEvt.count, resizeCount, "No more VV resizes should have occurred");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</head>
+<body>
+ Here is some text to stare at as the test runs. It serves no functional
+ purpose, but gives you an idea of the zoom level. It's harder to tell what
+ the zoom level is when the page is just solid white.
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_zoom_with_dynamic_toolbar.html b/gfx/layers/apz/test/mochitest/helper_zoom_with_dynamic_toolbar.html
new file mode 100644
index 0000000000..261bca1377
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_zoom_with_dynamic_toolbar.html
@@ -0,0 +1,45 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width">
+ <title>Zooming out to the initial scale with the dynamic toolbar</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+
+ <style>
+ html,body {
+ height: 100%;
+ margin: 0;
+ padding: 0;
+ }
+ </style>
+
+ <script type="application/javascript">
+
+async function test() {
+ ok(window.visualViewport.scale > 1.0,
+ "The scale value should be greater than 1.0");
+
+ // Do a pinch-zoom out to restore the initial scale.
+ await pinchZoomOutWithTouchAtCenter();
+ await promiseApzFlushedRepaints();
+
+ is(visualViewport.scale, 1.0,
+ "The initial scale value should be restored to 1.0");
+}
+
+SpecialPowers.getDOMWindowUtils(window).setDynamicToolbarMaxHeight(100);
+SpecialPowers.getDOMWindowUtils(window).setResolutionAndScaleTo(1.1)
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_zoom_with_touchpad.html b/gfx/layers/apz/test/mochitest/helper_zoom_with_touchpad.html
new file mode 100644
index 0000000000..6836864964
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_zoom_with_touchpad.html
@@ -0,0 +1,110 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width">
+ <title>Sanity check for Touchpad pinch zooming</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="application/javascript">
+
+async function test() {
+ // Scenario 1: zoom in
+ var initial_resolution = await getResolution();
+ ok(initial_resolution > 0,
+ "The initial_resolution is " + initial_resolution + ", which is some sane value");
+ await pinchZoomInWithTouchpad(641, 465);
+ // Flush state and get the resolution we're at now
+ await promiseApzFlushedRepaints();
+ let final_resolution = await getResolution();
+ ok(final_resolution > initial_resolution, "The final resolution (" + final_resolution + ") is greater after zooming in");
+
+ // Scenario 2: zoom out
+ initial_resolution = final_resolution;
+ ok(initial_resolution > 0,
+ "The initial_resolution is " + initial_resolution + ", which is some sane value");
+ await pinchZoomOutWithTouchpad(641, 465);
+ await promiseApzFlushedRepaints();
+ final_resolution = await getResolution();
+ ok(final_resolution < initial_resolution, "The final resolution (" + final_resolution + ") is smaller after zooming Out");
+
+ // Scenario 3: zoom in and out in the same gesture
+ initial_resolution = final_resolution;
+ ok(initial_resolution > 0,
+ "The initial_resolution is " + initial_resolution + ", which is some sane value");
+ await pinchZoomInOutWithTouchpad (641, 465);
+ await promiseApzFlushedRepaints();
+ final_resolution = await getResolution();
+ isfuzzy(initial_resolution, final_resolution, 0.0001, "The final resolution approximatly the same after zooming In and Out");
+
+ // Scenario 4: zoom in, with the page using preventDefault()
+ var resolveWheelPromise;
+ var wheelPromise = new Promise(resolve => { resolveWheelPromise = resolve; });
+ var deltaSum = 0;
+ initial_resolution = final_resolution;
+ var onWheel = function(e) {
+ if (e.ctrlKey) {
+ e.preventDefault();
+ deltaSum += e.deltaY;
+ // We observed that deltaSum will be around -42 by the time all wheel events have arrived.
+ if (deltaSum < -40) {
+ ok(true, "Accumulated a deltaY of -40");
+ resolveWheelPromise();
+ }
+ }
+ };
+
+ document.addEventListener("wheel", onWheel, { passive: false });
+ // Give APZ a chance to become aware of the listener, so it knows
+ // to queue events while it waits for a content response.
+ await promiseApzFlushedRepaints();
+ // Calling preventDefault() means the APZ:TransformEnd notification will never be sent.
+ await pinchZoomInWithTouchpad(641, 465, { waitForTransformEnd: false });
+ await wheelPromise;
+ document.removeEventListener("wheel", onWheel, { passive: false });
+ final_resolution = await getResolution();
+ is(final_resolution, initial_resolution,
+ "Calling preventDefault() on wheel event successfully prevents zooming");
+
+ // Scenario 5: check that page receives DOMMouseScroll event
+ var resolveDOMMouseScrollPromise;
+ var DOMMouseScrollPromise = new Promise(resolve => { resolveDOMMouseScrollPromise = resolve; });
+ deltaSum = 0;
+ initial_resolution = final_resolution;
+ var onDOMMouseScroll = function(e) {
+ if (e.ctrlKey) {
+ e.preventDefault();
+ deltaSum += e.detail;
+ if (deltaSum < -40) {
+ ok(true, "Accumulated a deltaSum of -40");
+ resolveDOMMouseScrollPromise();
+ }
+ }
+ };
+ document.addEventListener("DOMMouseScroll", onDOMMouseScroll, { passive: false });
+ await promiseApzFlushedRepaints();
+ await pinchZoomInWithTouchpad(641, 465, {
+ waitForTransformEnd: false,
+ waitForFrames: true
+ });
+ await DOMMouseScrollPromise;
+ document.removeEventListener("DOMMouseScroll", onDOMMouseScroll, { passive: false });
+ final_resolution = await getResolution();
+ is(final_resolution, initial_resolution,
+ "Calling preventDefault() on DOMMouseScroll event successfully prevents zooming");
+}
+
+waitUntilApzStable()
+.then(test)
+.then(subtestDone, subtestFailed);
+
+ </script>
+</head>
+<body>
+ Here is some text to stare at as the test runs. It serves no functional
+ purpose, but gives you an idea of the zoom level. It's harder to tell what
+ the zoom level is when the page is just solid white.
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_zoomed_pan.html b/gfx/layers/apz/test/mochitest/helper_zoomed_pan.html
new file mode 100644
index 0000000000..98547fb73f
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_zoomed_pan.html
@@ -0,0 +1,79 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0,minimum-scale=1.0">
+ <title>Ensure layout viewport responds to panning while pinched</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+ body {
+ margin: 0;
+ padding: 0;
+ }
+ #content {
+ height: 5000px;
+ width: 5000px;
+ background: repeating-linear-gradient(#EEE, #EEE 100px, #DDD 100px, #DDD 200px);
+ }
+ </style>
+</head>
+<body>
+ <div id="content"></div>
+ <script type="application/javascript">
+ const RESOLUTION = 4;
+ const OFFSET_SCREEN_PX = 50;
+ const OFFSET_CSS_PX = OFFSET_SCREEN_PX / RESOLUTION;
+
+ function computeDelta(visual) {
+ // Compute the distance from the right/bottom edge of the visual
+ // viewport to the same edge of the layout viewport and add the desired
+ // offset to that.
+ // We can ignore scrollbar width here since the scrollbar is layouted at
+ // the right/bottom edge of this content, not of this window in the case
+ // of containerful scrolling.
+ return visual - (visual / RESOLUTION) + OFFSET_CSS_PX;
+ }
+
+ async function test() {
+ const cases = [
+ {
+ x: 0,
+ y: 0,
+ dx: (width) => -computeDelta(width),
+ dy: (height) => 0,
+ expected: {
+ x: [OFFSET_CSS_PX, "x-offset was adjusted"],
+ y: [0, "y-offset was not affected"],
+ },
+ },
+ {
+ x: OFFSET_CSS_PX,
+ y: 0,
+ dx: (width) => 0,
+ dy: (height) => -computeDelta(height),
+ expected: {
+ x: [OFFSET_CSS_PX, "x-offset was not affected"],
+ y: [OFFSET_CSS_PX, "y-offset was adjusted"],
+ },
+ },
+ ];
+
+ for (let c of cases) {
+ await promiseNativeTouchDrag(window,
+ c.x,
+ c.y,
+ c.dx(document.documentElement.clientWidth),
+ c.dy(document.documentElement.clientHeight));
+ await promiseApzFlushedRepaints();
+ is(window.scrollX, c.expected.x[0], c.expected.x[1]);
+ is(window.scrollY, c.expected.y[0], c.expected.y[1]);
+ }
+ }
+
+ SpecialPowers.getDOMWindowUtils(window).setResolutionAndScaleTo(RESOLUTION);
+ waitUntilApzStable().then(test).then(subtestDone, subtestFailed);
+ </script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/mochitest.ini b/gfx/layers/apz/test/mochitest/mochitest.ini
new file mode 100644
index 0000000000..af357a0682
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/mochitest.ini
@@ -0,0 +1,125 @@
+[DEFAULT]
+ prefs =
+ gfx.font_loader.delay=0
+ support-files =
+ apz_test_native_event_utils.js
+ apz_test_utils.js
+ green100x100.png
+ helper_*.*
+ tags = apz
+[test_abort_smooth_scroll_by_instant_scroll.html]
+[test_bug1151667.html]
+ skip-if =
+ os == 'android' # wheel events not supported on mobile
+[test_bug1253683.html]
+ skip-if =
+ os == 'android' # wheel events not supported on mobile
+[test_bug1277814.html]
+ skip-if =
+ os == 'android' # wheel events not supported on mobile
+[test_bug1304689-2.html]
+[test_bug1304689.html]
+[test_frame_reconstruction.html]
+[test_group_bug1534549.html]
+[test_group_checkerboarding.html]
+ skip-if =
+ http3
+[test_group_displayport.html]
+[test_group_double_tap_zoom-2.html]
+ run-if = ((os == 'android') || (os == 'mac')) # FIXME: enable on more desktop platforms (see bug 1608506 comment 4)
+[test_group_double_tap_zoom.html]
+ run-if = ((os == 'android') || (os == 'mac')) # FIXME: enable on more desktop platforms (see bug 1608506 comment 4)
+[test_group_fullscreen.html]
+ run-if = (os == 'android')
+[test_group_hittest-1.html]
+ skip-if =
+ toolkit == 'android' # mouse events not supported on mobile
+[test_group_hittest-2.html]
+ skip-if =
+ toolkit == 'android' # mouse events not supported on mobile
+ os == 'win' && (bits == 32 || asan)
+ os == 'linux' && asan # stack is not large enough for the test
+ http3
+[test_group_hittest-3.html]
+ skip-if =
+ toolkit == 'android' # mouse events not supported on mobile
+ os == 'win' && (bits == 32 || asan)
+ http3
+[test_group_hittest-overscroll.html]
+ skip-if =
+ toolkit == 'android' # mouse events not supported on mobile
+[test_group_keyboard-2.html]
+[test_group_keyboard.html]
+[test_group_mainthread.html]
+[test_group_minimum_scale_size.html]
+ run-if = (os == 'android')
+[test_group_mouseevents.html]
+ skip-if =
+ toolkit == 'android' # mouse events not supported on mobile
+[test_group_overrides.html]
+ skip-if =
+ toolkit == 'android' # wheel events not supported on mobile
+[test_group_overscroll.html]
+ skip-if =
+ toolkit == 'android' # wheel events not supported on mobile
+[test_group_overscroll_handoff.html]
+ skip-if =
+ toolkit == 'android' # wheel events not supported on mobile
+ http3
+[test_group_pointerevents.html]
+ skip-if = (os == 'win' && os_version == '10.0') # Bug 1404836
+[test_group_programmatic_scroll_behavior.html]
+[test_group_scroll_linked_effect.html]
+ skip-if =
+ (toolkit == 'android') # wheel events not supported on mobile
+ http3
+[test_group_scroll_snap.html]
+ skip-if = (os == 'android') # wheel events not supported on mobile
+[test_group_scrollend.html]
+ skip-if = (toolkit == 'android') # wheel events not supported on mobile
+[test_group_scrollframe_activation.html]
+[test_group_touchevents-2.html]
+[test_group_touchevents-3.html]
+[test_group_touchevents-4.html]
+[test_group_touchevents-5.html]
+[test_group_touchevents.html]
+[test_group_wheelevents.html]
+ skip-if = (toolkit == 'android') # wheel events not supported on mobile
+[test_group_zoom-2.html]
+ skip-if = (os == 'win') # see bug 1495580 for Windows
+[test_group_zoom.html]
+ skip-if =
+ os == 'win' # see bug 1495580 for Windows
+[test_group_zoomToFocusedInput.html]
+[test_interrupted_reflow.html]
+[test_layerization.html]
+ skip-if =
+ os == 'android' # wheel events not supported on mobile
+ os == 'linux' && fission && headless # Bug 1722907
+[test_relative_update.html]
+ skip-if =
+ os == 'android' # wheel events not supported on mobile
+[test_scroll_inactive_bug1190112.html]
+ skip-if = (os == 'android') # wheel events not supported on mobile
+[test_scroll_inactive_flattened_frame.html]
+ skip-if = (os == 'android') # wheel events not supported on mobile
+[test_scroll_subframe_scrollbar.html]
+ skip-if = (os == 'android') # wheel events not supported on mobile
+[test_smoothness.html]
+ # hardware vsync only on win/mac
+ # Frame Uniformity recording is not implemented for webrender
+ skip-if =
+ debug
+ (os != 'mac' && os != 'win')
+ verify
+ true # Don't run in CI yet, see bug 1657477
+[test_touch_listeners_impacting_wheel.html]
+ skip-if =
+ toolkit == 'android' # wheel events not supported on mobile
+ toolkit == 'cocoa' # synthesized wheel smooth-scrolling not supported on OS X
+[test_wheel_scroll.html]
+ skip-if =
+ os == 'android' # wheel events not supported on mobile
+[test_wheel_transactions.html]
+ skip-if =
+ toolkit == 'android' # wheel events not supported on mobile
diff --git a/gfx/layers/apz/test/mochitest/test_abort_smooth_scroll_by_instant_scroll.html b/gfx/layers/apz/test/mochitest/test_abort_smooth_scroll_by_instant_scroll.html
new file mode 100644
index 0000000000..650c21cac7
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_abort_smooth_scroll_by_instant_scroll.html
@@ -0,0 +1,51 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test to make sure an on-going smooth scroll is aborted by a new
+ instant absolute scroll operation</title>
+ <meta charset="utf-8">
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script src="apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<div style="height: 100000px;"></div>
+<script type="application/javascript">
+async function test() {
+ // Trigger a smooth scroll.
+ document.scrollingElement.scrollTo({ top: 90000, behavior: "smooth" });
+ // Need to wait for a scroll event here since with the very small
+ // layout.css.scroll-behavior.spring-constant value it's possible that the
+ // scroll position hasn't yet been changed after a promiseApzFlushedRepaints
+ // call.
+ await waitForScrollEvent(window);
+ await promiseApzFlushedRepaints();
+
+ ok(document.scrollingElement.scrollTop > 0,
+ "Should have scrolled. scroll position: " + document.scrollingElement.scrollTop);
+
+ // Trigger an instant scroll.
+ document.scrollingElement.scrollTo({ top: 0, behavior: "instant" });
+ is(document.scrollingElement.scrollTop, 0,
+ "The previous smooth scroll operation should have been superseded by " +
+ "the instant scroll");
+
+ // Double check after a repaint request.
+ await promiseApzFlushedRepaints();
+ is(document.scrollingElement.scrollTop, 0,
+ "The scroll postion should have stayed after a repaint request");
+}
+
+SimpleTest.waitForExplicitFinish();
+
+// Use a very small spring constant value for smooth scrolling so that the
+// smooth scrollling keeps running at least for a few seconds.
+pushPrefs([["layout.css.scroll-behavior.spring-constant", 1]])
+.then(waitUntilApzStable)
+.then(test)
+.then(SimpleTest.finish, SimpleTest.finishWithFailure);
+
+</script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_bug1151667.html b/gfx/layers/apz/test/mochitest/test_bug1151667.html
new file mode 100644
index 0000000000..12a46b9094
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_bug1151667.html
@@ -0,0 +1,63 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1151667
+-->
+<head>
+ <title>Test for Bug 1151667</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <style>
+ #subframe {
+ margin-top: 100px;
+ height: 500px;
+ width: 500px;
+ overflow: scroll;
+ }
+ #subframe-content {
+ height: 1000px;
+ width: 500px;
+ /* the background is so that we can see it scroll*/
+ background: repeating-linear-gradient(#EEE, #EEE 100px, #DDD 100px, #DDD 200px);
+ }
+ #page-content {
+ height: 5000px;
+ width: 500px;
+ }
+ </style>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1151667">Mozilla Bug 1151667</a>
+<p id="display"></p>
+<div id="subframe">
+ <!-- This makes sure the subframe is scrollable -->
+ <div id="subframe-content"></div>
+</div>
+<!-- This makes sure the page is also scrollable, so it (rather than the subframe)
+ is considered the primary async-scrollable frame, and so the subframe isn't
+ layerized upon page load. -->
+<div id="page-content"></div>
+<pre id="test">
+<script type="application/javascript">
+
+async function test() {
+ var subframe = document.getElementById("subframe");
+ await promiseNativeWheelAndWaitForScrollEvent(subframe, 100, 150, 0, -10);
+
+ is(subframe.scrollTop > 0, true, "We should have scrolled the subframe down");
+ is(document.documentElement.scrollTop, 0, "We should not have scrolled the page");
+}
+
+SimpleTest.waitForExplicitFinish();
+waitUntilApzStable()
+ .then(test)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_bug1253683.html b/gfx/layers/apz/test/mochitest/test_bug1253683.html
new file mode 100644
index 0000000000..f12455fb3a
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_bug1253683.html
@@ -0,0 +1,59 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1253683
+-->
+<head>
+ <title>Test to ensure non-scrollable frames don't get layerized</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+ <p id="display"></p>
+ <div id="container" style="height: 500px; overflow:scroll">
+ <pre id="no_layer" style="background-color: #f5f5f5; margin: 15px; padding: 15px; margin-top: 100px; border: 1px solid #eee; overflow:scroll">sample code here</pre>
+ <div style="height: 5000px">spacer to make the 'container' div the root scrollable element</div>
+ </div>
+<pre id="test">
+<script type="application/javascript">
+
+async function test() {
+ var container = document.getElementById("container");
+ var no_layer = document.getElementById("no_layer");
+
+ // Check initial state
+ is(container.scrollTop, 0, "Initial scrollY should be 0");
+ ok(!isLayerized("no_layer"), "initially 'no_layer' should not be layerized");
+
+ // Scrolling over outer1 should layerize outer1, but not inner1.
+ await promiseMoveMouseAndScrollWheelOver(no_layer, 10, 10, true);
+ await promiseAllPaintsDone();
+ await promiseOnlyApzControllerFlushed();
+
+ ok(container.scrollTop > 0, "We should have scrolled the body");
+ ok(!isLayerized("no_layer"), "no_layer should still not be layerized");
+}
+
+if (isApzEnabled()) {
+ SimpleTest.waitForExplicitFinish();
+
+ // Turn off displayport expiry so that we don't miss failures where the
+ // displayport is set and expired before we check for layerization.
+ // Also enable APZ test logging, since we use that data to determine whether
+ // a scroll frame was layerized.
+ pushPrefs([["apz.displayport_expiry_ms", 0],
+ ["apz.test.logging_enabled", true]])
+ .then(waitUntilApzStable)
+ .then(forceLayerTreeToCompositor)
+ .then(test)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_bug1277814.html b/gfx/layers/apz/test/mochitest/test_bug1277814.html
new file mode 100644
index 0000000000..28c4619e4e
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_bug1277814.html
@@ -0,0 +1,105 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1277814
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1277814</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+ async function test() {
+ // Trigger the buggy scenario
+ var subframe = document.getElementById("bug1277814-div");
+ subframe.classList.add("a");
+
+ // The transform change is animated, so let's step through 1s of animation
+ var utils = SpecialPowers.getDOMWindowUtils(window);
+ for (var i = 0; i < 60; i++) {
+ utils.advanceTimeAndRefresh(16);
+ }
+ utils.restoreNormalRefresh();
+
+ // Wait for the layer tree with any updated dispatch-to-content region to
+ // get pushed over to the APZ
+ await promiseAllPaintsDone();
+ await promiseOnlyApzControllerFlushed();
+
+ // Trigger layerization of the subframe by scrolling the wheel over it
+ await promiseMoveMouseAndScrollWheelOver(subframe, 10, 10);
+
+ // Give APZ the chance to compute a displayport, and content
+ // to render based on it.
+ await promiseApzFlushedRepaints();
+
+ // Examine the content-side APZ test data
+ var contentTestData = utils.getContentAPZTestData();
+
+ // Test that the scroll frame for the div 'bug1277814-div' appears in
+ // the APZ test data. The bug this test is for causes the displayport
+ // calculation for this scroll frame to go wrong, causing it not to
+ // become layerized.
+ contentTestData = convertTestData(contentTestData);
+ var foundIt = false;
+ for (var seqNo in contentTestData.paints) {
+ var paint = contentTestData.paints[seqNo];
+ for (var scrollId in paint) {
+ var scrollFrame = paint[scrollId];
+ if ("contentDescription" in scrollFrame &&
+ scrollFrame.contentDescription.includes("bug1277814-div")) {
+ foundIt = true;
+ }
+ }
+ }
+ SimpleTest.ok(foundIt, "expected to find APZ test data for bug1277814-div");
+ }
+
+ if (isApzEnabled()) {
+ SimpleTest.waitForExplicitFinish();
+
+ pushPrefs([["apz.test.logging_enabled", true]])
+ .then(waitUntilApzStable)
+ .then(test)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+ }
+ </script>
+ <style>
+ #bug1277814-div
+ {
+ position: absolute;
+ left: 0;
+ top: 0;
+ padding: .5em;
+ overflow: auto;
+ color: white;
+ background: green;
+ max-width: 30em;
+ max-height: 6em;
+ visibility: hidden;
+ transform: scaleY(0);
+ transition: transform .15s ease-out, visibility 0s ease .15s;
+ }
+ #bug1277814-div.a
+ {
+ visibility: visible;
+ transform: scaleY(1);
+ transition: transform .15s ease-out;
+ }
+ </style>
+</head>
+<body>
+ <!-- Use a unique id because we'll be checking for it in the content
+ description logged in the APZ test data -->
+ <div id="bug1277814-div">
+ CoolCmd<br>CoolCmd<br>CoolCmd<br>CoolCmd<br>
+ CoolCmd<br>CoolCmd<br>CoolCmd<br>CoolCmd<br>
+ CoolCmd<br>CoolCmd<br>CoolCmd<br>CoolCmd<br>
+ CoolCmd<br>CoolCmd<br>CoolCmd<br>CoolCmd<br>
+ CoolCmd<br>CoolCmd<br>CoolCmd<br>CoolCmd<br>
+ <button>click me</button>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_bug1304689-2.html b/gfx/layers/apz/test/mochitest/test_bug1304689-2.html
new file mode 100644
index 0000000000..1c7b1255d9
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_bug1304689-2.html
@@ -0,0 +1,130 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1304689
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1285070</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <style type="text/css">
+ #outer {
+ height: 400px;
+ width: 415px;
+ overflow: scroll;
+ position: relative;
+ scroll-behavior: smooth;
+ }
+ #outer.contentBefore::before {
+ top: 0;
+ content: '';
+ display: block;
+ height: 2px;
+ position: absolute;
+ width: 100%;
+ z-index: 99;
+ }
+ </style>
+ <script type="application/javascript">
+
+async function test() {
+ var utils = SpecialPowers.DOMWindowUtils;
+ var elm = document.getElementById("outer");
+
+ // Set margins on the element, to ensure it is layerized
+ utils.setDisplayPortMarginsForElement(0, 0, 0, 0, elm, /* priority*/ 1);
+ await promiseAllPaintsDone();
+ await promiseOnlyApzControllerFlushed();
+
+ // Take control of the refresh driver
+ utils.advanceTimeAndRefresh(0);
+
+ // Start a smooth-scroll animation in the compositor and let it go a few
+ // frames, so that there is some "user scrolling" going on (per the comment
+ // in AsyncPanZoomController::NotifyLayersUpdated)
+ elm.scrollTop = 10;
+ utils.advanceTimeAndRefresh(16);
+ utils.advanceTimeAndRefresh(16);
+ utils.advanceTimeAndRefresh(16);
+ utils.advanceTimeAndRefresh(16);
+
+ // Do another scroll update but also do a frame reconstruction within the same
+ // tick of the refresh driver.
+ elm.scrollTop = 100;
+ elm.classList.add("contentBefore");
+
+ // Now let everything settle and all the animations run out
+ for (var i = 0; i < 60; i++) {
+ utils.advanceTimeAndRefresh(16);
+ }
+ utils.restoreNormalRefresh();
+
+ await promiseOnlyApzControllerFlushed();
+ is(elm.scrollTop, 100, "The scrollTop now should be y=100");
+}
+
+if (isApzEnabled()) {
+ SimpleTest.waitForExplicitFinish();
+ pushPrefs([["apz.displayport_expiry_ms", 0]])
+ .then(waitUntilApzStable)
+ .then(test)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+}
+
+ </script>
+</head>
+<body>
+ <div id="outer">
+ <div id="inner">
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ </div>
+ </div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_bug1304689.html b/gfx/layers/apz/test/mochitest/test_bug1304689.html
new file mode 100644
index 0000000000..85ca3d5503
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_bug1304689.html
@@ -0,0 +1,134 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1304689
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1285070</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <style type="text/css">
+ #outer {
+ height: 400px;
+ width: 415px;
+ overflow: scroll;
+ position: relative;
+ scroll-behavior: smooth;
+ }
+ #outer.instant {
+ scroll-behavior: auto;
+ }
+ #outer.contentBefore::before {
+ top: 0;
+ content: '';
+ display: block;
+ height: 2px;
+ position: absolute;
+ width: 100%;
+ z-index: 99;
+ }
+ </style>
+ <script type="application/javascript">
+
+async function test() {
+ var utils = SpecialPowers.DOMWindowUtils;
+ var elm = document.getElementById("outer");
+
+ // Set margins on the element, to ensure it is layerized
+ utils.setDisplayPortMarginsForElement(0, 0, 0, 0, elm, /* priority*/ 1);
+ await promiseAllPaintsDone();
+ await promiseOnlyApzControllerFlushed();
+
+ // Take control of the refresh driver
+ utils.advanceTimeAndRefresh(0);
+
+ // Start a smooth-scroll animation in the compositor and let it go a few
+ // frames, so that there is some "user scrolling" going on (per the comment
+ // in AsyncPanZoomController::NotifyLayersUpdated)
+ elm.scrollTop = 10;
+ utils.advanceTimeAndRefresh(16);
+ utils.advanceTimeAndRefresh(16);
+ utils.advanceTimeAndRefresh(16);
+ utils.advanceTimeAndRefresh(16);
+
+ // Do another scroll update but also do a frame reconstruction within the same
+ // tick of the refresh driver.
+ elm.classList.add("instant");
+ elm.scrollTop = 100;
+ elm.classList.add("contentBefore");
+
+ // Now let everything settle and all the animations run out
+ for (var i = 0; i < 60; i++) {
+ utils.advanceTimeAndRefresh(16);
+ }
+ utils.restoreNormalRefresh();
+
+ await promiseOnlyApzControllerFlushed();
+ is(elm.scrollTop, 100, "The scrollTop now should be y=100");
+}
+
+if (isApzEnabled()) {
+ SimpleTest.waitForExplicitFinish();
+ pushPrefs([["apz.displayport_expiry_ms", 0]])
+ .then(waitUntilApzStable)
+ .then(test)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+}
+
+ </script>
+</head>
+<body>
+ <div id="outer">
+ <div id="inner">
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ </div>
+ </div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_frame_reconstruction.html b/gfx/layers/apz/test/mochitest/test_frame_reconstruction.html
new file mode 100644
index 0000000000..1031701a3b
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_frame_reconstruction.html
@@ -0,0 +1,218 @@
+<!DOCTYPE html>
+<html>
+ <!--
+ https://bugzilla.mozilla.org/show_bug.cgi?id=1235899
+ -->
+ <head>
+ <title>Test for bug 1235899</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <style>
+ .outer {
+ height: 400px;
+ width: 415px;
+ overflow: hidden;
+ position: relative;
+ }
+ .inner {
+ height: 100%;
+ outline: none;
+ overflow-x: hidden;
+ overflow-y: scroll;
+ position: relative;
+ scroll-behavior: smooth;
+ }
+ .outer.contentBefore::before {
+ top: 0;
+ content: '';
+ display: block;
+ height: 2px;
+ position: absolute;
+ width: 100%;
+ z-index: 99;
+ }
+ </style>
+ </head>
+ <body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1235899">Mozilla Bug 1235899</a>
+<p id="display"></p>
+<div id="content">
+ <p>You should be able to fling this list without it stopping abruptly</p>
+ <div class="outer">
+ <div class="inner">
+ <ol>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ </ol>
+ </div>
+ </div>
+</div>
+
+<pre id="test">
+<script type="application/javascript">
+async function test() {
+ var elm = document.getElementsByClassName("inner")[0];
+ elm.scrollTop = 0;
+ await promiseOnlyApzControllerFlushed();
+
+ // Take over control of the refresh driver and compositor
+ var utils = SpecialPowers.DOMWindowUtils;
+ utils.advanceTimeAndRefresh(0);
+
+ // Kick off an APZ smooth-scroll to 0,200
+ elm.scrollTo(0, 200);
+ await promiseAllPaintsDone();
+
+ // Let's do a couple of frames of the animation, and make sure it's going
+ utils.advanceTimeAndRefresh(16);
+ utils.advanceTimeAndRefresh(16);
+ await promiseOnlyApzControllerFlushed();
+ ok(elm.scrollTop > 0, "APZ animation in progress, scrollTop is now " + elm.scrollTop);
+ ok(elm.scrollTop < 200, "APZ animation not yet completed, scrollTop is now " + elm.scrollTop);
+
+ var frameReconstructionTriggered = 0;
+ // Register the listener that triggers the frame reconstruction
+ elm.onscroll = function() {
+ // Do the reconstruction
+ elm.parentNode.classList.add("contentBefore");
+ frameReconstructionTriggered++;
+ // schedule a thing to undo the changes above
+ setTimeout(function() {
+ elm.parentNode.classList.remove("contentBefore");
+ }, 0);
+ };
+
+ // and do a few more frames of the animation, this should trigger the listener
+ // and the frame reconstruction
+ utils.advanceTimeAndRefresh(16);
+ utils.advanceTimeAndRefresh(16);
+ await promiseOnlyApzControllerFlushed();
+ ok(elm.scrollTop < 200, "APZ animation not yet completed, scrollTop is now " + elm.scrollTop);
+ ok(frameReconstructionTriggered > 0, "Frame reconstruction triggered, reconstruction triggered " + frameReconstructionTriggered + " times");
+
+ // and now run to completion
+ for (var i = 0; i < 100; i++) {
+ utils.advanceTimeAndRefresh(16);
+ }
+ utils.restoreNormalRefresh();
+ await promiseAllPaintsDone();
+ await promiseOnlyApzControllerFlushed();
+
+ is(elm.scrollTop, 200, "Element should have scrolled by 200px");
+}
+
+if (isApzEnabled()) {
+ SimpleTest.waitForExplicitFinish();
+ SimpleTest.expectAssertions(0, 1); // this test triggers an assertion, see bug 1247050
+ waitUntilApzStable()
+ .then(test)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+}
+
+</script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_group_bug1534549.html b/gfx/layers/apz/test/mochitest/test_group_bug1534549.html
new file mode 100644
index 0000000000..6ab8afa6f7
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_bug1534549.html
@@ -0,0 +1,37 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Tests for bug 1534549</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="application/javascript">
+ var touch_action_prefs = getPrefs("TOUCH_ACTION");
+
+ var subtests = [
+ // Tests that z-index ordering is respected by hit-test info.
+ { "file": "helper_touch_action_ordering_zindex.html", "prefs": touch_action_prefs },
+ // Tests that complex block/inline background ordering is respected by hit-test info.
+ { "file": "helper_touch_action_ordering_block.html", "prefs": touch_action_prefs },
+ ];
+
+ if (isApzEnabled()) {
+ ok(window.TouchEvent, "Check if TouchEvent is supported (it should be, the test harness forces it on everywhere)");
+ if (getPlatform() == "android") {
+ // This has a lot of subtests, and Android emulators are slow.
+ SimpleTest.requestLongerTimeout(2);
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ window.onload = function() {
+ runSubtestsSeriallyInFreshWindows(subtests)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+ };
+ }
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_group_checkerboarding.html b/gfx/layers/apz/test/mochitest/test_group_checkerboarding.html
new file mode 100644
index 0000000000..5386ffd740
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_checkerboarding.html
@@ -0,0 +1,83 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+ if (isApzEnabled()) {
+ SimpleTest.waitForExplicitFinish();
+
+
+ var prefs = [
+ ["apz.test.logging_enabled", true],
+ ["apz.displayport_expiry_ms", 0],
+ ["general.smoothScroll", false],
+ // Avoid extra paints from scrollbar fading out
+ ["layout.testing.overlay-scrollbars.always-visible", true],
+ ];
+
+ var px_ratio_1_prefs = [
+ ...prefs,
+ ["layout.css.devPixelsPerPx", 1.0],
+ ];
+
+ var zoom_and_pan_prefs = [
+ ...prefs,
+ ...getPrefs("TOUCH_EVENTS:PAN"),
+ ];
+
+ var no_multiplier_prefs = [
+ ...zoom_and_pan_prefs,
+ ["apz.x_skate_size_multiplier", "0.0"],
+ ["apz.y_skate_size_multiplier", "0.0"],
+ ["apz.x_stationary_size_multiplier", "0.0"],
+ ["apz.y_stationary_size_multiplier", "0.0"],
+ ];
+
+ var subtests = [
+ { file: "helper_checkerboard_apzforcedisabled.html", prefs },
+ { file: "helper_checkerboard_scrollinfo.html", prefs },
+ { file: "helper_horizontal_checkerboard.html", "prefs": px_ratio_1_prefs },
+ { file: "helper_checkerboard_no_multiplier.html", "prefs": no_multiplier_prefs },
+ { file: "helper_checkerboard_zoom_during_load.html", "prefs": no_multiplier_prefs },
+ { file: "helper_wide_crossorigin_iframe.html", prefs },
+ { file: "helper_reset_zoom_bug1818967.html", prefs },
+ ];
+
+ let platform = getPlatform();
+ if (platform != "windows") {
+ subtests.push(
+ { file: "helper_checkerboard_zoomoverflowhidden.html", "prefs": zoom_and_pan_prefs }
+ );
+ }
+
+ var scrollbarbutton_prefs = [
+ ...prefs,
+ ["general.smoothScroll", true],
+ // bug 1682919 only affects the main thread scrollbar button repeat codepath
+ ["apz.scrollbarbuttonrepeat.enabled", false]
+ ];
+
+ // Only Windows has scrollbar buttons in automation.
+ if (platform == "windows") {
+ subtests.push(
+ { file: "helper_scrollbarbuttonclick_checkerboard.html", "prefs": scrollbarbutton_prefs},
+ );
+ }
+
+ // Run the actual test in its own window, because it requires that the
+ // root APZC be scrollable. Mochitest pages themselves often run
+ // inside an iframe which means we have no control over the root APZC.
+ window.onload = () => {
+ runSubtestsSeriallyInFreshWindows(subtests)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+ };
+ }
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_group_displayport.html b/gfx/layers/apz/test/mochitest/test_group_displayport.html
new file mode 100644
index 0000000000..9ff8c524ad
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_displayport.html
@@ -0,0 +1,31 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Tests for DisplayPorts</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="application/javascript">
+
+ // Set the displayport expiry timeout to a small value, but in order to
+ // trigger the behavior seen in bug 1758760 this value must be greater
+ // than zero.
+ let displayportExpiryPrefs = [["apz.displayport_expiry_ms", 5]];
+ let subtests = [
+ { "file": "helper_displayport_expiry.html", "prefs": displayportExpiryPrefs },
+ ];
+
+ if (isApzEnabled()) {
+ SimpleTest.waitForExplicitFinish();
+ window.onload = function() {
+ runSubtestsSeriallyInFreshWindows(subtests)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+ };
+ }
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_group_double_tap_zoom-2.html b/gfx/layers/apz/test/mochitest/test_group_double_tap_zoom-2.html
new file mode 100644
index 0000000000..3fb4f9163c
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_double_tap_zoom-2.html
@@ -0,0 +1,89 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Various zoom-related tests that spawn in new windows</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+// Increase the tap timeouts so the double-tap is still detected in case of
+// random delays during testing.
+var doubletap_prefs = [
+ ["ui.click_hold_context_menus.delay", 10000],
+ ["apz.max_tap_time", 10000],
+];
+
+var longeranimation_doubletap_prefs = [
+ ...doubletap_prefs,
+ ["apz.zoom_animation_duration_ms", 1000],
+];
+
+var logging_and_doubletap_prefs = [
+ ...doubletap_prefs,
+ ["apz.test.logging_enabled", true],
+];
+
+var disable_default_zoomin_and_doubletap_prefs = [
+ ...doubletap_prefs,
+ ["apz.doubletapzoom.defaultzoomin", "1.0"],
+];
+
+var meta_viewport_and_doubletap_prefs = [
+ ...doubletap_prefs,
+ ["dom.meta-viewport.enabled", true],
+];
+
+var subtests = [
+ {"file": "helper_doubletap_zoom_smooth.html", "prefs": longeranimation_doubletap_prefs},
+ {"file": "helper_doubletap_zoom_fixedpos_overflow.html", "prefs": logging_and_doubletap_prefs},
+ {"file": "helper_doubletap_zoom_hscrollable.html", "prefs": disable_default_zoomin_and_doubletap_prefs},
+ {"file": "helper_doubletap_zoom_scrolled_overflowhidden.html", "prefs": doubletap_prefs},
+ {"file": "helper_doubletap_zoom_shadowdom.html", "prefs": doubletap_prefs},
+ {"file": "helper_doubletap_zoom_tablecell.html", "prefs": disable_default_zoomin_and_doubletap_prefs},
+ {"file": "helper_doubletap_zoom_gencon.html", "prefs": doubletap_prefs},
+ {"file": "helper_doubletap_zoom_hscrollable2.html", "prefs": doubletap_prefs},
+ {"file": "helper_doubletap_zoom_nothing.html", "prefs": doubletap_prefs},
+ {"file": "helper_doubletap_zoom_noscroll.html", "prefs": doubletap_prefs},
+ {"file": "helper_doubletap_zoom_square.html", "prefs": doubletap_prefs},
+ {"file": "helper_doubletap_zoom_oopif.html", "prefs": doubletap_prefs},
+ {"file": "helper_disallow_doubletap_zoom_inside_oopif.html", "prefs": meta_viewport_and_doubletap_prefs},
+ {"file": "helper_doubletap_zoom_nothing_listener.html", "prefs": doubletap_prefs},
+];
+
+if (getPlatform() == "mac") {
+ subtests.push(
+ {"file": "helper_doubletap_zoom_smooth.html?touchpad", "prefs": longeranimation_doubletap_prefs},
+ {"file": "helper_doubletap_zoom_fixedpos_overflow.html?touchpad", "prefs": logging_and_doubletap_prefs},
+ {"file": "helper_doubletap_zoom_hscrollable.html?touchpad", "prefs": disable_default_zoomin_and_doubletap_prefs},
+ {"file": "helper_doubletap_zoom_scrolled_overflowhidden.html?touchpad", "prefs": doubletap_prefs},
+ {"file": "helper_doubletap_zoom_shadowdom.html?touchpad", "prefs": doubletap_prefs},
+ {"file": "helper_doubletap_zoom_tablecell.html?touchpad", "prefs": disable_default_zoomin_and_doubletap_prefs},
+ {"file": "helper_doubletap_zoom_gencon.html?touchpad", "prefs": doubletap_prefs},
+ {"file": "helper_doubletap_zoom_hscrollable2.html?touchpad", "prefs": doubletap_prefs},
+ {"file": "helper_doubletap_zoom_nothing.html?touchpad", "prefs": doubletap_prefs},
+ {"file": "helper_doubletap_zoom_noscroll.html?touchpad", "prefs": doubletap_prefs},
+ {"file": "helper_doubletap_zoom_square.html?touchpad", "prefs": doubletap_prefs},
+ {"file": "helper_doubletap_zoom_oopif.html?touchpad", "prefs": doubletap_prefs},
+ {"file": "helper_disallow_doubletap_zoom_inside_oopif.html?touchpad", "prefs": meta_viewport_and_doubletap_prefs},
+ {"file": "helper_doubletap_zoom_nothing_listener.html?touchpad", "prefs": doubletap_prefs},
+ );
+}
+
+if (isApzEnabled()) {
+ // This has a lot of subtests, and Android emulators are slow.
+ SimpleTest.requestLongerTimeout(2);
+ SimpleTest.waitForExplicitFinish();
+ window.onload = function() {
+ runSubtestsSeriallyInFreshWindows(subtests)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+ };
+}
+
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_group_double_tap_zoom.html b/gfx/layers/apz/test/mochitest/test_group_double_tap_zoom.html
new file mode 100644
index 0000000000..fe4a0784a9
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_double_tap_zoom.html
@@ -0,0 +1,66 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Various zoom-related tests that spawn in new windows</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+// Increase the tap timeouts so the double-tap is still detected in case of
+// random delays during testing.
+var doubletap_prefs = [
+ ["ui.click_hold_context_menus.delay", 10000],
+ ["apz.max_tap_time", 10000],
+];
+
+var logging_and_doubletap_prefs = [
+ ...doubletap_prefs,
+ ["apz.test.logging_enabled", true],
+];
+
+var subtests = [
+ {"file": "helper_doubletap_zoom.html", "prefs": doubletap_prefs},
+ {"file": "helper_doubletap_zoom_img.html", "prefs": doubletap_prefs},
+ {"file": "helper_doubletap_zoom_textarea.html", "prefs": doubletap_prefs},
+ {"file": "helper_doubletap_zoom_horizontal_center.html", "prefs": doubletap_prefs},
+ {"file": "helper_doubletap_zoom_bug1702464.html", "prefs": doubletap_prefs},
+ {"file": "helper_doubletap_zoom_large_overflow.html", "prefs": doubletap_prefs},
+ {"file": "helper_doubletap_zoom_fixedpos.html", "prefs": logging_and_doubletap_prefs},
+ {"file": "helper_doubletap_zoom_tallwide.html", "prefs": doubletap_prefs},
+];
+
+if (getPlatform() == "mac") {
+ subtests.push(
+ {"file": "helper_doubletap_zoom.html?touchpad", "prefs": doubletap_prefs},
+ {"file": "helper_doubletap_zoom_img.html?touchpad", "prefs": doubletap_prefs},
+ {"file": "helper_doubletap_zoom_textarea.html?touchpad", "prefs": doubletap_prefs},
+ {"file": "helper_doubletap_zoom_horizontal_center.html?touchpad", "prefs": doubletap_prefs},
+ {"file": "helper_doubletap_zoom_small.html", "prefs": doubletap_prefs},
+ {"file": "helper_doubletap_zoom_small.html?touchpad", "prefs": doubletap_prefs},
+ {"file": "helper_doubletap_zoom_bug1702464.html?touchpad", "prefs": doubletap_prefs},
+ {"file": "helper_doubletap_zoom_htmlelement.html", "prefs": doubletap_prefs}, // scrollbars don't receive events or take space on android
+ {"file": "helper_doubletap_zoom_htmlelement.html?touchpad", "prefs": doubletap_prefs},
+ {"file": "helper_doubletap_zoom_large_overflow.html?touchpad", "prefs": doubletap_prefs},
+ {"file": "helper_doubletap_zoom_fixedpos.html?touchpad", "prefs": logging_and_doubletap_prefs},
+ {"file": "helper_doubletap_zoom_tallwide.html?touchpad", "prefs": doubletap_prefs},
+ );
+}
+
+if (isApzEnabled()) {
+ // This has a lot of subtests, and Android emulators are slow.
+ SimpleTest.requestLongerTimeout(2);
+ SimpleTest.waitForExplicitFinish();
+ window.onload = function() {
+ runSubtestsSeriallyInFreshWindows(subtests)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+ };
+}
+
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_group_fullscreen.html b/gfx/layers/apz/test/mochitest/test_group_fullscreen.html
new file mode 100644
index 0000000000..c31a3abffb
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_fullscreen.html
@@ -0,0 +1,32 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+ if (isApzEnabled()) {
+ SimpleTest.waitForExplicitFinish();
+
+ const subtests = [
+ { file: "helper_fullscreen.html",
+ prefs: [
+ ["apz.test.logging_enabled", true],
+ ["full-screen-api.allow-trusted-requests-only", false],
+ ],
+ },
+ ];
+ // Run the actual test in its own window, because it requires that the
+ // root APZC be scrollable. Mochitest pages themselves often run
+ // inside an iframe which means we have no control over the root APZC.
+ window.onload = () => {
+ runSubtestsSeriallyInFreshWindows(subtests)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+ };
+ }
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_group_hittest-1.html b/gfx/layers/apz/test/mochitest/test_group_hittest-1.html
new file mode 100644
index 0000000000..34bf245d6c
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_hittest-1.html
@@ -0,0 +1,59 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Various hit-testing tests that spawn in new windows - Part 1</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+var prefs = [
+ // Turn off displayport expiry so that we don't miss failures where the
+ // displayport is set and then expires before we get around to doing the
+ // hit-test inside the activated scrollframe.
+ ["apz.displayport_expiry_ms", 0],
+ // Always layerize the scrollbar track, so as to get consistent results
+ // across platforms. Eventually we should probably get rid of this and make
+ // the tests more robust in terms of testing all the different cross-platform
+ // variations.
+ ["layout.scrollbars.always-layerize-track", true],
+ // We need this pref to allow the synthetic mouse events to propagate to APZ,
+ // and to allow the MozMouseHittest event in particular to be dispatched to
+ // APZ as a MouseInput so the hit result is recorded.
+ ["test.events.async.enabled", true],
+ // Turns on APZTestData logging which we use to obtain the hit test results.
+ ["apz.test.logging_enabled", true],
+];
+
+var overscroll_prefs = [...prefs,
+ ["apz.overscroll.enabled", true],
+ ["apz.overscroll.test_async_scroll_offset.enabled", true],
+ ];
+
+var subtests = [
+ {"file": "helper_hittest_basic.html", "prefs": prefs},
+ {"file": "helper_hittest_fixed_in_scrolled_transform.html", "prefs": prefs},
+ {"file": "helper_hittest_float_bug1434846.html", "prefs": prefs},
+ {"file": "helper_hittest_float_bug1443518.html", "prefs": prefs},
+ {"file": "helper_hittest_checkerboard.html", "prefs": prefs},
+ {"file": "helper_hittest_backface_hidden.html", "prefs": prefs},
+ {"file": "helper_hittest_touchaction.html", "prefs": prefs},
+ {"file": "helper_hittest_nested_transforms_bug1459696.html", "prefs": prefs},
+ {"file": "helper_hittest_sticky_bug1478304.html", "prefs": prefs},
+ {"file": "helper_hittest_clipped_fixed_modal.html", "prefs": prefs},
+];
+
+if (isApzEnabled()) {
+ SimpleTest.waitForExplicitFinish();
+ window.onload = function() {
+ runSubtestsSeriallyInFreshWindows(subtests)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+ };
+}
+
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_group_hittest-2.html b/gfx/layers/apz/test/mochitest/test_group_hittest-2.html
new file mode 100644
index 0000000000..d144aab840
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_hittest-2.html
@@ -0,0 +1,72 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Various hit-testing tests that spawn in new windows - Part 2</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+var prefs = [
+ // Turn off displayport expiry so that we don't miss failures where the
+ // displayport is set and then expires before we get around to doing the
+ // hit-test inside the activated scrollframe.
+ ["apz.displayport_expiry_ms", 0],
+ // Always layerize the scrollbar track, so as to get consistent results
+ // across platforms. Eventually we should probably get rid of this and make
+ // the tests more robust in terms of testing all the different cross-platform
+ // variations.
+ ["layout.scrollbars.always-layerize-track", true],
+ // We need this pref to allow the synthetic mouse events to propagate to APZ,
+ // and to allow the MozMouseHittest event in particular to be dispatched to
+ // APZ as a MouseInput so the hit result is recorded.
+ ["test.events.async.enabled", true],
+ // Turns on APZTestData logging which we use to obtain the hit test results.
+ ["apz.test.logging_enabled", true],
+ // Prefs to ensure we can produce a precise amount of scrolling via
+ // synthesized touch-drag gestures.
+ ["apz.touch_start_tolerance", "0.0"],
+ ["apz.fling_min_velocity_threshold", "10000"],
+];
+
+var subtests = [
+ {"file": "helper_hittest_deep_scene_stack.html", "prefs": prefs},
+ {"file": "helper_hittest_pointerevents_svg.html", "prefs": prefs},
+ {"file": "helper_hittest_clippath.html", "prefs": prefs},
+ {"file": "helper_hittest_hoisted_scrollinfo.html", "prefs": prefs},
+ {"file": "helper_hittest_hidden_inactive_scrollframe.html", "prefs": prefs},
+ {"file": "helper_hittest_bug1715187.html", "prefs": prefs},
+ {"file": "helper_hittest_bug1715369.html", "prefs": prefs},
+ {"file": "helper_hittest_fixed.html", "prefs": prefs},
+ {"file": "helper_hittest_fixed-2.html", "prefs": prefs},
+ {"file": "helper_hittest_fixed-3.html", "prefs": prefs},
+ {"file": "helper_hittest_fixed_bg.html", "prefs": prefs},
+ {"file": "helper_hittest_fixed_item_over_oop_iframe.html", "prefs": prefs},
+ {"file": "helper_hittest_bug1730606-1.html", "prefs": prefs},
+ {"file": "helper_hittest_bug1730606-2.html", "prefs": prefs},
+ {"file": "helper_hittest_bug1730606-3.html", "prefs": prefs},
+ {"file": "helper_hittest_bug1730606-4.html", "prefs": prefs},
+ {"file": "helper_hittest_bug1257288.html", "prefs": prefs},
+ {"file": "helper_hittest_bug1119497.html", "prefs": prefs},
+ {"file": "helper_hittest_obscuration.html", "prefs": prefs},
+ // Note: iframe_perspective.html and iframe_perspective-3.html
+ // have been moved to test_group_hittest-3 due to bug 1829021.
+ {"file": "helper_hittest_iframe_perspective-2.html", "prefs": prefs},
+ // This test should be at the end, because it's prone to timeout.
+ {"file": "helper_hittest_spam.html", "prefs": prefs},
+];
+
+if (isApzEnabled()) {
+ SimpleTest.waitForExplicitFinish();
+ window.onload = function() {
+ runSubtestsSeriallyInFreshWindows(subtests)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+ };
+}
+
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_group_hittest-3.html b/gfx/layers/apz/test/mochitest/test_group_hittest-3.html
new file mode 100644
index 0000000000..f5675ee790
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_hittest-3.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Various hit-testing tests that spawn in new windows - Part 3</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+var prefs = [
+ // Turn off displayport expiry so that we don't miss failures where the
+ // displayport is set and then expires before we get around to doing the
+ // hit-test inside the activated scrollframe.
+ ["apz.displayport_expiry_ms", 0],
+ // Always layerize the scrollbar track, so as to get consistent results
+ // across platforms. Eventually we should probably get rid of this and make
+ // the tests more robust in terms of testing all the different cross-platform
+ // variations.
+ ["layout.scrollbars.always-layerize-track", true],
+ // We need this pref to allow the synthetic mouse events to propagate to APZ,
+ // and to allow the MozMouseHittest event in particular to be dispatched to
+ // APZ as a MouseInput so the hit result is recorded.
+ ["test.events.async.enabled", true],
+ // Turns on APZTestData logging which we use to obtain the hit test results.
+ ["apz.test.logging_enabled", true],
+ // Prefs to ensure we can produce a precise amount of scrolling via
+ // synthesized touch-drag gestures.
+ ["apz.touch_start_tolerance", "0.0"],
+ ["apz.fling_min_velocity_threshold", "10000"],
+];
+
+var subtests = [
+ {"file": "helper_hittest_iframe_perspective.html", "prefs": prefs},
+ {"file": "helper_hittest_iframe_perspective-3.html", "prefs": prefs},
+];
+
+if (isApzEnabled()) {
+ SimpleTest.waitForExplicitFinish();
+ window.onload = function() {
+ runSubtestsSeriallyInFreshWindows(subtests)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+ };
+}
+
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_group_hittest-overscroll.html b/gfx/layers/apz/test/mochitest/test_group_hittest-overscroll.html
new file mode 100644
index 0000000000..ee40c5dcdb
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_hittest-overscroll.html
@@ -0,0 +1,54 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Various hit-testing tests relevant with overscroll that spawn in new windows</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+var prefs = [
+ // Turn off displayport expiry so that we don't miss failures where the
+ // displayport is set and then expires before we get around to doing the
+ // hit-test inside the activated scrollframe.
+ ["apz.displayport_expiry_ms", 0],
+ // Always layerize the scrollbar track, so as to get consistent results
+ // across platforms. Eventually we should probably get rid of this and make
+ // the tests more robust in terms of testing all the different cross-platform
+ // variations.
+ ["layout.scrollbars.always-layerize-track", true],
+ // We need this pref to allow the synthetic mouse events to propagate to APZ,
+ // and to allow the MozMouseHittest event in particular to be dispatched to
+ // APZ as a MouseInput so the hit result is recorded.
+ ["test.events.async.enabled", true],
+ // Turns on APZTestData logging which we use to obtain the hit test results.
+ ["apz.test.logging_enabled", true],
+ // Prefs to ensure we can produce a precise amount of scrolling via
+ // synthesized touch-drag gestures.
+ ["apz.touch_start_tolerance", "0.0"],
+ ["apz.fling_min_velocity_threshold", "10000"],
+ // Overscroll relevant prefs.
+ ["apz.overscroll.enabled", true],
+ ["apz.overscroll.test_async_scroll_offset.enabled", true],
+];
+
+var subtests = [
+ {"file": "helper_hittest_overscroll.html", "prefs": prefs},
+ {"file": "helper_hittest_overscroll_subframe.html", "prefs": prefs},
+ {"file": "helper_hittest_overscroll_contextmenu.html", "prefs": prefs},
+];
+
+if (isApzEnabled()) {
+ SimpleTest.waitForExplicitFinish();
+ window.onload = function() {
+ runSubtestsSeriallyInFreshWindows(subtests)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+ };
+}
+
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_group_keyboard-2.html b/gfx/layers/apz/test/mochitest/test_group_keyboard-2.html
new file mode 100644
index 0000000000..c07607e1dc
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_keyboard-2.html
@@ -0,0 +1,46 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Various keyboard scrolling tests</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+var bug1756529_prefs= [
+ // Increase distance of line scroll (units here are "number of lines") so
+ // that a smooth scroll animation reliably takes multiple frames.
+ ["toolkit.scrollbox.verticalScrollDistance", 5],
+ // The test performs a single-tap gesture between test cases to cancel the
+ // animation from the previous test case. For test cases that run fast (e.g.
+ // instant scrolling), two single-taps could occur close enough in succession
+ // that they get interpreted as a double-tap.
+ ["apz.allow_double_tap_zooming", false]
+];
+
+var subtests = [
+ // Run helper_bug1756529.html twice, first exercising the main-thread keyboard
+ // scrolling codepaths (e.g. PresShell::ScrollPage()), and once exercising the
+ // APZ keyboard scrolling codepaths.
+ {"file": "helper_bug1756529.html", prefs: bug1756529_prefs},
+ {"file": "helper_bug1756529.html", prefs: [...bug1756529_prefs,
+ ["test.events.async.enabled", true]]},
+];
+
+if (isKeyApzEnabled()) {
+ SimpleTest.waitForExplicitFinish();
+ window.onload = function() {
+ runSubtestsSeriallyInFreshWindows(subtests)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+ };
+} else {
+ SimpleTest.ok(true, "Keyboard APZ is disabled");
+}
+ </script>
+</head>
+<body>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1383365">Async key scrolling test</a>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_group_keyboard.html b/gfx/layers/apz/test/mochitest/test_group_keyboard.html
new file mode 100644
index 0000000000..4f1bc0d869
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_keyboard.html
@@ -0,0 +1,60 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Various keyboard scrolling tests</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+var smoothness_prefs = [
+ ["apz.test.logging_enabled", true],
+ // The default verticalScrollDistance (which is 3) is too small for native
+ // keyboard scrolling, it sometimes produces same scroll offsets in the early
+ // stages of the smooth animation.
+ ["toolkit.scrollbox.verticalScrollDistance", 5],
+ // Use a longer animation duration to avoid the situation that the
+ // animation stops accidentally in between each arrow down key press.
+ // If the situation happens, scroll offsets will not change at the moment.
+ ["general.smoothScroll.lines.durationMaxMS", 1500],
+ ["general.smoothScroll.lines.durationMinMS", 1500]
+];
+
+var subtests = [
+ {"file": "helper_key_scroll.html", prefs: [["apz.test.logging_enabled", true],
+ ["test.events.async.enabled", true]]},
+ {"file": "helper_bug1674935.html", prefs: []},
+ {"file": "helper_bug1719330.html", prefs: [["general.smoothScroll", false]]},
+ {"file": "helper_relative_scroll_smoothness.html?input-type=key&scroll-method=scrollBy",
+ prefs: smoothness_prefs },
+ {"file": "helper_relative_scroll_smoothness.html?input-type=native-key&scroll-method=scrollBy",
+ prefs: smoothness_prefs },
+ {"file": "helper_relative_scroll_smoothness.html?input-type=native-key&scroll-method=scrollTo",
+ prefs: smoothness_prefs },
+ {"file": "helper_relative_scroll_smoothness.html?input-type=native-key&scroll-method=scrollTop",
+ prefs: smoothness_prefs },
+ {"file": "helper_bug1695598.html"},
+ {"file": "helper_scroll_snap_on_page_down_scroll.html"},
+ {"file": "helper_scroll_snap_on_page_down_scroll.html",
+ prefs: [["test.events.async.enabled", true]]},
+ {"file": "helper_transform_end_on_keyboard_scroll.html",
+ prefs: [["general.smoothScroll", false]] },
+];
+
+if (isKeyApzEnabled()) {
+ SimpleTest.waitForExplicitFinish();
+ window.onload = function() {
+ runSubtestsSeriallyInFreshWindows(subtests)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+ };
+} else {
+ SimpleTest.ok(true, "Keyboard APZ is disabled");
+}
+ </script>
+</head>
+<body>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1383365">Async key scrolling test</a>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_group_mainthread.html b/gfx/layers/apz/test/mochitest/test_group_mainthread.html
new file mode 100644
index 0000000000..3e1225cadf
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_mainthread.html
@@ -0,0 +1,51 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Tests that perform main-thread scrolling</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+var subtests = [
+ {"file": "helper_scrollby_bug1531796.html"},
+ // This test checks that scroll anchoring isn't invoked just after an async
+ // scroll operation (scrolIntoView) was triggered, in other words, the async
+ // operation happened on the main-thread but it hasn't reached to APZ yet, so
+ // we don't need to set layout.css.scroll-behavior.spring-constant explicitly
+ // since the async scroll animation doesn't need to keep running there.
+ {"file": "helper_scroll_anchoring_smooth_scroll.html"},
+ {"file": "helper_scroll_anchoring_smooth_scroll_with_set_timeout.html", prefs: [
+ // Unlike helper_scroll_anchoring_smooth_scroll.html, this test checks that
+ // scroll anchoring __is__ invoked when an async scroll animation triggered
+ // by mouse wheel is running so the animation needs to keep running for a
+ // while.
+ ["layout.css.scroll-behavior.spring-constant", 10]
+ ]},
+ {"file": "helper_visualscroll_clamp_restore.html", prefs: [
+ ["apz.test.logging_enabled", true],
+ ]},
+ {"file": "helper_smoothscroll_spam.html"},
+ {"file": "helper_smoothscroll_spam_interleaved.html"},
+ {"file": "helper_mainthread_scroll_bug1662379.html", prefs: [
+ ["apz.test.logging_enabled", true],
+ ]},
+ {"file": "helper_bug1519339_hidden_smoothscroll.html", prefs: [
+ ["apz.scrollend-event.content.enabled", true]
+ ]},
+];
+
+if (isApzEnabled()) {
+ SimpleTest.waitForExplicitFinish();
+ window.onload = function() {
+ runSubtestsSeriallyInFreshWindows(subtests)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+ };
+}
+
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_group_minimum_scale_size.html b/gfx/layers/apz/test/mochitest/test_group_minimum_scale_size.html
new file mode 100644
index 0000000000..2de924d6bd
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_minimum_scale_size.html
@@ -0,0 +1,48 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<meta charset="utf-8">
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script type="application/javascript" src="apz_test_utils.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+<script type="application/javascript">
+const prefs = [
+ // We need the APZ paint logging information
+ ["apz.test.logging_enabled", true],
+ // Dropping the touch slop to 0 makes the tests easier to write because
+ // we can just do a one-pixel drag to get over the pan threshold rather
+ // than having to hard-code some larger value.
+ ["apz.touch_start_tolerance", "0.0"],
+ // The subtests in this test do touch-drags to pan the page, but we don't
+ // want those pans to turn into fling animations, so we increase the
+ // fling-min threshold velocity to an arbitrarily large value.
+ ["apz.fling_min_velocity_threshold", "10000"],
+ // The helper_bug1280013's div gets a displayport on scroll, but if the
+ // test takes too long the displayport can expire before we read the value
+ // out of the test. So we disable displayport expiry for these tests.
+ ["apz.displayport_expiry_ms", 0],
+ // Similarly, explicitly enable support for meta viewport tags (which the
+ // test cases use) so they're processed even on desktop.
+ ["dom.meta-viewport.enabled", true],
+];
+
+const subtests = [
+ { file: "helper_minimum_scale_1_0.html", prefs },
+ { file: "helper_no_scalable_with_initial_scale.html", prefs },
+];
+
+if (isApzEnabled()) {
+ SimpleTest.waitForExplicitFinish();
+ // Run the actual test in its own window, because it requires that the
+ // root APZC be scrollable. Mochitest pages themselves often run
+ // inside an iframe which means we have no control over the root APZC.
+ window.onload = () => {
+ runSubtestsSeriallyInFreshWindows(subtests)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+ };
+}
+</script>
+</head>
+<body>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_group_mouseevents.html b/gfx/layers/apz/test/mochitest/test_group_mouseevents.html
new file mode 100644
index 0000000000..c7c86b76b5
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_mouseevents.html
@@ -0,0 +1,82 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Various mouse tests that spawn in new windows</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+var subtests = [
+ // Sanity test to synthesize a mouse click
+ {"file": "helper_click.html?dtc=false"},
+ // Same as above, but with a dispatch-to-content region that exercises the
+ // main-thread notification codepaths for mouse events
+ {"file": "helper_click.html?dtc=true"},
+ // Sanity test for click but with some mouse movement between the down and up
+ {"file": "helper_drag_click.html"},
+ // Test for dragging on the scrollbar of the root scrollable element works.
+ // This takes different codepaths with async zooming support enabled and
+ // disabled, and so needs to be tested separately for both.
+ {"file": "helper_drag_root_scrollbar.html", "prefs": [["apz.allow_zooming", false]]},
+ {"file": "helper_drag_root_scrollbar.html", "prefs": [["apz.allow_zooming", true]]},
+ {"file": "helper_drag_scrollbar_hittest.html", "prefs": [
+ // The test applies a scaling zoom to the page.
+ ["apz.allow_zooming", true],
+ // The test uses hitTest(), these two prefs are required for it.
+ ["test.events.async.enabled", true],
+ ["apz.test.logging_enabled", true],
+ // The test performs scrollbar dragging, avoid auto-hiding scrollbars.
+ ["ui.useOverlayScrollbars", false]
+ ]},
+ // Test for dragging on a fake-scrollbar element that scrolls the page
+ {"file": "helper_drag_scroll.html"},
+ // Test for dragging the scrollbar with a fixed-pos element overlaying it
+ {"file": "helper_bug1346632.html"},
+ // Test for scrollbar-dragging on a scrollframe that's inactive
+ {"file": "helper_bug1326290.html"},
+ // Test for scrollbar-dragging on a scrollframe inside an SVGEffects
+ {"file": "helper_bug1331693.html"},
+ // Test for scrollbar-dragging on a transformed scrollframe inside a fixed-pos item
+ {"file": "helper_bug1462961.html"},
+ // Scrollbar dragging where we exercise the snapback behaviour by moving the
+ // mouse away from the scrollbar during drag
+ {"file": "helper_scrollbar_snap_bug1501062.html"},
+ // Tests for scrollbar-dragging on scrollframes inside nested transforms
+ {"file": "helper_bug1490393.html"},
+ {"file": "helper_bug1490393-2.html"},
+ // Scrollbar-dragging on scrollframes inside filters inside transforms
+ {"file": "helper_bug1550510.html"},
+ // Drag-select some text after reconstructing the RSF of a non-RCD to ensure
+ // the pending visual offset update doesn't get stuck
+ {"file": "helper_visualscroll_nonrcd_rsf.html"},
+ // Scrollbar-dragging on scrollframes inside nested transforms with scale
+ {"file": "helper_bug1662800.html"},
+ // Scrollbar-dragging on subframe with enclosing translation transform
+ {"file": "helper_drag_bug1719913.html"},
+];
+
+// Android, mac, and linux (at least in our automation) do not have scrollbar buttons.
+if (getPlatform() == "windows") {
+ subtests.push(
+ // Basic test that click and hold on a scrollbar button works as expected
+ { file: "helper_scrollbarbutton_repeat.html", "prefs": [["general.smoothScroll", false]] }
+ );
+}
+
+if (isApzEnabled()) {
+ SimpleTest.waitForExplicitFinish();
+ SimpleTest.expectAssertions(0, 2); // from helper_bug1550510.html, bug 1232856
+ window.onload = function() {
+ runSubtestsSeriallyInFreshWindows(subtests)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+ };
+}
+
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_group_overrides.html b/gfx/layers/apz/test/mochitest/test_group_overrides.html
new file mode 100644
index 0000000000..434c32d24e
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_overrides.html
@@ -0,0 +1,37 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Various tests for event regions overrides</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+var prefs = [
+ // turn off smooth scrolling so that we don't have to wait for
+ // APZ animations to finish before sampling the scroll offset
+ ["general.smoothScroll", false],
+ // Increase the content response timeout because these tests do preventDefault
+ // and we want to make sure APZ actually waits for them.
+ ["apz.content_response_timeout", 10000],
+];
+
+var subtests = [
+ {"file": "helper_override_root.html", "prefs": prefs},
+ {"file": "helper_override_subdoc.html", "prefs": prefs},
+];
+
+if (isApzEnabled()) {
+ SimpleTest.waitForExplicitFinish();
+ window.onload = function() {
+ runSubtestsSeriallyInFreshWindows(subtests)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+ };
+}
+
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_group_overscroll.html b/gfx/layers/apz/test/mochitest/test_group_overscroll.html
new file mode 100644
index 0000000000..d94cd3b65f
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_overscroll.html
@@ -0,0 +1,35 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Tests for overscroll</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+var prefs = [
+ ["apz.overscroll.enabled", true],
+ ["apz.overscroll.test_async_scroll_offset.enabled", true],
+ ["apz.test.logging_enabled", true],
+];
+
+var subtests = [
+ {"file": "helper_overscroll_in_subscroller.html",
+ "prefs": [ ...prefs,
+ ["apz.overscroll.damping", 10.0] /* Make overscroll animations slower */ ] },
+];
+
+if (isApzEnabled()) {
+ SimpleTest.waitForExplicitFinish();
+ window.onload = function() {
+ runSubtestsSeriallyInFreshWindows(subtests)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+ };
+}
+
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_group_overscroll_handoff.html b/gfx/layers/apz/test/mochitest/test_group_overscroll_handoff.html
new file mode 100644
index 0000000000..2df4ee37b8
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_overscroll_handoff.html
@@ -0,0 +1,46 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Tests for overscroll handoff</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+var prefs = [
+ // turn off smooth scrolling so that we don't have to wait for
+ // APZ animations to finish before sampling the scroll offset
+ ["general.smoothScroll", false],
+ ["apz.test.mac.synth_wheel_input", true],
+ // ensure that any mouse movement will trigger a new wheel transaction,
+ // because in this test we move the mouse a bunch and want to recalculate
+ // the target APZC after each such movement.
+ ["mousewheel.transaction.ignoremovedelay", 0],
+ ["mousewheel.transaction.timeout", 0],
+];
+
+var subtests = [
+ {"file": "helper_position_fixed_scroll_handoff-1.html", prefs},
+ {"file": "helper_position_fixed_scroll_handoff-2.html", prefs},
+ {"file": "helper_position_fixed_scroll_handoff-3.html", prefs},
+ {"file": "helper_position_fixed_scroll_handoff-4.html", prefs},
+ {"file": "helper_position_fixed_scroll_handoff-5.html", prefs},
+ {"file": "helper_position_sticky_scroll_handoff.html", prefs},
+ {"file": "helper_wheelevents_handoff_on_iframe.html", "prefs": prefs},
+ {"file": "helper_wheelevents_handoff_on_non_scrollable_iframe.html", "prefs": prefs},
+];
+
+if (isApzEnabled()) {
+ SimpleTest.waitForExplicitFinish();
+ window.onload = function() {
+ runSubtestsSeriallyInFreshWindows(subtests)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+ };
+}
+
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_group_pointerevents.html b/gfx/layers/apz/test/mochitest/test_group_pointerevents.html
new file mode 100644
index 0000000000..21fa4585de
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_pointerevents.html
@@ -0,0 +1,43 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1285070
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1285070</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+ let isWindows = navigator.platform.indexOf("Win") == 0;
+ let isMac = getPlatform() == "mac";
+ var touch_action_prefs = getPrefs("TOUCH_ACTION");
+ var subtests = [
+ {"file": "helper_bug1285070.html"},
+ {"file": "helper_bug1299195.html", "prefs": [["dom.meta-viewport.enabled", isMac]]},
+ {"file": "helper_bug1414336.html", "prefs": [["apz.test.fails_with_native_injection", isWindows]]},
+ {"file": "helper_bug1502010_unconsumed_pan.html"},
+ {"file": "helper_bug1544966_zoom_on_touch_action_none.html", "prefs": touch_action_prefs},
+ {"file": "helper_bug1648491_no_pointercancel_with_dtc.html", "prefs": touch_action_prefs},
+ {"file": "helper_bug1663731_no_pointercancel_on_second_touchstart.html",
+ "prefs": touch_action_prefs},
+ {"file": "helper_bug1682170_pointercancel_on_touchaction_pinchzoom.html",
+ "prefs": touch_action_prefs},
+ ];
+
+ if (isApzEnabled()) {
+ SimpleTest.waitForExplicitFinish();
+ window.onload = function() {
+ runSubtestsSeriallyInFreshWindows(subtests)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+ };
+ }
+
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_group_programmatic_scroll_behavior.html b/gfx/layers/apz/test/mochitest/test_group_programmatic_scroll_behavior.html
new file mode 100644
index 0000000000..13bdb4efc4
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_programmatic_scroll_behavior.html
@@ -0,0 +1,38 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Various programmatic scroll tests that spawn in new windows</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+let smoothScrollEnabled = [["general.smoothScroll", true]];
+let smoothScrollDisabled = [["general.smoothScroll", false]];
+
+var subtests = [
+ {"file": "helper_programmatic_scroll_behavior.html?action=scrollIntoView", "prefs": smoothScrollEnabled},
+ {"file": "helper_programmatic_scroll_behavior.html?action=scrollIntoView", "prefs": smoothScrollDisabled},
+ {"file": "helper_programmatic_scroll_behavior.html?action=scrollBy", "prefs": smoothScrollEnabled},
+ {"file": "helper_programmatic_scroll_behavior.html?action=scrollBy", "prefs": smoothScrollDisabled},
+ {"file": "helper_programmatic_scroll_behavior.html?action=scrollTo", "prefs": smoothScrollEnabled},
+ {"file": "helper_programmatic_scroll_behavior.html?action=scrollTo", "prefs": smoothScrollDisabled},
+ {"file": "helper_programmatic_scroll_behavior.html?action=scroll", "prefs": smoothScrollEnabled},
+ {"file": "helper_programmatic_scroll_behavior.html?action=scroll", "prefs": smoothScrollDisabled},
+];
+
+if (isApzEnabled()) {
+ SimpleTest.waitForExplicitFinish();
+ window.onload = function() {
+ runSubtestsSeriallyInFreshWindows(subtests)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+ };
+}
+
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_group_scroll_linked_effect.html b/gfx/layers/apz/test/mochitest/test_group_scroll_linked_effect.html
new file mode 100644
index 0000000000..f29a382fb8
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_scroll_linked_effect.html
@@ -0,0 +1,33 @@
+<!DOCTYPE>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Tests for scroll linked effect</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+var prefs = [
+ ["apz.test.mac.synth_wheel_input", true],
+ ["apz.disable_for_scroll_linked_effects", false]
+];
+
+var subtests = [
+ {"file": "helper_scroll_linked_effect_by_wheel.html", prefs},
+ {"file": "helper_scroll_linked_effect_detector.html", prefs}
+];
+
+if (isApzEnabled()) {
+ SimpleTest.waitForExplicitFinish();
+ window.onload = function() {
+ runSubtestsSeriallyInFreshWindows(subtests)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+ };
+}
+
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_group_scroll_snap.html b/gfx/layers/apz/test/mochitest/test_group_scroll_snap.html
new file mode 100644
index 0000000000..9a1341c503
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_scroll_snap.html
@@ -0,0 +1,67 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Various tests for scroll snap</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+const prefs = [
+ ["general.smoothScroll", false],
+ // ensure that any mouse movement will trigger a new wheel transaction,
+ // because in this test we move the mouse a bunch and want to recalculate
+ // the target APZC after each such movement.
+ ["mousewheel.transaction.ignoremovedelay", 0],
+ ["mousewheel.transaction.timeout", 0],
+];
+
+const subtests = [
+ {"file": "helper_scroll_snap_no_valid_snap_position.html", "prefs": prefs},
+ {"file": "helper_scroll_snap_resnap_after_async_scroll.html",
+ // Specify a small `layout.css.scroll-behavior.spring-constant` value to
+ // keep the smooth scroll animation running long enough so that we can
+ // trigger a reflow during the animation.
+ "prefs": [["layout.css.scroll-behavior.spring-constant", 10],
+ ["apz.test.mac.synth_wheel_input", true]]},
+ {"file": "helper_scroll_snap_resnap_after_async_scroll.html",
+ "prefs": [["general.smoothScroll", false],
+ ["apz.test.mac.synth_wheel_input", true]]},
+ {"file": "helper_scroll_snap_resnap_after_async_scrollBy.html",
+ // Same as above helper_scroll_snap_resnap_after_async_scroll.html.
+ "prefs": [["layout.css.scroll-behavior.spring-constant", 10]]},
+ {"file": "helper_scroll_snap_not_resnap_during_panning.html",
+ // Specify a strong spring constant to make scroll snap animation be
+ // effective in a short span of time.
+ "prefs": [["layout.css.scroll-behavior.spring-constant", 1000]]},
+ {"file": "helper_scroll_snap_not_resnap_during_scrollbar_dragging.html",
+ // Same as above helper_scroll_snap_not_resnap_during_scrollbar_dragging.html.
+ "prefs": [["layout.css.scroll-behavior.spring-constant", 1000]]},
+ {"file": "helper_bug1780701.html"},
+ {"file": "helper_bug1783936.html",
+ // Shorten the scroll snap animation duration.
+ "prefs": [["layout.css.scroll-behavior.spring-constant", 1000],
+ // Avoid fling at the end of pan.
+ ["apz.fling_min_velocity_threshold", "10000"],
+ // This test needs mSimilateMomentum flag on headless mode.
+ ["apz.test.headless.simulate_momentum", true],
+ ["apz.gtk.kinetic_scroll.enabled", true],
+ // Use the pixel mode to make the test predictable easily.
+ ["apz.gtk.kinetic_scroll.delta_mode", 2],
+ ["apz.gtk.kinetic_scroll.pixel_delta_mode_multiplier", 1]]},
+];
+
+if (isApzEnabled()) {
+ SimpleTest.waitForExplicitFinish();
+ window.onload = function() {
+ runSubtestsSeriallyInFreshWindows(subtests)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+ };
+}
+
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_group_scrollend.html b/gfx/layers/apz/test/mochitest/test_group_scrollend.html
new file mode 100644
index 0000000000..9c2ee71a45
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_scrollend.html
@@ -0,0 +1,58 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Various scrollend tests that spawn in new windows</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+var basePrefs = [
+ ["apz.test.mac.synth_wheel_input", true],
+];
+
+var scrollendDisabledPrefs = [
+ ...basePrefs,
+ ["apz.scrollend-event.content.enabled", false],
+];
+
+var prefs = [
+ ...basePrefs,
+ ["apz.scrollend-event.content.enabled", true],
+];
+
+var smoothScrollDisabled = [
+ ...basePrefs,
+ ["general.smoothScroll", true],
+];
+
+var subtests = [
+ {"file": "helper_basic_scrollend.html?chrome-only=true",
+ "prefs": scrollendDisabledPrefs},
+ {"file": "helper_basic_scrollend.html?chrome-only=true", "prefs": prefs},
+ {"file": "helper_basic_scrollend.html?chrome-only=false", "prefs": prefs},
+ {"file": "helper_scrollend_bubbles.html?scroll-target=document", "prefs": prefs},
+ {"file": "helper_scrollend_bubbles.html?scroll-target=element", "prefs": prefs},
+ {"file": "helper_main_thread_smooth_scroll_scrollend.html", "prefs": prefs},
+ {"file": "helper_scrollend_bubbles.html?scroll-target=document",
+ "prefs": smoothScrollDisabled},
+ {"file": "helper_scrollend_bubbles.html?scroll-target=element",
+ "prefs": smoothScrollDisabled},
+ {"file": "helper_main_thread_smooth_scroll_scrollend.html",
+ "prefs": smoothScrollDisabled},
+];
+
+if (isApzEnabled()) {
+ SimpleTest.waitForExplicitFinish();
+ window.onload = function() {
+ runSubtestsSeriallyInFreshWindows(subtests)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+ };
+}
+
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_group_scrollframe_activation.html b/gfx/layers/apz/test/mochitest/test_group_scrollframe_activation.html
new file mode 100644
index 0000000000..2e75f45fc8
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_scrollframe_activation.html
@@ -0,0 +1,49 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1151663
+-->
+
+<head>
+ <meta charset="utf-8">
+ <title>Tests related to scrollframe activation</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="application/javascript">
+ if (isApzEnabled()) {
+ SimpleTest.waitForExplicitFinish();
+
+ let prefs = [
+ ["apz.test.logging_enabled", true]
+ ];
+
+ var subtests = [
+ { file: "helper_scrollframe_activation_on_load.html", prefs },
+ { file: "helper_bug982141.html", prefs },
+ ];
+
+ if (getPlatform() != "android") {
+ // promiseMoveMouseAndScrollWheelOver in helper_check_dp_size doesn't
+ // work on android. Trying to use promiseNativeTouchDrag on android
+ // leads to very large display ports (larger than even the bad case
+ // below), not sure why.
+ subtests.push(
+ { file: "helper_check_dp_size.html", prefs }
+ );
+ }
+
+ window.onload = function() {
+ runSubtestsSeriallyInFreshWindows(subtests)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+ };
+ }
+ </script>
+</head>
+
+<body>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1151663">Mozilla Bug 1151663</a>
+</body>
+
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_group_touchevents-2.html b/gfx/layers/apz/test/mochitest/test_group_touchevents-2.html
new file mode 100644
index 0000000000..decd615795
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_touchevents-2.html
@@ -0,0 +1,69 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Various touch tests that spawn in new windows (2)</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+var isWindows = getPlatform() == "windows";
+
+const shared_prefs = [
+ ["apz.test.fails_with_native_injection", isWindows],
+ ["dom.w3c_touch_events.legacy_apis.enabled", true],
+];
+
+var subtests = [
+ // Taps on media elements to make sure the touchend event is delivered
+ // properly. We increase the long-tap timeout to ensure it doesn't get trip
+ // during the tap.
+ // Also this test (on Windows) cannot satisfy the OS requirement of providing
+ // an injected touch event every 100ms, because it waits for a paint between
+ // the touchstart and the touchend, so we have to use the "fake injection"
+ // code instead.
+ {"file": "helper_bug1162771.html", "prefs": [...shared_prefs,
+ ["ui.click_hold_context_menus.delay", 10000]]},
+
+ // As with the previous test, this test cannot inject touch events every 100ms
+ // because it waits for a long-tap, so we have to use the "fake injection" code
+ // instead.
+ // This test also disables synthesized mousemoves from reflow so it can make
+ // more precise assertions about the order in which events arrive.
+ {"file": "helper_long_tap.html", "prefs": [...shared_prefs,
+ ["layout.reflow.synthMouseMove", false]]},
+
+ // For the following tests, we want to make sure APZ doesn't wait for a content
+ // response that is never going to arrive. To detect this we set the content response
+ // timeout to a day, so that the entire test times out and fails if APZ does
+ // end up waiting.
+ {"file": "helper_tap_passive.html", "prefs": [...shared_prefs,
+ ["apz.content_response_timeout", 24 * 60 * 60 * 1000]]},
+
+ {"file": "helper_tap_default_passive.html", "prefs": [...shared_prefs,
+ ["apz.content_response_timeout", 24 * 60 * 60 * 1000]]},
+
+ // Add new subtests to test_group_touch_events-4.html, not this file.
+];
+
+if (isApzEnabled()) {
+ ok(window.TouchEvent, "Check if TouchEvent is supported (it should be, the test harness forces it on everywhere)");
+ if (getPlatform() == "android") {
+ // This has a lot of subtests, and Android emulators are slow.
+ SimpleTest.requestLongerTimeout(2);
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ window.onload = function() {
+ runSubtestsSeriallyInFreshWindows(subtests)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+ };
+}
+
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_group_touchevents-3.html b/gfx/layers/apz/test/mochitest/test_group_touchevents-3.html
new file mode 100644
index 0000000000..a86c80a2ec
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_touchevents-3.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Various touch tests that spawn in new windows (3)</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+var touch_action_prefs = getPrefs("TOUCH_ACTION");
+
+var subtests = [
+ // Simple test to exercise touch-action CSS property
+ {"file": "helper_touch_action.html", "prefs": touch_action_prefs},
+ // More complex touch-action tests, with overlapping regions and such
+ {"file": "helper_touch_action_complex.html", "prefs": touch_action_prefs},
+ // Tests that touch-action CSS properties are handled in APZ without waiting
+ // on the main-thread, when possible
+ {"file": "helper_touch_action_regions.html", "prefs": touch_action_prefs},
+ // Tests that touch-action inside zero-opacity items are respected
+ {"file": "helper_touch_action_zero_opacity_bug1500864.html", "prefs": touch_action_prefs},
+
+ // Add new subtests to test_group_touchevents-4.html, not this file (exceptions
+ // may be made for quick-running tests that need the touch-action prefs)
+];
+
+if (isApzEnabled()) {
+ ok(window.TouchEvent, "Check if TouchEvent is supported (it should be, the test harness forces it on everywhere)");
+ if (getPlatform() == "android") {
+ // This has a lot of subtests, and Android emulators are slow.
+ SimpleTest.requestLongerTimeout(2);
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ window.onload = function() {
+ runSubtestsSeriallyInFreshWindows(subtests)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+ };
+}
+
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_group_touchevents-4.html b/gfx/layers/apz/test/mochitest/test_group_touchevents-4.html
new file mode 100644
index 0000000000..266fc72ee8
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_touchevents-4.html
@@ -0,0 +1,54 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Various touch tests that spawn in new windows (4)</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+var touch_action_prefs = getPrefs("TOUCH_ACTION");
+
+var subtests = [
+ // clicking on element with :active::after CSS property
+ {"file": "helper_bug1473108.html"},
+ // Resetting isFirstPaint shouldn't clobber the visual viewport
+ {"file": "helper_bug1509575.html", "prefs": getPrefs("TOUCH_EVENTS:PAN")},
+ // Exercise one of the main-thread touch-action determination codepaths.
+ {"file": "helper_bug1506497_touch_action_fixed_on_fixed.html", "prefs": touch_action_prefs},
+ {"file": "helper_bug1637113_main_thread_hit_test.html"},
+ {"file": "helper_bug1638458_contextmenu.html"},
+ {"file": "helper_bug1638441_fixed_pos_hit_test.html"},
+ {"file": "helper_bug1637135_narrow_viewport.html", "prefs": [["dom.meta-viewport.enabled", true]]},
+ {"file": "helper_bug1714934_mouseevent_buttons.html"},
+
+ // Add new subtests here. If this starts timing out because it's taking too
+ // long, create a test_group_touchevents-5.html file. Refer to 1423011#c57
+ // for more details.
+ // test_group_touchevents-5.html already exists because a new test would
+ // timeout (without making any process) with fission x-origin tests if added
+ // here. So you can add tests here or in test_group_touchevents-5.html until
+ // they start timing out.
+];
+
+if (isApzEnabled()) {
+ ok(window.TouchEvent, "Check if TouchEvent is supported (it should be, the test harness forces it on everywhere)");
+ if (getPlatform() == "android") {
+ // This has a lot of subtests, and Android emulators are slow.
+ SimpleTest.requestLongerTimeout(2);
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ window.onload = function() {
+ runSubtestsSeriallyInFreshWindows(subtests)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+ };
+}
+
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_group_touchevents-5.html b/gfx/layers/apz/test/mochitest/test_group_touchevents-5.html
new file mode 100644
index 0000000000..d4ae624a69
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_touchevents-5.html
@@ -0,0 +1,51 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Various touch tests that spawn in new windows (5)</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+
+var subtests = [
+ // tests that scrolling doesn't cause extra SchedulePaint calls
+ {"file": "helper_bug1669625.html", "dp_suppression": false},
+ {"file": "helper_touch_drag_root_scrollbar.html", "prefs": [["apz.allow_zooming", true]]},
+ {"file": "helper_touch_drag_root_scrollbar.html", "prefs": [["apz.allow_zooming", false]]},
+ {"file": "helper_overscroll_in_apz_test_data.html", "prefs": [
+ ["apz.overscroll.enabled", true],
+ ["apz.overscroll.test_async_scroll_offset.enabled", true],
+ ["apz.test.logging_enabled", true],
+ ]},
+ // Add new subtests here. If this starts timing out because it's taking too
+ // long, create a test_group_touchevents-6.html file. Refer to 1423011#c57
+ // for more details.
+ // You can still add tests to test_group_touchevents-4.html, it hasn't gotten
+ // too long yet, but this file was created because adding a specific test to
+ // test_group_touchevents-5.html would timeout (without making any progress)
+ // with fission x-origin tests. So you can add tests here or in
+ // test_group_touchevents-4.html until they start timing out.
+];
+
+if (isApzEnabled()) {
+ ok(window.TouchEvent, "Check if TouchEvent is supported (it should be, the test harness forces it on everywhere)");
+ if (getPlatform() == "android") {
+ // This has a lot of subtests, and Android emulators are slow.
+ SimpleTest.requestLongerTimeout(2);
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ window.onload = function() {
+ runSubtestsSeriallyInFreshWindows(subtests)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+ };
+}
+
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_group_touchevents.html b/gfx/layers/apz/test/mochitest/test_group_touchevents.html
new file mode 100644
index 0000000000..df24e24f3d
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_touchevents.html
@@ -0,0 +1,55 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Various touch tests that spawn in new windows</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+var basic_pan_prefs = getPrefs("TOUCH_EVENTS:PAN");
+
+var subtests = [
+ // Simple tests to exercise basic panning behaviour
+ {"file": "helper_basic_pan.html", "prefs": basic_pan_prefs},
+ {"file": "helper_div_pan.html", "prefs": basic_pan_prefs},
+ {"file": "helper_iframe_pan.html", "prefs": basic_pan_prefs},
+
+ // Simple test to exercise touch-tapping behaviour
+ {"file": "helper_tap.html"},
+ // Tapping, but with a full-zoom applied
+ {"file": "helper_tap_fullzoom.html"},
+
+ // For the following two tests, disable displayport suppression to make sure it
+ // doesn't interfere with the test by scheduling paints non-deterministically.
+ {"file": "helper_scrollto_tap.html?true",
+ "prefs": [["apz.paint_skipping.enabled", true]],
+ "dp_suppression": false},
+ {"file": "helper_scrollto_tap.html?false",
+ "prefs": [["apz.paint_skipping.enabled", false]],
+ "dp_suppression": false},
+
+ // Add new subtests to test_group_touch_events-4.html, not this file.
+];
+
+if (isApzEnabled()) {
+ ok(window.TouchEvent, "Check if TouchEvent is supported (it should be, the test harness forces it on everywhere)");
+ if (getPlatform() == "android") {
+ // This has a lot of subtests, and Android emulators are slow.
+ SimpleTest.requestLongerTimeout(2);
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ window.onload = function() {
+ runSubtestsSeriallyInFreshWindows(subtests)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+ };
+}
+
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_group_wheelevents.html b/gfx/layers/apz/test/mochitest/test_group_wheelevents.html
new file mode 100644
index 0000000000..34e243f679
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_wheelevents.html
@@ -0,0 +1,84 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Various wheel-scrolling tests that spawn in new windows</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+var prefs = [
+ // turn off smooth scrolling so that we don't have to wait for
+ // APZ animations to finish before sampling the scroll offset
+ ["general.smoothScroll", false],
+ // ensure that any mouse movement will trigger a new wheel transaction,
+ // because in this test we move the mouse a bunch and want to recalculate
+ // the target APZC after each such movement.
+ ["mousewheel.transaction.ignoremovedelay", 0],
+ ["mousewheel.transaction.timeout", 0],
+];
+
+// For helper_scroll_over_scrollbar, we need to set a pref to force
+// layerization of the scrollbar track to reproduce the bug being fixed.
+// Otherwise, the bug only manifests with overlay scrollbars on macOS,
+// or in a XUL RCD, both of which are hard to materialize in a test.
+var scrollbar_prefs = prefs.slice(); // make a copy
+scrollbar_prefs.push(["layout.scrollbars.always-layerize-track", true]);
+
+// For helper_overscroll_behavior_bug1425573, we need to set the APZ content
+// response timeout to 0, so we exercise the fallback codepath.
+var timeout_prefs = prefs.slice(); // make a copy
+timeout_prefs.push(["apz.content_response_timeout", 0]);
+
+var smoothness_prefs = [
+ ["apz.test.logging_enabled", true],
+ // We'd want to test real wheel events rather than pan events.
+ ["apz.test.mac.synth_wheel_input", true],
+ // Use a longer animation duration to avoid the situation that the
+ // animation stops accidentally in between each wheel event.
+ // If the situation happens, scroll offsets will not change at the moment.
+ ["general.smoothScroll.mouseWheel.durationMaxMS", 1500],
+ ["general.smoothScroll.mouseWheel.durationMinMS", 1500]
+];
+
+var subtests = [
+ {"file": "helper_scroll_on_position_fixed.html", "prefs": prefs},
+ {"file": "helper_bug1271432.html", "prefs": prefs},
+ {"file": "helper_overscroll_behavior_bug1425573.html", "prefs": timeout_prefs},
+ {"file": "helper_overscroll_behavior_bug1425603.html", "prefs": prefs},
+ {"file": "helper_overscroll_behavior_bug1494440.html", "prefs": prefs},
+ {"file": "helper_scroll_inactive_perspective.html", "prefs": prefs},
+ {"file": "helper_scroll_inactive_zindex.html", "prefs": prefs},
+ {"file": "helper_scroll_over_scrollbar.html", "prefs": scrollbar_prefs},
+ {"file": "helper_scroll_tables_perspective.html", "prefs": prefs},
+ {"file": "helper_relative_scroll_smoothness.html?input-type=wheel&scroll-method=scrollBy", prefs: smoothness_prefs },
+ {"file": "helper_relative_scroll_smoothness.html?input-type=wheel&scroll-method=scrollTo", prefs: smoothness_prefs },
+ {"file": "helper_relative_scroll_smoothness.html?input-type=wheel&scroll-method=scrollTop", prefs: smoothness_prefs },
+ {"file": "helper_transform_end_on_wheel_scroll.html",
+ prefs: [["general.smoothScroll", false],
+ ["apz.test.mac.synth_wheel_input", true]]},
+ {"file": "helper_scroll_anchoring_on_wheel.html", prefs: smoothness_prefs},
+];
+
+// Only Windows has the test api implemented for this test.
+if (getPlatform() == "windows") {
+ subtests.push(
+ {"file": "helper_dommousescroll.html", "prefs": prefs}
+ );
+}
+
+if (isApzEnabled()) {
+ SimpleTest.waitForExplicitFinish();
+ window.onload = function() {
+ runSubtestsSeriallyInFreshWindows(subtests)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+ };
+}
+
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_group_zoom-2.html b/gfx/layers/apz/test/mochitest/test_group_zoom-2.html
new file mode 100644
index 0000000000..1f94b9b5b6
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_zoom-2.html
@@ -0,0 +1,81 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Various zoom-related tests that spawn in new windows</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+var prefs = [
+ // We need the APZ paint logging information
+ ["apz.test.logging_enabled", true],
+ // Dropping the touch slop to 0 makes the tests easier to write because
+ // we can just do a one-pixel drag to get over the pan threshold rather
+ // than having to hard-code some larger value.
+ ["apz.touch_start_tolerance", "0.0"],
+ // The subtests in this test do touch-drags to pan the page, but we don't
+ // want those pans to turn into fling animations, so we increase the
+ // fling-min threshold velocity to an arbitrarily large value.
+ ["apz.fling_min_velocity_threshold", "10000"],
+ // The helper_bug1280013's div gets a displayport on scroll, but if the
+ // test takes too long the displayport can expire before we read the value
+ // out of the test. So we disable displayport expiry for these tests.
+ ["apz.displayport_expiry_ms", 0],
+ // Increase the content response timeout because some tests do preventDefault
+ // and we want to make sure APZ actually waits for them.
+ ["apz.content_response_timeout", 60000],
+ // Force consistent scroll-to-click behaviour across all platforms.
+ ["ui.scrollToClick", 0],
+ // Disable touch resampling so that touch events are processed without delay
+ // and we don't zoom more than expected due to overprediction.
+ ["android.touch_resampling.enabled", false],
+];
+
+var instant_repaint_prefs = [
+ // When zooming, trigger repaint requests for each scale event rather than
+ // delaying the repaints
+ ["apz.scale_repaint_delay_ms", 0],
+ ... prefs
+];
+
+var subtests = [
+ {"file": "helper_bug1280013.html", "prefs": prefs},
+ {"file": "helper_zoom_restore_position_tabswitch.html", "prefs": prefs},
+ {"file": "helper_zoom_with_dynamic_toolbar.html", "prefs": prefs},
+ {"file": "helper_visual_scrollbars_pagescroll.html", "prefs": prefs},
+ {"file": "helper_click_interrupt_animation.html", "prefs": prefs},
+ {"file": "helper_overflowhidden_zoom.html", "prefs": prefs},
+ {"file": "helper_zoom_keyboardscroll.html", "prefs": prefs},
+ {"file": "helper_zoom_out_clamped_scrollpos.html", "prefs": instant_repaint_prefs},
+ {"file": "helper_zoom_out_with_mainthread_clamping.html", "prefs": instant_repaint_prefs},
+ {"file": "helper_fixed_html_hittest.html", "prefs": prefs},
+ // {"file": "helper_zoom_oopif.html", "prefs": prefs}, // disabled, see bug 1716127
+ {"file": "helper_zoom_after_gpu_process_restart.html", "prefs": prefs}
+];
+
+if (isApzEnabled()) {
+ // This has a lot of subtests, and Android emulators are slow.
+ SimpleTest.requestLongerTimeout(2);
+ SimpleTest.waitForExplicitFinish();
+
+ if (getPlatform() == "linux") {
+ subtests.push(
+ {"file": "helper_zoom_with_touchpad.html", "prefs": prefs},
+ {"file": "helper_touchpad_pinch_and_pan.html", "prefs": prefs},
+ {"file": "helper_zoom_oopif.html?touchpad", "prefs": prefs},
+ );
+ }
+ window.onload = function() {
+ runSubtestsSeriallyInFreshWindows(subtests)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+ };
+}
+
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_group_zoom.html b/gfx/layers/apz/test/mochitest/test_group_zoom.html
new file mode 100644
index 0000000000..03f9bbebf8
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_zoom.html
@@ -0,0 +1,80 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Various zoom-related tests that spawn in new windows</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+var prefs = [
+ // We need the APZ paint logging information
+ ["apz.test.logging_enabled", true],
+ // Dropping the touch slop to 0 makes the tests easier to write because
+ // we can just do a one-pixel drag to get over the pan threshold rather
+ // than having to hard-code some larger value.
+ ["apz.touch_start_tolerance", "0.0"],
+ // The subtests in this test do touch-drags to pan the page, but we don't
+ // want those pans to turn into fling animations, so we increase the
+ // fling-min threshold velocity to an arbitrarily large value.
+ ["apz.fling_min_velocity_threshold", "10000"],
+ // The helper_bug1280013's div gets a displayport on scroll, but if the
+ // test takes too long the displayport can expire before we read the value
+ // out of the test. So we disable displayport expiry for these tests.
+ ["apz.displayport_expiry_ms", 0],
+ // Increase the content response timeout because some tests do preventDefault
+ // and we want to make sure APZ actually waits for them.
+ ["apz.content_response_timeout", 60000],
+ // Disable touch resampling so that touch events are processed without delay
+ // and we don't zoom more than expected due to overprediction.
+ ["android.touch_resampling.enabled", false],
+];
+
+// Increase the tap timeouts so the one-touch-pinch gesture is still detected
+// in case of random delays during testing. Also ensure that the feature is
+// actually enabled (which it should be by default, but it's good to be safe).
+var onetouchpinch_prefs = [
+ ...prefs,
+ ["apz.one_touch_pinch.enabled", true],
+ ["ui.click_hold_context_menus.delay", 10000],
+ ["apz.max_tap_time", 10000],
+];
+
+// For helper_fixed_pos_displayport the mechanism we use to record the
+// fixed-pos displayport only takes effect when not in a partial display list
+// update. So for this test we just disable retained display lists entirely.
+var no_rdl_prefs = [
+ ...prefs,
+ ["layout.display-list.retain", false],
+];
+
+var subtests = [
+ {"file": "helper_basic_zoom.html", "prefs": prefs},
+ {"file": "helper_basic_onetouchpinch.html", "prefs": onetouchpinch_prefs},
+ {"file": "helper_zoom_prevented.html", "prefs": prefs},
+ {"file": "helper_zoomed_pan.html", "prefs": prefs},
+ {"file": "helper_fixed_position_scroll_hittest.html", "prefs": prefs},
+ {"file": "helper_onetouchpinch_nested.html", "prefs": onetouchpinch_prefs},
+ {"file": "helper_visual_smooth_scroll.html", "prefs": prefs},
+ {"file": "helper_scroll_into_view_bug1516056.html", "prefs": prefs},
+ {"file": "helper_scroll_into_view_bug1562757.html", "prefs": prefs},
+ {"file": "helper_fixed_pos_displayport.html", "prefs": no_rdl_prefs},
+ // If you're adding more tests, add them to test_group_zoom-2.html
+];
+
+if (isApzEnabled()) {
+ // This has a lot of subtests, and Android emulators are slow.
+ SimpleTest.requestLongerTimeout(2);
+ SimpleTest.waitForExplicitFinish();
+ window.onload = function() {
+ runSubtestsSeriallyInFreshWindows(subtests)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+ };
+}
+
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_group_zoomToFocusedInput.html b/gfx/layers/apz/test/mochitest/test_group_zoomToFocusedInput.html
new file mode 100644
index 0000000000..72fd9dcc11
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_zoomToFocusedInput.html
@@ -0,0 +1,49 @@
+<!DOCTYPE>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Various zoomToFocusedInput tests</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+var subtests = [
+ {"file": "helper_zoomToFocusedInput_scroll.html"},
+ {"file": "helper_zoomToFocusedInput_multiline.html"},
+ {"file": "helper_zoomToFocusedInput_iframe.html"},
+ {"file": "helper_zoomToFocusedInput_fixed_bug1673511.html"},
+ {"file": "helper_zoomToFocusedInput_nozoom_bug1738696.html"},
+];
+
+// These tests rely on mobile viewport sizing, so only run them on
+// mobile for now. In the future we can consider running them on
+// on desktop, but only in configurations with overlay scrollbars
+// (see bug 1608506).
+let platform = getPlatform();
+if (platform == "android") {
+ subtests.push(
+ {"file": "helper_zoomToFocusedInput_nozoom.html"}
+ );
+ subtests.push(
+ {"file": "helper_zoomToFocusedInput_zoom.html"}
+ );
+ subtests.push(
+ {"file": "helper_zoomToFocusedInput_touch-action.html"}
+ );
+}
+
+if (isApzEnabled()) {
+ SimpleTest.waitForExplicitFinish();
+ window.onload = function() {
+ runSubtestsSeriallyInFreshWindows(subtests)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+ };
+}
+
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_interrupted_reflow.html b/gfx/layers/apz/test/mochitest/test_interrupted_reflow.html
new file mode 100644
index 0000000000..8fc72e05a5
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_interrupted_reflow.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<html>
+ <!--
+ https://bugzilla.mozilla.org/show_bug.cgi?id=1292781
+ -->
+ <head>
+ <title>Test for bug 1292781</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ </head>
+ <body>
+<script type="text/javascript">
+if (isApzEnabled()) {
+ SimpleTest.waitForExplicitFinish();
+
+ // Run the actual test in its own window, because it is supposed to be run
+ // in the top level content document to collect APZ test data up to the root
+ // content APZC by using nsIDOMWindowUtils.getCompositorAPZTestData which
+ // is not able to walk up to the root content APZC if the function gets called
+ // from OOP iframes. We could make it work across process boundaries, but so
+ // far running the test in a new top level content document would be fine.
+ var w = null;
+ window.onload = async () => {
+ await pushPrefs([["apz.test.logging_enabled", true],
+ ["apz.displayport_expiry_ms", 0]]);
+
+ w = window.open("helper_interrupted_reflow.html", "_blank");
+ };
+}
+
+function finishTest() {
+ w.close();
+ SimpleTest.finish();
+}
+</script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_layerization.html b/gfx/layers/apz/test/mochitest/test_layerization.html
new file mode 100644
index 0000000000..0ff76de317
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_layerization.html
@@ -0,0 +1,312 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1173580
+-->
+<head>
+ <title>Test for layerization</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <link rel="stylesheet" type="text/css" href="helper_subframe_style.css"/>
+ <style>
+ #container {
+ display: flex;
+ overflow: scroll;
+ height: 500px;
+ }
+ .outer-frame {
+ height: 500px;
+ overflow: scroll;
+ flex-basis: 100%;
+ background: repeating-linear-gradient(#CCC, #CCC 100px, #BBB 100px, #BBB 200px);
+ }
+ #container-content {
+ height: 200%;
+ }
+ </style>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1173580">APZ layerization tests</a>
+<p id="display"></p>
+<div id="container">
+ <div id="outer1" class="outer-frame">
+ <div id="inner1" class="inner-frame">
+ <div class="inner-content"></div>
+ </div>
+ </div>
+ <div id="outer2" class="outer-frame">
+ <div id="inner2" class="inner-frame">
+ <div class="inner-content"></div>
+ </div>
+ </div>
+ <iframe id="outer3" class="outer-frame" src="helper_iframe1.html"></iframe>
+ <iframe id="outer4" class="outer-frame" src="helper_iframe2.html"></iframe>
+<!-- The container-content div ensures 'container' is scrollable, so the
+ optimization that layerizes the primary async-scrollable frame on page
+ load layerizes it rather than its child subframes. -->
+ <div id="container-content"></div>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+// Scroll the mouse wheel over |element|.
+async function scrollWheelOver(element) {
+ await promiseMoveMouseAndScrollWheelOver(element, 10, 10, /* waitForScroll = */ false);
+}
+
+const DISPLAYPORT_EXPIRY = 100;
+
+let config = getHitTestConfig();
+let activateAllScrollFrames = config.activateAllScrollFrames;
+
+let heightMultiplier = SpecialPowers.getCharPref("apz.y_stationary_size_multiplier");
+// With WebRender, the effective height multiplier can be reduced
+// for alignment reasons. The reduction should be no more than a
+// factor of two.
+heightMultiplier /= 2;
+info("effective displayport height multipler is " + heightMultiplier);
+
+function hasNonZeroMarginDisplayPort(elementId, containingDoc = null) {
+ let dp = getLastContentDisplayportFor(elementId);
+ if (dp == null) {
+ return false;
+ }
+ let elt = (containingDoc != null ? containingDoc : document).getElementById(elementId);
+ info(elementId);
+ info("window size " + window.innerWidth + " " + window.innerHeight);
+ info("dp " + dp.x + " " + dp.y + " " + dp.width + " " + dp.height);
+ info("eltsize " + elt.clientWidth + " " + elt.clientHeight);
+ return dp.height >= heightMultiplier * Math.min(elt.clientHeight, window.innerHeight);
+}
+
+function hasMinimalDisplayPort(elementId, containingDoc = null) {
+ let dp = getLastContentDisplayportFor(elementId);
+ if (dp == null) {
+ return false;
+ }
+ let elt = (containingDoc != null ? containingDoc : document).getElementById(elementId);
+ info(elementId);
+ info("dp " + dp.x + " " + dp.y + " " + dp.width + " " + dp.height);
+ info("eltsize " + elt.clientWidth + " " + elt.clientHeight);
+ return dp.width <= (elt.clientWidth + 2) && dp.height <= (elt.clientHeight + 2);
+}
+
+function checkDirectActivation(elementId, containingDoc = null) {
+ if (activateAllScrollFrames) {
+ return hasNonZeroMarginDisplayPort(elementId, containingDoc);
+ }
+ return isLayerized(elementId);
+
+}
+
+function checkAncestorActivation(elementId, containingDoc = null) {
+ if (activateAllScrollFrames) {
+ return hasMinimalDisplayPort(elementId, containingDoc);
+ }
+ return isLayerized(elementId);
+
+}
+
+function checkInactive(elementId, containingDoc = null) {
+ if (activateAllScrollFrames) {
+ return hasMinimalDisplayPort(elementId, containingDoc);
+ }
+ return !isLayerized(elementId);
+
+}
+
+async function test() {
+ await SpecialPowers.pushPrefEnv({
+ "set": [
+ // Causes the test to intermittently fail on ASAN opt linux.
+ ["mousewheel.system_scroll_override.enabled", false],
+ ]
+ });
+
+ let outer3Doc = document.getElementById("outer3").contentDocument;
+ let outer4Doc = document.getElementById("outer4").contentDocument;
+
+ // Initially, everything should be inactive.
+ ok(checkInactive("outer1"), "initially 'outer1' should not be active");
+ ok(checkInactive("inner1"), "initially 'inner1' should not be active");
+ ok(checkInactive("outer2"), "initially 'outer2' should not be active");
+ ok(checkInactive("inner2"), "initially 'inner2' should not be active");
+ ok(checkInactive("outer3"), "initially 'outer3' should not be active");
+ ok(checkInactive("inner3", outer3Doc),
+ "initially 'inner3' should not be active");
+ ok(checkInactive("outer4"), "initially 'outer4' should not be active");
+ ok(checkInactive("inner4", outer4Doc),
+ "initially 'inner4' should not be active");
+
+ // Scrolling over outer1 should activate outer1 directly, but not inner1.
+ await scrollWheelOver(document.getElementById("outer1"));
+ await promiseAllPaintsDone();
+ await promiseOnlyApzControllerFlushed();
+ ok(checkDirectActivation("outer1"),
+ "scrolling 'outer1' should activate it directly");
+ ok(checkInactive("inner1"),
+ "scrolling 'outer1' should not cause 'inner1' to get activated");
+
+ // Scrolling over inner2 should activate inner2 directly, but outer2 only ancestrally.
+ await scrollWheelOver(document.getElementById("inner2"));
+ await promiseAllPaintsDone();
+ await promiseOnlyApzControllerFlushed();
+ ok(checkDirectActivation("inner2"),
+ "scrolling 'inner2' should cause it to be directly activated");
+ ok(checkAncestorActivation("outer2"),
+ "scrolling 'inner2' should cause 'outer2' to be activated as an ancestor");
+
+ // The second half of the test repeats the same checks as the first half,
+ // but with an iframe as the outer scrollable frame.
+
+ // Scrolling over outer3 should activate outer3 directly, but not inner3.
+ await scrollWheelOver(outer3Doc.documentElement);
+ await promiseAllPaintsDone();
+ await promiseOnlyApzControllerFlushed();
+ ok(checkDirectActivation("outer3"), "scrolling 'outer3' should cause it to be directly activated");
+ ok(checkInactive("inner3", outer3Doc),
+ "scrolling 'outer3' should not cause 'inner3' to be activated");
+
+ // Scrolling over inner4 should activate inner4 directly, but outer4 only ancestrally.
+ await scrollWheelOver(outer4Doc.getElementById("inner4"));
+ await promiseAllPaintsDone();
+ await promiseOnlyApzControllerFlushed();
+ ok(checkDirectActivation("inner4", outer4Doc),
+ "scrolling 'inner4' should cause it to be directly activated");
+ ok(checkAncestorActivation("outer4"),
+ "scrolling 'inner4' should cause 'outer4' to be activated");
+
+ // Now we enable displayport expiry, and verify that things are still
+ // activated as they were before.
+ await SpecialPowers.pushPrefEnv({"set": [["apz.displayport_expiry_ms", DISPLAYPORT_EXPIRY]]});
+ ok(checkDirectActivation("outer1"), "outer1 still has non zero display port after enabling expiry");
+ ok(checkInactive("inner1"), "inner1 is still has zero margin display port after enabling expiry");
+ ok(checkAncestorActivation("outer2"), "outer2 still has zero margin display port after enabling expiry");
+ ok(checkDirectActivation("inner2"), "inner2 still has non zero display port after enabling expiry");
+ ok(checkDirectActivation("outer3"), "outer3 still has non zero display port after enabling expiry");
+ ok(checkInactive("inner3", outer3Doc),
+ "inner3 still has zero margin display port after enabling expiry");
+ ok(checkDirectActivation("inner4", outer4Doc),
+ "inner4 still has non zero display port after enabling expiry");
+ ok(checkAncestorActivation("outer4"), "outer4 still has zero margin display port after enabling expiry");
+
+ // Now we trigger a scroll on some of the things still layerized, so that
+ // the displayport expiry gets triggered.
+
+ // Expire displayport with scrolling on outer1
+ await scrollWheelOver(document.getElementById("outer1"));
+ await promiseAllPaintsDone();
+ await promiseOnlyApzControllerFlushed();
+ await SpecialPowers.promiseTimeout(DISPLAYPORT_EXPIRY);
+ await promiseAllPaintsDone();
+ ok(checkInactive("outer1"), "outer1 is inactive after displayport expiry");
+ ok(checkInactive("inner1"), "inner1 is inactive after displayport expiry");
+
+ // Expire displayport with scrolling on inner2
+ await scrollWheelOver(document.getElementById("inner2"));
+ await promiseAllPaintsDone();
+ await promiseOnlyApzControllerFlushed();
+ // Once the expiry elapses, it will trigger expiry on outer2, so we check
+ // both, one at a time.
+ await SpecialPowers.promiseTimeout(DISPLAYPORT_EXPIRY);
+ await promiseAllPaintsDone();
+ ok(checkInactive("inner2"), "inner2 is inactive after displayport expiry");
+ await SpecialPowers.promiseTimeout(DISPLAYPORT_EXPIRY);
+ await promiseAllPaintsDone();
+ ok(checkInactive("outer2"), "outer2 is inactive with inner2");
+
+ // We need to wrap the next bit in a loop and keep retrying until it
+ // succeeds. Let me explain why this is the best option at this time. Below
+ // we scroll over inner3, this triggers a 100 ms timer to expire it's display
+ // port. Then when it expires it schedules a paint and triggers another
+ // 100 ms timer on it's parent, outer3, to expire. The paint needs to happen
+ // before the timer fires because the paint is what updates
+ // mIsParentToActiveScrollFrames on outer3, and mIsParentToActiveScrollFrames
+ // being true blocks a display port from expiring. It was true because it
+ // contained inner3, but no longer. In real life the timer is 15000 ms so a
+ // paint will happen, but here in a test the timer is 100 ms so that paint
+ // can not happen in time. We could add some more complication to this code
+ // just for this test, or we could just loop here.
+ let itWorked = false;
+ while (!itWorked) {
+ // Scroll on inner3. inner3 isn't layerized, and this will cause it to
+ // get layerized, but it will also trigger displayport expiration for inner3
+ // which will eventually trigger displayport expiration on inner3 and outer3.
+ // Note that the displayport expiration might actually happen before the wheel
+ // input is processed in the compositor (see bug 1246480 comment 3), and so
+ // we make sure not to wait for a scroll event here, since it may never fire.
+ // However, if we do get a scroll event while waiting for the expiry, we need
+ // to restart the expiry timer because the displayport expiry got reset. There's
+ // no good way that I can think of to deterministically avoid doing this.
+ let inner3 = outer3Doc.getElementById("inner3");
+ await scrollWheelOver(inner3);
+ await promiseAllPaintsDone();
+ await promiseOnlyApzControllerFlushed();
+ let timerPromise = new Promise(resolve => {
+ var timeoutTarget = function() {
+ inner3.removeEventListener("scroll", timeoutResetter);
+ resolve();
+ };
+ var timerId = setTimeout(timeoutTarget, DISPLAYPORT_EXPIRY);
+ var timeoutResetter = function() {
+ ok(true, "Got a scroll event; resetting timer...");
+ clearTimeout(timerId);
+ setTimeout(timeoutTarget, DISPLAYPORT_EXPIRY);
+ // by not updating timerId we ensure that this listener resets the timeout
+ // at most once.
+ };
+ inner3.addEventListener("scroll", timeoutResetter);
+ });
+ await timerPromise; // wait for the setTimeout to elapse
+
+ await promiseAllPaintsDone();
+ ok(checkInactive("inner3", outer3Doc),
+ "inner3 is inactive after expiry");
+ await SpecialPowers.promiseTimeout(DISPLAYPORT_EXPIRY);
+ await promiseAllPaintsDone();
+ if (checkInactive("outer3")) {
+ ok(true, "outer3 is inactive after inner3 triggered expiry");
+ itWorked = true;
+ }
+ }
+
+ // Scroll outer4 and wait for the expiry. It should NOT get expired because
+ // inner4 is still layerized
+ await scrollWheelOver(outer4Doc.documentElement);
+ await promiseAllPaintsDone();
+ await promiseOnlyApzControllerFlushed();
+ // Wait for the expiry to elapse
+ await SpecialPowers.promiseTimeout(DISPLAYPORT_EXPIRY);
+ await promiseAllPaintsDone();
+ ok(checkDirectActivation("inner4", outer4Doc),
+ "inner4 still is directly activated because it never expired");
+ ok(checkDirectActivation("outer4"),
+ "outer4 still still is directly activated because inner4 is still layerized");
+}
+
+if (isApzEnabled()) {
+ SimpleTest.waitForExplicitFinish();
+ SimpleTest.requestFlakyTimeout("we are testing code that measures an actual timeout");
+ SimpleTest.expectAssertions(0, 8); // we get a bunch of "ASSERTION: Bounds computation mismatch" sometimes (bug 1232856)
+
+ // Disable smooth scrolling, because it results in long-running scroll
+ // animations that can result in a 'scroll' event triggered by an earlier
+ // wheel event as corresponding to a later wheel event.
+ // Also enable APZ test logging, since we use that data to determine whether
+ // a scroll frame was layerized.
+ pushPrefs([["general.smoothScroll", false],
+ ["apz.displayport_expiry_ms", 0],
+ ["apz.test.logging_enabled", true]])
+ .then(waitUntilApzStable)
+ .then(test)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_relative_update.html b/gfx/layers/apz/test/mochitest/test_relative_update.html
new file mode 100644
index 0000000000..01c0ee1f9b
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_relative_update.html
@@ -0,0 +1,92 @@
+<!DOCTYPE HTML>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1453425
+-->
+<html>
+<head>
+ <title>Test for relative scroll offset updates (Bug 1453425)</title>
+ <meta charset="utf-8">
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <style type="text/css">
+ #frame {
+ width: 200px;
+ height: 400px;
+ overflow: scroll;
+ border: 1px solid black;
+ }
+ #first {
+ width: 200px;
+ height: 108px;
+ background-color: red;
+ }
+ #second {
+ width: 200px;
+ height: 692px;
+ background-color: green;
+ }
+ </style>
+</head>
+<body>
+ <div id="frame">
+ <div id="first"></div>
+ <div id="second"></div>
+ </div>
+<script type="application/javascript">
+async function test() {
+ var utils = SpecialPowers.DOMWindowUtils;
+
+ var elm = document.querySelector("#frame");
+ // Set a zero-margin displayport to ensure that the element is async-scrollable
+ utils.setDisplayPortMarginsForElement(0, 0, 0, 0, elm, 0);
+ elm.scrollTop = 0;
+
+ // Take over control of the refresh driver and don't allow a layer
+ // transaction until the main thread and APZ have processed two different
+ // scrolls.
+ await promiseApzFlushedRepaints();
+ utils.advanceTimeAndRefresh(0);
+
+ // Scroll instantly on the main thread by (0, 100).
+ elm.scrollBy(0, 100);
+
+ // We are not using `scroll-behavior`
+ is(elm.scrollTop, 100, "the main thread scroll should be instant");
+
+ // Dispatch a wheel event to have APZ scroll by (0, 8). Wait for the wheel
+ // event to ensure that the APZ has processed the scroll.
+ await promiseNativeWheelAndWaitForWheelEvent(elm, 40, 40, 0, -8);
+
+ // APZ should be handling the wheel scroll
+ is(elm.scrollTop, 100, "the wheel scroll should be handled by APZ");
+
+ // Restore control of the refresh driver, allowing the main thread to send a
+ // layer transaction containing the (0, 100) scroll.
+ utils.restoreNormalRefresh();
+
+ // Wait for all paints to finish and for the main thread to receive pending
+ // repaint requests with the scroll offset from the wheel event.
+ await promiseApzFlushedRepaints();
+
+ // The main thread scroll should not have overidden the APZ scroll, and we
+ // should see the effects of both scrolls.
+ ok(elm.scrollTop > 100, `expected element.scrollTop > 100. got element.scrollTop = ${elm.scrollTop}`);
+}
+
+if (isApzEnabled()) {
+ SimpleTest.waitForExplicitFinish();
+ // Receiving a relative scroll offset update can cause scroll animations to
+ // be cancelled. This should be fixed, but for now we can still test this
+ // by disabling smooth scrolling.
+ pushPrefs([["general.smoothScroll", false]])
+ .then(waitUntilApzStable)
+ .then(test)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+}
+
+</script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_scroll_inactive_bug1190112.html b/gfx/layers/apz/test/mochitest/test_scroll_inactive_bug1190112.html
new file mode 100644
index 0000000000..de54cf93fe
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_scroll_inactive_bug1190112.html
@@ -0,0 +1,553 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test scrolling flattened inactive frames</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+<style>
+p {
+ width:200px;
+ height:200px;
+ border:solid 1px black;
+ overflow:auto;
+}
+</style>
+</head>
+<body>
+<div id="iframe-body" style="overflow: auto; height: 1000px">
+<hr>
+<hr>
+<hr>
+<p>
+1 <br>
+2 <br>
+3 <br>
+4 <br>
+5 <br>
+6 <br>
+7 <br>
+8 <br>
+9 <br>
+10 <br>
+11 <br>
+12 <br>
+13 <br>
+14 <br>
+15 <br>
+16 <br>
+17 <br>
+18 <br>
+19 <br>
+20 <br>
+21 <br>
+22 <br>
+23 <br>
+24 <br>
+25 <br>
+26 <br>
+27 <br>
+28 <br>
+29 <br>
+30 <br>
+31 <br>
+32 <br>
+33 <br>
+34 <br>
+35 <br>
+36 <br>
+37 <br>
+38 <br>
+39 <br>
+40 <br>
+
+</p><p id="subframe">
+1 <br>
+2 <br>
+3 <br>
+4 <br>
+5 <br>
+6 <br>
+7 <br>
+8 <br>
+9 <br>
+10 <br>
+11 <br>
+12 <br>
+13 <br>
+14 <br>
+15 <br>
+16 <br>
+17 <br>
+18 <br>
+19 <br>
+20 <br>
+21 <br>
+22 <br>
+23 <br>
+24 <br>
+25 <br>
+26 <br>
+27 <br>
+28 <br>
+29 <br>
+30 <br>
+31 <br>
+32 <br>
+33 <br>
+34 <br>
+35 <br>
+36 <br>
+37 <br>
+38 <br>
+39 <br>
+40 <br>
+
+</p><p>
+1 <br>
+2 <br>
+3 <br>
+4 <br>
+5 <br>
+6 <br>
+7 <br>
+8 <br>
+9 <br>
+10 <br>
+11 <br>
+12 <br>
+13 <br>
+14 <br>
+15 <br>
+16 <br>
+17 <br>
+18 <br>
+19 <br>
+20 <br>
+21 <br>
+22 <br>
+23 <br>
+24 <br>
+25 <br>
+26 <br>
+27 <br>
+28 <br>
+29 <br>
+30 <br>
+31 <br>
+32 <br>
+33 <br>
+34 <br>
+35 <br>
+36 <br>
+37 <br>
+38 <br>
+39 <br>
+40 <br>
+
+</p><p>
+1 <br>
+2 <br>
+3 <br>
+4 <br>
+5 <br>
+6 <br>
+7 <br>
+8 <br>
+9 <br>
+10 <br>
+11 <br>
+12 <br>
+13 <br>
+14 <br>
+15 <br>
+16 <br>
+17 <br>
+18 <br>
+19 <br>
+20 <br>
+21 <br>
+22 <br>
+23 <br>
+24 <br>
+25 <br>
+26 <br>
+27 <br>
+28 <br>
+29 <br>
+30 <br>
+31 <br>
+32 <br>
+33 <br>
+34 <br>
+35 <br>
+36 <br>
+37 <br>
+38 <br>
+39 <br>
+40 <br>
+
+</p><p>
+1 <br>
+2 <br>
+3 <br>
+4 <br>
+5 <br>
+6 <br>
+7 <br>
+8 <br>
+9 <br>
+10 <br>
+11 <br>
+12 <br>
+13 <br>
+14 <br>
+15 <br>
+16 <br>
+17 <br>
+18 <br>
+19 <br>
+20 <br>
+21 <br>
+22 <br>
+23 <br>
+24 <br>
+25 <br>
+26 <br>
+27 <br>
+28 <br>
+29 <br>
+30 <br>
+31 <br>
+32 <br>
+33 <br>
+34 <br>
+35 <br>
+36 <br>
+37 <br>
+38 <br>
+39 <br>
+40 <br>
+
+</p><p>
+1 <br>
+2 <br>
+3 <br>
+4 <br>
+5 <br>
+6 <br>
+7 <br>
+8 <br>
+9 <br>
+10 <br>
+11 <br>
+12 <br>
+13 <br>
+14 <br>
+15 <br>
+16 <br>
+17 <br>
+18 <br>
+19 <br>
+20 <br>
+21 <br>
+22 <br>
+23 <br>
+24 <br>
+25 <br>
+26 <br>
+27 <br>
+28 <br>
+29 <br>
+30 <br>
+31 <br>
+32 <br>
+33 <br>
+34 <br>
+35 <br>
+36 <br>
+37 <br>
+38 <br>
+39 <br>
+40 <br>
+
+</p><p>
+1 <br>
+2 <br>
+3 <br>
+4 <br>
+5 <br>
+6 <br>
+7 <br>
+8 <br>
+9 <br>
+10 <br>
+11 <br>
+12 <br>
+13 <br>
+14 <br>
+15 <br>
+16 <br>
+17 <br>
+18 <br>
+19 <br>
+20 <br>
+21 <br>
+22 <br>
+23 <br>
+24 <br>
+25 <br>
+26 <br>
+27 <br>
+28 <br>
+29 <br>
+30 <br>
+31 <br>
+32 <br>
+33 <br>
+34 <br>
+35 <br>
+36 <br>
+37 <br>
+38 <br>
+39 <br>
+40 <br>
+
+</p><p>
+1 <br>
+2 <br>
+3 <br>
+4 <br>
+5 <br>
+6 <br>
+7 <br>
+8 <br>
+9 <br>
+10 <br>
+11 <br>
+12 <br>
+13 <br>
+14 <br>
+15 <br>
+16 <br>
+17 <br>
+18 <br>
+19 <br>
+20 <br>
+21 <br>
+22 <br>
+23 <br>
+24 <br>
+25 <br>
+26 <br>
+27 <br>
+28 <br>
+29 <br>
+30 <br>
+31 <br>
+32 <br>
+33 <br>
+34 <br>
+35 <br>
+36 <br>
+37 <br>
+38 <br>
+39 <br>
+40 <br>
+
+</p><p>
+1 <br>
+2 <br>
+3 <br>
+4 <br>
+5 <br>
+6 <br>
+7 <br>
+8 <br>
+9 <br>
+10 <br>
+11 <br>
+12 <br>
+13 <br>
+14 <br>
+15 <br>
+16 <br>
+17 <br>
+18 <br>
+19 <br>
+20 <br>
+21 <br>
+22 <br>
+23 <br>
+24 <br>
+25 <br>
+26 <br>
+27 <br>
+28 <br>
+29 <br>
+30 <br>
+31 <br>
+32 <br>
+33 <br>
+34 <br>
+35 <br>
+36 <br>
+37 <br>
+38 <br>
+39 <br>
+40 <br>
+
+</p><p>
+1 <br>
+2 <br>
+3 <br>
+4 <br>
+5 <br>
+6 <br>
+7 <br>
+8 <br>
+9 <br>
+10 <br>
+11 <br>
+12 <br>
+13 <br>
+14 <br>
+15 <br>
+16 <br>
+17 <br>
+18 <br>
+19 <br>
+20 <br>
+21 <br>
+22 <br>
+23 <br>
+24 <br>
+25 <br>
+26 <br>
+27 <br>
+28 <br>
+29 <br>
+30 <br>
+31 <br>
+32 <br>
+33 <br>
+34 <br>
+35 <br>
+36 <br>
+37 <br>
+38 <br>
+39 <br>
+40 <br>
+
+</p><p>
+1 <br>
+2 <br>
+3 <br>
+4 <br>
+5 <br>
+6 <br>
+7 <br>
+8 <br>
+9 <br>
+10 <br>
+11 <br>
+12 <br>
+13 <br>
+14 <br>
+15 <br>
+16 <br>
+17 <br>
+18 <br>
+19 <br>
+20 <br>
+21 <br>
+22 <br>
+23 <br>
+24 <br>
+25 <br>
+26 <br>
+27 <br>
+28 <br>
+29 <br>
+30 <br>
+31 <br>
+32 <br>
+33 <br>
+34 <br>
+35 <br>
+36 <br>
+37 <br>
+38 <br>
+39 <br>
+40 <br>
+
+</p>
+</div>
+<script clss="testbody" type="text/javascript">
+function ScrollTops() {
+ this.outerScrollTop = document.getElementById("iframe-body").scrollTop;
+ this.innerScrollTop = document.getElementById("subframe").scrollTop;
+}
+
+var DefaultEvent = {
+ deltaMode: WheelEvent.DOM_DELTA_LINE,
+ deltaX: 0, deltaY: 1,
+ lineOrPageDeltaX: 0, lineOrPageDeltaY: 1,
+};
+
+async function test() {
+ var subframe = document.getElementById("subframe");
+ var oldpos = new ScrollTops();
+ await new Promise(resolve => {
+ sendWheelAndPaint(subframe, 10, 10, DefaultEvent, resolve);
+ });
+
+ var newpos = new ScrollTops();
+ ok(oldpos.outerScrollTop == newpos.outerScrollTop, "viewport should not have scrolled");
+ ok(oldpos.innerScrollTop != newpos.innerScrollTop, "subframe should have scrolled");
+ oldpos = newpos;
+
+ // Scroll outer
+ var outer = document.getElementById("iframe-body");
+ await new Promise(resolve => {
+ sendWheelAndPaint(outer, 20, 5, DefaultEvent, resolve);
+ });
+
+ newpos = new ScrollTops();
+ ok(oldpos.outerScrollTop != newpos.outerScrollTop, "viewport should have scrolled");
+ ok(oldpos.innerScrollTop == newpos.innerScrollTop, "subframe should not have scrolled");
+ oldpos = newpos;
+
+ // Scroll inner again
+ // Tick the refresh driver once to make sure the compositor has sent the
+ // updated scroll offset for the outer scroller to WebRender, so that the
+ // hit-test in sendWheelAndPaint takes it into account. (This isn't needed
+ // if using non-WR layers, but doesn't hurt either).
+ var dwu = SpecialPowers.getDOMWindowUtils(window);
+ dwu.advanceTimeAndRefresh(16);
+ dwu.restoreNormalRefresh();
+
+ await new Promise(resolve => {
+ sendWheelAndPaint(subframe, 10, 10, DefaultEvent, resolve);
+ });
+
+ newpos = new ScrollTops();
+ ok(oldpos.outerScrollTop == newpos.outerScrollTop, "viewport should not have scrolled");
+ ok(oldpos.innerScrollTop != newpos.innerScrollTop, "subframe should have scrolled");
+}
+
+SimpleTest.waitForExplicitFinish();
+
+pushPrefs([["general.smoothScroll", false],
+ ["mousewheel.transaction.timeout", 0],
+ ["mousewheel.transaction.ignoremovedelay", 0],
+ ["test.events.async.enabled", true]])
+.then(waitUntilApzStable)
+.then(test)
+.then(SimpleTest.finish, SimpleTest.finishWithFailure);
+
+</script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_scroll_inactive_flattened_frame.html b/gfx/layers/apz/test/mochitest/test_scroll_inactive_flattened_frame.html
new file mode 100644
index 0000000000..47207cbb9f
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_scroll_inactive_flattened_frame.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test scrolling flattened inactive frames</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<div id="container" style="height: 300px; width: 600px; overflow: auto; background: yellow">
+ <div id="outer" style="height: 400px; width: 500px; overflow: auto; background: black">
+ <div id="inner" style="mix-blend-mode: screen; height: 800px; overflow: auto; background: purple">
+ </div>
+ </div>
+</div>
+<script class="testbody" type="text/javascript">
+async function test() {
+ var container = document.getElementById("container");
+ var outer = document.getElementById("outer");
+ var inner = document.getElementById("inner");
+ var outerScrollTop = outer.scrollTop;
+ var containerScrollTop = container.scrollTop;
+ var event = {
+ deltaMode: WheelEvent.DOM_DELTA_LINE,
+ deltaX: 0,
+ deltaY: 10,
+ lineOrPageDeltaX: 0,
+ lineOrPageDeltaY: 10,
+ };
+ await new Promise(resolve => {
+ sendWheelAndPaint(inner, 20, 30, event, resolve);
+ });
+ ok(container.scrollTop == containerScrollTop, "container scrollframe should not have scrolled");
+ ok(outer.scrollTop > outerScrollTop, "nested scrollframe should have scrolled");
+}
+
+SimpleTest.waitForExplicitFinish();
+
+pushPrefs([["general.smoothScroll", false],
+ ["mousewheel.transaction.timeout", 1000000],
+ ["test.events.async.enabled", true]])
+.then(waitUntilApzStable)
+.then(test)
+.then(SimpleTest.finish, SimpleTest.finishWithFailure);
+
+</script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_scroll_subframe_scrollbar.html b/gfx/layers/apz/test/mochitest/test_scroll_subframe_scrollbar.html
new file mode 100644
index 0000000000..10d53e9d04
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_scroll_subframe_scrollbar.html
@@ -0,0 +1,116 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test scrolling subframe scrollbars</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+<style>
+p {
+ width:200px;
+ height:200px;
+ border:solid 1px black;
+}
+</style>
+</head>
+<body>
+<p id="subframe">
+1 <br>
+2 <br>
+3 <br>
+4 <br>
+5 <br>
+6 <br>
+7 <br>
+8 <br>
+9 <br>
+10 <br>
+11 <br>
+12 <br>
+13 <br>
+14 <br>
+15 <br>
+16 <br>
+17 <br>
+18 <br>
+19 <br>
+20 <br>
+21 <br>
+22 <br>
+23 <br>
+24 <br>
+25 <br>
+26 <br>
+27 <br>
+28 <br>
+29 <br>
+30 <br>
+31 <br>
+32 <br>
+33 <br>
+34 <br>
+35 <br>
+36 <br>
+37 <br>
+38 <br>
+39 <br>
+40 <br>
+</p>
+<script clss="testbody" type="text/javascript">
+
+var DefaultEvent = {
+ deltaMode: WheelEvent.DOM_DELTA_LINE,
+ deltaX: 0, deltaY: 1,
+ lineOrPageDeltaX: 0, lineOrPageDeltaY: 1,
+};
+
+var ScrollbarWidth = 0;
+
+async function test() {
+ var subframe = document.getElementById("subframe");
+ var oldClientWidth = subframe.clientWidth;
+
+ subframe.style.overflow = "auto";
+ subframe.getBoundingClientRect();
+
+ await promiseAllPaintsDone(null, /*flush=*/true);
+
+ ScrollbarWidth = oldClientWidth - subframe.clientWidth;
+ if (!ScrollbarWidth) {
+ // Probably we have overlay scrollbars - abort the test.
+ ok(true, "overlay scrollbars - skipping test");
+ return;
+ }
+
+ ok(subframe.scrollHeight > subframe.clientHeight, "subframe should have scrollable content");
+
+ // Send a wheel event roughly to where we think the trackbar is. We pick a
+ // point at the bottom, in the middle of the trackbar, where the slider is
+ // unlikely to be (since it starts at the top).
+ var posX = subframe.clientWidth + (ScrollbarWidth / 2);
+ var posY = subframe.clientHeight - 20;
+
+ var oldScrollTop = subframe.scrollTop;
+
+ await new Promise(resolve => {
+ sendWheelAndPaint(subframe, posX, posY, DefaultEvent, resolve);
+ });
+
+ ok(subframe.scrollTop > oldScrollTop, "subframe should have scrolled");
+}
+
+SimpleTest.waitForExplicitFinish();
+
+pushPrefs([["general.smoothScroll", false],
+ ["mousewheel.transaction.timeout", 0],
+ ["mousewheel.transaction.ignoremovedelay", 0],
+ ["test.events.async.enabled", true]])
+.then(waitUntilApzStable)
+.then(test)
+.then(SimpleTest.finish, SimpleTest.finishWithFailure);
+
+</script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_smoothness.html b/gfx/layers/apz/test/mochitest/test_smoothness.html
new file mode 100644
index 0000000000..6db80d365e
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_smoothness.html
@@ -0,0 +1,71 @@
+<html>
+<head>
+ <title>Test Frame Uniformity While Scrolling</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+
+ <style>
+ #content {
+ height: 5000px;
+ background: repeating-linear-gradient(#EEE, #EEE 100px, #DDD 100px, #DDD 200px);
+ }
+ </style>
+ <script type="text/javascript">
+ var scrollEvents = 100;
+ var i = 0;
+ // Scroll points
+ var x = 100;
+ var y = 150;
+
+ SimpleTest.waitForExplicitFinish();
+ var utils = SpecialPowers.getDOMWindowUtils(window);
+
+ async function sendScrollEvent(aRafTimestamp) {
+ var scrollDiv = document.getElementById("content");
+
+ if (i < scrollEvents) {
+ i++;
+ // Scroll diff
+ var dx = 0;
+ var dy = -10; // Negative to scroll down
+ await synthesizeNativeWheel(scrollDiv, x, y, dx, dy);
+ window.requestAnimationFrame(sendScrollEvent);
+ } else {
+ // Locally, with silk and apz + e10s, retina 15" mbp usually get ~1.0 - 1.5
+ // w/o silk + e10s + apz, I get up to 7. Lower is better.
+ // Windows, I get ~3. Values are not valid w/o hardware vsync
+ var uniformities = utils.getFrameUniformityTestData();
+ for (var j = 0; j < uniformities.layerUniformities.length; j++) {
+ var layerResult = uniformities.layerUniformities[j];
+ var layerAddr = layerResult.layerAddress;
+ var uniformity = layerResult.frameUniformity;
+ var msg = "Layer: " + layerAddr.toString(16) + " Uniformity: " + uniformity;
+ SimpleTest.ok((uniformity >= 0) && (uniformity < 4.0), msg);
+ }
+ SimpleTest.finish();
+ }
+ }
+
+ function startTest() {
+ window.requestAnimationFrame(sendScrollEvent);
+ }
+
+ if (!isApzEnabled()) {
+ SimpleTest.ok(true, "APZ not enabled, skipping test");
+ SimpleTest.finish();
+ }
+
+ waitUntilApzStable()
+ .then(() => pushPrefs([["gfx.vsync.collect-scroll-transforms", true]]))
+ .then(startTest);
+ </script>
+</head>
+
+<body>
+ <div id="content">
+ </div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_touch_listeners_impacting_wheel.html b/gfx/layers/apz/test/mochitest/test_touch_listeners_impacting_wheel.html
new file mode 100644
index 0000000000..71147d5238
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_touch_listeners_impacting_wheel.html
@@ -0,0 +1,119 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1203140
+-->
+<head>
+ <title>Test for Bug 1203140</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <style>
+ </style>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1203140">Mozilla Bug 1203140</a>
+<p id="display"></p>
+<div id="content" style="overflow-y:scroll; height: 400px">
+ <p>The box below has a touch listener and a passive wheel listener. With touch events disabled, APZ shouldn't wait for any listeners.</p>
+ <div id="box" style="width: 200px; height: 200px; background-color: blue"></div>
+ <div style="height: 1000px; width: 10px">Div to make 'content' scrollable</div>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+const kResponseTimeoutMs = 2 * 60 * 1000; // 2 minutes
+
+function takeSnapshots(e) {
+ // Grab some snapshots, and make sure some of them are different (i.e. check
+ // the page is scrolling in the compositor, concurrently with this wheel
+ // listener running).
+ // Note that we want this function to take less time than the content response
+ // timeout, otherwise the scrolling will start even if we haven't returned,
+ // and that would invalidate purpose of the test.
+ var start = Date.now();
+ var lastSnapshot = null;
+ var success = false;
+
+ // Get the position of the 'content' div relative to the screen
+ var rect = rectRelativeToScreen(document.getElementById("content"));
+
+ for (var i = 0; i < 10; i++) {
+ SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(16);
+ var snapshot = getSnapshot(rect);
+ // dump("Took snapshot " + snapshot + "\n"); // this might help with debugging
+
+ if (lastSnapshot && lastSnapshot != snapshot) {
+ ok(true, "Found some different pixels in snapshot " + i + " compared to previous");
+ success = true;
+ }
+ lastSnapshot = snapshot;
+ }
+ ok(success, "Found some snapshots that were different");
+ ok((Date.now() - start) < kResponseTimeoutMs, "Snapshotting ran quickly enough");
+
+ // Until now, no scroll events will have been dispatched to content. That's
+ // because scroll events are dispatched on the main thread, which we've been
+ // hogging with the code above. At this point we restore the normal refresh
+ // behaviour and let the main thread go back to C++ code, so the scroll events
+ // fire and we unwind from the main test continuation.
+ SpecialPowers.DOMWindowUtils.restoreNormalRefresh();
+}
+
+async function test() {
+ var box = document.getElementById("box");
+
+ // Ensure the div is layerized by scrolling it
+ await promiseMoveMouseAndScrollWheelOver(box, 10, 10);
+
+ box.addEventListener("touchstart", function(e) {
+ ok(false, "This should never be run");
+ });
+ box.addEventListener("wheel", takeSnapshots, { capture: false, passive: true });
+
+ // Let the event regions and layerization propagate to the APZ
+ await promiseAllPaintsDone();
+ await promiseOnlyApzControllerFlushed();
+
+ await promiseNativeMouseEventWithAPZAndWaitForEvent({
+ type: "mousemove",
+ target: box,
+ offsetX: 10,
+ offsetY: 10,
+ });
+
+ // Take over control of the refresh driver and compositor
+ var utils = SpecialPowers.DOMWindowUtils;
+ utils.advanceTimeAndRefresh(0);
+
+ // Trigger an APZ scroll using a wheel event. If APZ is waiting for a
+ // content response, it will wait for takeSnapshots to finish running before
+ // it starts scrolling, which will cause the checks in takeSnapshots to fail.
+ await promiseNativeWheelAndWaitForScrollEvent(box, 10, 10, 0, -50);
+}
+
+if (isApzEnabled()) {
+ SimpleTest.waitForExplicitFinish();
+ // Disable touch events, so that APZ knows not to wait for touch listeners.
+ // Also explicitly set the content response timeout, so we know how long it
+ // is (see comment in takeSnapshots).
+ // Finally, enable smooth scrolling, so that the wheel-scroll we do as part
+ // of the test triggers an APZ animation rather than doing an instant scroll.
+ // Note that this pref doesn't work for the synthesized wheel events on OS X,
+ // those are hard-coded to be instant scrolls.
+ pushPrefs([["dom.w3c_touch_events.enabled", 0],
+ ["apz.content_response_timeout", kResponseTimeoutMs],
+ ["general.smoothscroll", true]])
+ .then(waitUntilApzStable)
+ .then(test)
+ .then(SimpleTest.finish, SimpleTest.finishWithFailure);
+}
+
+</script>
+</pre>
+
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_wheel_scroll.html b/gfx/layers/apz/test/mochitest/test_wheel_scroll.html
new file mode 100644
index 0000000000..1b50c223ed
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_wheel_scroll.html
@@ -0,0 +1,109 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1013412
+https://bugzilla.mozilla.org/show_bug.cgi?id=1168182
+-->
+<head>
+ <title>Test for Bug 1013412 and 1168182</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <style>
+ #content {
+ height: 800px;
+ overflow: scroll;
+ }
+
+ #scroller {
+ height: 2000px;
+ background: repeating-linear-gradient(#EEE, #EEE 100px, #DDD 100px, #DDD 200px);
+ }
+
+ #scrollbox {
+ margin-top: 200px;
+ width: 500px;
+ height: 500px;
+ border-radius: 250px;
+ box-shadow: inset 0 0 0 60px #555;
+ background: #777;
+ }
+
+ #circle {
+ position: relative;
+ left: 240px;
+ top: 20px;
+ border: 10px solid white;
+ border-radius: 10px;
+ width: 0px;
+ height: 0px;
+ transform-origin: 10px 230px;
+ will-change: transform;
+ }
+ </style>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1013412">Mozilla Bug 1013412</a>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1168182">Mozilla Bug 1168182</a>
+<p id="display"></p>
+<div id="content">
+ <p>Scrolling the page should be async, but scrolling over the dark circle should not scroll the page and instead rotate the white ball.</p>
+ <div id="scroller">
+ <div id="scrollbox">
+ <div id="circle"></div>
+ </div>
+ </div>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+var rotation = 0;
+var rotationAdjusted = false;
+
+var incrementForMode = function(mode) {
+ switch (mode) {
+ case WheelEvent.DOM_DELTA_PIXEL: return 1;
+ case WheelEvent.DOM_DELTA_LINE: return 15;
+ case WheelEvent.DOM_DELTA_PAGE: return 400;
+ }
+ return 0;
+};
+
+document.getElementById("scrollbox").addEventListener("wheel", function(e) {
+ rotation += e.deltaY * incrementForMode(e.deltaMode) * 0.2;
+ document.getElementById("circle").style.transform = "rotate(" + rotation + "deg)";
+ rotationAdjusted = true;
+ e.preventDefault();
+});
+
+async function test() {
+ var content = document.getElementById("content");
+ // enough iterations that we would scroll to the bottom of 'content'
+ for (let i = 0; i < 600 && content.scrollTop != content.scrollTopMax; i++) {
+ await promiseNativeWheelAndWaitForWheelEvent(content, 100, 150, 0, -5);
+ }
+ is(content.scrollTop > 0, true, "We should have scrolled down somewhat");
+ is(content.scrollTop, content.scrollTopMax, "We should have scrolled to the bottom of the scrollframe");
+ is(rotationAdjusted, false, "The rotation should not have been adjusted");
+}
+
+SimpleTest.waitForExplicitFinish();
+
+// If we allow smooth scrolling the "smooth" scrolling may cause the page to
+// glide past the scrollbox (which is supposed to stop the scrolling) and so
+// we might end up at the bottom of the page.
+pushPrefs([["general.smoothScroll", false],
+ ["mousewheel.transaction.timeout", 100000],
+ ["dom.event.wheel-event-groups.enabled", true]])
+.then(waitUntilApzStable)
+.then(test)
+.then(SimpleTest.finish, SimpleTest.finishWithFailure);
+
+</script>
+</pre>
+
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_wheel_transactions.html b/gfx/layers/apz/test/mochitest/test_wheel_transactions.html
new file mode 100644
index 0000000000..f015ea20be
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_wheel_transactions.html
@@ -0,0 +1,150 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1175585
+-->
+<head>
+ <title>Test for Bug 1175585</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <style>
+ #outer-frame {
+ height: 500px;
+ overflow: scroll;
+ background: repeating-linear-gradient(#CCC, #CCC 100px, #BBB 100px, #BBB 200px);
+ }
+ #inner-frame {
+ margin-top: 25%;
+ height: 200%;
+ width: 75%;
+ overflow: scroll;
+ }
+ #inner-content {
+ height: 200%;
+ width: 200%;
+ background: repeating-linear-gradient(#EEE, #EEE 100px, #DDD 100px, #DDD 200px);
+ }
+ </style>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1175585">APZ wheel transactions test</a>
+<p id="display"></p>
+<div id="outer-frame">
+ <div id="inner-frame">
+ <div id="inner-content"></div>
+ </div>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+async function scrollWheelOver(element, deltaY) {
+ await promiseNativeWheelAndWaitForScrollEvent(element, 10, 10, 0, deltaY);
+}
+
+async function test() {
+ var outer = document.getElementById("outer-frame");
+ var inner = document.getElementById("inner-frame");
+
+ // Register a wheel event listener that records the target of
+ // the last wheel event, so that we can make assertions about it.
+ let lastWheelTarget;
+ let firstWheelTarget;
+ let wheelEventOccurred = false;
+ var wheelTargetRecorder = function(e) {
+ if (!wheelEventOccurred) {
+ firstWheelTarget = e.target;
+ wheelEventOccurred = true;
+ }
+ lastWheelTarget = e.target;
+ };
+ window.addEventListener("wheel", wheelTargetRecorder);
+
+ // Scroll |outer| to the bottom.
+ while (outer.scrollTop < outer.scrollTopMax) {
+ await scrollWheelOver(outer, -10);
+ }
+
+ is(lastWheelTarget, firstWheelTarget,
+ "target " + lastWheelTarget.id + " should be " + lastWheelTarget.id);
+ window.removeEventListener("wheel", wheelTargetRecorder);
+
+ // Immediately after, scroll it back up a bit.
+ await scrollWheelOver(outer, 10);
+
+ // Check that it was |outer| that scrolled back, and |inner| didn't
+ // scroll at all, as all the above scrolls should be in the same
+ // transaction.
+ ok(outer.scrollTop < outer.scrollTopMax, "'outer' should have scrolled back a bit");
+ is(inner.scrollTop, 0, "'inner' should not have scrolled");
+
+ // The next part of the test is related to the transaction timeout.
+ // Turn it down a bit so waiting for the timeout to elapse doesn't
+ // slow down the test harness too much.
+ var timeout = 5;
+ await SpecialPowers.pushPrefEnv({"set": [["mousewheel.transaction.timeout", timeout]]});
+ SimpleTest.requestFlakyTimeout("we are testing code that measures actual elapsed time between two events");
+
+ // Scroll up a bit more. It's still |outer| scrolling because
+ // |inner| is still scrolled all the way to the top.
+ await scrollWheelOver(outer, 10);
+
+ // Wait for the transaction timeout to elapse.
+ // timeout * 5 is used to make it less likely that the timeout is less than
+ // the system timestamp resolution
+ await SpecialPowers.promiseTimeout(timeout * 5);
+
+ // Now scroll down. The transaction having timed out, the event
+ // should pick up a new target, and that should be |inner|.
+ await scrollWheelOver(outer, -10);
+ ok(inner.scrollTop > 0, "'inner' should have been scrolled");
+
+ // Finally, test scroll handoff after a timeout.
+
+ // Continue scrolling |inner| down to the bottom.
+ var prevScrollTop = inner.scrollTop;
+ while (inner.scrollTop < inner.scrollTopMax) {
+ await scrollWheelOver(outer, -10);
+ // Avoid a failure getting us into an infinite loop.
+ ok(inner.scrollTop > prevScrollTop, "scrolling down should increase scrollTop");
+ prevScrollTop = inner.scrollTop;
+ }
+
+ // Wait for the transaction timeout to elapse.
+ // timeout * 5 is used to make it less likely that the timeout is less than
+ // the system timestamp resolution
+ await SpecialPowers.promiseTimeout(timeout * 5);
+
+ // Continued downward scrolling should scroll |outer| to the bottom.
+ prevScrollTop = outer.scrollTop;
+ while (outer.scrollTop < outer.scrollTopMax) {
+ await scrollWheelOver(outer, -10);
+ // Avoid a failure getting us into an infinite loop.
+ ok(outer.scrollTop > prevScrollTop, "scrolling down should increase scrollTop");
+ prevScrollTop = outer.scrollTop;
+ }
+}
+
+SimpleTest.waitForExplicitFinish();
+
+// Disable smooth scrolling because it makes the test flaky (we don't have a good
+// way of detecting when the scrolling is finished).
+// Also, on macOS, force the native events to be wheel inputs rather than pan
+// inputs since this test is specifically testing things related to wheel
+// transactions.
+pushPrefs([["general.smoothScroll", false],
+ ["apz.test.mac.synth_wheel_input", true],
+ ["mousewheel.transaction.timeout", 1500],
+ ["dom.event.wheel-event-groups.enabled", true]])
+.then(waitUntilApzStable)
+.then(test)
+.then(SimpleTest.finish, SimpleTest.finishWithFailure);
+
+</script>
+</pre>
+
+</body>
+</html>
diff --git a/gfx/layers/apz/test/reftest/async-scrollbar-1-h-ref.html b/gfx/layers/apz/test/reftest/async-scrollbar-1-h-ref.html
new file mode 100644
index 0000000000..62d99b6dfe
--- /dev/null
+++ b/gfx/layers/apz/test/reftest/async-scrollbar-1-h-ref.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html class="reftest-wait"><head>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+</head>
+<body onload="scrollTo(450,0); document.documentElement.classList.remove('reftest-wait')">
+<div style="width: 9000px; height: 10px; background: white;"></div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/reftest/async-scrollbar-1-h-rtl-ref.html b/gfx/layers/apz/test/reftest/async-scrollbar-1-h-rtl-ref.html
new file mode 100644
index 0000000000..e40ac8debb
--- /dev/null
+++ b/gfx/layers/apz/test/reftest/async-scrollbar-1-h-rtl-ref.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html class="reftest-wait"><head>
+<meta name="viewport" content="width=device-width">
+<style> html { direction: rtl; } </style>
+</head>
+<body onload="scrollTo(-450,0); document.documentElement.classList.remove('reftest-wait')">
+<div style="width: 9000px; height: 10px; background: white;"></div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/reftest/async-scrollbar-1-h-rtl.html b/gfx/layers/apz/test/reftest/async-scrollbar-1-h-rtl.html
new file mode 100644
index 0000000000..81f7f77817
--- /dev/null
+++ b/gfx/layers/apz/test/reftest/async-scrollbar-1-h-rtl.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html class="reftest-wait"
+ reftest-async-scroll
+ reftest-async-scroll-x="-449" reftest-async-scroll-y="0"><head>
+<meta name="viewport" content="width=device-width">
+<style> html { direction: rtl; } </style>
+</head>
+<!-- Doing scrollTo(-1,0) is to activate the right arrow in the scrollbar
+ for non-overlay scrollbar environments -->
+<body onload="scrollTo(-1,0); document.documentElement.classList.remove('reftest-wait')">
+<div style="width: 9000px; height: 10px; background: white"></div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/reftest/async-scrollbar-1-h.html b/gfx/layers/apz/test/reftest/async-scrollbar-1-h.html
new file mode 100644
index 0000000000..5d30584acd
--- /dev/null
+++ b/gfx/layers/apz/test/reftest/async-scrollbar-1-h.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html class="reftest-wait"
+ reftest-async-scroll
+ reftest-async-scroll-x="449" reftest-async-scroll-y="0"><head>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+</head>
+<!-- Doing scrollTo(1,0) is to activate the left arrow in the scrollbar
+ for non-overlay scrollbar environments -->
+<body onload="scrollTo(1,0); document.documentElement.classList.remove('reftest-wait')">
+<div style="width: 9000px; height: 10px; background: white"></div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/reftest/async-scrollbar-1-v-fullzoom-ref.html b/gfx/layers/apz/test/reftest/async-scrollbar-1-v-fullzoom-ref.html
new file mode 100644
index 0000000000..6226a95070
--- /dev/null
+++ b/gfx/layers/apz/test/reftest/async-scrollbar-1-v-fullzoom-ref.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html class="reftest-wait" reftest-zoom="0.67"><head>
+<meta name="viewport" content="width=device-width">
+</head>
+<body onload="scrollTo(0,10000); document.documentElement.classList.remove('reftest-wait')">
+<div style="width: 10px; height: 20000px; background: white;"></div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/reftest/async-scrollbar-1-v-fullzoom.html b/gfx/layers/apz/test/reftest/async-scrollbar-1-v-fullzoom.html
new file mode 100644
index 0000000000..50c6d0854d
--- /dev/null
+++ b/gfx/layers/apz/test/reftest/async-scrollbar-1-v-fullzoom.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html class="reftest-wait"
+ reftest-async-scroll
+ reftest-async-scroll-x="0" reftest-async-scroll-y="9999"
+ reftest-zoom="0.67">
+<head>
+<meta name="viewport" content="width=device-width">
+</head>
+<!-- Doing scrollTo(0,1) is to activate the up arrow in the scrollbar
+ for non-overlay scrollbar environments -->
+<body onload="scrollTo(0,1); document.documentElement.classList.remove('reftest-wait')">
+<div style="width: 10px; height: 20000px; background: white;"></div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/reftest/async-scrollbar-1-v-ref.html b/gfx/layers/apz/test/reftest/async-scrollbar-1-v-ref.html
new file mode 100644
index 0000000000..aec5f89cbc
--- /dev/null
+++ b/gfx/layers/apz/test/reftest/async-scrollbar-1-v-ref.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html class="reftest-wait"><head>
+<meta name="viewport" content="width=device-width">
+</head>
+<body onload="scrollTo(0,10000); document.documentElement.classList.remove('reftest-wait')">
+<div style="width: 10px; height: 20000px; background: white;"></div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/reftest/async-scrollbar-1-v-rtl-ref.html b/gfx/layers/apz/test/reftest/async-scrollbar-1-v-rtl-ref.html
new file mode 100644
index 0000000000..81be67146f
--- /dev/null
+++ b/gfx/layers/apz/test/reftest/async-scrollbar-1-v-rtl-ref.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html class="reftest-wait"><head>
+<meta name="viewport" content="width=device-width">
+<style> html { direction: rtl; } </style>
+</head>
+<body onload="scrollTo(0,10000); document.documentElement.classList.remove('reftest-wait')">
+<div style="width: 10px; height: 20000px; background: white;"></div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/reftest/async-scrollbar-1-v-rtl.html b/gfx/layers/apz/test/reftest/async-scrollbar-1-v-rtl.html
new file mode 100644
index 0000000000..24e7705723
--- /dev/null
+++ b/gfx/layers/apz/test/reftest/async-scrollbar-1-v-rtl.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html class="reftest-wait"
+ reftest-async-scroll
+ reftest-async-scroll-x="0" reftest-async-scroll-y="9999"><head>
+<meta name="viewport" content="width=device-width">
+<style> html { direction: rtl; } </style>
+</head>
+<!-- Doing scrollTo(0,1) is to activate the up arrow in the scrollbar
+ for non-overlay scrollbar environments -->
+<body onload="scrollTo(0,1); document.documentElement.classList.remove('reftest-wait')">
+<div style="width: 10px; height: 20000px; background: white;"></div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/reftest/async-scrollbar-1-v.html b/gfx/layers/apz/test/reftest/async-scrollbar-1-v.html
new file mode 100644
index 0000000000..268f3b92e3
--- /dev/null
+++ b/gfx/layers/apz/test/reftest/async-scrollbar-1-v.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html class="reftest-wait"
+ reftest-async-scroll
+ reftest-async-scroll-x="0" reftest-async-scroll-y="9999"><head>
+<meta name="viewport" content="width=device-width">
+</head>
+<!-- Doing scrollTo(0,1) is to activate the up arrow in the scrollbar
+ for non-overlay scrollbar environments -->
+<body onload="scrollTo(0,1); document.documentElement.classList.remove('reftest-wait')">
+<div style="width: 10px; height: 20000px; background: white;"></div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/reftest/async-scrollbar-1-vh-ref.html b/gfx/layers/apz/test/reftest/async-scrollbar-1-vh-ref.html
new file mode 100644
index 0000000000..35922e3253
--- /dev/null
+++ b/gfx/layers/apz/test/reftest/async-scrollbar-1-vh-ref.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html class="reftest-wait"><head>
+<meta name="viewport" content="initial-scale=1,width=device-width">
+</head>
+<body onload="scrollTo(450,8000); document.documentElement.classList.remove('reftest-wait')">
+<div style="width: 9000px; height: 20000px; background: white;"></div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/reftest/async-scrollbar-1-vh-rtl-ref.html b/gfx/layers/apz/test/reftest/async-scrollbar-1-vh-rtl-ref.html
new file mode 100644
index 0000000000..22bf3cf1c8
--- /dev/null
+++ b/gfx/layers/apz/test/reftest/async-scrollbar-1-vh-rtl-ref.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html class="reftest-wait"><head>
+<meta name="viewport" content="initial-scale=1,width=device-width">
+<style> html { direction: rtl; } </style>
+</head>
+<body onload="scrollTo(-450,8000); document.documentElement.classList.remove('reftest-wait')">
+<div style="width: 9000px; height: 20000px; background: white;"></div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/reftest/async-scrollbar-1-vh-rtl.html b/gfx/layers/apz/test/reftest/async-scrollbar-1-vh-rtl.html
new file mode 100644
index 0000000000..09fce0bbe9
--- /dev/null
+++ b/gfx/layers/apz/test/reftest/async-scrollbar-1-vh-rtl.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html class="reftest-wait"
+ reftest-async-scroll
+ reftest-async-scroll-x="-440" reftest-async-scroll-y="7999"><head>
+<meta name="viewport" content="initial-scale=1,width=device-width">
+<style> html { direction: rtl; } </style>
+</head>
+<!-- Doing scrollTo(-10,1) is to activate the right/up arrows in the scrollbars
+ for non-overlay scrollbar environments -->
+<body onload="scrollTo(-10,1); document.documentElement.classList.remove('reftest-wait')">
+<div style="width: 9000px; height: 20000px; background: white;"></div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/reftest/async-scrollbar-1-vh.html b/gfx/layers/apz/test/reftest/async-scrollbar-1-vh.html
new file mode 100644
index 0000000000..a8d28ec414
--- /dev/null
+++ b/gfx/layers/apz/test/reftest/async-scrollbar-1-vh.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html class="reftest-wait"
+ reftest-async-scroll
+ reftest-async-scroll-x="449" reftest-async-scroll-y="7999"><head>
+<meta name="viewport" content="initial-scale=1,width=device-width">
+</head>
+<!-- Doing scrollTo(1,1) is to activate the left/up arrows in the scrollbars
+ for non-overlay scrollbar environments -->
+<body onload="scrollTo(1,1); document.documentElement.classList.remove('reftest-wait')">
+<div style="width: 9000px; height: 20000px; background: white;"></div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/reftest/frame-reconstruction-scroll-clamping-ref.html b/gfx/layers/apz/test/reftest/frame-reconstruction-scroll-clamping-ref.html
new file mode 100644
index 0000000000..3db9f2969e
--- /dev/null
+++ b/gfx/layers/apz/test/reftest/frame-reconstruction-scroll-clamping-ref.html
@@ -0,0 +1,27 @@
+<html>
+<script>
+ function run() {
+ document.body.classList.toggle('noscroll');
+ document.getElementById('spacer').style.height = '100%';
+ // Scroll to the very end, including any fractional pixels
+ document.body.scrollTop = document.body.scrollTopMax + 1;
+ }
+</script>
+<style>
+ html, body {
+ margin: 0;
+ padding: 0;
+ background-color: green;
+ }
+
+ .noscroll {
+ overflow: hidden;
+ height: 100%;
+ }
+</style>
+<body onload="run()">
+ <div id="spacer" style="height: 5000px">
+ This is the top of the page.
+ </div>
+ This is the bottom of the page.
+</body>
diff --git a/gfx/layers/apz/test/reftest/frame-reconstruction-scroll-clamping.html b/gfx/layers/apz/test/reftest/frame-reconstruction-scroll-clamping.html
new file mode 100644
index 0000000000..479363f3fb
--- /dev/null
+++ b/gfx/layers/apz/test/reftest/frame-reconstruction-scroll-clamping.html
@@ -0,0 +1,53 @@
+<html class="reftest-wait">
+<!--
+For bug 1266833; syncing the scroll offset to APZ properly when the scroll
+position is clamped to a smaller value during a frame reconstruction.
+-->
+<script>
+ function run() {
+ document.body.scrollTop = document.body.scrollTopMax;
+
+ // Let the scroll position propagate to APZ before we do the frame
+ // reconstruction. Ideally we would wait for flushApzRepaints here but
+ // we don't have access to DOMWindowUtils in a reftest, so we just wait
+ // 100ms to approximate it. With bug 1266833 fixed, this test should
+ // never fail regardless of what this timeout value is.
+ setTimeout(frameReconstruction, 100);
+ }
+
+ function frameReconstruction() {
+ document.body.classList.toggle('noscroll');
+ document.documentElement.classList.toggle('reconstruct-body');
+ document.getElementById('spacer').style.height = '100%';
+ document.documentElement.classList.remove('reftest-wait');
+ }
+</script>
+<style>
+ html, body {
+ margin: 0;
+ padding: 0;
+ background-color: green;
+ }
+
+ .noscroll {
+ overflow: hidden;
+ height: 100%;
+ }
+
+ /* Toggling this on and off triggers a frame reconstruction on the <body> */
+ html.reconstruct-body::before {
+ top: 0;
+ content: '';
+ display: block;
+ height: 2px;
+ position: absolute;
+ width: 100%;
+ z-index: 99;
+ }
+</style>
+<body onload="setTimeout(run, 0)">
+ <div id="spacer" style="height: 5000px">
+ This is the top of the page.
+ </div>
+ This is the bottom of the page.
+</body>
diff --git a/gfx/layers/apz/test/reftest/iframe-zoomed-child.html b/gfx/layers/apz/test/reftest/iframe-zoomed-child.html
new file mode 100644
index 0000000000..4d51f46399
--- /dev/null
+++ b/gfx/layers/apz/test/reftest/iframe-zoomed-child.html
@@ -0,0 +1,12 @@
+<style>
+div {
+ position: absolute;
+ background-color: green;
+ width: 10px;
+ height: 10px;
+}
+</style>
+<div style="top: 0; left: 0;"></div>
+<div style="top: 0; right: 0;"></div>
+<div style="bottom: 0; right: 0;"></div>
+<div style="bottom: 0; left: 0;"></div>
diff --git a/gfx/layers/apz/test/reftest/iframe-zoomed-ref.html b/gfx/layers/apz/test/reftest/iframe-zoomed-ref.html
new file mode 100644
index 0000000000..2c98a7eb6a
--- /dev/null
+++ b/gfx/layers/apz/test/reftest/iframe-zoomed-ref.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html reftest-resolution="1.5">
+<style>
+div {
+ margin-left: 100px;
+ margin-top: 100px;
+ width: 400px;
+ height: 400px;
+ border: 2px blue solid;
+}
+iframe {
+ width: 350px;
+ height: 350px;
+ border: 2px black solid;
+}
+</style>
+<div>
+<iframe src="iframe-zoomed-child.html"></iframe>
+</div>
+</html>
diff --git a/gfx/layers/apz/test/reftest/iframe-zoomed.html b/gfx/layers/apz/test/reftest/iframe-zoomed.html
new file mode 100644
index 0000000000..d3d5d914ba
--- /dev/null
+++ b/gfx/layers/apz/test/reftest/iframe-zoomed.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<html reftest-resolution="1.5">
+<style>
+div {
+ margin-left: 100px;
+ margin-top: 100px;
+ width: 400px;
+ height: 400px;
+ border: 2px blue solid;
+}
+iframe {
+ width: 350px;
+ height: 350px;
+ border: 2px black solid;
+}
+</style>
+<div>
+<!--
+ this data:text/html is generated from single-lined version of
+ iframe-zoomed-child.html by encodeURIComponent().
+ -->
+<iframe src="data:text/html,%3Cstyle%3Ediv%20%7Bposition%3A%20absolute%3Bbackground-color%3A%20green%3Bwidth%3A%2010px%3Bheight%3A%2010px%3B%7D%3C%2Fstyle%3E%3Cdiv%20style%3D%22top%3A%200%3B%20left%3A%200%3B%22%3E%3C%2Fdiv%3E%3Cdiv%20style%3D%22top%3A%200%3B%20right%3A%200%3B%22%3E%3C%2Fdiv%3E%3Cdiv%20style%3D%22bottom%3A%200%3B%20right%3A%200%3B%22%3E%3C%2Fdiv%3E%3Cdiv%20style%3D%22bottom%3A%200%3B%20left%3A%200%3B%22%3E%3C%2Fdiv%3E">
+</iframe>
+</div>
+</html>
diff --git a/gfx/layers/apz/test/reftest/initial-scale-1-ref.html b/gfx/layers/apz/test/reftest/initial-scale-1-ref.html
new file mode 100644
index 0000000000..cb51966a28
--- /dev/null
+++ b/gfx/layers/apz/test/reftest/initial-scale-1-ref.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html><head>
+<meta name="viewport" content="initial-scale=0.25,width=device-width">
+</head>
+<body>
+This tests that an initial-scale of 0 (i.e. garbage) is overridden<br/>
+with something a little more sane.
+</body>
+</html>
diff --git a/gfx/layers/apz/test/reftest/initial-scale-1.html b/gfx/layers/apz/test/reftest/initial-scale-1.html
new file mode 100644
index 0000000000..58babe9403
--- /dev/null
+++ b/gfx/layers/apz/test/reftest/initial-scale-1.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html><head>
+<meta name="viewport" content="initial-scale=0; width=device-width">
+</head>
+<body>
+This tests that an initial-scale of 0 (i.e. garbage) is overridden<br/>
+with something a little more sane.
+</body>
+</html>
diff --git a/gfx/layers/apz/test/reftest/pinch-zoom-position-fixed-ref.html b/gfx/layers/apz/test/reftest/pinch-zoom-position-fixed-ref.html
new file mode 100644
index 0000000000..f7d485c509
--- /dev/null
+++ b/gfx/layers/apz/test/reftest/pinch-zoom-position-fixed-ref.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<html reftest-resolution="1.5">
+<head>
+ <meta name="viewport" content="width=device-width">
+ <style>
+ body {
+ margin: 0;
+ height: 2000px;
+ overflow: hidden;
+ }
+ div {
+ position: absolute;
+ bottom: 0;
+ width: 100%;
+ height: 500px;
+ background: repeating-linear-gradient(90deg, transparent, transparent 20px, black 20px, black 40px);
+ }
+ </style>
+</head>
+<body>
+ <div></div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/reftest/pinch-zoom-position-fixed.html b/gfx/layers/apz/test/reftest/pinch-zoom-position-fixed.html
new file mode 100644
index 0000000000..c4476f4872
--- /dev/null
+++ b/gfx/layers/apz/test/reftest/pinch-zoom-position-fixed.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<html class="reftest-wait" reftest-resolution="1.5">
+<head>
+ <meta name="viewport" content="width=device-width">
+ <style>
+ body {
+ margin: 0;
+ height: 2000px;
+ overflow: hidden;
+ }
+ div {
+ position: fixed;
+ bottom: 0;
+ width: 100%;
+ height: 500px;
+ background: repeating-linear-gradient(90deg, transparent, transparent 20px, black 20px, black 40px);
+ }
+ </style>
+</head>
+<body onload="scrollTo(0, 500); document.documentElement.classList.remove('reftest-wait');">
+ <!-- Test that fixed position elements are attached to the layout viewport
+ instead of the visual viewport.
+
+ The reference page has a position:absolute element in place of a
+ position:fixed element, both positioned at the bottom of the page.
+
+ After zooming in, the top edge of the visual viewport will coincide with
+ the top edge of the layout viewport, but their bottom edges will
+ diverge.
+
+ Since absolute elements are attached to the initial containing block,
+ which coincides with the layout viewport on page load, the rendering of
+ the fixed element will only match if it is being attached to the layout
+ viewport. -->
+ <div></div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/reftest/pinch-zoom-position-sticky-ref.html b/gfx/layers/apz/test/reftest/pinch-zoom-position-sticky-ref.html
new file mode 100644
index 0000000000..c430b532df
--- /dev/null
+++ b/gfx/layers/apz/test/reftest/pinch-zoom-position-sticky-ref.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<html reftest-resolution="1.5">
+<head>
+ <meta name="viewport" content="width=device-width">
+ <style>
+ body {
+ margin: 0;
+ height: 2000px;
+ overflow: hidden;
+ }
+ #tall {
+ height: 100vh;
+ }
+ #sticky {
+ position: absolute;
+ bottom: 0;
+ width: 100%;
+ height: 500px;
+ background: repeating-linear-gradient(90deg, transparent, transparent 20px, black 20px, black 40px);
+ }
+ </style>
+</head>
+<body>
+ <div id="tall"></div>
+ <div id="sticky"></div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/reftest/pinch-zoom-position-sticky.html b/gfx/layers/apz/test/reftest/pinch-zoom-position-sticky.html
new file mode 100644
index 0000000000..245e0d775e
--- /dev/null
+++ b/gfx/layers/apz/test/reftest/pinch-zoom-position-sticky.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<html class="reftest-wait" reftest-resolution="1.5">
+<head>
+ <meta name="viewport" content="width=device-width">
+ <style>
+ body {
+ margin: 0;
+ height: 2000px;
+ overflow: hidden;
+ }
+ #tall {
+ height: 100vh;
+ }
+ #sticky {
+ position: sticky;
+ bottom: 0;
+ width: 100%;
+ height: 500px;
+ background: repeating-linear-gradient(90deg, transparent, transparent 20px, black 20px, black 40px);
+ }
+ </style>
+</head>
+<body onload="scrollTo(0, 500); document.documentElement.classList.remove('reftest-wait');">
+ <!-- This is similar to the pinch-zoom-position-fixed test, but we add a tall
+ element before the sticky element to ensure that the sticky element is
+ in its "fixed" configuration on page load. -->
+ <div id="tall"></div>
+ <div id="sticky"></div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/reftest/reftest.list b/gfx/layers/apz/test/reftest/reftest.list
new file mode 100644
index 0000000000..b346f54057
--- /dev/null
+++ b/gfx/layers/apz/test/reftest/reftest.list
@@ -0,0 +1,50 @@
+# The following tests test the async positioning of the scrollbars.
+# Basic root-frame scrollbar with async scrolling
+# First make sure that we are actually drawing scrollbars
+skip-if(!asyncPan) pref(apz.allow_zooming,true) != async-scrollbar-1-v.html about:blank
+skip-if(!asyncPan) pref(apz.allow_zooming,true) != async-scrollbar-1-v-ref.html about:blank
+fuzzy-if(Android,0-5,0-6) fuzzy-if(gtkWidget,1-8,8-32) fuzzy-if(cocoaWidget,16-22,20-44) skip-if(!asyncPan) pref(apz.allow_zooming,true) == async-scrollbar-1-v.html async-scrollbar-1-v-ref.html
+fuzzy-if(Android,0-13,0-10) fuzzy-if(gtkWidget,1-30,4-32) fuzzy-if(cocoaWidget,14-22,20-44) skip-if(!asyncPan) pref(apz.allow_zooming,true) == async-scrollbar-1-h.html async-scrollbar-1-h-ref.html
+fuzzy-if(Android,0-13,0-21) fuzzy-if(gtkWidget,1-4,4-24) fuzzy-if(cocoaWidget,11-18,44-88) skip-if(!asyncPan) pref(apz.allow_zooming,true) == async-scrollbar-1-vh.html async-scrollbar-1-vh-ref.html
+fuzzy-if(Android,0-5,0-6) fuzzy-if(gtkWidget,1-8,8-32) fuzzy-if(cocoaWidget,16-22,20-44) skip-if(!asyncPan) pref(apz.allow_zooming,true) == async-scrollbar-1-v-rtl.html async-scrollbar-1-v-rtl-ref.html
+fuzzy-if(Android,0-14,0-10) fuzzy-if(gtkWidget,1-30,12-32) fuzzy-if(cocoaWidget,14-22,20-44) skip-if(!asyncPan) pref(apz.allow_zooming,true) == async-scrollbar-1-h-rtl.html async-scrollbar-1-h-rtl-ref.html
+fuzzy-if(Android,0-43,0-26) fuzzy-if(gtkWidget,0-14,12-32) fuzzy-if(cocoaWidget,11-18,26-76) skip-if(!asyncPan) pref(apz.allow_zooming,true) == async-scrollbar-1-vh-rtl.html async-scrollbar-1-vh-rtl-ref.html
+
+# Different async zoom levels. Since the scrollthumb gets async-scaled in the
+# compositor, the border-radius ends of the scrollthumb are going to be a little
+# off, hence the fuzzy-if clauses.
+skip-if(Android) fuzzy(0-107,0-72) pref(apz.allow_zooming,true) pref(apz.scrollthumb.recalc,true) == root-scrollbar-async-zoomed-in.html root-scrollbar-async-zoomed-in-ref.html
+skip-if(Android) fuzzy(0-107,0-167) pref(apz.allow_zooming,true) pref(apz.scrollthumb.recalc,true) == root-scrollbar-async-zoomed-out.html root-scrollbar-async-zoomed-out-ref.html
+skip-if(!Android) fuzzy(0-54,0-33) pref(apz.allow_zooming,true) == root-scrollbar-async-zoomed-in.html root-scrollbar-async-zoomed-in-ref.html
+skip-if(!Android) fuzzy(0-53,0-30) pref(apz.allow_zooming,true) == root-scrollbar-async-zoomed-out.html root-scrollbar-async-zoomed-out-ref.html
+
+# Test that the compositor thumb sizing calculations handle a non-default device scale correctly
+fuzzy-if(Android,0-31,0-29) fuzzy-if(gtkWidget,0-18,0-49) fuzzy-if(cocoaWidget,0-21,0-53) == async-scrollbar-1-v-fullzoom.html async-scrollbar-1-v-fullzoom-ref.html
+
+# Test scrollbars working properly with pinch-zooming, i.e. different document resolutions.
+# As above, the end of the scrollthumb won't match perfectly, but the bulk of the scrollbar should be present and identical.
+# On desktop, even more fuzz is needed because thumb scaling is not exactly proportional: making the page twice as long
+# won't make the thumb exactly twice as short, which is what the test expects. That's fine, as the purpose of the test is
+# to catch more fundamental scrollbar rendering bugs such as the entire track being mispositioned or the thumb being
+# clipped away.
+fuzzy-if(Android,0-54,0-22) fuzzy-if(!Android,0-128,0-137) pref(apz.allow_zooming,true) == root-scrollbar-zoomed-in.html root-scrollbar-zoomed-in-ref.html
+fuzzy-if(Android,0-54,0-22) fuzzy-if(!Android,0-128,0-137) pref(apz.allow_zooming,true) pref(apz.allow_zooming_out,true) == root-scrollbar-zoomed-out.html root-scrollbar-zoomed-out-ref.html
+fuzzy-if(Android,0-54,0-27) fuzzy-if(!Android,0-128,0-137) pref(apz.allow_zooming,true) == root-scrollbar-zoomed-in-async-scroll.html root-scrollbar-zoomed-in-ref.html
+fuzzy-if(Android,0-54,0-25) fuzzy-if(!Android,0-128,0-137) pref(apz.allow_zooming,true) pref(apz.allow_zooming_out,true) == root-scrollbar-zoomed-out-async-scroll.html root-scrollbar-zoomed-out-ref.html
+fuzzy-if(Android,0-51,0-50) fuzzy-if(!Android,0-128,0-137) pref(apz.allow_zooming,true) == subframe-scrollbar-zoomed-in-async-scroll.html subframe-scrollbar-zoomed-in-async-scroll-ref.html
+fuzzy-if(Android,0-28,0-23) fuzzy-if(!Android,0-107,0-34) pref(apz.allow_zooming,true) pref(apz.allow_zooming_out,true) == subframe-scrollbar-zoomed-out-async-scroll.html subframe-scrollbar-zoomed-out-async-scroll-ref.html
+
+# Meta-viewport tag support
+skip-if(!Android) pref(apz.allow_zooming,true) == initial-scale-1.html initial-scale-1-ref.html
+
+skip-if(!asyncPan) == frame-reconstruction-scroll-clamping.html frame-reconstruction-scroll-clamping-ref.html
+
+# Test that position:fixed and position:sticky elements are attached to the
+# layout viewport.
+skip-if(winWidget&&isCoverageBuild) pref(apz.allow_zooming,true) == pinch-zoom-position-fixed.html pinch-zoom-position-fixed-ref.html
+skip-if(winWidget&&isCoverageBuild) pref(apz.allow_zooming,true) == pinch-zoom-position-sticky.html pinch-zoom-position-sticky-ref.html
+
+pref(apz.allow_zooming,true) == iframe-zoomed.html iframe-zoomed-ref.html
+pref(apz.allow_zooming,true) == scaled-iframe-zoomed.html scaled-iframe-zoomed-ref.html
+
+== root-scrollbars-1.html root-scrollbars-1-ref.html
diff --git a/gfx/layers/apz/test/reftest/root-scrollbar-async-zoomed-in-ref.html b/gfx/layers/apz/test/reftest/root-scrollbar-async-zoomed-in-ref.html
new file mode 100644
index 0000000000..9568836459
--- /dev/null
+++ b/gfx/layers/apz/test/reftest/root-scrollbar-async-zoomed-in-ref.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html class="reftest-wait"><head>
+<meta name="viewport" content="initial-scale=1,width=device-width">
+</head>
+<body onload="scrollTo(450,10000); document.documentElement.classList.remove('reftest-wait')">
+<div style="width: 9000px; height: 20000px; background: white;"></div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/reftest/root-scrollbar-async-zoomed-in.html b/gfx/layers/apz/test/reftest/root-scrollbar-async-zoomed-in.html
new file mode 100644
index 0000000000..31e1e99a3d
--- /dev/null
+++ b/gfx/layers/apz/test/reftest/root-scrollbar-async-zoomed-in.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html class="reftest-wait"
+ reftest-async-scroll
+ reftest-async-scroll-x="224" reftest-async-scroll-y="4999"
+ reftest-async-zoom="2.0"><head>
+<meta name="viewport" content="initial-scale=1,width=device-width">
+</head>
+<!-- Doing scrollTo(1,1) is to activate the left/up arrows in the scrollbars
+ for non-overlay scrollbar environments -->
+<body onload="scrollTo(1,1); document.documentElement.classList.remove('reftest-wait')">
+<div style="width: 4500px; height: 10000px; background: white;"></div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/reftest/root-scrollbar-async-zoomed-out-ref.html b/gfx/layers/apz/test/reftest/root-scrollbar-async-zoomed-out-ref.html
new file mode 100644
index 0000000000..9568836459
--- /dev/null
+++ b/gfx/layers/apz/test/reftest/root-scrollbar-async-zoomed-out-ref.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html class="reftest-wait"><head>
+<meta name="viewport" content="initial-scale=1,width=device-width">
+</head>
+<body onload="scrollTo(450,10000); document.documentElement.classList.remove('reftest-wait')">
+<div style="width: 9000px; height: 20000px; background: white;"></div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/reftest/root-scrollbar-async-zoomed-out.html b/gfx/layers/apz/test/reftest/root-scrollbar-async-zoomed-out.html
new file mode 100644
index 0000000000..4032c3c638
--- /dev/null
+++ b/gfx/layers/apz/test/reftest/root-scrollbar-async-zoomed-out.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html class="reftest-wait"
+ reftest-async-scroll
+ reftest-async-scroll-x="899" reftest-async-scroll-y="19999"
+ reftest-async-zoom="0.5"><head>
+<meta name="viewport" content="initial-scale=1,width=device-width">
+</head>
+<!-- Doing scrollTo(1,1) is to activate the left/up arrows in the scrollbars
+ for non-overlay scrollbar environments -->
+<body onload="scrollTo(1,1); document.documentElement.classList.remove('reftest-wait')">
+<div style="width: 18000px; height: 40000px; background: white;"></div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/reftest/root-scrollbar-zoomed-in-async-scroll.html b/gfx/layers/apz/test/reftest/root-scrollbar-zoomed-in-async-scroll.html
new file mode 100644
index 0000000000..04c829d427
--- /dev/null
+++ b/gfx/layers/apz/test/reftest/root-scrollbar-zoomed-in-async-scroll.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html class="reftest-wait" reftest-resolution="2.0"
+ reftest-async-scroll
+ reftest-async-scroll-x="224" reftest-async-scroll-y="4999"><head>
+<meta name="viewport" content="width=device-width">
+</head>
+<!-- Doing scrollTo(1,1) is to activate the left/up arrows in the scrollbars
+ for non-overlay scrollbar environments -->
+<body onload="scrollTo(1,1); document.documentElement.classList.remove('reftest-wait')">
+<div style="width: 4500px; height: 10000px; background: white;"></div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/reftest/root-scrollbar-zoomed-in-ref.html b/gfx/layers/apz/test/reftest/root-scrollbar-zoomed-in-ref.html
new file mode 100644
index 0000000000..9568836459
--- /dev/null
+++ b/gfx/layers/apz/test/reftest/root-scrollbar-zoomed-in-ref.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html class="reftest-wait"><head>
+<meta name="viewport" content="initial-scale=1,width=device-width">
+</head>
+<body onload="scrollTo(450,10000); document.documentElement.classList.remove('reftest-wait')">
+<div style="width: 9000px; height: 20000px; background: white;"></div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/reftest/root-scrollbar-zoomed-in.html b/gfx/layers/apz/test/reftest/root-scrollbar-zoomed-in.html
new file mode 100644
index 0000000000..c9cb6e80a7
--- /dev/null
+++ b/gfx/layers/apz/test/reftest/root-scrollbar-zoomed-in.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html class="reftest-wait" reftest-resolution="2.0"><head>
+<meta name="viewport" content="width=device-width">
+</head>
+<body onload="scrollTo(225,5000); document.documentElement.classList.remove('reftest-wait')">
+<div style="width: 4500px; height: 10000px; background: white;"></div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/reftest/root-scrollbar-zoomed-out-async-scroll.html b/gfx/layers/apz/test/reftest/root-scrollbar-zoomed-out-async-scroll.html
new file mode 100644
index 0000000000..465fac6211
--- /dev/null
+++ b/gfx/layers/apz/test/reftest/root-scrollbar-zoomed-out-async-scroll.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html class="reftest-wait" reftest-resolution="0.5"
+ reftest-async-scroll
+ reftest-async-scroll-x="899" reftest-async-scroll-y="19999"><head>
+<meta name="viewport" content="width=device-width">
+</head>
+<!-- Doing scrollTo(1,1) is to activate the left/up arrows in the scrollbars
+ for non-overlay scrollbar environments -->
+<body onload="scrollTo(1,1); document.documentElement.classList.remove('reftest-wait')">
+<div style="width: 18000px; height: 40000px; background: white;"></div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/reftest/root-scrollbar-zoomed-out-ref.html b/gfx/layers/apz/test/reftest/root-scrollbar-zoomed-out-ref.html
new file mode 100644
index 0000000000..9568836459
--- /dev/null
+++ b/gfx/layers/apz/test/reftest/root-scrollbar-zoomed-out-ref.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html class="reftest-wait"><head>
+<meta name="viewport" content="initial-scale=1,width=device-width">
+</head>
+<body onload="scrollTo(450,10000); document.documentElement.classList.remove('reftest-wait')">
+<div style="width: 9000px; height: 20000px; background: white;"></div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/reftest/root-scrollbar-zoomed-out.html b/gfx/layers/apz/test/reftest/root-scrollbar-zoomed-out.html
new file mode 100644
index 0000000000..0e3ec7173d
--- /dev/null
+++ b/gfx/layers/apz/test/reftest/root-scrollbar-zoomed-out.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html class="reftest-wait" reftest-resolution="0.5"><head>
+<meta name="viewport" content="width=device-width">
+</head>
+<body onload="scrollTo(900,20000); document.documentElement.classList.remove('reftest-wait')">
+<div style="width: 18000px; height: 40000px; background: white;"></div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/reftest/root-scrollbars-1-ref.html b/gfx/layers/apz/test/reftest/root-scrollbars-1-ref.html
new file mode 100644
index 0000000000..435609f8a3
--- /dev/null
+++ b/gfx/layers/apz/test/reftest/root-scrollbars-1-ref.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html><head>
+<meta name="viewport" content="width=device-width">
+<style>
+html, body {
+ margin: 0;
+}
+</style>
+<title>In this file the scrollbars that appear are non-root scrollbars</title>
+</head>
+<body>
+<div style="width: 100vw; height: 100vh; overflow: auto"><div style="width: 150vw; height: 150vh"></div></div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/reftest/root-scrollbars-1.html b/gfx/layers/apz/test/reftest/root-scrollbars-1.html
new file mode 100644
index 0000000000..e2560d48a9
--- /dev/null
+++ b/gfx/layers/apz/test/reftest/root-scrollbars-1.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html><head>
+<meta name="viewport" content="width=device-width">
+<style>
+html, body {
+ margin: 0;
+}
+</style>
+<title>In this file the scrollbars that appear are the root scrollbars</title>
+</head>
+<body>
+<div style="width: 150vw; height: 150vh"></div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/reftest/scaled-iframe-zoomed-ref.html b/gfx/layers/apz/test/reftest/scaled-iframe-zoomed-ref.html
new file mode 100644
index 0000000000..39847320e2
--- /dev/null
+++ b/gfx/layers/apz/test/reftest/scaled-iframe-zoomed-ref.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html reftest-resolution="1.5">
+<style>
+div {
+ margin-left: 100px;
+ margin-top: 100px;
+ width: 200px;
+ height: 200px;
+ border: 10px blue solid;
+ transform: scale(2);
+}
+iframe {
+ width: 150px;
+ height: 150px;
+ border: 10px black solid;
+}
+</style>
+<div>
+<iframe src="iframe-zoomed-child.html"></iframe>
+</div>
+</html>
diff --git a/gfx/layers/apz/test/reftest/scaled-iframe-zoomed.html b/gfx/layers/apz/test/reftest/scaled-iframe-zoomed.html
new file mode 100644
index 0000000000..89b09047f7
--- /dev/null
+++ b/gfx/layers/apz/test/reftest/scaled-iframe-zoomed.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html reftest-resolution="1.5">
+<style>
+div {
+ margin-left: 100px;
+ margin-top: 100px;
+ width: 200px;
+ height: 200px;
+ border: 10px blue solid;
+ transform: scale(2);
+}
+iframe {
+ width: 150px;
+ height: 150px;
+ border: 10px black solid;
+}
+</style>
+<div>
+<!--
+ this data:text/html is generated from single-lined version of
+ iframe-zoomed-child.html by encodeURIComponent().
+ -->
+<iframe src="data:text/html,%3Cstyle%3Ediv%20%7Bposition%3A%20absolute%3Bbackground-color%3A%20green%3Bwidth%3A%2010px%3Bheight%3A%2010px%3B%7D%3C%2Fstyle%3E%3Cdiv%20style%3D%22top%3A%200%3B%20left%3A%200%3B%22%3E%3C%2Fdiv%3E%3Cdiv%20style%3D%22top%3A%200%3B%20right%3A%200%3B%22%3E%3C%2Fdiv%3E%3Cdiv%20style%3D%22bottom%3A%200%3B%20right%3A%200%3B%22%3E%3C%2Fdiv%3E%3Cdiv%20style%3D%22bottom%3A%200%3B%20left%3A%200%3B%22%3E%3C%2Fdiv%3E">
+</iframe>
+</div>
+</html>
diff --git a/gfx/layers/apz/test/reftest/subframe-scrollbar-zoomed-in-async-scroll-ref.html b/gfx/layers/apz/test/reftest/subframe-scrollbar-zoomed-in-async-scroll-ref.html
new file mode 100644
index 0000000000..f2d640bc2e
--- /dev/null
+++ b/gfx/layers/apz/test/reftest/subframe-scrollbar-zoomed-in-async-scroll-ref.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html class="reftest-wait" reftest-resolution="2.0"><head>
+<meta name="viewport" content="width=device-width">
+</head>
+<body onload="subframe.scrollTo(225,5000); document.documentElement.classList.remove('reftest-wait')">
+<div id="subframe" style="width: 200px; height: 200px; overflow: scroll;">
+ <div style="width: 4500px; height: 10000px; background: white;"></div>
+</div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/reftest/subframe-scrollbar-zoomed-in-async-scroll.html b/gfx/layers/apz/test/reftest/subframe-scrollbar-zoomed-in-async-scroll.html
new file mode 100644
index 0000000000..2aa2a2627c
--- /dev/null
+++ b/gfx/layers/apz/test/reftest/subframe-scrollbar-zoomed-in-async-scroll.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html class="reftest-wait" reftest-resolution="2.0" reftest-async-scroll><head>
+<meta name="viewport" content="width=device-width">
+</head>
+<!-- Doing scrollTo(1,1) is to activate the left/up arrows in the scrollbars
+ for non-overlay scrollbar environments -->
+<body onload="subframe.scrollTo(1,1); document.documentElement.classList.remove('reftest-wait')">
+<div id="subframe" style="width: 200px; height: 200px; overflow: scroll;"
+ reftest-displayport-x="0" reftest-displayport-y="0"
+ reftest-displayport-w="1600" reftest-displayport-h="2000"
+ reftest-async-scroll-x="224" reftest-async-scroll-y="4999">
+ <div style="width: 4500px; height: 10000px; background: white;"></div>
+</div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/reftest/subframe-scrollbar-zoomed-out-async-scroll-ref.html b/gfx/layers/apz/test/reftest/subframe-scrollbar-zoomed-out-async-scroll-ref.html
new file mode 100644
index 0000000000..4283952f78
--- /dev/null
+++ b/gfx/layers/apz/test/reftest/subframe-scrollbar-zoomed-out-async-scroll-ref.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html class="reftest-wait" reftest-resolution="0.5"><head>
+<meta name="viewport" content="width=device-width">
+</head>
+<body onload="subframe.scrollTo(90,2000); document.documentElement.classList.remove('reftest-wait')">
+<div id="subframe" style="width: 200px; height: 200px; overflow: scroll;">
+ <div style="width: 1800px; height: 4000px; background: white;"></div>
+</div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/reftest/subframe-scrollbar-zoomed-out-async-scroll.html b/gfx/layers/apz/test/reftest/subframe-scrollbar-zoomed-out-async-scroll.html
new file mode 100644
index 0000000000..a0f1e08cf9
--- /dev/null
+++ b/gfx/layers/apz/test/reftest/subframe-scrollbar-zoomed-out-async-scroll.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html class="reftest-wait" reftest-resolution="0.5" reftest-async-scroll><head>
+<meta name="viewport" content="width=device-width">
+</head>
+<!-- Doing scrollTo(1,1) is to activate the left/up arrows in the scrollbars
+ for non-overlay scrollbar environments -->
+<body onload="subframe.scrollTo(1,1); document.documentElement.classList.remove('reftest-wait')">
+<div id="subframe" style="width: 200px; height: 200px; overflow: scroll;"
+ reftest-displayport-x="0" reftest-displayport-y="0"
+ reftest-displayport-w="1600" reftest-displayport-h="2000"
+ reftest-async-scroll-x="89" reftest-async-scroll-y="1999">
+ <div style="width: 1800px; height: 4000px; background: white;"></div>
+</div>
+</body>
+</html>
diff --git a/gfx/layers/apz/testutil/APZTestData.cpp b/gfx/layers/apz/testutil/APZTestData.cpp
new file mode 100644
index 0000000000..4154607d75
--- /dev/null
+++ b/gfx/layers/apz/testutil/APZTestData.cpp
@@ -0,0 +1,124 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "APZTestData.h"
+#include "mozilla/dom/APZTestDataBinding.h"
+#include "mozilla/dom/ToJSValue.h"
+#include "nsString.h"
+
+namespace mozilla {
+namespace layers {
+
+struct APZTestDataToJSConverter {
+ template <typename Key, typename Value, typename KeyValuePair>
+ static void ConvertMap(const std::map<Key, Value>& aFrom,
+ dom::Sequence<KeyValuePair>& aOutTo,
+ void (*aElementConverter)(const Key&, const Value&,
+ KeyValuePair&)) {
+ for (auto it = aFrom.begin(); it != aFrom.end(); ++it) {
+ if (!aOutTo.AppendElement(fallible)) {
+ // XXX(Bug 1632090) Instead of extending the array 1-by-1 (which might
+ // involve multiple reallocations) and potentially crashing here,
+ // SetCapacity could be called outside the loop once.
+ mozalloc_handle_oom(0);
+ }
+ aElementConverter(it->first, it->second, aOutTo.LastElement());
+ }
+ }
+
+ template <typename Src, typename Target>
+ static void ConvertList(const nsTArray<Src>& aFrom,
+ dom::Sequence<Target>& aOutTo,
+ void (*aElementConverter)(const Src&, Target&)) {
+ for (auto it = aFrom.begin(); it != aFrom.end(); ++it) {
+ if (!aOutTo.AppendElement(fallible)) {
+ // XXX(Bug 1632090) Instead of extending the array 1-by-1 (which might
+ // involve multiple reallocations) and potentially crashing here,
+ // SetCapacity could be called outside the loop once.
+ mozalloc_handle_oom(0);
+ }
+ aElementConverter(*it, aOutTo.LastElement());
+ }
+ }
+
+ static void ConvertAPZTestData(const APZTestData& aFrom,
+ dom::APZTestData& aOutTo) {
+ ConvertMap(aFrom.mPaints, aOutTo.mPaints.Construct(), ConvertBucket);
+ ConvertMap(aFrom.mRepaintRequests, aOutTo.mRepaintRequests.Construct(),
+ ConvertBucket);
+ ConvertList(aFrom.mHitResults, aOutTo.mHitResults.Construct(),
+ ConvertHitResult);
+ ConvertList(aFrom.mSampledResults, aOutTo.mSampledResults.Construct(),
+ ConvertSampledResult);
+ ConvertMap(aFrom.mAdditionalData, aOutTo.mAdditionalData.Construct(),
+ ConvertAdditionalDataEntry);
+ }
+
+ static void ConvertBucket(const SequenceNumber& aKey,
+ const APZTestData::Bucket& aValue,
+ dom::APZBucket& aOutKeyValuePair) {
+ aOutKeyValuePair.mSequenceNumber.Construct() = aKey;
+ ConvertMap(aValue, aOutKeyValuePair.mScrollFrames.Construct(),
+ ConvertScrollFrameData);
+ }
+
+ static void ConvertScrollFrameData(const APZTestData::ViewID& aKey,
+ const APZTestData::ScrollFrameData& aValue,
+ dom::ScrollFrameData& aOutKeyValuePair) {
+ aOutKeyValuePair.mScrollId.Construct() = aKey;
+ ConvertMap(aValue, aOutKeyValuePair.mEntries.Construct(), ConvertEntry);
+ }
+
+ static void ConvertEntry(const std::string& aKey, const std::string& aValue,
+ dom::ScrollFrameDataEntry& aOutKeyValuePair) {
+ ConvertString(aKey, aOutKeyValuePair.mKey.Construct());
+ ConvertString(aValue, aOutKeyValuePair.mValue.Construct());
+ }
+
+ static void ConvertAdditionalDataEntry(
+ const std::string& aKey, const std::string& aValue,
+ dom::AdditionalDataEntry& aOutKeyValuePair) {
+ ConvertString(aKey, aOutKeyValuePair.mKey.Construct());
+ ConvertString(aValue, aOutKeyValuePair.mValue.Construct());
+ }
+
+ static void ConvertString(const std::string& aFrom, nsString& aOutTo) {
+ CopyUTF8toUTF16(aFrom, aOutTo);
+ }
+
+ static void ConvertHitResult(const APZTestData::HitResult& aResult,
+ dom::APZHitResult& aOutHitResult) {
+ aOutHitResult.mScreenX.Construct() = aResult.point.x;
+ aOutHitResult.mScreenY.Construct() = aResult.point.y;
+ static_assert(MaxEnumValue<gfx::CompositorHitTestInfo::valueType>::value <
+ std::numeric_limits<uint16_t>::digits,
+ "CompositorHitTestFlags MAX value have to be less than "
+ "number of bits in uint16_t");
+ aOutHitResult.mHitResult.Construct() =
+ static_cast<uint16_t>(aResult.result.serialize());
+ aOutHitResult.mLayersId.Construct() = aResult.layersId.mId;
+ aOutHitResult.mScrollId.Construct() = aResult.scrollId;
+ }
+
+ static void ConvertSampledResult(const APZTestData::SampledResult& aResult,
+ dom::APZSampledResult& aOutSampledResult) {
+ aOutSampledResult.mScrollOffsetX.Construct() = aResult.scrollOffset.x;
+ aOutSampledResult.mScrollOffsetY.Construct() = aResult.scrollOffset.y;
+ aOutSampledResult.mLayersId.Construct() = aResult.layersId.mId;
+ aOutSampledResult.mScrollId.Construct() = aResult.scrollId;
+ aOutSampledResult.mSampledTimeStamp.Construct() = aResult.sampledTimeStamp;
+ }
+};
+
+bool APZTestData::ToJS(JS::MutableHandle<JS::Value> aOutValue,
+ JSContext* aContext) const {
+ dom::APZTestData result;
+ APZTestDataToJSConverter::ConvertAPZTestData(*this, result);
+ return dom::ToJSValue(aContext, result, aOutValue);
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/testutil/APZTestData.h b/gfx/layers/apz/testutil/APZTestData.h
new file mode 100644
index 0000000000..e4a73c80cc
--- /dev/null
+++ b/gfx/layers/apz/testutil/APZTestData.h
@@ -0,0 +1,252 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_APZTestData_h
+#define mozilla_layers_APZTestData_h
+
+#include <map>
+
+#include "nsDebug.h" // for NS_WARNING
+#include "nsDOMNavigationTiming.h" // for DOMHighResTimeStamp
+#include "nsTArray.h"
+#include "mozilla/Assertions.h" // for MOZ_ASSERT
+#include "mozilla/DebugOnly.h" // for DebugOnly
+#include "mozilla/GfxMessageUtils.h" // for ParamTraits specializations
+#include "mozilla/StaticPrefs_apz.h"
+#include "mozilla/ToString.h" // for ToString
+#include "mozilla/gfx/CompositorHitTestInfo.h"
+#include "mozilla/layers/LayersMessageUtils.h" // for ParamTraits specializations
+#include "mozilla/layers/ScrollableLayerGuid.h"
+#include "ipc/IPCMessageUtils.h"
+#include "js/TypeDecls.h"
+
+namespace mozilla {
+namespace layers {
+
+typedef uint32_t SequenceNumber;
+
+/**
+ * This structure is used to store information logged by various gecko
+ * components for later examination by test code.
+ * It contains a bucket for every paint (initiated on the client side),
+ * and every repaint request (initiated on the compositor side by
+ * AsyncPanZoomController::RequestContentRepait), which are identified by
+ * sequence numbers, and within that, a set of arbitrary string key/value
+ * pairs for every scrollable frame, identified by a scroll id.
+ * There are two instances of this data structure for every content thread:
+ * one on the client side and one of the compositor side.
+ * It also contains a list of hit-test results for MozMouseHittest events
+ * dispatched during testing. This list is only populated on the compositor
+ * instance of this class.
+ */
+// TODO(botond):
+// - Improve warnings/asserts.
+// - Add ability to associate a repaint request triggered during a layers
+// update with the sequence number of the paint that caused the layers
+// update.
+class APZTestData {
+ typedef ScrollableLayerGuid::ViewID ViewID;
+ friend struct IPC::ParamTraits<APZTestData>;
+ friend struct APZTestDataToJSConverter;
+
+ public:
+ void StartNewPaint(SequenceNumber aSequenceNumber) {
+ // We should never get more than one paint with the same sequence number.
+ MOZ_ASSERT(mPaints.find(aSequenceNumber) == mPaints.end());
+ mPaints.insert(DataStore::value_type(aSequenceNumber, Bucket()));
+ }
+ void LogTestDataForPaint(SequenceNumber aSequenceNumber, ViewID aScrollId,
+ const std::string& aKey, const std::string& aValue) {
+ LogTestDataImpl(mPaints, aSequenceNumber, aScrollId, aKey, aValue);
+ }
+
+ void StartNewRepaintRequest(SequenceNumber aSequenceNumber) {
+ typedef std::pair<DataStore::iterator, bool> InsertResultT;
+ DebugOnly<InsertResultT> insertResult = mRepaintRequests.insert(
+ DataStore::value_type(aSequenceNumber, Bucket()));
+ MOZ_ASSERT(((InsertResultT&)insertResult).second,
+ "Already have a repaint request with this sequence number");
+ }
+ void LogTestDataForRepaintRequest(SequenceNumber aSequenceNumber,
+ ViewID aScrollId, const std::string& aKey,
+ const std::string& aValue) {
+ LogTestDataImpl(mRepaintRequests, aSequenceNumber, aScrollId, aKey, aValue);
+ }
+ void RecordHitResult(const ScreenPoint& aPoint,
+ const mozilla::gfx::CompositorHitTestInfo& aResult,
+ const LayersId& aLayersId, const ViewID& aScrollId) {
+ mHitResults.AppendElement(HitResult{aPoint, aResult, aLayersId, aScrollId});
+ }
+ void RecordSampledResult(const CSSPoint& aScrollOffset,
+ DOMHighResTimeStamp aSampledTimeStamp,
+ const LayersId& aLayersId, const ViewID& aScrollId) {
+ mSampledResults.AppendElement(
+ SampledResult{aScrollOffset, aSampledTimeStamp, aLayersId, aScrollId});
+ }
+ void RecordAdditionalData(const std::string& aKey,
+ const std::string& aValue) {
+ mAdditionalData[aKey] = aValue;
+ }
+
+ // Convert this object to a JS representation.
+ bool ToJS(JS::MutableHandle<JS::Value> aOutValue, JSContext* aContext) const;
+
+ // Use dummy derived structures wrapping the typedefs to work around a type
+ // name length limit in MSVC.
+ typedef std::map<std::string, std::string> ScrollFrameDataBase;
+ struct ScrollFrameData : ScrollFrameDataBase {};
+ typedef std::map<ViewID, ScrollFrameData> BucketBase;
+ struct Bucket : BucketBase {};
+ typedef std::map<SequenceNumber, Bucket> DataStoreBase;
+ struct DataStore : DataStoreBase {};
+ struct HitResult {
+ ScreenPoint point;
+ mozilla::gfx::CompositorHitTestInfo result;
+ LayersId layersId;
+ ViewID scrollId;
+ };
+ struct SampledResult {
+ CSSPoint scrollOffset;
+ DOMHighResTimeStamp sampledTimeStamp;
+ LayersId layersId;
+ ViewID scrollId;
+ };
+
+ private:
+ DataStore mPaints;
+ DataStore mRepaintRequests;
+ CopyableTArray<HitResult> mHitResults;
+ CopyableTArray<SampledResult> mSampledResults;
+ // Additional free-form data that's not grouped paint or scroll frame.
+ std::map<std::string, std::string> mAdditionalData;
+
+ void LogTestDataImpl(DataStore& aDataStore, SequenceNumber aSequenceNumber,
+ ViewID aScrollId, const std::string& aKey,
+ const std::string& aValue) {
+ auto bucketIterator = aDataStore.find(aSequenceNumber);
+ if (bucketIterator == aDataStore.end()) {
+ MOZ_ASSERT(false,
+ "LogTestDataImpl called with nonexistent sequence number");
+ return;
+ }
+ Bucket& bucket = bucketIterator->second;
+ ScrollFrameData& scrollFrameData =
+ bucket[aScrollId]; // create if doesn't exist
+ MOZ_ASSERT(scrollFrameData.find(aKey) == scrollFrameData.end() ||
+ scrollFrameData[aKey] == aValue);
+ scrollFrameData.insert(ScrollFrameData::value_type(aKey, aValue));
+ }
+};
+
+// A helper class for logging data for a paint.
+class APZPaintLogHelper {
+ public:
+ APZPaintLogHelper(APZTestData* aTestData, SequenceNumber aPaintSequenceNumber)
+ : mTestData(aTestData), mPaintSequenceNumber(aPaintSequenceNumber) {
+ MOZ_ASSERT(!aTestData || StaticPrefs::apz_test_logging_enabled(),
+ "don't call me");
+ }
+
+ template <typename Value>
+ void LogTestData(ScrollableLayerGuid::ViewID aScrollId,
+ const std::string& aKey, const Value& aValue) const {
+ if (mTestData) { // avoid stringifying if mTestData == nullptr
+ LogTestData(aScrollId, aKey, ToString(aValue));
+ }
+ }
+
+ void LogTestData(ScrollableLayerGuid::ViewID aScrollId,
+ const std::string& aKey, const std::string& aValue) const {
+ if (mTestData) {
+ mTestData->LogTestDataForPaint(mPaintSequenceNumber, aScrollId, aKey,
+ aValue);
+ }
+ }
+
+ private:
+ APZTestData* mTestData;
+ SequenceNumber mPaintSequenceNumber;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+namespace IPC {
+
+template <>
+struct ParamTraits<mozilla::layers::APZTestData> {
+ typedef mozilla::layers::APZTestData paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.mPaints);
+ WriteParam(aWriter, aParam.mRepaintRequests);
+ WriteParam(aWriter, aParam.mHitResults);
+ WriteParam(aWriter, aParam.mSampledResults);
+ WriteParam(aWriter, aParam.mAdditionalData);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ return (ReadParam(aReader, &aResult->mPaints) &&
+ ReadParam(aReader, &aResult->mRepaintRequests) &&
+ ReadParam(aReader, &aResult->mHitResults) &&
+ ReadParam(aReader, &aResult->mSampledResults) &&
+ ReadParam(aReader, &aResult->mAdditionalData));
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::layers::APZTestData::ScrollFrameData>
+ : ParamTraits<mozilla::layers::APZTestData::ScrollFrameDataBase> {};
+
+template <>
+struct ParamTraits<mozilla::layers::APZTestData::Bucket>
+ : ParamTraits<mozilla::layers::APZTestData::BucketBase> {};
+
+template <>
+struct ParamTraits<mozilla::layers::APZTestData::DataStore>
+ : ParamTraits<mozilla::layers::APZTestData::DataStoreBase> {};
+
+template <>
+struct ParamTraits<mozilla::layers::APZTestData::HitResult> {
+ typedef mozilla::layers::APZTestData::HitResult paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.point);
+ WriteParam(aWriter, aParam.result);
+ WriteParam(aWriter, aParam.layersId);
+ WriteParam(aWriter, aParam.scrollId);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ return (ReadParam(aReader, &aResult->point) &&
+ ReadParam(aReader, &aResult->result) &&
+ ReadParam(aReader, &aResult->layersId) &&
+ ReadParam(aReader, &aResult->scrollId));
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::layers::APZTestData::SampledResult> {
+ typedef mozilla::layers::APZTestData::SampledResult paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.scrollOffset);
+ WriteParam(aWriter, aParam.sampledTimeStamp);
+ WriteParam(aWriter, aParam.layersId);
+ WriteParam(aWriter, aParam.scrollId);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ return (ReadParam(aReader, &aResult->scrollOffset) &&
+ ReadParam(aReader, &aResult->sampledTimeStamp) &&
+ ReadParam(aReader, &aResult->layersId) &&
+ ReadParam(aReader, &aResult->scrollId));
+ }
+};
+
+} // namespace IPC
+
+#endif /* mozilla_layers_APZTestData_h */
diff --git a/gfx/layers/apz/util/APZCCallbackHelper.cpp b/gfx/layers/apz/util/APZCCallbackHelper.cpp
new file mode 100644
index 0000000000..8b67baf316
--- /dev/null
+++ b/gfx/layers/apz/util/APZCCallbackHelper.cpp
@@ -0,0 +1,940 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "APZCCallbackHelper.h"
+
+#include "gfxPlatform.h" // For gfxPlatform::UseTiling
+
+#include "mozilla/AsyncEventDispatcher.h"
+#include "mozilla/EventForwards.h"
+#include "mozilla/dom/CustomEvent.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/MouseEventBinding.h"
+#include "mozilla/dom/BrowserParent.h"
+#include "mozilla/dom/ScriptSettings.h"
+#include "mozilla/IntegerPrintfMacros.h"
+#include "mozilla/layers/RepaintRequest.h"
+#include "mozilla/layers/WebRenderLayerManager.h"
+#include "mozilla/layers/WebRenderBridgeChild.h"
+#include "mozilla/DisplayPortUtils.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/ToString.h"
+#include "mozilla/ViewportUtils.h"
+#include "nsContainerFrame.h"
+#include "nsContentUtils.h"
+#include "nsIContent.h"
+#include "nsIDOMWindowUtils.h"
+#include "mozilla/dom/Document.h"
+#include "nsIInterfaceRequestorUtils.h"
+#include "nsIScrollableFrame.h"
+#include "nsLayoutUtils.h"
+#include "nsPrintfCString.h"
+#include "nsPIDOMWindow.h"
+#include "nsRefreshDriver.h"
+#include "nsString.h"
+#include "nsView.h"
+
+static mozilla::LazyLogModule sApzHlpLog("apz.helper");
+#define APZCCH_LOG(...) MOZ_LOG(sApzHlpLog, LogLevel::Debug, (__VA_ARGS__))
+static mozilla::LazyLogModule sDisplayportLog("apz.displayport");
+
+namespace mozilla {
+namespace layers {
+
+using dom::BrowserParent;
+
+uint64_t APZCCallbackHelper::sLastTargetAPZCNotificationInputBlock =
+ uint64_t(-1);
+
+static ScreenMargin RecenterDisplayPort(const ScreenMargin& aDisplayPort) {
+ ScreenMargin margins = aDisplayPort;
+ margins.right = margins.left = margins.LeftRight() / 2;
+ margins.top = margins.bottom = margins.TopBottom() / 2;
+ return margins;
+}
+
+static PresShell* GetPresShell(const nsIContent* aContent) {
+ if (dom::Document* doc = aContent->GetComposedDoc()) {
+ return doc->GetPresShell();
+ }
+ return nullptr;
+}
+
+static CSSPoint ScrollFrameTo(nsIScrollableFrame* aFrame,
+ const RepaintRequest& aRequest,
+ bool& aSuccessOut) {
+ aSuccessOut = false;
+ CSSPoint targetScrollPosition = aRequest.GetLayoutScrollOffset();
+
+ if (!aFrame) {
+ return targetScrollPosition;
+ }
+
+ CSSPoint geckoScrollPosition =
+ CSSPoint::FromAppUnits(aFrame->GetScrollPosition());
+
+ // If the repaint request was triggered due to a previous main-thread scroll
+ // offset update sent to the APZ, then we don't need to do another scroll here
+ // and we can just return.
+ if (!aRequest.GetScrollOffsetUpdated()) {
+ return geckoScrollPosition;
+ }
+
+ // If this frame is overflow:hidden, then the expectation is that it was
+ // sized in a way that respects its scrollable boundaries. For the root
+ // frame, this means that it cannot be scrolled in such a way that it moves
+ // the layout viewport. For a non-root frame, this means that it cannot be
+ // scrolled at all.
+ //
+ // In either case, |targetScrollPosition| should be the same as
+ // |geckoScrollPosition| here.
+ //
+ // However, this is slightly racy. We query the overflow property of the
+ // scroll frame at the time the repaint request arrives at the main thread
+ // (i.e., right now), but APZ made the decision of whether or not to allow
+ // scrolling based on the information it had at the time it processed the
+ // scroll event. The overflow property could have changed at some time
+ // between the two events and so APZ may have computed a scrollable region
+ // that is larger than what is actually allowed.
+ //
+ // Currently, we allow the scroll position to change even though the frame is
+ // overflow:hidden (that is, we take |targetScrollPosition|). If this turns
+ // out to be problematic, an alternative solution would be to ignore the
+ // scroll position change (that is, use |geckoScrollPosition|).
+ if (aFrame->GetScrollStyles().mVertical == StyleOverflow::Hidden &&
+ targetScrollPosition.y != geckoScrollPosition.y) {
+ NS_WARNING(
+ nsPrintfCString(
+ "APZCCH: targetScrollPosition.y (%f) != geckoScrollPosition.y (%f)",
+ targetScrollPosition.y.value, geckoScrollPosition.y.value)
+ .get());
+ }
+ if (aFrame->GetScrollStyles().mHorizontal == StyleOverflow::Hidden &&
+ targetScrollPosition.x != geckoScrollPosition.x) {
+ NS_WARNING(
+ nsPrintfCString(
+ "APZCCH: targetScrollPosition.x (%f) != geckoScrollPosition.x (%f)",
+ targetScrollPosition.x.value, geckoScrollPosition.x.value)
+ .get());
+ }
+
+ // If the scrollable frame is currently in the middle of an async or smooth
+ // scroll then we don't want to interrupt it (see bug 961280).
+ // Also if the scrollable frame got a scroll request from a higher priority
+ // origin since the last layers update, then we don't want to push our scroll
+ // request because we'll clobber that one, which is bad.
+ bool scrollInProgress = APZCCallbackHelper::IsScrollInProgress(aFrame);
+ if (!scrollInProgress) {
+ ScrollSnapTargetIds snapTargetIds = aRequest.GetLastSnapTargetIds();
+ aFrame->ScrollToCSSPixelsForApz(targetScrollPosition,
+ std::move(snapTargetIds));
+ geckoScrollPosition = CSSPoint::FromAppUnits(aFrame->GetScrollPosition());
+ aSuccessOut = true;
+ }
+ // Return the final scroll position after setting it so that anything that
+ // relies on it can have an accurate value. Note that even if we set it above
+ // re-querying it is a good idea because it may have gotten clamped or
+ // rounded.
+ return geckoScrollPosition;
+}
+
+/**
+ * Scroll the scroll frame associated with |aContent| to the scroll position
+ * requested in |aRequest|.
+ *
+ * Any difference between the requested and actual scroll positions is used to
+ * update the callback-transform stored on the content, and return a new
+ * display port.
+ */
+static DisplayPortMargins ScrollFrame(nsIContent* aContent,
+ const RepaintRequest& aRequest) {
+ // Scroll the window to the desired spot
+ nsIScrollableFrame* sf =
+ nsLayoutUtils::FindScrollableFrameFor(aRequest.GetScrollId());
+ if (sf) {
+ sf->ResetScrollInfoIfNeeded(aRequest.GetScrollGeneration(),
+ aRequest.GetScrollGenerationOnApz(),
+ aRequest.GetScrollAnimationType(),
+ nsIScrollableFrame::InScrollingGesture(
+ aRequest.IsInScrollingGesture()));
+ sf->SetScrollableByAPZ(!aRequest.IsScrollInfoLayer());
+ if (sf->IsRootScrollFrameOfDocument()) {
+ if (!APZCCallbackHelper::IsScrollInProgress(sf)) {
+ APZCCH_LOG("Setting VV offset to %s\n",
+ ToString(aRequest.GetVisualScrollOffset()).c_str());
+ if (sf->SetVisualViewportOffset(
+ CSSPoint::ToAppUnits(aRequest.GetVisualScrollOffset()),
+ /* aRepaint = */ false)) {
+ // sf can't be destroyed if SetVisualViewportOffset returned true.
+ sf->MarkEverScrolled();
+ }
+ }
+ }
+ }
+ // sf might have been destroyed by the call to SetVisualViewportOffset, so
+ // re-get it.
+ sf = nsLayoutUtils::FindScrollableFrameFor(aRequest.GetScrollId());
+ bool scrollUpdated = false;
+ auto displayPortMargins =
+ DisplayPortMargins::ForScrollFrame(sf, aRequest.GetDisplayPortMargins());
+ CSSPoint apzScrollOffset = aRequest.GetVisualScrollOffset();
+ CSSPoint actualScrollOffset = ScrollFrameTo(sf, aRequest, scrollUpdated);
+ CSSPoint scrollDelta = apzScrollOffset - actualScrollOffset;
+
+ if (scrollUpdated) {
+ if (aRequest.IsScrollInfoLayer()) {
+ // In cases where the APZ scroll offset is different from the content
+ // scroll offset, we want to interpret the margins as relative to the APZ
+ // scroll offset except when the frame is not scrollable by APZ.
+ // Therefore, if the layer is a scroll info layer, we leave the margins
+ // as-is and they will be interpreted as relative to the content scroll
+ // offset.
+ if (nsIFrame* frame = aContent->GetPrimaryFrame()) {
+ frame->SchedulePaint();
+ }
+ } else {
+ // Correct the display port due to the difference between the requested
+ // and actual scroll offsets.
+ displayPortMargins =
+ DisplayPortMargins::FromAPZ(aRequest.GetDisplayPortMargins(),
+ apzScrollOffset, actualScrollOffset);
+ }
+ } else if (aRequest.IsRootContent() &&
+ apzScrollOffset != aRequest.GetLayoutScrollOffset()) {
+ // APZ uses the visual viewport's offset to calculate where to place the
+ // display port, so the display port is misplaced when a pinch zoom occurs.
+ //
+ // We need to force a display port adjustment in the following paint to
+ // account for a difference between the requested and actual scroll
+ // offsets in repaints requested by
+ // AsyncPanZoomController::NotifyLayersUpdated.
+ displayPortMargins = DisplayPortMargins::FromAPZ(
+ aRequest.GetDisplayPortMargins(), apzScrollOffset, actualScrollOffset);
+ } else {
+ // For whatever reason we couldn't update the scroll offset on the scroll
+ // frame, which means the data APZ used for its displayport calculation is
+ // stale. Fall back to a sane default behaviour. Note that we don't
+ // tile-align the recentered displayport because tile-alignment depends on
+ // the scroll position, and the scroll position here is out of our control.
+ // See bug 966507 comment 21 for a more detailed explanation.
+ displayPortMargins = DisplayPortMargins::ForScrollFrame(
+ sf, RecenterDisplayPort(aRequest.GetDisplayPortMargins()));
+ }
+
+ // APZ transforms inputs assuming we applied the exact scroll offset it
+ // requested (|apzScrollOffset|). Since we may not have, record the difference
+ // between what APZ asked for and what we actually applied, and apply it to
+ // input events to compensate.
+ // Note that if the main-thread had a change in its scroll position, we don't
+ // want to record that difference here, because it can be large and throw off
+ // input events by a large amount. It is also going to be transient, because
+ // any main-thread scroll position change will be synced to APZ and we will
+ // get another repaint request when APZ confirms. In the interval while this
+ // is happening we can just leave the callback transform as it was.
+ bool mainThreadScrollChanged =
+ sf && sf->CurrentScrollGeneration() != aRequest.GetScrollGeneration() &&
+ nsLayoutUtils::CanScrollOriginClobberApz(sf->LastScrollOrigin());
+ if (aContent && !mainThreadScrollChanged) {
+ aContent->SetProperty(nsGkAtoms::apzCallbackTransform,
+ new CSSPoint(scrollDelta),
+ nsINode::DeleteProperty<CSSPoint>);
+ }
+
+ return displayPortMargins;
+}
+
+static void SetDisplayPortMargins(PresShell* aPresShell, nsIContent* aContent,
+ const DisplayPortMargins& aDisplayPortMargins,
+ CSSSize aDisplayPortBase) {
+ if (!aContent) {
+ return;
+ }
+
+ bool hadDisplayPort = DisplayPortUtils::HasDisplayPort(aContent);
+ if (MOZ_LOG_TEST(sDisplayportLog, LogLevel::Debug)) {
+ if (!hadDisplayPort) {
+ mozilla::layers::ScrollableLayerGuid::ViewID viewID =
+ mozilla::layers::ScrollableLayerGuid::NULL_SCROLL_ID;
+ nsLayoutUtils::FindIDFor(aContent, &viewID);
+ MOZ_LOG(
+ sDisplayportLog, LogLevel::Debug,
+ ("APZCCH installing displayport margins %s on scrollId=%" PRIu64 "\n",
+ ToString(aDisplayPortMargins).c_str(), viewID));
+ }
+ }
+ DisplayPortUtils::SetDisplayPortMargins(
+ aContent, aPresShell, aDisplayPortMargins,
+ hadDisplayPort ? DisplayPortUtils::ClearMinimalDisplayPortProperty::No
+ : DisplayPortUtils::ClearMinimalDisplayPortProperty::Yes,
+ 0);
+ if (!hadDisplayPort) {
+ DisplayPortUtils::SetZeroMarginDisplayPortOnAsyncScrollableAncestors(
+ aContent->GetPrimaryFrame());
+ }
+
+ nsRect base(0, 0, aDisplayPortBase.width * AppUnitsPerCSSPixel(),
+ aDisplayPortBase.height * AppUnitsPerCSSPixel());
+ DisplayPortUtils::SetDisplayPortBaseIfNotSet(aContent, base);
+}
+
+static void SetPaintRequestTime(nsIContent* aContent,
+ const TimeStamp& aPaintRequestTime) {
+ aContent->SetProperty(nsGkAtoms::paintRequestTime,
+ new TimeStamp(aPaintRequestTime),
+ nsINode::DeleteProperty<TimeStamp>);
+}
+
+void APZCCallbackHelper::NotifyLayerTransforms(
+ const nsTArray<MatrixMessage>& aTransforms) {
+ MOZ_ASSERT(NS_IsMainThread());
+ for (const MatrixMessage& msg : aTransforms) {
+ BrowserParent* parent =
+ BrowserParent::GetBrowserParentFromLayersId(msg.GetLayersId());
+ if (parent) {
+ parent->SetChildToParentConversionMatrix(
+ ViewAs<LayoutDeviceToLayoutDeviceMatrix4x4>(
+ msg.GetMatrix(),
+ PixelCastJustification::ContentProcessIsLayerInUiProcess),
+ msg.GetTopLevelViewportVisibleRectInBrowserCoords());
+ }
+ }
+}
+
+void APZCCallbackHelper::UpdateRootFrame(const RepaintRequest& aRequest) {
+ if (aRequest.GetScrollId() == ScrollableLayerGuid::NULL_SCROLL_ID) {
+ return;
+ }
+ RefPtr<nsIContent> content =
+ nsLayoutUtils::FindContentFor(aRequest.GetScrollId());
+ if (!content) {
+ return;
+ }
+
+ RefPtr<PresShell> presShell = GetPresShell(content);
+ if (!presShell || aRequest.GetPresShellId() != presShell->GetPresShellId()) {
+ return;
+ }
+
+ APZCCH_LOG("Handling request %s\n", ToString(aRequest).c_str());
+ if (nsLayoutUtils::AllowZoomingForDocument(presShell->GetDocument()) &&
+ aRequest.GetAsyncZoom().scale != 1.0) {
+ // If zooming is disabled then we don't really want to let APZ fiddle
+ // with these things. In theory setting the resolution here should be a
+ // no-op, but setting the visual viewport size is bad because it can cause a
+ // stale value to be returned by window.innerWidth/innerHeight (see bug
+ // 1187792).
+
+ float presShellResolution = presShell->GetResolution();
+
+ // If the pres shell resolution has changed on the content side side
+ // the time this repaint request was fired, consider this request out of
+ // date and drop it; setting a zoom based on the out-of-date resolution can
+ // have the effect of getting us stuck with the stale resolution.
+ // One might think that if the last ResolutionChangeOrigin was apz then the
+ // pres shell resolutions should match but
+ // that is not the case. We can get multiple repaint requests that has the
+ // same pres shell resolution (because apz didn't receive a content layers
+ // update inbetween) if the first has async zoom we apply that and chance
+ // the content pres shell resolution and thus when handling the second
+ // repaint request the pres shell resolution won't match. So that's why we
+ // also check if the last resolution change origin was apz (aka 'us').
+ if (!FuzzyEqualsMultiplicative(presShellResolution,
+ aRequest.GetPresShellResolution()) &&
+ presShell->GetLastResolutionChangeOrigin() !=
+ ResolutionChangeOrigin::Apz) {
+ return;
+ }
+
+ // The pres shell resolution is updated by the the async zoom since the
+ // last paint.
+ // We want to calculate the new presshell resolution as
+ // |aRequest.GetPresShellResolution() * aRequest.GetAsyncZoom()| but that
+ // calculation can lead to small inaccuracies due to limited floating point
+ // precision. Specifically,
+ // clang-format off
+ // asyncZoom = zoom / layerPixelsPerCSSPixel
+ // = zoom / (devPixelsPerCSSPixel * cumulativeResolution)
+ // clang-format on
+ // Since this is a root frame we generally do not allow css transforms to
+ // scale it, so it is very likely that cumulativeResolution ==
+ // presShellResoluion. So
+ // clang-format off
+ // newPresShellResoluion = presShellResoluion * asyncZoom
+ // = presShellResoluion * zoom / (devPixelsPerCSSPixel * presShellResoluion)
+ // = zoom / devPixelsPerCSSPixel
+ // clang-format on
+ // However, we want to keep the calculation general and so we do not assume
+ // presShellResoluion == cumulativeResolution, but rather factor those
+ // values out so they cancel and the floating point division has a very high
+ // probability of being exactly 1.
+ presShellResolution =
+ (aRequest.GetPresShellResolution() /
+ aRequest.GetCumulativeResolution().scale) *
+ (aRequest.GetZoom() / aRequest.GetDevPixelsPerCSSPixel()).scale;
+ presShell->SetResolutionAndScaleTo(presShellResolution,
+ ResolutionChangeOrigin::Apz);
+
+ // Changing the resolution will trigger a reflow which will cause the
+ // main-thread scroll position to be realigned in layer pixels. This
+ // (subpixel) scroll mutation can trigger a scroll update to APZ which
+ // is undesirable. Instead of having that happen as part of the post-reflow
+ // code, we force it to happen here with ScrollOrigin::Apz so that it
+ // doesn't trigger a scroll update to APZ.
+ nsIScrollableFrame* sf =
+ nsLayoutUtils::FindScrollableFrameFor(aRequest.GetScrollId());
+ CSSPoint currentScrollPosition =
+ CSSPoint::FromAppUnits(sf->GetScrollPosition());
+ ScrollSnapTargetIds snapTargetIds = aRequest.GetLastSnapTargetIds();
+ sf->ScrollToCSSPixelsForApz(currentScrollPosition,
+ std::move(snapTargetIds));
+ }
+
+ // Do this as late as possible since scrolling can flush layout. It also
+ // adjusts the display port margins, so do it before we set those.
+ DisplayPortMargins displayPortMargins = ScrollFrame(content, aRequest);
+
+ SetDisplayPortMargins(presShell, content, displayPortMargins,
+ aRequest.CalculateCompositedSizeInCssPixels());
+ SetPaintRequestTime(content, aRequest.GetPaintRequestTime());
+}
+
+void APZCCallbackHelper::UpdateSubFrame(const RepaintRequest& aRequest) {
+ if (aRequest.GetScrollId() == ScrollableLayerGuid::NULL_SCROLL_ID) {
+ return;
+ }
+ RefPtr<nsIContent> content =
+ nsLayoutUtils::FindContentFor(aRequest.GetScrollId());
+ if (!content) {
+ return;
+ }
+
+ // We don't currently support zooming for subframes, so nothing extra
+ // needs to be done beyond the tasks common to this and UpdateRootFrame.
+ DisplayPortMargins displayPortMargins = ScrollFrame(content, aRequest);
+ if (RefPtr<PresShell> presShell = GetPresShell(content)) {
+ SetDisplayPortMargins(presShell, content, displayPortMargins,
+ aRequest.CalculateCompositedSizeInCssPixels());
+ }
+ SetPaintRequestTime(content, aRequest.GetPaintRequestTime());
+}
+
+bool APZCCallbackHelper::GetOrCreateScrollIdentifiers(
+ nsIContent* aContent, uint32_t* aPresShellIdOut,
+ ScrollableLayerGuid::ViewID* aViewIdOut) {
+ if (!aContent) {
+ return false;
+ }
+ *aViewIdOut = nsLayoutUtils::FindOrCreateIDFor(aContent);
+ if (PresShell* presShell = GetPresShell(aContent)) {
+ *aPresShellIdOut = presShell->GetPresShellId();
+ return true;
+ }
+ return false;
+}
+
+void APZCCallbackHelper::InitializeRootDisplayport(PresShell* aPresShell) {
+ // Create a view-id and set a zero-margin displayport for the root element
+ // of the root document in the chrome process. This ensures that the scroll
+ // frame for this element gets an APZC, which in turn ensures that all content
+ // in the chrome processes is covered by an APZC.
+ // The displayport is zero-margin because this element is generally not
+ // actually scrollable (if it is, APZC will set proper margins when it's
+ // scrolled).
+ if (!aPresShell) {
+ return;
+ }
+
+ MOZ_ASSERT(aPresShell->GetDocument());
+ nsIContent* content = aPresShell->GetDocument()->GetDocumentElement();
+ if (!content) {
+ return;
+ }
+
+ uint32_t presShellId;
+ ScrollableLayerGuid::ViewID viewId;
+ if (APZCCallbackHelper::GetOrCreateScrollIdentifiers(content, &presShellId,
+ &viewId)) {
+ MOZ_LOG(
+ sDisplayportLog, LogLevel::Debug,
+ ("Initializing root displayport on scrollId=%" PRIu64 "\n", viewId));
+ Maybe<nsRect> baseRect =
+ DisplayPortUtils::GetRootDisplayportBase(aPresShell);
+ if (baseRect) {
+ DisplayPortUtils::SetDisplayPortBaseIfNotSet(content, *baseRect);
+ }
+
+ DisplayPortUtils::SetDisplayPortMargins(
+ content, aPresShell, DisplayPortMargins::Empty(content),
+ DisplayPortUtils::ClearMinimalDisplayPortProperty::Yes, 0);
+ DisplayPortUtils::SetZeroMarginDisplayPortOnAsyncScrollableAncestors(
+ content->GetPrimaryFrame());
+ }
+}
+
+nsPresContext* APZCCallbackHelper::GetPresContextForContent(
+ nsIContent* aContent) {
+ dom::Document* doc = aContent->GetComposedDoc();
+ if (!doc) {
+ return nullptr;
+ }
+ PresShell* presShell = doc->GetPresShell();
+ if (!presShell) {
+ return nullptr;
+ }
+ return presShell->GetPresContext();
+}
+
+PresShell* APZCCallbackHelper::GetRootContentDocumentPresShellForContent(
+ nsIContent* aContent) {
+ nsPresContext* context = GetPresContextForContent(aContent);
+ if (!context) {
+ return nullptr;
+ }
+ context = context->GetInProcessRootContentDocumentPresContext();
+ if (!context) {
+ return nullptr;
+ }
+ return context->PresShell();
+}
+
+nsEventStatus APZCCallbackHelper::DispatchWidgetEvent(WidgetGUIEvent& aEvent) {
+ nsEventStatus status = nsEventStatus_eConsumeNoDefault;
+ if (aEvent.mWidget) {
+ aEvent.mWidget->DispatchEvent(&aEvent, status);
+ }
+ return status;
+}
+
+nsEventStatus APZCCallbackHelper::DispatchSynthesizedMouseEvent(
+ EventMessage aMsg, const LayoutDevicePoint& aRefPoint, Modifiers aModifiers,
+ int32_t aClickCount, nsIWidget* aWidget) {
+ MOZ_ASSERT(aMsg == eMouseMove || aMsg == eMouseDown || aMsg == eMouseUp ||
+ aMsg == eMouseLongTap);
+
+ WidgetMouseEvent event(true, aMsg, aWidget, WidgetMouseEvent::eReal,
+ WidgetMouseEvent::eNormal);
+ event.mRefPoint = LayoutDeviceIntPoint::Truncate(aRefPoint.x, aRefPoint.y);
+ event.mButton = MouseButton::ePrimary;
+ event.mButtons |= MouseButtonsFlag::ePrimaryFlag;
+ event.mInputSource = dom::MouseEvent_Binding::MOZ_SOURCE_TOUCH;
+ if (aMsg == eMouseLongTap) {
+ event.mFlags.mOnlyChromeDispatch = true;
+ }
+ if (aMsg != eMouseMove) {
+ event.mClickCount = aClickCount;
+ }
+ event.mModifiers = aModifiers;
+ // Real touch events will generate corresponding pointer events. We set
+ // convertToPointer to false to prevent the synthesized mouse events generate
+ // pointer events again.
+ event.convertToPointer = false;
+ return DispatchWidgetEvent(event);
+}
+
+PreventDefaultResult APZCCallbackHelper::DispatchMouseEvent(
+ PresShell* aPresShell, const nsString& aType, const CSSPoint& aPoint,
+ int32_t aButton, int32_t aClickCount, int32_t aModifiers,
+ unsigned short aInputSourceArg, uint32_t aPointerId) {
+ NS_ENSURE_TRUE(aPresShell, PreventDefaultResult::ByContent);
+
+ PreventDefaultResult preventDefaultResult;
+ nsContentUtils::SendMouseEvent(
+ aPresShell, aType, aPoint.x, aPoint.y, aButton,
+ nsIDOMWindowUtils::MOUSE_BUTTONS_NOT_SPECIFIED, aClickCount, aModifiers,
+ /* aIgnoreRootScrollFrame = */ false, 0, aInputSourceArg, aPointerId,
+ false, &preventDefaultResult, false,
+ /* aIsWidgetEventSynthesized = */ false);
+ return preventDefaultResult;
+}
+
+void APZCCallbackHelper::FireSingleTapEvent(const LayoutDevicePoint& aPoint,
+ Modifiers aModifiers,
+ int32_t aClickCount,
+ nsIWidget* aWidget) {
+ if (aWidget->Destroyed()) {
+ return;
+ }
+ APZCCH_LOG("Dispatching single-tap component events to %s\n",
+ ToString(aPoint).c_str());
+ DispatchSynthesizedMouseEvent(eMouseMove, aPoint, aModifiers, aClickCount,
+ aWidget);
+ DispatchSynthesizedMouseEvent(eMouseDown, aPoint, aModifiers, aClickCount,
+ aWidget);
+ DispatchSynthesizedMouseEvent(eMouseUp, aPoint, aModifiers, aClickCount,
+ aWidget);
+}
+
+static dom::Element* GetDisplayportElementFor(
+ nsIScrollableFrame* aScrollableFrame) {
+ if (!aScrollableFrame) {
+ return nullptr;
+ }
+ nsIFrame* scrolledFrame = aScrollableFrame->GetScrolledFrame();
+ if (!scrolledFrame) {
+ return nullptr;
+ }
+ // |scrolledFrame| should at this point be the root content frame of the
+ // nearest ancestor scrollable frame. The element corresponding to this
+ // frame should be the one with the displayport set on it, so find that
+ // element and return it.
+ nsIContent* content = scrolledFrame->GetContent();
+ MOZ_ASSERT(content->IsElement()); // roc says this must be true
+ return content->AsElement();
+}
+
+static dom::Element* GetRootDocumentElementFor(nsIWidget* aWidget) {
+ // This returns the root element that ChromeProcessController sets the
+ // displayport on during initialization.
+ if (nsView* view = nsView::GetViewFor(aWidget)) {
+ if (PresShell* presShell = view->GetPresShell()) {
+ MOZ_ASSERT(presShell->GetDocument());
+ return presShell->GetDocument()->GetDocumentElement();
+ }
+ }
+ return nullptr;
+}
+
+namespace {
+
+using FrameForPointOption = nsLayoutUtils::FrameForPointOption;
+
+// Determine the scrollable target frame for the given point and add it to
+// the target list. If the frame doesn't have a displayport, set one.
+// Return whether or not the frame had a displayport that has already been
+// painted (in this case, the caller can send the SetTargetAPZC notification
+// right away, rather than waiting for a transaction to propagate the
+// displayport to APZ first).
+static bool PrepareForSetTargetAPZCNotification(
+ nsIWidget* aWidget, const LayersId& aLayersId, nsIFrame* aRootFrame,
+ const LayoutDeviceIntPoint& aRefPoint,
+ nsTArray<ScrollableLayerGuid>* aTargets) {
+ ScrollableLayerGuid guid(aLayersId, 0, ScrollableLayerGuid::NULL_SCROLL_ID);
+ RelativeTo relativeTo{aRootFrame, ViewportType::Visual};
+ nsPoint point = nsLayoutUtils::GetEventCoordinatesRelativeTo(
+ aWidget, aRefPoint, relativeTo);
+ nsIFrame* target = nsLayoutUtils::GetFrameForPoint(relativeTo, point);
+ nsIScrollableFrame* scrollAncestor =
+ target ? nsLayoutUtils::GetAsyncScrollableAncestorFrame(target)
+ : aRootFrame->PresShell()->GetRootScrollFrameAsScrollable();
+
+ // Assuming that if there's no scrollAncestor, there's already a displayPort.
+ nsCOMPtr<dom::Element> dpElement =
+ scrollAncestor ? GetDisplayportElementFor(scrollAncestor)
+ : GetRootDocumentElementFor(aWidget);
+
+ if (MOZ_LOG_TEST(sApzHlpLog, LogLevel::Debug)) {
+ nsAutoString dpElementDesc;
+ if (dpElement) {
+ dpElement->Describe(dpElementDesc);
+ }
+ APZCCH_LOG("For event at %s found scrollable element %p (%s)\n",
+ ToString(aRefPoint).c_str(), dpElement.get(),
+ NS_LossyConvertUTF16toASCII(dpElementDesc).get());
+ }
+
+ bool guidIsValid = APZCCallbackHelper::GetOrCreateScrollIdentifiers(
+ dpElement, &(guid.mPresShellId), &(guid.mScrollId));
+ aTargets->AppendElement(guid);
+
+ if (!guidIsValid) {
+ return false;
+ }
+ if (DisplayPortUtils::HasNonMinimalNonZeroDisplayPort(dpElement)) {
+ // If the element has a displayport but it hasn't been painted yet,
+ // we want the caller to wait for the paint to happen, but we don't
+ // need to set the displayport here since it's already been set.
+ return !DisplayPortUtils::HasPaintedDisplayPort(dpElement);
+ }
+
+ if (!scrollAncestor) {
+ // This can happen if the document element gets swapped out after
+ // ChromeProcessController runs InitializeRootDisplayport. In this case
+ // let's try to set a displayport again and bail out on this operation.
+ APZCCH_LOG("Widget %p's document element %p didn't have a displayport\n",
+ aWidget, dpElement.get());
+ APZCCallbackHelper::InitializeRootDisplayport(aRootFrame->PresShell());
+ return false;
+ }
+
+ APZCCH_LOG("%p didn't have a displayport, so setting one...\n",
+ dpElement.get());
+ MOZ_LOG(sDisplayportLog, LogLevel::Debug,
+ ("Activating displayport on scrollId=%" PRIu64 " for SetTargetAPZC\n",
+ guid.mScrollId));
+ bool activated = DisplayPortUtils::CalculateAndSetDisplayPortMargins(
+ scrollAncestor, DisplayPortUtils::RepaintMode::Repaint);
+ if (!activated) {
+ return false;
+ }
+
+ nsIFrame* frame = do_QueryFrame(scrollAncestor);
+ DisplayPortUtils::SetZeroMarginDisplayPortOnAsyncScrollableAncestors(frame);
+
+ return !DisplayPortUtils::HasPaintedDisplayPort(dpElement);
+}
+
+static void SendLayersDependentApzcTargetConfirmation(
+ nsIWidget* aWidget, uint64_t aInputBlockId,
+ nsTArray<ScrollableLayerGuid>&& aTargets) {
+ WindowRenderer* renderer = aWidget->GetWindowRenderer();
+ if (!renderer) {
+ return;
+ }
+
+ if (WebRenderLayerManager* wrlm = renderer->AsWebRender()) {
+ if (WebRenderBridgeChild* wrbc = wrlm->WrBridge()) {
+ wrbc->SendSetConfirmedTargetAPZC(aInputBlockId, aTargets);
+ }
+ return;
+ }
+}
+
+} // namespace
+
+DisplayportSetListener::DisplayportSetListener(
+ nsIWidget* aWidget, nsPresContext* aPresContext,
+ const uint64_t& aInputBlockId, nsTArray<ScrollableLayerGuid>&& aTargets)
+ : ManagedPostRefreshObserver(aPresContext),
+ mWidget(aWidget),
+ mInputBlockId(aInputBlockId),
+ mTargets(std::move(aTargets)) {
+ MOZ_ASSERT(!mAction, "Setting Action twice");
+ mAction = [instance = MOZ_KnownLive(this)](bool aWasCanceled) {
+ instance->OnPostRefresh();
+ return Unregister::Yes;
+ };
+}
+
+DisplayportSetListener::~DisplayportSetListener() = default;
+
+void DisplayportSetListener::Register() {
+ APZCCH_LOG("DisplayportSetListener::Register\n");
+ mPresContext->RegisterManagedPostRefreshObserver(this);
+}
+
+void DisplayportSetListener::OnPostRefresh() {
+ APZCCH_LOG("Got refresh, sending target APZCs for input block %" PRIu64 "\n",
+ mInputBlockId);
+ SendLayersDependentApzcTargetConfirmation(mWidget, mInputBlockId,
+ std::move(mTargets));
+}
+
+already_AddRefed<DisplayportSetListener>
+APZCCallbackHelper::SendSetTargetAPZCNotification(nsIWidget* aWidget,
+ dom::Document* aDocument,
+ const WidgetGUIEvent& aEvent,
+ const LayersId& aLayersId,
+ uint64_t aInputBlockId) {
+ if (!aWidget || !aDocument) {
+ return nullptr;
+ }
+ if (aInputBlockId == sLastTargetAPZCNotificationInputBlock) {
+ // We have already confirmed the target APZC for a previous event of this
+ // input block. If we activated a scroll frame for this input block,
+ // sending another target APZC confirmation would be harmful, as it might
+ // race the original confirmation (which needs to go through a layers
+ // transaction).
+ APZCCH_LOG("Not resending target APZC confirmation for input block %" PRIu64
+ "\n",
+ aInputBlockId);
+ return nullptr;
+ }
+ sLastTargetAPZCNotificationInputBlock = aInputBlockId;
+ if (PresShell* presShell = aDocument->GetPresShell()) {
+ if (nsIFrame* rootFrame = presShell->GetRootFrame()) {
+ bool waitForRefresh = false;
+ nsTArray<ScrollableLayerGuid> targets;
+
+ if (const WidgetTouchEvent* touchEvent = aEvent.AsTouchEvent()) {
+ for (size_t i = 0; i < touchEvent->mTouches.Length(); i++) {
+ waitForRefresh |= PrepareForSetTargetAPZCNotification(
+ aWidget, aLayersId, rootFrame, touchEvent->mTouches[i]->mRefPoint,
+ &targets);
+ }
+ } else if (const WidgetWheelEvent* wheelEvent = aEvent.AsWheelEvent()) {
+ waitForRefresh = PrepareForSetTargetAPZCNotification(
+ aWidget, aLayersId, rootFrame, wheelEvent->mRefPoint, &targets);
+ } else if (const WidgetMouseEvent* mouseEvent = aEvent.AsMouseEvent()) {
+ waitForRefresh = PrepareForSetTargetAPZCNotification(
+ aWidget, aLayersId, rootFrame, mouseEvent->mRefPoint, &targets);
+ }
+ // TODO: Do other types of events need to be handled?
+
+ if (!targets.IsEmpty()) {
+ if (waitForRefresh) {
+ APZCCH_LOG(
+ "At least one target got a new displayport, need to wait for "
+ "refresh\n");
+ return MakeAndAddRef<DisplayportSetListener>(
+ aWidget, presShell->GetPresContext(), aInputBlockId,
+ std::move(targets));
+ }
+ APZCCH_LOG("Sending target APZCs for input block %" PRIu64 "\n",
+ aInputBlockId);
+ aWidget->SetConfirmedTargetAPZC(aInputBlockId, targets);
+ }
+ }
+ }
+ return nullptr;
+}
+
+void APZCCallbackHelper::NotifyMozMouseScrollEvent(
+ const ScrollableLayerGuid::ViewID& aScrollId, const nsString& aEvent) {
+ nsCOMPtr<nsIContent> targetContent = nsLayoutUtils::FindContentFor(aScrollId);
+ if (!targetContent) {
+ return;
+ }
+ RefPtr<dom::Document> ownerDoc = targetContent->OwnerDoc();
+ if (!ownerDoc) {
+ return;
+ }
+
+ nsContentUtils::DispatchEventOnlyToChrome(ownerDoc, targetContent, aEvent,
+ CanBubble::eYes, Cancelable::eYes);
+}
+
+void APZCCallbackHelper::NotifyFlushComplete(PresShell* aPresShell) {
+ MOZ_ASSERT(NS_IsMainThread());
+ // In some cases, flushing the APZ state to the main thread doesn't actually
+ // trigger a flush and repaint (this is an intentional optimization - the
+ // stuff visible to the user is still correct). However, reftests update their
+ // snapshot based on invalidation events that are emitted during paints,
+ // so we ensure that we kick off a paint when an APZ flush is done. Note that
+ // only chrome/testing code can trigger this behaviour.
+ if (aPresShell && aPresShell->GetRootFrame()) {
+ aPresShell->GetRootFrame()->SchedulePaint(nsIFrame::PAINT_DEFAULT, false);
+ }
+
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ MOZ_ASSERT(observerService);
+ observerService->NotifyObservers(nullptr, "apz-repaints-flushed", nullptr);
+}
+
+/* static */
+bool APZCCallbackHelper::IsScrollInProgress(nsIScrollableFrame* aFrame) {
+ using AnimationState = nsIScrollableFrame::AnimationState;
+
+ return aFrame->ScrollAnimationState().contains(AnimationState::MainThread) ||
+ nsLayoutUtils::CanScrollOriginClobberApz(aFrame->LastScrollOrigin());
+}
+
+/* static */
+void APZCCallbackHelper::NotifyAsyncScrollbarDragInitiated(
+ uint64_t aDragBlockId, const ScrollableLayerGuid::ViewID& aScrollId,
+ ScrollDirection aDirection) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (nsIScrollableFrame* scrollFrame =
+ nsLayoutUtils::FindScrollableFrameFor(aScrollId)) {
+ scrollFrame->AsyncScrollbarDragInitiated(aDragBlockId, aDirection);
+ }
+}
+
+/* static */
+void APZCCallbackHelper::NotifyAsyncScrollbarDragRejected(
+ const ScrollableLayerGuid::ViewID& aScrollId) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (nsIScrollableFrame* scrollFrame =
+ nsLayoutUtils::FindScrollableFrameFor(aScrollId)) {
+ scrollFrame->AsyncScrollbarDragRejected();
+ }
+}
+
+/* static */
+void APZCCallbackHelper::NotifyAsyncAutoscrollRejected(
+ const ScrollableLayerGuid::ViewID& aScrollId) {
+ MOZ_ASSERT(NS_IsMainThread());
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ MOZ_ASSERT(observerService);
+
+ nsAutoString data;
+ data.AppendInt(aScrollId);
+ observerService->NotifyObservers(nullptr, "autoscroll-rejected-by-apz",
+ data.get());
+}
+
+/* static */
+void APZCCallbackHelper::CancelAutoscroll(
+ const ScrollableLayerGuid::ViewID& aScrollId) {
+ MOZ_ASSERT(NS_IsMainThread());
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ MOZ_ASSERT(observerService);
+
+ nsAutoString data;
+ data.AppendInt(aScrollId);
+ observerService->NotifyObservers(nullptr, "apz:cancel-autoscroll",
+ data.get());
+}
+
+/* static */
+void APZCCallbackHelper::NotifyScaleGestureComplete(
+ const nsCOMPtr<nsIWidget>& aWidget, float aScale) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (nsView* view = nsView::GetViewFor(aWidget)) {
+ if (PresShell* presShell = view->GetPresShell()) {
+ dom::Document* doc = presShell->GetDocument();
+ MOZ_ASSERT(doc);
+ if (nsPIDOMWindowInner* win = doc->GetInnerWindow()) {
+ dom::AutoJSAPI jsapi;
+ if (!jsapi.Init(win)) {
+ return;
+ }
+
+ JSContext* cx = jsapi.cx();
+ JS::Rooted<JS::Value> detail(cx, JS::Float32Value(aScale));
+ RefPtr<dom::CustomEvent> event =
+ NS_NewDOMCustomEvent(doc, nullptr, nullptr);
+ event->InitCustomEvent(cx, u"MozScaleGestureComplete"_ns,
+ /* CanBubble */ true,
+ /* Cancelable */ false, detail);
+ event->SetTrusted(true);
+ AsyncEventDispatcher* dispatcher = new AsyncEventDispatcher(doc, event);
+ dispatcher->mOnlyChromeDispatch = ChromeOnlyDispatch::eYes;
+
+ dispatcher->PostDOMEvent();
+ }
+ }
+ }
+}
+
+/* static */
+void APZCCallbackHelper::NotifyPinchGesture(
+ PinchGestureInput::PinchGestureType aType,
+ const LayoutDevicePoint& aFocusPoint, LayoutDeviceCoord aSpanChange,
+ Modifiers aModifiers, const nsCOMPtr<nsIWidget>& aWidget) {
+ APZCCH_LOG("APZCCallbackHelper dispatching pinch gesture\n");
+ EventMessage msg;
+ switch (aType) {
+ case PinchGestureInput::PINCHGESTURE_START:
+ msg = eMagnifyGestureStart;
+ break;
+ case PinchGestureInput::PINCHGESTURE_SCALE:
+ msg = eMagnifyGestureUpdate;
+ break;
+ case PinchGestureInput::PINCHGESTURE_FINGERLIFTED:
+ case PinchGestureInput::PINCHGESTURE_END:
+ msg = eMagnifyGesture;
+ break;
+ }
+
+ WidgetSimpleGestureEvent event(true, msg, aWidget.get());
+ // XXX mDelta for the eMagnifyGesture event is supposed to be the
+ // cumulative magnification over the entire gesture (per docs in
+ // SimpleGestureEvent.webidl) but currently APZ just sends us a zero
+ // aSpanChange for that event, so the mDelta is wrong. Nothing relies
+ // on that currently, but we might want to fix it at some point.
+ event.mDelta = aSpanChange;
+ event.mModifiers = aModifiers;
+ event.mRefPoint = RoundedToInt(aFocusPoint);
+
+ DispatchWidgetEvent(event);
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/util/APZCCallbackHelper.h b/gfx/layers/apz/util/APZCCallbackHelper.h
new file mode 100644
index 0000000000..7b1f7cb88b
--- /dev/null
+++ b/gfx/layers/apz/util/APZCCallbackHelper.h
@@ -0,0 +1,195 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_APZCCallbackHelper_h
+#define mozilla_layers_APZCCallbackHelper_h
+
+#include "InputData.h"
+#include "LayersTypes.h"
+#include "Units.h"
+#include "mozilla/EventForwards.h"
+#include "mozilla/layers/MatrixMessage.h"
+#include "nsRefreshObservers.h"
+
+#include <functional>
+
+class nsIContent;
+class nsIScrollableFrame;
+class nsIWidget;
+class nsPresContext;
+template <class T>
+struct already_AddRefed;
+template <class T>
+class nsCOMPtr;
+
+namespace mozilla {
+
+class PresShell;
+enum class PreventDefaultResult : uint8_t;
+
+namespace layers {
+
+struct RepaintRequest;
+
+/* Refer to documentation on SendSetTargetAPZCNotification for this class */
+class DisplayportSetListener : public ManagedPostRefreshObserver {
+ public:
+ DisplayportSetListener(nsIWidget* aWidget, nsPresContext*,
+ const uint64_t& aInputBlockId,
+ nsTArray<ScrollableLayerGuid>&& aTargets);
+ virtual ~DisplayportSetListener();
+ void Register();
+
+ private:
+ RefPtr<nsIWidget> mWidget;
+ uint64_t mInputBlockId;
+ nsTArray<ScrollableLayerGuid> mTargets;
+
+ void OnPostRefresh();
+};
+
+/* This class contains some helper methods that facilitate implementing the
+ GeckoContentController callback interface required by the
+ AsyncPanZoomController. Since different platforms need to implement this
+ interface in similar-but- not-quite-the-same ways, this utility class
+ provides some helpful methods to hold code that can be shared across the
+ different platform implementations.
+ */
+class APZCCallbackHelper {
+ typedef mozilla::layers::ScrollableLayerGuid ScrollableLayerGuid;
+
+ public:
+ static void NotifyLayerTransforms(const nsTArray<MatrixMessage>& aTransforms);
+
+ /* Applies the scroll and zoom parameters from the given RepaintRequest object
+ to the root frame for the given metrics' scrollId. If tiled thebes layers
+ are enabled, this will align the displayport to tile boundaries. Setting
+ the scroll position can cause some small adjustments to be made to the
+ actual scroll position. */
+ static void UpdateRootFrame(const RepaintRequest& aRequest);
+
+ /* Applies the scroll parameters from the given RepaintRequest object to the
+ subframe corresponding to given metrics' scrollId. If tiled thebes
+ layers are enabled, this will align the displayport to tile boundaries.
+ Setting the scroll position can cause some small adjustments to be made
+ to the actual scroll position. */
+ static void UpdateSubFrame(const RepaintRequest& aRequest);
+
+ /* Get the presShellId and view ID for the given content element.
+ * If the view ID does not exist, one is created.
+ * The pres shell ID should generally already exist; if it doesn't for some
+ * reason, false is returned. */
+ static bool GetOrCreateScrollIdentifiers(
+ nsIContent* aContent, uint32_t* aPresShellIdOut,
+ ScrollableLayerGuid::ViewID* aViewIdOut);
+
+ /* Initialize a zero-margin displayport on the root document element of the
+ given presShell. */
+ static void InitializeRootDisplayport(PresShell* aPresShell);
+
+ /* Get the pres context associated with the document enclosing |aContent|. */
+ static nsPresContext* GetPresContextForContent(nsIContent* aContent);
+
+ /* Get the pres shell associated with the root content document enclosing
+ * |aContent|. */
+ static PresShell* GetRootContentDocumentPresShellForContent(
+ nsIContent* aContent);
+
+ /* Dispatch a widget event via the widget stored in the event, if any.
+ * In a child process, allows the BrowserParent event-capture mechanism to
+ * intercept the event. */
+ static nsEventStatus DispatchWidgetEvent(WidgetGUIEvent& aEvent);
+
+ /* Synthesize a mouse event with the given parameters, and dispatch it
+ * via the given widget. */
+ static nsEventStatus DispatchSynthesizedMouseEvent(
+ EventMessage aMsg, const LayoutDevicePoint& aRefPoint,
+ Modifiers aModifiers, int32_t aClickCount, nsIWidget* aWidget);
+
+ /* Dispatch a mouse event with the given parameters.
+ * Return whether or not any listeners have called preventDefault on the
+ * event.
+ * This is a lightweight wrapper around nsContentUtils::SendMouseEvent()
+ * and as such expects |aPoint| to be in layout coordinates. */
+ MOZ_CAN_RUN_SCRIPT
+ static PreventDefaultResult DispatchMouseEvent(
+ PresShell* aPresShell, const nsString& aType, const CSSPoint& aPoint,
+ int32_t aButton, int32_t aClickCount, int32_t aModifiers,
+ unsigned short aInputSourceArg, uint32_t aPointerId);
+
+ /* Fire a single-tap event at the given point. The event is dispatched
+ * via the given widget. */
+ static void FireSingleTapEvent(const LayoutDevicePoint& aPoint,
+ Modifiers aModifiers, int32_t aClickCount,
+ nsIWidget* aWidget);
+
+ /* Perform hit-testing on the touch points of |aEvent| to determine
+ * which scrollable frames they target. If any of these frames don't have
+ * a displayport, set one.
+ *
+ * If any displayports need to be set, this function returns a heap-allocated
+ * object. The caller is responsible for calling Register() on that object.
+ *
+ * The object registers itself as a post-refresh observer on the presShell
+ * and ensures that notifications get sent to APZ correctly after the
+ * refresh.
+ *
+ * Having the caller manage this object is desirable in case they want to
+ * (a) know about the fact that a displayport needs to be set, and
+ * (b) register a post-refresh observer of their own that will run in
+ * a defined ordering relative to the APZ messages.
+ */
+ static already_AddRefed<DisplayportSetListener> SendSetTargetAPZCNotification(
+ nsIWidget* aWidget, mozilla::dom::Document* aDocument,
+ const WidgetGUIEvent& aEvent, const LayersId& aLayersId,
+ uint64_t aInputBlockId);
+
+ /* Notify content of a mouse scroll testing event. */
+ static void NotifyMozMouseScrollEvent(
+ const ScrollableLayerGuid::ViewID& aScrollId, const nsString& aEvent);
+
+ /* Notify content that the repaint flush is complete. */
+ static void NotifyFlushComplete(PresShell* aPresShell);
+
+ static void NotifyAsyncScrollbarDragInitiated(
+ uint64_t aDragBlockId, const ScrollableLayerGuid::ViewID& aScrollId,
+ ScrollDirection aDirection);
+ static void NotifyAsyncScrollbarDragRejected(
+ const ScrollableLayerGuid::ViewID& aScrollId);
+ static void NotifyAsyncAutoscrollRejected(
+ const ScrollableLayerGuid::ViewID& aScrollId);
+
+ static void CancelAutoscroll(const ScrollableLayerGuid::ViewID& aScrollId);
+ static void NotifyScaleGestureComplete(const nsCOMPtr<nsIWidget>& aWidget,
+ float aScale);
+
+ /*
+ * Check if the scrollable frame is currently in the middle of a main thread
+ * async or smooth scroll, or has already requested some other apz scroll that
+ * hasn't been acknowledged by apz.
+ *
+ * We want to discard apz updates to the main-thread scroll offset if this is
+ * true to prevent clobbering higher priority origins.
+ */
+ static bool IsScrollInProgress(nsIScrollableFrame* aFrame);
+
+ /* Notify content of the progress of a pinch gesture that APZ won't do
+ * zooming for (because the apz.allow_zooming pref is false). This function
+ * will dispatch appropriate WidgetSimpleGestureEvent events to gecko.
+ */
+ static void NotifyPinchGesture(PinchGestureInput::PinchGestureType aType,
+ const LayoutDevicePoint& aFocusPoint,
+ LayoutDeviceCoord aSpanChange,
+ Modifiers aModifiers,
+ const nsCOMPtr<nsIWidget>& aWidget);
+
+ private:
+ static uint64_t sLastTargetAPZCNotificationInputBlock;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif /* mozilla_layers_APZCCallbackHelper_h */
diff --git a/gfx/layers/apz/util/APZEventState.cpp b/gfx/layers/apz/util/APZEventState.cpp
new file mode 100644
index 0000000000..c2bf624dda
--- /dev/null
+++ b/gfx/layers/apz/util/APZEventState.cpp
@@ -0,0 +1,603 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "APZEventState.h"
+
+#include <utility>
+
+#include "APZCCallbackHelper.h"
+#include "ActiveElementManager.h"
+#include "TouchManager.h"
+#include "mozilla/BasicEvents.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/IntegerPrintfMacros.h"
+#include "mozilla/PositionedEventTargeting.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/StaticPrefs_dom.h"
+#include "mozilla/StaticPrefs_ui.h"
+#include "mozilla/ToString.h"
+#include "mozilla/TouchEvents.h"
+#include "mozilla/ViewportUtils.h"
+#include "mozilla/dom/BrowserChild.h"
+#include "mozilla/dom/MouseEventBinding.h"
+#include "mozilla/layers/APZCCallbackHelper.h"
+#include "mozilla/layers/IAPZCTreeManager.h"
+#include "mozilla/widget/nsAutoRollup.h"
+#include "nsCOMPtr.h"
+#include "nsDocShell.h"
+#include "nsIDOMWindowUtils.h"
+#include "nsINamed.h"
+#include "nsIScrollableFrame.h"
+#include "nsIScrollbarMediator.h"
+#include "nsIWeakReferenceUtils.h"
+#include "nsIWidget.h"
+#include "nsLayoutUtils.h"
+#include "nsQueryFrame.h"
+
+static mozilla::LazyLogModule sApzEvtLog("apz.eventstate");
+#define APZES_LOG(...) MOZ_LOG(sApzEvtLog, LogLevel::Debug, (__VA_ARGS__))
+
+// Static helper functions
+namespace {
+
+int32_t WidgetModifiersToDOMModifiers(mozilla::Modifiers aModifiers) {
+ int32_t result = 0;
+ if (aModifiers & mozilla::MODIFIER_SHIFT) {
+ result |= nsIDOMWindowUtils::MODIFIER_SHIFT;
+ }
+ if (aModifiers & mozilla::MODIFIER_CONTROL) {
+ result |= nsIDOMWindowUtils::MODIFIER_CONTROL;
+ }
+ if (aModifiers & mozilla::MODIFIER_ALT) {
+ result |= nsIDOMWindowUtils::MODIFIER_ALT;
+ }
+ if (aModifiers & mozilla::MODIFIER_META) {
+ result |= nsIDOMWindowUtils::MODIFIER_META;
+ }
+ if (aModifiers & mozilla::MODIFIER_ALTGRAPH) {
+ result |= nsIDOMWindowUtils::MODIFIER_ALTGRAPH;
+ }
+ if (aModifiers & mozilla::MODIFIER_CAPSLOCK) {
+ result |= nsIDOMWindowUtils::MODIFIER_CAPSLOCK;
+ }
+ if (aModifiers & mozilla::MODIFIER_FN) {
+ result |= nsIDOMWindowUtils::MODIFIER_FN;
+ }
+ if (aModifiers & mozilla::MODIFIER_FNLOCK) {
+ result |= nsIDOMWindowUtils::MODIFIER_FNLOCK;
+ }
+ if (aModifiers & mozilla::MODIFIER_NUMLOCK) {
+ result |= nsIDOMWindowUtils::MODIFIER_NUMLOCK;
+ }
+ if (aModifiers & mozilla::MODIFIER_SCROLLLOCK) {
+ result |= nsIDOMWindowUtils::MODIFIER_SCROLLLOCK;
+ }
+ if (aModifiers & mozilla::MODIFIER_SYMBOL) {
+ result |= nsIDOMWindowUtils::MODIFIER_SYMBOL;
+ }
+ if (aModifiers & mozilla::MODIFIER_SYMBOLLOCK) {
+ result |= nsIDOMWindowUtils::MODIFIER_SYMBOLLOCK;
+ }
+ if (aModifiers & mozilla::MODIFIER_OS) {
+ result |= nsIDOMWindowUtils::MODIFIER_OS;
+ }
+ return result;
+}
+
+} // namespace
+
+namespace mozilla {
+namespace layers {
+
+APZEventState::APZEventState(nsIWidget* aWidget,
+ ContentReceivedInputBlockCallback&& aCallback)
+ : mWidget(nullptr) // initialized in constructor body
+ ,
+ mActiveElementManager(new ActiveElementManager()),
+ mContentReceivedInputBlockCallback(std::move(aCallback)),
+ mPendingTouchPreventedResponse(false),
+ mPendingTouchPreventedBlockId(0),
+ mEndTouchIsClick(false),
+ mFirstTouchCancelled(false),
+ mTouchEndCancelled(false),
+ mSingleTapsPendingTargetInfo(),
+ mLastTouchIdentifier(0) {
+ nsresult rv;
+ mWidget = do_GetWeakReference(aWidget, &rv);
+ MOZ_ASSERT(NS_SUCCEEDED(rv),
+ "APZEventState constructed with a widget that"
+ " does not support weak references. APZ will NOT work!");
+}
+
+APZEventState::~APZEventState() = default;
+
+RefPtr<DelayedFireSingleTapEvent> DelayedFireSingleTapEvent::Create(
+ Maybe<SingleTapTargetInfo>&& aTargetInfo) {
+ nsCOMPtr<nsITimer> timer = NS_NewTimer();
+ RefPtr<DelayedFireSingleTapEvent> event =
+ new DelayedFireSingleTapEvent(std::move(aTargetInfo), timer);
+ nsresult rv = timer->InitWithCallback(
+ event, StaticPrefs::ui_touch_activation_duration_ms(),
+ nsITimer::TYPE_ONE_SHOT);
+ if (NS_FAILED(rv)) {
+ event->ClearTimer();
+ event = nullptr;
+ }
+ return event;
+}
+
+NS_IMETHODIMP DelayedFireSingleTapEvent::Notify(nsITimer*) {
+ APZES_LOG("DelayedFireSingeTapEvent notification ready=%d",
+ mTargetInfo.isSome());
+ // If the required information to fire the synthesized events has not
+ // been populated yet, we have not received the touch-end. In this case
+ // we should not fire the synthesized events here. The synthesized events
+ // will be fired on touch-end in this case.
+ if (mTargetInfo.isSome()) {
+ FireSingleTapEvent();
+ }
+ mTimer = nullptr;
+ return NS_OK;
+}
+
+NS_IMETHODIMP DelayedFireSingleTapEvent::GetName(nsACString& aName) {
+ aName.AssignLiteral("DelayedFireSingleTapEvent");
+ return NS_OK;
+}
+
+void DelayedFireSingleTapEvent::PopulateTargetInfo(
+ SingleTapTargetInfo&& aTargetInfo) {
+ MOZ_ASSERT(!mTargetInfo.isSome());
+ mTargetInfo = Some(std::move(aTargetInfo));
+ // If the timer no longer exists, we have surpassed the minimum elapsed
+ // time to delay the synthesized click. We can immediately fire the
+ // synthesized events in this case.
+ if (!mTimer) {
+ FireSingleTapEvent();
+ }
+}
+
+void DelayedFireSingleTapEvent::FireSingleTapEvent() {
+ MOZ_ASSERT(mTargetInfo.isSome());
+ nsCOMPtr<nsIWidget> widget = do_QueryReferent(mTargetInfo->mWidget);
+ if (widget) {
+ widget::nsAutoRollup rollup(mTargetInfo->mTouchRollup.get());
+ APZCCallbackHelper::FireSingleTapEvent(mTargetInfo->mPoint,
+ mTargetInfo->mModifiers,
+ mTargetInfo->mClickCount, widget);
+ }
+}
+
+NS_IMPL_ISUPPORTS(DelayedFireSingleTapEvent, nsITimerCallback, nsINamed)
+
+void APZEventState::ProcessSingleTap(const CSSPoint& aPoint,
+ const CSSToLayoutDeviceScale& aScale,
+ Modifiers aModifiers, int32_t aClickCount,
+ uint64_t aInputBlockId) {
+ APZES_LOG("Handling single tap at %s with %d\n", ToString(aPoint).c_str(),
+ mTouchEndCancelled);
+
+ RefPtr<nsIContent> touchRollup = GetTouchRollup();
+ mTouchRollup = nullptr;
+
+ nsCOMPtr<nsIWidget> widget = GetWidget();
+ if (!widget) {
+ return;
+ }
+
+ if (mTouchEndCancelled) {
+ return;
+ }
+
+ SingleTapTargetInfo targetInfo(mWidget, aPoint * aScale, aModifiers,
+ aClickCount, touchRollup);
+
+ auto delayedEvent = mSingleTapsPendingTargetInfo.find(aInputBlockId);
+ if (delayedEvent != mSingleTapsPendingTargetInfo.end()) {
+ APZES_LOG("Found tap for block=%" PRIu64, aInputBlockId);
+
+ // With the target info populated, the event will be fired as
+ // soon as the delay timer expires (or now, if it has already expired).
+ delayedEvent->second->PopulateTargetInfo(std::move(targetInfo));
+ mSingleTapsPendingTargetInfo.erase(delayedEvent);
+ } else {
+ APZES_LOG("Scheduling timer for click event\n");
+
+ // We don't need to keep a reference to the event, because the
+ // event and its timer keep each other alive until the timer expires
+ DelayedFireSingleTapEvent::Create(Some(std::move(targetInfo)));
+ }
+}
+
+PreventDefaultResult APZEventState::FireContextmenuEvents(
+ PresShell* aPresShell, const CSSPoint& aPoint,
+ const CSSToLayoutDeviceScale& aScale, Modifiers aModifiers,
+ const nsCOMPtr<nsIWidget>& aWidget) {
+ // Suppress retargeting for mouse events generated by a long-press
+ EventRetargetSuppression suppression;
+
+ // Synthesize mousemove event for allowing users to emulate to move mouse
+ // cursor over the element. As a result, users can open submenu UI which
+ // is opened when mouse cursor is moved over a link (i.e., it's a case that
+ // users cannot stay in the page after tapping it). So, this improves
+ // accessibility in websites which are designed for desktop.
+ // Note that we don't need to check whether mousemove event is consumed or
+ // not because Chrome also ignores the result.
+ APZCCallbackHelper::DispatchSynthesizedMouseEvent(
+ eMouseMove, aPoint * aScale, aModifiers, 0 /* clickCount */, aWidget);
+
+ // Converting the modifiers to DOM format for the DispatchMouseEvent call
+ // is the most useless thing ever because nsDOMWindowUtils::SendMouseEvent
+ // just converts them back to widget format, but that API has many callers,
+ // including in JS code, so it's not trivial to change.
+ CSSPoint point = CSSPoint::FromAppUnits(
+ ViewportUtils::VisualToLayout(CSSPoint::ToAppUnits(aPoint), aPresShell));
+ PreventDefaultResult preventDefaultResult =
+ APZCCallbackHelper::DispatchMouseEvent(
+ aPresShell, u"contextmenu"_ns, point, 2, 1,
+ WidgetModifiersToDOMModifiers(aModifiers),
+ dom::MouseEvent_Binding::MOZ_SOURCE_TOUCH,
+ 0 /* Use the default value here. */);
+
+ APZES_LOG("Contextmenu event %s\n", ToString(preventDefaultResult).c_str());
+ if (preventDefaultResult != PreventDefaultResult::No) {
+ // If the contextmenu event was handled then we're showing a contextmenu,
+ // and so we should remove any activation
+ mActiveElementManager->ClearActivation();
+#ifndef XP_WIN
+ } else {
+ // If the contextmenu wasn't consumed, fire the eMouseLongTap event.
+ nsEventStatus status = APZCCallbackHelper::DispatchSynthesizedMouseEvent(
+ eMouseLongTap, aPoint * aScale, aModifiers,
+ /*clickCount*/ 1, aWidget);
+ if (status == nsEventStatus_eConsumeNoDefault) {
+ // Assuming no JS actor listens eMouseLongTap events.
+ preventDefaultResult = PreventDefaultResult::ByContent;
+ } else {
+ preventDefaultResult = PreventDefaultResult::No;
+ }
+ APZES_LOG("eMouseLongTap event %s\n",
+ ToString(preventDefaultResult).c_str());
+#endif
+ }
+
+ return preventDefaultResult;
+}
+
+void APZEventState::ProcessLongTap(PresShell* aPresShell,
+ const CSSPoint& aPoint,
+ const CSSToLayoutDeviceScale& aScale,
+ Modifiers aModifiers,
+ uint64_t aInputBlockId) {
+ APZES_LOG("Handling long tap at %s\n", ToString(aPoint).c_str());
+
+ nsCOMPtr<nsIWidget> widget = GetWidget();
+ if (!widget) {
+ return;
+ }
+
+ SendPendingTouchPreventedResponse(false);
+
+#ifdef XP_WIN
+ // On Windows, we fire the contextmenu events when the user lifts their
+ // finger, in keeping with the platform convention. This happens in the
+ // ProcessLongTapUp function. However, we still fire the eMouseLongTap event
+ // at this time, because things like text selection or dragging may want
+ // to know about it.
+ nsEventStatus status = APZCCallbackHelper::DispatchSynthesizedMouseEvent(
+ eMouseLongTap, aPoint * aScale, aModifiers, /*clickCount*/ 1, widget);
+
+ PreventDefaultResult preventDefaultResult =
+ (status == nsEventStatus_eConsumeNoDefault)
+ ? PreventDefaultResult::ByContent
+ : PreventDefaultResult::No;
+#else
+ PreventDefaultResult preventDefaultResult =
+ FireContextmenuEvents(aPresShell, aPoint, aScale, aModifiers, widget);
+#endif
+ mContentReceivedInputBlockCallback(
+ aInputBlockId, preventDefaultResult != PreventDefaultResult::No);
+
+ const bool eventHandled =
+#ifdef MOZ_WIDGET_ANDROID
+ // On Android, GeckoView calls preventDefault() in a JSActor
+ // (ContentDelegateChild.jsm) when opening context menu so that we can
+ // tell whether contextmenu opens in response to the contextmenu event by
+ // checking where preventDefault() got called.
+ preventDefaultResult == PreventDefaultResult::ByChrome;
+#else
+ // Unfortunately on desktop platforms other than Windows we can't use
+ // the same approach for Android since we no longer call preventDefault()
+ // since bug 1558506. So for now, we keep the current behavior that is
+ // sending a touchcancel event if the contextmenu event was
+ // preventDefault-ed in an event handler in the content itself.
+ preventDefaultResult == PreventDefaultResult::ByContent;
+#endif
+ if (eventHandled) {
+ // Also send a touchcancel to content
+ // a) on Android if browser's contextmenu is open
+ // b) on Windows if the long tap event was consumed
+ // c) on other platforms if preventDefault() was called for the contextmenu
+ // event
+ // so that listeners that might be waiting for a touchend don't trigger.
+ WidgetTouchEvent cancelTouchEvent(true, eTouchCancel, widget.get());
+ cancelTouchEvent.mModifiers = aModifiers;
+ auto ldPoint = LayoutDeviceIntPoint::Round(aPoint * aScale);
+ cancelTouchEvent.mTouches.AppendElement(new mozilla::dom::Touch(
+ mLastTouchIdentifier, ldPoint, LayoutDeviceIntPoint(), 0, 0));
+ APZCCallbackHelper::DispatchWidgetEvent(cancelTouchEvent);
+ }
+}
+
+void APZEventState::ProcessLongTapUp(PresShell* aPresShell,
+ const CSSPoint& aPoint,
+ const CSSToLayoutDeviceScale& aScale,
+ Modifiers aModifiers) {
+#ifdef XP_WIN
+ nsCOMPtr<nsIWidget> widget = GetWidget();
+ if (widget) {
+ FireContextmenuEvents(aPresShell, aPoint, aScale, aModifiers, widget);
+ }
+#endif
+}
+
+void APZEventState::ProcessTouchEvent(
+ const WidgetTouchEvent& aEvent, const ScrollableLayerGuid& aGuid,
+ uint64_t aInputBlockId, nsEventStatus aApzResponse,
+ nsEventStatus aContentResponse,
+ nsTArray<TouchBehaviorFlags>&& aAllowedTouchBehaviors) {
+ if (aEvent.mMessage == eTouchStart && aEvent.mTouches.Length() > 0) {
+ mActiveElementManager->SetTargetElement(
+ aEvent.mTouches[0]->GetOriginalTarget());
+ mLastTouchIdentifier = aEvent.mTouches[0]->Identifier();
+ }
+ if (aEvent.mMessage == eTouchStart) {
+ // We get the allowed touch behaviors on a touchstart, but may not actually
+ // use them until the first touchmove, so we stash them in a member
+ // variable.
+ mTouchBlockAllowedBehaviors = std::move(aAllowedTouchBehaviors);
+ }
+
+ bool isTouchPrevented = aContentResponse == nsEventStatus_eConsumeNoDefault;
+ bool sentContentResponse = false;
+ APZES_LOG("Handling event type %d isPrevented=%d\n", aEvent.mMessage,
+ isTouchPrevented);
+ switch (aEvent.mMessage) {
+ case eTouchStart: {
+ mTouchEndCancelled = false;
+ mTouchRollup = do_GetWeakReference(widget::nsAutoRollup::GetLastRollup());
+
+ SendPendingTouchPreventedResponse(false);
+ // The above call may have sent a message to APZ if we get two
+ // TOUCH_STARTs in a row and just responded to the first one.
+
+ // We're about to send a response back to APZ, but we should only do it
+ // for events that went through APZ (which should be all of them).
+ MOZ_ASSERT(aEvent.mFlags.mHandledByAPZ);
+
+ // If the first touchstart event was preventDefaulted, ensure that any
+ // subsequent additional touchstart events also get preventDefaulted. This
+ // ensures that e.g. pinch zooming is prevented even if just the first
+ // touchstart was prevented by content.
+ if (mTouchCounter.GetActiveTouchCount() == 0) {
+ mFirstTouchCancelled = isTouchPrevented;
+ } else {
+ if (mFirstTouchCancelled && !isTouchPrevented) {
+ APZES_LOG(
+ "Propagating prevent-default from first-touch for block %" PRIu64
+ "\n",
+ aInputBlockId);
+ }
+ isTouchPrevented |= mFirstTouchCancelled;
+ }
+
+ if (isTouchPrevented) {
+ mContentReceivedInputBlockCallback(aInputBlockId, isTouchPrevented);
+ sentContentResponse = true;
+ } else {
+ APZES_LOG("Event not prevented; pending response for %" PRIu64 " %s\n",
+ aInputBlockId, ToString(aGuid).c_str());
+ mPendingTouchPreventedResponse = true;
+ mPendingTouchPreventedGuid = aGuid;
+ mPendingTouchPreventedBlockId = aInputBlockId;
+ }
+ break;
+ }
+
+ case eTouchEnd:
+ if (isTouchPrevented) {
+ mTouchEndCancelled = true;
+ mEndTouchIsClick = false;
+ }
+ [[fallthrough]];
+ case eTouchCancel:
+ mActiveElementManager->HandleTouchEndEvent(mEndTouchIsClick);
+ [[fallthrough]];
+ case eTouchMove: {
+ if (mPendingTouchPreventedResponse) {
+ MOZ_ASSERT(aGuid == mPendingTouchPreventedGuid);
+ }
+ sentContentResponse = SendPendingTouchPreventedResponse(isTouchPrevented);
+ break;
+ }
+
+ default:
+ MOZ_ASSERT_UNREACHABLE("Unknown touch event type");
+ break;
+ }
+
+ mTouchCounter.Update(aEvent);
+ if (mTouchCounter.GetActiveTouchCount() == 0) {
+ mFirstTouchCancelled = false;
+ }
+
+ APZES_LOG("Pointercancel if %d %d %d %d\n", sentContentResponse,
+ !isTouchPrevented, aApzResponse == nsEventStatus_eConsumeDoDefault,
+ MainThreadAgreesEventsAreConsumableByAPZ());
+ if (sentContentResponse && !isTouchPrevented &&
+ aApzResponse == nsEventStatus_eConsumeDoDefault &&
+ MainThreadAgreesEventsAreConsumableByAPZ()) {
+ WidgetTouchEvent cancelEvent(aEvent);
+ cancelEvent.mMessage = eTouchPointerCancel;
+ cancelEvent.mFlags.mCancelable = false; // mMessage != eTouchCancel;
+ for (uint32_t i = 0; i < cancelEvent.mTouches.Length(); ++i) {
+ if (mozilla::dom::Touch* touch = cancelEvent.mTouches[i]) {
+ touch->convertToPointer = true;
+ }
+ }
+ nsEventStatus status;
+ cancelEvent.mWidget->DispatchEvent(&cancelEvent, status);
+ }
+}
+
+bool APZEventState::MainThreadAgreesEventsAreConsumableByAPZ() const {
+ // APZ errs on the side of saying it can consume touch events to perform
+ // default user-agent behaviours. In particular it may say this if it hasn't
+ // received accurate touch-action information. Here we double-check using
+ // accurate touch-action information. This code is kinda-sorta the main
+ // thread equivalent of AsyncPanZoomController::ArePointerEventsConsumable().
+
+ switch (mTouchBlockAllowedBehaviors.Length()) {
+ case 0:
+ // If we don't have any touch-action (e.g. because it is disabled) then
+ // APZ has no restrictions.
+ return true;
+
+ case 1: {
+ // If there's one touch point in this touch block, then check the pan-x
+ // and pan-y flags. If neither is allowed, then we disagree with APZ and
+ // say that it can't do anything with this touch block. Note that it would
+ // be even better if we could check the allowed scroll directions of the
+ // scrollframe at this point and refine this further.
+ TouchBehaviorFlags flags = mTouchBlockAllowedBehaviors[0];
+ return (flags & AllowedTouchBehavior::HORIZONTAL_PAN) ||
+ (flags & AllowedTouchBehavior::VERTICAL_PAN);
+ }
+
+ case 2: {
+ // If there's two touch points in this touch block, check that they both
+ // allow zooming.
+ for (const auto& allowed : mTouchBlockAllowedBehaviors) {
+ if (!(allowed & AllowedTouchBehavior::PINCH_ZOOM)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ default:
+ // More than two touch points? APZ shouldn't be doing anything with this,
+ // so APZ shouldn't be consuming them.
+ return false;
+ }
+}
+
+void APZEventState::ProcessWheelEvent(const WidgetWheelEvent& aEvent,
+ uint64_t aInputBlockId) {
+ // If this event starts a swipe, indicate that it shouldn't result in a
+ // scroll by setting defaultPrevented to true.
+ bool defaultPrevented = aEvent.DefaultPrevented() || aEvent.TriggersSwipe();
+ mContentReceivedInputBlockCallback(aInputBlockId, defaultPrevented);
+}
+
+void APZEventState::ProcessMouseEvent(const WidgetMouseEvent& aEvent,
+ uint64_t aInputBlockId) {
+ bool defaultPrevented = false;
+ mContentReceivedInputBlockCallback(aInputBlockId, defaultPrevented);
+}
+
+void APZEventState::ProcessAPZStateChange(ViewID aViewId,
+ APZStateChange aChange, int aArg,
+ Maybe<uint64_t> aInputBlockId) {
+ switch (aChange) {
+ case APZStateChange::eTransformBegin: {
+ nsIScrollableFrame* sf = nsLayoutUtils::FindScrollableFrameFor(aViewId);
+ if (sf) {
+ sf->SetTransformingByAPZ(true);
+ sf->ScrollbarActivityStarted();
+ }
+
+ nsIContent* content = nsLayoutUtils::FindContentFor(aViewId);
+ dom::Document* doc = content ? content->GetComposedDoc() : nullptr;
+ nsCOMPtr<nsIDocShell> docshell(doc ? doc->GetDocShell() : nullptr);
+ if (docshell && sf) {
+ nsDocShell* nsdocshell = static_cast<nsDocShell*>(docshell.get());
+ nsdocshell->NotifyAsyncPanZoomStarted();
+ }
+ break;
+ }
+ case APZStateChange::eTransformEnd: {
+ nsIScrollableFrame* sf = nsLayoutUtils::FindScrollableFrameFor(aViewId);
+ if (sf) {
+ sf->SetTransformingByAPZ(false);
+ sf->ScrollbarActivityStopped();
+ }
+
+ nsIContent* content = nsLayoutUtils::FindContentFor(aViewId);
+ dom::Document* doc = content ? content->GetComposedDoc() : nullptr;
+ nsCOMPtr<nsIDocShell> docshell(doc ? doc->GetDocShell() : nullptr);
+ if (docshell && sf) {
+ nsDocShell* nsdocshell = static_cast<nsDocShell*>(docshell.get());
+ nsdocshell->NotifyAsyncPanZoomStopped();
+ }
+ break;
+ }
+ case APZStateChange::eStartTouch: {
+ bool canBePan = aArg;
+ mActiveElementManager->HandleTouchStart(canBePan);
+ // If this is a non-scrollable content, set a timer for the amount of
+ // time specified by ui.touch_activation.duration_ms to fire the
+ // synthesized click and mouse events.
+ APZES_LOG("%s: can-be-pan=%d", __FUNCTION__, aArg);
+ if (!canBePan) {
+ MOZ_ASSERT(aInputBlockId.isSome());
+ RefPtr<DelayedFireSingleTapEvent> delayedEvent =
+ DelayedFireSingleTapEvent::Create(Nothing());
+ DebugOnly<bool> insertResult =
+ mSingleTapsPendingTargetInfo.emplace(*aInputBlockId, delayedEvent)
+ .second;
+ MOZ_ASSERT(insertResult, "Failed to insert delayed tap event.");
+ }
+ break;
+ }
+ case APZStateChange::eStartPanning: {
+ // The user started to pan, so we don't want anything to be :active.
+ mActiveElementManager->ClearActivation();
+ break;
+ }
+ case APZStateChange::eEndTouch: {
+ mEndTouchIsClick = aArg;
+ mActiveElementManager->HandleTouchEnd();
+ break;
+ }
+ }
+}
+
+bool APZEventState::SendPendingTouchPreventedResponse(bool aPreventDefault) {
+ if (mPendingTouchPreventedResponse) {
+ APZES_LOG("Sending response %d for pending guid: %s\n", aPreventDefault,
+ ToString(mPendingTouchPreventedGuid).c_str());
+ mContentReceivedInputBlockCallback(mPendingTouchPreventedBlockId,
+ aPreventDefault);
+ mPendingTouchPreventedResponse = false;
+ return true;
+ }
+ return false;
+}
+
+already_AddRefed<nsIWidget> APZEventState::GetWidget() const {
+ nsCOMPtr<nsIWidget> result = do_QueryReferent(mWidget);
+ return result.forget();
+}
+
+already_AddRefed<nsIContent> APZEventState::GetTouchRollup() const {
+ nsCOMPtr<nsIContent> result = do_QueryReferent(mTouchRollup);
+ return result.forget();
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/util/APZEventState.h b/gfx/layers/apz/util/APZEventState.h
new file mode 100644
index 0000000000..e4f6c98303
--- /dev/null
+++ b/gfx/layers/apz/util/APZEventState.h
@@ -0,0 +1,190 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_APZEventState_h
+#define mozilla_layers_APZEventState_h
+
+#include <stdint.h>
+
+#include "Units.h"
+#include "mozilla/EventForwards.h"
+#include "mozilla/layers/GeckoContentControllerTypes.h" // for APZStateChange
+#include "mozilla/layers/ScrollableLayerGuid.h" // for ScrollableLayerGuid
+#include "mozilla/layers/TouchCounter.h" // for TouchCounter
+#include "mozilla/RefPtr.h"
+#include "mozilla/StaticPrefs_ui.h"
+#include "nsCOMPtr.h"
+#include "nsISupportsImpl.h" // for NS_INLINE_DECL_REFCOUNTING
+#include "nsITimer.h"
+#include "nsIWeakReferenceUtils.h" // for nsWeakPtr
+
+#include <functional>
+#include <unordered_map>
+
+template <class>
+class nsCOMPtr;
+class nsIContent;
+class nsIWidget;
+
+namespace mozilla {
+
+class PresShell;
+enum class PreventDefaultResult : uint8_t;
+
+namespace layers {
+
+class ActiveElementManager;
+
+typedef std::function<void(uint64_t /* input block id */,
+ bool /* prevent default */)>
+ ContentReceivedInputBlockCallback;
+
+struct SingleTapTargetInfo {
+ nsWeakPtr mWidget;
+ LayoutDevicePoint mPoint;
+ Modifiers mModifiers;
+ int32_t mClickCount;
+ RefPtr<nsIContent> mTouchRollup;
+
+ explicit SingleTapTargetInfo(nsWeakPtr aWidget, LayoutDevicePoint aPoint,
+ Modifiers aModifiers, int32_t aClickCount,
+ RefPtr<nsIContent> aTouchRollup)
+ : mWidget(std::move(aWidget)),
+ mPoint(aPoint),
+ mModifiers(aModifiers),
+ mClickCount(aClickCount),
+ mTouchRollup(std::move(aTouchRollup)) {}
+
+ SingleTapTargetInfo(SingleTapTargetInfo&&) = default;
+ SingleTapTargetInfo& operator=(SingleTapTargetInfo&&) = default;
+};
+
+class DelayedFireSingleTapEvent final : public nsITimerCallback,
+ public nsINamed {
+ private:
+ explicit DelayedFireSingleTapEvent(Maybe<SingleTapTargetInfo>&& aTargetInfo,
+ const nsCOMPtr<nsITimer>& aTimer)
+ : mTargetInfo(std::move(aTargetInfo))
+ // Hold the reference count until we are called back.
+ ,
+ mTimer(aTimer) {}
+
+ public:
+ NS_DECL_ISUPPORTS
+
+ static RefPtr<DelayedFireSingleTapEvent> Create(
+ Maybe<SingleTapTargetInfo>&& aTargetInfo);
+
+ NS_IMETHOD Notify(nsITimer*) override;
+
+ NS_IMETHOD GetName(nsACString& aName) override;
+
+ void PopulateTargetInfo(SingleTapTargetInfo&& aTargetInfo);
+
+ void FireSingleTapEvent();
+
+ void ClearTimer() { mTimer = nullptr; }
+
+ private:
+ ~DelayedFireSingleTapEvent() = default;
+
+ Maybe<SingleTapTargetInfo> mTargetInfo;
+ nsCOMPtr<nsITimer> mTimer;
+};
+
+/**
+ * A content-side component that keeps track of state for handling APZ
+ * gestures and sending APZ notifications.
+ */
+class APZEventState final {
+ typedef GeckoContentController_APZStateChange APZStateChange;
+ typedef ScrollableLayerGuid::ViewID ViewID;
+
+ public:
+ APZEventState(nsIWidget* aWidget,
+ ContentReceivedInputBlockCallback&& aCallback);
+
+ NS_INLINE_DECL_REFCOUNTING(APZEventState);
+
+ void ProcessSingleTap(const CSSPoint& aPoint,
+ const CSSToLayoutDeviceScale& aScale,
+ Modifiers aModifiers, int32_t aClickCount,
+ uint64_t aInputBlockId);
+ MOZ_CAN_RUN_SCRIPT
+ void ProcessLongTap(PresShell* aPresShell, const CSSPoint& aPoint,
+ const CSSToLayoutDeviceScale& aScale,
+ Modifiers aModifiers, uint64_t aInputBlockId);
+ MOZ_CAN_RUN_SCRIPT
+ void ProcessLongTapUp(PresShell* aPresShell, const CSSPoint& aPoint,
+ const CSSToLayoutDeviceScale& aScale,
+ Modifiers aModifiers);
+ void ProcessTouchEvent(const WidgetTouchEvent& aEvent,
+ const ScrollableLayerGuid& aGuid,
+ uint64_t aInputBlockId, nsEventStatus aApzResponse,
+ nsEventStatus aContentResponse,
+ nsTArray<TouchBehaviorFlags>&& aAllowedTouchBehaviors);
+ void ProcessWheelEvent(const WidgetWheelEvent& aEvent,
+ uint64_t aInputBlockId);
+ void ProcessMouseEvent(const WidgetMouseEvent& aEvent,
+ uint64_t aInputBlockId);
+ void ProcessAPZStateChange(ViewID aViewId, APZStateChange aChange, int aArg,
+ Maybe<uint64_t> aInputBlockId);
+
+ private:
+ ~APZEventState();
+ bool SendPendingTouchPreventedResponse(bool aPreventDefault);
+ MOZ_CAN_RUN_SCRIPT
+ PreventDefaultResult FireContextmenuEvents(
+ PresShell* aPresShell, const CSSPoint& aPoint,
+ const CSSToLayoutDeviceScale& aScale, Modifiers aModifiers,
+ const nsCOMPtr<nsIWidget>& aWidget);
+ already_AddRefed<nsIWidget> GetWidget() const;
+ already_AddRefed<nsIContent> GetTouchRollup() const;
+ bool MainThreadAgreesEventsAreConsumableByAPZ() const;
+
+ private:
+ nsWeakPtr mWidget;
+ RefPtr<ActiveElementManager> mActiveElementManager;
+ ContentReceivedInputBlockCallback mContentReceivedInputBlockCallback;
+ TouchCounter mTouchCounter;
+ bool mPendingTouchPreventedResponse;
+ ScrollableLayerGuid mPendingTouchPreventedGuid;
+ uint64_t mPendingTouchPreventedBlockId;
+ bool mEndTouchIsClick;
+ bool mFirstTouchCancelled;
+ bool mTouchEndCancelled;
+
+ // Store pending single tap event dispatch tasks keyed on the
+ // tap gesture's input block id. In the case where multiple taps
+ // occur in quick succession, we may receive a later tap while the
+ // dispatch for an earlier tap is still pending.
+ std::unordered_map<uint64_t, RefPtr<DelayedFireSingleTapEvent>>
+ mSingleTapsPendingTargetInfo;
+
+ int32_t mLastTouchIdentifier;
+ nsTArray<TouchBehaviorFlags> mTouchBlockAllowedBehaviors;
+
+ // Because touch-triggered mouse events (e.g. mouse events from a tap
+ // gesture) happen asynchronously from the touch events themselves, we
+ // need to stash and replicate some of the state from the touch events
+ // to the mouse events. One piece of state is the rollup content, which
+ // is the content for which a popup window was recently closed. If we
+ // don't replicate this state properly during the mouse events, the
+ // synthetic click might reopen a popup window that was just closed by
+ // the touch event, which is undesirable. See also documentation in
+ // nsAutoRollup.h
+ // Note that in cases where we get multiple touch blocks interleaved with
+ // their single-tap event notifications, mTouchRollup may hold an incorrect
+ // value. This is kind of an edge case, and falls in the same category of
+ // problems as bug 1227241. I intend that fixing that bug will also take
+ // care of this potential problem.
+ nsWeakPtr mTouchRollup;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif /* mozilla_layers_APZEventState_h */
diff --git a/gfx/layers/apz/util/APZTaskRunnable.cpp b/gfx/layers/apz/util/APZTaskRunnable.cpp
new file mode 100644
index 0000000000..6192f385a7
--- /dev/null
+++ b/gfx/layers/apz/util/APZTaskRunnable.cpp
@@ -0,0 +1,142 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "APZTaskRunnable.h"
+
+#include "mozilla/PresShell.h"
+#include "nsRefreshDriver.h"
+
+namespace mozilla::layers {
+
+NS_IMETHODIMP
+APZTaskRunnable::Run() {
+ if (!mController) {
+ mRegisteredPresShellId = 0;
+ return NS_OK;
+ }
+
+ // Move these variables first since below RequestContentPaint and
+ // NotifyFlushComplete might spin event loop so that any new incoming requests
+ // will be properly queued and run in the next refresh driver's tick.
+ const bool needsFlushCompleteNotification = mNeedsFlushCompleteNotification;
+ auto requests = std::move(mPendingRepaintRequestQueue);
+ mPendingRepaintRequestMap.clear();
+ mNeedsFlushCompleteNotification = false;
+ mRegisteredPresShellId = 0;
+ RefPtr<GeckoContentController> controller = mController;
+
+ // We need to process pending RepaintRequests first.
+ while (!requests.empty()) {
+ controller->RequestContentRepaint(requests.front());
+ requests.pop_front();
+ }
+
+ if (needsFlushCompleteNotification) {
+ // Then notify "apz-repaints-flushed" so that we can ensure that all pending
+ // scroll position updates have finished when the "apz-repaints-flushed"
+ // arrives.
+ controller->NotifyFlushComplete();
+ }
+
+ return NS_OK;
+}
+
+void APZTaskRunnable::QueueRequest(const RepaintRequest& aRequest) {
+ // If we are in test-controlled refreshes mode, process this |aRequest|
+ // synchronously.
+ if (IsTestControllingRefreshesEnabled()) {
+ // Flush all pending requests and notification just in case the refresh
+ // driver mode was changed before flushing them.
+ RefPtr<GeckoContentController> controller = mController;
+ Run();
+ controller->RequestContentRepaint(aRequest);
+ return;
+ }
+ EnsureRegisterAsEarlyRunner();
+
+ RepaintRequestKey key{aRequest.GetScrollId(), aRequest.GetScrollUpdateType()};
+
+ auto lastDiscardableRequest = mPendingRepaintRequestMap.find(key);
+ // If there's an existing request with the same key, we can discard it and we
+ // push the incoming one into the queue's tail so that we can ensure the order
+ // of processing requests.
+ if (lastDiscardableRequest != mPendingRepaintRequestMap.end()) {
+ for (auto it = mPendingRepaintRequestQueue.begin();
+ it != mPendingRepaintRequestQueue.end(); it++) {
+ if (RepaintRequestKey{it->GetScrollId(), it->GetScrollUpdateType()} ==
+ key) {
+ mPendingRepaintRequestQueue.erase(it);
+ break;
+ }
+ }
+ }
+ mPendingRepaintRequestMap.insert(key);
+ mPendingRepaintRequestQueue.push_back(aRequest);
+}
+
+void APZTaskRunnable::QueueFlushCompleteNotification() {
+ // If we are in test-controlled refreshes mode, notify apz-repaints-flushed
+ // synchronously.
+ if (IsTestControllingRefreshesEnabled()) {
+ // Flush all pending requests and notification just in case the refresh
+ // driver mode was changed before flushing them.
+ RefPtr<GeckoContentController> controller = mController;
+ Run();
+ controller->NotifyFlushComplete();
+ return;
+ }
+
+ EnsureRegisterAsEarlyRunner();
+
+ mNeedsFlushCompleteNotification = true;
+}
+
+bool APZTaskRunnable::IsRegisteredWithCurrentPresShell() const {
+ MOZ_ASSERT(mController);
+
+ uint32_t current = 0;
+ if (PresShell* presShell = mController->GetTopLevelPresShell()) {
+ current = presShell->GetPresShellId();
+ }
+ return mRegisteredPresShellId == current;
+}
+
+void APZTaskRunnable::EnsureRegisterAsEarlyRunner() {
+ if (IsRegisteredWithCurrentPresShell()) {
+ return;
+ }
+
+ // If the registered presshell id has been changed, we need to discard pending
+ // requests and notification since all of them are for documents which
+ // have been torn down.
+ if (mRegisteredPresShellId) {
+ mPendingRepaintRequestMap.clear();
+ mPendingRepaintRequestQueue.clear();
+ mNeedsFlushCompleteNotification = false;
+ }
+
+ if (PresShell* presShell = mController->GetTopLevelPresShell()) {
+ if (nsRefreshDriver* driver = presShell->GetRefreshDriver()) {
+ driver->AddEarlyRunner(this);
+ mRegisteredPresShellId = presShell->GetPresShellId();
+ }
+ }
+}
+
+bool APZTaskRunnable::IsTestControllingRefreshesEnabled() const {
+ if (!mController) {
+ return false;
+ }
+
+ if (PresShell* presShell = mController->GetTopLevelPresShell()) {
+ if (nsRefreshDriver* driver = presShell->GetRefreshDriver()) {
+ return driver->IsTestControllingRefreshesEnabled();
+ }
+ }
+ return false;
+}
+
+} // namespace mozilla::layers
diff --git a/gfx/layers/apz/util/APZTaskRunnable.h b/gfx/layers/apz/util/APZTaskRunnable.h
new file mode 100644
index 0000000000..f5de21abd4
--- /dev/null
+++ b/gfx/layers/apz/util/APZTaskRunnable.h
@@ -0,0 +1,89 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_RepaintRequestRunnable_h
+#define mozilla_layers_RepaintRequestRunnable_h
+
+#include <deque>
+#include <unordered_set>
+
+#include "mozilla/layers/GeckoContentController.h"
+#include "mozilla/layers/RepaintRequest.h"
+#include "mozilla/layers/ScrollableLayerGuid.h"
+#include "nsThreadUtils.h"
+
+namespace mozilla {
+namespace layers {
+
+class GeckoContentController;
+
+// A runnable invoked in nsRefreshDriver::Tick as an early runnable.
+class APZTaskRunnable final : public Runnable {
+ public:
+ explicit APZTaskRunnable(GeckoContentController* aController)
+ : Runnable("RepaintRequestRunnable"),
+ mController(aController),
+ mRegisteredPresShellId(0),
+ mNeedsFlushCompleteNotification(false) {}
+
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_DECL_NSIRUNNABLE
+
+ // Queue a RepaintRequest.
+ // If there's already a RepaintRequest having the same scroll id, the old
+ // one will be discarded.
+ void
+ QueueRequest(const RepaintRequest& aRequest);
+ void QueueFlushCompleteNotification();
+ void Revoke() {
+ mController = nullptr;
+ mRegisteredPresShellId = 0;
+ }
+
+ private:
+ void EnsureRegisterAsEarlyRunner();
+ bool IsRegisteredWithCurrentPresShell() const;
+ bool IsTestControllingRefreshesEnabled() const;
+
+ // Use a GeckoContentController raw pointer here since the owner of the
+ // GeckoContentController instance (an APZChild instance) holds a strong
+ // reference of this APZTaskRunnable instance and will call Revoke() before
+ // the GeckoContentController gets destroyed in the dtor of the APZChild
+ // instance.
+ GeckoContentController* mController;
+
+ struct RepaintRequestKey {
+ ScrollableLayerGuid::ViewID mScrollId;
+ RepaintRequest::ScrollOffsetUpdateType mScrollUpdateType;
+ bool operator==(const RepaintRequestKey& aOther) const {
+ return mScrollId == aOther.mScrollId &&
+ mScrollUpdateType == aOther.mScrollUpdateType;
+ }
+ struct HashFn {
+ std::size_t operator()(const RepaintRequestKey& aKey) const {
+ return HashGeneric(aKey.mScrollId, aKey.mScrollUpdateType);
+ }
+ };
+ };
+ using RepaintRequests =
+ std::unordered_set<RepaintRequestKey, RepaintRequestKey::HashFn>;
+ // We have an unordered_map and a deque for pending RepaintRequests. The
+ // unordered_map is for quick lookup and the deque is for processing the
+ // pending RepaintRequests in the order we queued.
+ RepaintRequests mPendingRepaintRequestMap;
+ std::deque<RepaintRequest> mPendingRepaintRequestQueue;
+ // This APZTaskRunnable instance is per APZChild instance, which means its
+ // lifetime is tied to the APZChild instance, thus this APZTaskRunnable
+ // instance will be (re-)used for different pres shells so we'd need to
+ // have to remember the pres shell which is currently tied to the APZChild
+ // to deliver queued requests and notifications to the proper pres shell.
+ uint32_t mRegisteredPresShellId;
+ bool mNeedsFlushCompleteNotification;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_RepaintRequestRunnable_h
diff --git a/gfx/layers/apz/util/APZThreadUtils.cpp b/gfx/layers/apz/util/APZThreadUtils.cpp
new file mode 100644
index 0000000000..d3bf43e61d
--- /dev/null
+++ b/gfx/layers/apz/util/APZThreadUtils.cpp
@@ -0,0 +1,119 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "APZThreadUtils.h"
+
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/ProfilerRunnable.h"
+#include "mozilla/StaticMutex.h"
+
+#include "nsISerialEventTarget.h"
+#include "nsThreadUtils.h"
+#include "nsXULAppAPI.h"
+
+namespace mozilla {
+namespace layers {
+
+static bool sThreadAssertionsEnabled = true;
+static StaticRefPtr<nsISerialEventTarget> sControllerThread;
+static StaticMutex sControllerThreadMutex MOZ_UNANNOTATED;
+
+/*static*/
+void APZThreadUtils::SetThreadAssertionsEnabled(bool aEnabled) {
+ StaticMutexAutoLock lock(sControllerThreadMutex);
+ sThreadAssertionsEnabled = aEnabled;
+}
+
+/*static*/
+bool APZThreadUtils::GetThreadAssertionsEnabled() {
+ StaticMutexAutoLock lock(sControllerThreadMutex);
+ return sThreadAssertionsEnabled;
+}
+
+/*static*/
+void APZThreadUtils::SetControllerThread(nsISerialEventTarget* aThread) {
+ // We must either be setting the initial controller thread, or removing it,
+ // or re-using an existing controller thread.
+ StaticMutexAutoLock lock(sControllerThreadMutex);
+ MOZ_ASSERT(!sControllerThread || !aThread || sControllerThread == aThread);
+ if (aThread != sControllerThread) {
+ // This can only happen once, on startup.
+ sControllerThread = aThread;
+ ClearOnShutdown(&sControllerThread);
+ }
+}
+
+/*static*/
+void APZThreadUtils::AssertOnControllerThread() {
+#if DEBUG
+ if (!GetThreadAssertionsEnabled()) {
+ return;
+ }
+ StaticMutexAutoLock lock(sControllerThreadMutex);
+ MOZ_ASSERT(sControllerThread && sControllerThread->IsOnCurrentThread());
+#endif
+}
+
+/*static*/
+void APZThreadUtils::RunOnControllerThread(RefPtr<Runnable>&& aTask,
+ uint32_t flags) {
+ RefPtr<nsISerialEventTarget> thread;
+ {
+ StaticMutexAutoLock lock(sControllerThreadMutex);
+ thread = sControllerThread;
+ }
+ RefPtr<Runnable> task = std::move(aTask);
+
+ if (!thread) {
+ // Could happen on startup or if Shutdown() got called.
+ NS_WARNING("Dropping task posted to controller thread");
+ return;
+ }
+
+ if (thread->IsOnCurrentThread()) {
+ AUTO_PROFILE_FOLLOWING_RUNNABLE(task);
+ task->Run();
+ } else {
+ thread->Dispatch(task.forget(), flags);
+ }
+}
+
+/*static*/
+bool APZThreadUtils::IsControllerThread() {
+ StaticMutexAutoLock lock(sControllerThreadMutex);
+ return sControllerThread && sControllerThread->IsOnCurrentThread();
+}
+
+/*static*/
+bool APZThreadUtils::IsControllerThreadAlive() {
+ StaticMutexAutoLock lock(sControllerThreadMutex);
+ return !!sControllerThread;
+}
+
+/*static*/
+void APZThreadUtils::DelayedDispatch(already_AddRefed<Runnable> aRunnable,
+ int aDelayMs) {
+ MOZ_ASSERT(!XRE_IsContentProcess(),
+ "ContentProcessController should only be used remotely.");
+ RefPtr<nsISerialEventTarget> thread;
+ {
+ StaticMutexAutoLock lock(sControllerThreadMutex);
+ thread = sControllerThread;
+ }
+ if (!thread) {
+ // Could happen on startup
+ NS_WARNING("Dropping task posted to controller thread");
+ return;
+ }
+ if (aDelayMs) {
+ thread->DelayedDispatch(std::move(aRunnable), aDelayMs);
+ } else {
+ thread->Dispatch(std::move(aRunnable));
+ }
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/util/APZThreadUtils.h b/gfx/layers/apz/util/APZThreadUtils.h
new file mode 100644
index 0000000000..f1560eee8c
--- /dev/null
+++ b/gfx/layers/apz/util/APZThreadUtils.h
@@ -0,0 +1,75 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_APZThreadUtils_h
+#define mozilla_layers_APZThreadUtils_h
+
+#include "nsIEventTarget.h"
+#include "nsINamed.h"
+#include "nsITimer.h"
+#include "nsString.h"
+
+class nsISerialEventTarget;
+
+namespace mozilla {
+
+class Runnable;
+
+namespace layers {
+
+class APZThreadUtils {
+ public:
+ /**
+ * In the gtest environment everything runs on one thread, so we
+ * shouldn't assert that we're on a particular thread. This enables
+ * that behaviour.
+ */
+ static void SetThreadAssertionsEnabled(bool aEnabled);
+ static bool GetThreadAssertionsEnabled();
+
+ /**
+ * Set the controller thread.
+ */
+ static void SetControllerThread(nsISerialEventTarget* aThread);
+
+ /**
+ * This can be used to assert that the current thread is the
+ * controller/UI thread (on which input events are received).
+ * This does nothing if thread assertions are disabled.
+ */
+ static void AssertOnControllerThread();
+
+ /**
+ * Run the given task on the APZ "controller thread" for this platform. If
+ * this function is called from the controller thread itself then the task is
+ * run immediately without getting queued.
+ */
+ static void RunOnControllerThread(
+ RefPtr<Runnable>&& aTask,
+ uint32_t flags = nsIEventTarget::DISPATCH_NORMAL);
+
+ /**
+ * Returns true if currently on APZ "controller thread".
+ */
+ static bool IsControllerThread();
+
+ /**
+ * Returns true if the controller thread is still alive.
+ */
+ static bool IsControllerThreadAlive();
+
+ /**
+ * Schedules a runnable to run on the controller thread at some time
+ * in the future.
+ */
+ static void DelayedDispatch(already_AddRefed<Runnable> aRunnable,
+ int aDelayMs);
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif /* mozilla_layers_APZThreadUtils_h */
diff --git a/gfx/layers/apz/util/ActiveElementManager.cpp b/gfx/layers/apz/util/ActiveElementManager.cpp
new file mode 100644
index 0000000000..8da36bb2c0
--- /dev/null
+++ b/gfx/layers/apz/util/ActiveElementManager.cpp
@@ -0,0 +1,178 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ActiveElementManager.h"
+#include "mozilla/EventStateManager.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/StaticPrefs_ui.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/Document.h"
+
+static mozilla::LazyLogModule sApzAemLog("apz.activeelement");
+#define AEM_LOG(...) MOZ_LOG(sApzAemLog, LogLevel::Debug, (__VA_ARGS__))
+
+namespace mozilla {
+namespace layers {
+
+ActiveElementManager::ActiveElementManager()
+ : mCanBePan(false), mCanBePanSet(false), mSetActiveTask(nullptr) {}
+
+ActiveElementManager::~ActiveElementManager() = default;
+
+void ActiveElementManager::SetTargetElement(dom::EventTarget* aTarget) {
+ if (mTarget) {
+ // Multiple fingers on screen (since HandleTouchEnd clears mTarget).
+ AEM_LOG("Multiple fingers on-screen, clearing target element\n");
+ CancelTask();
+ ResetActive();
+ ResetTouchBlockState();
+ return;
+ }
+
+ mTarget = dom::Element::FromEventTargetOrNull(aTarget);
+ AEM_LOG("Setting target element to %p\n", mTarget.get());
+ TriggerElementActivation();
+}
+
+void ActiveElementManager::HandleTouchStart(bool aCanBePan) {
+ AEM_LOG("Touch start, aCanBePan: %d\n", aCanBePan);
+ if (mCanBePanSet) {
+ // Multiple fingers on screen (since HandleTouchEnd clears mCanBePanSet).
+ AEM_LOG("Multiple fingers on-screen, clearing touch block state\n");
+ CancelTask();
+ ResetActive();
+ ResetTouchBlockState();
+ return;
+ }
+
+ mCanBePan = aCanBePan;
+ mCanBePanSet = true;
+ TriggerElementActivation();
+}
+
+void ActiveElementManager::TriggerElementActivation() {
+ // Both HandleTouchStart() and SetTargetElement() call this. They can be
+ // called in either order. One will set mCanBePanSet, and the other, mTarget.
+ // We want to actually trigger the activation once both are set.
+ if (!(mTarget && mCanBePanSet)) {
+ return;
+ }
+
+ // If the touch cannot be a pan, make mTarget :active right away.
+ // Otherwise, wait a bit to see if the user will pan or not.
+ if (!mCanBePan) {
+ SetActive(mTarget);
+ } else {
+ CancelTask(); // this is only needed because of bug 1169802. Fixing that
+ // bug properly should make this unnecessary.
+ MOZ_ASSERT(mSetActiveTask == nullptr);
+
+ RefPtr<CancelableRunnable> task =
+ NewCancelableRunnableMethod<nsCOMPtr<dom::Element>>(
+ "layers::ActiveElementManager::SetActiveTask", this,
+ &ActiveElementManager::SetActiveTask, mTarget);
+ mSetActiveTask = task;
+ NS_GetCurrentThread()->DelayedDispatch(
+ task.forget(), StaticPrefs::ui_touch_activation_delay_ms());
+ AEM_LOG("Scheduling mSetActiveTask %p\n", mSetActiveTask.get());
+ }
+}
+
+void ActiveElementManager::ClearActivation() {
+ AEM_LOG("Clearing element activation\n");
+ CancelTask();
+ ResetActive();
+}
+
+void ActiveElementManager::HandleTouchEndEvent(bool aWasClick) {
+ AEM_LOG("Touch end event, aWasClick: %d\n", aWasClick);
+
+ // If the touch was a click, make mTarget :active right away.
+ // nsEventStateManager will reset the active element when processing
+ // the mouse-down event generated by the click.
+ CancelTask();
+ if (aWasClick) {
+ // Scrollbar thumbs use a different mechanism for their active
+ // highlight (the "active" attribute), so don't set the active state
+ // on them because nothing will clear it.
+ if (!(mTarget && mTarget->IsXULElement(nsGkAtoms::thumb))) {
+ SetActive(mTarget);
+ }
+ } else {
+ // We might reach here if mCanBePan was false on touch-start and
+ // so we set the element active right away. Now it turns out the
+ // action was not a click so we need to reset the active element.
+ ResetActive();
+ }
+
+ ResetTouchBlockState();
+}
+
+void ActiveElementManager::HandleTouchEnd() {
+ AEM_LOG("Touch end, clearing pan state\n");
+ mCanBePanSet = false;
+}
+
+static nsPresContext* GetPresContextFor(nsIContent* aContent) {
+ if (!aContent) {
+ return nullptr;
+ }
+ PresShell* presShell = aContent->OwnerDoc()->GetPresShell();
+ if (!presShell) {
+ return nullptr;
+ }
+ return presShell->GetPresContext();
+}
+
+void ActiveElementManager::SetActive(dom::Element* aTarget) {
+ AEM_LOG("Setting active %p\n", aTarget);
+
+ if (nsPresContext* pc = GetPresContextFor(aTarget)) {
+ pc->EventStateManager()->SetContentState(aTarget,
+ dom::ElementState::ACTIVE);
+ }
+}
+
+void ActiveElementManager::ResetActive() {
+ AEM_LOG("Resetting active from %p\n", mTarget.get());
+
+ // Clear the :active flag from mTarget by setting it on the document root.
+ if (mTarget) {
+ dom::Element* root = mTarget->OwnerDoc()->GetDocumentElement();
+ if (root) {
+ AEM_LOG("Found root %p, making active\n", root);
+ SetActive(root);
+ }
+ }
+}
+
+void ActiveElementManager::ResetTouchBlockState() {
+ mTarget = nullptr;
+ mCanBePanSet = false;
+}
+
+void ActiveElementManager::SetActiveTask(
+ const nsCOMPtr<dom::Element>& aTarget) {
+ AEM_LOG("mSetActiveTask %p running\n", mSetActiveTask.get());
+
+ // This gets called from mSetActiveTask's Run() method. The message loop
+ // deletes the task right after running it, so we need to null out
+ // mSetActiveTask to make sure we're not left with a dangling pointer.
+ mSetActiveTask = nullptr;
+ SetActive(aTarget);
+}
+
+void ActiveElementManager::CancelTask() {
+ AEM_LOG("Cancelling task %p\n", mSetActiveTask.get());
+
+ if (mSetActiveTask) {
+ mSetActiveTask->Cancel();
+ mSetActiveTask = nullptr;
+ }
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/util/ActiveElementManager.h b/gfx/layers/apz/util/ActiveElementManager.h
new file mode 100644
index 0000000000..b783659962
--- /dev/null
+++ b/gfx/layers/apz/util/ActiveElementManager.h
@@ -0,0 +1,97 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_ActiveElementManager_h
+#define mozilla_layers_ActiveElementManager_h
+
+#include "nsCOMPtr.h"
+#include "nsISupportsImpl.h"
+
+namespace mozilla {
+
+class CancelableRunnable;
+
+namespace dom {
+class Element;
+class EventTarget;
+} // namespace dom
+
+namespace layers {
+
+/**
+ * Manages setting and clearing the ':active' CSS pseudostate in the presence
+ * of touch input.
+ */
+class ActiveElementManager final {
+ ~ActiveElementManager();
+
+ public:
+ NS_INLINE_DECL_REFCOUNTING(ActiveElementManager)
+
+ ActiveElementManager();
+
+ /**
+ * Specify the target of a touch. Typically this should be called right
+ * after HandleTouchStart(), but in cases where the APZ needs to wait for
+ * a content response the HandleTouchStart() may be delayed, in which case
+ * this function can be called first.
+ * |aTarget| may be nullptr.
+ */
+ void SetTargetElement(dom::EventTarget* aTarget);
+ /**
+ * Handle a touch-start state notification from APZ. This notification
+ * may be delayed until after touch listeners have responded to the APZ.
+ * @param aCanBePan whether the touch can be a pan
+ */
+ void HandleTouchStart(bool aCanBePan);
+ /**
+ * Clear the active element.
+ */
+ void ClearActivation();
+ /**
+ * Handle a touch-end or touch-cancel event.
+ * @param aWasClick whether the touch was a click
+ */
+ void HandleTouchEndEvent(bool aWasClick);
+ /**
+ * Handle a touch-end state notification from APZ. This notification may be
+ * delayed until after touch listeners have responded to the APZ.
+ */
+ void HandleTouchEnd();
+
+ private:
+ /**
+ * The target of the first touch point in the current touch block.
+ */
+ nsCOMPtr<dom::Element> mTarget;
+ /**
+ * Whether the current touch block can be a pan. Set in HandleTouchStart().
+ */
+ bool mCanBePan;
+ /**
+ * Whether mCanBePan has been set for the current touch block.
+ * We need to keep track of this to allow HandleTouchStart() and
+ * SetTargetElement() to be called in either order.
+ */
+ bool mCanBePanSet;
+ /**
+ * A task for calling SetActive() after a timeout.
+ */
+ RefPtr<CancelableRunnable> mSetActiveTask;
+
+ // Helpers
+ void TriggerElementActivation();
+ void SetActive(dom::Element* aTarget);
+ void ResetActive();
+ void ResetTouchBlockState();
+ void SetActiveTask(const nsCOMPtr<dom::Element>& aTarget);
+ void CancelTask();
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif /* mozilla_layers_ActiveElementManager_h */
diff --git a/gfx/layers/apz/util/CheckerboardReportService.cpp b/gfx/layers/apz/util/CheckerboardReportService.cpp
new file mode 100644
index 0000000000..b6ae712b3f
--- /dev/null
+++ b/gfx/layers/apz/util/CheckerboardReportService.cpp
@@ -0,0 +1,217 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "CheckerboardReportService.h"
+
+#include "jsapi.h" // for JS_Now
+#include "MainThreadUtils.h" // for NS_IsMainThread
+#include "mozilla/Assertions.h" // for MOZ_ASSERT
+#include "mozilla/ClearOnShutdown.h" // for ClearOnShutdown
+#include "mozilla/Preferences.h"
+#include "mozilla/Services.h"
+#include "mozilla/StaticPrefs_apz.h"
+#include "mozilla/Unused.h"
+#include "mozilla/dom/CheckerboardReportServiceBinding.h" // for dom::CheckerboardReports
+#include "mozilla/gfx/GPUParent.h"
+#include "mozilla/gfx/GPUProcessManager.h"
+#include "nsContentUtils.h" // for nsContentUtils
+#include "nsIObserverService.h"
+#include "nsXULAppAPI.h"
+
+namespace mozilla {
+namespace layers {
+
+/*static*/
+StaticRefPtr<CheckerboardEventStorage> CheckerboardEventStorage::sInstance;
+
+/*static*/
+already_AddRefed<CheckerboardEventStorage>
+CheckerboardEventStorage::GetInstance() {
+ // The instance in the parent process does all the work, so if this is getting
+ // called in the child process something is likely wrong.
+ MOZ_ASSERT(XRE_IsParentProcess());
+
+ MOZ_ASSERT(NS_IsMainThread());
+ if (!sInstance) {
+ sInstance = new CheckerboardEventStorage();
+ ClearOnShutdown(&sInstance);
+ }
+ RefPtr<CheckerboardEventStorage> instance = sInstance.get();
+ return instance.forget();
+}
+
+void CheckerboardEventStorage::Report(uint32_t aSeverity,
+ const std::string& aLog) {
+ if (!NS_IsMainThread()) {
+ RefPtr<Runnable> task = NS_NewRunnableFunction(
+ "layers::CheckerboardEventStorage::Report",
+ [aSeverity, aLog]() -> void {
+ CheckerboardEventStorage::Report(aSeverity, aLog);
+ });
+ NS_DispatchToMainThread(task.forget());
+ return;
+ }
+
+ if (XRE_IsGPUProcess()) {
+ if (gfx::GPUParent* gpu = gfx::GPUParent::GetSingleton()) {
+ nsCString log(aLog.c_str());
+ Unused << gpu->SendReportCheckerboard(aSeverity, log);
+ }
+ return;
+ }
+
+ RefPtr<CheckerboardEventStorage> storage = GetInstance();
+ storage->ReportCheckerboard(aSeverity, aLog);
+}
+
+void CheckerboardEventStorage::ReportCheckerboard(uint32_t aSeverity,
+ const std::string& aLog) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (aSeverity == 0) {
+ // This code assumes all checkerboard reports have a nonzero severity.
+ return;
+ }
+
+ CheckerboardReport severe(aSeverity, JS_Now(), aLog);
+ CheckerboardReport recent;
+
+ // First look in the "severe" reports to see if the new one belongs in that
+ // list.
+ for (int i = 0; i < SEVERITY_MAX_INDEX; i++) {
+ if (mCheckerboardReports[i].mSeverity >= severe.mSeverity) {
+ continue;
+ }
+ // The new one deserves to be in the "severe" list. Take the one getting
+ // bumped off the list, and put it in |recent| for possible insertion into
+ // the recents list.
+ recent = mCheckerboardReports[SEVERITY_MAX_INDEX - 1];
+
+ // Shuffle the severe list down, insert the new one.
+ for (int j = SEVERITY_MAX_INDEX - 1; j > i; j--) {
+ mCheckerboardReports[j] = mCheckerboardReports[j - 1];
+ }
+ mCheckerboardReports[i] = severe;
+ severe.mSeverity = 0; // mark |severe| as inserted
+ break;
+ }
+
+ // If |severe.mSeverity| is nonzero, the incoming report didn't get inserted
+ // into the severe list; put it into |recent| for insertion into the recent
+ // list.
+ if (severe.mSeverity) {
+ MOZ_ASSERT(recent.mSeverity == 0, "recent should be empty here");
+ recent = severe;
+ } // else |recent| may hold a report that got knocked out of the severe list.
+
+ if (recent.mSeverity == 0) {
+ // Nothing to be inserted into the recent list.
+ return;
+ }
+
+ // If it wasn't in the "severe" list, add it to the "recent" list.
+ for (int i = SEVERITY_MAX_INDEX; i < RECENT_MAX_INDEX; i++) {
+ if (mCheckerboardReports[i].mTimestamp >= recent.mTimestamp) {
+ continue;
+ }
+ // |recent| needs to be inserted at |i|. Shuffle the remaining ones down
+ // and insert it.
+ for (int j = RECENT_MAX_INDEX - 1; j > i; j--) {
+ mCheckerboardReports[j] = mCheckerboardReports[j - 1];
+ }
+ mCheckerboardReports[i] = recent;
+ break;
+ }
+}
+
+void CheckerboardEventStorage::GetReports(
+ nsTArray<dom::CheckerboardReport>& aOutReports) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ for (int i = 0; i < RECENT_MAX_INDEX; i++) {
+ CheckerboardReport& r = mCheckerboardReports[i];
+ if (r.mSeverity == 0) {
+ continue;
+ }
+ dom::CheckerboardReport report;
+ report.mSeverity.Construct() = r.mSeverity;
+ report.mTimestamp.Construct() = r.mTimestamp / 1000; // micros to millis
+ report.mLog.Construct() =
+ NS_ConvertUTF8toUTF16(r.mLog.c_str(), r.mLog.size());
+ report.mReason.Construct() = (i < SEVERITY_MAX_INDEX)
+ ? dom::CheckerboardReason::Severe
+ : dom::CheckerboardReason::Recent;
+ aOutReports.AppendElement(report);
+ }
+}
+
+} // namespace layers
+
+namespace dom {
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(CheckerboardReportService, mParent)
+
+/*static*/
+bool CheckerboardReportService::IsEnabled(JSContext* aCtx, JSObject* aGlobal) {
+ // Only allow this in the parent process
+ if (!XRE_IsParentProcess()) {
+ return false;
+ }
+ // Allow privileged code or about:checkerboard (unprivileged) to access this.
+ return nsContentUtils::IsSystemCaller(aCtx) ||
+ nsContentUtils::IsSpecificAboutPage(aGlobal, "about:checkerboard");
+}
+
+/*static*/
+already_AddRefed<CheckerboardReportService>
+CheckerboardReportService::Constructor(const dom::GlobalObject& aGlobal) {
+ RefPtr<CheckerboardReportService> ces =
+ new CheckerboardReportService(aGlobal.GetAsSupports());
+ return ces.forget();
+}
+
+CheckerboardReportService::CheckerboardReportService(nsISupports* aParent)
+ : mParent(aParent) {}
+
+JSObject* CheckerboardReportService::WrapObject(
+ JSContext* aCtx, JS::Handle<JSObject*> aGivenProto) {
+ return CheckerboardReportService_Binding::Wrap(aCtx, this, aGivenProto);
+}
+
+nsISupports* CheckerboardReportService::GetParentObject() { return mParent; }
+
+void CheckerboardReportService::GetReports(
+ nsTArray<dom::CheckerboardReport>& aOutReports) {
+ RefPtr<mozilla::layers::CheckerboardEventStorage> instance =
+ mozilla::layers::CheckerboardEventStorage::GetInstance();
+ MOZ_ASSERT(instance);
+ instance->GetReports(aOutReports);
+}
+
+bool CheckerboardReportService::IsRecordingEnabled() const {
+ return StaticPrefs::apz_record_checkerboarding();
+}
+
+void CheckerboardReportService::SetRecordingEnabled(bool aEnabled) {
+ Preferences::SetBool("apz.record_checkerboarding", aEnabled);
+}
+
+void CheckerboardReportService::FlushActiveReports() {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ gfx::GPUProcessManager* gpu = gfx::GPUProcessManager::Get();
+ if (gpu && gpu->NotifyGpuObservers("APZ:FlushActiveCheckerboard")) {
+ return;
+ }
+
+ nsCOMPtr<nsIObserverService> obsSvc = mozilla::services::GetObserverService();
+ MOZ_ASSERT(obsSvc);
+ if (obsSvc) {
+ obsSvc->NotifyObservers(nullptr, "APZ:FlushActiveCheckerboard", nullptr);
+ }
+}
+
+} // namespace dom
+} // namespace mozilla
diff --git a/gfx/layers/apz/util/CheckerboardReportService.h b/gfx/layers/apz/util/CheckerboardReportService.h
new file mode 100644
index 0000000000..d9b37509c5
--- /dev/null
+++ b/gfx/layers/apz/util/CheckerboardReportService.h
@@ -0,0 +1,138 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_CheckerboardReportService_h
+#define mozilla_dom_CheckerboardReportService_h
+
+#include <string>
+
+#include "js/TypeDecls.h" // for JSContext, JSObject
+#include "mozilla/StaticPtr.h" // for StaticRefPtr
+#include "nsCOMPtr.h" // for nsCOMPtr
+#include "nsISupports.h" // for NS_INLINE_DECL_REFCOUNTING
+#include "nsTArrayForwardDeclare.h" // for nsTArray
+#include "nsWrapperCache.h" // for nsWrapperCache
+
+namespace mozilla {
+
+namespace dom {
+struct CheckerboardReport;
+}
+
+namespace layers {
+
+// CheckerboardEventStorage is a singleton that stores info on checkerboard
+// events, so that they can be accessed from about:checkerboard and visualized.
+// Note that this class is NOT threadsafe, and all methods must be called on
+// the main thread.
+class CheckerboardEventStorage {
+ NS_INLINE_DECL_REFCOUNTING(CheckerboardEventStorage)
+
+ public:
+ /**
+ * Get the singleton instance.
+ */
+ static already_AddRefed<CheckerboardEventStorage> GetInstance();
+
+ /**
+ * Get the stored checkerboard reports.
+ */
+ void GetReports(nsTArray<dom::CheckerboardReport>& aOutReports);
+
+ /**
+ * Save a checkerboard event log, optionally dropping older ones that were
+ * less severe or less recent. Zero-severity reports may be ignored entirely.
+ */
+ static void Report(uint32_t aSeverity, const std::string& aLog);
+
+ private:
+ /* Stuff for refcounted singleton */
+ CheckerboardEventStorage() = default;
+ virtual ~CheckerboardEventStorage() = default;
+
+ static StaticRefPtr<CheckerboardEventStorage> sInstance;
+
+ void ReportCheckerboard(uint32_t aSeverity, const std::string& aLog);
+
+ private:
+ /**
+ * Struct that this class uses internally to store a checkerboard report.
+ */
+ struct CheckerboardReport {
+ uint32_t mSeverity; // if 0, this report is empty
+ int64_t mTimestamp; // microseconds since epoch, as from JS_Now()
+ std::string mLog;
+
+ CheckerboardReport() : mSeverity(0), mTimestamp(0) {}
+
+ CheckerboardReport(uint32_t aSeverity, int64_t aTimestamp,
+ const std::string& aLog)
+ : mSeverity(aSeverity), mTimestamp(aTimestamp), mLog(aLog) {}
+ };
+
+ // The first 5 (indices 0-4) are the most severe ones in decreasing order
+ // of severity; the next 5 (indices 5-9) are the most recent ones that are
+ // not already in the "severe" list.
+ static const int SEVERITY_MAX_INDEX = 5;
+ static const int RECENT_MAX_INDEX = 10;
+ CheckerboardReport mCheckerboardReports[RECENT_MAX_INDEX];
+};
+
+} // namespace layers
+
+namespace dom {
+
+class GlobalObject;
+
+/**
+ * CheckerboardReportService is a wrapper object that allows access to the
+ * stuff in CheckerboardEventStorage (above). We need this wrapper for proper
+ * garbage/cycle collection, since this can be accessed from JS.
+ */
+class CheckerboardReportService : public nsWrapperCache {
+ public:
+ /**
+ * Check if the given page is allowed to access this object via the WebIDL
+ * bindings. It only returns true if the page is about:checkerboard.
+ */
+ static bool IsEnabled(JSContext* aCtx, JSObject* aGlobal);
+
+ /*
+ * Other standard WebIDL binding glue.
+ */
+
+ static already_AddRefed<CheckerboardReportService> Constructor(
+ const dom::GlobalObject& aGlobal);
+
+ explicit CheckerboardReportService(nsISupports* aSupports);
+
+ JSObject* WrapObject(JSContext* aCtx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ nsISupports* GetParentObject();
+
+ NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(CheckerboardReportService)
+ NS_DECL_CYCLE_COLLECTION_NATIVE_WRAPPERCACHE_CLASS(CheckerboardReportService)
+
+ public:
+ /*
+ * The methods exposed via the webidl.
+ */
+ void GetReports(nsTArray<dom::CheckerboardReport>& aOutReports);
+ bool IsRecordingEnabled() const;
+ void SetRecordingEnabled(bool aEnabled);
+ void FlushActiveReports();
+
+ private:
+ virtual ~CheckerboardReportService() = default;
+
+ nsCOMPtr<nsISupports> mParent;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif /* mozilla_layers_CheckerboardReportService_h */
diff --git a/gfx/layers/apz/util/ChromeProcessController.cpp b/gfx/layers/apz/util/ChromeProcessController.cpp
new file mode 100644
index 0000000000..d16466ad7a
--- /dev/null
+++ b/gfx/layers/apz/util/ChromeProcessController.cpp
@@ -0,0 +1,356 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ChromeProcessController.h"
+
+#include "MainThreadUtils.h" // for NS_IsMainThread()
+#include "base/task.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/layers/CompositorBridgeParent.h"
+#include "mozilla/layers/APZCCallbackHelper.h"
+#include "mozilla/layers/APZEventState.h"
+#include "mozilla/layers/APZThreadUtils.h"
+#include "mozilla/layers/IAPZCTreeManager.h"
+#include "mozilla/layers/InputAPZContext.h"
+#include "mozilla/layers/DoubleTapToZoom.h"
+#include "mozilla/layers/RepaintRequest.h"
+#include "mozilla/dom/Document.h"
+#include "nsIInterfaceRequestorUtils.h"
+#include "nsLayoutUtils.h"
+#include "nsView.h"
+
+static mozilla::LazyLogModule sApzChromeLog("apz.cc.chrome");
+
+using namespace mozilla;
+using namespace mozilla::layers;
+using namespace mozilla::widget;
+
+ChromeProcessController::ChromeProcessController(
+ nsIWidget* aWidget, APZEventState* aAPZEventState,
+ IAPZCTreeManager* aAPZCTreeManager)
+ : mWidget(aWidget),
+ mAPZEventState(aAPZEventState),
+ mAPZCTreeManager(aAPZCTreeManager),
+ mUIThread(NS_GetCurrentThread()) {
+ // Otherwise we're initializing mUIThread incorrectly.
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aAPZEventState);
+ MOZ_ASSERT(aAPZCTreeManager);
+
+ mUIThread->Dispatch(
+ NewRunnableMethod("layers::ChromeProcessController::InitializeRoot", this,
+ &ChromeProcessController::InitializeRoot));
+}
+
+ChromeProcessController::~ChromeProcessController() = default;
+
+void ChromeProcessController::InitializeRoot() {
+ APZCCallbackHelper::InitializeRootDisplayport(GetPresShell());
+}
+
+void ChromeProcessController::NotifyLayerTransforms(
+ nsTArray<MatrixMessage>&& aTransforms) {
+ if (!mUIThread->IsOnCurrentThread()) {
+ mUIThread->Dispatch(
+ NewRunnableMethod<StoreCopyPassByRRef<nsTArray<MatrixMessage>>>(
+ "layers::ChromeProcessController::NotifyLayerTransforms", this,
+ &ChromeProcessController::NotifyLayerTransforms,
+ std::move(aTransforms)));
+ return;
+ }
+
+ APZCCallbackHelper::NotifyLayerTransforms(aTransforms);
+}
+
+void ChromeProcessController::RequestContentRepaint(
+ const RepaintRequest& aRequest) {
+ MOZ_ASSERT(IsRepaintThread());
+
+ if (aRequest.IsRootContent()) {
+ APZCCallbackHelper::UpdateRootFrame(aRequest);
+ } else {
+ APZCCallbackHelper::UpdateSubFrame(aRequest);
+ }
+}
+
+bool ChromeProcessController::IsRepaintThread() { return NS_IsMainThread(); }
+
+void ChromeProcessController::DispatchToRepaintThread(
+ already_AddRefed<Runnable> aTask) {
+ NS_DispatchToMainThread(std::move(aTask));
+}
+
+void ChromeProcessController::Destroy() {
+ if (!mUIThread->IsOnCurrentThread()) {
+ mUIThread->Dispatch(
+ NewRunnableMethod("layers::ChromeProcessController::Destroy", this,
+ &ChromeProcessController::Destroy));
+ return;
+ }
+
+ MOZ_ASSERT(mUIThread->IsOnCurrentThread());
+ mWidget = nullptr;
+ mAPZEventState = nullptr;
+}
+
+PresShell* ChromeProcessController::GetPresShell() const {
+ if (!mWidget) {
+ return nullptr;
+ }
+ if (nsView* view = nsView::GetViewFor(mWidget)) {
+ return view->GetPresShell();
+ }
+ return nullptr;
+}
+
+dom::Document* ChromeProcessController::GetRootDocument() const {
+ if (PresShell* presShell = GetPresShell()) {
+ return presShell->GetDocument();
+ }
+ return nullptr;
+}
+
+dom::Document* ChromeProcessController::GetRootContentDocument(
+ const ScrollableLayerGuid::ViewID& aScrollId) const {
+ nsIContent* content = nsLayoutUtils::FindContentFor(aScrollId);
+ if (!content) {
+ return nullptr;
+ }
+ if (PresShell* presShell =
+ APZCCallbackHelper::GetRootContentDocumentPresShellForContent(
+ content)) {
+ return presShell->GetDocument();
+ }
+ return nullptr;
+}
+
+void ChromeProcessController::HandleDoubleTap(
+ const mozilla::CSSPoint& aPoint, Modifiers aModifiers,
+ const ScrollableLayerGuid& aGuid) {
+ MOZ_ASSERT(mUIThread->IsOnCurrentThread());
+
+ RefPtr<dom::Document> document = GetRootContentDocument(aGuid.mScrollId);
+ if (!document.get()) {
+ return;
+ }
+
+ ZoomTarget zoomTarget = CalculateRectToZoomTo(document, aPoint);
+
+ uint32_t presShellId;
+ ScrollableLayerGuid::ViewID viewId;
+ if (APZCCallbackHelper::GetOrCreateScrollIdentifiers(
+ document->GetDocumentElement(), &presShellId, &viewId)) {
+ mAPZCTreeManager->ZoomToRect(
+ ScrollableLayerGuid(aGuid.mLayersId, presShellId, viewId), zoomTarget,
+ ZoomToRectBehavior::DEFAULT_BEHAVIOR);
+ }
+}
+
+void ChromeProcessController::HandleTap(
+ TapType aType, const mozilla::LayoutDevicePoint& aPoint,
+ Modifiers aModifiers, const ScrollableLayerGuid& aGuid,
+ uint64_t aInputBlockId) {
+ MOZ_LOG(sApzChromeLog, LogLevel::Debug,
+ ("HandleTap called with %d\n", (int)aType));
+ if (!mUIThread->IsOnCurrentThread()) {
+ MOZ_LOG(sApzChromeLog, LogLevel::Debug, ("HandleTap redispatching\n"));
+ mUIThread->Dispatch(
+ NewRunnableMethod<TapType, mozilla::LayoutDevicePoint, Modifiers,
+ ScrollableLayerGuid, uint64_t>(
+ "layers::ChromeProcessController::HandleTap", this,
+ &ChromeProcessController::HandleTap, aType, aPoint, aModifiers,
+ aGuid, aInputBlockId));
+ return;
+ }
+
+ if (!mAPZEventState) {
+ return;
+ }
+
+ RefPtr<PresShell> presShell = GetPresShell();
+ if (!presShell) {
+ return;
+ }
+ if (!presShell->GetPresContext()) {
+ return;
+ }
+ CSSToLayoutDeviceScale scale(
+ presShell->GetPresContext()->CSSToDevPixelScale());
+
+ CSSPoint point = aPoint / scale;
+
+ // Stash the guid in InputAPZContext so that when the visual-to-layout
+ // transform is applied to the event's coordinates, we use the right transform
+ // based on the scroll frame being targeted.
+ // The other values don't really matter.
+ InputAPZContext context(aGuid, aInputBlockId, nsEventStatus_eSentinel);
+
+ switch (aType) {
+ case TapType::eSingleTap:
+ mAPZEventState->ProcessSingleTap(point, scale, aModifiers, 1,
+ aInputBlockId);
+ break;
+ case TapType::eDoubleTap:
+ HandleDoubleTap(point, aModifiers, aGuid);
+ break;
+ case TapType::eSecondTap:
+ mAPZEventState->ProcessSingleTap(point, scale, aModifiers, 2,
+ aInputBlockId);
+ break;
+ case TapType::eLongTap: {
+ RefPtr<APZEventState> eventState(mAPZEventState);
+ eventState->ProcessLongTap(presShell, point, scale, aModifiers,
+ aInputBlockId);
+ break;
+ }
+ case TapType::eLongTapUp: {
+ RefPtr<APZEventState> eventState(mAPZEventState);
+ eventState->ProcessLongTapUp(presShell, point, scale, aModifiers);
+ break;
+ }
+ }
+}
+
+void ChromeProcessController::NotifyPinchGesture(
+ PinchGestureInput::PinchGestureType aType, const ScrollableLayerGuid& aGuid,
+ const LayoutDevicePoint& aFocusPoint, LayoutDeviceCoord aSpanChange,
+ Modifiers aModifiers) {
+ if (!mUIThread->IsOnCurrentThread()) {
+ mUIThread->Dispatch(
+ NewRunnableMethod<PinchGestureInput::PinchGestureType,
+ ScrollableLayerGuid, LayoutDevicePoint,
+ LayoutDeviceCoord, Modifiers>(
+ "layers::ChromeProcessController::NotifyPinchGesture", this,
+ &ChromeProcessController::NotifyPinchGesture, aType, aGuid,
+ aFocusPoint, aSpanChange, aModifiers));
+ return;
+ }
+
+ if (mWidget) {
+ // Dispatch the call to APZCCallbackHelper::NotifyPinchGesture to the main
+ // thread so that it runs asynchronously from the current call. This is
+ // because the call can run arbitrary JS code, which can also spin the event
+ // loop and cause undesirable re-entrancy in APZ.
+ mUIThread->Dispatch(NewRunnableFunction(
+ "layers::ChromeProcessController::NotifyPinchGestureAsync",
+ &APZCCallbackHelper::NotifyPinchGesture, aType, aFocusPoint,
+ aSpanChange, aModifiers, mWidget));
+ }
+}
+
+void ChromeProcessController::NotifyAPZStateChange(
+ const ScrollableLayerGuid& aGuid, APZStateChange aChange, int aArg,
+ Maybe<uint64_t> aInputBlockId) {
+ if (!mUIThread->IsOnCurrentThread()) {
+ mUIThread->Dispatch(NewRunnableMethod<ScrollableLayerGuid, APZStateChange,
+ int, Maybe<uint64_t>>(
+ "layers::ChromeProcessController::NotifyAPZStateChange", this,
+ &ChromeProcessController::NotifyAPZStateChange, aGuid, aChange, aArg,
+ aInputBlockId));
+ return;
+ }
+
+ if (!mAPZEventState) {
+ return;
+ }
+
+ mAPZEventState->ProcessAPZStateChange(aGuid.mScrollId, aChange, aArg,
+ aInputBlockId);
+}
+
+void ChromeProcessController::NotifyMozMouseScrollEvent(
+ const ScrollableLayerGuid::ViewID& aScrollId, const nsString& aEvent) {
+ if (!mUIThread->IsOnCurrentThread()) {
+ mUIThread->Dispatch(
+ NewRunnableMethod<ScrollableLayerGuid::ViewID, nsString>(
+ "layers::ChromeProcessController::NotifyMozMouseScrollEvent", this,
+ &ChromeProcessController::NotifyMozMouseScrollEvent, aScrollId,
+ aEvent));
+ return;
+ }
+
+ APZCCallbackHelper::NotifyMozMouseScrollEvent(aScrollId, aEvent);
+}
+
+void ChromeProcessController::NotifyFlushComplete() {
+ MOZ_ASSERT(IsRepaintThread());
+
+ APZCCallbackHelper::NotifyFlushComplete(GetPresShell());
+}
+
+void ChromeProcessController::NotifyAsyncScrollbarDragInitiated(
+ uint64_t aDragBlockId, const ScrollableLayerGuid::ViewID& aScrollId,
+ ScrollDirection aDirection) {
+ if (!mUIThread->IsOnCurrentThread()) {
+ mUIThread->Dispatch(NewRunnableMethod<uint64_t, ScrollableLayerGuid::ViewID,
+ ScrollDirection>(
+ "layers::ChromeProcessController::NotifyAsyncScrollbarDragInitiated",
+ this, &ChromeProcessController::NotifyAsyncScrollbarDragInitiated,
+ aDragBlockId, aScrollId, aDirection));
+ return;
+ }
+
+ APZCCallbackHelper::NotifyAsyncScrollbarDragInitiated(aDragBlockId, aScrollId,
+ aDirection);
+}
+
+void ChromeProcessController::NotifyAsyncScrollbarDragRejected(
+ const ScrollableLayerGuid::ViewID& aScrollId) {
+ if (!mUIThread->IsOnCurrentThread()) {
+ mUIThread->Dispatch(NewRunnableMethod<ScrollableLayerGuid::ViewID>(
+ "layers::ChromeProcessController::NotifyAsyncScrollbarDragRejected",
+ this, &ChromeProcessController::NotifyAsyncScrollbarDragRejected,
+ aScrollId));
+ return;
+ }
+
+ APZCCallbackHelper::NotifyAsyncScrollbarDragRejected(aScrollId);
+}
+
+void ChromeProcessController::NotifyAsyncAutoscrollRejected(
+ const ScrollableLayerGuid::ViewID& aScrollId) {
+ if (!mUIThread->IsOnCurrentThread()) {
+ mUIThread->Dispatch(NewRunnableMethod<ScrollableLayerGuid::ViewID>(
+ "layers::ChromeProcessController::NotifyAsyncAutoscrollRejected", this,
+ &ChromeProcessController::NotifyAsyncAutoscrollRejected, aScrollId));
+ return;
+ }
+
+ APZCCallbackHelper::NotifyAsyncAutoscrollRejected(aScrollId);
+}
+
+void ChromeProcessController::CancelAutoscroll(
+ const ScrollableLayerGuid& aGuid) {
+ if (!mUIThread->IsOnCurrentThread()) {
+ mUIThread->Dispatch(NewRunnableMethod<ScrollableLayerGuid>(
+ "layers::ChromeProcessController::CancelAutoscroll", this,
+ &ChromeProcessController::CancelAutoscroll, aGuid));
+ return;
+ }
+
+ APZCCallbackHelper::CancelAutoscroll(aGuid.mScrollId);
+}
+
+void ChromeProcessController::NotifyScaleGestureComplete(
+ const ScrollableLayerGuid& aGuid, float aScale) {
+ if (!mUIThread->IsOnCurrentThread()) {
+ mUIThread->Dispatch(NewRunnableMethod<ScrollableLayerGuid, float>(
+ "layers::ChromeProcessController::NotifyScaleGestureComplete", this,
+ &ChromeProcessController::NotifyScaleGestureComplete, aGuid, aScale));
+ return;
+ }
+
+ if (mWidget) {
+ // Dispatch the call to APZCCallbackHelper::NotifyScaleGestureComplete
+ // to the main thread so that it runs asynchronously from the current call.
+ // This is because the call can run arbitrary JS code, which can also spin
+ // the event loop and cause undesirable re-entrancy in APZ.
+ mUIThread->Dispatch(NewRunnableFunction(
+ "layers::ChromeProcessController::NotifyScaleGestureComplete",
+ &APZCCallbackHelper::NotifyScaleGestureComplete, mWidget, aScale));
+ }
+}
diff --git a/gfx/layers/apz/util/ChromeProcessController.h b/gfx/layers/apz/util/ChromeProcessController.h
new file mode 100644
index 0000000000..7328d62adb
--- /dev/null
+++ b/gfx/layers/apz/util/ChromeProcessController.h
@@ -0,0 +1,102 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_ChromeProcessController_h
+#define mozilla_layers_ChromeProcessController_h
+
+#include "mozilla/layers/GeckoContentController.h"
+#include "nsCOMPtr.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/layers/MatrixMessage.h"
+
+class nsIDOMWindowUtils;
+class nsISerialEventTarget;
+class nsIWidget;
+
+namespace mozilla {
+class PresShell;
+namespace dom {
+class Document;
+}
+
+namespace layers {
+
+class IAPZCTreeManager;
+class APZEventState;
+
+/**
+ * ChromeProcessController is a GeckoContentController attached to the root of
+ * a compositor's layer tree. It's used directly by APZ by default, and remoted
+ * using PAPZ if there is a gpu process.
+ *
+ * If ChromeProcessController needs to implement a new method on
+ * GeckoContentController PAPZ, APZChild, and RemoteContentController must be
+ * updated to handle it.
+ */
+class ChromeProcessController : public mozilla::layers::GeckoContentController {
+ protected:
+ typedef mozilla::layers::FrameMetrics FrameMetrics;
+ typedef mozilla::layers::ScrollableLayerGuid ScrollableLayerGuid;
+
+ public:
+ explicit ChromeProcessController(nsIWidget* aWidget,
+ APZEventState* aAPZEventState,
+ IAPZCTreeManager* aAPZCTreeManager);
+ virtual ~ChromeProcessController();
+ void Destroy() override;
+
+ // GeckoContentController interface
+ void NotifyLayerTransforms(nsTArray<MatrixMessage>&& aTransforms) override;
+ void RequestContentRepaint(const RepaintRequest& aRequest) override;
+ bool IsRepaintThread() override;
+ void DispatchToRepaintThread(already_AddRefed<Runnable> aTask) override;
+ MOZ_CAN_RUN_SCRIPT
+ void HandleTap(TapType aType, const mozilla::LayoutDevicePoint& aPoint,
+ Modifiers aModifiers, const ScrollableLayerGuid& aGuid,
+ uint64_t aInputBlockId) override;
+ void NotifyPinchGesture(PinchGestureInput::PinchGestureType aType,
+ const ScrollableLayerGuid& aGuid,
+ const LayoutDevicePoint& aFocusPoint,
+ LayoutDeviceCoord aSpanChange,
+ Modifiers aModifiers) override;
+ void NotifyAPZStateChange(const ScrollableLayerGuid& aGuid,
+ APZStateChange aChange, int aArg,
+ Maybe<uint64_t> aInputBlockId) override;
+ void NotifyMozMouseScrollEvent(const ScrollableLayerGuid::ViewID& aScrollId,
+ const nsString& aEvent) override;
+ void NotifyFlushComplete() override;
+ void NotifyAsyncScrollbarDragInitiated(
+ uint64_t aDragBlockId, const ScrollableLayerGuid::ViewID& aScrollId,
+ ScrollDirection aDirection) override;
+ void NotifyAsyncScrollbarDragRejected(
+ const ScrollableLayerGuid::ViewID& aScrollId) override;
+ void NotifyAsyncAutoscrollRejected(
+ const ScrollableLayerGuid::ViewID& aScrollId) override;
+ void CancelAutoscroll(const ScrollableLayerGuid& aGuid) override;
+ void NotifyScaleGestureComplete(const ScrollableLayerGuid& aGuid,
+ float aScale) override;
+
+ PresShell* GetTopLevelPresShell() const override { return GetPresShell(); }
+
+ private:
+ nsCOMPtr<nsIWidget> mWidget;
+ RefPtr<APZEventState> mAPZEventState;
+ RefPtr<IAPZCTreeManager> mAPZCTreeManager;
+ nsCOMPtr<nsISerialEventTarget> mUIThread;
+
+ void InitializeRoot();
+ PresShell* GetPresShell() const;
+ dom::Document* GetRootDocument() const;
+ dom::Document* GetRootContentDocument(
+ const ScrollableLayerGuid::ViewID& aScrollId) const;
+ void HandleDoubleTap(const mozilla::CSSPoint& aPoint, Modifiers aModifiers,
+ const ScrollableLayerGuid& aGuid);
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif /* mozilla_layers_ChromeProcessController_h */
diff --git a/gfx/layers/apz/util/ContentProcessController.cpp b/gfx/layers/apz/util/ContentProcessController.cpp
new file mode 100644
index 0000000000..332d0523fe
--- /dev/null
+++ b/gfx/layers/apz/util/ContentProcessController.cpp
@@ -0,0 +1,123 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ContentProcessController.h"
+
+#include "mozilla/PresShell.h"
+#include "mozilla/dom/BrowserChild.h"
+#include "mozilla/layers/APZCCallbackHelper.h"
+#include "mozilla/layers/APZChild.h"
+#include "nsIContentInlines.h"
+
+#include "InputData.h" // for InputData
+
+namespace mozilla {
+namespace layers {
+
+ContentProcessController::ContentProcessController(
+ const RefPtr<dom::BrowserChild>& aBrowser)
+ : mBrowser(aBrowser) {
+ MOZ_ASSERT(mBrowser);
+}
+
+void ContentProcessController::NotifyLayerTransforms(
+ nsTArray<MatrixMessage>&& aTransforms) {
+ // This should never get called
+ MOZ_ASSERT(false);
+}
+
+void ContentProcessController::RequestContentRepaint(
+ const RepaintRequest& aRequest) {
+ if (mBrowser) {
+ mBrowser->UpdateFrame(aRequest);
+ }
+}
+
+void ContentProcessController::HandleTap(TapType aType,
+ const LayoutDevicePoint& aPoint,
+ Modifiers aModifiers,
+ const ScrollableLayerGuid& aGuid,
+ uint64_t aInputBlockId) {
+ // This should never get called
+ MOZ_ASSERT(false);
+}
+
+void ContentProcessController::NotifyPinchGesture(
+ PinchGestureInput::PinchGestureType aType, const ScrollableLayerGuid& aGuid,
+ const LayoutDevicePoint& aFocusPoint, LayoutDeviceCoord aSpanChange,
+ Modifiers aModifiers) {
+ // This should never get called
+ MOZ_ASSERT_UNREACHABLE("Unexpected message to content process");
+}
+
+void ContentProcessController::NotifyAPZStateChange(
+ const ScrollableLayerGuid& aGuid, APZStateChange aChange, int aArg,
+ Maybe<uint64_t> aInputBlockId) {
+ if (mBrowser) {
+ mBrowser->NotifyAPZStateChange(aGuid.mScrollId, aChange, aArg,
+ aInputBlockId);
+ }
+}
+
+void ContentProcessController::NotifyMozMouseScrollEvent(
+ const ScrollableLayerGuid::ViewID& aScrollId, const nsString& aEvent) {
+ if (mBrowser) {
+ APZCCallbackHelper::NotifyMozMouseScrollEvent(aScrollId, aEvent);
+ }
+}
+
+void ContentProcessController::NotifyFlushComplete() {
+ if (mBrowser) {
+ RefPtr<PresShell> presShell = mBrowser->GetTopLevelPresShell();
+ APZCCallbackHelper::NotifyFlushComplete(presShell);
+ }
+}
+
+void ContentProcessController::NotifyAsyncScrollbarDragInitiated(
+ uint64_t aDragBlockId, const ScrollableLayerGuid::ViewID& aScrollId,
+ ScrollDirection aDirection) {
+ APZCCallbackHelper::NotifyAsyncScrollbarDragInitiated(aDragBlockId, aScrollId,
+ aDirection);
+}
+
+void ContentProcessController::NotifyAsyncScrollbarDragRejected(
+ const ScrollableLayerGuid::ViewID& aScrollId) {
+ APZCCallbackHelper::NotifyAsyncScrollbarDragRejected(aScrollId);
+}
+
+void ContentProcessController::NotifyAsyncAutoscrollRejected(
+ const ScrollableLayerGuid::ViewID& aScrollId) {
+ APZCCallbackHelper::NotifyAsyncAutoscrollRejected(aScrollId);
+}
+
+void ContentProcessController::CancelAutoscroll(
+ const ScrollableLayerGuid& aGuid) {
+ // This should never get called
+ MOZ_ASSERT_UNREACHABLE("Unexpected message to content process");
+}
+
+void ContentProcessController::NotifyScaleGestureComplete(
+ const ScrollableLayerGuid& aGuid, float aScale) {
+ // This should never get called
+ MOZ_ASSERT_UNREACHABLE("Unexpected message to content process");
+}
+
+bool ContentProcessController::IsRepaintThread() { return NS_IsMainThread(); }
+
+void ContentProcessController::DispatchToRepaintThread(
+ already_AddRefed<Runnable> aTask) {
+ NS_DispatchToMainThread(std::move(aTask));
+}
+
+PresShell* ContentProcessController::GetTopLevelPresShell() const {
+ if (!mBrowser) {
+ return nullptr;
+ }
+ return mBrowser->GetTopLevelPresShell();
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/util/ContentProcessController.h b/gfx/layers/apz/util/ContentProcessController.h
new file mode 100644
index 0000000000..02d32df72e
--- /dev/null
+++ b/gfx/layers/apz/util/ContentProcessController.h
@@ -0,0 +1,93 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_ContentProcessController_h
+#define mozilla_layers_ContentProcessController_h
+
+#include "mozilla/layers/GeckoContentController.h"
+
+class nsIObserver;
+
+namespace mozilla {
+
+namespace dom {
+class BrowserChild;
+} // namespace dom
+
+namespace layers {
+
+class APZChild;
+
+/**
+ * ContentProcessController is a GeckoContentController for a BrowserChild, and
+ * is always remoted using PAPZ/APZChild.
+ *
+ * ContentProcessController is created in ContentChild when a layer tree id has
+ * been allocated for a PBrowser that lives in that content process, and is
+ * destroyed when the Destroy message is received, or when the tab dies.
+ *
+ * If ContentProcessController needs to implement a new method on
+ * GeckoContentController PAPZ, APZChild, and RemoteContentController must be
+ * updated to handle it.
+ */
+class ContentProcessController final : public GeckoContentController {
+ public:
+ explicit ContentProcessController(const RefPtr<dom::BrowserChild>& aBrowser);
+
+ // GeckoContentController
+
+ void NotifyLayerTransforms(nsTArray<MatrixMessage>&& aTransforms) override;
+
+ void RequestContentRepaint(const RepaintRequest& aRequest) override;
+
+ void HandleTap(TapType aType, const LayoutDevicePoint& aPoint,
+ Modifiers aModifiers, const ScrollableLayerGuid& aGuid,
+ uint64_t aInputBlockId) override;
+
+ void NotifyPinchGesture(PinchGestureInput::PinchGestureType aType,
+ const ScrollableLayerGuid& aGuid,
+ const LayoutDevicePoint& aFocusPoint,
+ LayoutDeviceCoord aSpanChange,
+ Modifiers aModifiers) override;
+
+ void NotifyAPZStateChange(const ScrollableLayerGuid& aGuid,
+ APZStateChange aChange, int aArg,
+ Maybe<uint64_t> aInputBlockId) override;
+
+ void NotifyMozMouseScrollEvent(const ScrollableLayerGuid::ViewID& aScrollId,
+ const nsString& aEvent) override;
+
+ void NotifyFlushComplete() override;
+
+ void NotifyAsyncScrollbarDragInitiated(
+ uint64_t aDragBlockId, const ScrollableLayerGuid::ViewID& aScrollId,
+ ScrollDirection aDirection) override;
+ void NotifyAsyncScrollbarDragRejected(
+ const ScrollableLayerGuid::ViewID& aScrollId) override;
+
+ void NotifyAsyncAutoscrollRejected(
+ const ScrollableLayerGuid::ViewID& aScrollId) override;
+
+ void CancelAutoscroll(const ScrollableLayerGuid& aGuid) override;
+
+ void NotifyScaleGestureComplete(const ScrollableLayerGuid& aGuid,
+ float aScale) override;
+
+ bool IsRepaintThread() override;
+
+ void DispatchToRepaintThread(already_AddRefed<Runnable> aTask) override;
+
+ PresShell* GetTopLevelPresShell() const override;
+
+ private:
+ RefPtr<dom::BrowserChild> mBrowser;
+};
+
+} // namespace layers
+
+} // namespace mozilla
+
+#endif // mozilla_layers_ContentProcessController_h
diff --git a/gfx/layers/apz/util/DoubleTapToZoom.cpp b/gfx/layers/apz/util/DoubleTapToZoom.cpp
new file mode 100644
index 0000000000..c40d79ced3
--- /dev/null
+++ b/gfx/layers/apz/util/DoubleTapToZoom.cpp
@@ -0,0 +1,376 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "DoubleTapToZoom.h"
+
+#include <algorithm> // for std::min, std::max
+
+#include "mozilla/PresShell.h"
+#include "mozilla/AlreadyAddRefed.h"
+#include "mozilla/dom/Element.h"
+#include "nsCOMPtr.h"
+#include "nsIContent.h"
+#include "mozilla/dom/Document.h"
+#include "nsIFrame.h"
+#include "nsIFrameInlines.h"
+#include "nsIScrollableFrame.h"
+#include "nsTableCellFrame.h"
+#include "nsLayoutUtils.h"
+#include "nsStyleConsts.h"
+#include "mozilla/ViewportUtils.h"
+#include "mozilla/EventListenerManager.h"
+
+namespace mozilla {
+namespace layers {
+
+namespace {
+
+using FrameForPointOption = nsLayoutUtils::FrameForPointOption;
+
+static bool IsGeneratedContent(nsIContent* aContent) {
+ // We exclude marks because making them double tap targets does not seem
+ // desirable.
+ return aContent->IsGeneratedContentContainerForBefore() ||
+ aContent->IsGeneratedContentContainerForAfter();
+}
+
+// Returns the DOM element found at |aPoint|, interpreted as being relative to
+// the root frame of |aPresShell| in visual coordinates. If the point is inside
+// a subdocument, returns an element inside the subdocument, rather than the
+// subdocument element (and does so recursively). The implementation was adapted
+// from DocumentOrShadowRoot::ElementFromPoint(), with the notable exception
+// that we don't pass nsLayoutUtils::IGNORE_CROSS_DOC to GetFrameForPoint(), so
+// as to get the behaviour described above in the presence of subdocuments.
+static already_AddRefed<dom::Element> ElementFromPoint(
+ const RefPtr<PresShell>& aPresShell, const CSSPoint& aPoint) {
+ nsIFrame* rootFrame = aPresShell->GetRootFrame();
+ if (!rootFrame) {
+ return nullptr;
+ }
+ nsIFrame* frame = nsLayoutUtils::GetFrameForPoint(
+ RelativeTo{rootFrame, ViewportType::Visual}, CSSPoint::ToAppUnits(aPoint),
+ {{FrameForPointOption::IgnorePaintSuppression}});
+ while (frame && (!frame->GetContent() ||
+ (frame->GetContent()->IsInNativeAnonymousSubtree() &&
+ !IsGeneratedContent(frame->GetContent())))) {
+ frame = nsLayoutUtils::GetParentOrPlaceholderFor(frame);
+ }
+ if (!frame) {
+ return nullptr;
+ }
+ // FIXME(emilio): This should probably use the flattened tree, GetParent() is
+ // not guaranteed to be an element in presence of shadow DOM.
+ nsIContent* content = frame->GetContent();
+ if (!content) {
+ return nullptr;
+ }
+ if (dom::Element* element = content->GetAsElementOrParentElement()) {
+ return do_AddRef(element);
+ }
+ return nullptr;
+}
+
+// Get table cell from element, parent or grand parent.
+static dom::Element* GetNearbyTableCell(
+ const nsCOMPtr<dom::Element>& aElement) {
+ nsTableCellFrame* tableCell = do_QueryFrame(aElement->GetPrimaryFrame());
+ if (tableCell) {
+ return aElement.get();
+ }
+ if (dom::Element* parent = aElement->GetFlattenedTreeParentElement()) {
+ nsTableCellFrame* tableCell = do_QueryFrame(parent->GetPrimaryFrame());
+ if (tableCell) {
+ return parent;
+ }
+ if (dom::Element* grandParent = parent->GetFlattenedTreeParentElement()) {
+ tableCell = do_QueryFrame(grandParent->GetPrimaryFrame());
+ if (tableCell) {
+ return grandParent;
+ }
+ }
+ }
+ return nullptr;
+}
+
+static bool ShouldZoomToElement(
+ const nsCOMPtr<dom::Element>& aElement,
+ const RefPtr<dom::Document>& aRootContentDocument,
+ nsIScrollableFrame* aRootScrollFrame, const FrameMetrics& aMetrics) {
+ if (nsIFrame* frame = aElement->GetPrimaryFrame()) {
+ if (frame->StyleDisplay()->IsInlineFlow() &&
+ // Replaced elements are suitable zoom targets because they act like
+ // inline-blocks instead of inline. (textarea's are the specific reason
+ // we do this)
+ !frame->IsFrameOfType(nsIFrame::eReplaced)) {
+ return false;
+ }
+ }
+ // Trying to zoom to the html element will just end up scrolling to the start
+ // of the document, return false and we'll run out of elements and just
+ // zoomout (without scrolling to the start).
+ if (aElement->OwnerDoc() == aRootContentDocument &&
+ aElement->IsHTMLElement(nsGkAtoms::html)) {
+ return false;
+ }
+ if (aElement->IsAnyOfHTMLElements(nsGkAtoms::li, nsGkAtoms::q)) {
+ return false;
+ }
+
+ // Ignore elements who are table cells or their parents are table cells, and
+ // they take up less than 30% of page rect width because they are likely cells
+ // in data tables (as opposed to tables used for layout purposes), and we
+ // don't want to zoom to them. This heuristic is quite naive and leaves a lot
+ // to be desired.
+ if (dom::Element* tableCell = GetNearbyTableCell(aElement)) {
+ CSSRect rect =
+ nsLayoutUtils::GetBoundingContentRect(tableCell, aRootScrollFrame);
+ if (rect.width < 0.3 * aMetrics.GetScrollableRect().width) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+// Calculates if zooming to aRect would have almost the same zoom level as
+// aCompositedArea currently has. If so we would want to zoom out instead.
+static bool RectHasAlmostSameZoomLevel(const CSSRect& aRect,
+ const CSSRect& aCompositedArea) {
+ // This functions checks to see if the area of the rect visible in the
+ // composition bounds (i.e. the overlapArea variable below) is approximately
+ // the max area of the rect we can show.
+
+ // AsyncPanZoomController::ZoomToRect will adjust the zoom and scroll offset
+ // so that the zoom to rect fills the composited area. If after adjusting the
+ // scroll offset _only_ the rect would fill the composited area we want to
+ // zoom out (we don't want to _just_ scroll, we want to do some amount of
+ // zooming, either in or out it doesn't matter which). So translate both rects
+ // to the same origin and then compute their overlap, which is what the
+ // following calculation does.
+
+ float overlapArea = std::min(aRect.width, aCompositedArea.width) *
+ std::min(aRect.height, aCompositedArea.height);
+ float availHeight = std::min(
+ aRect.Width() * aCompositedArea.Height() / aCompositedArea.Width(),
+ aRect.Height());
+ float showing = overlapArea / (aRect.Width() * availHeight);
+ float ratioW = aRect.Width() / aCompositedArea.Width();
+ float ratioH = aRect.Height() / aCompositedArea.Height();
+
+ return showing > 0.9 && (ratioW > 0.9 || ratioH > 0.9);
+}
+
+} // namespace
+
+static CSSRect AddHMargin(const CSSRect& aRect, const CSSCoord& aMargin,
+ const FrameMetrics& aMetrics) {
+ CSSRect rect =
+ CSSRect(std::max(aMetrics.GetScrollableRect().X(), aRect.X() - aMargin),
+ aRect.Y(), aRect.Width() + 2 * aMargin, aRect.Height());
+ // Constrict the rect to the screen's right edge
+ rect.SetWidth(
+ std::min(rect.Width(), aMetrics.GetScrollableRect().XMost() - rect.X()));
+ return rect;
+}
+
+static CSSRect AddVMargin(const CSSRect& aRect, const CSSCoord& aMargin,
+ const FrameMetrics& aMetrics) {
+ CSSRect rect =
+ CSSRect(aRect.X(),
+ std::max(aMetrics.GetScrollableRect().Y(), aRect.Y() - aMargin),
+ aRect.Width(), aRect.Height() + 2 * aMargin);
+ // Constrict the rect to the screen's bottom edge
+ rect.SetHeight(
+ std::min(rect.Height(), aMetrics.GetScrollableRect().YMost() - rect.Y()));
+ return rect;
+}
+
+static bool IsReplacedElement(const nsCOMPtr<dom::Element>& aElement) {
+ if (nsIFrame* frame = aElement->GetPrimaryFrame()) {
+ if (frame->IsFrameOfType(nsIFrame::eReplaced)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+static bool HasNonPassiveWheelListenerOnAncestor(nsIContent* aContent) {
+ for (nsIContent* content = aContent; content;
+ content = content->GetFlattenedTreeParent()) {
+ EventListenerManager* elm = content->GetExistingListenerManager();
+ if (elm && elm->HasNonPassiveWheelListener()) {
+ return true;
+ }
+ }
+ return false;
+}
+
+ZoomTarget CalculateRectToZoomTo(
+ const RefPtr<dom::Document>& aRootContentDocument, const CSSPoint& aPoint) {
+ // Ensure the layout information we get is up-to-date.
+ aRootContentDocument->FlushPendingNotifications(FlushType::Layout);
+
+ // An empty rect as return value is interpreted as "zoom out".
+ const CSSRect zoomOut;
+
+ RefPtr<PresShell> presShell = aRootContentDocument->GetPresShell();
+ if (!presShell) {
+ return ZoomTarget{zoomOut, CantZoomOutBehavior::ZoomIn};
+ }
+
+ nsIScrollableFrame* rootScrollFrame =
+ presShell->GetRootScrollFrameAsScrollable();
+ if (!rootScrollFrame) {
+ return ZoomTarget{zoomOut, CantZoomOutBehavior::ZoomIn};
+ }
+
+ CSSPoint documentRelativePoint =
+ CSSPoint::FromAppUnits(ViewportUtils::VisualToLayout(
+ CSSPoint::ToAppUnits(aPoint), presShell)) +
+ CSSPoint::FromAppUnits(rootScrollFrame->GetScrollPosition());
+
+ nsCOMPtr<dom::Element> element = ElementFromPoint(presShell, aPoint);
+ if (!element) {
+ return ZoomTarget{zoomOut, CantZoomOutBehavior::ZoomIn, Nothing(),
+ Some(documentRelativePoint)};
+ }
+
+ CantZoomOutBehavior cantZoomOutBehavior =
+ HasNonPassiveWheelListenerOnAncestor(element)
+ ? CantZoomOutBehavior::Nothing
+ : CantZoomOutBehavior::ZoomIn;
+
+ FrameMetrics metrics =
+ nsLayoutUtils::CalculateBasicFrameMetrics(rootScrollFrame);
+
+ while (element && !ShouldZoomToElement(element, aRootContentDocument,
+ rootScrollFrame, metrics)) {
+ element = element->GetFlattenedTreeParentElement();
+ }
+
+ if (!element) {
+ return ZoomTarget{zoomOut, cantZoomOutBehavior, Nothing(),
+ Some(documentRelativePoint)};
+ }
+
+ CSSPoint visualScrollOffset = metrics.GetVisualScrollOffset();
+ CSSRect compositedArea(visualScrollOffset,
+ metrics.CalculateCompositedSizeInCssPixels());
+ Maybe<CSSRect> nearestScrollClip;
+ CSSRect rect = nsLayoutUtils::GetBoundingContentRect(element, rootScrollFrame,
+ &nearestScrollClip);
+
+ // In some cases, like overflow: visible and overflowing content, the bounding
+ // client rect of the targeted element won't contain the point the user double
+ // tapped on. In that case we use the scrollable overflow rect if it contains
+ // the user point.
+ if (!rect.Contains(documentRelativePoint)) {
+ if (nsIFrame* scrolledFrame = rootScrollFrame->GetScrolledFrame()) {
+ if (nsIFrame* f = element->GetPrimaryFrame()) {
+ nsRect overflowRect = f->ScrollableOverflowRect();
+ nsLayoutUtils::TransformResult res =
+ nsLayoutUtils::TransformRect(f, scrolledFrame, overflowRect);
+ MOZ_ASSERT(res == nsLayoutUtils::TRANSFORM_SUCCEEDED ||
+ res == nsLayoutUtils::NONINVERTIBLE_TRANSFORM);
+ if (res == nsLayoutUtils::TRANSFORM_SUCCEEDED) {
+ CSSRect overflowRectCSS = CSSRect::FromAppUnits(overflowRect);
+ if (nearestScrollClip.isSome()) {
+ overflowRectCSS = nearestScrollClip->Intersect(overflowRectCSS);
+ }
+ if (overflowRectCSS.Contains(documentRelativePoint)) {
+ rect = overflowRectCSS;
+ }
+ }
+ }
+ }
+ }
+
+ CSSRect elementBoundingRect = rect;
+
+ // Generally we zoom to the width of some element, but sometimes we zoom to
+ // the height. We set this to true when that happens so that we can add a
+ // vertical margin to the rect, otherwise it looks weird.
+ bool heightConstrained = false;
+
+ // If the element is taller than the visible area of the page scale
+ // the height of the |rect| so that it has the same aspect ratio as
+ // the root frame. The clipped |rect| is centered on the y value of
+ // the touch point. This allows tall narrow elements to be zoomed.
+ if (!rect.IsEmpty() && compositedArea.Width() > 0.0f &&
+ compositedArea.Height() > 0.0f) {
+ // Calculate the height of the rect if it had the same aspect ratio as
+ // compositedArea.
+ const float widthRatio = rect.Width() / compositedArea.Width();
+ float targetHeight = compositedArea.Height() * widthRatio;
+
+ // We don't want to cut off the top or bottoms of replaced elements that are
+ // square or wider in aspect ratio.
+
+ // If it's a replaced element and we would otherwise trim it's height below
+ if (IsReplacedElement(element) && targetHeight < rect.Height() &&
+ // If the target rect is at most 1.1x away from being square or wider
+ // aspect ratio
+ rect.Height() < 1.1 * rect.Width() &&
+ // and our compositedArea is wider than it is tall
+ compositedArea.Width() >= compositedArea.Height()) {
+ heightConstrained = true;
+ // Expand the width of the rect so that it fills compositedArea so that if
+ // we are already zoomed to this element then the IsRectZoomedIn call
+ // below returns true so that we zoom out. This won't change what we
+ // actually zoom to as we are just making the rect the same aspect ratio
+ // as compositedArea.
+ float targetWidth =
+ rect.Height() * compositedArea.Width() / compositedArea.Height();
+ MOZ_ASSERT(targetWidth > rect.Width());
+ if (targetWidth > rect.Width()) {
+ rect.x -= (targetWidth - rect.Width()) / 2;
+ rect.SetWidth(targetWidth);
+ // keep elementBoundingRect containing rect
+ elementBoundingRect = rect;
+ }
+
+ } else if (targetHeight < rect.Height()) {
+ // Trim the height so that the target rect has the same aspect ratio as
+ // compositedArea, centering it around the user tap point.
+ float newY = documentRelativePoint.y - (targetHeight * 0.5f);
+ if ((newY + targetHeight) > rect.YMost()) {
+ rect.MoveByY(rect.Height() - targetHeight);
+ } else if (newY > rect.Y()) {
+ rect.MoveToY(newY);
+ }
+ rect.SetHeight(targetHeight);
+ }
+ }
+
+ const CSSCoord margin = 15;
+ rect = AddHMargin(rect, margin, metrics);
+
+ if (heightConstrained) {
+ rect = AddVMargin(rect, margin, metrics);
+ }
+
+ // If the rect is already taking up most of the visible area and is
+ // stretching the width of the page, then we want to zoom out instead.
+ if (RectHasAlmostSameZoomLevel(rect, compositedArea)) {
+ return ZoomTarget{zoomOut, cantZoomOutBehavior, Nothing(),
+ Some(documentRelativePoint)};
+ }
+
+ elementBoundingRect = AddHMargin(elementBoundingRect, margin, metrics);
+
+ // Unlike rect, elementBoundingRect is the full height of the element we are
+ // zooming to. If we zoom to it without a margin it can look a weird, so give
+ // it a vertical margin.
+ elementBoundingRect = AddVMargin(elementBoundingRect, margin, metrics);
+
+ rect.Round();
+ elementBoundingRect.Round();
+ return ZoomTarget{rect, cantZoomOutBehavior, Some(elementBoundingRect),
+ Some(documentRelativePoint)};
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/util/DoubleTapToZoom.h b/gfx/layers/apz/util/DoubleTapToZoom.h
new file mode 100644
index 0000000000..91264deef9
--- /dev/null
+++ b/gfx/layers/apz/util/DoubleTapToZoom.h
@@ -0,0 +1,62 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_DoubleTapToZoom_h
+#define mozilla_layers_DoubleTapToZoom_h
+
+#include "Units.h"
+
+template <class T>
+class RefPtr;
+
+namespace mozilla {
+namespace dom {
+class Document;
+}
+
+namespace layers {
+
+enum class CantZoomOutBehavior : int8_t { Nothing = 0, ZoomIn };
+
+struct ZoomTarget {
+ // The preferred target rect that we'd like to zoom in on, if possible. An
+ // empty rect means the browser should zoom out.
+ CSSRect targetRect;
+
+ // If we are asked to zoom out but cannot (due to zoom constraints, etc), then
+ // zoom in some small amount to provide feedback to the user.
+ CantZoomOutBehavior cantZoomOutBehavior = CantZoomOutBehavior::Nothing;
+
+ // If zooming all the way in on |targetRect| is not possible (for example, due
+ // to a max zoom constraint), |elementBoundingRect| may be used to inform a
+ // more optimal target scroll position (for example, we may try to maximize
+ // the area of |elementBoundingRect| that's showing, while keeping
+ // |targetRect| in view and keeping the zoom as close to the desired zoom as
+ // possible).
+ Maybe<CSSRect> elementBoundingRect;
+
+ // The document relative (ie if the content inside the root scroll frame
+ // existed without that scroll frame) pointer position at the time of the
+ // double tap or location of the double tap if we can compute it. Only used if
+ // the rest of this ZoomTarget is asking to zoom out but we are already at the
+ // minimum zoom. In which case we zoom in a small amount on this point.
+ Maybe<CSSPoint> documentRelativePointerPosition;
+};
+
+/**
+ * For a double tap at |aPoint|, return a ZoomTarget struct with contains a rect
+ * to which the browser should zoom in response (see ZoomTarget definition for
+ * more details). An empty rect means the browser should zoom out. |aDocument|
+ * should be the root content document for the content that was tapped.
+ */
+ZoomTarget CalculateRectToZoomTo(
+ const RefPtr<mozilla::dom::Document>& aRootContentDocument,
+ const CSSPoint& aPoint);
+
+} // namespace layers
+} // namespace mozilla
+
+#endif /* mozilla_layers_DoubleTapToZoom_h */
diff --git a/gfx/layers/apz/util/InputAPZContext.cpp b/gfx/layers/apz/util/InputAPZContext.cpp
new file mode 100644
index 0000000000..77573221ff
--- /dev/null
+++ b/gfx/layers/apz/util/InputAPZContext.cpp
@@ -0,0 +1,65 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "InputAPZContext.h"
+
+namespace mozilla {
+namespace layers {
+
+ScrollableLayerGuid InputAPZContext::sGuid;
+uint64_t InputAPZContext::sBlockId = 0;
+nsEventStatus InputAPZContext::sApzResponse = nsEventStatus_eSentinel;
+bool InputAPZContext::sPendingLayerization = false;
+bool InputAPZContext::sRoutedToChildProcess = false;
+
+/*static*/
+ScrollableLayerGuid InputAPZContext::GetTargetLayerGuid() { return sGuid; }
+
+/*static*/
+uint64_t InputAPZContext::GetInputBlockId() { return sBlockId; }
+
+/*static*/
+nsEventStatus InputAPZContext::GetApzResponse() { return sApzResponse; }
+
+/*static*/
+bool InputAPZContext::HavePendingLayerization() { return sPendingLayerization; }
+
+/*static*/
+bool InputAPZContext::WasRoutedToChildProcess() {
+ return sRoutedToChildProcess;
+}
+
+InputAPZContext::InputAPZContext(const ScrollableLayerGuid& aGuid,
+ const uint64_t& aBlockId,
+ const nsEventStatus& aApzResponse,
+ bool aPendingLayerization)
+ : mOldGuid(sGuid),
+ mOldBlockId(sBlockId),
+ mOldApzResponse(sApzResponse),
+ mOldPendingLayerization(sPendingLayerization),
+ mOldRoutedToChildProcess(sRoutedToChildProcess) {
+ sGuid = aGuid;
+ sBlockId = aBlockId;
+ sApzResponse = aApzResponse;
+ sPendingLayerization = aPendingLayerization;
+ sRoutedToChildProcess = false;
+}
+
+InputAPZContext::~InputAPZContext() {
+ sGuid = mOldGuid;
+ sBlockId = mOldBlockId;
+ sApzResponse = mOldApzResponse;
+ sPendingLayerization = mOldPendingLayerization;
+ sRoutedToChildProcess = mOldRoutedToChildProcess;
+}
+
+/*static*/
+void InputAPZContext::SetRoutedToChildProcess() {
+ sRoutedToChildProcess = true;
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/util/InputAPZContext.h b/gfx/layers/apz/util/InputAPZContext.h
new file mode 100644
index 0000000000..928359ab1d
--- /dev/null
+++ b/gfx/layers/apz/util/InputAPZContext.h
@@ -0,0 +1,69 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_InputAPZContext_h
+#define mozilla_layers_InputAPZContext_h
+
+#include "mozilla/EventForwards.h"
+#include "mozilla/layers/ScrollableLayerGuid.h"
+
+namespace mozilla {
+namespace layers {
+
+// InputAPZContext is used to communicate various pieces of information
+// around the codebase without having to plumb it through lots of functions
+// and codepaths. Conceptually it is attached to a WidgetInputEvent that is
+// relevant to APZ.
+//
+// There are two types of information bits propagated using this class. One
+// type is propagated "downwards" (from a process entry point like nsBaseWidget
+// or BrowserChild) into deeper code that is run during complicated operations
+// like event dispatch. The other type is information that is propagated
+// "upwards", from the deeper code back to the entry point.
+class MOZ_STACK_CLASS InputAPZContext {
+ private:
+ // State that is propagated downwards from InputAPZContext creation into
+ // "deeper" code.
+ static ScrollableLayerGuid sGuid;
+ static uint64_t sBlockId;
+ static nsEventStatus sApzResponse;
+ static bool sPendingLayerization;
+
+ // State that is set in deeper code and propagated upwards.
+ static bool sRoutedToChildProcess;
+
+ public:
+ // Functions to access downwards-propagated data
+ static ScrollableLayerGuid GetTargetLayerGuid();
+ static uint64_t GetInputBlockId();
+ static nsEventStatus GetApzResponse();
+ static bool HavePendingLayerization();
+
+ // Functions to access upwards-propagated data
+ static bool WasRoutedToChildProcess();
+
+ // Constructor sets the data to be propagated downwards
+ InputAPZContext(const ScrollableLayerGuid& aGuid, const uint64_t& aBlockId,
+ const nsEventStatus& aApzResponse,
+ bool aPendingLayerization = false);
+ ~InputAPZContext();
+
+ // Functions to set data to be propagated upwards
+ static void SetRoutedToChildProcess();
+
+ private:
+ ScrollableLayerGuid mOldGuid;
+ uint64_t mOldBlockId;
+ nsEventStatus mOldApzResponse;
+ bool mOldPendingLayerization;
+
+ bool mOldRoutedToChildProcess;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif /* mozilla_layers_InputAPZContext_h */
diff --git a/gfx/layers/apz/util/ScrollLinkedEffectDetector.cpp b/gfx/layers/apz/util/ScrollLinkedEffectDetector.cpp
new file mode 100644
index 0000000000..eb456fa243
--- /dev/null
+++ b/gfx/layers/apz/util/ScrollLinkedEffectDetector.cpp
@@ -0,0 +1,48 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ScrollLinkedEffectDetector.h"
+
+#include "mozilla/dom/Document.h"
+#include "nsThreadUtils.h"
+
+namespace mozilla {
+namespace layers {
+
+uint32_t ScrollLinkedEffectDetector::sDepth = 0;
+bool ScrollLinkedEffectDetector::sFoundScrollLinkedEffect = false;
+
+/* static */
+void ScrollLinkedEffectDetector::PositioningPropertyMutated() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (sDepth > 0) {
+ // We are inside a scroll event dispatch
+ sFoundScrollLinkedEffect = true;
+ }
+}
+
+ScrollLinkedEffectDetector::ScrollLinkedEffectDetector(
+ dom::Document* aDoc, const TimeStamp& aTimeStamp)
+ : mDocument(aDoc), mTimeStamp(aTimeStamp) {
+ MOZ_ASSERT(NS_IsMainThread());
+ sDepth++;
+}
+
+ScrollLinkedEffectDetector::~ScrollLinkedEffectDetector() {
+ sDepth--;
+ if (sDepth == 0) {
+ // We have exited all (possibly-nested) scroll event dispatches,
+ // record whether or not we found an effect, and reset state
+ if (sFoundScrollLinkedEffect) {
+ mDocument->ReportHasScrollLinkedEffect(mTimeStamp);
+ sFoundScrollLinkedEffect = false;
+ }
+ }
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/util/ScrollLinkedEffectDetector.h b/gfx/layers/apz/util/ScrollLinkedEffectDetector.h
new file mode 100644
index 0000000000..4568fe649b
--- /dev/null
+++ b/gfx/layers/apz/util/ScrollLinkedEffectDetector.h
@@ -0,0 +1,48 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_ScrollLinkedEffectDetector_h
+#define mozilla_layers_ScrollLinkedEffectDetector_h
+
+#include "mozilla/RefPtr.h"
+#include "mozilla/TimeStamp.h"
+
+namespace mozilla {
+
+namespace dom {
+class Document;
+}
+
+namespace layers {
+
+// ScrollLinkedEffectDetector is used to detect the existence of a scroll-linked
+// effect on a webpage. Generally speaking, a scroll-linked effect is something
+// on the page that animates or changes with respect to the scroll position.
+// Content authors usually rely on running some JS in response to the scroll
+// event in order to implement such effects, and therefore it tends to be laggy
+// or work improperly with APZ enabled. This class helps us detect such an
+// effect so that we can warn the author and/or take other preventative
+// measures.
+class MOZ_STACK_CLASS ScrollLinkedEffectDetector final {
+ private:
+ static uint32_t sDepth;
+ static bool sFoundScrollLinkedEffect;
+
+ public:
+ static void PositioningPropertyMutated();
+
+ ScrollLinkedEffectDetector(dom::Document*, const TimeStamp& aTimeStamp);
+ ~ScrollLinkedEffectDetector();
+
+ private:
+ RefPtr<dom::Document> mDocument;
+ TimeStamp mTimeStamp;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif /* mozilla_layers_ScrollLinkedEffectDetector_h */
diff --git a/gfx/layers/apz/util/ScrollingInteractionContext.cpp b/gfx/layers/apz/util/ScrollingInteractionContext.cpp
new file mode 100644
index 0000000000..1a92a9eb07
--- /dev/null
+++ b/gfx/layers/apz/util/ScrollingInteractionContext.cpp
@@ -0,0 +1,29 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ScrollingInteractionContext.h"
+
+namespace mozilla::layers {
+
+/*static*/
+bool ScrollingInteractionContext::sScrollingToAnchor = false;
+
+/*static*/
+bool ScrollingInteractionContext::IsScrollingToAnchor() {
+ return sScrollingToAnchor;
+}
+
+ScrollingInteractionContext::ScrollingInteractionContext(
+ bool aScrollingToAnchor)
+ : mOldScrollingToAnchor(sScrollingToAnchor) {
+ sScrollingToAnchor = aScrollingToAnchor;
+}
+
+ScrollingInteractionContext::~ScrollingInteractionContext() {
+ sScrollingToAnchor = mOldScrollingToAnchor;
+}
+
+} // namespace mozilla::layers
diff --git a/gfx/layers/apz/util/ScrollingInteractionContext.h b/gfx/layers/apz/util/ScrollingInteractionContext.h
new file mode 100644
index 0000000000..cae953008b
--- /dev/null
+++ b/gfx/layers/apz/util/ScrollingInteractionContext.h
@@ -0,0 +1,40 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_ScrollingInteractionContext_h
+#define mozilla_layers_ScrollingInteractionContext_h
+
+#include "mozilla/EventForwards.h"
+#include "mozilla/layers/ScrollableLayerGuid.h"
+
+namespace mozilla {
+namespace layers {
+
+// The ScrollingInteractionContext is used to store minor details of the
+// current scrolling interaction on the stack to avoid having to pass them
+// though the callstack
+class MOZ_STACK_CLASS ScrollingInteractionContext {
+ private:
+ static bool sScrollingToAnchor;
+
+ public:
+ // Functions to access downwards-propagated data
+ static bool IsScrollingToAnchor();
+
+ // Constructor sets the data to be propagated downwards
+ explicit ScrollingInteractionContext(bool aScrollingToAnchor);
+
+ // Destructor restores the previous state
+ ~ScrollingInteractionContext();
+
+ private:
+ bool mOldScrollingToAnchor;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif /* mozilla_layers_ScrollingInteractionContext_h */
diff --git a/gfx/layers/apz/util/TouchActionHelper.cpp b/gfx/layers/apz/util/TouchActionHelper.cpp
new file mode 100644
index 0000000000..4598e30a6a
--- /dev/null
+++ b/gfx/layers/apz/util/TouchActionHelper.cpp
@@ -0,0 +1,131 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "TouchActionHelper.h"
+
+#include "mozilla/layers/IAPZCTreeManager.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/TouchEvents.h"
+#include "nsContainerFrame.h"
+#include "nsIFrameInlines.h"
+#include "nsIScrollableFrame.h"
+#include "nsLayoutUtils.h"
+
+namespace mozilla::layers {
+
+static void UpdateAllowedBehavior(StyleTouchAction aTouchActionValue,
+ bool aConsiderPanning,
+ TouchBehaviorFlags& aOutBehavior) {
+ if (aTouchActionValue != StyleTouchAction::AUTO) {
+ // Double-tap-zooming need property value AUTO
+ aOutBehavior &= ~AllowedTouchBehavior::ANIMATING_ZOOM;
+ if (aTouchActionValue != StyleTouchAction::MANIPULATION &&
+ !(aTouchActionValue & StyleTouchAction::PINCH_ZOOM)) {
+ // Pinch-zooming needs value AUTO or MANIPULATION, or the PINCH_ZOOM bit
+ // set
+ aOutBehavior &= ~AllowedTouchBehavior::PINCH_ZOOM;
+ }
+ }
+
+ if (aConsiderPanning) {
+ if (aTouchActionValue == StyleTouchAction::NONE) {
+ aOutBehavior &= ~AllowedTouchBehavior::VERTICAL_PAN;
+ aOutBehavior &= ~AllowedTouchBehavior::HORIZONTAL_PAN;
+ }
+
+ // Values pan-x and pan-y set at the same time to the same element do not
+ // affect panning constraints. Therefore we need to check whether pan-x is
+ // set without pan-y and the same for pan-y.
+ if ((aTouchActionValue & StyleTouchAction::PAN_X) &&
+ !(aTouchActionValue & StyleTouchAction::PAN_Y)) {
+ aOutBehavior &= ~AllowedTouchBehavior::VERTICAL_PAN;
+ } else if ((aTouchActionValue & StyleTouchAction::PAN_Y) &&
+ !(aTouchActionValue & StyleTouchAction::PAN_X)) {
+ aOutBehavior &= ~AllowedTouchBehavior::HORIZONTAL_PAN;
+ }
+ }
+}
+
+static TouchBehaviorFlags GetAllowedTouchBehaviorForPoint(
+ nsIWidget* aWidget, RelativeTo aRootFrame,
+ const LayoutDeviceIntPoint& aPoint) {
+ nsPoint relativePoint =
+ nsLayoutUtils::GetEventCoordinatesRelativeTo(aWidget, aPoint, aRootFrame);
+
+ nsIFrame* target = nsLayoutUtils::GetFrameForPoint(aRootFrame, relativePoint);
+
+ return TouchActionHelper::GetAllowedTouchBehaviorForFrame(target);
+}
+
+nsTArray<TouchBehaviorFlags> TouchActionHelper::GetAllowedTouchBehavior(
+ nsIWidget* aWidget, dom::Document* aDocument,
+ const WidgetTouchEvent& aEvent) {
+ nsTArray<TouchBehaviorFlags> flags;
+ if (!aWidget || !aDocument) {
+ return flags;
+ }
+ if (PresShell* presShell = aDocument->GetPresShell()) {
+ if (nsIFrame* rootFrame = presShell->GetRootFrame()) {
+ for (const auto& touch : aEvent.mTouches) {
+ flags.AppendElement(GetAllowedTouchBehaviorForPoint(
+ aWidget, RelativeTo{rootFrame, ViewportType::Visual},
+ touch->mRefPoint));
+ }
+ }
+ }
+ return flags;
+}
+
+TouchBehaviorFlags TouchActionHelper::GetAllowedTouchBehaviorForFrame(
+ nsIFrame* aFrame) {
+ TouchBehaviorFlags behavior = AllowedTouchBehavior::VERTICAL_PAN |
+ AllowedTouchBehavior::HORIZONTAL_PAN |
+ AllowedTouchBehavior::PINCH_ZOOM |
+ AllowedTouchBehavior::ANIMATING_ZOOM;
+
+ if (!aFrame) {
+ return behavior;
+ }
+
+ nsIScrollableFrame* nearestScrollableParent =
+ nsLayoutUtils::GetNearestScrollableFrame(aFrame, 0);
+ nsIFrame* nearestScrollableFrame = do_QueryFrame(nearestScrollableParent);
+
+ // We're walking up the DOM tree until we meet the element with touch behavior
+ // and accumulating touch-action restrictions of all elements in this chain.
+ // The exact quote from the spec, that clarifies more:
+ // To determine the effect of a touch, find the nearest ancestor (starting
+ // from the element itself) that has a default touch behavior. Then examine
+ // the touch-action property of each element between the hit tested element
+ // and the element with the default touch behavior (including both the hit
+ // tested element and the element with the default touch behavior). If the
+ // touch-action property of any of those elements disallows the default touch
+ // behavior, do nothing. Otherwise allow the element to start considering the
+ // touch for the purposes of executing a default touch behavior.
+
+ // Currently we support only two touch behaviors: panning and zooming.
+ // For panning we walk up until we meet the first scrollable element (the
+ // element that supports panning) or root element. For zooming we walk up
+ // until the root element since Firefox currently supports only zooming of the
+ // root frame but not the subframes.
+
+ bool considerPanning = true;
+
+ for (nsIFrame* frame = aFrame; frame && frame->GetContent() && behavior;
+ frame = frame->GetInFlowParent()) {
+ UpdateAllowedBehavior(frame->UsedTouchAction(), considerPanning, behavior);
+
+ if (frame == nearestScrollableFrame) {
+ // We met the scrollable element, after it we shouldn't consider
+ // touch-action values for the purpose of panning but only for zooming.
+ considerPanning = false;
+ }
+ }
+
+ return behavior;
+}
+
+} // namespace mozilla::layers
diff --git a/gfx/layers/apz/util/TouchActionHelper.h b/gfx/layers/apz/util/TouchActionHelper.h
new file mode 100644
index 0000000000..b83d0d9ecb
--- /dev/null
+++ b/gfx/layers/apz/util/TouchActionHelper.h
@@ -0,0 +1,46 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef __mozilla_layers_TouchActionHelper_h__
+#define __mozilla_layers_TouchActionHelper_h__
+
+#include "mozilla/layers/LayersTypes.h" // for TouchBehaviorFlags
+#include "RelativeTo.h" // for RelativeTo
+
+class nsIWidget;
+namespace mozilla {
+
+namespace dom {
+class Document;
+} // namespace dom
+
+class WidgetTouchEvent;
+} // namespace mozilla
+
+namespace mozilla::layers {
+
+/*
+ * Helper class to figure out the allowed touch behavior for frames, as per
+ * the touch-action spec.
+ */
+class TouchActionHelper {
+ public:
+ /*
+ * Performs hit testing on content, finds frame that corresponds to the touch
+ * points of aEvent and retrieves touch-action CSS property value from it
+ * according the rules specified in the spec:
+ * http://www.w3.org/TR/pointerevents/#the-touch-action-css-property.
+ */
+ static nsTArray<TouchBehaviorFlags> GetAllowedTouchBehavior(
+ nsIWidget* aWidget, dom::Document* aDocument,
+ const WidgetTouchEvent& aPoint);
+
+ static TouchBehaviorFlags GetAllowedTouchBehaviorForFrame(nsIFrame* aFrame);
+};
+
+} // namespace mozilla::layers
+
+#endif /*__mozilla_layers_TouchActionHelper_h__ */
diff --git a/gfx/layers/apz/util/TouchCounter.cpp b/gfx/layers/apz/util/TouchCounter.cpp
new file mode 100644
index 0000000000..9e4d6f1ba6
--- /dev/null
+++ b/gfx/layers/apz/util/TouchCounter.cpp
@@ -0,0 +1,74 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "TouchCounter.h"
+
+#include "InputData.h"
+#include "mozilla/TouchEvents.h"
+
+namespace mozilla {
+namespace layers {
+
+TouchCounter::TouchCounter() : mActiveTouchCount(0) {}
+
+void TouchCounter::Update(const MultiTouchInput& aInput) {
+ switch (aInput.mType) {
+ case MultiTouchInput::MULTITOUCH_START:
+ // touch-start event contains all active touches of the current session
+ mActiveTouchCount = aInput.mTouches.Length();
+ break;
+ case MultiTouchInput::MULTITOUCH_END:
+ if (mActiveTouchCount >= aInput.mTouches.Length()) {
+ // touch-end event contains only released touches
+ mActiveTouchCount -= aInput.mTouches.Length();
+ } else {
+ NS_WARNING("Got an unexpected touchend");
+ mActiveTouchCount = 0;
+ }
+ break;
+ case MultiTouchInput::MULTITOUCH_CANCEL:
+ mActiveTouchCount = 0;
+ break;
+ case MultiTouchInput::MULTITOUCH_MOVE:
+ break;
+ }
+}
+
+void TouchCounter::Update(const WidgetTouchEvent& aEvent) {
+ switch (aEvent.mMessage) {
+ case eTouchStart:
+ // touch-start event contains all active touches of the current session
+ mActiveTouchCount = aEvent.mTouches.Length();
+ break;
+ case eTouchEnd: {
+ // touch-end contains all touches, but ones being lifted are marked as
+ // changed
+ uint32_t liftedTouches = 0;
+ for (const auto& touch : aEvent.mTouches) {
+ if (touch->mChanged) {
+ liftedTouches++;
+ }
+ }
+ if (mActiveTouchCount >= liftedTouches) {
+ mActiveTouchCount -= liftedTouches;
+ } else {
+ NS_WARNING("Got an unexpected touchend");
+ mActiveTouchCount = 0;
+ }
+ break;
+ }
+ case eTouchCancel:
+ mActiveTouchCount = 0;
+ break;
+ default:
+ break;
+ }
+}
+
+uint32_t TouchCounter::GetActiveTouchCount() const { return mActiveTouchCount; }
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/util/TouchCounter.h b/gfx/layers/apz/util/TouchCounter.h
new file mode 100644
index 0000000000..c13f475355
--- /dev/null
+++ b/gfx/layers/apz/util/TouchCounter.h
@@ -0,0 +1,36 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_TouchCounter_h
+#define mozilla_layers_TouchCounter_h
+
+#include "mozilla/EventForwards.h"
+
+namespace mozilla {
+
+class MultiTouchInput;
+
+namespace layers {
+
+// TouchCounter simply tracks the number of active touch points. Feed it
+// your input events to update the internal state. Generally you should
+// only be calling one of the Update functions, depending on which type
+// of touch inputs you have access to.
+class TouchCounter {
+ public:
+ TouchCounter();
+ void Update(const MultiTouchInput& aInput);
+ void Update(const WidgetTouchEvent& aEvent);
+ uint32_t GetActiveTouchCount() const;
+
+ private:
+ uint32_t mActiveTouchCount;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif /* mozilla_layers_TouchCounter_h */
diff --git a/gfx/layers/client/CanvasClient.cpp b/gfx/layers/client/CanvasClient.cpp
new file mode 100644
index 0000000000..22792e6caf
--- /dev/null
+++ b/gfx/layers/client/CanvasClient.cpp
@@ -0,0 +1,96 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "CanvasClient.h"
+
+#include "gfx2DGlue.h" // for ImageFormatToSurfaceFormat
+#include "gfxPlatform.h" // for gfxPlatform
+#include "mozilla/gfx/BaseSize.h" // for BaseSize
+#include "mozilla/gfx/gfxVars.h"
+#include "mozilla/layers/BufferTexture.h"
+#include "mozilla/layers/CompositableForwarder.h"
+#include "mozilla/layers/CompositorBridgeChild.h" // for CompositorBridgeChild
+#include "mozilla/layers/LayersTypes.h"
+#include "mozilla/layers/OOPCanvasRenderer.h"
+#include "mozilla/layers/TextureClient.h" // for TextureClient, etc
+#include "mozilla/layers/TextureClientOGL.h"
+#include "mozilla/layers/TextureClientRecycleAllocator.h"
+#include "nsDebug.h" // for printf_stderr, NS_ASSERTION
+#include "nsXULAppAPI.h" // for XRE_GetProcessType, etc
+
+using namespace mozilla::gfx;
+using namespace mozilla::gl;
+
+namespace mozilla {
+namespace layers {
+
+void CanvasClient::UseTexture(TextureClient* const aTexture) {
+ MOZ_ASSERT(aTexture);
+
+ const auto isClientNonPremult =
+ bool(mTextureFlags & TextureFlags::NON_PREMULTIPLIED);
+ const auto isTextureNonPremult =
+ bool(aTexture->GetFlags() & TextureFlags::NON_PREMULTIPLIED);
+ MOZ_ALWAYS_TRUE(isTextureNonPremult == isClientNonPremult);
+
+ bool changed = false;
+
+ if (aTexture != mFrontBuffer) {
+ if (!aTexture->IsSharedWithCompositor()) {
+ if (!AddTextureClient(aTexture)) {
+ return;
+ }
+ }
+ changed = true;
+ mFrontBuffer = aTexture;
+ }
+
+ AutoTArray<CompositableForwarder::TimedTextureClient, 1> textures;
+ CompositableForwarder::TimedTextureClient* t = textures.AppendElement();
+ t->mTextureClient = aTexture;
+ t->mPictureRect = nsIntRect(nsIntPoint(0, 0), aTexture->GetSize());
+ t->mFrameID = mFrameID;
+
+ GetForwarder()->UseTextures(this, textures);
+ if (changed) {
+ aTexture->SyncWithObject(GetForwarder()->GetSyncObject());
+ }
+}
+
+static constexpr bool kIsWindows =
+#ifdef XP_WIN
+ true;
+#else
+ false;
+#endif
+
+RefPtr<TextureClient> CanvasClient::CreateTextureClientForCanvas(
+ const gfx::SurfaceFormat aFormat, const gfx::IntSize aSize,
+ const TextureFlags aFlags) {
+ if (kIsWindows) {
+ // With WebRender, host side uses data of TextureClient longer.
+ // Then back buffer reuse in CanvasClient2D::Update() does not work. It
+ // causes a lot of TextureClient allocations. For reducing the allocations,
+ // TextureClientRecycler is used.
+ if (GetForwarder() && GetForwarder()->GetCompositorBackendType() ==
+ LayersBackend::LAYERS_WR) {
+ return GetTextureClientRecycler()->CreateOrRecycle(
+ aFormat, aSize, BackendSelector::Canvas, mTextureFlags | aFlags);
+ }
+ return CreateTextureClientForDrawing(
+ aFormat, aSize, BackendSelector::Canvas, mTextureFlags | aFlags);
+ }
+
+ // XXX - We should use CreateTextureClientForDrawing, but we first need
+ // to use double buffering.
+ gfx::BackendType backend =
+ gfxPlatform::GetPlatform()->GetPreferredCanvasBackend();
+ return TextureClient::CreateForRawBufferAccess(
+ GetForwarder(), aFormat, aSize, backend, mTextureFlags | aFlags);
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/client/CanvasClient.h b/gfx/layers/client/CanvasClient.h
new file mode 100644
index 0000000000..2136e6f652
--- /dev/null
+++ b/gfx/layers/client/CanvasClient.h
@@ -0,0 +1,71 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_GFX_CANVASCLIENT_H
+#define MOZILLA_GFX_CANVASCLIENT_H
+
+#include "mozilla/Assertions.h" // for MOZ_ASSERT, etc
+#include "mozilla/Attributes.h" // for override
+#include "mozilla/RefPtr.h" // for RefPtr, already_AddRefed
+#include "mozilla/layers/CompositableClient.h" // for CompositableClient
+#include "mozilla/layers/CompositorTypes.h" // for TextureInfo, etc
+#include "mozilla/layers/LayersSurfaces.h" // for SurfaceDescriptor
+#include "mozilla/layers/TextureClient.h" // for TextureClient, etc
+#include "mozilla/layers/PersistentBufferProvider.h"
+
+#include "mozilla/MaybeOneOf.h"
+
+#include "mozilla/mozalloc.h" // for operator delete
+
+#include "mozilla/gfx/Point.h" // for IntSize
+#include "mozilla/gfx/Types.h" // for SurfaceFormat
+
+namespace mozilla {
+namespace layers {
+
+class CompositableForwarder;
+
+/**
+ * Compositable client for 2d and webgl canvas.
+ */
+class CanvasClient final : public CompositableClient {
+ int32_t mFrameID = 0;
+ RefPtr<TextureClient> mFrontBuffer;
+
+ public:
+ /**
+ * Creates, configures, and returns a new canvas client. If necessary, a
+ * message will be sent to the compositor to create a corresponding image
+ * host.
+ */
+ CanvasClient(CompositableForwarder* aFwd, const TextureFlags flags)
+ : CompositableClient(aFwd, flags) {}
+
+ virtual ~CanvasClient() = default;
+
+ void Clear() { mFrontBuffer = nullptr; }
+
+ bool AddTextureClient(TextureClient* aTexture) override {
+ ++mFrameID;
+ return CompositableClient::AddTextureClient(aTexture);
+ }
+
+ TextureInfo GetTextureInfo() const override {
+ return TextureInfo(CompositableType::IMAGE, mTextureFlags);
+ }
+
+ void OnDetach() override { Clear(); }
+
+ RefPtr<TextureClient> CreateTextureClientForCanvas(gfx::SurfaceFormat,
+ gfx::IntSize,
+ TextureFlags);
+ void UseTexture(TextureClient*);
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif
diff --git a/gfx/layers/client/CompositableClient.cpp b/gfx/layers/client/CompositableClient.cpp
new file mode 100644
index 0000000000..9178f8f5c1
--- /dev/null
+++ b/gfx/layers/client/CompositableClient.cpp
@@ -0,0 +1,182 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/layers/CompositableClient.h"
+
+#include <stdint.h> // for uint64_t, uint32_t
+
+#include "gfxPlatform.h" // for gfxPlatform
+#include "mozilla/layers/CompositableForwarder.h"
+#include "mozilla/layers/ImageBridgeChild.h"
+#include "mozilla/layers/TextureClient.h" // for TextureClient, etc
+#include "mozilla/layers/TextureClientOGL.h"
+#include "mozilla/layers/TextureClientRecycleAllocator.h"
+#include "mozilla/mozalloc.h" // for operator delete, etc
+#ifdef XP_WIN
+# include "gfxWindowsPlatform.h" // for gfxWindowsPlatform
+# include "mozilla/layers/TextureD3D11.h"
+#endif
+#include "IPDLActor.h"
+#include "gfxUtils.h"
+
+namespace mozilla {
+namespace layers {
+
+using namespace mozilla::gfx;
+
+void CompositableClient::InitIPDL(const CompositableHandle& aHandle) {
+ MOZ_ASSERT(aHandle);
+
+ mForwarder->AssertInForwarderThread();
+
+ mHandle = aHandle;
+ mIsAsync |= !NS_IsMainThread();
+ // By simply taking the lock on mTextureClientRecycler we force memory barrier
+ // on mHandle and mIsAsync which makes them behave as Atomic as they are only
+ // ever set in this method.
+ auto l = mTextureClientRecycler.Lock();
+}
+
+CompositableClient::CompositableClient(CompositableForwarder* aForwarder,
+ TextureFlags aTextureFlags)
+ : mForwarder(aForwarder),
+ mTextureFlags(aTextureFlags),
+ mTextureClientRecycler("CompositableClient::mTextureClientRecycler"),
+ mIsAsync(false) {}
+
+CompositableClient::~CompositableClient() { Destroy(); }
+
+LayersBackend CompositableClient::GetCompositorBackendType() const {
+ return mForwarder->GetCompositorBackendType();
+}
+
+bool CompositableClient::Connect(ImageContainer* aImageContainer) {
+ MOZ_ASSERT(!mHandle);
+ if (!GetForwarder() || mHandle) {
+ return false;
+ }
+
+ GetForwarder()->AssertInForwarderThread();
+ GetForwarder()->Connect(this, aImageContainer);
+ return true;
+}
+
+bool CompositableClient::IsConnected() const {
+ // CanSend() is only reliable in the same thread as the IPDL channel.
+ mForwarder->AssertInForwarderThread();
+ return !!mHandle;
+}
+
+void CompositableClient::Destroy() {
+ auto l = mTextureClientRecycler.Lock();
+ if (*l) {
+ (*l)->Destroy();
+ }
+
+ if (mHandle) {
+ mForwarder->ReleaseCompositable(mHandle);
+ mHandle = CompositableHandle();
+ }
+}
+
+CompositableHandle CompositableClient::GetAsyncHandle() const {
+ if (mIsAsync) {
+ return mHandle;
+ }
+ return CompositableHandle();
+}
+
+already_AddRefed<TextureClient> CompositableClient::CreateBufferTextureClient(
+ gfx::SurfaceFormat aFormat, gfx::IntSize aSize,
+ gfx::BackendType aMoz2DBackend, TextureFlags aTextureFlags) {
+ return TextureClient::CreateForRawBufferAccess(GetForwarder(), aFormat, aSize,
+ aMoz2DBackend,
+ aTextureFlags | mTextureFlags);
+}
+
+already_AddRefed<TextureClient>
+CompositableClient::CreateTextureClientForDrawing(
+ gfx::SurfaceFormat aFormat, gfx::IntSize aSize, BackendSelector aSelector,
+ TextureFlags aTextureFlags, TextureAllocationFlags aAllocFlags) {
+ return TextureClient::CreateForDrawing(
+ GetForwarder(), aFormat, aSize, aSelector, aTextureFlags | mTextureFlags,
+ aAllocFlags);
+}
+
+already_AddRefed<TextureClient>
+CompositableClient::CreateTextureClientFromSurface(
+ gfx::SourceSurface* aSurface, BackendSelector aSelector,
+ TextureFlags aTextureFlags, TextureAllocationFlags aAllocFlags) {
+ return TextureClient::CreateFromSurface(GetForwarder(), aSurface, aSelector,
+ aTextureFlags | mTextureFlags,
+ aAllocFlags);
+}
+
+bool CompositableClient::AddTextureClient(TextureClient* aClient) {
+ if (!aClient) {
+ return false;
+ }
+ aClient->SetAddedToCompositableClient();
+ return aClient->InitIPDLActor(mForwarder);
+}
+
+void CompositableClient::ClearCachedResources() {
+ auto l = mTextureClientRecycler.Lock();
+ if (*l) {
+ (*l)->ShrinkToMinimumSize();
+ }
+}
+
+void CompositableClient::HandleMemoryPressure() {
+ auto l = mTextureClientRecycler.Lock();
+ if (*l) {
+ (*l)->ShrinkToMinimumSize();
+ }
+}
+
+void CompositableClient::RemoveTexture(TextureClient* aTexture) {
+ mForwarder->RemoveTextureFromCompositable(this, aTexture);
+}
+
+TextureClientRecycleAllocator* CompositableClient::GetTextureClientRecycler() {
+ auto l = mTextureClientRecycler.Lock();
+ if (*l) {
+ return *l;
+ }
+
+ if (!mForwarder || !mForwarder->GetTextureForwarder()) {
+ return nullptr;
+ }
+
+ *l = new layers::TextureClientRecycleAllocator(mForwarder);
+ return *l;
+}
+
+void CompositableClient::DumpTextureClient(std::stringstream& aStream,
+ TextureClient* aTexture,
+ TextureDumpMode aCompress) {
+ if (!aTexture) {
+ return;
+ }
+ RefPtr<gfx::DataSourceSurface> dSurf = aTexture->GetAsSurface();
+ if (!dSurf) {
+ return;
+ }
+ if (aCompress == TextureDumpMode::Compress) {
+ aStream << gfxUtils::GetAsLZ4Base64Str(dSurf).get();
+ } else {
+ aStream << gfxUtils::GetAsDataURI(dSurf).get();
+ }
+}
+
+AutoRemoveTexture::~AutoRemoveTexture() {
+ if (mCompositable && mTexture && mCompositable->IsConnected()) {
+ mCompositable->RemoveTexture(mTexture);
+ }
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/client/CompositableClient.h b/gfx/layers/client/CompositableClient.h
new file mode 100644
index 0000000000..5ca8ae067d
--- /dev/null
+++ b/gfx/layers/client/CompositableClient.h
@@ -0,0 +1,208 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_GFX_BUFFERCLIENT_H
+#define MOZILLA_GFX_BUFFERCLIENT_H
+
+#include <stdint.h> // for uint64_t
+
+#include <map> // for map
+#include <vector> // for vector
+
+#include "mozilla/Assertions.h" // for MOZ_CRASH
+#include "mozilla/DataMutex.h"
+#include "mozilla/RefPtr.h" // for already_AddRefed, RefCounted
+#include "mozilla/gfx/Types.h" // for SurfaceFormat
+#include "mozilla/layers/CompositorTypes.h"
+#include "mozilla/layers/LayersTypes.h" // for LayersBackend, TextureDumpMode
+#include "mozilla/layers/TextureClient.h" // for TextureClient
+#include "mozilla/webrender/WebRenderTypes.h" // for RenderRoot
+#include "nsISupportsImpl.h" // for MOZ_COUNT_CTOR, etc
+
+namespace mozilla {
+namespace layers {
+
+class CompositableClient;
+class ImageBridgeChild;
+class ImageContainer;
+class CompositableForwarder;
+class CompositableChild;
+class TextureClientRecycleAllocator;
+class ContentClientRemoteBuffer;
+
+/**
+ * CompositableClient manages the texture-specific logic for composite layers,
+ * independently of the layer. It is the content side of a CompositableClient/
+ * CompositableHost pair.
+ *
+ * CompositableClient's purpose is to send texture data to the compositor side
+ * along with any extra information about how the texture is to be composited.
+ * Things like opacity or transformation belong to layer and not compositable.
+ *
+ * Since Compositables are independent of layers it is possible to create one,
+ * connect it to the compositor side, and start sending images to it. This alone
+ * is arguably not very useful, but it means that as long as a shadow layer can
+ * do the proper magic to find a reference to the right CompositableHost on the
+ * Compositor side, a Compositable client can be used outside of the main
+ * shadow layer forwarder machinery that is used on the main thread.
+ *
+ * The first step is to create a Compositable client and call Connect().
+ * Connect() creates the underlying IPDL actor (see CompositableChild) and the
+ * corresponding CompositableHost on the other side.
+ *
+ * To do async texture transfer (like async-video), the CompositableClient
+ * should be created with a different CompositableForwarder (like
+ * ImageBridgeChild) and attachment is done with
+ * CompositableForwarder::AttachAsyncCompositable that takes an identifier
+ * instead of a CompositableChild, since the CompositableClient is not managed
+ * by this layer forwarder (the matching uses a global map on the compositor
+ * side, see CompositableMap in ImageBridgeParent.cpp)
+ *
+ * Subclasses: Painted layers use ContentClients, ImageLayers use ImageClients,
+ * Canvas layers use CanvasClients (but ImageHosts). We have a different
+ * subclass where we have a different way of interfacing with the textures - in
+ * terms of drawing into the compositable and/or passing its contents to the
+ * compostior.
+ */
+class CompositableClient {
+ protected:
+ virtual ~CompositableClient();
+
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(CompositableClient)
+
+ explicit CompositableClient(CompositableForwarder* aForwarder,
+ TextureFlags aFlags = TextureFlags::NO_FLAGS);
+
+ virtual void Dump(std::stringstream& aStream, const char* aPrefix = "",
+ bool aDumpHtml = false,
+ TextureDumpMode aCompress = TextureDumpMode::Compress){};
+
+ virtual TextureInfo GetTextureInfo() const = 0;
+
+ LayersBackend GetCompositorBackendType() const;
+
+ already_AddRefed<TextureClient> CreateBufferTextureClient(
+ gfx::SurfaceFormat aFormat, gfx::IntSize aSize,
+ gfx::BackendType aMoz2dBackend = gfx::BackendType::NONE,
+ TextureFlags aFlags = TextureFlags::DEFAULT);
+
+ already_AddRefed<TextureClient> CreateTextureClientForDrawing(
+ gfx::SurfaceFormat aFormat, gfx::IntSize aSize, BackendSelector aSelector,
+ TextureFlags aTextureFlags,
+ TextureAllocationFlags aAllocFlags = ALLOC_DEFAULT);
+
+ already_AddRefed<TextureClient> CreateTextureClientFromSurface(
+ gfx::SourceSurface* aSurface, BackendSelector aSelector,
+ TextureFlags aTextureFlags,
+ TextureAllocationFlags aAllocFlags = ALLOC_DEFAULT);
+
+ /**
+ * Establishes the connection with compositor side through IPDL
+ */
+ virtual bool Connect(ImageContainer* aImageContainer = nullptr);
+
+ void Destroy();
+
+ bool IsConnected() const;
+
+ CompositableForwarder* GetForwarder() const { return mForwarder; }
+
+ /**
+ * This identifier is what lets us attach async compositables with a shadow
+ * layer. It is not used if the compositable is used with the regular shadow
+ * layer forwarder.
+ *
+ * If this returns empty, it means the compositable is not async (it is used
+ * on the main thread).
+ */
+ CompositableHandle GetAsyncHandle() const;
+
+ /**
+ * Handle for IPDL communication.
+ */
+ CompositableHandle GetIPCHandle() const { return mHandle; }
+
+ /**
+ * Tells the Compositor to create a TextureHost for this TextureClient.
+ */
+ virtual bool AddTextureClient(TextureClient* aClient);
+
+ /**
+ * A hook for the when the Compositable is detached from it's layer.
+ */
+ virtual void OnDetach() {}
+
+ /**
+ * Clear any resources that are not immediately necessary. This may be called
+ * in low-memory conditions.
+ */
+ virtual void ClearCachedResources();
+
+ /**
+ * Shrink memory usage.
+ * Called when "memory-pressure" is observed.
+ */
+ virtual void HandleMemoryPressure();
+
+ /**
+ * Should be called when deataching a TextureClient from a Compositable,
+ * because some platforms need to do some extra book keeping when this
+ * happens.
+ *
+ * See AutoRemoveTexture to automatically invoke this at the end of a scope.
+ */
+ virtual void RemoveTexture(TextureClient* aTexture);
+
+ void InitIPDL(const CompositableHandle& aHandle);
+
+ TextureFlags GetTextureFlags() const { return mTextureFlags; }
+
+ TextureClientRecycleAllocator* GetTextureClientRecycler();
+
+ bool HasTextureClientRecycler() {
+ auto lock = mTextureClientRecycler.Lock();
+ return !!(*lock);
+ }
+
+ static void DumpTextureClient(std::stringstream& aStream,
+ TextureClient* aTexture,
+ TextureDumpMode aCompress);
+
+ protected:
+ RefPtr<CompositableForwarder> mForwarder;
+ // Some layers may want to enforce some flags to all their textures
+ // (like disallowing tiling)
+ Atomic<TextureFlags> mTextureFlags;
+ DataMutex<RefPtr<TextureClientRecycleAllocator>> mTextureClientRecycler;
+
+ // Only ever accessed via mTextureClientRecycler's Lock
+ CompositableHandle mHandle;
+ bool mIsAsync;
+
+ friend class CompositableChild;
+};
+
+/**
+ * Helper to call RemoveTexture at the end of a scope.
+ */
+struct AutoRemoveTexture {
+ explicit AutoRemoveTexture(CompositableClient* aCompositable,
+ TextureClient* aTexture = nullptr)
+ : mTexture(aTexture), mCompositable(aCompositable) {}
+
+ ~AutoRemoveTexture();
+
+ RefPtr<TextureClient> mTexture;
+
+ private:
+ CompositableClient* mCompositable;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif
diff --git a/gfx/layers/client/GPUVideoTextureClient.cpp b/gfx/layers/client/GPUVideoTextureClient.cpp
new file mode 100644
index 0000000000..f946197bfe
--- /dev/null
+++ b/gfx/layers/client/GPUVideoTextureClient.cpp
@@ -0,0 +1,57 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "GPUVideoTextureClient.h"
+#include "GPUVideoImage.h"
+#include "mozilla/gfx/2D.h"
+
+namespace mozilla {
+namespace layers {
+
+using namespace gfx;
+
+GPUVideoTextureData::GPUVideoTextureData(IGPUVideoSurfaceManager* aManager,
+ const SurfaceDescriptorGPUVideo& aSD,
+ const gfx::IntSize& aSize)
+ : mManager(aManager), mSD(aSD), mSize(aSize) {}
+
+GPUVideoTextureData::~GPUVideoTextureData() = default;
+
+bool GPUVideoTextureData::Serialize(SurfaceDescriptor& aOutDescriptor) {
+ aOutDescriptor = mSD;
+ return true;
+}
+
+void GPUVideoTextureData::FillInfo(TextureData::Info& aInfo) const {
+ aInfo.size = mSize;
+ // TODO: We should probably try do better for this.
+ // layers::Image doesn't expose a format, so it's hard
+ // to figure out in VideoDecoderParent.
+ aInfo.format = SurfaceFormat::B8G8R8X8;
+ aInfo.hasSynchronization = false;
+ aInfo.supportsMoz2D = false;
+ aInfo.canExposeMappedData = false;
+}
+
+already_AddRefed<SourceSurface> GPUVideoTextureData::GetAsSourceSurface() {
+ return mManager->Readback(mSD);
+}
+
+void GPUVideoTextureData::Deallocate(LayersIPCChannel* aAllocator) {
+ mManager->DeallocateSurfaceDescriptor(mSD);
+ mSD = SurfaceDescriptorGPUVideo();
+}
+
+void GPUVideoTextureData::Forget(LayersIPCChannel* aAllocator) {
+ // We always need to manually deallocate on the client side.
+ // Ideally we'd set up our TextureClient with the DEALLOCATE_CLIENT
+ // flag, but that forces texture destruction to be synchronous.
+ // Instead let's just deallocate from here as well.
+ Deallocate(aAllocator);
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/client/GPUVideoTextureClient.h b/gfx/layers/client/GPUVideoTextureClient.h
new file mode 100644
index 0000000000..93b2e761ca
--- /dev/null
+++ b/gfx/layers/client/GPUVideoTextureClient.h
@@ -0,0 +1,55 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_GFX_GPUVIDEOTEXTURECLIENT_H
+#define MOZILLA_GFX_GPUVIDEOTEXTURECLIENT_H
+
+#include "mozilla/layers/TextureClient.h"
+
+namespace mozilla {
+namespace gfx {
+class SourceSurface;
+}
+
+namespace layers {
+class IGPUVideoSurfaceManager;
+
+class GPUVideoTextureData : public TextureData {
+ public:
+ GPUVideoTextureData(IGPUVideoSurfaceManager* aManager,
+ const SurfaceDescriptorGPUVideo& aSD,
+ const gfx::IntSize& aSize);
+ virtual ~GPUVideoTextureData();
+
+ void FillInfo(TextureData::Info& aInfo) const override;
+
+ bool Lock(OpenMode) override { return true; };
+
+ void Unlock() override{};
+
+ bool Serialize(SurfaceDescriptor& aOutDescriptor) override;
+
+ void Deallocate(LayersIPCChannel* aAllocator) override;
+
+ void Forget(LayersIPCChannel* aAllocator) override;
+
+ already_AddRefed<gfx::SourceSurface> GetAsSourceSurface();
+
+ GPUVideoTextureData* AsGPUVideoTextureData() override { return this; }
+
+ protected:
+ RefPtr<IGPUVideoSurfaceManager> mManager;
+ SurfaceDescriptorGPUVideo mSD;
+ gfx::IntSize mSize;
+
+ public:
+ const decltype(mSD)& SD() const { return mSD; }
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // MOZILLA_GFX_GPUVIDEOTEXTURECLIENT_H
diff --git a/gfx/layers/client/ImageClient.cpp b/gfx/layers/client/ImageClient.cpp
new file mode 100644
index 0000000000..fb89d69f6b
--- /dev/null
+++ b/gfx/layers/client/ImageClient.cpp
@@ -0,0 +1,277 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ImageClient.h"
+
+#include <stdint.h> // for uint32_t
+
+#include "ImageContainer.h" // for Image, PlanarYCbCrImage, etc
+#include "ImageTypes.h" // for ImageFormat::PLANAR_YCBCR, etc
+#include "GLImages.h" // for SurfaceTextureImage::Data, etc
+#include "gfx2DGlue.h" // for ImageFormatToSurfaceFormat
+#include "gfxPlatform.h" // for gfxPlatform
+#include "mozilla/Assertions.h" // for MOZ_ASSERT, etc
+#include "mozilla/RefPtr.h" // for RefPtr, already_AddRefed
+#include "mozilla/gfx/2D.h"
+#include "mozilla/gfx/BaseSize.h" // for BaseSize
+#include "mozilla/gfx/Point.h" // for IntSize
+#include "mozilla/gfx/Types.h" // for SurfaceFormat, etc
+#include "mozilla/layers/CompositableClient.h" // for CompositableClient
+#include "mozilla/layers/CompositableForwarder.h"
+#include "mozilla/layers/CompositorTypes.h" // for CompositableType, etc
+#include "mozilla/layers/ISurfaceAllocator.h"
+#include "mozilla/layers/LayersSurfaces.h" // for SurfaceDescriptor, etc
+#include "mozilla/layers/TextureForwarder.h"
+#include "mozilla/layers/TextureClient.h" // for TextureClient, etc
+#include "mozilla/layers/TextureClientOGL.h" // for SurfaceTextureClient
+#include "mozilla/mozalloc.h" // for operator delete, etc
+#include "nsCOMPtr.h" // for already_AddRefed
+#include "nsDebug.h" // for NS_WARNING, NS_ASSERTION
+#include "nsISupportsImpl.h" // for Image::Release, etc
+#include "nsRect.h" // for mozilla::gfx::IntRect
+
+namespace mozilla {
+namespace layers {
+
+using namespace mozilla::gfx;
+
+/* static */
+already_AddRefed<ImageClient> ImageClient::CreateImageClient(
+ CompositableType aCompositableHostType, CompositableForwarder* aForwarder,
+ TextureFlags aFlags) {
+ RefPtr<ImageClient> result = nullptr;
+ switch (aCompositableHostType) {
+ case CompositableType::IMAGE:
+ result =
+ new ImageClientSingle(aForwarder, aFlags, CompositableType::IMAGE);
+ break;
+ case CompositableType::UNKNOWN:
+ result = nullptr;
+ break;
+ default:
+ MOZ_CRASH("GFX: unhandled program type image");
+ }
+
+ NS_ASSERTION(result, "Failed to create ImageClient");
+
+ return result.forget();
+}
+
+void ImageClient::RemoveTexture(TextureClient* aTexture) {
+ GetForwarder()->RemoveTextureFromCompositable(this, aTexture);
+}
+
+ImageClientSingle::ImageClientSingle(CompositableForwarder* aFwd,
+ TextureFlags aFlags,
+ CompositableType aType)
+ : ImageClient(aFwd, aFlags, aType) {}
+
+TextureInfo ImageClientSingle::GetTextureInfo() const {
+ return TextureInfo(CompositableType::IMAGE);
+}
+
+void ImageClientSingle::FlushAllImages() {
+ for (auto& b : mBuffers) {
+ // It should be safe to just assume a default render root here, even if
+ // the texture actually presents in a content render root, as the only
+ // risk would be if the content render root has not / is not going to
+ // generate a frame before the texture gets cleared.
+ RemoveTexture(b.mTextureClient);
+ }
+ mBuffers.Clear();
+}
+
+/* static */
+already_AddRefed<TextureClient> ImageClient::CreateTextureClientForImage(
+ Image* aImage, KnowsCompositor* aKnowsCompositor) {
+ RefPtr<TextureClient> texture;
+ if (aImage->GetFormat() == ImageFormat::PLANAR_YCBCR) {
+ PlanarYCbCrImage* ycbcr = static_cast<PlanarYCbCrImage*>(aImage);
+ const PlanarYCbCrData* data = ycbcr->GetData();
+ if (!data) {
+ return nullptr;
+ }
+ texture = TextureClient::CreateForYCbCr(
+ aKnowsCompositor, data->mPictureRect, data->YDataSize(), data->mYStride,
+ data->CbCrDataSize(), data->mCbCrStride, data->mStereoMode,
+ data->mColorDepth, data->mYUVColorSpace, data->mColorRange,
+ data->mChromaSubsampling, TextureFlags::DEFAULT);
+ if (!texture) {
+ return nullptr;
+ }
+
+ TextureClientAutoLock autoLock(texture, OpenMode::OPEN_WRITE_ONLY);
+ if (!autoLock.Succeeded()) {
+ return nullptr;
+ }
+
+ bool status = UpdateYCbCrTextureClient(texture, *data);
+ MOZ_ASSERT(status);
+ if (!status) {
+ return nullptr;
+ }
+#ifdef MOZ_WIDGET_ANDROID
+ } else if (aImage->GetFormat() == ImageFormat::SURFACE_TEXTURE) {
+ gfx::IntSize size = aImage->GetSize();
+ SurfaceTextureImage* typedImage = aImage->AsSurfaceTextureImage();
+ texture = AndroidSurfaceTextureData::CreateTextureClient(
+ typedImage->GetHandle(), size, typedImage->GetContinuous(),
+ typedImage->GetOriginPos(), typedImage->GetHasAlpha(),
+ typedImage->GetTransformOverride(),
+ aKnowsCompositor->GetTextureForwarder(), TextureFlags::DEFAULT);
+#endif
+ } else {
+ RefPtr<gfx::SourceSurface> surface = aImage->GetAsSourceSurface();
+ MOZ_ASSERT(surface);
+ texture = TextureClient::CreateForDrawing(
+ aKnowsCompositor, surface->GetFormat(), aImage->GetSize(),
+ BackendSelector::Content, TextureFlags::DEFAULT);
+ if (!texture) {
+ return nullptr;
+ }
+
+ MOZ_ASSERT(texture->CanExposeDrawTarget());
+
+ if (!texture->Lock(OpenMode::OPEN_WRITE_ONLY)) {
+ return nullptr;
+ }
+
+ {
+ // We must not keep a reference to the DrawTarget after it has been
+ // unlocked.
+ DrawTarget* dt = texture->BorrowDrawTarget();
+ if (!dt) {
+ gfxWarning()
+ << "ImageClientSingle::UpdateImage failed in BorrowDrawTarget";
+ return nullptr;
+ }
+ MOZ_ASSERT(surface.get());
+ dt->CopySurface(surface, IntRect(IntPoint(), surface->GetSize()),
+ IntPoint());
+ }
+
+ texture->Unlock();
+ }
+ return texture.forget();
+}
+
+bool ImageClientSingle::UpdateImage(ImageContainer* aContainer) {
+ AutoTArray<ImageContainer::OwningImage, 4> images;
+ uint32_t generationCounter;
+ aContainer->GetCurrentImages(&images, &generationCounter);
+
+ if (mLastUpdateGenerationCounter == generationCounter) {
+ return true;
+ }
+ mLastUpdateGenerationCounter = generationCounter;
+
+ // Don't try to update to invalid images.
+ images.RemoveElementsBy(
+ [](const auto& image) { return !image.mImage->IsValid(); });
+ if (images.IsEmpty()) {
+ // This can happen if a ClearAllImages raced with SetCurrentImages from
+ // another thread and ClearImagesFromImageBridge ran after the
+ // SetCurrentImages call but before UpdateImageClientNow.
+ // This can also happen if all images in the list are invalid.
+ // We return true because the caller would attempt to recreate the
+ // ImageClient otherwise, and that isn't going to help.
+ for (auto& b : mBuffers) {
+ RemoveTexture(b.mTextureClient);
+ }
+ mBuffers.Clear();
+ return true;
+ }
+
+ nsTArray<Buffer> newBuffers;
+ AutoTArray<CompositableForwarder::TimedTextureClient, 4> textures;
+
+ for (auto& img : images) {
+ Image* image = img.mImage;
+
+ RefPtr<TextureClient> texture = image->GetTextureClient(GetForwarder());
+ const bool hasTextureClient = !!texture;
+
+ for (int32_t i = mBuffers.Length() - 1; i >= 0; --i) {
+ if (mBuffers[i].mImageSerial == image->GetSerial()) {
+ if (hasTextureClient) {
+ MOZ_ASSERT(image->GetTextureClient(GetForwarder()) ==
+ mBuffers[i].mTextureClient);
+ } else {
+ texture = mBuffers[i].mTextureClient;
+ }
+ // Remove this element from mBuffers so mBuffers only contains
+ // images that aren't present in 'images'
+ mBuffers.RemoveElementAt(i);
+ }
+ }
+
+ if (!texture) {
+ // Slow path, we should not be hitting it very often and if we do it means
+ // we are using an Image class that is not backed by textureClient and we
+ // should fix it.
+ texture = CreateTextureClientForImage(image, GetForwarder());
+ }
+
+ if (!texture) {
+ return false;
+ }
+
+ // We check if the texture's allocator is still open, since in between media
+ // decoding a frame and adding it to the compositable, we could have
+ // restarted the GPU process.
+ if (!texture->GetAllocator()->IPCOpen()) {
+ continue;
+ }
+ if (!AddTextureClient(texture)) {
+ return false;
+ }
+
+ CompositableForwarder::TimedTextureClient* t = textures.AppendElement();
+ t->mTextureClient = texture;
+ t->mTimeStamp = img.mTimeStamp;
+ t->mPictureRect = image->GetPictureRect();
+ t->mFrameID = img.mFrameID;
+ t->mProducerID = img.mProducerID;
+
+ Buffer* newBuf = newBuffers.AppendElement();
+ newBuf->mImageSerial = image->GetSerial();
+ newBuf->mTextureClient = texture;
+
+ texture->SyncWithObject(GetForwarder()->GetSyncObject());
+ }
+
+ GetForwarder()->UseTextures(this, textures);
+
+ for (auto& b : mBuffers) {
+ RemoveTexture(b.mTextureClient);
+ }
+ mBuffers = std::move(newBuffers);
+
+ return true;
+}
+
+RefPtr<TextureClient> ImageClientSingle::GetForwardedTexture() {
+ if (mBuffers.Length() == 0) {
+ return nullptr;
+ }
+ return mBuffers[0].mTextureClient;
+}
+
+bool ImageClientSingle::AddTextureClient(TextureClient* aTexture) {
+ MOZ_ASSERT((mTextureFlags & aTexture->GetFlags()) == mTextureFlags);
+ return CompositableClient::AddTextureClient(aTexture);
+}
+
+void ImageClientSingle::OnDetach() { mBuffers.Clear(); }
+
+ImageClient::ImageClient(CompositableForwarder* aFwd, TextureFlags aFlags,
+ CompositableType aType)
+ : CompositableClient(aFwd, aFlags),
+ mType(aType),
+ mLastUpdateGenerationCounter(0) {}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/client/ImageClient.h b/gfx/layers/client/ImageClient.h
new file mode 100644
index 0000000000..899e40adff
--- /dev/null
+++ b/gfx/layers/client/ImageClient.h
@@ -0,0 +1,118 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_GFX_IMAGECLIENT_H
+#define MOZILLA_GFX_IMAGECLIENT_H
+
+#include <stdint.h> // for uint32_t, uint64_t
+#include <sys/types.h> // for int32_t
+#include "mozilla/Attributes.h" // for override
+#include "mozilla/RefPtr.h" // for RefPtr, already_AddRefed
+#include "mozilla/gfx/Types.h" // for SurfaceFormat
+#include "mozilla/layers/CompositableClient.h" // for CompositableClient
+#include "mozilla/layers/CompositorTypes.h" // for CompositableType, etc
+#include "mozilla/layers/LayersSurfaces.h" // for SurfaceDescriptor
+#include "mozilla/layers/TextureClient.h" // for TextureClient, etc
+#include "mozilla/mozalloc.h" // for operator delete
+#include "nsCOMPtr.h" // for already_AddRefed
+#include "nsRect.h" // for mozilla::gfx::IntRect
+
+namespace mozilla {
+namespace layers {
+
+class CompositableForwarder;
+class Image;
+class ImageContainer;
+class ImageClientSingle;
+
+/**
+ * Image clients are used by basic image layers on the content thread, they
+ * always match with an ImageHost on the compositor thread. See
+ * CompositableClient.h for information on connecting clients to hosts.
+ */
+class ImageClient : public CompositableClient {
+ public:
+ /**
+ * Creates, configures, and returns a new image client. If necessary, a
+ * message will be sent to the compositor to create a corresponding image
+ * host.
+ */
+ static already_AddRefed<ImageClient> CreateImageClient(
+ CompositableType aImageHostType, CompositableForwarder* aFwd,
+ TextureFlags aFlags);
+
+ virtual ~ImageClient() = default;
+
+ /**
+ * Update this ImageClient from aContainer in aLayer
+ * returns false if this is the wrong kind of ImageClient for aContainer.
+ * Note that returning true does not necessarily imply success
+ */
+ virtual bool UpdateImage(ImageContainer* aContainer) = 0;
+
+ /**
+ * asynchronously remove all the textures used by the image client.
+ *
+ */
+ virtual void FlushAllImages() {}
+
+ virtual void RemoveTexture(TextureClient* aTexture) override;
+
+ virtual ImageClientSingle* AsImageClientSingle() { return nullptr; }
+
+ static already_AddRefed<TextureClient> CreateTextureClientForImage(
+ Image* aImage, KnowsCompositor* aForwarder);
+
+ uint32_t GetLastUpdateGenerationCounter() {
+ return mLastUpdateGenerationCounter;
+ }
+
+ virtual RefPtr<TextureClient> GetForwardedTexture() { return nullptr; }
+
+ protected:
+ ImageClient(CompositableForwarder* aFwd, TextureFlags aFlags,
+ CompositableType aType);
+
+ CompositableType mType;
+ uint32_t mLastUpdateGenerationCounter;
+};
+
+/**
+ * An image client which uses a single texture client.
+ */
+class ImageClientSingle : public ImageClient {
+ public:
+ ImageClientSingle(CompositableForwarder* aFwd, TextureFlags aFlags,
+ CompositableType aType);
+
+ bool UpdateImage(ImageContainer* aContainer) override;
+
+ void OnDetach() override;
+
+ bool AddTextureClient(TextureClient* aTexture) override;
+
+ TextureInfo GetTextureInfo() const override;
+
+ void FlushAllImages() override;
+
+ ImageClientSingle* AsImageClientSingle() override { return this; }
+
+ RefPtr<TextureClient> GetForwardedTexture() override;
+
+ bool IsEmpty() { return mBuffers.IsEmpty(); }
+
+ protected:
+ struct Buffer {
+ RefPtr<TextureClient> mTextureClient;
+ int32_t mImageSerial;
+ };
+ nsTArray<Buffer> mBuffers;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif
diff --git a/gfx/layers/client/TextureClient.cpp b/gfx/layers/client/TextureClient.cpp
new file mode 100644
index 0000000000..252c6f4d6a
--- /dev/null
+++ b/gfx/layers/client/TextureClient.cpp
@@ -0,0 +1,1794 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/layers/TextureClient.h"
+
+#include <stdint.h> // for uint8_t, uint32_t, etc
+
+#include "BufferTexture.h"
+#include "IPDLActor.h"
+#include "ImageContainer.h" // for PlanarYCbCrData, etc
+#include "MainThreadUtils.h"
+#include "gfx2DGlue.h"
+#include "gfxPlatform.h" // for gfxPlatform
+#include "gfxUtils.h" // for gfxUtils::GetAsLZ4Base64Str
+#include "mozilla/Atomics.h"
+#include "mozilla/Mutex.h"
+#include "mozilla/ProfilerLabels.h"
+#include "mozilla/SchedulerGroup.h"
+#include "mozilla/StaticPrefs_gfx.h"
+#include "mozilla/StaticPrefs_layers.h"
+#include "mozilla/gfx/2D.h"
+#include "mozilla/gfx/DataSurfaceHelpers.h" // for CreateDataSourceSurfaceByCloning
+#include "mozilla/gfx/Logging.h" // for gfxDebug
+#include "mozilla/gfx/gfxVars.h"
+#include "mozilla/ipc/CrossProcessSemaphore.h"
+#include "mozilla/ipc/SharedMemory.h" // for SharedMemory, etc
+#include "mozilla/layers/CompositableForwarder.h"
+#include "mozilla/layers/ISurfaceAllocator.h"
+#include "mozilla/layers/ImageBridgeChild.h"
+#include "mozilla/layers/ImageDataSerializer.h"
+#include "mozilla/layers/PTextureChild.h"
+#include "mozilla/layers/TextureClientOGL.h"
+#include "mozilla/layers/TextureClientRecycleAllocator.h"
+#include "mozilla/layers/TextureRecorded.h"
+#include "nsDebug.h" // for NS_ASSERTION, NS_WARNING, etc
+#include "nsISerialEventTarget.h"
+#include "nsISupportsImpl.h" // for MOZ_COUNT_CTOR, etc
+#include "nsPrintfCString.h" // for nsPrintfCString
+
+#ifdef XP_WIN
+# include "gfx2DGlue.h"
+# include "gfxWindowsPlatform.h"
+# include "mozilla/gfx/DeviceManagerDx.h"
+# include "mozilla/layers/TextureD3D11.h"
+#endif
+#ifdef MOZ_WAYLAND
+# include <gtk/gtkx.h>
+
+# include "gfxPlatformGtk.h"
+# include "mozilla/layers/DMABUFTextureClientOGL.h"
+# include "mozilla/widget/nsWaylandDisplay.h"
+#endif
+
+#ifdef XP_MACOSX
+# include "mozilla/layers/MacIOSurfaceTextureClientOGL.h"
+#endif
+
+#if 0
+# define RECYCLE_LOG(...) printf_stderr(__VA_ARGS__)
+#else
+# define RECYCLE_LOG(...) \
+ do { \
+ } while (0)
+#endif
+
+namespace mozilla::layers {
+
+using namespace mozilla::ipc;
+using namespace mozilla::gl;
+using namespace mozilla::gfx;
+
+struct TextureDeallocParams {
+ TextureData* data;
+ RefPtr<TextureChild> actor;
+ RefPtr<LayersIPCChannel> allocator;
+ bool clientDeallocation;
+ bool syncDeallocation;
+};
+
+void DeallocateTextureClient(TextureDeallocParams params);
+
+/**
+ * TextureChild is the content-side incarnation of the PTexture IPDL actor.
+ *
+ * TextureChild is used to synchronize a texture client and its corresponding
+ * TextureHost if needed (a TextureClient that is not shared with the compositor
+ * does not have a TextureChild)
+ *
+ * During the deallocation phase, a TextureChild may hold its recently destroyed
+ * TextureClient's data until the compositor side confirmed that it is safe to
+ * deallocte or recycle the it.
+ */
+class TextureChild final : PTextureChild {
+ ~TextureChild() {
+ // We should have deallocated mTextureData in ActorDestroy
+ MOZ_ASSERT(!mTextureData);
+ MOZ_ASSERT_IF(!mOwnerCalledDestroy, !mTextureClient);
+ }
+
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(TextureChild)
+
+ TextureChild()
+ : mCompositableForwarder(nullptr),
+ mTextureForwarder(nullptr),
+ mTextureClient(nullptr),
+ mTextureData(nullptr),
+ mDestroyed(false),
+ mIPCOpen(false),
+ mOwnsTextureData(false),
+ mOwnerCalledDestroy(false),
+ mUsesImageBridge(false) {}
+
+ mozilla::ipc::IPCResult Recv__delete__() override { return IPC_OK(); }
+
+ LayersIPCChannel* GetAllocator() { return mTextureForwarder; }
+
+ void ActorDestroy(ActorDestroyReason why) override;
+
+ bool IPCOpen() const { return mIPCOpen; }
+
+ void Lock() const {
+ if (mUsesImageBridge) {
+ mLock.Enter();
+ }
+ }
+
+ void Unlock() const {
+ if (mUsesImageBridge) {
+ mLock.Leave();
+ }
+ }
+
+ private:
+ // AddIPDLReference and ReleaseIPDLReference are only to be called by
+ // CreateIPDLActor and DestroyIPDLActor, respectively. We intentionally make
+ // them private to prevent misuse. The purpose of these methods is to be aware
+ // of when the IPC system around this actor goes down: mIPCOpen is then set to
+ // false.
+ void AddIPDLReference() {
+ MOZ_ASSERT(mIPCOpen == false);
+ mIPCOpen = true;
+ AddRef();
+ }
+ void ReleaseIPDLReference() {
+ MOZ_ASSERT(mIPCOpen == false);
+ Release();
+ }
+
+ /// The normal way to destroy the actor.
+ ///
+ /// This will asynchronously send a Destroy message to the parent actor, whom
+ /// will send the delete message.
+ void Destroy(const TextureDeallocParams& aParams);
+
+ // This lock is used order to prevent several threads to access the
+ // TextureClient's data concurrently. In particular, it prevents shutdown
+ // code to destroy a texture while another thread is reading or writing into
+ // it.
+ // In most places, the lock is held in short and bounded scopes in which we
+ // don't block on any other resource. There are few exceptions to this, which
+ // are discussed below.
+ //
+ // The locking pattern of TextureClient may in some case upset deadlock
+ // detection tools such as TSan. Typically our tile rendering code will lock
+ // all of its tiles, render into them and unlock them all right after that,
+ // which looks something like:
+ //
+ // Lock tile A
+ // Lock tile B
+ // Lock tile C
+ // Apply drawing commands to tiles A, B and C
+ // Unlock tile A
+ // Unlock tile B
+ // Unlock tile C
+ //
+ // And later, we may end up rendering a tile buffer that has the same tiles,
+ // in a different order, for example:
+ //
+ // Lock tile B
+ // Lock tile A
+ // Lock tile D
+ // Apply drawing commands to tiles A, B and D
+ // Unlock tile B
+ // Unlock tile A
+ // Unlock tile D
+ //
+ // This is because textures being expensive to create, we recycle them as much
+ // as possible and they may reappear in the tile buffer in a different order.
+ //
+ // Unfortunately this is not very friendly to TSan's analysis, which will see
+ // that B was once locked while A was locked, and then A locked while B was
+ // locked. TSan identifies this as a potential dead-lock which would be the
+ // case if this kind of inconsistent and dependent locking order was happening
+ // concurrently.
+ // In the case of TextureClient, dependent locking only ever happens on the
+ // thread that draws into the texture (let's call it the producer thread).
+ // Other threads may call into a method that can lock the texture in a short
+ // and bounded scope inside of which it is not allowed to do anything that
+ // could cause the thread to block. A given texture can only have one producer
+ // thread.
+ //
+ // Another example of TSan-unfriendly locking pattern is when copying a
+ // texture into another, which also never happens outside of the producer
+ // thread. Copying A into B looks like this:
+ //
+ // Lock texture B
+ // Lock texture A
+ // Copy A into B
+ // Unlock A
+ // Unlock B
+ //
+ // In a given frame we may need to copy A into B and in another frame copy
+ // B into A. For example A and B can be the Front and Back buffers,
+ // alternating roles and the copy is needed to avoid the cost of re-drawing
+ // the valid region.
+ //
+ // The important rule is that all of the dependent locking must occur only
+ // in the texture's producer thread to avoid deadlocks.
+ mutable gfx::CriticalSection mLock;
+
+ RefPtr<CompositableForwarder> mCompositableForwarder;
+ RefPtr<TextureForwarder> mTextureForwarder;
+
+ TextureClient* mTextureClient;
+ TextureData* mTextureData;
+ Atomic<bool> mDestroyed;
+ bool mIPCOpen;
+ bool mOwnsTextureData;
+ bool mOwnerCalledDestroy;
+ bool mUsesImageBridge;
+
+ friend class TextureClient;
+ friend void DeallocateTextureClient(TextureDeallocParams params);
+};
+
+static inline gfx::BackendType BackendTypeForBackendSelector(
+ LayersBackend aLayersBackend, BackendSelector aSelector) {
+ switch (aSelector) {
+ case BackendSelector::Canvas:
+ return gfxPlatform::GetPlatform()->GetPreferredCanvasBackend();
+ case BackendSelector::Content:
+ return gfxPlatform::GetPlatform()->GetContentBackendFor(aLayersBackend);
+ default:
+ MOZ_ASSERT_UNREACHABLE("Unknown backend selector");
+ return gfx::BackendType::NONE;
+ }
+};
+
+static TextureType GetTextureType(gfx::SurfaceFormat aFormat,
+ gfx::IntSize aSize,
+ KnowsCompositor* aKnowsCompositor,
+ BackendSelector aSelector,
+ TextureAllocationFlags aAllocFlags) {
+ LayersBackend layersBackend = aKnowsCompositor->GetCompositorBackendType();
+ gfx::BackendType moz2DBackend =
+ BackendTypeForBackendSelector(layersBackend, aSelector);
+ Unused << moz2DBackend;
+
+#ifdef XP_WIN
+ int32_t maxTextureSize = aKnowsCompositor->GetMaxTextureSize();
+ if ((layersBackend == LayersBackend::LAYERS_WR &&
+ !aKnowsCompositor->UsingSoftwareWebRender()) &&
+ (moz2DBackend == gfx::BackendType::DIRECT2D ||
+ moz2DBackend == gfx::BackendType::DIRECT2D1_1) &&
+ aSize.width <= maxTextureSize && aSize.height <= maxTextureSize &&
+ !(aAllocFlags & (ALLOC_UPDATE_FROM_SURFACE | ALLOC_DO_NOT_ACCELERATE))) {
+ return TextureType::D3D11;
+ }
+#endif
+
+#ifdef MOZ_WAYLAND
+ if ((layersBackend == LayersBackend::LAYERS_WR &&
+ !aKnowsCompositor->UsingSoftwareWebRender()) &&
+ widget::DMABufDevice::IsDMABufTexturesEnabled() &&
+ aFormat != SurfaceFormat::A8) {
+ return TextureType::DMABUF;
+ }
+#endif
+
+#ifdef XP_MACOSX
+ if (StaticPrefs::gfx_use_iosurface_textures_AtStartup()) {
+ return TextureType::MacIOSurface;
+ }
+#endif
+
+#ifdef MOZ_WIDGET_ANDROID
+ if (StaticPrefs::gfx_use_surfacetexture_textures_AtStartup()) {
+ return TextureType::AndroidNativeWindow;
+ }
+#endif
+
+ return TextureType::Unknown;
+}
+
+TextureType PreferredCanvasTextureType(KnowsCompositor* aKnowsCompositor) {
+ return GetTextureType(gfx::SurfaceFormat::R8G8B8A8, {1, 1}, aKnowsCompositor,
+ BackendSelector::Canvas,
+ TextureAllocationFlags::ALLOC_DEFAULT);
+}
+
+static bool ShouldRemoteTextureType(TextureType aTextureType,
+ BackendSelector aSelector) {
+ if (aSelector != BackendSelector::Canvas || !gfxPlatform::UseRemoteCanvas()) {
+ return false;
+ }
+
+ switch (aTextureType) {
+ case TextureType::D3D11:
+ return true;
+ default:
+ return false;
+ }
+}
+
+/* static */
+TextureData* TextureData::Create(TextureForwarder* aAllocator,
+ gfx::SurfaceFormat aFormat, gfx::IntSize aSize,
+ KnowsCompositor* aKnowsCompositor,
+ BackendSelector aSelector,
+ TextureFlags aTextureFlags,
+ TextureAllocationFlags aAllocFlags) {
+ TextureType textureType =
+ GetTextureType(aFormat, aSize, aKnowsCompositor, aSelector, aAllocFlags);
+
+ if (ShouldRemoteTextureType(textureType, aSelector)) {
+ RefPtr<CanvasChild> canvasChild = aAllocator->GetCanvasChild();
+ if (canvasChild) {
+ return new RecordedTextureData(canvasChild.forget(), aSize, aFormat,
+ textureType);
+ }
+
+ // We don't have a CanvasChild, but are supposed to be remote.
+ // Fall back to software.
+ textureType = TextureType::Unknown;
+ }
+
+#if defined(XP_MACOSX) || defined(MOZ_WAYLAND)
+ gfx::BackendType moz2DBackend = BackendTypeForBackendSelector(
+ aKnowsCompositor->GetCompositorBackendType(), aSelector);
+#endif
+
+ switch (textureType) {
+#ifdef XP_WIN
+ case TextureType::D3D11:
+ return D3D11TextureData::Create(aSize, aFormat, aAllocFlags);
+#endif
+
+#ifdef MOZ_WAYLAND
+ case TextureType::DMABUF:
+ return DMABUFTextureData::Create(aSize, aFormat, moz2DBackend);
+#endif
+
+#ifdef XP_MACOSX
+ case TextureType::MacIOSurface:
+ return MacIOSurfaceTextureData::Create(aSize, aFormat, moz2DBackend);
+#endif
+#ifdef MOZ_WIDGET_ANDROID
+ case TextureType::AndroidNativeWindow:
+ return AndroidNativeWindowTextureData::Create(aSize, aFormat);
+#endif
+ default:
+ return nullptr;
+ }
+}
+
+/* static */
+bool TextureData::IsRemote(KnowsCompositor* aKnowsCompositor,
+ BackendSelector aSelector) {
+ TextureType textureType = GetTextureType(
+ gfx::SurfaceFormat::UNKNOWN, gfx::IntSize(1, 1), aKnowsCompositor,
+ aSelector, TextureAllocationFlags::ALLOC_DEFAULT);
+
+ return ShouldRemoteTextureType(textureType, aSelector);
+}
+
+static void DestroyTextureData(TextureData* aTextureData,
+ LayersIPCChannel* aAllocator, bool aDeallocate) {
+ if (!aTextureData) {
+ return;
+ }
+
+ if (aDeallocate) {
+ aTextureData->Deallocate(aAllocator);
+ } else {
+ aTextureData->Forget(aAllocator);
+ }
+ delete aTextureData;
+}
+
+void TextureChild::ActorDestroy(ActorDestroyReason why) {
+ AUTO_PROFILER_LABEL("TextureChild::ActorDestroy", GRAPHICS);
+ MOZ_ASSERT(mIPCOpen);
+ mIPCOpen = false;
+
+ if (mTextureData) {
+ DestroyTextureData(mTextureData, GetAllocator(), mOwnsTextureData);
+ mTextureData = nullptr;
+ }
+}
+
+void TextureChild::Destroy(const TextureDeallocParams& aParams) {
+ MOZ_ASSERT(!mOwnerCalledDestroy);
+ if (mOwnerCalledDestroy) {
+ return;
+ }
+
+ mOwnerCalledDestroy = true;
+
+ if (!IPCOpen()) {
+ DestroyTextureData(aParams.data, aParams.allocator,
+ aParams.clientDeallocation);
+ return;
+ }
+
+ // DestroyTextureData will be called by TextureChild::ActorDestroy
+ mTextureData = aParams.data;
+ mOwnsTextureData = aParams.clientDeallocation;
+
+ if (!mCompositableForwarder ||
+ !mCompositableForwarder->DestroyInTransaction(this)) {
+ this->SendDestroy();
+ }
+}
+
+/* static */
+Atomic<uint64_t> TextureClient::sSerialCounter(0);
+
+static void DeallocateTextureClientSyncProxy(TextureDeallocParams params,
+ ReentrantMonitor* aBarrier,
+ bool* aDone) {
+ DeallocateTextureClient(params);
+ ReentrantMonitorAutoEnter autoMon(*aBarrier);
+ *aDone = true;
+ aBarrier->NotifyAll();
+}
+
+/// The logic for synchronizing a TextureClient's deallocation goes here.
+///
+/// This funciton takes care of dispatching work to the right thread using
+/// a synchronous proxy if needed, and handles client/host deallocation.
+void DeallocateTextureClient(TextureDeallocParams params) {
+ if (!params.actor && !params.data) {
+ // Nothing to do
+ return;
+ }
+
+ TextureChild* actor = params.actor;
+ nsCOMPtr<nsISerialEventTarget> ipdlThread;
+
+ if (params.allocator) {
+ ipdlThread = params.allocator->GetThread();
+ if (!ipdlThread) {
+ // An allocator with no thread means we are too late in the shutdown
+ // sequence.
+ gfxCriticalError() << "Texture deallocated too late during shutdown";
+ return;
+ }
+ }
+
+ // First make sure that the work is happening on the IPDL thread.
+ if (ipdlThread && !ipdlThread->IsOnCurrentThread()) {
+ if (params.syncDeallocation) {
+ bool done = false;
+ ReentrantMonitor barrier MOZ_UNANNOTATED("DeallocateTextureClient");
+ ReentrantMonitorAutoEnter autoMon(barrier);
+ ipdlThread->Dispatch(NewRunnableFunction(
+ "DeallocateTextureClientSyncProxyRunnable",
+ DeallocateTextureClientSyncProxy, params, &barrier, &done));
+ while (!done) {
+ barrier.Wait();
+ }
+ } else {
+ ipdlThread->Dispatch(NewRunnableFunction(
+ "DeallocateTextureClientRunnable", DeallocateTextureClient, params));
+ }
+ // The work has been forwarded to the IPDL thread, we are done.
+ return;
+ }
+
+ // Below this line, we are either in the IPDL thread or ther is no IPDL
+ // thread anymore.
+
+ if (!ipdlThread) {
+ // If we don't have a thread we can't know for sure that we are in
+ // the IPDL thread and use the LayersIPCChannel.
+ // This should ideally not happen outside of gtest, but some shutdown
+ // raciness could put us in this situation.
+ params.allocator = nullptr;
+ }
+
+ if (!actor) {
+ // We don't have an IPDL actor, probably because we destroyed the
+ // TextureClient before sharing it with the compositor. It means the data
+ // cannot be owned by the TextureHost since we never created the
+ // TextureHost...
+ DestroyTextureData(params.data, params.allocator, /* aDeallocate */ true);
+ return;
+ }
+
+ actor->Destroy(params);
+}
+
+void TextureClient::Destroy() {
+ // Async paints should have been flushed by now.
+ MOZ_RELEASE_ASSERT(mPaintThreadRefs == 0);
+
+ if (mActor && !mIsLocked) {
+ mActor->Lock();
+ }
+
+ mBorrowedDrawTarget = nullptr;
+ mReadLock = nullptr;
+
+ RefPtr<TextureChild> actor = mActor;
+ mActor = nullptr;
+
+ if (actor && !actor->mDestroyed.compareExchange(false, true)) {
+ actor->Unlock();
+ actor = nullptr;
+ }
+
+ TextureData* data = mData;
+ mData = nullptr;
+
+ if (data || actor) {
+ TextureDeallocParams params;
+ params.actor = actor;
+ params.allocator = mAllocator;
+ params.clientDeallocation = !!(mFlags & TextureFlags::DEALLOCATE_CLIENT);
+ params.data = data;
+ // At the moment we always deallocate synchronously when deallocating on the
+ // client side, but having asynchronous deallocate in some of the cases will
+ // be a worthwhile optimization.
+ params.syncDeallocation = !!(mFlags & TextureFlags::DEALLOCATE_CLIENT);
+
+ // Release the lock before calling DeallocateTextureClient because the
+ // latter may wait for the main thread which could create a dead-lock.
+
+ if (actor) {
+ actor->Unlock();
+ }
+
+ DeallocateTextureClient(params);
+ }
+}
+
+void TextureClient::LockActor() const {
+ if (mActor) {
+ mActor->Lock();
+ }
+}
+
+void TextureClient::UnlockActor() const {
+ if (mActor) {
+ mActor->Unlock();
+ }
+}
+
+bool TextureClient::IsReadLocked() const {
+ if (!mReadLock) {
+ return false;
+ }
+ MOZ_ASSERT(mReadLock->AsNonBlockingLock(),
+ "Can only check locked for non-blocking locks!");
+ return mReadLock->AsNonBlockingLock()->GetReadCount() > 1;
+}
+
+bool TextureClient::TryReadLock() {
+ if (!mReadLock || mIsReadLocked) {
+ return true;
+ }
+
+ if (mReadLock->AsNonBlockingLock()) {
+ if (IsReadLocked()) {
+ return false;
+ }
+ }
+
+ if (!mReadLock->TryReadLock(TimeDuration::FromMilliseconds(500))) {
+ return false;
+ }
+
+ mIsReadLocked = true;
+ return true;
+}
+
+void TextureClient::ReadUnlock() {
+ if (!mIsReadLocked) {
+ return;
+ }
+ MOZ_ASSERT(mReadLock);
+ mReadLock->ReadUnlock();
+ mIsReadLocked = false;
+}
+
+bool TextureClient::Lock(OpenMode aMode) {
+ MOZ_ASSERT(IsValid());
+ MOZ_ASSERT(!mIsLocked);
+ if (!IsValid()) {
+ return false;
+ }
+ if (mIsLocked) {
+ return mOpenMode == aMode;
+ }
+
+ if ((aMode & OpenMode::OPEN_WRITE || !mInfo.canConcurrentlyReadLock) &&
+ !TryReadLock()) {
+ // Only warn if attempting to write. Attempting to read is acceptable usage.
+ if (aMode & OpenMode::OPEN_WRITE) {
+ NS_WARNING(
+ "Attempt to Lock a texture that is being read by the compositor!");
+ }
+ return false;
+ }
+
+ LockActor();
+
+ mIsLocked = mData->Lock(aMode);
+ mOpenMode = aMode;
+
+ auto format = GetFormat();
+ if (mIsLocked && CanExposeDrawTarget() &&
+ (aMode & OpenMode::OPEN_READ_WRITE) == OpenMode::OPEN_READ_WRITE &&
+ NS_IsMainThread() &&
+ // the formats that we apparently expect, in the cairo backend. Any other
+ // format will trigger an assertion in GfxFormatToCairoFormat.
+ (format == SurfaceFormat::A8R8G8B8_UINT32 ||
+ format == SurfaceFormat::X8R8G8B8_UINT32 ||
+ format == SurfaceFormat::A8 || format == SurfaceFormat::R5G6B5_UINT16)) {
+ if (!BorrowDrawTarget()) {
+ // Failed to get a DrawTarget, means we won't be able to write into the
+ // texture, might as well fail now.
+ Unlock();
+ return false;
+ }
+ }
+
+ if (!mIsLocked) {
+ UnlockActor();
+ ReadUnlock();
+ }
+
+ return mIsLocked;
+}
+
+void TextureClient::Unlock() {
+ MOZ_ASSERT(IsValid());
+ MOZ_ASSERT(mIsLocked);
+ if (!IsValid() || !mIsLocked) {
+ return;
+ }
+
+ if (mBorrowedDrawTarget) {
+ if (!(mOpenMode & OpenMode::OPEN_ASYNC)) {
+ if (mOpenMode & OpenMode::OPEN_WRITE) {
+ mBorrowedDrawTarget->Flush();
+ }
+
+ mBorrowedDrawTarget->DetachAllSnapshots();
+ // If this assertion is hit, it means something is holding a strong
+ // reference to our DrawTarget externally, which is not allowed.
+ MOZ_ASSERT(mBorrowedDrawTarget->refCount() <= mExpectedDtRefs);
+ }
+
+ mBorrowedDrawTarget = nullptr;
+ }
+
+ if (mOpenMode & OpenMode::OPEN_WRITE) {
+ mUpdated = true;
+ }
+
+ if (mData) {
+ mData->Unlock();
+ }
+ mIsLocked = false;
+ mOpenMode = OpenMode::OPEN_NONE;
+
+ UnlockActor();
+ ReadUnlock();
+}
+
+void TextureClient::EnableReadLock() {
+ if (!mReadLock) {
+ if (mAllocator->GetTileLockAllocator()) {
+ mReadLock = NonBlockingTextureReadLock::Create(mAllocator);
+ } else {
+ // IPC is down
+ gfxCriticalError() << "TextureClient::EnableReadLock IPC is down";
+ }
+ }
+}
+
+bool TextureClient::OnForwardedToHost() {
+ if (mData) {
+ mData->OnForwardedToHost();
+ }
+
+ if (mReadLock && mUpdated) {
+ // Take a read lock on behalf of the TextureHost. The latter will unlock
+ // after the shared data is available again for drawing.
+ mReadLock->ReadLock();
+ mUpdated = false;
+ return true;
+ }
+
+ return false;
+}
+
+TextureClient::~TextureClient() {
+ // TextureClients should be kept alive while there are references on the
+ // paint thread.
+ MOZ_ASSERT(mPaintThreadRefs == 0);
+ mReadLock = nullptr;
+ Destroy();
+}
+
+void TextureClient::UpdateFromSurface(gfx::SourceSurface* aSurface) {
+ MOZ_ASSERT(IsValid());
+ MOZ_ASSERT(mIsLocked);
+ MOZ_ASSERT(aSurface);
+ // If you run into this assertion, make sure the texture was locked write-only
+ // rather than read-write.
+ MOZ_ASSERT(!mBorrowedDrawTarget);
+
+ // XXX - It would be better to first try the DrawTarget approach and fallback
+ // to the backend-specific implementation because the latter will usually do
+ // an expensive read-back + cpu-side copy if the texture is on the gpu.
+ // There is a bug with the DrawTarget approach, though specific to reading
+ // back from WebGL (where R and B channel end up inverted) to figure out
+ // first.
+ if (mData->UpdateFromSurface(aSurface)) {
+ return;
+ }
+ if (CanExposeDrawTarget() && NS_IsMainThread()) {
+ RefPtr<DrawTarget> dt = BorrowDrawTarget();
+
+ MOZ_ASSERT(dt);
+ if (dt) {
+ dt->CopySurface(aSurface,
+ gfx::IntRect(gfx::IntPoint(0, 0), aSurface->GetSize()),
+ gfx::IntPoint(0, 0));
+ return;
+ }
+ }
+ NS_WARNING("TextureClient::UpdateFromSurface failed");
+}
+
+already_AddRefed<TextureClient> TextureClient::CreateSimilar(
+ LayersBackend aLayersBackend, TextureFlags aFlags,
+ TextureAllocationFlags aAllocFlags) const {
+ MOZ_ASSERT(IsValid());
+
+ MOZ_ASSERT(!mIsLocked);
+ if (mIsLocked) {
+ return nullptr;
+ }
+
+ LockActor();
+ TextureData* data =
+ mData->CreateSimilar(mAllocator, aLayersBackend, aFlags, aAllocFlags);
+ UnlockActor();
+
+ if (!data) {
+ return nullptr;
+ }
+
+ return MakeAndAddRef<TextureClient>(data, aFlags, mAllocator);
+}
+
+gfx::DrawTarget* TextureClient::BorrowDrawTarget() {
+ MOZ_ASSERT(IsValid());
+ MOZ_ASSERT(mIsLocked);
+ // TODO- We can't really assert that at the moment because there is code that
+ // Borrows the DrawTarget, just to get a snapshot, which is legit in term of
+ // OpenMode but we should have a way to get a SourceSurface directly instead.
+ // MOZ_ASSERT(mOpenMode & OpenMode::OPEN_WRITE);
+
+ if (!IsValid() || !mIsLocked) {
+ return nullptr;
+ }
+
+ if (!mBorrowedDrawTarget) {
+ mBorrowedDrawTarget = mData->BorrowDrawTarget();
+#ifdef DEBUG
+ mExpectedDtRefs = mBorrowedDrawTarget ? mBorrowedDrawTarget->refCount() : 0;
+#endif
+ }
+
+ return mBorrowedDrawTarget;
+}
+
+void TextureClient::EndDraw() {
+ MOZ_ASSERT(mOpenMode & OpenMode::OPEN_READ_WRITE);
+
+ // Because EndDraw is used when we are not unlocking this TextureClient at the
+ // end of a transaction, we need to Flush and DetachAllSnapshots to ensure any
+ // dependents are updated.
+ mBorrowedDrawTarget->Flush();
+ mBorrowedDrawTarget->DetachAllSnapshots();
+ MOZ_ASSERT(mBorrowedDrawTarget->refCount() <= mExpectedDtRefs);
+
+ mBorrowedDrawTarget = nullptr;
+ mData->EndDraw();
+}
+
+already_AddRefed<gfx::SourceSurface> TextureClient::BorrowSnapshot() {
+ MOZ_ASSERT(mIsLocked);
+
+ RefPtr<gfx::SourceSurface> surface = mData->BorrowSnapshot();
+ if (!surface) {
+ surface = BorrowDrawTarget()->Snapshot();
+ }
+
+ return surface.forget();
+}
+
+bool TextureClient::BorrowMappedData(MappedTextureData& aMap) {
+ MOZ_ASSERT(IsValid());
+
+ // TODO - SharedRGBImage just accesses the buffer without properly locking
+ // the texture. It's bad.
+ // MOZ_ASSERT(mIsLocked);
+ // if (!mIsLocked) {
+ // return nullptr;
+ //}
+
+ return mData ? mData->BorrowMappedData(aMap) : false;
+}
+
+bool TextureClient::BorrowMappedYCbCrData(MappedYCbCrTextureData& aMap) {
+ MOZ_ASSERT(IsValid());
+
+ return mData ? mData->BorrowMappedYCbCrData(aMap) : false;
+}
+
+bool TextureClient::ToSurfaceDescriptor(SurfaceDescriptor& aOutDescriptor) {
+ MOZ_ASSERT(IsValid());
+
+ return mData ? mData->Serialize(aOutDescriptor) : false;
+}
+
+// static
+PTextureChild* TextureClient::CreateIPDLActor() {
+ TextureChild* c = new TextureChild();
+ c->AddIPDLReference();
+ return c;
+}
+
+// static
+bool TextureClient::DestroyIPDLActor(PTextureChild* actor) {
+ static_cast<TextureChild*>(actor)->ReleaseIPDLReference();
+ return true;
+}
+
+// static
+already_AddRefed<TextureClient> TextureClient::AsTextureClient(
+ PTextureChild* actor) {
+ if (!actor) {
+ return nullptr;
+ }
+
+ TextureChild* tc = static_cast<TextureChild*>(actor);
+
+ tc->Lock();
+
+ // Since TextureClient may be destroyed asynchronously with respect to its
+ // IPDL actor, we must acquire a reference within a lock. The mDestroyed bit
+ // tells us whether or not the main thread has disconnected the TextureClient
+ // from its actor.
+ if (tc->mDestroyed) {
+ tc->Unlock();
+ return nullptr;
+ }
+
+ RefPtr<TextureClient> texture = tc->mTextureClient;
+ tc->Unlock();
+
+ return texture.forget();
+}
+
+bool TextureClient::IsSharedWithCompositor() const {
+ return mActor && mActor->IPCOpen();
+}
+
+void TextureClient::AddFlags(TextureFlags aFlags) {
+ MOZ_ASSERT(
+ !IsSharedWithCompositor() ||
+ ((GetFlags() & TextureFlags::RECYCLE) && !IsAddedToCompositableClient()));
+ mFlags |= aFlags;
+}
+
+void TextureClient::RemoveFlags(TextureFlags aFlags) {
+ MOZ_ASSERT(
+ !IsSharedWithCompositor() ||
+ ((GetFlags() & TextureFlags::RECYCLE) && !IsAddedToCompositableClient()));
+ mFlags &= ~aFlags;
+}
+
+void TextureClient::RecycleTexture(TextureFlags aFlags) {
+ MOZ_ASSERT(GetFlags() & TextureFlags::RECYCLE);
+ MOZ_ASSERT(!mIsLocked);
+
+ mAddedToCompositableClient = false;
+ if (mFlags != aFlags) {
+ mFlags = aFlags;
+ }
+}
+
+void TextureClient::SetAddedToCompositableClient() {
+ if (!mAddedToCompositableClient) {
+ mAddedToCompositableClient = true;
+ if (!(GetFlags() & TextureFlags::RECYCLE)) {
+ return;
+ }
+ MOZ_ASSERT(!mIsLocked);
+ LockActor();
+ if (IsValid() && mActor && !mActor->mDestroyed && mActor->IPCOpen()) {
+ mActor->SendRecycleTexture(mFlags);
+ }
+ UnlockActor();
+ }
+}
+
+static void CancelTextureClientNotifyNotUsed(uint64_t aTextureId,
+ LayersIPCChannel* aAllocator) {
+ if (!aAllocator) {
+ return;
+ }
+ nsCOMPtr<nsISerialEventTarget> thread = aAllocator->GetThread();
+ if (!thread) {
+ return;
+ }
+ if (thread->IsOnCurrentThread()) {
+ aAllocator->CancelWaitForNotifyNotUsed(aTextureId);
+ } else {
+ thread->Dispatch(NewRunnableFunction(
+ "CancelTextureClientNotifyNotUsedRunnable",
+ CancelTextureClientNotifyNotUsed, aTextureId, aAllocator));
+ }
+}
+
+void TextureClient::CancelWaitForNotifyNotUsed() {
+ if (GetFlags() & TextureFlags::RECYCLE) {
+ CancelTextureClientNotifyNotUsed(mSerial, GetAllocator());
+ return;
+ }
+}
+
+/* static */
+void TextureClient::TextureClientRecycleCallback(TextureClient* aClient,
+ void* aClosure) {
+ MOZ_ASSERT(aClient->GetRecycleAllocator());
+ aClient->GetRecycleAllocator()->RecycleTextureClient(aClient);
+}
+
+void TextureClient::SetRecycleAllocator(
+ ITextureClientRecycleAllocator* aAllocator) {
+ mRecycleAllocator = aAllocator;
+ if (aAllocator) {
+ SetRecycleCallback(TextureClientRecycleCallback, nullptr);
+ } else {
+ ClearRecycleCallback();
+ }
+}
+
+bool TextureClient::InitIPDLActor(CompositableForwarder* aForwarder) {
+ MOZ_ASSERT(aForwarder && aForwarder->GetTextureForwarder()->GetThread() ==
+ mAllocator->GetThread());
+
+ if (mActor && !mActor->IPCOpen()) {
+ return false;
+ }
+
+ if (mActor && !mActor->mDestroyed) {
+ CompositableForwarder* currentFwd = mActor->mCompositableForwarder;
+ TextureForwarder* currentTexFwd = mActor->mTextureForwarder;
+ if (currentFwd != aForwarder) {
+ // It's a bit iffy but right now ShadowLayerForwarder inherits
+ // TextureForwarder even though it should not.
+ // ShadowLayerForwarder::GetTextureForwarder actually returns a pointer to
+ // the CompositorBridgeChild. It's Ok for a texture to move from a
+ // ShadowLayerForwarder to another, but not form a CompositorBridgeChild
+ // to another (they use different channels).
+ if (currentTexFwd && currentTexFwd != aForwarder->GetTextureForwarder()) {
+ gfxCriticalError()
+ << "Attempt to move a texture to a different channel CF.";
+ MOZ_ASSERT_UNREACHABLE("unexpected to be called");
+ return false;
+ }
+ if (currentFwd && currentFwd->GetCompositorBackendType() !=
+ aForwarder->GetCompositorBackendType()) {
+ gfxCriticalError()
+ << "Attempt to move a texture to different compositor backend.";
+ MOZ_ASSERT_UNREACHABLE("unexpected to be called");
+ return false;
+ }
+ mActor->mCompositableForwarder = aForwarder;
+ mActor->mUsesImageBridge =
+ aForwarder->GetTextureForwarder()->UsesImageBridge();
+ }
+ return true;
+ }
+ MOZ_ASSERT(!mActor || mActor->mDestroyed,
+ "Cannot use a texture on several IPC channels.");
+
+ SurfaceDescriptor desc;
+ if (!ToSurfaceDescriptor(desc)) {
+ return false;
+ }
+
+ // Try external image id allocation.
+ mExternalImageId =
+ aForwarder->GetTextureForwarder()->GetNextExternalImageId();
+
+ ReadLockDescriptor readLockDescriptor = null_t();
+ if (mReadLock) {
+ mReadLock->Serialize(readLockDescriptor, GetAllocator()->GetParentPid());
+ }
+
+ PTextureChild* actor = aForwarder->GetTextureForwarder()->CreateTexture(
+ desc, std::move(readLockDescriptor),
+ aForwarder->GetCompositorBackendType(), GetFlags(),
+ dom::ContentParentId(), mSerial, mExternalImageId);
+
+ if (!actor) {
+ gfxCriticalNote << static_cast<int32_t>(desc.type()) << ", "
+ << static_cast<int32_t>(
+ aForwarder->GetCompositorBackendType())
+ << ", " << static_cast<uint32_t>(GetFlags()) << ", "
+ << mSerial;
+ return false;
+ }
+
+ mActor = static_cast<TextureChild*>(actor);
+ mActor->mCompositableForwarder = aForwarder;
+ mActor->mTextureForwarder = aForwarder->GetTextureForwarder();
+ mActor->mTextureClient = this;
+
+ // If the TextureClient is already locked, we have to lock TextureChild's
+ // mutex since it will be unlocked in TextureClient::Unlock.
+ if (mIsLocked) {
+ LockActor();
+ }
+
+ return mActor->IPCOpen();
+}
+
+bool TextureClient::InitIPDLActor(KnowsCompositor* aKnowsCompositor,
+ const dom::ContentParentId& aContentId) {
+ MOZ_ASSERT(aKnowsCompositor &&
+ aKnowsCompositor->GetTextureForwarder()->GetThread() ==
+ mAllocator->GetThread());
+ TextureForwarder* fwd = aKnowsCompositor->GetTextureForwarder();
+ if (mActor && !mActor->mDestroyed) {
+ CompositableForwarder* currentFwd = mActor->mCompositableForwarder;
+ TextureForwarder* currentTexFwd = mActor->mTextureForwarder;
+
+ if (currentFwd) {
+ gfxCriticalError()
+ << "Attempt to remove a texture from a CompositableForwarder.";
+ return false;
+ }
+
+ if (currentTexFwd && currentTexFwd != fwd) {
+ gfxCriticalError()
+ << "Attempt to move a texture to a different channel TF.";
+ return false;
+ }
+ mActor->mTextureForwarder = fwd;
+ return true;
+ }
+ MOZ_ASSERT(!mActor || mActor->mDestroyed,
+ "Cannot use a texture on several IPC channels.");
+
+ SurfaceDescriptor desc;
+ if (!ToSurfaceDescriptor(desc)) {
+ return false;
+ }
+
+ // Try external image id allocation.
+ mExternalImageId =
+ aKnowsCompositor->GetTextureForwarder()->GetNextExternalImageId();
+
+ ReadLockDescriptor readLockDescriptor = null_t();
+ if (mReadLock) {
+ mReadLock->Serialize(readLockDescriptor, GetAllocator()->GetParentPid());
+ }
+
+ PTextureChild* actor =
+ fwd->CreateTexture(desc, std::move(readLockDescriptor),
+ aKnowsCompositor->GetCompositorBackendType(),
+ GetFlags(), aContentId, mSerial, mExternalImageId);
+ if (!actor) {
+ gfxCriticalNote << static_cast<int32_t>(desc.type()) << ", "
+ << static_cast<int32_t>(
+ aKnowsCompositor->GetCompositorBackendType())
+ << ", " << static_cast<uint32_t>(GetFlags()) << ", "
+ << mSerial;
+ return false;
+ }
+
+ mActor = static_cast<TextureChild*>(actor);
+ mActor->mTextureForwarder = fwd;
+ mActor->mTextureClient = this;
+
+ // If the TextureClient is already locked, we have to lock TextureChild's
+ // mutex since it will be unlocked in TextureClient::Unlock.
+ if (mIsLocked) {
+ LockActor();
+ }
+
+ return mActor->IPCOpen();
+}
+
+PTextureChild* TextureClient::GetIPDLActor() { return mActor; }
+
+// static
+already_AddRefed<TextureClient> TextureClient::CreateForDrawing(
+ KnowsCompositor* aAllocator, gfx::SurfaceFormat aFormat, gfx::IntSize aSize,
+ BackendSelector aSelector, TextureFlags aTextureFlags,
+ TextureAllocationFlags aAllocFlags) {
+ return TextureClient::CreateForDrawing(aAllocator->GetTextureForwarder(),
+ aFormat, aSize, aAllocator, aSelector,
+ aTextureFlags, aAllocFlags);
+}
+
+// static
+already_AddRefed<TextureClient> TextureClient::CreateForDrawing(
+ TextureForwarder* aAllocator, gfx::SurfaceFormat aFormat,
+ gfx::IntSize aSize, KnowsCompositor* aKnowsCompositor,
+ BackendSelector aSelector, TextureFlags aTextureFlags,
+ TextureAllocationFlags aAllocFlags) {
+ LayersBackend layersBackend = aKnowsCompositor->GetCompositorBackendType();
+ gfx::BackendType moz2DBackend =
+ BackendTypeForBackendSelector(layersBackend, aSelector);
+
+ // also test the validity of aAllocator
+ if (!aAllocator || !aAllocator->IPCOpen()) {
+ return nullptr;
+ }
+
+ if (!gfx::Factory::AllowedSurfaceSize(aSize)) {
+ return nullptr;
+ }
+
+ TextureData* data =
+ TextureData::Create(aAllocator, aFormat, aSize, aKnowsCompositor,
+ aSelector, aTextureFlags, aAllocFlags);
+
+ if (data) {
+ return MakeAndAddRef<TextureClient>(data, aTextureFlags, aAllocator);
+ }
+
+ // Can't do any better than a buffer texture client.
+ return TextureClient::CreateForRawBufferAccess(aAllocator, aFormat, aSize,
+ moz2DBackend, layersBackend,
+ aTextureFlags, aAllocFlags);
+}
+
+// static
+already_AddRefed<TextureClient> TextureClient::CreateFromSurface(
+ KnowsCompositor* aAllocator, gfx::SourceSurface* aSurface,
+ BackendSelector aSelector, TextureFlags aTextureFlags,
+ TextureAllocationFlags aAllocFlags) {
+ // also test the validity of aAllocator
+ if (!aAllocator || !aAllocator->GetTextureForwarder()->IPCOpen()) {
+ return nullptr;
+ }
+
+ gfx::IntSize size = aSurface->GetSize();
+
+ if (!gfx::Factory::AllowedSurfaceSize(size)) {
+ return nullptr;
+ }
+
+ TextureData* data = nullptr;
+#if defined(XP_WIN)
+ LayersBackend layersBackend = aAllocator->GetCompositorBackendType();
+ gfx::BackendType moz2DBackend =
+ BackendTypeForBackendSelector(layersBackend, aSelector);
+
+ int32_t maxTextureSize = aAllocator->GetMaxTextureSize();
+
+ if (layersBackend == LayersBackend::LAYERS_WR &&
+ (moz2DBackend == gfx::BackendType::DIRECT2D ||
+ moz2DBackend == gfx::BackendType::DIRECT2D1_1) &&
+ size.width <= maxTextureSize && size.height <= maxTextureSize) {
+ data = D3D11TextureData::Create(aSurface, aAllocFlags);
+ }
+#endif
+
+ if (data) {
+ return MakeAndAddRef<TextureClient>(data, aTextureFlags,
+ aAllocator->GetTextureForwarder());
+ }
+
+ // Fall back to using UpdateFromSurface
+
+ TextureAllocationFlags allocFlags =
+ TextureAllocationFlags(aAllocFlags | ALLOC_UPDATE_FROM_SURFACE);
+ RefPtr<TextureClient> client =
+ CreateForDrawing(aAllocator, aSurface->GetFormat(), size, aSelector,
+ aTextureFlags, allocFlags);
+ if (!client) {
+ return nullptr;
+ }
+
+ TextureClientAutoLock autoLock(client, OpenMode::OPEN_WRITE_ONLY);
+ if (!autoLock.Succeeded()) {
+ return nullptr;
+ }
+
+ client->UpdateFromSurface(aSurface);
+ return client.forget();
+}
+
+// static
+already_AddRefed<TextureClient> TextureClient::CreateForRawBufferAccess(
+ KnowsCompositor* aAllocator, gfx::SurfaceFormat aFormat, gfx::IntSize aSize,
+ gfx::BackendType aMoz2DBackend, TextureFlags aTextureFlags,
+ TextureAllocationFlags aAllocFlags) {
+ return CreateForRawBufferAccess(
+ aAllocator->GetTextureForwarder(), aFormat, aSize, aMoz2DBackend,
+ aAllocator->GetCompositorBackendType(), aTextureFlags, aAllocFlags);
+}
+
+// static
+already_AddRefed<TextureClient> TextureClient::CreateForRawBufferAccess(
+ LayersIPCChannel* aAllocator, gfx::SurfaceFormat aFormat,
+ gfx::IntSize aSize, gfx::BackendType aMoz2DBackend,
+ LayersBackend aLayersBackend, TextureFlags aTextureFlags,
+ TextureAllocationFlags aAllocFlags) {
+ // also test the validity of aAllocator
+ if (!aAllocator || !aAllocator->IPCOpen()) {
+ return nullptr;
+ }
+
+ if (!gfx::Factory::AllowedSurfaceSize(aSize)) {
+ return nullptr;
+ }
+
+ if (aFormat == SurfaceFormat::B8G8R8X8) {
+ // Skia doesn't support RGBX, so ensure we clear the buffer for the proper
+ // alpha values.
+ aAllocFlags = TextureAllocationFlags(aAllocFlags | ALLOC_CLEAR_BUFFER);
+ }
+
+ // Note that we ignore the backend type if we get here. It should only be D2D
+ // or Skia, and D2D does not support data surfaces. Therefore it is safe to
+ // force the buffer to be Skia.
+ NS_WARNING_ASSERTION(aMoz2DBackend == gfx::BackendType::SKIA ||
+ aMoz2DBackend == gfx::BackendType::DIRECT2D ||
+ aMoz2DBackend == gfx::BackendType::DIRECT2D1_1,
+ "Unsupported TextureClient backend type");
+
+ TextureData* texData = BufferTextureData::Create(
+ aSize, aFormat, gfx::BackendType::SKIA, aLayersBackend, aTextureFlags,
+ aAllocFlags, aAllocator);
+ if (!texData) {
+ return nullptr;
+ }
+
+ return MakeAndAddRef<TextureClient>(texData, aTextureFlags, aAllocator);
+}
+
+// static
+already_AddRefed<TextureClient> TextureClient::CreateForYCbCr(
+ KnowsCompositor* aAllocator, const gfx::IntRect& aDisplay,
+ const gfx::IntSize& aYSize, uint32_t aYStride,
+ const gfx::IntSize& aCbCrSize, uint32_t aCbCrStride, StereoMode aStereoMode,
+ gfx::ColorDepth aColorDepth, gfx::YUVColorSpace aYUVColorSpace,
+ gfx::ColorRange aColorRange, gfx::ChromaSubsampling aSubsampling,
+ TextureFlags aTextureFlags) {
+ if (!aAllocator || !aAllocator->GetLayersIPCActor()->IPCOpen()) {
+ return nullptr;
+ }
+
+ if (!gfx::Factory::AllowedSurfaceSize(aYSize)) {
+ return nullptr;
+ }
+
+ TextureData* data = BufferTextureData::CreateForYCbCr(
+ aAllocator, aDisplay, aYSize, aYStride, aCbCrSize, aCbCrStride,
+ aStereoMode, aColorDepth, aYUVColorSpace, aColorRange, aSubsampling,
+ aTextureFlags);
+ if (!data) {
+ return nullptr;
+ }
+
+ return MakeAndAddRef<TextureClient>(data, aTextureFlags,
+ aAllocator->GetTextureForwarder());
+}
+
+TextureClient::TextureClient(TextureData* aData, TextureFlags aFlags,
+ LayersIPCChannel* aAllocator)
+ : AtomicRefCountedWithFinalize("TextureClient"),
+ mAllocator(aAllocator),
+ mActor(nullptr),
+ mData(aData),
+ mFlags(aFlags),
+ mOpenMode(OpenMode::OPEN_NONE)
+#ifdef DEBUG
+ ,
+ mExpectedDtRefs(0)
+#endif
+ ,
+ mIsLocked(false),
+ mIsReadLocked(false),
+ mUpdated(false),
+ mAddedToCompositableClient(false),
+ mFwdTransactionId(0),
+ mSerial(++sSerialCounter)
+#ifdef GFX_DEBUG_TRACK_CLIENTS_IN_POOL
+ ,
+ mPoolTracker(nullptr)
+#endif
+{
+ mData->FillInfo(mInfo);
+ mFlags |= mData->GetTextureFlags();
+
+ if (mFlags & TextureFlags::NON_BLOCKING_READ_LOCK) {
+ MOZ_ASSERT(!(mFlags & TextureFlags::BLOCKING_READ_LOCK));
+ EnableReadLock();
+ } else if (mFlags & TextureFlags::BLOCKING_READ_LOCK) {
+ MOZ_ASSERT(!(mFlags & TextureFlags::NON_BLOCKING_READ_LOCK));
+ EnableBlockingReadLock();
+ }
+}
+
+bool TextureClient::CopyToTextureClient(TextureClient* aTarget,
+ const gfx::IntRect* aRect,
+ const gfx::IntPoint* aPoint) {
+ MOZ_ASSERT(IsLocked());
+ MOZ_ASSERT(aTarget->IsLocked());
+
+ if (!aTarget->CanExposeDrawTarget() || !CanExposeDrawTarget()) {
+ return false;
+ }
+
+ RefPtr<DrawTarget> destinationTarget = aTarget->BorrowDrawTarget();
+ if (!destinationTarget) {
+ gfxWarning() << "TextureClient::CopyToTextureClient (dest) failed in "
+ "BorrowDrawTarget";
+ return false;
+ }
+
+ RefPtr<DrawTarget> sourceTarget = BorrowDrawTarget();
+ if (!sourceTarget) {
+ gfxWarning() << "TextureClient::CopyToTextureClient (src) failed in "
+ "BorrowDrawTarget";
+ return false;
+ }
+
+ RefPtr<gfx::SourceSurface> source = sourceTarget->Snapshot();
+ destinationTarget->CopySurface(
+ source, aRect ? *aRect : gfx::IntRect(gfx::IntPoint(0, 0), GetSize()),
+ aPoint ? *aPoint : gfx::IntPoint(0, 0));
+ return true;
+}
+
+already_AddRefed<gfx::DataSourceSurface> TextureClient::GetAsSurface() {
+ if (!Lock(OpenMode::OPEN_READ)) {
+ return nullptr;
+ }
+ RefPtr<gfx::DataSourceSurface> data;
+ { // scope so that the DrawTarget is destroyed before Unlock()
+ RefPtr<gfx::DrawTarget> dt = BorrowDrawTarget();
+ if (dt) {
+ RefPtr<gfx::SourceSurface> surf = dt->Snapshot();
+ if (surf) {
+ data = surf->GetDataSurface();
+ }
+ }
+ }
+ Unlock();
+ return data.forget();
+}
+
+void TextureClient::GetSurfaceDescriptorRemoteDecoder(
+ SurfaceDescriptorRemoteDecoder* const aOutDesc) {
+ const auto handle = GetSerial();
+
+ RemoteDecoderVideoSubDescriptor subDesc = null_t();
+ MOZ_RELEASE_ASSERT(mData);
+ mData->GetSubDescriptor(&subDesc);
+
+ *aOutDesc =
+ SurfaceDescriptorRemoteDecoder(handle, std::move(subDesc), Nothing());
+}
+
+class MemoryTextureReadLock : public NonBlockingTextureReadLock {
+ public:
+ MemoryTextureReadLock();
+
+ virtual ~MemoryTextureReadLock();
+
+ bool ReadLock() override;
+
+ int32_t ReadUnlock() override;
+
+ int32_t GetReadCount() override;
+
+ LockType GetType() override { return TYPE_NONBLOCKING_MEMORY; }
+
+ bool IsValid() const override { return true; };
+
+ bool Serialize(ReadLockDescriptor& aOutput, base::ProcessId aOther) override;
+
+ Atomic<int32_t> mReadCount;
+};
+
+// The cross-prcess implementation of TextureReadLock.
+//
+// Since we don't use cross-process reference counting for the ReadLock objects,
+// we use the lock's internal counter as a way to know when to deallocate the
+// underlying shmem section: when the counter is equal to 1, it means that the
+// lock is not "held" (the texture is writable), when the counter is equal to 0
+// it means that we can safely deallocate the shmem section without causing a
+// race condition with the other process.
+class ShmemTextureReadLock : public NonBlockingTextureReadLock {
+ public:
+ struct ShmReadLockInfo {
+ int32_t readCount;
+ };
+
+ explicit ShmemTextureReadLock(LayersIPCChannel* aAllocator);
+
+ virtual ~ShmemTextureReadLock();
+
+ bool ReadLock() override;
+
+ int32_t ReadUnlock() override;
+
+ int32_t GetReadCount() override;
+
+ bool IsValid() const override { return mAllocSuccess; };
+
+ LockType GetType() override { return TYPE_NONBLOCKING_SHMEM; }
+
+ bool Serialize(ReadLockDescriptor& aOutput, base::ProcessId aOther) override;
+
+ mozilla::layers::ShmemSection& GetShmemSection() { return mShmemSection; }
+
+ explicit ShmemTextureReadLock(
+ const mozilla::layers::ShmemSection& aShmemSection)
+ : mShmemSection(aShmemSection), mAllocSuccess(true) {
+ MOZ_COUNT_CTOR(ShmemTextureReadLock);
+ }
+
+ ShmReadLockInfo* GetShmReadLockInfoPtr() {
+ return reinterpret_cast<ShmReadLockInfo*>(
+ mShmemSection.shmem().get<char>() + mShmemSection.offset());
+ }
+
+ RefPtr<LayersIPCChannel> mClientAllocator;
+ mozilla::layers::ShmemSection mShmemSection;
+ bool mAllocSuccess;
+};
+
+class CrossProcessSemaphoreReadLock : public TextureReadLock {
+ public:
+ CrossProcessSemaphoreReadLock()
+ : mSemaphore(CrossProcessSemaphore::Create("TextureReadLock", 1)),
+ mShared(false) {}
+ explicit CrossProcessSemaphoreReadLock(CrossProcessSemaphoreHandle aHandle)
+ : mSemaphore(CrossProcessSemaphore::Create(std::move(aHandle))),
+ mShared(false) {}
+
+ bool ReadLock() override {
+ if (!IsValid()) {
+ return false;
+ }
+ return mSemaphore->Wait();
+ }
+ bool TryReadLock(TimeDuration aTimeout) override {
+ if (!IsValid()) {
+ return false;
+ }
+ return mSemaphore->Wait(Some(aTimeout));
+ }
+ int32_t ReadUnlock() override {
+ if (!IsValid()) {
+ return 1;
+ }
+ mSemaphore->Signal();
+ return 1;
+ }
+ bool IsValid() const override { return !!mSemaphore; }
+
+ bool Serialize(ReadLockDescriptor& aOutput, base::ProcessId aOther) override;
+
+ LockType GetType() override { return TYPE_CROSS_PROCESS_SEMAPHORE; }
+
+ UniquePtr<CrossProcessSemaphore> mSemaphore;
+ bool mShared;
+};
+
+// static
+already_AddRefed<TextureReadLock> TextureReadLock::Deserialize(
+ ReadLockDescriptor&& aDescriptor, ISurfaceAllocator* aAllocator) {
+ switch (aDescriptor.type()) {
+ case ReadLockDescriptor::TShmemSection: {
+ const ShmemSection& section = aDescriptor.get_ShmemSection();
+ MOZ_RELEASE_ASSERT(section.shmem().IsReadable());
+ return MakeAndAddRef<ShmemTextureReadLock>(section);
+ }
+ case ReadLockDescriptor::Tuintptr_t: {
+ if (!aAllocator->IsSameProcess()) {
+ // Trying to use a memory based lock instead of a shmem based one in
+ // the cross-process case is a bad security violation.
+ NS_ERROR(
+ "A client process may be trying to peek at the host's address "
+ "space!");
+ return nullptr;
+ }
+ RefPtr<TextureReadLock> lock =
+ reinterpret_cast<MemoryTextureReadLock*>(aDescriptor.get_uintptr_t());
+
+ MOZ_ASSERT(lock);
+ if (lock) {
+ // The corresponding AddRef is in MemoryTextureReadLock::Serialize
+ lock.get()->Release();
+ }
+
+ return lock.forget();
+ }
+ case ReadLockDescriptor::TCrossProcessSemaphoreDescriptor: {
+ return MakeAndAddRef<CrossProcessSemaphoreReadLock>(
+ std::move(aDescriptor.get_CrossProcessSemaphoreDescriptor().sem()));
+ }
+ case ReadLockDescriptor::Tnull_t: {
+ return nullptr;
+ }
+ default: {
+ // Invalid descriptor.
+ MOZ_DIAGNOSTIC_ASSERT(false);
+ }
+ }
+ return nullptr;
+}
+// static
+already_AddRefed<TextureReadLock> NonBlockingTextureReadLock::Create(
+ LayersIPCChannel* aAllocator) {
+ if (aAllocator->IsSameProcess()) {
+ // If our compositor is in the same process, we can save some cycles by not
+ // using shared memory.
+ return MakeAndAddRef<MemoryTextureReadLock>();
+ }
+
+ return MakeAndAddRef<ShmemTextureReadLock>(aAllocator);
+}
+
+MemoryTextureReadLock::MemoryTextureReadLock() : mReadCount(1) {
+ MOZ_COUNT_CTOR(MemoryTextureReadLock);
+}
+
+MemoryTextureReadLock::~MemoryTextureReadLock() {
+ // One read count that is added in constructor.
+ MOZ_ASSERT(mReadCount == 1);
+ MOZ_COUNT_DTOR(MemoryTextureReadLock);
+}
+
+bool MemoryTextureReadLock::Serialize(ReadLockDescriptor& aOutput,
+ base::ProcessId aOther) {
+ // AddRef here and Release when receiving on the host side to make sure the
+ // reference count doesn't go to zero before the host receives the message.
+ // see TextureReadLock::Deserialize
+ this->AddRef();
+ aOutput = ReadLockDescriptor(uintptr_t(this));
+ return true;
+}
+
+bool MemoryTextureReadLock::ReadLock() {
+ NS_ASSERT_OWNINGTHREAD(MemoryTextureReadLock);
+
+ ++mReadCount;
+ return true;
+}
+
+int32_t MemoryTextureReadLock::ReadUnlock() {
+ int32_t readCount = --mReadCount;
+ MOZ_ASSERT(readCount >= 0);
+
+ return readCount;
+}
+
+int32_t MemoryTextureReadLock::GetReadCount() {
+ NS_ASSERT_OWNINGTHREAD(MemoryTextureReadLock);
+ return mReadCount;
+}
+
+ShmemTextureReadLock::ShmemTextureReadLock(LayersIPCChannel* aAllocator)
+ : mClientAllocator(aAllocator), mAllocSuccess(false) {
+ MOZ_COUNT_CTOR(ShmemTextureReadLock);
+ MOZ_ASSERT(mClientAllocator);
+ MOZ_ASSERT(mClientAllocator->GetTileLockAllocator());
+#define MOZ_ALIGN_WORD(x) (((x) + 3) & ~3)
+ if (mClientAllocator->GetTileLockAllocator()->AllocShmemSection(
+ MOZ_ALIGN_WORD(sizeof(ShmReadLockInfo)), &mShmemSection)) {
+ ShmReadLockInfo* info = GetShmReadLockInfoPtr();
+ info->readCount = 1;
+ mAllocSuccess = true;
+ }
+}
+
+ShmemTextureReadLock::~ShmemTextureReadLock() {
+ if (mClientAllocator) {
+ // Release one read count that is added in constructor.
+ // The count is kept for calling GetReadCount() by TextureClientPool.
+ ReadUnlock();
+ }
+ MOZ_COUNT_DTOR(ShmemTextureReadLock);
+}
+
+bool ShmemTextureReadLock::Serialize(ReadLockDescriptor& aOutput,
+ base::ProcessId aOther) {
+ aOutput = ReadLockDescriptor(GetShmemSection());
+ return true;
+}
+
+bool ShmemTextureReadLock::ReadLock() {
+ NS_ASSERT_OWNINGTHREAD(ShmemTextureReadLock);
+ if (!mAllocSuccess) {
+ return false;
+ }
+ ShmReadLockInfo* info = GetShmReadLockInfoPtr();
+ PR_ATOMIC_INCREMENT(&info->readCount);
+ return true;
+}
+
+int32_t ShmemTextureReadLock::ReadUnlock() {
+ if (!mAllocSuccess) {
+ return 0;
+ }
+ ShmReadLockInfo* info = GetShmReadLockInfoPtr();
+ int32_t readCount = PR_ATOMIC_DECREMENT(&info->readCount);
+ MOZ_ASSERT(readCount >= 0);
+ if (readCount <= 0) {
+ if (mClientAllocator && mClientAllocator->GetTileLockAllocator()) {
+ mClientAllocator->GetTileLockAllocator()->DeallocShmemSection(
+ mShmemSection);
+ } else {
+ // we are on the compositor process, or IPC is down.
+ FixedSizeSmallShmemSectionAllocator::FreeShmemSection(mShmemSection);
+ }
+ }
+ return readCount;
+}
+
+int32_t ShmemTextureReadLock::GetReadCount() {
+ NS_ASSERT_OWNINGTHREAD(ShmemTextureReadLock);
+ if (!mAllocSuccess) {
+ return 0;
+ }
+ ShmReadLockInfo* info = GetShmReadLockInfoPtr();
+ return info->readCount;
+}
+
+bool CrossProcessSemaphoreReadLock::Serialize(ReadLockDescriptor& aOutput,
+ base::ProcessId aOther) {
+ if (!mShared && IsValid()) {
+ aOutput = ReadLockDescriptor(
+ CrossProcessSemaphoreDescriptor(mSemaphore->CloneHandle()));
+ mSemaphore->CloseHandle();
+ mShared = true;
+ return true;
+ } else {
+ return mShared;
+ }
+}
+
+void TextureClient::EnableBlockingReadLock() {
+ if (!mReadLock) {
+ mReadLock = new CrossProcessSemaphoreReadLock();
+ }
+}
+
+bool UpdateYCbCrTextureClient(TextureClient* aTexture,
+ const PlanarYCbCrData& aData) {
+ MOZ_ASSERT(aTexture);
+ MOZ_ASSERT(aTexture->IsLocked());
+ MOZ_ASSERT(aTexture->GetFormat() == gfx::SurfaceFormat::YUV,
+ "This textureClient can only use YCbCr data");
+ MOZ_ASSERT(!aTexture->IsImmutable());
+ MOZ_ASSERT(aTexture->IsValid());
+ MOZ_ASSERT(aData.mCbSkip == aData.mCrSkip);
+
+ MappedYCbCrTextureData mapped;
+ if (!aTexture->BorrowMappedYCbCrData(mapped)) {
+ NS_WARNING("Failed to extract YCbCr info!");
+ return false;
+ }
+
+ uint32_t bytesPerPixel =
+ BytesPerPixel(SurfaceFormatForColorDepth(aData.mColorDepth));
+ MappedYCbCrTextureData srcData;
+ srcData.y.data = aData.mYChannel;
+ srcData.y.size = aData.YDataSize();
+ srcData.y.stride = aData.mYStride;
+ srcData.y.skip = aData.mYSkip;
+ srcData.y.bytesPerPixel = bytesPerPixel;
+ srcData.cb.data = aData.mCbChannel;
+ srcData.cb.size = aData.CbCrDataSize();
+ srcData.cb.stride = aData.mCbCrStride;
+ srcData.cb.skip = aData.mCbSkip;
+ srcData.cb.bytesPerPixel = bytesPerPixel;
+ srcData.cr.data = aData.mCrChannel;
+ srcData.cr.size = aData.CbCrDataSize();
+ srcData.cr.stride = aData.mCbCrStride;
+ srcData.cr.skip = aData.mCrSkip;
+ srcData.cr.bytesPerPixel = bytesPerPixel;
+ srcData.metadata = nullptr;
+
+ if (!srcData.CopyInto(mapped)) {
+ NS_WARNING("Failed to copy image data!");
+ return false;
+ }
+
+ if (TextureRequiresLocking(aTexture->GetFlags())) {
+ // We don't have support for proper locking yet, so we'll
+ // have to be immutable instead.
+ aTexture->MarkImmutable();
+ }
+ return true;
+}
+
+already_AddRefed<TextureClient> TextureClient::CreateWithData(
+ TextureData* aData, TextureFlags aFlags, LayersIPCChannel* aAllocator) {
+ if (!aData) {
+ return nullptr;
+ }
+ return MakeAndAddRef<TextureClient>(aData, aFlags, aAllocator);
+}
+
+template <class PixelDataType>
+static void copyData(PixelDataType* aDst,
+ const MappedYCbCrChannelData& aChannelDst,
+ PixelDataType* aSrc,
+ const MappedYCbCrChannelData& aChannelSrc) {
+ uint8_t* srcByte = reinterpret_cast<uint8_t*>(aSrc);
+ const int32_t srcSkip = aChannelSrc.skip + 1;
+ uint8_t* dstByte = reinterpret_cast<uint8_t*>(aDst);
+ const int32_t dstSkip = aChannelDst.skip + 1;
+ for (int32_t i = 0; i < aChannelSrc.size.height; ++i) {
+ for (int32_t j = 0; j < aChannelSrc.size.width; ++j) {
+ *aDst = *aSrc;
+ aSrc += srcSkip;
+ aDst += dstSkip;
+ }
+ srcByte += aChannelSrc.stride;
+ aSrc = reinterpret_cast<PixelDataType*>(srcByte);
+ dstByte += aChannelDst.stride;
+ aDst = reinterpret_cast<PixelDataType*>(dstByte);
+ }
+}
+
+bool MappedYCbCrChannelData::CopyInto(MappedYCbCrChannelData& aDst) {
+ if (!data || !aDst.data || size != aDst.size) {
+ return false;
+ }
+
+ if (stride == aDst.stride && skip == aDst.skip) {
+ // fast path!
+ // We assume that the padding in the destination is there for alignment
+ // purposes and doesn't contain useful data.
+ memcpy(aDst.data, data, stride * size.height);
+ return true;
+ }
+
+ if (aDst.skip == 0 && skip == 0) {
+ // fast-ish path
+ for (int32_t i = 0; i < size.height; ++i) {
+ memcpy(aDst.data + i * aDst.stride, data + i * stride,
+ size.width * bytesPerPixel);
+ }
+ return true;
+ }
+
+ MOZ_ASSERT(bytesPerPixel == 1 || bytesPerPixel == 2);
+ // slow path
+ if (bytesPerPixel == 1) {
+ copyData(aDst.data, aDst, data, *this);
+ } else if (bytesPerPixel == 2) {
+ copyData(reinterpret_cast<uint16_t*>(aDst.data), aDst,
+ reinterpret_cast<uint16_t*>(data), *this);
+ }
+ return true;
+}
+
+} // namespace mozilla::layers
diff --git a/gfx/layers/client/TextureClient.h b/gfx/layers/client/TextureClient.h
new file mode 100644
index 0000000000..f799e34bbe
--- /dev/null
+++ b/gfx/layers/client/TextureClient.h
@@ -0,0 +1,814 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_GFX_TEXTURECLIENT_H
+#define MOZILLA_GFX_TEXTURECLIENT_H
+
+#include <stddef.h> // for size_t
+#include <stdint.h> // for uint32_t, uint8_t, uint64_t
+
+#include "GLTextureImage.h" // for TextureImage
+#include "GfxTexturesReporter.h"
+#include "ImageTypes.h" // for StereoMode
+#include "mozilla/Assertions.h" // for MOZ_ASSERT, etc
+#include "mozilla/Atomics.h"
+#include "mozilla/Attributes.h" // for override
+#include "mozilla/DebugOnly.h"
+#include "mozilla/RefPtr.h" // for RefPtr, RefCounted
+#include "mozilla/dom/ipc/IdType.h"
+#include "mozilla/gfx/2D.h" // for DrawTarget
+#include "mozilla/gfx/CriticalSection.h"
+#include "mozilla/gfx/Point.h" // for IntSize
+#include "mozilla/gfx/Types.h" // for SurfaceFormat
+#include "mozilla/ipc/FileDescriptor.h"
+#include "mozilla/ipc/Shmem.h" // for Shmem
+#include "mozilla/layers/AtomicRefCountedWithFinalize.h"
+#include "mozilla/layers/CompositorTypes.h" // for TextureFlags, etc
+#include "mozilla/layers/ISurfaceAllocator.h"
+#include "mozilla/layers/LayersSurfaces.h" // for SurfaceDescriptor
+#include "mozilla/layers/LayersTypes.h"
+#include "mozilla/layers/SyncObject.h"
+#include "mozilla/mozalloc.h" // for operator delete
+#include "mozilla/webrender/WebRenderTypes.h"
+#include "nsCOMPtr.h" // for already_AddRefed
+#include "nsISupportsImpl.h" // for TextureImage::AddRef, etc
+#include "nsThreadUtils.h"
+#include "pratom.h"
+
+class gfxImageSurface;
+struct ID3D11Device;
+
+namespace mozilla {
+
+// When defined, we track which pool the tile came from and test for
+// any inconsistencies. This can be defined in release build as well.
+#ifdef DEBUG
+# define GFX_DEBUG_TRACK_CLIENTS_IN_POOL 1
+#endif
+
+namespace layers {
+
+class AndroidHardwareBufferTextureData;
+class BufferTextureData;
+class CompositableForwarder;
+class KnowsCompositor;
+class LayersIPCChannel;
+class CompositableClient;
+struct PlanarYCbCrData;
+class Image;
+class PTextureChild;
+class TextureChild;
+class TextureData;
+class GPUVideoTextureData;
+class TextureClient;
+class ITextureClientRecycleAllocator;
+class SharedSurfaceTextureData;
+#ifdef GFX_DEBUG_TRACK_CLIENTS_IN_POOL
+class TextureClientPool;
+#endif
+class TextureForwarder;
+
+/**
+ * TextureClient is the abstraction that allows us to share data between the
+ * content and the compositor side.
+ */
+
+enum TextureAllocationFlags {
+ ALLOC_DEFAULT = 0,
+ ALLOC_CLEAR_BUFFER =
+ 1 << 1, // Clear the buffer to whatever is best for the draw target
+ ALLOC_CLEAR_BUFFER_WHITE = 1 << 2, // explicit all white
+ ALLOC_CLEAR_BUFFER_BLACK = 1 << 3, // explicit all black
+ ALLOC_DISALLOW_BUFFERTEXTURECLIENT = 1 << 4,
+
+ // Allocate the texture for out-of-band content updates. This is mostly for
+ // TextureClientD3D11, which may otherwise choose D3D10 or non-KeyedMutex
+ // surfaces when used on the main thread.
+ ALLOC_FOR_OUT_OF_BAND_CONTENT = 1 << 5,
+
+ // Disable any cross-device synchronization. This is also for
+ // TextureClientD3D11, and creates a texture without KeyedMutex.
+ ALLOC_MANUAL_SYNCHRONIZATION = 1 << 6,
+
+ // The texture is going to be updated using UpdateFromSurface and needs to
+ // support that call.
+ ALLOC_UPDATE_FROM_SURFACE = 1 << 7,
+
+ // Do not use an accelerated texture type.
+ ALLOC_DO_NOT_ACCELERATE = 1 << 8,
+};
+
+enum class BackendSelector { Content, Canvas };
+
+/// Temporary object providing direct access to a Texture's memory.
+///
+/// see TextureClient::CanExposeMappedData() and
+/// TextureClient::BorrowMappedData().
+struct MappedTextureData {
+ uint8_t* data;
+ gfx::IntSize size;
+ int32_t stride;
+ gfx::SurfaceFormat format;
+};
+
+struct MappedYCbCrChannelData {
+ uint8_t* data;
+ gfx::IntSize size;
+ int32_t stride;
+ int32_t skip;
+ uint32_t bytesPerPixel;
+
+ bool CopyInto(MappedYCbCrChannelData& aDst);
+};
+
+struct MappedYCbCrTextureData {
+ MappedYCbCrChannelData y;
+ MappedYCbCrChannelData cb;
+ MappedYCbCrChannelData cr;
+ // Sad but because of how SharedPlanarYCbCrData is used we have to expose this
+ // for now.
+ uint8_t* metadata;
+ StereoMode stereoMode;
+
+ bool CopyInto(MappedYCbCrTextureData& aDst) {
+ return y.CopyInto(aDst.y) && cb.CopyInto(aDst.cb) && cr.CopyInto(aDst.cr);
+ }
+};
+
+class ReadLockDescriptor;
+class NonBlockingTextureReadLock;
+
+// A class to help implement copy-on-write semantics for shared textures.
+//
+// A TextureClient/Host pair can opt into using a ReadLock by calling
+// TextureClient::EnableReadLock. This will equip the TextureClient with a
+// ReadLock object that will be automatically ReadLock()'ed by the texture
+// itself when it is written into (see TextureClient::Unlock). A
+// TextureReadLock's counter starts at 1 and is expected to be equal to 1 when
+// the lock is destroyed. See ShmemTextureReadLock for explanations about why we
+// use 1 instead of 0 as the initial state. TextureReadLock is mostly internally
+// managed by the TextureClient/Host pair, and the compositable only has to
+// forward it during updates. If an update message contains a null_t lock, it
+// means that the texture was not written into on the content side, and there is
+// no synchronization required on the compositor side (or it means that the
+// texture pair did not opt into using ReadLocks). On the compositor side, the
+// TextureHost can receive a ReadLock during a transaction, and will both
+// ReadUnlock() it and drop it as soon as the shared data is available again for
+// writing (the texture upload is done, or the compositor not reading the
+// texture anymore). The lock is dropped to make sure it is ReadUnlock()'ed only
+// once.
+class TextureReadLock {
+ protected:
+ virtual ~TextureReadLock() = default;
+
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(TextureReadLock)
+
+ virtual bool ReadLock() = 0;
+ virtual bool TryReadLock(TimeDuration aTimeout) { return ReadLock(); }
+ virtual int32_t ReadUnlock() = 0;
+ virtual bool IsValid() const = 0;
+
+ static already_AddRefed<TextureReadLock> Deserialize(
+ ReadLockDescriptor&& aDescriptor, ISurfaceAllocator* aAllocator);
+
+ virtual bool Serialize(ReadLockDescriptor& aOutput,
+ base::ProcessId aOther) = 0;
+
+ enum LockType {
+ TYPE_NONBLOCKING_MEMORY,
+ TYPE_NONBLOCKING_SHMEM,
+ TYPE_CROSS_PROCESS_SEMAPHORE
+ };
+ virtual LockType GetType() = 0;
+
+ virtual NonBlockingTextureReadLock* AsNonBlockingLock() { return nullptr; }
+
+ protected:
+ NS_DECL_OWNINGTHREAD
+};
+
+class NonBlockingTextureReadLock : public TextureReadLock {
+ public:
+ virtual int32_t GetReadCount() = 0;
+
+ static already_AddRefed<TextureReadLock> Create(LayersIPCChannel* aAllocator);
+
+ NonBlockingTextureReadLock* AsNonBlockingLock() override { return this; }
+};
+
+#ifdef XP_WIN
+class D3D11TextureData;
+class DXGIYCbCrTextureData;
+#endif
+
+class TextureData {
+ public:
+ struct Info {
+ gfx::IntSize size;
+ gfx::SurfaceFormat format;
+ bool hasSynchronization;
+ bool supportsMoz2D;
+ bool canExposeMappedData;
+ bool canConcurrentlyReadLock;
+
+ Info()
+ : format(gfx::SurfaceFormat::UNKNOWN),
+ hasSynchronization(false),
+ supportsMoz2D(false),
+ canExposeMappedData(false),
+ canConcurrentlyReadLock(true) {}
+ };
+
+ static TextureData* Create(TextureForwarder* aAllocator,
+ gfx::SurfaceFormat aFormat, gfx::IntSize aSize,
+ KnowsCompositor* aKnowsCompositor,
+ BackendSelector aSelector,
+ TextureFlags aTextureFlags,
+ TextureAllocationFlags aAllocFlags);
+
+ static bool IsRemote(KnowsCompositor* aKnowsCompositor,
+ BackendSelector aSelector);
+
+ MOZ_COUNTED_DTOR_VIRTUAL(TextureData)
+
+ virtual void FillInfo(TextureData::Info& aInfo) const = 0;
+
+ virtual bool Lock(OpenMode aMode) = 0;
+
+ virtual void Unlock() = 0;
+
+ virtual already_AddRefed<gfx::DrawTarget> BorrowDrawTarget() {
+ return nullptr;
+ }
+
+ /**
+ * When the TextureData is not being Unlocked, this can be used to inform a
+ * TextureData that drawing has finished until the next BorrowDrawTarget.
+ */
+ virtual void EndDraw() {}
+
+ virtual already_AddRefed<gfx::SourceSurface> BorrowSnapshot() {
+ return nullptr;
+ }
+
+ virtual bool BorrowMappedData(MappedTextureData&) { return false; }
+
+ virtual bool BorrowMappedYCbCrData(MappedYCbCrTextureData&) { return false; }
+
+ virtual void Deallocate(LayersIPCChannel* aAllocator) = 0;
+
+ /// Depending on the texture's flags either Deallocate or Forget is called.
+ virtual void Forget(LayersIPCChannel* aAllocator) {}
+
+ virtual bool Serialize(SurfaceDescriptor& aDescriptor) = 0;
+ virtual void GetSubDescriptor(RemoteDecoderVideoSubDescriptor* aOutDesc) {}
+
+ virtual void OnForwardedToHost() {}
+
+ virtual TextureData* CreateSimilar(
+ LayersIPCChannel* aAllocator, LayersBackend aLayersBackend,
+ TextureFlags aFlags = TextureFlags::DEFAULT,
+ TextureAllocationFlags aAllocFlags = ALLOC_DEFAULT) const {
+ return nullptr;
+ }
+
+ virtual bool UpdateFromSurface(gfx::SourceSurface* aSurface) {
+ return false;
+ };
+
+ virtual void SyncWithObject(RefPtr<SyncObjectClient> aSyncObject){};
+
+ virtual TextureFlags GetTextureFlags() const {
+ return TextureFlags::NO_FLAGS;
+ }
+
+#ifdef XP_WIN
+ virtual D3D11TextureData* AsD3D11TextureData() { return nullptr; }
+ virtual DXGIYCbCrTextureData* AsDXGIYCbCrTextureData() { return nullptr; }
+#endif
+
+ virtual BufferTextureData* AsBufferTextureData() { return nullptr; }
+
+ virtual GPUVideoTextureData* AsGPUVideoTextureData() { return nullptr; }
+
+ virtual AndroidHardwareBufferTextureData*
+ AsAndroidHardwareBufferTextureData() {
+ return nullptr;
+ }
+
+ // It is used by AndroidHardwareBufferTextureData and
+ // SharedSurfaceTextureData. Returns buffer id when it owns
+ // AndroidHardwareBuffer. It is used only on android.
+ virtual Maybe<uint64_t> GetBufferId() const { return Nothing(); }
+
+ // The acquire fence is a fence that is used for waiting until rendering to
+ // its AHardwareBuffer is completed.
+ // It is used only on android.
+ virtual mozilla::ipc::FileDescriptor GetAcquireFence() {
+ return mozilla::ipc::FileDescriptor();
+ }
+
+ protected:
+ MOZ_COUNTED_DEFAULT_CTOR(TextureData)
+};
+
+/**
+ * TextureClient is a thin abstraction over texture data that need to be shared
+ * between the content process and the compositor process. It is the
+ * content-side half of a TextureClient/TextureHost pair. A corresponding
+ * TextureHost lives on the compositor-side.
+ *
+ * TextureClient's primary purpose is to present texture data in a way that is
+ * understood by the IPC system. There are two ways to use it:
+ * - Use it to serialize image data that is not IPC-friendly (most likely
+ * involving a copy into shared memory)
+ * - preallocate it and paint directly into it, which avoids copy but requires
+ * the painting code to be aware of TextureClient (or at least the underlying
+ * shared memory).
+ *
+ * There is always one and only one TextureClient per TextureHost, and the
+ * TextureClient/Host pair only owns one buffer of image data through its
+ * lifetime. This means that the lifetime of the underlying shared data
+ * matches the lifetime of the TextureClient/Host pair. It also means
+ * TextureClient/Host do not implement double buffering, which is the
+ * responsibility of the compositable (which would use pairs of Textures).
+ * In order to send several different buffers to the compositor side, use
+ * several TextureClients.
+ */
+class TextureClient : public AtomicRefCountedWithFinalize<TextureClient> {
+ public:
+ TextureClient(TextureData* aData, TextureFlags aFlags,
+ LayersIPCChannel* aAllocator);
+
+ virtual ~TextureClient();
+
+ static already_AddRefed<TextureClient> CreateWithData(
+ TextureData* aData, TextureFlags aFlags, LayersIPCChannel* aAllocator);
+
+ // Creates and allocates a TextureClient usable with Moz2D.
+ static already_AddRefed<TextureClient> CreateForDrawing(
+ KnowsCompositor* aAllocator, gfx::SurfaceFormat aFormat,
+ gfx::IntSize aSize, BackendSelector aSelector, TextureFlags aTextureFlags,
+ TextureAllocationFlags flags = ALLOC_DEFAULT);
+
+ static already_AddRefed<TextureClient> CreateFromSurface(
+ KnowsCompositor* aAllocator, gfx::SourceSurface* aSurface,
+ BackendSelector aSelector, TextureFlags aTextureFlags,
+ TextureAllocationFlags aAllocFlags);
+
+ // Creates and allocates a TextureClient supporting the YCbCr format.
+ static already_AddRefed<TextureClient> CreateForYCbCr(
+ KnowsCompositor* aAllocator, const gfx::IntRect& aDisplay,
+ const gfx::IntSize& aYSize, uint32_t aYStride,
+ const gfx::IntSize& aCbCrSize, uint32_t aCbCrStride,
+ StereoMode aStereoMode, gfx::ColorDepth aColorDepth,
+ gfx::YUVColorSpace aYUVColorSpace, gfx::ColorRange aColorRange,
+ gfx::ChromaSubsampling aSubsampling, TextureFlags aTextureFlags);
+
+ // Creates and allocates a TextureClient (can be accessed through raw
+ // pointers).
+ static already_AddRefed<TextureClient> CreateForRawBufferAccess(
+ KnowsCompositor* aAllocator, gfx::SurfaceFormat aFormat,
+ gfx::IntSize aSize, gfx::BackendType aMoz2dBackend,
+ TextureFlags aTextureFlags, TextureAllocationFlags flags = ALLOC_DEFAULT);
+
+ // Creates and allocates a TextureClient of the same type.
+ already_AddRefed<TextureClient> CreateSimilar(
+ LayersBackend aLayersBackend = LayersBackend::LAYERS_NONE,
+ TextureFlags aFlags = TextureFlags::DEFAULT,
+ TextureAllocationFlags aAllocFlags = ALLOC_DEFAULT) const;
+
+ /**
+ * Locks the shared data, allowing the caller to get access to it.
+ *
+ * Please always lock/unlock when accessing the shared data.
+ * If Lock() returns false, you should not attempt to access the shared data.
+ */
+ bool Lock(OpenMode aMode);
+
+ void Unlock();
+
+ bool IsLocked() const { return mIsLocked; }
+
+ gfx::IntSize GetSize() const { return mInfo.size; }
+
+ gfx::SurfaceFormat GetFormat() const { return mInfo.format; }
+
+ /**
+ * Returns true if this texture has a synchronization mechanism (mutex, fence,
+ * etc.). Textures that do not implement synchronization should be immutable
+ * or should use immediate uploads (see TextureFlags in CompositorTypes.h)
+ * Even if a texture does not implement synchronization, Lock and Unlock need
+ * to be used appropriately since the latter are also there to map/numap data.
+ */
+ bool HasSynchronization() const { return mInfo.hasSynchronization; }
+
+ bool CanExposeDrawTarget() const { return mInfo.supportsMoz2D; }
+
+ bool CanExposeMappedData() const { return mInfo.canExposeMappedData; }
+
+ /**
+ * Returns a DrawTarget to draw into the TextureClient.
+ * This function should never be called when not on the main thread!
+ *
+ * This must never be called on a TextureClient that is not sucessfully
+ * locked. When called several times within one Lock/Unlock pair, this method
+ * should return the same DrawTarget. The DrawTarget is automatically flushed
+ * by the TextureClient when the latter is unlocked, and the DrawTarget that
+ * will be returned within the next lock/unlock pair may or may not be the
+ * same object. Do not keep references to the DrawTarget outside of the
+ * lock/unlock pair.
+ *
+ * This is typically used as follows:
+ *
+ * if (!texture->Lock(OpenMode::OPEN_READ_WRITE)) {
+ * return false;
+ * }
+ * {
+ * // Restrict this code's scope to ensure all references to dt are gone
+ * // when Unlock is called.
+ * DrawTarget* dt = texture->BorrowDrawTarget();
+ * // use the draw target ...
+ * }
+ * texture->Unlock();
+ *
+ */
+ gfx::DrawTarget* BorrowDrawTarget();
+
+ /**
+ * When the TextureClient is not being Unlocked, this can be used to inform it
+ * that drawing has finished until the next BorrowDrawTarget.
+ */
+ void EndDraw();
+
+ already_AddRefed<gfx::SourceSurface> BorrowSnapshot();
+
+ /**
+ * Similar to BorrowDrawTarget but provides direct access to the texture's
+ * bits instead of a DrawTarget.
+ */
+ bool BorrowMappedData(MappedTextureData&);
+ bool BorrowMappedYCbCrData(MappedYCbCrTextureData&);
+
+ /**
+ * This function can be used to update the contents of the TextureClient
+ * off the main thread.
+ */
+ void UpdateFromSurface(gfx::SourceSurface* aSurface);
+
+ /**
+ * This method is strictly for debugging. It causes locking and
+ * needless copies.
+ */
+ already_AddRefed<gfx::DataSourceSurface> GetAsSurface();
+
+ /**
+ * Copies a rectangle from this texture client to a position in aTarget.
+ * It is assumed that the necessary locks are in place; so this should at
+ * least have a read lock and aTarget should at least have a write lock.
+ */
+ bool CopyToTextureClient(TextureClient* aTarget, const gfx::IntRect* aRect,
+ const gfx::IntPoint* aPoint);
+
+ /**
+ * Allocate and deallocate a TextureChild actor.
+ *
+ * TextureChild is an implementation detail of TextureClient that is not
+ * exposed to the rest of the code base. CreateIPDLActor and DestroyIPDLActor
+ * are for use with the managing IPDL protocols only (so that they can
+ * implement AllocPextureChild and DeallocPTextureChild).
+ */
+ static PTextureChild* CreateIPDLActor();
+ static bool DestroyIPDLActor(PTextureChild* actor);
+
+ /**
+ * Get the TextureClient corresponding to the actor passed in parameter.
+ */
+ static already_AddRefed<TextureClient> AsTextureClient(PTextureChild* actor);
+
+ /**
+ * TextureFlags contain important information about various aspects
+ * of the texture, like how its liferime is managed, and how it
+ * should be displayed.
+ * See TextureFlags in CompositorTypes.h.
+ */
+ TextureFlags GetFlags() const { return mFlags; }
+
+ bool HasFlags(TextureFlags aFlags) const {
+ return (mFlags & aFlags) == aFlags;
+ }
+
+ void AddFlags(TextureFlags aFlags);
+
+ void RemoveFlags(TextureFlags aFlags);
+
+ // Must not be called when TextureClient is in use by CompositableClient.
+ void RecycleTexture(TextureFlags aFlags);
+
+ /**
+ * After being shared with the compositor side, an immutable texture is never
+ * modified, it can only be read. It is safe to not Lock/Unlock immutable
+ * textures.
+ */
+ bool IsImmutable() const { return !!(mFlags & TextureFlags::IMMUTABLE); }
+
+ void MarkImmutable() { AddFlags(TextureFlags::IMMUTABLE); }
+
+ bool IsSharedWithCompositor() const;
+
+ /**
+ * If this method returns false users of TextureClient are not allowed
+ * to access the shared data.
+ */
+ bool IsValid() const { return !!mData; }
+
+ /**
+ * Called when TextureClient is added to CompositableClient.
+ */
+ void SetAddedToCompositableClient();
+
+ /**
+ * If this method retuns false, TextureClient is already added to
+ * CompositableClient, since its creation or recycling.
+ */
+ bool IsAddedToCompositableClient() const {
+ return mAddedToCompositableClient;
+ }
+
+ /**
+ * Create and init the TextureChild/Parent IPDL actor pair
+ * with a CompositableForwarder.
+ *
+ * Should be called only once per TextureClient.
+ * The TextureClient must not be locked when calling this method.
+ */
+ bool InitIPDLActor(CompositableForwarder* aForwarder);
+
+ /**
+ * Create and init the TextureChild/Parent IPDL actor pair
+ * with a TextureForwarder.
+ *
+ * Should be called only once per TextureClient.
+ * The TextureClient must not be locked when calling this method.
+ */
+ bool InitIPDLActor(KnowsCompositor* aKnowsCompositor,
+ const dom::ContentParentId& aContentId);
+
+ /**
+ * Return a pointer to the IPDLActor.
+ *
+ * This is to be used with IPDL messages only. Do not store the returned
+ * pointer.
+ */
+ PTextureChild* GetIPDLActor();
+
+ /**
+ * Triggers the destruction of the shared data and the corresponding
+ * TextureHost.
+ *
+ * If the texture flags contain TextureFlags::DEALLOCATE_CLIENT, the
+ * destruction will be synchronously coordinated with the compositor side,
+ * otherwise it will be done asynchronously.
+ */
+ void Destroy();
+
+ /**
+ * Track how much of this texture is wasted.
+ * For example we might allocate a 256x256 tile but only use 10x10.
+ */
+ void SetWaste(int aWasteArea) {
+ mWasteTracker.Update(aWasteArea, BytesPerPixel(GetFormat()));
+ }
+
+ void SyncWithObject(RefPtr<SyncObjectClient> aSyncObject) {
+ mData->SyncWithObject(aSyncObject);
+ }
+
+ LayersIPCChannel* GetAllocator() { return mAllocator; }
+
+ ITextureClientRecycleAllocator* GetRecycleAllocator() {
+ return mRecycleAllocator;
+ }
+ void SetRecycleAllocator(ITextureClientRecycleAllocator* aAllocator);
+
+ /// If you add new code that uses this method, you are probably doing
+ /// something wrong.
+ TextureData* GetInternalData() { return mData; }
+ const TextureData* GetInternalData() const { return mData; }
+
+ uint64_t GetSerial() const { return mSerial; }
+ void GetSurfaceDescriptorRemoteDecoder(
+ SurfaceDescriptorRemoteDecoder* aOutDesc);
+
+ void CancelWaitForNotifyNotUsed();
+
+ /**
+ * Set last transaction id of CompositableForwarder.
+ *
+ * Called when TextureClient has TextureFlags::RECYCLE flag.
+ * When CompositableForwarder forwards the TextureClient with
+ * TextureFlags::RECYCLE, it holds TextureClient's ref until host side
+ * releases it. The host side sends TextureClient release message.
+ * The id is used to check if the message is for the last TextureClient
+ * forwarding.
+ */
+ void SetLastFwdTransactionId(uint64_t aTransactionId) {
+ MOZ_ASSERT(mFwdTransactionId <= aTransactionId);
+ mFwdTransactionId = aTransactionId;
+ }
+
+ uint64_t GetLastFwdTransactionId() { return mFwdTransactionId; }
+
+ TextureReadLock* GetReadLock() { return mReadLock; }
+
+ bool IsReadLocked() const;
+
+ bool TryReadLock();
+ void ReadUnlock();
+
+ void SetUpdated() { mUpdated = true; }
+
+ bool OnForwardedToHost();
+
+ // Mark that the TextureClient will be used by the paint thread, and should
+ // not free its underlying texture data. This must only be called from the
+ // main thread.
+ void AddPaintThreadRef();
+
+ // Mark that the TextureClient is no longer in use by the PaintThread. This
+ // must only be called from the PaintThread.
+ void DropPaintThreadRef();
+
+ wr::MaybeExternalImageId GetExternalImageKey() { return mExternalImageId; }
+
+ private:
+ static void TextureClientRecycleCallback(TextureClient* aClient,
+ void* aClosure);
+
+ // Internal helpers for creating texture clients using the actual forwarder
+ // instead of KnowsCompositor. TextureClientPool uses these to let it cache
+ // texture clients per-process instead of per ShadowLayerForwarder, but
+ // everyone else should use the public functions instead.
+ friend class TextureClientPool;
+ static already_AddRefed<TextureClient> CreateForDrawing(
+ TextureForwarder* aAllocator, gfx::SurfaceFormat aFormat,
+ gfx::IntSize aSize, KnowsCompositor* aKnowsCompositor,
+ BackendSelector aSelector, TextureFlags aTextureFlags,
+ TextureAllocationFlags aAllocFlags = ALLOC_DEFAULT);
+
+ static already_AddRefed<TextureClient> CreateForRawBufferAccess(
+ LayersIPCChannel* aAllocator, gfx::SurfaceFormat aFormat,
+ gfx::IntSize aSize, gfx::BackendType aMoz2dBackend,
+ LayersBackend aLayersBackend, TextureFlags aTextureFlags,
+ TextureAllocationFlags flags = ALLOC_DEFAULT);
+
+ void EnableReadLock();
+ void EnableBlockingReadLock();
+
+ /**
+ * Called once, during the destruction of the Texture, on the thread in which
+ * texture's reference count reaches 0 (could be any thread).
+ *
+ * Here goes the shut-down code that uses virtual methods.
+ * Must only be called by Release().
+ */
+ void Finalize() {}
+
+ friend class AtomicRefCountedWithFinalize<TextureClient>;
+
+ protected:
+ /**
+ * Should only be called *once* per texture, in TextureClient::InitIPDLActor.
+ * Some texture implementations rely on the fact that the descriptor will be
+ * deserialized.
+ * Calling ToSurfaceDescriptor again after it has already returned true,
+ * or never constructing a TextureHost with aDescriptor may result in a memory
+ * leak (see TextureClientD3D9 for example).
+ */
+ bool ToSurfaceDescriptor(SurfaceDescriptor& aDescriptor);
+
+ void LockActor() const;
+ void UnlockActor() const;
+
+ TextureData::Info mInfo;
+
+ RefPtr<LayersIPCChannel> mAllocator;
+ RefPtr<TextureChild> mActor;
+ RefPtr<ITextureClientRecycleAllocator> mRecycleAllocator;
+ RefPtr<TextureReadLock> mReadLock;
+
+ TextureData* mData;
+ RefPtr<gfx::DrawTarget> mBorrowedDrawTarget;
+
+ TextureFlags mFlags;
+
+ gl::GfxTextureWasteTracker mWasteTracker;
+
+ OpenMode mOpenMode;
+#ifdef DEBUG
+ uint32_t mExpectedDtRefs;
+#endif
+ bool mIsLocked;
+ bool mIsReadLocked;
+ // This member tracks that the texture was written into until the update
+ // is sent to the compositor. We need this remember to lock mReadLock on
+ // behalf of the compositor just before sending the notification.
+ bool mUpdated;
+
+ // Used when TextureClient is recycled with TextureFlags::RECYCLE flag.
+ bool mAddedToCompositableClient;
+
+ uint64_t mFwdTransactionId;
+
+ // Serial id of TextureClient. It is unique in current process.
+ const uint64_t mSerial;
+
+ // When non-zero, texture data must not be freed.
+ mozilla::Atomic<uintptr_t> mPaintThreadRefs;
+
+ // External image id. It is unique if it is allocated.
+ // The id is allocated in TextureClient::InitIPDLActor().
+ // Its allocation is supported by
+ // CompositorBridgeChild and ImageBridgeChild for now.
+ wr::MaybeExternalImageId mExternalImageId;
+
+ // Used to assign serial ids of TextureClient.
+ static mozilla::Atomic<uint64_t> sSerialCounter;
+
+ friend class TextureChild;
+ friend void TestTextureClientSurface(TextureClient*, gfxImageSurface*);
+ friend void TestTextureClientYCbCr(TextureClient*, PlanarYCbCrData&);
+ friend already_AddRefed<TextureHost> CreateTextureHostWithBackend(
+ TextureClient*, ISurfaceAllocator*, LayersBackend&);
+
+#ifdef GFX_DEBUG_TRACK_CLIENTS_IN_POOL
+ public:
+ // Pointer to the pool this tile came from.
+ TextureClientPool* mPoolTracker;
+#endif
+};
+
+/**
+ * Task that releases TextureClient pointer on a specified thread.
+ */
+class TextureClientReleaseTask : public Runnable {
+ public:
+ explicit TextureClientReleaseTask(TextureClient* aClient)
+ : Runnable("layers::TextureClientReleaseTask"), mTextureClient(aClient) {}
+
+ NS_IMETHOD Run() override {
+ mTextureClient = nullptr;
+ return NS_OK;
+ }
+
+ private:
+ RefPtr<TextureClient> mTextureClient;
+};
+
+// Automatically lock and unlock a texture. Since texture locking is fallible,
+// Succeeded() must be checked on the guard object before proceeding.
+class MOZ_RAII TextureClientAutoLock {
+ public:
+ TextureClientAutoLock(TextureClient* aTexture, OpenMode aMode)
+ : mTexture(aTexture), mSucceeded(false) {
+ mSucceeded = mTexture->Lock(aMode);
+#ifdef DEBUG
+ mChecked = false;
+#endif
+ }
+ ~TextureClientAutoLock() {
+ MOZ_ASSERT(mChecked);
+ if (mSucceeded) {
+ mTexture->Unlock();
+ }
+ }
+
+ bool Succeeded() {
+#ifdef DEBUG
+ mChecked = true;
+#endif
+ return mSucceeded;
+ }
+
+ private:
+ TextureClient* mTexture;
+#ifdef DEBUG
+ bool mChecked;
+#endif
+ bool mSucceeded;
+};
+
+/// Convenience function to set the content of ycbcr texture.
+bool UpdateYCbCrTextureClient(TextureClient* aTexture,
+ const PlanarYCbCrData& aData);
+
+TextureType PreferredCanvasTextureType(KnowsCompositor* aKnowsCompositor);
+
+} // namespace layers
+} // namespace mozilla
+
+#endif
diff --git a/gfx/layers/client/TextureClientPool.cpp b/gfx/layers/client/TextureClientPool.cpp
new file mode 100644
index 0000000000..1dbef5fb84
--- /dev/null
+++ b/gfx/layers/client/TextureClientPool.cpp
@@ -0,0 +1,307 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "TextureClientPool.h"
+#include "CompositableClient.h"
+#include "mozilla/layers/CompositableForwarder.h"
+#include "mozilla/layers/TextureForwarder.h"
+#include "mozilla/StaticPrefs_layers.h"
+
+#include "nsComponentManagerUtils.h"
+
+#define TCP_LOG(...)
+// #define TCP_LOG(...) printf_stderr(__VA_ARGS__);
+
+namespace mozilla {
+namespace layers {
+
+// We want to shrink to our maximum size of N unused tiles
+// after a timeout to allow for short-term budget requirements
+static void ShrinkCallback(nsITimer* aTimer, void* aClosure) {
+ static_cast<TextureClientPool*>(aClosure)->ShrinkToMaximumSize();
+}
+
+// After a certain amount of inactivity, let's clear the pool so that
+// we don't hold onto tiles needlessly. In general, allocations are
+// cheap enough that re-allocating isn't an issue unless we're allocating
+// at an inopportune time (e.g. mid-animation).
+static void ClearCallback(nsITimer* aTimer, void* aClosure) {
+ static_cast<TextureClientPool*>(aClosure)->Clear();
+}
+
+TextureClientPool::TextureClientPool(
+ KnowsCompositor* aKnowsCompositor, gfx::SurfaceFormat aFormat,
+ gfx::IntSize aSize, TextureFlags aFlags, uint32_t aShrinkTimeoutMsec,
+ uint32_t aClearTimeoutMsec, uint32_t aInitialPoolSize,
+ uint32_t aPoolUnusedSize, TextureForwarder* aAllocator)
+ : mKnowsCompositor(aKnowsCompositor),
+ mFormat(aFormat),
+ mSize(aSize),
+ mFlags(aFlags),
+ mShrinkTimeoutMsec(aShrinkTimeoutMsec),
+ mClearTimeoutMsec(aClearTimeoutMsec),
+ mInitialPoolSize(aInitialPoolSize),
+ mPoolUnusedSize(aPoolUnusedSize),
+ mOutstandingClients(0),
+ mSurfaceAllocator(aAllocator),
+ mDestroyed(false) {
+ TCP_LOG("TexturePool %p created with maximum unused texture clients %u\n",
+ this, mInitialPoolSize);
+ mShrinkTimer = NS_NewTimer();
+ mClearTimer = NS_NewTimer();
+ if (aFormat == gfx::SurfaceFormat::UNKNOWN) {
+ gfxWarning() << "Creating texture pool for SurfaceFormat::UNKNOWN format";
+ }
+}
+
+TextureClientPool::~TextureClientPool() {
+ mShrinkTimer->Cancel();
+ mClearTimer->Cancel();
+}
+
+#ifdef GFX_DEBUG_TRACK_CLIENTS_IN_POOL
+static bool TestClientPool(const char* what, TextureClient* aClient,
+ TextureClientPool* aPool) {
+ if (!aClient || !aPool) {
+ return false;
+ }
+
+ TextureClientPool* actual = aClient->mPoolTracker;
+ bool ok = (actual == aPool);
+ if (ok) {
+ ok = (aClient->GetFormat() == aPool->GetFormat());
+ }
+
+ if (!ok) {
+ if (actual) {
+ gfxCriticalError() << "Pool error(" << what << "): " << aPool << "-"
+ << aPool->GetFormat() << ", " << actual << "-"
+ << actual->GetFormat() << ", " << aClient->GetFormat();
+ MOZ_CRASH("GFX: Crashing with actual");
+ } else {
+ gfxCriticalError() << "Pool error(" << what << "): " << aPool << "-"
+ << aPool->GetFormat() << ", nullptr, "
+ << aClient->GetFormat();
+ MOZ_CRASH("GFX: Crashing without actual");
+ }
+ }
+ return ok;
+}
+#endif
+
+already_AddRefed<TextureClient> TextureClientPool::GetTextureClient() {
+ // Try to fetch a client from the pool
+ RefPtr<TextureClient> textureClient;
+
+ // We initially allocate mInitialPoolSize for our pool. If we run
+ // out of TextureClients, we allocate additional TextureClients to try and
+ // keep around mPoolUnusedSize
+ if (mTextureClients.empty()) {
+ AllocateTextureClient();
+ }
+
+ if (mTextureClients.empty()) {
+ // All our allocations failed, return nullptr
+ return nullptr;
+ }
+
+ mOutstandingClients++;
+ textureClient = mTextureClients.top();
+ mTextureClients.pop();
+#ifdef GFX_DEBUG_TRACK_CLIENTS_IN_POOL
+ if (textureClient) {
+ textureClient->mPoolTracker = this;
+ }
+ DebugOnly<bool> ok = TestClientPool("fetch", textureClient, this);
+ MOZ_ASSERT(ok);
+#endif
+ TCP_LOG("TexturePool %p giving %p from pool; size %u outstanding %u\n", this,
+ textureClient.get(), mTextureClients.size(), mOutstandingClients);
+
+ return textureClient.forget();
+}
+
+void TextureClientPool::AllocateTextureClient() {
+ TCP_LOG("TexturePool %p allocating TextureClient, outstanding %u\n", this,
+ mOutstandingClients);
+
+ TextureAllocationFlags allocFlags = ALLOC_DEFAULT;
+
+ RefPtr<TextureClient> newClient;
+ if (StaticPrefs::layers_force_shmem_tiles_AtStartup()) {
+ // gfx::BackendType::NONE means use the content backend
+ newClient = TextureClient::CreateForRawBufferAccess(
+ mSurfaceAllocator, mFormat, mSize, gfx::BackendType::NONE, GetBackend(),
+ mFlags, allocFlags);
+ } else {
+ newClient = TextureClient::CreateForDrawing(
+ mSurfaceAllocator, mFormat, mSize, mKnowsCompositor,
+ BackendSelector::Content, mFlags, allocFlags);
+ }
+
+ if (newClient) {
+ mTextureClients.push(newClient);
+ }
+}
+
+void TextureClientPool::ResetTimers() {
+ // Shrink down if we're beyond our maximum size
+ if (mShrinkTimeoutMsec &&
+ mTextureClients.size() + mTextureClientsDeferred.size() >
+ mPoolUnusedSize) {
+ TCP_LOG("TexturePool %p scheduling a shrink-to-max-size\n", this);
+ mShrinkTimer->InitWithNamedFuncCallback(
+ ShrinkCallback, this, mShrinkTimeoutMsec, nsITimer::TYPE_ONE_SHOT,
+ "layers::TextureClientPool::ResetTimers");
+ }
+
+ // Clear pool after a period of inactivity to reduce memory consumption
+ if (mClearTimeoutMsec) {
+ TCP_LOG("TexturePool %p scheduling a clear\n", this);
+ mClearTimer->InitWithNamedFuncCallback(
+ ClearCallback, this, mClearTimeoutMsec, nsITimer::TYPE_ONE_SHOT,
+ "layers::TextureClientPool::ResetTimers");
+ }
+}
+
+void TextureClientPool::ReturnTextureClient(TextureClient* aClient) {
+ if (!aClient || mDestroyed) {
+ return;
+ }
+#ifdef GFX_DEBUG_TRACK_CLIENTS_IN_POOL
+ DebugOnly<bool> ok = TestClientPool("return", aClient, this);
+ MOZ_ASSERT(ok);
+#endif
+ // Add the client to the pool:
+ MOZ_ASSERT(mOutstandingClients > mTextureClientsDeferred.size());
+ mOutstandingClients--;
+ mTextureClients.push(aClient);
+ TCP_LOG("TexturePool %p had client %p returned; size %u outstanding %u\n",
+ this, aClient, mTextureClients.size(), mOutstandingClients);
+
+ ResetTimers();
+}
+
+void TextureClientPool::ReturnTextureClientDeferred(TextureClient* aClient) {
+ if (!aClient || mDestroyed) {
+ return;
+ }
+ MOZ_ASSERT(aClient->GetReadLock());
+#ifdef GFX_DEBUG_TRACK_CLIENTS_IN_POOL
+ DebugOnly<bool> ok = TestClientPool("defer", aClient, this);
+ MOZ_ASSERT(ok);
+#endif
+ mTextureClientsDeferred.push_back(aClient);
+ TCP_LOG(
+ "TexturePool %p had client %p defer-returned, size %u outstanding %u\n",
+ this, aClient, mTextureClientsDeferred.size(), mOutstandingClients);
+
+ ResetTimers();
+}
+
+void TextureClientPool::ShrinkToMaximumSize() {
+ // We're over our desired maximum size, immediately shrink down to the
+ // maximum.
+ //
+ // We cull from the deferred TextureClients first, as we can't reuse those
+ // until they get returned.
+ uint32_t totalUnusedTextureClients =
+ mTextureClients.size() + mTextureClientsDeferred.size();
+
+ // If we have > mInitialPoolSize outstanding, then we want to keep around
+ // mPoolUnusedSize at a maximum. If we have fewer than mInitialPoolSize
+ // outstanding, then keep around the entire initial pool size.
+ uint32_t targetUnusedClients;
+ if (mOutstandingClients > mInitialPoolSize) {
+ targetUnusedClients = mPoolUnusedSize;
+ } else {
+ targetUnusedClients = mInitialPoolSize;
+ }
+
+ TCP_LOG(
+ "TexturePool %p shrinking to maximum unused size %u; current pool size "
+ "%u; total outstanding %u\n",
+ this, targetUnusedClients, totalUnusedTextureClients,
+ mOutstandingClients);
+
+ while (totalUnusedTextureClients > targetUnusedClients) {
+ if (!mTextureClientsDeferred.empty()) {
+ mOutstandingClients--;
+ TCP_LOG("TexturePool %p dropped deferred client %p; %u remaining\n", this,
+ mTextureClientsDeferred.front().get(),
+ mTextureClientsDeferred.size() - 1);
+ mTextureClientsDeferred.pop_front();
+ } else {
+ TCP_LOG("TexturePool %p dropped non-deferred client %p; %u remaining\n",
+ this, mTextureClients.top().get(), mTextureClients.size() - 1);
+ mTextureClients.pop();
+ }
+ totalUnusedTextureClients--;
+ }
+}
+
+void TextureClientPool::ReturnDeferredClients() {
+ if (mTextureClientsDeferred.empty()) {
+ return;
+ }
+
+ TCP_LOG("TexturePool %p returning %u deferred clients to pool\n", this,
+ mTextureClientsDeferred.size());
+
+ ReturnUnlockedClients();
+ ShrinkToMaximumSize();
+}
+
+void TextureClientPool::ReturnUnlockedClients() {
+ for (auto it = mTextureClientsDeferred.begin();
+ it != mTextureClientsDeferred.end();) {
+ MOZ_ASSERT((*it)->GetReadLock()->AsNonBlockingLock()->GetReadCount() >= 1);
+ // Last count is held by the lock itself.
+ if (!(*it)->IsReadLocked()) {
+ mTextureClients.push(*it);
+ it = mTextureClientsDeferred.erase(it);
+
+ MOZ_ASSERT(mOutstandingClients > 0);
+ mOutstandingClients--;
+ } else {
+ it++;
+ }
+ }
+}
+
+void TextureClientPool::ReportClientLost() {
+ MOZ_ASSERT(mOutstandingClients > mTextureClientsDeferred.size());
+ mOutstandingClients--;
+ TCP_LOG("TexturePool %p getting report client lost; down to %u outstanding\n",
+ this, mOutstandingClients);
+}
+
+void TextureClientPool::Clear() {
+ TCP_LOG("TexturePool %p getting cleared\n", this);
+ while (!mTextureClients.empty()) {
+ TCP_LOG("TexturePool %p releasing client %p\n", this,
+ mTextureClients.top().get());
+ mTextureClients.pop();
+ }
+ while (!mTextureClientsDeferred.empty()) {
+ MOZ_ASSERT(mOutstandingClients > 0);
+ mOutstandingClients--;
+ TCP_LOG("TexturePool %p releasing deferred client %p\n", this,
+ mTextureClientsDeferred.front().get());
+ mTextureClientsDeferred.pop_front();
+ }
+}
+
+void TextureClientPool::Destroy() {
+ Clear();
+ mDestroyed = true;
+ mInitialPoolSize = 0;
+ mPoolUnusedSize = 0;
+ mKnowsCompositor = nullptr;
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/client/TextureClientPool.h b/gfx/layers/client/TextureClientPool.h
new file mode 100644
index 0000000000..d557ca9a03
--- /dev/null
+++ b/gfx/layers/client/TextureClientPool.h
@@ -0,0 +1,175 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_GFX_TEXTURECLIENTPOOL_H
+#define MOZILLA_GFX_TEXTURECLIENTPOOL_H
+
+#include "mozilla/gfx/Types.h"
+#include "mozilla/gfx/Point.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/layers/KnowsCompositor.h"
+#include "TextureClient.h"
+#include "nsITimer.h"
+#include <stack>
+#include <list>
+
+namespace mozilla {
+namespace layers {
+
+class ISurfaceAllocator;
+class TextureForwarder;
+class TextureReadLock;
+
+class TextureClientAllocator {
+ protected:
+ virtual ~TextureClientAllocator() = default;
+
+ public:
+ NS_INLINE_DECL_REFCOUNTING(TextureClientAllocator)
+
+ virtual already_AddRefed<TextureClient> GetTextureClient() = 0;
+
+ /**
+ * Return a TextureClient that is not yet ready to be reused, but will be
+ * imminently.
+ */
+ virtual void ReturnTextureClientDeferred(TextureClient* aClient) = 0;
+
+ virtual void ReportClientLost() = 0;
+};
+
+class TextureClientPool final : public TextureClientAllocator {
+ virtual ~TextureClientPool();
+
+ public:
+ TextureClientPool(KnowsCompositor* aKnowsCompositor,
+ gfx::SurfaceFormat aFormat, gfx::IntSize aSize,
+ TextureFlags aFlags, uint32_t aShrinkTimeoutMsec,
+ uint32_t aClearTimeoutMsec, uint32_t aInitialPoolSize,
+ uint32_t aPoolUnusedSize, TextureForwarder* aAllocator);
+
+ /**
+ * Gets an allocated TextureClient of size and format that are determined
+ * by the initialisation parameters given to the pool. This will either be
+ * a cached client that was returned to the pool, or a newly allocated
+ * client if one isn't available.
+ *
+ * All clients retrieved by this method should be returned using the return
+ * functions, or reported lost so that the pool can manage its size correctly.
+ */
+ already_AddRefed<TextureClient> GetTextureClient() override;
+
+ /**
+ * Return a TextureClient that is no longer being used and is ready for
+ * immediate re-use or destruction.
+ */
+ void ReturnTextureClient(TextureClient* aClient);
+
+ /**
+ * Return a TextureClient that is not yet ready to be reused, but will be
+ * imminently.
+ */
+ void ReturnTextureClientDeferred(TextureClient* aClient) override;
+
+ /**
+ * Return any clients to the pool that were previously returned in
+ * ReturnTextureClientDeferred.
+ */
+ void ReturnDeferredClients();
+
+ /**
+ * Attempt to shrink the pool so that there are no more than
+ * mInitialPoolSize outstanding.
+ */
+ void ShrinkToMaximumSize();
+
+ /**
+ * Report that a client retrieved via GetTextureClient() has become
+ * unusable, so that it will no longer be tracked.
+ */
+ void ReportClientLost() override;
+
+ /**
+ * Calling this will cause the pool to attempt to relinquish any unused
+ * clients.
+ */
+ void Clear();
+
+ LayersBackend GetBackend() const {
+ return mKnowsCompositor->GetCompositorBackendType();
+ }
+ int32_t GetMaxTextureSize() const {
+ return mKnowsCompositor->GetMaxTextureSize();
+ }
+ gfx::SurfaceFormat GetFormat() { return mFormat; }
+ TextureFlags GetFlags() const { return mFlags; }
+
+ /**
+ * Clear the pool and put it in a state where it won't recycle any new
+ * texture.
+ */
+ void Destroy();
+
+ private:
+ void ReturnUnlockedClients();
+
+ /// Allocate a single TextureClient to be returned from the pool.
+ void AllocateTextureClient();
+
+ /// Reset and/or initialise timers for shrinking/clearing the pool.
+ void ResetTimers();
+
+ /// KnowsCompositor passed to the TextureClient for buffer creation.
+ RefPtr<KnowsCompositor> mKnowsCompositor;
+
+ /// Format is passed to the TextureClient for buffer creation.
+ gfx::SurfaceFormat mFormat;
+
+ /// The width and height of the tiles to be used.
+ gfx::IntSize mSize;
+
+ /// Flags passed to the TextureClient for buffer creation.
+ const TextureFlags mFlags;
+
+ /// How long to wait after a TextureClient is returned before trying
+ /// to shrink the pool to its maximum size of mPoolUnusedSize.
+ uint32_t mShrinkTimeoutMsec;
+
+ /// How long to wait after a TextureClient is returned before trying
+ /// to clear the pool.
+ uint32_t mClearTimeoutMsec;
+
+ // The initial number of unused texture clients to seed the pool with
+ // on construction
+ uint32_t mInitialPoolSize;
+
+ // How many unused texture clients to try and keep around if we go over
+ // the initial allocation
+ uint32_t mPoolUnusedSize;
+
+ /// This is a total number of clients in the wild and in the stack of
+ /// deferred clients (see below). So, the total number of clients in
+ /// existence is always mOutstandingClients + the size of mTextureClients.
+ uint32_t mOutstandingClients;
+
+ std::stack<RefPtr<TextureClient>> mTextureClients;
+
+ std::list<RefPtr<TextureClient>> mTextureClientsDeferred;
+ RefPtr<nsITimer> mShrinkTimer;
+ RefPtr<nsITimer> mClearTimer;
+ // This mSurfaceAllocator owns us, so no need to hold a ref to it
+ TextureForwarder* mSurfaceAllocator;
+
+ // Keep track of whether this pool has been destroyed or not. If it has,
+ // we won't accept returns of TextureClients anymore, and the refcounting
+ // should take care of their destruction.
+ bool mDestroyed;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif /* MOZILLA_GFX_TEXTURECLIENTPOOL_H */
diff --git a/gfx/layers/client/TextureClientRecycleAllocator.cpp b/gfx/layers/client/TextureClientRecycleAllocator.cpp
new file mode 100644
index 0000000000..a88e272330
--- /dev/null
+++ b/gfx/layers/client/TextureClientRecycleAllocator.cpp
@@ -0,0 +1,261 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "gfxPlatform.h"
+#include "ImageContainer.h"
+#include "mozilla/layers/BufferTexture.h"
+#include "mozilla/layers/ISurfaceAllocator.h"
+#include "mozilla/layers/TextureForwarder.h"
+#include "TextureClientRecycleAllocator.h"
+
+namespace mozilla {
+namespace layers {
+
+// Used to keep TextureClient's reference count stable as not to disrupt
+// recycling.
+class TextureClientHolder final {
+ ~TextureClientHolder() = default;
+
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(TextureClientHolder)
+
+ explicit TextureClientHolder(TextureClient* aClient)
+ : mTextureClient(aClient), mWillRecycle(true) {}
+
+ TextureClient* GetTextureClient() { return mTextureClient; }
+
+ bool WillRecycle() { return mWillRecycle; }
+
+ void ClearWillRecycle() { mWillRecycle = false; }
+
+ void ClearTextureClient() { mTextureClient = nullptr; }
+
+ protected:
+ RefPtr<TextureClient> mTextureClient;
+ bool mWillRecycle;
+};
+
+class MOZ_RAII DefaultTextureClientAllocationHelper
+ : public ITextureClientAllocationHelper {
+ public:
+ DefaultTextureClientAllocationHelper(
+ TextureClientRecycleAllocator* aAllocator, gfx::SurfaceFormat aFormat,
+ gfx::IntSize aSize, BackendSelector aSelector, TextureFlags aTextureFlags,
+ TextureAllocationFlags aAllocationFlags)
+ : ITextureClientAllocationHelper(aFormat, aSize, aSelector, aTextureFlags,
+ aAllocationFlags),
+ mAllocator(aAllocator) {}
+
+ bool IsCompatible(TextureClient* aTextureClient) override {
+ if (aTextureClient->GetFormat() != mFormat ||
+ aTextureClient->GetSize() != mSize) {
+ return false;
+ }
+ return true;
+ }
+
+ already_AddRefed<TextureClient> Allocate(
+ KnowsCompositor* aKnowsCompositor) override {
+ return mAllocator->Allocate(mFormat, mSize, mSelector, mTextureFlags,
+ mAllocationFlags);
+ }
+
+ protected:
+ TextureClientRecycleAllocator* mAllocator;
+};
+
+YCbCrTextureClientAllocationHelper::YCbCrTextureClientAllocationHelper(
+ const PlanarYCbCrData& aData, const gfx::IntSize& aYSize,
+ const gfx::IntSize& aCbCrSize, TextureFlags aTextureFlags)
+ : ITextureClientAllocationHelper(gfx::SurfaceFormat::YUV, aYSize,
+ BackendSelector::Content, aTextureFlags,
+ ALLOC_DEFAULT),
+ mData(aData),
+ mYSize(aYSize),
+ mCbCrSize(aCbCrSize) {}
+
+YCbCrTextureClientAllocationHelper::YCbCrTextureClientAllocationHelper(
+ const PlanarYCbCrData& aData, TextureFlags aTextureFlags)
+ : YCbCrTextureClientAllocationHelper(aData, aData.YDataSize(),
+ aData.CbCrDataSize(), aTextureFlags) {}
+
+bool YCbCrTextureClientAllocationHelper::IsCompatible(
+ TextureClient* aTextureClient) {
+ MOZ_ASSERT(aTextureClient->GetFormat() == gfx::SurfaceFormat::YUV);
+
+ BufferTextureData* bufferData =
+ aTextureClient->GetInternalData()->AsBufferTextureData();
+
+ if (!bufferData ||
+ !bufferData->GetPictureRect().IsEqualEdges(mData.mPictureRect) ||
+ bufferData->GetYSize().isNothing() ||
+ bufferData->GetYSize().ref() != mYSize ||
+ bufferData->GetCbCrSize().isNothing() ||
+ bufferData->GetCbCrSize().ref() != mCbCrSize ||
+ bufferData->GetYStride().isNothing() ||
+ bufferData->GetYStride().ref() != mData.mYStride ||
+ bufferData->GetCbCrStride().isNothing() ||
+ bufferData->GetCbCrStride().ref() != mData.mCbCrStride ||
+ bufferData->GetYUVColorSpace().isNothing() ||
+ bufferData->GetYUVColorSpace().ref() != mData.mYUVColorSpace ||
+ bufferData->GetColorDepth().isNothing() ||
+ bufferData->GetColorDepth().ref() != mData.mColorDepth ||
+ bufferData->GetStereoMode().isNothing() ||
+ bufferData->GetStereoMode().ref() != mData.mStereoMode ||
+ bufferData->GetChromaSubsampling().isNothing() ||
+ bufferData->GetChromaSubsampling().ref() != mData.mChromaSubsampling) {
+ return false;
+ }
+ return true;
+}
+
+already_AddRefed<TextureClient> YCbCrTextureClientAllocationHelper::Allocate(
+ KnowsCompositor* aKnowsCompositor) {
+ return TextureClient::CreateForYCbCr(
+ aKnowsCompositor, mData.mPictureRect, mYSize, mData.mYStride, mCbCrSize,
+ mData.mCbCrStride, mData.mStereoMode, mData.mColorDepth,
+ mData.mYUVColorSpace, mData.mColorRange, mData.mChromaSubsampling,
+ mTextureFlags);
+}
+
+TextureClientRecycleAllocator::TextureClientRecycleAllocator(
+ KnowsCompositor* aKnowsCompositor)
+ : mKnowsCompositor(aKnowsCompositor),
+ mMaxPooledSize(kMaxPooledSized),
+ mLock("TextureClientRecycleAllocatorImp.mLock"),
+ mIsDestroyed(false) {}
+
+TextureClientRecycleAllocator::~TextureClientRecycleAllocator() {
+ MutexAutoLock lock(mLock);
+ while (!mPooledClients.empty()) {
+ mPooledClients.pop();
+ }
+ MOZ_ASSERT(mInUseClients.empty());
+}
+
+void TextureClientRecycleAllocator::SetMaxPoolSize(uint32_t aMax) {
+ mMaxPooledSize = aMax;
+}
+
+already_AddRefed<TextureClient> TextureClientRecycleAllocator::CreateOrRecycle(
+ gfx::SurfaceFormat aFormat, gfx::IntSize aSize, BackendSelector aSelector,
+ TextureFlags aTextureFlags, TextureAllocationFlags aAllocFlags) {
+ MOZ_ASSERT(!(aTextureFlags & TextureFlags::RECYCLE));
+ DefaultTextureClientAllocationHelper helper(this, aFormat, aSize, aSelector,
+ aTextureFlags, aAllocFlags);
+ return CreateOrRecycle(helper);
+}
+
+already_AddRefed<TextureClient> TextureClientRecycleAllocator::CreateOrRecycle(
+ ITextureClientAllocationHelper& aHelper) {
+ MOZ_ASSERT(aHelper.mTextureFlags & TextureFlags::RECYCLE);
+
+ RefPtr<TextureClientHolder> textureHolder;
+
+ {
+ MutexAutoLock lock(mLock);
+ if (mIsDestroyed || !mKnowsCompositor->GetTextureForwarder()) {
+ return nullptr;
+ }
+ if (!mPooledClients.empty()) {
+ textureHolder = mPooledClients.top();
+ mPooledClients.pop();
+ // If the texture's allocator is not open or a pooled TextureClient is
+ // not compatible, release it.
+ if (!textureHolder->GetTextureClient()->GetAllocator()->IPCOpen() ||
+ !aHelper.IsCompatible(textureHolder->GetTextureClient())) {
+ // Release TextureClient.
+ RefPtr<Runnable> task =
+ new TextureClientReleaseTask(textureHolder->GetTextureClient());
+ textureHolder->ClearTextureClient();
+ textureHolder = nullptr;
+ mKnowsCompositor->GetTextureForwarder()->GetThread()->Dispatch(
+ task.forget());
+ } else {
+ textureHolder->GetTextureClient()->RecycleTexture(
+ aHelper.mTextureFlags);
+ }
+ }
+ }
+
+ if (!textureHolder) {
+ // Allocate new TextureClient
+ RefPtr<TextureClient> texture = aHelper.Allocate(mKnowsCompositor);
+ if (!texture) {
+ return nullptr;
+ }
+ textureHolder = new TextureClientHolder(texture);
+ }
+
+ {
+ MutexAutoLock lock(mLock);
+ MOZ_ASSERT(mInUseClients.find(textureHolder->GetTextureClient()) ==
+ mInUseClients.end());
+ // Register TextureClient
+ mInUseClients[textureHolder->GetTextureClient()] = textureHolder;
+ }
+ RefPtr<TextureClient> client(textureHolder->GetTextureClient());
+
+ // Make sure the texture holds a reference to us, and ask it to call
+ // RecycleTextureClient when its ref count drops to 1.
+ client->SetRecycleAllocator(this);
+ return client.forget();
+}
+
+already_AddRefed<TextureClient> TextureClientRecycleAllocator::Allocate(
+ gfx::SurfaceFormat aFormat, gfx::IntSize aSize, BackendSelector aSelector,
+ TextureFlags aTextureFlags, TextureAllocationFlags aAllocFlags) {
+ return TextureClient::CreateForDrawing(mKnowsCompositor, aFormat, aSize,
+ aSelector, aTextureFlags, aAllocFlags);
+}
+
+void TextureClientRecycleAllocator::ShrinkToMinimumSize() {
+ MutexAutoLock lock(mLock);
+ while (!mPooledClients.empty()) {
+ mPooledClients.pop();
+ }
+ // We can not clear using TextureClients safely.
+ // Just clear WillRecycle here.
+ std::map<TextureClient*, RefPtr<TextureClientHolder> >::iterator it;
+ for (it = mInUseClients.begin(); it != mInUseClients.end(); it++) {
+ RefPtr<TextureClientHolder> holder = it->second;
+ holder->ClearWillRecycle();
+ }
+}
+
+void TextureClientRecycleAllocator::Destroy() {
+ MutexAutoLock lock(mLock);
+ while (!mPooledClients.empty()) {
+ mPooledClients.pop();
+ }
+ mIsDestroyed = true;
+}
+
+void TextureClientRecycleAllocator::RecycleTextureClient(
+ TextureClient* aClient) {
+ // Clearing the recycle allocator drops a reference, so make sure we stay
+ // alive for the duration of this function.
+ RefPtr<TextureClientRecycleAllocator> kungFuDeathGrip(this);
+ aClient->SetRecycleAllocator(nullptr);
+
+ RefPtr<TextureClientHolder> textureHolder;
+ {
+ MutexAutoLock lock(mLock);
+ if (mInUseClients.find(aClient) != mInUseClients.end()) {
+ textureHolder =
+ mInUseClients[aClient]; // Keep reference count of
+ // TextureClientHolder within lock.
+ if (textureHolder->WillRecycle() && !mIsDestroyed &&
+ mPooledClients.size() < mMaxPooledSize) {
+ mPooledClients.push(textureHolder);
+ }
+ mInUseClients.erase(aClient);
+ }
+ }
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/client/TextureClientRecycleAllocator.h b/gfx/layers/client/TextureClientRecycleAllocator.h
new file mode 100644
index 0000000000..4abaa60c4e
--- /dev/null
+++ b/gfx/layers/client/TextureClientRecycleAllocator.h
@@ -0,0 +1,140 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_GFX_TEXTURECLIENT_RECYCLE_ALLOCATOR_H
+#define MOZILLA_GFX_TEXTURECLIENT_RECYCLE_ALLOCATOR_H
+
+#include <map>
+#include <stack>
+#include "mozilla/gfx/Types.h"
+#include "mozilla/layers/TextureForwarder.h"
+#include "mozilla/RefPtr.h"
+#include "TextureClient.h"
+#include "mozilla/Mutex.h"
+
+namespace mozilla {
+namespace layers {
+
+class TextureClientHolder;
+struct PlanarYCbCrData;
+
+class ITextureClientRecycleAllocator {
+ protected:
+ virtual ~ITextureClientRecycleAllocator() = default;
+
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(ITextureClientRecycleAllocator)
+
+ protected:
+ friend class TextureClient;
+ virtual void RecycleTextureClient(TextureClient* aClient) = 0;
+};
+
+class ITextureClientAllocationHelper {
+ public:
+ ITextureClientAllocationHelper(gfx::SurfaceFormat aFormat, gfx::IntSize aSize,
+ BackendSelector aSelector,
+ TextureFlags aTextureFlags,
+ TextureAllocationFlags aAllocationFlags)
+ : mFormat(aFormat),
+ mSize(aSize),
+ mSelector(aSelector),
+ mTextureFlags(aTextureFlags |
+ TextureFlags::RECYCLE) // Set recycle flag
+ ,
+ mAllocationFlags(aAllocationFlags) {}
+
+ virtual already_AddRefed<TextureClient> Allocate(
+ KnowsCompositor* aKnowsCompositor) = 0;
+ virtual bool IsCompatible(TextureClient* aTextureClient) = 0;
+
+ const gfx::SurfaceFormat mFormat;
+ const gfx::IntSize mSize;
+ const BackendSelector mSelector;
+ const TextureFlags mTextureFlags;
+ const TextureAllocationFlags mAllocationFlags;
+};
+
+class MOZ_RAII YCbCrTextureClientAllocationHelper
+ : public ITextureClientAllocationHelper {
+ public:
+ YCbCrTextureClientAllocationHelper(const PlanarYCbCrData& aData,
+ const gfx::IntSize& aYSize,
+ const gfx::IntSize& aCbCrSize,
+ TextureFlags aTextureFlags);
+
+ YCbCrTextureClientAllocationHelper(const PlanarYCbCrData& aData,
+ TextureFlags aTextureFlags);
+
+ bool IsCompatible(TextureClient* aTextureClient) override;
+
+ already_AddRefed<TextureClient> Allocate(
+ KnowsCompositor* aKnowsCompositor) override;
+
+ protected:
+ const PlanarYCbCrData& mData;
+ const gfx::IntSize mYSize;
+ const gfx::IntSize mCbCrSize;
+};
+
+/**
+ * TextureClientRecycleAllocator provides TextureClients allocation and
+ * recycling capabilities. It expects allocations of same sizes and
+ * attributres. If a recycled TextureClient is different from
+ * requested one, the recycled one is dropped and new TextureClient is
+ * allocated.
+ *
+ * By default this uses TextureClient::CreateForDrawing to allocate new texture
+ * clients.
+ */
+class TextureClientRecycleAllocator : public ITextureClientRecycleAllocator {
+ protected:
+ virtual ~TextureClientRecycleAllocator();
+
+ public:
+ explicit TextureClientRecycleAllocator(KnowsCompositor* aKnowsCompositor);
+
+ void SetMaxPoolSize(uint32_t aMax);
+
+ // Creates and allocates a TextureClient.
+ already_AddRefed<TextureClient> CreateOrRecycle(
+ gfx::SurfaceFormat aFormat, gfx::IntSize aSize, BackendSelector aSelector,
+ TextureFlags aTextureFlags, TextureAllocationFlags flags = ALLOC_DEFAULT);
+
+ already_AddRefed<TextureClient> CreateOrRecycle(
+ ITextureClientAllocationHelper& aHelper);
+
+ void ShrinkToMinimumSize();
+
+ void Destroy();
+
+ KnowsCompositor* GetKnowsCompositor() { return mKnowsCompositor; }
+
+ protected:
+ virtual already_AddRefed<TextureClient> Allocate(
+ gfx::SurfaceFormat aFormat, gfx::IntSize aSize, BackendSelector aSelector,
+ TextureFlags aTextureFlags, TextureAllocationFlags aAllocFlags);
+
+ const RefPtr<KnowsCompositor> mKnowsCompositor;
+
+ friend class DefaultTextureClientAllocationHelper;
+ void RecycleTextureClient(TextureClient* aClient) override;
+
+ static const uint32_t kMaxPooledSized = 2;
+ uint32_t mMaxPooledSize;
+
+ std::map<TextureClient*, RefPtr<TextureClientHolder> > mInUseClients;
+
+ // stack is good from Graphics cache usage point of view.
+ std::stack<RefPtr<TextureClientHolder> > mPooledClients;
+ Mutex mLock MOZ_UNANNOTATED;
+ bool mIsDestroyed;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif /* MOZILLA_GFX_TEXTURECLIENT_RECYCLE_ALLOCATOR_H */
diff --git a/gfx/layers/client/TextureClientSharedSurface.cpp b/gfx/layers/client/TextureClientSharedSurface.cpp
new file mode 100644
index 0000000000..63b33335d4
--- /dev/null
+++ b/gfx/layers/client/TextureClientSharedSurface.cpp
@@ -0,0 +1,92 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "TextureClientSharedSurface.h"
+
+#include "GLContext.h"
+#include "mozilla/gfx/2D.h"
+#include "mozilla/gfx/Logging.h" // for gfxDebug
+#include "mozilla/layers/ISurfaceAllocator.h"
+#include "mozilla/Unused.h"
+#include "nsThreadUtils.h"
+#include "SharedSurface.h"
+
+#ifdef MOZ_WIDGET_ANDROID
+# include "mozilla/layers/AndroidHardwareBuffer.h"
+#endif
+
+using namespace mozilla::gl;
+
+namespace mozilla {
+namespace layers {
+
+/* static */
+already_AddRefed<TextureClient> SharedSurfaceTextureData::CreateTextureClient(
+ const layers::SurfaceDescriptor& aDesc, const gfx::SurfaceFormat aFormat,
+ gfx::IntSize aSize, TextureFlags aFlags, LayersIPCChannel* aAllocator) {
+ auto data = MakeUnique<SharedSurfaceTextureData>(aDesc, aFormat, aSize);
+ return TextureClient::CreateWithData(data.release(), aFlags, aAllocator);
+}
+
+SharedSurfaceTextureData::SharedSurfaceTextureData(
+ const SurfaceDescriptor& desc, const gfx::SurfaceFormat format,
+ const gfx::IntSize size)
+ : mDesc(desc), mFormat(format), mSize(size) {}
+
+SharedSurfaceTextureData::~SharedSurfaceTextureData() = default;
+
+void SharedSurfaceTextureData::Deallocate(LayersIPCChannel*) {}
+
+void SharedSurfaceTextureData::FillInfo(TextureData::Info& aInfo) const {
+ aInfo.size = mSize;
+ aInfo.format = mFormat;
+ aInfo.hasSynchronization = false;
+ aInfo.supportsMoz2D = false;
+ aInfo.canExposeMappedData = false;
+}
+
+bool SharedSurfaceTextureData::Serialize(SurfaceDescriptor& aOutDescriptor) {
+ aOutDescriptor = mDesc;
+ return true;
+}
+
+TextureFlags SharedSurfaceTextureData::GetTextureFlags() const {
+ TextureFlags flags = TextureFlags::NO_FLAGS;
+ return flags;
+}
+
+Maybe<uint64_t> SharedSurfaceTextureData::GetBufferId() const {
+#ifdef MOZ_WIDGET_ANDROID
+ if (mDesc.type() ==
+ SurfaceDescriptor::TSurfaceDescriptorAndroidHardwareBuffer) {
+ const SurfaceDescriptorAndroidHardwareBuffer& desc =
+ mDesc.get_SurfaceDescriptorAndroidHardwareBuffer();
+ return Some(desc.bufferId());
+ }
+#endif
+ return Nothing();
+}
+
+mozilla::ipc::FileDescriptor SharedSurfaceTextureData::GetAcquireFence() {
+#ifdef MOZ_WIDGET_ANDROID
+ if (mDesc.type() ==
+ SurfaceDescriptor::TSurfaceDescriptorAndroidHardwareBuffer) {
+ const SurfaceDescriptorAndroidHardwareBuffer& desc =
+ mDesc.get_SurfaceDescriptorAndroidHardwareBuffer();
+ RefPtr<AndroidHardwareBuffer> buffer =
+ AndroidHardwareBufferManager::Get()->GetBuffer(desc.bufferId());
+ if (!buffer) {
+ return ipc::FileDescriptor();
+ }
+
+ return buffer->GetAcquireFence();
+ }
+#endif
+ return ipc::FileDescriptor();
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/client/TextureClientSharedSurface.h b/gfx/layers/client/TextureClientSharedSurface.h
new file mode 100644
index 0000000000..913afed848
--- /dev/null
+++ b/gfx/layers/client/TextureClientSharedSurface.h
@@ -0,0 +1,68 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_GFX_TEXTURECLIENT_SHAREDSURFACE_H
+#define MOZILLA_GFX_TEXTURECLIENT_SHAREDSURFACE_H
+
+#include <cstddef> // for size_t
+#include <stdint.h> // for uint32_t, uint8_t, uint64_t
+#include "GLContextTypes.h" // for GLContext (ptr only), etc
+#include "TextureClient.h"
+#include "mozilla/Assertions.h" // for MOZ_ASSERT, etc
+#include "mozilla/RefPtr.h" // for RefPtr, RefCounted
+#include "mozilla/gfx/Point.h" // for IntSize
+#include "mozilla/gfx/Types.h" // for SurfaceFormat
+#include "mozilla/layers/CompositorTypes.h" // for TextureFlags, etc
+#include "mozilla/layers/LayersSurfaces.h" // for SurfaceDescriptor
+
+namespace mozilla {
+namespace gl {
+class GLContext;
+class SharedSurface;
+class SurfaceFactory;
+} // namespace gl
+
+namespace layers {
+
+class SharedSurfaceTextureClient;
+
+class SharedSurfaceTextureData : public TextureData {
+ friend class SharedSurfaceTextureClient;
+
+ public:
+ const SurfaceDescriptor mDesc;
+ const gfx::SurfaceFormat mFormat;
+ const gfx::IntSize mSize;
+
+ static already_AddRefed<TextureClient> CreateTextureClient(
+ const layers::SurfaceDescriptor& aDesc, const gfx::SurfaceFormat aFormat,
+ gfx::IntSize aSize, TextureFlags aFlags, LayersIPCChannel* aAllocator);
+
+ SharedSurfaceTextureData(const SurfaceDescriptor&, gfx::SurfaceFormat,
+ gfx::IntSize);
+ virtual ~SharedSurfaceTextureData();
+
+ bool Lock(OpenMode) override { return false; }
+
+ void Unlock() override {}
+
+ void FillInfo(TextureData::Info& aInfo) const override;
+
+ bool Serialize(SurfaceDescriptor& aOutDescriptor) override;
+
+ void Deallocate(LayersIPCChannel*) override;
+
+ TextureFlags GetTextureFlags() const override;
+
+ Maybe<uint64_t> GetBufferId() const override;
+
+ mozilla::ipc::FileDescriptor GetAcquireFence() override;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // MOZILLA_GFX_TEXTURECLIENT_SHAREDSURFACE_H
diff --git a/gfx/layers/client/TextureRecorded.cpp b/gfx/layers/client/TextureRecorded.cpp
new file mode 100644
index 0000000000..3a88a6addd
--- /dev/null
+++ b/gfx/layers/client/TextureRecorded.cpp
@@ -0,0 +1,122 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "TextureRecorded.h"
+
+#include "RecordedCanvasEventImpl.h"
+
+namespace mozilla {
+namespace layers {
+
+// The texture ID is used in the GPU process both to lookup the real texture in
+// the canvas threads and to lookup the SurfaceDescriptor for that texture in
+// the compositor thread. It is therefore important that the ID is unique (per
+// recording process), otherwise an old descriptor can be picked up. This means
+// we can't use the pointer in the recording process as an ID like we do for
+// other objects.
+static int64_t sNextRecordedTextureId = 0;
+
+RecordedTextureData::RecordedTextureData(
+ already_AddRefed<CanvasChild> aCanvasChild, gfx::IntSize aSize,
+ gfx::SurfaceFormat aFormat, TextureType aTextureType)
+ : mCanvasChild(aCanvasChild), mSize(aSize), mFormat(aFormat) {
+ mCanvasChild->EnsureRecorder(aTextureType);
+}
+
+RecordedTextureData::~RecordedTextureData() {
+ // We need the translator to drop its reference for the DrawTarget first,
+ // because the TextureData might need to destroy its DrawTarget within a lock.
+ mDT = nullptr;
+ mCanvasChild->RecordEvent(RecordedTextureDestruction(mTextureId));
+}
+
+void RecordedTextureData::FillInfo(TextureData::Info& aInfo) const {
+ aInfo.size = mSize;
+ aInfo.format = mFormat;
+ aInfo.supportsMoz2D = true;
+ aInfo.hasSynchronization = true;
+}
+
+bool RecordedTextureData::Lock(OpenMode aMode) {
+ mCanvasChild->EnsureBeginTransaction();
+ if (!mDT) {
+ mTextureId = sNextRecordedTextureId++;
+ mCanvasChild->RecordEvent(RecordedNextTextureId(mTextureId));
+ mDT = mCanvasChild->CreateDrawTarget(mSize, mFormat);
+ if (!mDT) {
+ return false;
+ }
+
+ // We lock the TextureData when we create it to get the remote DrawTarget.
+ mCanvasChild->OnTextureWriteLock();
+ mLockedMode = aMode;
+ return true;
+ }
+
+ mCanvasChild->RecordEvent(RecordedTextureLock(mTextureId, aMode));
+ if (aMode & OpenMode::OPEN_WRITE) {
+ mCanvasChild->OnTextureWriteLock();
+ }
+ mLockedMode = aMode;
+ return true;
+}
+
+void RecordedTextureData::Unlock() {
+ if ((mLockedMode == OpenMode::OPEN_READ_WRITE) &&
+ mCanvasChild->ShouldCacheDataSurface()) {
+ mSnapshot = mDT->Snapshot();
+ mDT->DetachAllSnapshots();
+ mCanvasChild->RecordEvent(RecordedCacheDataSurface(mSnapshot.get()));
+ }
+
+ mCanvasChild->RecordEvent(RecordedTextureUnlock(mTextureId));
+ mLockedMode = OpenMode::OPEN_NONE;
+}
+
+already_AddRefed<gfx::DrawTarget> RecordedTextureData::BorrowDrawTarget() {
+ mSnapshot = nullptr;
+ return do_AddRef(mDT);
+}
+
+void RecordedTextureData::EndDraw() {
+ MOZ_ASSERT(mDT->hasOneRef());
+ MOZ_ASSERT(mLockedMode == OpenMode::OPEN_READ_WRITE);
+
+ if (mCanvasChild->ShouldCacheDataSurface()) {
+ mSnapshot = mDT->Snapshot();
+ mCanvasChild->RecordEvent(RecordedCacheDataSurface(mSnapshot.get()));
+ }
+}
+
+already_AddRefed<gfx::SourceSurface> RecordedTextureData::BorrowSnapshot() {
+ MOZ_ASSERT(mDT);
+
+ if (mSnapshot) {
+ return mCanvasChild->WrapSurface(mSnapshot);
+ }
+
+ return mCanvasChild->WrapSurface(mDT->Snapshot());
+}
+
+void RecordedTextureData::Deallocate(LayersIPCChannel* aAllocator) {}
+
+bool RecordedTextureData::Serialize(SurfaceDescriptor& aDescriptor) {
+ aDescriptor = SurfaceDescriptorRecorded(mTextureId);
+ return true;
+}
+
+void RecordedTextureData::OnForwardedToHost() {
+ mCanvasChild->OnTextureForwarded();
+}
+
+TextureFlags RecordedTextureData::GetTextureFlags() const {
+ // With WebRender, resource open happens asynchronously on RenderThread.
+ // Use WAIT_HOST_USAGE_END to keep TextureClient alive during host side usage.
+ return TextureFlags::WAIT_HOST_USAGE_END;
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/client/TextureRecorded.h b/gfx/layers/client/TextureRecorded.h
new file mode 100644
index 0000000000..d393d90821
--- /dev/null
+++ b/gfx/layers/client/TextureRecorded.h
@@ -0,0 +1,60 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_TextureRecorded_h
+#define mozilla_layers_TextureRecorded_h
+
+#include "TextureClient.h"
+#include "mozilla/layers/CanvasChild.h"
+#include "mozilla/layers/LayersTypes.h"
+
+namespace mozilla {
+namespace layers {
+
+class RecordedTextureData final : public TextureData {
+ public:
+ RecordedTextureData(already_AddRefed<CanvasChild> aCanvasChild,
+ gfx::IntSize aSize, gfx::SurfaceFormat aFormat,
+ TextureType aTextureType);
+
+ void FillInfo(TextureData::Info& aInfo) const final;
+
+ bool Lock(OpenMode aMode) final;
+
+ void Unlock() final;
+
+ already_AddRefed<gfx::DrawTarget> BorrowDrawTarget() final;
+
+ void EndDraw() final;
+
+ already_AddRefed<gfx::SourceSurface> BorrowSnapshot() final;
+
+ void Deallocate(LayersIPCChannel* aAllocator) final;
+
+ bool Serialize(SurfaceDescriptor& aDescriptor) final;
+
+ void OnForwardedToHost() final;
+
+ TextureFlags GetTextureFlags() const final;
+
+ private:
+ DISALLOW_COPY_AND_ASSIGN(RecordedTextureData);
+
+ ~RecordedTextureData() override;
+
+ int64_t mTextureId;
+ RefPtr<CanvasChild> mCanvasChild;
+ gfx::IntSize mSize;
+ gfx::SurfaceFormat mFormat;
+ RefPtr<gfx::DrawTarget> mDT;
+ RefPtr<gfx::SourceSurface> mSnapshot;
+ OpenMode mLockedMode;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_TextureRecorded_h
diff --git a/gfx/layers/composite/CompositableHost.cpp b/gfx/layers/composite/CompositableHost.cpp
new file mode 100644
index 0000000000..a64f55d716
--- /dev/null
+++ b/gfx/layers/composite/CompositableHost.cpp
@@ -0,0 +1,69 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "CompositableHost.h"
+#include <map> // for _Rb_tree_iterator, map, etc
+#include <utility> // for pair
+#include "Effects.h" // for EffectMask, Effect, etc
+#include "gfxUtils.h"
+#include "mozilla/gfx/gfxVars.h"
+#include "mozilla/layers/LayersSurfaces.h" // for SurfaceDescriptor
+#include "mozilla/layers/TextureHost.h" // for TextureHost, etc
+#include "mozilla/layers/WebRenderImageHost.h"
+#include "mozilla/RefPtr.h" // for nsRefPtr
+#include "nsDebug.h" // for NS_WARNING
+#include "nsISupportsImpl.h" // for MOZ_COUNT_CTOR, etc
+#include "gfxPlatform.h" // for gfxPlatform
+#include "IPDLActor.h"
+
+namespace mozilla {
+
+using namespace gfx;
+
+namespace layers {
+
+class Compositor;
+
+CompositableHost::CompositableHost(const TextureInfo& aTextureInfo)
+ : mTextureInfo(aTextureInfo), mCompositorBridgeID(0) {
+ MOZ_COUNT_CTOR(CompositableHost);
+}
+
+CompositableHost::~CompositableHost() { MOZ_COUNT_DTOR(CompositableHost); }
+
+void CompositableHost::UseTextureHost(const nsTArray<TimedTexture>& aTextures) {
+}
+
+void CompositableHost::RemoveTextureHost(TextureHost* aTexture) {}
+
+/* static */
+already_AddRefed<CompositableHost> CompositableHost::Create(
+ const TextureInfo& aTextureInfo) {
+ RefPtr<CompositableHost> result;
+ switch (aTextureInfo.mCompositableType) {
+ case CompositableType::IMAGE:
+ result = new WebRenderImageHost(aTextureInfo);
+ break;
+ default:
+ NS_ERROR("Unknown CompositableType");
+ }
+ return result.forget();
+}
+
+void CompositableHost::DumpTextureHost(std::stringstream& aStream,
+ TextureHost* aTexture) {
+ if (!aTexture) {
+ return;
+ }
+ RefPtr<gfx::DataSourceSurface> dSurf = aTexture->GetAsSurface();
+ if (!dSurf) {
+ return;
+ }
+ aStream << gfxUtils::GetAsDataURI(dSurf).get();
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/composite/CompositableHost.h b/gfx/layers/composite/CompositableHost.h
new file mode 100644
index 0000000000..d34e3677ba
--- /dev/null
+++ b/gfx/layers/composite/CompositableHost.h
@@ -0,0 +1,126 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_GFX_BUFFERHOST_H
+#define MOZILLA_GFX_BUFFERHOST_H
+
+#include <stdint.h> // for uint64_t
+#include <stdio.h> // for FILE
+#include "gfxRect.h" // for gfxRect
+#include "mozilla/Assertions.h" // for MOZ_ASSERT, etc
+#include "mozilla/Attributes.h" // for override
+#include "mozilla/RefPtr.h" // for RefPtr, RefCounted, etc
+// #include "mozilla/gfx/MatrixFwd.h" // for Matrix4x4
+#include "mozilla/gfx/Polygon.h" // for Polygon
+#include "mozilla/gfx/Rect.h" // for Rect
+#include "mozilla/ipc/ProtocolUtils.h"
+#include "mozilla/layers/CompositorTypes.h" // for TextureInfo, etc
+// #include "mozilla/layers/LayersTypes.h" // for LayerRenderState, etc
+#include "mozilla/layers/LayersMessages.h"
+#include "mozilla/layers/TextureHost.h" // for TextureHost
+#include "nsCOMPtr.h" // for already_AddRefed
+#include "nscore.h" // for nsACString
+#include "Units.h" // for CSSToScreenScale
+
+namespace mozilla {
+
+namespace layers {
+
+class WebRenderImageHost;
+
+struct ImageCompositeNotificationInfo {
+ base::ProcessId mImageBridgeProcessId;
+ ImageCompositeNotification mNotification;
+};
+
+struct AsyncCompositableRef {
+ AsyncCompositableRef() : mProcessId(base::kInvalidProcessId) {}
+ AsyncCompositableRef(base::ProcessId aProcessId,
+ const CompositableHandle& aHandle)
+ : mProcessId(aProcessId), mHandle(aHandle) {}
+ explicit operator bool() const { return !!mHandle; }
+ base::ProcessId mProcessId;
+ CompositableHandle mHandle;
+};
+
+/**
+ * The compositor-side counterpart to CompositableClient. Responsible for
+ * updating textures and data about textures from IPC and how textures are
+ * composited (tiling, double buffering, etc.).
+ *
+ * Update (for images/canvases) and UpdateThebes (for Thebes) are called during
+ * the layers transaction to update the Compositbale's textures from the
+ * content side. The actual update (and any syncronous upload) is done by the
+ * TextureHost, but it is coordinated by the CompositableHost.
+ *
+ * Composite is called by the owning layer when it is composited.
+ * CompositableHost will use its TextureHost(s) and call Compositor::DrawQuad to
+ * do the actual rendering.
+ */
+class CompositableHost {
+ protected:
+ virtual ~CompositableHost();
+
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(CompositableHost)
+ explicit CompositableHost(const TextureInfo& aTextureInfo);
+
+ static already_AddRefed<CompositableHost> Create(
+ const TextureInfo& aTextureInfo);
+
+ virtual WebRenderImageHost* AsWebRenderImageHost() { return nullptr; }
+
+ virtual void Dump(std::stringstream& aStream, const char* aPrefix = "",
+ bool aDumpHtml = false) {}
+ static void DumpTextureHost(std::stringstream& aStream,
+ TextureHost* aTexture);
+
+ struct TimedTexture {
+ CompositableTextureHostRef mTexture;
+ TimeStamp mTimeStamp;
+ gfx::IntRect mPictureRect;
+ int32_t mFrameID;
+ int32_t mProducerID;
+ };
+ virtual void UseTextureHost(const nsTArray<TimedTexture>& aTextures);
+ virtual void RemoveTextureHost(TextureHost* aTexture);
+
+ // Enable remote texture push callback
+ virtual void EnableRemoteTexturePushCallback(
+ const RemoteTextureOwnerId aOwnerId, const base::ProcessId aForPid,
+ const gfx::IntSize aSize, const TextureFlags aFlags) = 0;
+ // Called from RemoteTextureMap when a new remote texture is pushed
+ virtual void NotifyPushTexture(const RemoteTextureId aTextureId,
+ const RemoteTextureOwnerId aOwnerId,
+ const base::ProcessId aForPid) = 0;
+
+ uint64_t GetCompositorBridgeID() const { return mCompositorBridgeID; }
+
+ const AsyncCompositableRef& GetAsyncRef() const { return mAsyncRef; }
+ void SetAsyncRef(const AsyncCompositableRef& aRef) { mAsyncRef = aRef; }
+
+ void SetCompositorBridgeID(uint64_t aID) { mCompositorBridgeID = aID; }
+
+ /// Called when shutting down the layer tree.
+ /// This is a good place to clear all potential gpu resources before the
+ /// widget is is destroyed.
+ virtual void CleanupResources() {}
+
+ virtual void OnReleased() {}
+
+ virtual uint32_t GetDroppedFrames() { return 0; }
+
+ protected:
+ protected:
+ TextureInfo mTextureInfo;
+ AsyncCompositableRef mAsyncRef;
+ uint64_t mCompositorBridgeID;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif
diff --git a/gfx/layers/composite/Diagnostics.h b/gfx/layers/composite/Diagnostics.h
new file mode 100644
index 0000000000..b348128a20
--- /dev/null
+++ b/gfx/layers/composite/Diagnostics.h
@@ -0,0 +1,29 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_gfx_layers_composite_Diagnostics_h
+#define mozilla_gfx_layers_composite_Diagnostics_h
+
+#include "mozilla/Maybe.h"
+#include <cstdint>
+
+namespace mozilla {
+namespace layers {
+
+// These statistics are collected by layers backends, preferably by the GPU
+struct GPUStats {
+ GPUStats() : mInvalidPixels(0), mScreenPixels(0), mPixelsFilled(0) {}
+
+ uint32_t mInvalidPixels;
+ uint32_t mScreenPixels;
+ uint32_t mPixelsFilled;
+ Maybe<float> mDrawTime;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_gfx_layers_composite_Diagnostics_h
diff --git a/gfx/layers/composite/FontData.h b/gfx/layers/composite/FontData.h
new file mode 100644
index 0000000000..aa315f2519
--- /dev/null
+++ b/gfx/layers/composite/FontData.h
@@ -0,0 +1,304 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// This is explicitly not guarded as we want only 1 file to include this and
+// it's good if things break if someone else does.
+
+namespace mozilla {
+namespace layers {
+namespace normal_font {
+const unsigned char sFontPNG[] = {
+ 0x89, 0x50, 0x4e, 0x47, 0xd, 0xa, 0x1a, 0xa, 0x0, 0x0, 0x0, 0xd,
+ 0x49, 0x48, 0x44, 0x52, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x0,
+ 0x8, 0x0, 0x0, 0x0, 0x0, 0x79, 0x19, 0xf7, 0xba, 0x0, 0x0, 0xb,
+ 0xfe, 0x49, 0x44, 0x41, 0x54, 0x78, 0xda, 0xed, 0x5d, 0xd1, 0xb6, 0xe3,
+ 0x20, 0x8, 0x74, 0x72, 0xf2, 0xff, 0xbf, 0x3c, 0xfb, 0xd0, 0xbb, 0xbd,
+ 0x51, 0x8, 0x48, 0xd4, 0xc4, 0xde, 0xda, 0xb3, 0x67, 0x77, 0x6d, 0x13,
+ 0x63, 0x10, 0x81, 0x41, 0x40, 0x30, 0x7d, 0xf7, 0x67, 0x4b, 0x8b, 0x0,
+ 0xdf, 0xfd, 0xd9, 0x2f, 0xdc, 0x3, 0xc6, 0xda, 0x76, 0x67, 0x29, 0xa5,
+ 0x94, 0xb8, 0x38, 0x20, 0x42, 0x33, 0xc0, 0x68, 0xaa, 0x37, 0x64, 0x14,
+ 0x47, 0xc2, 0x67, 0x13, 0x80, 0x5, 0xbf, 0x91, 0xb0, 0x5f, 0x9f, 0x73,
+ 0x71, 0x40, 0x39, 0x61, 0x1c, 0xcd, 0xff, 0x39, 0x7d, 0x8, 0x1e, 0x29,
+ 0x72, 0x3f, 0x1, 0x90, 0x12, 0x8a, 0xf9, 0xc, 0x52, 0x20, 0x9f, 0x51,
+ 0x2, 0x8e, 0xd0, 0x99, 0x8d, 0x3, 0x7a, 0x2f, 0x2, 0x7a, 0x4, 0xa4,
+ 0x90, 0xba, 0x8, 0x68, 0x1, 0x28, 0x32, 0x1a, 0x87, 0x6f, 0x50, 0x71,
+ 0x7f, 0x5f, 0x29, 0xf, 0x85, 0xa3, 0x18, 0x5b, 0x10, 0xc, 0x10, 0x80,
+ 0xf2, 0x1d, 0x79, 0x4e, 0x5e, 0x8f, 0xfc, 0xaf, 0x1, 0xb0, 0x60, 0x50,
+ 0x4, 0x17, 0x40, 0xc6, 0xd3, 0xf4, 0xd5, 0x2e, 0x3a, 0xdb, 0x1, 0x5d,
+ 0x85, 0x78, 0x8a, 0xbd, 0x7e, 0x33, 0xc3, 0x24, 0xe4, 0x52, 0x70, 0xff,
+ 0xbc, 0xf5, 0x8f, 0xf0, 0x82, 0xe2, 0x5c, 0x1c, 0xd0, 0x9b, 0x83, 0x1c,
+ 0x7a, 0x50, 0xb4, 0x33, 0xa9, 0xf9, 0xf5, 0x58, 0x0, 0xb, 0xe, 0x2f,
+ 0x2, 0x2c, 0x2, 0x7c, 0x3d, 0x1, 0xea, 0x8c, 0xb9, 0xc, 0x4f, 0x16,
+ 0x70, 0x14, 0x21, 0xc5, 0x8e, 0x60, 0xfb, 0x1, 0xe, 0x80, 0xf5, 0xc2,
+ 0x52, 0x8c, 0xb6, 0x9, 0x52, 0xdc, 0xfb, 0xc6, 0x35, 0x4, 0x40, 0xa,
+ 0x81, 0x8d, 0x66, 0xbd, 0xce, 0xc9, 0x8, 0x80, 0x6f, 0x93, 0x1, 0x6d,
+ 0xef, 0xcf, 0xf1, 0xf4, 0xc2, 0xc0, 0xf9, 0xd9, 0xdb, 0xe7, 0x7f, 0x30,
+ 0x5, 0x50, 0x98, 0x6b, 0x48, 0xe8, 0x9, 0xaf, 0x77, 0x14, 0xf8, 0xbe,
+ 0xf5, 0x7d, 0xa0, 0xfa, 0xb, 0x18, 0xa2, 0x6f, 0x3e, 0x9e, 0xc2, 0x5c,
+ 0x65, 0x5f, 0xe3, 0x75, 0xa7, 0x30, 0x87, 0x3d, 0xa, 0xd8, 0x42, 0x8b,
+ 0x8a, 0x3f, 0x81, 0xb6, 0x7f, 0xa1, 0xa6, 0x8f, 0x80, 0x7, 0xa2, 0x5d,
+ 0x8, 0xd2, 0xd1, 0xfb, 0x7f, 0x4a, 0x4a, 0x6e, 0xfe, 0xc, 0x3c, 0xab,
+ 0xa6, 0x9e, 0x31, 0x84, 0x4c, 0x9e, 0xa3, 0xfb, 0x45, 0x47, 0x7f, 0xc5,
+ 0xc2, 0x2, 0xcb, 0x1f, 0xb0, 0x38, 0x60, 0x11, 0x60, 0x7a, 0x2, 0xe0,
+ 0x6f, 0x11, 0x0, 0x1e, 0x5a, 0x85, 0x3, 0x97, 0x5f, 0xde, 0x1, 0x94,
+ 0x5f, 0x9c, 0x93, 0x2b, 0xf2, 0x73, 0x77, 0x68, 0xd1, 0xce, 0x1, 0x60,
+ 0x6e, 0x3b, 0x82, 0x44, 0x26, 0x5c, 0xe1, 0x6d, 0x5f, 0xd3, 0xb2, 0x43,
+ 0x91, 0xb4, 0xcd, 0xb0, 0x27, 0x97, 0x0, 0x12, 0x4, 0x53, 0xc, 0x54,
+ 0x25, 0x44, 0x6f, 0x4d, 0x95, 0x77, 0xe7, 0x13, 0x80, 0xd2, 0x30, 0xf4,
+ 0x2d, 0x1f, 0xf0, 0xb4, 0x45, 0x47, 0x11, 0x13, 0xb7, 0xda, 0x49, 0x7b,
+ 0xf7, 0x29, 0x22, 0x12, 0xc1, 0x23, 0xa2, 0x3, 0x1b, 0xec, 0xd, 0x30,
+ 0xc7, 0x7f, 0x3f, 0x4b, 0x82, 0x9d, 0x18, 0xe0, 0x2, 0x1, 0xca, 0xdd,
+ 0xd9, 0x32, 0xe4, 0xe2, 0x35, 0xb6, 0xdf, 0xab, 0xd0, 0x86, 0xaf, 0x59,
+ 0xf0, 0x80, 0x4b, 0xcc, 0xe0, 0xd6, 0xe1, 0x7e, 0x41, 0x4, 0xb8, 0xf,
+ 0x11, 0xf8, 0x9a, 0x37, 0x1a, 0x9c, 0xc1, 0x27, 0x6d, 0xd1, 0xe, 0x5e,
+ 0x22, 0x80, 0x21, 0x99, 0xcc, 0x89, 0xc, 0xee, 0x72, 0x28, 0xbe, 0x1d,
+ 0x10, 0x17, 0xd3, 0x60, 0x4a, 0x7c, 0x7, 0xee, 0x10, 0x0, 0x81, 0x71,
+ 0x73, 0x1c, 0xdd, 0x86, 0xe8, 0xf, 0x86, 0x1a, 0xfb, 0x50, 0xc8, 0x77,
+ 0xe3, 0xe3, 0xb7, 0xfb, 0x57, 0xdd, 0x5c, 0x78, 0x76, 0x86, 0xd5, 0x99,
+ 0x8d, 0xe1, 0xee, 0x1, 0x2d, 0x7f, 0xc0, 0x82, 0xc3, 0x8b, 0x0, 0xdf,
+ 0xfd, 0xd9, 0x15, 0xcb, 0x1a, 0x22, 0xb6, 0x94, 0x56, 0xbb, 0x94, 0x62,
+ 0xa5, 0x62, 0x80, 0x68, 0x9a, 0xbf, 0xf7, 0xdd, 0xf9, 0xd2, 0xed, 0x82,
+ 0x2c, 0x52, 0x54, 0x40, 0x15, 0xc8, 0xb7, 0xcb, 0xd0, 0x88, 0xb0, 0xc6,
+ 0x6d, 0x6c, 0x5c, 0x5e, 0xcf, 0xe2, 0xe, 0xbf, 0xff, 0x36, 0x59, 0xe,
+ 0x7b, 0x3c, 0x9b, 0x7b, 0x3, 0x3b, 0x68, 0xfd, 0x96, 0x94, 0x92, 0x8a,
+ 0x0, 0x12, 0x33, 0x5c, 0xbc, 0xb4, 0x42, 0xe9, 0x81, 0xa1, 0xf0, 0xb,
+ 0x96, 0x70, 0x4f, 0x2e, 0xa9, 0xd4, 0x64, 0xeb, 0x79, 0x33, 0x80, 0xa4,
+ 0xf0, 0x63, 0xc6, 0x72, 0x16, 0x76, 0xdb, 0x2f, 0x30, 0x98, 0x83, 0x5,
+ 0x29, 0x4d, 0x1b, 0x7b, 0x86, 0xc2, 0xd6, 0x7d, 0x88, 0x63, 0x93, 0x8c,
+ 0xa7, 0x6, 0x63, 0x4, 0x90, 0x8f, 0x28, 0xd7, 0xac, 0x39, 0x0, 0xfa,
+ 0xbd, 0x83, 0x85, 0x43, 0x25, 0x6, 0xc6, 0xfc, 0x68, 0xf1, 0x73, 0x7,
+ 0xd5, 0xde, 0x24, 0x60, 0xfe, 0xcf, 0x57, 0xc8, 0x9, 0x1, 0xc6, 0x58,
+ 0xde, 0x5f, 0x22, 0xd9, 0xe3, 0xc5, 0x12, 0x40, 0x63, 0xb0, 0xb4, 0x97,
+ 0x14, 0xc7, 0xce, 0xc6, 0xfc, 0x85, 0xfb, 0x33, 0x96, 0xa4, 0xdb, 0x9f,
+ 0x92, 0xae, 0x60, 0xe9, 0x69, 0x57, 0x6f, 0x2b, 0x5a, 0xd1, 0x6e, 0x33,
+ 0xa5, 0xa6, 0xeb, 0x5b, 0xad, 0x84, 0x47, 0xc1, 0xd7, 0x74, 0xf8, 0x6d,
+ 0xfb, 0xf2, 0xf7, 0x5f, 0x70, 0x78, 0xa1, 0xc1, 0x45, 0x80, 0x45, 0x80,
+ 0xaf, 0xf7, 0x7, 0x28, 0xd1, 0x9c, 0xb4, 0xc0, 0x45, 0x91, 0x4c, 0xaa,
+ 0xda, 0xd, 0x87, 0x6f, 0xdc, 0xfb, 0x99, 0x3f, 0x12, 0x34, 0xf1, 0xbb,
+ 0x7b, 0xfd, 0xff, 0x76, 0xfe, 0x6f, 0xb9, 0xbd, 0xc8, 0x23, 0x1, 0xde,
+ 0x9b, 0x78, 0xbf, 0xf0, 0x4e, 0xdb, 0xeb, 0xcb, 0x9e, 0x50, 0x98, 0x5e,
+ 0x25, 0xe0, 0x37, 0xd1, 0x1b, 0x63, 0xda, 0x47, 0xcb, 0x2c, 0xb5, 0x6e,
+ 0xff, 0x79, 0x63, 0xdb, 0x90, 0x7a, 0x7f, 0xb9, 0xfd, 0xfe, 0xbf, 0x12,
+ 0x82, 0xbc, 0x2e, 0xf7, 0xf6, 0xb4, 0x83, 0xf7, 0x8f, 0x54, 0xde, 0x25,
+ 0x5a, 0x67, 0xf6, 0xe5, 0xf6, 0x42, 0x63, 0xc7, 0x98, 0x87, 0x32, 0xe4,
+ 0xe3, 0xd3, 0x6c, 0x21, 0x22, 0x42, 0xa2, 0xfd, 0x8d, 0x48, 0x6d, 0x8f,
+ 0x4c, 0x4, 0xed, 0x29, 0x42, 0x41, 0x1, 0xfc, 0xbc, 0x84, 0xfe, 0xaf,
+ 0xdc, 0x60, 0xee, 0xcd, 0xee, 0xa, 0x53, 0x6a, 0x3e, 0xbe, 0xd0, 0x9a,
+ 0xad, 0xf0, 0x50, 0x1c, 0x1e, 0xc2, 0xe0, 0x4b, 0x81, 0x2e, 0xb, 0x50,
+ 0x44, 0x30, 0xb8, 0x70, 0x18, 0x85, 0x2c, 0x9c, 0xd8, 0x44, 0x66, 0xd7,
+ 0xfd, 0xf6, 0x2d, 0xfd, 0x6c, 0x60, 0x57, 0xf7, 0x49, 0x28, 0x33, 0x16,
+ 0x61, 0x10, 0x84, 0x6e, 0x40, 0x8d, 0x60, 0xf3, 0x78, 0xfe, 0x78, 0x79,
+ 0xf1, 0xfc, 0xfd, 0xcd, 0x84, 0xb9, 0xfa, 0x3c, 0xf4, 0x51, 0xae, 0x69,
+ 0xc2, 0x5e, 0xe3, 0x5e, 0xcd, 0x8, 0x22, 0x22, 0x53, 0xa8, 0x6f, 0x13,
+ 0x14, 0xe3, 0x63, 0x15, 0xd5, 0xb5, 0xe7, 0x1f, 0x63, 0x79, 0xbe, 0xd2,
+ 0x12, 0xfc, 0x55, 0x0, 0x5f, 0x8a, 0x8b, 0x97, 0x3f, 0x60, 0xa1, 0xc1,
+ 0x45, 0x80, 0x45, 0x80, 0x2f, 0xf7, 0x7, 0x54, 0xe1, 0xfd, 0xf3, 0x8d,
+ 0x82, 0x12, 0xfe, 0x97, 0x96, 0xa4, 0x5, 0xcf, 0x7f, 0x77, 0xc5, 0x60,
+ 0xf8, 0x7, 0xe4, 0xe3, 0x2d, 0xbf, 0x3e, 0x82, 0x7b, 0xd1, 0x7b, 0x1d,
+ 0xde, 0x17, 0xe, 0x88, 0x77, 0xdb, 0xdb, 0xcd, 0x67, 0x79, 0x7d, 0xd1,
+ 0x13, 0xe5, 0xde, 0x20, 0xa5, 0xbb, 0xc1, 0xc1, 0x16, 0x96, 0xf7, 0xc0,
+ 0x89, 0xd3, 0x3d, 0x8b, 0xf, 0xf0, 0xf3, 0x67, 0xdb, 0x94, 0x2f, 0x8e,
+ 0x86, 0x99, 0x48, 0x99, 0x8, 0xc4, 0x4e, 0xb6, 0x22, 0xf7, 0xf1, 0x32,
+ 0x20, 0x6, 0xcf, 0x47, 0x18, 0x3a, 0x74, 0x6a, 0x89, 0x79, 0x78, 0xbf,
+ 0xf8, 0x9d, 0x68, 0xe3, 0x80, 0x17, 0x58, 0x3d, 0x40, 0x56, 0x5, 0xba,
+ 0x34, 0x6, 0xcf, 0xc6, 0x84, 0x60, 0x45, 0x3d, 0x38, 0xb0, 0xc5, 0x3f,
+ 0x50, 0xb1, 0x1e, 0x22, 0xc1, 0xf7, 0x21, 0x77, 0x87, 0xcc, 0x37, 0xb8,
+ 0xb4, 0x4, 0x1a, 0x53, 0x1e, 0x30, 0xb3, 0xc1, 0xbd, 0x55, 0xe1, 0xf5,
+ 0x23, 0x5, 0xd0, 0xba, 0xe6, 0xf9, 0x2b, 0xfd, 0x6b, 0x16, 0x29, 0x38,
+ 0x74, 0x7a, 0xf6, 0x3a, 0xbc, 0x7e, 0xe0, 0x33, 0x57, 0x6, 0x8, 0x4,
+ 0x4f, 0xd7, 0x89, 0x6d, 0x5f, 0x31, 0x14, 0xaa, 0xea, 0x4f, 0x9e, 0x19,
+ 0x23, 0x82, 0xad, 0x17, 0x54, 0xc8, 0x80, 0xbe, 0x19, 0x1e, 0x93, 0x9b,
+ 0xc2, 0x3, 0xec, 0x9c, 0xc1, 0x2c, 0x60, 0x8e, 0xae, 0x32, 0x81, 0x86,
+ 0xcb, 0x21, 0xb2, 0xd0, 0xe0, 0x22, 0xc0, 0xf, 0x1, 0xf0, 0xed, 0x4,
+ 0xf0, 0xec, 0x88, 0xba, 0xd2, 0x4f, 0xd7, 0x3b, 0x40, 0xe7, 0x76, 0x7c,
+ 0x9, 0xc, 0x16, 0x83, 0x51, 0x31, 0xdb, 0x23, 0x3c, 0xbf, 0x4d, 0x6,
+ 0x88, 0x3, 0xb, 0x0, 0xf3, 0x77, 0x79, 0x79, 0x79, 0x9e, 0x81, 0xe8,
+ 0xce, 0xea, 0xa0, 0x66, 0x86, 0x4d, 0xd3, 0x39, 0x1, 0xc7, 0x3e, 0x61,
+ 0x8f, 0x7f, 0x57, 0x67, 0xcc, 0xa, 0x77, 0x57, 0x32, 0x3c, 0x60, 0xdc,
+ 0x4f, 0x25, 0x40, 0x43, 0x44, 0x2c, 0x4, 0xb9, 0xc4, 0x45, 0x8f, 0x38,
+ 0x42, 0x58, 0x7b, 0xfc, 0x15, 0xc9, 0xd3, 0x74, 0xd9, 0xb4, 0xad, 0xd4,
+ 0x35, 0x3b, 0x2f, 0x2a, 0x25, 0xe1, 0x3e, 0x66, 0x9, 0xb6, 0xaa, 0x85,
+ 0xd1, 0x6a, 0x65, 0x78, 0x41, 0x45, 0x36, 0x3e, 0x63, 0xf8, 0x89, 0x19,
+ 0x5d, 0xcd, 0xd7, 0x8a, 0xa2, 0xaa, 0x70, 0x27, 0x1, 0x8d, 0x9a, 0xef,
+ 0x82, 0x8b, 0x65, 0x20, 0x7, 0x10, 0x5, 0x8d, 0x8b, 0x9c, 0x94, 0x12,
+ 0xee, 0x97, 0x29, 0x2e, 0x74, 0xf2, 0xfe, 0xa2, 0x29, 0x31, 0xe7, 0x3e,
+ 0x95, 0xeb, 0xd4, 0xe3, 0xd3, 0xc8, 0x1f, 0x1c, 0x73, 0xed, 0x47, 0x60,
+ 0x81, 0xc9, 0x4c, 0xef, 0x7, 0x38, 0x20, 0x96, 0x1a, 0xb, 0xfe, 0x39,
+ 0x2, 0x2c, 0x38, 0xbc, 0x8, 0xb0, 0x8, 0xf0, 0xa7, 0x8, 0x70, 0xbb,
+ 0x54, 0xb7, 0x4f, 0x33, 0x88, 0xfa, 0xb3, 0x3f, 0xef, 0x98, 0x1d, 0x17,
+ 0xa, 0x79, 0xa5, 0xc6, 0x52, 0x51, 0x4e, 0x4f, 0xc1, 0xcb, 0x79, 0x81,
+ 0x3, 0x17, 0xef, 0xcb, 0x19, 0xc1, 0x69, 0xf3, 0xdd, 0x86, 0x71, 0x7d,
+ 0x51, 0x8f, 0x12, 0x23, 0xeb, 0x50, 0xed, 0x1a, 0x5e, 0x36, 0x2b, 0xe,
+ 0x78, 0x78, 0xff, 0x82, 0x59, 0xb, 0xaf, 0x58, 0x39, 0xda, 0xd6, 0xa8,
+ 0xc5, 0x15, 0xaa, 0x4b, 0x8c, 0x41, 0x2a, 0x6, 0x4d, 0x71, 0x58, 0xfb,
+ 0xc5, 0xda, 0xde, 0x6a, 0x13, 0x8d, 0x41, 0x5a, 0x15, 0x26, 0xb6, 0xb,
+ 0x6f, 0x0, 0x44, 0x5f, 0xb8, 0xd, 0xd9, 0x4, 0x93, 0x9d, 0x29, 0x18,
+ 0xd8, 0x3a, 0xd3, 0xf2, 0x82, 0x16, 0xf0, 0xce, 0xf8, 0x8c, 0x4f, 0x11,
+ 0x4c, 0x8a, 0xd9, 0x45, 0x57, 0x5b, 0x8d, 0xdb, 0x2d, 0x5c, 0xc8, 0x16,
+ 0x35, 0xab, 0xce, 0x99, 0xf0, 0x88, 0xe0, 0xc0, 0xf0, 0x9a, 0xa2, 0x12,
+ 0xef, 0x9b, 0x54, 0xd6, 0xfc, 0x1, 0xc9, 0xf4, 0xf, 0xb8, 0x27, 0x82,
+ 0xd2, 0x3c, 0x42, 0x62, 0x6c, 0x7c, 0xc0, 0x9e, 0x3c, 0x17, 0xa7, 0xeb,
+ 0x24, 0x55, 0xda, 0xf4, 0xae, 0xe7, 0x69, 0xb3, 0xa7, 0xc0, 0xad, 0x61,
+ 0xcf, 0x9, 0xd, 0xa1, 0xb1, 0x3c, 0xcf, 0xd9, 0x2d, 0x41, 0xb7, 0x1e,
+ 0xb, 0xbd, 0xf8, 0x0, 0x4e, 0x44, 0xee, 0x85, 0x6, 0x3b, 0x31, 0x5,
+ 0xc6, 0xdd, 0xff, 0x11, 0x1c, 0x30, 0xb2, 0x32, 0xcf, 0xf3, 0x4, 0x78,
+ 0x78, 0x4, 0xfb, 0x15, 0x3d, 0x1b, 0x14, 0x34, 0x66, 0x21, 0x20, 0x50,
+ 0xad, 0x54, 0x86, 0xf3, 0x2, 0x5, 0xe7, 0x8a, 0x8d, 0xa7, 0xbf, 0x5a,
+ 0xa9, 0xb3, 0xd1, 0x74, 0xfe, 0xce, 0x73, 0x46, 0x59, 0x6a, 0x2e, 0x87,
+ 0xa7, 0x6e, 0x2a, 0xaf, 0x53, 0x80, 0xd0, 0x41, 0x93, 0xfb, 0x18, 0x6b,
+ 0x23, 0x9f, 0x81, 0xb2, 0xd6, 0x17, 0x12, 0xa3, 0x24, 0x66, 0xcb, 0x74,
+ 0x98, 0xd9, 0xe3, 0xf1, 0xc3, 0xdb, 0x82, 0xb1, 0xcf, 0x32, 0xd8, 0x7c,
+ 0xaa, 0x52, 0xc8, 0x7b, 0xdc, 0xa1, 0x31, 0xb6, 0x54, 0xfa, 0xeb, 0x78,
+ 0x2, 0xbc, 0x32, 0xec, 0x99, 0x64, 0x6c, 0x32, 0x4e, 0x57, 0x3a, 0xaf,
+ 0x50, 0xec, 0xa, 0x7, 0x8c, 0x3e, 0x81, 0x13, 0xc4, 0xfb, 0xaf, 0x54,
+ 0xe6, 0x27, 0x9c, 0x3c, 0xfd, 0xb2, 0xa2, 0xdb, 0x98, 0xc8, 0x4f, 0xb2,
+ 0x6, 0x7b, 0x57, 0xf7, 0xd8, 0xaa, 0x41, 0x7e, 0x37, 0xa1, 0x8, 0xc7,
+ 0x75, 0xad, 0xa0, 0x61, 0x8b, 0x2, 0x6e, 0x98, 0x9f, 0x8b, 0xc6, 0x11,
+ 0xe4, 0x20, 0xe1, 0xb0, 0x53, 0x3c, 0x78, 0x99, 0x6a, 0x8a, 0x15, 0x4,
+ 0x7c, 0xa9, 0xe5, 0x5f, 0x19, 0x30, 0xdc, 0xe, 0xe8, 0xae, 0x2, 0x59,
+ 0x1, 0xf2, 0x3d, 0xbc, 0x7a, 0x2c, 0xe9, 0xd0, 0xe8, 0xa2, 0x65, 0x13,
+ 0x83, 0x5c, 0x60, 0x80, 0xf, 0xfb, 0x80, 0xe3, 0xef, 0xf8, 0xd3, 0x40,
+ 0x6b, 0xf9, 0x3, 0x16, 0x1, 0x3e, 0x90, 0xeb, 0x8f, 0xba, 0x75, 0x9b,
+ 0x60, 0x38, 0x68, 0xbc, 0x20, 0xfa, 0x80, 0xcc, 0x6d, 0xed, 0x9f, 0x3d,
+ 0xee, 0x9d, 0x3f, 0xd8, 0x38, 0x3e, 0x69, 0x46, 0xbc, 0x77, 0xa7, 0xd1,
+ 0x7, 0x7b, 0x50, 0x3b, 0xc, 0x0, 0xe7, 0x4, 0x70, 0x2d, 0xa7, 0xbe,
+ 0x72, 0x53, 0x3f, 0xc4, 0x17, 0xae, 0x7d, 0x13, 0xb5, 0x2c, 0x72, 0xcb,
+ 0x11, 0x2c, 0xfc, 0x1, 0xd7, 0xd5, 0xc4, 0x18, 0x3b, 0x80, 0xe0, 0x7b,
+ 0xcb, 0xd0, 0x3, 0x6b, 0x48, 0x66, 0x78, 0xaa, 0xb0, 0x4, 0xf3, 0xfd,
+ 0x78, 0xf7, 0x7c, 0x1, 0x3a, 0x33, 0x46, 0xaf, 0xde, 0x7f, 0xf, 0x45,
+ 0x6d, 0x55, 0xb3, 0x7f, 0x7b, 0xb4, 0xc, 0x9c, 0x88, 0x3b, 0xb5, 0x0,
+ 0xba, 0x40, 0x2b, 0x9e, 0x1e, 0xd2, 0x78, 0x1, 0xe, 0xca, 0xd3, 0xef,
+ 0x52, 0xaa, 0x5e, 0x2, 0x1d, 0xe8, 0x91, 0xbd, 0x82, 0x5f, 0xe, 0x84,
+ 0x87, 0x3f, 0xa9, 0x36, 0x17, 0xfb, 0x72, 0xfe, 0xf9, 0x3e, 0x60, 0xf9,
+ 0x9a, 0xf9, 0x1a, 0x57, 0x8a, 0xe7, 0x57, 0x84, 0x48, 0x5c, 0x77, 0x88,
+ 0xc, 0x10, 0x60, 0x1c, 0xc1, 0x42, 0xfe, 0xfb, 0xe0, 0xaa, 0x3f, 0xe0,
+ 0x3, 0xc, 0xb7, 0xae, 0x5a, 0xc0, 0x96, 0xa9, 0xb, 0xb, 0x2c, 0x2,
+ 0x2c, 0x2, 0x2c, 0x2, 0x2c, 0x2, 0x7c, 0xd3, 0x67, 0xff, 0xb6, 0x17,
+ 0xbe, 0x52, 0x43, 0xc4, 0x2f, 0xd9, 0x27, 0x2e, 0x86, 0xb1, 0x97, 0x57,
+ 0xe6, 0xed, 0x81, 0x96, 0xe2, 0x6e, 0x87, 0x56, 0xf6, 0xc6, 0x40, 0xcd,
+ 0xe6, 0x72, 0x92, 0x25, 0xc, 0x3, 0xdb, 0xf5, 0xce, 0x79, 0x7f, 0x17,
+ 0x28, 0x78, 0x5, 0x9, 0x9d, 0x76, 0xe2, 0x9f, 0x3c, 0xd, 0x27, 0x7d,
+ 0x1c, 0x45, 0x82, 0x41, 0x4d, 0xe9, 0x30, 0xfb, 0xfd, 0x81, 0x63, 0xb0,
+ 0x69, 0x23, 0xb4, 0x14, 0xfd, 0xb9, 0x4, 0xa0, 0x8e, 0xce, 0x2c, 0xe0,
+ 0x1a, 0xf2, 0x89, 0xb8, 0xd1, 0xe6, 0xc8, 0xa2, 0xb1, 0x65, 0xd7, 0x71,
+ 0xbc, 0x6d, 0x45, 0x77, 0xef, 0xe1, 0x45, 0x81, 0x82, 0x27, 0x50, 0x54,
+ 0x7b, 0x87, 0x13, 0x41, 0x51, 0x75, 0xa4, 0x26, 0x4f, 0xfc, 0x37, 0x49,
+ 0xd6, 0x5b, 0xa8, 0xa8, 0x8b, 0xdd, 0x78, 0xe0, 0xa2, 0xe5, 0x4e, 0x50,
+ 0xc7, 0xe, 0xb6, 0xe1, 0xdf, 0x54, 0xd4, 0x14, 0x35, 0xdf, 0x5f, 0x1c,
+ 0x4f, 0xa0, 0xbc, 0x2d, 0xfd, 0x18, 0x21, 0x1b, 0xdf, 0x9b, 0x24, 0x64,
+ 0x99, 0x72, 0xe3, 0x48, 0x6e, 0x2f, 0xbb, 0xdc, 0xb9, 0xc0, 0x5d, 0x71,
+ 0x3c, 0x19, 0x63, 0xb5, 0x16, 0x50, 0x17, 0x9d, 0xa1, 0x5, 0xf0, 0xa,
+ 0x6a, 0x39, 0xed, 0x41, 0xf5, 0xdf, 0x44, 0xb4, 0x80, 0x26, 0xd6, 0x58,
+ 0xdf, 0xa1, 0xd3, 0x5f, 0xd, 0x1, 0x9c, 0x73, 0x85, 0x9d, 0x3e, 0xa3,
+ 0x6a, 0x30, 0x35, 0xd6, 0x2c, 0x6d, 0xb6, 0x3, 0x7a, 0x7b, 0x8, 0xbc,
+ 0x83, 0x93, 0xbe, 0xcf, 0x14, 0x9e, 0xcb, 0x5, 0xb3, 0x4d, 0x3e, 0xbe,
+ 0xf1, 0xd8, 0x60, 0xb9, 0xc4, 0x16, 0x1, 0x16, 0x1, 0xfa, 0x20, 0xec,
+ 0xcf, 0x25, 0x80, 0x13, 0xb6, 0xe8, 0xd4, 0x92, 0xab, 0x88, 0x33, 0xb,
+ 0xa6, 0x7f, 0x47, 0xb, 0x2, 0x94, 0xf9, 0xe6, 0x79, 0xfb, 0xff, 0xb3,
+ 0x71, 0x2a, 0x4, 0xa5, 0x61, 0xe5, 0x5, 0x36, 0x22, 0x18, 0x5c, 0x1e,
+ 0xb, 0x71, 0x80, 0x63, 0xfb, 0xeb, 0xd7, 0x83, 0x67, 0x6d, 0xc7, 0xf4,
+ 0xaa, 0x58, 0x2, 0x22, 0x82, 0x83, 0xee, 0x84, 0xb4, 0xac, 0xa, 0x36,
+ 0x5e, 0x2f, 0xec, 0x2e, 0xd5, 0xb6, 0x3f, 0x10, 0x80, 0x88, 0x15, 0x77,
+ 0xe7, 0xc3, 0x87, 0x11, 0xaa, 0xee, 0x0, 0x83, 0x49, 0x49, 0x24, 0x3,
+ 0xa0, 0x8a, 0x50, 0x59, 0x14, 0xc6, 0xaa, 0x80, 0xff, 0xd1, 0x39, 0x8a,
+ 0xd2, 0xab, 0xf0, 0x26, 0x88, 0x3c, 0xc9, 0x12, 0xe, 0xe2, 0x10, 0x5d,
+ 0xfc, 0xdb, 0x3e, 0x34, 0xff, 0x3, 0x66, 0xdd, 0xc9, 0xb6, 0x2b, 0x1c,
+ 0x85, 0xce, 0xdb, 0xdb, 0x8, 0x49, 0x10, 0xf7, 0x81, 0x5, 0x5, 0xf8,
+ 0x23, 0xc9, 0x90, 0xb5, 0x7f, 0x4f, 0x8d, 0x4b, 0xc5, 0xef, 0x8f, 0xdb,
+ 0x1, 0xcd, 0xa6, 0x67, 0x9d, 0xd4, 0x36, 0xfc, 0xb2, 0x59, 0x73, 0x1b,
+ 0xb4, 0x4c, 0x87, 0x1a, 0xef, 0x9a, 0x58, 0xb3, 0x95, 0x33, 0x4e, 0x4f,
+ 0xa1, 0xdd, 0x4f, 0x84, 0x38, 0x5b, 0x5e, 0x9f, 0x23, 0x21, 0x6, 0xb5,
+ 0xb2, 0x2e, 0x2c, 0xce, 0xd9, 0xcd, 0x24, 0x57, 0xf9, 0xfb, 0x60, 0x30,
+ 0xa4, 0x79, 0x18, 0xa6, 0xca, 0x5d, 0xcd, 0x17, 0xc8, 0xf3, 0x5b, 0x63,
+ 0xb7, 0xe7, 0x1b, 0xe4, 0x3e, 0xc7, 0x3b, 0x38, 0x60, 0x6a, 0x40, 0xbe,
+ 0xfc, 0x1, 0xaa, 0xd4, 0xe, 0xc6, 0x4b, 0x77, 0x57, 0x1a, 0x5e, 0xd5,
+ 0xda, 0x5e, 0x37, 0xff, 0x27, 0x0, 0x1d, 0x1d, 0x22, 0x4c, 0x19, 0x11,
+ 0x7f, 0x8d, 0xc8, 0x33, 0x5d, 0x5b, 0xd1, 0x7b, 0x7e, 0x76, 0x6c, 0x73,
+ 0xcb, 0xcd, 0x83, 0x96, 0xc0, 0x47, 0xe5, 0x51, 0xa9, 0xc9, 0xf3, 0xdd,
+ 0xdb, 0xb9, 0x11, 0xa8, 0x9d, 0x67, 0x78, 0x7e, 0x80, 0xa1, 0xdb, 0x6e,
+ 0x1b, 0xdc, 0xae, 0xb1, 0x74, 0xe7, 0x76, 0xa1, 0xea, 0xd4, 0xf3, 0xc,
+ 0x3, 0x60, 0xa0, 0x2f, 0x77, 0x6d, 0x77, 0x71, 0x59, 0xd1, 0x9a, 0x66,
+ 0x8d, 0x6c, 0xf2, 0xb0, 0xaf, 0xde, 0xed, 0xb9, 0x3f, 0xfb, 0x5d, 0x62,
+ 0x26, 0x55, 0x7, 0xf3, 0x46, 0xdb, 0xed, 0x4, 0xe8, 0x7d, 0xcc, 0x89,
+ 0xd6, 0xe6, 0x75, 0x7, 0xc0, 0x58, 0x19, 0xb0, 0xcb, 0x53, 0x5e, 0x47,
+ 0xb4, 0xe7, 0x5d, 0x12, 0xf, 0x38, 0x44, 0x90, 0x66, 0xa2, 0xc8, 0xaf,
+ 0xef, 0x6c, 0xe8, 0xbf, 0x98, 0xd6, 0xe, 0x78, 0xc4, 0x66, 0x9b, 0xc9,
+ 0x50, 0x7c, 0x64, 0x6f, 0x70, 0x26, 0x2d, 0xf9, 0x8, 0x1, 0x66, 0xe2,
+ 0x80, 0xe5, 0xf, 0xd0, 0x25, 0xa3, 0x3, 0x6f, 0xe1, 0x41, 0xf0, 0xd6,
+ 0x7c, 0xef, 0x26, 0x88, 0x1f, 0xd2, 0x31, 0x5b, 0xd, 0x9c, 0x75, 0xfc,
+ 0x5, 0x25, 0xe6, 0x76, 0xfd, 0x5, 0x51, 0x19, 0x11, 0x84, 0xf8, 0x33,
+ 0x9d, 0xe1, 0xf2, 0x9, 0x58, 0x40, 0x98, 0xda, 0x22, 0xb0, 0x50, 0xc4,
+ 0xcb, 0x37, 0xb6, 0x35, 0x86, 0x65, 0x51, 0x75, 0xa8, 0xb7, 0x65, 0x7a,
+ 0x3a, 0x9c, 0x3d, 0xa9, 0xdb, 0xe9, 0x96, 0xcf, 0xa4, 0xb5, 0x5d, 0xe5,
+ 0x93, 0x69, 0xd1, 0xb1, 0x8c, 0x3c, 0x6e, 0x93, 0xab, 0x4e, 0xdd, 0xcc,
+ 0x3c, 0xc4, 0xef, 0xb7, 0xb6, 0xb, 0x21, 0xf5, 0x33, 0xa0, 0x7e, 0xa6,
+ 0x41, 0x99, 0xb0, 0x60, 0x47, 0xa8, 0xdc, 0xbf, 0x31, 0xa2, 0x9d, 0x27,
+ 0x30, 0xb6, 0x40, 0xdf, 0x99, 0x53, 0xe6, 0xc5, 0x1, 0x4a, 0x69, 0xb3,
+ 0xc1, 0x86, 0xda, 0xf0, 0xf7, 0xf, 0xd5, 0xeb, 0xdf, 0xa8, 0x64, 0x7b,
+ 0x73, 0xf0, 0xf6, 0xee, 0xd3, 0x96, 0xf0, 0xf1, 0x64, 0xf1, 0xfb, 0x97,
+ 0x80, 0xac, 0xae, 0x3f, 0x58, 0x15, 0x2b, 0x1e, 0xa4, 0xc3, 0xf6, 0xf5,
+ 0xa6, 0x8e, 0x41, 0xca, 0xc4, 0xdf, 0xb, 0x9a, 0xdb, 0x49, 0xd4, 0x34,
+ 0x91, 0xd7, 0x67, 0x87, 0x98, 0xc4, 0xda, 0x72, 0xbe, 0x69, 0x95, 0x8c,
+ 0x4, 0x9f, 0xb6, 0x3, 0x5e, 0x23, 0xb0, 0xc2, 0x90, 0xa2, 0x6d, 0xbb,
+ 0xe0, 0xc4, 0xab, 0x82, 0xe2, 0x3a, 0x7c, 0xfd, 0x51, 0x38, 0xbc, 0x8,
+ 0xb0, 0x8, 0xb0, 0x8, 0xf0, 0xa7, 0x9, 0x80, 0x4f, 0x23, 0x40, 0xe7,
+ 0x0, 0x11, 0xc4, 0x83, 0x63, 0x1b, 0x1c, 0x40, 0x3d, 0x22, 0x44, 0x9a,
+ 0x3, 0x44, 0x10, 0x33, 0xc5, 0x9b, 0x1d, 0x40, 0xb0, 0xcc, 0xf6, 0xd0,
+ 0xdd, 0xcb, 0xe, 0xf8, 0x39, 0x61, 0xa2, 0xc9, 0xf0, 0xaa, 0x68, 0x67,
+ 0xb1, 0x79, 0x9d, 0xfb, 0x6f, 0x8e, 0x10, 0xd1, 0x22, 0x8b, 0x7b, 0xb7,
+ 0x8f, 0x5f, 0xf6, 0xee, 0xbf, 0x59, 0x8, 0xb6, 0xee, 0x4e, 0x5f, 0xd9,
+ 0xcd, 0xe6, 0x6c, 0x4b, 0xc0, 0x11, 0xfa, 0x1d, 0xda, 0xa8, 0x97, 0xe3,
+ 0xd1, 0x76, 0x2b, 0x1, 0x40, 0xe9, 0xa3, 0xeb, 0xdd, 0xb6, 0xe1, 0x6a,
+ 0x7, 0x96, 0x6a, 0x34, 0x84, 0x46, 0xf3, 0x80, 0x60, 0x2, 0xcc, 0xc3,
+ 0x4, 0xa0, 0x88, 0xef, 0xef, 0xdf, 0x4e, 0x19, 0x4, 0x9f, 0x4b, 0xb,
+ 0x3c, 0x61, 0x7, 0x4c, 0x65, 0x7b, 0x3c, 0x1, 0x86, 0xa6, 0xa, 0xa2,
+ 0x7b, 0x82, 0x0, 0x53, 0x71, 0xc0, 0x72, 0x89, 0x2d, 0x7f, 0xc0, 0x22,
+ 0x80, 0x62, 0x9a, 0xc5, 0x2, 0x44, 0x7a, 0x27, 0x94, 0xdc, 0x19, 0x20,
+ 0xf2, 0x3a, 0x78, 0xd9, 0xc6, 0xfb, 0xbd, 0xfd, 0x5, 0x63, 0xfd, 0x3,
+ 0xe2, 0x7a, 0x67, 0x7a, 0x3a, 0x63, 0xab, 0x9, 0x5, 0xaf, 0xf3, 0xb4,
+ 0x4d, 0x46, 0xf8, 0x24, 0xa7, 0x66, 0x44, 0x6b, 0x5b, 0x24, 0x7e, 0xe6,
+ 0x17, 0x88, 0xf8, 0x81, 0x58, 0x5b, 0xa9, 0x17, 0x21, 0xba, 0x3f, 0xfe,
+ 0xfe, 0x8a, 0x16, 0x2f, 0xf7, 0x92, 0x46, 0xfa, 0x7, 0xb4, 0x19, 0xba,
+ 0x3f, 0xd9, 0xd8, 0x40, 0x83, 0xe9, 0x86, 0xbd, 0x5a, 0xc5, 0x67, 0xd4,
+ 0xcf, 0xcc, 0xb4, 0xb, 0x0, 0x94, 0xbf, 0xdf, 0xbe, 0x3d, 0xae, 0xd6,
+ 0x96, 0x7b, 0xd0, 0x36, 0x9e, 0xc0, 0xe, 0xe8, 0x1e, 0x1f, 0x62, 0xf7,
+ 0x58, 0xfc, 0xfe, 0x8, 0x16, 0xf0, 0x78, 0xa2, 0xaf, 0x7b, 0x0, 0xae,
+ 0x84, 0x70, 0xaa, 0xe6, 0xf6, 0xf6, 0x12, 0x2b, 0xe3, 0x2b, 0xae, 0x2f,
+ 0xea, 0x33, 0x6, 0xdb, 0x22, 0x20, 0xa, 0x91, 0xf0, 0x81, 0xbf, 0xf0,
+ 0xf1, 0x2, 0x70, 0xc6, 0x97, 0xd0, 0x98, 0x5b, 0xa6, 0x38, 0x25, 0x34,
+ 0x3e, 0xfe, 0xf5, 0x13, 0x43, 0xbf, 0x7f, 0xbd, 0x3f, 0xe0, 0x1f, 0x5f,
+ 0x4e, 0x4b, 0x19, 0x56, 0xcb, 0xb5, 0x20, 0x0, 0x0, 0x0, 0x0, 0x49,
+ 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82};
+const unsigned short sGlyphWidths[256] = {
+ 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
+ 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 4, 3, 5, 7, 7, 12,
+ 9, 2, 4, 4, 5, 8, 4, 4, 4, 4, 7, 7, 7, 7, 7, 7, 7, 7, 7,
+ 7, 4, 4, 8, 8, 8, 7, 13, 9, 9, 9, 9, 9, 8, 10, 9, 3, 6, 9,
+ 7, 11, 9, 10, 9, 10, 9, 9, 7, 9, 9, 13, 7, 9, 7, 4, 4, 4, 5,
+ 7, 4, 7, 7, 7, 7, 7, 3, 7, 7, 3, 3, 7, 3, 11, 7, 7, 7, 7,
+ 4, 7, 4, 7, 5, 9, 7, 7, 7, 4, 3, 4, 8, 10, 7, 10, 3, 7, 4,
+ 13, 7, 7, 4, 14, 9, 4, 13, 10, 7, 10, 10, 3, 3, 4, 4, 5, 7, 13,
+ 4, 13, 7, 4, 12, 10, 7, 9, 4, 3, 7, 7, 7, 7, 3, 7, 4, 10, 4,
+ 7, 8, 4, 10, 7, 5, 7, 4, 4, 4, 7, 7, 4, 4, 4, 5, 7, 11, 11,
+ 11, 8, 9, 9, 9, 9, 9, 9, 13, 9, 9, 9, 9, 9, 3, 3, 3, 3, 9,
+ 9, 10, 10, 10, 10, 10, 8, 10, 9, 9, 9, 9, 9, 9, 9, 7, 7, 7, 7,
+ 7, 7, 12, 7, 7, 7, 7, 7, 3, 3, 3, 3, 7, 7, 7, 7, 7, 7, 7,
+ 7, 7, 7, 7, 7, 7, 7, 7, 7};
+} // namespace normal_font
+
+const FontBitmapInfo sDefaultCompositorFont = {
+ mozilla::Nothing(),
+ mozilla::Some(&normal_font::sGlyphWidths[0]),
+ 256,
+ 256,
+ 16,
+ 16,
+ 0,
+ normal_font::sFontPNG,
+ sizeof(normal_font::sFontPNG)};
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/composite/FrameUniformityData.cpp b/gfx/layers/composite/FrameUniformityData.cpp
new file mode 100644
index 0000000000..2589de292e
--- /dev/null
+++ b/gfx/layers/composite/FrameUniformityData.cpp
@@ -0,0 +1,38 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "FrameUniformityData.h"
+
+#include "mozilla/TimeStamp.h"
+#include "mozilla/dom/APZTestDataBinding.h"
+#include "mozilla/dom/ToJSValue.h"
+#include "nsTArray.h"
+
+namespace mozilla {
+namespace layers {
+
+using namespace gfx;
+
+bool FrameUniformityData::ToJS(JS::MutableHandle<JS::Value> aOutValue,
+ JSContext* aContext) {
+ dom::FrameUniformityResults results;
+ dom::Sequence<dom::FrameUniformity>& layers =
+ results.mLayerUniformities.Construct();
+
+ for (const auto& [layerAddr, uniformity] : mUniformities) {
+ // FIXME: Make this infallible after bug 968520 is done.
+ MOZ_ALWAYS_TRUE(layers.AppendElement(fallible));
+ dom::FrameUniformity& entry = layers.LastElement();
+
+ entry.mLayerAddress.Construct() = layerAddr;
+ entry.mFrameUniformity.Construct() = uniformity;
+ }
+
+ return dom::ToJSValue(aContext, results, aOutValue);
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/composite/FrameUniformityData.h b/gfx/layers/composite/FrameUniformityData.h
new file mode 100644
index 0000000000..d1e69065af
--- /dev/null
+++ b/gfx/layers/composite/FrameUniformityData.h
@@ -0,0 +1,49 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_FrameUniformityData_h_
+#define mozilla_layers_FrameUniformityData_h_
+
+#include "ipc/IPCMessageUtils.h"
+#include "js/TypeDecls.h"
+#include "ipc/IPCMessageUtilsSpecializations.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/UniquePtr.h"
+#include "nsTArray.h"
+
+namespace mozilla {
+namespace layers {
+
+class FrameUniformityData {
+ friend struct IPC::ParamTraits<FrameUniformityData>;
+
+ public:
+ bool ToJS(JS::MutableHandle<JS::Value> aOutValue, JSContext* aContext);
+ // Contains the calculated frame uniformities
+ std::map<uintptr_t, float> mUniformities;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+namespace IPC {
+template <>
+struct ParamTraits<mozilla::layers::FrameUniformityData> {
+ typedef mozilla::layers::FrameUniformityData paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.mUniformities);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ return ParamTraitsStd<std::map<uintptr_t, float>>::Read(
+ aReader, &aResult->mUniformities);
+ }
+};
+
+} // namespace IPC
+
+#endif // mozilla_layers_FrameUniformityData_h_
diff --git a/gfx/layers/composite/GPUVideoTextureHost.cpp b/gfx/layers/composite/GPUVideoTextureHost.cpp
new file mode 100644
index 0000000000..69080c984a
--- /dev/null
+++ b/gfx/layers/composite/GPUVideoTextureHost.cpp
@@ -0,0 +1,236 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "GPUVideoTextureHost.h"
+
+#include "ImageContainer.h"
+#include "mozilla/RemoteDecoderManagerParent.h"
+#include "mozilla/layers/ImageBridgeParent.h"
+#include "mozilla/layers/VideoBridgeParent.h"
+#include "mozilla/webrender/RenderTextureHostWrapper.h"
+#include "mozilla/webrender/RenderThread.h"
+
+namespace mozilla {
+namespace layers {
+
+GPUVideoTextureHost::GPUVideoTextureHost(
+ const dom::ContentParentId& aContentId, TextureFlags aFlags,
+ const SurfaceDescriptorGPUVideo& aDescriptor)
+ : TextureHost(TextureHostType::Unknown, aFlags),
+ mContentId(aContentId),
+ mDescriptor(aDescriptor) {
+ MOZ_COUNT_CTOR(GPUVideoTextureHost);
+}
+
+GPUVideoTextureHost::~GPUVideoTextureHost() {
+ MOZ_COUNT_DTOR(GPUVideoTextureHost);
+}
+
+GPUVideoTextureHost* GPUVideoTextureHost::CreateFromDescriptor(
+ const dom::ContentParentId& aContentId, TextureFlags aFlags,
+ const SurfaceDescriptorGPUVideo& aDescriptor) {
+ return new GPUVideoTextureHost(aContentId, aFlags, aDescriptor);
+}
+
+TextureHost* GPUVideoTextureHost::EnsureWrappedTextureHost() {
+ if (mWrappedTextureHost) {
+ return mWrappedTextureHost;
+ }
+
+ const auto& sd =
+ static_cast<const SurfaceDescriptorRemoteDecoder&>(mDescriptor);
+ VideoBridgeParent* parent = VideoBridgeParent::GetSingleton(sd.source());
+ if (!parent) {
+ // The VideoBridge went away. This can happen if the RDD process
+ // crashes.
+ return nullptr;
+ }
+
+ mWrappedTextureHost = parent->LookupTexture(mContentId, sd.handle());
+
+ if (!mWrappedTextureHost) {
+ // The TextureHost hasn't been registered yet. This is due to a race
+ // between the ImageBridge (content) and the VideoBridge (RDD) and the
+ // ImageBridge won. See bug
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1630733#c14 for more
+ // details.
+ return nullptr;
+ }
+
+ if (mExternalImageId.isSome()) {
+ // External image id is allocated by mWrappedTextureHost.
+ mWrappedTextureHost->EnsureRenderTexture(Nothing());
+ MOZ_ASSERT(mWrappedTextureHost->mExternalImageId.isSome());
+ auto wrappedId = mWrappedTextureHost->mExternalImageId.ref();
+
+ RefPtr<wr::RenderTextureHost> texture =
+ new wr::RenderTextureHostWrapper(wrappedId);
+ wr::RenderThread::Get()->RegisterExternalImage(mExternalImageId.ref(),
+ texture.forget());
+ }
+
+ return mWrappedTextureHost;
+}
+
+bool GPUVideoTextureHost::IsValid() { return !!EnsureWrappedTextureHost(); }
+
+gfx::YUVColorSpace GPUVideoTextureHost::GetYUVColorSpace() const {
+ MOZ_ASSERT(mWrappedTextureHost, "Image isn't valid yet");
+ if (!mWrappedTextureHost) {
+ return TextureHost::GetYUVColorSpace();
+ }
+ return mWrappedTextureHost->GetYUVColorSpace();
+}
+
+gfx::ColorDepth GPUVideoTextureHost::GetColorDepth() const {
+ MOZ_ASSERT(mWrappedTextureHost, "Image isn't valid yet");
+ if (!mWrappedTextureHost) {
+ return TextureHost::GetColorDepth();
+ }
+ return mWrappedTextureHost->GetColorDepth();
+}
+
+gfx::ColorRange GPUVideoTextureHost::GetColorRange() const {
+ MOZ_ASSERT(mWrappedTextureHost, "Image isn't valid yet");
+ if (!mWrappedTextureHost) {
+ return TextureHost::GetColorRange();
+ }
+ return mWrappedTextureHost->GetColorRange();
+}
+
+gfx::IntSize GPUVideoTextureHost::GetSize() const {
+ MOZ_ASSERT(mWrappedTextureHost, "Image isn't valid yet");
+ if (!mWrappedTextureHost) {
+ return gfx::IntSize();
+ }
+ return mWrappedTextureHost->GetSize();
+}
+
+gfx::SurfaceFormat GPUVideoTextureHost::GetFormat() const {
+ MOZ_ASSERT(mWrappedTextureHost, "Image isn't valid yet");
+ if (!mWrappedTextureHost) {
+ return gfx::SurfaceFormat::UNKNOWN;
+ }
+ return mWrappedTextureHost->GetFormat();
+}
+
+void GPUVideoTextureHost::CreateRenderTexture(
+ const wr::ExternalImageId& aExternalImageId) {
+ MOZ_ASSERT(mExternalImageId.isSome());
+
+ // When mWrappedTextureHost already exist, call CreateRenderTexture() here.
+ // In other cases, EnsureWrappedTextureHost() handles CreateRenderTexture().
+
+ if (mWrappedTextureHost) {
+ // External image id is allocated by mWrappedTextureHost.
+ mWrappedTextureHost->EnsureRenderTexture(Nothing());
+ MOZ_ASSERT(mWrappedTextureHost->mExternalImageId.isSome());
+ auto wrappedId = mWrappedTextureHost->mExternalImageId.ref();
+
+ RefPtr<wr::RenderTextureHost> texture =
+ new wr::RenderTextureHostWrapper(wrappedId);
+ wr::RenderThread::Get()->RegisterExternalImage(mExternalImageId.ref(),
+ texture.forget());
+ return;
+ }
+
+ EnsureWrappedTextureHost();
+}
+
+void GPUVideoTextureHost::MaybeDestroyRenderTexture() {
+ if (mExternalImageId.isNothing() || !mWrappedTextureHost) {
+ // RenderTextureHost was not created
+ return;
+ }
+ // When GPUVideoTextureHost created RenderTextureHost, delete it here.
+ TextureHost::DestroyRenderTexture(mExternalImageId.ref());
+}
+
+uint32_t GPUVideoTextureHost::NumSubTextures() {
+ if (!EnsureWrappedTextureHost()) {
+ return 0;
+ }
+ return EnsureWrappedTextureHost()->NumSubTextures();
+}
+
+void GPUVideoTextureHost::PushResourceUpdates(
+ wr::TransactionBuilder& aResources, ResourceUpdateOp aOp,
+ const Range<wr::ImageKey>& aImageKeys, const wr::ExternalImageId& aExtID) {
+ MOZ_ASSERT(EnsureWrappedTextureHost(), "Image isn't valid yet");
+ if (!EnsureWrappedTextureHost()) {
+ return;
+ }
+ EnsureWrappedTextureHost()->PushResourceUpdates(aResources, aOp, aImageKeys,
+ aExtID);
+}
+
+void GPUVideoTextureHost::PushDisplayItems(
+ wr::DisplayListBuilder& aBuilder, const wr::LayoutRect& aBounds,
+ const wr::LayoutRect& aClip, wr::ImageRendering aFilter,
+ const Range<wr::ImageKey>& aImageKeys, PushDisplayItemFlagSet aFlags) {
+ MOZ_ASSERT(EnsureWrappedTextureHost(), "Image isn't valid yet");
+ MOZ_ASSERT(aImageKeys.length() > 0);
+ if (!EnsureWrappedTextureHost()) {
+ return;
+ }
+
+ EnsureWrappedTextureHost()->PushDisplayItems(aBuilder, aBounds, aClip,
+ aFilter, aImageKeys, aFlags);
+}
+
+bool GPUVideoTextureHost::SupportsExternalCompositing(
+ WebRenderBackend aBackend) {
+ if (!EnsureWrappedTextureHost()) {
+ return false;
+ }
+ return EnsureWrappedTextureHost()->SupportsExternalCompositing(aBackend);
+}
+
+void GPUVideoTextureHost::UnbindTextureSource() {
+ if (EnsureWrappedTextureHost()) {
+ EnsureWrappedTextureHost()->UnbindTextureSource();
+ }
+ // Handle read unlock
+ TextureHost::UnbindTextureSource();
+}
+
+void GPUVideoTextureHost::NotifyNotUsed() {
+ if (EnsureWrappedTextureHost()) {
+ EnsureWrappedTextureHost()->NotifyNotUsed();
+ }
+ TextureHost::NotifyNotUsed();
+}
+
+BufferTextureHost* GPUVideoTextureHost::AsBufferTextureHost() {
+ if (EnsureWrappedTextureHost()) {
+ return EnsureWrappedTextureHost()->AsBufferTextureHost();
+ }
+ return nullptr;
+}
+
+bool GPUVideoTextureHost::IsWrappingSurfaceTextureHost() {
+ if (EnsureWrappedTextureHost()) {
+ return EnsureWrappedTextureHost()->IsWrappingSurfaceTextureHost();
+ }
+ return false;
+}
+
+TextureHostType GPUVideoTextureHost::GetTextureHostType() {
+ if (!mWrappedTextureHost) {
+ return TextureHostType::Unknown;
+ }
+ return mWrappedTextureHost->GetTextureHostType();
+}
+
+bool GPUVideoTextureHost::NeedsDeferredDeletion() const {
+ if (!mWrappedTextureHost) {
+ return TextureHost::NeedsDeferredDeletion();
+ }
+ return mWrappedTextureHost->NeedsDeferredDeletion();
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/composite/GPUVideoTextureHost.h b/gfx/layers/composite/GPUVideoTextureHost.h
new file mode 100644
index 0000000000..b4f6cc249e
--- /dev/null
+++ b/gfx/layers/composite/GPUVideoTextureHost.h
@@ -0,0 +1,92 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_GFX_GPUVIDEOTEXTUREHOST_H
+#define MOZILLA_GFX_GPUVIDEOTEXTUREHOST_H
+
+#include "mozilla/layers/TextureHost.h"
+
+namespace mozilla {
+namespace layers {
+
+class GPUVideoTextureHost : public TextureHost {
+ public:
+ static GPUVideoTextureHost* CreateFromDescriptor(
+ const dom::ContentParentId& aContentId, TextureFlags aFlags,
+ const SurfaceDescriptorGPUVideo& aDescriptor);
+
+ virtual ~GPUVideoTextureHost();
+
+ void DeallocateDeviceData() override {}
+
+ gfx::SurfaceFormat GetFormat() const override;
+
+ already_AddRefed<gfx::DataSourceSurface> GetAsSurface() override {
+ return nullptr; // XXX - implement this (for MOZ_DUMP_PAINTING)
+ }
+
+ gfx::YUVColorSpace GetYUVColorSpace() const override;
+ gfx::ColorDepth GetColorDepth() const override;
+ gfx::ColorRange GetColorRange() const override;
+
+ gfx::IntSize GetSize() const override;
+
+ bool IsValid() override;
+
+#ifdef MOZ_LAYERS_HAVE_LOG
+ const char* Name() override { return "GPUVideoTextureHost"; }
+#endif
+
+ void CreateRenderTexture(
+ const wr::ExternalImageId& aExternalImageId) override;
+
+ void MaybeDestroyRenderTexture() override;
+
+ uint32_t NumSubTextures() override;
+
+ void PushResourceUpdates(wr::TransactionBuilder& aResources,
+ ResourceUpdateOp aOp,
+ const Range<wr::ImageKey>& aImageKeys,
+ const wr::ExternalImageId& aExtID) override;
+
+ void PushDisplayItems(wr::DisplayListBuilder& aBuilder,
+ const wr::LayoutRect& aBounds,
+ const wr::LayoutRect& aClip, wr::ImageRendering aFilter,
+ const Range<wr::ImageKey>& aImageKeys,
+ PushDisplayItemFlagSet aFlags) override;
+
+ bool SupportsExternalCompositing(WebRenderBackend aBackend) override;
+
+ void UnbindTextureSource() override;
+
+ void NotifyNotUsed() override;
+
+ BufferTextureHost* AsBufferTextureHost() override;
+
+ bool IsWrappingSurfaceTextureHost() override;
+
+ TextureHostType GetTextureHostType() override;
+
+ bool NeedsDeferredDeletion() const override;
+
+ const dom::ContentParentId& GetContentId() const { return mContentId; }
+
+ protected:
+ GPUVideoTextureHost(const dom::ContentParentId& aContentId,
+ TextureFlags aFlags,
+ const SurfaceDescriptorGPUVideo& aDescriptor);
+
+ TextureHost* EnsureWrappedTextureHost();
+
+ RefPtr<TextureHost> mWrappedTextureHost;
+ dom::ContentParentId mContentId;
+ SurfaceDescriptorGPUVideo mDescriptor;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // MOZILLA_GFX_GPUVIDEOTEXTUREHOST_H
diff --git a/gfx/layers/composite/ImageComposite.cpp b/gfx/layers/composite/ImageComposite.cpp
new file mode 100644
index 0000000000..7c36519843
--- /dev/null
+++ b/gfx/layers/composite/ImageComposite.cpp
@@ -0,0 +1,378 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ImageComposite.h"
+
+#include <inttypes.h>
+
+#include "mozilla/ProfilerMarkers.h"
+#include "gfxPlatform.h"
+#include "nsPrintfCString.h"
+
+namespace mozilla {
+
+using namespace gfx;
+
+namespace layers {
+
+/* static */ const float ImageComposite::BIAS_TIME_MS = 1.0f;
+
+ImageComposite::ImageComposite() = default;
+
+ImageComposite::~ImageComposite() = default;
+
+TimeStamp ImageComposite::GetBiasedTime(const TimeStamp& aInput) const {
+ switch (mBias) {
+ case ImageComposite::BIAS_NEGATIVE:
+ return aInput - TimeDuration::FromMilliseconds(BIAS_TIME_MS);
+ case ImageComposite::BIAS_POSITIVE:
+ return aInput + TimeDuration::FromMilliseconds(BIAS_TIME_MS);
+ default:
+ return aInput;
+ }
+}
+
+void ImageComposite::UpdateBias(size_t aImageIndex, bool aFrameChanged) {
+ MOZ_ASSERT(aImageIndex < ImagesCount());
+
+ TimeStamp compositionTime = GetCompositionTime();
+ TimeStamp compositedImageTime = mImages[aImageIndex].mTimeStamp;
+ TimeStamp nextImageTime = aImageIndex + 1 < ImagesCount()
+ ? mImages[aImageIndex + 1].mTimeStamp
+ : TimeStamp();
+
+ if (profiler_thread_is_being_profiled_for_markers() && compositedImageTime &&
+ nextImageTime) {
+ TimeDuration offsetCurrent = compositedImageTime - compositionTime;
+ TimeDuration offsetNext = nextImageTime - compositionTime;
+ nsPrintfCString str("current %.2lfms, next %.2lfms",
+ offsetCurrent.ToMilliseconds(),
+ offsetNext.ToMilliseconds());
+ PROFILER_MARKER_TEXT("Video frame offsets", GRAPHICS, {}, str);
+ }
+
+ if (compositedImageTime.IsNull()) {
+ mBias = ImageComposite::BIAS_NONE;
+ return;
+ }
+ TimeDuration threshold = TimeDuration::FromMilliseconds(1.5);
+ if (compositionTime - compositedImageTime < threshold &&
+ compositionTime - compositedImageTime > -threshold) {
+ // The chosen frame's time is very close to the composition time (probably
+ // just before the current composition time, but due to previously set
+ // negative bias, it could be just after the current composition time too).
+ // If the inter-frame time is almost exactly equal to (a multiple of)
+ // the inter-composition time, then we're in a dangerous situation because
+ // jitter might cause frames to fall one side or the other of the
+ // composition times, causing many frames to be skipped or duplicated.
+ // Try to prevent that by adding a negative bias to the frame times during
+ // the next composite; that should ensure the next frame's time is treated
+ // as falling just before a composite time.
+ mBias = ImageComposite::BIAS_NEGATIVE;
+ return;
+ }
+ if (!nextImageTime.IsNull() && nextImageTime - compositionTime < threshold &&
+ nextImageTime - compositionTime > -threshold) {
+ // The next frame's time is very close to our composition time (probably
+ // just after the current composition time, but due to previously set
+ // positive bias, it could be just before the current composition time too).
+ // We're in a dangerous situation because jitter might cause frames to
+ // fall one side or the other of the composition times, causing many frames
+ // to be skipped or duplicated.
+ // Specifically, the next composite is at risk of picking the "next + 1"
+ // frame rather than the "next" frame, which would cause the "next" frame to
+ // be skipped. Try to prevent that by adding a positive bias to the frame
+ // times during the next composite; if the inter-frame time is almost
+ // exactly equal to the inter-composition time, that should ensure that the
+ // next + 1 frame falls just *after* the next composition time, and the next
+ // composite should then pick the next frame rather than the next + 1 frame.
+ mBias = ImageComposite::BIAS_POSITIVE;
+ return;
+ }
+ if (aFrameChanged) {
+ // The current and next video frames are a sufficient distance from the
+ // composition time and we can reliably pick the right frame without bias.
+ // Reset the bias.
+ // We only do this when the frame changed. Otherwise, when playing a 30fps
+ // video on a 60fps display, we'd keep resetting the bias during the "middle
+ // frames".
+ mBias = ImageComposite::BIAS_NONE;
+ }
+}
+
+int ImageComposite::ChooseImageIndex() {
+ // ChooseImageIndex is called for all images in the layer when it is visible.
+ // Change to this behaviour would break dropped frames counting calculation:
+ // We rely on this assumption to determine if during successive runs an
+ // image is returned that isn't the one following immediately the previous one
+ if (mImages.IsEmpty()) {
+ return -1;
+ }
+
+ TimeStamp compositionTime = GetCompositionTime();
+ auto compositionOpportunityId = GetCompositionOpportunityId();
+ if (compositionTime &&
+ compositionOpportunityId != mLastChooseImageIndexComposition) {
+ // We are inside a composition, in the first call to ChooseImageIndex during
+ // this composition.
+ // Find the newest frame whose biased timestamp is at or before
+ // `compositionTime`.
+ uint32_t imageIndex = 0;
+ while (imageIndex + 1 < mImages.Length() &&
+ mImages[imageIndex + 1].mTextureHost->IsValid() &&
+ GetBiasedTime(mImages[imageIndex + 1].mTimeStamp) <=
+ compositionTime) {
+ ++imageIndex;
+ }
+
+ if (!mImages[imageIndex].mTextureHost->IsValid()) {
+ // Still not ready to be shown.
+ return -1;
+ }
+
+ bool wasVisibleAtPreviousComposition =
+ compositionOpportunityId == mLastChooseImageIndexComposition.Next();
+
+ bool frameChanged =
+ UpdateCompositedFrame(imageIndex, wasVisibleAtPreviousComposition);
+ UpdateBias(imageIndex, frameChanged);
+
+ mLastChooseImageIndexComposition = compositionOpportunityId;
+
+ return imageIndex;
+ }
+
+ // We've been called before during this composition, or we're not in a
+ // composition. Just return the last image we picked (if it's one of the
+ // current images).
+ for (uint32_t i = 0; i < mImages.Length(); ++i) {
+ if (mImages[i].mFrameID == mLastFrameID &&
+ mImages[i].mProducerID == mLastProducerID) {
+ return i;
+ }
+ }
+
+ return 0;
+}
+
+const ImageComposite::TimedImage* ImageComposite::ChooseImage() {
+ int index = ChooseImageIndex();
+ return index >= 0 ? &mImages[index] : nullptr;
+}
+
+void ImageComposite::RemoveImagesWithTextureHost(TextureHost* aTexture) {
+ for (int32_t i = mImages.Length() - 1; i >= 0; --i) {
+ if (mImages[i].mTextureHost == aTexture) {
+ aTexture->UnbindTextureSource();
+ mImages.RemoveElementAt(i);
+ }
+ }
+}
+
+void ImageComposite::ClearImages() { mImages.Clear(); }
+
+void ImageComposite::SetImages(nsTArray<TimedImage>&& aNewImages) {
+ if (!aNewImages.IsEmpty()) {
+ DetectTimeStampJitter(&aNewImages[0]);
+
+ // Frames older than the first frame in aNewImages that we haven't shown yet
+ // will never be shown.
+ CountSkippedFrames(&aNewImages[0]);
+
+ if (profiler_thread_is_being_profiled_for_markers()) {
+ int len = aNewImages.Length();
+ const auto& first = aNewImages[0];
+ const auto& last = aNewImages.LastElement();
+ nsPrintfCString str("%d %s, frameID %" PRId32 " (prod %" PRId32
+ ") to frameID %" PRId32 " (prod %" PRId32 ")",
+ len, len == 1 ? "image" : "images", first.mFrameID,
+ first.mProducerID, last.mFrameID, last.mProducerID);
+ PROFILER_MARKER_TEXT("ImageComposite::SetImages", GRAPHICS, {}, str);
+ }
+ }
+ mImages = std::move(aNewImages);
+}
+
+// Returns whether the frame changed.
+bool ImageComposite::UpdateCompositedFrame(
+ int aImageIndex, bool aWasVisibleAtPreviousComposition) {
+ MOZ_RELEASE_ASSERT(aImageIndex >= 0);
+ MOZ_RELEASE_ASSERT(aImageIndex < static_cast<int>(mImages.Length()));
+ const TimedImage& image = mImages[aImageIndex];
+
+ auto compositionOpportunityId = GetCompositionOpportunityId();
+ TimeStamp compositionTime = GetCompositionTime();
+ MOZ_RELEASE_ASSERT(compositionTime,
+ "Should only be called during a composition");
+
+ nsCString descr;
+ if (profiler_thread_is_being_profiled_for_markers()) {
+ nsCString relativeTimeString;
+ if (image.mTimeStamp) {
+ relativeTimeString.AppendPrintf(
+ " [relative timestamp %.1lfms]",
+ (image.mTimeStamp - compositionTime).ToMilliseconds());
+ }
+ int remainingImages = mImages.Length() - 1 - aImageIndex;
+ static const char* kBiasStrings[] = {"NONE", "NEGATIVE", "POSITIVE"};
+ descr.AppendPrintf(
+ "frameID %" PRId32 " (producerID %" PRId32 ") [composite %" PRIu64
+ "] [bias %s] [%d remaining %s]%s",
+ image.mFrameID, image.mProducerID, compositionOpportunityId.mId,
+ kBiasStrings[mBias], remainingImages,
+ remainingImages == 1 ? "image" : "images", relativeTimeString.get());
+ if (mLastProducerID != image.mProducerID) {
+ descr.AppendPrintf(", previous producerID: %" PRId32, mLastProducerID);
+ } else if (mLastFrameID != image.mFrameID) {
+ descr.AppendPrintf(", previous frameID: %" PRId32, mLastFrameID);
+ } else {
+ descr.AppendLiteral(", no change");
+ }
+ }
+ PROFILER_MARKER_TEXT("UpdateCompositedFrame", GRAPHICS, {}, descr);
+
+ if (mLastFrameID == image.mFrameID && mLastProducerID == image.mProducerID) {
+ // The frame didn't change.
+ return false;
+ }
+
+ CountSkippedFrames(&image);
+
+ int32_t dropped = mSkippedFramesSinceLastComposite;
+ mSkippedFramesSinceLastComposite = 0;
+
+ if (!aWasVisibleAtPreviousComposition) {
+ // This video was not part of the on-screen scene during the previous
+ // composition opportunity, for example it may have been scrolled off-screen
+ // or in a background tab, or compositing might have been paused.
+ // Ignore any skipped frames and don't count them as dropped.
+ dropped = 0;
+ }
+
+ if (dropped > 0) {
+ mDroppedFrames += dropped;
+ if (profiler_thread_is_being_profiled_for_markers()) {
+ const char* frameOrFrames = dropped == 1 ? "frame" : "frames";
+ nsPrintfCString text("%" PRId32 " %s dropped: %" PRId32 " -> %" PRId32
+ " (producer %" PRId32 ")",
+ dropped, frameOrFrames, mLastFrameID, image.mFrameID,
+ mLastProducerID);
+ PROFILER_MARKER_TEXT("Video frames dropped", GRAPHICS, {}, text);
+ }
+ }
+
+ mLastFrameID = image.mFrameID;
+ mLastProducerID = image.mProducerID;
+ mLastFrameUpdateComposition = compositionOpportunityId;
+
+ return true;
+}
+
+void ImageComposite::OnFinishRendering(int aImageIndex,
+ const TimedImage* aImage,
+ base::ProcessId aProcessId,
+ const CompositableHandle& aHandle) {
+ if (mLastFrameUpdateComposition != GetCompositionOpportunityId()) {
+ // The frame did not change in this composition.
+ return;
+ }
+
+ if (aHandle) {
+ ImageCompositeNotificationInfo info;
+ info.mImageBridgeProcessId = aProcessId;
+ info.mNotification = ImageCompositeNotification(
+ aHandle, aImage->mTimeStamp, GetCompositionTime(), mLastFrameID,
+ mLastProducerID);
+ AppendImageCompositeNotification(info);
+ }
+}
+
+const ImageComposite::TimedImage* ImageComposite::GetImage(
+ size_t aIndex) const {
+ if (aIndex >= mImages.Length()) {
+ return nullptr;
+ }
+ return &mImages[aIndex];
+}
+
+void ImageComposite::CountSkippedFrames(const TimedImage* aImage) {
+ if (aImage->mProducerID != mLastProducerID) {
+ // Switched producers.
+ return;
+ }
+
+ if (mImages.IsEmpty() || aImage->mFrameID <= mLastFrameID + 1) {
+ // No frames were skipped.
+ return;
+ }
+
+ uint32_t targetFrameRate = gfxPlatform::TargetFrameRate();
+ if (targetFrameRate == 0) {
+ // Can't know whether we could have reasonably displayed all video frames.
+ return;
+ }
+
+ double targetFrameDurationMS = 1000.0 / targetFrameRate;
+
+ // Count how many images in mImages were skipped between mLastFrameID and
+ // aImage.mFrameID. Only count frames for which we can estimate a duration by
+ // looking at the next frame's timestamp, and only if the video frame rate is
+ // no faster than the target frame rate.
+ int32_t skipped = 0;
+ for (size_t i = 0; i + 1 < mImages.Length(); i++) {
+ const auto& img = mImages[i];
+ if (img.mProducerID != aImage->mProducerID ||
+ img.mFrameID <= mLastFrameID || img.mFrameID >= aImage->mFrameID) {
+ continue;
+ }
+
+ // We skipped img! Estimate img's time duration.
+ const auto& next = mImages[i + 1];
+ if (next.mProducerID != aImage->mProducerID) {
+ continue;
+ }
+
+ MOZ_ASSERT(next.mFrameID > img.mFrameID);
+ TimeDuration duration = next.mTimeStamp - img.mTimeStamp;
+ if (floor(duration.ToMilliseconds()) >= floor(targetFrameDurationMS)) {
+ // Count the frame.
+ skipped++;
+ }
+ }
+
+ mSkippedFramesSinceLastComposite += skipped;
+}
+
+void ImageComposite::DetectTimeStampJitter(const TimedImage* aNewImage) {
+ if (!profiler_thread_is_being_profiled_for_markers() ||
+ aNewImage->mTimeStamp.IsNull()) {
+ return;
+ }
+
+ // Find aNewImage in mImages and compute its timestamp delta, if found.
+ // Ideally, a given video frame should never change its timestamp (jitter
+ // should be zero). However, we re-adjust video frame timestamps based on the
+ // audio clock. If the audio clock drifts compared to the system clock, or if
+ // there are bugs or inaccuracies in the computation of these timestamps,
+ // jitter will be non-zero.
+ Maybe<TimeDuration> jitter;
+ for (const auto& img : mImages) {
+ if (img.mProducerID == aNewImage->mProducerID &&
+ img.mFrameID == aNewImage->mFrameID) {
+ if (!img.mTimeStamp.IsNull()) {
+ jitter = Some(aNewImage->mTimeStamp - img.mTimeStamp);
+ }
+ break;
+ }
+ }
+ if (jitter) {
+ nsPrintfCString text("%.2lfms", jitter->ToMilliseconds());
+ PROFILER_MARKER_TEXT("VideoFrameTimeStampJitter", GRAPHICS, {}, text);
+ }
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/composite/ImageComposite.h b/gfx/layers/composite/ImageComposite.h
new file mode 100644
index 0000000000..05e812c8b3
--- /dev/null
+++ b/gfx/layers/composite/ImageComposite.h
@@ -0,0 +1,139 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_GFX_IMAGECOMPOSITE_H
+#define MOZILLA_GFX_IMAGECOMPOSITE_H
+
+#include "CompositableHost.h" // for CompositableTextureHostRef
+#include "mozilla/gfx/2D.h"
+#include "mozilla/TimeStamp.h" // for TimeStamp
+#include "nsTArray.h"
+
+namespace mozilla {
+namespace layers {
+
+/**
+ * Implements Image selection logic.
+ */
+class ImageComposite {
+ public:
+ static const float BIAS_TIME_MS;
+
+ explicit ImageComposite();
+ virtual ~ImageComposite();
+
+ int32_t GetFrameID() {
+ const TimedImage* img = ChooseImage();
+ return img ? img->mFrameID : -1;
+ }
+
+ int32_t GetProducerID() {
+ const TimedImage* img = ChooseImage();
+ return img ? img->mProducerID : -1;
+ }
+
+ int32_t GetLastFrameID() const { return mLastFrameID; }
+ int32_t GetLastProducerID() const { return mLastProducerID; }
+ uint32_t GetDroppedFramesAndReset() {
+ uint32_t dropped = mDroppedFrames;
+ mDroppedFrames = 0;
+ return dropped;
+ }
+
+ enum Bias {
+ // Don't apply bias to frame times
+ BIAS_NONE,
+ // Apply a negative bias to frame times to keep them before the vsync time
+ BIAS_NEGATIVE,
+ // Apply a positive bias to frame times to keep them after the vsync time
+ BIAS_POSITIVE,
+ };
+
+ protected:
+ virtual TimeStamp GetCompositionTime() const = 0;
+ virtual CompositionOpportunityId GetCompositionOpportunityId() const = 0;
+ virtual void AppendImageCompositeNotification(
+ const ImageCompositeNotificationInfo& aInfo) const = 0;
+
+ struct TimedImage {
+ CompositableTextureHostRef mTextureHost;
+ TimeStamp mTimeStamp;
+ gfx::IntRect mPictureRect;
+ int32_t mFrameID;
+ int32_t mProducerID;
+ };
+
+ /**
+ * ChooseImage is guaranteed to return the same TimedImage every time it's
+ * called during the same composition, up to the end of Composite() ---
+ * it depends only on mImages, mCompositor->GetCompositionTime(), and mBias.
+ * mBias is updated at the end of Composite().
+ */
+ const TimedImage* ChooseImage();
+ int ChooseImageIndex();
+ const TimedImage* GetImage(size_t aIndex) const;
+ size_t ImagesCount() const { return mImages.Length(); }
+ const nsTArray<TimedImage>& Images() const { return mImages; }
+
+ void RemoveImagesWithTextureHost(TextureHost* aTexture);
+ void ClearImages();
+ void SetImages(nsTArray<TimedImage>&& aNewImages);
+
+ protected:
+ // Send ImageComposite notifications and update the ChooseImage bias.
+ void OnFinishRendering(int aImageIndex, const TimedImage* aImage,
+ base::ProcessId aProcessId,
+ const CompositableHandle& aHandle);
+
+ int32_t mLastFrameID = -1;
+ int32_t mLastProducerID = -1;
+
+ private:
+ nsTArray<TimedImage> mImages;
+ TimeStamp GetBiasedTime(const TimeStamp& aInput) const;
+
+ // Called when we know that frames older than aImage will never get another
+ // chance to be displayed.
+ // Counts frames in mImages that were skipped, and adds the skipped count to
+ // mSkippedFramesSinceLastComposite.
+ void CountSkippedFrames(const TimedImage* aImage);
+
+ // Update mLastFrameID and mLastProducerID, and report dropped frames.
+ // Returns whether the frame changed.
+ bool UpdateCompositedFrame(int aImageIndex,
+ bool aWasVisibleAtPreviousComposition);
+
+ void UpdateBias(size_t aImageIndex, bool aFrameUpdated);
+
+ // Emit a profiler marker about video frame timestamp jitter.
+ void DetectTimeStampJitter(const TimedImage* aNewImage);
+
+ /**
+ * Bias to apply to the next frame.
+ */
+ Bias mBias = BIAS_NONE;
+
+ // Video frames that were in mImages but that will never reach the screen.
+ // This count is reset in UpdateCompositedFrame.
+ int32_t mSkippedFramesSinceLastComposite = 0;
+
+ // The number of dropped frames since the last call to
+ // GetDroppedFramesAndReset(). Not all skipped frames are considered "dropped"
+ // - for example, videos that are scrolled out of view or videos in background
+ // tabs are expected to skip frames, and those skipped frames are not counted
+ // as dropped frames.
+ uint32_t mDroppedFrames = 0;
+
+ // The composition opportunity IDs of the last calls to ChooseImageIndex and
+ // UpdateCompositedFrame, respectively.
+ CompositionOpportunityId mLastChooseImageIndexComposition;
+ CompositionOpportunityId mLastFrameUpdateComposition;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // MOZILLA_GFX_IMAGECOMPOSITE_H
diff --git a/gfx/layers/composite/RemoteTextureHostWrapper.cpp b/gfx/layers/composite/RemoteTextureHostWrapper.cpp
new file mode 100644
index 0000000000..b44f3bd36c
--- /dev/null
+++ b/gfx/layers/composite/RemoteTextureHostWrapper.cpp
@@ -0,0 +1,240 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "RemoteTextureHostWrapper.h"
+
+#include "mozilla/gfx/gfxVars.h"
+#include "mozilla/layers/AsyncImagePipelineManager.h"
+#include "mozilla/layers/CompositorThread.h"
+#include "mozilla/layers/RemoteTextureMap.h"
+#include "mozilla/layers/WebRenderTextureHost.h"
+#include "mozilla/StaticPrefs_webgl.h"
+#include "mozilla/webrender/RenderTextureHostWrapper.h"
+#include "mozilla/webrender/RenderThread.h"
+
+namespace mozilla::layers {
+
+/* static */
+RefPtr<TextureHost> RemoteTextureHostWrapper::Create(
+ const RemoteTextureId aTextureId, const RemoteTextureOwnerId aOwnerId,
+ const base::ProcessId aForPid, const gfx::IntSize aSize,
+ const TextureFlags aFlags) {
+ RefPtr<TextureHost> textureHost =
+ new RemoteTextureHostWrapper(aTextureId, aOwnerId, aForPid, aSize,
+ aFlags | TextureFlags::REMOTE_TEXTURE);
+ auto externalImageId = AsyncImagePipelineManager::GetNextExternalImageId();
+ textureHost = new WebRenderTextureHost(aFlags, textureHost, externalImageId);
+ return textureHost;
+}
+
+RemoteTextureHostWrapper::RemoteTextureHostWrapper(
+ const RemoteTextureId aTextureId, const RemoteTextureOwnerId aOwnerId,
+ const base::ProcessId aForPid, const gfx::IntSize aSize,
+ const TextureFlags aFlags)
+ : TextureHost(TextureHostType::Unknown, aFlags),
+ mTextureId(aTextureId),
+ mOwnerId(aOwnerId),
+ mForPid(aForPid),
+ mSize(aSize) {
+ MOZ_COUNT_CTOR(RemoteTextureHostWrapper);
+}
+
+RemoteTextureHostWrapper::~RemoteTextureHostWrapper() {
+ MOZ_COUNT_DTOR(RemoteTextureHostWrapper);
+}
+
+bool RemoteTextureHostWrapper::IsValid() {
+ return !!mRemoteTextureForDisplayList;
+}
+
+gfx::YUVColorSpace RemoteTextureHostWrapper::GetYUVColorSpace() const {
+ MOZ_ASSERT(mRemoteTextureForDisplayList, "TextureHost isn't valid yet");
+ if (!mRemoteTextureForDisplayList) {
+ return TextureHost::GetYUVColorSpace();
+ }
+ return mRemoteTextureForDisplayList->GetYUVColorSpace();
+}
+
+gfx::ColorDepth RemoteTextureHostWrapper::GetColorDepth() const {
+ MOZ_ASSERT(mRemoteTextureForDisplayList, "TextureHost isn't valid yet");
+ if (!mRemoteTextureForDisplayList) {
+ return TextureHost::GetColorDepth();
+ }
+ return mRemoteTextureForDisplayList->GetColorDepth();
+}
+
+gfx::ColorRange RemoteTextureHostWrapper::GetColorRange() const {
+ MOZ_ASSERT(mRemoteTextureForDisplayList, "TextureHost isn't valid yet");
+ if (!mRemoteTextureForDisplayList) {
+ return TextureHost::GetColorRange();
+ }
+ return mRemoteTextureForDisplayList->GetColorRange();
+}
+
+gfx::IntSize RemoteTextureHostWrapper::GetSize() const {
+ MOZ_ASSERT(mRemoteTextureForDisplayList, "TextureHost isn't valid yet");
+ if (!mRemoteTextureForDisplayList) {
+ return gfx::IntSize();
+ }
+ return mRemoteTextureForDisplayList->GetSize();
+}
+
+gfx::SurfaceFormat RemoteTextureHostWrapper::GetFormat() const {
+ MOZ_ASSERT(mRemoteTextureForDisplayList, "TextureHost isn't valid yet");
+ if (!mRemoteTextureForDisplayList) {
+ return gfx::SurfaceFormat::UNKNOWN;
+ }
+ return mRemoteTextureForDisplayList->GetFormat();
+}
+
+void RemoteTextureHostWrapper::CreateRenderTexture(
+ const wr::ExternalImageId& aExternalImageId) {
+ MOZ_ASSERT(mExternalImageId.isSome());
+ MOZ_ASSERT(mRemoteTextureForDisplayList);
+ MOZ_ASSERT(mRemoteTextureForDisplayList->mExternalImageId.isSome());
+
+ if (mIsSyncMode) {
+ // sync mode
+ // mRemoteTextureForDisplayList is also used for WebRender rendering.
+ auto wrappedId = mRemoteTextureForDisplayList->mExternalImageId.ref();
+ RefPtr<wr::RenderTextureHost> texture =
+ new wr::RenderTextureHostWrapper(wrappedId);
+ wr::RenderThread::Get()->RegisterExternalImage(mExternalImageId.ref(),
+ texture.forget());
+ } else {
+ // async mode
+ // mRemoteTextureForDisplayList could be previous remote texture's
+ // TextureHost that is compatible to the mTextureId's TextureHost.
+ // mRemoteTextureForDisplayList might not be used WebRender rendering.
+ RefPtr<wr::RenderTextureHost> texture =
+ new wr::RenderTextureHostWrapper(mTextureId, mOwnerId, mForPid);
+ wr::RenderThread::Get()->RegisterExternalImage(mExternalImageId.ref(),
+ texture.forget());
+ }
+}
+
+void RemoteTextureHostWrapper::MaybeDestroyRenderTexture() {
+ if (mExternalImageId.isNothing() || !mRemoteTextureForDisplayList) {
+ // RenderTextureHost was not created
+ return;
+ }
+ TextureHost::DestroyRenderTexture(mExternalImageId.ref());
+}
+
+uint32_t RemoteTextureHostWrapper::NumSubTextures() {
+ if (!mRemoteTextureForDisplayList) {
+ return 0;
+ }
+ return mRemoteTextureForDisplayList->NumSubTextures();
+}
+
+void RemoteTextureHostWrapper::PushResourceUpdates(
+ wr::TransactionBuilder& aResources, ResourceUpdateOp aOp,
+ const Range<wr::ImageKey>& aImageKeys, const wr::ExternalImageId& aExtID) {
+ MOZ_ASSERT(mRemoteTextureForDisplayList, "TextureHost isn't valid yet");
+ if (!mRemoteTextureForDisplayList) {
+ return;
+ }
+ mRemoteTextureForDisplayList->PushResourceUpdates(aResources, aOp, aImageKeys,
+ aExtID);
+}
+
+void RemoteTextureHostWrapper::PushDisplayItems(
+ wr::DisplayListBuilder& aBuilder, const wr::LayoutRect& aBounds,
+ const wr::LayoutRect& aClip, wr::ImageRendering aFilter,
+ const Range<wr::ImageKey>& aImageKeys, PushDisplayItemFlagSet aFlags) {
+ MOZ_ASSERT(mRemoteTextureForDisplayList, "TextureHost isn't valid yet");
+ MOZ_ASSERT(aImageKeys.length() > 0);
+ if (!mRemoteTextureForDisplayList) {
+ return;
+ }
+ mRemoteTextureForDisplayList->PushDisplayItems(aBuilder, aBounds, aClip,
+ aFilter, aImageKeys, aFlags);
+}
+
+bool RemoteTextureHostWrapper::SupportsExternalCompositing(
+ WebRenderBackend aBackend) {
+ if (!mRemoteTextureForDisplayList) {
+ return false;
+ }
+ return mRemoteTextureForDisplayList->SupportsExternalCompositing(aBackend);
+}
+
+void RemoteTextureHostWrapper::UnbindTextureSource() {}
+
+void RemoteTextureHostWrapper::NotifyNotUsed() {
+ if (mRemoteTextureForDisplayList) {
+ // Release mRemoteTextureForDisplayList.
+ RemoteTextureMap::Get()->ReleaseRemoteTextureHostForDisplayList(this);
+ }
+ MOZ_ASSERT(!mRemoteTextureForDisplayList);
+
+ RemoteTextureMap::Get()->UnregisterRemoteTextureHostWrapper(
+ mTextureId, mOwnerId, mForPid);
+}
+
+TextureHostType RemoteTextureHostWrapper::GetTextureHostType() {
+ if (!mRemoteTextureForDisplayList) {
+ return TextureHostType::Unknown;
+ }
+ return mRemoteTextureForDisplayList->GetTextureHostType();
+}
+
+bool RemoteTextureHostWrapper::IsReadyForRendering() {
+ return !!mRemoteTextureForDisplayList;
+}
+
+void RemoteTextureHostWrapper::ApplyTextureFlagsToRemoteTexture() {
+ if (!mRemoteTextureForDisplayList) {
+ return;
+ }
+ mRemoteTextureForDisplayList->SetFlags(mFlags |
+ TextureFlags::DEALLOCATE_CLIENT);
+}
+
+TextureHost* RemoteTextureHostWrapper::GetRemoteTextureHostForDisplayList(
+ const MonitorAutoLock& aProofOfLock) {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+ return mRemoteTextureForDisplayList;
+}
+
+void RemoteTextureHostWrapper::SetRemoteTextureHostForDisplayList(
+ const MonitorAutoLock& aProofOfLock, TextureHost* aTextureHost,
+ bool aIsSyncMode) {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+ mRemoteTextureForDisplayList = aTextureHost;
+ mIsSyncMode = aIsSyncMode;
+}
+
+void RemoteTextureHostWrapper::ClearRemoteTextureHostForDisplayList(
+ const MonitorAutoLock& aProofOfLoc) {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+ mRemoteTextureForDisplayList = nullptr;
+}
+
+bool RemoteTextureHostWrapper::IsWrappingSurfaceTextureHost() {
+ if (!mRemoteTextureForDisplayList) {
+ return false;
+ }
+ return mRemoteTextureForDisplayList->IsWrappingSurfaceTextureHost();
+}
+
+bool RemoteTextureHostWrapper::NeedsDeferredDeletion() const {
+ if (!mRemoteTextureForDisplayList) {
+ return true;
+ }
+ return mRemoteTextureForDisplayList->NeedsDeferredDeletion();
+}
+
+AndroidHardwareBuffer* RemoteTextureHostWrapper::GetAndroidHardwareBuffer()
+ const {
+ if (!mRemoteTextureForDisplayList) {
+ return nullptr;
+ }
+ return mRemoteTextureForDisplayList->GetAndroidHardwareBuffer();
+}
+
+} // namespace mozilla::layers
diff --git a/gfx/layers/composite/RemoteTextureHostWrapper.h b/gfx/layers/composite/RemoteTextureHostWrapper.h
new file mode 100644
index 0000000000..a27cee72f1
--- /dev/null
+++ b/gfx/layers/composite/RemoteTextureHostWrapper.h
@@ -0,0 +1,124 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_GFX_RemoteTextureHostWrapper_H
+#define MOZILLA_GFX_RemoteTextureHostWrapper_H
+
+#include "mozilla/layers/TextureHost.h"
+#include "mozilla/Monitor.h"
+
+namespace mozilla::layers {
+
+// This class wraps TextureHost of remote texture.
+// In sync mode, mRemoteTextureForDisplayList holds TextureHost of mTextureId.
+// In async mode, it could be previous remote texture's TextureHost that is
+// compatible to the mTextureId's TextureHost.
+class RemoteTextureHostWrapper : public TextureHost {
+ public:
+ static RefPtr<TextureHost> Create(const RemoteTextureId aTextureId,
+ const RemoteTextureOwnerId aOwnerId,
+ const base::ProcessId aForPid,
+ const gfx::IntSize aSize,
+ const TextureFlags aFlags);
+
+ void DeallocateDeviceData() override {}
+
+ gfx::SurfaceFormat GetFormat() const override;
+
+ already_AddRefed<gfx::DataSourceSurface> GetAsSurface() override {
+ return nullptr;
+ }
+
+ gfx::YUVColorSpace GetYUVColorSpace() const override;
+ gfx::ColorDepth GetColorDepth() const override;
+ gfx::ColorRange GetColorRange() const override;
+
+ gfx::IntSize GetSize() const override;
+
+ bool IsValid() override;
+
+#ifdef MOZ_LAYERS_HAVE_LOG
+ const char* Name() override { return "RemoteTextureHostWrapper"; }
+#endif
+
+ void CreateRenderTexture(
+ const wr::ExternalImageId& aExternalImageId) override;
+
+ void MaybeDestroyRenderTexture() override;
+
+ uint32_t NumSubTextures() override;
+
+ void PushResourceUpdates(wr::TransactionBuilder& aResources,
+ ResourceUpdateOp aOp,
+ const Range<wr::ImageKey>& aImageKeys,
+ const wr::ExternalImageId& aExtID) override;
+
+ void PushDisplayItems(wr::DisplayListBuilder& aBuilder,
+ const wr::LayoutRect& aBounds,
+ const wr::LayoutRect& aClip, wr::ImageRendering aFilter,
+ const Range<wr::ImageKey>& aImageKeys,
+ PushDisplayItemFlagSet aFlags) override;
+
+ bool SupportsExternalCompositing(WebRenderBackend aBackend) override;
+
+ void UnbindTextureSource() override;
+
+ void NotifyNotUsed() override;
+
+ RemoteTextureHostWrapper* AsRemoteTextureHostWrapper() override {
+ return this;
+ }
+
+ TextureHostType GetTextureHostType() override;
+
+ bool IsWrappingSurfaceTextureHost() override;
+
+ bool NeedsDeferredDeletion() const override;
+
+ AndroidHardwareBuffer* GetAndroidHardwareBuffer() const override;
+
+ bool IsReadyForRendering();
+
+ void ApplyTextureFlagsToRemoteTexture();
+
+ const RemoteTextureId mTextureId;
+ const RemoteTextureOwnerId mOwnerId;
+ const base::ProcessId mForPid;
+ const gfx::IntSize mSize;
+
+ protected:
+ RemoteTextureHostWrapper(const RemoteTextureId aTextureId,
+ const RemoteTextureOwnerId aOwnerId,
+ const base::ProcessId aForPid,
+ const gfx::IntSize aSize, const TextureFlags aFlags);
+ virtual ~RemoteTextureHostWrapper();
+
+ // Called only by RemoteTextureMap
+ TextureHost* GetRemoteTextureHostForDisplayList(
+ const MonitorAutoLock& aProofOfLock);
+ // Called only by RemoteTextureMap
+ void SetRemoteTextureHostForDisplayList(const MonitorAutoLock& aProofOfLock,
+ TextureHost* aTextureHost,
+ bool aIsSyncMode);
+ void ClearRemoteTextureHostForDisplayList(
+ const MonitorAutoLock& aProofOfLock);
+
+ // Updated by RemoteTextureMap
+ //
+ // Hold compositable ref of remote texture's TextureHost that is used for
+ // building WebRender display list. In sync mode, it is TextureHost of
+ // mTextureId. In async mode, it could be previous TextureHost that is
+ // compatible to the mTextureId's TextureHost.
+ CompositableTextureHostRef mRemoteTextureForDisplayList;
+
+ bool mIsSyncMode = true;
+
+ friend class RemoteTextureMap;
+};
+
+} // namespace mozilla::layers
+
+#endif // MOZILLA_GFX_RemoteTextureHostWrapper_H
diff --git a/gfx/layers/composite/TextureHost.cpp b/gfx/layers/composite/TextureHost.cpp
new file mode 100644
index 0000000000..bb11b3463b
--- /dev/null
+++ b/gfx/layers/composite/TextureHost.cpp
@@ -0,0 +1,848 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "TextureHost.h"
+
+#include "CompositableHost.h" // for CompositableHost
+#include "mozilla/gfx/2D.h" // for DataSourceSurface, Factory
+#include "mozilla/gfx/gfxVars.h"
+#include "mozilla/ipc/Shmem.h" // for Shmem
+#include "mozilla/layers/AsyncImagePipelineManager.h"
+#include "mozilla/layers/BufferTexture.h"
+#include "mozilla/layers/CompositableTransactionParent.h" // for CompositableParentManager
+#include "mozilla/layers/CompositorBridgeParent.h"
+#include "mozilla/layers/Compositor.h" // for Compositor
+#include "mozilla/layers/ISurfaceAllocator.h" // for ISurfaceAllocator
+#include "mozilla/layers/ImageBridgeParent.h" // for ImageBridgeParent
+#include "mozilla/layers/LayersSurfaces.h" // for SurfaceDescriptor, etc
+#include "mozilla/layers/RemoteTextureMap.h"
+#include "mozilla/layers/TextureHostOGL.h" // for TextureHostOGL
+#include "mozilla/layers/ImageDataSerializer.h"
+#include "mozilla/layers/TextureClient.h"
+#include "mozilla/layers/GPUVideoTextureHost.h"
+#include "mozilla/layers/WebRenderTextureHost.h"
+#include "mozilla/StaticPrefs_layers.h"
+#include "mozilla/StaticPrefs_gfx.h"
+#include "mozilla/webrender/RenderBufferTextureHost.h"
+#include "mozilla/webrender/RenderExternalTextureHost.h"
+#include "mozilla/webrender/RenderThread.h"
+#include "mozilla/webrender/WebRenderAPI.h"
+#include "nsAString.h"
+#include "mozilla/RefPtr.h" // for nsRefPtr
+#include "nsPrintfCString.h" // for nsPrintfCString
+#include "mozilla/layers/PTextureParent.h"
+#include "mozilla/Unused.h"
+#include <limits>
+#include "../opengl/CompositorOGL.h"
+
+#include "gfxUtils.h"
+#include "IPDLActor.h"
+
+#ifdef XP_MACOSX
+# include "../opengl/MacIOSurfaceTextureHostOGL.h"
+#endif
+
+#ifdef XP_WIN
+# include "../d3d11/CompositorD3D11.h"
+# include "mozilla/layers/TextureD3D11.h"
+# ifdef MOZ_WMF_MEDIA_ENGINE
+# include "mozilla/layers/DcompSurfaceImage.h"
+# endif
+#endif
+
+#if 0
+# define RECYCLE_LOG(...) printf_stderr(__VA_ARGS__)
+#else
+# define RECYCLE_LOG(...) \
+ do { \
+ } while (0)
+#endif
+
+namespace mozilla {
+namespace layers {
+
+/**
+ * TextureParent is the host-side IPDL glue between TextureClient and
+ * TextureHost. It is an IPDL actor just like LayerParent, CompositableParent,
+ * etc.
+ */
+class TextureParent : public ParentActor<PTextureParent> {
+ public:
+ TextureParent(HostIPCAllocator* aAllocator,
+ const dom::ContentParentId& aContentId, uint64_t aSerial,
+ const wr::MaybeExternalImageId& aExternalImageId);
+
+ virtual ~TextureParent();
+
+ bool Init(const SurfaceDescriptor& aSharedData,
+ ReadLockDescriptor&& aReadLock, const LayersBackend& aLayersBackend,
+ const TextureFlags& aFlags);
+
+ void NotifyNotUsed(uint64_t aTransactionId);
+
+ mozilla::ipc::IPCResult RecvRecycleTexture(
+ const TextureFlags& aTextureFlags) final;
+
+ TextureHost* GetTextureHost() { return mTextureHost; }
+
+ void Destroy() override;
+
+ const dom::ContentParentId& GetContentId() const { return mContentId; }
+
+ uint64_t GetSerial() const { return mSerial; }
+
+ HostIPCAllocator* mSurfaceAllocator;
+ RefPtr<TextureHost> mTextureHost;
+ dom::ContentParentId mContentId;
+ // mSerial is unique in TextureClient's process.
+ const uint64_t mSerial;
+ wr::MaybeExternalImageId mExternalImageId;
+};
+
+static bool WrapWithWebRenderTextureHost(ISurfaceAllocator* aDeallocator,
+ LayersBackend aBackend,
+ TextureFlags aFlags) {
+ if (!aDeallocator) {
+ return false;
+ }
+ if ((aFlags & TextureFlags::SNAPSHOT) ||
+ (!aDeallocator->UsesImageBridge() &&
+ !aDeallocator->AsCompositorBridgeParentBase())) {
+ return false;
+ }
+ return true;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+PTextureParent* TextureHost::CreateIPDLActor(
+ HostIPCAllocator* aAllocator, const SurfaceDescriptor& aSharedData,
+ ReadLockDescriptor&& aReadLock, LayersBackend aLayersBackend,
+ TextureFlags aFlags, const dom::ContentParentId& aContentId,
+ uint64_t aSerial, const wr::MaybeExternalImageId& aExternalImageId) {
+ TextureParent* actor =
+ new TextureParent(aAllocator, aContentId, aSerial, aExternalImageId);
+ if (!actor->Init(aSharedData, std::move(aReadLock), aLayersBackend, aFlags)) {
+ actor->ActorDestroy(ipc::IProtocol::ActorDestroyReason::FailedConstructor);
+ delete actor;
+ return nullptr;
+ }
+ return actor;
+}
+
+// static
+bool TextureHost::DestroyIPDLActor(PTextureParent* actor) {
+ delete actor;
+ return true;
+}
+
+// static
+bool TextureHost::SendDeleteIPDLActor(PTextureParent* actor) {
+ return PTextureParent::Send__delete__(actor);
+}
+
+// static
+TextureHost* TextureHost::AsTextureHost(PTextureParent* actor) {
+ if (!actor) {
+ return nullptr;
+ }
+ return static_cast<TextureParent*>(actor)->mTextureHost;
+}
+
+// static
+uint64_t TextureHost::GetTextureSerial(PTextureParent* actor) {
+ if (!actor) {
+ return UINT64_MAX;
+ }
+ return static_cast<TextureParent*>(actor)->mSerial;
+}
+
+// static
+dom::ContentParentId TextureHost::GetTextureContentId(PTextureParent* actor) {
+ if (!actor) {
+ return dom::ContentParentId();
+ }
+ return static_cast<TextureParent*>(actor)->mContentId;
+}
+
+PTextureParent* TextureHost::GetIPDLActor() { return mActor; }
+
+void TextureHost::SetLastFwdTransactionId(uint64_t aTransactionId) {
+ MOZ_ASSERT(mFwdTransactionId <= aTransactionId);
+ mFwdTransactionId = aTransactionId;
+}
+
+already_AddRefed<TextureHost> CreateDummyBufferTextureHost(
+ mozilla::layers::LayersBackend aBackend,
+ mozilla::layers::TextureFlags aFlags) {
+ // Ensure that the host will delete the memory.
+ aFlags &= ~TextureFlags::DEALLOCATE_CLIENT;
+ aFlags |= TextureFlags::DUMMY_TEXTURE;
+ UniquePtr<TextureData> textureData(BufferTextureData::Create(
+ gfx::IntSize(1, 1), gfx::SurfaceFormat::B8G8R8A8, gfx::BackendType::SKIA,
+ aBackend, aFlags, TextureAllocationFlags::ALLOC_DEFAULT, nullptr));
+ SurfaceDescriptor surfDesc;
+ textureData->Serialize(surfDesc);
+ const SurfaceDescriptorBuffer& bufferDesc =
+ surfDesc.get_SurfaceDescriptorBuffer();
+ const MemoryOrShmem& data = bufferDesc.data();
+ RefPtr<TextureHost> host =
+ new MemoryTextureHost(reinterpret_cast<uint8_t*>(data.get_uintptr_t()),
+ bufferDesc.desc(), aFlags);
+ return host.forget();
+}
+
+already_AddRefed<TextureHost> TextureHost::Create(
+ const SurfaceDescriptor& aDesc, ReadLockDescriptor&& aReadLock,
+ ISurfaceAllocator* aDeallocator, LayersBackend aBackend,
+ TextureFlags aFlags, wr::MaybeExternalImageId& aExternalImageId) {
+ RefPtr<TextureHost> result;
+
+ switch (aDesc.type()) {
+ case SurfaceDescriptor::TSurfaceDescriptorBuffer:
+ case SurfaceDescriptor::TSurfaceDescriptorGPUVideo:
+ result = CreateBackendIndependentTextureHost(aDesc, aDeallocator,
+ aBackend, aFlags);
+ break;
+
+ case SurfaceDescriptor::TEGLImageDescriptor:
+ case SurfaceDescriptor::TSurfaceTextureDescriptor:
+ case SurfaceDescriptor::TSurfaceDescriptorAndroidHardwareBuffer:
+ case SurfaceDescriptor::TSurfaceDescriptorSharedGLTexture:
+ case SurfaceDescriptor::TSurfaceDescriptorDMABuf:
+ result = CreateTextureHostOGL(aDesc, aDeallocator, aBackend, aFlags);
+ break;
+
+ case SurfaceDescriptor::TSurfaceDescriptorMacIOSurface:
+ result = CreateTextureHostOGL(aDesc, aDeallocator, aBackend, aFlags);
+ break;
+
+#ifdef XP_WIN
+ case SurfaceDescriptor::TSurfaceDescriptorD3D10:
+ case SurfaceDescriptor::TSurfaceDescriptorDXGIYCbCr:
+ result = CreateTextureHostD3D11(aDesc, aDeallocator, aBackend, aFlags);
+ break;
+# ifdef MOZ_WMF_MEDIA_ENGINE
+ case SurfaceDescriptor::TSurfaceDescriptorDcompSurface:
+ result =
+ CreateTextureHostDcompSurface(aDesc, aDeallocator, aBackend, aFlags);
+ break;
+# endif
+#endif
+ case SurfaceDescriptor::TSurfaceDescriptorRecorded: {
+ const SurfaceDescriptorRecorded& desc =
+ aDesc.get_SurfaceDescriptorRecorded();
+ CompositorBridgeParentBase* actor =
+ aDeallocator ? aDeallocator->AsCompositorBridgeParentBase() : nullptr;
+ UniquePtr<SurfaceDescriptor> realDesc =
+ actor
+ ? actor->LookupSurfaceDescriptorForClientTexture(desc.textureId())
+ : nullptr;
+ if (!realDesc) {
+ gfxCriticalNote << "Failed to get descriptor for recorded texture.";
+ // Create a dummy to prevent any crashes due to missing IPDL actors.
+ result = CreateDummyBufferTextureHost(aBackend, aFlags);
+ break;
+ }
+
+ result =
+ TextureHost::Create(*realDesc, std::move(aReadLock), aDeallocator,
+ aBackend, aFlags, aExternalImageId);
+ return result.forget();
+ }
+ default:
+ MOZ_CRASH("GFX: Unsupported Surface type host");
+ }
+
+ if (!result) {
+ gfxCriticalNote << "TextureHost creation failure type=" << aDesc.type();
+ }
+
+ if (result && WrapWithWebRenderTextureHost(aDeallocator, aBackend, aFlags)) {
+ MOZ_ASSERT(aExternalImageId.isSome());
+ result = new WebRenderTextureHost(aFlags, result, aExternalImageId.ref());
+ }
+
+ if (result) {
+ result->DeserializeReadLock(std::move(aReadLock), aDeallocator);
+ }
+
+ return result.forget();
+}
+
+already_AddRefed<TextureHost> CreateBackendIndependentTextureHost(
+ const SurfaceDescriptor& aDesc, ISurfaceAllocator* aDeallocator,
+ LayersBackend aBackend, TextureFlags aFlags) {
+ RefPtr<TextureHost> result;
+ switch (aDesc.type()) {
+ case SurfaceDescriptor::TSurfaceDescriptorBuffer: {
+ const SurfaceDescriptorBuffer& bufferDesc =
+ aDesc.get_SurfaceDescriptorBuffer();
+ const MemoryOrShmem& data = bufferDesc.data();
+ switch (data.type()) {
+ case MemoryOrShmem::TShmem: {
+ const ipc::Shmem& shmem = data.get_Shmem();
+ const BufferDescriptor& desc = bufferDesc.desc();
+ if (!shmem.IsReadable()) {
+ // We failed to map the shmem so we can't verify its size. This
+ // should not be a fatal error, so just create the texture with
+ // nothing backing it.
+ result = new ShmemTextureHost(shmem, desc, aDeallocator, aFlags);
+ break;
+ }
+
+ size_t bufSize = shmem.Size<char>();
+ size_t reqSize = SIZE_MAX;
+ switch (desc.type()) {
+ case BufferDescriptor::TYCbCrDescriptor: {
+ const YCbCrDescriptor& ycbcr = desc.get_YCbCrDescriptor();
+ reqSize = ImageDataSerializer::ComputeYCbCrBufferSize(
+ ycbcr.ySize(), ycbcr.yStride(), ycbcr.cbCrSize(),
+ ycbcr.cbCrStride(), ycbcr.yOffset(), ycbcr.cbOffset(),
+ ycbcr.crOffset());
+ break;
+ }
+ case BufferDescriptor::TRGBDescriptor: {
+ const RGBDescriptor& rgb = desc.get_RGBDescriptor();
+ reqSize = ImageDataSerializer::ComputeRGBBufferSize(rgb.size(),
+ rgb.format());
+ break;
+ }
+ default:
+ gfxCriticalError()
+ << "Bad buffer host descriptor " << (int)desc.type();
+ MOZ_CRASH("GFX: Bad descriptor");
+ }
+
+ if (reqSize == 0 || bufSize < reqSize) {
+ NS_ERROR(
+ "A client process gave a shmem too small to fit for its "
+ "descriptor!");
+ return nullptr;
+ }
+
+ result = new ShmemTextureHost(shmem, desc, aDeallocator, aFlags);
+ break;
+ }
+ case MemoryOrShmem::Tuintptr_t: {
+ if (aDeallocator && !aDeallocator->IsSameProcess()) {
+ NS_ERROR(
+ "A client process is trying to peek at our address space using "
+ "a MemoryTexture!");
+ return nullptr;
+ }
+
+ result = new MemoryTextureHost(
+ reinterpret_cast<uint8_t*>(data.get_uintptr_t()),
+ bufferDesc.desc(), aFlags);
+ break;
+ }
+ default:
+ gfxCriticalError()
+ << "Failed texture host for backend " << (int)data.type();
+ MOZ_CRASH("GFX: No texture host for backend");
+ }
+ break;
+ }
+ case SurfaceDescriptor::TSurfaceDescriptorGPUVideo: {
+ MOZ_ASSERT(aDesc.get_SurfaceDescriptorGPUVideo().type() ==
+ SurfaceDescriptorGPUVideo::TSurfaceDescriptorRemoteDecoder);
+ result = GPUVideoTextureHost::CreateFromDescriptor(
+ aDeallocator->GetContentId(), aFlags,
+ aDesc.get_SurfaceDescriptorGPUVideo());
+ break;
+ }
+ default: {
+ NS_WARNING("No backend independent TextureHost for this descriptor type");
+ }
+ }
+ return result.forget();
+}
+
+TextureHost::TextureHost(TextureHostType aType, TextureFlags aFlags)
+ : AtomicRefCountedWithFinalize("TextureHost"),
+ mTextureHostType(aType),
+ mActor(nullptr),
+ mFlags(aFlags),
+ mCompositableCount(0),
+ mFwdTransactionId(0),
+ mReadLocked(false) {}
+
+TextureHost::~TextureHost() {
+ if (mReadLocked) {
+ // If we still have a ReadLock, unlock it. At this point we don't care about
+ // the texture client being written into on the other side since it should
+ // be destroyed by now. But we will hit assertions if we don't ReadUnlock
+ // before destroying the lock itself.
+ ReadUnlock();
+ }
+ if (mDestroyedCallback) {
+ mDestroyedCallback();
+ }
+}
+
+void TextureHost::Finalize() {
+ MaybeDestroyRenderTexture();
+
+ if (!(GetFlags() & TextureFlags::DEALLOCATE_CLIENT)) {
+ DeallocateSharedData();
+ DeallocateDeviceData();
+ }
+}
+
+void TextureHost::UnbindTextureSource() {
+ if (mReadLocked) {
+ ReadUnlock();
+ }
+}
+
+void TextureHost::RecycleTexture(TextureFlags aFlags) {
+ MOZ_ASSERT(GetFlags() & TextureFlags::RECYCLE);
+ MOZ_ASSERT(aFlags & TextureFlags::RECYCLE);
+ mFlags = aFlags;
+}
+
+void TextureHost::PrepareForUse() {}
+
+void TextureHost::NotifyNotUsed() {
+ if (!mActor) {
+ if ((mFlags & TextureFlags::REMOTE_TEXTURE) && AsSurfaceTextureHost()) {
+ MOZ_ASSERT(mExternalImageId.isSome());
+ wr::RenderThread::Get()->NotifyNotUsed(*mExternalImageId);
+ }
+ return;
+ }
+
+ // Do not need to call NotifyNotUsed() if TextureHost does not have
+ // TextureFlags::RECYCLE flag nor TextureFlags::WAIT_HOST_USAGE_END flag.
+ if (!(GetFlags() & TextureFlags::RECYCLE) &&
+ !(GetFlags() & TextureFlags::WAIT_HOST_USAGE_END)) {
+ return;
+ }
+
+ static_cast<TextureParent*>(mActor)->NotifyNotUsed(mFwdTransactionId);
+}
+
+void TextureHost::CallNotifyNotUsed() {
+ if (!mActor) {
+ return;
+ }
+ static_cast<TextureParent*>(mActor)->NotifyNotUsed(mFwdTransactionId);
+}
+
+void TextureHost::MaybeDestroyRenderTexture() {
+ if (mExternalImageId.isNothing()) {
+ // RenderTextureHost was not created
+ return;
+ }
+ // When TextureHost created RenderTextureHost, delete it here.
+ TextureHost::DestroyRenderTexture(mExternalImageId.ref());
+ mExternalImageId = Nothing();
+}
+
+void TextureHost::DestroyRenderTexture(
+ const wr::ExternalImageId& aExternalImageId) {
+ wr::RenderThread::Get()->UnregisterExternalImage(aExternalImageId);
+}
+
+void TextureHost::EnsureRenderTexture(
+ const wr::MaybeExternalImageId& aExternalImageId) {
+ if (aExternalImageId.isNothing()) {
+ // TextureHost is wrapped by GPUVideoTextureHost.
+ if (mExternalImageId.isSome()) {
+ // RenderTextureHost was already created.
+ return;
+ }
+ mExternalImageId =
+ Some(AsyncImagePipelineManager::GetNextExternalImageId());
+ } else {
+ // TextureHost is wrapped by WebRenderTextureHost.
+ if (aExternalImageId == mExternalImageId) {
+ // The texture has already been created.
+ return;
+ }
+ MOZ_ASSERT(mExternalImageId.isNothing());
+ mExternalImageId = aExternalImageId;
+ }
+ CreateRenderTexture(mExternalImageId.ref());
+}
+
+TextureSource::TextureSource() : mCompositableCount(0) {}
+
+TextureSource::~TextureSource() = default;
+BufferTextureHost::BufferTextureHost(const BufferDescriptor& aDesc,
+ TextureFlags aFlags)
+ : TextureHost(TextureHostType::Buffer, aFlags), mLocked(false) {
+ mDescriptor = aDesc;
+ switch (mDescriptor.type()) {
+ case BufferDescriptor::TYCbCrDescriptor: {
+ const YCbCrDescriptor& ycbcr = mDescriptor.get_YCbCrDescriptor();
+ mSize = ycbcr.display().Size();
+ mFormat = gfx::SurfaceFormat::YUV;
+ break;
+ }
+ case BufferDescriptor::TRGBDescriptor: {
+ const RGBDescriptor& rgb = mDescriptor.get_RGBDescriptor();
+ mSize = rgb.size();
+ mFormat = rgb.format();
+ break;
+ }
+ default:
+ gfxCriticalError() << "Bad buffer host descriptor "
+ << (int)mDescriptor.type();
+ MOZ_CRASH("GFX: Bad descriptor");
+ }
+
+#ifdef XP_MACOSX
+ const int kMinSize = 1024;
+ const int kMaxSize = 4096;
+ mUseExternalTextures =
+ kMaxSize >= mSize.width && mSize.width >= kMinSize &&
+ kMaxSize >= mSize.height && mSize.height >= kMinSize &&
+ StaticPrefs::gfx_webrender_enable_client_storage_AtStartup();
+#else
+ mUseExternalTextures = false;
+#endif
+}
+
+BufferTextureHost::~BufferTextureHost() = default;
+
+void BufferTextureHost::DeallocateDeviceData() {}
+
+void BufferTextureHost::CreateRenderTexture(
+ const wr::ExternalImageId& aExternalImageId) {
+ RefPtr<wr::RenderTextureHost> texture;
+
+ if (UseExternalTextures()) {
+ texture =
+ new wr::RenderExternalTextureHost(GetBuffer(), GetBufferDescriptor());
+ } else {
+ texture =
+ new wr::RenderBufferTextureHost(GetBuffer(), GetBufferDescriptor());
+ }
+
+ wr::RenderThread::Get()->RegisterExternalImage(aExternalImageId,
+ texture.forget());
+}
+
+uint32_t BufferTextureHost::NumSubTextures() {
+ if (GetFormat() == gfx::SurfaceFormat::YUV) {
+ return 3;
+ }
+
+ return 1;
+}
+
+void BufferTextureHost::PushResourceUpdates(
+ wr::TransactionBuilder& aResources, ResourceUpdateOp aOp,
+ const Range<wr::ImageKey>& aImageKeys, const wr::ExternalImageId& aExtID) {
+ auto method = aOp == TextureHost::ADD_IMAGE
+ ? &wr::TransactionBuilder::AddExternalImage
+ : &wr::TransactionBuilder::UpdateExternalImage;
+
+ // Use native textures if our backend requires it, or if our backend doesn't
+ // forbid it and we want to use them.
+ NativeTexturePolicy policy =
+ BackendNativeTexturePolicy(aResources.GetBackendType(), GetSize());
+ bool useNativeTexture =
+ (policy == REQUIRE) || (policy != FORBID && UseExternalTextures());
+ auto imageType = useNativeTexture ? wr::ExternalImageType::TextureHandle(
+ wr::ImageBufferKind::TextureRect)
+ : wr::ExternalImageType::Buffer();
+
+ if (GetFormat() != gfx::SurfaceFormat::YUV) {
+ MOZ_ASSERT(aImageKeys.length() == 1);
+
+ wr::ImageDescriptor descriptor(
+ GetSize(),
+ ImageDataSerializer::ComputeRGBStride(GetFormat(), GetSize().width),
+ GetFormat());
+ (aResources.*method)(aImageKeys[0], descriptor, aExtID, imageType, 0);
+ } else {
+ MOZ_ASSERT(aImageKeys.length() == 3);
+
+ const layers::YCbCrDescriptor& desc = mDescriptor.get_YCbCrDescriptor();
+ gfx::IntSize ySize = desc.display().Size();
+ gfx::IntSize cbcrSize = ImageDataSerializer::GetCroppedCbCrSize(desc);
+ wr::ImageDescriptor yDescriptor(
+ ySize, desc.yStride(), SurfaceFormatForColorDepth(desc.colorDepth()));
+ wr::ImageDescriptor cbcrDescriptor(
+ cbcrSize, desc.cbCrStride(),
+ SurfaceFormatForColorDepth(desc.colorDepth()));
+ (aResources.*method)(aImageKeys[0], yDescriptor, aExtID, imageType, 0);
+ (aResources.*method)(aImageKeys[1], cbcrDescriptor, aExtID, imageType, 1);
+ (aResources.*method)(aImageKeys[2], cbcrDescriptor, aExtID, imageType, 2);
+ }
+}
+
+void BufferTextureHost::PushDisplayItems(wr::DisplayListBuilder& aBuilder,
+ const wr::LayoutRect& aBounds,
+ const wr::LayoutRect& aClip,
+ wr::ImageRendering aFilter,
+ const Range<wr::ImageKey>& aImageKeys,
+ PushDisplayItemFlagSet aFlags) {
+ // SWGL should always try to bypass shaders and composite directly.
+ bool preferCompositorSurface =
+ aFlags.contains(PushDisplayItemFlag::PREFER_COMPOSITOR_SURFACE);
+ bool useExternalSurface =
+ aFlags.contains(PushDisplayItemFlag::SUPPORTS_EXTERNAL_BUFFER_TEXTURES);
+ if (GetFormat() != gfx::SurfaceFormat::YUV) {
+ MOZ_ASSERT(aImageKeys.length() == 1);
+ aBuilder.PushImage(aBounds, aClip, true, false, aFilter, aImageKeys[0],
+ !(mFlags & TextureFlags::NON_PREMULTIPLIED),
+ wr::ColorF{1.0f, 1.0f, 1.0f, 1.0f},
+ preferCompositorSurface, useExternalSurface);
+ } else {
+ MOZ_ASSERT(aImageKeys.length() == 3);
+ const YCbCrDescriptor& desc = mDescriptor.get_YCbCrDescriptor();
+ aBuilder.PushYCbCrPlanarImage(
+ aBounds, aClip, true, aImageKeys[0], aImageKeys[1], aImageKeys[2],
+ wr::ToWrColorDepth(desc.colorDepth()),
+ wr::ToWrYuvColorSpace(desc.yUVColorSpace()),
+ wr::ToWrColorRange(desc.colorRange()), aFilter, preferCompositorSurface,
+ useExternalSurface);
+ }
+}
+
+void TextureHost::DeserializeReadLock(ReadLockDescriptor&& aDesc,
+ ISurfaceAllocator* aAllocator) {
+ if (mReadLock) {
+ return;
+ }
+
+ mReadLock = TextureReadLock::Deserialize(std::move(aDesc), aAllocator);
+}
+
+void TextureHost::SetReadLocked() {
+ if (!mReadLock) {
+ return;
+ }
+ // If mReadLocked is true it means we haven't read unlocked yet and the
+ // content side should not have been able to write into this texture and read
+ // lock again!
+ MOZ_ASSERT(!mReadLocked);
+ mReadLocked = true;
+}
+
+void TextureHost::ReadUnlock() {
+ if (mReadLock && mReadLocked) {
+ mReadLock->ReadUnlock();
+ mReadLocked = false;
+ }
+}
+
+bool TextureHost::NeedsYFlip() const {
+ return bool(mFlags & TextureFlags::ORIGIN_BOTTOM_LEFT);
+}
+
+void BufferTextureHost::UnbindTextureSource() {
+ // This texture is not used by any layer anymore.
+ // If the texture has an intermediate buffer we don't care either because
+ // texture uploads are also performed synchronously for BufferTextureHost.
+ ReadUnlock();
+}
+
+gfx::SurfaceFormat BufferTextureHost::GetFormat() const { return mFormat; }
+
+gfx::YUVColorSpace BufferTextureHost::GetYUVColorSpace() const {
+ if (mFormat == gfx::SurfaceFormat::YUV) {
+ const YCbCrDescriptor& desc = mDescriptor.get_YCbCrDescriptor();
+ return desc.yUVColorSpace();
+ }
+ return gfx::YUVColorSpace::Identity;
+}
+
+gfx::ColorDepth BufferTextureHost::GetColorDepth() const {
+ if (mFormat == gfx::SurfaceFormat::YUV) {
+ const YCbCrDescriptor& desc = mDescriptor.get_YCbCrDescriptor();
+ return desc.colorDepth();
+ }
+ return gfx::ColorDepth::COLOR_8;
+}
+
+gfx::ColorRange BufferTextureHost::GetColorRange() const {
+ if (mFormat == gfx::SurfaceFormat::YUV) {
+ const YCbCrDescriptor& desc = mDescriptor.get_YCbCrDescriptor();
+ return desc.colorRange();
+ }
+ return TextureHost::GetColorRange();
+}
+
+already_AddRefed<gfx::DataSourceSurface> BufferTextureHost::GetAsSurface() {
+ RefPtr<gfx::DataSourceSurface> result;
+ if (mFormat == gfx::SurfaceFormat::UNKNOWN) {
+ NS_WARNING("BufferTextureHost: unsupported format!");
+ return nullptr;
+ } else if (mFormat == gfx::SurfaceFormat::YUV) {
+ result = ImageDataSerializer::DataSourceSurfaceFromYCbCrDescriptor(
+ GetBuffer(), mDescriptor.get_YCbCrDescriptor());
+ if (NS_WARN_IF(!result)) {
+ return nullptr;
+ }
+ } else {
+ result = gfx::Factory::CreateWrappingDataSourceSurface(
+ GetBuffer(),
+ ImageDataSerializer::GetRGBStride(mDescriptor.get_RGBDescriptor()),
+ mSize, mFormat);
+ }
+ return result.forget();
+}
+
+ShmemTextureHost::ShmemTextureHost(const ipc::Shmem& aShmem,
+ const BufferDescriptor& aDesc,
+ ISurfaceAllocator* aDeallocator,
+ TextureFlags aFlags)
+ : BufferTextureHost(aDesc, aFlags), mDeallocator(aDeallocator) {
+ if (aShmem.IsReadable()) {
+ mShmem = MakeUnique<ipc::Shmem>(aShmem);
+ } else {
+ // This can happen if we failed to map the shmem on this process, perhaps
+ // because it was big and we didn't have enough contiguous address space
+ // available, even though we did on the child process.
+ // As a result this texture will be in an invalid state and Lock will
+ // always fail.
+
+ gfxCriticalNote << "Failed to create a valid ShmemTextureHost";
+ }
+
+ MOZ_COUNT_CTOR(ShmemTextureHost);
+}
+
+ShmemTextureHost::~ShmemTextureHost() {
+ MOZ_ASSERT(!mShmem || (mFlags & TextureFlags::DEALLOCATE_CLIENT),
+ "Leaking our buffer");
+ DeallocateDeviceData();
+ MOZ_COUNT_DTOR(ShmemTextureHost);
+}
+
+void ShmemTextureHost::DeallocateSharedData() {
+ if (mShmem) {
+ MOZ_ASSERT(mDeallocator,
+ "Shared memory would leak without a ISurfaceAllocator");
+ mDeallocator->AsShmemAllocator()->DeallocShmem(*mShmem);
+ mShmem = nullptr;
+ }
+}
+
+void ShmemTextureHost::ForgetSharedData() {
+ if (mShmem) {
+ mShmem = nullptr;
+ }
+}
+
+void ShmemTextureHost::OnShutdown() { mShmem = nullptr; }
+
+uint8_t* ShmemTextureHost::GetBuffer() {
+ return mShmem ? mShmem->get<uint8_t>() : nullptr;
+}
+
+size_t ShmemTextureHost::GetBufferSize() {
+ return mShmem ? mShmem->Size<uint8_t>() : 0;
+}
+
+MemoryTextureHost::MemoryTextureHost(uint8_t* aBuffer,
+ const BufferDescriptor& aDesc,
+ TextureFlags aFlags)
+ : BufferTextureHost(aDesc, aFlags), mBuffer(aBuffer) {
+ MOZ_COUNT_CTOR(MemoryTextureHost);
+}
+
+MemoryTextureHost::~MemoryTextureHost() {
+ MOZ_ASSERT(!mBuffer || (mFlags & TextureFlags::DEALLOCATE_CLIENT),
+ "Leaking our buffer");
+ DeallocateDeviceData();
+ MOZ_COUNT_DTOR(MemoryTextureHost);
+}
+
+void MemoryTextureHost::DeallocateSharedData() {
+ if (mBuffer) {
+ GfxMemoryImageReporter::WillFree(mBuffer);
+ }
+ delete[] mBuffer;
+ mBuffer = nullptr;
+}
+
+void MemoryTextureHost::ForgetSharedData() { mBuffer = nullptr; }
+
+uint8_t* MemoryTextureHost::GetBuffer() { return mBuffer; }
+
+size_t MemoryTextureHost::GetBufferSize() {
+ // MemoryTextureHost just trusts that the buffer size is large enough to read
+ // anything we need to. That's because MemoryTextureHost has to trust the
+ // buffer pointer anyway, so the security model here is just that
+ // MemoryTexture's are restricted to same-process clients.
+ return std::numeric_limits<size_t>::max();
+}
+
+TextureParent::TextureParent(HostIPCAllocator* aSurfaceAllocator,
+ const dom::ContentParentId& aContentId,
+ uint64_t aSerial,
+ const wr::MaybeExternalImageId& aExternalImageId)
+ : mSurfaceAllocator(aSurfaceAllocator),
+ mContentId(aContentId),
+ mSerial(aSerial),
+ mExternalImageId(aExternalImageId) {
+ MOZ_COUNT_CTOR(TextureParent);
+}
+
+TextureParent::~TextureParent() { MOZ_COUNT_DTOR(TextureParent); }
+
+void TextureParent::NotifyNotUsed(uint64_t aTransactionId) {
+ if (!mTextureHost) {
+ return;
+ }
+ mSurfaceAllocator->NotifyNotUsed(this, aTransactionId);
+}
+
+bool TextureParent::Init(const SurfaceDescriptor& aSharedData,
+ ReadLockDescriptor&& aReadLock,
+ const LayersBackend& aBackend,
+ const TextureFlags& aFlags) {
+ mTextureHost =
+ TextureHost::Create(aSharedData, std::move(aReadLock), mSurfaceAllocator,
+ aBackend, aFlags, mExternalImageId);
+ if (mTextureHost) {
+ mTextureHost->mActor = this;
+ }
+
+ return !!mTextureHost;
+}
+
+void TextureParent::Destroy() {
+ if (!mTextureHost) {
+ return;
+ }
+
+ if (mTextureHost->mReadLocked) {
+ // ReadUnlock here to make sure the ReadLock's shmem does not outlive the
+ // protocol that created it.
+ mTextureHost->ReadUnlock();
+ }
+
+ if (mTextureHost->GetFlags() & TextureFlags::DEALLOCATE_CLIENT) {
+ mTextureHost->ForgetSharedData();
+ }
+
+ mTextureHost->mActor = nullptr;
+ mTextureHost = nullptr;
+}
+
+void TextureHost::ReceivedDestroy(PTextureParent* aActor) {
+ static_cast<TextureParent*>(aActor)->RecvDestroy();
+}
+
+mozilla::ipc::IPCResult TextureParent::RecvRecycleTexture(
+ const TextureFlags& aTextureFlags) {
+ if (!mTextureHost) {
+ return IPC_OK();
+ }
+ mTextureHost->RecycleTexture(aTextureFlags);
+ return IPC_OK();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/composite/TextureHost.h b/gfx/layers/composite/TextureHost.h
new file mode 100644
index 0000000000..8aedc6be4b
--- /dev/null
+++ b/gfx/layers/composite/TextureHost.h
@@ -0,0 +1,1011 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_GFX_TEXTUREHOST_H
+#define MOZILLA_GFX_TEXTUREHOST_H
+
+#include <stddef.h> // for size_t
+#include <stdint.h> // for uint64_t, uint32_t, uint8_t
+#include "mozilla/Assertions.h" // for MOZ_ASSERT, etc
+#include "mozilla/Attributes.h" // for override
+#include "mozilla/RefPtr.h" // for RefPtr, already_AddRefed, etc
+#include "mozilla/dom/ipc/IdType.h"
+#include "mozilla/gfx/Logging.h"
+#include "mozilla/gfx/Matrix.h"
+#include "mozilla/gfx/Point.h" // for IntSize, IntPoint
+#include "mozilla/gfx/Rect.h"
+#include "mozilla/gfx/Types.h" // for SurfaceFormat, etc
+#include "mozilla/ipc/FileDescriptor.h"
+#include "mozilla/layers/CompositorTypes.h" // for TextureFlags, etc
+#include "mozilla/layers/LayersTypes.h" // for LayerRenderState, etc
+#include "mozilla/layers/LayersSurfaces.h"
+#include "mozilla/layers/TextureSourceProvider.h"
+#include "mozilla/mozalloc.h" // for operator delete
+#include "mozilla/Range.h"
+#include "mozilla/UniquePtr.h" // for UniquePtr
+#include "mozilla/webrender/WebRenderTypes.h"
+#include "nsCOMPtr.h" // for already_AddRefed
+#include "nsDebug.h" // for NS_WARNING
+#include "nsISupportsImpl.h" // for MOZ_COUNT_CTOR, etc
+#include "nsRect.h"
+#include "nsRegion.h" // for nsIntRegion
+#include "nsTraceRefcnt.h" // for MOZ_COUNT_CTOR, etc
+#include "nscore.h" // for nsACString
+#include "mozilla/layers/AtomicRefCountedWithFinalize.h"
+
+class MacIOSurface;
+namespace mozilla {
+namespace gfx {
+class DataSourceSurface;
+}
+
+namespace ipc {
+class Shmem;
+} // namespace ipc
+
+namespace wr {
+class DisplayListBuilder;
+class TransactionBuilder;
+} // namespace wr
+
+namespace layers {
+
+class AndroidHardwareBuffer;
+class AndroidHardwareBufferTextureHost;
+class BufferDescriptor;
+class BufferTextureHost;
+class Compositor;
+class CompositableParentManager;
+class ReadLockDescriptor;
+class CompositorBridgeParent;
+class SurfaceDescriptor;
+class HostIPCAllocator;
+class ISurfaceAllocator;
+class MacIOSurfaceTextureHostOGL;
+class ShmemTextureHost;
+class SurfaceTextureHost;
+class TextureHostOGL;
+class TextureReadLock;
+class TextureSourceOGL;
+class TextureSourceD3D11;
+class DataTextureSource;
+class PTextureParent;
+class RemoteTextureHostWrapper;
+class TextureParent;
+class WebRenderTextureHost;
+class WrappingTextureSourceYCbCrBasic;
+
+/**
+ * A view on a TextureHost where the texture is internally represented as tiles
+ * (contrast with a tiled buffer, where each texture is a tile). For iteration
+ * by the texture's buffer host. This is only useful when the underlying surface
+ * is too big to fit in one device texture, which forces us to split it in
+ * smaller parts. Tiled Compositable is a different thing.
+ */
+class BigImageIterator {
+ public:
+ virtual void BeginBigImageIteration() = 0;
+ virtual void EndBigImageIteration(){};
+ virtual gfx::IntRect GetTileRect() = 0;
+ virtual size_t GetTileCount() = 0;
+ virtual bool NextTile() = 0;
+};
+
+/**
+ * TextureSource is the interface for texture objects that can be composited
+ * by a given compositor backend. Since the drawing APIs are different
+ * between backends, the TextureSource interface is split into different
+ * interfaces (TextureSourceOGL, etc.), and TextureSource mostly provide
+ * access to these interfaces.
+ *
+ * This class is used on the compositor side.
+ */
+class TextureSource : public RefCounted<TextureSource> {
+ public:
+ MOZ_DECLARE_REFCOUNTED_VIRTUAL_TYPENAME(TextureSource)
+
+ TextureSource();
+
+ virtual ~TextureSource();
+
+ virtual const char* Name() const = 0;
+
+ /**
+ * Should be overridden in order to deallocate the data that is associated
+ * with the rendering backend, such as GL textures.
+ */
+ virtual void DeallocateDeviceData() {}
+
+ /**
+ * Return the size of the texture in texels.
+ * If this is a tile iterator, GetSize must return the size of the current
+ * tile.
+ */
+ virtual gfx::IntSize GetSize() const = 0;
+
+ /**
+ * Return the pixel format of this texture
+ */
+ virtual gfx::SurfaceFormat GetFormat() const {
+ return gfx::SurfaceFormat::UNKNOWN;
+ }
+
+ /**
+ * Cast to a TextureSource for for each backend..
+ */
+ virtual TextureSourceOGL* AsSourceOGL() {
+ gfxCriticalNote << "Failed to cast " << Name()
+ << " into a TextureSourceOGL";
+ return nullptr;
+ }
+ virtual TextureSourceD3D11* AsSourceD3D11() { return nullptr; }
+ /**
+ * Cast to a DataTextureSurce.
+ */
+ virtual DataTextureSource* AsDataTextureSource() { return nullptr; }
+
+ /**
+ * Overload this if the TextureSource supports big textures that don't fit in
+ * one device texture and must be tiled internally.
+ */
+ virtual BigImageIterator* AsBigImageIterator() { return nullptr; }
+
+ virtual void Unbind() {}
+
+ void SetNextSibling(TextureSource* aTexture) { mNextSibling = aTexture; }
+
+ TextureSource* GetNextSibling() const { return mNextSibling; }
+
+ /**
+ * In some rare cases we currently need to consider a group of textures as one
+ * TextureSource, that can be split in sub-TextureSources.
+ */
+ TextureSource* GetSubSource(int index) {
+ switch (index) {
+ case 0:
+ return this;
+ case 1:
+ return GetNextSibling();
+ case 2:
+ return GetNextSibling() ? GetNextSibling()->GetNextSibling() : nullptr;
+ }
+ return nullptr;
+ }
+
+ void AddCompositableRef() { ++mCompositableCount; }
+
+ void ReleaseCompositableRef() {
+ --mCompositableCount;
+ MOZ_ASSERT(mCompositableCount >= 0);
+ }
+
+ // When iterating as a BigImage, this creates temporary TextureSources
+ // wrapping individual tiles.
+ virtual RefPtr<TextureSource> ExtractCurrentTile() {
+ NS_WARNING("Implementation does not expose tile sources");
+ return nullptr;
+ }
+
+ int NumCompositableRefs() const { return mCompositableCount; }
+
+ // The direct-map cpu buffer should be alive when gpu uses it. And it
+ // should not be updated while gpu reads it. This Sync() function
+ // implements this synchronized behavior by allowing us to check if
+ // the GPU is done with the texture, and block on it if aBlocking is
+ // true.
+ virtual bool Sync(bool aBlocking) { return true; }
+
+ protected:
+ RefPtr<TextureSource> mNextSibling;
+ int mCompositableCount;
+};
+
+/// Equivalent of a RefPtr<TextureSource>, that calls AddCompositableRef and
+/// ReleaseCompositableRef in addition to the usual AddRef and Release.
+///
+/// The semantoics of these CompositableTextureRefs are important because they
+/// are used both as a synchronization/safety mechanism, and as an optimization
+/// mechanism. They are also tricky and subtle because we use them in a very
+/// implicit way (assigning to a CompositableTextureRef is less visible than
+/// explicitly calling a method or whatnot).
+/// It is Therefore important to be careful about the way we use this tool.
+///
+/// CompositableTextureRef is a mechanism that lets us count how many
+/// compositables are using a given texture (for TextureSource and TextureHost).
+/// We use it to run specific code when a texture is not used anymore, and also
+/// we trigger fast paths on some operations when we can see that the texture's
+/// CompositableTextureRef counter is equal to 1 (the texture is not shared
+/// between compositables).
+/// This means that it is important to observe the following rules:
+/// * CompositableHosts that receive UseTexture and similar messages *must*
+/// store all of the TextureHosts they receive in CompositableTextureRef slots
+/// for as long as they may be using them.
+/// * CompositableHosts must store each texture in a *single*
+/// CompositableTextureRef slot to ensure that the counter properly reflects how
+/// many compositables are using the texture. If a compositable needs to hold
+/// two references to a given texture (for example to have a pointer to the
+/// current texture in a list of textures that may be used), it can hold its
+/// extra references with RefPtr or whichever pointer type makes sense.
+template <typename T>
+class CompositableTextureRef {
+ public:
+ CompositableTextureRef() = default;
+
+ explicit CompositableTextureRef(const CompositableTextureRef& aOther) {
+ *this = aOther;
+ }
+
+ explicit CompositableTextureRef(T* aOther) { *this = aOther; }
+
+ ~CompositableTextureRef() {
+ if (mRef) {
+ mRef->ReleaseCompositableRef();
+ }
+ }
+
+ CompositableTextureRef& operator=(const CompositableTextureRef& aOther) {
+ if (aOther.get()) {
+ aOther->AddCompositableRef();
+ }
+ if (mRef) {
+ mRef->ReleaseCompositableRef();
+ }
+ mRef = aOther.get();
+ return *this;
+ }
+
+ CompositableTextureRef& operator=(T* aOther) {
+ if (aOther) {
+ aOther->AddCompositableRef();
+ }
+ if (mRef) {
+ mRef->ReleaseCompositableRef();
+ }
+ mRef = aOther;
+ return *this;
+ }
+
+ T* get() const { return mRef; }
+ operator T*() const { return mRef; }
+ T* operator->() const { return mRef; }
+ T& operator*() const { return *mRef; }
+
+ private:
+ RefPtr<T> mRef;
+};
+
+typedef CompositableTextureRef<TextureSource> CompositableTextureSourceRef;
+typedef CompositableTextureRef<TextureHost> CompositableTextureHostRef;
+
+/**
+ * Interface for TextureSources that can be updated from a DataSourceSurface.
+ *
+ * All backend should implement at least one DataTextureSource.
+ */
+class DataTextureSource : public TextureSource {
+ public:
+ DataTextureSource() : mOwner(0), mUpdateSerial(0) {}
+
+ const char* Name() const override { return "DataTextureSource"; }
+
+ DataTextureSource* AsDataTextureSource() override { return this; }
+
+ /**
+ * Upload a (portion of) surface to the TextureSource.
+ *
+ * The DataTextureSource doesn't own aSurface, although it owns and manage
+ * the device texture it uploads to internally.
+ */
+ virtual bool Update(gfx::DataSourceSurface* aSurface,
+ nsIntRegion* aDestRegion = nullptr,
+ gfx::IntPoint* aSrcOffset = nullptr,
+ gfx::IntPoint* aDstOffset = nullptr) = 0;
+
+ /**
+ * A facility to avoid reuploading when it is not necessary.
+ * The caller of Update can use GetUpdateSerial to see if the number has
+ * changed since last update, and call SetUpdateSerial after each successful
+ * update. The caller is responsible for managing the update serial except
+ * when the texture data is deallocated in which case the TextureSource should
+ * always reset the update serial to zero.
+ */
+ uint32_t GetUpdateSerial() const { return mUpdateSerial; }
+ void SetUpdateSerial(uint32_t aValue) { mUpdateSerial = aValue; }
+
+ // By default at least set the update serial to zero.
+ // overloaded versions should do that too.
+ void DeallocateDeviceData() override { SetUpdateSerial(0); }
+
+#ifdef DEBUG
+ /**
+ * Provide read access to the data as a DataSourceSurface.
+ *
+ * This is expected to be very slow and should be used for mostly debugging.
+ * XXX - implement everywhere and make it pure virtual.
+ */
+ virtual already_AddRefed<gfx::DataSourceSurface> ReadBack() {
+ return nullptr;
+ };
+#endif
+
+ void SetOwner(TextureHost* aOwner) {
+ auto newOwner = (uintptr_t)aOwner;
+ if (newOwner != mOwner) {
+ mOwner = newOwner;
+ SetUpdateSerial(0);
+ }
+ }
+
+ bool IsOwnedBy(TextureHost* aOwner) const {
+ return mOwner == (uintptr_t)aOwner;
+ }
+
+ bool HasOwner() const { return !IsOwnedBy(nullptr); }
+
+ private:
+ // We store mOwner as an integer rather than as a pointer to make it clear
+ // it is not intended to be dereferenced.
+ uintptr_t mOwner;
+ uint32_t mUpdateSerial;
+};
+
+enum class TextureHostType : int8_t {
+ Unknown = 0,
+ Buffer,
+ DXGI,
+ DXGIYCbCr,
+ DcompSurface,
+ DMABUF,
+ MacIOSurface,
+ AndroidSurfaceTexture,
+ AndroidHardwareBuffer,
+ EGLImage,
+ GLTexture,
+ Last
+};
+
+/**
+ * TextureHost is a thin abstraction over texture data that need to be shared
+ * between the content process and the compositor process. It is the
+ * compositor-side half of a TextureClient/TextureHost pair. A corresponding
+ * TextureClient lives on the content-side.
+ *
+ * TextureHost only knows how to deserialize or synchronize generic image data
+ * (SurfaceDescriptor) and provide access to one or more TextureSource objects
+ * (these provide the necessary APIs for compositor backends to composite the
+ * image).
+ *
+ * A TextureHost implementation corresponds to one SurfaceDescriptor type, as
+ * opposed to TextureSource that corresponds to device textures.
+ * This means that for YCbCr planes, even though they are represented as
+ * 3 textures internally (3 TextureSources), we use 1 TextureHost and not 3,
+ * because the 3 planes are stored in the same buffer of shared memory, before
+ * they are uploaded separately.
+ *
+ * There is always one and only one TextureHost per TextureClient, and the
+ * TextureClient/Host pair only owns one buffer of image data through its
+ * lifetime. This means that the lifetime of the underlying shared data
+ * matches the lifetime of the TextureClient/Host pair. It also means
+ * TextureClient/Host do not implement double buffering, which is the
+ * reponsibility of the compositable (which would use two Texture pairs).
+ *
+ * The Lock/Unlock mecanism here mirrors Lock/Unlock in TextureClient.
+ *
+ */
+class TextureHost : public AtomicRefCountedWithFinalize<TextureHost> {
+ /**
+ * Called once, just before the destructor.
+ *
+ * Here goes the shut-down code that uses virtual methods.
+ * Must only be called by Release().
+ */
+ void Finalize();
+
+ friend class AtomicRefCountedWithFinalize<TextureHost>;
+
+ public:
+ TextureHost(TextureHostType aType, TextureFlags aFlags);
+
+ protected:
+ virtual ~TextureHost();
+
+ public:
+ /**
+ * Factory method.
+ */
+ static already_AddRefed<TextureHost> Create(
+ const SurfaceDescriptor& aDesc, ReadLockDescriptor&& aReadLock,
+ ISurfaceAllocator* aDeallocator, LayersBackend aBackend,
+ TextureFlags aFlags, wr::MaybeExternalImageId& aExternalImageId);
+
+ /**
+ * Lock the texture host for compositing without using compositor.
+ */
+ virtual bool LockWithoutCompositor() { return true; }
+ /**
+ * Similar to Unlock(), but it should be called with LockWithoutCompositor().
+ */
+ virtual void UnlockWithoutCompositor() {}
+
+ /**
+ * Note that the texture host format can be different from its corresponding
+ * texture source's. For example a ShmemTextureHost can have the ycbcr
+ * format and produce 3 "alpha" textures sources.
+ */
+ virtual gfx::SurfaceFormat GetFormat() const = 0;
+ /**
+ * Return the format used for reading the texture.
+ * Apple's YCBCR_422 is R8G8B8X8.
+ */
+ virtual gfx::SurfaceFormat GetReadFormat() const { return GetFormat(); }
+
+ virtual gfx::YUVColorSpace GetYUVColorSpace() const {
+ return gfx::YUVColorSpace::Identity;
+ }
+
+ /**
+ * Return the color depth of the image. Used with YUV textures.
+ */
+ virtual gfx::ColorDepth GetColorDepth() const {
+ return gfx::ColorDepth::COLOR_8;
+ }
+
+ /**
+ * Return true if using full range values (0-255 if 8 bits YUV). Used with YUV
+ * textures.
+ */
+ virtual gfx::ColorRange GetColorRange() const {
+ return gfx::ColorRange::LIMITED;
+ }
+
+ /**
+ * Called when another TextureHost will take over.
+ */
+ virtual void UnbindTextureSource();
+
+ virtual bool IsValid() { return true; }
+
+ /**
+ * Should be overridden in order to deallocate the data that is associated
+ * with the rendering backend, such as GL textures.
+ */
+ virtual void DeallocateDeviceData() {}
+
+ /**
+ * Should be overridden in order to deallocate the data that is shared with
+ * the content side, such as shared memory.
+ */
+ virtual void DeallocateSharedData() {}
+
+ /**
+ * Should be overridden in order to force the TextureHost to drop all
+ * references to it's shared data.
+ *
+ * This is important to ensure the correctness of the deallocation protocol.
+ */
+ virtual void ForgetSharedData() {}
+
+ virtual gfx::IntSize GetSize() const = 0;
+
+ /**
+ * Should be overridden if TextureHost supports crop rect.
+ */
+ virtual void SetCropRect(nsIntRect aCropRect) {}
+
+ /**
+ * Debug facility.
+ * XXX - cool kids use Moz2D. See bug 882113.
+ */
+ virtual already_AddRefed<gfx::DataSourceSurface> GetAsSurface() = 0;
+
+ /**
+ * XXX - Flags should only be set at creation time, this will be removed.
+ */
+ void SetFlags(TextureFlags aFlags) { mFlags = aFlags; }
+
+ /**
+ * XXX - Flags should only be set at creation time, this will be removed.
+ */
+ void AddFlag(TextureFlags aFlag) { mFlags |= aFlag; }
+
+ TextureFlags GetFlags() { return mFlags; }
+
+ wr::MaybeExternalImageId GetMaybeExternalImageId() const {
+ return mExternalImageId;
+ }
+
+ /**
+ * Allocate and deallocate a TextureParent actor.
+ *
+ * TextureParent< is an implementation detail of TextureHost that is not
+ * exposed to the rest of the code base. CreateIPDLActor and DestroyIPDLActor
+ * are for use with the managing IPDL protocols only (so that they can
+ * implement AllocPTextureParent and DeallocPTextureParent).
+ */
+ static PTextureParent* CreateIPDLActor(
+ HostIPCAllocator* aAllocator, const SurfaceDescriptor& aSharedData,
+ ReadLockDescriptor&& aDescriptor, LayersBackend aLayersBackend,
+ TextureFlags aFlags, const dom::ContentParentId& aContentId,
+ uint64_t aSerial, const wr::MaybeExternalImageId& aExternalImageId);
+ static bool DestroyIPDLActor(PTextureParent* actor);
+
+ /**
+ * Destroy the TextureChild/Parent pair.
+ */
+ static bool SendDeleteIPDLActor(PTextureParent* actor);
+
+ static void ReceivedDestroy(PTextureParent* actor);
+
+ /**
+ * Get the TextureHost corresponding to the actor passed in parameter.
+ */
+ static TextureHost* AsTextureHost(PTextureParent* actor);
+
+ static uint64_t GetTextureSerial(PTextureParent* actor);
+
+ static dom::ContentParentId GetTextureContentId(PTextureParent* actor);
+
+ /**
+ * Return a pointer to the IPDLActor.
+ *
+ * This is to be used with IPDL messages only. Do not store the returned
+ * pointer.
+ */
+ PTextureParent* GetIPDLActor();
+
+ // If a texture host holds a reference to shmem, it should override this
+ // method to forget about the shmem _without_ releasing it.
+ virtual void OnShutdown() {}
+
+ // Forget buffer actor. Used only for hacky fix for bug 966446.
+ virtual void ForgetBufferActor() {}
+
+ virtual const char* Name() { return "TextureHost"; }
+
+ /**
+ * Returns true if the TextureHost can be released before the rendering is
+ * completed, otherwise returns false.
+ */
+ virtual bool NeedsDeferredDeletion() const { return true; }
+
+ void AddCompositableRef() {
+ ++mCompositableCount;
+ if (mCompositableCount == 1) {
+ PrepareForUse();
+ }
+ }
+
+ void ReleaseCompositableRef() {
+ --mCompositableCount;
+ MOZ_ASSERT(mCompositableCount >= 0);
+ if (mCompositableCount == 0) {
+ UnbindTextureSource();
+ // Send mFwdTransactionId to client side if necessary.
+ NotifyNotUsed();
+ }
+ }
+
+ int NumCompositableRefs() const { return mCompositableCount; }
+
+ void SetLastFwdTransactionId(uint64_t aTransactionId);
+
+ void DeserializeReadLock(ReadLockDescriptor&& aDesc,
+ ISurfaceAllocator* aAllocator);
+ void SetReadLocked();
+
+ TextureReadLock* GetReadLock() { return mReadLock; }
+
+ virtual BufferTextureHost* AsBufferTextureHost() { return nullptr; }
+ virtual ShmemTextureHost* AsShmemTextureHost() { return nullptr; }
+ virtual MacIOSurfaceTextureHostOGL* AsMacIOSurfaceTextureHost() {
+ return nullptr;
+ }
+ virtual WebRenderTextureHost* AsWebRenderTextureHost() { return nullptr; }
+ virtual SurfaceTextureHost* AsSurfaceTextureHost() { return nullptr; }
+ virtual AndroidHardwareBufferTextureHost*
+ AsAndroidHardwareBufferTextureHost() {
+ return nullptr;
+ }
+ virtual RemoteTextureHostWrapper* AsRemoteTextureHostWrapper() {
+ return nullptr;
+ }
+
+ virtual bool IsWrappingSurfaceTextureHost() { return false; }
+
+ // Create the corresponding RenderTextureHost type of this texture, and
+ // register the RenderTextureHost into render thread.
+ virtual void CreateRenderTexture(
+ const wr::ExternalImageId& aExternalImageId) {
+ MOZ_RELEASE_ASSERT(
+ false,
+ "No CreateRenderTexture() implementation for this TextureHost type.");
+ }
+
+ void EnsureRenderTexture(const wr::MaybeExternalImageId& aExternalImageId);
+
+ // Destroy RenderTextureHost when it was created by the TextureHost.
+ // It is called in TextureHost::Finalize().
+ virtual void MaybeDestroyRenderTexture();
+
+ static void DestroyRenderTexture(const wr::ExternalImageId& aExternalImageId);
+
+ /// Returns the number of actual textures that will be used to render this.
+ /// For example in a lot of YUV cases it will be 3
+ virtual uint32_t NumSubTextures() { return 1; }
+
+ enum ResourceUpdateOp {
+ ADD_IMAGE,
+ UPDATE_IMAGE,
+ };
+
+ // Add all necessary TextureHost informations to the resource update queue.
+ virtual void PushResourceUpdates(wr::TransactionBuilder& aResources,
+ ResourceUpdateOp aOp,
+ const Range<wr::ImageKey>& aImageKeys,
+ const wr::ExternalImageId& aExtID) {
+ MOZ_ASSERT_UNREACHABLE("Unimplemented");
+ }
+
+ enum class PushDisplayItemFlag {
+ // Passed if the caller wants these display items to be promoted
+ // to compositor surfaces if possible.
+ PREFER_COMPOSITOR_SURFACE,
+
+ // Passed in the RenderCompositor supports BufferTextureHosts
+ // being used directly as external compositor surfaces.
+ SUPPORTS_EXTERNAL_BUFFER_TEXTURES,
+ };
+ using PushDisplayItemFlagSet = EnumSet<PushDisplayItemFlag>;
+
+ // Put all necessary WR commands into DisplayListBuilder for this textureHost
+ // rendering.
+ virtual void PushDisplayItems(wr::DisplayListBuilder& aBuilder,
+ const wr::LayoutRect& aBounds,
+ const wr::LayoutRect& aClip,
+ wr::ImageRendering aFilter,
+ const Range<wr::ImageKey>& aKeys,
+ PushDisplayItemFlagSet aFlags) {
+ MOZ_ASSERT_UNREACHABLE(
+ "No PushDisplayItems() implementation for this TextureHost type.");
+ }
+
+ /**
+ * Some API's can use the cross-process IOSurface directly, such as OpenVR
+ */
+ virtual MacIOSurface* GetMacIOSurface() { return nullptr; }
+
+ virtual bool NeedsYFlip() const;
+
+ virtual void SetAcquireFence(mozilla::ipc::FileDescriptor&& aFenceFd) {}
+
+ virtual void SetReleaseFence(mozilla::ipc::FileDescriptor&& aFenceFd) {}
+
+ virtual mozilla::ipc::FileDescriptor GetAndResetReleaseFence() {
+ return mozilla::ipc::FileDescriptor();
+ }
+
+ virtual AndroidHardwareBuffer* GetAndroidHardwareBuffer() const {
+ return nullptr;
+ }
+
+ virtual bool SupportsExternalCompositing(WebRenderBackend aBackend) {
+ return false;
+ }
+
+ virtual TextureHostType GetTextureHostType() { return mTextureHostType; }
+
+ // Our WebRender backend may impose restrictions on whether textures are
+ // prepared as native textures or not, or it may have no restriction at
+ // all. This enumerates those possibilities.
+ enum NativeTexturePolicy {
+ REQUIRE,
+ FORBID,
+ DONT_CARE,
+ };
+
+ static NativeTexturePolicy BackendNativeTexturePolicy(
+ layers::WebRenderBackend aBackend, gfx::IntSize aSize) {
+ static const int32_t SWGL_DIMENSION_MAX = 1 << 15;
+ if (aBackend == WebRenderBackend::SOFTWARE) {
+ return (aSize.width <= SWGL_DIMENSION_MAX &&
+ aSize.height <= SWGL_DIMENSION_MAX)
+ ? REQUIRE
+ : FORBID;
+ }
+ return DONT_CARE;
+ }
+
+ void SetDestroyedCallback(std::function<void()>&& aDestroyedCallback) {
+ MOZ_ASSERT(!mDestroyedCallback);
+ mDestroyedCallback = std::move(aDestroyedCallback);
+ }
+
+ protected:
+ virtual void ReadUnlock();
+
+ void RecycleTexture(TextureFlags aFlags);
+
+ /**
+ * Called when mCompositableCount becomes from 0 to 1.
+ */
+ virtual void PrepareForUse();
+
+ /**
+ * Called when mCompositableCount becomes 0.
+ */
+ virtual void NotifyNotUsed();
+
+ // for Compositor.
+ void CallNotifyNotUsed();
+
+ TextureHostType mTextureHostType;
+ PTextureParent* mActor;
+ RefPtr<TextureReadLock> mReadLock;
+ TextureFlags mFlags;
+ int mCompositableCount;
+ uint64_t mFwdTransactionId;
+ bool mReadLocked;
+ wr::MaybeExternalImageId mExternalImageId;
+
+ std::function<void()> mDestroyedCallback;
+
+ friend class Compositor;
+ friend class RemoteTextureHostWrapper;
+ friend class TextureParent;
+ friend class TextureSourceProvider;
+ friend class GPUVideoTextureHost;
+ friend class WebRenderTextureHost;
+};
+
+/**
+ * TextureHost that wraps a random access buffer such as a Shmem or some raw
+ * memory.
+ *
+ * This TextureHost is backend-independent and the backend-specific bits are
+ * in the TextureSource.
+ * This class must be inherited to implement GetBuffer and DeallocSharedData
+ * (see ShmemTextureHost and MemoryTextureHost)
+ *
+ * Uploads happen when Lock is called.
+ *
+ * BufferTextureHost supports YCbCr and flavours of RGBA images (RGBX, A, etc.).
+ */
+class BufferTextureHost : public TextureHost {
+ public:
+ BufferTextureHost(const BufferDescriptor& aDescriptor, TextureFlags aFlags);
+
+ virtual ~BufferTextureHost();
+
+ virtual uint8_t* GetBuffer() = 0;
+
+ virtual size_t GetBufferSize() = 0;
+
+ void UnbindTextureSource() override;
+
+ void DeallocateDeviceData() override;
+
+ /**
+ * Return the format that is exposed to the compositor when calling
+ * BindTextureSource.
+ *
+ * If the shared format is YCbCr and the compositor does not support it,
+ * GetFormat will be RGB32 (even though mFormat is SurfaceFormat::YUV).
+ */
+ gfx::SurfaceFormat GetFormat() const override;
+
+ gfx::YUVColorSpace GetYUVColorSpace() const override;
+
+ gfx::ColorDepth GetColorDepth() const override;
+
+ gfx::ColorRange GetColorRange() const override;
+
+ gfx::IntSize GetSize() const override { return mSize; }
+
+ already_AddRefed<gfx::DataSourceSurface> GetAsSurface() override;
+
+ bool NeedsDeferredDeletion() const override {
+ return TextureHost::NeedsDeferredDeletion() || UseExternalTextures();
+ }
+
+ BufferTextureHost* AsBufferTextureHost() override { return this; }
+
+ const BufferDescriptor& GetBufferDescriptor() const { return mDescriptor; }
+
+ void CreateRenderTexture(
+ const wr::ExternalImageId& aExternalImageId) override;
+
+ uint32_t NumSubTextures() override;
+
+ void PushResourceUpdates(wr::TransactionBuilder& aResources,
+ ResourceUpdateOp aOp,
+ const Range<wr::ImageKey>& aImageKeys,
+ const wr::ExternalImageId& aExtID) override;
+
+ void PushDisplayItems(wr::DisplayListBuilder& aBuilder,
+ const wr::LayoutRect& aBounds,
+ const wr::LayoutRect& aClip, wr::ImageRendering aFilter,
+ const Range<wr::ImageKey>& aImageKeys,
+ PushDisplayItemFlagSet aFlags) override;
+
+ protected:
+ bool UseExternalTextures() const { return mUseExternalTextures; }
+
+ BufferDescriptor mDescriptor;
+ RefPtr<Compositor> mCompositor;
+ gfx::IntSize mSize;
+ gfx::SurfaceFormat mFormat;
+ bool mLocked;
+ bool mUseExternalTextures;
+
+ class DataTextureSourceYCbCrBasic;
+};
+
+/**
+ * TextureHost that wraps shared memory.
+ * the corresponding texture on the client side is ShmemTextureClient.
+ * This TextureHost is backend-independent.
+ */
+class ShmemTextureHost : public BufferTextureHost {
+ public:
+ ShmemTextureHost(const mozilla::ipc::Shmem& aShmem,
+ const BufferDescriptor& aDesc,
+ ISurfaceAllocator* aDeallocator, TextureFlags aFlags);
+
+ protected:
+ ~ShmemTextureHost();
+
+ public:
+ void DeallocateSharedData() override;
+
+ void ForgetSharedData() override;
+
+ uint8_t* GetBuffer() override;
+
+ size_t GetBufferSize() override;
+
+ const char* Name() override { return "ShmemTextureHost"; }
+
+ void OnShutdown() override;
+
+ ShmemTextureHost* AsShmemTextureHost() override { return this; }
+
+ protected:
+ UniquePtr<mozilla::ipc::Shmem> mShmem;
+ RefPtr<ISurfaceAllocator> mDeallocator;
+};
+
+/**
+ * TextureHost that wraps raw memory.
+ * The corresponding texture on the client side is MemoryTextureClient.
+ * Can obviously not be used in a cross process setup.
+ * This TextureHost is backend-independent.
+ */
+class MemoryTextureHost : public BufferTextureHost {
+ public:
+ MemoryTextureHost(uint8_t* aBuffer, const BufferDescriptor& aDesc,
+ TextureFlags aFlags);
+
+ protected:
+ ~MemoryTextureHost();
+
+ public:
+ void DeallocateSharedData() override;
+
+ void ForgetSharedData() override;
+
+ uint8_t* GetBuffer() override;
+
+ size_t GetBufferSize() override;
+
+ const char* Name() override { return "MemoryTextureHost"; }
+
+ protected:
+ uint8_t* mBuffer;
+};
+
+class MOZ_STACK_CLASS AutoLockTextureHostWithoutCompositor {
+ public:
+ explicit AutoLockTextureHostWithoutCompositor(TextureHost* aTexture)
+ : mTexture(aTexture) {
+ mLocked = mTexture ? mTexture->LockWithoutCompositor() : false;
+ }
+
+ ~AutoLockTextureHostWithoutCompositor() {
+ if (mTexture && mLocked) {
+ mTexture->UnlockWithoutCompositor();
+ }
+ }
+
+ bool Failed() { return mTexture && !mLocked; }
+
+ private:
+ RefPtr<TextureHost> mTexture;
+ bool mLocked;
+};
+
+/**
+ * This can be used as an offscreen rendering target by the compositor, and
+ * subsequently can be used as a source by the compositor.
+ */
+class CompositingRenderTarget : public TextureSource {
+ public:
+ explicit CompositingRenderTarget(const gfx::IntPoint& aOrigin)
+ : mClearOnBind(false),
+ mOrigin(aOrigin),
+ mZNear(0),
+ mZFar(0),
+ mHasComplexProjection(false),
+ mEnableDepthBuffer(false) {}
+ virtual ~CompositingRenderTarget() = default;
+
+ const char* Name() const override { return "CompositingRenderTarget"; }
+
+#ifdef MOZ_DUMP_PAINTING
+ virtual already_AddRefed<gfx::DataSourceSurface> Dump(
+ Compositor* aCompositor) {
+ return nullptr;
+ }
+#endif
+
+ /**
+ * Perform a clear when recycling a non opaque surface.
+ * The clear is deferred to when the render target is bound.
+ */
+ void ClearOnBind() { mClearOnBind = true; }
+
+ const gfx::IntPoint& GetOrigin() const { return mOrigin; }
+ gfx::IntRect GetRect() { return gfx::IntRect(GetOrigin(), GetSize()); }
+
+ /**
+ * If a Projection matrix is set, then it is used for rendering to
+ * this render target instead of generating one. If no explicit
+ * projection is set, Compositors are expected to generate an
+ * orthogonal maaping that maps 0..1 to the full size of the render
+ * target.
+ */
+ bool HasComplexProjection() const { return mHasComplexProjection; }
+ void ClearProjection() { mHasComplexProjection = false; }
+ void SetProjection(const gfx::Matrix4x4& aNewMatrix, bool aEnableDepthBuffer,
+ float aZNear, float aZFar) {
+ mProjectionMatrix = aNewMatrix;
+ mEnableDepthBuffer = aEnableDepthBuffer;
+ mZNear = aZNear;
+ mZFar = aZFar;
+ mHasComplexProjection = true;
+ }
+ void GetProjection(gfx::Matrix4x4& aMatrix, bool& aEnableDepth, float& aZNear,
+ float& aZFar) {
+ MOZ_ASSERT(mHasComplexProjection);
+ aMatrix = mProjectionMatrix;
+ aEnableDepth = mEnableDepthBuffer;
+ aZNear = mZNear;
+ aZFar = mZFar;
+ }
+
+ protected:
+ bool mClearOnBind;
+
+ private:
+ gfx::IntPoint mOrigin;
+
+ gfx::Matrix4x4 mProjectionMatrix;
+ float mZNear, mZFar;
+ bool mHasComplexProjection;
+ bool mEnableDepthBuffer;
+};
+
+/**
+ * Creates a TextureHost that can be used with any of the existing backends
+ * Not all SurfaceDescriptor types are supported
+ */
+already_AddRefed<TextureHost> CreateBackendIndependentTextureHost(
+ const SurfaceDescriptor& aDesc, ISurfaceAllocator* aDeallocator,
+ LayersBackend aBackend, TextureFlags aFlags);
+
+} // namespace layers
+} // namespace mozilla
+
+#endif
diff --git a/gfx/layers/d3d11/BlendShaderConstants.h b/gfx/layers/d3d11/BlendShaderConstants.h
new file mode 100644
index 0000000000..4c7493b6db
--- /dev/null
+++ b/gfx/layers/d3d11/BlendShaderConstants.h
@@ -0,0 +1,18 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_GFX_LAYERS_D3D11_BLENDSHADERCONSTANTS_H_
+#define MOZILLA_GFX_LAYERS_D3D11_BLENDSHADERCONSTANTS_H_
+
+// These constants are shared between CompositorD3D11 and the blend pixel
+// shader.
+#define PS_LAYER_RGB 0
+#define PS_LAYER_RGBA 1
+#define PS_LAYER_YCBCR 2
+#define PS_LAYER_COLOR 3
+#define PS_LAYER_NV12 4
+
+#endif // MOZILLA_GFX_LAYERS_D3D11_BLENDSHADERCONSTANTS_H_
diff --git a/gfx/layers/d3d11/CompositorD3D11.cpp b/gfx/layers/d3d11/CompositorD3D11.cpp
new file mode 100644
index 0000000000..662fc47132
--- /dev/null
+++ b/gfx/layers/d3d11/CompositorD3D11.cpp
@@ -0,0 +1,1400 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "CompositorD3D11.h"
+
+#include "TextureD3D11.h"
+
+#include "gfxWindowsPlatform.h"
+#include "nsIWidget.h"
+#include "mozilla/gfx/D3D11Checks.h"
+#include "mozilla/gfx/DeviceManagerDx.h"
+#include "mozilla/gfx/GPUParent.h"
+#include "mozilla/gfx/Swizzle.h"
+#include "mozilla/layers/Diagnostics.h"
+#include "mozilla/layers/Effects.h"
+#include "mozilla/layers/HelpersD3D11.h"
+#include "nsWindowsHelpers.h"
+#include "gfxConfig.h"
+#include "gfxCrashReporterUtils.h"
+#include "gfxUtils.h"
+#include "mozilla/gfx/StackArray.h"
+#include "mozilla/widget/WinCompositorWidget.h"
+
+#include "mozilla/EnumeratedArray.h"
+#include "mozilla/ProfilerState.h"
+#include "mozilla/StaticPrefs_gfx.h"
+#include "mozilla/StaticPrefs_layers.h"
+#include "mozilla/Telemetry.h"
+
+#include "D3D11ShareHandleImage.h"
+#include "DeviceAttachmentsD3D11.h"
+#include "BlendShaderConstants.h"
+
+#include <versionhelpers.h> // For IsWindows8OrGreater
+#include <winsdkver.h>
+
+namespace mozilla {
+
+using namespace gfx;
+
+namespace layers {
+
+bool CanUsePartialPresents(ID3D11Device* aDevice);
+
+const FLOAT sBlendFactor[] = {0, 0, 0, 0};
+
+class AsyncReadbackBufferD3D11 final : public AsyncReadbackBuffer {
+ public:
+ AsyncReadbackBufferD3D11(ID3D11DeviceContext* aContext,
+ ID3D11Texture2D* aTexture, const IntSize& aSize);
+
+ bool MapAndCopyInto(DataSourceSurface* aSurface,
+ const IntSize& aReadSize) const override;
+
+ ID3D11Texture2D* GetTexture() { return mTexture; }
+
+ private:
+ RefPtr<ID3D11DeviceContext> mContext;
+ RefPtr<ID3D11Texture2D> mTexture;
+};
+
+AsyncReadbackBufferD3D11::AsyncReadbackBufferD3D11(
+ ID3D11DeviceContext* aContext, ID3D11Texture2D* aTexture,
+ const IntSize& aSize)
+ : AsyncReadbackBuffer(aSize), mContext(aContext), mTexture(aTexture) {}
+
+bool AsyncReadbackBufferD3D11::MapAndCopyInto(DataSourceSurface* aSurface,
+ const IntSize& aReadSize) const {
+ D3D11_MAPPED_SUBRESOURCE map;
+ HRESULT hr = mContext->Map(mTexture, 0, D3D11_MAP_READ, 0, &map);
+
+ if (FAILED(hr)) {
+ return false;
+ }
+
+ RefPtr<DataSourceSurface> sourceSurface =
+ Factory::CreateWrappingDataSourceSurface(static_cast<uint8_t*>(map.pData),
+ map.RowPitch, mSize,
+ SurfaceFormat::B8G8R8A8);
+
+ bool result;
+ {
+ DataSourceSurface::ScopedMap sourceMap(sourceSurface,
+ DataSourceSurface::READ);
+ DataSourceSurface::ScopedMap destMap(aSurface, DataSourceSurface::WRITE);
+
+ result = SwizzleData(sourceMap.GetData(), sourceMap.GetStride(),
+ SurfaceFormat::B8G8R8A8, destMap.GetData(),
+ destMap.GetStride(), aSurface->GetFormat(), aReadSize);
+ }
+
+ mContext->Unmap(mTexture, 0);
+
+ return result;
+}
+
+CompositorD3D11::CompositorD3D11(widget::CompositorWidget* aWidget)
+ : Compositor(aWidget),
+ mWindowRTCopy(nullptr),
+ mAttachments(nullptr),
+ mHwnd(nullptr),
+ mDisableSequenceForNextFrame(false),
+ mAllowPartialPresents(false),
+ mIsDoubleBuffered(false),
+ mVerifyBuffersFailed(false),
+ mUseMutexOnPresent(false) {
+ mUseMutexOnPresent = StaticPrefs::gfx_use_mutex_on_present_AtStartup();
+}
+
+CompositorD3D11::~CompositorD3D11() {}
+
+template <typename VertexType>
+void CompositorD3D11::SetVertexBuffer(ID3D11Buffer* aBuffer) {
+ UINT size = sizeof(VertexType);
+ UINT offset = 0;
+ mContext->IASetVertexBuffers(0, 1, &aBuffer, &size, &offset);
+}
+
+bool CompositorD3D11::Initialize(nsCString* const out_failureReason) {
+ ScopedGfxFeatureReporter reporter("D3D11 Layers");
+
+ HRESULT hr;
+
+ DeviceManagerDx::Get()->GetCompositorDevices(&mDevice, &mAttachments);
+ if (!mDevice) {
+ gfxCriticalNote << "[D3D11] failed to get compositor device.";
+ *out_failureReason = "FEATURE_FAILURE_D3D11_NO_DEVICE";
+ return false;
+ }
+ if (!mAttachments || !mAttachments->IsValid()) {
+ gfxCriticalNote << "[D3D11] failed to get compositor device attachments";
+ *out_failureReason = mAttachments ? mAttachments->GetFailureId()
+ : "FEATURE_FAILURE_NO_ATTACHMENTS"_ns;
+ return false;
+ }
+
+ mDevice->GetImmediateContext(getter_AddRefs(mContext));
+ if (!mContext) {
+ gfxCriticalNote << "[D3D11] failed to get immediate context";
+ *out_failureReason = "FEATURE_FAILURE_D3D11_CONTEXT";
+ return false;
+ }
+
+ mFeatureLevel = mDevice->GetFeatureLevel();
+
+ mHwnd = mWidget->AsWindows()->GetHwnd();
+
+ memset(&mVSConstants, 0, sizeof(VertexShaderConstants));
+
+ RefPtr<IDXGIDevice> dxgiDevice;
+ RefPtr<IDXGIAdapter> dxgiAdapter;
+
+ mDevice->QueryInterface(dxgiDevice.StartAssignment());
+ dxgiDevice->GetAdapter(getter_AddRefs(dxgiAdapter));
+
+ {
+ RefPtr<IDXGIFactory> dxgiFactory;
+ dxgiAdapter->GetParent(IID_PPV_ARGS(dxgiFactory.StartAssignment()));
+
+ RefPtr<IDXGIFactory2> dxgiFactory2;
+ hr = dxgiFactory->QueryInterface(
+ (IDXGIFactory2**)getter_AddRefs(dxgiFactory2));
+
+#if (_WIN32_WINDOWS_MAXVER >= 0x0A00)
+ if (gfxVars::UseDoubleBufferingWithCompositor() && SUCCEEDED(hr) &&
+ dxgiFactory2) {
+ // DXGI_SCALING_NONE is not available on Windows 7 with Platform Update.
+ // This looks awful for things like the awesome bar and browser window
+ // resizing so we don't use a flip buffer chain here. When using
+ // EFFECT_SEQUENTIAL it looks like windows doesn't stretch the surface
+ // when resizing. We chose not to run this before Windows 10 because it
+ // appears sometimes this breaks our ability to test ASAP compositing.
+ RefPtr<IDXGISwapChain1> swapChain;
+
+ DXGI_SWAP_CHAIN_DESC1 swapDesc;
+ ::ZeroMemory(&swapDesc, sizeof(swapDesc));
+ swapDesc.Width = 0;
+ swapDesc.Height = 0;
+ swapDesc.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
+ swapDesc.SampleDesc.Count = 1;
+ swapDesc.SampleDesc.Quality = 0;
+ swapDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
+ swapDesc.BufferCount = 2;
+ swapDesc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL;
+ swapDesc.Scaling = DXGI_SCALING_NONE;
+ mIsDoubleBuffered = true;
+ swapDesc.Flags = 0;
+
+ /**
+ * Create a swap chain, this swap chain will contain the backbuffer for
+ * the window we draw to. The front buffer is the full screen front
+ * buffer.
+ */
+ hr = dxgiFactory2->CreateSwapChainForHwnd(mDevice, mHwnd, &swapDesc,
+ nullptr, nullptr,
+ getter_AddRefs(swapChain));
+ if (SUCCEEDED(hr)) {
+ DXGI_RGBA color = {1.0f, 1.0f, 1.0f, 1.0f};
+ swapChain->SetBackgroundColor(&color);
+
+ mSwapChain = swapChain;
+ } else if (mWidget->AsWindows()->GetCompositorHwnd()) {
+ // Destroy compositor window.
+ mWidget->AsWindows()->DestroyCompositorWindow();
+ mHwnd = mWidget->AsWindows()->GetHwnd();
+ }
+ }
+
+ // In some configurations double buffering may have failed with an
+ // ACCESS_DENIED error.
+ if (!mSwapChain)
+#endif
+ {
+ if (mWidget->AsWindows()->GetCompositorHwnd()) {
+ // Destroy compositor window.
+ mWidget->AsWindows()->DestroyCompositorWindow();
+ mHwnd = mWidget->AsWindows()->GetHwnd();
+ }
+
+ DXGI_SWAP_CHAIN_DESC swapDesc;
+ ::ZeroMemory(&swapDesc, sizeof(swapDesc));
+ swapDesc.BufferDesc.Width = 0;
+ swapDesc.BufferDesc.Height = 0;
+ swapDesc.BufferDesc.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
+ swapDesc.BufferDesc.RefreshRate.Numerator = 60;
+ swapDesc.BufferDesc.RefreshRate.Denominator = 1;
+ swapDesc.SampleDesc.Count = 1;
+ swapDesc.SampleDesc.Quality = 0;
+ swapDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
+ swapDesc.BufferCount = 1;
+ swapDesc.OutputWindow = mHwnd;
+ swapDesc.Windowed = TRUE;
+ swapDesc.Flags = 0;
+ swapDesc.SwapEffect = DXGI_SWAP_EFFECT_SEQUENTIAL;
+
+ /**
+ * Create a swap chain, this swap chain will contain the backbuffer for
+ * the window we draw to. The front buffer is the full screen front
+ * buffer.
+ */
+ hr = dxgiFactory->CreateSwapChain(dxgiDevice, &swapDesc,
+ getter_AddRefs(mSwapChain));
+ if (Failed(hr, "create swap chain")) {
+ *out_failureReason = "FEATURE_FAILURE_D3D11_SWAP_CHAIN";
+ return false;
+ }
+ }
+
+ // We need this because we don't want DXGI to respond to Alt+Enter.
+ dxgiFactory->MakeWindowAssociation(mHwnd, DXGI_MWA_NO_WINDOW_CHANGES);
+ }
+
+ if (!mWidget->InitCompositor(this)) {
+ *out_failureReason = "FEATURE_FAILURE_D3D11_INIT_COMPOSITOR";
+ return false;
+ }
+
+ mAllowPartialPresents = CanUsePartialPresents(mDevice);
+
+ reporter.SetSuccessful();
+ return true;
+}
+
+bool CanUsePartialPresents(ID3D11Device* aDevice) {
+ if (StaticPrefs::gfx_partialpresent_force() > 0) {
+ return true;
+ }
+ if (StaticPrefs::gfx_partialpresent_force() < 0) {
+ return false;
+ }
+ if (DeviceManagerDx::Get()->IsWARP()) {
+ return true;
+ }
+
+ DXGI_ADAPTER_DESC desc;
+ if (!D3D11Checks::GetDxgiDesc(aDevice, &desc)) {
+ return false;
+ }
+
+ // We have to disable partial presents on NVIDIA (bug 1189940).
+ if (desc.VendorId == 0x10de) {
+ return false;
+ }
+
+ return true;
+}
+
+already_AddRefed<DataTextureSource> CompositorD3D11::CreateDataTextureSource(
+ TextureFlags aFlags) {
+ RefPtr<DataTextureSource> result =
+ new DataTextureSourceD3D11(gfx::SurfaceFormat::UNKNOWN, this, aFlags);
+ return result.forget();
+}
+
+int32_t CompositorD3D11::GetMaxTextureSize() const {
+ return GetMaxTextureSizeForFeatureLevel(mFeatureLevel);
+}
+
+already_AddRefed<CompositingRenderTarget> CompositorD3D11::CreateRenderTarget(
+ const gfx::IntRect& aRect, SurfaceInitMode aInit) {
+ MOZ_ASSERT(!aRect.IsZeroArea());
+
+ if (aRect.IsZeroArea()) {
+ return nullptr;
+ }
+
+ CD3D11_TEXTURE2D_DESC desc(
+ DXGI_FORMAT_B8G8R8A8_UNORM, aRect.width, aRect.height, 1, 1,
+ D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_RENDER_TARGET);
+
+ RefPtr<ID3D11Texture2D> texture;
+ HRESULT hr =
+ mDevice->CreateTexture2D(&desc, nullptr, getter_AddRefs(texture));
+ if (FAILED(hr) || !texture) {
+ gfxCriticalNote << "Failed in CreateRenderTarget " << hexa(hr);
+ return nullptr;
+ }
+
+ RefPtr<CompositingRenderTargetD3D11> rt =
+ new CompositingRenderTargetD3D11(texture, aRect.TopLeft());
+ rt->SetSize(IntSize(aRect.Width(), aRect.Height()));
+
+ if (aInit == INIT_MODE_CLEAR) {
+ FLOAT clear[] = {0, 0, 0, 0};
+ mContext->ClearRenderTargetView(rt->mRTView, clear);
+ }
+
+ return rt.forget();
+}
+
+RefPtr<ID3D11Texture2D> CompositorD3D11::CreateTexture(
+ const gfx::IntRect& aRect, const CompositingRenderTarget* aSource,
+ const gfx::IntPoint& aSourcePoint) {
+ MOZ_ASSERT(!aRect.IsZeroArea());
+
+ if (aRect.IsZeroArea()) {
+ return nullptr;
+ }
+
+ CD3D11_TEXTURE2D_DESC desc(
+ DXGI_FORMAT_B8G8R8A8_UNORM, aRect.Width(), aRect.Height(), 1, 1,
+ D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_RENDER_TARGET);
+
+ RefPtr<ID3D11Texture2D> texture;
+ HRESULT hr =
+ mDevice->CreateTexture2D(&desc, nullptr, getter_AddRefs(texture));
+
+ if (FAILED(hr) || !texture) {
+ gfxCriticalNote << "Failed in CreateRenderTargetFromSource " << hexa(hr);
+ HandleError(hr);
+ return nullptr;
+ }
+
+ if (aSource) {
+ const CompositingRenderTargetD3D11* sourceD3D11 =
+ static_cast<const CompositingRenderTargetD3D11*>(aSource);
+
+ const IntSize& srcSize = sourceD3D11->GetSize();
+ MOZ_ASSERT(srcSize.width >= 0 && srcSize.height >= 0,
+ "render targets should have nonnegative sizes");
+
+ IntRect srcRect(IntPoint(), srcSize);
+ IntRect copyRect(aSourcePoint, aRect.Size());
+ if (!srcRect.Contains(copyRect)) {
+ NS_WARNING("Could not copy the whole copy rect from the render target");
+ }
+
+ copyRect = copyRect.Intersect(srcRect);
+
+ if (!copyRect.IsEmpty()) {
+ D3D11_BOX copyBox;
+ copyBox.front = 0;
+ copyBox.back = 1;
+ copyBox.left = copyRect.X();
+ copyBox.top = copyRect.Y();
+ copyBox.right = copyRect.XMost();
+ copyBox.bottom = copyRect.YMost();
+
+ mContext->CopySubresourceRegion(
+ texture, 0, 0, 0, 0, sourceD3D11->GetD3D11Texture(), 0, &copyBox);
+ }
+ }
+
+ return texture;
+}
+
+bool CompositorD3D11::ShouldAllowFrameRecording() const {
+ return mAllowFrameRecording ||
+ profiler_feature_active(ProfilerFeature::Screenshots);
+}
+
+already_AddRefed<CompositingRenderTarget>
+CompositorD3D11::GetWindowRenderTarget() const {
+ if (!ShouldAllowFrameRecording()) {
+ return nullptr;
+ }
+
+ if (!mDefaultRT) {
+ return nullptr;
+ }
+
+ const IntSize size = mDefaultRT->GetSize();
+
+ RefPtr<ID3D11Texture2D> rtTexture;
+
+ if (!mWindowRTCopy || mWindowRTCopy->GetSize() != size) {
+ /*
+ * The compositor screenshots infrastructure is going to scale down the
+ * render target returned by this method. However, mDefaultRT does not
+ * contain a texture created wth the D3D11_BIND_SHADER_RESOURCE flag, so if
+ * we were to simply return mDefaultRT then scaling would fail.
+ */
+ CD3D11_TEXTURE2D_DESC desc(DXGI_FORMAT_B8G8R8A8_UNORM, size.width,
+ size.height, 1, 1, D3D11_BIND_SHADER_RESOURCE);
+
+ HRESULT hr =
+ mDevice->CreateTexture2D(&desc, nullptr, getter_AddRefs(rtTexture));
+ if (FAILED(hr)) {
+ return nullptr;
+ }
+
+ mWindowRTCopy = MakeRefPtr<CompositingRenderTargetD3D11>(
+ rtTexture, IntPoint(0, 0), DXGI_FORMAT_B8G8R8A8_UNORM);
+ mWindowRTCopy->SetSize(size);
+ } else {
+ rtTexture = mWindowRTCopy->GetD3D11Texture();
+ }
+
+ const RefPtr<ID3D11Texture2D> sourceTexture = mDefaultRT->GetD3D11Texture();
+ mContext->CopyResource(rtTexture, sourceTexture);
+
+ return RefPtr<CompositingRenderTarget>(
+ static_cast<CompositingRenderTarget*>(mWindowRTCopy))
+ .forget();
+}
+
+bool CompositorD3D11::ReadbackRenderTarget(CompositingRenderTarget* aSource,
+ AsyncReadbackBuffer* aDest) {
+ RefPtr<CompositingRenderTargetD3D11> srcTexture =
+ static_cast<CompositingRenderTargetD3D11*>(aSource);
+ RefPtr<AsyncReadbackBufferD3D11> destBuffer =
+ static_cast<AsyncReadbackBufferD3D11*>(aDest);
+
+ mContext->CopyResource(destBuffer->GetTexture(),
+ srcTexture->GetD3D11Texture());
+
+ return true;
+}
+
+already_AddRefed<AsyncReadbackBuffer>
+CompositorD3D11::CreateAsyncReadbackBuffer(const gfx::IntSize& aSize) {
+ RefPtr<ID3D11Texture2D> texture;
+
+ CD3D11_TEXTURE2D_DESC desc(DXGI_FORMAT_B8G8R8A8_UNORM, aSize.width,
+ aSize.height, 1, 1, 0, D3D11_USAGE_STAGING,
+ D3D11_CPU_ACCESS_READ);
+
+ HRESULT hr =
+ mDevice->CreateTexture2D(&desc, nullptr, getter_AddRefs(texture));
+
+ if (FAILED(hr)) {
+ HandleError(hr);
+ return nullptr;
+ }
+
+ return MakeAndAddRef<AsyncReadbackBufferD3D11>(mContext, texture, aSize);
+}
+
+bool CompositorD3D11::BlitRenderTarget(CompositingRenderTarget* aSource,
+ const gfx::IntSize& aSourceSize,
+ const gfx::IntSize& aDestSize) {
+ RefPtr<CompositingRenderTargetD3D11> source =
+ static_cast<CompositingRenderTargetD3D11*>(aSource);
+
+ RefPtr<TexturedEffect> texturedEffect = CreateTexturedEffect(
+ SurfaceFormat::B8G8R8A8, source, SamplingFilter::LINEAR, true);
+ texturedEffect->mTextureCoords =
+ Rect(0, 0, Float(aSourceSize.width) / Float(source->GetSize().width),
+ Float(aSourceSize.height) / Float(source->GetSize().height));
+
+ EffectChain effect;
+ effect.mPrimaryEffect = texturedEffect;
+
+ const Float scaleX = Float(aDestSize.width) / Float(aSourceSize.width);
+ const Float scaleY = Float(aDestSize.height) / (aSourceSize.height);
+ const Matrix4x4 transform = Matrix4x4::Scaling(scaleX, scaleY, 1.0f);
+
+ const Rect sourceRect(0, 0, aSourceSize.width, aSourceSize.height);
+
+ DrawQuad(sourceRect, IntRect(0, 0, aDestSize.width, aDestSize.height), effect,
+ 1.0f, transform, sourceRect);
+
+ return true;
+}
+
+void CompositorD3D11::SetRenderTarget(CompositingRenderTarget* aRenderTarget) {
+ MOZ_ASSERT(aRenderTarget);
+ CompositingRenderTargetD3D11* newRT =
+ static_cast<CompositingRenderTargetD3D11*>(aRenderTarget);
+ if (mCurrentRT != newRT) {
+ mCurrentRT = newRT;
+ mCurrentRT->BindRenderTarget(mContext);
+ }
+
+ if (newRT->HasComplexProjection()) {
+ gfx::Matrix4x4 projection;
+ bool depthEnable;
+ float zNear, zFar;
+ newRT->GetProjection(projection, depthEnable, zNear, zFar);
+ PrepareViewport(newRT->GetSize(), projection, zNear, zFar);
+ } else {
+ PrepareViewport(newRT->GetSize());
+ }
+}
+
+ID3D11PixelShader* CompositorD3D11::GetPSForEffect(Effect* aEffect) {
+ switch (aEffect->mType) {
+ case EffectTypes::RGB: {
+ SurfaceFormat format =
+ static_cast<TexturedEffect*>(aEffect)->mTexture->GetFormat();
+ return (format == SurfaceFormat::B8G8R8A8 ||
+ format == SurfaceFormat::R8G8B8A8)
+ ? mAttachments->mRGBAShader
+ : mAttachments->mRGBShader;
+ }
+ case EffectTypes::NV12:
+ return mAttachments->mNV12Shader;
+ case EffectTypes::YCBCR:
+ return mAttachments->mYCbCrShader;
+ default:
+ NS_WARNING("No shader to load");
+ return nullptr;
+ }
+}
+
+void CompositorD3D11::ClearRect(const gfx::Rect& aRect) {
+ if (aRect.IsEmpty()) {
+ return;
+ }
+
+ mContext->OMSetBlendState(mAttachments->mDisabledBlendState, sBlendFactor,
+ 0xFFFFFFFF);
+
+ Matrix4x4 identity;
+ memcpy(&mVSConstants.layerTransform, &identity._11, 64);
+
+ mVSConstants.layerQuad = aRect;
+ mVSConstants.renderTargetOffset[0] = 0;
+ mVSConstants.renderTargetOffset[1] = 0;
+ mPSConstants.layerOpacity[0] = 1.0f;
+
+ D3D11_RECT scissor;
+ scissor.left = aRect.X();
+ scissor.right = aRect.XMost();
+ scissor.top = aRect.Y();
+ scissor.bottom = aRect.YMost();
+ mContext->RSSetScissorRects(1, &scissor);
+ mContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP);
+ mContext->VSSetShader(mAttachments->mVSQuadShader, nullptr, 0);
+
+ mContext->PSSetShader(mAttachments->mSolidColorShader, nullptr, 0);
+ mPSConstants.layerColor[0] = 0;
+ mPSConstants.layerColor[1] = 0;
+ mPSConstants.layerColor[2] = 0;
+ mPSConstants.layerColor[3] = 0;
+
+ if (!UpdateConstantBuffers()) {
+ NS_WARNING("Failed to update shader constant buffers");
+ return;
+ }
+
+ mContext->Draw(4, 0);
+
+ // Restore the default blend state.
+ mContext->OMSetBlendState(mAttachments->mPremulBlendState, sBlendFactor,
+ 0xFFFFFFFF);
+}
+
+void CompositorD3D11::PrepareStaticVertexBuffer() {
+ mContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP);
+ mContext->IASetInputLayout(mAttachments->mInputLayout);
+ SetVertexBuffer<Vertex>(mAttachments->mVertexBuffer);
+}
+
+void CompositorD3D11::Draw(const gfx::Rect& aRect,
+ const gfx::Rect* aTexCoords) {
+ Rect layerRects[4] = {aRect};
+ Rect textureRects[4] = {};
+ size_t rects = 1;
+
+ if (aTexCoords) {
+ rects = DecomposeIntoNoRepeatRects(aRect, *aTexCoords, &layerRects,
+ &textureRects);
+ }
+
+ for (size_t i = 0; i < rects; i++) {
+ mVSConstants.layerQuad = layerRects[i];
+ mVSConstants.textureCoords = textureRects[i];
+
+ if (!UpdateConstantBuffers()) {
+ NS_WARNING("Failed to update shader constant buffers");
+ break;
+ }
+
+ mContext->Draw(4, 0);
+ }
+}
+
+void CompositorD3D11::DrawQuad(const gfx::Rect& aRect,
+ const gfx::IntRect& aClipRect,
+ const EffectChain& aEffectChain,
+ gfx::Float aOpacity,
+ const gfx::Matrix4x4& aTransform,
+ const gfx::Rect& aVisibleRect) {
+ if (mCurrentClip.IsEmpty()) {
+ return;
+ }
+
+ MOZ_ASSERT(mCurrentRT, "No render target");
+
+ memcpy(&mVSConstants.layerTransform, &aTransform._11, 64);
+ IntPoint origin = mCurrentRT->GetOrigin();
+ mVSConstants.renderTargetOffset[0] = origin.x;
+ mVSConstants.renderTargetOffset[1] = origin.y;
+
+ mPSConstants.layerOpacity[0] = aOpacity;
+
+ D3D11_RECT scissor;
+
+ IntRect clipRect(aClipRect.X(), aClipRect.Y(), aClipRect.Width(),
+ aClipRect.Height());
+ if (mCurrentRT == mDefaultRT) {
+ clipRect = clipRect.Intersect(mCurrentClip);
+ }
+
+ if (clipRect.IsEmpty()) {
+ return;
+ }
+
+ scissor.left = clipRect.X();
+ scissor.right = clipRect.XMost();
+ scissor.top = clipRect.Y();
+ scissor.bottom = clipRect.YMost();
+
+ bool restoreBlendMode = false;
+
+ mContext->RSSetScissorRects(1, &scissor);
+
+ RefPtr<ID3D11VertexShader> vertexShader = mAttachments->mVSQuadShader;
+
+ RefPtr<ID3D11PixelShader> pixelShader =
+ GetPSForEffect(aEffectChain.mPrimaryEffect);
+
+ mContext->VSSetShader(vertexShader, nullptr, 0);
+ mContext->PSSetShader(pixelShader, nullptr, 0);
+
+ const Rect* pTexCoordRect = nullptr;
+
+ switch (aEffectChain.mPrimaryEffect->mType) {
+ case EffectTypes::RGB: {
+ TexturedEffect* texturedEffect =
+ static_cast<TexturedEffect*>(aEffectChain.mPrimaryEffect.get());
+
+ pTexCoordRect = &texturedEffect->mTextureCoords;
+
+ TextureSourceD3D11* source = texturedEffect->mTexture->AsSourceD3D11();
+
+ if (!source) {
+ NS_WARNING("Missing texture source!");
+ return;
+ }
+
+ ID3D11ShaderResourceView* srView = source->GetShaderResourceView();
+ mContext->PSSetShaderResources(TexSlot::RGB, 1, &srView);
+
+ if (texturedEffect->mPremultipliedCopy) {
+ MOZ_RELEASE_ASSERT(texturedEffect->mPremultiplied);
+ mContext->OMSetBlendState(mAttachments->mPremulCopyState, sBlendFactor,
+ 0xFFFFFFFF);
+ restoreBlendMode = true;
+ } else if (!texturedEffect->mPremultiplied) {
+ mContext->OMSetBlendState(mAttachments->mNonPremulBlendState,
+ sBlendFactor, 0xFFFFFFFF);
+ restoreBlendMode = true;
+ }
+
+ SetSamplerForSamplingFilter(texturedEffect->mSamplingFilter);
+ } break;
+ case EffectTypes::NV12: {
+ EffectNV12* effectNV12 =
+ static_cast<EffectNV12*>(aEffectChain.mPrimaryEffect.get());
+
+ pTexCoordRect = &effectNV12->mTextureCoords;
+
+ TextureSourceD3D11* source = effectNV12->mTexture->AsSourceD3D11();
+ if (!source) {
+ NS_WARNING("Missing texture source!");
+ return;
+ }
+
+ RefPtr<ID3D11Texture2D> texture = source->GetD3D11Texture();
+ if (!texture) {
+ NS_WARNING("No texture found in texture source!");
+ }
+
+ D3D11_TEXTURE2D_DESC sourceDesc;
+ texture->GetDesc(&sourceDesc);
+ MOZ_DIAGNOSTIC_ASSERT(sourceDesc.Format == DXGI_FORMAT_NV12 ||
+ sourceDesc.Format == DXGI_FORMAT_P010 ||
+ sourceDesc.Format == DXGI_FORMAT_P016);
+
+ // Might want to cache these for efficiency.
+ RefPtr<ID3D11ShaderResourceView> srViewY;
+ RefPtr<ID3D11ShaderResourceView> srViewCbCr;
+ D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc =
+ CD3D11_SHADER_RESOURCE_VIEW_DESC(D3D11_SRV_DIMENSION_TEXTURE2D,
+ sourceDesc.Format == DXGI_FORMAT_NV12
+ ? DXGI_FORMAT_R8_UNORM
+ : DXGI_FORMAT_R16_UNORM);
+ mDevice->CreateShaderResourceView(texture, &srvDesc,
+ getter_AddRefs(srViewY));
+ srvDesc.Format = sourceDesc.Format == DXGI_FORMAT_NV12
+ ? DXGI_FORMAT_R8G8_UNORM
+ : DXGI_FORMAT_R16G16_UNORM;
+ mDevice->CreateShaderResourceView(texture, &srvDesc,
+ getter_AddRefs(srViewCbCr));
+
+ ID3D11ShaderResourceView* views[] = {srViewY, srViewCbCr};
+ mContext->PSSetShaderResources(TexSlot::Y, 2, views);
+
+ const float* yuvToRgb =
+ gfxUtils::YuvToRgbMatrix4x3RowMajor(effectNV12->mYUVColorSpace);
+ memcpy(&mPSConstants.yuvColorMatrix, yuvToRgb,
+ sizeof(mPSConstants.yuvColorMatrix));
+ mPSConstants.vCoefficient[0] =
+ RescalingFactorForColorDepth(effectNV12->mColorDepth);
+
+ SetSamplerForSamplingFilter(effectNV12->mSamplingFilter);
+ } break;
+ case EffectTypes::YCBCR: {
+ EffectYCbCr* ycbcrEffect =
+ static_cast<EffectYCbCr*>(aEffectChain.mPrimaryEffect.get());
+
+ SetSamplerForSamplingFilter(SamplingFilter::LINEAR);
+
+ pTexCoordRect = &ycbcrEffect->mTextureCoords;
+
+ const int Y = 0, Cb = 1, Cr = 2;
+ TextureSource* source = ycbcrEffect->mTexture;
+
+ if (!source) {
+ NS_WARNING("No texture to composite");
+ return;
+ }
+
+ if (!source->GetSubSource(Y) || !source->GetSubSource(Cb) ||
+ !source->GetSubSource(Cr)) {
+ // This can happen if we failed to upload the textures, most likely
+ // because of unsupported dimensions (we don't tile YCbCr textures).
+ return;
+ }
+
+ const float* yuvToRgb =
+ gfxUtils::YuvToRgbMatrix4x3RowMajor(ycbcrEffect->mYUVColorSpace);
+ memcpy(&mPSConstants.yuvColorMatrix, yuvToRgb,
+ sizeof(mPSConstants.yuvColorMatrix));
+
+ // Adjust range according to the bit depth.
+ mPSConstants.vCoefficient[0] =
+ RescalingFactorForColorDepth(ycbcrEffect->mColorDepth);
+
+ TextureSourceD3D11* sourceY = source->GetSubSource(Y)->AsSourceD3D11();
+ TextureSourceD3D11* sourceCb = source->GetSubSource(Cb)->AsSourceD3D11();
+ TextureSourceD3D11* sourceCr = source->GetSubSource(Cr)->AsSourceD3D11();
+
+ ID3D11ShaderResourceView* srViews[3] = {
+ sourceY->GetShaderResourceView(), sourceCb->GetShaderResourceView(),
+ sourceCr->GetShaderResourceView()};
+ mContext->PSSetShaderResources(TexSlot::Y, 3, srViews);
+ } break;
+ default:
+ NS_WARNING("Unknown shader type");
+ return;
+ }
+
+ Draw(aRect, pTexCoordRect);
+
+ if (restoreBlendMode) {
+ mContext->OMSetBlendState(mAttachments->mPremulBlendState, sBlendFactor,
+ 0xFFFFFFFF);
+ }
+}
+
+Maybe<IntRect> CompositorD3D11::BeginFrameForWindow(
+ const nsIntRegion& aInvalidRegion, const Maybe<IntRect>& aClipRect,
+ const IntRect& aRenderBounds, const nsIntRegion& aOpaqueRegion) {
+ MOZ_RELEASE_ASSERT(!mTarget, "mTarget not cleared properly");
+ return BeginFrame(aInvalidRegion, aClipRect, aRenderBounds, aOpaqueRegion);
+}
+
+Maybe<IntRect> CompositorD3D11::BeginFrame(const nsIntRegion& aInvalidRegion,
+ const Maybe<IntRect>& aClipRect,
+ const IntRect& aRenderBounds,
+ const nsIntRegion& aOpaqueRegion) {
+ // Don't composite if we are minimised. Other than for the sake of efficency,
+ // this is important because resizing our buffers when mimised will fail and
+ // cause a crash when we're restored.
+ NS_ASSERTION(mHwnd, "Couldn't find an HWND when initialising?");
+ if (mWidget->IsHidden()) {
+ // We are not going to render, and not going to call EndFrame so we have to
+ // read-unlock our textures to prevent them from accumulating.
+ return Nothing();
+ }
+
+ if (mDevice->GetDeviceRemovedReason() != S_OK) {
+ if (!mAttachments->IsDeviceReset()) {
+ gfxCriticalNote << "GFX: D3D11 skip BeginFrame with device-removed.";
+ mAttachments->SetDeviceReset();
+ }
+ return Nothing();
+ }
+
+ LayoutDeviceIntSize oldSize = mSize;
+
+ EnsureSize();
+
+ IntRect rect = IntRect(IntPoint(0, 0), mSize.ToUnknownSize());
+ // Sometimes the invalid region is larger than we want to draw.
+ nsIntRegion invalidRegionSafe;
+
+ if (mSize != oldSize) {
+ invalidRegionSafe = rect;
+ } else {
+ invalidRegionSafe.And(aInvalidRegion, rect);
+ }
+
+ IntRect invalidRect = invalidRegionSafe.GetBounds();
+
+ IntRect clipRect = invalidRect;
+ if (aClipRect) {
+ clipRect.IntersectRect(clipRect, *aClipRect);
+ }
+
+ if (clipRect.IsEmpty()) {
+ CancelFrame();
+ return Nothing();
+ }
+
+ PrepareStaticVertexBuffer();
+
+ mBackBufferInvalid.Or(mBackBufferInvalid, invalidRegionSafe);
+ if (mIsDoubleBuffered) {
+ mFrontBufferInvalid.Or(mFrontBufferInvalid, invalidRegionSafe);
+ }
+
+ // We have to call UpdateRenderTarget after we've determined the invalid regi
+ // Failed to create a render target or the view.
+ if (!UpdateRenderTarget() || !mDefaultRT || !mDefaultRT->mRTView ||
+ mSize.width <= 0 || mSize.height <= 0) {
+ return Nothing();
+ }
+
+ mCurrentClip = mBackBufferInvalid.GetBounds();
+
+ mContext->RSSetState(mAttachments->mRasterizerState);
+
+ SetRenderTarget(mDefaultRT);
+
+ IntRegion regionToClear(mCurrentClip);
+ regionToClear.Sub(regionToClear, aOpaqueRegion);
+
+ ClearRect(Rect(regionToClear.GetBounds()));
+
+ mContext->OMSetBlendState(mAttachments->mPremulBlendState, sBlendFactor,
+ 0xFFFFFFFF);
+
+ if (mAttachments->mSyncObject) {
+ if (!mAttachments->mSyncObject->Synchronize()) {
+ // It's timeout here. Since the timeout is related to the driver-removed,
+ // skip this frame.
+ return Nothing();
+ }
+ }
+
+ return Some(rect);
+}
+
+void CompositorD3D11::EndFrame() {
+ if (!profiler_feature_active(ProfilerFeature::Screenshots) && mWindowRTCopy) {
+ mWindowRTCopy = nullptr;
+ }
+
+ if (!mDefaultRT) {
+ Compositor::EndFrame();
+ mTarget = nullptr;
+ return;
+ }
+
+ if (XRE_IsParentProcess() && mDevice->GetDeviceRemovedReason() != S_OK) {
+ gfxCriticalNote << "GFX: D3D11 skip EndFrame with device-removed.";
+ Compositor::EndFrame();
+ mTarget = nullptr;
+ mCurrentRT = nullptr;
+ return;
+ }
+
+ LayoutDeviceIntSize oldSize = mSize;
+ EnsureSize();
+ if (mSize.width <= 0 || mSize.height <= 0) {
+ Compositor::EndFrame();
+ mTarget = nullptr;
+ return;
+ }
+
+ RefPtr<ID3D11Query> query;
+ if (mRecycledQuery) {
+ query = mRecycledQuery.forget();
+ } else {
+ CD3D11_QUERY_DESC desc(D3D11_QUERY_EVENT);
+ mDevice->CreateQuery(&desc, getter_AddRefs(query));
+ }
+ if (query) {
+ mContext->End(query);
+ }
+
+ if (oldSize == mSize) {
+ Present();
+ if (StaticPrefs::gfx_compositor_clearstate()) {
+ mContext->ClearState();
+ }
+ }
+
+ // Block until the previous frame's work has been completed.
+ if (mQuery) {
+ BOOL result;
+ WaitForFrameGPUQuery(mDevice, mContext, mQuery, &result);
+ // Store the query for recycling
+ mRecycledQuery = mQuery;
+ }
+ // Store the query for this frame so we can flush it next time.
+ mQuery = query;
+
+ Compositor::EndFrame();
+ mTarget = nullptr;
+ mCurrentRT = nullptr;
+}
+
+void CompositorD3D11::Present() {
+ UINT presentInterval = 0;
+
+ bool isWARP = DeviceManagerDx::Get()->IsWARP();
+ if (isWARP) {
+ // When we're using WARP we cannot present immediately as it causes us
+ // to tear when rendering. When not using WARP it appears the DWM takes
+ // care of tearing for us.
+ presentInterval = 1;
+ }
+
+ // This must be called before present so our back buffer has the validated
+ // window content.
+ if (mTarget) {
+ PaintToTarget();
+ }
+
+ RefPtr<IDXGISwapChain1> chain;
+ HRESULT hr =
+ mSwapChain->QueryInterface((IDXGISwapChain1**)getter_AddRefs(chain));
+
+ RefPtr<IDXGIKeyedMutex> mutex;
+ if (mUseMutexOnPresent && mAttachments->mSyncObject) {
+ SyncObjectD3D11Host* d3dSyncObj =
+ (SyncObjectD3D11Host*)mAttachments->mSyncObject.get();
+ mutex = d3dSyncObj->GetKeyedMutex();
+ MOZ_ASSERT(mutex);
+ }
+
+ if (SUCCEEDED(hr) && mAllowPartialPresents) {
+ DXGI_PRESENT_PARAMETERS params;
+ PodZero(&params);
+ params.DirtyRectsCount = mBackBufferInvalid.GetNumRects();
+ StackArray<RECT, 4> rects(params.DirtyRectsCount);
+
+ uint32_t i = 0;
+ for (auto iter = mBackBufferInvalid.RectIter(); !iter.Done(); iter.Next()) {
+ const IntRect& r = iter.Get();
+ rects[i].left = r.X();
+ rects[i].top = r.Y();
+ rects[i].bottom = r.YMost();
+ rects[i].right = r.XMost();
+ i++;
+ }
+
+ params.pDirtyRects = params.DirtyRectsCount ? rects.data() : nullptr;
+
+ if (mutex) {
+ hr = mutex->AcquireSync(0, 2000);
+ NS_ENSURE_TRUE_VOID(SUCCEEDED(hr));
+ }
+
+ chain->Present1(
+ presentInterval,
+ mDisableSequenceForNextFrame ? DXGI_PRESENT_DO_NOT_SEQUENCE : 0,
+ &params);
+
+ if (mutex) {
+ mutex->ReleaseSync(0);
+ }
+ } else {
+ if (mutex) {
+ hr = mutex->AcquireSync(0, 2000);
+ NS_ENSURE_TRUE_VOID(SUCCEEDED(hr));
+ }
+
+ hr = mSwapChain->Present(
+ 0, mDisableSequenceForNextFrame ? DXGI_PRESENT_DO_NOT_SEQUENCE : 0);
+
+ if (mutex) {
+ mutex->ReleaseSync(0);
+ }
+
+ if (FAILED(hr)) {
+ gfxCriticalNote << "D3D11 swap chain preset failed " << hexa(hr);
+ HandleError(hr);
+ }
+ }
+
+ if (mIsDoubleBuffered) {
+ mBackBufferInvalid = mFrontBufferInvalid;
+ mFrontBufferInvalid.SetEmpty();
+ } else {
+ mBackBufferInvalid.SetEmpty();
+ }
+
+ mDisableSequenceForNextFrame = false;
+}
+
+void CompositorD3D11::CancelFrame(bool aNeedFlush) {
+ // Flush the context, otherwise the driver might hold some resources alive
+ // until the next flush or present.
+ if (aNeedFlush) {
+ mContext->Flush();
+ }
+}
+
+void CompositorD3D11::PrepareViewport(const gfx::IntSize& aSize) {
+ // This view matrix translates coordinates from 0..width and 0..height to
+ // -1..1 on the X axis, and -1..1 on the Y axis (flips the Y coordinate)
+ Matrix viewMatrix = Matrix::Translation(-1.0, 1.0);
+ viewMatrix.PreScale(2.0f / float(aSize.width), 2.0f / float(aSize.height));
+ viewMatrix.PreScale(1.0f, -1.0f);
+
+ Matrix4x4 projection = Matrix4x4::From2D(viewMatrix);
+ projection._33 = 0.0f;
+
+ PrepareViewport(aSize, projection, 0.0f, 1.0f);
+}
+
+void CompositorD3D11::PrepareViewport(const gfx::IntSize& aSize,
+ const gfx::Matrix4x4& aProjection,
+ float aZNear, float aZFar) {
+ D3D11_VIEWPORT viewport;
+ viewport.MaxDepth = aZFar;
+ viewport.MinDepth = aZNear;
+ viewport.Width = aSize.width;
+ viewport.Height = aSize.height;
+ viewport.TopLeftX = 0;
+ viewport.TopLeftY = 0;
+
+ mContext->RSSetViewports(1, &viewport);
+
+ memcpy(&mVSConstants.projection, &aProjection._11,
+ sizeof(mVSConstants.projection));
+}
+
+void CompositorD3D11::EnsureSize() { mSize = mWidget->GetClientSize(); }
+
+bool CompositorD3D11::VerifyBufferSize() {
+ mWidget->AsWindows()->UpdateCompositorWndSizeIfNecessary();
+
+ DXGI_SWAP_CHAIN_DESC swapDesc;
+ HRESULT hr;
+
+ hr = mSwapChain->GetDesc(&swapDesc);
+ if (FAILED(hr)) {
+ gfxCriticalError() << "Failed to get the description " << hexa(hr) << ", "
+ << mSize << ", " << (int)mVerifyBuffersFailed;
+ HandleError(hr);
+ return false;
+ }
+
+ if (((static_cast<int32_t>(swapDesc.BufferDesc.Width) == mSize.width &&
+ static_cast<int32_t>(swapDesc.BufferDesc.Height) == mSize.height) ||
+ mSize.width <= 0 || mSize.height <= 0) &&
+ !mVerifyBuffersFailed) {
+ return true;
+ }
+
+ ID3D11RenderTargetView* view = nullptr;
+ mContext->OMSetRenderTargets(1, &view, nullptr);
+
+ if (mDefaultRT) {
+ RefPtr<ID3D11RenderTargetView> rtView = mDefaultRT->mRTView;
+ RefPtr<ID3D11ShaderResourceView> srView = mDefaultRT->mSRV;
+
+ // Make sure the texture, which belongs to the swapchain, is destroyed
+ // before resizing the swapchain.
+ if (mCurrentRT == mDefaultRT) {
+ mCurrentRT = nullptr;
+ }
+
+ MOZ_ASSERT(mDefaultRT->hasOneRef());
+ mDefaultRT = nullptr;
+
+ RefPtr<ID3D11Resource> resource;
+ rtView->GetResource(getter_AddRefs(resource));
+
+ ULONG newRefCnt = rtView.forget().take()->Release();
+
+ if (newRefCnt > 0) {
+ gfxCriticalError() << "mRTView not destroyed on final release! RefCnt: "
+ << newRefCnt;
+ }
+
+ if (srView) {
+ newRefCnt = srView.forget().take()->Release();
+
+ if (newRefCnt > 0) {
+ gfxCriticalError() << "mSRV not destroyed on final release! RefCnt: "
+ << newRefCnt;
+ }
+ }
+
+ newRefCnt = resource.forget().take()->Release();
+
+ if (newRefCnt > 0) {
+ gfxCriticalError()
+ << "Unexpecting lingering references to backbuffer! RefCnt: "
+ << newRefCnt;
+ }
+ }
+
+ hr = mSwapChain->ResizeBuffers(0, mSize.width, mSize.height,
+ DXGI_FORMAT_B8G8R8A8_UNORM, 0);
+
+ mVerifyBuffersFailed = FAILED(hr);
+ if (mVerifyBuffersFailed) {
+ gfxCriticalNote << "D3D11 swap resize buffers failed " << hexa(hr) << " on "
+ << mSize;
+ HandleError(hr);
+ mBufferSize = LayoutDeviceIntSize();
+ } else {
+ mBufferSize = mSize;
+ }
+
+ mBackBufferInvalid = mFrontBufferInvalid =
+ IntRect(0, 0, mSize.width, mSize.height);
+
+ return !mVerifyBuffersFailed;
+}
+
+bool CompositorD3D11::UpdateRenderTarget() {
+ HRESULT hr;
+
+ RefPtr<ID3D11Texture2D> backBuf;
+
+ if (!VerifyBufferSize()) {
+ gfxCriticalNote << "Failed VerifyBufferSize in UpdateRenderTarget "
+ << mSize;
+ return false;
+ }
+
+ if (mSize.width <= 0 || mSize.height <= 0) {
+ gfxCriticalNote << "Invalid size in UpdateRenderTarget " << mSize << ", "
+ << (int)mVerifyBuffersFailed;
+ return false;
+ }
+
+ hr = mSwapChain->GetBuffer(0, __uuidof(ID3D11Texture2D),
+ (void**)backBuf.StartAssignment());
+ if (hr == DXGI_ERROR_INVALID_CALL) {
+ // This happens on some GPUs/drivers when there's a TDR.
+ if (mDevice->GetDeviceRemovedReason() != S_OK) {
+ gfxCriticalError() << "GetBuffer returned invalid call! " << mSize << ", "
+ << (int)mVerifyBuffersFailed;
+ return false;
+ }
+ }
+
+ if (FAILED(hr)) {
+ gfxCriticalNote << "Failed in UpdateRenderTarget " << hexa(hr) << ", "
+ << mSize << ", " << (int)mVerifyBuffersFailed;
+ HandleError(hr);
+ return false;
+ }
+
+ IntRegion validFront;
+ validFront.Sub(mBackBufferInvalid, mFrontBufferInvalid);
+
+ if (mIsDoubleBuffered && !validFront.IsEmpty()) {
+ RefPtr<ID3D11Texture2D> frontBuf;
+ hr = mSwapChain->GetBuffer(1, __uuidof(ID3D11Texture2D),
+ (void**)frontBuf.StartAssignment());
+
+ if (SUCCEEDED(hr)) {
+ for (auto iter = validFront.RectIter(); !iter.Done(); iter.Next()) {
+ const IntRect& rect = iter.Get();
+
+ D3D11_BOX box;
+ box.back = 1;
+ box.front = 0;
+ box.left = rect.X();
+ box.right = rect.XMost();
+ box.top = rect.Y();
+ box.bottom = rect.YMost();
+ mContext->CopySubresourceRegion(backBuf, 0, rect.X(), rect.Y(), 0,
+ frontBuf, 0, &box);
+ }
+ mBackBufferInvalid = mFrontBufferInvalid;
+ }
+ }
+
+ mDefaultRT = new CompositingRenderTargetD3D11(backBuf, IntPoint(0, 0));
+ mDefaultRT->SetSize(mSize.ToUnknownSize());
+
+ return true;
+}
+
+bool CompositorD3D11::UpdateConstantBuffers() {
+ HRESULT hr;
+ D3D11_MAPPED_SUBRESOURCE resource;
+ resource.pData = nullptr;
+
+ hr = mContext->Map(mAttachments->mVSConstantBuffer, 0,
+ D3D11_MAP_WRITE_DISCARD, 0, &resource);
+ if (FAILED(hr) || !resource.pData) {
+ gfxCriticalError() << "Failed to map VSConstantBuffer. Result: " << hexa(hr)
+ << ", " << (int)mVerifyBuffersFailed;
+ HandleError(hr);
+ return false;
+ }
+ *(VertexShaderConstants*)resource.pData = mVSConstants;
+ mContext->Unmap(mAttachments->mVSConstantBuffer, 0);
+ resource.pData = nullptr;
+
+ hr = mContext->Map(mAttachments->mPSConstantBuffer, 0,
+ D3D11_MAP_WRITE_DISCARD, 0, &resource);
+ if (FAILED(hr) || !resource.pData) {
+ gfxCriticalError() << "Failed to map PSConstantBuffer. Result: " << hexa(hr)
+ << ", " << (int)mVerifyBuffersFailed;
+ HandleError(hr);
+ return false;
+ }
+ *(PixelShaderConstants*)resource.pData = mPSConstants;
+ mContext->Unmap(mAttachments->mPSConstantBuffer, 0);
+
+ ID3D11Buffer* buffer = mAttachments->mVSConstantBuffer;
+
+ mContext->VSSetConstantBuffers(0, 1, &buffer);
+
+ buffer = mAttachments->mPSConstantBuffer;
+ mContext->PSSetConstantBuffers(0, 1, &buffer);
+ return true;
+}
+
+void CompositorD3D11::SetSamplerForSamplingFilter(
+ SamplingFilter aSamplingFilter) {
+ ID3D11SamplerState* sampler;
+ switch (aSamplingFilter) {
+ case SamplingFilter::POINT:
+ sampler = mAttachments->mPointSamplerState;
+ break;
+ case SamplingFilter::LINEAR:
+ default:
+ sampler = mAttachments->mLinearSamplerState;
+ break;
+ }
+
+ mContext->PSSetSamplers(0, 1, &sampler);
+}
+
+void CompositorD3D11::PaintToTarget() {
+ RefPtr<ID3D11Texture2D> backBuf;
+ HRESULT hr;
+
+ hr = mSwapChain->GetBuffer(0, __uuidof(ID3D11Texture2D),
+ (void**)backBuf.StartAssignment());
+ if (FAILED(hr)) {
+ gfxCriticalErrorOnce(gfxCriticalError::DefaultOptions(false))
+ << "Failed in PaintToTarget 1";
+ HandleError(hr);
+ return;
+ }
+
+ D3D11_TEXTURE2D_DESC bbDesc;
+ backBuf->GetDesc(&bbDesc);
+
+ CD3D11_TEXTURE2D_DESC softDesc(bbDesc.Format, bbDesc.Width, bbDesc.Height);
+ softDesc.MipLevels = 1;
+ softDesc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
+ softDesc.Usage = D3D11_USAGE_STAGING;
+ softDesc.BindFlags = 0;
+
+ RefPtr<ID3D11Texture2D> readTexture;
+
+ hr =
+ mDevice->CreateTexture2D(&softDesc, nullptr, getter_AddRefs(readTexture));
+ if (FAILED(hr)) {
+ gfxCriticalErrorOnce(gfxCriticalError::DefaultOptions(false))
+ << "Failed in PaintToTarget 2";
+ HandleError(hr);
+ return;
+ }
+ mContext->CopyResource(readTexture, backBuf);
+
+ D3D11_MAPPED_SUBRESOURCE map;
+ hr = mContext->Map(readTexture, 0, D3D11_MAP_READ, 0, &map);
+ if (FAILED(hr)) {
+ gfxCriticalErrorOnce(gfxCriticalError::DefaultOptions(false))
+ << "Failed in PaintToTarget 3";
+ HandleError(hr);
+ return;
+ }
+ RefPtr<DataSourceSurface> sourceSurface =
+ Factory::CreateWrappingDataSourceSurface(
+ (uint8_t*)map.pData, map.RowPitch,
+ IntSize(bbDesc.Width, bbDesc.Height), SurfaceFormat::B8G8R8A8);
+ mTarget->CopySurface(sourceSurface,
+ IntRect(0, 0, bbDesc.Width, bbDesc.Height),
+ IntPoint(-mTargetBounds.X(), -mTargetBounds.Y()));
+
+ mTarget->Flush();
+ mContext->Unmap(readTexture, 0);
+}
+
+bool CompositorD3D11::Failed(HRESULT hr, const char* aContext) {
+ if (SUCCEEDED(hr)) return false;
+
+ gfxCriticalNote << "[D3D11] " << aContext << " failed: " << hexa(hr) << ", "
+ << (int)mVerifyBuffersFailed;
+ return true;
+}
+
+SyncObjectHost* CompositorD3D11::GetSyncObject() {
+ if (mAttachments) {
+ return mAttachments->mSyncObject;
+ }
+ return nullptr;
+}
+
+void CompositorD3D11::HandleError(HRESULT hr, Severity aSeverity) {
+ if (SUCCEEDED(hr)) {
+ return;
+ }
+
+ if (aSeverity == Critical) {
+ MOZ_CRASH("GFX: Unrecoverable D3D11 error");
+ }
+
+ if (mDevice && DeviceManagerDx::Get()->GetCompositorDevice() != mDevice) {
+ gfxCriticalNote << "Out of sync D3D11 devices in HandleError, "
+ << (int)mVerifyBuffersFailed;
+ }
+
+ HRESULT hrOnReset = S_OK;
+ bool deviceRemoved = hr == DXGI_ERROR_DEVICE_REMOVED;
+
+ if (deviceRemoved && mDevice) {
+ hrOnReset = mDevice->GetDeviceRemovedReason();
+ } else if (hr == DXGI_ERROR_INVALID_CALL && mDevice) {
+ hrOnReset = mDevice->GetDeviceRemovedReason();
+ if (hrOnReset != S_OK) {
+ deviceRemoved = true;
+ }
+ }
+
+ // Device reset may not be an error on our side, but can mess things up so
+ // it's useful to see it in the reports.
+ gfxCriticalError(CriticalLog::DefaultOptions(!deviceRemoved))
+ << (deviceRemoved ? "[CompositorD3D11] device removed with error code: "
+ : "[CompositorD3D11] error code: ")
+ << hexa(hr) << ", " << hexa(hrOnReset) << ", "
+ << (int)mVerifyBuffersFailed;
+
+ // Crash if we are making invalid calls outside of device removal
+ if (hr == DXGI_ERROR_INVALID_CALL) {
+ gfxDevCrash(deviceRemoved ? LogReason::D3D11InvalidCallDeviceRemoved
+ : LogReason::D3D11InvalidCall)
+ << "Invalid D3D11 api call";
+ }
+
+ if (aSeverity == Recoverable) {
+ NS_WARNING("Encountered a recoverable D3D11 error");
+ }
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/d3d11/CompositorD3D11.h b/gfx/layers/d3d11/CompositorD3D11.h
new file mode 100644
index 0000000000..d8cdd2d2f8
--- /dev/null
+++ b/gfx/layers/d3d11/CompositorD3D11.h
@@ -0,0 +1,227 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_GFX_COMPOSITORD3D11_H
+#define MOZILLA_GFX_COMPOSITORD3D11_H
+
+#include "mozilla/gfx/2D.h"
+#include "gfx2DGlue.h"
+#include "mozilla/layers/Compositor.h"
+#include "TextureD3D11.h"
+#include <d3d11.h>
+#include <dxgi1_2.h>
+#include "ShaderDefinitionsD3D11.h"
+
+class nsWidget;
+
+namespace mozilla {
+namespace layers {
+
+#define LOGD3D11(param)
+
+class DeviceAttachmentsD3D11;
+
+class CompositorD3D11 : public Compositor {
+ public:
+ explicit CompositorD3D11(widget::CompositorWidget* aWidget);
+ virtual ~CompositorD3D11();
+
+ CompositorD3D11* AsCompositorD3D11() override { return this; }
+
+ bool Initialize(nsCString* const out_failureReason) override;
+
+ already_AddRefed<DataTextureSource> CreateDataTextureSource(
+ TextureFlags aFlags = TextureFlags::NO_FLAGS) override;
+
+ int32_t GetMaxTextureSize() const final;
+
+ already_AddRefed<CompositingRenderTarget> CreateRenderTarget(
+ const gfx::IntRect& aRect, SurfaceInitMode aInit) override;
+
+ void SetRenderTarget(CompositingRenderTarget* aSurface) override;
+ already_AddRefed<CompositingRenderTarget> GetCurrentRenderTarget()
+ const override {
+ return do_AddRef(mCurrentRT);
+ }
+ already_AddRefed<CompositingRenderTarget> GetWindowRenderTarget()
+ const override;
+
+ bool ReadbackRenderTarget(CompositingRenderTarget* aSource,
+ AsyncReadbackBuffer* aDest) override;
+ already_AddRefed<AsyncReadbackBuffer> CreateAsyncReadbackBuffer(
+ const gfx::IntSize& aSize) override;
+
+ bool BlitRenderTarget(CompositingRenderTarget* aSource,
+ const gfx::IntSize& aSourceSize,
+ const gfx::IntSize& aDestSize) override;
+
+ void SetDestinationSurfaceSize(const gfx::IntSize& aSize) override {}
+
+ void ClearRect(const gfx::Rect& aRect);
+
+ void DrawQuad(const gfx::Rect& aRect, const gfx::IntRect& aClipRect,
+ const EffectChain& aEffectChain, gfx::Float aOpacity,
+ const gfx::Matrix4x4& aTransform,
+ const gfx::Rect& aVisibleRect) override;
+
+ /**
+ * Start a new frame.
+ */
+ Maybe<gfx::IntRect> BeginFrameForWindow(
+ const nsIntRegion& aInvalidRegion, const Maybe<gfx::IntRect>& aClipRect,
+ const gfx::IntRect& aRenderBounds,
+ const nsIntRegion& aOpaqueRegion) override;
+
+ /**
+ * Flush the current frame to the screen.
+ */
+ void EndFrame() override;
+
+ void CancelFrame(bool aNeedFlush = true) override;
+
+ /**
+ * Setup the viewport and projection matrix for rendering
+ * to a window of the given dimensions.
+ */
+ virtual void PrepareViewport(const gfx::IntSize& aSize);
+ virtual void PrepareViewport(const gfx::IntSize& aSize,
+ const gfx::Matrix4x4& aProjection, float aZNear,
+ float aZFar);
+
+#ifdef MOZ_DUMP_PAINTING
+ const char* Name() const override { return "Direct3D 11"; }
+#endif
+
+ // For TextureSourceProvider.
+ ID3D11Device* GetD3D11Device() const override { return mDevice; }
+
+ ID3D11Device* GetDevice() { return mDevice; }
+
+ ID3D11DeviceContext* GetDC() { return mContext; }
+
+ virtual void RequestAllowFrameRecording(bool aWillRecord) override {
+ mAllowFrameRecording = aWillRecord;
+ }
+
+ void Readback(gfx::DrawTarget* aDrawTarget) {
+ mTarget = aDrawTarget;
+ mTargetBounds = gfx::IntRect();
+ PaintToTarget();
+ mTarget = nullptr;
+ }
+
+ SyncObjectHost* GetSyncObject();
+
+ private:
+ enum Severity {
+ Recoverable,
+ DebugAssert,
+ Critical,
+ };
+
+ void HandleError(HRESULT hr, Severity aSeverity = DebugAssert);
+
+ // Same as Failed(), except the severity is critical (with no abort) and
+ // a string prefix must be provided.
+ bool Failed(HRESULT hr, const char* aContext);
+
+ // ensure mSize is up to date with respect to mWidget
+ void EnsureSize();
+ bool VerifyBufferSize();
+ bool UpdateRenderTarget();
+ bool UpdateConstantBuffers();
+ void SetSamplerForSamplingFilter(gfx::SamplingFilter aSamplingFilter);
+
+ ID3D11PixelShader* GetPSForEffect(Effect* aEffect);
+ Maybe<gfx::IntRect> BeginFrame(const nsIntRegion& aInvalidRegion,
+ const Maybe<gfx::IntRect>& aClipRect,
+ const gfx::IntRect& aRenderBounds,
+ const nsIntRegion& aOpaqueRegion);
+ void PaintToTarget();
+ RefPtr<ID3D11Texture2D> CreateTexture(const gfx::IntRect& aRect,
+ const CompositingRenderTarget* aSource,
+ const gfx::IntPoint& aSourcePoint);
+
+ void PrepareStaticVertexBuffer();
+
+ void Draw(const gfx::Rect& aGeometry, const gfx::Rect* aTexCoords);
+
+ void Present();
+
+ template <typename VertexType>
+ void SetVertexBuffer(ID3D11Buffer* aBuffer);
+
+ /**
+ * Whether or not the recorder should be recording frames.
+ *
+ * When this returns true, the CompositorD3D11 will allocate and return window
+ * render targets from |GetWindowRenderTarget|, which otherwise returns
+ * nullptr.
+ *
+ * This will be true when either we are recording a profile with screenshots
+ * enabled or the |LayerManagerComposite| has requested us to record frames
+ * for the |CompositionRecorder|.
+ */
+ bool ShouldAllowFrameRecording() const;
+
+ // The DrawTarget from BeginFrameForTarget, which EndFrame needs to copy the
+ // window contents into.
+ // Only non-null between BeginFrameForTarget and EndFrame.
+ RefPtr<gfx::DrawTarget> mTarget;
+ gfx::IntRect mTargetBounds;
+
+ RefPtr<ID3D11DeviceContext> mContext;
+ RefPtr<ID3D11Device> mDevice;
+ RefPtr<IDXGISwapChain> mSwapChain;
+ RefPtr<CompositingRenderTargetD3D11> mDefaultRT;
+ RefPtr<CompositingRenderTargetD3D11> mCurrentRT;
+ mutable RefPtr<CompositingRenderTargetD3D11> mWindowRTCopy;
+
+ RefPtr<ID3D11Query> mQuery;
+ RefPtr<ID3D11Query> mRecycledQuery;
+
+ RefPtr<DeviceAttachmentsD3D11> mAttachments;
+
+ LayoutDeviceIntSize mSize;
+
+ // The size that we passed to ResizeBuffers to set
+ // the swapchain buffer size.
+ LayoutDeviceIntSize mBufferSize;
+
+ HWND mHwnd;
+
+ D3D_FEATURE_LEVEL mFeatureLevel;
+
+ VertexShaderConstants mVSConstants;
+ PixelShaderConstants mPSConstants;
+ bool mDisableSequenceForNextFrame;
+ bool mAllowPartialPresents;
+ bool mIsDoubleBuffered;
+
+ gfx::IntRegion mFrontBufferInvalid;
+ gfx::IntRegion mBackBufferInvalid;
+ // This is the clip rect applied to the default DrawTarget (i.e. the window)
+ gfx::IntRect mCurrentClip;
+
+ bool mVerifyBuffersFailed;
+ bool mUseMutexOnPresent;
+ bool mAllowFrameRecording;
+};
+
+namespace TexSlot {
+static const int RGB = 0;
+static const int Y = 1;
+static const int Cb = 2;
+static const int Cr = 3;
+static const int RGBWhite = 4;
+static const int Mask = 5;
+static const int Backdrop = 6;
+} // namespace TexSlot
+
+} // namespace layers
+} // namespace mozilla
+
+#endif
diff --git a/gfx/layers/d3d11/CompositorD3D11.hlsl b/gfx/layers/d3d11/CompositorD3D11.hlsl
new file mode 100644
index 0000000000..4e13011782
--- /dev/null
+++ b/gfx/layers/d3d11/CompositorD3D11.hlsl
@@ -0,0 +1,186 @@
+/* -*- Mode: C++; tab-width: 20; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "BlendShaderConstants.h"
+
+typedef float4 rect;
+
+float4x4 mLayerTransform : register(vs, c0);
+float4x4 mProjection : register(vs, c4);
+float4 vRenderTargetOffset : register(vs, c8);
+rect vTextureCoords : register(vs, c9);
+rect vLayerQuad : register(vs, c10);
+
+float4 fLayerColor : register(ps, c0);
+float fLayerOpacity : register(ps, c1);
+
+// x = layer type
+// y = mask type
+// z = blend op
+// w = is premultiplied
+
+float fCoefficient : register(ps, c3);
+
+row_major float3x3 mYuvColorMatrix : register(ps, c4);
+
+sampler sSampler : register(ps, s0);
+
+// The mix-blend mega shader uses all variables, so we have to make sure they
+// are assigned fixed slots.
+Texture2D tRGB : register(ps, t0);
+Texture2D tY : register(ps, t1);
+Texture2D tCb : register(ps, t2);
+Texture2D tCr : register(ps, t3);
+
+struct VS_INPUT {
+ float2 vPosition : POSITION;
+};
+
+struct VS_TEX_INPUT {
+ float2 vPosition : POSITION;
+ float2 vTexCoords : TEXCOORD0;
+};
+
+struct VS_OUTPUT {
+ float4 vPosition : SV_Position;
+ float2 vTexCoords : TEXCOORD0;
+};
+
+struct PS_OUTPUT {
+ float4 vSrc;
+ float4 vAlpha;
+};
+
+float2 TexCoords(const float2 aPosition)
+{
+ float2 result;
+ const float2 size = vTextureCoords.zw;
+ result.x = vTextureCoords.x + aPosition.x * size.x;
+ result.y = vTextureCoords.y + aPosition.y * size.y;
+
+ return result;
+}
+
+SamplerState LayerTextureSamplerLinear
+{
+ Filter = MIN_MAG_MIP_LINEAR;
+ AddressU = Clamp;
+ AddressV = Clamp;
+};
+
+float4 TransformedPosition(float2 aInPosition)
+{
+ // the current vertex's position on the quad
+ // [x,y,0,1] is mandated by the CSS Transforms spec as the point value to transform
+ float4 position = float4(0, 0, 0, 1);
+
+ // We use 4 component floats to uniquely describe a rectangle, by the structure
+ // of x, y, width, height. This allows us to easily generate the 4 corners
+ // of any rectangle from the 4 corners of the 0,0-1,1 quad that we use as the
+ // stream source for our LayerQuad vertex shader. We do this by doing:
+ // Xout = x + Xin * width
+ // Yout = y + Yin * height
+ float2 size = vLayerQuad.zw;
+ position.x = vLayerQuad.x + aInPosition.x * size.x;
+ position.y = vLayerQuad.y + aInPosition.y * size.y;
+
+ position = mul(mLayerTransform, position);
+
+ return position;
+}
+
+float4 VertexPosition(float4 aTransformedPosition)
+{
+ float4 result;
+ result.w = aTransformedPosition.w;
+ result.xyz = aTransformedPosition.xyz / aTransformedPosition.w;
+ result -= vRenderTargetOffset;
+ result.xyz *= result.w;
+
+ result = mul(mProjection, result);
+
+ return result;
+}
+
+VS_OUTPUT LayerQuadVS(const VS_INPUT aVertex)
+{
+ VS_OUTPUT outp;
+ float4 position = TransformedPosition(aVertex.vPosition);
+
+ outp.vPosition = VertexPosition(position);
+ outp.vTexCoords = TexCoords(aVertex.vPosition.xy);
+
+ return outp;
+}
+
+/* From Rec601:
+[R] [1.1643835616438356, 0.0, 1.5960267857142858] [ Y - 16]
+[G] = [1.1643835616438358, -0.3917622900949137, -0.8129676472377708] x [Cb - 128]
+[B] [1.1643835616438356, 2.017232142857143, 8.862867620416422e-17] [Cr - 128]
+
+For [0,1] instead of [0,255], and to 5 places:
+[R] [1.16438, 0.00000, 1.59603] [ Y - 0.06275]
+[G] = [1.16438, -0.39176, -0.81297] x [Cb - 0.50196]
+[B] [1.16438, 2.01723, 0.00000] [Cr - 0.50196]
+
+From Rec709:
+[R] [1.1643835616438356, 4.2781193979771426e-17, 1.7927410714285714] [ Y - 16]
+[G] = [1.1643835616438358, -0.21324861427372963, -0.532909328559444] x [Cb - 128]
+[B] [1.1643835616438356, 2.1124017857142854, 0.0] [Cr - 128]
+
+For [0,1] instead of [0,255], and to 5 places:
+[R] [1.16438, 0.00000, 1.79274] [ Y - 0.06275]
+[G] = [1.16438, -0.21325, -0.53291] x [Cb - 0.50196]
+[B] [1.16438, 2.11240, 0.00000] [Cr - 0.50196]
+*/
+float4 CalculateYCbCrColor(const float2 aTexCoords)
+{
+ float3 yuv = float3(
+ tY.Sample(sSampler, aTexCoords).r,
+ tCb.Sample(sSampler, aTexCoords).r,
+ tCr.Sample(sSampler, aTexCoords).r);
+ yuv = yuv * fCoefficient - float3(0.06275, 0.50196, 0.50196);
+
+ return float4(mul(mYuvColorMatrix, yuv), 1.0);
+}
+
+float4 CalculateNV12Color(const float2 aTexCoords)
+{
+ float3 yuv = float3(
+ tY.Sample(sSampler, aTexCoords).r,
+ tCb.Sample(sSampler, aTexCoords).r,
+ tCb.Sample(sSampler, aTexCoords).g);
+ yuv = yuv * fCoefficient - float3(0.06275, 0.50196, 0.50196);
+
+ return float4(mul(mYuvColorMatrix, yuv), 1.0);
+}
+
+float4 SolidColorShader(const VS_OUTPUT aVertex) : SV_Target
+{
+ return fLayerColor;
+}
+
+float4 RGBAShader(const VS_OUTPUT aVertex) : SV_Target
+{
+ return tRGB.Sample(sSampler, aVertex.vTexCoords) * fLayerOpacity;
+}
+
+float4 RGBShader(const VS_OUTPUT aVertex) : SV_Target
+{
+ float4 result;
+ result = tRGB.Sample(sSampler, aVertex.vTexCoords) * fLayerOpacity;
+ result.a = fLayerOpacity;
+ return result;
+}
+
+float4 YCbCrShader(const VS_OUTPUT aVertex) : SV_Target
+{
+ return CalculateYCbCrColor(aVertex.vTexCoords) * fLayerOpacity;
+}
+
+float4 NV12Shader(const VS_OUTPUT aVertex) : SV_Target
+{
+ return CalculateNV12Color(aVertex.vTexCoords) * fLayerOpacity;
+}
diff --git a/gfx/layers/d3d11/DeviceAttachmentsD3D11.cpp b/gfx/layers/d3d11/DeviceAttachmentsD3D11.cpp
new file mode 100644
index 0000000000..3a80e30cc4
--- /dev/null
+++ b/gfx/layers/d3d11/DeviceAttachmentsD3D11.cpp
@@ -0,0 +1,249 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "DeviceAttachmentsD3D11.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/gfx/Logging.h"
+#include "mozilla/layers/Compositor.h"
+#include "CompositorD3D11Shaders.h"
+#include "ShaderDefinitionsD3D11.h"
+
+namespace mozilla {
+namespace layers {
+
+using namespace gfx;
+
+DeviceAttachmentsD3D11::DeviceAttachmentsD3D11(ID3D11Device* device)
+ : mDevice(device),
+ mContinueInit(true),
+ mInitialized(false),
+ mDeviceReset(false) {}
+
+DeviceAttachmentsD3D11::~DeviceAttachmentsD3D11() {}
+
+/* static */
+RefPtr<DeviceAttachmentsD3D11> DeviceAttachmentsD3D11::Create(
+ ID3D11Device* aDevice) {
+ // We don't return null even if the attachments object even if it fails to
+ // initialize, so the compositor can grab the failure ID.
+ RefPtr<DeviceAttachmentsD3D11> attachments =
+ new DeviceAttachmentsD3D11(aDevice);
+ attachments->Initialize();
+ return attachments.forget();
+}
+
+bool DeviceAttachmentsD3D11::Initialize() {
+ D3D11_INPUT_ELEMENT_DESC layout[] = {
+ {"POSITION", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 0,
+ D3D11_INPUT_PER_VERTEX_DATA, 0},
+ };
+
+ HRESULT hr;
+ hr = mDevice->CreateInputLayout(
+ layout, sizeof(layout) / sizeof(D3D11_INPUT_ELEMENT_DESC), LayerQuadVS,
+ sizeof(LayerQuadVS), getter_AddRefs(mInputLayout));
+
+ if (Failed(hr, "CreateInputLayout")) {
+ mInitFailureId = "FEATURE_FAILURE_D3D11_INPUT_LAYOUT";
+ return false;
+ }
+
+ Vertex vertices[] = {{{0.0, 0.0}}, {{1.0, 0.0}}, {{0.0, 1.0}}, {{1.0, 1.0}}};
+ CD3D11_BUFFER_DESC bufferDesc(sizeof(vertices), D3D11_BIND_VERTEX_BUFFER);
+ D3D11_SUBRESOURCE_DATA data;
+ data.pSysMem = (void*)vertices;
+
+ hr = mDevice->CreateBuffer(&bufferDesc, &data, getter_AddRefs(mVertexBuffer));
+ if (Failed(hr, "create vertex buffer")) {
+ mInitFailureId = "FEATURE_FAILURE_D3D11_VERTEX_BUFFER";
+ return false;
+ }
+ if (!CreateShaders()) {
+ mInitFailureId = "FEATURE_FAILURE_D3D11_CREATE_SHADERS";
+ return false;
+ }
+
+ CD3D11_BUFFER_DESC cBufferDesc(sizeof(VertexShaderConstants),
+ D3D11_BIND_CONSTANT_BUFFER,
+ D3D11_USAGE_DYNAMIC, D3D11_CPU_ACCESS_WRITE);
+
+ hr = mDevice->CreateBuffer(&cBufferDesc, nullptr,
+ getter_AddRefs(mVSConstantBuffer));
+ if (Failed(hr, "create vs buffer")) {
+ mInitFailureId = "FEATURE_FAILURE_D3D11_VS_BUFFER";
+ return false;
+ }
+
+ cBufferDesc.ByteWidth = sizeof(PixelShaderConstants);
+ hr = mDevice->CreateBuffer(&cBufferDesc, nullptr,
+ getter_AddRefs(mPSConstantBuffer));
+ if (Failed(hr, "create ps buffer")) {
+ mInitFailureId = "FEATURE_FAILURE_D3D11_PS_BUFFER";
+ return false;
+ }
+
+ CD3D11_RASTERIZER_DESC rastDesc(D3D11_DEFAULT);
+ rastDesc.CullMode = D3D11_CULL_NONE;
+ rastDesc.ScissorEnable = TRUE;
+
+ hr = mDevice->CreateRasterizerState(&rastDesc,
+ getter_AddRefs(mRasterizerState));
+ if (Failed(hr, "create rasterizer")) {
+ mInitFailureId = "FEATURE_FAILURE_D3D11_RASTERIZER";
+ return false;
+ }
+
+ CD3D11_SAMPLER_DESC samplerDesc(D3D11_DEFAULT);
+ hr = mDevice->CreateSamplerState(&samplerDesc,
+ getter_AddRefs(mLinearSamplerState));
+ if (Failed(hr, "create linear sampler")) {
+ mInitFailureId = "FEATURE_FAILURE_D3D11_LINEAR_SAMPLER";
+ return false;
+ }
+
+ samplerDesc.Filter = D3D11_FILTER_MIN_MAG_MIP_POINT;
+ hr = mDevice->CreateSamplerState(&samplerDesc,
+ getter_AddRefs(mPointSamplerState));
+ if (Failed(hr, "create point sampler")) {
+ mInitFailureId = "FEATURE_FAILURE_D3D11_POINT_SAMPLER";
+ return false;
+ }
+
+ CD3D11_BLEND_DESC blendDesc(D3D11_DEFAULT);
+ D3D11_RENDER_TARGET_BLEND_DESC rtBlendPremul = {TRUE,
+ D3D11_BLEND_ONE,
+ D3D11_BLEND_INV_SRC_ALPHA,
+ D3D11_BLEND_OP_ADD,
+ D3D11_BLEND_ONE,
+ D3D11_BLEND_INV_SRC_ALPHA,
+ D3D11_BLEND_OP_ADD,
+ D3D11_COLOR_WRITE_ENABLE_ALL};
+ blendDesc.RenderTarget[0] = rtBlendPremul;
+ hr = mDevice->CreateBlendState(&blendDesc, getter_AddRefs(mPremulBlendState));
+ if (Failed(hr, "create pm blender")) {
+ mInitFailureId = "FEATURE_FAILURE_D3D11_PM_BLENDER";
+ return false;
+ }
+
+ D3D11_RENDER_TARGET_BLEND_DESC rtCopyPremul = {TRUE,
+ D3D11_BLEND_ONE,
+ D3D11_BLEND_ZERO,
+ D3D11_BLEND_OP_ADD,
+ D3D11_BLEND_ONE,
+ D3D11_BLEND_ZERO,
+ D3D11_BLEND_OP_ADD,
+ D3D11_COLOR_WRITE_ENABLE_ALL};
+ blendDesc.RenderTarget[0] = rtCopyPremul;
+ hr = mDevice->CreateBlendState(&blendDesc, getter_AddRefs(mPremulCopyState));
+ if (Failed(hr, "create pm copy blender")) {
+ mInitFailureId = "FEATURE_FAILURE_D3D11_PM_COPY_BLENDER";
+ return false;
+ }
+
+ D3D11_RENDER_TARGET_BLEND_DESC rtBlendNonPremul = {
+ TRUE,
+ D3D11_BLEND_SRC_ALPHA,
+ D3D11_BLEND_INV_SRC_ALPHA,
+ D3D11_BLEND_OP_ADD,
+ D3D11_BLEND_ONE,
+ D3D11_BLEND_INV_SRC_ALPHA,
+ D3D11_BLEND_OP_ADD,
+ D3D11_COLOR_WRITE_ENABLE_ALL};
+ blendDesc.RenderTarget[0] = rtBlendNonPremul;
+ hr = mDevice->CreateBlendState(&blendDesc,
+ getter_AddRefs(mNonPremulBlendState));
+ if (Failed(hr, "create npm blender")) {
+ mInitFailureId = "FEATURE_FAILURE_D3D11_NPM_BLENDER";
+ return false;
+ }
+
+ D3D11_RENDER_TARGET_BLEND_DESC rtBlendDisabled = {
+ FALSE,
+ D3D11_BLEND_SRC_ALPHA,
+ D3D11_BLEND_INV_SRC_ALPHA,
+ D3D11_BLEND_OP_ADD,
+ D3D11_BLEND_ONE,
+ D3D11_BLEND_INV_SRC_ALPHA,
+ D3D11_BLEND_OP_ADD,
+ D3D11_COLOR_WRITE_ENABLE_ALL};
+ blendDesc.RenderTarget[0] = rtBlendDisabled;
+ hr = mDevice->CreateBlendState(&blendDesc,
+ getter_AddRefs(mDisabledBlendState));
+ if (Failed(hr, "create null blender")) {
+ mInitFailureId = "FEATURE_FAILURE_D3D11_NULL_BLENDER";
+ return false;
+ }
+
+ if (!InitSyncObject()) {
+ mInitFailureId = "FEATURE_FAILURE_D3D11_OBJ_SYNC";
+ return false;
+ }
+
+ mInitialized = true;
+ return true;
+}
+
+bool DeviceAttachmentsD3D11::InitSyncObject() {
+ // Sync object is not supported on WARP.
+ if (DeviceManagerDx::Get()->IsWARP()) {
+ return true;
+ }
+
+ MOZ_ASSERT(!mSyncObject);
+ MOZ_ASSERT(mDevice);
+
+ mSyncObject = SyncObjectHost::CreateSyncObjectHost(mDevice);
+ MOZ_ASSERT(mSyncObject);
+
+ return mSyncObject->Init();
+}
+
+bool DeviceAttachmentsD3D11::CreateShaders() {
+ InitVertexShader(sLayerQuadVS, mVSQuadShader);
+
+ InitPixelShader(sSolidColorShader, mSolidColorShader);
+ InitPixelShader(sRGBShader, mRGBShader);
+ InitPixelShader(sRGBAShader, mRGBAShader);
+ InitPixelShader(sYCbCrShader, mYCbCrShader);
+ InitPixelShader(sNV12Shader, mNV12Shader);
+ return mContinueInit;
+}
+
+void DeviceAttachmentsD3D11::InitVertexShader(const ShaderBytes& aShader,
+ ID3D11VertexShader** aOut) {
+ if (!mContinueInit) {
+ return;
+ }
+ if (Failed(mDevice->CreateVertexShader(aShader.mData, aShader.mLength,
+ nullptr, aOut),
+ "create vs")) {
+ mContinueInit = false;
+ }
+}
+
+void DeviceAttachmentsD3D11::InitPixelShader(const ShaderBytes& aShader,
+ ID3D11PixelShader** aOut) {
+ if (!mContinueInit) {
+ return;
+ }
+ if (Failed(mDevice->CreatePixelShader(aShader.mData, aShader.mLength, nullptr,
+ aOut),
+ "create ps")) {
+ mContinueInit = false;
+ }
+}
+
+bool DeviceAttachmentsD3D11::Failed(HRESULT hr, const char* aContext) {
+ if (SUCCEEDED(hr)) {
+ return false;
+ }
+
+ gfxCriticalNote << "[D3D11] " << aContext << " failed: " << hexa(hr);
+ return true;
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/d3d11/DeviceAttachmentsD3D11.h b/gfx/layers/d3d11/DeviceAttachmentsD3D11.h
new file mode 100644
index 0000000000..4837dc76f1
--- /dev/null
+++ b/gfx/layers/d3d11/DeviceAttachmentsD3D11.h
@@ -0,0 +1,96 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_gfx_layers_d3d11_DeviceAttachmentsD3D11_h
+#define mozilla_gfx_layers_d3d11_DeviceAttachmentsD3D11_h
+
+#include "mozilla/EnumeratedArray.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/gfx/DeviceManagerDx.h"
+#include "mozilla/layers/CompositorTypes.h"
+#include "mozilla/layers/SyncObject.h"
+#include <d3d11.h>
+#include <dxgi1_2.h>
+
+namespace mozilla {
+namespace layers {
+
+struct ShaderBytes;
+
+class DeviceAttachmentsD3D11 final {
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(DeviceAttachmentsD3D11);
+
+ public:
+ static RefPtr<DeviceAttachmentsD3D11> Create(ID3D11Device* aDevice);
+
+ bool IsValid() const { return mInitialized; }
+ const nsCString& GetFailureId() const {
+ MOZ_ASSERT(!IsValid());
+ return mInitFailureId;
+ }
+
+ RefPtr<ID3D11InputLayout> mInputLayout;
+
+ RefPtr<ID3D11Buffer> mVertexBuffer;
+
+ RefPtr<ID3D11VertexShader> mVSQuadShader;
+
+ RefPtr<ID3D11PixelShader> mSolidColorShader;
+ RefPtr<ID3D11PixelShader> mRGBAShader;
+ RefPtr<ID3D11PixelShader> mRGBShader;
+ RefPtr<ID3D11PixelShader> mYCbCrShader;
+ RefPtr<ID3D11PixelShader> mNV12Shader;
+ RefPtr<ID3D11Buffer> mPSConstantBuffer;
+ RefPtr<ID3D11Buffer> mVSConstantBuffer;
+ RefPtr<ID3D11RasterizerState> mRasterizerState;
+ RefPtr<ID3D11SamplerState> mLinearSamplerState;
+ RefPtr<ID3D11SamplerState> mPointSamplerState;
+
+ RefPtr<ID3D11BlendState> mPremulBlendState;
+ RefPtr<ID3D11BlendState> mPremulCopyState;
+ RefPtr<ID3D11BlendState> mNonPremulBlendState;
+ RefPtr<ID3D11BlendState> mDisabledBlendState;
+
+ RefPtr<SyncObjectHost> mSyncObject;
+
+ void SetDeviceReset() { mDeviceReset = true; }
+ bool IsDeviceReset() const { return mDeviceReset; }
+
+ private:
+ explicit DeviceAttachmentsD3D11(ID3D11Device* device);
+ ~DeviceAttachmentsD3D11();
+
+ bool Initialize();
+ bool CreateShaders();
+ bool InitSyncObject();
+
+ void InitVertexShader(const ShaderBytes& aShader,
+ RefPtr<ID3D11VertexShader>& aDest) {
+ InitVertexShader(aShader, getter_AddRefs(aDest));
+ }
+ void InitPixelShader(const ShaderBytes& aShader,
+ RefPtr<ID3D11PixelShader>& aDest) {
+ InitPixelShader(aShader, getter_AddRefs(aDest));
+ }
+
+ void InitVertexShader(const ShaderBytes& aShader, ID3D11VertexShader** aOut);
+ void InitPixelShader(const ShaderBytes& aShader, ID3D11PixelShader** aOut);
+
+ bool Failed(HRESULT hr, const char* aContext);
+
+ private:
+ // Only used during initialization.
+ RefPtr<ID3D11Device> mDevice;
+ bool mContinueInit;
+ bool mInitialized;
+ bool mDeviceReset;
+ nsCString mInitFailureId;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_gfx_layers_d3d11_DeviceAttachmentsD3D11_h
diff --git a/gfx/layers/d3d11/HelpersD3D11.h b/gfx/layers/d3d11/HelpersD3D11.h
new file mode 100644
index 0000000000..ef40acd0a0
--- /dev/null
+++ b/gfx/layers/d3d11/HelpersD3D11.h
@@ -0,0 +1,67 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_gfx_layers_d3d11_HelpersD3D11_h
+#define mozilla_gfx_layers_d3d11_HelpersD3D11_h
+
+#include <d3d11.h>
+#include <array>
+#include "mozilla/Telemetry.h"
+#include "mozilla/TimeStamp.h"
+
+namespace mozilla {
+namespace layers {
+
+template <typename T>
+static inline bool WaitForGPUQuery(ID3D11Device* aDevice,
+ ID3D11DeviceContext* aContext,
+ ID3D11Query* aQuery, T* aOut) {
+ TimeStamp start = TimeStamp::Now();
+ while (aContext->GetData(aQuery, aOut, sizeof(*aOut), 0) != S_OK) {
+ if (aDevice->GetDeviceRemovedReason() != S_OK) {
+ return false;
+ }
+ if (TimeStamp::Now() - start > TimeDuration::FromSeconds(2)) {
+ return false;
+ }
+ Sleep(0);
+ }
+ return true;
+}
+
+static inline bool WaitForFrameGPUQuery(ID3D11Device* aDevice,
+ ID3D11DeviceContext* aContext,
+ ID3D11Query* aQuery, BOOL* aOut) {
+ TimeStamp start = TimeStamp::Now();
+ bool success = true;
+ while (aContext->GetData(aQuery, aOut, sizeof(*aOut), 0) != S_OK) {
+ if (aDevice->GetDeviceRemovedReason() != S_OK) {
+ return false;
+ }
+ if (TimeStamp::Now() - start > TimeDuration::FromSeconds(2)) {
+ success = false;
+ break;
+ }
+ Sleep(0);
+ }
+ Telemetry::AccumulateTimeDelta(Telemetry::GPU_WAIT_TIME_MS, start);
+ return success;
+}
+
+inline void ClearResource(ID3D11Device* const device, ID3D11Resource* const res,
+ const std::array<float, 4>& vals) {
+ RefPtr<ID3D11RenderTargetView> rtv;
+ (void)device->CreateRenderTargetView(res, nullptr, getter_AddRefs(rtv));
+
+ RefPtr<ID3D11DeviceContext> context;
+ device->GetImmediateContext(getter_AddRefs(context));
+ context->ClearRenderTargetView(rtv, vals.data());
+}
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_gfx_layers_d3d11_HelpersD3D11_h
diff --git a/gfx/layers/d3d11/ShaderDefinitionsD3D11.h b/gfx/layers/d3d11/ShaderDefinitionsD3D11.h
new file mode 100644
index 0000000000..1e15cf7987
--- /dev/null
+++ b/gfx/layers/d3d11/ShaderDefinitionsD3D11.h
@@ -0,0 +1,40 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_gfx_layers_d3d11_ShaderDefinitionsD3D11_h
+#define mozilla_gfx_layers_d3d11_ShaderDefinitionsD3D11_h
+
+#include "mozilla/gfx/Rect.h"
+
+namespace mozilla {
+namespace layers {
+
+struct VertexShaderConstants {
+ float layerTransform[4][4];
+ float projection[4][4];
+ float renderTargetOffset[4];
+ gfx::Rect textureCoords;
+ gfx::Rect layerQuad;
+ float maskTransform[4][4];
+ float backdropTransform[4][4];
+};
+
+struct PixelShaderConstants {
+ float layerColor[4];
+ float layerOpacity[4];
+ int blendConfig[4];
+ float vCoefficient[4];
+ float yuvColorMatrix[3][4];
+};
+
+struct Vertex {
+ float position[2];
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_gfx_layers_d3d11_ShaderDefinitionsD3D11_h
diff --git a/gfx/layers/d3d11/TextureD3D11.cpp b/gfx/layers/d3d11/TextureD3D11.cpp
new file mode 100644
index 0000000000..a7bcc02b8f
--- /dev/null
+++ b/gfx/layers/d3d11/TextureD3D11.cpp
@@ -0,0 +1,1962 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "TextureD3D11.h"
+
+#include "CompositorD3D11.h"
+#include "Effects.h"
+#include "MainThreadUtils.h"
+#include "gfx2DGlue.h"
+#include "gfxContext.h"
+#include "gfxWindowsPlatform.h"
+#include "mozilla/StaticPrefs_gfx.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/gfx/DataSurfaceHelpers.h"
+#include "mozilla/gfx/DeviceManagerDx.h"
+#include "mozilla/gfx/Logging.h"
+#include "mozilla/gfx/gfxVars.h"
+#include "mozilla/layers/CompositorBridgeChild.h"
+#include "mozilla/layers/D3D11TextureIMFSampleImage.h"
+#include "mozilla/layers/HelpersD3D11.h"
+#include "mozilla/webrender/RenderD3D11TextureHost.h"
+#include "mozilla/webrender/RenderThread.h"
+#include "mozilla/webrender/WebRenderAPI.h"
+
+namespace mozilla {
+
+using namespace gfx;
+
+namespace layers {
+
+static const GUID sD3D11TextureUsage = {
+ 0xd89275b0,
+ 0x6c7d,
+ 0x4038,
+ {0xb5, 0xfa, 0x4d, 0x87, 0x16, 0xd5, 0xcc, 0x4e}};
+
+/* This class gets its lifetime tied to a D3D texture
+ * and increments memory usage on construction and decrements
+ * on destruction */
+class TextureMemoryMeasurer final : public IUnknown {
+ public:
+ explicit TextureMemoryMeasurer(size_t aMemoryUsed) {
+ mMemoryUsed = aMemoryUsed;
+ gfxWindowsPlatform::sD3D11SharedTextures += mMemoryUsed;
+ mRefCnt = 0;
+ }
+ STDMETHODIMP_(ULONG) AddRef() {
+ mRefCnt++;
+ return mRefCnt;
+ }
+ STDMETHODIMP QueryInterface(REFIID riid, void** ppvObject) {
+ IUnknown* punk = nullptr;
+ if (riid == IID_IUnknown) {
+ punk = this;
+ }
+ *ppvObject = punk;
+ if (punk) {
+ punk->AddRef();
+ return S_OK;
+ } else {
+ return E_NOINTERFACE;
+ }
+ }
+
+ STDMETHODIMP_(ULONG) Release() {
+ int refCnt = --mRefCnt;
+ if (refCnt == 0) {
+ gfxWindowsPlatform::sD3D11SharedTextures -= mMemoryUsed;
+ delete this;
+ }
+ return refCnt;
+ }
+
+ private:
+ int mRefCnt;
+ int mMemoryUsed;
+
+ ~TextureMemoryMeasurer() = default;
+};
+
+static DXGI_FORMAT SurfaceFormatToDXGIFormat(gfx::SurfaceFormat aFormat) {
+ switch (aFormat) {
+ case SurfaceFormat::B8G8R8A8:
+ return DXGI_FORMAT_B8G8R8A8_UNORM;
+ case SurfaceFormat::B8G8R8X8:
+ return DXGI_FORMAT_B8G8R8A8_UNORM;
+ case SurfaceFormat::R8G8B8A8:
+ return DXGI_FORMAT_R8G8B8A8_UNORM;
+ case SurfaceFormat::R8G8B8X8:
+ return DXGI_FORMAT_R8G8B8A8_UNORM;
+ case SurfaceFormat::A8:
+ return DXGI_FORMAT_R8_UNORM;
+ case SurfaceFormat::A16:
+ return DXGI_FORMAT_R16_UNORM;
+ default:
+ MOZ_ASSERT(false, "unsupported format");
+ return DXGI_FORMAT_UNKNOWN;
+ }
+}
+
+void ReportTextureMemoryUsage(ID3D11Texture2D* aTexture, size_t aBytes) {
+ aTexture->SetPrivateDataInterface(sD3D11TextureUsage,
+ new TextureMemoryMeasurer(aBytes));
+}
+
+static uint32_t GetRequiredTilesD3D11(uint32_t aSize, uint32_t aMaxSize) {
+ uint32_t requiredTiles = aSize / aMaxSize;
+ if (aSize % aMaxSize) {
+ requiredTiles++;
+ }
+ return requiredTiles;
+}
+
+static IntRect GetTileRectD3D11(uint32_t aID, IntSize aSize,
+ uint32_t aMaxSize) {
+ uint32_t horizontalTiles = GetRequiredTilesD3D11(aSize.width, aMaxSize);
+ uint32_t verticalTiles = GetRequiredTilesD3D11(aSize.height, aMaxSize);
+
+ uint32_t verticalTile = aID / horizontalTiles;
+ uint32_t horizontalTile = aID % horizontalTiles;
+
+ return IntRect(
+ horizontalTile * aMaxSize, verticalTile * aMaxSize,
+ horizontalTile < (horizontalTiles - 1) ? aMaxSize
+ : aSize.width % aMaxSize,
+ verticalTile < (verticalTiles - 1) ? aMaxSize : aSize.height % aMaxSize);
+}
+
+AutoTextureLock::AutoTextureLock(IDXGIKeyedMutex* aMutex, HRESULT& aResult,
+ uint32_t aTimeout) {
+ mMutex = aMutex;
+ if (mMutex) {
+ mResult = mMutex->AcquireSync(0, aTimeout);
+ aResult = mResult;
+ } else {
+ aResult = E_INVALIDARG;
+ }
+}
+
+AutoTextureLock::~AutoTextureLock() {
+ if (mMutex && !FAILED(mResult) && mResult != WAIT_TIMEOUT &&
+ mResult != WAIT_ABANDONED) {
+ mMutex->ReleaseSync(0);
+ }
+}
+
+ID3D11ShaderResourceView* TextureSourceD3D11::GetShaderResourceView() {
+ MOZ_ASSERT(mTexture == GetD3D11Texture(),
+ "You need to override GetShaderResourceView if you're overriding "
+ "GetD3D11Texture!");
+
+ if (!mSRV && mTexture) {
+ RefPtr<ID3D11Device> device;
+ mTexture->GetDevice(getter_AddRefs(device));
+
+ // see comment in CompositingRenderTargetD3D11 constructor
+ CD3D11_SHADER_RESOURCE_VIEW_DESC srvDesc(D3D11_SRV_DIMENSION_TEXTURE2D,
+ mFormatOverride);
+ D3D11_SHADER_RESOURCE_VIEW_DESC* desc =
+ mFormatOverride == DXGI_FORMAT_UNKNOWN ? nullptr : &srvDesc;
+
+ HRESULT hr =
+ device->CreateShaderResourceView(mTexture, desc, getter_AddRefs(mSRV));
+ if (FAILED(hr)) {
+ gfxCriticalNote << "[D3D11] TextureSourceD3D11:GetShaderResourceView "
+ "CreateSRV failure "
+ << gfx::hexa(hr);
+ return nullptr;
+ }
+ }
+ return mSRV;
+}
+
+DataTextureSourceD3D11::DataTextureSourceD3D11(ID3D11Device* aDevice,
+ SurfaceFormat aFormat,
+ TextureFlags aFlags)
+ : mDevice(aDevice),
+ mFormat(aFormat),
+ mFlags(aFlags),
+ mCurrentTile(0),
+ mIsTiled(false),
+ mIterating(false),
+ mAllowTextureUploads(true) {}
+
+DataTextureSourceD3D11::DataTextureSourceD3D11(ID3D11Device* aDevice,
+ SurfaceFormat aFormat,
+ ID3D11Texture2D* aTexture)
+ : mDevice(aDevice),
+ mFormat(aFormat),
+ mFlags(TextureFlags::NO_FLAGS),
+ mCurrentTile(0),
+ mIsTiled(false),
+ mIterating(false),
+ mAllowTextureUploads(false) {
+ mTexture = aTexture;
+ D3D11_TEXTURE2D_DESC desc;
+ aTexture->GetDesc(&desc);
+
+ mSize = IntSize(desc.Width, desc.Height);
+}
+
+DataTextureSourceD3D11::DataTextureSourceD3D11(gfx::SurfaceFormat aFormat,
+ TextureSourceProvider* aProvider,
+ ID3D11Texture2D* aTexture)
+ : DataTextureSourceD3D11(aProvider->GetD3D11Device(), aFormat, aTexture) {}
+
+DataTextureSourceD3D11::DataTextureSourceD3D11(gfx::SurfaceFormat aFormat,
+ TextureSourceProvider* aProvider,
+ TextureFlags aFlags)
+ : DataTextureSourceD3D11(aProvider->GetD3D11Device(), aFormat, aFlags) {}
+
+DataTextureSourceD3D11::~DataTextureSourceD3D11() {}
+
+enum class SerializeWithMoz2D : bool { No, Yes };
+
+template <typename T> // ID3D10Texture2D or ID3D11Texture2D
+static bool LockD3DTexture(
+ T* aTexture, SerializeWithMoz2D aSerialize = SerializeWithMoz2D::No) {
+ MOZ_ASSERT(aTexture);
+ RefPtr<IDXGIKeyedMutex> mutex;
+ aTexture->QueryInterface((IDXGIKeyedMutex**)getter_AddRefs(mutex));
+ // Textures created by the DXVA decoders don't have a mutex for
+ // synchronization
+ if (mutex) {
+ HRESULT hr;
+ if (aSerialize == SerializeWithMoz2D::Yes) {
+ AutoSerializeWithMoz2D serializeWithMoz2D(BackendType::DIRECT2D1_1);
+ hr = mutex->AcquireSync(0, 10000);
+ } else {
+ hr = mutex->AcquireSync(0, 10000);
+ }
+ if (hr == WAIT_TIMEOUT) {
+ RefPtr<ID3D11Device> device;
+ aTexture->GetDevice(getter_AddRefs(device));
+ if (!device) {
+ gfxCriticalNote << "GFX: D3D11 lock mutex timeout - no device returned";
+ } else if (device->GetDeviceRemovedReason() != S_OK) {
+ gfxCriticalNote << "GFX: D3D11 lock mutex timeout - device removed";
+ } else {
+ gfxDevCrash(LogReason::D3DLockTimeout)
+ << "D3D lock mutex timeout - device not removed";
+ }
+ } else if (hr == WAIT_ABANDONED) {
+ gfxCriticalNote << "GFX: D3D11 lock mutex abandoned";
+ }
+
+ if (FAILED(hr)) {
+ NS_WARNING("Failed to lock the texture");
+ return false;
+ }
+ }
+ return true;
+}
+
+template <typename T>
+static bool HasKeyedMutex(T* aTexture) {
+ MOZ_ASSERT(aTexture);
+ RefPtr<IDXGIKeyedMutex> mutex;
+ aTexture->QueryInterface((IDXGIKeyedMutex**)getter_AddRefs(mutex));
+ return !!mutex;
+}
+
+template <typename T> // ID3D10Texture2D or ID3D11Texture2D
+static void UnlockD3DTexture(
+ T* aTexture, SerializeWithMoz2D aSerialize = SerializeWithMoz2D::No) {
+ MOZ_ASSERT(aTexture);
+ RefPtr<IDXGIKeyedMutex> mutex;
+ aTexture->QueryInterface((IDXGIKeyedMutex**)getter_AddRefs(mutex));
+ if (mutex) {
+ HRESULT hr;
+ if (aSerialize == SerializeWithMoz2D::Yes) {
+ AutoSerializeWithMoz2D serializeWithMoz2D(BackendType::DIRECT2D1_1);
+ hr = mutex->ReleaseSync(0);
+ } else {
+ hr = mutex->ReleaseSync(0);
+ }
+ if (FAILED(hr)) {
+ NS_WARNING("Failed to unlock the texture");
+ }
+ }
+}
+
+D3D11TextureData::D3D11TextureData(ID3D11Texture2D* aTexture,
+ uint32_t aArrayIndex, gfx::IntSize aSize,
+ gfx::SurfaceFormat aFormat,
+ TextureAllocationFlags aFlags)
+ : mSize(aSize),
+ mFormat(aFormat),
+ mNeedsClear(aFlags & ALLOC_CLEAR_BUFFER),
+ mHasSynchronization(HasKeyedMutex(aTexture)),
+ mTexture(aTexture),
+ mArrayIndex(aArrayIndex),
+ mAllocationFlags(aFlags) {
+ MOZ_ASSERT(aTexture);
+}
+
+static void DestroyDrawTarget(RefPtr<DrawTarget>& aDT,
+ RefPtr<ID3D11Texture2D>& aTexture) {
+ // An Azure DrawTarget needs to be locked when it gets nullptr'ed as this is
+ // when it calls EndDraw. This EndDraw should not execute anything so it
+ // shouldn't -really- need the lock but the debug layer chokes on this.
+ LockD3DTexture(aTexture.get(), SerializeWithMoz2D::Yes);
+ aDT = nullptr;
+
+ // Do the serialization here, so we can hold it while destroying the texture.
+ AutoSerializeWithMoz2D serializeWithMoz2D(BackendType::DIRECT2D1_1);
+ UnlockD3DTexture(aTexture.get(), SerializeWithMoz2D::No);
+ aTexture = nullptr;
+}
+
+D3D11TextureData::~D3D11TextureData() {
+ if (mDrawTarget) {
+ DestroyDrawTarget(mDrawTarget, mTexture);
+ }
+
+ if (mGpuProcessTextureId.isSome()) {
+ auto* textureMap = GpuProcessD3D11TextureMap::Get();
+ if (textureMap) {
+ textureMap->Unregister(mGpuProcessTextureId.ref());
+ } else {
+ gfxCriticalNoteOnce << "GpuProcessD3D11TextureMap does not exist";
+ }
+ }
+}
+
+bool D3D11TextureData::Lock(OpenMode aMode) {
+ if (!LockD3DTexture(mTexture.get(), SerializeWithMoz2D::Yes)) {
+ return false;
+ }
+
+ if (NS_IsMainThread()) {
+ if (!PrepareDrawTargetInLock(aMode)) {
+ Unlock();
+ return false;
+ }
+ }
+
+ return true;
+}
+
+bool D3D11TextureData::PrepareDrawTargetInLock(OpenMode aMode) {
+ // Make sure that successful write-lock means we will have a DrawTarget to
+ // write into.
+ if (!mDrawTarget && (aMode & OpenMode::OPEN_WRITE || mNeedsClear)) {
+ mDrawTarget = BorrowDrawTarget();
+ if (!mDrawTarget) {
+ return false;
+ }
+ }
+
+ // Reset transform
+ mDrawTarget->SetTransform(Matrix());
+
+ if (mNeedsClear) {
+ mDrawTarget->ClearRect(Rect(0, 0, mSize.width, mSize.height));
+ mNeedsClear = false;
+ }
+
+ return true;
+}
+
+void D3D11TextureData::Unlock() {
+ UnlockD3DTexture(mTexture.get(), SerializeWithMoz2D::Yes);
+}
+
+void D3D11TextureData::FillInfo(TextureData::Info& aInfo) const {
+ aInfo.size = mSize;
+ aInfo.format = mFormat;
+ aInfo.supportsMoz2D = true;
+ aInfo.hasSynchronization = mHasSynchronization;
+}
+
+void D3D11TextureData::SyncWithObject(RefPtr<SyncObjectClient> aSyncObject) {
+ if (!aSyncObject || mHasSynchronization) {
+ // When we have per texture synchronization we sync using the keyed mutex.
+ return;
+ }
+
+ MOZ_ASSERT(aSyncObject->GetSyncType() == SyncObjectClient::SyncType::D3D11);
+ SyncObjectD3D11Client* sync =
+ static_cast<SyncObjectD3D11Client*>(aSyncObject.get());
+ sync->RegisterTexture(mTexture);
+}
+
+bool D3D11TextureData::SerializeSpecific(
+ SurfaceDescriptorD3D10* const aOutDesc) {
+ RefPtr<IDXGIResource> resource;
+ GetDXGIResource((IDXGIResource**)getter_AddRefs(resource));
+ if (!resource) {
+ return false;
+ }
+ HANDLE sharedHandle = 0;
+ if (mGpuProcessTextureId.isNothing()) {
+ HRESULT hr = resource->GetSharedHandle(&sharedHandle);
+ if (FAILED(hr)) {
+ LOGD3D11("Error getting shared handle for texture.");
+ return false;
+ }
+ }
+ *aOutDesc = SurfaceDescriptorD3D10((WindowsHandle)sharedHandle,
+ mGpuProcessTextureId, mArrayIndex, mFormat,
+ mSize, mColorSpace, mColorRange);
+ return true;
+}
+
+bool D3D11TextureData::Serialize(SurfaceDescriptor& aOutDescriptor) {
+ SurfaceDescriptorD3D10 desc;
+ if (!SerializeSpecific(&desc)) return false;
+
+ aOutDescriptor = std::move(desc);
+ return true;
+}
+
+void D3D11TextureData::GetSubDescriptor(
+ RemoteDecoderVideoSubDescriptor* const aOutDesc) {
+ SurfaceDescriptorD3D10 ret;
+ if (!SerializeSpecific(&ret)) return;
+
+ *aOutDesc = std::move(ret);
+}
+
+/* static */
+already_AddRefed<TextureClient> D3D11TextureData::CreateTextureClient(
+ ID3D11Texture2D* aTexture, uint32_t aIndex, gfx::IntSize aSize,
+ gfx::SurfaceFormat aFormat, gfx::ColorSpace2 aColorSpace,
+ gfx::ColorRange aColorRange, KnowsCompositor* aKnowsCompositor,
+ RefPtr<IMFSampleUsageInfo> aUsageInfo) {
+ D3D11TextureData* data = new D3D11TextureData(
+ aTexture, aIndex, aSize, aFormat,
+ TextureAllocationFlags::ALLOC_MANUAL_SYNCHRONIZATION);
+ data->mColorSpace = aColorSpace;
+ data->SetColorRange(aColorRange);
+
+ RefPtr<TextureClient> textureClient = MakeAndAddRef<TextureClient>(
+ data, TextureFlags::NO_FLAGS, aKnowsCompositor->GetTextureForwarder());
+ const auto textureId = GpuProcessD3D11TextureMap::GetNextTextureId();
+ data->SetGpuProcessTextureId(textureId);
+
+ // Register ID3D11Texture2D to GpuProcessD3D11TextureMap
+ auto* textureMap = GpuProcessD3D11TextureMap::Get();
+ if (textureMap) {
+ textureMap->Register(textureId, aTexture, aIndex, aSize, aUsageInfo);
+ } else {
+ gfxCriticalNoteOnce << "GpuProcessD3D11TextureMap does not exist";
+ }
+
+ return textureClient.forget();
+}
+
+D3D11TextureData* D3D11TextureData::Create(IntSize aSize, SurfaceFormat aFormat,
+ TextureAllocationFlags aFlags,
+ ID3D11Device* aDevice) {
+ return Create(aSize, aFormat, nullptr, aFlags, aDevice);
+}
+
+D3D11TextureData* D3D11TextureData::Create(SourceSurface* aSurface,
+ TextureAllocationFlags aFlags,
+ ID3D11Device* aDevice) {
+ return Create(aSurface->GetSize(), aSurface->GetFormat(), aSurface, aFlags,
+ aDevice);
+}
+
+D3D11TextureData* D3D11TextureData::Create(IntSize aSize, SurfaceFormat aFormat,
+ SourceSurface* aSurface,
+ TextureAllocationFlags aFlags,
+ ID3D11Device* aDevice) {
+ if (aFormat == SurfaceFormat::A8) {
+ // Currently we don't support A8 surfaces. Fallback.
+ return nullptr;
+ }
+
+ // Just grab any device. We never use the immediate context, so the devices
+ // are fine to use from any thread.
+ RefPtr<ID3D11Device> device = aDevice;
+ if (!device) {
+ device = DeviceManagerDx::Get()->GetContentDevice();
+ if (!device) {
+ return nullptr;
+ }
+ }
+
+ CD3D11_TEXTURE2D_DESC newDesc(
+ DXGI_FORMAT_B8G8R8A8_UNORM, aSize.width, aSize.height, 1, 1,
+ D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE);
+
+ if (aFormat == SurfaceFormat::NV12) {
+ newDesc.Format = DXGI_FORMAT_NV12;
+ } else if (aFormat == SurfaceFormat::P010) {
+ newDesc.Format = DXGI_FORMAT_P010;
+ } else if (aFormat == SurfaceFormat::P016) {
+ newDesc.Format = DXGI_FORMAT_P016;
+ }
+
+ newDesc.MiscFlags = D3D11_RESOURCE_MISC_SHARED;
+ if (!NS_IsMainThread()) {
+ // On the main thread we use the syncobject to handle synchronization.
+ if (!(aFlags & ALLOC_MANUAL_SYNCHRONIZATION)) {
+ newDesc.MiscFlags = D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX;
+ }
+ }
+
+ if (aSurface && newDesc.MiscFlags == D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX &&
+ !DeviceManagerDx::Get()->CanInitializeKeyedMutexTextures()) {
+ return nullptr;
+ }
+
+ D3D11_SUBRESOURCE_DATA uploadData;
+ D3D11_SUBRESOURCE_DATA* uploadDataPtr = nullptr;
+ RefPtr<DataSourceSurface> srcSurf;
+ DataSourceSurface::MappedSurface sourceMap;
+
+ if (aSurface) {
+ srcSurf = aSurface->GetDataSurface();
+
+ if (!srcSurf) {
+ gfxCriticalError()
+ << "Failed to GetDataSurface in D3D11TextureData::Create";
+ return nullptr;
+ }
+
+ if (!srcSurf->Map(DataSourceSurface::READ, &sourceMap)) {
+ gfxCriticalError()
+ << "Failed to map source surface for D3D11TextureData::Create";
+ return nullptr;
+ }
+ }
+
+ if (srcSurf && !DeviceManagerDx::Get()->HasCrashyInitData()) {
+ uploadData.pSysMem = sourceMap.mData;
+ uploadData.SysMemPitch = sourceMap.mStride;
+ uploadData.SysMemSlicePitch = 0; // unused
+
+ uploadDataPtr = &uploadData;
+ }
+
+ // See bug 1397040
+ RefPtr<ID3D10Multithread> mt;
+ device->QueryInterface((ID3D10Multithread**)getter_AddRefs(mt));
+
+ RefPtr<ID3D11Texture2D> texture11;
+
+ {
+ AutoSerializeWithMoz2D serializeWithMoz2D(BackendType::DIRECT2D1_1);
+ D3D11MTAutoEnter lock(mt.forget());
+
+ HRESULT hr = device->CreateTexture2D(&newDesc, uploadDataPtr,
+ getter_AddRefs(texture11));
+
+ if (FAILED(hr) || !texture11) {
+ gfxCriticalNote << "[D3D11] 2 CreateTexture2D failure Size: " << aSize
+ << "texture11: " << texture11
+ << " Code: " << gfx::hexa(hr);
+ return nullptr;
+ }
+
+ if (srcSurf && DeviceManagerDx::Get()->HasCrashyInitData()) {
+ D3D11_BOX box;
+ box.front = box.top = box.left = 0;
+ box.back = 1;
+ box.right = aSize.width;
+ box.bottom = aSize.height;
+ RefPtr<ID3D11DeviceContext> ctx;
+ device->GetImmediateContext(getter_AddRefs(ctx));
+ ctx->UpdateSubresource(texture11, 0, &box, sourceMap.mData,
+ sourceMap.mStride, 0);
+ }
+ }
+
+ if (srcSurf) {
+ srcSurf->Unmap();
+ }
+
+ // If we created the texture with a keyed mutex, then we expect all operations
+ // on it to be synchronized using it. If we did an initial upload using
+ // aSurface then bizarely this isn't covered, so we insert a manual
+ // lock/unlock pair to force this.
+ if (aSurface && newDesc.MiscFlags == D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX) {
+ if (!LockD3DTexture(texture11.get(), SerializeWithMoz2D::Yes)) {
+ return nullptr;
+ }
+ UnlockD3DTexture(texture11.get(), SerializeWithMoz2D::Yes);
+ }
+ texture11->SetPrivateDataInterface(
+ sD3D11TextureUsage,
+ new TextureMemoryMeasurer(newDesc.Width * newDesc.Height * 4));
+ return new D3D11TextureData(texture11, 0, aSize, aFormat, aFlags);
+}
+
+void D3D11TextureData::Deallocate(LayersIPCChannel* aAllocator) {
+ mDrawTarget = nullptr;
+ mTexture = nullptr;
+}
+
+TextureData* D3D11TextureData::CreateSimilar(
+ LayersIPCChannel* aAllocator, LayersBackend aLayersBackend,
+ TextureFlags aFlags, TextureAllocationFlags aAllocFlags) const {
+ return D3D11TextureData::Create(mSize, mFormat, aAllocFlags);
+}
+
+void D3D11TextureData::GetDXGIResource(IDXGIResource** aOutResource) {
+ mTexture->QueryInterface(aOutResource);
+}
+
+TextureFlags D3D11TextureData::GetTextureFlags() const {
+ // With WebRender, resource open happens asynchronously on RenderThread.
+ // During opening the resource on host side, TextureClient needs to be alive.
+ // With WAIT_HOST_USAGE_END, keep TextureClient alive during host side usage.
+ return TextureFlags::WAIT_HOST_USAGE_END;
+}
+
+DXGIYCbCrTextureData* DXGIYCbCrTextureData::Create(
+ IDirect3DTexture9* aTextureY, IDirect3DTexture9* aTextureCb,
+ IDirect3DTexture9* aTextureCr, HANDLE aHandleY, HANDLE aHandleCb,
+ HANDLE aHandleCr, const gfx::IntSize& aSize, const gfx::IntSize& aSizeY,
+ const gfx::IntSize& aSizeCbCr, gfx::ColorDepth aColorDepth,
+ YUVColorSpace aYUVColorSpace, gfx::ColorRange aColorRange) {
+ if (!aHandleY || !aHandleCb || !aHandleCr || !aTextureY || !aTextureCb ||
+ !aTextureCr) {
+ return nullptr;
+ }
+
+ DXGIYCbCrTextureData* texture = new DXGIYCbCrTextureData();
+ texture->mHandles[0] = aHandleY;
+ texture->mHandles[1] = aHandleCb;
+ texture->mHandles[2] = aHandleCr;
+ texture->mD3D9Textures[0] = aTextureY;
+ texture->mD3D9Textures[1] = aTextureCb;
+ texture->mD3D9Textures[2] = aTextureCr;
+ texture->mSize = aSize;
+ texture->mSizeY = aSizeY;
+ texture->mSizeCbCr = aSizeCbCr;
+ texture->mColorDepth = aColorDepth;
+ texture->mYUVColorSpace = aYUVColorSpace;
+ texture->mColorRange = aColorRange;
+
+ return texture;
+}
+
+DXGIYCbCrTextureData* DXGIYCbCrTextureData::Create(
+ ID3D11Texture2D* aTextureY, ID3D11Texture2D* aTextureCb,
+ ID3D11Texture2D* aTextureCr, const gfx::IntSize& aSize,
+ const gfx::IntSize& aSizeY, const gfx::IntSize& aSizeCbCr,
+ gfx::ColorDepth aColorDepth, YUVColorSpace aYUVColorSpace,
+ gfx::ColorRange aColorRange) {
+ if (!aTextureY || !aTextureCb || !aTextureCr) {
+ return nullptr;
+ }
+
+ aTextureY->SetPrivateDataInterface(
+ sD3D11TextureUsage,
+ new TextureMemoryMeasurer(aSizeY.width * aSizeY.height));
+ aTextureCb->SetPrivateDataInterface(
+ sD3D11TextureUsage,
+ new TextureMemoryMeasurer(aSizeCbCr.width * aSizeCbCr.height));
+ aTextureCr->SetPrivateDataInterface(
+ sD3D11TextureUsage,
+ new TextureMemoryMeasurer(aSizeCbCr.width * aSizeCbCr.height));
+
+ RefPtr<IDXGIResource> resource;
+
+ aTextureY->QueryInterface((IDXGIResource**)getter_AddRefs(resource));
+
+ HANDLE handleY;
+ HRESULT hr = resource->GetSharedHandle(&handleY);
+ if (FAILED(hr)) {
+ return nullptr;
+ }
+
+ aTextureCb->QueryInterface((IDXGIResource**)getter_AddRefs(resource));
+
+ HANDLE handleCb;
+ hr = resource->GetSharedHandle(&handleCb);
+ if (FAILED(hr)) {
+ return nullptr;
+ }
+
+ aTextureCr->QueryInterface((IDXGIResource**)getter_AddRefs(resource));
+ HANDLE handleCr;
+ hr = resource->GetSharedHandle(&handleCr);
+ if (FAILED(hr)) {
+ return nullptr;
+ }
+
+ DXGIYCbCrTextureData* texture = new DXGIYCbCrTextureData();
+ texture->mHandles[0] = handleY;
+ texture->mHandles[1] = handleCb;
+ texture->mHandles[2] = handleCr;
+ texture->mD3D11Textures[0] = aTextureY;
+ texture->mD3D11Textures[1] = aTextureCb;
+ texture->mD3D11Textures[2] = aTextureCr;
+ texture->mSize = aSize;
+ texture->mSizeY = aSizeY;
+ texture->mSizeCbCr = aSizeCbCr;
+ texture->mColorDepth = aColorDepth;
+ texture->mYUVColorSpace = aYUVColorSpace;
+ texture->mColorRange = aColorRange;
+
+ return texture;
+}
+
+void DXGIYCbCrTextureData::FillInfo(TextureData::Info& aInfo) const {
+ aInfo.size = mSize;
+ aInfo.format = gfx::SurfaceFormat::YUV;
+ aInfo.supportsMoz2D = false;
+ aInfo.hasSynchronization = false;
+}
+
+void DXGIYCbCrTextureData::SerializeSpecific(
+ SurfaceDescriptorDXGIYCbCr* const aOutDesc) {
+ *aOutDesc = SurfaceDescriptorDXGIYCbCr(
+ (WindowsHandle)mHandles[0], (WindowsHandle)mHandles[1],
+ (WindowsHandle)mHandles[2], mSize, mSizeY, mSizeCbCr, mColorDepth,
+ mYUVColorSpace, mColorRange);
+}
+
+bool DXGIYCbCrTextureData::Serialize(SurfaceDescriptor& aOutDescriptor) {
+ SurfaceDescriptorDXGIYCbCr desc;
+ SerializeSpecific(&desc);
+
+ aOutDescriptor = std::move(desc);
+ return true;
+}
+
+void DXGIYCbCrTextureData::GetSubDescriptor(
+ RemoteDecoderVideoSubDescriptor* const aOutDesc) {
+ SurfaceDescriptorDXGIYCbCr desc;
+ SerializeSpecific(&desc);
+
+ *aOutDesc = std::move(desc);
+}
+
+void DXGIYCbCrTextureData::Deallocate(LayersIPCChannel*) {
+ mD3D9Textures[0] = nullptr;
+ mD3D9Textures[1] = nullptr;
+ mD3D9Textures[2] = nullptr;
+ mD3D11Textures[0] = nullptr;
+ mD3D11Textures[1] = nullptr;
+ mD3D11Textures[2] = nullptr;
+}
+
+TextureFlags DXGIYCbCrTextureData::GetTextureFlags() const {
+ // With WebRender, resource open happens asynchronously on RenderThread.
+ // During opening the resource on host side, TextureClient needs to be alive.
+ // With WAIT_HOST_USAGE_END, keep TextureClient alive during host side usage.
+ return TextureFlags::WAIT_HOST_USAGE_END;
+}
+
+already_AddRefed<TextureHost> CreateTextureHostD3D11(
+ const SurfaceDescriptor& aDesc, ISurfaceAllocator* aDeallocator,
+ LayersBackend aBackend, TextureFlags aFlags) {
+ RefPtr<TextureHost> result;
+ switch (aDesc.type()) {
+ case SurfaceDescriptor::TSurfaceDescriptorD3D10: {
+ result =
+ new DXGITextureHostD3D11(aFlags, aDesc.get_SurfaceDescriptorD3D10());
+ break;
+ }
+ case SurfaceDescriptor::TSurfaceDescriptorDXGIYCbCr: {
+ result = new DXGIYCbCrTextureHostD3D11(
+ aFlags, aDesc.get_SurfaceDescriptorDXGIYCbCr());
+ break;
+ }
+ default: {
+ MOZ_ASSERT_UNREACHABLE("Unsupported SurfaceDescriptor type");
+ }
+ }
+ return result.forget();
+}
+
+already_AddRefed<DrawTarget> D3D11TextureData::BorrowDrawTarget() {
+ MOZ_ASSERT(NS_IsMainThread() || NS_IsInCanvasThreadOrWorker());
+
+ if (!mDrawTarget && mTexture) {
+ // This may return a null DrawTarget
+ mDrawTarget = Factory::CreateDrawTargetForD3D11Texture(mTexture, mFormat);
+ if (!mDrawTarget) {
+ gfxCriticalNote << "Could not borrow DrawTarget (D3D11) " << (int)mFormat;
+ }
+ }
+
+ RefPtr<DrawTarget> result = mDrawTarget;
+ return result.forget();
+}
+
+bool D3D11TextureData::UpdateFromSurface(gfx::SourceSurface* aSurface) {
+ // Supporting texture updates after creation requires an ID3D11DeviceContext
+ // and those aren't threadsafe. We'd need to either lock, or have a device for
+ // whatever thread this runs on and we're trying to avoid extra devices (bug
+ // 1284672).
+ MOZ_ASSERT(false,
+ "UpdateFromSurface not supported for D3D11! Use CreateFromSurface "
+ "instead");
+ return false;
+}
+
+DXGITextureHostD3D11::DXGITextureHostD3D11(
+ TextureFlags aFlags, const SurfaceDescriptorD3D10& aDescriptor)
+ : TextureHost(TextureHostType::DXGI, aFlags),
+ mGpuProcessTextureId(aDescriptor.gpuProcessTextureId()),
+ mArrayIndex(aDescriptor.arrayIndex()),
+ mSize(aDescriptor.size()),
+ mHandle(aDescriptor.handle()),
+ mFormat(aDescriptor.format()),
+ mColorSpace(aDescriptor.colorSpace()),
+ mColorRange(aDescriptor.colorRange()),
+ mIsLocked(false) {}
+
+bool DXGITextureHostD3D11::EnsureTexture() {
+ if (mGpuProcessTextureId.isSome()) {
+ return false;
+ }
+
+ RefPtr<ID3D11Device> device;
+ if (mTexture) {
+ mTexture->GetDevice(getter_AddRefs(device));
+ if (device == DeviceManagerDx::Get()->GetCompositorDevice()) {
+ NS_WARNING("Incompatible texture.");
+ return true;
+ }
+ mTexture = nullptr;
+ }
+
+ device = GetDevice();
+ if (!device || device != DeviceManagerDx::Get()->GetCompositorDevice()) {
+ NS_WARNING("No device or incompatible device.");
+ return false;
+ }
+
+ HRESULT hr = device->OpenSharedResource(
+ (HANDLE)mHandle, __uuidof(ID3D11Texture2D),
+ (void**)(ID3D11Texture2D**)getter_AddRefs(mTexture));
+ if (FAILED(hr)) {
+ MOZ_ASSERT(false, "Failed to open shared texture");
+ return false;
+ }
+
+ D3D11_TEXTURE2D_DESC desc;
+ mTexture->GetDesc(&desc);
+ mSize = IntSize(desc.Width, desc.Height);
+ return true;
+}
+
+RefPtr<ID3D11Device> DXGITextureHostD3D11::GetDevice() {
+ if (mFlags & TextureFlags::INVALID_COMPOSITOR) {
+ return nullptr;
+ }
+
+ return mDevice;
+}
+
+bool DXGITextureHostD3D11::LockWithoutCompositor() {
+ if (!mDevice) {
+ mDevice = DeviceManagerDx::Get()->GetCompositorDevice();
+ }
+ return LockInternal();
+}
+
+void DXGITextureHostD3D11::UnlockWithoutCompositor() { UnlockInternal(); }
+
+bool DXGITextureHostD3D11::LockInternal() {
+ if (!GetDevice()) {
+ NS_WARNING("trying to lock a TextureHost without a D3D device");
+ return false;
+ }
+
+ if (!EnsureTextureSource()) {
+ return false;
+ }
+
+ mIsLocked = LockD3DTexture(mTextureSource->GetD3D11Texture());
+
+ return mIsLocked;
+}
+
+already_AddRefed<gfx::DataSourceSurface> DXGITextureHostD3D11::GetAsSurface() {
+ switch (GetFormat()) {
+ case gfx::SurfaceFormat::R8G8B8X8:
+ case gfx::SurfaceFormat::R8G8B8A8:
+ case gfx::SurfaceFormat::B8G8R8A8:
+ case gfx::SurfaceFormat::B8G8R8X8:
+ break;
+ default: {
+ MOZ_ASSERT_UNREACHABLE("DXGITextureHostD3D11: unsupported format!");
+ return nullptr;
+ }
+ }
+
+ AutoLockTextureHostWithoutCompositor autoLock(this);
+ if (autoLock.Failed()) {
+ NS_WARNING("Failed to lock the D3DTexture");
+ return nullptr;
+ }
+
+ RefPtr<ID3D11Device> device;
+ mTexture->GetDevice(getter_AddRefs(device));
+
+ D3D11_TEXTURE2D_DESC textureDesc = {0};
+ mTexture->GetDesc(&textureDesc);
+
+ RefPtr<ID3D11DeviceContext> context;
+ device->GetImmediateContext(getter_AddRefs(context));
+
+ textureDesc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
+ textureDesc.Usage = D3D11_USAGE_STAGING;
+ textureDesc.BindFlags = 0;
+ textureDesc.MiscFlags = 0;
+ textureDesc.MipLevels = 1;
+ RefPtr<ID3D11Texture2D> cpuTexture;
+ HRESULT hr = device->CreateTexture2D(&textureDesc, nullptr,
+ getter_AddRefs(cpuTexture));
+ if (FAILED(hr)) {
+ return nullptr;
+ }
+
+ context->CopyResource(cpuTexture, mTexture);
+
+ D3D11_MAPPED_SUBRESOURCE mappedSubresource;
+ hr = context->Map(cpuTexture, 0, D3D11_MAP_READ, 0, &mappedSubresource);
+ if (FAILED(hr)) {
+ return nullptr;
+ }
+
+ RefPtr<DataSourceSurface> surf = gfx::CreateDataSourceSurfaceFromData(
+ IntSize(textureDesc.Width, textureDesc.Height), GetFormat(),
+ (uint8_t*)mappedSubresource.pData, mappedSubresource.RowPitch);
+ context->Unmap(cpuTexture, 0);
+ return surf.forget();
+}
+
+bool DXGITextureHostD3D11::EnsureTextureSource() {
+ if (mTextureSource) {
+ return true;
+ }
+
+ if (!EnsureTexture()) {
+ DeviceManagerDx::Get()->ForceDeviceReset(
+ ForcedDeviceResetReason::OPENSHAREDHANDLE);
+ return false;
+ }
+
+ mTextureSource = new DataTextureSourceD3D11(mDevice, mFormat, mTexture);
+ return true;
+}
+
+void DXGITextureHostD3D11::UnlockInternal() {
+ UnlockD3DTexture(mTextureSource->GetD3D11Texture());
+}
+
+void DXGITextureHostD3D11::CreateRenderTexture(
+ const wr::ExternalImageId& aExternalImageId) {
+ RefPtr<wr::RenderTextureHost> texture =
+ new wr::RenderDXGITextureHost(mHandle, mGpuProcessTextureId, mArrayIndex,
+ mFormat, mColorSpace, mColorRange, mSize);
+ wr::RenderThread::Get()->RegisterExternalImage(aExternalImageId,
+ texture.forget());
+}
+
+uint32_t DXGITextureHostD3D11::NumSubTextures() {
+ switch (GetFormat()) {
+ case gfx::SurfaceFormat::R8G8B8X8:
+ case gfx::SurfaceFormat::R8G8B8A8:
+ case gfx::SurfaceFormat::B8G8R8A8:
+ case gfx::SurfaceFormat::B8G8R8X8: {
+ return 1;
+ }
+ case gfx::SurfaceFormat::NV12:
+ case gfx::SurfaceFormat::P010:
+ case gfx::SurfaceFormat::P016: {
+ return 2;
+ }
+ default: {
+ MOZ_ASSERT_UNREACHABLE("unexpected format");
+ return 1;
+ }
+ }
+}
+
+void DXGITextureHostD3D11::PushResourceUpdates(
+ wr::TransactionBuilder& aResources, ResourceUpdateOp aOp,
+ const Range<wr::ImageKey>& aImageKeys, const wr::ExternalImageId& aExtID) {
+ if (!gfx::gfxVars::UseWebRenderANGLE()) {
+ MOZ_ASSERT_UNREACHABLE("unexpected to be called without ANGLE");
+ return;
+ }
+
+ MOZ_ASSERT(mHandle || mGpuProcessTextureId.isSome());
+ auto method = aOp == TextureHost::ADD_IMAGE
+ ? &wr::TransactionBuilder::AddExternalImage
+ : &wr::TransactionBuilder::UpdateExternalImage;
+ switch (mFormat) {
+ case gfx::SurfaceFormat::R8G8B8X8:
+ case gfx::SurfaceFormat::R8G8B8A8:
+ case gfx::SurfaceFormat::B8G8R8A8:
+ case gfx::SurfaceFormat::B8G8R8X8: {
+ MOZ_ASSERT(aImageKeys.length() == 1);
+
+ wr::ImageDescriptor descriptor(mSize, GetFormat());
+ // Prefer TextureExternal unless the backend requires TextureRect.
+ TextureHost::NativeTexturePolicy policy =
+ TextureHost::BackendNativeTexturePolicy(aResources.GetBackendType(),
+ mSize);
+ auto imageType = policy == TextureHost::NativeTexturePolicy::REQUIRE
+ ? wr::ExternalImageType::TextureHandle(
+ wr::ImageBufferKind::TextureRect)
+ : wr::ExternalImageType::TextureHandle(
+ wr::ImageBufferKind::TextureExternal);
+ (aResources.*method)(aImageKeys[0], descriptor, aExtID, imageType, 0);
+ break;
+ }
+ case gfx::SurfaceFormat::P010:
+ case gfx::SurfaceFormat::P016:
+ case gfx::SurfaceFormat::NV12: {
+ MOZ_ASSERT(aImageKeys.length() == 2);
+ MOZ_ASSERT(mSize.width % 2 == 0);
+ MOZ_ASSERT(mSize.height % 2 == 0);
+
+ wr::ImageDescriptor descriptor0(mSize, mFormat == gfx::SurfaceFormat::NV12
+ ? gfx::SurfaceFormat::A8
+ : gfx::SurfaceFormat::A16);
+ wr::ImageDescriptor descriptor1(mSize / 2,
+ mFormat == gfx::SurfaceFormat::NV12
+ ? gfx::SurfaceFormat::R8G8
+ : gfx::SurfaceFormat::R16G16);
+ // Prefer TextureExternal unless the backend requires TextureRect.
+ TextureHost::NativeTexturePolicy policy =
+ TextureHost::BackendNativeTexturePolicy(aResources.GetBackendType(),
+ mSize);
+ auto imageType = policy == TextureHost::NativeTexturePolicy::REQUIRE
+ ? wr::ExternalImageType::TextureHandle(
+ wr::ImageBufferKind::TextureRect)
+ : wr::ExternalImageType::TextureHandle(
+ wr::ImageBufferKind::TextureExternal);
+ (aResources.*method)(aImageKeys[0], descriptor0, aExtID, imageType, 0);
+ (aResources.*method)(aImageKeys[1], descriptor1, aExtID, imageType, 1);
+ break;
+ }
+ default: {
+ MOZ_ASSERT_UNREACHABLE("unexpected to be called");
+ }
+ }
+}
+
+void DXGITextureHostD3D11::PushDisplayItems(
+ wr::DisplayListBuilder& aBuilder, const wr::LayoutRect& aBounds,
+ const wr::LayoutRect& aClip, wr::ImageRendering aFilter,
+ const Range<wr::ImageKey>& aImageKeys, PushDisplayItemFlagSet aFlags) {
+ bool preferCompositorSurface =
+ aFlags.contains(PushDisplayItemFlag::PREFER_COMPOSITOR_SURFACE);
+ if (!gfx::gfxVars::UseWebRenderANGLE()) {
+ MOZ_ASSERT_UNREACHABLE("unexpected to be called without ANGLE");
+ return;
+ }
+
+ switch (GetFormat()) {
+ case gfx::SurfaceFormat::R8G8B8X8:
+ case gfx::SurfaceFormat::R8G8B8A8:
+ case gfx::SurfaceFormat::B8G8R8A8:
+ case gfx::SurfaceFormat::B8G8R8X8: {
+ MOZ_ASSERT(aImageKeys.length() == 1);
+ aBuilder.PushImage(
+ aBounds, aClip, true, false, aFilter, aImageKeys[0],
+ !(mFlags & TextureFlags::NON_PREMULTIPLIED),
+ wr::ColorF{1.0f, 1.0f, 1.0f, 1.0f}, preferCompositorSurface,
+ SupportsExternalCompositing(aBuilder.GetBackendType()));
+ break;
+ }
+ case gfx::SurfaceFormat::P010:
+ case gfx::SurfaceFormat::P016:
+ case gfx::SurfaceFormat::NV12: {
+ // DXGI_FORMAT_P010 stores its 10 bit value in the most significant bits
+ // of each 16 bit word with the unused lower bits cleared to zero so that
+ // it may be handled as if it was DXGI_FORMAT_P016. This is approximately
+ // perceptually correct. However, due to rounding error, the precise
+ // quantized value after sampling may be off by 1.
+ MOZ_ASSERT(aImageKeys.length() == 2);
+ aBuilder.PushNV12Image(
+ aBounds, aClip, true, aImageKeys[0], aImageKeys[1],
+ GetFormat() == gfx::SurfaceFormat::NV12 ? wr::ColorDepth::Color8
+ : wr::ColorDepth::Color16,
+ wr::ToWrYuvColorSpace(ToYUVColorSpace(mColorSpace)),
+ wr::ToWrColorRange(mColorRange), aFilter, preferCompositorSurface,
+ SupportsExternalCompositing(aBuilder.GetBackendType()));
+ break;
+ }
+ default: {
+ MOZ_ASSERT_UNREACHABLE("unexpected to be called");
+ }
+ }
+}
+
+bool DXGITextureHostD3D11::SupportsExternalCompositing(
+ WebRenderBackend aBackend) {
+ if (aBackend == WebRenderBackend::SOFTWARE) {
+ return true;
+ }
+ // XXX Add P010 and P016 support.
+ if (GetFormat() == gfx::SurfaceFormat::NV12 &&
+ gfx::gfxVars::UseWebRenderDCompVideoOverlayWin()) {
+ return true;
+ }
+ return false;
+}
+
+DXGIYCbCrTextureHostD3D11::DXGIYCbCrTextureHostD3D11(
+ TextureFlags aFlags, const SurfaceDescriptorDXGIYCbCr& aDescriptor)
+ : TextureHost(TextureHostType::DXGIYCbCr, aFlags),
+ mSize(aDescriptor.size()),
+ mSizeY(aDescriptor.sizeY()),
+ mSizeCbCr(aDescriptor.sizeCbCr()),
+ mIsLocked(false),
+ mColorDepth(aDescriptor.colorDepth()),
+ mYUVColorSpace(aDescriptor.yUVColorSpace()),
+ mColorRange(aDescriptor.colorRange()) {
+ mHandles[0] = aDescriptor.handleY();
+ mHandles[1] = aDescriptor.handleCb();
+ mHandles[2] = aDescriptor.handleCr();
+}
+
+bool DXGIYCbCrTextureHostD3D11::EnsureTexture() {
+ RefPtr<ID3D11Device> device;
+ if (mTextures[0]) {
+ mTextures[0]->GetDevice(getter_AddRefs(device));
+ if (device == DeviceManagerDx::Get()->GetCompositorDevice()) {
+ NS_WARNING("Incompatible texture.");
+ return true;
+ }
+ mTextures[0] = nullptr;
+ mTextures[1] = nullptr;
+ mTextures[2] = nullptr;
+ }
+
+ if (!GetDevice() ||
+ GetDevice() != DeviceManagerDx::Get()->GetCompositorDevice()) {
+ NS_WARNING("No device or incompatible device.");
+ return false;
+ }
+
+ device = GetDevice();
+ RefPtr<ID3D11Texture2D> textures[3];
+
+ HRESULT hr = device->OpenSharedResource(
+ (HANDLE)mHandles[0], __uuidof(ID3D11Texture2D),
+ (void**)(ID3D11Texture2D**)getter_AddRefs(textures[0]));
+ if (FAILED(hr)) {
+ NS_WARNING("Failed to open shared texture for Y Plane");
+ return false;
+ }
+
+ hr = device->OpenSharedResource(
+ (HANDLE)mHandles[1], __uuidof(ID3D11Texture2D),
+ (void**)(ID3D11Texture2D**)getter_AddRefs(textures[1]));
+ if (FAILED(hr)) {
+ NS_WARNING("Failed to open shared texture for Cb Plane");
+ return false;
+ }
+
+ hr = device->OpenSharedResource(
+ (HANDLE)mHandles[2], __uuidof(ID3D11Texture2D),
+ (void**)(ID3D11Texture2D**)getter_AddRefs(textures[2]));
+ if (FAILED(hr)) {
+ NS_WARNING("Failed to open shared texture for Cr Plane");
+ return false;
+ }
+
+ mTextures[0] = textures[0].forget();
+ mTextures[1] = textures[1].forget();
+ mTextures[2] = textures[2].forget();
+
+ return true;
+}
+
+RefPtr<ID3D11Device> DXGIYCbCrTextureHostD3D11::GetDevice() { return nullptr; }
+
+bool DXGIYCbCrTextureHostD3D11::EnsureTextureSource() { return false; }
+
+void DXGIYCbCrTextureHostD3D11::CreateRenderTexture(
+ const wr::ExternalImageId& aExternalImageId) {
+ RefPtr<wr::RenderTextureHost> texture = new wr::RenderDXGIYCbCrTextureHost(
+ mHandles, mYUVColorSpace, mColorDepth, mColorRange, mSizeY, mSizeCbCr);
+
+ wr::RenderThread::Get()->RegisterExternalImage(aExternalImageId,
+ texture.forget());
+}
+
+uint32_t DXGIYCbCrTextureHostD3D11::NumSubTextures() {
+ // ycbcr use 3 sub textures.
+ return 3;
+}
+
+void DXGIYCbCrTextureHostD3D11::PushResourceUpdates(
+ wr::TransactionBuilder& aResources, ResourceUpdateOp aOp,
+ const Range<wr::ImageKey>& aImageKeys, const wr::ExternalImageId& aExtID) {
+ if (!gfx::gfxVars::UseWebRenderANGLE()) {
+ MOZ_ASSERT_UNREACHABLE("unexpected to be called without ANGLE");
+ return;
+ }
+
+ MOZ_ASSERT(mHandles[0] && mHandles[1] && mHandles[2]);
+ MOZ_ASSERT(aImageKeys.length() == 3);
+ // Assume the chroma planes are rounded up if the luma plane is odd sized.
+ MOZ_ASSERT((mSizeCbCr.width == mSizeY.width ||
+ mSizeCbCr.width == (mSizeY.width + 1) >> 1) &&
+ (mSizeCbCr.height == mSizeY.height ||
+ mSizeCbCr.height == (mSizeY.height + 1) >> 1));
+
+ auto method = aOp == TextureHost::ADD_IMAGE
+ ? &wr::TransactionBuilder::AddExternalImage
+ : &wr::TransactionBuilder::UpdateExternalImage;
+
+ // Prefer TextureExternal unless the backend requires TextureRect.
+ // Use a size that is the maximum of the Y and CbCr sizes.
+ IntSize textureSize = std::max(mSizeY, mSizeCbCr);
+ TextureHost::NativeTexturePolicy policy =
+ TextureHost::BackendNativeTexturePolicy(aResources.GetBackendType(),
+ textureSize);
+ auto imageType = policy == TextureHost::NativeTexturePolicy::REQUIRE
+ ? wr::ExternalImageType::TextureHandle(
+ wr::ImageBufferKind::TextureRect)
+ : wr::ExternalImageType::TextureHandle(
+ wr::ImageBufferKind::TextureExternal);
+
+ // y
+ wr::ImageDescriptor descriptor0(mSizeY, gfx::SurfaceFormat::A8);
+ // cb and cr
+ wr::ImageDescriptor descriptor1(mSizeCbCr, gfx::SurfaceFormat::A8);
+ (aResources.*method)(aImageKeys[0], descriptor0, aExtID, imageType, 0);
+ (aResources.*method)(aImageKeys[1], descriptor1, aExtID, imageType, 1);
+ (aResources.*method)(aImageKeys[2], descriptor1, aExtID, imageType, 2);
+}
+
+void DXGIYCbCrTextureHostD3D11::PushDisplayItems(
+ wr::DisplayListBuilder& aBuilder, const wr::LayoutRect& aBounds,
+ const wr::LayoutRect& aClip, wr::ImageRendering aFilter,
+ const Range<wr::ImageKey>& aImageKeys, PushDisplayItemFlagSet aFlags) {
+ if (!gfx::gfxVars::UseWebRenderANGLE()) {
+ MOZ_ASSERT_UNREACHABLE("unexpected to be called without ANGLE");
+ return;
+ }
+
+ MOZ_ASSERT(aImageKeys.length() == 3);
+
+ aBuilder.PushYCbCrPlanarImage(
+ aBounds, aClip, true, aImageKeys[0], aImageKeys[1], aImageKeys[2],
+ wr::ToWrColorDepth(mColorDepth), wr::ToWrYuvColorSpace(mYUVColorSpace),
+ wr::ToWrColorRange(mColorRange), aFilter,
+ aFlags.contains(PushDisplayItemFlag::PREFER_COMPOSITOR_SURFACE),
+ SupportsExternalCompositing(aBuilder.GetBackendType()));
+}
+
+bool DXGIYCbCrTextureHostD3D11::SupportsExternalCompositing(
+ WebRenderBackend aBackend) {
+ return aBackend == WebRenderBackend::SOFTWARE;
+}
+
+bool DataTextureSourceD3D11::Update(DataSourceSurface* aSurface,
+ nsIntRegion* aDestRegion,
+ IntPoint* aSrcOffset,
+ IntPoint* aDstOffset) {
+ // Incremental update with a source offset is only used on Mac so it is not
+ // clear that we ever will need to support it for D3D.
+ MOZ_ASSERT(!aSrcOffset);
+ MOZ_RELEASE_ASSERT(!aDstOffset);
+ MOZ_ASSERT(aSurface);
+
+ MOZ_ASSERT(mAllowTextureUploads);
+ if (!mAllowTextureUploads) {
+ return false;
+ }
+
+ HRESULT hr;
+
+ if (!mDevice) {
+ return false;
+ }
+
+ uint32_t bpp = BytesPerPixel(aSurface->GetFormat());
+ DXGI_FORMAT dxgiFormat = SurfaceFormatToDXGIFormat(aSurface->GetFormat());
+
+ mSize = aSurface->GetSize();
+ mFormat = aSurface->GetFormat();
+
+ CD3D11_TEXTURE2D_DESC desc(dxgiFormat, mSize.width, mSize.height, 1, 1);
+
+ int32_t maxSize = GetMaxTextureSizeFromDevice(mDevice);
+ if ((mSize.width <= maxSize && mSize.height <= maxSize) ||
+ (mFlags & TextureFlags::DISALLOW_BIGIMAGE)) {
+ if (mTexture) {
+ D3D11_TEXTURE2D_DESC currentDesc;
+ mTexture->GetDesc(&currentDesc);
+
+ // Make sure there's no size mismatch, if there is, recreate.
+ if (static_cast<int32_t>(currentDesc.Width) != mSize.width ||
+ static_cast<int32_t>(currentDesc.Height) != mSize.height ||
+ currentDesc.Format != dxgiFormat) {
+ mTexture = nullptr;
+ // Make sure we upload the whole surface.
+ aDestRegion = nullptr;
+ }
+ }
+
+ nsIntRegion* regionToUpdate = aDestRegion;
+ if (!mTexture) {
+ hr = mDevice->CreateTexture2D(&desc, nullptr, getter_AddRefs(mTexture));
+ mIsTiled = false;
+ if (FAILED(hr) || !mTexture) {
+ Reset();
+ return false;
+ }
+
+ if (mFlags & TextureFlags::COMPONENT_ALPHA) {
+ regionToUpdate = nullptr;
+ }
+ }
+
+ DataSourceSurface::MappedSurface map;
+ if (!aSurface->Map(DataSourceSurface::MapType::READ, &map)) {
+ gfxCriticalError() << "Failed to map surface.";
+ Reset();
+ return false;
+ }
+
+ RefPtr<ID3D11DeviceContext> context;
+ mDevice->GetImmediateContext(getter_AddRefs(context));
+
+ if (regionToUpdate) {
+ for (auto iter = regionToUpdate->RectIter(); !iter.Done(); iter.Next()) {
+ const IntRect& rect = iter.Get();
+ D3D11_BOX box;
+ box.front = 0;
+ box.back = 1;
+ box.left = rect.X();
+ box.top = rect.Y();
+ box.right = rect.XMost();
+ box.bottom = rect.YMost();
+
+ void* data = map.mData + map.mStride * rect.Y() +
+ BytesPerPixel(aSurface->GetFormat()) * rect.X();
+
+ context->UpdateSubresource(mTexture, 0, &box, data, map.mStride,
+ map.mStride * rect.Height());
+ }
+ } else {
+ context->UpdateSubresource(mTexture, 0, nullptr, map.mData, map.mStride,
+ map.mStride * mSize.height);
+ }
+
+ aSurface->Unmap();
+ } else {
+ mIsTiled = true;
+ uint32_t tileCount = GetRequiredTilesD3D11(mSize.width, maxSize) *
+ GetRequiredTilesD3D11(mSize.height, maxSize);
+
+ mTileTextures.resize(tileCount);
+ mTileSRVs.resize(tileCount);
+ mTexture = nullptr;
+
+ DataSourceSurface::ScopedMap map(aSurface, DataSourceSurface::READ);
+ if (!map.IsMapped()) {
+ gfxCriticalError() << "Failed to map surface.";
+ Reset();
+ return false;
+ }
+
+ for (uint32_t i = 0; i < tileCount; i++) {
+ IntRect tileRect = GetTileRect(i);
+
+ desc.Width = tileRect.Width();
+ desc.Height = tileRect.Height();
+ desc.Usage = D3D11_USAGE_IMMUTABLE;
+
+ D3D11_SUBRESOURCE_DATA initData;
+ initData.pSysMem =
+ map.GetData() + tileRect.Y() * map.GetStride() + tileRect.X() * bpp;
+ initData.SysMemPitch = map.GetStride();
+
+ hr = mDevice->CreateTexture2D(&desc, &initData,
+ getter_AddRefs(mTileTextures[i]));
+ if (FAILED(hr) || !mTileTextures[i]) {
+ Reset();
+ return false;
+ }
+ }
+ }
+ return true;
+}
+
+ID3D11Texture2D* DataTextureSourceD3D11::GetD3D11Texture() const {
+ return mIterating ? mTileTextures[mCurrentTile] : mTexture;
+}
+
+RefPtr<TextureSource> DataTextureSourceD3D11::ExtractCurrentTile() {
+ MOZ_ASSERT(mIterating);
+ return new DataTextureSourceD3D11(mDevice, mFormat,
+ mTileTextures[mCurrentTile]);
+}
+
+ID3D11ShaderResourceView* DataTextureSourceD3D11::GetShaderResourceView() {
+ if (mIterating) {
+ if (!mTileSRVs[mCurrentTile]) {
+ if (!mTileTextures[mCurrentTile]) {
+ return nullptr;
+ }
+
+ RefPtr<ID3D11Device> device;
+ mTileTextures[mCurrentTile]->GetDevice(getter_AddRefs(device));
+ HRESULT hr = device->CreateShaderResourceView(
+ mTileTextures[mCurrentTile], nullptr,
+ getter_AddRefs(mTileSRVs[mCurrentTile]));
+ if (FAILED(hr)) {
+ gfxCriticalNote
+ << "[D3D11] DataTextureSourceD3D11:GetShaderResourceView CreateSRV "
+ "failure "
+ << gfx::hexa(hr);
+ return nullptr;
+ }
+ }
+ return mTileSRVs[mCurrentTile];
+ }
+
+ return TextureSourceD3D11::GetShaderResourceView();
+}
+
+void DataTextureSourceD3D11::Reset() {
+ mTexture = nullptr;
+ mTileSRVs.resize(0);
+ mTileTextures.resize(0);
+ mIsTiled = false;
+ mSize.width = 0;
+ mSize.height = 0;
+}
+
+IntRect DataTextureSourceD3D11::GetTileRect(uint32_t aIndex) const {
+ return GetTileRectD3D11(aIndex, mSize, GetMaxTextureSizeFromDevice(mDevice));
+}
+
+IntRect DataTextureSourceD3D11::GetTileRect() {
+ IntRect rect = GetTileRect(mCurrentTile);
+ return IntRect(rect.X(), rect.Y(), rect.Width(), rect.Height());
+}
+
+CompositingRenderTargetD3D11::CompositingRenderTargetD3D11(
+ ID3D11Texture2D* aTexture, const gfx::IntPoint& aOrigin,
+ DXGI_FORMAT aFormatOverride)
+ : CompositingRenderTarget(aOrigin) {
+ MOZ_ASSERT(aTexture);
+
+ mTexture = aTexture;
+
+ RefPtr<ID3D11Device> device;
+ mTexture->GetDevice(getter_AddRefs(device));
+
+ mFormatOverride = aFormatOverride;
+
+ // If we happen to have a typeless underlying DXGI surface, we need to be
+ // explicit about the format here. (Such a surface could come from an external
+ // source, such as the Oculus compositor)
+ CD3D11_RENDER_TARGET_VIEW_DESC rtvDesc(D3D11_RTV_DIMENSION_TEXTURE2D,
+ mFormatOverride);
+ D3D11_RENDER_TARGET_VIEW_DESC* desc =
+ aFormatOverride == DXGI_FORMAT_UNKNOWN ? nullptr : &rtvDesc;
+
+ HRESULT hr =
+ device->CreateRenderTargetView(mTexture, desc, getter_AddRefs(mRTView));
+
+ if (FAILED(hr)) {
+ LOGD3D11("Failed to create RenderTargetView.");
+ }
+}
+
+void CompositingRenderTargetD3D11::BindRenderTarget(
+ ID3D11DeviceContext* aContext) {
+ if (mClearOnBind) {
+ FLOAT clear[] = {0, 0, 0, 0};
+ aContext->ClearRenderTargetView(mRTView, clear);
+ mClearOnBind = false;
+ }
+ ID3D11RenderTargetView* view = mRTView;
+ aContext->OMSetRenderTargets(1, &view, nullptr);
+}
+
+IntSize CompositingRenderTargetD3D11::GetSize() const {
+ return TextureSourceD3D11::GetSize();
+}
+
+static inline bool ShouldDevCrashOnSyncInitFailure() {
+ // Compositor shutdown does not wait for video decoding to finish, so it is
+ // possible for the compositor to destroy the SyncObject before video has a
+ // chance to initialize it.
+ if (!NS_IsMainThread()) {
+ return false;
+ }
+
+ // Note: CompositorIsInGPUProcess is a main-thread-only function.
+ return !CompositorBridgeChild::CompositorIsInGPUProcess() &&
+ !DeviceManagerDx::Get()->HasDeviceReset();
+}
+
+SyncObjectD3D11Host::SyncObjectD3D11Host(ID3D11Device* aDevice)
+ : mSyncHandle(0), mDevice(aDevice) {
+ MOZ_ASSERT(aDevice);
+}
+
+bool SyncObjectD3D11Host::Init() {
+ CD3D11_TEXTURE2D_DESC desc(
+ DXGI_FORMAT_B8G8R8A8_UNORM, 1, 1, 1, 1,
+ D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_RENDER_TARGET);
+ desc.MiscFlags = D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX;
+
+ RefPtr<ID3D11Texture2D> texture;
+ HRESULT hr =
+ mDevice->CreateTexture2D(&desc, nullptr, getter_AddRefs(texture));
+ if (FAILED(hr) || !texture) {
+ gfxWarning() << "Could not create a sync texture: " << gfx::hexa(hr);
+ return false;
+ }
+
+ hr = texture->QueryInterface((IDXGIResource**)getter_AddRefs(mSyncTexture));
+ if (FAILED(hr) || !mSyncTexture) {
+ gfxWarning() << "Could not QI sync texture: " << gfx::hexa(hr);
+ return false;
+ }
+
+ hr = mSyncTexture->QueryInterface(
+ (IDXGIKeyedMutex**)getter_AddRefs(mKeyedMutex));
+ if (FAILED(hr) || !mKeyedMutex) {
+ gfxWarning() << "Could not QI keyed-mutex: " << gfx::hexa(hr);
+ return false;
+ }
+
+ hr = mSyncTexture->GetSharedHandle(&mSyncHandle);
+ if (FAILED(hr) || !mSyncHandle) {
+ NS_DispatchToMainThread(NS_NewRunnableFunction(
+ "layers::SyncObjectD3D11Renderer::Init",
+ []() -> void { Accumulate(Telemetry::D3D11_SYNC_HANDLE_FAILURE, 1); }));
+ gfxWarning() << "Could not get sync texture shared handle: "
+ << gfx::hexa(hr);
+ return false;
+ }
+
+ return true;
+}
+
+SyncHandle SyncObjectD3D11Host::GetSyncHandle() { return mSyncHandle; }
+
+bool SyncObjectD3D11Host::Synchronize(bool aFallible) {
+ HRESULT hr;
+ AutoTextureLock lock(mKeyedMutex, hr, 10000);
+
+ if (hr == WAIT_TIMEOUT) {
+ hr = mDevice->GetDeviceRemovedReason();
+ if (hr != S_OK) {
+ // Since the timeout is related to the driver-removed. Return false for
+ // error handling.
+ gfxCriticalNote << "GFX: D3D11 timeout with device-removed:"
+ << gfx::hexa(hr);
+ } else if (aFallible) {
+ gfxCriticalNote << "GFX: D3D11 timeout on the D3D11 sync lock.";
+ } else {
+ // There is no driver-removed event. Crash with this timeout.
+ MOZ_CRASH("GFX: D3D11 normal status timeout");
+ }
+
+ return false;
+ }
+ if (hr == WAIT_ABANDONED) {
+ gfxCriticalNote << "GFX: AL_D3D11 abandoned sync";
+ }
+
+ return true;
+}
+
+SyncObjectD3D11Client::SyncObjectD3D11Client(SyncHandle aSyncHandle,
+ ID3D11Device* aDevice)
+ : mSyncLock("SyncObjectD3D11"), mSyncHandle(aSyncHandle), mDevice(aDevice) {
+ MOZ_ASSERT(aDevice);
+}
+
+SyncObjectD3D11Client::SyncObjectD3D11Client(SyncHandle aSyncHandle)
+ : mSyncLock("SyncObjectD3D11"), mSyncHandle(aSyncHandle) {}
+
+bool SyncObjectD3D11Client::Init(ID3D11Device* aDevice, bool aFallible) {
+ if (mKeyedMutex) {
+ return true;
+ }
+
+ HRESULT hr = aDevice->OpenSharedResource(
+ mSyncHandle, __uuidof(ID3D11Texture2D),
+ (void**)(ID3D11Texture2D**)getter_AddRefs(mSyncTexture));
+ if (FAILED(hr) || !mSyncTexture) {
+ gfxCriticalNote << "Failed to OpenSharedResource for SyncObjectD3D11: "
+ << hexa(hr);
+ if (!aFallible && ShouldDevCrashOnSyncInitFailure()) {
+ gfxDevCrash(LogReason::D3D11FinalizeFrame)
+ << "Without device reset: " << hexa(hr);
+ }
+ return false;
+ }
+
+ hr = mSyncTexture->QueryInterface(__uuidof(IDXGIKeyedMutex),
+ getter_AddRefs(mKeyedMutex));
+ if (FAILED(hr) || !mKeyedMutex) {
+ // Leave both the critical error and MOZ_CRASH for now; the critical error
+ // lets us "save" the hr value. We will probably eventually replace this
+ // with gfxDevCrash.
+ if (!aFallible) {
+ gfxCriticalError() << "Failed to get KeyedMutex (2): " << hexa(hr);
+ MOZ_CRASH("GFX: Cannot get D3D11 KeyedMutex");
+ } else {
+ gfxCriticalNote << "Failed to get KeyedMutex (3): " << hexa(hr);
+ }
+ return false;
+ }
+
+ return true;
+}
+
+void SyncObjectD3D11Client::RegisterTexture(ID3D11Texture2D* aTexture) {
+ mSyncedTextures.push_back(aTexture);
+}
+
+bool SyncObjectD3D11Client::IsSyncObjectValid() {
+ MOZ_ASSERT(mDevice);
+ return true;
+}
+
+// We have only 1 sync object. As a thing that somehow works,
+// we copy each of the textures that need to be synced with the compositor
+// into our sync object and only use a lock for this sync object.
+// This way, we don't have to sync every texture we send to the compositor.
+// We only have to do this once per transaction.
+bool SyncObjectD3D11Client::Synchronize(bool aFallible) {
+ MOZ_ASSERT(mDevice);
+ // Since this can be called from either the Paint or Main thread.
+ // We don't want this to race since we initialize the sync texture here
+ // too.
+ MutexAutoLock syncLock(mSyncLock);
+
+ if (!mSyncedTextures.size()) {
+ return true;
+ }
+ if (!Init(mDevice, aFallible)) {
+ return false;
+ }
+
+ return SynchronizeInternal(mDevice, aFallible);
+}
+
+bool SyncObjectD3D11Client::SynchronizeInternal(ID3D11Device* aDevice,
+ bool aFallible) {
+ mSyncLock.AssertCurrentThreadOwns();
+
+ HRESULT hr;
+ AutoTextureLock lock(mKeyedMutex, hr, 20000);
+
+ if (hr == WAIT_TIMEOUT) {
+ if (DeviceManagerDx::Get()->HasDeviceReset()) {
+ gfxWarning() << "AcquireSync timed out because of device reset.";
+ return false;
+ }
+ if (aFallible) {
+ gfxWarning() << "Timeout on the D3D11 sync lock.";
+ } else {
+ gfxDevCrash(LogReason::D3D11SyncLock)
+ << "Timeout on the D3D11 sync lock.";
+ }
+ return false;
+ }
+
+ D3D11_BOX box;
+ box.front = box.top = box.left = 0;
+ box.back = box.bottom = box.right = 1;
+
+ RefPtr<ID3D11DeviceContext> ctx;
+ aDevice->GetImmediateContext(getter_AddRefs(ctx));
+
+ for (auto iter = mSyncedTextures.begin(); iter != mSyncedTextures.end();
+ iter++) {
+ ctx->CopySubresourceRegion(mSyncTexture, 0, 0, 0, 0, *iter, 0, &box);
+ }
+
+ mSyncedTextures.clear();
+
+ return true;
+}
+
+uint32_t GetMaxTextureSizeFromDevice(ID3D11Device* aDevice) {
+ return GetMaxTextureSizeForFeatureLevel(aDevice->GetFeatureLevel());
+}
+
+AutoLockD3D11Texture::AutoLockD3D11Texture(ID3D11Texture2D* aTexture) {
+ aTexture->QueryInterface((IDXGIKeyedMutex**)getter_AddRefs(mMutex));
+ if (!mMutex) {
+ return;
+ }
+ HRESULT hr = mMutex->AcquireSync(0, 10000);
+ if (hr == WAIT_TIMEOUT) {
+ MOZ_CRASH("GFX: IMFYCbCrImage timeout");
+ }
+
+ if (FAILED(hr)) {
+ NS_WARNING("Failed to lock the texture");
+ }
+}
+
+AutoLockD3D11Texture::~AutoLockD3D11Texture() {
+ if (!mMutex) {
+ return;
+ }
+ HRESULT hr = mMutex->ReleaseSync(0);
+ if (FAILED(hr)) {
+ NS_WARNING("Failed to unlock the texture");
+ }
+}
+
+SyncObjectD3D11ClientContentDevice::SyncObjectD3D11ClientContentDevice(
+ SyncHandle aSyncHandle)
+ : SyncObjectD3D11Client(aSyncHandle) {}
+
+bool SyncObjectD3D11ClientContentDevice::Synchronize(bool aFallible) {
+ // Since this can be called from either the Paint or Main thread.
+ // We don't want this to race since we initialize the sync texture here
+ // too.
+ MutexAutoLock syncLock(mSyncLock);
+
+ MOZ_ASSERT(mContentDevice);
+
+ if (!mSyncedTextures.size()) {
+ return true;
+ }
+
+ if (!Init(mContentDevice, aFallible)) {
+ return false;
+ }
+
+ RefPtr<ID3D11Device> dev;
+ mSyncTexture->GetDevice(getter_AddRefs(dev));
+
+ if (dev == DeviceManagerDx::Get()->GetContentDevice()) {
+ if (DeviceManagerDx::Get()->HasDeviceReset()) {
+ return false;
+ }
+ }
+
+ if (dev != mContentDevice) {
+ gfxWarning() << "Attempt to sync texture from invalid device.";
+ return false;
+ }
+
+ return SyncObjectD3D11Client::SynchronizeInternal(dev, aFallible);
+}
+
+bool SyncObjectD3D11ClientContentDevice::IsSyncObjectValid() {
+ RefPtr<ID3D11Device> dev;
+ // There is a case that devices are not initialized yet with WebRender.
+ if (gfxPlatform::GetPlatform()->DevicesInitialized()) {
+ dev = DeviceManagerDx::Get()->GetContentDevice();
+ }
+
+ // Update mDevice if the ContentDevice initialization is detected.
+ if (!mContentDevice && dev && NS_IsMainThread()) {
+ mContentDevice = dev;
+ }
+
+ if (!dev || (NS_IsMainThread() && dev != mContentDevice)) {
+ return false;
+ }
+ return true;
+}
+
+void SyncObjectD3D11ClientContentDevice::EnsureInitialized() {
+ if (mContentDevice) {
+ return;
+ }
+
+ if (XRE_IsGPUProcess() || !gfxPlatform::GetPlatform()->DevicesInitialized()) {
+ return;
+ }
+
+ mContentDevice = DeviceManagerDx::Get()->GetContentDevice();
+}
+
+StaticAutoPtr<GpuProcessD3D11TextureMap> GpuProcessD3D11TextureMap::sInstance;
+
+/* static */
+void GpuProcessD3D11TextureMap::Init() {
+ MOZ_ASSERT(XRE_IsGPUProcess());
+ sInstance = new GpuProcessD3D11TextureMap();
+}
+
+/* static */
+void GpuProcessD3D11TextureMap::Shutdown() {
+ MOZ_ASSERT(XRE_IsGPUProcess());
+ sInstance = nullptr;
+}
+
+/* static */
+GpuProcessTextureId GpuProcessD3D11TextureMap::GetNextTextureId() {
+ MOZ_ASSERT(XRE_IsGPUProcess());
+ return GpuProcessTextureId::GetNext();
+}
+
+GpuProcessD3D11TextureMap::GpuProcessD3D11TextureMap()
+ : mD3D11TexturesById("D3D11TextureMap::mD3D11TexturesById") {}
+
+GpuProcessD3D11TextureMap::~GpuProcessD3D11TextureMap() {}
+
+void GpuProcessD3D11TextureMap::Register(
+ GpuProcessTextureId aTextureId, ID3D11Texture2D* aTexture,
+ uint32_t aArrayIndex, const gfx::IntSize& aSize,
+ RefPtr<IMFSampleUsageInfo> aUsageInfo) {
+ MOZ_RELEASE_ASSERT(aTexture);
+ MOZ_RELEASE_ASSERT(aUsageInfo);
+
+ auto textures = mD3D11TexturesById.Lock();
+
+ auto it = textures->find(aTextureId);
+ if (it != textures->end()) {
+ MOZ_ASSERT_UNREACHABLE("unexpected to be called");
+ return;
+ }
+ textures->emplace(aTextureId,
+ TextureHolder(aTexture, aArrayIndex, aSize, aUsageInfo));
+}
+
+void GpuProcessD3D11TextureMap::Unregister(GpuProcessTextureId aTextureId) {
+ auto textures = mD3D11TexturesById.Lock();
+
+ auto it = textures->find(aTextureId);
+ if (it == textures->end()) {
+ return;
+ }
+ textures->erase(it);
+}
+
+RefPtr<ID3D11Texture2D> GpuProcessD3D11TextureMap::GetTexture(
+ GpuProcessTextureId aTextureId) {
+ auto textures = mD3D11TexturesById.Lock();
+
+ auto it = textures->find(aTextureId);
+ if (it == textures->end()) {
+ return nullptr;
+ }
+
+ return it->second.mTexture;
+}
+
+Maybe<HANDLE> GpuProcessD3D11TextureMap::GetSharedHandleOfCopiedTexture(
+ GpuProcessTextureId aTextureId) {
+ TextureHolder holder;
+ {
+ auto textures = mD3D11TexturesById.Lock();
+
+ auto it = textures->find(aTextureId);
+ if (it == textures->end()) {
+ return Nothing();
+ }
+
+ if (it->second.mCopiedTextureSharedHandle.isSome()) {
+ return it->second.mCopiedTextureSharedHandle;
+ }
+
+ holder = it->second;
+ }
+
+ RefPtr<ID3D11Device> device;
+ holder.mTexture->GetDevice(getter_AddRefs(device));
+ if (!device) {
+ return Nothing();
+ }
+
+ RefPtr<ID3D11DeviceContext> context;
+ device->GetImmediateContext(getter_AddRefs(context));
+ if (!context) {
+ return Nothing();
+ }
+
+ CD3D11_TEXTURE2D_DESC newDesc(
+ DXGI_FORMAT_NV12, holder.mSize.width, holder.mSize.height, 1, 1,
+ D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE);
+ newDesc.MiscFlags = D3D11_RESOURCE_MISC_SHARED;
+
+ RefPtr<ID3D11Texture2D> copiedTexture;
+ HRESULT hr =
+ device->CreateTexture2D(&newDesc, nullptr, getter_AddRefs(copiedTexture));
+ if (FAILED(hr)) {
+ return Nothing();
+ }
+
+ D3D11_TEXTURE2D_DESC inDesc;
+ holder.mTexture->GetDesc(&inDesc);
+
+ D3D11_TEXTURE2D_DESC outDesc;
+ copiedTexture->GetDesc(&outDesc);
+
+ UINT height = std::min(inDesc.Height, outDesc.Height);
+ UINT width = std::min(inDesc.Width, outDesc.Width);
+ D3D11_BOX srcBox = {0, 0, 0, width, height, 1};
+
+ context->CopySubresourceRegion(copiedTexture, 0, 0, 0, 0, holder.mTexture,
+ holder.mArrayIndex, &srcBox);
+
+ RefPtr<IDXGIResource> resource;
+ copiedTexture->QueryInterface((IDXGIResource**)getter_AddRefs(resource));
+ if (!resource) {
+ return Nothing();
+ }
+
+ HANDLE sharedHandle;
+ hr = resource->GetSharedHandle(&sharedHandle);
+ if (FAILED(hr)) {
+ return Nothing();
+ }
+
+ RefPtr<ID3D11Query> query;
+ CD3D11_QUERY_DESC desc(D3D11_QUERY_EVENT);
+ hr = device->CreateQuery(&desc, getter_AddRefs(query));
+ if (FAILED(hr) || !query) {
+ gfxWarning() << "Could not create D3D11_QUERY_EVENT: " << gfx::hexa(hr);
+ return Nothing();
+ }
+
+ context->End(query);
+
+ BOOL result;
+ bool ret = WaitForFrameGPUQuery(device, context, query, &result);
+ if (!ret) {
+ gfxCriticalNoteOnce << "WaitForFrameGPUQuery() failed";
+ }
+
+ {
+ auto textures = mD3D11TexturesById.Lock();
+
+ auto it = textures->find(aTextureId);
+ if (it == textures->end()) {
+ MOZ_ASSERT_UNREACHABLE("unexpected to be called");
+ return Nothing();
+ }
+
+ // Disable no video copy for future decoded video frames. Since
+ // GetSharedHandleOfCopiedTexture() is slow.
+ it->second.mIMFSampleUsageInfo->DisableZeroCopyNV12Texture();
+
+ it->second.mCopiedTexture = copiedTexture;
+ it->second.mCopiedTextureSharedHandle = Some(sharedHandle);
+ }
+
+ return Some(sharedHandle);
+}
+
+GpuProcessD3D11TextureMap::TextureHolder::TextureHolder(
+ ID3D11Texture2D* aTexture, uint32_t aArrayIndex, const gfx::IntSize& aSize,
+ RefPtr<IMFSampleUsageInfo> aUsageInfo)
+ : mTexture(aTexture),
+ mArrayIndex(aArrayIndex),
+ mSize(aSize),
+ mIMFSampleUsageInfo(aUsageInfo) {}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/d3d11/TextureD3D11.h b/gfx/layers/d3d11/TextureD3D11.h
new file mode 100644
index 0000000000..2d43835d5b
--- /dev/null
+++ b/gfx/layers/d3d11/TextureD3D11.h
@@ -0,0 +1,649 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_GFX_TEXTURED3D11_H
+#define MOZILLA_GFX_TEXTURED3D11_H
+
+#include <d3d11.h>
+
+#include <vector>
+
+#include "d3d9.h"
+#include "gfxWindowsPlatform.h"
+#include "mozilla/DataMutex.h"
+#include "mozilla/GfxMessageUtils.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/gfx/2D.h"
+#include "mozilla/layers/Compositor.h"
+#include "mozilla/layers/SyncObject.h"
+#include "mozilla/layers/TextureClient.h"
+#include "mozilla/layers/TextureHost.h"
+
+namespace mozilla {
+namespace gl {
+class GLBlitHelper;
+}
+
+namespace layers {
+
+already_AddRefed<TextureHost> CreateTextureHostD3D11(
+ const SurfaceDescriptor& aDesc, ISurfaceAllocator* aDeallocator,
+ LayersBackend aBackend, TextureFlags aFlags);
+
+class MOZ_RAII AutoTextureLock final {
+ public:
+ AutoTextureLock(IDXGIKeyedMutex* aMutex, HRESULT& aResult,
+ uint32_t aTimeout = 0);
+ ~AutoTextureLock();
+
+ private:
+ RefPtr<IDXGIKeyedMutex> mMutex;
+ HRESULT mResult;
+};
+
+class CompositorD3D11;
+class IMFSampleUsageInfo;
+
+class D3D11TextureData final : public TextureData {
+ public:
+ // If aDevice is null, use one provided by gfxWindowsPlatform.
+ static D3D11TextureData* Create(gfx::IntSize aSize,
+ gfx::SurfaceFormat aFormat,
+ TextureAllocationFlags aAllocFlags,
+ ID3D11Device* aDevice = nullptr);
+ static D3D11TextureData* Create(gfx::SourceSurface* aSurface,
+ TextureAllocationFlags aAllocFlags,
+ ID3D11Device* aDevice = nullptr);
+
+ static already_AddRefed<TextureClient> CreateTextureClient(
+ ID3D11Texture2D* aTexture, uint32_t aIndex, gfx::IntSize aSize,
+ gfx::SurfaceFormat aFormat, gfx::ColorSpace2 aColorSpace,
+ gfx::ColorRange aColorRange, KnowsCompositor* aKnowsCompositor,
+ RefPtr<IMFSampleUsageInfo> aUsageInfo);
+
+ virtual ~D3D11TextureData();
+
+ bool UpdateFromSurface(gfx::SourceSurface* aSurface) override;
+
+ bool Lock(OpenMode aMode) override;
+
+ void Unlock() override;
+
+ already_AddRefed<gfx::DrawTarget> BorrowDrawTarget() override;
+
+ TextureData* CreateSimilar(LayersIPCChannel* aAllocator,
+ LayersBackend aLayersBackend, TextureFlags aFlags,
+ TextureAllocationFlags aAllocFlags) const override;
+
+ void SyncWithObject(RefPtr<SyncObjectClient> aSyncObject) override;
+
+ ID3D11Texture2D* GetD3D11Texture() const { return mTexture; }
+
+ void Deallocate(LayersIPCChannel* aAllocator) override;
+
+ D3D11TextureData* AsD3D11TextureData() override { return this; }
+
+ TextureAllocationFlags GetTextureAllocationFlags() const {
+ return mAllocationFlags;
+ }
+
+ void FillInfo(TextureData::Info& aInfo) const override;
+
+ bool Serialize(SurfaceDescriptor& aOutDescrptor) override;
+ void GetSubDescriptor(RemoteDecoderVideoSubDescriptor* aOutDesc) override;
+
+ gfx::ColorRange GetColorRange() const { return mColorRange; }
+ void SetColorRange(gfx::ColorRange aColorRange) { mColorRange = aColorRange; }
+
+ gfx::IntSize GetSize() const { return mSize; }
+ gfx::SurfaceFormat GetSurfaceFormat() const { return mFormat; }
+
+ TextureFlags GetTextureFlags() const override;
+
+ void SetGpuProcessTextureId(GpuProcessTextureId aTextureId) {
+ mGpuProcessTextureId = Some(aTextureId);
+ }
+
+ Maybe<GpuProcessTextureId> GetGpuProcessTextureId() {
+ return mGpuProcessTextureId;
+ }
+
+ private:
+ D3D11TextureData(ID3D11Texture2D* aTexture, uint32_t aArrayIndex,
+ gfx::IntSize aSize, gfx::SurfaceFormat aFormat,
+ TextureAllocationFlags aFlags);
+
+ void GetDXGIResource(IDXGIResource** aOutResource);
+
+ bool PrepareDrawTargetInLock(OpenMode aMode);
+
+ friend class gl::GLBlitHelper;
+ bool SerializeSpecific(SurfaceDescriptorD3D10* aOutDesc);
+
+ static D3D11TextureData* Create(gfx::IntSize aSize,
+ gfx::SurfaceFormat aFormat,
+ gfx::SourceSurface* aSurface,
+ TextureAllocationFlags aAllocFlags,
+ ID3D11Device* aDevice = nullptr);
+
+ // Hold on to the DrawTarget because it is expensive to create one each
+ // ::Lock.
+ RefPtr<gfx::DrawTarget> mDrawTarget;
+ const gfx::IntSize mSize;
+ const gfx::SurfaceFormat mFormat;
+
+ public:
+ gfx::ColorSpace2 mColorSpace = gfx::ColorSpace2::SRGB;
+
+ private:
+ gfx::ColorRange mColorRange = gfx::ColorRange::LIMITED;
+ bool mNeedsClear = false;
+ const bool mHasSynchronization;
+
+ RefPtr<ID3D11Texture2D> mTexture;
+ Maybe<GpuProcessTextureId> mGpuProcessTextureId;
+ uint32_t mArrayIndex = 0;
+ const TextureAllocationFlags mAllocationFlags;
+};
+
+class DXGIYCbCrTextureData : public TextureData {
+ friend class gl::GLBlitHelper;
+
+ public:
+ static DXGIYCbCrTextureData* Create(
+ IDirect3DTexture9* aTextureY, IDirect3DTexture9* aTextureCb,
+ IDirect3DTexture9* aTextureCr, HANDLE aHandleY, HANDLE aHandleCb,
+ HANDLE aHandleCr, const gfx::IntSize& aSize, const gfx::IntSize& aSizeY,
+ const gfx::IntSize& aSizeCbCr, gfx::ColorDepth aColorDepth,
+ gfx::YUVColorSpace aYUVColorSpace, gfx::ColorRange aColorRange);
+
+ static DXGIYCbCrTextureData* Create(
+ ID3D11Texture2D* aTextureCb, ID3D11Texture2D* aTextureY,
+ ID3D11Texture2D* aTextureCr, const gfx::IntSize& aSize,
+ const gfx::IntSize& aSizeY, const gfx::IntSize& aSizeCbCr,
+ gfx::ColorDepth aColorDepth, gfx::YUVColorSpace aYUVColorSpace,
+ gfx::ColorRange aColorRange);
+
+ bool Lock(OpenMode) override { return true; }
+
+ void Unlock() override {}
+
+ void FillInfo(TextureData::Info& aInfo) const override;
+
+ void SerializeSpecific(SurfaceDescriptorDXGIYCbCr* aOutDesc);
+ bool Serialize(SurfaceDescriptor& aOutDescriptor) override;
+ void GetSubDescriptor(RemoteDecoderVideoSubDescriptor* aOutDesc) override;
+
+ already_AddRefed<gfx::DrawTarget> BorrowDrawTarget() override {
+ return nullptr;
+ }
+
+ void Deallocate(LayersIPCChannel* aAllocator) override;
+
+ bool UpdateFromSurface(gfx::SourceSurface*) override { return false; }
+
+ TextureFlags GetTextureFlags() const override;
+
+ DXGIYCbCrTextureData* AsDXGIYCbCrTextureData() override { return this; }
+
+ gfx::IntSize GetYSize() const { return mSizeY; }
+
+ gfx::IntSize GetCbCrSize() const { return mSizeCbCr; }
+
+ gfx::ColorDepth GetColorDepth() const { return mColorDepth; }
+ gfx::YUVColorSpace GetYUVColorSpace() const { return mYUVColorSpace; }
+ gfx::ColorRange GetColorRange() const { return mColorRange; }
+
+ ID3D11Texture2D* GetD3D11Texture(size_t index) {
+ return mD3D11Textures[index];
+ }
+
+ protected:
+ RefPtr<ID3D11Texture2D> mD3D11Textures[3];
+ RefPtr<IDirect3DTexture9> mD3D9Textures[3];
+ HANDLE mHandles[3];
+ gfx::IntSize mSize;
+ gfx::IntSize mSizeY;
+ gfx::IntSize mSizeCbCr;
+ gfx::ColorDepth mColorDepth;
+ gfx::YUVColorSpace mYUVColorSpace;
+ gfx::ColorRange mColorRange;
+};
+
+/**
+ * TextureSource that provides with the necessary APIs to be composited by a
+ * CompositorD3D11.
+ */
+class TextureSourceD3D11 {
+ public:
+ TextureSourceD3D11() : mFormatOverride(DXGI_FORMAT_UNKNOWN) {}
+ virtual ~TextureSourceD3D11() = default;
+
+ virtual ID3D11Texture2D* GetD3D11Texture() const { return mTexture; }
+ virtual ID3D11ShaderResourceView* GetShaderResourceView();
+
+ protected:
+ virtual gfx::IntSize GetSize() const { return mSize; }
+
+ gfx::IntSize mSize;
+ RefPtr<ID3D11Texture2D> mTexture;
+ RefPtr<ID3D11ShaderResourceView> mSRV;
+ DXGI_FORMAT mFormatOverride;
+};
+
+/**
+ * A TextureSource that implements the DataTextureSource interface.
+ * it can be used without a TextureHost and is able to upload texture data
+ * from a gfx::DataSourceSurface.
+ */
+class DataTextureSourceD3D11 : public DataTextureSource,
+ public TextureSourceD3D11,
+ public BigImageIterator {
+ public:
+ /// Constructor allowing the texture to perform texture uploads.
+ ///
+ /// The texture can be used as an actual DataTextureSource.
+ DataTextureSourceD3D11(ID3D11Device* aDevice, gfx::SurfaceFormat aFormat,
+ TextureFlags aFlags);
+
+ /// Constructor for textures created around DXGI shared handles, disallowing
+ /// texture uploads.
+ ///
+ /// The texture CANNOT be used as a DataTextureSource.
+ DataTextureSourceD3D11(ID3D11Device* aDevice, gfx::SurfaceFormat aFormat,
+ ID3D11Texture2D* aTexture);
+
+ DataTextureSourceD3D11(gfx::SurfaceFormat aFormat,
+ TextureSourceProvider* aProvider,
+ ID3D11Texture2D* aTexture);
+ DataTextureSourceD3D11(gfx::SurfaceFormat aFormat,
+ TextureSourceProvider* aProvider, TextureFlags aFlags);
+
+ virtual ~DataTextureSourceD3D11();
+
+ const char* Name() const override { return "DataTextureSourceD3D11"; }
+
+ // DataTextureSource
+
+ bool Update(gfx::DataSourceSurface* aSurface,
+ nsIntRegion* aDestRegion = nullptr,
+ gfx::IntPoint* aSrcOffset = nullptr,
+ gfx::IntPoint* aDstOffset = nullptr) override;
+
+ // TextureSource
+
+ TextureSourceD3D11* AsSourceD3D11() override { return this; }
+
+ ID3D11Texture2D* GetD3D11Texture() const override;
+
+ ID3D11ShaderResourceView* GetShaderResourceView() override;
+
+ // Returns nullptr if this texture was created by a DXGI TextureHost.
+ DataTextureSource* AsDataTextureSource() override {
+ return mAllowTextureUploads ? this : nullptr;
+ }
+
+ void DeallocateDeviceData() override { mTexture = nullptr; }
+
+ gfx::IntSize GetSize() const override { return mSize; }
+
+ gfx::SurfaceFormat GetFormat() const override { return mFormat; }
+
+ // BigImageIterator
+
+ BigImageIterator* AsBigImageIterator() override {
+ return mIsTiled ? this : nullptr;
+ }
+
+ size_t GetTileCount() override { return mTileTextures.size(); }
+
+ bool NextTile() override { return (++mCurrentTile < mTileTextures.size()); }
+
+ gfx::IntRect GetTileRect() override;
+
+ void EndBigImageIteration() override { mIterating = false; }
+
+ void BeginBigImageIteration() override {
+ mIterating = true;
+ mCurrentTile = 0;
+ }
+
+ RefPtr<TextureSource> ExtractCurrentTile() override;
+
+ void Reset();
+
+ protected:
+ gfx::IntRect GetTileRect(uint32_t aIndex) const;
+
+ std::vector<RefPtr<ID3D11Texture2D>> mTileTextures;
+ std::vector<RefPtr<ID3D11ShaderResourceView>> mTileSRVs;
+ RefPtr<ID3D11Device> mDevice;
+ gfx::SurfaceFormat mFormat;
+ TextureFlags mFlags;
+ uint32_t mCurrentTile;
+ bool mIsTiled;
+ bool mIterating;
+ // Sadly, the code was originally organized so that this class is used both in
+ // the cases where we want to perform texture uploads through the
+ // DataTextureSource interface, and the cases where we wrap the texture around
+ // an existing DXGI handle in which case we should not use it as a
+ // DataTextureSource. This member differentiates the two scenarios. When it is
+ // false the texture "pretends" to not be a DataTextureSource.
+ bool mAllowTextureUploads;
+};
+
+/**
+ * A TextureHost for shared D3D11 textures.
+ */
+class DXGITextureHostD3D11 : public TextureHost {
+ public:
+ DXGITextureHostD3D11(TextureFlags aFlags,
+ const SurfaceDescriptorD3D10& aDescriptor);
+
+ void DeallocateDeviceData() override {}
+
+ gfx::SurfaceFormat GetFormat() const override { return mFormat; }
+
+ bool LockWithoutCompositor() override;
+ void UnlockWithoutCompositor() override;
+
+ gfx::IntSize GetSize() const override { return mSize; }
+ gfx::ColorRange GetColorRange() const override { return mColorRange; }
+
+ already_AddRefed<gfx::DataSourceSurface> GetAsSurface() override;
+
+ void CreateRenderTexture(
+ const wr::ExternalImageId& aExternalImageId) override;
+
+ uint32_t NumSubTextures() override;
+
+ void PushResourceUpdates(wr::TransactionBuilder& aResources,
+ ResourceUpdateOp aOp,
+ const Range<wr::ImageKey>& aImageKeys,
+ const wr::ExternalImageId& aExtID) override;
+
+ void PushDisplayItems(wr::DisplayListBuilder& aBuilder,
+ const wr::LayoutRect& aBounds,
+ const wr::LayoutRect& aClip, wr::ImageRendering aFilter,
+ const Range<wr::ImageKey>& aImageKeys,
+ PushDisplayItemFlagSet aFlags) override;
+
+ bool SupportsExternalCompositing(WebRenderBackend aBackend) override;
+
+ protected:
+ bool LockInternal();
+ void UnlockInternal();
+
+ bool EnsureTextureSource();
+
+ RefPtr<ID3D11Device> GetDevice();
+
+ bool EnsureTexture();
+
+ RefPtr<ID3D11Device> mDevice;
+ RefPtr<ID3D11Texture2D> mTexture;
+ Maybe<GpuProcessTextureId> mGpuProcessTextureId;
+ uint32_t mArrayIndex = 0;
+ RefPtr<DataTextureSourceD3D11> mTextureSource;
+ gfx::IntSize mSize;
+ WindowsHandle mHandle;
+ gfx::SurfaceFormat mFormat;
+
+ public:
+ const gfx::ColorSpace2 mColorSpace;
+
+ protected:
+ const gfx::ColorRange mColorRange;
+ bool mIsLocked;
+};
+
+class DXGIYCbCrTextureHostD3D11 : public TextureHost {
+ public:
+ DXGIYCbCrTextureHostD3D11(TextureFlags aFlags,
+ const SurfaceDescriptorDXGIYCbCr& aDescriptor);
+
+ void DeallocateDeviceData() override {}
+
+ gfx::SurfaceFormat GetFormat() const override {
+ return gfx::SurfaceFormat::YUV;
+ }
+
+ gfx::ColorDepth GetColorDepth() const override { return mColorDepth; }
+ gfx::YUVColorSpace GetYUVColorSpace() const override {
+ return mYUVColorSpace;
+ }
+ gfx::ColorRange GetColorRange() const override { return mColorRange; }
+
+ gfx::IntSize GetSize() const override { return mSize; }
+
+ already_AddRefed<gfx::DataSourceSurface> GetAsSurface() override {
+ return nullptr;
+ }
+
+ void CreateRenderTexture(
+ const wr::ExternalImageId& aExternalImageId) override;
+
+ uint32_t NumSubTextures() override;
+
+ void PushResourceUpdates(wr::TransactionBuilder& aResources,
+ ResourceUpdateOp aOp,
+ const Range<wr::ImageKey>& aImageKeys,
+ const wr::ExternalImageId& aExtID) override;
+
+ void PushDisplayItems(wr::DisplayListBuilder& aBuilder,
+ const wr::LayoutRect& aBounds,
+ const wr::LayoutRect& aClip, wr::ImageRendering aFilter,
+ const Range<wr::ImageKey>& aImageKeys,
+ PushDisplayItemFlagSet aFlags) override;
+
+ bool SupportsExternalCompositing(WebRenderBackend aBackend) override;
+
+ private:
+ bool EnsureTextureSource();
+
+ protected:
+ RefPtr<ID3D11Device> GetDevice();
+
+ bool EnsureTexture();
+
+ RefPtr<ID3D11Texture2D> mTextures[3];
+ RefPtr<DataTextureSourceD3D11> mTextureSources[3];
+
+ gfx::IntSize mSize;
+ gfx::IntSize mSizeY;
+ gfx::IntSize mSizeCbCr;
+ WindowsHandle mHandles[3];
+ bool mIsLocked;
+ gfx::ColorDepth mColorDepth;
+ gfx::YUVColorSpace mYUVColorSpace;
+ gfx::ColorRange mColorRange;
+};
+
+class CompositingRenderTargetD3D11 : public CompositingRenderTarget,
+ public TextureSourceD3D11 {
+ public:
+ CompositingRenderTargetD3D11(
+ ID3D11Texture2D* aTexture, const gfx::IntPoint& aOrigin,
+ DXGI_FORMAT aFormatOverride = DXGI_FORMAT_UNKNOWN);
+
+ const char* Name() const override { return "CompositingRenderTargetD3D11"; }
+
+ TextureSourceD3D11* AsSourceD3D11() override { return this; }
+
+ void BindRenderTarget(ID3D11DeviceContext* aContext);
+
+ gfx::IntSize GetSize() const override;
+
+ void SetSize(const gfx::IntSize& aSize) { mSize = aSize; }
+
+ private:
+ friend class CompositorD3D11;
+ RefPtr<ID3D11RenderTargetView> mRTView;
+};
+
+class SyncObjectD3D11Host : public SyncObjectHost {
+ public:
+ explicit SyncObjectD3D11Host(ID3D11Device* aDevice);
+
+ bool Init() override;
+
+ SyncHandle GetSyncHandle() override;
+
+ bool Synchronize(bool aFallible) override;
+
+ IDXGIKeyedMutex* GetKeyedMutex() { return mKeyedMutex.get(); };
+
+ private:
+ virtual ~SyncObjectD3D11Host() = default;
+
+ SyncHandle mSyncHandle;
+ RefPtr<ID3D11Device> mDevice;
+ RefPtr<IDXGIResource> mSyncTexture;
+ RefPtr<IDXGIKeyedMutex> mKeyedMutex;
+};
+
+class SyncObjectD3D11Client : public SyncObjectClient {
+ public:
+ SyncObjectD3D11Client(SyncHandle aSyncHandle, ID3D11Device* aDevice);
+
+ bool Synchronize(bool aFallible) override;
+
+ bool IsSyncObjectValid() override;
+
+ void EnsureInitialized() override {}
+
+ SyncType GetSyncType() override { return SyncType::D3D11; }
+
+ void RegisterTexture(ID3D11Texture2D* aTexture);
+
+ protected:
+ explicit SyncObjectD3D11Client(SyncHandle aSyncHandle);
+ bool Init(ID3D11Device* aDevice, bool aFallible);
+ bool SynchronizeInternal(ID3D11Device* aDevice, bool aFallible);
+ Mutex mSyncLock MOZ_UNANNOTATED;
+ RefPtr<ID3D11Texture2D> mSyncTexture;
+ std::vector<ID3D11Texture2D*> mSyncedTextures;
+
+ private:
+ const SyncHandle mSyncHandle;
+ RefPtr<IDXGIKeyedMutex> mKeyedMutex;
+ const RefPtr<ID3D11Device> mDevice;
+};
+
+class SyncObjectD3D11ClientContentDevice : public SyncObjectD3D11Client {
+ public:
+ explicit SyncObjectD3D11ClientContentDevice(SyncHandle aSyncHandle);
+
+ bool Synchronize(bool aFallible) override;
+
+ bool IsSyncObjectValid() override;
+
+ void EnsureInitialized() override;
+
+ private:
+ RefPtr<ID3D11Device> mContentDevice;
+};
+
+inline uint32_t GetMaxTextureSizeForFeatureLevel(
+ D3D_FEATURE_LEVEL aFeatureLevel) {
+ int32_t maxTextureSize;
+ switch (aFeatureLevel) {
+ case D3D_FEATURE_LEVEL_11_1:
+ case D3D_FEATURE_LEVEL_11_0:
+ maxTextureSize = D3D11_REQ_TEXTURE2D_U_OR_V_DIMENSION;
+ break;
+ case D3D_FEATURE_LEVEL_10_1:
+ case D3D_FEATURE_LEVEL_10_0:
+ maxTextureSize = D3D10_REQ_TEXTURE2D_U_OR_V_DIMENSION;
+ break;
+ case D3D_FEATURE_LEVEL_9_3:
+ maxTextureSize = D3D_FL9_3_REQ_TEXTURE2D_U_OR_V_DIMENSION;
+ break;
+ default:
+ maxTextureSize = D3D_FL9_1_REQ_TEXTURE2D_U_OR_V_DIMENSION;
+ }
+ return maxTextureSize;
+}
+
+uint32_t GetMaxTextureSizeFromDevice(ID3D11Device* aDevice);
+void ReportTextureMemoryUsage(ID3D11Texture2D* aTexture, size_t aBytes);
+
+class AutoLockD3D11Texture {
+ public:
+ explicit AutoLockD3D11Texture(ID3D11Texture2D* aTexture);
+ ~AutoLockD3D11Texture();
+
+ private:
+ RefPtr<IDXGIKeyedMutex> mMutex;
+};
+
+class D3D11MTAutoEnter {
+ public:
+ explicit D3D11MTAutoEnter(already_AddRefed<ID3D10Multithread> aMT)
+ : mMT(aMT) {
+ if (mMT) {
+ mMT->Enter();
+ }
+ }
+ ~D3D11MTAutoEnter() {
+ if (mMT) {
+ mMT->Leave();
+ }
+ }
+
+ private:
+ RefPtr<ID3D10Multithread> mMT;
+};
+
+/**
+ * A class to manage ID3D11Texture2Ds that is shared without using shared handle
+ * in GPU process. On some GPUs, ID3D11Texture2Ds of hardware decoded video
+ * frames with zero video frame copy could not use shared handle.
+ */
+class GpuProcessD3D11TextureMap {
+ public:
+ static void Init();
+ static void Shutdown();
+ static GpuProcessD3D11TextureMap* Get() { return sInstance; }
+ static GpuProcessTextureId GetNextTextureId();
+
+ GpuProcessD3D11TextureMap();
+ ~GpuProcessD3D11TextureMap();
+
+ void Register(GpuProcessTextureId aTextureId, ID3D11Texture2D* aTexture,
+ uint32_t aArrayIndex, const gfx::IntSize& aSize,
+ RefPtr<IMFSampleUsageInfo> aUsageInfo);
+ void Unregister(GpuProcessTextureId aTextureId);
+
+ RefPtr<ID3D11Texture2D> GetTexture(GpuProcessTextureId aTextureId);
+ Maybe<HANDLE> GetSharedHandleOfCopiedTexture(GpuProcessTextureId aTextureId);
+
+ private:
+ struct TextureHolder {
+ TextureHolder(ID3D11Texture2D* aTexture, uint32_t aArrayIndex,
+ const gfx::IntSize& aSize,
+ RefPtr<IMFSampleUsageInfo> aUsageInfo);
+ TextureHolder() = default;
+
+ RefPtr<ID3D11Texture2D> mTexture;
+ uint32_t mArrayIndex = 0;
+ gfx::IntSize mSize;
+ RefPtr<IMFSampleUsageInfo> mIMFSampleUsageInfo;
+ RefPtr<ID3D11Texture2D> mCopiedTexture;
+ Maybe<HANDLE> mCopiedTextureSharedHandle;
+ };
+
+ DataMutex<std::unordered_map<GpuProcessTextureId, TextureHolder,
+ GpuProcessTextureId::HashFn>>
+ mD3D11TexturesById;
+
+ static StaticAutoPtr<GpuProcessD3D11TextureMap> sInstance;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif /* MOZILLA_GFX_TEXTURED3D11_H */
diff --git a/gfx/layers/d3d11/genshaders.py b/gfx/layers/d3d11/genshaders.py
new file mode 100644
index 0000000000..b9701978d5
--- /dev/null
+++ b/gfx/layers/d3d11/genshaders.py
@@ -0,0 +1,176 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+import argparse
+import codecs
+import locale
+import os
+import re
+import subprocess
+import sys
+import tempfile
+
+import buildconfig
+import yaml
+
+
+def shell_main():
+ parser = argparse.ArgumentParser()
+ parser.add_argument("-o", "--output", type=str, required=True, help="Output file")
+ parser.add_argument("manifest", type=str, help="Manifest source file")
+ args = parser.parse_args()
+
+ with open(args.output, "w") as out_file:
+ process_manifest(out_file, args.manifest)
+
+
+def main(output_fp, input_filename):
+ return process_manifest(output_fp, input_filename)
+
+
+HEADER = """// AUTOGENERATED - DO NOT EDIT
+namespace mozilla {
+namespace layers {
+
+struct ShaderBytes { const void* mData; size_t mLength; };
+"""
+FOOTER = """
+} // namespace layers
+} // namespace mozilla"""
+
+
+def process_manifest(output_fp, manifest_filename):
+ with codecs.open(manifest_filename, "r", "UTF-8") as in_fp:
+ manifest = yaml.safe_load(in_fp)
+ shader_folder, _ = os.path.split(manifest_filename)
+
+ output_fp.write(HEADER)
+
+ deps = set()
+ for block in manifest:
+ if "type" not in block:
+ raise Exception("Expected 'type' key with shader mode")
+ if "file" not in block:
+ raise Exception("Expected 'file' key with shader file")
+ if "shaders" not in block:
+ raise Exception("Expected 'shaders' key with shader name list")
+
+ shader_file = os.path.join(shader_folder, block["file"])
+ deps.add(shader_file)
+
+ shader_model = block["type"]
+ for shader_name in block["shaders"]:
+ new_deps = run_fxc(
+ shader_model=shader_model,
+ shader_file=shader_file,
+ shader_name=shader_name,
+ output_fp=output_fp,
+ )
+ deps |= new_deps
+
+ output_fp.write(FOOTER)
+ return deps
+
+
+def run_fxc(shader_model, shader_file, shader_name, output_fp):
+ fxc_location = buildconfig.substs["FXC"]
+
+ argv = [
+ fxc_location,
+ "-nologo",
+ "-T{0}".format(shader_model),
+ os.path.relpath(shader_file),
+ "-E{0}".format(shader_name),
+ "-Vn{0}".format(shader_name),
+ "-Vi",
+ ]
+ if "WINNT" not in buildconfig.substs["HOST_OS_ARCH"]:
+ argv.insert(0, buildconfig.substs["WINE"])
+ if shader_model.startswith("vs_"):
+ argv += ["-DVERTEX_SHADER"]
+ elif shader_model.startswith("ps_"):
+ argv += ["-DPIXEL_SHADER"]
+
+ deps = None
+ with ScopedTempFilename() as temp_filename:
+ argv += ["-Fh{0}".format(os.path.relpath(temp_filename))]
+
+ sys.stdout.write("{0}\n".format(" ".join(argv)))
+ sys.stdout.flush()
+ proc_stdout = subprocess.check_output(argv)
+ proc_stdout = decode_console_text(sys.stdout, proc_stdout)
+ deps = find_dependencies(proc_stdout)
+ assert "fxc2" in fxc_location or len(deps) > 0
+
+ with open(temp_filename, "r") as temp_fp:
+ output_fp.write(temp_fp.read())
+
+ output_fp.write("ShaderBytes s{0} = {{ {0}, sizeof({0}) }};\n".format(shader_name))
+ return deps
+
+
+def find_dependencies(fxc_output):
+ # Dependencies look like this:
+ # Resolved to [<path>]
+ #
+ # Microsoft likes to change output strings based on the user's language, so
+ # instead of pattern matching on that string, we take everything in between
+ # brackets. We filter out potentially bogus strings later.
+ deps = set()
+ for line in fxc_output.split("\n"):
+ m = re.search(r"\[([^\]]+)\]", line)
+ if m is None:
+ continue
+ dep_path = m.group(1)
+ dep_path = os.path.normpath(dep_path)
+ # When run via Wine, FXC's output contains Windows paths on the Z drive.
+ # We want to normalize them back to unix paths for the build system.
+ if "WINNT" not in buildconfig.substs[
+ "HOST_OS_ARCH"
+ ] and dep_path.lower().startswith("z:"):
+ dep_path = dep_path[2:].replace("\\", "/")
+ if os.path.isfile(dep_path):
+ deps.add(dep_path)
+ return deps
+
+
+# Python reads the raw bytes from stdout, so we need to try our best to
+# capture that as a valid Python string.
+
+
+def decode_console_text(pipe, text):
+ try:
+ if pipe.encoding:
+ return text.decode(pipe.encoding, "replace")
+ except Exception:
+ pass
+ try:
+ return text.decode(locale.getpreferredencoding(), "replace")
+ except Exception:
+ return text.decode("utf8", "replace")
+
+
+# Allocate a temporary file name and delete it when done. We need an extra
+# wrapper for this since TemporaryNamedFile holds the file open.
+
+
+class ScopedTempFilename(object):
+ def __init__(self):
+ self.name = None
+
+ def __enter__(self):
+ with tempfile.NamedTemporaryFile(dir=os.getcwd(), delete=False) as tmp:
+ self.name = tmp.name
+ return self.name
+
+ def __exit__(self, type, value, traceback):
+ if not self.name:
+ return
+ try:
+ os.unlink(self.name)
+ except Exception:
+ pass
+
+
+if __name__ == "__main__":
+ shell_main()
diff --git a/gfx/layers/d3d11/shaders.manifest b/gfx/layers/d3d11/shaders.manifest
new file mode 100644
index 0000000000..d8836d2740
--- /dev/null
+++ b/gfx/layers/d3d11/shaders.manifest
@@ -0,0 +1,13 @@
+- type: vs_4_0_level_9_3
+ file: CompositorD3D11.hlsl
+ shaders:
+ - LayerQuadVS
+
+- type: ps_4_0_level_9_3
+ file: CompositorD3D11.hlsl
+ shaders:
+ - SolidColorShader
+ - RGBShader
+ - RGBAShader
+ - YCbCrShader
+ - NV12Shader
diff --git a/gfx/layers/ipc/APZCTreeManagerChild.cpp b/gfx/layers/ipc/APZCTreeManagerChild.cpp
new file mode 100644
index 0000000000..3d51aba9f1
--- /dev/null
+++ b/gfx/layers/ipc/APZCTreeManagerChild.cpp
@@ -0,0 +1,225 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/layers/APZCTreeManagerChild.h"
+
+#include "InputData.h" // for InputData
+#include "mozilla/dom/BrowserParent.h" // for BrowserParent
+#include "mozilla/layers/APZCCallbackHelper.h" // for APZCCallbackHelper
+#include "mozilla/layers/APZInputBridgeChild.h" // for APZInputBridgeChild
+#include "mozilla/layers/GeckoContentController.h" // for GeckoContentController
+#include "mozilla/layers/RemoteCompositorSession.h" // for RemoteCompositorSession
+#ifdef MOZ_WIDGET_ANDROID
+# include "mozilla/jni/Utils.h" // for DispatchToGeckoPriorityQueue
+#endif
+
+namespace mozilla {
+namespace layers {
+
+APZCTreeManagerChild::APZCTreeManagerChild()
+ : mCompositorSession(nullptr), mIPCOpen(false) {}
+
+APZCTreeManagerChild::~APZCTreeManagerChild() = default;
+
+void APZCTreeManagerChild::SetCompositorSession(
+ RemoteCompositorSession* aSession) {
+ // Exactly one of mCompositorSession and aSession must be null (i.e. either
+ // we're setting mCompositorSession or we're clearing it).
+ MOZ_ASSERT(!mCompositorSession ^ !aSession);
+ mCompositorSession = aSession;
+}
+
+void APZCTreeManagerChild::SetInputBridge(APZInputBridgeChild* aInputBridge) {
+ // The input bridge only exists from the UI process to the GPU process.
+ MOZ_ASSERT(XRE_IsParentProcess());
+ MOZ_ASSERT(!mInputBridge);
+
+ mInputBridge = aInputBridge;
+}
+
+void APZCTreeManagerChild::Destroy() {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (mInputBridge) {
+ mInputBridge->Destroy();
+ mInputBridge = nullptr;
+ }
+}
+
+void APZCTreeManagerChild::SetKeyboardMap(const KeyboardMap& aKeyboardMap) {
+ MOZ_ASSERT(NS_IsMainThread());
+ SendSetKeyboardMap(aKeyboardMap);
+}
+
+void APZCTreeManagerChild::ZoomToRect(const ScrollableLayerGuid& aGuid,
+ const ZoomTarget& aZoomTarget,
+ const uint32_t aFlags) {
+ MOZ_ASSERT(NS_IsMainThread());
+ SendZoomToRect(aGuid, aZoomTarget, aFlags);
+}
+
+void APZCTreeManagerChild::ContentReceivedInputBlock(uint64_t aInputBlockId,
+ bool aPreventDefault) {
+ MOZ_ASSERT(NS_IsMainThread());
+ SendContentReceivedInputBlock(aInputBlockId, aPreventDefault);
+}
+
+void APZCTreeManagerChild::SetTargetAPZC(
+ uint64_t aInputBlockId, const nsTArray<ScrollableLayerGuid>& aTargets) {
+ MOZ_ASSERT(NS_IsMainThread());
+ SendSetTargetAPZC(aInputBlockId, aTargets);
+}
+
+void APZCTreeManagerChild::UpdateZoomConstraints(
+ const ScrollableLayerGuid& aGuid,
+ const Maybe<ZoomConstraints>& aConstraints) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (mIPCOpen) {
+ SendUpdateZoomConstraints(aGuid, aConstraints);
+ }
+}
+
+void APZCTreeManagerChild::SetDPI(float aDpiValue) {
+ MOZ_ASSERT(NS_IsMainThread());
+ SendSetDPI(aDpiValue);
+}
+
+void APZCTreeManagerChild::SetAllowedTouchBehavior(
+ uint64_t aInputBlockId, const nsTArray<TouchBehaviorFlags>& aValues) {
+ MOZ_ASSERT(NS_IsMainThread());
+ SendSetAllowedTouchBehavior(aInputBlockId, aValues);
+}
+
+void APZCTreeManagerChild::SetBrowserGestureResponse(
+ uint64_t aInputBlockId, BrowserGestureResponse aResponse) {
+ MOZ_ASSERT(NS_IsMainThread());
+ SendSetBrowserGestureResponse(aInputBlockId, aResponse);
+}
+
+void APZCTreeManagerChild::StartScrollbarDrag(
+ const ScrollableLayerGuid& aGuid, const AsyncDragMetrics& aDragMetrics) {
+ MOZ_ASSERT(NS_IsMainThread());
+ SendStartScrollbarDrag(aGuid, aDragMetrics);
+}
+
+bool APZCTreeManagerChild::StartAutoscroll(const ScrollableLayerGuid& aGuid,
+ const ScreenPoint& aAnchorLocation) {
+ MOZ_ASSERT(NS_IsMainThread());
+ return SendStartAutoscroll(aGuid, aAnchorLocation);
+}
+
+void APZCTreeManagerChild::StopAutoscroll(const ScrollableLayerGuid& aGuid) {
+ MOZ_ASSERT(NS_IsMainThread());
+ SendStopAutoscroll(aGuid);
+}
+
+void APZCTreeManagerChild::SetLongTapEnabled(bool aTapGestureEnabled) {
+ MOZ_ASSERT(NS_IsMainThread());
+ SendSetLongTapEnabled(aTapGestureEnabled);
+}
+
+APZInputBridge* APZCTreeManagerChild::InputBridge() {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ MOZ_ASSERT(mInputBridge);
+
+ return mInputBridge.get();
+}
+
+void APZCTreeManagerChild::AddIPDLReference() {
+ MOZ_ASSERT(mIPCOpen == false);
+ mIPCOpen = true;
+ AddRef();
+}
+
+void APZCTreeManagerChild::ReleaseIPDLReference() {
+ mIPCOpen = false;
+ Release();
+}
+
+void APZCTreeManagerChild::ActorDestroy(ActorDestroyReason aWhy) {
+ mIPCOpen = false;
+}
+
+mozilla::ipc::IPCResult APZCTreeManagerChild::RecvHandleTap(
+ const TapType& aType, const LayoutDevicePoint& aPoint,
+ const Modifiers& aModifiers, const ScrollableLayerGuid& aGuid,
+ const uint64_t& aInputBlockId) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ if (mCompositorSession &&
+ mCompositorSession->RootLayerTreeId() == aGuid.mLayersId &&
+ mCompositorSession->GetContentController()) {
+ RefPtr<GeckoContentController> controller =
+ mCompositorSession->GetContentController();
+ controller->HandleTap(aType, aPoint, aModifiers, aGuid, aInputBlockId);
+ return IPC_OK();
+ }
+ dom::BrowserParent* tab =
+ dom::BrowserParent::GetBrowserParentFromLayersId(aGuid.mLayersId);
+ if (tab) {
+#ifdef MOZ_WIDGET_ANDROID
+ // On Android, touch events are dispatched from the UI thread to the main
+ // thread using the Android priority queue. It is possible that this tap has
+ // made it to the GPU process and back before they have been processed. We
+ // must therefore dispatch this message to the same queue, otherwise the tab
+ // may receive the tap event before the touch events that synthesized it.
+ mozilla::jni::DispatchToGeckoPriorityQueue(
+ NewRunnableMethod<TapType, LayoutDevicePoint, Modifiers,
+ ScrollableLayerGuid, uint64_t>(
+ "dom::BrowserParent::SendHandleTap", tab,
+ &dom::BrowserParent::SendHandleTap, aType, aPoint, aModifiers,
+ aGuid, aInputBlockId));
+#else
+ tab->SendHandleTap(aType, aPoint, aModifiers, aGuid, aInputBlockId);
+#endif
+ }
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult APZCTreeManagerChild::RecvNotifyPinchGesture(
+ const PinchGestureType& aType, const ScrollableLayerGuid& aGuid,
+ const LayoutDevicePoint& aFocusPoint, const LayoutDeviceCoord& aSpanChange,
+ const Modifiers& aModifiers) {
+ // This will only get sent from the GPU process to the parent process, so
+ // this function should never get called in the content process.
+ MOZ_ASSERT(XRE_IsParentProcess());
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // We want to handle it in this process regardless of what the target guid
+ // of the pinch is. This may change in the future.
+ if (mCompositorSession && mCompositorSession->GetWidget()) {
+ APZCCallbackHelper::NotifyPinchGesture(aType, aFocusPoint, aSpanChange,
+ aModifiers,
+ mCompositorSession->GetWidget());
+ }
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult APZCTreeManagerChild::RecvCancelAutoscroll(
+ const ScrollableLayerGuid::ViewID& aScrollId) {
+ // This will only get sent from the GPU process to the parent process, so
+ // this function should never get called in the content process.
+ MOZ_ASSERT(XRE_IsParentProcess());
+ MOZ_ASSERT(NS_IsMainThread());
+
+ APZCCallbackHelper::CancelAutoscroll(aScrollId);
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult APZCTreeManagerChild::RecvNotifyScaleGestureComplete(
+ const ScrollableLayerGuid::ViewID& aScrollId, float aScale) {
+ // This will only get sent from the GPU process to the parent process, so
+ // this function should never get called in the content process.
+ MOZ_ASSERT(XRE_IsParentProcess());
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (mCompositorSession && mCompositorSession->GetWidget()) {
+ APZCCallbackHelper::NotifyScaleGestureComplete(
+ mCompositorSession->GetWidget(), aScale);
+ }
+ return IPC_OK();
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/ipc/APZCTreeManagerChild.h b/gfx/layers/ipc/APZCTreeManagerChild.h
new file mode 100644
index 0000000000..b92e322dea
--- /dev/null
+++ b/gfx/layers/ipc/APZCTreeManagerChild.h
@@ -0,0 +1,105 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_APZCTreeManagerChild_h
+#define mozilla_layers_APZCTreeManagerChild_h
+
+#include "mozilla/layers/APZInputBridge.h"
+#include "mozilla/layers/IAPZCTreeManager.h"
+#include "mozilla/layers/PAPZCTreeManagerChild.h"
+
+#include <unordered_map>
+
+namespace mozilla {
+namespace layers {
+
+class APZInputBridgeChild;
+class RemoteCompositorSession;
+
+class APZCTreeManagerChild : public IAPZCTreeManager,
+ public PAPZCTreeManagerChild {
+ friend class PAPZCTreeManagerChild;
+ using TapType = GeckoContentController_TapType;
+
+ public:
+ APZCTreeManagerChild();
+
+ void SetCompositorSession(RemoteCompositorSession* aSession);
+ void SetInputBridge(APZInputBridgeChild* aInputBridge);
+ void Destroy();
+
+ void SetKeyboardMap(const KeyboardMap& aKeyboardMap) override;
+
+ void ZoomToRect(const ScrollableLayerGuid& aGuid,
+ const ZoomTarget& aZoomTarget,
+ const uint32_t aFlags = DEFAULT_BEHAVIOR) override;
+
+ void ContentReceivedInputBlock(uint64_t aInputBlockId,
+ bool aPreventDefault) override;
+
+ void SetTargetAPZC(uint64_t aInputBlockId,
+ const nsTArray<ScrollableLayerGuid>& aTargets) override;
+
+ void UpdateZoomConstraints(
+ const ScrollableLayerGuid& aGuid,
+ const Maybe<ZoomConstraints>& aConstraints) override;
+
+ void SetDPI(float aDpiValue) override;
+
+ void SetAllowedTouchBehavior(
+ uint64_t aInputBlockId,
+ const nsTArray<TouchBehaviorFlags>& aValues) override;
+
+ void SetBrowserGestureResponse(uint64_t aInputBlockId,
+ BrowserGestureResponse aResponse) override;
+
+ void StartScrollbarDrag(const ScrollableLayerGuid& aGuid,
+ const AsyncDragMetrics& aDragMetrics) override;
+
+ bool StartAutoscroll(const ScrollableLayerGuid& aGuid,
+ const ScreenPoint& aAnchorLocation) override;
+
+ void StopAutoscroll(const ScrollableLayerGuid& aGuid) override;
+
+ void SetLongTapEnabled(bool aTapGestureEnabled) override;
+
+ APZInputBridge* InputBridge() override;
+
+ void AddIPDLReference();
+ void ReleaseIPDLReference();
+ void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ protected:
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY
+ mozilla::ipc::IPCResult RecvHandleTap(const TapType& aType,
+ const LayoutDevicePoint& aPoint,
+ const Modifiers& aModifiers,
+ const ScrollableLayerGuid& aGuid,
+ const uint64_t& aInputBlockId);
+
+ mozilla::ipc::IPCResult RecvNotifyPinchGesture(
+ const PinchGestureType& aType, const ScrollableLayerGuid& aGuid,
+ const LayoutDevicePoint& aFocusPoint,
+ const LayoutDeviceCoord& aSpanChange, const Modifiers& aModifiers);
+
+ mozilla::ipc::IPCResult RecvCancelAutoscroll(
+ const ScrollableLayerGuid::ViewID& aScrollId);
+
+ mozilla::ipc::IPCResult RecvNotifyScaleGestureComplete(
+ const ScrollableLayerGuid::ViewID& aScrollId, float aScale);
+
+ virtual ~APZCTreeManagerChild();
+
+ private:
+ MOZ_NON_OWNING_REF RemoteCompositorSession* mCompositorSession;
+ RefPtr<APZInputBridgeChild> mInputBridge;
+ bool mIPCOpen;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_APZCTreeManagerChild_h
diff --git a/gfx/layers/ipc/APZCTreeManagerParent.cpp b/gfx/layers/ipc/APZCTreeManagerParent.cpp
new file mode 100644
index 0000000000..ff05acd083
--- /dev/null
+++ b/gfx/layers/ipc/APZCTreeManagerParent.cpp
@@ -0,0 +1,199 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/layers/APZCTreeManagerParent.h"
+
+#include "apz/src/APZCTreeManager.h"
+#include "mozilla/layers/APZThreadUtils.h"
+#include "mozilla/layers/APZUpdater.h"
+#include "mozilla/layers/CompositorBridgeParent.h"
+#include "mozilla/layers/CompositorThread.h"
+#include "nsThreadUtils.h"
+
+namespace mozilla {
+namespace layers {
+
+APZCTreeManagerParent::APZCTreeManagerParent(
+ LayersId aLayersId, RefPtr<APZCTreeManager> aAPZCTreeManager,
+ RefPtr<APZUpdater> aAPZUpdater)
+ : mLayersId(aLayersId),
+ mTreeManager(std::move(aAPZCTreeManager)),
+ mUpdater(std::move(aAPZUpdater)) {
+ MOZ_ASSERT(mTreeManager != nullptr);
+ MOZ_ASSERT(mUpdater != nullptr);
+ MOZ_ASSERT(mUpdater->HasTreeManager(mTreeManager));
+}
+
+APZCTreeManagerParent::~APZCTreeManagerParent() = default;
+
+void APZCTreeManagerParent::ChildAdopted(
+ RefPtr<APZCTreeManager> aAPZCTreeManager, RefPtr<APZUpdater> aAPZUpdater) {
+ MOZ_ASSERT(aAPZCTreeManager != nullptr);
+ MOZ_ASSERT(aAPZUpdater != nullptr);
+ MOZ_ASSERT(aAPZUpdater->HasTreeManager(aAPZCTreeManager));
+ mTreeManager = std::move(aAPZCTreeManager);
+ mUpdater = std::move(aAPZUpdater);
+}
+
+mozilla::ipc::IPCResult APZCTreeManagerParent::RecvSetKeyboardMap(
+ const KeyboardMap& aKeyboardMap) {
+ mUpdater->RunOnUpdaterThread(
+ mLayersId, NewRunnableMethod<KeyboardMap>(
+ "layers::IAPZCTreeManager::SetKeyboardMap", mTreeManager,
+ &IAPZCTreeManager::SetKeyboardMap, aKeyboardMap));
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult APZCTreeManagerParent::RecvZoomToRect(
+ const ScrollableLayerGuid& aGuid, const ZoomTarget& aZoomTarget,
+ const uint32_t& aFlags) {
+ if (!IsGuidValid(aGuid)) {
+ return IPC_FAIL_NO_REASON(this);
+ }
+
+ mUpdater->RunOnUpdaterThread(
+ aGuid.mLayersId,
+ NewRunnableMethod<ScrollableLayerGuid, ZoomTarget, uint32_t>(
+ "layers::IAPZCTreeManager::ZoomToRect", mTreeManager,
+ &IAPZCTreeManager::ZoomToRect, aGuid, aZoomTarget, aFlags));
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult APZCTreeManagerParent::RecvContentReceivedInputBlock(
+ const uint64_t& aInputBlockId, const bool& aPreventDefault) {
+ mUpdater->RunOnUpdaterThread(
+ mLayersId, NewRunnableMethod<uint64_t, bool>(
+ "layers::IAPZCTreeManager::ContentReceivedInputBlock",
+ mTreeManager, &IAPZCTreeManager::ContentReceivedInputBlock,
+ aInputBlockId, aPreventDefault));
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult APZCTreeManagerParent::RecvSetTargetAPZC(
+ const uint64_t& aInputBlockId, nsTArray<ScrollableLayerGuid>&& aTargets) {
+ mUpdater->RunOnUpdaterThread(
+ mLayersId,
+ NewRunnableMethod<uint64_t,
+ StoreCopyPassByRRef<nsTArray<ScrollableLayerGuid>>>(
+ "layers::IAPZCTreeManager::SetTargetAPZC", mTreeManager,
+ &IAPZCTreeManager::SetTargetAPZC, aInputBlockId,
+ std::move(aTargets)));
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult APZCTreeManagerParent::RecvUpdateZoomConstraints(
+ const ScrollableLayerGuid& aGuid,
+ const Maybe<ZoomConstraints>& aConstraints) {
+ if (!IsGuidValid(aGuid)) {
+ return IPC_FAIL_NO_REASON(this);
+ }
+
+ mTreeManager->UpdateZoomConstraints(aGuid, aConstraints);
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult APZCTreeManagerParent::RecvSetDPI(
+ const float& aDpiValue) {
+ mUpdater->RunOnUpdaterThread(
+ mLayersId,
+ NewRunnableMethod<float>("layers::IAPZCTreeManager::SetDPI", mTreeManager,
+ &IAPZCTreeManager::SetDPI, aDpiValue));
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult APZCTreeManagerParent::RecvSetAllowedTouchBehavior(
+ const uint64_t& aInputBlockId, nsTArray<TouchBehaviorFlags>&& aValues) {
+ mUpdater->RunOnUpdaterThread(
+ mLayersId,
+ NewRunnableMethod<uint64_t,
+ StoreCopyPassByRRef<nsTArray<TouchBehaviorFlags>>>(
+ "layers::IAPZCTreeManager::SetAllowedTouchBehavior", mTreeManager,
+ &IAPZCTreeManager::SetAllowedTouchBehavior, aInputBlockId,
+ std::move(aValues)));
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult APZCTreeManagerParent::RecvSetBrowserGestureResponse(
+ const uint64_t& aInputBlockId, const BrowserGestureResponse& aResponse) {
+ mUpdater->RunOnUpdaterThread(
+ mLayersId, NewRunnableMethod<uint64_t, BrowserGestureResponse>(
+ "layers::IAPZCTreeManager::SetBrowserGestureResponse",
+ mTreeManager, &IAPZCTreeManager::SetBrowserGestureResponse,
+ aInputBlockId, aResponse));
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult APZCTreeManagerParent::RecvStartScrollbarDrag(
+ const ScrollableLayerGuid& aGuid, const AsyncDragMetrics& aDragMetrics) {
+ if (!IsGuidValid(aGuid)) {
+ return IPC_FAIL_NO_REASON(this);
+ }
+
+ mUpdater->RunOnUpdaterThread(
+ aGuid.mLayersId,
+ NewRunnableMethod<ScrollableLayerGuid, AsyncDragMetrics>(
+ "layers::IAPZCTreeManager::StartScrollbarDrag", mTreeManager,
+ &IAPZCTreeManager::StartScrollbarDrag, aGuid, aDragMetrics));
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult APZCTreeManagerParent::RecvStartAutoscroll(
+ const ScrollableLayerGuid& aGuid, const ScreenPoint& aAnchorLocation) {
+ // Unlike RecvStartScrollbarDrag(), this message comes from the parent
+ // process (via nsBaseWidget::mAPZC) rather than from the child process
+ // (via BrowserChild::mApzcTreeManager), so there is no need to check the
+ // layers id against mLayersId (and in any case, it wouldn't match, because
+ // mLayersId stores the parent process's layers id, while nsBaseWidget is
+ // sending the child process's layers id).
+
+ mUpdater->RunOnControllerThread(
+ mLayersId,
+ NewRunnableMethod<ScrollableLayerGuid, ScreenPoint>(
+ "layers::IAPZCTreeManager::StartAutoscroll", mTreeManager,
+ &IAPZCTreeManager::StartAutoscroll, aGuid, aAnchorLocation));
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult APZCTreeManagerParent::RecvStopAutoscroll(
+ const ScrollableLayerGuid& aGuid) {
+ // See RecvStartAutoscroll() for why we don't check the layers id.
+
+ mUpdater->RunOnControllerThread(
+ mLayersId, NewRunnableMethod<ScrollableLayerGuid>(
+ "layers::IAPZCTreeManager::StopAutoscroll", mTreeManager,
+ &IAPZCTreeManager::StopAutoscroll, aGuid));
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult APZCTreeManagerParent::RecvSetLongTapEnabled(
+ const bool& aLongTapEnabled) {
+ mUpdater->RunOnUpdaterThread(
+ mLayersId,
+ NewRunnableMethod<bool>(
+ "layers::IAPZCTreeManager::SetLongTapEnabled", mTreeManager,
+ &IAPZCTreeManager::SetLongTapEnabled, aLongTapEnabled));
+
+ return IPC_OK();
+}
+
+bool APZCTreeManagerParent::IsGuidValid(const ScrollableLayerGuid& aGuid) {
+ if (aGuid.mLayersId != mLayersId) {
+ NS_ERROR("Unexpected layers id");
+ return false;
+ }
+ return true;
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/ipc/APZCTreeManagerParent.h b/gfx/layers/ipc/APZCTreeManagerParent.h
new file mode 100644
index 0000000000..c75091c796
--- /dev/null
+++ b/gfx/layers/ipc/APZCTreeManagerParent.h
@@ -0,0 +1,81 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_APZCTreeManagerParent_h
+#define mozilla_layers_APZCTreeManagerParent_h
+
+#include "mozilla/layers/PAPZCTreeManagerParent.h"
+
+namespace mozilla {
+namespace layers {
+
+class APZCTreeManager;
+class APZUpdater;
+
+class APZCTreeManagerParent : public PAPZCTreeManagerParent {
+ public:
+ APZCTreeManagerParent(LayersId aLayersId,
+ RefPtr<APZCTreeManager> aAPZCTreeManager,
+ RefPtr<APZUpdater> mAPZUpdater);
+ virtual ~APZCTreeManagerParent();
+
+ LayersId GetLayersId() const { return mLayersId; }
+
+ /**
+ * Called when the layer tree that this protocol is connected to
+ * is adopted by another compositor, and we need to switch APZCTreeManagers.
+ */
+ void ChildAdopted(RefPtr<APZCTreeManager> aAPZCTreeManager,
+ RefPtr<APZUpdater> aAPZUpdater);
+
+ mozilla::ipc::IPCResult RecvSetKeyboardMap(const KeyboardMap& aKeyboardMap);
+
+ mozilla::ipc::IPCResult RecvZoomToRect(const ScrollableLayerGuid& aGuid,
+ const ZoomTarget& aZoomTarget,
+ const uint32_t& aFlags);
+
+ mozilla::ipc::IPCResult RecvContentReceivedInputBlock(
+ const uint64_t& aInputBlockId, const bool& aPreventDefault);
+
+ mozilla::ipc::IPCResult RecvSetTargetAPZC(
+ const uint64_t& aInputBlockId, nsTArray<ScrollableLayerGuid>&& aTargets);
+
+ mozilla::ipc::IPCResult RecvUpdateZoomConstraints(
+ const ScrollableLayerGuid& aGuid,
+ const Maybe<ZoomConstraints>& aConstraints);
+
+ mozilla::ipc::IPCResult RecvSetDPI(const float& aDpiValue);
+
+ mozilla::ipc::IPCResult RecvSetAllowedTouchBehavior(
+ const uint64_t& aInputBlockId, nsTArray<TouchBehaviorFlags>&& aValues);
+
+ mozilla::ipc::IPCResult RecvSetBrowserGestureResponse(
+ const uint64_t& aInputBlockId, const BrowserGestureResponse& aResponse);
+
+ mozilla::ipc::IPCResult RecvStartScrollbarDrag(
+ const ScrollableLayerGuid& aGuid, const AsyncDragMetrics& aDragMetrics);
+
+ mozilla::ipc::IPCResult RecvStartAutoscroll(
+ const ScrollableLayerGuid& aGuid, const ScreenPoint& aAnchorLocation);
+
+ mozilla::ipc::IPCResult RecvStopAutoscroll(const ScrollableLayerGuid& aGuid);
+
+ mozilla::ipc::IPCResult RecvSetLongTapEnabled(const bool& aTapGestureEnabled);
+
+ void ActorDestroy(ActorDestroyReason aWhy) override {}
+
+ private:
+ bool IsGuidValid(const ScrollableLayerGuid& aGuid);
+
+ LayersId mLayersId;
+ RefPtr<APZCTreeManager> mTreeManager;
+ RefPtr<APZUpdater> mUpdater;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_APZCTreeManagerParent_h
diff --git a/gfx/layers/ipc/APZChild.cpp b/gfx/layers/ipc/APZChild.cpp
new file mode 100644
index 0000000000..00146ac59a
--- /dev/null
+++ b/gfx/layers/ipc/APZChild.cpp
@@ -0,0 +1,113 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/layers/APZChild.h"
+#include "mozilla/layers/GeckoContentController.h"
+
+#include "mozilla/dom/BrowserChild.h"
+#include "mozilla/layers/APZCCallbackHelper.h"
+
+#include "InputData.h" // for InputData
+
+namespace mozilla {
+namespace layers {
+
+APZChild::APZChild(RefPtr<GeckoContentController> aController)
+ : mController(aController) {
+ MOZ_ASSERT(mController);
+}
+
+APZChild::~APZChild() {
+ if (mAPZTaskRunnable) {
+ mAPZTaskRunnable->Revoke();
+ mAPZTaskRunnable = nullptr;
+ }
+ if (mController) {
+ mController->Destroy();
+ mController = nullptr;
+ }
+}
+
+mozilla::ipc::IPCResult APZChild::RecvLayerTransforms(
+ nsTArray<MatrixMessage>&& aTransforms) {
+ mController->NotifyLayerTransforms(std::move(aTransforms));
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult APZChild::RecvRequestContentRepaint(
+ const RepaintRequest& aRequest) {
+ MOZ_ASSERT(mController->IsRepaintThread());
+
+ EnsureAPZTaskRunnable();
+
+ mAPZTaskRunnable->QueueRequest(aRequest);
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult APZChild::RecvUpdateOverscrollVelocity(
+ const ScrollableLayerGuid& aGuid, const float& aX, const float& aY,
+ const bool& aIsRootContent) {
+ mController->UpdateOverscrollVelocity(aGuid, aX, aY, aIsRootContent);
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult APZChild::RecvUpdateOverscrollOffset(
+ const ScrollableLayerGuid& aGuid, const float& aX, const float& aY,
+ const bool& aIsRootContent) {
+ mController->UpdateOverscrollOffset(aGuid, aX, aY, aIsRootContent);
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult APZChild::RecvNotifyMozMouseScrollEvent(
+ const ViewID& aScrollId, const nsString& aEvent) {
+ mController->NotifyMozMouseScrollEvent(aScrollId, aEvent);
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult APZChild::RecvNotifyAPZStateChange(
+ const ScrollableLayerGuid& aGuid, const APZStateChange& aChange,
+ const int& aArg, Maybe<uint64_t> aInputBlockId) {
+ mController->NotifyAPZStateChange(aGuid, aChange, aArg, aInputBlockId);
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult APZChild::RecvNotifyFlushComplete() {
+ MOZ_ASSERT(mController->IsRepaintThread());
+ EnsureAPZTaskRunnable();
+
+ mAPZTaskRunnable->QueueFlushCompleteNotification();
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult APZChild::RecvNotifyAsyncScrollbarDragInitiated(
+ const uint64_t& aDragBlockId, const ViewID& aScrollId,
+ const ScrollDirection& aDirection) {
+ mController->NotifyAsyncScrollbarDragInitiated(aDragBlockId, aScrollId,
+ aDirection);
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult APZChild::RecvNotifyAsyncScrollbarDragRejected(
+ const ViewID& aScrollId) {
+ mController->NotifyAsyncScrollbarDragRejected(aScrollId);
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult APZChild::RecvNotifyAsyncAutoscrollRejected(
+ const ViewID& aScrollId) {
+ mController->NotifyAsyncAutoscrollRejected(aScrollId);
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult APZChild::RecvDestroy() {
+ // mController->Destroy will be called in the destructor
+ PAPZChild::Send__delete__(this);
+ return IPC_OK();
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/ipc/APZChild.h b/gfx/layers/ipc/APZChild.h
new file mode 100644
index 0000000000..dfb3216127
--- /dev/null
+++ b/gfx/layers/ipc/APZChild.h
@@ -0,0 +1,79 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_APZChild_h
+#define mozilla_layers_APZChild_h
+
+#include "mozilla/layers/PAPZChild.h"
+#include "mozilla/layers/APZTaskRunnable.h"
+
+namespace mozilla {
+namespace layers {
+
+class GeckoContentController;
+
+/**
+ * APZChild implements PAPZChild and is used to remote a GeckoContentController
+ * that lives in a different process than where APZ lives.
+ */
+class APZChild final : public PAPZChild {
+ public:
+ using APZStateChange = GeckoContentController_APZStateChange;
+
+ explicit APZChild(RefPtr<GeckoContentController> aController);
+ virtual ~APZChild();
+
+ mozilla::ipc::IPCResult RecvLayerTransforms(
+ nsTArray<MatrixMessage>&& aTransforms);
+
+ mozilla::ipc::IPCResult RecvRequestContentRepaint(
+ const RepaintRequest& aRequest);
+
+ mozilla::ipc::IPCResult RecvUpdateOverscrollVelocity(
+ const ScrollableLayerGuid& aGuid, const float& aX, const float& aY,
+ const bool& aIsRootContent);
+
+ mozilla::ipc::IPCResult RecvUpdateOverscrollOffset(
+ const ScrollableLayerGuid& aGuid, const float& aX, const float& aY,
+ const bool& aIsRootContent);
+
+ mozilla::ipc::IPCResult RecvNotifyMozMouseScrollEvent(const ViewID& aScrollId,
+ const nsString& aEvent);
+
+ mozilla::ipc::IPCResult RecvNotifyAPZStateChange(
+ const ScrollableLayerGuid& aGuid, const APZStateChange& aChange,
+ const int& aArg, Maybe<uint64_t> aInputBlockId);
+
+ mozilla::ipc::IPCResult RecvNotifyFlushComplete();
+
+ mozilla::ipc::IPCResult RecvNotifyAsyncScrollbarDragInitiated(
+ const uint64_t& aDragBlockId, const ViewID& aScrollId,
+ const ScrollDirection& aDirection);
+ mozilla::ipc::IPCResult RecvNotifyAsyncScrollbarDragRejected(
+ const ViewID& aScrollId);
+
+ mozilla::ipc::IPCResult RecvNotifyAsyncAutoscrollRejected(
+ const ViewID& aScrollId);
+
+ mozilla::ipc::IPCResult RecvDestroy();
+
+ private:
+ void EnsureAPZTaskRunnable() {
+ if (!mAPZTaskRunnable) {
+ mAPZTaskRunnable = new APZTaskRunnable(mController);
+ }
+ }
+
+ RefPtr<GeckoContentController> mController;
+ // A runnable invoked in a nsRefreshDriver's tick to update multiple
+ // RepaintRequests and notify a "apz-repaints-flushed" at the same time.
+ RefPtr<APZTaskRunnable> mAPZTaskRunnable;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_APZChild_h
diff --git a/gfx/layers/ipc/APZInputBridgeChild.cpp b/gfx/layers/ipc/APZInputBridgeChild.cpp
new file mode 100644
index 0000000000..bf059143ec
--- /dev/null
+++ b/gfx/layers/ipc/APZInputBridgeChild.cpp
@@ -0,0 +1,211 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/layers/APZInputBridgeChild.h"
+
+#include "InputData.h" // for InputData, etc
+#include "mozilla/gfx/GPUProcessManager.h"
+#include "mozilla/ipc/Endpoint.h"
+#include "mozilla/layers/APZThreadUtils.h"
+#include "mozilla/layers/SynchronousTask.h"
+
+namespace mozilla {
+namespace layers {
+
+/* static */
+RefPtr<APZInputBridgeChild> APZInputBridgeChild::Create(
+ const uint64_t& aProcessToken, Endpoint<PAPZInputBridgeChild>&& aEndpoint) {
+ RefPtr<APZInputBridgeChild> child = new APZInputBridgeChild(aProcessToken);
+
+ MOZ_ASSERT(APZThreadUtils::IsControllerThreadAlive());
+
+ APZThreadUtils::RunOnControllerThread(
+ NewRunnableMethod<Endpoint<PAPZInputBridgeChild>&&>(
+ "layers::APZInputBridgeChild::Open", child,
+ &APZInputBridgeChild::Open, std::move(aEndpoint)));
+
+ return child;
+}
+
+APZInputBridgeChild::APZInputBridgeChild(const uint64_t& aProcessToken)
+ : mIsOpen(false), mProcessToken(aProcessToken) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ MOZ_ASSERT(NS_IsMainThread());
+}
+
+APZInputBridgeChild::~APZInputBridgeChild() = default;
+
+void APZInputBridgeChild::Open(Endpoint<PAPZInputBridgeChild>&& aEndpoint) {
+ APZThreadUtils::AssertOnControllerThread();
+
+ mIsOpen = aEndpoint.Bind(this);
+
+ if (!mIsOpen) {
+ // The GPU Process Manager might be gone if we receive ActorDestroy very
+ // late in shutdown.
+ if (gfx::GPUProcessManager* gpm = gfx::GPUProcessManager::Get()) {
+ gpm->NotifyRemoteActorDestroyed(mProcessToken);
+ }
+ return;
+ }
+}
+
+void APZInputBridgeChild::Destroy() {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // Destroy will get called from the main thread, so we must synchronously
+ // dispatch to the controller thread to close the bridge.
+ layers::SynchronousTask task("layers::APZInputBridgeChild::Destroy");
+ APZThreadUtils::RunOnControllerThread(
+ NS_NewRunnableFunction("layers::APZInputBridgeChild::Destroy", [&]() {
+ APZThreadUtils::AssertOnControllerThread();
+ AutoCompleteTask complete(&task);
+
+ // Clear the process token so that we don't notify the GPUProcessManager
+ // about an abnormal shutdown, thereby tearing down the GPU process.
+ mProcessToken = 0;
+
+ if (mIsOpen) {
+ PAPZInputBridgeChild::Close();
+ mIsOpen = false;
+ }
+ }));
+
+ task.Wait();
+}
+
+void APZInputBridgeChild::ActorDestroy(ActorDestroyReason aWhy) {
+ mIsOpen = false;
+
+ if (mProcessToken) {
+ gfx::GPUProcessManager::Get()->NotifyRemoteActorDestroyed(mProcessToken);
+ mProcessToken = 0;
+ }
+}
+
+APZEventResult APZInputBridgeChild::ReceiveInputEvent(
+ InputData& aEvent, InputBlockCallback&& aCallback) {
+ MOZ_ASSERT(mIsOpen);
+ APZThreadUtils::AssertOnControllerThread();
+
+ APZEventResult res;
+ switch (aEvent.mInputType) {
+ case MULTITOUCH_INPUT: {
+ MultiTouchInput& event = aEvent.AsMultiTouchInput();
+ MultiTouchInput processedEvent;
+
+ SendReceiveMultiTouchInputEvent(event, !!aCallback, &res,
+ &processedEvent);
+
+ event = processedEvent;
+ break;
+ }
+ case MOUSE_INPUT: {
+ MouseInput& event = aEvent.AsMouseInput();
+ MouseInput processedEvent;
+
+ SendReceiveMouseInputEvent(event, !!aCallback, &res, &processedEvent);
+
+ event = processedEvent;
+ break;
+ }
+ case PANGESTURE_INPUT: {
+ PanGestureInput& event = aEvent.AsPanGestureInput();
+ PanGestureInput processedEvent;
+
+ SendReceivePanGestureInputEvent(event, !!aCallback, &res,
+ &processedEvent);
+
+ event = processedEvent;
+ break;
+ }
+ case PINCHGESTURE_INPUT: {
+ PinchGestureInput& event = aEvent.AsPinchGestureInput();
+ PinchGestureInput processedEvent;
+
+ SendReceivePinchGestureInputEvent(event, !!aCallback, &res,
+ &processedEvent);
+
+ event = processedEvent;
+ break;
+ }
+ case TAPGESTURE_INPUT: {
+ TapGestureInput& event = aEvent.AsTapGestureInput();
+ TapGestureInput processedEvent;
+
+ SendReceiveTapGestureInputEvent(event, !!aCallback, &res,
+ &processedEvent);
+
+ event = processedEvent;
+ break;
+ }
+ case SCROLLWHEEL_INPUT: {
+ ScrollWheelInput& event = aEvent.AsScrollWheelInput();
+ ScrollWheelInput processedEvent;
+
+ SendReceiveScrollWheelInputEvent(event, !!aCallback, &res,
+ &processedEvent);
+
+ event = processedEvent;
+ break;
+ }
+ case KEYBOARD_INPUT: {
+ KeyboardInput& event = aEvent.AsKeyboardInput();
+ KeyboardInput processedEvent;
+
+ SendReceiveKeyboardInputEvent(event, !!aCallback, &res, &processedEvent);
+
+ event = processedEvent;
+ break;
+ }
+ default: {
+ MOZ_ASSERT_UNREACHABLE("Invalid InputData type.");
+ res.SetStatusAsConsumeNoDefault();
+ break;
+ }
+ }
+
+ if (aCallback && res.WillHaveDelayedResult()) {
+ mInputBlockCallbacks.emplace(res.mInputBlockId, std::move(aCallback));
+ }
+
+ return res;
+}
+
+mozilla::ipc::IPCResult APZInputBridgeChild::RecvCallInputBlockCallback(
+ uint64_t aInputBlockId, const APZHandledResult& aHandledResult) {
+ auto it = mInputBlockCallbacks.find(aInputBlockId);
+ if (it != mInputBlockCallbacks.end()) {
+ it->second(aInputBlockId, aHandledResult);
+ // The callback is one-shot; discard it after calling it.
+ mInputBlockCallbacks.erase(it);
+ }
+
+ return IPC_OK();
+}
+
+void APZInputBridgeChild::ProcessUnhandledEvent(
+ LayoutDeviceIntPoint* aRefPoint, ScrollableLayerGuid* aOutTargetGuid,
+ uint64_t* aOutFocusSequenceNumber, LayersId* aOutLayersId) {
+ MOZ_ASSERT(mIsOpen);
+ APZThreadUtils::AssertOnControllerThread();
+
+ SendProcessUnhandledEvent(*aRefPoint, aRefPoint, aOutTargetGuid,
+ aOutFocusSequenceNumber, aOutLayersId);
+}
+
+void APZInputBridgeChild::UpdateWheelTransaction(
+ LayoutDeviceIntPoint aRefPoint, EventMessage aEventMessage,
+ const Maybe<ScrollableLayerGuid>& aTargetGuid) {
+ MOZ_ASSERT(mIsOpen);
+ APZThreadUtils::AssertOnControllerThread();
+
+ SendUpdateWheelTransaction(aRefPoint, aEventMessage, aTargetGuid);
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/ipc/APZInputBridgeChild.h b/gfx/layers/ipc/APZInputBridgeChild.h
new file mode 100644
index 0000000000..dd77d95f36
--- /dev/null
+++ b/gfx/layers/ipc/APZInputBridgeChild.h
@@ -0,0 +1,62 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_APZInputBridgeChild_h
+#define mozilla_layers_APZInputBridgeChild_h
+
+#include "mozilla/layers/APZInputBridge.h"
+#include "mozilla/layers/PAPZInputBridgeChild.h"
+
+namespace mozilla {
+namespace layers {
+
+class APZInputBridgeChild : public PAPZInputBridgeChild, public APZInputBridge {
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(APZInputBridgeChild, final)
+
+ public:
+ static RefPtr<APZInputBridgeChild> Create(
+ const uint64_t& aProcessToken,
+ Endpoint<PAPZInputBridgeChild>&& aEndpoint);
+
+ void Destroy();
+
+ APZEventResult ReceiveInputEvent(
+ InputData& aEvent,
+ InputBlockCallback&& aCallback = InputBlockCallback()) override;
+
+ mozilla::ipc::IPCResult RecvCallInputBlockCallback(
+ uint64_t aInputBlockId, const APZHandledResult& handledResult);
+
+ protected:
+ void ProcessUnhandledEvent(LayoutDeviceIntPoint* aRefPoint,
+ ScrollableLayerGuid* aOutTargetGuid,
+ uint64_t* aOutFocusSequenceNumber,
+ LayersId* aOutLayersId) override;
+
+ void UpdateWheelTransaction(
+ LayoutDeviceIntPoint aRefPoint, EventMessage aEventMessage,
+ const Maybe<ScrollableLayerGuid>& aTargetGuid) override;
+
+ void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ explicit APZInputBridgeChild(const uint64_t& aProcessToken);
+ virtual ~APZInputBridgeChild();
+
+ private:
+ void Open(Endpoint<PAPZInputBridgeChild>&& aEndpoint);
+
+ bool mIsOpen;
+ uint64_t mProcessToken;
+
+ using InputBlockCallbackMap =
+ std::unordered_map<uint64_t, InputBlockCallback>;
+ InputBlockCallbackMap mInputBlockCallbacks;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_APZInputBridgeChild_h
diff --git a/gfx/layers/ipc/APZInputBridgeParent.cpp b/gfx/layers/ipc/APZInputBridgeParent.cpp
new file mode 100644
index 0000000000..fcc642ff7a
--- /dev/null
+++ b/gfx/layers/ipc/APZInputBridgeParent.cpp
@@ -0,0 +1,213 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/layers/APZInputBridgeParent.h"
+
+#include "mozilla/ipc/Endpoint.h"
+#include "mozilla/layers/APZInputBridge.h"
+#include "mozilla/layers/CompositorBridgeParent.h"
+#include "mozilla/layers/IAPZCTreeManager.h"
+#include "InputData.h"
+
+namespace mozilla {
+namespace layers {
+
+/* static */
+RefPtr<APZInputBridgeParent> APZInputBridgeParent::Create(
+ const LayersId& aLayersId, Endpoint<PAPZInputBridgeParent>&& aEndpoint) {
+ RefPtr<APZInputBridgeParent> parent = new APZInputBridgeParent(aLayersId);
+ if (!aEndpoint.Bind(parent)) {
+ // We can't recover from this.
+ MOZ_CRASH("Failed to bind APZInputBridgeParent to endpoint");
+ }
+
+ return parent;
+}
+
+APZInputBridgeParent::APZInputBridgeParent(const LayersId& aLayersId) {
+ MOZ_ASSERT(XRE_IsGPUProcess());
+ MOZ_ASSERT(NS_IsMainThread());
+
+ mTreeManager = CompositorBridgeParent::GetAPZCTreeManager(aLayersId);
+ MOZ_ASSERT(mTreeManager);
+}
+
+APZInputBridgeParent::~APZInputBridgeParent() = default;
+
+mozilla::ipc::IPCResult APZInputBridgeParent::RecvReceiveMultiTouchInputEvent(
+ const MultiTouchInput& aEvent, bool aWantsCallback,
+ APZEventResult* aOutResult, MultiTouchInput* aOutEvent) {
+ MultiTouchInput event = aEvent;
+
+ APZInputBridge::InputBlockCallback callback;
+ if (aWantsCallback) {
+ callback = [self = RefPtr<APZInputBridgeParent>(this)](
+ uint64_t aInputBlockId,
+ const APZHandledResult& aHandledResult) {
+ Unused << self->SendCallInputBlockCallback(aInputBlockId, aHandledResult);
+ };
+ }
+
+ *aOutResult = mTreeManager->InputBridge()->ReceiveInputEvent(
+ event, std::move(callback));
+ *aOutEvent = event;
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult APZInputBridgeParent::RecvReceiveMouseInputEvent(
+ const MouseInput& aEvent, bool aWantsCallback, APZEventResult* aOutResult,
+ MouseInput* aOutEvent) {
+ MouseInput event = aEvent;
+
+ APZInputBridge::InputBlockCallback callback;
+ if (aWantsCallback) {
+ callback = [self = RefPtr<APZInputBridgeParent>(this)](
+ uint64_t aInputBlockId,
+ const APZHandledResult& aHandledResult) {
+ Unused << self->SendCallInputBlockCallback(aInputBlockId, aHandledResult);
+ };
+ }
+
+ *aOutResult = mTreeManager->InputBridge()->ReceiveInputEvent(
+ event, std::move(callback));
+ *aOutEvent = event;
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult APZInputBridgeParent::RecvReceivePanGestureInputEvent(
+ const PanGestureInput& aEvent, bool aWantsCallback,
+ APZEventResult* aOutResult, PanGestureInput* aOutEvent) {
+ PanGestureInput event = aEvent;
+
+ APZInputBridge::InputBlockCallback callback;
+ if (aWantsCallback) {
+ callback = [self = RefPtr<APZInputBridgeParent>(this)](
+ uint64_t aInputBlockId,
+ const APZHandledResult& aHandledResult) {
+ Unused << self->SendCallInputBlockCallback(aInputBlockId, aHandledResult);
+ };
+ }
+
+ *aOutResult = mTreeManager->InputBridge()->ReceiveInputEvent(
+ event, std::move(callback));
+ *aOutEvent = event;
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult APZInputBridgeParent::RecvReceivePinchGestureInputEvent(
+ const PinchGestureInput& aEvent, bool aWantsCallback,
+ APZEventResult* aOutResult, PinchGestureInput* aOutEvent) {
+ PinchGestureInput event = aEvent;
+
+ APZInputBridge::InputBlockCallback callback;
+ if (aWantsCallback) {
+ callback = [self = RefPtr<APZInputBridgeParent>(this)](
+ uint64_t aInputBlockId,
+ const APZHandledResult& aHandledResult) {
+ Unused << self->SendCallInputBlockCallback(aInputBlockId, aHandledResult);
+ };
+ }
+
+ *aOutResult = mTreeManager->InputBridge()->ReceiveInputEvent(
+ event, std::move(callback));
+ *aOutEvent = event;
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult APZInputBridgeParent::RecvReceiveTapGestureInputEvent(
+ const TapGestureInput& aEvent, bool aWantsCallback,
+ APZEventResult* aOutResult, TapGestureInput* aOutEvent) {
+ TapGestureInput event = aEvent;
+
+ APZInputBridge::InputBlockCallback callback;
+ if (aWantsCallback) {
+ callback = [self = RefPtr<APZInputBridgeParent>(this)](
+ uint64_t aInputBlockId,
+ const APZHandledResult& aHandledResult) {
+ Unused << self->SendCallInputBlockCallback(aInputBlockId, aHandledResult);
+ };
+ }
+
+ *aOutResult = mTreeManager->InputBridge()->ReceiveInputEvent(
+ event, std::move(callback));
+ *aOutEvent = event;
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult APZInputBridgeParent::RecvReceiveScrollWheelInputEvent(
+ const ScrollWheelInput& aEvent, bool aWantsCallback,
+ APZEventResult* aOutResult, ScrollWheelInput* aOutEvent) {
+ ScrollWheelInput event = aEvent;
+
+ APZInputBridge::InputBlockCallback callback;
+ if (aWantsCallback) {
+ callback = [self = RefPtr<APZInputBridgeParent>(this)](
+ uint64_t aInputBlockId,
+ const APZHandledResult& aHandledResult) {
+ Unused << self->SendCallInputBlockCallback(aInputBlockId, aHandledResult);
+ };
+ }
+
+ *aOutResult = mTreeManager->InputBridge()->ReceiveInputEvent(
+ event, std::move(callback));
+ *aOutEvent = event;
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult APZInputBridgeParent::RecvReceiveKeyboardInputEvent(
+ const KeyboardInput& aEvent, bool aWantsCallback,
+ APZEventResult* aOutResult, KeyboardInput* aOutEvent) {
+ KeyboardInput event = aEvent;
+
+ APZInputBridge::InputBlockCallback callback;
+ if (aWantsCallback) {
+ callback = [self = RefPtr<APZInputBridgeParent>(this)](
+ uint64_t aInputBlockId,
+ const APZHandledResult& aHandledResult) {
+ Unused << self->SendCallInputBlockCallback(aInputBlockId, aHandledResult);
+ };
+ }
+
+ *aOutResult = mTreeManager->InputBridge()->ReceiveInputEvent(
+ event, std::move(callback));
+ *aOutEvent = event;
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult APZInputBridgeParent::RecvUpdateWheelTransaction(
+ const LayoutDeviceIntPoint& aRefPoint, const EventMessage& aEventMessage,
+ const Maybe<ScrollableLayerGuid>& aTargetGuid) {
+ mTreeManager->InputBridge()->UpdateWheelTransaction(aRefPoint, aEventMessage,
+ aTargetGuid);
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult APZInputBridgeParent::RecvProcessUnhandledEvent(
+ const LayoutDeviceIntPoint& aRefPoint, LayoutDeviceIntPoint* aOutRefPoint,
+ ScrollableLayerGuid* aOutTargetGuid, uint64_t* aOutFocusSequenceNumber,
+ LayersId* aOutLayersId) {
+ LayoutDeviceIntPoint refPoint = aRefPoint;
+ mTreeManager->InputBridge()->ProcessUnhandledEvent(
+ &refPoint, aOutTargetGuid, aOutFocusSequenceNumber, aOutLayersId);
+ *aOutRefPoint = refPoint;
+
+ return IPC_OK();
+}
+
+void APZInputBridgeParent::ActorDestroy(ActorDestroyReason aWhy) {
+ // We shouldn't need it after this
+ mTreeManager = nullptr;
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/ipc/APZInputBridgeParent.h b/gfx/layers/ipc/APZInputBridgeParent.h
new file mode 100644
index 0000000000..71f43d092b
--- /dev/null
+++ b/gfx/layers/ipc/APZInputBridgeParent.h
@@ -0,0 +1,75 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_APZInputBridgeParent_h
+#define mozilla_layers_APZInputBridgeParent_h
+
+#include "mozilla/layers/PAPZInputBridgeParent.h"
+
+namespace mozilla {
+namespace layers {
+
+class IAPZCTreeManager;
+
+class APZInputBridgeParent : public PAPZInputBridgeParent {
+ NS_INLINE_DECL_REFCOUNTING(APZInputBridgeParent, final)
+
+ public:
+ static RefPtr<APZInputBridgeParent> Create(
+ const LayersId& aLayersId, Endpoint<PAPZInputBridgeParent>&& aEndpoint);
+
+ mozilla::ipc::IPCResult RecvReceiveMultiTouchInputEvent(
+ const MultiTouchInput& aEvent, bool aWantsCallback,
+ APZEventResult* aOutResult, MultiTouchInput* aOutEvent);
+
+ mozilla::ipc::IPCResult RecvReceiveMouseInputEvent(const MouseInput& aEvent,
+ bool aWantsCallback,
+ APZEventResult* aOutResult,
+ MouseInput* aOutEvent);
+
+ mozilla::ipc::IPCResult RecvReceivePanGestureInputEvent(
+ const PanGestureInput& aEvent, bool aWantsCallback,
+ APZEventResult* aOutResult, PanGestureInput* aOutEvent);
+
+ mozilla::ipc::IPCResult RecvReceivePinchGestureInputEvent(
+ const PinchGestureInput& aEvent, bool aWantsCallback,
+ APZEventResult* aOutResult, PinchGestureInput* aOutEvent);
+
+ mozilla::ipc::IPCResult RecvReceiveTapGestureInputEvent(
+ const TapGestureInput& aEvent, bool aWantsCallback,
+ APZEventResult* aOutResult, TapGestureInput* aOutEvent);
+
+ mozilla::ipc::IPCResult RecvReceiveScrollWheelInputEvent(
+ const ScrollWheelInput& aEvent, bool aWantsCallback,
+ APZEventResult* aOutResult, ScrollWheelInput* aOutEvent);
+
+ mozilla::ipc::IPCResult RecvReceiveKeyboardInputEvent(
+ const KeyboardInput& aEvent, bool aWantsCallback,
+ APZEventResult* aOutResult, KeyboardInput* aOutEvent);
+
+ mozilla::ipc::IPCResult RecvUpdateWheelTransaction(
+ const LayoutDeviceIntPoint& aRefPoint, const EventMessage& aEventMessage,
+ const Maybe<ScrollableLayerGuid>& aTargetGuid);
+
+ mozilla::ipc::IPCResult RecvProcessUnhandledEvent(
+ const LayoutDeviceIntPoint& aRefPoint, LayoutDeviceIntPoint* aOutRefPoint,
+ ScrollableLayerGuid* aOutTargetGuid, uint64_t* aOutFocusSequenceNumber,
+ LayersId* aOutLayersId);
+
+ void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ protected:
+ explicit APZInputBridgeParent(const LayersId& aLayersId);
+ virtual ~APZInputBridgeParent();
+
+ private:
+ RefPtr<IAPZCTreeManager> mTreeManager;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_APZInputBridgeParent_h
diff --git a/gfx/layers/ipc/CanvasChild.cpp b/gfx/layers/ipc/CanvasChild.cpp
new file mode 100644
index 0000000000..2f8464402d
--- /dev/null
+++ b/gfx/layers/ipc/CanvasChild.cpp
@@ -0,0 +1,345 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "CanvasChild.h"
+
+#include "MainThreadUtils.h"
+#include "mozilla/gfx/DrawTargetRecording.h"
+#include "mozilla/gfx/Tools.h"
+#include "mozilla/gfx/Rect.h"
+#include "mozilla/gfx/Point.h"
+#include "mozilla/ipc/Endpoint.h"
+#include "mozilla/ipc/ProcessChild.h"
+#include "mozilla/layers/CanvasDrawEventRecorder.h"
+#include "nsIObserverService.h"
+#include "RecordedCanvasEventImpl.h"
+
+namespace mozilla {
+namespace layers {
+
+class RingBufferWriterServices final
+ : public CanvasEventRingBuffer::WriterServices {
+ public:
+ explicit RingBufferWriterServices(RefPtr<CanvasChild> aCanvasChild)
+ : mCanvasChild(std::move(aCanvasChild)) {}
+
+ ~RingBufferWriterServices() final = default;
+
+ bool ReaderClosed() final {
+ return !mCanvasChild->GetIPCChannel()->CanSend() ||
+ ipc::ProcessChild::ExpectingShutdown();
+ }
+
+ void ResumeReader() final { mCanvasChild->ResumeTranslation(); }
+
+ private:
+ RefPtr<CanvasChild> mCanvasChild;
+};
+
+class SourceSurfaceCanvasRecording final : public gfx::SourceSurface {
+ public:
+ MOZ_DECLARE_REFCOUNTED_VIRTUAL_TYPENAME(SourceSurfaceCanvasRecording, final)
+
+ SourceSurfaceCanvasRecording(
+ const RefPtr<gfx::SourceSurface>& aRecordedSuface,
+ CanvasChild* aCanvasChild,
+ const RefPtr<CanvasDrawEventRecorder>& aRecorder)
+ : mRecordedSurface(aRecordedSuface),
+ mCanvasChild(aCanvasChild),
+ mRecorder(aRecorder) {
+ // It's important that AddStoredObject is called first because that will
+ // run any pending processing required by recorded objects that have been
+ // deleted off the main thread.
+ mRecorder->AddStoredObject(this);
+ mRecorder->RecordEvent(RecordedAddSurfaceAlias(this, aRecordedSuface));
+ }
+
+ ~SourceSurfaceCanvasRecording() {
+ ReferencePtr surfaceAlias = this;
+ if (NS_IsMainThread()) {
+ ReleaseOnMainThread(std::move(mRecorder), surfaceAlias,
+ std::move(mRecordedSurface), std::move(mCanvasChild));
+ return;
+ }
+
+ mRecorder->AddPendingDeletion(
+ [recorder = std::move(mRecorder), surfaceAlias,
+ aliasedSurface = std::move(mRecordedSurface),
+ canvasChild = std::move(mCanvasChild)]() mutable -> void {
+ ReleaseOnMainThread(std::move(recorder), surfaceAlias,
+ std::move(aliasedSurface),
+ std::move(canvasChild));
+ });
+ }
+
+ gfx::SurfaceType GetType() const final { return mRecordedSurface->GetType(); }
+
+ gfx::IntSize GetSize() const final { return mRecordedSurface->GetSize(); }
+
+ gfx::SurfaceFormat GetFormat() const final {
+ return mRecordedSurface->GetFormat();
+ }
+
+ already_AddRefed<gfx::DataSourceSurface> GetDataSurface() final {
+ EnsureDataSurfaceOnMainThread();
+ return do_AddRef(mDataSourceSurface);
+ }
+
+ private:
+ void EnsureDataSurfaceOnMainThread() {
+ // The data can only be retrieved on the main thread.
+ if (!mDataSourceSurface && NS_IsMainThread()) {
+ mDataSourceSurface = mCanvasChild->GetDataSurface(mRecordedSurface);
+ }
+ }
+
+ // Used to ensure that clean-up that requires it is done on the main thread.
+ static void ReleaseOnMainThread(RefPtr<CanvasDrawEventRecorder> aRecorder,
+ ReferencePtr aSurfaceAlias,
+ RefPtr<gfx::SourceSurface> aAliasedSurface,
+ RefPtr<CanvasChild> aCanvasChild) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ aRecorder->RemoveStoredObject(aSurfaceAlias);
+ aRecorder->RecordEvent(RecordedRemoveSurfaceAlias(aSurfaceAlias));
+ aAliasedSurface = nullptr;
+ aCanvasChild = nullptr;
+ aRecorder = nullptr;
+ }
+
+ RefPtr<gfx::SourceSurface> mRecordedSurface;
+ RefPtr<CanvasChild> mCanvasChild;
+ RefPtr<CanvasDrawEventRecorder> mRecorder;
+ RefPtr<gfx::DataSourceSurface> mDataSourceSurface;
+};
+
+CanvasChild::CanvasChild(Endpoint<PCanvasChild>&& aEndpoint) {
+ aEndpoint.Bind(this);
+}
+
+CanvasChild::~CanvasChild() = default;
+
+static void NotifyCanvasDeviceReset() {
+ nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
+ if (obs) {
+ obs->NotifyObservers(nullptr, "canvas-device-reset", nullptr);
+ }
+}
+
+ipc::IPCResult CanvasChild::RecvNotifyDeviceChanged() {
+ NotifyCanvasDeviceReset();
+ mRecorder->RecordEvent(RecordedDeviceChangeAcknowledged());
+ return IPC_OK();
+}
+
+/* static */ bool CanvasChild::mDeactivated = false;
+
+ipc::IPCResult CanvasChild::RecvDeactivate() {
+ mDeactivated = true;
+ NotifyCanvasDeviceReset();
+ return IPC_OK();
+}
+
+void CanvasChild::EnsureRecorder(TextureType aTextureType) {
+ if (!mRecorder) {
+ MOZ_ASSERT(mTextureType == TextureType::Unknown);
+ mTextureType = aTextureType;
+ mRecorder = MakeAndAddRef<CanvasDrawEventRecorder>();
+ SharedMemoryBasic::Handle handle;
+ CrossProcessSemaphoreHandle readerSem;
+ CrossProcessSemaphoreHandle writerSem;
+ if (!mRecorder->Init(OtherPid(), &handle, &readerSem, &writerSem,
+ MakeUnique<RingBufferWriterServices>(this))) {
+ mRecorder = nullptr;
+ return;
+ }
+
+ if (CanSend()) {
+ Unused << SendInitTranslator(mTextureType, std::move(handle),
+ std::move(readerSem), std::move(writerSem));
+ }
+ }
+
+ MOZ_RELEASE_ASSERT(mTextureType == aTextureType,
+ "We only support one remote TextureType currently.");
+}
+
+void CanvasChild::ActorDestroy(ActorDestroyReason aWhy) {
+ // Explicitly drop our reference to the recorder, because it holds a reference
+ // to us via the ResumeTranslation callback.
+ mRecorder = nullptr;
+}
+
+void CanvasChild::ResumeTranslation() {
+ if (CanSend()) {
+ SendResumeTranslation();
+ }
+}
+
+void CanvasChild::Destroy() {
+ if (CanSend()) {
+ Close();
+ }
+}
+
+void CanvasChild::OnTextureWriteLock() {
+ // We drop mRecorder in ActorDestroy to break the reference cycle.
+ if (!mRecorder) {
+ return;
+ }
+
+ mHasOutstandingWriteLock = true;
+ mLastWriteLockCheckpoint = mRecorder->CreateCheckpoint();
+}
+
+void CanvasChild::OnTextureForwarded() {
+ // We drop mRecorder in ActorDestroy to break the reference cycle.
+ if (!mRecorder) {
+ return;
+ }
+
+ if (mHasOutstandingWriteLock) {
+ mRecorder->RecordEvent(RecordedCanvasFlush());
+ if (!mRecorder->WaitForCheckpoint(mLastWriteLockCheckpoint)) {
+ gfxWarning() << "Timed out waiting for last write lock to be processed.";
+ }
+
+ mHasOutstandingWriteLock = false;
+ }
+
+ // We hold onto the last transaction's external surfaces until we have waited
+ // for the write locks in this transaction. This means we know that the
+ // surfaces have been picked up in the canvas threads and there is no race
+ // with them being removed from SharedSurfacesParent. Note this releases the
+ // current contents of mLastTransactionExternalSurfaces.
+ mRecorder->TakeExternalSurfaces(mLastTransactionExternalSurfaces);
+}
+
+void CanvasChild::EnsureBeginTransaction() {
+ // We drop mRecorder in ActorDestroy to break the reference cycle.
+ if (!mRecorder) {
+ return;
+ }
+
+ if (!mIsInTransaction) {
+ mRecorder->RecordEvent(RecordedCanvasBeginTransaction());
+ mIsInTransaction = true;
+ }
+}
+
+void CanvasChild::EndTransaction() {
+ // We drop mRecorder in ActorDestroy to break the reference cycle.
+ if (!mRecorder) {
+ return;
+ }
+
+ if (mIsInTransaction) {
+ mRecorder->RecordEvent(RecordedCanvasEndTransaction());
+ mIsInTransaction = false;
+ mLastNonEmptyTransaction = TimeStamp::NowLoRes();
+ }
+
+ ++mTransactionsSinceGetDataSurface;
+}
+
+bool CanvasChild::ShouldBeCleanedUp() const {
+ // Always return true if we've been deactivated.
+ if (Deactivated()) {
+ return true;
+ }
+
+ // We can only be cleaned up if nothing else references our recorder.
+ if (mRecorder && !mRecorder->hasOneRef()) {
+ return false;
+ }
+
+ static const TimeDuration kCleanUpCanvasThreshold =
+ TimeDuration::FromSeconds(10);
+ return TimeStamp::NowLoRes() - mLastNonEmptyTransaction >
+ kCleanUpCanvasThreshold;
+}
+
+already_AddRefed<gfx::DrawTarget> CanvasChild::CreateDrawTarget(
+ gfx::IntSize aSize, gfx::SurfaceFormat aFormat) {
+ // We drop mRecorder in ActorDestroy to break the reference cycle.
+ if (!mRecorder) {
+ return nullptr;
+ }
+
+ RefPtr<gfx::DrawTarget> dummyDt = gfx::Factory::CreateDrawTarget(
+ gfx::BackendType::SKIA, gfx::IntSize(1, 1), aFormat);
+ RefPtr<gfx::DrawTarget> dt = MakeAndAddRef<gfx::DrawTargetRecording>(
+ mRecorder, dummyDt, gfx::IntRect(gfx::IntPoint(0, 0), aSize));
+ return dt.forget();
+}
+
+void CanvasChild::RecordEvent(const gfx::RecordedEvent& aEvent) {
+ // We drop mRecorder in ActorDestroy to break the reference cycle.
+ if (!mRecorder) {
+ return;
+ }
+
+ mRecorder->RecordEvent(aEvent);
+}
+
+already_AddRefed<gfx::DataSourceSurface> CanvasChild::GetDataSurface(
+ const gfx::SourceSurface* aSurface) {
+ MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aSurface);
+
+ // We drop mRecorder in ActorDestroy to break the reference cycle.
+ if (!mRecorder) {
+ return nullptr;
+ }
+
+ // mTransactionsSinceGetDataSurface is used to determine if we want to prepare
+ // a DataSourceSurface in the GPU process up front at the end of the
+ // transaction, but that only makes sense if the canvas JS is requesting data
+ // in between transactions.
+ if (!mIsInTransaction) {
+ mTransactionsSinceGetDataSurface = 0;
+ }
+ EnsureBeginTransaction();
+ mRecorder->RecordEvent(RecordedPrepareDataForSurface(aSurface));
+ uint32_t checkpoint = mRecorder->CreateCheckpoint();
+
+ gfx::IntSize ssSize = aSurface->GetSize();
+ gfx::SurfaceFormat ssFormat = aSurface->GetFormat();
+ size_t dataFormatWidth = ssSize.width * BytesPerPixel(ssFormat);
+ RefPtr<gfx::DataSourceSurface> dataSurface =
+ gfx::Factory::CreateDataSourceSurfaceWithStride(ssSize, ssFormat,
+ dataFormatWidth);
+ if (!dataSurface) {
+ gfxWarning() << "Failed to create DataSourceSurface.";
+ return nullptr;
+ }
+ gfx::DataSourceSurface::ScopedMap map(dataSurface,
+ gfx::DataSourceSurface::READ_WRITE);
+ char* dest = reinterpret_cast<char*>(map.GetData());
+ if (!mRecorder->WaitForCheckpoint(checkpoint)) {
+ gfxWarning() << "Timed out preparing data for DataSourceSurface.";
+ return dataSurface.forget();
+ }
+
+ mRecorder->RecordEvent(RecordedGetDataForSurface(aSurface));
+ mRecorder->ReturnRead(dest, ssSize.height * dataFormatWidth);
+
+ return dataSurface.forget();
+}
+
+already_AddRefed<gfx::SourceSurface> CanvasChild::WrapSurface(
+ const RefPtr<gfx::SourceSurface>& aSurface) {
+ MOZ_ASSERT(aSurface);
+ // We drop mRecorder in ActorDestroy to break the reference cycle.
+ if (!mRecorder) {
+ return nullptr;
+ }
+
+ return MakeAndAddRef<SourceSurfaceCanvasRecording>(aSurface, this, mRecorder);
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/ipc/CanvasChild.h b/gfx/layers/ipc/CanvasChild.h
new file mode 100644
index 0000000000..ca5b864008
--- /dev/null
+++ b/gfx/layers/ipc/CanvasChild.h
@@ -0,0 +1,152 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_CanvasChild_h
+#define mozilla_layers_CanvasChild_h
+
+#include "mozilla/gfx/RecordedEvent.h"
+#include "mozilla/ipc/CrossProcessSemaphore.h"
+#include "mozilla/layers/PCanvasChild.h"
+#include "mozilla/layers/SourceSurfaceSharedData.h"
+#include "nsRefPtrHashtable.h"
+#include "nsTArray.h"
+
+namespace mozilla {
+
+namespace gfx {
+class SourceSurface;
+}
+
+namespace layers {
+class CanvasDrawEventRecorder;
+
+class CanvasChild final : public PCanvasChild {
+ public:
+ NS_INLINE_DECL_REFCOUNTING(CanvasChild)
+
+ explicit CanvasChild(Endpoint<PCanvasChild>&& aEndpoint);
+
+ /**
+ * @returns true if remote canvas has been deactivated due to failure.
+ */
+ static bool Deactivated() { return mDeactivated; }
+
+ ipc::IPCResult RecvNotifyDeviceChanged();
+
+ ipc::IPCResult RecvDeactivate();
+
+ /**
+ * Ensures that the DrawEventRecorder has been created.
+ *
+ * @params aTextureType the TextureType to create in the CanvasTranslator.
+ */
+ void EnsureRecorder(TextureType aTextureType);
+
+ /**
+ * Send a messsage to our CanvasParent to resume translation.
+ */
+ void ResumeTranslation();
+
+ /**
+ * Clean up IPDL actor.
+ */
+ void Destroy();
+
+ /**
+ * Called when a RecordedTextureData is write locked.
+ */
+ void OnTextureWriteLock();
+
+ /**
+ * Called when a RecordedTextureData is forwarded to the compositor.
+ */
+ void OnTextureForwarded();
+
+ /**
+ * @returns true if we should be caching data surfaces in the GPU process.
+ */
+ bool ShouldCacheDataSurface() const {
+ return mTransactionsSinceGetDataSurface < kCacheDataSurfaceThreshold;
+ }
+
+ /**
+ * Ensures that we have sent a begin transaction event, since the last
+ * end transaction.
+ */
+ void EnsureBeginTransaction();
+
+ /**
+ * Send an end transaction event to indicate the end of events for this frame.
+ */
+ void EndTransaction();
+
+ /**
+ * @returns true if the canvas IPC classes have not been used for some time
+ * and can be cleaned up.
+ */
+ bool ShouldBeCleanedUp() const;
+
+ /**
+ * Create a DrawTargetRecording for a canvas texture.
+ * @param aSize size for the DrawTarget
+ * @param aFormat SurfaceFormat for the DrawTarget
+ * @returns newly created DrawTargetRecording
+ */
+ already_AddRefed<gfx::DrawTarget> CreateDrawTarget(
+ gfx::IntSize aSize, gfx::SurfaceFormat aFormat);
+
+ /**
+ * Record an event for processing by the CanvasParent's CanvasTranslator.
+ * @param aEvent the event to record
+ */
+ void RecordEvent(const gfx::RecordedEvent& aEvent);
+
+ /**
+ * Wrap the given surface, so that we can provide a DataSourceSurface if
+ * required.
+ * @param aSurface the SourceSurface to wrap
+ * @returns a SourceSurface that can provide a DataSourceSurface if required
+ */
+ already_AddRefed<gfx::SourceSurface> WrapSurface(
+ const RefPtr<gfx::SourceSurface>& aSurface);
+
+ /**
+ * Get DataSourceSurface from the translated equivalent version of aSurface in
+ * the GPU process.
+ * @param aSurface the SourceSurface in this process for which we need a
+ * DataSourceSurface
+ * @returns a DataSourceSurface created from data for aSurface retrieve from
+ * GPU process
+ */
+ already_AddRefed<gfx::DataSourceSurface> GetDataSurface(
+ const gfx::SourceSurface* aSurface);
+
+ protected:
+ void ActorDestroy(ActorDestroyReason aWhy) final;
+
+ private:
+ DISALLOW_COPY_AND_ASSIGN(CanvasChild);
+
+ ~CanvasChild() final;
+
+ static const uint32_t kCacheDataSurfaceThreshold = 10;
+
+ static bool mDeactivated;
+
+ RefPtr<CanvasDrawEventRecorder> mRecorder;
+ TextureType mTextureType = TextureType::Unknown;
+ uint32_t mLastWriteLockCheckpoint = 0;
+ uint32_t mTransactionsSinceGetDataSurface = kCacheDataSurfaceThreshold;
+ TimeStamp mLastNonEmptyTransaction = TimeStamp::NowLoRes();
+ std::vector<RefPtr<gfx::SourceSurface>> mLastTransactionExternalSurfaces;
+ bool mIsInTransaction = false;
+ bool mHasOutstandingWriteLock = false;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_CanvasChild_h
diff --git a/gfx/layers/ipc/CanvasThread.cpp b/gfx/layers/ipc/CanvasThread.cpp
new file mode 100644
index 0000000000..3d0a387c04
--- /dev/null
+++ b/gfx/layers/ipc/CanvasThread.cpp
@@ -0,0 +1,138 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "CanvasThread.h"
+
+#include "mozilla/SharedThreadPool.h"
+#include "nsThreadUtils.h"
+#include "prsystem.h"
+
+bool NS_IsInCanvasThreadOrWorker() {
+ return mozilla::layers::CanvasThreadHolder::IsInCanvasThreadOrWorker();
+}
+
+namespace mozilla {
+namespace layers {
+
+StaticDataMutex<StaticRefPtr<CanvasThreadHolder>>
+ CanvasThreadHolder::sCanvasThreadHolder("sCanvasThreadHolder");
+
+CanvasThreadHolder::CanvasThreadHolder(
+ already_AddRefed<nsISerialEventTarget> aCanvasThread,
+ already_AddRefed<nsIThreadPool> aCanvasWorkers)
+ : mCanvasThread(aCanvasThread),
+ mCanvasWorkers(aCanvasWorkers),
+ mCompositorThreadKeepAlive(CompositorThreadHolder::GetSingleton()) {
+ MOZ_ASSERT(NS_IsInCompositorThread());
+ MOZ_ASSERT(mCanvasThread);
+ MOZ_ASSERT(mCanvasWorkers);
+}
+
+CanvasThreadHolder::~CanvasThreadHolder() {
+ // Note we can't just use NS_IsInCompositorThread() here because
+ // sCompositorThreadHolder might have already gone.
+ MOZ_ASSERT(
+ mCompositorThreadKeepAlive->GetCompositorThread()->IsOnCurrentThread());
+}
+
+/* static */
+already_AddRefed<CanvasThreadHolder> CanvasThreadHolder::EnsureCanvasThread() {
+ MOZ_ASSERT(NS_IsInCompositorThread());
+
+ auto lockedCanvasThreadHolder = sCanvasThreadHolder.Lock();
+ if (!lockedCanvasThreadHolder.ref()) {
+ nsCOMPtr<nsISerialEventTarget> canvasThread;
+ nsresult rv =
+ NS_CreateBackgroundTaskQueue("Canvas", getter_AddRefs(canvasThread));
+ NS_ENSURE_SUCCESS(rv, nullptr);
+
+ // Given that the canvas workers are receiving instructions from
+ // content processes, it probably doesn't make sense to have more than
+ // half the number of processors doing canvas drawing. We set the
+ // lower limit to 2, so that even on single processor systems, if
+ // there is more than one window with canvas drawing, the OS can
+ // manage the load between them.
+ uint32_t threadLimit = std::max(2, PR_GetNumberOfProcessors() / 2);
+ nsCOMPtr<nsIThreadPool> canvasWorkers =
+ SharedThreadPool::Get("CanvasWorkers"_ns, threadLimit);
+ if (!canvasWorkers) {
+ return nullptr;
+ }
+
+ lockedCanvasThreadHolder.ref() =
+ new CanvasThreadHolder(canvasThread.forget(), canvasWorkers.forget());
+ }
+
+ return do_AddRef(lockedCanvasThreadHolder.ref());
+}
+
+/* static */
+void CanvasThreadHolder::ReleaseOnCompositorThread(
+ already_AddRefed<CanvasThreadHolder> aCanvasThreadHolder) {
+ RefPtr<CanvasThreadHolder> canvasThreadHolder = aCanvasThreadHolder;
+ auto lockedCanvasThreadHolder = sCanvasThreadHolder.Lock();
+ lockedCanvasThreadHolder.ref()
+ ->mCompositorThreadKeepAlive->GetCompositorThread()
+ ->Dispatch(NS_NewRunnableFunction(
+ "CanvasThreadHolder::StaticRelease",
+ [canvasThreadHolder = std::move(canvasThreadHolder)]() mutable {
+ RefPtr<CanvasThreadHolder> threadHolder =
+ canvasThreadHolder.forget();
+ threadHolder = nullptr;
+
+ auto lockedCanvasThreadHolder = sCanvasThreadHolder.Lock();
+ if (lockedCanvasThreadHolder.ref()->mRefCnt == 1) {
+ lockedCanvasThreadHolder.ref() = nullptr;
+ }
+ }));
+}
+
+/* static */
+bool CanvasThreadHolder::IsInCanvasThread() {
+ auto lockedCanvasThreadHolder = sCanvasThreadHolder.Lock();
+ return lockedCanvasThreadHolder.ref() &&
+ lockedCanvasThreadHolder.ref()->mCanvasThread->IsOnCurrentThread();
+}
+
+/* static */
+bool CanvasThreadHolder::IsInCanvasWorker() {
+ auto lockedCanvasThreadHolder = sCanvasThreadHolder.Lock();
+ return lockedCanvasThreadHolder.ref() &&
+ lockedCanvasThreadHolder.ref()->mCanvasWorkers->IsOnCurrentThread();
+}
+
+/* static */
+bool CanvasThreadHolder::IsInCanvasThreadOrWorker() {
+ auto lockedCanvasThreadHolder = sCanvasThreadHolder.Lock();
+ return lockedCanvasThreadHolder.ref() &&
+ (lockedCanvasThreadHolder.ref()->mCanvasWorkers->IsOnCurrentThread() ||
+ lockedCanvasThreadHolder.ref()->mCanvasThread->IsOnCurrentThread());
+}
+
+/* static */
+void CanvasThreadHolder::MaybeDispatchToCanvasThread(
+ already_AddRefed<nsIRunnable> aRunnable) {
+ auto lockedCanvasThreadHolder = sCanvasThreadHolder.Lock();
+ if (!lockedCanvasThreadHolder.ref()) {
+ // There is no canvas thread just release the runnable.
+ nsCOMPtr<nsIRunnable> runnable = aRunnable;
+ return;
+ }
+
+ lockedCanvasThreadHolder.ref()->mCanvasThread->Dispatch(std::move(aRunnable));
+}
+
+void CanvasThreadHolder::DispatchToCanvasThread(
+ already_AddRefed<nsIRunnable> aRunnable) {
+ mCanvasThread->Dispatch(std::move(aRunnable));
+}
+
+already_AddRefed<TaskQueue> CanvasThreadHolder::CreateWorkerTaskQueue() {
+ return TaskQueue::Create(do_AddRef(mCanvasWorkers), "CanvasWorker").forget();
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/ipc/CanvasThread.h b/gfx/layers/ipc/CanvasThread.h
new file mode 100644
index 0000000000..ebcd4bc8d5
--- /dev/null
+++ b/gfx/layers/ipc/CanvasThread.h
@@ -0,0 +1,97 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_CanvasThread_h
+#define mozilla_layers_CanvasThread_h
+
+#include "mozilla/layers/CompositorThread.h"
+#include "mozilla/DataMutex.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/TaskQueue.h"
+#include "nsIThreadPool.h"
+
+namespace mozilla {
+namespace layers {
+
+/**
+ * The CanvasThreadHolder is used to manage the lifetime of the canvas
+ * thread (IPC) and workers and also provides methods for using them.
+ */
+class CanvasThreadHolder final {
+ // We only AddRef/Release on the Compositor thread.
+ NS_INLINE_DECL_REFCOUNTING(CanvasThreadHolder)
+
+ public:
+ /**
+ * Ensures that the canvas thread and workers are created.
+ * This must be called on the compositor thread.
+ * @return a CanvasThreadHolder
+ */
+ static already_AddRefed<CanvasThreadHolder> EnsureCanvasThread();
+
+ /**
+ * Used to release CanvasThreadHolder references, which must be released on
+ * the compositor thread.
+ * @param aCanvasThreadHolder the reference to release
+ */
+ static void ReleaseOnCompositorThread(
+ already_AddRefed<CanvasThreadHolder> aCanvasThreadHolder);
+
+ /**
+ * @return true if in the canvas thread
+ */
+ static bool IsInCanvasThread();
+
+ /**
+ * @return true if in a canvas worker thread
+ */
+ static bool IsInCanvasWorker();
+
+ /**
+ * @return true if in the canvas thread or worker
+ */
+ static bool IsInCanvasThreadOrWorker();
+
+ /**
+ * Static method to dispatch a runnable to the canvas thread. If there is no
+ * canvas thread this will just release aRunnable.
+ * @param aRunnable the runnable to dispatch
+ */
+ static void MaybeDispatchToCanvasThread(
+ already_AddRefed<nsIRunnable> aRunnable);
+
+ /**
+ * Dispatch a runnable to the canvas thread.
+ * @param aRunnable the runnable to dispatch
+ */
+ void DispatchToCanvasThread(already_AddRefed<nsIRunnable> aRunnable);
+
+ /**
+ * Create a TaskQueue for the canvas worker threads. This must be shutdown
+ * before the reference to CanvasThreadHolder is dropped.
+ * @return a TaskQueue that dispatches to the canvas worker threads
+ */
+ already_AddRefed<TaskQueue> CreateWorkerTaskQueue();
+
+ private:
+ static StaticDataMutex<StaticRefPtr<CanvasThreadHolder>> sCanvasThreadHolder;
+
+ CanvasThreadHolder(already_AddRefed<nsISerialEventTarget> aCanvasThread,
+ already_AddRefed<nsIThreadPool> aCanvasWorkers);
+
+ ~CanvasThreadHolder();
+
+ nsCOMPtr<nsISerialEventTarget> mCanvasThread;
+ RefPtr<nsIThreadPool> mCanvasWorkers;
+
+ // Hold a reference to prevent the compositor thread ending.
+ RefPtr<CompositorThreadHolder> mCompositorThreadKeepAlive;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_CanvasThread_h
diff --git a/gfx/layers/ipc/CanvasTranslator.cpp b/gfx/layers/ipc/CanvasTranslator.cpp
new file mode 100644
index 0000000000..81c40270fb
--- /dev/null
+++ b/gfx/layers/ipc/CanvasTranslator.cpp
@@ -0,0 +1,560 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+#include "CanvasTranslator.h"
+
+#include "gfxGradientCache.h"
+#include "mozilla/gfx/2D.h"
+#include "mozilla/gfx/GPUParent.h"
+#include "mozilla/gfx/Logging.h"
+#include "mozilla/ipc/Endpoint.h"
+#include "mozilla/layers/SharedSurfacesParent.h"
+#include "mozilla/layers/TextureClient.h"
+#include "mozilla/SyncRunnable.h"
+#include "mozilla/Telemetry.h"
+#include "nsTHashSet.h"
+#include "RecordedCanvasEventImpl.h"
+
+#if defined(XP_WIN)
+# include "mozilla/gfx/DeviceManagerDx.h"
+# include "mozilla/layers/TextureD3D11.h"
+#endif
+
+namespace mozilla {
+namespace layers {
+
+// When in a transaction we wait for a short time because we're expecting more
+// events from the content process. We don't want to wait for too long in case
+// other content processes are waiting for events to process.
+static const TimeDuration kReadEventTimeout = TimeDuration::FromMilliseconds(5);
+
+static const TimeDuration kDescriptorTimeout =
+ TimeDuration::FromMilliseconds(10000);
+
+class RingBufferReaderServices final
+ : public CanvasEventRingBuffer::ReaderServices {
+ public:
+ explicit RingBufferReaderServices(RefPtr<CanvasTranslator> aCanvasTranslator)
+ : mCanvasTranslator(std::move(aCanvasTranslator)) {}
+
+ ~RingBufferReaderServices() final = default;
+
+ bool WriterClosed() final {
+ return !mCanvasTranslator->GetIPCChannel()->CanSend();
+ }
+
+ private:
+ RefPtr<CanvasTranslator> mCanvasTranslator;
+};
+
+TextureData* CanvasTranslator::CreateTextureData(TextureType aTextureType,
+ const gfx::IntSize& aSize,
+ gfx::SurfaceFormat aFormat) {
+ TextureData* textureData = nullptr;
+ switch (aTextureType) {
+#ifdef XP_WIN
+ case TextureType::D3D11: {
+ textureData =
+ D3D11TextureData::Create(aSize, aFormat, ALLOC_CLEAR_BUFFER, mDevice);
+ break;
+ }
+#endif
+ default:
+ MOZ_CRASH("Unsupported TextureType for CanvasTranslator.");
+ }
+
+ return textureData;
+}
+
+typedef nsTHashSet<RefPtr<CanvasTranslator>> CanvasTranslatorSet;
+
+static CanvasTranslatorSet& CanvasTranslators() {
+ MOZ_ASSERT(CanvasThreadHolder::IsInCanvasThread());
+ static CanvasTranslatorSet* sCanvasTranslator = new CanvasTranslatorSet();
+ return *sCanvasTranslator;
+}
+
+static void EnsureAllClosed() {
+ for (const auto& key : CanvasTranslators()) {
+ key->Close();
+ }
+}
+
+/* static */ void CanvasTranslator::Shutdown() {
+ // If the dispatch fails there is no canvas thread and so no translators.
+ CanvasThreadHolder::MaybeDispatchToCanvasThread(NewRunnableFunction(
+ "CanvasTranslator::EnsureAllClosed", &EnsureAllClosed));
+}
+
+/* static */ already_AddRefed<CanvasTranslator> CanvasTranslator::Create(
+ ipc::Endpoint<PCanvasParent>&& aEndpoint) {
+ MOZ_ASSERT(NS_IsInCompositorThread());
+
+ RefPtr<CanvasThreadHolder> threadHolder =
+ CanvasThreadHolder::EnsureCanvasThread();
+ RefPtr<CanvasTranslator> canvasTranslator =
+ new CanvasTranslator(do_AddRef(threadHolder));
+ threadHolder->DispatchToCanvasThread(
+ NewRunnableMethod<Endpoint<PCanvasParent>&&>(
+ "CanvasTranslator::Bind", canvasTranslator, &CanvasTranslator::Bind,
+ std::move(aEndpoint)));
+ return canvasTranslator.forget();
+}
+
+CanvasTranslator::CanvasTranslator(
+ already_AddRefed<CanvasThreadHolder> aCanvasThreadHolder)
+ : gfx::InlineTranslator(), mCanvasThreadHolder(aCanvasThreadHolder) {
+ // Track when remote canvas has been activated.
+ Telemetry::ScalarAdd(Telemetry::ScalarID::GFX_CANVAS_REMOTE_ACTIVATED, 1);
+}
+
+CanvasTranslator::~CanvasTranslator() {
+ // The textures need to be the last thing holding their DrawTargets, so that
+ // they can destroy them within a lock.
+ mDrawTargets.Clear();
+ mBaseDT = nullptr;
+}
+
+void CanvasTranslator::Bind(Endpoint<PCanvasParent>&& aEndpoint) {
+ if (!aEndpoint.Bind(this)) {
+ return;
+ }
+
+ CanvasTranslators().Insert(this);
+}
+
+mozilla::ipc::IPCResult CanvasTranslator::RecvInitTranslator(
+ const TextureType& aTextureType,
+ ipc::SharedMemoryBasic::Handle&& aReadHandle,
+ CrossProcessSemaphoreHandle&& aReaderSem,
+ CrossProcessSemaphoreHandle&& aWriterSem) {
+ if (mStream) {
+ return IPC_FAIL(this, "RecvInitTranslator called twice.");
+ }
+
+ mTextureType = aTextureType;
+
+ // We need to initialize the stream first, because it might be used to
+ // communicate other failures back to the writer.
+ mStream = MakeUnique<CanvasEventRingBuffer>();
+ if (!mStream->InitReader(std::move(aReadHandle), std::move(aReaderSem),
+ std::move(aWriterSem),
+ MakeUnique<RingBufferReaderServices>(this))) {
+ return IPC_FAIL(this, "Failed to initialize ring buffer reader.");
+ }
+
+#if defined(XP_WIN)
+ if (!CheckForFreshCanvasDevice(__LINE__)) {
+ gfxCriticalNote << "GFX: CanvasTranslator failed to get device";
+ return IPC_OK();
+ }
+#endif
+
+ mTranslationTaskQueue = mCanvasThreadHolder->CreateWorkerTaskQueue();
+ return RecvResumeTranslation();
+}
+
+ipc::IPCResult CanvasTranslator::RecvResumeTranslation() {
+ if (mDeactivated) {
+ // The other side might have sent a resume message before we deactivated.
+ return IPC_OK();
+ }
+
+ MOZ_ALWAYS_SUCCEEDS(mTranslationTaskQueue->Dispatch(
+ NewRunnableMethod("CanvasTranslator::StartTranslation", this,
+ &CanvasTranslator::StartTranslation)));
+
+ return IPC_OK();
+}
+
+void CanvasTranslator::StartTranslation() {
+ if (!TranslateRecording() && GetIPCChannel()->CanSend()) {
+ MOZ_ALWAYS_SUCCEEDS(mTranslationTaskQueue->Dispatch(
+ NewRunnableMethod("CanvasTranslator::StartTranslation", this,
+ &CanvasTranslator::StartTranslation)));
+ }
+
+ // If the stream has been marked as bad and the Writer hasn't failed,
+ // deactivate remote canvas.
+ if (!mStream->good() && !mStream->WriterFailed()) {
+ Telemetry::ScalarAdd(
+ Telemetry::ScalarID::GFX_CANVAS_REMOTE_DEACTIVATED_BAD_STREAM, 1);
+ Deactivate();
+ }
+}
+
+void CanvasTranslator::ActorDestroy(ActorDestroyReason why) {
+ MOZ_ASSERT(CanvasThreadHolder::IsInCanvasThread());
+
+ if (!mTranslationTaskQueue) {
+ return FinishShutdown();
+ }
+
+ mTranslationTaskQueue->BeginShutdown()->Then(
+ GetCurrentSerialEventTarget(), __func__, this,
+ &CanvasTranslator::FinishShutdown, &CanvasTranslator::FinishShutdown);
+}
+
+void CanvasTranslator::FinishShutdown() {
+ MOZ_ASSERT(CanvasThreadHolder::IsInCanvasThread());
+
+ // mTranslationTaskQueue has shutdown we can safely drop the ring buffer to
+ // break the cycle caused by RingBufferReaderServices.
+ mStream = nullptr;
+
+ // CanvasTranslators has a MOZ_ASSERT(CanvasThreadHolder::IsInCanvasThread())
+ // to ensure it is only called on the Canvas Thread. This takes a lock on
+ // CanvasThreadHolder::sCanvasThreadHolder, which is also locked in
+ // CanvasThreadHolder::StaticRelease on the compositor thread from
+ // ReleaseOnCompositorThread below. If that lock wins the race with the one in
+ // IsInCanvasThread and it is the last CanvasThreadHolder reference then it
+ // shuts down the canvas thread waiting for it to finish. However
+ // IsInCanvasThread is waiting for the lock on the canvas thread and we
+ // deadlock. So, we need to call CanvasTranslators before
+ // ReleaseOnCompositorThread.
+ CanvasTranslatorSet& canvasTranslators = CanvasTranslators();
+ CanvasThreadHolder::ReleaseOnCompositorThread(mCanvasThreadHolder.forget());
+ canvasTranslators.Remove(this);
+}
+
+void CanvasTranslator::Deactivate() {
+ if (mDeactivated) {
+ return;
+ }
+ mDeactivated = true;
+
+ // We need to tell the other side to deactivate. Make sure the stream is
+ // marked as bad so that the writing side won't wait for space to write.
+ mStream->SetIsBad();
+ mCanvasThreadHolder->DispatchToCanvasThread(
+ NewRunnableMethod("CanvasTranslator::SendDeactivate", this,
+ &CanvasTranslator::SendDeactivate));
+
+ // Unlock all of our textures.
+ for (auto const& entry : mTextureDatas) {
+ entry.second->Unlock();
+ }
+
+ // Also notify anyone waiting for a surface descriptor. This must be done
+ // after mDeactivated is set to true.
+ mSurfaceDescriptorsMonitor.NotifyAll();
+}
+
+bool CanvasTranslator::TranslateRecording() {
+ MOZ_ASSERT(CanvasThreadHolder::IsInCanvasWorker());
+
+ int32_t eventType = mStream->ReadNextEvent();
+ while (mStream->good()) {
+ bool success = RecordedEvent::DoWithEventFromStream(
+ *mStream, static_cast<RecordedEvent::EventType>(eventType),
+ [&](RecordedEvent* recordedEvent) -> bool {
+ // Make sure that the whole event was read from the stream.
+ if (!mStream->good()) {
+ if (!GetIPCChannel()->CanSend()) {
+ // The other side has closed only warn about read failure.
+ gfxWarning() << "Failed to read event type: "
+ << recordedEvent->GetType();
+ } else {
+ gfxCriticalNote << "Failed to read event type: "
+ << recordedEvent->GetType();
+ }
+ return false;
+ }
+
+ return recordedEvent->PlayEvent(this);
+ });
+
+ // Check the stream is good here or we will log the issue twice.
+ if (!mStream->good()) {
+ return true;
+ }
+
+ if (!success && !HandleExtensionEvent(eventType)) {
+ if (mDeviceResetInProgress) {
+ // We've notified the recorder of a device change, so we are expecting
+ // failures. Log as a warning to prevent crash reporting being flooded.
+ gfxWarning() << "Failed to play canvas event type: " << eventType;
+ } else {
+ gfxCriticalNote << "Failed to play canvas event type: " << eventType;
+ }
+ if (!mStream->good()) {
+ return true;
+ }
+ }
+
+ if (!mIsInTransaction) {
+ return mStream->StopIfEmpty();
+ }
+
+ if (!mStream->HasDataToRead()) {
+ // We're going to wait for the next event, so take the opportunity to
+ // flush the rendering.
+ Flush();
+ if (!mStream->WaitForDataToRead(kReadEventTimeout, 0)) {
+ return true;
+ }
+ }
+
+ eventType = mStream->ReadNextEvent();
+ }
+
+ return true;
+}
+
+#define READ_AND_PLAY_CANVAS_EVENT_TYPE(_typeenum, _class) \
+ case _typeenum: { \
+ auto e = _class(*mStream); \
+ if (!mStream->good()) { \
+ if (!GetIPCChannel()->CanSend()) { \
+ /* The other side has closed only warn about read failure. */ \
+ gfxWarning() << "Failed to read event type: " << _typeenum; \
+ } else { \
+ gfxCriticalNote << "Failed to read event type: " << _typeenum; \
+ } \
+ return false; \
+ } \
+ return e.PlayCanvasEvent(this); \
+ }
+
+bool CanvasTranslator::HandleExtensionEvent(int32_t aType) {
+ // This is where we handle extensions to the Moz2D Recording events to handle
+ // canvas specific things.
+ switch (aType) {
+ FOR_EACH_CANVAS_EVENT(READ_AND_PLAY_CANVAS_EVENT_TYPE)
+ default:
+ return false;
+ }
+}
+
+void CanvasTranslator::BeginTransaction() { mIsInTransaction = true; }
+
+void CanvasTranslator::Flush() {
+#if defined(XP_WIN)
+ // We can end up without a device, due to a reset and failure to re-create.
+ if (!mDevice) {
+ return;
+ }
+
+ gfx::AutoSerializeWithMoz2D serializeWithMoz2D(mBackendType);
+ RefPtr<ID3D11DeviceContext> deviceContext;
+ mDevice->GetImmediateContext(getter_AddRefs(deviceContext));
+ deviceContext->Flush();
+#endif
+}
+
+void CanvasTranslator::EndTransaction() {
+ Flush();
+ // At the end of a transaction is a good time to check if a new canvas device
+ // has been created, even if a reset did not occur.
+ Unused << CheckForFreshCanvasDevice(__LINE__);
+ mIsInTransaction = false;
+}
+
+void CanvasTranslator::DeviceChangeAcknowledged() {
+ mDeviceResetInProgress = false;
+}
+
+bool CanvasTranslator::CreateReferenceTexture() {
+ if (mReferenceTextureData) {
+ mReferenceTextureData->Unlock();
+ }
+
+ mReferenceTextureData.reset(CreateTextureData(
+ mTextureType, gfx::IntSize(1, 1), gfx::SurfaceFormat::B8G8R8A8));
+ if (!mReferenceTextureData) {
+ return false;
+ }
+
+ mReferenceTextureData->Lock(OpenMode::OPEN_READ_WRITE);
+ mBaseDT = mReferenceTextureData->BorrowDrawTarget();
+ if (!mBaseDT) {
+ // We might get a null draw target due to a device failure, just return
+ // false so that we can recover.
+ return false;
+ }
+
+ mBackendType = mBaseDT->GetBackendType();
+ return true;
+}
+
+bool CanvasTranslator::CheckForFreshCanvasDevice(int aLineNumber) {
+#if defined(XP_WIN)
+ // If a new device has already been created, use that one.
+ RefPtr<ID3D11Device> device = gfx::DeviceManagerDx::Get()->GetCanvasDevice();
+ if (device && device != mDevice) {
+ if (mDevice) {
+ // We already had a device, notify child of change.
+ NotifyDeviceChanged();
+ }
+ mDevice = device.forget();
+ return CreateReferenceTexture();
+ }
+
+ if (mDevice) {
+ if (mDevice->GetDeviceRemovedReason() == S_OK) {
+ return false;
+ }
+
+ gfxCriticalNote << "GFX: CanvasTranslator detected a device reset at "
+ << aLineNumber;
+ NotifyDeviceChanged();
+ }
+
+ RefPtr<Runnable> runnable = NS_NewRunnableFunction(
+ "CanvasTranslator NotifyDeviceReset",
+ []() { gfx::GPUParent::GetSingleton()->NotifyDeviceReset(); });
+
+ // It is safe to wait here because only the Compositor thread waits on us and
+ // the main thread doesn't wait on the compositor thread in the GPU process.
+ SyncRunnable::DispatchToThread(GetMainThreadSerialEventTarget(), runnable,
+ /*aForceDispatch*/ true);
+
+ mDevice = gfx::DeviceManagerDx::Get()->GetCanvasDevice();
+ if (!mDevice) {
+ // We don't have a canvas device, we need to deactivate.
+ Telemetry::ScalarAdd(
+ Telemetry::ScalarID::GFX_CANVAS_REMOTE_DEACTIVATED_NO_DEVICE, 1);
+ Deactivate();
+ return false;
+ }
+
+ return CreateReferenceTexture();
+#else
+ return false;
+#endif
+}
+
+void CanvasTranslator::NotifyDeviceChanged() {
+ mDeviceResetInProgress = true;
+ mCanvasThreadHolder->DispatchToCanvasThread(
+ NewRunnableMethod("CanvasTranslator::SendNotifyDeviceChanged", this,
+ &CanvasTranslator::SendNotifyDeviceChanged));
+}
+
+void CanvasTranslator::AddSurfaceDescriptor(int64_t aTextureId,
+ TextureData* aTextureData) {
+ UniquePtr<SurfaceDescriptor> descriptor = MakeUnique<SurfaceDescriptor>();
+ if (!aTextureData->Serialize(*descriptor)) {
+ MOZ_CRASH("Failed to serialize");
+ }
+
+ MonitorAutoLock lock(mSurfaceDescriptorsMonitor);
+ mSurfaceDescriptors[aTextureId] = std::move(descriptor);
+ mSurfaceDescriptorsMonitor.Notify();
+}
+
+already_AddRefed<gfx::DrawTarget> CanvasTranslator::CreateDrawTarget(
+ gfx::ReferencePtr aRefPtr, const gfx::IntSize& aSize,
+ gfx::SurfaceFormat aFormat) {
+ RefPtr<gfx::DrawTarget> dt;
+ do {
+ TextureData* textureData = CreateTextureData(mTextureType, aSize, aFormat);
+ if (textureData) {
+ MOZ_DIAGNOSTIC_ASSERT(mNextTextureId >= 0, "No texture ID set");
+ textureData->Lock(OpenMode::OPEN_READ_WRITE);
+ mTextureDatas[mNextTextureId] = UniquePtr<TextureData>(textureData);
+ AddSurfaceDescriptor(mNextTextureId, textureData);
+ dt = textureData->BorrowDrawTarget();
+ }
+ } while (!dt && CheckForFreshCanvasDevice(__LINE__));
+ AddDrawTarget(aRefPtr, dt);
+ mNextTextureId = -1;
+
+ return dt.forget();
+}
+
+void CanvasTranslator::RemoveTexture(int64_t aTextureId) {
+ mTextureDatas.erase(aTextureId);
+
+ // It is possible that the texture from the content process has never been
+ // forwarded to the GPU process, so make sure its descriptor is removed.
+ MonitorAutoLock lock(mSurfaceDescriptorsMonitor);
+ mSurfaceDescriptors.erase(aTextureId);
+}
+
+TextureData* CanvasTranslator::LookupTextureData(int64_t aTextureId) {
+ TextureMap::const_iterator result = mTextureDatas.find(aTextureId);
+ if (result == mTextureDatas.end()) {
+ return nullptr;
+ }
+ return result->second.get();
+}
+
+UniquePtr<SurfaceDescriptor> CanvasTranslator::WaitForSurfaceDescriptor(
+ int64_t aTextureId) {
+ MonitorAutoLock lock(mSurfaceDescriptorsMonitor);
+ DescriptorMap::iterator result;
+ while ((result = mSurfaceDescriptors.find(aTextureId)) ==
+ mSurfaceDescriptors.end()) {
+ // If remote canvas has been deactivated just return null.
+ if (mDeactivated) {
+ return nullptr;
+ }
+
+ CVStatus status = mSurfaceDescriptorsMonitor.Wait(kDescriptorTimeout);
+ if (status == CVStatus::Timeout) {
+ // If something has gone wrong and the texture has already been destroyed,
+ // it will have cleaned up its descriptor.
+ return nullptr;
+ }
+ }
+
+ UniquePtr<SurfaceDescriptor> descriptor = std::move(result->second);
+ mSurfaceDescriptors.erase(aTextureId);
+ return descriptor;
+}
+
+already_AddRefed<gfx::SourceSurface> CanvasTranslator::LookupExternalSurface(
+ uint64_t aKey) {
+ return SharedSurfacesParent::Get(wr::ToExternalImageId(aKey));
+}
+
+already_AddRefed<gfx::GradientStops> CanvasTranslator::GetOrCreateGradientStops(
+ gfx::DrawTarget* aDrawTarget, gfx::GradientStop* aRawStops,
+ uint32_t aNumStops, gfx::ExtendMode aExtendMode) {
+ MOZ_ASSERT(aDrawTarget);
+ nsTArray<gfx::GradientStop> rawStopArray(aRawStops, aNumStops);
+ return gfx::gfxGradientCache::GetOrCreateGradientStops(
+ aDrawTarget, rawStopArray, aExtendMode);
+}
+
+gfx::DataSourceSurface* CanvasTranslator::LookupDataSurface(
+ gfx::ReferencePtr aRefPtr) {
+ return mDataSurfaces.GetWeak(aRefPtr);
+}
+
+void CanvasTranslator::AddDataSurface(
+ gfx::ReferencePtr aRefPtr, RefPtr<gfx::DataSourceSurface>&& aSurface) {
+ mDataSurfaces.InsertOrUpdate(aRefPtr, std::move(aSurface));
+}
+
+void CanvasTranslator::RemoveDataSurface(gfx::ReferencePtr aRefPtr) {
+ mDataSurfaces.Remove(aRefPtr);
+}
+
+void CanvasTranslator::SetPreparedMap(
+ gfx::ReferencePtr aSurface,
+ UniquePtr<gfx::DataSourceSurface::ScopedMap> aMap) {
+ mMappedSurface = aSurface;
+ mPreparedMap = std::move(aMap);
+}
+
+UniquePtr<gfx::DataSourceSurface::ScopedMap> CanvasTranslator::GetPreparedMap(
+ gfx::ReferencePtr aSurface) {
+ if (!mPreparedMap) {
+ // We might fail to set the map during, for example, device resets.
+ return nullptr;
+ }
+
+ MOZ_RELEASE_ASSERT(mMappedSurface == aSurface,
+ "aSurface must match previously stored surface.");
+
+ mMappedSurface = nullptr;
+ return std::move(mPreparedMap);
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/ipc/CanvasTranslator.h b/gfx/layers/ipc/CanvasTranslator.h
new file mode 100644
index 0000000000..af2769b0b4
--- /dev/null
+++ b/gfx/layers/ipc/CanvasTranslator.h
@@ -0,0 +1,306 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_CanvasTranslator_h
+#define mozilla_layers_CanvasTranslator_h
+
+#include <unordered_map>
+#include <vector>
+
+#include "mozilla/gfx/InlineTranslator.h"
+#include "mozilla/layers/CanvasDrawEventRecorder.h"
+#include "mozilla/layers/CanvasThread.h"
+#include "mozilla/layers/LayersSurfaces.h"
+#include "mozilla/layers/PCanvasParent.h"
+#include "mozilla/ipc/CrossProcessSemaphore.h"
+#include "mozilla/Monitor.h"
+#include "mozilla/UniquePtr.h"
+
+namespace mozilla {
+namespace layers {
+
+class TextureData;
+
+class CanvasTranslator final : public gfx::InlineTranslator,
+ public PCanvasParent {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(CanvasTranslator)
+
+ friend class PProtocolParent;
+
+ /**
+ * Create an uninitialized CanvasTranslator and bind it to the given endpoint
+ * on the CanvasPlaybackLoop.
+ *
+ * @param aEndpoint the endpoint to bind to
+ * @return the new CanvasTranslator
+ */
+ static already_AddRefed<CanvasTranslator> Create(
+ Endpoint<PCanvasParent>&& aEndpoint);
+
+ /**
+ * Shutdown all of the CanvasTranslators.
+ */
+ static void Shutdown();
+
+ /**
+ * Initialize the canvas translator for a particular TextureType and
+ * CanvasEventRingBuffer.
+ *
+ * @param aTextureType the TextureType the translator will create
+ * @param aReadHandle handle to the shared memory for the
+ * CanvasEventRingBuffer
+ * @param aReaderSem reading blocked semaphore for the CanvasEventRingBuffer
+ * @param aWriterSem writing blocked semaphore for the CanvasEventRingBuffer
+ */
+ ipc::IPCResult RecvInitTranslator(
+ const TextureType& aTextureType,
+ ipc::SharedMemoryBasic::Handle&& aReadHandle,
+ CrossProcessSemaphoreHandle&& aReaderSem,
+ CrossProcessSemaphoreHandle&& aWriterSem);
+
+ /**
+ * Used to tell the CanvasTranslator to start translating again after it has
+ * stopped due to a timeout waiting for events.
+ */
+ ipc::IPCResult RecvResumeTranslation();
+
+ void ActorDestroy(ActorDestroyReason why) final;
+
+ /**
+ * Translates events until no more are available or the end of a transaction
+ * If this returns false the caller of this is responsible for re-calling
+ * this function.
+ *
+ * @returns true if all events are processed and false otherwise.
+ */
+ bool TranslateRecording();
+
+ /**
+ * Marks the beginning of rendering for a transaction. While in a transaction
+ * the translator will wait for a short time for events before returning.
+ * When not in a transaction the translator will only translate one event at a
+ * time.
+ */
+ void BeginTransaction();
+
+ /**
+ * Marks the end of a transaction.
+ */
+ void EndTransaction();
+
+ /**
+ * Flushes canvas drawing, for example to a device.
+ */
+ void Flush();
+
+ /**
+ * Marks that device change processing in the writing process has finished.
+ */
+ void DeviceChangeAcknowledged();
+
+ /**
+ * Used to send data back to the writer. This is done through the same shared
+ * memory so the writer must wait and read the response after it has submitted
+ * the event that uses this.
+ *
+ * @param aData the data to be written back to the writer
+ * @param aSize the number of chars to write
+ */
+ void ReturnWrite(const char* aData, size_t aSize) {
+ mStream->ReturnWrite(aData, aSize);
+ }
+
+ /**
+ * Set the texture ID that will be used as a lookup for the texture created by
+ * the next CreateDrawTarget.
+ */
+ void SetNextTextureId(int64_t aNextTextureId) {
+ mNextTextureId = aNextTextureId;
+ }
+
+ /**
+ * Used during playback of events to create DrawTargets. For the
+ * CanvasTranslator this means creating TextureDatas and getting the
+ * DrawTargets from those.
+ *
+ * @param aRefPtr the key to store the created DrawTarget against
+ * @param aSize the size of the DrawTarget
+ * @param aFormat the surface format for the DrawTarget
+ * @returns the new DrawTarget
+ */
+ already_AddRefed<gfx::DrawTarget> CreateDrawTarget(
+ gfx::ReferencePtr aRefPtr, const gfx::IntSize& aSize,
+ gfx::SurfaceFormat aFormat) final;
+
+ already_AddRefed<gfx::GradientStops> GetOrCreateGradientStops(
+ gfx::DrawTarget* aDrawTarget, gfx::GradientStop* aRawStops,
+ uint32_t aNumStops, gfx::ExtendMode aExtendMode) final;
+
+ /**
+ * Get the TextureData associated with a TextureData from another process.
+ *
+ * @param aTextureId the key used to find the TextureData
+ * @returns the TextureData found
+ */
+ TextureData* LookupTextureData(int64_t aTextureId);
+
+ /**
+ * Waits for the SurfaceDescriptor associated with a TextureData from another
+ * process to be created and then returns it.
+ *
+ * @param aTextureId the key used to find the SurfaceDescriptor
+ * @returns the SurfaceDescriptor found
+ */
+ UniquePtr<SurfaceDescriptor> WaitForSurfaceDescriptor(int64_t aTextureId);
+
+ /**
+ * Removes the texture and other objects associated with a texture ID.
+ *
+ * @param aTextureId the texture ID to remove
+ */
+ void RemoveTexture(int64_t aTextureId);
+
+ /**
+ * Overriden to remove any DataSourceSurfaces associated with the RefPtr.
+ *
+ * @param aRefPtr the key to the surface
+ * @param aSurface the surface to store
+ */
+ void AddSourceSurface(gfx::ReferencePtr aRefPtr,
+ gfx::SourceSurface* aSurface) final {
+ if (mMappedSurface == aRefPtr) {
+ mPreparedMap = nullptr;
+ mMappedSurface = nullptr;
+ }
+ RemoveDataSurface(aRefPtr);
+ InlineTranslator::AddSourceSurface(aRefPtr, aSurface);
+ }
+
+ /**
+ * Removes the SourceSurface and other objects associated with a SourceSurface
+ * from another process.
+ *
+ * @param aRefPtr the key to the objects to remove
+ */
+ void RemoveSourceSurface(gfx::ReferencePtr aRefPtr) final {
+ if (mMappedSurface == aRefPtr) {
+ mPreparedMap = nullptr;
+ mMappedSurface = nullptr;
+ }
+ RemoveDataSurface(aRefPtr);
+ InlineTranslator::RemoveSourceSurface(aRefPtr);
+ }
+
+ already_AddRefed<gfx::SourceSurface> LookupExternalSurface(
+ uint64_t aKey) final;
+
+ /**
+ * Gets the cached DataSourceSurface, if it exists, associated with a
+ * SourceSurface from another process.
+ *
+ * @param aRefPtr the key used to find the DataSourceSurface
+ * @returns the DataSourceSurface or nullptr if not found
+ */
+ gfx::DataSourceSurface* LookupDataSurface(gfx::ReferencePtr aRefPtr);
+
+ /**
+ * Used to cache the DataSourceSurface from a SourceSurface associated with a
+ * SourceSurface from another process. This is to improve performance if we
+ * require the data for that SourceSurface.
+ *
+ * @param aRefPtr the key used to store the DataSourceSurface
+ * @param aSurface the DataSourceSurface to store
+ */
+ void AddDataSurface(gfx::ReferencePtr aRefPtr,
+ RefPtr<gfx::DataSourceSurface>&& aSurface);
+
+ /**
+ * Gets the cached DataSourceSurface, if it exists, associated with a
+ * SourceSurface from another process.
+ *
+ * @param aRefPtr the key used to find the DataSourceSurface
+ * @returns the DataSourceSurface or nullptr if not found
+ */
+ void RemoveDataSurface(gfx::ReferencePtr aRefPtr);
+
+ /**
+ * Sets a ScopedMap, to be used in a later event.
+ *
+ * @param aSurface the associated surface in the other process
+ * @param aMap the ScopedMap to store
+ */
+ void SetPreparedMap(gfx::ReferencePtr aSurface,
+ UniquePtr<gfx::DataSourceSurface::ScopedMap> aMap);
+
+ /**
+ * Gets the ScopedMap stored using SetPreparedMap.
+ *
+ * @param aSurface must match the surface from the SetPreparedMap call
+ * @returns the ScopedMap if aSurface matches otherwise nullptr
+ */
+ UniquePtr<gfx::DataSourceSurface::ScopedMap> GetPreparedMap(
+ gfx::ReferencePtr aSurface);
+
+ private:
+ explicit CanvasTranslator(
+ already_AddRefed<CanvasThreadHolder> aCanvasThreadHolder);
+
+ ~CanvasTranslator();
+
+ void Bind(Endpoint<PCanvasParent>&& aEndpoint);
+
+ void StartTranslation();
+
+ void FinishShutdown();
+
+ void Deactivate();
+
+ TextureData* CreateTextureData(TextureType aTextureType,
+ const gfx::IntSize& aSize,
+ gfx::SurfaceFormat aFormat);
+
+ void AddSurfaceDescriptor(int64_t aTextureId, TextureData* atextureData);
+
+ bool HandleExtensionEvent(int32_t aType);
+
+ bool CreateReferenceTexture();
+ bool CheckForFreshCanvasDevice(int aLineNumber);
+ void NotifyDeviceChanged();
+
+ RefPtr<CanvasThreadHolder> mCanvasThreadHolder;
+ RefPtr<TaskQueue> mTranslationTaskQueue;
+#if defined(XP_WIN)
+ RefPtr<ID3D11Device> mDevice;
+#endif
+ // We hold the ring buffer as a UniquePtr so we can drop it once
+ // mTranslationTaskQueue has shutdown to break a RefPtr cycle.
+ UniquePtr<CanvasEventRingBuffer> mStream;
+ TextureType mTextureType = TextureType::Unknown;
+ UniquePtr<TextureData> mReferenceTextureData;
+ // Sometimes during device reset our reference DrawTarget can be null, so we
+ // hold the BackendType separately.
+ gfx::BackendType mBackendType = gfx::BackendType::NONE;
+ typedef std::unordered_map<int64_t, UniquePtr<TextureData>> TextureMap;
+ TextureMap mTextureDatas;
+ int64_t mNextTextureId = -1;
+ nsRefPtrHashtable<nsPtrHashKey<void>, gfx::DataSourceSurface> mDataSurfaces;
+ gfx::ReferencePtr mMappedSurface;
+ UniquePtr<gfx::DataSourceSurface::ScopedMap> mPreparedMap;
+ typedef std::unordered_map<int64_t, UniquePtr<SurfaceDescriptor>>
+ DescriptorMap;
+ DescriptorMap mSurfaceDescriptors MOZ_GUARDED_BY(mSurfaceDescriptorsMonitor);
+ Monitor mSurfaceDescriptorsMonitor{
+ "CanvasTranslator::mSurfaceDescriptorsMonitor"};
+ Atomic<bool> mDeactivated{false};
+ bool mIsInTransaction = false;
+ bool mDeviceResetInProgress = false;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_CanvasTranslator_h
diff --git a/gfx/layers/ipc/CompositableForwarder.cpp b/gfx/layers/ipc/CompositableForwarder.cpp
new file mode 100644
index 0000000000..369dff086a
--- /dev/null
+++ b/gfx/layers/ipc/CompositableForwarder.cpp
@@ -0,0 +1,15 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "CompositableForwarder.h"
+
+#include "mozilla/layers/CompositableClient.h"
+#include "mozilla/layers/TextureClient.h"
+
+namespace mozilla::layers {
+CompositableForwarder::CompositableForwarder() = default;
+CompositableForwarder::~CompositableForwarder() = default;
+} // namespace mozilla::layers
diff --git a/gfx/layers/ipc/CompositableForwarder.h b/gfx/layers/ipc/CompositableForwarder.h
new file mode 100644
index 0000000000..da74a142e6
--- /dev/null
+++ b/gfx/layers/ipc/CompositableForwarder.h
@@ -0,0 +1,110 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_LAYERS_COMPOSITABLEFORWARDER
+#define MOZILLA_LAYERS_COMPOSITABLEFORWARDER
+
+#include <stdint.h> // for int32_t, uint32_t, uint64_t
+#include "mozilla/Assertions.h" // for AssertionConditionType, MOZ_ASSERT, MOZ_ASSERT_HELPER1
+#include "mozilla/RefPtr.h" // for RefPtr
+#include "mozilla/TimeStamp.h" // for TimeStamp
+#include "mozilla/layers/KnowsCompositor.h" // for KnowsCompositor
+#include "nsRect.h" // for nsIntRect
+#include "nsRegion.h" // for nsIntRegion
+#include "nsTArray.h" // for nsTArray
+
+namespace mozilla {
+namespace layers {
+class CompositableClient;
+class CompositableHandle;
+class ImageContainer;
+class PTextureChild;
+class SurfaceDescriptorTiles;
+class TextureClient;
+
+/**
+ * A transaction is a set of changes that happenned on the content side, that
+ * should be sent to the compositor side.
+ * CompositableForwarder is an interface to manage a transaction of
+ * compositable objetcs.
+ *
+ * ShadowLayerForwarder is an example of a CompositableForwarder (that can
+ * additionally forward modifications of the Layer tree).
+ * ImageBridgeChild is another CompositableForwarder.
+ *
+ * CompositableForwarder implements KnowsCompositor for simplicity as all
+ * implementations of CompositableForwarder currently also implement
+ * KnowsCompositor. This dependency could be split if we add new use cases.
+ */
+class CompositableForwarder : public KnowsCompositor {
+ public:
+ CompositableForwarder();
+ ~CompositableForwarder();
+
+ /**
+ * Setup the IPDL actor for aCompositable to be part of layers
+ * transactions.
+ */
+ virtual void Connect(CompositableClient* aCompositable,
+ ImageContainer* aImageContainer = nullptr) = 0;
+
+ virtual void ReleaseCompositable(const CompositableHandle& aHandle) = 0;
+ virtual bool DestroyInTransaction(PTextureChild* aTexture) = 0;
+
+ /**
+ * Tell the CompositableHost on the compositor side to remove the texture
+ * from the CompositableHost.
+ * This function does not delete the TextureHost corresponding to the
+ * TextureClient passed in parameter.
+ * When the TextureClient has TEXTURE_DEALLOCATE_CLIENT flag,
+ * the transaction becomes synchronous.
+ */
+ virtual void RemoveTextureFromCompositable(CompositableClient* aCompositable,
+ TextureClient* aTexture) = 0;
+
+ struct TimedTextureClient {
+ TimedTextureClient()
+ : mTextureClient(nullptr), mFrameID(0), mProducerID(0) {}
+
+ TextureClient* mTextureClient;
+ TimeStamp mTimeStamp;
+ nsIntRect mPictureRect;
+ int32_t mFrameID;
+ int32_t mProducerID;
+ };
+ /**
+ * Tell the CompositableHost on the compositor side what textures to use for
+ * the next composition.
+ */
+ virtual void UseTextures(CompositableClient* aCompositable,
+ const nsTArray<TimedTextureClient>& aTextures) = 0;
+
+ virtual void UseRemoteTexture(CompositableClient* aCompositable,
+ const RemoteTextureId aTextureId,
+ const RemoteTextureOwnerId aOwnerId,
+ const gfx::IntSize aSize,
+ const TextureFlags aFlags) = 0;
+
+ virtual void EnableRemoteTexturePushCallback(
+ CompositableClient* aCompositable, const RemoteTextureOwnerId aOwnerId,
+ const gfx::IntSize aSize, const TextureFlags aFlags) = 0;
+
+ virtual void UpdateFwdTransactionId() = 0;
+ virtual uint64_t GetFwdTransactionId() = 0;
+
+ virtual bool InForwarderThread() = 0;
+
+ void AssertInForwarderThread() { MOZ_ASSERT(InForwarderThread()); }
+
+ protected:
+ nsTArray<RefPtr<TextureClient>> mTexturesToRemove;
+ nsTArray<RefPtr<CompositableClient>> mCompositableClientsToRemove;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif
diff --git a/gfx/layers/ipc/CompositableTransactionParent.cpp b/gfx/layers/ipc/CompositableTransactionParent.cpp
new file mode 100644
index 0000000000..cd8deb2c73
--- /dev/null
+++ b/gfx/layers/ipc/CompositableTransactionParent.cpp
@@ -0,0 +1,174 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "CompositableTransactionParent.h"
+#include "CompositableHost.h" // for CompositableParent, etc
+#include "CompositorBridgeParent.h" // for CompositorBridgeParent
+#include "mozilla/Assertions.h" // for MOZ_ASSERT, etc
+#include "mozilla/RefPtr.h" // for RefPtr
+#include "mozilla/layers/CompositorTypes.h"
+#include "mozilla/layers/ImageBridgeParent.h" // for ImageBridgeParent
+#include "mozilla/layers/LayersSurfaces.h" // for SurfaceDescriptor
+#include "mozilla/layers/LayersTypes.h" // for MOZ_LAYERS_LOG
+#include "mozilla/layers/TextureHost.h" // for TextureHost
+#include "mozilla/layers/WebRenderImageHost.h"
+#include "mozilla/mozalloc.h" // for operator delete
+#include "mozilla/Unused.h"
+#include "nsDebug.h" // for NS_WARNING, NS_ASSERTION
+#include "nsRegion.h" // for nsIntRegion
+
+namespace mozilla {
+namespace layers {
+
+bool CompositableParentManager::ReceiveCompositableUpdate(
+ const CompositableOperation& aEdit) {
+ // Ignore all operations on compositables created on stale compositors. We
+ // return true because the child is unable to handle errors.
+ RefPtr<CompositableHost> compositable =
+ FindCompositable(aEdit.compositable());
+ if (!compositable) {
+ return false;
+ }
+ return ReceiveCompositableUpdate(aEdit.detail(), WrapNotNull(compositable),
+ aEdit.compositable());
+}
+
+bool CompositableParentManager::ReceiveCompositableUpdate(
+ const CompositableOperationDetail& aDetail,
+ NotNull<CompositableHost*> aCompositable,
+ const CompositableHandle& aHandle) {
+ switch (aDetail.type()) {
+ case CompositableOperationDetail::TOpRemoveTexture: {
+ const OpRemoveTexture& op = aDetail.get_OpRemoveTexture();
+
+ RefPtr<TextureHost> tex =
+ TextureHost::AsTextureHost(op.texture().AsParent());
+
+ MOZ_ASSERT(tex.get());
+ aCompositable->RemoveTextureHost(tex);
+ break;
+ }
+ case CompositableOperationDetail::TOpUseTexture: {
+ const OpUseTexture& op = aDetail.get_OpUseTexture();
+
+ AutoTArray<CompositableHost::TimedTexture, 4> textures;
+ for (auto& timedTexture : op.textures()) {
+ CompositableHost::TimedTexture* t = textures.AppendElement();
+ t->mTexture =
+ TextureHost::AsTextureHost(timedTexture.texture().AsParent());
+ MOZ_ASSERT(t->mTexture);
+ t->mTimeStamp = timedTexture.timeStamp();
+ t->mPictureRect = timedTexture.picture();
+ t->mFrameID = timedTexture.frameID();
+ t->mProducerID = timedTexture.producerID();
+ if (timedTexture.readLocked()) {
+ t->mTexture->SetReadLocked();
+ }
+ }
+ if (textures.Length() > 0) {
+ aCompositable->UseTextureHost(textures);
+
+ for (auto& timedTexture : op.textures()) {
+ RefPtr<TextureHost> texture =
+ TextureHost::AsTextureHost(timedTexture.texture().AsParent());
+ if (texture) {
+ texture->SetLastFwdTransactionId(mFwdTransactionId);
+ // Make sure that each texture was handled by the compositable
+ // because the recycling logic depends on it.
+ MOZ_ASSERT(texture->NumCompositableRefs() > 0);
+ }
+ }
+ }
+ break;
+ }
+ case CompositableOperationDetail::TOpUseRemoteTexture: {
+ const OpUseRemoteTexture& op = aDetail.get_OpUseRemoteTexture();
+ auto* host = aCompositable->AsWebRenderImageHost();
+ MOZ_ASSERT(host);
+
+ host->PushPendingRemoteTexture(op.textureId(), op.ownerId(),
+ GetChildProcessId(), op.size(),
+ op.textureFlags());
+ host->UseRemoteTexture();
+ break;
+ }
+ case CompositableOperationDetail::TOpEnableRemoteTexturePushCallback: {
+ const OpEnableRemoteTexturePushCallback& op =
+ aDetail.get_OpEnableRemoteTexturePushCallback();
+
+ aCompositable->SetAsyncRef(
+ AsyncCompositableRef(GetChildProcessId(), aHandle));
+ aCompositable->EnableRemoteTexturePushCallback(
+ op.ownerId(), GetChildProcessId(), op.size(), op.textureFlags());
+ break;
+ }
+ default: {
+ MOZ_ASSERT(false, "bad type");
+ }
+ }
+
+ return true;
+}
+
+void CompositableParentManager::DestroyActor(const OpDestroy& aOp) {
+ switch (aOp.type()) {
+ case OpDestroy::TPTexture: {
+ auto actor = aOp.get_PTexture().AsParent();
+ TextureHost::ReceivedDestroy(actor);
+ break;
+ }
+ case OpDestroy::TCompositableHandle: {
+ ReleaseCompositable(aOp.get_CompositableHandle());
+ break;
+ }
+ default: {
+ MOZ_ASSERT(false, "unsupported type");
+ }
+ }
+}
+
+RefPtr<CompositableHost> CompositableParentManager::AddCompositable(
+ const CompositableHandle& aHandle, const TextureInfo& aInfo) {
+ if (mCompositables.find(aHandle.Value()) != mCompositables.end()) {
+ NS_ERROR("Client should not allocate duplicate handles");
+ return nullptr;
+ }
+ if (!aHandle) {
+ NS_ERROR("Client should not allocate 0 as a handle");
+ return nullptr;
+ }
+
+ RefPtr<CompositableHost> host = CompositableHost::Create(aInfo);
+ if (!host) {
+ return nullptr;
+ }
+
+ mCompositables[aHandle.Value()] = host;
+ return host;
+}
+
+RefPtr<CompositableHost> CompositableParentManager::FindCompositable(
+ const CompositableHandle& aHandle) {
+ auto iter = mCompositables.find(aHandle.Value());
+ if (iter == mCompositables.end()) {
+ return nullptr;
+ }
+
+ return iter->second;
+}
+
+void CompositableParentManager::ReleaseCompositable(
+ const CompositableHandle& aHandle) {
+ auto iter = mCompositables.find(aHandle.Value());
+ if (iter == mCompositables.end()) {
+ return;
+ }
+ iter->second->OnReleased();
+ mCompositables.erase(iter);
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/ipc/CompositableTransactionParent.h b/gfx/layers/ipc/CompositableTransactionParent.h
new file mode 100644
index 0000000000..172c6bb035
--- /dev/null
+++ b/gfx/layers/ipc/CompositableTransactionParent.h
@@ -0,0 +1,64 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_LAYERS_COMPOSITABLETRANSACTIONPARENT_H
+#define MOZILLA_LAYERS_COMPOSITABLETRANSACTIONPARENT_H
+
+#include <vector> // for vector
+#include "mozilla/Attributes.h" // for override
+#include "mozilla/NotNull.h"
+#include "mozilla/layers/ISurfaceAllocator.h" // for ISurfaceAllocator
+#include "mozilla/layers/LayersMessages.h" // for EditReply, etc
+#include "mozilla/layers/TextureClient.h"
+#include "CompositableHost.h"
+
+namespace mozilla {
+namespace layers {
+
+// Since PCompositble has two potential manager protocols, we can't just call
+// the Manager() method usually generated when there's one manager protocol,
+// so both manager protocols implement this and we keep a reference to them
+// through this interface.
+class CompositableParentManager : public HostIPCAllocator {
+ public:
+ CompositableParentManager() = default;
+
+ void DestroyActor(const OpDestroy& aOp);
+
+ void UpdateFwdTransactionId(uint64_t aTransactionId) {
+ MOZ_ASSERT(mFwdTransactionId < aTransactionId);
+ mFwdTransactionId = aTransactionId;
+ }
+
+ uint64_t GetFwdTransactionId() { return mFwdTransactionId; }
+
+ RefPtr<CompositableHost> AddCompositable(const CompositableHandle& aHandle,
+ const TextureInfo& aInfo);
+ RefPtr<CompositableHost> FindCompositable(const CompositableHandle& aHandle);
+
+ protected:
+ /**
+ * Handle the IPDL messages that affect PCompositable actors.
+ */
+ bool ReceiveCompositableUpdate(const CompositableOperation& aEdit);
+ bool ReceiveCompositableUpdate(const CompositableOperationDetail& aDetail,
+ NotNull<CompositableHost*> aCompositable,
+ const CompositableHandle& aHandle);
+
+ void ReleaseCompositable(const CompositableHandle& aHandle);
+
+ uint64_t mFwdTransactionId = 0;
+
+ /**
+ * Mapping form IDs to CompositableHosts.
+ */
+ std::map<uint64_t, RefPtr<CompositableHost>> mCompositables;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif
diff --git a/gfx/layers/ipc/CompositorBench.cpp b/gfx/layers/ipc/CompositorBench.cpp
new file mode 100644
index 0000000000..0fb4d8012b
--- /dev/null
+++ b/gfx/layers/ipc/CompositorBench.cpp
@@ -0,0 +1,352 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "CompositorBench.h"
+
+#ifdef MOZ_COMPOSITOR_BENCH
+# include "mozilla/gfx/2D.h"
+# include "mozilla/layers/Compositor.h"
+# include "mozilla/layers/Effects.h"
+# include "mozilla/ProfilerMarkers.h"
+# include "mozilla/StaticPrefs_layers.h"
+# include "mozilla/TimeStamp.h"
+# include <math.h>
+
+# define TEST_STEPS 1000
+# define DURATION_THRESHOLD 30
+# define THRESHOLD_ABORT_COUNT 5
+
+namespace mozilla {
+namespace layers {
+
+using namespace mozilla::gfx;
+
+static float SimplePseudoRandom(int aStep, int aCount) {
+ srand(aStep * 1000 + aCount);
+ return static_cast<float>(rand()) / static_cast<float>(RAND_MAX);
+}
+
+class BenchTest {
+ public:
+ BenchTest(const char* aTestName) : mTestName(aTestName) {}
+
+ virtual ~BenchTest() = default;
+
+ virtual void Setup(Compositor* aCompositor, size_t aStep) {}
+ virtual void Teardown(Compositor* aCompositor) {}
+ virtual void DrawFrame(Compositor* aCompositor, const gfx::Rect& aScreenRect,
+ size_t aStep) = 0;
+
+ const char* ToString() { return mTestName; }
+
+ private:
+ const char* mTestName;
+};
+
+static void DrawFrameTrivialQuad(Compositor* aCompositor,
+ const gfx::Rect& aScreenRect, size_t aStep,
+ const EffectChain& effects) {
+ for (size_t i = 0; i < aStep * 10; i++) {
+ const gfx::Rect& rect = gfx::Rect(i % (int)aScreenRect.width,
+ (int)(i / aScreenRect.height), 1, 1);
+ const gfx::Rect& clipRect = aScreenRect;
+
+ float opacity = 1.f;
+
+ gfx::Matrix transform2d;
+
+ gfx::Matrix4x4 transform = gfx::Matrix4x4::From2D(transform2d);
+
+ aCompositor->DrawQuad(rect, clipRect, effects, opacity, transform);
+ }
+}
+
+static void DrawFrameStressQuad(Compositor* aCompositor,
+ const gfx::Rect& aScreenRect, size_t aStep,
+ const EffectChain& effects) {
+ for (size_t i = 0; i < aStep * 10; i++) {
+ const gfx::Rect& rect =
+ gfx::Rect(aScreenRect.width * SimplePseudoRandom(i, 0),
+ aScreenRect.height * SimplePseudoRandom(i, 1),
+ aScreenRect.width * SimplePseudoRandom(i, 2),
+ aScreenRect.height * SimplePseudoRandom(i, 3));
+ const gfx::Rect& clipRect = aScreenRect;
+
+ float opacity = 1.f;
+
+ gfx::Matrix transform2d;
+ transform2d = transform2d.PreRotate(SimplePseudoRandom(i, 4) * 70.f);
+
+ gfx::Matrix4x4 transform = gfx::Matrix4x4::From2D(transform2d);
+
+ aCompositor->DrawQuad(rect, clipRect, effects, opacity, transform);
+ }
+}
+
+class EffectSolidColorBench : public BenchTest {
+ public:
+ EffectSolidColorBench()
+ : BenchTest("EffectSolidColorBench (clear frame with EffectSolidColor)") {
+ }
+
+ void DrawFrame(Compositor* aCompositor, const gfx::Rect& aScreenRect,
+ size_t aStep) {
+ float tmp;
+ float red = modff(aStep * 0.03f, &tmp);
+ EffectChain effects;
+ effects.mPrimaryEffect =
+ new EffectSolidColor(gfx::DeviceColor(red, 0.4f, 0.4f, 1.0f));
+
+ const gfx::Rect& rect = aScreenRect;
+ const gfx::Rect& clipRect = aScreenRect;
+
+ float opacity = 1.f;
+
+ gfx::Matrix4x4 transform;
+ aCompositor->DrawQuad(rect, clipRect, effects, opacity, transform);
+ }
+};
+
+class EffectSolidColorTrivialBench : public BenchTest {
+ public:
+ EffectSolidColorTrivialBench()
+ : BenchTest("EffectSolidColorTrivialBench (10s 1x1 EffectSolidColor)") {}
+
+ void DrawFrame(Compositor* aCompositor, const gfx::Rect& aScreenRect,
+ size_t aStep) {
+ EffectChain effects;
+ effects.mPrimaryEffect = CreateEffect(aStep);
+
+ DrawFrameTrivialQuad(aCompositor, aScreenRect, aStep, effects);
+ }
+
+ already_AddRefed<Effect> CreateEffect(size_t i) {
+ float tmp;
+ float red = modff(i * 0.03f, &tmp);
+ EffectChain effects;
+ return MakeAndAddRef<EffectSolidColor>(
+ gfx::DeviceColor(red, 0.4f, 0.4f, 1.0f));
+ }
+};
+
+class EffectSolidColorStressBench : public BenchTest {
+ public:
+ EffectSolidColorStressBench()
+ : BenchTest(
+ "EffectSolidColorStressBench (10s various EffectSolidColor)") {}
+
+ void DrawFrame(Compositor* aCompositor, const gfx::Rect& aScreenRect,
+ size_t aStep) {
+ EffectChain effects;
+ effects.mPrimaryEffect = CreateEffect(aStep);
+
+ DrawFrameStressQuad(aCompositor, aScreenRect, aStep, effects);
+ }
+
+ already_AddRefed<Effect> CreateEffect(size_t i) {
+ float tmp;
+ float red = modff(i * 0.03f, &tmp);
+ EffectChain effects;
+ return MakeAndAddRef<EffectSolidColor>(
+ gfx::DeviceColor(red, 0.4f, 0.4f, 1.0f));
+ }
+};
+
+class UploadBench : public BenchTest {
+ public:
+ UploadBench() : BenchTest("Upload Bench (10s 256x256 upload)") {}
+
+ uint32_t* mBuf;
+ RefPtr<DataSourceSurface> mSurface;
+ RefPtr<DataTextureSource> mTexture;
+
+ virtual void Setup(Compositor* aCompositor, size_t aStep) {
+ int bytesPerPixel = 4;
+ int w = 256;
+ int h = 256;
+ mBuf = (uint32_t*)malloc(w * h * sizeof(uint32_t));
+
+ mSurface = Factory::CreateWrappingDataSourceSurface(
+ reinterpret_cast<uint8_t*>(mBuf), w * bytesPerPixel, IntSize(w, h),
+ SurfaceFormat::B8G8R8A8);
+ mTexture = aCompositor->CreateDataTextureSource();
+ }
+
+ virtual void Teardown(Compositor* aCompositor) {
+ mSurface = nullptr;
+ mTexture = nullptr;
+ free(mBuf);
+ }
+
+ void DrawFrame(Compositor* aCompositor, const gfx::Rect& aScreenRect,
+ size_t aStep) {
+ for (size_t i = 0; i < aStep * 10; i++) {
+ mTexture->Update(mSurface);
+ }
+ }
+};
+
+class TrivialTexturedQuadBench : public BenchTest {
+ public:
+ TrivialTexturedQuadBench()
+ : BenchTest("Trvial Textured Quad (10s 256x256 quads)") {}
+
+ uint32_t* mBuf;
+ RefPtr<DataSourceSurface> mSurface;
+ RefPtr<DataTextureSource> mTexture;
+
+ virtual void Setup(Compositor* aCompositor, size_t aStep) {
+ int bytesPerPixel = 4;
+ size_t w = 256;
+ size_t h = 256;
+ mBuf = (uint32_t*)malloc(w * h * sizeof(uint32_t));
+ for (size_t i = 0; i < w * h; i++) {
+ mBuf[i] = 0xFF00008F;
+ }
+
+ mSurface = Factory::CreateWrappingDataSourceSurface(
+ reinterpret_cast<uint8_t*>(mBuf), w * bytesPerPixel, IntSize(w, h),
+ SurfaceFormat::B8G8R8A8);
+ mTexture = aCompositor->CreateDataTextureSource();
+ mTexture->Update(mSurface);
+ }
+
+ void DrawFrame(Compositor* aCompositor, const gfx::Rect& aScreenRect,
+ size_t aStep) {
+ EffectChain effects;
+ effects.mPrimaryEffect = CreateEffect(aStep);
+
+ DrawFrameTrivialQuad(aCompositor, aScreenRect, aStep, effects);
+ }
+
+ virtual void Teardown(Compositor* aCompositor) {
+ mSurface = nullptr;
+ mTexture = nullptr;
+ free(mBuf);
+ }
+
+ already_AddRefed<Effect> CreateEffect(size_t i) {
+ return CreateTexturedEffect(SurfaceFormat::B8G8R8A8, mTexture,
+ SamplingFilter::POINT, true);
+ }
+};
+
+class StressTexturedQuadBench : public BenchTest {
+ public:
+ StressTexturedQuadBench()
+ : BenchTest("Stress Textured Quad (10s 256x256 quads)") {}
+
+ uint32_t* mBuf;
+ RefPtr<DataSourceSurface> mSurface;
+ RefPtr<DataTextureSource> mTexture;
+
+ virtual void Setup(Compositor* aCompositor, size_t aStep) {
+ int bytesPerPixel = 4;
+ size_t w = 256;
+ size_t h = 256;
+ mBuf = (uint32_t*)malloc(w * h * sizeof(uint32_t));
+ for (size_t i = 0; i < w * h; i++) {
+ mBuf[i] = 0xFF00008F;
+ }
+
+ mSurface = Factory::CreateWrappingDataSourceSurface(
+ reinterpret_cast<uint8_t*>(mBuf), w * bytesPerPixel, IntSize(w, h),
+ SurfaceFormat::B8G8R8A8);
+ mTexture = aCompositor->CreateDataTextureSource();
+ mTexture->Update(mSurface);
+ }
+
+ void DrawFrame(Compositor* aCompositor, const gfx::Rect& aScreenRect,
+ size_t aStep) {
+ EffectChain effects;
+ effects.mPrimaryEffect = CreateEffect(aStep);
+
+ DrawFrameStressQuad(aCompositor, aScreenRect, aStep, effects);
+ }
+
+ virtual void Teardown(Compositor* aCompositor) {
+ mSurface = nullptr;
+ mTexture = nullptr;
+ free(mBuf);
+ }
+
+ virtual already_AddRefed<Effect> CreateEffect(size_t i) {
+ return CreateTexturedEffect(SurfaceFormat::B8G8R8A8, mTexture,
+ SamplingFilter::POINT, true);
+ }
+};
+
+static void RunCompositorBench(Compositor* aCompositor,
+ const gfx::Rect& aScreenRect) {
+ std::vector<BenchTest*> tests;
+
+ tests.push_back(new EffectSolidColorBench());
+ tests.push_back(new UploadBench());
+ tests.push_back(new EffectSolidColorTrivialBench());
+ tests.push_back(new EffectSolidColorStressBench());
+ tests.push_back(new TrivialTexturedQuadBench());
+ tests.push_back(new StressTexturedQuadBench());
+
+ for (size_t i = 0; i < tests.size(); i++) {
+ BenchTest* test = tests[i];
+ std::vector<TimeDuration> results;
+ int testsOverThreshold = 0;
+ PROFILER_MARKER_UNTYPED(
+ ProfilerString8View::WrapNullTerminatedString(test->ToString()),
+ GRAPHICS);
+ for (size_t j = 0; j < TEST_STEPS; j++) {
+ test->Setup(aCompositor, j);
+
+ TimeStamp start = TimeStamp::Now();
+ IntRect screenRect(aScreenRect.x, aScreenRect.y, aScreenRect.width,
+ aScreenRect.height);
+ aCompositor->BeginFrame(IntRect(screenRect.x, screenRect.y,
+ screenRect.width, screenRect.height),
+ nullptr, aScreenRect, nullptr, nullptr);
+
+ test->DrawFrame(aCompositor, aScreenRect, j);
+
+ aCompositor->EndFrame();
+ results.push_back(TimeStamp::Now() - start);
+
+ if (results[j].ToMilliseconds() > DURATION_THRESHOLD) {
+ testsOverThreshold++;
+ if (testsOverThreshold == THRESHOLD_ABORT_COUNT) {
+ test->Teardown(aCompositor);
+ break;
+ }
+ } else {
+ testsOverThreshold = 0;
+ }
+ test->Teardown(aCompositor);
+ }
+
+ printf_stderr("%s\n", test->ToString());
+ printf_stderr("Run step, Time (ms)\n");
+ for (size_t j = 0; j < results.size(); j++) {
+ printf_stderr("%i,%f\n", j, (float)results[j].ToMilliseconds());
+ }
+ printf_stderr("\n", test->ToString());
+ }
+
+ for (size_t i = 0; i < tests.size(); i++) {
+ delete tests[i];
+ }
+}
+
+void CompositorBench(Compositor* aCompositor, const gfx::IntRect& aScreenRect) {
+ static bool sRanBenchmark = false;
+ bool wantBenchmark = StaticPrefs::layers_bench_enabled();
+ if (wantBenchmark && !sRanBenchmark) {
+ RunCompositorBench(aCompositor, aScreenRect);
+ }
+ sRanBenchmark = wantBenchmark;
+}
+
+} // namespace layers
+} // namespace mozilla
+
+#endif
diff --git a/gfx/layers/ipc/CompositorBench.h b/gfx/layers/ipc/CompositorBench.h
new file mode 100644
index 0000000000..4a3c5874e9
--- /dev/null
+++ b/gfx/layers/ipc/CompositorBench.h
@@ -0,0 +1,30 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_CompositorBench_h
+#define mozilla_layers_CompositorBench_h
+
+#include "mozilla/gfx/Rect.h" // for Rect
+
+namespace mozilla {
+namespace layers {
+
+class Compositor;
+
+// Uncomment this line to rebuild with compositor bench.
+// #define MOZ_COMPOSITOR_BENCH
+
+#ifdef MOZ_COMPOSITOR_BENCH
+void CompositorBench(Compositor* aCompositor, const gfx::IntRect& aScreenRect);
+#else
+static inline void CompositorBench(Compositor* aCompositor,
+ const gfx::IntRect& aScreenRect) {}
+#endif
+
+} // namespace layers
+} // namespace mozilla
+
+#endif
diff --git a/gfx/layers/ipc/CompositorBridgeChild.cpp b/gfx/layers/ipc/CompositorBridgeChild.cpp
new file mode 100644
index 0000000000..72210607e2
--- /dev/null
+++ b/gfx/layers/ipc/CompositorBridgeChild.cpp
@@ -0,0 +1,648 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/layers/CompositorBridgeChild.h"
+#include "mozilla/layers/CompositorBridgeParent.h"
+#include "mozilla/layers/CompositorThread.h"
+#include <stddef.h> // for size_t
+#include "base/task.h" // for NewRunnableMethod, etc
+#include "mozilla/StaticPrefs_layers.h"
+#include "mozilla/layers/CompositorManagerChild.h"
+#include "mozilla/layers/ImageBridgeChild.h"
+#include "mozilla/layers/APZChild.h"
+#include "mozilla/layers/IAPZCTreeManager.h"
+#include "mozilla/layers/APZCTreeManagerChild.h"
+#include "mozilla/layers/CanvasChild.h"
+#include "mozilla/layers/WebRenderLayerManager.h"
+#include "mozilla/layers/PTextureChild.h"
+#include "mozilla/layers/TextureClient.h" // for TextureClient
+#include "mozilla/layers/TextureClientPool.h" // for TextureClientPool
+#include "mozilla/layers/WebRenderBridgeChild.h"
+#include "mozilla/layers/SyncObject.h" // for SyncObjectClient
+#include "mozilla/gfx/gfxVars.h"
+#include "mozilla/gfx/GPUProcessManager.h"
+#include "mozilla/gfx/Logging.h"
+#include "mozilla/ipc/Endpoint.h"
+#include "mozilla/mozalloc.h" // for operator new, etc
+#include "mozilla/Telemetry.h"
+#include "gfxConfig.h"
+#include "nsDebug.h" // for NS_WARNING
+#include "nsISupportsImpl.h" // for MOZ_COUNT_CTOR, etc
+#include "nsTArray.h" // for nsTArray, nsTArray_Impl
+#include "mozilla/dom/BrowserChild.h"
+#include "mozilla/dom/BrowserParent.h"
+#include "mozilla/dom/ContentChild.h"
+#include "mozilla/Unused.h"
+#include "mozilla/DebugOnly.h"
+#include "mozilla/SpinEventLoopUntil.h"
+#include "nsThreadUtils.h"
+#if defined(XP_WIN)
+# include "WinUtils.h"
+#endif
+#include "mozilla/widget/CompositorWidget.h"
+#ifdef MOZ_WIDGET_SUPPORTS_OOP_COMPOSITING
+# include "mozilla/widget/CompositorWidgetChild.h"
+#endif
+#include "VsyncSource.h"
+
+using mozilla::Unused;
+using mozilla::gfx::GPUProcessManager;
+
+namespace mozilla {
+namespace layers {
+
+static int sShmemCreationCounter = 0;
+
+static void ResetShmemCounter() { sShmemCreationCounter = 0; }
+
+static void ShmemAllocated(CompositorBridgeChild* aProtocol) {
+ sShmemCreationCounter++;
+ if (sShmemCreationCounter > 256) {
+ aProtocol->SendSyncWithCompositor();
+ ResetShmemCounter();
+ MOZ_PERFORMANCE_WARNING(
+ "gfx", "The number of shmem allocations is too damn high!");
+ }
+}
+
+static StaticRefPtr<CompositorBridgeChild> sCompositorBridge;
+
+Atomic<int32_t> KnowsCompositor::sSerialCounter(0);
+
+CompositorBridgeChild::CompositorBridgeChild(CompositorManagerChild* aManager)
+ : mCompositorManager(aManager),
+ mIdNamespace(0),
+ mResourceId(0),
+ mCanSend(false),
+ mActorDestroyed(false),
+ mFwdTransactionId(0),
+ mThread(NS_GetCurrentThread()),
+ mProcessToken(0),
+ mSectionAllocator(nullptr) {
+ MOZ_ASSERT(NS_IsMainThread());
+}
+
+CompositorBridgeChild::~CompositorBridgeChild() {
+ if (mCanSend) {
+ gfxCriticalError() << "CompositorBridgeChild was not deinitialized";
+ }
+}
+
+bool CompositorBridgeChild::IsSameProcess() const {
+ return OtherPid() == base::GetCurrentProcId();
+}
+
+void CompositorBridgeChild::PrepareFinalDestroy() {
+ // Because of medium high priority DidComposite, we need to repost to
+ // medium high priority queue to ensure the actor is destroyed after possible
+ // pending DidComposite message.
+ nsCOMPtr<nsIRunnable> runnable =
+ NewRunnableMethod("CompositorBridgeChild::AfterDestroy", this,
+ &CompositorBridgeChild::AfterDestroy);
+ NS_DispatchToCurrentThreadQueue(runnable.forget(),
+ EventQueuePriority::MediumHigh);
+}
+
+void CompositorBridgeChild::AfterDestroy() {
+ // Note that we cannot rely upon mCanSend here because we already set that to
+ // false to prevent normal IPDL calls from being made after SendWillClose.
+ // The only time we should not issue Send__delete__ is if the actor is already
+ // destroyed, e.g. the compositor process crashed.
+ if (!mActorDestroyed) {
+ // We saw this send fail quite often with "Channel closing", probably a race
+ // with the other side closing or some event scheduling order.
+ if (GetIPCChannel()->CanSend()) {
+ Send__delete__(this);
+ }
+ mActorDestroyed = true;
+ }
+
+ if (mCanvasChild) {
+ mCanvasChild->Destroy();
+ }
+
+ if (sCompositorBridge == this) {
+ sCompositorBridge = nullptr;
+ }
+}
+
+void CompositorBridgeChild::Destroy() {
+ // This must not be called from the destructor!
+ mTexturesWaitingNotifyNotUsed.clear();
+
+ // Destroying the layer manager may cause all sorts of things to happen, so
+ // let's make sure there is still a reference to keep this alive whatever
+ // happens.
+ RefPtr<CompositorBridgeChild> selfRef = this;
+
+ for (size_t i = 0; i < mTexturePools.Length(); i++) {
+ mTexturePools[i]->Destroy();
+ }
+
+ if (mSectionAllocator) {
+ delete mSectionAllocator;
+ mSectionAllocator = nullptr;
+ }
+
+ if (mLayerManager) {
+ mLayerManager->Destroy();
+ mLayerManager = nullptr;
+ }
+
+ if (!mCanSend) {
+ // We may have already called destroy but still have lingering references
+ // or CompositorBridgeChild::ActorDestroy was called. Ensure that we do our
+ // post destroy clean up no matter what. It is safe to call multiple times.
+ NS_GetCurrentThread()->Dispatch(
+ NewRunnableMethod("CompositorBridgeChild::PrepareFinalDestroy", selfRef,
+ &CompositorBridgeChild::PrepareFinalDestroy));
+ return;
+ }
+
+ AutoTArray<PWebRenderBridgeChild*, 16> wrBridges;
+ ManagedPWebRenderBridgeChild(wrBridges);
+ for (int i = wrBridges.Length() - 1; i >= 0; --i) {
+ RefPtr<WebRenderBridgeChild> wrBridge =
+ static_cast<WebRenderBridgeChild*>(wrBridges[i]);
+ wrBridge->Destroy(/* aIsSync */ false);
+ }
+
+ AutoTArray<PAPZChild*, 16> apzChildren;
+ ManagedPAPZChild(apzChildren);
+ for (PAPZChild* child : apzChildren) {
+ Unused << child->SendDestroy();
+ }
+
+ const ManagedContainer<PTextureChild>& textures = ManagedPTextureChild();
+ for (const auto& key : textures) {
+ RefPtr<TextureClient> texture = TextureClient::AsTextureClient(key);
+
+ if (texture) {
+ texture->Destroy();
+ }
+ }
+
+ // The WillClose message is synchronous, so we know that after it returns
+ // any messages sent by the above code will have been processed on the
+ // other side.
+ SendWillClose();
+ mCanSend = false;
+
+ // We no longer care about unexpected shutdowns, in the remote process case.
+ mProcessToken = 0;
+
+ // The call just made to SendWillClose can result in IPC from the
+ // CompositorBridgeParent to the CompositorBridgeChild (e.g. caused by the
+ // destruction of shared memory). We need to ensure this gets processed by the
+ // CompositorBridgeChild before it gets destroyed. It suffices to ensure that
+ // events already in the thread get processed before the
+ // CompositorBridgeChild is destroyed, so we add a task to the thread to
+ // handle compositor destruction.
+
+ // From now on we can't send any message message.
+ NS_GetCurrentThread()->Dispatch(
+ NewRunnableMethod("CompositorBridgeChild::PrepareFinalDestroy", selfRef,
+ &CompositorBridgeChild::PrepareFinalDestroy));
+}
+
+// static
+void CompositorBridgeChild::ShutDown() {
+ if (sCompositorBridge) {
+ sCompositorBridge->Destroy();
+ SpinEventLoopUntil("CompositorBridgeChild::ShutDown"_ns,
+ [&]() { return !sCompositorBridge; });
+ }
+}
+
+void CompositorBridgeChild::InitForContent(uint32_t aNamespace) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aNamespace);
+
+ if (RefPtr<CompositorBridgeChild> old = sCompositorBridge.forget()) {
+ // Note that at this point, ActorDestroy may not have been called yet,
+ // meaning mCanSend is still true. In this case we will try to send a
+ // synchronous WillClose message to the parent, and will certainly get
+ // a false result and a MsgDropped processing error. This is okay.
+ old->Destroy();
+ }
+
+ mCanSend = true;
+ mIdNamespace = aNamespace;
+
+ sCompositorBridge = this;
+}
+
+void CompositorBridgeChild::InitForWidget(uint64_t aProcessToken,
+ WebRenderLayerManager* aLayerManager,
+ uint32_t aNamespace) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aProcessToken);
+ MOZ_ASSERT(aLayerManager);
+ MOZ_ASSERT(aNamespace);
+
+ mCanSend = true;
+ mProcessToken = aProcessToken;
+ mLayerManager = aLayerManager;
+ mIdNamespace = aNamespace;
+}
+
+/*static*/
+CompositorBridgeChild* CompositorBridgeChild::Get() {
+ // This is only expected to be used in child processes. While the parent
+ // process does have CompositorBridgeChild instances, it has _multiple_ (one
+ // per window), and therefore there is no global singleton available.
+ MOZ_ASSERT(!XRE_IsParentProcess());
+ return sCompositorBridge;
+}
+
+/* static */
+bool CompositorBridgeChild::CompositorIsInGPUProcess() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (XRE_IsParentProcess()) {
+ return !!GPUProcessManager::Get()->GetGPUChild();
+ }
+
+ MOZ_ASSERT(XRE_IsContentProcess());
+ CompositorBridgeChild* bridge = CompositorBridgeChild::Get();
+ if (!bridge) {
+ return false;
+ }
+
+ return bridge->OtherPid() != dom::ContentChild::GetSingleton()->OtherPid();
+}
+
+mozilla::ipc::IPCResult CompositorBridgeChild::RecvDidComposite(
+ const LayersId& aId, const nsTArray<TransactionId>& aTransactionIds,
+ const TimeStamp& aCompositeStart, const TimeStamp& aCompositeEnd) {
+ // Hold a reference to keep texture pools alive. See bug 1387799
+ const auto texturePools = mTexturePools.Clone();
+
+ for (const auto& id : aTransactionIds) {
+ if (mLayerManager) {
+ MOZ_ASSERT(!aId.IsValid());
+ MOZ_ASSERT(mLayerManager->GetBackendType() == LayersBackend::LAYERS_WR);
+ // Hold a reference to keep LayerManager alive. See Bug 1242668.
+ RefPtr<WebRenderLayerManager> m = mLayerManager;
+ m->DidComposite(id, aCompositeStart, aCompositeEnd);
+ } else if (aId.IsValid()) {
+ RefPtr<dom::BrowserChild> child = dom::BrowserChild::GetFrom(aId);
+ if (child) {
+ child->DidComposite(id, aCompositeStart, aCompositeEnd);
+ }
+ }
+ }
+
+ for (size_t i = 0; i < texturePools.Length(); i++) {
+ texturePools[i]->ReturnDeferredClients();
+ }
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult CompositorBridgeChild::RecvNotifyFrameStats(
+ nsTArray<FrameStats>&& aFrameStats) {
+ gfxPlatform::GetPlatform()->NotifyFrameStats(std::move(aFrameStats));
+ return IPC_OK();
+}
+
+void CompositorBridgeChild::ActorDestroy(ActorDestroyReason aWhy) {
+ if (aWhy == AbnormalShutdown) {
+ // If the parent side runs into a problem then the actor will be destroyed.
+ // There is nothing we can do in the child side, here sets mCanSend as
+ // false.
+ gfxCriticalNote << "CompositorBridgeChild receives IPC close with "
+ "reason=AbnormalShutdown";
+ }
+
+ mCanSend = false;
+ mActorDestroyed = true;
+
+ if (mProcessToken && XRE_IsParentProcess()) {
+ GPUProcessManager::Get()->NotifyRemoteActorDestroyed(mProcessToken);
+ }
+}
+
+bool CompositorBridgeChild::SendWillClose() {
+ MOZ_RELEASE_ASSERT(mCanSend);
+ return PCompositorBridgeChild::SendWillClose();
+}
+
+bool CompositorBridgeChild::SendPause() {
+ if (!mCanSend) {
+ return false;
+ }
+ return PCompositorBridgeChild::SendPause();
+}
+
+bool CompositorBridgeChild::SendResume() {
+ if (!mCanSend) {
+ return false;
+ }
+ return PCompositorBridgeChild::SendResume();
+}
+
+bool CompositorBridgeChild::SendResumeAsync() {
+ if (!mCanSend) {
+ return false;
+ }
+ return PCompositorBridgeChild::SendResumeAsync();
+}
+
+bool CompositorBridgeChild::SendAdoptChild(const LayersId& id) {
+ if (!mCanSend) {
+ return false;
+ }
+ return PCompositorBridgeChild::SendAdoptChild(id);
+}
+
+bool CompositorBridgeChild::SendFlushRendering(
+ const wr::RenderReasons& aReasons) {
+ if (!mCanSend) {
+ return false;
+ }
+ return PCompositorBridgeChild::SendFlushRendering(aReasons);
+}
+
+bool CompositorBridgeChild::SendStartFrameTimeRecording(
+ const int32_t& bufferSize, uint32_t* startIndex) {
+ if (!mCanSend) {
+ return false;
+ }
+ return PCompositorBridgeChild::SendStartFrameTimeRecording(bufferSize,
+ startIndex);
+}
+
+bool CompositorBridgeChild::SendStopFrameTimeRecording(
+ const uint32_t& startIndex, nsTArray<float>* intervals) {
+ if (!mCanSend) {
+ return false;
+ }
+ return PCompositorBridgeChild::SendStopFrameTimeRecording(startIndex,
+ intervals);
+}
+
+PTextureChild* CompositorBridgeChild::AllocPTextureChild(
+ const SurfaceDescriptor&, ReadLockDescriptor&, const LayersBackend&,
+ const TextureFlags&, const LayersId&, const uint64_t& aSerial,
+ const wr::MaybeExternalImageId& aExternalImageId) {
+ return TextureClient::CreateIPDLActor();
+}
+
+bool CompositorBridgeChild::DeallocPTextureChild(PTextureChild* actor) {
+ return TextureClient::DestroyIPDLActor(actor);
+}
+
+mozilla::ipc::IPCResult CompositorBridgeChild::RecvParentAsyncMessages(
+ nsTArray<AsyncParentMessageData>&& aMessages) {
+ for (AsyncParentMessageArray::index_type i = 0; i < aMessages.Length(); ++i) {
+ const AsyncParentMessageData& message = aMessages[i];
+
+ switch (message.type()) {
+ case AsyncParentMessageData::TOpNotifyNotUsed: {
+ const OpNotifyNotUsed& op = message.get_OpNotifyNotUsed();
+ NotifyNotUsed(op.TextureId(), op.fwdTransactionId());
+ break;
+ }
+ default:
+ NS_ERROR("unknown AsyncParentMessageData type");
+ return IPC_FAIL_NO_REASON(this);
+ }
+ }
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult CompositorBridgeChild::RecvObserveLayersUpdate(
+ const LayersId& aLayersId, const LayersObserverEpoch& aEpoch,
+ const bool& aActive) {
+ // This message is sent via the window compositor, not the tab compositor -
+ // however it still has a layers id.
+ MOZ_ASSERT(aLayersId.IsValid());
+ MOZ_ASSERT(XRE_IsParentProcess());
+
+ if (RefPtr<dom::BrowserParent> tab =
+ dom::BrowserParent::GetBrowserParentFromLayersId(aLayersId)) {
+ tab->LayerTreeUpdate(aEpoch, aActive);
+ }
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult CompositorBridgeChild::RecvCompositorOptionsChanged(
+ const LayersId& aLayersId, const CompositorOptions& aNewOptions) {
+ MOZ_ASSERT(aLayersId.IsValid());
+ MOZ_ASSERT(XRE_IsParentProcess());
+
+ if (RefPtr<dom::BrowserParent> tab =
+ dom::BrowserParent::GetBrowserParentFromLayersId(aLayersId)) {
+ Unused << tab->SendCompositorOptionsChanged(aNewOptions);
+ }
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult CompositorBridgeChild::RecvNotifyJankedAnimations(
+ const LayersId& aLayersId, nsTArray<uint64_t>&& aJankedAnimations) {
+ if (mLayerManager) {
+ MOZ_ASSERT(!aLayersId.IsValid());
+ mLayerManager->UpdatePartialPrerenderedAnimations(aJankedAnimations);
+ } else if (aLayersId.IsValid()) {
+ RefPtr<dom::BrowserChild> child = dom::BrowserChild::GetFrom(aLayersId);
+ if (child) {
+ child->NotifyJankedAnimations(aJankedAnimations);
+ }
+ }
+
+ return IPC_OK();
+}
+
+void CompositorBridgeChild::HoldUntilCompositableRefReleasedIfNecessary(
+ TextureClient* aClient) {
+ if (!aClient) {
+ return;
+ }
+
+ bool waitNotifyNotUsed =
+ aClient->GetFlags() & TextureFlags::RECYCLE ||
+ aClient->GetFlags() & TextureFlags::WAIT_HOST_USAGE_END;
+ if (!waitNotifyNotUsed) {
+ return;
+ }
+
+ aClient->SetLastFwdTransactionId(GetFwdTransactionId());
+ mTexturesWaitingNotifyNotUsed.emplace(aClient->GetSerial(), aClient);
+}
+
+void CompositorBridgeChild::NotifyNotUsed(uint64_t aTextureId,
+ uint64_t aFwdTransactionId) {
+ auto it = mTexturesWaitingNotifyNotUsed.find(aTextureId);
+ if (it != mTexturesWaitingNotifyNotUsed.end()) {
+ if (aFwdTransactionId < it->second->GetLastFwdTransactionId()) {
+ // Released on host side, but client already requested newer use texture.
+ return;
+ }
+ mTexturesWaitingNotifyNotUsed.erase(it);
+ }
+}
+
+void CompositorBridgeChild::CancelWaitForNotifyNotUsed(uint64_t aTextureId) {
+ mTexturesWaitingNotifyNotUsed.erase(aTextureId);
+}
+
+FixedSizeSmallShmemSectionAllocator*
+CompositorBridgeChild::GetTileLockAllocator() {
+ if (!IPCOpen()) {
+ return nullptr;
+ }
+
+ if (!mSectionAllocator) {
+ mSectionAllocator = new FixedSizeSmallShmemSectionAllocator(this);
+ }
+ return mSectionAllocator;
+}
+
+PTextureChild* CompositorBridgeChild::CreateTexture(
+ const SurfaceDescriptor& aSharedData, ReadLockDescriptor&& aReadLock,
+ LayersBackend aLayersBackend, TextureFlags aFlags,
+ const dom::ContentParentId& aContentId, uint64_t aSerial,
+ wr::MaybeExternalImageId& aExternalImageId) {
+ PTextureChild* textureChild =
+ AllocPTextureChild(aSharedData, aReadLock, aLayersBackend, aFlags,
+ LayersId{0} /* FIXME */, aSerial, aExternalImageId);
+
+ return SendPTextureConstructor(
+ textureChild, aSharedData, std::move(aReadLock), aLayersBackend, aFlags,
+ LayersId{0} /* FIXME? */, aSerial, aExternalImageId);
+}
+
+already_AddRefed<CanvasChild> CompositorBridgeChild::GetCanvasChild() {
+ MOZ_ASSERT(gfx::gfxVars::RemoteCanvasEnabled());
+
+ if (CanvasChild::Deactivated()) {
+ return nullptr;
+ }
+
+ if (!mCanvasChild) {
+ ipc::Endpoint<PCanvasParent> parentEndpoint;
+ ipc::Endpoint<PCanvasChild> childEndpoint;
+ nsresult rv = PCanvas::CreateEndpoints(OtherPid(), base::GetCurrentProcId(),
+ &parentEndpoint, &childEndpoint);
+ if (NS_SUCCEEDED(rv)) {
+ Unused << SendInitPCanvasParent(std::move(parentEndpoint));
+ mCanvasChild = new CanvasChild(std::move(childEndpoint));
+ }
+ }
+
+ return do_AddRef(mCanvasChild);
+}
+
+void CompositorBridgeChild::EndCanvasTransaction() {
+ if (mCanvasChild) {
+ mCanvasChild->EndTransaction();
+ if (mCanvasChild->ShouldBeCleanedUp()) {
+ mCanvasChild->Destroy();
+ Unused << SendReleasePCanvasParent();
+ mCanvasChild = nullptr;
+ }
+ }
+}
+
+bool CompositorBridgeChild::AllocUnsafeShmem(size_t aSize, ipc::Shmem* aShmem) {
+ ShmemAllocated(this);
+ return PCompositorBridgeChild::AllocUnsafeShmem(aSize, aShmem);
+}
+
+bool CompositorBridgeChild::AllocShmem(size_t aSize, ipc::Shmem* aShmem) {
+ ShmemAllocated(this);
+ return PCompositorBridgeChild::AllocShmem(aSize, aShmem);
+}
+
+bool CompositorBridgeChild::DeallocShmem(ipc::Shmem& aShmem) {
+ if (!mCanSend) {
+ return false;
+ }
+ return PCompositorBridgeChild::DeallocShmem(aShmem);
+}
+
+widget::PCompositorWidgetChild*
+CompositorBridgeChild::AllocPCompositorWidgetChild(
+ const CompositorWidgetInitData& aInitData) {
+ // We send the constructor manually.
+ MOZ_CRASH("Should not be called");
+ return nullptr;
+}
+
+bool CompositorBridgeChild::DeallocPCompositorWidgetChild(
+ PCompositorWidgetChild* aActor) {
+#ifdef MOZ_WIDGET_SUPPORTS_OOP_COMPOSITING
+ delete aActor;
+ return true;
+#else
+ return false;
+#endif
+}
+
+PAPZCTreeManagerChild* CompositorBridgeChild::AllocPAPZCTreeManagerChild(
+ const LayersId& aLayersId) {
+ APZCTreeManagerChild* child = new APZCTreeManagerChild();
+ child->AddIPDLReference();
+
+ return child;
+}
+
+PAPZChild* CompositorBridgeChild::AllocPAPZChild(const LayersId& aLayersId) {
+ // We send the constructor manually.
+ MOZ_CRASH("Should not be called");
+ return nullptr;
+}
+
+bool CompositorBridgeChild::DeallocPAPZChild(PAPZChild* aActor) {
+ delete aActor;
+ return true;
+}
+
+bool CompositorBridgeChild::DeallocPAPZCTreeManagerChild(
+ PAPZCTreeManagerChild* aActor) {
+ APZCTreeManagerChild* child = static_cast<APZCTreeManagerChild*>(aActor);
+ child->ReleaseIPDLReference();
+ return true;
+}
+
+// -
+
+PWebRenderBridgeChild* CompositorBridgeChild::AllocPWebRenderBridgeChild(
+ const wr::PipelineId& aPipelineId, const LayoutDeviceIntSize&,
+ const WindowKind&) {
+ WebRenderBridgeChild* child = new WebRenderBridgeChild(aPipelineId);
+ child->AddIPDLReference();
+ return child;
+}
+
+bool CompositorBridgeChild::DeallocPWebRenderBridgeChild(
+ PWebRenderBridgeChild* aActor) {
+ WebRenderBridgeChild* child = static_cast<WebRenderBridgeChild*>(aActor);
+ child->ReleaseIPDLReference();
+ return true;
+}
+
+uint64_t CompositorBridgeChild::GetNextResourceId() {
+ ++mResourceId;
+ MOZ_RELEASE_ASSERT(mResourceId != UINT32_MAX);
+
+ uint64_t id = mIdNamespace;
+ id = (id << 32) | mResourceId;
+
+ return id;
+}
+
+wr::MaybeExternalImageId CompositorBridgeChild::GetNextExternalImageId() {
+ return Some(wr::ToExternalImageId(GetNextResourceId()));
+}
+
+wr::PipelineId CompositorBridgeChild::GetNextPipelineId() {
+ return wr::AsPipelineId(GetNextResourceId());
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/ipc/CompositorBridgeChild.h b/gfx/layers/ipc/CompositorBridgeChild.h
new file mode 100644
index 0000000000..723cbc5705
--- /dev/null
+++ b/gfx/layers/ipc/CompositorBridgeChild.h
@@ -0,0 +1,251 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_CompositorBridgeChild_h
+#define mozilla_layers_CompositorBridgeChild_h
+
+#include "base/basictypes.h" // for DISALLOW_EVIL_CONSTRUCTORS
+#include "mozilla/Assertions.h" // for MOZ_ASSERT_HELPER2
+#include "mozilla/Attributes.h" // for override
+#include "mozilla/Monitor.h"
+#include "mozilla/ipc/ProtocolUtils.h"
+#include "mozilla/layers/PCompositorBridgeChild.h"
+#include "mozilla/layers/TextureForwarder.h" // for TextureForwarder
+#include "mozilla/webrender/WebRenderTypes.h"
+#include "nsClassHashtable.h" // for nsClassHashtable
+#include "nsCOMPtr.h" // for nsCOMPtr
+#include "nsHashKeys.h" // for nsUint64HashKey
+#include "nsISupportsImpl.h" // for NS_INLINE_DECL_REFCOUNTING
+#include "nsIWeakReferenceUtils.h"
+
+#include <unordered_map>
+
+namespace mozilla {
+
+namespace dom {
+class BrowserChild;
+} // namespace dom
+
+namespace widget {
+class CompositorWidget;
+} // namespace widget
+
+namespace layers {
+
+using mozilla::dom::BrowserChild;
+
+class IAPZCTreeManager;
+class APZCTreeManagerChild;
+class CanvasChild;
+class CompositorBridgeParent;
+class CompositorManagerChild;
+class CompositorOptions;
+class WebRenderLayerManager;
+class TextureClient;
+class TextureClientPool;
+struct FrameMetrics;
+
+class CompositorBridgeChild final : public PCompositorBridgeChild,
+ public TextureForwarder {
+ typedef nsTArray<AsyncParentMessageData> AsyncParentMessageArray;
+
+ friend class PCompositorBridgeChild;
+
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(CompositorBridgeChild, override);
+
+ explicit CompositorBridgeChild(CompositorManagerChild* aManager);
+
+ /**
+ * Initialize the singleton compositor bridge for a content process.
+ */
+ void InitForContent(uint32_t aNamespace);
+
+ void InitForWidget(uint64_t aProcessToken,
+ WebRenderLayerManager* aLayerManager, uint32_t aNamespace);
+
+ void Destroy();
+
+ static CompositorBridgeChild* Get();
+
+ // Returns whether the compositor is in the GPU process (false if in the UI
+ // process). This may only be called on the main thread.
+ static bool CompositorIsInGPUProcess();
+
+ mozilla::ipc::IPCResult RecvDidComposite(
+ const LayersId& aId, const nsTArray<TransactionId>& aTransactionIds,
+ const TimeStamp& aCompositeStart, const TimeStamp& aCompositeEnd);
+
+ mozilla::ipc::IPCResult RecvNotifyFrameStats(
+ nsTArray<FrameStats>&& aFrameStats);
+
+ mozilla::ipc::IPCResult RecvNotifyJankedAnimations(
+ const LayersId& aLayersId, nsTArray<uint64_t>&& aJankedAnimations);
+
+ PTextureChild* AllocPTextureChild(
+ const SurfaceDescriptor& aSharedData, ReadLockDescriptor& aReadLock,
+ const LayersBackend& aLayersBackend, const TextureFlags& aFlags,
+ const LayersId& aId, const uint64_t& aSerial,
+ const wr::MaybeExternalImageId& aExternalImageId);
+
+ bool DeallocPTextureChild(PTextureChild* actor);
+
+ mozilla::ipc::IPCResult RecvParentAsyncMessages(
+ nsTArray<AsyncParentMessageData>&& aMessages);
+ PTextureChild* CreateTexture(
+ const SurfaceDescriptor& aSharedData, ReadLockDescriptor&& aReadLock,
+ LayersBackend aLayersBackend, TextureFlags aFlags,
+ const dom::ContentParentId& aContentId, uint64_t aSerial,
+ wr::MaybeExternalImageId& aExternalImageId) override;
+
+ already_AddRefed<CanvasChild> GetCanvasChild() final;
+
+ void EndCanvasTransaction();
+
+ // Beware that these methods don't override their super-class equivalent
+ // (which are not virtual), they just overload them. All of these Send*
+ // methods just add a sanity check (that it is not too late send a message)
+ // and forward the call to the super-class's equivalent method. This means
+ // that it is correct to call directly the super-class methods, but you won't
+ // get the extra safety provided here.
+ bool SendWillClose();
+ bool SendPause();
+ bool SendResume();
+ bool SendResumeAsync();
+ bool SendAdoptChild(const LayersId& id);
+ bool SendFlushRendering(const wr::RenderReasons& aReasons);
+ bool SendStartFrameTimeRecording(const int32_t& bufferSize,
+ uint32_t* startIndex);
+ bool SendStopFrameTimeRecording(const uint32_t& startIndex,
+ nsTArray<float>* intervals);
+ bool IsSameProcess() const override;
+
+ bool IPCOpen() const override { return mCanSend; }
+
+ static void ShutDown();
+
+ void UpdateFwdTransactionId() { ++mFwdTransactionId; }
+ uint64_t GetFwdTransactionId() { return mFwdTransactionId; }
+
+ /**
+ * Hold TextureClient ref until end of usage on host side if
+ * TextureFlags::RECYCLE is set. Host side's usage is checked via
+ * CompositableRef.
+ */
+ void HoldUntilCompositableRefReleasedIfNecessary(TextureClient* aClient);
+
+ /**
+ * Notify id of Texture When host side end its use. Transaction id is used to
+ * make sure if there is no newer usage.
+ */
+ void NotifyNotUsed(uint64_t aTextureId, uint64_t aFwdTransactionId);
+
+ void CancelWaitForNotifyNotUsed(uint64_t aTextureId) override;
+
+ FixedSizeSmallShmemSectionAllocator* GetTileLockAllocator() override;
+
+ nsISerialEventTarget* GetThread() const override { return mThread; }
+
+ base::ProcessId GetParentPid() const override { return OtherPid(); }
+
+ bool AllocUnsafeShmem(size_t aSize, mozilla::ipc::Shmem* aShmem) override;
+ bool AllocShmem(size_t aSize, mozilla::ipc::Shmem* aShmem) override;
+ bool DeallocShmem(mozilla::ipc::Shmem& aShmem) override;
+
+ PCompositorWidgetChild* AllocPCompositorWidgetChild(
+ const CompositorWidgetInitData& aInitData);
+ bool DeallocPCompositorWidgetChild(PCompositorWidgetChild* aActor);
+
+ PAPZCTreeManagerChild* AllocPAPZCTreeManagerChild(const LayersId& aLayersId);
+ bool DeallocPAPZCTreeManagerChild(PAPZCTreeManagerChild* aActor);
+
+ PAPZChild* AllocPAPZChild(const LayersId& aLayersId);
+ bool DeallocPAPZChild(PAPZChild* aActor);
+
+ PWebRenderBridgeChild* AllocPWebRenderBridgeChild(
+ const wr::PipelineId& aPipelineId, const LayoutDeviceIntSize&,
+ const WindowKind&);
+ bool DeallocPWebRenderBridgeChild(PWebRenderBridgeChild* aActor);
+
+ wr::MaybeExternalImageId GetNextExternalImageId() override;
+
+ wr::PipelineId GetNextPipelineId();
+
+ private:
+ // Private destructor, to discourage deletion outside of Release():
+ virtual ~CompositorBridgeChild();
+
+ // Must only be called from the paint thread. If the main thread is delaying
+ // IPC messages, this forwards all such delayed IPC messages to the I/O thread
+ // and resumes IPC.
+ void ResumeIPCAfterAsyncPaint();
+
+ void PrepareFinalDestroy();
+ void AfterDestroy();
+
+ void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ mozilla::ipc::IPCResult RecvObserveLayersUpdate(
+ const LayersId& aLayersId, const LayersObserverEpoch& aEpoch,
+ const bool& aActive);
+
+ mozilla::ipc::IPCResult RecvCompositorOptionsChanged(
+ const LayersId& aLayersId, const CompositorOptions& aNewOptions);
+
+ uint64_t GetNextResourceId();
+
+ RefPtr<CompositorManagerChild> mCompositorManager;
+
+ RefPtr<WebRenderLayerManager> mLayerManager;
+
+ uint32_t mIdNamespace;
+ uint32_t mResourceId;
+
+ // When not multi-process, hold a reference to the CompositorBridgeParent to
+ // keep it alive. This reference should be null in multi-process.
+ RefPtr<CompositorBridgeParent> mCompositorBridgeParent;
+
+ DISALLOW_EVIL_CONSTRUCTORS(CompositorBridgeChild);
+
+ // True until the beginning of the two-step shutdown sequence of this actor.
+ bool mCanSend;
+
+ // False until the actor is destroyed.
+ bool mActorDestroyed;
+
+ /**
+ * Transaction id of ShadowLayerForwarder.
+ * It is incremented by UpdateFwdTransactionId() in each BeginTransaction()
+ * call.
+ */
+ uint64_t mFwdTransactionId;
+
+ /**
+ * Hold TextureClients refs until end of their usages on host side.
+ * It defer calling of TextureClient recycle callback.
+ */
+ std::unordered_map<uint64_t, RefPtr<TextureClient>>
+ mTexturesWaitingNotifyNotUsed;
+
+ nsCOMPtr<nsISerialEventTarget> mThread;
+
+ AutoTArray<RefPtr<TextureClientPool>, 2> mTexturePools;
+
+ uint64_t mProcessToken;
+
+ FixedSizeSmallShmemSectionAllocator* mSectionAllocator;
+
+ // TextureClients that must be kept alive during async painting. This
+ // is only accessed on the main thread.
+ nsTArray<RefPtr<TextureClient>> mTextureClientsForAsyncPaint;
+
+ RefPtr<CanvasChild> mCanvasChild;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_CompositorBrigedChild_h
diff --git a/gfx/layers/ipc/CompositorBridgeParent.cpp b/gfx/layers/ipc/CompositorBridgeParent.cpp
new file mode 100644
index 0000000000..0e9cf4fd21
--- /dev/null
+++ b/gfx/layers/ipc/CompositorBridgeParent.cpp
@@ -0,0 +1,1988 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/layers/CompositorBridgeParent.h"
+
+#include <stdio.h> // for fprintf, stdout
+#include <stdint.h> // for uint64_t
+#include <map> // for _Rb_tree_iterator, etc
+#include <utility> // for pair
+
+#include "apz/src/APZCTreeManager.h" // for APZCTreeManager
+#include "base/process.h" // for ProcessId
+#include "gfxContext.h" // for gfxContext
+#include "gfxPlatform.h" // for gfxPlatform
+#include "TreeTraversal.h" // for ForEachNode
+#ifdef MOZ_WIDGET_GTK
+# include "gfxPlatformGtk.h" // for gfxPlatform
+#endif
+#include "mozilla/AutoRestore.h" // for AutoRestore
+#include "mozilla/ClearOnShutdown.h" // for ClearOnShutdown
+#include "mozilla/DebugOnly.h" // for DebugOnly
+#include "mozilla/StaticPrefs_gfx.h"
+#include "mozilla/StaticPrefs_layers.h"
+#include "mozilla/StaticPrefs_layout.h"
+#include "mozilla/dom/BrowserParent.h"
+#include "mozilla/gfx/2D.h" // for DrawTarget
+#include "mozilla/gfx/Point.h" // for IntSize
+#include "mozilla/gfx/Rect.h" // for IntSize
+#include "mozilla/gfx/gfxVars.h" // for gfxVars
+#include "mozilla/gfx/GPUParent.h"
+#include "mozilla/gfx/GPUProcessManager.h"
+#include "mozilla/layers/APZCTreeManagerParent.h" // for APZCTreeManagerParent
+#include "mozilla/layers/APZSampler.h" // for APZSampler
+#include "mozilla/layers/APZThreadUtils.h" // for APZThreadUtils
+#include "mozilla/layers/APZUpdater.h" // for APZUpdater
+#include "mozilla/layers/CompositionRecorder.h" // for CompositionRecorder
+#include "mozilla/layers/Compositor.h" // for Compositor
+#include "mozilla/layers/CompositorAnimationStorage.h" // for CompositorAnimationStorage
+#include "mozilla/layers/CompositorManagerParent.h" // for CompositorManagerParent
+#include "mozilla/layers/CompositorOGL.h" // for CompositorOGL
+#include "mozilla/layers/CompositorThread.h"
+#include "mozilla/layers/CompositorTypes.h"
+#include "mozilla/layers/CompositorVsyncScheduler.h"
+#include "mozilla/layers/ContentCompositorBridgeParent.h"
+#include "mozilla/layers/FrameUniformityData.h"
+#include "mozilla/layers/GeckoContentController.h"
+#include "mozilla/layers/ImageBridgeParent.h"
+#include "mozilla/layers/LayerTreeOwnerTracker.h"
+#include "mozilla/layers/LayersTypes.h"
+#include "mozilla/layers/OMTASampler.h"
+#include "mozilla/layers/RemoteContentController.h"
+#include "mozilla/layers/UiCompositorControllerParent.h"
+#include "mozilla/layers/WebRenderBridgeParent.h"
+#include "mozilla/layers/AsyncImagePipelineManager.h"
+#include "mozilla/webrender/WebRenderAPI.h"
+#include "mozilla/webrender/RenderThread.h"
+#include "mozilla/media/MediaSystemResourceService.h" // for MediaSystemResourceService
+#include "mozilla/mozalloc.h" // for operator new, etc
+#include "mozilla/PodOperations.h"
+#include "mozilla/ProfilerLabels.h"
+#include "mozilla/ProfilerMarkers.h"
+#include "mozilla/Telemetry.h"
+#include "nsCOMPtr.h" // for already_AddRefed
+#include "nsDebug.h" // for NS_ASSERTION, etc
+#include "nsISupportsImpl.h" // for MOZ_COUNT_CTOR, etc
+#include "nsIWidget.h" // for nsIWidget
+#include "nsTArray.h" // for nsTArray
+#include "nsThreadUtils.h" // for NS_IsMainThread
+#ifdef XP_WIN
+# include "mozilla/layers/CompositorD3D11.h"
+# include "mozilla/widget/WinCompositorWidget.h"
+# include "mozilla/WindowsVersion.h"
+#endif
+#include "mozilla/ipc/ProtocolTypes.h"
+#include "mozilla/Unused.h"
+#include "mozilla/Hal.h"
+#include "mozilla/HalTypes.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/VsyncDispatcher.h"
+#if defined(XP_WIN) || defined(MOZ_WIDGET_GTK)
+# include "VsyncSource.h"
+#endif
+#include "mozilla/widget/CompositorWidget.h"
+#ifdef MOZ_WIDGET_SUPPORTS_OOP_COMPOSITING
+# include "mozilla/widget/CompositorWidgetParent.h"
+#endif
+#ifdef XP_WIN
+# include "mozilla/gfx/DeviceManagerDx.h"
+#endif
+
+namespace mozilla {
+
+namespace layers {
+
+using namespace mozilla::ipc;
+using namespace mozilla::gfx;
+
+using base::ProcessId;
+
+using mozilla::Telemetry::LABELS_CONTENT_FRAME_TIME_REASON;
+
+/// Equivalent to asserting CompositorThreadHolder::IsInCompositorThread with
+/// the addition that it doesn't assert if the compositor thread holder is
+/// already gone during late shutdown.
+static void AssertIsInCompositorThread() {
+ MOZ_RELEASE_ASSERT(!CompositorThread() ||
+ CompositorThreadHolder::IsInCompositorThread());
+}
+
+CompositorBridgeParentBase::CompositorBridgeParentBase(
+ CompositorManagerParent* aManager)
+ : mCanSend(true), mCompositorManager(aManager) {}
+
+CompositorBridgeParentBase::~CompositorBridgeParentBase() = default;
+
+ProcessId CompositorBridgeParentBase::GetChildProcessId() { return OtherPid(); }
+
+dom::ContentParentId CompositorBridgeParentBase::GetContentId() {
+ return mCompositorManager->GetContentId();
+}
+
+void CompositorBridgeParentBase::NotifyNotUsed(PTextureParent* aTexture,
+ uint64_t aTransactionId) {
+ RefPtr<TextureHost> texture = TextureHost::AsTextureHost(aTexture);
+ if (!texture) {
+ return;
+ }
+
+ if (!(texture->GetFlags() & TextureFlags::RECYCLE) &&
+ !(texture->GetFlags() & TextureFlags::WAIT_HOST_USAGE_END)) {
+ return;
+ }
+
+ uint64_t textureId = TextureHost::GetTextureSerial(aTexture);
+ mPendingAsyncMessage.push_back(OpNotifyNotUsed(textureId, aTransactionId));
+}
+
+void CompositorBridgeParentBase::SendAsyncMessage(
+ const nsTArray<AsyncParentMessageData>& aMessage) {
+ Unused << SendParentAsyncMessages(aMessage);
+}
+
+bool CompositorBridgeParentBase::AllocShmem(size_t aSize, ipc::Shmem* aShmem) {
+ return PCompositorBridgeParent::AllocShmem(aSize, aShmem);
+}
+
+bool CompositorBridgeParentBase::AllocUnsafeShmem(size_t aSize,
+ ipc::Shmem* aShmem) {
+ return PCompositorBridgeParent::AllocUnsafeShmem(aSize, aShmem);
+}
+
+bool CompositorBridgeParentBase::DeallocShmem(ipc::Shmem& aShmem) {
+ return PCompositorBridgeParent::DeallocShmem(aShmem);
+}
+
+CompositorBridgeParent::LayerTreeState::LayerTreeState()
+ : mApzcTreeManagerParent(nullptr),
+ mParent(nullptr),
+ mContentCompositorBridgeParent(nullptr) {}
+
+CompositorBridgeParent::LayerTreeState::~LayerTreeState() {
+ if (mController) {
+ mController->Destroy();
+ }
+}
+
+typedef std::map<LayersId, CompositorBridgeParent::LayerTreeState> LayerTreeMap;
+LayerTreeMap sIndirectLayerTrees;
+StaticAutoPtr<mozilla::Monitor> sIndirectLayerTreesLock;
+
+static void EnsureLayerTreeMapReady() {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (!sIndirectLayerTreesLock) {
+ sIndirectLayerTreesLock = new Monitor("IndirectLayerTree");
+ mozilla::ClearOnShutdown(&sIndirectLayerTreesLock);
+ }
+}
+
+template <typename Lambda>
+inline void CompositorBridgeParent::ForEachIndirectLayerTree(
+ const Lambda& aCallback) {
+ sIndirectLayerTreesLock->AssertCurrentThreadOwns();
+ for (auto it = sIndirectLayerTrees.begin(); it != sIndirectLayerTrees.end();
+ it++) {
+ LayerTreeState* state = &it->second;
+ if (state->mParent == this) {
+ aCallback(state, it->first);
+ }
+ }
+}
+
+/*static*/ template <typename Lambda>
+inline void CompositorBridgeParent::ForEachWebRenderBridgeParent(
+ const Lambda& aCallback) {
+ sIndirectLayerTreesLock->AssertCurrentThreadOwns();
+ for (auto& it : sIndirectLayerTrees) {
+ LayerTreeState* state = &it.second;
+ if (state->mWrBridge) {
+ aCallback(state->mWrBridge);
+ }
+ }
+}
+
+/**
+ * A global map referencing each compositor by ID.
+ *
+ * This map is used by the ImageBridge protocol to trigger
+ * compositions without having to keep references to the
+ * compositor
+ */
+typedef std::map<uint64_t, CompositorBridgeParent*> CompositorMap;
+static StaticAutoPtr<CompositorMap> sCompositorMap;
+
+void CompositorBridgeParent::Setup() {
+ EnsureLayerTreeMapReady();
+
+ MOZ_ASSERT(!sCompositorMap);
+ sCompositorMap = new CompositorMap;
+}
+
+void CompositorBridgeParent::FinishShutdown() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (sCompositorMap) {
+ MOZ_ASSERT(sCompositorMap->empty());
+ sCompositorMap = nullptr;
+ }
+
+ // TODO: this should be empty by now...
+ MonitorAutoLock lock(*sIndirectLayerTreesLock);
+ sIndirectLayerTrees.clear();
+}
+
+CompositorBridgeParent::CompositorBridgeParent(
+ CompositorManagerParent* aManager, CSSToLayoutDeviceScale aScale,
+ const TimeDuration& aVsyncRate, const CompositorOptions& aOptions,
+ bool aUseExternalSurfaceSize, const gfx::IntSize& aSurfaceSize,
+ uint64_t aInnerWindowId)
+ : CompositorBridgeParentBase(aManager),
+ mWidget(nullptr),
+ mScale(aScale),
+ mVsyncRate(aVsyncRate),
+ mPaused(false),
+ mHaveCompositionRecorder(false),
+ mIsForcedFirstPaint(false),
+ mUseExternalSurfaceSize(aUseExternalSurfaceSize),
+ mEGLSurfaceSize(aSurfaceSize),
+ mOptions(aOptions),
+ mCompositorBridgeID(0),
+ mRootLayerTreeID{0},
+ mInnerWindowId(aInnerWindowId),
+ mCompositorScheduler(nullptr),
+ mAnimationStorage(nullptr) {}
+
+void CompositorBridgeParent::InitSameProcess(widget::CompositorWidget* aWidget,
+ const LayersId& aLayerTreeId) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ MOZ_ASSERT(NS_IsMainThread());
+
+ mWidget = aWidget;
+ mRootLayerTreeID = aLayerTreeId;
+#if defined(XP_WIN)
+ // when run in headless mode, no WinCompositorWidget is created
+ if (widget::WinCompositorWidget* windows = mWidget->AsWindows()) {
+ windows->SetRootLayerTreeID(mRootLayerTreeID);
+ }
+#endif
+
+ Initialize();
+}
+
+mozilla::ipc::IPCResult CompositorBridgeParent::RecvInitialize(
+ const LayersId& aRootLayerTreeId) {
+ MOZ_ASSERT(XRE_IsGPUProcess());
+
+ mRootLayerTreeID = aRootLayerTreeId;
+#ifdef XP_WIN
+ // headless mode is probably always same-process; but just in case...
+ if (widget::WinCompositorWidget* windows = mWidget->AsWindows()) {
+ windows->SetRootLayerTreeID(mRootLayerTreeID);
+ }
+#endif
+
+ Initialize();
+ return IPC_OK();
+}
+
+void CompositorBridgeParent::Initialize() {
+ MOZ_ASSERT(CompositorThread(),
+ "The compositor thread must be Initialized before instanciating a "
+ "CompositorBridgeParent.");
+
+ if (mOptions.UseAPZ()) {
+ MOZ_ASSERT(!mApzcTreeManager);
+ MOZ_ASSERT(!mApzSampler);
+ MOZ_ASSERT(!mApzUpdater);
+ mApzcTreeManager = new APZCTreeManager(mRootLayerTreeID);
+ mApzSampler = new APZSampler(mApzcTreeManager, true);
+ mApzUpdater = new APZUpdater(mApzcTreeManager, true);
+ }
+
+ CompositorAnimationStorage* animationStorage = GetAnimationStorage();
+ mOMTASampler = new OMTASampler(animationStorage, mRootLayerTreeID);
+
+ mPaused = mOptions.InitiallyPaused();
+
+ mCompositorBridgeID = 0;
+ // FIXME: This holds on the the fact that right now the only thing that
+ // can destroy this instance is initialized on the compositor thread after
+ // this task has been processed.
+ MOZ_ASSERT(CompositorThread());
+ CompositorThread()->Dispatch(NewRunnableFunction(
+ "AddCompositorRunnable", &AddCompositor, this, &mCompositorBridgeID));
+
+ { // scope lock
+ MonitorAutoLock lock(*sIndirectLayerTreesLock);
+ sIndirectLayerTrees[mRootLayerTreeID].mParent = this;
+ }
+}
+
+LayersId CompositorBridgeParent::RootLayerTreeId() {
+ MOZ_ASSERT(mRootLayerTreeID.IsValid());
+ return mRootLayerTreeID;
+}
+
+CompositorBridgeParent::~CompositorBridgeParent() {
+ MOZ_DIAGNOSTIC_ASSERT(
+ !mCanSend,
+ "ActorDestroy or RecvWillClose should have been called first.");
+ MOZ_DIAGNOSTIC_ASSERT(mRefCnt == 0,
+ "ActorDealloc should have been called first.");
+ nsTArray<PTextureParent*> textures;
+ ManagedPTextureParent(textures);
+ // We expect all textures to be destroyed by now.
+ MOZ_DIAGNOSTIC_ASSERT(textures.Length() == 0);
+ for (unsigned int i = 0; i < textures.Length(); ++i) {
+ RefPtr<TextureHost> tex = TextureHost::AsTextureHost(textures[i]);
+ tex->DeallocateDeviceData();
+ }
+ // Check if WebRender/Compositor was shutdown.
+ if (mWrBridge) {
+ gfxCriticalNote << "CompositorBridgeParent destroyed without shutdown";
+ }
+}
+
+void CompositorBridgeParent::ForceIsFirstPaint() {
+ if (mWrBridge) {
+ mIsForcedFirstPaint = true;
+ }
+}
+
+void CompositorBridgeParent::StopAndClearResources() {
+ mPaused = true;
+
+ // We need to clear the APZ tree before we destroy the WebRender API below,
+ // because in the case of async scene building that will shut down the updater
+ // thread and we need to run the task before that happens.
+ MOZ_ASSERT((mApzSampler != nullptr) == (mApzcTreeManager != nullptr));
+ MOZ_ASSERT((mApzUpdater != nullptr) == (mApzcTreeManager != nullptr));
+ if (mApzUpdater) {
+ mApzSampler->Destroy();
+ mApzSampler = nullptr;
+ mApzUpdater->ClearTree(mRootLayerTreeID);
+ mApzUpdater = nullptr;
+ mApzcTreeManager = nullptr;
+ }
+
+ if (mWrBridge) {
+ // Ensure we are not holding the sIndirectLayerTreesLock when destroying
+ // the WebRenderBridgeParent instances because it may block on WR.
+ std::vector<RefPtr<WebRenderBridgeParent>> indirectBridgeParents;
+ { // scope lock
+ MonitorAutoLock lock(*sIndirectLayerTreesLock);
+ ForEachIndirectLayerTree([&](LayerTreeState* lts, LayersId) -> void {
+ if (lts->mWrBridge) {
+ indirectBridgeParents.emplace_back(lts->mWrBridge.forget());
+ }
+ lts->mParent = nullptr;
+ });
+ }
+ for (const RefPtr<WebRenderBridgeParent>& bridge : indirectBridgeParents) {
+ bridge->Destroy();
+ }
+ indirectBridgeParents.clear();
+
+ RefPtr<wr::WebRenderAPI> api = mWrBridge->GetWebRenderAPI();
+ // Ensure we are not holding the sIndirectLayerTreesLock here because we
+ // are going to block on WR threads in order to shut it down properly.
+ mWrBridge->Destroy();
+ mWrBridge = nullptr;
+
+ if (api) {
+ // Make extra sure we are done cleaning WebRender up before continuing.
+ // After that we wont have a way to talk to a lot of the webrender parts.
+ api->FlushSceneBuilder();
+ api = nullptr;
+ }
+
+ if (mAsyncImageManager) {
+ mAsyncImageManager->Destroy();
+ // WebRenderAPI should be already destructed
+ mAsyncImageManager = nullptr;
+ }
+ }
+
+ // This must be destroyed now since it accesses the widget.
+ if (mCompositorScheduler) {
+ mCompositorScheduler->Destroy();
+ mCompositorScheduler = nullptr;
+ }
+
+ if (mOMTASampler) {
+ mOMTASampler->Destroy();
+ mOMTASampler = nullptr;
+ }
+
+ // After this point, it is no longer legal to access the widget.
+ mWidget = nullptr;
+
+ // Clear mAnimationStorage here to ensure that the compositor thread
+ // still exists when we destroy it.
+ mAnimationStorage = nullptr;
+}
+
+mozilla::ipc::IPCResult CompositorBridgeParent::RecvWillClose() {
+ StopAndClearResources();
+ // Once we get the WillClose message, the client side is going to go away
+ // soon and we can't be guaranteed that sending messages will work.
+ mCanSend = false;
+ return IPC_OK();
+}
+
+void CompositorBridgeParent::DeferredDestroy() {
+ MOZ_ASSERT(!NS_IsMainThread());
+ mSelfRef = nullptr;
+}
+
+mozilla::ipc::IPCResult CompositorBridgeParent::RecvPause() {
+ PauseComposition();
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult CompositorBridgeParent::RecvRequestFxrOutput() {
+#ifdef XP_WIN
+ // Continue forwarding the request to the Widget + SwapChain
+ mWidget->AsWindows()->RequestFxrOutput();
+#endif
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult CompositorBridgeParent::RecvResume() {
+ ResumeComposition();
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult CompositorBridgeParent::RecvResumeAsync() {
+ ResumeComposition();
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult
+CompositorBridgeParent::RecvWaitOnTransactionProcessed() {
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult CompositorBridgeParent::RecvFlushRendering(
+ const wr::RenderReasons& aReasons) {
+ if (mWrBridge) {
+ mWrBridge->FlushRendering(aReasons);
+ return IPC_OK();
+ }
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult CompositorBridgeParent::RecvNotifyMemoryPressure() {
+ NotifyMemoryPressure();
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult CompositorBridgeParent::RecvFlushRenderingAsync(
+ const wr::RenderReasons& aReasons) {
+ if (mWrBridge) {
+ mWrBridge->FlushRendering(aReasons, false);
+ return IPC_OK();
+ }
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult CompositorBridgeParent::RecvForcePresent(
+ const wr::RenderReasons& aReasons) {
+ if (mWrBridge) {
+ mWrBridge->ScheduleForcedGenerateFrame(aReasons);
+ }
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult CompositorBridgeParent::RecvStartFrameTimeRecording(
+ const int32_t& aBufferSize, uint32_t* aOutStartIndex) {
+ if (mWrBridge) {
+ *aOutStartIndex = mWrBridge->StartFrameTimeRecording(aBufferSize);
+ } else {
+ *aOutStartIndex = 0;
+ }
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult CompositorBridgeParent::RecvStopFrameTimeRecording(
+ const uint32_t& aStartIndex, nsTArray<float>* intervals) {
+ if (mWrBridge) {
+ mWrBridge->StopFrameTimeRecording(aStartIndex, *intervals);
+ }
+ return IPC_OK();
+}
+
+void CompositorBridgeParent::ActorDestroy(ActorDestroyReason why) {
+ mCanSend = false;
+
+ StopAndClearResources();
+
+ RemoveCompositor(mCompositorBridgeID);
+
+ { // scope lock
+ MonitorAutoLock lock(*sIndirectLayerTreesLock);
+ sIndirectLayerTrees.erase(mRootLayerTreeID);
+ }
+
+ // There are chances that the ref count reaches zero on the main thread
+ // shortly after this function returns while some ipdl code still needs to run
+ // on this thread. We must keep the compositor parent alive untill the code
+ // handling message reception is finished on this thread.
+ mSelfRef = this;
+ NS_GetCurrentThread()->Dispatch(
+ NewRunnableMethod("layers::CompositorBridgeParent::DeferredDestroy", this,
+ &CompositorBridgeParent::DeferredDestroy));
+}
+
+void CompositorBridgeParent::ScheduleRenderOnCompositorThread(
+ wr::RenderReasons aReasons) {
+ MOZ_ASSERT(CompositorThread());
+ CompositorThread()->Dispatch(NewRunnableMethod<wr::RenderReasons>(
+ "layers::CompositorBridgeParent::ScheduleComposition", this,
+ &CompositorBridgeParent::ScheduleComposition, aReasons));
+}
+
+void CompositorBridgeParent::PauseComposition() {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread(),
+ "PauseComposition() can only be called on the compositor thread");
+
+ if (!mPaused) {
+ mPaused = true;
+
+ TimeStamp now = TimeStamp::Now();
+ if (mWrBridge) {
+ mWrBridge->Pause();
+ NotifyPipelineRendered(mWrBridge->PipelineId(),
+ mWrBridge->GetCurrentEpoch(), VsyncId(), now, now,
+ now);
+ }
+ }
+}
+
+bool CompositorBridgeParent::ResumeComposition() {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread(),
+ "ResumeComposition() can only be called on the compositor thread");
+
+ bool resumed = mWidget->OnResumeComposition();
+ resumed = resumed && mWrBridge->Resume();
+
+ if (!resumed) {
+#ifdef MOZ_WIDGET_ANDROID
+ // We can't get a surface. This could be because the activity changed
+ // between the time resume was scheduled and now.
+ __android_log_print(
+ ANDROID_LOG_INFO, "CompositorBridgeParent",
+ "Unable to renew compositor surface; remaining in paused state");
+#endif
+ return false;
+ }
+
+ mPaused = false;
+
+ mCompositorScheduler->ForceComposeToTarget(wr::RenderReasons::WIDGET, nullptr,
+ nullptr);
+ return true;
+}
+
+void CompositorBridgeParent::SetEGLSurfaceRect(int x, int y, int width,
+ int height) {
+ NS_ASSERTION(mUseExternalSurfaceSize,
+ "Compositor created without UseExternalSurfaceSize provided");
+ mEGLSurfaceSize.SizeTo(width, height);
+}
+
+bool CompositorBridgeParent::ResumeCompositionAndResize(int x, int y, int width,
+ int height) {
+ SetEGLSurfaceRect(x, y, width, height);
+ return ResumeComposition();
+}
+
+void CompositorBridgeParent::ScheduleComposition(wr::RenderReasons aReasons) {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+ if (mPaused) {
+ return;
+ }
+
+ if (mWrBridge) {
+ mWrBridge->ScheduleGenerateFrame(aReasons);
+ }
+}
+
+PAPZCTreeManagerParent* CompositorBridgeParent::AllocPAPZCTreeManagerParent(
+ const LayersId& aLayersId) {
+ // This should only ever get called in the GPU process.
+ MOZ_ASSERT(XRE_IsGPUProcess());
+ // We should only ever get this if APZ is enabled in this compositor.
+ MOZ_ASSERT(mOptions.UseAPZ());
+ // The mApzcTreeManager and mApzUpdater should have been created via
+ // RecvInitialize()
+ MOZ_ASSERT(mApzcTreeManager);
+ MOZ_ASSERT(mApzUpdater);
+ // The main process should pass in 0 because we assume mRootLayerTreeID
+ MOZ_ASSERT(!aLayersId.IsValid());
+
+ MonitorAutoLock lock(*sIndirectLayerTreesLock);
+ CompositorBridgeParent::LayerTreeState& state =
+ sIndirectLayerTrees[mRootLayerTreeID];
+ MOZ_ASSERT(state.mParent.get() == this);
+ MOZ_ASSERT(!state.mApzcTreeManagerParent);
+ state.mApzcTreeManagerParent = new APZCTreeManagerParent(
+ mRootLayerTreeID, mApzcTreeManager, mApzUpdater);
+
+ return state.mApzcTreeManagerParent;
+}
+
+bool CompositorBridgeParent::DeallocPAPZCTreeManagerParent(
+ PAPZCTreeManagerParent* aActor) {
+ delete aActor;
+ return true;
+}
+
+void CompositorBridgeParent::AllocateAPZCTreeManagerParent(
+ const MonitorAutoLock& aProofOfLayerTreeStateLock,
+ const LayersId& aLayersId, LayerTreeState& aState) {
+ MOZ_ASSERT(aState.mParent == this);
+ MOZ_ASSERT(mApzcTreeManager);
+ MOZ_ASSERT(mApzUpdater);
+ MOZ_ASSERT(!aState.mApzcTreeManagerParent);
+ aState.mApzcTreeManagerParent =
+ new APZCTreeManagerParent(aLayersId, mApzcTreeManager, mApzUpdater);
+}
+
+PAPZParent* CompositorBridgeParent::AllocPAPZParent(const LayersId& aLayersId) {
+ // This is the CompositorBridgeParent for a window, and so should only be
+ // creating a PAPZ instance if it lives in the GPU process. Instances that
+ // live in the UI process should going through SetControllerForLayerTree.
+ MOZ_RELEASE_ASSERT(XRE_IsGPUProcess());
+
+ // We should only ever get this if APZ is enabled on this compositor.
+ MOZ_RELEASE_ASSERT(mOptions.UseAPZ());
+
+ // The main process should pass in 0 because we assume mRootLayerTreeID
+ MOZ_RELEASE_ASSERT(!aLayersId.IsValid());
+
+ RemoteContentController* controller = new RemoteContentController();
+
+ // Increment the controller's refcount before we return it. This will keep the
+ // controller alive until it is released by IPDL in DeallocPAPZParent.
+ controller->AddRef();
+
+ MonitorAutoLock lock(*sIndirectLayerTreesLock);
+ CompositorBridgeParent::LayerTreeState& state =
+ sIndirectLayerTrees[mRootLayerTreeID];
+ MOZ_RELEASE_ASSERT(!state.mController);
+ state.mController = controller;
+
+ return controller;
+}
+
+bool CompositorBridgeParent::DeallocPAPZParent(PAPZParent* aActor) {
+ RemoteContentController* controller =
+ static_cast<RemoteContentController*>(aActor);
+ controller->Release();
+ return true;
+}
+
+RefPtr<APZSampler> CompositorBridgeParent::GetAPZSampler() const {
+ return mApzSampler;
+}
+
+RefPtr<APZUpdater> CompositorBridgeParent::GetAPZUpdater() const {
+ return mApzUpdater;
+}
+
+RefPtr<OMTASampler> CompositorBridgeParent::GetOMTASampler() const {
+ return mOMTASampler;
+}
+
+CompositorBridgeParent*
+CompositorBridgeParent::GetCompositorBridgeParentFromLayersId(
+ const LayersId& aLayersId) {
+ MonitorAutoLock lock(*sIndirectLayerTreesLock);
+ return sIndirectLayerTrees[aLayersId].mParent;
+}
+
+/*static*/
+RefPtr<CompositorBridgeParent>
+CompositorBridgeParent::GetCompositorBridgeParentFromWindowId(
+ const wr::WindowId& aWindowId) {
+ MonitorAutoLock lock(*sIndirectLayerTreesLock);
+ for (auto it = sIndirectLayerTrees.begin(); it != sIndirectLayerTrees.end();
+ it++) {
+ LayerTreeState* state = &it->second;
+ if (!state->mWrBridge) {
+ continue;
+ }
+ // state->mWrBridge might be a root WebRenderBridgeParent or one of a
+ // content process, but in either case the state->mParent will be the same.
+ // So we don't need to distinguish between the two.
+ if (RefPtr<wr::WebRenderAPI> api = state->mWrBridge->GetWebRenderAPI()) {
+ if (api->GetId() == aWindowId) {
+ return state->mParent;
+ }
+ }
+ }
+ return nullptr;
+}
+
+bool CompositorBridgeParent::SetTestSampleTime(const LayersId& aId,
+ const TimeStamp& aTime) {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+
+ if (aTime.IsNull()) {
+ return false;
+ }
+
+ mTestTime = Some(aTime);
+ if (mApzcTreeManager) {
+ mApzcTreeManager->SetTestSampleTime(mTestTime);
+ }
+
+ if (mWrBridge) {
+ mWrBridge->FlushRendering(wr::RenderReasons::TESTING);
+ return true;
+ }
+
+ return true;
+}
+
+void CompositorBridgeParent::LeaveTestMode(const LayersId& aId) {
+ mTestTime = Nothing();
+ if (mApzcTreeManager) {
+ mApzcTreeManager->SetTestSampleTime(mTestTime);
+ }
+}
+
+CompositorAnimationStorage* CompositorBridgeParent::GetAnimationStorage() {
+ if (!mAnimationStorage) {
+ mAnimationStorage = new CompositorAnimationStorage(this);
+ }
+ return mAnimationStorage;
+}
+
+void CompositorBridgeParent::NotifyJankedAnimations(
+ const JankedAnimations& aJankedAnimations) {
+ MOZ_ASSERT(!aJankedAnimations.empty());
+
+ if (StaticPrefs::layout_animation_prerender_partial_jank()) {
+ return;
+ }
+
+ for (const auto& entry : aJankedAnimations) {
+ const LayersId& layersId = entry.first;
+ const nsTArray<uint64_t>& animations = entry.second;
+ if (layersId == mRootLayerTreeID) {
+ if (mWrBridge) {
+ Unused << SendNotifyJankedAnimations(LayersId{0}, animations);
+ }
+ // It unlikely happens multiple processes have janked animations at same
+ // time, so it should be fine with enumerating sIndirectLayerTrees every
+ // time.
+ } else if (const LayerTreeState* state = GetIndirectShadowTree(layersId)) {
+ if (ContentCompositorBridgeParent* cpcp =
+ state->mContentCompositorBridgeParent) {
+ Unused << cpcp->SendNotifyJankedAnimations(layersId, animations);
+ }
+ }
+ }
+}
+
+void CompositorBridgeParent::SetTestAsyncScrollOffset(
+ const LayersId& aLayersId, const ScrollableLayerGuid::ViewID& aScrollId,
+ const CSSPoint& aPoint) {
+ if (mApzUpdater) {
+ MOZ_ASSERT(aLayersId.IsValid());
+ mApzUpdater->SetTestAsyncScrollOffset(aLayersId, aScrollId, aPoint);
+ }
+}
+
+void CompositorBridgeParent::SetTestAsyncZoom(
+ const LayersId& aLayersId, const ScrollableLayerGuid::ViewID& aScrollId,
+ const LayerToParentLayerScale& aZoom) {
+ if (mApzUpdater) {
+ MOZ_ASSERT(aLayersId.IsValid());
+ mApzUpdater->SetTestAsyncZoom(aLayersId, aScrollId, aZoom);
+ }
+}
+
+void CompositorBridgeParent::FlushApzRepaints(const LayersId& aLayersId) {
+ MOZ_ASSERT(mApzUpdater);
+ MOZ_ASSERT(aLayersId.IsValid());
+ mApzUpdater->RunOnControllerThread(
+ aLayersId, NS_NewRunnableFunction(
+ "layers::CompositorBridgeParent::FlushApzRepaints",
+ [=]() { APZCTreeManager::FlushApzRepaints(aLayersId); }));
+}
+
+void CompositorBridgeParent::GetAPZTestData(const LayersId& aLayersId,
+ APZTestData* aOutData) {
+ if (mApzUpdater) {
+ MOZ_ASSERT(aLayersId.IsValid());
+ mApzUpdater->GetAPZTestData(aLayersId, aOutData);
+ }
+}
+
+void CompositorBridgeParent::GetFrameUniformity(const LayersId& aLayersId,
+ FrameUniformityData* aOutData) {
+}
+
+void CompositorBridgeParent::SetConfirmedTargetAPZC(
+ const LayersId& aLayersId, const uint64_t& aInputBlockId,
+ nsTArray<ScrollableLayerGuid>&& aTargets) {
+ if (!mApzcTreeManager || !mApzUpdater) {
+ return;
+ }
+ // Need to specifically bind this since it's overloaded.
+ void (APZCTreeManager::*setTargetApzcFunc)(
+ uint64_t, const nsTArray<ScrollableLayerGuid>&) =
+ &APZCTreeManager::SetTargetAPZC;
+ RefPtr<Runnable> task =
+ NewRunnableMethod<uint64_t,
+ StoreCopyPassByRRef<nsTArray<ScrollableLayerGuid>>>(
+ "layers::CompositorBridgeParent::SetConfirmedTargetAPZC",
+ mApzcTreeManager.get(), setTargetApzcFunc, aInputBlockId,
+ std::move(aTargets));
+ mApzUpdater->RunOnUpdaterThread(aLayersId, task.forget());
+}
+
+void CompositorBridgeParent::SetFixedLayerMargins(ScreenIntCoord aTop,
+ ScreenIntCoord aBottom) {
+ if (mApzcTreeManager) {
+ mApzcTreeManager->SetFixedLayerMargins(aTop, aBottom);
+ }
+
+ ScheduleComposition(wr::RenderReasons::RESIZE);
+}
+
+void CompositorBridgeParent::AddCompositor(CompositorBridgeParent* compositor,
+ uint64_t* outID) {
+ AssertIsInCompositorThread();
+
+ static uint64_t sNextID = 1;
+
+ ++sNextID;
+ (*sCompositorMap)[sNextID] = compositor;
+ *outID = sNextID;
+}
+
+CompositorBridgeParent* CompositorBridgeParent::RemoveCompositor(uint64_t id) {
+ AssertIsInCompositorThread();
+
+ CompositorMap::iterator it = sCompositorMap->find(id);
+ if (it == sCompositorMap->end()) {
+ return nullptr;
+ }
+ CompositorBridgeParent* retval = it->second;
+ sCompositorMap->erase(it);
+ return retval;
+}
+
+void CompositorBridgeParent::NotifyVsync(const VsyncEvent& aVsync,
+ const LayersId& aLayersId) {
+ MOZ_ASSERT(XRE_GetProcessType() == GeckoProcessType_GPU);
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+
+ MonitorAutoLock lock(*sIndirectLayerTreesLock);
+ auto it = sIndirectLayerTrees.find(aLayersId);
+ if (it == sIndirectLayerTrees.end()) return;
+
+ CompositorBridgeParent* cbp = it->second.mParent;
+ if (!cbp || !cbp->mWidget) return;
+
+ RefPtr<VsyncObserver> obs = cbp->mWidget->GetVsyncObserver();
+ if (!obs) return;
+
+ obs->NotifyVsync(aVsync);
+}
+
+/* static */
+void CompositorBridgeParent::ScheduleForcedComposition(
+ const LayersId& aLayersId, wr::RenderReasons aReasons) {
+ MOZ_ASSERT(XRE_GetProcessType() == GeckoProcessType_GPU);
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+
+ MonitorAutoLock lock(*sIndirectLayerTreesLock);
+ auto it = sIndirectLayerTrees.find(aLayersId);
+ if (it == sIndirectLayerTrees.end()) {
+ return;
+ }
+
+ CompositorBridgeParent* cbp = it->second.mParent;
+ if (!cbp || !cbp->mWidget) {
+ return;
+ }
+
+ if (cbp->mWrBridge) {
+ cbp->mWrBridge->ScheduleForcedGenerateFrame(aReasons);
+ }
+}
+
+mozilla::ipc::IPCResult CompositorBridgeParent::RecvNotifyChildCreated(
+ const LayersId& child, CompositorOptions* aOptions) {
+ MonitorAutoLock lock(*sIndirectLayerTreesLock);
+ NotifyChildCreated(child);
+ *aOptions = mOptions;
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult CompositorBridgeParent::RecvNotifyChildRecreated(
+ const LayersId& aChild, CompositorOptions* aOptions) {
+ MonitorAutoLock lock(*sIndirectLayerTreesLock);
+
+ if (sIndirectLayerTrees.find(aChild) != sIndirectLayerTrees.end()) {
+ NS_WARNING("Invalid to register the same layer tree twice");
+ return IPC_FAIL_NO_REASON(this);
+ }
+
+ NotifyChildCreated(aChild);
+ *aOptions = mOptions;
+ return IPC_OK();
+}
+
+void CompositorBridgeParent::NotifyChildCreated(LayersId aChild) {
+ sIndirectLayerTreesLock->AssertCurrentThreadOwns();
+ sIndirectLayerTrees[aChild].mParent = this;
+}
+
+mozilla::ipc::IPCResult CompositorBridgeParent::RecvMapAndNotifyChildCreated(
+ const LayersId& aChild, const base::ProcessId& aOwnerPid,
+ CompositorOptions* aOptions) {
+ // We only use this message when the remote compositor is in the GPU process.
+ // It is harmless to call it, though.
+ MOZ_ASSERT(XRE_IsGPUProcess());
+
+ LayerTreeOwnerTracker::Get()->Map(aChild, aOwnerPid);
+
+ MonitorAutoLock lock(*sIndirectLayerTreesLock);
+ NotifyChildCreated(aChild);
+ *aOptions = mOptions;
+ return IPC_OK();
+}
+
+enum class CompositorOptionsChangeKind {
+ eSupported,
+ eBestEffort,
+ eUnsupported
+};
+
+static CompositorOptionsChangeKind ClassifyCompositorOptionsChange(
+ const CompositorOptions& aOld, const CompositorOptions& aNew) {
+ if (aOld == aNew) {
+ return CompositorOptionsChangeKind::eSupported;
+ }
+ if (aOld.EqualsIgnoringApzEnablement(aNew)) {
+ return CompositorOptionsChangeKind::eBestEffort;
+ }
+ return CompositorOptionsChangeKind::eUnsupported;
+}
+
+mozilla::ipc::IPCResult CompositorBridgeParent::RecvAdoptChild(
+ const LayersId& child) {
+ RefPtr<APZUpdater> oldApzUpdater;
+ APZCTreeManagerParent* parent;
+ bool apzEnablementChanged = false;
+ RefPtr<WebRenderBridgeParent> childWrBridge;
+
+ // Before adopting the child, save the old compositor's root content
+ // controller. We may need this to clear old layer transforms associated
+ // with the child.
+ // This is outside the lock because GetGeckoContentControllerForRoot()
+ // does its own locking.
+ RefPtr<GeckoContentController> oldRootController =
+ GetGeckoContentControllerForRoot(child);
+
+ { // scope lock
+ MonitorAutoLock lock(*sIndirectLayerTreesLock);
+ // If child is already belong to this CompositorBridgeParent,
+ // no need to handle adopting child.
+ if (sIndirectLayerTrees[child].mParent == this) {
+ return IPC_OK();
+ }
+
+ if (sIndirectLayerTrees[child].mParent) {
+ switch (ClassifyCompositorOptionsChange(
+ sIndirectLayerTrees[child].mParent->mOptions, mOptions)) {
+ case CompositorOptionsChangeKind::eUnsupported: {
+ MOZ_ASSERT(false,
+ "Moving tab between windows whose compositor options"
+ "differ in unsupported ways. Things may break in "
+ "unexpected ways");
+ break;
+ }
+ case CompositorOptionsChangeKind::eBestEffort: {
+ NS_WARNING(
+ "Moving tab between windows with different APZ enablement. "
+ "This is supported on a best-effort basis, but some things may "
+ "break.");
+ apzEnablementChanged = true;
+ break;
+ }
+ case CompositorOptionsChangeKind::eSupported: {
+ // The common case, no action required.
+ break;
+ }
+ }
+ oldApzUpdater = sIndirectLayerTrees[child].mParent->mApzUpdater;
+ }
+ if (mWrBridge) {
+ childWrBridge = sIndirectLayerTrees[child].mWrBridge;
+ }
+ parent = sIndirectLayerTrees[child].mApzcTreeManagerParent;
+ }
+
+ if (childWrBridge) {
+ MOZ_ASSERT(mWrBridge);
+ RefPtr<wr::WebRenderAPI> api = mWrBridge->GetWebRenderAPI();
+ api = api->Clone();
+ wr::Epoch newEpoch = childWrBridge->UpdateWebRender(
+ mWrBridge->CompositorScheduler(), std::move(api),
+ mWrBridge->AsyncImageManager(),
+ mWrBridge->GetTextureFactoryIdentifier());
+ // Pretend we composited, since parent CompositorBridgeParent was replaced.
+ TimeStamp now = TimeStamp::Now();
+ NotifyPipelineRendered(childWrBridge->PipelineId(), newEpoch, VsyncId(),
+ now, now, now);
+ }
+
+ {
+ MonitorAutoLock lock(*sIndirectLayerTreesLock);
+ // Update sIndirectLayerTrees[child].mParent after
+ // WebRenderBridgeParent::UpdateWebRender().
+ NotifyChildCreated(child);
+ }
+
+ if (oldApzUpdater) {
+ // If we are moving a child from an APZ-enabled window to an APZ-disabled
+ // window (which can happen if e.g. a WebExtension moves a tab into a
+ // popup window), try to handle it gracefully by clearing the old layer
+ // transforms associated with the child. (Since the new compositor is
+ // APZ-disabled, there will be nothing to update the transforms going
+ // forward.)
+ if (!mApzUpdater && oldRootController) {
+ // Tell the old APZCTreeManager not to send any more layer transforms
+ // for this layers ids.
+ oldApzUpdater->MarkAsDetached(child);
+
+ // Clear the current transforms.
+ nsTArray<MatrixMessage> clear;
+ clear.AppendElement(MatrixMessage(Nothing(), ScreenRect(), child));
+ oldRootController->NotifyLayerTransforms(std::move(clear));
+ }
+ }
+ if (mApzUpdater) {
+ if (parent) {
+ MOZ_ASSERT(mApzcTreeManager);
+ parent->ChildAdopted(mApzcTreeManager, mApzUpdater);
+ }
+ mApzUpdater->NotifyLayerTreeAdopted(child, oldApzUpdater);
+ }
+ if (apzEnablementChanged) {
+ Unused << SendCompositorOptionsChanged(child, mOptions);
+ }
+ return IPC_OK();
+}
+
+PWebRenderBridgeParent* CompositorBridgeParent::AllocPWebRenderBridgeParent(
+ const wr::PipelineId& aPipelineId, const LayoutDeviceIntSize& aSize,
+ const WindowKind& aWindowKind) {
+ MOZ_ASSERT(wr::AsLayersId(aPipelineId) == mRootLayerTreeID);
+ MOZ_ASSERT(!mWrBridge);
+ MOZ_ASSERT(!mCompositorScheduler);
+ MOZ_ASSERT(mWidget);
+
+#ifdef XP_WIN
+ if (mWidget && mWidget->AsWindows()) {
+ const auto options = mWidget->GetCompositorOptions();
+ if (!options.UseSoftwareWebRender() &&
+ (DeviceManagerDx::Get()->CanUseDComp() ||
+ gfxVars::UseWebRenderFlipSequentialWin())) {
+ mWidget->AsWindows()->EnsureCompositorWindow();
+ } else if (options.UseSoftwareWebRender() &&
+ mWidget->AsWindows()->GetCompositorHwnd()) {
+ mWidget->AsWindows()->DestroyCompositorWindow();
+ }
+ }
+#endif
+
+ RefPtr<widget::CompositorWidget> widget = mWidget;
+ wr::WrWindowId windowId = wr::NewWindowId();
+ if (mApzUpdater) {
+ // If APZ is enabled, we need to register the APZ updater with the window id
+ // before the updater thread is created in WebRenderAPI::Create, so
+ // that the callback from the updater thread can find the right APZUpdater.
+ mApzUpdater->SetWebRenderWindowId(windowId);
+ }
+ if (mApzSampler) {
+ // Same as for mApzUpdater, but for the sampler thread.
+ mApzSampler->SetWebRenderWindowId(windowId);
+ }
+ if (mOMTASampler) {
+ // Same, but for the OMTA sampler.
+ mOMTASampler->SetWebRenderWindowId(windowId);
+ }
+
+ nsCString error("FEATURE_FAILURE_WEBRENDER_INITIALIZE_UNSPECIFIED");
+ RefPtr<wr::WebRenderAPI> api = wr::WebRenderAPI::Create(
+ this, std::move(widget), windowId, aSize, aWindowKind, error);
+ if (!api) {
+ mWrBridge =
+ WebRenderBridgeParent::CreateDestroyed(aPipelineId, std::move(error));
+ mWrBridge.get()->AddRef(); // IPDL reference
+ return mWrBridge;
+ }
+
+#ifdef MOZ_WIDGET_ANDROID
+ // On Android, WebRenderAPI::Resume() call is triggered from Java side. But
+ // Java side does not know about fallback to RenderCompositorOGLSWGL. In this
+ // fallback case, RenderCompositor::Resume() needs to be called from gfx code.
+ if (!mPaused && mWidget->GetCompositorOptions().UseSoftwareWebRender() &&
+ mWidget->GetCompositorOptions().AllowSoftwareWebRenderOGL()) {
+ api->Resume();
+ }
+#endif
+
+ wr::TransactionBuilder txn(api);
+ txn.SetRootPipeline(aPipelineId);
+ api->SendTransaction(txn);
+
+ bool useCompositorWnd = false;
+#ifdef XP_WIN
+ // Headless mode uses HeadlessWidget.
+ if (mWidget->AsWindows()) {
+ useCompositorWnd = !!mWidget->AsWindows()->GetCompositorHwnd();
+ }
+#endif
+ mAsyncImageManager =
+ new AsyncImagePipelineManager(api->Clone(), useCompositorWnd);
+ RefPtr<AsyncImagePipelineManager> asyncMgr = mAsyncImageManager;
+ mWrBridge = new WebRenderBridgeParent(this, aPipelineId, mWidget, nullptr,
+ std::move(api), std::move(asyncMgr),
+ mVsyncRate);
+ mWrBridge.get()->AddRef(); // IPDL reference
+
+ mCompositorScheduler = mWrBridge->CompositorScheduler();
+ MOZ_ASSERT(mCompositorScheduler);
+ { // scope lock
+ MonitorAutoLock lock(*sIndirectLayerTreesLock);
+ MOZ_ASSERT(sIndirectLayerTrees[mRootLayerTreeID].mWrBridge == nullptr);
+ sIndirectLayerTrees[mRootLayerTreeID].mWrBridge = mWrBridge;
+ }
+ return mWrBridge;
+}
+
+bool CompositorBridgeParent::DeallocPWebRenderBridgeParent(
+ PWebRenderBridgeParent* aActor) {
+ WebRenderBridgeParent* parent = static_cast<WebRenderBridgeParent*>(aActor);
+ {
+ MonitorAutoLock lock(*sIndirectLayerTreesLock);
+ auto it = sIndirectLayerTrees.find(wr::AsLayersId(parent->PipelineId()));
+ if (it != sIndirectLayerTrees.end()) {
+ it->second.mWrBridge = nullptr;
+ }
+ }
+ parent->Release(); // IPDL reference
+ return true;
+}
+
+void CompositorBridgeParent::NotifyMemoryPressure() {
+ if (mWrBridge) {
+ RefPtr<wr::WebRenderAPI> api = mWrBridge->GetWebRenderAPI();
+ if (api) {
+ api->NotifyMemoryPressure();
+ }
+ }
+}
+
+void CompositorBridgeParent::AccumulateMemoryReport(wr::MemoryReport* aReport) {
+ if (mWrBridge) {
+ RefPtr<wr::WebRenderAPI> api = mWrBridge->GetWebRenderAPI();
+ if (api) {
+ api->AccumulateMemoryReport(aReport);
+ }
+ }
+}
+
+/*static*/
+void CompositorBridgeParent::InitializeStatics() {
+ gfxVars::SetForceSubpixelAAWherePossibleListener(&UpdateQualitySettings);
+ gfxVars::SetWebRenderDebugFlagsListener(&UpdateDebugFlags);
+ gfxVars::SetWebRenderBoolParametersListener(&UpdateWebRenderBoolParameters);
+ gfxVars::SetWebRenderBatchingLookbackListener(&UpdateWebRenderParameters);
+ gfxVars::SetWebRenderBlobTileSizeListener(&UpdateWebRenderParameters);
+ gfxVars::SetWebRenderBatchedUploadThresholdListener(
+ &UpdateWebRenderParameters);
+
+ gfxVars::SetWebRenderProfilerUIListener(&UpdateWebRenderProfilerUI);
+}
+
+/*static*/
+void CompositorBridgeParent::UpdateQualitySettings() {
+ if (!CompositorThreadHolder::IsInCompositorThread()) {
+ if (CompositorThread()) {
+ CompositorThread()->Dispatch(
+ NewRunnableFunction("CompositorBridgeParent::UpdateQualitySettings",
+ &CompositorBridgeParent::UpdateQualitySettings));
+ }
+
+ // If there is no compositor thread, e.g. due to shutdown, then we can
+ // safefully just ignore this request.
+ return;
+ }
+
+ MonitorAutoLock lock(*sIndirectLayerTreesLock);
+ ForEachWebRenderBridgeParent([&](WebRenderBridgeParent* wrBridge) -> void {
+ if (!wrBridge->IsRootWebRenderBridgeParent()) {
+ return;
+ }
+ wrBridge->UpdateQualitySettings();
+ });
+}
+
+/*static*/
+void CompositorBridgeParent::UpdateDebugFlags() {
+ if (!CompositorThreadHolder::IsInCompositorThread()) {
+ if (CompositorThread()) {
+ CompositorThread()->Dispatch(
+ NewRunnableFunction("CompositorBridgeParent::UpdateDebugFlags",
+ &CompositorBridgeParent::UpdateDebugFlags));
+ }
+
+ // If there is no compositor thread, e.g. due to shutdown, then we can
+ // safefully just ignore this request.
+ return;
+ }
+
+ MonitorAutoLock lock(*sIndirectLayerTreesLock);
+ ForEachWebRenderBridgeParent([&](WebRenderBridgeParent* wrBridge) -> void {
+ if (!wrBridge->IsRootWebRenderBridgeParent()) {
+ return;
+ }
+ wrBridge->UpdateDebugFlags();
+ });
+}
+
+/*static*/
+void CompositorBridgeParent::UpdateWebRenderBoolParameters() {
+ if (!CompositorThreadHolder::IsInCompositorThread()) {
+ if (CompositorThread()) {
+ CompositorThread()->Dispatch(NewRunnableFunction(
+ "CompositorBridgeParent::UpdateWebRenderBoolParameters",
+ &CompositorBridgeParent::UpdateWebRenderBoolParameters));
+ }
+
+ return;
+ }
+
+ MonitorAutoLock lock(*sIndirectLayerTreesLock);
+ ForEachWebRenderBridgeParent([&](WebRenderBridgeParent* wrBridge) -> void {
+ if (!wrBridge->IsRootWebRenderBridgeParent()) {
+ return;
+ }
+ wrBridge->UpdateBoolParameters();
+ });
+}
+
+/*static*/
+void CompositorBridgeParent::UpdateWebRenderParameters() {
+ if (!CompositorThreadHolder::IsInCompositorThread()) {
+ if (CompositorThread()) {
+ CompositorThread()->Dispatch(NewRunnableFunction(
+ "CompositorBridgeParent::UpdateWebRenderParameters",
+ &CompositorBridgeParent::UpdateWebRenderParameters));
+ }
+
+ return;
+ }
+
+ MonitorAutoLock lock(*sIndirectLayerTreesLock);
+ ForEachWebRenderBridgeParent([&](WebRenderBridgeParent* wrBridge) -> void {
+ if (!wrBridge->IsRootWebRenderBridgeParent()) {
+ return;
+ }
+ wrBridge->UpdateParameters();
+ });
+}
+
+/*static*/
+void CompositorBridgeParent::UpdateWebRenderProfilerUI() {
+ if (!sIndirectLayerTreesLock) {
+ return;
+ }
+ MonitorAutoLock lock(*sIndirectLayerTreesLock);
+ ForEachWebRenderBridgeParent([&](WebRenderBridgeParent* wrBridge) -> void {
+ if (!wrBridge->IsRootWebRenderBridgeParent()) {
+ return;
+ }
+ wrBridge->UpdateProfilerUI();
+ });
+}
+
+RefPtr<WebRenderBridgeParent> CompositorBridgeParent::GetWebRenderBridgeParent()
+ const {
+ return mWrBridge;
+}
+
+Maybe<TimeStamp> CompositorBridgeParent::GetTestingTimeStamp() const {
+ return mTestTime;
+}
+
+void EraseLayerState(LayersId aId) {
+ RefPtr<APZUpdater> apz;
+ RefPtr<WebRenderBridgeParent> wrBridge;
+
+ { // scope lock
+ MonitorAutoLock lock(*sIndirectLayerTreesLock);
+ auto iter = sIndirectLayerTrees.find(aId);
+ if (iter != sIndirectLayerTrees.end()) {
+ CompositorBridgeParent* parent = iter->second.mParent;
+ if (parent) {
+ apz = parent->GetAPZUpdater();
+ }
+ wrBridge = iter->second.mWrBridge;
+ sIndirectLayerTrees.erase(iter);
+ }
+ }
+
+ if (apz) {
+ apz->NotifyLayerTreeRemoved(aId);
+ }
+
+ if (wrBridge) {
+ wrBridge->Destroy();
+ }
+}
+
+/*static*/
+void CompositorBridgeParent::DeallocateLayerTreeId(LayersId aId) {
+ MOZ_ASSERT(NS_IsMainThread());
+ // Here main thread notifies compositor to remove an element from
+ // sIndirectLayerTrees. This removed element might be queried soon.
+ // Checking the elements of sIndirectLayerTrees exist or not before using.
+ if (!CompositorThread()) {
+ gfxCriticalError() << "Attempting to post to an invalid Compositor Thread";
+ return;
+ }
+ CompositorThread()->Dispatch(
+ NewRunnableFunction("EraseLayerStateRunnable", &EraseLayerState, aId));
+}
+
+static void UpdateControllerForLayersId(LayersId aLayersId,
+ GeckoContentController* aController) {
+ // Adopt ref given to us by SetControllerForLayerTree()
+ MonitorAutoLock lock(*sIndirectLayerTreesLock);
+ sIndirectLayerTrees[aLayersId].mController =
+ already_AddRefed<GeckoContentController>(aController);
+}
+
+ScopedLayerTreeRegistration::ScopedLayerTreeRegistration(
+ LayersId aLayersId, GeckoContentController* aController)
+ : mLayersId(aLayersId) {
+ EnsureLayerTreeMapReady();
+ MonitorAutoLock lock(*sIndirectLayerTreesLock);
+ sIndirectLayerTrees[aLayersId].mController = aController;
+}
+
+ScopedLayerTreeRegistration::~ScopedLayerTreeRegistration() {
+ MonitorAutoLock lock(*sIndirectLayerTreesLock);
+ sIndirectLayerTrees.erase(mLayersId);
+}
+
+/*static*/
+void CompositorBridgeParent::SetControllerForLayerTree(
+ LayersId aLayersId, GeckoContentController* aController) {
+ // This ref is adopted by UpdateControllerForLayersId().
+ aController->AddRef();
+ CompositorThread()->Dispatch(NewRunnableFunction(
+ "UpdateControllerForLayersIdRunnable", &UpdateControllerForLayersId,
+ aLayersId, aController));
+}
+
+/*static*/
+already_AddRefed<IAPZCTreeManager> CompositorBridgeParent::GetAPZCTreeManager(
+ LayersId aLayersId) {
+ EnsureLayerTreeMapReady();
+ MonitorAutoLock lock(*sIndirectLayerTreesLock);
+ LayerTreeMap::iterator cit = sIndirectLayerTrees.find(aLayersId);
+ if (sIndirectLayerTrees.end() == cit) {
+ return nullptr;
+ }
+ LayerTreeState* lts = &cit->second;
+
+ RefPtr<IAPZCTreeManager> apzctm =
+ lts->mParent ? lts->mParent->mApzcTreeManager.get() : nullptr;
+ return apzctm.forget();
+}
+
+static void InsertVsyncProfilerMarker(TimeStamp aVsyncTimestamp) {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+ if (profiler_thread_is_being_profiled_for_markers()) {
+ // Tracks when a vsync occurs according to the HardwareComposer.
+ struct VsyncMarker {
+ static constexpr mozilla::Span<const char> MarkerTypeName() {
+ return mozilla::MakeStringSpan("VsyncTimestamp");
+ }
+ static void StreamJSONMarkerData(
+ baseprofiler::SpliceableJSONWriter& aWriter) {}
+ static MarkerSchema MarkerTypeDisplay() {
+ using MS = MarkerSchema;
+ MS schema{MS::Location::MarkerChart, MS::Location::MarkerTable};
+ // Nothing outside the defaults.
+ return schema;
+ }
+ };
+ profiler_add_marker("VsyncTimestamp", geckoprofiler::category::GRAPHICS,
+ MarkerTiming::InstantAt(aVsyncTimestamp),
+ VsyncMarker{});
+ }
+}
+
+/*static */
+void CompositorBridgeParent::PostInsertVsyncProfilerMarker(
+ TimeStamp aVsyncTimestamp) {
+ // Called in the vsync thread
+ if (profiler_is_active() && CompositorThreadHolder::IsActive()) {
+ CompositorThread()->Dispatch(
+ NewRunnableFunction("InsertVsyncProfilerMarkerRunnable",
+ InsertVsyncProfilerMarker, aVsyncTimestamp));
+ }
+}
+
+widget::PCompositorWidgetParent*
+CompositorBridgeParent::AllocPCompositorWidgetParent(
+ const CompositorWidgetInitData& aInitData) {
+#if defined(MOZ_WIDGET_SUPPORTS_OOP_COMPOSITING)
+ if (mWidget) {
+ // Should not create two widgets on the same compositor.
+ return nullptr;
+ }
+
+ widget::CompositorWidgetParent* widget =
+ new widget::CompositorWidgetParent(aInitData, mOptions);
+ widget->AddRef();
+
+ // Sending the constructor acts as initialization as well.
+ mWidget = widget;
+ return widget;
+#else
+ return nullptr;
+#endif
+}
+
+bool CompositorBridgeParent::DeallocPCompositorWidgetParent(
+ PCompositorWidgetParent* aActor) {
+#if defined(MOZ_WIDGET_SUPPORTS_OOP_COMPOSITING)
+ static_cast<widget::CompositorWidgetParent*>(aActor)->Release();
+ return true;
+#else
+ return false;
+#endif
+}
+
+CompositorController*
+CompositorBridgeParent::LayerTreeState::GetCompositorController() const {
+ return mParent;
+}
+
+void CompositorBridgeParent::NotifyDidSceneBuild(
+ RefPtr<const wr::WebRenderPipelineInfo> aInfo) {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+ if (mPaused) {
+ return;
+ }
+
+ if (mWrBridge) {
+ mWrBridge->NotifyDidSceneBuild(aInfo);
+ }
+}
+
+void CompositorBridgeParent::NotifyDidRender(const VsyncId& aCompositeStartId,
+ TimeStamp& aCompositeStart,
+ TimeStamp& aRenderStart,
+ TimeStamp& aCompositeEnd,
+ wr::RendererStats* aStats) {
+ if (!mWrBridge) {
+ return;
+ }
+
+ MOZ_RELEASE_ASSERT(mWrBridge->IsRootWebRenderBridgeParent());
+
+ RefPtr<UiCompositorControllerParent> uiController =
+ UiCompositorControllerParent::GetFromRootLayerTreeId(mRootLayerTreeID);
+
+ if (uiController && mIsForcedFirstPaint) {
+ uiController->NotifyFirstPaint();
+ mIsForcedFirstPaint = false;
+ }
+
+ nsTArray<CompositionPayload> payload =
+ mWrBridge->TakePendingScrollPayload(aCompositeStartId);
+ if (!payload.IsEmpty()) {
+ RecordCompositionPayloadsPresented(aCompositeEnd, payload);
+ }
+
+ nsTArray<ImageCompositeNotificationInfo> notifications;
+ mWrBridge->ExtractImageCompositeNotifications(&notifications);
+ if (!notifications.IsEmpty()) {
+ Unused << ImageBridgeParent::NotifyImageComposites(notifications);
+ }
+}
+
+bool CompositorBridgeParent::sStable = false;
+uint32_t CompositorBridgeParent::sFramesComposited = 0;
+
+/* static */ void CompositorBridgeParent::ResetStable() {
+ if (!CompositorThreadHolder::IsInCompositorThread()) {
+ if (CompositorThread()) {
+ CompositorThread()->Dispatch(
+ NewRunnableFunction("CompositorBridgeParent::ResetStable",
+ &CompositorBridgeParent::ResetStable));
+ }
+
+ // If there is no compositor thread, e.g. due to shutdown, then we can
+ // safefully just ignore this request.
+ return;
+ }
+
+ sStable = false;
+ sFramesComposited = 0;
+}
+
+void CompositorBridgeParent::MaybeDeclareStable() {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+
+ if (sStable) {
+ return;
+ }
+
+ // Once we render as many frames as the threshold, we declare this instance of
+ // the GPU process 'stable'. This causes the parent process to always respawn
+ // the GPU process if it crashes.
+ if (++sFramesComposited >=
+ StaticPrefs::layers_gpu_process_stable_frame_threshold()) {
+ sStable = true;
+
+ NS_DispatchToMainThread(NS_NewRunnableFunction(
+ "CompositorBridgeParent::MaybeDeclareStable", []() -> void {
+ if (XRE_IsParentProcess()) {
+ GPUProcessManager* gpm = GPUProcessManager::Get();
+ if (gpm) {
+ gpm->OnProcessDeclaredStable();
+ }
+ } else {
+ gfx::GPUParent* gpu = gfx::GPUParent::GetSingleton();
+ if (gpu && gpu->CanSend()) {
+ Unused << gpu->SendDeclareStable();
+ }
+ }
+ }));
+ }
+}
+
+void CompositorBridgeParent::NotifyPipelineRendered(
+ const wr::PipelineId& aPipelineId, const wr::Epoch& aEpoch,
+ const VsyncId& aCompositeStartId, TimeStamp& aCompositeStart,
+ TimeStamp& aRenderStart, TimeStamp& aCompositeEnd,
+ wr::RendererStats* aStats) {
+ if (!mWrBridge || !mAsyncImageManager) {
+ return;
+ }
+
+ bool isRoot = mWrBridge->PipelineId() == aPipelineId;
+ RefPtr<WebRenderBridgeParent> wrBridge =
+ isRoot ? mWrBridge
+ : RefPtr<WebRenderBridgeParent>(
+ mAsyncImageManager->GetWrBridge(aPipelineId));
+ if (!wrBridge) {
+ return;
+ }
+
+ CompositorBridgeParentBase* compBridge =
+ isRoot ? this : wrBridge->GetCompositorBridge();
+ if (!compBridge) {
+ return;
+ }
+
+ MOZ_RELEASE_ASSERT(isRoot == wrBridge->IsRootWebRenderBridgeParent());
+
+ wrBridge->RemoveEpochDataPriorTo(aEpoch);
+
+ nsTArray<FrameStats> stats;
+ nsTArray<TransactionId> transactions;
+
+ RefPtr<UiCompositorControllerParent> uiController =
+ UiCompositorControllerParent::GetFromRootLayerTreeId(mRootLayerTreeID);
+
+ wrBridge->FlushTransactionIdsForEpoch(
+ aEpoch, aCompositeStartId, aCompositeStart, aRenderStart, aCompositeEnd,
+ uiController, aStats, stats, transactions);
+ if (transactions.IsEmpty()) {
+ MOZ_ASSERT(stats.IsEmpty());
+ return;
+ }
+
+ MaybeDeclareStable();
+
+ LayersId layersId = isRoot ? LayersId{0} : wrBridge->GetLayersId();
+ Unused << compBridge->SendDidComposite(layersId, transactions,
+ aCompositeStart, aCompositeEnd);
+
+ if (!stats.IsEmpty()) {
+ Unused << SendNotifyFrameStats(stats);
+ }
+}
+
+RefPtr<AsyncImagePipelineManager>
+CompositorBridgeParent::GetAsyncImagePipelineManager() const {
+ return mAsyncImageManager;
+}
+
+/* static */ CompositorBridgeParent::LayerTreeState*
+CompositorBridgeParent::GetIndirectShadowTree(LayersId aId) {
+ // Only the compositor thread should use this method variant
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+
+ MonitorAutoLock lock(*sIndirectLayerTreesLock);
+ LayerTreeMap::iterator cit = sIndirectLayerTrees.find(aId);
+ if (sIndirectLayerTrees.end() == cit) {
+ return nullptr;
+ }
+ return &cit->second;
+}
+
+/* static */
+bool CompositorBridgeParent::CallWithIndirectShadowTree(
+ LayersId aId,
+ const std::function<void(CompositorBridgeParent::LayerTreeState&)>& aFunc) {
+ if (!sIndirectLayerTreesLock) {
+ // Can hapen during shutdown
+ return false;
+ }
+ // Note that this does not make things universally threadsafe just because the
+ // sIndirectLayerTreesLock mutex is held. This is because the compositor
+ // thread can mutate the LayerTreeState outside the lock. It does however
+ // ensure that the *storage* for the LayerTreeState remains stable, since we
+ // should always hold the lock when adding/removing entries to the map.
+ MonitorAutoLock lock(*sIndirectLayerTreesLock);
+ LayerTreeMap::iterator cit = sIndirectLayerTrees.find(aId);
+ if (sIndirectLayerTrees.end() == cit) {
+ return false;
+ }
+ aFunc(cit->second);
+ return true;
+}
+
+static CompositorBridgeParent::LayerTreeState* GetStateForRoot(
+ LayersId aContentLayersId, const MonitorAutoLock& aProofOfLock) {
+ CompositorBridgeParent::LayerTreeState* contentState = nullptr;
+ LayerTreeMap::iterator itr = sIndirectLayerTrees.find(aContentLayersId);
+ if (sIndirectLayerTrees.end() != itr) {
+ contentState = &itr->second;
+ }
+
+ // |contentState| is the state for the content process, but we want the
+ // APZCTMParent for the parent process owning that content process. So we have
+ // to jump to the LayerTreeState for the root layer tree id for that layer
+ // tree, and use the mApzcTreeManagerParent from that. This should also work
+ // with nested content processes, because RootLayerTreeId() will bypass any
+ // intermediate processes' ids and go straight to the root.
+ if (contentState && contentState->mParent) {
+ LayersId rootLayersId = contentState->mParent->RootLayerTreeId();
+ itr = sIndirectLayerTrees.find(rootLayersId);
+ CompositorBridgeParent::LayerTreeState* rootState =
+ (sIndirectLayerTrees.end() != itr) ? &itr->second : nullptr;
+ return rootState;
+ }
+
+ // Don't return contentState, that would be a lie!
+ return nullptr;
+}
+
+/* static */
+APZCTreeManagerParent* CompositorBridgeParent::GetApzcTreeManagerParentForRoot(
+ LayersId aContentLayersId) {
+ MonitorAutoLock lock(*sIndirectLayerTreesLock);
+ CompositorBridgeParent::LayerTreeState* state =
+ GetStateForRoot(aContentLayersId, lock);
+ return state ? state->mApzcTreeManagerParent : nullptr;
+}
+
+/* static */
+GeckoContentController*
+CompositorBridgeParent::GetGeckoContentControllerForRoot(
+ LayersId aContentLayersId) {
+ MonitorAutoLock lock(*sIndirectLayerTreesLock);
+ CompositorBridgeParent::LayerTreeState* state =
+ GetStateForRoot(aContentLayersId, lock);
+ return state ? state->mController.get() : nullptr;
+}
+
+PTextureParent* CompositorBridgeParent::AllocPTextureParent(
+ const SurfaceDescriptor& aSharedData, ReadLockDescriptor& aReadLock,
+ const LayersBackend& aLayersBackend, const TextureFlags& aFlags,
+ const LayersId& aId, const uint64_t& aSerial,
+ const wr::MaybeExternalImageId& aExternalImageId) {
+ return TextureHost::CreateIPDLActor(
+ this, aSharedData, std::move(aReadLock), aLayersBackend, aFlags,
+ mCompositorManager->GetContentId(), aSerial, aExternalImageId);
+}
+
+bool CompositorBridgeParent::DeallocPTextureParent(PTextureParent* actor) {
+ return TextureHost::DestroyIPDLActor(actor);
+}
+
+mozilla::ipc::IPCResult CompositorBridgeParent::RecvInitPCanvasParent(
+ Endpoint<PCanvasParent>&& aEndpoint) {
+ MOZ_CRASH("PCanvasParent shouldn't be created via CompositorBridgeParent.");
+}
+
+mozilla::ipc::IPCResult CompositorBridgeParent::RecvReleasePCanvasParent() {
+ MOZ_CRASH("PCanvasParent shouldn't be released via CompositorBridgeParent.");
+}
+
+bool CompositorBridgeParent::IsSameProcess() const {
+ return OtherPid() == base::GetCurrentProcId();
+}
+
+void CompositorBridgeParent::NotifyWebRenderDisableNativeCompositor() {
+ MOZ_ASSERT(CompositorThread()->IsOnCurrentThread());
+ if (mWrBridge) {
+ mWrBridge->DisableNativeCompositor();
+ }
+}
+
+int32_t RecordContentFrameTime(
+ const VsyncId& aTxnId, const TimeStamp& aVsyncStart,
+ const TimeStamp& aTxnStart, const VsyncId& aCompositeId,
+ const TimeStamp& aCompositeEnd, const TimeDuration& aFullPaintTime,
+ const TimeDuration& aVsyncRate, bool aContainsSVGGroup,
+ bool aRecordUploadStats, wr::RendererStats* aStats /* = nullptr */) {
+ double latencyMs = (aCompositeEnd - aTxnStart).ToMilliseconds();
+ double latencyNorm = latencyMs / aVsyncRate.ToMilliseconds();
+ int32_t fracLatencyNorm = lround(latencyNorm * 100.0);
+
+ if (profiler_thread_is_being_profiled_for_markers()) {
+ struct ContentFrameMarker {
+ static constexpr Span<const char> MarkerTypeName() {
+ return MakeStringSpan("CONTENT_FRAME_TIME");
+ }
+ static void StreamJSONMarkerData(
+ baseprofiler::SpliceableJSONWriter& aWriter) {}
+ static MarkerSchema MarkerTypeDisplay() {
+ using MS = MarkerSchema;
+ MS schema{MS::Location::MarkerChart, MS::Location::MarkerTable};
+ // Nothing outside the defaults.
+ return schema;
+ }
+ };
+
+ profiler_add_marker("CONTENT_FRAME_TIME", geckoprofiler::category::GRAPHICS,
+ MarkerTiming::Interval(aTxnStart, aCompositeEnd),
+ ContentFrameMarker{});
+ }
+
+ Telemetry::Accumulate(Telemetry::CONTENT_FRAME_TIME, fracLatencyNorm);
+
+ if (!(aTxnId == VsyncId()) && aVsyncStart) {
+ latencyMs = (aCompositeEnd - aVsyncStart).ToMilliseconds();
+ latencyNorm = latencyMs / aVsyncRate.ToMilliseconds();
+ fracLatencyNorm = lround(latencyNorm * 100.0);
+ int32_t result = fracLatencyNorm;
+ Telemetry::Accumulate(Telemetry::CONTENT_FRAME_TIME_VSYNC, fracLatencyNorm);
+
+ if (aContainsSVGGroup) {
+ Telemetry::Accumulate(Telemetry::CONTENT_FRAME_TIME_WITH_SVG,
+ fracLatencyNorm);
+ }
+
+ // Record CONTENT_FRAME_TIME_REASON.
+ //
+ // Note that deseralizing a layers update (RecvUpdate) can delay the receipt
+ // of the composite vsync message
+ // (CompositorBridgeParent::CompositeToTarget), since they're using the same
+ // thread. This can mean that compositing might start significantly late,
+ // but this code will still detect it as having successfully started on the
+ // right vsync (which is somewhat correct). We'd now have reduced time left
+ // in the vsync interval to finish compositing, so the chances of a missed
+ // frame increases. This is effectively including the RecvUpdate work as
+ // part of the 'compositing' phase for this metric, but it isn't included in
+ // COMPOSITE_TIME, and *is* included in CONTENT_FULL_PAINT_TIME.
+ //
+ // Also of note is that when the root WebRenderBridgeParent decides to
+ // skip a composite (due to the Renderer being busy), that won't notify
+ // child WebRenderBridgeParents. That failure will show up as the
+ // composite starting late (since it did), but it's really a fault of a
+ // slow composite on the previous frame, not a slow
+ // CONTENT_FULL_PAINT_TIME. It would be nice to have a separate bucket for
+ // this category (scene was ready on the next vsync, but we chose not to
+ // composite), but I can't find a way to locate the right child
+ // WebRenderBridgeParents from the root. WebRender notifies us of the
+ // child pipelines contained within a render, after it finishes, but I
+ // can't see how to query what child pipeline would have been rendered,
+ // when we choose to not do it.
+ if (fracLatencyNorm < 200) {
+ // Success
+ Telemetry::AccumulateCategorical(
+ LABELS_CONTENT_FRAME_TIME_REASON::OnTime);
+ } else {
+ if (aCompositeId == VsyncId()) {
+ // aCompositeId is 0, possibly something got trigged from
+ // outside vsync?
+ Telemetry::AccumulateCategorical(
+ LABELS_CONTENT_FRAME_TIME_REASON::NoVsyncNoId);
+ } else if (aTxnId >= aCompositeId) {
+ // Vsync ids are nonsensical, maybe we're trying to catch up?
+ Telemetry::AccumulateCategorical(
+ LABELS_CONTENT_FRAME_TIME_REASON::NoVsync);
+ } else if (aCompositeId - aTxnId > 1) {
+ // Composite started late (and maybe took too long as well)
+ if (aFullPaintTime >= TimeDuration::FromMilliseconds(20)) {
+ Telemetry::AccumulateCategorical(
+ LABELS_CONTENT_FRAME_TIME_REASON::MissedCompositeLong);
+ } else if (aFullPaintTime >= TimeDuration::FromMilliseconds(10)) {
+ Telemetry::AccumulateCategorical(
+ LABELS_CONTENT_FRAME_TIME_REASON::MissedCompositeMid);
+ } else if (aFullPaintTime >= TimeDuration::FromMilliseconds(5)) {
+ Telemetry::AccumulateCategorical(
+ LABELS_CONTENT_FRAME_TIME_REASON::MissedCompositeLow);
+ } else {
+ Telemetry::AccumulateCategorical(
+ LABELS_CONTENT_FRAME_TIME_REASON::MissedComposite);
+ }
+ } else {
+ // Composite started on time, but must have taken too long.
+ Telemetry::AccumulateCategorical(
+ LABELS_CONTENT_FRAME_TIME_REASON::SlowComposite);
+ }
+ }
+
+ if (aRecordUploadStats) {
+ if (aStats) {
+ latencyMs -= (double(aStats->resource_upload_time) / 1000000.0);
+ latencyNorm = latencyMs / aVsyncRate.ToMilliseconds();
+ fracLatencyNorm = lround(latencyNorm * 100.0);
+ }
+ Telemetry::Accumulate(
+ Telemetry::CONTENT_FRAME_TIME_WITHOUT_RESOURCE_UPLOAD,
+ fracLatencyNorm);
+
+ if (aStats) {
+ latencyMs -= (double(aStats->gpu_cache_upload_time) / 1000000.0);
+ latencyNorm = latencyMs / aVsyncRate.ToMilliseconds();
+ fracLatencyNorm = lround(latencyNorm * 100.0);
+ }
+ Telemetry::Accumulate(Telemetry::CONTENT_FRAME_TIME_WITHOUT_UPLOAD,
+ fracLatencyNorm);
+ }
+ return result;
+ }
+
+ return 0;
+}
+
+mozilla::ipc::IPCResult CompositorBridgeParent::RecvBeginRecording(
+ const TimeStamp& aRecordingStart, BeginRecordingResolver&& aResolve) {
+ if (mHaveCompositionRecorder) {
+ aResolve(false);
+ return IPC_OK();
+ }
+
+ if (mWrBridge) {
+ mWrBridge->BeginRecording(aRecordingStart);
+ }
+
+ mHaveCompositionRecorder = true;
+ aResolve(true);
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult CompositorBridgeParent::RecvEndRecording(
+ EndRecordingResolver&& aResolve) {
+ if (!mHaveCompositionRecorder) {
+ aResolve(Nothing());
+ return IPC_OK();
+ }
+
+ if (mWrBridge) {
+ mWrBridge->EndRecording()->Then(
+ NS_GetCurrentThread(), __func__,
+ [resolve{aResolve}](FrameRecording&& recording) {
+ resolve(Some(std::move(recording)));
+ },
+ [resolve{aResolve}]() { resolve(Nothing()); });
+ } else {
+ aResolve(Nothing());
+ }
+
+ mHaveCompositionRecorder = false;
+
+ return IPC_OK();
+}
+
+void CompositorBridgeParent::FlushPendingWrTransactionEventsWithWait() {
+ if (!mWrBridge) {
+ return;
+ }
+
+ std::vector<RefPtr<WebRenderBridgeParent>> bridgeParents;
+ { // scope lock
+ MonitorAutoLock lock(*sIndirectLayerTreesLock);
+ ForEachIndirectLayerTree([&](LayerTreeState* lts, LayersId) -> void {
+ if (lts->mWrBridge) {
+ bridgeParents.emplace_back(lts->mWrBridge);
+ }
+ });
+ }
+
+ for (auto& bridge : bridgeParents) {
+ bridge->FlushPendingWrTransactionEventsWithWait();
+ }
+}
+
+void RecordCompositionPayloadsPresented(
+ const TimeStamp& aCompositionEndTime,
+ const nsTArray<CompositionPayload>& aPayloads) {
+ if (aPayloads.Length()) {
+ TimeStamp presented = aCompositionEndTime;
+ for (const CompositionPayload& payload : aPayloads) {
+ if (profiler_thread_is_being_profiled_for_markers()) {
+ MOZ_RELEASE_ASSERT(payload.mType <= kHighestCompositionPayloadType);
+ nsAutoCString name(
+ kCompositionPayloadTypeNames[uint8_t(payload.mType)]);
+ name.AppendLiteral(" Payload Presented");
+ // This doesn't really need to be a text marker. Once we have a version
+ // of profiler_add_marker that accepts both a start time and an end
+ // time, we could use that here.
+ nsPrintfCString text(
+ "Latency: %dms",
+ int32_t((presented - payload.mTimeStamp).ToMilliseconds()));
+ PROFILER_MARKER_TEXT(
+ name, GRAPHICS,
+ MarkerTiming::Interval(payload.mTimeStamp, presented), text);
+ }
+
+ if (payload.mType == CompositionPayloadType::eKeyPress) {
+ Telemetry::AccumulateTimeDelta(
+ mozilla::Telemetry::KEYPRESS_PRESENT_LATENCY, payload.mTimeStamp,
+ presented);
+ } else if (payload.mType == CompositionPayloadType::eAPZScroll) {
+ Telemetry::AccumulateTimeDelta(
+ mozilla::Telemetry::SCROLL_PRESENT_LATENCY, payload.mTimeStamp,
+ presented);
+ } else if (payload.mType ==
+ CompositionPayloadType::eMouseUpFollowedByClick) {
+ Telemetry::AccumulateTimeDelta(
+ mozilla::Telemetry::MOUSEUP_FOLLOWED_BY_CLICK_PRESENT_LATENCY,
+ payload.mTimeStamp, presented);
+ }
+ }
+ }
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/ipc/CompositorBridgeParent.h b/gfx/layers/ipc/CompositorBridgeParent.h
new file mode 100644
index 0000000000..6b15b535f1
--- /dev/null
+++ b/gfx/layers/ipc/CompositorBridgeParent.h
@@ -0,0 +1,656 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_CompositorBridgeParent_h
+#define mozilla_layers_CompositorBridgeParent_h
+
+#include <stdint.h> // for uint64_t
+#include <unordered_map>
+#include "mozilla/Assertions.h" // for MOZ_ASSERT_HELPER2
+#include "mozilla/Maybe.h"
+#include "mozilla/Monitor.h" // for Monitor
+#include "mozilla/RefPtr.h" // for RefPtr
+#include "mozilla/TimeStamp.h" // for TimeStamp
+#include "mozilla/gfx/Point.h" // for IntSize
+#include "mozilla/ipc/ProtocolUtils.h"
+#include "mozilla/ipc/SharedMemory.h"
+#include "mozilla/layers/CompositorController.h"
+#include "mozilla/layers/CompositorVsyncSchedulerOwner.h"
+#include "mozilla/layers/FocusTarget.h"
+#include "mozilla/layers/ISurfaceAllocator.h" // for IShmemAllocator
+#include "mozilla/layers/LayersTypes.h"
+#include "mozilla/layers/PCompositorBridgeParent.h"
+#include "mozilla/webrender/WebRenderTypes.h"
+
+namespace mozilla {
+
+namespace gfx {
+class GPUProcessManager;
+class GPUParent;
+} // namespace gfx
+
+namespace ipc {
+class Shmem;
+} // namespace ipc
+
+namespace widget {
+class CompositorWidget;
+}
+
+namespace wr {
+class WebRenderPipelineInfo;
+struct Epoch;
+struct MemoryReport;
+struct PipelineId;
+struct RendererStats;
+} // namespace wr
+
+namespace layers {
+
+class APZCTreeManager;
+class APZCTreeManagerParent;
+class APZSampler;
+class APZTestData;
+class APZUpdater;
+class AsyncImagePipelineManager;
+class CompositorAnimationStorage;
+class CompositorBridgeParent;
+class CompositorManagerParent;
+class CompositorVsyncScheduler;
+class FrameUniformityData;
+class GeckoContentController;
+class IAPZCTreeManager;
+class OMTASampler;
+class ContentCompositorBridgeParent;
+class CompositorThreadHolder;
+class InProcessCompositorSession;
+class UiCompositorControllerParent;
+class WebRenderBridgeParent;
+class WebRenderScrollDataWrapper;
+struct CollectedFrames;
+
+struct ScopedLayerTreeRegistration {
+ // For WebRender
+ ScopedLayerTreeRegistration(LayersId aLayersId,
+ GeckoContentController* aController);
+ ~ScopedLayerTreeRegistration();
+
+ private:
+ LayersId mLayersId;
+};
+
+class CompositorBridgeParentBase : public PCompositorBridgeParent,
+ public HostIPCAllocator,
+ public mozilla::ipc::IShmemAllocator {
+ friend class PCompositorBridgeParent;
+
+ public:
+ explicit CompositorBridgeParentBase(CompositorManagerParent* aManager);
+
+ virtual bool SetTestSampleTime(const LayersId& aId, const TimeStamp& aTime) {
+ return true;
+ }
+ virtual void LeaveTestMode(const LayersId& aId) {}
+ enum class TransformsToSkip : uint8_t { NoneOfThem = 0, APZ = 1 };
+ virtual void SetTestAsyncScrollOffset(
+ const LayersId& aLayersId, const ScrollableLayerGuid::ViewID& aScrollId,
+ const CSSPoint& aPoint) = 0;
+ virtual void SetTestAsyncZoom(const LayersId& aLayersId,
+ const ScrollableLayerGuid::ViewID& aScrollId,
+ const LayerToParentLayerScale& aZoom) = 0;
+ virtual void FlushApzRepaints(const LayersId& aLayersId) = 0;
+ virtual void GetAPZTestData(const LayersId& aLayersId,
+ APZTestData* aOutData) {}
+ virtual void GetFrameUniformity(const LayersId& aLayersId,
+ FrameUniformityData* data) = 0;
+ virtual void SetConfirmedTargetAPZC(
+ const LayersId& aLayersId, const uint64_t& aInputBlockId,
+ nsTArray<ScrollableLayerGuid>&& aTargets) = 0;
+
+ IShmemAllocator* AsShmemAllocator() override { return this; }
+
+ CompositorBridgeParentBase* AsCompositorBridgeParentBase() override {
+ return this;
+ }
+
+ mozilla::ipc::IPCResult RecvSyncWithCompositor() { return IPC_OK(); }
+
+ mozilla::ipc::IPCResult Recv__delete__() override { return IPC_OK(); }
+
+ virtual void ObserveLayersUpdate(LayersId aLayersId,
+ LayersObserverEpoch aEpoch,
+ bool aActive) = 0;
+
+ // HostIPCAllocator
+ base::ProcessId GetChildProcessId() override;
+ dom::ContentParentId GetContentId() override;
+ void NotifyNotUsed(PTextureParent* aTexture,
+ uint64_t aTransactionId) override;
+ void SendAsyncMessage(
+ const nsTArray<AsyncParentMessageData>& aMessage) override;
+
+ // IShmemAllocator
+ bool AllocShmem(size_t aSize, mozilla::ipc::Shmem* aShmem) override;
+ bool AllocUnsafeShmem(size_t aSize, mozilla::ipc::Shmem* aShmem) override;
+ bool DeallocShmem(mozilla::ipc::Shmem& aShmem) override;
+
+ NS_IMETHOD_(MozExternalRefCountType) AddRef() override {
+ return HostIPCAllocator::AddRef();
+ }
+ NS_IMETHOD_(MozExternalRefCountType) Release() override {
+ return HostIPCAllocator::Release();
+ }
+ virtual bool IsRemote() const { return false; }
+
+ virtual UniquePtr<SurfaceDescriptor> LookupSurfaceDescriptorForClientTexture(
+ const int64_t aTextureId) {
+ MOZ_CRASH("Should only be called on ContentCompositorBridgeParent.");
+ }
+
+ virtual void NotifyMemoryPressure() {}
+ virtual void AccumulateMemoryReport(wr::MemoryReport*) {}
+
+ protected:
+ virtual ~CompositorBridgeParentBase();
+
+ virtual PAPZParent* AllocPAPZParent(const LayersId& layersId) = 0;
+ virtual bool DeallocPAPZParent(PAPZParent* aActor) = 0;
+
+ virtual PAPZCTreeManagerParent* AllocPAPZCTreeManagerParent(
+ const LayersId& layersId) = 0;
+ virtual bool DeallocPAPZCTreeManagerParent(
+ PAPZCTreeManagerParent* aActor) = 0;
+
+ virtual PTextureParent* AllocPTextureParent(
+ const SurfaceDescriptor& aSharedData, ReadLockDescriptor& aReadLock,
+ const LayersBackend& aBackend, const TextureFlags& aTextureFlags,
+ const LayersId& id, const uint64_t& aSerial,
+ const MaybeExternalImageId& aExternalImageId) = 0;
+ virtual bool DeallocPTextureParent(PTextureParent* aActor) = 0;
+
+ virtual PWebRenderBridgeParent* AllocPWebRenderBridgeParent(
+ const PipelineId& pipelineId, const LayoutDeviceIntSize& aSize,
+ const WindowKind& aWindowKind) = 0;
+ virtual bool DeallocPWebRenderBridgeParent(
+ PWebRenderBridgeParent* aActor) = 0;
+
+ virtual PCompositorWidgetParent* AllocPCompositorWidgetParent(
+ const CompositorWidgetInitData& aInitData) = 0;
+ virtual bool DeallocPCompositorWidgetParent(
+ PCompositorWidgetParent* aActor) = 0;
+
+ virtual mozilla::ipc::IPCResult RecvAdoptChild(const LayersId& id) = 0;
+ virtual mozilla::ipc::IPCResult RecvFlushRenderingAsync(
+ const wr::RenderReasons& aReasons) = 0;
+ virtual mozilla::ipc::IPCResult RecvForcePresent(
+ const wr::RenderReasons& aReasons) = 0;
+ virtual mozilla::ipc::IPCResult RecvBeginRecording(
+ const TimeStamp& aRecordingStart, BeginRecordingResolver&& aResolve) = 0;
+ virtual mozilla::ipc::IPCResult RecvEndRecording(
+ EndRecordingResolver&& aResolve) = 0;
+ virtual mozilla::ipc::IPCResult RecvInitialize(
+ const LayersId& rootLayerTreeId) = 0;
+ virtual mozilla::ipc::IPCResult RecvWillClose() = 0;
+ virtual mozilla::ipc::IPCResult RecvPause() = 0;
+ virtual mozilla::ipc::IPCResult RecvRequestFxrOutput() = 0;
+ virtual mozilla::ipc::IPCResult RecvResume() = 0;
+ virtual mozilla::ipc::IPCResult RecvResumeAsync() = 0;
+ virtual mozilla::ipc::IPCResult RecvNotifyChildCreated(
+ const LayersId& id, CompositorOptions* compositorOptions) = 0;
+ virtual mozilla::ipc::IPCResult RecvMapAndNotifyChildCreated(
+ const LayersId& id, const ProcessId& owner,
+ CompositorOptions* compositorOptions) = 0;
+ virtual mozilla::ipc::IPCResult RecvNotifyChildRecreated(
+ const LayersId& id, CompositorOptions* compositorOptions) = 0;
+ virtual mozilla::ipc::IPCResult RecvFlushRendering(
+ const wr::RenderReasons& aReasons) = 0;
+ virtual mozilla::ipc::IPCResult RecvNotifyMemoryPressure() = 0;
+ virtual mozilla::ipc::IPCResult RecvWaitOnTransactionProcessed() = 0;
+ virtual mozilla::ipc::IPCResult RecvStartFrameTimeRecording(
+ const int32_t& bufferSize, uint32_t* startIndex) = 0;
+ virtual mozilla::ipc::IPCResult RecvStopFrameTimeRecording(
+ const uint32_t& startIndex, nsTArray<float>* intervals) = 0;
+ virtual mozilla::ipc::IPCResult RecvCheckContentOnlyTDR(
+ const uint32_t& sequenceNum, bool* isContentOnlyTDR) = 0;
+ virtual mozilla::ipc::IPCResult RecvInitPCanvasParent(
+ Endpoint<PCanvasParent>&& aEndpoint) = 0;
+ virtual mozilla::ipc::IPCResult RecvReleasePCanvasParent() = 0;
+
+ bool mCanSend;
+
+ protected:
+ RefPtr<CompositorManagerParent> mCompositorManager;
+};
+
+MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(
+ CompositorBridgeParentBase::TransformsToSkip)
+
+class CompositorBridgeParent final : public CompositorBridgeParentBase,
+ public CompositorController {
+ friend class CompositorThreadHolder;
+ friend class InProcessCompositorSession;
+ friend class gfx::GPUProcessManager;
+ friend class gfx::GPUParent;
+ friend class PCompositorBridgeParent;
+
+ public:
+ NS_IMETHOD_(MozExternalRefCountType) AddRef() override {
+ return CompositorBridgeParentBase::AddRef();
+ }
+ NS_IMETHOD_(MozExternalRefCountType) Release() override {
+ return CompositorBridgeParentBase::Release();
+ }
+
+ explicit CompositorBridgeParent(CompositorManagerParent* aManager,
+ CSSToLayoutDeviceScale aScale,
+ const TimeDuration& aVsyncRate,
+ const CompositorOptions& aOptions,
+ bool aUseExternalSurfaceSize,
+ const gfx::IntSize& aSurfaceSize,
+ uint64_t aInnerWindowId);
+
+ void InitSameProcess(widget::CompositorWidget* aWidget,
+ const LayersId& aLayerTreeId);
+
+ mozilla::ipc::IPCResult RecvInitialize(
+ const LayersId& aRootLayerTreeId) override;
+ mozilla::ipc::IPCResult RecvWillClose() override;
+ mozilla::ipc::IPCResult RecvPause() override;
+ mozilla::ipc::IPCResult RecvRequestFxrOutput() override;
+ mozilla::ipc::IPCResult RecvResume() override;
+ mozilla::ipc::IPCResult RecvResumeAsync() override;
+ mozilla::ipc::IPCResult RecvNotifyChildCreated(
+ const LayersId& child, CompositorOptions* aOptions) override;
+ mozilla::ipc::IPCResult RecvMapAndNotifyChildCreated(
+ const LayersId& child, const base::ProcessId& pid,
+ CompositorOptions* aOptions) override;
+ mozilla::ipc::IPCResult RecvNotifyChildRecreated(
+ const LayersId& child, CompositorOptions* aOptions) override;
+ mozilla::ipc::IPCResult RecvAdoptChild(const LayersId& child) override;
+ mozilla::ipc::IPCResult RecvFlushRendering(
+ const wr::RenderReasons& aReasons) override;
+ mozilla::ipc::IPCResult RecvFlushRenderingAsync(
+ const wr::RenderReasons& aReasons) override;
+ mozilla::ipc::IPCResult RecvWaitOnTransactionProcessed() override;
+ mozilla::ipc::IPCResult RecvForcePresent(
+ const wr::RenderReasons& aReasons) override;
+
+ mozilla::ipc::IPCResult RecvStartFrameTimeRecording(
+ const int32_t& aBufferSize, uint32_t* aOutStartIndex) override;
+ mozilla::ipc::IPCResult RecvStopFrameTimeRecording(
+ const uint32_t& aStartIndex, nsTArray<float>* intervals) override;
+
+ mozilla::ipc::IPCResult RecvCheckContentOnlyTDR(
+ const uint32_t& sequenceNum, bool* isContentOnlyTDR) override {
+ return IPC_OK();
+ }
+
+ mozilla::ipc::IPCResult RecvNotifyMemoryPressure() override;
+ mozilla::ipc::IPCResult RecvBeginRecording(
+ const TimeStamp& aRecordingStart,
+ BeginRecordingResolver&& aResolve) override;
+ mozilla::ipc::IPCResult RecvEndRecording(
+ EndRecordingResolver&& aResolve) override;
+
+ void NotifyMemoryPressure() override;
+ void AccumulateMemoryReport(wr::MemoryReport*) override;
+
+ void ActorDestroy(ActorDestroyReason why) override;
+
+ bool SetTestSampleTime(const LayersId& aId, const TimeStamp& aTime) override;
+ void LeaveTestMode(const LayersId& aId) override;
+ CompositorAnimationStorage* GetAnimationStorage();
+ using JankedAnimations =
+ std::unordered_map<LayersId, nsTArray<uint64_t>, LayersId::HashFn>;
+ void NotifyJankedAnimations(const JankedAnimations& aJankedAnimations);
+ void SetTestAsyncScrollOffset(const LayersId& aLayersId,
+ const ScrollableLayerGuid::ViewID& aScrollId,
+ const CSSPoint& aPoint) override;
+ void SetTestAsyncZoom(const LayersId& aLayersId,
+ const ScrollableLayerGuid::ViewID& aScrollId,
+ const LayerToParentLayerScale& aZoom) override;
+ void FlushApzRepaints(const LayersId& aLayersId) override;
+ void GetAPZTestData(const LayersId& aLayersId,
+ APZTestData* aOutData) override;
+ void GetFrameUniformity(const LayersId& aLayersId,
+ FrameUniformityData* data) override;
+ void SetConfirmedTargetAPZC(
+ const LayersId& aLayersId, const uint64_t& aInputBlockId,
+ nsTArray<ScrollableLayerGuid>&& aTargets) override;
+ void SetFixedLayerMargins(ScreenIntCoord aTop, ScreenIntCoord aBottom);
+
+ PTextureParent* AllocPTextureParent(
+ const SurfaceDescriptor& aSharedData, ReadLockDescriptor& aReadLock,
+ const LayersBackend& aLayersBackend, const TextureFlags& aFlags,
+ const LayersId& aId, const uint64_t& aSerial,
+ const wr::MaybeExternalImageId& aExternalImageId) override;
+ bool DeallocPTextureParent(PTextureParent* actor) override;
+
+ mozilla::ipc::IPCResult RecvInitPCanvasParent(
+ Endpoint<PCanvasParent>&& aEndpoint) final;
+
+ mozilla::ipc::IPCResult RecvReleasePCanvasParent() final;
+
+ bool IsSameProcess() const override;
+
+ void NotifyWebRenderDisableNativeCompositor();
+
+ void NotifyDidRender(const VsyncId& aCompositeStartId,
+ TimeStamp& aCompositeStart, TimeStamp& aRenderStart,
+ TimeStamp& aCompositeEnd,
+ wr::RendererStats* aStats = nullptr);
+ void NotifyPipelineRendered(const wr::PipelineId& aPipelineId,
+ const wr::Epoch& aEpoch,
+ const VsyncId& aCompositeStartId,
+ TimeStamp& aCompositeStart,
+ TimeStamp& aRenderStart, TimeStamp& aCompositeEnd,
+ wr::RendererStats* aStats = nullptr);
+ void NotifyDidSceneBuild(RefPtr<const wr::WebRenderPipelineInfo> aInfo);
+ RefPtr<AsyncImagePipelineManager> GetAsyncImagePipelineManager() const;
+
+ PCompositorWidgetParent* AllocPCompositorWidgetParent(
+ const CompositorWidgetInitData& aInitData) override;
+ bool DeallocPCompositorWidgetParent(PCompositorWidgetParent* aActor) override;
+
+ void ObserveLayersUpdate(LayersId aLayersId, LayersObserverEpoch aEpoch,
+ bool aActive) override {}
+
+ /**
+ * This forces the is-first-paint flag to true. This is intended to
+ * be called by the widget code when it loses its viewport information
+ * (or for whatever reason wants to refresh the viewport information).
+ * The information refresh happens because the compositor will call
+ * SetFirstPaintViewport on the next frame of composition.
+ */
+ void ForceIsFirstPaint();
+
+ void NotifyChildCreated(LayersId aChild);
+
+ void AsyncRender();
+
+ // Can be called from any thread
+ void ScheduleRenderOnCompositorThread(wr::RenderReasons aReasons) override;
+
+ void ScheduleComposition(wr::RenderReasons aReasons);
+
+ static void ScheduleForcedComposition(const LayersId& aLayersId,
+ wr::RenderReasons aReasons);
+
+ /**
+ * Returns the unique layer tree identifier that corresponds to the root
+ * tree of this compositor.
+ */
+ LayersId RootLayerTreeId();
+
+ /**
+ * Initialize statics.
+ */
+ static void InitializeStatics();
+
+ /**
+ * Notify the compositor for the given layer tree that vsync has occurred.
+ */
+ static void NotifyVsync(const VsyncEvent& aVsync, const LayersId& aLayersId);
+
+ /**
+ * Set aController as the pan/zoom callback for the subtree referred
+ * to by aLayersId.
+ *
+ * Must run on content main thread.
+ */
+ static void SetControllerForLayerTree(LayersId aLayersId,
+ GeckoContentController* aController);
+
+ struct LayerTreeState {
+ LayerTreeState();
+ ~LayerTreeState();
+ RefPtr<GeckoContentController> mController;
+ APZCTreeManagerParent* mApzcTreeManagerParent;
+ RefPtr<CompositorBridgeParent> mParent;
+ RefPtr<WebRenderBridgeParent> mWrBridge;
+ // Pointer to the ContentCompositorBridgeParent. Used by APZCs to share
+ // their FrameMetrics with the corresponding child process that holds
+ // the PCompositorBridgeChild
+ ContentCompositorBridgeParent* mContentCompositorBridgeParent;
+
+ CompositorController* GetCompositorController() const;
+ RefPtr<UiCompositorControllerParent> mUiControllerParent;
+ };
+
+ /**
+ * Lookup the indirect shadow tree for |aId| and return it if it
+ * exists. Otherwise null is returned. This must only be called on
+ * the compositor thread.
+ */
+ static LayerTreeState* GetIndirectShadowTree(LayersId aId);
+
+ /**
+ * Lookup the indirect shadow tree for |aId|, call the function object and
+ * return true if found. If not found, return false.
+ */
+ static bool CallWithIndirectShadowTree(
+ LayersId aId, const std::function<void(LayerTreeState&)>& aFunc);
+
+ /**
+ * Given the layers id for a content process, get the APZCTreeManagerParent
+ * for the corresponding *root* layers id. That is, the APZCTreeManagerParent,
+ * if one is found, will always be connected to the parent process rather
+ * than a content process. Note that unless the compositor process is
+ * separated this is expected to return null, because if the compositor is
+ * living in the gecko parent process then there is no APZCTreeManagerParent
+ * for the parent process.
+ */
+ static APZCTreeManagerParent* GetApzcTreeManagerParentForRoot(
+ LayersId aContentLayersId);
+ /**
+ * Same as the GetApzcTreeManagerParentForRoot function, but returns
+ * the GeckoContentController for the parent process.
+ */
+ static GeckoContentController* GetGeckoContentControllerForRoot(
+ LayersId aContentLayersId);
+
+ /**
+ * Used by the profiler to denote when a vsync occured
+ */
+ static void PostInsertVsyncProfilerMarker(mozilla::TimeStamp aVsyncTimestamp);
+
+ widget::CompositorWidget* GetWidget() { return mWidget; }
+
+ PAPZCTreeManagerParent* AllocPAPZCTreeManagerParent(
+ const LayersId& aLayersId) override;
+ bool DeallocPAPZCTreeManagerParent(PAPZCTreeManagerParent* aActor) override;
+
+ // Helper method so that we don't have to expose mApzcTreeManager to
+ // ContentCompositorBridgeParent.
+ void AllocateAPZCTreeManagerParent(
+ const MonitorAutoLock& aProofOfLayerTreeStateLock,
+ const LayersId& aLayersId, LayerTreeState& aLayerTreeStateToUpdate);
+
+ PAPZParent* AllocPAPZParent(const LayersId& aLayersId) override;
+ bool DeallocPAPZParent(PAPZParent* aActor) override;
+
+ RefPtr<APZSampler> GetAPZSampler() const;
+ RefPtr<APZUpdater> GetAPZUpdater() const;
+ RefPtr<OMTASampler> GetOMTASampler() const;
+
+ uint64_t GetInnerWindowId() const { return mInnerWindowId; }
+
+ CompositorOptions GetOptions() const { return mOptions; }
+
+ TimeDuration GetVsyncInterval() const {
+ // the variable is called "rate" but really it's an interval
+ return mVsyncRate;
+ }
+
+ PWebRenderBridgeParent* AllocPWebRenderBridgeParent(
+ const wr::PipelineId& aPipelineId, const LayoutDeviceIntSize& aSize,
+ const WindowKind& aWindowKind) override;
+ bool DeallocPWebRenderBridgeParent(PWebRenderBridgeParent* aActor) override;
+ RefPtr<WebRenderBridgeParent> GetWebRenderBridgeParent() const;
+ Maybe<TimeStamp> GetTestingTimeStamp() const;
+
+ static CompositorBridgeParent* GetCompositorBridgeParentFromLayersId(
+ const LayersId& aLayersId);
+ static RefPtr<CompositorBridgeParent> GetCompositorBridgeParentFromWindowId(
+ const wr::WindowId& aWindowId);
+
+ /**
+ * This returns a reference to the IAPZCTreeManager "controller subinterface"
+ * to which pan/zoom-related events can be sent. The controller subinterface
+ * doesn't expose any sampler-thread APZCTreeManager methods.
+ */
+ static already_AddRefed<IAPZCTreeManager> GetAPZCTreeManager(
+ LayersId aLayersId);
+
+ WebRenderBridgeParent* GetWrBridge() { return mWrBridge; }
+
+ void FlushPendingWrTransactionEventsWithWait();
+
+ private:
+ void Initialize();
+
+ /**
+ * Called during destruction in order to release resources as early as
+ * possible.
+ */
+ void StopAndClearResources();
+
+ /**
+ * Release compositor-thread resources referred to by |aID|.
+ *
+ * Must run on the content main thread.
+ */
+ static void DeallocateLayerTreeId(LayersId aId);
+
+ /**
+ * Notify the compositor the quality settings have been updated.
+ */
+ static void UpdateQualitySettings();
+
+ /**
+ * Notify the compositor the debug flags have been updated.
+ */
+ static void UpdateDebugFlags();
+
+ /**
+ * Notify the compositor some webrender parameters have been updated.
+ */
+ static void UpdateWebRenderParameters();
+
+ /**
+ * Notify the compositor some webrender parameters have been updated.
+ */
+ static void UpdateWebRenderBoolParameters();
+
+ /**
+ * Notify the compositor webrender profiler UI string has been updated.
+ */
+ static void UpdateWebRenderProfilerUI();
+
+ static void ResetStable();
+
+ void MaybeDeclareStable();
+
+ protected:
+ // Protected destructor, to discourage deletion outside of Release():
+ virtual ~CompositorBridgeParent();
+
+ void DeferredDestroy();
+
+ void SetEGLSurfaceRect(int x, int y, int width, int height);
+
+ public:
+ void PauseComposition();
+ bool ResumeComposition();
+ bool ResumeCompositionAndResize(int x, int y, int width, int height);
+ bool IsPaused() { return mPaused; }
+
+ protected:
+ /**
+ * Add a compositor to the global compositor map.
+ */
+ static void AddCompositor(CompositorBridgeParent* compositor, uint64_t* id);
+ /**
+ * Remove a compositor from the global compositor map.
+ */
+ static CompositorBridgeParent* RemoveCompositor(uint64_t id);
+
+ /**
+ * Creates the global compositor map.
+ */
+ static void Setup();
+
+ /**
+ * Remaning cleanups after the compositore thread is gone.
+ */
+ static void FinishShutdown();
+
+ // The indirect layer tree lock must be held before calling this function.
+ // Callback should take (LayerTreeState* aState, const LayersId& aLayersId)
+ template <typename Lambda>
+ inline void ForEachIndirectLayerTree(const Lambda& aCallback);
+
+ // The indirect layer tree lock must be held before calling this function.
+ // Callback should take (LayerTreeState* aState, const LayersId& aLayersId)
+ template <typename Lambda>
+ static inline void ForEachWebRenderBridgeParent(const Lambda& aCallback);
+
+ static bool sStable;
+ static uint32_t sFramesComposited;
+
+ RefPtr<AsyncImagePipelineManager> mAsyncImageManager;
+ RefPtr<WebRenderBridgeParent> mWrBridge;
+ widget::CompositorWidget* mWidget;
+ Maybe<TimeStamp> mTestTime;
+ CSSToLayoutDeviceScale mScale;
+ TimeDuration mVsyncRate;
+
+ bool mPaused;
+ bool mHaveCompositionRecorder;
+ bool mIsForcedFirstPaint;
+
+ bool mUseExternalSurfaceSize;
+ gfx::IntSize mEGLSurfaceSize;
+
+ CompositorOptions mOptions;
+
+ uint64_t mCompositorBridgeID;
+ LayersId mRootLayerTreeID;
+
+ RefPtr<APZCTreeManager> mApzcTreeManager;
+ RefPtr<APZSampler> mApzSampler;
+ RefPtr<APZUpdater> mApzUpdater;
+ RefPtr<OMTASampler> mOMTASampler;
+
+ // Store the inner window id of the browser window, to use it in
+ // profiler markers.
+ uint64_t mInnerWindowId;
+
+ RefPtr<CompositorVsyncScheduler> mCompositorScheduler;
+ // This makes sure the compositorParent is not destroyed before receiving
+ // confirmation that the channel is closed.
+ // mSelfRef is cleared in DeferredDestroy which is scheduled by ActorDestroy.
+ RefPtr<CompositorBridgeParent> mSelfRef;
+ RefPtr<CompositorAnimationStorage> mAnimationStorage;
+
+ DISALLOW_EVIL_CONSTRUCTORS(CompositorBridgeParent);
+};
+
+int32_t RecordContentFrameTime(
+ const VsyncId& aTxnId, const TimeStamp& aVsyncStart,
+ const TimeStamp& aTxnStart, const VsyncId& aCompositeId,
+ const TimeStamp& aCompositeEnd, const TimeDuration& aFullPaintTime,
+ const TimeDuration& aVsyncRate, bool aContainsSVGGroup,
+ bool aRecordUploadStats, wr::RendererStats* aStats = nullptr);
+
+void RecordCompositionPayloadsPresented(
+ const TimeStamp& aCompositionEndTime,
+ const nsTArray<CompositionPayload>& aPayloads);
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_CompositorBridgeParent_h
diff --git a/gfx/layers/ipc/CompositorManagerChild.cpp b/gfx/layers/ipc/CompositorManagerChild.cpp
new file mode 100644
index 0000000000..89e72ddc01
--- /dev/null
+++ b/gfx/layers/ipc/CompositorManagerChild.cpp
@@ -0,0 +1,255 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/layers/CompositorManagerChild.h"
+
+#include "mozilla/StaticPrefs_layers.h"
+#include "mozilla/layers/CompositorBridgeChild.h"
+#include "mozilla/layers/CompositorManagerParent.h"
+#include "mozilla/layers/CompositorThread.h"
+#include "mozilla/gfx/gfxVars.h"
+#include "mozilla/gfx/GPUProcessManager.h"
+#include "mozilla/dom/ContentChild.h" // for ContentChild
+#include "mozilla/dom/BrowserChild.h" // for BrowserChild
+#include "mozilla/ipc/Endpoint.h"
+#include "VsyncSource.h"
+
+namespace mozilla {
+namespace layers {
+
+using gfx::GPUProcessManager;
+
+StaticRefPtr<CompositorManagerChild> CompositorManagerChild::sInstance;
+Atomic<base::ProcessId> CompositorManagerChild::sOtherPid(0);
+
+/* static */
+bool CompositorManagerChild::IsInitialized(uint64_t aProcessToken) {
+ MOZ_ASSERT(NS_IsMainThread());
+ return sInstance && sInstance->CanSend() &&
+ sInstance->mProcessToken == aProcessToken;
+}
+
+/* static */
+void CompositorManagerChild::InitSameProcess(uint32_t aNamespace,
+ uint64_t aProcessToken) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (NS_WARN_IF(IsInitialized(aProcessToken))) {
+ MOZ_ASSERT_UNREACHABLE("Already initialized same process");
+ return;
+ }
+
+ RefPtr<CompositorManagerParent> parent =
+ CompositorManagerParent::CreateSameProcess();
+ RefPtr<CompositorManagerChild> child =
+ new CompositorManagerChild(parent, aProcessToken, aNamespace);
+ if (NS_WARN_IF(!child->CanSend())) {
+ MOZ_DIAGNOSTIC_ASSERT(false, "Failed to open same process protocol");
+ return;
+ }
+
+ parent->BindComplete(/* aIsRoot */ true);
+ sInstance = std::move(child);
+ sOtherPid = sInstance->OtherPid();
+}
+
+/* static */
+bool CompositorManagerChild::Init(Endpoint<PCompositorManagerChild>&& aEndpoint,
+ uint32_t aNamespace,
+ uint64_t aProcessToken /* = 0 */) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (sInstance) {
+ MOZ_ASSERT(sInstance->mNamespace != aNamespace);
+ }
+
+ sInstance = new CompositorManagerChild(std::move(aEndpoint), aProcessToken,
+ aNamespace);
+ sOtherPid = sInstance->OtherPid();
+ return sInstance->CanSend();
+}
+
+/* static */
+void CompositorManagerChild::Shutdown() {
+ MOZ_ASSERT(NS_IsMainThread());
+ CompositorBridgeChild::ShutDown();
+
+ if (!sInstance) {
+ return;
+ }
+
+ sInstance->Close();
+ sInstance = nullptr;
+ sOtherPid = 0;
+}
+
+/* static */
+void CompositorManagerChild::OnGPUProcessLost(uint64_t aProcessToken) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // Since GPUChild and CompositorManagerChild will race on ActorDestroy, we
+ // cannot know if the CompositorManagerChild is about to be released but has
+ // yet to be. As such, we want to pre-emptively set mCanSend to false.
+ if (sInstance && sInstance->mProcessToken == aProcessToken) {
+ sInstance->mCanSend = false;
+ sOtherPid = 0;
+ }
+}
+
+/* static */
+bool CompositorManagerChild::CreateContentCompositorBridge(
+ uint32_t aNamespace) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (NS_WARN_IF(!sInstance || !sInstance->CanSend())) {
+ return false;
+ }
+
+ CompositorBridgeOptions options = ContentCompositorOptions();
+
+ RefPtr<CompositorBridgeChild> bridge = new CompositorBridgeChild(sInstance);
+ if (NS_WARN_IF(
+ !sInstance->SendPCompositorBridgeConstructor(bridge, options))) {
+ return false;
+ }
+
+ bridge->InitForContent(aNamespace);
+ return true;
+}
+
+/* static */
+already_AddRefed<CompositorBridgeChild>
+CompositorManagerChild::CreateWidgetCompositorBridge(
+ uint64_t aProcessToken, WebRenderLayerManager* aLayerManager,
+ uint32_t aNamespace, CSSToLayoutDeviceScale aScale,
+ const CompositorOptions& aOptions, bool aUseExternalSurfaceSize,
+ const gfx::IntSize& aSurfaceSize, uint64_t aInnerWindowId) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ MOZ_ASSERT(NS_IsMainThread());
+ if (NS_WARN_IF(!sInstance || !sInstance->CanSend())) {
+ return nullptr;
+ }
+
+ TimeDuration vsyncRate =
+ gfxPlatform::GetPlatform()->GetGlobalVsyncDispatcher()->GetVsyncRate();
+
+ CompositorBridgeOptions options = WidgetCompositorOptions(
+ aScale, vsyncRate, aOptions, aUseExternalSurfaceSize, aSurfaceSize,
+ aInnerWindowId);
+
+ RefPtr<CompositorBridgeChild> bridge = new CompositorBridgeChild(sInstance);
+ if (NS_WARN_IF(
+ !sInstance->SendPCompositorBridgeConstructor(bridge, options))) {
+ return nullptr;
+ }
+
+ bridge->InitForWidget(aProcessToken, aLayerManager, aNamespace);
+ return bridge.forget();
+}
+
+/* static */
+already_AddRefed<CompositorBridgeChild>
+CompositorManagerChild::CreateSameProcessWidgetCompositorBridge(
+ WebRenderLayerManager* aLayerManager, uint32_t aNamespace) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ MOZ_ASSERT(NS_IsMainThread());
+ if (NS_WARN_IF(!sInstance || !sInstance->CanSend())) {
+ return nullptr;
+ }
+
+ CompositorBridgeOptions options = SameProcessWidgetCompositorOptions();
+
+ RefPtr<CompositorBridgeChild> bridge = new CompositorBridgeChild(sInstance);
+ if (NS_WARN_IF(
+ !sInstance->SendPCompositorBridgeConstructor(bridge, options))) {
+ return nullptr;
+ }
+
+ bridge->InitForWidget(1, aLayerManager, aNamespace);
+ return bridge.forget();
+}
+
+CompositorManagerChild::CompositorManagerChild(CompositorManagerParent* aParent,
+ uint64_t aProcessToken,
+ uint32_t aNamespace)
+ : mProcessToken(aProcessToken),
+ mNamespace(aNamespace),
+ mResourceId(0),
+ mCanSend(false),
+ mSameProcess(true) {
+ MOZ_ASSERT(aParent);
+
+ SetOtherProcessId(base::GetCurrentProcId());
+ if (NS_WARN_IF(!Open(aParent, CompositorThread(), ipc::ChildSide))) {
+ return;
+ }
+
+ mCanSend = true;
+ SetReplyTimeout();
+}
+
+CompositorManagerChild::CompositorManagerChild(
+ Endpoint<PCompositorManagerChild>&& aEndpoint, uint64_t aProcessToken,
+ uint32_t aNamespace)
+ : mProcessToken(aProcessToken),
+ mNamespace(aNamespace),
+ mResourceId(0),
+ mCanSend(false),
+ mSameProcess(false) {
+ if (NS_WARN_IF(!aEndpoint.Bind(this))) {
+ return;
+ }
+
+ mCanSend = true;
+ SetReplyTimeout();
+}
+
+void CompositorManagerChild::ActorDestroy(ActorDestroyReason aReason) {
+ mCanSend = false;
+ if (sInstance == this) {
+ sInstance = nullptr;
+ }
+}
+
+void CompositorManagerChild::HandleFatalError(const char* aMsg) {
+ dom::ContentChild::FatalErrorIfNotUsingGPUProcess(aMsg, OtherPid());
+}
+
+void CompositorManagerChild::ProcessingError(Result aCode,
+ const char* aReason) {
+ if (aCode != MsgDropped) {
+ gfxDevCrash(gfx::LogReason::ProcessingError)
+ << "Processing error in CompositorBridgeChild: " << int(aCode);
+ }
+}
+
+void CompositorManagerChild::SetReplyTimeout() {
+#ifndef DEBUG
+ // Add a timeout for release builds to kill GPU process when it hangs.
+ if (XRE_IsParentProcess() && GPUProcessManager::Get()->GetGPUChild()) {
+ int32_t timeout =
+ StaticPrefs::layers_gpu_process_ipc_reply_timeout_ms_AtStartup();
+ SetReplyTimeoutMs(timeout);
+ }
+#endif
+}
+
+bool CompositorManagerChild::ShouldContinueFromReplyTimeout() {
+ if (XRE_IsParentProcess()) {
+ gfxCriticalNote << "Killing GPU process due to IPC reply timeout";
+ MOZ_DIAGNOSTIC_ASSERT(GPUProcessManager::Get()->GetGPUChild());
+ GPUProcessManager::Get()->KillProcess();
+ }
+ return false;
+}
+
+mozilla::ipc::IPCResult CompositorManagerChild::RecvNotifyWebRenderError(
+ const WebRenderError&& aError) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ MOZ_ASSERT(NS_IsMainThread());
+ GPUProcessManager::Get()->NotifyWebRenderError(aError);
+ return IPC_OK();
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/ipc/CompositorManagerChild.h b/gfx/layers/ipc/CompositorManagerChild.h
new file mode 100644
index 0000000000..f92ebde460
--- /dev/null
+++ b/gfx/layers/ipc/CompositorManagerChild.h
@@ -0,0 +1,119 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_GFX_COMPOSITORMANAGERCHILD_H
+#define MOZILLA_GFX_COMPOSITORMANAGERCHILD_H
+
+#include <stddef.h> // for size_t
+#include <stdint.h> // for uint32_t, uint64_t
+#include "mozilla/Atomics.h"
+#include "mozilla/Attributes.h" // for override
+#include "mozilla/RefPtr.h" // for already_AddRefed
+#include "mozilla/StaticPtr.h" // for StaticRefPtr
+#include "mozilla/layers/PCompositorManagerChild.h"
+
+namespace mozilla {
+namespace layers {
+
+class CompositorBridgeChild;
+class CompositorManagerParent;
+class WebRenderLayerManager;
+
+class CompositorManagerChild : public PCompositorManagerChild {
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(CompositorManagerChild, override)
+
+ public:
+ static bool IsInitialized(uint64_t aProcessToken);
+ static void InitSameProcess(uint32_t aNamespace, uint64_t aProcessToken);
+ static bool Init(Endpoint<PCompositorManagerChild>&& aEndpoint,
+ uint32_t aNamespace, uint64_t aProcessToken = 0);
+ static void Shutdown();
+ static void OnGPUProcessLost(uint64_t aProcessToken);
+
+ static bool CreateContentCompositorBridge(uint32_t aNamespace);
+
+ static already_AddRefed<CompositorBridgeChild> CreateWidgetCompositorBridge(
+ uint64_t aProcessToken, WebRenderLayerManager* aLayerManager,
+ uint32_t aNamespace, CSSToLayoutDeviceScale aScale,
+ const CompositorOptions& aOptions, bool aUseExternalSurfaceSize,
+ const gfx::IntSize& aSurfaceSize, uint64_t aInnerWindowId);
+
+ static already_AddRefed<CompositorBridgeChild>
+ CreateSameProcessWidgetCompositorBridge(WebRenderLayerManager* aLayerManager,
+ uint32_t aNamespace);
+
+ static CompositorManagerChild* GetInstance() {
+ MOZ_ASSERT(NS_IsMainThread());
+ return sInstance;
+ }
+
+ // Threadsafe way to get the compositor process ID.
+ static base::ProcessId GetOtherPid() { return sOtherPid; }
+
+ bool CanSend() const {
+ MOZ_ASSERT(NS_IsMainThread());
+ return mCanSend;
+ }
+
+ bool SameProcess() const {
+ MOZ_ASSERT(NS_IsMainThread());
+ return mSameProcess;
+ }
+
+ uint32_t GetNextResourceId() {
+ MOZ_ASSERT(NS_IsMainThread());
+ return ++mResourceId;
+ }
+
+ uint32_t GetNamespace() const { return mNamespace; }
+
+ bool OwnsExternalImageId(const wr::ExternalImageId& aId) const {
+ return mNamespace == static_cast<uint32_t>(wr::AsUint64(aId) >> 32);
+ }
+
+ wr::ExternalImageId GetNextExternalImageId() {
+ uint64_t id = GetNextResourceId();
+ MOZ_RELEASE_ASSERT(id != 0);
+ id |= (static_cast<uint64_t>(mNamespace) << 32);
+ return wr::ToExternalImageId(id);
+ }
+
+ void ActorDestroy(ActorDestroyReason aReason) override;
+
+ void HandleFatalError(const char* aMsg) override;
+
+ void ProcessingError(Result aCode, const char* aReason) override;
+
+ bool ShouldContinueFromReplyTimeout() override;
+
+ mozilla::ipc::IPCResult RecvNotifyWebRenderError(
+ const WebRenderError&& aError);
+
+ private:
+ static StaticRefPtr<CompositorManagerChild> sInstance;
+ static Atomic<base::ProcessId> sOtherPid;
+
+ CompositorManagerChild(CompositorManagerParent* aParent,
+ uint64_t aProcessToken, uint32_t aNamespace);
+
+ CompositorManagerChild(Endpoint<PCompositorManagerChild>&& aEndpoint,
+ uint64_t aProcessToken, uint32_t aNamespace);
+
+ virtual ~CompositorManagerChild() = default;
+
+ void SetReplyTimeout();
+
+ uint64_t mProcessToken;
+ uint32_t mNamespace;
+ uint32_t mResourceId;
+ bool mCanSend;
+ bool mSameProcess;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif
diff --git a/gfx/layers/ipc/CompositorManagerParent.cpp b/gfx/layers/ipc/CompositorManagerParent.cpp
new file mode 100644
index 0000000000..d0d7bb862b
--- /dev/null
+++ b/gfx/layers/ipc/CompositorManagerParent.cpp
@@ -0,0 +1,338 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/layers/CompositorManagerParent.h"
+#include "mozilla/gfx/GPUParent.h"
+#include "mozilla/gfx/CanvasManagerParent.h"
+#include "mozilla/webrender/RenderThread.h"
+#include "mozilla/ipc/Endpoint.h"
+#include "mozilla/layers/CompositorBridgeParent.h"
+#include "mozilla/layers/ContentCompositorBridgeParent.h"
+#include "mozilla/layers/CompositorThread.h"
+#include "mozilla/layers/SharedSurfacesParent.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/Unused.h"
+#include "gfxPlatform.h"
+#include "VsyncSource.h"
+
+namespace mozilla {
+namespace layers {
+
+StaticRefPtr<CompositorManagerParent> CompositorManagerParent::sInstance;
+StaticMutex CompositorManagerParent::sMutex;
+
+#ifdef COMPOSITOR_MANAGER_PARENT_EXPLICIT_SHUTDOWN
+StaticAutoPtr<nsTArray<CompositorManagerParent*>>
+ CompositorManagerParent::sActiveActors;
+#endif
+
+/* static */
+already_AddRefed<CompositorManagerParent>
+CompositorManagerParent::CreateSameProcess() {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ MOZ_ASSERT(NS_IsMainThread());
+ StaticMutexAutoLock lock(sMutex);
+
+ // We are creating a manager for the UI process, inside the combined GPU/UI
+ // process. It is created more-or-less the same but we retain a reference to
+ // the parent to access state.
+ if (NS_WARN_IF(sInstance)) {
+ MOZ_ASSERT_UNREACHABLE("Already initialized");
+ return nullptr;
+ }
+
+ // The child is responsible for setting up the IPC channel in the same
+ // process case because if we open from the child perspective, we can do it
+ // on the main thread and complete before we return the manager handles.
+ RefPtr<CompositorManagerParent> parent =
+ new CompositorManagerParent(dom::ContentParentId());
+ parent->SetOtherProcessId(base::GetCurrentProcId());
+ return parent.forget();
+}
+
+/* static */
+bool CompositorManagerParent::Create(
+ Endpoint<PCompositorManagerParent>&& aEndpoint,
+ dom::ContentParentId aChildId, bool aIsRoot) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // We are creating a manager for the another process, inside the GPU process
+ // (or UI process if it subsumbed the GPU process).
+ MOZ_ASSERT(aEndpoint.OtherPid() != base::GetCurrentProcId());
+
+ if (!CompositorThreadHolder::IsActive()) {
+ return false;
+ }
+
+ RefPtr<CompositorManagerParent> bridge =
+ new CompositorManagerParent(aChildId);
+
+ RefPtr<Runnable> runnable =
+ NewRunnableMethod<Endpoint<PCompositorManagerParent>&&, bool>(
+ "CompositorManagerParent::Bind", bridge,
+ &CompositorManagerParent::Bind, std::move(aEndpoint), aIsRoot);
+ CompositorThread()->Dispatch(runnable.forget());
+ return true;
+}
+
+/* static */
+already_AddRefed<CompositorBridgeParent>
+CompositorManagerParent::CreateSameProcessWidgetCompositorBridge(
+ CSSToLayoutDeviceScale aScale, const CompositorOptions& aOptions,
+ bool aUseExternalSurfaceSize, const gfx::IntSize& aSurfaceSize,
+ uint64_t aInnerWindowId) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // When we are in a combined UI / GPU process, InProcessCompositorSession
+ // requires both the parent and child PCompositorBridge actors for its own
+ // construction, which is done on the main thread. Normally
+ // CompositorBridgeParent is created on the compositor thread via the IPDL
+ // plumbing (CompositorManagerParent::AllocPCompositorBridgeParent). Thus to
+ // actually get a reference to the parent, we would need to block on the
+ // compositor thread until it handles our constructor message. Because only
+ // one one IPDL constructor is permitted per parent and child protocol, we
+ // cannot make the normal case async and this case sync. Instead what we do
+ // is leave the constructor async (a boon to the content process setup) and
+ // create the parent ahead of time. It will pull the preinitialized parent
+ // from the queue when it receives the message and give that to IPDL.
+
+ // Note that the static mutex not only is used to protect sInstance, but also
+ // mPendingCompositorBridges.
+ StaticMutexAutoLock lock(sMutex);
+ if (NS_WARN_IF(!sInstance)) {
+ return nullptr;
+ }
+
+ TimeDuration vsyncRate =
+ gfxPlatform::GetPlatform()->GetGlobalVsyncDispatcher()->GetVsyncRate();
+
+ RefPtr<CompositorBridgeParent> bridge = new CompositorBridgeParent(
+ sInstance, aScale, vsyncRate, aOptions, aUseExternalSurfaceSize,
+ aSurfaceSize, aInnerWindowId);
+
+ sInstance->mPendingCompositorBridges.AppendElement(bridge);
+ return bridge.forget();
+}
+
+CompositorManagerParent::CompositorManagerParent(
+ dom::ContentParentId aContentId)
+ : mContentId(aContentId),
+ mCompositorThreadHolder(CompositorThreadHolder::GetSingleton()) {}
+
+CompositorManagerParent::~CompositorManagerParent() = default;
+
+void CompositorManagerParent::Bind(
+ Endpoint<PCompositorManagerParent>&& aEndpoint, bool aIsRoot) {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+ if (NS_WARN_IF(!aEndpoint.Bind(this))) {
+ return;
+ }
+
+ BindComplete(aIsRoot);
+}
+
+void CompositorManagerParent::BindComplete(bool aIsRoot) {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread() ||
+ NS_IsMainThread());
+
+ StaticMutexAutoLock lock(sMutex);
+ if (aIsRoot) {
+ sInstance = this;
+ }
+
+#ifdef COMPOSITOR_MANAGER_PARENT_EXPLICIT_SHUTDOWN
+ if (!sActiveActors) {
+ sActiveActors = new nsTArray<CompositorManagerParent*>();
+ }
+ sActiveActors->AppendElement(this);
+#endif
+}
+
+void CompositorManagerParent::ActorDestroy(ActorDestroyReason aReason) {
+ SharedSurfacesParent::DestroyProcess(OtherPid());
+
+ GetCurrentSerialEventTarget()->Dispatch(
+ NewRunnableMethod("layers::CompositorManagerParent::DeferredDestroy",
+ this, &CompositorManagerParent::DeferredDestroy));
+
+ StaticMutexAutoLock lock(sMutex);
+ if (sInstance == this) {
+ sInstance = nullptr;
+ }
+
+#ifdef COMPOSITOR_MANAGER_PARENT_EXPLICIT_SHUTDOWN
+ if (sActiveActors) {
+ sActiveActors->RemoveElement(this);
+ }
+#endif
+}
+
+void CompositorManagerParent::DeferredDestroy() {
+ mCompositorThreadHolder = nullptr;
+}
+
+#ifdef COMPOSITOR_MANAGER_PARENT_EXPLICIT_SHUTDOWN
+/* static */
+void CompositorManagerParent::ShutdownInternal() {
+ UniquePtr<nsTArray<CompositorManagerParent*>> actors;
+
+ // We move here because we may attempt to acquire the same lock during the
+ // destroy to remove the reference in sActiveActors.
+ {
+ StaticMutexAutoLock lock(sMutex);
+ actors = WrapUnique(sActiveActors.forget());
+ }
+
+ if (actors) {
+ for (auto& actor : *actors) {
+ actor->Close();
+ }
+ }
+}
+#endif // COMPOSITOR_MANAGER_PARENT_EXPLICIT_SHUTDOWN
+
+/* static */
+void CompositorManagerParent::Shutdown() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+#ifdef COMPOSITOR_MANAGER_PARENT_EXPLICIT_SHUTDOWN
+ CompositorThread()->Dispatch(NS_NewRunnableFunction(
+ "layers::CompositorManagerParent::Shutdown",
+ []() -> void { CompositorManagerParent::ShutdownInternal(); }));
+#endif
+}
+
+already_AddRefed<PCompositorBridgeParent>
+CompositorManagerParent::AllocPCompositorBridgeParent(
+ const CompositorBridgeOptions& aOpt) {
+ switch (aOpt.type()) {
+ case CompositorBridgeOptions::TContentCompositorOptions: {
+ RefPtr<ContentCompositorBridgeParent> bridge =
+ new ContentCompositorBridgeParent(this);
+ return bridge.forget();
+ }
+ case CompositorBridgeOptions::TWidgetCompositorOptions: {
+ // Only the UI process is allowed to create widget compositors in the
+ // compositor process.
+ gfx::GPUParent* gpu = gfx::GPUParent::GetSingleton();
+ if (NS_WARN_IF(!gpu || OtherPid() != gpu->OtherPid())) {
+ MOZ_ASSERT_UNREACHABLE("Child cannot create widget compositor!");
+ break;
+ }
+
+ const WidgetCompositorOptions& opt = aOpt.get_WidgetCompositorOptions();
+ RefPtr<CompositorBridgeParent> bridge = new CompositorBridgeParent(
+ this, opt.scale(), opt.vsyncRate(), opt.options(),
+ opt.useExternalSurfaceSize(), opt.surfaceSize(), opt.innerWindowId());
+ return bridge.forget();
+ }
+ case CompositorBridgeOptions::TSameProcessWidgetCompositorOptions: {
+ // If the GPU and UI process are combined, we actually already created the
+ // CompositorBridgeParent, so we need to reuse that to inject it into the
+ // IPDL framework.
+ if (NS_WARN_IF(OtherPid() != base::GetCurrentProcId())) {
+ MOZ_ASSERT_UNREACHABLE("Child cannot create same process compositor!");
+ break;
+ }
+
+ // Note that the static mutex not only is used to protect sInstance, but
+ // also mPendingCompositorBridges.
+ StaticMutexAutoLock lock(sMutex);
+ if (mPendingCompositorBridges.IsEmpty()) {
+ break;
+ }
+
+ RefPtr<CompositorBridgeParent> bridge = mPendingCompositorBridges[0];
+ mPendingCompositorBridges.RemoveElementAt(0);
+ return bridge.forget();
+ }
+ default:
+ break;
+ }
+
+ return nullptr;
+}
+
+mozilla::ipc::IPCResult CompositorManagerParent::RecvAddSharedSurface(
+ const wr::ExternalImageId& aId, SurfaceDescriptorShared&& aDesc) {
+ SharedSurfacesParent::Add(aId, std::move(aDesc), OtherPid());
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult CompositorManagerParent::RecvRemoveSharedSurface(
+ const wr::ExternalImageId& aId) {
+ SharedSurfacesParent::Remove(aId);
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult CompositorManagerParent::RecvReportSharedSurfacesMemory(
+ ReportSharedSurfacesMemoryResolver&& aResolver) {
+ SharedSurfacesMemoryReport report;
+ SharedSurfacesParent::AccumulateMemoryReport(OtherPid(), report);
+ aResolver(std::move(report));
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult CompositorManagerParent::RecvNotifyMemoryPressure() {
+ nsTArray<PCompositorBridgeParent*> compositorBridges;
+ ManagedPCompositorBridgeParent(compositorBridges);
+ for (auto bridge : compositorBridges) {
+ static_cast<CompositorBridgeParentBase*>(bridge)->NotifyMemoryPressure();
+ }
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult CompositorManagerParent::RecvReportMemory(
+ ReportMemoryResolver&& aResolver) {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+ MemoryReport aggregate;
+ PodZero(&aggregate);
+
+ // Accumulate RenderBackend usage.
+ nsTArray<PCompositorBridgeParent*> compositorBridges;
+ ManagedPCompositorBridgeParent(compositorBridges);
+ for (auto bridge : compositorBridges) {
+ static_cast<CompositorBridgeParentBase*>(bridge)->AccumulateMemoryReport(
+ &aggregate);
+ }
+
+ // Accumulate Renderer usage asynchronously, and resolve.
+ //
+ // Note that the IPDL machinery requires aResolver to be called on this
+ // thread, so we can't just pass it over to the renderer thread. We use
+ // an intermediate MozPromise instead.
+ wr::RenderThread::AccumulateMemoryReport(aggregate)->Then(
+ CompositorThread(), __func__,
+ [resolver = std::move(aResolver)](MemoryReport aReport) {
+ resolver(aReport);
+ },
+ [](bool) {
+ MOZ_ASSERT_UNREACHABLE("MemoryReport promises are never rejected");
+ });
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult CompositorManagerParent::RecvInitCanvasManager(
+ Endpoint<PCanvasManagerParent>&& aEndpoint) {
+ gfx::CanvasManagerParent::Init(std::move(aEndpoint));
+ return IPC_OK();
+}
+
+/* static */
+void CompositorManagerParent::NotifyWebRenderError(wr::WebRenderError aError) {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+
+ StaticMutexAutoLock lock(sMutex);
+ if (NS_WARN_IF(!sInstance)) {
+ return;
+ }
+ Unused << sInstance->SendNotifyWebRenderError(aError);
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/ipc/CompositorManagerParent.h b/gfx/layers/ipc/CompositorManagerParent.h
new file mode 100644
index 0000000000..1273474649
--- /dev/null
+++ b/gfx/layers/ipc/CompositorManagerParent.h
@@ -0,0 +1,95 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_GFX_COMPOSITORMANAGERPARENT_H
+#define MOZILLA_GFX_COMPOSITORMANAGERPARENT_H
+
+#include <stdint.h> // for uint32_t
+#include "mozilla/Attributes.h" // for override
+#include "mozilla/StaticPtr.h" // for StaticRefPtr
+#include "mozilla/StaticMutex.h" // for StaticMutex
+#include "mozilla/RefPtr.h" // for already_AddRefed
+#include "mozilla/dom/ipc/IdType.h"
+#include "mozilla/layers/PCompositorManagerParent.h"
+#include "nsTArray.h" // for AutoTArray
+
+namespace mozilla {
+namespace layers {
+
+class CompositorBridgeParent;
+class CompositorThreadHolder;
+
+#ifndef DEBUG
+# define COMPOSITOR_MANAGER_PARENT_EXPLICIT_SHUTDOWN
+#endif
+
+class CompositorManagerParent final : public PCompositorManagerParent {
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(CompositorManagerParent, final)
+
+ public:
+ static already_AddRefed<CompositorManagerParent> CreateSameProcess();
+ static bool Create(Endpoint<PCompositorManagerParent>&& aEndpoint,
+ dom::ContentParentId aContentId, bool aIsRoot);
+ static void Shutdown();
+
+ static already_AddRefed<CompositorBridgeParent>
+ CreateSameProcessWidgetCompositorBridge(CSSToLayoutDeviceScale aScale,
+ const CompositorOptions& aOptions,
+ bool aUseExternalSurfaceSize,
+ const gfx::IntSize& aSurfaceSize,
+ uint64_t aInnerWindowId);
+
+ mozilla::ipc::IPCResult RecvAddSharedSurface(const wr::ExternalImageId& aId,
+ SurfaceDescriptorShared&& aDesc);
+ mozilla::ipc::IPCResult RecvRemoveSharedSurface(
+ const wr::ExternalImageId& aId);
+ mozilla::ipc::IPCResult RecvReportSharedSurfacesMemory(
+ ReportSharedSurfacesMemoryResolver&&);
+
+ mozilla::ipc::IPCResult RecvNotifyMemoryPressure();
+
+ mozilla::ipc::IPCResult RecvReportMemory(ReportMemoryResolver&&);
+
+ mozilla::ipc::IPCResult RecvInitCanvasManager(
+ Endpoint<PCanvasManagerParent>&&);
+
+ void BindComplete(bool aIsRoot);
+ void ActorDestroy(ActorDestroyReason aReason) override;
+
+ already_AddRefed<PCompositorBridgeParent> AllocPCompositorBridgeParent(
+ const CompositorBridgeOptions& aOpt);
+
+ static void NotifyWebRenderError(wr::WebRenderError aError);
+
+ const dom::ContentParentId& GetContentId() const { return mContentId; }
+
+ private:
+ static StaticRefPtr<CompositorManagerParent> sInstance;
+ static StaticMutex sMutex MOZ_UNANNOTATED;
+
+#ifdef COMPOSITOR_MANAGER_PARENT_EXPLICIT_SHUTDOWN
+ static StaticAutoPtr<nsTArray<CompositorManagerParent*>> sActiveActors;
+ static void ShutdownInternal();
+#endif
+
+ explicit CompositorManagerParent(dom::ContentParentId aChildId);
+ virtual ~CompositorManagerParent();
+
+ void Bind(Endpoint<PCompositorManagerParent>&& aEndpoint, bool aIsRoot);
+
+ void DeferredDestroy();
+
+ dom::ContentParentId mContentId;
+
+ RefPtr<CompositorThreadHolder> mCompositorThreadHolder;
+
+ AutoTArray<RefPtr<CompositorBridgeParent>, 1> mPendingCompositorBridges;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif
diff --git a/gfx/layers/ipc/CompositorThread.cpp b/gfx/layers/ipc/CompositorThread.cpp
new file mode 100644
index 0000000000..c66de80cfb
--- /dev/null
+++ b/gfx/layers/ipc/CompositorThread.cpp
@@ -0,0 +1,197 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#include "CompositorThread.h"
+
+#include "CompositorBridgeParent.h"
+#include "gfxGradientCache.h"
+#include "MainThreadUtils.h"
+#include "VRManagerParent.h"
+#include "mozilla/BackgroundHangMonitor.h"
+#include "mozilla/SpinEventLoopUntil.h"
+#include "mozilla/gfx/gfxVars.h"
+#include "mozilla/layers/CanvasTranslator.h"
+#include "mozilla/layers/CompositorManagerParent.h"
+#include "mozilla/layers/ImageBridgeParent.h"
+#include "mozilla/layers/VideoBridgeParent.h"
+#include "mozilla/media/MediaSystemResourceService.h"
+#include "nsThread.h"
+#include "nsThreadUtils.h"
+
+namespace mozilla {
+namespace layers {
+
+static StaticRefPtr<CompositorThreadHolder> sCompositorThreadHolder;
+static Atomic<bool> sFinishedCompositorShutDown(false);
+static mozilla::BackgroundHangMonitor* sBackgroundHangMonitor;
+static ProfilerThreadId sProfilerThreadId;
+
+nsISerialEventTarget* CompositorThread() {
+ return sCompositorThreadHolder
+ ? sCompositorThreadHolder->GetCompositorThread()
+ : nullptr;
+}
+
+CompositorThreadHolder* CompositorThreadHolder::GetSingleton() {
+ return sCompositorThreadHolder;
+}
+
+CompositorThreadHolder::CompositorThreadHolder()
+ : mCompositorThread(CreateCompositorThread()) {
+ MOZ_ASSERT(NS_IsMainThread());
+}
+
+CompositorThreadHolder::~CompositorThreadHolder() {
+ sFinishedCompositorShutDown = true;
+}
+
+/* static */ already_AddRefed<nsIThread>
+CompositorThreadHolder::CreateCompositorThread() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ MOZ_ASSERT(!sCompositorThreadHolder,
+ "The compositor thread has already been started!");
+
+ // When the CanvasRenderer thread is disabled, WebGL may be handled on this
+ // thread, requiring a bigger stack size. See: CanvasManagerParent::Init
+ //
+ // This is 4M, which is higher than the default 256K.
+ // Increased with bug 1753349 to accommodate the `chromium/5359` branch of
+ // ANGLE, which has large peak stack usage for some pathological shader
+ // compilations.
+ //
+ // Previously increased to 512K to accommodate Mesa in bug 1753340.
+ //
+ // Previously increased to 320K to avoid a stack overflow in the
+ // Intel Vulkan driver initialization in bug 1716120.
+ //
+ // Note: we only override it if it's limited already.
+ uint32_t stackSize = nsIThreadManager::DEFAULT_STACK_SIZE;
+ if (stackSize) {
+ stackSize =
+ std::max(stackSize, gfx::gfxVars::SupportsThreadsafeGL() &&
+ !gfx::gfxVars::UseCanvasRenderThread()
+ ? 4096U << 10
+ : 512U << 10);
+ }
+
+ nsCOMPtr<nsIThread> compositorThread;
+ nsresult rv = NS_NewNamedThread(
+ "Compositor", getter_AddRefs(compositorThread),
+ NS_NewRunnableFunction(
+ "CompositorThreadHolder::CompositorThreadHolderSetup",
+ []() {
+ sProfilerThreadId = profiler_current_thread_id();
+ sBackgroundHangMonitor = new mozilla::BackgroundHangMonitor(
+ "Compositor",
+ /* Timeout values are powers-of-two to enable us get better
+ data. 128ms is chosen for transient hangs because 8Hz should
+ be the minimally acceptable goal for Compositor
+ responsiveness (normal goal is 60Hz). */
+ 128,
+ /* 2048ms is chosen for permanent hangs because it's longer than
+ * most Compositor hangs seen in the wild, but is short enough
+ * to not miss getting native hang stacks. */
+ 2048);
+ nsCOMPtr<nsIThread> thread = NS_GetCurrentThread();
+ static_cast<nsThread*>(thread.get())->SetUseHangMonitor(true);
+ }),
+ {.stackSize = stackSize});
+
+ if (NS_FAILED(rv)) {
+ return nullptr;
+ }
+
+ CompositorBridgeParent::Setup();
+ ImageBridgeParent::Setup();
+
+ return compositorThread.forget();
+}
+
+void CompositorThreadHolder::Start() {
+ MOZ_ASSERT(NS_IsMainThread(), "Should be on the main Thread!");
+ MOZ_ASSERT(!sCompositorThreadHolder,
+ "The compositor thread has already been started!");
+
+ // We unset the holder instead of asserting because failing to start the
+ // compositor thread may not be a fatal error. As long as this succeeds in
+ // either the GPU process or the UI process, the user will have a usable
+ // browser. If we get neither, it will crash as soon as we try to post to the
+ // compositor thread for the first time.
+ sProfilerThreadId = ProfilerThreadId();
+ sCompositorThreadHolder = new CompositorThreadHolder();
+ if (!sCompositorThreadHolder->GetCompositorThread()) {
+ gfxCriticalNote << "Compositor thread not started ("
+ << XRE_IsParentProcess() << ")";
+ sCompositorThreadHolder = nullptr;
+ }
+}
+
+void CompositorThreadHolder::Shutdown() {
+ MOZ_ASSERT(NS_IsMainThread(), "Should be on the main Thread!");
+ if (!sCompositorThreadHolder) {
+ // We've already shutdown or never started.
+ return;
+ }
+
+ ImageBridgeParent::Shutdown();
+ gfx::VRManagerParent::Shutdown();
+ MediaSystemResourceService::Shutdown();
+ CompositorManagerParent::Shutdown();
+ CanvasTranslator::Shutdown();
+ gfx::gfxGradientCache::Shutdown();
+
+ // Ensure there are no pending tasks that would cause an access to the
+ // thread's HangMonitor. APZ and Canvas can keep a reference to the compositor
+ // thread and may continue to dispatch tasks on it as the system shuts down.
+ CompositorThread()->Dispatch(NS_NewRunnableFunction(
+ "CompositorThreadHolder::Shutdown",
+ [compositorThreadHolder =
+ RefPtr<CompositorThreadHolder>(sCompositorThreadHolder),
+ backgroundHangMonitor = UniquePtr<mozilla::BackgroundHangMonitor>(
+ sBackgroundHangMonitor)]() {
+ VideoBridgeParent::UnregisterExternalImages();
+ nsCOMPtr<nsIThread> thread = NS_GetCurrentThread();
+ static_cast<nsThread*>(thread.get())->SetUseHangMonitor(false);
+ }));
+
+ sCompositorThreadHolder = nullptr;
+ sBackgroundHangMonitor = nullptr;
+
+ SpinEventLoopUntil("CompositorThreadHolder::Shutdown"_ns, [&]() {
+ bool finished = sFinishedCompositorShutDown;
+ return finished;
+ });
+
+ // At this point, the CompositorThreadHolder instance will have been
+ // destroyed, but the compositor thread itself may still be running due to
+ // APZ/Canvas code holding a reference to the underlying
+ // nsIThread/nsISerialEventTarget. Any tasks scheduled to run on the
+ // compositor thread earlier in this function will have been run to
+ // completion.
+ CompositorBridgeParent::FinishShutdown();
+}
+
+/* static */
+bool CompositorThreadHolder::IsInCompositorThread() {
+ if (!CompositorThread()) {
+ return false;
+ }
+ bool in = false;
+ MOZ_ALWAYS_SUCCEEDS(CompositorThread()->IsOnCurrentThread(&in));
+ return in;
+}
+
+/* static */
+ProfilerThreadId CompositorThreadHolder::GetThreadId() {
+ return sCompositorThreadHolder ? sProfilerThreadId : ProfilerThreadId();
+}
+
+} // namespace layers
+} // namespace mozilla
+
+bool NS_IsInCompositorThread() {
+ return mozilla::layers::CompositorThreadHolder::IsInCompositorThread();
+}
diff --git a/gfx/layers/ipc/CompositorThread.h b/gfx/layers/ipc/CompositorThread.h
new file mode 100644
index 0000000000..3a07e21620
--- /dev/null
+++ b/gfx/layers/ipc/CompositorThread.h
@@ -0,0 +1,69 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#ifndef mozilla_layers_CompositorThread_h
+#define mozilla_layers_CompositorThread_h
+
+#include "nsISupportsImpl.h"
+#include "nsIThread.h"
+
+namespace mozilla::baseprofiler {
+class BaseProfilerThreadId;
+}
+using ProfilerThreadId = mozilla::baseprofiler::BaseProfilerThreadId;
+class nsISerialEventTarget;
+class nsIThread;
+
+namespace mozilla {
+namespace layers {
+
+class CompositorThreadHolder final {
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING_WITH_DELETE_ON_MAIN_THREAD(
+ CompositorThreadHolder)
+
+ public:
+ CompositorThreadHolder();
+
+ nsISerialEventTarget* GetCompositorThread() const {
+ return mCompositorThread;
+ }
+
+ static CompositorThreadHolder* GetSingleton();
+
+ static bool IsActive() { return !!GetSingleton(); }
+
+ /**
+ * Creates the compositor thread and the global compositor map.
+ */
+ static void Start();
+
+ /*
+ * Waits for all [CrossProcess]CompositorBridgeParents to shutdown and
+ * releases compositor-thread owned resources.
+ */
+ static void Shutdown();
+
+ // Returns true if the calling thread is the compositor thread.
+ static bool IsInCompositorThread();
+
+ // Thread id to use as a MarkerThreadId option for profiler markers.
+ static ProfilerThreadId GetThreadId();
+
+ private:
+ ~CompositorThreadHolder();
+
+ nsCOMPtr<nsIThread> mCompositorThread;
+
+ static already_AddRefed<nsIThread> CreateCompositorThread();
+
+ friend class CompositorBridgeParent;
+};
+
+nsISerialEventTarget* CompositorThread();
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_CompositorThread_h
diff --git a/gfx/layers/ipc/CompositorVsyncScheduler.cpp b/gfx/layers/ipc/CompositorVsyncScheduler.cpp
new file mode 100644
index 0000000000..6ef8903015
--- /dev/null
+++ b/gfx/layers/ipc/CompositorVsyncScheduler.cpp
@@ -0,0 +1,382 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/layers/CompositorVsyncScheduler.h"
+
+#include <stdio.h> // for fprintf, stdout
+#include <stdint.h> // for uint64_t
+#include "base/task.h" // for CancelableTask, etc
+#include "base/thread.h" // for Thread
+#include "gfxPlatform.h" // for gfxPlatform
+#ifdef MOZ_WIDGET_GTK
+# include "gfxPlatformGtk.h" // for gfxPlatform
+#endif
+#include "mozilla/AutoRestore.h" // for AutoRestore
+#include "mozilla/DebugOnly.h" // for DebugOnly
+#include "mozilla/StaticPrefs_gfx.h"
+#include "mozilla/StaticPrefs_layers.h"
+#include "mozilla/gfx/2D.h" // for DrawTarget
+#include "mozilla/gfx/Point.h" // for IntSize
+#include "mozilla/gfx/Rect.h" // for IntSize
+#include "mozilla/layers/CompositorThread.h"
+#include "mozilla/layers/CompositorVsyncSchedulerOwner.h"
+#include "mozilla/mozalloc.h" // for operator new, etc
+#include "nsCOMPtr.h" // for already_AddRefed
+#include "nsDebug.h" // for NS_ASSERTION, etc
+#include "nsISupportsImpl.h" // for MOZ_COUNT_CTOR, etc
+#include "nsIWidget.h" // for nsIWidget
+#include "nsThreadUtils.h" // for NS_IsMainThread
+#include "mozilla/Telemetry.h"
+#include "mozilla/VsyncDispatcher.h"
+#if defined(XP_WIN) || defined(MOZ_WIDGET_GTK)
+# include "VsyncSource.h"
+#endif
+#include "mozilla/widget/CompositorWidget.h"
+#include "VRManager.h"
+
+namespace mozilla {
+
+namespace layers {
+
+using namespace mozilla::gfx;
+
+CompositorVsyncScheduler::Observer::Observer(CompositorVsyncScheduler* aOwner)
+ : mMutex("CompositorVsyncScheduler.Observer.Mutex"), mOwner(aOwner) {}
+
+CompositorVsyncScheduler::Observer::~Observer() { MOZ_ASSERT(!mOwner); }
+
+void CompositorVsyncScheduler::Observer::NotifyVsync(const VsyncEvent& aVsync) {
+ MutexAutoLock lock(mMutex);
+ if (!mOwner) {
+ return;
+ }
+ mOwner->NotifyVsync(aVsync);
+}
+
+void CompositorVsyncScheduler::Observer::Destroy() {
+ MutexAutoLock lock(mMutex);
+ mOwner = nullptr;
+}
+
+CompositorVsyncScheduler::CompositorVsyncScheduler(
+ CompositorVsyncSchedulerOwner* aVsyncSchedulerOwner,
+ widget::CompositorWidget* aWidget)
+ : mVsyncSchedulerOwner(aVsyncSchedulerOwner),
+ mLastComposeTime(SampleTime::FromNow()),
+ mLastVsyncTime(TimeStamp::Now()),
+ mLastVsyncOutputTime(TimeStamp::Now()),
+ mIsObservingVsync(false),
+ mRendersDelayedByVsyncReasons(wr::RenderReasons::NONE),
+ mVsyncNotificationsSkipped(0),
+ mWidget(aWidget),
+ mCurrentCompositeTaskMonitor("CurrentCompositeTaskMonitor"),
+ mCurrentCompositeTask(nullptr),
+ mCurrentCompositeTaskReasons(wr::RenderReasons::NONE),
+ mCurrentVRTaskMonitor("CurrentVRTaskMonitor"),
+ mCurrentVRTask(nullptr) {
+ mVsyncObserver = new Observer(this);
+
+ // mAsapScheduling is set on the main thread during init,
+ // but is only accessed after on the compositor thread.
+ mAsapScheduling =
+ StaticPrefs::layers_offmainthreadcomposition_frame_rate() == 0 ||
+ gfxPlatform::IsInLayoutAsapMode();
+}
+
+CompositorVsyncScheduler::~CompositorVsyncScheduler() {
+ MOZ_ASSERT(!mIsObservingVsync);
+ MOZ_ASSERT(!mVsyncObserver);
+ // The CompositorVsyncDispatcher is cleaned up before this in the
+ // nsBaseWidget, which stops vsync listeners
+ mVsyncSchedulerOwner = nullptr;
+}
+
+void CompositorVsyncScheduler::Destroy() {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+
+ if (!mVsyncObserver) {
+ // Destroy was already called on this object.
+ return;
+ }
+ UnobserveVsync();
+ mVsyncObserver->Destroy();
+ mVsyncObserver = nullptr;
+
+ mCompositeRequestedAt = TimeStamp();
+ CancelCurrentCompositeTask();
+ CancelCurrentVRTask();
+}
+
+void CompositorVsyncScheduler::PostCompositeTask(const VsyncEvent& aVsyncEvent,
+ wr::RenderReasons aReasons) {
+ MonitorAutoLock lock(mCurrentCompositeTaskMonitor);
+ mCurrentCompositeTaskReasons = mCurrentCompositeTaskReasons | aReasons;
+ if (mCurrentCompositeTask == nullptr && CompositorThread()) {
+ RefPtr<CancelableRunnable> task =
+ NewCancelableRunnableMethod<VsyncEvent, wr::RenderReasons>(
+ "layers::CompositorVsyncScheduler::Composite", this,
+ &CompositorVsyncScheduler::Composite, aVsyncEvent, aReasons);
+ mCurrentCompositeTask = task;
+ CompositorThread()->Dispatch(task.forget());
+ }
+}
+
+void CompositorVsyncScheduler::PostVRTask(TimeStamp aTimestamp) {
+ MonitorAutoLock lockVR(mCurrentVRTaskMonitor);
+ if (mCurrentVRTask == nullptr && CompositorThread()) {
+ RefPtr<CancelableRunnable> task = NewCancelableRunnableMethod<TimeStamp>(
+ "layers::CompositorVsyncScheduler::DispatchVREvents", this,
+ &CompositorVsyncScheduler::DispatchVREvents, aTimestamp);
+ mCurrentVRTask = task;
+ CompositorThread()->Dispatch(task.forget());
+ }
+}
+
+void CompositorVsyncScheduler::ScheduleComposition(wr::RenderReasons aReasons) {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+ if (!mVsyncObserver) {
+ // Destroy was already called on this object.
+ return;
+ }
+
+ // Make a synthetic vsync event for the calls to PostCompositeTask below.
+ TimeStamp vsyncTime = TimeStamp::Now();
+ TimeStamp outputTime = vsyncTime + mVsyncSchedulerOwner->GetVsyncInterval();
+ VsyncEvent vsyncEvent(VsyncId(), vsyncTime, outputTime);
+
+ if (mAsapScheduling) {
+ // Used only for performance testing purposes, and when recording/replaying
+ // to ensure that graphics are up to date.
+ PostCompositeTask(vsyncEvent, aReasons);
+ } else {
+ if (!mCompositeRequestedAt) {
+ mCompositeRequestedAt = TimeStamp::Now();
+ }
+ if (!mIsObservingVsync && mCompositeRequestedAt) {
+ ObserveVsync();
+ // Starting to observe vsync is an async operation that goes
+ // through the main thread of the UI process. It's possible that
+ // we're blocking there waiting on a composite, so schedule an initial
+ // one now to get things started.
+ PostCompositeTask(vsyncEvent,
+ aReasons | wr::RenderReasons::START_OBSERVING_VSYNC);
+ } else {
+ mRendersDelayedByVsyncReasons = aReasons;
+ }
+ }
+}
+
+void CompositorVsyncScheduler::NotifyVsync(const VsyncEvent& aVsync) {
+ // Called from the vsync dispatch thread. When in the GPU Process, that's
+ // the same as the compositor thread.
+#ifdef DEBUG
+# ifdef MOZ_WAYLAND
+ // On Wayland, we dispatch vsync from the main thread, without a GPU process.
+ // To allow this, we skip the following asserts if we're currently utilizing
+ // the Wayland backend. The IsParentProcess guard is needed to ensure that
+ // we don't accidentally attempt to initialize the gfxPlatform in the GPU
+ // process on X11.
+ if (!XRE_IsParentProcess() ||
+ !gfxPlatformGtk::GetPlatform()->IsWaylandDisplay())
+# endif // MOZ_WAYLAND
+ {
+ MOZ_ASSERT_IF(XRE_IsParentProcess(),
+ !CompositorThreadHolder::IsInCompositorThread());
+ MOZ_ASSERT(!NS_IsMainThread());
+ }
+
+ MOZ_ASSERT_IF(XRE_GetProcessType() == GeckoProcessType_GPU,
+ CompositorThreadHolder::IsInCompositorThread());
+#endif // DEBUG
+
+#if defined(MOZ_WIDGET_ANDROID)
+ gfx::VRManager* vm = gfx::VRManager::Get();
+ if (!vm->IsPresenting()) {
+ PostCompositeTask(aVsync, wr::RenderReasons::VSYNC);
+ }
+#else
+ PostCompositeTask(aVsync, wr::RenderReasons::VSYNC);
+#endif // defined(MOZ_WIDGET_ANDROID)
+
+ PostVRTask(aVsync.mTime);
+}
+
+void CompositorVsyncScheduler::CancelCurrentVRTask() {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread() ||
+ NS_IsMainThread());
+ MonitorAutoLock lock(mCurrentVRTaskMonitor);
+ if (mCurrentVRTask) {
+ mCurrentVRTask->Cancel();
+ mCurrentVRTask = nullptr;
+ }
+}
+
+wr::RenderReasons CompositorVsyncScheduler::CancelCurrentCompositeTask() {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread() ||
+ NS_IsMainThread());
+ MonitorAutoLock lock(mCurrentCompositeTaskMonitor);
+ wr::RenderReasons canceledTaskRenderReasons = mCurrentCompositeTaskReasons;
+ mCurrentCompositeTaskReasons = wr::RenderReasons::NONE;
+ if (mCurrentCompositeTask) {
+ mCurrentCompositeTask->Cancel();
+ mCurrentCompositeTask = nullptr;
+ }
+
+ return canceledTaskRenderReasons;
+}
+
+void CompositorVsyncScheduler::Composite(const VsyncEvent& aVsyncEvent,
+ wr::RenderReasons aReasons) {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+ MOZ_ASSERT(mVsyncSchedulerOwner);
+
+ { // scope lock
+ MonitorAutoLock lock(mCurrentCompositeTaskMonitor);
+ aReasons =
+ aReasons | mCurrentCompositeTaskReasons | mRendersDelayedByVsyncReasons;
+ mCurrentCompositeTaskReasons = wr::RenderReasons::NONE;
+ mRendersDelayedByVsyncReasons = wr::RenderReasons::NONE;
+ mCurrentCompositeTask = nullptr;
+ }
+
+ mLastVsyncTime = aVsyncEvent.mTime;
+ mLastVsyncOutputTime = aVsyncEvent.mOutputTime;
+ mLastVsyncId = aVsyncEvent.mId;
+
+ if (!mAsapScheduling) {
+ // Some early exit conditions if we're not in ASAP mode
+ if (aVsyncEvent.mTime < mLastComposeTime.Time()) {
+ // We can sometimes get vsync timestamps that are in the past
+ // compared to the last compose with force composites.
+ // In those cases, wait until the next vsync;
+ return;
+ }
+
+ if (mVsyncSchedulerOwner->IsPendingComposite()) {
+ // If previous composite is still on going, finish it and wait for the
+ // next vsync.
+ mVsyncSchedulerOwner->FinishPendingComposite();
+ return;
+ }
+ }
+
+ if (mCompositeRequestedAt || mAsapScheduling) {
+ mCompositeRequestedAt = TimeStamp();
+ mLastComposeTime = SampleTime::FromVsync(aVsyncEvent.mTime);
+
+ // Tell the owner to do a composite
+ mVsyncSchedulerOwner->CompositeToTarget(aVsyncEvent.mId, aReasons, nullptr,
+ nullptr);
+
+ mVsyncNotificationsSkipped = 0;
+
+ TimeDuration compositeFrameTotal = TimeStamp::Now() - aVsyncEvent.mTime;
+ mozilla::Telemetry::Accumulate(
+ mozilla::Telemetry::COMPOSITE_FRAME_ROUNDTRIP_TIME,
+ compositeFrameTotal.ToMilliseconds());
+ } else if (mVsyncNotificationsSkipped++ >
+ StaticPrefs::gfx_vsync_compositor_unobserve_count_AtStartup()) {
+ UnobserveVsync();
+ }
+}
+
+void CompositorVsyncScheduler::ForceComposeToTarget(wr::RenderReasons aReasons,
+ gfx::DrawTarget* aTarget,
+ const IntRect* aRect) {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+
+ /**
+ * bug 1138502 - There are cases such as during long-running window resizing
+ * events where we receive many force-composites. We also continue to get
+ * vsync notifications. Because the force-composites trigger compositing and
+ * clear the mCompositeRequestedAt timestamp, the vsync notifications will not
+ * need to do anything and so will increment the mVsyncNotificationsSkipped
+ * counter to indicate the vsync was ignored. If this happens enough times, we
+ * will disable listening for vsync entirely. On the next force-composite we
+ * will enable listening for vsync again, and continued force-composites and
+ * vsyncs will cause oscillation between observing vsync and not. On some
+ * platforms, enabling/disabling vsync is not free and this oscillating
+ * behavior causes a performance hit. In order to avoid this problem, we reset
+ * the mVsyncNotificationsSkipped counter to keep vsync enabled.
+ */
+ mVsyncNotificationsSkipped = 0;
+
+ mLastComposeTime = SampleTime::FromNow();
+ MOZ_ASSERT(mVsyncSchedulerOwner);
+ mVsyncSchedulerOwner->CompositeToTarget(VsyncId(), aReasons, aTarget, aRect);
+}
+
+bool CompositorVsyncScheduler::NeedsComposite() {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+ return (bool)mCompositeRequestedAt;
+}
+
+bool CompositorVsyncScheduler::FlushPendingComposite() {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+ if (mCompositeRequestedAt) {
+ wr::RenderReasons reasons = CancelCurrentCompositeTask();
+ ForceComposeToTarget(reasons, nullptr, nullptr);
+ return true;
+ }
+ return false;
+}
+
+void CompositorVsyncScheduler::ObserveVsync() {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+ mWidget->ObserveVsync(mVsyncObserver);
+ mIsObservingVsync = true;
+}
+
+void CompositorVsyncScheduler::UnobserveVsync() {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+ mWidget->ObserveVsync(nullptr);
+ mIsObservingVsync = false;
+}
+
+void CompositorVsyncScheduler::DispatchVREvents(TimeStamp aVsyncTimestamp) {
+ {
+ MonitorAutoLock lock(mCurrentVRTaskMonitor);
+ mCurrentVRTask = nullptr;
+ }
+ // This only allows to be called by CompositorVsyncScheduler::PostVRTask()
+ // When the process is going to shutdown, the runnable has chance to be
+ // executed by other threads, we only want it to be run in the compositor
+ // thread.
+ if (!CompositorThreadHolder::IsInCompositorThread()) {
+ return;
+ }
+
+ VRManager* vm = VRManager::Get();
+ vm->NotifyVsync(aVsyncTimestamp);
+}
+
+const SampleTime& CompositorVsyncScheduler::GetLastComposeTime() const {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+ return mLastComposeTime;
+}
+
+const TimeStamp& CompositorVsyncScheduler::GetLastVsyncTime() const {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+ return mLastVsyncTime;
+}
+
+const TimeStamp& CompositorVsyncScheduler::GetLastVsyncOutputTime() const {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+ return mLastVsyncOutputTime;
+}
+
+const VsyncId& CompositorVsyncScheduler::GetLastVsyncId() const {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+ return mLastVsyncId;
+}
+
+void CompositorVsyncScheduler::UpdateLastComposeTime() {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+ mLastComposeTime = SampleTime::FromNow();
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/ipc/CompositorVsyncScheduler.h b/gfx/layers/ipc/CompositorVsyncScheduler.h
new file mode 100644
index 0000000000..8363450043
--- /dev/null
+++ b/gfx/layers/ipc/CompositorVsyncScheduler.h
@@ -0,0 +1,185 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_CompositorVsyncScheduler_h
+#define mozilla_layers_CompositorVsyncScheduler_h
+
+#include <stdint.h> // for uint64_t
+
+#include "mozilla/Attributes.h" // for override
+#include "mozilla/Monitor.h" // for Monitor
+#include "mozilla/RefPtr.h" // for RefPtr
+#include "mozilla/TimeStamp.h" // for TimeStamp
+#include "mozilla/gfx/Point.h" // for IntSize
+#include "mozilla/layers/SampleTime.h"
+#include "mozilla/webrender/webrender_ffi.h"
+#include "mozilla/VsyncDispatcher.h"
+#include "mozilla/widget/CompositorWidget.h"
+#include "nsISupportsImpl.h"
+
+namespace mozilla {
+
+class CancelableRunnable;
+class Runnable;
+
+namespace gfx {
+class DrawTarget;
+} // namespace gfx
+
+namespace layers {
+
+class CompositorVsyncSchedulerOwner;
+
+/**
+ * Manages the vsync (de)registration and tracking on behalf of the
+ * compositor when it need to paint.
+ * Turns vsync notifications into scheduled composites.
+ **/
+class CompositorVsyncScheduler {
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(CompositorVsyncScheduler)
+
+ public:
+ CompositorVsyncScheduler(CompositorVsyncSchedulerOwner* aVsyncSchedulerOwner,
+ widget::CompositorWidget* aWidget);
+
+ /**
+ * Notify this class of a vsync. This will trigger a composite if one is
+ * needed. This must be called from the vsync dispatch thread.
+ */
+ void NotifyVsync(const VsyncEvent& aVsync);
+
+ /**
+ * Do cleanup. This must be called on the compositor thread.
+ */
+ void Destroy();
+
+ /**
+ * Notify this class that a composition is needed. This will trigger a
+ * composition soon (likely at the next vsync). This must be called on the
+ * compositor thread.
+ */
+ void ScheduleComposition(wr::RenderReasons aReasons);
+
+ /**
+ * Cancel any composite task that has been scheduled but hasn't run yet.
+ *
+ * Returns the render reasons of the canceled task if any.
+ */
+ wr::RenderReasons CancelCurrentCompositeTask();
+
+ /**
+ * Check if a composite is pending. This is generally true between a call
+ * to ScheduleComposition() and the time the composite happens.
+ */
+ bool NeedsComposite();
+
+ /**
+ * Force a composite to happen right away, without waiting for the next vsync.
+ * This must be called on the compositor thread.
+ */
+ void ForceComposeToTarget(wr::RenderReasons aReasons,
+ gfx::DrawTarget* aTarget,
+ const gfx::IntRect* aRect);
+
+ /**
+ * If a composite is pending, force it to trigger right away. This must be
+ * called on the compositor thread. Returns true if there was a composite
+ * flushed.
+ */
+ bool FlushPendingComposite();
+
+ /**
+ * Return the vsync timestamp of the last or ongoing composite. Must be called
+ * on the compositor thread.
+ */
+ const SampleTime& GetLastComposeTime() const;
+
+ /**
+ * Return the vsync timestamp and id of the most recently received
+ * vsync event. Must be called on the compositor thread.
+ */
+ const TimeStamp& GetLastVsyncTime() const;
+ const TimeStamp& GetLastVsyncOutputTime() const;
+ const VsyncId& GetLastVsyncId() const;
+
+ /**
+ * Update LastCompose TimeStamp to current timestamp.
+ * The function is typically used when composition is handled outside the
+ * CompositorVsyncScheduler.
+ */
+ void UpdateLastComposeTime();
+
+ private:
+ virtual ~CompositorVsyncScheduler();
+
+ // Post a task to run Composite() on the compositor thread, if there isn't
+ // such a task already queued. Can be called from any thread.
+ void PostCompositeTask(const VsyncEvent& aVsyncEvent,
+ wr::RenderReasons aReasons);
+
+ // Post a task to run DispatchVREvents() on the VR thread, if there isn't
+ // such a task already queued. Can be called from any thread.
+ void PostVRTask(TimeStamp aTimestamp);
+
+ /**
+ * Cancel any VR task that has been scheduled but hasn't run yet.
+ */
+ void CancelCurrentVRTask();
+
+ // This gets run at vsync time and "does" a composite (which really means
+ // update internal state and call the owner to do the composite).
+ void Composite(const VsyncEvent& aVsyncEvent, wr::RenderReasons aReasons);
+
+ void ObserveVsync();
+ void UnobserveVsync();
+
+ void DispatchVREvents(TimeStamp aVsyncTimestamp);
+
+ class Observer final : public VsyncObserver {
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(CompositorVsyncScheduler::Observer,
+ override)
+
+ public:
+ explicit Observer(CompositorVsyncScheduler* aOwner);
+ void NotifyVsync(const VsyncEvent& aVsync) override;
+ void Destroy();
+
+ private:
+ virtual ~Observer();
+
+ Mutex mMutex MOZ_UNANNOTATED;
+ // Hold raw pointer to avoid mutual reference.
+ CompositorVsyncScheduler* mOwner;
+ };
+
+ CompositorVsyncSchedulerOwner* mVsyncSchedulerOwner;
+ SampleTime mLastComposeTime;
+ TimeStamp mLastVsyncTime;
+ TimeStamp mLastVsyncOutputTime;
+ VsyncId mLastVsyncId;
+
+ bool mAsapScheduling;
+ bool mIsObservingVsync;
+ // Accessed on the compositor thread.
+ wr::RenderReasons mRendersDelayedByVsyncReasons;
+ TimeStamp mCompositeRequestedAt;
+ int32_t mVsyncNotificationsSkipped;
+ widget::CompositorWidget* mWidget;
+ RefPtr<CompositorVsyncScheduler::Observer> mVsyncObserver;
+
+ mozilla::Monitor mCurrentCompositeTaskMonitor MOZ_UNANNOTATED;
+ RefPtr<CancelableRunnable> mCurrentCompositeTask;
+ // Accessed on multiple threads, guarded by mCurrentCompositeTaskMonitor.
+ wr::RenderReasons mCurrentCompositeTaskReasons;
+
+ mozilla::Monitor mCurrentVRTaskMonitor MOZ_UNANNOTATED;
+ RefPtr<CancelableRunnable> mCurrentVRTask;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_CompositorVsyncScheduler_h
diff --git a/gfx/layers/ipc/CompositorVsyncSchedulerOwner.h b/gfx/layers/ipc/CompositorVsyncSchedulerOwner.h
new file mode 100644
index 0000000000..ac147fd241
--- /dev/null
+++ b/gfx/layers/ipc/CompositorVsyncSchedulerOwner.h
@@ -0,0 +1,34 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_CompositorVsyncSchedulerOwner_h
+#define mozilla_layers_CompositorVsyncSchedulerOwner_h
+
+#include "mozilla/VsyncDispatcher.h"
+#include "mozilla/webrender/webrender_ffi.h"
+
+namespace mozilla {
+
+namespace gfx {
+class DrawTarget;
+} // namespace gfx
+
+namespace layers {
+
+class CompositorVsyncSchedulerOwner {
+ public:
+ virtual bool IsPendingComposite() = 0;
+ virtual void FinishPendingComposite() = 0;
+ virtual void CompositeToTarget(VsyncId aId, wr::RenderReasons aReasons,
+ gfx::DrawTarget* aTarget,
+ const gfx::IntRect* aRect = nullptr) = 0;
+ virtual TimeDuration GetVsyncInterval() const = 0;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_CompositorVsyncSchedulerOwner_h
diff --git a/gfx/layers/ipc/ContentCompositorBridgeParent.cpp b/gfx/layers/ipc/ContentCompositorBridgeParent.cpp
new file mode 100644
index 0000000000..352e7b0d63
--- /dev/null
+++ b/gfx/layers/ipc/ContentCompositorBridgeParent.cpp
@@ -0,0 +1,461 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/layers/ContentCompositorBridgeParent.h"
+
+#include <stdint.h> // for uint64_t
+
+#include "apz/src/APZCTreeManager.h" // for APZCTreeManager
+#include "gfxUtils.h"
+#ifdef XP_WIN
+# include "mozilla/gfx/DeviceManagerDx.h" // for DeviceManagerDx
+# include "mozilla/layers/ImageDataSerializer.h"
+#endif
+#include "mozilla/layers/AnimationHelper.h" // for CompositorAnimationStorage
+#include "mozilla/layers/APZCTreeManagerParent.h" // for APZCTreeManagerParent
+#include "mozilla/layers/APZUpdater.h" // for APZUpdater
+#include "mozilla/layers/CompositorManagerParent.h"
+#include "mozilla/layers/CompositorOptions.h"
+#include "mozilla/layers/CompositorThread.h"
+#include "mozilla/layers/LayerTreeOwnerTracker.h"
+#include "mozilla/layers/RemoteContentController.h"
+#include "mozilla/layers/WebRenderBridgeParent.h"
+#include "mozilla/layers/AsyncImagePipelineManager.h"
+#include "mozilla/mozalloc.h" // for operator new, etc
+#include "nsDebug.h" // for NS_ASSERTION, etc
+#include "nsTArray.h" // for nsTArray
+#include "nsXULAppAPI.h" // for XRE_GetIOMessageLoop
+#include "mozilla/Unused.h"
+#include "mozilla/StaticPrefs_dom.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/BaseProfilerMarkerTypes.h"
+#include "GeckoProfiler.h"
+
+namespace mozilla::layers {
+
+// defined in CompositorBridgeParent.cpp
+using LayerTreeMap = std::map<LayersId, CompositorBridgeParent::LayerTreeState>;
+extern LayerTreeMap sIndirectLayerTrees;
+extern StaticAutoPtr<mozilla::Monitor> sIndirectLayerTreesLock;
+void EraseLayerState(LayersId aId);
+
+void ContentCompositorBridgeParent::ActorDestroy(ActorDestroyReason aWhy) {
+ mCanSend = false;
+
+ // We must keep this object alive untill the code handling message
+ // reception is finished on this thread.
+ GetCurrentSerialEventTarget()->Dispatch(NewRunnableMethod(
+ "layers::ContentCompositorBridgeParent::DeferredDestroy", this,
+ &ContentCompositorBridgeParent::DeferredDestroy));
+}
+
+PAPZCTreeManagerParent*
+ContentCompositorBridgeParent::AllocPAPZCTreeManagerParent(
+ const LayersId& aLayersId) {
+ // Check to see if this child process has access to this layer tree.
+ if (!LayerTreeOwnerTracker::Get()->IsMapped(aLayersId, OtherPid())) {
+ NS_ERROR(
+ "Unexpected layers id in AllocPAPZCTreeManagerParent; dropping "
+ "message...");
+ return nullptr;
+ }
+
+ MonitorAutoLock lock(*sIndirectLayerTreesLock);
+ CompositorBridgeParent::LayerTreeState& state =
+ sIndirectLayerTrees[aLayersId];
+
+ // If the widget has shutdown its compositor, we may not have had a chance yet
+ // to unmap our layers id, and we could get here without a parent compositor.
+ // In this case return an empty APZCTM.
+ if (!state.mParent) {
+ // Note: we immediately call ClearTree since otherwise the APZCTM will
+ // retain a reference to itself, through the checkerboard observer.
+ LayersId dummyId{0};
+ const bool connectedToWebRender = false;
+ RefPtr<APZCTreeManager> temp = new APZCTreeManager(dummyId);
+ RefPtr<APZUpdater> tempUpdater = new APZUpdater(temp, connectedToWebRender);
+ tempUpdater->ClearTree(dummyId);
+ return new APZCTreeManagerParent(aLayersId, temp, tempUpdater);
+ }
+
+ // If we do not have APZ enabled, we should gracefully fail.
+ if (!state.mParent->GetOptions().UseAPZ()) {
+ return nullptr;
+ }
+
+ state.mParent->AllocateAPZCTreeManagerParent(lock, aLayersId, state);
+ return state.mApzcTreeManagerParent;
+}
+
+bool ContentCompositorBridgeParent::DeallocPAPZCTreeManagerParent(
+ PAPZCTreeManagerParent* aActor) {
+ APZCTreeManagerParent* parent = static_cast<APZCTreeManagerParent*>(aActor);
+
+ MonitorAutoLock lock(*sIndirectLayerTreesLock);
+ auto iter = sIndirectLayerTrees.find(parent->GetLayersId());
+ if (iter != sIndirectLayerTrees.end()) {
+ CompositorBridgeParent::LayerTreeState& state = iter->second;
+ MOZ_ASSERT(state.mApzcTreeManagerParent == parent);
+ state.mApzcTreeManagerParent = nullptr;
+ }
+
+ delete parent;
+
+ return true;
+}
+
+PAPZParent* ContentCompositorBridgeParent::AllocPAPZParent(
+ const LayersId& aLayersId) {
+ // Check to see if this child process has access to this layer tree.
+ if (!LayerTreeOwnerTracker::Get()->IsMapped(aLayersId, OtherPid())) {
+ NS_ERROR("Unexpected layers id in AllocPAPZParent; dropping message...");
+ return nullptr;
+ }
+
+ RemoteContentController* controller = new RemoteContentController();
+
+ // Increment the controller's refcount before we return it. This will keep the
+ // controller alive until it is released by IPDL in DeallocPAPZParent.
+ controller->AddRef();
+
+ MonitorAutoLock lock(*sIndirectLayerTreesLock);
+ CompositorBridgeParent::LayerTreeState& state =
+ sIndirectLayerTrees[aLayersId];
+ MOZ_ASSERT(!state.mController);
+ state.mController = controller;
+
+ return controller;
+}
+
+bool ContentCompositorBridgeParent::DeallocPAPZParent(PAPZParent* aActor) {
+ RemoteContentController* controller =
+ static_cast<RemoteContentController*>(aActor);
+ controller->Release();
+ return true;
+}
+
+PWebRenderBridgeParent*
+ContentCompositorBridgeParent::AllocPWebRenderBridgeParent(
+ const wr::PipelineId& aPipelineId, const LayoutDeviceIntSize& aSize,
+ const WindowKind& aWindowKind) {
+ LayersId layersId = wr::AsLayersId(aPipelineId);
+ // Check to see if this child process has access to this layer tree.
+ if (!LayerTreeOwnerTracker::Get()->IsMapped(layersId, OtherPid())) {
+ NS_ERROR(
+ "Unexpected layers id in AllocPWebRenderBridgeParent; dropping "
+ "message...");
+ return nullptr;
+ }
+
+ RefPtr<CompositorBridgeParent> cbp = nullptr;
+ RefPtr<WebRenderBridgeParent> root = nullptr;
+
+ { // scope lock
+ MonitorAutoLock lock(*sIndirectLayerTreesLock);
+ MOZ_ASSERT(sIndirectLayerTrees.find(layersId) != sIndirectLayerTrees.end());
+ MOZ_ASSERT(sIndirectLayerTrees[layersId].mWrBridge == nullptr);
+ cbp = sIndirectLayerTrees[layersId].mParent;
+ if (cbp) {
+ root = sIndirectLayerTrees[cbp->RootLayerTreeId()].mWrBridge;
+ }
+ }
+
+ RefPtr<wr::WebRenderAPI> api;
+ if (root) {
+ api = root->GetWebRenderAPI();
+ }
+
+ if (!root || !api) {
+ // This could happen when this function is called after
+ // CompositorBridgeParent destruction. This was observed during Tab move
+ // between different windows.
+ NS_WARNING(
+ nsPrintfCString("Created child without a matching parent? root %p",
+ root.get())
+ .get());
+ nsCString error("NO_PARENT");
+ WebRenderBridgeParent* parent =
+ WebRenderBridgeParent::CreateDestroyed(aPipelineId, std::move(error));
+ parent->AddRef(); // IPDL reference
+ return parent;
+ }
+
+ api = api->Clone();
+ RefPtr<AsyncImagePipelineManager> holder = root->AsyncImageManager();
+ WebRenderBridgeParent* parent = new WebRenderBridgeParent(
+ this, aPipelineId, nullptr, root->CompositorScheduler(), std::move(api),
+ std::move(holder), cbp->GetVsyncInterval());
+ parent->AddRef(); // IPDL reference
+
+ { // scope lock
+ MonitorAutoLock lock(*sIndirectLayerTreesLock);
+ sIndirectLayerTrees[layersId].mContentCompositorBridgeParent = this;
+ sIndirectLayerTrees[layersId].mWrBridge = parent;
+ }
+
+ return parent;
+}
+
+bool ContentCompositorBridgeParent::DeallocPWebRenderBridgeParent(
+ PWebRenderBridgeParent* aActor) {
+ WebRenderBridgeParent* parent = static_cast<WebRenderBridgeParent*>(aActor);
+ EraseLayerState(wr::AsLayersId(parent->PipelineId()));
+ parent->Release(); // IPDL reference
+ return true;
+}
+
+mozilla::ipc::IPCResult ContentCompositorBridgeParent::RecvNotifyChildCreated(
+ const LayersId& child, CompositorOptions* aOptions) {
+ MonitorAutoLock lock(*sIndirectLayerTreesLock);
+ for (auto& entry : sIndirectLayerTrees) {
+ CompositorBridgeParent::LayerTreeState& lts = entry.second;
+ if (lts.mParent && lts.mContentCompositorBridgeParent == this) {
+ lts.mParent->NotifyChildCreated(child);
+ *aOptions = lts.mParent->GetOptions();
+ return IPC_OK();
+ }
+ }
+ return IPC_FAIL_NO_REASON(this);
+}
+
+mozilla::ipc::IPCResult
+ContentCompositorBridgeParent::RecvMapAndNotifyChildCreated(
+ const LayersId& child, const base::ProcessId& pid,
+ CompositorOptions* aOptions) {
+ // This can only be called from the browser process, as the mapping
+ // ensures proper window ownership of layer trees.
+ return IPC_FAIL_NO_REASON(this);
+}
+
+mozilla::ipc::IPCResult
+ContentCompositorBridgeParent::RecvNotifyMemoryPressure() {
+ // This can only be called from the browser process.
+ return IPC_FAIL_NO_REASON(this);
+}
+
+mozilla::ipc::IPCResult ContentCompositorBridgeParent::RecvCheckContentOnlyTDR(
+ const uint32_t& sequenceNum, bool* isContentOnlyTDR) {
+ *isContentOnlyTDR = false;
+#ifdef XP_WIN
+ gfx::ContentDeviceData compositor;
+
+ gfx::DeviceManagerDx* dm = gfx::DeviceManagerDx::Get();
+
+ // Check that the D3D11 device sequence numbers match.
+ gfx::D3D11DeviceStatus status;
+ dm->ExportDeviceInfo(&status);
+
+ if (sequenceNum == static_cast<uint32_t>(status.sequenceNumber()) &&
+ !dm->HasDeviceReset()) {
+ *isContentOnlyTDR = true;
+ }
+
+#endif
+ return IPC_OK();
+};
+
+void ContentCompositorBridgeParent::DidCompositeLocked(
+ LayersId aId, const VsyncId& aVsyncId, TimeStamp& aCompositeStart,
+ TimeStamp& aCompositeEnd) {
+ sIndirectLayerTreesLock->AssertCurrentThreadOwns();
+ if (sIndirectLayerTrees[aId].mWrBridge) {
+ MOZ_ASSERT(false); // this should never get called for a WR compositor
+ }
+}
+
+bool ContentCompositorBridgeParent::SetTestSampleTime(const LayersId& aId,
+ const TimeStamp& aTime) {
+ MOZ_ASSERT(aId.IsValid());
+ const CompositorBridgeParent::LayerTreeState* state =
+ CompositorBridgeParent::GetIndirectShadowTree(aId);
+ if (!state) {
+ return false;
+ }
+
+ MOZ_ASSERT(state->mParent);
+ return state->mParent->SetTestSampleTime(aId, aTime);
+}
+
+void ContentCompositorBridgeParent::LeaveTestMode(const LayersId& aId) {
+ MOZ_ASSERT(aId.IsValid());
+ const CompositorBridgeParent::LayerTreeState* state =
+ CompositorBridgeParent::GetIndirectShadowTree(aId);
+ if (!state) {
+ return;
+ }
+
+ MOZ_ASSERT(state->mParent);
+ state->mParent->LeaveTestMode(aId);
+}
+
+void ContentCompositorBridgeParent::SetTestAsyncScrollOffset(
+ const LayersId& aLayersId, const ScrollableLayerGuid::ViewID& aScrollId,
+ const CSSPoint& aPoint) {
+ MOZ_ASSERT(aLayersId.IsValid());
+ const CompositorBridgeParent::LayerTreeState* state =
+ CompositorBridgeParent::GetIndirectShadowTree(aLayersId);
+ if (!state) {
+ return;
+ }
+
+ MOZ_ASSERT(state->mParent);
+ state->mParent->SetTestAsyncScrollOffset(aLayersId, aScrollId, aPoint);
+}
+
+void ContentCompositorBridgeParent::SetTestAsyncZoom(
+ const LayersId& aLayersId, const ScrollableLayerGuid::ViewID& aScrollId,
+ const LayerToParentLayerScale& aZoom) {
+ MOZ_ASSERT(aLayersId.IsValid());
+ const CompositorBridgeParent::LayerTreeState* state =
+ CompositorBridgeParent::GetIndirectShadowTree(aLayersId);
+ if (!state) {
+ return;
+ }
+
+ MOZ_ASSERT(state->mParent);
+ state->mParent->SetTestAsyncZoom(aLayersId, aScrollId, aZoom);
+}
+
+void ContentCompositorBridgeParent::FlushApzRepaints(
+ const LayersId& aLayersId) {
+ MOZ_ASSERT(aLayersId.IsValid());
+ const CompositorBridgeParent::LayerTreeState* state =
+ CompositorBridgeParent::GetIndirectShadowTree(aLayersId);
+ if (!state || !state->mParent) {
+ return;
+ }
+
+ state->mParent->FlushApzRepaints(aLayersId);
+}
+
+void ContentCompositorBridgeParent::GetAPZTestData(const LayersId& aLayersId,
+ APZTestData* aOutData) {
+ MOZ_ASSERT(aLayersId.IsValid());
+ const CompositorBridgeParent::LayerTreeState* state =
+ CompositorBridgeParent::GetIndirectShadowTree(aLayersId);
+ if (!state || !state->mParent) {
+ return;
+ }
+
+ state->mParent->GetAPZTestData(aLayersId, aOutData);
+}
+
+void ContentCompositorBridgeParent::GetFrameUniformity(
+ const LayersId& aLayersId, FrameUniformityData* aOutData) {
+ MOZ_ASSERT(aLayersId.IsValid());
+ const CompositorBridgeParent::LayerTreeState* state =
+ CompositorBridgeParent::GetIndirectShadowTree(aLayersId);
+ if (!state || !state->mParent) {
+ return;
+ }
+
+ state->mParent->GetFrameUniformity(aLayersId, aOutData);
+}
+
+void ContentCompositorBridgeParent::SetConfirmedTargetAPZC(
+ const LayersId& aLayersId, const uint64_t& aInputBlockId,
+ nsTArray<ScrollableLayerGuid>&& aTargets) {
+ MOZ_ASSERT(aLayersId.IsValid());
+ const CompositorBridgeParent::LayerTreeState* state =
+ CompositorBridgeParent::GetIndirectShadowTree(aLayersId);
+ if (!state || !state->mParent) {
+ return;
+ }
+
+ state->mParent->SetConfirmedTargetAPZC(aLayersId, aInputBlockId,
+ std::move(aTargets));
+}
+
+void ContentCompositorBridgeParent::DeferredDestroy() { mSelfRef = nullptr; }
+
+ContentCompositorBridgeParent::~ContentCompositorBridgeParent() {
+ MOZ_ASSERT(XRE_GetIOMessageLoop());
+}
+
+PTextureParent* ContentCompositorBridgeParent::AllocPTextureParent(
+ const SurfaceDescriptor& aSharedData, ReadLockDescriptor& aReadLock,
+ const LayersBackend& aLayersBackend, const TextureFlags& aFlags,
+ const LayersId& aId, const uint64_t& aSerial,
+ const wr::MaybeExternalImageId& aExternalImageId) {
+ CompositorBridgeParent::LayerTreeState* state = nullptr;
+
+ LayerTreeMap::iterator itr = sIndirectLayerTrees.find(aId);
+ if (sIndirectLayerTrees.end() != itr) {
+ state = &itr->second;
+ }
+
+ TextureFlags flags = aFlags;
+
+ LayersBackend actualBackend = LayersBackend::LAYERS_NONE;
+ if (!state) {
+ // The compositor was recreated, and we're receiving layers updates for a
+ // a layer manager that will soon be discarded or invalidated. We can't
+ // return null because this will mess up deserialization later and we'll
+ // kill the content process. Instead, we signal that the underlying
+ // TextureHost should not attempt to access the compositor.
+ flags |= TextureFlags::INVALID_COMPOSITOR;
+ } else if (actualBackend != LayersBackend::LAYERS_NONE &&
+ aLayersBackend != actualBackend) {
+ gfxDevCrash(gfx::LogReason::PAllocTextureBackendMismatch)
+ << "Texture backend is wrong";
+ }
+
+ return TextureHost::CreateIPDLActor(
+ this, aSharedData, std::move(aReadLock), aLayersBackend, aFlags,
+ mCompositorManager->GetContentId(), aSerial, aExternalImageId);
+}
+
+bool ContentCompositorBridgeParent::DeallocPTextureParent(
+ PTextureParent* actor) {
+ return TextureHost::DestroyIPDLActor(actor);
+}
+
+mozilla::ipc::IPCResult ContentCompositorBridgeParent::RecvInitPCanvasParent(
+ Endpoint<PCanvasParent>&& aEndpoint) {
+ MOZ_RELEASE_ASSERT(!mCanvasTranslator,
+ "mCanvasTranslator must be released before recreating.");
+
+ mCanvasTranslator = CanvasTranslator::Create(std::move(aEndpoint));
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult
+ContentCompositorBridgeParent::RecvReleasePCanvasParent() {
+ MOZ_RELEASE_ASSERT(mCanvasTranslator,
+ "mCanvasTranslator hasn't been created.");
+
+ mCanvasTranslator = nullptr;
+ return IPC_OK();
+}
+
+UniquePtr<SurfaceDescriptor>
+ContentCompositorBridgeParent::LookupSurfaceDescriptorForClientTexture(
+ const int64_t aTextureId) {
+ MOZ_RELEASE_ASSERT(mCanvasTranslator,
+ "mCanvasTranslator hasn't been created.");
+
+ return mCanvasTranslator->WaitForSurfaceDescriptor(aTextureId);
+}
+
+bool ContentCompositorBridgeParent::IsSameProcess() const {
+ return OtherPid() == base::GetCurrentProcId();
+}
+
+void ContentCompositorBridgeParent::ObserveLayersUpdate(
+ LayersId aLayersId, LayersObserverEpoch aEpoch, bool aActive) {
+ MOZ_ASSERT(aLayersId.IsValid());
+
+ CompositorBridgeParent::LayerTreeState* state =
+ CompositorBridgeParent::GetIndirectShadowTree(aLayersId);
+ if (!state || !state->mParent) {
+ return;
+ }
+
+ Unused << state->mParent->SendObserveLayersUpdate(aLayersId, aEpoch, aActive);
+}
+
+} // namespace mozilla::layers
diff --git a/gfx/layers/ipc/ContentCompositorBridgeParent.h b/gfx/layers/ipc/ContentCompositorBridgeParent.h
new file mode 100644
index 0000000000..77836eac55
--- /dev/null
+++ b/gfx/layers/ipc/ContentCompositorBridgeParent.h
@@ -0,0 +1,187 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_ContentCompositorBridgeParent_h
+#define mozilla_layers_ContentCompositorBridgeParent_h
+
+#include "mozilla/layers/CanvasTranslator.h"
+#include "mozilla/layers/CompositorBridgeParent.h"
+#include "mozilla/layers/CompositorThread.h"
+#include "mozilla/UniquePtr.h"
+
+namespace mozilla::layers {
+
+class CompositorOptions;
+
+/**
+ * This class handles layer updates pushed directly from child processes to
+ * the compositor thread. It's associated with a CompositorBridgeParent on the
+ * compositor thread. While it uses the PCompositorBridge protocol to manage
+ * these updates, it doesn't actually drive compositing itself. For that it
+ * hands off work to the CompositorBridgeParent it's associated with.
+ */
+class ContentCompositorBridgeParent final : public CompositorBridgeParentBase {
+ friend class CompositorBridgeParent;
+
+ public:
+ explicit ContentCompositorBridgeParent(CompositorManagerParent* aManager)
+ : CompositorBridgeParentBase(aManager), mDestroyCalled(false) {}
+
+ void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ // FIXME/bug 774388: work out what shutdown protocol we need.
+ mozilla::ipc::IPCResult RecvInitialize(
+ const LayersId& aRootLayerTreeId) override {
+ return IPC_FAIL_NO_REASON(this);
+ }
+ mozilla::ipc::IPCResult RecvWillClose() override { return IPC_OK(); }
+ mozilla::ipc::IPCResult RecvPause() override { return IPC_OK(); }
+ mozilla::ipc::IPCResult RecvRequestFxrOutput() override {
+ return IPC_FAIL_NO_REASON(this);
+ }
+ mozilla::ipc::IPCResult RecvResume() override { return IPC_OK(); }
+ mozilla::ipc::IPCResult RecvResumeAsync() override { return IPC_OK(); }
+ mozilla::ipc::IPCResult RecvNotifyChildCreated(
+ const LayersId& child, CompositorOptions* aOptions) override;
+ mozilla::ipc::IPCResult RecvMapAndNotifyChildCreated(
+ const LayersId& child, const base::ProcessId& pid,
+ CompositorOptions* aOptions) override;
+ mozilla::ipc::IPCResult RecvNotifyChildRecreated(
+ const LayersId& child, CompositorOptions* aOptions) override {
+ return IPC_FAIL_NO_REASON(this);
+ }
+ mozilla::ipc::IPCResult RecvAdoptChild(const LayersId& child) override {
+ return IPC_FAIL_NO_REASON(this);
+ }
+ mozilla::ipc::IPCResult RecvFlushRendering(
+ const wr::RenderReasons&) override {
+ return IPC_OK();
+ }
+ mozilla::ipc::IPCResult RecvFlushRenderingAsync(
+ const wr::RenderReasons&) override {
+ return IPC_OK();
+ }
+ mozilla::ipc::IPCResult RecvForcePresent(const wr::RenderReasons&) override {
+ return IPC_OK();
+ }
+ mozilla::ipc::IPCResult RecvWaitOnTransactionProcessed() override {
+ return IPC_OK();
+ }
+ mozilla::ipc::IPCResult RecvStartFrameTimeRecording(
+ const int32_t& aBufferSize, uint32_t* aOutStartIndex) override {
+ return IPC_OK();
+ }
+ mozilla::ipc::IPCResult RecvStopFrameTimeRecording(
+ const uint32_t& aStartIndex, nsTArray<float>* intervals) override {
+ return IPC_OK();
+ }
+
+ mozilla::ipc::IPCResult RecvNotifyMemoryPressure() override;
+
+ mozilla::ipc::IPCResult RecvCheckContentOnlyTDR(
+ const uint32_t& sequenceNum, bool* isContentOnlyTDR) override;
+
+ mozilla::ipc::IPCResult RecvBeginRecording(
+ const TimeStamp& aRecordingStart,
+ BeginRecordingResolver&& aResolve) override {
+ aResolve(false);
+ return IPC_OK();
+ }
+
+ mozilla::ipc::IPCResult RecvEndRecording(
+ EndRecordingResolver&& aResolve) override {
+ aResolve(Nothing());
+ return IPC_OK();
+ }
+
+ bool SetTestSampleTime(const LayersId& aId, const TimeStamp& aTime) override;
+ void LeaveTestMode(const LayersId& aId) override;
+ void SetTestAsyncScrollOffset(const LayersId& aLayersId,
+ const ScrollableLayerGuid::ViewID& aScrollId,
+ const CSSPoint& aPoint) override;
+ void SetTestAsyncZoom(const LayersId& aLayersId,
+ const ScrollableLayerGuid::ViewID& aScrollId,
+ const LayerToParentLayerScale& aZoom) override;
+ void FlushApzRepaints(const LayersId& aLayersId) override;
+ void GetAPZTestData(const LayersId& aLayersId,
+ APZTestData* aOutData) override;
+ void GetFrameUniformity(const LayersId& aLayersId,
+ FrameUniformityData* aOutData) override;
+ void SetConfirmedTargetAPZC(
+ const LayersId& aLayersId, const uint64_t& aInputBlockId,
+ nsTArray<ScrollableLayerGuid>&& aTargets) override;
+
+ // Use DidCompositeLocked if you already hold a lock on
+ // sIndirectLayerTreesLock; Otherwise use DidComposite, which would request
+ // the lock automatically.
+ void DidCompositeLocked(LayersId aId, const VsyncId& aVsyncId,
+ TimeStamp& aCompositeStart, TimeStamp& aCompositeEnd);
+
+ PTextureParent* AllocPTextureParent(
+ const SurfaceDescriptor& aSharedData, ReadLockDescriptor& aReadLock,
+ const LayersBackend& aLayersBackend, const TextureFlags& aFlags,
+ const LayersId& aId, const uint64_t& aSerial,
+ const wr::MaybeExternalImageId& aExternalImageId) override;
+
+ bool DeallocPTextureParent(PTextureParent* actor) override;
+
+ mozilla::ipc::IPCResult RecvInitPCanvasParent(
+ Endpoint<PCanvasParent>&& aEndpoint) final;
+
+ mozilla::ipc::IPCResult RecvReleasePCanvasParent() final;
+
+ bool IsSameProcess() const override;
+
+ PCompositorWidgetParent* AllocPCompositorWidgetParent(
+ const CompositorWidgetInitData& aInitData) override {
+ // Not allowed.
+ return nullptr;
+ }
+ bool DeallocPCompositorWidgetParent(
+ PCompositorWidgetParent* aActor) override {
+ // Not allowed.
+ return false;
+ }
+
+ PAPZCTreeManagerParent* AllocPAPZCTreeManagerParent(
+ const LayersId& aLayersId) override;
+ bool DeallocPAPZCTreeManagerParent(PAPZCTreeManagerParent* aActor) override;
+
+ PAPZParent* AllocPAPZParent(const LayersId& aLayersId) override;
+ bool DeallocPAPZParent(PAPZParent* aActor) override;
+
+ PWebRenderBridgeParent* AllocPWebRenderBridgeParent(
+ const wr::PipelineId& aPipelineId, const LayoutDeviceIntSize& aSize,
+ const WindowKind& aWindowKind) override;
+ bool DeallocPWebRenderBridgeParent(PWebRenderBridgeParent* aActor) override;
+
+ void ObserveLayersUpdate(LayersId aLayersId, LayersObserverEpoch aEpoch,
+ bool aActive) override;
+
+ bool IsRemote() const override { return true; }
+
+ UniquePtr<SurfaceDescriptor> LookupSurfaceDescriptorForClientTexture(
+ const int64_t aTextureId) final;
+
+ private:
+ // Private destructor, to discourage deletion outside of Release():
+ virtual ~ContentCompositorBridgeParent();
+
+ void DeferredDestroy();
+
+ // There can be many CPCPs, and IPDL-generated code doesn't hold a
+ // reference to top-level actors. So we hold a reference to
+ // ourself. This is released (deferred) in ActorDestroy().
+ RefPtr<ContentCompositorBridgeParent> mSelfRef;
+
+ bool mDestroyCalled;
+
+ RefPtr<CanvasTranslator> mCanvasTranslator;
+};
+
+} // namespace mozilla::layers
+
+#endif // mozilla_layers_ContentCompositorBridgeParent_h
diff --git a/gfx/layers/ipc/ISurfaceAllocator.cpp b/gfx/layers/ipc/ISurfaceAllocator.cpp
new file mode 100644
index 0000000000..7baca7a9b6
--- /dev/null
+++ b/gfx/layers/ipc/ISurfaceAllocator.cpp
@@ -0,0 +1,217 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ISurfaceAllocator.h"
+
+#include "mozilla/layers/ImageBridgeParent.h" // for ImageBridgeParent
+#include "mozilla/layers/TextureHost.h" // for TextureHost
+#include "mozilla/layers/TextureForwarder.h"
+#include "mozilla/layers/CompositableForwarder.h"
+
+namespace mozilla {
+namespace layers {
+
+NS_IMPL_ISUPPORTS(GfxMemoryImageReporter, nsIMemoryReporter)
+
+mozilla::Atomic<ptrdiff_t> GfxMemoryImageReporter::sAmount(0);
+
+void HostIPCAllocator::SendPendingAsyncMessages() {
+ if (mPendingAsyncMessage.empty()) {
+ return;
+ }
+
+ // Some type of AsyncParentMessageData message could have
+ // one file descriptor (e.g. OpDeliverFence).
+ // A number of file descriptors per gecko ipc message have a limitation
+ // on OS_POSIX (MACOSX or LINUX).
+ static const uint32_t kMaxMessageNumber =
+ IPC::Message::MAX_DESCRIPTORS_PER_MESSAGE;
+
+ nsTArray<AsyncParentMessageData> messages;
+ messages.SetCapacity(mPendingAsyncMessage.size());
+ for (size_t i = 0; i < mPendingAsyncMessage.size(); i++) {
+ messages.AppendElement(mPendingAsyncMessage[i]);
+ // Limit maximum number of messages.
+ if (messages.Length() >= kMaxMessageNumber) {
+ SendAsyncMessage(messages);
+ // Initialize Messages.
+ messages.Clear();
+ }
+ }
+
+ if (messages.Length() > 0) {
+ SendAsyncMessage(messages);
+ }
+ mPendingAsyncMessage.clear();
+}
+
+// XXX - We should actually figure out the minimum shmem allocation size on
+// a certain platform and use that.
+const uint32_t sShmemPageSize = 4096;
+
+#ifdef DEBUG
+const uint32_t sSupportedBlockSize = 4;
+#endif
+
+FixedSizeSmallShmemSectionAllocator::FixedSizeSmallShmemSectionAllocator(
+ LayersIPCChannel* aShmProvider)
+ : mShmProvider(aShmProvider) {
+ MOZ_ASSERT(mShmProvider);
+}
+
+FixedSizeSmallShmemSectionAllocator::~FixedSizeSmallShmemSectionAllocator() {
+ ShrinkShmemSectionHeap();
+}
+
+bool FixedSizeSmallShmemSectionAllocator::IPCOpen() const {
+ return mShmProvider->IPCOpen();
+}
+
+bool FixedSizeSmallShmemSectionAllocator::AllocShmemSection(
+ uint32_t aSize, ShmemSection* aShmemSection) {
+ // For now we only support sizes of 4. If we want to support different sizes
+ // some more complicated bookkeeping should be added.
+ MOZ_ASSERT(aSize == sSupportedBlockSize);
+ MOZ_ASSERT(aShmemSection);
+
+ if (!IPCOpen()) {
+ gfxCriticalError() << "Attempt to allocate a ShmemSection after shutdown.";
+ return false;
+ }
+
+ uint32_t allocationSize = (aSize + sizeof(ShmemSectionHeapAllocation));
+
+ for (size_t i = 0; i < mUsedShmems.size(); i++) {
+ ShmemSectionHeapHeader* header =
+ mUsedShmems[i].get<ShmemSectionHeapHeader>();
+ if ((header->mAllocatedBlocks + 1) * allocationSize +
+ sizeof(ShmemSectionHeapHeader) <
+ sShmemPageSize) {
+ aShmemSection->shmem() = mUsedShmems[i];
+ MOZ_ASSERT(mUsedShmems[i].IsWritable());
+ break;
+ }
+ }
+
+ if (!aShmemSection->shmem().IsWritable()) {
+ ipc::Shmem tmp;
+ if (!mShmProvider->AllocUnsafeShmem(sShmemPageSize, &tmp)) {
+ return false;
+ }
+
+ ShmemSectionHeapHeader* header = tmp.get<ShmemSectionHeapHeader>();
+ header->mTotalBlocks = 0;
+ header->mAllocatedBlocks = 0;
+
+ mUsedShmems.push_back(tmp);
+ aShmemSection->shmem() = tmp;
+ }
+
+ MOZ_ASSERT(aShmemSection->shmem().IsWritable());
+
+ ShmemSectionHeapHeader* header =
+ aShmemSection->shmem().get<ShmemSectionHeapHeader>();
+ uint8_t* heap =
+ aShmemSection->shmem().get<uint8_t>() + sizeof(ShmemSectionHeapHeader);
+
+ ShmemSectionHeapAllocation* allocHeader = nullptr;
+
+ if (header->mTotalBlocks > header->mAllocatedBlocks) {
+ // Search for the first available block.
+ for (size_t i = 0; i < header->mTotalBlocks; i++) {
+ allocHeader = reinterpret_cast<ShmemSectionHeapAllocation*>(heap);
+
+ if (allocHeader->mStatus == STATUS_FREED) {
+ break;
+ }
+ heap += allocationSize;
+ }
+ MOZ_ASSERT(allocHeader && allocHeader->mStatus == STATUS_FREED);
+ MOZ_ASSERT(allocHeader->mSize == sSupportedBlockSize);
+ } else {
+ heap += header->mTotalBlocks * allocationSize;
+
+ header->mTotalBlocks++;
+ allocHeader = reinterpret_cast<ShmemSectionHeapAllocation*>(heap);
+ allocHeader->mSize = aSize;
+ }
+
+ MOZ_ASSERT(allocHeader);
+ header->mAllocatedBlocks++;
+ allocHeader->mStatus = STATUS_ALLOCATED;
+
+ aShmemSection->size() = aSize;
+ aShmemSection->offset() = (heap + sizeof(ShmemSectionHeapAllocation)) -
+ aShmemSection->shmem().get<uint8_t>();
+ ShrinkShmemSectionHeap();
+ return true;
+}
+
+void FixedSizeSmallShmemSectionAllocator::FreeShmemSection(
+ mozilla::layers::ShmemSection& aShmemSection) {
+ MOZ_ASSERT(aShmemSection.size() == sSupportedBlockSize);
+ MOZ_ASSERT(aShmemSection.offset() < sShmemPageSize - sSupportedBlockSize);
+
+ if (!aShmemSection.shmem().IsWritable()) {
+ return;
+ }
+
+ ShmemSectionHeapAllocation* allocHeader =
+ reinterpret_cast<ShmemSectionHeapAllocation*>(
+ aShmemSection.shmem().get<char>() + aShmemSection.offset() -
+ sizeof(ShmemSectionHeapAllocation));
+
+ MOZ_ASSERT(allocHeader->mSize == aShmemSection.size());
+
+ DebugOnly<bool> success =
+ allocHeader->mStatus.compareExchange(STATUS_ALLOCATED, STATUS_FREED);
+ // If this fails something really weird is going on.
+ MOZ_ASSERT(success);
+
+ ShmemSectionHeapHeader* header =
+ aShmemSection.shmem().get<ShmemSectionHeapHeader>();
+ header->mAllocatedBlocks--;
+}
+
+void FixedSizeSmallShmemSectionAllocator::DeallocShmemSection(
+ mozilla::layers::ShmemSection& aShmemSection) {
+ if (!IPCOpen()) {
+ gfxCriticalNote << "Attempt to dealloc a ShmemSections after shutdown.";
+ return;
+ }
+
+ FreeShmemSection(aShmemSection);
+ ShrinkShmemSectionHeap();
+}
+
+void FixedSizeSmallShmemSectionAllocator::ShrinkShmemSectionHeap() {
+ if (!IPCOpen()) {
+ mUsedShmems.clear();
+ return;
+ }
+
+ // The loop will terminate as we either increase i, or decrease size
+ // every time through.
+ size_t i = 0;
+ while (i < mUsedShmems.size()) {
+ ShmemSectionHeapHeader* header =
+ mUsedShmems[i].get<ShmemSectionHeapHeader>();
+ if (header->mAllocatedBlocks == 0) {
+ mShmProvider->DeallocShmem(mUsedShmems[i]);
+ // We don't particularly care about order, move the last one in the array
+ // to this position.
+ if (i < mUsedShmems.size() - 1) {
+ mUsedShmems[i] = mUsedShmems[mUsedShmems.size() - 1];
+ }
+ mUsedShmems.pop_back();
+ } else {
+ i++;
+ }
+ }
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/ipc/ISurfaceAllocator.h b/gfx/layers/ipc/ISurfaceAllocator.h
new file mode 100644
index 0000000000..77edf4fc5f
--- /dev/null
+++ b/gfx/layers/ipc/ISurfaceAllocator.h
@@ -0,0 +1,285 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GFX_LAYERS_ISURFACEDEALLOCATOR
+#define GFX_LAYERS_ISURFACEDEALLOCATOR
+
+#include <stddef.h> // for size_t
+#include <stdint.h> // for uint32_t
+#include "gfxTypes.h"
+#include "mozilla/dom/ipc/IdType.h"
+#include "mozilla/gfx/Point.h" // for IntSize
+#include "mozilla/ipc/SharedMemory.h" // for SharedMemory, etc
+#include "mozilla/RefPtr.h"
+#include "nsIMemoryReporter.h" // for nsIMemoryReporter
+#include "mozilla/Atomics.h" // for Atomic
+#include "mozilla/layers/LayersMessages.h" // for ShmemSection
+
+namespace mozilla {
+namespace ipc {
+class Shmem;
+class IShmemAllocator;
+} // namespace ipc
+namespace gfx {
+class DataSourceSurface;
+} // namespace gfx
+
+namespace layers {
+
+class CompositableForwarder;
+class CompositorBridgeParentBase;
+class TextureForwarder;
+
+class ShmemSectionAllocator;
+class LegacySurfaceDescriptorAllocator;
+class ClientIPCAllocator;
+class HostIPCAllocator;
+class LayersIPCChannel;
+
+enum BufferCapabilities {
+ DEFAULT_BUFFER_CAPS = 0,
+ /**
+ * The allocated buffer must be efficiently mappable as a DataSourceSurface.
+ */
+ MAP_AS_IMAGE_SURFACE = 1 << 0,
+ /**
+ * The allocated buffer will be used for GL rendering only
+ */
+ USING_GL_RENDERING_ONLY = 1 << 1
+};
+
+class SurfaceDescriptor;
+
+/**
+ * An interface used to create and destroy surfaces that are shared with the
+ * Compositor process (using shmem, or other platform specific memory)
+ *
+ * Most of the methods here correspond to methods that are implemented by IPDL
+ * actors without a common polymorphic interface.
+ * These methods should be only called in the ipdl implementor's thread, unless
+ * specified otherwise in the implementing class.
+ */
+class ISurfaceAllocator {
+ public:
+ MOZ_DECLARE_REFCOUNTED_TYPENAME(ISurfaceAllocator)
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(ISurfaceAllocator)
+
+ ISurfaceAllocator() = default;
+
+ // down-casting
+
+ virtual mozilla::ipc::IShmemAllocator* AsShmemAllocator() { return nullptr; }
+
+ virtual ShmemSectionAllocator* AsShmemSectionAllocator() { return nullptr; }
+
+ virtual CompositableForwarder* AsCompositableForwarder() { return nullptr; }
+
+ virtual TextureForwarder* GetTextureForwarder() { return nullptr; }
+
+ virtual ClientIPCAllocator* AsClientAllocator() { return nullptr; }
+
+ virtual HostIPCAllocator* AsHostIPCAllocator() { return nullptr; }
+
+ virtual LegacySurfaceDescriptorAllocator*
+ AsLegacySurfaceDescriptorAllocator() {
+ return nullptr;
+ }
+
+ virtual CompositorBridgeParentBase* AsCompositorBridgeParentBase() {
+ return nullptr;
+ }
+
+ // ipc info
+
+ virtual bool IPCOpen() const { return true; }
+
+ virtual bool IsSameProcess() const = 0;
+
+ virtual bool UsesImageBridge() const { return false; }
+
+ virtual bool UsesWebRenderBridge() const { return false; }
+
+ virtual dom::ContentParentId GetContentId() { return dom::ContentParentId(); }
+
+ protected:
+ void Finalize() {}
+
+ virtual ~ISurfaceAllocator() = default;
+};
+
+/// Methods that are specific to the client/child side.
+class ClientIPCAllocator : public ISurfaceAllocator {
+ public:
+ ClientIPCAllocator() = default;
+
+ ClientIPCAllocator* AsClientAllocator() override { return this; }
+
+ virtual base::ProcessId GetParentPid() const = 0;
+
+ virtual MessageLoop* GetMessageLoop() const = 0;
+
+ virtual void CancelWaitForNotifyNotUsed(uint64_t aTextureId) = 0;
+};
+
+/// Methods that are specific to the host/parent side.
+class HostIPCAllocator : public ISurfaceAllocator {
+ public:
+ HostIPCAllocator() = default;
+
+ HostIPCAllocator* AsHostIPCAllocator() override { return this; }
+
+ /**
+ * Get child side's process Id.
+ */
+ virtual base::ProcessId GetChildProcessId() = 0;
+
+ virtual void NotifyNotUsed(PTextureParent* aTexture,
+ uint64_t aTransactionId) = 0;
+
+ virtual void SendAsyncMessage(
+ const nsTArray<AsyncParentMessageData>& aMessage) = 0;
+
+ virtual void SendPendingAsyncMessages();
+
+ virtual void SetAboutToSendAsyncMessages() {
+ mAboutToSendAsyncMessages = true;
+ }
+
+ bool IsAboutToSendAsyncMessages() { return mAboutToSendAsyncMessages; }
+
+ protected:
+ std::vector<AsyncParentMessageData> mPendingAsyncMessage;
+ bool mAboutToSendAsyncMessages = false;
+};
+
+/// An allocator that can group allocations in bigger chunks of shared memory.
+///
+/// The allocated shmem sections can only be deallocated by the same allocator
+/// instance (and only in the child process).
+class ShmemSectionAllocator {
+ public:
+ virtual bool AllocShmemSection(uint32_t aSize,
+ ShmemSection* aShmemSection) = 0;
+
+ virtual void DeallocShmemSection(ShmemSection& aShmemSection) = 0;
+
+ virtual void MemoryPressure() {}
+};
+
+/// Some old stuff that's still around and used for screenshots.
+///
+/// New code should not need this (see TextureClient).
+class LegacySurfaceDescriptorAllocator {
+ public:
+ virtual bool AllocSurfaceDescriptor(const gfx::IntSize& aSize,
+ gfxContentType aContent,
+ SurfaceDescriptor* aBuffer) = 0;
+
+ virtual bool AllocSurfaceDescriptorWithCaps(const gfx::IntSize& aSize,
+ gfxContentType aContent,
+ uint32_t aCaps,
+ SurfaceDescriptor* aBuffer) = 0;
+
+ virtual void DestroySurfaceDescriptor(SurfaceDescriptor* aSurface) = 0;
+};
+
+bool IsSurfaceDescriptorValid(const SurfaceDescriptor& aSurface);
+
+already_AddRefed<gfx::DataSourceSurface> GetSurfaceForDescriptor(
+ const SurfaceDescriptor& aDescriptor);
+
+uint8_t* GetAddressFromDescriptor(const SurfaceDescriptor& aDescriptor);
+
+void DestroySurfaceDescriptor(mozilla::ipc::IShmemAllocator* aAllocator,
+ SurfaceDescriptor* aSurface);
+
+class GfxMemoryImageReporter final : public nsIMemoryReporter {
+ ~GfxMemoryImageReporter() = default;
+
+ public:
+ NS_DECL_ISUPPORTS
+
+ GfxMemoryImageReporter() {
+#ifdef DEBUG
+ // There must be only one instance of this class, due to |sAmount|
+ // being static.
+ static bool hasRun = false;
+ MOZ_ASSERT(!hasRun);
+ hasRun = true;
+#endif
+ }
+
+ MOZ_DEFINE_MALLOC_SIZE_OF_ON_ALLOC(MallocSizeOfOnAlloc)
+ MOZ_DEFINE_MALLOC_SIZE_OF_ON_FREE(MallocSizeOfOnFree)
+
+ static void DidAlloc(void* aPointer) {
+ sAmount += MallocSizeOfOnAlloc(aPointer);
+ }
+
+ static void WillFree(void* aPointer) {
+ sAmount -= MallocSizeOfOnFree(aPointer);
+ }
+
+ NS_IMETHOD CollectReports(nsIHandleReportCallback* aHandleReport,
+ nsISupports* aData, bool aAnonymize) override {
+ MOZ_COLLECT_REPORT(
+ "explicit/gfx/heap-textures", KIND_HEAP, UNITS_BYTES, sAmount,
+ "Heap memory shared between threads by texture clients and hosts.");
+
+ return NS_OK;
+ }
+
+ private:
+ // Typically we use |size_t| in memory reporters, but in the past this
+ // variable has sometimes gone negative due to missing DidAlloc() calls.
+ // Therefore, we use a signed type so that any such negative values show up
+ // as negative in about:memory, rather than as enormous positive numbers.
+ static mozilla::Atomic<ptrdiff_t> sAmount;
+};
+
+/// A simple shmem section allocator that can only allocate small
+/// fixed size elements (only intended to be used to store tile
+/// copy-on-write locks for now).
+class FixedSizeSmallShmemSectionAllocator final : public ShmemSectionAllocator {
+ public:
+ enum AllocationStatus { STATUS_ALLOCATED, STATUS_FREED };
+
+ struct ShmemSectionHeapHeader {
+ Atomic<uint32_t> mTotalBlocks;
+ Atomic<uint32_t> mAllocatedBlocks;
+ };
+
+ struct ShmemSectionHeapAllocation {
+ Atomic<uint32_t> mStatus;
+ uint32_t mSize;
+ };
+
+ explicit FixedSizeSmallShmemSectionAllocator(LayersIPCChannel* aShmProvider);
+
+ ~FixedSizeSmallShmemSectionAllocator();
+
+ bool AllocShmemSection(uint32_t aSize, ShmemSection* aShmemSection) override;
+
+ void DeallocShmemSection(ShmemSection& aShmemSection) override;
+
+ void MemoryPressure() override { ShrinkShmemSectionHeap(); }
+
+ // can be called on the compositor process.
+ static void FreeShmemSection(ShmemSection& aShmemSection);
+
+ void ShrinkShmemSectionHeap();
+
+ bool IPCOpen() const;
+
+ protected:
+ std::vector<mozilla::ipc::Shmem> mUsedShmems;
+ LayersIPCChannel* mShmProvider;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif
diff --git a/gfx/layers/ipc/ImageBridgeChild.cpp b/gfx/layers/ipc/ImageBridgeChild.cpp
new file mode 100644
index 0000000000..6dcf2be415
--- /dev/null
+++ b/gfx/layers/ipc/ImageBridgeChild.cpp
@@ -0,0 +1,939 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ImageBridgeChild.h"
+
+#include <vector> // for vector
+
+#include "ImageBridgeParent.h" // for ImageBridgeParent
+#include "ImageContainer.h" // for ImageContainer
+#include "SynchronousTask.h"
+#include "mozilla/Assertions.h" // for MOZ_ASSERT, etc
+#include "mozilla/Monitor.h" // for Monitor, MonitorAutoLock
+#include "mozilla/ReentrantMonitor.h" // for ReentrantMonitor, etc
+#include "mozilla/StaticMutex.h"
+#include "mozilla/StaticPtr.h" // for StaticRefPtr
+#include "mozilla/dom/ContentChild.h"
+#include "mozilla/gfx/Point.h" // for IntSize
+#include "mozilla/gfx/gfxVars.h"
+#include "mozilla/ipc/Endpoint.h"
+#include "mozilla/ipc/MessageChannel.h" // for MessageChannel, etc
+#include "mozilla/layers/CompositableClient.h" // for CompositableChild, etc
+#include "mozilla/layers/CompositorThread.h"
+#include "mozilla/layers/ISurfaceAllocator.h" // for ISurfaceAllocator
+#include "mozilla/layers/ImageClient.h" // for ImageClient
+#include "mozilla/layers/LayersMessages.h" // for CompositableOperation
+#include "mozilla/layers/TextureClient.h" // for TextureClient
+#include "mozilla/layers/TextureClient.h"
+#include "mozilla/media/MediaSystemResourceManager.h" // for MediaSystemResourceManager
+#include "mozilla/media/MediaSystemResourceManagerChild.h" // for MediaSystemResourceManagerChild
+#include "mozilla/mozalloc.h" // for operator new, etc
+#include "transport/runnable_utils.h"
+#include "nsContentUtils.h"
+#include "nsISupportsImpl.h" // for ImageContainer::AddRef, etc
+#include "nsTArray.h" // for AutoTArray, nsTArray, etc
+#include "nsTArrayForwardDeclare.h" // for AutoTArray
+#include "nsThreadUtils.h" // for NS_IsMainThread
+
+#if defined(XP_WIN)
+# include "mozilla/gfx/DeviceManagerDx.h"
+#endif
+
+namespace mozilla {
+namespace ipc {
+class Shmem;
+} // namespace ipc
+
+namespace layers {
+
+using namespace mozilla::ipc;
+using namespace mozilla::gfx;
+using namespace mozilla::media;
+
+typedef std::vector<CompositableOperation> OpVector;
+typedef nsTArray<OpDestroy> OpDestroyVector;
+
+struct CompositableTransaction {
+ CompositableTransaction() : mFinished(true) {}
+ ~CompositableTransaction() { End(); }
+ bool Finished() const { return mFinished; }
+ void Begin() {
+ MOZ_ASSERT(mFinished);
+ mFinished = false;
+ }
+ void End() {
+ mFinished = true;
+ mOperations.clear();
+ mDestroyedActors.Clear();
+ }
+ bool IsEmpty() const {
+ return mOperations.empty() && mDestroyedActors.IsEmpty();
+ }
+ void AddNoSwapEdit(const CompositableOperation& op) {
+ MOZ_ASSERT(!Finished(), "forgot BeginTransaction?");
+ mOperations.push_back(op);
+ }
+
+ OpVector mOperations;
+ OpDestroyVector mDestroyedActors;
+
+ bool mFinished;
+};
+
+struct AutoEndTransaction final {
+ explicit AutoEndTransaction(CompositableTransaction* aTxn) : mTxn(aTxn) {}
+ ~AutoEndTransaction() { mTxn->End(); }
+ CompositableTransaction* mTxn;
+};
+
+void ImageBridgeChild::UseTextures(
+ CompositableClient* aCompositable,
+ const nsTArray<TimedTextureClient>& aTextures) {
+ MOZ_ASSERT(aCompositable);
+ MOZ_ASSERT(aCompositable->GetIPCHandle());
+ MOZ_ASSERT(aCompositable->IsConnected());
+
+ AutoTArray<TimedTexture, 4> textures;
+
+ for (auto& t : aTextures) {
+ MOZ_ASSERT(t.mTextureClient);
+ MOZ_ASSERT(t.mTextureClient->GetIPDLActor());
+
+ if (!t.mTextureClient->IsSharedWithCompositor()) {
+ return;
+ }
+
+ bool readLocked = t.mTextureClient->OnForwardedToHost();
+
+ textures.AppendElement(TimedTexture(
+ WrapNotNull(t.mTextureClient->GetIPDLActor()), t.mTimeStamp,
+ t.mPictureRect, t.mFrameID, t.mProducerID, readLocked));
+
+ // Wait end of usage on host side if TextureFlags::RECYCLE is set
+ HoldUntilCompositableRefReleasedIfNecessary(t.mTextureClient);
+ }
+ mTxn->AddNoSwapEdit(CompositableOperation(aCompositable->GetIPCHandle(),
+ OpUseTexture(textures)));
+}
+
+void ImageBridgeChild::UseRemoteTexture(CompositableClient* aCompositable,
+ const RemoteTextureId aTextureId,
+ const RemoteTextureOwnerId aOwnerId,
+ const gfx::IntSize aSize,
+ const TextureFlags aFlags) {
+ MOZ_ASSERT(aCompositable);
+ MOZ_ASSERT(aCompositable->GetIPCHandle());
+ MOZ_ASSERT(aCompositable->IsConnected());
+
+ mTxn->AddNoSwapEdit(CompositableOperation(
+ aCompositable->GetIPCHandle(),
+ OpUseRemoteTexture(aTextureId, aOwnerId, aSize, aFlags)));
+}
+
+void ImageBridgeChild::EnableRemoteTexturePushCallback(
+ CompositableClient* aCompositable, const RemoteTextureOwnerId aOwnerId,
+ const gfx::IntSize aSize, const TextureFlags aFlags) {
+ MOZ_ASSERT(aCompositable);
+ MOZ_ASSERT(aCompositable->GetIPCHandle());
+ MOZ_ASSERT(aCompositable->IsConnected());
+
+ mTxn->AddNoSwapEdit(CompositableOperation(
+ aCompositable->GetIPCHandle(),
+ OpEnableRemoteTexturePushCallback(aOwnerId, aSize, aFlags)));
+}
+
+void ImageBridgeChild::HoldUntilCompositableRefReleasedIfNecessary(
+ TextureClient* aClient) {
+ if (!aClient) {
+ return;
+ }
+
+ // Wait ReleaseCompositableRef only when TextureFlags::RECYCLE or
+ // TextureFlags::WAIT_HOST_USAGE_END is set on ImageBridge.
+ bool waitNotifyNotUsed =
+ aClient->GetFlags() & TextureFlags::RECYCLE ||
+ aClient->GetFlags() & TextureFlags::WAIT_HOST_USAGE_END;
+ if (!waitNotifyNotUsed) {
+ return;
+ }
+
+ aClient->SetLastFwdTransactionId(GetFwdTransactionId());
+ mTexturesWaitingNotifyNotUsed.emplace(aClient->GetSerial(), aClient);
+}
+
+void ImageBridgeChild::NotifyNotUsed(uint64_t aTextureId,
+ uint64_t aFwdTransactionId) {
+ auto it = mTexturesWaitingNotifyNotUsed.find(aTextureId);
+ if (it != mTexturesWaitingNotifyNotUsed.end()) {
+ if (aFwdTransactionId < it->second->GetLastFwdTransactionId()) {
+ // Released on host side, but client already requested newer use texture.
+ return;
+ }
+ mTexturesWaitingNotifyNotUsed.erase(it);
+ }
+}
+
+void ImageBridgeChild::CancelWaitForNotifyNotUsed(uint64_t aTextureId) {
+ MOZ_ASSERT(InImageBridgeChildThread());
+ mTexturesWaitingNotifyNotUsed.erase(aTextureId);
+}
+
+// Singleton
+static StaticMutex sImageBridgeSingletonLock MOZ_UNANNOTATED;
+static StaticRefPtr<ImageBridgeChild> sImageBridgeChildSingleton;
+static StaticRefPtr<nsIThread> sImageBridgeChildThread;
+
+// dispatched function
+void ImageBridgeChild::ShutdownStep1(SynchronousTask* aTask) {
+ AutoCompleteTask complete(aTask);
+
+ MOZ_ASSERT(InImageBridgeChildThread(),
+ "Should be in ImageBridgeChild thread.");
+
+ MediaSystemResourceManager::Shutdown();
+
+ // Force all managed protocols to shut themselves down cleanly
+ nsTArray<PTextureChild*> textures;
+ ManagedPTextureChild(textures);
+ for (int i = textures.Length() - 1; i >= 0; --i) {
+ RefPtr<TextureClient> client = TextureClient::AsTextureClient(textures[i]);
+ if (client) {
+ client->Destroy();
+ }
+ }
+
+ if (mCanSend) {
+ SendWillClose();
+ }
+ MarkShutDown();
+
+ // From now on, no message can be sent through the image bridge from the
+ // client side except the final Stop message.
+}
+
+// dispatched function
+void ImageBridgeChild::ShutdownStep2(SynchronousTask* aTask) {
+ AutoCompleteTask complete(aTask);
+
+ MOZ_ASSERT(InImageBridgeChildThread(),
+ "Should be in ImageBridgeChild thread.");
+ if (!mDestroyed) {
+ Close();
+ }
+}
+
+void ImageBridgeChild::ActorDestroy(ActorDestroyReason aWhy) {
+ mCanSend = false;
+ mDestroyed = true;
+ {
+ MutexAutoLock lock(mContainerMapLock);
+ mImageContainerListeners.clear();
+ }
+}
+
+void ImageBridgeChild::CreateImageClientSync(SynchronousTask* aTask,
+ RefPtr<ImageClient>* result,
+ CompositableType aType,
+ ImageContainer* aImageContainer) {
+ AutoCompleteTask complete(aTask);
+ *result = CreateImageClientNow(aType, aImageContainer);
+}
+
+ImageBridgeChild::ImageBridgeChild(uint32_t aNamespace)
+ : mNamespace(aNamespace),
+ mCanSend(false),
+ mDestroyed(false),
+ mFwdTransactionId(0),
+ mContainerMapLock("ImageBridgeChild.mContainerMapLock") {
+ MOZ_ASSERT(mNamespace);
+ MOZ_ASSERT(NS_IsMainThread());
+
+ mTxn = new CompositableTransaction();
+}
+
+ImageBridgeChild::~ImageBridgeChild() { delete mTxn; }
+
+void ImageBridgeChild::MarkShutDown() {
+ mTexturesWaitingNotifyNotUsed.clear();
+
+ mCanSend = false;
+}
+
+void ImageBridgeChild::Connect(CompositableClient* aCompositable,
+ ImageContainer* aImageContainer) {
+ MOZ_ASSERT(aCompositable);
+ MOZ_ASSERT(InImageBridgeChildThread());
+ MOZ_ASSERT(CanSend());
+
+ CompositableHandle handle = CompositableHandle::GetNext();
+
+ // ImageClient of ImageContainer provides aImageContainer.
+ // But offscreen canvas does not provide it.
+ if (aImageContainer) {
+ MutexAutoLock lock(mContainerMapLock);
+ MOZ_ASSERT(mImageContainerListeners.find(uint64_t(handle)) ==
+ mImageContainerListeners.end());
+ mImageContainerListeners.emplace(
+ uint64_t(handle), aImageContainer->GetImageContainerListener());
+ }
+
+ aCompositable->InitIPDL(handle);
+ SendNewCompositable(handle, aCompositable->GetTextureInfo());
+}
+
+void ImageBridgeChild::ForgetImageContainer(const CompositableHandle& aHandle) {
+ MutexAutoLock lock(mContainerMapLock);
+ mImageContainerListeners.erase(aHandle.Value());
+}
+
+/* static */
+RefPtr<ImageBridgeChild> ImageBridgeChild::GetSingleton() {
+ StaticMutexAutoLock lock(sImageBridgeSingletonLock);
+ return sImageBridgeChildSingleton;
+}
+
+void ImageBridgeChild::UpdateImageClient(RefPtr<ImageContainer> aContainer) {
+ if (!aContainer) {
+ return;
+ }
+
+ if (!InImageBridgeChildThread()) {
+ RefPtr<Runnable> runnable =
+ WrapRunnable(RefPtr<ImageBridgeChild>(this),
+ &ImageBridgeChild::UpdateImageClient, aContainer);
+ GetThread()->Dispatch(runnable.forget());
+ return;
+ }
+
+ if (!CanSend()) {
+ return;
+ }
+
+ RefPtr<ImageClient> client = aContainer->GetImageClient();
+ if (NS_WARN_IF(!client)) {
+ return;
+ }
+
+ // If the client has become disconnected before this event was dispatched,
+ // early return now.
+ if (!client->IsConnected()) {
+ return;
+ }
+
+ BeginTransaction();
+ client->UpdateImage(aContainer);
+ EndTransaction();
+}
+
+void ImageBridgeChild::UpdateCompositable(
+ const RefPtr<ImageContainer> aContainer, const RemoteTextureId aTextureId,
+ const RemoteTextureOwnerId aOwnerId, const gfx::IntSize aSize,
+ const TextureFlags aFlags) {
+ if (!aContainer) {
+ return;
+ }
+
+ if (!InImageBridgeChildThread()) {
+ RefPtr<Runnable> runnable = WrapRunnable(
+ RefPtr<ImageBridgeChild>(this), &ImageBridgeChild::UpdateCompositable,
+ aContainer, aTextureId, aOwnerId, aSize, aFlags);
+ GetThread()->Dispatch(runnable.forget());
+ return;
+ }
+
+ if (!CanSend()) {
+ return;
+ }
+
+ RefPtr<ImageClient> client = aContainer->GetImageClient();
+ if (NS_WARN_IF(!client)) {
+ return;
+ }
+
+ // If the client has become disconnected before this event was dispatched,
+ // early return now.
+ if (!client->IsConnected()) {
+ return;
+ }
+
+ BeginTransaction();
+ UseRemoteTexture(client, aTextureId, aOwnerId, aSize, aFlags);
+ EndTransaction();
+}
+
+void ImageBridgeChild::FlushAllImagesSync(SynchronousTask* aTask,
+ ImageClient* aClient,
+ ImageContainer* aContainer) {
+ AutoCompleteTask complete(aTask);
+
+ if (!CanSend()) {
+ return;
+ }
+
+ MOZ_ASSERT(aClient);
+ BeginTransaction();
+ if (aContainer) {
+ aContainer->ClearImagesFromImageBridge();
+ }
+ aClient->FlushAllImages();
+ EndTransaction();
+}
+
+void ImageBridgeChild::FlushAllImages(ImageClient* aClient,
+ ImageContainer* aContainer) {
+ MOZ_ASSERT(aClient);
+ MOZ_ASSERT(!InImageBridgeChildThread());
+
+ if (InImageBridgeChildThread()) {
+ NS_ERROR(
+ "ImageBridgeChild::FlushAllImages() is called on ImageBridge thread.");
+ return;
+ }
+
+ SynchronousTask task("FlushAllImages Lock");
+
+ // RefPtrs on arguments are not needed since this dispatches synchronously.
+ RefPtr<Runnable> runnable = WrapRunnable(
+ RefPtr<ImageBridgeChild>(this), &ImageBridgeChild::FlushAllImagesSync,
+ &task, aClient, aContainer);
+ GetThread()->Dispatch(runnable.forget());
+
+ task.Wait();
+}
+
+void ImageBridgeChild::BeginTransaction() {
+ MOZ_ASSERT(CanSend());
+ MOZ_ASSERT(mTxn->Finished(), "uncommitted txn?");
+ UpdateFwdTransactionId();
+ mTxn->Begin();
+}
+
+void ImageBridgeChild::EndTransaction() {
+ MOZ_ASSERT(CanSend());
+ MOZ_ASSERT(!mTxn->Finished(), "forgot BeginTransaction?");
+
+ AutoEndTransaction _(mTxn);
+
+ if (mTxn->IsEmpty()) {
+ return;
+ }
+
+ AutoTArray<CompositableOperation, 10> cset;
+ cset.SetCapacity(mTxn->mOperations.size());
+ if (!mTxn->mOperations.empty()) {
+ cset.AppendElements(&mTxn->mOperations.front(), mTxn->mOperations.size());
+ }
+
+ if (!SendUpdate(cset, mTxn->mDestroyedActors, GetFwdTransactionId())) {
+ NS_WARNING("could not send async texture transaction");
+ return;
+ }
+}
+
+bool ImageBridgeChild::InitForContent(Endpoint<PImageBridgeChild>&& aEndpoint,
+ uint32_t aNamespace) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ gfxPlatform::GetPlatform();
+
+ if (!sImageBridgeChildThread) {
+ nsCOMPtr<nsIThread> thread;
+ nsresult rv = NS_NewNamedThread("ImageBridgeChld", getter_AddRefs(thread));
+ MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv),
+ "Failed to start ImageBridgeChild thread!");
+ sImageBridgeChildThread = thread.forget();
+ }
+
+ RefPtr<ImageBridgeChild> child = new ImageBridgeChild(aNamespace);
+
+ child->GetThread()->Dispatch(NS_NewRunnableFunction(
+ "layers::ImageBridgeChild::Bind",
+ [child, endpoint = std::move(aEndpoint)]() mutable {
+ child->Bind(std::move(endpoint));
+ }));
+
+ // Assign this after so other threads can't post messages before we connect to
+ // IPDL.
+ {
+ StaticMutexAutoLock lock(sImageBridgeSingletonLock);
+ sImageBridgeChildSingleton = child;
+ }
+
+ return true;
+}
+
+bool ImageBridgeChild::ReinitForContent(Endpoint<PImageBridgeChild>&& aEndpoint,
+ uint32_t aNamespace) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // Note that at this point, ActorDestroy may not have been called yet,
+ // meaning mCanSend is still true. In this case we will try to send a
+ // synchronous WillClose message to the parent, and will certainly get a
+ // false result and a MsgDropped processing error. This is okay.
+ ShutdownSingleton();
+
+ return InitForContent(std::move(aEndpoint), aNamespace);
+}
+
+void ImageBridgeChild::Bind(Endpoint<PImageBridgeChild>&& aEndpoint) {
+ if (!aEndpoint.Bind(this)) {
+ return;
+ }
+
+ mCanSend = true;
+}
+
+void ImageBridgeChild::BindSameProcess(RefPtr<ImageBridgeParent> aParent) {
+ Open(aParent, aParent->GetThread(), mozilla::ipc::ChildSide);
+
+ mCanSend = true;
+}
+
+/* static */
+void ImageBridgeChild::ShutDown() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ ShutdownSingleton();
+
+ if (sImageBridgeChildThread) {
+ sImageBridgeChildThread->Shutdown();
+ sImageBridgeChildThread = nullptr;
+ }
+}
+
+/* static */
+void ImageBridgeChild::ShutdownSingleton() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (RefPtr<ImageBridgeChild> child = GetSingleton()) {
+ child->WillShutdown();
+
+ StaticMutexAutoLock lock(sImageBridgeSingletonLock);
+ sImageBridgeChildSingleton = nullptr;
+ }
+}
+
+void ImageBridgeChild::WillShutdown() {
+ {
+ SynchronousTask task("ImageBridge ShutdownStep1 lock");
+
+ RefPtr<Runnable> runnable =
+ WrapRunnable(RefPtr<ImageBridgeChild>(this),
+ &ImageBridgeChild::ShutdownStep1, &task);
+ GetThread()->Dispatch(runnable.forget());
+
+ task.Wait();
+ }
+
+ {
+ SynchronousTask task("ImageBridge ShutdownStep2 lock");
+
+ RefPtr<Runnable> runnable =
+ WrapRunnable(RefPtr<ImageBridgeChild>(this),
+ &ImageBridgeChild::ShutdownStep2, &task);
+ GetThread()->Dispatch(runnable.forget());
+
+ task.Wait();
+ }
+}
+
+void ImageBridgeChild::InitSameProcess(uint32_t aNamespace) {
+ NS_ASSERTION(NS_IsMainThread(), "Should be on the main Thread!");
+
+ MOZ_ASSERT(!sImageBridgeChildSingleton);
+ MOZ_ASSERT(!sImageBridgeChildThread);
+
+ nsCOMPtr<nsIThread> thread;
+ nsresult rv = NS_NewNamedThread("ImageBridgeChld", getter_AddRefs(thread));
+ MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv),
+ "Failed to start ImageBridgeChild thread!");
+ sImageBridgeChildThread = thread.forget();
+
+ RefPtr<ImageBridgeChild> child = new ImageBridgeChild(aNamespace);
+ RefPtr<ImageBridgeParent> parent = ImageBridgeParent::CreateSameProcess();
+
+ RefPtr<Runnable> runnable =
+ WrapRunnable(child, &ImageBridgeChild::BindSameProcess, parent);
+ child->GetThread()->Dispatch(runnable.forget());
+
+ // Assign this after so other threads can't post messages before we connect to
+ // IPDL.
+ {
+ StaticMutexAutoLock lock(sImageBridgeSingletonLock);
+ sImageBridgeChildSingleton = child;
+ }
+}
+
+/* static */
+void ImageBridgeChild::InitWithGPUProcess(
+ Endpoint<PImageBridgeChild>&& aEndpoint, uint32_t aNamespace) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(!sImageBridgeChildSingleton);
+ MOZ_ASSERT(!sImageBridgeChildThread);
+
+ nsCOMPtr<nsIThread> thread;
+ nsresult rv = NS_NewNamedThread("ImageBridgeChld", getter_AddRefs(thread));
+ MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv),
+ "Failed to start ImageBridgeChild thread!");
+ sImageBridgeChildThread = thread.forget();
+
+ RefPtr<ImageBridgeChild> child = new ImageBridgeChild(aNamespace);
+
+ child->GetThread()->Dispatch(NS_NewRunnableFunction(
+ "layers::ImageBridgeChild::Bind",
+ [child, endpoint = std::move(aEndpoint)]() mutable {
+ child->Bind(std::move(endpoint));
+ }));
+
+ // Assign this after so other threads can't post messages before we connect to
+ // IPDL.
+ {
+ StaticMutexAutoLock lock(sImageBridgeSingletonLock);
+ sImageBridgeChildSingleton = child;
+ }
+}
+
+bool InImageBridgeChildThread() {
+ return sImageBridgeChildThread &&
+ sImageBridgeChildThread->IsOnCurrentThread();
+}
+
+nsISerialEventTarget* ImageBridgeChild::GetThread() const {
+ return sImageBridgeChildThread;
+}
+
+/* static */
+void ImageBridgeChild::IdentifyCompositorTextureHost(
+ const TextureFactoryIdentifier& aIdentifier) {
+ if (RefPtr<ImageBridgeChild> child = GetSingleton()) {
+ child->UpdateTextureFactoryIdentifier(aIdentifier);
+ }
+}
+
+void ImageBridgeChild::UpdateTextureFactoryIdentifier(
+ const TextureFactoryIdentifier& aIdentifier) {
+ IdentifyTextureHost(aIdentifier);
+}
+
+RefPtr<ImageClient> ImageBridgeChild::CreateImageClient(
+ CompositableType aType, ImageContainer* aImageContainer) {
+ if (InImageBridgeChildThread()) {
+ return CreateImageClientNow(aType, aImageContainer);
+ }
+
+ SynchronousTask task("CreateImageClient Lock");
+
+ RefPtr<ImageClient> result = nullptr;
+
+ RefPtr<Runnable> runnable = WrapRunnable(
+ RefPtr<ImageBridgeChild>(this), &ImageBridgeChild::CreateImageClientSync,
+ &task, &result, aType, aImageContainer);
+ GetThread()->Dispatch(runnable.forget());
+
+ task.Wait();
+
+ return result;
+}
+
+RefPtr<ImageClient> ImageBridgeChild::CreateImageClientNow(
+ CompositableType aType, ImageContainer* aImageContainer) {
+ MOZ_ASSERT(InImageBridgeChildThread());
+ if (!CanSend()) {
+ return nullptr;
+ }
+
+ RefPtr<ImageClient> client =
+ ImageClient::CreateImageClient(aType, this, TextureFlags::NO_FLAGS);
+ MOZ_ASSERT(client, "failed to create ImageClient");
+ if (client) {
+ client->Connect(aImageContainer);
+ }
+ return client;
+}
+
+bool ImageBridgeChild::AllocUnsafeShmem(size_t aSize, ipc::Shmem* aShmem) {
+ if (!InImageBridgeChildThread()) {
+ return DispatchAllocShmemInternal(aSize, aShmem,
+ true); // true: unsafe
+ }
+
+ if (!CanSend()) {
+ return false;
+ }
+ return PImageBridgeChild::AllocUnsafeShmem(aSize, aShmem);
+}
+
+bool ImageBridgeChild::AllocShmem(size_t aSize, ipc::Shmem* aShmem) {
+ if (!InImageBridgeChildThread()) {
+ return DispatchAllocShmemInternal(aSize, aShmem,
+ false); // false: unsafe
+ }
+
+ if (!CanSend()) {
+ return false;
+ }
+ return PImageBridgeChild::AllocShmem(aSize, aShmem);
+}
+
+void ImageBridgeChild::ProxyAllocShmemNow(SynchronousTask* aTask, size_t aSize,
+ ipc::Shmem* aShmem, bool aUnsafe,
+ bool* aSuccess) {
+ AutoCompleteTask complete(aTask);
+
+ if (!CanSend()) {
+ return;
+ }
+
+ bool ok = false;
+ if (aUnsafe) {
+ ok = AllocUnsafeShmem(aSize, aShmem);
+ } else {
+ ok = AllocShmem(aSize, aShmem);
+ }
+ *aSuccess = ok;
+}
+
+bool ImageBridgeChild::DispatchAllocShmemInternal(size_t aSize,
+ ipc::Shmem* aShmem,
+ bool aUnsafe) {
+ SynchronousTask task("AllocatorProxy alloc");
+
+ bool success = false;
+ RefPtr<Runnable> runnable = WrapRunnable(
+ RefPtr<ImageBridgeChild>(this), &ImageBridgeChild::ProxyAllocShmemNow,
+ &task, aSize, aShmem, aUnsafe, &success);
+ GetThread()->Dispatch(runnable.forget());
+
+ task.Wait();
+
+ return success;
+}
+
+void ImageBridgeChild::ProxyDeallocShmemNow(SynchronousTask* aTask,
+ ipc::Shmem* aShmem, bool* aResult) {
+ AutoCompleteTask complete(aTask);
+
+ if (!CanSend()) {
+ return;
+ }
+ *aResult = DeallocShmem(*aShmem);
+}
+
+bool ImageBridgeChild::DeallocShmem(ipc::Shmem& aShmem) {
+ if (InImageBridgeChildThread()) {
+ if (!CanSend()) {
+ return false;
+ }
+ return PImageBridgeChild::DeallocShmem(aShmem);
+ }
+
+ // If we can't post a task, then we definitely cannot send, so there's
+ // no reason to queue up this send.
+ if (!CanPostTask()) {
+ return false;
+ }
+
+ SynchronousTask task("AllocatorProxy Dealloc");
+ bool result = false;
+
+ RefPtr<Runnable> runnable = WrapRunnable(
+ RefPtr<ImageBridgeChild>(this), &ImageBridgeChild::ProxyDeallocShmemNow,
+ &task, &aShmem, &result);
+ GetThread()->Dispatch(runnable.forget());
+
+ task.Wait();
+ return result;
+}
+
+PTextureChild* ImageBridgeChild::AllocPTextureChild(
+ const SurfaceDescriptor&, ReadLockDescriptor&, const LayersBackend&,
+ const TextureFlags&, const uint64_t& aSerial,
+ const wr::MaybeExternalImageId& aExternalImageId) {
+ MOZ_ASSERT(CanSend());
+ return TextureClient::CreateIPDLActor();
+}
+
+bool ImageBridgeChild::DeallocPTextureChild(PTextureChild* actor) {
+ return TextureClient::DestroyIPDLActor(actor);
+}
+
+PMediaSystemResourceManagerChild*
+ImageBridgeChild::AllocPMediaSystemResourceManagerChild() {
+ MOZ_ASSERT(CanSend());
+ return new mozilla::media::MediaSystemResourceManagerChild();
+}
+
+bool ImageBridgeChild::DeallocPMediaSystemResourceManagerChild(
+ PMediaSystemResourceManagerChild* aActor) {
+ MOZ_ASSERT(aActor);
+ delete static_cast<mozilla::media::MediaSystemResourceManagerChild*>(aActor);
+ return true;
+}
+
+mozilla::ipc::IPCResult ImageBridgeChild::RecvParentAsyncMessages(
+ nsTArray<AsyncParentMessageData>&& aMessages) {
+ for (AsyncParentMessageArray::index_type i = 0; i < aMessages.Length(); ++i) {
+ const AsyncParentMessageData& message = aMessages[i];
+
+ switch (message.type()) {
+ case AsyncParentMessageData::TOpNotifyNotUsed: {
+ const OpNotifyNotUsed& op = message.get_OpNotifyNotUsed();
+ NotifyNotUsed(op.TextureId(), op.fwdTransactionId());
+ break;
+ }
+ default:
+ NS_ERROR("unknown AsyncParentMessageData type");
+ return IPC_FAIL_NO_REASON(this);
+ }
+ }
+ return IPC_OK();
+}
+
+RefPtr<ImageContainerListener> ImageBridgeChild::FindListener(
+ const CompositableHandle& aHandle) {
+ RefPtr<ImageContainerListener> listener;
+ MutexAutoLock lock(mContainerMapLock);
+ auto it = mImageContainerListeners.find(aHandle.Value());
+ if (it != mImageContainerListeners.end()) {
+ listener = it->second;
+ }
+ return listener;
+}
+
+mozilla::ipc::IPCResult ImageBridgeChild::RecvDidComposite(
+ nsTArray<ImageCompositeNotification>&& aNotifications) {
+ for (auto& n : aNotifications) {
+ RefPtr<ImageContainerListener> listener = FindListener(n.compositable());
+ if (listener) {
+ listener->NotifyComposite(n);
+ }
+ }
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult ImageBridgeChild::RecvReportFramesDropped(
+ const CompositableHandle& aHandle, const uint32_t& aFrames) {
+ RefPtr<ImageContainerListener> listener = FindListener(aHandle);
+ if (listener) {
+ listener->NotifyDropped(aFrames);
+ }
+
+ return IPC_OK();
+}
+
+PTextureChild* ImageBridgeChild::CreateTexture(
+ const SurfaceDescriptor& aSharedData, ReadLockDescriptor&& aReadLock,
+ LayersBackend aLayersBackend, TextureFlags aFlags,
+ const dom::ContentParentId& aContentId, uint64_t aSerial,
+ wr::MaybeExternalImageId& aExternalImageId) {
+ MOZ_ASSERT(CanSend());
+ return SendPTextureConstructor(aSharedData, std::move(aReadLock),
+ aLayersBackend, aFlags, aSerial,
+ aExternalImageId);
+}
+
+static bool IBCAddOpDestroy(CompositableTransaction* aTxn,
+ const OpDestroy& op) {
+ if (aTxn->Finished()) {
+ return false;
+ }
+
+ aTxn->mDestroyedActors.AppendElement(op);
+ return true;
+}
+
+bool ImageBridgeChild::DestroyInTransaction(PTextureChild* aTexture) {
+ return IBCAddOpDestroy(mTxn, OpDestroy(WrapNotNull(aTexture)));
+}
+
+bool ImageBridgeChild::DestroyInTransaction(const CompositableHandle& aHandle) {
+ return IBCAddOpDestroy(mTxn, OpDestroy(aHandle));
+}
+
+void ImageBridgeChild::RemoveTextureFromCompositable(
+ CompositableClient* aCompositable, TextureClient* aTexture) {
+ MOZ_ASSERT(CanSend());
+ MOZ_ASSERT(aTexture);
+ MOZ_ASSERT(aTexture->IsSharedWithCompositor());
+ MOZ_ASSERT(aCompositable->IsConnected());
+ if (!aTexture || !aTexture->IsSharedWithCompositor() ||
+ !aCompositable->IsConnected()) {
+ return;
+ }
+
+ mTxn->AddNoSwapEdit(CompositableOperation(
+ aCompositable->GetIPCHandle(),
+ OpRemoveTexture(WrapNotNull(aTexture->GetIPDLActor()))));
+}
+
+bool ImageBridgeChild::IsSameProcess() const {
+ return OtherPid() == base::GetCurrentProcId();
+}
+
+bool ImageBridgeChild::CanPostTask() const {
+ // During shutdown, the cycle collector may free objects that are holding a
+ // reference to ImageBridgeChild. Since this happens on the main thread,
+ // ImageBridgeChild will attempt to post a task to the ImageBridge thread.
+ // However the thread manager has already been shut down, so the task cannot
+ // post.
+ //
+ // It's okay if this races. We only care about the shutdown case where
+ // everything's happening on the main thread. Even if it races outside of
+ // shutdown, it's still harmless to post the task, since the task must
+ // check CanSend().
+ return !mDestroyed;
+}
+
+void ImageBridgeChild::ReleaseCompositable(const CompositableHandle& aHandle) {
+ if (!InImageBridgeChildThread()) {
+ // If we can't post a task, then we definitely cannot send, so there's
+ // no reason to queue up this send.
+ if (!CanPostTask()) {
+ return;
+ }
+
+ RefPtr<Runnable> runnable =
+ WrapRunnable(RefPtr<ImageBridgeChild>(this),
+ &ImageBridgeChild::ReleaseCompositable, aHandle);
+ GetThread()->Dispatch(runnable.forget());
+ return;
+ }
+
+ if (!CanSend()) {
+ return;
+ }
+
+ if (!DestroyInTransaction(aHandle)) {
+ SendReleaseCompositable(aHandle);
+ }
+
+ {
+ MutexAutoLock lock(mContainerMapLock);
+ mImageContainerListeners.erase(aHandle.Value());
+ }
+}
+
+bool ImageBridgeChild::CanSend() const {
+ MOZ_ASSERT(InImageBridgeChildThread());
+ return mCanSend;
+}
+
+void ImageBridgeChild::HandleFatalError(const char* aMsg) {
+ dom::ContentChild::FatalErrorIfNotUsingGPUProcess(aMsg, OtherPid());
+}
+
+wr::MaybeExternalImageId ImageBridgeChild::GetNextExternalImageId() {
+ static uint32_t sNextID = 1;
+ ++sNextID;
+ MOZ_RELEASE_ASSERT(sNextID != UINT32_MAX);
+
+ uint64_t imageId = mNamespace;
+ imageId = imageId << 32 | sNextID;
+ return Some(wr::ToExternalImageId(imageId));
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/ipc/ImageBridgeChild.h b/gfx/layers/ipc/ImageBridgeChild.h
new file mode 100644
index 0000000000..ee8ff97299
--- /dev/null
+++ b/gfx/layers/ipc/ImageBridgeChild.h
@@ -0,0 +1,376 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_GFX_IMAGEBRIDGECHILD_H
+#define MOZILLA_GFX_IMAGEBRIDGECHILD_H
+
+#include <stddef.h> // for size_t
+#include <stdint.h> // for uint32_t, uint64_t
+#include <unordered_map>
+
+#include "mozilla/Attributes.h" // for override
+#include "mozilla/Atomics.h"
+#include "mozilla/RefPtr.h" // for already_AddRefed
+#include "mozilla/ipc/SharedMemory.h" // for SharedMemory, etc
+#include "mozilla/layers/CompositableForwarder.h"
+#include "mozilla/layers/CompositorTypes.h"
+#include "mozilla/layers/PImageBridgeChild.h"
+#include "mozilla/layers/TextureForwarder.h"
+#include "mozilla/Mutex.h"
+#include "mozilla/webrender/WebRenderTypes.h"
+#include "nsRegion.h" // for nsIntRegion
+#include "mozilla/gfx/Rect.h"
+#include "mozilla/ReentrantMonitor.h" // for ReentrantMonitor, etc
+
+namespace mozilla {
+namespace ipc {
+class Shmem;
+} // namespace ipc
+
+namespace layers {
+
+class ImageClient;
+class ImageContainer;
+class ImageContainerListener;
+class ImageBridgeParent;
+class CompositableClient;
+struct CompositableTransaction;
+class Image;
+class TextureClient;
+class SynchronousTask;
+
+/**
+ * Returns true if the current thread is the ImageBrdigeChild's thread.
+ *
+ * Can be called from any thread.
+ */
+bool InImageBridgeChildThread();
+
+/**
+ * The ImageBridge protocol is meant to allow ImageContainers to forward images
+ * directly to the compositor thread/process without using the main thread.
+ *
+ * ImageBridgeChild is a CompositableForwarder just like ShadowLayerForwarder.
+ * This means it also does transactions with the compositor thread/process,
+ * except that the transactions are restricted to operations on the
+ * Compositables and cannot contain messages affecting layers directly.
+ *
+ * ImageBridgeChild is also a ISurfaceAllocator. It can be used to allocate or
+ * deallocate data that is shared with the compositor. The main differerence
+ * with other ISurfaceAllocators is that some of its overriden methods can be
+ * invoked from any thread.
+ *
+ * There are three important phases in the ImageBridge protocol. These three
+ * steps can do different things depending if (A) the ImageContainer uses
+ * ImageBridge or (B) it does not use ImageBridge:
+ *
+ * - When an ImageContainer calls its method SetCurrentImage:
+ * - (A) The image is sent directly to the compositor process through the
+ * ImageBridge IPDL protocol.
+ * On the compositor side the image is stored in a global table that
+ * associates the image with an ID corresponding to the ImageContainer, and a
+ * composition is triggered.
+ * - (B) Since it does not have an ImageBridge, the image is not sent yet.
+ * instead the will be sent to the compositor during the next layer
+ * transaction (on the main thread).
+ *
+ * - During a Layer transaction:
+ * - (A) The ImageContainer uses ImageBridge. The image is already available
+ * to the compositor process because it has been sent with SetCurrentImage.
+ * Yet, the CompositableHost on the compositor side will needs the ID
+ * referring to the ImageContainer to access the Image. So during the Swap
+ * operation that happens in the transaction, we swap the container ID rather
+ * than the image data.
+ * - (B) Since the ImageContainer does not use ImageBridge, the image data is
+ * swaped.
+ *
+ * - During composition:
+ * - (A) The CompositableHost has an AsyncID, it looks up the ID in the
+ * global table to see if there is an image. If there is no image, nothing is
+ * rendered.
+ * - (B) The CompositableHost has image data rather than an ID (meaning it is
+ * not using ImageBridge), then it just composites the image data normally.
+ *
+ * This means that there might be a possibility for the ImageBridge to send the
+ * first frame before the first layer transaction that will pass the container
+ * ID to the CompositableHost happens. In this (unlikely) case the layer is not
+ * composited until the layer transaction happens. This means this scenario is
+ * not harmful.
+ *
+ * Since sending an image through imageBridge triggers compositing, the main
+ * thread is not used at all (except for the very first transaction that
+ * provides the CompositableHost with an AsyncID).
+ */
+class ImageBridgeChild final : public PImageBridgeChild,
+ public CompositableForwarder,
+ public TextureForwarder {
+ friend class ImageContainer;
+
+ typedef nsTArray<AsyncParentMessageData> AsyncParentMessageArray;
+
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(ImageBridgeChild, override);
+
+ TextureForwarder* GetTextureForwarder() override { return this; }
+ LayersIPCActor* GetLayersIPCActor() override { return this; }
+
+ /**
+ * Creates the image bridge with a dedicated thread for ImageBridgeChild.
+ *
+ * We may want to use a specifi thread in the future. In this case, use
+ * CreateWithThread instead.
+ */
+ static void InitSameProcess(uint32_t aNamespace);
+
+ static void InitWithGPUProcess(Endpoint<PImageBridgeChild>&& aEndpoint,
+ uint32_t aNamespace);
+ static bool InitForContent(Endpoint<PImageBridgeChild>&& aEndpoint,
+ uint32_t aNamespace);
+ static bool ReinitForContent(Endpoint<PImageBridgeChild>&& aEndpoint,
+ uint32_t aNamespace);
+
+ /**
+ * Destroys the image bridge by calling DestroyBridge, and destroys the
+ * ImageBridge's thread.
+ *
+ * If you don't want to destroy the thread, call DestroyBridge directly
+ * instead.
+ */
+ static void ShutDown();
+
+ /**
+ * returns the singleton instance.
+ *
+ * can be called from any thread.
+ */
+ static RefPtr<ImageBridgeChild> GetSingleton();
+
+ static void IdentifyCompositorTextureHost(
+ const TextureFactoryIdentifier& aIdentifier);
+
+ void BeginTransaction();
+ void EndTransaction();
+
+ /**
+ * Returns the ImageBridgeChild's thread.
+ *
+ * Can be called from any thread.
+ */
+ nsISerialEventTarget* GetThread() const override;
+
+ base::ProcessId GetParentPid() const override { return OtherPid(); }
+
+ PTextureChild* AllocPTextureChild(
+ const SurfaceDescriptor& aSharedData, ReadLockDescriptor& aReadLock,
+ const LayersBackend& aLayersBackend, const TextureFlags& aFlags,
+ const uint64_t& aSerial,
+ const wr::MaybeExternalImageId& aExternalImageId);
+
+ bool DeallocPTextureChild(PTextureChild* actor);
+
+ PMediaSystemResourceManagerChild* AllocPMediaSystemResourceManagerChild();
+ bool DeallocPMediaSystemResourceManagerChild(
+ PMediaSystemResourceManagerChild* aActor);
+
+ mozilla::ipc::IPCResult RecvParentAsyncMessages(
+ nsTArray<AsyncParentMessageData>&& aMessages);
+
+ mozilla::ipc::IPCResult RecvDidComposite(
+ nsTArray<ImageCompositeNotification>&& aNotifications);
+
+ mozilla::ipc::IPCResult RecvReportFramesDropped(
+ const CompositableHandle& aHandle, const uint32_t& aFrames);
+
+ // Create an ImageClient from any thread.
+ RefPtr<ImageClient> CreateImageClient(CompositableType aType,
+ ImageContainer* aImageContainer);
+
+ // Create an ImageClient from the ImageBridge thread.
+ RefPtr<ImageClient> CreateImageClientNow(CompositableType aType,
+ ImageContainer* aImageContainer);
+
+ void UpdateImageClient(RefPtr<ImageContainer> aContainer);
+
+ void UpdateCompositable(const RefPtr<ImageContainer> aContainer,
+ const RemoteTextureId aTextureId,
+ const RemoteTextureOwnerId aOwnerId,
+ const gfx::IntSize aSize, const TextureFlags aFlags);
+
+ /**
+ * Flush all Images sent to CompositableHost.
+ */
+ void FlushAllImages(ImageClient* aClient, ImageContainer* aContainer);
+
+ bool IPCOpen() const override { return mCanSend; }
+
+ private:
+ /**
+ * This must be called by the static function DeleteImageBridgeSync defined
+ * in ImageBridgeChild.cpp ONLY.
+ */
+ virtual ~ImageBridgeChild();
+
+ // Helpers for dispatching.
+ void CreateImageClientSync(SynchronousTask* aTask,
+ RefPtr<ImageClient>* result,
+ CompositableType aType,
+ ImageContainer* aImageContainer);
+
+ void FlushAllImagesSync(SynchronousTask* aTask, ImageClient* aClient,
+ ImageContainer* aContainer);
+
+ void ProxyAllocShmemNow(SynchronousTask* aTask, size_t aSize,
+ mozilla::ipc::Shmem* aShmem, bool aUnsafe,
+ bool* aSuccess);
+ void ProxyDeallocShmemNow(SynchronousTask* aTask, mozilla::ipc::Shmem* aShmem,
+ bool* aResult);
+
+ void UpdateTextureFactoryIdentifier(
+ const TextureFactoryIdentifier& aIdentifier);
+
+ public:
+ // CompositableForwarder
+
+ void Connect(CompositableClient* aCompositable,
+ ImageContainer* aImageContainer) override;
+
+ bool UsesImageBridge() const override { return true; }
+
+ /**
+ * See CompositableForwarder::UseTextures
+ */
+ void UseTextures(CompositableClient* aCompositable,
+ const nsTArray<TimedTextureClient>& aTextures) override;
+
+ void UseRemoteTexture(CompositableClient* aCompositable,
+ const RemoteTextureId aTextureId,
+ const RemoteTextureOwnerId aOwnerId,
+ const gfx::IntSize aSize,
+ const TextureFlags aFlags) override;
+
+ void EnableRemoteTexturePushCallback(CompositableClient* aCompositable,
+ const RemoteTextureOwnerId aOwnerId,
+ const gfx::IntSize aSize,
+ const TextureFlags aFlags) override;
+
+ void ReleaseCompositable(const CompositableHandle& aHandle) override;
+
+ void ForgetImageContainer(const CompositableHandle& aHandle);
+
+ /**
+ * Hold TextureClient ref until end of usage on host side if
+ * TextureFlags::RECYCLE is set. Host side's usage is checked via
+ * CompositableRef.
+ */
+ void HoldUntilCompositableRefReleasedIfNecessary(TextureClient* aClient);
+
+ /**
+ * Notify id of Texture When host side end its use. Transaction id is used to
+ * make sure if there is no newer usage.
+ */
+ void NotifyNotUsed(uint64_t aTextureId, uint64_t aFwdTransactionId);
+
+ void CancelWaitForNotifyNotUsed(uint64_t aTextureId) override;
+
+ bool DestroyInTransaction(PTextureChild* aTexture) override;
+ bool DestroyInTransaction(const CompositableHandle& aHandle);
+
+ void RemoveTextureFromCompositable(CompositableClient* aCompositable,
+ TextureClient* aTexture) override;
+
+ // ISurfaceAllocator
+
+ /**
+ * See ISurfaceAllocator.h
+ * Can be used from any thread.
+ * If used outside the ImageBridgeChild thread, it will proxy a synchronous
+ * call on the ImageBridgeChild thread.
+ */
+ bool AllocUnsafeShmem(size_t aSize, mozilla::ipc::Shmem* aShmem) override;
+ bool AllocShmem(size_t aSize, mozilla::ipc::Shmem* aShmem) override;
+
+ /**
+ * See ISurfaceAllocator.h
+ * Can be used from any thread.
+ * If used outside the ImageBridgeChild thread, it will proxy a synchronous
+ * call on the ImageBridgeChild thread.
+ */
+ bool DeallocShmem(mozilla::ipc::Shmem& aShmem) override;
+
+ PTextureChild* CreateTexture(
+ const SurfaceDescriptor& aSharedData, ReadLockDescriptor&& aReadLock,
+ LayersBackend aLayersBackend, TextureFlags aFlags,
+ const dom::ContentParentId& aContentId, uint64_t aSerial,
+ wr::MaybeExternalImageId& aExternalImageId) override;
+
+ bool IsSameProcess() const override;
+
+ void UpdateFwdTransactionId() override { ++mFwdTransactionId; }
+ uint64_t GetFwdTransactionId() override { return mFwdTransactionId; }
+
+ bool InForwarderThread() override { return InImageBridgeChildThread(); }
+
+ void HandleFatalError(const char* aMsg) override;
+
+ wr::MaybeExternalImageId GetNextExternalImageId() override;
+
+ protected:
+ explicit ImageBridgeChild(uint32_t aNamespace);
+ bool DispatchAllocShmemInternal(size_t aSize, Shmem* aShmem, bool aUnsafe);
+
+ void Bind(Endpoint<PImageBridgeChild>&& aEndpoint);
+ void BindSameProcess(RefPtr<ImageBridgeParent> aParent);
+
+ void SendImageBridgeThreadId();
+
+ void WillShutdown();
+ void ShutdownStep1(SynchronousTask* aTask);
+ void ShutdownStep2(SynchronousTask* aTask);
+ void MarkShutDown();
+
+ void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ bool CanSend() const;
+ bool CanPostTask() const;
+
+ static void ShutdownSingleton();
+
+ private:
+ uint32_t mNamespace;
+
+ CompositableTransaction* mTxn;
+
+ bool mCanSend;
+ mozilla::Atomic<bool> mDestroyed;
+
+ /**
+ * Transaction id of CompositableForwarder.
+ * It is incrementaed by UpdateFwdTransactionId() in each BeginTransaction()
+ * call.
+ */
+ uint64_t mFwdTransactionId;
+
+ /**
+ * Hold TextureClients refs until end of their usages on host side.
+ * It defer calling of TextureClient recycle callback.
+ */
+ std::unordered_map<uint64_t, RefPtr<TextureClient>>
+ mTexturesWaitingNotifyNotUsed;
+
+ /**
+ * Mapping from async compositable IDs to image containers.
+ */
+ Mutex mContainerMapLock MOZ_UNANNOTATED;
+ std::unordered_map<uint64_t, RefPtr<ImageContainerListener>>
+ mImageContainerListeners;
+ RefPtr<ImageContainerListener> FindListener(
+ const CompositableHandle& aHandle);
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif
diff --git a/gfx/layers/ipc/ImageBridgeParent.cpp b/gfx/layers/ipc/ImageBridgeParent.cpp
new file mode 100644
index 0000000000..bd9b2f6dd7
--- /dev/null
+++ b/gfx/layers/ipc/ImageBridgeParent.cpp
@@ -0,0 +1,428 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ImageBridgeParent.h"
+#include <stdint.h> // for uint64_t, uint32_t
+#include "CompositableHost.h" // for CompositableParent, Create
+#include "base/process.h" // for ProcessId
+#include "base/task.h" // for CancelableTask, DeleteTask, etc
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/gfx/Point.h" // for IntSize
+#include "mozilla/Hal.h" // for hal::SetCurrentThreadPriority()
+#include "mozilla/HalTypes.h" // for hal::THREAD_PRIORITY_COMPOSITOR
+#include "mozilla/ipc/Endpoint.h"
+#include "mozilla/ipc/MessageChannel.h" // for MessageChannel, etc
+#include "mozilla/media/MediaSystemResourceManagerParent.h" // for MediaSystemResourceManagerParent
+#include "mozilla/layers/BufferTexture.h"
+#include "mozilla/layers/CompositableTransactionParent.h"
+#include "mozilla/layers/LayersMessages.h" // for EditReply
+#include "mozilla/layers/PImageBridgeParent.h"
+#include "mozilla/layers/TextureHostOGL.h" // for TextureHostOGL
+#include "mozilla/layers/Compositor.h"
+#include "mozilla/Monitor.h"
+#include "mozilla/mozalloc.h" // for operator new, etc
+#include "mozilla/ProfilerLabels.h"
+#include "mozilla/ProfilerMarkers.h"
+#include "mozilla/Unused.h"
+#include "nsDebug.h" // for NS_ASSERTION, etc
+#include "nsISupportsImpl.h" // for ImageBridgeParent::Release, etc
+#include "nsTArray.h" // for nsTArray, nsTArray_Impl
+#include "nsTArrayForwardDeclare.h" // for nsTArray
+#include "nsXULAppAPI.h" // for XRE_GetIOMessageLoop
+#include "mozilla/layers/TextureHost.h"
+#include "nsThreadUtils.h"
+
+#if defined(OS_WIN)
+# include "mozilla/layers/TextureD3D11.h"
+#endif
+
+namespace mozilla {
+namespace layers {
+
+using namespace mozilla::ipc;
+using namespace mozilla::gfx;
+using namespace mozilla::media;
+
+ImageBridgeParent::ImageBridgeMap ImageBridgeParent::sImageBridges;
+
+StaticAutoPtr<mozilla::Monitor> sImageBridgesLock;
+
+static StaticRefPtr<ImageBridgeParent> sImageBridgeParentSingleton;
+
+/* static */
+void ImageBridgeParent::Setup() {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (!sImageBridgesLock) {
+ sImageBridgesLock = new Monitor("ImageBridges");
+ mozilla::ClearOnShutdown(&sImageBridgesLock);
+ }
+}
+
+ImageBridgeParent::ImageBridgeParent(nsISerialEventTarget* aThread,
+ ProcessId aChildProcessId,
+ dom::ContentParentId aContentId)
+ : mThread(aThread),
+ mContentId(aContentId),
+ mClosed(false),
+ mCompositorThreadHolder(CompositorThreadHolder::GetSingleton()) {
+ MOZ_ASSERT(NS_IsMainThread());
+ SetOtherProcessId(aChildProcessId);
+}
+
+ImageBridgeParent::~ImageBridgeParent() = default;
+
+/* static */
+ImageBridgeParent* ImageBridgeParent::CreateSameProcess() {
+ base::ProcessId pid = base::GetCurrentProcId();
+ RefPtr<ImageBridgeParent> parent =
+ new ImageBridgeParent(CompositorThread(), pid, dom::ContentParentId());
+
+ {
+ MonitorAutoLock lock(*sImageBridgesLock);
+ MOZ_RELEASE_ASSERT(sImageBridges.count(pid) == 0);
+ sImageBridges[pid] = parent;
+ }
+
+ sImageBridgeParentSingleton = parent;
+ return parent;
+}
+
+/* static */
+bool ImageBridgeParent::CreateForGPUProcess(
+ Endpoint<PImageBridgeParent>&& aEndpoint) {
+ MOZ_ASSERT(XRE_GetProcessType() == GeckoProcessType_GPU);
+
+ nsCOMPtr<nsISerialEventTarget> compositorThread = CompositorThread();
+ if (!compositorThread) {
+ return false;
+ }
+
+ RefPtr<ImageBridgeParent> parent = new ImageBridgeParent(
+ compositorThread, aEndpoint.OtherPid(), dom::ContentParentId());
+
+ compositorThread->Dispatch(NewRunnableMethod<Endpoint<PImageBridgeParent>&&>(
+ "layers::ImageBridgeParent::Bind", parent, &ImageBridgeParent::Bind,
+ std::move(aEndpoint)));
+
+ sImageBridgeParentSingleton = parent;
+ return true;
+}
+
+/* static */
+void ImageBridgeParent::ShutdownInternal() {
+ // We make a copy because we don't want to hold the lock while closing and we
+ // don't want the object to get freed underneath us.
+ nsTArray<RefPtr<ImageBridgeParent>> actors;
+ {
+ MonitorAutoLock lock(*sImageBridgesLock);
+ for (const auto& iter : sImageBridges) {
+ actors.AppendElement(iter.second);
+ }
+ }
+
+ for (auto const& actor : actors) {
+ MOZ_RELEASE_ASSERT(!actor->mClosed);
+ actor->Close();
+ }
+
+ sImageBridgeParentSingleton = nullptr;
+}
+
+/* static */
+void ImageBridgeParent::Shutdown() {
+ CompositorThread()->Dispatch(NS_NewRunnableFunction(
+ "ImageBridgeParent::Shutdown",
+ []() -> void { ImageBridgeParent::ShutdownInternal(); }));
+}
+
+void ImageBridgeParent::ActorDestroy(ActorDestroyReason aWhy) {
+ // Can't alloc/dealloc shmems from now on.
+ mClosed = true;
+
+ for (const auto& entry : mCompositables) {
+ entry.second->OnReleased();
+ }
+ mCompositables.clear();
+ {
+ MonitorAutoLock lock(*sImageBridgesLock);
+ sImageBridges.erase(OtherPid());
+ }
+ GetThread()->Dispatch(
+ NewRunnableMethod("layers::ImageBridgeParent::DeferredDestroy", this,
+ &ImageBridgeParent::DeferredDestroy));
+
+ // It is very important that this method gets called at shutdown (be it a
+ // clean or an abnormal shutdown), because DeferredDestroy is what clears
+ // mCompositorThreadHolder. If mCompositorThreadHolder is not null and
+ // ActorDestroy is not called, the ImageBridgeParent is leaked which causes
+ // the CompositorThreadHolder to be leaked and CompsoitorParent's shutdown
+ // ends up spinning the event loop forever, waiting for the compositor thread
+ // to terminate.
+}
+
+class MOZ_STACK_CLASS AutoImageBridgeParentAsyncMessageSender final {
+ public:
+ explicit AutoImageBridgeParentAsyncMessageSender(
+ ImageBridgeParent* aImageBridge,
+ nsTArray<OpDestroy>* aToDestroy = nullptr)
+ : mImageBridge(aImageBridge), mToDestroy(aToDestroy) {
+ mImageBridge->SetAboutToSendAsyncMessages();
+ }
+
+ ~AutoImageBridgeParentAsyncMessageSender() {
+ mImageBridge->SendPendingAsyncMessages();
+ if (mToDestroy) {
+ for (const auto& op : *mToDestroy) {
+ mImageBridge->DestroyActor(op);
+ }
+ }
+ }
+
+ private:
+ ImageBridgeParent* mImageBridge;
+ nsTArray<OpDestroy>* mToDestroy;
+};
+
+mozilla::ipc::IPCResult ImageBridgeParent::RecvUpdate(
+ EditArray&& aEdits, OpDestroyArray&& aToDestroy,
+ const uint64_t& aFwdTransactionId) {
+ AUTO_PROFILER_TRACING_MARKER("Paint", "ImageBridgeTransaction", GRAPHICS);
+ AUTO_PROFILER_LABEL("ImageBridgeParent::RecvUpdate", GRAPHICS);
+
+ // This ensures that destroy operations are always processed. It is not safe
+ // to early-return from RecvUpdate without doing so.
+ AutoImageBridgeParentAsyncMessageSender autoAsyncMessageSender(this,
+ &aToDestroy);
+ UpdateFwdTransactionId(aFwdTransactionId);
+
+ for (const auto& edit : aEdits) {
+ RefPtr<CompositableHost> compositable =
+ FindCompositable(edit.compositable());
+ if (!compositable ||
+ !ReceiveCompositableUpdate(edit.detail(), WrapNotNull(compositable),
+ edit.compositable())) {
+ return IPC_FAIL_NO_REASON(this);
+ }
+ uint32_t dropped = compositable->GetDroppedFrames();
+ if (dropped) {
+ Unused << SendReportFramesDropped(edit.compositable(), dropped);
+ }
+ }
+
+ return IPC_OK();
+}
+
+/* static */
+bool ImageBridgeParent::CreateForContent(
+ Endpoint<PImageBridgeParent>&& aEndpoint, dom::ContentParentId aContentId) {
+ nsCOMPtr<nsISerialEventTarget> compositorThread = CompositorThread();
+ if (!compositorThread) {
+ return false;
+ }
+
+ RefPtr<ImageBridgeParent> bridge =
+ new ImageBridgeParent(compositorThread, aEndpoint.OtherPid(), aContentId);
+ compositorThread->Dispatch(NewRunnableMethod<Endpoint<PImageBridgeParent>&&>(
+ "layers::ImageBridgeParent::Bind", bridge, &ImageBridgeParent::Bind,
+ std::move(aEndpoint)));
+
+ return true;
+}
+
+void ImageBridgeParent::Bind(Endpoint<PImageBridgeParent>&& aEndpoint) {
+ if (!aEndpoint.Bind(this)) return;
+
+ // If the child process ID was reused by the OS before the ImageBridgeParent
+ // object was destroyed, we need to clean it up first.
+ RefPtr<ImageBridgeParent> oldActor;
+ {
+ MonitorAutoLock lock(*sImageBridgesLock);
+ ImageBridgeMap::const_iterator i = sImageBridges.find(OtherPid());
+ if (i != sImageBridges.end()) {
+ oldActor = i->second;
+ }
+ }
+
+ // We can't hold the lock during Close because it erases itself from the map.
+ if (oldActor) {
+ MOZ_RELEASE_ASSERT(!oldActor->mClosed);
+ oldActor->Close();
+ }
+
+ {
+ MonitorAutoLock lock(*sImageBridgesLock);
+ sImageBridges[OtherPid()] = this;
+ }
+}
+
+mozilla::ipc::IPCResult ImageBridgeParent::RecvWillClose() {
+ // If there is any texture still alive we have to force it to deallocate the
+ // device data (GL textures, etc.) now because shortly after SenStop() returns
+ // on the child side the widget will be destroyed along with it's associated
+ // GL context.
+ nsTArray<PTextureParent*> textures;
+ ManagedPTextureParent(textures);
+ for (unsigned int i = 0; i < textures.Length(); ++i) {
+ RefPtr<TextureHost> tex = TextureHost::AsTextureHost(textures[i]);
+ tex->DeallocateDeviceData();
+ }
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult ImageBridgeParent::RecvNewCompositable(
+ const CompositableHandle& aHandle, const TextureInfo& aInfo) {
+ RefPtr<CompositableHost> host = AddCompositable(aHandle, aInfo);
+ if (!host) {
+ return IPC_FAIL_NO_REASON(this);
+ }
+
+ host->SetAsyncRef(AsyncCompositableRef(OtherPid(), aHandle));
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult ImageBridgeParent::RecvReleaseCompositable(
+ const CompositableHandle& aHandle) {
+ ReleaseCompositable(aHandle);
+ return IPC_OK();
+}
+
+PTextureParent* ImageBridgeParent::AllocPTextureParent(
+ const SurfaceDescriptor& aSharedData, ReadLockDescriptor& aReadLock,
+ const LayersBackend& aLayersBackend, const TextureFlags& aFlags,
+ const uint64_t& aSerial, const wr::MaybeExternalImageId& aExternalImageId) {
+ return TextureHost::CreateIPDLActor(this, aSharedData, std::move(aReadLock),
+ aLayersBackend, aFlags, mContentId,
+ aSerial, aExternalImageId);
+}
+
+bool ImageBridgeParent::DeallocPTextureParent(PTextureParent* actor) {
+ return TextureHost::DestroyIPDLActor(actor);
+}
+
+PMediaSystemResourceManagerParent*
+ImageBridgeParent::AllocPMediaSystemResourceManagerParent() {
+ return new mozilla::media::MediaSystemResourceManagerParent();
+}
+
+bool ImageBridgeParent::DeallocPMediaSystemResourceManagerParent(
+ PMediaSystemResourceManagerParent* aActor) {
+ MOZ_ASSERT(aActor);
+ delete static_cast<mozilla::media::MediaSystemResourceManagerParent*>(aActor);
+ return true;
+}
+
+void ImageBridgeParent::SendAsyncMessage(
+ const nsTArray<AsyncParentMessageData>& aMessage) {
+ mozilla::Unused << SendParentAsyncMessages(aMessage);
+}
+
+class ProcessIdComparator {
+ public:
+ bool Equals(const ImageCompositeNotificationInfo& aA,
+ const ImageCompositeNotificationInfo& aB) const {
+ return aA.mImageBridgeProcessId == aB.mImageBridgeProcessId;
+ }
+ bool LessThan(const ImageCompositeNotificationInfo& aA,
+ const ImageCompositeNotificationInfo& aB) const {
+ return aA.mImageBridgeProcessId < aB.mImageBridgeProcessId;
+ }
+};
+
+/* static */
+bool ImageBridgeParent::NotifyImageComposites(
+ nsTArray<ImageCompositeNotificationInfo>& aNotifications) {
+ // Group the notifications by destination process ID and then send the
+ // notifications in one message per group.
+ aNotifications.Sort(ProcessIdComparator());
+ uint32_t i = 0;
+ bool ok = true;
+ while (i < aNotifications.Length()) {
+ AutoTArray<ImageCompositeNotification, 1> notifications;
+ notifications.AppendElement(aNotifications[i].mNotification);
+ uint32_t end = i + 1;
+ MOZ_ASSERT(aNotifications[i].mNotification.compositable());
+ ProcessId pid = aNotifications[i].mImageBridgeProcessId;
+ while (end < aNotifications.Length() &&
+ aNotifications[end].mImageBridgeProcessId == pid) {
+ notifications.AppendElement(aNotifications[end].mNotification);
+ ++end;
+ }
+ RefPtr<ImageBridgeParent> bridge = GetInstance(pid);
+ if (!bridge || bridge->mClosed) {
+ i = end;
+ continue;
+ }
+ bridge->SendPendingAsyncMessages();
+ if (!bridge->SendDidComposite(notifications)) {
+ ok = false;
+ }
+ i = end;
+ }
+ return ok;
+}
+
+void ImageBridgeParent::DeferredDestroy() { mCompositorThreadHolder = nullptr; }
+
+already_AddRefed<ImageBridgeParent> ImageBridgeParent::GetInstance(
+ ProcessId aId) {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+ MonitorAutoLock lock(*sImageBridgesLock);
+ ImageBridgeMap::const_iterator i = sImageBridges.find(aId);
+ if (i == sImageBridges.end()) {
+ NS_WARNING("Cannot find image bridge for process!");
+ return nullptr;
+ }
+ RefPtr<ImageBridgeParent> bridge = i->second;
+ return bridge.forget();
+}
+
+bool ImageBridgeParent::AllocShmem(size_t aSize, ipc::Shmem* aShmem) {
+ if (mClosed) {
+ return false;
+ }
+ return PImageBridgeParent::AllocShmem(aSize, aShmem);
+}
+
+bool ImageBridgeParent::AllocUnsafeShmem(size_t aSize, ipc::Shmem* aShmem) {
+ if (mClosed) {
+ return false;
+ }
+ return PImageBridgeParent::AllocUnsafeShmem(aSize, aShmem);
+}
+
+bool ImageBridgeParent::DeallocShmem(ipc::Shmem& aShmem) {
+ if (mClosed) {
+ return false;
+ }
+ return PImageBridgeParent::DeallocShmem(aShmem);
+}
+
+bool ImageBridgeParent::IsSameProcess() const {
+ return OtherPid() == base::GetCurrentProcId();
+}
+
+void ImageBridgeParent::NotifyNotUsed(PTextureParent* aTexture,
+ uint64_t aTransactionId) {
+ RefPtr<TextureHost> texture = TextureHost::AsTextureHost(aTexture);
+ if (!texture) {
+ return;
+ }
+
+ if (!(texture->GetFlags() & TextureFlags::RECYCLE) &&
+ !(texture->GetFlags() & TextureFlags::WAIT_HOST_USAGE_END)) {
+ return;
+ }
+
+ uint64_t textureId = TextureHost::GetTextureSerial(aTexture);
+ mPendingAsyncMessage.push_back(OpNotifyNotUsed(textureId, aTransactionId));
+
+ if (!IsAboutToSendAsyncMessages()) {
+ SendPendingAsyncMessages();
+ }
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/ipc/ImageBridgeParent.h b/gfx/layers/ipc/ImageBridgeParent.h
new file mode 100644
index 0000000000..7f2c7ce4ce
--- /dev/null
+++ b/gfx/layers/ipc/ImageBridgeParent.h
@@ -0,0 +1,151 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef gfx_layers_ipc_ImageBridgeParent_h_
+#define gfx_layers_ipc_ImageBridgeParent_h_
+
+#include <stddef.h> // for size_t
+#include <stdint.h> // for uint32_t, uint64_t
+#include "CompositableTransactionParent.h"
+#include "mozilla/Assertions.h" // for MOZ_ASSERT_HELPER2
+#include "mozilla/Attributes.h" // for override
+#include "mozilla/dom/ipc/IdType.h"
+#include "mozilla/ipc/ProtocolUtils.h"
+#include "mozilla/ipc/SharedMemory.h" // for SharedMemory, etc
+#include "mozilla/layers/CompositorThread.h"
+#include "mozilla/layers/PImageBridgeParent.h"
+#include "nsISupportsImpl.h"
+#include "nsTArrayForwardDeclare.h" // for nsTArray
+
+namespace mozilla {
+namespace ipc {
+class Shmem;
+} // namespace ipc
+
+namespace layers {
+
+struct ImageCompositeNotificationInfo;
+
+/**
+ * ImageBridgeParent is the manager Protocol of async Compositables.
+ */
+class ImageBridgeParent final : public PImageBridgeParent,
+ public CompositableParentManager,
+ public mozilla::ipc::IShmemAllocator {
+ public:
+ typedef nsTArray<CompositableOperation> EditArray;
+ typedef nsTArray<OpDestroy> OpDestroyArray;
+
+ protected:
+ ImageBridgeParent(nsISerialEventTarget* aThread, ProcessId aChildProcessId,
+ dom::ContentParentId aContentId);
+
+ public:
+ NS_IMETHOD_(MozExternalRefCountType) AddRef() override {
+ return ISurfaceAllocator::AddRef();
+ }
+ NS_IMETHOD_(MozExternalRefCountType) Release() override {
+ return ISurfaceAllocator::Release();
+ }
+
+ /**
+ * Creates the globals of ImageBridgeParent.
+ */
+ static void Setup();
+
+ static ImageBridgeParent* CreateSameProcess();
+ static bool CreateForGPUProcess(Endpoint<PImageBridgeParent>&& aEndpoint);
+ static bool CreateForContent(Endpoint<PImageBridgeParent>&& aEndpoint,
+ dom::ContentParentId aContentId);
+ static void Shutdown();
+
+ IShmemAllocator* AsShmemAllocator() override { return this; }
+
+ void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ // CompositableParentManager
+ void SendAsyncMessage(
+ const nsTArray<AsyncParentMessageData>& aMessage) override;
+
+ void NotifyNotUsed(PTextureParent* aTexture,
+ uint64_t aTransactionId) override;
+
+ base::ProcessId GetChildProcessId() override { return OtherPid(); }
+ dom::ContentParentId GetContentId() override { return mContentId; }
+
+ // PImageBridge
+ mozilla::ipc::IPCResult RecvUpdate(EditArray&& aEdits,
+ OpDestroyArray&& aToDestroy,
+ const uint64_t& aFwdTransactionId);
+
+ PTextureParent* AllocPTextureParent(
+ const SurfaceDescriptor& aSharedData, ReadLockDescriptor& aReadLock,
+ const LayersBackend& aLayersBackend, const TextureFlags& aFlags,
+ const uint64_t& aSerial,
+ const wr::MaybeExternalImageId& aExternalImageId);
+ bool DeallocPTextureParent(PTextureParent* actor);
+
+ mozilla::ipc::IPCResult RecvNewCompositable(const CompositableHandle& aHandle,
+ const TextureInfo& aInfo);
+ mozilla::ipc::IPCResult RecvReleaseCompositable(
+ const CompositableHandle& aHandle);
+
+ PMediaSystemResourceManagerParent* AllocPMediaSystemResourceManagerParent();
+ bool DeallocPMediaSystemResourceManagerParent(
+ PMediaSystemResourceManagerParent* aActor);
+
+ // Shutdown step 1
+ mozilla::ipc::IPCResult RecvWillClose();
+
+ nsISerialEventTarget* GetThread() const { return mThread; }
+
+ // IShmemAllocator
+
+ bool AllocShmem(size_t aSize, ipc::Shmem* aShmem) override;
+
+ bool AllocUnsafeShmem(size_t aSize, ipc::Shmem* aShmem) override;
+
+ bool DeallocShmem(ipc::Shmem& aShmem) override;
+
+ bool IsSameProcess() const override;
+
+ static already_AddRefed<ImageBridgeParent> GetInstance(ProcessId aId);
+
+ static bool NotifyImageComposites(
+ nsTArray<ImageCompositeNotificationInfo>& aNotifications);
+
+ bool UsesImageBridge() const override { return true; }
+
+ bool IPCOpen() const override { return !mClosed; }
+
+ protected:
+ void Bind(Endpoint<PImageBridgeParent>&& aEndpoint);
+
+ private:
+ virtual ~ImageBridgeParent();
+
+ static void ShutdownInternal();
+
+ void DeferredDestroy();
+ nsCOMPtr<nsISerialEventTarget> mThread;
+
+ dom::ContentParentId mContentId;
+
+ bool mClosed;
+
+ /**
+ * Map of all living ImageBridgeParent instances
+ */
+ typedef std::map<base::ProcessId, ImageBridgeParent*> ImageBridgeMap;
+ static ImageBridgeMap sImageBridges;
+
+ RefPtr<CompositorThreadHolder> mCompositorThreadHolder;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // gfx_layers_ipc_ImageBridgeParent_h_
diff --git a/gfx/layers/ipc/KnowsCompositor.cpp b/gfx/layers/ipc/KnowsCompositor.cpp
new file mode 100644
index 0000000000..4d2ed90603
--- /dev/null
+++ b/gfx/layers/ipc/KnowsCompositor.cpp
@@ -0,0 +1,113 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "KnowsCompositor.h"
+#include "mozilla/layers/ImageDataSerializer.h"
+#include "mozilla/layers/ImageBridgeChild.h"
+#include "mozilla/ipc/ProtocolUtils.h"
+
+namespace mozilla {
+namespace layers {
+
+void KnowsCompositor::IdentifyTextureHost(
+ const TextureFactoryIdentifier& aIdentifier) {
+ auto lock = mData.Lock();
+ lock.ref().mTextureFactoryIdentifier = aIdentifier;
+
+ lock.ref().mSyncObject =
+ SyncObjectClient::CreateSyncObjectClientForContentDevice(
+ aIdentifier.mSyncHandle);
+}
+
+KnowsCompositor::KnowsCompositor()
+ : mData("KnowsCompositorMutex"), mSerial(++sSerialCounter) {}
+
+KnowsCompositor::~KnowsCompositor() = default;
+
+KnowsCompositorMediaProxy::KnowsCompositorMediaProxy(
+ const TextureFactoryIdentifier& aIdentifier) {
+ auto lock = mData.Lock();
+ lock.ref().mTextureFactoryIdentifier = aIdentifier;
+ // overwrite mSerial's value set by the parent class because we use the same
+ // serial as the KnowsCompositor we are proxying.
+ mThreadSafeAllocator = ImageBridgeChild::GetSingleton();
+ lock.ref().mSyncObject = mThreadSafeAllocator->GetSyncObject();
+}
+
+KnowsCompositorMediaProxy::~KnowsCompositorMediaProxy() = default;
+
+TextureForwarder* KnowsCompositorMediaProxy::GetTextureForwarder() {
+ return mThreadSafeAllocator->GetTextureForwarder();
+}
+
+LayersIPCActor* KnowsCompositorMediaProxy::GetLayersIPCActor() {
+ return mThreadSafeAllocator->GetLayersIPCActor();
+}
+
+ActiveResourceTracker* KnowsCompositorMediaProxy::GetActiveResourceTracker() {
+ return mThreadSafeAllocator->GetActiveResourceTracker();
+}
+
+void KnowsCompositorMediaProxy::SyncWithCompositor() {
+ mThreadSafeAllocator->SyncWithCompositor();
+}
+
+bool IsSurfaceDescriptorValid(const SurfaceDescriptor& aSurface) {
+ return aSurface.type() != SurfaceDescriptor::T__None &&
+ aSurface.type() != SurfaceDescriptor::Tnull_t;
+}
+
+uint8_t* GetAddressFromDescriptor(const SurfaceDescriptor& aDescriptor) {
+ MOZ_ASSERT(IsSurfaceDescriptorValid(aDescriptor));
+ MOZ_RELEASE_ASSERT(
+ aDescriptor.type() == SurfaceDescriptor::TSurfaceDescriptorBuffer,
+ "GFX: surface descriptor is not the right type.");
+
+ auto memOrShmem = aDescriptor.get_SurfaceDescriptorBuffer().data();
+ if (memOrShmem.type() == MemoryOrShmem::TShmem) {
+ return memOrShmem.get_Shmem().get<uint8_t>();
+ } else {
+ return reinterpret_cast<uint8_t*>(memOrShmem.get_uintptr_t());
+ }
+}
+
+already_AddRefed<gfx::DataSourceSurface> GetSurfaceForDescriptor(
+ const SurfaceDescriptor& aDescriptor) {
+ if (aDescriptor.type() != SurfaceDescriptor::TSurfaceDescriptorBuffer) {
+ return nullptr;
+ }
+ uint8_t* data = GetAddressFromDescriptor(aDescriptor);
+ auto rgb =
+ aDescriptor.get_SurfaceDescriptorBuffer().desc().get_RGBDescriptor();
+ uint32_t stride = ImageDataSerializer::GetRGBStride(rgb);
+ return gfx::Factory::CreateWrappingDataSourceSurface(data, stride, rgb.size(),
+ rgb.format());
+}
+
+void DestroySurfaceDescriptor(ipc::IShmemAllocator* aAllocator,
+ SurfaceDescriptor* aSurface) {
+ MOZ_ASSERT(aSurface);
+
+ SurfaceDescriptorBuffer& desc = aSurface->get_SurfaceDescriptorBuffer();
+ switch (desc.data().type()) {
+ case MemoryOrShmem::TShmem: {
+ aAllocator->DeallocShmem(desc.data().get_Shmem());
+ break;
+ }
+ case MemoryOrShmem::Tuintptr_t: {
+ uint8_t* ptr = (uint8_t*)desc.data().get_uintptr_t();
+ GfxMemoryImageReporter::WillFree(ptr);
+ delete[] ptr;
+ break;
+ }
+ default:
+ MOZ_CRASH("surface type not implemented!");
+ }
+ *aSurface = SurfaceDescriptor();
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/ipc/KnowsCompositor.h b/gfx/layers/ipc/KnowsCompositor.h
new file mode 100644
index 0000000000..7c8c3b780d
--- /dev/null
+++ b/gfx/layers/ipc/KnowsCompositor.h
@@ -0,0 +1,261 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_LAYERS_KNOWSCOMPOSITOR
+#define MOZILLA_LAYERS_KNOWSCOMPOSITOR
+
+#include "mozilla/layers/LayersTypes.h" // for LayersBackend
+#include "mozilla/layers/CompositorTypes.h"
+#include "nsExpirationTracker.h"
+#include "mozilla/DataMutex.h"
+#include "mozilla/layers/SyncObject.h"
+
+namespace mozilla {
+namespace layers {
+
+class TextureForwarder;
+class LayersIPCActor;
+class ImageBridgeChild;
+
+/**
+ * See ActiveResourceTracker below.
+ */
+class ActiveResource {
+ public:
+ virtual void NotifyInactive() = 0;
+ nsExpirationState* GetExpirationState() { return &mExpirationState; }
+ bool IsActivityTracked() { return mExpirationState.IsTracked(); }
+
+ private:
+ nsExpirationState mExpirationState;
+};
+
+/**
+ * A convenience class on top of nsExpirationTracker
+ */
+class ActiveResourceTracker : public nsExpirationTracker<ActiveResource, 3> {
+ public:
+ ActiveResourceTracker(uint32_t aExpirationCycle, const char* aName,
+ nsIEventTarget* aEventTarget)
+ : nsExpirationTracker(aExpirationCycle, aName, aEventTarget) {}
+
+ void NotifyExpired(ActiveResource* aResource) override {
+ RemoveObject(aResource);
+ aResource->NotifyInactive();
+ }
+};
+
+/**
+ * An abstract interface for classes that are tied to a specific Compositor
+ * across IPDL and uses TextureFactoryIdentifier to describe this Compositor.
+ */
+class KnowsCompositor {
+ public:
+ NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING
+
+ KnowsCompositor();
+ virtual ~KnowsCompositor();
+
+ void IdentifyTextureHost(const TextureFactoryIdentifier& aIdentifier);
+
+ // The sync object for the global content device.
+ RefPtr<SyncObjectClient> GetSyncObject() {
+ auto lock = mData.Lock();
+ if (lock.ref().mSyncObject) {
+ lock.ref().mSyncObject->EnsureInitialized();
+ }
+ return lock.ref().mSyncObject;
+ }
+
+ /// And by "thread-safe" here we merely mean "okay to hold strong references
+ /// to from multiple threads". Not all methods actually are thread-safe.
+ virtual bool IsThreadSafe() const { return true; }
+
+ virtual RefPtr<KnowsCompositor> GetForMedia() {
+ return RefPtr<KnowsCompositor>(this);
+ }
+
+ int32_t GetMaxTextureSize() const {
+ auto lock = mData.Lock();
+ return lock.ref().mTextureFactoryIdentifier.mMaxTextureSize;
+ }
+
+ /**
+ * Returns the type of backend that is used off the main thread.
+ * We only don't allow changing the backend type at runtime so this value can
+ * be queried once and will not change until Gecko is restarted.
+ */
+ LayersBackend GetCompositorBackendType() const {
+ auto lock = mData.Lock();
+ return lock.ref().mTextureFactoryIdentifier.mParentBackend;
+ }
+
+ WebRenderCompositor GetWebRenderCompositorType() const {
+ auto lock = mData.Lock();
+ return lock.ref().mTextureFactoryIdentifier.mWebRenderCompositor;
+ }
+
+ bool SupportsTextureBlitting() const {
+ auto lock = mData.Lock();
+ return lock.ref().mTextureFactoryIdentifier.mSupportsTextureBlitting;
+ }
+
+ bool SupportsPartialUploads() const {
+ auto lock = mData.Lock();
+ return lock.ref().mTextureFactoryIdentifier.mSupportsPartialUploads;
+ }
+
+ bool SupportsComponentAlpha() const {
+ auto lock = mData.Lock();
+ return lock.ref().mTextureFactoryIdentifier.mSupportsComponentAlpha;
+ }
+
+ bool SupportsD3D11NV12() const {
+ auto lock = mData.Lock();
+ return lock.ref().mTextureFactoryIdentifier.mSupportsD3D11NV12;
+ }
+
+ bool SupportsD3D11() const {
+ auto lock = mData.Lock();
+ return lock.ref().mTextureFactoryIdentifier.mParentBackend ==
+ layers::LayersBackend::LAYERS_WR &&
+ (lock.ref().mTextureFactoryIdentifier.mCompositorUseANGLE ||
+ lock.ref().mTextureFactoryIdentifier.mWebRenderCompositor ==
+ layers::WebRenderCompositor::D3D11);
+ }
+
+ bool GetCompositorUseANGLE() const {
+ auto lock = mData.Lock();
+ return lock.ref().mTextureFactoryIdentifier.mCompositorUseANGLE;
+ }
+
+ bool GetCompositorUseDComp() const {
+ auto lock = mData.Lock();
+ return lock.ref().mTextureFactoryIdentifier.mCompositorUseDComp;
+ }
+
+ bool GetUseCompositorWnd() const {
+ auto lock = mData.Lock();
+ return lock.ref().mTextureFactoryIdentifier.mUseCompositorWnd;
+ }
+
+ WebRenderBackend GetWebRenderBackend() const {
+ auto lock = mData.Lock();
+ MOZ_ASSERT(lock.ref().mTextureFactoryIdentifier.mParentBackend ==
+ layers::LayersBackend::LAYERS_WR);
+ return lock.ref().mTextureFactoryIdentifier.mWebRenderBackend;
+ }
+
+ bool UsingHardwareWebRender() const {
+ auto lock = mData.Lock();
+ return lock.ref().mTextureFactoryIdentifier.mParentBackend ==
+ layers::LayersBackend::LAYERS_WR &&
+ lock.ref().mTextureFactoryIdentifier.mWebRenderBackend ==
+ WebRenderBackend::HARDWARE;
+ }
+
+ bool UsingSoftwareWebRender() const {
+ auto lock = mData.Lock();
+ return lock.ref().mTextureFactoryIdentifier.mParentBackend ==
+ layers::LayersBackend::LAYERS_WR &&
+ lock.ref().mTextureFactoryIdentifier.mWebRenderBackend ==
+ WebRenderBackend::SOFTWARE;
+ }
+
+ bool UsingSoftwareWebRenderD3D11() const {
+ auto lock = mData.Lock();
+ return lock.ref().mTextureFactoryIdentifier.mParentBackend ==
+ layers::LayersBackend::LAYERS_WR &&
+ lock.ref().mTextureFactoryIdentifier.mWebRenderBackend ==
+ WebRenderBackend::SOFTWARE &&
+ lock.ref().mTextureFactoryIdentifier.mWebRenderCompositor ==
+ layers::WebRenderCompositor::D3D11;
+ }
+
+ bool UsingSoftwareWebRenderOpenGL() const {
+ auto lock = mData.Lock();
+ return lock.ref().mTextureFactoryIdentifier.mParentBackend ==
+ layers::LayersBackend::LAYERS_WR &&
+ lock.ref().mTextureFactoryIdentifier.mWebRenderBackend ==
+ WebRenderBackend::SOFTWARE &&
+ lock.ref().mTextureFactoryIdentifier.mWebRenderCompositor ==
+ layers::WebRenderCompositor::OPENGL;
+ }
+
+ TextureFactoryIdentifier GetTextureFactoryIdentifier() const {
+ auto lock = mData.Lock();
+ return lock.ref().mTextureFactoryIdentifier;
+ }
+
+ int32_t GetSerial() const { return mSerial; }
+
+ /**
+ * Sends a synchronous ping to the compsoitor.
+ *
+ * This is bad for performance and should only be called as a last resort if
+ * the compositor may be blocked for a long period of time, to avoid that the
+ * content process accumulates resource allocations that the compositor is not
+ * consuming and releasing.
+ */
+ virtual void SyncWithCompositor() { MOZ_ASSERT_UNREACHABLE("Unimplemented"); }
+
+ /**
+ * Helpers for finding other related interface. These are infallible.
+ */
+ virtual TextureForwarder* GetTextureForwarder() = 0;
+ virtual LayersIPCActor* GetLayersIPCActor() = 0;
+ virtual ActiveResourceTracker* GetActiveResourceTracker() {
+ MOZ_ASSERT_UNREACHABLE("Unimplemented");
+ return nullptr;
+ }
+
+ protected:
+ struct SharedData {
+ TextureFactoryIdentifier mTextureFactoryIdentifier;
+ RefPtr<SyncObjectClient> mSyncObject;
+ };
+ mutable DataMutex<SharedData> mData;
+
+ const int32_t mSerial;
+ static mozilla::Atomic<int32_t> sSerialCounter;
+};
+
+/// Some implementations of KnowsCompositor can be used off their IPDL thread
+/// like the ImageBridgeChild, and others just can't. Instead of passing them
+/// we create a proxy KnowsCompositor that has information about compositor
+/// backend but proxies allocations to the ImageBridge.
+/// This is kind of specific to the needs of media which wants to allocate
+/// textures, usually through the Image Bridge accessed by KnowsCompositor but
+/// also wants access to the compositor backend information that ImageBridge
+/// doesn't know about.
+///
+/// This is really a band aid to what turned into a class hierarchy horror show.
+/// Hopefully we can come back and simplify this some way.
+class KnowsCompositorMediaProxy : public KnowsCompositor {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(KnowsCompositorMediaProxy, override);
+
+ explicit KnowsCompositorMediaProxy(
+ const TextureFactoryIdentifier& aIdentifier);
+
+ TextureForwarder* GetTextureForwarder() override;
+
+ LayersIPCActor* GetLayersIPCActor() override;
+
+ ActiveResourceTracker* GetActiveResourceTracker() override;
+
+ void SyncWithCompositor() override;
+
+ protected:
+ virtual ~KnowsCompositorMediaProxy();
+
+ RefPtr<ImageBridgeChild> mThreadSafeAllocator;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif
diff --git a/gfx/layers/ipc/LayerTreeOwnerTracker.cpp b/gfx/layers/ipc/LayerTreeOwnerTracker.cpp
new file mode 100644
index 0000000000..ab3551ad29
--- /dev/null
+++ b/gfx/layers/ipc/LayerTreeOwnerTracker.cpp
@@ -0,0 +1,68 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "LayerTreeOwnerTracker.h"
+
+#include "mozilla/StaticPtr.h" // for StaticAutoPtr
+#include "mozilla/gfx/GPUChild.h" // for GPUChild
+#include "mozilla/gfx/GPUProcessManager.h" // for GPUProcessManager
+
+#include <functional>
+#include <utility> // for std::make_pair
+
+namespace mozilla {
+namespace layers {
+
+static StaticAutoPtr<LayerTreeOwnerTracker> sSingleton;
+
+LayerTreeOwnerTracker::LayerTreeOwnerTracker()
+ : mLayerIdsLock("LayerTreeOwnerTrackerLock") {}
+
+void LayerTreeOwnerTracker::Initialize() {
+ MOZ_ASSERT(!sSingleton);
+ sSingleton = new LayerTreeOwnerTracker();
+}
+
+void LayerTreeOwnerTracker::Shutdown() { sSingleton = nullptr; }
+
+LayerTreeOwnerTracker* LayerTreeOwnerTracker::Get() { return sSingleton; }
+
+void LayerTreeOwnerTracker::Map(LayersId aLayersId,
+ base::ProcessId aProcessId) {
+ MutexAutoLock lock(mLayerIdsLock);
+
+ // Add the mapping to the list
+ mLayerIds[aLayersId] = aProcessId;
+}
+
+void LayerTreeOwnerTracker::Unmap(LayersId aLayersId,
+ base::ProcessId aProcessId) {
+ MutexAutoLock lock(mLayerIdsLock);
+
+ MOZ_ASSERT(mLayerIds[aLayersId] == aProcessId);
+ mLayerIds.erase(aLayersId);
+}
+
+bool LayerTreeOwnerTracker::IsMapped(LayersId aLayersId,
+ base::ProcessId aProcessId) {
+ MutexAutoLock lock(mLayerIdsLock);
+
+ auto iter = mLayerIds.find(aLayersId);
+ return iter != mLayerIds.end() && iter->second == aProcessId;
+}
+
+void LayerTreeOwnerTracker::Iterate(
+ const std::function<void(LayersId aLayersId, base::ProcessId aProcessId)>&
+ aCallback) {
+ MutexAutoLock lock(mLayerIdsLock);
+
+ for (const auto& iter : mLayerIds) {
+ aCallback(iter.first, iter.second);
+ }
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/ipc/LayerTreeOwnerTracker.h b/gfx/layers/ipc/LayerTreeOwnerTracker.h
new file mode 100644
index 0000000000..651c695309
--- /dev/null
+++ b/gfx/layers/ipc/LayerTreeOwnerTracker.h
@@ -0,0 +1,73 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_LayerTreeOwnerTracker_h
+#define mozilla_layers_LayerTreeOwnerTracker_h
+
+#include "base/process.h" // for base::ProcessId
+#include "LayersTypes.h" // for LayersId
+#include "mozilla/Mutex.h" // for mozilla::Mutex
+
+#include <functional>
+#include <map>
+
+namespace mozilla {
+
+namespace dom {
+class ContentParent;
+}
+
+namespace layers {
+
+/**
+ * A utility class for tracking which content processes should be allowed
+ * to access which layer trees.
+ *
+ * ProcessId's are used to track which content process can access the layer
+ * tree, and in the case of nested browser's we use the top level content
+ * processes' ProcessId.
+ *
+ * This class is only available in the main process and gpu process. Mappings
+ * are synced from main process to the gpu process. The actual syncing happens
+ * in GPUProcessManager, and so this class should not be used directly.
+ */
+class LayerTreeOwnerTracker final {
+ public:
+ static void Initialize();
+ static void Shutdown();
+ static LayerTreeOwnerTracker* Get();
+
+ /**
+ * Map aLayersId and aProcessId together so that that process
+ * can access that layer tree.
+ */
+ void Map(LayersId aLayersId, base::ProcessId aProcessId);
+
+ /**
+ * Remove an existing mapping.
+ */
+ void Unmap(LayersId aLayersId, base::ProcessId aProcessId);
+
+ /**
+ * Checks whether it is okay for aProcessId to access aLayersId.
+ */
+ bool IsMapped(LayersId aLayersId, base::ProcessId aProcessId);
+
+ void Iterate(
+ const std::function<void(LayersId aLayersId, base::ProcessId aProcessId)>&
+ aCallback);
+
+ private:
+ LayerTreeOwnerTracker();
+
+ mozilla::Mutex mLayerIdsLock MOZ_UNANNOTATED;
+ std::map<LayersId, base::ProcessId> mLayerIds;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_LayerTreeOwnerTracker_h
diff --git a/gfx/layers/ipc/LayersMessageUtils.h b/gfx/layers/ipc/LayersMessageUtils.h
new file mode 100644
index 0000000000..df421f79a6
--- /dev/null
+++ b/gfx/layers/ipc/LayersMessageUtils.h
@@ -0,0 +1,1156 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_LayersMessageUtils
+#define mozilla_layers_LayersMessageUtils
+
+#include <stdint.h>
+
+#include <utility>
+
+#include "FrameMetrics.h"
+#include "VsyncSource.h"
+#include "chrome/common/ipc_message_utils.h"
+#include "ipc/EnumSerializer.h"
+#include "ipc/IPCMessageUtils.h"
+#include "mozilla/MotionPathUtils.h"
+#include "mozilla/ServoBindings.h"
+#include "mozilla/ipc/ByteBuf.h"
+#include "mozilla/layers/APZInputBridge.h"
+#include "mozilla/layers/AsyncDragMetrics.h"
+#include "mozilla/layers/CompositorOptions.h"
+#include "mozilla/layers/CompositorTypes.h"
+#include "mozilla/layers/FocusTarget.h"
+#include "mozilla/layers/GeckoContentControllerTypes.h"
+#include "mozilla/layers/KeyboardMap.h"
+#include "mozilla/layers/LayersTypes.h"
+#include "mozilla/layers/MatrixMessage.h"
+#include "mozilla/layers/OverlayInfo.h"
+#include "mozilla/layers/RepaintRequest.h"
+#include "mozilla/layers/ScrollbarData.h"
+#include "nsSize.h"
+#include "mozilla/layers/DoubleTapToZoom.h"
+
+// For ParamTraits, could be moved to cpp file
+#include "ipc/nsGUIEventIPC.h"
+#include "mozilla/GfxMessageUtils.h"
+#include "mozilla/ipc/ByteBufUtils.h"
+
+#ifdef _MSC_VER
+# pragma warning(disable : 4800)
+#endif
+
+namespace IPC {
+
+template <>
+struct ParamTraits<mozilla::layers::LayersId>
+ : public PlainOldDataSerializer<mozilla::layers::LayersId> {};
+
+template <typename T>
+struct ParamTraits<mozilla::layers::BaseTransactionId<T>>
+ : public PlainOldDataSerializer<mozilla::layers::BaseTransactionId<T>> {};
+
+template <>
+struct ParamTraits<mozilla::VsyncId>
+ : public PlainOldDataSerializer<mozilla::VsyncId> {};
+
+template <>
+struct ParamTraits<mozilla::VsyncEvent> {
+ typedef mozilla::VsyncEvent paramType;
+
+ static void Write(MessageWriter* writer, const paramType& param) {
+ WriteParam(writer, param.mId);
+ WriteParam(writer, param.mTime);
+ WriteParam(writer, param.mOutputTime);
+ }
+ static bool Read(MessageReader* reader, paramType* result) {
+ return ReadParam(reader, &result->mId) &&
+ ReadParam(reader, &result->mTime) &&
+ ReadParam(reader, &result->mOutputTime);
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::layers::MatrixMessage> {
+ typedef mozilla::layers::MatrixMessage paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.mMatrix);
+ WriteParam(aWriter, aParam.mTopLevelViewportVisibleRectInBrowserCoords);
+ WriteParam(aWriter, aParam.mLayersId);
+ }
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ return ReadParam(aReader, &aResult->mMatrix) &&
+ ReadParam(aReader,
+ &aResult->mTopLevelViewportVisibleRectInBrowserCoords) &&
+ ReadParam(aReader, &aResult->mLayersId);
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::layers::LayersObserverEpoch>
+ : public PlainOldDataSerializer<mozilla::layers::LayersObserverEpoch> {};
+
+template <>
+struct ParamTraits<mozilla::layers::WindowKind>
+ : public ContiguousEnumSerializer<mozilla::layers::WindowKind,
+ mozilla::layers::WindowKind::MAIN,
+ mozilla::layers::WindowKind::LAST> {};
+
+template <>
+struct ParamTraits<mozilla::layers::LayersBackend>
+ : public ContiguousEnumSerializer<
+ mozilla::layers::LayersBackend,
+ mozilla::layers::LayersBackend::LAYERS_NONE,
+ mozilla::layers::LayersBackend::LAYERS_LAST> {};
+
+template <>
+struct ParamTraits<mozilla::layers::WebRenderBackend>
+ : public ContiguousEnumSerializer<
+ mozilla::layers::WebRenderBackend,
+ mozilla::layers::WebRenderBackend::HARDWARE,
+ mozilla::layers::WebRenderBackend::LAST> {};
+
+template <>
+struct ParamTraits<mozilla::layers::WebRenderCompositor>
+ : public ContiguousEnumSerializer<
+ mozilla::layers::WebRenderCompositor,
+ mozilla::layers::WebRenderCompositor::DRAW,
+ mozilla::layers::WebRenderCompositor::LAST> {};
+
+template <>
+struct ParamTraits<mozilla::layers::TextureType>
+ : public ContiguousEnumSerializer<mozilla::layers::TextureType,
+ mozilla::layers::TextureType::Unknown,
+ mozilla::layers::TextureType::Last> {};
+
+template <>
+struct ParamTraits<mozilla::layers::ScaleMode>
+ : public ContiguousEnumSerializerInclusive<
+ mozilla::layers::ScaleMode, mozilla::layers::ScaleMode::SCALE_NONE,
+ mozilla::layers::kHighestScaleMode> {};
+
+template <>
+struct ParamTraits<mozilla::StyleScrollSnapStrictness>
+ : public ContiguousEnumSerializerInclusive<
+ mozilla::StyleScrollSnapStrictness,
+ mozilla::StyleScrollSnapStrictness::None,
+ mozilla::StyleScrollSnapStrictness::Proximity> {};
+
+template <>
+struct ParamTraits<mozilla::layers::TextureFlags>
+ : public BitFlagsEnumSerializer<mozilla::layers::TextureFlags,
+ mozilla::layers::TextureFlags::ALL_BITS> {};
+
+template <>
+struct ParamTraits<mozilla::layers::DiagnosticTypes>
+ : public BitFlagsEnumSerializer<
+ mozilla::layers::DiagnosticTypes,
+ mozilla::layers::DiagnosticTypes::ALL_BITS> {};
+
+template <>
+struct ParamTraits<mozilla::layers::ScrollDirection>
+ : public ContiguousEnumSerializerInclusive<
+ mozilla::layers::ScrollDirection,
+ mozilla::layers::ScrollDirection::eVertical,
+ mozilla::layers::kHighestScrollDirection> {};
+
+template <>
+struct ParamTraits<mozilla::layers::FrameMetrics::ScrollOffsetUpdateType>
+ : public ContiguousEnumSerializerInclusive<
+ mozilla::layers::FrameMetrics::ScrollOffsetUpdateType,
+ mozilla::layers::FrameMetrics::ScrollOffsetUpdateType::eNone,
+ mozilla::layers::FrameMetrics::sHighestScrollOffsetUpdateType> {};
+
+template <>
+struct ParamTraits<mozilla::layers::RepaintRequest::ScrollOffsetUpdateType>
+ : public ContiguousEnumSerializerInclusive<
+ mozilla::layers::RepaintRequest::ScrollOffsetUpdateType,
+ mozilla::layers::RepaintRequest::ScrollOffsetUpdateType::eNone,
+ mozilla::layers::RepaintRequest::sHighestScrollOffsetUpdateType> {};
+
+template <>
+struct ParamTraits<mozilla::layers::OverscrollBehavior>
+ : public ContiguousEnumSerializerInclusive<
+ mozilla::layers::OverscrollBehavior,
+ mozilla::layers::OverscrollBehavior::Auto,
+ mozilla::layers::kHighestOverscrollBehavior> {};
+
+template <>
+struct ParamTraits<mozilla::layers::LayerHandle> {
+ typedef mozilla::layers::LayerHandle paramType;
+
+ static void Write(MessageWriter* writer, const paramType& param) {
+ WriteParam(writer, param.mHandle);
+ }
+ static bool Read(MessageReader* reader, paramType* result) {
+ return ReadParam(reader, &result->mHandle);
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::layers::CompositableHandle> {
+ typedef mozilla::layers::CompositableHandle paramType;
+
+ static void Write(MessageWriter* writer, const paramType& param) {
+ WriteParam(writer, param.mHandle);
+ }
+ static bool Read(MessageReader* reader, paramType* result) {
+ return ReadParam(reader, &result->mHandle);
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::layers::CompositableHandleOwner>
+ : public ContiguousEnumSerializerInclusive<
+ mozilla::layers::CompositableHandleOwner,
+ mozilla::layers::CompositableHandleOwner::WebRenderBridge,
+ mozilla::layers::CompositableHandleOwner::ImageBridge> {};
+
+template <>
+struct ParamTraits<mozilla::layers::RemoteTextureId> {
+ typedef mozilla::layers::RemoteTextureId paramType;
+
+ static void Write(MessageWriter* writer, const paramType& param) {
+ WriteParam(writer, param.mId);
+ }
+ static bool Read(MessageReader* reader, paramType* result) {
+ return ReadParam(reader, &result->mId);
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::layers::RemoteTextureOwnerId> {
+ typedef mozilla::layers::RemoteTextureOwnerId paramType;
+
+ static void Write(MessageWriter* writer, const paramType& param) {
+ WriteParam(writer, param.mId);
+ }
+ static bool Read(MessageReader* reader, paramType* result) {
+ return ReadParam(reader, &result->mId);
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::layers::GpuProcessTextureId> {
+ typedef mozilla::layers::GpuProcessTextureId paramType;
+
+ static void Write(MessageWriter* writer, const paramType& param) {
+ WriteParam(writer, param.mId);
+ }
+ static bool Read(MessageReader* reader, paramType* result) {
+ return ReadParam(reader, &result->mId);
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::layers::FrameMetrics>
+ : BitfieldHelper<mozilla::layers::FrameMetrics> {
+ typedef mozilla::layers::FrameMetrics paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.mScrollId);
+ WriteParam(aWriter, aParam.mPresShellResolution);
+ WriteParam(aWriter, aParam.mCompositionBounds);
+ WriteParam(aWriter, aParam.mCompositionBoundsWidthIgnoringScrollbars);
+ WriteParam(aWriter, aParam.mDisplayPort);
+ WriteParam(aWriter, aParam.mScrollableRect);
+ WriteParam(aWriter, aParam.mCumulativeResolution);
+ WriteParam(aWriter, aParam.mDevPixelsPerCSSPixel);
+ WriteParam(aWriter, aParam.mScrollOffset);
+ WriteParam(aWriter, aParam.mZoom);
+ WriteParam(aWriter, aParam.mScrollGeneration);
+ WriteParam(aWriter, aParam.mBoundingCompositionSize);
+ WriteParam(aWriter, aParam.mPresShellId);
+ WriteParam(aWriter, aParam.mLayoutViewport);
+ WriteParam(aWriter, aParam.mTransformToAncestorScale);
+ WriteParam(aWriter, aParam.mPaintRequestTime);
+ WriteParam(aWriter, aParam.mVisualDestination);
+ WriteParam(aWriter, aParam.mVisualScrollUpdateType);
+ WriteParam(aWriter, aParam.mFixedLayerMargins);
+ WriteParam(aWriter, aParam.mCompositionSizeWithoutDynamicToolbar);
+ WriteParam(aWriter, aParam.mIsRootContent);
+ WriteParam(aWriter, aParam.mIsScrollInfoLayer);
+ WriteParam(aWriter, aParam.mHasNonZeroDisplayPortMargins);
+ WriteParam(aWriter, aParam.mMinimalDisplayPort);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ return (
+ ReadParam(aReader, &aResult->mScrollId) &&
+ ReadParam(aReader, &aResult->mPresShellResolution) &&
+ ReadParam(aReader, &aResult->mCompositionBounds) &&
+ ReadParam(aReader,
+ &aResult->mCompositionBoundsWidthIgnoringScrollbars) &&
+ ReadParam(aReader, &aResult->mDisplayPort) &&
+ ReadParam(aReader, &aResult->mScrollableRect) &&
+ ReadParam(aReader, &aResult->mCumulativeResolution) &&
+ ReadParam(aReader, &aResult->mDevPixelsPerCSSPixel) &&
+ ReadParam(aReader, &aResult->mScrollOffset) &&
+ ReadParam(aReader, &aResult->mZoom) &&
+ ReadParam(aReader, &aResult->mScrollGeneration) &&
+ ReadParam(aReader, &aResult->mBoundingCompositionSize) &&
+ ReadParam(aReader, &aResult->mPresShellId) &&
+ ReadParam(aReader, &aResult->mLayoutViewport) &&
+ ReadParam(aReader, &aResult->mTransformToAncestorScale) &&
+ ReadParam(aReader, &aResult->mPaintRequestTime) &&
+ ReadParam(aReader, &aResult->mVisualDestination) &&
+ ReadParam(aReader, &aResult->mVisualScrollUpdateType) &&
+ ReadParam(aReader, &aResult->mFixedLayerMargins) &&
+ ReadParam(aReader, &aResult->mCompositionSizeWithoutDynamicToolbar) &&
+ ReadBoolForBitfield(aReader, aResult, &paramType::SetIsRootContent) &&
+ ReadBoolForBitfield(aReader, aResult,
+ &paramType::SetIsScrollInfoLayer) &&
+ ReadBoolForBitfield(aReader, aResult,
+ &paramType::SetHasNonZeroDisplayPortMargins) &&
+ ReadBoolForBitfield(aReader, aResult,
+ &paramType::SetMinimalDisplayPort));
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::APZScrollAnimationType>
+ : public ContiguousEnumSerializerInclusive<
+ mozilla::APZScrollAnimationType, mozilla::APZScrollAnimationType::No,
+ mozilla::APZScrollAnimationType::TriggeredByUserInput> {};
+
+template <>
+struct ParamTraits<mozilla::ScrollSnapTargetIds> {
+ typedef mozilla::ScrollSnapTargetIds paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.mIdsOnX);
+ WriteParam(aWriter, aParam.mIdsOnY);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ return ReadParam(aReader, &aResult->mIdsOnX) &&
+ ReadParam(aReader, &aResult->mIdsOnY);
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::layers::RepaintRequest>
+ : BitfieldHelper<mozilla::layers::RepaintRequest> {
+ typedef mozilla::layers::RepaintRequest paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.mScrollId);
+ WriteParam(aWriter, aParam.mPresShellResolution);
+ WriteParam(aWriter, aParam.mCompositionBounds);
+ WriteParam(aWriter, aParam.mCumulativeResolution);
+ WriteParam(aWriter, aParam.mDevPixelsPerCSSPixel);
+ WriteParam(aWriter, aParam.mScrollOffset);
+ WriteParam(aWriter, aParam.mZoom);
+ WriteParam(aWriter, aParam.mScrollGeneration);
+ WriteParam(aWriter, aParam.mScrollGenerationOnApz);
+ WriteParam(aWriter, aParam.mDisplayPortMargins);
+ WriteParam(aWriter, aParam.mPresShellId);
+ WriteParam(aWriter, aParam.mLayoutViewport);
+ WriteParam(aWriter, aParam.mTransformToAncestorScale);
+ WriteParam(aWriter, aParam.mPaintRequestTime);
+ WriteParam(aWriter, aParam.mScrollUpdateType);
+ WriteParam(aWriter, aParam.mScrollAnimationType);
+ WriteParam(aWriter, aParam.mLastSnapTargetIds);
+ WriteParam(aWriter, aParam.mIsRootContent);
+ WriteParam(aWriter, aParam.mIsScrollInfoLayer);
+ WriteParam(aWriter, aParam.mIsInScrollingGesture);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ return (
+ ReadParam(aReader, &aResult->mScrollId) &&
+ ReadParam(aReader, &aResult->mPresShellResolution) &&
+ ReadParam(aReader, &aResult->mCompositionBounds) &&
+ ReadParam(aReader, &aResult->mCumulativeResolution) &&
+ ReadParam(aReader, &aResult->mDevPixelsPerCSSPixel) &&
+ ReadParam(aReader, &aResult->mScrollOffset) &&
+ ReadParam(aReader, &aResult->mZoom) &&
+ ReadParam(aReader, &aResult->mScrollGeneration) &&
+ ReadParam(aReader, &aResult->mScrollGenerationOnApz) &&
+ ReadParam(aReader, &aResult->mDisplayPortMargins) &&
+ ReadParam(aReader, &aResult->mPresShellId) &&
+ ReadParam(aReader, &aResult->mLayoutViewport) &&
+ ReadParam(aReader, &aResult->mTransformToAncestorScale) &&
+ ReadParam(aReader, &aResult->mPaintRequestTime) &&
+ ReadParam(aReader, &aResult->mScrollUpdateType) &&
+ ReadParam(aReader, &aResult->mScrollAnimationType) &&
+ ReadParam(aReader, &aResult->mLastSnapTargetIds) &&
+ ReadBoolForBitfield(aReader, aResult, &paramType::SetIsRootContent) &&
+ ReadBoolForBitfield(aReader, aResult,
+ &paramType::SetIsScrollInfoLayer) &&
+ ReadBoolForBitfield(aReader, aResult,
+ &paramType::SetIsInScrollingGesture));
+ }
+};
+
+template <>
+struct ParamTraits<nsSize> {
+ typedef nsSize paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.width);
+ WriteParam(aWriter, aParam.height);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ return ReadParam(aReader, &aResult->width) &&
+ ReadParam(aReader, &aResult->height);
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::StyleScrollSnapStop>
+ : public ContiguousEnumSerializerInclusive<
+ mozilla::StyleScrollSnapStop, mozilla::StyleScrollSnapStop::Normal,
+ mozilla::StyleScrollSnapStop::Always> {};
+
+template <>
+struct ParamTraits<mozilla::ScrollSnapTargetId>
+ : public PlainOldDataSerializer<mozilla::ScrollSnapTargetId> {};
+
+template <>
+struct ParamTraits<mozilla::layers::ScrollSnapInfo::SnapTarget> {
+ typedef mozilla::layers::ScrollSnapInfo::SnapTarget paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.mSnapPositionX);
+ WriteParam(aWriter, aParam.mSnapPositionY);
+ WriteParam(aWriter, aParam.mSnapArea);
+ WriteParam(aWriter, aParam.mScrollSnapStop);
+ WriteParam(aWriter, aParam.mTargetId);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ return ReadParam(aReader, &aResult->mSnapPositionX) &&
+ ReadParam(aReader, &aResult->mSnapPositionY) &&
+ ReadParam(aReader, &aResult->mSnapArea) &&
+ ReadParam(aReader, &aResult->mScrollSnapStop) &&
+ ReadParam(aReader, &aResult->mTargetId);
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::layers::ScrollSnapInfo::ScrollSnapRange> {
+ typedef mozilla::layers::ScrollSnapInfo::ScrollSnapRange paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.mStart);
+ WriteParam(aWriter, aParam.mEnd);
+ WriteParam(aWriter, aParam.mTargetId);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ return ReadParam(aReader, &aResult->mStart) &&
+ ReadParam(aReader, &aResult->mEnd) &&
+ ReadParam(aReader, &aResult->mTargetId);
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::layers::ScrollSnapInfo> {
+ typedef mozilla::layers::ScrollSnapInfo paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.mScrollSnapStrictnessX);
+ WriteParam(aWriter, aParam.mScrollSnapStrictnessY);
+ WriteParam(aWriter, aParam.mSnapTargets);
+ WriteParam(aWriter, aParam.mXRangeWiderThanSnapport);
+ WriteParam(aWriter, aParam.mYRangeWiderThanSnapport);
+ WriteParam(aWriter, aParam.mSnapportSize);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ return (ReadParam(aReader, &aResult->mScrollSnapStrictnessX) &&
+ ReadParam(aReader, &aResult->mScrollSnapStrictnessY) &&
+ ReadParam(aReader, &aResult->mSnapTargets) &&
+ ReadParam(aReader, &aResult->mXRangeWiderThanSnapport) &&
+ ReadParam(aReader, &aResult->mYRangeWiderThanSnapport) &&
+ ReadParam(aReader, &aResult->mSnapportSize));
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::layers::OverscrollBehaviorInfo> {
+ // Not using PlainOldDataSerializer so we get enum validation
+ // for the members.
+
+ typedef mozilla::layers::OverscrollBehaviorInfo paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.mBehaviorX);
+ WriteParam(aWriter, aParam.mBehaviorY);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ return (ReadParam(aReader, &aResult->mBehaviorX) &&
+ ReadParam(aReader, &aResult->mBehaviorY));
+ }
+};
+
+template <typename T>
+struct ParamTraits<mozilla::ScrollGeneration<T>>
+ : PlainOldDataSerializer<mozilla::ScrollGeneration<T>> {};
+
+template <>
+struct ParamTraits<mozilla::ScrollUpdateType>
+ : public ContiguousEnumSerializerInclusive<
+ mozilla::ScrollUpdateType, mozilla::ScrollUpdateType::Absolute,
+ mozilla::ScrollUpdateType::PureRelative> {};
+
+template <>
+struct ParamTraits<mozilla::ScrollMode>
+ : public ContiguousEnumSerializerInclusive<mozilla::ScrollMode,
+ mozilla::ScrollMode::Instant,
+ mozilla::ScrollMode::Normal> {};
+
+template <>
+struct ParamTraits<mozilla::ScrollOrigin>
+ : public ContiguousEnumSerializerInclusive<
+ mozilla::ScrollOrigin, mozilla::ScrollOrigin::None,
+ mozilla::ScrollOrigin::Scrollbars> {};
+
+template <>
+struct ParamTraits<mozilla::ScrollTriggeredByScript>
+ : public ContiguousEnumSerializerInclusive<
+ mozilla::ScrollTriggeredByScript,
+ mozilla::ScrollTriggeredByScript::No,
+ mozilla::ScrollTriggeredByScript::Yes> {};
+
+template <>
+struct ParamTraits<mozilla::ScrollPositionUpdate> {
+ typedef mozilla::ScrollPositionUpdate paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.mScrollGeneration);
+ WriteParam(aWriter, aParam.mType);
+ WriteParam(aWriter, aParam.mScrollMode);
+ WriteParam(aWriter, aParam.mScrollOrigin);
+ WriteParam(aWriter, aParam.mDestination);
+ WriteParam(aWriter, aParam.mSource);
+ WriteParam(aWriter, aParam.mDelta);
+ WriteParam(aWriter, aParam.mTriggeredByScript);
+ WriteParam(aWriter, aParam.mSnapTargetIds);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ return ReadParam(aReader, &aResult->mScrollGeneration) &&
+ ReadParam(aReader, &aResult->mType) &&
+ ReadParam(aReader, &aResult->mScrollMode) &&
+ ReadParam(aReader, &aResult->mScrollOrigin) &&
+ ReadParam(aReader, &aResult->mDestination) &&
+ ReadParam(aReader, &aResult->mSource) &&
+ ReadParam(aReader, &aResult->mDelta) &&
+ ReadParam(aReader, &aResult->mTriggeredByScript) &&
+ ReadParam(aReader, &aResult->mSnapTargetIds);
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::layers::ScrollMetadata>
+ : BitfieldHelper<mozilla::layers::ScrollMetadata> {
+ typedef mozilla::layers::ScrollMetadata paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.mMetrics);
+ WriteParam(aWriter, aParam.mSnapInfo);
+ WriteParam(aWriter, aParam.mScrollParentId);
+ WriteParam(aWriter, aParam.GetContentDescription());
+ WriteParam(aWriter, aParam.mLineScrollAmount);
+ WriteParam(aWriter, aParam.mPageScrollAmount);
+ WriteParam(aWriter, aParam.mHasScrollgrab);
+ WriteParam(aWriter, aParam.mIsLayersIdRoot);
+ WriteParam(aWriter, aParam.mIsAutoDirRootContentRTL);
+ WriteParam(aWriter, aParam.mForceDisableApz);
+ WriteParam(aWriter, aParam.mResolutionUpdated);
+ WriteParam(aWriter, aParam.mIsRDMTouchSimulationActive);
+ WriteParam(aWriter, aParam.mDidContentGetPainted);
+ WriteParam(aWriter, aParam.mForceMousewheelAutodir);
+ WriteParam(aWriter, aParam.mForceMousewheelAutodirHonourRoot);
+ WriteParam(aWriter, aParam.mIsPaginatedPresentation);
+ WriteParam(aWriter, aParam.mDisregardedDirection);
+ WriteParam(aWriter, aParam.mOverscrollBehavior);
+ WriteParam(aWriter, aParam.mScrollUpdates);
+ }
+
+ static bool ReadContentDescription(MessageReader* aReader,
+ paramType* aResult) {
+ nsCString str;
+ if (!ReadParam(aReader, &str)) {
+ return false;
+ }
+ aResult->SetContentDescription(str);
+ return true;
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ return (ReadParam(aReader, &aResult->mMetrics) &&
+ ReadParam(aReader, &aResult->mSnapInfo) &&
+ ReadParam(aReader, &aResult->mScrollParentId) &&
+ ReadContentDescription(aReader, aResult) &&
+ ReadParam(aReader, &aResult->mLineScrollAmount) &&
+ ReadParam(aReader, &aResult->mPageScrollAmount) &&
+ ReadBoolForBitfield(aReader, aResult,
+ &paramType::SetHasScrollgrab) &&
+ ReadBoolForBitfield(aReader, aResult,
+ &paramType::SetIsLayersIdRoot) &&
+ ReadBoolForBitfield(aReader, aResult,
+ &paramType::SetIsAutoDirRootContentRTL) &&
+ ReadBoolForBitfield(aReader, aResult,
+ &paramType::SetForceDisableApz) &&
+ ReadBoolForBitfield(aReader, aResult,
+ &paramType::SetResolutionUpdated) &&
+ ReadBoolForBitfield(aReader, aResult,
+ &paramType::SetIsRDMTouchSimulationActive)) &&
+ ReadBoolForBitfield(aReader, aResult,
+ &paramType::SetDidContentGetPainted) &&
+ ReadBoolForBitfield(aReader, aResult,
+ &paramType::SetForceMousewheelAutodir) &&
+ ReadBoolForBitfield(
+ aReader, aResult,
+ &paramType::SetForceMousewheelAutodirHonourRoot) &&
+ ReadBoolForBitfield(aReader, aResult,
+ &paramType::SetIsPaginatedPresentation) &&
+ ReadParam(aReader, &aResult->mDisregardedDirection) &&
+ ReadParam(aReader, &aResult->mOverscrollBehavior) &&
+ ReadParam(aReader, &aResult->mScrollUpdates);
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::layers::TextureFactoryIdentifier> {
+ typedef mozilla::layers::TextureFactoryIdentifier paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.mParentBackend);
+ WriteParam(aWriter, aParam.mWebRenderBackend);
+ WriteParam(aWriter, aParam.mWebRenderCompositor);
+ WriteParam(aWriter, aParam.mParentProcessType);
+ WriteParam(aWriter, aParam.mMaxTextureSize);
+ WriteParam(aWriter, aParam.mCompositorUseANGLE);
+ WriteParam(aWriter, aParam.mCompositorUseDComp);
+ WriteParam(aWriter, aParam.mUseCompositorWnd);
+ WriteParam(aWriter, aParam.mSupportsTextureBlitting);
+ WriteParam(aWriter, aParam.mSupportsPartialUploads);
+ WriteParam(aWriter, aParam.mSupportsComponentAlpha);
+ WriteParam(aWriter, aParam.mSupportsD3D11NV12);
+ WriteParam(aWriter, aParam.mSyncHandle);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ bool result = ReadParam(aReader, &aResult->mParentBackend) &&
+ ReadParam(aReader, &aResult->mWebRenderBackend) &&
+ ReadParam(aReader, &aResult->mWebRenderCompositor) &&
+ ReadParam(aReader, &aResult->mParentProcessType) &&
+ ReadParam(aReader, &aResult->mMaxTextureSize) &&
+ ReadParam(aReader, &aResult->mCompositorUseANGLE) &&
+ ReadParam(aReader, &aResult->mCompositorUseDComp) &&
+ ReadParam(aReader, &aResult->mUseCompositorWnd) &&
+ ReadParam(aReader, &aResult->mSupportsTextureBlitting) &&
+ ReadParam(aReader, &aResult->mSupportsPartialUploads) &&
+ ReadParam(aReader, &aResult->mSupportsComponentAlpha) &&
+ ReadParam(aReader, &aResult->mSupportsD3D11NV12) &&
+ ReadParam(aReader, &aResult->mSyncHandle);
+ return result;
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::layers::TextureInfo> {
+ typedef mozilla::layers::TextureInfo paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.mCompositableType);
+ WriteParam(aWriter, aParam.mTextureFlags);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ return ReadParam(aReader, &aResult->mCompositableType) &&
+ ReadParam(aReader, &aResult->mTextureFlags);
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::layers::CompositableType>
+ : public ContiguousEnumSerializer<
+ mozilla::layers::CompositableType,
+ mozilla::layers::CompositableType::UNKNOWN,
+ mozilla::layers::CompositableType::COUNT> {};
+
+template <>
+struct ParamTraits<mozilla::layers::ScrollableLayerGuid> {
+ typedef mozilla::layers::ScrollableLayerGuid paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.mLayersId);
+ WriteParam(aWriter, aParam.mPresShellId);
+ WriteParam(aWriter, aParam.mScrollId);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ return (ReadParam(aReader, &aResult->mLayersId) &&
+ ReadParam(aReader, &aResult->mPresShellId) &&
+ ReadParam(aReader, &aResult->mScrollId));
+ }
+};
+
+template <>
+struct ParamTraits<nsEventStatus>
+ : public ContiguousEnumSerializer<nsEventStatus, nsEventStatus_eIgnore,
+ nsEventStatus_eSentinel> {};
+
+template <>
+struct ParamTraits<mozilla::layers::APZHandledPlace>
+ : public ContiguousEnumSerializer<
+ mozilla::layers::APZHandledPlace,
+ mozilla::layers::APZHandledPlace::Unhandled,
+ mozilla::layers::APZHandledPlace::Last> {};
+
+template <>
+struct ParamTraits<mozilla::layers::ScrollDirections> {
+ typedef mozilla::layers::ScrollDirections paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.serialize());
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ uint8_t value;
+ if (!ReadParam(aReader, &value)) {
+ return false;
+ }
+ aResult->deserialize(value);
+ return true;
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::layers::APZHandledResult> {
+ typedef mozilla::layers::APZHandledResult paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.mPlace);
+ WriteParam(aWriter, aParam.mScrollableDirections);
+ WriteParam(aWriter, aParam.mOverscrollDirections);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ return (ReadParam(aReader, &aResult->mPlace) &&
+ ReadParam(aReader, &aResult->mScrollableDirections) &&
+ ReadParam(aReader, &aResult->mOverscrollDirections));
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::layers::APZEventResult> {
+ typedef mozilla::layers::APZEventResult paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.GetStatus());
+ WriteParam(aWriter, aParam.GetHandledResult());
+ WriteParam(aWriter, aParam.mTargetGuid);
+ WriteParam(aWriter, aParam.mInputBlockId);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ nsEventStatus status;
+ if (!ReadParam(aReader, &status)) {
+ return false;
+ }
+ aResult->UpdateStatus(status);
+
+ mozilla::Maybe<mozilla::layers::APZHandledResult> handledResult;
+ if (!ReadParam(aReader, &handledResult)) {
+ return false;
+ }
+ aResult->UpdateHandledResult(handledResult);
+
+ return (ReadParam(aReader, &aResult->mTargetGuid) &&
+ ReadParam(aReader, &aResult->mInputBlockId));
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::layers::ZoomConstraints> {
+ typedef mozilla::layers::ZoomConstraints paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.mAllowZoom);
+ WriteParam(aWriter, aParam.mAllowDoubleTapZoom);
+ WriteParam(aWriter, aParam.mMinZoom);
+ WriteParam(aWriter, aParam.mMaxZoom);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ return (ReadParam(aReader, &aResult->mAllowZoom) &&
+ ReadParam(aReader, &aResult->mAllowDoubleTapZoom) &&
+ ReadParam(aReader, &aResult->mMinZoom) &&
+ ReadParam(aReader, &aResult->mMaxZoom));
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::layers::FocusTarget::ScrollTargets> {
+ typedef mozilla::layers::FocusTarget::ScrollTargets paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.mHorizontal);
+ WriteParam(aWriter, aParam.mVertical);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ return ReadParam(aReader, &aResult->mHorizontal) &&
+ ReadParam(aReader, &aResult->mVertical);
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::layers::FocusTarget::NoFocusTarget>
+ : public EmptyStructSerializer<
+ mozilla::layers::FocusTarget::NoFocusTarget> {};
+
+template <>
+struct ParamTraits<mozilla::layers::FocusTarget> {
+ typedef mozilla::layers::FocusTarget paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.mSequenceNumber);
+ WriteParam(aWriter, aParam.mFocusHasKeyEventListeners);
+ WriteParam(aWriter, aParam.mData);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ if (!ReadParam(aReader, &aResult->mSequenceNumber) ||
+ !ReadParam(aReader, &aResult->mFocusHasKeyEventListeners) ||
+ !ReadParam(aReader, &aResult->mData)) {
+ return false;
+ }
+ return true;
+ }
+};
+
+template <>
+struct ParamTraits<
+ mozilla::layers::KeyboardScrollAction::KeyboardScrollActionType>
+ : public ContiguousEnumSerializerInclusive<
+ mozilla::layers::KeyboardScrollAction::KeyboardScrollActionType,
+ mozilla::layers::KeyboardScrollAction::KeyboardScrollActionType::
+ eScrollCharacter,
+ mozilla::layers::KeyboardScrollAction::
+ sHighestKeyboardScrollActionType> {};
+
+template <>
+struct ParamTraits<mozilla::layers::KeyboardScrollAction> {
+ typedef mozilla::layers::KeyboardScrollAction paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.mType);
+ WriteParam(aWriter, aParam.mForward);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ return ReadParam(aReader, &aResult->mType) &&
+ ReadParam(aReader, &aResult->mForward);
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::layers::KeyboardShortcut> {
+ typedef mozilla::layers::KeyboardShortcut paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.mAction);
+ WriteParam(aWriter, aParam.mKeyCode);
+ WriteParam(aWriter, aParam.mCharCode);
+ WriteParam(aWriter, aParam.mModifiers);
+ WriteParam(aWriter, aParam.mModifiersMask);
+ WriteParam(aWriter, aParam.mEventType);
+ WriteParam(aWriter, aParam.mDispatchToContent);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ return ReadParam(aReader, &aResult->mAction) &&
+ ReadParam(aReader, &aResult->mKeyCode) &&
+ ReadParam(aReader, &aResult->mCharCode) &&
+ ReadParam(aReader, &aResult->mModifiers) &&
+ ReadParam(aReader, &aResult->mModifiersMask) &&
+ ReadParam(aReader, &aResult->mEventType) &&
+ ReadParam(aReader, &aResult->mDispatchToContent);
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::layers::KeyboardMap> {
+ typedef mozilla::layers::KeyboardMap paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.Shortcuts());
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ nsTArray<mozilla::layers::KeyboardShortcut> shortcuts;
+ if (!ReadParam(aReader, &shortcuts)) {
+ return false;
+ }
+ *aResult = mozilla::layers::KeyboardMap(std::move(shortcuts));
+ return true;
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::layers::GeckoContentController_TapType>
+ : public ContiguousEnumSerializerInclusive<
+ mozilla::layers::GeckoContentController_TapType,
+ mozilla::layers::GeckoContentController_TapType::eSingleTap,
+ mozilla::layers::kHighestGeckoContentController_TapType> {};
+
+template <>
+struct ParamTraits<mozilla::layers::GeckoContentController_APZStateChange>
+ : public ContiguousEnumSerializerInclusive<
+ mozilla::layers::GeckoContentController_APZStateChange,
+ mozilla::layers::GeckoContentController_APZStateChange::
+ eTransformBegin,
+ mozilla::layers::kHighestGeckoContentController_APZStateChange> {};
+
+template <>
+struct ParamTraits<mozilla::layers::EventRegionsOverride>
+ : public BitFlagsEnumSerializer<
+ mozilla::layers::EventRegionsOverride,
+ mozilla::layers::EventRegionsOverride::ALL_BITS> {};
+
+template <>
+struct ParamTraits<mozilla::layers::AsyncDragMetrics> {
+ typedef mozilla::layers::AsyncDragMetrics paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.mViewId);
+ WriteParam(aWriter, aParam.mPresShellId);
+ WriteParam(aWriter, aParam.mDragStartSequenceNumber);
+ WriteParam(aWriter, aParam.mScrollbarDragOffset);
+ WriteParam(aWriter, aParam.mDirection);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ return (ReadParam(aReader, &aResult->mViewId) &&
+ ReadParam(aReader, &aResult->mPresShellId) &&
+ ReadParam(aReader, &aResult->mDragStartSequenceNumber) &&
+ ReadParam(aReader, &aResult->mScrollbarDragOffset) &&
+ ReadParam(aReader, &aResult->mDirection));
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::layers::BrowserGestureResponse>
+ : public ContiguousEnumSerializerInclusive<
+ mozilla::layers::BrowserGestureResponse,
+ mozilla::layers::BrowserGestureResponse::NotConsumed,
+ mozilla::layers::BrowserGestureResponse::Consumed> {};
+
+template <>
+struct ParamTraits<mozilla::layers::CompositorOptions> {
+ typedef mozilla::layers::CompositorOptions paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.mUseAPZ);
+ WriteParam(aWriter, aParam.mUseSoftwareWebRender);
+ WriteParam(aWriter, aParam.mAllowSoftwareWebRenderD3D11);
+ WriteParam(aWriter, aParam.mAllowSoftwareWebRenderOGL);
+ WriteParam(aWriter, aParam.mInitiallyPaused);
+ WriteParam(aWriter, aParam.mNeedFastSnaphot);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ return ReadParam(aReader, &aResult->mUseAPZ) &&
+ ReadParam(aReader, &aResult->mUseSoftwareWebRender) &&
+ ReadParam(aReader, &aResult->mAllowSoftwareWebRenderD3D11) &&
+ ReadParam(aReader, &aResult->mAllowSoftwareWebRenderOGL) &&
+ ReadParam(aReader, &aResult->mInitiallyPaused) &&
+ ReadParam(aReader, &aResult->mNeedFastSnaphot);
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::layers::OverlaySupportType>
+ : public ContiguousEnumSerializerInclusive<
+ mozilla::layers::OverlaySupportType,
+ mozilla::layers::OverlaySupportType::None,
+ mozilla::layers::OverlaySupportType::MAX> {};
+
+template <>
+struct ParamTraits<mozilla::layers::OverlayInfo> {
+ typedef mozilla::layers::OverlayInfo paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.mSupportsOverlays);
+ WriteParam(aWriter, aParam.mNv12Overlay);
+ WriteParam(aWriter, aParam.mYuy2Overlay);
+ WriteParam(aWriter, aParam.mBgra8Overlay);
+ WriteParam(aWriter, aParam.mRgb10a2Overlay);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ return ReadParam(aReader, &aResult->mSupportsOverlays) &&
+ ReadParam(aReader, &aResult->mNv12Overlay) &&
+ ReadParam(aReader, &aResult->mYuy2Overlay) &&
+ ReadParam(aReader, &aResult->mBgra8Overlay) &&
+ ReadParam(aReader, &aResult->mRgb10a2Overlay);
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::layers::SwapChainInfo> {
+ typedef mozilla::layers::SwapChainInfo paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.mTearingSupported);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ return ReadParam(aReader, &aResult->mTearingSupported);
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::layers::ScrollbarLayerType>
+ : public ContiguousEnumSerializerInclusive<
+ mozilla::layers::ScrollbarLayerType,
+ mozilla::layers::ScrollbarLayerType::None,
+ mozilla::layers::kHighestScrollbarLayerType> {};
+
+template <>
+struct ParamTraits<mozilla::layers::ScrollbarData> {
+ typedef mozilla::layers::ScrollbarData paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.mDirection);
+ WriteParam(aWriter, aParam.mScrollbarLayerType);
+ WriteParam(aWriter, aParam.mThumbRatio);
+ WriteParam(aWriter, aParam.mThumbStart);
+ WriteParam(aWriter, aParam.mThumbLength);
+ WriteParam(aWriter, aParam.mThumbMinLength);
+ WriteParam(aWriter, aParam.mThumbIsAsyncDraggable);
+ WriteParam(aWriter, aParam.mScrollTrackStart);
+ WriteParam(aWriter, aParam.mScrollTrackLength);
+ WriteParam(aWriter, aParam.mTargetViewId);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ return ReadParam(aReader, &aResult->mDirection) &&
+ ReadParam(aReader, &aResult->mScrollbarLayerType) &&
+ ReadParam(aReader, &aResult->mThumbRatio) &&
+ ReadParam(aReader, &aResult->mThumbStart) &&
+ ReadParam(aReader, &aResult->mThumbLength) &&
+ ReadParam(aReader, &aResult->mThumbMinLength) &&
+ ReadParam(aReader, &aResult->mThumbIsAsyncDraggable) &&
+ ReadParam(aReader, &aResult->mScrollTrackStart) &&
+ ReadParam(aReader, &aResult->mScrollTrackLength) &&
+ ReadParam(aReader, &aResult->mTargetViewId);
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::layers::CompositionPayloadType>
+ : public ContiguousEnumSerializerInclusive<
+ mozilla::layers::CompositionPayloadType,
+ mozilla::layers::CompositionPayloadType::eKeyPress,
+ mozilla::layers::kHighestCompositionPayloadType> {};
+
+template <>
+struct ParamTraits<mozilla::layers::CompositionPayload> {
+ typedef mozilla::layers::CompositionPayload paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.mType);
+ WriteParam(aWriter, aParam.mTimeStamp);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ return ReadParam(aReader, &aResult->mType) &&
+ ReadParam(aReader, &aResult->mTimeStamp);
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::RayReferenceData> {
+ typedef mozilla::RayReferenceData paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.mInitialPosition);
+ WriteParam(aWriter, aParam.mContainingBlockRect);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ return (ReadParam(aReader, &aResult->mInitialPosition) &&
+ ReadParam(aReader, &aResult->mContainingBlockRect));
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::layers::CantZoomOutBehavior>
+ : public ContiguousEnumSerializerInclusive<
+ mozilla::layers::CantZoomOutBehavior,
+ mozilla::layers::CantZoomOutBehavior::Nothing,
+ mozilla::layers::CantZoomOutBehavior::ZoomIn> {};
+
+template <>
+struct ParamTraits<mozilla::layers::ZoomTarget> {
+ typedef mozilla::layers::ZoomTarget paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.targetRect);
+ WriteParam(aWriter, aParam.cantZoomOutBehavior);
+ WriteParam(aWriter, aParam.elementBoundingRect);
+ WriteParam(aWriter, aParam.documentRelativePointerPosition);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ return (ReadParam(aReader, &aResult->targetRect) &&
+ ReadParam(aReader, &aResult->cantZoomOutBehavior) &&
+ ReadParam(aReader, &aResult->elementBoundingRect) &&
+ ReadParam(aReader, &aResult->documentRelativePointerPosition));
+ }
+};
+
+#define IMPL_PARAMTRAITS_BY_SERDE(type_) \
+ template <> \
+ struct ParamTraits<mozilla::type_> { \
+ typedef mozilla::type_ paramType; \
+ static void Write(MessageWriter* aWriter, const paramType& aParam) { \
+ mozilla::ipc::ByteBuf v; \
+ mozilla::DebugOnly<bool> rv = Servo_##type_##_Serialize(&aParam, &v); \
+ MOZ_ASSERT(rv, "Serialize ##type_## failed"); \
+ WriteParam(aWriter, std::move(v)); \
+ } \
+ static ReadResult<paramType> Read(MessageReader* aReader) { \
+ mozilla::ipc::ByteBuf in; \
+ ReadResult<paramType> result; \
+ if (!ReadParam(aReader, &in) || !in.mData) { \
+ return result; \
+ } \
+ /* TODO: Should be able to initialize `result` in-place instead */ \
+ mozilla::AlignedStorage2<paramType> value; \
+ if (!Servo_##type_##_Deserialize(&in, value.addr())) { \
+ return result; \
+ } \
+ result = std::move(*value.addr()); \
+ value.addr()->~paramType(); \
+ return result; \
+ } \
+ };
+
+IMPL_PARAMTRAITS_BY_SERDE(LengthPercentage)
+IMPL_PARAMTRAITS_BY_SERDE(StyleOffsetPath)
+IMPL_PARAMTRAITS_BY_SERDE(StyleOffsetRotate)
+IMPL_PARAMTRAITS_BY_SERDE(StylePositionOrAuto)
+IMPL_PARAMTRAITS_BY_SERDE(StyleRotate)
+IMPL_PARAMTRAITS_BY_SERDE(StyleScale)
+IMPL_PARAMTRAITS_BY_SERDE(StyleTranslate)
+IMPL_PARAMTRAITS_BY_SERDE(StyleTransform)
+IMPL_PARAMTRAITS_BY_SERDE(StyleComputedTimingFunction)
+
+} /* namespace IPC */
+
+#endif /* mozilla_layers_LayersMessageUtils */
diff --git a/gfx/layers/ipc/LayersMessages.ipdlh b/gfx/layers/ipc/LayersMessages.ipdlh
new file mode 100644
index 0000000000..94abbcdde1
--- /dev/null
+++ b/gfx/layers/ipc/LayersMessages.ipdlh
@@ -0,0 +1,362 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: sw=2 ts=8 et :
+ */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+include LayersSurfaces;
+include protocol PCompositorBridge;
+include protocol PTexture;
+
+include "gfxipc/ShadowLayerUtils.h";
+include "mozilla/GfxMessageUtils.h";
+include "mozilla/layers/LayersMessageUtils.h";
+
+using mozilla::gfx::Glyph from "mozilla/gfx/2D.h";
+using mozilla::gfx::SamplingFilter from "mozilla/gfx/2D.h";
+using struct mozilla::gfx::DeviceColor from "mozilla/gfx/2D.h";
+using mozilla::gfx::Point from "mozilla/gfx/Point.h";
+using mozilla::gfx::Point3D from "mozilla/gfx/Point.h";
+using mozilla::gfx::IntPoint from "mozilla/gfx/Point.h";
+using mozilla::gfx::Matrix4x4 from "mozilla/gfx/Matrix.h";
+using mozilla::SideBits from "mozilla/gfx/Types.h";
+using nscolor from "nsColor.h";
+using nscoord from "nsCoord.h";
+using struct nsRect from "nsRect.h";
+using struct nsPoint from "nsPoint.h";
+using mozilla::TimeDuration from "mozilla/TimeStamp.h";
+using class mozilla::TimeStamp from "mozilla/TimeStamp.h";
+using mozilla::ScreenRotation from "mozilla/WidgetUtils.h";
+using nsCSSPropertyID from "nsCSSPropertyID.h";
+using mozilla::hal::ScreenOrientation from "mozilla/HalIPCUtils.h";
+using struct mozilla::layers::TextureInfo from "mozilla/layers/CompositorTypes.h";
+using mozilla::CSSPoint from "Units.h";
+using mozilla::CSSRect from "Units.h";
+using mozilla::gfx::IntSize from "mozilla/gfx/2D.h";
+using mozilla::LayerMargin from "Units.h";
+using mozilla::LayerPoint from "Units.h";
+using mozilla::LayerCoord from "Units.h";
+using mozilla::LayerSize from "Units.h";
+using mozilla::LayerRect from "Units.h";
+using mozilla::LayerIntSize from "Units.h";
+using mozilla::LayerIntRect from "Units.h";
+using mozilla::LayerIntRegion from "Units.h";
+using mozilla::layers::TextureFlags from "mozilla/layers/CompositorTypes.h";
+using mozilla::ParentLayerIntRect from "Units.h";
+using mozilla::ParentLayerRect from "Units.h";
+using mozilla::LayoutDeviceIntRect from "Units.h";
+using mozilla::LayoutDevicePoint from "Units.h";
+using mozilla::LayoutDeviceRect from "Units.h";
+using mozilla::layers::ScaleMode from "mozilla/layers/LayersTypes.h";
+using mozilla::layers::EventRegionsOverride from "mozilla/layers/LayersTypes.h";
+using mozilla::layers::DiagnosticTypes from "mozilla/layers/CompositorTypes.h";
+using mozilla::layers::ScrollableLayerGuid::ViewID from "mozilla/layers/ScrollableLayerGuid.h";
+using mozilla::layers::ScrollDirection from "mozilla/layers/LayersTypes.h";
+using mozilla::layers::LayersBackend from "mozilla/layers/LayersTypes.h";
+using mozilla::layers::LayerHandle from "mozilla/layers/LayersTypes.h";
+using mozilla::layers::CompositableHandle from "mozilla/layers/LayersTypes.h";
+using mozilla::layers::CompositionPayload from "mozilla/layers/LayersTypes.h";
+[MoveOnly] using mozilla::CrossProcessSemaphoreHandle from "mozilla/ipc/CrossProcessSemaphore.h";
+using struct mozilla::void_t from "mozilla/ipc/IPCCore.h";
+using mozilla::layers::LayersId from "mozilla/layers/LayersTypes.h";
+using mozilla::layers::RemoteTextureId from "mozilla/layers/LayersTypes.h";
+using mozilla::layers::RemoteTextureOwnerId from "mozilla/layers/LayersTypes.h";
+using mozilla::layers::TransactionId from "mozilla/layers/LayersTypes.h";
+using mozilla::LengthPercentage from "mozilla/ServoStyleConsts.h";
+using mozilla::RayReferenceData from "mozilla/MotionPathUtils.h";
+using mozilla::StyleOffsetPath from "mozilla/ServoStyleConsts.h";
+using mozilla::StyleOffsetRotate from "mozilla/ServoStyleConsts.h";
+using mozilla::StylePositionOrAuto from "mozilla/ServoStyleConsts.h";
+using mozilla::StyleRotate from "mozilla/ServoStyleConsts.h";
+using mozilla::StyleScale from "mozilla/ServoStyleConsts.h";
+using mozilla::StyleTranslate from "mozilla/ServoStyleConsts.h";
+using mozilla::StyleTransform from "mozilla/ServoStyleConsts.h";
+using mozilla::StyleComputedTimingFunction from "mozilla/ServoStyleConsts.h";
+
+namespace mozilla {
+namespace layers {
+
+struct TargetConfig {
+ IntRect naturalBounds;
+ ScreenRotation rotation;
+ ScreenOrientation orientation;
+ nsIntRegion clearRegion;
+};
+
+struct OpAttachCompositable {
+ LayerHandle layer;
+ CompositableHandle compositable;
+};
+
+struct OpAttachAsyncCompositable {
+ LayerHandle layer;
+ CompositableHandle compositable;
+};
+struct LayerColor { DeviceColor value; };
+
+[Comparable] union Animatable {
+ null_t;
+ float;
+ nscolor;
+ StyleRotate;
+ StyleScale;
+ StyleTranslate;
+ StyleTransform;
+ StyleOffsetPath;
+ LengthPercentage;
+ StyleOffsetRotate;
+ StylePositionOrAuto;
+};
+
+struct AnimationSegment {
+ Animatable startState;
+ Animatable endState;
+ float startPortion;
+ float endPortion;
+ uint8_t startComposite;
+ uint8_t endComposite;
+ StyleComputedTimingFunction? sampleFn;
+};
+
+[Comparable] struct MotionPathData {
+ // the transform-origin property for motion in css pixels
+ CSSPoint origin;
+ // the adjustment for the anchor point of motion path.
+ CSSPoint anchorAdjustment;
+ RayReferenceData rayReferenceData;
+};
+
+[Comparable] struct PartialPrerenderData {
+ LayoutDeviceRect rect;
+ SideBits overflowedSides;
+ // the scroll id of the nearest scrollable frame of this partial prerender
+ // data.
+ ViewID scrollId;
+ // The clip rectangle of the nearest scrollable frame.
+ // NOTE: This should be used only for fallback cases where APZ is not enabled.
+ ParentLayerRect clipRect;
+ // a transform from the coordinate space of the animated element to a
+ // coordinate space where the `clipRect` can be applied.
+ Matrix4x4 transformInClip; // Used only for WebRender.
+ // the position relative to the reference frame of the animated transform
+ // element in the element coordinate space.
+ LayoutDevicePoint position; // Used only for WebRender.
+};
+
+// Transforms need extra information to correctly convert the list of transform
+// functions to a Matrix4x4 that can be applied directly to the layer.
+[Comparable] struct TransformData {
+ // the origin of the frame being transformed in app units
+ nsPoint origin;
+ // the transform-origin property for the transform in device pixels
+ Point3D transformOrigin;
+ nsRect bounds;
+ int32_t appUnitsPerDevPixel;
+ MotionPathData? motionPathData;
+ PartialPrerenderData? partialPrerenderData;
+};
+
+// The scroll timeline information.
+struct ScrollTimelineOptions {
+ // The source of the scroll-timeline.
+ ViewID source;
+ // The physical direction.
+ ScrollDirection axis;
+};
+
+struct Animation {
+ // The zero time of this Animation's timeline. May be null if isNotPlaying is
+ // true.
+ TimeStamp originTime;
+ // The start time is relative to the originTime. This allows us to represent
+ // start times in the distant past that cannot be expressed using a TimeStamp.
+ TimeDuration? startTime;
+ TimeDuration delay;
+ TimeDuration endDelay;
+ // The value of the animation's current time at the moment it was sent to the
+ // compositor. This value will be used for below cases:
+ // 1) Animations that are play-pending. Initially these animations will have a
+ // null |startTime|. Once the animation is ready to start (i.e. painting
+ // has finished), we calculate an appropriate value of |startTime| such
+ // that playback begins from |holdTime|.
+ // 2) Not playing animations (e.g. paused and finished animations). In this
+ // case the |holdTime| represents the current time the animation will
+ // maintain.
+ TimeDuration holdTime;
+ TimeDuration duration;
+ // For each frame, the interpolation point is computed based on the
+ // startTime, the direction, the duration, and the current time.
+ // The segments must uniquely cover the portion from 0.0 to 1.0
+ AnimationSegment[] segments;
+ // Number of times to repeat the animation, including positive infinity.
+ // Values <= 0 mean the animation will not play (although events are still
+ // dispatched on the main thread).
+ float iterations;
+ float iterationStart;
+ // This uses the NS_STYLE_ANIMATION_DIRECTION_* constants.
+ uint8_t direction;
+ // This uses dom::FillMode.
+ uint8_t fillMode;
+ nsCSSPropertyID property;
+ float playbackRate;
+ // When performing an asynchronous update to the playbackRate, |playbackRate|
+ // above is the updated playbackRate while |previousPlaybackRate| is the
+ // existing playbackRate. This is used by AnimationInfo to update the
+ // startTime based on the 'readyTime' (timestamp at the end of painting)
+ // and is not used beyond that point.
+ //
+ // It is set to numeric_limits<float>::quiet_NaN() when no asynchronous update
+ // to the playbackRate is being performed.
+ float previousPlaybackRate;
+ // This is used in the transformed progress calculation.
+ StyleComputedTimingFunction? easingFunction;
+ uint8_t iterationComposite;
+ // True if the animation has a fixed current time (e.g. paused and
+ // forward-filling animations).
+ bool isNotPlaying;
+ // True if this is not an animating property. For some transform-like
+ // properties, we just send their baseStyles for merging with other animating
+ // properties. In this case, we don't have animation information on this
+ // property, and so don't need to do interpolation.
+ bool isNotAnimating;
+ // The base style that animations should composite with. This is only set for
+ // 1. scroll animations with positive delays, or
+ // 2. animations with a composite mode of additive or accumulate, and only for
+ // the first animation in the set (i.e. the animation that is lowest in the
+ // stack).
+ // In all other cases the value is null_t.
+ Animatable baseStyle;
+ // An optional data specific for transform like properies.
+ TransformData? transformData;
+ // If this is present, the animation is driven by a ScrollTimeline, and
+ // this structure contains information about that timeline.
+ ScrollTimelineOptions? scrollTimelineOptions;
+};
+
+struct CompositorAnimations {
+ Animation[] animations;
+ // This id is used to map the layer animations between content
+ // and compositor side
+ uint64_t id;
+};
+
+struct ShmemSection {
+ Shmem shmem;
+ uint32_t offset;
+ uint32_t size;
+};
+
+struct CrossProcessSemaphoreDescriptor {
+ CrossProcessSemaphoreHandle sem;
+};
+
+union ReadLockDescriptor {
+ ShmemSection;
+ CrossProcessSemaphoreDescriptor;
+ uintptr_t;
+ null_t;
+};
+
+/**
+ * Tells the CompositableHost to remove the corresponding TextureHost
+ */
+struct OpRemoveTexture {
+ PTexture texture;
+};
+
+struct TimedTexture {
+ PTexture texture;
+ TimeStamp timeStamp;
+ IntRect picture;
+ uint32_t frameID;
+ uint32_t producerID;
+ bool readLocked;
+};
+
+/**
+ * Tells the compositor-side which textures to use (for example, as front buffer
+ * if there are several textures for double buffering).
+ * This provides a list of textures with timestamps, ordered by timestamp.
+ * The newest texture whose timestamp is <= the current time is rendered
+ * (where null is considered less than every other timestamp). If there is no
+ * such texture, the first texture is rendered.
+ * The first timestamp value can be null, but the others must not be.
+ * The list must not be empty.
+ */
+struct OpUseTexture {
+ TimedTexture[] textures;
+};
+
+struct OpUseRemoteTexture {
+ RemoteTextureId textureId;
+ RemoteTextureOwnerId ownerId;
+ IntSize size;
+ TextureFlags textureFlags;
+};
+
+struct OpEnableRemoteTexturePushCallback {
+ RemoteTextureOwnerId ownerId;
+ IntSize size;
+ TextureFlags textureFlags;
+};
+
+struct OpNotifyNotUsed {
+ uint64_t TextureId;
+ uint64_t fwdTransactionId;
+};
+
+union CompositableOperationDetail {
+ OpRemoveTexture;
+
+ OpUseTexture;
+
+ OpUseRemoteTexture;
+
+ OpEnableRemoteTexturePushCallback;
+};
+
+struct CompositableOperation {
+ CompositableHandle compositable;
+ CompositableOperationDetail detail;
+};
+
+// Operations related to destroying resources, always handled after the other
+// operations for safety.
+union OpDestroy {
+ PTexture;
+ CompositableHandle;
+};
+
+// Replies to operations
+
+struct OpContentBufferSwap {
+ CompositableHandle compositable;
+ nsIntRegion frontUpdatedRegion;
+};
+
+/**
+ * An ImageCompositeNotification is sent the first time a particular
+ * image is composited by an ImageHost.
+ */
+struct ImageCompositeNotification {
+ CompositableHandle compositable;
+ TimeStamp imageTimeStamp;
+ TimeStamp firstCompositeTimeStamp;
+ uint32_t frameID;
+ uint32_t producerID;
+};
+
+union AsyncParentMessageData {
+ OpNotifyNotUsed;
+};
+
+union OMTAValue {
+ null_t;
+ nscolor;
+ float;
+ Matrix4x4;
+};
+
+} // namespace
+} // namespace
diff --git a/gfx/layers/ipc/LayersSurfaces.ipdlh b/gfx/layers/ipc/LayersSurfaces.ipdlh
new file mode 100644
index 0000000000..c7f432ccb6
--- /dev/null
+++ b/gfx/layers/ipc/LayersSurfaces.ipdlh
@@ -0,0 +1,205 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+include "gfxipc/ShadowLayerUtils.h";
+include "mozilla/GfxMessageUtils.h";
+include "mozilla/layers/LayersMessageUtils.h";
+
+using gfxPoint from "gfxPoint.h";
+using nsIntRegion from "nsRegion.h";
+using mozilla::StereoMode from "ImageTypes.h";
+using struct mozilla::null_t from "mozilla/ipc/IPCCore.h";
+using mozilla::WindowsHandle from "mozilla/ipc/IPCTypes.h";
+using mozilla::gfx::YUVColorSpace from "mozilla/gfx/Types.h";
+using mozilla::gfx::ChromaSubsampling from "mozilla/gfx/Types.h";
+using mozilla::gfx::ColorDepth from "mozilla/gfx/Types.h";
+using mozilla::gfx::ColorRange from "mozilla/gfx/Types.h";
+using mozilla::gfx::ColorSpace2 from "mozilla/gfx/Types.h";
+using mozilla::gfx::SurfaceFormat from "mozilla/gfx/Types.h";
+using mozilla::gfx::IntRect from "mozilla/gfx/Rect.h";
+using mozilla::gfx::IntSize from "mozilla/gfx/Point.h";
+using mozilla::gfx::Matrix4x4 from "mozilla/gfx/Matrix.h";
+[MoveOnly] using mozilla::ipc::SharedMemoryBasic::Handle from "mozilla/ipc/SharedMemoryBasic.h";
+using gfxImageFormat from "gfxTypes.h";
+using mozilla::layers::MaybeVideoBridgeSource from "mozilla/layers/VideoBridgeUtils.h";
+using mozilla::layers::RemoteTextureId from "mozilla/layers/LayersTypes.h";
+using mozilla::layers::RemoteTextureOwnerId from "mozilla/layers/LayersTypes.h";
+using mozilla::layers::GpuProcessTextureId from "mozilla/layers/LayersTypes.h";
+
+namespace mozilla {
+namespace layers {
+
+[Comparable] struct SurfaceDescriptorD3D10 {
+ WindowsHandle handle;
+ GpuProcessTextureId? gpuProcessTextureId;
+ uint32_t arrayIndex;
+ SurfaceFormat format;
+ IntSize size;
+ ColorSpace2 colorSpace;
+ ColorRange colorRange;
+};
+
+[Comparable] struct SurfaceDescriptorDXGIYCbCr {
+ WindowsHandle handleY;
+ WindowsHandle handleCb;
+ WindowsHandle handleCr;
+ IntSize size;
+ IntSize sizeY;
+ IntSize sizeCbCr;
+ ColorDepth colorDepth;
+ YUVColorSpace yUVColorSpace;
+ ColorRange colorRange;
+};
+
+[Comparable] struct SurfaceDescriptorMacIOSurface {
+ uint32_t surfaceId;
+ bool isOpaque;
+ YUVColorSpace yUVColorSpace;
+};
+
+[Comparable] struct SurfaceDescriptorDMABuf {
+ uint32_t bufferType;
+ uint64_t[] modifier;
+ uint32_t flags;
+ FileDescriptor[] fds;
+ uint32_t[] width;
+ uint32_t[] height;
+ uint32_t[] widthAligned;
+ uint32_t[] heightAligned;
+ uint32_t[] format;
+ uint32_t[] strides;
+ uint32_t[] offsets;
+ YUVColorSpace yUVColorSpace;
+ ColorRange colorRange;
+ FileDescriptor[] fence;
+ uint32_t uid;
+ FileDescriptor[] refCount;
+};
+
+[Comparable] struct SurfaceTextureDescriptor {
+ uint64_t handle;
+ IntSize size;
+ SurfaceFormat format;
+ bool continuous;
+ Matrix4x4? transformOverride;
+};
+
+[Comparable] struct SurfaceDescriptorAndroidHardwareBuffer {
+ uint64_t bufferId;
+ IntSize size;
+ SurfaceFormat format;
+};
+
+[Comparable] struct EGLImageDescriptor {
+ uintptr_t image; // `EGLImage` is a `void*`.
+ uintptr_t fence;
+ IntSize size;
+ bool hasAlpha;
+};
+
+[Comparable] struct SurfaceDescriptorSharedGLTexture {
+ uint32_t texture;
+ uint32_t target;
+ uintptr_t fence;
+ IntSize size;
+ bool hasAlpha;
+};
+
+[Comparable] struct SurfaceDescriptorDcompSurface {
+ FileDescriptor handle;
+ IntSize size;
+ SurfaceFormat format;
+};
+
+[Comparable] union RemoteDecoderVideoSubDescriptor {
+ SurfaceDescriptorD3D10;
+ SurfaceDescriptorDXGIYCbCr;
+ SurfaceDescriptorDMABuf;
+ SurfaceDescriptorMacIOSurface;
+ SurfaceDescriptorDcompSurface;
+ null_t;
+};
+
+[Comparable] struct SurfaceDescriptorRemoteDecoder {
+ uint64_t handle;
+ RemoteDecoderVideoSubDescriptor subdesc;
+ MaybeVideoBridgeSource source;
+};
+
+[Comparable] union SurfaceDescriptorGPUVideo {
+ SurfaceDescriptorRemoteDecoder;
+};
+
+[Comparable] struct RGBDescriptor {
+ IntSize size;
+ SurfaceFormat format;
+};
+
+[Comparable] struct YCbCrDescriptor {
+ IntRect display;
+ IntSize ySize;
+ uint32_t yStride;
+ IntSize cbCrSize;
+ uint32_t cbCrStride;
+ uint32_t yOffset;
+ uint32_t cbOffset;
+ uint32_t crOffset;
+ StereoMode stereoMode;
+ ColorDepth colorDepth;
+ YUVColorSpace yUVColorSpace;
+ ColorRange colorRange;
+ ChromaSubsampling chromaSubsampling;
+};
+
+[Comparable] union BufferDescriptor {
+ RGBDescriptor;
+ YCbCrDescriptor;
+};
+
+[Comparable] union MemoryOrShmem {
+ uintptr_t;
+ Shmem;
+};
+
+[Comparable] struct SurfaceDescriptorBuffer {
+ BufferDescriptor desc;
+ MemoryOrShmem data;
+};
+
+[Comparable] struct SurfaceDescriptorShared
+{
+ IntSize size;
+ int32_t stride;
+ SurfaceFormat format;
+ Handle handle;
+};
+
+[Comparable] struct SurfaceDescriptorRecorded {
+ int64_t textureId;
+};
+
+[Comparable] struct SurfaceDescriptorRemoteTexture {
+ RemoteTextureId textureId;
+ RemoteTextureOwnerId ownerId;
+};
+
+[Comparable] union SurfaceDescriptor {
+ SurfaceDescriptorBuffer;
+ SurfaceDescriptorD3D10;
+ SurfaceDescriptorDXGIYCbCr;
+ SurfaceDescriptorDMABuf;
+ SurfaceTextureDescriptor;
+ SurfaceDescriptorAndroidHardwareBuffer;
+ EGLImageDescriptor;
+ SurfaceDescriptorMacIOSurface;
+ SurfaceDescriptorSharedGLTexture;
+ SurfaceDescriptorGPUVideo;
+ SurfaceDescriptorRecorded;
+ SurfaceDescriptorRemoteTexture;
+ SurfaceDescriptorDcompSurface;
+ null_t;
+};
+
+} // namespace
+} // namespace
diff --git a/gfx/layers/ipc/PAPZ.ipdl b/gfx/layers/ipc/PAPZ.ipdl
new file mode 100644
index 0000000000..5525a25edd
--- /dev/null
+++ b/gfx/layers/ipc/PAPZ.ipdl
@@ -0,0 +1,81 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: sw=2 ts=8 et :
+ */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+include "mozilla/GfxMessageUtils.h";
+include "mozilla/layers/LayersMessageUtils.h";
+include "mozilla/layers/RemoteContentController.h";
+
+include protocol PCompositorBridge;
+
+using mozilla::CSSRect from "Units.h";
+using struct mozilla::layers::RepaintRequest from "mozilla/layers/RepaintRequest.h";
+using struct mozilla::layers::ScrollableLayerGuid from "mozilla/layers/ScrollableLayerGuid.h";
+using mozilla::layers::ScrollableLayerGuid::ViewID from "mozilla/layers/ScrollableLayerGuid.h";
+using mozilla::layers::GeckoContentController_APZStateChange from "mozilla/layers/GeckoContentControllerTypes.h";
+using mozilla::layers::ScrollDirection from "mozilla/layers/LayersTypes.h";
+using mozilla::layers::MatrixMessage from "mozilla/layers/MatrixMessage.h";
+using mozilla::dom::TabId from "mozilla/dom/ipc/IdType.h";
+using mozilla::dom::ContentParentId from "mozilla/dom/ipc/IdType.h";
+using mozilla::layers::AsyncDragMetrics from "mozilla/layers/AsyncDragMetrics.h";
+using class nsRegion from "nsRegion.h";
+
+namespace mozilla {
+namespace layers {
+
+
+/**
+ * PAPZ is a protocol for remoting a GeckoContentController. PAPZ lives on the
+ * PCompositorBridge protocol which either connects to the compositor thread
+ * in the main process, or to the compositor thread in the gpu processs.
+ *
+ * PAPZParent lives in the compositor thread, while PAPZChild lives wherever the remoted
+ * GeckoContentController lives (generally the main thread of the main or content process).
+ * RemoteContentController implements PAPZParent, while APZChild implements PAPZChild.
+ *
+ * PAPZ is always used for ContentProcessController and only used for ChromeProcessController
+ * when there is a gpu process, otherwhise ChromeProcessController is used directly on the
+ * compositor thread. Only the methods that are used by the [Chrome,Content]ProcessController
+ * are implemented. If a new method is needed then PAPZ, APZChild, and RemoteContentController
+ * must be updated to handle it.
+ */
+[ManualDealloc, ParentImpl="RemoteContentController"]
+sync protocol PAPZ
+{
+ manager PCompositorBridge;
+
+parent:
+ async __delete__();
+
+child:
+ async LayerTransforms(MatrixMessage[] aTransforms);
+
+ [Priority=control]
+ async RequestContentRepaint(RepaintRequest request);
+
+ async UpdateOverscrollVelocity(ScrollableLayerGuid aGuid, float aX, float aY, bool aIsRootContent);
+
+ async UpdateOverscrollOffset(ScrollableLayerGuid aGuid, float aX, float aY, bool aIsRootContent);
+
+ async NotifyMozMouseScrollEvent(ViewID aScrollId, nsString aEvent);
+
+ async NotifyAPZStateChange(ScrollableLayerGuid aGuid, GeckoContentController_APZStateChange aChange, int aArg, uint64_t? aInputBlockId);
+
+ [Priority=control]
+ async NotifyFlushComplete();
+
+ async NotifyAsyncScrollbarDragInitiated(uint64_t aDragBlockId, ViewID aScrollId, ScrollDirection aDirection);
+
+ async NotifyAsyncScrollbarDragRejected(ViewID aScrollId);
+
+ async NotifyAsyncAutoscrollRejected(ViewID aScrollId);
+
+both:
+ async Destroy();
+};
+
+} // layers
+} // mozilla
diff --git a/gfx/layers/ipc/PAPZCTreeManager.ipdl b/gfx/layers/ipc/PAPZCTreeManager.ipdl
new file mode 100644
index 0000000000..9e814fa363
--- /dev/null
+++ b/gfx/layers/ipc/PAPZCTreeManager.ipdl
@@ -0,0 +1,95 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+include "mozilla/GfxMessageUtils.h";
+include "mozilla/layers/LayersMessageUtils.h";
+include "ipc/nsGUIEventIPC.h";
+
+include protocol PCompositorBridge;
+
+using mozilla::CSSRect from "Units.h";
+using mozilla::LayoutDeviceCoord from "Units.h";
+using mozilla::LayoutDevicePoint from "Units.h";
+using mozilla::ScreenPoint from "Units.h";
+using mozilla::layers::ZoomTarget from "mozilla/layers/DoubleTapToZoom.h";
+using mozilla::layers::ZoomConstraints from "mozilla/layers/ZoomConstraints.h";
+using mozilla::layers::ScrollableLayerGuid from "mozilla/layers/ScrollableLayerGuid.h";
+using mozilla::layers::ScrollableLayerGuid::ViewID from "mozilla/layers/ScrollableLayerGuid.h";
+using mozilla::layers::TouchBehaviorFlags from "mozilla/layers/LayersTypes.h";
+using mozilla::layers::AsyncDragMetrics from "mozilla/layers/AsyncDragMetrics.h";
+using mozilla::layers::GeckoContentController_TapType from "mozilla/layers/GeckoContentControllerTypes.h";
+using mozilla::layers::APZHandledResult from "mozilla/layers/APZInputBridge.h";
+using mozilla::layers::BrowserGestureResponse from "mozilla/layers/APZInputBridge.h";
+using class mozilla::layers::KeyboardMap from "mozilla/layers/KeyboardMap.h";
+using mozilla::wr::RenderRoot from "mozilla/webrender/WebRenderTypes.h";
+
+using mozilla::Modifiers from "mozilla/EventForwards.h";
+using mozilla::PinchGestureInput::PinchGestureType from "InputData.h";
+
+namespace mozilla {
+namespace layers {
+
+/**
+ * PAPZCTreeManager is a protocol for remoting an IAPZCTreeManager. PAPZCTreeManager
+ * lives on the PCompositorBridge protocol which either connects to the compositor
+ * thread in the main process, or to the compositor thread in the gpu processs.
+ *
+ * PAPZCTreeManagerParent lives in the compositor thread, while PAPZCTreeManagerChild
+ * lives in the main thread of the main or the content process. APZCTreeManagerParent
+ * and APZCTreeManagerChild implement this protocol.
+ */
+[ManualDealloc]
+protocol PAPZCTreeManager
+{
+manager PCompositorBridge;
+
+parent:
+
+ // These messages correspond to the methods
+ // on the IAPZCTreeManager interface
+
+ async ZoomToRect(ScrollableLayerGuid aGuid, ZoomTarget aZoomTarget, uint32_t Flags);
+
+ async ContentReceivedInputBlock(uint64_t aInputBlockId, bool PreventDefault);
+
+ async SetTargetAPZC(uint64_t aInputBlockId, ScrollableLayerGuid[] Targets);
+
+ async UpdateZoomConstraints(ScrollableLayerGuid aGuid, ZoomConstraints? aConstraints);
+
+ async SetKeyboardMap(KeyboardMap aKeyboardMap);
+
+ async SetDPI(float aDpiValue);
+
+ async SetAllowedTouchBehavior(uint64_t aInputBlockId, TouchBehaviorFlags[] aValues);
+
+ async StartScrollbarDrag(ScrollableLayerGuid aGuid, AsyncDragMetrics aDragMetrics);
+
+ async StartAutoscroll(ScrollableLayerGuid aGuid, ScreenPoint aAnchorLocation);
+
+ async StopAutoscroll(ScrollableLayerGuid aGuid);
+
+ async SetLongTapEnabled(bool aTapGestureEnabled);
+
+ async SetBrowserGestureResponse(uint64_t aInputBlockId,
+ BrowserGestureResponse aResponse);
+
+ async __delete__();
+
+child:
+
+ async HandleTap(GeckoContentController_TapType aType, LayoutDevicePoint point, Modifiers aModifiers,
+ ScrollableLayerGuid aGuid, uint64_t aInputBlockId);
+
+ async NotifyPinchGesture(PinchGestureType aType, ScrollableLayerGuid aGuid,
+ LayoutDevicePoint aFocusPoint, LayoutDeviceCoord aSpanChange,
+ Modifiers aModifiers);
+
+ async CancelAutoscroll(ViewID aScrollId);
+
+ async NotifyScaleGestureComplete(ViewID aScrollId, float aScale);
+};
+
+} // namespace gfx
+} // namespace mozilla
diff --git a/gfx/layers/ipc/PAPZInputBridge.ipdl b/gfx/layers/ipc/PAPZInputBridge.ipdl
new file mode 100644
index 0000000000..20a569c1d2
--- /dev/null
+++ b/gfx/layers/ipc/PAPZInputBridge.ipdl
@@ -0,0 +1,87 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+include "ipc/nsGUIEventIPC.h";
+
+using mozilla::LayoutDeviceIntPoint from "Units.h";
+using struct mozilla::layers::ScrollableLayerGuid from "mozilla/layers/ScrollableLayerGuid.h";
+using struct mozilla::layers::APZEventResult from "mozilla/layers/APZInputBridge.h";
+using struct mozilla::layers::APZHandledResult from "mozilla/layers/APZInputBridge.h";
+
+using mozilla::EventMessage from "mozilla/EventForwards.h";
+using class mozilla::MultiTouchInput from "InputData.h";
+using class mozilla::MouseInput from "InputData.h";
+using class mozilla::PanGestureInput from "InputData.h";
+using class mozilla::PinchGestureInput from "InputData.h";
+using class mozilla::TapGestureInput from "InputData.h";
+using class mozilla::ScrollWheelInput from "InputData.h";
+using class mozilla::KeyboardInput from "InputData.h";
+
+using mozilla::layers::LayersId from "mozilla/layers/LayersTypes.h";
+
+namespace mozilla {
+namespace layers {
+
+/**
+ * This protocol is used to send input events from the UI process to the
+ * GPU process for handling by APZ. There is one instance per top-level
+ * compositor, or in other words, one instance per concrete APZCTreeManager
+ * instance. The child side lives on the controller thread in the UI process,
+ * ie the main thread on most platforms, but the Android UI thread on Android.
+ * The parent side lives on the main thread in the GPU process. If there is no
+ * GPU process, then this protocol is not instantiated.
+ */
+sync protocol PAPZInputBridge
+{
+parent:
+ // The following messages are used to
+ // implement the ReceiveInputEvent methods
+
+ sync ReceiveMultiTouchInputEvent(MultiTouchInput aEvent, bool aWantsCallback)
+ returns(APZEventResult aOutResult, MultiTouchInput aOutEvent);
+
+ sync ReceiveMouseInputEvent(MouseInput aEvent, bool aWantsCallback)
+ returns (APZEventResult aOutResult,
+ MouseInput aOutEvent);
+
+ sync ReceivePanGestureInputEvent(PanGestureInput aEvent, bool aWantsCallback)
+ returns (APZEventResult aOutResult,
+ PanGestureInput aOutEvent);
+
+ sync ReceivePinchGestureInputEvent(PinchGestureInput aEvent,
+ bool aWantsCallback)
+ returns (APZEventResult aOutResult,
+ PinchGestureInput aOutEvent);
+
+ sync ReceiveTapGestureInputEvent(TapGestureInput aEvent, bool aWantsCallback)
+ returns (APZEventResult aOutResult,
+ TapGestureInput aOutEvent);
+
+ sync ReceiveScrollWheelInputEvent(ScrollWheelInput aEvent,
+ bool aWantsCallback)
+ returns (APZEventResult aOutResult,
+ ScrollWheelInput aOutEvent);
+
+ sync ReceiveKeyboardInputEvent(KeyboardInput aEvent, bool aWantsCallback)
+ returns (APZEventResult aOutResult,
+ KeyboardInput aOutEvent);
+
+ async UpdateWheelTransaction(LayoutDeviceIntPoint aRefPoint,
+ EventMessage aEventMessage,
+ ScrollableLayerGuid? aTargetGuid);
+
+ sync ProcessUnhandledEvent(LayoutDeviceIntPoint aRefPoint)
+ returns (LayoutDeviceIntPoint aOutRefPoint,
+ ScrollableLayerGuid aOutTargetGuid,
+ uint64_t aOutFocusSequenceNumber,
+ LayersId aOutLayersId);
+
+child:
+ async CallInputBlockCallback(uint64_t aInputBlockId,
+ APZHandledResult aHandledResult);
+};
+
+} // namespace gfx
+} // namespace mozilla
diff --git a/gfx/layers/ipc/PCanvas.ipdl b/gfx/layers/ipc/PCanvas.ipdl
new file mode 100644
index 0000000000..6f068ac00c
--- /dev/null
+++ b/gfx/layers/ipc/PCanvas.ipdl
@@ -0,0 +1,52 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: sw=2 ts=8 et : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+include "mozilla/layers/LayersMessageUtils.h";
+include "mozilla/layers/CanvasTranslator.h";
+
+[MoveOnly] using mozilla::CrossProcessSemaphoreHandle from "mozilla/ipc/CrossProcessSemaphore.h";
+using mozilla::layers::TextureType from "mozilla/layers/LayersTypes.h";
+[MoveOnly] using mozilla::ipc::SharedMemoryBasic::Handle from "mozilla/ipc/SharedMemoryBasic.h";
+
+namespace mozilla {
+namespace layers {
+
+/**
+ * PCanvas is the IPDL for recorded Canvas drawing.
+ */
+[NeedsOtherPid, ParentImpl="CanvasTranslator"]
+async protocol PCanvas {
+parent:
+ /**
+ * Initialize a CanvasTranslator for a particular TextureType, which
+ * translates events from a CanvasEventRingBuffer. aReadHandle is the shared
+ * memory handle for the ring buffer. aReaderSem and aWriterSem are handles
+ * for the semaphores to handle waiting on either side.
+ */
+ async InitTranslator(TextureType aTextureType, Handle aReadHandle,
+ CrossProcessSemaphoreHandle aReaderSem,
+ CrossProcessSemaphoreHandle aWriterSem);
+
+ /**
+ * Used to tell the CanvasTranslator to start translating again after it has
+ * stopped due to a timeout waiting for events.
+ */
+ async ResumeTranslation();
+
+ child:
+ /**
+ * Notify that the canvas device used by the translator has changed.
+ */
+ async NotifyDeviceChanged();
+
+ /**
+ * Deactivate remote canvas, which will cause fall back to software.
+ */
+ async Deactivate();
+};
+
+} // layers
+} // mozilla
diff --git a/gfx/layers/ipc/PCompositorBridge.ipdl b/gfx/layers/ipc/PCompositorBridge.ipdl
new file mode 100644
index 0000000000..1340b7850f
--- /dev/null
+++ b/gfx/layers/ipc/PCompositorBridge.ipdl
@@ -0,0 +1,214 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: sw=2 ts=8 et :
+ */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+include LayersSurfaces;
+include LayersMessages;
+include PlatformWidgetTypes;
+include PCompositorBridgeTypes;
+include protocol PAPZ;
+include protocol PAPZCTreeManager;
+include protocol PBrowser;
+include protocol PCanvas;
+include protocol PCompositorManager;
+include protocol PCompositorWidget;
+include protocol PTexture;
+include protocol PWebRenderBridge;
+include "mozilla/GfxMessageUtils.h";
+include "mozilla/layers/LayersMessageUtils.h";
+include "mozilla/layers/WebRenderMessageUtils.h";
+include "mozilla/layers/CompositorBridgeParent.h";
+
+using struct mozilla::null_t from "mozilla/ipc/IPCCore.h";
+using struct mozilla::layers::LayersId from "mozilla/layers/LayersTypes.h";
+using struct mozilla::layers::TextureFactoryIdentifier from "mozilla/layers/CompositorTypes.h";
+using struct mozilla::layers::ScrollableLayerGuid from "mozilla/layers/ScrollableLayerGuid.h";
+using mozilla::layers::ScrollableLayerGuid::ViewID from "mozilla/layers/ScrollableLayerGuid.h";
+using mozilla::layers::WindowKind from "mozilla/layers/LayersTypes.h";
+using mozilla::layers::LayersBackend from "mozilla/layers/LayersTypes.h";
+using mozilla::CSSIntRegion from "Units.h";
+using mozilla::LayoutDeviceIntPoint from "Units.h";
+using mozilla::LayoutDeviceIntRegion from "Units.h";
+using mozilla::LayoutDeviceIntSize from "Units.h";
+using class mozilla::TimeStamp from "mozilla/TimeStamp.h";
+using mozilla::layers::TextureFlags from "mozilla/layers/CompositorTypes.h";
+using mozilla::layers::CompositorOptions from "mozilla/layers/CompositorOptions.h";
+using mozilla::wr::PipelineId from "mozilla/webrender/WebRenderTypes.h";
+using mozilla::wr::IdNamespace from "mozilla/webrender/WebRenderTypes.h";
+using mozilla::wr::RenderReasons from "mozilla/webrender/webrender_ffi.h";
+using base::ProcessId from "base/process.h";
+using mozilla::wr::MaybeExternalImageId from "mozilla/webrender/WebRenderTypes.h";
+using mozilla::layers::LayersObserverEpoch from "mozilla/layers/LayersTypes.h";
+using mozilla::layers::TransactionId from "mozilla/layers/LayersTypes.h";
+
+namespace mozilla {
+namespace layers {
+
+struct FrameStats {
+ TransactionId id;
+ TimeStamp compositeStart;
+ TimeStamp renderStart;
+ TimeStamp compositeEnd;
+ int32_t contentFrameTime;
+ double resourceUploadTime;
+ double gpuCacheUploadTime;
+ TimeStamp transactionStart;
+ TimeStamp refreshStart;
+ TimeStamp fwdTime;
+ TimeStamp sceneBuiltTime;
+ uint32_t skippedComposites;
+ nsCString url;
+};
+
+/**
+ * The PCompositorBridge protocol is a top-level protocol for the compositor.
+ * There is an instance of the protocol for each compositor, plus one for each
+ * content process. In other words:
+ * - There is a CompositorBridgeParent/CompositorBridgeChild pair created
+ * for each "top level browser window", which has its own compositor. The
+ * CompositorBridgeChild instance lives in the UI process, and the
+ * CompositorBridgeParent lives in the GPU process (if there is one) or the
+ * UI process otherwise.
+ * - There is also a ContentCompositorBridgeParent/CompositorBridgeChild
+ * pair created for each content process. The ContentCompositorBridgeParent
+ * lives in the GPU process (if there is one) or the UI process otherwise. The
+ * CompositorBridgeChild is a singleton in the content process. Note that
+ * a given content process may have multiple browser instances (represented
+ * by BrowserChild instances), that are attached to different windows, and therefore
+ * rendered by different compositors. This means that when a browser instance
+ * sends messages via its CompositorBridgeChild, the corresponding
+ * ContentCompositorBridgeParent has to use the layers id in the message
+ * to find the correct compositor or CompositorBridgeParent to pass the message
+ * on to.
+ *
+ * One of the main goals of this protocol is to manage the PLayerTransaction sub-
+ * protocol, which is per-browser. A lot of the functions in the protocol are
+ * basically multiplexing/demultiplexing stuff in PLayerTransaction.
+ */
+[ParentImpl="CompositorBridgeParentBase"]
+sync protocol PCompositorBridge
+{
+ manager PCompositorManager;
+
+ manages PAPZ;
+ manages PAPZCTreeManager;
+ // A Compositor manages a single Layer Manager (PLayerTransaction)
+ manages PTexture;
+ manages PCompositorWidget;
+ manages PWebRenderBridge;
+
+child:
+ // The compositor completed a layers transaction. id is the layers id
+ // of the child layer tree that was composited (or 0 when notifying
+ // the root layer tree).
+ // transactionId is the id of the transaction before this composite, or 0
+ // if there was no transaction since the last composite.
+ [Priority=control] async DidComposite(LayersId id,
+ TransactionId[] transactionId,
+ TimeStamp compositeStart,
+ TimeStamp compositeEnd);
+
+ async NotifyFrameStats(FrameStats[] aFrameStats);
+
+ async ParentAsyncMessages(AsyncParentMessageData[] aMessages);
+
+ async ObserveLayersUpdate(LayersId aLayersId, LayersObserverEpoch aEpoch, bool aActive);
+
+ async CompositorOptionsChanged(LayersId id, CompositorOptions newOptions);
+
+ async NotifyJankedAnimations(LayersId id, uint64_t[] aJankedAnimations);
+
+parent:
+ async __delete__();
+
+ // Must be called before Initialize().
+ async PCompositorWidget(CompositorWidgetInitData aInitData);
+
+ // When out-of-process, this must be called to finish initialization.
+ sync Initialize(LayersId rootLayerTreeId);
+
+ // Must be called after Initialize(), and only succeeds if AsyncPanZoomEnabled() is true.
+ async PAPZ(LayersId layersId);
+ async PAPZCTreeManager(LayersId layersId);
+
+ // The child is about to be destroyed, so perform any necessary cleanup.
+ sync WillClose();
+
+ // Pause/resume the compositor. These are intended to be used on mobile, when
+ // the compositor needs to pause/resume in lockstep with the application.
+ sync Pause();
+ sync Resume();
+ async ResumeAsync();
+
+ // See bug 1316632 comment #33 for why this has to be sync. Otherwise,
+ // there are ordering issues with SendPLayerTransactionConstructor.
+ sync NotifyChildCreated(LayersId id)
+ returns (CompositorOptions compositorOptions);
+
+ // This version of NotifyChildCreated also performs a layer tree mapping.
+ //
+ // See bug 1316632 comment #33 for why this has to be sync. Otherwise,
+ // there are ordering issues with SendPLayerTransactionConstructor.
+ sync MapAndNotifyChildCreated(LayersId id, ProcessId owner)
+ returns (CompositorOptions compositorOptions);
+
+ async AdoptChild(LayersId id);
+
+ // Same as NotifyChildCreated, but used when child processes need to
+ // reassociate layers. This must be synchronous to ensure that the
+ // association happens before PLayerTransactions are sent over the
+ // cross-process bridge.
+ sync NotifyChildRecreated(LayersId id)
+ returns (CompositorOptions compositorOptions);
+
+ async NotifyMemoryPressure();
+
+ // Make sure any pending composites are started immediately and
+ // block until they are completed.
+ sync FlushRendering(RenderReasons aReasons);
+
+ // Same as FlushRendering, but asynchronous, since not all platforms require
+ // synchronous repaints on resize.
+ async FlushRenderingAsync(RenderReasons aReasons);
+
+ // Make sure any pending composites have been received.
+ sync WaitOnTransactionProcessed();
+
+ // Force an additional frame presentation to be executed. This is used to
+ // work around a windows presentation bug (See Bug 1232042)
+ async ForcePresent(RenderReasons aReasons);
+
+ sync StartFrameTimeRecording(int32_t bufferSize)
+ returns (uint32_t startIndex);
+
+ sync StopFrameTimeRecording(uint32_t startIndex)
+ returns (float[] intervals);
+
+ async PTexture(SurfaceDescriptor aSharedData, ReadLockDescriptor aReadLock, LayersBackend aBackend, TextureFlags aTextureFlags, LayersId id, uint64_t aSerial, MaybeExternalImageId aExternalImageId);
+
+ async InitPCanvasParent(Endpoint<PCanvasParent> aEndpoint);
+ async ReleasePCanvasParent();
+
+ sync SyncWithCompositor();
+
+ // The pipelineId is the same as the layersId
+ async PWebRenderBridge(PipelineId pipelineId, LayoutDeviceIntSize aSize, WindowKind aKind);
+
+ sync CheckContentOnlyTDR(uint32_t sequenceNum)
+ returns (bool isContentOnlyTDR);
+
+ async BeginRecording(TimeStamp aRecordingStart)
+ returns (bool success);
+
+ async EndRecording()
+ returns (FrameRecording? recording);
+
+ // To set up sharing the composited output to Firefox Reality on Desktop
+ async RequestFxrOutput();
+};
+
+} // layers
+} // mozilla
diff --git a/gfx/layers/ipc/PCompositorBridgeTypes.ipdlh b/gfx/layers/ipc/PCompositorBridgeTypes.ipdlh
new file mode 100644
index 0000000000..f07a7a3fb0
--- /dev/null
+++ b/gfx/layers/ipc/PCompositorBridgeTypes.ipdlh
@@ -0,0 +1,23 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+using mozilla::TimeStamp from "mozilla/TimeStamp.h";
+[MoveOnly] using class mozilla::ipc::BigBuffer from "mozilla/ipc/BigBuffer.h";
+
+namespace mozilla {
+namespace layers {
+
+struct RecordedFrameData {
+ TimeStamp timeOffset;
+ uint32_t length;
+};
+
+struct FrameRecording {
+ TimeStamp startTime;
+ RecordedFrameData[] frames;
+ BigBuffer bytes;
+};
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/ipc/PCompositorManager.ipdl b/gfx/layers/ipc/PCompositorManager.ipdl
new file mode 100644
index 0000000000..3848ac2b16
--- /dev/null
+++ b/gfx/layers/ipc/PCompositorManager.ipdl
@@ -0,0 +1,97 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: sw=2 ts=8 et :
+ */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+include protocol PCanvasManager;
+include protocol PCompositorBridge;
+include LayersSurfaces;
+include "mozilla/GfxMessageUtils.h";
+include "mozilla/layers/WebRenderMessageUtils.h";
+
+using struct mozilla::void_t from "mozilla/ipc/IPCCore.h";
+using mozilla::TimeDuration from "mozilla/TimeStamp.h";
+using mozilla::CSSToLayoutDeviceScale from "Units.h";
+using mozilla::gfx::IntSize from "mozilla/gfx/2D.h";
+[MoveOnly] using mozilla::ipc::SharedMemoryBasic::Handle from "mozilla/ipc/SharedMemoryBasic.h";
+using mozilla::layers::CompositorOptions from "mozilla/layers/LayersMessageUtils.h";
+using mozilla::wr::ExternalImageId from "mozilla/webrender/WebRenderTypes.h";
+using mozilla::wr::MemoryReport from "mozilla/webrender/WebRenderTypes.h";
+using mozilla::wr::WebRenderError from "mozilla/webrender/WebRenderTypes.h";
+using mozilla::layers::SharedSurfacesMemoryReport from "mozilla/layers/SharedSurfacesMemoryReport.h";
+
+namespace mozilla {
+namespace layers {
+
+struct WidgetCompositorOptions {
+ CSSToLayoutDeviceScale scale;
+ TimeDuration vsyncRate;
+ CompositorOptions options;
+ bool useExternalSurfaceSize;
+ IntSize surfaceSize;
+ uint64_t innerWindowId;
+};
+
+struct ContentCompositorOptions {
+};
+
+struct SameProcessWidgetCompositorOptions {
+};
+
+union CompositorBridgeOptions {
+ ContentCompositorOptions;
+ WidgetCompositorOptions;
+ SameProcessWidgetCompositorOptions;
+};
+
+/**
+ * The PCompositorManager protocol is the top-level protocol between the
+ * compositor thread and the creators of compositors. It exists in the
+ * following conditions:
+ * - One PCompositorManager between the GPU process and each content process.
+ * If the GPU/UI processes are combined, there is one PCompositorManager
+ * between the combined GPU/UI process and each content process.
+ * - One PCompositorManager between the GPU process and the UI process. If
+ * they are combined, there is still one PCompositorManager, but both the
+ * child and parent live in the same process.
+ * The intention for this protocol is to facilitate communication with the
+ * compositor thread for compositor data that is only shared once, rather than
+ * per PCompositorBridge instance.
+ */
+[NeedsOtherPid]
+sync protocol PCompositorManager
+{
+ manages PCompositorBridge;
+
+parent:
+ /**
+ * There are three variants of a PCompositorBridge protocol, each of which can
+ * only be created by certain processes and configurations:
+ * - A "content" PCompositorBridge is requested by each content process,
+ * representing the drawable area for Web content.
+ * - A "widget" PCompositorBridge is requested by the UI process for each
+ * "top level browser window" for chrome and such.
+ * - A "same process widget" PCompositorBridge is requested by the combined
+ * GPU/UI process for each "top level browser window" as above.
+ * See gfx/layers/ipc/PCompositorBridge.ipdl for more details.
+ */
+ async PCompositorBridge(CompositorBridgeOptions options);
+
+ async AddSharedSurface(ExternalImageId aId, SurfaceDescriptorShared aDesc);
+ async RemoveSharedSurface(ExternalImageId aId);
+ async ReportSharedSurfacesMemory() returns (SharedSurfacesMemoryReport aReport);
+
+ async NotifyMemoryPressure();
+
+ async ReportMemory() returns (MemoryReport aReport);
+
+ async InitCanvasManager(Endpoint<PCanvasManagerParent> endpoint);
+
+child:
+ async NotifyWebRenderError(WebRenderError error);
+};
+
+} // layers
+} // mozilla
diff --git a/gfx/layers/ipc/PImageBridge.ipdl b/gfx/layers/ipc/PImageBridge.ipdl
new file mode 100644
index 0000000000..07aed85e57
--- /dev/null
+++ b/gfx/layers/ipc/PImageBridge.ipdl
@@ -0,0 +1,65 @@
+/* -*- Mode: C++; tab-width: 20; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+include LayersSurfaces;
+include LayersMessages;
+include protocol PTexture;
+include ProtocolTypes;
+include protocol PMediaSystemResourceManager;
+
+include "mozilla/GfxMessageUtils.h";
+include "mozilla/layers/LayersMessageUtils.h";
+include "mozilla/layers/WebRenderMessageUtils.h";
+
+using struct mozilla::layers::TextureInfo from "mozilla/layers/CompositorTypes.h";
+using mozilla::layers::TextureFlags from "mozilla/layers/CompositorTypes.h";
+using mozilla::layers::CompositableHandle from "mozilla/layers/LayersTypes.h";
+using mozilla::wr::MaybeExternalImageId from "mozilla/webrender/WebRenderTypes.h";
+
+namespace mozilla {
+namespace layers {
+
+/**
+ * The PImageBridge protocol is used to allow isolated threads or processes to push
+ * frames directly to the compositor thread/process without relying on the main thread
+ * which might be too busy dealing with content script.
+ */
+[NeedsOtherPid]
+sync protocol PImageBridge
+{
+ manages PTexture;
+ manages PMediaSystemResourceManager;
+
+child:
+ async ParentAsyncMessages(AsyncParentMessageData[] aMessages);
+
+ async DidComposite(ImageCompositeNotification[] aNotifications);
+
+ // Report the number of frames dropped for the given CompositableHost.
+ async ReportFramesDropped(CompositableHandle aHandle, uint32_t aFrames);
+
+parent:
+ async Update(CompositableOperation[] ops, OpDestroy[] toDestroy, uint64_t fwdTransactionId);
+
+ // First step of the destruction sequence. This puts ImageBridge
+ // in a state in which it can't send asynchronous messages
+ // so as to not race with the channel getting closed.
+ // In the child side, the Closing the channel does not happen right after WillClose,
+ // it is scheduled in the ImageBridgeChild's message queue in order to ensure
+ // that all of the messages from the parent side have been received and processed
+ // before sending closing the channel.
+ async WillClose();
+
+ async PTexture(SurfaceDescriptor aSharedData, ReadLockDescriptor aReadLock, LayersBackend aBackend, TextureFlags aTextureFlags, uint64_t aSerial, MaybeExternalImageId aExternalImageId);
+ async PMediaSystemResourceManager();
+
+ sync NewCompositable(CompositableHandle aHandle, TextureInfo aInfo);
+ async ReleaseCompositable(CompositableHandle aHandle);
+};
+
+
+} // namespace
+} // namespace
+
diff --git a/gfx/layers/ipc/PTexture.ipdl b/gfx/layers/ipc/PTexture.ipdl
new file mode 100644
index 0000000000..ad6cf8fc13
--- /dev/null
+++ b/gfx/layers/ipc/PTexture.ipdl
@@ -0,0 +1,40 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: sw=2 ts=8 et :
+ */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+include LayersSurfaces;
+include protocol PCompositorBridge;
+include protocol PImageBridge;
+include protocol PVideoBridge;
+include "mozilla/GfxMessageUtils.h";
+include "mozilla/layers/LayersMessageUtils.h";
+
+using mozilla::layers::TextureFlags from "mozilla/layers/CompositorTypes.h";
+
+namespace mozilla {
+namespace layers {
+
+/**
+ * PTexture is the IPDL glue between a TextureClient and a TextureHost.
+ */
+[ManualDealloc, ChildImpl=virtual, ParentImpl=virtual]
+sync protocol PTexture {
+ manager PImageBridge or PCompositorBridge or PVideoBridge;
+
+child:
+ async __delete__();
+
+parent:
+ /**
+ * Asynchronously tell the compositor side to remove the texture.
+ */
+ async Destroy();
+
+ async RecycleTexture(TextureFlags aTextureFlags);
+};
+
+} // layers
+} // mozilla
diff --git a/gfx/layers/ipc/PUiCompositorController.ipdl b/gfx/layers/ipc/PUiCompositorController.ipdl
new file mode 100644
index 0000000000..46b733caa2
--- /dev/null
+++ b/gfx/layers/ipc/PUiCompositorController.ipdl
@@ -0,0 +1,48 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: sw=2 ts=8 et :
+ */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+using mozilla::CSSRect from "Units.h";
+using mozilla::CSSToScreenScale from "Units.h";
+using mozilla::ScreenIntSize from "Units.h";
+using mozilla::ScreenPoint from "Units.h";
+
+include "mozilla/GfxMessageUtils.h";
+
+namespace mozilla {
+namespace layers {
+
+/**
+ * The PUiCompositorController protocol is used to pause and resume the
+ * compositor from the UI thread. Primarily used on Android to coordinate registering and
+ * releasing the surface with the compositor.
+ */
+[NeedsOtherPid]
+sync protocol PUiCompositorController
+{
+
+parent:
+ // Pause/resume the compositor. These are intended to be used on mobile, when
+ // the compositor needs to pause/resume in lockstep with the application.
+ sync Pause();
+ sync Resume() returns (bool aOutResumed);
+ sync ResumeAndResize(int32_t aX, int32_t aY, int32_t aWidth, int32_t aHeight)
+ returns (bool aOutResumed);
+
+ async InvalidateAndRender();
+ async MaxToolbarHeight(int32_t aHeight);
+ async FixedBottomOffset(int32_t aOffset);
+ async DefaultClearColor(uint32_t aColor);
+ async RequestScreenPixels();
+ async EnableLayerUpdateNotifications(bool aEnable);
+child:
+ async ToolbarAnimatorMessageFromCompositor(int32_t aMessage);
+ async RootFrameMetrics(ScreenPoint aScrollOffset, CSSToScreenScale aZoom);
+ async ScreenPixels(Shmem aMem, ScreenIntSize aSize, bool aNeedsYFlip);
+};
+
+} // layers
+} // mozilla
diff --git a/gfx/layers/ipc/PVideoBridge.ipdl b/gfx/layers/ipc/PVideoBridge.ipdl
new file mode 100644
index 0000000000..abb160d239
--- /dev/null
+++ b/gfx/layers/ipc/PVideoBridge.ipdl
@@ -0,0 +1,39 @@
+/* -*- Mode: C++; tab-width: 20; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+include LayersSurfaces;
+include LayersMessages;
+include protocol PTexture;
+
+include "mozilla/GfxMessageUtils.h";
+include "mozilla/layers/LayersMessageUtils.h";
+
+using mozilla::dom::ContentParentId from "mozilla/dom/ipc/IdType.h";
+using mozilla::layers::TextureFlags from "mozilla/layers/CompositorTypes.h";
+
+namespace mozilla {
+namespace layers {
+
+/**
+ * The PVideoBridge protocol is used to share textures from the video decoders
+ * to the compositor.
+ */
+[NeedsOtherPid]
+sync protocol PVideoBridge
+{
+ manages PTexture;
+
+parent:
+ /**
+ * Since PVideoBridge creates textures on behalf of another process, we also
+ * supply ContentParentId of the owning content process.
+ */
+ async PTexture(SurfaceDescriptor aSharedData, ReadLockDescriptor aReadLock, LayersBackend aBackend,
+ TextureFlags aTextureFlags, ContentParentId aContentId, uint64_t aSerial);
+};
+
+} // namespace
+} // namespace
+
diff --git a/gfx/layers/ipc/PWebRenderBridge.ipdl b/gfx/layers/ipc/PWebRenderBridge.ipdl
new file mode 100644
index 0000000000..e3e29112e1
--- /dev/null
+++ b/gfx/layers/ipc/PWebRenderBridge.ipdl
@@ -0,0 +1,139 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: sw=2 ts=8 et :
+ */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+include LayersSurfaces;
+include LayersMessages;
+include "mozilla/GfxMessageUtils.h";
+include "mozilla/layers/WebRenderMessageUtils.h";
+
+include WebRenderMessages;
+include protocol PCompositorBridge;
+include protocol PTexture;
+
+using mozilla::layers::APZTestData from "mozilla/layers/APZTestData.h";
+using mozilla::layers::FrameUniformityData from "mozilla/layers/FrameUniformityData.h";
+using mozilla::layers::ScrollableLayerGuid from "mozilla/layers/ScrollableLayerGuid.h";
+using struct mozilla::layers::TextureFactoryIdentifier from "mozilla/layers/CompositorTypes.h";
+using struct mozilla::layers::TextureInfo from "mozilla/layers/CompositorTypes.h";
+using mozilla::layers::CompositionPayload from "mozilla/layers/LayersTypes.h";
+using mozilla::layers::CompositableHandle from "mozilla/layers/LayersTypes.h";
+using mozilla::wr::BuiltDisplayListDescriptor from "mozilla/webrender/webrender_ffi.h";
+using mozilla::wr::RenderReasons from "mozilla/webrender/webrender_ffi.h";
+using mozilla::wr::IdNamespace from "mozilla/webrender/WebRenderTypes.h";
+using mozilla::wr::MaybeIdNamespace from "mozilla/webrender/WebRenderTypes.h";
+using mozilla::wr::ExternalImageKeyPair from "mozilla/webrender/WebRenderTypes.h";
+[MoveOnly] using mozilla::layers::DisplayListData from "mozilla/layers/RenderRootTypes.h";
+[MoveOnly] using mozilla::layers::MaybeTransactionData from "mozilla/layers/RenderRootTypes.h";
+using mozilla::layers::FocusTarget from "mozilla/layers/FocusTarget.h";
+using mozilla::layers::LayersObserverEpoch from "mozilla/layers/LayersTypes.h";
+using mozilla::layers::TransactionId from "mozilla/layers/LayersTypes.h";
+using mozilla::VsyncId from "mozilla/VsyncDispatcher.h";
+
+namespace mozilla {
+namespace layers {
+
+[ManualDealloc, ParentImpl=virtual]
+sync protocol PWebRenderBridge
+{
+ manager PCompositorBridge;
+
+parent:
+ sync EnsureConnected()
+ returns (TextureFactoryIdentifier textureFactoryIdentifier, MaybeIdNamespace maybeIdNamespace, nsCString error);
+
+ async NewCompositable(CompositableHandle handle, TextureInfo info);
+ async ReleaseCompositable(CompositableHandle compositable);
+
+ async DeleteCompositorAnimations(uint64_t[] aIds);
+ async SetDisplayList(DisplayListData displayList,
+ OpDestroy[] toDestroy, uint64_t fwdTransactionId, TransactionId transactionId,
+ bool containsSVGGroup,
+ VsyncId vsyncId, TimeStamp vsyncStartTime,
+ TimeStamp refreshStartTime, TimeStamp txnStartTime, nsCString txnURL, TimeStamp fwdTime,
+ CompositionPayload[] payloads);
+ async EmptyTransaction(FocusTarget focusTarget,
+ MaybeTransactionData transationData,
+ OpDestroy[] toDestroy, uint64_t fwdTransactionId, TransactionId transactionId,
+ VsyncId vsyncId, TimeStamp vsyncStartTime,
+ TimeStamp refreshStartTime, TimeStamp txnStartTime,
+ nsCString txnURL, TimeStamp fwdTime,
+ CompositionPayload[] payloads);
+ async SetFocusTarget(FocusTarget focusTarget);
+ async UpdateResources(IdNamespace aIdNamespace, OpUpdateResource[] aResourceUpdates,
+ RefCountedShmem[] aSmallShmems, Shmem[] aLargeShmems);
+ async ParentCommands(IdNamespace aIdNamespace, WebRenderParentCommand[] commands);
+ sync GetSnapshot(PTexture texture) returns (bool aNeedsYFlip);
+ async SetLayersObserverEpoch(LayersObserverEpoch childEpoch);
+ async ClearCachedResources();
+ async ClearAnimationResources();
+ async SetDefaultClearColor(uint32_t aColor);
+ // Invalidate rendered frame
+ async InvalidateRenderedFrame();
+ // Schedule a composite if one isn't already scheduled.
+ async ScheduleComposite(RenderReasons aReasons);
+ // Save the frame capture to disk
+ async Capture();
+
+ // Start capturing each frame to disk. See
+ // nsIDOMWindowUtils::wrStartCaptureSequence for documentation.
+ async StartCaptureSequence(nsCString aPath, uint32_t aFlags);
+
+ // Stop the captures started by StartCaptureSequence. See
+ // nsIDOMWindowUtils::wrStopCaptureSequence for documentation.
+ async StopCaptureSequence();
+
+ // Replacement for PCompositorBridge::SyncWithCompositor, but for WR. We need
+ // it on PWebRenderBridge because it associated with a particular top-level
+ // window, and PCompositorBridge doesn't allow doing that in a secure manner.
+ sync SyncWithCompositor();
+
+ // Tell the compositor to notify APZ that a target scroll frame has been
+ // confirmed for an input event.
+ async SetConfirmedTargetAPZC(uint64_t aInputBlockId, ScrollableLayerGuid[] aTargets);
+
+ // Testing APIs
+
+ // Enter test mode, set the sample time to sampleTime, and resample
+ // animations. sampleTime must not be null.
+ sync SetTestSampleTime(TimeStamp sampleTime);
+ // Leave test mode and resume normal compositing
+ sync LeaveTestMode();
+
+ // Returns the |OMTAValue| for the compositor animation with the given id.
+ sync GetAnimationValue(uint64_t aCompositorAnimationsId) returns (OMTAValue value);
+
+ // The next time the display list tree is composited, add this async scroll offset
+ // in CSS pixels for the given ViewID.
+ // Useful for testing rendering of async scrolling.
+ sync SetAsyncScrollOffset(ViewID scrollId, float x, float y);
+
+ // The next time the display list is composited, include this async zoom in
+ // for the given ViewID.
+ // Useful for testing rendering of async zooming.
+ sync SetAsyncZoom(ViewID scrollId, float zoom);
+
+ // Flush any pending APZ repaints to the main thread.
+ async FlushApzRepaints();
+
+ // Get a copy of the compositor-side APZ test data instance for this
+ // layers id.
+ sync GetAPZTestData() returns (APZTestData data);
+
+ // Child requests frame uniformity measurements
+ sync GetFrameUniformity() returns (FrameUniformityData data);
+
+
+ async Shutdown();
+ sync ShutdownSync();
+child:
+ async WrUpdated(IdNamespace aNewIdNamespace, TextureFactoryIdentifier textureFactoryIdentifier);
+ async WrReleasedImages(ExternalImageKeyPair[] pairs);
+ async __delete__();
+};
+
+} // layers
+} // mozilla
diff --git a/gfx/layers/ipc/RefCountedShmem.cpp b/gfx/layers/ipc/RefCountedShmem.cpp
new file mode 100644
index 0000000000..8eec82dc9a
--- /dev/null
+++ b/gfx/layers/ipc/RefCountedShmem.cpp
@@ -0,0 +1,93 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "RefCountedShmem.h"
+
+#include "mozilla/Atomics.h"
+#include "mozilla/ipc/ProtocolUtils.h"
+#include "mozilla/layers/WebRenderMessages.h"
+
+#define SHM_REFCOUNT_HEADER_SIZE 16
+
+namespace mozilla {
+namespace layers {
+
+uint8_t* RefCountedShm::GetBytes(const RefCountedShmem& aShm) {
+ uint8_t* data = aShm.buffer().get<uint8_t>();
+ if (!data) {
+ return nullptr;
+ }
+ return data + SHM_REFCOUNT_HEADER_SIZE;
+}
+
+size_t RefCountedShm::GetSize(const RefCountedShmem& aShm) {
+ if (!IsValid(aShm)) {
+ return 0;
+ }
+ size_t totalSize = aShm.buffer().Size<uint8_t>();
+ if (totalSize < SHM_REFCOUNT_HEADER_SIZE) {
+ return 0;
+ }
+
+ return totalSize - SHM_REFCOUNT_HEADER_SIZE;
+}
+
+bool RefCountedShm::IsValid(const RefCountedShmem& aShm) {
+ return aShm.buffer().IsWritable() &&
+ aShm.buffer().Size<uint8_t>() > SHM_REFCOUNT_HEADER_SIZE;
+}
+
+int32_t RefCountedShm::GetReferenceCount(const RefCountedShmem& aShm) {
+ if (!IsValid(aShm)) {
+ return 0;
+ }
+
+ return *aShm.buffer().get<Atomic<int32_t>>();
+}
+
+int32_t RefCountedShm::AddRef(const RefCountedShmem& aShm) {
+ if (!IsValid(aShm)) {
+ return 0;
+ }
+
+ auto* counter = aShm.buffer().get<Atomic<int32_t>>();
+ if (counter) {
+ return (*counter)++;
+ }
+ return 0;
+}
+
+int32_t RefCountedShm::Release(const RefCountedShmem& aShm) {
+ if (!IsValid(aShm)) {
+ return 0;
+ }
+
+ auto* counter = aShm.buffer().get<Atomic<int32_t>>();
+ if (counter) {
+ return --(*counter);
+ }
+
+ return 0;
+}
+
+bool RefCountedShm::Alloc(mozilla::ipc::IProtocol* aAllocator, size_t aSize,
+ RefCountedShmem& aShm) {
+ MOZ_ASSERT(!IsValid(aShm));
+ auto size = aSize + SHM_REFCOUNT_HEADER_SIZE;
+ if (!aAllocator->AllocUnsafeShmem(size, &aShm.buffer())) {
+ return false;
+ }
+ return true;
+}
+
+void RefCountedShm::Dealloc(mozilla::ipc::IProtocol* aAllocator,
+ RefCountedShmem& aShm) {
+ aAllocator->DeallocShmem(aShm.buffer());
+ aShm.buffer() = ipc::Shmem();
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/ipc/RefCountedShmem.h b/gfx/layers/ipc/RefCountedShmem.h
new file mode 100644
index 0000000000..390d6ea25f
--- /dev/null
+++ b/gfx/layers/ipc/RefCountedShmem.h
@@ -0,0 +1,48 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_LAYERS_REFCOUNTED_SHMEM_H
+#define MOZILLA_LAYERS_REFCOUNTED_SHMEM_H
+
+#include "mozilla/ipc/Shmem.h"
+#include "chrome/common/ipc_message_utils.h"
+
+namespace mozilla {
+namespace ipc {
+class IProtocol;
+}
+
+namespace layers {
+
+// This class is IPDL-defined
+class RefCountedShmem;
+
+// This just implement the methods externally.
+class RefCountedShm {
+ public:
+ static uint8_t* GetBytes(const RefCountedShmem& aShm);
+
+ static size_t GetSize(const RefCountedShmem& aShm);
+
+ static bool IsValid(const RefCountedShmem& aShm);
+
+ static bool Alloc(mozilla::ipc::IProtocol* aAllocator, size_t aSize,
+ RefCountedShmem& aShm);
+
+ static void Dealloc(mozilla::ipc::IProtocol* aAllocator,
+ RefCountedShmem& aShm);
+
+ static int32_t GetReferenceCount(const RefCountedShmem& aShm);
+
+ static int32_t AddRef(const RefCountedShmem& aShm);
+
+ static int32_t Release(const RefCountedShmem& aShm);
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif
diff --git a/gfx/layers/ipc/RemoteContentController.cpp b/gfx/layers/ipc/RemoteContentController.cpp
new file mode 100644
index 0000000000..d6137e4826
--- /dev/null
+++ b/gfx/layers/ipc/RemoteContentController.cpp
@@ -0,0 +1,519 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/layers/RemoteContentController.h"
+
+#include "CompositorThread.h"
+#include "MainThreadUtils.h"
+#include "mozilla/dom/BrowserParent.h"
+#include "mozilla/layers/APZCCallbackHelper.h"
+#include "mozilla/layers/APZCTreeManagerParent.h" // for APZCTreeManagerParent
+#include "mozilla/layers/APZThreadUtils.h"
+#include "mozilla/layers/CompositorBridgeParent.h"
+#include "mozilla/layers/MatrixMessage.h"
+#include "mozilla/gfx/GPUProcessManager.h"
+#include "mozilla/Unused.h"
+#include "Units.h"
+#ifdef MOZ_WIDGET_ANDROID
+# include "mozilla/jni/Utils.h"
+#endif
+
+static mozilla::LazyLogModule sApzRemoteLog("apz.cc.remote");
+
+namespace mozilla {
+namespace layers {
+
+using namespace mozilla::gfx;
+
+RemoteContentController::RemoteContentController()
+ : mCompositorThread(NS_GetCurrentThread()), mCanSend(true) {
+ MOZ_ASSERT(CompositorThread()->IsOnCurrentThread());
+}
+
+RemoteContentController::~RemoteContentController() = default;
+
+void RemoteContentController::NotifyLayerTransforms(
+ nsTArray<MatrixMessage>&& aTransforms) {
+ if (!mCompositorThread->IsOnCurrentThread()) {
+ // We have to send messages from the compositor thread
+ mCompositorThread->Dispatch(
+ NewRunnableMethod<StoreCopyPassByRRef<nsTArray<MatrixMessage>>>(
+ "layers::RemoteContentController::NotifyLayerTransforms", this,
+ &RemoteContentController::NotifyLayerTransforms,
+ std::move(aTransforms)));
+ return;
+ }
+
+ if (mCanSend) {
+ Unused << SendLayerTransforms(aTransforms);
+ }
+}
+
+void RemoteContentController::RequestContentRepaint(
+ const RepaintRequest& aRequest) {
+ MOZ_ASSERT(IsRepaintThread());
+
+ if (mCanSend) {
+ Unused << SendRequestContentRepaint(aRequest);
+ }
+}
+
+void RemoteContentController::HandleTapOnMainThread(TapType aTapType,
+ LayoutDevicePoint aPoint,
+ Modifiers aModifiers,
+ ScrollableLayerGuid aGuid,
+ uint64_t aInputBlockId) {
+ MOZ_LOG(sApzRemoteLog, LogLevel::Debug,
+ ("HandleTapOnMainThread(%d)", (int)aTapType));
+ MOZ_ASSERT(NS_IsMainThread());
+
+ dom::BrowserParent* tab =
+ dom::BrowserParent::GetBrowserParentFromLayersId(aGuid.mLayersId);
+ if (tab) {
+ tab->SendHandleTap(aTapType, aPoint, aModifiers, aGuid, aInputBlockId);
+ }
+}
+
+void RemoteContentController::HandleTapOnCompositorThread(
+ TapType aTapType, LayoutDevicePoint aPoint, Modifiers aModifiers,
+ ScrollableLayerGuid aGuid, uint64_t aInputBlockId) {
+ MOZ_ASSERT(XRE_IsGPUProcess());
+ MOZ_ASSERT(mCompositorThread->IsOnCurrentThread());
+
+ // The raw pointer to APZCTreeManagerParent is ok here because we are on the
+ // compositor thread.
+ APZCTreeManagerParent* apzctmp =
+ CompositorBridgeParent::GetApzcTreeManagerParentForRoot(aGuid.mLayersId);
+ if (apzctmp) {
+ Unused << apzctmp->SendHandleTap(aTapType, aPoint, aModifiers, aGuid,
+ aInputBlockId);
+ }
+}
+
+void RemoteContentController::HandleTap(TapType aTapType,
+ const LayoutDevicePoint& aPoint,
+ Modifiers aModifiers,
+ const ScrollableLayerGuid& aGuid,
+ uint64_t aInputBlockId) {
+ MOZ_LOG(sApzRemoteLog, LogLevel::Debug, ("HandleTap(%d)", (int)aTapType));
+ APZThreadUtils::AssertOnControllerThread();
+
+ if (XRE_GetProcessType() == GeckoProcessType_GPU) {
+ if (mCompositorThread->IsOnCurrentThread()) {
+ HandleTapOnCompositorThread(aTapType, aPoint, aModifiers, aGuid,
+ aInputBlockId);
+ } else {
+ // We have to send messages from the compositor thread
+ mCompositorThread->Dispatch(
+ NewRunnableMethod<TapType, LayoutDevicePoint, Modifiers,
+ ScrollableLayerGuid, uint64_t>(
+ "layers::RemoteContentController::HandleTapOnCompositorThread",
+ this, &RemoteContentController::HandleTapOnCompositorThread,
+ aTapType, aPoint, aModifiers, aGuid, aInputBlockId));
+ }
+ return;
+ }
+
+ MOZ_ASSERT(XRE_IsParentProcess());
+
+ if (NS_IsMainThread()) {
+ HandleTapOnMainThread(aTapType, aPoint, aModifiers, aGuid, aInputBlockId);
+ } else {
+ // We must be on Android, running on the Java UI thread
+#ifndef MOZ_WIDGET_ANDROID
+ MOZ_ASSERT(false);
+#else
+ // We don't want to get the BrowserParent or call
+ // BrowserParent::SendHandleTap() from a non-main thread, so we need to
+ // redispatch to the main thread. However, we should use the same mechanism
+ // that the Android widget uses when dispatching input events to Gecko,
+ // which is nsAppShell::PostEvent. Note in particular that using
+ // NS_DispatchToMainThread would post to a different message loop, and
+ // introduces the possibility of this tap event getting processed out of
+ // order with respect to the touch events that synthesized it.
+ mozilla::jni::DispatchToGeckoPriorityQueue(
+ NewRunnableMethod<TapType, LayoutDevicePoint, Modifiers,
+ ScrollableLayerGuid, uint64_t>(
+ "layers::RemoteContentController::HandleTapOnMainThread", this,
+ &RemoteContentController::HandleTapOnMainThread, aTapType, aPoint,
+ aModifiers, aGuid, aInputBlockId));
+#endif
+ }
+}
+
+void RemoteContentController::NotifyPinchGestureOnCompositorThread(
+ PinchGestureInput::PinchGestureType aType, const ScrollableLayerGuid& aGuid,
+ const LayoutDevicePoint& aFocusPoint, LayoutDeviceCoord aSpanChange,
+ Modifiers aModifiers) {
+ MOZ_ASSERT(mCompositorThread->IsOnCurrentThread());
+
+ // The raw pointer to APZCTreeManagerParent is ok here because we are on the
+ // compositor thread.
+ APZCTreeManagerParent* apzctmp =
+ CompositorBridgeParent::GetApzcTreeManagerParentForRoot(aGuid.mLayersId);
+ if (apzctmp) {
+ Unused << apzctmp->SendNotifyPinchGesture(aType, aGuid, aFocusPoint,
+ aSpanChange, aModifiers);
+ }
+}
+
+void RemoteContentController::NotifyPinchGesture(
+ PinchGestureInput::PinchGestureType aType, const ScrollableLayerGuid& aGuid,
+ const LayoutDevicePoint& aFocusPoint, LayoutDeviceCoord aSpanChange,
+ Modifiers aModifiers) {
+ APZThreadUtils::AssertOnControllerThread();
+
+ // For now we only ever want to handle this NotifyPinchGesture message in
+ // the parent process, even if the APZ is sending it to a content process.
+
+ // If we're in the GPU process, try to find a handle to the parent process
+ // and send it there.
+ if (XRE_IsGPUProcess()) {
+ if (mCompositorThread->IsOnCurrentThread()) {
+ NotifyPinchGestureOnCompositorThread(aType, aGuid, aFocusPoint,
+ aSpanChange, aModifiers);
+ } else {
+ mCompositorThread->Dispatch(
+ NewRunnableMethod<PinchGestureInput::PinchGestureType,
+ ScrollableLayerGuid, LayoutDevicePoint,
+ LayoutDeviceCoord, Modifiers>(
+ "layers::RemoteContentController::"
+ "NotifyPinchGestureOnCompositorThread",
+ this,
+ &RemoteContentController::NotifyPinchGestureOnCompositorThread,
+ aType, aGuid, aFocusPoint, aSpanChange, aModifiers));
+ }
+ return;
+ }
+
+ // If we're in the parent process, handle it directly. We don't have a handle
+ // to the widget though, so we fish out the ChromeProcessController and
+ // delegate to that instead.
+ if (XRE_IsParentProcess()) {
+ MOZ_ASSERT(NS_IsMainThread());
+ RefPtr<GeckoContentController> rootController =
+ CompositorBridgeParent::GetGeckoContentControllerForRoot(
+ aGuid.mLayersId);
+ if (rootController) {
+ rootController->NotifyPinchGesture(aType, aGuid, aFocusPoint, aSpanChange,
+ aModifiers);
+ }
+ }
+}
+
+bool RemoteContentController::IsRepaintThread() {
+ return mCompositorThread->IsOnCurrentThread();
+}
+
+void RemoteContentController::DispatchToRepaintThread(
+ already_AddRefed<Runnable> aTask) {
+ mCompositorThread->Dispatch(std::move(aTask));
+}
+
+void RemoteContentController::NotifyAPZStateChange(
+ const ScrollableLayerGuid& aGuid, APZStateChange aChange, int aArg,
+ Maybe<uint64_t> aInputBlockId) {
+ if (!mCompositorThread->IsOnCurrentThread()) {
+ // We have to send messages from the compositor thread
+ mCompositorThread->Dispatch(
+ NewRunnableMethod<ScrollableLayerGuid, APZStateChange, int,
+ Maybe<uint64_t>>(
+ "layers::RemoteContentController::NotifyAPZStateChange", this,
+ &RemoteContentController::NotifyAPZStateChange, aGuid, aChange,
+ aArg, aInputBlockId));
+ return;
+ }
+
+ if (mCanSend) {
+ Unused << SendNotifyAPZStateChange(aGuid, aChange, aArg, aInputBlockId);
+ }
+}
+
+void RemoteContentController::UpdateOverscrollVelocity(
+ const ScrollableLayerGuid& aGuid, float aX, float aY, bool aIsRootContent) {
+ if (XRE_IsParentProcess()) {
+#ifdef MOZ_WIDGET_ANDROID
+ // We always want these to go to the parent process on Android
+ if (!NS_IsMainThread()) {
+ mozilla::jni::DispatchToGeckoPriorityQueue(
+ NewRunnableMethod<ScrollableLayerGuid, float, float, bool>(
+ "layers::RemoteContentController::UpdateOverscrollVelocity", this,
+ &RemoteContentController::UpdateOverscrollVelocity, aGuid, aX, aY,
+ aIsRootContent));
+ return;
+ }
+#endif
+
+ MOZ_RELEASE_ASSERT(NS_IsMainThread());
+ RefPtr<GeckoContentController> rootController =
+ CompositorBridgeParent::GetGeckoContentControllerForRoot(
+ aGuid.mLayersId);
+ if (rootController) {
+ rootController->UpdateOverscrollVelocity(aGuid, aX, aY, aIsRootContent);
+ }
+ } else if (XRE_IsGPUProcess()) {
+ if (!mCompositorThread->IsOnCurrentThread()) {
+ mCompositorThread->Dispatch(
+ NewRunnableMethod<ScrollableLayerGuid, float, float, bool>(
+ "layers::RemoteContentController::UpdateOverscrollVelocity", this,
+ &RemoteContentController::UpdateOverscrollVelocity, aGuid, aX, aY,
+ aIsRootContent));
+ return;
+ }
+
+ MOZ_RELEASE_ASSERT(mCompositorThread->IsOnCurrentThread());
+ GeckoContentController* rootController =
+ CompositorBridgeParent::GetGeckoContentControllerForRoot(
+ aGuid.mLayersId);
+ if (rootController) {
+ MOZ_RELEASE_ASSERT(rootController->IsRemote());
+ Unused << static_cast<RemoteContentController*>(rootController)
+ ->SendUpdateOverscrollVelocity(aGuid, aX, aY,
+ aIsRootContent);
+ }
+ }
+}
+
+void RemoteContentController::UpdateOverscrollOffset(
+ const ScrollableLayerGuid& aGuid, float aX, float aY, bool aIsRootContent) {
+ if (XRE_IsParentProcess()) {
+#ifdef MOZ_WIDGET_ANDROID
+ // We always want these to go to the parent process on Android
+ if (!NS_IsMainThread()) {
+ mozilla::jni::DispatchToGeckoPriorityQueue(
+ NewRunnableMethod<ScrollableLayerGuid, float, float, bool>(
+ "layers::RemoteContentController::UpdateOverscrollOffset", this,
+ &RemoteContentController::UpdateOverscrollOffset, aGuid, aX, aY,
+ aIsRootContent));
+ return;
+ }
+#endif
+
+ MOZ_RELEASE_ASSERT(NS_IsMainThread());
+ RefPtr<GeckoContentController> rootController =
+ CompositorBridgeParent::GetGeckoContentControllerForRoot(
+ aGuid.mLayersId);
+ if (rootController) {
+ rootController->UpdateOverscrollOffset(aGuid, aX, aY, aIsRootContent);
+ }
+ } else if (XRE_IsGPUProcess()) {
+ if (!mCompositorThread->IsOnCurrentThread()) {
+ mCompositorThread->Dispatch(
+ NewRunnableMethod<ScrollableLayerGuid, float, float, bool>(
+ "layers::RemoteContentController::UpdateOverscrollOffset", this,
+ &RemoteContentController::UpdateOverscrollOffset, aGuid, aX, aY,
+ aIsRootContent));
+ return;
+ }
+
+ MOZ_RELEASE_ASSERT(mCompositorThread->IsOnCurrentThread());
+ GeckoContentController* rootController =
+ CompositorBridgeParent::GetGeckoContentControllerForRoot(
+ aGuid.mLayersId);
+ if (rootController) {
+ MOZ_RELEASE_ASSERT(rootController->IsRemote());
+ Unused << static_cast<RemoteContentController*>(rootController)
+ ->SendUpdateOverscrollOffset(aGuid, aX, aY, aIsRootContent);
+ }
+ }
+}
+
+void RemoteContentController::NotifyMozMouseScrollEvent(
+ const ScrollableLayerGuid::ViewID& aScrollId, const nsString& aEvent) {
+ if (!mCompositorThread->IsOnCurrentThread()) {
+ // We have to send messages from the compositor thread
+ mCompositorThread->Dispatch(
+ NewRunnableMethod<ScrollableLayerGuid::ViewID, nsString>(
+ "layers::RemoteContentController::NotifyMozMouseScrollEvent", this,
+ &RemoteContentController::NotifyMozMouseScrollEvent, aScrollId,
+ aEvent));
+ return;
+ }
+
+ if (mCanSend) {
+ Unused << SendNotifyMozMouseScrollEvent(aScrollId, aEvent);
+ }
+}
+
+void RemoteContentController::NotifyFlushComplete() {
+ MOZ_ASSERT(IsRepaintThread());
+
+ if (mCanSend) {
+ Unused << SendNotifyFlushComplete();
+ }
+}
+
+void RemoteContentController::NotifyAsyncScrollbarDragInitiated(
+ uint64_t aDragBlockId, const ScrollableLayerGuid::ViewID& aScrollId,
+ ScrollDirection aDirection) {
+ if (!mCompositorThread->IsOnCurrentThread()) {
+ // We have to send messages from the compositor thread
+ mCompositorThread->Dispatch(NewRunnableMethod<uint64_t,
+ ScrollableLayerGuid::ViewID,
+ ScrollDirection>(
+ "layers::RemoteContentController::NotifyAsyncScrollbarDragInitiated",
+ this, &RemoteContentController::NotifyAsyncScrollbarDragInitiated,
+ aDragBlockId, aScrollId, aDirection));
+ return;
+ }
+
+ if (mCanSend) {
+ Unused << SendNotifyAsyncScrollbarDragInitiated(aDragBlockId, aScrollId,
+ aDirection);
+ }
+}
+
+void RemoteContentController::NotifyAsyncScrollbarDragRejected(
+ const ScrollableLayerGuid::ViewID& aScrollId) {
+ if (!mCompositorThread->IsOnCurrentThread()) {
+ // We have to send messages from the compositor thread
+ mCompositorThread->Dispatch(NewRunnableMethod<ScrollableLayerGuid::ViewID>(
+ "layers::RemoteContentController::NotifyAsyncScrollbarDragRejected",
+ this, &RemoteContentController::NotifyAsyncScrollbarDragRejected,
+ aScrollId));
+ return;
+ }
+
+ if (mCanSend) {
+ Unused << SendNotifyAsyncScrollbarDragRejected(aScrollId);
+ }
+}
+
+void RemoteContentController::NotifyAsyncAutoscrollRejected(
+ const ScrollableLayerGuid::ViewID& aScrollId) {
+ if (!mCompositorThread->IsOnCurrentThread()) {
+ // We have to send messages from the compositor thread
+ mCompositorThread->Dispatch(NewRunnableMethod<ScrollableLayerGuid::ViewID>(
+ "layers::RemoteContentController::NotifyAsyncAutoscrollRejected", this,
+ &RemoteContentController::NotifyAsyncAutoscrollRejected, aScrollId));
+ return;
+ }
+
+ if (mCanSend) {
+ Unused << SendNotifyAsyncAutoscrollRejected(aScrollId);
+ }
+}
+
+void RemoteContentController::CancelAutoscroll(
+ const ScrollableLayerGuid& aGuid) {
+ if (XRE_GetProcessType() == GeckoProcessType_GPU) {
+ CancelAutoscrollCrossProcess(aGuid);
+ } else {
+ CancelAutoscrollInProcess(aGuid);
+ }
+}
+
+void RemoteContentController::CancelAutoscrollInProcess(
+ const ScrollableLayerGuid& aGuid) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+
+ if (!NS_IsMainThread()) {
+ NS_DispatchToMainThread(NewRunnableMethod<ScrollableLayerGuid>(
+ "layers::RemoteContentController::CancelAutoscrollInProcess", this,
+ &RemoteContentController::CancelAutoscrollInProcess, aGuid));
+ return;
+ }
+
+ APZCCallbackHelper::CancelAutoscroll(aGuid.mScrollId);
+}
+
+void RemoteContentController::CancelAutoscrollCrossProcess(
+ const ScrollableLayerGuid& aGuid) {
+ MOZ_ASSERT(XRE_IsGPUProcess());
+
+ if (!mCompositorThread->IsOnCurrentThread()) {
+ mCompositorThread->Dispatch(NewRunnableMethod<ScrollableLayerGuid>(
+ "layers::RemoteContentController::CancelAutoscrollCrossProcess", this,
+ &RemoteContentController::CancelAutoscrollCrossProcess, aGuid));
+ return;
+ }
+
+ // The raw pointer to APZCTreeManagerParent is ok here because we are on the
+ // compositor thread.
+ if (APZCTreeManagerParent* parent =
+ CompositorBridgeParent::GetApzcTreeManagerParentForRoot(
+ aGuid.mLayersId)) {
+ Unused << parent->SendCancelAutoscroll(aGuid.mScrollId);
+ }
+}
+
+void RemoteContentController::NotifyScaleGestureComplete(
+ const ScrollableLayerGuid& aGuid, float aScale) {
+ if (XRE_GetProcessType() == GeckoProcessType_GPU) {
+ NotifyScaleGestureCompleteCrossProcess(aGuid, aScale);
+ } else {
+ NotifyScaleGestureCompleteInProcess(aGuid, aScale);
+ }
+}
+
+void RemoteContentController::NotifyScaleGestureCompleteInProcess(
+ const ScrollableLayerGuid& aGuid, float aScale) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+
+ if (!NS_IsMainThread()) {
+ NS_DispatchToMainThread(NewRunnableMethod<ScrollableLayerGuid, float>(
+ "layers::RemoteContentController::NotifyScaleGestureCompleteInProcess",
+ this, &RemoteContentController::NotifyScaleGestureCompleteInProcess,
+ aGuid, aScale));
+ return;
+ }
+
+ RefPtr<GeckoContentController> rootController =
+ CompositorBridgeParent::GetGeckoContentControllerForRoot(aGuid.mLayersId);
+ if (rootController) {
+ MOZ_ASSERT(rootController != this);
+ if (rootController != this) {
+ rootController->NotifyScaleGestureComplete(aGuid, aScale);
+ }
+ }
+}
+
+void RemoteContentController::NotifyScaleGestureCompleteCrossProcess(
+ const ScrollableLayerGuid& aGuid, float aScale) {
+ MOZ_ASSERT(XRE_IsGPUProcess());
+
+ if (!mCompositorThread->IsOnCurrentThread()) {
+ mCompositorThread->Dispatch(NewRunnableMethod<ScrollableLayerGuid, float>(
+ "layers::RemoteContentController::"
+ "NotifyScaleGestureCompleteCrossProcess",
+ this, &RemoteContentController::NotifyScaleGestureCompleteCrossProcess,
+ aGuid, aScale));
+ return;
+ }
+
+ // The raw pointer to APZCTreeManagerParent is ok here because we are on the
+ // compositor thread.
+ if (APZCTreeManagerParent* parent =
+ CompositorBridgeParent::GetApzcTreeManagerParentForRoot(
+ aGuid.mLayersId)) {
+ Unused << parent->SendNotifyScaleGestureComplete(aGuid.mScrollId, aScale);
+ }
+}
+
+void RemoteContentController::ActorDestroy(ActorDestroyReason aWhy) {
+ // This controller could possibly be kept alive longer after this
+ // by a RefPtr, but it is no longer valid to send messages.
+ mCanSend = false;
+}
+
+void RemoteContentController::Destroy() {
+ if (mCanSend) {
+ mCanSend = false;
+ Unused << SendDestroy();
+ }
+}
+
+mozilla::ipc::IPCResult RemoteContentController::RecvDestroy() {
+ // The actor on the other side is about to get destroyed, so let's not send
+ // it any more messages.
+ mCanSend = false;
+ return IPC_OK();
+}
+
+bool RemoteContentController::IsRemote() { return true; }
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/ipc/RemoteContentController.h b/gfx/layers/ipc/RemoteContentController.h
new file mode 100644
index 0000000000..168e924329
--- /dev/null
+++ b/gfx/layers/ipc/RemoteContentController.h
@@ -0,0 +1,123 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_RemoteContentController_h
+#define mozilla_layers_RemoteContentController_h
+
+#include "mozilla/layers/GeckoContentController.h"
+#include "mozilla/layers/PAPZParent.h"
+
+namespace mozilla {
+
+namespace dom {
+class BrowserParent;
+}
+
+namespace layers {
+
+/**
+ * RemoteContentController implements PAPZChild and is used to access a
+ * GeckoContentController that lives in a different process.
+ *
+ * RemoteContentController lives on the compositor thread. All methods can
+ * be called off the compositor thread and will get dispatched to the right
+ * thread, with the exception of RequestContentRepaint and NotifyFlushComplete,
+ * which must be called on the repaint thread, which in this case is the
+ * compositor thread.
+ */
+class RemoteContentController : public GeckoContentController,
+ public PAPZParent {
+ using GeckoContentController::APZStateChange;
+ using GeckoContentController::TapType;
+
+ public:
+ RemoteContentController();
+
+ virtual ~RemoteContentController();
+
+ void NotifyLayerTransforms(nsTArray<MatrixMessage>&& aTransforms) override;
+
+ void RequestContentRepaint(const RepaintRequest& aRequest) override;
+
+ void HandleTap(TapType aTapType, const LayoutDevicePoint& aPoint,
+ Modifiers aModifiers, const ScrollableLayerGuid& aGuid,
+ uint64_t aInputBlockId) override;
+
+ void NotifyPinchGesture(PinchGestureInput::PinchGestureType aType,
+ const ScrollableLayerGuid& aGuid,
+ const LayoutDevicePoint& aFocusPoint,
+ LayoutDeviceCoord aSpanChange,
+ Modifiers aModifiers) override;
+
+ bool IsRepaintThread() override;
+
+ void DispatchToRepaintThread(already_AddRefed<Runnable> aTask) override;
+
+ void NotifyAPZStateChange(const ScrollableLayerGuid& aGuid,
+ APZStateChange aChange, int aArg,
+ Maybe<uint64_t> aInputBlockId) override;
+
+ void UpdateOverscrollVelocity(const ScrollableLayerGuid& aGuid, float aX,
+ float aY, bool aIsRootContent) override;
+
+ void UpdateOverscrollOffset(const ScrollableLayerGuid& aGuid, float aX,
+ float aY, bool aIsRootContent) override;
+
+ void NotifyMozMouseScrollEvent(const ScrollableLayerGuid::ViewID& aScrollId,
+ const nsString& aEvent) override;
+
+ void NotifyFlushComplete() override;
+
+ void NotifyAsyncScrollbarDragInitiated(
+ uint64_t aDragBlockId, const ScrollableLayerGuid::ViewID& aScrollId,
+ ScrollDirection aDirection) override;
+ void NotifyAsyncScrollbarDragRejected(
+ const ScrollableLayerGuid::ViewID& aScrollId) override;
+
+ void NotifyAsyncAutoscrollRejected(
+ const ScrollableLayerGuid::ViewID& aScrollId) override;
+
+ void CancelAutoscroll(const ScrollableLayerGuid& aScrollId) override;
+
+ void NotifyScaleGestureComplete(const ScrollableLayerGuid& aGuid,
+ float aScale) override;
+
+ void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ void Destroy() override;
+ mozilla::ipc::IPCResult RecvDestroy();
+
+ bool IsRemote() override;
+
+ private:
+ nsCOMPtr<nsISerialEventTarget> mCompositorThread;
+ bool mCanSend;
+
+ void HandleTapOnMainThread(TapType aType, LayoutDevicePoint aPoint,
+ Modifiers aModifiers, ScrollableLayerGuid aGuid,
+ uint64_t aInputBlockId);
+ void HandleTapOnCompositorThread(TapType aType, LayoutDevicePoint aPoint,
+ Modifiers aModifiers,
+ ScrollableLayerGuid aGuid,
+ uint64_t aInputBlockId);
+ void NotifyPinchGestureOnCompositorThread(
+ PinchGestureInput::PinchGestureType aType,
+ const ScrollableLayerGuid& aGuid, const LayoutDevicePoint& aFocusPoint,
+ LayoutDeviceCoord aSpanChange, Modifiers aModifiers);
+
+ void CancelAutoscrollInProcess(const ScrollableLayerGuid& aScrollId);
+ void CancelAutoscrollCrossProcess(const ScrollableLayerGuid& aScrollId);
+ void NotifyScaleGestureCompleteInProcess(const ScrollableLayerGuid& aGuid,
+ float aScale);
+ void NotifyScaleGestureCompleteCrossProcess(const ScrollableLayerGuid& aGuid,
+ float aScale);
+};
+
+} // namespace layers
+
+} // namespace mozilla
+
+#endif // mozilla_layers_RemoteContentController_h
diff --git a/gfx/layers/ipc/ShadowLayerUtils.h b/gfx/layers/ipc/ShadowLayerUtils.h
new file mode 100644
index 0000000000..de07854b04
--- /dev/null
+++ b/gfx/layers/ipc/ShadowLayerUtils.h
@@ -0,0 +1,27 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef IPC_ShadowLayerUtils_h
+#define IPC_ShadowLayerUtils_h
+
+#include "ipc/EnumSerializer.h"
+#include "ipc/IPCMessageUtils.h"
+#include "GLContextTypes.h"
+#include "SurfaceDescriptor.h"
+#include "SurfaceTypes.h"
+#include "mozilla/WidgetUtils.h"
+
+namespace IPC {
+
+template <>
+struct ParamTraits<mozilla::ScreenRotation>
+ : public ContiguousEnumSerializer<mozilla::ScreenRotation,
+ mozilla::ROTATION_0,
+ mozilla::ROTATION_COUNT> {};
+
+} // namespace IPC
+
+#endif // IPC_ShadowLayerUtils_h
diff --git a/gfx/layers/ipc/SharedPlanarYCbCrImage.cpp b/gfx/layers/ipc/SharedPlanarYCbCrImage.cpp
new file mode 100644
index 0000000000..5b40268b5f
--- /dev/null
+++ b/gfx/layers/ipc/SharedPlanarYCbCrImage.cpp
@@ -0,0 +1,174 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "SharedPlanarYCbCrImage.h"
+#include <stddef.h> // for size_t
+#include <stdio.h> // for printf
+#include "gfx2DGlue.h" // for Moz2D transition helpers
+#include "ISurfaceAllocator.h" // for ISurfaceAllocator, etc
+#include "mozilla/Assertions.h" // for MOZ_ASSERT, etc
+#include "mozilla/gfx/Types.h" // for SurfaceFormat::SurfaceFormat::YUV
+#include "mozilla/ipc/SharedMemory.h" // for SharedMemory, etc
+#include "mozilla/layers/ImageClient.h" // for ImageClient
+#include "mozilla/layers/LayersSurfaces.h" // for SurfaceDescriptor, etc
+#include "mozilla/layers/TextureClient.h"
+#include "mozilla/layers/TextureClientRecycleAllocator.h"
+#include "mozilla/layers/BufferTexture.h"
+#include "mozilla/layers/ImageDataSerializer.h"
+#include "mozilla/layers/ImageBridgeChild.h" // for ImageBridgeChild
+#include "mozilla/mozalloc.h" // for operator delete
+#include "nsISupportsImpl.h" // for Image::AddRef
+#include "mozilla/ipc/Shmem.h"
+
+namespace mozilla {
+namespace layers {
+
+using namespace mozilla::ipc;
+
+SharedPlanarYCbCrImage::SharedPlanarYCbCrImage(ImageClient* aCompositable)
+ : mCompositable(aCompositable) {
+ MOZ_COUNT_CTOR(SharedPlanarYCbCrImage);
+}
+
+SharedPlanarYCbCrImage::SharedPlanarYCbCrImage(
+ TextureClientRecycleAllocator* aRecycleAllocator)
+ : mRecycleAllocator(aRecycleAllocator) {
+ MOZ_COUNT_CTOR(SharedPlanarYCbCrImage);
+}
+
+SharedPlanarYCbCrImage::~SharedPlanarYCbCrImage() {
+ MOZ_COUNT_DTOR(SharedPlanarYCbCrImage);
+}
+
+TextureClientRecycleAllocator* SharedPlanarYCbCrImage::RecycleAllocator() {
+ static const uint32_t MAX_POOLED_VIDEO_COUNT = 5;
+
+ if (!mRecycleAllocator && mCompositable) {
+ if (!mCompositable->HasTextureClientRecycler()) {
+ // Initialize TextureClientRecycler
+ mCompositable->GetTextureClientRecycler()->SetMaxPoolSize(
+ MAX_POOLED_VIDEO_COUNT);
+ }
+ mRecycleAllocator = mCompositable->GetTextureClientRecycler();
+ }
+ return mRecycleAllocator;
+}
+
+size_t SharedPlanarYCbCrImage::SizeOfExcludingThis(
+ MallocSizeOf aMallocSizeOf) const {
+ // NB: Explicitly skipping mTextureClient, the memory is already reported
+ // at time of allocation in GfxMemoryImageReporter.
+ // Not owned:
+ // - mCompositable
+ return 0;
+}
+
+TextureClient* SharedPlanarYCbCrImage::GetTextureClient(
+ KnowsCompositor* aKnowsCompositor) {
+ return mTextureClient.get();
+}
+
+already_AddRefed<gfx::SourceSurface>
+SharedPlanarYCbCrImage::GetAsSourceSurface() {
+ if (!IsValid()) {
+ NS_WARNING("Can't get as surface");
+ return nullptr;
+ }
+ return PlanarYCbCrImage::GetAsSourceSurface();
+}
+
+bool SharedPlanarYCbCrImage::CopyData(const PlanarYCbCrData& aData) {
+ // If mTextureClient has not already been allocated by CreateEmptyBuffer,
+ // allocate it. This code path is slower than the one used when
+ // CreateEmptyBuffer has been called since it will trigger a full copy.
+ if (!mTextureClient &&
+ !CreateEmptyBuffer(aData, aData.YDataSize(), aData.CbCrDataSize())) {
+ return false;
+ }
+
+ TextureClientAutoLock autoLock(mTextureClient, OpenMode::OPEN_WRITE_ONLY);
+ if (!autoLock.Succeeded()) {
+ MOZ_ASSERT(false, "Failed to lock the texture.");
+ return false;
+ }
+
+ if (!UpdateYCbCrTextureClient(mTextureClient, aData)) {
+ MOZ_ASSERT(false, "Failed to copy YCbCr data into the TextureClient");
+ return false;
+ }
+ mTextureClient->MarkImmutable();
+ return true;
+}
+
+bool SharedPlanarYCbCrImage::AdoptData(const Data& aData) {
+ MOZ_ASSERT(false, "This shouldn't be used.");
+ return false;
+}
+
+bool SharedPlanarYCbCrImage::IsValid() const {
+ return mTextureClient && mTextureClient->IsValid();
+}
+
+bool SharedPlanarYCbCrImage::CreateEmptyBuffer(const PlanarYCbCrData& aData,
+ const gfx::IntSize& aYSize,
+ const gfx::IntSize& aCbCrSize) {
+ MOZ_ASSERT(!mTextureClient, "This image already has allocated data");
+
+ TextureFlags flags =
+ mCompositable ? mCompositable->GetTextureFlags() : TextureFlags::DEFAULT;
+ {
+ YCbCrTextureClientAllocationHelper helper(aData, aYSize, aCbCrSize, flags);
+ mTextureClient = RecycleAllocator()->CreateOrRecycle(helper);
+ }
+
+ if (!mTextureClient) {
+ NS_WARNING("SharedPlanarYCbCrImage::Allocate failed.");
+ return false;
+ }
+
+ MappedYCbCrTextureData mapped;
+ // The locking here is sort of a lie. The SharedPlanarYCbCrImage just pulls
+ // pointers out of the TextureClient and keeps them around, which works only
+ // because the underlyin BufferTextureData is always mapped in memory even
+ // outside of the lock/unlock interval. That's sad and new code should follow
+ // this example.
+ if (!mTextureClient->Lock(OpenMode::OPEN_READ) ||
+ !mTextureClient->BorrowMappedYCbCrData(mapped)) {
+ MOZ_CRASH("GFX: Cannot lock or borrow mapped YCbCr");
+ }
+
+ // copy some of aData's values in mData (most of them)
+ mData.mYChannel = mapped.y.data;
+ mData.mCbChannel = mapped.cb.data;
+ mData.mCrChannel = mapped.cr.data;
+ mData.mPictureRect = aData.mPictureRect;
+ mData.mStereoMode = aData.mStereoMode;
+ mData.mYUVColorSpace = aData.mYUVColorSpace;
+ mData.mColorDepth = aData.mColorDepth;
+ mData.mChromaSubsampling = aData.mChromaSubsampling;
+ // those members are not always equal to aData's, due to potentially different
+ // packing.
+ mData.mYSkip = 0;
+ mData.mCbSkip = 0;
+ mData.mCrSkip = 0;
+ mData.mYStride = aData.mYStride;
+ mData.mCbCrStride = aData.mCbCrStride;
+
+ // do not set mBuffer like in PlanarYCbCrImage because the later
+ // will try to manage this memory without knowing it belongs to a
+ // shmem.
+ mBufferSize = ImageDataSerializer::ComputeYCbCrBufferSize(
+ aYSize, mData.mYStride, aCbCrSize, mData.mCbCrStride);
+ mSize = mData.mPictureRect.Size();
+ mOrigin = mData.mPictureRect.TopLeft();
+
+ mTextureClient->Unlock();
+
+ return mBufferSize > 0;
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/ipc/SharedPlanarYCbCrImage.h b/gfx/layers/ipc/SharedPlanarYCbCrImage.h
new file mode 100644
index 0000000000..8f6263600d
--- /dev/null
+++ b/gfx/layers/ipc/SharedPlanarYCbCrImage.h
@@ -0,0 +1,63 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include <stdint.h> // for uint8_t, uint32_t
+#include "ImageContainer.h" // for PlanarYCbCrImage, etc
+#include "mozilla/Attributes.h" // for override
+#include "mozilla/RefPtr.h" // for RefPtr
+#include "mozilla/ipc/Shmem.h" // for Shmem
+#include "nsCOMPtr.h" // for already_AddRefed
+#include "nsDebug.h" // for NS_WARNING
+#include "nsISupportsImpl.h" // for MOZ_COUNT_CTOR
+
+#ifndef MOZILLA_LAYERS_SHAREDPLANARYCBCRIMAGE_H
+# define MOZILLA_LAYERS_SHAREDPLANARYCBCRIMAGE_H
+
+namespace mozilla {
+namespace layers {
+
+class ImageClient;
+class TextureClient;
+class TextureClientRecycleAllocator;
+
+class SharedPlanarYCbCrImage : public PlanarYCbCrImage {
+ public:
+ explicit SharedPlanarYCbCrImage(ImageClient* aCompositable);
+ explicit SharedPlanarYCbCrImage(
+ TextureClientRecycleAllocator* aRecycleAllocator);
+
+ protected:
+ virtual ~SharedPlanarYCbCrImage();
+
+ public:
+ TextureClient* GetTextureClient(KnowsCompositor* aKnowsCompositor) override;
+
+ already_AddRefed<gfx::SourceSurface> GetAsSourceSurface() override;
+ bool CopyData(const PlanarYCbCrData& aData) override;
+ bool AdoptData(const Data& aData) override;
+ bool CreateEmptyBuffer(const Data& aData, const gfx::IntSize& aYSize,
+ const gfx::IntSize& aCbCrSize) override;
+
+ bool IsValid() const override;
+
+ size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override {
+ return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+ }
+
+ size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override;
+
+ TextureClientRecycleAllocator* RecycleAllocator();
+
+ private:
+ RefPtr<TextureClient> mTextureClient;
+ RefPtr<ImageClient> mCompositable;
+ RefPtr<TextureClientRecycleAllocator> mRecycleAllocator;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif
diff --git a/gfx/layers/ipc/SharedRGBImage.cpp b/gfx/layers/ipc/SharedRGBImage.cpp
new file mode 100644
index 0000000000..392cb09610
--- /dev/null
+++ b/gfx/layers/ipc/SharedRGBImage.cpp
@@ -0,0 +1,156 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "SharedRGBImage.h"
+#include "ImageTypes.h" // for ImageFormat::SHARED_RGB, etc
+#include "mozilla/ipc/Shmem.h" // for Shmem
+#include "gfx2DGlue.h" // for ImageFormatToSurfaceFormat, etc
+#include "gfxPlatform.h" // for gfxPlatform, gfxImageFormat
+#include "mozilla/gfx/Point.h" // for IntSIze
+#include "mozilla/layers/BufferTexture.h"
+#include "mozilla/layers/ISurfaceAllocator.h" // for ISurfaceAllocator, etc
+#include "mozilla/layers/ImageClient.h" // for ImageClient
+#include "mozilla/layers/LayersSurfaces.h" // for SurfaceDescriptor, etc
+#include "mozilla/layers/TextureClient.h" // for BufferTextureClient, etc
+#include "mozilla/layers/TextureClientRecycleAllocator.h" // for ITextureClientAllocationHelper
+#include "mozilla/layers/ImageBridgeChild.h" // for ImageBridgeChild
+#include "mozilla/mozalloc.h" // for operator delete, etc
+#include "nsDebug.h" // for NS_WARNING, NS_ASSERTION
+#include "nsISupportsImpl.h" // for Image::AddRef, etc
+#include "nsProxyRelease.h"
+#include "nsRect.h" // for mozilla::gfx::IntRect
+
+// Just big enough for a 1080p RGBA32 frame
+#define MAX_FRAME_SIZE (16 * 1024 * 1024)
+
+namespace mozilla {
+namespace layers {
+
+class TextureClientForRawBufferAccessAllocationHelper
+ : public ITextureClientAllocationHelper {
+ public:
+ TextureClientForRawBufferAccessAllocationHelper(gfx::SurfaceFormat aFormat,
+ gfx::IntSize aSize,
+ TextureFlags aTextureFlags)
+ : ITextureClientAllocationHelper(aFormat, aSize, BackendSelector::Content,
+ aTextureFlags, ALLOC_DEFAULT) {}
+
+ bool IsCompatible(TextureClient* aTextureClient) override {
+ bool ret = aTextureClient->GetFormat() == mFormat &&
+ aTextureClient->GetSize() == mSize;
+ return ret;
+ }
+
+ already_AddRefed<TextureClient> Allocate(
+ KnowsCompositor* aAllocator) override {
+ return TextureClient::CreateForRawBufferAccess(
+ aAllocator, mFormat, mSize, gfx::BackendType::NONE, mTextureFlags);
+ }
+};
+
+SharedRGBImage::SharedRGBImage(ImageClient* aCompositable)
+ : Image(nullptr, ImageFormat::SHARED_RGB), mCompositable(aCompositable) {
+ MOZ_COUNT_CTOR(SharedRGBImage);
+}
+
+SharedRGBImage::SharedRGBImage(TextureClientRecycleAllocator* aRecycleAllocator)
+ : Image(nullptr, ImageFormat::SHARED_RGB),
+ mRecycleAllocator(aRecycleAllocator) {
+ MOZ_COUNT_CTOR(SharedRGBImage);
+}
+
+SharedRGBImage::~SharedRGBImage() {
+ MOZ_COUNT_DTOR(SharedRGBImage);
+ NS_ReleaseOnMainThread("SharedRGBImage::mSourceSurface",
+ mSourceSurface.forget());
+}
+
+TextureClientRecycleAllocator* SharedRGBImage::RecycleAllocator() {
+ static const uint32_t MAX_POOLED_VIDEO_COUNT = 5;
+
+ if (!mRecycleAllocator && mCompositable) {
+ if (!mCompositable->HasTextureClientRecycler()) {
+ // Initialize TextureClientRecycler
+ mCompositable->GetTextureClientRecycler()->SetMaxPoolSize(
+ MAX_POOLED_VIDEO_COUNT);
+ }
+ mRecycleAllocator = mCompositable->GetTextureClientRecycler();
+ }
+ return mRecycleAllocator;
+}
+
+bool SharedRGBImage::Allocate(gfx::IntSize aSize, gfx::SurfaceFormat aFormat) {
+ mSize = aSize;
+
+ TextureFlags flags =
+ mCompositable ? mCompositable->GetTextureFlags() : TextureFlags::DEFAULT;
+ {
+ TextureClientForRawBufferAccessAllocationHelper helper(aFormat, aSize,
+ flags);
+ mTextureClient = RecycleAllocator()->CreateOrRecycle(helper);
+ }
+
+ return !!mTextureClient;
+}
+
+gfx::IntSize SharedRGBImage::GetSize() const { return mSize; }
+
+TextureClient* SharedRGBImage::GetTextureClient(
+ KnowsCompositor* aKnowsCompositor) {
+ return mTextureClient.get();
+}
+
+static void ReleaseTextureClient(void* aData) {
+ RELEASE_MANUALLY(static_cast<TextureClient*>(aData));
+}
+
+static gfx::UserDataKey sTextureClientKey;
+
+already_AddRefed<gfx::SourceSurface> SharedRGBImage::GetAsSourceSurface() {
+ NS_ASSERTION(NS_IsMainThread(), "Must be main thread");
+
+ if (mSourceSurface) {
+ RefPtr<gfx::SourceSurface> surface(mSourceSurface);
+ return surface.forget();
+ }
+
+ RefPtr<gfx::SourceSurface> surface;
+ {
+ // We are 'borrowing' the DrawTarget and retaining a permanent reference to
+ // the underlying data (via the surface). It is in this instance since we
+ // know that the TextureClient is always wrapping a BufferTextureData and
+ // therefore it won't go away underneath us.
+ BufferTextureData* decoded_buffer =
+ mTextureClient->GetInternalData()->AsBufferTextureData();
+ RefPtr<gfx::DrawTarget> drawTarget = decoded_buffer->BorrowDrawTarget();
+
+ if (!drawTarget) {
+ return nullptr;
+ }
+
+ surface = drawTarget->Snapshot();
+ if (!surface) {
+ return nullptr;
+ }
+
+ // The surface may outlive the owning TextureClient. So, we need to ensure
+ // that the surface keeps the TextureClient alive via a reference held in
+ // user data. The TextureClient's DrawTarget only has a weak reference to
+ // the surface, so we won't create any cycles by just referencing the
+ // TextureClient.
+ if (!surface->GetUserData(&sTextureClientKey)) {
+ surface->AddUserData(&sTextureClientKey, mTextureClient,
+ ReleaseTextureClient);
+ ADDREF_MANUALLY(mTextureClient);
+ }
+ }
+
+ mSourceSurface = surface;
+ return surface.forget();
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/ipc/SharedRGBImage.h b/gfx/layers/ipc/SharedRGBImage.h
new file mode 100644
index 0000000000..ddec2dc9aa
--- /dev/null
+++ b/gfx/layers/ipc/SharedRGBImage.h
@@ -0,0 +1,61 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef SHAREDRGBIMAGE_H_
+#define SHAREDRGBIMAGE_H_
+
+#include <stddef.h> // for size_t
+#include <stdint.h> // for uint8_t
+
+#include "ImageContainer.h" // for ISharedImage, Image, etc
+#include "gfxTypes.h"
+#include "mozilla/Attributes.h" // for override
+#include "mozilla/RefPtr.h" // for RefPtr
+#include "mozilla/gfx/Point.h" // for IntSize
+#include "mozilla/gfx/Types.h" // for SurfaceFormat
+#include "nsCOMPtr.h" // for already_AddRefed
+
+namespace mozilla {
+namespace layers {
+
+class ImageClient;
+class TextureClient;
+
+/**
+ * Stores RGB data in shared memory
+ * It is assumed that the image width and stride are equal
+ */
+class SharedRGBImage : public Image {
+ public:
+ explicit SharedRGBImage(ImageClient* aCompositable);
+ explicit SharedRGBImage(TextureClientRecycleAllocator* aRecycleAllocator);
+
+ protected:
+ virtual ~SharedRGBImage();
+
+ public:
+ TextureClient* GetTextureClient(KnowsCompositor* aKnowsCompositor) override;
+
+ gfx::IntSize GetSize() const override;
+
+ already_AddRefed<gfx::SourceSurface> GetAsSourceSurface() override;
+
+ bool Allocate(gfx::IntSize aSize, gfx::SurfaceFormat aFormat);
+
+ private:
+ TextureClientRecycleAllocator* RecycleAllocator();
+
+ gfx::IntSize mSize;
+ RefPtr<TextureClient> mTextureClient;
+ RefPtr<ImageClient> mCompositable;
+ RefPtr<TextureClientRecycleAllocator> mRecycleAllocator;
+ RefPtr<gfx::SourceSurface> mSourceSurface;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif
diff --git a/gfx/layers/ipc/SharedSurfacesChild.cpp b/gfx/layers/ipc/SharedSurfacesChild.cpp
new file mode 100644
index 0000000000..1949acb8ad
--- /dev/null
+++ b/gfx/layers/ipc/SharedSurfacesChild.cpp
@@ -0,0 +1,587 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "SharedSurfacesChild.h"
+#include "SharedSurfacesParent.h"
+#include "CompositorManagerChild.h"
+#include "mozilla/layers/IpcResourceUpdateQueue.h"
+#include "mozilla/layers/SourceSurfaceSharedData.h"
+#include "mozilla/layers/WebRenderBridgeChild.h"
+#include "mozilla/layers/RenderRootStateManager.h"
+#include "mozilla/SchedulerGroup.h"
+#include "mozilla/StaticPrefs_image.h"
+
+namespace mozilla {
+namespace layers {
+
+using namespace mozilla::gfx;
+
+/* static */
+UserDataKey SharedSurfacesChild::sSharedKey;
+
+SharedSurfacesChild::ImageKeyData::ImageKeyData(
+ RenderRootStateManager* aManager, const wr::ImageKey& aImageKey)
+ : mManager(aManager), mImageKey(aImageKey) {}
+
+SharedSurfacesChild::ImageKeyData::ImageKeyData(
+ SharedSurfacesChild::ImageKeyData&& aOther)
+ : mManager(std::move(aOther.mManager)),
+ mDirtyRect(std::move(aOther.mDirtyRect)),
+ mImageKey(aOther.mImageKey) {}
+
+SharedSurfacesChild::ImageKeyData& SharedSurfacesChild::ImageKeyData::operator=(
+ SharedSurfacesChild::ImageKeyData&& aOther) {
+ mManager = std::move(aOther.mManager);
+ mDirtyRect = std::move(aOther.mDirtyRect);
+ mImageKey = aOther.mImageKey;
+ return *this;
+}
+
+SharedSurfacesChild::ImageKeyData::~ImageKeyData() = default;
+
+void SharedSurfacesChild::ImageKeyData::MergeDirtyRect(
+ const Maybe<IntRect>& aDirtyRect) {
+ if (mDirtyRect) {
+ if (aDirtyRect) {
+ mDirtyRect->UnionRect(mDirtyRect.ref(), aDirtyRect.ref());
+ }
+ } else {
+ mDirtyRect = aDirtyRect;
+ }
+}
+
+SharedSurfacesChild::SharedUserData::SharedUserData(
+ const wr::ExternalImageId& aId)
+ : Runnable("SharedSurfacesChild::SharedUserData"),
+ mId(aId),
+ mShared(false) {}
+
+SharedSurfacesChild::SharedUserData::~SharedUserData() {
+ // We may fail to dispatch during shutdown, and since it would be issued on
+ // the main thread, it releases the runnable instead of leaking it.
+ if (mShared || !mKeys.IsEmpty()) {
+ if (NS_IsMainThread()) {
+ SharedSurfacesChild::Unshare(mId, mShared, mKeys);
+ } else {
+ MOZ_ASSERT_UNREACHABLE("Shared resources not released!");
+ }
+ }
+}
+
+/* static */
+void SharedSurfacesChild::SharedUserData::Destroy(void* aClosure) {
+ MOZ_ASSERT(aClosure);
+ RefPtr<SharedUserData> data =
+ dont_AddRef(static_cast<SharedUserData*>(aClosure));
+ if (data->mShared || !data->mKeys.IsEmpty()) {
+ SchedulerGroup::Dispatch(TaskCategory::Other, data.forget());
+ }
+}
+
+NS_IMETHODIMP SharedSurfacesChild::SharedUserData::Run() {
+ SharedSurfacesChild::Unshare(mId, mShared, mKeys);
+ mShared = false;
+ mKeys.Clear();
+ return NS_OK;
+}
+
+wr::ImageKey SharedSurfacesChild::SharedUserData::UpdateKey(
+ RenderRootStateManager* aManager, wr::IpcResourceUpdateQueue& aResources,
+ const Maybe<IntRect>& aDirtyRect) {
+ MOZ_ASSERT(aManager);
+ MOZ_ASSERT(!aManager->IsDestroyed());
+
+ // We iterate through all of the items to ensure we clean up the old
+ // RenderRootStateManager references. Most of the time there will be few
+ // entries and this should not be particularly expensive compared to the
+ // cost of duplicating image keys. In an ideal world, we would generate a
+ // single key for the surface, and it would be usable on all of the
+ // renderer instances. For now, we must allocate a key for each WR bridge.
+ wr::ImageKey key;
+ bool found = false;
+ auto i = mKeys.Length();
+ while (i > 0) {
+ --i;
+ ImageKeyData& entry = mKeys[i];
+ if (entry.mManager->IsDestroyed()) {
+ mKeys.RemoveElementAt(i);
+ } else if (entry.mManager == aManager) {
+ WebRenderBridgeChild* wrBridge = aManager->WrBridge();
+ MOZ_ASSERT(wrBridge);
+
+ // Even if the manager is the same, its underlying WebRenderBridgeChild
+ // can change state. If our namespace differs, then our old key has
+ // already been discarded.
+ bool ownsKey = wrBridge->GetNamespace() == entry.mImageKey.mNamespace;
+ if (!ownsKey) {
+ entry.mImageKey = wrBridge->GetNextImageKey();
+ entry.TakeDirtyRect();
+ aResources.AddSharedExternalImage(mId, entry.mImageKey);
+ } else {
+ entry.MergeDirtyRect(aDirtyRect);
+ Maybe<IntRect> dirtyRect = entry.TakeDirtyRect();
+ if (dirtyRect) {
+ MOZ_ASSERT(mShared);
+ aResources.UpdateSharedExternalImage(
+ mId, entry.mImageKey, ViewAs<ImagePixel>(dirtyRect.ref()));
+ }
+ }
+
+ key = entry.mImageKey;
+ found = true;
+ } else {
+ // We don't have the resource update queue for this manager, so just
+ // accumulate the dirty rects until it is requested.
+ entry.MergeDirtyRect(aDirtyRect);
+ }
+ }
+
+ if (!found) {
+ key = aManager->WrBridge()->GetNextImageKey();
+ ImageKeyData data(aManager, key);
+ mKeys.AppendElement(std::move(data));
+ aResources.AddSharedExternalImage(mId, key);
+ }
+
+ return key;
+}
+
+/* static */
+SourceSurfaceSharedData* SharedSurfacesChild::AsSourceSurfaceSharedData(
+ SourceSurface* aSurface) {
+ MOZ_ASSERT(aSurface);
+ switch (aSurface->GetType()) {
+ case SurfaceType::DATA_SHARED:
+ case SurfaceType::DATA_RECYCLING_SHARED:
+ return static_cast<SourceSurfaceSharedData*>(aSurface);
+ default:
+ return nullptr;
+ }
+}
+
+/* static */
+nsresult SharedSurfacesChild::ShareInternal(SourceSurfaceSharedData* aSurface,
+ SharedUserData** aUserData) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aSurface);
+ MOZ_ASSERT(aUserData);
+
+ CompositorManagerChild* manager = CompositorManagerChild::GetInstance();
+ if (NS_WARN_IF(!manager || !manager->CanSend())) {
+ // We cannot try to share the surface, most likely because the GPU process
+ // crashed. Ideally, we would retry when it is ready, but the handles may be
+ // a scarce resource, which can cause much more serious problems if we run
+ // out. Better to copy into a fresh buffer later.
+ aSurface->FinishedSharing();
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ SharedUserData* data =
+ static_cast<SharedUserData*>(aSurface->GetUserData(&sSharedKey));
+ if (!data) {
+ data =
+ MakeAndAddRef<SharedUserData>(manager->GetNextExternalImageId()).take();
+ aSurface->AddUserData(&sSharedKey, data, SharedUserData::Destroy);
+ } else if (!manager->OwnsExternalImageId(data->Id())) {
+ // If the id isn't owned by us, that means the bridge was reinitialized, due
+ // to the GPU process crashing. All previous mappings have been released.
+ data->SetId(manager->GetNextExternalImageId());
+ } else if (data->IsShared()) {
+ // It has already been shared with the GPU process.
+ *aUserData = data;
+ return NS_OK;
+ }
+
+ // Ensure that the handle doesn't get released until after we have finished
+ // sending the buffer to the GPU process and/or reallocating it.
+ // FinishedSharing is not a sufficient condition because another thread may
+ // decide we are done while we are in the processing of sharing our newly
+ // reallocated handle. Once it goes out of scope, it may release the handle.
+ SourceSurfaceSharedData::HandleLock lock(aSurface);
+
+ // If we live in the same process, then it is a simple matter of directly
+ // asking the parent instance to store a pointer to the same data, no need
+ // to map the data into our memory space twice.
+ if (manager->SameProcess()) {
+ SharedSurfacesParent::AddSameProcess(data->Id(), aSurface);
+ data->MarkShared();
+ *aUserData = data;
+ return NS_OK;
+ }
+
+ // Attempt to share a handle with the GPU process. The handle may or may not
+ // be available -- it will only be available if it is either not yet finalized
+ // and/or if it has been finalized but never used for drawing in process.
+ ipc::SharedMemoryBasic::Handle handle = ipc::SharedMemoryBasic::NULLHandle();
+ nsresult rv = aSurface->CloneHandle(handle);
+ if (rv == NS_ERROR_NOT_AVAILABLE) {
+ // It is at least as expensive to copy the image to the GPU process if we
+ // have already closed the handle necessary to share, but if we reallocate
+ // the shared buffer to get a new handle, we can save some memory.
+ if (NS_WARN_IF(!aSurface->ReallocHandle())) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ // Reattempt the sharing of the handle to the GPU process.
+ rv = aSurface->CloneHandle(handle);
+ }
+
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_ASSERT(rv != NS_ERROR_NOT_AVAILABLE);
+ return rv;
+ }
+
+ SurfaceFormat format = aSurface->GetFormat();
+ MOZ_RELEASE_ASSERT(
+ format == SurfaceFormat::B8G8R8X8 || format == SurfaceFormat::B8G8R8A8,
+ "bad format");
+
+ data->MarkShared();
+ manager->SendAddSharedSurface(
+ data->Id(),
+ SurfaceDescriptorShared(aSurface->GetSize(), aSurface->Stride(), format,
+ std::move(handle)));
+ *aUserData = data;
+ return NS_OK;
+}
+
+/* static */
+void SharedSurfacesChild::Share(SourceSurfaceSharedData* aSurface) {
+ MOZ_ASSERT(aSurface);
+
+ // The IPDL actor to do sharing can only be accessed on the main thread so we
+ // need to dispatch if off the main thread. However there is no real danger if
+ // we end up racing because if it is already shared, this method will do
+ // nothing.
+ if (!NS_IsMainThread()) {
+ class ShareRunnable final : public Runnable {
+ public:
+ explicit ShareRunnable(SourceSurfaceSharedData* aSurface)
+ : Runnable("SharedSurfacesChild::Share"), mSurface(aSurface) {}
+
+ NS_IMETHOD Run() override {
+ SharedUserData* unused = nullptr;
+ SharedSurfacesChild::ShareInternal(mSurface, &unused);
+ return NS_OK;
+ }
+
+ private:
+ RefPtr<SourceSurfaceSharedData> mSurface;
+ };
+
+ SchedulerGroup::Dispatch(TaskCategory::Other,
+ MakeAndAddRef<ShareRunnable>(aSurface));
+ return;
+ }
+
+ SharedUserData* unused = nullptr;
+ SharedSurfacesChild::ShareInternal(aSurface, &unused);
+}
+
+/* static */
+nsresult SharedSurfacesChild::Share(SourceSurfaceSharedData* aSurface,
+ RenderRootStateManager* aManager,
+ wr::IpcResourceUpdateQueue& aResources,
+ wr::ImageKey& aKey) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aSurface);
+ MOZ_ASSERT(aManager);
+
+ // Each time the surface changes, the producers of SourceSurfaceSharedData
+ // surfaces promise to increment the invalidation counter each time the
+ // surface has changed. We can use this counter to determine whether or not
+ // we should update our paired ImageKey.
+ Maybe<IntRect> dirtyRect = aSurface->TakeDirtyRect();
+ SharedUserData* data = nullptr;
+ nsresult rv = SharedSurfacesChild::ShareInternal(aSurface, &data);
+ if (NS_SUCCEEDED(rv)) {
+ MOZ_ASSERT(data);
+ aKey = data->UpdateKey(aManager, aResources, dirtyRect);
+ }
+
+ return rv;
+}
+
+/* static */
+nsresult SharedSurfacesChild::Share(SourceSurface* aSurface,
+ RenderRootStateManager* aManager,
+ wr::IpcResourceUpdateQueue& aResources,
+ wr::ImageKey& aKey) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aSurface);
+ MOZ_ASSERT(aManager);
+
+ auto sharedSurface = AsSourceSurfaceSharedData(aSurface);
+ if (!sharedSurface) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+
+ return Share(sharedSurface, aManager, aResources, aKey);
+}
+
+/* static */
+nsresult SharedSurfacesChild::Share(SourceSurface* aSurface,
+ wr::ExternalImageId& aId) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aSurface);
+
+ auto sharedSurface = AsSourceSurfaceSharedData(aSurface);
+ if (!sharedSurface) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+
+ // The external image ID does not change with the invalidation counter. The
+ // caller of this should be aware of the invalidations of the surface through
+ // another mechanism (e.g. imgRequestProxy listener notifications).
+ SharedUserData* data = nullptr;
+ nsresult rv = ShareInternal(sharedSurface, &data);
+ if (NS_SUCCEEDED(rv)) {
+ MOZ_ASSERT(data);
+ aId = data->Id();
+ }
+
+ return rv;
+}
+
+/* static */
+void SharedSurfacesChild::Unshare(const wr::ExternalImageId& aId,
+ bool aReleaseId,
+ nsTArray<ImageKeyData>& aKeys) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ for (const auto& entry : aKeys) {
+ if (!entry.mManager->IsDestroyed()) {
+ entry.mManager->AddImageKeyForDiscard(entry.mImageKey);
+ }
+ }
+
+ if (!aReleaseId) {
+ // We don't own the external image ID itself.
+ return;
+ }
+
+ CompositorManagerChild* manager = CompositorManagerChild::GetInstance();
+ if (MOZ_UNLIKELY(!manager || !manager->CanSend())) {
+ return;
+ }
+
+ if (manager->OwnsExternalImageId(aId)) {
+ // Only attempt to release current mappings in the compositor process. It is
+ // possible we had a surface that was previously shared, the compositor
+ // process crashed / was restarted, and then we freed the surface. In that
+ // case we know the mapping has already been freed.
+ manager->SendRemoveSharedSurface(aId);
+ }
+}
+
+/* static */ Maybe<wr::ExternalImageId> SharedSurfacesChild::GetExternalId(
+ const SourceSurfaceSharedData* aSurface) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aSurface);
+
+ SharedUserData* data =
+ static_cast<SharedUserData*>(aSurface->GetUserData(&sSharedKey));
+ if (!data || !data->IsShared()) {
+ return Nothing();
+ }
+
+ return Some(data->Id());
+}
+
+AnimationImageKeyData::AnimationImageKeyData(RenderRootStateManager* aManager,
+ const wr::ImageKey& aImageKey)
+ : SharedSurfacesChild::ImageKeyData(aManager, aImageKey) {}
+
+AnimationImageKeyData::AnimationImageKeyData(AnimationImageKeyData&& aOther)
+ : SharedSurfacesChild::ImageKeyData(std::move(aOther)),
+ mPendingRelease(std::move(aOther.mPendingRelease)) {}
+
+AnimationImageKeyData& AnimationImageKeyData::operator=(
+ AnimationImageKeyData&& aOther) {
+ mPendingRelease = std::move(aOther.mPendingRelease);
+ SharedSurfacesChild::ImageKeyData::operator=(std::move(aOther));
+ return *this;
+}
+
+AnimationImageKeyData::~AnimationImageKeyData() = default;
+
+SharedSurfacesAnimation::~SharedSurfacesAnimation() {
+ MOZ_ASSERT(mKeys.IsEmpty());
+}
+
+void SharedSurfacesAnimation::Destroy() {
+ if (!NS_IsMainThread()) {
+ nsCOMPtr<nsIRunnable> task =
+ NewRunnableMethod("SharedSurfacesAnimation::Destroy", this,
+ &SharedSurfacesAnimation::Destroy);
+ SchedulerGroup::Dispatch(TaskCategory::Other, task.forget());
+ return;
+ }
+
+ if (mKeys.IsEmpty()) {
+ return;
+ }
+
+ for (const auto& entry : mKeys) {
+ MOZ_ASSERT(!entry.mManager->IsDestroyed());
+ if (StaticPrefs::image_animated_decode_on_demand_recycle_AtStartup()) {
+ entry.mManager->DeregisterAsyncAnimation(entry.mImageKey);
+ }
+ entry.mManager->AddImageKeyForDiscard(entry.mImageKey);
+ }
+
+ mKeys.Clear();
+}
+
+void SharedSurfacesAnimation::HoldSurfaceForRecycling(
+ AnimationImageKeyData& aEntry, SourceSurfaceSharedData* aSurface) {
+ if (aSurface->GetType() != SurfaceType::DATA_RECYCLING_SHARED) {
+ return;
+ }
+
+ MOZ_ASSERT(StaticPrefs::image_animated_decode_on_demand_recycle_AtStartup());
+ aEntry.mPendingRelease.AppendElement(aSurface);
+}
+
+nsresult SharedSurfacesAnimation::SetCurrentFrame(
+ SourceSurfaceSharedData* aSurface, const gfx::IntRect& aDirtyRect) {
+ MOZ_ASSERT(aSurface);
+
+ SharedSurfacesChild::SharedUserData* data = nullptr;
+ nsresult rv = SharedSurfacesChild::ShareInternal(aSurface, &data);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ MOZ_ASSERT(data);
+ mId = data->Id();
+
+ auto i = mKeys.Length();
+ while (i > 0) {
+ --i;
+ AnimationImageKeyData& entry = mKeys[i];
+ MOZ_ASSERT(!entry.mManager->IsDestroyed());
+
+ entry.MergeDirtyRect(Some(aDirtyRect));
+ Maybe<IntRect> dirtyRect = entry.TakeDirtyRect();
+ if (dirtyRect) {
+ HoldSurfaceForRecycling(entry, aSurface);
+ auto& resourceUpdates = entry.mManager->AsyncResourceUpdates();
+ resourceUpdates.UpdateSharedExternalImage(
+ mId, entry.mImageKey, ViewAs<ImagePixel>(dirtyRect.ref()));
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult SharedSurfacesAnimation::UpdateKey(
+ SourceSurfaceSharedData* aSurface, RenderRootStateManager* aManager,
+ wr::IpcResourceUpdateQueue& aResources, wr::ImageKey& aKey) {
+ SharedSurfacesChild::SharedUserData* data = nullptr;
+ nsresult rv = SharedSurfacesChild::ShareInternal(aSurface, &data);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ MOZ_ASSERT(data);
+ if (wr::AsUint64(mId) != wr::AsUint64(data->Id())) {
+ mKeys.Clear();
+ mId = data->Id();
+ }
+
+ // We iterate through all of the items to ensure we clean up the old
+ // RenderRootStateManager references. Most of the time there will be few
+ // entries and this should not be particularly expensive compared to the
+ // cost of duplicating image keys. In an ideal world, we would generate a
+ // single key for the surface, and it would be usable on all of the
+ // renderer instances. For now, we must allocate a key for each WR bridge.
+ bool found = false;
+ auto i = mKeys.Length();
+ while (i > 0) {
+ --i;
+ AnimationImageKeyData& entry = mKeys[i];
+ MOZ_ASSERT(!entry.mManager->IsDestroyed());
+ if (entry.mManager == aManager) {
+ WebRenderBridgeChild* wrBridge = aManager->WrBridge();
+ MOZ_ASSERT(wrBridge);
+
+ // Even if the manager is the same, its underlying WebRenderBridgeChild
+ // can change state. If our namespace differs, then our old key has
+ // already been discarded.
+ bool ownsKey = wrBridge->GetNamespace() == entry.mImageKey.mNamespace;
+ if (!ownsKey) {
+ entry.mImageKey = wrBridge->GetNextImageKey();
+ HoldSurfaceForRecycling(entry, aSurface);
+ aResources.AddSharedExternalImage(mId, entry.mImageKey);
+ } else {
+ MOZ_ASSERT(entry.mDirtyRect.isNothing());
+ }
+
+ aKey = entry.mImageKey;
+ found = true;
+ break;
+ }
+ }
+
+ if (!found) {
+ aKey = aManager->WrBridge()->GetNextImageKey();
+ if (StaticPrefs::image_animated_decode_on_demand_recycle_AtStartup()) {
+ aManager->RegisterAsyncAnimation(aKey, this);
+ }
+
+ AnimationImageKeyData data(aManager, aKey);
+ HoldSurfaceForRecycling(data, aSurface);
+ mKeys.AppendElement(std::move(data));
+ aResources.AddSharedExternalImage(mId, aKey);
+ }
+
+ return NS_OK;
+}
+
+void SharedSurfacesAnimation::ReleasePreviousFrame(
+ RenderRootStateManager* aManager, const wr::ExternalImageId& aId) {
+ MOZ_ASSERT(aManager);
+
+ auto i = mKeys.Length();
+ while (i > 0) {
+ --i;
+ AnimationImageKeyData& entry = mKeys[i];
+ MOZ_ASSERT(!entry.mManager->IsDestroyed());
+ if (entry.mManager == aManager) {
+ size_t k;
+ for (k = 0; k < entry.mPendingRelease.Length(); ++k) {
+ Maybe<wr::ExternalImageId> extId =
+ SharedSurfacesChild::GetExternalId(entry.mPendingRelease[k]);
+ if (extId && extId.ref() == aId) {
+ break;
+ }
+ }
+
+ if (k == entry.mPendingRelease.Length()) {
+ continue;
+ }
+
+ entry.mPendingRelease.RemoveElementsAt(0, k + 1);
+ break;
+ }
+ }
+}
+
+void SharedSurfacesAnimation::Invalidate(RenderRootStateManager* aManager) {
+ auto i = mKeys.Length();
+ while (i > 0) {
+ --i;
+ AnimationImageKeyData& entry = mKeys[i];
+ if (entry.mManager == aManager) {
+ mKeys.RemoveElementAt(i);
+ break;
+ }
+ }
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/ipc/SharedSurfacesChild.h b/gfx/layers/ipc/SharedSurfacesChild.h
new file mode 100644
index 0000000000..6fb40931fb
--- /dev/null
+++ b/gfx/layers/ipc/SharedSurfacesChild.h
@@ -0,0 +1,250 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_GFX_SHAREDSURFACESCHILD_H
+#define MOZILLA_GFX_SHAREDSURFACESCHILD_H
+
+#include <stdint.h> // for uint32_t, uint64_t
+#include "mozilla/Attributes.h" // for override
+#include "mozilla/Maybe.h" // for Maybe
+#include "mozilla/RefPtr.h" // for already_AddRefed
+#include "mozilla/StaticPtr.h" // for StaticRefPtr
+#include "mozilla/gfx/UserData.h" // for UserDataKey
+#include "mozilla/webrender/WebRenderTypes.h" // for wr::ImageKey
+#include "nsTArray.h" // for AutoTArray
+#include "nsThreadUtils.h" // for Runnable
+#include "ImageTypes.h" // for ContainerProducerID
+
+namespace mozilla {
+namespace layers {
+class AnimationImageKeyData;
+} // namespace layers
+} // namespace mozilla
+
+template <>
+struct nsTArray_RelocationStrategy<mozilla::layers::AnimationImageKeyData> {
+ typedef nsTArray_RelocateUsingMoveConstructor<
+ mozilla::layers::AnimationImageKeyData>
+ Type;
+};
+
+namespace mozilla {
+namespace gfx {
+class SourceSurface;
+class SourceSurfaceSharedData;
+} // namespace gfx
+
+namespace wr {
+class IpcResourceUpdateQueue;
+} // namespace wr
+
+namespace layers {
+
+class CompositorManagerChild;
+class RenderRootStateManager;
+
+class SharedSurfacesChild {
+ public:
+ /**
+ * Request that the surface be mapped into the compositor thread's memory
+ * space. This is useful for when the caller itself has no present need for
+ * the surface to be mapped, but knows there will be such a need in the
+ * future. This may be called from any thread, but it may cause a dispatch to
+ * the main thread.
+ */
+ static void Share(gfx::SourceSurfaceSharedData* aSurface);
+
+ /**
+ * Request that the surface be mapped into the compositor thread's memory
+ * space, and a valid ExternalImageId be generated for it for use with
+ * WebRender. This must be called from the main thread.
+ */
+ static nsresult Share(gfx::SourceSurface* aSurface, wr::ExternalImageId& aId);
+
+ /**
+ * Request that the surface be mapped into the compositor thread's memory
+ * space, and a valid ImageKey be generated for it for use with WebRender.
+ * This must be called from the main thread.
+ */
+ static nsresult Share(gfx::SourceSurfaceSharedData* aSurface,
+ RenderRootStateManager* aManager,
+ wr::IpcResourceUpdateQueue& aResources,
+ wr::ImageKey& aKey);
+
+ /**
+ * Request that the surface be mapped into the compositor thread's memory
+ * space, and a valid ImageKey be generated for it for use with WebRender.
+ * This must be called from the main thread.
+ */
+ static nsresult Share(gfx::SourceSurface* aSurface,
+ RenderRootStateManager* aManager,
+ wr::IpcResourceUpdateQueue& aResources,
+ wr::ImageKey& aKey);
+
+ /**
+ * Get the external ID, if any, bound to the shared surface. Used for memory
+ * reporting purposes.
+ */
+ static Maybe<wr::ExternalImageId> GetExternalId(
+ const gfx::SourceSurfaceSharedData* aSurface);
+
+ /**
+ * Get the surface (or its underlying surface) as a SourceSurfaceSharedData
+ * pointer, if valid.
+ */
+ static gfx::SourceSurfaceSharedData* AsSourceSurfaceSharedData(
+ gfx::SourceSurface* aSurface);
+
+ class ImageKeyData {
+ public:
+ ImageKeyData(RenderRootStateManager* aManager,
+ const wr::ImageKey& aImageKey);
+ virtual ~ImageKeyData();
+
+ ImageKeyData(ImageKeyData&& aOther);
+ ImageKeyData& operator=(ImageKeyData&& aOther);
+ ImageKeyData(const ImageKeyData&) = delete;
+ ImageKeyData& operator=(const ImageKeyData&) = delete;
+
+ void MergeDirtyRect(const Maybe<gfx::IntRect>& aDirtyRect);
+
+ Maybe<gfx::IntRect> TakeDirtyRect() { return std::move(mDirtyRect); }
+
+ RefPtr<RenderRootStateManager> mManager;
+ Maybe<gfx::IntRect> mDirtyRect;
+ wr::ImageKey mImageKey;
+ };
+
+ private:
+ SharedSurfacesChild() = delete;
+ ~SharedSurfacesChild() = delete;
+
+ friend class SharedSurfacesAnimation;
+
+ class SharedUserData final : public Runnable {
+ public:
+ explicit SharedUserData(const wr::ExternalImageId& aId);
+ virtual ~SharedUserData();
+
+ SharedUserData(const SharedUserData& aOther) = delete;
+ SharedUserData& operator=(const SharedUserData& aOther) = delete;
+
+ SharedUserData(SharedUserData&& aOther) = delete;
+ SharedUserData& operator=(SharedUserData&& aOther) = delete;
+
+ static void Destroy(void* aClosure);
+
+ NS_IMETHOD Run() override;
+
+ const wr::ExternalImageId& Id() const { return mId; }
+
+ void SetId(const wr::ExternalImageId& aId) {
+ mId = aId;
+ mKeys.Clear();
+ mShared = false;
+ }
+
+ bool IsShared() const { return mShared; }
+
+ void MarkShared() {
+ MOZ_ASSERT(!mShared);
+ mShared = true;
+ }
+
+ wr::ImageKey UpdateKey(RenderRootStateManager* aManager,
+ wr::IpcResourceUpdateQueue& aResources,
+ const Maybe<gfx::IntRect>& aDirtyRect);
+
+ protected:
+ AutoTArray<ImageKeyData, 1> mKeys;
+ wr::ExternalImageId mId;
+ bool mShared : 1;
+ };
+
+ static nsresult ShareInternal(gfx::SourceSurfaceSharedData* aSurface,
+ SharedUserData** aUserData);
+
+ static void Unshare(const wr::ExternalImageId& aId, bool aReleaseId,
+ nsTArray<ImageKeyData>& aKeys);
+
+ static void DestroySharedUserData(void* aClosure);
+
+ static gfx::UserDataKey sSharedKey;
+};
+
+class AnimationImageKeyData final : public SharedSurfacesChild::ImageKeyData {
+ public:
+ AnimationImageKeyData(RenderRootStateManager* aManager,
+ const wr::ImageKey& aImageKey);
+
+ virtual ~AnimationImageKeyData();
+
+ AnimationImageKeyData(AnimationImageKeyData&& aOther);
+ AnimationImageKeyData& operator=(AnimationImageKeyData&& aOther);
+
+ AutoTArray<RefPtr<gfx::SourceSurfaceSharedData>, 2> mPendingRelease;
+};
+
+/**
+ * This helper class owns a single ImageKey which will map to different external
+ * image IDs representing different frames in an animation.
+ */
+class SharedSurfacesAnimation final {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(SharedSurfacesAnimation)
+
+ SharedSurfacesAnimation() = default;
+
+ void Destroy();
+
+ /**
+ * Set the animation to display the given frame.
+ * @param aSurface The current frame.
+ * @param aDirtyRect Dirty rect representing the change between the new frame
+ * and the previous frame. We will request only the delta
+ * be reuploaded by WebRender.
+ */
+ nsresult SetCurrentFrame(gfx::SourceSurfaceSharedData* aSurface,
+ const gfx::IntRect& aDirtyRect);
+
+ /**
+ * Generate an ImageKey for the given frame.
+ * @param aSurface The current frame. This should match what was cached via
+ * SetCurrentFrame, but if it does not, it will need to
+ * regenerate the cached ImageKey.
+ */
+ nsresult UpdateKey(gfx::SourceSurfaceSharedData* aSurface,
+ RenderRootStateManager* aManager,
+ wr::IpcResourceUpdateQueue& aResources,
+ wr::ImageKey& aKey);
+
+ /**
+ * Release our reference to all frames up to and including the frame which
+ * has an external image ID which matches aId.
+ */
+ void ReleasePreviousFrame(RenderRootStateManager* aManager,
+ const wr::ExternalImageId& aId);
+
+ /**
+ * Destroy any state information bound for the given layer manager. Any
+ * image keys are already invalid.
+ */
+ void Invalidate(RenderRootStateManager* aManager);
+
+ private:
+ ~SharedSurfacesAnimation();
+
+ void HoldSurfaceForRecycling(AnimationImageKeyData& aEntry,
+ gfx::SourceSurfaceSharedData* aSurface);
+
+ AutoTArray<AnimationImageKeyData, 1> mKeys;
+ wr::ExternalImageId mId;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif
diff --git a/gfx/layers/ipc/SharedSurfacesMemoryReport.h b/gfx/layers/ipc/SharedSurfacesMemoryReport.h
new file mode 100644
index 0000000000..81baf1349f
--- /dev/null
+++ b/gfx/layers/ipc/SharedSurfacesMemoryReport.h
@@ -0,0 +1,59 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_GFX_SHAREDSURFACESMEMORYREPORT_H
+#define MOZILLA_GFX_SHAREDSURFACESMEMORYREPORT_H
+
+#include <cstdint> // for uint32_t
+#include <unordered_map>
+#include "base/process.h"
+#include "ipc/IPCMessageUtils.h"
+#include "ipc/IPCMessageUtilsSpecializations.h"
+#include "mozilla/gfx/Point.h" // for IntSize
+
+namespace mozilla {
+namespace layers {
+
+class SharedSurfacesMemoryReport final {
+ public:
+ class SurfaceEntry final {
+ public:
+ base::ProcessId mCreatorPid;
+ gfx::IntSize mSize;
+ int32_t mStride;
+ uint32_t mConsumers;
+ bool mCreatorRef;
+ };
+
+ std::unordered_map<uint64_t, SurfaceEntry> mSurfaces;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+namespace IPC {
+
+template <>
+struct ParamTraits<mozilla::layers::SharedSurfacesMemoryReport> {
+ typedef mozilla::layers::SharedSurfacesMemoryReport paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.mSurfaces);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ return ReadParam(aReader, &aResult->mSurfaces);
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::layers::SharedSurfacesMemoryReport::SurfaceEntry>
+ : public PlainOldDataSerializer<
+ mozilla::layers::SharedSurfacesMemoryReport::SurfaceEntry> {};
+
+} // namespace IPC
+
+#endif
diff --git a/gfx/layers/ipc/SharedSurfacesParent.cpp b/gfx/layers/ipc/SharedSurfacesParent.cpp
new file mode 100644
index 0000000000..73d985db4c
--- /dev/null
+++ b/gfx/layers/ipc/SharedSurfacesParent.cpp
@@ -0,0 +1,385 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "SharedSurfacesParent.h"
+#include "mozilla/DebugOnly.h"
+#include "mozilla/StaticPrefs_image.h"
+#include "mozilla/gfx/Logging.h"
+#include "mozilla/gfx/gfxVars.h"
+#include "mozilla/gfx/GPUProcessManager.h"
+#include "mozilla/layers/SharedSurfacesMemoryReport.h"
+#include "mozilla/layers/SourceSurfaceSharedData.h"
+#include "mozilla/layers/CompositorThread.h"
+#include "mozilla/webrender/RenderSharedSurfaceTextureHost.h"
+#include "mozilla/webrender/RenderThread.h"
+#include "nsThreadUtils.h" // for GetCurrentSerialEventTarget
+
+namespace mozilla {
+namespace layers {
+
+using namespace mozilla::gfx;
+
+StaticMonitor SharedSurfacesParent::sMonitor;
+StaticAutoPtr<SharedSurfacesParent> SharedSurfacesParent::sInstance;
+
+// Short wait to allow for a surface to be added, where the consumer has a
+// different thread route.
+static const TimeDuration kGetTimeout = TimeDuration::FromMilliseconds(50);
+
+void SharedSurfacesParent::MappingTracker::NotifyExpiredLocked(
+ SourceSurfaceSharedDataWrapper* aSurface,
+ const StaticMonitorAutoLock& aAutoLock) {
+ RemoveObjectLocked(aSurface, aAutoLock);
+ mExpired.AppendElement(aSurface);
+}
+
+void SharedSurfacesParent::MappingTracker::TakeExpired(
+ nsTArray<RefPtr<gfx::SourceSurfaceSharedDataWrapper>>& aExpired,
+ const StaticMonitorAutoLock& aAutoLock) {
+ aExpired = std::move(mExpired);
+}
+
+void SharedSurfacesParent::MappingTracker::NotifyHandlerEnd() {
+ nsTArray<RefPtr<gfx::SourceSurfaceSharedDataWrapper>> expired;
+ {
+ StaticMonitorAutoLock lock(sMonitor);
+ TakeExpired(expired, lock);
+ }
+
+ SharedSurfacesParent::ExpireMap(expired);
+}
+
+SharedSurfacesParent::SharedSurfacesParent()
+ : mTracker(
+ StaticPrefs::image_mem_shared_unmap_min_expiration_ms_AtStartup(),
+ mozilla::GetCurrentSerialEventTarget()) {}
+
+/* static */
+void SharedSurfacesParent::Initialize() {
+ MOZ_ASSERT(NS_IsMainThread());
+ StaticMonitorAutoLock lock(sMonitor);
+ if (!sInstance) {
+ sInstance = new SharedSurfacesParent();
+ }
+}
+
+/* static */
+void SharedSurfacesParent::ShutdownRenderThread() {
+ // The main thread should blocked on waiting for the render thread to
+ // complete so this should be safe to release off the main thread.
+ MOZ_ASSERT(wr::RenderThread::IsInRenderThread());
+ StaticMonitorAutoLock lock(sMonitor);
+ MOZ_ASSERT(sInstance);
+
+ for (const auto& key : sInstance->mSurfaces.Keys()) {
+ // There may be lingering consumers of the surfaces that didn't get shutdown
+ // yet but since we are here, we know the render thread is finished and we
+ // can unregister everything.
+ wr::RenderThread::Get()->UnregisterExternalImageDuringShutdown(
+ wr::ToExternalImageId(key));
+ }
+}
+
+/* static */
+void SharedSurfacesParent::Shutdown() {
+ // The compositor thread and render threads are shutdown, so this is the last
+ // thread that could use it. The expiration tracker needs to be freed on the
+ // main thread.
+ MOZ_ASSERT(NS_IsMainThread());
+ StaticMonitorAutoLock lock(sMonitor);
+ sInstance = nullptr;
+}
+
+/* static */
+already_AddRefed<DataSourceSurface> SharedSurfacesParent::Get(
+ const wr::ExternalImageId& aId) {
+ StaticMonitorAutoLock lock(sMonitor);
+ if (!sInstance) {
+ gfxCriticalNote << "SSP:Get " << wr::AsUint64(aId) << " shtd";
+ return nullptr;
+ }
+
+ RefPtr<SourceSurfaceSharedDataWrapper> surface;
+ while (
+ !sInstance->mSurfaces.Get(wr::AsUint64(aId), getter_AddRefs(surface))) {
+ CVStatus status = lock.Wait(kGetTimeout);
+ if (status == CVStatus::Timeout) {
+ return nullptr;
+ }
+ }
+ return surface.forget();
+}
+
+/* static */
+already_AddRefed<DataSourceSurface> SharedSurfacesParent::Acquire(
+ const wr::ExternalImageId& aId) {
+ StaticMonitorAutoLock lock(sMonitor);
+ if (!sInstance) {
+ gfxCriticalNote << "SSP:Acq " << wr::AsUint64(aId) << " shtd";
+ return nullptr;
+ }
+
+ RefPtr<SourceSurfaceSharedDataWrapper> surface;
+ sInstance->mSurfaces.Get(wr::AsUint64(aId), getter_AddRefs(surface));
+
+ if (surface) {
+ DebugOnly<bool> rv = surface->AddConsumer();
+ MOZ_ASSERT(!rv);
+ }
+ return surface.forget();
+}
+
+/* static */
+bool SharedSurfacesParent::Release(const wr::ExternalImageId& aId,
+ bool aForCreator) {
+ StaticMonitorAutoLock lock(sMonitor);
+ if (!sInstance) {
+ return false;
+ }
+
+ uint64_t id = wr::AsUint64(aId);
+ RefPtr<SourceSurfaceSharedDataWrapper> surface;
+ sInstance->mSurfaces.Get(wr::AsUint64(aId), getter_AddRefs(surface));
+ if (!surface) {
+ return false;
+ }
+
+ if (surface->RemoveConsumer(aForCreator)) {
+ RemoveTrackingLocked(surface, lock);
+ wr::RenderThread::Get()->UnregisterExternalImage(wr::ToExternalImageId(id));
+ sInstance->mSurfaces.Remove(id);
+ }
+
+ return true;
+}
+
+/* static */
+void SharedSurfacesParent::AddSameProcess(const wr::ExternalImageId& aId,
+ SourceSurfaceSharedData* aSurface) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ MOZ_ASSERT(NS_IsMainThread());
+ StaticMonitorAutoLock lock(sMonitor);
+ if (!sInstance) {
+ gfxCriticalNote << "SSP:Ads " << wr::AsUint64(aId) << " shtd";
+ return;
+ }
+
+ // If the child bridge detects it is in the combined UI/GPU process, then it
+ // will insert a wrapper surface holding the shared memory buffer directly.
+ // This is good because we avoid mapping the same shared memory twice, but
+ // still allow the original surface to be freed and remove the wrapper from
+ // the table when it is no longer needed.
+ RefPtr<SourceSurfaceSharedDataWrapper> surface =
+ new SourceSurfaceSharedDataWrapper();
+ surface->Init(aSurface);
+
+ uint64_t id = wr::AsUint64(aId);
+ MOZ_ASSERT(!sInstance->mSurfaces.Contains(id));
+
+ auto texture = MakeRefPtr<wr::RenderSharedSurfaceTextureHost>(surface);
+ wr::RenderThread::Get()->RegisterExternalImage(aId, texture.forget());
+
+ surface->AddConsumer();
+ sInstance->mSurfaces.InsertOrUpdate(id, std::move(surface));
+ lock.NotifyAll();
+}
+
+/* static */
+void SharedSurfacesParent::DestroyProcess(base::ProcessId aPid) {
+ StaticMonitorAutoLock lock(sMonitor);
+ if (!sInstance) {
+ return;
+ }
+
+ // Note that the destruction of a parent may not be cheap if it still has a
+ // lot of surfaces still bound that require unmapping.
+ for (auto i = sInstance->mSurfaces.Iter(); !i.Done(); i.Next()) {
+ SourceSurfaceSharedDataWrapper* surface = i.Data();
+ if (surface->GetCreatorPid() == aPid && surface->HasCreatorRef() &&
+ surface->RemoveConsumer(/* aForCreator */ true)) {
+ RemoveTrackingLocked(surface, lock);
+ wr::RenderThread::Get()->UnregisterExternalImage(
+ wr::ToExternalImageId(i.Key()));
+ i.Remove();
+ }
+ }
+}
+
+/* static */
+void SharedSurfacesParent::Add(const wr::ExternalImageId& aId,
+ SurfaceDescriptorShared&& aDesc,
+ base::ProcessId aPid) {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+ MOZ_ASSERT(aPid != base::GetCurrentProcId());
+
+ RefPtr<SourceSurfaceSharedDataWrapper> surface =
+ new SourceSurfaceSharedDataWrapper();
+
+ // We preferentially map in new surfaces when they are initially received
+ // because we are likely to reference them in a display list soon. The unmap
+ // will ensure we add the surface to the expiration tracker. We do it outside
+ // the mutex to ensure we always lock the surface mutex first, and our mutex
+ // second, to avoid deadlock.
+ //
+ // Note that the surface wrapper maps in the given handle as read only.
+ surface->Init(aDesc.size(), aDesc.stride(), aDesc.format(),
+ std::move(aDesc.handle()), aPid);
+
+ StaticMonitorAutoLock lock(sMonitor);
+ if (!sInstance) {
+ gfxCriticalNote << "SSP:Add " << wr::AsUint64(aId) << " shtd";
+ return;
+ }
+
+ uint64_t id = wr::AsUint64(aId);
+ MOZ_ASSERT(!sInstance->mSurfaces.Contains(id));
+
+ auto texture = MakeRefPtr<wr::RenderSharedSurfaceTextureHost>(surface);
+ wr::RenderThread::Get()->RegisterExternalImage(aId, texture.forget());
+
+ surface->AddConsumer();
+ sInstance->mSurfaces.InsertOrUpdate(id, std::move(surface));
+ lock.NotifyAll();
+}
+
+/* static */
+void SharedSurfacesParent::Remove(const wr::ExternalImageId& aId) {
+ DebugOnly<bool> rv = Release(aId, /* aForCreator */ true);
+ MOZ_ASSERT(rv);
+}
+
+/* static */
+void SharedSurfacesParent::AddTrackingLocked(
+ SourceSurfaceSharedDataWrapper* aSurface,
+ const StaticMonitorAutoLock& aAutoLock) {
+ MOZ_ASSERT(!aSurface->GetExpirationState()->IsTracked());
+ sInstance->mTracker.AddObjectLocked(aSurface, aAutoLock);
+}
+
+/* static */
+void SharedSurfacesParent::AddTracking(
+ SourceSurfaceSharedDataWrapper* aSurface) {
+ StaticMonitorAutoLock lock(sMonitor);
+ if (!sInstance) {
+ return;
+ }
+
+ AddTrackingLocked(aSurface, lock);
+}
+
+/* static */
+void SharedSurfacesParent::RemoveTrackingLocked(
+ SourceSurfaceSharedDataWrapper* aSurface,
+ const StaticMonitorAutoLock& aAutoLock) {
+ if (!aSurface->GetExpirationState()->IsTracked()) {
+ return;
+ }
+
+ sInstance->mTracker.RemoveObjectLocked(aSurface, aAutoLock);
+}
+
+/* static */
+void SharedSurfacesParent::RemoveTracking(
+ SourceSurfaceSharedDataWrapper* aSurface) {
+ StaticMonitorAutoLock lock(sMonitor);
+ if (!sInstance) {
+ return;
+ }
+
+ RemoveTrackingLocked(aSurface, lock);
+}
+
+/* static */
+bool SharedSurfacesParent::AgeOneGenerationLocked(
+ nsTArray<RefPtr<SourceSurfaceSharedDataWrapper>>& aExpired,
+ const StaticMonitorAutoLock& aAutoLock) {
+ if (sInstance->mTracker.IsEmptyLocked(aAutoLock)) {
+ return false;
+ }
+
+ sInstance->mTracker.AgeOneGenerationLocked(aAutoLock);
+ sInstance->mTracker.TakeExpired(aExpired, aAutoLock);
+ return true;
+}
+
+/* static */
+bool SharedSurfacesParent::AgeOneGeneration(
+ nsTArray<RefPtr<SourceSurfaceSharedDataWrapper>>& aExpired) {
+ StaticMonitorAutoLock lock(sMonitor);
+ if (!sInstance) {
+ return false;
+ }
+
+ return AgeOneGenerationLocked(aExpired, lock);
+}
+
+/* static */
+bool SharedSurfacesParent::AgeAndExpireOneGeneration() {
+ nsTArray<RefPtr<SourceSurfaceSharedDataWrapper>> expired;
+ bool aged = AgeOneGeneration(expired);
+ ExpireMap(expired);
+ return aged;
+}
+
+/* static */
+void SharedSurfacesParent::ExpireMap(
+ nsTArray<RefPtr<SourceSurfaceSharedDataWrapper>>& aExpired) {
+ for (auto& surface : aExpired) {
+ surface->ExpireMap();
+ }
+}
+
+/* static */
+void SharedSurfacesParent::AccumulateMemoryReport(
+ base::ProcessId aPid, SharedSurfacesMemoryReport& aReport) {
+ StaticMonitorAutoLock lock(sMonitor);
+ if (!sInstance) {
+ return;
+ }
+
+ for (const auto& entry : sInstance->mSurfaces) {
+ SourceSurfaceSharedDataWrapper* surface = entry.GetData();
+ if (surface->GetCreatorPid() == aPid) {
+ aReport.mSurfaces.insert(std::make_pair(
+ entry.GetKey(),
+ SharedSurfacesMemoryReport::SurfaceEntry{
+ aPid, surface->GetSize(), surface->Stride(),
+ surface->GetConsumers(), surface->HasCreatorRef()}));
+ }
+ }
+}
+
+/* static */
+bool SharedSurfacesParent::AccumulateMemoryReport(
+ SharedSurfacesMemoryReport& aReport) {
+ if (XRE_IsParentProcess()) {
+ GPUProcessManager* gpm = GPUProcessManager::Get();
+ if (!gpm || gpm->GPUProcessPid() != base::kInvalidProcessId) {
+ return false;
+ }
+ } else if (!XRE_IsGPUProcess()) {
+ return false;
+ }
+
+ StaticMonitorAutoLock lock(sMonitor);
+ if (!sInstance) {
+ return true;
+ }
+
+ for (const auto& entry : sInstance->mSurfaces) {
+ SourceSurfaceSharedDataWrapper* surface = entry.GetData();
+ aReport.mSurfaces.insert(std::make_pair(
+ entry.GetKey(),
+ SharedSurfacesMemoryReport::SurfaceEntry{
+ surface->GetCreatorPid(), surface->GetSize(), surface->Stride(),
+ surface->GetConsumers(), surface->HasCreatorRef()}));
+ }
+
+ return true;
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/ipc/SharedSurfacesParent.h b/gfx/layers/ipc/SharedSurfacesParent.h
new file mode 100644
index 0000000000..1ccc99b2f0
--- /dev/null
+++ b/gfx/layers/ipc/SharedSurfacesParent.h
@@ -0,0 +1,135 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_GFX_SHAREDSURFACESPARENT_H
+#define MOZILLA_GFX_SHAREDSURFACESPARENT_H
+
+#include <stdint.h> // for uint32_t
+#include "mozilla/Attributes.h" // for override
+#include "mozilla/StaticMonitor.h" // for StaticMutex
+#include "mozilla/StaticPtr.h" // for StaticAutoPtr
+#include "mozilla/RefPtr.h" // for already_AddRefed
+#include "mozilla/ipc/SharedMemory.h" // for SharedMemory, etc
+#include "mozilla/gfx/2D.h" // for SurfaceFormat
+#include "mozilla/gfx/Point.h" // for IntSize
+#include "mozilla/layers/LayersSurfaces.h" // for SurfaceDescriptorShared
+#include "mozilla/layers/SourceSurfaceSharedData.h"
+#include "mozilla/webrender/WebRenderTypes.h" // for wr::ExternalImageId
+#include "nsExpirationTracker.h"
+#include "nsRefPtrHashtable.h"
+
+namespace mozilla {
+namespace gfx {
+class DataSourceSurface;
+} // namespace gfx
+
+namespace layers {
+
+class SharedSurfacesChild;
+class SharedSurfacesMemoryReport;
+
+class SharedSurfacesParent final {
+ public:
+ static void Initialize();
+ static void ShutdownRenderThread();
+ static void Shutdown();
+
+ // Get without increasing the consumer count.
+ static already_AddRefed<gfx::DataSourceSurface> Get(
+ const wr::ExternalImageId& aId);
+
+ // Get but also increase the consumer count. Must call Release after finished.
+ static already_AddRefed<gfx::DataSourceSurface> Acquire(
+ const wr::ExternalImageId& aId);
+
+ static bool Release(const wr::ExternalImageId& aId, bool aForCreator = false);
+
+ static void Add(const wr::ExternalImageId& aId,
+ SurfaceDescriptorShared&& aDesc, base::ProcessId aPid);
+
+ static void Remove(const wr::ExternalImageId& aId);
+
+ static void DestroyProcess(base::ProcessId aPid);
+
+ static void AccumulateMemoryReport(base::ProcessId aPid,
+ SharedSurfacesMemoryReport& aReport);
+
+ static bool AccumulateMemoryReport(SharedSurfacesMemoryReport& aReport);
+
+ static void AddTracking(gfx::SourceSurfaceSharedDataWrapper* aSurface);
+
+ static void RemoveTracking(gfx::SourceSurfaceSharedDataWrapper* aSurface);
+
+ static bool AgeOneGeneration(
+ nsTArray<RefPtr<gfx::SourceSurfaceSharedDataWrapper>>& aExpired);
+
+ static bool AgeAndExpireOneGeneration();
+
+ private:
+ friend class SharedSurfacesChild;
+ friend class gfx::SourceSurfaceSharedDataWrapper;
+
+ SharedSurfacesParent();
+
+ static void AddSameProcess(const wr::ExternalImageId& aId,
+ gfx::SourceSurfaceSharedData* aSurface);
+
+ static void AddTrackingLocked(gfx::SourceSurfaceSharedDataWrapper* aSurface,
+ const StaticMonitorAutoLock& aAutoLock);
+
+ static void RemoveTrackingLocked(
+ gfx::SourceSurfaceSharedDataWrapper* aSurface,
+ const StaticMonitorAutoLock& aAutoLock);
+
+ static bool AgeOneGenerationLocked(
+ nsTArray<RefPtr<gfx::SourceSurfaceSharedDataWrapper>>& aExpired,
+ const StaticMonitorAutoLock& aAutoLock);
+
+ static void ExpireMap(
+ nsTArray<RefPtr<gfx::SourceSurfaceSharedDataWrapper>>& aExpired);
+
+ static StaticMonitor sMonitor MOZ_UNANNOTATED;
+
+ static StaticAutoPtr<SharedSurfacesParent> sInstance;
+
+ nsRefPtrHashtable<nsUint64HashKey, gfx::SourceSurfaceSharedDataWrapper>
+ mSurfaces;
+
+ class MappingTracker final
+ : public ExpirationTrackerImpl<gfx::SourceSurfaceSharedDataWrapper, 4,
+ StaticMonitor, StaticMonitorAutoLock> {
+ public:
+ explicit MappingTracker(uint32_t aExpirationTimeoutMS,
+ nsIEventTarget* aEventTarget)
+ : ExpirationTrackerImpl<gfx::SourceSurfaceSharedDataWrapper, 4,
+ StaticMonitor, StaticMonitorAutoLock>(
+ aExpirationTimeoutMS, "SharedMappingTracker", aEventTarget) {}
+
+ void TakeExpired(
+ nsTArray<RefPtr<gfx::SourceSurfaceSharedDataWrapper>>& aExpired,
+ const StaticMonitorAutoLock& aAutoLock);
+
+ protected:
+ void NotifyExpiredLocked(gfx::SourceSurfaceSharedDataWrapper* aSurface,
+ const StaticMonitorAutoLock& aAutoLock) override;
+
+ void NotifyHandlerEndLocked(
+ const StaticMonitorAutoLock& aAutoLock) override {}
+
+ void NotifyHandlerEnd() override;
+
+ StaticMonitor& GetMutex() override { return sMonitor; }
+
+ nsTArray<RefPtr<gfx::SourceSurfaceSharedDataWrapper>> mExpired;
+ };
+
+ MappingTracker mTracker;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif
diff --git a/gfx/layers/ipc/SurfaceDescriptor.h b/gfx/layers/ipc/SurfaceDescriptor.h
new file mode 100644
index 0000000000..6d4c2d3271
--- /dev/null
+++ b/gfx/layers/ipc/SurfaceDescriptor.h
@@ -0,0 +1,14 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef IPC_SurfaceDescriptor_h
+#define IPC_SurfaceDescriptor_h
+
+#if defined(XP_MACOSX)
+# define MOZ_HAVE_PLATFORM_SPECIFIC_LAYER_BUFFERS
+#endif
+
+#endif // IPC_SurfaceDescriptor_h
diff --git a/gfx/layers/ipc/SynchronousTask.h b/gfx/layers/ipc/SynchronousTask.h
new file mode 100644
index 0000000000..06f14caafc
--- /dev/null
+++ b/gfx/layers/ipc/SynchronousTask.h
@@ -0,0 +1,73 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_GFX_SYNCHRONOUSTASK_H
+#define MOZILLA_GFX_SYNCHRONOUSTASK_H
+
+#include "mozilla/ReentrantMonitor.h" // for ReentrantMonitor, etc
+
+namespace mozilla {
+namespace layers {
+
+// Helper that creates a monitor and a "done" flag, then enters the monitor.
+class MOZ_STACK_CLASS SynchronousTask {
+ friend class AutoCompleteTask;
+
+ public:
+ explicit SynchronousTask(const char* name)
+ : mMonitor(name), mAutoEnter(mMonitor), mDone(false) {}
+
+ nsresult Wait(PRIntervalTime aInterval = PR_INTERVAL_NO_TIMEOUT) {
+ // For indefinite timeouts, wait in a while loop to handle spurious
+ // wakeups.
+ while (aInterval == PR_INTERVAL_NO_TIMEOUT && !mDone) {
+ mMonitor.Wait();
+ }
+
+ // For finite timeouts, we only check once for completion, and otherwise
+ // rely on the ReentrantMonitor to manage the interval. If the monitor
+ // returns too early, we'll never know, but we can check if the mDone
+ // flag was set to true, indicating that the task finished successfully.
+ if (!mDone) {
+ // We ignore the return value from ReentrantMonitor::Wait, because it's
+ // always NS_OK, even in the case of timeout.
+ mMonitor.Wait(aInterval);
+
+ if (!mDone) {
+ return NS_ERROR_ABORT;
+ }
+ }
+
+ return NS_OK;
+ }
+
+ private:
+ void Complete() {
+ mDone = true;
+ mMonitor.NotifyAll();
+ }
+
+ private:
+ ReentrantMonitor mMonitor MOZ_UNANNOTATED;
+ ReentrantMonitorAutoEnter mAutoEnter;
+ bool mDone;
+};
+
+class MOZ_STACK_CLASS AutoCompleteTask final {
+ public:
+ explicit AutoCompleteTask(SynchronousTask* aTask)
+ : mTask(aTask), mAutoEnter(aTask->mMonitor) {}
+ ~AutoCompleteTask() { mTask->Complete(); }
+
+ private:
+ SynchronousTask* mTask;
+ ReentrantMonitorAutoEnter mAutoEnter;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif
diff --git a/gfx/layers/ipc/TextureForwarder.h b/gfx/layers/ipc/TextureForwarder.h
new file mode 100644
index 0000000000..af84390143
--- /dev/null
+++ b/gfx/layers/ipc/TextureForwarder.h
@@ -0,0 +1,92 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_LAYERS_TEXTUREFORWARDER
+#define MOZILLA_LAYERS_TEXTUREFORWARDER
+
+#include <stdint.h> // for int32_t, uint64_t
+#include "gfxTypes.h"
+#include "mozilla/dom/ipc/IdType.h"
+#include "mozilla/ipc/ProtocolUtils.h"
+#include "mozilla/layers/LayersMessages.h" // for Edit, etc
+#include "mozilla/layers/LayersTypes.h" // for LayersBackend
+#include "mozilla/layers/TextureClient.h" // for TextureClient
+#include "mozilla/layers/KnowsCompositor.h"
+#include "nsISerialEventTarget.h"
+
+namespace mozilla {
+namespace layers {
+class CanvasChild;
+
+/**
+ * An abstract interface for classes that implement the autogenerated
+ * IPDL actor class. Lets us check if they are still valid for IPC.
+ */
+class LayersIPCActor {
+ public:
+ virtual bool IPCOpen() const { return true; }
+};
+
+/**
+ * An abstract interface for LayersIPCActors that implement a top-level
+ * IPDL protocol so also have their own channel.
+ * Has their own MessageLoop for message dispatch, and can allocate
+ * shmem.
+ */
+class LayersIPCChannel : public LayersIPCActor,
+ public mozilla::ipc::IShmemAllocator {
+ public:
+ NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING
+
+ virtual bool IsSameProcess() const = 0;
+
+ virtual bool UsesImageBridge() const { return false; }
+
+ virtual base::ProcessId GetParentPid() const = 0;
+
+ virtual nsISerialEventTarget* GetThread() const = 0;
+
+ virtual FixedSizeSmallShmemSectionAllocator* GetTileLockAllocator() {
+ return nullptr;
+ }
+
+ virtual void CancelWaitForNotifyNotUsed(uint64_t aTextureId) = 0;
+
+ virtual wr::MaybeExternalImageId GetNextExternalImageId() {
+ return Nothing();
+ }
+
+ protected:
+ virtual ~LayersIPCChannel() = default;
+};
+
+/**
+ * An abstract interface for classes that can allocate PTexture objects
+ * across IPDL. Currently a sub-class of LayersIPCChannel for simplicity
+ * since all our implementations use both, but could be independant if needed.
+ */
+class TextureForwarder : public LayersIPCChannel {
+ public:
+ /**
+ * Create a TextureChild/Parent pair as as well as the TextureHost on the
+ * parent side.
+ */
+ virtual PTextureChild* CreateTexture(
+ const SurfaceDescriptor& aSharedData, ReadLockDescriptor&& aReadLock,
+ LayersBackend aLayersBackend, TextureFlags aFlags,
+ const dom::ContentParentId& aContentId, uint64_t aSerial,
+ wr::MaybeExternalImageId& aExternalImageId) = 0;
+
+ /**
+ * Returns the CanvasChild for this TextureForwarder.
+ */
+ virtual already_AddRefed<CanvasChild> GetCanvasChild() { return nullptr; };
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif
diff --git a/gfx/layers/ipc/UiCompositorControllerChild.cpp b/gfx/layers/ipc/UiCompositorControllerChild.cpp
new file mode 100644
index 0000000000..c1bf49bc27
--- /dev/null
+++ b/gfx/layers/ipc/UiCompositorControllerChild.cpp
@@ -0,0 +1,350 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/layers/UiCompositorControllerChild.h"
+
+#include "mozilla/dom/ContentChild.h"
+#include "mozilla/layers/CompositorThread.h"
+#include "mozilla/layers/SynchronousTask.h"
+#include "mozilla/layers/UiCompositorControllerMessageTypes.h"
+#include "mozilla/layers/UiCompositorControllerParent.h"
+#include "mozilla/gfx/GPUProcessManager.h"
+#include "mozilla/ipc/Endpoint.h"
+#include "mozilla/StaticPtr.h"
+#include "nsBaseWidget.h"
+#include "nsProxyRelease.h"
+#include "nsThreadUtils.h"
+
+#if defined(MOZ_WIDGET_ANDROID)
+# include "mozilla/widget/AndroidUiThread.h"
+
+static RefPtr<nsThread> GetUiThread() { return mozilla::GetAndroidUiThread(); }
+#else
+static RefPtr<nsThread> GetUiThread() {
+ MOZ_CRASH("Platform does not support UiCompositorController");
+ return nullptr;
+}
+#endif // defined(MOZ_WIDGET_ANDROID)
+
+namespace mozilla {
+namespace layers {
+
+// public:
+/* static */
+RefPtr<UiCompositorControllerChild>
+UiCompositorControllerChild::CreateForSameProcess(
+ const LayersId& aRootLayerTreeId, nsBaseWidget* aWidget) {
+ RefPtr<UiCompositorControllerChild> child =
+ new UiCompositorControllerChild(0, aWidget);
+ child->mParent = new UiCompositorControllerParent(aRootLayerTreeId);
+ GetUiThread()->Dispatch(
+ NewRunnableMethod(
+ "layers::UiCompositorControllerChild::OpenForSameProcess", child,
+ &UiCompositorControllerChild::OpenForSameProcess),
+ nsIThread::DISPATCH_NORMAL);
+ return child;
+}
+
+/* static */
+RefPtr<UiCompositorControllerChild>
+UiCompositorControllerChild::CreateForGPUProcess(
+ const uint64_t& aProcessToken,
+ Endpoint<PUiCompositorControllerChild>&& aEndpoint, nsBaseWidget* aWidget) {
+ RefPtr<UiCompositorControllerChild> child =
+ new UiCompositorControllerChild(aProcessToken, aWidget);
+
+ RefPtr<nsIRunnable> task =
+ NewRunnableMethod<Endpoint<PUiCompositorControllerChild>&&>(
+ "layers::UiCompositorControllerChild::OpenForGPUProcess", child,
+ &UiCompositorControllerChild::OpenForGPUProcess,
+ std::move(aEndpoint));
+
+ GetUiThread()->Dispatch(task.forget(), nsIThread::DISPATCH_NORMAL);
+ return child;
+}
+
+bool UiCompositorControllerChild::Pause() {
+ if (!mIsOpen) {
+ return false;
+ }
+ return SendPause();
+}
+
+bool UiCompositorControllerChild::Resume() {
+ if (!mIsOpen) {
+ return false;
+ }
+ bool resumed = false;
+ return SendResume(&resumed) && resumed;
+}
+
+bool UiCompositorControllerChild::ResumeAndResize(const int32_t& aX,
+ const int32_t& aY,
+ const int32_t& aWidth,
+ const int32_t& aHeight) {
+ if (!mIsOpen) {
+ mResize = Some(gfx::IntRect(aX, aY, aWidth, aHeight));
+ // Since we are caching these values, pretend the call succeeded.
+ return true;
+ }
+ bool resumed = false;
+ return SendResumeAndResize(aX, aY, aWidth, aHeight, &resumed) && resumed;
+}
+
+bool UiCompositorControllerChild::InvalidateAndRender() {
+ if (!mIsOpen) {
+ return false;
+ }
+ return SendInvalidateAndRender();
+}
+
+bool UiCompositorControllerChild::SetMaxToolbarHeight(const int32_t& aHeight) {
+ if (!mIsOpen) {
+ mMaxToolbarHeight = Some(aHeight);
+ // Since we are caching this value, pretend the call succeeded.
+ return true;
+ }
+ return SendMaxToolbarHeight(aHeight);
+}
+
+bool UiCompositorControllerChild::SetFixedBottomOffset(int32_t aOffset) {
+ return SendFixedBottomOffset(aOffset);
+}
+
+bool UiCompositorControllerChild::ToolbarAnimatorMessageFromUI(
+ const int32_t& aMessage) {
+ if (!mIsOpen) {
+ return false;
+ }
+
+ if (aMessage == IS_COMPOSITOR_CONTROLLER_OPEN) {
+ RecvToolbarAnimatorMessageFromCompositor(COMPOSITOR_CONTROLLER_OPEN);
+ }
+
+ return true;
+}
+
+bool UiCompositorControllerChild::SetDefaultClearColor(const uint32_t& aColor) {
+ if (!mIsOpen) {
+ mDefaultClearColor = Some(aColor);
+ // Since we are caching this value, pretend the call succeeded.
+ return true;
+ }
+
+ return SendDefaultClearColor(aColor);
+}
+
+bool UiCompositorControllerChild::RequestScreenPixels() {
+ if (!mIsOpen) {
+ return false;
+ }
+
+ return SendRequestScreenPixels();
+}
+
+bool UiCompositorControllerChild::EnableLayerUpdateNotifications(
+ const bool& aEnable) {
+ if (!mIsOpen) {
+ mLayerUpdateEnabled = Some(aEnable);
+ // Since we are caching this value, pretend the call succeeded.
+ return true;
+ }
+
+ return SendEnableLayerUpdateNotifications(aEnable);
+}
+
+void UiCompositorControllerChild::Destroy() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ layers::SynchronousTask task("UiCompositorControllerChild::Destroy");
+ GetUiThread()->Dispatch(NS_NewRunnableFunction(
+ "layers::UiCompositorControllerChild::Destroy", [&]() {
+ MOZ_ASSERT(GetUiThread()->IsOnCurrentThread());
+ AutoCompleteTask complete(&task);
+
+ // Clear the process token so that we don't notify the GPUProcessManager
+ // about an abnormal shutdown, thereby tearing down the GPU process.
+ mProcessToken = 0;
+
+ if (mWidget) {
+ // Dispatch mWidget to main thread to prevent it from being destructed
+ // by the ui thread.
+ RefPtr<nsIWidget> widget = std::move(mWidget);
+ NS_ReleaseOnMainThread("UiCompositorControllerChild::mWidget",
+ widget.forget());
+ }
+
+ if (mIsOpen) {
+ // Close the underlying IPC channel.
+ PUiCompositorControllerChild::Close();
+ mIsOpen = false;
+ }
+ }));
+
+ task.Wait();
+}
+
+bool UiCompositorControllerChild::DeallocPixelBuffer(Shmem& aMem) {
+ return DeallocShmem(aMem);
+}
+
+// protected:
+void UiCompositorControllerChild::ActorDestroy(ActorDestroyReason aWhy) {
+ mIsOpen = false;
+ mParent = nullptr;
+
+ if (mProcessToken) {
+ gfx::GPUProcessManager::Get()->NotifyRemoteActorDestroyed(mProcessToken);
+ mProcessToken = 0;
+ }
+}
+
+void UiCompositorControllerChild::ProcessingError(Result aCode,
+ const char* aReason) {
+ if (aCode != MsgDropped) {
+ gfxDevCrash(gfx::LogReason::ProcessingError)
+ << "Processing error in UiCompositorControllerChild: " << int(aCode);
+ }
+}
+
+void UiCompositorControllerChild::HandleFatalError(const char* aMsg) {
+ dom::ContentChild::FatalErrorIfNotUsingGPUProcess(aMsg, OtherPid());
+}
+
+mozilla::ipc::IPCResult
+UiCompositorControllerChild::RecvToolbarAnimatorMessageFromCompositor(
+ const int32_t& aMessage) {
+#if defined(MOZ_WIDGET_ANDROID)
+ if (mWidget) {
+ mWidget->RecvToolbarAnimatorMessageFromCompositor(aMessage);
+ }
+#endif // defined(MOZ_WIDGET_ANDROID)
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult UiCompositorControllerChild::RecvRootFrameMetrics(
+ const ScreenPoint& aScrollOffset, const CSSToScreenScale& aZoom) {
+#if defined(MOZ_WIDGET_ANDROID)
+ if (mWidget) {
+ mWidget->UpdateRootFrameMetrics(aScrollOffset, aZoom);
+ }
+#endif // defined(MOZ_WIDGET_ANDROID)
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult UiCompositorControllerChild::RecvScreenPixels(
+ ipc::Shmem&& aMem, const ScreenIntSize& aSize, bool aNeedsYFlip) {
+#if defined(MOZ_WIDGET_ANDROID)
+ if (mWidget) {
+ mWidget->RecvScreenPixels(std::move(aMem), aSize, aNeedsYFlip);
+ }
+#endif // defined(MOZ_WIDGET_ANDROID)
+
+ return IPC_OK();
+}
+
+// private:
+UiCompositorControllerChild::UiCompositorControllerChild(
+ const uint64_t& aProcessToken, nsBaseWidget* aWidget)
+ : mIsOpen(false), mProcessToken(aProcessToken), mWidget(aWidget) {}
+
+UiCompositorControllerChild::~UiCompositorControllerChild() = default;
+
+void UiCompositorControllerChild::OpenForSameProcess() {
+ MOZ_ASSERT(GetUiThread()->IsOnCurrentThread());
+
+ mIsOpen = Open(mParent, mozilla::layers::CompositorThread(),
+ mozilla::ipc::ChildSide);
+
+ if (!mIsOpen) {
+ mParent = nullptr;
+ return;
+ }
+
+ mParent->InitializeForSameProcess();
+ SendCachedValues();
+ // Let Ui thread know the connection is open;
+ RecvToolbarAnimatorMessageFromCompositor(COMPOSITOR_CONTROLLER_OPEN);
+}
+
+void UiCompositorControllerChild::OpenForGPUProcess(
+ Endpoint<PUiCompositorControllerChild>&& aEndpoint) {
+ MOZ_ASSERT(GetUiThread()->IsOnCurrentThread());
+
+ mIsOpen = aEndpoint.Bind(this);
+
+ if (!mIsOpen) {
+ // The GPU Process Manager might be gone if we receive ActorDestroy very
+ // late in shutdown.
+ if (gfx::GPUProcessManager* gpm = gfx::GPUProcessManager::Get()) {
+ gpm->NotifyRemoteActorDestroyed(mProcessToken);
+ }
+ return;
+ }
+
+ SendCachedValues();
+ // Let Ui thread know the connection is open;
+ RecvToolbarAnimatorMessageFromCompositor(COMPOSITOR_CONTROLLER_OPEN);
+}
+
+void UiCompositorControllerChild::SendCachedValues() {
+ MOZ_ASSERT(mIsOpen);
+ if (mResize) {
+ bool resumed;
+ SendResumeAndResize(mResize.ref().x, mResize.ref().y, mResize.ref().width,
+ mResize.ref().height, &resumed);
+ mResize.reset();
+ }
+ if (mMaxToolbarHeight) {
+ SendMaxToolbarHeight(mMaxToolbarHeight.ref());
+ mMaxToolbarHeight.reset();
+ }
+ if (mDefaultClearColor) {
+ SendDefaultClearColor(mDefaultClearColor.ref());
+ mDefaultClearColor.reset();
+ }
+ if (mLayerUpdateEnabled) {
+ SendEnableLayerUpdateNotifications(mLayerUpdateEnabled.ref());
+ mLayerUpdateEnabled.reset();
+ }
+}
+
+#ifdef MOZ_WIDGET_ANDROID
+void UiCompositorControllerChild::SetCompositorSurfaceManager(
+ java::CompositorSurfaceManager::Param aCompositorSurfaceManager) {
+ MOZ_ASSERT(!mCompositorSurfaceManager,
+ "SetCompositorSurfaceManager must only be called once.");
+ MOZ_ASSERT(mProcessToken != 0,
+ "SetCompositorSurfaceManager must only be called for GPU process "
+ "controllers.");
+ mCompositorSurfaceManager = aCompositorSurfaceManager;
+};
+
+void UiCompositorControllerChild::OnCompositorSurfaceChanged(
+ int32_t aWidgetId, java::sdk::Surface::Param aSurface) {
+ // If mCompositorSurfaceManager is not set then there is no GPU process and
+ // we do not need to do anything.
+ if (mCompositorSurfaceManager == nullptr) {
+ return;
+ }
+
+ nsresult result =
+ mCompositorSurfaceManager->OnSurfaceChanged(aWidgetId, aSurface);
+
+ // If our remote binder has died then notify the GPU process manager.
+ if (NS_FAILED(result)) {
+ if (mProcessToken) {
+ gfx::GPUProcessManager::Get()->NotifyRemoteActorDestroyed(mProcessToken);
+ mProcessToken = 0;
+ }
+ }
+}
+#endif
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/ipc/UiCompositorControllerChild.h b/gfx/layers/ipc/UiCompositorControllerChild.h
new file mode 100644
index 0000000000..bf543ee6a3
--- /dev/null
+++ b/gfx/layers/ipc/UiCompositorControllerChild.h
@@ -0,0 +1,120 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#ifndef include_gfx_ipc_UiCompositorControllerChild_h
+#define include_gfx_ipc_UiCompositorControllerChild_h
+
+#include "mozilla/layers/PUiCompositorControllerChild.h"
+
+#include "mozilla/gfx/2D.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/ipc/Shmem.h"
+#include "mozilla/layers/UiCompositorControllerParent.h"
+#include "mozilla/RefPtr.h"
+#include "nsThread.h"
+#ifdef MOZ_WIDGET_ANDROID
+# include "SurfaceTexture.h"
+# include "mozilla/java/CompositorSurfaceManagerWrappers.h"
+#endif
+
+class nsBaseWidget;
+
+namespace mozilla {
+namespace layers {
+
+class UiCompositorControllerChild final
+ : protected PUiCompositorControllerChild {
+ friend class PUiCompositorControllerChild;
+
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(UiCompositorControllerChild, final)
+
+ static RefPtr<UiCompositorControllerChild> CreateForSameProcess(
+ const LayersId& aRootLayerTreeId, nsBaseWidget* aWidget);
+ static RefPtr<UiCompositorControllerChild> CreateForGPUProcess(
+ const uint64_t& aProcessToken,
+ Endpoint<PUiCompositorControllerChild>&& aEndpoint,
+ nsBaseWidget* aWidget);
+
+ bool Pause();
+ bool Resume();
+ bool ResumeAndResize(const int32_t& aX, const int32_t& aY,
+ const int32_t& aHeight, const int32_t& aWidth);
+ bool InvalidateAndRender();
+ bool SetMaxToolbarHeight(const int32_t& aHeight);
+ bool SetFixedBottomOffset(int32_t aOffset);
+ bool ToolbarAnimatorMessageFromUI(const int32_t& aMessage);
+ bool SetDefaultClearColor(const uint32_t& aColor);
+ bool RequestScreenPixels();
+ bool EnableLayerUpdateNotifications(const bool& aEnable);
+
+ void Destroy();
+
+ bool DeallocPixelBuffer(Shmem& aMem);
+
+#ifdef MOZ_WIDGET_ANDROID
+ // Set mCompositorSurfaceManager. Must be called straight after initialization
+ // for GPU process controllers. Do not call for in-process controllers. This
+ // is separate from CreateForGPUProcess to avoid cluttering its declaration
+ // with JNI types.
+ void SetCompositorSurfaceManager(
+ java::CompositorSurfaceManager::Param aCompositorSurfaceManager);
+
+ // Send a Surface to the GPU process that a given widget ID should be
+ // composited in to. If not using a GPU process this function does nothing, as
+ // the InProcessCompositorWidget can read the Surface directly from the
+ // widget.
+ //
+ // Note that this function does not actually use the PUiCompositorController
+ // IPDL protocol, and instead uses Android's binder IPC mechanism via
+ // mCompositorSurfaceManager. It can be called from any thread.
+ void OnCompositorSurfaceChanged(int32_t aWidgetId,
+ java::sdk::Surface::Param aSurface);
+#endif
+
+ protected:
+ void ActorDestroy(ActorDestroyReason aWhy) override;
+ void ProcessingError(Result aCode, const char* aReason) override;
+ void HandleFatalError(const char* aMsg) override;
+ mozilla::ipc::IPCResult RecvToolbarAnimatorMessageFromCompositor(
+ const int32_t& aMessage);
+ mozilla::ipc::IPCResult RecvRootFrameMetrics(const ScreenPoint& aScrollOffset,
+ const CSSToScreenScale& aZoom);
+ mozilla::ipc::IPCResult RecvScreenPixels(Shmem&& aMem,
+ const ScreenIntSize& aSize,
+ bool aNeedsYFlip);
+
+ private:
+ explicit UiCompositorControllerChild(const uint64_t& aProcessToken,
+ nsBaseWidget* aWidget);
+ virtual ~UiCompositorControllerChild();
+ void OpenForSameProcess();
+ void OpenForGPUProcess(Endpoint<PUiCompositorControllerChild>&& aEndpoint);
+ void SendCachedValues();
+
+ bool mIsOpen;
+ uint64_t mProcessToken;
+ Maybe<gfx::IntRect> mResize;
+ Maybe<int32_t> mMaxToolbarHeight;
+ Maybe<uint32_t> mDefaultClearColor;
+ Maybe<bool> mLayerUpdateEnabled;
+ RefPtr<nsBaseWidget> mWidget;
+ // Should only be set when compositor is in process.
+ RefPtr<UiCompositorControllerParent> mParent;
+
+#ifdef MOZ_WIDGET_ANDROID
+ // Android interface to send Surfaces to the GPU process. This uses Android
+ // binder rather than IPDL because Surfaces cannot be sent via IPDL. It lives
+ // here regardless because it is a conceptually logical location, even if the
+ // underlying IPC mechanism is different.
+ // This will be null if there is no GPU process.
+ mozilla::java::CompositorSurfaceManager::GlobalRef mCompositorSurfaceManager;
+#endif
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // include_gfx_ipc_UiCompositorControllerChild_h
diff --git a/gfx/layers/ipc/UiCompositorControllerMessageTypes.h b/gfx/layers/ipc/UiCompositorControllerMessageTypes.h
new file mode 100644
index 0000000000..3e68e7be31
--- /dev/null
+++ b/gfx/layers/ipc/UiCompositorControllerMessageTypes.h
@@ -0,0 +1,32 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef include_gfx_ipc_UiCompositorControllerMessageTypes_h
+#define include_gfx_ipc_UiCompositorControllerMessageTypes_h
+
+namespace mozilla {
+namespace layers {
+
+//
+// NOTE: These values are also defined in
+// mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java
+// and must be kept in sync. Any new message added here must also be added
+// there.
+//
+
+// clang-format off
+enum UiCompositorControllerMessageTypes {
+ FIRST_PAINT = 0, // Sent from compositor after first paint
+ LAYERS_UPDATED = 1, // Sent from the compositor when any layer has been updated
+ COMPOSITOR_CONTROLLER_OPEN = 2, // Compositor controller IPC is open
+ IS_COMPOSITOR_CONTROLLER_OPEN = 3 // Special message sent from controller to query if the compositor controller is open
+};
+// clang-format on
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // include_gfx_ipc_UiCompositorControllerMessageTypes_h
diff --git a/gfx/layers/ipc/UiCompositorControllerParent.cpp b/gfx/layers/ipc/UiCompositorControllerParent.cpp
new file mode 100644
index 0000000000..1e34969e84
--- /dev/null
+++ b/gfx/layers/ipc/UiCompositorControllerParent.cpp
@@ -0,0 +1,296 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#include "UiCompositorControllerParent.h"
+
+#if defined(MOZ_WIDGET_ANDROID)
+# include "apz/src/APZCTreeManager.h"
+# include "mozilla/widget/AndroidCompositorWidget.h"
+#endif
+#include <utility>
+
+#include "FrameMetrics.h"
+#include "SynchronousTask.h"
+#include "mozilla/Unused.h"
+#include "mozilla/gfx/Types.h"
+#include "mozilla/ipc/Endpoint.h"
+#include "mozilla/layers/Compositor.h"
+#include "mozilla/layers/CompositorBridgeParent.h"
+#include "mozilla/layers/CompositorThread.h"
+#include "mozilla/layers/UiCompositorControllerMessageTypes.h"
+#include "mozilla/layers/WebRenderBridgeParent.h"
+
+namespace mozilla {
+namespace layers {
+
+typedef CompositorBridgeParent::LayerTreeState LayerTreeState;
+
+/* static */
+RefPtr<UiCompositorControllerParent>
+UiCompositorControllerParent::GetFromRootLayerTreeId(
+ const LayersId& aRootLayerTreeId) {
+ RefPtr<UiCompositorControllerParent> controller;
+ CompositorBridgeParent::CallWithIndirectShadowTree(
+ aRootLayerTreeId, [&](LayerTreeState& aState) -> void {
+ controller = aState.mUiControllerParent;
+ });
+ return controller;
+}
+
+/* static */
+RefPtr<UiCompositorControllerParent> UiCompositorControllerParent::Start(
+ const LayersId& aRootLayerTreeId,
+ Endpoint<PUiCompositorControllerParent>&& aEndpoint) {
+ RefPtr<UiCompositorControllerParent> parent =
+ new UiCompositorControllerParent(aRootLayerTreeId);
+
+ RefPtr<Runnable> task =
+ NewRunnableMethod<Endpoint<PUiCompositorControllerParent>&&>(
+ "layers::UiCompositorControllerParent::Open", parent,
+ &UiCompositorControllerParent::Open, std::move(aEndpoint));
+ CompositorThread()->Dispatch(task.forget());
+
+ return parent;
+}
+
+mozilla::ipc::IPCResult UiCompositorControllerParent::RecvPause() {
+ CompositorBridgeParent* parent =
+ CompositorBridgeParent::GetCompositorBridgeParentFromLayersId(
+ mRootLayerTreeId);
+ if (parent) {
+ parent->PauseComposition();
+ }
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult UiCompositorControllerParent::RecvResume(
+ bool* aOutResumed) {
+ *aOutResumed = false;
+ CompositorBridgeParent* parent =
+ CompositorBridgeParent::GetCompositorBridgeParentFromLayersId(
+ mRootLayerTreeId);
+ if (parent) {
+ *aOutResumed = parent->ResumeComposition();
+ }
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult UiCompositorControllerParent::RecvResumeAndResize(
+ const int32_t& aX, const int32_t& aY, const int32_t& aWidth,
+ const int32_t& aHeight, bool* aOutResumed) {
+ *aOutResumed = false;
+ CompositorBridgeParent* parent =
+ CompositorBridgeParent::GetCompositorBridgeParentFromLayersId(
+ mRootLayerTreeId);
+ if (parent) {
+ // Front-end expects a first paint callback upon resume/resize.
+ parent->ForceIsFirstPaint();
+#if defined(MOZ_WIDGET_ANDROID)
+ parent->GetWidget()->AsAndroid()->NotifyClientSizeChanged(
+ LayoutDeviceIntSize(aWidth, aHeight));
+#endif
+ *aOutResumed = parent->ResumeCompositionAndResize(aX, aY, aWidth, aHeight);
+ }
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult
+UiCompositorControllerParent::RecvInvalidateAndRender() {
+ CompositorBridgeParent* parent =
+ CompositorBridgeParent::GetCompositorBridgeParentFromLayersId(
+ mRootLayerTreeId);
+ if (parent) {
+ parent->ScheduleComposition(wr::RenderReasons::OTHER);
+ }
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult UiCompositorControllerParent::RecvMaxToolbarHeight(
+ const int32_t& aHeight) {
+ mMaxToolbarHeight = aHeight;
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult UiCompositorControllerParent::RecvFixedBottomOffset(
+ const int32_t& aOffset) {
+#if defined(MOZ_WIDGET_ANDROID)
+ CompositorBridgeParent* parent =
+ CompositorBridgeParent::GetCompositorBridgeParentFromLayersId(
+ mRootLayerTreeId);
+ if (parent) {
+ parent->SetFixedLayerMargins(0, aOffset);
+ }
+#endif // defined(MOZ_WIDGET_ANDROID)
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult UiCompositorControllerParent::RecvDefaultClearColor(
+ const uint32_t& aColor) {
+ LayerTreeState* state =
+ CompositorBridgeParent::GetIndirectShadowTree(mRootLayerTreeId);
+
+ if (state && state->mWrBridge) {
+ state->mWrBridge->SetClearColor(gfx::DeviceColor::UnusualFromARGB(aColor));
+ }
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult
+UiCompositorControllerParent::RecvRequestScreenPixels() {
+#if defined(MOZ_WIDGET_ANDROID)
+ LayerTreeState* state =
+ CompositorBridgeParent::GetIndirectShadowTree(mRootLayerTreeId);
+
+ if (state && state->mWrBridge) {
+ state->mWrBridge->RequestScreenPixels(this);
+ state->mWrBridge->ScheduleForcedGenerateFrame(wr::RenderReasons::OTHER);
+ }
+#endif // defined(MOZ_WIDGET_ANDROID)
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult
+UiCompositorControllerParent::RecvEnableLayerUpdateNotifications(
+ const bool& aEnable) {
+#if defined(MOZ_WIDGET_ANDROID)
+ // Layers updates are need by Robocop test which enables them
+ mCompositorLayersUpdateEnabled = aEnable;
+#endif // defined(MOZ_WIDGET_ANDROID)
+
+ return IPC_OK();
+}
+
+void UiCompositorControllerParent::ActorDestroy(ActorDestroyReason aWhy) {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+ Shutdown();
+}
+
+void UiCompositorControllerParent::ToolbarAnimatorMessageFromCompositor(
+ int32_t aMessage) {
+ // This function can be call from ether compositor or controller thread.
+ if (!CompositorThreadHolder::IsInCompositorThread()) {
+ CompositorThread()->Dispatch(NewRunnableMethod<int32_t>(
+ "layers::UiCompositorControllerParent::"
+ "ToolbarAnimatorMessageFromCompositor",
+ this,
+ &UiCompositorControllerParent::ToolbarAnimatorMessageFromCompositor,
+ aMessage));
+ return;
+ }
+
+ Unused << SendToolbarAnimatorMessageFromCompositor(aMessage);
+}
+
+bool UiCompositorControllerParent::AllocPixelBuffer(const int32_t aSize,
+ ipc::Shmem* aMem) {
+ MOZ_ASSERT(aSize > 0);
+ return AllocShmem(aSize, aMem);
+}
+
+void UiCompositorControllerParent::NotifyLayersUpdated() {
+#ifdef MOZ_WIDGET_ANDROID
+ if (mCompositorLayersUpdateEnabled) {
+ ToolbarAnimatorMessageFromCompositor(LAYERS_UPDATED);
+ }
+#endif
+}
+
+void UiCompositorControllerParent::NotifyFirstPaint() {
+ ToolbarAnimatorMessageFromCompositor(FIRST_PAINT);
+}
+
+void UiCompositorControllerParent::NotifyUpdateScreenMetrics(
+ const GeckoViewMetrics& aMetrics) {
+#if defined(MOZ_WIDGET_ANDROID)
+ // TODO: Need to handle different x-and y-scales.
+ CSSToScreenScale scale = ViewTargetAs<ScreenPixel>(
+ aMetrics.mZoom, PixelCastJustification::ScreenIsParentLayerForRoot);
+ ScreenPoint scrollOffset = aMetrics.mVisualScrollOffset * scale;
+ CompositorThread()->Dispatch(NewRunnableMethod<ScreenPoint, CSSToScreenScale>(
+ "UiCompositorControllerParent::SendRootFrameMetrics", this,
+ &UiCompositorControllerParent::SendRootFrameMetrics, scrollOffset,
+ scale));
+#endif
+}
+
+UiCompositorControllerParent::UiCompositorControllerParent(
+ const LayersId& aRootLayerTreeId)
+ : mRootLayerTreeId(aRootLayerTreeId)
+#ifdef MOZ_WIDGET_ANDROID
+ ,
+ mCompositorLayersUpdateEnabled(false)
+#endif
+ ,
+ mMaxToolbarHeight(0) {
+ MOZ_COUNT_CTOR(UiCompositorControllerParent);
+}
+
+UiCompositorControllerParent::~UiCompositorControllerParent() {
+ MOZ_COUNT_DTOR(UiCompositorControllerParent);
+}
+
+void UiCompositorControllerParent::InitializeForSameProcess() {
+ // This function is called by UiCompositorControllerChild in the main thread.
+ // So dispatch to the compositor thread to Initialize.
+ if (!CompositorThreadHolder::IsInCompositorThread()) {
+ SetOtherProcessId(base::GetCurrentProcId());
+ SynchronousTask task(
+ "UiCompositorControllerParent::InitializeForSameProcess");
+
+ CompositorThread()->Dispatch(NS_NewRunnableFunction(
+ "UiCompositorControllerParent::InitializeForSameProcess", [&]() {
+ AutoCompleteTask complete(&task);
+ InitializeForSameProcess();
+ }));
+
+ task.Wait();
+ return;
+ }
+
+ Initialize();
+}
+
+void UiCompositorControllerParent::InitializeForOutOfProcess() {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+ Initialize();
+}
+
+void UiCompositorControllerParent::Initialize() {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+ LayerTreeState* state =
+ CompositorBridgeParent::GetIndirectShadowTree(mRootLayerTreeId);
+ MOZ_ASSERT(state);
+ MOZ_ASSERT(state->mParent);
+ if (!state || !state->mParent) {
+ return;
+ }
+ state->mUiControllerParent = this;
+}
+
+void UiCompositorControllerParent::Open(
+ Endpoint<PUiCompositorControllerParent>&& aEndpoint) {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+ if (!aEndpoint.Bind(this)) {
+ // We can't recover from this.
+ MOZ_CRASH("Failed to bind UiCompositorControllerParent to endpoint");
+ }
+ InitializeForOutOfProcess();
+}
+
+void UiCompositorControllerParent::Shutdown() {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+ LayerTreeState* state =
+ CompositorBridgeParent::GetIndirectShadowTree(mRootLayerTreeId);
+ if (state) {
+ state->mUiControllerParent = nullptr;
+ }
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/ipc/UiCompositorControllerParent.h b/gfx/layers/ipc/UiCompositorControllerParent.h
new file mode 100644
index 0000000000..e6284e7dfb
--- /dev/null
+++ b/gfx/layers/ipc/UiCompositorControllerParent.h
@@ -0,0 +1,84 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#ifndef include_gfx_ipc_UiCompositorControllerParent_h
+#define include_gfx_ipc_UiCompositorControllerParent_h
+
+#include "mozilla/layers/PUiCompositorControllerParent.h"
+#include "mozilla/layers/APZUtils.h"
+#include "mozilla/ipc/Shmem.h"
+#include "mozilla/RefPtr.h"
+
+namespace mozilla {
+namespace layers {
+
+struct FrameMetrics;
+
+class UiCompositorControllerParent final
+ : public PUiCompositorControllerParent {
+ // UiCompositorControllerChild needs to call the private constructor when
+ // running in process.
+ friend class UiCompositorControllerChild;
+
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(UiCompositorControllerParent, final)
+
+ static RefPtr<UiCompositorControllerParent> GetFromRootLayerTreeId(
+ const LayersId& aRootLayerTreeId);
+ static RefPtr<UiCompositorControllerParent> Start(
+ const LayersId& aRootLayerTreeId,
+ Endpoint<PUiCompositorControllerParent>&& aEndpoint);
+
+ // PUiCompositorControllerParent functions
+ mozilla::ipc::IPCResult RecvPause();
+ mozilla::ipc::IPCResult RecvResume(bool* aOutResumed);
+ mozilla::ipc::IPCResult RecvResumeAndResize(const int32_t& aX,
+ const int32_t& aY,
+ const int32_t& aHeight,
+ const int32_t& aWidth,
+ bool* aOutResumed);
+ mozilla::ipc::IPCResult RecvInvalidateAndRender();
+ mozilla::ipc::IPCResult RecvMaxToolbarHeight(const int32_t& aHeight);
+ mozilla::ipc::IPCResult RecvFixedBottomOffset(const int32_t& aOffset);
+ mozilla::ipc::IPCResult RecvDefaultClearColor(const uint32_t& aColor);
+ mozilla::ipc::IPCResult RecvRequestScreenPixels();
+ mozilla::ipc::IPCResult RecvEnableLayerUpdateNotifications(
+ const bool& aEnable);
+ void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ // Class specific functions
+ void ToolbarAnimatorMessageFromCompositor(int32_t aMessage);
+ bool AllocPixelBuffer(const int32_t aSize, Shmem* aMem);
+
+ // Called when a layer has been updated so the UI thread may be notified if
+ // necessary.
+ void NotifyLayersUpdated();
+ void NotifyFirstPaint();
+ void NotifyUpdateScreenMetrics(const GeckoViewMetrics& aMetrics);
+
+ private:
+ explicit UiCompositorControllerParent(const LayersId& aRootLayerTreeId);
+ virtual ~UiCompositorControllerParent();
+ void InitializeForSameProcess();
+ void InitializeForOutOfProcess();
+ void Initialize();
+ void Open(Endpoint<PUiCompositorControllerParent>&& aEndpoint);
+ void Shutdown();
+
+ LayersId mRootLayerTreeId;
+
+#if defined(MOZ_WIDGET_ANDROID)
+ bool mCompositorLayersUpdateEnabled; // Flag set to true when the UI thread
+ // is expecting to be notified when a
+ // layer has been updated
+#endif // defined(MOZ_WIDGET_ANDROID)
+
+ int32_t mMaxToolbarHeight;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // include_gfx_ipc_UiCompositorControllerParent_h
diff --git a/gfx/layers/ipc/VideoBridgeChild.cpp b/gfx/layers/ipc/VideoBridgeChild.cpp
new file mode 100644
index 0000000000..9d956013a0
--- /dev/null
+++ b/gfx/layers/ipc/VideoBridgeChild.cpp
@@ -0,0 +1,183 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "VideoBridgeChild.h"
+#include "VideoBridgeParent.h"
+#include "CompositorThread.h"
+#include "mozilla/dom/ContentChild.h"
+#include "mozilla/ipc/Endpoint.h"
+#include "mozilla/StaticMutex.h"
+#include "transport/runnable_utils.h"
+#include "SynchronousTask.h"
+
+namespace mozilla {
+namespace layers {
+
+// Singleton
+static StaticMutex sVideoBridgeLock MOZ_UNANNOTATED;
+StaticRefPtr<VideoBridgeChild> sVideoBridge;
+
+/* static */
+void VideoBridgeChild::StartupForGPUProcess() {
+ ipc::Endpoint<PVideoBridgeParent> parentPipe;
+ ipc::Endpoint<PVideoBridgeChild> childPipe;
+
+ PVideoBridge::CreateEndpoints(base::GetCurrentProcId(),
+ base::GetCurrentProcId(), &parentPipe,
+ &childPipe);
+
+ VideoBridgeChild::Open(std::move(childPipe));
+ VideoBridgeParent::Open(std::move(parentPipe), VideoBridgeSource::GpuProcess);
+}
+
+/* static */
+void VideoBridgeChild::Open(Endpoint<PVideoBridgeChild>&& aEndpoint) {
+ StaticMutexAutoLock lock(sVideoBridgeLock);
+ MOZ_ASSERT(!sVideoBridge || !sVideoBridge->CanSend());
+ sVideoBridge = new VideoBridgeChild();
+
+ if (!aEndpoint.Bind(sVideoBridge)) {
+ // We can't recover from this.
+ MOZ_CRASH("Failed to bind VideoBridgeChild to endpoint");
+ }
+}
+
+/* static */
+void VideoBridgeChild::Shutdown() {
+ StaticMutexAutoLock lock(sVideoBridgeLock);
+ if (sVideoBridge) {
+ sVideoBridge->Close();
+ sVideoBridge = nullptr;
+ }
+}
+
+VideoBridgeChild::VideoBridgeChild()
+ : mThread(GetCurrentSerialEventTarget()), mCanSend(true) {}
+
+VideoBridgeChild::~VideoBridgeChild() = default;
+
+VideoBridgeChild* VideoBridgeChild::GetSingleton() {
+ StaticMutexAutoLock lock(sVideoBridgeLock);
+ return sVideoBridge;
+}
+
+bool VideoBridgeChild::AllocUnsafeShmem(size_t aSize, ipc::Shmem* aShmem) {
+ if (!mThread->IsOnCurrentThread()) {
+ return DispatchAllocShmemInternal(aSize, aShmem, true); // true: unsafe
+ }
+
+ if (!CanSend()) {
+ return false;
+ }
+
+ return PVideoBridgeChild::AllocUnsafeShmem(aSize, aShmem);
+}
+
+bool VideoBridgeChild::AllocShmem(size_t aSize, ipc::Shmem* aShmem) {
+ MOZ_ASSERT(CanSend());
+ return PVideoBridgeChild::AllocShmem(aSize, aShmem);
+}
+
+void VideoBridgeChild::ProxyAllocShmemNow(SynchronousTask* aTask, size_t aSize,
+ ipc::Shmem* aShmem, bool aUnsafe,
+ bool* aSuccess) {
+ AutoCompleteTask complete(aTask);
+
+ if (!CanSend()) {
+ return;
+ }
+
+ bool ok = false;
+ if (aUnsafe) {
+ ok = AllocUnsafeShmem(aSize, aShmem);
+ } else {
+ ok = AllocShmem(aSize, aShmem);
+ }
+ *aSuccess = ok;
+}
+
+bool VideoBridgeChild::DispatchAllocShmemInternal(size_t aSize,
+ ipc::Shmem* aShmem,
+ bool aUnsafe) {
+ SynchronousTask task("AllocatorProxy alloc");
+
+ bool success = false;
+ RefPtr<Runnable> runnable = WrapRunnable(
+ RefPtr<VideoBridgeChild>(this), &VideoBridgeChild::ProxyAllocShmemNow,
+ &task, aSize, aShmem, aUnsafe, &success);
+ GetThread()->Dispatch(runnable.forget());
+
+ task.Wait();
+
+ return success;
+}
+
+void VideoBridgeChild::ProxyDeallocShmemNow(SynchronousTask* aTask,
+ ipc::Shmem* aShmem, bool* aResult) {
+ AutoCompleteTask complete(aTask);
+
+ if (!CanSend()) {
+ return;
+ }
+ *aResult = DeallocShmem(*aShmem);
+}
+
+bool VideoBridgeChild::DeallocShmem(ipc::Shmem& aShmem) {
+ if (GetThread()->IsOnCurrentThread()) {
+ if (!CanSend()) {
+ return false;
+ }
+ return PVideoBridgeChild::DeallocShmem(aShmem);
+ }
+
+ SynchronousTask task("AllocatorProxy Dealloc");
+ bool result = false;
+
+ RefPtr<Runnable> runnable = WrapRunnable(
+ RefPtr<VideoBridgeChild>(this), &VideoBridgeChild::ProxyDeallocShmemNow,
+ &task, &aShmem, &result);
+ GetThread()->Dispatch(runnable.forget());
+
+ task.Wait();
+ return result;
+}
+
+PTextureChild* VideoBridgeChild::AllocPTextureChild(
+ const SurfaceDescriptor&, ReadLockDescriptor&, const LayersBackend&,
+ const TextureFlags&, const dom::ContentParentId& aContentId,
+ const uint64_t& aSerial) {
+ MOZ_ASSERT(CanSend());
+ return TextureClient::CreateIPDLActor();
+}
+
+bool VideoBridgeChild::DeallocPTextureChild(PTextureChild* actor) {
+ return TextureClient::DestroyIPDLActor(actor);
+}
+
+void VideoBridgeChild::ActorDestroy(ActorDestroyReason aWhy) {
+ mCanSend = false;
+}
+
+PTextureChild* VideoBridgeChild::CreateTexture(
+ const SurfaceDescriptor& aSharedData, ReadLockDescriptor&& aReadLock,
+ LayersBackend aLayersBackend, TextureFlags aFlags,
+ const dom::ContentParentId& aContentId, uint64_t aSerial,
+ wr::MaybeExternalImageId& aExternalImageId) {
+ MOZ_ASSERT(CanSend());
+ return SendPTextureConstructor(aSharedData, std::move(aReadLock),
+ aLayersBackend, aFlags, aContentId, aSerial);
+}
+
+bool VideoBridgeChild::IsSameProcess() const {
+ return OtherPid() == base::GetCurrentProcId();
+}
+
+void VideoBridgeChild::HandleFatalError(const char* aMsg) {
+ dom::ContentChild::FatalErrorIfNotUsingGPUProcess(aMsg, OtherPid());
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/ipc/VideoBridgeChild.h b/gfx/layers/ipc/VideoBridgeChild.h
new file mode 100644
index 0000000000..378fa9e337
--- /dev/null
+++ b/gfx/layers/ipc/VideoBridgeChild.h
@@ -0,0 +1,88 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_GFX_VIDEOBRIDGECHILD_H
+#define MOZILLA_GFX_VIDEOBRIDGECHILD_H
+
+#include "mozilla/layers/PVideoBridgeChild.h"
+#include "mozilla/layers/VideoBridgeUtils.h"
+#include "ISurfaceAllocator.h"
+#include "TextureForwarder.h"
+
+namespace mozilla {
+namespace layers {
+
+class SynchronousTask;
+
+class VideoBridgeChild final : public PVideoBridgeChild,
+ public TextureForwarder {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(VideoBridgeChild, override);
+
+ static void StartupForGPUProcess();
+ static void Shutdown();
+
+ static VideoBridgeChild* GetSingleton();
+
+ // PVideoBridgeChild
+ PTextureChild* AllocPTextureChild(const SurfaceDescriptor& aSharedData,
+ ReadLockDescriptor& aReadLock,
+ const LayersBackend& aLayersBackend,
+ const TextureFlags& aFlags,
+ const dom::ContentParentId& aContentId,
+ const uint64_t& aSerial);
+ bool DeallocPTextureChild(PTextureChild* actor);
+
+ void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ // ISurfaceAllocator
+ bool AllocUnsafeShmem(size_t aSize, mozilla::ipc::Shmem* aShmem) override;
+ bool AllocShmem(size_t aSize, mozilla::ipc::Shmem* aShmem) override;
+ bool DeallocShmem(mozilla::ipc::Shmem& aShmem) override;
+
+ // TextureForwarder
+ PTextureChild* CreateTexture(
+ const SurfaceDescriptor& aSharedData, ReadLockDescriptor&& aReadLock,
+ LayersBackend aLayersBackend, TextureFlags aFlags,
+ const dom::ContentParentId& aContentId, uint64_t aSerial,
+ wr::MaybeExternalImageId& aExternalImageId) override;
+
+ // ClientIPCAllocator
+ base::ProcessId GetParentPid() const override { return OtherPid(); }
+ nsISerialEventTarget* GetThread() const override { return mThread; }
+ void CancelWaitForNotifyNotUsed(uint64_t aTextureId) override {
+ MOZ_ASSERT(false, "NO RECYCLING HERE");
+ }
+
+ // ISurfaceAllocator
+ bool IsSameProcess() const override;
+
+ bool CanSend() { return mCanSend; }
+
+ static void Open(Endpoint<PVideoBridgeChild>&& aEndpoint);
+
+ protected:
+ void HandleFatalError(const char* aMsg) override;
+ bool DispatchAllocShmemInternal(size_t aSize, mozilla::ipc::Shmem* aShmem,
+ bool aUnsafe);
+ void ProxyAllocShmemNow(SynchronousTask* aTask, size_t aSize,
+ mozilla::ipc::Shmem* aShmem, bool aUnsafe,
+ bool* aSuccess);
+ void ProxyDeallocShmemNow(SynchronousTask* aTask, mozilla::ipc::Shmem* aShmem,
+ bool* aResult);
+
+ private:
+ VideoBridgeChild();
+ virtual ~VideoBridgeChild();
+
+ nsCOMPtr<nsISerialEventTarget> mThread;
+ bool mCanSend;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif
diff --git a/gfx/layers/ipc/VideoBridgeParent.cpp b/gfx/layers/ipc/VideoBridgeParent.cpp
new file mode 100644
index 0000000000..89cc85aee3
--- /dev/null
+++ b/gfx/layers/ipc/VideoBridgeParent.cpp
@@ -0,0 +1,233 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "VideoBridgeParent.h"
+#include "CompositorThread.h"
+#include "mozilla/DataMutex.h"
+#include "mozilla/ipc/Endpoint.h"
+#include "mozilla/layers/TextureHost.h"
+#include "mozilla/layers/VideoBridgeUtils.h"
+#include "mozilla/webrender/RenderThread.h"
+
+namespace mozilla {
+namespace layers {
+
+using namespace mozilla::ipc;
+using namespace mozilla::gfx;
+
+using VideoBridgeTable =
+ EnumeratedArray<VideoBridgeSource, VideoBridgeSource::_Count,
+ VideoBridgeParent*>;
+
+static StaticDataMutex<VideoBridgeTable> sVideoBridgeFromProcess(
+ "VideoBridges");
+static Atomic<bool> sVideoBridgeParentShutDown(false);
+
+VideoBridgeParent::VideoBridgeParent(VideoBridgeSource aSource)
+ : mCompositorThreadHolder(CompositorThreadHolder::GetSingleton()),
+ mClosed(false) {
+ auto videoBridgeFromProcess = sVideoBridgeFromProcess.Lock();
+ switch (aSource) {
+ case VideoBridgeSource::RddProcess:
+ case VideoBridgeSource::GpuProcess:
+ case VideoBridgeSource::MFMediaEngineCDMProcess:
+ (*videoBridgeFromProcess)[aSource] = this;
+ break;
+ default:
+ MOZ_CRASH("Unhandled case");
+ }
+}
+
+VideoBridgeParent::~VideoBridgeParent() {
+ auto videoBridgeFromProcess = sVideoBridgeFromProcess.Lock();
+ for (auto& bridgeParent : *videoBridgeFromProcess) {
+ if (bridgeParent == this) {
+ bridgeParent = nullptr;
+ }
+ }
+}
+
+/* static */
+void VideoBridgeParent::Open(Endpoint<PVideoBridgeParent>&& aEndpoint,
+ VideoBridgeSource aSource) {
+ RefPtr<VideoBridgeParent> parent = new VideoBridgeParent(aSource);
+
+ CompositorThread()->Dispatch(
+ NewRunnableMethod<Endpoint<PVideoBridgeParent>&&>(
+ "gfx::layers::VideoBridgeParent::Bind", parent,
+ &VideoBridgeParent::Bind, std::move(aEndpoint)));
+}
+
+void VideoBridgeParent::Bind(Endpoint<PVideoBridgeParent>&& aEndpoint) {
+ if (!aEndpoint.Bind(this)) {
+ // We can't recover from this.
+ MOZ_CRASH("Failed to bind VideoBridgeParent to endpoint");
+ }
+}
+
+/* static */
+VideoBridgeParent* VideoBridgeParent::GetSingleton(
+ const Maybe<VideoBridgeSource>& aSource) {
+ MOZ_ASSERT(aSource.isSome());
+ auto videoBridgeFromProcess = sVideoBridgeFromProcess.Lock();
+ switch (aSource.value()) {
+ case VideoBridgeSource::RddProcess:
+ case VideoBridgeSource::GpuProcess:
+ case VideoBridgeSource::MFMediaEngineCDMProcess:
+ MOZ_ASSERT((*videoBridgeFromProcess)[aSource.value()]);
+ return (*videoBridgeFromProcess)[aSource.value()];
+ default:
+ MOZ_CRASH("Unhandled case");
+ }
+}
+
+TextureHost* VideoBridgeParent::LookupTexture(
+ const dom::ContentParentId& aContentId, uint64_t aSerial) {
+ MOZ_DIAGNOSTIC_ASSERT(CompositorThread() &&
+ CompositorThread()->IsOnCurrentThread());
+ auto* actor = mTextureMap[aSerial];
+ if (NS_WARN_IF(!actor)) {
+ return nullptr;
+ }
+
+ if (NS_WARN_IF(aContentId != TextureHost::GetTextureContentId(actor))) {
+ return nullptr;
+ }
+
+ return TextureHost::AsTextureHost(mTextureMap[aSerial]);
+}
+
+void VideoBridgeParent::ActorDestroy(ActorDestroyReason aWhy) {
+ bool shutdown = sVideoBridgeParentShutDown;
+
+ if (!shutdown && aWhy == AbnormalShutdown) {
+ gfxCriticalNote
+ << "VideoBridgeParent receives IPC close with reason=AbnormalShutdown";
+ }
+ // Can't alloc/dealloc shmems from now on.
+ mClosed = true;
+
+ mCompositorThreadHolder = nullptr;
+ ReleaseCompositorThread();
+}
+
+/* static */
+void VideoBridgeParent::Shutdown() {
+ sVideoBridgeParentShutDown = true;
+
+ auto videoBridgeFromProcess = sVideoBridgeFromProcess.Lock();
+ for (auto& bridgeParent : *videoBridgeFromProcess) {
+ if (bridgeParent) {
+ bridgeParent->ReleaseCompositorThread();
+ }
+ }
+}
+
+/* static */
+void VideoBridgeParent::UnregisterExternalImages() {
+ MOZ_ASSERT(sVideoBridgeParentShutDown);
+
+ auto videoBridgeFromProcess = sVideoBridgeFromProcess.Lock();
+ for (auto& bridgeParent : *videoBridgeFromProcess) {
+ if (bridgeParent) {
+ bridgeParent->DoUnregisterExternalImages();
+ }
+ }
+}
+
+void VideoBridgeParent::DoUnregisterExternalImages() {
+ const ManagedContainer<PTextureParent>& textures = ManagedPTextureParent();
+ for (const auto& key : textures) {
+ RefPtr<TextureHost> texture = TextureHost::AsTextureHost(key);
+
+ if (texture) {
+ texture->MaybeDestroyRenderTexture();
+ }
+ }
+}
+
+void VideoBridgeParent::ReleaseCompositorThread() {
+ mCompositorThreadHolder = nullptr;
+}
+
+PTextureParent* VideoBridgeParent::AllocPTextureParent(
+ const SurfaceDescriptor& aSharedData, ReadLockDescriptor& aReadLock,
+ const LayersBackend& aLayersBackend, const TextureFlags& aFlags,
+ const dom::ContentParentId& aContentId, const uint64_t& aSerial) {
+ PTextureParent* parent = TextureHost::CreateIPDLActor(
+ this, aSharedData, std::move(aReadLock), aLayersBackend, aFlags,
+ aContentId, aSerial, Nothing());
+
+ if (!parent) {
+ return nullptr;
+ }
+
+ mTextureMap[aSerial] = parent;
+ return parent;
+}
+
+bool VideoBridgeParent::DeallocPTextureParent(PTextureParent* actor) {
+ mTextureMap.erase(TextureHost::GetTextureSerial(actor));
+ return TextureHost::DestroyIPDLActor(actor);
+}
+
+void VideoBridgeParent::SendAsyncMessage(
+ const nsTArray<AsyncParentMessageData>& aMessage) {
+ MOZ_ASSERT(false, "AsyncMessages not supported");
+}
+
+bool VideoBridgeParent::AllocShmem(size_t aSize, ipc::Shmem* aShmem) {
+ if (mClosed) {
+ return false;
+ }
+ return PVideoBridgeParent::AllocShmem(aSize, aShmem);
+}
+
+bool VideoBridgeParent::AllocUnsafeShmem(size_t aSize, ipc::Shmem* aShmem) {
+ if (mClosed) {
+ return false;
+ }
+ return PVideoBridgeParent::AllocUnsafeShmem(aSize, aShmem);
+}
+
+bool VideoBridgeParent::DeallocShmem(ipc::Shmem& aShmem) {
+ if (mClosed) {
+ return false;
+ }
+ return PVideoBridgeParent::DeallocShmem(aShmem);
+}
+
+bool VideoBridgeParent::IsSameProcess() const {
+ return OtherPid() == base::GetCurrentProcId();
+}
+
+void VideoBridgeParent::NotifyNotUsed(PTextureParent* aTexture,
+ uint64_t aTransactionId) {}
+
+void VideoBridgeParent::OnChannelError() {
+ bool shutdown = sVideoBridgeParentShutDown;
+ if (!shutdown) {
+ // Destory RenderBufferTextureHosts. Shmems of ShmemTextureHosts are going
+ // to be destroyed
+ std::vector<wr::ExternalImageId> ids;
+ auto& ptextures = ManagedPTextureParent();
+ for (const auto& ptexture : ptextures) {
+ RefPtr<TextureHost> texture = TextureHost::AsTextureHost(ptexture);
+ if (texture && texture->AsShmemTextureHost() &&
+ texture->GetMaybeExternalImageId().isSome()) {
+ ids.emplace_back(texture->GetMaybeExternalImageId().ref());
+ }
+ }
+ if (!ids.empty()) {
+ wr::RenderThread::Get()->DestroyExternalImagesSyncWait(std::move(ids));
+ }
+ }
+
+ PVideoBridgeParent::OnChannelError();
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/ipc/VideoBridgeParent.h b/gfx/layers/ipc/VideoBridgeParent.h
new file mode 100644
index 0000000000..9a3466f59a
--- /dev/null
+++ b/gfx/layers/ipc/VideoBridgeParent.h
@@ -0,0 +1,86 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef gfx_layers_ipc_VideoBridgeParent_h_
+#define gfx_layers_ipc_VideoBridgeParent_h_
+
+#include "mozilla/layers/ISurfaceAllocator.h"
+#include "mozilla/layers/PVideoBridgeParent.h"
+
+namespace mozilla {
+namespace layers {
+
+enum class VideoBridgeSource : uint8_t;
+class CompositorThreadHolder;
+
+class VideoBridgeParent final : public PVideoBridgeParent,
+ public HostIPCAllocator,
+ public mozilla::ipc::IShmemAllocator {
+ public:
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(VideoBridgeParent, HostIPCAllocator)
+
+ static VideoBridgeParent* GetSingleton(
+ const Maybe<VideoBridgeSource>& aSource);
+
+ static void Open(Endpoint<PVideoBridgeParent>&& aEndpoint,
+ VideoBridgeSource aSource);
+ static void Shutdown();
+ static void UnregisterExternalImages();
+
+ TextureHost* LookupTexture(const dom::ContentParentId& aContentId,
+ uint64_t aSerial);
+
+ // PVideoBridgeParent
+ void ActorDestroy(ActorDestroyReason aWhy) override;
+ PTextureParent* AllocPTextureParent(const SurfaceDescriptor& aSharedData,
+ ReadLockDescriptor& aReadLock,
+ const LayersBackend& aLayersBackend,
+ const TextureFlags& aFlags,
+ const dom::ContentParentId& aContentId,
+ const uint64_t& aSerial);
+ bool DeallocPTextureParent(PTextureParent* actor);
+
+ // HostIPCAllocator
+ base::ProcessId GetChildProcessId() override { return OtherPid(); }
+ void NotifyNotUsed(PTextureParent* aTexture,
+ uint64_t aTransactionId) override;
+ void SendAsyncMessage(
+ const nsTArray<AsyncParentMessageData>& aMessage) override;
+
+ // ISurfaceAllocator
+ IShmemAllocator* AsShmemAllocator() override { return this; }
+ bool IsSameProcess() const override;
+ bool IPCOpen() const override { return !mClosed; }
+
+ // IShmemAllocator
+ bool AllocShmem(size_t aSize, ipc::Shmem* aShmem) override;
+
+ bool AllocUnsafeShmem(size_t aSize, ipc::Shmem* aShmem) override;
+
+ bool DeallocShmem(ipc::Shmem& aShmem) override;
+
+ void OnChannelError() override;
+
+ private:
+ ~VideoBridgeParent();
+
+ explicit VideoBridgeParent(VideoBridgeSource aSource);
+ void Bind(Endpoint<PVideoBridgeParent>&& aEndpoint);
+
+ void ReleaseCompositorThread();
+ void DoUnregisterExternalImages();
+
+ RefPtr<CompositorThreadHolder> mCompositorThreadHolder;
+
+ std::map<uint64_t, PTextureParent*> mTextureMap;
+
+ bool mClosed;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // gfx_layers_ipc_VideoBridgeParent_h_
diff --git a/gfx/layers/ipc/VideoBridgeUtils.h b/gfx/layers/ipc/VideoBridgeUtils.h
new file mode 100644
index 0000000000..4163b5e08f
--- /dev/null
+++ b/gfx/layers/ipc/VideoBridgeUtils.h
@@ -0,0 +1,38 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef IPC_VideoBridgeUtils_h
+#define IPC_VideoBridgeUtils_h
+
+#include "ipc/EnumSerializer.h"
+
+namespace mozilla {
+namespace layers {
+
+enum class VideoBridgeSource : uint8_t {
+ RddProcess,
+ GpuProcess,
+ MFMediaEngineCDMProcess,
+ _Count,
+};
+
+typedef Maybe<VideoBridgeSource> MaybeVideoBridgeSource;
+
+} // namespace layers
+} // namespace mozilla
+
+namespace IPC {
+
+template <>
+struct ParamTraits<mozilla::layers::VideoBridgeSource>
+ : public ContiguousEnumSerializer<
+ mozilla::layers::VideoBridgeSource,
+ mozilla::layers::VideoBridgeSource::RddProcess,
+ mozilla::layers::VideoBridgeSource::_Count> {};
+
+} // namespace IPC
+
+#endif // IPC_VideoBridgeUtils_h
diff --git a/gfx/layers/ipc/WebRenderMessages.ipdlh b/gfx/layers/ipc/WebRenderMessages.ipdlh
new file mode 100644
index 0000000000..eb5633a506
--- /dev/null
+++ b/gfx/layers/ipc/WebRenderMessages.ipdlh
@@ -0,0 +1,199 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: sw=2 ts=8 et :
+ */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+include "mozilla/layers/LayersMessageUtils.h";
+include "mozilla/layers/WebRenderMessageUtils.h";
+
+include LayersSurfaces;
+include LayersMessages;
+include protocol PTexture;
+
+using mozilla::layers::CompositableHandle from "mozilla/layers/LayersTypes.h";
+using mozilla::layers::CompositableHandleOwner from "mozilla/layers/LayersTypes.h";
+using mozilla::wr::LayoutSize from "mozilla/webrender/webrender_ffi.h";
+using mozilla::wr::ImageDescriptor from "mozilla/webrender/webrender_ffi.h";
+using mozilla::wr::ImageRendering from "mozilla/webrender/webrender_ffi.h";
+using mozilla::wr::MixBlendMode from "mozilla/webrender/webrender_ffi.h";
+using mozilla::wr::ExternalImageId from "mozilla/webrender/WebRenderTypes.h";
+using mozilla::wr::MaybeFontInstanceOptions from "mozilla/webrender/WebRenderTypes.h";
+using mozilla::wr::MaybeFontInstancePlatformOptions from "mozilla/webrender/WebRenderTypes.h";
+using mozilla::wr::FontInstanceKey from "mozilla/webrender/WebRenderTypes.h";
+using mozilla::wr::FontKey from "mozilla/webrender/WebRenderTypes.h";
+using mozilla::wr::ImageKey from "mozilla/webrender/WebRenderTypes.h";
+using mozilla::wr::BlobImageKey from "mozilla/webrender/WebRenderTypes.h";
+using mozilla::wr::PipelineId from "mozilla/webrender/WebRenderTypes.h";
+using mozilla::gfx::MaybeIntSize from "mozilla/gfx/Point.h";
+using mozilla::LayoutDeviceRect from "Units.h";
+using mozilla::LayoutDeviceSize from "Units.h";
+using mozilla::ImageIntRect from "Units.h";
+using mozilla::gfx::Rect from "mozilla/gfx/Rect.h";
+using mozilla::VideoInfo::Rotation from "MediaInfo.h";
+using mozilla::gfx::Matrix4x4 from "mozilla/gfx/Matrix.h";
+using struct mozilla::void_t from "mozilla/ipc/IPCCore.h";
+
+namespace mozilla {
+namespace layers {
+
+struct RefCountedShmem {
+ Shmem buffer;
+};
+
+struct OpAddSharedExternalImage {
+ ExternalImageId externalImageId;
+ ImageKey key;
+};
+
+struct OpPushExternalImageForTexture {
+ ExternalImageId externalImageId;
+ ImageKey key;
+ PTexture texture;
+ bool isUpdate;
+};
+
+struct OpAddCompositorAnimations {
+ CompositorAnimations data;
+};
+
+struct OpAddPipelineIdForCompositable {
+ PipelineId pipelineId;
+ CompositableHandle handle;
+ CompositableHandleOwner owner;
+};
+
+struct OpRemovePipelineIdForCompositable {
+ PipelineId pipelineId;
+};
+
+struct OpReleaseTextureOfImage {
+ ImageKey key;
+};
+
+struct OpUpdateAsyncImagePipeline {
+ PipelineId pipelineId;
+ LayoutDeviceRect scBounds;
+ Rotation rotation;
+ ImageRendering filter;
+ MixBlendMode mixBlendMode;
+};
+
+struct OpUpdatedAsyncImagePipeline {
+ PipelineId pipelineId;
+};
+
+union WebRenderParentCommand {
+ OpAddPipelineIdForCompositable;
+ OpRemovePipelineIdForCompositable;
+ OpReleaseTextureOfImage;
+ OpUpdateAsyncImagePipeline;
+ OpUpdatedAsyncImagePipeline;
+ CompositableOperation;
+ OpAddCompositorAnimations;
+};
+
+struct OffsetRange {
+ uint32_t source;
+ uint32_t start;
+ uint32_t length;
+};
+
+struct OpAddImage {
+ ImageDescriptor descriptor;
+ OffsetRange bytes;
+ uint16_t tiling;
+ ImageKey key;
+};
+
+struct OpAddBlobImage {
+ ImageDescriptor descriptor;
+ OffsetRange bytes;
+ ImageIntRect visibleRect;
+ uint16_t tiling;
+ BlobImageKey key;
+};
+
+struct OpUpdateImage {
+ ImageDescriptor descriptor;
+ OffsetRange bytes;
+ ImageKey key;
+};
+
+struct OpUpdateBlobImage {
+ ImageDescriptor descriptor;
+ OffsetRange bytes;
+ BlobImageKey key;
+ ImageIntRect visibleRect;
+ ImageIntRect dirtyRect;
+};
+
+struct OpSetBlobImageVisibleArea {
+ ImageIntRect area;
+ BlobImageKey key;
+};
+
+struct OpUpdateSharedExternalImage {
+ ExternalImageId externalImageId;
+ ImageKey key;
+ ImageIntRect dirtyRect;
+};
+
+struct OpDeleteImage {
+ ImageKey key;
+};
+
+struct OpDeleteBlobImage {
+ BlobImageKey key;
+};
+
+struct OpAddRawFont {
+ OffsetRange bytes;
+ uint32_t fontIndex;
+ FontKey key;
+};
+
+struct OpAddFontDescriptor {
+ OffsetRange bytes;
+ uint32_t fontIndex;
+ FontKey key;
+};
+
+struct OpDeleteFont {
+ FontKey key;
+};
+
+struct OpAddFontInstance {
+ MaybeFontInstanceOptions options;
+ MaybeFontInstancePlatformOptions platformOptions;
+ OffsetRange variations;
+ FontInstanceKey instanceKey;
+ FontKey fontKey;
+ float glyphSize;
+};
+
+struct OpDeleteFontInstance {
+ FontInstanceKey key;
+};
+
+union OpUpdateResource {
+ OpAddImage;
+ OpAddBlobImage;
+ OpUpdateImage;
+ OpUpdateBlobImage;
+ OpSetBlobImageVisibleArea;
+ OpDeleteImage;
+ OpDeleteBlobImage;
+ OpAddRawFont;
+ OpAddFontDescriptor;
+ OpDeleteFont;
+ OpAddFontInstance;
+ OpDeleteFontInstance;
+ OpAddSharedExternalImage;
+ OpPushExternalImageForTexture;
+ OpUpdateSharedExternalImage;
+};
+
+} // namespace
+} // namespace
diff --git a/gfx/layers/layerviewer/hide.png b/gfx/layers/layerviewer/hide.png
new file mode 100644
index 0000000000..9a92e2c1b1
--- /dev/null
+++ b/gfx/layers/layerviewer/hide.png
Binary files differ
diff --git a/gfx/layers/layerviewer/index.html b/gfx/layers/layerviewer/index.html
new file mode 100644
index 0000000000..533a4e20f5
--- /dev/null
+++ b/gfx/layers/layerviewer/index.html
@@ -0,0 +1,57 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>GFX Display List & Layer Visualizer</title>
+ <meta charset="utf-8" />
+ <link rel="stylesheet" type="text/css" href="tree.css" />
+ <script src="layerTreeView.js"></script>
+ <style>
+ .csstooltip {
+ z-index: 5;
+ background: white;
+ border: solid 1px black;
+ position: absolute;
+ padding: 5px;
+ margin: 5px;
+ max-width: 300px;
+ }
+ </style>
+ </head>
+ <body>
+ <h1>GFX Layers dump visualizer:</h1>
+ Paste your display list or layers dump into this textarea:<br />
+ <textarea
+ id="input_layers_dump"
+ style="width: 100%; height: 80%"
+ cols="80"
+ rows="10"
+ >
+ClientLayerManager (0x1264f5000)
+ ClientContainerLayer (0x1263fe200) [visible=< (x=0, y=0, w=1457, h=1163); >] [opaqueContent] [metrics0={ [cb=(x=0.000000, y=0.000000, w=1457.000000, h=1163.000000)] [sr=(x=0.000000, y=0.000000, w=1457.000000, h=1163.000000)] [s=(0,0)] [dp=(x=0.000000, y=0.000000, w=1457.000000, h=1163.000000)] [cdp=(x=0.000000, y=0.000000, w=0.000000, h=0.000000)] [color=rgba(0, 0, 0, 0.000000)] [scrollId=3] [z=1] }]
+ ClientTiledPaintedLayer (0x1263b3600) [bounds=(x=-1, y=0, w=1458, h=1163)] [visible=< (x=0, y=0, w=1457, h=79); >] { hitregion=< (x=0, y=0, w=1457, h=47); (x=-1, y=47, w=1458, h=24); (x=0, y=71, w=1457, h=1092); > dispatchtocontentregion=< (x=68, y=9, w=1375, h=31); (x=944, y=47, w=280, h=24); >} [opaqueContent] [valid=< (x=0, y=0, w=1457, h=79); >]
+ SingleTiledContentClient (0x126f80680)
+ ClientContainerLayer (0x122a33f00) [clip=(x=0, y=79, w=1457, h=1084)] [visible=< (x=0, y=79, w=1457, h=1084); >] [opaqueContent]
+ ClientTiledPaintedLayer (0x11e11a700) [bounds=(x=0, y=79, w=1457, h=1084)] [visible=< (x=0, y=79, w=1457, h=1084); >] { hitregion=< (x=0, y=79, w=1457, h=1084); > dispatchtocontentregion=< (x=0, y=125, w=1457, h=1034); >} [opaqueContent] [valid=< (x=0, y=79, w=1457, h=1084); >]
+ SingleTiledContentClient (0x1226d52c0)
+ </textarea>
+ <br />
+ <input type="button" value="Process pasted log" onclick="log_pasted()" />
+ <br />
+ <br />
+ Help: To get a layers dump go to about:config and set
+ layout.display-list.dump;true
+ <script>
+ function log_pasted() {
+ var container = parseMultiLineDump(
+ document.getElementById("input_layers_dump").value
+ );
+ document.body.innerHTML = "";
+ document.body.appendChild(container);
+ }
+ </script>
+ </body>
+</html>
diff --git a/gfx/layers/layerviewer/layerTreeView.js b/gfx/layers/layerviewer/layerTreeView.js
new file mode 100644
index 0000000000..9e382ecdd9
--- /dev/null
+++ b/gfx/layers/layerviewer/layerTreeView.js
@@ -0,0 +1,972 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function toFixed(num, fixed) {
+ fixed = fixed || 0;
+ fixed = Math.pow(10, fixed);
+ return Math.floor(num * fixed) / fixed;
+}
+function createElement(name, props) {
+ var el = document.createElement(name);
+
+ for (var key in props) {
+ if (key === "style") {
+ for (var styleName in props.style) {
+ el.style[styleName] = props.style[styleName];
+ }
+ } else {
+ el[key] = props[key];
+ }
+ }
+
+ return el;
+}
+
+function parseDisplayList(lines) {
+ var root = {
+ line: "DisplayListRoot 0",
+ name: "DisplayListRoot",
+ address: "0x0",
+ frame: "Root",
+ children: [],
+ };
+
+ var objectAtIndentation = {
+ "-1": root,
+ };
+
+ for (var i = 0; i < lines.length; i++) {
+ var line = lines[i];
+
+ var layerObject = {
+ line,
+ children: [],
+ };
+ if (!root) {
+ root = layerObject;
+ }
+
+ var matches = line.match(
+ "(\\s*)(\\w+)\\sp=(\\w+)\\sf=(.*?)\\((.*?)\\)\\s(z=(\\w+)\\s)?(.*?)?( layer=(\\w+))?$"
+ );
+ if (!matches) {
+ dump("Failed to match: " + line + "\n");
+ continue;
+ }
+
+ var indentation = Math.floor(matches[1].length / 2);
+ objectAtIndentation[indentation] = layerObject;
+ var parent = objectAtIndentation[indentation - 1];
+ if (parent) {
+ parent.children.push(layerObject);
+ }
+
+ layerObject.name = matches[2];
+ layerObject.address = matches[3]; // Use 0x prefix to be consistent with layer dump
+ layerObject.frame = matches[4];
+ layerObject.contentDescriptor = matches[5];
+ layerObject.z = matches[7];
+ var rest = matches[8];
+ if (matches[10]) {
+ // WrapList don't provide a layer
+ layerObject.layer = matches[10];
+ }
+ layerObject.rest = rest;
+
+ // the content node name doesn't have a prefix, this makes the parsing easier
+ rest = "content" + rest;
+
+ var nesting = 0;
+ var startIndex;
+ var lastSpace = -1;
+ for (var j = 0; j < rest.length; j++) {
+ if (rest.charAt(j) == "(") {
+ nesting++;
+ if (nesting == 1) {
+ startIndex = j;
+ }
+ } else if (rest.charAt(j) == ")") {
+ nesting--;
+ if (nesting == 0) {
+ var name = rest.substring(lastSpace + 1, startIndex);
+ var value = rest.substring(startIndex + 1, j);
+
+ var rectMatches = value.match("^(.*?),(.*?),(.*?),(.*?)$");
+ if (rectMatches) {
+ layerObject[name] = [
+ parseFloat(rectMatches[1]),
+ parseFloat(rectMatches[2]),
+ parseFloat(rectMatches[3]),
+ parseFloat(rectMatches[4]),
+ ];
+ } else {
+ layerObject[name] = value;
+ }
+ }
+ } else if (nesting == 0 && rest.charAt(j) == " ") {
+ lastSpace = j;
+ }
+ }
+ // dump("FIELDS: " + JSON.stringify(fields) + "\n");
+ }
+ return root;
+}
+
+function trim(s) {
+ return (s || "").replace(/^\s+|\s+$/g, "");
+}
+
+function getDataURI(str) {
+ if (str.indexOf("data:image/png;base64,") == 0) {
+ return str;
+ }
+
+ var matches = str.match(
+ "data:image/lz4bgra;base64,([0-9]+),([0-9]+),([0-9]+),(.*)"
+ );
+ if (!matches) {
+ return null;
+ }
+
+ var canvas = document.createElement("canvas");
+ var w = parseInt(matches[1]);
+ var stride = parseInt(matches[2]);
+ var h = parseInt(matches[3]);
+ canvas.width = w;
+ canvas.height = h;
+
+ // TODO handle stride
+
+ var binary_string = window.atob(matches[4]);
+ var len = binary_string.length;
+ var bytes = new Uint8Array(len);
+ var decoded = new Uint8Array(stride * h);
+ for (var i = 0; i < len; i++) {
+ var ascii = binary_string.charCodeAt(i);
+ bytes[i] = ascii;
+ }
+
+ var ctxt = canvas.getContext("2d");
+ var out = ctxt.createImageData(w, h);
+ // This is actually undefined throughout the tree and it isn't clear what it
+ // should be. Since this is only development code, leave it alone for now.
+ // eslint-disable-next-line no-undef
+ LZ4_uncompressChunk(bytes, decoded);
+
+ for (var x = 0; x < w; x++) {
+ for (var y = 0; y < h; y++) {
+ out.data[4 * x + 4 * y * w + 0] = decoded[4 * x + y * stride + 2];
+ out.data[4 * x + 4 * y * w + 1] = decoded[4 * x + y * stride + 1];
+ out.data[4 * x + 4 * y * w + 2] = decoded[4 * x + y * stride + 0];
+ out.data[4 * x + 4 * y * w + 3] = decoded[4 * x + y * stride + 3];
+ }
+ }
+
+ ctxt.putImageData(out, 0, 0);
+ return canvas.toDataURL();
+}
+
+function parseLayers(layersDumpLines) {
+ function parseMatrix2x3(str) {
+ str = trim(str);
+
+ // Something like '[ 1 0; 0 1; 0 158; ]'
+ var matches = str.match("^\\[ (.*?) (.*?); (.*?) (.*?); (.*?) (.*?); \\]$");
+ if (!matches) {
+ return null;
+ }
+
+ var matrix = [
+ [parseFloat(matches[1]), parseFloat(matches[2])],
+ [parseFloat(matches[3]), parseFloat(matches[4])],
+ [parseFloat(matches[5]), parseFloat(matches[6])],
+ ];
+
+ return matrix;
+ }
+ function parseColor(str) {
+ str = trim(str);
+
+ // Something like 'rgba(0, 0, 0, 0)'
+ var colorMatches = str.match("^rgba\\((.*), (.*), (.*), (.*)\\)$");
+ if (!colorMatches) {
+ return null;
+ }
+
+ var color = {
+ r: colorMatches[1],
+ g: colorMatches[2],
+ b: colorMatches[3],
+ a: colorMatches[4],
+ };
+ return color;
+ }
+ function parseFloat_cleo(str) {
+ str = trim(str);
+
+ // Something like 2.000
+ if (parseFloat(str) == str) {
+ return parseFloat(str);
+ }
+
+ return null;
+ }
+ function parseRect2D(str) {
+ str = trim(str);
+
+ // Something like '(x=0, y=0, w=2842, h=158)'
+ var rectMatches = str.match("^\\(x=(.*?), y=(.*?), w=(.*?), h=(.*?)\\)$");
+ if (!rectMatches) {
+ return null;
+ }
+
+ var rect = [
+ parseFloat(rectMatches[1]),
+ parseFloat(rectMatches[2]),
+ parseFloat(rectMatches[3]),
+ parseFloat(rectMatches[4]),
+ ];
+ return rect;
+ }
+ function parseRegion(str) {
+ str = trim(str);
+
+ // Something like '< (x=0, y=0, w=2842, h=158); (x=0, y=1718, w=2842, h=500); >'
+ if (str.charAt(0) != "<" || str.charAt(str.length - 1) != ">") {
+ return null;
+ }
+
+ var region = [];
+ str = trim(str.substring(1, str.length - 1));
+ while (str != "") {
+ var rectMatches = str.match(
+ "^\\(x=(.*?), y=(.*?), w=(.*?), h=(.*?)\\);(.*)$"
+ );
+ if (!rectMatches) {
+ return null;
+ }
+
+ var rect = [
+ parseFloat(rectMatches[1]),
+ parseFloat(rectMatches[2]),
+ parseFloat(rectMatches[3]),
+ parseFloat(rectMatches[4]),
+ ];
+ str = trim(rectMatches[5]);
+ region.push(rect);
+ }
+ return region;
+ }
+
+ var LAYERS_LINE_REGEX = "(\\s*)(\\w+)\\s\\((\\w+)\\)(.*)";
+
+ var root;
+ var objectAtIndentation = [];
+ for (var i = 0; i < layersDumpLines.length; i++) {
+ // Something like 'ThebesLayerComposite (0x12104cc00) [shadow-visible=< (x=0, y=0, w=1920, h=158); >] [visible=< (x=0, y=0, w=1920, h=158); >] [opaqueContent] [valid=< (x=0, y=0, w=1920, h=2218); >]'
+ var line = layersDumpLines[i].name || layersDumpLines[i];
+
+ var tileMatches = line.match("(\\s*)Tile \\(x=(.*), y=(.*)\\): (.*)");
+ if (tileMatches) {
+ let indentation = Math.floor(matches[1].length / 2);
+ var x = tileMatches[2];
+ var y = tileMatches[3];
+ var dataUri = tileMatches[4];
+ let parent = objectAtIndentation[indentation - 1];
+ var tiles = parent.tiles || {};
+
+ tiles[x] = tiles[x] || {};
+ tiles[x][y] = dataUri;
+
+ parent.tiles = tiles;
+
+ continue;
+ }
+
+ var surfaceMatches = line.match("(\\s*)Surface: (.*)");
+ if (surfaceMatches) {
+ let indentation = Math.floor(matches[1].length / 2);
+ let parent =
+ objectAtIndentation[indentation - 1] ||
+ objectAtIndentation[indentation - 2];
+
+ var surfaceURI = surfaceMatches[2];
+ if (parent.surfaceURI != null) {
+ console.log(
+ "error: surfaceURI already set for this layer " + parent.line
+ );
+ }
+ parent.surfaceURI = surfaceURI;
+
+ // Look for the buffer-rect offset
+ var contentHostLine =
+ layersDumpLines[i - 2].name || layersDumpLines[i - 2];
+ let matches = contentHostLine.match(LAYERS_LINE_REGEX);
+ if (matches) {
+ var contentHostRest = matches[4];
+ parent.contentHostProp = {};
+ parseProperties(contentHostRest, parent.contentHostProp);
+ }
+
+ continue;
+ }
+
+ var layerObject = {
+ line,
+ children: [],
+ };
+ if (!root) {
+ root = layerObject;
+ }
+
+ let matches = line.match(LAYERS_LINE_REGEX);
+ if (!matches) {
+ continue; // Something like a texturehost dump. Safe to ignore
+ }
+
+ if (
+ matches[2].includes("TiledContentHost") ||
+ matches[2].includes("ContentHost") ||
+ matches[2].includes("ContentClient") ||
+ matches[2].includes("MemoryTextureHost") ||
+ matches[2].includes("ImageHost")
+ ) {
+ continue; // We're already pretty good at visualizing these
+ }
+
+ var indentation = Math.floor(matches[1].length / 2);
+ objectAtIndentation[indentation] = layerObject;
+ for (var c = indentation + 1; c < objectAtIndentation.length; c++) {
+ objectAtIndentation[c] = null;
+ }
+ if (indentation > 0) {
+ var parent = objectAtIndentation[indentation - 1];
+ while (!parent) {
+ indentation--;
+ parent = objectAtIndentation[indentation - 1];
+ }
+
+ parent.children.push(layerObject);
+ }
+
+ layerObject.name = matches[2];
+ layerObject.address = matches[3];
+
+ var rest = matches[4];
+
+ function parseProperties(rest, layerObject) {
+ var fields = [];
+ var nesting = 0;
+ var startIndex;
+ for (let j = 0; j < rest.length; j++) {
+ if (rest.charAt(j) == "[") {
+ nesting++;
+ if (nesting == 1) {
+ startIndex = j;
+ }
+ } else if (rest.charAt(j) == "]") {
+ nesting--;
+ if (nesting == 0) {
+ fields.push(rest.substring(startIndex + 1, j));
+ }
+ }
+ }
+
+ for (let j = 0; j < fields.length; j++) {
+ // Something like 'valid=< (x=0, y=0, w=1920, h=2218); >' or 'opaqueContent'
+ var field = fields[j];
+ // dump("FIELD: " + field + "\n");
+ var parts = field.split("=", 2);
+ var fieldName = parts[0];
+ rest = field.substring(fieldName.length + 1);
+ if (parts.length == 1) {
+ layerObject[fieldName] = "true";
+ layerObject[fieldName].type = "bool";
+ continue;
+ }
+ var float = parseFloat_cleo(rest);
+ if (float) {
+ layerObject[fieldName] = float;
+ layerObject[fieldName].type = "float";
+ continue;
+ }
+ var region = parseRegion(rest);
+ if (region) {
+ layerObject[fieldName] = region;
+ layerObject[fieldName].type = "region";
+ continue;
+ }
+ var rect = parseRect2D(rest);
+ if (rect) {
+ layerObject[fieldName] = rect;
+ layerObject[fieldName].type = "rect2d";
+ continue;
+ }
+ var matrix = parseMatrix2x3(rest);
+ if (matrix) {
+ layerObject[fieldName] = matrix;
+ layerObject[fieldName].type = "matrix2x3";
+ continue;
+ }
+ var color = parseColor(rest);
+ if (color) {
+ layerObject[fieldName] = color;
+ layerObject[fieldName].type = "color";
+ continue;
+ }
+ if (rest[0] == "{" && rest[rest.length - 1] == "}") {
+ var object = {};
+ parseProperties(rest.substring(1, rest.length - 2).trim(), object);
+ layerObject[fieldName] = object;
+ layerObject[fieldName].type = "object";
+ continue;
+ }
+ fieldName = fieldName.split(" ")[0];
+ layerObject[fieldName] = rest[0];
+ layerObject[fieldName].type = "string";
+ }
+ }
+ parseProperties(rest, layerObject);
+
+ if (!layerObject["shadow-transform"]) {
+ // No shadow transform = identify
+ layerObject["shadow-transform"] = [
+ [1, 0],
+ [0, 1],
+ [0, 0],
+ ];
+ }
+
+ // Compute screenTransformX/screenTransformY
+ // TODO Fully support transforms
+ if (layerObject["shadow-transform"] && layerObject.transform) {
+ layerObject["screen-transform"] = [
+ layerObject["shadow-transform"][2][0],
+ layerObject["shadow-transform"][2][1],
+ ];
+ var currIndentation = indentation - 1;
+ while (currIndentation >= 0) {
+ var transform =
+ objectAtIndentation[currIndentation]["shadow-transform"] ||
+ objectAtIndentation[currIndentation].transform;
+ if (transform) {
+ layerObject["screen-transform"][0] += transform[2][0];
+ layerObject["screen-transform"][1] += transform[2][1];
+ }
+ currIndentation--;
+ }
+ }
+
+ // dump("Fields: " + JSON.stringify(fields) + "\n");
+ }
+ root.compositeTime = layersDumpLines.compositeTime;
+ // dump("OBJECTS: " + JSON.stringify(root) + "\n");
+ return root;
+}
+function populateLayers(
+ root,
+ displayList,
+ pane,
+ previewParent,
+ hasSeenRoot,
+ contentScale,
+ rootPreviewParent
+) {
+ contentScale = contentScale || 1;
+ rootPreviewParent = rootPreviewParent || previewParent;
+
+ function getDisplayItemForLayer(displayList) {
+ var items = [];
+ if (!displayList) {
+ return items;
+ }
+ if (displayList.layer == root.address) {
+ items.push(displayList);
+ }
+ for (var i = 0; i < displayList.children.length; i++) {
+ var subDisplayItems = getDisplayItemForLayer(displayList.children[i]);
+ for (let j = 0; j < subDisplayItems.length; j++) {
+ items.push(subDisplayItems[j]);
+ }
+ }
+ return items;
+ }
+ var elem = createElement("div", {
+ className: "layerObjectDescription",
+ textContent: root.line,
+ style: {
+ whiteSpace: "pre",
+ },
+ onmouseover() {
+ if (this.layerViewport) {
+ this.layerViewport.classList.add("layerHover");
+ }
+ },
+ onmouseout() {
+ if (this.layerViewport) {
+ this.layerViewport.classList.remove("layerHover");
+ }
+ },
+ });
+ var icon = createElement("img", {
+ src: "show.png",
+ style: {
+ width: "12px",
+ height: "12px",
+ marginLeft: "4px",
+ marginRight: "4px",
+ cursor: "pointer",
+ },
+ onclick() {
+ if (this.layerViewport) {
+ if (this.layerViewport.style.visibility == "hidden") {
+ this.layerViewport.style.visibility = "";
+ this.src = "show.png";
+ } else {
+ this.layerViewport.style.visibility = "hidden";
+ this.src = "hide.png";
+ }
+ }
+ },
+ });
+ elem.insertBefore(icon, elem.firstChild);
+ pane.appendChild(elem);
+
+ if (root["shadow-visible"] || root.visible) {
+ var visibleRegion = root["shadow-visible"] || root.visible;
+ var layerViewport = createElement("div", {
+ id: root.address + "_viewport",
+ style: {
+ position: "absolute",
+ pointerEvents: "none",
+ },
+ });
+ elem.layerViewport = layerViewport;
+ icon.layerViewport = layerViewport;
+ var layerViewportMatrix = [1, 0, 0, 1, 0, 0];
+ if (root["shadow-clip"] || root.clip) {
+ var clip = root["shadow-clip"] || root.clip;
+ var clipElem = createElement("div", {
+ id: root.address + "_clip",
+ style: {
+ left: clip[0] + "px",
+ top: clip[1] + "px",
+ width: clip[2] + "px",
+ height: clip[3] + "px",
+ position: "absolute",
+ overflow: "hidden",
+ pointerEvents: "none",
+ },
+ });
+ layerViewportMatrix[4] += -clip[0];
+ layerViewportMatrix[5] += -clip[1];
+ layerViewport.style.transform =
+ "translate(-" + clip[0] + "px, -" + clip[1] + "px)";
+ }
+ if (root["shadow-transform"] || root.transform) {
+ var matrix = root["shadow-transform"] || root.transform;
+ layerViewportMatrix[0] = matrix[0][0];
+ layerViewportMatrix[1] = matrix[0][1];
+ layerViewportMatrix[2] = matrix[1][0];
+ layerViewportMatrix[3] = matrix[1][1];
+ layerViewportMatrix[4] += matrix[2][0];
+ layerViewportMatrix[5] += matrix[2][1];
+ }
+ layerViewport.style.transform =
+ "matrix(" +
+ layerViewportMatrix[0] +
+ "," +
+ layerViewportMatrix[1] +
+ "," +
+ layerViewportMatrix[2] +
+ "," +
+ layerViewportMatrix[3] +
+ "," +
+ layerViewportMatrix[4] +
+ "," +
+ layerViewportMatrix[5] +
+ ")";
+ if (!hasSeenRoot) {
+ hasSeenRoot = true;
+ layerViewport.style.transform =
+ "scale(" + 1 / contentScale + "," + 1 / contentScale + ")";
+ }
+ if (clipElem) {
+ previewParent.appendChild(clipElem);
+ clipElem.appendChild(layerViewport);
+ } else {
+ previewParent.appendChild(layerViewport);
+ }
+ previewParent = layerViewport;
+ for (let i = 0; i < visibleRegion.length; i++) {
+ let rect2d = visibleRegion[i];
+ var layerPreview = createElement("div", {
+ id: root.address + "_visible_part" + i + "-" + visibleRegion.length,
+ className: "layerPreview",
+ style: {
+ position: "absolute",
+ left: rect2d[0] + "px",
+ top: rect2d[1] + "px",
+ width: rect2d[2] + "px",
+ height: rect2d[3] + "px",
+ overflow: "hidden",
+ border: "solid 1px black",
+ background:
+ 'url("noise.png"), linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.2))',
+ },
+ });
+ layerViewport.appendChild(layerPreview);
+
+ function isInside(rect1, rect2) {
+ if (
+ rect1[0] + rect1[2] < rect2[0] &&
+ rect2[0] + rect2[2] < rect1[0] &&
+ rect1[1] + rect1[3] < rect2[1] &&
+ rect2[1] + rect2[3] < rect1[1]
+ ) {
+ return true;
+ }
+ return true;
+ }
+
+ var hasImg = false;
+ // Add tile img objects for this part
+ var previewOffset = rect2d;
+
+ if (root.tiles) {
+ hasImg = true;
+ for (var x in root.tiles) {
+ for (var y in root.tiles[x]) {
+ if (isInside(rect2d, [x, y, 512, 512])) {
+ var tileImgElem = createElement("img", {
+ src: getDataURI(root.tiles[x][y]),
+ style: {
+ position: "absolute",
+ left: x - previewOffset[0] + "px",
+ top: y - previewOffset[1] + "px",
+ pointerEvents: "auto",
+ },
+ });
+ layerPreview.appendChild(tileImgElem);
+ }
+ }
+ }
+ layerPreview.style.background = "";
+ } else if (root.surfaceURI) {
+ hasImg = true;
+ var offsetX = 0;
+ var offsetY = 0;
+ if (root.contentHostProp && root.contentHostProp["buffer-rect"]) {
+ offsetX = root.contentHostProp["buffer-rect"][0];
+ offsetY = root.contentHostProp["buffer-rect"][1];
+ }
+ var surfaceImgElem = createElement("img", {
+ src: getDataURI(root.surfaceURI),
+ style: {
+ position: "absolute",
+ left: offsetX - previewOffset[0] + "px",
+ top: offsetY - previewOffset[1] + "px",
+ pointerEvents: "auto",
+ },
+ });
+ layerPreview.appendChild(surfaceImgElem);
+ layerPreview.style.background = "";
+ } else if (root.color) {
+ hasImg = true;
+ layerPreview.style.background =
+ "rgba(" +
+ root.color.r +
+ ", " +
+ root.color.g +
+ ", " +
+ root.color.b +
+ ", " +
+ root.color.a +
+ ")";
+ }
+
+ if (hasImg || true) {
+ layerPreview.mouseoverElem = elem;
+ layerPreview.onmouseenter = function () {
+ this.mouseoverElem.onmouseover();
+ };
+ layerPreview.onmouseout = function () {
+ this.mouseoverElem.onmouseout();
+ };
+ }
+ }
+
+ var layerDisplayItems = getDisplayItemForLayer(displayList);
+ for (let i = 0; i < layerDisplayItems.length; i++) {
+ var displayItem = layerDisplayItems[i];
+ var displayElem = createElement("div", {
+ className: "layerObjectDescription",
+ textContent: " " + trim(displayItem.line),
+ style: {
+ whiteSpace: "pre",
+ },
+ displayItem,
+ layerViewport,
+ onmouseover() {
+ if (this.diPreview) {
+ this.diPreview.classList.add("displayHover");
+
+ var description = "";
+ if (this.displayItem.contentDescriptor) {
+ description += "Content: " + this.displayItem.contentDescriptor;
+ } else {
+ description += "Content: Unknown";
+ }
+ description +=
+ "<br>Item: " +
+ this.displayItem.name +
+ " (" +
+ this.displayItem.address +
+ ")";
+ description +=
+ "<br>Layer: " + root.name + " (" + root.address + ")";
+ if (this.displayItem.frame) {
+ description += "<br>Frame: " + this.displayItem.frame;
+ }
+ if (this.displayItem.layerBounds) {
+ description +=
+ "<br>Bounds: [" +
+ toFixed(this.displayItem.layerBounds[0] / 60, 2) +
+ ", " +
+ toFixed(this.displayItem.layerBounds[1] / 60, 2) +
+ ", " +
+ toFixed(this.displayItem.layerBounds[2] / 60, 2) +
+ ", " +
+ toFixed(this.displayItem.layerBounds[3] / 60, 2) +
+ "] (CSS Pixels)";
+ }
+ if (this.displayItem.z) {
+ description += "<br>Z: " + this.displayItem.z;
+ }
+ // At the end
+ if (this.displayItem.rest) {
+ description += "<br>" + this.displayItem.rest;
+ }
+
+ var box = this.diPreview.getBoundingClientRect();
+ this.diPreview.tooltip = createElement("div", {
+ className: "csstooltip",
+ innerHTML: description,
+ style: {
+ top:
+ Math.min(
+ box.bottom,
+ document.documentElement.clientHeight - 150
+ ) + "px",
+ left: box.left + "px",
+ },
+ });
+
+ document.body.appendChild(this.diPreview.tooltip);
+ }
+ },
+ onmouseout() {
+ if (this.diPreview) {
+ this.diPreview.classList.remove("displayHover");
+ document.body.removeChild(this.diPreview.tooltip);
+ }
+ },
+ });
+
+ icon = createElement("img", {
+ style: {
+ width: "12px",
+ height: "12px",
+ marginLeft: "4px",
+ marginRight: "4px",
+ },
+ });
+ displayElem.insertBefore(icon, displayElem.firstChild);
+ pane.appendChild(displayElem);
+ // bounds doesn't adjust for within the layer. It's not a bad fallback but
+ // will have the wrong offset
+ let rect2d = displayItem.layerBounds || displayItem.bounds;
+ if (rect2d) {
+ // This doesn't place them corectly
+ var appUnitsToPixels = 60 / contentScale;
+ let diPreview = createElement("div", {
+ id: "displayitem_" + displayItem.content + "_" + displayItem.address,
+ className: "layerPreview",
+ style: {
+ position: "absolute",
+ left: rect2d[0] / appUnitsToPixels + "px",
+ top: rect2d[1] / appUnitsToPixels + "px",
+ width: rect2d[2] / appUnitsToPixels + "px",
+ height: rect2d[3] / appUnitsToPixels + "px",
+ border: "solid 1px gray",
+ pointerEvents: "auto",
+ },
+ displayElem,
+ onmouseover() {
+ this.displayElem.onmouseover();
+ },
+ onmouseout() {
+ this.displayElem.onmouseout();
+ },
+ });
+
+ layerViewport.appendChild(diPreview);
+ displayElem.diPreview = diPreview;
+ }
+ }
+ }
+
+ for (var i = 0; i < root.children.length; i++) {
+ populateLayers(
+ root.children[i],
+ displayList,
+ pane,
+ previewParent,
+ hasSeenRoot,
+ contentScale,
+ rootPreviewParent
+ );
+ }
+}
+
+// This function takes a stdout snippet and finds the frames
+function parseMultiLineDump(log) {
+ var container = createElement("div", {
+ style: {
+ height: "100%",
+ position: "relative",
+ },
+ });
+
+ var layerManagerFirstLine = "[a-zA-Z]*LayerManager \\(.*$\n";
+ var nextLineStartWithSpace = "([ \\t].*$\n)*";
+ var layersRegex = "(" + layerManagerFirstLine + nextLineStartWithSpace + ")";
+
+ var startLine = "Painting --- after optimization:\n";
+ var endLine = "Painting --- layer tree:";
+ var displayListRegex = "(" + startLine + "(.*\n)*?" + endLine + ")";
+
+ var regex = new RegExp(layersRegex + "|" + displayListRegex, "gm");
+ var matches = log.match(regex);
+ console.log(matches);
+ window.matches = matches;
+
+ var matchList = createElement("span", {
+ style: {
+ height: "95%",
+ width: "10%",
+ position: "relative",
+ border: "solid black 2px",
+ display: "inline-block",
+ float: "left",
+ overflow: "auto",
+ },
+ });
+ container.appendChild(matchList);
+ var contents = createElement("span", {
+ style: {
+ height: "95%",
+ width: "88%",
+ display: "inline-block",
+ },
+ textContent: "Click on a frame on the left to view the layer tree",
+ });
+ container.appendChild(contents);
+
+ var lastDisplayList = null;
+ var frameID = 1;
+ for (let i = 0; i < matches.length; i++) {
+ var currMatch = matches[i];
+
+ if (currMatch.indexOf(startLine) == 0) {
+ // Display list match
+ var matchLines = matches[i].split("\n");
+ lastDisplayList = parseDisplayList(matchLines);
+ } else {
+ // Layer tree match:
+ let displayList = lastDisplayList;
+ lastDisplayList = null;
+ var currFrameDiv = createElement("a", {
+ style: {
+ padding: "3px",
+ display: "block",
+ },
+ href: "#",
+ textContent: "LayerTree " + frameID++,
+ onclick() {
+ contents.innerHTML = "";
+ var matchLines = matches[i].split("\n");
+ var dumpDiv = parseDump(matchLines, displayList);
+ contents.appendChild(dumpDiv);
+ },
+ });
+ matchList.appendChild(currFrameDiv);
+ }
+ }
+
+ return container;
+}
+
+function parseDump(log, displayList, compositeTitle, compositeTime) {
+ compositeTitle |= "";
+ compositeTime |= 0;
+
+ var container = createElement("div", {
+ style: {
+ background: "white",
+ height: "100%",
+ position: "relative",
+ },
+ });
+
+ if (compositeTitle == null && compositeTime == null) {
+ var titleDiv = createElement("div", {
+ className: "treeColumnHeader",
+ style: {
+ width: "100%",
+ },
+ textContent:
+ compositeTitle +
+ (compositeTitle ? " (near " + compositeTime.toFixed(0) + " ms)" : ""),
+ });
+ container.appendChild(titleDiv);
+ }
+
+ var mainDiv = createElement("div", {
+ style: {
+ position: "absolute",
+ top: "16px",
+ left: "0px",
+ right: "0px",
+ bottom: "0px",
+ },
+ });
+ container.appendChild(mainDiv);
+
+ var layerListPane = createElement("div", {
+ style: {
+ cssFloat: "left",
+ height: "100%",
+ width: "300px",
+ overflowY: "scroll",
+ },
+ });
+ mainDiv.appendChild(layerListPane);
+
+ var previewDiv = createElement("div", {
+ style: {
+ position: "absolute",
+ left: "300px",
+ right: "0px",
+ top: "0px",
+ bottom: "0px",
+ overflow: "auto",
+ },
+ });
+ mainDiv.appendChild(previewDiv);
+
+ var root = parseLayers(log);
+ populateLayers(root, displayList, layerListPane, previewDiv);
+ return container;
+}
diff --git a/gfx/layers/layerviewer/noise.png b/gfx/layers/layerviewer/noise.png
new file mode 100644
index 0000000000..01d340aaa9
--- /dev/null
+++ b/gfx/layers/layerviewer/noise.png
Binary files differ
diff --git a/gfx/layers/layerviewer/show.png b/gfx/layers/layerviewer/show.png
new file mode 100644
index 0000000000..7038b660c8
--- /dev/null
+++ b/gfx/layers/layerviewer/show.png
Binary files differ
diff --git a/gfx/layers/layerviewer/tree.css b/gfx/layers/layerviewer/tree.css
new file mode 100644
index 0000000000..f4ff6eb57b
--- /dev/null
+++ b/gfx/layers/layerviewer/tree.css
@@ -0,0 +1,37 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+html, body {
+ height: 100%;
+ overflow: hidden;
+}
+.layerObjectDescription:hover {
+ background-color: #E8E8E8;
+}
+
+.layerHover > .layerPreview::after {
+ position: absolute;
+ top: 0; right: 0; bottom: 0; left: 0;
+ content: "";
+ background-color: rgba(0,0,0,0.2);
+ box-shadow: -2px 2px 0 #FFF;
+}
+
+@keyframes layerHoverAnimation {
+ 0% { transform: scale(1); }
+ 50% { transform: scale(1.2); }
+ 100% { transform: scale(1); }
+}
+
+.displayHover {
+ background: rgba(0, 128, 0, 0.8);
+}
+
+.layerHover > .layerPreview {
+ animation: layerHoverAnimation 200ms;
+ background: gold !important;
+ box-shadow: 10px 10px 5px #888888;
+ border-color: blue !important;
+ z-index: 10;
+}
diff --git a/gfx/layers/moz.build b/gfx/layers/moz.build
new file mode 100644
index 0000000000..c5345e22d2
--- /dev/null
+++ b/gfx/layers/moz.build
@@ -0,0 +1,520 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+with Files("**"):
+ BUG_COMPONENT = ("Core", "Graphics")
+
+with Files("apz/**"):
+ BUG_COMPONENT = ("Core", "Panning and Zooming")
+
+EXPORTS += [
+ "composite/CompositableHost.h",
+ "CompositorTypes.h",
+ "D3D9SurfaceImage.h",
+ "FrameMetrics.h",
+ "GLImages.h",
+ "GPUVideoImage.h",
+ "ImageContainer.h",
+ "ImageTypes.h",
+ "IMFYCbCrImage.h",
+ "LayersTypes.h",
+ "LayerUserData.h",
+ "opengl/OGLShaderConfig.h",
+ "opengl/OGLShaderProgram.h",
+]
+
+if CONFIG["MOZ_WIDGET_TOOLKIT"] == "windows":
+ SOURCES += [
+ "D3D11ShareHandleImage.cpp",
+ "D3D11TextureIMFSampleImage.cpp",
+ "D3D11YCbCrImage.cpp",
+ ]
+ UNIFIED_SOURCES += [
+ "D3D9SurfaceImage.cpp",
+ "IMFYCbCrImage.cpp",
+ ]
+ if CONFIG["MOZ_ENABLE_D3D10_LAYER"]:
+ EXPORTS.mozilla.layers += [
+ "d3d11/CompositorD3D11.h",
+ "d3d11/DeviceAttachmentsD3D11.h",
+ "d3d11/HelpersD3D11.h",
+ "d3d11/ShaderDefinitionsD3D11.h",
+ "d3d11/TextureD3D11.h",
+ ]
+ UNIFIED_SOURCES += [
+ "d3d11/TextureD3D11.cpp",
+ ]
+ SOURCES += [
+ "d3d11/CompositorD3D11.cpp",
+ "d3d11/DeviceAttachmentsD3D11.cpp",
+ ]
+ if CONFIG["MOZ_WMF_MEDIA_ENGINE"]:
+ EXPORTS.mozilla.layers += [
+ "DcompSurfaceImage.h",
+ ]
+ UNIFIED_SOURCES += [
+ "DcompSurfaceImage.cpp",
+ ]
+
+EXPORTS.gfxipc += [
+ "ipc/ShadowLayerUtils.h",
+ "ipc/SurfaceDescriptor.h",
+]
+
+EXPORTS.mozilla.dom += [
+ "apz/util/CheckerboardReportService.h",
+]
+
+EXPORTS.mozilla.gfx += [
+ "BuildConstants.h",
+]
+
+EXPORTS.mozilla.layers += [
+ "AnimationHelper.h",
+ "AnimationInfo.h",
+ "AnimationStorageData.h",
+ "apz/public/APZInputBridge.h",
+ "apz/public/APZPublicUtils.h",
+ "apz/public/APZSampler.h",
+ "apz/public/APZUpdater.h",
+ "apz/public/CompositorController.h",
+ "apz/public/GeckoContentController.h",
+ "apz/public/GeckoContentControllerTypes.h",
+ "apz/public/IAPZCTreeManager.h",
+ "apz/public/MatrixMessage.h",
+ # exporting things from apz/src is temporary until we extract a
+ # proper interface for the code there
+ "apz/src/APZUtils.h",
+ "apz/src/AsyncDragMetrics.h",
+ "apz/src/FocusTarget.h",
+ "apz/src/KeyboardMap.h",
+ "apz/src/KeyboardScrollAction.h",
+ "apz/testutil/APZTestData.h",
+ "apz/util/ActiveElementManager.h",
+ "apz/util/APZCCallbackHelper.h",
+ "apz/util/APZEventState.h",
+ "apz/util/APZTaskRunnable.h",
+ "apz/util/APZThreadUtils.h",
+ "apz/util/ChromeProcessController.h",
+ "apz/util/ContentProcessController.h",
+ "apz/util/DoubleTapToZoom.h",
+ "apz/util/InputAPZContext.h",
+ "apz/util/ScrollingInteractionContext.h",
+ "apz/util/ScrollLinkedEffectDetector.h",
+ "apz/util/TouchActionHelper.h",
+ "apz/util/TouchCounter.h",
+ "AtomicRefCountedWithFinalize.h",
+ "AxisPhysicsModel.h",
+ "AxisPhysicsMSDModel.h",
+ "BSPTree.h",
+ "BufferTexture.h",
+ "BuildConstants.h",
+ "CanvasDrawEventRecorder.h",
+ "CanvasRenderer.h",
+ "client/CanvasClient.h",
+ "client/CompositableClient.h",
+ "client/GPUVideoTextureClient.h",
+ "client/ImageClient.h",
+ "client/TextureClient.h",
+ "client/TextureClientPool.h",
+ "client/TextureClientRecycleAllocator.h",
+ "client/TextureClientSharedSurface.h",
+ "client/TextureRecorded.h",
+ "composite/Diagnostics.h",
+ "composite/FrameUniformityData.h",
+ "composite/GPUVideoTextureHost.h",
+ "composite/ImageComposite.h",
+ "composite/RemoteTextureHostWrapper.h",
+ "composite/TextureHost.h",
+ "CompositionRecorder.h",
+ "Compositor.h",
+ "CompositorAnimationStorage.h",
+ "CompositorTypes.h",
+ "D3D11ShareHandleImage.h",
+ "D3D11TextureIMFSampleImage.h",
+ "D3D11YCbCrImage.h",
+ "D3D9SurfaceImage.h",
+ "DirectionUtils.h",
+ "Effects.h",
+ "ImageDataSerializer.h",
+ "ipc/APZChild.h",
+ "ipc/APZCTreeManagerChild.h",
+ "ipc/APZCTreeManagerParent.h",
+ "ipc/APZInputBridgeChild.h",
+ "ipc/APZInputBridgeParent.h",
+ "ipc/CanvasChild.h",
+ "ipc/CanvasThread.h",
+ "ipc/CanvasTranslator.h",
+ "ipc/CompositableForwarder.h",
+ "ipc/CompositableTransactionParent.h",
+ "ipc/CompositorBridgeChild.h",
+ "ipc/CompositorBridgeParent.h",
+ "ipc/CompositorManagerChild.h",
+ "ipc/CompositorManagerParent.h",
+ "ipc/CompositorThread.h",
+ "ipc/CompositorVsyncScheduler.h",
+ "ipc/CompositorVsyncSchedulerOwner.h",
+ "ipc/ContentCompositorBridgeParent.h",
+ "ipc/ImageBridgeChild.h",
+ "ipc/ImageBridgeParent.h",
+ "ipc/ISurfaceAllocator.h",
+ "ipc/KnowsCompositor.h",
+ "ipc/LayersMessageUtils.h",
+ "ipc/LayerTreeOwnerTracker.h",
+ "ipc/RefCountedShmem.h",
+ "ipc/RemoteContentController.h",
+ "ipc/SharedPlanarYCbCrImage.h",
+ "ipc/SharedRGBImage.h",
+ "ipc/SharedSurfacesChild.h",
+ "ipc/SharedSurfacesMemoryReport.h",
+ "ipc/SharedSurfacesParent.h",
+ "ipc/SynchronousTask.h",
+ "ipc/TextureForwarder.h",
+ "ipc/UiCompositorControllerChild.h",
+ "ipc/UiCompositorControllerMessageTypes.h",
+ "ipc/UiCompositorControllerParent.h",
+ "ipc/VideoBridgeChild.h",
+ "ipc/VideoBridgeParent.h",
+ "ipc/VideoBridgeUtils.h",
+ "LayersTypes.h",
+ "MemoryPressureObserver.h",
+ "NativeLayer.h",
+ "OOPCanvasRenderer.h",
+ "opengl/CompositingRenderTargetOGL.h",
+ "opengl/CompositorOGL.h",
+ "opengl/MacIOSurfaceTextureClientOGL.h",
+ "opengl/MacIOSurfaceTextureHostOGL.h",
+ "opengl/TextureClientOGL.h",
+ "opengl/TextureHostOGL.h",
+ "PersistentBufferProvider.h",
+ "ProfilerScreenshots.h",
+ "RemoteTextureMap.h",
+ "RepaintRequest.h",
+ "SampleTime.h",
+ "ScreenshotGrabber.h",
+ "ScrollableLayerGuid.h",
+ "ScrollbarData.h",
+ "ShareableCanvasRenderer.h",
+ "SourceSurfaceSharedData.h",
+ "SurfacePool.h",
+ "SyncObject.h",
+ "TextureSourceProvider.h",
+ "TextureWrapperImage.h",
+ "TransactionIdAllocator.h",
+ "TreeTraversal.h",
+ "UpdateImageHelper.h",
+ "wr/AsyncImagePipelineManager.h",
+ "wr/ClipManager.h",
+ "wr/DisplayItemCache.h",
+ "wr/HitTestInfoManager.h",
+ "wr/IpcResourceUpdateQueue.h",
+ "wr/OMTAController.h",
+ "wr/OMTASampler.h",
+ "wr/RenderRootStateManager.h",
+ "wr/RenderRootTypes.h",
+ "wr/StackingContextHelper.h",
+ "wr/WebRenderBridgeChild.h",
+ "wr/WebRenderBridgeParent.h",
+ "wr/WebRenderCanvasRenderer.h",
+ "wr/WebRenderCommandBuilder.h",
+ "wr/WebRenderDrawEventRecorder.h",
+ "wr/WebRenderImageHost.h",
+ "wr/WebRenderLayerManager.h",
+ "wr/WebRenderMessageUtils.h",
+ "wr/WebRenderScrollData.h",
+ "wr/WebRenderScrollDataWrapper.h",
+ "wr/WebRenderTextureHost.h",
+ "wr/WebRenderUserData.h",
+ "ZoomConstraints.h",
+]
+
+if CONFIG["MOZ_WAYLAND"]:
+ EXPORTS.mozilla.layers += [
+ "DMABUFSurfaceImage.h",
+ "NativeLayerWayland.h",
+ "opengl/DMABUFTextureClientOGL.h",
+ "opengl/DMABUFTextureHostOGL.h",
+ "SurfacePoolWayland.h",
+ ]
+ UNIFIED_SOURCES += [
+ "NativeLayerWayland.cpp",
+ "SurfacePoolWayland.cpp",
+ ]
+ SOURCES += [
+ "DMABUFSurfaceImage.cpp",
+ "opengl/DMABUFTextureClientOGL.cpp",
+ "opengl/DMABUFTextureHostOGL.cpp",
+ ]
+
+if CONFIG["MOZ_WIDGET_TOOLKIT"] == "cocoa":
+ EXPORTS.mozilla.layers += [
+ "NativeLayerCA.h",
+ "SurfacePoolCA.h",
+ ]
+ EXPORTS += [
+ "MacIOSurfaceHelpers.h",
+ "MacIOSurfaceImage.h",
+ ]
+ UNIFIED_SOURCES += [
+ "NativeLayerCA.mm",
+ "SurfacePoolCA.mm",
+ ]
+ SOURCES += [
+ "MacIOSurfaceHelpers.cpp",
+ "MacIOSurfaceImage.cpp",
+ ]
+ OS_LIBS += [
+ "-framework IOSurface",
+ ]
+
+if CONFIG["MOZ_WIDGET_TOOLKIT"] == "android":
+ EXPORTS.mozilla.layers += [
+ "AndroidHardwareBuffer.h",
+ ]
+ UNIFIED_SOURCES += [
+ "AndroidHardwareBuffer.cpp",
+ "apz/src/AndroidAPZ.cpp",
+ "apz/src/AndroidFlingPhysics.cpp",
+ "apz/src/AndroidVelocityTracker.cpp",
+ ]
+
+UNIFIED_SOURCES += [
+ "AnimationHelper.cpp",
+ "AnimationInfo.cpp",
+ "apz/src/APZCTreeManager.cpp",
+ "apz/src/APZInputBridge.cpp",
+ "apz/src/APZPublicUtils.cpp",
+ "apz/src/APZSampler.cpp",
+ "apz/src/APZUpdater.cpp",
+ "apz/src/APZUtils.cpp",
+ "apz/src/AsyncPanZoomController.cpp",
+ "apz/src/AutoscrollAnimation.cpp",
+ "apz/src/Axis.cpp",
+ "apz/src/CheckerboardEvent.cpp",
+ "apz/src/DragTracker.cpp",
+ "apz/src/ExpectedGeckoMetrics.cpp",
+ "apz/src/FlingAccelerator.cpp",
+ "apz/src/FocusState.cpp",
+ "apz/src/FocusTarget.cpp",
+ "apz/src/GenericScrollAnimation.cpp",
+ "apz/src/GestureEventListener.cpp",
+ "apz/src/HitTestingTreeNode.cpp",
+ "apz/src/IAPZHitTester.cpp",
+ "apz/src/InputBlockState.cpp",
+ "apz/src/InputQueue.cpp",
+ "apz/src/KeyboardMap.cpp",
+ "apz/src/KeyboardScrollAction.cpp",
+ "apz/src/OverscrollHandoffState.cpp",
+ "apz/src/PotentialCheckerboardDurationTracker.cpp",
+ "apz/src/QueuedInput.cpp",
+ "apz/src/SampledAPZCState.cpp",
+ "apz/src/ScrollThumbUtils.cpp",
+ "apz/src/SimpleVelocityTracker.cpp",
+ "apz/src/SmoothMsdScrollAnimation.cpp",
+ "apz/src/SmoothScrollAnimation.cpp",
+ "apz/src/WheelScrollAnimation.cpp",
+ "apz/src/WRHitTester.cpp",
+ "apz/testutil/APZTestData.cpp",
+ "apz/util/ActiveElementManager.cpp",
+ "apz/util/APZCCallbackHelper.cpp",
+ "apz/util/APZEventState.cpp",
+ "apz/util/APZTaskRunnable.cpp",
+ "apz/util/APZThreadUtils.cpp",
+ "apz/util/CheckerboardReportService.cpp",
+ "apz/util/ChromeProcessController.cpp",
+ "apz/util/ContentProcessController.cpp",
+ "apz/util/DoubleTapToZoom.cpp",
+ "apz/util/InputAPZContext.cpp",
+ "apz/util/ScrollingInteractionContext.cpp",
+ "apz/util/ScrollLinkedEffectDetector.cpp",
+ "apz/util/TouchActionHelper.cpp",
+ "apz/util/TouchCounter.cpp",
+ "AxisPhysicsModel.cpp",
+ "AxisPhysicsMSDModel.cpp",
+ "BSPTree.cpp",
+ "BufferTexture.cpp",
+ "CanvasDrawEventRecorder.cpp",
+ "CanvasRenderer.cpp",
+ "client/CanvasClient.cpp",
+ "client/CompositableClient.cpp",
+ "client/GPUVideoTextureClient.cpp",
+ "client/ImageClient.cpp",
+ "client/TextureClientPool.cpp",
+ "client/TextureClientRecycleAllocator.cpp",
+ "client/TextureClientSharedSurface.cpp",
+ "client/TextureRecorded.cpp",
+ "composite/CompositableHost.cpp",
+ "composite/FrameUniformityData.cpp",
+ "composite/GPUVideoTextureHost.cpp",
+ "composite/ImageComposite.cpp",
+ "composite/RemoteTextureHostWrapper.cpp",
+ "CompositionRecorder.cpp",
+ "Compositor.cpp",
+ "CompositorAnimationStorage.cpp",
+ "CompositorTypes.cpp",
+ "Effects.cpp",
+ "FrameMetrics.cpp",
+ "GLImages.cpp",
+ "ImageDataSerializer.cpp",
+ "ipc/APZChild.cpp",
+ "ipc/APZCTreeManagerChild.cpp",
+ "ipc/APZCTreeManagerParent.cpp",
+ "ipc/APZInputBridgeChild.cpp",
+ "ipc/APZInputBridgeParent.cpp",
+ "ipc/CanvasChild.cpp",
+ "ipc/CanvasThread.cpp",
+ "ipc/CanvasTranslator.cpp",
+ "ipc/CompositableForwarder.cpp",
+ "ipc/CompositableTransactionParent.cpp",
+ "ipc/CompositorBench.cpp",
+ "ipc/CompositorBridgeChild.cpp",
+ "ipc/CompositorBridgeParent.cpp",
+ "ipc/CompositorManagerChild.cpp",
+ "ipc/CompositorManagerParent.cpp",
+ "ipc/CompositorThread.cpp",
+ "ipc/CompositorVsyncScheduler.cpp",
+ "ipc/ContentCompositorBridgeParent.cpp",
+ "ipc/ImageBridgeChild.cpp",
+ "ipc/ImageBridgeParent.cpp",
+ "ipc/ISurfaceAllocator.cpp",
+ "ipc/KnowsCompositor.cpp",
+ "ipc/LayerTreeOwnerTracker.cpp",
+ "ipc/RefCountedShmem.cpp",
+ "ipc/RemoteContentController.cpp",
+ "ipc/SharedPlanarYCbCrImage.cpp",
+ "ipc/SharedRGBImage.cpp",
+ "ipc/SharedSurfacesChild.cpp",
+ "ipc/SharedSurfacesParent.cpp",
+ "ipc/UiCompositorControllerChild.cpp",
+ "ipc/UiCompositorControllerParent.cpp",
+ "ipc/VideoBridgeChild.cpp",
+ "ipc/VideoBridgeParent.cpp",
+ "LayersTypes.cpp",
+ "MemoryPressureObserver.cpp",
+ "opengl/CompositingRenderTargetOGL.cpp",
+ "opengl/CompositorOGL.cpp",
+ "opengl/OGLShaderProgram.cpp",
+ "opengl/TextureClientOGL.cpp",
+ "opengl/TextureHostOGL.cpp",
+ "ProfilerScreenshots.cpp",
+ "RemoteTextureMap.cpp",
+ "RepaintRequest.cpp",
+ "SampleTime.cpp",
+ "ScreenshotGrabber.cpp",
+ "ScrollableLayerGuid.cpp",
+ "ShareableCanvasRenderer.cpp",
+ "SourceSurfaceSharedData.cpp",
+ "SyncObject.cpp",
+ "TextureSourceProvider.cpp",
+ "TextureWrapperImage.cpp",
+ "wr/AsyncImagePipelineManager.cpp",
+ "wr/ClipManager.cpp",
+ "wr/DisplayItemCache.cpp",
+ "wr/HitTestInfoManager.cpp",
+ "wr/IpcResourceUpdateQueue.cpp",
+ "wr/OMTAController.cpp",
+ "wr/OMTASampler.cpp",
+ "wr/RenderRootStateManager.cpp",
+ "wr/RenderRootTypes.cpp",
+ "wr/StackingContextHelper.cpp",
+ "wr/WebRenderBridgeChild.cpp",
+ "wr/WebRenderBridgeParent.cpp",
+ "wr/WebRenderCanvasRenderer.cpp",
+ "wr/WebRenderCommandBuilder.cpp",
+ "wr/WebRenderDrawEventRecorder.cpp",
+ "wr/WebRenderImageHost.cpp",
+ "wr/WebRenderLayerManager.cpp",
+ "wr/WebRenderScrollData.cpp",
+ "wr/WebRenderUserData.cpp",
+ "ZoomConstraints.cpp",
+ # XXX here are some unified build error.
+ #'wr/WebRenderTextureHost.cpp'
+]
+
+SOURCES += [
+ "client/TextureClient.cpp",
+ "composite/TextureHost.cpp",
+ "ImageContainer.cpp",
+ "PersistentBufferProvider.cpp",
+ "wr/WebRenderTextureHost.cpp",
+]
+
+DEFINES["GOOGLE_PROTOBUF_NO_RTTI"] = True
+DEFINES["GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER"] = True
+DEFINES["MOZ_APP_VERSION"] = CONFIG["MOZ_APP_VERSION"]
+
+if CONFIG["MOZ_WIDGET_TOOLKIT"] == "cocoa":
+ SOURCES += [
+ "opengl/MacIOSurfaceTextureClientOGL.cpp",
+ "opengl/MacIOSurfaceTextureHostOGL.cpp",
+ ]
+
+IPDL_SOURCES += [
+ "ipc/LayersMessages.ipdlh",
+ "ipc/LayersSurfaces.ipdlh",
+ "ipc/PAPZ.ipdl",
+ "ipc/PAPZCTreeManager.ipdl",
+ "ipc/PAPZInputBridge.ipdl",
+ "ipc/PCanvas.ipdl",
+ "ipc/PCompositorBridge.ipdl",
+ "ipc/PCompositorBridgeTypes.ipdlh",
+ "ipc/PCompositorManager.ipdl",
+ "ipc/PImageBridge.ipdl",
+ "ipc/PTexture.ipdl",
+ "ipc/PUiCompositorController.ipdl",
+ "ipc/PVideoBridge.ipdl",
+ "ipc/PWebRenderBridge.ipdl",
+ "ipc/WebRenderMessages.ipdlh",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+if CONFIG["COMPILE_ENVIRONMENT"] and CONFIG["MOZ_WIDGET_TOOLKIT"] == "windows":
+ GeneratedFile(
+ "CompositorD3D11Shaders.h",
+ script="d3d11/genshaders.py",
+ inputs=["d3d11/shaders.manifest"],
+ )
+
+LOCAL_INCLUDES += [
+ "/docshell/base", # for nsDocShell.h
+ "/dom/canvas", # for intertwined WebGL headers
+ "/gfx/cairo/cairo/src",
+ "/layout/base", # for TouchManager.h
+ "/layout/generic", # for nsTextFrame.h
+ "/media/libyuv/libyuv/include", # for libyuv.h
+]
+
+FINAL_LIBRARY = "xul"
+
+if CONFIG["MOZ_DEBUG"]:
+ DEFINES["D3D_DEBUG_INFO"] = True
+
+if CONFIG["MOZ_ENABLE_D3D10_LAYER"]:
+ DEFINES["MOZ_ENABLE_D3D10_LAYER"] = True
+
+if CONFIG["ENABLE_TESTS"]:
+ DIRS += ["apz/test/gtest"]
+ DIRS += ["apz/test/gtest/mvm"]
+
+MOCHITEST_MANIFESTS += ["apz/test/mochitest/mochitest.ini"]
+BROWSER_CHROME_MANIFESTS += ["apz/test/mochitest/browser.ini"]
+
+if CONFIG["MOZ_WIDGET_TOOLKIT"] == "gtk":
+ CXXFLAGS += CONFIG["MOZ_GTK3_CFLAGS"]
+
+CXXFLAGS += ["-Werror=switch"]
+
+LOCAL_INCLUDES += CONFIG["SKIA_INCLUDES"]
+
+# Suppress warnings in third-party code.
+if CONFIG["CC_TYPE"] == "gcc":
+ CXXFLAGS += ["-Wno-maybe-uninitialized"]
+
+UNIFIED_SOURCES += []
+
+# Add libFuzzer configuration directives
+include("/tools/fuzzing/libfuzzer-config.mozbuild")
diff --git a/gfx/layers/opengl/CompositingRenderTargetOGL.cpp b/gfx/layers/opengl/CompositingRenderTargetOGL.cpp
new file mode 100644
index 0000000000..9fbc207a6b
--- /dev/null
+++ b/gfx/layers/opengl/CompositingRenderTargetOGL.cpp
@@ -0,0 +1,117 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "CompositingRenderTargetOGL.h"
+#include "GLContext.h"
+#include "GLReadTexImageHelper.h"
+#include "ScopedGLHelpers.h"
+#include "mozilla/gfx/2D.h"
+
+namespace mozilla {
+namespace layers {
+
+using namespace mozilla::gfx;
+using namespace mozilla::gl;
+
+CompositingRenderTargetOGL::~CompositingRenderTargetOGL() {
+ if (mGLResourceOwnership == GLResourceOwnership::OWNED_BY_RENDER_TARGET &&
+ mGL && mGL->MakeCurrent()) {
+ mGL->fDeleteTextures(1, &mTextureHandle);
+ mGL->fDeleteFramebuffers(1, &mFBO);
+ }
+}
+
+void CompositingRenderTargetOGL::BindTexture(GLenum aTextureUnit,
+ GLenum aTextureTarget) {
+ MOZ_ASSERT(!mNeedInitialization);
+ MOZ_ASSERT(mTextureHandle != 0);
+ mGL->fActiveTexture(aTextureUnit);
+ mGL->fBindTexture(aTextureTarget, mTextureHandle);
+}
+
+void CompositingRenderTargetOGL::BindRenderTarget() {
+ bool needsClear = false;
+
+ if (mNeedInitialization) {
+ Initialize(mNeedInitialization->mFBOTextureTarget);
+ if (mNeedInitialization->mInitMode == INIT_MODE_CLEAR) {
+ needsClear = true;
+ mClearOnBind = false;
+ }
+ mNeedInitialization = Nothing();
+ } else {
+ GLuint fbo = GetFBO();
+ mGL->fBindFramebuffer(LOCAL_GL_FRAMEBUFFER, fbo);
+ GLenum result = mGL->fCheckFramebufferStatus(LOCAL_GL_FRAMEBUFFER);
+ if (result != LOCAL_GL_FRAMEBUFFER_COMPLETE) {
+ // The main framebuffer (0) of non-offscreen contexts
+ // might be backed by a EGLSurface that needs to be renewed.
+ if (mFBO == 0 && !mGL->IsOffscreen()) {
+ mGL->RenewSurface(mCompositor->GetWidget());
+ result = mGL->fCheckFramebufferStatus(LOCAL_GL_FRAMEBUFFER);
+ }
+ if (result != LOCAL_GL_FRAMEBUFFER_COMPLETE) {
+ nsAutoCString msg;
+ msg.AppendPrintf(
+ "Framebuffer not complete -- CheckFramebufferStatus returned 0x%x, "
+ "GLContext=%p, IsOffscreen()=%d, mFBO=%d, "
+ "aRect.width=%d, aRect.height=%d",
+ result, mGL.get(), mGL->IsOffscreen(), mFBO, mSize.width,
+ mSize.height);
+ NS_WARNING(msg.get());
+ }
+ }
+
+ needsClear = mClearOnBind;
+ }
+
+ if (needsClear) {
+ ScopedGLState scopedScissorTestState(mGL, LOCAL_GL_SCISSOR_TEST, true);
+ ScopedScissorRect autoScissorRect(mGL, 0, 0, mSize.width, mSize.height);
+ mGL->fClearColor(0.0, 0.0, 0.0, 0.0);
+ mGL->fClearDepth(0.0);
+ mGL->fClear(LOCAL_GL_COLOR_BUFFER_BIT | LOCAL_GL_DEPTH_BUFFER_BIT);
+ }
+}
+
+GLuint CompositingRenderTargetOGL::GetFBO() const {
+ MOZ_ASSERT(!mNeedInitialization);
+ return mFBO == 0 ? mGL->GetDefaultFramebuffer() : mFBO;
+}
+
+#ifdef MOZ_DUMP_PAINTING
+already_AddRefed<DataSourceSurface> CompositingRenderTargetOGL::Dump(
+ Compositor* aCompositor) {
+ MOZ_ASSERT(!mNeedInitialization);
+ CompositorOGL* compositorOGL = aCompositor->AsCompositorOGL();
+ return ReadBackSurface(mGL, mTextureHandle, true,
+ compositorOGL->GetFBOFormat());
+}
+#endif
+
+void CompositingRenderTargetOGL::Initialize(GLenum aFBOTextureTarget) {
+ // TODO: call mGL->GetBackbufferFB(), use that
+ GLuint fbo = mFBO == 0 ? mGL->GetDefaultFramebuffer() : mFBO;
+ mGL->fBindFramebuffer(LOCAL_GL_FRAMEBUFFER, fbo);
+ mGL->fFramebufferTexture2D(LOCAL_GL_FRAMEBUFFER, LOCAL_GL_COLOR_ATTACHMENT0,
+ aFBOTextureTarget, mTextureHandle, 0);
+
+ // Making this call to fCheckFramebufferStatus prevents a crash on
+ // PowerVR. See bug 695246.
+ GLenum result = mGL->fCheckFramebufferStatus(LOCAL_GL_FRAMEBUFFER);
+ if (result != LOCAL_GL_FRAMEBUFFER_COMPLETE) {
+ nsAutoCString msg;
+ msg.AppendPrintf(
+ "Framebuffer not complete -- error 0x%x, aFBOTextureTarget 0x%x, mFBO "
+ "%d, mTextureHandle %d, aRect.width %d, aRect.height %d",
+ result, aFBOTextureTarget, mFBO, mTextureHandle, mSize.width,
+ mSize.height);
+ NS_ERROR(msg.get());
+ }
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/opengl/CompositingRenderTargetOGL.h b/gfx/layers/opengl/CompositingRenderTargetOGL.h
new file mode 100644
index 0000000000..14e7265706
--- /dev/null
+++ b/gfx/layers/opengl/CompositingRenderTargetOGL.h
@@ -0,0 +1,212 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_GFX_COMPOSITINGRENDERTARGETOGL_H
+#define MOZILLA_GFX_COMPOSITINGRENDERTARGETOGL_H
+
+#include "GLContextTypes.h" // for GLContext
+#include "GLDefs.h" // for GLenum, LOCAL_GL_FRAMEBUFFER, etc
+#include "mozilla/Assertions.h" // for MOZ_ASSERT, etc
+#include "mozilla/Attributes.h" // for override
+#include "mozilla/RefPtr.h" // for RefPtr, already_AddRefed
+#include "mozilla/gfx/Point.h" // for IntSize, IntSizeTyped
+#include "mozilla/gfx/Types.h" // for SurfaceFormat, etc
+#include "mozilla/layers/Compositor.h" // for SurfaceInitMode, etc
+#include "mozilla/layers/TextureHost.h" // for CompositingRenderTarget
+#include "mozilla/layers/CompositorOGL.h" // for CompositorOGL
+#include "mozilla/mozalloc.h" // for operator new
+#include "nsAString.h"
+#include "nsCOMPtr.h" // for already_AddRefed
+#include "nsDebug.h" // for NS_ERROR, NS_WARNING
+#include "nsString.h" // for nsAutoCString
+
+namespace mozilla {
+namespace gl {
+class BindableTexture;
+} // namespace gl
+namespace gfx {
+class DataSourceSurface;
+} // namespace gfx
+
+namespace layers {
+
+class TextureSource;
+
+class CompositingRenderTargetOGL : public CompositingRenderTarget {
+ typedef mozilla::gl::GLContext GLContext;
+
+ friend class CompositorOGL;
+
+ enum class GLResourceOwnership : uint8_t {
+ // Framebuffer and texture will be deleted when the RenderTarget is
+ // destroyed.
+ OWNED_BY_RENDER_TARGET,
+
+ // Framebuffer and texture are only used by the RenderTarget, but never
+ // deleted.
+ EXTERNALLY_OWNED
+ };
+
+ struct InitParams {
+ GLenum mFBOTextureTarget;
+ SurfaceInitMode mInitMode;
+ };
+
+ public:
+ ~CompositingRenderTargetOGL();
+
+ const char* Name() const override { return "CompositingRenderTargetOGL"; }
+
+ /**
+ * Create a render target around the default FBO, for rendering straight to
+ * the window.
+ */
+ static already_AddRefed<CompositingRenderTargetOGL> CreateForWindow(
+ CompositorOGL* aCompositor, const gfx::IntSize& aSize) {
+ RefPtr<CompositingRenderTargetOGL> result = new CompositingRenderTargetOGL(
+ aCompositor, gfx::IntRect(gfx::IntPoint(), aSize), gfx::IntPoint(),
+ aSize, GLResourceOwnership::EXTERNALLY_OWNED, 0, 0, Nothing());
+ return result.forget();
+ }
+
+ static already_AddRefed<CompositingRenderTargetOGL>
+ CreateForNewFBOAndTakeOwnership(CompositorOGL* aCompositor, GLuint aTexture,
+ GLuint aFBO, const gfx::IntRect& aRect,
+ const gfx::IntPoint& aClipSpaceOrigin,
+ const gfx::IntSize& aPhySize,
+ GLenum aFBOTextureTarget,
+ SurfaceInitMode aInit) {
+ RefPtr<CompositingRenderTargetOGL> result = new CompositingRenderTargetOGL(
+ aCompositor, aRect, aClipSpaceOrigin, aPhySize,
+ GLResourceOwnership::OWNED_BY_RENDER_TARGET, aTexture, aFBO,
+ Some(InitParams{aFBOTextureTarget, aInit}));
+ return result.forget();
+ }
+
+ static already_AddRefed<CompositingRenderTargetOGL>
+ CreateForExternallyOwnedFBO(CompositorOGL* aCompositor, GLuint aFBO,
+ const gfx::IntRect& aRect,
+ const gfx::IntPoint& aClipSpaceOrigin) {
+ RefPtr<CompositingRenderTargetOGL> result = new CompositingRenderTargetOGL(
+ aCompositor, aRect, aClipSpaceOrigin, aRect.Size(),
+ GLResourceOwnership::EXTERNALLY_OWNED, 0, aFBO, Nothing());
+ return result.forget();
+ }
+
+ void BindTexture(GLenum aTextureUnit, GLenum aTextureTarget);
+
+ /**
+ * Call when we want to draw into our FBO
+ */
+ void BindRenderTarget();
+
+ bool IsWindow() { return mFBO == 0; }
+
+ GLuint GetFBO() const;
+
+ GLuint GetTextureHandle() const {
+ MOZ_ASSERT(!mNeedInitialization);
+ return mTextureHandle;
+ }
+
+ // TextureSourceOGL
+ TextureSourceOGL* AsSourceOGL() override {
+ // XXX - Bug 900770
+ MOZ_ASSERT(
+ false,
+ "CompositingRenderTargetOGL should not be used as a TextureSource");
+ return nullptr;
+ }
+ gfx::IntSize GetSize() const override { return mSize; }
+
+ // The point that DrawGeometry's aClipRect is relative to. Will be (0, 0) for
+ // root render targets and equal to GetOrigin() for non-root render targets.
+ gfx::IntPoint GetClipSpaceOrigin() const { return mClipSpaceOrigin; }
+
+ gfx::SurfaceFormat GetFormat() const override {
+ // XXX - Should it be implemented ? is the above assert true ?
+ MOZ_ASSERT(false, "Not implemented");
+ return gfx::SurfaceFormat::UNKNOWN;
+ }
+
+ // In render target coordinates, i.e. the same space as GetOrigin().
+ // NOT relative to mClipSpaceOrigin!
+ void SetClipRect(const Maybe<gfx::IntRect>& aRect) { mClipRect = aRect; }
+ const Maybe<gfx::IntRect>& GetClipRect() const { return mClipRect; }
+
+#ifdef MOZ_DUMP_PAINTING
+ already_AddRefed<gfx::DataSourceSurface> Dump(
+ Compositor* aCompositor) override;
+#endif
+
+ const gfx::IntSize& GetInitSize() const { return mSize; }
+ const gfx::IntSize& GetPhysicalSize() const { return mPhySize; }
+
+ protected:
+ CompositingRenderTargetOGL(CompositorOGL* aCompositor,
+ const gfx::IntRect& aRect,
+ const gfx::IntPoint& aClipSpaceOrigin,
+ const gfx::IntSize& aPhySize,
+ GLResourceOwnership aGLResourceOwnership,
+ GLuint aTexure, GLuint aFBO,
+ const Maybe<InitParams>& aNeedInitialization)
+ : CompositingRenderTarget(aRect.TopLeft()),
+ mNeedInitialization(aNeedInitialization),
+ mSize(aRect.Size()),
+ mPhySize(aPhySize),
+ mCompositor(aCompositor),
+ mGL(aCompositor->gl()),
+ mClipSpaceOrigin(aClipSpaceOrigin),
+ mGLResourceOwnership(aGLResourceOwnership),
+ mTextureHandle(aTexure),
+ mFBO(aFBO) {
+ MOZ_ASSERT(mGL);
+ }
+
+ /**
+ * Actually do the initialisation.
+ * We do this lazily so that when we first set this render target on the
+ * compositor we do not have to re-bind the FBO after unbinding it, or
+ * alternatively leave the FBO bound after creation. Note that we leave our
+ * FBO bound, and so calling this method is only suitable when about to use
+ * this render target.
+ */
+ void Initialize(GLenum aFBOTextureTarget);
+
+ /**
+ * Some() between construction and Initialize, if initialization was
+ * requested.
+ */
+ Maybe<InitParams> mNeedInitialization;
+
+ /*
+ * Users of render target would draw in logical size, but it is
+ * actually drawn to a surface in physical size. GL surfaces have
+ * a limitation on their size, a smaller surface would be
+ * allocated for the render target if the caller requests in a
+ * size too big.
+ */
+ gfx::IntSize mSize; // Logical size, the expected by callers.
+ gfx::IntSize mPhySize; // Physical size, the real size of the surface.
+
+ /**
+ * There is temporary a cycle between the compositor and the render target,
+ * each having a strong ref to the other. The compositor's reference to
+ * the target is always cleared at the end of a frame.
+ */
+ RefPtr<CompositorOGL> mCompositor;
+ RefPtr<GLContext> mGL;
+ Maybe<gfx::IntRect> mClipRect;
+ gfx::IntPoint mClipSpaceOrigin;
+ GLResourceOwnership mGLResourceOwnership;
+ GLuint mTextureHandle;
+ GLuint mFBO;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif /* MOZILLA_GFX_SURFACEOGL_H */
diff --git a/gfx/layers/opengl/CompositorOGL.cpp b/gfx/layers/opengl/CompositorOGL.cpp
new file mode 100644
index 0000000000..9f948adb3d
--- /dev/null
+++ b/gfx/layers/opengl/CompositorOGL.cpp
@@ -0,0 +1,1747 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "CompositorOGL.h"
+#include <stddef.h> // for size_t
+#include <stdint.h> // for uint32_t, uint8_t
+#include <stdlib.h> // for free, malloc
+#include "GLContextProvider.h" // for GLContextProvider
+#include "GLContext.h" // for GLContext
+#include "GLUploadHelpers.h"
+#include "gfxCrashReporterUtils.h" // for ScopedGfxFeatureReporter
+#include "gfxEnv.h" // for gfxEnv
+#include "gfxPlatform.h" // for gfxPlatform
+#include "gfxRect.h" // for gfxRect
+#include "gfxUtils.h" // for gfxUtils, etc
+#include "mozilla/ArrayUtils.h" // for ArrayLength
+#include "mozilla/Preferences.h" // for Preferences
+#include "mozilla/ProfilerLabels.h"
+#include "mozilla/StaticPrefs_gfx.h"
+#include "mozilla/StaticPrefs_layers.h"
+#include "mozilla/StaticPrefs_nglayout.h"
+#include "mozilla/gfx/BasePoint.h" // for BasePoint
+#include "mozilla/gfx/Matrix.h" // for Matrix4x4, Matrix
+#include "mozilla/gfx/Triangle.h" // for Triangle
+#include "mozilla/gfx/gfxVars.h" // for gfxVars
+#include "mozilla/layers/ImageDataSerializer.h"
+#include "mozilla/layers/NativeLayer.h"
+#include "mozilla/layers/CompositingRenderTargetOGL.h"
+#include "mozilla/layers/Effects.h" // for EffectChain, TexturedEffect, etc
+#include "mozilla/layers/TextureHost.h" // for TextureSource, etc
+#include "mozilla/layers/TextureHostOGL.h" // for TextureSourceOGL, etc
+#include "mozilla/layers/PTextureParent.h" // for OtherPid() on PTextureParent
+#include "mozilla/mozalloc.h" // for operator delete, etc
+#include "nsAppRunner.h"
+#include "nsAString.h"
+#include "nsClassHashtable.h"
+#include "nsIConsoleService.h" // for nsIConsoleService, etc
+#include "nsIWidget.h" // for nsIWidget
+#include "nsLiteralString.h" // for NS_LITERAL_STRING
+#include "nsMathUtils.h" // for NS_roundf
+#include "nsRect.h" // for mozilla::gfx::IntRect
+#include "nsServiceManagerUtils.h" // for do_GetService
+#include "nsString.h" // for nsString, nsAutoCString, etc
+#include "OGLShaderProgram.h" // for ShaderProgramOGL, etc
+#include "ScopedGLHelpers.h"
+#include "GLReadTexImageHelper.h"
+#include "HeapCopyOfStackArray.h"
+#include "GLBlitHelper.h"
+#include "mozilla/gfx/Swizzle.h"
+#ifdef MOZ_WAYLAND
+# include "mozilla/widget/GtkCompositorWidget.h"
+#endif
+#if MOZ_WIDGET_ANDROID
+# include "GLContextEGL.h"
+# include "GLLibraryEGL.h"
+# include "mozilla/java/GeckoSurfaceTextureWrappers.h"
+# include "mozilla/layers/AndroidHardwareBuffer.h"
+#endif
+
+namespace mozilla {
+
+using namespace gfx;
+
+namespace layers {
+
+using namespace mozilla::gl;
+
+static const GLuint kCoordinateAttributeIndex = 0;
+static const GLuint kTexCoordinateAttributeIndex = 1;
+
+class DummyTextureSourceOGL : public TextureSource, public TextureSourceOGL {
+ public:
+ const char* Name() const override { return "DummyTextureSourceOGL"; }
+
+ TextureSourceOGL* AsSourceOGL() override { return this; }
+
+ void BindTexture(GLenum activetex,
+ gfx::SamplingFilter aSamplingFilter) override {}
+
+ bool IsValid() const override { return false; }
+
+ gfx::IntSize GetSize() const override { return gfx::IntSize(); }
+
+ gfx::SurfaceFormat GetFormat() const override {
+ return gfx::SurfaceFormat::B8G8R8A8;
+ }
+
+ GLenum GetWrapMode() const override { return LOCAL_GL_CLAMP_TO_EDGE; }
+
+ protected:
+};
+
+class AsyncReadbackBufferOGL final : public AsyncReadbackBuffer {
+ public:
+ AsyncReadbackBufferOGL(GLContext* aGL, const IntSize& aSize);
+
+ bool MapAndCopyInto(DataSourceSurface* aSurface,
+ const IntSize& aReadSize) const override;
+
+ void Bind() const {
+ mGL->fBindBuffer(LOCAL_GL_PIXEL_PACK_BUFFER, mBufferHandle);
+ mGL->fPixelStorei(LOCAL_GL_PACK_ALIGNMENT, 1);
+ }
+
+ protected:
+ virtual ~AsyncReadbackBufferOGL();
+
+ private:
+ GLContext* mGL;
+ GLuint mBufferHandle;
+};
+
+AsyncReadbackBufferOGL::AsyncReadbackBufferOGL(GLContext* aGL,
+ const IntSize& aSize)
+ : AsyncReadbackBuffer(aSize), mGL(aGL), mBufferHandle(0) {
+ size_t bufferByteCount = mSize.width * mSize.height * 4;
+ mGL->fGenBuffers(1, &mBufferHandle);
+
+ ScopedPackState scopedPackState(mGL);
+ Bind();
+ mGL->fBufferData(LOCAL_GL_PIXEL_PACK_BUFFER, bufferByteCount, nullptr,
+ LOCAL_GL_STREAM_READ);
+}
+
+AsyncReadbackBufferOGL::~AsyncReadbackBufferOGL() {
+ if (mGL && mGL->MakeCurrent()) {
+ mGL->fDeleteBuffers(1, &mBufferHandle);
+ }
+}
+
+bool AsyncReadbackBufferOGL::MapAndCopyInto(DataSourceSurface* aSurface,
+ const IntSize& aReadSize) const {
+ MOZ_RELEASE_ASSERT(aReadSize <= aSurface->GetSize());
+
+ if (!mGL || !mGL->MakeCurrent()) {
+ return false;
+ }
+
+ ScopedPackState scopedPackState(mGL);
+ Bind();
+
+ const uint8_t* srcData = nullptr;
+ if (mGL->IsSupported(GLFeature::map_buffer_range)) {
+ srcData = static_cast<uint8_t*>(mGL->fMapBufferRange(
+ LOCAL_GL_PIXEL_PACK_BUFFER, 0, aReadSize.height * aReadSize.width * 4,
+ LOCAL_GL_MAP_READ_BIT));
+ } else {
+ srcData = static_cast<uint8_t*>(
+ mGL->fMapBuffer(LOCAL_GL_PIXEL_PACK_BUFFER, LOCAL_GL_READ_ONLY));
+ }
+
+ if (!srcData) {
+ return false;
+ }
+
+ int32_t srcStride = mSize.width * 4; // Bind() sets an alignment of 1
+ DataSourceSurface::ScopedMap map(aSurface, DataSourceSurface::WRITE);
+ uint8_t* destData = map.GetData();
+ int32_t destStride = map.GetStride();
+ SurfaceFormat destFormat = aSurface->GetFormat();
+ for (int32_t destRow = 0; destRow < aReadSize.height; destRow++) {
+ // Turn srcData upside down during the copy.
+ int32_t srcRow = aReadSize.height - 1 - destRow;
+ const uint8_t* src = &srcData[srcRow * srcStride];
+ uint8_t* dest = &destData[destRow * destStride];
+ SwizzleData(src, srcStride, SurfaceFormat::R8G8B8A8, dest, destStride,
+ destFormat, IntSize(aReadSize.width, 1));
+ }
+
+ mGL->fUnmapBuffer(LOCAL_GL_PIXEL_PACK_BUFFER);
+
+ return true;
+}
+
+PerUnitTexturePoolOGL::PerUnitTexturePoolOGL(gl::GLContext* aGL)
+ : mTextureTarget(0), // zero is never a valid texture target
+ mGL(aGL) {}
+
+PerUnitTexturePoolOGL::~PerUnitTexturePoolOGL() { DestroyTextures(); }
+
+CompositorOGL::CompositorOGL(widget::CompositorWidget* aWidget,
+ int aSurfaceWidth, int aSurfaceHeight,
+ bool aUseExternalSurfaceSize)
+ : Compositor(aWidget),
+ mWidgetSize(-1, -1),
+ mSurfaceSize(aSurfaceWidth, aSurfaceHeight),
+ mFBOTextureTarget(0),
+ mWindowRenderTarget(nullptr),
+ mQuadVBO(0),
+ mTriangleVBO(0),
+ mPreviousFrameDoneSync(nullptr),
+ mThisFrameDoneSync(nullptr),
+ mHasBGRA(0),
+ mUseExternalSurfaceSize(aUseExternalSurfaceSize),
+ mFrameInProgress(false),
+ mDestroyed(false),
+ mViewportSize(0, 0) {
+ if (aWidget->GetNativeLayerRoot()) {
+ // We can only render into native layers, our GLContext won't have a usable
+ // default framebuffer.
+ mCanRenderToDefaultFramebuffer = false;
+ }
+ MOZ_COUNT_CTOR(CompositorOGL);
+}
+
+CompositorOGL::~CompositorOGL() { MOZ_COUNT_DTOR(CompositorOGL); }
+
+already_AddRefed<mozilla::gl::GLContext> CompositorOGL::CreateContext() {
+ RefPtr<GLContext> context;
+
+ // Used by mock widget to create an offscreen context
+ nsIWidget* widget = mWidget->RealWidget();
+ void* widgetOpenGLContext =
+ widget ? widget->GetNativeData(NS_NATIVE_OPENGL_CONTEXT) : nullptr;
+ if (widgetOpenGLContext) {
+ GLContext* alreadyRefed = reinterpret_cast<GLContext*>(widgetOpenGLContext);
+ return already_AddRefed<GLContext>(alreadyRefed);
+ }
+
+#ifdef XP_WIN
+ if (gfxEnv::MOZ_LAYERS_PREFER_EGL()) {
+ printf_stderr("Trying GL layers...\n");
+ context = gl::GLContextProviderEGL::CreateForCompositorWidget(
+ mWidget, /* aHardwareWebRender */ false, /* aForceAccelerated */ false);
+ }
+#endif
+
+ // Allow to create offscreen GL context for main Layer Manager
+ if (!context && gfxEnv::MOZ_LAYERS_PREFER_OFFSCREEN()) {
+ nsCString discardFailureId;
+ context = GLContextProvider::CreateHeadless(
+ {CreateContextFlags::REQUIRE_COMPAT_PROFILE}, &discardFailureId);
+ if (!context->CreateOffscreenDefaultFb(mSurfaceSize)) {
+ context = nullptr;
+ }
+ }
+
+ if (!context) {
+ context = gl::GLContextProvider::CreateForCompositorWidget(
+ mWidget,
+ /* aHardwareWebRender */ false,
+ gfxVars::RequiresAcceleratedGLContextForCompositorOGL());
+ }
+
+ if (!context) {
+ NS_WARNING("Failed to create CompositorOGL context");
+ }
+
+ return context.forget();
+}
+
+void CompositorOGL::Destroy() {
+ Compositor::Destroy();
+
+ if (mTexturePool) {
+ mTexturePool->Clear();
+ mTexturePool = nullptr;
+ }
+
+ if (!mDestroyed) {
+ mDestroyed = true;
+ CleanupResources();
+ }
+}
+
+void CompositorOGL::CleanupResources() {
+ if (!mGLContext) return;
+
+ if (mSurfacePoolHandle) {
+ mSurfacePoolHandle->Pool()->DestroyGLResourcesForContext(mGLContext);
+ mSurfacePoolHandle = nullptr;
+ }
+
+ RefPtr<GLContext> ctx = mGLContext->GetSharedContext();
+ if (!ctx) {
+ ctx = mGLContext;
+ }
+
+ if (!ctx->MakeCurrent()) {
+ // Leak resources!
+ mQuadVBO = 0;
+ mTriangleVBO = 0;
+ mPreviousFrameDoneSync = nullptr;
+ mThisFrameDoneSync = nullptr;
+ mProgramsHolder = nullptr;
+ mGLContext = nullptr;
+ mNativeLayersReferenceRT = nullptr;
+ mFullWindowRenderTarget = nullptr;
+ return;
+ }
+
+ mProgramsHolder = nullptr;
+ mNativeLayersReferenceRT = nullptr;
+ mFullWindowRenderTarget = nullptr;
+
+#ifdef MOZ_WIDGET_GTK
+ // TextureSources might hold RefPtr<gl::GLContext>.
+ // All of them needs to be released to destroy GLContext.
+ // GLContextGLX has to be destroyed before related gtk window is destroyed.
+ for (auto textureSource : mRegisteredTextureSources) {
+ textureSource->DeallocateDeviceData();
+ }
+ mRegisteredTextureSources.clear();
+#endif
+
+ ctx->fBindFramebuffer(LOCAL_GL_FRAMEBUFFER, 0);
+
+ if (mQuadVBO) {
+ ctx->fDeleteBuffers(1, &mQuadVBO);
+ mQuadVBO = 0;
+ }
+
+ if (mTriangleVBO) {
+ ctx->fDeleteBuffers(1, &mTriangleVBO);
+ mTriangleVBO = 0;
+ }
+
+ mGLContext->MakeCurrent();
+
+ if (mPreviousFrameDoneSync) {
+ mGLContext->fDeleteSync(mPreviousFrameDoneSync);
+ mPreviousFrameDoneSync = nullptr;
+ }
+
+ if (mThisFrameDoneSync) {
+ mGLContext->fDeleteSync(mThisFrameDoneSync);
+ mThisFrameDoneSync = nullptr;
+ }
+
+ if (mOwnsGLContext) {
+ // On the main thread the Widget will be destroyed soon and calling
+ // MakeCurrent after that could cause a crash (at least with GLX, see bug
+ // 1059793), unless context is marked as destroyed. There may be some
+ // textures still alive that will try to call MakeCurrent on the context so
+ // let's make sure it is marked destroyed now.
+ mGLContext->MarkDestroyed();
+ }
+
+ mGLContext = nullptr;
+}
+
+bool CompositorOGL::Initialize(GLContext* aGLContext,
+ RefPtr<ShaderProgramOGLsHolder> aProgramsHolder,
+ nsCString* const out_failureReason) {
+ MOZ_ASSERT(!mDestroyed);
+ MOZ_ASSERT(!mGLContext);
+
+ mGLContext = aGLContext;
+ mProgramsHolder = aProgramsHolder;
+ mOwnsGLContext = false;
+
+ return Initialize(out_failureReason);
+}
+
+bool CompositorOGL::Initialize(nsCString* const out_failureReason) {
+ ScopedGfxFeatureReporter reporter("GL Layers");
+
+ // Do not allow double initialization
+ MOZ_ASSERT(mGLContext == nullptr || !mOwnsGLContext,
+ "Don't reinitialize CompositorOGL");
+
+ if (!mGLContext) {
+ MOZ_ASSERT(mOwnsGLContext);
+ mGLContext = CreateContext();
+ }
+
+#ifdef MOZ_WIDGET_ANDROID
+ if (!mGLContext) {
+ *out_failureReason = "FEATURE_FAILURE_OPENGL_NO_ANDROID_CONTEXT";
+ MOZ_CRASH("We need a context on Android");
+ }
+#endif
+
+ if (!mGLContext) {
+ *out_failureReason = "FEATURE_FAILURE_OPENGL_CREATE_CONTEXT";
+ return false;
+ }
+
+ if (!mProgramsHolder) {
+ mProgramsHolder = new ShaderProgramOGLsHolder(mGLContext);
+ }
+
+ MakeCurrent();
+
+ mHasBGRA = mGLContext->IsExtensionSupported(
+ gl::GLContext::EXT_texture_format_BGRA8888) ||
+ mGLContext->IsExtensionSupported(gl::GLContext::EXT_bgra);
+
+ mGLContext->fBlendFuncSeparate(LOCAL_GL_ONE, LOCAL_GL_ONE_MINUS_SRC_ALPHA,
+ LOCAL_GL_ONE, LOCAL_GL_ONE_MINUS_SRC_ALPHA);
+ mGLContext->fEnable(LOCAL_GL_BLEND);
+
+ // initialise a common shader to check that we can actually compile a shader
+ RefPtr<DummyTextureSourceOGL> source = new DummyTextureSourceOGL;
+ RefPtr<EffectRGB> effect =
+ new EffectRGB(source, /* aPremultiplied */ true, SamplingFilter::GOOD);
+ ShaderConfigOGL config = GetShaderConfigFor(effect);
+ if (!GetShaderProgramFor(config)) {
+ *out_failureReason = "FEATURE_FAILURE_OPENGL_COMPILE_SHADER";
+ return false;
+ }
+
+ if (mGLContext->WorkAroundDriverBugs()) {
+ /**
+ * We'll test the ability here to bind NPOT textures to a framebuffer, if
+ * this fails we'll try ARB_texture_rectangle.
+ */
+
+ GLenum textureTargets[] = {LOCAL_GL_TEXTURE_2D, LOCAL_GL_NONE};
+
+ if (!mGLContext->IsGLES()) {
+ // No TEXTURE_RECTANGLE_ARB available on ES2
+ textureTargets[1] = LOCAL_GL_TEXTURE_RECTANGLE_ARB;
+ }
+
+ mFBOTextureTarget = LOCAL_GL_NONE;
+
+ GLuint testFBO = 0;
+ mGLContext->fGenFramebuffers(1, &testFBO);
+ GLuint testTexture = 0;
+
+ for (uint32_t i = 0; i < ArrayLength(textureTargets); i++) {
+ GLenum target = textureTargets[i];
+ if (!target) continue;
+
+ mGLContext->fGenTextures(1, &testTexture);
+ mGLContext->fBindTexture(target, testTexture);
+ mGLContext->fTexParameteri(target, LOCAL_GL_TEXTURE_MIN_FILTER,
+ LOCAL_GL_NEAREST);
+ mGLContext->fTexParameteri(target, LOCAL_GL_TEXTURE_MAG_FILTER,
+ LOCAL_GL_NEAREST);
+ mGLContext->fTexImage2D(
+ target, 0, LOCAL_GL_RGBA, 5, 3, /* sufficiently NPOT */
+ 0, LOCAL_GL_RGBA, LOCAL_GL_UNSIGNED_BYTE, nullptr);
+
+ // unbind this texture, in preparation for binding it to the FBO
+ mGLContext->fBindTexture(target, 0);
+
+ mGLContext->fBindFramebuffer(LOCAL_GL_FRAMEBUFFER, testFBO);
+ mGLContext->fFramebufferTexture2D(LOCAL_GL_FRAMEBUFFER,
+ LOCAL_GL_COLOR_ATTACHMENT0, target,
+ testTexture, 0);
+
+ if (mGLContext->fCheckFramebufferStatus(LOCAL_GL_FRAMEBUFFER) ==
+ LOCAL_GL_FRAMEBUFFER_COMPLETE) {
+ mFBOTextureTarget = target;
+ mGLContext->fDeleteTextures(1, &testTexture);
+ break;
+ }
+
+ mGLContext->fDeleteTextures(1, &testTexture);
+ }
+
+ if (testFBO) {
+ mGLContext->fDeleteFramebuffers(1, &testFBO);
+ }
+
+ if (mFBOTextureTarget == LOCAL_GL_NONE) {
+ /* Unable to find a texture target that works with FBOs and NPOT textures
+ */
+ *out_failureReason = "FEATURE_FAILURE_OPENGL_NO_TEXTURE_TARGET";
+ return false;
+ }
+ } else {
+ // not trying to work around driver bugs, so TEXTURE_2D should just work
+ mFBOTextureTarget = LOCAL_GL_TEXTURE_2D;
+ }
+
+ // back to default framebuffer, to avoid confusion
+ mGLContext->fBindFramebuffer(LOCAL_GL_FRAMEBUFFER, 0);
+
+ if (mFBOTextureTarget == LOCAL_GL_TEXTURE_RECTANGLE_ARB) {
+ /* If we're using TEXTURE_RECTANGLE, then we must have the ARB
+ * extension -- the EXT variant does not provide support for
+ * texture rectangle access inside GLSL (sampler2DRect,
+ * texture2DRect).
+ */
+ if (!mGLContext->IsExtensionSupported(
+ gl::GLContext::ARB_texture_rectangle)) {
+ *out_failureReason = "FEATURE_FAILURE_OPENGL_ARB_EXT";
+ return false;
+ }
+ }
+
+ // Create a VBO for triangle vertices.
+ mGLContext->fGenBuffers(1, &mTriangleVBO);
+
+ /* Create a simple quad VBO */
+ mGLContext->fGenBuffers(1, &mQuadVBO);
+
+ // 4 quads, with the number of the quad (vertexID) encoded in w.
+ GLfloat vertices[] = {
+ 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f,
+ 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f,
+
+ 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f,
+ 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 1.0f,
+
+ 0.0f, 0.0f, 0.0f, 2.0f, 1.0f, 0.0f, 0.0f, 2.0f, 0.0f, 1.0f, 0.0f, 2.0f,
+ 1.0f, 0.0f, 0.0f, 2.0f, 0.0f, 1.0f, 0.0f, 2.0f, 1.0f, 1.0f, 0.0f, 2.0f,
+
+ 0.0f, 0.0f, 0.0f, 3.0f, 1.0f, 0.0f, 0.0f, 3.0f, 0.0f, 1.0f, 0.0f, 3.0f,
+ 1.0f, 0.0f, 0.0f, 3.0f, 0.0f, 1.0f, 0.0f, 3.0f, 1.0f, 1.0f, 0.0f, 3.0f,
+ };
+ HeapCopyOfStackArray<GLfloat> verticesOnHeap(vertices);
+
+ mGLContext->fBindBuffer(LOCAL_GL_ARRAY_BUFFER, mQuadVBO);
+ mGLContext->fBufferData(LOCAL_GL_ARRAY_BUFFER, verticesOnHeap.ByteLength(),
+ verticesOnHeap.Data(), LOCAL_GL_STATIC_DRAW);
+ mGLContext->fBindBuffer(LOCAL_GL_ARRAY_BUFFER, 0);
+
+ nsCOMPtr<nsIConsoleService> console(
+ do_GetService(NS_CONSOLESERVICE_CONTRACTID));
+
+ if (console) {
+ nsString msg;
+ msg += nsLiteralString(
+ u"OpenGL compositor Initialized Succesfully.\nVersion: ");
+ msg += NS_ConvertUTF8toUTF16(nsDependentCString(
+ (const char*)mGLContext->fGetString(LOCAL_GL_VERSION)));
+ msg += u"\nVendor: "_ns;
+ msg += NS_ConvertUTF8toUTF16(nsDependentCString(
+ (const char*)mGLContext->fGetString(LOCAL_GL_VENDOR)));
+ msg += u"\nRenderer: "_ns;
+ msg += NS_ConvertUTF8toUTF16(nsDependentCString(
+ (const char*)mGLContext->fGetString(LOCAL_GL_RENDERER)));
+ msg += u"\nFBO Texture Target: "_ns;
+ if (mFBOTextureTarget == LOCAL_GL_TEXTURE_2D)
+ msg += u"TEXTURE_2D"_ns;
+ else
+ msg += u"TEXTURE_RECTANGLE"_ns;
+ console->LogStringMessage(msg.get());
+ }
+
+ reporter.SetSuccessful();
+
+ return true;
+}
+
+/*
+ * Returns a size that is equal to, or larger than and closest to,
+ * aSize where both width and height are powers of two.
+ * If the OpenGL setup is capable of using non-POT textures,
+ * then it will just return aSize.
+ */
+static IntSize CalculatePOTSize(const IntSize& aSize, GLContext* gl) {
+ if (CanUploadNonPowerOfTwo(gl)) return aSize;
+
+ return IntSize(RoundUpPow2(aSize.width), RoundUpPow2(aSize.height));
+}
+
+gfx::Rect CompositorOGL::GetTextureCoordinates(gfx::Rect textureRect,
+ TextureSource* aTexture) {
+ // If the OpenGL setup does not support non-power-of-two textures then the
+ // texture's width and height will have been increased to the next
+ // power-of-two (unless already a power of two). In that case we must scale
+ // the texture coordinates to account for that.
+ if (!CanUploadNonPowerOfTwo(mGLContext)) {
+ const IntSize& textureSize = aTexture->GetSize();
+ const IntSize potSize = CalculatePOTSize(textureSize, mGLContext);
+ if (potSize != textureSize) {
+ const float xScale = (float)textureSize.width / (float)potSize.width;
+ const float yScale = (float)textureSize.height / (float)potSize.height;
+ textureRect.Scale(xScale, yScale);
+ }
+ }
+
+ return textureRect;
+}
+
+void CompositorOGL::PrepareViewport(CompositingRenderTargetOGL* aRenderTarget) {
+ MOZ_ASSERT(aRenderTarget);
+ // Logical surface size.
+ const gfx::IntSize& size = aRenderTarget->GetSize();
+ // Physical surface size.
+ const gfx::IntSize& phySize = aRenderTarget->GetPhysicalSize();
+
+ // Set the viewport correctly.
+ mGLContext->fViewport(mSurfaceOrigin.x, mSurfaceOrigin.y, phySize.width,
+ phySize.height);
+
+ mViewportSize = size;
+
+ if (!aRenderTarget->HasComplexProjection()) {
+ // We flip the view matrix around so that everything is right-side up; we're
+ // drawing directly into the window's back buffer, so this keeps things
+ // looking correct.
+ // XXX: We keep track of whether the window size changed, so we could skip
+ // this update if it hadn't changed since the last call.
+
+ // Matrix to transform (0, 0, aWidth, aHeight) to viewport space (-1.0, 1.0,
+ // 2, 2) and flip the contents.
+ Matrix viewMatrix;
+ viewMatrix.PreTranslate(-1.0, 1.0);
+ viewMatrix.PreScale(2.0f / float(size.width), 2.0f / float(size.height));
+ viewMatrix.PreScale(1.0f, -1.0f);
+
+ MOZ_ASSERT(mCurrentRenderTarget, "No destination");
+
+ Matrix4x4 matrix3d = Matrix4x4::From2D(viewMatrix);
+ matrix3d._33 = 0.0f;
+ mProjMatrix = matrix3d;
+ mGLContext->fDepthRange(0.0f, 1.0f);
+ } else {
+ bool depthEnable;
+ float zNear, zFar;
+ aRenderTarget->GetProjection(mProjMatrix, depthEnable, zNear, zFar);
+ mGLContext->fDepthRange(zNear, zFar);
+ }
+}
+
+already_AddRefed<CompositingRenderTarget> CompositorOGL::CreateRenderTarget(
+ const IntRect& aRect, SurfaceInitMode aInit) {
+ MOZ_ASSERT(!aRect.IsZeroArea(),
+ "Trying to create a render target of invalid size");
+
+ if (aRect.IsZeroArea()) {
+ return nullptr;
+ }
+
+ if (!gl()) {
+ // CompositingRenderTargetOGL does not work without a gl context.
+ return nullptr;
+ }
+
+ GLuint tex = 0;
+ GLuint fbo = 0;
+ IntRect rect = aRect;
+ IntSize fboSize;
+ CreateFBOWithTexture(rect, false, 0, &fbo, &tex, &fboSize);
+ return CompositingRenderTargetOGL::CreateForNewFBOAndTakeOwnership(
+ this, tex, fbo, aRect, aRect.TopLeft(), aRect.Size(), mFBOTextureTarget,
+ aInit);
+}
+
+void CompositorOGL::SetRenderTarget(CompositingRenderTarget* aSurface) {
+ MOZ_ASSERT(aSurface);
+ CompositingRenderTargetOGL* surface =
+ static_cast<CompositingRenderTargetOGL*>(aSurface);
+ if (mCurrentRenderTarget != surface) {
+ mCurrentRenderTarget = surface;
+ surface->BindRenderTarget();
+ }
+
+ PrepareViewport(mCurrentRenderTarget);
+}
+
+already_AddRefed<CompositingRenderTarget>
+CompositorOGL::GetCurrentRenderTarget() const {
+ return do_AddRef(mCurrentRenderTarget);
+}
+
+already_AddRefed<CompositingRenderTarget> CompositorOGL::GetWindowRenderTarget()
+ const {
+ return do_AddRef(mWindowRenderTarget);
+}
+
+already_AddRefed<AsyncReadbackBuffer> CompositorOGL::CreateAsyncReadbackBuffer(
+ const IntSize& aSize) {
+ return MakeAndAddRef<AsyncReadbackBufferOGL>(mGLContext, aSize);
+}
+
+bool CompositorOGL::ReadbackRenderTarget(CompositingRenderTarget* aSource,
+ AsyncReadbackBuffer* aDest) {
+ IntSize size = aSource->GetSize();
+ MOZ_RELEASE_ASSERT(aDest->GetSize() == size);
+
+ RefPtr<CompositingRenderTarget> previousTarget = GetCurrentRenderTarget();
+ if (previousTarget != aSource) {
+ SetRenderTarget(aSource);
+ }
+
+ ScopedPackState scopedPackState(mGLContext);
+ static_cast<AsyncReadbackBufferOGL*>(aDest)->Bind();
+
+ mGLContext->fReadPixels(0, 0, size.width, size.height, LOCAL_GL_RGBA,
+ LOCAL_GL_UNSIGNED_BYTE, 0);
+
+ if (previousTarget != aSource) {
+ SetRenderTarget(previousTarget);
+ }
+ return true;
+}
+
+bool CompositorOGL::BlitRenderTarget(CompositingRenderTarget* aSource,
+ const gfx::IntSize& aSourceSize,
+ const gfx::IntSize& aDestSize) {
+ if (!mGLContext->IsSupported(GLFeature::framebuffer_blit)) {
+ return false;
+ }
+ CompositingRenderTargetOGL* source =
+ static_cast<CompositingRenderTargetOGL*>(aSource);
+ GLuint srcFBO = source->GetFBO();
+ GLuint destFBO = mCurrentRenderTarget->GetFBO();
+ mGLContext->BlitHelper()->BlitFramebufferToFramebuffer(
+ srcFBO, destFBO, IntRect(IntPoint(), aSourceSize),
+ IntRect(IntPoint(), aDestSize), LOCAL_GL_LINEAR);
+ return true;
+}
+
+static GLenum GetFrameBufferInternalFormat(
+ GLContext* gl, GLuint aFrameBuffer,
+ mozilla::widget::CompositorWidget* aWidget) {
+ if (aFrameBuffer == 0) { // default framebuffer
+ return aWidget->GetGLFrameBufferFormat();
+ }
+ return LOCAL_GL_RGBA;
+}
+
+Maybe<IntRect> CompositorOGL::BeginFrameForWindow(
+ const nsIntRegion& aInvalidRegion, const Maybe<IntRect>& aClipRect,
+ const IntRect& aRenderBounds, const nsIntRegion& aOpaqueRegion) {
+ MOZ_RELEASE_ASSERT(!mTarget, "mTarget not cleared properly");
+ return BeginFrame(aInvalidRegion, aClipRect, aRenderBounds, aOpaqueRegion);
+}
+
+Maybe<IntRect> CompositorOGL::BeginFrame(const nsIntRegion& aInvalidRegion,
+ const Maybe<IntRect>& aClipRect,
+ const IntRect& aRenderBounds,
+ const nsIntRegion& aOpaqueRegion) {
+ AUTO_PROFILER_LABEL("CompositorOGL::BeginFrame", GRAPHICS);
+
+ MOZ_ASSERT(!mFrameInProgress,
+ "frame still in progress (should have called EndFrame");
+
+ IntRect rect;
+ if (mUseExternalSurfaceSize) {
+ rect = IntRect(IntPoint(), mSurfaceSize);
+ } else {
+ rect = aRenderBounds;
+ }
+
+ // We can't draw anything to something with no area
+ // so just return
+ if (rect.IsZeroArea()) {
+ return Nothing();
+ }
+
+ // If the widget size changed, we have to force a MakeCurrent
+ // to make sure that GL sees the updated widget size.
+ if (mWidgetSize.ToUnknownSize() != rect.Size()) {
+ MakeCurrent(ForceMakeCurrent);
+
+ mWidgetSize = LayoutDeviceIntSize::FromUnknownSize(rect.Size());
+#ifdef MOZ_WAYLAND
+ if (mWidget && mWidget->AsGTK()) {
+ mWidget->AsGTK()->SetEGLNativeWindowSize(mWidgetSize);
+ }
+#endif
+ } else {
+ MakeCurrent();
+ }
+
+#ifdef MOZ_WIDGET_ANDROID
+ java::GeckoSurfaceTexture::DestroyUnused((int64_t)mGLContext.get());
+ mGLContext->MakeCurrent(); // DestroyUnused can change the current context!
+#endif
+
+ // Default blend function implements "OVER"
+ mGLContext->fBlendFuncSeparate(LOCAL_GL_ONE, LOCAL_GL_ONE_MINUS_SRC_ALPHA,
+ LOCAL_GL_ONE, LOCAL_GL_ONE_MINUS_SRC_ALPHA);
+ mGLContext->fEnable(LOCAL_GL_BLEND);
+
+ RefPtr<CompositingRenderTarget> rt;
+ if (mCanRenderToDefaultFramebuffer) {
+ rt = CompositingRenderTargetOGL::CreateForWindow(this, rect.Size());
+ } else if (mTarget) {
+ rt = CreateRenderTarget(rect, INIT_MODE_CLEAR);
+ } else {
+ MOZ_CRASH("Unexpected call");
+ }
+
+ if (!rt) {
+ return Nothing();
+ }
+
+ // We're about to actually draw a frame.
+ mFrameInProgress = true;
+
+ SetRenderTarget(rt);
+ mWindowRenderTarget = mCurrentRenderTarget;
+
+ for (auto iter = aInvalidRegion.RectIter(); !iter.Done(); iter.Next()) {
+ const IntRect& r = iter.Get();
+ mCurrentFrameInvalidRegion.OrWith(
+ IntRect(r.X(), FlipY(r.YMost()), r.Width(), r.Height()));
+ }
+ // Check to see if there is any transparent dirty region that would require
+ // clearing. If not, just invalidate the framebuffer if supported.
+ // TODO: Currently we initialize the clear region to the widget bounds as
+ // SwapBuffers will update the entire framebuffer. On platforms that support
+ // damage regions, we could initialize this to mCurrentFrameInvalidRegion.
+ IntRegion regionToClear(rect);
+ regionToClear.SubOut(aOpaqueRegion);
+ GLbitfield clearBits = LOCAL_GL_DEPTH_BUFFER_BIT;
+ if (regionToClear.IsEmpty() &&
+ mGLContext->IsSupported(GLFeature::invalidate_framebuffer)) {
+ GLenum attachments[] = {LOCAL_GL_COLOR};
+ mGLContext->fInvalidateFramebuffer(
+ LOCAL_GL_FRAMEBUFFER, MOZ_ARRAY_LENGTH(attachments), attachments);
+ } else {
+ clearBits |= LOCAL_GL_COLOR_BUFFER_BIT;
+ }
+
+#if defined(MOZ_WIDGET_ANDROID)
+ if ((mSurfaceOrigin.x > 0) || (mSurfaceOrigin.y > 0)) {
+ mGLContext->fClearColor(
+ StaticPrefs::gfx_compositor_override_clear_color_r(),
+ StaticPrefs::gfx_compositor_override_clear_color_g(),
+ StaticPrefs::gfx_compositor_override_clear_color_b(),
+ StaticPrefs::gfx_compositor_override_clear_color_a());
+ } else {
+ mGLContext->fClearColor(mClearColor.r, mClearColor.g, mClearColor.b,
+ mClearColor.a);
+ }
+#else
+ mGLContext->fClearColor(mClearColor.r, mClearColor.g, mClearColor.b,
+ mClearColor.a);
+#endif // defined(MOZ_WIDGET_ANDROID)
+ mGLContext->fClear(clearBits);
+
+ return Some(rect);
+}
+
+void CompositorOGL::CreateFBOWithTexture(const gfx::IntRect& aRect,
+ bool aCopyFromSource,
+ GLuint aSourceFrameBuffer,
+ GLuint* aFBO, GLuint* aTexture,
+ gfx::IntSize* aAllocSize) {
+ *aTexture =
+ CreateTexture(aRect, aCopyFromSource, aSourceFrameBuffer, aAllocSize);
+ mGLContext->fGenFramebuffers(1, aFBO);
+}
+
+// Should be called after calls to fReadPixels or fCopyTexImage2D, and other
+// GL read calls.
+static void WorkAroundAppleIntelHD3000GraphicsGLDriverBug(GLContext* aGL) {
+#ifdef XP_MACOSX
+ if (aGL->WorkAroundDriverBugs() &&
+ aGL->Renderer() == GLRenderer::IntelHD3000) {
+ // Work around a bug in the Apple Intel HD Graphics 3000 driver (bug
+ // 1586627, filed with Apple as FB7379358). This bug has been found present
+ // on 10.9.3 and on 10.13.6, so it likely affects all shipped versions of
+ // this driver. (macOS 10.14 does not support this GPU.)
+ // The bug manifests as follows: Reading from a framebuffer puts that
+ // framebuffer into a state such that deleting that framebuffer can break
+ // other framebuffers in certain cases. More specifically, if you have two
+ // framebuffers A and B, the following sequence of events breaks subsequent
+ // drawing to B:
+ // 1. A becomes "most recently read-from framebuffer".
+ // 2. B is drawn to.
+ // 3. A is deleted, and other GL state (such as GL_SCISSOR enabled state)
+ // is touched.
+ // 4. B is drawn to again.
+ // Now all draws to framebuffer B, including the draw from step 4, will
+ // render at the wrong position and upside down.
+ //
+ // When AfterGLReadCall() is called, the currently bound framebuffer is the
+ // framebuffer that has been read from most recently. So in the presence of
+ // this bug, deleting this framebuffer has now become dangerous. We work
+ // around the bug by creating a new short-lived framebuffer, making that new
+ // framebuffer the most recently read-from framebuffer (using
+ // glCopyTexImage2D), and then deleting it under controlled circumstances.
+ // This deletion is not affected by the bug because our deletion call is not
+ // interleaved with draw calls to another framebuffer and a touching of the
+ // GL scissor enabled state.
+
+ ScopedTexture texForReading(aGL);
+ {
+ // Initialize a 1x1 texture.
+ ScopedBindTexture autoBindTexForReading(aGL, texForReading);
+ aGL->fTexImage2D(LOCAL_GL_TEXTURE_2D, 0, LOCAL_GL_RGBA, 1, 1, 0,
+ LOCAL_GL_RGBA, LOCAL_GL_UNSIGNED_BYTE, nullptr);
+ aGL->fTexParameteri(LOCAL_GL_TEXTURE_2D, LOCAL_GL_TEXTURE_MIN_FILTER,
+ LOCAL_GL_LINEAR);
+ aGL->fTexParameteri(LOCAL_GL_TEXTURE_2D, LOCAL_GL_TEXTURE_MAG_FILTER,
+ LOCAL_GL_LINEAR);
+ }
+ // Make a framebuffer around the texture.
+ ScopedFramebufferForTexture autoFBForReading(aGL, texForReading);
+ if (autoFBForReading.IsComplete()) {
+ // "Read" from the framebuffer, by initializing a new texture using
+ // glCopyTexImage2D. This flips the bad bit on autoFBForReading.FB().
+ ScopedBindFramebuffer autoFB(aGL, autoFBForReading.FB());
+ ScopedTexture texReadingDest(aGL);
+ ScopedBindTexture autoBindTexReadingDest(aGL, texReadingDest);
+ aGL->fCopyTexImage2D(LOCAL_GL_TEXTURE_2D, 0, LOCAL_GL_RGBA, 0, 0, 1, 1,
+ 0);
+ }
+ // When autoFBForReading goes out of scope, the "poisoned" framebuffer is
+ // deleted, and the bad state seems to go away along with it.
+ }
+#endif
+}
+
+GLuint CompositorOGL::CreateTexture(const IntRect& aRect, bool aCopyFromSource,
+ GLuint aSourceFrameBuffer,
+ IntSize* aAllocSize) {
+ // we're about to create a framebuffer backed by textures to use as an
+ // intermediate surface. What to do if its size (as given by aRect) would
+ // exceed the maximum texture size supported by the GL? The present code
+ // chooses the compromise of just clamping the framebuffer's size to the max
+ // supported size. This gives us a lower resolution rendering of the
+ // intermediate surface (children layers). See bug 827170 for a discussion.
+ IntRect clampedRect = aRect;
+ int32_t maxTexSize = GetMaxTextureSize();
+ clampedRect.SetWidth(std::min(clampedRect.Width(), maxTexSize));
+ clampedRect.SetHeight(std::min(clampedRect.Height(), maxTexSize));
+
+ auto clampedRectWidth = clampedRect.Width();
+ auto clampedRectHeight = clampedRect.Height();
+
+ GLuint tex;
+
+ mGLContext->fActiveTexture(LOCAL_GL_TEXTURE0);
+ mGLContext->fGenTextures(1, &tex);
+ mGLContext->fBindTexture(mFBOTextureTarget, tex);
+
+ if (aCopyFromSource) {
+ GLuint curFBO = mCurrentRenderTarget->GetFBO();
+ if (curFBO != aSourceFrameBuffer) {
+ mGLContext->fBindFramebuffer(LOCAL_GL_FRAMEBUFFER, aSourceFrameBuffer);
+ }
+
+ // We're going to create an RGBA temporary fbo. But to
+ // CopyTexImage() from the current framebuffer, the framebuffer's
+ // format has to be compatible with the new texture's. So we
+ // check the format of the framebuffer here and take a slow path
+ // if it's incompatible.
+ GLenum format =
+ GetFrameBufferInternalFormat(gl(), aSourceFrameBuffer, mWidget);
+
+ bool isFormatCompatibleWithRGBA =
+ gl()->IsGLES() ? (format == LOCAL_GL_RGBA) : true;
+
+ if (isFormatCompatibleWithRGBA) {
+ mGLContext->fCopyTexImage2D(mFBOTextureTarget, 0, LOCAL_GL_RGBA,
+ clampedRect.X(), FlipY(clampedRect.YMost()),
+ clampedRectWidth, clampedRectHeight, 0);
+ WorkAroundAppleIntelHD3000GraphicsGLDriverBug(mGLContext);
+ } else {
+ // Curses, incompatible formats. Take a slow path.
+
+ // RGBA
+ size_t bufferSize = clampedRectWidth * clampedRectHeight * 4;
+ auto buf = MakeUnique<uint8_t[]>(bufferSize);
+
+ mGLContext->fReadPixels(clampedRect.X(), clampedRect.Y(),
+ clampedRectWidth, clampedRectHeight,
+ LOCAL_GL_RGBA, LOCAL_GL_UNSIGNED_BYTE, buf.get());
+ WorkAroundAppleIntelHD3000GraphicsGLDriverBug(mGLContext);
+ mGLContext->fTexImage2D(mFBOTextureTarget, 0, LOCAL_GL_RGBA,
+ clampedRectWidth, clampedRectHeight, 0,
+ LOCAL_GL_RGBA, LOCAL_GL_UNSIGNED_BYTE, buf.get());
+ }
+
+ GLenum error = mGLContext->fGetError();
+ if (error != LOCAL_GL_NO_ERROR) {
+ nsAutoCString msg;
+ msg.AppendPrintf(
+ "Texture initialization failed! -- error 0x%x, Source %d, Source "
+ "format %d, RGBA Compat %d",
+ error, aSourceFrameBuffer, format, isFormatCompatibleWithRGBA);
+ NS_ERROR(msg.get());
+ }
+ } else {
+ mGLContext->fTexImage2D(mFBOTextureTarget, 0, LOCAL_GL_RGBA,
+ clampedRectWidth, clampedRectHeight, 0,
+ LOCAL_GL_RGBA, LOCAL_GL_UNSIGNED_BYTE, nullptr);
+ }
+ mGLContext->fTexParameteri(mFBOTextureTarget, LOCAL_GL_TEXTURE_MIN_FILTER,
+ LOCAL_GL_LINEAR);
+ mGLContext->fTexParameteri(mFBOTextureTarget, LOCAL_GL_TEXTURE_MAG_FILTER,
+ LOCAL_GL_LINEAR);
+ mGLContext->fTexParameteri(mFBOTextureTarget, LOCAL_GL_TEXTURE_WRAP_S,
+ LOCAL_GL_CLAMP_TO_EDGE);
+ mGLContext->fTexParameteri(mFBOTextureTarget, LOCAL_GL_TEXTURE_WRAP_T,
+ LOCAL_GL_CLAMP_TO_EDGE);
+ mGLContext->fBindTexture(mFBOTextureTarget, 0);
+
+ if (aAllocSize) {
+ aAllocSize->width = clampedRectWidth;
+ aAllocSize->height = clampedRectHeight;
+ }
+
+ return tex;
+}
+
+ShaderConfigOGL CompositorOGL::GetShaderConfigFor(Effect* aEffect,
+ bool aDEAAEnabled) const {
+ ShaderConfigOGL config;
+
+ switch (aEffect->mType) {
+ case EffectTypes::YCBCR: {
+ config.SetYCbCr(true);
+ EffectYCbCr* effectYCbCr = static_cast<EffectYCbCr*>(aEffect);
+ config.SetColorMultiplier(
+ RescalingFactorForColorDepth(effectYCbCr->mColorDepth));
+ config.SetTextureTarget(
+ effectYCbCr->mTexture->AsSourceOGL()->GetTextureTarget());
+ break;
+ }
+ case EffectTypes::NV12:
+ config.SetNV12(true);
+ if (gl()->IsExtensionSupported(gl::GLContext::ARB_texture_rectangle)) {
+ config.SetTextureTarget(LOCAL_GL_TEXTURE_RECTANGLE_ARB);
+ } else {
+ config.SetTextureTarget(LOCAL_GL_TEXTURE_2D);
+ }
+ break;
+ default: {
+ MOZ_ASSERT(aEffect->mType == EffectTypes::RGB);
+ TexturedEffect* texturedEffect = static_cast<TexturedEffect*>(aEffect);
+ TextureSourceOGL* source = texturedEffect->mTexture->AsSourceOGL();
+ MOZ_ASSERT_IF(
+ source->GetTextureTarget() == LOCAL_GL_TEXTURE_EXTERNAL,
+ source->GetFormat() == gfx::SurfaceFormat::R8G8B8A8 ||
+ source->GetFormat() == gfx::SurfaceFormat::R8G8B8X8 ||
+ source->GetFormat() == gfx::SurfaceFormat::B8G8R8A8 ||
+ source->GetFormat() == gfx::SurfaceFormat::B8G8R8X8 ||
+ source->GetFormat() == gfx::SurfaceFormat::R5G6B5_UINT16);
+ MOZ_ASSERT_IF(
+ source->GetTextureTarget() == LOCAL_GL_TEXTURE_RECTANGLE_ARB,
+ source->GetFormat() == gfx::SurfaceFormat::R8G8B8A8 ||
+ source->GetFormat() == gfx::SurfaceFormat::R8G8B8X8 ||
+ source->GetFormat() == gfx::SurfaceFormat::R5G6B5_UINT16 ||
+ source->GetFormat() == gfx::SurfaceFormat::YUV422);
+ config = ShaderConfigFromTargetAndFormat(source->GetTextureTarget(),
+ source->GetFormat());
+ if (!texturedEffect->mPremultiplied) {
+ config.SetNoPremultipliedAlpha();
+ }
+ break;
+ }
+ }
+ config.SetDEAA(aDEAAEnabled);
+ return config;
+}
+
+ShaderProgramOGL* CompositorOGL::GetShaderProgramFor(
+ const ShaderConfigOGL& aConfig) {
+ ShaderProgramOGL* shader = mProgramsHolder->GetShaderProgramFor(aConfig);
+ return shader;
+}
+
+void CompositorOGL::ResetProgram() { mProgramsHolder->ResetCurrentProgram(); }
+
+static bool SetBlendMode(GLContext* aGL, gfx::CompositionOp aBlendMode,
+ bool aIsPremultiplied = true) {
+ if (BlendOpIsMixBlendMode(aBlendMode)) {
+ // Mix-blend modes require an extra step (or more) that cannot be expressed
+ // in the fixed-function blending capabilities of opengl. We handle them
+ // separately in shaders, and the shaders assume we will use our default
+ // blend function for compositing (premultiplied OP_OVER).
+ return false;
+ }
+ if (aBlendMode == gfx::CompositionOp::OP_OVER && aIsPremultiplied) {
+ return false;
+ }
+
+ GLenum srcBlend;
+ GLenum dstBlend;
+ GLenum srcAlphaBlend = LOCAL_GL_ONE;
+ GLenum dstAlphaBlend = LOCAL_GL_ONE_MINUS_SRC_ALPHA;
+
+ switch (aBlendMode) {
+ case gfx::CompositionOp::OP_OVER:
+ MOZ_ASSERT(!aIsPremultiplied);
+ srcBlend = LOCAL_GL_SRC_ALPHA;
+ dstBlend = LOCAL_GL_ONE_MINUS_SRC_ALPHA;
+ break;
+ case gfx::CompositionOp::OP_SOURCE:
+ srcBlend = aIsPremultiplied ? LOCAL_GL_ONE : LOCAL_GL_SRC_ALPHA;
+ dstBlend = LOCAL_GL_ZERO;
+ srcAlphaBlend = LOCAL_GL_ONE;
+ dstAlphaBlend = LOCAL_GL_ZERO;
+ break;
+ default:
+ MOZ_ASSERT_UNREACHABLE("Unsupported blend mode!");
+ return false;
+ }
+
+ aGL->fBlendFuncSeparate(srcBlend, dstBlend, srcAlphaBlend, dstAlphaBlend);
+ return true;
+}
+
+gfx::Point3D CompositorOGL::GetLineCoefficients(const gfx::Point& aPoint1,
+ const gfx::Point& aPoint2) {
+ // Return standard coefficients for a line between aPoint1 and aPoint2
+ // for standard line equation:
+ //
+ // Ax + By + C = 0
+ //
+ // A = (p1.y – p2.y)
+ // B = (p2.x – p1.x)
+ // C = (p1.x * p2.y) – (p2.x * p1.y)
+
+ gfx::Point3D coeffecients;
+ coeffecients.x = aPoint1.y - aPoint2.y;
+ coeffecients.y = aPoint2.x - aPoint1.x;
+ coeffecients.z =
+ aPoint1.x.value * aPoint2.y.value - aPoint2.x.value * aPoint1.y.value;
+
+ coeffecients *= 1.0f / sqrtf(coeffecients.x * coeffecients.x +
+ coeffecients.y * coeffecients.y);
+
+ // Offset outwards by 0.5 pixel as the edge is considered to be 1 pixel
+ // wide and included within the interior of the polygon
+ coeffecients.z += 0.5f;
+
+ return coeffecients;
+}
+
+void CompositorOGL::DrawQuad(const Rect& aRect, const IntRect& aClipRect,
+ const EffectChain& aEffectChain, Float aOpacity,
+ const gfx::Matrix4x4& aTransform,
+ const gfx::Rect& aVisibleRect) {
+ AUTO_PROFILER_LABEL("CompositorOGL::DrawQuad", GRAPHICS);
+
+ DrawGeometry(aRect, aRect, aClipRect, aEffectChain, aOpacity, aTransform,
+ aVisibleRect);
+}
+
+template <typename Geometry>
+void CompositorOGL::DrawGeometry(const Geometry& aGeometry,
+ const gfx::Rect& aRect,
+ const gfx::IntRect& aClipRect,
+ const EffectChain& aEffectChain,
+ gfx::Float aOpacity,
+ const gfx::Matrix4x4& aTransform,
+ const gfx::Rect& aVisibleRect) {
+ MOZ_ASSERT(mFrameInProgress, "frame not started");
+ MOZ_ASSERT(mCurrentRenderTarget, "No destination");
+
+ MakeCurrent();
+
+ // Convert aClipRect into render target space, and intersect it with the
+ // render target's clip.
+ IntRect clipRect = aClipRect + mCurrentRenderTarget->GetClipSpaceOrigin();
+ if (Maybe<IntRect> rtClip = mCurrentRenderTarget->GetClipRect()) {
+ clipRect = clipRect.Intersect(*rtClip);
+ }
+
+ Rect destRect = aTransform.TransformAndClipBounds(
+ aRect, Rect(mCurrentRenderTarget->GetRect().Intersect(clipRect)));
+ if (destRect.IsEmpty()) {
+ return;
+ }
+
+ // Move clipRect into device space.
+ IntPoint offset = mCurrentRenderTarget->GetOrigin();
+ clipRect -= offset;
+
+ if (!mTarget && mCurrentRenderTarget->IsWindow()) {
+ clipRect.MoveBy(mSurfaceOrigin.x, -mSurfaceOrigin.y);
+ }
+
+ ScopedGLState scopedScissorTestState(mGLContext, LOCAL_GL_SCISSOR_TEST, true);
+ ScopedScissorRect autoScissorRect(mGLContext, clipRect.X(),
+ FlipY(clipRect.Y() + clipRect.Height()),
+ clipRect.Width(), clipRect.Height());
+
+ // Only apply DEAA to quads that have been transformed such that aliasing
+ // could be visible
+ bool bEnableAA = StaticPrefs::layers_deaa_enabled() &&
+ !aTransform.Is2DIntegerTranslation();
+
+ ShaderConfigOGL config =
+ GetShaderConfigFor(aEffectChain.mPrimaryEffect, bEnableAA);
+
+ config.SetOpacity(aOpacity != 1.f);
+ ApplyPrimitiveConfig(config, aGeometry);
+
+ ShaderProgramOGL* program = mProgramsHolder->ActivateProgram(config);
+ if (!program) {
+ return;
+ }
+ program->SetProjectionMatrix(mProjMatrix);
+ program->SetLayerTransform(aTransform);
+ program->SetRenderOffset(offset.x, offset.y);
+
+ if (aOpacity != 1.f) program->SetLayerOpacity(aOpacity);
+
+ if (config.mFeatures & ENABLE_TEXTURE_RECT) {
+ TextureSourceOGL* source = nullptr;
+ TexturedEffect* texturedEffect =
+ static_cast<TexturedEffect*>(aEffectChain.mPrimaryEffect.get());
+ source = texturedEffect->mTexture->AsSourceOGL();
+ // This is used by IOSurface that use 0,0...w,h coordinate rather then
+ // 0,0..1,1.
+ program->SetTexCoordMultiplier(source->GetSize().width,
+ source->GetSize().height);
+ }
+
+ // XXX kip - These calculations could be performed once per layer rather than
+ // for every tile. This might belong in Compositor.cpp once DEAA
+ // is implemented for DirectX.
+ if (bEnableAA) {
+ // Calculate the transformed vertices of aVisibleRect in screen space
+ // pixels, mirroring the calculations in the vertex shader
+ Matrix4x4 flatTransform = aTransform;
+ flatTransform.PostTranslate(-offset.x, -offset.y, 0.0f);
+ flatTransform *= mProjMatrix;
+
+ Rect viewportClip = Rect(-1.0f, -1.0f, 2.0f, 2.0f);
+ size_t edgeCount = 0;
+ Point3D coefficients[4];
+
+ Point points[Matrix4x4::kTransformAndClipRectMaxVerts];
+ size_t pointCount =
+ flatTransform.TransformAndClipRect(aVisibleRect, viewportClip, points);
+ for (size_t i = 0; i < pointCount; i++) {
+ points[i] = Point((points[i].x * 0.5f + 0.5f) * mViewportSize.width,
+ (points[i].y * 0.5f + 0.5f) * mViewportSize.height);
+ }
+ if (pointCount > 2) {
+ // Use shoelace formula on a triangle in the clipped quad to determine if
+ // winding order is reversed. Iterate through the triangles until one is
+ // found with a non-zero area.
+ float winding = 0.0f;
+ size_t wp = 0;
+ while (winding == 0.0f && wp < pointCount) {
+ int wp1 = (wp + 1) % pointCount;
+ int wp2 = (wp + 2) % pointCount;
+ winding = (points[wp1].x - points[wp].x).value *
+ (points[wp1].y + points[wp].y).value +
+ (points[wp2].x - points[wp1].x).value *
+ (points[wp2].y + points[wp1].y).value +
+ (points[wp].x - points[wp2].x).value *
+ (points[wp].y + points[wp2].y).value;
+ wp++;
+ }
+ bool frontFacing = winding >= 0.0f;
+
+ // Calculate the line coefficients used by the DEAA shader to determine
+ // the sub-pixel coverage of the edge pixels
+ for (size_t i = 0; i < pointCount; i++) {
+ const Point& p1 = points[i];
+ const Point& p2 = points[(i + 1) % pointCount];
+ // Create a DEAA edge for any non-straight lines, to a maximum of 4
+ if (p1.x != p2.x && p1.y != p2.y && edgeCount < 4) {
+ if (frontFacing) {
+ coefficients[edgeCount++] = GetLineCoefficients(p2, p1);
+ } else {
+ coefficients[edgeCount++] = GetLineCoefficients(p1, p2);
+ }
+ }
+ }
+ }
+
+ // The coefficients that are not needed must not cull any fragments.
+ // We fill these unused coefficients with a clipping plane that has no
+ // effect.
+ for (size_t i = edgeCount; i < 4; i++) {
+ coefficients[i] = Point3D(0.0f, 1.0f, mViewportSize.height);
+ }
+
+ // Set uniforms required by DEAA shader
+ Matrix4x4 transformInverted = aTransform;
+ transformInverted.Invert();
+ program->SetLayerTransformInverse(transformInverted);
+ program->SetDEAAEdges(coefficients);
+ program->SetVisibleCenter(aVisibleRect.Center());
+ program->SetViewportSize(mViewportSize);
+ }
+
+ bool didSetBlendMode = false;
+
+ switch (aEffectChain.mPrimaryEffect->mType) {
+ case EffectTypes::RGB: {
+ TexturedEffect* texturedEffect =
+ static_cast<TexturedEffect*>(aEffectChain.mPrimaryEffect.get());
+ TextureSource* source = texturedEffect->mTexture;
+
+ didSetBlendMode = SetBlendMode(gl(), gfx::CompositionOp::OP_OVER,
+ texturedEffect->mPremultiplied);
+
+ gfx::SamplingFilter samplingFilter = texturedEffect->mSamplingFilter;
+
+ source->AsSourceOGL()->BindTexture(LOCAL_GL_TEXTURE0, samplingFilter);
+
+ program->SetTextureUnit(0);
+
+ Matrix4x4 textureTransform = source->AsSourceOGL()->GetTextureTransform();
+ program->SetTextureTransform(textureTransform);
+
+ BindAndDrawGeometryWithTextureRect(
+ program, aGeometry, texturedEffect->mTextureCoords, source);
+ source->AsSourceOGL()->MaybeFenceTexture();
+ } break;
+ case EffectTypes::YCBCR: {
+ EffectYCbCr* effectYCbCr =
+ static_cast<EffectYCbCr*>(aEffectChain.mPrimaryEffect.get());
+ TextureSource* sourceYCbCr = effectYCbCr->mTexture;
+ const int Y = 0, Cb = 1, Cr = 2;
+ TextureSourceOGL* sourceY = sourceYCbCr->GetSubSource(Y)->AsSourceOGL();
+ TextureSourceOGL* sourceCb = sourceYCbCr->GetSubSource(Cb)->AsSourceOGL();
+ TextureSourceOGL* sourceCr = sourceYCbCr->GetSubSource(Cr)->AsSourceOGL();
+
+ if (!sourceY || !sourceCb || !sourceCr) {
+ NS_WARNING("Invalid layer texture.");
+ return;
+ }
+
+ sourceY->BindTexture(LOCAL_GL_TEXTURE0, effectYCbCr->mSamplingFilter);
+ sourceCb->BindTexture(LOCAL_GL_TEXTURE1, effectYCbCr->mSamplingFilter);
+ sourceCr->BindTexture(LOCAL_GL_TEXTURE2, effectYCbCr->mSamplingFilter);
+
+ if (config.mFeatures & ENABLE_TEXTURE_RECT) {
+ // This is used by IOSurface that use 0,0...w,h coordinate rather then
+ // 0,0..1,1.
+ program->SetCbCrTexCoordMultiplier(sourceCb->GetSize().width,
+ sourceCb->GetSize().height);
+ }
+
+ program->SetYCbCrTextureUnits(Y, Cb, Cr);
+ program->SetTextureTransform(Matrix4x4());
+ program->SetYUVColorSpace(effectYCbCr->mYUVColorSpace);
+
+ BindAndDrawGeometryWithTextureRect(program, aGeometry,
+ effectYCbCr->mTextureCoords,
+ sourceYCbCr->GetSubSource(Y));
+ sourceY->MaybeFenceTexture();
+ sourceCb->MaybeFenceTexture();
+ sourceCr->MaybeFenceTexture();
+ } break;
+ case EffectTypes::NV12: {
+ EffectNV12* effectNV12 =
+ static_cast<EffectNV12*>(aEffectChain.mPrimaryEffect.get());
+ TextureSource* sourceNV12 = effectNV12->mTexture;
+ const int Y = 0, CbCr = 1;
+ TextureSourceOGL* sourceY = sourceNV12->GetSubSource(Y)->AsSourceOGL();
+ TextureSourceOGL* sourceCbCr =
+ sourceNV12->GetSubSource(CbCr)->AsSourceOGL();
+
+ if (!sourceY || !sourceCbCr) {
+ NS_WARNING("Invalid layer texture.");
+ return;
+ }
+
+ sourceY->BindTexture(LOCAL_GL_TEXTURE0, effectNV12->mSamplingFilter);
+ sourceCbCr->BindTexture(LOCAL_GL_TEXTURE1, effectNV12->mSamplingFilter);
+
+ if (config.mFeatures & ENABLE_TEXTURE_RECT) {
+ // This is used by IOSurface that use 0,0...w,h coordinate rather then
+ // 0,0..1,1.
+ program->SetCbCrTexCoordMultiplier(sourceCbCr->GetSize().width,
+ sourceCbCr->GetSize().height);
+ }
+
+ program->SetNV12TextureUnits(Y, CbCr);
+ program->SetTextureTransform(Matrix4x4());
+ program->SetYUVColorSpace(effectNV12->mYUVColorSpace);
+
+ BindAndDrawGeometryWithTextureRect(program, aGeometry,
+ effectNV12->mTextureCoords,
+ sourceNV12->GetSubSource(Y));
+ sourceY->MaybeFenceTexture();
+ sourceCbCr->MaybeFenceTexture();
+ } break;
+ default:
+ MOZ_ASSERT(false, "Unhandled effect type");
+ break;
+ }
+
+ if (didSetBlendMode) {
+ gl()->fBlendFuncSeparate(LOCAL_GL_ONE, LOCAL_GL_ONE_MINUS_SRC_ALPHA,
+ LOCAL_GL_ONE, LOCAL_GL_ONE_MINUS_SRC_ALPHA);
+ }
+
+ // in case rendering has used some other GL context
+ MakeCurrent();
+}
+
+void CompositorOGL::BindAndDrawGeometry(ShaderProgramOGL* aProgram,
+ const gfx::Rect& aRect) {
+ BindAndDrawQuad(aProgram, aRect);
+}
+
+void CompositorOGL::BindAndDrawGeometry(
+ ShaderProgramOGL* aProgram,
+ const nsTArray<gfx::TexturedTriangle>& aTriangles) {
+ NS_ASSERTION(aProgram->HasInitialized(),
+ "Shader program not correctly initialized");
+
+ const nsTArray<TexturedVertex> vertices =
+ TexturedTrianglesToVertexArray(aTriangles);
+
+ mGLContext->fBindBuffer(LOCAL_GL_ARRAY_BUFFER, mTriangleVBO);
+ mGLContext->fBufferData(LOCAL_GL_ARRAY_BUFFER,
+ vertices.Length() * sizeof(TexturedVertex),
+ vertices.Elements(), LOCAL_GL_STREAM_DRAW);
+
+ const GLsizei stride = 4 * sizeof(GLfloat);
+ InitializeVAO(kCoordinateAttributeIndex, 2, stride, 0);
+ InitializeVAO(kTexCoordinateAttributeIndex, 2, stride, 2 * sizeof(GLfloat));
+
+ mGLContext->fDrawArrays(LOCAL_GL_TRIANGLES, 0, vertices.Length());
+
+ mGLContext->fDisableVertexAttribArray(kCoordinateAttributeIndex);
+ mGLContext->fDisableVertexAttribArray(kTexCoordinateAttributeIndex);
+ mGLContext->fBindBuffer(LOCAL_GL_ARRAY_BUFFER, 0);
+}
+
+// |aRect| is the rectangle we want to draw to. We will draw it with
+// up to 4 draw commands if necessary to avoid wrapping.
+// |aTexCoordRect| is the rectangle from the texture that we want to
+// draw using the given program.
+// |aTexture| is the texture we are drawing. Its actual size can be
+// larger than the rectangle given by |texCoordRect|.
+void CompositorOGL::BindAndDrawGeometryWithTextureRect(
+ ShaderProgramOGL* aProg, const Rect& aRect, const Rect& aTexCoordRect,
+ TextureSource* aTexture) {
+ Rect scaledTexCoordRect = GetTextureCoordinates(aTexCoordRect, aTexture);
+ Rect layerRects[4];
+ Rect textureRects[4];
+ size_t rects = DecomposeIntoNoRepeatRects(aRect, scaledTexCoordRect,
+ &layerRects, &textureRects);
+
+ BindAndDrawQuads(aProg, rects, layerRects, textureRects);
+}
+
+void CompositorOGL::BindAndDrawGeometryWithTextureRect(
+ ShaderProgramOGL* aProg, const nsTArray<gfx::TexturedTriangle>& aTriangles,
+ const gfx::Rect& aTexCoordRect, TextureSource* aTexture) {
+ BindAndDrawGeometry(aProg, aTriangles);
+}
+
+void CompositorOGL::BindAndDrawQuads(ShaderProgramOGL* aProg, int aQuads,
+ const Rect* aLayerRects,
+ const Rect* aTextureRects) {
+ NS_ASSERTION(aProg->HasInitialized(),
+ "Shader program not correctly initialized");
+
+ mGLContext->fBindBuffer(LOCAL_GL_ARRAY_BUFFER, mQuadVBO);
+ InitializeVAO(kCoordinateAttributeIndex, 4, 0, 0);
+
+ aProg->SetLayerRects(aLayerRects);
+ if (aProg->GetTextureCount() > 0) {
+ aProg->SetTextureRects(aTextureRects);
+ }
+
+ // We are using GL_TRIANGLES here because the Mac Intel drivers fail to
+ // properly process uniform arrays with GL_TRIANGLE_STRIP. Go figure.
+ mGLContext->fDrawArrays(LOCAL_GL_TRIANGLES, 0, 6 * aQuads);
+ mGLContext->fDisableVertexAttribArray(kCoordinateAttributeIndex);
+ mGLContext->fBindBuffer(LOCAL_GL_ARRAY_BUFFER, 0);
+}
+
+void CompositorOGL::InitializeVAO(const GLuint aAttrib, const GLint aComponents,
+ const GLsizei aStride, const size_t aOffset) {
+ mGLContext->fVertexAttribPointer(aAttrib, aComponents, LOCAL_GL_FLOAT,
+ LOCAL_GL_FALSE, aStride,
+ reinterpret_cast<GLvoid*>(aOffset));
+ mGLContext->fEnableVertexAttribArray(aAttrib);
+}
+
+#ifdef MOZ_DUMP_PAINTING
+template <typename T>
+void WriteSnapshotToDumpFile_internal(T* aObj, DataSourceSurface* aSurf) {
+ nsCString string(aObj->Name());
+ string.Append('-');
+ string.AppendInt((uint64_t)aObj);
+ if (gfxUtils::sDumpPaintFile != stderr) {
+ fprintf_stderr(gfxUtils::sDumpPaintFile, R"(array["%s"]=")",
+ string.BeginReading());
+ }
+ gfxUtils::DumpAsDataURI(aSurf, gfxUtils::sDumpPaintFile);
+ if (gfxUtils::sDumpPaintFile != stderr) {
+ fprintf_stderr(gfxUtils::sDumpPaintFile, R"(";)");
+ }
+}
+
+void WriteSnapshotToDumpFile(Compositor* aCompositor, DrawTarget* aTarget) {
+ RefPtr<SourceSurface> surf = aTarget->Snapshot();
+ RefPtr<DataSourceSurface> dSurf = surf->GetDataSurface();
+ WriteSnapshotToDumpFile_internal(aCompositor, dSurf);
+}
+#endif
+
+void CompositorOGL::EndFrame() {
+ AUTO_PROFILER_LABEL("CompositorOGL::EndFrame", GRAPHICS);
+
+#ifdef MOZ_DUMP_PAINTING
+ if (gfxEnv::MOZ_DISABLE_FORCE_PRESENT()) {
+ LayoutDeviceIntSize size;
+ if (mUseExternalSurfaceSize) {
+ size = LayoutDeviceIntSize(mSurfaceSize.width, mSurfaceSize.height);
+ } else {
+ size = mWidget->GetClientSize();
+ }
+ RefPtr<DrawTarget> target =
+ gfxPlatform::GetPlatform()->CreateOffscreenContentDrawTarget(
+ IntSize(size.width, size.height), SurfaceFormat::B8G8R8A8);
+ if (target) {
+ CopyToTarget(target, nsIntPoint(), Matrix());
+ WriteSnapshotToDumpFile(this, target);
+ }
+ }
+#endif
+
+ mFrameInProgress = false;
+ mShouldInvalidateWindow = false;
+
+ if (mTarget) {
+ CopyToTarget(mTarget, mTargetBounds.TopLeft(), Matrix());
+ mGLContext->fBindBuffer(LOCAL_GL_ARRAY_BUFFER, 0);
+ mTarget = nullptr;
+ mWindowRenderTarget = nullptr;
+ mCurrentRenderTarget = nullptr;
+ Compositor::EndFrame();
+ return;
+ }
+
+ mWindowRenderTarget = nullptr;
+ mCurrentRenderTarget = nullptr;
+
+ if (mTexturePool) {
+ mTexturePool->EndFrame();
+ }
+
+ InsertFrameDoneSync();
+
+ mGLContext->SetDamage(mCurrentFrameInvalidRegion);
+ mGLContext->SwapBuffers();
+ mGLContext->fBindBuffer(LOCAL_GL_ARRAY_BUFFER, 0);
+
+ // Unbind all textures
+ for (GLuint i = 0; i <= 4; i++) {
+ mGLContext->fActiveTexture(LOCAL_GL_TEXTURE0 + i);
+ mGLContext->fBindTexture(LOCAL_GL_TEXTURE_2D, 0);
+ if (!mGLContext->IsGLES()) {
+ mGLContext->fBindTexture(LOCAL_GL_TEXTURE_RECTANGLE_ARB, 0);
+ }
+ }
+
+ mCurrentFrameInvalidRegion.SetEmpty();
+
+ Compositor::EndFrame();
+}
+
+void CompositorOGL::InsertFrameDoneSync() {
+#ifdef XP_MACOSX
+ // Only do this on macOS.
+ // On other platforms, SwapBuffers automatically applies back-pressure.
+ if (mThisFrameDoneSync) {
+ mGLContext->fDeleteSync(mThisFrameDoneSync);
+ }
+ mThisFrameDoneSync =
+ mGLContext->fFenceSync(LOCAL_GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
+#elif defined(MOZ_WIDGET_ANDROID)
+ const auto& gle = gl::GLContextEGL::Cast(mGLContext);
+ const auto& egl = gle->mEgl;
+
+ EGLSync sync = nullptr;
+ if (AndroidHardwareBufferApi::Get()) {
+ sync = egl->fCreateSync(LOCAL_EGL_SYNC_NATIVE_FENCE_ANDROID, nullptr);
+ }
+ if (sync) {
+ int fenceFd = egl->fDupNativeFenceFDANDROID(sync);
+ if (fenceFd >= 0) {
+ mReleaseFenceFd = ipc::FileDescriptor(UniqueFileHandle(fenceFd));
+ }
+ egl->fDestroySync(sync);
+ sync = nullptr;
+ }
+#endif
+}
+
+ipc::FileDescriptor CompositorOGL::GetReleaseFence() { return mReleaseFenceFd; }
+
+bool CompositorOGL::NeedToRecreateFullWindowRenderTarget() const {
+ if (!ShouldRecordFrames()) {
+ return false;
+ }
+ if (!mFullWindowRenderTarget) {
+ return true;
+ }
+ IntSize windowSize = mWidget->GetClientSize().ToUnknownSize();
+ return mFullWindowRenderTarget->GetSize() != windowSize;
+}
+
+void CompositorOGL::SetDestinationSurfaceSize(const IntSize& aSize) {
+ mSurfaceSize.width = aSize.width;
+ mSurfaceSize.height = aSize.height;
+}
+
+void CompositorOGL::CopyToTarget(DrawTarget* aTarget,
+ const nsIntPoint& aTopLeft,
+ const gfx::Matrix& aTransform) {
+ MOZ_ASSERT(aTarget);
+ IntRect rect;
+ if (mUseExternalSurfaceSize) {
+ rect = IntRect(0, 0, mSurfaceSize.width, mSurfaceSize.height);
+ } else {
+ rect = IntRect(0, 0, mWidgetSize.width, mWidgetSize.height);
+ }
+ GLint width = rect.Width();
+ GLint height = rect.Height();
+
+ if ((int64_t(width) * int64_t(height) * int64_t(4)) > INT32_MAX) {
+ NS_ERROR("Widget size too big - integer overflow!");
+ return;
+ }
+
+ RefPtr<DataSourceSurface> source = Factory::CreateDataSourceSurface(
+ rect.Size(), gfx::SurfaceFormat::B8G8R8A8);
+ if (NS_WARN_IF(!source)) {
+ return;
+ }
+
+ ReadPixelsIntoDataSurface(mGLContext, source);
+
+ // Map from GL space to Cairo space and reverse the world transform.
+ Matrix glToCairoTransform = aTransform;
+ glToCairoTransform.Invert();
+ glToCairoTransform.PreScale(1.0, -1.0);
+ glToCairoTransform.PreTranslate(0.0, -height);
+
+ glToCairoTransform.PostTranslate(-aTopLeft.x, -aTopLeft.y);
+
+ Matrix oldMatrix = aTarget->GetTransform();
+ aTarget->SetTransform(glToCairoTransform);
+ Rect floatRect = Rect(rect.X(), rect.Y(), width, height);
+ aTarget->DrawSurface(source, floatRect, floatRect, DrawSurfaceOptions(),
+ DrawOptions(1.0f, CompositionOp::OP_SOURCE));
+ aTarget->SetTransform(oldMatrix);
+ aTarget->Flush();
+}
+
+void CompositorOGL::Pause() {
+#ifdef MOZ_WIDGET_ANDROID
+ if (!gl() || gl()->IsDestroyed()) return;
+ // ReleaseSurface internally calls MakeCurrent
+ gl()->ReleaseSurface();
+#elif defined(MOZ_WAYLAND)
+ // ReleaseSurface internally calls MakeCurrent
+ gl()->ReleaseSurface();
+#endif
+}
+
+bool CompositorOGL::Resume() {
+#if defined(MOZ_WIDGET_ANDROID) || defined(MOZ_WIDGET_UIKIT) || \
+ defined(MOZ_WAYLAND)
+ if (!gl() || gl()->IsDestroyed()) return false;
+
+ // RenewSurface internally calls MakeCurrent.
+ return gl()->RenewSurface(GetWidget());
+#else
+ return true;
+#endif
+}
+
+already_AddRefed<DataTextureSource> CompositorOGL::CreateDataTextureSource(
+ TextureFlags aFlags) {
+ if (!gl()) {
+ return nullptr;
+ }
+
+ return MakeAndAddRef<TextureImageTextureSourceOGL>(this, aFlags);
+}
+
+int32_t CompositorOGL::GetMaxTextureSize() const {
+ MOZ_ASSERT(mGLContext);
+ GLint texSize = 0;
+ mGLContext->fGetIntegerv(LOCAL_GL_MAX_TEXTURE_SIZE, &texSize);
+ MOZ_ASSERT(texSize != 0);
+ return texSize;
+}
+
+void CompositorOGL::MakeCurrent(MakeCurrentFlags aFlags) {
+ if (mDestroyed) {
+ NS_WARNING("Call on destroyed layer manager");
+ return;
+ }
+ mGLContext->MakeCurrent(aFlags & ForceMakeCurrent);
+}
+
+GLuint CompositorOGL::GetTemporaryTexture(GLenum aTarget, GLenum aUnit) {
+ if (!mTexturePool) {
+ mTexturePool = new PerUnitTexturePoolOGL(gl());
+ }
+ return mTexturePool->GetTexture(aTarget, aUnit);
+}
+
+GLuint PerUnitTexturePoolOGL::GetTexture(GLenum aTarget, GLenum aTextureUnit) {
+ if (mTextureTarget == 0) {
+ mTextureTarget = aTarget;
+ }
+ MOZ_ASSERT(mTextureTarget == aTarget);
+
+ size_t index = aTextureUnit - LOCAL_GL_TEXTURE0;
+ // lazily grow the array of temporary textures
+ if (mTextures.Length() <= index) {
+ size_t prevLength = mTextures.Length();
+ mTextures.SetLength(index + 1);
+ for (unsigned int i = prevLength; i <= index; ++i) {
+ mTextures[i] = 0;
+ }
+ }
+ // lazily initialize the temporary textures
+ if (!mTextures[index]) {
+ if (!mGL->MakeCurrent()) {
+ return 0;
+ }
+ mGL->fGenTextures(1, &mTextures[index]);
+ mGL->fBindTexture(aTarget, mTextures[index]);
+ mGL->fTexParameteri(aTarget, LOCAL_GL_TEXTURE_WRAP_S,
+ LOCAL_GL_CLAMP_TO_EDGE);
+ mGL->fTexParameteri(aTarget, LOCAL_GL_TEXTURE_WRAP_T,
+ LOCAL_GL_CLAMP_TO_EDGE);
+ }
+ return mTextures[index];
+}
+
+void PerUnitTexturePoolOGL::DestroyTextures() {
+ if (mGL && mGL->MakeCurrent()) {
+ if (mTextures.Length() > 0) {
+ mGL->fDeleteTextures(mTextures.Length(), &mTextures[0]);
+ }
+ }
+ mTextures.SetLength(0);
+}
+
+void CompositorOGL::RegisterTextureSource(TextureSource* aTextureSource) {
+#ifdef MOZ_WIDGET_GTK
+ if (mDestroyed) {
+ return;
+ }
+ mRegisteredTextureSources.insert(aTextureSource);
+#endif
+}
+
+void CompositorOGL::UnregisterTextureSource(TextureSource* aTextureSource) {
+#ifdef MOZ_WIDGET_GTK
+ if (mDestroyed) {
+ return;
+ }
+ mRegisteredTextureSources.erase(aTextureSource);
+#endif
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/opengl/CompositorOGL.h b/gfx/layers/opengl/CompositorOGL.h
new file mode 100644
index 0000000000..878372aa64
--- /dev/null
+++ b/gfx/layers/opengl/CompositorOGL.h
@@ -0,0 +1,446 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_GFX_COMPOSITOROGL_H
+#define MOZILLA_GFX_COMPOSITOROGL_H
+
+#include <map>
+#include <unordered_map>
+#include <unordered_set>
+
+#include "gfx2DGlue.h"
+#include "GLContextTypes.h" // for GLContext, etc
+#include "GLDefs.h" // for GLuint, LOCAL_GL_TEXTURE_2D, etc
+#include "OGLShaderConfig.h" // for ShaderConfigOGL
+#include "Units.h" // for ScreenPoint
+#include "mozilla/Assertions.h" // for MOZ_ASSERT, etc
+#include "mozilla/Attributes.h" // for override, final
+#include "mozilla/RefPtr.h" // for already_AddRefed, RefPtr
+#include "mozilla/gfx/2D.h" // for DrawTarget
+#include "mozilla/gfx/BaseSize.h" // for BaseSize
+#include "mozilla/gfx/MatrixFwd.h" // for Matrix4x4
+#include "mozilla/gfx/Point.h" // for IntSize, Point
+#include "mozilla/gfx/Rect.h" // for Rect, IntRect
+#include "mozilla/gfx/Triangle.h" // for Triangle
+#include "mozilla/gfx/Types.h" // for Float, SurfaceFormat, etc
+#include "mozilla/ipc/FileDescriptor.h"
+#include "mozilla/layers/Compositor.h" // for SurfaceInitMode, Compositor, etc
+#include "mozilla/layers/CompositorTypes.h" // for MaskType::MaskType::NumMaskTypes, etc
+#include "mozilla/layers/LayersTypes.h"
+#include "nsCOMPtr.h" // for already_AddRefed
+#include "nsDebug.h" // for NS_ASSERTION, NS_WARNING
+#include "nsISupportsImpl.h" // for MOZ_COUNT_CTOR, etc
+#include "nsTArray.h" // for AutoTArray, nsTArray, etc
+#include "nsThreadUtils.h" // for nsRunnable
+#include "nsXULAppAPI.h" // for XRE_GetProcessType
+#include "nscore.h" // for NS_IMETHOD
+
+class nsIWidget;
+
+namespace mozilla {
+
+namespace layers {
+
+class CompositingRenderTarget;
+class CompositingRenderTargetOGL;
+class DataTextureSource;
+class ShaderProgramOGL;
+class ShaderProgramOGLsHolder;
+class TextureSource;
+class TextureSourceOGL;
+class BufferTextureHost;
+struct Effect;
+struct EffectChain;
+
+/**
+ * Interface for pools of temporary gl textures for the compositor.
+ * The textures are fully owned by the pool, so the latter is responsible
+ * calling fDeleteTextures accordingly.
+ * Users of GetTexture receive a texture that is only valid for the duration
+ * of the current frame.
+ * This is primarily intended for direct texturing APIs that need to attach
+ * shared objects (such as an EGLImage) to a gl texture.
+ */
+class CompositorTexturePoolOGL {
+ protected:
+ virtual ~CompositorTexturePoolOGL() = default;
+
+ public:
+ NS_INLINE_DECL_REFCOUNTING(CompositorTexturePoolOGL)
+
+ virtual void Clear() = 0;
+
+ virtual GLuint GetTexture(GLenum aTarget, GLenum aEnum) = 0;
+
+ virtual void EndFrame() = 0;
+};
+
+/**
+ * Agressively reuses textures. One gl texture per texture unit in total.
+ * So far this hasn't shown the best results on b2g.
+ */
+class PerUnitTexturePoolOGL : public CompositorTexturePoolOGL {
+ public:
+ explicit PerUnitTexturePoolOGL(gl::GLContext* aGL);
+ virtual ~PerUnitTexturePoolOGL();
+
+ void Clear() override { DestroyTextures(); }
+
+ GLuint GetTexture(GLenum aTarget, GLenum aUnit) override;
+
+ void EndFrame() override {}
+
+ protected:
+ void DestroyTextures();
+
+ GLenum mTextureTarget;
+ nsTArray<GLuint> mTextures;
+ RefPtr<gl::GLContext> mGL;
+};
+
+// If you want to make this class not final, first remove calls to virtual
+// methods (Destroy) that are made in the destructor.
+class CompositorOGL final : public Compositor {
+ typedef mozilla::gl::GLContext GLContext;
+
+ friend class CompositingRenderTargetOGL;
+
+ RefPtr<ShaderProgramOGLsHolder> mProgramsHolder;
+
+ public:
+ explicit CompositorOGL(widget::CompositorWidget* aWidget,
+ int aSurfaceWidth = -1, int aSurfaceHeight = -1,
+ bool aUseExternalSurfaceSize = false);
+
+ protected:
+ virtual ~CompositorOGL();
+
+ public:
+ CompositorOGL* AsCompositorOGL() override { return this; }
+
+ already_AddRefed<DataTextureSource> CreateDataTextureSource(
+ TextureFlags aFlags = TextureFlags::NO_FLAGS) override;
+
+ bool Initialize(GLContext* aGLContext,
+ RefPtr<ShaderProgramOGLsHolder> aProgramsHolder,
+ nsCString* const out_failureReason);
+
+ bool Initialize(nsCString* const out_failureReason) override;
+
+ void Destroy() override;
+
+ // Returns a render target for the native layer.
+ // aInvalidRegion is in window coordinates, i.e. in the same space as
+ // aNativeLayer->GetPosition().
+ already_AddRefed<CompositingRenderTargetOGL> RenderTargetForNativeLayer(
+ NativeLayer* aNativeLayer, const gfx::IntRegion& aInvalidRegion);
+
+ already_AddRefed<CompositingRenderTarget> CreateRenderTarget(
+ const gfx::IntRect& aRect, SurfaceInitMode aInit) override;
+
+ void SetRenderTarget(CompositingRenderTarget* aSurface) override;
+ already_AddRefed<CompositingRenderTarget> GetCurrentRenderTarget()
+ const override;
+ already_AddRefed<CompositingRenderTarget> GetWindowRenderTarget()
+ const override;
+
+ bool ReadbackRenderTarget(CompositingRenderTarget* aSource,
+ AsyncReadbackBuffer* aDest) override;
+
+ already_AddRefed<AsyncReadbackBuffer> CreateAsyncReadbackBuffer(
+ const gfx::IntSize& aSize) override;
+
+ bool BlitRenderTarget(CompositingRenderTarget* aSource,
+ const gfx::IntSize& aSourceSize,
+ const gfx::IntSize& aDestSize) override;
+
+ void DrawQuad(const gfx::Rect& aRect, const gfx::IntRect& aClipRect,
+ const EffectChain& aEffectChain, gfx::Float aOpacity,
+ const gfx::Matrix4x4& aTransform,
+ const gfx::Rect& aVisibleRect) override;
+
+ void EndFrame() override;
+
+ int32_t GetMaxTextureSize() const override;
+
+ /**
+ * Set the size of the EGL surface we're rendering to, if we're rendering to
+ * an EGL surface.
+ */
+ void SetDestinationSurfaceSize(const gfx::IntSize& aSize) override;
+
+ typedef uint32_t MakeCurrentFlags;
+ static const MakeCurrentFlags ForceMakeCurrent = 0x1;
+ void MakeCurrent(MakeCurrentFlags aFlags = 0);
+
+#ifdef MOZ_DUMP_PAINTING
+ const char* Name() const override { return "OGL"; }
+#endif // MOZ_DUMP_PAINTING
+
+ void Pause() override;
+ bool Resume() override;
+
+ GLContext* gl() const { return mGLContext; }
+ GLContext* GetGLContext() const override { return mGLContext; }
+
+ /**
+ * Clear the program state. This must be called
+ * before operating on the GLContext directly. */
+ void ResetProgram();
+
+ gfx::SurfaceFormat GetFBOFormat() const {
+ return gfx::SurfaceFormat::R8G8B8A8;
+ }
+
+ /**
+ * The compositor provides with temporary textures for use with direct
+ * textruing.
+ */
+ GLuint GetTemporaryTexture(GLenum aTarget, GLenum aUnit);
+
+ const gfx::IntSize GetDestinationSurfaceSize() const {
+ return gfx::IntSize(mSurfaceSize.width, mSurfaceSize.height);
+ }
+
+ /**
+ * Allow the origin of the surface to be offset so that content does not
+ * start at (0, 0) on the surface.
+ */
+ void SetSurfaceOrigin(const ScreenIntPoint& aOrigin) {
+ mSurfaceOrigin = aOrigin;
+ }
+
+ // Register TextureSource which own device data that have to be deleted before
+ // destroying this CompositorOGL.
+ void RegisterTextureSource(TextureSource* aTextureSource);
+ void UnregisterTextureSource(TextureSource* aTextureSource);
+
+ ipc::FileDescriptor GetReleaseFence();
+
+ private:
+ template <typename Geometry>
+ void DrawGeometry(const Geometry& aGeometry, const gfx::Rect& aRect,
+ const gfx::IntRect& aClipRect,
+ const EffectChain& aEffectChain, gfx::Float aOpacity,
+ const gfx::Matrix4x4& aTransform,
+ const gfx::Rect& aVisibleRect);
+
+ void PrepareViewport(CompositingRenderTargetOGL* aRenderTarget);
+
+ void InsertFrameDoneSync();
+
+ bool NeedToRecreateFullWindowRenderTarget() const;
+
+ /** Widget associated with this compositor */
+ LayoutDeviceIntSize mWidgetSize;
+ RefPtr<GLContext> mGLContext;
+ bool mOwnsGLContext = true;
+ RefPtr<SurfacePoolHandle> mSurfacePoolHandle;
+ gfx::Matrix4x4 mProjMatrix;
+ bool mCanRenderToDefaultFramebuffer = true;
+
+ /** The size of the surface we are rendering to */
+ gfx::IntSize mSurfaceSize;
+
+ /** The origin of the content on the surface */
+ ScreenIntPoint mSurfaceOrigin;
+
+ already_AddRefed<mozilla::gl::GLContext> CreateContext();
+
+ /** Texture target to use for FBOs */
+ GLenum mFBOTextureTarget;
+
+ /** Currently bound render target */
+ RefPtr<CompositingRenderTargetOGL> mCurrentRenderTarget;
+
+ // The 1x1 dummy render target that's the "current" render target between
+ // BeginFrameForNativeLayers and EndFrame but outside pairs of
+ // Begin/EndRenderingToNativeLayer. Created on demand.
+ RefPtr<CompositingRenderTarget> mNativeLayersReferenceRT;
+
+ // The render target that profiler screenshots / frame recording read from.
+ // This will be the actual window framebuffer when rendering to a window, and
+ // it will be mFullWindowRenderTarget when rendering to native layers.
+ RefPtr<CompositingRenderTargetOGL> mWindowRenderTarget;
+
+ // Non-null when using native layers and frame recording is requested.
+ // EndNormalDrawing() maintains a copy of the entire window contents in this
+ // render target, by copying from the native layer render targets.
+ RefPtr<CompositingRenderTargetOGL> mFullWindowRenderTarget;
+
+ /**
+ * VBO that has some basics in it for a textured quad, including vertex
+ * coords and texcoords.
+ */
+ GLuint mQuadVBO;
+
+ /**
+ * VBO that stores dynamic triangle geometry.
+ */
+ GLuint mTriangleVBO;
+
+ // Used to apply back-pressure in WaitForPreviousFrameDoneSync().
+ GLsync mPreviousFrameDoneSync;
+ GLsync mThisFrameDoneSync;
+
+ bool mHasBGRA;
+
+ /**
+ * When rendering to some EGL surfaces (e.g. on Android), we rely on being
+ * told about size changes (via SetSurfaceSize) rather than pulling this
+ * information from the widget.
+ */
+ bool mUseExternalSurfaceSize;
+
+ /**
+ * Have we had DrawQuad calls since the last frame was rendered?
+ */
+ bool mFrameInProgress;
+
+ // Only true between BeginFromeForNativeLayers and EndFrame, and only if the
+ // full window render target needed to be recreated in the current frame.
+ bool mShouldInvalidateWindow = false;
+
+ /* Start a new frame.
+ */
+ Maybe<gfx::IntRect> BeginFrameForWindow(
+ const nsIntRegion& aInvalidRegion, const Maybe<gfx::IntRect>& aClipRect,
+ const gfx::IntRect& aRenderBounds,
+ const nsIntRegion& aOpaqueRegion) override;
+
+ Maybe<gfx::IntRect> BeginFrame(const nsIntRegion& aInvalidRegion,
+ const Maybe<gfx::IntRect>& aClipRect,
+ const gfx::IntRect& aRenderBounds,
+ const nsIntRegion& aOpaqueRegion);
+
+ ShaderConfigOGL GetShaderConfigFor(Effect* aEffect,
+ bool aDEAAEnabled = false) const;
+
+ ShaderProgramOGL* GetShaderProgramFor(const ShaderConfigOGL& aConfig);
+
+ void ApplyPrimitiveConfig(ShaderConfigOGL& aConfig, const gfx::Rect&) {
+ aConfig.SetDynamicGeometry(false);
+ }
+
+ void ApplyPrimitiveConfig(ShaderConfigOGL& aConfig,
+ const nsTArray<gfx::TexturedTriangle>&) {
+ aConfig.SetDynamicGeometry(true);
+ }
+
+ /**
+ * Create a FBO backed by a texture.
+ * Note that the texture target type will be
+ * of the type returned by FBOTextureTarget; different
+ * shaders are required to sample from the different
+ * texture types.
+ */
+ void CreateFBOWithTexture(const gfx::IntRect& aRect, bool aCopyFromSource,
+ GLuint aSourceFrameBuffer, GLuint* aFBO,
+ GLuint* aTexture,
+ gfx::IntSize* aAllocSize = nullptr);
+
+ GLuint CreateTexture(const gfx::IntRect& aRect, bool aCopyFromSource,
+ GLuint aSourceFrameBuffer,
+ gfx::IntSize* aAllocSize = nullptr);
+
+ gfx::Point3D GetLineCoefficients(const gfx::Point& aPoint1,
+ const gfx::Point& aPoint2);
+
+ void CleanupResources();
+
+ void BindAndDrawQuads(ShaderProgramOGL* aProg, int aQuads,
+ const gfx::Rect* aLayerRect,
+ const gfx::Rect* aTextureRect);
+
+ void BindAndDrawQuad(ShaderProgramOGL* aProg, const gfx::Rect& aLayerRect,
+ const gfx::Rect& aTextureRect = gfx::Rect(0.0f, 0.0f,
+ 1.0f, 1.0f)) {
+ gfx::Rect layerRects[4];
+ gfx::Rect textureRects[4];
+ layerRects[0] = aLayerRect;
+ textureRects[0] = aTextureRect;
+ BindAndDrawQuads(aProg, 1, layerRects, textureRects);
+ }
+
+ void BindAndDrawGeometry(ShaderProgramOGL* aProgram, const gfx::Rect& aRect);
+
+ void BindAndDrawGeometry(ShaderProgramOGL* aProgram,
+ const nsTArray<gfx::TexturedTriangle>& aTriangles);
+
+ void BindAndDrawGeometryWithTextureRect(ShaderProgramOGL* aProg,
+ const gfx::Rect& aRect,
+ const gfx::Rect& aTexCoordRect,
+ TextureSource* aTexture);
+
+ void BindAndDrawGeometryWithTextureRect(
+ ShaderProgramOGL* aProg,
+ const nsTArray<gfx::TexturedTriangle>& aTriangles,
+ const gfx::Rect& aTexCoordRect, TextureSource* aTexture);
+
+ void InitializeVAO(const GLuint aAttribIndex, const GLint aComponents,
+ const GLsizei aStride, const size_t aOffset);
+
+ gfx::Rect GetTextureCoordinates(gfx::Rect textureRect,
+ TextureSource* aTexture);
+
+ /**
+ * Copies the content of the current render target to the set transaction
+ * target.
+ */
+ void CopyToTarget(gfx::DrawTarget* aTarget, const nsIntPoint& aTopLeft,
+ const gfx::Matrix& aWorldMatrix);
+
+ /**
+ * Implements the flipping of the y-axis to convert from layers/compositor
+ * coordinates to OpenGL coordinates.
+ *
+ * Indeed, the only coordinate system that OpenGL knows has the y-axis
+ * pointing upwards, but the layers/compositor coordinate system has the
+ * y-axis pointing downwards, for good reason as Web pages are typically
+ * scrolled downwards. So, some flipping has to take place; FlippedY does it.
+ */
+ GLint FlipY(GLint y) const { return mViewportSize.height - y; }
+
+ // The DrawTarget from BeginFrameForTarget, which EndFrame needs to copy the
+ // window contents into.
+ // Only non-null between BeginFrameForTarget and EndFrame.
+ RefPtr<gfx::DrawTarget> mTarget;
+ gfx::IntRect mTargetBounds;
+
+ RefPtr<CompositorTexturePoolOGL> mTexturePool;
+
+ // The native layer that we're currently rendering to, if any.
+ // Non-null only between BeginFrame and EndFrame if BeginFrame has been called
+ // with a non-null aNativeLayer.
+ RefPtr<NativeLayer> mCurrentNativeLayer;
+
+#ifdef MOZ_WIDGET_GTK
+ // Hold TextureSources which own device data that have to be deleted before
+ // destroying this CompositorOGL.
+ std::unordered_set<TextureSource*> mRegisteredTextureSources;
+#endif
+
+ // FileDescriptor of release fence.
+ // Release fence is a fence that is used for waiting until usage/composite of
+ // AHardwareBuffer is ended. The fence is delivered to client side via
+ // ImageBridge. It is used only on android.
+ ipc::FileDescriptor mReleaseFenceFd;
+
+ bool mDestroyed;
+
+ /**
+ * Size of the OpenGL context's primary framebuffer in pixels. Used by
+ * FlipY for the y-flipping calculation and by the DEAA shader.
+ */
+ gfx::IntSize mViewportSize;
+
+ gfx::IntRegion mCurrentFrameInvalidRegion;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif /* MOZILLA_GFX_COMPOSITOROGL_H */
diff --git a/gfx/layers/opengl/DMABUFTextureClientOGL.cpp b/gfx/layers/opengl/DMABUFTextureClientOGL.cpp
new file mode 100644
index 0000000000..017c9aba8c
--- /dev/null
+++ b/gfx/layers/opengl/DMABUFTextureClientOGL.cpp
@@ -0,0 +1,123 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "DMABUFTextureClientOGL.h"
+#include "mozilla/widget/DMABufSurface.h"
+#include "gfxPlatform.h"
+
+namespace mozilla::layers {
+
+using namespace gfx;
+
+DMABUFTextureData::DMABUFTextureData(DMABufSurface* aSurface,
+ BackendType aBackend)
+ : mSurface(aSurface), mBackend(aBackend) {
+ MOZ_ASSERT(mSurface);
+}
+
+DMABUFTextureData::~DMABUFTextureData() = default;
+
+/* static */ DMABUFTextureData* DMABUFTextureData::Create(
+ const IntSize& aSize, SurfaceFormat aFormat, BackendType aBackend) {
+ if (aFormat != SurfaceFormat::B8G8R8A8 &&
+ aFormat != SurfaceFormat::B8G8R8X8) {
+ // TODO
+ NS_WARNING("DMABUFTextureData::Create() - wrong surface format!");
+ return nullptr;
+ }
+
+ int flags = DMABUF_TEXTURE;
+ if (aFormat == SurfaceFormat::B8G8R8A8) {
+ flags |= DMABUF_ALPHA;
+ }
+ RefPtr<DMABufSurface> surf =
+ DMABufSurfaceRGBA::CreateDMABufSurface(aSize.width, aSize.height, flags);
+ if (!surf) {
+ NS_WARNING("DMABUFTextureData::Create() failed!");
+ return nullptr;
+ }
+ return new DMABUFTextureData(surf, aBackend);
+}
+
+TextureData* DMABUFTextureData::CreateSimilar(
+ LayersIPCChannel* aAllocator, LayersBackend aLayersBackend,
+ TextureFlags aFlags, TextureAllocationFlags aAllocFlags) const {
+ return DMABUFTextureData::Create(
+ gfx::IntSize(mSurface->GetWidth(), mSurface->GetHeight()),
+ mSurface->GetFormat(), mBackend);
+}
+
+bool DMABUFTextureData::Serialize(SurfaceDescriptor& aOutDescriptor) {
+ return mSurface->Serialize(aOutDescriptor);
+}
+
+void DMABUFTextureData::GetSubDescriptor(
+ RemoteDecoderVideoSubDescriptor* const aOutDesc) {
+ SurfaceDescriptor desc;
+ if (!mSurface->Serialize(desc)) {
+ return;
+ }
+ *aOutDesc = static_cast<SurfaceDescriptorDMABuf>(desc);
+}
+
+void DMABUFTextureData::FillInfo(TextureData::Info& aInfo) const {
+ aInfo.size = gfx::IntSize(mSurface->GetWidth(), mSurface->GetHeight());
+ aInfo.format = mSurface->GetFormat();
+ aInfo.hasSynchronization = false;
+ aInfo.supportsMoz2D = true;
+ aInfo.canExposeMappedData = false;
+}
+
+bool DMABUFTextureData::Lock(OpenMode) {
+ auto surf = mSurface->GetAsDMABufSurfaceRGBA();
+ MOZ_ASSERT(!surf->IsMapped(), "Already locked?");
+ surf->Map();
+ return true;
+}
+
+void DMABUFTextureData::Unlock() {
+ auto surf = mSurface->GetAsDMABufSurfaceRGBA();
+ MOZ_ASSERT(surf->IsMapped(), "Already unlocked?");
+ surf->Unmap();
+}
+
+already_AddRefed<DataSourceSurface> DMABUFTextureData::GetAsSurface() {
+ // TODO: Update for debug purposes.
+ return nullptr;
+}
+
+already_AddRefed<DrawTarget> DMABUFTextureData::BorrowDrawTarget() {
+ MOZ_ASSERT(mBackend != BackendType::NONE);
+ if (mBackend == BackendType::NONE) {
+ // shouldn't happen, but degrade gracefully
+ return nullptr;
+ }
+ auto surf = mSurface->GetAsDMABufSurfaceRGBA();
+ if (!surf->GetMappedRegion()) {
+ return nullptr;
+ }
+ return Factory::CreateDrawTargetForData(
+ mBackend, (unsigned char*)surf->GetMappedRegion(),
+ IntSize(surf->GetWidth(), surf->GetHeight()),
+ surf->GetMappedRegionStride(), surf->GetFormat(), true);
+}
+
+void DMABUFTextureData::Deallocate(LayersIPCChannel*) { mSurface = nullptr; }
+
+void DMABUFTextureData::Forget(LayersIPCChannel*) { mSurface = nullptr; }
+
+bool DMABUFTextureData::UpdateFromSurface(gfx::SourceSurface* aSurface) {
+ RefPtr<DrawTarget> dt = BorrowDrawTarget();
+ if (!dt) {
+ return false;
+ }
+
+ dt->CopySurface(aSurface, IntRect(IntPoint(), aSurface->GetSize()),
+ IntPoint());
+ return true;
+}
+
+} // namespace mozilla::layers
diff --git a/gfx/layers/opengl/DMABUFTextureClientOGL.h b/gfx/layers/opengl/DMABUFTextureClientOGL.h
new file mode 100644
index 0000000000..f924159cc3
--- /dev/null
+++ b/gfx/layers/opengl/DMABUFTextureClientOGL.h
@@ -0,0 +1,67 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_GFX_MACIOSURFACETEXTURECLIENTOGL_H
+#define MOZILLA_GFX_MACIOSURFACETEXTURECLIENTOGL_H
+
+#include "mozilla/layers/TextureClientOGL.h"
+
+class DMABufSurface;
+
+namespace mozilla {
+namespace layers {
+
+class DMABUFTextureData : public TextureData {
+ public:
+ static DMABUFTextureData* Create(const gfx::IntSize& aSize,
+ gfx::SurfaceFormat aFormat,
+ gfx::BackendType aBackend);
+
+ static DMABUFTextureData* Create(DMABufSurface* aSurface,
+ gfx::BackendType aBackend) {
+ return new DMABUFTextureData(aSurface, aBackend);
+ }
+
+ ~DMABUFTextureData();
+
+ virtual TextureData* CreateSimilar(
+ LayersIPCChannel* aAllocator, LayersBackend aLayersBackend,
+ TextureFlags aFlags = TextureFlags::DEFAULT,
+ TextureAllocationFlags aAllocFlags = ALLOC_DEFAULT) const override;
+
+ void FillInfo(TextureData::Info& aInfo) const override;
+
+ bool Lock(OpenMode) override;
+
+ void Unlock() override;
+
+ already_AddRefed<gfx::DrawTarget> BorrowDrawTarget() override;
+
+ bool Serialize(SurfaceDescriptor& aOutDescriptor) override;
+
+ void GetSubDescriptor(
+ RemoteDecoderVideoSubDescriptor* const aOutDesc) override;
+
+ void Deallocate(LayersIPCChannel*) override;
+
+ void Forget(LayersIPCChannel*) override;
+
+ bool UpdateFromSurface(gfx::SourceSurface* aSurface) override;
+
+ // For debugging purposes only.
+ already_AddRefed<gfx::DataSourceSurface> GetAsSurface();
+
+ protected:
+ DMABUFTextureData(DMABufSurface* aSurface, gfx::BackendType aBackend);
+
+ RefPtr<DMABufSurface> mSurface;
+ gfx::BackendType mBackend;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // MOZILLA_GFX_MACIOSURFACETEXTURECLIENTOGL_H
diff --git a/gfx/layers/opengl/DMABUFTextureHostOGL.cpp b/gfx/layers/opengl/DMABUFTextureHostOGL.cpp
new file mode 100644
index 0000000000..19202038e8
--- /dev/null
+++ b/gfx/layers/opengl/DMABUFTextureHostOGL.cpp
@@ -0,0 +1,186 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "DMABUFTextureHostOGL.h"
+#include "mozilla/widget/DMABufSurface.h"
+#include "mozilla/webrender/RenderDMABUFTextureHost.h"
+#include "mozilla/webrender/RenderThread.h"
+#include "mozilla/webrender/WebRenderAPI.h"
+#include "GLContextEGL.h"
+
+namespace mozilla::layers {
+
+DMABUFTextureHostOGL::DMABUFTextureHostOGL(TextureFlags aFlags,
+ const SurfaceDescriptor& aDesc)
+ : TextureHost(TextureHostType::DMABUF, aFlags) {
+ MOZ_COUNT_CTOR(DMABUFTextureHostOGL);
+
+ // DMABufSurface::CreateDMABufSurface() can fail, for instance when we're run
+ // out of file descriptors.
+ mSurface =
+ DMABufSurface::CreateDMABufSurface(aDesc.get_SurfaceDescriptorDMABuf());
+}
+
+DMABUFTextureHostOGL::~DMABUFTextureHostOGL() {
+ MOZ_COUNT_DTOR(DMABUFTextureHostOGL);
+}
+
+gfx::SurfaceFormat DMABUFTextureHostOGL::GetFormat() const {
+ if (!mSurface) {
+ return gfx::SurfaceFormat::UNKNOWN;
+ }
+ return mSurface->GetFormat();
+}
+
+gfx::YUVColorSpace DMABUFTextureHostOGL::GetYUVColorSpace() const {
+ if (!mSurface) {
+ return gfx::YUVColorSpace::Identity;
+ }
+ return mSurface->GetYUVColorSpace();
+}
+
+gfx::ColorRange DMABUFTextureHostOGL::GetColorRange() const {
+ if (!mSurface) {
+ return gfx::ColorRange::LIMITED;
+ }
+ return mSurface->IsFullRange() ? gfx::ColorRange::FULL
+ : gfx::ColorRange::LIMITED;
+}
+
+uint32_t DMABUFTextureHostOGL::NumSubTextures() {
+ return mSurface ? mSurface->GetTextureCount() : 0;
+}
+
+gfx::IntSize DMABUFTextureHostOGL::GetSize() const {
+ if (!mSurface) {
+ return gfx::IntSize();
+ }
+ return gfx::IntSize(mSurface->GetWidth(), mSurface->GetHeight());
+}
+
+gl::GLContext* DMABUFTextureHostOGL::gl() const { return nullptr; }
+
+void DMABUFTextureHostOGL::CreateRenderTexture(
+ const wr::ExternalImageId& aExternalImageId) {
+ if (!mSurface) {
+ return;
+ }
+ RefPtr<wr::RenderTextureHost> texture =
+ new wr::RenderDMABUFTextureHost(mSurface);
+ wr::RenderThread::Get()->RegisterExternalImage(aExternalImageId,
+ texture.forget());
+}
+
+void DMABUFTextureHostOGL::PushResourceUpdates(
+ wr::TransactionBuilder& aResources, ResourceUpdateOp aOp,
+ const Range<wr::ImageKey>& aImageKeys, const wr::ExternalImageId& aExtID) {
+ if (!mSurface) {
+ return;
+ }
+
+ auto method = aOp == TextureHost::ADD_IMAGE
+ ? &wr::TransactionBuilder::AddExternalImage
+ : &wr::TransactionBuilder::UpdateExternalImage;
+ auto imageType =
+ wr::ExternalImageType::TextureHandle(wr::ImageBufferKind::Texture2D);
+
+ switch (mSurface->GetFormat()) {
+ case gfx::SurfaceFormat::R8G8B8X8:
+ case gfx::SurfaceFormat::R8G8B8A8:
+ case gfx::SurfaceFormat::B8G8R8X8:
+ case gfx::SurfaceFormat::B8G8R8A8: {
+ MOZ_ASSERT(aImageKeys.length() == 1);
+ // XXX Add RGBA handling. Temporary hack to avoid crash
+ // With BGRA format setting, rendering works without problem.
+ wr::ImageDescriptor descriptor(GetSize(), mSurface->GetFormat());
+ (aResources.*method)(aImageKeys[0], descriptor, aExtID, imageType, 0);
+ break;
+ }
+ case gfx::SurfaceFormat::NV12: {
+ MOZ_ASSERT(aImageKeys.length() == 2);
+ MOZ_ASSERT(mSurface->GetTextureCount() == 2);
+ wr::ImageDescriptor descriptor0(
+ gfx::IntSize(mSurface->GetWidth(0), mSurface->GetHeight(0)),
+ gfx::SurfaceFormat::A8);
+ wr::ImageDescriptor descriptor1(
+ gfx::IntSize(mSurface->GetWidth(1), mSurface->GetHeight(1)),
+ gfx::SurfaceFormat::R8G8);
+ (aResources.*method)(aImageKeys[0], descriptor0, aExtID, imageType, 0);
+ (aResources.*method)(aImageKeys[1], descriptor1, aExtID, imageType, 1);
+ break;
+ }
+ case gfx::SurfaceFormat::YUV: {
+ MOZ_ASSERT(aImageKeys.length() == 3);
+ MOZ_ASSERT(mSurface->GetTextureCount() == 3);
+ wr::ImageDescriptor descriptor0(
+ gfx::IntSize(mSurface->GetWidth(0), mSurface->GetHeight(0)),
+ gfx::SurfaceFormat::A8);
+ wr::ImageDescriptor descriptor1(
+ gfx::IntSize(mSurface->GetWidth(1), mSurface->GetHeight(1)),
+ gfx::SurfaceFormat::A8);
+ (aResources.*method)(aImageKeys[0], descriptor0, aExtID, imageType, 0);
+ (aResources.*method)(aImageKeys[1], descriptor1, aExtID, imageType, 1);
+ (aResources.*method)(aImageKeys[2], descriptor1, aExtID, imageType, 2);
+ break;
+ }
+ default: {
+ MOZ_ASSERT_UNREACHABLE("unexpected to be called");
+ }
+ }
+}
+
+void DMABUFTextureHostOGL::PushDisplayItems(
+ wr::DisplayListBuilder& aBuilder, const wr::LayoutRect& aBounds,
+ const wr::LayoutRect& aClip, wr::ImageRendering aFilter,
+ const Range<wr::ImageKey>& aImageKeys, PushDisplayItemFlagSet aFlags) {
+ if (!mSurface) {
+ return;
+ }
+ bool preferCompositorSurface =
+ aFlags.contains(PushDisplayItemFlag::PREFER_COMPOSITOR_SURFACE);
+ switch (mSurface->GetFormat()) {
+ case gfx::SurfaceFormat::R8G8B8X8:
+ case gfx::SurfaceFormat::R8G8B8A8:
+ case gfx::SurfaceFormat::B8G8R8A8:
+ case gfx::SurfaceFormat::B8G8R8X8: {
+ MOZ_ASSERT(aImageKeys.length() == 1);
+ aBuilder.PushImage(aBounds, aClip, true, false, aFilter, aImageKeys[0],
+ !(mFlags & TextureFlags::NON_PREMULTIPLIED),
+ wr::ColorF{1.0f, 1.0f, 1.0f, 1.0f},
+ preferCompositorSurface);
+ break;
+ }
+ case gfx::SurfaceFormat::NV12: {
+ MOZ_ASSERT(aImageKeys.length() == 2);
+ MOZ_ASSERT(mSurface->GetTextureCount() == 2);
+ // Those images can only be generated at present by the VAAPI H264 decoder
+ // which only supports 8 bits color depth.
+ aBuilder.PushNV12Image(aBounds, aClip, true, aImageKeys[0], aImageKeys[1],
+ wr::ColorDepth::Color8,
+ wr::ToWrYuvColorSpace(GetYUVColorSpace()),
+ wr::ToWrColorRange(GetColorRange()), aFilter,
+ preferCompositorSurface);
+ break;
+ }
+ case gfx::SurfaceFormat::YUV: {
+ MOZ_ASSERT(aImageKeys.length() == 3);
+ MOZ_ASSERT(mSurface->GetTextureCount() == 3);
+ // Those images can only be generated at present by the VAAPI vp8 decoder
+ // which only supports 8 bits color depth.
+ aBuilder.PushYCbCrPlanarImage(
+ aBounds, aClip, true, aImageKeys[0], aImageKeys[1], aImageKeys[2],
+ wr::ColorDepth::Color8, wr::ToWrYuvColorSpace(GetYUVColorSpace()),
+ wr::ToWrColorRange(GetColorRange()), aFilter,
+ preferCompositorSurface);
+ break;
+ }
+ default: {
+ MOZ_ASSERT_UNREACHABLE("unexpected to be called");
+ }
+ }
+}
+
+} // namespace mozilla::layers
diff --git a/gfx/layers/opengl/DMABUFTextureHostOGL.h b/gfx/layers/opengl/DMABUFTextureHostOGL.h
new file mode 100644
index 0000000000..13510a3c03
--- /dev/null
+++ b/gfx/layers/opengl/DMABUFTextureHostOGL.h
@@ -0,0 +1,69 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_GFX_DMABUFTEXTUREHOSTOGL_H
+#define MOZILLA_GFX_DMABUFTEXTUREHOSTOGL_H
+
+#include "mozilla/gfx/2D.h"
+#include "mozilla/layers/CompositorOGL.h"
+#include "mozilla/layers/TextureHostOGL.h"
+
+class DMABufSurface;
+
+namespace mozilla {
+namespace layers {
+
+/**
+ * A TextureHost for shared class DMABufSurface;
+ */
+class DMABUFTextureHostOGL : public TextureHost {
+ public:
+ DMABUFTextureHostOGL(TextureFlags aFlags, const SurfaceDescriptor& aDesc);
+ virtual ~DMABUFTextureHostOGL();
+
+ bool IsValid() override { return !!mSurface; }
+
+ gfx::SurfaceFormat GetFormat() const override;
+
+ already_AddRefed<gfx::DataSourceSurface> GetAsSurface() override {
+ return nullptr; // XXX - implement this (for MOZ_DUMP_PAINTING)
+ }
+
+ gl::GLContext* gl() const;
+
+ gfx::IntSize GetSize() const override;
+
+#ifdef MOZ_LAYERS_HAVE_LOG
+ const char* Name() override { return "DMABUFTextureHostOGL"; }
+#endif
+ uint32_t NumSubTextures() override;
+
+ gfx::YUVColorSpace GetYUVColorSpace() const override;
+ gfx::ColorRange GetColorRange() const override;
+
+ void CreateRenderTexture(
+ const wr::ExternalImageId& aExternalImageId) override;
+
+ void PushResourceUpdates(wr::TransactionBuilder& aResources,
+ ResourceUpdateOp aOp,
+ const Range<wr::ImageKey>& aImageKeys,
+ const wr::ExternalImageId& aExtID) override;
+
+ void PushDisplayItems(wr::DisplayListBuilder& aBuilder,
+ const wr::LayoutRect& aBounds,
+ const wr::LayoutRect& aClip, wr::ImageRendering aFilter,
+ const Range<wr::ImageKey>& aImageKeys,
+ PushDisplayItemFlagSet aFlags) override;
+
+ protected:
+ RefPtr<GLTextureSource> mTextureSource;
+ RefPtr<DMABufSurface> mSurface;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // MOZILLA_GFX_DMABUFTEXTUREHOSTOGL_H
diff --git a/gfx/layers/opengl/EGLImageHelpers.cpp b/gfx/layers/opengl/EGLImageHelpers.cpp
new file mode 100644
index 0000000000..a5abfe449f
--- /dev/null
+++ b/gfx/layers/opengl/EGLImageHelpers.cpp
@@ -0,0 +1,58 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "EGLImageHelpers.h"
+#include "GLContext.h"
+#include "GLLibraryEGL.h"
+
+namespace mozilla {
+namespace layers {
+
+using namespace gl;
+
+EGLImage EGLImageCreateFromNativeBuffer(GLContext* aGL, void* aBuffer,
+ const gfx::IntSize& aCropSize) {
+ EGLint attrs[] = {
+ LOCAL_EGL_IMAGE_PRESERVED,
+ LOCAL_EGL_TRUE,
+ LOCAL_EGL_NONE,
+ LOCAL_EGL_NONE,
+ };
+
+ EGLint cropAttrs[] = {
+ LOCAL_EGL_IMAGE_PRESERVED,
+ LOCAL_EGL_TRUE,
+ LOCAL_EGL_IMAGE_CROP_LEFT_ANDROID,
+ 0,
+ LOCAL_EGL_IMAGE_CROP_TOP_ANDROID,
+ 0,
+ LOCAL_EGL_IMAGE_CROP_RIGHT_ANDROID,
+ aCropSize.width,
+ LOCAL_EGL_IMAGE_CROP_BOTTOM_ANDROID,
+ aCropSize.height,
+ LOCAL_EGL_NONE,
+ LOCAL_EGL_NONE,
+ };
+
+ auto* egl = gl::GLLibraryEGL::Get();
+ bool hasCropRect = (aCropSize.width != 0 && aCropSize.height != 0);
+ EGLint* usedAttrs = attrs;
+ if (hasCropRect &&
+ egl->IsExtensionSupported(GLLibraryEGL::EGL_ANDROID_image_crop)) {
+ usedAttrs = cropAttrs;
+ }
+
+ return egl->fCreateImage(egl->Display(), EGL_NO_CONTEXT,
+ LOCAL_EGL_NATIVE_BUFFER_ANDROID, aBuffer, usedAttrs);
+}
+
+void EGLImageDestroy(GLContext* aGL, EGLImage aImage) {
+ auto* egl = gl::GLLibraryEGL::Get();
+ egl->fDestroyImage(egl->Display(), aImage);
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/opengl/EGLImageHelpers.h b/gfx/layers/opengl/EGLImageHelpers.h
new file mode 100644
index 0000000000..59eb944c0d
--- /dev/null
+++ b/gfx/layers/opengl/EGLImageHelpers.h
@@ -0,0 +1,28 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef EGLIMAGEHELPERS_H_
+#define EGLIMAGEHELPERS_H_
+
+#include "mozilla/gfx/Point.h"
+
+typedef void* EGLImage;
+
+namespace mozilla {
+namespace gl {
+class GLContext;
+}
+
+namespace layers {
+
+EGLImage EGLImageCreateFromNativeBuffer(gl::GLContext* aGL, void* aBuffer,
+ const gfx::IntSize& aCropSize);
+void EGLImageDestroy(gl::GLContext* aGL, EGLImage aImage);
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // EGLIMAGEHELPERS_H_
diff --git a/gfx/layers/opengl/MacIOSurfaceTextureClientOGL.cpp b/gfx/layers/opengl/MacIOSurfaceTextureClientOGL.cpp
new file mode 100644
index 0000000000..9a14daacc7
--- /dev/null
+++ b/gfx/layers/opengl/MacIOSurfaceTextureClientOGL.cpp
@@ -0,0 +1,120 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MacIOSurfaceTextureClientOGL.h"
+#include "mozilla/gfx/MacIOSurface.h"
+#include "MacIOSurfaceHelpers.h"
+#include "gfxPlatform.h"
+
+namespace mozilla {
+namespace layers {
+
+using namespace gfx;
+
+MacIOSurfaceTextureData::MacIOSurfaceTextureData(MacIOSurface* aSurface,
+ BackendType aBackend)
+ : mSurface(aSurface), mBackend(aBackend) {
+ MOZ_ASSERT(mSurface);
+}
+
+MacIOSurfaceTextureData::~MacIOSurfaceTextureData() = default;
+
+// static
+MacIOSurfaceTextureData* MacIOSurfaceTextureData::Create(MacIOSurface* aSurface,
+ BackendType aBackend) {
+ MOZ_ASSERT(aSurface);
+ if (!aSurface) {
+ return nullptr;
+ }
+ return new MacIOSurfaceTextureData(aSurface, aBackend);
+}
+
+MacIOSurfaceTextureData* MacIOSurfaceTextureData::Create(const IntSize& aSize,
+ SurfaceFormat aFormat,
+ BackendType aBackend) {
+ if (aFormat != SurfaceFormat::B8G8R8A8 &&
+ aFormat != SurfaceFormat::B8G8R8X8) {
+ return nullptr;
+ }
+
+ RefPtr<MacIOSurface> surf = MacIOSurface::CreateIOSurface(
+ aSize.width, aSize.height, aFormat == SurfaceFormat::B8G8R8A8);
+ if (!surf) {
+ return nullptr;
+ }
+
+ return Create(surf, aBackend);
+}
+
+bool MacIOSurfaceTextureData::Serialize(SurfaceDescriptor& aOutDescriptor) {
+ aOutDescriptor = SurfaceDescriptorMacIOSurface(mSurface->GetIOSurfaceID(),
+ !mSurface->HasAlpha(),
+ mSurface->GetYUVColorSpace());
+ return true;
+}
+
+void MacIOSurfaceTextureData::GetSubDescriptor(
+ RemoteDecoderVideoSubDescriptor* const aOutDesc) {
+ *aOutDesc = SurfaceDescriptorMacIOSurface(mSurface->GetIOSurfaceID(),
+ !mSurface->HasAlpha(),
+ mSurface->GetYUVColorSpace());
+}
+
+void MacIOSurfaceTextureData::FillInfo(TextureData::Info& aInfo) const {
+ aInfo.size = gfx::IntSize(mSurface->GetDevicePixelWidth(),
+ mSurface->GetDevicePixelHeight());
+ aInfo.format =
+ mSurface->HasAlpha() ? SurfaceFormat::B8G8R8A8 : SurfaceFormat::B8G8R8X8;
+ aInfo.hasSynchronization = false;
+ aInfo.supportsMoz2D = true;
+ aInfo.canExposeMappedData = false;
+}
+
+bool MacIOSurfaceTextureData::Lock(OpenMode) {
+ mSurface->Lock(false);
+ return true;
+}
+
+void MacIOSurfaceTextureData::Unlock() { mSurface->Unlock(false); }
+
+already_AddRefed<DataSourceSurface> MacIOSurfaceTextureData::GetAsSurface() {
+ RefPtr<SourceSurface> surf = CreateSourceSurfaceFromMacIOSurface(mSurface);
+ return surf->GetDataSurface();
+}
+
+already_AddRefed<DrawTarget> MacIOSurfaceTextureData::BorrowDrawTarget() {
+ MOZ_ASSERT(mBackend != BackendType::NONE);
+ if (mBackend == BackendType::NONE) {
+ // shouldn't happen, but degrade gracefully
+ return nullptr;
+ }
+ return Factory::CreateDrawTargetForData(
+ mBackend, (unsigned char*)mSurface->GetBaseAddress(),
+ IntSize(mSurface->GetWidth(), mSurface->GetHeight()),
+ mSurface->GetBytesPerRow(),
+ mSurface->HasAlpha() ? SurfaceFormat::B8G8R8A8 : SurfaceFormat::B8G8R8X8,
+ true);
+}
+
+void MacIOSurfaceTextureData::Deallocate(LayersIPCChannel*) {
+ mSurface = nullptr;
+}
+
+void MacIOSurfaceTextureData::Forget(LayersIPCChannel*) { mSurface = nullptr; }
+
+bool MacIOSurfaceTextureData::UpdateFromSurface(gfx::SourceSurface* aSurface) {
+ RefPtr<DrawTarget> dt = BorrowDrawTarget();
+ if (!dt) {
+ return false;
+ }
+
+ dt->CopySurface(aSurface, IntRect(IntPoint(), aSurface->GetSize()),
+ IntPoint());
+ return true;
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/opengl/MacIOSurfaceTextureClientOGL.h b/gfx/layers/opengl/MacIOSurfaceTextureClientOGL.h
new file mode 100644
index 0000000000..f7206e0b1b
--- /dev/null
+++ b/gfx/layers/opengl/MacIOSurfaceTextureClientOGL.h
@@ -0,0 +1,60 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_GFX_MACIOSURFACETEXTURECLIENTOGL_H
+#define MOZILLA_GFX_MACIOSURFACETEXTURECLIENTOGL_H
+
+#include "mozilla/layers/TextureClientOGL.h"
+
+class MacIOSurface;
+
+namespace mozilla {
+namespace layers {
+
+class MacIOSurfaceTextureData : public TextureData {
+ public:
+ static MacIOSurfaceTextureData* Create(MacIOSurface* aSurface,
+ gfx::BackendType aBackend);
+
+ static MacIOSurfaceTextureData* Create(const gfx::IntSize& aSize,
+ gfx::SurfaceFormat aFormat,
+ gfx::BackendType aBackend);
+
+ ~MacIOSurfaceTextureData();
+
+ void FillInfo(TextureData::Info& aInfo) const override;
+
+ bool Lock(OpenMode) override;
+
+ void Unlock() override;
+
+ already_AddRefed<gfx::DrawTarget> BorrowDrawTarget() override;
+
+ bool Serialize(SurfaceDescriptor& aOutDescriptor) override;
+
+ void GetSubDescriptor(
+ RemoteDecoderVideoSubDescriptor* const aOutDesc) override;
+
+ void Deallocate(LayersIPCChannel*) override;
+
+ void Forget(LayersIPCChannel*) override;
+
+ bool UpdateFromSurface(gfx::SourceSurface* aSurface) override;
+
+ // For debugging purposes only.
+ already_AddRefed<gfx::DataSourceSurface> GetAsSurface();
+
+ protected:
+ MacIOSurfaceTextureData(MacIOSurface* aSurface, gfx::BackendType aBackend);
+
+ RefPtr<MacIOSurface> mSurface;
+ gfx::BackendType mBackend;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // MOZILLA_GFX_MACIOSURFACETEXTURECLIENTOGL_H
diff --git a/gfx/layers/opengl/MacIOSurfaceTextureHostOGL.cpp b/gfx/layers/opengl/MacIOSurfaceTextureHostOGL.cpp
new file mode 100644
index 0000000000..4e47f6ae8b
--- /dev/null
+++ b/gfx/layers/opengl/MacIOSurfaceTextureHostOGL.cpp
@@ -0,0 +1,240 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MacIOSurfaceTextureHostOGL.h"
+#include "mozilla/gfx/gfxVars.h"
+#include "mozilla/gfx/MacIOSurface.h"
+#include "mozilla/webrender/RenderMacIOSurfaceTextureHost.h"
+#include "mozilla/webrender/RenderThread.h"
+#include "mozilla/webrender/WebRenderAPI.h"
+#include "GLContextCGL.h"
+
+namespace mozilla {
+namespace layers {
+
+MacIOSurfaceTextureHostOGL::MacIOSurfaceTextureHostOGL(
+ TextureFlags aFlags, const SurfaceDescriptorMacIOSurface& aDescriptor)
+ : TextureHost(TextureHostType::MacIOSurface, aFlags) {
+ MOZ_COUNT_CTOR(MacIOSurfaceTextureHostOGL);
+ mSurface = MacIOSurface::LookupSurface(aDescriptor.surfaceId(),
+ !aDescriptor.isOpaque(),
+ aDescriptor.yUVColorSpace());
+ if (!mSurface) {
+ gfxCriticalNote << "Failed to look up MacIOSurface";
+ }
+}
+
+MacIOSurfaceTextureHostOGL::~MacIOSurfaceTextureHostOGL() {
+ MOZ_COUNT_DTOR(MacIOSurfaceTextureHostOGL);
+}
+
+gfx::SurfaceFormat MacIOSurfaceTextureHostOGL::GetFormat() const {
+ if (!mSurface) {
+ return gfx::SurfaceFormat::UNKNOWN;
+ }
+ return mSurface->GetFormat();
+}
+
+gfx::SurfaceFormat MacIOSurfaceTextureHostOGL::GetReadFormat() const {
+ if (!mSurface) {
+ return gfx::SurfaceFormat::UNKNOWN;
+ }
+ return mSurface->GetReadFormat();
+}
+
+gfx::IntSize MacIOSurfaceTextureHostOGL::GetSize() const {
+ if (!mSurface) {
+ return gfx::IntSize();
+ }
+ return gfx::IntSize(mSurface->GetDevicePixelWidth(),
+ mSurface->GetDevicePixelHeight());
+}
+
+gl::GLContext* MacIOSurfaceTextureHostOGL::gl() const { return nullptr; }
+
+gfx::YUVColorSpace MacIOSurfaceTextureHostOGL::GetYUVColorSpace() const {
+ if (!mSurface) {
+ return gfx::YUVColorSpace::Identity;
+ }
+ return mSurface->GetYUVColorSpace();
+}
+
+gfx::ColorRange MacIOSurfaceTextureHostOGL::GetColorRange() const {
+ if (!mSurface) {
+ return gfx::ColorRange::LIMITED;
+ }
+ return mSurface->IsFullRange() ? gfx::ColorRange::FULL
+ : gfx::ColorRange::LIMITED;
+}
+
+void MacIOSurfaceTextureHostOGL::CreateRenderTexture(
+ const wr::ExternalImageId& aExternalImageId) {
+ RefPtr<wr::RenderTextureHost> texture =
+ new wr::RenderMacIOSurfaceTextureHost(GetMacIOSurface());
+
+ bool isDRM = (bool)(mFlags & TextureFlags::DRM_SOURCE);
+ texture->SetIsFromDRMSource(isDRM);
+
+ wr::RenderThread::Get()->RegisterExternalImage(aExternalImageId,
+ texture.forget());
+}
+
+uint32_t MacIOSurfaceTextureHostOGL::NumSubTextures() {
+ if (!mSurface) {
+ return 0;
+ }
+
+ switch (GetFormat()) {
+ case gfx::SurfaceFormat::R8G8B8X8:
+ case gfx::SurfaceFormat::R8G8B8A8:
+ case gfx::SurfaceFormat::B8G8R8A8:
+ case gfx::SurfaceFormat::B8G8R8X8:
+ case gfx::SurfaceFormat::YUV422: {
+ return 1;
+ }
+ case gfx::SurfaceFormat::NV12:
+ case gfx::SurfaceFormat::P010: {
+ return 2;
+ }
+ default: {
+ MOZ_ASSERT_UNREACHABLE("unexpected format");
+ return 1;
+ }
+ }
+}
+
+void MacIOSurfaceTextureHostOGL::PushResourceUpdates(
+ wr::TransactionBuilder& aResources, ResourceUpdateOp aOp,
+ const Range<wr::ImageKey>& aImageKeys, const wr::ExternalImageId& aExtID) {
+ MOZ_ASSERT(mSurface);
+
+ auto method = aOp == TextureHost::ADD_IMAGE
+ ? &wr::TransactionBuilder::AddExternalImage
+ : &wr::TransactionBuilder::UpdateExternalImage;
+ auto imageType =
+ wr::ExternalImageType::TextureHandle(wr::ImageBufferKind::TextureRect);
+
+ switch (GetFormat()) {
+ case gfx::SurfaceFormat::B8G8R8A8:
+ case gfx::SurfaceFormat::B8G8R8X8: {
+ MOZ_ASSERT(aImageKeys.length() == 1);
+ MOZ_ASSERT(mSurface->GetPlaneCount() == 0);
+ // The internal pixel format of MacIOSurface is always BGRX or BGRA
+ // format.
+ auto format = GetFormat() == gfx::SurfaceFormat::B8G8R8A8
+ ? gfx::SurfaceFormat::B8G8R8A8
+ : gfx::SurfaceFormat::B8G8R8X8;
+ wr::ImageDescriptor descriptor(GetSize(), format);
+ (aResources.*method)(aImageKeys[0], descriptor, aExtID, imageType, 0);
+ break;
+ }
+ case gfx::SurfaceFormat::YUV422: {
+ // This is the special buffer format. The buffer contents could be a
+ // converted RGB interleaving data or a YCbCr interleaving data depending
+ // on the different platform setting. (e.g. It will be RGB at OpenGL 2.1
+ // and YCbCr at OpenGL 3.1)
+ MOZ_ASSERT(aImageKeys.length() == 1);
+ MOZ_ASSERT(mSurface->GetPlaneCount() == 0);
+ wr::ImageDescriptor descriptor(GetSize(), gfx::SurfaceFormat::B8G8R8X8);
+ (aResources.*method)(aImageKeys[0], descriptor, aExtID, imageType, 0);
+ break;
+ }
+ case gfx::SurfaceFormat::NV12: {
+ MOZ_ASSERT(aImageKeys.length() == 2);
+ MOZ_ASSERT(mSurface->GetPlaneCount() == 2);
+ wr::ImageDescriptor descriptor0(
+ gfx::IntSize(mSurface->GetDevicePixelWidth(0),
+ mSurface->GetDevicePixelHeight(0)),
+ gfx::SurfaceFormat::A8);
+ wr::ImageDescriptor descriptor1(
+ gfx::IntSize(mSurface->GetDevicePixelWidth(1),
+ mSurface->GetDevicePixelHeight(1)),
+ gfx::SurfaceFormat::R8G8);
+ (aResources.*method)(aImageKeys[0], descriptor0, aExtID, imageType, 0);
+ (aResources.*method)(aImageKeys[1], descriptor1, aExtID, imageType, 1);
+ break;
+ }
+ case gfx::SurfaceFormat::P010: {
+ MOZ_ASSERT(aImageKeys.length() == 2);
+ MOZ_ASSERT(mSurface->GetPlaneCount() == 2);
+ wr::ImageDescriptor descriptor0(
+ gfx::IntSize(mSurface->GetDevicePixelWidth(0),
+ mSurface->GetDevicePixelHeight(0)),
+ gfx::SurfaceFormat::A16);
+ wr::ImageDescriptor descriptor1(
+ gfx::IntSize(mSurface->GetDevicePixelWidth(1),
+ mSurface->GetDevicePixelHeight(1)),
+ gfx::SurfaceFormat::R16G16);
+ (aResources.*method)(aImageKeys[0], descriptor0, aExtID, imageType, 0);
+ (aResources.*method)(aImageKeys[1], descriptor1, aExtID, imageType, 1);
+ break;
+ }
+ default: {
+ MOZ_ASSERT_UNREACHABLE("unexpected to be called");
+ }
+ }
+}
+
+void MacIOSurfaceTextureHostOGL::PushDisplayItems(
+ wr::DisplayListBuilder& aBuilder, const wr::LayoutRect& aBounds,
+ const wr::LayoutRect& aClip, wr::ImageRendering aFilter,
+ const Range<wr::ImageKey>& aImageKeys, PushDisplayItemFlagSet aFlags) {
+ bool preferCompositorSurface =
+ aFlags.contains(PushDisplayItemFlag::PREFER_COMPOSITOR_SURFACE);
+ switch (GetFormat()) {
+ case gfx::SurfaceFormat::B8G8R8A8:
+ case gfx::SurfaceFormat::B8G8R8X8: {
+ MOZ_ASSERT(aImageKeys.length() == 1);
+ MOZ_ASSERT(mSurface->GetPlaneCount() == 0);
+ // We disable external compositing for RGB surfaces for now until
+ // we've tested support more thoroughly. Bug 1667917.
+ aBuilder.PushImage(aBounds, aClip, true, false, aFilter, aImageKeys[0],
+ !(mFlags & TextureFlags::NON_PREMULTIPLIED),
+ wr::ColorF{1.0f, 1.0f, 1.0f, 1.0f},
+ preferCompositorSurface,
+ /* aSupportsExternalCompositing */ true);
+ break;
+ }
+ case gfx::SurfaceFormat::YUV422: {
+ MOZ_ASSERT(aImageKeys.length() == 1);
+ MOZ_ASSERT(mSurface->GetPlaneCount() == 0);
+ // Those images can only be generated at present by the Apple H264 decoder
+ // which only supports 8 bits color depth.
+ aBuilder.PushYCbCrInterleavedImage(
+ aBounds, aClip, true, aImageKeys[0], wr::ColorDepth::Color8,
+ wr::ToWrYuvColorSpace(GetYUVColorSpace()),
+ wr::ToWrColorRange(GetColorRange()), aFilter, preferCompositorSurface,
+ /* aSupportsExternalCompositing */ true);
+ break;
+ }
+ case gfx::SurfaceFormat::NV12: {
+ MOZ_ASSERT(aImageKeys.length() == 2);
+ MOZ_ASSERT(mSurface->GetPlaneCount() == 2);
+ aBuilder.PushNV12Image(
+ aBounds, aClip, true, aImageKeys[0], aImageKeys[1],
+ wr::ColorDepth::Color8, wr::ToWrYuvColorSpace(GetYUVColorSpace()),
+ wr::ToWrColorRange(GetColorRange()), aFilter, preferCompositorSurface,
+ /* aSupportsExternalCompositing */ true);
+ break;
+ }
+ case gfx::SurfaceFormat::P010: {
+ MOZ_ASSERT(aImageKeys.length() == 2);
+ MOZ_ASSERT(mSurface->GetPlaneCount() == 2);
+ aBuilder.PushP010Image(
+ aBounds, aClip, true, aImageKeys[0], aImageKeys[1],
+ wr::ColorDepth::Color10, wr::ToWrYuvColorSpace(GetYUVColorSpace()),
+ wr::ToWrColorRange(GetColorRange()), aFilter, preferCompositorSurface,
+ /* aSupportsExternalCompositing */ true);
+ break;
+ }
+ default: {
+ MOZ_ASSERT_UNREACHABLE("unexpected to be called");
+ }
+ }
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/opengl/MacIOSurfaceTextureHostOGL.h b/gfx/layers/opengl/MacIOSurfaceTextureHostOGL.h
new file mode 100644
index 0000000000..d4aaa630de
--- /dev/null
+++ b/gfx/layers/opengl/MacIOSurfaceTextureHostOGL.h
@@ -0,0 +1,84 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_GFX_MACIOSURFACETEXTUREHOSTOGL_H
+#define MOZILLA_GFX_MACIOSURFACETEXTUREHOSTOGL_H
+
+#include "MacIOSurfaceHelpers.h"
+#include "mozilla/gfx/2D.h"
+#include "mozilla/layers/CompositorOGL.h"
+#include "mozilla/layers/TextureHostOGL.h"
+
+class MacIOSurface;
+
+namespace mozilla {
+namespace layers {
+
+/**
+ * A TextureHost for shared MacIOSurface
+ *
+ * Most of the logic actually happens in MacIOSurfaceTextureSourceOGL.
+ */
+class MacIOSurfaceTextureHostOGL : public TextureHost {
+ public:
+ MacIOSurfaceTextureHostOGL(TextureFlags aFlags,
+ const SurfaceDescriptorMacIOSurface& aDescriptor);
+ virtual ~MacIOSurfaceTextureHostOGL();
+
+ // MacIOSurfaceTextureSourceOGL doesn't own any GL texture
+ void DeallocateDeviceData() override {}
+
+ gfx::SurfaceFormat GetFormat() const override;
+ gfx::SurfaceFormat GetReadFormat() const override;
+
+ already_AddRefed<gfx::DataSourceSurface> GetAsSurface() override {
+ RefPtr<gfx::SourceSurface> surf =
+ CreateSourceSurfaceFromMacIOSurface(GetMacIOSurface());
+ return surf->GetDataSurface();
+ }
+
+ gl::GLContext* gl() const;
+
+ gfx::IntSize GetSize() const override;
+
+#ifdef MOZ_LAYERS_HAVE_LOG
+ const char* Name() override { return "MacIOSurfaceTextureHostOGL"; }
+#endif
+
+ MacIOSurfaceTextureHostOGL* AsMacIOSurfaceTextureHost() override {
+ return this;
+ }
+
+ MacIOSurface* GetMacIOSurface() override { return mSurface; }
+
+ void CreateRenderTexture(
+ const wr::ExternalImageId& aExternalImageId) override;
+
+ uint32_t NumSubTextures() override;
+
+ void PushResourceUpdates(wr::TransactionBuilder& aResources,
+ ResourceUpdateOp aOp,
+ const Range<wr::ImageKey>& aImageKeys,
+ const wr::ExternalImageId& aExtID) override;
+
+ void PushDisplayItems(wr::DisplayListBuilder& aBuilder,
+ const wr::LayoutRect& aBounds,
+ const wr::LayoutRect& aClip, wr::ImageRendering aFilter,
+ const Range<wr::ImageKey>& aImageKeys,
+ PushDisplayItemFlagSet aFlags) override;
+
+ gfx::YUVColorSpace GetYUVColorSpace() const override;
+ gfx::ColorRange GetColorRange() const override;
+
+ protected:
+ RefPtr<GLTextureSource> mTextureSource;
+ RefPtr<MacIOSurface> mSurface;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // MOZILLA_GFX_MACIOSURFACETEXTUREHOSTOGL_H
diff --git a/gfx/layers/opengl/OGLShaderConfig.h b/gfx/layers/opengl/OGLShaderConfig.h
new file mode 100644
index 0000000000..f7c939aa98
--- /dev/null
+++ b/gfx/layers/opengl/OGLShaderConfig.h
@@ -0,0 +1,255 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GFX_OGLSHADERCONFIG_H
+#define GFX_OGLSHADERCONFIG_H
+
+#include "gfxTypes.h"
+#include "ImageTypes.h"
+#include "mozilla/Assertions.h" // for MOZ_ASSERT, etc
+#include "mozilla/RefPtr.h" // for RefPtr
+#include "mozilla/gfx/Matrix.h" // for Matrix4x4
+#include "mozilla/gfx/Rect.h" // for Rect
+#include "mozilla/gfx/Types.h"
+#include "nsDebug.h" // for NS_ASSERTION
+#include "nsPoint.h" // for nsIntPoint
+#include "nsTArray.h" // for nsTArray
+#include "mozilla/layers/CompositorTypes.h"
+
+namespace mozilla {
+namespace layers {
+
+enum ShaderFeatures {
+ ENABLE_RENDER_COLOR = 0x01,
+ ENABLE_TEXTURE_RECT = 0x02,
+ ENABLE_TEXTURE_EXTERNAL = 0x04,
+ ENABLE_TEXTURE_YCBCR = 0x08,
+ ENABLE_TEXTURE_NV12 = 0x10,
+ ENABLE_TEXTURE_COMPONENT_ALPHA = 0x20,
+ ENABLE_TEXTURE_NO_ALPHA = 0x40,
+ ENABLE_TEXTURE_RB_SWAP = 0x80,
+ ENABLE_OPACITY = 0x100,
+ ENABLE_BLUR = 0x200,
+ ENABLE_COLOR_MATRIX = 0x400,
+ ENABLE_MASK = 0x800,
+ ENABLE_NO_PREMUL_ALPHA = 0x1000,
+ ENABLE_DEAA = 0x2000,
+ ENABLE_DYNAMIC_GEOMETRY = 0x4000,
+ ENABLE_MASK_TEXTURE_RECT = 0x8000,
+ ENABLE_TEXTURE_NV12_GA_SWITCH = 0x10000,
+};
+
+class KnownUniform {
+ public:
+ // this needs to be kept in sync with strings in 'AddUniforms'
+ enum KnownUniformName {
+ NotAKnownUniform = -1,
+
+ LayerTransform = 0,
+ LayerTransformInverse,
+ MaskTransform,
+ BackdropTransform,
+ LayerRects,
+ MatrixProj,
+ TextureTransform,
+ TextureRects,
+ RenderTargetOffset,
+ LayerOpacity,
+ Texture,
+ YTexture,
+ CbTexture,
+ CrTexture,
+ RenderColor,
+ TexCoordMultiplier,
+ CbCrTexCoordMultiplier,
+ SSEdges,
+ ViewportSize,
+ VisibleCenter,
+ YuvColorMatrix,
+ YuvOffsetVector,
+
+ KnownUniformCount
+ };
+
+ KnownUniform() {
+ mName = NotAKnownUniform;
+ mNameString = nullptr;
+ mLocation = -1;
+ memset(&mValue, 0, sizeof(mValue));
+ }
+
+ bool UpdateUniform(int32_t i1) {
+ if (mLocation == -1) return false;
+ if (mValue.i1 != i1) {
+ mValue.i1 = i1;
+ return true;
+ }
+ return false;
+ }
+
+ bool UpdateUniform(float f1) {
+ if (mLocation == -1) return false;
+ if (mValue.f1 != f1) {
+ mValue.f1 = f1;
+ return true;
+ }
+ return false;
+ }
+
+ bool UpdateUniform(float f1, float f2) {
+ if (mLocation == -1) return false;
+ if (mValue.f16v[0] != f1 || mValue.f16v[1] != f2) {
+ mValue.f16v[0] = f1;
+ mValue.f16v[1] = f2;
+ return true;
+ }
+ return false;
+ }
+
+ bool UpdateUniform(float f1, float f2, float f3, float f4) {
+ if (mLocation == -1) return false;
+ if (mValue.f16v[0] != f1 || mValue.f16v[1] != f2 || mValue.f16v[2] != f3 ||
+ mValue.f16v[3] != f4) {
+ mValue.f16v[0] = f1;
+ mValue.f16v[1] = f2;
+ mValue.f16v[2] = f3;
+ mValue.f16v[3] = f4;
+ return true;
+ }
+ return false;
+ }
+
+ bool UpdateUniform(int cnt, const float* fp) {
+ if (mLocation == -1) return false;
+ switch (cnt) {
+ case 1:
+ case 2:
+ case 3:
+ case 4:
+ case 9:
+ case 16:
+ if (memcmp(mValue.f16v, fp, sizeof(float) * cnt) != 0) {
+ memcpy(mValue.f16v, fp, sizeof(float) * cnt);
+ return true;
+ }
+ return false;
+ }
+
+ MOZ_ASSERT_UNREACHABLE("cnt must be 1 2 3 4 9 or 16");
+ return false;
+ }
+
+ bool UpdateArrayUniform(int cnt, const float* fp) {
+ if (mLocation == -1) return false;
+ if (cnt > 16) {
+ return false;
+ }
+
+ if (memcmp(mValue.f16v, fp, sizeof(float) * cnt) != 0) {
+ memcpy(mValue.f16v, fp, sizeof(float) * cnt);
+ return true;
+ }
+ return false;
+ }
+
+ bool UpdateArrayUniform(int cnt, const gfx::Point3D* points) {
+ if (mLocation == -1) return false;
+ if (cnt > 4) {
+ return false;
+ }
+
+ float fp[12];
+ float* d = fp;
+ for (int i = 0; i < cnt; i++) {
+ // Note: Do not want to make assumptions about .x, .y, .z member packing.
+ // If gfx::Point3D is updated to make this guarantee, SIMD optimizations
+ // may be possible
+ *d++ = points[i].x;
+ *d++ = points[i].y;
+ *d++ = points[i].z;
+ }
+
+ if (memcmp(mValue.f16v, fp, sizeof(float) * cnt * 3) != 0) {
+ memcpy(mValue.f16v, fp, sizeof(float) * cnt * 3);
+ return true;
+ }
+ return false;
+ }
+
+ KnownUniformName mName;
+ const char* mNameString;
+ int32_t mLocation;
+
+ union {
+ int i1;
+ float f1;
+ float f16v[16];
+ } mValue;
+};
+
+class ShaderConfigOGL {
+ public:
+ ShaderConfigOGL()
+ : mFeatures(0),
+ mMultiplier(1),
+ mCompositionOp(gfx::CompositionOp::OP_OVER) {}
+
+ void SetRenderColor(bool aEnabled);
+ void SetTextureTarget(GLenum aTarget);
+ void SetMaskTextureTarget(GLenum aTarget);
+ void SetRBSwap(bool aEnabled);
+ void SetNoAlpha(bool aEnabled);
+ void SetOpacity(bool aEnabled);
+ void SetYCbCr(bool aEnabled);
+ void SetNV12(bool aEnabled);
+ void SetComponentAlpha(bool aEnabled);
+ void SetColorMatrix(bool aEnabled);
+ void SetBlur(bool aEnabled);
+ void SetMask(bool aEnabled);
+ void SetDEAA(bool aEnabled);
+ void SetCompositionOp(gfx::CompositionOp aOp);
+ void SetNoPremultipliedAlpha();
+ void SetDynamicGeometry(bool aEnabled);
+ void SetColorMultiplier(uint32_t aMultiplier);
+
+ bool operator<(const ShaderConfigOGL& other) const {
+ return mFeatures < other.mFeatures ||
+ (mFeatures == other.mFeatures &&
+ (int)mCompositionOp < (int)other.mCompositionOp) ||
+ (mFeatures == other.mFeatures &&
+ (int)mCompositionOp == (int)other.mCompositionOp &&
+ mMultiplier < other.mMultiplier);
+ }
+
+ public:
+ void SetFeature(int aBitmask, bool aState) {
+ if (aState)
+ mFeatures |= aBitmask;
+ else
+ mFeatures &= (~aBitmask);
+ }
+
+ int mFeatures;
+ uint32_t mMultiplier;
+ gfx::CompositionOp mCompositionOp;
+};
+
+static inline ShaderConfigOGL ShaderConfigFromTargetAndFormat(
+ GLenum aTarget, gfx::SurfaceFormat aFormat) {
+ ShaderConfigOGL config;
+ config.SetTextureTarget(aTarget);
+ config.SetRBSwap(aFormat == gfx::SurfaceFormat::B8G8R8A8 ||
+ aFormat == gfx::SurfaceFormat::B8G8R8X8);
+ config.SetNoAlpha(aFormat == gfx::SurfaceFormat::B8G8R8X8 ||
+ aFormat == gfx::SurfaceFormat::R8G8B8X8 ||
+ aFormat == gfx::SurfaceFormat::R5G6B5_UINT16);
+ return config;
+}
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // GFX_OGLSHADERCONFIG_H
diff --git a/gfx/layers/opengl/OGLShaderProgram.cpp b/gfx/layers/opengl/OGLShaderProgram.cpp
new file mode 100644
index 0000000000..a5efa07f48
--- /dev/null
+++ b/gfx/layers/opengl/OGLShaderProgram.cpp
@@ -0,0 +1,1058 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "OGLShaderProgram.h"
+
+#include <stdint.h> // for uint32_t
+
+#include <sstream> // for std::ostringstream
+
+#include "GLContext.h"
+#include "gfxEnv.h"
+#include "gfxRect.h" // for gfxRect
+#include "gfxUtils.h"
+#include "mozilla/DebugOnly.h" // for DebugOnly
+#include "mozilla/gfx/Logging.h"
+#include "mozilla/layers/Compositor.h" // for BlendOpIsMixBlendMode
+#include "nsAString.h"
+#include "nsString.h" // for nsAutoCString
+
+namespace mozilla {
+namespace layers {
+
+using std::endl;
+
+#define GAUSSIAN_KERNEL_HALF_WIDTH 11
+#define GAUSSIAN_KERNEL_STEP 0.2
+
+static void AddUniforms(ProgramProfileOGL& aProfile) {
+ // This needs to be kept in sync with the KnownUniformName enum
+ static const char* sKnownUniformNames[] = {"uLayerTransform",
+ "uLayerTransformInverse",
+ "uMaskTransform",
+ "uBackdropTransform",
+ "uLayerRects",
+ "uMatrixProj",
+ "uTextureTransform",
+ "uTextureRects",
+ "uRenderTargetOffset",
+ "uLayerOpacity",
+ "uTexture",
+ "uYTexture",
+ "uCbTexture",
+ "uCrTexture",
+ "uRenderColor",
+ "uTexCoordMultiplier",
+ "uCbCrTexCoordMultiplier",
+ "uSSEdges",
+ "uViewportSize",
+ "uVisibleCenter",
+ "uYuvColorMatrix",
+ "uYuvOffsetVector",
+ nullptr};
+
+ for (int i = 0; sKnownUniformNames[i] != nullptr; ++i) {
+ aProfile.mUniforms[i].mNameString = sKnownUniformNames[i];
+ aProfile.mUniforms[i].mName = (KnownUniform::KnownUniformName)i;
+ }
+}
+
+void ShaderConfigOGL::SetRenderColor(bool aEnabled) {
+ SetFeature(ENABLE_RENDER_COLOR, aEnabled);
+}
+
+void ShaderConfigOGL::SetTextureTarget(GLenum aTarget) {
+ SetFeature(ENABLE_TEXTURE_EXTERNAL | ENABLE_TEXTURE_RECT, false);
+ switch (aTarget) {
+ case LOCAL_GL_TEXTURE_EXTERNAL:
+ SetFeature(ENABLE_TEXTURE_EXTERNAL, true);
+ break;
+ case LOCAL_GL_TEXTURE_RECTANGLE_ARB:
+ SetFeature(ENABLE_TEXTURE_RECT, true);
+ break;
+ }
+}
+
+void ShaderConfigOGL::SetMaskTextureTarget(GLenum aTarget) {
+ if (aTarget == LOCAL_GL_TEXTURE_RECTANGLE_ARB) {
+ SetFeature(ENABLE_MASK_TEXTURE_RECT, true);
+ } else {
+ MOZ_ASSERT(aTarget == LOCAL_GL_TEXTURE_2D);
+ SetFeature(ENABLE_MASK_TEXTURE_RECT, false);
+ }
+}
+
+void ShaderConfigOGL::SetRBSwap(bool aEnabled) {
+ SetFeature(ENABLE_TEXTURE_RB_SWAP, aEnabled);
+}
+
+void ShaderConfigOGL::SetNoAlpha(bool aEnabled) {
+ SetFeature(ENABLE_TEXTURE_NO_ALPHA, aEnabled);
+}
+
+void ShaderConfigOGL::SetOpacity(bool aEnabled) {
+ SetFeature(ENABLE_OPACITY, aEnabled);
+}
+
+void ShaderConfigOGL::SetYCbCr(bool aEnabled) {
+ SetFeature(ENABLE_TEXTURE_YCBCR, aEnabled);
+ MOZ_ASSERT(!(mFeatures & ENABLE_TEXTURE_NV12));
+}
+
+void ShaderConfigOGL::SetColorMultiplier(uint32_t aMultiplier) {
+ MOZ_ASSERT(mFeatures & ENABLE_TEXTURE_YCBCR,
+ "Multiplier only supported with YCbCr!");
+ mMultiplier = aMultiplier;
+}
+
+void ShaderConfigOGL::SetNV12(bool aEnabled) {
+ SetFeature(ENABLE_TEXTURE_NV12, aEnabled);
+ MOZ_ASSERT(!(mFeatures & ENABLE_TEXTURE_YCBCR));
+#ifdef MOZ_WAYLAND
+ SetFeature(ENABLE_TEXTURE_NV12_GA_SWITCH, aEnabled);
+#endif
+}
+
+void ShaderConfigOGL::SetComponentAlpha(bool aEnabled) {
+ SetFeature(ENABLE_TEXTURE_COMPONENT_ALPHA, aEnabled);
+}
+
+void ShaderConfigOGL::SetColorMatrix(bool aEnabled) {
+ SetFeature(ENABLE_COLOR_MATRIX, aEnabled);
+}
+
+void ShaderConfigOGL::SetBlur(bool aEnabled) {
+ SetFeature(ENABLE_BLUR, aEnabled);
+}
+
+void ShaderConfigOGL::SetMask(bool aEnabled) {
+ SetFeature(ENABLE_MASK, aEnabled);
+}
+
+void ShaderConfigOGL::SetNoPremultipliedAlpha() {
+ SetFeature(ENABLE_NO_PREMUL_ALPHA, true);
+}
+
+void ShaderConfigOGL::SetDEAA(bool aEnabled) {
+ SetFeature(ENABLE_DEAA, aEnabled);
+}
+
+void ShaderConfigOGL::SetCompositionOp(gfx::CompositionOp aOp) {
+ mCompositionOp = aOp;
+}
+
+void ShaderConfigOGL::SetDynamicGeometry(bool aEnabled) {
+ SetFeature(ENABLE_DYNAMIC_GEOMETRY, aEnabled);
+}
+
+/* static */
+ProgramProfileOGL ProgramProfileOGL::GetProfileFor(ShaderConfigOGL aConfig) {
+ ProgramProfileOGL result;
+ std::ostringstream fs, vs;
+
+ AddUniforms(result);
+
+ gfx::CompositionOp blendOp = aConfig.mCompositionOp;
+
+ vs << "#ifdef GL_ES" << endl;
+ vs << "#define EDGE_PRECISION mediump" << endl;
+ vs << "#else" << endl;
+ vs << "#define EDGE_PRECISION" << endl;
+ vs << "#endif" << endl;
+ vs << "uniform mat4 uMatrixProj;" << endl;
+ vs << "uniform vec4 uLayerRects[4];" << endl;
+ vs << "uniform mat4 uLayerTransform;" << endl;
+ if (aConfig.mFeatures & ENABLE_DEAA) {
+ vs << "uniform mat4 uLayerTransformInverse;" << endl;
+ vs << "uniform EDGE_PRECISION vec3 uSSEdges[4];" << endl;
+ vs << "uniform vec2 uVisibleCenter;" << endl;
+ vs << "uniform vec2 uViewportSize;" << endl;
+ }
+ vs << "uniform vec2 uRenderTargetOffset;" << endl;
+
+ if (!(aConfig.mFeatures & ENABLE_DYNAMIC_GEOMETRY)) {
+ vs << "attribute vec4 aCoord;" << endl;
+ } else {
+ vs << "attribute vec2 aCoord;" << endl;
+ }
+
+ result.mAttributes.AppendElement(std::pair<nsCString, GLuint>{"aCoord", 0});
+
+ if (!(aConfig.mFeatures & ENABLE_RENDER_COLOR)) {
+ vs << "uniform mat4 uTextureTransform;" << endl;
+ vs << "uniform vec4 uTextureRects[4];" << endl;
+ vs << "varying vec2 vTexCoord;" << endl;
+
+ if (aConfig.mFeatures & ENABLE_DYNAMIC_GEOMETRY) {
+ vs << "attribute vec2 aTexCoord;" << endl;
+ result.mAttributes.AppendElement(
+ std::pair<nsCString, GLuint>{"aTexCoord", 1});
+ }
+ }
+
+ if (BlendOpIsMixBlendMode(blendOp)) {
+ vs << "uniform mat4 uBackdropTransform;" << endl;
+ vs << "varying vec2 vBackdropCoord;" << endl;
+ }
+
+ if (aConfig.mFeatures & ENABLE_MASK) {
+ vs << "uniform mat4 uMaskTransform;" << endl;
+ vs << "varying vec3 vMaskCoord;" << endl;
+ }
+
+ vs << "void main() {" << endl;
+
+ if (aConfig.mFeatures & ENABLE_DYNAMIC_GEOMETRY) {
+ vs << " vec4 finalPosition = vec4(aCoord.xy, 0.0, 1.0);" << endl;
+ } else {
+ vs << " int vertexID = int(aCoord.w);" << endl;
+ vs << " vec4 layerRect = uLayerRects[vertexID];" << endl;
+ vs << " vec4 finalPosition = vec4(aCoord.xy * layerRect.zw + "
+ "layerRect.xy, 0.0, 1.0);"
+ << endl;
+ }
+
+ vs << " finalPosition = uLayerTransform * finalPosition;" << endl;
+
+ if (aConfig.mFeatures & ENABLE_DEAA) {
+ // XXX kip - The DEAA shader could be made simpler if we switch to
+ // using dynamic vertex buffers instead of sending everything
+ // in through uniforms. This would enable passing information
+ // about how to dilate each vertex explicitly and eliminate the
+ // need to extrapolate this with the sub-pixel coverage
+ // calculation in the vertex shader.
+
+ // Calculate the screen space position of this vertex, in screen pixels
+ vs << " vec4 ssPos = finalPosition;" << endl;
+ vs << " ssPos.xy -= uRenderTargetOffset * finalPosition.w;" << endl;
+ vs << " ssPos = uMatrixProj * ssPos;" << endl;
+ vs << " ssPos.xy = ((ssPos.xy/ssPos.w)*0.5+0.5)*uViewportSize;" << endl;
+
+ if (aConfig.mFeatures & ENABLE_MASK ||
+ !(aConfig.mFeatures & ENABLE_RENDER_COLOR)) {
+ vs << " vec4 coordAdjusted;" << endl;
+ vs << " coordAdjusted.xy = aCoord.xy;" << endl;
+ }
+
+ // It is necessary to dilate edges away from uVisibleCenter to ensure that
+ // fragments with less than 50% sub-pixel coverage will be shaded.
+ // This offset is applied when the sub-pixel coverage of the vertex is
+ // less than 100%. Expanding by 0.5 pixels in screen space is sufficient
+ // to include these pixels.
+ vs << " if (dot(uSSEdges[0], vec3(ssPos.xy, 1.0)) < 1.5 ||" << endl;
+ vs << " dot(uSSEdges[1], vec3(ssPos.xy, 1.0)) < 1.5 ||" << endl;
+ vs << " dot(uSSEdges[2], vec3(ssPos.xy, 1.0)) < 1.5 ||" << endl;
+ vs << " dot(uSSEdges[3], vec3(ssPos.xy, 1.0)) < 1.5) {" << endl;
+ // If the shader reaches this branch, then this vertex is on the edge of
+ // the layer's visible rect and should be dilated away from the center of
+ // the visible rect. We don't want to hit this for inner facing
+ // edges between tiles, as the pixels may be covered twice without clipping
+ // against uSSEdges. If all edges were dilated, it would result in
+ // artifacts visible within semi-transparent layers with multiple tiles.
+ vs << " vec4 visibleCenter = uLayerTransform * vec4(uVisibleCenter, "
+ "0.0, 1.0);"
+ << endl;
+ vs << " vec2 dilateDir = finalPosition.xy / finalPosition.w - "
+ "visibleCenter.xy / visibleCenter.w;"
+ << endl;
+ vs << " vec2 offset = sign(dilateDir) * 0.5;" << endl;
+ vs << " finalPosition.xy += offset * finalPosition.w;" << endl;
+ if (!(aConfig.mFeatures & ENABLE_RENDER_COLOR)) {
+ // We must adjust the texture coordinates to compensate for the dilation
+ vs << " coordAdjusted = uLayerTransformInverse * finalPosition;"
+ << endl;
+ vs << " coordAdjusted /= coordAdjusted.w;" << endl;
+
+ if (!(aConfig.mFeatures & ENABLE_DYNAMIC_GEOMETRY)) {
+ vs << " coordAdjusted.xy -= layerRect.xy;" << endl;
+ vs << " coordAdjusted.xy /= layerRect.zw;" << endl;
+ }
+ }
+ vs << " }" << endl;
+
+ if (!(aConfig.mFeatures & ENABLE_RENDER_COLOR)) {
+ if (aConfig.mFeatures & ENABLE_DYNAMIC_GEOMETRY) {
+ vs << " vTexCoord = (uTextureTransform * vec4(aTexCoord, 0.0, "
+ "1.0)).xy;"
+ << endl;
+ } else {
+ vs << " vec4 textureRect = uTextureRects[vertexID];" << endl;
+ vs << " vec2 texCoord = coordAdjusted.xy * textureRect.zw + "
+ "textureRect.xy;"
+ << endl;
+ vs << " vTexCoord = (uTextureTransform * vec4(texCoord, 0.0, 1.0)).xy;"
+ << endl;
+ }
+ }
+ } else if (!(aConfig.mFeatures & ENABLE_RENDER_COLOR)) {
+ if (aConfig.mFeatures & ENABLE_DYNAMIC_GEOMETRY) {
+ vs << " vTexCoord = (uTextureTransform * vec4(aTexCoord, 0.0, 1.0)).xy;"
+ << endl;
+ } else {
+ vs << " vec4 textureRect = uTextureRects[vertexID];" << endl;
+ vs << " vec2 texCoord = aCoord.xy * textureRect.zw + textureRect.xy;"
+ << endl;
+ vs << " vTexCoord = (uTextureTransform * vec4(texCoord, 0.0, 1.0)).xy;"
+ << endl;
+ }
+ }
+
+ if (aConfig.mFeatures & ENABLE_MASK) {
+ vs << " vMaskCoord.xy = (uMaskTransform * (finalPosition / "
+ "finalPosition.w)).xy;"
+ << endl;
+ // correct for perspective correct interpolation, see comment in D3D11
+ // shader
+ vs << " vMaskCoord.z = 1.0;" << endl;
+ vs << " vMaskCoord *= finalPosition.w;" << endl;
+ }
+ vs << " finalPosition.xy -= uRenderTargetOffset * finalPosition.w;" << endl;
+ vs << " finalPosition = uMatrixProj * finalPosition;" << endl;
+ if (BlendOpIsMixBlendMode(blendOp)) {
+ // Translate from clip space (-1, 1) to (0..1), apply the backdrop
+ // transform, then invert the y-axis.
+ vs << " vBackdropCoord.x = (finalPosition.x + 1.0) / 2.0;" << endl;
+ vs << " vBackdropCoord.y = 1.0 - (finalPosition.y + 1.0) / 2.0;" << endl;
+ vs << " vBackdropCoord = (uBackdropTransform * vec4(vBackdropCoord.xy, "
+ "0.0, 1.0)).xy;"
+ << endl;
+ vs << " vBackdropCoord.y = 1.0 - vBackdropCoord.y;" << endl;
+ }
+ vs << " gl_Position = finalPosition;" << endl;
+ vs << "}" << endl;
+
+ if (aConfig.mFeatures & ENABLE_TEXTURE_RECT) {
+ fs << "#extension GL_ARB_texture_rectangle : require" << endl;
+ }
+ if (aConfig.mFeatures & ENABLE_TEXTURE_EXTERNAL) {
+ fs << "#extension GL_OES_EGL_image_external : require" << endl;
+ }
+ fs << "#ifdef GL_ES" << endl;
+ fs << "precision mediump float;" << endl;
+ fs << "#define COLOR_PRECISION lowp" << endl;
+ fs << "#define EDGE_PRECISION mediump" << endl;
+ fs << "#else" << endl;
+ fs << "#define COLOR_PRECISION" << endl;
+ fs << "#define EDGE_PRECISION" << endl;
+ fs << "#endif" << endl;
+ if (aConfig.mFeatures & ENABLE_RENDER_COLOR) {
+ fs << "uniform COLOR_PRECISION vec4 uRenderColor;" << endl;
+ } else {
+ // for tiling, texcoord can be greater than the lowfp range
+ fs << "varying vec2 vTexCoord;" << endl;
+ if (aConfig.mFeatures & ENABLE_BLUR) {
+ fs << "uniform bool uBlurAlpha;" << endl;
+ fs << "uniform vec2 uBlurRadius;" << endl;
+ fs << "uniform vec2 uBlurOffset;" << endl;
+ fs << "uniform float uBlurGaussianKernel[" << GAUSSIAN_KERNEL_HALF_WIDTH
+ << "];" << endl;
+ }
+ if (aConfig.mFeatures & ENABLE_COLOR_MATRIX) {
+ fs << "uniform mat4 uColorMatrix;" << endl;
+ fs << "uniform vec4 uColorMatrixVector;" << endl;
+ }
+ if (aConfig.mFeatures & ENABLE_OPACITY) {
+ fs << "uniform COLOR_PRECISION float uLayerOpacity;" << endl;
+ }
+ }
+ if (BlendOpIsMixBlendMode(blendOp)) {
+ fs << "varying vec2 vBackdropCoord;" << endl;
+ }
+
+ const char* sampler2D = "sampler2D";
+ const char* texture2D = "texture2D";
+
+ if (aConfig.mFeatures & ENABLE_TEXTURE_RECT) {
+ fs << "uniform vec2 uTexCoordMultiplier;" << endl;
+ if (aConfig.mFeatures & ENABLE_TEXTURE_YCBCR ||
+ aConfig.mFeatures & ENABLE_TEXTURE_NV12) {
+ fs << "uniform vec2 uCbCrTexCoordMultiplier;" << endl;
+ }
+ sampler2D = "sampler2DRect";
+ texture2D = "texture2DRect";
+ }
+
+ const char* maskSampler2D = "sampler2D";
+ const char* maskTexture2D = "texture2D";
+
+ if (aConfig.mFeatures & ENABLE_MASK &&
+ aConfig.mFeatures & ENABLE_MASK_TEXTURE_RECT) {
+ fs << "uniform vec2 uMaskCoordMultiplier;" << endl;
+ maskSampler2D = "sampler2DRect";
+ maskTexture2D = "texture2DRect";
+ }
+
+ if (aConfig.mFeatures & ENABLE_TEXTURE_EXTERNAL) {
+ sampler2D = "samplerExternalOES";
+ }
+
+ if (aConfig.mFeatures & ENABLE_TEXTURE_YCBCR) {
+ fs << "uniform " << sampler2D << " uYTexture;" << endl;
+ fs << "uniform " << sampler2D << " uCbTexture;" << endl;
+ fs << "uniform " << sampler2D << " uCrTexture;" << endl;
+ fs << "uniform mat3 uYuvColorMatrix;" << endl;
+ fs << "uniform vec3 uYuvOffsetVector;" << endl;
+ } else if (aConfig.mFeatures & ENABLE_TEXTURE_NV12) {
+ fs << "uniform " << sampler2D << " uYTexture;" << endl;
+ fs << "uniform " << sampler2D << " uCbTexture;" << endl;
+ fs << "uniform mat3 uYuvColorMatrix;" << endl;
+ fs << "uniform vec3 uYuvOffsetVector;" << endl;
+ } else if (aConfig.mFeatures & ENABLE_TEXTURE_COMPONENT_ALPHA) {
+ fs << "uniform " << sampler2D << " uBlackTexture;" << endl;
+ fs << "uniform " << sampler2D << " uWhiteTexture;" << endl;
+ fs << "uniform bool uTexturePass2;" << endl;
+ } else {
+ fs << "uniform " << sampler2D << " uTexture;" << endl;
+ }
+
+ if (BlendOpIsMixBlendMode(blendOp)) {
+ // Component alpha should be flattened away inside blend containers.
+ MOZ_ASSERT(!(aConfig.mFeatures & ENABLE_TEXTURE_COMPONENT_ALPHA));
+
+ fs << "uniform sampler2D uBackdropTexture;" << endl;
+ }
+
+ if (aConfig.mFeatures & ENABLE_MASK) {
+ fs << "varying vec3 vMaskCoord;" << endl;
+ fs << "uniform " << maskSampler2D << " uMaskTexture;" << endl;
+ }
+
+ if (aConfig.mFeatures & ENABLE_DEAA) {
+ fs << "uniform EDGE_PRECISION vec3 uSSEdges[4];" << endl;
+ }
+
+ if (BlendOpIsMixBlendMode(blendOp)) {
+ BuildMixBlender(aConfig, fs);
+ }
+
+ if (!(aConfig.mFeatures & ENABLE_RENDER_COLOR)) {
+ fs << "vec4 sample(vec2 coord) {" << endl;
+ fs << " vec4 color;" << endl;
+ if (aConfig.mFeatures & ENABLE_TEXTURE_YCBCR ||
+ aConfig.mFeatures & ENABLE_TEXTURE_NV12) {
+ if (aConfig.mFeatures & ENABLE_TEXTURE_YCBCR) {
+ if (aConfig.mFeatures & ENABLE_TEXTURE_RECT) {
+ fs << " COLOR_PRECISION float y = " << texture2D
+ << "(uYTexture, coord * uTexCoordMultiplier).r;" << endl;
+ fs << " COLOR_PRECISION float cb = " << texture2D
+ << "(uCbTexture, coord * uCbCrTexCoordMultiplier).r;" << endl;
+ fs << " COLOR_PRECISION float cr = " << texture2D
+ << "(uCrTexture, coord * uCbCrTexCoordMultiplier).r;" << endl;
+ } else {
+ fs << " COLOR_PRECISION float y = " << texture2D
+ << "(uYTexture, coord).r;" << endl;
+ fs << " COLOR_PRECISION float cb = " << texture2D
+ << "(uCbTexture, coord).r;" << endl;
+ fs << " COLOR_PRECISION float cr = " << texture2D
+ << "(uCrTexture, coord).r;" << endl;
+ }
+ } else {
+ if (aConfig.mFeatures & ENABLE_TEXTURE_RECT) {
+ fs << " COLOR_PRECISION float y = " << texture2D
+ << "(uYTexture, coord * uTexCoordMultiplier).r;" << endl;
+ fs << " COLOR_PRECISION float cb = " << texture2D
+ << "(uCbTexture, coord * uCbCrTexCoordMultiplier).r;" << endl;
+ if (aConfig.mFeatures & ENABLE_TEXTURE_NV12_GA_SWITCH) {
+ fs << " COLOR_PRECISION float cr = " << texture2D
+ << "(uCbTexture, coord * uCbCrTexCoordMultiplier).g;" << endl;
+ } else {
+ fs << " COLOR_PRECISION float cr = " << texture2D
+ << "(uCbTexture, coord * uCbCrTexCoordMultiplier).a;" << endl;
+ }
+ } else {
+ fs << " COLOR_PRECISION float y = " << texture2D
+ << "(uYTexture, coord).r;" << endl;
+ fs << " COLOR_PRECISION float cb = " << texture2D
+ << "(uCbTexture, coord).r;" << endl;
+ if (aConfig.mFeatures & ENABLE_TEXTURE_NV12_GA_SWITCH) {
+ fs << " COLOR_PRECISION float cr = " << texture2D
+ << "(uCbTexture, coord).g;" << endl;
+ } else {
+ fs << " COLOR_PRECISION float cr = " << texture2D
+ << "(uCbTexture, coord).a;" << endl;
+ }
+ }
+ }
+ fs << " vec3 yuv = vec3(y, cb, cr);" << endl;
+ if (aConfig.mMultiplier != 1) {
+ fs << " yuv *= " << aConfig.mMultiplier << ".0;" << endl;
+ }
+ fs << " yuv -= uYuvOffsetVector;" << endl;
+ fs << " color.rgb = uYuvColorMatrix * yuv;" << endl;
+ fs << " color.a = 1.0;" << endl;
+ } else if (aConfig.mFeatures & ENABLE_TEXTURE_COMPONENT_ALPHA) {
+ if (aConfig.mFeatures & ENABLE_TEXTURE_RECT) {
+ fs << " COLOR_PRECISION vec3 onBlack = " << texture2D
+ << "(uBlackTexture, coord * uTexCoordMultiplier).rgb;" << endl;
+ fs << " COLOR_PRECISION vec3 onWhite = " << texture2D
+ << "(uWhiteTexture, coord * uTexCoordMultiplier).rgb;" << endl;
+ } else {
+ fs << " COLOR_PRECISION vec3 onBlack = " << texture2D
+ << "(uBlackTexture, coord).rgb;" << endl;
+ fs << " COLOR_PRECISION vec3 onWhite = " << texture2D
+ << "(uWhiteTexture, coord).rgb;" << endl;
+ }
+ fs << " COLOR_PRECISION vec4 alphas = (1.0 - onWhite + onBlack).rgbg;"
+ << endl;
+ fs << " if (uTexturePass2)" << endl;
+ fs << " color = vec4(onBlack, alphas.a);" << endl;
+ fs << " else" << endl;
+ fs << " color = alphas;" << endl;
+ } else {
+ if (aConfig.mFeatures & ENABLE_TEXTURE_RECT) {
+ fs << " color = " << texture2D
+ << "(uTexture, coord * uTexCoordMultiplier);" << endl;
+ } else {
+ fs << " color = " << texture2D << "(uTexture, coord);" << endl;
+ }
+ }
+ if (aConfig.mFeatures & ENABLE_TEXTURE_RB_SWAP) {
+ fs << " color = color.bgra;" << endl;
+ }
+ if (aConfig.mFeatures & ENABLE_TEXTURE_NO_ALPHA) {
+ fs << " color = vec4(color.rgb, 1.0);" << endl;
+ }
+ fs << " return color;" << endl;
+ fs << "}" << endl;
+ if (aConfig.mFeatures & ENABLE_BLUR) {
+ fs << "vec4 sampleAtRadius(vec2 coord, float radius) {" << endl;
+ fs << " coord += uBlurOffset;" << endl;
+ fs << " coord += radius * uBlurRadius;" << endl;
+ fs << " if (coord.x < 0. || coord.y < 0. || coord.x > 1. || coord.y > "
+ "1.)"
+ << endl;
+ fs << " return vec4(0, 0, 0, 0);" << endl;
+ fs << " return sample(coord);" << endl;
+ fs << "}" << endl;
+ fs << "vec4 blur(vec4 color, vec2 coord) {" << endl;
+ fs << " vec4 total = color * uBlurGaussianKernel[0];" << endl;
+ fs << " for (int i = 1; i < " << GAUSSIAN_KERNEL_HALF_WIDTH << "; ++i) {"
+ << endl;
+ fs << " float r = float(i) * " << GAUSSIAN_KERNEL_STEP << ";" << endl;
+ fs << " float k = uBlurGaussianKernel[i];" << endl;
+ fs << " total += sampleAtRadius(coord, r) * k;" << endl;
+ fs << " total += sampleAtRadius(coord, -r) * k;" << endl;
+ fs << " }" << endl;
+ fs << " if (uBlurAlpha) {" << endl;
+ fs << " color *= total.a;" << endl;
+ fs << " } else {" << endl;
+ fs << " color = total;" << endl;
+ fs << " }" << endl;
+ fs << " return color;" << endl;
+ fs << "}" << endl;
+ }
+ }
+ fs << "void main() {" << endl;
+ if (aConfig.mFeatures & ENABLE_RENDER_COLOR) {
+ fs << " vec4 color = uRenderColor;" << endl;
+ } else {
+ fs << " vec4 color = sample(vTexCoord);" << endl;
+ if (aConfig.mFeatures & ENABLE_BLUR) {
+ fs << " color = blur(color, vTexCoord);" << endl;
+ }
+ if (aConfig.mFeatures & ENABLE_COLOR_MATRIX) {
+ fs << " color = uColorMatrix * vec4(color.rgb / color.a, color.a) + "
+ "uColorMatrixVector;"
+ << endl;
+ fs << " color.rgb *= color.a;" << endl;
+ }
+ if (aConfig.mFeatures & ENABLE_OPACITY) {
+ fs << " color *= uLayerOpacity;" << endl;
+ }
+ }
+ if (aConfig.mFeatures & ENABLE_DEAA) {
+ // Calculate the sub-pixel coverage of the pixel and modulate its opacity
+ // by that amount to perform DEAA.
+ fs << " vec3 ssPos = vec3(gl_FragCoord.xy, 1.0);" << endl;
+ fs << " float deaaCoverage = clamp(dot(uSSEdges[0], ssPos), 0.0, 1.0);"
+ << endl;
+ fs << " deaaCoverage *= clamp(dot(uSSEdges[1], ssPos), 0.0, 1.0);" << endl;
+ fs << " deaaCoverage *= clamp(dot(uSSEdges[2], ssPos), 0.0, 1.0);" << endl;
+ fs << " deaaCoverage *= clamp(dot(uSSEdges[3], ssPos), 0.0, 1.0);" << endl;
+ fs << " color *= deaaCoverage;" << endl;
+ }
+ if (BlendOpIsMixBlendMode(blendOp)) {
+ fs << " vec4 backdrop = texture2D(uBackdropTexture, vBackdropCoord);"
+ << endl;
+ fs << " color = mixAndBlend(backdrop, color);" << endl;
+ }
+ if (aConfig.mFeatures & ENABLE_MASK) {
+ fs << " vec2 maskCoords = vMaskCoord.xy / vMaskCoord.z;" << endl;
+ if (aConfig.mFeatures & ENABLE_MASK_TEXTURE_RECT) {
+ fs << " COLOR_PRECISION float mask = " << maskTexture2D
+ << "(uMaskTexture, maskCoords * uMaskCoordMultiplier).r;" << endl;
+ } else {
+ fs << " COLOR_PRECISION float mask = " << maskTexture2D
+ << "(uMaskTexture, maskCoords).r;" << endl;
+ }
+ fs << " color *= mask;" << endl;
+ } else {
+ fs << " COLOR_PRECISION float mask = 1.0;" << endl;
+ fs << " color *= mask;" << endl;
+ }
+ fs << " gl_FragColor = color;" << endl;
+ fs << "}" << endl;
+
+ result.mVertexShaderString = vs.str();
+ result.mFragmentShaderString = fs.str();
+
+ if (aConfig.mFeatures & ENABLE_RENDER_COLOR) {
+ result.mTextureCount = 0;
+ } else {
+ if (aConfig.mFeatures & ENABLE_TEXTURE_YCBCR) {
+ result.mTextureCount = 3;
+ } else if (aConfig.mFeatures & ENABLE_TEXTURE_NV12) {
+ result.mTextureCount = 2;
+ } else if (aConfig.mFeatures & ENABLE_TEXTURE_COMPONENT_ALPHA) {
+ result.mTextureCount = 2;
+ } else {
+ result.mTextureCount = 1;
+ }
+ }
+ if (aConfig.mFeatures & ENABLE_MASK) {
+ result.mTextureCount = 1;
+ }
+ if (BlendOpIsMixBlendMode(blendOp)) {
+ result.mTextureCount += 1;
+ }
+
+ return result;
+}
+
+void ProgramProfileOGL::BuildMixBlender(const ShaderConfigOGL& aConfig,
+ std::ostringstream& fs) {
+ // From the "Compositing and Blending Level 1" spec.
+ // Generate helper functions first.
+ switch (aConfig.mCompositionOp) {
+ case gfx::CompositionOp::OP_OVERLAY:
+ case gfx::CompositionOp::OP_HARD_LIGHT:
+ // Note: we substitute (2*src-1) into the screen formula below.
+ fs << "float hardlight(float dest, float src) {" << endl;
+ fs << " if (src <= 0.5) {" << endl;
+ fs << " return dest * (2.0 * src);" << endl;
+ fs << " } else {" << endl;
+ fs << " return 2.0*dest + 2.0*src - 1.0 - 2.0*dest*src;" << endl;
+ fs << " }" << endl;
+ fs << "}" << endl;
+ break;
+ case gfx::CompositionOp::OP_COLOR_DODGE:
+ fs << "float dodge(float dest, float src) {" << endl;
+ fs << " if (dest == 0.0) {" << endl;
+ fs << " return 0.0;" << endl;
+ fs << " } else if (src == 1.0) {" << endl;
+ fs << " return 1.0;" << endl;
+ fs << " } else {" << endl;
+ fs << " return min(1.0, dest / (1.0 - src));" << endl;
+ fs << " }" << endl;
+ fs << "}" << endl;
+ break;
+ case gfx::CompositionOp::OP_COLOR_BURN:
+ fs << "float burn(float dest, float src) {" << endl;
+ fs << " if (dest == 1.0) {" << endl;
+ fs << " return 1.0;" << endl;
+ fs << " } else if (src == 0.0) {" << endl;
+ fs << " return 0.0;" << endl;
+ fs << " } else {" << endl;
+ fs << " return 1.0 - min(1.0, (1.0 - dest) / src);" << endl;
+ fs << " }" << endl;
+ fs << "}" << endl;
+ break;
+ case gfx::CompositionOp::OP_SOFT_LIGHT:
+ fs << "float darken(float dest) {" << endl;
+ fs << " if (dest <= 0.25) {" << endl;
+ fs << " return ((16.0 * dest - 12.0) * dest + 4.0) * dest;" << endl;
+ fs << " } else {" << endl;
+ fs << " return sqrt(dest);" << endl;
+ fs << " }" << endl;
+ fs << "}" << endl;
+ fs << "float softlight(float dest, float src) {" << endl;
+ fs << " if (src <= 0.5) {" << endl;
+ fs << " return dest - (1.0 - 2.0 * src) * dest * (1.0 - dest);"
+ << endl;
+ fs << " } else {" << endl;
+ fs << " return dest + (2.0 * src - 1.0) * (darken(dest) - dest);"
+ << endl;
+ fs << " }" << endl;
+ fs << "}" << endl;
+ break;
+ case gfx::CompositionOp::OP_HUE:
+ case gfx::CompositionOp::OP_SATURATION:
+ case gfx::CompositionOp::OP_COLOR:
+ case gfx::CompositionOp::OP_LUMINOSITY:
+ fs << "float Lum(vec3 c) {" << endl;
+ fs << " return dot(vec3(0.3, 0.59, 0.11), c);" << endl;
+ fs << "}" << endl;
+ fs << "vec3 ClipColor(vec3 c) {" << endl;
+ fs << " float L = Lum(c);" << endl;
+ fs << " float n = min(min(c.r, c.g), c.b);" << endl;
+ fs << " float x = max(max(c.r, c.g), c.b);" << endl;
+ fs << " if (n < 0.0) {" << endl;
+ fs << " c = L + (((c - L) * L) / (L - n));" << endl;
+ fs << " }" << endl;
+ fs << " if (x > 1.0) {" << endl;
+ fs << " c = L + (((c - L) * (1.0 - L)) / (x - L));" << endl;
+ fs << " }" << endl;
+ fs << " return c;" << endl;
+ fs << "}" << endl;
+ fs << "vec3 SetLum(vec3 c, float L) {" << endl;
+ fs << " float d = L - Lum(c);" << endl;
+ fs << " return ClipColor(vec3(" << endl;
+ fs << " c.r + d," << endl;
+ fs << " c.g + d," << endl;
+ fs << " c.b + d));" << endl;
+ fs << "}" << endl;
+ fs << "float Sat(vec3 c) {" << endl;
+ fs << " return max(max(c.r, c.g), c.b) - min(min(c.r, c.g), c.b);"
+ << endl;
+ fs << "}" << endl;
+
+ // To use this helper, re-arrange rgb such that r=min, g=mid, and b=max.
+ fs << "vec3 SetSatInner(vec3 c, float s) {" << endl;
+ fs << " if (c.b > c.r) {" << endl;
+ fs << " c.g = (((c.g - c.r) * s) / (c.b - c.r));" << endl;
+ fs << " c.b = s;" << endl;
+ fs << " } else {" << endl;
+ fs << " c.gb = vec2(0.0, 0.0);" << endl;
+ fs << " }" << endl;
+ fs << " return vec3(0.0, c.gb);" << endl;
+ fs << "}" << endl;
+
+ fs << "vec3 SetSat(vec3 c, float s) {" << endl;
+ fs << " if (c.r <= c.g) {" << endl;
+ fs << " if (c.g <= c.b) {" << endl;
+ fs << " c.rgb = SetSatInner(c.rgb, s);" << endl;
+ fs << " } else if (c.r <= c.b) {" << endl;
+ fs << " c.rbg = SetSatInner(c.rbg, s);" << endl;
+ fs << " } else {" << endl;
+ fs << " c.brg = SetSatInner(c.brg, s);" << endl;
+ fs << " }" << endl;
+ fs << " } else if (c.r <= c.b) {" << endl;
+ fs << " c.grb = SetSatInner(c.grb, s);" << endl;
+ fs << " } else if (c.g <= c.b) {" << endl;
+ fs << " c.gbr = SetSatInner(c.gbr, s);" << endl;
+ fs << " } else {" << endl;
+ fs << " c.bgr = SetSatInner(c.bgr, s);" << endl;
+ fs << " }" << endl;
+ fs << " return c;" << endl;
+ fs << "}" << endl;
+ break;
+ default:
+ break;
+ }
+
+ // Generate the main blending helper.
+ fs << "vec3 blend(vec3 dest, vec3 src) {" << endl;
+ switch (aConfig.mCompositionOp) {
+ case gfx::CompositionOp::OP_MULTIPLY:
+ fs << " return dest * src;" << endl;
+ break;
+ case gfx::CompositionOp::OP_SCREEN:
+ fs << " return dest + src - (dest * src);" << endl;
+ break;
+ case gfx::CompositionOp::OP_OVERLAY:
+ fs << " return vec3(" << endl;
+ fs << " hardlight(src.r, dest.r)," << endl;
+ fs << " hardlight(src.g, dest.g)," << endl;
+ fs << " hardlight(src.b, dest.b));" << endl;
+ break;
+ case gfx::CompositionOp::OP_DARKEN:
+ fs << " return min(dest, src);" << endl;
+ break;
+ case gfx::CompositionOp::OP_LIGHTEN:
+ fs << " return max(dest, src);" << endl;
+ break;
+ case gfx::CompositionOp::OP_COLOR_DODGE:
+ fs << " return vec3(" << endl;
+ fs << " dodge(dest.r, src.r)," << endl;
+ fs << " dodge(dest.g, src.g)," << endl;
+ fs << " dodge(dest.b, src.b));" << endl;
+ break;
+ case gfx::CompositionOp::OP_COLOR_BURN:
+ fs << " return vec3(" << endl;
+ fs << " burn(dest.r, src.r)," << endl;
+ fs << " burn(dest.g, src.g)," << endl;
+ fs << " burn(dest.b, src.b));" << endl;
+ break;
+ case gfx::CompositionOp::OP_HARD_LIGHT:
+ fs << " return vec3(" << endl;
+ fs << " hardlight(dest.r, src.r)," << endl;
+ fs << " hardlight(dest.g, src.g)," << endl;
+ fs << " hardlight(dest.b, src.b));" << endl;
+ break;
+ case gfx::CompositionOp::OP_SOFT_LIGHT:
+ fs << " return vec3(" << endl;
+ fs << " softlight(dest.r, src.r)," << endl;
+ fs << " softlight(dest.g, src.g)," << endl;
+ fs << " softlight(dest.b, src.b));" << endl;
+ break;
+ case gfx::CompositionOp::OP_DIFFERENCE:
+ fs << " return abs(dest - src);" << endl;
+ break;
+ case gfx::CompositionOp::OP_EXCLUSION:
+ fs << " return dest + src - 2.0*dest*src;" << endl;
+ break;
+ case gfx::CompositionOp::OP_HUE:
+ fs << " return SetLum(SetSat(src, Sat(dest)), Lum(dest));" << endl;
+ break;
+ case gfx::CompositionOp::OP_SATURATION:
+ fs << " return SetLum(SetSat(dest, Sat(src)), Lum(dest));" << endl;
+ break;
+ case gfx::CompositionOp::OP_COLOR:
+ fs << " return SetLum(src, Lum(dest));" << endl;
+ break;
+ case gfx::CompositionOp::OP_LUMINOSITY:
+ fs << " return SetLum(dest, Lum(src));" << endl;
+ break;
+ default:
+ MOZ_ASSERT_UNREACHABLE("unknown blend mode");
+ }
+ fs << "}" << endl;
+
+ // Generate the mix-blend function the fragment shader will call.
+ fs << "vec4 mixAndBlend(vec4 backdrop, vec4 color) {" << endl;
+
+ // Shortcut when the backdrop or source alpha is 0, otherwise we may leak
+ // Infinity into the blend function and return incorrect results.
+ fs << " if (backdrop.a == 0.0) {" << endl;
+ fs << " return color;" << endl;
+ fs << " }" << endl;
+ fs << " if (color.a == 0.0) {" << endl;
+ fs << " return vec4(0.0, 0.0, 0.0, 0.0);" << endl;
+ fs << " }" << endl;
+
+ // The spec assumes there is no premultiplied alpha. The backdrop is always
+ // premultiplied, so undo the premultiply. If the source is premultiplied we
+ // must fix that as well.
+ fs << " backdrop.rgb /= backdrop.a;" << endl;
+ if (!(aConfig.mFeatures & ENABLE_NO_PREMUL_ALPHA)) {
+ fs << " color.rgb /= color.a;" << endl;
+ }
+ fs << " vec3 blended = blend(backdrop.rgb, color.rgb);" << endl;
+ fs << " color.rgb = (1.0 - backdrop.a) * color.rgb + backdrop.a * "
+ "blended.rgb;"
+ << endl;
+ fs << " color.rgb *= color.a;" << endl;
+ fs << " return color;" << endl;
+ fs << "}" << endl;
+}
+
+ShaderProgramOGL::ShaderProgramOGL(GLContext* aGL,
+ const ProgramProfileOGL& aProfile)
+ : mGL(aGL), mProgram(0), mProfile(aProfile), mProgramState(STATE_NEW) {}
+
+ShaderProgramOGL::~ShaderProgramOGL() {
+ if (mProgram <= 0) {
+ return;
+ }
+
+ RefPtr<GLContext> ctx = mGL->GetSharedContext();
+ if (!ctx) {
+ ctx = mGL;
+ }
+ ctx->MakeCurrent();
+ ctx->fDeleteProgram(mProgram);
+}
+
+bool ShaderProgramOGL::Initialize() {
+ NS_ASSERTION(mProgramState == STATE_NEW,
+ "Shader program has already been initialised");
+
+ std::ostringstream vs, fs;
+ for (uint32_t i = 0; i < mProfile.mDefines.Length(); ++i) {
+ vs << mProfile.mDefines[i] << endl;
+ fs << mProfile.mDefines[i] << endl;
+ }
+ vs << mProfile.mVertexShaderString << endl;
+ fs << mProfile.mFragmentShaderString << endl;
+
+ if (!CreateProgram(vs.str().c_str(), fs.str().c_str())) {
+ mProgramState = STATE_ERROR;
+ return false;
+ }
+
+ mProgramState = STATE_OK;
+
+ for (uint32_t i = 0; i < KnownUniform::KnownUniformCount; ++i) {
+ mProfile.mUniforms[i].mLocation =
+ mGL->fGetUniformLocation(mProgram, mProfile.mUniforms[i].mNameString);
+ }
+
+ return true;
+}
+
+GLint ShaderProgramOGL::CreateShader(GLenum aShaderType,
+ const char* aShaderSource) {
+ GLint success, len = 0;
+
+ GLint sh = mGL->fCreateShader(aShaderType);
+ mGL->fShaderSource(sh, 1, (const GLchar**)&aShaderSource, nullptr);
+ mGL->fCompileShader(sh);
+ mGL->fGetShaderiv(sh, LOCAL_GL_COMPILE_STATUS, &success);
+ mGL->fGetShaderiv(sh, LOCAL_GL_INFO_LOG_LENGTH, (GLint*)&len);
+ /* Even if compiling is successful, there may still be warnings. Print them
+ * in a debug build. The > 10 is to catch silly compilers that might put
+ * some whitespace in the log but otherwise leave it empty.
+ */
+ if (!success
+#ifdef DEBUG
+ || (len > 10 && gfxEnv::MOZ_DEBUG_SHADERS())
+#endif
+ ) {
+ nsAutoCString log;
+ log.SetLength(len);
+ mGL->fGetShaderInfoLog(sh, len, (GLint*)&len, (char*)log.BeginWriting());
+ log.Truncate(len);
+
+ if (!success) {
+ printf_stderr("=== SHADER COMPILATION FAILED ===\n");
+ } else {
+ printf_stderr("=== SHADER COMPILATION WARNINGS ===\n");
+ }
+
+ printf_stderr("=== Source:\n%s\n", aShaderSource);
+ printf_stderr("=== Log:\n%s\n", log.get());
+ printf_stderr("============\n");
+
+ if (!success) {
+ mGL->fDeleteShader(sh);
+ return 0;
+ }
+ }
+
+ return sh;
+}
+
+bool ShaderProgramOGL::CreateProgram(const char* aVertexShaderString,
+ const char* aFragmentShaderString) {
+ GLuint vertexShader =
+ CreateShader(LOCAL_GL_VERTEX_SHADER, aVertexShaderString);
+ GLuint fragmentShader =
+ CreateShader(LOCAL_GL_FRAGMENT_SHADER, aFragmentShaderString);
+
+ if (!vertexShader || !fragmentShader) return false;
+
+ GLint result = mGL->fCreateProgram();
+ mGL->fAttachShader(result, vertexShader);
+ mGL->fAttachShader(result, fragmentShader);
+
+ for (std::pair<nsCString, GLuint>& attribute : mProfile.mAttributes) {
+ mGL->fBindAttribLocation(result, attribute.second, attribute.first.get());
+ }
+
+ mGL->fLinkProgram(result);
+
+ GLint success, len;
+ mGL->fGetProgramiv(result, LOCAL_GL_LINK_STATUS, &success);
+ mGL->fGetProgramiv(result, LOCAL_GL_INFO_LOG_LENGTH, (GLint*)&len);
+ /* Even if linking is successful, there may still be warnings. Print them
+ * in a debug build. The > 10 is to catch silly compilers that might put
+ * some whitespace in the log but otherwise leave it empty.
+ */
+ if (!success
+#ifdef DEBUG
+ || (len > 10 && gfxEnv::MOZ_DEBUG_SHADERS())
+#endif
+ ) {
+ nsAutoCString log;
+ log.SetLength(len);
+ mGL->fGetProgramInfoLog(result, len, (GLint*)&len,
+ (char*)log.BeginWriting());
+
+ if (!success) {
+ printf_stderr("=== PROGRAM LINKING FAILED ===\n");
+ } else {
+ printf_stderr("=== PROGRAM LINKING WARNINGS ===\n");
+ }
+ printf_stderr("=== Log:\n%s\n", log.get());
+ printf_stderr("============\n");
+ }
+
+ // We can mark the shaders for deletion; they're attached to the program
+ // and will remain attached.
+ mGL->fDeleteShader(vertexShader);
+ mGL->fDeleteShader(fragmentShader);
+
+ if (!success) {
+ mGL->fDeleteProgram(result);
+ return false;
+ }
+
+ mProgram = result;
+ return true;
+}
+
+GLuint ShaderProgramOGL::GetProgram() {
+ if (mProgramState == STATE_NEW) {
+ if (!Initialize()) {
+ NS_WARNING("Shader could not be initialised");
+ }
+ }
+ MOZ_ASSERT(HasInitialized(),
+ "Attempting to get a program that's not been initialized!");
+ return mProgram;
+}
+
+void ShaderProgramOGL::SetYUVColorSpace(gfx::YUVColorSpace aYUVColorSpace) {
+ const float* yuvToRgb =
+ gfxUtils::YuvToRgbMatrix3x3ColumnMajor(aYUVColorSpace);
+ SetMatrix3fvUniform(KnownUniform::YuvColorMatrix, yuvToRgb);
+ if (aYUVColorSpace == gfx::YUVColorSpace::Identity) {
+ const float identity[] = {0.0, 0.0, 0.0};
+ SetVec3fvUniform(KnownUniform::YuvOffsetVector, identity);
+ } else {
+ const float offset[] = {0.06275, 0.50196, 0.50196};
+ SetVec3fvUniform(KnownUniform::YuvOffsetVector, offset);
+ }
+}
+
+ShaderProgramOGLsHolder::ShaderProgramOGLsHolder(gl::GLContext* aGL)
+ : mGL(aGL) {}
+
+ShaderProgramOGLsHolder::~ShaderProgramOGLsHolder() { Clear(); }
+
+ShaderProgramOGL* ShaderProgramOGLsHolder::GetShaderProgramFor(
+ const ShaderConfigOGL& aConfig) {
+ auto iter = mPrograms.find(aConfig);
+ if (iter != mPrograms.end()) {
+ return iter->second.get();
+ }
+
+ ProgramProfileOGL profile = ProgramProfileOGL::GetProfileFor(aConfig);
+ auto shader = MakeUnique<ShaderProgramOGL>(mGL, profile);
+ if (!shader->Initialize()) {
+ gfxCriticalError() << "Shader compilation failure, cfg:"
+ << " features: " << gfx::hexa(aConfig.mFeatures)
+ << " multiplier: " << aConfig.mMultiplier
+ << " op: " << aConfig.mCompositionOp;
+ return nullptr;
+ }
+
+ mPrograms.emplace(aConfig, std::move(shader));
+ return mPrograms[aConfig].get();
+}
+
+void ShaderProgramOGLsHolder::Clear() { mPrograms.clear(); }
+
+ShaderProgramOGL* ShaderProgramOGLsHolder::ActivateProgram(
+ const ShaderConfigOGL& aConfig) {
+ ShaderProgramOGL* program = GetShaderProgramFor(aConfig);
+ MOZ_DIAGNOSTIC_ASSERT(program);
+ if (!program) {
+ return nullptr;
+ }
+ if (mCurrentProgram != program) {
+ mGL->fUseProgram(program->GetProgram());
+ mCurrentProgram = program;
+ }
+ return program;
+}
+
+void ShaderProgramOGLsHolder::ResetCurrentProgram() {
+ mCurrentProgram = nullptr;
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/opengl/OGLShaderProgram.h b/gfx/layers/opengl/OGLShaderProgram.h
new file mode 100644
index 0000000000..cfeb7dd23e
--- /dev/null
+++ b/gfx/layers/opengl/OGLShaderProgram.h
@@ -0,0 +1,377 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GFX_OGLSHADERPROGRAM_H
+#define GFX_OGLSHADERPROGRAM_H
+
+#include <map>
+#include <string>
+#include <utility>
+
+#include "GLContext.h" // for fast inlines of glUniform*
+#include "mozilla/UniquePtr.h"
+#include "OGLShaderConfig.h"
+
+namespace mozilla {
+namespace layers {
+
+#if defined(DEBUG)
+# define CHECK_CURRENT_PROGRAM 1
+# define ASSERT_THIS_PROGRAM \
+ do { \
+ GLuint currentProgram; \
+ mGL->GetUIntegerv(LOCAL_GL_CURRENT_PROGRAM, &currentProgram); \
+ MOZ_ASSERT(currentProgram == mProgram, \
+ "SetUniform with wrong program active!"); \
+ } while (0)
+#else
+# define ASSERT_THIS_PROGRAM \
+ do { \
+ } while (0)
+#endif
+
+/**
+ * This struct represents the shaders that make up a program and the uniform
+ * and attribute parmeters that those shaders take.
+ * It is used by ShaderProgramOGL.
+ * Use the factory method GetProfileFor to create instances.
+ */
+struct ProgramProfileOGL {
+ /**
+ * Factory method; creates an instance of this class for the given
+ * ShaderConfigOGL
+ */
+ static ProgramProfileOGL GetProfileFor(ShaderConfigOGL aConfig);
+
+ // the source code for the program's shaders
+ std::string mVertexShaderString;
+ std::string mFragmentShaderString;
+
+ // the vertex attributes
+ CopyableTArray<std::pair<nsCString, GLuint>> mAttributes;
+
+ KnownUniform mUniforms[KnownUniform::KnownUniformCount];
+ CopyableTArray<const char*> mDefines;
+ size_t mTextureCount;
+
+ ProgramProfileOGL() : mTextureCount(0) {}
+
+ private:
+ static void BuildMixBlender(const ShaderConfigOGL& aConfig,
+ std::ostringstream& fs);
+};
+
+/**
+ * Represents an OGL shader program. The details of a program are represented
+ * by a ProgramProfileOGL
+ */
+class ShaderProgramOGL {
+ public:
+ typedef mozilla::gl::GLContext GLContext;
+
+ ShaderProgramOGL(GLContext* aGL, const ProgramProfileOGL& aProfile);
+
+ ~ShaderProgramOGL();
+
+ bool HasInitialized() {
+ NS_ASSERTION(mProgramState != STATE_OK || mProgram > 0,
+ "Inconsistent program state");
+ return mProgramState == STATE_OK;
+ }
+
+ GLuint GetProgram();
+
+ bool Initialize();
+
+ GLint CreateShader(GLenum aShaderType, const char* aShaderSource);
+
+ /**
+ * Creates a program and stores its id.
+ */
+ bool CreateProgram(const char* aVertexShaderString,
+ const char* aFragmentShaderString);
+
+ /**
+ * The following set of methods set a uniform argument to the shader program.
+ * Not all uniforms may be set for all programs, and such uses will throw
+ * an assertion.
+ */
+ void SetLayerTransform(const gfx::Matrix4x4& aMatrix) {
+ SetMatrixUniform(KnownUniform::LayerTransform, aMatrix);
+ }
+
+ void SetLayerTransformInverse(const gfx::Matrix4x4& aMatrix) {
+ SetMatrixUniform(KnownUniform::LayerTransformInverse, aMatrix);
+ }
+
+ void SetDEAAEdges(const gfx::Point3D* aEdges) {
+ SetArrayUniform(KnownUniform::SSEdges, 4, aEdges);
+ }
+
+ void SetViewportSize(const gfx::IntSize& aSize) {
+ float vals[2] = {(float)aSize.width, (float)aSize.height};
+ SetUniform(KnownUniform::ViewportSize, 2, vals);
+ }
+
+ void SetVisibleCenter(const gfx::Point& aVisibleCenter) {
+ float vals[2] = {aVisibleCenter.x, aVisibleCenter.y};
+ SetUniform(KnownUniform::VisibleCenter, 2, vals);
+ }
+
+ void SetLayerRects(const gfx::Rect* aRects) {
+ float vals[16] = {
+ aRects[0].X(), aRects[0].Y(), aRects[0].Width(), aRects[0].Height(),
+ aRects[1].X(), aRects[1].Y(), aRects[1].Width(), aRects[1].Height(),
+ aRects[2].X(), aRects[2].Y(), aRects[2].Width(), aRects[2].Height(),
+ aRects[3].X(), aRects[3].Y(), aRects[3].Width(), aRects[3].Height()};
+ SetUniform(KnownUniform::LayerRects, 16, vals);
+ }
+
+ void SetProjectionMatrix(const gfx::Matrix4x4& aMatrix) {
+ SetMatrixUniform(KnownUniform::MatrixProj, aMatrix);
+ }
+
+ // sets this program's texture transform, if it uses one
+ void SetTextureTransform(const gfx::Matrix4x4& aMatrix) {
+ SetMatrixUniform(KnownUniform::TextureTransform, aMatrix);
+ }
+
+ void SetTextureRects(const gfx::Rect* aRects) {
+ float vals[16] = {
+ aRects[0].X(), aRects[0].Y(), aRects[0].Width(), aRects[0].Height(),
+ aRects[1].X(), aRects[1].Y(), aRects[1].Width(), aRects[1].Height(),
+ aRects[2].X(), aRects[2].Y(), aRects[2].Width(), aRects[2].Height(),
+ aRects[3].X(), aRects[3].Y(), aRects[3].Width(), aRects[3].Height()};
+ SetUniform(KnownUniform::TextureRects, 16, vals);
+ }
+
+ void SetRenderOffset(const nsIntPoint& aOffset) {
+ float vals[4] = {float(aOffset.x), float(aOffset.y)};
+ SetUniform(KnownUniform::RenderTargetOffset, 2, vals);
+ }
+
+ void SetRenderOffset(float aX, float aY) {
+ float vals[2] = {aX, aY};
+ SetUniform(KnownUniform::RenderTargetOffset, 2, vals);
+ }
+
+ void SetLayerOpacity(float aOpacity) {
+ SetUniform(KnownUniform::LayerOpacity, aOpacity);
+ }
+
+ void SetTextureUnit(GLint aUnit) { SetUniform(KnownUniform::Texture, aUnit); }
+ void SetYTextureUnit(GLint aUnit) {
+ SetUniform(KnownUniform::YTexture, aUnit);
+ }
+
+ void SetCbTextureUnit(GLint aUnit) {
+ SetUniform(KnownUniform::CbTexture, aUnit);
+ }
+
+ void SetCrTextureUnit(GLint aUnit) {
+ SetUniform(KnownUniform::CrTexture, aUnit);
+ }
+
+ void SetYCbCrTextureUnits(GLint aYUnit, GLint aCbUnit, GLint aCrUnit) {
+ SetUniform(KnownUniform::YTexture, aYUnit);
+ SetUniform(KnownUniform::CbTexture, aCbUnit);
+ SetUniform(KnownUniform::CrTexture, aCrUnit);
+ }
+
+ void SetNV12TextureUnits(GLint aYUnit, GLint aCbCrUnit) {
+ SetUniform(KnownUniform::YTexture, aYUnit);
+ SetUniform(KnownUniform::CbTexture, aCbCrUnit);
+ }
+
+ void SetTexCoordMultiplier(float aWidth, float aHeight) {
+ float f[] = {aWidth, aHeight};
+ SetUniform(KnownUniform::TexCoordMultiplier, 2, f);
+ }
+
+ void SetCbCrTexCoordMultiplier(float aWidth, float aHeight) {
+ float f[] = {aWidth, aHeight};
+ SetUniform(KnownUniform::CbCrTexCoordMultiplier, 2, f);
+ }
+
+ void SetYUVColorSpace(gfx::YUVColorSpace aYUVColorSpace);
+
+ size_t GetTextureCount() const { return mProfile.mTextureCount; }
+
+ protected:
+ RefPtr<GLContext> mGL;
+ // the OpenGL id of the program
+ GLuint mProgram;
+ ProgramProfileOGL mProfile;
+ enum { STATE_NEW, STATE_OK, STATE_ERROR } mProgramState;
+
+#ifdef CHECK_CURRENT_PROGRAM
+ static int sCurrentProgramKey;
+#endif
+
+ void SetUniform(KnownUniform::KnownUniformName aKnownUniform,
+ float aFloatValue) {
+ ASSERT_THIS_PROGRAM;
+ NS_ASSERTION(
+ aKnownUniform >= 0 && aKnownUniform < KnownUniform::KnownUniformCount,
+ "Invalid known uniform");
+
+ KnownUniform& ku(mProfile.mUniforms[aKnownUniform]);
+ if (ku.UpdateUniform(aFloatValue)) {
+ mGL->fUniform1f(ku.mLocation, aFloatValue);
+ }
+ }
+
+ void SetUniform(KnownUniform::KnownUniformName aKnownUniform,
+ const gfx::DeviceColor& aColor) {
+ ASSERT_THIS_PROGRAM;
+ NS_ASSERTION(
+ aKnownUniform >= 0 && aKnownUniform < KnownUniform::KnownUniformCount,
+ "Invalid known uniform");
+
+ KnownUniform& ku(mProfile.mUniforms[aKnownUniform]);
+ if (ku.UpdateUniform(aColor.r, aColor.g, aColor.b, aColor.a)) {
+ mGL->fUniform4fv(ku.mLocation, 1, ku.mValue.f16v);
+ }
+ }
+
+ void SetUniform(KnownUniform::KnownUniformName aKnownUniform, int aLength,
+ const float* aFloatValues) {
+ ASSERT_THIS_PROGRAM;
+ NS_ASSERTION(
+ aKnownUniform >= 0 && aKnownUniform < KnownUniform::KnownUniformCount,
+ "Invalid known uniform");
+
+ KnownUniform& ku(mProfile.mUniforms[aKnownUniform]);
+ if (ku.UpdateUniform(aLength, aFloatValues)) {
+ switch (aLength) {
+ case 1:
+ mGL->fUniform1fv(ku.mLocation, 1, ku.mValue.f16v);
+ break;
+ case 2:
+ mGL->fUniform2fv(ku.mLocation, 1, ku.mValue.f16v);
+ break;
+ case 3:
+ mGL->fUniform3fv(ku.mLocation, 1, ku.mValue.f16v);
+ break;
+ case 4:
+ mGL->fUniform4fv(ku.mLocation, 1, ku.mValue.f16v);
+ break;
+ case 16:
+ mGL->fUniform4fv(ku.mLocation, 4, ku.mValue.f16v);
+ break;
+ default:
+ MOZ_ASSERT_UNREACHABLE("Bogus aLength param");
+ }
+ }
+ }
+
+ void SetArrayUniform(KnownUniform::KnownUniformName aKnownUniform,
+ int aLength, float* aFloatValues) {
+ ASSERT_THIS_PROGRAM;
+ NS_ASSERTION(
+ aKnownUniform >= 0 && aKnownUniform < KnownUniform::KnownUniformCount,
+ "Invalid known uniform");
+
+ KnownUniform& ku(mProfile.mUniforms[aKnownUniform]);
+ if (ku.UpdateArrayUniform(aLength, aFloatValues)) {
+ mGL->fUniform1fv(ku.mLocation, aLength, ku.mValue.f16v);
+ }
+ }
+
+ void SetArrayUniform(KnownUniform::KnownUniformName aKnownUniform,
+ int aLength, const gfx::Point3D* aPointValues) {
+ ASSERT_THIS_PROGRAM;
+ NS_ASSERTION(
+ aKnownUniform >= 0 && aKnownUniform < KnownUniform::KnownUniformCount,
+ "Invalid known uniform");
+
+ KnownUniform& ku(mProfile.mUniforms[aKnownUniform]);
+ if (ku.UpdateArrayUniform(aLength, aPointValues)) {
+ mGL->fUniform3fv(ku.mLocation, aLength, ku.mValue.f16v);
+ }
+ }
+
+ void SetUniform(KnownUniform::KnownUniformName aKnownUniform,
+ GLint aIntValue) {
+ ASSERT_THIS_PROGRAM;
+ NS_ASSERTION(
+ aKnownUniform >= 0 && aKnownUniform < KnownUniform::KnownUniformCount,
+ "Invalid known uniform");
+
+ KnownUniform& ku(mProfile.mUniforms[aKnownUniform]);
+ if (ku.UpdateUniform(aIntValue)) {
+ mGL->fUniform1i(ku.mLocation, aIntValue);
+ }
+ }
+
+ void SetMatrixUniform(KnownUniform::KnownUniformName aKnownUniform,
+ const float* aFloatValues) {
+ ASSERT_THIS_PROGRAM;
+ NS_ASSERTION(
+ aKnownUniform >= 0 && aKnownUniform < KnownUniform::KnownUniformCount,
+ "Invalid known uniform");
+
+ KnownUniform& ku(mProfile.mUniforms[aKnownUniform]);
+ if (ku.UpdateUniform(16, aFloatValues)) {
+ mGL->fUniformMatrix4fv(ku.mLocation, 1, false, ku.mValue.f16v);
+ }
+ }
+
+ void SetMatrix3fvUniform(KnownUniform::KnownUniformName aKnownUniform,
+ const float* aFloatValues) {
+ ASSERT_THIS_PROGRAM;
+ NS_ASSERTION(
+ aKnownUniform >= 0 && aKnownUniform < KnownUniform::KnownUniformCount,
+ "Invalid known uniform");
+
+ KnownUniform& ku(mProfile.mUniforms[aKnownUniform]);
+ if (ku.UpdateUniform(9, aFloatValues)) {
+ mGL->fUniformMatrix3fv(ku.mLocation, 1, false, ku.mValue.f16v);
+ }
+ }
+
+ void SetVec3fvUniform(KnownUniform::KnownUniformName aKnownUniform,
+ const float* aFloatValues) {
+ ASSERT_THIS_PROGRAM;
+ NS_ASSERTION(
+ aKnownUniform >= 0 && aKnownUniform < KnownUniform::KnownUniformCount,
+ "Invalid known uniform");
+
+ KnownUniform& ku(mProfile.mUniforms[aKnownUniform]);
+ if (ku.UpdateUniform(3, aFloatValues)) {
+ mGL->fUniform3fv(ku.mLocation, 1, ku.mValue.f16v);
+ }
+ }
+
+ void SetMatrixUniform(KnownUniform::KnownUniformName aKnownUniform,
+ const gfx::Matrix4x4& aMatrix) {
+ SetMatrixUniform(aKnownUniform, &aMatrix._11);
+ }
+};
+
+class ShaderProgramOGLsHolder final {
+ public:
+ NS_INLINE_DECL_REFCOUNTING(ShaderProgramOGLsHolder)
+
+ explicit ShaderProgramOGLsHolder(gl::GLContext* aGL);
+
+ ShaderProgramOGL* GetShaderProgramFor(const ShaderConfigOGL& aConfig);
+ void Clear();
+ ShaderProgramOGL* ActivateProgram(const ShaderConfigOGL& aConfig);
+ void ResetCurrentProgram();
+
+ protected:
+ ~ShaderProgramOGLsHolder();
+
+ const RefPtr<gl::GLContext> mGL;
+ std::map<ShaderConfigOGL, UniquePtr<ShaderProgramOGL>> mPrograms;
+ ShaderProgramOGL* mCurrentProgram = nullptr;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // GFX_OGLSHADERPROGRAM_H
diff --git a/gfx/layers/opengl/TextureClientOGL.cpp b/gfx/layers/opengl/TextureClientOGL.cpp
new file mode 100644
index 0000000000..39d6010831
--- /dev/null
+++ b/gfx/layers/opengl/TextureClientOGL.cpp
@@ -0,0 +1,328 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "GLContext.h" // for GLContext, etc
+#include "mozilla/Assertions.h" // for MOZ_ASSERT, etc
+#include "mozilla/layers/ISurfaceAllocator.h"
+#include "mozilla/layers/TextureClientOGL.h"
+#include "mozilla/gfx/2D.h" // for Factory
+#include "mozilla/gfx/Point.h" // for IntSize
+#include "GLLibraryEGL.h"
+
+#ifdef MOZ_WIDGET_ANDROID
+# include <jni.h>
+# include <android/native_window.h>
+# include <android/native_window_jni.h>
+# include <sys/socket.h>
+# include "mozilla/ipc/FileDescriptor.h"
+# include "mozilla/java/GeckoSurfaceWrappers.h"
+# include "mozilla/java/SurfaceAllocatorWrappers.h"
+# include "mozilla/layers/AndroidHardwareBuffer.h"
+# include "mozilla/UniquePtrExtensions.h"
+#endif
+
+using namespace mozilla::gl;
+
+namespace mozilla {
+namespace layers {
+
+class CompositableForwarder;
+
+////////////////////////////////////////////////////////////////////////
+// AndroidSurface
+
+#ifdef MOZ_WIDGET_ANDROID
+
+already_AddRefed<TextureClient> AndroidSurfaceTextureData::CreateTextureClient(
+ AndroidSurfaceTextureHandle aHandle, gfx::IntSize aSize, bool aContinuous,
+ gl::OriginPos aOriginPos, bool aHasAlpha,
+ Maybe<gfx::Matrix4x4> aTransformOverride, LayersIPCChannel* aAllocator,
+ TextureFlags aFlags) {
+ if (aOriginPos == gl::OriginPos::BottomLeft) {
+ aFlags |= TextureFlags::ORIGIN_BOTTOM_LEFT;
+ }
+
+ return TextureClient::CreateWithData(
+ new AndroidSurfaceTextureData(aHandle, aSize, aContinuous, aHasAlpha,
+ aTransformOverride),
+ aFlags, aAllocator);
+}
+
+AndroidSurfaceTextureData::AndroidSurfaceTextureData(
+ AndroidSurfaceTextureHandle aHandle, gfx::IntSize aSize, bool aContinuous,
+ bool aHasAlpha, Maybe<gfx::Matrix4x4> aTransformOverride)
+ : mHandle(aHandle),
+ mSize(aSize),
+ mContinuous(aContinuous),
+ mHasAlpha(aHasAlpha),
+ mTransformOverride(aTransformOverride) {
+ MOZ_ASSERT(mHandle);
+}
+
+AndroidSurfaceTextureData::~AndroidSurfaceTextureData() {}
+
+void AndroidSurfaceTextureData::FillInfo(TextureData::Info& aInfo) const {
+ aInfo.size = mSize;
+ aInfo.format = gfx::SurfaceFormat::UNKNOWN;
+ aInfo.hasSynchronization = false;
+ aInfo.supportsMoz2D = false;
+ aInfo.canExposeMappedData = false;
+}
+
+bool AndroidSurfaceTextureData::Serialize(SurfaceDescriptor& aOutDescriptor) {
+ aOutDescriptor = SurfaceTextureDescriptor(
+ mHandle, mSize,
+ mHasAlpha ? gfx::SurfaceFormat::R8G8B8A8 : gfx::SurfaceFormat::R8G8B8X8,
+ mContinuous, mTransformOverride);
+ return true;
+}
+
+#endif // MOZ_WIDGET_ANDROID
+
+////////////////////////////////////////////////////////////////////////
+// AndroidNativeWindow
+
+#ifdef MOZ_WIDGET_ANDROID
+
+AndroidNativeWindowTextureData* AndroidNativeWindowTextureData::Create(
+ gfx::IntSize aSize, gfx::SurfaceFormat aFormat) {
+ if (aFormat != gfx::SurfaceFormat::R8G8B8A8 &&
+ aFormat != gfx::SurfaceFormat::R8G8B8X8 &&
+ aFormat != gfx::SurfaceFormat::B8G8R8A8 &&
+ aFormat != gfx::SurfaceFormat::B8G8R8X8 &&
+ aFormat != gfx::SurfaceFormat::R5G6B5_UINT16) {
+ return nullptr;
+ }
+
+ auto surface =
+ java::GeckoSurface::LocalRef(java::SurfaceAllocator::AcquireSurface(
+ aSize.width, aSize.height, true /* single-buffer mode */));
+ if (surface) {
+ return new AndroidNativeWindowTextureData(surface, aSize, aFormat);
+ }
+
+ return nullptr;
+}
+
+AndroidNativeWindowTextureData::AndroidNativeWindowTextureData(
+ java::GeckoSurface::Param aSurface, gfx::IntSize aSize,
+ gfx::SurfaceFormat aFormat)
+ : mSurface(aSurface), mIsLocked(false), mSize(aSize), mFormat(aFormat) {
+ mNativeWindow = ANativeWindow_fromSurface(jni::GetEnvForThread(),
+ mSurface->GetSurface().Get());
+ MOZ_ASSERT(mNativeWindow, "Failed to create NativeWindow.");
+
+ // SurfaceTextures don't technically support BGR, but we can just pretend to
+ // be RGB.
+ int32_t format = WINDOW_FORMAT_RGBA_8888;
+ switch (aFormat) {
+ case gfx::SurfaceFormat::R8G8B8A8:
+ case gfx::SurfaceFormat::B8G8R8A8:
+ format = WINDOW_FORMAT_RGBA_8888;
+ break;
+
+ case gfx::SurfaceFormat::R8G8B8X8:
+ case gfx::SurfaceFormat::B8G8R8X8:
+ format = WINDOW_FORMAT_RGBX_8888;
+ break;
+
+ case gfx::SurfaceFormat::R5G6B5_UINT16:
+ format = WINDOW_FORMAT_RGB_565;
+ break;
+
+ default:
+ MOZ_ASSERT(false, "Unsupported AndroidNativeWindowTextureData format.");
+ }
+
+ DebugOnly<int32_t> r = ANativeWindow_setBuffersGeometry(
+ mNativeWindow, mSize.width, mSize.height, format);
+ MOZ_ASSERT(r == 0, "ANativeWindow_setBuffersGeometry failed.");
+
+ // Ideally here we'd call ANativeWindow_setBuffersTransform() with the
+ // identity transform, but that is only available on api level >= 26.
+ // Instead use SurfaceDescriptor's transformOverride flag when serializing.
+}
+
+void AndroidNativeWindowTextureData::FillInfo(TextureData::Info& aInfo) const {
+ aInfo.size = mSize;
+ aInfo.format = mFormat;
+ aInfo.hasSynchronization = false;
+ aInfo.supportsMoz2D = true;
+ aInfo.canExposeMappedData = false;
+ aInfo.canConcurrentlyReadLock = false;
+}
+
+bool AndroidNativeWindowTextureData::Serialize(
+ SurfaceDescriptor& aOutDescriptor) {
+ aOutDescriptor = SurfaceTextureDescriptor(
+ mSurface->GetHandle(), mSize, mFormat, false /* not continuous */,
+ Some(gfx::Matrix4x4()) /* always use identity transform */);
+ return true;
+}
+
+bool AndroidNativeWindowTextureData::Lock(OpenMode) {
+ // ANativeWindows can only be locked and unlocked a single time, after which
+ // we must wait until they receive ownership back from the host.
+ // Therefore we must only actually call ANativeWindow_lock() once per cycle.
+ if (!mIsLocked) {
+ int32_t r = ANativeWindow_lock(mNativeWindow, &mBuffer, nullptr);
+ if (r == -ENOMEM) {
+ return false;
+ } else if (r < 0) {
+ MOZ_CRASH("ANativeWindow_lock failed.");
+ }
+ mIsLocked = true;
+ }
+ return true;
+}
+
+void AndroidNativeWindowTextureData::Unlock() {
+ // The TextureClient may want to call Lock again before handing ownership
+ // to the host, so we cannot call ANativeWindow_unlockAndPost yet.
+}
+
+void AndroidNativeWindowTextureData::Forget(LayersIPCChannel*) {
+ MOZ_ASSERT(!mIsLocked,
+ "ANativeWindow should not be released while locked.\n");
+ ANativeWindow_release(mNativeWindow);
+ mNativeWindow = nullptr;
+ java::SurfaceAllocator::DisposeSurface(mSurface);
+ mSurface = nullptr;
+}
+
+already_AddRefed<gfx::DrawTarget>
+AndroidNativeWindowTextureData::BorrowDrawTarget() {
+ const int bpp = (mFormat == gfx::SurfaceFormat::R5G6B5_UINT16) ? 2 : 4;
+
+ return gfx::Factory::CreateDrawTargetForData(
+ gfx::BackendType::SKIA, static_cast<unsigned char*>(mBuffer.bits),
+ gfx::IntSize(mBuffer.width, mBuffer.height), mBuffer.stride * bpp,
+ mFormat, true);
+}
+
+void AndroidNativeWindowTextureData::OnForwardedToHost() {
+ if (mIsLocked) {
+ int32_t r = ANativeWindow_unlockAndPost(mNativeWindow);
+ if (r < 0) {
+ MOZ_CRASH("ANativeWindow_unlockAndPost failed\n.");
+ }
+ mIsLocked = false;
+ }
+}
+
+AndroidHardwareBufferTextureData* AndroidHardwareBufferTextureData::Create(
+ gfx::IntSize aSize, gfx::SurfaceFormat aFormat) {
+ RefPtr<AndroidHardwareBuffer> buffer =
+ AndroidHardwareBuffer::Create(aSize, aFormat);
+ if (!buffer) {
+ return nullptr;
+ }
+ return new AndroidHardwareBufferTextureData(buffer, aSize, aFormat);
+}
+
+AndroidHardwareBufferTextureData::AndroidHardwareBufferTextureData(
+ AndroidHardwareBuffer* aAndroidHardwareBuffer, gfx::IntSize aSize,
+ gfx::SurfaceFormat aFormat)
+ : mAndroidHardwareBuffer(aAndroidHardwareBuffer),
+ mSize(aSize),
+ mFormat(aFormat),
+ mAddress(nullptr),
+ mIsLocked(false) {}
+
+AndroidHardwareBufferTextureData::~AndroidHardwareBufferTextureData() {}
+
+void AndroidHardwareBufferTextureData::FillInfo(
+ TextureData::Info& aInfo) const {
+ aInfo.size = mSize;
+ aInfo.format = mFormat;
+ aInfo.hasSynchronization = true;
+ aInfo.supportsMoz2D = true;
+ aInfo.canExposeMappedData = false;
+ aInfo.canConcurrentlyReadLock = true;
+}
+
+bool AndroidHardwareBufferTextureData::Serialize(
+ SurfaceDescriptor& aOutDescriptor) {
+ aOutDescriptor = SurfaceDescriptorAndroidHardwareBuffer(
+ mAndroidHardwareBuffer->mId, mSize, mFormat);
+ return true;
+}
+
+bool AndroidHardwareBufferTextureData::Lock(OpenMode aMode) {
+ if (!mIsLocked) {
+ MOZ_ASSERT(!mAddress);
+
+ uint64_t usage = 0;
+ if (aMode & OpenMode::OPEN_READ) {
+ usage |= AHARDWAREBUFFER_USAGE_CPU_READ_OFTEN;
+ }
+ if (aMode & OpenMode::OPEN_WRITE) {
+ usage |= AHARDWAREBUFFER_USAGE_CPU_WRITE_OFTEN;
+ }
+
+ int ret = mAndroidHardwareBuffer->Lock(usage, 0, &mAddress);
+ if (ret) {
+ mAddress = nullptr;
+ return false;
+ }
+ mIsLocked = true;
+ }
+ return true;
+}
+
+void AndroidHardwareBufferTextureData::Unlock() {
+ // The TextureClient may want to call Lock again before handing ownership
+ // to the host, so we cannot call AHardwareBuffer_unlock yet.
+}
+
+void AndroidHardwareBufferTextureData::Forget(LayersIPCChannel*) {
+ MOZ_ASSERT(!mIsLocked);
+ mAndroidHardwareBuffer = nullptr;
+ mAddress = nullptr;
+}
+
+already_AddRefed<gfx::DrawTarget>
+AndroidHardwareBufferTextureData::BorrowDrawTarget() {
+ MOZ_ASSERT(mIsLocked);
+
+ const int bpp = (mFormat == gfx::SurfaceFormat::R5G6B5_UINT16) ? 2 : 4;
+
+ return gfx::Factory::CreateDrawTargetForData(
+ gfx::BackendType::SKIA, static_cast<unsigned char*>(mAddress),
+ gfx::IntSize(mAndroidHardwareBuffer->mSize.width,
+ mAndroidHardwareBuffer->mSize.height),
+ mAndroidHardwareBuffer->mStride * bpp, mFormat, true);
+}
+
+void AndroidHardwareBufferTextureData::OnForwardedToHost() {
+ if (mIsLocked) {
+ mAndroidHardwareBuffer->Unlock();
+ mAddress = nullptr;
+ mIsLocked = false;
+ }
+}
+
+TextureFlags AndroidHardwareBufferTextureData::GetTextureFlags() const {
+ return TextureFlags::WAIT_HOST_USAGE_END;
+}
+
+Maybe<uint64_t> AndroidHardwareBufferTextureData::GetBufferId() const {
+ return Some(mAndroidHardwareBuffer->mId);
+}
+
+mozilla::ipc::FileDescriptor
+AndroidHardwareBufferTextureData::GetAcquireFence() {
+ if (!mAndroidHardwareBuffer) {
+ return ipc::FileDescriptor();
+ }
+
+ return mAndroidHardwareBuffer->GetAcquireFence();
+}
+
+#endif // MOZ_WIDGET_ANDROID
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/opengl/TextureClientOGL.h b/gfx/layers/opengl/TextureClientOGL.h
new file mode 100644
index 0000000000..7e10e4611c
--- /dev/null
+++ b/gfx/layers/opengl/TextureClientOGL.h
@@ -0,0 +1,164 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_GFX_TEXTURECLIENTOGL_H
+#define MOZILLA_GFX_TEXTURECLIENTOGL_H
+
+#include "GLContextTypes.h" // for SharedTextureHandle, etc
+#include "GLImages.h"
+#include "gfxTypes.h"
+#include "mozilla/Attributes.h" // for override
+#include "mozilla/gfx/Point.h" // for IntSize
+#include "mozilla/gfx/Types.h" // for SurfaceFormat
+#include "mozilla/layers/CompositorTypes.h"
+#include "mozilla/layers/LayersSurfaces.h" // for SurfaceDescriptor
+#include "mozilla/layers/TextureClient.h" // for TextureClient, etc
+#ifdef MOZ_WIDGET_ANDROID
+# include "AndroidSurfaceTexture.h"
+# include "AndroidNativeWindow.h"
+# include "mozilla/java/GeckoSurfaceWrappers.h"
+# include "mozilla/layers/AndroidHardwareBuffer.h"
+#endif
+
+namespace mozilla {
+
+namespace gfx {
+
+class DrawTarget;
+
+} // namespace gfx
+
+namespace layers {
+
+#ifdef MOZ_WIDGET_ANDROID
+
+class AndroidHardwareBuffer;
+
+class AndroidSurfaceTextureData : public TextureData {
+ public:
+ static already_AddRefed<TextureClient> CreateTextureClient(
+ AndroidSurfaceTextureHandle aHandle, gfx::IntSize aSize, bool aContinuous,
+ gl::OriginPos aOriginPos, bool aHasAlpha,
+ Maybe<gfx::Matrix4x4> aTransformOverride, LayersIPCChannel* aAllocator,
+ TextureFlags aFlags);
+
+ virtual ~AndroidSurfaceTextureData();
+
+ void FillInfo(TextureData::Info& aInfo) const override;
+
+ bool Serialize(SurfaceDescriptor& aOutDescriptor) override;
+
+ // Useless functions.
+ bool Lock(OpenMode) override { return true; }
+
+ void Unlock() override {}
+
+ // Our data is always owned externally.
+ void Deallocate(LayersIPCChannel*) override {}
+
+ protected:
+ AndroidSurfaceTextureData(AndroidSurfaceTextureHandle aHandle,
+ gfx::IntSize aSize, bool aContinuous,
+ bool aHasAlpha,
+ Maybe<gfx::Matrix4x4> aTransformOverride);
+
+ const AndroidSurfaceTextureHandle mHandle;
+ const gfx::IntSize mSize;
+ const bool mContinuous;
+ const bool mHasAlpha;
+ const Maybe<gfx::Matrix4x4> mTransformOverride;
+};
+
+class AndroidNativeWindowTextureData : public TextureData {
+ public:
+ static AndroidNativeWindowTextureData* Create(gfx::IntSize aSize,
+ gfx::SurfaceFormat aFormat);
+
+ void FillInfo(TextureData::Info& aInfo) const override;
+
+ bool Serialize(SurfaceDescriptor& aOutDescriptor) override;
+
+ bool Lock(OpenMode) override;
+ void Unlock() override;
+
+ void Forget(LayersIPCChannel*) override;
+ void Deallocate(LayersIPCChannel*) override {}
+
+ already_AddRefed<gfx::DrawTarget> BorrowDrawTarget() override;
+
+ void OnForwardedToHost() override;
+
+ protected:
+ AndroidNativeWindowTextureData(java::GeckoSurface::Param aSurface,
+ gfx::IntSize aSize,
+ gfx::SurfaceFormat aFormat);
+
+ private:
+ java::GeckoSurface::GlobalRef mSurface;
+ ANativeWindow* mNativeWindow;
+ ANativeWindow_Buffer mBuffer;
+ // Keeps track of whether the underlying NativeWindow is actually locked.
+ bool mIsLocked;
+
+ const gfx::IntSize mSize;
+ const gfx::SurfaceFormat mFormat;
+};
+
+class AndroidHardwareBufferTextureData : public TextureData {
+ public:
+ static AndroidHardwareBufferTextureData* Create(gfx::IntSize aSize,
+ gfx::SurfaceFormat aFormat);
+
+ virtual ~AndroidHardwareBufferTextureData();
+
+ void FillInfo(TextureData::Info& aInfo) const override;
+
+ bool Serialize(SurfaceDescriptor& aOutDescriptor) override;
+
+ bool Lock(OpenMode aMode) override;
+ void Unlock() override;
+
+ void Forget(LayersIPCChannel*) override;
+ void Deallocate(LayersIPCChannel*) override {}
+
+ already_AddRefed<gfx::DrawTarget> BorrowDrawTarget() override;
+
+ void OnForwardedToHost() override;
+
+ TextureFlags GetTextureFlags() const override;
+
+ Maybe<uint64_t> GetBufferId() const override;
+
+ mozilla::ipc::FileDescriptor GetAcquireFence() override;
+
+ AndroidHardwareBufferTextureData* AsAndroidHardwareBufferTextureData()
+ override {
+ return this;
+ }
+
+ AndroidHardwareBuffer* GetBuffer() { return mAndroidHardwareBuffer; }
+
+ protected:
+ AndroidHardwareBufferTextureData(
+ AndroidHardwareBuffer* aAndroidHardwareBuffer, gfx::IntSize aSize,
+ gfx::SurfaceFormat aFormat);
+
+ RefPtr<AndroidHardwareBuffer> mAndroidHardwareBuffer;
+ const gfx::IntSize mSize;
+ const gfx::SurfaceFormat mFormat;
+
+ void* mAddress;
+
+ // Keeps track of whether the underlying NativeWindow is actually locked.
+ bool mIsLocked;
+};
+
+#endif // MOZ_WIDGET_ANDROID
+
+} // namespace layers
+} // namespace mozilla
+
+#endif
diff --git a/gfx/layers/opengl/TextureHostOGL.cpp b/gfx/layers/opengl/TextureHostOGL.cpp
new file mode 100644
index 0000000000..f3310dda1c
--- /dev/null
+++ b/gfx/layers/opengl/TextureHostOGL.cpp
@@ -0,0 +1,1049 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "TextureHostOGL.h"
+
+#include "GLContextEGL.h" // for GLContext, etc
+#include "GLLibraryEGL.h" // for GLLibraryEGL
+#include "GLUploadHelpers.h"
+#include "GLReadTexImageHelper.h"
+#include "gfx2DGlue.h" // for ContentForFormat, etc
+#include "mozilla/gfx/2D.h" // for DataSourceSurface
+#include "mozilla/gfx/BaseSize.h" // for BaseSize
+#include "mozilla/gfx/gfxVars.h"
+#include "mozilla/gfx/Logging.h" // for gfxCriticalError
+#include "mozilla/layers/ISurfaceAllocator.h"
+#include "mozilla/webrender/RenderEGLImageTextureHost.h"
+#include "mozilla/webrender/WebRenderAPI.h"
+#include "nsRegion.h" // for nsIntRegion
+#include "GfxTexturesReporter.h" // for GfxTexturesReporter
+#include "GeckoProfiler.h"
+
+#ifdef XP_MACOSX
+# include "mozilla/layers/MacIOSurfaceTextureHostOGL.h"
+#endif
+
+#ifdef MOZ_WIDGET_ANDROID
+# include "mozilla/layers/AndroidHardwareBuffer.h"
+# include "mozilla/webrender/RenderAndroidHardwareBufferTextureHost.h"
+# include "mozilla/webrender/RenderAndroidSurfaceTextureHost.h"
+#endif
+
+#ifdef MOZ_WAYLAND
+# include "mozilla/layers/DMABUFTextureHostOGL.h"
+#endif
+
+using namespace mozilla::gl;
+using namespace mozilla::gfx;
+
+namespace mozilla {
+namespace layers {
+
+class Compositor;
+
+void ApplySamplingFilterToBoundTexture(gl::GLContext* aGL,
+ gfx::SamplingFilter aSamplingFilter,
+ GLuint aTarget) {
+ GLenum filter =
+ (aSamplingFilter == gfx::SamplingFilter::POINT ? LOCAL_GL_NEAREST
+ : LOCAL_GL_LINEAR);
+
+ aGL->fTexParameteri(aTarget, LOCAL_GL_TEXTURE_MIN_FILTER, filter);
+ aGL->fTexParameteri(aTarget, LOCAL_GL_TEXTURE_MAG_FILTER, filter);
+}
+
+already_AddRefed<TextureHost> CreateTextureHostOGL(
+ const SurfaceDescriptor& aDesc, ISurfaceAllocator* aDeallocator,
+ LayersBackend aBackend, TextureFlags aFlags) {
+ RefPtr<TextureHost> result;
+ switch (aDesc.type()) {
+#ifdef MOZ_WIDGET_ANDROID
+ case SurfaceDescriptor::TSurfaceTextureDescriptor: {
+ const SurfaceTextureDescriptor& desc =
+ aDesc.get_SurfaceTextureDescriptor();
+ java::GeckoSurfaceTexture::LocalRef surfaceTexture =
+ java::GeckoSurfaceTexture::Lookup(desc.handle());
+
+ result = new SurfaceTextureHost(aFlags, surfaceTexture, desc.size(),
+ desc.format(), desc.continuous(),
+ desc.transformOverride());
+ break;
+ }
+ case SurfaceDescriptor::TSurfaceDescriptorAndroidHardwareBuffer: {
+ const SurfaceDescriptorAndroidHardwareBuffer& desc =
+ aDesc.get_SurfaceDescriptorAndroidHardwareBuffer();
+ result = AndroidHardwareBufferTextureHost::Create(aFlags, desc);
+ break;
+ }
+#endif
+
+ case SurfaceDescriptor::TEGLImageDescriptor: {
+ const EGLImageDescriptor& desc = aDesc.get_EGLImageDescriptor();
+ result = new EGLImageTextureHost(aFlags, (EGLImage)desc.image(),
+ (EGLSync)desc.fence(), desc.size(),
+ desc.hasAlpha());
+ break;
+ }
+
+#ifdef MOZ_WAYLAND
+ case SurfaceDescriptor::TSurfaceDescriptorDMABuf: {
+ result = new DMABUFTextureHostOGL(aFlags, aDesc);
+ break;
+ }
+#endif
+
+#ifdef XP_MACOSX
+ case SurfaceDescriptor::TSurfaceDescriptorMacIOSurface: {
+ const SurfaceDescriptorMacIOSurface& desc =
+ aDesc.get_SurfaceDescriptorMacIOSurface();
+ result = new MacIOSurfaceTextureHostOGL(aFlags, desc);
+ break;
+ }
+#endif
+
+ case SurfaceDescriptor::TSurfaceDescriptorSharedGLTexture: {
+ const auto& desc = aDesc.get_SurfaceDescriptorSharedGLTexture();
+ result =
+ new GLTextureHost(aFlags, desc.texture(), desc.target(),
+ (GLsync)desc.fence(), desc.size(), desc.hasAlpha());
+ break;
+ }
+ default: {
+ MOZ_ASSERT_UNREACHABLE("Unsupported SurfaceDescriptor type");
+ break;
+ }
+ }
+ return result.forget();
+}
+
+static gl::TextureImage::Flags FlagsToGLFlags(TextureFlags aFlags) {
+ uint32_t result = TextureImage::NoFlags;
+
+ if (aFlags & TextureFlags::USE_NEAREST_FILTER)
+ result |= TextureImage::UseNearestFilter;
+ if (aFlags & TextureFlags::ORIGIN_BOTTOM_LEFT)
+ result |= TextureImage::OriginBottomLeft;
+ if (aFlags & TextureFlags::DISALLOW_BIGIMAGE)
+ result |= TextureImage::DisallowBigImage;
+
+ return static_cast<gl::TextureImage::Flags>(result);
+}
+
+TextureImageTextureSourceOGL::TextureImageTextureSourceOGL(
+ CompositorOGL* aCompositor, TextureFlags aFlags)
+ : mGL(aCompositor->gl()),
+ mCompositor(aCompositor),
+ mFlags(aFlags),
+ mIterating(false) {
+ if (mCompositor) {
+ mCompositor->RegisterTextureSource(this);
+ }
+}
+
+TextureImageTextureSourceOGL::~TextureImageTextureSourceOGL() {
+ DeallocateDeviceData();
+}
+
+void TextureImageTextureSourceOGL::DeallocateDeviceData() {
+ mTexImage = nullptr;
+ mGL = nullptr;
+ if (mCompositor) {
+ mCompositor->UnregisterTextureSource(this);
+ }
+ SetUpdateSerial(0);
+}
+
+bool TextureImageTextureSourceOGL::Update(gfx::DataSourceSurface* aSurface,
+ nsIntRegion* aDestRegion,
+ gfx::IntPoint* aSrcOffset,
+ gfx::IntPoint* aDstOffset) {
+ GLContext* gl = mGL;
+ MOZ_ASSERT(gl);
+ if (!gl || !gl->MakeCurrent()) {
+ NS_WARNING(
+ "trying to update TextureImageTextureSourceOGL without a GLContext");
+ return false;
+ }
+ if (!aSurface) {
+ gfxCriticalError() << "Invalid surface for OGL update";
+ return false;
+ }
+ MOZ_ASSERT(aSurface);
+
+ IntSize size = aSurface->GetSize();
+ if (!mTexImage || (mTexImage->GetSize() != size && !aSrcOffset) ||
+ mTexImage->GetContentType() !=
+ gfx::ContentForFormat(aSurface->GetFormat())) {
+ if (mFlags & TextureFlags::DISALLOW_BIGIMAGE) {
+ GLint maxTextureSize;
+ gl->fGetIntegerv(LOCAL_GL_MAX_TEXTURE_SIZE, &maxTextureSize);
+ if (size.width > maxTextureSize || size.height > maxTextureSize) {
+ NS_WARNING("Texture exceeds maximum texture size, refusing upload");
+ return false;
+ }
+ // Explicitly use CreateBasicTextureImage instead of CreateTextureImage,
+ // because CreateTextureImage might still choose to create a tiled
+ // texture image.
+ mTexImage = CreateBasicTextureImage(
+ gl, size, gfx::ContentForFormat(aSurface->GetFormat()),
+ LOCAL_GL_CLAMP_TO_EDGE, FlagsToGLFlags(mFlags));
+ } else {
+ // XXX - clarify which size we want to use. IncrementalContentHost will
+ // require the size of the destination surface to be different from
+ // the size of aSurface.
+ // See bug 893300 (tracks the implementation of ContentHost for new
+ // textures).
+ mTexImage = CreateTextureImage(
+ gl, size, gfx::ContentForFormat(aSurface->GetFormat()),
+ LOCAL_GL_CLAMP_TO_EDGE, FlagsToGLFlags(mFlags),
+ SurfaceFormatToImageFormat(aSurface->GetFormat()));
+ }
+ ClearCachedFilter();
+
+ if (aDestRegion && !aSrcOffset &&
+ !aDestRegion->IsEqual(gfx::IntRect(0, 0, size.width, size.height))) {
+ // UpdateFromDataSource will ignore our specified aDestRegion since the
+ // texture hasn't been allocated with glTexImage2D yet. Call Resize() to
+ // force the allocation (full size, but no upload), and then we'll only
+ // upload the pixels we care about below.
+ mTexImage->Resize(size);
+ }
+ }
+
+ return mTexImage->UpdateFromDataSource(aSurface, aDestRegion, aSrcOffset,
+ aDstOffset);
+}
+
+void TextureImageTextureSourceOGL::EnsureBuffer(const IntSize& aSize,
+ gfxContentType aContentType) {
+ if (!mTexImage || mTexImage->GetSize() != aSize ||
+ mTexImage->GetContentType() != aContentType) {
+ mTexImage =
+ CreateTextureImage(mGL, aSize, aContentType, LOCAL_GL_CLAMP_TO_EDGE,
+ FlagsToGLFlags(mFlags));
+ }
+ mTexImage->Resize(aSize);
+}
+
+gfx::IntSize TextureImageTextureSourceOGL::GetSize() const {
+ if (mTexImage) {
+ if (mIterating) {
+ return mTexImage->GetTileRect().Size();
+ }
+ return mTexImage->GetSize();
+ }
+ NS_WARNING("Trying to query the size of an empty TextureSource.");
+ return gfx::IntSize(0, 0);
+}
+
+gfx::SurfaceFormat TextureImageTextureSourceOGL::GetFormat() const {
+ if (mTexImage) {
+ return mTexImage->GetTextureFormat();
+ }
+ NS_WARNING("Trying to query the format of an empty TextureSource.");
+ return gfx::SurfaceFormat::UNKNOWN;
+}
+
+gfx::IntRect TextureImageTextureSourceOGL::GetTileRect() {
+ return mTexImage->GetTileRect();
+}
+
+void TextureImageTextureSourceOGL::BindTexture(
+ GLenum aTextureUnit, gfx::SamplingFilter aSamplingFilter) {
+ MOZ_ASSERT(mTexImage,
+ "Trying to bind a TextureSource that does not have an underlying "
+ "GL texture.");
+ mTexImage->BindTexture(aTextureUnit);
+ SetSamplingFilter(mGL, aSamplingFilter);
+}
+
+////////////////////////////////////////////////////////////////////////
+// GLTextureSource
+
+GLTextureSource::GLTextureSource(TextureSourceProvider* aProvider,
+ GLuint aTextureHandle, GLenum aTarget,
+ gfx::IntSize aSize, gfx::SurfaceFormat aFormat)
+ : GLTextureSource(aProvider->GetGLContext(), aTextureHandle, aTarget, aSize,
+ aFormat) {}
+
+GLTextureSource::GLTextureSource(GLContext* aGL, GLuint aTextureHandle,
+ GLenum aTarget, gfx::IntSize aSize,
+ gfx::SurfaceFormat aFormat)
+ : mGL(aGL),
+ mTextureHandle(aTextureHandle),
+ mTextureTarget(aTarget),
+ mSize(aSize),
+ mFormat(aFormat) {
+ MOZ_COUNT_CTOR(GLTextureSource);
+}
+
+GLTextureSource::~GLTextureSource() {
+ MOZ_COUNT_DTOR(GLTextureSource);
+ DeleteTextureHandle();
+}
+
+void GLTextureSource::DeallocateDeviceData() { DeleteTextureHandle(); }
+
+void GLTextureSource::DeleteTextureHandle() {
+ GLContext* gl = this->gl();
+ if (mTextureHandle != 0 && gl && gl->MakeCurrent()) {
+ gl->fDeleteTextures(1, &mTextureHandle);
+ }
+ mTextureHandle = 0;
+}
+
+void GLTextureSource::BindTexture(GLenum aTextureUnit,
+ gfx::SamplingFilter aSamplingFilter) {
+ MOZ_ASSERT(mTextureHandle != 0);
+ GLContext* gl = this->gl();
+ if (!gl || !gl->MakeCurrent()) {
+ return;
+ }
+ gl->fActiveTexture(aTextureUnit);
+ gl->fBindTexture(mTextureTarget, mTextureHandle);
+ ApplySamplingFilterToBoundTexture(gl, aSamplingFilter, mTextureTarget);
+}
+
+bool GLTextureSource::IsValid() const { return !!gl() && mTextureHandle != 0; }
+
+////////////////////////////////////////////////////////////////////////
+// DirectMapTextureSource
+
+DirectMapTextureSource::DirectMapTextureSource(gl::GLContext* aContext,
+ gfx::DataSourceSurface* aSurface)
+ : GLTextureSource(aContext, 0, LOCAL_GL_TEXTURE_RECTANGLE_ARB,
+ aSurface->GetSize(), aSurface->GetFormat()),
+ mSync(0) {
+ MOZ_ASSERT(aSurface);
+
+ UpdateInternal(aSurface, nullptr, nullptr, true);
+}
+
+DirectMapTextureSource::DirectMapTextureSource(TextureSourceProvider* aProvider,
+ gfx::DataSourceSurface* aSurface)
+ : DirectMapTextureSource(aProvider->GetGLContext(), aSurface) {}
+
+DirectMapTextureSource::~DirectMapTextureSource() {
+ if (!mSync || !gl() || !gl()->MakeCurrent() || gl()->IsDestroyed()) {
+ return;
+ }
+
+ gl()->fDeleteSync(mSync);
+ mSync = 0;
+}
+
+bool DirectMapTextureSource::Update(gfx::DataSourceSurface* aSurface,
+ nsIntRegion* aDestRegion,
+ gfx::IntPoint* aSrcOffset,
+ gfx::IntPoint* aDstOffset) {
+ MOZ_RELEASE_ASSERT(aDstOffset == nullptr);
+ if (!aSurface) {
+ return false;
+ }
+
+ return UpdateInternal(aSurface, aDestRegion, aSrcOffset, false);
+}
+
+void DirectMapTextureSource::MaybeFenceTexture() {
+ if (!gl() || !gl()->MakeCurrent() || gl()->IsDestroyed()) {
+ return;
+ }
+
+ if (mSync) {
+ gl()->fDeleteSync(mSync);
+ }
+ mSync = gl()->fFenceSync(LOCAL_GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
+}
+
+bool DirectMapTextureSource::Sync(bool aBlocking) {
+ if (!gl() || !gl()->MakeCurrent() || gl()->IsDestroyed()) {
+ // We use this function to decide whether we can unlock the texture
+ // and clean it up. If we return false here and for whatever reason
+ // the context is absent or invalid, the compositor will keep a
+ // reference to this texture forever.
+ return true;
+ }
+
+ if (!mSync) {
+ return false;
+ }
+
+ GLenum waitResult =
+ gl()->fClientWaitSync(mSync, LOCAL_GL_SYNC_FLUSH_COMMANDS_BIT,
+ aBlocking ? LOCAL_GL_TIMEOUT_IGNORED : 0);
+ return waitResult == LOCAL_GL_ALREADY_SIGNALED ||
+ waitResult == LOCAL_GL_CONDITION_SATISFIED;
+}
+
+bool DirectMapTextureSource::UpdateInternal(gfx::DataSourceSurface* aSurface,
+ nsIntRegion* aDestRegion,
+ gfx::IntPoint* aSrcOffset,
+ bool aInit) {
+ if (!gl() || !gl()->MakeCurrent()) {
+ return false;
+ }
+
+ if (aInit) {
+ gl()->fGenTextures(1, &mTextureHandle);
+ gl()->fBindTexture(LOCAL_GL_TEXTURE_RECTANGLE_ARB, mTextureHandle);
+
+ gl()->fTexParameteri(LOCAL_GL_TEXTURE_RECTANGLE_ARB,
+ LOCAL_GL_TEXTURE_STORAGE_HINT_APPLE,
+ LOCAL_GL_STORAGE_CACHED_APPLE);
+ gl()->fTextureRangeAPPLE(LOCAL_GL_TEXTURE_RECTANGLE_ARB,
+ aSurface->Stride() * aSurface->GetSize().height,
+ aSurface->GetData());
+
+ gl()->fTexParameteri(LOCAL_GL_TEXTURE_RECTANGLE_ARB,
+ LOCAL_GL_TEXTURE_WRAP_S, LOCAL_GL_CLAMP_TO_EDGE);
+ gl()->fTexParameteri(LOCAL_GL_TEXTURE_RECTANGLE_ARB,
+ LOCAL_GL_TEXTURE_WRAP_T, LOCAL_GL_CLAMP_TO_EDGE);
+ }
+
+ MOZ_ASSERT(mTextureHandle);
+
+ // APPLE_client_storage
+ gl()->fPixelStorei(LOCAL_GL_UNPACK_CLIENT_STORAGE_APPLE, LOCAL_GL_TRUE);
+
+ nsIntRegion destRegion = aDestRegion
+ ? *aDestRegion
+ : IntRect(0, 0, aSurface->GetSize().width,
+ aSurface->GetSize().height);
+ gfx::IntPoint srcPoint = aSrcOffset ? *aSrcOffset : gfx::IntPoint(0, 0);
+ mFormat = gl::UploadSurfaceToTexture(
+ gl(), aSurface, destRegion, mTextureHandle, aSurface->GetSize(), nullptr,
+ aInit, srcPoint, gfx::IntPoint(0, 0), LOCAL_GL_TEXTURE0,
+ LOCAL_GL_TEXTURE_RECTANGLE_ARB);
+
+ if (mSync) {
+ gl()->fDeleteSync(mSync);
+ mSync = 0;
+ }
+
+ gl()->fPixelStorei(LOCAL_GL_UNPACK_CLIENT_STORAGE_APPLE, LOCAL_GL_FALSE);
+ return true;
+}
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+// SurfaceTextureHost
+
+#ifdef MOZ_WIDGET_ANDROID
+
+SurfaceTextureSource::SurfaceTextureSource(
+ TextureSourceProvider* aProvider,
+ mozilla::java::GeckoSurfaceTexture::Ref& aSurfTex,
+ gfx::SurfaceFormat aFormat, GLenum aTarget, GLenum aWrapMode,
+ gfx::IntSize aSize, Maybe<gfx::Matrix4x4> aTransformOverride)
+ : mGL(aProvider->GetGLContext()),
+ mSurfTex(aSurfTex),
+ mFormat(aFormat),
+ mTextureTarget(aTarget),
+ mWrapMode(aWrapMode),
+ mSize(aSize),
+ mTransformOverride(aTransformOverride) {}
+
+void SurfaceTextureSource::BindTexture(GLenum aTextureUnit,
+ gfx::SamplingFilter aSamplingFilter) {
+ MOZ_ASSERT(mSurfTex);
+ GLContext* gl = this->gl();
+ if (!gl || !gl->MakeCurrent()) {
+ NS_WARNING("Trying to bind a texture without a GLContext");
+ return;
+ }
+
+ gl->fActiveTexture(aTextureUnit);
+ gl->fBindTexture(mTextureTarget, mSurfTex->GetTexName());
+
+ ApplySamplingFilterToBoundTexture(gl, aSamplingFilter, mTextureTarget);
+}
+
+bool SurfaceTextureSource::IsValid() const { return !!gl(); }
+
+gfx::Matrix4x4 SurfaceTextureSource::GetTextureTransform() {
+ MOZ_ASSERT(mSurfTex);
+
+ gfx::Matrix4x4 ret;
+
+ // GetTransformMatrix() returns the transform set by the producer side of the
+ // SurfaceTexture that must be applied to texture coordinates when
+ // sampling. In some cases we may have set an override value, such as in
+ // AndroidNativeWindowTextureData where we own the producer side, or for
+ // MediaCodec output on devices where where we know the value is incorrect.
+ if (mTransformOverride) {
+ ret = *mTransformOverride;
+ } else {
+ const auto& surf = java::sdk::SurfaceTexture::LocalRef(
+ java::sdk::SurfaceTexture::Ref::From(mSurfTex));
+ AndroidSurfaceTexture::GetTransformMatrix(surf, &ret);
+ }
+
+ return ret;
+}
+
+void SurfaceTextureSource::DeallocateDeviceData() { mSurfTex = nullptr; }
+
+////////////////////////////////////////////////////////////////////////
+
+SurfaceTextureHost::SurfaceTextureHost(
+ TextureFlags aFlags, mozilla::java::GeckoSurfaceTexture::Ref& aSurfTex,
+ gfx::IntSize aSize, gfx::SurfaceFormat aFormat, bool aContinuousUpdate,
+ Maybe<Matrix4x4> aTransformOverride)
+ : TextureHost(TextureHostType::AndroidSurfaceTexture, aFlags),
+ mSurfTex(aSurfTex),
+ mSize(aSize),
+ mFormat(aFormat),
+ mContinuousUpdate(aContinuousUpdate),
+ mTransformOverride(aTransformOverride) {
+ if (!mSurfTex) {
+ return;
+ }
+
+ // Continuous update makes no sense with single buffer mode
+ MOZ_ASSERT(!mSurfTex->IsSingleBuffer() || !mContinuousUpdate);
+
+ mSurfTex->IncrementUse();
+}
+
+SurfaceTextureHost::~SurfaceTextureHost() {
+ if (mSurfTex) {
+ mSurfTex->DecrementUse();
+ mSurfTex = nullptr;
+ }
+}
+
+gl::GLContext* SurfaceTextureHost::gl() const { return nullptr; }
+
+gfx::SurfaceFormat SurfaceTextureHost::GetFormat() const { return mFormat; }
+
+void SurfaceTextureHost::DeallocateDeviceData() {
+ if (mTextureSource) {
+ mTextureSource->DeallocateDeviceData();
+ }
+
+ if (mSurfTex) {
+ mSurfTex->DecrementUse();
+ mSurfTex = nullptr;
+ }
+}
+
+void SurfaceTextureHost::CreateRenderTexture(
+ const wr::ExternalImageId& aExternalImageId) {
+ bool isRemoteTexture = !!(mFlags & TextureFlags::REMOTE_TEXTURE);
+ RefPtr<wr::RenderTextureHost> texture =
+ new wr::RenderAndroidSurfaceTextureHost(
+ mSurfTex, mSize, mFormat, mContinuousUpdate, mTransformOverride,
+ isRemoteTexture);
+ wr::RenderThread::Get()->RegisterExternalImage(aExternalImageId,
+ texture.forget());
+}
+
+uint32_t SurfaceTextureHost::NumSubTextures() { return mSurfTex ? 1 : 0; }
+
+void SurfaceTextureHost::PushResourceUpdates(
+ wr::TransactionBuilder& aResources, ResourceUpdateOp aOp,
+ const Range<wr::ImageKey>& aImageKeys, const wr::ExternalImageId& aExtID) {
+ auto method = aOp == TextureHost::ADD_IMAGE
+ ? &wr::TransactionBuilder::AddExternalImage
+ : &wr::TransactionBuilder::UpdateExternalImage;
+
+ // Prefer TextureExternal unless the backend requires TextureRect.
+ TextureHost::NativeTexturePolicy policy =
+ TextureHost::BackendNativeTexturePolicy(aResources.GetBackendType(),
+ GetSize());
+ auto imageType = policy == TextureHost::NativeTexturePolicy::REQUIRE
+ ? wr::ExternalImageType::TextureHandle(
+ wr::ImageBufferKind::TextureRect)
+ : wr::ExternalImageType::TextureHandle(
+ wr::ImageBufferKind::TextureExternal);
+
+ switch (GetFormat()) {
+ case gfx::SurfaceFormat::R8G8B8X8:
+ case gfx::SurfaceFormat::R8G8B8A8: {
+ MOZ_ASSERT(aImageKeys.length() == 1);
+
+ // XXX Add RGBA handling. Temporary hack to avoid crash
+ // With BGRA format setting, rendering works without problem.
+ auto format = GetFormat() == gfx::SurfaceFormat::R8G8B8A8
+ ? gfx::SurfaceFormat::B8G8R8A8
+ : gfx::SurfaceFormat::B8G8R8X8;
+ wr::ImageDescriptor descriptor(GetSize(), format);
+ (aResources.*method)(aImageKeys[0], descriptor, aExtID, imageType, 0);
+ break;
+ }
+ default: {
+ MOZ_ASSERT_UNREACHABLE("unexpected to be called");
+ }
+ }
+}
+
+void SurfaceTextureHost::PushDisplayItems(wr::DisplayListBuilder& aBuilder,
+ const wr::LayoutRect& aBounds,
+ const wr::LayoutRect& aClip,
+ wr::ImageRendering aFilter,
+ const Range<wr::ImageKey>& aImageKeys,
+ PushDisplayItemFlagSet aFlags) {
+ bool preferCompositorSurface =
+ aFlags.contains(PushDisplayItemFlag::PREFER_COMPOSITOR_SURFACE);
+ bool supportsExternalCompositing =
+ SupportsExternalCompositing(aBuilder.GetBackendType());
+
+ switch (GetFormat()) {
+ case gfx::SurfaceFormat::R8G8B8X8:
+ case gfx::SurfaceFormat::R8G8B8A8:
+ case gfx::SurfaceFormat::B8G8R8A8:
+ case gfx::SurfaceFormat::B8G8R8X8: {
+ MOZ_ASSERT(aImageKeys.length() == 1);
+ aBuilder.PushImage(aBounds, aClip, true, false, aFilter, aImageKeys[0],
+ !(mFlags & TextureFlags::NON_PREMULTIPLIED),
+ wr::ColorF{1.0f, 1.0f, 1.0f, 1.0f},
+ preferCompositorSurface, supportsExternalCompositing);
+ break;
+ }
+ default: {
+ MOZ_ASSERT_UNREACHABLE("unexpected to be called");
+ }
+ }
+}
+
+bool SurfaceTextureHost::SupportsExternalCompositing(
+ WebRenderBackend aBackend) {
+ return aBackend == WebRenderBackend::SOFTWARE;
+}
+
+////////////////////////////////////////////////////////////////////////
+// AndroidHardwareBufferTextureSource
+
+AndroidHardwareBufferTextureSource::AndroidHardwareBufferTextureSource(
+ TextureSourceProvider* aProvider,
+ AndroidHardwareBuffer* aAndroidHardwareBuffer, gfx::SurfaceFormat aFormat,
+ GLenum aTarget, GLenum aWrapMode, gfx::IntSize aSize)
+ : mGL(aProvider->GetGLContext()),
+ mAndroidHardwareBuffer(aAndroidHardwareBuffer),
+ mFormat(aFormat),
+ mTextureTarget(aTarget),
+ mWrapMode(aWrapMode),
+ mSize(aSize),
+ mEGLImage(EGL_NO_IMAGE),
+ mTextureHandle(0) {}
+
+AndroidHardwareBufferTextureSource::~AndroidHardwareBufferTextureSource() {
+ DeleteTextureHandle();
+ DestroyEGLImage();
+}
+
+bool AndroidHardwareBufferTextureSource::EnsureEGLImage() {
+ if (!mAndroidHardwareBuffer) {
+ return false;
+ }
+
+ auto fenceFd = mAndroidHardwareBuffer->GetAndResetAcquireFence();
+ if (fenceFd.IsValid()) {
+ const auto& gle = gl::GLContextEGL::Cast(mGL);
+ const auto& egl = gle->mEgl;
+
+ auto rawFD = fenceFd.TakePlatformHandle();
+ const EGLint attribs[] = {LOCAL_EGL_SYNC_NATIVE_FENCE_FD_ANDROID,
+ rawFD.get(), LOCAL_EGL_NONE};
+
+ EGLSync sync =
+ egl->fCreateSync(LOCAL_EGL_SYNC_NATIVE_FENCE_ANDROID, attribs);
+ if (sync) {
+ // Release fd here, since it is owned by EGLSync
+ Unused << rawFD.release();
+
+ if (egl->IsExtensionSupported(gl::EGLExtension::KHR_wait_sync)) {
+ egl->fWaitSync(sync, 0);
+ } else {
+ egl->fClientWaitSync(sync, 0, LOCAL_EGL_FOREVER);
+ }
+ egl->fDestroySync(sync);
+ } else {
+ gfxCriticalNote << "Failed to create EGLSync from acquire fence fd";
+ }
+ }
+
+ if (mTextureHandle) {
+ return true;
+ }
+
+ if (!mEGLImage) {
+ // XXX add crop handling for video
+ // Should only happen the first time.
+ const auto& gle = gl::GLContextEGL::Cast(mGL);
+ const auto& egl = gle->mEgl;
+
+ const EGLint attrs[] = {
+ LOCAL_EGL_IMAGE_PRESERVED,
+ LOCAL_EGL_TRUE,
+ LOCAL_EGL_NONE,
+ LOCAL_EGL_NONE,
+ };
+
+ EGLClientBuffer clientBuffer = egl->mLib->fGetNativeClientBufferANDROID(
+ mAndroidHardwareBuffer->GetNativeBuffer());
+ mEGLImage = egl->fCreateImage(
+ EGL_NO_CONTEXT, LOCAL_EGL_NATIVE_BUFFER_ANDROID, clientBuffer, attrs);
+ }
+ MOZ_ASSERT(mEGLImage);
+
+ mGL->fGenTextures(1, &mTextureHandle);
+ mGL->fBindTexture(LOCAL_GL_TEXTURE_EXTERNAL, mTextureHandle);
+ mGL->fTexParameteri(LOCAL_GL_TEXTURE_EXTERNAL, LOCAL_GL_TEXTURE_WRAP_T,
+ LOCAL_GL_CLAMP_TO_EDGE);
+ mGL->fTexParameteri(LOCAL_GL_TEXTURE_EXTERNAL, LOCAL_GL_TEXTURE_WRAP_S,
+ LOCAL_GL_CLAMP_TO_EDGE);
+ mGL->fEGLImageTargetTexture2D(LOCAL_GL_TEXTURE_EXTERNAL, mEGLImage);
+
+ return true;
+}
+
+void AndroidHardwareBufferTextureSource::DeleteTextureHandle() {
+ if (!mTextureHandle) {
+ return;
+ }
+ MOZ_ASSERT(mGL);
+ mGL->fDeleteTextures(1, &mTextureHandle);
+ mTextureHandle = 0;
+}
+
+void AndroidHardwareBufferTextureSource::DestroyEGLImage() {
+ if (!mEGLImage) {
+ return;
+ }
+ MOZ_ASSERT(mGL);
+ const auto& gle = gl::GLContextEGL::Cast(mGL);
+ const auto& egl = gle->mEgl;
+ egl->fDestroyImage(mEGLImage);
+ mEGLImage = EGL_NO_IMAGE;
+}
+
+void AndroidHardwareBufferTextureSource::BindTexture(
+ GLenum aTextureUnit, gfx::SamplingFilter aSamplingFilter) {
+ MOZ_ASSERT(mAndroidHardwareBuffer);
+ GLContext* gl = this->gl();
+ if (!gl || !gl->MakeCurrent()) {
+ NS_WARNING("Trying to bind a texture without a GLContext");
+ return;
+ }
+
+ if (!EnsureEGLImage()) {
+ return;
+ }
+
+ gl->fActiveTexture(aTextureUnit);
+ gl->fBindTexture(mTextureTarget, mTextureHandle);
+
+ ApplySamplingFilterToBoundTexture(gl, aSamplingFilter, mTextureTarget);
+}
+
+bool AndroidHardwareBufferTextureSource::IsValid() const { return !!gl(); }
+
+void AndroidHardwareBufferTextureSource::DeallocateDeviceData() {
+ DestroyEGLImage();
+ DeleteTextureHandle();
+ mAndroidHardwareBuffer = nullptr;
+}
+
+////////////////////////////////////////////////////////////////////////
+// AndroidHardwareBufferTextureHost
+
+/* static */
+already_AddRefed<AndroidHardwareBufferTextureHost>
+AndroidHardwareBufferTextureHost::Create(
+ TextureFlags aFlags, const SurfaceDescriptorAndroidHardwareBuffer& aDesc) {
+ RefPtr<AndroidHardwareBuffer> buffer =
+ AndroidHardwareBufferManager::Get()->GetBuffer(aDesc.bufferId());
+ if (!buffer) {
+ return nullptr;
+ }
+ RefPtr<AndroidHardwareBufferTextureHost> host =
+ new AndroidHardwareBufferTextureHost(aFlags, buffer);
+ return host.forget();
+}
+
+AndroidHardwareBufferTextureHost::AndroidHardwareBufferTextureHost(
+ TextureFlags aFlags, AndroidHardwareBuffer* aAndroidHardwareBuffer)
+ : TextureHost(TextureHostType::AndroidHardwareBuffer, aFlags),
+ mAndroidHardwareBuffer(aAndroidHardwareBuffer) {
+ MOZ_ASSERT(mAndroidHardwareBuffer);
+}
+
+AndroidHardwareBufferTextureHost::~AndroidHardwareBufferTextureHost() {}
+
+gl::GLContext* AndroidHardwareBufferTextureHost::gl() const { return nullptr; }
+
+void AndroidHardwareBufferTextureHost::NotifyNotUsed() {
+ TextureHost::NotifyNotUsed();
+}
+
+gfx::SurfaceFormat AndroidHardwareBufferTextureHost::GetFormat() const {
+ if (mAndroidHardwareBuffer) {
+ return mAndroidHardwareBuffer->mFormat;
+ }
+ return gfx::SurfaceFormat::UNKNOWN;
+}
+
+gfx::IntSize AndroidHardwareBufferTextureHost::GetSize() const {
+ if (mAndroidHardwareBuffer) {
+ return mAndroidHardwareBuffer->mSize;
+ }
+ return gfx::IntSize();
+}
+
+void AndroidHardwareBufferTextureHost::DeallocateDeviceData() {
+ mAndroidHardwareBuffer = nullptr;
+}
+
+void AndroidHardwareBufferTextureHost::SetAcquireFence(
+ mozilla::ipc::FileDescriptor&& aFenceFd) {
+ if (!mAndroidHardwareBuffer) {
+ return;
+ }
+ mAndroidHardwareBuffer->SetAcquireFence(std::move(aFenceFd));
+}
+
+void AndroidHardwareBufferTextureHost::SetReleaseFence(
+ mozilla::ipc::FileDescriptor&& aFenceFd) {
+ if (!mAndroidHardwareBuffer) {
+ return;
+ }
+ mAndroidHardwareBuffer->SetReleaseFence(std::move(aFenceFd));
+}
+
+mozilla::ipc::FileDescriptor
+AndroidHardwareBufferTextureHost::GetAndResetReleaseFence() {
+ if (!mAndroidHardwareBuffer) {
+ return mozilla::ipc::FileDescriptor();
+ }
+ return mAndroidHardwareBuffer->GetAndResetReleaseFence();
+}
+
+void AndroidHardwareBufferTextureHost::CreateRenderTexture(
+ const wr::ExternalImageId& aExternalImageId) {
+ RefPtr<wr::RenderTextureHost> texture =
+ new wr::RenderAndroidHardwareBufferTextureHost(mAndroidHardwareBuffer);
+ wr::RenderThread::Get()->RegisterExternalImage(aExternalImageId,
+ texture.forget());
+}
+
+uint32_t AndroidHardwareBufferTextureHost::NumSubTextures() {
+ return mAndroidHardwareBuffer ? 1 : 0;
+}
+
+void AndroidHardwareBufferTextureHost::PushResourceUpdates(
+ wr::TransactionBuilder& aResources, ResourceUpdateOp aOp,
+ const Range<wr::ImageKey>& aImageKeys, const wr::ExternalImageId& aExtID) {
+ auto method = aOp == TextureHost::ADD_IMAGE
+ ? &wr::TransactionBuilder::AddExternalImage
+ : &wr::TransactionBuilder::UpdateExternalImage;
+
+ // Prefer TextureExternal unless the backend requires TextureRect.
+ TextureHost::NativeTexturePolicy policy =
+ TextureHost::BackendNativeTexturePolicy(aResources.GetBackendType(),
+ GetSize());
+ auto imageType = policy == TextureHost::NativeTexturePolicy::REQUIRE
+ ? wr::ExternalImageType::TextureHandle(
+ wr::ImageBufferKind::TextureRect)
+ : wr::ExternalImageType::TextureHandle(
+ wr::ImageBufferKind::TextureExternal);
+
+ switch (GetFormat()) {
+ case gfx::SurfaceFormat::R8G8B8X8:
+ case gfx::SurfaceFormat::R8G8B8A8: {
+ MOZ_ASSERT(aImageKeys.length() == 1);
+
+ // XXX Add RGBA handling. Temporary hack to avoid crash
+ // With BGRA format setting, rendering works without problem.
+ auto format = GetFormat() == gfx::SurfaceFormat::R8G8B8A8
+ ? gfx::SurfaceFormat::B8G8R8A8
+ : gfx::SurfaceFormat::B8G8R8X8;
+ wr::ImageDescriptor descriptor(GetSize(), format);
+ (aResources.*method)(aImageKeys[0], descriptor, aExtID, imageType, 0);
+ break;
+ }
+ default: {
+ MOZ_ASSERT_UNREACHABLE("unexpected to be called");
+ }
+ }
+}
+
+void AndroidHardwareBufferTextureHost::PushDisplayItems(
+ wr::DisplayListBuilder& aBuilder, const wr::LayoutRect& aBounds,
+ const wr::LayoutRect& aClip, wr::ImageRendering aFilter,
+ const Range<wr::ImageKey>& aImageKeys, PushDisplayItemFlagSet aFlags) {
+ bool preferCompositorSurface =
+ aFlags.contains(PushDisplayItemFlag::PREFER_COMPOSITOR_SURFACE);
+ bool supportsExternalCompositing =
+ SupportsExternalCompositing(aBuilder.GetBackendType());
+
+ switch (GetFormat()) {
+ case gfx::SurfaceFormat::R8G8B8X8:
+ case gfx::SurfaceFormat::R8G8B8A8:
+ case gfx::SurfaceFormat::B8G8R8A8:
+ case gfx::SurfaceFormat::B8G8R8X8: {
+ MOZ_ASSERT(aImageKeys.length() == 1);
+ aBuilder.PushImage(aBounds, aClip, true, false, aFilter, aImageKeys[0],
+ !(mFlags & TextureFlags::NON_PREMULTIPLIED),
+ wr::ColorF{1.0f, 1.0f, 1.0f, 1.0f},
+ preferCompositorSurface, supportsExternalCompositing);
+ break;
+ }
+ default: {
+ MOZ_ASSERT_UNREACHABLE("unexpected to be called");
+ }
+ }
+}
+
+bool AndroidHardwareBufferTextureHost::SupportsExternalCompositing(
+ WebRenderBackend aBackend) {
+ return aBackend == WebRenderBackend::SOFTWARE;
+}
+
+#endif // MOZ_WIDGET_ANDROID
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+// EGLImage
+
+EGLImageTextureSource::EGLImageTextureSource(TextureSourceProvider* aProvider,
+ EGLImage aImage,
+ gfx::SurfaceFormat aFormat,
+ GLenum aTarget, GLenum aWrapMode,
+ gfx::IntSize aSize)
+ : mImage(aImage),
+ mFormat(aFormat),
+ mTextureTarget(aTarget),
+ mWrapMode(aWrapMode),
+ mSize(aSize) {
+ MOZ_ASSERT(mTextureTarget == LOCAL_GL_TEXTURE_2D ||
+ mTextureTarget == LOCAL_GL_TEXTURE_EXTERNAL);
+}
+
+void EGLImageTextureSource::BindTexture(GLenum aTextureUnit,
+ gfx::SamplingFilter aSamplingFilter) {
+ GLContext* gl = this->gl();
+ if (!gl || !gl->MakeCurrent()) {
+ NS_WARNING("Trying to bind a texture without a GLContext");
+ return;
+ }
+
+#ifdef DEBUG
+ const bool supportsEglImage = [&]() {
+ const auto& gle = GLContextEGL::Cast(gl);
+ const auto& egl = gle->mEgl;
+
+ return egl->HasKHRImageBase() &&
+ egl->IsExtensionSupported(EGLExtension::KHR_gl_texture_2D_image) &&
+ gl->IsExtensionSupported(GLContext::OES_EGL_image);
+ }();
+ MOZ_ASSERT(supportsEglImage, "EGLImage not supported or disabled in runtime");
+#endif
+
+ GLuint tex = mCompositor->GetTemporaryTexture(mTextureTarget, aTextureUnit);
+
+ gl->fActiveTexture(aTextureUnit);
+ gl->fBindTexture(mTextureTarget, tex);
+
+ gl->fEGLImageTargetTexture2D(mTextureTarget, mImage);
+
+ ApplySamplingFilterToBoundTexture(gl, aSamplingFilter, mTextureTarget);
+}
+
+bool EGLImageTextureSource::IsValid() const { return !!gl(); }
+
+gfx::Matrix4x4 EGLImageTextureSource::GetTextureTransform() {
+ gfx::Matrix4x4 ret;
+ return ret;
+}
+
+////////////////////////////////////////////////////////////////////////
+
+EGLImageTextureHost::EGLImageTextureHost(TextureFlags aFlags, EGLImage aImage,
+ EGLSync aSync, gfx::IntSize aSize,
+ bool hasAlpha)
+ : TextureHost(TextureHostType::EGLImage, aFlags),
+ mImage(aImage),
+ mSync(aSync),
+ mSize(aSize),
+ mHasAlpha(hasAlpha) {}
+
+EGLImageTextureHost::~EGLImageTextureHost() = default;
+
+gl::GLContext* EGLImageTextureHost::gl() const { return nullptr; }
+
+gfx::SurfaceFormat EGLImageTextureHost::GetFormat() const {
+ MOZ_ASSERT(mTextureSource);
+ return mTextureSource ? mTextureSource->GetFormat()
+ : gfx::SurfaceFormat::UNKNOWN;
+}
+
+void EGLImageTextureHost::CreateRenderTexture(
+ const wr::ExternalImageId& aExternalImageId) {
+ RefPtr<wr::RenderTextureHost> texture =
+ new wr::RenderEGLImageTextureHost(mImage, mSync, mSize);
+ wr::RenderThread::Get()->RegisterExternalImage(aExternalImageId,
+ texture.forget());
+}
+
+void EGLImageTextureHost::PushResourceUpdates(
+ wr::TransactionBuilder& aResources, ResourceUpdateOp aOp,
+ const Range<wr::ImageKey>& aImageKeys, const wr::ExternalImageId& aExtID) {
+ auto method = aOp == TextureHost::ADD_IMAGE
+ ? &wr::TransactionBuilder::AddExternalImage
+ : &wr::TransactionBuilder::UpdateExternalImage;
+ auto imageType = wr::ExternalImageType::TextureHandle(
+ wr::ImageBufferKind::TextureExternal);
+
+ gfx::SurfaceFormat format =
+ mHasAlpha ? gfx::SurfaceFormat::R8G8B8A8 : gfx::SurfaceFormat::R8G8B8X8;
+
+ MOZ_ASSERT(aImageKeys.length() == 1);
+ // XXX Add RGBA handling. Temporary hack to avoid crash
+ // With BGRA format setting, rendering works without problem.
+ auto formatTmp = format == gfx::SurfaceFormat::R8G8B8A8
+ ? gfx::SurfaceFormat::B8G8R8A8
+ : gfx::SurfaceFormat::B8G8R8X8;
+ wr::ImageDescriptor descriptor(GetSize(), formatTmp);
+ (aResources.*method)(aImageKeys[0], descriptor, aExtID, imageType, 0);
+}
+
+void EGLImageTextureHost::PushDisplayItems(
+ wr::DisplayListBuilder& aBuilder, const wr::LayoutRect& aBounds,
+ const wr::LayoutRect& aClip, wr::ImageRendering aFilter,
+ const Range<wr::ImageKey>& aImageKeys, PushDisplayItemFlagSet aFlags) {
+ MOZ_ASSERT(aImageKeys.length() == 1);
+ aBuilder.PushImage(
+ aBounds, aClip, true, false, aFilter, aImageKeys[0],
+ !(mFlags & TextureFlags::NON_PREMULTIPLIED),
+ wr::ColorF{1.0f, 1.0f, 1.0f, 1.0f},
+ aFlags.contains(PushDisplayItemFlag::PREFER_COMPOSITOR_SURFACE));
+}
+
+//
+
+GLTextureHost::GLTextureHost(TextureFlags aFlags, GLuint aTextureHandle,
+ GLenum aTarget, GLsync aSync, gfx::IntSize aSize,
+ bool aHasAlpha)
+ : TextureHost(TextureHostType::GLTexture, aFlags),
+ mTexture(aTextureHandle),
+ mTarget(aTarget),
+ mSync(aSync),
+ mSize(aSize),
+ mHasAlpha(aHasAlpha) {}
+
+GLTextureHost::~GLTextureHost() = default;
+
+gl::GLContext* GLTextureHost::gl() const { return nullptr; }
+
+gfx::SurfaceFormat GLTextureHost::GetFormat() const {
+ MOZ_ASSERT(mTextureSource);
+ return mTextureSource ? mTextureSource->GetFormat()
+ : gfx::SurfaceFormat::UNKNOWN;
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/opengl/TextureHostOGL.h b/gfx/layers/opengl/TextureHostOGL.h
new file mode 100644
index 0000000000..e8d0dc6355
--- /dev/null
+++ b/gfx/layers/opengl/TextureHostOGL.h
@@ -0,0 +1,652 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_GFX_TEXTUREOGL_H
+#define MOZILLA_GFX_TEXTUREOGL_H
+
+#include <stddef.h> // for size_t
+#include <stdint.h> // for uint64_t
+#include "CompositableHost.h"
+#include "GLContextTypes.h" // for GLContext
+#include "GLDefs.h" // for GLenum, LOCAL_GL_CLAMP_TO_EDGE, etc
+#include "GLTextureImage.h" // for TextureImage
+#include "gfxTypes.h"
+#include "mozilla/GfxMessageUtils.h" // for gfxContentType
+#include "mozilla/Assertions.h" // for MOZ_ASSERT, etc
+#include "mozilla/Attributes.h" // for override
+#include "mozilla/RefPtr.h" // for RefPtr
+#include "mozilla/gfx/Matrix.h" // for Matrix4x4
+#include "mozilla/gfx/Point.h" // for IntSize, IntPoint
+#include "mozilla/gfx/Types.h" // for SurfaceFormat, etc
+#include "mozilla/layers/CompositorOGL.h" // for CompositorOGL
+#include "mozilla/layers/CompositorTypes.h" // for TextureFlags
+#include "mozilla/layers/LayersSurfaces.h" // for SurfaceDescriptor
+#include "mozilla/layers/TextureHost.h" // for TextureHost, etc
+#include "mozilla/mozalloc.h" // for operator delete, etc
+#include "mozilla/webrender/RenderThread.h"
+#include "nsCOMPtr.h" // for already_AddRefed
+#include "nsDebug.h" // for NS_WARNING
+#include "nsISupportsImpl.h" // for TextureImage::Release, etc
+#include "nsRegionFwd.h" // for nsIntRegion
+
+#ifdef MOZ_WIDGET_ANDROID
+# include "AndroidSurfaceTexture.h"
+# include "mozilla/java/GeckoSurfaceTextureWrappers.h"
+#endif
+
+namespace mozilla {
+namespace gfx {
+class DataSourceSurface;
+} // namespace gfx
+
+namespace layers {
+
+class Compositor;
+class CompositorOGL;
+class AndroidHardwareBuffer;
+class SurfaceDescriptorAndroidHardwareBuffer;
+class TextureImageTextureSourceOGL;
+class GLTextureSource;
+
+void ApplySamplingFilterToBoundTexture(gl::GLContext* aGL,
+ gfx::SamplingFilter aSamplingFilter,
+ GLuint aTarget = LOCAL_GL_TEXTURE_2D);
+
+already_AddRefed<TextureHost> CreateTextureHostOGL(
+ const SurfaceDescriptor& aDesc, ISurfaceAllocator* aDeallocator,
+ LayersBackend aBackend, TextureFlags aFlags);
+
+/*
+ * TextureHost implementations for the OpenGL backend.
+ *
+ * Note that it is important to be careful about the ownership model with
+ * the OpenGL backend, due to some widget limitation on Linux: before
+ * the nsBaseWidget associated with our OpenGL context has been completely
+ * deleted, every resource belonging to the OpenGL context MUST have been
+ * released. At the moment the teardown sequence happens in the middle of
+ * the nsBaseWidget's destructor, meaning that at a given moment we must be
+ * able to easily find and release all the GL resources.
+ * The point is: be careful about the ownership model and limit the number
+ * of objects sharing references to GL resources to make the tear down
+ * sequence as simple as possible.
+ */
+
+/**
+ * TextureSourceOGL provides the necessary API for CompositorOGL to composite
+ * a TextureSource.
+ */
+class TextureSourceOGL {
+ public:
+ TextureSourceOGL()
+ : mCachedSamplingFilter(gfx::SamplingFilter::GOOD),
+ mHasCachedSamplingFilter(false) {}
+
+ virtual bool IsValid() const = 0;
+
+ virtual void BindTexture(GLenum aTextureUnit,
+ gfx::SamplingFilter aSamplingFilter) = 0;
+
+ // To be overridden in textures that need this. This method will be called
+ // when the compositor has used the texture to draw. This allows us to set
+ // a fence with glFenceSync which we can wait on later to ensure the GPU
+ // is done with the draw calls using that texture. We would like to be able
+ // to simply use glFinishObjectAPPLE, but this returns earlier than
+ // expected with nvidia drivers.
+ virtual void MaybeFenceTexture() {}
+
+ virtual gfx::IntSize GetSize() const = 0;
+
+ virtual GLenum GetTextureTarget() const { return LOCAL_GL_TEXTURE_2D; }
+
+ virtual gfx::SurfaceFormat GetFormat() const = 0;
+
+ virtual GLenum GetWrapMode() const = 0;
+
+ virtual gfx::Matrix4x4 GetTextureTransform() { return gfx::Matrix4x4(); }
+
+ virtual TextureImageTextureSourceOGL* AsTextureImageTextureSource() {
+ return nullptr;
+ }
+
+ virtual GLTextureSource* AsGLTextureSource() { return nullptr; }
+
+ void SetSamplingFilter(gl::GLContext* aGL,
+ gfx::SamplingFilter aSamplingFilter) {
+ if (mHasCachedSamplingFilter && mCachedSamplingFilter == aSamplingFilter) {
+ return;
+ }
+ mHasCachedSamplingFilter = true;
+ mCachedSamplingFilter = aSamplingFilter;
+ ApplySamplingFilterToBoundTexture(aGL, aSamplingFilter, GetTextureTarget());
+ }
+
+ void ClearCachedFilter() { mHasCachedSamplingFilter = false; }
+
+ private:
+ gfx::SamplingFilter mCachedSamplingFilter;
+ bool mHasCachedSamplingFilter;
+};
+
+/**
+ * A TextureSource backed by a TextureImage.
+ *
+ * Depending on the underlying TextureImage, may support texture tiling, so
+ * make sure to check AsBigImageIterator() and use the texture accordingly.
+ *
+ * This TextureSource can be used without a TextureHost and manage it's own
+ * GL texture(s).
+ */
+class TextureImageTextureSourceOGL final : public DataTextureSource,
+ public TextureSourceOGL,
+ public BigImageIterator {
+ public:
+ explicit TextureImageTextureSourceOGL(
+ CompositorOGL* aCompositor, TextureFlags aFlags = TextureFlags::DEFAULT);
+
+ const char* Name() const override { return "TextureImageTextureSourceOGL"; }
+ // DataTextureSource
+
+ bool Update(gfx::DataSourceSurface* aSurface,
+ nsIntRegion* aDestRegion = nullptr,
+ gfx::IntPoint* aSrcOffset = nullptr,
+ gfx::IntPoint* aDstOffset = nullptr) override;
+
+ void EnsureBuffer(const gfx::IntSize& aSize, gfxContentType aContentType);
+
+ TextureImageTextureSourceOGL* AsTextureImageTextureSource() override {
+ return this;
+ }
+
+ // TextureSource
+
+ void DeallocateDeviceData() override;
+
+ TextureSourceOGL* AsSourceOGL() override { return this; }
+
+ void BindTexture(GLenum aTextureUnit,
+ gfx::SamplingFilter aSamplingFilter) override;
+
+ gfx::IntSize GetSize() const override;
+
+ gfx::SurfaceFormat GetFormat() const override;
+
+ bool IsValid() const override { return !!mTexImage; }
+
+ GLenum GetWrapMode() const override { return mTexImage->GetWrapMode(); }
+
+ // BigImageIterator
+
+ BigImageIterator* AsBigImageIterator() override { return this; }
+
+ void BeginBigImageIteration() override {
+ mTexImage->BeginBigImageIteration();
+ mIterating = true;
+ }
+
+ void EndBigImageIteration() override { mIterating = false; }
+
+ gfx::IntRect GetTileRect() override;
+
+ size_t GetTileCount() override { return mTexImage->GetTileCount(); }
+
+ bool NextTile() override { return mTexImage->NextTile(); }
+
+ gl::GLContext* gl() const { return mGL; }
+
+ protected:
+ ~TextureImageTextureSourceOGL();
+
+ RefPtr<gl::TextureImage> mTexImage;
+ RefPtr<gl::GLContext> mGL;
+ RefPtr<CompositorOGL> mCompositor;
+ TextureFlags mFlags;
+ bool mIterating;
+};
+
+/**
+ * A texture source for GL textures.
+ *
+ * It does not own any GL texture, and attaches its shared handle to one of
+ * the compositor's temporary textures when binding.
+ *
+ * The shared texture handle is owned by the TextureHost.
+ */
+class GLTextureSource : public DataTextureSource, public TextureSourceOGL {
+ public:
+ GLTextureSource(TextureSourceProvider* aProvider, GLuint aTextureHandle,
+ GLenum aTarget, gfx::IntSize aSize,
+ gfx::SurfaceFormat aFormat);
+
+ GLTextureSource(gl::GLContext* aGL, GLuint aTextureHandle, GLenum aTarget,
+ gfx::IntSize aSize, gfx::SurfaceFormat aFormat);
+
+ virtual ~GLTextureSource();
+
+ const char* Name() const override { return "GLTextureSource"; }
+
+ GLTextureSource* AsGLTextureSource() override { return this; }
+
+ TextureSourceOGL* AsSourceOGL() override { return this; }
+
+ void BindTexture(GLenum activetex,
+ gfx::SamplingFilter aSamplingFilter) override;
+
+ bool IsValid() const override;
+
+ gfx::IntSize GetSize() const override { return mSize; }
+
+ gfx::SurfaceFormat GetFormat() const override { return mFormat; }
+
+ GLenum GetTextureTarget() const override { return mTextureTarget; }
+
+ GLenum GetWrapMode() const override { return LOCAL_GL_CLAMP_TO_EDGE; }
+
+ void DeallocateDeviceData() override;
+
+ void SetSize(gfx::IntSize aSize) { mSize = aSize; }
+
+ void SetFormat(gfx::SurfaceFormat aFormat) { mFormat = aFormat; }
+
+ GLuint GetTextureHandle() const { return mTextureHandle; }
+
+ gl::GLContext* gl() const { return mGL; }
+
+ bool Update(gfx::DataSourceSurface* aSurface,
+ nsIntRegion* aDestRegion = nullptr,
+ gfx::IntPoint* aSrcOffset = nullptr,
+ gfx::IntPoint* aDstOffset = nullptr) override {
+ return false;
+ }
+
+ protected:
+ void DeleteTextureHandle();
+
+ RefPtr<gl::GLContext> mGL;
+ RefPtr<CompositorOGL> mCompositor;
+ GLuint mTextureHandle;
+ GLenum mTextureTarget;
+ gfx::IntSize mSize;
+ gfx::SurfaceFormat mFormat;
+};
+
+// This texture source try to wrap "aSurface" in ctor for compositor direct
+// access. Since we can't know the timing for gpu buffer access, the surface
+// should be alive until the ~ClientStorageTextureSource(). And if we try to
+// update the surface we mapped before, we need to call Sync() to make sure
+// the surface is not used by compositor.
+class DirectMapTextureSource : public GLTextureSource {
+ public:
+ DirectMapTextureSource(gl::GLContext* aContext,
+ gfx::DataSourceSurface* aSurface);
+ DirectMapTextureSource(TextureSourceProvider* aProvider,
+ gfx::DataSourceSurface* aSurface);
+ ~DirectMapTextureSource();
+
+ bool Update(gfx::DataSourceSurface* aSurface,
+ nsIntRegion* aDestRegion = nullptr,
+ gfx::IntPoint* aSrcOffset = nullptr,
+ gfx::IntPoint* aDstOffset = nullptr) override;
+
+ // If aBlocking is false, check if this texture is no longer being used
+ // by the GPU - if aBlocking is true, this will block until the GPU is
+ // done with it.
+ bool Sync(bool aBlocking) override;
+
+ void MaybeFenceTexture() override;
+
+ private:
+ bool UpdateInternal(gfx::DataSourceSurface* aSurface,
+ nsIntRegion* aDestRegion, gfx::IntPoint* aSrcOffset,
+ bool aInit);
+
+ GLsync mSync;
+};
+
+class GLTextureHost : public TextureHost {
+ public:
+ GLTextureHost(TextureFlags aFlags, GLuint aTextureHandle, GLenum aTarget,
+ GLsync aSync, gfx::IntSize aSize, bool aHasAlpha);
+
+ virtual ~GLTextureHost();
+
+ // We don't own anything.
+ void DeallocateDeviceData() override {}
+
+ gfx::SurfaceFormat GetFormat() const override;
+
+ already_AddRefed<gfx::DataSourceSurface> GetAsSurface() override {
+ return nullptr; // XXX - implement this (for MOZ_DUMP_PAINTING)
+ }
+
+ gl::GLContext* gl() const;
+
+ gfx::IntSize GetSize() const override { return mSize; }
+
+ const char* Name() override { return "GLTextureHost"; }
+
+ protected:
+ const GLuint mTexture;
+ const GLenum mTarget;
+ GLsync mSync;
+ const gfx::IntSize mSize;
+ const bool mHasAlpha;
+ RefPtr<GLTextureSource> mTextureSource;
+};
+
+////////////////////////////////////////////////////////////////////////
+// SurfaceTexture
+
+#ifdef MOZ_WIDGET_ANDROID
+
+class SurfaceTextureSource : public TextureSource, public TextureSourceOGL {
+ public:
+ SurfaceTextureSource(TextureSourceProvider* aProvider,
+ java::GeckoSurfaceTexture::Ref& aSurfTex,
+ gfx::SurfaceFormat aFormat, GLenum aTarget,
+ GLenum aWrapMode, gfx::IntSize aSize,
+ Maybe<gfx::Matrix4x4> aTransformOverride);
+
+ const char* Name() const override { return "SurfaceTextureSource"; }
+
+ TextureSourceOGL* AsSourceOGL() override { return this; }
+
+ void BindTexture(GLenum activetex,
+ gfx::SamplingFilter aSamplingFilter) override;
+
+ bool IsValid() const override;
+
+ gfx::IntSize GetSize() const override { return mSize; }
+
+ gfx::SurfaceFormat GetFormat() const override { return mFormat; }
+
+ gfx::Matrix4x4 GetTextureTransform() override;
+
+ GLenum GetTextureTarget() const override { return mTextureTarget; }
+
+ GLenum GetWrapMode() const override { return mWrapMode; }
+
+ void DeallocateDeviceData() override;
+
+ gl::GLContext* gl() const { return mGL; }
+
+ protected:
+ RefPtr<gl::GLContext> mGL;
+ mozilla::java::GeckoSurfaceTexture::GlobalRef mSurfTex;
+ const gfx::SurfaceFormat mFormat;
+ const GLenum mTextureTarget;
+ const GLenum mWrapMode;
+ const gfx::IntSize mSize;
+ const Maybe<gfx::Matrix4x4> mTransformOverride;
+};
+
+class SurfaceTextureHost : public TextureHost {
+ public:
+ SurfaceTextureHost(TextureFlags aFlags,
+ mozilla::java::GeckoSurfaceTexture::Ref& aSurfTex,
+ gfx::IntSize aSize, gfx::SurfaceFormat aFormat,
+ bool aContinuousUpdate,
+ Maybe<gfx::Matrix4x4> aTransformOverride);
+
+ virtual ~SurfaceTextureHost();
+
+ void DeallocateDeviceData() override;
+
+ gfx::SurfaceFormat GetFormat() const override;
+
+ already_AddRefed<gfx::DataSourceSurface> GetAsSurface() override {
+ return nullptr; // XXX - implement this (for MOZ_DUMP_PAINTING)
+ }
+
+ gl::GLContext* gl() const;
+
+ gfx::IntSize GetSize() const override { return mSize; }
+
+ const char* Name() override { return "SurfaceTextureHost"; }
+
+ SurfaceTextureHost* AsSurfaceTextureHost() override { return this; }
+
+ bool IsWrappingSurfaceTextureHost() override { return true; }
+
+ void CreateRenderTexture(
+ const wr::ExternalImageId& aExternalImageId) override;
+
+ uint32_t NumSubTextures() override;
+
+ void PushResourceUpdates(wr::TransactionBuilder& aResources,
+ ResourceUpdateOp aOp,
+ const Range<wr::ImageKey>& aImageKeys,
+ const wr::ExternalImageId& aExtID) override;
+
+ void PushDisplayItems(wr::DisplayListBuilder& aBuilder,
+ const wr::LayoutRect& aBounds,
+ const wr::LayoutRect& aClip, wr::ImageRendering aFilter,
+ const Range<wr::ImageKey>& aImageKeys,
+ PushDisplayItemFlagSet aFlags) override;
+
+ bool SupportsExternalCompositing(WebRenderBackend aBackend) override;
+
+ // gecko does not need deferred deletion with WebRender
+ // GPU/hardware task end could be checked by android fence.
+ // SurfaceTexture uses android fence internally,
+ bool NeedsDeferredDeletion() const override { return false; }
+
+ protected:
+ mozilla::java::GeckoSurfaceTexture::GlobalRef mSurfTex;
+ const gfx::IntSize mSize;
+ const gfx::SurfaceFormat mFormat;
+ bool mContinuousUpdate;
+ const Maybe<gfx::Matrix4x4> mTransformOverride;
+ RefPtr<CompositorOGL> mCompositor;
+ RefPtr<SurfaceTextureSource> mTextureSource;
+};
+
+class AndroidHardwareBufferTextureSource : public TextureSource,
+ public TextureSourceOGL {
+ public:
+ AndroidHardwareBufferTextureSource(
+ TextureSourceProvider* aProvider,
+ AndroidHardwareBuffer* aAndroidHardwareBuffer, gfx::SurfaceFormat aFormat,
+ GLenum aTarget, GLenum aWrapMode, gfx::IntSize aSize);
+
+ const char* Name() const override { return "SurfaceTextureSource"; }
+
+ TextureSourceOGL* AsSourceOGL() override { return this; }
+
+ void BindTexture(GLenum activetex,
+ gfx::SamplingFilter aSamplingFilter) override;
+
+ bool IsValid() const override;
+
+ gfx::IntSize GetSize() const override { return mSize; }
+
+ gfx::SurfaceFormat GetFormat() const override { return mFormat; }
+
+ GLenum GetTextureTarget() const override { return mTextureTarget; }
+
+ GLenum GetWrapMode() const override { return mWrapMode; }
+
+ void DeallocateDeviceData() override;
+
+ gl::GLContext* gl() const { return mGL; }
+
+ protected:
+ virtual ~AndroidHardwareBufferTextureSource();
+
+ bool EnsureEGLImage();
+ void DestroyEGLImage();
+ void DeleteTextureHandle();
+
+ RefPtr<gl::GLContext> mGL;
+ RefPtr<AndroidHardwareBuffer> mAndroidHardwareBuffer;
+ const gfx::SurfaceFormat mFormat;
+ const GLenum mTextureTarget;
+ const GLenum mWrapMode;
+ const gfx::IntSize mSize;
+
+ EGLImage mEGLImage;
+ GLuint mTextureHandle;
+};
+
+class AndroidHardwareBufferTextureHost : public TextureHost {
+ public:
+ static already_AddRefed<AndroidHardwareBufferTextureHost> Create(
+ TextureFlags aFlags, const SurfaceDescriptorAndroidHardwareBuffer& aDesc);
+
+ AndroidHardwareBufferTextureHost(
+ TextureFlags aFlags, AndroidHardwareBuffer* aAndroidHardwareBuffer);
+
+ virtual ~AndroidHardwareBufferTextureHost();
+
+ void DeallocateDeviceData() override;
+
+ gfx::SurfaceFormat GetFormat() const override;
+
+ gfx::IntSize GetSize() const override;
+
+ void NotifyNotUsed() override;
+
+ already_AddRefed<gfx::DataSourceSurface> GetAsSurface() override {
+ return nullptr; // XXX - implement this (for MOZ_DUMP_PAINTING)
+ }
+
+ gl::GLContext* gl() const;
+
+ const char* Name() override { return "AndroidHardwareBufferTextureHost"; }
+
+ AndroidHardwareBufferTextureHost* AsAndroidHardwareBufferTextureHost()
+ override {
+ return this;
+ }
+
+ void CreateRenderTexture(
+ const wr::ExternalImageId& aExternalImageId) override;
+
+ uint32_t NumSubTextures() override;
+
+ void PushResourceUpdates(wr::TransactionBuilder& aResources,
+ ResourceUpdateOp aOp,
+ const Range<wr::ImageKey>& aImageKeys,
+ const wr::ExternalImageId& aExtID) override;
+
+ void PushDisplayItems(wr::DisplayListBuilder& aBuilder,
+ const wr::LayoutRect& aBounds,
+ const wr::LayoutRect& aClip, wr::ImageRendering aFilter,
+ const Range<wr::ImageKey>& aImageKeys,
+ PushDisplayItemFlagSet aFlags) override;
+
+ void SetAcquireFence(mozilla::ipc::FileDescriptor&& aFenceFd) override;
+
+ void SetReleaseFence(mozilla::ipc::FileDescriptor&& aFenceFd) override;
+
+ mozilla::ipc::FileDescriptor GetAndResetReleaseFence() override;
+
+ AndroidHardwareBuffer* GetAndroidHardwareBuffer() const override {
+ return mAndroidHardwareBuffer;
+ }
+
+ bool SupportsExternalCompositing(WebRenderBackend aBackend) override;
+
+ // gecko does not need deferred deletion with WebRender
+ // GPU/hardware task end could be checked by android fence.
+ bool NeedsDeferredDeletion() const override { return false; }
+
+ protected:
+ RefPtr<AndroidHardwareBuffer> mAndroidHardwareBuffer;
+};
+
+#endif // MOZ_WIDGET_ANDROID
+
+////////////////////////////////////////////////////////////////////////
+// EGLImage
+
+class EGLImageTextureSource : public TextureSource, public TextureSourceOGL {
+ public:
+ EGLImageTextureSource(TextureSourceProvider* aProvider, EGLImage aImage,
+ gfx::SurfaceFormat aFormat, GLenum aTarget,
+ GLenum aWrapMode, gfx::IntSize aSize);
+
+ const char* Name() const override { return "EGLImageTextureSource"; }
+
+ TextureSourceOGL* AsSourceOGL() override { return this; }
+
+ void BindTexture(GLenum activetex,
+ gfx::SamplingFilter aSamplingFilter) override;
+
+ bool IsValid() const override;
+
+ gfx::IntSize GetSize() const override { return mSize; }
+
+ gfx::SurfaceFormat GetFormat() const override { return mFormat; }
+
+ gfx::Matrix4x4 GetTextureTransform() override;
+
+ GLenum GetTextureTarget() const override { return mTextureTarget; }
+
+ GLenum GetWrapMode() const override { return mWrapMode; }
+
+ // We don't own anything.
+ void DeallocateDeviceData() override {}
+
+ gl::GLContext* gl() const { return mGL; }
+
+ protected:
+ RefPtr<gl::GLContext> mGL;
+ RefPtr<CompositorOGL> mCompositor;
+ const EGLImage mImage;
+ const gfx::SurfaceFormat mFormat;
+ const GLenum mTextureTarget;
+ const GLenum mWrapMode;
+ const gfx::IntSize mSize;
+};
+
+class EGLImageTextureHost final : public TextureHost {
+ public:
+ EGLImageTextureHost(TextureFlags aFlags, EGLImage aImage, EGLSync aSync,
+ gfx::IntSize aSize, bool hasAlpha);
+
+ virtual ~EGLImageTextureHost();
+
+ // We don't own anything.
+ void DeallocateDeviceData() override {}
+
+ gfx::SurfaceFormat GetFormat() const override;
+
+ already_AddRefed<gfx::DataSourceSurface> GetAsSurface() override {
+ return nullptr; // XXX - implement this (for MOZ_DUMP_PAINTING)
+ }
+
+ gl::GLContext* gl() const;
+
+ gfx::IntSize GetSize() const override { return mSize; }
+
+ const char* Name() override { return "EGLImageTextureHost"; }
+
+ void CreateRenderTexture(
+ const wr::ExternalImageId& aExternalImageId) override;
+
+ void PushResourceUpdates(wr::TransactionBuilder& aResources,
+ ResourceUpdateOp aOp,
+ const Range<wr::ImageKey>& aImageKeys,
+ const wr::ExternalImageId& aExtID) override;
+
+ void PushDisplayItems(wr::DisplayListBuilder& aBuilder,
+ const wr::LayoutRect& aBounds,
+ const wr::LayoutRect& aClip, wr::ImageRendering aFilter,
+ const Range<wr::ImageKey>& aImageKeys,
+ PushDisplayItemFlagSet aFlags) override;
+
+ protected:
+ const EGLImage mImage;
+ const EGLSync mSync;
+ const gfx::IntSize mSize;
+ const bool mHasAlpha;
+ RefPtr<EGLImageTextureSource> mTextureSource;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif /* MOZILLA_GFX_TEXTUREOGL_H */
diff --git a/gfx/layers/wr/AsyncImagePipelineManager.cpp b/gfx/layers/wr/AsyncImagePipelineManager.cpp
new file mode 100644
index 0000000000..4849b651a3
--- /dev/null
+++ b/gfx/layers/wr/AsyncImagePipelineManager.cpp
@@ -0,0 +1,771 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "AsyncImagePipelineManager.h"
+
+#include <algorithm>
+#include <iterator>
+
+#include "CompositableHost.h"
+#include "gfxEnv.h"
+#include "mozilla/gfx/gfxVars.h"
+#include "mozilla/layers/CompositorThread.h"
+#include "mozilla/layers/RemoteTextureHostWrapper.h"
+#include "mozilla/layers/SharedSurfacesParent.h"
+#include "mozilla/layers/WebRenderImageHost.h"
+#include "mozilla/layers/WebRenderTextureHost.h"
+#include "mozilla/webrender/RenderThread.h"
+#include "mozilla/webrender/WebRenderAPI.h"
+#include "mozilla/webrender/WebRenderTypes.h"
+
+#ifdef MOZ_WIDGET_ANDROID
+# include "mozilla/layers/TextureHostOGL.h"
+#endif
+
+namespace mozilla {
+namespace layers {
+
+AsyncImagePipelineManager::ForwardingExternalImage::~ForwardingExternalImage() {
+ DebugOnly<bool> released = SharedSurfacesParent::Release(mImageId);
+ MOZ_ASSERT(released);
+}
+
+AsyncImagePipelineManager::AsyncImagePipeline::AsyncImagePipeline(
+ wr::PipelineId aPipelineId, layers::WebRenderBackend aBackend)
+ : mInitialised(false),
+ mIsChanged(false),
+ mUseExternalImage(false),
+ mRotation(VideoInfo::Rotation::kDegree_0),
+ mFilter(wr::ImageRendering::Auto),
+ mMixBlendMode(wr::MixBlendMode::Normal),
+ mDLBuilder(aPipelineId, aBackend) {}
+
+AsyncImagePipelineManager::AsyncImagePipelineManager(
+ RefPtr<wr::WebRenderAPI>&& aApi, bool aUseCompositorWnd)
+ : mApi(aApi),
+ mUseCompositorWnd(aUseCompositorWnd),
+ mIdNamespace(mApi->GetNamespace()),
+ mUseTripleBuffering(mApi->GetUseTripleBuffering()),
+ mResourceId(0),
+ mAsyncImageEpoch{0},
+ mWillGenerateFrame(false),
+ mDestroyed(false),
+#ifdef XP_WIN
+ mUseWebRenderDCompVideoOverlayWin(
+ gfx::gfxVars::UseWebRenderDCompVideoOverlayWin()),
+#endif
+ mRenderSubmittedUpdatesLock("SubmittedUpdatesLock"),
+ mLastCompletedFrameId(0) {
+ MOZ_COUNT_CTOR(AsyncImagePipelineManager);
+}
+
+AsyncImagePipelineManager::~AsyncImagePipelineManager() {
+ MOZ_COUNT_DTOR(AsyncImagePipelineManager);
+}
+
+void AsyncImagePipelineManager::Destroy() {
+ MOZ_ASSERT(!mDestroyed);
+ mApi = nullptr;
+ mPipelineTexturesHolders.Clear();
+ mDestroyed = true;
+}
+
+/* static */
+wr::ExternalImageId AsyncImagePipelineManager::GetNextExternalImageId() {
+ static std::atomic<uint64_t> sCounter = 0;
+
+ uint64_t id = ++sCounter;
+ // Upper 32bit(namespace) needs to be 0.
+ // Namespace other than 0 might be used by others.
+ MOZ_RELEASE_ASSERT(id != UINT32_MAX);
+ return wr::ToExternalImageId(id);
+}
+
+void AsyncImagePipelineManager::SetWillGenerateFrame() {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+
+ mWillGenerateFrame = true;
+}
+
+bool AsyncImagePipelineManager::GetAndResetWillGenerateFrame() {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+
+ bool ret = mWillGenerateFrame;
+ mWillGenerateFrame = false;
+ return ret;
+}
+
+void AsyncImagePipelineManager::AddPipeline(const wr::PipelineId& aPipelineId,
+ WebRenderBridgeParent* aWrBridge) {
+ if (mDestroyed) {
+ return;
+ }
+
+ mPipelineTexturesHolders.WithEntryHandle(
+ wr::AsUint64(aPipelineId), [&](auto&& holder) {
+ if (holder) {
+ // This could happen during tab move between different windows.
+ // Previously removed holder could be still alive for waiting
+ // destroyed.
+ MOZ_ASSERT(holder.Data()->mDestroyedEpoch.isSome());
+ holder.Data()->mDestroyedEpoch = Nothing(); // Revive holder
+ holder.Data()->mWrBridge = aWrBridge;
+ return;
+ }
+
+ holder.Insert(MakeUnique<PipelineTexturesHolder>())->mWrBridge =
+ aWrBridge;
+ });
+}
+
+void AsyncImagePipelineManager::RemovePipeline(
+ const wr::PipelineId& aPipelineId, const wr::Epoch& aEpoch) {
+ if (mDestroyed) {
+ return;
+ }
+
+ PipelineTexturesHolder* holder =
+ mPipelineTexturesHolders.Get(wr::AsUint64(aPipelineId));
+ MOZ_ASSERT(holder);
+ if (!holder) {
+ return;
+ }
+ holder->mWrBridge = nullptr;
+ holder->mDestroyedEpoch = Some(aEpoch);
+}
+
+WebRenderBridgeParent* AsyncImagePipelineManager::GetWrBridge(
+ const wr::PipelineId& aPipelineId) {
+ if (mDestroyed) {
+ return nullptr;
+ }
+
+ PipelineTexturesHolder* holder =
+ mPipelineTexturesHolders.Get(wr::AsUint64(aPipelineId));
+ if (!holder) {
+ return nullptr;
+ }
+ if (holder->mWrBridge) {
+ MOZ_ASSERT(holder->mDestroyedEpoch.isNothing());
+ return holder->mWrBridge;
+ }
+
+ return nullptr;
+}
+
+void AsyncImagePipelineManager::AddAsyncImagePipeline(
+ const wr::PipelineId& aPipelineId, WebRenderImageHost* aImageHost) {
+ if (mDestroyed) {
+ return;
+ }
+ MOZ_ASSERT(aImageHost);
+ uint64_t id = wr::AsUint64(aPipelineId);
+
+ MOZ_ASSERT(!mAsyncImagePipelines.Contains(id));
+ auto holder =
+ MakeUnique<AsyncImagePipeline>(aPipelineId, mApi->GetBackendType());
+ holder->mImageHost = aImageHost;
+ mAsyncImagePipelines.InsertOrUpdate(id, std::move(holder));
+ AddPipeline(aPipelineId, /* aWrBridge */ nullptr);
+}
+
+void AsyncImagePipelineManager::RemoveAsyncImagePipeline(
+ const wr::PipelineId& aPipelineId, wr::TransactionBuilder& aTxn) {
+ if (mDestroyed) {
+ return;
+ }
+
+ uint64_t id = wr::AsUint64(aPipelineId);
+ if (auto entry = mAsyncImagePipelines.Lookup(id)) {
+ const auto& holder = entry.Data();
+ wr::Epoch epoch = GetNextImageEpoch();
+ aTxn.ClearDisplayList(epoch, aPipelineId);
+ for (wr::ImageKey key : holder->mKeys) {
+ aTxn.DeleteImage(key);
+ }
+ entry.Remove();
+ RemovePipeline(aPipelineId, epoch);
+ }
+}
+
+void AsyncImagePipelineManager::UpdateAsyncImagePipeline(
+ const wr::PipelineId& aPipelineId, const LayoutDeviceRect& aScBounds,
+ const VideoInfo::Rotation aRotation, const wr::ImageRendering& aFilter,
+ const wr::MixBlendMode& aMixBlendMode) {
+ if (mDestroyed) {
+ return;
+ }
+ AsyncImagePipeline* pipeline =
+ mAsyncImagePipelines.Get(wr::AsUint64(aPipelineId));
+ if (!pipeline) {
+ return;
+ }
+ pipeline->mInitialised = true;
+ pipeline->Update(aScBounds, aRotation, aFilter, aMixBlendMode);
+}
+
+Maybe<TextureHost::ResourceUpdateOp> AsyncImagePipelineManager::UpdateImageKeys(
+ const wr::Epoch& aEpoch, const wr::PipelineId& aPipelineId,
+ AsyncImagePipeline* aPipeline, nsTArray<wr::ImageKey>& aKeys,
+ wr::TransactionBuilder& aSceneBuilderTxn,
+ wr::TransactionBuilder& aMaybeFastTxn, RemoteTextureInfoList* aList) {
+ MOZ_ASSERT(aKeys.IsEmpty());
+ MOZ_ASSERT(aPipeline);
+
+ TextureHost* texture =
+ aPipeline->mImageHost->GetAsTextureHostForComposite(this);
+ TextureHost* previousTexture = aPipeline->mCurrentTexture.get();
+
+ if (texture == previousTexture) {
+ // The texture has not changed, just reuse previous ImageKeys.
+ aKeys = aPipeline->mKeys.Clone();
+ return Nothing();
+ }
+
+ if (!texture || texture->NumSubTextures() == 0) {
+ // We don't have a new texture or texture does not have SubTextures, there
+ // isn't much we can do.
+ aKeys = aPipeline->mKeys.Clone();
+ return Nothing();
+ }
+
+ // Check if pending Remote texture exists.
+ auto* wrapper = texture->AsRemoteTextureHostWrapper();
+ if (aList && wrapper && wrapper->IsReadyForRendering()) {
+ aList->mList.emplace(wrapper->mTextureId, wrapper->mOwnerId,
+ wrapper->mForPid);
+ }
+
+ aPipeline->mCurrentTexture = texture;
+
+ WebRenderTextureHost* wrTexture = texture->AsWebRenderTextureHost();
+ MOZ_ASSERT(wrTexture);
+ if (!wrTexture) {
+ gfxCriticalNote << "WebRenderTextureHost is not used";
+ }
+
+ bool useExternalImage = !!wrTexture;
+ aPipeline->mUseExternalImage = useExternalImage;
+
+ // The non-external image code path falls back to converting the texture into
+ // an rgb image.
+ auto numKeys = useExternalImage ? texture->NumSubTextures() : 1;
+ MOZ_ASSERT(numKeys > 0);
+
+ // If we already had a texture and the format hasn't changed, better to reuse
+ // the image keys than create new ones.
+ auto backend = aSceneBuilderTxn.GetBackendType();
+ bool canUpdate =
+ !!previousTexture &&
+ previousTexture->GetTextureHostType() == texture->GetTextureHostType() &&
+ previousTexture->GetSize() == texture->GetSize() &&
+ previousTexture->GetFormat() == texture->GetFormat() &&
+ previousTexture->GetColorDepth() == texture->GetColorDepth() &&
+ previousTexture->NeedsYFlip() == texture->NeedsYFlip() &&
+ previousTexture->SupportsExternalCompositing(backend) ==
+ texture->SupportsExternalCompositing(backend) &&
+ aPipeline->mKeys.Length() == numKeys;
+
+ if (!canUpdate) {
+ for (auto key : aPipeline->mKeys) {
+ // Destroy ImageKeys on transaction of scene builder thread, since
+ // DisplayList is updated on SceneBuilder thread. It prevents too early
+ // ImageKey deletion.
+ aSceneBuilderTxn.DeleteImage(key);
+ }
+ aPipeline->mKeys.Clear();
+ for (uint32_t i = 0; i < numKeys; ++i) {
+ aPipeline->mKeys.AppendElement(GenerateImageKey());
+ }
+ }
+
+ aKeys = aPipeline->mKeys.Clone();
+
+ auto op = canUpdate ? TextureHost::UPDATE_IMAGE : TextureHost::ADD_IMAGE;
+
+ if (!useExternalImage) {
+ return UpdateWithoutExternalImage(texture, aKeys[0], op, aMaybeFastTxn);
+ }
+
+ wrTexture->MaybeNotifyForUse(aMaybeFastTxn);
+
+ Range<wr::ImageKey> keys(&aKeys[0], aKeys.Length());
+ auto externalImageKey = wrTexture->GetExternalImageKey();
+ wrTexture->PushResourceUpdates(aMaybeFastTxn, op, keys, externalImageKey);
+
+ return Some(op);
+}
+
+Maybe<TextureHost::ResourceUpdateOp>
+AsyncImagePipelineManager::UpdateWithoutExternalImage(
+ TextureHost* aTexture, wr::ImageKey aKey, TextureHost::ResourceUpdateOp aOp,
+ wr::TransactionBuilder& aTxn) {
+ MOZ_ASSERT(aTexture);
+
+ RefPtr<gfx::DataSourceSurface> dSurf = aTexture->GetAsSurface();
+ if (!dSurf) {
+ NS_ERROR("TextureHost does not return DataSourceSurface");
+ return Nothing();
+ }
+ gfx::DataSourceSurface::MappedSurface map;
+ if (!dSurf->Map(gfx::DataSourceSurface::MapType::READ, &map)) {
+ NS_ERROR("DataSourceSurface failed to map");
+ return Nothing();
+ }
+
+ gfx::IntSize size = dSurf->GetSize();
+ wr::ImageDescriptor descriptor(size, map.mStride, dSurf->GetFormat());
+
+ // Costly copy right here...
+ wr::Vec<uint8_t> bytes;
+ bytes.PushBytes(Range<uint8_t>(map.mData, size.height * map.mStride));
+
+ if (aOp == TextureHost::UPDATE_IMAGE) {
+ aTxn.UpdateImageBuffer(aKey, descriptor, bytes);
+ } else {
+ aTxn.AddImage(aKey, descriptor, bytes);
+ }
+
+ dSurf->Unmap();
+
+ return Some(aOp);
+}
+
+void AsyncImagePipelineManager::ApplyAsyncImagesOfImageBridge(
+ wr::TransactionBuilder& aSceneBuilderTxn,
+ wr::TransactionBuilder& aFastTxn) {
+ if (mDestroyed || mAsyncImagePipelines.Count() == 0) {
+ return;
+ }
+
+#ifdef XP_WIN
+ // UseWebRenderDCompVideoOverlayWin() could be changed from true to false,
+ // when DCompVideoOverlay task is failed. In this case, DisplayItems need to
+ // be re-pushed to WebRender for disabling video overlay.
+ bool isChanged = mUseWebRenderDCompVideoOverlayWin !=
+ gfx::gfxVars::UseWebRenderDCompVideoOverlayWin();
+ if (isChanged) {
+ mUseWebRenderDCompVideoOverlayWin =
+ gfx::gfxVars::UseWebRenderDCompVideoOverlayWin();
+ }
+#endif
+
+ wr::Epoch epoch = GetNextImageEpoch();
+
+ // We use a pipeline with a very small display list for each video element.
+ // Update each of them if needed.
+ for (const auto& entry : mAsyncImagePipelines) {
+ wr::PipelineId pipelineId = wr::AsPipelineId(entry.GetKey());
+ AsyncImagePipeline* pipeline = entry.GetWeak();
+
+#ifdef XP_WIN
+ if (isChanged) {
+ pipeline->mIsChanged = true;
+ }
+#endif
+
+ // If aync image pipeline does not use ImageBridge, do not need to apply.
+ if (!pipeline->mImageHost->GetAsyncRef()) {
+ continue;
+ }
+ ApplyAsyncImageForPipeline(epoch, pipelineId, pipeline, aSceneBuilderTxn,
+ aFastTxn, /* aList */ nullptr);
+ }
+}
+
+wr::WrRotation ToWrRotation(VideoInfo::Rotation aRotation) {
+ switch (aRotation) {
+ case VideoInfo::Rotation::kDegree_0:
+ return wr::WrRotation::Degree0;
+ case VideoInfo::Rotation::kDegree_90:
+ return wr::WrRotation::Degree90;
+ case VideoInfo::Rotation::kDegree_180:
+ return wr::WrRotation::Degree180;
+ case VideoInfo::Rotation::kDegree_270:
+ return wr::WrRotation::Degree270;
+ }
+ return wr::WrRotation::Degree0;
+}
+
+void AsyncImagePipelineManager::ApplyAsyncImageForPipeline(
+ const wr::Epoch& aEpoch, const wr::PipelineId& aPipelineId,
+ AsyncImagePipeline* aPipeline, wr::TransactionBuilder& aSceneBuilderTxn,
+ wr::TransactionBuilder& aMaybeFastTxn, RemoteTextureInfoList* aList) {
+ nsTArray<wr::ImageKey> keys;
+ auto op = UpdateImageKeys(aEpoch, aPipelineId, aPipeline, keys,
+ aSceneBuilderTxn, aMaybeFastTxn, aList);
+
+ bool updateDisplayList =
+ aPipeline->mInitialised &&
+ (aPipeline->mIsChanged || op == Some(TextureHost::ADD_IMAGE)) &&
+ !!aPipeline->mCurrentTexture;
+
+ if (!updateDisplayList) {
+ // We don't need to update the display list, either because we can't or
+ // because the previous one is still up to date. We may, however, have
+ // updated some resources.
+
+ // Use transaction of scene builder thread to notify epoch.
+ // It is for making epoch update consistent.
+ aSceneBuilderTxn.UpdateEpoch(aPipelineId, aEpoch);
+ if (aPipeline->mCurrentTexture) {
+ HoldExternalImage(aPipelineId, aEpoch, aPipeline->mCurrentTexture);
+ }
+ return;
+ }
+
+ aPipeline->mIsChanged = false;
+ aPipeline->mDLBuilder.Begin();
+
+ float opacity = 1.0f;
+ wr::StackingContextParams params;
+ params.opacity = &opacity;
+ params.mix_blend_mode = aPipeline->mMixBlendMode;
+
+ wr::WrComputedTransformData computedTransform;
+ computedTransform.vertical_flip =
+ aPipeline->mCurrentTexture && aPipeline->mCurrentTexture->NeedsYFlip();
+ computedTransform.scale_from = {
+ float(aPipeline->mCurrentTexture->GetSize().width),
+ float(aPipeline->mCurrentTexture->GetSize().height)};
+ computedTransform.rotation = ToWrRotation(aPipeline->mRotation);
+ // We don't have a frame / per-frame key here, but we can use the pipeline id
+ // and the key kind to create a unique stable key.
+ computedTransform.key = wr::SpatialKey(
+ aPipelineId.mNamespace, aPipelineId.mHandle, wr::SpatialKeyKind::APZ);
+ params.computed_transform = &computedTransform;
+
+ Maybe<wr::WrSpatialId> referenceFrameId =
+ aPipeline->mDLBuilder.PushStackingContext(
+ params, wr::ToLayoutRect(aPipeline->mScBounds),
+ // This is fine to do unconditionally because we only push images
+ // here.
+ wr::RasterSpace::Screen());
+
+ Maybe<wr::SpaceAndClipChainHelper> spaceAndClipChainHelper;
+ if (referenceFrameId) {
+ spaceAndClipChainHelper.emplace(aPipeline->mDLBuilder,
+ referenceFrameId.ref());
+ }
+
+ if (aPipeline->mCurrentTexture && !keys.IsEmpty()) {
+ LayoutDeviceRect rect(0, 0, aPipeline->mCurrentTexture->GetSize().width,
+ aPipeline->mCurrentTexture->GetSize().height);
+
+ if (aPipeline->mUseExternalImage) {
+ MOZ_ASSERT(aPipeline->mCurrentTexture->AsWebRenderTextureHost());
+ Range<wr::ImageKey> range_keys(&keys[0], keys.Length());
+ TextureHost::PushDisplayItemFlagSet flags;
+ flags += TextureHost::PushDisplayItemFlag::PREFER_COMPOSITOR_SURFACE;
+ if (mApi->SupportsExternalBufferTextures()) {
+ flags +=
+ TextureHost::PushDisplayItemFlag::SUPPORTS_EXTERNAL_BUFFER_TEXTURES;
+ }
+ aPipeline->mCurrentTexture->PushDisplayItems(
+ aPipeline->mDLBuilder, wr::ToLayoutRect(rect), wr::ToLayoutRect(rect),
+ aPipeline->mFilter, range_keys, flags);
+ HoldExternalImage(aPipelineId, aEpoch, aPipeline->mCurrentTexture);
+ } else {
+ MOZ_ASSERT(keys.Length() == 1);
+ aPipeline->mDLBuilder.PushImage(wr::ToLayoutRect(rect),
+ wr::ToLayoutRect(rect), true, false,
+ aPipeline->mFilter, keys[0]);
+ }
+ }
+
+ spaceAndClipChainHelper.reset();
+ aPipeline->mDLBuilder.PopStackingContext(referenceFrameId.isSome());
+
+ wr::BuiltDisplayList dl;
+ aPipeline->mDLBuilder.End(dl);
+ aSceneBuilderTxn.SetDisplayList(aEpoch, aPipelineId, dl.dl_desc, dl.dl_items,
+ dl.dl_cache, dl.dl_spatial_tree);
+}
+
+void AsyncImagePipelineManager::ApplyAsyncImageForPipeline(
+ const wr::PipelineId& aPipelineId, wr::TransactionBuilder& aTxn,
+ wr::TransactionBuilder& aTxnForImageBridge, RemoteTextureInfoList* aList) {
+ AsyncImagePipeline* pipeline =
+ mAsyncImagePipelines.Get(wr::AsUint64(aPipelineId));
+ if (!pipeline) {
+ return;
+ }
+
+ // ready event of RemoteTexture that uses ImageBridge do not need to be
+ // checked here.
+ if (pipeline->mImageHost->GetAsyncRef()) {
+ aList = nullptr;
+ }
+
+ wr::TransactionBuilder fastTxn(mApi, /* aUseSceneBuilderThread */ false);
+ wr::AutoTransactionSender sender(mApi, &fastTxn);
+
+ // Transaction for async image pipeline that uses ImageBridge always need to
+ // be non low priority.
+ auto& sceneBuilderTxn =
+ pipeline->mImageHost->GetAsyncRef() ? aTxnForImageBridge : aTxn;
+
+ // Use transaction of using non scene builder thread when ImageHost uses
+ // ImageBridge. ApplyAsyncImagesOfImageBridge() handles transaction of adding
+ // and updating wr::ImageKeys of ImageHosts that uses ImageBridge. Then
+ // AsyncImagePipelineManager always needs to use non scene builder thread
+ // transaction for adding and updating wr::ImageKeys of ImageHosts that uses
+ // ImageBridge. Otherwise, ordering of wr::ImageKeys updating in webrender
+ // becomes inconsistent.
+ auto& maybeFastTxn = pipeline->mImageHost->GetAsyncRef() ? fastTxn : aTxn;
+
+ wr::Epoch epoch = GetNextImageEpoch();
+
+ ApplyAsyncImageForPipeline(epoch, aPipelineId, pipeline, sceneBuilderTxn,
+ maybeFastTxn, aList);
+}
+
+void AsyncImagePipelineManager::SetEmptyDisplayList(
+ const wr::PipelineId& aPipelineId, wr::TransactionBuilder& aTxn,
+ wr::TransactionBuilder& aTxnForImageBridge) {
+ AsyncImagePipeline* pipeline =
+ mAsyncImagePipelines.Get(wr::AsUint64(aPipelineId));
+ if (!pipeline) {
+ return;
+ }
+
+ // Transaction for async image pipeline that uses ImageBridge always need to
+ // be non low priority.
+ auto& txn = pipeline->mImageHost->GetAsyncRef() ? aTxnForImageBridge : aTxn;
+
+ wr::Epoch epoch = GetNextImageEpoch();
+ wr::DisplayListBuilder builder(aPipelineId, mApi->GetBackendType());
+ builder.Begin();
+
+ wr::BuiltDisplayList dl;
+ builder.End(dl);
+ txn.SetDisplayList(epoch, aPipelineId, dl.dl_desc, dl.dl_items, dl.dl_cache,
+ dl.dl_spatial_tree);
+}
+
+void AsyncImagePipelineManager::HoldExternalImage(
+ const wr::PipelineId& aPipelineId, const wr::Epoch& aEpoch,
+ TextureHost* aTexture) {
+ if (mDestroyed) {
+ return;
+ }
+ MOZ_ASSERT(aTexture);
+
+ PipelineTexturesHolder* holder =
+ mPipelineTexturesHolders.Get(wr::AsUint64(aPipelineId));
+ MOZ_ASSERT(holder);
+ if (!holder) {
+ return;
+ }
+ if (aTexture->NeedsDeferredDeletion()) {
+ // Hold WebRenderTextureHost until rendering completed.
+ holder->mTextureHostsUntilRenderCompleted.emplace_back(
+ MakeUnique<ForwardingTextureHost>(aEpoch, aTexture));
+ } else {
+ // Hold WebRenderTextureHost until submitted for rendering.
+ holder->mTextureHostsUntilRenderSubmitted.emplace_back(aEpoch, aTexture);
+ }
+}
+
+void AsyncImagePipelineManager::HoldExternalImage(
+ const wr::PipelineId& aPipelineId, const wr::Epoch& aEpoch,
+ const wr::ExternalImageId& aImageId) {
+ if (mDestroyed) {
+ SharedSurfacesParent::Release(aImageId);
+ return;
+ }
+
+ PipelineTexturesHolder* holder =
+ mPipelineTexturesHolders.Get(wr::AsUint64(aPipelineId));
+ MOZ_ASSERT(holder);
+ if (!holder) {
+ SharedSurfacesParent::Release(aImageId);
+ return;
+ }
+
+ holder->mExternalImages.emplace_back(
+ MakeUnique<ForwardingExternalImage>(aEpoch, aImageId));
+}
+
+void AsyncImagePipelineManager::NotifyPipelinesUpdated(
+ RefPtr<const wr::WebRenderPipelineInfo> aInfo,
+ wr::RenderedFrameId aLatestFrameId,
+ wr::RenderedFrameId aLastCompletedFrameId, ipc::FileDescriptor&& aFenceFd) {
+ MOZ_ASSERT(wr::RenderThread::IsInRenderThread());
+ MOZ_ASSERT(mLastCompletedFrameId <= aLastCompletedFrameId.mId);
+ MOZ_ASSERT(aLatestFrameId.IsValid());
+
+ mLastCompletedFrameId = aLastCompletedFrameId.mId;
+
+ {
+ // We need to lock for mRenderSubmittedUpdates because it can be accessed
+ // on the compositor thread.
+ MutexAutoLock lock(mRenderSubmittedUpdatesLock);
+
+ // Move the pending updates into the submitted ones.
+ mRenderSubmittedUpdates.emplace_back(
+ aLatestFrameId,
+ WebRenderPipelineInfoHolder(std::move(aInfo), std::move(aFenceFd)));
+ }
+
+ // Queue a runnable on the compositor thread to process the updates.
+ // This will also call CheckForTextureHostsNotUsedByGPU.
+ layers::CompositorThread()->Dispatch(
+ NewRunnableMethod("ProcessPipelineUpdates", this,
+ &AsyncImagePipelineManager::ProcessPipelineUpdates));
+}
+
+void AsyncImagePipelineManager::ProcessPipelineUpdates() {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+
+ if (mDestroyed) {
+ return;
+ }
+
+ std::vector<std::pair<wr::RenderedFrameId, WebRenderPipelineInfoHolder>>
+ submittedUpdates;
+ {
+ // We need to lock for mRenderSubmittedUpdates because it can be accessed on
+ // the compositor thread.
+ MutexAutoLock lock(mRenderSubmittedUpdatesLock);
+ mRenderSubmittedUpdates.swap(submittedUpdates);
+ }
+
+ // submittedUpdates is a vector of RenderedFrameIds paired with vectors of
+ // WebRenderPipelineInfo.
+ for (auto update : submittedUpdates) {
+ auto& holder = update.second;
+ const auto& info = holder.mInfo->Raw();
+
+ mReleaseFenceFd = std::move(holder.mFenceFd);
+
+ for (auto& epoch : info.epochs) {
+ ProcessPipelineRendered(epoch.pipeline_id, epoch.epoch, update.first);
+ }
+ for (auto& removedPipeline : info.removed_pipelines) {
+ ProcessPipelineRemoved(removedPipeline, update.first);
+ }
+ }
+ CheckForTextureHostsNotUsedByGPU();
+}
+
+void AsyncImagePipelineManager::ProcessPipelineRendered(
+ const wr::PipelineId& aPipelineId, const wr::Epoch& aEpoch,
+ wr::RenderedFrameId aRenderedFrameId) {
+ if (auto entry = mPipelineTexturesHolders.Lookup(wr::AsUint64(aPipelineId))) {
+ const auto& holder = entry.Data();
+ // For TextureHosts that can be released on render submission, using aEpoch
+ // find the first that we can't release and then release all prior to that.
+ auto firstSubmittedHostToKeep = std::find_if(
+ holder->mTextureHostsUntilRenderSubmitted.begin(),
+ holder->mTextureHostsUntilRenderSubmitted.end(),
+ [&aEpoch](const auto& entry) { return aEpoch <= entry.mEpoch; });
+#ifdef MOZ_WIDGET_ANDROID
+ // Set release fence if TextureHost owns AndroidHardwareBuffer.
+ // The TextureHost handled by mTextureHostsUntilRenderSubmitted instead of
+ // mTextureHostsUntilRenderCompleted, since android fence could be used
+ // to wait until its end of usage by GPU.
+ for (auto it = holder->mTextureHostsUntilRenderSubmitted.begin();
+ it != firstSubmittedHostToKeep; ++it) {
+ const auto& entry = it;
+ if (entry->mTexture->GetAndroidHardwareBuffer() &&
+ mReleaseFenceFd.IsValid()) {
+ ipc::FileDescriptor fenceFd = mReleaseFenceFd;
+ entry->mTexture->SetReleaseFence(std::move(fenceFd));
+ }
+ }
+#endif
+ holder->mTextureHostsUntilRenderSubmitted.erase(
+ holder->mTextureHostsUntilRenderSubmitted.begin(),
+ firstSubmittedHostToKeep);
+
+ // For TextureHosts that need to wait until render completed, find the first
+ // that is later than aEpoch and then move all prior to that to
+ // mTexturesInUseByGPU paired with aRenderedFrameId. These will be released
+ // once rendering has completed for aRenderedFrameId.
+ auto firstCompletedHostToKeep = std::find_if(
+ holder->mTextureHostsUntilRenderCompleted.begin(),
+ holder->mTextureHostsUntilRenderCompleted.end(),
+ [&aEpoch](const auto& entry) { return aEpoch <= entry->mEpoch; });
+ if (firstCompletedHostToKeep !=
+ holder->mTextureHostsUntilRenderCompleted.begin()) {
+ std::vector<UniquePtr<ForwardingTextureHost>> hostsUntilCompleted(
+ std::make_move_iterator(
+ holder->mTextureHostsUntilRenderCompleted.begin()),
+ std::make_move_iterator(firstCompletedHostToKeep));
+ mTexturesInUseByGPU.emplace_back(aRenderedFrameId,
+ std::move(hostsUntilCompleted));
+ holder->mTextureHostsUntilRenderCompleted.erase(
+ holder->mTextureHostsUntilRenderCompleted.begin(),
+ firstCompletedHostToKeep);
+ }
+
+ // Using aEpoch, find the first external image that we can't release and
+ // then release all prior to that.
+ auto firstImageToKeep = std::find_if(
+ holder->mExternalImages.begin(), holder->mExternalImages.end(),
+ [&aEpoch](const auto& entry) { return aEpoch <= entry->mEpoch; });
+ holder->mExternalImages.erase(holder->mExternalImages.begin(),
+ firstImageToKeep);
+ }
+}
+
+void AsyncImagePipelineManager::ProcessPipelineRemoved(
+ const wr::RemovedPipeline& aRemovedPipeline,
+ wr::RenderedFrameId aRenderedFrameId) {
+ if (mDestroyed) {
+ return;
+ }
+ if (auto entry = mPipelineTexturesHolders.Lookup(
+ wr::AsUint64(aRemovedPipeline.pipeline_id))) {
+ const auto& holder = entry.Data();
+ if (holder->mDestroyedEpoch.isSome()) {
+ if (!holder->mTextureHostsUntilRenderCompleted.empty()) {
+ // Move all TextureHosts that must be held until render completed to
+ // mTexturesInUseByGPU paired with aRenderedFrameId.
+ mTexturesInUseByGPU.emplace_back(
+ aRenderedFrameId,
+ std::move(holder->mTextureHostsUntilRenderCompleted));
+ }
+
+ // Remove Pipeline releasing all remaining TextureHosts and external
+ // images.
+ entry.Remove();
+ }
+
+ // If mDestroyedEpoch contains nothing it means we reused the same pipeline
+ // id (probably because we moved the tab to another window). In this case we
+ // need to keep the holder.
+ }
+}
+
+void AsyncImagePipelineManager::CheckForTextureHostsNotUsedByGPU() {
+ uint64_t lastCompletedFrameId = mLastCompletedFrameId;
+
+ // Find first entry after mLastCompletedFrameId and release all prior ones.
+ auto firstTexturesToKeep =
+ std::find_if(mTexturesInUseByGPU.begin(), mTexturesInUseByGPU.end(),
+ [lastCompletedFrameId](const auto& entry) {
+ return lastCompletedFrameId < entry.first.mId;
+ });
+ mTexturesInUseByGPU.erase(mTexturesInUseByGPU.begin(), firstTexturesToKeep);
+}
+
+wr::Epoch AsyncImagePipelineManager::GetNextImageEpoch() {
+ mAsyncImageEpoch.mHandle++;
+ return mAsyncImageEpoch;
+}
+
+AsyncImagePipelineManager::WebRenderPipelineInfoHolder::
+ WebRenderPipelineInfoHolder(RefPtr<const wr::WebRenderPipelineInfo>&& aInfo,
+ ipc::FileDescriptor&& aFenceFd)
+ : mInfo(aInfo), mFenceFd(aFenceFd) {}
+
+AsyncImagePipelineManager::WebRenderPipelineInfoHolder::
+ ~WebRenderPipelineInfoHolder() = default;
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/wr/AsyncImagePipelineManager.h b/gfx/layers/wr/AsyncImagePipelineManager.h
new file mode 100644
index 0000000000..1ed144c548
--- /dev/null
+++ b/gfx/layers/wr/AsyncImagePipelineManager.h
@@ -0,0 +1,281 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_GFX_WEBRENDERCOMPOSITABLE_HOLDER_H
+#define MOZILLA_GFX_WEBRENDERCOMPOSITABLE_HOLDER_H
+
+#include <vector>
+
+#include "CompositableHost.h"
+#include "mozilla/gfx/Point.h"
+#include "mozilla/ipc/FileDescriptor.h"
+#include "mozilla/layers/RemoteTextureMap.h"
+#include "mozilla/layers/TextureHost.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/webrender/WebRenderAPI.h"
+#include "mozilla/webrender/WebRenderTypes.h"
+#include "nsClassHashtable.h"
+
+namespace mozilla {
+
+namespace wr {
+class DisplayListBuilder;
+class WebRenderAPI;
+class WebRenderPipelineInfo;
+} // namespace wr
+
+namespace layers {
+
+class CompositableHost;
+class CompositorVsyncScheduler;
+class WebRenderImageHost;
+class WebRenderTextureHost;
+
+class AsyncImagePipelineManager final {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(AsyncImagePipelineManager)
+
+ explicit AsyncImagePipelineManager(RefPtr<wr::WebRenderAPI>&& aApi,
+ bool aUseCompositorWnd);
+
+ protected:
+ ~AsyncImagePipelineManager();
+
+ public:
+ void Destroy();
+
+ bool UseCompositorWnd() const { return mUseCompositorWnd; }
+
+ void AddPipeline(const wr::PipelineId& aPipelineId,
+ WebRenderBridgeParent* aWrBridge);
+ void RemovePipeline(const wr::PipelineId& aPipelineId,
+ const wr::Epoch& aEpoch);
+ WebRenderBridgeParent* GetWrBridge(const wr::PipelineId& aPipelineId);
+
+ void HoldExternalImage(const wr::PipelineId& aPipelineId,
+ const wr::Epoch& aEpoch, TextureHost* aTexture);
+ void HoldExternalImage(const wr::PipelineId& aPipelineId,
+ const wr::Epoch& aEpoch,
+ const wr::ExternalImageId& aImageId);
+
+ // This is called from the Renderer thread to notify this class about the
+ // pipelines in the most recently completed update.
+ // @param aInfo PipelineInfo for the update
+ // @param aLatestFrameId RenderedFrameId if a frame has been submitted for
+ // rendering, invalid if not
+ // @param aLastCompletedFrameId RenderedFrameId for the last completed frame
+ void NotifyPipelinesUpdated(RefPtr<const wr::WebRenderPipelineInfo> aInfo,
+ wr::RenderedFrameId aLatestFrameId,
+ wr::RenderedFrameId aLastCompletedFrameId,
+ ipc::FileDescriptor&& aFenceFd);
+
+ // This is run on the compositor thread to process mRenderSubmittedUpdates. We
+ // make this public because we need to invoke it from other places.
+ void ProcessPipelineUpdates();
+
+ TimeStamp GetCompositionTime() const { return mCompositionTime; }
+ CompositionOpportunityId GetCompositionOpportunityId() const {
+ return mCompositionOpportunityId;
+ }
+
+ void SetCompositionInfo(TimeStamp aTimeStamp,
+ CompositionOpportunityId aCompositionOpportunityId) {
+ mCompositionTime = aTimeStamp;
+ mCompositionOpportunityId = aCompositionOpportunityId;
+ if (!mCompositionTime.IsNull() && !mCompositeUntilTime.IsNull() &&
+ mCompositionTime >= mCompositeUntilTime) {
+ mCompositeUntilTime = TimeStamp();
+ }
+ }
+ void CompositeUntil(TimeStamp aTimeStamp) {
+ if (mCompositeUntilTime.IsNull() || mCompositeUntilTime < aTimeStamp) {
+ mCompositeUntilTime = aTimeStamp;
+ }
+ }
+ TimeStamp GetCompositeUntilTime() const { return mCompositeUntilTime; }
+
+ void AddAsyncImagePipeline(const wr::PipelineId& aPipelineId,
+ WebRenderImageHost* aImageHost);
+ void RemoveAsyncImagePipeline(const wr::PipelineId& aPipelineId,
+ wr::TransactionBuilder& aTxn);
+
+ void UpdateAsyncImagePipeline(const wr::PipelineId& aPipelineId,
+ const LayoutDeviceRect& aScBounds,
+ VideoInfo::Rotation aRotation,
+ const wr::ImageRendering& aFilter,
+ const wr::MixBlendMode& aMixBlendMode);
+ void ApplyAsyncImagesOfImageBridge(wr::TransactionBuilder& aSceneBuilderTxn,
+ wr::TransactionBuilder& aFastTxn);
+ void ApplyAsyncImageForPipeline(const wr::PipelineId& aPipelineId,
+ wr::TransactionBuilder& aTxn,
+ wr::TransactionBuilder& aTxnForImageBridge,
+ RemoteTextureInfoList* aList);
+
+ void SetEmptyDisplayList(const wr::PipelineId& aPipelineId,
+ wr::TransactionBuilder& aTxn,
+ wr::TransactionBuilder& aTxnForImageBridge);
+
+ void AppendImageCompositeNotification(
+ const ImageCompositeNotificationInfo& aNotification) {
+ mImageCompositeNotifications.AppendElement(aNotification);
+ }
+
+ void FlushImageNotifications(
+ nsTArray<ImageCompositeNotificationInfo>* aNotifications) {
+ aNotifications->AppendElements(std::move(mImageCompositeNotifications));
+ }
+
+ void SetWillGenerateFrame();
+ bool GetAndResetWillGenerateFrame();
+
+ static wr::ExternalImageId GetNextExternalImageId();
+
+ private:
+ void ProcessPipelineRendered(const wr::PipelineId& aPipelineId,
+ const wr::Epoch& aEpoch,
+ wr::RenderedFrameId aRenderedFrameId);
+ void ProcessPipelineRemoved(const wr::RemovedPipeline& aRemovedPipeline,
+ wr::RenderedFrameId aRenderedFrameId);
+
+ wr::Epoch GetNextImageEpoch();
+ uint32_t GetNextResourceId() { return ++mResourceId; }
+ wr::IdNamespace GetNamespace() { return mIdNamespace; }
+ wr::ImageKey GenerateImageKey() {
+ wr::ImageKey key;
+ key.mNamespace = GetNamespace();
+ key.mHandle = GetNextResourceId();
+ return key;
+ }
+
+ struct ForwardingTextureHost {
+ ForwardingTextureHost(const wr::Epoch& aEpoch, TextureHost* aTexture)
+ : mEpoch(aEpoch), mTexture(aTexture) {}
+ wr::Epoch mEpoch;
+ CompositableTextureHostRef mTexture;
+ };
+
+ struct ForwardingExternalImage {
+ ForwardingExternalImage(const wr::Epoch& aEpoch,
+ const wr::ExternalImageId& aImageId)
+ : mEpoch(aEpoch), mImageId(aImageId) {}
+ ~ForwardingExternalImage();
+ wr::Epoch mEpoch;
+ wr::ExternalImageId mImageId;
+ };
+
+ struct PipelineTexturesHolder {
+ // Holds forwarding WebRenderTextureHosts.
+ std::vector<ForwardingTextureHost> mTextureHostsUntilRenderSubmitted;
+ // TextureHosts that must be held until rendering has completed. UniquePtr
+ // is used to make the entries movable, ideally ForwardingTextureHost would
+ // be fully movable.
+ std::vector<UniquePtr<ForwardingTextureHost>>
+ mTextureHostsUntilRenderCompleted;
+ std::vector<UniquePtr<ForwardingExternalImage>> mExternalImages;
+ Maybe<wr::Epoch> mDestroyedEpoch;
+ WebRenderBridgeParent* MOZ_NON_OWNING_REF mWrBridge = nullptr;
+ };
+
+ struct AsyncImagePipeline {
+ AsyncImagePipeline(wr::PipelineId aPipelineId,
+ layers::WebRenderBackend aBackend);
+ void Update(const LayoutDeviceRect& aScBounds,
+ VideoInfo::Rotation aRotation,
+ const wr::ImageRendering& aFilter,
+ const wr::MixBlendMode& aMixBlendMode) {
+ mIsChanged |= !mScBounds.IsEqualEdges(aScBounds) ||
+ mRotation != aRotation || mFilter != aFilter ||
+ mMixBlendMode != aMixBlendMode;
+ mScBounds = aScBounds;
+ mRotation = aRotation;
+ mFilter = aFilter;
+ mMixBlendMode = aMixBlendMode;
+ }
+
+ bool mInitialised;
+ bool mIsChanged;
+ bool mUseExternalImage;
+ LayoutDeviceRect mScBounds;
+ VideoInfo::Rotation mRotation;
+ wr::ImageRendering mFilter;
+ wr::MixBlendMode mMixBlendMode;
+ RefPtr<WebRenderImageHost> mImageHost;
+ CompositableTextureHostRef mCurrentTexture;
+ nsTArray<wr::ImageKey> mKeys;
+ wr::DisplayListBuilder mDLBuilder;
+ };
+
+ void ApplyAsyncImageForPipeline(const wr::Epoch& aEpoch,
+ const wr::PipelineId& aPipelineId,
+ AsyncImagePipeline* aPipeline,
+ wr::TransactionBuilder& aSceneBuilderTxn,
+ wr::TransactionBuilder& aMaybeFastTxn,
+ RemoteTextureInfoList* aList);
+ Maybe<TextureHost::ResourceUpdateOp> UpdateImageKeys(
+ const wr::Epoch& aEpoch, const wr::PipelineId& aPipelineId,
+ AsyncImagePipeline* aPipeline, nsTArray<wr::ImageKey>& aKeys,
+ wr::TransactionBuilder& aSceneBuilderTxn,
+ wr::TransactionBuilder& aMaybeFastTxn, RemoteTextureInfoList* aList);
+ Maybe<TextureHost::ResourceUpdateOp> UpdateWithoutExternalImage(
+ TextureHost* aTexture, wr::ImageKey aKey, TextureHost::ResourceUpdateOp,
+ wr::TransactionBuilder& aTxn);
+
+ void CheckForTextureHostsNotUsedByGPU();
+
+ RefPtr<wr::WebRenderAPI> mApi;
+ bool mUseCompositorWnd;
+
+ const wr::IdNamespace mIdNamespace;
+ const bool mUseTripleBuffering;
+ uint32_t mResourceId;
+
+ nsClassHashtable<nsUint64HashKey, PipelineTexturesHolder>
+ mPipelineTexturesHolders;
+ nsClassHashtable<nsUint64HashKey, AsyncImagePipeline> mAsyncImagePipelines;
+ wr::Epoch mAsyncImageEpoch;
+ bool mWillGenerateFrame;
+ bool mDestroyed;
+
+#ifdef XP_WIN
+ bool mUseWebRenderDCompVideoOverlayWin;
+#endif
+
+ // Render time for the current composition.
+ TimeStamp mCompositionTime;
+
+ // CompositionOpportunityId of the current composition.
+ CompositionOpportunityId mCompositionOpportunityId;
+
+ // When nonnull, during rendering, some compositable indicated that it will
+ // change its rendering at this time. In order not to miss it, we composite
+ // on every vsync until this time occurs (this is the latest such time).
+ TimeStamp mCompositeUntilTime;
+
+ nsTArray<ImageCompositeNotificationInfo> mImageCompositeNotifications;
+
+ struct WebRenderPipelineInfoHolder {
+ WebRenderPipelineInfoHolder(RefPtr<const wr::WebRenderPipelineInfo>&& aInfo,
+ ipc::FileDescriptor&& aFenceFd);
+ ~WebRenderPipelineInfoHolder();
+ RefPtr<const wr::WebRenderPipelineInfo> mInfo;
+ ipc::FileDescriptor mFenceFd;
+ };
+
+ std::vector<std::pair<wr::RenderedFrameId, WebRenderPipelineInfoHolder>>
+ mRenderSubmittedUpdates;
+ Mutex mRenderSubmittedUpdatesLock MOZ_UNANNOTATED;
+
+ Atomic<uint64_t> mLastCompletedFrameId;
+ std::vector<std::pair<wr::RenderedFrameId,
+ std::vector<UniquePtr<ForwardingTextureHost>>>>
+ mTexturesInUseByGPU;
+ ipc::FileDescriptor mReleaseFenceFd;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif /* MOZILLA_GFX_WEBRENDERCOMPOSITABLE_HOLDER_H */
diff --git a/gfx/layers/wr/ClipManager.cpp b/gfx/layers/wr/ClipManager.cpp
new file mode 100644
index 0000000000..d201c85411
--- /dev/null
+++ b/gfx/layers/wr/ClipManager.cpp
@@ -0,0 +1,501 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/layers/ClipManager.h"
+
+#include "DisplayItemClipChain.h"
+#include "FrameMetrics.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/layers/StackingContextHelper.h"
+#include "mozilla/layers/WebRenderLayerManager.h"
+#include "mozilla/webrender/WebRenderAPI.h"
+#include "nsDisplayList.h"
+#include "nsRefreshDriver.h"
+#include "nsStyleStructInlines.h"
+#include "UnitTransforms.h"
+
+// clang-format off
+#define CLIP_LOG(...)
+//#define CLIP_LOG(s_, ...) printf_stderr("CLIP(%s): " s_, __func__, ## __VA_ARGS__)
+//#define CLIP_LOG(s_, ...) if (XRE_IsContentProcess()) printf_stderr("CLIP(%s): " s_, __func__, ## __VA_ARGS__)
+// clang-format on
+
+namespace mozilla {
+namespace layers {
+
+ClipManager::ClipManager() : mManager(nullptr), mBuilder(nullptr) {}
+
+void ClipManager::BeginBuild(WebRenderLayerManager* aManager,
+ wr::DisplayListBuilder& aBuilder) {
+ MOZ_ASSERT(!mManager);
+ mManager = aManager;
+ MOZ_ASSERT(!mBuilder);
+ mBuilder = &aBuilder;
+ MOZ_ASSERT(mCacheStack.empty());
+ mCacheStack.emplace();
+ MOZ_ASSERT(mASROverride.empty());
+ MOZ_ASSERT(mItemClipStack.empty());
+}
+
+void ClipManager::EndBuild() {
+ mBuilder = nullptr;
+ mManager = nullptr;
+ mCacheStack.pop();
+ MOZ_ASSERT(mCacheStack.empty());
+ MOZ_ASSERT(mASROverride.empty());
+ MOZ_ASSERT(mItemClipStack.empty());
+}
+
+void ClipManager::BeginList(const StackingContextHelper& aStackingContext) {
+ CLIP_LOG("begin list %p affects = %d, ref-frame = %d\n", &aStackingContext,
+ aStackingContext.AffectsClipPositioning(),
+ aStackingContext.ReferenceFrameId().isSome());
+
+ ItemClips clips(nullptr, nullptr, 0, false);
+ if (!mItemClipStack.empty()) {
+ clips = mItemClipStack.top();
+ }
+
+ if (aStackingContext.AffectsClipPositioning()) {
+ if (auto referenceFrameId = aStackingContext.ReferenceFrameId()) {
+ PushOverrideForASR(clips.mASR, *referenceFrameId);
+ clips.mScrollId = *referenceFrameId;
+ } else {
+ // Start a new cache
+ mCacheStack.emplace();
+ }
+ if (clips.mChain) {
+ clips.mClipChainId =
+ DefineClipChain(clips.mChain, clips.mAppUnitsPerDevPixel);
+ }
+ }
+
+ CLIP_LOG(" push: clip: %p, asr: %p, scroll = %zu, clip = %zu\n",
+ clips.mChain, clips.mASR, clips.mScrollId.id,
+ clips.mClipChainId.valueOr(wr::WrClipChainId{0}).id);
+
+ mItemClipStack.push(clips);
+}
+
+void ClipManager::EndList(const StackingContextHelper& aStackingContext) {
+ MOZ_ASSERT(!mItemClipStack.empty());
+
+ CLIP_LOG("end list %p\n", &aStackingContext);
+
+ mBuilder->SetClipChainLeaf(Nothing());
+ mItemClipStack.pop();
+
+ if (aStackingContext.AffectsClipPositioning()) {
+ if (aStackingContext.ReferenceFrameId()) {
+ PopOverrideForASR(mItemClipStack.empty() ? nullptr
+ : mItemClipStack.top().mASR);
+ } else {
+ MOZ_ASSERT(!mCacheStack.empty());
+ mCacheStack.pop();
+ }
+ }
+}
+
+void ClipManager::PushOverrideForASR(const ActiveScrolledRoot* aASR,
+ const wr::WrSpatialId& aSpatialId) {
+ wr::WrSpatialId space = GetScrollLayer(aASR);
+
+ CLIP_LOG("Pushing %p override %zu -> %zu\n", aASR, space.id, aSpatialId.id);
+ auto it = mASROverride.insert({space, std::stack<wr::WrSpatialId>()});
+ it.first->second.push(aSpatialId);
+
+ // Start a new cache
+ mCacheStack.emplace();
+
+ // Fix up our cached item clip if needed.
+ if (!mItemClipStack.empty()) {
+ auto& top = mItemClipStack.top();
+ if (top.mASR == aASR) {
+ top.mScrollId = aSpatialId;
+ if (top.mChain) {
+ top.mClipChainId =
+ DefineClipChain(top.mChain, top.mAppUnitsPerDevPixel);
+ }
+ }
+ }
+}
+
+void ClipManager::PopOverrideForASR(const ActiveScrolledRoot* aASR) {
+ MOZ_ASSERT(!mCacheStack.empty());
+ mCacheStack.pop();
+
+ wr::WrSpatialId space = GetScrollLayer(aASR);
+ auto it = mASROverride.find(space);
+ if (it == mASROverride.end()) {
+ MOZ_ASSERT_UNREACHABLE("Push/PopOverrideForASR should be balanced");
+ } else {
+ CLIP_LOG("Popping %p override %zu -> %zu\n", aASR, space.id,
+ it->second.top().id);
+ it->second.pop();
+ }
+
+ if (!mItemClipStack.empty()) {
+ auto& top = mItemClipStack.top();
+ if (top.mASR == aASR) {
+ top.mScrollId = (it == mASROverride.end() || it->second.empty())
+ ? space
+ : it->second.top();
+ if (top.mChain) {
+ top.mClipChainId =
+ DefineClipChain(top.mChain, top.mAppUnitsPerDevPixel);
+ }
+ }
+ }
+
+ if (it != mASROverride.end() && it->second.empty()) {
+ mASROverride.erase(it);
+ }
+}
+
+wr::WrSpatialId ClipManager::SpatialIdAfterOverride(
+ const wr::WrSpatialId& aSpatialId) {
+ auto it = mASROverride.find(aSpatialId);
+ if (it == mASROverride.end()) {
+ return aSpatialId;
+ }
+ MOZ_ASSERT(!it->second.empty());
+ CLIP_LOG("Overriding %zu with %zu\n", aSpatialId.id, it->second.top().id);
+ return it->second.top();
+}
+
+wr::WrSpaceAndClipChain ClipManager::SwitchItem(nsDisplayListBuilder* aBuilder,
+ nsDisplayItem* aItem) {
+ const DisplayItemClipChain* clip = aItem->GetClipChain();
+ const DisplayItemClipChain* inheritedClipChain =
+ mBuilder->GetInheritedClipChain();
+ if (inheritedClipChain && inheritedClipChain != clip) {
+ if (!clip) {
+ clip = mBuilder->GetInheritedClipChain();
+ } else {
+ clip = aBuilder->CreateClipChainIntersection(
+ mBuilder->GetInheritedClipChain(), clip);
+ }
+ }
+ const ActiveScrolledRoot* asr = aItem->GetActiveScrolledRoot();
+ DisplayItemType type = aItem->GetType();
+ if (type == DisplayItemType::TYPE_STICKY_POSITION) {
+ // For sticky position items, the ASR is computed differently depending on
+ // whether the item has a fixed descendant or not. But for WebRender
+ // purposes we always want to use the ASR that would have been used if it
+ // didn't have fixed descendants, which is stored as the "container ASR" on
+ // the sticky item.
+ auto* sticky = static_cast<nsDisplayStickyPosition*>(aItem);
+ asr = sticky->GetContainerASR();
+
+ // If the leafmost clip for the sticky item is just the displayport clip,
+ // then skip it. This allows sticky items to remain visible even if the
+ // rest of the content in the enclosing scrollframe is checkerboarding.
+ if (sticky->IsClippedToDisplayPort() && clip && clip->mASR == asr) {
+ clip = clip->mParent;
+ }
+ }
+
+ CLIP_LOG("processing item %p (%s) asr %p clip %p, inherited = %p\n", aItem,
+ DisplayItemTypeName(aItem->GetType()), asr, clip,
+ inheritedClipChain);
+
+ // In most cases we can combine the leaf of the clip chain with the clip rect
+ // of the display item. This reduces the number of clip items, which avoids
+ // some overhead further down the pipeline.
+ bool separateLeaf = false;
+ if (clip && clip->mASR == asr && clip->mClip.GetRoundedRectCount() == 0) {
+ // Container display items are not currently supported because the clip
+ // rect of a stacking context is not handled the same as normal display
+ // items.
+ separateLeaf = !aItem->GetChildren();
+ }
+
+ // Zoom display items report their bounds etc using the parent document's
+ // APD because zoom items act as a conversion layer between the two different
+ // APDs.
+ const int32_t auPerDevPixel = [&] {
+ if (type == DisplayItemType::TYPE_ZOOM) {
+ return static_cast<nsDisplayZoom*>(aItem)->GetParentAppUnitsPerDevPixel();
+ }
+ return aItem->Frame()->PresContext()->AppUnitsPerDevPixel();
+ }();
+
+ ItemClips clips(asr, clip, auPerDevPixel, separateLeaf);
+ MOZ_ASSERT(!mItemClipStack.empty());
+ if (clips.HasSameInputs(mItemClipStack.top())) {
+ // Early-exit because if the clips are the same as aItem's previous sibling,
+ // then we don't need to do do the work of popping the old stuff and then
+ // pushing it right back on for the new item. Note that if aItem doesn't
+ // have a previous sibling, that means BeginList would have been called
+ // just before this, which will have pushed a ItemClips(nullptr, nullptr)
+ // onto mItemClipStack, so the HasSameInputs check should return false.
+ CLIP_LOG("\tearly-exit for %p\n", aItem);
+ return mItemClipStack.top().GetSpaceAndClipChain();
+ }
+
+ // Pop aItem's previous sibling's stuff from mBuilder in preparation for
+ // pushing aItem's stuff.
+ mItemClipStack.pop();
+
+ // If the leaf of the clip chain is going to be merged with the display item's
+ // clip rect, then we should create a clip chain id from the leaf's parent.
+ if (separateLeaf) {
+ CLIP_LOG("\tseparate leaf detected, ignoring the last clip\n");
+ clip = clip->mParent;
+ }
+
+ // There are two ASR chains here that we need to be fully defined. One is the
+ // ASR chain pointed to by |asr|. The other is the
+ // ASR chain pointed to by clip->mASR. We pick the leafmost
+ // of these two chains because that one will include the other. Calling
+ // DefineScrollLayers with this leafmost ASR will recursively define all the
+ // ASRs that we care about for this item, but will not actually push
+ // anything onto the WR stack.
+ const ActiveScrolledRoot* leafmostASR = asr;
+ if (clip) {
+ leafmostASR = ActiveScrolledRoot::PickDescendant(leafmostASR, clip->mASR);
+ }
+ Maybe<wr::WrSpatialId> leafmostId = DefineScrollLayers(leafmostASR, aItem);
+ Unused << leafmostId;
+
+ // Define all the clips in the item's clip chain, and obtain a clip chain id
+ // for it.
+ clips.mClipChainId = DefineClipChain(clip, auPerDevPixel);
+
+ wr::WrSpatialId space = GetScrollLayer(asr);
+ clips.mScrollId = SpatialIdAfterOverride(space);
+ CLIP_LOG("\tassigning %d -> %d\n", (int)space.id, (int)clips.mScrollId.id);
+
+ // Now that we have the scroll id and a clip id for the item, push it onto
+ // the WR stack.
+ clips.UpdateSeparateLeaf(*mBuilder, auPerDevPixel);
+ auto spaceAndClipChain = clips.GetSpaceAndClipChain();
+
+ CLIP_LOG(" push: clip: %p, asr: %p, scroll = %zu, clip = %zu\n",
+ clips.mChain, clips.mASR, clips.mScrollId.id,
+ clips.mClipChainId.valueOr(wr::WrClipChainId{0}).id);
+
+ mItemClipStack.push(clips);
+
+ CLIP_LOG("done setup for %p\n", aItem);
+ return spaceAndClipChain;
+}
+
+wr::WrSpatialId ClipManager::GetScrollLayer(const ActiveScrolledRoot* aASR) {
+ for (const ActiveScrolledRoot* asr = aASR; asr; asr = asr->mParent) {
+ Maybe<wr::WrSpatialId> space =
+ mBuilder->GetScrollIdForDefinedScrollLayer(asr->GetViewId());
+ if (space) {
+ return *space;
+ }
+
+ // If this ASR doesn't have a scroll ID, then we should check its ancestor.
+ // There may not be one defined because the ASR may not be scrollable or we
+ // failed to get the scroll metadata.
+ }
+
+ Maybe<wr::WrSpatialId> space = mBuilder->GetScrollIdForDefinedScrollLayer(
+ ScrollableLayerGuid::NULL_SCROLL_ID);
+ MOZ_ASSERT(space.isSome());
+ return *space;
+}
+
+Maybe<wr::WrSpatialId> ClipManager::DefineScrollLayers(
+ const ActiveScrolledRoot* aASR, nsDisplayItem* aItem) {
+ if (!aASR) {
+ // Recursion base case
+ return Nothing();
+ }
+ ScrollableLayerGuid::ViewID viewId = aASR->GetViewId();
+ Maybe<wr::WrSpatialId> space =
+ mBuilder->GetScrollIdForDefinedScrollLayer(viewId);
+ if (space) {
+ // If we've already defined this scroll layer before, we can early-exit
+ return space;
+ }
+ // Recurse to define the ancestors
+ Maybe<wr::WrSpatialId> ancestorSpace =
+ DefineScrollLayers(aASR->mParent, aItem);
+
+ Maybe<ScrollMetadata> metadata =
+ aASR->mScrollableFrame->ComputeScrollMetadata(mManager, aItem->Frame(),
+ aItem->ToReferenceFrame());
+ if (!metadata) {
+ MOZ_ASSERT_UNREACHABLE("Expected scroll metadata to be available!");
+ return ancestorSpace;
+ }
+
+ FrameMetrics& metrics = metadata->GetMetrics();
+ if (!metrics.IsScrollable()) {
+ // This item is a scrolling no-op, skip over it in the ASR chain.
+ return ancestorSpace;
+ }
+
+ nsIScrollableFrame* scrollableFrame = aASR->mScrollableFrame;
+ nsIFrame* scrollFrame = do_QueryFrame(scrollableFrame);
+ nsPoint offset = scrollFrame->GetOffsetToCrossDoc(aItem->Frame()) +
+ aItem->ToReferenceFrame();
+ int32_t auPerDevPixel = aItem->Frame()->PresContext()->AppUnitsPerDevPixel();
+ nsRect scrollPort = scrollableFrame->GetScrollPortRect() + offset;
+ LayoutDeviceRect clipBounds =
+ LayoutDeviceRect::FromAppUnits(scrollPort, auPerDevPixel);
+
+ // The content rect that we hand to PushScrollLayer should be relative to
+ // the same origin as the clipBounds that we hand to PushScrollLayer -
+ // that is, both of them should be relative to the stacking context `aSc`.
+ // However, when we get the scrollable rect from the FrameMetrics, the
+ // origin has nothing to do with the position of the frame but instead
+ // represents the minimum allowed scroll offset of the scrollable content.
+ // While APZ uses this to clamp the scroll position, we don't need to send
+ // this to WebRender at all. Instead, we take the position from the
+ // composition bounds.
+ LayoutDeviceRect contentRect =
+ metrics.GetExpandedScrollableRect() * metrics.GetDevPixelsPerCSSPixel();
+ contentRect.MoveTo(clipBounds.TopLeft());
+
+ Maybe<wr::WrSpatialId> parent = ancestorSpace;
+ if (parent) {
+ *parent = SpatialIdAfterOverride(*parent);
+ }
+ // The external scroll offset is accumulated into the local space positions of
+ // display items inside WR, so that the elements hash (intern) to the same
+ // content ID for quick comparisons. To avoid invalidations when the
+ // auPerDevPixel is not a round value, round here directly from app units.
+ // This guarantees we won't introduce any inaccuracy in the external scroll
+ // offset passed to WR.
+ const bool useRoundedOffset =
+ StaticPrefs::apz_rounded_external_scroll_offset();
+ LayoutDevicePoint scrollOffset =
+ useRoundedOffset
+ ? LayoutDevicePoint::FromAppUnitsRounded(
+ scrollableFrame->GetScrollPosition(), auPerDevPixel)
+ : LayoutDevicePoint::FromAppUnits(
+ scrollableFrame->GetScrollPosition(), auPerDevPixel);
+
+ // Currently we track scroll-linked effects at the granularity of documents,
+ // not scroll frames, so we consider a scroll frame to have a scroll-linked
+ // effect whenever its containing document does.
+ nsPresContext* presContext = aItem->Frame()->PresContext();
+ const bool hasScrollLinkedEffect =
+ !StaticPrefs::apz_disable_for_scroll_linked_effects() &&
+ presContext->Document()->HasScrollLinkedEffect();
+
+ return Some(mBuilder->DefineScrollLayer(
+ viewId, parent, wr::ToLayoutRect(contentRect),
+ wr::ToLayoutRect(clipBounds), wr::ToLayoutVector2D(scrollOffset),
+ wr::ToWrAPZScrollGeneration(scrollableFrame->ScrollGenerationOnApz()),
+ wr::ToWrHasScrollLinkedEffect(hasScrollLinkedEffect),
+ wr::SpatialKey(uint64_t(scrollFrame), 0, wr::SpatialKeyKind::Scroll)));
+}
+
+Maybe<wr::WrClipChainId> ClipManager::DefineClipChain(
+ const DisplayItemClipChain* aChain, int32_t aAppUnitsPerDevPixel) {
+ MOZ_ASSERT(!mCacheStack.empty());
+ AutoTArray<wr::WrClipId, 6> allClipIds;
+ ClipIdMap& cache = mCacheStack.top();
+ // Iterate through the clips in the current item's clip chain, define them
+ // in WR, and put their IDs into |clipIds|.
+ for (const DisplayItemClipChain* chain = aChain; chain;
+ chain = chain->mParent) {
+ if (!chain->mClip.HasClip()) {
+ // This item in the chain is a no-op, skip over it
+ continue;
+ }
+
+ auto emplaceResult = cache.try_emplace(chain);
+ auto& chainClipIds = emplaceResult.first->second;
+ if (!emplaceResult.second) {
+ // Found it in the currently-active cache, so just use the id we have for
+ // it.
+ CLIP_LOG("cache[%p] => hit\n", chain);
+ allClipIds.AppendElements(chainClipIds);
+ continue;
+ }
+
+ LayoutDeviceRect clip = LayoutDeviceRect::FromAppUnits(
+ chain->mClip.GetClipRect(), aAppUnitsPerDevPixel);
+ AutoTArray<wr::ComplexClipRegion, 6> wrRoundedRects;
+ chain->mClip.ToComplexClipRegions(aAppUnitsPerDevPixel, wrRoundedRects);
+
+ wr::WrSpatialId space = GetScrollLayer(chain->mASR);
+ // Define the clip
+ space = SpatialIdAfterOverride(space);
+
+ auto rectClipId =
+ mBuilder->DefineRectClip(Some(space), wr::ToLayoutRect(clip));
+ CLIP_LOG("cache[%p] <= %zu\n", chain, rectClipId.id);
+ chainClipIds.AppendElement(rectClipId);
+
+ for (const auto& complexClip : wrRoundedRects) {
+ auto complexClipId =
+ mBuilder->DefineRoundedRectClip(Some(space), complexClip);
+ CLIP_LOG("cache[%p] <= %zu\n", chain, complexClipId.id);
+ chainClipIds.AppendElement(complexClipId);
+ }
+
+ allClipIds.AppendElements(chainClipIds);
+ }
+
+ if (allClipIds.IsEmpty()) {
+ return Nothing();
+ }
+
+ return Some(mBuilder->DefineClipChain(allClipIds));
+}
+
+ClipManager::~ClipManager() {
+ MOZ_ASSERT(!mBuilder);
+ MOZ_ASSERT(mCacheStack.empty());
+ MOZ_ASSERT(mItemClipStack.empty());
+}
+
+ClipManager::ItemClips::ItemClips(const ActiveScrolledRoot* aASR,
+ const DisplayItemClipChain* aChain,
+ int32_t aAppUnitsPerDevPixel,
+ bool aSeparateLeaf)
+ : mASR(aASR),
+ mChain(aChain),
+ mAppUnitsPerDevPixel(aAppUnitsPerDevPixel),
+ mSeparateLeaf(aSeparateLeaf) {
+ mScrollId = wr::wr_root_scroll_node_id();
+}
+
+void ClipManager::ItemClips::UpdateSeparateLeaf(
+ wr::DisplayListBuilder& aBuilder, int32_t aAppUnitsPerDevPixel) {
+ Maybe<wr::LayoutRect> clipLeaf;
+ if (mSeparateLeaf) {
+ MOZ_ASSERT(mChain);
+ clipLeaf.emplace(wr::ToLayoutRect(LayoutDeviceRect::FromAppUnits(
+ mChain->mClip.GetClipRect(), aAppUnitsPerDevPixel)));
+ }
+
+ aBuilder.SetClipChainLeaf(clipLeaf);
+}
+
+bool ClipManager::ItemClips::HasSameInputs(const ItemClips& aOther) {
+ if (mASR != aOther.mASR || mChain != aOther.mChain ||
+ mSeparateLeaf != aOther.mSeparateLeaf) {
+ return false;
+ }
+ // AUPDP only matters if we have a clip chain, since it's only used to compute
+ // the device space clip rect.
+ if (mChain && mAppUnitsPerDevPixel != aOther.mAppUnitsPerDevPixel) {
+ return false;
+ }
+ return true;
+}
+
+wr::WrSpaceAndClipChain ClipManager::ItemClips::GetSpaceAndClipChain() const {
+ auto spaceAndClipChain = wr::RootScrollNodeWithChain();
+ spaceAndClipChain.space = mScrollId;
+ if (mClipChainId) {
+ spaceAndClipChain.clip_chain = mClipChainId->id;
+ }
+ return spaceAndClipChain;
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/wr/ClipManager.h b/gfx/layers/wr/ClipManager.h
new file mode 100644
index 0000000000..7253fec8bb
--- /dev/null
+++ b/gfx/layers/wr/ClipManager.h
@@ -0,0 +1,150 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GFX_CLIPMANAGER_H
+#define GFX_CLIPMANAGER_H
+
+#include <stack>
+#include <unordered_map>
+
+#include "mozilla/Attributes.h"
+#include "mozilla/webrender/WebRenderAPI.h"
+
+namespace mozilla {
+
+class nsDisplayItem;
+struct ActiveScrolledRoot;
+struct DisplayItemClipChain;
+
+namespace wr {
+class DisplayListBuilder;
+}
+
+namespace layers {
+
+class StackingContextHelper;
+class WebRenderLayerManager;
+
+/**
+ * This class manages creating and assigning scroll layers and clips in
+ * WebRender based on the gecko display list. It has a few public functions that
+ * are intended to be invoked while traversing the Gecko display list, and it
+ * uses the ASR and clip information from the display list to create the
+ * necessary clip state in WebRender.
+ *
+ * The structure of the clip state in WebRender ends up quite similar to how
+ * it is in Gecko. For each ASR in Gecko, we create a scroll layer (i.e. a
+ * scrolling clip) in WebRender; these form a tree structure similar to the
+ * ASR tree structure. Ancestors of scroll layers are always other scroll
+ * layers, or the root scroll node.
+ * The DisplayItemClipChain list of clips from the gecko display list is
+ * converted to a WR clip chain and pushed on the stack prior to creating
+ * any WR commands for that item, and is popped afterwards. In addition,
+ * the WR clip chain has a parent pointer, which points to the clip chain for
+ * any enclosing stacking context. This again results in a strucuture very
+ * similar to that in Gecko, where the clips from container display items get
+ * applied to the contained display items.
+ */
+class ClipManager {
+ public:
+ ClipManager();
+
+ void BeginBuild(WebRenderLayerManager* aManager,
+ wr::DisplayListBuilder& aBuilder);
+ void EndBuild();
+
+ void BeginList(const StackingContextHelper& aStackingContext);
+ void EndList(const StackingContextHelper& aStackingContext);
+
+ wr::WrSpaceAndClipChain SwitchItem(nsDisplayListBuilder* aBuilder,
+ nsDisplayItem* aItem);
+ ~ClipManager();
+
+ void PushOverrideForASR(const ActiveScrolledRoot* aASR,
+ const wr::WrSpatialId& aSpatialId);
+ void PopOverrideForASR(const ActiveScrolledRoot* aASR);
+
+ private:
+ wr::WrSpatialId SpatialIdAfterOverride(const wr::WrSpatialId& aSpatialId);
+ wr::WrSpatialId GetScrollLayer(const ActiveScrolledRoot* aASR);
+
+ Maybe<wr::WrSpatialId> DefineScrollLayers(const ActiveScrolledRoot* aASR,
+ nsDisplayItem* aItem);
+
+ Maybe<wr::WrClipChainId> DefineClipChain(const DisplayItemClipChain* aChain,
+ int32_t aAppUnitsPerDevPixel);
+
+ WebRenderLayerManager* MOZ_NON_OWNING_REF mManager;
+ wr::DisplayListBuilder* mBuilder;
+
+ // Stack of clip caches. Each cache contains a map from gecko
+ // DisplayItemClipChain objects to webrender WrClipIds, which allows us to
+ // avoid redefining identical clips in WR. However, the gecko
+ // DisplayItemClipChain items get deduplicated quite aggressively, without
+ // regard to things like the enclosing reference frame or mask. On the WR
+ // side, we cannot deduplicate clips that aggressively. So what we do is
+ // any time we enter a new reference frame (for example) we create a new clip
+ // cache on mCacheStack. This ensures we continue caching stuff within a given
+ // reference frame, but disallow caching stuff across reference frames. In
+ // general we need to do this anytime PushOverrideForASR is called, as that is
+ // called for the same set of conditions for which we cannot deduplicate
+ // clips.
+ using ClipIdMap = std::unordered_map<const DisplayItemClipChain*,
+ AutoTArray<wr::WrClipId, 4>>;
+ std::stack<ClipIdMap> mCacheStack;
+
+ // A map that holds the cache overrides created by (a) "out of band" clips,
+ // i.e. clips that are generated by display items but that ClipManager
+ // doesn't know about and (b) stacking contexts that affect clip positioning.
+ // These are called "cache overrides" because while we're inside these things,
+ // we cannot use the ASR from the gecko display list as-is. Fundamentally this
+ // results from a mismatch between the ASR+clip items on the gecko side and
+ // the ClipScrollTree on the WR side; the WR side incorporates things like
+ // transforms and stacking context origins while the gecko side manages those
+ // differently.
+ // Any time ClipManager wants to define a new clip as a child of ASR X, it
+ // should first check the cache overrides to see if there is a cache override
+ // item ((a) or (b) above) that is already a child of X, and then define that
+ // clip as a child of Y instead. This map stores X -> Y, which allows
+ // ClipManager to do the necessary lookup. Note that there theoretically might
+ // be multiple different "Y" clips (in case of nested cache overrides), which
+ // is why we need a stack.
+ std::unordered_map<wr::WrSpatialId, std::stack<wr::WrSpatialId>> mASROverride;
+
+ // This holds some clip state for a single nsDisplayItem
+ struct ItemClips {
+ ItemClips(const ActiveScrolledRoot* aASR,
+ const DisplayItemClipChain* aChain, int32_t aAppUnitsPerDevPixel,
+ bool aSeparateLeaf);
+
+ // These are the "inputs" - they come from the nsDisplayItem
+ const ActiveScrolledRoot* mASR;
+ const DisplayItemClipChain* mChain;
+ int32_t mAppUnitsPerDevPixel;
+ bool mSeparateLeaf;
+
+ // These are the "outputs" - they are pushed to WR as needed
+ wr::WrSpatialId mScrollId;
+ Maybe<wr::WrClipChainId> mClipChainId;
+
+ void UpdateSeparateLeaf(wr::DisplayListBuilder& aBuilder,
+ int32_t aAppUnitsPerDevPixel);
+ bool HasSameInputs(const ItemClips& aOther);
+ wr::WrSpaceAndClipChain GetSpaceAndClipChain() const;
+ };
+
+ // A stack of ItemClips corresponding to the nsDisplayItem ancestry. Each
+ // time we recurse into a nsDisplayItem's child list, this stack size
+ // increases by one. The topmost item on the stack is for the display item
+ // we are currently processing and items deeper on the stack are for that
+ // display item's ancestors.
+ std::stack<ItemClips> mItemClipStack;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif
diff --git a/gfx/layers/wr/DisplayItemCache.cpp b/gfx/layers/wr/DisplayItemCache.cpp
new file mode 100644
index 0000000000..cd55da7424
--- /dev/null
+++ b/gfx/layers/wr/DisplayItemCache.cpp
@@ -0,0 +1,203 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "DisplayItemCache.h"
+#include "nsDisplayList.h"
+
+namespace mozilla {
+namespace layers {
+
+DisplayItemCache::DisplayItemCache()
+ : mDisplayList(nullptr),
+ mMaximumSize(0),
+ mPipelineId{},
+ mCaching(false),
+ mInvalid(false),
+ mSuppressed(false) {}
+
+void DisplayItemCache::SetDisplayList(nsDisplayListBuilder* aBuilder,
+ nsDisplayList* aList) {
+ if (!IsEnabled()) {
+ return;
+ }
+
+ MOZ_ASSERT(aBuilder);
+ MOZ_ASSERT(aList);
+
+ const bool listChanged = mDisplayList != aList;
+ const bool partialBuild = !aBuilder->PartialBuildFailed();
+
+ if (listChanged && partialBuild) {
+ // If the display list changed during a partial update, it means that
+ // |SetDisplayList()| has missed one rebuilt display list.
+ mDisplayList = nullptr;
+ return;
+ }
+
+ if (listChanged || !partialBuild) {
+ // The display list has been changed or rebuilt.
+ mDisplayList = aList;
+ mInvalid = true;
+ }
+
+ UpdateState();
+}
+
+void DisplayItemCache::SetPipelineId(const wr::PipelineId& aPipelineId) {
+ mInvalid = mInvalid || !(mPipelineId == aPipelineId);
+ mPipelineId = aPipelineId;
+}
+
+void DisplayItemCache::UpdateState() {
+ // |mCaching == true| if:
+ // 1) |SetDisplayList()| is called with a fully rebuilt display list
+ // followed by
+ // 2a) |SetDisplayList()| is called with a partially updated display list
+ // or
+ // 2b) |SkipWaitingForPartialDisplayList()| is called
+ mCaching = !mInvalid;
+
+#if 0
+ Stats().Print();
+ Stats().Reset();
+#endif
+
+ if (IsEmpty()) {
+ // The cache is empty so nothing needs to be updated.
+ mInvalid = false;
+ return;
+ }
+
+ // Clear the cache if the current state is invalid.
+ if (mInvalid) {
+ Clear();
+ } else {
+ FreeUnusedSlots();
+ }
+
+ mInvalid = false;
+}
+
+void DisplayItemCache::Clear() {
+ memset(mSlots.Elements(), 0, mSlots.Length() * sizeof(Slot));
+ mFreeSlots.ClearAndRetainStorage();
+
+ for (size_t i = 0; i < CurrentSize(); ++i) {
+ mFreeSlots.AppendElement(i);
+ }
+}
+
+Maybe<uint16_t> DisplayItemCache::GetNextFreeSlot() {
+ if (mFreeSlots.IsEmpty() && !GrowIfPossible()) {
+ return Nothing();
+ }
+
+ return Some(mFreeSlots.PopLastElement());
+}
+
+bool DisplayItemCache::GrowIfPossible() {
+ if (IsFull()) {
+ return false;
+ }
+
+ const auto currentSize = CurrentSize();
+ MOZ_ASSERT(currentSize <= mMaximumSize);
+
+ // New slots are added one by one, which is amortized O(1) time complexity due
+ // to underlying storage implementation.
+ mSlots.AppendElement();
+ mFreeSlots.AppendElement(currentSize);
+ return true;
+}
+
+void DisplayItemCache::FreeUnusedSlots() {
+ for (size_t i = 0; i < CurrentSize(); ++i) {
+ auto& slot = mSlots[i];
+
+ if (!slot.mUsed && slot.mOccupied) {
+ // This entry contained a cached item, but was not used.
+ slot.mOccupied = false;
+ mFreeSlots.AppendElement(i);
+ }
+
+ slot.mUsed = false;
+ }
+}
+
+void DisplayItemCache::SetCapacity(const size_t aInitialSize,
+ const size_t aMaximumSize) {
+ mMaximumSize = aMaximumSize;
+ mSlots.SetLength(aInitialSize);
+ mFreeSlots.SetCapacity(aMaximumSize);
+ Clear();
+}
+
+Maybe<uint16_t> DisplayItemCache::AssignSlot(nsPaintedDisplayItem* aItem) {
+ if (!mCaching || !aItem->CanBeReused() || !aItem->CanBeCached()) {
+ return Nothing();
+ }
+
+ auto& slot = aItem->CacheIndex();
+
+ if (!slot) {
+ slot = GetNextFreeSlot();
+ if (!slot) {
+ // The item does not fit in the cache.
+ return Nothing();
+ }
+ }
+
+ MOZ_ASSERT(slot && CurrentSize() > *slot);
+ return slot;
+}
+
+void DisplayItemCache::MarkSlotOccupied(
+ uint16_t aSlotIndex, const wr::WrSpaceAndClipChain& aSpaceAndClip) {
+ // Caching of the item succeeded, update the slot state.
+ auto& slot = mSlots[aSlotIndex];
+ MOZ_ASSERT(!slot.mOccupied);
+ slot.mOccupied = true;
+ MOZ_ASSERT(!slot.mUsed);
+ slot.mUsed = true;
+ slot.mSpaceAndClip = aSpaceAndClip;
+}
+
+Maybe<uint16_t> DisplayItemCache::CanReuseItem(
+ nsPaintedDisplayItem* aItem, const wr::WrSpaceAndClipChain& aSpaceAndClip) {
+ auto& slotIndex = aItem->CacheIndex();
+ if (!slotIndex) {
+ return Nothing();
+ }
+
+ MOZ_ASSERT(slotIndex && CurrentSize() > *slotIndex);
+
+ auto& slot = mSlots[*slotIndex];
+ if (!slot.mOccupied) {
+ // The display item has a stale cache slot. Recache the item.
+ return Nothing();
+ }
+
+ if (mSuppressed) {
+ slot.mOccupied = false;
+ slotIndex = Nothing();
+ return Nothing();
+ }
+
+ if (!(aSpaceAndClip == slot.mSpaceAndClip)) {
+ // Spatial id and clip id can change between display lists, if items that
+ // generate them change their order.
+ slot.mOccupied = false;
+ aItem->SetCantBeCached();
+ slotIndex = Nothing();
+ } else {
+ slot.mUsed = true;
+ }
+
+ return slotIndex;
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/wr/DisplayItemCache.h b/gfx/layers/wr/DisplayItemCache.h
new file mode 100644
index 0000000000..a44b4a52f5
--- /dev/null
+++ b/gfx/layers/wr/DisplayItemCache.h
@@ -0,0 +1,210 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GFX_DISPLAY_ITEM_CACHE_H
+#define GFX_DISPLAY_ITEM_CACHE_H
+
+#include "mozilla/webrender/WebRenderAPI.h"
+#include "nsTArray.h"
+
+namespace mozilla {
+
+class nsDisplayList;
+class nsDisplayListBuilder;
+class nsPaintedDisplayItem;
+
+namespace wr {
+class DisplayListBuilder;
+} // namespace wr
+
+namespace layers {
+
+class CacheStats {
+ public:
+ CacheStats() = default;
+
+ void Reset() { mCached = mReused = mTotal = 0; }
+
+ void Print() {
+ static uint64_t avgC = 1;
+ static uint64_t avgR = 1;
+ static uint64_t avgT = 1;
+
+ avgC += mCached;
+ avgR += mReused;
+ avgT += mTotal;
+
+ printf("Cached: %zu (avg: %f), Reused: %zu (avg: %f), Total: %zu\n",
+ mCached, (double)avgC / (double)avgT, mReused,
+ (double)avgR / (double)avgT, mTotal);
+ }
+
+ void AddCached() { mCached++; }
+ void AddReused() { mReused++; }
+ void AddTotal() { mTotal++; }
+
+ private:
+ size_t mCached = 0;
+ size_t mReused = 0;
+ size_t mTotal = 0;
+};
+
+/**
+ * DisplayItemCache keeps track of which Gecko display items have already had
+ * their respective WebRender display items sent to WebRender backend.
+ *
+ * Ideally creating the WR display items for a Gecko display item would not
+ * depend on any external state. However currently pipeline id, clip id, and
+ * spatial id can change between display lists, even if the Gecko display items
+ * have not. This state is tracked by DisplayItemCache.
+ */
+class DisplayItemCache final {
+ public:
+ DisplayItemCache();
+
+ /**
+ * Clears the cache.
+ */
+ void Clear();
+
+ /**
+ * Sets the initial and max cache size to given |aInitialSize| and |aMaxSize|.
+ */
+ void SetCapacity(const size_t aInitialSize, const size_t aMaximumSize);
+
+ /**
+ * Sets the display list used by the cache.
+ */
+ void SetDisplayList(nsDisplayListBuilder* aBuilder, nsDisplayList* aList);
+
+ /**
+ * Sets the pipeline id used by the cache.
+ */
+ void SetPipelineId(const wr::PipelineId& aPipelineId);
+
+ /**
+ * Enables caching immediately if the cache is valid, and display list is set.
+ */
+ void SkipWaitingForPartialDisplayList() {
+ mCaching = mDisplayList && !mInvalid;
+ }
+
+ /**
+ * Returns true if display item caching is enabled, otherwise false.
+ */
+ bool IsEnabled() const { return !mSuppressed && mMaximumSize > 0; }
+
+ /**
+ * Suppress display item caching. This doesn't clear any existing cached
+ * items or change the underlying capacity, it just makes IsEnabled() return
+ * false.
+ * It will also make CanReuseItem return false for the duration of the
+ * suppression.
+ */
+ bool SetSuppressed(bool aSuppressed) {
+ if (aSuppressed == mSuppressed) {
+ return mSuppressed;
+ }
+ mSuppressed = aSuppressed;
+ return !mSuppressed;
+ }
+
+ /**
+ * Returns true if there are no cached items, otherwise false.
+ */
+ bool IsEmpty() const { return mFreeSlots.Length() == CurrentSize(); }
+
+ /**
+ * Returns true if the cache has reached the maximum size, otherwise false.
+ */
+ bool IsFull() const {
+ return mFreeSlots.IsEmpty() && CurrentSize() == mMaximumSize;
+ }
+
+ /**
+ * Returns the current cache size.
+ */
+ size_t CurrentSize() const { return mSlots.Length(); }
+
+ /**
+ * If there are free slots in the cache, assigns a cache slot to the given
+ * display item |aItem| and returns it. Otherwise returns Nothing().
+ */
+ Maybe<uint16_t> AssignSlot(nsPaintedDisplayItem* aItem);
+
+ /**
+ * Marks the slot with the given |slotIndex| occupied and used.
+ * Also stores the current space and clipchain |aSpaceAndClip|.
+ */
+ void MarkSlotOccupied(uint16_t slotIndex,
+ const wr::WrSpaceAndClipChain& aSpaceAndClip);
+
+ /**
+ * Returns the slot index of the the given display item |aItem|, if the item
+ * can be reused. The current space and clipchain |aSpaceAndClip| is used to
+ * check whether the cached item is still valid.
+ * If the item cannot be reused, returns Nothing().
+ */
+ Maybe<uint16_t> CanReuseItem(nsPaintedDisplayItem* aItem,
+ const wr::WrSpaceAndClipChain& aSpaceAndClip);
+
+ CacheStats& Stats() { return mCacheStats; }
+
+ private:
+ struct Slot {
+ Slot() : mSpaceAndClip{}, mOccupied(false), mUsed(false) {}
+
+ wr::WrSpaceAndClipChain mSpaceAndClip;
+ bool mOccupied;
+ bool mUsed;
+ };
+
+ void FreeUnusedSlots();
+ Maybe<uint16_t> GetNextFreeSlot();
+ bool GrowIfPossible();
+ void UpdateState();
+
+ // The lifetime of display lists exceed the lifetime of DisplayItemCache.
+ // This pointer stores the address of the display list that is using this
+ // cache, and it is only used for pointer comparisons.
+ nsDisplayList* mDisplayList;
+
+ size_t mMaximumSize;
+ nsTArray<Slot> mSlots;
+ nsTArray<uint16_t> mFreeSlots;
+
+ wr::PipelineId mPipelineId;
+ bool mCaching;
+ bool mInvalid;
+ bool mSuppressed;
+
+ CacheStats mCacheStats;
+};
+
+class MOZ_RAII AutoDisplayItemCacheSuppressor {
+ public:
+ explicit AutoDisplayItemCacheSuppressor(DisplayItemCache* aCache)
+ : mCache(aCache) {
+ mWasSuppressed = mCache->SetSuppressed(true);
+ }
+
+ // Note that this restores the original state rather than unconditionally
+ // unsuppressing the cache for future-proofing/robustification. Currently
+ // we only ever use this RAII in one non-recursive function, but we might
+ // decide to expand its usage to other scenarios and end up with nested
+ // suppressions, in which case restoring the state back to what we found it
+ // is better.
+ ~AutoDisplayItemCacheSuppressor() { mCache->SetSuppressed(mWasSuppressed); }
+
+ private:
+ DisplayItemCache* mCache;
+ bool mWasSuppressed;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif /* GFX_DISPLAY_ITEM_CACHE_H */
diff --git a/gfx/layers/wr/HitTestInfoManager.cpp b/gfx/layers/wr/HitTestInfoManager.cpp
new file mode 100644
index 0000000000..3365a92fbf
--- /dev/null
+++ b/gfx/layers/wr/HitTestInfoManager.cpp
@@ -0,0 +1,135 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "HitTestInfoManager.h"
+#include "HitTestInfo.h"
+
+#include "nsDisplayList.h"
+
+#define DEBUG_HITTEST_INFO 0
+#if DEBUG_HITTEST_INFO
+# define HITTEST_INFO_LOG(...) printf_stderr(__VA_ARGS__)
+#else
+# define HITTEST_INFO_LOG(...)
+#endif
+
+namespace mozilla::layers {
+
+using ViewID = ScrollableLayerGuid::ViewID;
+
+/**
+ * TODO(miko): This used to be a performance bottle-neck, but it does not show
+ * up in profiles anymore, see bugs 1424637 and 1424968.
+ * A better way of doing this would be to store current app units per dev pixel
+ * in wr::DisplayListBuilder, and update it whenever display items that separate
+ * presshell boundaries are encountered.
+ */
+static int32_t GetAppUnitsFromDisplayItem(nsDisplayItem* aItem) {
+ nsIFrame* frame = aItem->Frame();
+ MOZ_ASSERT(frame);
+ return frame->PresContext()->AppUnitsPerDevPixel();
+}
+
+static void CreateWebRenderCommands(wr::DisplayListBuilder& aBuilder,
+ nsDisplayItem* aItem, const nsRect& aArea,
+ const gfx::CompositorHitTestInfo& aFlags,
+ const ViewID& aViewId) {
+ const Maybe<SideBits> sideBits =
+ aBuilder.GetContainingFixedPosSideBits(aItem->GetActiveScrolledRoot());
+
+ const LayoutDeviceRect devRect =
+ LayoutDeviceRect::FromAppUnits(aArea, GetAppUnitsFromDisplayItem(aItem));
+ const wr::LayoutRect rect = wr::ToLayoutRect(devRect);
+
+ aBuilder.PushHitTest(rect, rect, !aItem->BackfaceIsHidden(), aViewId, aFlags,
+ sideBits.valueOr(SideBits::eNone));
+}
+
+HitTestInfoManager::HitTestInfoManager()
+ : mArea(nsRect()),
+ mFlags(gfx::CompositorHitTestInvisibleToHit),
+ mViewId(ScrollableLayerGuid::NULL_SCROLL_ID),
+ mSpaceAndClipChain(wr::InvalidScrollNodeWithChain()) {}
+
+void HitTestInfoManager::Reset() {
+ mArea = nsRect();
+ mFlags = gfx::CompositorHitTestInvisibleToHit;
+ mViewId = ScrollableLayerGuid::NULL_SCROLL_ID;
+ mSpaceAndClipChain = wr::InvalidScrollNodeWithChain();
+
+ HITTEST_INFO_LOG("* HitTestInfoManager::Reset\n");
+}
+
+bool HitTestInfoManager::ProcessItem(
+ nsDisplayItem* aItem, wr::DisplayListBuilder& aBuilder,
+ nsDisplayListBuilder* aDisplayListBuilder) {
+ MOZ_ASSERT(aItem);
+
+ HITTEST_INFO_LOG("* HitTestInfoManager::ProcessItem(%d, %s, has=%d)\n",
+ getpid(), aItem->Frame()->ListTag().get(),
+ aItem->HasHitTestInfo());
+
+ if (MOZ_UNLIKELY(aItem->GetType() == DisplayItemType::TYPE_REMOTE)) {
+ // Remote frames might contain hit-test-info items inside (but those
+ // aren't processed by this process of course), so we can't optimize out the
+ // next hit-test info item because it might be on top of the iframe.
+ Reset();
+ }
+
+ if (!aItem->HasHitTestInfo()) {
+ return false;
+ }
+
+ const HitTestInfo& hitTestInfo = aItem->GetHitTestInfo();
+ const nsRect& area = hitTestInfo.Area();
+ const gfx::CompositorHitTestInfo& flags = hitTestInfo.Info();
+
+ if (flags == gfx::CompositorHitTestInvisibleToHit || area.IsEmpty()) {
+ return false;
+ }
+
+ const auto viewId =
+ hitTestInfo.GetViewId(aBuilder, aItem->GetActiveScrolledRoot());
+ const auto spaceAndClipChain = aBuilder.CurrentSpaceAndClipChain();
+
+ if (!Update(area, flags, viewId, spaceAndClipChain)) {
+ // The previous hit test information is still valid.
+ return false;
+ }
+
+ HITTEST_INFO_LOG("+ [%d, %d, %d, %d]: flags: 0x%x, viewId: %lu\n", area.x,
+ area.y, area.width, area.height, flags.serialize(), viewId);
+
+ CreateWebRenderCommands(aBuilder, aItem, area, flags, viewId);
+
+ return true;
+}
+
+/**
+ * Updates the current hit testing information if necessary.
+ * Returns true if the hit testing information was changed.
+ */
+bool HitTestInfoManager::Update(const nsRect& aArea,
+ const gfx::CompositorHitTestInfo& aFlags,
+ const ViewID& aViewId,
+ const wr::WrSpaceAndClipChain& aSpaceAndClip) {
+ if (mViewId == aViewId && mFlags == aFlags && mArea.Contains(aArea) &&
+ mSpaceAndClipChain == aSpaceAndClip) {
+ // The previous hit testing information can be reused.
+ HITTEST_INFO_LOG("s [%d, %d, %d, %d]: flags: 0x%x, viewId: %lu\n", aArea.x,
+ aArea.y, aArea.width, aArea.height, aFlags.serialize(),
+ aViewId);
+ return false;
+ }
+
+ mArea = aArea;
+ mFlags = aFlags;
+ mViewId = aViewId;
+ mSpaceAndClipChain = aSpaceAndClip;
+ return true;
+}
+
+} // namespace mozilla::layers
diff --git a/gfx/layers/wr/HitTestInfoManager.h b/gfx/layers/wr/HitTestInfoManager.h
new file mode 100644
index 0000000000..5ee7b566dc
--- /dev/null
+++ b/gfx/layers/wr/HitTestInfoManager.h
@@ -0,0 +1,66 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GFX_HITTESTINFOMANAGER_H
+#define GFX_HITTESTINFOMANAGER_H
+
+#include "mozilla/gfx/CompositorHitTestInfo.h"
+#include "mozilla/layers/ScrollableLayerGuid.h"
+#include "mozilla/webrender/WebRenderAPI.h"
+#include "nsRect.h"
+
+namespace mozilla {
+
+class nsDisplayItem;
+class nsDisplayListBuilder;
+
+namespace wr {
+class DisplayListBuilder;
+}
+
+namespace layers {
+
+/**
+ * This class extracts the hit testing information (area, flags, ViewId) from
+ * Gecko display items and pushes them into WebRender display list.
+ *
+ * The hit testing information is deduplicated: a new hit test item is only
+ * added if the new area is not contained in the previous area, or if the flags,
+ * ViewId, or current spatial id is different.
+ */
+class HitTestInfoManager {
+ public:
+ HitTestInfoManager();
+
+ /**
+ * Resets the previous hit testing information.
+ */
+ void Reset();
+
+ /**
+ * Extracts the hit testing information from |aItem|, and if necessary, adds
+ * a new WebRender hit test item using |aBuilder|.
+ *
+ * Returns true if a hit test item was pushed.
+ */
+ bool ProcessItem(nsDisplayItem* aItem, wr::DisplayListBuilder& aBuilder,
+ nsDisplayListBuilder* aDisplayListBuilder);
+
+ private:
+ bool Update(const nsRect& aArea, const gfx::CompositorHitTestInfo& aFlags,
+ const ScrollableLayerGuid::ViewID& aViewId,
+ const wr::WrSpaceAndClipChain& aSpaceAndClip);
+
+ nsRect mArea;
+ gfx::CompositorHitTestInfo mFlags;
+ ScrollableLayerGuid::ViewID mViewId;
+ wr::WrSpaceAndClipChain mSpaceAndClipChain;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif
diff --git a/gfx/layers/wr/IpcResourceUpdateQueue.cpp b/gfx/layers/wr/IpcResourceUpdateQueue.cpp
new file mode 100644
index 0000000000..d19dc7f2f9
--- /dev/null
+++ b/gfx/layers/wr/IpcResourceUpdateQueue.cpp
@@ -0,0 +1,484 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "IpcResourceUpdateQueue.h"
+#include <string.h>
+#include <algorithm>
+#include "mozilla/Maybe.h"
+#include "mozilla/ipc/SharedMemory.h"
+#include "mozilla/layers/PTextureChild.h"
+#include "mozilla/layers/WebRenderBridgeChild.h"
+
+namespace mozilla {
+namespace wr {
+
+using namespace mozilla::layers;
+
+ShmSegmentsWriter::ShmSegmentsWriter(layers::WebRenderBridgeChild* aAllocator,
+ size_t aChunkSize)
+ : mShmAllocator(aAllocator), mCursor(0), mChunkSize(aChunkSize) {
+ MOZ_ASSERT(mShmAllocator);
+}
+
+ShmSegmentsWriter::~ShmSegmentsWriter() { Clear(); }
+
+ShmSegmentsWriter::ShmSegmentsWriter(ShmSegmentsWriter&& aOther) noexcept
+ : mSmallAllocs(std::move(aOther.mSmallAllocs)),
+ mLargeAllocs(std::move(aOther.mLargeAllocs)),
+ mShmAllocator(aOther.mShmAllocator),
+ mCursor(aOther.mCursor),
+ mChunkSize(aOther.mChunkSize) {
+ aOther.mCursor = 0;
+}
+
+ShmSegmentsWriter& ShmSegmentsWriter::operator=(
+ ShmSegmentsWriter&& aOther) noexcept {
+ MOZ_ASSERT(IsEmpty(), "Will forget existing updates!");
+ Clear();
+ mSmallAllocs = std::move(aOther.mSmallAllocs);
+ mLargeAllocs = std::move(aOther.mLargeAllocs);
+ mShmAllocator = aOther.mShmAllocator;
+ mCursor = aOther.mCursor;
+ mChunkSize = aOther.mChunkSize;
+ aOther.mCursor = 0;
+ return *this;
+}
+
+layers::OffsetRange ShmSegmentsWriter::Write(Range<uint8_t> aBytes) {
+ const size_t start = mCursor;
+ const size_t length = aBytes.length();
+
+ if (length >= mChunkSize * 4) {
+ auto range = AllocLargeChunk(length);
+ if (range.length()) {
+ // Allocation was successful
+ uint8_t* dstPtr = mLargeAllocs.LastElement().get<uint8_t>();
+ memcpy(dstPtr, aBytes.begin().get(), length);
+ }
+ return range;
+ }
+
+ int remainingBytesToCopy = length;
+
+ size_t srcCursor = 0;
+ size_t dstCursor = mCursor;
+ size_t currAllocLen = mSmallAllocs.Length();
+
+ while (remainingBytesToCopy > 0) {
+ if (dstCursor >= mSmallAllocs.Length() * mChunkSize) {
+ if (!AllocChunk()) {
+ // Allocation failed, so roll back to the state at the start of this
+ // Write() call and abort.
+ while (mSmallAllocs.Length() > currAllocLen) {
+ RefCountedShmem shm = mSmallAllocs.PopLastElement();
+ RefCountedShm::Dealloc(mShmAllocator, shm);
+ }
+ MOZ_ASSERT(mSmallAllocs.Length() == currAllocLen);
+ return layers::OffsetRange(0, start, 0);
+ }
+ // Allocation succeeded, so dstCursor should now be pointing to
+ // something inside the allocation buffer
+ MOZ_ASSERT(dstCursor < (mSmallAllocs.Length() * mChunkSize));
+ }
+
+ const size_t dstMaxOffset = mChunkSize * mSmallAllocs.Length();
+ const size_t dstBaseOffset = mChunkSize * (mSmallAllocs.Length() - 1);
+
+ MOZ_ASSERT(dstCursor >= dstBaseOffset);
+ MOZ_ASSERT(dstCursor <= dstMaxOffset);
+
+ size_t availableRange = dstMaxOffset - dstCursor;
+ size_t copyRange = std::min<int>(availableRange, remainingBytesToCopy);
+
+ uint8_t* srcPtr = &aBytes[srcCursor];
+ uint8_t* dstPtr = RefCountedShm::GetBytes(mSmallAllocs.LastElement()) +
+ (dstCursor - dstBaseOffset);
+
+ memcpy(dstPtr, srcPtr, copyRange);
+
+ srcCursor += copyRange;
+ dstCursor += copyRange;
+ remainingBytesToCopy -= copyRange;
+
+ // sanity check
+ MOZ_ASSERT(remainingBytesToCopy >= 0);
+ }
+
+ mCursor += length;
+
+ return layers::OffsetRange(0, start, length);
+}
+
+bool ShmSegmentsWriter::AllocChunk() {
+ RefCountedShmem shm;
+ if (!mShmAllocator->AllocResourceShmem(mChunkSize, shm)) {
+ gfxCriticalNote << "ShmSegmentsWriter failed to allocate chunk #"
+ << mSmallAllocs.Length();
+ MOZ_ASSERT(false, "ShmSegmentsWriter fails to allocate chunk");
+ return false;
+ }
+ RefCountedShm::AddRef(shm);
+ mSmallAllocs.AppendElement(shm);
+ return true;
+}
+
+layers::OffsetRange ShmSegmentsWriter::AllocLargeChunk(size_t aSize) {
+ ipc::Shmem shm;
+ if (!mShmAllocator->AllocShmem(aSize, &shm)) {
+ gfxCriticalNote
+ << "ShmSegmentsWriter failed to allocate large chunk of size " << aSize;
+ MOZ_ASSERT(false, "ShmSegmentsWriter fails to allocate large chunk");
+ return layers::OffsetRange(0, 0, 0);
+ }
+ mLargeAllocs.AppendElement(shm);
+
+ return layers::OffsetRange(mLargeAllocs.Length(), 0, aSize);
+}
+
+void ShmSegmentsWriter::Flush(nsTArray<RefCountedShmem>& aSmallAllocs,
+ nsTArray<ipc::Shmem>& aLargeAllocs) {
+ MOZ_ASSERT(aSmallAllocs.IsEmpty());
+ MOZ_ASSERT(aLargeAllocs.IsEmpty());
+ aSmallAllocs = std::move(mSmallAllocs);
+ aLargeAllocs = std::move(mLargeAllocs);
+ mCursor = 0;
+}
+
+bool ShmSegmentsWriter::IsEmpty() const { return mCursor == 0; }
+
+void ShmSegmentsWriter::Clear() {
+ if (mShmAllocator) {
+ IpcResourceUpdateQueue::ReleaseShmems(mShmAllocator, mSmallAllocs);
+ IpcResourceUpdateQueue::ReleaseShmems(mShmAllocator, mLargeAllocs);
+ }
+ mCursor = 0;
+}
+
+ShmSegmentsReader::ShmSegmentsReader(
+ const nsTArray<RefCountedShmem>& aSmallShmems,
+ const nsTArray<ipc::Shmem>& aLargeShmems)
+ : mSmallAllocs(aSmallShmems), mLargeAllocs(aLargeShmems), mChunkSize(0) {
+ if (mSmallAllocs.IsEmpty()) {
+ return;
+ }
+
+ mChunkSize = RefCountedShm::GetSize(mSmallAllocs[0]);
+
+ // Check that all shmems are readable and have the same size. If anything
+ // isn't right, set mChunkSize to zero which signifies that the reader is
+ // in an invalid state and Read calls will return false;
+ for (const auto& shm : mSmallAllocs) {
+ if (!RefCountedShm::IsValid(shm) ||
+ RefCountedShm::GetSize(shm) != mChunkSize ||
+ RefCountedShm::GetBytes(shm) == nullptr) {
+ mChunkSize = 0;
+ return;
+ }
+ }
+
+ for (const auto& shm : mLargeAllocs) {
+ if (!shm.IsReadable() || shm.get<uint8_t>() == nullptr) {
+ mChunkSize = 0;
+ return;
+ }
+ }
+}
+
+bool ShmSegmentsReader::ReadLarge(const layers::OffsetRange& aRange,
+ wr::Vec<uint8_t>& aInto) {
+ // source = zero is for small allocs.
+ MOZ_RELEASE_ASSERT(aRange.source() != 0);
+ if (aRange.source() > mLargeAllocs.Length()) {
+ return false;
+ }
+ size_t id = aRange.source() - 1;
+ const ipc::Shmem& shm = mLargeAllocs[id];
+ if (shm.Size<uint8_t>() < aRange.length()) {
+ return false;
+ }
+
+ uint8_t* srcPtr = shm.get<uint8_t>();
+ aInto.PushBytes(Range<uint8_t>(srcPtr, aRange.length()));
+
+ return true;
+}
+
+bool ShmSegmentsReader::Read(const layers::OffsetRange& aRange,
+ wr::Vec<uint8_t>& aInto) {
+ if (aRange.length() == 0) {
+ return true;
+ }
+
+ if (aRange.source() != 0) {
+ return ReadLarge(aRange, aInto);
+ }
+
+ if (mChunkSize == 0) {
+ return false;
+ }
+
+ if (aRange.start() + aRange.length() > mChunkSize * mSmallAllocs.Length()) {
+ return false;
+ }
+
+ size_t initialLength = aInto.Length();
+
+ size_t srcCursor = aRange.start();
+ size_t remainingBytesToCopy = aRange.length();
+ while (remainingBytesToCopy > 0) {
+ const size_t shm_idx = srcCursor / mChunkSize;
+ const size_t ptrOffset = srcCursor % mChunkSize;
+ const size_t copyRange =
+ std::min(remainingBytesToCopy, mChunkSize - ptrOffset);
+ uint8_t* srcPtr =
+ RefCountedShm::GetBytes(mSmallAllocs[shm_idx]) + ptrOffset;
+
+ aInto.PushBytes(Range<uint8_t>(srcPtr, copyRange));
+
+ srcCursor += copyRange;
+ remainingBytesToCopy -= copyRange;
+ }
+
+ return aInto.Length() - initialLength == aRange.length();
+}
+
+Maybe<Range<uint8_t>> ShmSegmentsReader::GetReadPointerLarge(
+ const layers::OffsetRange& aRange) {
+ // source = zero is for small allocs.
+ MOZ_RELEASE_ASSERT(aRange.source() != 0);
+ if (aRange.source() > mLargeAllocs.Length()) {
+ return Nothing();
+ }
+ size_t id = aRange.source() - 1;
+ const ipc::Shmem& shm = mLargeAllocs[id];
+ if (shm.Size<uint8_t>() < aRange.length()) {
+ return Nothing();
+ }
+
+ uint8_t* srcPtr = shm.get<uint8_t>();
+ return Some(Range<uint8_t>(srcPtr, aRange.length()));
+}
+
+Maybe<Range<uint8_t>> ShmSegmentsReader::GetReadPointer(
+ const layers::OffsetRange& aRange) {
+ if (aRange.length() == 0) {
+ return Some(Range<uint8_t>());
+ }
+
+ if (aRange.source() != 0) {
+ return GetReadPointerLarge(aRange);
+ }
+
+ if (mChunkSize == 0 ||
+ aRange.start() + aRange.length() > mChunkSize * mSmallAllocs.Length()) {
+ return Nothing();
+ }
+
+ size_t srcCursor = aRange.start();
+ size_t remainingBytesToCopy = aRange.length();
+ const size_t shm_idx = srcCursor / mChunkSize;
+ const size_t ptrOffset = srcCursor % mChunkSize;
+ // Return nothing if we can't return a pointer to the full range
+ if (mChunkSize - ptrOffset < remainingBytesToCopy) {
+ return Nothing();
+ }
+ uint8_t* srcPtr = RefCountedShm::GetBytes(mSmallAllocs[shm_idx]) + ptrOffset;
+ return Some(Range<uint8_t>(srcPtr, remainingBytesToCopy));
+}
+
+IpcResourceUpdateQueue::IpcResourceUpdateQueue(
+ layers::WebRenderBridgeChild* aAllocator, size_t aChunkSize)
+ : mWriter(aAllocator, aChunkSize) {}
+
+IpcResourceUpdateQueue::IpcResourceUpdateQueue(
+ IpcResourceUpdateQueue&& aOther) noexcept
+ : mWriter(std::move(aOther.mWriter)),
+ mUpdates(std::move(aOther.mUpdates)) {}
+
+IpcResourceUpdateQueue& IpcResourceUpdateQueue::operator=(
+ IpcResourceUpdateQueue&& aOther) noexcept {
+ MOZ_ASSERT(IsEmpty(), "Will forget existing updates!");
+ mWriter = std::move(aOther.mWriter);
+ mUpdates = std::move(aOther.mUpdates);
+ return *this;
+}
+
+void IpcResourceUpdateQueue::ReplaceResources(IpcResourceUpdateQueue&& aOther) {
+ MOZ_ASSERT(IsEmpty(), "Will forget existing updates!");
+ mWriter = std::move(aOther.mWriter);
+ mUpdates = std::move(aOther.mUpdates);
+}
+
+bool IpcResourceUpdateQueue::AddImage(ImageKey key,
+ const ImageDescriptor& aDescriptor,
+ Range<uint8_t> aBytes) {
+ auto bytes = mWriter.Write(aBytes);
+ if (!bytes.length()) {
+ return false;
+ }
+ mUpdates.AppendElement(layers::OpAddImage(aDescriptor, bytes, 0, key));
+ return true;
+}
+
+bool IpcResourceUpdateQueue::AddBlobImage(BlobImageKey key,
+ const ImageDescriptor& aDescriptor,
+ Range<uint8_t> aBytes,
+ ImageIntRect aVisibleRect) {
+ MOZ_RELEASE_ASSERT(aDescriptor.width > 0 && aDescriptor.height > 0);
+ auto bytes = mWriter.Write(aBytes);
+ if (!bytes.length()) {
+ return false;
+ }
+ mUpdates.AppendElement(
+ layers::OpAddBlobImage(aDescriptor, bytes, aVisibleRect, 0, key));
+ return true;
+}
+
+void IpcResourceUpdateQueue::AddSharedExternalImage(wr::ExternalImageId aExtId,
+ wr::ImageKey aKey) {
+ mUpdates.AppendElement(layers::OpAddSharedExternalImage(aExtId, aKey));
+}
+
+void IpcResourceUpdateQueue::PushExternalImageForTexture(
+ wr::ExternalImageId aExtId, wr::ImageKey aKey,
+ layers::TextureClient* aTexture, bool aIsUpdate) {
+ MOZ_ASSERT(aTexture);
+ MOZ_ASSERT(aTexture->GetIPDLActor());
+ MOZ_RELEASE_ASSERT(aTexture->GetIPDLActor()->GetIPCChannel() ==
+ mWriter.WrBridge()->GetIPCChannel());
+ mUpdates.AppendElement(layers::OpPushExternalImageForTexture(
+ aExtId, aKey, WrapNotNull(aTexture->GetIPDLActor()), aIsUpdate));
+}
+
+bool IpcResourceUpdateQueue::UpdateImageBuffer(
+ ImageKey aKey, const ImageDescriptor& aDescriptor, Range<uint8_t> aBytes) {
+ auto bytes = mWriter.Write(aBytes);
+ if (!bytes.length()) {
+ return false;
+ }
+ mUpdates.AppendElement(layers::OpUpdateImage(aDescriptor, bytes, aKey));
+ return true;
+}
+
+bool IpcResourceUpdateQueue::UpdateBlobImage(BlobImageKey aKey,
+ const ImageDescriptor& aDescriptor,
+ Range<uint8_t> aBytes,
+ ImageIntRect aVisibleRect,
+ ImageIntRect aDirtyRect) {
+ MOZ_ASSERT(aVisibleRect.width > 0 && aVisibleRect.height > 0);
+
+ auto bytes = mWriter.Write(aBytes);
+ if (!bytes.length()) {
+ return false;
+ }
+ mUpdates.AppendElement(layers::OpUpdateBlobImage(aDescriptor, bytes, aKey,
+ aVisibleRect, aDirtyRect));
+ return true;
+}
+
+void IpcResourceUpdateQueue::UpdateSharedExternalImage(
+ wr::ExternalImageId aExtId, wr::ImageKey aKey, ImageIntRect aDirtyRect) {
+ mUpdates.AppendElement(
+ layers::OpUpdateSharedExternalImage(aExtId, aKey, aDirtyRect));
+}
+
+void IpcResourceUpdateQueue::SetBlobImageVisibleArea(
+ wr::BlobImageKey aKey, const ImageIntRect& aArea) {
+ mUpdates.AppendElement(layers::OpSetBlobImageVisibleArea(aArea, aKey));
+}
+
+void IpcResourceUpdateQueue::DeleteImage(ImageKey aKey) {
+ mUpdates.AppendElement(layers::OpDeleteImage(aKey));
+}
+
+void IpcResourceUpdateQueue::DeleteBlobImage(BlobImageKey aKey) {
+ mUpdates.AppendElement(layers::OpDeleteBlobImage(aKey));
+}
+
+bool IpcResourceUpdateQueue::AddRawFont(wr::FontKey aKey, Range<uint8_t> aBytes,
+ uint32_t aIndex) {
+ auto bytes = mWriter.Write(aBytes);
+ if (!bytes.length()) {
+ return false;
+ }
+ mUpdates.AppendElement(layers::OpAddRawFont(bytes, aIndex, aKey));
+ return true;
+}
+
+bool IpcResourceUpdateQueue::AddFontDescriptor(wr::FontKey aKey,
+ Range<uint8_t> aBytes,
+ uint32_t aIndex) {
+ auto bytes = mWriter.Write(aBytes);
+ if (!bytes.length()) {
+ return false;
+ }
+ mUpdates.AppendElement(layers::OpAddFontDescriptor(bytes, aIndex, aKey));
+ return true;
+}
+
+void IpcResourceUpdateQueue::DeleteFont(wr::FontKey aKey) {
+ mUpdates.AppendElement(layers::OpDeleteFont(aKey));
+}
+
+void IpcResourceUpdateQueue::AddFontInstance(
+ wr::FontInstanceKey aKey, wr::FontKey aFontKey, float aGlyphSize,
+ const wr::FontInstanceOptions* aOptions,
+ const wr::FontInstancePlatformOptions* aPlatformOptions,
+ Range<const gfx::FontVariation> aVariations) {
+ auto bytes = mWriter.WriteAsBytes(aVariations);
+ mUpdates.AppendElement(layers::OpAddFontInstance(
+ aOptions ? Some(*aOptions) : Nothing(),
+ aPlatformOptions ? Some(*aPlatformOptions) : Nothing(), bytes, aKey,
+ aFontKey, aGlyphSize));
+}
+
+void IpcResourceUpdateQueue::DeleteFontInstance(wr::FontInstanceKey aKey) {
+ mUpdates.AppendElement(layers::OpDeleteFontInstance(aKey));
+}
+
+void IpcResourceUpdateQueue::Flush(
+ nsTArray<layers::OpUpdateResource>& aUpdates,
+ nsTArray<layers::RefCountedShmem>& aSmallAllocs,
+ nsTArray<ipc::Shmem>& aLargeAllocs) {
+ aUpdates = std::move(mUpdates);
+ mWriter.Flush(aSmallAllocs, aLargeAllocs);
+}
+
+bool IpcResourceUpdateQueue::IsEmpty() const {
+ if (mUpdates.Length() == 0) {
+ MOZ_ASSERT(mWriter.IsEmpty());
+ return true;
+ }
+ return false;
+}
+
+void IpcResourceUpdateQueue::Clear() {
+ mWriter.Clear();
+ mUpdates.Clear();
+}
+
+// static
+void IpcResourceUpdateQueue::ReleaseShmems(
+ ipc::IProtocol* aShmAllocator, nsTArray<layers::RefCountedShmem>& aShms) {
+ for (auto& shm : aShms) {
+ if (RefCountedShm::IsValid(shm) && RefCountedShm::Release(shm) == 0) {
+ RefCountedShm::Dealloc(aShmAllocator, shm);
+ }
+ }
+ aShms.Clear();
+}
+
+// static
+void IpcResourceUpdateQueue::ReleaseShmems(ipc::IProtocol* aShmAllocator,
+ nsTArray<ipc::Shmem>& aShms) {
+ for (auto& shm : aShms) {
+ aShmAllocator->DeallocShmem(shm);
+ }
+ aShms.Clear();
+}
+
+} // namespace wr
+} // namespace mozilla
diff --git a/gfx/layers/wr/IpcResourceUpdateQueue.h b/gfx/layers/wr/IpcResourceUpdateQueue.h
new file mode 100644
index 0000000000..6096ddbddb
--- /dev/null
+++ b/gfx/layers/wr/IpcResourceUpdateQueue.h
@@ -0,0 +1,195 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GFX_WR_IPCRESOURCEUPDATEQUEUE_H
+#define GFX_WR_IPCRESOURCEUPDATEQUEUE_H
+
+#include "mozilla/layers/WebRenderMessages.h"
+#include "mozilla/layers/RefCountedShmem.h"
+#include "mozilla/layers/TextureClient.h"
+#include "mozilla/webrender/WebRenderTypes.h"
+
+namespace mozilla {
+namespace ipc {
+class IShmemAllocator;
+}
+namespace layers {
+class TextureClient;
+class WebRenderBridgeChild;
+} // namespace layers
+
+namespace wr {
+
+/// ShmSegmentsWriter pushes bytes in a sequence of fixed size shmems for small
+/// allocations and creates dedicated shmems for large allocations.
+class ShmSegmentsWriter {
+ public:
+ ShmSegmentsWriter(layers::WebRenderBridgeChild* aAllocator,
+ size_t aChunkSize);
+ ~ShmSegmentsWriter();
+
+ ShmSegmentsWriter(ShmSegmentsWriter&& aOther) noexcept;
+ ShmSegmentsWriter& operator=(ShmSegmentsWriter&& aOther) noexcept;
+
+ ShmSegmentsWriter(const ShmSegmentsWriter& aOther) = delete;
+ ShmSegmentsWriter& operator=(const ShmSegmentsWriter& aOther) = delete;
+
+ layers::OffsetRange Write(Range<uint8_t> aBytes);
+
+ template <typename T>
+ layers::OffsetRange WriteAsBytes(Range<T> aValues) {
+ return Write(Range<uint8_t>((uint8_t*)aValues.begin().get(),
+ aValues.length() * sizeof(T)));
+ }
+
+ void Flush(nsTArray<layers::RefCountedShmem>& aSmallAllocs,
+ nsTArray<mozilla::ipc::Shmem>& aLargeAllocs);
+
+ void Clear();
+ bool IsEmpty() const;
+
+ layers::WebRenderBridgeChild* WrBridge() const { return mShmAllocator; }
+ size_t ChunkSize() const { return mChunkSize; }
+
+ protected:
+ bool AllocChunk();
+ layers::OffsetRange AllocLargeChunk(size_t aSize);
+
+ nsTArray<layers::RefCountedShmem> mSmallAllocs;
+ nsTArray<mozilla::ipc::Shmem> mLargeAllocs;
+ layers::WebRenderBridgeChild* mShmAllocator;
+ size_t mCursor;
+ size_t mChunkSize;
+};
+
+class ShmSegmentsReader {
+ public:
+ ShmSegmentsReader(const nsTArray<layers::RefCountedShmem>& aSmallShmems,
+ const nsTArray<mozilla::ipc::Shmem>& aLargeShmems);
+
+ bool Read(const layers::OffsetRange& aRange, wr::Vec<uint8_t>& aInto);
+
+ // Get a read pointer, if possible, directly into the shm. If the range has
+ // been broken up into multiple chunks that can't be represented by a single
+ // range, nothing will be returned to indicate failure.
+ Maybe<Range<uint8_t>> GetReadPointer(const layers::OffsetRange& aRange);
+
+ // Get a read pointer, if possible, directly into the shm. Otherwise, copy
+ // it into the Vec and return a pointer to that contiguous memory instead.
+ // If all fails, return nothing.
+ Maybe<Range<uint8_t>> GetReadPointerOrCopy(const layers::OffsetRange& aRange,
+ wr::Vec<uint8_t>& aInto) {
+ if (Maybe<Range<uint8_t>> ptr = GetReadPointer(aRange)) {
+ return ptr;
+ } else {
+ size_t initialLength = aInto.Length();
+ if (Read(aRange, aInto)) {
+ return Some(Range<uint8_t>(aInto.Data() + initialLength,
+ aInto.Length() - initialLength));
+ } else {
+ return Nothing();
+ }
+ }
+ }
+
+ protected:
+ bool ReadLarge(const layers::OffsetRange& aRange, wr::Vec<uint8_t>& aInto);
+
+ Maybe<Range<uint8_t>> GetReadPointerLarge(const layers::OffsetRange& aRange);
+
+ const nsTArray<layers::RefCountedShmem>& mSmallAllocs;
+ const nsTArray<mozilla::ipc::Shmem>& mLargeAllocs;
+ size_t mChunkSize;
+};
+
+class IpcResourceUpdateQueue {
+ public:
+ // Because we are using shmems, the size should be a multiple of the page
+ // size. Each shmem has two guard pages, and the minimum shmem size (at least
+ // one Windows) is 64k which is already quite large for a lot of the resources
+ // we use here. The RefCountedShmem type used to allocate the chunks keeps a
+ // 16 bytes header in the buffer which we account for here as well. So we pick
+ // 64k - 2 * 4k - 16 = 57328 bytes as the default alloc size.
+ explicit IpcResourceUpdateQueue(layers::WebRenderBridgeChild* aAllocator,
+ size_t aChunkSize = 57328);
+
+ IpcResourceUpdateQueue(IpcResourceUpdateQueue&& aOther) noexcept;
+ IpcResourceUpdateQueue& operator=(IpcResourceUpdateQueue&& aOther) noexcept;
+
+ IpcResourceUpdateQueue(const IpcResourceUpdateQueue& aOther) = delete;
+ IpcResourceUpdateQueue& operator=(const IpcResourceUpdateQueue& aOther) =
+ delete;
+
+ // Moves over everything but the subqueues
+ void ReplaceResources(IpcResourceUpdateQueue&& aOther);
+
+ bool AddImage(wr::ImageKey aKey, const ImageDescriptor& aDescriptor,
+ Range<uint8_t> aBytes);
+
+ bool AddBlobImage(wr::BlobImageKey aKey, const ImageDescriptor& aDescriptor,
+ Range<uint8_t> aBytes, ImageIntRect aVisibleRect);
+
+ void AddSharedExternalImage(wr::ExternalImageId aExtId, wr::ImageKey aKey);
+
+ void PushExternalImageForTexture(wr::ExternalImageId aExtId,
+ wr::ImageKey aKey,
+ layers::TextureClient* aTexture,
+ bool aIsUpdate);
+
+ bool UpdateImageBuffer(wr::ImageKey aKey, const ImageDescriptor& aDescriptor,
+ Range<uint8_t> aBytes);
+
+ bool UpdateBlobImage(wr::BlobImageKey aKey,
+ const ImageDescriptor& aDescriptor,
+ Range<uint8_t> aBytes, ImageIntRect aVisibleRect,
+ ImageIntRect aDirtyRect);
+
+ void UpdateSharedExternalImage(ExternalImageId aExtID, ImageKey aKey,
+ ImageIntRect aDirtyRect);
+
+ void SetBlobImageVisibleArea(BlobImageKey aKey, const ImageIntRect& aArea);
+
+ void DeleteImage(wr::ImageKey aKey);
+
+ void DeleteBlobImage(wr::BlobImageKey aKey);
+
+ bool AddRawFont(wr::FontKey aKey, Range<uint8_t> aBytes, uint32_t aIndex);
+
+ bool AddFontDescriptor(wr::FontKey aKey, Range<uint8_t> aBytes,
+ uint32_t aIndex);
+
+ void DeleteFont(wr::FontKey aKey);
+
+ void AddFontInstance(wr::FontInstanceKey aKey, wr::FontKey aFontKey,
+ float aGlyphSize,
+ const wr::FontInstanceOptions* aOptions,
+ const wr::FontInstancePlatformOptions* aPlatformOptions,
+ Range<const gfx::FontVariation> aVariations);
+
+ void DeleteFontInstance(wr::FontInstanceKey aKey);
+
+ void Clear();
+
+ void Flush(nsTArray<layers::OpUpdateResource>& aUpdates,
+ nsTArray<layers::RefCountedShmem>& aSmallAllocs,
+ nsTArray<mozilla::ipc::Shmem>& aLargeAllocs);
+
+ bool IsEmpty() const;
+
+ static void ReleaseShmems(mozilla::ipc::IProtocol*,
+ nsTArray<layers::RefCountedShmem>& aShms);
+ static void ReleaseShmems(mozilla::ipc::IProtocol*,
+ nsTArray<mozilla::ipc::Shmem>& aShms);
+
+ protected:
+ ShmSegmentsWriter mWriter;
+ nsTArray<layers::OpUpdateResource> mUpdates;
+};
+
+} // namespace wr
+} // namespace mozilla
+
+#endif
diff --git a/gfx/layers/wr/OMTAController.cpp b/gfx/layers/wr/OMTAController.cpp
new file mode 100644
index 0000000000..527ec881f6
--- /dev/null
+++ b/gfx/layers/wr/OMTAController.cpp
@@ -0,0 +1,41 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/layers/OMTAController.h"
+
+#include "mozilla/layers/CompositorBridgeParent.h"
+#include "mozilla/layers/CompositorThread.h"
+#include "mozilla/StaticPrefs_layout.h"
+
+namespace mozilla {
+namespace layers {
+
+void OMTAController::NotifyJankedAnimations(
+ JankedAnimations&& aJankedAnimations) const {
+ if (StaticPrefs::layout_animation_prerender_partial_jank()) {
+ return;
+ }
+
+ if (!CompositorThread()) {
+ return;
+ }
+
+ if (!CompositorThread()->IsOnCurrentThread()) {
+ CompositorThread()->Dispatch(NewRunnableMethod<JankedAnimations&&>(
+ "layers::OMTAController::NotifyJankedAnimations", this,
+ &OMTAController::NotifyJankedAnimations, std::move(aJankedAnimations)));
+ return;
+ }
+
+ if (CompositorBridgeParent* bridge =
+ CompositorBridgeParent::GetCompositorBridgeParentFromLayersId(
+ mRootLayersId)) {
+ bridge->NotifyJankedAnimations(aJankedAnimations);
+ }
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/wr/OMTAController.h b/gfx/layers/wr/OMTAController.h
new file mode 100644
index 0000000000..3119650267
--- /dev/null
+++ b/gfx/layers/wr/OMTAController.h
@@ -0,0 +1,44 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_OMTAController_h
+#define mozilla_layers_OMTAController_h
+
+#include <unordered_map>
+
+#include "mozilla/layers/LayersTypes.h" // for LayersId
+#include "nsISerialEventTarget.h"
+#include "nsISupportsImpl.h"
+#include "nsTArrayForwardDeclare.h"
+
+namespace mozilla {
+namespace layers {
+
+/**
+ * This class just delegates the jank animations notification to the compositor
+ * thread from the sampler thread.
+ */
+class OMTAController final {
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(OMTAController)
+
+ public:
+ explicit OMTAController(LayersId aRootLayersId)
+ : mRootLayersId(aRootLayersId) {}
+
+ using JankedAnimations =
+ std::unordered_map<LayersId, nsTArray<uint64_t>, LayersId::HashFn>;
+ void NotifyJankedAnimations(JankedAnimations&& aJankedAnimations) const;
+
+ private:
+ ~OMTAController() = default;
+
+ LayersId mRootLayersId;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_OMTAController_h
diff --git a/gfx/layers/wr/OMTASampler.cpp b/gfx/layers/wr/OMTASampler.cpp
new file mode 100644
index 0000000000..c9616e87e1
--- /dev/null
+++ b/gfx/layers/wr/OMTASampler.cpp
@@ -0,0 +1,248 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/layers/OMTASampler.h"
+
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/layers/CompositorAnimationStorage.h"
+#include "mozilla/layers/CompositorThread.h"
+#include "mozilla/layers/OMTAController.h"
+#include "mozilla/layers/SynchronousTask.h"
+#include "mozilla/layers/WebRenderBridgeParent.h"
+#include "mozilla/webrender/WebRenderAPI.h"
+
+namespace mozilla {
+namespace layers {
+
+StaticMutex OMTASampler::sWindowIdLock;
+StaticAutoPtr<std::unordered_map<uint64_t, RefPtr<OMTASampler>>>
+ OMTASampler::sWindowIdMap;
+
+OMTASampler::OMTASampler(const RefPtr<CompositorAnimationStorage>& aAnimStorage,
+ LayersId aRootLayersId)
+ : mAnimStorage(aAnimStorage),
+ mStorageLock("OMTASampler::mStorageLock"),
+ mThreadIdLock("OMTASampler::mThreadIdLock"),
+ mSampleTimeLock("OMTASampler::mSampleTimeLock"),
+ mIsInTestMode(false) {
+ mController = new OMTAController(aRootLayersId);
+}
+
+void OMTASampler::Destroy() {
+ StaticMutexAutoLock lock(sWindowIdLock);
+ if (mWindowId) {
+ MOZ_ASSERT(sWindowIdMap);
+ sWindowIdMap->erase(wr::AsUint64(*mWindowId));
+ }
+}
+
+void OMTASampler::SetWebRenderWindowId(const wr::WrWindowId& aWindowId) {
+ StaticMutexAutoLock lock(sWindowIdLock);
+ MOZ_ASSERT(!mWindowId);
+ mWindowId = Some(aWindowId);
+ if (!sWindowIdMap) {
+ sWindowIdMap = new std::unordered_map<uint64_t, RefPtr<OMTASampler>>();
+ NS_DispatchToMainThread(
+ NS_NewRunnableFunction("OMTASampler::ClearOnShutdown",
+ [] { ClearOnShutdown(&sWindowIdMap); }));
+ }
+ (*sWindowIdMap)[wr::AsUint64(aWindowId)] = this;
+}
+
+/*static*/
+void OMTASampler::SetSamplerThread(const wr::WrWindowId& aWindowId) {
+ if (RefPtr<OMTASampler> sampler = GetSampler(aWindowId)) {
+ MutexAutoLock lock(sampler->mThreadIdLock);
+ sampler->mSamplerThreadId = Some(PlatformThread::CurrentId());
+ }
+}
+
+/*static*/
+void OMTASampler::Sample(const wr::WrWindowId& aWindowId,
+ wr::Transaction* aTransaction) {
+ if (RefPtr<OMTASampler> sampler = GetSampler(aWindowId)) {
+ wr::TransactionWrapper txn(aTransaction);
+ sampler->Sample(txn);
+ }
+}
+
+void OMTASampler::SetSampleTime(const TimeStamp& aSampleTime) {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+
+ const bool hasAnimations = HasAnimations();
+
+ MutexAutoLock lock(mSampleTimeLock);
+
+ // Reset the previous time stamp if we don't already have any running
+ // animations to avoid using the time which is far behind for newly
+ // started animations.
+ mPreviousSampleTime = hasAnimations ? std::move(mSampleTime) : TimeStamp();
+ mSampleTime = aSampleTime;
+}
+
+void OMTASampler::ResetPreviousSampleTime() {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+ MutexAutoLock lock(mSampleTimeLock);
+
+ mPreviousSampleTime = TimeStamp();
+}
+
+void OMTASampler::Sample(wr::TransactionWrapper& aTxn) {
+ MOZ_ASSERT(IsSamplerThread());
+
+ // If we are in test mode, don't sample with the current time stamp, it will
+ // skew cached animation values.
+ if (mIsInTestMode) {
+ return;
+ }
+
+ TimeStamp sampleTime;
+ TimeStamp previousSampleTime;
+ { // scope lock
+ MutexAutoLock lock(mSampleTimeLock);
+
+ // If mSampleTime is null we're in a startup phase where the
+ // WebRenderBridgeParent hasn't yet provided us with a sample time.
+ // If we're that early there probably aren't any OMTA animations happening
+ // anyway, so using Timestamp::Now() should be fine.
+ sampleTime = mSampleTime.IsNull() ? TimeStamp::Now() : mSampleTime;
+ previousSampleTime = mPreviousSampleTime;
+ }
+
+ WrAnimations animations = SampleAnimations(previousSampleTime, sampleTime);
+
+ aTxn.AppendDynamicProperties(animations.mOpacityArrays,
+ animations.mTransformArrays,
+ animations.mColorArrays);
+}
+
+WrAnimations OMTASampler::SampleAnimations(const TimeStamp& aPreviousSampleTime,
+ const TimeStamp& aSampleTime) {
+ MOZ_ASSERT(IsSamplerThread());
+
+ MutexAutoLock lock(mStorageLock);
+
+ mAnimStorage->SampleAnimations(mController, aPreviousSampleTime, aSampleTime);
+
+ return mAnimStorage->CollectWebRenderAnimations();
+}
+
+OMTAValue OMTASampler::GetOMTAValue(const uint64_t& aId) const {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+ MutexAutoLock lock(mStorageLock);
+
+ return mAnimStorage->GetOMTAValue(aId);
+}
+
+void OMTASampler::SampleForTesting(const Maybe<TimeStamp>& aTestingSampleTime) {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+
+ TimeStamp sampleTime;
+ TimeStamp previousSampleTime;
+ { // scope lock
+ MutexAutoLock timeLock(mSampleTimeLock);
+ if (aTestingSampleTime) {
+ // If we are on testing refresh mode, use the testing time stamp for both
+ // of the previous sample time and the current sample time since unlike
+ // normal refresh mode, the testing mode animations on the compositor are
+ // synchronously composed, so we don't need to worry about the time gap
+ // between the main thread and compositor thread.
+ sampleTime = *aTestingSampleTime;
+ previousSampleTime = *aTestingSampleTime;
+ } else {
+ sampleTime = mSampleTime;
+ previousSampleTime = mPreviousSampleTime;
+ }
+ }
+
+ MutexAutoLock storageLock(mStorageLock);
+ mAnimStorage->SampleAnimations(mController, previousSampleTime, sampleTime);
+}
+
+void OMTASampler::SetAnimations(
+ uint64_t aId, const LayersId& aLayersId,
+ const nsTArray<layers::Animation>& aAnimations) {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+ MutexAutoLock lock(mStorageLock);
+
+ mAnimStorage->SetAnimations(aId, aLayersId, aAnimations);
+}
+
+bool OMTASampler::HasAnimations() const {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+ MutexAutoLock lock(mStorageLock);
+
+ return mAnimStorage->HasAnimations();
+}
+
+void OMTASampler::ClearActiveAnimations(
+ std::unordered_map<uint64_t, wr::WrEpoch>& aActiveAnimations) {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+ MutexAutoLock lock(mStorageLock);
+ for (const auto& id : aActiveAnimations) {
+ mAnimStorage->ClearById(id.first);
+ }
+}
+
+void OMTASampler::RemoveEpochDataPriorTo(
+ std::queue<CompositorAnimationIdsForEpoch>& aCompositorAnimationsToDelete,
+ std::unordered_map<uint64_t, wr::WrEpoch>& aActiveAnimations,
+ const wr::WrEpoch& aRenderedEpoch) {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+ MutexAutoLock lock(mStorageLock);
+
+ while (!aCompositorAnimationsToDelete.empty()) {
+ if (aRenderedEpoch < aCompositorAnimationsToDelete.front().mEpoch) {
+ break;
+ }
+ for (uint64_t id : aCompositorAnimationsToDelete.front().mIds) {
+ const auto activeAnim = aActiveAnimations.find(id);
+ if (activeAnim == aActiveAnimations.end()) {
+ NS_ERROR("Tried to delete invalid animation");
+ continue;
+ }
+ // Check if animation delete request is still valid.
+ if (activeAnim->second <= aCompositorAnimationsToDelete.front().mEpoch) {
+ mAnimStorage->ClearById(id);
+ aActiveAnimations.erase(activeAnim);
+ }
+ }
+ aCompositorAnimationsToDelete.pop();
+ }
+}
+
+bool OMTASampler::IsSamplerThread() const {
+ MutexAutoLock lock(mThreadIdLock);
+ return mSamplerThreadId && PlatformThread::CurrentId() == *mSamplerThreadId;
+}
+
+/*static*/
+already_AddRefed<OMTASampler> OMTASampler::GetSampler(
+ const wr::WrWindowId& aWindowId) {
+ RefPtr<OMTASampler> sampler;
+ StaticMutexAutoLock lock(sWindowIdLock);
+ if (sWindowIdMap) {
+ auto it = sWindowIdMap->find(wr::AsUint64(aWindowId));
+ if (it != sWindowIdMap->end()) {
+ sampler = it->second;
+ }
+ }
+ return sampler.forget();
+}
+
+} // namespace layers
+} // namespace mozilla
+
+void omta_register_sampler(mozilla::wr::WrWindowId aWindowId) {
+ mozilla::layers::OMTASampler::SetSamplerThread(aWindowId);
+}
+
+void omta_sample(mozilla::wr::WrWindowId aWindowId,
+ mozilla::wr::Transaction* aTransaction) {
+ mozilla::layers::OMTASampler::Sample(aWindowId, aTransaction);
+}
+
+void omta_deregister_sampler(mozilla::wr::WrWindowId aWindowId) {}
diff --git a/gfx/layers/wr/OMTASampler.h b/gfx/layers/wr/OMTASampler.h
new file mode 100644
index 0000000000..513c905fa1
--- /dev/null
+++ b/gfx/layers/wr/OMTASampler.h
@@ -0,0 +1,156 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_OMTASampler_h
+#define mozilla_layers_OMTASampler_h
+
+#include <unordered_map>
+#include <queue>
+
+#include "base/platform_thread.h" // for PlatformThreadId
+#include "mozilla/layers/OMTAController.h" // for OMTAController
+#include "mozilla/Atomics.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/StaticMutex.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/webrender/WebRenderTypes.h" // For WrWindowId, WrEpoch, etc.
+
+namespace mozilla {
+
+class TimeStamp;
+
+namespace wr {
+struct Transaction;
+class TransactionWrapper;
+} // namespace wr
+
+namespace layers {
+class Animation;
+class CompositorAnimationStorage;
+class OMTAValue;
+struct CompositorAnimationIdsForEpoch;
+struct LayersId;
+struct WrAnimations;
+
+/**
+ * This interface exposes OMTA methods related to "sampling" (i.e. calculating
+ * animating values) and "". All sampling methods should be called on the
+ * sampler thread, all some of them should be called on the compositor thread.
+ */
+class OMTASampler final {
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(OMTASampler)
+
+ public:
+ OMTASampler(const RefPtr<CompositorAnimationStorage>& aAnimStorage,
+ LayersId aRootLayersId);
+
+ // Whoever creates this sampler is responsible for calling Destroy() on it
+ // before releasing the owning refptr.
+ void Destroy();
+
+ void SetWebRenderWindowId(const wr::WrWindowId& aWindowId);
+
+ /**
+ * This function is invoked from rust on the render backend thread when it
+ * is created. It effectively tells the OMTASampler "the current thread is
+ * the sampler thread for this window id" and allows OMTASampler to remember
+ * which thread it is.
+ */
+ static void SetSamplerThread(const wr::WrWindowId& aWindowId);
+
+ static void Sample(const wr::WrWindowId& aWindowId, wr::Transaction* aTxn);
+
+ /**
+ * Sample all animations, called on the sampler thread.
+ */
+ void Sample(wr::TransactionWrapper& aTxn);
+
+ /**
+ * These funtions get called on the the compositor thread.
+ */
+ void SetSampleTime(const TimeStamp& aSampleTime);
+ void ResetPreviousSampleTime();
+ void SetAnimations(uint64_t aId, const LayersId& aLayersId,
+ const nsTArray<layers::Animation>& aAnimations);
+ bool HasAnimations() const;
+
+ /**
+ * Clear AnimatedValues and Animations data, called on the compositor
+ * thread.
+ */
+ void ClearActiveAnimations(
+ std::unordered_map<uint64_t, wr::Epoch>& aActiveAnimations);
+ void RemoveEpochDataPriorTo(
+ std::queue<CompositorAnimationIdsForEpoch>& aCompositorAnimationsToDelete,
+ std::unordered_map<uint64_t, wr::Epoch>& aActiveAnimations,
+ const wr::Epoch& aRenderedEpoch);
+
+ // Those two methods are for testing called on the compositor thread.
+ OMTAValue GetOMTAValue(const uint64_t& aId) const;
+ /**
+ * There are two possibilities when this function gets called, either 1) in
+ * testing refesh driver mode or 2) in normal refresh driver mode. In the case
+ * of 2) |aTestingSampleTime| should be Nothing() so that we can use
+ * |mPreviousSampleTime| and |mSampleTime| for sampling animations.
+ */
+ void SampleForTesting(const Maybe<TimeStamp>& aTestingSampleTime);
+
+ /**
+ * Returns true if currently on the "sampler thread".
+ */
+ bool IsSamplerThread() const;
+
+ void EnterTestMode() { mIsInTestMode = true; }
+ void LeaveTestMode() { mIsInTestMode = false; }
+
+ protected:
+ ~OMTASampler() = default;
+
+ static already_AddRefed<OMTASampler> GetSampler(
+ const wr::WrWindowId& aWindowId);
+
+ private:
+ WrAnimations SampleAnimations(const TimeStamp& aPreviousSampleTime,
+ const TimeStamp& aSampleTime);
+
+ RefPtr<OMTAController> mController;
+ // Can only be accessed or modified while holding mStorageLock.
+ RefPtr<CompositorAnimationStorage> mAnimStorage;
+ mutable Mutex mStorageLock MOZ_UNANNOTATED;
+
+ // Used to manage the mapping from a WR window id to OMTASampler. These are
+ // only used if WebRender is enabled. Both sWindowIdMap and mWindowId should
+ // only be used while holding the sWindowIdLock. Note that we use a
+ // StaticAutoPtr wrapper on sWindowIdMap to avoid a static initializer for the
+ // unordered_map. This also avoids the initializer/memory allocation in cases
+ // where we're not using WebRender.
+ static StaticMutex sWindowIdLock MOZ_UNANNOTATED;
+ static StaticAutoPtr<std::unordered_map<uint64_t, RefPtr<OMTASampler>>>
+ sWindowIdMap;
+ Maybe<wr::WrWindowId> mWindowId;
+
+ // Lock used to protected mSamplerThreadId
+ mutable Mutex mThreadIdLock MOZ_UNANNOTATED;
+ // If WebRender is enabled, this holds the thread id of the render backend
+ // thread (which is the sampler thread) for the compositor associated with
+ // this OMTASampler instance.
+ Maybe<PlatformThreadId> mSamplerThreadId;
+
+ Mutex mSampleTimeLock MOZ_UNANNOTATED;
+ // Can only be accessed or modified while holding mSampleTimeLock.
+ TimeStamp mSampleTime;
+ // Same as |mSampleTime|, can only be accessed or modified while holding
+ // mSampleTimeLock.
+ // We basically use this time stamp instead of |mSampleTime| to make
+ // animations more in sync with other animations on the main thread.
+ TimeStamp mPreviousSampleTime;
+ Atomic<bool> mIsInTestMode;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_OMTASampler_h
diff --git a/gfx/layers/wr/RenderRootStateManager.cpp b/gfx/layers/wr/RenderRootStateManager.cpp
new file mode 100644
index 0000000000..8ddaa601a9
--- /dev/null
+++ b/gfx/layers/wr/RenderRootStateManager.cpp
@@ -0,0 +1,210 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/layers/RenderRootStateManager.h"
+
+#include "mozilla/layers/WebRenderBridgeChild.h"
+#include "mozilla/layers/WebRenderLayerManager.h"
+
+namespace mozilla {
+namespace layers {
+
+// RenderRootStateManager shares its ref count with the WebRenderLayerManager
+// that created it. You can think of the two classes as being one unit, except
+// there are multiple RenderRootStateManagers per WebRenderLayerManager. Since
+// we need to reference the WebRenderLayerManager and it needs to reference us,
+// this avoids us needing to involve the cycle collector.
+void RenderRootStateManager::AddRef() { mLayerManager->AddRef(); }
+
+void RenderRootStateManager::Release() { mLayerManager->Release(); }
+
+WebRenderBridgeChild* RenderRootStateManager::WrBridge() const {
+ return mLayerManager->WrBridge();
+}
+
+WebRenderCommandBuilder& RenderRootStateManager::CommandBuilder() {
+ return mLayerManager->CommandBuilder();
+}
+
+RenderRootStateManager::WebRenderUserDataRefTable*
+RenderRootStateManager::GetWebRenderUserDataTable() {
+ return mLayerManager->GetWebRenderUserDataTable();
+}
+
+wr::IpcResourceUpdateQueue& RenderRootStateManager::AsyncResourceUpdates() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!mAsyncResourceUpdates) {
+ mAsyncResourceUpdates.emplace(WrBridge());
+
+ RefPtr<Runnable> task = NewRunnableMethod(
+ "RenderRootStateManager::FlushAsyncResourceUpdates", this,
+ &RenderRootStateManager::FlushAsyncResourceUpdates);
+ NS_DispatchToMainThread(task.forget());
+ }
+
+ return mAsyncResourceUpdates.ref();
+}
+
+void RenderRootStateManager::Destroy() {
+ ClearAsyncAnimations();
+
+ if (WrBridge()) {
+ // Just clear ImageKeys, they are deleted during WebRenderAPI destruction.
+ DiscardLocalImages();
+ // CompositorAnimations are cleared by WebRenderBridgeParent.
+ mDiscardedCompositorAnimationsIds.Clear();
+ }
+
+ mActiveCompositorAnimationIds.clear();
+
+ mDestroyed = true;
+}
+
+void RenderRootStateManager::FlushAsyncResourceUpdates() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!mAsyncResourceUpdates) {
+ return;
+ }
+
+ if (!IsDestroyed() && WrBridge()) {
+ WrBridge()->UpdateResources(mAsyncResourceUpdates.ref());
+ }
+
+ mAsyncResourceUpdates.reset();
+}
+
+void RenderRootStateManager::AddImageKeyForDiscard(wr::ImageKey key) {
+ mImageKeysToDelete.AppendElement(key);
+}
+
+void RenderRootStateManager::AddBlobImageKeyForDiscard(wr::BlobImageKey key) {
+ mBlobImageKeysToDelete.AppendElement(key);
+}
+
+void RenderRootStateManager::DiscardImagesInTransaction(
+ wr::IpcResourceUpdateQueue& aResources) {
+ for (const auto& key : mImageKeysToDelete) {
+ aResources.DeleteImage(key);
+ }
+ for (const auto& key : mBlobImageKeysToDelete) {
+ aResources.DeleteBlobImage(key);
+ }
+ mImageKeysToDelete.Clear();
+ mBlobImageKeysToDelete.Clear();
+}
+
+void RenderRootStateManager::DiscardLocalImages() {
+ // Removes images but doesn't tell the parent side about them
+ // This is useful in empty / failed transactions where we created
+ // image keys but didn't tell the parent about them yet.
+ mImageKeysToDelete.Clear();
+ mBlobImageKeysToDelete.Clear();
+}
+
+void RenderRootStateManager::ClearCachedResources() {
+ mActiveCompositorAnimationIds.clear();
+ mDiscardedCompositorAnimationsIds.Clear();
+}
+
+void RenderRootStateManager::AddActiveCompositorAnimationId(uint64_t aId) {
+ // In layers-free mode we track the active compositor animation ids on the
+ // client side so that we don't try to discard the same animation id multiple
+ // times. We could just ignore the multiple-discard on the parent side, but
+ // checking on the content side reduces IPC traffic.
+ mActiveCompositorAnimationIds.insert(aId);
+}
+
+void RenderRootStateManager::AddCompositorAnimationsIdForDiscard(uint64_t aId) {
+ if (mActiveCompositorAnimationIds.erase(aId)) {
+ // For layers-free ensure we don't try to discard an animation id that
+ // wasn't active. We also remove it from mActiveCompositorAnimationIds so we
+ // don't discard it again unless it gets re-activated.
+ mDiscardedCompositorAnimationsIds.AppendElement(aId);
+ }
+}
+
+void RenderRootStateManager::DiscardCompositorAnimations() {
+ if (WrBridge()->IPCOpen() && !mDiscardedCompositorAnimationsIds.IsEmpty()) {
+ WrBridge()->SendDeleteCompositorAnimations(
+ mDiscardedCompositorAnimationsIds);
+ }
+ mDiscardedCompositorAnimationsIds.Clear();
+}
+
+void RenderRootStateManager::RegisterAsyncAnimation(
+ const wr::ImageKey& aKey, SharedSurfacesAnimation* aAnimation) {
+ mAsyncAnimations.insert(std::make_pair(wr::AsUint64(aKey), aAnimation));
+}
+
+void RenderRootStateManager::DeregisterAsyncAnimation(
+ const wr::ImageKey& aKey) {
+ mAsyncAnimations.erase(wr::AsUint64(aKey));
+}
+
+void RenderRootStateManager::ClearAsyncAnimations() {
+ for (const auto& i : mAsyncAnimations) {
+ i.second->Invalidate(this);
+ }
+ mAsyncAnimations.clear();
+}
+
+void RenderRootStateManager::WrReleasedImages(
+ const nsTArray<wr::ExternalImageKeyPair>& aPairs) {
+ // A SharedSurfaceAnimation object's lifetime is tied to its owning
+ // ImageContainer. When the ImageContainer is released,
+ // SharedSurfaceAnimation::Destroy is called which should ensure it is removed
+ // from the layer manager. Whenever the namespace for the
+ // WebRenderLayerManager itself is invalidated (e.g. we changed windows, or
+ // were destroyed ourselves), we callback into the SharedSurfaceAnimation
+ // object to remove its image key for us and any bound surfaces. If, for any
+ // reason, we somehow missed an WrReleasedImages call before the animation
+ // was bound to the layer manager, it will free those associated surfaces on
+ // the next ReleasePreviousFrame call.
+ for (const auto& pair : aPairs) {
+ auto i = mAsyncAnimations.find(wr::AsUint64(pair.key));
+ if (i != mAsyncAnimations.end()) {
+ i->second->ReleasePreviousFrame(this, pair.id);
+ }
+ }
+}
+
+void RenderRootStateManager::AddWebRenderParentCommand(
+ const WebRenderParentCommand& aCmd) {
+ WrBridge()->AddWebRenderParentCommand(aCmd);
+}
+void RenderRootStateManager::UpdateResources(
+ wr::IpcResourceUpdateQueue& aResources) {
+ WrBridge()->UpdateResources(aResources);
+}
+void RenderRootStateManager::AddPipelineIdForCompositable(
+ const wr::PipelineId& aPipelineId, const CompositableHandle& aHandle,
+ CompositableHandleOwner aOwner) {
+ WrBridge()->AddPipelineIdForCompositable(aPipelineId, aHandle, aOwner);
+}
+void RenderRootStateManager::RemovePipelineIdForCompositable(
+ const wr::PipelineId& aPipelineId) {
+ WrBridge()->RemovePipelineIdForCompositable(aPipelineId);
+}
+/// Release TextureClient that is bounded to ImageKey.
+/// It is used for recycling TextureClient.
+void RenderRootStateManager::ReleaseTextureOfImage(const wr::ImageKey& aKey) {
+ WrBridge()->ReleaseTextureOfImage(aKey);
+}
+
+Maybe<wr::FontInstanceKey> RenderRootStateManager::GetFontKeyForScaledFont(
+ gfx::ScaledFont* aScaledFont, wr::IpcResourceUpdateQueue& aResources) {
+ return WrBridge()->GetFontKeyForScaledFont(aScaledFont, aResources);
+}
+
+Maybe<wr::FontKey> RenderRootStateManager::GetFontKeyForUnscaledFont(
+ gfx::UnscaledFont* aUnscaledFont, wr::IpcResourceUpdateQueue& aResources) {
+ return WrBridge()->GetFontKeyForUnscaledFont(aUnscaledFont, aResources);
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/wr/RenderRootStateManager.h b/gfx/layers/wr/RenderRootStateManager.h
new file mode 100644
index 0000000000..6c0221776e
--- /dev/null
+++ b/gfx/layers/wr/RenderRootStateManager.h
@@ -0,0 +1,97 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GFX_RENDERROOTSTATEMANAGER_H
+#define GFX_RENDERROOTSTATEMANAGER_H
+
+#include "mozilla/webrender/WebRenderAPI.h"
+
+#include "mozilla/layers/IpcResourceUpdateQueue.h"
+#include "mozilla/layers/SharedSurfacesChild.h"
+#include "mozilla/layers/WebRenderCommandBuilder.h"
+#include "nsTHashSet.h"
+
+namespace mozilla {
+
+namespace layers {
+
+class RenderRootStateManager {
+ typedef nsTHashSet<RefPtr<WebRenderUserData>> WebRenderUserDataRefTable;
+
+ public:
+ void AddRef();
+ void Release();
+
+ RenderRootStateManager() : mLayerManager(nullptr), mDestroyed(false) {}
+
+ void Destroy();
+ bool IsDestroyed() { return mDestroyed; }
+ wr::IpcResourceUpdateQueue& AsyncResourceUpdates();
+ WebRenderBridgeChild* WrBridge() const;
+ WebRenderCommandBuilder& CommandBuilder();
+ WebRenderUserDataRefTable* GetWebRenderUserDataTable();
+ WebRenderLayerManager* LayerManager() { return mLayerManager; }
+
+ void AddImageKeyForDiscard(wr::ImageKey key);
+ void AddBlobImageKeyForDiscard(wr::BlobImageKey key);
+ void DiscardImagesInTransaction(wr::IpcResourceUpdateQueue& aResources);
+ void DiscardLocalImages();
+
+ void ClearCachedResources();
+
+ // Methods to manage the compositor animation ids. Active animations are still
+ // going, and when they end we discard them and remove them from the active
+ // list.
+ void AddActiveCompositorAnimationId(uint64_t aId);
+ void AddCompositorAnimationsIdForDiscard(uint64_t aId);
+ void DiscardCompositorAnimations();
+
+ void RegisterAsyncAnimation(const wr::ImageKey& aKey,
+ SharedSurfacesAnimation* aAnimation);
+ void DeregisterAsyncAnimation(const wr::ImageKey& aKey);
+ void ClearAsyncAnimations();
+ void WrReleasedImages(const nsTArray<wr::ExternalImageKeyPair>& aPairs);
+
+ void AddWebRenderParentCommand(const WebRenderParentCommand& aCmd);
+ void UpdateResources(wr::IpcResourceUpdateQueue& aResources);
+ void AddPipelineIdForCompositable(const wr::PipelineId& aPipelineId,
+ const CompositableHandle& aHandle,
+ CompositableHandleOwner aOwner);
+ void RemovePipelineIdForCompositable(const wr::PipelineId& aPipelineId);
+ /// Release TextureClient that is bounded to ImageKey.
+ /// It is used for recycling TextureClient.
+ void ReleaseTextureOfImage(const wr::ImageKey& aKey);
+ Maybe<wr::FontInstanceKey> GetFontKeyForScaledFont(
+ gfx::ScaledFont* aScaledFont, wr::IpcResourceUpdateQueue& aResources);
+ Maybe<wr::FontKey> GetFontKeyForUnscaledFont(
+ gfx::UnscaledFont* aUnscaledFont, wr::IpcResourceUpdateQueue& aResources);
+
+ void FlushAsyncResourceUpdates();
+
+ private:
+ WebRenderLayerManager* mLayerManager;
+ Maybe<wr::IpcResourceUpdateQueue> mAsyncResourceUpdates;
+ nsTArray<wr::ImageKey> mImageKeysToDelete;
+ nsTArray<wr::BlobImageKey> mBlobImageKeysToDelete;
+ std::unordered_map<uint64_t, RefPtr<SharedSurfacesAnimation>>
+ mAsyncAnimations;
+
+ // Set of compositor animation ids for which there are active animations (as
+ // of the last transaction) on the compositor side.
+ std::unordered_set<uint64_t> mActiveCompositorAnimationIds;
+ // Compositor animation ids for animations that are done now and that we want
+ // the compositor to discard information for.
+ nsTArray<uint64_t> mDiscardedCompositorAnimationsIds;
+
+ bool mDestroyed;
+
+ friend class WebRenderLayerManager;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif /* GFX_RENDERROOTSTATEMANAGER_H */
diff --git a/gfx/layers/wr/RenderRootTypes.cpp b/gfx/layers/wr/RenderRootTypes.cpp
new file mode 100644
index 0000000000..18385fa85e
--- /dev/null
+++ b/gfx/layers/wr/RenderRootTypes.cpp
@@ -0,0 +1,110 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "RenderRootTypes.h"
+#include "mozilla/layers/WebRenderMessageUtils.h"
+#include "mozilla/layers/WebRenderBridgeChild.h"
+
+namespace mozilla {
+namespace ipc {
+
+void IPDLParamTraits<mozilla::layers::DisplayListData>::Write(
+ IPC::MessageWriter* aWriter, IProtocol* aActor, paramType&& aParam) {
+ WriteIPDLParam(aWriter, aActor, aParam.mIdNamespace);
+ WriteIPDLParam(aWriter, aActor, aParam.mRect);
+ WriteIPDLParam(aWriter, aActor, aParam.mCommands);
+ WriteIPDLParam(aWriter, aActor, std::move(aParam.mDLItems));
+ WriteIPDLParam(aWriter, aActor, std::move(aParam.mDLCache));
+ WriteIPDLParam(aWriter, aActor, std::move(aParam.mDLSpatialTree));
+ WriteIPDLParam(aWriter, aActor, aParam.mDLDesc);
+ WriteIPDLParam(aWriter, aActor, aParam.mRemotePipelineIds);
+ WriteIPDLParam(aWriter, aActor, aParam.mResourceUpdates);
+ WriteIPDLParam(aWriter, aActor, aParam.mSmallShmems);
+ WriteIPDLParam(aWriter, aActor, std::move(aParam.mLargeShmems));
+ WriteIPDLParam(aWriter, aActor, aParam.mScrollData);
+}
+
+bool IPDLParamTraits<mozilla::layers::DisplayListData>::Read(
+ IPC::MessageReader* aReader, IProtocol* aActor, paramType* aResult) {
+ if (ReadIPDLParam(aReader, aActor, &aResult->mIdNamespace) &&
+ ReadIPDLParam(aReader, aActor, &aResult->mRect) &&
+ ReadIPDLParam(aReader, aActor, &aResult->mCommands) &&
+ ReadIPDLParam(aReader, aActor, &aResult->mDLItems) &&
+ ReadIPDLParam(aReader, aActor, &aResult->mDLCache) &&
+ ReadIPDLParam(aReader, aActor, &aResult->mDLSpatialTree) &&
+ ReadIPDLParam(aReader, aActor, &aResult->mDLDesc) &&
+ ReadIPDLParam(aReader, aActor, &aResult->mRemotePipelineIds) &&
+ ReadIPDLParam(aReader, aActor, &aResult->mResourceUpdates) &&
+ ReadIPDLParam(aReader, aActor, &aResult->mSmallShmems) &&
+ ReadIPDLParam(aReader, aActor, &aResult->mLargeShmems) &&
+ ReadIPDLParam(aReader, aActor, &aResult->mScrollData)) {
+ return true;
+ }
+ return false;
+}
+
+void WriteScrollUpdates(IPC::MessageWriter* aWriter, IProtocol* aActor,
+ layers::ScrollUpdatesMap& aParam) {
+ // ICK: we need to manually serialize this map because
+ // nsDataHashTable doesn't support it (and other maps cause other issues)
+ WriteIPDLParam(aWriter, aActor, aParam.Count());
+ for (auto it = aParam.ConstIter(); !it.Done(); it.Next()) {
+ WriteIPDLParam(aWriter, aActor, it.Key());
+ WriteIPDLParam(aWriter, aActor, it.Data());
+ }
+}
+
+bool ReadScrollUpdates(IPC::MessageReader* aReader, IProtocol* aActor,
+ layers::ScrollUpdatesMap* aResult) {
+ // Manually deserialize mScrollUpdates as a stream of K,V pairs
+ uint32_t count;
+ if (!ReadIPDLParam(aReader, aActor, &count)) {
+ return false;
+ }
+
+ layers::ScrollUpdatesMap map(count);
+ for (size_t i = 0; i < count; ++i) {
+ layers::ScrollableLayerGuid::ViewID key;
+ nsTArray<mozilla::ScrollPositionUpdate> data;
+ if (!ReadIPDLParam(aReader, aActor, &key) ||
+ !ReadIPDLParam(aReader, aActor, &data)) {
+ return false;
+ }
+ map.InsertOrUpdate(key, std::move(data));
+ }
+
+ MOZ_RELEASE_ASSERT(map.Count() == count);
+ *aResult = std::move(map);
+ return true;
+}
+
+void IPDLParamTraits<mozilla::layers::TransactionData>::Write(
+ IPC::MessageWriter* aWriter, IProtocol* aActor, paramType&& aParam) {
+ WriteIPDLParam(aWriter, aActor, aParam.mIdNamespace);
+ WriteIPDLParam(aWriter, aActor, aParam.mCommands);
+ WriteIPDLParam(aWriter, aActor, aParam.mResourceUpdates);
+ WriteIPDLParam(aWriter, aActor, aParam.mSmallShmems);
+ WriteIPDLParam(aWriter, aActor, std::move(aParam.mLargeShmems));
+ WriteScrollUpdates(aWriter, aActor, aParam.mScrollUpdates);
+ WriteIPDLParam(aWriter, aActor, aParam.mPaintSequenceNumber);
+}
+
+bool IPDLParamTraits<mozilla::layers::TransactionData>::Read(
+ IPC::MessageReader* aReader, IProtocol* aActor, paramType* aResult) {
+ if (ReadIPDLParam(aReader, aActor, &aResult->mIdNamespace) &&
+ ReadIPDLParam(aReader, aActor, &aResult->mCommands) &&
+ ReadIPDLParam(aReader, aActor, &aResult->mResourceUpdates) &&
+ ReadIPDLParam(aReader, aActor, &aResult->mSmallShmems) &&
+ ReadIPDLParam(aReader, aActor, &aResult->mLargeShmems) &&
+ ReadScrollUpdates(aReader, aActor, &aResult->mScrollUpdates) &&
+ ReadIPDLParam(aReader, aActor, &aResult->mPaintSequenceNumber)) {
+ return true;
+ }
+ return false;
+}
+
+} // namespace ipc
+} // namespace mozilla
diff --git a/gfx/layers/wr/RenderRootTypes.h b/gfx/layers/wr/RenderRootTypes.h
new file mode 100644
index 0000000000..9921a98f0e
--- /dev/null
+++ b/gfx/layers/wr/RenderRootTypes.h
@@ -0,0 +1,75 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GFX_RENDERROOTTYPES_H
+#define GFX_RENDERROOTTYPES_H
+
+#include "mozilla/webrender/WebRenderAPI.h"
+#include "mozilla/webrender/WebRenderTypes.h"
+#include "mozilla/layers/WebRenderMessages.h"
+#include "mozilla/layers/WebRenderScrollData.h"
+
+namespace mozilla {
+
+namespace layers {
+
+struct DisplayListData {
+ wr::IdNamespace mIdNamespace;
+ LayoutDeviceRect mRect;
+ nsTArray<WebRenderParentCommand> mCommands;
+ Maybe<mozilla::ipc::ByteBuf> mDLItems;
+ Maybe<mozilla::ipc::ByteBuf> mDLCache;
+ Maybe<mozilla::ipc::ByteBuf> mDLSpatialTree;
+ wr::BuiltDisplayListDescriptor mDLDesc;
+ nsTArray<wr::PipelineId> mRemotePipelineIds;
+ nsTArray<OpUpdateResource> mResourceUpdates;
+ nsTArray<RefCountedShmem> mSmallShmems;
+ nsTArray<mozilla::ipc::Shmem> mLargeShmems;
+ Maybe<WebRenderScrollData> mScrollData;
+};
+
+struct TransactionData {
+ wr::IdNamespace mIdNamespace;
+ nsTArray<WebRenderParentCommand> mCommands;
+ nsTArray<OpUpdateResource> mResourceUpdates;
+ nsTArray<RefCountedShmem> mSmallShmems;
+ nsTArray<mozilla::ipc::Shmem> mLargeShmems;
+ ScrollUpdatesMap mScrollUpdates;
+ uint32_t mPaintSequenceNumber;
+};
+
+typedef Maybe<TransactionData> MaybeTransactionData;
+
+} // namespace layers
+
+namespace ipc {
+
+template <>
+struct IPDLParamTraits<mozilla::layers::DisplayListData> {
+ typedef mozilla::layers::DisplayListData paramType;
+
+ static void Write(IPC::MessageWriter* aWriter, IProtocol* aActor,
+ paramType&& aParam);
+
+ static bool Read(IPC::MessageReader* aReader, IProtocol* aActor,
+ paramType* aResult);
+};
+
+template <>
+struct IPDLParamTraits<mozilla::layers::TransactionData> {
+ typedef mozilla::layers::TransactionData paramType;
+
+ static void Write(IPC::MessageWriter* aWriter, IProtocol* aActor,
+ paramType&& aParam);
+
+ static bool Read(IPC::MessageReader* aReader, IProtocol* aActor,
+ paramType* aResult);
+};
+
+} // namespace ipc
+} // namespace mozilla
+
+#endif /* GFX_RENDERROOTTYPES_H */
diff --git a/gfx/layers/wr/StackingContextHelper.cpp b/gfx/layers/wr/StackingContextHelper.cpp
new file mode 100644
index 0000000000..2e0ff1e0c1
--- /dev/null
+++ b/gfx/layers/wr/StackingContextHelper.cpp
@@ -0,0 +1,275 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/layers/StackingContextHelper.h"
+
+#include "mozilla/PresShell.h"
+#include "mozilla/gfx/Point.h"
+#include "mozilla/gfx/Matrix.h"
+#include "UnitTransforms.h"
+#include "nsDisplayList.h"
+#include "mozilla/dom/BrowserChild.h"
+#include "nsLayoutUtils.h"
+#include "ActiveLayerTracker.h"
+
+namespace mozilla {
+namespace layers {
+using namespace gfx;
+
+StackingContextHelper::StackingContextHelper()
+ : mBuilder(nullptr),
+ mScale(1.0f, 1.0f),
+ mAffectsClipPositioning(false),
+ mDeferredTransformItem(nullptr),
+ mRasterizeLocally(false) {
+ // mOrigin remains at 0,0
+}
+
+static nsSize ComputeDesiredDisplaySizeForAnimation(nsIFrame* aContainerFrame) {
+ // Use the size of the nearest widget as the maximum size. This
+ // is important since it might be a popup that is bigger than the
+ // pres context's size.
+ nsPresContext* presContext = aContainerFrame->PresContext();
+ nsIWidget* widget = aContainerFrame->GetNearestWidget();
+ if (widget) {
+ return LayoutDevicePixel::ToAppUnits(widget->GetClientSize(),
+ presContext->AppUnitsPerDevPixel());
+ }
+
+ return presContext->GetVisibleArea().Size();
+}
+
+/* static */
+MatrixScales ChooseScale(nsIFrame* aContainerFrame,
+ nsDisplayItem* aContainerItem,
+ const nsRect& aVisibleRect, float aXScale,
+ float aYScale, const Matrix& aTransform2d,
+ bool aCanDraw2D) {
+ MatrixScales scale;
+ // XXX Should we do something for 3D transforms?
+ if (aCanDraw2D && !aContainerFrame->Combines3DTransformWithAncestors() &&
+ !aContainerFrame->HasPerspective()) {
+ // If the container's transform is animated off main thread, fix a suitable
+ // scale size for animation
+ if (aContainerItem &&
+ aContainerItem->GetType() == DisplayItemType::TYPE_TRANSFORM &&
+ // FIXME: What we need is only transform, rotate, and scale, not
+ // translate, so it's be better to use a property set, instead of
+ // display item type here.
+ EffectCompositor::HasAnimationsForCompositor(
+ aContainerFrame, DisplayItemType::TYPE_TRANSFORM)) {
+ nsSize displaySize =
+ ComputeDesiredDisplaySizeForAnimation(aContainerFrame);
+ // compute scale using the animation on the container, taking ancestors in
+ // to account
+ nsSize scaledVisibleSize = nsSize(aVisibleRect.Width() * aXScale,
+ aVisibleRect.Height() * aYScale);
+ scale = nsLayoutUtils::ComputeSuitableScaleForAnimation(
+ aContainerFrame, scaledVisibleSize, displaySize);
+ // multiply by the scale inherited from ancestors--we use a uniform
+ // scale factor to prevent blurring when the layer is rotated.
+ float incomingScale = std::max(aXScale, aYScale);
+ scale = scale * ScaleFactor<UnknownUnits, UnknownUnits>(incomingScale);
+ } else {
+ // Scale factors are normalized to a power of 2 to reduce the number of
+ // resolution changes
+ scale = aTransform2d.ScaleFactors();
+ // For frames with a changing scale transform round scale factors up to
+ // nearest power-of-2 boundary so that we don't keep having to redraw
+ // the content as it scales up and down. Rounding up to nearest
+ // power-of-2 boundary ensures we never scale up, only down --- avoiding
+ // jaggies. It also ensures we never scale down by more than a factor of
+ // 2, avoiding bad downscaling quality.
+ Matrix frameTransform;
+ if (ActiveLayerTracker::IsScaleSubjectToAnimation(aContainerFrame)) {
+ scale.xScale = gfxUtils::ClampToScaleFactor(scale.xScale);
+ scale.yScale = gfxUtils::ClampToScaleFactor(scale.yScale);
+
+ // Limit animated scale factors to not grow excessively beyond the
+ // display size.
+ nsSize maxScale(4, 4);
+ if (!aVisibleRect.IsEmpty()) {
+ nsSize displaySize =
+ ComputeDesiredDisplaySizeForAnimation(aContainerFrame);
+ maxScale = Max(maxScale, displaySize / aVisibleRect.Size());
+ }
+ if (scale.xScale > maxScale.width) {
+ scale.xScale = gfxUtils::ClampToScaleFactor(maxScale.width, true);
+ }
+ if (scale.yScale > maxScale.height) {
+ scale.yScale = gfxUtils::ClampToScaleFactor(maxScale.height, true);
+ }
+ } else {
+ // XXX Do we need to move nearly-integer values to integers here?
+ }
+ }
+ // If the scale factors are too small, just use 1.0. The content is being
+ // scaled out of sight anyway.
+ if (fabs(scale.xScale) < 1e-8 || fabs(scale.yScale) < 1e-8) {
+ scale = MatrixScales(1.0, 1.0);
+ }
+ } else {
+ scale = MatrixScales(1.0, 1.0);
+ }
+
+ // Prevent the scale from getting too large, to avoid excessive memory
+ // allocation. Usually memory allocation is limited by the visible region,
+ // which should be restricted to the display port. But at very large scales
+ // the visible region itself can become excessive due to rounding errors.
+ // Clamping the scale here prevents that.
+ return MatrixScales(std::min(scale.xScale, 32768.0f),
+ std::min(scale.yScale, 32768.0f));
+}
+
+StackingContextHelper::StackingContextHelper(
+ const StackingContextHelper& aParentSC, const ActiveScrolledRoot* aAsr,
+ nsIFrame* aContainerFrame, nsDisplayItem* aContainerItem,
+ wr::DisplayListBuilder& aBuilder, const wr::StackingContextParams& aParams,
+ const LayoutDeviceRect& aBounds)
+ : mBuilder(&aBuilder),
+ mScale(1.0f, 1.0f),
+ mDeferredTransformItem(aParams.mDeferredTransformItem),
+ mRasterizeLocally(aParams.mRasterizeLocally ||
+ aParentSC.mRasterizeLocally) {
+ MOZ_ASSERT(!aContainerItem || aContainerItem->CreatesStackingContextHelper());
+
+ mOrigin = aParentSC.mOrigin + aBounds.TopLeft();
+ // Compute scale for fallback rendering. We don't try to guess a scale for 3d
+ // transformed items
+
+ if (aParams.mBoundTransform) {
+ gfx::Matrix transform2d;
+ bool canDraw2D = aParams.mBoundTransform->CanDraw2D(&transform2d);
+ if (canDraw2D &&
+ aParams.reference_frame_kind != wr::WrReferenceFrameKind::Perspective &&
+ !aContainerFrame->Combines3DTransformWithAncestors()) {
+ mInheritedTransform = transform2d * aParentSC.mInheritedTransform;
+
+ int32_t apd = aContainerFrame->PresContext()->AppUnitsPerDevPixel();
+ nsRect r = LayoutDevicePixel::ToAppUnits(aBounds, apd);
+ mScale = ChooseScale(aContainerFrame, aContainerItem, r,
+ aParentSC.mScale.xScale, aParentSC.mScale.yScale,
+ mInheritedTransform,
+ /* aCanDraw2D = */ true);
+ } else {
+ mScale = gfx::MatrixScales(1.0f, 1.0f);
+ mInheritedTransform = gfx::Matrix::Scaling(1.f, 1.f);
+ }
+
+ if (aParams.mAnimated) {
+ mSnappingSurfaceTransform = gfx::Matrix::Scaling(mScale);
+ } else {
+ mSnappingSurfaceTransform =
+ transform2d * aParentSC.mSnappingSurfaceTransform;
+ }
+
+ } else if (aParams.reference_frame_kind ==
+ wr::WrReferenceFrameKind::Transform &&
+ aContainerItem &&
+ aContainerItem->GetType() == DisplayItemType::TYPE_ASYNC_ZOOM &&
+ aContainerItem->Frame()) {
+ float resolution = aContainerItem->Frame()->PresShell()->GetResolution();
+ gfx::Matrix transform = gfx::Matrix::Scaling(resolution, resolution);
+
+ mInheritedTransform = transform * aParentSC.mInheritedTransform;
+ mScale =
+ ScaleFactor<UnknownUnits, UnknownUnits>(resolution) * aParentSC.mScale;
+
+ MOZ_ASSERT(!aParams.mAnimated);
+ mSnappingSurfaceTransform = transform * aParentSC.mSnappingSurfaceTransform;
+
+ } else if (!aAsr && !aContainerFrame && !aContainerItem &&
+ aParams.mRootReferenceFrame) {
+ // this is the root stacking context helper
+ Scale2D resolution;
+
+ // If we are in a remote browser, then apply scaling from ancestor browsers
+ if (mozilla::dom::BrowserChild* browserChild =
+ mozilla::dom::BrowserChild::GetFrom(
+ aParams.mRootReferenceFrame->PresShell())) {
+ resolution = browserChild->GetEffectsInfo().mRasterScale;
+ }
+
+ gfx::Matrix transform =
+ gfx::Matrix::Scaling(resolution.xScale, resolution.yScale);
+
+ mInheritedTransform = transform * aParentSC.mInheritedTransform;
+ mScale = aParentSC.mScale * resolution;
+
+ MOZ_ASSERT(!aParams.mAnimated);
+ mSnappingSurfaceTransform = transform * aParentSC.mSnappingSurfaceTransform;
+
+ } else {
+ mInheritedTransform = aParentSC.mInheritedTransform;
+ mScale = aParentSC.mScale;
+ }
+
+ auto rasterSpace =
+ mRasterizeLocally
+ ? wr::RasterSpace::Local(std::max(mScale.xScale, mScale.yScale))
+ : wr::RasterSpace::Screen();
+
+ MOZ_ASSERT(!aParams.clip.IsNone());
+ mReferenceFrameId = mBuilder->PushStackingContext(
+ aParams, wr::ToLayoutRect(aBounds), rasterSpace);
+
+ if (mReferenceFrameId) {
+ mSpaceAndClipChainHelper.emplace(aBuilder, mReferenceFrameId.ref());
+ }
+
+ mAffectsClipPositioning =
+ mReferenceFrameId.isSome() || (aBounds.TopLeft() != LayoutDevicePoint());
+
+ // If the parent stacking context has a deferred transform item, inherit it
+ // into this stacking context, as long as the ASR hasn't changed. Refer to
+ // the comments on StackingContextHelper::mDeferredTransformItem for an
+ // explanation of what goes in these fields.
+ if (aParentSC.mDeferredTransformItem &&
+ aAsr == aParentSC.mDeferredTransformItem->GetActiveScrolledRoot()) {
+ if (mDeferredTransformItem) {
+ // If we are deferring another transform, put the combined transform from
+ // all the ancestor deferred items into mDeferredAncestorTransform
+ mDeferredAncestorTransform = aParentSC.GetDeferredTransformMatrix();
+ } else {
+ // We are not deferring another transform, so we can just inherit the
+ // parent stacking context's deferred data without any modification.
+ mDeferredTransformItem = aParentSC.mDeferredTransformItem;
+ mDeferredAncestorTransform = aParentSC.mDeferredAncestorTransform;
+ }
+ }
+}
+
+StackingContextHelper::~StackingContextHelper() {
+ if (mBuilder) {
+ mSpaceAndClipChainHelper.reset();
+ mBuilder->PopStackingContext(mReferenceFrameId.isSome());
+ }
+}
+
+nsDisplayTransform* StackingContextHelper::GetDeferredTransformItem() const {
+ return mDeferredTransformItem;
+}
+
+Maybe<gfx::Matrix4x4> StackingContextHelper::GetDeferredTransformMatrix()
+ const {
+ if (mDeferredTransformItem) {
+ // See the comments on StackingContextHelper::mDeferredTransformItem for
+ // an explanation of what's stored in mDeferredTransformItem and
+ // mDeferredAncestorTransform. Here we need to return the combined transform
+ // transform from all the deferred ancestors, including
+ // mDeferredTransformItem.
+ gfx::Matrix4x4 result = mDeferredTransformItem->GetTransform().GetMatrix();
+ if (mDeferredAncestorTransform) {
+ result = result * *mDeferredAncestorTransform;
+ }
+ return Some(result);
+ } else {
+ return Nothing();
+ }
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/wr/StackingContextHelper.h b/gfx/layers/wr/StackingContextHelper.h
new file mode 100644
index 0000000000..368449a2fd
--- /dev/null
+++ b/gfx/layers/wr/StackingContextHelper.h
@@ -0,0 +1,129 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GFX_STACKINGCONTEXTHELPER_H
+#define GFX_STACKINGCONTEXTHELPER_H
+
+#include "mozilla/Attributes.h"
+#include "mozilla/gfx/MatrixFwd.h"
+#include "mozilla/webrender/WebRenderAPI.h"
+#include "mozilla/webrender/WebRenderTypes.h"
+#include "Units.h"
+
+namespace mozilla {
+
+class nsDisplayTransform;
+struct ActiveScrolledRoot;
+
+namespace layers {
+
+/**
+ * This is a helper class that pushes/pops a stacking context, and manages
+ * some of the coordinate space transformations needed.
+ */
+class MOZ_RAII StackingContextHelper {
+ public:
+ StackingContextHelper(const StackingContextHelper& aParentSC,
+ const ActiveScrolledRoot* aAsr,
+ nsIFrame* aContainerFrame,
+ nsDisplayItem* aContainerItem,
+ wr::DisplayListBuilder& aBuilder,
+ const wr::StackingContextParams& aParams,
+ const LayoutDeviceRect& aBounds = LayoutDeviceRect());
+
+ // This version of the constructor should only be used at the root level
+ // of the tree, so that we have a StackingContextHelper to pass down into
+ // the RenderLayer traversal, but don't actually want it to push a stacking
+ // context on the display list builder.
+ StackingContextHelper();
+
+ // Pops the stacking context, if one was pushed during the constructor.
+ ~StackingContextHelper();
+
+ // Export the inherited scale
+ gfx::MatrixScales GetInheritedScale() const { return mScale; }
+
+ const gfx::Matrix& GetInheritedTransform() const {
+ return mInheritedTransform;
+ }
+
+ const gfx::Matrix& GetSnappingSurfaceTransform() const {
+ return mSnappingSurfaceTransform;
+ }
+
+ nsDisplayTransform* GetDeferredTransformItem() const;
+ Maybe<gfx::Matrix4x4> GetDeferredTransformMatrix() const;
+
+ bool AffectsClipPositioning() const { return mAffectsClipPositioning; }
+ Maybe<wr::WrSpatialId> ReferenceFrameId() const { return mReferenceFrameId; }
+
+ const LayoutDevicePoint& GetOrigin() const { return mOrigin; }
+
+ private:
+ wr::DisplayListBuilder* mBuilder;
+ gfx::MatrixScales mScale;
+ gfx::Matrix mInheritedTransform;
+ LayoutDevicePoint mOrigin;
+
+ // The "snapping surface" defines the space that we want to snap in.
+ // You can think of it as the nearest physical surface.
+ // Animated transforms create a new snapping surface, so that changes to their
+ // transform don't affect the snapping of their contents. Non-animated
+ // transforms do *not* create a new snapping surface, so that for example the
+ // existence of a non-animated identity transform does not affect snapping.
+ gfx::Matrix mSnappingSurfaceTransform;
+ bool mAffectsClipPositioning;
+ Maybe<wr::WrSpatialId> mReferenceFrameId;
+ Maybe<wr::SpaceAndClipChainHelper> mSpaceAndClipChainHelper;
+
+ // The deferred transform item is used when building the WebRenderScrollData
+ // structure. The backstory is that APZ needs to know about transforms that
+ // apply to the different APZC instances. Prior to bug 1423370, we would do
+ // this by creating a new WebRenderLayerScrollData for each nsDisplayTransform
+ // item we encountered. However, this was unnecessarily expensive because it
+ // turned out a lot of nsDisplayTransform items didn't have new ASRs defined
+ // as descendants, so we'd create the WebRenderLayerScrollData and send it
+ // over to APZ even though the transform information was not needed in that
+ // case.
+ //
+ // In bug 1423370 and friends, this was optimized by "deferring" a
+ // nsDisplayTransform item when we encountered it during display list
+ // traversal. If we found a descendant of that transform item that had a
+ // new ASR or otherwise was "relevant to APZ", we would then pluck the
+ // transform matrix off the deferred item and put it on the
+ // WebRenderLayerScrollData instance created for that APZ-relevant descendant.
+ //
+ // One complication with this is if there are multiple nsDisplayTransform
+ // items in the ancestor chain for the APZ-relevant item. As we traverse the
+ // display list, we will defer the outermost nsDisplayTransform item, and when
+ // we encounter the next one we will need to merge it with the already-
+ // deferred one somehow. What we do in this case is have
+ // mDeferredTransformItem always point to the "innermost" deferred transform
+ // item (i.e. the closest ancestor nsDisplayTransform item of the item that
+ // created this StackingContextHelper). And then we use
+ // mDeferredAncestorTransform to store the product of all the other transforms
+ // that were deferred. As a result, there is an invariant here that if
+ // mDeferredTransformItem is nullptr, mDeferredAncestorTransform will also
+ // be Nothing(). Note that we can only do this if the nsDisplayTransform items
+ // share the same ASR. If we are processing an nsDisplayTransform item with a
+ // different ASR than the previously-deferred item, we assume that the
+ // previously-deferred transform will get sent to APZ as part of a separate
+ // WebRenderLayerScrollData item, and so we don't need to bother with any
+ // merging. (The merging probably wouldn't even make sense because the
+ // coordinate spaces might be different in the face of async scrolling). This
+ // behaviour of forcing a WebRenderLayerScrollData item to be generated when
+ // the ASR changes is implemented in
+ // WebRenderCommandBuilder::CreateWebRenderCommandsFromDisplayList.
+ nsDisplayTransform* mDeferredTransformItem;
+ Maybe<gfx::Matrix4x4> mDeferredAncestorTransform;
+
+ bool mRasterizeLocally;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif /* GFX_STACKINGCONTEXTHELPER_H */
diff --git a/gfx/layers/wr/WebRenderBridgeChild.cpp b/gfx/layers/wr/WebRenderBridgeChild.cpp
new file mode 100644
index 0000000000..ff6ec72231
--- /dev/null
+++ b/gfx/layers/wr/WebRenderBridgeChild.cpp
@@ -0,0 +1,602 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/layers/WebRenderBridgeChild.h"
+
+#include "gfxPlatform.h"
+#include "mozilla/StaticPrefs_gfx.h"
+#include "mozilla/layers/CompositableClient.h"
+#include "mozilla/layers/CompositorBridgeChild.h"
+#include "mozilla/layers/ImageDataSerializer.h"
+#include "mozilla/layers/IpcResourceUpdateQueue.h"
+#include "mozilla/layers/StackingContextHelper.h"
+#include "mozilla/layers/PTextureChild.h"
+#include "mozilla/layers/WebRenderLayerManager.h"
+#include "mozilla/webrender/WebRenderAPI.h"
+#include "PDMFactory.h"
+
+namespace mozilla {
+namespace layers {
+
+using namespace mozilla::gfx;
+
+WebRenderBridgeChild::WebRenderBridgeChild(const wr::PipelineId& aPipelineId)
+ : mIsInTransaction(false),
+ mIsInClearCachedResources(false),
+ mIdNamespace{0},
+ mResourceId(0),
+ mPipelineId(aPipelineId),
+ mManager(nullptr),
+ mIPCOpen(false),
+ mDestroyed(false),
+ mSentDisplayList(false),
+ mFontKeysDeleted(0),
+ mFontInstanceKeysDeleted(0) {}
+
+WebRenderBridgeChild::~WebRenderBridgeChild() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mDestroyed);
+}
+
+void WebRenderBridgeChild::Destroy(bool aIsSync) {
+ if (!IPCOpen()) {
+ return;
+ }
+
+ DoDestroy();
+
+ if (aIsSync) {
+ SendShutdownSync();
+ } else {
+ SendShutdown();
+ }
+}
+
+void WebRenderBridgeChild::ActorDestroy(ActorDestroyReason why) { DoDestroy(); }
+
+void WebRenderBridgeChild::DoDestroy() {
+ if (RefCountedShm::IsValid(mResourceShm) &&
+ RefCountedShm::Release(mResourceShm) == 0) {
+ RefCountedShm::Dealloc(this, mResourceShm);
+ mResourceShm = RefCountedShmem();
+ }
+
+ // mDestroyed is used to prevent calling Send__delete__() twice.
+ // When this function is called from CompositorBridgeChild::Destroy().
+ // mActiveResourceTracker is not cleared here, since it is
+ // used by PersistentBufferProviderShared.
+ mDestroyed = true;
+ mManager = nullptr;
+}
+
+void WebRenderBridgeChild::AddWebRenderParentCommand(
+ const WebRenderParentCommand& aCmd) {
+ mParentCommands.AppendElement(aCmd);
+}
+
+void WebRenderBridgeChild::BeginTransaction() {
+ MOZ_ASSERT(!mDestroyed);
+
+ UpdateFwdTransactionId();
+ mIsInTransaction = true;
+}
+
+void WebRenderBridgeChild::UpdateResources(
+ wr::IpcResourceUpdateQueue& aResources) {
+ if (!IPCOpen()) {
+ aResources.Clear();
+ return;
+ }
+
+ if (aResources.IsEmpty()) {
+ return;
+ }
+
+ nsTArray<OpUpdateResource> resourceUpdates;
+ nsTArray<RefCountedShmem> smallShmems;
+ nsTArray<ipc::Shmem> largeShmems;
+ aResources.Flush(resourceUpdates, smallShmems, largeShmems);
+
+ this->SendUpdateResources(mIdNamespace, resourceUpdates, smallShmems,
+ std::move(largeShmems));
+}
+
+bool WebRenderBridgeChild::EndTransaction(
+ DisplayListData&& aDisplayListData, TransactionId aTransactionId,
+ bool aContainsSVGGroup, const mozilla::VsyncId& aVsyncId,
+ const mozilla::TimeStamp& aVsyncStartTime,
+ const mozilla::TimeStamp& aRefreshStartTime,
+ const mozilla::TimeStamp& aTxnStartTime, const nsCString& aTxnURL) {
+ MOZ_ASSERT(!mDestroyed);
+ MOZ_ASSERT(mIsInTransaction);
+
+ TimeStamp fwdTime = TimeStamp::Now();
+
+ aDisplayListData.mCommands = std::move(mParentCommands);
+ aDisplayListData.mIdNamespace = mIdNamespace;
+
+ nsTArray<CompositionPayload> payloads;
+ if (mManager) {
+ mManager->TakeCompositionPayloads(payloads);
+ }
+
+ mSentDisplayList = true;
+ bool ret = this->SendSetDisplayList(
+ std::move(aDisplayListData), mDestroyedActors, GetFwdTransactionId(),
+ aTransactionId, aContainsSVGGroup, aVsyncId, aVsyncStartTime,
+ aRefreshStartTime, aTxnStartTime, aTxnURL, fwdTime, payloads);
+
+ // With multiple render roots, we may not have sent all of our
+ // mParentCommands, so go ahead and go through our mParentCommands and ensure
+ // they get sent.
+ ProcessWebRenderParentCommands();
+ mDestroyedActors.Clear();
+ mIsInTransaction = false;
+
+ return ret;
+}
+
+void WebRenderBridgeChild::EndEmptyTransaction(
+ const FocusTarget& aFocusTarget, Maybe<TransactionData>&& aTransactionData,
+ TransactionId aTransactionId, const mozilla::VsyncId& aVsyncId,
+ const mozilla::TimeStamp& aVsyncStartTime,
+ const mozilla::TimeStamp& aRefreshStartTime,
+ const mozilla::TimeStamp& aTxnStartTime, const nsCString& aTxnURL) {
+ MOZ_ASSERT(!mDestroyed);
+ MOZ_ASSERT(mIsInTransaction);
+
+ TimeStamp fwdTime = TimeStamp::Now();
+
+ if (aTransactionData) {
+ aTransactionData->mCommands = std::move(mParentCommands);
+ }
+
+ nsTArray<CompositionPayload> payloads;
+ if (mManager) {
+ mManager->TakeCompositionPayloads(payloads);
+ }
+
+ this->SendEmptyTransaction(
+ aFocusTarget, std::move(aTransactionData), mDestroyedActors,
+ GetFwdTransactionId(), aTransactionId, aVsyncId, aVsyncStartTime,
+ aRefreshStartTime, aTxnStartTime, aTxnURL, fwdTime, payloads);
+
+ // With multiple render roots, we may not have sent all of our
+ // mParentCommands, so go ahead and go through our mParentCommands and ensure
+ // they get sent.
+ ProcessWebRenderParentCommands();
+ mDestroyedActors.Clear();
+ mIsInTransaction = false;
+}
+
+void WebRenderBridgeChild::ProcessWebRenderParentCommands() {
+ MOZ_ASSERT(!mDestroyed);
+
+ if (!mParentCommands.IsEmpty()) {
+ this->SendParentCommands(mIdNamespace, mParentCommands);
+ mParentCommands.Clear();
+ }
+}
+
+void WebRenderBridgeChild::AddPipelineIdForCompositable(
+ const wr::PipelineId& aPipelineId, const CompositableHandle& aHandle,
+ CompositableHandleOwner aOwner) {
+ AddWebRenderParentCommand(
+ OpAddPipelineIdForCompositable(aPipelineId, aHandle, aOwner));
+}
+
+void WebRenderBridgeChild::RemovePipelineIdForCompositable(
+ const wr::PipelineId& aPipelineId) {
+ AddWebRenderParentCommand(OpRemovePipelineIdForCompositable(aPipelineId));
+}
+
+wr::ExternalImageId WebRenderBridgeChild::GetNextExternalImageId() {
+ wr::MaybeExternalImageId id =
+ GetCompositorBridgeChild()->GetNextExternalImageId();
+ MOZ_RELEASE_ASSERT(id.isSome());
+ return id.value();
+}
+
+void WebRenderBridgeChild::ReleaseTextureOfImage(const wr::ImageKey& aKey) {
+ AddWebRenderParentCommand(OpReleaseTextureOfImage(aKey));
+}
+
+struct FontFileDataSink {
+ wr::FontKey* mFontKey;
+ WebRenderBridgeChild* mWrBridge;
+ wr::IpcResourceUpdateQueue* mResources;
+};
+
+static void WriteFontFileData(const uint8_t* aData, uint32_t aLength,
+ uint32_t aIndex, void* aBaton) {
+ FontFileDataSink* sink = static_cast<FontFileDataSink*>(aBaton);
+
+ *sink->mFontKey = sink->mWrBridge->GetNextFontKey();
+
+ sink->mResources->AddRawFont(
+ *sink->mFontKey, Range<uint8_t>(const_cast<uint8_t*>(aData), aLength),
+ aIndex);
+}
+
+static void WriteFontDescriptor(const uint8_t* aData, uint32_t aLength,
+ uint32_t aIndex, void* aBaton) {
+ FontFileDataSink* sink = static_cast<FontFileDataSink*>(aBaton);
+
+ *sink->mFontKey = sink->mWrBridge->GetNextFontKey();
+
+ sink->mResources->AddFontDescriptor(
+ *sink->mFontKey, Range<uint8_t>(const_cast<uint8_t*>(aData), aLength),
+ aIndex);
+}
+
+void WebRenderBridgeChild::PushGlyphs(
+ wr::DisplayListBuilder& aBuilder, wr::IpcResourceUpdateQueue& aResources,
+ Range<const wr::GlyphInstance> aGlyphs, gfx::ScaledFont* aFont,
+ const wr::ColorF& aColor, const StackingContextHelper& aSc,
+ const wr::LayoutRect& aBounds, const wr::LayoutRect& aClip,
+ bool aBackfaceVisible, const wr::GlyphOptions* aGlyphOptions) {
+ MOZ_ASSERT(aFont);
+
+ Maybe<wr::WrFontInstanceKey> key = GetFontKeyForScaledFont(aFont, aResources);
+ MOZ_ASSERT(key.isSome());
+
+ if (key.isSome()) {
+ aBuilder.PushText(aBounds, aClip, aBackfaceVisible, aColor, key.value(),
+ aGlyphs, aGlyphOptions);
+ }
+}
+
+Maybe<wr::FontInstanceKey> WebRenderBridgeChild::GetFontKeyForScaledFont(
+ gfx::ScaledFont* aScaledFont, wr::IpcResourceUpdateQueue& aResources) {
+ MOZ_ASSERT(!mDestroyed);
+ MOZ_ASSERT(aScaledFont);
+ MOZ_ASSERT(aScaledFont->CanSerialize());
+
+ return mFontInstanceKeys.WithEntryHandle(
+ aScaledFont, [&](auto&& entry) -> Maybe<wr::FontInstanceKey> {
+ if (!entry) {
+ Maybe<wr::FontKey> fontKey = GetFontKeyForUnscaledFont(
+ aScaledFont->GetUnscaledFont(), aResources);
+ if (fontKey.isNothing()) {
+ return Nothing();
+ }
+
+ wr::FontInstanceKey instanceKey = GetNextFontInstanceKey();
+
+ Maybe<wr::FontInstanceOptions> options;
+ Maybe<wr::FontInstancePlatformOptions> platformOptions;
+ std::vector<FontVariation> variations;
+ aScaledFont->GetWRFontInstanceOptions(&options, &platformOptions,
+ &variations);
+
+ aResources.AddFontInstance(
+ instanceKey, fontKey.value(), aScaledFont->GetSize(),
+ options.ptrOr(nullptr), platformOptions.ptrOr(nullptr),
+ Range<const FontVariation>(variations.data(), variations.size()));
+
+ entry.Insert(instanceKey);
+ }
+
+ return Some(*entry);
+ });
+}
+
+Maybe<wr::FontKey> WebRenderBridgeChild::GetFontKeyForUnscaledFont(
+ gfx::UnscaledFont* aUnscaled, wr::IpcResourceUpdateQueue& aResources) {
+ MOZ_ASSERT(!mDestroyed);
+
+ return mFontKeys.WithEntryHandle(
+ aUnscaled, [&](auto&& entry) -> Maybe<wr::FontKey> {
+ if (!entry) {
+ wr::FontKey fontKey = {wr::IdNamespace{0}, 0};
+ FontFileDataSink sink = {&fontKey, this, &aResources};
+ // First try to retrieve a descriptor for the font, as this is much
+ // cheaper to send over IPC than the full raw font data. If this is
+ // not possible, then and only then fall back to getting the raw font
+ // file data. If that fails, then the only thing left to do is signal
+ // failure by returning a null font key.
+ if (!aUnscaled->GetFontDescriptor(WriteFontDescriptor, &sink) &&
+ !aUnscaled->GetFontFileData(WriteFontFileData, &sink)) {
+ return Nothing();
+ }
+
+ entry.Insert(fontKey);
+ }
+
+ return Some(*entry);
+ });
+}
+
+void WebRenderBridgeChild::RemoveExpiredFontKeys(
+ wr::IpcResourceUpdateQueue& aResourceUpdates) {
+ uint32_t counter = gfx::ScaledFont::DeletionCounter();
+ if (mFontInstanceKeysDeleted != counter) {
+ mFontInstanceKeysDeleted = counter;
+ for (auto iter = mFontInstanceKeys.Iter(); !iter.Done(); iter.Next()) {
+ if (!iter.Key()) {
+ aResourceUpdates.DeleteFontInstance(iter.Data());
+ iter.Remove();
+ }
+ }
+ }
+ counter = gfx::UnscaledFont::DeletionCounter();
+ if (mFontKeysDeleted != counter) {
+ mFontKeysDeleted = counter;
+ for (auto iter = mFontKeys.Iter(); !iter.Done(); iter.Next()) {
+ if (!iter.Key()) {
+ aResourceUpdates.DeleteFont(iter.Data());
+ iter.Remove();
+ }
+ }
+ }
+}
+
+CompositorBridgeChild* WebRenderBridgeChild::GetCompositorBridgeChild() {
+ if (!IPCOpen()) {
+ return nullptr;
+ }
+ return static_cast<CompositorBridgeChild*>(Manager());
+}
+
+TextureForwarder* WebRenderBridgeChild::GetTextureForwarder() {
+ return static_cast<TextureForwarder*>(GetCompositorBridgeChild());
+}
+
+LayersIPCActor* WebRenderBridgeChild::GetLayersIPCActor() {
+ return static_cast<LayersIPCActor*>(GetCompositorBridgeChild());
+}
+
+void WebRenderBridgeChild::SyncWithCompositor() {
+ if (!IPCOpen()) {
+ return;
+ }
+ SendSyncWithCompositor();
+}
+
+void WebRenderBridgeChild::Connect(CompositableClient* aCompositable,
+ ImageContainer* aImageContainer) {
+ MOZ_ASSERT(!mDestroyed);
+ MOZ_ASSERT(aCompositable);
+
+ CompositableHandle handle = CompositableHandle::GetNext();
+ mCompositables.InsertOrUpdate(uint64_t(handle), aCompositable);
+
+ aCompositable->InitIPDL(handle);
+ SendNewCompositable(handle, aCompositable->GetTextureInfo());
+}
+
+bool WebRenderBridgeChild::AddOpDestroy(const OpDestroy& aOp) {
+ if (!mIsInTransaction) {
+ return false;
+ }
+
+ mDestroyedActors.AppendElement(aOp);
+ return true;
+}
+
+void WebRenderBridgeChild::ReleaseCompositable(
+ const CompositableHandle& aHandle) {
+ if (!IPCOpen()) {
+ // This can happen if the IPC connection was torn down, because, e.g.
+ // the GPU process died.
+ return;
+ }
+ if (!DestroyInTransaction(aHandle)) {
+ SendReleaseCompositable(aHandle);
+ }
+ mCompositables.Remove(aHandle.Value());
+}
+
+bool WebRenderBridgeChild::DestroyInTransaction(PTextureChild* aTexture) {
+ return AddOpDestroy(OpDestroy(WrapNotNull(aTexture)));
+}
+
+bool WebRenderBridgeChild::DestroyInTransaction(
+ const CompositableHandle& aHandle) {
+ return AddOpDestroy(OpDestroy(aHandle));
+}
+
+void WebRenderBridgeChild::RemoveTextureFromCompositable(
+ CompositableClient* aCompositable, TextureClient* aTexture) {
+ MOZ_ASSERT(aCompositable);
+ MOZ_ASSERT(aTexture);
+ MOZ_ASSERT(aTexture->GetIPDLActor());
+ MOZ_RELEASE_ASSERT(aTexture->GetIPDLActor()->GetIPCChannel() ==
+ GetIPCChannel());
+ if (!aCompositable->IsConnected() || !aTexture->GetIPDLActor()) {
+ // We don't have an actor anymore, don't try to use it!
+ return;
+ }
+
+ AddWebRenderParentCommand(CompositableOperation(
+ aCompositable->GetIPCHandle(),
+ OpRemoveTexture(WrapNotNull(aTexture->GetIPDLActor()))));
+}
+
+void WebRenderBridgeChild::UseTextures(
+ CompositableClient* aCompositable,
+ const nsTArray<TimedTextureClient>& aTextures) {
+ MOZ_ASSERT(aCompositable);
+
+ if (!aCompositable->IsConnected()) {
+ return;
+ }
+
+ AutoTArray<TimedTexture, 4> textures;
+
+ for (auto& t : aTextures) {
+ MOZ_ASSERT(t.mTextureClient);
+ MOZ_ASSERT(t.mTextureClient->GetIPDLActor());
+ MOZ_RELEASE_ASSERT(t.mTextureClient->GetIPDLActor()->GetIPCChannel() ==
+ GetIPCChannel());
+ bool readLocked = t.mTextureClient->OnForwardedToHost();
+
+ textures.AppendElement(TimedTexture(
+ WrapNotNull(t.mTextureClient->GetIPDLActor()), t.mTimeStamp,
+ t.mPictureRect, t.mFrameID, t.mProducerID, readLocked));
+ GetCompositorBridgeChild()->HoldUntilCompositableRefReleasedIfNecessary(
+ t.mTextureClient);
+ }
+ AddWebRenderParentCommand(CompositableOperation(aCompositable->GetIPCHandle(),
+ OpUseTexture(textures)));
+}
+
+void WebRenderBridgeChild::UseRemoteTexture(CompositableClient* aCompositable,
+ const RemoteTextureId aTextureId,
+ const RemoteTextureOwnerId aOwnerId,
+ const gfx::IntSize aSize,
+ const TextureFlags aFlags) {
+ AddWebRenderParentCommand(CompositableOperation(
+ aCompositable->GetIPCHandle(),
+ OpUseRemoteTexture(aTextureId, aOwnerId, aSize, aFlags)));
+}
+
+void WebRenderBridgeChild::EnableRemoteTexturePushCallback(
+ CompositableClient* aCompositable, const RemoteTextureOwnerId aOwnerId,
+ const gfx::IntSize aSize, const TextureFlags aFlags) {
+ AddWebRenderParentCommand(CompositableOperation(
+ aCompositable->GetIPCHandle(),
+ OpEnableRemoteTexturePushCallback(aOwnerId, aSize, aFlags)));
+}
+
+void WebRenderBridgeChild::UpdateFwdTransactionId() {
+ GetCompositorBridgeChild()->UpdateFwdTransactionId();
+}
+
+uint64_t WebRenderBridgeChild::GetFwdTransactionId() {
+ return GetCompositorBridgeChild()->GetFwdTransactionId();
+}
+
+bool WebRenderBridgeChild::InForwarderThread() { return NS_IsMainThread(); }
+
+mozilla::ipc::IPCResult WebRenderBridgeChild::RecvWrUpdated(
+ const wr::IdNamespace& aNewIdNamespace,
+ const TextureFactoryIdentifier& textureFactoryIdentifier) {
+ if (mManager) {
+ mManager->WrUpdated();
+ }
+ IdentifyTextureHost(textureFactoryIdentifier);
+ // Update mIdNamespace to identify obsolete keys and messages by
+ // WebRenderBridgeParent. Since usage of invalid keys could cause crash in
+ // webrender.
+ mIdNamespace = aNewIdNamespace;
+ // Just clear FontInstaceKeys/FontKeys, they are removed during WebRenderAPI
+ // destruction.
+ mFontInstanceKeys.Clear();
+ mFontKeys.Clear();
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult WebRenderBridgeChild::RecvWrReleasedImages(
+ nsTArray<wr::ExternalImageKeyPair>&& aPairs) {
+ if (mManager) {
+ mManager->WrReleasedImages(aPairs);
+ }
+ return IPC_OK();
+}
+
+void WebRenderBridgeChild::BeginClearCachedResources() {
+ mSentDisplayList = false;
+ mIsInClearCachedResources = true;
+ // Clear display list and animtaions at parent side before clearing cached
+ // resources on client side. It prevents to clear resources before clearing
+ // display list at parent side.
+ SendClearCachedResources();
+}
+
+void WebRenderBridgeChild::EndClearCachedResources() {
+ if (!IPCOpen()) {
+ mIsInClearCachedResources = false;
+ return;
+ }
+ ProcessWebRenderParentCommands();
+ mIsInClearCachedResources = false;
+}
+
+void WebRenderBridgeChild::SetWebRenderLayerManager(
+ WebRenderLayerManager* aManager) {
+ MOZ_ASSERT(aManager && !mManager);
+ mManager = aManager;
+
+ MOZ_ASSERT(NS_IsMainThread() || !XRE_IsContentProcess());
+ mActiveResourceTracker =
+ MakeUnique<ActiveResourceTracker>(1000, "CompositableForwarder", nullptr);
+}
+
+ipc::IShmemAllocator* WebRenderBridgeChild::GetShmemAllocator() {
+ if (!IPCOpen()) {
+ return nullptr;
+ }
+ return static_cast<CompositorBridgeChild*>(Manager());
+}
+
+RefPtr<KnowsCompositor> WebRenderBridgeChild::GetForMedia() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // Ensure device initialization for video playback unless they are all remote.
+ // The devices are lazily initialized with WebRender to reduce memory usage.
+ if (!PDMFactory::AllDecodersAreRemote()) {
+ gfxPlatform::GetPlatform()->EnsureDevicesInitialized();
+ }
+
+ return MakeAndAddRef<KnowsCompositorMediaProxy>(
+ GetTextureFactoryIdentifier());
+}
+
+bool WebRenderBridgeChild::AllocResourceShmem(size_t aSize,
+ RefCountedShmem& aShm) {
+ // We keep a single shmem around to reuse later if it is reference count has
+ // dropped back to 1 (the reference held by the WebRenderBridgeChild).
+
+ // If the cached shmem exists, has the correct size and isn't held by anything
+ // other than us, recycle it.
+ bool alreadyAllocated = RefCountedShm::IsValid(mResourceShm);
+ if (alreadyAllocated) {
+ if (RefCountedShm::GetSize(mResourceShm) == aSize &&
+ RefCountedShm::GetReferenceCount(mResourceShm) <= 1) {
+ MOZ_ASSERT(RefCountedShm::GetReferenceCount(mResourceShm) == 1);
+ aShm = mResourceShm;
+ return true;
+ }
+ }
+
+ // If there was no cached shmem or we couldn't recycle it, alloc a new one.
+ if (!RefCountedShm::Alloc(this, aSize, aShm)) {
+ return false;
+ }
+
+ // Now that we have a valid shmem, put it in the cache if we don't have one
+ // yet.
+ if (!alreadyAllocated) {
+ mResourceShm = aShm;
+ RefCountedShm::AddRef(aShm);
+ }
+
+ return true;
+}
+
+void WebRenderBridgeChild::DeallocResourceShmem(RefCountedShmem& aShm) {
+ if (!RefCountedShm::IsValid(aShm)) {
+ return;
+ }
+ MOZ_ASSERT(RefCountedShm::GetReferenceCount(aShm) == 0);
+
+ RefCountedShm::Dealloc(this, aShm);
+}
+
+void WebRenderBridgeChild::Capture() { this->SendCapture(); }
+
+void WebRenderBridgeChild::StartCaptureSequence(const nsCString& aPath,
+ uint32_t aFlags) {
+ this->SendStartCaptureSequence(aPath, aFlags);
+}
+
+void WebRenderBridgeChild::StopCaptureSequence() {
+ this->SendStopCaptureSequence();
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/wr/WebRenderBridgeChild.h b/gfx/layers/wr/WebRenderBridgeChild.h
new file mode 100644
index 0000000000..2bd8dc8570
--- /dev/null
+++ b/gfx/layers/wr/WebRenderBridgeChild.h
@@ -0,0 +1,269 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_WebRenderBridgeChild_h
+#define mozilla_layers_WebRenderBridgeChild_h
+
+#include "mozilla/layers/CompositableForwarder.h"
+#include "mozilla/layers/PWebRenderBridgeChild.h"
+
+namespace mozilla {
+
+namespace widget {
+class CompositorWidget;
+}
+
+namespace wr {
+class DisplayListBuilder;
+class ResourceUpdateQueue;
+class IpcResourceUpdateQueue;
+} // namespace wr
+
+namespace layers {
+
+class CompositableClient;
+class CompositorBridgeChild;
+class StackingContextHelper;
+class TextureForwarder;
+class WebRenderLayerManager;
+
+template <class T>
+class ThreadSafeWeakPtrHashKey : public PLDHashEntryHdr {
+ public:
+ typedef RefPtr<T> KeyType;
+ typedef const T* KeyTypePointer;
+
+ explicit ThreadSafeWeakPtrHashKey(KeyTypePointer aKey)
+ : mKey(do_AddRef(const_cast<T*>(aKey))) {}
+
+ KeyType GetKey() const { return do_AddRef(mKey); }
+ bool KeyEquals(KeyTypePointer aKey) const { return mKey == aKey; }
+
+ static KeyTypePointer KeyToPointer(const KeyType& aKey) { return aKey.get(); }
+ static PLDHashNumber HashKey(KeyTypePointer aKey) {
+ return NS_PTR_TO_UINT32(aKey) >> 2;
+ }
+ enum { ALLOW_MEMMOVE = true };
+
+ private:
+ ThreadSafeWeakPtr<T> mKey;
+};
+
+typedef ThreadSafeWeakPtrHashKey<gfx::UnscaledFont> UnscaledFontHashKey;
+typedef ThreadSafeWeakPtrHashKey<gfx::ScaledFont> ScaledFontHashKey;
+
+class WebRenderBridgeChild final : public PWebRenderBridgeChild,
+ public CompositableForwarder {
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(WebRenderBridgeChild, override)
+
+ friend class PWebRenderBridgeChild;
+
+ public:
+ explicit WebRenderBridgeChild(const wr::PipelineId& aPipelineId);
+
+ void AddWebRenderParentCommand(const WebRenderParentCommand& aCmd);
+ bool HasWebRenderParentCommands() { return !mParentCommands.IsEmpty(); }
+
+ void UpdateResources(wr::IpcResourceUpdateQueue& aResources);
+ void BeginTransaction();
+ bool EndTransaction(DisplayListData&& aDisplayListData,
+ TransactionId aTransactionId, bool aContainsSVGroup,
+ const mozilla::VsyncId& aVsyncId,
+ const mozilla::TimeStamp& aVsyncStartTime,
+ const mozilla::TimeStamp& aRefreshStartTime,
+ const mozilla::TimeStamp& aTxnStartTime,
+ const nsCString& aTxtURL);
+ void EndEmptyTransaction(const FocusTarget& aFocusTarget,
+ Maybe<TransactionData>&& aTransactionData,
+ TransactionId aTransactionId,
+ const mozilla::VsyncId& aVsyncId,
+ const mozilla::TimeStamp& aVsyncStartTime,
+ const mozilla::TimeStamp& aRefreshStartTime,
+ const mozilla::TimeStamp& aTxnStartTime,
+ const nsCString& aTxtURL);
+ void ProcessWebRenderParentCommands();
+
+ CompositorBridgeChild* GetCompositorBridgeChild();
+
+ wr::PipelineId GetPipeline() { return mPipelineId; }
+
+ // KnowsCompositor
+ TextureForwarder* GetTextureForwarder() override;
+ LayersIPCActor* GetLayersIPCActor() override;
+ void SyncWithCompositor() override;
+ ActiveResourceTracker* GetActiveResourceTracker() override {
+ return mActiveResourceTracker.get();
+ }
+
+ void AddPipelineIdForCompositable(const wr::PipelineId& aPipelineId,
+ const CompositableHandle& aHandle,
+ CompositableHandleOwner aOwner);
+ void RemovePipelineIdForCompositable(const wr::PipelineId& aPipelineId);
+
+ /// Release TextureClient that is bounded to ImageKey.
+ /// It is used for recycling TextureClient.
+ void ReleaseTextureOfImage(const wr::ImageKey& aKey);
+
+ /**
+ * Clean this up, finishing with SendShutDown() which will cause __delete__
+ * to be sent from the parent side.
+ */
+ void Destroy(bool aIsSync);
+ bool IPCOpen() const { return mIPCOpen && !mDestroyed; }
+ bool GetSentDisplayList() const { return mSentDisplayList; }
+ bool IsDestroyed() const { return mDestroyed; }
+
+ uint32_t GetNextResourceId() { return ++mResourceId; }
+ wr::IdNamespace GetNamespace() { return mIdNamespace; }
+ void SetNamespace(wr::IdNamespace aIdNamespace) {
+ mIdNamespace = aIdNamespace;
+ }
+
+ bool MatchesNamespace(const wr::ImageKey& aImageKey) const {
+ return aImageKey.mNamespace == mIdNamespace;
+ }
+
+ bool MatchesNamespace(const wr::BlobImageKey& aBlobKey) const {
+ return MatchesNamespace(aBlobKey._0);
+ }
+
+ wr::FontKey GetNextFontKey() {
+ return wr::FontKey{GetNamespace(), GetNextResourceId()};
+ }
+
+ wr::FontInstanceKey GetNextFontInstanceKey() {
+ return wr::FontInstanceKey{GetNamespace(), GetNextResourceId()};
+ }
+
+ wr::WrImageKey GetNextImageKey() {
+ return wr::WrImageKey{GetNamespace(), GetNextResourceId()};
+ }
+
+ void PushGlyphs(wr::DisplayListBuilder& aBuilder,
+ wr::IpcResourceUpdateQueue& aResources,
+ Range<const wr::GlyphInstance> aGlyphs,
+ gfx::ScaledFont* aFont, const wr::ColorF& aColor,
+ const StackingContextHelper& aSc,
+ const wr::LayoutRect& aBounds, const wr::LayoutRect& aClip,
+ bool aBackfaceVisible,
+ const wr::GlyphOptions* aGlyphOptions = nullptr);
+
+ Maybe<wr::FontInstanceKey> GetFontKeyForScaledFont(
+ gfx::ScaledFont* aScaledFont, wr::IpcResourceUpdateQueue& aResources);
+ Maybe<wr::FontKey> GetFontKeyForUnscaledFont(
+ gfx::UnscaledFont* aUnscaledFont, wr::IpcResourceUpdateQueue& aResources);
+ void RemoveExpiredFontKeys(wr::IpcResourceUpdateQueue& aResources);
+
+ void BeginClearCachedResources();
+ void EndClearCachedResources();
+
+ void SetWebRenderLayerManager(WebRenderLayerManager* aManager);
+
+ mozilla::ipc::IShmemAllocator* GetShmemAllocator();
+
+ bool IsThreadSafe() const override { return false; }
+
+ RefPtr<KnowsCompositor> GetForMedia() override;
+
+ /// Alloc a specific type of shmem that is intended for use in
+ /// IpcResourceUpdateQueue only, and cache at most one of them,
+ /// when called multiple times.
+ ///
+ /// Do not use this for anything else.
+ bool AllocResourceShmem(size_t aSize, RefCountedShmem& aShm);
+ /// Dealloc shared memory that was allocated with AllocResourceShmem.
+ ///
+ /// Do not use this for anything else.
+ void DeallocResourceShmem(RefCountedShmem& aShm);
+
+ void Capture();
+ void StartCaptureSequence(const nsCString& path, uint32_t aFlags);
+ void StopCaptureSequence();
+
+ private:
+ friend class CompositorBridgeChild;
+
+ ~WebRenderBridgeChild();
+
+ wr::ExternalImageId GetNextExternalImageId();
+
+ // CompositableForwarder
+ void Connect(CompositableClient* aCompositable,
+ ImageContainer* aImageContainer = nullptr) override;
+ void ReleaseCompositable(const CompositableHandle& aHandle) override;
+ bool DestroyInTransaction(PTextureChild* aTexture) override;
+ bool DestroyInTransaction(const CompositableHandle& aHandle);
+ void RemoveTextureFromCompositable(CompositableClient* aCompositable,
+ TextureClient* aTexture) override;
+ void UseTextures(CompositableClient* aCompositable,
+ const nsTArray<TimedTextureClient>& aTextures) override;
+ void UseRemoteTexture(CompositableClient* aCompositable,
+ const RemoteTextureId aTextureId,
+ const RemoteTextureOwnerId aOwnerId,
+ const gfx::IntSize aSize,
+ const TextureFlags aFlags) override;
+ void EnableRemoteTexturePushCallback(CompositableClient* aCompositable,
+ const RemoteTextureOwnerId aOwnerId,
+ const gfx::IntSize aSize,
+ const TextureFlags aFlags) override;
+ void UpdateFwdTransactionId() override;
+ uint64_t GetFwdTransactionId() override;
+ bool InForwarderThread() override;
+
+ void ActorDestroy(ActorDestroyReason why) override;
+
+ void DoDestroy();
+
+ mozilla::ipc::IPCResult RecvWrUpdated(
+ const wr::IdNamespace& aNewIdNamespace,
+ const TextureFactoryIdentifier& textureFactoryIdentifier);
+ mozilla::ipc::IPCResult RecvWrReleasedImages(
+ nsTArray<wr::ExternalImageKeyPair>&& aPairs);
+
+ void AddIPDLReference() {
+ MOZ_ASSERT(mIPCOpen == false);
+ mIPCOpen = true;
+ AddRef();
+ }
+ void ReleaseIPDLReference() {
+ MOZ_ASSERT(mIPCOpen == true);
+ mIPCOpen = false;
+ Release();
+ }
+
+ bool AddOpDestroy(const OpDestroy& aOp);
+
+ nsTArray<OpDestroy> mDestroyedActors;
+ nsTArray<WebRenderParentCommand> mParentCommands;
+ nsTHashMap<nsUint64HashKey, CompositableClient*> mCompositables;
+ bool mIsInTransaction;
+ bool mIsInClearCachedResources;
+ wr::IdNamespace mIdNamespace;
+ uint32_t mResourceId;
+ wr::PipelineId mPipelineId;
+ WebRenderLayerManager* mManager;
+
+ bool mIPCOpen;
+ bool mDestroyed;
+ // True iff we have called SendSetDisplayList and haven't called
+ // SendClearCachedResources since that call.
+ bool mSentDisplayList;
+
+ uint32_t mFontKeysDeleted;
+ nsTHashMap<UnscaledFontHashKey, wr::FontKey> mFontKeys;
+
+ uint32_t mFontInstanceKeysDeleted;
+ nsTHashMap<ScaledFontHashKey, wr::FontInstanceKey> mFontInstanceKeys;
+
+ UniquePtr<ActiveResourceTracker> mActiveResourceTracker;
+
+ RefCountedShmem mResourceShm;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_WebRenderBridgeChild_h
diff --git a/gfx/layers/wr/WebRenderBridgeParent.cpp b/gfx/layers/wr/WebRenderBridgeParent.cpp
new file mode 100644
index 0000000000..b8a262e577
--- /dev/null
+++ b/gfx/layers/wr/WebRenderBridgeParent.cpp
@@ -0,0 +1,2909 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/layers/WebRenderBridgeParent.h"
+
+#include "CompositableHost.h"
+#include "gfxEnv.h"
+#include "gfxPlatform.h"
+#include "gfxOTSUtils.h"
+#include "GeckoProfiler.h"
+#include "GLContext.h"
+#include "GLContextProvider.h"
+#include "GLLibraryLoader.h"
+#include "nsExceptionHandler.h"
+#include "mozilla/Range.h"
+#include "mozilla/EnumeratedRange.h"
+#include "mozilla/StaticPrefs_gfx.h"
+#include "mozilla/StaticPrefs_webgl.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/gfx/gfxVars.h"
+#include "mozilla/gfx/GPUParent.h"
+#include "mozilla/layers/AnimationHelper.h"
+#include "mozilla/layers/APZSampler.h"
+#include "mozilla/layers/APZUpdater.h"
+#include "mozilla/layers/Compositor.h"
+#include "mozilla/layers/CompositorBridgeParent.h"
+#include "mozilla/layers/CompositorAnimationStorage.h"
+#include "mozilla/layers/CompositorThread.h"
+#include "mozilla/layers/CompositorVsyncScheduler.h"
+#include "mozilla/layers/ImageBridgeParent.h"
+#include "mozilla/layers/ImageDataSerializer.h"
+#include "mozilla/layers/IpcResourceUpdateQueue.h"
+#include "mozilla/layers/OMTASampler.h"
+#include "mozilla/layers/SharedSurfacesParent.h"
+#include "mozilla/layers/TextureHost.h"
+#include "mozilla/layers/AsyncImagePipelineManager.h"
+#include "mozilla/layers/UiCompositorControllerParent.h"
+#include "mozilla/layers/WebRenderImageHost.h"
+#include "mozilla/layers/WebRenderTextureHost.h"
+#include "mozilla/ProfilerMarkerTypes.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/TimeStamp.h"
+#include "mozilla/Unused.h"
+#include "mozilla/webrender/RenderTextureHostSWGL.h"
+#include "mozilla/webrender/RenderThread.h"
+#include "mozilla/widget/CompositorWidget.h"
+
+#ifdef XP_WIN
+# include "mozilla/gfx/DeviceManagerDx.h"
+# include "mozilla/widget/WinCompositorWidget.h"
+#endif
+#if defined(MOZ_WIDGET_GTK)
+# include "mozilla/widget/GtkCompositorWidget.h"
+#endif
+
+bool is_in_main_thread() { return NS_IsMainThread(); }
+
+bool is_in_compositor_thread() {
+ return mozilla::layers::CompositorThreadHolder::IsInCompositorThread();
+}
+
+bool is_in_render_thread() {
+ return mozilla::wr::RenderThread::IsInRenderThread();
+}
+
+bool gecko_profiler_thread_is_being_profiled() {
+ return profiler_thread_is_being_profiled(ThreadProfilingFeatures::Any);
+}
+
+bool is_glcontext_gles(void* const glcontext_ptr) {
+ MOZ_RELEASE_ASSERT(glcontext_ptr);
+ return reinterpret_cast<mozilla::gl::GLContext*>(glcontext_ptr)->IsGLES();
+}
+
+bool is_glcontext_angle(void* glcontext_ptr) {
+ MOZ_ASSERT(glcontext_ptr);
+
+ mozilla::gl::GLContext* glcontext =
+ reinterpret_cast<mozilla::gl::GLContext*>(glcontext_ptr);
+ if (!glcontext) {
+ return false;
+ }
+ return glcontext->IsANGLE();
+}
+
+const char* gfx_wr_resource_path_override() {
+ return gfxPlatform::WebRenderResourcePathOverride();
+}
+
+bool gfx_wr_use_optimized_shaders() {
+ return mozilla::gfx::gfxVars::UseWebRenderOptimizedShaders();
+}
+
+void gfx_critical_note(const char* msg) { gfxCriticalNote << msg; }
+
+void gfx_critical_error(const char* msg) { gfxCriticalError() << msg; }
+
+void gecko_printf_stderr_output(const char* msg) { printf_stderr("%s\n", msg); }
+
+void* get_proc_address_from_glcontext(void* glcontext_ptr,
+ const char* procname) {
+ mozilla::gl::GLContext* glcontext =
+ reinterpret_cast<mozilla::gl::GLContext*>(glcontext_ptr);
+ MOZ_ASSERT(glcontext);
+ if (!glcontext) {
+ return nullptr;
+ }
+ const auto& loader = glcontext->GetSymbolLoader();
+ MOZ_ASSERT(loader);
+
+ const auto ret = loader->GetProcAddress(procname);
+ return reinterpret_cast<void*>(ret);
+}
+
+static CrashReporter::Annotation FromWrCrashAnnotation(
+ mozilla::wr::CrashAnnotation aAnnotation) {
+ switch (aAnnotation) {
+ case mozilla::wr::CrashAnnotation::CompileShader:
+ return CrashReporter::Annotation::GraphicsCompileShader;
+ case mozilla::wr::CrashAnnotation::DrawShader:
+ return CrashReporter::Annotation::GraphicsDrawShader;
+ default:
+ MOZ_ASSERT_UNREACHABLE("Unhandled annotation!");
+ return CrashReporter::Annotation::Count;
+ }
+}
+
+extern "C" {
+
+void gfx_wr_set_crash_annotation(mozilla::wr::CrashAnnotation aAnnotation,
+ const char* aValue) {
+ MOZ_ASSERT(aValue);
+
+ auto annotation = FromWrCrashAnnotation(aAnnotation);
+ if (annotation == CrashReporter::Annotation::Count) {
+ return;
+ }
+
+ CrashReporter::AnnotateCrashReport(annotation, nsDependentCString(aValue));
+}
+
+void gfx_wr_clear_crash_annotation(mozilla::wr::CrashAnnotation aAnnotation) {
+ auto annotation = FromWrCrashAnnotation(aAnnotation);
+ if (annotation == CrashReporter::Annotation::Count) {
+ return;
+ }
+
+ CrashReporter::RemoveCrashReportAnnotation(annotation);
+}
+}
+
+namespace mozilla {
+
+namespace layers {
+
+using namespace mozilla::gfx;
+
+LazyLogModule gWebRenderBridgeParentLog("WebRenderBridgeParent");
+#define LOG(...) \
+ MOZ_LOG(gWebRenderBridgeParentLog, LogLevel::Debug, (__VA_ARGS__))
+
+class ScheduleObserveLayersUpdate : public wr::NotificationHandler {
+ public:
+ ScheduleObserveLayersUpdate(RefPtr<CompositorBridgeParentBase> aBridge,
+ LayersId aLayersId, LayersObserverEpoch aEpoch,
+ bool aIsActive)
+ : mBridge(aBridge),
+ mLayersId(aLayersId),
+ mObserverEpoch(aEpoch),
+ mIsActive(aIsActive) {}
+
+ void Notify(wr::Checkpoint) override {
+ CompositorThread()->Dispatch(
+ NewRunnableMethod<LayersId, LayersObserverEpoch, int>(
+ "ObserveLayersUpdate", mBridge,
+ &CompositorBridgeParentBase::ObserveLayersUpdate, mLayersId,
+ mObserverEpoch, mIsActive));
+ }
+
+ protected:
+ RefPtr<CompositorBridgeParentBase> mBridge;
+ LayersId mLayersId;
+ LayersObserverEpoch mObserverEpoch;
+ bool mIsActive;
+};
+
+class SceneBuiltNotification : public wr::NotificationHandler {
+ public:
+ SceneBuiltNotification(WebRenderBridgeParent* aParent, wr::Epoch aEpoch,
+ TimeStamp aTxnStartTime)
+ : mParent(aParent), mEpoch(aEpoch), mTxnStartTime(aTxnStartTime) {}
+
+ void Notify(wr::Checkpoint) override {
+ auto startTime = this->mTxnStartTime;
+ RefPtr<WebRenderBridgeParent> parent = mParent;
+ wr::Epoch epoch = mEpoch;
+ CompositorThread()->Dispatch(NS_NewRunnableFunction(
+ "SceneBuiltNotificationRunnable", [parent, epoch, startTime]() {
+ auto endTime = TimeStamp::Now();
+ if (profiler_thread_is_being_profiled_for_markers()) {
+ PROFILER_MARKER("CONTENT_FULL_PAINT_TIME", GRAPHICS,
+ MarkerTiming::Interval(startTime, endTime),
+ ContentBuildMarker);
+ }
+ Telemetry::Accumulate(
+ Telemetry::CONTENT_FULL_PAINT_TIME,
+ static_cast<uint32_t>((endTime - startTime).ToMilliseconds()));
+ parent->NotifySceneBuiltForEpoch(epoch, endTime);
+ }));
+ }
+
+ protected:
+ RefPtr<WebRenderBridgeParent> mParent;
+ wr::Epoch mEpoch;
+ TimeStamp mTxnStartTime;
+};
+
+class WebRenderBridgeParent::ScheduleSharedSurfaceRelease final
+ : public wr::NotificationHandler {
+ public:
+ explicit ScheduleSharedSurfaceRelease(WebRenderBridgeParent* aWrBridge)
+ : mWrBridge(aWrBridge), mSurfaces(20) {}
+
+ ~ScheduleSharedSurfaceRelease() override {
+ if (!mSurfaces.IsEmpty()) {
+ MOZ_ASSERT_UNREACHABLE("Unreleased surfaces!");
+ gfxCriticalNote << "ScheduleSharedSurfaceRelease destroyed non-empty";
+ NotifyInternal(/* aFromCheckpoint */ false);
+ }
+ }
+
+ void Add(const wr::ImageKey& aKey, const wr::ExternalImageId& aId) {
+ mSurfaces.AppendElement(wr::ExternalImageKeyPair{aKey, aId});
+ }
+
+ void Notify(wr::Checkpoint) override {
+ NotifyInternal(/* aFromCheckpoint */ true);
+ }
+
+ private:
+ void NotifyInternal(bool aFromCheckpoint) {
+ CompositorThread()->Dispatch(
+ NewRunnableMethod<nsTArray<wr::ExternalImageKeyPair>, bool>(
+ "ObserveSharedSurfaceRelease", mWrBridge,
+ &WebRenderBridgeParent::ObserveSharedSurfaceRelease,
+ std::move(mSurfaces), aFromCheckpoint));
+ }
+
+ RefPtr<WebRenderBridgeParent> mWrBridge;
+ nsTArray<wr::ExternalImageKeyPair> mSurfaces;
+};
+
+class MOZ_STACK_CLASS AutoWebRenderBridgeParentAsyncMessageSender final {
+ public:
+ explicit AutoWebRenderBridgeParentAsyncMessageSender(
+ WebRenderBridgeParent* aWebRenderBridgeParent,
+ nsTArray<OpDestroy>* aDestroyActors = nullptr)
+ : mWebRenderBridgeParent(aWebRenderBridgeParent),
+ mActorsToDestroy(aDestroyActors) {
+ mWebRenderBridgeParent->SetAboutToSendAsyncMessages();
+ }
+
+ ~AutoWebRenderBridgeParentAsyncMessageSender() {
+ mWebRenderBridgeParent->SendPendingAsyncMessages();
+ if (mActorsToDestroy) {
+ // Destroy the actors after sending the async messages because the latter
+ // may contain references to some actors.
+ for (const auto& op : *mActorsToDestroy) {
+ mWebRenderBridgeParent->DestroyActor(op);
+ }
+ }
+ }
+
+ private:
+ WebRenderBridgeParent* mWebRenderBridgeParent;
+ nsTArray<OpDestroy>* mActorsToDestroy;
+};
+
+WebRenderBridgeParent::WebRenderBridgeParent(
+ CompositorBridgeParentBase* aCompositorBridge,
+ const wr::PipelineId& aPipelineId, widget::CompositorWidget* aWidget,
+ CompositorVsyncScheduler* aScheduler, RefPtr<wr::WebRenderAPI>&& aApi,
+ RefPtr<AsyncImagePipelineManager>&& aImageMgr, TimeDuration aVsyncRate)
+ : mCompositorBridge(aCompositorBridge),
+ mPipelineId(aPipelineId),
+ mWidget(aWidget),
+ mApi(aApi),
+ mAsyncImageManager(aImageMgr),
+ mCompositorScheduler(aScheduler),
+ mVsyncRate(aVsyncRate),
+ mChildLayersObserverEpoch{0},
+ mParentLayersObserverEpoch{0},
+ mWrEpoch{0},
+ mIdNamespace(aApi->GetNamespace()),
+#if defined(MOZ_WIDGET_ANDROID)
+ mScreenPixelsTarget(nullptr),
+#endif
+ mBlobTileSize(256),
+ mSkippedCompositeReasons(wr::RenderReasons::NONE),
+ mDestroyed(false),
+ mReceivedDisplayList(false),
+ mIsFirstPaint(true),
+ mSkippedComposite(false),
+ mDisablingNativeCompositor(false),
+ mPendingScrollPayloads("WebRenderBridgeParent::mPendingScrollPayloads") {
+ MOZ_ASSERT(mAsyncImageManager);
+ LOG("WebRenderBridgeParent::WebRenderBridgeParent() PipelineId %" PRIx64
+ " Id %" PRIx64 " root %d",
+ wr::AsUint64(mPipelineId), wr::AsUint64(mApi->GetId()),
+ IsRootWebRenderBridgeParent());
+
+ mAsyncImageManager->AddPipeline(mPipelineId, this);
+ if (IsRootWebRenderBridgeParent()) {
+ MOZ_ASSERT(!mCompositorScheduler);
+ mCompositorScheduler = new CompositorVsyncScheduler(this, mWidget);
+ UpdateDebugFlags();
+ UpdateQualitySettings();
+ UpdateProfilerUI();
+ UpdateParameters();
+ // Start with the cached bool parameter bits inverted so that we update them
+ // all.
+ mBoolParameterBits = ~gfxVars::WebRenderBoolParameters();
+ UpdateBoolParameters();
+ }
+}
+
+WebRenderBridgeParent::WebRenderBridgeParent(const wr::PipelineId& aPipelineId,
+ nsCString&& aError)
+ : mCompositorBridge(nullptr),
+ mPipelineId(aPipelineId),
+ mChildLayersObserverEpoch{0},
+ mParentLayersObserverEpoch{0},
+ mWrEpoch{0},
+ mIdNamespace{0},
+ mInitError(aError),
+ mDestroyed(true),
+ mReceivedDisplayList(false),
+ mIsFirstPaint(false),
+ mSkippedComposite(false),
+ mDisablingNativeCompositor(false),
+ mPendingScrollPayloads("WebRenderBridgeParent::mPendingScrollPayloads") {
+ LOG("WebRenderBridgeParent::WebRenderBridgeParent() PipelineId %" PRIx64 "",
+ wr::AsUint64(mPipelineId));
+}
+
+WebRenderBridgeParent::~WebRenderBridgeParent() {
+ LOG("WebRenderBridgeParent::WebRenderBridgeParent() PipelineId %" PRIx64 "",
+ wr::AsUint64(mPipelineId));
+}
+
+/* static */
+WebRenderBridgeParent* WebRenderBridgeParent::CreateDestroyed(
+ const wr::PipelineId& aPipelineId, nsCString&& aError) {
+ return new WebRenderBridgeParent(aPipelineId, std::move(aError));
+}
+
+mozilla::ipc::IPCResult WebRenderBridgeParent::RecvEnsureConnected(
+ TextureFactoryIdentifier* aTextureFactoryIdentifier,
+ MaybeIdNamespace* aMaybeIdNamespace, nsCString* aError) {
+ if (mDestroyed) {
+ *aTextureFactoryIdentifier =
+ TextureFactoryIdentifier(LayersBackend::LAYERS_NONE);
+ *aMaybeIdNamespace = Nothing();
+ if (mInitError.IsEmpty()) {
+ // Got destroyed after we initialized but before the handshake finished?
+ aError->AssignLiteral("FEATURE_FAILURE_WEBRENDER_INITIALIZE_RACE");
+ } else {
+ *aError = std::move(mInitError);
+ }
+ return IPC_OK();
+ }
+
+ MOZ_ASSERT(mIdNamespace.mHandle != 0);
+ *aTextureFactoryIdentifier = GetTextureFactoryIdentifier();
+ *aMaybeIdNamespace = Some(mIdNamespace);
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult WebRenderBridgeParent::RecvShutdown() {
+ return HandleShutdown();
+}
+
+mozilla::ipc::IPCResult WebRenderBridgeParent::RecvShutdownSync() {
+ return HandleShutdown();
+}
+
+mozilla::ipc::IPCResult WebRenderBridgeParent::HandleShutdown() {
+ Destroy();
+ IProtocol* mgr = Manager();
+ if (!Send__delete__(this)) {
+ return IPC_FAIL_NO_REASON(mgr);
+ }
+ return IPC_OK();
+}
+
+void WebRenderBridgeParent::Destroy() {
+ if (mDestroyed) {
+ return;
+ }
+ LOG("WebRenderBridgeParent::Destroy() PipelineId %" PRIx64 " Id %" PRIx64
+ " root %d",
+ wr::AsUint64(mPipelineId), wr::AsUint64(mApi->GetId()),
+ IsRootWebRenderBridgeParent());
+
+ mDestroyed = true;
+ if (mWebRenderBridgeRef) {
+ // Break mutual reference
+ mWebRenderBridgeRef->Clear();
+ mWebRenderBridgeRef = nullptr;
+ }
+ for (const auto& entry : mCompositables) {
+ entry.second->OnReleased();
+ }
+ mCompositables.clear();
+ ClearResources();
+}
+
+struct WROTSAlloc {
+ wr::Vec<uint8_t> mVec;
+
+ void* Grow(void* aPtr, size_t aLength) {
+ if (aLength > mVec.Capacity()) {
+ mVec.Reserve(aLength - mVec.Capacity());
+ }
+ return mVec.inner.data;
+ }
+ wr::Vec<uint8_t> ShrinkToFit(void* aPtr, size_t aLength) {
+ wr::Vec<uint8_t> result(std::move(mVec));
+ result.inner.length = aLength;
+ return result;
+ }
+ void Free(void* aPtr) {}
+};
+
+static bool ReadRawFont(const OpAddRawFont& aOp, wr::ShmSegmentsReader& aReader,
+ wr::TransactionBuilder& aUpdates) {
+ wr::Vec<uint8_t> sourceBytes;
+ Maybe<Range<uint8_t>> ptr =
+ aReader.GetReadPointerOrCopy(aOp.bytes(), sourceBytes);
+ if (ptr.isNothing()) {
+ gfxCriticalNote << "No read pointer from reader for sanitizing font "
+ << aOp.key().mHandle;
+ return false;
+ }
+ Range<uint8_t>& source = ptr.ref();
+ // Attempt to sanitize the font before passing it along for updating.
+ // Ensure that we're not strict here about font types, since any font that
+ // failed generating a descriptor might end up here as raw font data.
+ size_t lengthHint = gfxOTSContext::GuessSanitizedFontSize(
+ source.begin().get(), source.length(), false);
+ if (!lengthHint) {
+ gfxCriticalNote << "Could not determine font type for sanitizing font "
+ << aOp.key().mHandle;
+ return false;
+ }
+ gfxOTSExpandingMemoryStream<WROTSAlloc> output(lengthHint);
+ gfxOTSContext otsContext;
+ if (!otsContext.Process(&output, source.begin().get(), source.length())) {
+ gfxCriticalNote << "Failed sanitizing font " << aOp.key().mHandle;
+ return false;
+ }
+ wr::Vec<uint8_t> bytes = output.forget();
+
+ aUpdates.AddRawFont(aOp.key(), bytes, aOp.fontIndex());
+ return true;
+}
+
+bool WebRenderBridgeParent::UpdateResources(
+ const nsTArray<OpUpdateResource>& aResourceUpdates,
+ const nsTArray<RefCountedShmem>& aSmallShmems,
+ const nsTArray<ipc::Shmem>& aLargeShmems,
+ wr::TransactionBuilder& aUpdates) {
+ wr::ShmSegmentsReader reader(aSmallShmems, aLargeShmems);
+ UniquePtr<ScheduleSharedSurfaceRelease> scheduleRelease;
+
+ while (GPUParent::MaybeFlushMemory()) {
+ // If the GPU process has memory pressure, preemptively unmap some of our
+ // shared memory images. If we are in the parent process, the expiration
+ // tracker itself will listen for the memory pressure event.
+ if (!SharedSurfacesParent::AgeAndExpireOneGeneration()) {
+ break;
+ }
+ }
+
+ bool success = true;
+ for (const auto& cmd : aResourceUpdates) {
+ switch (cmd.type()) {
+ case OpUpdateResource::TOpAddImage: {
+ const auto& op = cmd.get_OpAddImage();
+ if (!MatchesNamespace(op.key())) {
+ MOZ_ASSERT_UNREACHABLE("Stale image key (add)!");
+ break;
+ }
+
+ wr::Vec<uint8_t> bytes;
+ if (reader.Read(op.bytes(), bytes)) {
+ aUpdates.AddImage(op.key(), op.descriptor(), bytes);
+ } else {
+ gfxCriticalNote << "TOpAddImage failed";
+ success = false;
+ }
+ break;
+ }
+ case OpUpdateResource::TOpUpdateImage: {
+ const auto& op = cmd.get_OpUpdateImage();
+ if (!MatchesNamespace(op.key())) {
+ MOZ_ASSERT_UNREACHABLE("Stale image key (update)!");
+ break;
+ }
+
+ wr::Vec<uint8_t> bytes;
+ if (reader.Read(op.bytes(), bytes)) {
+ aUpdates.UpdateImageBuffer(op.key(), op.descriptor(), bytes);
+ } else {
+ gfxCriticalNote << "TOpUpdateImage failed";
+ success = false;
+ }
+ break;
+ }
+ case OpUpdateResource::TOpAddBlobImage: {
+ const auto& op = cmd.get_OpAddBlobImage();
+ if (!MatchesNamespace(op.key())) {
+ MOZ_ASSERT_UNREACHABLE("Stale blob image key (add)!");
+ break;
+ }
+
+ wr::Vec<uint8_t> bytes;
+ if (reader.Read(op.bytes(), bytes)) {
+ aUpdates.AddBlobImage(op.key(), op.descriptor(), mBlobTileSize, bytes,
+ wr::ToDeviceIntRect(op.visibleRect()));
+ } else {
+ gfxCriticalNote << "TOpAddBlobImage failed";
+ success = false;
+ }
+
+ break;
+ }
+ case OpUpdateResource::TOpUpdateBlobImage: {
+ const auto& op = cmd.get_OpUpdateBlobImage();
+ if (!MatchesNamespace(op.key())) {
+ MOZ_ASSERT_UNREACHABLE("Stale blob image key (update)!");
+ break;
+ }
+
+ wr::Vec<uint8_t> bytes;
+ if (reader.Read(op.bytes(), bytes)) {
+ aUpdates.UpdateBlobImage(op.key(), op.descriptor(), bytes,
+ wr::ToDeviceIntRect(op.visibleRect()),
+ wr::ToLayoutIntRect(op.dirtyRect()));
+ } else {
+ gfxCriticalNote << "TOpUpdateBlobImage failed";
+ success = false;
+ }
+ break;
+ }
+ case OpUpdateResource::TOpSetBlobImageVisibleArea: {
+ const auto& op = cmd.get_OpSetBlobImageVisibleArea();
+ if (!MatchesNamespace(op.key())) {
+ MOZ_ASSERT_UNREACHABLE("Stale blob image key (visible)!");
+ break;
+ }
+ aUpdates.SetBlobImageVisibleArea(op.key(),
+ wr::ToDeviceIntRect(op.area()));
+ break;
+ }
+ case OpUpdateResource::TOpAddSharedExternalImage: {
+ const auto& op = cmd.get_OpAddSharedExternalImage();
+ // gfxCriticalNote is called on error
+ if (!AddSharedExternalImage(op.externalImageId(), op.key(), aUpdates)) {
+ success = false;
+ }
+ break;
+ }
+ case OpUpdateResource::TOpPushExternalImageForTexture: {
+ const auto& op = cmd.get_OpPushExternalImageForTexture();
+ CompositableTextureHostRef texture;
+ texture = TextureHost::AsTextureHost(op.texture().AsParent());
+ // gfxCriticalNote is called on error
+ if (!PushExternalImageForTexture(op.externalImageId(), op.key(),
+ texture, op.isUpdate(), aUpdates)) {
+ success = false;
+ }
+ break;
+ }
+ case OpUpdateResource::TOpUpdateSharedExternalImage: {
+ const auto& op = cmd.get_OpUpdateSharedExternalImage();
+ // gfxCriticalNote is called on error
+ if (!UpdateSharedExternalImage(op.externalImageId(), op.key(),
+ op.dirtyRect(), aUpdates,
+ scheduleRelease)) {
+ success = false;
+ }
+ break;
+ }
+ case OpUpdateResource::TOpAddRawFont: {
+ if (!ReadRawFont(cmd.get_OpAddRawFont(), reader, aUpdates)) {
+ success = false;
+ }
+ break;
+ }
+ case OpUpdateResource::TOpAddFontDescriptor: {
+ const auto& op = cmd.get_OpAddFontDescriptor();
+ if (!MatchesNamespace(op.key())) {
+ MOZ_ASSERT_UNREACHABLE("Stale font key (add descriptor)!");
+ break;
+ }
+
+ wr::Vec<uint8_t> bytes;
+ if (reader.Read(op.bytes(), bytes)) {
+ aUpdates.AddFontDescriptor(op.key(), bytes, op.fontIndex());
+ } else {
+ gfxCriticalNote << "TOpAddFontDescriptor failed";
+ success = false;
+ }
+ break;
+ }
+ case OpUpdateResource::TOpAddFontInstance: {
+ const auto& op = cmd.get_OpAddFontInstance();
+ if (!MatchesNamespace(op.instanceKey()) ||
+ !MatchesNamespace(op.fontKey())) {
+ MOZ_ASSERT_UNREACHABLE("Stale font key (add instance)!");
+ break;
+ }
+
+ wr::Vec<uint8_t> variations;
+ if (reader.Read(op.variations(), variations)) {
+ aUpdates.AddFontInstance(op.instanceKey(), op.fontKey(),
+ op.glyphSize(), op.options().ptrOr(nullptr),
+ op.platformOptions().ptrOr(nullptr),
+ variations);
+ } else {
+ gfxCriticalNote << "TOpAddFontInstance failed";
+ success = false;
+ }
+ break;
+ }
+ case OpUpdateResource::TOpDeleteImage: {
+ const auto& op = cmd.get_OpDeleteImage();
+ if (!MatchesNamespace(op.key())) {
+ // TODO(aosmond): We should also assert here, but the callers are less
+ // careful about checking when cleaning up their old keys. We should
+ // perform an audit on image key usage.
+ break;
+ }
+
+ DeleteImage(op.key(), aUpdates);
+ break;
+ }
+ case OpUpdateResource::TOpDeleteBlobImage: {
+ const auto& op = cmd.get_OpDeleteBlobImage();
+ if (!MatchesNamespace(op.key())) {
+ MOZ_ASSERT_UNREACHABLE("Stale blob image key (delete)!");
+ break;
+ }
+
+ aUpdates.DeleteBlobImage(op.key());
+ break;
+ }
+ case OpUpdateResource::TOpDeleteFont: {
+ const auto& op = cmd.get_OpDeleteFont();
+ if (!MatchesNamespace(op.key())) {
+ MOZ_ASSERT_UNREACHABLE("Stale font key (delete)!");
+ break;
+ }
+
+ aUpdates.DeleteFont(op.key());
+ break;
+ }
+ case OpUpdateResource::TOpDeleteFontInstance: {
+ const auto& op = cmd.get_OpDeleteFontInstance();
+ if (!MatchesNamespace(op.key())) {
+ MOZ_ASSERT_UNREACHABLE("Stale font instance key (delete)!");
+ break;
+ }
+
+ aUpdates.DeleteFontInstance(op.key());
+ break;
+ }
+ case OpUpdateResource::T__None:
+ break;
+ }
+ }
+
+ if (scheduleRelease) {
+ // When software WR is enabled, shared surfaces are read during rendering
+ // rather than copied to the texture cache.
+ wr::Checkpoint when = mApi->GetBackendType() == WebRenderBackend::SOFTWARE
+ ? wr::Checkpoint::FrameRendered
+ : wr::Checkpoint::FrameTexturesUpdated;
+ aUpdates.Notify(when, std::move(scheduleRelease));
+ }
+
+ MOZ_ASSERT(success);
+ return success;
+}
+
+bool WebRenderBridgeParent::AddSharedExternalImage(
+ wr::ExternalImageId aExtId, wr::ImageKey aKey,
+ wr::TransactionBuilder& aResources) {
+ if (!MatchesNamespace(aKey)) {
+ MOZ_ASSERT_UNREACHABLE("Stale shared external image key (add)!");
+ return true;
+ }
+
+ auto key = wr::AsUint64(aKey);
+ auto it = mSharedSurfaceIds.find(key);
+ if (it != mSharedSurfaceIds.end()) {
+ gfxCriticalNote << "Readding known shared surface: " << key;
+ return false;
+ }
+
+ RefPtr<DataSourceSurface> dSurf = SharedSurfacesParent::Acquire(aExtId);
+ if (!dSurf) {
+ gfxCriticalNote
+ << "DataSourceSurface of SharedSurfaces does not exist for extId:"
+ << wr::AsUint64(aExtId);
+ return false;
+ }
+
+ mSharedSurfaceIds.insert(std::make_pair(key, aExtId));
+
+ // Prefer raw buffers, unless our backend requires native textures.
+ IntSize surfaceSize = dSurf->GetSize();
+ TextureHost::NativeTexturePolicy policy =
+ TextureHost::BackendNativeTexturePolicy(mApi->GetBackendType(),
+ surfaceSize);
+ auto imageType =
+ policy == TextureHost::NativeTexturePolicy::REQUIRE
+ ? wr::ExternalImageType::TextureHandle(wr::ImageBufferKind::Texture2D)
+ : wr::ExternalImageType::Buffer();
+ wr::ImageDescriptor descriptor(surfaceSize, dSurf->Stride(),
+ dSurf->GetFormat());
+ aResources.AddExternalImage(aKey, descriptor, aExtId, imageType, 0);
+ return true;
+}
+
+bool WebRenderBridgeParent::PushExternalImageForTexture(
+ wr::ExternalImageId aExtId, wr::ImageKey aKey, TextureHost* aTexture,
+ bool aIsUpdate, wr::TransactionBuilder& aResources) {
+ if (!MatchesNamespace(aKey)) {
+ MOZ_ASSERT_UNREACHABLE("Stale texture external image key!");
+ return true;
+ }
+
+ if (!aTexture) {
+ gfxCriticalNote << "TextureHost does not exist for extId:"
+ << wr::AsUint64(aExtId);
+ return false;
+ }
+
+ auto op = aIsUpdate ? TextureHost::UPDATE_IMAGE : TextureHost::ADD_IMAGE;
+ WebRenderTextureHost* wrTexture = aTexture->AsWebRenderTextureHost();
+ if (wrTexture) {
+ Range<wr::ImageKey> keys(&aKey, 1);
+ wrTexture->PushResourceUpdates(aResources, op, keys,
+ wrTexture->GetExternalImageKey());
+ auto it = mTextureHosts.find(wr::AsUint64(aKey));
+ MOZ_ASSERT((it == mTextureHosts.end() && !aIsUpdate) ||
+ (it != mTextureHosts.end() && aIsUpdate));
+ if (it != mTextureHosts.end()) {
+ // Release Texture if it exists.
+ ReleaseTextureOfImage(aKey);
+ }
+ mTextureHosts.emplace(wr::AsUint64(aKey),
+ CompositableTextureHostRef(aTexture));
+ return true;
+ }
+
+ RefPtr<DataSourceSurface> dSurf = aTexture->GetAsSurface();
+ if (!dSurf) {
+ gfxCriticalNote
+ << "TextureHost does not return DataSourceSurface for extId:"
+ << wr::AsUint64(aExtId);
+ return false;
+ }
+
+ DataSourceSurface::MappedSurface map;
+ if (!dSurf->Map(gfx::DataSourceSurface::MapType::READ, &map)) {
+ gfxCriticalNote << "DataSourceSurface failed to map for Image for extId:"
+ << wr::AsUint64(aExtId);
+ return false;
+ }
+
+ IntSize size = dSurf->GetSize();
+ wr::ImageDescriptor descriptor(size, map.mStride, dSurf->GetFormat());
+ wr::Vec<uint8_t> data;
+ data.PushBytes(Range<uint8_t>(map.mData, size.height * map.mStride));
+
+ if (op == TextureHost::UPDATE_IMAGE) {
+ aResources.UpdateImageBuffer(aKey, descriptor, data);
+ } else {
+ aResources.AddImage(aKey, descriptor, data);
+ }
+
+ dSurf->Unmap();
+
+ return true;
+}
+
+bool WebRenderBridgeParent::UpdateSharedExternalImage(
+ wr::ExternalImageId aExtId, wr::ImageKey aKey,
+ const ImageIntRect& aDirtyRect, wr::TransactionBuilder& aResources,
+ UniquePtr<ScheduleSharedSurfaceRelease>& aScheduleRelease) {
+ if (!MatchesNamespace(aKey)) {
+ MOZ_ASSERT_UNREACHABLE("Stale shared external image key (update)!");
+ return true;
+ }
+
+ auto key = wr::AsUint64(aKey);
+ auto it = mSharedSurfaceIds.find(key);
+ if (it == mSharedSurfaceIds.end()) {
+ gfxCriticalNote << "Updating unknown shared surface: " << key;
+ return false;
+ }
+
+ RefPtr<DataSourceSurface> dSurf;
+ if (it->second == aExtId) {
+ dSurf = SharedSurfacesParent::Get(aExtId);
+ } else {
+ dSurf = SharedSurfacesParent::Acquire(aExtId);
+ }
+
+ if (!dSurf) {
+ gfxCriticalNote << "Shared surface does not exist for extId:"
+ << wr::AsUint64(aExtId);
+ return false;
+ }
+
+ if (!(it->second == aExtId)) {
+ // We already have a mapping for this image key, so ensure we release the
+ // previous external image ID. This can happen when an image is animated,
+ // and it is changing the external image that the animation points to.
+ if (!aScheduleRelease) {
+ aScheduleRelease = MakeUnique<ScheduleSharedSurfaceRelease>(this);
+ }
+ aScheduleRelease->Add(aKey, it->second);
+ it->second = aExtId;
+ }
+
+ // Prefer raw buffers, unless our backend requires native textures.
+ IntSize surfaceSize = dSurf->GetSize();
+ TextureHost::NativeTexturePolicy policy =
+ TextureHost::BackendNativeTexturePolicy(mApi->GetBackendType(),
+ surfaceSize);
+ auto imageType =
+ policy == TextureHost::NativeTexturePolicy::REQUIRE
+ ? wr::ExternalImageType::TextureHandle(wr::ImageBufferKind::Texture2D)
+ : wr::ExternalImageType::Buffer();
+ wr::ImageDescriptor descriptor(surfaceSize, dSurf->Stride(),
+ dSurf->GetFormat());
+ aResources.UpdateExternalImageWithDirtyRect(
+ aKey, descriptor, aExtId, imageType, wr::ToDeviceIntRect(aDirtyRect), 0);
+
+ return true;
+}
+
+void WebRenderBridgeParent::ObserveSharedSurfaceRelease(
+ const nsTArray<wr::ExternalImageKeyPair>& aPairs,
+ const bool& aFromCheckpoint) {
+ if (!mDestroyed) {
+ Unused << SendWrReleasedImages(aPairs);
+ }
+
+ if (!aFromCheckpoint && mAsyncImageManager) {
+ // We failed to receive a checkpoint notification, so we are releasing these
+ // surfaces blind. Let's wait until the next epoch to complete releasing.
+ for (const auto& pair : aPairs) {
+ mAsyncImageManager->HoldExternalImage(mPipelineId, mWrEpoch, pair.id);
+ }
+ return;
+ }
+
+ // We hit the checkpoint, so we know we can safely release the surfaces now.
+ for (const auto& pair : aPairs) {
+ SharedSurfacesParent::Release(pair.id);
+ }
+}
+
+mozilla::ipc::IPCResult WebRenderBridgeParent::RecvUpdateResources(
+ const wr::IdNamespace& aIdNamespace,
+ nsTArray<OpUpdateResource>&& aResourceUpdates,
+ nsTArray<RefCountedShmem>&& aSmallShmems,
+ nsTArray<ipc::Shmem>&& aLargeShmems) {
+ const bool isValidMessage = aIdNamespace == mIdNamespace;
+
+ if (mDestroyed || !isValidMessage) {
+ wr::IpcResourceUpdateQueue::ReleaseShmems(this, aSmallShmems);
+ wr::IpcResourceUpdateQueue::ReleaseShmems(this, aLargeShmems);
+ return IPC_OK();
+ }
+
+ LOG("WebRenderBridgeParent::RecvUpdateResources() PipelineId %" PRIx64
+ " Id %" PRIx64 " root %d",
+ wr::AsUint64(mPipelineId), wr::AsUint64(mApi->GetId()),
+ IsRootWebRenderBridgeParent());
+
+ wr::TransactionBuilder txn(mApi);
+ txn.SetLowPriority(!IsRootWebRenderBridgeParent());
+
+ Unused << GetNextWrEpoch();
+
+ bool success =
+ UpdateResources(aResourceUpdates, aSmallShmems, aLargeShmems, txn);
+ wr::IpcResourceUpdateQueue::ReleaseShmems(this, aSmallShmems);
+ wr::IpcResourceUpdateQueue::ReleaseShmems(this, aLargeShmems);
+
+ // Even when txn.IsResourceUpdatesEmpty() is true, there could be resource
+ // updates. It is handled by WebRenderTextureHostWrapper. In this case
+ // txn.IsRenderedFrameInvalidated() becomes true.
+ if (!txn.IsResourceUpdatesEmpty() || txn.IsRenderedFrameInvalidated()) {
+ // There are resource updates, then we update Epoch of transaction.
+ txn.UpdateEpoch(mPipelineId, mWrEpoch);
+ mAsyncImageManager->SetWillGenerateFrame();
+ ScheduleGenerateFrame(wr::RenderReasons::RESOURCE_UPDATE);
+ } else {
+ // If TransactionBuilder does not have resource updates nor display list,
+ // ScheduleGenerateFrame is not triggered via SceneBuilder and there is no
+ // need to update WrEpoch.
+ // Then we want to rollback WrEpoch. See Bug 1490117.
+ RollbackWrEpoch();
+ }
+
+ mApi->SendTransaction(txn);
+
+ if (!success) {
+ return IPC_FAIL(this, "Invalid WebRender resource data shmem or address.");
+ }
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult WebRenderBridgeParent::RecvDeleteCompositorAnimations(
+ nsTArray<uint64_t>&& aIds) {
+ if (mDestroyed) {
+ return IPC_OK();
+ }
+
+ LOG("WebRenderBridgeParent::RecvDeleteCompositorAnimations() PipelineId "
+ "%" PRIx64 " Id %" PRIx64 " root %d",
+ wr::AsUint64(mPipelineId), wr::AsUint64(mApi->GetId()),
+ IsRootWebRenderBridgeParent());
+
+ // Once mWrEpoch has been rendered, we can delete these compositor animations
+ mCompositorAnimationsToDelete.push(
+ CompositorAnimationIdsForEpoch(mWrEpoch, std::move(aIds)));
+ return IPC_OK();
+}
+
+void WebRenderBridgeParent::RemoveEpochDataPriorTo(
+ const wr::Epoch& aRenderedEpoch) {
+ if (RefPtr<OMTASampler> sampler = GetOMTASampler()) {
+ sampler->RemoveEpochDataPriorTo(mCompositorAnimationsToDelete,
+ mActiveAnimations, aRenderedEpoch);
+ }
+}
+
+bool WebRenderBridgeParent::IsRootWebRenderBridgeParent() const {
+ return !!mWidget;
+}
+
+void WebRenderBridgeParent::BeginRecording(const TimeStamp& aRecordingStart) {
+ mApi->BeginRecording(aRecordingStart, mPipelineId);
+}
+
+RefPtr<wr::WebRenderAPI::EndRecordingPromise>
+WebRenderBridgeParent::EndRecording() {
+ return mApi->EndRecording();
+}
+
+void WebRenderBridgeParent::AddPendingScrollPayload(
+ CompositionPayload& aPayload, const VsyncId& aCompositeStartId) {
+ auto pendingScrollPayloads = mPendingScrollPayloads.Lock();
+ nsTArray<CompositionPayload>* payloads =
+ pendingScrollPayloads->GetOrInsertNew(aCompositeStartId.mId);
+
+ payloads->AppendElement(aPayload);
+}
+
+nsTArray<CompositionPayload> WebRenderBridgeParent::TakePendingScrollPayload(
+ const VsyncId& aCompositeStartId) {
+ auto pendingScrollPayloads = mPendingScrollPayloads.Lock();
+ nsTArray<CompositionPayload> payload;
+ if (nsTArray<CompositionPayload>* storedPayload =
+ pendingScrollPayloads->Get(aCompositeStartId.mId)) {
+ payload.AppendElements(std::move(*storedPayload));
+ pendingScrollPayloads->Remove(aCompositeStartId.mId);
+ }
+ return payload;
+}
+
+CompositorBridgeParent* WebRenderBridgeParent::GetRootCompositorBridgeParent()
+ const {
+ if (!mCompositorBridge) {
+ return nullptr;
+ }
+
+ if (IsRootWebRenderBridgeParent()) {
+ // This WebRenderBridgeParent is attached to the root
+ // CompositorBridgeParent.
+ return static_cast<CompositorBridgeParent*>(mCompositorBridge);
+ }
+
+ // Otherwise, this WebRenderBridgeParent is attached to a
+ // ContentCompositorBridgeParent so we have an extra level of
+ // indirection to unravel.
+ CompositorBridgeParent::LayerTreeState* lts =
+ CompositorBridgeParent::GetIndirectShadowTree(GetLayersId());
+ if (!lts) {
+ return nullptr;
+ }
+ return lts->mParent;
+}
+
+RefPtr<WebRenderBridgeParent>
+WebRenderBridgeParent::GetRootWebRenderBridgeParent() const {
+ CompositorBridgeParent* cbp = GetRootCompositorBridgeParent();
+ if (!cbp) {
+ return nullptr;
+ }
+
+ return cbp->GetWebRenderBridgeParent();
+}
+
+void WebRenderBridgeParent::UpdateAPZFocusState(const FocusTarget& aFocus) {
+ CompositorBridgeParent* cbp = GetRootCompositorBridgeParent();
+ if (!cbp) {
+ return;
+ }
+ LayersId rootLayersId = cbp->RootLayerTreeId();
+ if (RefPtr<APZUpdater> apz = cbp->GetAPZUpdater()) {
+ apz->UpdateFocusState(rootLayersId, GetLayersId(), aFocus);
+ }
+}
+
+void WebRenderBridgeParent::UpdateAPZScrollData(const wr::Epoch& aEpoch,
+ WebRenderScrollData&& aData) {
+ CompositorBridgeParent* cbp = GetRootCompositorBridgeParent();
+ if (!cbp) {
+ return;
+ }
+ LayersId rootLayersId = cbp->RootLayerTreeId();
+ if (RefPtr<APZUpdater> apz = cbp->GetAPZUpdater()) {
+ apz->UpdateScrollDataAndTreeState(rootLayersId, GetLayersId(), aEpoch,
+ std::move(aData));
+ }
+}
+
+void WebRenderBridgeParent::UpdateAPZScrollOffsets(
+ ScrollUpdatesMap&& aUpdates, uint32_t aPaintSequenceNumber) {
+ CompositorBridgeParent* cbp = GetRootCompositorBridgeParent();
+ if (!cbp) {
+ return;
+ }
+ LayersId rootLayersId = cbp->RootLayerTreeId();
+ if (RefPtr<APZUpdater> apz = cbp->GetAPZUpdater()) {
+ apz->UpdateScrollOffsets(rootLayersId, GetLayersId(), std::move(aUpdates),
+ aPaintSequenceNumber);
+ }
+}
+
+void WebRenderBridgeParent::SetAPZSampleTime() {
+ CompositorBridgeParent* cbp = GetRootCompositorBridgeParent();
+ if (!cbp) {
+ return;
+ }
+ if (RefPtr<APZSampler> apz = cbp->GetAPZSampler()) {
+ SampleTime animationTime;
+ if (Maybe<TimeStamp> testTime = cbp->GetTestingTimeStamp()) {
+ animationTime = SampleTime::FromTest(*testTime);
+ } else {
+ animationTime = mCompositorScheduler->GetLastComposeTime();
+ }
+ TimeDuration frameInterval = cbp->GetVsyncInterval();
+ // As with the non-webrender codepath in AsyncCompositionManager, we want to
+ // use the timestamp for the next vsync when advancing animations.
+ if (frameInterval != TimeDuration::Forever()) {
+ animationTime = animationTime + frameInterval;
+ }
+ apz->SetSampleTime(animationTime);
+ }
+}
+
+bool WebRenderBridgeParent::SetDisplayList(
+ const LayoutDeviceRect& aRect, ipc::ByteBuf&& aDLItems,
+ ipc::ByteBuf&& aDLCache, ipc::ByteBuf&& aSpatialTreeDL,
+ const wr::BuiltDisplayListDescriptor& aDLDesc,
+ const nsTArray<OpUpdateResource>& aResourceUpdates,
+ const nsTArray<RefCountedShmem>& aSmallShmems,
+ const nsTArray<ipc::Shmem>& aLargeShmems, const TimeStamp& aTxnStartTime,
+ wr::TransactionBuilder& aTxn, wr::Epoch aWrEpoch,
+ bool aObserveLayersUpdate) {
+ bool success =
+ UpdateResources(aResourceUpdates, aSmallShmems, aLargeShmems, aTxn);
+
+ wr::Vec<uint8_t> dlItems(std::move(aDLItems));
+ wr::Vec<uint8_t> dlCache(std::move(aDLCache));
+ wr::Vec<uint8_t> dlSpatialTreeData(std::move(aSpatialTreeDL));
+
+ if (IsRootWebRenderBridgeParent()) {
+#ifdef MOZ_WIDGET_GTK
+ if (mWidget->AsGTK()) {
+ mWidget->AsGTK()->RemoteLayoutSizeUpdated(aRect);
+ }
+#endif
+ LayoutDeviceIntSize widgetSize = mWidget->GetClientSize();
+ LayoutDeviceIntRect rect =
+ LayoutDeviceIntRect(LayoutDeviceIntPoint(), widgetSize);
+ aTxn.SetDocumentView(rect);
+ }
+ aTxn.SetDisplayList(aWrEpoch, mPipelineId, aDLDesc, dlItems, dlCache,
+ dlSpatialTreeData);
+
+ if (aObserveLayersUpdate) {
+ aTxn.Notify(
+ wr::Checkpoint::SceneBuilt,
+ MakeUnique<ScheduleObserveLayersUpdate>(
+ mCompositorBridge, GetLayersId(), mChildLayersObserverEpoch, true));
+ }
+
+ if (!IsRootWebRenderBridgeParent()) {
+ aTxn.Notify(wr::Checkpoint::SceneBuilt, MakeUnique<SceneBuiltNotification>(
+ this, aWrEpoch, aTxnStartTime));
+ }
+
+ mApi->SendTransaction(aTxn);
+
+ // We will schedule generating a frame after the scene
+ // build is done, so we don't need to do it here.
+ return success;
+}
+
+bool WebRenderBridgeParent::ProcessDisplayListData(
+ DisplayListData& aDisplayList, wr::Epoch aWrEpoch,
+ const TimeStamp& aTxnStartTime, bool aValidTransaction,
+ bool aObserveLayersUpdate) {
+ wr::TransactionBuilder txn(mApi);
+ Maybe<wr::AutoTransactionSender> sender;
+
+ if (aDisplayList.mScrollData && !aDisplayList.mScrollData->Validate()) {
+ // If the scroll data is invalid, the entire transaction needs to be dropped
+ // because the scroll data and the display list cross-reference each other.
+ MOZ_ASSERT(
+ false,
+ "Content sent malformed scroll data (or validation check has a bug)");
+ aValidTransaction = false;
+ }
+
+ if (!aValidTransaction) {
+ return true;
+ }
+
+ MOZ_ASSERT(aDisplayList.mIdNamespace == mIdNamespace);
+
+ // Note that this needs to happen before the display list transaction is
+ // sent to WebRender, so that the UpdateHitTestingTree call is guaranteed to
+ // be in the updater queue at the time that the scene swap completes.
+ if (aDisplayList.mScrollData) {
+ UpdateAPZScrollData(aWrEpoch, std::move(aDisplayList.mScrollData.ref()));
+ }
+
+ txn.SetLowPriority(!IsRootWebRenderBridgeParent());
+ sender.emplace(mApi, &txn);
+ bool success = true;
+
+ success =
+ ProcessWebRenderParentCommands(aDisplayList.mCommands, txn) && success;
+
+ if (aDisplayList.mDLItems && aDisplayList.mDLCache &&
+ aDisplayList.mDLSpatialTree) {
+ success = SetDisplayList(
+ aDisplayList.mRect, std::move(aDisplayList.mDLItems.ref()),
+ std::move(aDisplayList.mDLCache.ref()),
+ std::move(aDisplayList.mDLSpatialTree.ref()),
+ aDisplayList.mDLDesc, aDisplayList.mResourceUpdates,
+ aDisplayList.mSmallShmems, aDisplayList.mLargeShmems,
+ aTxnStartTime, txn, aWrEpoch, aObserveLayersUpdate) &&
+ success;
+ }
+ return success;
+}
+
+mozilla::ipc::IPCResult WebRenderBridgeParent::RecvSetDisplayList(
+ DisplayListData&& aDisplayList, nsTArray<OpDestroy>&& aToDestroy,
+ const uint64_t& aFwdTransactionId, const TransactionId& aTransactionId,
+ const bool& aContainsSVGGroup, const VsyncId& aVsyncId,
+ const TimeStamp& aVsyncStartTime, const TimeStamp& aRefreshStartTime,
+ const TimeStamp& aTxnStartTime, const nsACString& aTxnURL,
+ const TimeStamp& aFwdTime, nsTArray<CompositionPayload>&& aPayloads) {
+ if (mDestroyed) {
+ for (const auto& op : aToDestroy) {
+ DestroyActor(op);
+ }
+ wr::IpcResourceUpdateQueue::ReleaseShmems(this, aDisplayList.mSmallShmems);
+ wr::IpcResourceUpdateQueue::ReleaseShmems(this, aDisplayList.mLargeShmems);
+ return IPC_OK();
+ }
+
+ LOG("WebRenderBridgeParent::RecvSetDisplayList() PipelineId %" PRIx64
+ " Id %" PRIx64 " root %d",
+ wr::AsUint64(mPipelineId), wr::AsUint64(mApi->GetId()),
+ IsRootWebRenderBridgeParent());
+
+ if (!IsRootWebRenderBridgeParent()) {
+ CrashReporter::AnnotateCrashReport(CrashReporter::Annotation::URL, aTxnURL);
+ }
+
+ CompositorBridgeParent* cbp = GetRootCompositorBridgeParent();
+ uint64_t innerWindowId = cbp ? cbp->GetInnerWindowId() : 0;
+ AUTO_PROFILER_TRACING_MARKER_INNERWINDOWID("Paint", "SetDisplayList",
+ GRAPHICS, innerWindowId);
+ UpdateFwdTransactionId(aFwdTransactionId);
+
+ // This ensures that destroy operations are always processed. It is not safe
+ // to early-return from RecvDPEnd without doing so.
+ AutoWebRenderBridgeParentAsyncMessageSender autoAsyncMessageSender(
+ this, &aToDestroy);
+
+ wr::Epoch wrEpoch = GetNextWrEpoch();
+
+ mReceivedDisplayList = true;
+
+ if (aDisplayList.mScrollData && aDisplayList.mScrollData->IsFirstPaint()) {
+ mIsFirstPaint = true;
+ }
+
+ bool validTransaction = aDisplayList.mIdNamespace == mIdNamespace;
+ bool observeLayersUpdate = ShouldParentObserveEpoch();
+
+ bool success = ProcessDisplayListData(aDisplayList, wrEpoch, aTxnStartTime,
+ validTransaction, observeLayersUpdate);
+
+ if (!validTransaction && observeLayersUpdate) {
+ mCompositorBridge->ObserveLayersUpdate(GetLayersId(),
+ mChildLayersObserverEpoch, true);
+ }
+
+ if (!IsRootWebRenderBridgeParent()) {
+ aPayloads.AppendElement(
+ CompositionPayload{CompositionPayloadType::eContentPaint, aFwdTime});
+ }
+
+ HoldPendingTransactionId(wrEpoch, aTransactionId, aContainsSVGGroup, aVsyncId,
+ aVsyncStartTime, aRefreshStartTime, aTxnStartTime,
+ aTxnURL, aFwdTime, mIsFirstPaint,
+ std::move(aPayloads));
+ mIsFirstPaint = false;
+
+ if (!validTransaction) {
+ // Pretend we composited since someone is wating for this event,
+ // though DisplayList was not pushed to webrender.
+ if (CompositorBridgeParent* cbp = GetRootCompositorBridgeParent()) {
+ TimeStamp now = TimeStamp::Now();
+ cbp->NotifyPipelineRendered(mPipelineId, wrEpoch, VsyncId(), now, now,
+ now);
+ }
+ }
+
+ wr::IpcResourceUpdateQueue::ReleaseShmems(this, aDisplayList.mSmallShmems);
+ wr::IpcResourceUpdateQueue::ReleaseShmems(this, aDisplayList.mLargeShmems);
+
+ if (!success) {
+ return IPC_FAIL(this, "Failed to process DisplayListData.");
+ }
+
+ return IPC_OK();
+}
+
+bool WebRenderBridgeParent::ProcessEmptyTransactionUpdates(
+ TransactionData& aData, bool* aScheduleComposite) {
+ *aScheduleComposite = false;
+ wr::TransactionBuilder txn(mApi);
+ txn.SetLowPriority(!IsRootWebRenderBridgeParent());
+
+ if (!aData.mScrollUpdates.IsEmpty()) {
+ UpdateAPZScrollOffsets(std::move(aData.mScrollUpdates),
+ aData.mPaintSequenceNumber);
+ }
+
+ // Update WrEpoch for UpdateResources() and ProcessWebRenderParentCommands().
+ // WrEpoch is used to manage ExternalImages lifetimes in
+ // AsyncImagePipelineManager.
+ Unused << GetNextWrEpoch();
+
+ const bool validTransaction = aData.mIdNamespace == mIdNamespace;
+ bool success = true;
+
+ if (validTransaction) {
+ success = UpdateResources(aData.mResourceUpdates, aData.mSmallShmems,
+ aData.mLargeShmems, txn);
+ if (!aData.mCommands.IsEmpty()) {
+ success = ProcessWebRenderParentCommands(aData.mCommands, txn) && success;
+ }
+ }
+
+ if (ShouldParentObserveEpoch()) {
+ txn.Notify(
+ wr::Checkpoint::SceneBuilt,
+ MakeUnique<ScheduleObserveLayersUpdate>(
+ mCompositorBridge, GetLayersId(), mChildLayersObserverEpoch, true));
+ }
+
+ // Even when txn.IsResourceUpdatesEmpty() is true, there could be resource
+ // updates. It is handled by WebRenderTextureHostWrapper. In this case
+ // txn.IsRenderedFrameInvalidated() becomes true.
+ if (!txn.IsResourceUpdatesEmpty() || txn.IsRenderedFrameInvalidated()) {
+ // There are resource updates, then we update Epoch of transaction.
+ txn.UpdateEpoch(mPipelineId, mWrEpoch);
+ *aScheduleComposite = true;
+ } else {
+ // If TransactionBuilder does not have resource updates nor display list,
+ // ScheduleGenerateFrame is not triggered via SceneBuilder and there is no
+ // need to update WrEpoch.
+ // Then we want to rollback WrEpoch. See Bug 1490117.
+ RollbackWrEpoch();
+ }
+
+ if (!txn.IsEmpty()) {
+ mApi->SendTransaction(txn);
+ }
+
+ if (*aScheduleComposite) {
+ mAsyncImageManager->SetWillGenerateFrame();
+ }
+
+ return success;
+}
+
+mozilla::ipc::IPCResult WebRenderBridgeParent::RecvEmptyTransaction(
+ const FocusTarget& aFocusTarget, Maybe<TransactionData>&& aTransactionData,
+ nsTArray<OpDestroy>&& aToDestroy, const uint64_t& aFwdTransactionId,
+ const TransactionId& aTransactionId, const VsyncId& aVsyncId,
+ const TimeStamp& aVsyncStartTime, const TimeStamp& aRefreshStartTime,
+ const TimeStamp& aTxnStartTime, const nsACString& aTxnURL,
+ const TimeStamp& aFwdTime, nsTArray<CompositionPayload>&& aPayloads) {
+ if (mDestroyed) {
+ for (const auto& op : aToDestroy) {
+ DestroyActor(op);
+ }
+ if (aTransactionData) {
+ wr::IpcResourceUpdateQueue::ReleaseShmems(this,
+ aTransactionData->mSmallShmems);
+ wr::IpcResourceUpdateQueue::ReleaseShmems(this,
+ aTransactionData->mLargeShmems);
+ }
+ return IPC_OK();
+ }
+
+ LOG("WebRenderBridgeParent::RecvEmptyTransaction() PipelineId %" PRIx64
+ " Id %" PRIx64 " root %d",
+ wr::AsUint64(mPipelineId), wr::AsUint64(mApi->GetId()),
+ IsRootWebRenderBridgeParent());
+
+ if (!IsRootWebRenderBridgeParent()) {
+ CrashReporter::AnnotateCrashReport(CrashReporter::Annotation::URL, aTxnURL);
+ }
+
+ AUTO_PROFILER_TRACING_MARKER("Paint", "EmptyTransaction", GRAPHICS);
+ UpdateFwdTransactionId(aFwdTransactionId);
+
+ // This ensures that destroy operations are always processed. It is not safe
+ // to early-return without doing so.
+ AutoWebRenderBridgeParentAsyncMessageSender autoAsyncMessageSender(
+ this, &aToDestroy);
+
+ UpdateAPZFocusState(aFocusTarget);
+
+ bool scheduleAnyComposite = false;
+ wr::RenderReasons renderReasons = wr::RenderReasons::NONE;
+
+ bool success = true;
+ if (aTransactionData) {
+ bool scheduleComposite = false;
+ success =
+ ProcessEmptyTransactionUpdates(*aTransactionData, &scheduleComposite);
+ scheduleAnyComposite = scheduleAnyComposite || scheduleComposite;
+ renderReasons |= wr::RenderReasons::RESOURCE_UPDATE;
+ }
+
+ // If we are going to kick off a new composite as a result of this
+ // transaction, or if there are already composite-triggering pending
+ // transactions inflight, then set sendDidComposite to false because we will
+ // send the DidComposite message after the composite occurs.
+ // If there are no pending transactions and we're not going to do a
+ // composite, then we leave sendDidComposite as true so we just send
+ // the DidComposite notification now.
+ bool sendDidComposite =
+ !scheduleAnyComposite && mPendingTransactionIds.empty();
+
+ // Only register a value for CONTENT_FRAME_TIME telemetry if we actually drew
+ // something. It is for consistency with disabling WebRender.
+ HoldPendingTransactionId(mWrEpoch, aTransactionId, false, aVsyncId,
+ aVsyncStartTime, aRefreshStartTime, aTxnStartTime,
+ aTxnURL, aFwdTime,
+ /* aIsFirstPaint */ false, std::move(aPayloads),
+ /* aUseForTelemetry */ scheduleAnyComposite);
+
+ if (scheduleAnyComposite) {
+ ScheduleGenerateFrame(renderReasons);
+ } else if (sendDidComposite) {
+ // The only thing in the pending transaction id queue should be the entry
+ // we just added, and now we're going to pretend we rendered it
+ MOZ_ASSERT(mPendingTransactionIds.size() == 1);
+ if (CompositorBridgeParent* cbp = GetRootCompositorBridgeParent()) {
+ TimeStamp now = TimeStamp::Now();
+ cbp->NotifyPipelineRendered(mPipelineId, mWrEpoch, VsyncId(), now, now,
+ now);
+ }
+ }
+
+ if (aTransactionData) {
+ wr::IpcResourceUpdateQueue::ReleaseShmems(this,
+ aTransactionData->mSmallShmems);
+ wr::IpcResourceUpdateQueue::ReleaseShmems(this,
+ aTransactionData->mLargeShmems);
+ }
+
+ if (!success) {
+ return IPC_FAIL(this, "Failed to process empty transaction update.");
+ }
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult WebRenderBridgeParent::RecvSetFocusTarget(
+ const FocusTarget& aFocusTarget) {
+ UpdateAPZFocusState(aFocusTarget);
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult WebRenderBridgeParent::RecvParentCommands(
+ const wr::IdNamespace& aIdNamespace,
+ nsTArray<WebRenderParentCommand>&& aCommands) {
+ if (mDestroyed) {
+ return IPC_OK();
+ }
+
+ const bool isValidMessage = aIdNamespace == mIdNamespace;
+ if (!isValidMessage) {
+ return IPC_OK();
+ }
+
+ LOG("WebRenderBridgeParent::RecvParentCommands() PipelineId %" PRIx64
+ " Id %" PRIx64 " root %d",
+ wr::AsUint64(mPipelineId), wr::AsUint64(mApi->GetId()),
+ IsRootWebRenderBridgeParent());
+
+ wr::TransactionBuilder txn(mApi);
+ txn.SetLowPriority(!IsRootWebRenderBridgeParent());
+ bool success = ProcessWebRenderParentCommands(aCommands, txn);
+ mApi->SendTransaction(txn);
+
+ if (!success) {
+ return IPC_FAIL(this, "Invalid parent command found");
+ }
+
+ return IPC_OK();
+}
+
+bool WebRenderBridgeParent::ProcessWebRenderParentCommands(
+ const nsTArray<WebRenderParentCommand>& aCommands,
+ wr::TransactionBuilder& aTxn) {
+ // Transaction for async image pipeline that uses ImageBridge always need to
+ // be non low priority.
+ wr::TransactionBuilder txnForImageBridge(mApi);
+ wr::AutoTransactionSender sender(mApi, &txnForImageBridge);
+
+ bool success = true;
+ for (nsTArray<WebRenderParentCommand>::index_type i = 0;
+ i < aCommands.Length(); ++i) {
+ const WebRenderParentCommand& cmd = aCommands[i];
+ switch (cmd.type()) {
+ case WebRenderParentCommand::TOpAddPipelineIdForCompositable: {
+ const OpAddPipelineIdForCompositable& op =
+ cmd.get_OpAddPipelineIdForCompositable();
+ AddPipelineIdForCompositable(op.pipelineId(), op.handle(), op.owner(),
+ aTxn, txnForImageBridge);
+ break;
+ }
+ case WebRenderParentCommand::TOpRemovePipelineIdForCompositable: {
+ const OpRemovePipelineIdForCompositable& op =
+ cmd.get_OpRemovePipelineIdForCompositable();
+ RemovePipelineIdForCompositable(op.pipelineId(), aTxn);
+ break;
+ }
+ case WebRenderParentCommand::TOpReleaseTextureOfImage: {
+ const OpReleaseTextureOfImage& op = cmd.get_OpReleaseTextureOfImage();
+ ReleaseTextureOfImage(op.key());
+ break;
+ }
+ case WebRenderParentCommand::TOpUpdateAsyncImagePipeline: {
+ const OpUpdateAsyncImagePipeline& op =
+ cmd.get_OpUpdateAsyncImagePipeline();
+ mAsyncImageManager->UpdateAsyncImagePipeline(
+ op.pipelineId(), op.scBounds(), op.rotation(), op.filter(),
+ op.mixBlendMode());
+ auto* list = mApi->GetPendingRemoteTextureInfoList();
+ MOZ_ASSERT_IF(IsRootWebRenderBridgeParent(), !list);
+ mAsyncImageManager->ApplyAsyncImageForPipeline(op.pipelineId(), aTxn,
+ txnForImageBridge, list);
+ break;
+ }
+ case WebRenderParentCommand::TOpUpdatedAsyncImagePipeline: {
+ const OpUpdatedAsyncImagePipeline& op =
+ cmd.get_OpUpdatedAsyncImagePipeline();
+ aTxn.InvalidateRenderedFrame(wr::RenderReasons::ASYNC_IMAGE);
+ auto* list = mApi->GetPendingRemoteTextureInfoList();
+ MOZ_ASSERT_IF(IsRootWebRenderBridgeParent(), !list);
+ mAsyncImageManager->ApplyAsyncImageForPipeline(op.pipelineId(), aTxn,
+ txnForImageBridge, list);
+ break;
+ }
+ case WebRenderParentCommand::TCompositableOperation: {
+ if (!ReceiveCompositableUpdate(cmd.get_CompositableOperation())) {
+ NS_ERROR("ReceiveCompositableUpdate failed");
+ }
+ break;
+ }
+ case WebRenderParentCommand::TOpAddCompositorAnimations: {
+ const OpAddCompositorAnimations& op =
+ cmd.get_OpAddCompositorAnimations();
+ CompositorAnimations data(std::move(op.data()));
+ // AnimationHelper::GetNextCompositorAnimationsId() encodes the child
+ // process PID in the upper 32 bits of the id, verify that this is as
+ // expected.
+ if ((data.id() >> 32) != (uint64_t)OtherPid()) {
+ gfxCriticalNote << "TOpAddCompositorAnimations bad id";
+ success = false;
+ continue;
+ }
+ if (data.animations().Length()) {
+ if (RefPtr<OMTASampler> sampler = GetOMTASampler()) {
+ sampler->SetAnimations(data.id(), GetLayersId(), data.animations());
+ const auto activeAnim = mActiveAnimations.find(data.id());
+ if (activeAnim == mActiveAnimations.end()) {
+ mActiveAnimations.emplace(data.id(), mWrEpoch);
+ } else {
+ // Update wr::Epoch if the animation already exists.
+ activeAnim->second = mWrEpoch;
+ }
+ }
+ }
+ break;
+ }
+ default: {
+ // other commands are handle on the child
+ break;
+ }
+ }
+ }
+
+ MOZ_ASSERT(success);
+ return success;
+}
+
+void WebRenderBridgeParent::FlushSceneBuilds() {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+
+ // Since we are sending transactions through the scene builder thread, we need
+ // to block until all the inflight transactions have been processed. This
+ // flush message blocks until all previously sent scenes have been built
+ // and received by the render backend thread.
+ mApi->FlushSceneBuilder();
+ // The post-swap hook for async-scene-building calls the
+ // ScheduleRenderOnCompositorThread function from the scene builder thread,
+ // which then triggers a call to ScheduleGenerateFrame() on the compositor
+ // thread. But since *this* function is running on the compositor thread,
+ // that scheduling will not happen until this call stack unwinds (or we
+ // could spin a nested event loop, but that's more messy). Instead, we
+ // simulate it ourselves by calling ScheduleGenerateFrame() directly.
+ // Note also that the post-swap hook will run and do another
+ // ScheduleGenerateFrame() after we unwind here, so we will end up with an
+ // extra render/composite that is probably avoidable, but in practice we
+ // shouldn't be calling this function all that much in production so this
+ // is probably fine. If it becomes an issue we can add more state tracking
+ // machinery to optimize it away.
+ ScheduleGenerateFrame(wr::RenderReasons::FLUSH);
+}
+
+void WebRenderBridgeParent::FlushFrameGeneration(wr::RenderReasons aReasons) {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+ MOZ_ASSERT(IsRootWebRenderBridgeParent()); // This function is only useful on
+ // the root WRBP
+
+ // This forces a new GenerateFrame transaction to be sent to the render
+ // backend thread, if one is pending. This doesn't block on any other threads.
+ if (mCompositorScheduler->NeedsComposite()) {
+ mCompositorScheduler->CancelCurrentCompositeTask();
+ // Update timestamp of scheduler for APZ and animation.
+ mCompositorScheduler->UpdateLastComposeTime();
+ MaybeGenerateFrame(VsyncId(), /* aForceGenerateFrame */ true,
+ aReasons | wr::RenderReasons::FLUSH);
+ }
+}
+
+void WebRenderBridgeParent::FlushFramePresentation() {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+
+ // This sends a message to the render backend thread to send a message
+ // to the renderer thread, and waits for that message to be processed. So
+ // this effectively blocks on the render backend and renderer threads,
+ // following the same codepath that WebRender takes to render and composite
+ // a frame.
+ mApi->WaitFlushed();
+}
+
+void WebRenderBridgeParent::DisableNativeCompositor() {
+ // Make sure that SceneBuilder thread does not have a task.
+ mApi->FlushSceneBuilder();
+ // Disable WebRender's native compositor usage
+ mApi->EnableNativeCompositor(false);
+ // Ensure we generate and render a frame immediately.
+ ScheduleForcedGenerateFrame(wr::RenderReasons::CONFIG_CHANGE);
+
+ mDisablingNativeCompositor = true;
+}
+
+void WebRenderBridgeParent::UpdateQualitySettings() {
+ if (!IsRootWebRenderBridgeParent()) {
+ MOZ_ASSERT_UNREACHABLE("unexpected to be called");
+ return;
+ }
+ wr::TransactionBuilder txn(mApi);
+ txn.UpdateQualitySettings(gfxVars::ForceSubpixelAAWherePossible());
+ mApi->SendTransaction(txn);
+}
+
+void WebRenderBridgeParent::UpdateDebugFlags() {
+ if (!IsRootWebRenderBridgeParent()) {
+ MOZ_ASSERT_UNREACHABLE("unexpected to be called");
+ return;
+ }
+
+ mApi->UpdateDebugFlags(gfxVars::WebRenderDebugFlags());
+}
+
+void WebRenderBridgeParent::UpdateProfilerUI() {
+ if (!IsRootWebRenderBridgeParent()) {
+ MOZ_ASSERT_UNREACHABLE("unexpected to be called");
+ return;
+ }
+
+ nsCString uiString = gfxVars::GetWebRenderProfilerUIOrDefault();
+ mApi->SetProfilerUI(uiString);
+}
+
+void WebRenderBridgeParent::UpdateParameters() {
+ if (!IsRootWebRenderBridgeParent()) {
+ MOZ_ASSERT_UNREACHABLE("unexpected to be called");
+ return;
+ }
+
+ uint32_t count = gfxVars::WebRenderBatchingLookback();
+ mApi->SetBatchingLookback(count);
+ mApi->SetInt(wr::IntParameter::BatchedUploadThreshold,
+ gfxVars::WebRenderBatchedUploadThreshold());
+
+ mBlobTileSize = gfxVars::WebRenderBlobTileSize();
+}
+
+void WebRenderBridgeParent::UpdateBoolParameters() {
+ if (!IsRootWebRenderBridgeParent()) {
+ MOZ_ASSERT_UNREACHABLE("unexpected to be called");
+ return;
+ }
+
+ uint32_t bits = gfxVars::WebRenderBoolParameters();
+ uint32_t changedBits = mBoolParameterBits ^ bits;
+
+ for (auto paramName : MakeEnumeratedRange(wr::BoolParameter::Sentinel)) {
+ uint32_t i = (uint32_t)paramName;
+ if (changedBits & (1 << i)) {
+ bool value = (bits & (1 << i)) != 0;
+ mApi->SetBool(paramName, value);
+ }
+ }
+ mBoolParameterBits = bits;
+}
+
+#if defined(MOZ_WIDGET_ANDROID)
+void WebRenderBridgeParent::RequestScreenPixels(
+ UiCompositorControllerParent* aController) {
+ mScreenPixelsTarget = aController;
+}
+
+void WebRenderBridgeParent::MaybeCaptureScreenPixels() {
+ if (!mScreenPixelsTarget) {
+ return;
+ }
+
+ if (mDestroyed) {
+ return;
+ }
+
+ if (auto* cbp = GetRootCompositorBridgeParent()) {
+ cbp->FlushPendingWrTransactionEventsWithWait();
+ }
+
+ // This function should only get called in the root WRBP.
+ MOZ_ASSERT(IsRootWebRenderBridgeParent());
+# ifdef DEBUG
+ CompositorBridgeParent* cbp = GetRootCompositorBridgeParent();
+ MOZ_ASSERT(cbp && !cbp->IsPaused());
+# endif
+
+ SurfaceFormat format = SurfaceFormat::R8G8B8A8; // On android we use RGBA8
+ auto client_size = mWidget->GetClientSize();
+ size_t buffer_size =
+ client_size.width * client_size.height * BytesPerPixel(format);
+
+ ipc::Shmem mem;
+ if (!mScreenPixelsTarget->AllocPixelBuffer(buffer_size, &mem)) {
+ // Failed to alloc shmem, Just bail out.
+ return;
+ }
+
+ IntSize size(client_size.width, client_size.height);
+
+ bool needsYFlip = false;
+ mApi->Readback(TimeStamp::Now(), size, format,
+ Range<uint8_t>(mem.get<uint8_t>(), buffer_size), &needsYFlip);
+
+ Unused << mScreenPixelsTarget->SendScreenPixels(
+ std::move(mem), ScreenIntSize(client_size.width, client_size.height),
+ needsYFlip);
+
+ mScreenPixelsTarget = nullptr;
+}
+#endif
+
+mozilla::ipc::IPCResult WebRenderBridgeParent::RecvGetSnapshot(
+ NotNull<PTextureParent*> aTexture, bool* aNeedsYFlip) {
+ *aNeedsYFlip = false;
+ CompositorBridgeParent* cbp = GetRootCompositorBridgeParent();
+ if (mDestroyed || !cbp || cbp->IsPaused()) {
+ return IPC_OK();
+ }
+
+ if (auto* cbp = GetRootCompositorBridgeParent()) {
+ cbp->FlushPendingWrTransactionEventsWithWait();
+ }
+
+ LOG("WebRenderBridgeParent::RecvGetSnapshot() PipelineId %" PRIx64
+ " Id %" PRIx64 " root %d",
+ wr::AsUint64(mPipelineId), wr::AsUint64(mApi->GetId()),
+ IsRootWebRenderBridgeParent());
+
+ // This function should only get called in the root WRBP. If this function
+ // gets called in a non-root WRBP, we will set mForceRendering in this WRBP
+ // but it will have no effect because CompositeToTarget (which reads the
+ // flag) only gets invoked in the root WRBP. So we assert that this is the
+ // root WRBP (i.e. has a non-null mWidget) to catch violations of this rule.
+ MOZ_ASSERT(IsRootWebRenderBridgeParent());
+
+ RefPtr<TextureHost> texture = TextureHost::AsTextureHost(aTexture);
+ if (!texture) {
+ // We kill the content process rather than have it continue with an invalid
+ // snapshot, that may be too harsh and we could decide to return some sort
+ // of error to the child process and let it deal with it...
+ return IPC_FAIL_NO_REASON(this);
+ }
+
+ // XXX Add other TextureHost supports.
+ // Only BufferTextureHost is supported now.
+ BufferTextureHost* bufferTexture = texture->AsBufferTextureHost();
+ if (!bufferTexture) {
+ // We kill the content process rather than have it continue with an invalid
+ // snapshot, that may be too harsh and we could decide to return some sort
+ // of error to the child process and let it deal with it...
+ return IPC_FAIL_NO_REASON(this);
+ }
+
+ TimeStamp start = TimeStamp::Now();
+ MOZ_ASSERT(bufferTexture->GetBufferDescriptor().type() ==
+ BufferDescriptor::TRGBDescriptor);
+ DebugOnly<uint32_t> stride = ImageDataSerializer::GetRGBStride(
+ bufferTexture->GetBufferDescriptor().get_RGBDescriptor());
+ uint8_t* buffer = bufferTexture->GetBuffer();
+ IntSize size = bufferTexture->GetSize();
+
+ MOZ_ASSERT(buffer);
+ // For now the only formats we get here are RGBA and BGRA, and code below is
+ // assuming a bpp of 4. If we allow other formats, the code needs adjusting
+ // accordingly.
+ MOZ_ASSERT(BytesPerPixel(bufferTexture->GetFormat()) == 4);
+ uint32_t buffer_size = size.width * size.height * 4;
+
+ // Assert the stride of the buffer is what webrender expects
+ MOZ_ASSERT((uint32_t)(size.width * 4) == stride);
+
+ FlushSceneBuilds();
+ FlushFrameGeneration(wr::RenderReasons::SNAPSHOT);
+ mApi->Readback(start, size, bufferTexture->GetFormat(),
+ Range<uint8_t>(buffer, buffer_size), aNeedsYFlip);
+
+ return IPC_OK();
+}
+
+void WebRenderBridgeParent::AddPipelineIdForCompositable(
+ const wr::PipelineId& aPipelineId, const CompositableHandle& aHandle,
+ const CompositableHandleOwner& aOwner, wr::TransactionBuilder& aTxn,
+ wr::TransactionBuilder& aTxnForImageBridge) {
+ if (mDestroyed) {
+ return;
+ }
+
+ MOZ_ASSERT(mAsyncCompositables.find(wr::AsUint64(aPipelineId)) ==
+ mAsyncCompositables.end());
+
+ RefPtr<CompositableHost> host;
+ switch (aOwner) {
+ case CompositableHandleOwner::WebRenderBridge:
+ host = FindCompositable(aHandle);
+ break;
+ case CompositableHandleOwner::ImageBridge: {
+ RefPtr<ImageBridgeParent> imageBridge =
+ ImageBridgeParent::GetInstance(OtherPid());
+ if (!imageBridge) {
+ return;
+ }
+ host = imageBridge->FindCompositable(aHandle);
+ break;
+ }
+ }
+
+ if (!host) {
+ return;
+ }
+
+ WebRenderImageHost* wrHost = host->AsWebRenderImageHost();
+ MOZ_ASSERT(wrHost);
+ if (!wrHost) {
+ gfxCriticalNote
+ << "Incompatible CompositableHost at WebRenderBridgeParent.";
+ }
+
+ if (!wrHost) {
+ return;
+ }
+
+ wrHost->SetWrBridge(aPipelineId, this);
+ mAsyncCompositables.emplace(wr::AsUint64(aPipelineId), wrHost);
+ mAsyncImageManager->AddAsyncImagePipeline(aPipelineId, wrHost);
+
+ // If this is being called from WebRenderBridgeParent::RecvSetDisplayList,
+ // then aTxn might contain a display list that references pipelines that
+ // we just added to the async image manager.
+ // If we send the display list alone then WR will not yet have the content for
+ // the pipelines and so it will emit errors; the SetEmptyDisplayList call
+ // below ensure that we provide its content to WR as part of the same
+ // transaction.
+ mAsyncImageManager->SetEmptyDisplayList(aPipelineId, aTxn,
+ aTxnForImageBridge);
+}
+
+void WebRenderBridgeParent::RemovePipelineIdForCompositable(
+ const wr::PipelineId& aPipelineId, wr::TransactionBuilder& aTxn) {
+ if (mDestroyed) {
+ return;
+ }
+
+ auto it = mAsyncCompositables.find(wr::AsUint64(aPipelineId));
+ if (it == mAsyncCompositables.end()) {
+ return;
+ }
+ RefPtr<WebRenderImageHost>& wrHost = it->second;
+
+ wrHost->ClearWrBridge(aPipelineId, this);
+ mAsyncImageManager->RemoveAsyncImagePipeline(aPipelineId, aTxn);
+ aTxn.RemovePipeline(aPipelineId);
+ mAsyncCompositables.erase(wr::AsUint64(aPipelineId));
+}
+
+void WebRenderBridgeParent::DeleteImage(const ImageKey& aKey,
+ wr::TransactionBuilder& aUpdates) {
+ if (mDestroyed) {
+ return;
+ }
+
+ auto it = mSharedSurfaceIds.find(wr::AsUint64(aKey));
+ if (it != mSharedSurfaceIds.end()) {
+ mAsyncImageManager->HoldExternalImage(mPipelineId, mWrEpoch, it->second);
+ mSharedSurfaceIds.erase(it);
+ }
+
+ aUpdates.DeleteImage(aKey);
+}
+
+void WebRenderBridgeParent::ReleaseTextureOfImage(const wr::ImageKey& aKey) {
+ if (mDestroyed) {
+ return;
+ }
+
+ uint64_t id = wr::AsUint64(aKey);
+ CompositableTextureHostRef texture;
+ WebRenderTextureHost* wrTexture = nullptr;
+
+ auto it = mTextureHosts.find(id);
+ if (it != mTextureHosts.end()) {
+ wrTexture = (*it).second->AsWebRenderTextureHost();
+ }
+ if (wrTexture) {
+ mAsyncImageManager->HoldExternalImage(mPipelineId, mWrEpoch, wrTexture);
+ }
+ mTextureHosts.erase(id);
+}
+
+mozilla::ipc::IPCResult WebRenderBridgeParent::RecvSetLayersObserverEpoch(
+ const LayersObserverEpoch& aChildEpoch) {
+ if (mDestroyed) {
+ return IPC_OK();
+ }
+ mChildLayersObserverEpoch = aChildEpoch;
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult WebRenderBridgeParent::RecvClearCachedResources() {
+ if (mDestroyed) {
+ return IPC_OK();
+ }
+
+ LOG("WebRenderBridgeParent::RecvClearCachedResources() PipelineId %" PRIx64
+ " Id %" PRIx64 " root %d",
+ wr::AsUint64(mPipelineId), wr::AsUint64(mApi->GetId()),
+ IsRootWebRenderBridgeParent());
+
+ // Clear resources
+ wr::TransactionBuilder txn(mApi);
+ txn.SetLowPriority(true);
+ txn.ClearDisplayList(GetNextWrEpoch(), mPipelineId);
+ txn.Notify(
+ wr::Checkpoint::SceneBuilt,
+ MakeUnique<ScheduleObserveLayersUpdate>(
+ mCompositorBridge, GetLayersId(), mChildLayersObserverEpoch, false));
+ mApi->SendTransaction(txn);
+
+ // Schedule generate frame to clean up Pipeline
+ ScheduleGenerateFrame(wr::RenderReasons::CLEAR_RESOURCES);
+
+ ClearAnimationResources();
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult WebRenderBridgeParent::RecvClearAnimationResources() {
+ if (!mDestroyed) {
+ ClearAnimationResources();
+ }
+
+ return IPC_OK();
+}
+
+wr::Epoch WebRenderBridgeParent::UpdateWebRender(
+ CompositorVsyncScheduler* aScheduler, RefPtr<wr::WebRenderAPI>&& aApi,
+ AsyncImagePipelineManager* aImageMgr,
+ const TextureFactoryIdentifier& aTextureFactoryIdentifier) {
+ MOZ_ASSERT(!IsRootWebRenderBridgeParent());
+ MOZ_ASSERT(aScheduler);
+ MOZ_ASSERT(aApi);
+ MOZ_ASSERT(aImageMgr);
+
+ if (mDestroyed) {
+ return mWrEpoch;
+ }
+
+ // Update id name space to identify obsoleted keys.
+ // Since usage of invalid keys could cause crash in webrender.
+ mIdNamespace = aApi->GetNamespace();
+ // XXX Remove it when webrender supports sharing/moving Keys between different
+ // webrender instances.
+ // XXX It requests client to update/reallocate webrender related resources,
+ // but parent side does not wait end of the update.
+ // The code could become simpler if we could serialise old keys deallocation
+ // and new keys allocation. But we do not do it, it is because client side
+ // deallocate old layers/webrender keys after new layers/webrender keys
+ // allocation. Without client side's layout refactoring, we could not finish
+ // all old layers/webrender keys removals before new layer/webrender keys
+ // allocation. In future, we could address the problem.
+ Unused << SendWrUpdated(mIdNamespace, aTextureFactoryIdentifier);
+ CompositorBridgeParentBase* cBridge = mCompositorBridge;
+ // XXX Stop to clear resources if webreder supports resources sharing between
+ // different webrender instances.
+ ClearResources();
+ mCompositorBridge = cBridge;
+ mCompositorScheduler = aScheduler;
+ mApi = aApi;
+ mAsyncImageManager = aImageMgr;
+
+ // Register pipeline to updated AsyncImageManager.
+ mAsyncImageManager->AddPipeline(mPipelineId, this);
+
+ LOG("WebRenderBridgeParent::UpdateWebRender() PipelineId %" PRIx64
+ " Id %" PRIx64 " root %d",
+ wr::AsUint64(mPipelineId), wr::AsUint64(mApi->GetId()),
+ IsRootWebRenderBridgeParent());
+
+ return GetNextWrEpoch(); // Update webrender epoch
+}
+
+mozilla::ipc::IPCResult WebRenderBridgeParent::RecvInvalidateRenderedFrame() {
+ // This function should only get called in the root WRBP
+ MOZ_ASSERT(IsRootWebRenderBridgeParent());
+ LOG("WebRenderBridgeParent::RecvInvalidateRenderedFrame() PipelineId %" PRIx64
+ " Id %" PRIx64 " root %d",
+ wr::AsUint64(mPipelineId), wr::AsUint64(mApi->GetId()),
+ IsRootWebRenderBridgeParent());
+
+ InvalidateRenderedFrame(wr::RenderReasons::WIDGET);
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult WebRenderBridgeParent::RecvScheduleComposite(
+ const wr::RenderReasons& aReasons) {
+ LOG("WebRenderBridgeParent::RecvScheduleComposite() PipelineId %" PRIx64
+ " Id %" PRIx64 " root %d",
+ wr::AsUint64(mPipelineId), wr::AsUint64(mApi->GetId()),
+ IsRootWebRenderBridgeParent());
+
+ // Caller of LayerManager::ScheduleComposite() expects that it trigger
+ // composite. Then we do not want to skip generate frame.
+ ScheduleForcedGenerateFrame(aReasons);
+ return IPC_OK();
+}
+
+void WebRenderBridgeParent::InvalidateRenderedFrame(
+ wr::RenderReasons aReasons) {
+ if (mDestroyed) {
+ return;
+ }
+
+ wr::TransactionBuilder fastTxn(mApi, /* aUseSceneBuilderThread */ false);
+ fastTxn.InvalidateRenderedFrame(aReasons);
+ mApi->SendTransaction(fastTxn);
+}
+
+void WebRenderBridgeParent::ScheduleForcedGenerateFrame(
+ wr::RenderReasons aReasons) {
+ if (mDestroyed) {
+ return;
+ }
+
+ InvalidateRenderedFrame(aReasons);
+ ScheduleGenerateFrame(aReasons);
+}
+
+mozilla::ipc::IPCResult WebRenderBridgeParent::RecvCapture() {
+ if (!mDestroyed) {
+ mApi->Capture();
+ }
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult WebRenderBridgeParent::RecvStartCaptureSequence(
+ const nsACString& aPath, const uint32_t& aFlags) {
+ if (!mDestroyed) {
+ mApi->StartCaptureSequence(aPath, aFlags);
+ }
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult WebRenderBridgeParent::RecvStopCaptureSequence() {
+ if (!mDestroyed) {
+ mApi->StopCaptureSequence();
+ }
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult WebRenderBridgeParent::RecvSyncWithCompositor() {
+ LOG("WebRenderBridgeParent::RecvSyncWithCompositor() PipelineId %" PRIx64
+ " Id %" PRIx64 " root %d",
+ wr::AsUint64(mPipelineId), wr::AsUint64(mApi->GetId()),
+ IsRootWebRenderBridgeParent());
+
+ if (mDestroyed) {
+ return IPC_OK();
+ }
+
+ FlushSceneBuilds();
+ if (RefPtr<WebRenderBridgeParent> root = GetRootWebRenderBridgeParent()) {
+ root->FlushFrameGeneration(wr::RenderReasons::CONTENT_SYNC);
+ }
+ FlushFramePresentation();
+ // Finally, we force the AsyncImagePipelineManager to handle all the
+ // pipeline updates produced in the last step, so that it frees any
+ // unneeded textures. Then we can return from this sync IPC call knowing
+ // that we've done everything we can to flush stuff on the compositor.
+ mAsyncImageManager->ProcessPipelineUpdates();
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult WebRenderBridgeParent::RecvSetConfirmedTargetAPZC(
+ const uint64_t& aBlockId, nsTArray<ScrollableLayerGuid>&& aTargets) {
+ for (size_t i = 0; i < aTargets.Length(); i++) {
+ // Guard against bad data from hijacked child processes
+ if (aTargets[i].mLayersId != GetLayersId()) {
+ NS_ERROR(
+ "Unexpected layers id in RecvSetConfirmedTargetAPZC; dropping "
+ "message...");
+ return IPC_FAIL(this, "Bad layers id");
+ }
+ }
+
+ if (mDestroyed) {
+ return IPC_OK();
+ }
+ mCompositorBridge->SetConfirmedTargetAPZC(GetLayersId(), aBlockId,
+ std::move(aTargets));
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult WebRenderBridgeParent::RecvSetTestSampleTime(
+ const TimeStamp& aTime) {
+ if (mDestroyed) {
+ return IPC_FAIL_NO_REASON(this);
+ }
+
+ if (!mCompositorBridge->SetTestSampleTime(GetLayersId(), aTime)) {
+ return IPC_FAIL_NO_REASON(this);
+ }
+ if (RefPtr<OMTASampler> sampler = GetOMTASampler()) {
+ sampler->EnterTestMode();
+ }
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult WebRenderBridgeParent::RecvLeaveTestMode() {
+ if (mDestroyed) {
+ return IPC_FAIL_NO_REASON(this);
+ }
+
+ mCompositorBridge->LeaveTestMode(GetLayersId());
+ if (RefPtr<OMTASampler> sampler = GetOMTASampler()) {
+ sampler->LeaveTestMode();
+ }
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult WebRenderBridgeParent::RecvGetAnimationValue(
+ const uint64_t& aCompositorAnimationsId, OMTAValue* aValue) {
+ if (mDestroyed) {
+ return IPC_FAIL_NO_REASON(this);
+ }
+
+ if (RefPtr<OMTASampler> sampler = GetOMTASampler()) {
+ Maybe<TimeStamp> testingTimeStamp;
+ if (CompositorBridgeParent* cbp = GetRootCompositorBridgeParent()) {
+ testingTimeStamp = cbp->GetTestingTimeStamp();
+ }
+
+ sampler->SampleForTesting(testingTimeStamp);
+ *aValue = sampler->GetOMTAValue(aCompositorAnimationsId);
+ }
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult WebRenderBridgeParent::RecvSetAsyncScrollOffset(
+ const ScrollableLayerGuid::ViewID& aScrollId, const float& aX,
+ const float& aY) {
+ if (mDestroyed) {
+ return IPC_OK();
+ }
+ mCompositorBridge->SetTestAsyncScrollOffset(GetLayersId(), aScrollId,
+ CSSPoint(aX, aY));
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult WebRenderBridgeParent::RecvSetAsyncZoom(
+ const ScrollableLayerGuid::ViewID& aScrollId, const float& aZoom) {
+ if (mDestroyed) {
+ return IPC_OK();
+ }
+ mCompositorBridge->SetTestAsyncZoom(GetLayersId(), aScrollId,
+ LayerToParentLayerScale(aZoom));
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult WebRenderBridgeParent::RecvFlushApzRepaints() {
+ if (mDestroyed) {
+ return IPC_OK();
+ }
+ mCompositorBridge->FlushApzRepaints(GetLayersId());
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult WebRenderBridgeParent::RecvGetAPZTestData(
+ APZTestData* aOutData) {
+ if (mDestroyed) {
+ return IPC_FAIL_NO_REASON(this);
+ }
+
+ mCompositorBridge->GetAPZTestData(GetLayersId(), aOutData);
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult WebRenderBridgeParent::RecvGetFrameUniformity(
+ FrameUniformityData* aOutData) {
+ if (mDestroyed) {
+ return IPC_FAIL_NO_REASON(this);
+ }
+
+ mCompositorBridge->GetFrameUniformity(GetLayersId(), aOutData);
+ return IPC_OK();
+}
+
+void WebRenderBridgeParent::ActorDestroy(ActorDestroyReason aWhy) { Destroy(); }
+
+void WebRenderBridgeParent::ResetPreviousSampleTime() {
+ if (RefPtr<OMTASampler> sampler = GetOMTASampler()) {
+ sampler->ResetPreviousSampleTime();
+ }
+}
+
+RefPtr<OMTASampler> WebRenderBridgeParent::GetOMTASampler() const {
+ CompositorBridgeParent* cbp = GetRootCompositorBridgeParent();
+ if (!cbp) {
+ return nullptr;
+ }
+ return cbp->GetOMTASampler();
+}
+
+void WebRenderBridgeParent::SetOMTASampleTime() {
+ MOZ_ASSERT(IsRootWebRenderBridgeParent());
+ if (RefPtr<OMTASampler> sampler = GetOMTASampler()) {
+ sampler->SetSampleTime(mCompositorScheduler->GetLastComposeTime().Time());
+ }
+}
+
+void WebRenderBridgeParent::RetrySkippedComposite() {
+ if (!mSkippedComposite) {
+ return;
+ }
+
+ mSkippedComposite = false;
+ if (mCompositorScheduler) {
+ mCompositorScheduler->ScheduleComposition(mSkippedCompositeReasons |
+ RenderReasons::SKIPPED_COMPOSITE);
+ }
+ mSkippedCompositeReasons = wr::RenderReasons::NONE;
+}
+
+void WebRenderBridgeParent::CompositeToTarget(VsyncId aId,
+ wr::RenderReasons aReasons,
+ gfx::DrawTarget* aTarget,
+ const gfx::IntRect* aRect) {
+ // This function should only get called in the root WRBP
+ MOZ_ASSERT(IsRootWebRenderBridgeParent());
+
+ // The two arguments are part of the CompositorVsyncSchedulerOwner API but in
+ // this implementation they should never be non-null.
+ MOZ_ASSERT(aTarget == nullptr);
+ MOZ_ASSERT(aRect == nullptr);
+
+ LOG("WebRenderBridgeParent::CompositeToTarget() PipelineId %" PRIx64
+ " Id %" PRIx64 " root %d",
+ wr::AsUint64(mPipelineId), wr::AsUint64(mApi->GetId()),
+ IsRootWebRenderBridgeParent());
+
+ CompositorBridgeParent* cbp = GetRootCompositorBridgeParent();
+ uint64_t innerWindowId = cbp ? cbp->GetInnerWindowId() : 0;
+ AUTO_PROFILER_TRACING_MARKER_INNERWINDOWID("Paint", "CompositeToTarget",
+ GRAPHICS, innerWindowId);
+
+ bool paused = true;
+ if (cbp) {
+ paused = cbp->IsPaused();
+ }
+
+ if (paused || !mReceivedDisplayList) {
+ ResetPreviousSampleTime();
+ mCompositionOpportunityId = mCompositionOpportunityId.Next();
+ PROFILER_MARKER_TEXT("Discarded composite", GRAPHICS,
+ MarkerInnerWindowId(innerWindowId),
+ paused ? "Paused"_ns : "No display list"_ns);
+ return;
+ }
+
+ mSkippedComposite =
+ wr::RenderThread::Get()->TooManyPendingFrames(mApi->GetId());
+
+ if (mSkippedComposite) {
+ // Render thread is busy, try next time.
+ mSkippedComposite = true;
+ mSkippedCompositeReasons = mSkippedCompositeReasons | aReasons;
+ ResetPreviousSampleTime();
+
+ // Record that we skipped presenting a frame for
+ // all pending transactions that have finished scene building.
+ for (auto& id : mPendingTransactionIds) {
+ if (id.mSceneBuiltTime) {
+ id.mSkippedComposites++;
+ }
+ }
+
+ PROFILER_MARKER_TEXT("SkippedComposite", GRAPHICS,
+ MarkerInnerWindowId(innerWindowId),
+ "Too many pending frames");
+
+ Telemetry::ScalarAdd(Telemetry::ScalarID::GFX_SKIPPED_COMPOSITES, 1);
+
+ return;
+ }
+
+ mCompositionOpportunityId = mCompositionOpportunityId.Next();
+ MaybeGenerateFrame(aId, /* aForceGenerateFrame */ false, aReasons);
+}
+
+TimeDuration WebRenderBridgeParent::GetVsyncInterval() const {
+ // This function should only get called in the root WRBP
+ MOZ_ASSERT(IsRootWebRenderBridgeParent());
+ if (CompositorBridgeParent* cbp = GetRootCompositorBridgeParent()) {
+ return cbp->GetVsyncInterval();
+ }
+ return TimeDuration();
+}
+
+void WebRenderBridgeParent::MaybeGenerateFrame(VsyncId aId,
+ bool aForceGenerateFrame,
+ wr::RenderReasons aReasons) {
+ // This function should only get called in the root WRBP
+ MOZ_ASSERT(IsRootWebRenderBridgeParent());
+ LOG("WebRenderBridgeParent::MaybeGenerateFrame() PipelineId %" PRIx64
+ " Id %" PRIx64 " root %d",
+ wr::AsUint64(mPipelineId), wr::AsUint64(mApi->GetId()),
+ IsRootWebRenderBridgeParent());
+
+ if (CompositorBridgeParent* cbp = GetRootCompositorBridgeParent()) {
+ // Skip WR render during paused state.
+ if (cbp->IsPaused()) {
+ TimeStamp now = TimeStamp::Now();
+ PROFILER_MARKER_TEXT(
+ "SkippedComposite", GRAPHICS,
+ MarkerOptions(MarkerInnerWindowId(cbp->GetInnerWindowId()),
+ MarkerTiming::InstantAt(now)),
+ "CompositorBridgeParent is paused");
+ cbp->NotifyPipelineRendered(mPipelineId, mWrEpoch, VsyncId(), now, now,
+ now);
+ return;
+ }
+ }
+
+ TimeStamp start = TimeStamp::Now();
+
+ // Ensure GenerateFrame is handled on the render backend thread rather
+ // than going through the scene builder thread. That way we continue
+ // generating frames with the old scene even during slow scene builds.
+ wr::TransactionBuilder fastTxn(mApi, false /* useSceneBuilderThread */);
+ // Handle transaction that is related to DisplayList.
+ wr::TransactionBuilder sceneBuilderTxn(mApi);
+ wr::AutoTransactionSender sender(mApi, &sceneBuilderTxn);
+
+ mAsyncImageManager->SetCompositionInfo(start, mCompositionOpportunityId);
+ mAsyncImageManager->ApplyAsyncImagesOfImageBridge(sceneBuilderTxn, fastTxn);
+ mAsyncImageManager->SetCompositionInfo(TimeStamp(),
+ CompositionOpportunityId{});
+
+ if (!mAsyncImageManager->GetCompositeUntilTime().IsNull()) {
+ // Trigger another CompositeToTarget() call because there might be another
+ // frame that we want to generate after this one.
+ // It will check if we actually want to generate the frame or not.
+ mCompositorScheduler->ScheduleComposition(
+ wr::RenderReasons::ASYNC_IMAGE_COMPOSITE_UNTIL);
+ }
+
+ bool generateFrame = !fastTxn.IsEmpty() || aForceGenerateFrame;
+
+ if (mAsyncImageManager->GetAndResetWillGenerateFrame()) {
+ aReasons |= wr::RenderReasons::ASYNC_IMAGE;
+ generateFrame = true;
+ }
+
+ if (!generateFrame) {
+ // Could skip generating frame now.
+ PROFILER_MARKER_TEXT("SkippedComposite", GRAPHICS,
+ MarkerTiming::InstantAt(start),
+ "No reason to generate frame");
+ ResetPreviousSampleTime();
+ return;
+ }
+
+ if (RefPtr<OMTASampler> sampler = GetOMTASampler()) {
+ if (sampler->HasAnimations()) {
+ ScheduleGenerateFrame(wr::RenderReasons::ANIMATED_PROPERTY);
+ }
+ }
+
+ SetOMTASampleTime();
+ SetAPZSampleTime();
+
+#if defined(ENABLE_FRAME_LATENCY_LOG)
+ auto startTime = TimeStamp::Now();
+ mApi->SetFrameStartTime(startTime);
+#endif
+
+ fastTxn.GenerateFrame(aId, aReasons);
+ wr::RenderThread::Get()->IncPendingFrameCount(mApi->GetId(), aId, start);
+
+ mApi->SendTransaction(fastTxn);
+
+#if defined(MOZ_WIDGET_ANDROID)
+ MaybeCaptureScreenPixels();
+#endif
+
+ mMostRecentComposite = TimeStamp::Now();
+
+ // During disabling native compositor, webrender needs to render twice.
+ // Otherwise, browser flashes black.
+ // XXX better fix?
+ if (mDisablingNativeCompositor) {
+ mDisablingNativeCompositor = false;
+
+ // Ensure we generate and render a frame immediately.
+ ScheduleForcedGenerateFrame(aReasons);
+ }
+}
+
+void WebRenderBridgeParent::HoldPendingTransactionId(
+ const wr::Epoch& aWrEpoch, TransactionId aTransactionId,
+ bool aContainsSVGGroup, const VsyncId& aVsyncId,
+ const TimeStamp& aVsyncStartTime, const TimeStamp& aRefreshStartTime,
+ const TimeStamp& aTxnStartTime, const nsACString& aTxnURL,
+ const TimeStamp& aFwdTime, const bool aIsFirstPaint,
+ nsTArray<CompositionPayload>&& aPayloads, const bool aUseForTelemetry) {
+ MOZ_ASSERT(aTransactionId > LastPendingTransactionId());
+ mPendingTransactionIds.push_back(PendingTransactionId(
+ aWrEpoch, aTransactionId, aContainsSVGGroup, aVsyncId, aVsyncStartTime,
+ aRefreshStartTime, aTxnStartTime, aTxnURL, aFwdTime, aIsFirstPaint,
+ aUseForTelemetry, std::move(aPayloads)));
+}
+
+TransactionId WebRenderBridgeParent::LastPendingTransactionId() {
+ TransactionId id{0};
+ if (!mPendingTransactionIds.empty()) {
+ id = mPendingTransactionIds.back().mId;
+ }
+ return id;
+}
+
+void WebRenderBridgeParent::NotifySceneBuiltForEpoch(
+ const wr::Epoch& aEpoch, const TimeStamp& aEndTime) {
+ for (auto& id : mPendingTransactionIds) {
+ if (id.mEpoch.mHandle == aEpoch.mHandle) {
+ id.mSceneBuiltTime = aEndTime;
+ break;
+ }
+ }
+}
+
+void WebRenderBridgeParent::NotifyDidSceneBuild(
+ RefPtr<const wr::WebRenderPipelineInfo> aInfo) {
+ MOZ_ASSERT(IsRootWebRenderBridgeParent());
+ if (!mCompositorScheduler) {
+ return;
+ }
+
+ mAsyncImageManager->SetWillGenerateFrame();
+
+ // If the scheduler has a composite more recent than our last composite (which
+ // we missed), and we're within the threshold ms of the last vsync, then
+ // kick of a late composite.
+ TimeStamp lastVsync = mCompositorScheduler->GetLastVsyncTime();
+ VsyncId lastVsyncId = mCompositorScheduler->GetLastVsyncId();
+ if (lastVsyncId == VsyncId() || !mMostRecentComposite ||
+ mMostRecentComposite >= lastVsync ||
+ ((TimeStamp::Now() - lastVsync).ToMilliseconds() >
+ StaticPrefs::gfx_webrender_late_scenebuild_threshold())) {
+ mCompositorScheduler->ScheduleComposition(wr::RenderReasons::SCENE);
+ return;
+ }
+
+ // Look through all the pipelines contained within the built scene
+ // and check which vsync they initiated from.
+ const auto& info = aInfo->Raw();
+ for (const auto& epoch : info.epochs) {
+ WebRenderBridgeParent* wrBridge = this;
+ if (!(epoch.pipeline_id == PipelineId())) {
+ wrBridge = mAsyncImageManager->GetWrBridge(epoch.pipeline_id);
+ }
+
+ if (wrBridge) {
+ VsyncId startId = wrBridge->GetVsyncIdForEpoch(epoch.epoch);
+ // If any of the pipelines started building on the current vsync (i.e
+ // we did all of display list building and scene building within the
+ // threshold), then don't do an early composite.
+ if (startId == lastVsyncId) {
+ mCompositorScheduler->ScheduleComposition(wr::RenderReasons::SCENE);
+ return;
+ }
+ }
+ }
+
+ CompositeToTarget(mCompositorScheduler->GetLastVsyncId(),
+ wr::RenderReasons::SCENE, nullptr, nullptr);
+}
+
+static Telemetry::HistogramID GetHistogramId(const bool aIsLargePaint,
+ const bool aIsFullDisplayList) {
+ const Telemetry::HistogramID histogramIds[] = {
+ Telemetry::CONTENT_SMALL_PAINT_PHASE_WEIGHT_PARTIAL,
+ Telemetry::CONTENT_LARGE_PAINT_PHASE_WEIGHT_PARTIAL,
+ Telemetry::CONTENT_SMALL_PAINT_PHASE_WEIGHT_FULL,
+ Telemetry::CONTENT_LARGE_PAINT_PHASE_WEIGHT_FULL,
+ };
+
+ return histogramIds[(aIsFullDisplayList * 2) + aIsLargePaint];
+}
+
+static void RecordPaintPhaseTelemetry(wr::RendererStats* aStats) {
+ if (!aStats || !aStats->full_paint) {
+ return;
+ }
+
+ const double geckoDL = aStats->gecko_display_list_time;
+ const double wrDL = aStats->wr_display_list_time;
+ const double sceneBuild = aStats->scene_build_time;
+ const double frameBuild = aStats->frame_build_time;
+ const double totalMs = geckoDL + wrDL + sceneBuild + frameBuild;
+
+ // If the total time was >= 16ms, then it's likely we missed a frame due to
+ // painting. We bucket these metrics separately.
+ const bool isLargePaint = totalMs >= 16.0;
+
+ // Split the results based on display list build type, partial or full.
+ const bool isFullDisplayList = aStats->full_display_list;
+
+ auto AsPercentage = [&](const double aTimeMs) -> double {
+ MOZ_ASSERT(aTimeMs <= totalMs);
+ return (aTimeMs / totalMs) * 100.0;
+ };
+
+ auto RecordKey = [&](const nsCString& aKey, const double aTimeMs) -> void {
+ const auto val = static_cast<uint32_t>(AsPercentage(aTimeMs));
+ const auto histogramId = GetHistogramId(isLargePaint, isFullDisplayList);
+ Telemetry::Accumulate(histogramId, aKey, val);
+ };
+
+ RecordKey("dl"_ns, geckoDL);
+ RecordKey("wrdl"_ns, wrDL);
+ RecordKey("sb"_ns, sceneBuild);
+ RecordKey("fb"_ns, frameBuild);
+}
+
+void WebRenderBridgeParent::FlushTransactionIdsForEpoch(
+ const wr::Epoch& aEpoch, const VsyncId& aCompositeStartId,
+ const TimeStamp& aCompositeStartTime, const TimeStamp& aRenderStartTime,
+ const TimeStamp& aEndTime, UiCompositorControllerParent* aUiController,
+ wr::RendererStats* aStats, nsTArray<FrameStats>& aOutputStats,
+ nsTArray<TransactionId>& aOutputTransactions) {
+ while (!mPendingTransactionIds.empty()) {
+ const auto& transactionId = mPendingTransactionIds.front();
+
+ if (aEpoch.mHandle < transactionId.mEpoch.mHandle) {
+ break;
+ }
+
+ if (!IsRootWebRenderBridgeParent() && !mVsyncRate.IsZero() &&
+ transactionId.mUseForTelemetry) {
+ auto fullPaintTime =
+ transactionId.mSceneBuiltTime
+ ? transactionId.mSceneBuiltTime - transactionId.mTxnStartTime
+ : TimeDuration::FromMilliseconds(0);
+
+ int32_t contentFrameTime = RecordContentFrameTime(
+ transactionId.mVsyncId, transactionId.mVsyncStartTime,
+ transactionId.mTxnStartTime, aCompositeStartId, aEndTime,
+ fullPaintTime, mVsyncRate, transactionId.mContainsSVGGroup, true,
+ aStats);
+
+ RecordPaintPhaseTelemetry(aStats);
+
+ if (StaticPrefs::gfx_logging_slow_frames_enabled_AtStartup() &&
+ contentFrameTime > 200) {
+ aOutputStats.AppendElement(FrameStats(
+ transactionId.mId, aCompositeStartTime, aRenderStartTime, aEndTime,
+ contentFrameTime,
+ aStats ? (double(aStats->resource_upload_time) / 1000000.0) : 0.0,
+ aStats ? (double(aStats->gpu_cache_upload_time) / 1000000.0) : 0.0,
+ transactionId.mTxnStartTime, transactionId.mRefreshStartTime,
+ transactionId.mFwdTime, transactionId.mSceneBuiltTime,
+ transactionId.mSkippedComposites, transactionId.mTxnURL));
+ }
+ }
+
+#if defined(ENABLE_FRAME_LATENCY_LOG)
+ if (transactionId.mRefreshStartTime) {
+ int32_t latencyMs =
+ lround((aEndTime - transactionId.mRefreshStartTime).ToMilliseconds());
+ printf_stderr(
+ "From transaction start to end of generate frame latencyMs %d this "
+ "%p\n",
+ latencyMs, this);
+ }
+ if (transactionId.mFwdTime) {
+ int32_t latencyMs =
+ lround((aEndTime - transactionId.mFwdTime).ToMilliseconds());
+ printf_stderr(
+ "From forwarding transaction to end of generate frame latencyMs %d "
+ "this %p\n",
+ latencyMs, this);
+ }
+#endif
+
+ if (aUiController && transactionId.mIsFirstPaint) {
+ aUiController->NotifyFirstPaint();
+ }
+
+ RecordCompositionPayloadsPresented(aEndTime, transactionId.mPayloads);
+
+ aOutputTransactions.AppendElement(transactionId.mId);
+ mPendingTransactionIds.pop_front();
+ }
+}
+
+LayersId WebRenderBridgeParent::GetLayersId() const {
+ return wr::AsLayersId(mPipelineId);
+}
+
+void WebRenderBridgeParent::ScheduleGenerateFrame(wr::RenderReasons aReasons) {
+ if (mCompositorScheduler) {
+ mAsyncImageManager->SetWillGenerateFrame();
+ mCompositorScheduler->ScheduleComposition(aReasons);
+ }
+}
+
+void WebRenderBridgeParent::FlushRendering(wr::RenderReasons aReasons,
+ bool aWaitForPresent) {
+ if (mDestroyed) {
+ return;
+ }
+
+ // This gets called during e.g. window resizes, so we need to flush the
+ // scene (which has the display list at the new window size).
+ FlushSceneBuilds();
+ FlushFrameGeneration(aReasons);
+ if (aWaitForPresent) {
+ FlushFramePresentation();
+ }
+}
+
+ipc::IPCResult WebRenderBridgeParent::RecvSetDefaultClearColor(
+ const uint32_t& aColor) {
+ SetClearColor(gfx::DeviceColor::FromABGR(aColor));
+ return IPC_OK();
+}
+
+void WebRenderBridgeParent::SetClearColor(const gfx::DeviceColor& aColor) {
+ MOZ_ASSERT(IsRootWebRenderBridgeParent());
+
+ if (!IsRootWebRenderBridgeParent() || mDestroyed) {
+ return;
+ }
+
+ mApi->SetClearColor(aColor);
+}
+
+void WebRenderBridgeParent::Pause() {
+ MOZ_ASSERT(IsRootWebRenderBridgeParent());
+ LOG("WebRenderBridgeParent::Pause() PipelineId %" PRIx64 " Id %" PRIx64
+ " root %d",
+ wr::AsUint64(mPipelineId), wr::AsUint64(mApi->GetId()),
+ IsRootWebRenderBridgeParent());
+
+ if (!IsRootWebRenderBridgeParent() || mDestroyed) {
+ return;
+ }
+
+ mApi->Pause();
+}
+
+bool WebRenderBridgeParent::Resume() {
+ MOZ_ASSERT(IsRootWebRenderBridgeParent());
+ LOG("WebRenderBridgeParent::Resume() PipelineId %" PRIx64 " Id %" PRIx64
+ " root %d",
+ wr::AsUint64(mPipelineId), wr::AsUint64(mApi->GetId()),
+ IsRootWebRenderBridgeParent());
+
+ if (!IsRootWebRenderBridgeParent() || mDestroyed) {
+ return false;
+ }
+
+ if (!mApi->Resume()) {
+ return false;
+ }
+
+ // Ensure we generate and render a frame immediately.
+ ScheduleForcedGenerateFrame(wr::RenderReasons::WIDGET);
+ return true;
+}
+
+void WebRenderBridgeParent::ClearResources() {
+ if (!mApi) {
+ return;
+ }
+
+ if (!IsRootWebRenderBridgeParent()) {
+ mApi->FlushPendingWrTransactionEventsWithoutWait();
+ }
+
+ LOG("WebRenderBridgeParent::ClearResources() PipelineId %" PRIx64
+ " Id %" PRIx64 " root %d",
+ wr::AsUint64(mPipelineId), wr::AsUint64(mApi->GetId()),
+ IsRootWebRenderBridgeParent());
+
+ wr::Epoch wrEpoch = GetNextWrEpoch();
+ mReceivedDisplayList = false;
+ // Schedule generate frame to clean up Pipeline
+ ScheduleGenerateFrame(wr::RenderReasons::CLEAR_RESOURCES);
+
+ // WrFontKeys and WrImageKeys are deleted during WebRenderAPI destruction.
+ for (const auto& entry : mTextureHosts) {
+ WebRenderTextureHost* wrTexture = entry.second->AsWebRenderTextureHost();
+ MOZ_ASSERT(wrTexture);
+ if (wrTexture) {
+ mAsyncImageManager->HoldExternalImage(mPipelineId, wrEpoch, wrTexture);
+ }
+ }
+ mTextureHosts.clear();
+
+ for (const auto& entry : mSharedSurfaceIds) {
+ mAsyncImageManager->HoldExternalImage(mPipelineId, mWrEpoch, entry.second);
+ }
+ mSharedSurfaceIds.clear();
+
+ mAsyncImageManager->RemovePipeline(mPipelineId, wrEpoch);
+
+ wr::TransactionBuilder txn(mApi);
+ txn.SetLowPriority(true);
+ txn.ClearDisplayList(wrEpoch, mPipelineId);
+
+ for (const auto& entry : mAsyncCompositables) {
+ wr::PipelineId pipelineId = wr::AsPipelineId(entry.first);
+ RefPtr<WebRenderImageHost> host = entry.second;
+ host->ClearWrBridge(pipelineId, this);
+ mAsyncImageManager->RemoveAsyncImagePipeline(pipelineId, txn);
+ txn.RemovePipeline(pipelineId);
+ }
+ mAsyncCompositables.clear();
+ txn.RemovePipeline(mPipelineId);
+ mApi->SendTransaction(txn);
+
+ ClearAnimationResources();
+
+ if (IsRootWebRenderBridgeParent()) {
+ mCompositorScheduler->Destroy();
+ mApi->DestroyRenderer();
+ }
+
+ mCompositorScheduler = nullptr;
+ mAsyncImageManager = nullptr;
+ mApi = nullptr;
+ mCompositorBridge = nullptr;
+}
+
+void WebRenderBridgeParent::ClearAnimationResources() {
+ if (RefPtr<OMTASampler> sampler = GetOMTASampler()) {
+ sampler->ClearActiveAnimations(mActiveAnimations);
+ }
+ mActiveAnimations.clear();
+ std::queue<CompositorAnimationIdsForEpoch>().swap(
+ mCompositorAnimationsToDelete); // clear queue
+}
+
+bool WebRenderBridgeParent::ShouldParentObserveEpoch() {
+ if (mParentLayersObserverEpoch == mChildLayersObserverEpoch) {
+ return false;
+ }
+
+ mParentLayersObserverEpoch = mChildLayersObserverEpoch;
+ return true;
+}
+
+void WebRenderBridgeParent::SendAsyncMessage(
+ const nsTArray<AsyncParentMessageData>& aMessage) {
+ MOZ_ASSERT_UNREACHABLE("unexpected to be called");
+}
+
+void WebRenderBridgeParent::SendPendingAsyncMessages() {
+ MOZ_ASSERT(mCompositorBridge);
+ mCompositorBridge->SendPendingAsyncMessages();
+}
+
+void WebRenderBridgeParent::SetAboutToSendAsyncMessages() {
+ MOZ_ASSERT(mCompositorBridge);
+ mCompositorBridge->SetAboutToSendAsyncMessages();
+}
+
+void WebRenderBridgeParent::NotifyNotUsed(PTextureParent* aTexture,
+ uint64_t aTransactionId) {
+ MOZ_ASSERT_UNREACHABLE("unexpected to be called");
+}
+
+base::ProcessId WebRenderBridgeParent::GetChildProcessId() {
+ return OtherPid();
+}
+
+dom::ContentParentId WebRenderBridgeParent::GetContentId() {
+ MOZ_ASSERT(mCompositorBridge);
+ return mCompositorBridge->GetContentId();
+}
+
+bool WebRenderBridgeParent::IsSameProcess() const {
+ return OtherPid() == base::GetCurrentProcId();
+}
+
+mozilla::ipc::IPCResult WebRenderBridgeParent::RecvNewCompositable(
+ const CompositableHandle& aHandle, const TextureInfo& aInfo) {
+ if (mDestroyed) {
+ return IPC_OK();
+ }
+ if (!AddCompositable(aHandle, aInfo)) {
+ return IPC_FAIL_NO_REASON(this);
+ }
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult WebRenderBridgeParent::RecvReleaseCompositable(
+ const CompositableHandle& aHandle) {
+ if (mDestroyed) {
+ return IPC_OK();
+ }
+ ReleaseCompositable(aHandle);
+ return IPC_OK();
+}
+
+TextureFactoryIdentifier WebRenderBridgeParent::GetTextureFactoryIdentifier() {
+ MOZ_ASSERT(mApi);
+
+#ifdef XP_WIN
+ const bool supportsD3D11NV12 = gfx::DeviceManagerDx::Get()->CanUseNV12();
+#else
+ const bool supportsD3D11NV12 = false;
+#endif
+
+ TextureFactoryIdentifier ident(
+ mApi->GetBackendType(), mApi->GetCompositorType(), XRE_GetProcessType(),
+ mApi->GetMaxTextureSize(), mApi->GetUseANGLE(), mApi->GetUseDComp(),
+ mAsyncImageManager->UseCompositorWnd(), false, false, false,
+ supportsD3D11NV12, mApi->GetSyncHandle());
+ return ident;
+}
+
+wr::Epoch WebRenderBridgeParent::GetNextWrEpoch() {
+ MOZ_RELEASE_ASSERT(mWrEpoch.mHandle != UINT32_MAX);
+ mWrEpoch.mHandle++;
+ return mWrEpoch;
+}
+
+void WebRenderBridgeParent::RollbackWrEpoch() {
+ MOZ_RELEASE_ASSERT(mWrEpoch.mHandle != 0);
+ mWrEpoch.mHandle--;
+}
+
+void WebRenderBridgeParent::ExtractImageCompositeNotifications(
+ nsTArray<ImageCompositeNotificationInfo>* aNotifications) {
+ MOZ_ASSERT(IsRootWebRenderBridgeParent());
+ if (mDestroyed) {
+ return;
+ }
+ mAsyncImageManager->FlushImageNotifications(aNotifications);
+}
+
+void WebRenderBridgeParent::FlushPendingWrTransactionEventsWithWait() {
+ if (mDestroyed || IsRootWebRenderBridgeParent()) {
+ return;
+ }
+ mApi->FlushPendingWrTransactionEventsWithWait();
+}
+
+RefPtr<WebRenderBridgeParentRef>
+WebRenderBridgeParent::GetWebRenderBridgeParentRef() {
+ if (mDestroyed) {
+ return nullptr;
+ }
+
+ if (!mWebRenderBridgeRef) {
+ mWebRenderBridgeRef = new WebRenderBridgeParentRef(this);
+ }
+ return mWebRenderBridgeRef;
+}
+
+WebRenderBridgeParentRef::WebRenderBridgeParentRef(
+ WebRenderBridgeParent* aWebRenderBridge)
+ : mWebRenderBridge(aWebRenderBridge) {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+ MOZ_ASSERT(mWebRenderBridge);
+}
+
+RefPtr<WebRenderBridgeParent> WebRenderBridgeParentRef::WrBridge() {
+ return mWebRenderBridge;
+}
+
+void WebRenderBridgeParentRef::Clear() {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+ mWebRenderBridge = nullptr;
+}
+
+WebRenderBridgeParentRef::~WebRenderBridgeParentRef() {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+ MOZ_ASSERT(!mWebRenderBridge);
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/wr/WebRenderBridgeParent.h b/gfx/layers/wr/WebRenderBridgeParent.h
new file mode 100644
index 0000000000..c96713e752
--- /dev/null
+++ b/gfx/layers/wr/WebRenderBridgeParent.h
@@ -0,0 +1,533 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_WebRenderBridgeParent_h
+#define mozilla_layers_WebRenderBridgeParent_h
+
+#include <unordered_map>
+#include <unordered_set>
+
+#include "CompositableHost.h" // for CompositableHost, ImageCompositeNotificationInfo
+#include "GLContextProvider.h"
+#include "mozilla/DataMutex.h"
+#include "mozilla/layers/CompositableTransactionParent.h"
+#include "mozilla/layers/CompositorVsyncSchedulerOwner.h"
+#include "mozilla/layers/PWebRenderBridgeParent.h"
+#include "mozilla/HashTable.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/Result.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/webrender/WebRenderTypes.h"
+#include "mozilla/webrender/WebRenderAPI.h"
+#include "nsTArrayForwardDeclare.h"
+#include "WindowRenderer.h"
+
+namespace mozilla {
+
+namespace gl {
+class GLContext;
+}
+
+namespace widget {
+class CompositorWidget;
+}
+
+namespace wr {
+class WebRenderAPI;
+class WebRenderPipelineInfo;
+} // namespace wr
+
+namespace layers {
+
+class AsyncImagePipelineManager;
+class Compositor;
+class CompositorBridgeParentBase;
+class CompositorVsyncScheduler;
+class OMTASampler;
+class UiCompositorControllerParent;
+class WebRenderBridgeParentRef;
+class WebRenderImageHost;
+struct WrAnimations;
+
+struct CompositorAnimationIdsForEpoch {
+ CompositorAnimationIdsForEpoch(const wr::Epoch& aEpoch,
+ nsTArray<uint64_t>&& aIds)
+ : mEpoch(aEpoch), mIds(std::move(aIds)) {}
+
+ wr::Epoch mEpoch;
+ nsTArray<uint64_t> mIds;
+};
+
+class WebRenderBridgeParent final : public PWebRenderBridgeParent,
+ public CompositorVsyncSchedulerOwner,
+ public CompositableParentManager,
+ public FrameRecorder {
+ public:
+ WebRenderBridgeParent(CompositorBridgeParentBase* aCompositorBridge,
+ const wr::PipelineId& aPipelineId,
+ widget::CompositorWidget* aWidget,
+ CompositorVsyncScheduler* aScheduler,
+ RefPtr<wr::WebRenderAPI>&& aApi,
+ RefPtr<AsyncImagePipelineManager>&& aImageMgr,
+ TimeDuration aVsyncRate);
+
+ static WebRenderBridgeParent* CreateDestroyed(
+ const wr::PipelineId& aPipelineId, nsCString&& aError);
+
+ wr::PipelineId PipelineId() { return mPipelineId; }
+ already_AddRefed<wr::WebRenderAPI> GetWebRenderAPI() {
+ return do_AddRef(mApi);
+ }
+ AsyncImagePipelineManager* AsyncImageManager() { return mAsyncImageManager; }
+ CompositorVsyncScheduler* CompositorScheduler() {
+ return mCompositorScheduler.get();
+ }
+ CompositorBridgeParentBase* GetCompositorBridge() {
+ return mCompositorBridge;
+ }
+
+ void UpdateQualitySettings();
+ void UpdateDebugFlags();
+ void UpdateParameters();
+ void UpdateBoolParameters();
+ void UpdateProfilerUI();
+
+ mozilla::ipc::IPCResult RecvEnsureConnected(
+ TextureFactoryIdentifier* aTextureFactoryIdentifier,
+ MaybeIdNamespace* aMaybeIdNamespace, nsCString* aError) override;
+
+ mozilla::ipc::IPCResult RecvNewCompositable(
+ const CompositableHandle& aHandle, const TextureInfo& aInfo) override;
+ mozilla::ipc::IPCResult RecvReleaseCompositable(
+ const CompositableHandle& aHandle) override;
+
+ mozilla::ipc::IPCResult RecvShutdown() override;
+ mozilla::ipc::IPCResult RecvShutdownSync() override;
+ mozilla::ipc::IPCResult RecvDeleteCompositorAnimations(
+ nsTArray<uint64_t>&& aIds) override;
+ mozilla::ipc::IPCResult RecvUpdateResources(
+ const wr::IdNamespace& aIdNamespace,
+ nsTArray<OpUpdateResource>&& aUpdates,
+ nsTArray<RefCountedShmem>&& aSmallShmems,
+ nsTArray<ipc::Shmem>&& aLargeShmems) override;
+ mozilla::ipc::IPCResult RecvSetDisplayList(
+ DisplayListData&& aDisplayList, nsTArray<OpDestroy>&& aToDestroy,
+ const uint64_t& aFwdTransactionId, const TransactionId& aTransactionId,
+ const bool& aContainsSVGGroup, const VsyncId& aVsyncId,
+ const TimeStamp& aVsyncStartTime, const TimeStamp& aRefreshStartTime,
+ const TimeStamp& aTxnStartTime, const nsACString& aTxnURL,
+ const TimeStamp& aFwdTime,
+ nsTArray<CompositionPayload>&& aPayloads) override;
+ mozilla::ipc::IPCResult RecvEmptyTransaction(
+ const FocusTarget& aFocusTarget,
+ Maybe<TransactionData>&& aTransactionData,
+ nsTArray<OpDestroy>&& aToDestroy, const uint64_t& aFwdTransactionId,
+ const TransactionId& aTransactionId, const VsyncId& aVsyncId,
+ const TimeStamp& aVsyncStartTime, const TimeStamp& aRefreshStartTime,
+ const TimeStamp& aTxnStartTime, const nsACString& aTxnURL,
+ const TimeStamp& aFwdTime,
+ nsTArray<CompositionPayload>&& aPayloads) override;
+ mozilla::ipc::IPCResult RecvSetFocusTarget(
+ const FocusTarget& aFocusTarget) override;
+ mozilla::ipc::IPCResult RecvParentCommands(
+ const wr::IdNamespace& aIdNamespace,
+ nsTArray<WebRenderParentCommand>&& commands) override;
+ mozilla::ipc::IPCResult RecvGetSnapshot(NotNull<PTextureParent*> aTexture,
+ bool* aNeedsYFlip) override;
+
+ mozilla::ipc::IPCResult RecvSetLayersObserverEpoch(
+ const LayersObserverEpoch& aChildEpoch) override;
+
+ mozilla::ipc::IPCResult RecvClearCachedResources() override;
+ mozilla::ipc::IPCResult RecvClearAnimationResources() override;
+ mozilla::ipc::IPCResult RecvInvalidateRenderedFrame() override;
+ mozilla::ipc::IPCResult RecvScheduleComposite(
+ const wr::RenderReasons& aReasons) override;
+ mozilla::ipc::IPCResult RecvCapture() override;
+ mozilla::ipc::IPCResult RecvStartCaptureSequence(
+ const nsACString& path, const uint32_t& aFlags) override;
+ mozilla::ipc::IPCResult RecvStopCaptureSequence() override;
+ mozilla::ipc::IPCResult RecvSyncWithCompositor() override;
+
+ mozilla::ipc::IPCResult RecvSetConfirmedTargetAPZC(
+ const uint64_t& aBlockId,
+ nsTArray<ScrollableLayerGuid>&& aTargets) override;
+
+ mozilla::ipc::IPCResult RecvSetTestSampleTime(
+ const TimeStamp& aTime) override;
+ mozilla::ipc::IPCResult RecvLeaveTestMode() override;
+ mozilla::ipc::IPCResult RecvGetAnimationValue(
+ const uint64_t& aCompositorAnimationsId, OMTAValue* aValue) override;
+ mozilla::ipc::IPCResult RecvSetAsyncScrollOffset(
+ const ScrollableLayerGuid::ViewID& aScrollId, const float& aX,
+ const float& aY) override;
+ mozilla::ipc::IPCResult RecvSetAsyncZoom(
+ const ScrollableLayerGuid::ViewID& aScrollId,
+ const float& aZoom) override;
+ mozilla::ipc::IPCResult RecvFlushApzRepaints() override;
+ mozilla::ipc::IPCResult RecvGetAPZTestData(APZTestData* data) override;
+ mozilla::ipc::IPCResult RecvGetFrameUniformity(
+ FrameUniformityData* aOutData) override;
+
+ void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ mozilla::ipc::IPCResult RecvSetDefaultClearColor(
+ const uint32_t& aColor) override;
+ void SetClearColor(const gfx::DeviceColor& aColor);
+
+ void Pause();
+ bool Resume();
+
+ void Destroy();
+
+ // CompositorVsyncSchedulerOwner
+ bool IsPendingComposite() override { return false; }
+ void FinishPendingComposite() override {}
+ void CompositeToTarget(VsyncId aId, wr::RenderReasons aReasons,
+ gfx::DrawTarget* aTarget,
+ const gfx::IntRect* aRect = nullptr) override;
+ TimeDuration GetVsyncInterval() const override;
+
+ // CompositableParentManager
+ bool IsSameProcess() const override;
+ base::ProcessId GetChildProcessId() override;
+ dom::ContentParentId GetContentId() override;
+ void NotifyNotUsed(PTextureParent* aTexture,
+ uint64_t aTransactionId) override;
+ void SendAsyncMessage(
+ const nsTArray<AsyncParentMessageData>& aMessage) override;
+ void SendPendingAsyncMessages() override;
+ void SetAboutToSendAsyncMessages() override;
+
+ void HoldPendingTransactionId(
+ const wr::Epoch& aWrEpoch, TransactionId aTransactionId,
+ bool aContainsSVGGroup, const VsyncId& aVsyncId,
+ const TimeStamp& aVsyncStartTime, const TimeStamp& aRefreshStartTime,
+ const TimeStamp& aTxnStartTime, const nsACString& aTxnURL,
+ const TimeStamp& aFwdTime, const bool aIsFirstPaint,
+ nsTArray<CompositionPayload>&& aPayloads,
+ const bool aUseForTelemetry = true);
+ TransactionId LastPendingTransactionId();
+ void FlushTransactionIdsForEpoch(
+ const wr::Epoch& aEpoch, const VsyncId& aCompositeStartId,
+ const TimeStamp& aCompositeStartTime, const TimeStamp& aRenderStartTime,
+ const TimeStamp& aEndTime, UiCompositorControllerParent* aUiController,
+ wr::RendererStats* aStats, nsTArray<FrameStats>& aOutputStats,
+ nsTArray<TransactionId>& aOutputTransactions);
+ void NotifySceneBuiltForEpoch(const wr::Epoch& aEpoch,
+ const TimeStamp& aEndTime);
+
+ void RetrySkippedComposite();
+
+ TextureFactoryIdentifier GetTextureFactoryIdentifier();
+
+ void ExtractImageCompositeNotifications(
+ nsTArray<ImageCompositeNotificationInfo>* aNotifications);
+
+ wr::Epoch GetCurrentEpoch() const { return mWrEpoch; }
+ wr::IdNamespace GetIdNamespace() { return mIdNamespace; }
+
+ bool MatchesNamespace(const wr::ImageKey& aImageKey) const {
+ return aImageKey.mNamespace == mIdNamespace;
+ }
+
+ bool MatchesNamespace(const wr::BlobImageKey& aBlobKey) const {
+ return MatchesNamespace(wr::AsImageKey(aBlobKey));
+ }
+
+ bool MatchesNamespace(const wr::FontKey& aFontKey) const {
+ return aFontKey.mNamespace == mIdNamespace;
+ }
+
+ bool MatchesNamespace(const wr::FontInstanceKey& aFontKey) const {
+ return aFontKey.mNamespace == mIdNamespace;
+ }
+
+ void FlushRendering(wr::RenderReasons aReasons, bool aWaitForPresent = true);
+
+ /**
+ * Schedule generating WebRender frame definitely at next composite timing.
+ *
+ * WebRenderBridgeParent uses composite timing to check if there is an update
+ * to AsyncImagePipelines. If there is no update, WebRenderBridgeParent skips
+ * to generate frame. If we need to generate new frame at next composite
+ * timing, call this method.
+ *
+ * Call CompositorVsyncScheduler::ScheduleComposition() directly, if we just
+ * want to trigger AsyncImagePipelines update checks.
+ */
+ void ScheduleGenerateFrame(wr::RenderReasons aReason);
+
+ /**
+ * Invalidate rendered frame.
+ *
+ * WebRender could skip frame rendering if there is no update.
+ * This function is used to force invalidating even when there is no update.
+ */
+ void InvalidateRenderedFrame(wr::RenderReasons aReasons);
+
+ /**
+ * Schedule forced frame rendering at next composite timing.
+ *
+ * WebRender could skip frame rendering if there is no update.
+ * This function is used to force rendering even when there is no update.
+ */
+ void ScheduleForcedGenerateFrame(wr::RenderReasons aReasons);
+
+ void NotifyDidSceneBuild(RefPtr<const wr::WebRenderPipelineInfo> aInfo);
+
+ wr::Epoch UpdateWebRender(
+ CompositorVsyncScheduler* aScheduler, RefPtr<wr::WebRenderAPI>&& aApi,
+ AsyncImagePipelineManager* aImageMgr,
+ const TextureFactoryIdentifier& aTextureFactoryIdentifier);
+
+ void RemoveEpochDataPriorTo(const wr::Epoch& aRenderedEpoch);
+
+ bool IsRootWebRenderBridgeParent() const;
+ LayersId GetLayersId() const;
+
+ void BeginRecording(const TimeStamp& aRecordingStart);
+
+#if defined(MOZ_WIDGET_ANDROID)
+ /**
+ * Request a screengrab for android
+ */
+ void RequestScreenPixels(UiCompositorControllerParent* aController);
+ void MaybeCaptureScreenPixels();
+#endif
+ /**
+ * Stop recording and the frames collected since the call to BeginRecording
+ */
+ RefPtr<wr::WebRenderAPI::EndRecordingPromise> EndRecording();
+
+ void DisableNativeCompositor();
+ void AddPendingScrollPayload(CompositionPayload& aPayload,
+ const VsyncId& aCompositeStartId);
+
+ nsTArray<CompositionPayload> TakePendingScrollPayload(
+ const VsyncId& aCompositeStartId);
+
+ RefPtr<WebRenderBridgeParentRef> GetWebRenderBridgeParentRef();
+
+ void FlushPendingWrTransactionEventsWithWait();
+
+ private:
+ class ScheduleSharedSurfaceRelease;
+
+ WebRenderBridgeParent(const wr::PipelineId& aPipelineId, nsCString&& aError);
+ virtual ~WebRenderBridgeParent();
+
+ bool ProcessEmptyTransactionUpdates(TransactionData& aData,
+ bool* aScheduleComposite);
+
+ bool ProcessDisplayListData(DisplayListData& aDisplayList, wr::Epoch aWrEpoch,
+ const TimeStamp& aTxnStartTime,
+ bool aValidTransaction,
+ bool aObserveLayersUpdate);
+
+ bool SetDisplayList(const LayoutDeviceRect& aRect, ipc::ByteBuf&& aDLItems,
+ ipc::ByteBuf&& aDLCache, ipc::ByteBuf&& aSpatialTreeDL,
+ const wr::BuiltDisplayListDescriptor& aDLDesc,
+ const nsTArray<OpUpdateResource>& aResourceUpdates,
+ const nsTArray<RefCountedShmem>& aSmallShmems,
+ const nsTArray<ipc::Shmem>& aLargeShmems,
+ const TimeStamp& aTxnStartTime,
+ wr::TransactionBuilder& aTxn, wr::Epoch aWrEpoch,
+ bool aObserveLayersUpdate);
+
+ void UpdateAPZFocusState(const FocusTarget& aFocus);
+ void UpdateAPZScrollData(const wr::Epoch& aEpoch,
+ WebRenderScrollData&& aData);
+ void UpdateAPZScrollOffsets(ScrollUpdatesMap&& aUpdates,
+ uint32_t aPaintSequenceNumber);
+
+ bool UpdateResources(const nsTArray<OpUpdateResource>& aResourceUpdates,
+ const nsTArray<RefCountedShmem>& aSmallShmems,
+ const nsTArray<ipc::Shmem>& aLargeShmems,
+ wr::TransactionBuilder& aUpdates);
+ bool AddSharedExternalImage(wr::ExternalImageId aExtId, wr::ImageKey aKey,
+ wr::TransactionBuilder& aResources);
+ bool UpdateSharedExternalImage(
+ wr::ExternalImageId aExtId, wr::ImageKey aKey,
+ const ImageIntRect& aDirtyRect, wr::TransactionBuilder& aResources,
+ UniquePtr<ScheduleSharedSurfaceRelease>& aScheduleRelease);
+ void ObserveSharedSurfaceRelease(
+ const nsTArray<wr::ExternalImageKeyPair>& aPairs,
+ const bool& aFromCheckpoint);
+
+ bool PushExternalImageForTexture(wr::ExternalImageId aExtId,
+ wr::ImageKey aKey, TextureHost* aTexture,
+ bool aIsUpdate,
+ wr::TransactionBuilder& aResources);
+
+ void AddPipelineIdForCompositable(const wr::PipelineId& aPipelineIds,
+ const CompositableHandle& aHandle,
+ const CompositableHandleOwner& aOwner,
+ wr::TransactionBuilder& aTxn,
+ wr::TransactionBuilder& aTxnForImageBridge);
+ void RemovePipelineIdForCompositable(const wr::PipelineId& aPipelineId,
+ wr::TransactionBuilder& aTxn);
+
+ void DeleteImage(const wr::ImageKey& aKey, wr::TransactionBuilder& aUpdates);
+ void ReleaseTextureOfImage(const wr::ImageKey& aKey);
+
+ bool ProcessWebRenderParentCommands(
+ const nsTArray<WebRenderParentCommand>& aCommands,
+ wr::TransactionBuilder& aTxn);
+
+ void ClearResources();
+ void ClearAnimationResources();
+ bool ShouldParentObserveEpoch();
+ mozilla::ipc::IPCResult HandleShutdown();
+
+ void ResetPreviousSampleTime();
+
+ void SetOMTASampleTime();
+ RefPtr<OMTASampler> GetOMTASampler() const;
+
+ CompositorBridgeParent* GetRootCompositorBridgeParent() const;
+
+ RefPtr<WebRenderBridgeParent> GetRootWebRenderBridgeParent() const;
+
+ // Tell APZ what the subsequent sampling's timestamp should be.
+ void SetAPZSampleTime();
+
+ wr::Epoch GetNextWrEpoch();
+ // This function is expected to be used when GetNextWrEpoch() is called,
+ // but TransactionBuilder does not have resource updates nor display list.
+ // In this case, ScheduleGenerateFrame is not triggered via SceneBuilder.
+ // Then we want to rollback WrEpoch. See Bug 1490117.
+ void RollbackWrEpoch();
+
+ void FlushSceneBuilds();
+ void FlushFrameGeneration(wr::RenderReasons aReasons);
+ void FlushFramePresentation();
+
+ void MaybeGenerateFrame(VsyncId aId, bool aForceGenerateFrame,
+ wr::RenderReasons aReasons);
+
+ VsyncId GetVsyncIdForEpoch(const wr::Epoch& aEpoch) {
+ for (auto& id : mPendingTransactionIds) {
+ if (id.mEpoch.mHandle == aEpoch.mHandle) {
+ return id.mVsyncId;
+ }
+ }
+ return VsyncId();
+ }
+
+ private:
+ struct PendingTransactionId {
+ PendingTransactionId(const wr::Epoch& aEpoch, TransactionId aId,
+ bool aContainsSVGGroup, const VsyncId& aVsyncId,
+ const TimeStamp& aVsyncStartTime,
+ const TimeStamp& aRefreshStartTime,
+ const TimeStamp& aTxnStartTime,
+ const nsACString& aTxnURL, const TimeStamp& aFwdTime,
+ const bool aIsFirstPaint, const bool aUseForTelemetry,
+ nsTArray<CompositionPayload>&& aPayloads)
+ : mEpoch(aEpoch),
+ mId(aId),
+ mVsyncId(aVsyncId),
+ mVsyncStartTime(aVsyncStartTime),
+ mRefreshStartTime(aRefreshStartTime),
+ mTxnStartTime(aTxnStartTime),
+ mTxnURL(aTxnURL),
+ mFwdTime(aFwdTime),
+ mSkippedComposites(0),
+ mContainsSVGGroup(aContainsSVGGroup),
+ mIsFirstPaint(aIsFirstPaint),
+ mUseForTelemetry(aUseForTelemetry),
+ mPayloads(std::move(aPayloads)) {}
+ wr::Epoch mEpoch;
+ TransactionId mId;
+ VsyncId mVsyncId;
+ TimeStamp mVsyncStartTime;
+ TimeStamp mRefreshStartTime;
+ TimeStamp mTxnStartTime;
+ nsCString mTxnURL;
+ TimeStamp mFwdTime;
+ TimeStamp mSceneBuiltTime;
+ uint32_t mSkippedComposites;
+ bool mContainsSVGGroup;
+ bool mIsFirstPaint;
+ bool mUseForTelemetry;
+ nsTArray<CompositionPayload> mPayloads;
+ };
+
+ CompositorBridgeParentBase* MOZ_NON_OWNING_REF mCompositorBridge;
+ wr::PipelineId mPipelineId;
+ RefPtr<widget::CompositorWidget> mWidget;
+ RefPtr<wr::WebRenderAPI> mApi;
+ RefPtr<AsyncImagePipelineManager> mAsyncImageManager;
+ RefPtr<CompositorVsyncScheduler> mCompositorScheduler;
+ // mActiveAnimations is used to avoid leaking animations when
+ // WebRenderBridgeParent is destroyed abnormally and Tab move between
+ // different windows.
+ std::unordered_map<uint64_t, wr::Epoch> mActiveAnimations;
+ std::unordered_map<uint64_t, RefPtr<WebRenderImageHost>> mAsyncCompositables;
+ std::unordered_map<uint64_t, CompositableTextureHostRef> mTextureHosts;
+ std::unordered_map<uint64_t, wr::ExternalImageId> mSharedSurfaceIds;
+
+ TimeDuration mVsyncRate;
+ TimeStamp mPreviousFrameTimeStamp;
+ // These fields keep track of the latest layer observer epoch values in the
+ // child and the parent. mChildLayersObserverEpoch is the latest epoch value
+ // received from the child. mParentLayersObserverEpoch is the latest epoch
+ // value that we have told BrowserParent about (via ObserveLayerUpdate).
+ LayersObserverEpoch mChildLayersObserverEpoch;
+ LayersObserverEpoch mParentLayersObserverEpoch;
+
+ std::deque<PendingTransactionId> mPendingTransactionIds;
+ std::queue<CompositorAnimationIdsForEpoch> mCompositorAnimationsToDelete;
+ wr::Epoch mWrEpoch;
+ wr::IdNamespace mIdNamespace;
+ CompositionOpportunityId mCompositionOpportunityId;
+ nsCString mInitError;
+
+ TimeStamp mMostRecentComposite;
+
+ RefPtr<WebRenderBridgeParentRef> mWebRenderBridgeRef;
+
+#if defined(MOZ_WIDGET_ANDROID)
+ UiCompositorControllerParent* mScreenPixelsTarget;
+#endif
+ uint32_t mBoolParameterBits;
+ uint16_t mBlobTileSize;
+ wr::RenderReasons mSkippedCompositeReasons;
+ bool mDestroyed;
+ bool mReceivedDisplayList;
+ bool mIsFirstPaint;
+ bool mSkippedComposite;
+ bool mDisablingNativeCompositor;
+ // These payloads are being used for SCROLL_PRESENT_LATENCY telemetry
+ DataMutex<nsClassHashtable<nsUint64HashKey, nsTArray<CompositionPayload>>>
+ mPendingScrollPayloads;
+};
+
+// Use this class, since WebRenderBridgeParent could not supports
+// ThreadSafeWeakPtr.
+// This class provides a ref of WebRenderBridgeParent when
+// the WebRenderBridgeParent is not destroyed. Then it works similar to
+// weak pointer.
+class WebRenderBridgeParentRef final {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(WebRenderBridgeParentRef)
+
+ explicit WebRenderBridgeParentRef(WebRenderBridgeParent* aWebRenderBridge);
+
+ RefPtr<WebRenderBridgeParent> WrBridge();
+ void Clear();
+
+ protected:
+ ~WebRenderBridgeParentRef();
+
+ RefPtr<WebRenderBridgeParent> mWebRenderBridge;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_WebRenderBridgeParent_h
diff --git a/gfx/layers/wr/WebRenderCanvasRenderer.cpp b/gfx/layers/wr/WebRenderCanvasRenderer.cpp
new file mode 100644
index 0000000000..21c13221ac
--- /dev/null
+++ b/gfx/layers/wr/WebRenderCanvasRenderer.cpp
@@ -0,0 +1,90 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "WebRenderCanvasRenderer.h"
+
+#include "GLContext.h"
+#include "GLScreenBuffer.h"
+#include "mozilla/layers/CompositorBridgeChild.h"
+#include "mozilla/StaticPrefs_webgl.h"
+#include "SharedSurfaceGL.h"
+#include "WebRenderBridgeChild.h"
+
+namespace mozilla {
+namespace layers {
+
+CompositableForwarder* WebRenderCanvasRenderer::GetForwarder() {
+ return mManager->WrBridge();
+}
+
+WebRenderCanvasRendererAsync::~WebRenderCanvasRendererAsync() {
+ if (mPipelineId.isSome()) {
+ mManager->RemovePipelineIdForCompositable(mPipelineId.ref());
+ mPipelineId.reset();
+ }
+}
+
+void WebRenderCanvasRendererAsync::Initialize(const CanvasRendererData& aData) {
+ WebRenderCanvasRenderer::Initialize(aData);
+
+ ClearCachedResources();
+}
+
+bool WebRenderCanvasRendererAsync::CreateCompositable() {
+ if (!mCanvasClient) {
+ auto compositableFlags = TextureFlags::NO_FLAGS;
+ if (!mData.mIsAlphaPremult) {
+ // WR needs this flag marked on the compositable, not just the texture.
+ compositableFlags |= TextureFlags::NON_PREMULTIPLIED;
+ }
+ mCanvasClient = new CanvasClient(GetForwarder(), compositableFlags);
+ mCanvasClient->Connect();
+ }
+ return true;
+}
+
+void WebRenderCanvasRendererAsync::EnsurePipeline() {
+ MOZ_ASSERT(mCanvasClient);
+ if (!mCanvasClient) {
+ return;
+ }
+
+ if (mPipelineId) {
+ return;
+ }
+
+ // Alloc async image pipeline id.
+ mPipelineId = Some(
+ mManager->WrBridge()->GetCompositorBridgeChild()->GetNextPipelineId());
+ mManager->AddPipelineIdForCompositable(
+ mPipelineId.ref(), mCanvasClient->GetIPCHandle(),
+ CompositableHandleOwner::WebRenderBridge);
+}
+
+void WebRenderCanvasRendererAsync::ClearCachedResources() {
+ if (mPipelineId.isSome()) {
+ mManager->RemovePipelineIdForCompositable(mPipelineId.ref());
+ mPipelineId.reset();
+ }
+}
+
+void WebRenderCanvasRendererAsync::
+ UpdateCompositableClientForEmptyTransaction() {
+ bool wasDirty = IsDirty();
+ UpdateCompositableClient();
+ if (wasDirty && mPipelineId.isSome()) {
+ // Notify an update of async image pipeline during empty transaction.
+ // During non empty transaction, WebRenderBridgeParent receives
+ // OpUpdateAsyncImagePipeline message, but during empty transaction, the
+ // message is not sent to WebRenderBridgeParent. Then
+ // OpUpdatedAsyncImagePipeline is used to notify the update.
+ mManager->AddWebRenderParentCommand(
+ OpUpdatedAsyncImagePipeline(mPipelineId.ref()));
+ }
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/wr/WebRenderCanvasRenderer.h b/gfx/layers/wr/WebRenderCanvasRenderer.h
new file mode 100644
index 0000000000..de4a1b4fa0
--- /dev/null
+++ b/gfx/layers/wr/WebRenderCanvasRenderer.h
@@ -0,0 +1,56 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GFX_WEBRENDERCANVASRENDERER_H
+#define GFX_WEBRENDERCANVASRENDERER_H
+
+#include "mozilla/layers/RenderRootStateManager.h"
+#include "ShareableCanvasRenderer.h"
+
+namespace mozilla {
+namespace layers {
+
+class WebRenderCanvasRenderer : public ShareableCanvasRenderer {
+ public:
+ explicit WebRenderCanvasRenderer(RenderRootStateManager* aManager)
+ : mManager(aManager) {}
+
+ CompositableForwarder* GetForwarder() override;
+ RenderRootStateManager* GetRenderRootStateManager() { return mManager; }
+
+ protected:
+ RefPtr<RenderRootStateManager> mManager;
+};
+
+class WebRenderCanvasRendererAsync final : public WebRenderCanvasRenderer {
+ public:
+ explicit WebRenderCanvasRendererAsync(RenderRootStateManager* aManager)
+ : WebRenderCanvasRenderer(aManager) {}
+ virtual ~WebRenderCanvasRendererAsync();
+
+ WebRenderCanvasRendererAsync* AsWebRenderCanvasRendererAsync() override {
+ return this;
+ }
+
+ void Initialize(const CanvasRendererData& aData) override;
+ bool CreateCompositable() override;
+ void EnsurePipeline() override;
+
+ void ClearCachedResources() override;
+
+ void UpdateCompositableClientForEmptyTransaction();
+
+ Maybe<wr::PipelineId> GetPipelineId() { return mPipelineId; }
+
+ protected:
+ Maybe<wr::PipelineId> mPipelineId;
+ bool mIsAsync = false;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif
diff --git a/gfx/layers/wr/WebRenderCommandBuilder.cpp b/gfx/layers/wr/WebRenderCommandBuilder.cpp
new file mode 100644
index 0000000000..f280bb8a0c
--- /dev/null
+++ b/gfx/layers/wr/WebRenderCommandBuilder.cpp
@@ -0,0 +1,2959 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "WebRenderCommandBuilder.h"
+
+#include "mozilla/AutoRestore.h"
+#include "mozilla/DebugOnly.h"
+#include "mozilla/EffectCompositor.h"
+#include "mozilla/ProfilerLabels.h"
+#include "mozilla/StaticPrefs_gfx.h"
+#include "mozilla/SVGGeometryFrame.h"
+#include "mozilla/SVGImageFrame.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/gfx/2D.h"
+#include "mozilla/gfx/Logging.h"
+#include "mozilla/gfx/Types.h"
+#include "mozilla/image/WebRenderImageProvider.h"
+#include "mozilla/layers/AnimationHelper.h"
+#include "mozilla/layers/ClipManager.h"
+#include "mozilla/layers/ImageClient.h"
+#include "mozilla/layers/RenderRootStateManager.h"
+#include "mozilla/layers/WebRenderBridgeChild.h"
+#include "mozilla/layers/WebRenderLayerManager.h"
+#include "mozilla/layers/IpcResourceUpdateQueue.h"
+#include "mozilla/layers/SharedSurfacesChild.h"
+#include "mozilla/layers/SourceSurfaceSharedData.h"
+#include "mozilla/layers/StackingContextHelper.h"
+#include "mozilla/layers/UpdateImageHelper.h"
+#include "mozilla/layers/WebRenderDrawEventRecorder.h"
+#include "UnitTransforms.h"
+#include "gfxEnv.h"
+#include "nsDisplayListInvalidation.h"
+#include "nsLayoutUtils.h"
+#include "nsTHashSet.h"
+#include "WebRenderCanvasRenderer.h"
+
+#include <cstdint>
+
+namespace mozilla {
+namespace layers {
+
+using namespace gfx;
+using namespace image;
+static int sIndent;
+#include <stdarg.h>
+#include <stdio.h>
+
+static void GP(const char* fmt, ...) {
+ va_list args;
+ va_start(args, fmt);
+#if 0
+ for (int i = 0; i < sIndent; i++) { printf(" "); }
+ vprintf(fmt, args);
+#endif
+ va_end(args);
+}
+
+bool FitsInt32(const float aVal) {
+ // Although int32_t min and max can't be represented exactly with floats, the
+ // cast truncates towards zero which is what we want here.
+ const float min = static_cast<float>(std::numeric_limits<int32_t>::min());
+ const float max = static_cast<float>(std::numeric_limits<int32_t>::max());
+ return aVal > min && aVal < max;
+}
+
+// XXX: problems:
+// - How do we deal with scrolling while having only a single invalidation rect?
+// We can have a valid rect and an invalid rect. As we scroll the valid rect
+// will move and the invalid rect will be the new area
+
+struct BlobItemData;
+static void DestroyBlobGroupDataProperty(nsTArray<BlobItemData*>* aArray);
+NS_DECLARE_FRAME_PROPERTY_WITH_DTOR(BlobGroupDataProperty,
+ nsTArray<BlobItemData*>,
+ DestroyBlobGroupDataProperty);
+
+// These are currently manually allocated and ownership is help by the
+// mDisplayItems hash table in DIGroup
+struct BlobItemData {
+ // a weak pointer to the frame for this item.
+ // DisplayItemData has a mFrameList to deal with merged frames. Hopefully we
+ // don't need to worry about that.
+ nsIFrame* mFrame;
+
+ uint32_t mDisplayItemKey;
+ nsTArray<BlobItemData*>*
+ mArray; // a weak pointer to the array that's owned by the frame property
+
+ LayerIntRect mRect;
+ // It would be nice to not need this. We need to be able to call
+ // ComputeInvalidationRegion. ComputeInvalidationRegion will sometimes reach
+ // into parent style structs to get information that can change the
+ // invalidation region
+ UniquePtr<nsDisplayItemGeometry> mGeometry;
+ DisplayItemClip mClip;
+ bool mInvisible;
+ bool mUsed; // initialized near construction
+ // XXX: only used for debugging
+ bool mInvalid;
+
+ // a weak pointer to the group that owns this item
+ // we use this to track whether group for a particular item has changed
+ struct DIGroup* mGroup;
+
+ // We need to keep a list of all the external surfaces used by the blob image.
+ // We do this on a per-display item basis so that the lists remains correct
+ // during invalidations.
+ std::vector<RefPtr<SourceSurface>> mExternalSurfaces;
+
+ BlobItemData(DIGroup* aGroup, nsDisplayItem* aItem)
+ : mInvisible(false), mUsed(false), mGroup(aGroup) {
+ mInvalid = false;
+ mDisplayItemKey = aItem->GetPerFrameKey();
+ AddFrame(aItem->Frame());
+ }
+
+ private:
+ void AddFrame(nsIFrame* aFrame) {
+ mFrame = aFrame;
+
+ nsTArray<BlobItemData*>* array =
+ aFrame->GetProperty(BlobGroupDataProperty());
+ if (!array) {
+ array = new nsTArray<BlobItemData*>();
+ aFrame->SetProperty(BlobGroupDataProperty(), array);
+ }
+ array->AppendElement(this);
+ mArray = array;
+ }
+
+ public:
+ void ClearFrame() {
+ // Delete the weak pointer to this BlobItemData on the frame
+ MOZ_RELEASE_ASSERT(mFrame);
+ // the property may already be removed if WebRenderUserData got deleted
+ // first so we use our own mArray pointer.
+ mArray->RemoveElement(this);
+
+ // drop the entire property if nothing's left in the array
+ if (mArray->IsEmpty()) {
+ // If the frame is in the process of being destroyed this will fail
+ // but that's ok, because the the property will be removed then anyways
+ mFrame->RemoveProperty(BlobGroupDataProperty());
+ }
+ mFrame = nullptr;
+ }
+
+ ~BlobItemData() {
+ if (mFrame) {
+ ClearFrame();
+ }
+ }
+};
+
+static BlobItemData* GetBlobItemData(nsDisplayItem* aItem) {
+ nsIFrame* frame = aItem->Frame();
+ uint32_t key = aItem->GetPerFrameKey();
+ const nsTArray<BlobItemData*>* array =
+ frame->GetProperty(BlobGroupDataProperty());
+ if (array) {
+ for (BlobItemData* item : *array) {
+ if (item->mDisplayItemKey == key) {
+ return item;
+ }
+ }
+ }
+ return nullptr;
+}
+
+// We keep around the BlobItemData so that when we invalidate it get properly
+// included in the rect
+static void DestroyBlobGroupDataProperty(nsTArray<BlobItemData*>* aArray) {
+ for (BlobItemData* item : *aArray) {
+ GP("DestroyBlobGroupDataProperty: %p-%d\n", item->mFrame,
+ item->mDisplayItemKey);
+ item->mFrame = nullptr;
+ }
+ delete aArray;
+}
+
+static void TakeExternalSurfaces(
+ WebRenderDrawEventRecorder* aRecorder,
+ std::vector<RefPtr<SourceSurface>>& aExternalSurfaces,
+ RenderRootStateManager* aManager, wr::IpcResourceUpdateQueue& aResources) {
+ aRecorder->TakeExternalSurfaces(aExternalSurfaces);
+
+ for (auto& surface : aExternalSurfaces) {
+ // While we don't use the image key with the surface, because the blob image
+ // renderer doesn't have easy access to the resource set, we still want to
+ // ensure one is generated. That will ensure the surface remains alive until
+ // at least the last epoch which the blob image could be used in.
+ wr::ImageKey key;
+ DebugOnly<nsresult> rv =
+ SharedSurfacesChild::Share(surface, aManager, aResources, key);
+ MOZ_ASSERT(rv.value != NS_ERROR_NOT_IMPLEMENTED);
+ }
+}
+
+struct DIGroup;
+struct Grouper {
+ explicit Grouper(ClipManager& aClipManager)
+ : mAppUnitsPerDevPixel(0),
+ mDisplayListBuilder(nullptr),
+ mClipManager(aClipManager) {}
+
+ int32_t mAppUnitsPerDevPixel;
+ nsDisplayListBuilder* mDisplayListBuilder;
+ ClipManager& mClipManager;
+ HitTestInfoManager mHitTestInfoManager;
+ Matrix mTransform;
+
+ // Paint the list of aChildren display items.
+ void PaintContainerItem(DIGroup* aGroup, nsDisplayItem* aItem,
+ BlobItemData* aData, const IntRect& aItemBounds,
+ bool aDirty, nsDisplayList* aChildren,
+ gfxContext* aContext,
+ WebRenderDrawEventRecorder* aRecorder,
+ RenderRootStateManager* aRootManager,
+ wr::IpcResourceUpdateQueue& aResources);
+
+ // Builds groups of display items split based on 'layer activity'
+ void ConstructGroups(nsDisplayListBuilder* aDisplayListBuilder,
+ WebRenderCommandBuilder* aCommandBuilder,
+ wr::DisplayListBuilder& aBuilder,
+ wr::IpcResourceUpdateQueue& aResources, DIGroup* aGroup,
+ nsDisplayList* aList, nsDisplayItem* aWrappingItem,
+ const StackingContextHelper& aSc);
+ // Builds a group of display items without promoting anything to active.
+ bool ConstructGroupInsideInactive(WebRenderCommandBuilder* aCommandBuilder,
+ wr::DisplayListBuilder& aBuilder,
+ wr::IpcResourceUpdateQueue& aResources,
+ DIGroup* aGroup, nsDisplayList* aList,
+ const StackingContextHelper& aSc);
+ // Helper method for processing a single inactive item
+ bool ConstructItemInsideInactive(WebRenderCommandBuilder* aCommandBuilder,
+ wr::DisplayListBuilder& aBuilder,
+ wr::IpcResourceUpdateQueue& aResources,
+ DIGroup* aGroup, nsDisplayItem* aItem,
+ const StackingContextHelper& aSc,
+ bool* aOutIsInvisible);
+ ~Grouper() = default;
+};
+
+// Returns whether this is an item for which complete invalidation was
+// reliant on LayerTreeInvalidation in the pre-webrender world.
+static bool IsContainerLayerItem(nsDisplayItem* aItem) {
+ switch (aItem->GetType()) {
+ case DisplayItemType::TYPE_WRAP_LIST:
+ case DisplayItemType::TYPE_CONTAINER:
+ case DisplayItemType::TYPE_TRANSFORM:
+ case DisplayItemType::TYPE_OPACITY:
+ case DisplayItemType::TYPE_FILTER:
+ case DisplayItemType::TYPE_BLEND_CONTAINER:
+ case DisplayItemType::TYPE_BLEND_MODE:
+ case DisplayItemType::TYPE_MASK:
+ case DisplayItemType::TYPE_PERSPECTIVE: {
+ return true;
+ }
+ default: {
+ return false;
+ }
+ }
+}
+
+#include <sstream>
+
+static bool DetectContainerLayerPropertiesBoundsChange(
+ nsDisplayItem* aItem, BlobItemData* aData,
+ nsDisplayItemGeometry& aGeometry) {
+ if (aItem->GetType() == DisplayItemType::TYPE_FILTER) {
+ // Filters get clipped to the BuildingRect since they can
+ // have huge bounds outside of the visible area.
+ aGeometry.mBounds = aGeometry.mBounds.Intersect(aItem->GetBuildingRect());
+ }
+
+ return !aGeometry.mBounds.IsEqualEdges(aData->mGeometry->mBounds);
+}
+
+/* A Display Item Group. This represents a set of diplay items that
+ * have been grouped together for rasterization and can be partially
+ * invalidated. It also tracks a number of properties from the environment
+ * that when changed would cause us to repaint like mScale. */
+struct DIGroup {
+ // XXX: Storing owning pointers to the BlobItemData in a hash table is not
+ // a good choice. There are two better options:
+ //
+ // 1. We should just be using a linked list for this stuff.
+ // That we can iterate over only the used items.
+ // We remove from the unused list and add to the used list
+ // when we see an item.
+ //
+ // we allocate using a free list.
+ //
+ // 2. We can use a Vec and use SwapRemove().
+ // We'll just need to be careful when iterating.
+ // The advantage of a Vec is that everything stays compact
+ // and we don't need to heap allocate the BlobItemData's
+ nsTHashSet<BlobItemData*> mDisplayItems;
+
+ LayerIntRect mInvalidRect;
+ LayerIntRect mVisibleRect;
+ // This is the last visible rect sent to WebRender. It's used
+ // to compute the invalid rect and ensure that we send
+ // the appropriate data to WebRender for merging.
+ LayerIntRect mLastVisibleRect;
+
+ // This is the intersection of mVisibleRect and mLastVisibleRect
+ // we ensure that mInvalidRect is contained in mPreservedRect
+ LayerIntRect mPreservedRect;
+ // mHitTestBounds is the same as mActualBounds except for the bounds
+ // of invisible items which are accounted for in the former but not
+ // in the latter.
+ LayerIntRect mHitTestBounds;
+ LayerIntRect mActualBounds;
+ int32_t mAppUnitsPerDevPixel;
+ gfx::MatrixScales mScale;
+ ScrollableLayerGuid::ViewID mScrollId;
+ CompositorHitTestInfo mHitInfo;
+ LayerPoint mResidualOffset;
+ LayerIntRect mLayerBounds; // mGroupBounds converted to Layer space
+ // mLayerBounds clipped to the container/parent of the
+ // current item being processed.
+ LayerIntRect mClippedImageBounds; // mLayerBounds with the clipping of any
+ // containers applied
+ Maybe<wr::BlobImageKey> mKey;
+ std::vector<RefPtr<ScaledFont>> mFonts;
+
+ DIGroup()
+ : mAppUnitsPerDevPixel(0),
+ mScrollId(ScrollableLayerGuid::NULL_SCROLL_ID),
+ mHitInfo(CompositorHitTestInvisibleToHit) {}
+
+ void InvalidateRect(const LayerIntRect& aRect) {
+ auto r = aRect.Intersect(mPreservedRect);
+ // Empty rects get dropped
+ if (!r.IsEmpty()) {
+ mInvalidRect = mInvalidRect.Union(r);
+ }
+ }
+
+ LayerIntRect ItemBounds(nsDisplayItem* aItem) {
+ BlobItemData* data = GetBlobItemData(aItem);
+ return data->mRect;
+ }
+
+ void ClearItems() {
+ GP("items: %d\n", mDisplayItems.Count());
+ for (BlobItemData* data : mDisplayItems) {
+ GP("Deleting %p-%d\n", data->mFrame, data->mDisplayItemKey);
+ delete data;
+ }
+ mDisplayItems.Clear();
+ }
+
+ void ClearImageKey(RenderRootStateManager* aManager, bool aForce = false) {
+ if (mKey) {
+ MOZ_RELEASE_ASSERT(aForce || mInvalidRect.IsEmpty());
+ aManager->AddBlobImageKeyForDiscard(*mKey);
+ mKey = Nothing();
+ }
+ mFonts.clear();
+ }
+
+ static LayerIntRect ToDeviceSpace(nsRect aBounds, Matrix& aMatrix,
+ int32_t aAppUnitsPerDevPixel) {
+ // RoundedOut can convert empty rectangles to non-empty ones
+ // so special case them here
+ if (aBounds.IsEmpty()) {
+ return LayerIntRect();
+ }
+ return LayerIntRect::FromUnknownRect(RoundedOut(aMatrix.TransformBounds(
+ ToRect(nsLayoutUtils::RectToGfxRect(aBounds, aAppUnitsPerDevPixel)))));
+ }
+
+ bool ComputeGeometryChange(nsDisplayItem* aItem, BlobItemData* aData,
+ Matrix& aMatrix, nsDisplayListBuilder* aBuilder) {
+ // If the frame is marked as invalidated, and didn't specify a rect to
+ // invalidate then we want to invalidate both the old and new bounds,
+ // otherwise we only want to invalidate the changed areas. If we do get an
+ // invalid rect, then we want to add this on top of the change areas.
+ nsRect invalid;
+ bool invalidated = false;
+ const DisplayItemClip& clip = aItem->GetClip();
+
+ int32_t appUnitsPerDevPixel =
+ aItem->Frame()->PresContext()->AppUnitsPerDevPixel();
+ MOZ_RELEASE_ASSERT(mAppUnitsPerDevPixel == appUnitsPerDevPixel);
+ GP("\n");
+ GP("clippedImageRect %d %d %d %d\n", mClippedImageBounds.x,
+ mClippedImageBounds.y, mClippedImageBounds.width,
+ mClippedImageBounds.height);
+ LayerIntSize size = mVisibleRect.Size();
+ GP("imageSize: %d %d\n", size.width, size.height);
+ /*if (aItem->IsReused() && aData->mGeometry) {
+ return;
+ }*/
+
+ GP("pre mInvalidRect: %s %p-%d - inv: %d %d %d %d\n", aItem->Name(),
+ aItem->Frame(), aItem->GetPerFrameKey(), mInvalidRect.x, mInvalidRect.y,
+ mInvalidRect.width, mInvalidRect.height);
+ if (!aData->mGeometry) {
+ // This item is being added for the first time, invalidate its entire
+ // area.
+ UniquePtr<nsDisplayItemGeometry> geometry(
+ aItem->AllocateGeometry(aBuilder));
+ nsRect clippedBounds = clip.ApplyNonRoundedIntersection(
+ geometry->ComputeInvalidationRegion());
+ aData->mGeometry = std::move(geometry);
+
+ LayerIntRect transformedRect =
+ ToDeviceSpace(clippedBounds, aMatrix, appUnitsPerDevPixel);
+ aData->mRect = transformedRect.Intersect(mClippedImageBounds);
+ GP("CGC %s %d %d %d %d\n", aItem->Name(), clippedBounds.x,
+ clippedBounds.y, clippedBounds.width, clippedBounds.height);
+ GP("%d %d, %f %f\n", mVisibleRect.TopLeft().x.value,
+ mVisibleRect.TopLeft().y.value, aMatrix._11, aMatrix._22);
+ GP("mRect %d %d %d %d\n", aData->mRect.x, aData->mRect.y,
+ aData->mRect.width, aData->mRect.height);
+ InvalidateRect(aData->mRect);
+ aData->mInvalid = true;
+ invalidated = true;
+ } else if (aItem->IsInvalid(invalid) && invalid.IsEmpty()) {
+ UniquePtr<nsDisplayItemGeometry> geometry(
+ aItem->AllocateGeometry(aBuilder));
+ nsRect clippedBounds = clip.ApplyNonRoundedIntersection(
+ geometry->ComputeInvalidationRegion());
+ aData->mGeometry = std::move(geometry);
+
+ GP("matrix: %f %f\n", aMatrix._31, aMatrix._32);
+ GP("frame invalid invalidate: %s\n", aItem->Name());
+ GP("old rect: %d %d %d %d\n", aData->mRect.x, aData->mRect.y,
+ aData->mRect.width, aData->mRect.height);
+ InvalidateRect(aData->mRect);
+ // We want to snap to outside pixels. When should we multiply by the
+ // matrix?
+ // XXX: TransformBounds is expensive. We should avoid doing it if we have
+ // no transform
+ LayerIntRect transformedRect =
+ ToDeviceSpace(clippedBounds, aMatrix, appUnitsPerDevPixel);
+ aData->mRect = transformedRect.Intersect(mClippedImageBounds);
+ InvalidateRect(aData->mRect);
+ GP("new rect: %d %d %d %d\n", aData->mRect.x, aData->mRect.y,
+ aData->mRect.width, aData->mRect.height);
+ aData->mInvalid = true;
+ invalidated = true;
+ } else {
+ GP("else invalidate: %s\n", aItem->Name());
+ nsRegion combined;
+ // this includes situations like reflow changing the position
+ aItem->ComputeInvalidationRegion(aBuilder, aData->mGeometry.get(),
+ &combined);
+ if (!combined.IsEmpty()) {
+ // There might be no point in doing this elaborate tracking here to get
+ // smaller areas
+ InvalidateRect(aData->mRect); // invalidate the old area -- in theory
+ // combined should take care of this
+ UniquePtr<nsDisplayItemGeometry> geometry(
+ aItem->AllocateGeometry(aBuilder));
+ // invalidate the invalidated area.
+
+ aData->mGeometry = std::move(geometry);
+
+ nsRect clippedBounds = clip.ApplyNonRoundedIntersection(
+ aData->mGeometry->ComputeInvalidationRegion());
+ LayerIntRect transformedRect =
+ ToDeviceSpace(clippedBounds, aMatrix, appUnitsPerDevPixel);
+ aData->mRect = transformedRect.Intersect(mClippedImageBounds);
+ InvalidateRect(aData->mRect);
+
+ aData->mInvalid = true;
+ invalidated = true;
+ } else {
+ if (aData->mClip != clip) {
+ UniquePtr<nsDisplayItemGeometry> geometry(
+ aItem->AllocateGeometry(aBuilder));
+ if (!IsContainerLayerItem(aItem)) {
+ // the bounds of layer items can change on us without
+ // ComputeInvalidationRegion returning any change. Other items
+ // shouldn't have any hidden geometry change.
+ MOZ_RELEASE_ASSERT(
+ geometry->mBounds.IsEqualEdges(aData->mGeometry->mBounds));
+ } else {
+ aData->mGeometry = std::move(geometry);
+ }
+ nsRect clippedBounds = clip.ApplyNonRoundedIntersection(
+ aData->mGeometry->ComputeInvalidationRegion());
+ LayerIntRect transformedRect =
+ ToDeviceSpace(clippedBounds, aMatrix, appUnitsPerDevPixel);
+ InvalidateRect(aData->mRect);
+ aData->mRect = transformedRect.Intersect(mClippedImageBounds);
+ InvalidateRect(aData->mRect);
+ invalidated = true;
+
+ GP("ClipChange: %s %d %d %d %d\n", aItem->Name(), aData->mRect.x,
+ aData->mRect.y, aData->mRect.XMost(), aData->mRect.YMost());
+
+ } else if (IsContainerLayerItem(aItem)) {
+ UniquePtr<nsDisplayItemGeometry> geometry(
+ aItem->AllocateGeometry(aBuilder));
+ // we need to catch bounds changes of containers so that we continue
+ // to have the correct bounds rects in the recording
+ if (DetectContainerLayerPropertiesBoundsChange(aItem, aData,
+ *geometry)) {
+ nsRect clippedBounds = clip.ApplyNonRoundedIntersection(
+ geometry->ComputeInvalidationRegion());
+ aData->mGeometry = std::move(geometry);
+ LayerIntRect transformedRect =
+ ToDeviceSpace(clippedBounds, aMatrix, appUnitsPerDevPixel);
+ InvalidateRect(aData->mRect);
+ aData->mRect = transformedRect.Intersect(mClippedImageBounds);
+ InvalidateRect(aData->mRect);
+ invalidated = true;
+ GP("DetectContainerLayerPropertiesBoundsChange change\n");
+ } else {
+ // Handle changes in mClippedImageBounds
+ nsRect clippedBounds = clip.ApplyNonRoundedIntersection(
+ geometry->ComputeInvalidationRegion());
+ LayerIntRect transformedRect =
+ ToDeviceSpace(clippedBounds, aMatrix, appUnitsPerDevPixel);
+ auto rect = transformedRect.Intersect(mClippedImageBounds);
+ if (!rect.IsEqualEdges(aData->mRect)) {
+ GP("ContainerLayer image rect bounds change\n");
+ InvalidateRect(aData->mRect);
+ aData->mRect = rect;
+ InvalidateRect(aData->mRect);
+ invalidated = true;
+ } else {
+ GP("Layer NoChange: %s %d %d %d %d\n", aItem->Name(),
+ aData->mRect.x, aData->mRect.y, aData->mRect.XMost(),
+ aData->mRect.YMost());
+ }
+ }
+ } else {
+ UniquePtr<nsDisplayItemGeometry> geometry(
+ aItem->AllocateGeometry(aBuilder));
+ nsRect clippedBounds = clip.ApplyNonRoundedIntersection(
+ geometry->ComputeInvalidationRegion());
+ LayerIntRect transformedRect =
+ ToDeviceSpace(clippedBounds, aMatrix, appUnitsPerDevPixel);
+ auto rect = transformedRect.Intersect(mClippedImageBounds);
+ // Make sure we update mRect for mClippedImageBounds changes
+ if (!rect.IsEqualEdges(aData->mRect)) {
+ GP("ContainerLayer image rect bounds change\n");
+ InvalidateRect(aData->mRect);
+ aData->mRect = rect;
+ InvalidateRect(aData->mRect);
+ invalidated = true;
+ } else {
+ GP("NoChange: %s %d %d %d %d\n", aItem->Name(), aData->mRect.x,
+ aData->mRect.y, aData->mRect.XMost(), aData->mRect.YMost());
+ }
+ }
+ }
+ }
+
+ mHitTestBounds.OrWith(aData->mRect);
+ if (!aData->mInvisible) {
+ mActualBounds.OrWith(aData->mRect);
+ }
+ aData->mClip = clip;
+ GP("post mInvalidRect: %d %d %d %d\n", mInvalidRect.x, mInvalidRect.y,
+ mInvalidRect.width, mInvalidRect.height);
+ return invalidated;
+ }
+
+ void EndGroup(WebRenderLayerManager* aWrManager,
+ nsDisplayListBuilder* aDisplayListBuilder,
+ wr::DisplayListBuilder& aBuilder,
+ wr::IpcResourceUpdateQueue& aResources, Grouper* aGrouper,
+ nsDisplayList::iterator aStartItem,
+ nsDisplayList::iterator aEndItem) {
+ GP("\n\n");
+ GP("Begin EndGroup\n");
+
+ auto scale = LayoutDeviceToLayerScale2D::FromUnknownScale(mScale);
+
+ auto hitTestRect = mVisibleRect.Intersect(ViewAs<LayerPixel>(
+ mHitTestBounds, PixelCastJustification::LayerIsImage));
+ if (!hitTestRect.IsEmpty()) {
+ auto deviceHitTestRect =
+ (LayerRect(hitTestRect) - mResidualOffset) / scale;
+ PushHitTest(aBuilder, deviceHitTestRect);
+ }
+
+ mVisibleRect = mVisibleRect.Intersect(ViewAs<LayerPixel>(
+ mActualBounds, PixelCastJustification::LayerIsImage));
+
+ if (mVisibleRect.IsEmpty()) {
+ return;
+ }
+
+ // Invalidate any unused items
+ GP("mDisplayItems\n");
+ mDisplayItems.RemoveIf([&](BlobItemData* data) {
+ GP(" : %p-%d\n", data->mFrame, data->mDisplayItemKey);
+ if (!data->mUsed) {
+ GP("Invalidate unused: %p-%d\n", data->mFrame, data->mDisplayItemKey);
+ InvalidateRect(data->mRect);
+ delete data;
+ return true;
+ }
+
+ data->mUsed = false;
+ return false;
+ });
+
+ IntSize dtSize = mVisibleRect.Size().ToUnknownSize();
+ // The actual display item's size shouldn't have the scale factored in
+ // Round the bounds out to leave space for unsnapped content
+ LayoutDeviceRect itemBounds =
+ (LayerRect(mVisibleRect) - mResidualOffset) / scale;
+
+ if (mInvalidRect.IsEmpty() && mVisibleRect.IsEqualEdges(mLastVisibleRect)) {
+ GP("Not repainting group because it's empty\n");
+ GP("End EndGroup\n");
+ if (mKey) {
+ // Although the contents haven't changed, the visible area *may* have,
+ // so request it be updated unconditionally (wr should be able to easily
+ // detect if this is a no-op on its side, if that matters)
+ aResources.SetBlobImageVisibleArea(
+ *mKey, ViewAs<ImagePixel>(mVisibleRect,
+ PixelCastJustification::LayerIsImage));
+ mLastVisibleRect = mVisibleRect;
+ PushImage(aBuilder, itemBounds);
+ }
+ return;
+ }
+
+ std::vector<RefPtr<ScaledFont>> fonts;
+ bool validFonts = true;
+ RefPtr<WebRenderDrawEventRecorder> recorder =
+ MakeAndAddRef<WebRenderDrawEventRecorder>(
+ [&](MemStream& aStream,
+ std::vector<RefPtr<ScaledFont>>& aScaledFonts) {
+ size_t count = aScaledFonts.size();
+ aStream.write((const char*)&count, sizeof(count));
+ for (auto& scaled : aScaledFonts) {
+ Maybe<wr::FontInstanceKey> key =
+ aWrManager->WrBridge()->GetFontKeyForScaledFont(scaled,
+ aResources);
+ if (key.isNothing()) {
+ validFonts = false;
+ break;
+ }
+ BlobFont font = {key.value(), scaled};
+ aStream.write((const char*)&font, sizeof(font));
+ }
+ fonts = std::move(aScaledFonts);
+ });
+
+ RefPtr<gfx::DrawTarget> dummyDt =
+ gfxPlatform::GetPlatform()->ScreenReferenceDrawTarget();
+
+ RefPtr<gfx::DrawTarget> dt = gfx::Factory::CreateRecordingDrawTarget(
+ recorder, dummyDt, mLayerBounds.ToUnknownRect());
+ if (!dt || !dt->IsValid()) {
+ gfxCriticalNote << "Failed to create drawTarget for blob image";
+ return;
+ }
+
+ gfxContext context(dt);
+ context.SetMatrix(Matrix::Scaling(mScale).PostTranslate(mResidualOffset.x,
+ mResidualOffset.y));
+
+ GP("mInvalidRect: %d %d %d %d\n", mInvalidRect.x, mInvalidRect.y,
+ mInvalidRect.width, mInvalidRect.height);
+
+ RenderRootStateManager* rootManager =
+ aWrManager->GetRenderRootStateManager();
+
+ bool empty = aStartItem == aEndItem;
+ if (empty) {
+ ClearImageKey(rootManager, true);
+ return;
+ }
+
+ PaintItemRange(aGrouper, aStartItem, aEndItem, &context, recorder,
+ rootManager, aResources);
+
+ // XXX: set this correctly perhaps using
+ // aItem->GetOpaqueRegion(aDisplayListBuilder, &snapped).
+ // Contains(paintBounds);?
+ wr::OpacityType opacity = wr::OpacityType::HasAlphaChannel;
+
+ bool hasItems = recorder->Finish();
+ GP("%d Finish\n", hasItems);
+ if (!validFonts) {
+ gfxCriticalNote << "Failed serializing fonts for blob image";
+ return;
+ }
+ Range<uint8_t> bytes((uint8_t*)recorder->mOutputStream.mData,
+ recorder->mOutputStream.mLength);
+ if (!mKey) {
+ // we don't want to send a new image that doesn't have any
+ // items in it
+ if (!hasItems || mVisibleRect.IsEmpty()) {
+ GP("Skipped group with no items\n");
+ return;
+ }
+
+ wr::BlobImageKey key =
+ wr::BlobImageKey{aWrManager->WrBridge()->GetNextImageKey()};
+ GP("No previous key making new one %d\n", key._0.mHandle);
+ wr::ImageDescriptor descriptor(dtSize, 0, dt->GetFormat(), opacity);
+ MOZ_RELEASE_ASSERT(bytes.length() > sizeof(size_t));
+ if (!aResources.AddBlobImage(
+ key, descriptor, bytes,
+ ViewAs<ImagePixel>(mVisibleRect,
+ PixelCastJustification::LayerIsImage))) {
+ return;
+ }
+ mKey = Some(key);
+ } else {
+ MOZ_DIAGNOSTIC_ASSERT(
+ aWrManager->WrBridge()->MatchesNamespace(mKey.ref()),
+ "Stale blob key for group!");
+
+ wr::ImageDescriptor descriptor(dtSize, 0, dt->GetFormat(), opacity);
+
+ // Convert mInvalidRect to image space by subtracting the corner of the
+ // image bounds
+ auto dirtyRect = ViewAs<ImagePixel>(mInvalidRect,
+ PixelCastJustification::LayerIsImage);
+
+ auto bottomRight = dirtyRect.BottomRight();
+ GP("check invalid %d %d - %d %d\n", bottomRight.x.value,
+ bottomRight.y.value, dtSize.width, dtSize.height);
+ GP("Update Blob %d %d %d %d\n", mInvalidRect.x, mInvalidRect.y,
+ mInvalidRect.width, mInvalidRect.height);
+ if (!aResources.UpdateBlobImage(
+ *mKey, descriptor, bytes,
+ ViewAs<ImagePixel>(mVisibleRect,
+ PixelCastJustification::LayerIsImage),
+ dirtyRect)) {
+ return;
+ }
+ }
+ mFonts = std::move(fonts);
+ aResources.SetBlobImageVisibleArea(
+ *mKey,
+ ViewAs<ImagePixel>(mVisibleRect, PixelCastJustification::LayerIsImage));
+ mLastVisibleRect = mVisibleRect;
+ PushImage(aBuilder, itemBounds);
+ GP("End EndGroup\n\n");
+ }
+
+ void PushImage(wr::DisplayListBuilder& aBuilder,
+ const LayoutDeviceRect& bounds) {
+ wr::LayoutRect dest = wr::ToLayoutRect(bounds);
+ GP("PushImage: %f %f %f %f\n", dest.min.x, dest.min.y, dest.max.x,
+ dest.max.y);
+ // wr::ToImageRendering(aItem->Frame()->UsedImageRendering());
+ auto rendering = wr::ImageRendering::Auto;
+ bool backfaceHidden = false;
+
+ // XXX - clipping the item against the paint rect breaks some content.
+ // cf. Bug 1455422.
+ // wr::LayoutRect clip = wr::ToLayoutRect(bounds.Intersect(mVisibleRect));
+
+ aBuilder.PushImage(dest, dest, !backfaceHidden, false, rendering,
+ wr::AsImageKey(*mKey));
+ }
+
+ void PushHitTest(wr::DisplayListBuilder& aBuilder,
+ const LayoutDeviceRect& bounds) {
+ wr::LayoutRect dest = wr::ToLayoutRect(bounds);
+ GP("PushHitTest: %f %f %f %f\n", dest.min.x, dest.min.y, dest.max.x,
+ dest.max.y);
+
+ // We don't really know the exact shape of this blob because it may contain
+ // SVG shapes. Also mHitInfo may be a combination of hit info flags from
+ // different shapes so generate an irregular-area hit-test region for it.
+ CompositorHitTestInfo hitInfo = mHitInfo;
+ if (hitInfo.contains(CompositorHitTestFlags::eVisibleToHitTest)) {
+ hitInfo += CompositorHitTestFlags::eIrregularArea;
+ }
+
+ bool backfaceHidden = false;
+ aBuilder.PushHitTest(dest, dest, !backfaceHidden, mScrollId, hitInfo,
+ SideBits::eNone);
+ }
+
+ void PaintItemRange(Grouper* aGrouper, nsDisplayList::iterator aStartItem,
+ nsDisplayList::iterator aEndItem, gfxContext* aContext,
+ WebRenderDrawEventRecorder* aRecorder,
+ RenderRootStateManager* aRootManager,
+ wr::IpcResourceUpdateQueue& aResources) {
+ LayerIntSize size = mVisibleRect.Size();
+ for (auto it = aStartItem; it != aEndItem; ++it) {
+ nsDisplayItem* item = *it;
+ MOZ_ASSERT(item);
+
+ BlobItemData* data = GetBlobItemData(item);
+ if (data->mInvisible) {
+ continue;
+ }
+
+ LayerIntRect bounds = data->mRect;
+ auto bottomRight = bounds.BottomRight();
+
+ GP("Trying %s %p-%d %d %d %d %d\n", item->Name(), item->Frame(),
+ item->GetPerFrameKey(), bounds.x, bounds.y, bounds.XMost(),
+ bounds.YMost());
+
+ if (item->GetType() == DisplayItemType::TYPE_COMPOSITOR_HITTEST_INFO) {
+ continue;
+ }
+
+ GP("paint check invalid %d %d - %d %d\n", bottomRight.x.value,
+ bottomRight.y.value, size.width, size.height);
+ // skip empty items
+ if (bounds.IsEmpty()) {
+ continue;
+ }
+
+ bool dirty = true;
+ auto preservedBounds = bounds.Intersect(mPreservedRect);
+ if (!mInvalidRect.Contains(preservedBounds)) {
+ GP("Passing\n");
+ dirty = false;
+ BlobItemData* data = GetBlobItemData(item);
+ if (data->mInvalid) {
+ gfxCriticalError()
+ << "DisplayItem" << item->Name() << "-should be invalid";
+ }
+ // if the item is invalid it needs to be fully contained
+ MOZ_RELEASE_ASSERT(!data->mInvalid);
+ }
+
+ nsDisplayList* children = item->GetChildren();
+ if (children) {
+ // If we aren't dirty, we still need to iterate over the children to
+ // ensure the blob index data is recorded the same as before to allow
+ // the merging of the parts inside in the invalid rect. Any items that
+ // are painted as a single item need to avoid repainting in that case.
+ GP("doing children in EndGroup\n");
+ aGrouper->PaintContainerItem(this, item, data, bounds.ToUnknownRect(),
+ dirty, children, aContext, aRecorder,
+ aRootManager, aResources);
+ continue;
+ }
+ nsPaintedDisplayItem* paintedItem = item->AsPaintedDisplayItem();
+ if (!paintedItem) {
+ continue;
+ }
+ if (dirty) {
+ // What should the clip settting strategy be? We can set the full
+ // clip everytime. this is probably easiest for now. An alternative
+ // would be to put the push and the pop into separate items and let
+ // invalidation handle it that way.
+ DisplayItemClip currentClip = paintedItem->GetClip();
+
+ if (currentClip.HasClip()) {
+ aContext->Save();
+ currentClip.ApplyTo(aContext, aGrouper->mAppUnitsPerDevPixel);
+ }
+ aContext->NewPath();
+ GP("painting %s %p-%d\n", paintedItem->Name(), paintedItem->Frame(),
+ paintedItem->GetPerFrameKey());
+ if (aGrouper->mDisplayListBuilder->IsPaintingToWindow()) {
+ paintedItem->Frame()->AddStateBits(NS_FRAME_PAINTED_THEBES);
+ }
+
+ paintedItem->Paint(aGrouper->mDisplayListBuilder, aContext);
+ TakeExternalSurfaces(aRecorder, data->mExternalSurfaces, aRootManager,
+ aResources);
+
+ if (currentClip.HasClip()) {
+ aContext->Restore();
+ }
+ }
+ aContext->GetDrawTarget()->FlushItem(bounds.ToUnknownRect());
+ }
+ }
+
+ ~DIGroup() {
+ GP("Group destruct\n");
+ for (BlobItemData* data : mDisplayItems) {
+ GP("Deleting %p-%d\n", data->mFrame, data->mDisplayItemKey);
+ delete data;
+ }
+ }
+};
+
+// If we have an item we need to make sure it matches the current group
+// otherwise it means the item switched groups and we need to invalidate
+// it and recreate the data.
+static BlobItemData* GetBlobItemDataForGroup(nsDisplayItem* aItem,
+ DIGroup* aGroup) {
+ BlobItemData* data = GetBlobItemData(aItem);
+ if (data) {
+ MOZ_RELEASE_ASSERT(data->mGroup->mDisplayItems.Contains(data));
+ if (data->mGroup != aGroup) {
+ GP("group don't match %p %p\n", data->mGroup, aGroup);
+ data->ClearFrame();
+ // the item is for another group
+ // it should be cleared out as being unused at the end of this paint
+ data = nullptr;
+ }
+ }
+ if (!data) {
+ GP("Allocating blob data\n");
+ data = new BlobItemData(aGroup, aItem);
+ aGroup->mDisplayItems.Insert(data);
+ }
+ data->mUsed = true;
+ return data;
+}
+
+void Grouper::PaintContainerItem(DIGroup* aGroup, nsDisplayItem* aItem,
+ BlobItemData* aData,
+ const IntRect& aItemBounds, bool aDirty,
+ nsDisplayList* aChildren, gfxContext* aContext,
+ WebRenderDrawEventRecorder* aRecorder,
+ RenderRootStateManager* aRootManager,
+ wr::IpcResourceUpdateQueue& aResources) {
+ switch (aItem->GetType()) {
+ case DisplayItemType::TYPE_TRANSFORM: {
+ DisplayItemClip currentClip = aItem->GetClip();
+
+ gfxContextMatrixAutoSaveRestore saveMatrix;
+ if (currentClip.HasClip()) {
+ aContext->Save();
+ currentClip.ApplyTo(aContext, this->mAppUnitsPerDevPixel);
+ aContext->GetDrawTarget()->FlushItem(aItemBounds);
+ } else {
+ saveMatrix.SetContext(aContext);
+ }
+
+ auto transformItem = static_cast<nsDisplayTransform*>(aItem);
+ Matrix4x4Flagged trans = transformItem->GetTransform();
+ Matrix trans2d;
+ if (!trans.Is2D(&trans2d)) {
+ // Painting will cause us to include the item's recording in the blob.
+ // We only want to do that if it is dirty, because otherwise the
+ // recording might change (e.g. due to factor of 2 scaling of images
+ // giving different results) and the merging will discard it because it
+ // is outside the invalid rect.
+ if (aDirty) {
+ // We don't currently support doing invalidation inside 3d transforms.
+ // For now just paint it as a single item.
+ aItem->AsPaintedDisplayItem()->Paint(mDisplayListBuilder, aContext);
+ TakeExternalSurfaces(aRecorder, aData->mExternalSurfaces,
+ aRootManager, aResources);
+ }
+ aContext->GetDrawTarget()->FlushItem(aItemBounds);
+ } else if (!trans2d.IsSingular()) {
+ aContext->Multiply(ThebesMatrix(trans2d));
+ aGroup->PaintItemRange(this, aChildren->begin(), aChildren->end(),
+ aContext, aRecorder, aRootManager, aResources);
+ }
+
+ if (currentClip.HasClip()) {
+ aContext->Restore();
+ aContext->GetDrawTarget()->FlushItem(aItemBounds);
+ }
+ break;
+ }
+ case DisplayItemType::TYPE_OPACITY: {
+ auto opacityItem = static_cast<nsDisplayOpacity*>(aItem);
+ float opacity = opacityItem->GetOpacity();
+ if (opacity == 0.0f) {
+ return;
+ }
+
+ aContext->GetDrawTarget()->PushLayer(false, opacityItem->GetOpacity(),
+ nullptr, mozilla::gfx::Matrix(),
+ aItemBounds);
+ GP("beginGroup %s %p-%d\n", aItem->Name(), aItem->Frame(),
+ aItem->GetPerFrameKey());
+ aContext->GetDrawTarget()->FlushItem(aItemBounds);
+ aGroup->PaintItemRange(this, aChildren->begin(), aChildren->end(),
+ aContext, aRecorder, aRootManager, aResources);
+ aContext->GetDrawTarget()->PopLayer();
+ GP("endGroup %s %p-%d\n", aItem->Name(), aItem->Frame(),
+ aItem->GetPerFrameKey());
+ aContext->GetDrawTarget()->FlushItem(aItemBounds);
+ break;
+ }
+ case DisplayItemType::TYPE_BLEND_MODE: {
+ auto blendItem = static_cast<nsDisplayBlendMode*>(aItem);
+ auto blendMode = blendItem->BlendMode();
+ aContext->GetDrawTarget()->PushLayerWithBlend(
+ false, 1.0, nullptr, mozilla::gfx::Matrix(), aItemBounds, false,
+ blendMode);
+ GP("beginGroup %s %p-%d\n", aItem->Name(), aItem->Frame(),
+ aItem->GetPerFrameKey());
+ aContext->GetDrawTarget()->FlushItem(aItemBounds);
+ aGroup->PaintItemRange(this, aChildren->begin(), aChildren->end(),
+ aContext, aRecorder, aRootManager, aResources);
+ aContext->GetDrawTarget()->PopLayer();
+ GP("endGroup %s %p-%d\n", aItem->Name(), aItem->Frame(),
+ aItem->GetPerFrameKey());
+ aContext->GetDrawTarget()->FlushItem(aItemBounds);
+ break;
+ }
+ case DisplayItemType::TYPE_BLEND_CONTAINER: {
+ aContext->GetDrawTarget()->PushLayer(false, 1.0, nullptr,
+ mozilla::gfx::Matrix(), aItemBounds);
+ GP("beginGroup %s %p-%d\n", aItem->Name(), aItem->Frame(),
+ aItem->GetPerFrameKey());
+ aContext->GetDrawTarget()->FlushItem(aItemBounds);
+ aGroup->PaintItemRange(this, aChildren->begin(), aChildren->end(),
+ aContext, aRecorder, aRootManager, aResources);
+ aContext->GetDrawTarget()->PopLayer();
+ GP("endGroup %s %p-%d\n", aItem->Name(), aItem->Frame(),
+ aItem->GetPerFrameKey());
+ aContext->GetDrawTarget()->FlushItem(aItemBounds);
+ break;
+ }
+ case DisplayItemType::TYPE_MASK: {
+ GP("Paint Mask\n");
+ auto maskItem = static_cast<nsDisplayMasksAndClipPaths*>(aItem);
+ if (maskItem->IsValidMask()) {
+ maskItem->PaintWithContentsPaintCallback(
+ mDisplayListBuilder, aContext, [&] {
+ GP("beginGroup %s %p-%d\n", aItem->Name(), aItem->Frame(),
+ aItem->GetPerFrameKey());
+ aContext->GetDrawTarget()->FlushItem(aItemBounds);
+ aGroup->PaintItemRange(this, aChildren->begin(), aChildren->end(),
+ aContext, aRecorder, aRootManager,
+ aResources);
+ GP("endGroup %s %p-%d\n", aItem->Name(), aItem->Frame(),
+ aItem->GetPerFrameKey());
+ });
+ TakeExternalSurfaces(aRecorder, aData->mExternalSurfaces, aRootManager,
+ aResources);
+ aContext->GetDrawTarget()->FlushItem(aItemBounds);
+ }
+ break;
+ }
+ case DisplayItemType::TYPE_FILTER: {
+ GP("Paint Filter\n");
+ // Painting will cause us to include the item's recording in the blob. We
+ // only want to do that if it is dirty, because otherwise the recording
+ // might change (e.g. due to factor of 2 scaling of images giving
+ // different results) and the merging will discard it because it is
+ // outside the invalid rect.
+ if (aDirty) {
+ auto filterItem = static_cast<nsDisplayFilters*>(aItem);
+
+ nsRegion visible(aItem->GetClippedBounds(mDisplayListBuilder));
+ nsRect buildingRect = aItem->GetBuildingRect();
+ visible.And(visible, buildingRect);
+
+ filterItem->Paint(mDisplayListBuilder, aContext);
+ TakeExternalSurfaces(aRecorder, aData->mExternalSurfaces, aRootManager,
+ aResources);
+ }
+ aContext->GetDrawTarget()->FlushItem(aItemBounds);
+ break;
+ }
+
+ default:
+ aGroup->PaintItemRange(this, aChildren->begin(), aChildren->end(),
+ aContext, aRecorder, aRootManager, aResources);
+ break;
+ }
+}
+
+class WebRenderGroupData : public WebRenderUserData {
+ public:
+ WebRenderGroupData(RenderRootStateManager* aWRManager, nsDisplayItem* aItem);
+ virtual ~WebRenderGroupData();
+
+ WebRenderGroupData* AsGroupData() override { return this; }
+ UserDataType GetType() override { return UserDataType::eGroup; }
+ static UserDataType Type() { return UserDataType::eGroup; }
+
+ DIGroup mSubGroup;
+ DIGroup mFollowingGroup;
+};
+
+enum class ItemActivity : uint8_t {
+ /// Item must not be active.
+ No = 0,
+ /// Could be active if it has no layerization cost.
+ /// Typically active if first of an item group.
+ Could = 1,
+ /// Should be active unless something external makes that less useful.
+ /// For example if the item is affected by a complex mask, it remains
+ /// inactive.
+ Should = 2,
+ /// Must be active regardless of external factors.
+ Must = 3,
+};
+
+ItemActivity CombineActivity(ItemActivity a, ItemActivity b) {
+ return a > b ? a : b;
+}
+
+bool ActivityAtLeast(ItemActivity rhs, ItemActivity atLeast) {
+ return rhs >= atLeast;
+}
+
+static ItemActivity IsItemProbablyActive(
+ nsDisplayItem* aItem, mozilla::wr::DisplayListBuilder& aBuilder,
+ mozilla::wr::IpcResourceUpdateQueue& aResources,
+ const mozilla::layers::StackingContextHelper& aSc,
+ mozilla::layers::RenderRootStateManager* aManager,
+ nsDisplayListBuilder* aDisplayListBuilder, bool aSiblingActive,
+ bool aUniformlyScaled);
+
+static ItemActivity HasActiveChildren(
+ const nsDisplayList& aList, mozilla::wr::DisplayListBuilder& aBuilder,
+ mozilla::wr::IpcResourceUpdateQueue& aResources,
+ const mozilla::layers::StackingContextHelper& aSc,
+ mozilla::layers::RenderRootStateManager* aManager,
+ nsDisplayListBuilder* aDisplayListBuilder, bool aUniformlyScaled) {
+ ItemActivity activity = ItemActivity::No;
+ for (nsDisplayItem* item : aList) {
+ // Here we only want to know if a child must be active, so we don't specify
+ // when the item is first or last, which can cause an item that could be
+ // either decide to be active. This is a bit conservative and avoids some
+ // extra layers. It's a good tradeoff until we get to the point where most
+ // items could have been active but none *had* to. Right now this is
+ // unlikely but as more svg items get webrenderized it will be better to
+ // make them active more aggressively.
+ auto childActivity =
+ IsItemProbablyActive(item, aBuilder, aResources, aSc, aManager,
+ aDisplayListBuilder, false, aUniformlyScaled);
+ activity = CombineActivity(activity, childActivity);
+ if (activity == ItemActivity::Must) {
+ return activity;
+ }
+ }
+ return activity;
+}
+
+static ItemActivity AssessBounds(const StackingContextHelper& aSc,
+ nsDisplayListBuilder* aDisplayListBuilder,
+ nsDisplayItem* aItem,
+ bool aHasActivePrecedingSibling) {
+ // Arbitrary threshold up for adjustments. What we want to avoid here
+ // is alternating between active and non active items and create a lot
+ // of overlapping blobs, so we only make images active if they are
+ // costly enough that it's worth the risk of having more layers. As we
+ // move more blob items into wr display items it will become less of a
+ // concern.
+ constexpr float largeish = 512;
+
+ bool snap = false;
+ nsRect bounds = aItem->GetBounds(aDisplayListBuilder, &snap);
+
+ float appUnitsPerDevPixel =
+ static_cast<float>(aItem->Frame()->PresContext()->AppUnitsPerDevPixel());
+
+ float width =
+ static_cast<float>(bounds.width) * aSc.GetInheritedScale().xScale;
+ float height =
+ static_cast<float>(bounds.height) * aSc.GetInheritedScale().yScale;
+
+ // Webrender doesn't handle primitives smaller than a pixel well, so
+ // avoid making them active.
+ if (width >= appUnitsPerDevPixel && height >= appUnitsPerDevPixel) {
+ if (aHasActivePrecedingSibling || width > largeish || height > largeish) {
+ return ItemActivity::Should;
+ }
+
+ return ItemActivity::Could;
+ }
+
+ return ItemActivity::No;
+}
+
+// This function decides whether we want to treat this item as "active", which
+// means that it's a container item which we will turn into a WebRender
+// StackingContext, or whether we treat it as "inactive" and include it inside
+// the parent blob image.
+//
+// We can't easily use GetLayerState because it wants a bunch of layers related
+// information.
+static ItemActivity IsItemProbablyActive(
+ nsDisplayItem* aItem, mozilla::wr::DisplayListBuilder& aBuilder,
+ mozilla::wr::IpcResourceUpdateQueue& aResources,
+ const mozilla::layers::StackingContextHelper& aSc,
+ mozilla::layers::RenderRootStateManager* aManager,
+ nsDisplayListBuilder* aDisplayListBuilder, bool aHasActivePrecedingSibling,
+ bool aUniformlyScaled) {
+ switch (aItem->GetType()) {
+ case DisplayItemType::TYPE_TRANSFORM: {
+ nsDisplayTransform* transformItem =
+ static_cast<nsDisplayTransform*>(aItem);
+ const Matrix4x4Flagged& t = transformItem->GetTransform();
+ Matrix t2d;
+ bool is2D = t.Is2D(&t2d);
+ if (!is2D) {
+ return ItemActivity::Must;
+ }
+
+ auto activity = HasActiveChildren(*transformItem->GetChildren(), aBuilder,
+ aResources, aSc, aManager,
+ aDisplayListBuilder, aUniformlyScaled);
+
+ if (transformItem->MayBeAnimated(aDisplayListBuilder)) {
+ activity = CombineActivity(activity, ItemActivity::Should);
+ }
+
+ return activity;
+ }
+ case DisplayItemType::TYPE_OPACITY: {
+ nsDisplayOpacity* opacityItem = static_cast<nsDisplayOpacity*>(aItem);
+ if (opacityItem->NeedsActiveLayer(aDisplayListBuilder,
+ opacityItem->Frame())) {
+ return ItemActivity::Must;
+ }
+ return HasActiveChildren(*opacityItem->GetChildren(), aBuilder,
+ aResources, aSc, aManager, aDisplayListBuilder,
+ aUniformlyScaled);
+ }
+ case DisplayItemType::TYPE_FOREIGN_OBJECT: {
+ return ItemActivity::Must;
+ }
+ case DisplayItemType::TYPE_SVG_GEOMETRY: {
+ auto* svgItem = static_cast<DisplaySVGGeometry*>(aItem);
+ if (StaticPrefs::gfx_webrender_svg_shapes() && aUniformlyScaled &&
+ svgItem->ShouldBeActive(aBuilder, aResources, aSc, aManager,
+ aDisplayListBuilder)) {
+ return AssessBounds(aSc, aDisplayListBuilder, aItem,
+ aHasActivePrecedingSibling);
+ }
+
+ return ItemActivity::No;
+ }
+ case DisplayItemType::TYPE_SVG_IMAGE: {
+ auto* svgItem = static_cast<DisplaySVGImage*>(aItem);
+ if (StaticPrefs::gfx_webrender_svg_images() && aUniformlyScaled &&
+ svgItem->ShouldBeActive(aBuilder, aResources, aSc, aManager,
+ aDisplayListBuilder)) {
+ return AssessBounds(aSc, aDisplayListBuilder, aItem,
+ aHasActivePrecedingSibling);
+ }
+
+ return ItemActivity::No;
+ }
+ case DisplayItemType::TYPE_BLEND_MODE: {
+ /* BLEND_MODE needs to be active if it might have a previous sibling
+ * that is active so that it's able to blend with that content. */
+ if (aHasActivePrecedingSibling) {
+ return ItemActivity::Must;
+ }
+
+ return HasActiveChildren(*aItem->GetChildren(), aBuilder, aResources, aSc,
+ aManager, aDisplayListBuilder, aUniformlyScaled);
+ }
+ case DisplayItemType::TYPE_MASK: {
+ if (aItem->GetChildren()) {
+ auto activity =
+ HasActiveChildren(*aItem->GetChildren(), aBuilder, aResources, aSc,
+ aManager, aDisplayListBuilder, aUniformlyScaled);
+ // For masked items, don't bother with making children active since we
+ // are going to have to need to paint and upload a large mask anyway.
+ if (activity < ItemActivity::Must) {
+ return ItemActivity::No;
+ }
+ return activity;
+ }
+ return ItemActivity::No;
+ }
+ case DisplayItemType::TYPE_WRAP_LIST:
+ case DisplayItemType::TYPE_CONTAINER:
+ case DisplayItemType::TYPE_PERSPECTIVE: {
+ if (aItem->GetChildren()) {
+ return HasActiveChildren(*aItem->GetChildren(), aBuilder, aResources,
+ aSc, aManager, aDisplayListBuilder,
+ aUniformlyScaled);
+ }
+ return ItemActivity::No;
+ }
+ case DisplayItemType::TYPE_FILTER: {
+ nsDisplayFilters* filters = static_cast<nsDisplayFilters*>(aItem);
+ if (filters->CanCreateWebRenderCommands()) {
+ // Items are usually expensive enough on the CPU that we want to
+ // make them active whenever we can.
+ return ItemActivity::Must;
+ }
+ return ItemActivity::No;
+ }
+ default:
+ // TODO: handle other items?
+ return ItemActivity::No;
+ }
+}
+
+// This does a pass over the display lists and will join the display items
+// into groups as well as paint them
+void Grouper::ConstructGroups(nsDisplayListBuilder* aDisplayListBuilder,
+ WebRenderCommandBuilder* aCommandBuilder,
+ wr::DisplayListBuilder& aBuilder,
+ wr::IpcResourceUpdateQueue& aResources,
+ DIGroup* aGroup, nsDisplayList* aList,
+ nsDisplayItem* aWrappingItem,
+ const StackingContextHelper& aSc) {
+ RenderRootStateManager* manager =
+ aCommandBuilder->mManager->GetRenderRootStateManager();
+
+ nsDisplayList::iterator startOfCurrentGroup = aList->end();
+ DIGroup* currentGroup = aGroup;
+
+ // We need to track whether we have active siblings for mixed blend mode.
+ bool encounteredActiveItem = false;
+ bool isFirstGroup = true;
+ // Track whether the item is the first (visible) of its group in which case
+ // making it active won't add extra layers.
+ bool isFirst = true;
+
+ for (auto it = aList->begin(); it != aList->end(); ++it) {
+ nsDisplayItem* item = *it;
+ MOZ_ASSERT(item);
+
+ if (item->HasHitTestInfo()) {
+ // Accumulate the hit-test info flags. In cases where there are multiple
+ // hittest-info display items with different flags, mHitInfo will have
+ // the union of all those flags. If that is the case, we will
+ // additionally set eIrregularArea (at the site that we use mHitInfo)
+ // so that downstream consumers of this (primarily APZ) will know that
+ // the exact shape of what gets hit with what is unknown.
+ currentGroup->mHitInfo += item->GetHitTestInfo().Info();
+ }
+
+ if (startOfCurrentGroup == aList->end()) {
+ startOfCurrentGroup = it;
+ if (!isFirstGroup) {
+ mClipManager.SwitchItem(aDisplayListBuilder, aWrappingItem);
+ }
+ }
+
+ bool isLast = it.HasNext();
+
+ // WebRender's anti-aliasing approximation is not very good under
+ // non-uniform scales.
+ bool uniformlyScaled =
+ fabs(aGroup->mScale.xScale - aGroup->mScale.yScale) < 0.1;
+
+ auto activity = IsItemProbablyActive(
+ item, aBuilder, aResources, aSc, manager, mDisplayListBuilder,
+ encounteredActiveItem, uniformlyScaled);
+ auto threshold =
+ isFirst || isLast ? ItemActivity::Could : ItemActivity::Should;
+
+ if (activity >= threshold) {
+ encounteredActiveItem = true;
+ // We're going to be starting a new group.
+ RefPtr<WebRenderGroupData> groupData =
+ aCommandBuilder->CreateOrRecycleWebRenderUserData<WebRenderGroupData>(
+ item);
+
+ groupData->mFollowingGroup.mInvalidRect.SetEmpty();
+
+ // Initialize groupData->mFollowingGroup with data from currentGroup.
+ // We want to copy out this information before calling EndGroup because
+ // EndGroup will set mLastVisibleRect depending on whether
+ // we send something to WebRender.
+
+ // TODO: compute the group bounds post-grouping, so that they can be
+ // tighter for just the sublist that made it into this group.
+ // We want to ensure the tight bounds are still clipped by area
+ // that we're building the display list for.
+ if (groupData->mFollowingGroup.mScale != currentGroup->mScale ||
+ groupData->mFollowingGroup.mAppUnitsPerDevPixel !=
+ currentGroup->mAppUnitsPerDevPixel ||
+ groupData->mFollowingGroup.mResidualOffset !=
+ currentGroup->mResidualOffset) {
+ if (groupData->mFollowingGroup.mAppUnitsPerDevPixel !=
+ currentGroup->mAppUnitsPerDevPixel) {
+ GP("app unit change following: %d %d\n",
+ groupData->mFollowingGroup.mAppUnitsPerDevPixel,
+ currentGroup->mAppUnitsPerDevPixel);
+ }
+ // The group changed size
+ GP("Inner group size change\n");
+ groupData->mFollowingGroup.ClearItems();
+ groupData->mFollowingGroup.ClearImageKey(
+ aCommandBuilder->mManager->GetRenderRootStateManager());
+ }
+ groupData->mFollowingGroup.mAppUnitsPerDevPixel =
+ currentGroup->mAppUnitsPerDevPixel;
+ groupData->mFollowingGroup.mLayerBounds = currentGroup->mLayerBounds;
+ groupData->mFollowingGroup.mClippedImageBounds =
+ currentGroup->mClippedImageBounds;
+ groupData->mFollowingGroup.mScale = currentGroup->mScale;
+ groupData->mFollowingGroup.mResidualOffset =
+ currentGroup->mResidualOffset;
+ groupData->mFollowingGroup.mVisibleRect = currentGroup->mVisibleRect;
+ groupData->mFollowingGroup.mPreservedRect =
+ groupData->mFollowingGroup.mVisibleRect.Intersect(
+ groupData->mFollowingGroup.mLastVisibleRect);
+ groupData->mFollowingGroup.mActualBounds = LayerIntRect();
+ groupData->mFollowingGroup.mHitTestBounds = LayerIntRect();
+ groupData->mFollowingGroup.mHitInfo = currentGroup->mHitInfo;
+
+ currentGroup->EndGroup(aCommandBuilder->mManager, aDisplayListBuilder,
+ aBuilder, aResources, this, startOfCurrentGroup,
+ it);
+
+ {
+ auto spaceAndClipChain =
+ mClipManager.SwitchItem(aDisplayListBuilder, item);
+ wr::SpaceAndClipChainHelper saccHelper(aBuilder, spaceAndClipChain);
+ bool hasHitTest = mHitTestInfoManager.ProcessItem(item, aBuilder,
+ aDisplayListBuilder);
+ // XXX - This is hacky. Some items have hit testing info on them but we
+ // also have dedicated hit testing items, the flags of which apply to
+ // the the group that contains them. We don't want layerization to
+ // affect that so if the item didn't emit any hit testing then we still
+ // push a hit test item if the previous group had some hit test flags
+ // set. This is obviously not great. Hit testing should be independent
+ // from how we layerize.
+ if (!hasHitTest &&
+ currentGroup->mHitInfo != gfx::CompositorHitTestInvisibleToHit) {
+ auto hitTestRect = item->GetBuildingRect();
+ if (!hitTestRect.IsEmpty()) {
+ currentGroup->PushHitTest(
+ aBuilder, LayoutDeviceRect::FromAppUnits(
+ hitTestRect, currentGroup->mAppUnitsPerDevPixel));
+ }
+ }
+
+ sIndent++;
+ // Note: this call to CreateWebRenderCommands can recurse back into
+ // this function.
+ bool createdWRCommands = item->CreateWebRenderCommands(
+ aBuilder, aResources, aSc, manager, mDisplayListBuilder);
+ MOZ_RELEASE_ASSERT(
+ createdWRCommands,
+ "active transforms should always succeed at creating "
+ "WebRender commands");
+ sIndent--;
+ }
+
+ isFirstGroup = false;
+ startOfCurrentGroup = aList->end();
+ currentGroup = &groupData->mFollowingGroup;
+ isFirst = true;
+ } else { // inactive item
+ bool isInvisible = false;
+ ConstructItemInsideInactive(aCommandBuilder, aBuilder, aResources,
+ currentGroup, item, aSc, &isInvisible);
+ if (!isInvisible) {
+ // Invisible items don't count.
+ isFirst = false;
+ }
+ }
+ }
+
+ currentGroup->EndGroup(aCommandBuilder->mManager, aDisplayListBuilder,
+ aBuilder, aResources, this, startOfCurrentGroup,
+ aList->end());
+}
+
+// This does a pass over the display lists and will join the display items
+// into a single group.
+bool Grouper::ConstructGroupInsideInactive(
+ WebRenderCommandBuilder* aCommandBuilder, wr::DisplayListBuilder& aBuilder,
+ wr::IpcResourceUpdateQueue& aResources, DIGroup* aGroup,
+ nsDisplayList* aList, const StackingContextHelper& aSc) {
+ bool invalidated = false;
+ for (nsDisplayItem* item : *aList) {
+ if (item->HasHitTestInfo()) {
+ // Accumulate the hit-test info flags. In cases where there are multiple
+ // hittest-info display items with different flags, mHitInfo will have
+ // the union of all those flags. If that is the case, we will
+ // additionally set eIrregularArea (at the site that we use mHitInfo)
+ // so that downstream consumers of this (primarily APZ) will know that
+ // the exact shape of what gets hit with what is unknown.
+ aGroup->mHitInfo += item->GetHitTestInfo().Info();
+ }
+
+ bool invisible = false;
+ invalidated |= ConstructItemInsideInactive(
+ aCommandBuilder, aBuilder, aResources, aGroup, item, aSc, &invisible);
+ }
+ return invalidated;
+}
+
+bool Grouper::ConstructItemInsideInactive(
+ WebRenderCommandBuilder* aCommandBuilder, wr::DisplayListBuilder& aBuilder,
+ wr::IpcResourceUpdateQueue& aResources, DIGroup* aGroup,
+ nsDisplayItem* aItem, const StackingContextHelper& aSc,
+ bool* aOutIsInvisible) {
+ nsDisplayList* children = aItem->GetChildren();
+ BlobItemData* data = GetBlobItemDataForGroup(aItem, aGroup);
+
+ /* mInvalid unfortunately persists across paints. Clear it so that if we don't
+ * set it to 'true' we ensure that we're not using the value from the last
+ * time that we painted */
+ data->mInvalid = false;
+ data->mInvisible = aItem->IsInvisible();
+ *aOutIsInvisible = data->mInvisible;
+
+ // we compute the geometry change here because we have the transform around
+ // still
+ bool invalidated = aGroup->ComputeGeometryChange(aItem, data, mTransform,
+ mDisplayListBuilder);
+
+ // Temporarily restrict the image bounds to the bounds of the container so
+ // that clipped children within the container know about the clip. This
+ // ensures that the bounds passed to FlushItem are contained in the bounds of
+ // the clip so that we don't include items in the recording without including
+ // their corresponding clipping items.
+ auto oldClippedImageBounds = aGroup->mClippedImageBounds;
+ aGroup->mClippedImageBounds =
+ aGroup->mClippedImageBounds.Intersect(data->mRect);
+
+ if (aItem->GetType() == DisplayItemType::TYPE_FILTER) {
+ // If ConstructGroupInsideInactive finds any change, we invalidate the
+ // entire container item. This is needed because blob merging requires the
+ // entire item to be within the invalid region.
+ Matrix m = mTransform;
+ mTransform = Matrix();
+ sIndent++;
+ if (ConstructGroupInsideInactive(aCommandBuilder, aBuilder, aResources,
+ aGroup, children, aSc)) {
+ data->mInvalid = true;
+ aGroup->InvalidateRect(data->mRect);
+ invalidated = true;
+ }
+ sIndent--;
+ mTransform = m;
+ } else if (aItem->GetType() == DisplayItemType::TYPE_TRANSFORM) {
+ Matrix m = mTransform;
+ nsDisplayTransform* transformItem = static_cast<nsDisplayTransform*>(aItem);
+ const Matrix4x4Flagged& t = transformItem->GetTransform();
+ Matrix t2d;
+ bool is2D = t.CanDraw2D(&t2d);
+ if (!is2D) {
+ // If ConstructGroupInsideInactive finds any change, we invalidate the
+ // entire container item. This is needed because blob merging requires the
+ // entire item to be within the invalid region.
+ mTransform = Matrix();
+ sIndent++;
+ if (ConstructGroupInsideInactive(aCommandBuilder, aBuilder, aResources,
+ aGroup, children, aSc)) {
+ data->mInvalid = true;
+ aGroup->InvalidateRect(data->mRect);
+ invalidated = true;
+ }
+ sIndent--;
+ } else {
+ GP("t2d: %f %f\n", t2d._31, t2d._32);
+ mTransform.PreMultiply(t2d);
+ GP("mTransform: %f %f\n", mTransform._31, mTransform._32);
+ sIndent++;
+ invalidated |= ConstructGroupInsideInactive(
+ aCommandBuilder, aBuilder, aResources, aGroup, children, aSc);
+ sIndent--;
+ }
+ mTransform = m;
+ } else if (children) {
+ sIndent++;
+ invalidated |= ConstructGroupInsideInactive(
+ aCommandBuilder, aBuilder, aResources, aGroup, children, aSc);
+ sIndent--;
+ }
+
+ GP("Including %s of %d\n", aItem->Name(), aGroup->mDisplayItems.Count());
+ aGroup->mClippedImageBounds = oldClippedImageBounds;
+ return invalidated;
+}
+
+/* This is just a copy of nsRect::ScaleToOutsidePixels with an offset added in.
+ * The offset is applied just before the rounding. It's in the scaled space. */
+static mozilla::LayerIntRect ScaleToOutsidePixelsOffset(
+ nsRect aRect, float aXScale, float aYScale, nscoord aAppUnitsPerPixel,
+ LayerPoint aOffset) {
+ mozilla::LayerIntRect rect;
+ rect.SetNonEmptyBox(
+ NSToIntFloor(NSAppUnitsToFloatPixels(aRect.x, float(aAppUnitsPerPixel)) *
+ aXScale +
+ aOffset.x),
+ NSToIntFloor(NSAppUnitsToFloatPixels(aRect.y, float(aAppUnitsPerPixel)) *
+ aYScale +
+ aOffset.y),
+ NSToIntCeil(
+ NSAppUnitsToFloatPixels(aRect.XMost(), float(aAppUnitsPerPixel)) *
+ aXScale +
+ aOffset.x),
+ NSToIntCeil(
+ NSAppUnitsToFloatPixels(aRect.YMost(), float(aAppUnitsPerPixel)) *
+ aYScale +
+ aOffset.y));
+ return rect;
+}
+
+/* This function is the same as the above except that it rounds to the
+ * nearest instead of rounding out. We use it for attempting to compute the
+ * actual pixel bounds of opaque items */
+static mozilla::gfx::IntRect ScaleToNearestPixelsOffset(
+ nsRect aRect, float aXScale, float aYScale, nscoord aAppUnitsPerPixel,
+ LayerPoint aOffset) {
+ mozilla::gfx::IntRect rect;
+ rect.SetNonEmptyBox(
+ NSToIntFloor(NSAppUnitsToFloatPixels(aRect.x, float(aAppUnitsPerPixel)) *
+ aXScale +
+ aOffset.x + 0.5),
+ NSToIntFloor(NSAppUnitsToFloatPixels(aRect.y, float(aAppUnitsPerPixel)) *
+ aYScale +
+ aOffset.y + 0.5),
+ NSToIntFloor(
+ NSAppUnitsToFloatPixels(aRect.XMost(), float(aAppUnitsPerPixel)) *
+ aXScale +
+ aOffset.x + 0.5),
+ NSToIntFloor(
+ NSAppUnitsToFloatPixels(aRect.YMost(), float(aAppUnitsPerPixel)) *
+ aYScale +
+ aOffset.y + 0.5));
+ return rect;
+}
+
+RenderRootStateManager* WebRenderCommandBuilder::GetRenderRootStateManager() {
+ return mManager->GetRenderRootStateManager();
+}
+
+void WebRenderCommandBuilder::DoGroupingForDisplayList(
+ nsDisplayList* aList, nsDisplayItem* aWrappingItem,
+ nsDisplayListBuilder* aDisplayListBuilder, const StackingContextHelper& aSc,
+ wr::DisplayListBuilder& aBuilder, wr::IpcResourceUpdateQueue& aResources) {
+ if (!aList->GetBottom()) {
+ return;
+ }
+
+ GP("DoGroupingForDisplayList\n");
+
+ mClipManager.BeginList(aSc);
+ mHitTestInfoManager.Reset();
+ Grouper g(mClipManager);
+
+ int32_t appUnitsPerDevPixel =
+ aWrappingItem->Frame()->PresContext()->AppUnitsPerDevPixel();
+
+ g.mDisplayListBuilder = aDisplayListBuilder;
+ RefPtr<WebRenderGroupData> groupData =
+ CreateOrRecycleWebRenderUserData<WebRenderGroupData>(aWrappingItem);
+
+ bool snapped;
+ nsRect groupBounds =
+ aWrappingItem->GetUntransformedBounds(aDisplayListBuilder, &snapped);
+ DIGroup& group = groupData->mSubGroup;
+
+ auto scale = aSc.GetInheritedScale();
+ GP("Inherited scale %f %f\n", scale.xScale, scale.yScale);
+
+ auto trans =
+ ViewAs<LayerPixel>(aSc.GetSnappingSurfaceTransform().GetTranslation());
+ auto snappedTrans = LayerIntPoint::Floor(trans);
+ LayerPoint residualOffset = trans - snappedTrans;
+
+ auto layerBounds =
+ ScaleToOutsidePixelsOffset(groupBounds, scale.xScale, scale.yScale,
+ appUnitsPerDevPixel, residualOffset);
+
+ const nsRect& untransformedPaintRect =
+ aWrappingItem->GetUntransformedPaintRect();
+
+ auto visibleRect = ScaleToOutsidePixelsOffset(
+ untransformedPaintRect, scale.xScale, scale.yScale,
+ appUnitsPerDevPixel, residualOffset)
+ .Intersect(layerBounds);
+
+ GP("LayerBounds: %d %d %d %d\n", layerBounds.x, layerBounds.y,
+ layerBounds.width, layerBounds.height);
+ GP("VisibleRect: %d %d %d %d\n", visibleRect.x, visibleRect.y,
+ visibleRect.width, visibleRect.height);
+
+ GP("Inherited scale %f %f\n", scale.xScale, scale.yScale);
+
+ group.mInvalidRect.SetEmpty();
+ if (group.mAppUnitsPerDevPixel != appUnitsPerDevPixel ||
+ group.mScale != scale || group.mResidualOffset != residualOffset) {
+ GP("Property change. Deleting blob\n");
+
+ if (group.mAppUnitsPerDevPixel != appUnitsPerDevPixel) {
+ GP(" App unit change %d -> %d\n", group.mAppUnitsPerDevPixel,
+ appUnitsPerDevPixel);
+ }
+
+ if (group.mScale != scale) {
+ GP(" Scale %f %f -> %f %f\n", group.mScale.xScale, group.mScale.yScale,
+ scale.xScale, scale.yScale);
+ }
+
+ if (group.mResidualOffset != residualOffset) {
+ GP(" Residual Offset %f %f -> %f %f\n", group.mResidualOffset.x.value,
+ group.mResidualOffset.y.value, residualOffset.x.value,
+ residualOffset.y.value);
+ }
+
+ group.ClearItems();
+ group.ClearImageKey(mManager->GetRenderRootStateManager());
+ }
+
+ ScrollableLayerGuid::ViewID scrollId = ScrollableLayerGuid::NULL_SCROLL_ID;
+ if (const ActiveScrolledRoot* asr = aWrappingItem->GetActiveScrolledRoot()) {
+ scrollId = asr->GetViewId();
+ }
+
+ g.mAppUnitsPerDevPixel = appUnitsPerDevPixel;
+ group.mResidualOffset = residualOffset;
+ group.mLayerBounds = layerBounds;
+ group.mVisibleRect = visibleRect;
+ group.mActualBounds = LayerIntRect();
+ group.mHitTestBounds = LayerIntRect();
+ group.mPreservedRect = group.mVisibleRect.Intersect(group.mLastVisibleRect);
+ group.mAppUnitsPerDevPixel = appUnitsPerDevPixel;
+ group.mClippedImageBounds = layerBounds;
+
+ g.mTransform =
+ Matrix::Scaling(scale).PostTranslate(residualOffset.x, residualOffset.y);
+ group.mScale = scale;
+ group.mScrollId = scrollId;
+ g.ConstructGroups(aDisplayListBuilder, this, aBuilder, aResources, &group,
+ aList, aWrappingItem, aSc);
+ mClipManager.EndList(aSc);
+}
+
+WebRenderCommandBuilder::WebRenderCommandBuilder(
+ WebRenderLayerManager* aManager)
+ : mManager(aManager),
+ mLastAsr(nullptr),
+ mBuilderDumpIndex(0),
+ mDumpIndent(0),
+ mDoGrouping(false),
+ mContainsSVGGroup(false) {}
+
+void WebRenderCommandBuilder::Destroy() {
+ mLastCanvasDatas.Clear();
+ ClearCachedResources();
+}
+
+void WebRenderCommandBuilder::EmptyTransaction() {
+ // We need to update canvases that might have changed.
+ for (RefPtr<WebRenderCanvasData> canvasData : mLastCanvasDatas) {
+ WebRenderCanvasRendererAsync* canvas = canvasData->GetCanvasRenderer();
+ if (canvas) {
+ canvas->UpdateCompositableClientForEmptyTransaction();
+ }
+ }
+}
+
+bool WebRenderCommandBuilder::NeedsEmptyTransaction() {
+ return !mLastCanvasDatas.IsEmpty();
+}
+
+void WebRenderCommandBuilder::BuildWebRenderCommands(
+ wr::DisplayListBuilder& aBuilder,
+ wr::IpcResourceUpdateQueue& aResourceUpdates, nsDisplayList* aDisplayList,
+ nsDisplayListBuilder* aDisplayListBuilder, WebRenderScrollData& aScrollData,
+ WrFiltersHolder&& aFilters) {
+ AUTO_PROFILER_LABEL_CATEGORY_PAIR(GRAPHICS_WRDisplayList);
+
+ StackingContextHelper sc;
+ aScrollData = WebRenderScrollData(mManager, aDisplayListBuilder);
+ MOZ_ASSERT(mLayerScrollData.empty());
+ mClipManager.BeginBuild(mManager, aBuilder);
+ mHitTestInfoManager.Reset();
+
+ mBuilderDumpIndex = 0;
+ mLastCanvasDatas.Clear();
+ mLastAsr = nullptr;
+ mContainsSVGGroup = false;
+ MOZ_ASSERT(mDumpIndent == 0);
+
+ {
+ wr::StackingContextParams params;
+ params.mRootReferenceFrame = aDisplayListBuilder->RootReferenceFrame();
+ params.mFilters = std::move(aFilters.filters);
+ params.mFilterDatas = std::move(aFilters.filter_datas);
+ params.clip =
+ wr::WrStackingContextClip::ClipChain(aBuilder.CurrentClipChainId());
+
+ StackingContextHelper pageRootSc(sc, nullptr, nullptr, nullptr, aBuilder,
+ params);
+ if (ShouldDumpDisplayList(aDisplayListBuilder)) {
+ mBuilderDumpIndex =
+ aBuilder.Dump(mDumpIndent + 1, Some(mBuilderDumpIndex), Nothing());
+ }
+ CreateWebRenderCommandsFromDisplayList(aDisplayList, nullptr,
+ aDisplayListBuilder, pageRootSc,
+ aBuilder, aResourceUpdates);
+ }
+
+ // Make a "root" layer data that has everything else as descendants
+ mLayerScrollData.emplace_back();
+ mLayerScrollData.back().InitializeRoot(mLayerScrollData.size() - 1);
+ auto callback =
+ [&aScrollData](ScrollableLayerGuid::ViewID aScrollId) -> bool {
+ return aScrollData.HasMetadataFor(aScrollId).isSome();
+ };
+ Maybe<ScrollMetadata> rootMetadata =
+ nsLayoutUtils::GetRootMetadata(aDisplayListBuilder, mManager, callback);
+ if (rootMetadata) {
+ // Put the fallback root metadata on the rootmost layer that is
+ // a matching async zoom container, or the root layer that we just
+ // created above.
+ size_t rootMetadataTarget = mLayerScrollData.size() - 1;
+ for (size_t i = rootMetadataTarget; i > 0; i--) {
+ if (auto zoomContainerId =
+ mLayerScrollData[i - 1].GetAsyncZoomContainerId()) {
+ if (*zoomContainerId == rootMetadata->GetMetrics().GetScrollId()) {
+ rootMetadataTarget = i - 1;
+ break;
+ }
+ }
+ }
+ mLayerScrollData[rootMetadataTarget].AppendScrollMetadata(
+ aScrollData, rootMetadata.ref());
+ }
+
+ // Append the WebRenderLayerScrollData items into WebRenderScrollData
+ // in reverse order, from topmost to bottommost. This is in keeping with
+ // the semantics of WebRenderScrollData.
+ for (auto it = mLayerScrollData.rbegin(); it != mLayerScrollData.rend();
+ it++) {
+ aScrollData.AddLayerData(std::move(*it));
+ }
+ mLayerScrollData.clear();
+ mClipManager.EndBuild();
+
+ // Remove the user data those are not displayed on the screen and
+ // also reset the data to unused for next transaction.
+ RemoveUnusedAndResetWebRenderUserData();
+}
+
+bool WebRenderCommandBuilder::ShouldDumpDisplayList(
+ nsDisplayListBuilder* aBuilder) {
+ return aBuilder && aBuilder->IsInActiveDocShell() &&
+ ((XRE_IsParentProcess() &&
+ StaticPrefs::gfx_webrender_debug_dl_dump_parent()) ||
+ (XRE_IsContentProcess() &&
+ StaticPrefs::gfx_webrender_debug_dl_dump_content()));
+}
+
+void WebRenderCommandBuilder::CreateWebRenderCommands(
+ nsDisplayItem* aItem, mozilla::wr::DisplayListBuilder& aBuilder,
+ mozilla::wr::IpcResourceUpdateQueue& aResources,
+ const StackingContextHelper& aSc,
+ nsDisplayListBuilder* aDisplayListBuilder) {
+ mHitTestInfoManager.ProcessItem(aItem, aBuilder, aDisplayListBuilder);
+ if (aItem->GetType() == DisplayItemType::TYPE_COMPOSITOR_HITTEST_INFO) {
+ // The hit test information was processed above.
+ return;
+ }
+
+ auto* item = aItem->AsPaintedDisplayItem();
+ MOZ_RELEASE_ASSERT(item, "Tried to paint item that cannot be painted");
+
+ if (aBuilder.ReuseItem(item)) {
+ // No further processing should be needed, since the item was reused.
+ return;
+ }
+
+ RenderRootStateManager* manager = mManager->GetRenderRootStateManager();
+
+ // Note: this call to CreateWebRenderCommands can recurse back into
+ // this function if the |item| is a wrapper for a sublist.
+ const bool createdWRCommands = aItem->CreateWebRenderCommands(
+ aBuilder, aResources, aSc, manager, aDisplayListBuilder);
+
+ if (!createdWRCommands) {
+ PushItemAsImage(aItem, aBuilder, aResources, aSc, aDisplayListBuilder);
+ }
+}
+
+// A helper struct to store information needed when creating a new
+// WebRenderLayerScrollData in CreateWebRenderCommandsFromDisplayList().
+// This information is gathered before the recursion, and then used to
+// emit the new layer after the recursion.
+struct NewLayerData {
+ size_t mLayerCountBeforeRecursing = 0;
+ const ActiveScrolledRoot* mStopAtAsr = nullptr;
+
+ // Information pertaining to the deferred transform.
+ nsDisplayTransform* mDeferredItem = nullptr;
+ ScrollableLayerGuid::ViewID mDeferredId = ScrollableLayerGuid::NULL_SCROLL_ID;
+ bool mTransformShouldGetOwnLayer = false;
+
+ void ComputeDeferredTransformInfo(
+ const StackingContextHelper& aSc, nsDisplayItem* aItem,
+ nsDisplayTransform* aLastDeferredTransform) {
+ // See the comments on StackingContextHelper::mDeferredTransformItem
+ // for an overview of what deferred transforms are.
+ // In the case where we deferred a transform, but have a child display
+ // item with a different ASR than the deferred transform item, we cannot
+ // put the transform on the WebRenderLayerScrollData item for the child.
+ // We cannot do this because it will not conform to APZ's expectations
+ // with respect to how the APZ tree ends up structured. In particular,
+ // the GetTransformToThis() for the child APZ (which is created for the
+ // child item's ASR) will not include the transform when we actually do
+ // want it to.
+ // When we run into this scenario, we solve it by creating two
+ // WebRenderLayerScrollData items; one that just holds the transform,
+ // that we deferred, and a child WebRenderLayerScrollData item that
+ // holds the scroll metadata for the child's ASR.
+ mDeferredItem = aSc.GetDeferredTransformItem();
+ // If this deferred transform is already slated to be emitted onto an
+ // ancestor layer, do not emit it on this layer as well. Note that it's
+ // sufficient to check the most recently deferred item here, because
+ // there's only one per stacking context, and we emit it when changing
+ // stacking contexts.
+ if (mDeferredItem == aLastDeferredTransform) {
+ mDeferredItem = nullptr;
+ }
+ if (mDeferredItem) {
+ // It's possible the transform's ASR is not only an ancestor of
+ // the item's ASR, but an ancestor of stopAtAsr. In such cases,
+ // don't use the transform at all at this level (it would be
+ // scrolled by stopAtAsr which is incorrect). The transform will
+ // instead be emitted as part of the ancestor WebRenderLayerScrollData
+ // node (the one with stopAtAsr as its item ASR), or one of its
+ // ancetors in turn.
+ if (ActiveScrolledRoot::IsProperAncestor(
+ mDeferredItem->GetActiveScrolledRoot(), mStopAtAsr)) {
+ mDeferredItem = nullptr;
+ }
+ }
+ if (mDeferredItem) {
+ if (const auto* asr = mDeferredItem->GetActiveScrolledRoot()) {
+ mDeferredId = asr->GetViewId();
+ }
+ if (mDeferredItem->GetActiveScrolledRoot() !=
+ aItem->GetActiveScrolledRoot()) {
+ mTransformShouldGetOwnLayer = true;
+ } else if (aItem->GetType() == DisplayItemType::TYPE_SCROLL_INFO_LAYER) {
+ // A scroll info layer has its own scroll id that's not reflected
+ // in item->GetActiveScrolledRoot(), but will be added to the
+ // WebRenderLayerScrollData node, so it needs to be treated as
+ // having a distinct ASR from the deferred transform item.
+ mTransformShouldGetOwnLayer = true;
+ }
+ }
+ }
+};
+
+void WebRenderCommandBuilder::CreateWebRenderCommandsFromDisplayList(
+ nsDisplayList* aDisplayList, nsDisplayItem* aWrappingItem,
+ nsDisplayListBuilder* aDisplayListBuilder, const StackingContextHelper& aSc,
+ wr::DisplayListBuilder& aBuilder, wr::IpcResourceUpdateQueue& aResources,
+ bool aNewClipList) {
+ if (mDoGrouping) {
+ MOZ_RELEASE_ASSERT(
+ aWrappingItem,
+ "Only the root list should have a null wrapping item, and mDoGrouping "
+ "should never be true for the root list.");
+ GP("actually entering the grouping code\n");
+ DoGroupingForDisplayList(aDisplayList, aWrappingItem, aDisplayListBuilder,
+ aSc, aBuilder, aResources);
+ return;
+ }
+
+ bool dumpEnabled = ShouldDumpDisplayList(aDisplayListBuilder);
+ if (dumpEnabled) {
+ // If we're inside a nested display list, print the WR DL items from the
+ // wrapper item before we start processing the nested items.
+ mBuilderDumpIndex =
+ aBuilder.Dump(mDumpIndent + 1, Some(mBuilderDumpIndex), Nothing());
+ }
+
+ FlattenedDisplayListIterator iter(aDisplayListBuilder, aDisplayList);
+ if (!iter.HasNext()) {
+ return;
+ }
+
+ mDumpIndent++;
+ if (aNewClipList) {
+ mClipManager.BeginList(aSc);
+ }
+
+ const bool apzEnabled = mManager->AsyncPanZoomEnabled();
+ do {
+ nsDisplayItem* item = iter.GetNextItem();
+
+ DisplayItemType itemType = item->GetType();
+
+ // If this is a new (not retained/reused) item, then we need to disable
+ // the display item cache for descendants, since it's possible that some of
+ // them got cached with a flattened opacity values., which may no longer be
+ // applied.
+ Maybe<AutoDisplayItemCacheSuppressor> cacheSuppressor;
+
+ if (itemType == DisplayItemType::TYPE_OPACITY) {
+ nsDisplayOpacity* opacity = static_cast<nsDisplayOpacity*>(item);
+
+ if (!opacity->IsReused()) {
+ cacheSuppressor.emplace(aBuilder.GetDisplayItemCache());
+ }
+
+ if (opacity->CanApplyOpacityToChildren(
+ mManager->GetRenderRootStateManager()->LayerManager(),
+ aDisplayListBuilder, aBuilder.GetInheritedOpacity())) {
+ // If all our children support handling the opacity directly, then push
+ // the opacity and clip onto the builder and skip creating a stacking
+ // context.
+ float oldOpacity = aBuilder.GetInheritedOpacity();
+ const DisplayItemClipChain* oldClip = aBuilder.GetInheritedClipChain();
+ aBuilder.SetInheritedOpacity(oldOpacity * opacity->GetOpacity());
+ aBuilder.PushInheritedClipChain(aDisplayListBuilder,
+ opacity->GetClipChain());
+
+ CreateWebRenderCommandsFromDisplayList(opacity->GetChildren(), item,
+ aDisplayListBuilder, aSc,
+ aBuilder, aResources, false);
+
+ aBuilder.SetInheritedOpacity(oldOpacity);
+ aBuilder.SetInheritedClipChain(oldClip);
+ continue;
+ }
+ }
+
+ // If this is an unscrolled background color item, in the root display list
+ // for the parent process, consider doing opaque checks.
+ if (XRE_IsParentProcess() && !aWrappingItem &&
+ itemType == DisplayItemType::TYPE_BACKGROUND_COLOR &&
+ !item->GetActiveScrolledRoot() &&
+ item->GetClip().GetRoundedRectCount() == 0) {
+ bool snap;
+ nsRegion opaque = item->GetOpaqueRegion(aDisplayListBuilder, &snap);
+ if (opaque.GetNumRects() == 1) {
+ nsRect clippedOpaque =
+ item->GetClip().ApplyNonRoundedIntersection(opaque.GetBounds());
+ if (!clippedOpaque.IsEmpty()) {
+ aDisplayListBuilder->AddWindowOpaqueRegion(item->Frame(),
+ clippedOpaque);
+ }
+ }
+ }
+
+ Maybe<NewLayerData> newLayerData;
+ if (apzEnabled) {
+ // For some types of display items we want to force a new
+ // WebRenderLayerScrollData object, to ensure we preserve the APZ-relevant
+ // data that is in the display item.
+ if (item->UpdateScrollData(nullptr, nullptr)) {
+ newLayerData = Some(NewLayerData());
+ }
+
+ // Anytime the ASR changes we also want to force a new layer data because
+ // the stack of scroll metadata is going to be different for this
+ // display item than previously, so we can't squash the display items
+ // into the same "layer".
+ const ActiveScrolledRoot* asr = item->GetActiveScrolledRoot();
+ if (asr != mLastAsr) {
+ mLastAsr = asr;
+ newLayerData = Some(NewLayerData());
+ }
+
+ // Refer to the comment on StackingContextHelper::mDeferredTransformItem
+ // for an overview of what this is about. This bit of code applies to the
+ // case where we are deferring a transform item, and we then need to defer
+ // another transform with a different ASR. In such a case we cannot just
+ // merge the deferred transforms, but need to force a new
+ // WebRenderLayerScrollData item to flush the old deferred transform, so
+ // that we can then start deferring the new one.
+ if (!newLayerData && item->CreatesStackingContextHelper() &&
+ aSc.GetDeferredTransformItem() &&
+ aSc.GetDeferredTransformItem()->GetActiveScrolledRoot() != asr) {
+ newLayerData = Some(NewLayerData());
+ }
+
+ // If we're going to create a new layer data for this item, stash the
+ // ASR so that if we recurse into a sublist they will know where to stop
+ // walking up their ASR chain when building scroll metadata.
+ if (newLayerData) {
+ newLayerData->mLayerCountBeforeRecursing = mLayerScrollData.size();
+ newLayerData->mStopAtAsr =
+ mAsrStack.empty() ? nullptr : mAsrStack.back();
+ newLayerData->ComputeDeferredTransformInfo(
+ aSc, item,
+ mDeferredTransformStack.empty() ? nullptr
+ : mDeferredTransformStack.back());
+
+ // Ensure our children's |stopAtAsr| is not be an ancestor of our
+ // |stopAtAsr|, otherwise we could get cyclic scroll metadata
+ // annotations.
+ const ActiveScrolledRoot* stopAtAsrForChildren =
+ ActiveScrolledRoot::PickDescendant(asr, newLayerData->mStopAtAsr);
+ // Additionally, while unusual and probably indicative of a poorly
+ // behaved display list, it's possible to have a deferred transform item
+ // which we will emit as its own layer on the way out of the recursion,
+ // whose ASR (let's call it T) is a *descendant* of the current item's
+ // ASR. In such cases, make sure our children have stopAtAsr=T,
+ // otherwise ASRs in the range [T, asr) may be emitted in duplicate,
+ // leading again to cylic scroll metadata annotations.
+ if (newLayerData->mTransformShouldGetOwnLayer) {
+ stopAtAsrForChildren = ActiveScrolledRoot::PickDescendant(
+ stopAtAsrForChildren,
+ newLayerData->mDeferredItem->GetActiveScrolledRoot());
+ }
+ mAsrStack.push_back(stopAtAsrForChildren);
+
+ // If we're going to emit a deferred transform onto this layer,
+ // keep track of that so descendant layers know not to emit the
+ // same deferred transform.
+ if (newLayerData->mDeferredItem) {
+ mDeferredTransformStack.push_back(newLayerData->mDeferredItem);
+ }
+ }
+ }
+
+ // This is where we emulate the clip/scroll stack that was previously
+ // implemented on the WR display list side.
+ auto spaceAndClipChain = mClipManager.SwitchItem(aDisplayListBuilder, item);
+ wr::SpaceAndClipChainHelper saccHelper(aBuilder, spaceAndClipChain);
+
+ { // scope restoreDoGrouping
+ AutoRestore<bool> restoreDoGrouping(mDoGrouping);
+ if (itemType == DisplayItemType::TYPE_SVG_WRAPPER) {
+ // Inside an <svg>, all display items that are not LAYER_ACTIVE wrapper
+ // display items (like animated transforms / opacity) share the same
+ // animated geometry root, so we can combine subsequent items of that
+ // type into the same image.
+ mContainsSVGGroup = mDoGrouping = true;
+ GP("attempting to enter the grouping code\n");
+ }
+
+ if (dumpEnabled) {
+ std::stringstream ss;
+ nsIFrame::PrintDisplayItem(aDisplayListBuilder, item, ss,
+ static_cast<uint32_t>(mDumpIndent));
+ printf_stderr("%s", ss.str().c_str());
+ }
+
+ CreateWebRenderCommands(item, aBuilder, aResources, aSc,
+ aDisplayListBuilder);
+
+ if (dumpEnabled) {
+ mBuilderDumpIndex =
+ aBuilder.Dump(mDumpIndent + 1, Some(mBuilderDumpIndex), Nothing());
+ }
+ }
+
+ if (apzEnabled) {
+ if (newLayerData) {
+ // Pop the thing we pushed before the recursion, so the topmost item on
+ // the stack is enclosing display item's ASR (or the stack is empty)
+ mAsrStack.pop_back();
+
+ if (newLayerData->mDeferredItem) {
+ MOZ_ASSERT(!mDeferredTransformStack.empty());
+ mDeferredTransformStack.pop_back();
+ }
+
+ const ActiveScrolledRoot* stopAtAsr = newLayerData->mStopAtAsr;
+
+ int32_t descendants =
+ mLayerScrollData.size() - newLayerData->mLayerCountBeforeRecursing;
+
+ nsDisplayTransform* deferred = newLayerData->mDeferredItem;
+ ScrollableLayerGuid::ViewID deferredId = newLayerData->mDeferredId;
+
+ if (newLayerData->mTransformShouldGetOwnLayer) {
+ // This creates the child WebRenderLayerScrollData for |item|, but
+ // omits the transform (hence the Nothing() as the last argument to
+ // Initialize(...)). We also need to make sure that the ASR from
+ // the deferred transform item is not on this node, so we use that
+ // ASR as the "stop at" ASR for this WebRenderLayerScrollData.
+ mLayerScrollData.emplace_back();
+ mLayerScrollData.back().Initialize(
+ mManager->GetScrollData(), item, descendants,
+ deferred->GetActiveScrolledRoot(), Nothing(),
+ ScrollableLayerGuid::NULL_SCROLL_ID);
+
+ // The above WebRenderLayerScrollData will also be a descendant of
+ // the transform-holding WebRenderLayerScrollData we create below.
+ descendants++;
+
+ // This creates the WebRenderLayerScrollData for the deferred
+ // transform item. This holds the transform matrix and the remaining
+ // ASRs needed to complete the ASR chain (i.e. the ones from the
+ // stopAtAsr down to the deferred transform item's ASR, which must be
+ // "between" stopAtAsr and |item|'s ASR in the ASR tree).
+ mLayerScrollData.emplace_back();
+ mLayerScrollData.back().Initialize(
+ mManager->GetScrollData(), deferred, descendants, stopAtAsr,
+ aSc.GetDeferredTransformMatrix(), deferredId);
+ } else {
+ // This is the "simple" case where we don't need to create two
+ // WebRenderLayerScrollData items; we can just create one that also
+ // holds the deferred transform matrix, if any.
+ mLayerScrollData.emplace_back();
+ mLayerScrollData.back().Initialize(
+ mManager->GetScrollData(), item, descendants, stopAtAsr,
+ deferred ? aSc.GetDeferredTransformMatrix() : Nothing(),
+ deferredId);
+ }
+ }
+ }
+ } while (iter.HasNext());
+
+ mDumpIndent--;
+ if (aNewClipList) {
+ mClipManager.EndList(aSc);
+ }
+}
+
+void WebRenderCommandBuilder::PushOverrideForASR(
+ const ActiveScrolledRoot* aASR, const wr::WrSpatialId& aSpatialId) {
+ mClipManager.PushOverrideForASR(aASR, aSpatialId);
+}
+
+void WebRenderCommandBuilder::PopOverrideForASR(
+ const ActiveScrolledRoot* aASR) {
+ mClipManager.PopOverrideForASR(aASR);
+}
+
+Maybe<wr::ImageKey> WebRenderCommandBuilder::CreateImageKey(
+ nsDisplayItem* aItem, ImageContainer* aContainer,
+ mozilla::wr::DisplayListBuilder& aBuilder,
+ mozilla::wr::IpcResourceUpdateQueue& aResources,
+ mozilla::wr::ImageRendering aRendering, const StackingContextHelper& aSc,
+ gfx::IntSize& aSize, const Maybe<LayoutDeviceRect>& aAsyncImageBounds) {
+ RefPtr<WebRenderImageData> imageData =
+ CreateOrRecycleWebRenderUserData<WebRenderImageData>(aItem);
+ MOZ_ASSERT(imageData);
+
+ if (aContainer->IsAsync()) {
+ MOZ_ASSERT(aAsyncImageBounds);
+
+ LayoutDeviceRect rect = aAsyncImageBounds.value();
+ LayoutDeviceRect scBounds(LayoutDevicePoint(0, 0), rect.Size());
+ // TODO!
+ // We appear to be using the image bridge for a lot (most/all?) of
+ // layers-free image handling and that breaks frame consistency.
+ imageData->CreateAsyncImageWebRenderCommands(
+ aBuilder, aContainer, aSc, rect, scBounds, aContainer->GetRotation(),
+ aRendering, wr::MixBlendMode::Normal, !aItem->BackfaceIsHidden());
+ return Nothing();
+ }
+
+ AutoLockImage autoLock(aContainer);
+ if (!autoLock.HasImage()) {
+ return Nothing();
+ }
+ mozilla::layers::Image* image = autoLock.GetImage();
+ aSize = image->GetSize();
+
+ return imageData->UpdateImageKey(aContainer, aResources);
+}
+
+bool WebRenderCommandBuilder::PushImage(
+ nsDisplayItem* aItem, ImageContainer* aContainer,
+ mozilla::wr::DisplayListBuilder& aBuilder,
+ mozilla::wr::IpcResourceUpdateQueue& aResources,
+ const StackingContextHelper& aSc, const LayoutDeviceRect& aRect,
+ const LayoutDeviceRect& aClip) {
+ auto rendering = wr::ToImageRendering(aItem->Frame()->UsedImageRendering());
+ gfx::IntSize size;
+ Maybe<wr::ImageKey> key =
+ CreateImageKey(aItem, aContainer, aBuilder, aResources, rendering, aSc,
+ size, Some(aRect));
+ if (aContainer->IsAsync()) {
+ // Async ImageContainer does not create ImageKey, instead it uses Pipeline.
+ MOZ_ASSERT(key.isNothing());
+ return true;
+ }
+ if (!key) {
+ return false;
+ }
+
+ auto r = wr::ToLayoutRect(aRect);
+ auto c = wr::ToLayoutRect(aClip);
+ aBuilder.PushImage(r, c, !aItem->BackfaceIsHidden(), false, rendering,
+ key.value());
+
+ return true;
+}
+
+Maybe<wr::ImageKey> WebRenderCommandBuilder::CreateImageProviderKey(
+ nsDisplayItem* aItem, image::WebRenderImageProvider* aProvider,
+ image::ImgDrawResult aDrawResult,
+ mozilla::wr::IpcResourceUpdateQueue& aResources) {
+ RefPtr<WebRenderImageProviderData> imageData =
+ CreateOrRecycleWebRenderUserData<WebRenderImageProviderData>(aItem);
+ MOZ_ASSERT(imageData);
+ return imageData->UpdateImageKey(aProvider, aDrawResult, aResources);
+}
+
+bool WebRenderCommandBuilder::PushImageProvider(
+ nsDisplayItem* aItem, image::WebRenderImageProvider* aProvider,
+ image::ImgDrawResult aDrawResult, mozilla::wr::DisplayListBuilder& aBuilder,
+ mozilla::wr::IpcResourceUpdateQueue& aResources,
+ const LayoutDeviceRect& aRect, const LayoutDeviceRect& aClip) {
+ Maybe<wr::ImageKey> key =
+ CreateImageProviderKey(aItem, aProvider, aDrawResult, aResources);
+ if (!key) {
+ return false;
+ }
+
+ bool antialiased = aItem->GetType() == DisplayItemType::TYPE_SVG_GEOMETRY;
+
+ auto rendering = wr::ToImageRendering(aItem->Frame()->UsedImageRendering());
+ auto r = wr::ToLayoutRect(aRect);
+ auto c = wr::ToLayoutRect(aClip);
+ aBuilder.PushImage(r, c, !aItem->BackfaceIsHidden(), antialiased, rendering,
+ key.value());
+
+ return true;
+}
+
+static void PaintItemByDrawTarget(nsDisplayItem* aItem, gfx::DrawTarget* aDT,
+ const LayoutDevicePoint& aOffset,
+ const IntRect& visibleRect,
+ nsDisplayListBuilder* aDisplayListBuilder,
+ const gfx::MatrixScales& aScale,
+ Maybe<gfx::DeviceColor>& aHighlight) {
+ MOZ_ASSERT(aDT && aDT->IsValid());
+
+ // XXX Why is this ClearRect() needed?
+ aDT->ClearRect(Rect(visibleRect));
+ gfxContext context(aDT);
+
+ switch (aItem->GetType()) {
+ case DisplayItemType::TYPE_SVG_WRAPPER:
+ case DisplayItemType::TYPE_MASK: {
+ // These items should be handled by other code paths
+ MOZ_RELEASE_ASSERT(0);
+ break;
+ }
+ default:
+ if (!aItem->AsPaintedDisplayItem()) {
+ break;
+ }
+
+ context.SetMatrix(context.CurrentMatrix().PreScale(aScale).PreTranslate(
+ -aOffset.x, -aOffset.y));
+ if (aDisplayListBuilder->IsPaintingToWindow()) {
+ aItem->Frame()->AddStateBits(NS_FRAME_PAINTED_THEBES);
+ }
+ aItem->AsPaintedDisplayItem()->Paint(aDisplayListBuilder, &context);
+ break;
+ }
+
+ if (aHighlight && aItem->GetType() != DisplayItemType::TYPE_MASK) {
+ // Apply highlight fills, if the appropriate prefs are set.
+ // We don't do this for masks because we'd be filling the A8 mask surface,
+ // which isn't very useful.
+ aDT->SetTransform(gfx::Matrix());
+ aDT->FillRect(Rect(visibleRect), gfx::ColorPattern(aHighlight.value()));
+ }
+}
+
+bool WebRenderCommandBuilder::ComputeInvalidationForDisplayItem(
+ nsDisplayListBuilder* aBuilder, const nsPoint& aShift,
+ nsDisplayItem* aItem) {
+ RefPtr<WebRenderFallbackData> fallbackData =
+ CreateOrRecycleWebRenderUserData<WebRenderFallbackData>(aItem);
+
+ nsRect invalid;
+ if (!fallbackData->mGeometry || aItem->IsInvalid(invalid)) {
+ fallbackData->mGeometry = WrapUnique(aItem->AllocateGeometry(aBuilder));
+ return true;
+ }
+
+ fallbackData->mGeometry->MoveBy(aShift);
+ nsRegion combined;
+ aItem->ComputeInvalidationRegion(aBuilder, fallbackData->mGeometry.get(),
+ &combined);
+
+ UniquePtr<nsDisplayItemGeometry> geometry;
+ if (!combined.IsEmpty() || aItem->NeedsGeometryUpdates()) {
+ geometry = WrapUnique(aItem->AllocateGeometry(aBuilder));
+ }
+
+ fallbackData->mClip.AddOffsetAndComputeDifference(
+ aShift, fallbackData->mGeometry->ComputeInvalidationRegion(),
+ aItem->GetClip(),
+ geometry ? geometry->ComputeInvalidationRegion()
+ : fallbackData->mGeometry->ComputeInvalidationRegion(),
+ &combined);
+
+ if (geometry) {
+ fallbackData->mGeometry = std::move(geometry);
+ }
+ fallbackData->mClip = aItem->GetClip();
+
+ if (!combined.IsEmpty()) {
+ return true;
+ } else if (aItem->GetChildren()) {
+ return ComputeInvalidationForDisplayList(aBuilder, aShift,
+ aItem->GetChildren());
+ }
+ return false;
+}
+
+bool WebRenderCommandBuilder::ComputeInvalidationForDisplayList(
+ nsDisplayListBuilder* aBuilder, const nsPoint& aShift,
+ nsDisplayList* aList) {
+ FlattenedDisplayListIterator iter(aBuilder, aList);
+ while (iter.HasNext()) {
+ if (ComputeInvalidationForDisplayItem(aBuilder, aShift,
+ iter.GetNextItem())) {
+ return true;
+ }
+ }
+ return false;
+}
+
+// When drawing fallback images we create either
+// a real image or a blob image that will contain the display item.
+// In the case of a blob image we paint the item at 0,0 instead
+// of trying to keep at aItem->GetBounds().TopLeft() like we do
+// with SVG. We do this because there's not necessarily a reference frame
+// between us and the rest of the world so the the coordinates
+// that we get for the bounds are not necessarily stable across scrolling
+// or other movement.
+already_AddRefed<WebRenderFallbackData>
+WebRenderCommandBuilder::GenerateFallbackData(
+ nsDisplayItem* aItem, wr::DisplayListBuilder& aBuilder,
+ wr::IpcResourceUpdateQueue& aResources, const StackingContextHelper& aSc,
+ nsDisplayListBuilder* aDisplayListBuilder, LayoutDeviceRect& aImageRect) {
+ const bool paintOnContentSide = aItem->MustPaintOnContentSide();
+ bool useBlobImage =
+ StaticPrefs::gfx_webrender_blob_images() && !paintOnContentSide;
+ Maybe<gfx::DeviceColor> highlight = Nothing();
+ if (StaticPrefs::gfx_webrender_debug_highlight_painted_layers()) {
+ highlight = Some(useBlobImage ? gfx::DeviceColor(1.0, 0.0, 0.0, 0.5)
+ : gfx::DeviceColor(1.0, 1.0, 0.0, 0.5));
+ }
+
+ RefPtr<WebRenderFallbackData> fallbackData =
+ CreateOrRecycleWebRenderUserData<WebRenderFallbackData>(aItem);
+
+ bool snap;
+ nsRect itemBounds = aItem->GetBounds(aDisplayListBuilder, &snap);
+
+ // Blob images will only draw the visible area of the blob so we don't need to
+ // clip them here and can just rely on the webrender clipping.
+ // TODO We also don't clip native themed widget to avoid over-invalidation
+ // during scrolling. It would be better to support a sort of streaming/tiling
+ // scheme for large ones but the hope is that we should not have large native
+ // themed items.
+ nsRect paintBounds = (useBlobImage || paintOnContentSide)
+ ? itemBounds
+ : aItem->GetClippedBounds(aDisplayListBuilder);
+
+ nsRect buildingRect = aItem->GetBuildingRect();
+
+ const int32_t appUnitsPerDevPixel =
+ aItem->Frame()->PresContext()->AppUnitsPerDevPixel();
+ auto bounds =
+ LayoutDeviceRect::FromAppUnits(paintBounds, appUnitsPerDevPixel);
+ if (bounds.IsEmpty()) {
+ return nullptr;
+ }
+
+ MatrixScales scale = aSc.GetInheritedScale();
+ MatrixScales oldScale = fallbackData->mScale;
+ // We tolerate slight changes in scale so that we don't, for example,
+ // rerasterize on MotionMark
+ bool differentScale = gfx::FuzzyEqual(scale.xScale, oldScale.xScale, 1e-6f) &&
+ gfx::FuzzyEqual(scale.yScale, oldScale.yScale, 1e-6f);
+
+ auto layerScale = LayoutDeviceToLayerScale2D::FromUnknownScale(scale);
+
+ auto trans =
+ ViewAs<LayerPixel>(aSc.GetSnappingSurfaceTransform().GetTranslation());
+
+ if (!FitsInt32(trans.X()) || !FitsInt32(trans.Y())) {
+ // The translation overflowed int32_t.
+ return nullptr;
+ }
+
+ auto snappedTrans = LayerIntPoint::Floor(trans);
+ LayerPoint residualOffset = trans - snappedTrans;
+
+ nsRegion opaqueRegion = aItem->GetOpaqueRegion(aDisplayListBuilder, &snap);
+ wr::OpacityType opacity = opaqueRegion.Contains(paintBounds)
+ ? wr::OpacityType::Opaque
+ : wr::OpacityType::HasAlphaChannel;
+
+ LayerIntRect dtRect, visibleRect;
+ // If we think the item is opaque we round the bounds
+ // to the nearest pixel instead of rounding them out. If we rounded
+ // out we'd potentially introduce transparent pixels.
+ //
+ // Ideally we'd be able to ask an item its bounds in pixels and whether
+ // they're all opaque. Unfortunately no such API exists so we currently
+ // just hope that we get it right.
+ if (aBuilder.GetInheritedOpacity() == 1.0f &&
+ opacity == wr::OpacityType::Opaque && snap) {
+ dtRect = LayerIntRect::FromUnknownRect(
+ ScaleToNearestPixelsOffset(paintBounds, scale.xScale, scale.yScale,
+ appUnitsPerDevPixel, residualOffset));
+
+ visibleRect =
+ LayerIntRect::FromUnknownRect(
+ ScaleToNearestPixelsOffset(buildingRect, scale.xScale, scale.yScale,
+ appUnitsPerDevPixel, residualOffset))
+ .Intersect(dtRect);
+ } else {
+ dtRect = ScaleToOutsidePixelsOffset(paintBounds, scale.xScale, scale.yScale,
+ appUnitsPerDevPixel, residualOffset);
+
+ visibleRect =
+ ScaleToOutsidePixelsOffset(buildingRect, scale.xScale, scale.yScale,
+ appUnitsPerDevPixel, residualOffset)
+ .Intersect(dtRect);
+ }
+
+ auto visibleSize = visibleRect.Size();
+ // these rectangles can overflow from scaling so try to
+ // catch that with IsEmpty() checks. See bug 1622126.
+ if (visibleSize.IsEmpty() || dtRect.IsEmpty()) {
+ return nullptr;
+ }
+
+ if (useBlobImage) {
+ // Display item bounds should be unscaled
+ aImageRect = visibleRect / layerScale;
+ } else {
+ // Display item bounds should be unscaled
+ aImageRect = dtRect / layerScale;
+ }
+
+ // We always paint items at 0,0 so the visibleRect that we use inside the blob
+ // is needs to be adjusted by the display item bounds top left.
+ visibleRect -= dtRect.TopLeft();
+
+ nsDisplayItemGeometry* geometry = fallbackData->mGeometry.get();
+
+ bool needPaint = true;
+
+ MOZ_RELEASE_ASSERT(aItem->GetType() != DisplayItemType::TYPE_SVG_WRAPPER);
+ if (geometry && !fallbackData->IsInvalid() &&
+ aItem->GetType() != DisplayItemType::TYPE_SVG_WRAPPER && differentScale) {
+ nsRect invalid;
+ if (!aItem->IsInvalid(invalid)) {
+ nsPoint shift = itemBounds.TopLeft() - geometry->mBounds.TopLeft();
+ geometry->MoveBy(shift);
+
+ nsRegion invalidRegion;
+ aItem->ComputeInvalidationRegion(aDisplayListBuilder, geometry,
+ &invalidRegion);
+
+ nsRect lastBounds = fallbackData->mBounds;
+ lastBounds.MoveBy(shift);
+
+ if (lastBounds.IsEqualInterior(paintBounds) && invalidRegion.IsEmpty() &&
+ aBuilder.GetInheritedOpacity() == fallbackData->mOpacity) {
+ if (aItem->GetType() == DisplayItemType::TYPE_FILTER) {
+ needPaint = ComputeInvalidationForDisplayList(
+ aDisplayListBuilder, shift, aItem->GetChildren());
+ if (!buildingRect.IsEqualInterior(fallbackData->mBuildingRect)) {
+ needPaint = true;
+ }
+ } else {
+ needPaint = false;
+ }
+ }
+ }
+ }
+
+ if (needPaint || !fallbackData->GetImageKey()) {
+ fallbackData->mGeometry =
+ WrapUnique(aItem->AllocateGeometry(aDisplayListBuilder));
+
+ gfx::SurfaceFormat format = aItem->GetType() == DisplayItemType::TYPE_MASK
+ ? gfx::SurfaceFormat::A8
+ : (opacity == wr::OpacityType::Opaque
+ ? gfx::SurfaceFormat::B8G8R8X8
+ : gfx::SurfaceFormat::B8G8R8A8);
+ if (useBlobImage) {
+ MOZ_ASSERT(!opaqueRegion.IsComplex());
+
+ std::vector<RefPtr<ScaledFont>> fonts;
+ bool validFonts = true;
+ RefPtr<WebRenderDrawEventRecorder> recorder =
+ MakeAndAddRef<WebRenderDrawEventRecorder>(
+ [&](MemStream& aStream,
+ std::vector<RefPtr<ScaledFont>>& aScaledFonts) {
+ size_t count = aScaledFonts.size();
+ aStream.write((const char*)&count, sizeof(count));
+ for (auto& scaled : aScaledFonts) {
+ Maybe<wr::FontInstanceKey> key =
+ mManager->WrBridge()->GetFontKeyForScaledFont(scaled,
+ aResources);
+ if (key.isNothing()) {
+ validFonts = false;
+ break;
+ }
+ BlobFont font = {key.value(), scaled};
+ aStream.write((const char*)&font, sizeof(font));
+ }
+ fonts = std::move(aScaledFonts);
+ });
+ RefPtr<gfx::DrawTarget> dummyDt = gfx::Factory::CreateDrawTarget(
+ gfx::BackendType::SKIA, gfx::IntSize(1, 1), format);
+ RefPtr<gfx::DrawTarget> dt = gfx::Factory::CreateRecordingDrawTarget(
+ recorder, dummyDt, (dtRect - dtRect.TopLeft()).ToUnknownRect());
+ if (aBuilder.GetInheritedOpacity() != 1.0f) {
+ dt->PushLayer(false, aBuilder.GetInheritedOpacity(), nullptr,
+ gfx::Matrix());
+ }
+ PaintItemByDrawTarget(aItem, dt, (dtRect / layerScale).TopLeft(),
+ /*aVisibleRect: */ dt->GetRect(),
+ aDisplayListBuilder, scale, highlight);
+ if (aBuilder.GetInheritedOpacity() != 1.0f) {
+ dt->PopLayer();
+ }
+
+ // the item bounds are relative to the blob origin which is
+ // dtRect.TopLeft()
+ recorder->FlushItem((dtRect - dtRect.TopLeft()).ToUnknownRect());
+ recorder->Finish();
+
+ if (!validFonts) {
+ gfxCriticalNote << "Failed serializing fonts for blob image";
+ return nullptr;
+ }
+
+ Range<uint8_t> bytes((uint8_t*)recorder->mOutputStream.mData,
+ recorder->mOutputStream.mLength);
+ wr::BlobImageKey key =
+ wr::BlobImageKey{mManager->WrBridge()->GetNextImageKey()};
+ wr::ImageDescriptor descriptor(visibleSize.ToUnknownSize(), 0,
+ dt->GetFormat(), opacity);
+ if (!aResources.AddBlobImage(
+ key, descriptor, bytes,
+ ViewAs<ImagePixel>(visibleRect,
+ PixelCastJustification::LayerIsImage))) {
+ return nullptr;
+ }
+ TakeExternalSurfaces(recorder, fallbackData->mExternalSurfaces,
+ mManager->GetRenderRootStateManager(), aResources);
+ fallbackData->SetBlobImageKey(key);
+ fallbackData->SetFonts(fonts);
+ } else {
+ WebRenderImageData* imageData = fallbackData->PaintIntoImage();
+
+ imageData->CreateImageClientIfNeeded();
+ RefPtr<ImageClient> imageClient = imageData->GetImageClient();
+ RefPtr<ImageContainer> imageContainer = MakeAndAddRef<ImageContainer>();
+
+ {
+ UpdateImageHelper helper(imageContainer, imageClient,
+ dtRect.Size().ToUnknownSize(), format);
+ {
+ RefPtr<gfx::DrawTarget> dt = helper.GetDrawTarget();
+ if (!dt) {
+ return nullptr;
+ }
+ if (aBuilder.GetInheritedOpacity() != 1.0f) {
+ dt->PushLayer(false, aBuilder.GetInheritedOpacity(), nullptr,
+ gfx::Matrix());
+ }
+ PaintItemByDrawTarget(aItem, dt,
+ /*aOffset: */ aImageRect.TopLeft(),
+ /*aVisibleRect: */ dt->GetRect(),
+ aDisplayListBuilder, scale, highlight);
+ if (aBuilder.GetInheritedOpacity() != 1.0f) {
+ dt->PopLayer();
+ }
+ }
+
+ // Update image if there it's invalidated.
+ if (!helper.UpdateImage()) {
+ return nullptr;
+ }
+ }
+
+ // Force update the key in fallback data since we repaint the image in
+ // this path. If not force update, fallbackData may reuse the original key
+ // because it doesn't know UpdateImageHelper already updated the image
+ // container.
+ if (!imageData->UpdateImageKey(imageContainer, aResources, true)) {
+ return nullptr;
+ }
+ }
+
+ fallbackData->mScale = scale;
+ fallbackData->mOpacity = aBuilder.GetInheritedOpacity();
+ fallbackData->SetInvalid(false);
+ }
+
+ if (useBlobImage) {
+ MOZ_DIAGNOSTIC_ASSERT(mManager->WrBridge()->MatchesNamespace(
+ fallbackData->GetBlobImageKey().ref()),
+ "Stale blob key for fallback!");
+
+ aResources.SetBlobImageVisibleArea(
+ fallbackData->GetBlobImageKey().value(),
+ ViewAs<ImagePixel>(visibleRect, PixelCastJustification::LayerIsImage));
+ }
+
+ // Update current bounds to fallback data
+ fallbackData->mBounds = paintBounds;
+ fallbackData->mBuildingRect = buildingRect;
+
+ MOZ_ASSERT(fallbackData->GetImageKey());
+
+ return fallbackData.forget();
+}
+
+void WebRenderMaskData::ClearImageKey() {
+ if (mBlobKey) {
+ mManager->AddBlobImageKeyForDiscard(mBlobKey.value());
+ }
+ mBlobKey.reset();
+}
+
+void WebRenderMaskData::Invalidate() {
+ mMaskStyle = nsStyleImageLayers(nsStyleImageLayers::LayerType::Mask);
+}
+
+Maybe<wr::ImageMask> WebRenderCommandBuilder::BuildWrMaskImage(
+ nsDisplayMasksAndClipPaths* aMaskItem, wr::DisplayListBuilder& aBuilder,
+ wr::IpcResourceUpdateQueue& aResources, const StackingContextHelper& aSc,
+ nsDisplayListBuilder* aDisplayListBuilder,
+ const LayoutDeviceRect& aBounds) {
+ RefPtr<WebRenderMaskData> maskData =
+ CreateOrRecycleWebRenderUserData<WebRenderMaskData>(aMaskItem);
+
+ if (!maskData) {
+ return Nothing();
+ }
+
+ bool snap;
+ nsRect bounds = aMaskItem->GetBounds(aDisplayListBuilder, &snap);
+
+ const int32_t appUnitsPerDevPixel =
+ aMaskItem->Frame()->PresContext()->AppUnitsPerDevPixel();
+
+ MatrixScales scale = aSc.GetInheritedScale();
+ MatrixScales oldScale = maskData->mScale;
+ // This scale determination should probably be done using
+ // ChooseScaleAndSetTransform but for now we just fake it.
+ // We tolerate slight changes in scale so that we don't, for example,
+ // rerasterize on MotionMark
+ bool sameScale = FuzzyEqual(scale.xScale, oldScale.xScale, 1e-6f) &&
+ FuzzyEqual(scale.yScale, oldScale.yScale, 1e-6f);
+
+ LayerIntRect itemRect =
+ LayerIntRect::FromUnknownRect(bounds.ScaleToOutsidePixels(
+ scale.xScale, scale.yScale, appUnitsPerDevPixel));
+
+ LayerIntRect visibleRect =
+ LayerIntRect::FromUnknownRect(
+ aMaskItem->GetBuildingRect().ScaleToOutsidePixels(
+ scale.xScale, scale.yScale, appUnitsPerDevPixel))
+ .SafeIntersect(itemRect);
+
+ if (visibleRect.IsEmpty()) {
+ return Nothing();
+ }
+
+ LayoutDeviceToLayerScale2D layerScale(scale.xScale, scale.yScale);
+ LayoutDeviceRect imageRect = LayerRect(visibleRect) / layerScale;
+
+ nsPoint maskOffset = aMaskItem->ToReferenceFrame() - bounds.TopLeft();
+
+ bool shouldHandleOpacity = aBuilder.GetInheritedOpacity() != 1.0f;
+
+ nsRect dirtyRect;
+ // If this mask item is being painted for the first time, some members of
+ // WebRenderMaskData are still default initialized. This is intentional.
+ if (aMaskItem->IsInvalid(dirtyRect) ||
+ !itemRect.IsEqualInterior(maskData->mItemRect) ||
+ !(aMaskItem->Frame()->StyleSVGReset()->mMask == maskData->mMaskStyle) ||
+ maskOffset != maskData->mMaskOffset || !sameScale ||
+ shouldHandleOpacity != maskData->mShouldHandleOpacity) {
+ IntSize size = itemRect.Size().ToUnknownSize();
+
+ if (!Factory::AllowedSurfaceSize(size)) {
+ return Nothing();
+ }
+
+ std::vector<RefPtr<ScaledFont>> fonts;
+ bool validFonts = true;
+ RefPtr<WebRenderDrawEventRecorder> recorder =
+ MakeAndAddRef<WebRenderDrawEventRecorder>(
+ [&](MemStream& aStream,
+ std::vector<RefPtr<ScaledFont>>& aScaledFonts) {
+ size_t count = aScaledFonts.size();
+ aStream.write((const char*)&count, sizeof(count));
+
+ for (auto& scaled : aScaledFonts) {
+ Maybe<wr::FontInstanceKey> key =
+ mManager->WrBridge()->GetFontKeyForScaledFont(scaled,
+ aResources);
+ if (key.isNothing()) {
+ validFonts = false;
+ break;
+ }
+ BlobFont font = {key.value(), scaled};
+ aStream.write((const char*)&font, sizeof(font));
+ }
+
+ fonts = std::move(aScaledFonts);
+ });
+
+ RefPtr<DrawTarget> dummyDt = Factory::CreateDrawTarget(
+ BackendType::SKIA, IntSize(1, 1), SurfaceFormat::A8);
+ RefPtr<DrawTarget> dt = Factory::CreateRecordingDrawTarget(
+ recorder, dummyDt, IntRect(IntPoint(0, 0), size));
+ if (!dt || !dt->IsValid()) {
+ gfxCriticalNote << "Failed to create drawTarget for blob mask image";
+ return Nothing();
+ }
+
+ gfxContext context(dt);
+ context.SetMatrix(context.CurrentMatrix()
+ .PreTranslate(-itemRect.x, -itemRect.y)
+ .PreScale(scale));
+
+ bool maskPainted = false;
+ bool maskIsComplete = aMaskItem->PaintMask(
+ aDisplayListBuilder, &context, shouldHandleOpacity, &maskPainted);
+ if (!maskPainted) {
+ return Nothing();
+ }
+
+ // If a mask is incomplete or missing (e.g. it's display: none) the proper
+ // behaviour depends on the masked frame being html or svg.
+ //
+ // For an HTML frame:
+ // According to css-masking spec, always create a mask surface when
+ // we have any item in maskFrame even if all of those items are
+ // non-resolvable <mask-sources> or <images> so continue with the
+ // painting code. Note that in a common case of no layer of the mask being
+ // complete or even partially complete then the mask surface will be
+ // transparent black so this results in hiding the frame.
+ // For an SVG frame:
+ // SVG 1.1 say that if we fail to resolve a mask, we should draw the
+ // object unmasked so return Nothing().
+ if (!maskIsComplete &&
+ aMaskItem->Frame()->HasAnyStateBits(NS_FRAME_SVG_LAYOUT)) {
+ return Nothing();
+ }
+
+ recorder->FlushItem(IntRect(0, 0, size.width, size.height));
+ recorder->Finish();
+
+ if (!validFonts) {
+ gfxCriticalNote << "Failed serializing fonts for blob mask image";
+ return Nothing();
+ }
+
+ Range<uint8_t> bytes((uint8_t*)recorder->mOutputStream.mData,
+ recorder->mOutputStream.mLength);
+ wr::BlobImageKey key =
+ wr::BlobImageKey{mManager->WrBridge()->GetNextImageKey()};
+ wr::ImageDescriptor descriptor(size, 0, dt->GetFormat(),
+ wr::OpacityType::HasAlphaChannel);
+ if (!aResources.AddBlobImage(key, descriptor, bytes,
+ ImageIntRect(0, 0, size.width, size.height))) {
+ return Nothing();
+ }
+ maskData->ClearImageKey();
+ maskData->mBlobKey = Some(key);
+ maskData->mFonts = fonts;
+ TakeExternalSurfaces(recorder, maskData->mExternalSurfaces,
+ mManager->GetRenderRootStateManager(), aResources);
+ if (maskIsComplete) {
+ maskData->mItemRect = itemRect;
+ maskData->mMaskOffset = maskOffset;
+ maskData->mScale = scale;
+ maskData->mMaskStyle = aMaskItem->Frame()->StyleSVGReset()->mMask;
+ maskData->mShouldHandleOpacity = shouldHandleOpacity;
+ }
+ }
+
+ aResources.SetBlobImageVisibleArea(
+ maskData->mBlobKey.value(),
+ ViewAs<ImagePixel>(visibleRect - itemRect.TopLeft(),
+ PixelCastJustification::LayerIsImage));
+
+ MOZ_DIAGNOSTIC_ASSERT(
+ mManager->WrBridge()->MatchesNamespace(maskData->mBlobKey.ref()),
+ "Stale blob key for mask!");
+
+ wr::ImageMask imageMask;
+ imageMask.image = wr::AsImageKey(maskData->mBlobKey.value());
+ imageMask.rect = wr::ToLayoutRect(imageRect);
+ return Some(imageMask);
+}
+
+bool WebRenderCommandBuilder::PushItemAsImage(
+ nsDisplayItem* aItem, wr::DisplayListBuilder& aBuilder,
+ wr::IpcResourceUpdateQueue& aResources, const StackingContextHelper& aSc,
+ nsDisplayListBuilder* aDisplayListBuilder) {
+ LayoutDeviceRect imageRect;
+ RefPtr<WebRenderFallbackData> fallbackData = GenerateFallbackData(
+ aItem, aBuilder, aResources, aSc, aDisplayListBuilder, imageRect);
+ if (!fallbackData) {
+ return false;
+ }
+
+ wr::LayoutRect dest = wr::ToLayoutRect(imageRect);
+ auto rendering = wr::ToImageRendering(aItem->Frame()->UsedImageRendering());
+ aBuilder.PushImage(dest, dest, !aItem->BackfaceIsHidden(), false, rendering,
+ fallbackData->GetImageKey().value());
+ return true;
+}
+
+void WebRenderCommandBuilder::RemoveUnusedAndResetWebRenderUserData() {
+ mWebRenderUserDatas.RemoveIf([&](WebRenderUserData* data) {
+ if (!data->IsUsed()) {
+ nsIFrame* frame = data->GetFrame();
+
+ MOZ_ASSERT(frame->HasProperty(WebRenderUserDataProperty::Key()));
+
+ WebRenderUserDataTable* userDataTable =
+ frame->GetProperty(WebRenderUserDataProperty::Key());
+
+ MOZ_ASSERT(userDataTable->Count());
+
+ userDataTable->Remove(
+ WebRenderUserDataKey(data->GetDisplayItemKey(), data->GetType()));
+
+ if (!userDataTable->Count()) {
+ frame->RemoveProperty(WebRenderUserDataProperty::Key());
+ userDataTable = nullptr;
+ }
+
+ switch (data->GetType()) {
+ case WebRenderUserData::UserDataType::eCanvas:
+ mLastCanvasDatas.Remove(data->AsCanvasData());
+ break;
+ case WebRenderUserData::UserDataType::eAnimation:
+ EffectCompositor::ClearIsRunningOnCompositor(
+ frame, GetDisplayItemTypeFromKey(data->GetDisplayItemKey()));
+ break;
+ default:
+ break;
+ }
+
+ return true;
+ }
+
+ data->SetUsed(false);
+ return false;
+ });
+}
+
+void WebRenderCommandBuilder::ClearCachedResources() {
+ RemoveUnusedAndResetWebRenderUserData();
+ // UserDatas should only be in the used state during a call to
+ // WebRenderCommandBuilder::BuildWebRenderCommands The should always be false
+ // upon return from BuildWebRenderCommands().
+ MOZ_RELEASE_ASSERT(mWebRenderUserDatas.Count() == 0);
+}
+
+WebRenderGroupData::WebRenderGroupData(
+ RenderRootStateManager* aRenderRootStateManager, nsDisplayItem* aItem)
+ : WebRenderUserData(aRenderRootStateManager, aItem) {
+ MOZ_COUNT_CTOR(WebRenderGroupData);
+}
+
+WebRenderGroupData::~WebRenderGroupData() {
+ MOZ_COUNT_DTOR(WebRenderGroupData);
+ GP("Group data destruct\n");
+ mSubGroup.ClearImageKey(mManager, true);
+ mFollowingGroup.ClearImageKey(mManager, true);
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/wr/WebRenderCommandBuilder.h b/gfx/layers/wr/WebRenderCommandBuilder.h
new file mode 100644
index 0000000000..68c9a4ce63
--- /dev/null
+++ b/gfx/layers/wr/WebRenderCommandBuilder.h
@@ -0,0 +1,237 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GFX_WEBRENDERCOMMANDBUILDER_H
+#define GFX_WEBRENDERCOMMANDBUILDER_H
+
+#include "mozilla/webrender/WebRenderAPI.h"
+#include "mozilla/layers/ClipManager.h"
+#include "mozilla/layers/HitTestInfoManager.h"
+#include "mozilla/layers/WebRenderMessages.h"
+#include "mozilla/layers/WebRenderScrollData.h"
+#include "mozilla/layers/WebRenderUserData.h"
+#include "mozilla/SVGIntegrationUtils.h" // for WrFiltersHolder
+#include "nsDisplayList.h"
+#include "nsIFrame.h"
+#include "nsTHashSet.h"
+#include "DisplayItemCache.h"
+#include "ImgDrawResult.h"
+
+namespace mozilla {
+
+namespace image {
+class WebRenderImageProvider;
+}
+
+namespace layers {
+
+class ImageClient;
+class ImageContainer;
+class WebRenderBridgeChild;
+class WebRenderCanvasData;
+class WebRenderCanvasRendererAsync;
+class WebRenderImageData;
+class WebRenderFallbackData;
+class WebRenderParentCommand;
+class WebRenderUserData;
+
+class WebRenderCommandBuilder final {
+ typedef nsTHashSet<RefPtr<WebRenderUserData>> WebRenderUserDataRefTable;
+ typedef nsTHashSet<RefPtr<WebRenderCanvasData>> CanvasDataSet;
+
+ public:
+ explicit WebRenderCommandBuilder(WebRenderLayerManager* aManager);
+
+ void Destroy();
+
+ void EmptyTransaction();
+
+ bool NeedsEmptyTransaction();
+
+ void BuildWebRenderCommands(wr::DisplayListBuilder& aBuilder,
+ wr::IpcResourceUpdateQueue& aResourceUpdates,
+ nsDisplayList* aDisplayList,
+ nsDisplayListBuilder* aDisplayListBuilder,
+ WebRenderScrollData& aScrollData,
+ WrFiltersHolder&& aFilters);
+
+ void PushOverrideForASR(const ActiveScrolledRoot* aASR,
+ const wr::WrSpatialId& aSpatialId);
+ void PopOverrideForASR(const ActiveScrolledRoot* aASR);
+
+ Maybe<wr::ImageKey> CreateImageKey(
+ nsDisplayItem* aItem, ImageContainer* aContainer,
+ mozilla::wr::DisplayListBuilder& aBuilder,
+ mozilla::wr::IpcResourceUpdateQueue& aResources,
+ mozilla::wr::ImageRendering aRendering, const StackingContextHelper& aSc,
+ gfx::IntSize& aSize, const Maybe<LayoutDeviceRect>& aAsyncImageBounds);
+
+ Maybe<wr::ImageKey> CreateImageProviderKey(
+ nsDisplayItem* aItem, image::WebRenderImageProvider* aProvider,
+ image::ImgDrawResult aDrawResult,
+ mozilla::wr::IpcResourceUpdateQueue& aResources);
+
+ WebRenderUserDataRefTable* GetWebRenderUserDataTable() {
+ return &mWebRenderUserDatas;
+ }
+
+ bool PushImage(nsDisplayItem* aItem, ImageContainer* aContainer,
+ mozilla::wr::DisplayListBuilder& aBuilder,
+ mozilla::wr::IpcResourceUpdateQueue& aResources,
+ const StackingContextHelper& aSc,
+ const LayoutDeviceRect& aRect, const LayoutDeviceRect& aClip);
+
+ bool PushImageProvider(nsDisplayItem* aItem,
+ image::WebRenderImageProvider* aProvider,
+ image::ImgDrawResult aDrawResult,
+ mozilla::wr::DisplayListBuilder& aBuilder,
+ mozilla::wr::IpcResourceUpdateQueue& aResources,
+ const LayoutDeviceRect& aRect,
+ const LayoutDeviceRect& aClip);
+
+ Maybe<wr::ImageMask> BuildWrMaskImage(
+ nsDisplayMasksAndClipPaths* aMaskItem, wr::DisplayListBuilder& aBuilder,
+ wr::IpcResourceUpdateQueue& aResources, const StackingContextHelper& aSc,
+ nsDisplayListBuilder* aDisplayListBuilder,
+ const LayoutDeviceRect& aBounds);
+
+ bool PushItemAsImage(nsDisplayItem* aItem, wr::DisplayListBuilder& aBuilder,
+ wr::IpcResourceUpdateQueue& aResources,
+ const StackingContextHelper& aSc,
+ nsDisplayListBuilder* aDisplayListBuilder);
+
+ void CreateWebRenderCommandsFromDisplayList(
+ nsDisplayList* aDisplayList, nsDisplayItem* aWrappingItem,
+ nsDisplayListBuilder* aDisplayListBuilder,
+ const StackingContextHelper& aSc, wr::DisplayListBuilder& aBuilder,
+ wr::IpcResourceUpdateQueue& aResources, bool aNewClipList = true);
+
+ // aWrappingItem has to be non-null.
+ void DoGroupingForDisplayList(nsDisplayList* aDisplayList,
+ nsDisplayItem* aWrappingItem,
+ nsDisplayListBuilder* aDisplayListBuilder,
+ const StackingContextHelper& aSc,
+ wr::DisplayListBuilder& aBuilder,
+ wr::IpcResourceUpdateQueue& aResources);
+
+ already_AddRefed<WebRenderFallbackData> GenerateFallbackData(
+ nsDisplayItem* aItem, wr::DisplayListBuilder& aBuilder,
+ wr::IpcResourceUpdateQueue& aResources, const StackingContextHelper& aSc,
+ nsDisplayListBuilder* aDisplayListBuilder, LayoutDeviceRect& aImageRect);
+
+ void RemoveUnusedAndResetWebRenderUserData();
+ void ClearCachedResources();
+
+ bool ShouldDumpDisplayList(nsDisplayListBuilder* aBuilder);
+ wr::usize GetBuilderDumpIndex() const { return mBuilderDumpIndex; }
+
+ bool GetContainsSVGGroup() { return mContainsSVGGroup; }
+
+ // Those are data that we kept between transactions. We used to cache some
+ // data in the layer. But in layers free mode, we don't have layer which
+ // means we need some other place to cached the data between transaction.
+ // We store the data in frame's property.
+ template <class T>
+ already_AddRefed<T> CreateOrRecycleWebRenderUserData(
+ nsDisplayItem* aItem, bool* aOutIsRecycled = nullptr) {
+ MOZ_ASSERT(aItem);
+ nsIFrame* frame = aItem->Frame();
+ if (aOutIsRecycled) {
+ *aOutIsRecycled = true;
+ }
+
+ WebRenderUserDataTable* userDataTable =
+ frame->GetProperty(WebRenderUserDataProperty::Key());
+
+ if (!userDataTable) {
+ userDataTable = new WebRenderUserDataTable();
+ frame->AddProperty(WebRenderUserDataProperty::Key(), userDataTable);
+ }
+
+ RefPtr<WebRenderUserData>& data = userDataTable->LookupOrInsertWith(
+ WebRenderUserDataKey(aItem->GetPerFrameKey(), T::Type()), [&] {
+ auto data = MakeRefPtr<T>(GetRenderRootStateManager(), aItem);
+ mWebRenderUserDatas.Insert(data);
+ if (aOutIsRecycled) {
+ *aOutIsRecycled = false;
+ }
+ return data;
+ });
+
+ MOZ_ASSERT(data);
+ MOZ_ASSERT(data->GetType() == T::Type());
+
+ // Mark the data as being used. We will remove unused user data in the end
+ // of EndTransaction.
+ data->SetUsed(true);
+
+ switch (T::Type()) {
+ case WebRenderUserData::UserDataType::eCanvas:
+ mLastCanvasDatas.Insert(data->AsCanvasData());
+ break;
+ default:
+ break;
+ }
+
+ RefPtr<T> res = static_cast<T*>(data.get());
+ return res.forget();
+ }
+
+ WebRenderLayerManager* mManager;
+
+ private:
+ RenderRootStateManager* GetRenderRootStateManager();
+ void CreateWebRenderCommands(nsDisplayItem* aItem,
+ mozilla::wr::DisplayListBuilder& aBuilder,
+ mozilla::wr::IpcResourceUpdateQueue& aResources,
+ const StackingContextHelper& aSc,
+ nsDisplayListBuilder* aDisplayListBuilder);
+
+ bool ComputeInvalidationForDisplayItem(nsDisplayListBuilder* aBuilder,
+ const nsPoint& aShift,
+ nsDisplayItem* aItem);
+ bool ComputeInvalidationForDisplayList(nsDisplayListBuilder* aBuilder,
+ const nsPoint& aShift,
+ nsDisplayList* aList);
+
+ ClipManager mClipManager;
+ HitTestInfoManager mHitTestInfoManager;
+
+ // We use this as a temporary data structure while building the mScrollData
+ // inside a layers-free transaction.
+ std::vector<WebRenderLayerScrollData> mLayerScrollData;
+ // We use this as a temporary data structure to track the current display
+ // item's ASR as we recurse in CreateWebRenderCommandsFromDisplayList. We
+ // need this so that WebRenderLayerScrollData items that deeper in the
+ // tree don't duplicate scroll metadata that their ancestors already have.
+ std::vector<const ActiveScrolledRoot*> mAsrStack;
+ // A similar stack to track the deferred transform that we decided to emit
+ // most recently.
+ std::vector<nsDisplayTransform*> mDeferredTransformStack;
+ const ActiveScrolledRoot* mLastAsr;
+
+ WebRenderUserDataRefTable mWebRenderUserDatas;
+
+ // Store of WebRenderCanvasData objects for use in empty transactions
+ CanvasDataSet mLastCanvasDatas;
+
+ wr::usize mBuilderDumpIndex;
+ wr::usize mDumpIndent;
+
+ public:
+ // Whether consecutive inactive display items should be grouped into one
+ // blob image.
+ bool mDoGrouping;
+
+ // True if the most recently build display list contained an svg that
+ // we did grouping for.
+ bool mContainsSVGGroup;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif /* GFX_WEBRENDERCOMMANDBUILDER_H */
diff --git a/gfx/layers/wr/WebRenderDrawEventRecorder.cpp b/gfx/layers/wr/WebRenderDrawEventRecorder.cpp
new file mode 100644
index 0000000000..db4345d592
--- /dev/null
+++ b/gfx/layers/wr/WebRenderDrawEventRecorder.cpp
@@ -0,0 +1,33 @@
+/* -*- Mode: C++; tab-width: 20; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "WebRenderDrawEventRecorder.h"
+#include "mozilla/layers/SharedSurfacesChild.h"
+#include "mozilla/layers/SharedSurfacesParent.h"
+
+namespace mozilla {
+using namespace gfx;
+
+namespace layers {
+
+void WebRenderDrawEventRecorder::StoreSourceSurfaceRecording(
+ SourceSurface* aSurface, const char* aReason) {
+ wr::ExternalImageId extId;
+ nsresult rv = layers::SharedSurfacesChild::Share(aSurface, extId);
+ if (NS_FAILED(rv)) {
+ DrawEventRecorderMemory::StoreSourceSurfaceRecording(aSurface, aReason);
+ return;
+ }
+
+ StoreExternalSurfaceRecording(aSurface, wr::AsUint64(extId));
+}
+
+already_AddRefed<SourceSurface> WebRenderTranslator::LookupExternalSurface(
+ uint64_t aKey) {
+ return SharedSurfacesParent::Get(wr::ToExternalImageId(aKey));
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/wr/WebRenderDrawEventRecorder.h b/gfx/layers/wr/WebRenderDrawEventRecorder.h
new file mode 100644
index 0000000000..b08ff5b959
--- /dev/null
+++ b/gfx/layers/wr/WebRenderDrawEventRecorder.h
@@ -0,0 +1,49 @@
+/* -*- Mode: C++; tab-width: 20; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_LAYERS_WEBRENDERDRAWTARGETRECORDER_H
+#define MOZILLA_LAYERS_WEBRENDERDRAWTARGETRECORDER_H
+
+#include "mozilla/gfx/DrawEventRecorder.h"
+#include "mozilla/gfx/InlineTranslator.h"
+#include "mozilla/webrender/webrender_ffi.h"
+
+namespace mozilla {
+namespace layers {
+
+struct BlobFont {
+ wr::FontInstanceKey mFontInstanceKey;
+ gfx::ReferencePtr mScaledFontPtr;
+};
+
+class WebRenderDrawEventRecorder final : public gfx::DrawEventRecorderMemory {
+ public:
+ MOZ_DECLARE_REFCOUNTED_VIRTUAL_TYPENAME(WebRenderDrawEventRecorder, final)
+
+ explicit WebRenderDrawEventRecorder(
+ const gfx::SerializeResourcesFn& aSerialize)
+ : DrawEventRecorderMemory(aSerialize) {}
+
+ void StoreSourceSurfaceRecording(gfx::SourceSurface* aSurface,
+ const char* aReason) final;
+
+ private:
+ virtual ~WebRenderDrawEventRecorder() = default;
+};
+
+class WebRenderTranslator final : public gfx::InlineTranslator {
+ public:
+ explicit WebRenderTranslator(gfx::DrawTarget* aDT,
+ void* aFontContext = nullptr)
+ : InlineTranslator(aDT, aFontContext) {}
+
+ already_AddRefed<gfx::SourceSurface> LookupExternalSurface(
+ uint64_t aKey) final;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif
diff --git a/gfx/layers/wr/WebRenderImageHost.cpp b/gfx/layers/wr/WebRenderImageHost.cpp
new file mode 100644
index 0000000000..361c08f9c2
--- /dev/null
+++ b/gfx/layers/wr/WebRenderImageHost.cpp
@@ -0,0 +1,395 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "WebRenderImageHost.h"
+
+#include <utility>
+
+#include "mozilla/ScopeExit.h"
+#include "mozilla/gfx/gfxVars.h"
+#include "mozilla/layers/AsyncImagePipelineManager.h"
+#include "mozilla/layers/CompositorThread.h"
+#include "mozilla/layers/CompositorVsyncScheduler.h" // for CompositorVsyncScheduler
+#include "mozilla/layers/RemoteTextureHostWrapper.h"
+#include "mozilla/layers/RemoteTextureMap.h"
+#include "mozilla/layers/WebRenderBridgeParent.h"
+#include "mozilla/layers/WebRenderTextureHost.h"
+#include "mozilla/StaticPrefs_webgl.h"
+#include "nsAString.h"
+#include "nsDebug.h" // for NS_WARNING, NS_ASSERTION
+#include "nsPrintfCString.h" // for nsPrintfCString
+#include "nsString.h" // for nsAutoCString
+
+namespace mozilla {
+
+using namespace gfx;
+
+namespace layers {
+
+class ISurfaceAllocator;
+
+WebRenderImageHost::WebRenderImageHost(const TextureInfo& aTextureInfo)
+ : CompositableHost(aTextureInfo),
+ ImageComposite(),
+ mCurrentAsyncImageManager(nullptr) {}
+
+WebRenderImageHost::~WebRenderImageHost() {
+ MOZ_ASSERT(mPendingRemoteTextureWrappers.empty());
+ MOZ_ASSERT(mWrBridges.empty());
+}
+
+void WebRenderImageHost::OnReleased() {
+ if (mRemoteTextureOwnerIdOfPushCallback) {
+ RemoteTextureMap::Get()->UnregisterRemoteTexturePushListener(
+ *mRemoteTextureOwnerIdOfPushCallback, mForPidOfPushCallback, this);
+ mRemoteTextureOwnerIdOfPushCallback = Nothing();
+ mSizeOfPushCallback = gfx::IntSize();
+ mFlagsOfPushCallback = TextureFlags::NO_FLAGS;
+ }
+ if (!mPendingRemoteTextureWrappers.empty()) {
+ mPendingRemoteTextureWrappers.clear();
+ }
+}
+
+void WebRenderImageHost::UseTextureHost(
+ const nsTArray<TimedTexture>& aTextures) {
+ CompositableHost::UseTextureHost(aTextures);
+ MOZ_ASSERT(aTextures.Length() >= 1);
+
+ if (!mPendingRemoteTextureWrappers.empty()) {
+ mPendingRemoteTextureWrappers.clear();
+ }
+
+ if (mCurrentTextureHost &&
+ mCurrentTextureHost->AsRemoteTextureHostWrapper()) {
+ mCurrentTextureHost = nullptr;
+ }
+
+ nsTArray<TimedImage> newImages;
+
+ for (uint32_t i = 0; i < aTextures.Length(); ++i) {
+ const TimedTexture& t = aTextures[i];
+ MOZ_ASSERT(t.mTexture);
+ if (i + 1 < aTextures.Length() && t.mProducerID == mLastProducerID &&
+ t.mFrameID < mLastFrameID) {
+ // Ignore frames before a frame that we already composited. We don't
+ // ever want to display these frames. This could be important if
+ // the frame producer adjusts timestamps (e.g. to track the audio clock)
+ // and the new frame times are earlier.
+ continue;
+ }
+ TimedImage& img = *newImages.AppendElement();
+ img.mTextureHost = t.mTexture;
+ img.mTimeStamp = t.mTimeStamp;
+ img.mPictureRect = t.mPictureRect;
+ img.mFrameID = t.mFrameID;
+ img.mProducerID = t.mProducerID;
+ img.mTextureHost->SetCropRect(img.mPictureRect);
+ }
+
+ SetImages(std::move(newImages));
+
+ if (GetAsyncRef()) {
+ for (const auto& it : mWrBridges) {
+ RefPtr<WebRenderBridgeParent> wrBridge = it.second->WrBridge();
+ if (wrBridge && wrBridge->CompositorScheduler()) {
+ wrBridge->CompositorScheduler()->ScheduleComposition(
+ wr::RenderReasons::ASYNC_IMAGE);
+ }
+ }
+ }
+
+ // Video producers generally send replacement images with the same frameID but
+ // slightly different timestamps in order to sync with the audio clock. This
+ // means that any CompositeUntil() call we made in Composite() may no longer
+ // guarantee that we'll composite until the next frame is ready. Fix that
+ // here.
+ if (mLastFrameID >= 0 && !mWrBridges.empty()) {
+ for (const auto& img : Images()) {
+ bool frameComesAfter =
+ img.mFrameID > mLastFrameID || img.mProducerID != mLastProducerID;
+ if (frameComesAfter && !img.mTimeStamp.IsNull()) {
+ for (const auto& it : mWrBridges) {
+ RefPtr<WebRenderBridgeParent> wrBridge = it.second->WrBridge();
+ if (wrBridge) {
+ wrBridge->AsyncImageManager()->CompositeUntil(
+ img.mTimeStamp + TimeDuration::FromMilliseconds(BIAS_TIME_MS));
+ }
+ }
+ break;
+ }
+ }
+ }
+}
+
+void WebRenderImageHost::PushPendingRemoteTexture(
+ const RemoteTextureId aTextureId, const RemoteTextureOwnerId aOwnerId,
+ const base::ProcessId aForPid, const gfx::IntSize aSize,
+ const TextureFlags aFlags) {
+ // Ensure aOwnerId is the same as RemoteTextureOwnerId of pending
+ // RemoteTextures.
+ if (!mPendingRemoteTextureWrappers.empty()) {
+ auto* wrapper =
+ mPendingRemoteTextureWrappers.front()->AsRemoteTextureHostWrapper();
+ MOZ_ASSERT(wrapper);
+ if (wrapper->mOwnerId != aOwnerId || wrapper->mForPid != aForPid) {
+ // Clear when RemoteTextureOwner is different.
+ mPendingRemoteTextureWrappers.clear();
+ mWaitingReadyCallback = false;
+ }
+ }
+
+ RefPtr<TextureHost> texture =
+ RemoteTextureMap::Get()->GetOrCreateRemoteTextureHostWrapper(
+ aTextureId, aOwnerId, aForPid, aSize, aFlags);
+ MOZ_ASSERT(texture);
+ mPendingRemoteTextureWrappers.push_back(
+ CompositableTextureHostRef(texture.get()));
+}
+
+void WebRenderImageHost::UseRemoteTexture() {
+ if (mPendingRemoteTextureWrappers.empty()) {
+ return;
+ }
+
+ const bool useAsyncRemoteTexture =
+ gfx::gfxVars::UseCanvasRenderThread() &&
+ StaticPrefs::webgl_out_of_process_async_present() &&
+ !gfx::gfxVars::WebglOopAsyncPresentForceSync();
+ const bool useReadyCallback = GetAsyncRef() && useAsyncRemoteTexture &&
+ mRemoteTextureOwnerIdOfPushCallback.isNothing();
+ CompositableTextureHostRef texture;
+
+ if (useReadyCallback) {
+ if (mWaitingReadyCallback) {
+ return;
+ }
+ MOZ_ASSERT(!mWaitingReadyCallback);
+
+ auto readyCallback = [self = RefPtr<WebRenderImageHost>(this)](
+ const RemoteTextureInfo aInfo) {
+ RefPtr<nsIRunnable> runnable = NS_NewRunnableFunction(
+ "WebRenderImageHost::UseRemoteTexture",
+ [self = std::move(self), aInfo]() {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+
+ if (self->mPendingRemoteTextureWrappers.empty()) {
+ return;
+ }
+
+ auto* wrapper = self->mPendingRemoteTextureWrappers.front()
+ ->AsRemoteTextureHostWrapper();
+ MOZ_ASSERT(wrapper);
+ if (wrapper->mOwnerId != aInfo.mOwnerId ||
+ wrapper->mForPid != aInfo.mForPid) {
+ // obsoleted callback
+ return;
+ }
+
+ self->mWaitingReadyCallback = false;
+ self->UseRemoteTexture();
+ });
+
+ CompositorThread()->Dispatch(runnable.forget());
+ };
+
+ // Check which of the pending remote textures is the most recent and ready.
+ while (!mPendingRemoteTextureWrappers.empty()) {
+ auto* wrapper =
+ mPendingRemoteTextureWrappers.front()->AsRemoteTextureHostWrapper();
+ mWaitingReadyCallback =
+ RemoteTextureMap::Get()->GetRemoteTextureForDisplayList(
+ wrapper, readyCallback);
+ MOZ_ASSERT_IF(mWaitingReadyCallback, !wrapper->IsReadyForRendering());
+ if (!wrapper->IsReadyForRendering()) {
+ break;
+ }
+ texture = mPendingRemoteTextureWrappers.front();
+ mPendingRemoteTextureWrappers.pop_front();
+ }
+ } else {
+ texture = mPendingRemoteTextureWrappers.front();
+ auto* wrapper = texture->AsRemoteTextureHostWrapper();
+ mPendingRemoteTextureWrappers.pop_front();
+ MOZ_ASSERT(mPendingRemoteTextureWrappers.empty());
+
+ std::function<void(const RemoteTextureInfo&)> function;
+ RemoteTextureMap::Get()->GetRemoteTextureForDisplayList(
+ wrapper, std::move(function));
+ }
+
+ if (!texture ||
+ !texture->AsRemoteTextureHostWrapper()->IsReadyForRendering()) {
+ return;
+ }
+
+ SetCurrentTextureHost(texture);
+
+ if (GetAsyncRef()) {
+ for (const auto& it : mWrBridges) {
+ RefPtr<WebRenderBridgeParent> wrBridge = it.second->WrBridge();
+ if (wrBridge && wrBridge->CompositorScheduler()) {
+ wrBridge->CompositorScheduler()->ScheduleComposition(
+ wr::RenderReasons::ASYNC_IMAGE);
+ }
+ }
+ }
+}
+
+void WebRenderImageHost::EnableRemoteTexturePushCallback(
+ const RemoteTextureOwnerId aOwnerId, const base::ProcessId aForPid,
+ const gfx::IntSize aSize, const TextureFlags aFlags) {
+ if (!GetAsyncRef()) {
+ MOZ_ASSERT_UNREACHABLE("unexpected to be called");
+ return;
+ }
+
+ if (mRemoteTextureOwnerIdOfPushCallback.isSome()) {
+ RemoteTextureMap::Get()->UnregisterRemoteTexturePushListener(aOwnerId,
+ aForPid, this);
+ }
+
+ RemoteTextureMap::Get()->RegisterRemoteTexturePushListener(aOwnerId, aForPid,
+ this);
+ mRemoteTextureOwnerIdOfPushCallback = Some(aOwnerId);
+ mForPidOfPushCallback = aForPid;
+ mSizeOfPushCallback = aSize;
+ mFlagsOfPushCallback = aFlags;
+}
+
+void WebRenderImageHost::NotifyPushTexture(const RemoteTextureId aTextureId,
+ const RemoteTextureOwnerId aOwnerId,
+ const base::ProcessId aForPid) {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+
+ if (mRemoteTextureOwnerIdOfPushCallback != Some(aOwnerId)) {
+ // RemoteTextureOwnerId is already obsoleted
+ return;
+ }
+ PushPendingRemoteTexture(aTextureId, aOwnerId, aForPid, mSizeOfPushCallback,
+ mFlagsOfPushCallback);
+ UseRemoteTexture();
+}
+
+void WebRenderImageHost::CleanupResources() {
+ ClearImages();
+ SetCurrentTextureHost(nullptr);
+}
+
+void WebRenderImageHost::RemoveTextureHost(TextureHost* aTexture) {
+ CompositableHost::RemoveTextureHost(aTexture);
+ RemoveImagesWithTextureHost(aTexture);
+}
+
+TimeStamp WebRenderImageHost::GetCompositionTime() const {
+ TimeStamp time;
+
+ MOZ_ASSERT(mCurrentAsyncImageManager);
+ if (mCurrentAsyncImageManager) {
+ time = mCurrentAsyncImageManager->GetCompositionTime();
+ }
+ return time;
+}
+
+CompositionOpportunityId WebRenderImageHost::GetCompositionOpportunityId()
+ const {
+ CompositionOpportunityId id;
+
+ MOZ_ASSERT(mCurrentAsyncImageManager);
+ if (mCurrentAsyncImageManager) {
+ id = mCurrentAsyncImageManager->GetCompositionOpportunityId();
+ }
+ return id;
+}
+
+void WebRenderImageHost::AppendImageCompositeNotification(
+ const ImageCompositeNotificationInfo& aInfo) const {
+ if (mCurrentAsyncImageManager) {
+ mCurrentAsyncImageManager->AppendImageCompositeNotification(aInfo);
+ }
+}
+
+TextureHost* WebRenderImageHost::GetAsTextureHostForComposite(
+ AsyncImagePipelineManager* aAsyncImageManager) {
+ if (mCurrentTextureHost &&
+ mCurrentTextureHost->AsRemoteTextureHostWrapper()) {
+ return mCurrentTextureHost;
+ }
+
+ mCurrentAsyncImageManager = aAsyncImageManager;
+ const auto onExit =
+ mozilla::MakeScopeExit([&]() { mCurrentAsyncImageManager = nullptr; });
+
+ int imageIndex = ChooseImageIndex();
+ if (imageIndex < 0) {
+ SetCurrentTextureHost(nullptr);
+ return nullptr;
+ }
+
+ if (uint32_t(imageIndex) + 1 < ImagesCount()) {
+ mCurrentAsyncImageManager->CompositeUntil(
+ GetImage(imageIndex + 1)->mTimeStamp +
+ TimeDuration::FromMilliseconds(BIAS_TIME_MS));
+ }
+
+ const TimedImage* img = GetImage(imageIndex);
+ SetCurrentTextureHost(img->mTextureHost);
+
+ if (mCurrentAsyncImageManager->GetCompositionTime()) {
+ // We are in a composition. Send ImageCompositeNotifications.
+ OnFinishRendering(imageIndex, img, mAsyncRef.mProcessId, mAsyncRef.mHandle);
+ }
+
+ return mCurrentTextureHost;
+}
+
+void WebRenderImageHost::SetCurrentTextureHost(TextureHost* aTexture) {
+ if (aTexture == mCurrentTextureHost.get()) {
+ return;
+ }
+ mCurrentTextureHost = aTexture;
+}
+
+void WebRenderImageHost::Dump(std::stringstream& aStream, const char* aPrefix,
+ bool aDumpHtml) {
+ for (const auto& img : Images()) {
+ aStream << aPrefix;
+ aStream << (aDumpHtml ? "<ul><li>TextureHost: " : "TextureHost: ");
+ DumpTextureHost(aStream, img.mTextureHost);
+ aStream << (aDumpHtml ? " </li></ul> " : " ");
+ }
+}
+
+void WebRenderImageHost::SetWrBridge(const wr::PipelineId& aPipelineId,
+ WebRenderBridgeParent* aWrBridge) {
+ MOZ_ASSERT(aWrBridge);
+ MOZ_ASSERT(!mCurrentAsyncImageManager);
+#ifdef DEBUG
+ const auto it = mWrBridges.find(wr::AsUint64(aPipelineId));
+ MOZ_ASSERT(it == mWrBridges.end());
+#endif
+ RefPtr<WebRenderBridgeParentRef> ref =
+ aWrBridge->GetWebRenderBridgeParentRef();
+ mWrBridges.emplace(wr::AsUint64(aPipelineId), ref);
+}
+
+void WebRenderImageHost::ClearWrBridge(const wr::PipelineId& aPipelineId,
+ WebRenderBridgeParent* aWrBridge) {
+ MOZ_ASSERT(aWrBridge);
+ MOZ_ASSERT(!mCurrentAsyncImageManager);
+
+ const auto it = mWrBridges.find(wr::AsUint64(aPipelineId));
+ MOZ_ASSERT(it != mWrBridges.end());
+ if (it == mWrBridges.end()) {
+ gfxCriticalNote << "WrBridge mismatch happened";
+ return;
+ }
+ mWrBridges.erase(it);
+ SetCurrentTextureHost(nullptr);
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/wr/WebRenderImageHost.h b/gfx/layers/wr/WebRenderImageHost.h
new file mode 100644
index 0000000000..23e0f88f88
--- /dev/null
+++ b/gfx/layers/wr/WebRenderImageHost.h
@@ -0,0 +1,100 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_GFX_WEBRENDERIMAGEHOST_H
+#define MOZILLA_GFX_WEBRENDERIMAGEHOST_H
+
+#include <deque>
+#include <unordered_map>
+
+#include "CompositableHost.h" // for CompositableHost
+#include "mozilla/layers/ImageComposite.h" // for ImageComposite
+#include "mozilla/WeakPtr.h"
+
+namespace mozilla {
+namespace layers {
+
+class AsyncImagePipelineManager;
+class WebRenderBridgeParent;
+class WebRenderBridgeParentRef;
+
+/**
+ * ImageHost. Works with ImageClientSingle and ImageClientBuffered
+ */
+class WebRenderImageHost : public CompositableHost, public ImageComposite {
+ public:
+ explicit WebRenderImageHost(const TextureInfo& aTextureInfo);
+ virtual ~WebRenderImageHost();
+
+ void UseTextureHost(const nsTArray<TimedTexture>& aTextures) override;
+ void RemoveTextureHost(TextureHost* aTexture) override;
+
+ void EnableRemoteTexturePushCallback(const RemoteTextureOwnerId aOwnerId,
+ const base::ProcessId aForPid,
+ const gfx::IntSize aSize,
+ const TextureFlags aFlags) override;
+
+ void NotifyPushTexture(const RemoteTextureId aTextureId,
+ const RemoteTextureOwnerId aOwnerId,
+ const base::ProcessId aForPid) override;
+
+ void Dump(std::stringstream& aStream, const char* aPrefix = "",
+ bool aDumpHtml = false) override;
+
+ void CleanupResources() override;
+
+ void OnReleased() override;
+
+ uint32_t GetDroppedFrames() override { return GetDroppedFramesAndReset(); }
+
+ WebRenderImageHost* AsWebRenderImageHost() override { return this; }
+
+ void PushPendingRemoteTexture(const RemoteTextureId aTextureId,
+ const RemoteTextureOwnerId aOwnerId,
+ const base::ProcessId aForPid,
+ const gfx::IntSize aSize,
+ const TextureFlags aFlags);
+ void UseRemoteTexture();
+
+ TextureHost* GetAsTextureHostForComposite(
+ AsyncImagePipelineManager* aAsyncImageManager);
+
+ void SetWrBridge(const wr::PipelineId& aPipelineId,
+ WebRenderBridgeParent* aWrBridge);
+
+ void ClearWrBridge(const wr::PipelineId& aPipelineId,
+ WebRenderBridgeParent* aWrBridge);
+
+ TextureHost* GetCurrentTextureHost() { return mCurrentTextureHost; }
+
+ protected:
+ // ImageComposite
+ TimeStamp GetCompositionTime() const override;
+ CompositionOpportunityId GetCompositionOpportunityId() const override;
+ void AppendImageCompositeNotification(
+ const ImageCompositeNotificationInfo& aInfo) const override;
+
+ void SetCurrentTextureHost(TextureHost* aTexture);
+
+ std::unordered_map<uint64_t, RefPtr<WebRenderBridgeParentRef>> mWrBridges;
+
+ AsyncImagePipelineManager* mCurrentAsyncImageManager;
+
+ CompositableTextureHostRef mCurrentTextureHost;
+
+ std::deque<CompositableTextureHostRef> mPendingRemoteTextureWrappers;
+ bool mWaitingReadyCallback = false;
+
+ Maybe<RemoteTextureOwnerId> mRemoteTextureOwnerIdOfPushCallback;
+ base::ProcessId mForPidOfPushCallback;
+ gfx::IntSize mSizeOfPushCallback;
+ TextureFlags mFlagsOfPushCallback = TextureFlags::NO_FLAGS;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // MOZILLA_GFX_WEBRENDERIMAGEHOST_H
diff --git a/gfx/layers/wr/WebRenderLayerManager.cpp b/gfx/layers/wr/WebRenderLayerManager.cpp
new file mode 100644
index 0000000000..8dc6c13ac1
--- /dev/null
+++ b/gfx/layers/wr/WebRenderLayerManager.cpp
@@ -0,0 +1,821 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "WebRenderLayerManager.h"
+
+#include "GeckoProfiler.h"
+#include "mozilla/StaticPrefs_apz.h"
+#include "mozilla/StaticPrefs_layers.h"
+#include "mozilla/dom/BrowserChild.h"
+#include "mozilla/gfx/DrawEventRecorder.h"
+#include "mozilla/gfx/gfxVars.h"
+#include "mozilla/layers/CompositorBridgeChild.h"
+#include "mozilla/layers/StackingContextHelper.h"
+#include "mozilla/layers/TextureClient.h"
+#include "mozilla/layers/TransactionIdAllocator.h"
+#include "mozilla/layers/WebRenderBridgeChild.h"
+#include "mozilla/layers/UpdateImageHelper.h"
+#include "mozilla/PerfStats.h"
+#include "nsDisplayList.h"
+#include "nsLayoutUtils.h"
+#include "WebRenderCanvasRenderer.h"
+#include "LayerUserData.h"
+
+#ifdef XP_WIN
+# include "gfxDWriteFonts.h"
+# include "mozilla/WindowsProcessMitigations.h"
+#endif
+
+namespace mozilla {
+
+using namespace gfx;
+
+namespace layers {
+
+WebRenderLayerManager::WebRenderLayerManager(nsIWidget* aWidget)
+ : mWidget(aWidget),
+ mLatestTransactionId{0},
+ mNeedsComposite(false),
+ mIsFirstPaint(false),
+ mDestroyed(false),
+ mTarget(nullptr),
+ mPaintSequenceNumber(0),
+ mWebRenderCommandBuilder(this) {
+ MOZ_COUNT_CTOR(WebRenderLayerManager);
+ mStateManager.mLayerManager = this;
+
+ if (XRE_IsContentProcess() &&
+ StaticPrefs::gfx_webrender_enable_item_cache_AtStartup()) {
+ static const size_t kInitialCacheSize = 1024;
+ static const size_t kMaximumCacheSize = 10240;
+
+ mDisplayItemCache.SetCapacity(kInitialCacheSize, kMaximumCacheSize);
+ }
+}
+
+KnowsCompositor* WebRenderLayerManager::AsKnowsCompositor() { return mWrChild; }
+
+bool WebRenderLayerManager::Initialize(
+ PCompositorBridgeChild* aCBChild, wr::PipelineId aLayersId,
+ TextureFactoryIdentifier* aTextureFactoryIdentifier, nsCString& aError) {
+ MOZ_ASSERT(mWrChild == nullptr);
+ MOZ_ASSERT(aTextureFactoryIdentifier);
+
+ // When we fail to initialize WebRender, it is useful to know if it has ever
+ // succeeded, or if this is the first attempt.
+ static bool hasInitialized = false;
+
+ WindowKind windowKind;
+ if (mWidget->GetWindowType() != widget::WindowType::Popup) {
+ windowKind = WindowKind::MAIN;
+ } else {
+ windowKind = WindowKind::SECONDARY;
+ }
+
+ LayoutDeviceIntSize size = mWidget->GetClientSize();
+ // Check widget size
+ if (!wr::WindowSizeSanityCheck(size.width, size.height)) {
+ gfxCriticalNoteOnce << "Widget size is not valid " << size
+ << " isParent: " << XRE_IsParentProcess();
+ }
+
+ PWebRenderBridgeChild* bridge =
+ aCBChild->SendPWebRenderBridgeConstructor(aLayersId, size, windowKind);
+ if (!bridge) {
+ // This should only fail if we attempt to access a layer we don't have
+ // permission for, or more likely, the GPU process crashed again during
+ // reinitialization. We can expect to be notified again to reinitialize
+ // (which may or may not be using WebRender).
+ gfxCriticalNote << "Failed to create WebRenderBridgeChild.";
+ aError.Assign(hasInitialized
+ ? "FEATURE_FAILURE_WEBRENDER_INITIALIZE_IPDL_POST"_ns
+ : "FEATURE_FAILURE_WEBRENDER_INITIALIZE_IPDL_FIRST"_ns);
+ return false;
+ }
+
+ mWrChild = static_cast<WebRenderBridgeChild*>(bridge);
+
+ TextureFactoryIdentifier textureFactoryIdentifier;
+ wr::MaybeIdNamespace idNamespace;
+ // Sync ipc
+ if (!WrBridge()->SendEnsureConnected(&textureFactoryIdentifier, &idNamespace,
+ &aError)) {
+ gfxCriticalNote << "Failed as lost WebRenderBridgeChild.";
+ aError.Assign(hasInitialized
+ ? "FEATURE_FAILURE_WEBRENDER_INITIALIZE_SYNC_POST"_ns
+ : "FEATURE_FAILURE_WEBRENDER_INITIALIZE_SYNC_FIRST"_ns);
+ return false;
+ }
+
+ if (textureFactoryIdentifier.mParentBackend == LayersBackend::LAYERS_NONE ||
+ idNamespace.isNothing()) {
+ gfxCriticalNote << "Failed to connect WebRenderBridgeChild. isParent="
+ << XRE_IsParentProcess();
+ aError.Append(hasInitialized ? "_POST"_ns : "_FIRST"_ns);
+ return false;
+ }
+
+ WrBridge()->SetWebRenderLayerManager(this);
+ WrBridge()->IdentifyTextureHost(textureFactoryIdentifier);
+ WrBridge()->SetNamespace(idNamespace.ref());
+ *aTextureFactoryIdentifier = textureFactoryIdentifier;
+
+ mDLBuilder = MakeUnique<wr::DisplayListBuilder>(
+ WrBridge()->GetPipeline(), WrBridge()->GetWebRenderBackend());
+
+ hasInitialized = true;
+ return true;
+}
+
+void WebRenderLayerManager::Destroy() { DoDestroy(/* aIsSync */ false); }
+
+void WebRenderLayerManager::DoDestroy(bool aIsSync) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (IsDestroyed()) {
+ return;
+ }
+
+ mDLBuilder = nullptr;
+ mUserData.Destroy();
+ mPartialPrerenderedAnimations.Clear();
+
+ mStateManager.Destroy();
+
+ if (WrBridge()) {
+ WrBridge()->Destroy(aIsSync);
+ }
+
+ mWebRenderCommandBuilder.Destroy();
+
+ if (mTransactionIdAllocator) {
+ // Make sure to notify the refresh driver just in case it's waiting on a
+ // pending transaction. Do this at the top of the event loop so we don't
+ // cause a paint to occur during compositor shutdown.
+ RefPtr<TransactionIdAllocator> allocator = mTransactionIdAllocator;
+ TransactionId id = mLatestTransactionId;
+
+ RefPtr<Runnable> task = NS_NewRunnableFunction(
+ "TransactionIdAllocator::NotifyTransactionCompleted",
+ [allocator, id]() -> void {
+ allocator->ClearPendingTransactions();
+ allocator->NotifyTransactionCompleted(id);
+ });
+ NS_DispatchToMainThread(task.forget());
+ }
+
+ // Forget the widget pointer in case we outlive our owning widget.
+ mWidget = nullptr;
+ mDestroyed = true;
+}
+
+WebRenderLayerManager::~WebRenderLayerManager() {
+ Destroy();
+ MOZ_COUNT_DTOR(WebRenderLayerManager);
+}
+
+CompositorBridgeChild* WebRenderLayerManager::GetCompositorBridgeChild() {
+ return WrBridge()->GetCompositorBridgeChild();
+}
+
+void WebRenderLayerManager::GetBackendName(nsAString& name) {
+ if (WrBridge()->UsingSoftwareWebRenderD3D11()) {
+ name.AssignLiteral("WebRender (Software D3D11)");
+ } else if (WrBridge()->UsingSoftwareWebRenderOpenGL()) {
+ name.AssignLiteral("WebRender (Software OpenGL)");
+ } else if (WrBridge()->UsingSoftwareWebRender()) {
+ name.AssignLiteral("WebRender (Software)");
+ } else {
+ name.AssignLiteral("WebRender");
+ }
+}
+
+uint32_t WebRenderLayerManager::StartFrameTimeRecording(int32_t aBufferSize) {
+ CompositorBridgeChild* renderer = GetCompositorBridgeChild();
+ if (renderer) {
+ uint32_t startIndex;
+ renderer->SendStartFrameTimeRecording(aBufferSize, &startIndex);
+ return startIndex;
+ }
+ return -1;
+}
+
+void WebRenderLayerManager::StopFrameTimeRecording(
+ uint32_t aStartIndex, nsTArray<float>& aFrameIntervals) {
+ CompositorBridgeChild* renderer = GetCompositorBridgeChild();
+ if (renderer) {
+ renderer->SendStopFrameTimeRecording(aStartIndex, &aFrameIntervals);
+ }
+}
+
+void WebRenderLayerManager::TakeCompositionPayloads(
+ nsTArray<CompositionPayload>& aPayloads) {
+ aPayloads.Clear();
+
+ std::swap(mPayload, aPayloads);
+}
+
+bool WebRenderLayerManager::BeginTransactionWithTarget(gfxContext* aTarget,
+ const nsCString& aURL) {
+ mTarget = aTarget;
+ bool retval = BeginTransaction(aURL);
+ if (!retval) {
+ mTarget = nullptr;
+ }
+ return retval;
+}
+
+bool WebRenderLayerManager::BeginTransaction(const nsCString& aURL) {
+ if (!WrBridge()->IPCOpen()) {
+ gfxCriticalNote << "IPC Channel is already torn down unexpectedly\n";
+ return false;
+ }
+
+ mTransactionStart = TimeStamp::Now();
+ mURL = aURL;
+
+ // Increment the paint sequence number even if test logging isn't
+ // enabled in this process; it may be enabled in the parent process,
+ // and the parent process expects unique sequence numbers.
+ ++mPaintSequenceNumber;
+ if (StaticPrefs::apz_test_logging_enabled()) {
+ mApzTestData.StartNewPaint(mPaintSequenceNumber);
+ }
+ return true;
+}
+
+bool WebRenderLayerManager::EndEmptyTransaction(EndTransactionFlags aFlags) {
+ auto clearTarget = MakeScopeExit([&] { mTarget = nullptr; });
+
+ // If we haven't sent a display list (since creation or since the last time we
+ // sent ClearDisplayList to the parent) then we can't do an empty transaction
+ // because the parent doesn't have a display list for us and we need to send a
+ // display list first.
+ if (!WrBridge()->GetSentDisplayList()) {
+ return false;
+ }
+
+ mDisplayItemCache.SkipWaitingForPartialDisplayList();
+
+ // Since we don't do repeat transactions right now, just set the time
+ mAnimationReadyTime = TimeStamp::Now();
+
+ // Don't block on hidden windows on Linux as it may block all rendering.
+ const bool throttle = mWidget->IsMapped();
+ mLatestTransactionId = mTransactionIdAllocator->GetTransactionId(throttle);
+
+ if (aFlags & EndTransactionFlags::END_NO_COMPOSITE &&
+ !mWebRenderCommandBuilder.NeedsEmptyTransaction()) {
+ if (mPendingScrollUpdates.IsEmpty()) {
+ MOZ_ASSERT(!mTarget);
+ WrBridge()->SendSetFocusTarget(mFocusTarget);
+ // Revoke TransactionId to trigger next paint.
+ mTransactionIdAllocator->RevokeTransactionId(mLatestTransactionId);
+ mLatestTransactionId = mLatestTransactionId.Prev();
+ return true;
+ }
+ }
+
+ LayoutDeviceIntSize size = mWidget->GetClientSize();
+ WrBridge()->BeginTransaction();
+
+ mWebRenderCommandBuilder.EmptyTransaction();
+
+ // Get the time of when the refresh driver start its tick (if available),
+ // otherwise use the time of when LayerManager::BeginTransaction was called.
+ TimeStamp refreshStart = mTransactionIdAllocator->GetTransactionStart();
+ if (!refreshStart) {
+ refreshStart = mTransactionStart;
+ }
+
+ // Skip the synchronization for buffer since we also skip the painting during
+ // device-reset status.
+ if (!gfxPlatform::GetPlatform()->DidRenderingDeviceReset()) {
+ if (WrBridge()->GetSyncObject() &&
+ WrBridge()->GetSyncObject()->IsSyncObjectValid()) {
+ WrBridge()->GetSyncObject()->Synchronize();
+ }
+ }
+
+ GetCompositorBridgeChild()->EndCanvasTransaction();
+
+ Maybe<TransactionData> transactionData;
+ if (mStateManager.mAsyncResourceUpdates || !mPendingScrollUpdates.IsEmpty() ||
+ WrBridge()->HasWebRenderParentCommands()) {
+ transactionData.emplace();
+ transactionData->mIdNamespace = WrBridge()->GetNamespace();
+ transactionData->mPaintSequenceNumber = mPaintSequenceNumber;
+ if (mStateManager.mAsyncResourceUpdates) {
+ mStateManager.mAsyncResourceUpdates->Flush(
+ transactionData->mResourceUpdates, transactionData->mSmallShmems,
+ transactionData->mLargeShmems);
+ }
+ transactionData->mScrollUpdates = std::move(mPendingScrollUpdates);
+ for (const auto& scrollId : transactionData->mScrollUpdates.Keys()) {
+ nsLayoutUtils::NotifyPaintSkipTransaction(/*scroll id=*/scrollId);
+ }
+ }
+
+ Maybe<wr::IpcResourceUpdateQueue> nothing;
+ WrBridge()->EndEmptyTransaction(mFocusTarget, std::move(transactionData),
+ mLatestTransactionId,
+ mTransactionIdAllocator->GetVsyncId(),
+ mTransactionIdAllocator->GetVsyncStart(),
+ refreshStart, mTransactionStart, mURL);
+ mTransactionStart = TimeStamp();
+
+ MakeSnapshotIfRequired(size);
+ return true;
+}
+
+void WebRenderLayerManager::EndTransactionWithoutLayer(
+ nsDisplayList* aDisplayList, nsDisplayListBuilder* aDisplayListBuilder,
+ WrFiltersHolder&& aFilters, WebRenderBackgroundData* aBackground,
+ const double aGeckoDLBuildTime) {
+ AUTO_PROFILER_TRACING_MARKER("Paint", "WrDisplayList", GRAPHICS);
+
+ auto clearTarget = MakeScopeExit([&] { mTarget = nullptr; });
+
+ // Since we don't do repeat transactions right now, just set the time
+ mAnimationReadyTime = TimeStamp::Now();
+
+ WrBridge()->BeginTransaction();
+
+ LayoutDeviceIntSize size = mWidget->GetClientSize();
+
+ mDLBuilder->Begin(&mDisplayItemCache);
+
+ wr::IpcResourceUpdateQueue resourceUpdates(WrBridge());
+ wr::usize builderDumpIndex = 0;
+ bool containsSVGGroup = false;
+ bool dumpEnabled =
+ mWebRenderCommandBuilder.ShouldDumpDisplayList(aDisplayListBuilder);
+ Maybe<AutoDisplayItemCacheSuppressor> cacheSuppressor;
+ if (dumpEnabled) {
+ cacheSuppressor.emplace(&mDisplayItemCache);
+ printf_stderr("-- WebRender display list build --\n");
+ }
+
+ if (XRE_IsContentProcess() &&
+ StaticPrefs::gfx_webrender_debug_dl_dump_content_serialized()) {
+ mDLBuilder->DumpSerializedDisplayList();
+ }
+
+ if (aDisplayList) {
+ MOZ_ASSERT(aDisplayListBuilder && !aBackground);
+ mDisplayItemCache.SetDisplayList(aDisplayListBuilder, aDisplayList);
+
+ mWebRenderCommandBuilder.BuildWebRenderCommands(
+ *mDLBuilder, resourceUpdates, aDisplayList, aDisplayListBuilder,
+ mScrollData, std::move(aFilters));
+
+ aDisplayListBuilder->NotifyAndClearScrollFrames();
+
+ builderDumpIndex = mWebRenderCommandBuilder.GetBuilderDumpIndex();
+ containsSVGGroup = mWebRenderCommandBuilder.GetContainsSVGGroup();
+ } else {
+ // ViewToPaint does not have frame yet, then render only background clolor.
+ MOZ_ASSERT(!aDisplayListBuilder && aBackground);
+ aBackground->AddWebRenderCommands(*mDLBuilder);
+ if (dumpEnabled) {
+ printf_stderr("(no display list; background only)\n");
+ builderDumpIndex =
+ mDLBuilder->Dump(/*indent*/ 1, Some(builderDumpIndex), Nothing());
+ }
+ }
+
+ mWidget->AddWindowOverlayWebRenderCommands(WrBridge(), *mDLBuilder,
+ resourceUpdates);
+ if (dumpEnabled) {
+ printf_stderr("(window overlay)\n");
+ Unused << mDLBuilder->Dump(/*indent*/ 1, Some(builderDumpIndex), Nothing());
+ }
+
+ if (AsyncPanZoomEnabled()) {
+ if (mIsFirstPaint) {
+ mScrollData.SetIsFirstPaint();
+ mIsFirstPaint = false;
+ }
+ mScrollData.SetPaintSequenceNumber(mPaintSequenceNumber);
+ if (dumpEnabled) {
+ std::stringstream str;
+ str << mScrollData;
+ print_stderr(str);
+ }
+ }
+
+ // Since we're sending a full mScrollData that will include the new scroll
+ // offsets, and we can throw away the pending scroll updates we had kept for
+ // an empty transaction.
+ auto scrollIdsUpdated = ClearPendingScrollInfoUpdate();
+ for (ScrollableLayerGuid::ViewID update : scrollIdsUpdated) {
+ nsLayoutUtils::NotifyPaintSkipTransaction(update);
+ }
+
+ // Don't block on hidden windows on Linux as it may block all rendering.
+ const bool throttle = mWidget->IsMapped();
+ mLatestTransactionId = mTransactionIdAllocator->GetTransactionId(throttle);
+
+ // Get the time of when the refresh driver start its tick (if available),
+ // otherwise use the time of when LayerManager::BeginTransaction was called.
+ TimeStamp refreshStart = mTransactionIdAllocator->GetTransactionStart();
+ if (!refreshStart) {
+ refreshStart = mTransactionStart;
+ }
+
+ if (mStateManager.mAsyncResourceUpdates) {
+ if (resourceUpdates.IsEmpty()) {
+ resourceUpdates.ReplaceResources(
+ std::move(mStateManager.mAsyncResourceUpdates.ref()));
+ } else {
+ WrBridge()->UpdateResources(mStateManager.mAsyncResourceUpdates.ref());
+ }
+ mStateManager.mAsyncResourceUpdates.reset();
+ }
+ mStateManager.DiscardImagesInTransaction(resourceUpdates);
+
+ WrBridge()->RemoveExpiredFontKeys(resourceUpdates);
+
+ // Skip the synchronization for buffer since we also skip the painting during
+ // device-reset status.
+ if (!gfxPlatform::GetPlatform()->DidRenderingDeviceReset()) {
+ if (WrBridge()->GetSyncObject() &&
+ WrBridge()->GetSyncObject()->IsSyncObjectValid()) {
+ WrBridge()->GetSyncObject()->Synchronize();
+ }
+ }
+
+ GetCompositorBridgeChild()->EndCanvasTransaction();
+
+ {
+ AUTO_PROFILER_TRACING_MARKER("Paint", "ForwardDPTransaction", GRAPHICS);
+ DisplayListData dlData;
+ mDLBuilder->End(dlData);
+ resourceUpdates.Flush(dlData.mResourceUpdates, dlData.mSmallShmems,
+ dlData.mLargeShmems);
+ dlData.mRect =
+ LayoutDeviceRect(LayoutDevicePoint(), LayoutDeviceSize(size));
+ dlData.mScrollData.emplace(std::move(mScrollData));
+ dlData.mDLDesc.gecko_display_list_type =
+ aDisplayListBuilder && aDisplayListBuilder->PartialBuildFailed()
+ ? wr::GeckoDisplayListType::Full(aGeckoDLBuildTime)
+ : wr::GeckoDisplayListType::Partial(aGeckoDLBuildTime);
+
+ // convert from nanoseconds to microseconds
+ auto duration = TimeDuration::FromMicroseconds(
+ double(dlData.mDLDesc.builder_finish_time -
+ dlData.mDLDesc.builder_start_time) /
+ 1000.);
+ PerfStats::RecordMeasurement(PerfStats::Metric::WrDisplayListBuilding,
+ duration);
+ bool ret = WrBridge()->EndTransaction(
+ std::move(dlData), mLatestTransactionId, containsSVGGroup,
+ mTransactionIdAllocator->GetVsyncId(),
+ mTransactionIdAllocator->GetVsyncStart(), refreshStart,
+ mTransactionStart, mURL);
+ if (!ret) {
+ // Failed to send display list, reset display item cache state.
+ mDisplayItemCache.Clear();
+ }
+
+ WrBridge()->SendSetFocusTarget(mFocusTarget);
+ mFocusTarget = FocusTarget();
+ }
+
+ // Discard animations after calling WrBridge()->EndTransaction().
+ // It updates mWrEpoch in WebRenderBridgeParent. The updated mWrEpoch is
+ // necessary for deleting animations at the correct time.
+ mStateManager.DiscardCompositorAnimations();
+
+ mTransactionStart = TimeStamp();
+
+ MakeSnapshotIfRequired(size);
+ mNeedsComposite = false;
+}
+
+void WebRenderLayerManager::SetFocusTarget(const FocusTarget& aFocusTarget) {
+ mFocusTarget = aFocusTarget;
+}
+
+bool WebRenderLayerManager::AsyncPanZoomEnabled() const {
+ return mWidget->AsyncPanZoomEnabled();
+}
+
+IntRect ToOutsideIntRect(const gfxRect& aRect) {
+ return IntRect::RoundOut(aRect.X(), aRect.Y(), aRect.Width(), aRect.Height());
+}
+
+void WebRenderLayerManager::MakeSnapshotIfRequired(LayoutDeviceIntSize aSize) {
+ auto clearTarget = MakeScopeExit([&] { mTarget = nullptr; });
+
+ if (!mTarget || !mTarget->GetDrawTarget() || aSize.IsEmpty()) {
+ return;
+ }
+
+ // XXX Add other TextureData supports.
+ // Only BufferTexture is supported now.
+
+ // TODO: fixup for proper surface format.
+ // The GLES spec only guarantees that RGBA can be used with glReadPixels,
+ // so on Android we use RGBA.
+ SurfaceFormat format =
+#ifdef MOZ_WIDGET_ANDROID
+ SurfaceFormat::R8G8B8A8;
+#else
+ SurfaceFormat::B8G8R8A8;
+#endif
+ RefPtr<TextureClient> texture = TextureClient::CreateForRawBufferAccess(
+ WrBridge(), format, aSize.ToUnknownSize(), BackendType::SKIA,
+ TextureFlags::SNAPSHOT);
+ if (!texture) {
+ return;
+ }
+
+ // The other side knows our ContentParentId and WebRenderBridgeChild will
+ // ignore the one provided here in favour of what WebRenderBridgeParent
+ // already has.
+ texture->InitIPDLActor(WrBridge(), dom::ContentParentId());
+ if (!texture->GetIPDLActor()) {
+ return;
+ }
+
+ IntRect bounds = ToOutsideIntRect(mTarget->GetClipExtents());
+ bool needsYFlip = false;
+ if (!WrBridge()->SendGetSnapshot(WrapNotNull(texture->GetIPDLActor()),
+ &needsYFlip)) {
+ return;
+ }
+
+ TextureClientAutoLock autoLock(texture, OpenMode::OPEN_READ_ONLY);
+ if (!autoLock.Succeeded()) {
+ return;
+ }
+ RefPtr<DrawTarget> drawTarget = texture->BorrowDrawTarget();
+ if (!drawTarget || !drawTarget->IsValid()) {
+ return;
+ }
+ RefPtr<SourceSurface> snapshot = drawTarget->Snapshot();
+ /*
+ static int count = 0;
+ char filename[100];
+ snprintf(filename, 100, "output%d.png", count++);
+ printf_stderr("Writing to :%s\n", filename);
+ gfxUtils::WriteAsPNG(snapshot, filename);
+ */
+
+ Rect dst(bounds.X(), bounds.Y(), bounds.Width(), bounds.Height());
+ Rect src(0, 0, bounds.Width(), bounds.Height());
+
+ Matrix m;
+ if (needsYFlip) {
+ m = Matrix::Scaling(1.0, -1.0).PostTranslate(0.0, aSize.height);
+ }
+ SurfacePattern pattern(snapshot, ExtendMode::CLAMP, m);
+ DrawTarget* dt = mTarget->GetDrawTarget();
+ MOZ_RELEASE_ASSERT(dt);
+ dt->FillRect(dst, pattern);
+
+ mTarget = nullptr;
+}
+
+void WebRenderLayerManager::DiscardImages() {
+ wr::IpcResourceUpdateQueue resources(WrBridge());
+ mStateManager.DiscardImagesInTransaction(resources);
+ WrBridge()->UpdateResources(resources);
+}
+
+void WebRenderLayerManager::DiscardLocalImages() {
+ mStateManager.DiscardLocalImages();
+}
+
+void WebRenderLayerManager::SetLayersObserverEpoch(LayersObserverEpoch aEpoch) {
+ if (WrBridge()->IPCOpen()) {
+ WrBridge()->SendSetLayersObserverEpoch(aEpoch);
+ }
+}
+
+void WebRenderLayerManager::DidComposite(
+ TransactionId aTransactionId, const mozilla::TimeStamp& aCompositeStart,
+ const mozilla::TimeStamp& aCompositeEnd) {
+ if (IsDestroyed()) {
+ return;
+ }
+
+ MOZ_ASSERT(mWidget);
+
+ // Notifying the observers may tick the refresh driver which can cause
+ // a lot of different things to happen that may affect the lifetime of
+ // this layer manager. So let's make sure this object stays alive until
+ // the end of the method invocation.
+ RefPtr<WebRenderLayerManager> selfRef = this;
+
+ // |aTransactionId| will be > 0 if the compositor is acknowledging a shadow
+ // layers transaction.
+ if (aTransactionId.IsValid()) {
+ nsIWidgetListener* listener = mWidget->GetWidgetListener();
+ if (listener) {
+ listener->DidCompositeWindow(aTransactionId, aCompositeStart,
+ aCompositeEnd);
+ }
+ listener = mWidget->GetAttachedWidgetListener();
+ if (listener) {
+ listener->DidCompositeWindow(aTransactionId, aCompositeStart,
+ aCompositeEnd);
+ }
+ if (mTransactionIdAllocator) {
+ mTransactionIdAllocator->NotifyTransactionCompleted(aTransactionId);
+ }
+ }
+}
+
+void WebRenderLayerManager::ClearCachedResources() {
+ if (!WrBridge()->IPCOpen()) {
+ gfxCriticalNote << "IPC Channel is already torn down unexpectedly\n";
+ return;
+ }
+ WrBridge()->BeginClearCachedResources();
+ // We flush any pending async resource updates before we clear the display
+ // list items because some resources (e.g. images) might be shared between
+ // multiple layer managers, not get freed here, and we want to keep their
+ // states consistent.
+ mStateManager.FlushAsyncResourceUpdates();
+ mWebRenderCommandBuilder.ClearCachedResources();
+ DiscardImages();
+ mStateManager.ClearCachedResources();
+ WrBridge()->EndClearCachedResources();
+}
+
+void WebRenderLayerManager::ClearAnimationResources() {
+ if (!WrBridge()->IPCOpen()) {
+ gfxCriticalNote << "IPC Channel is already torn down unexpectedly\n";
+ return;
+ }
+ WrBridge()->SendClearAnimationResources();
+}
+
+void WebRenderLayerManager::WrUpdated() {
+ ClearAsyncAnimations();
+ mStateManager.mAsyncResourceUpdates.reset();
+ mWebRenderCommandBuilder.ClearCachedResources();
+ DiscardLocalImages();
+ mDisplayItemCache.Clear();
+
+ if (mWidget) {
+ if (dom::BrowserChild* browserChild = mWidget->GetOwningBrowserChild()) {
+ browserChild->SchedulePaint();
+ }
+ }
+}
+
+void WebRenderLayerManager::UpdateTextureFactoryIdentifier(
+ const TextureFactoryIdentifier& aNewIdentifier) {
+ WrBridge()->IdentifyTextureHost(aNewIdentifier);
+}
+
+TextureFactoryIdentifier WebRenderLayerManager::GetTextureFactoryIdentifier() {
+ return WrBridge()->GetTextureFactoryIdentifier();
+}
+
+void WebRenderLayerManager::SetTransactionIdAllocator(
+ TransactionIdAllocator* aAllocator) {
+ // When changing the refresh driver, the previous refresh driver may never
+ // receive updates of pending transactions it's waiting for. So clear the
+ // waiting state before assigning another refresh driver.
+ if (mTransactionIdAllocator && (aAllocator != mTransactionIdAllocator)) {
+ mTransactionIdAllocator->ClearPendingTransactions();
+
+ // We should also reset the transaction id of the new allocator to previous
+ // allocator's last transaction id, so that completed transactions for
+ // previous allocator will be ignored and won't confuse the new allocator.
+ if (aAllocator) {
+ aAllocator->ResetInitialTransactionId(
+ mTransactionIdAllocator->LastTransactionId());
+ }
+ }
+
+ mTransactionIdAllocator = aAllocator;
+}
+
+TransactionId WebRenderLayerManager::GetLastTransactionId() {
+ return mLatestTransactionId;
+}
+
+void WebRenderLayerManager::FlushRendering(wr::RenderReasons aReasons) {
+ CompositorBridgeChild* cBridge = GetCompositorBridgeChild();
+ if (!cBridge) {
+ return;
+ }
+ MOZ_ASSERT(mWidget);
+
+ // If value of IsResizingNativeWidget() is nothing, we assume that resizing
+ // might happen.
+ bool resizing = mWidget && mWidget->IsResizingNativeWidget().valueOr(true);
+
+ if (resizing) {
+ aReasons = aReasons | wr::RenderReasons::RESIZE;
+ }
+
+ // Limit async FlushRendering to !resizing and Win DComp.
+ // XXX relax the limitation
+ if (WrBridge()->GetCompositorUseDComp() && !resizing) {
+ cBridge->SendFlushRenderingAsync(aReasons);
+ } else if (mWidget->SynchronouslyRepaintOnResize() ||
+ StaticPrefs::layers_force_synchronous_resize()) {
+ cBridge->SendFlushRendering(aReasons);
+ } else {
+ cBridge->SendFlushRenderingAsync(aReasons);
+ }
+}
+
+void WebRenderLayerManager::WaitOnTransactionProcessed() {
+ CompositorBridgeChild* bridge = GetCompositorBridgeChild();
+ if (bridge) {
+ bridge->SendWaitOnTransactionProcessed();
+ }
+}
+
+void WebRenderLayerManager::SendInvalidRegion(const nsIntRegion& aRegion) {
+ // XXX Webrender does not support invalid region yet.
+
+#ifdef XP_WIN
+ // When DWM is disabled, each window does not have own back buffer. They would
+ // paint directly to a buffer that was to be displayed by the video card.
+ // WM_PAINT via SendInvalidRegion() requests necessary re-paint.
+ const bool needsInvalidate = !gfx::gfxVars::DwmCompositionEnabled();
+#else
+ const bool needsInvalidate = true;
+#endif
+ if (needsInvalidate && WrBridge()) {
+ WrBridge()->SendInvalidateRenderedFrame();
+ }
+}
+
+void WebRenderLayerManager::ScheduleComposite(wr::RenderReasons aReasons) {
+ WrBridge()->SendScheduleComposite(aReasons);
+}
+
+already_AddRefed<PersistentBufferProvider>
+WebRenderLayerManager::CreatePersistentBufferProvider(
+ const gfx::IntSize& aSize, gfx::SurfaceFormat aFormat) {
+ if (!gfxPlatform::UseRemoteCanvas()) {
+#ifdef XP_WIN
+ // Any kind of hardware acceleration is incompatible with Win32k Lockdown
+ // We don't initialize devices here so that PersistentBufferProviderShared
+ // will fall back to using a piece of shared memory as a backing for the
+ // canvas
+ if (!IsWin32kLockedDown()) {
+ gfxPlatform::GetPlatform()->EnsureDevicesInitialized();
+ }
+#else
+ gfxPlatform::GetPlatform()->EnsureDevicesInitialized();
+#endif
+ }
+
+ RefPtr<PersistentBufferProvider> provider =
+ PersistentBufferProviderShared::Create(aSize, aFormat,
+ AsKnowsCompositor());
+ if (provider) {
+ return provider.forget();
+ }
+
+ return WindowRenderer::CreatePersistentBufferProvider(aSize, aFormat);
+}
+
+void WebRenderLayerManager::ClearAsyncAnimations() {
+ mStateManager.ClearAsyncAnimations();
+}
+
+void WebRenderLayerManager::WrReleasedImages(
+ const nsTArray<wr::ExternalImageKeyPair>& aPairs) {
+ mStateManager.WrReleasedImages(aPairs);
+}
+
+void WebRenderLayerManager::GetFrameUniformity(FrameUniformityData* aOutData) {
+ WrBridge()->SendGetFrameUniformity(aOutData);
+}
+
+/*static*/
+void WebRenderLayerManager::LayerUserDataDestroy(void* data) {
+ delete static_cast<LayerUserData*>(data);
+}
+
+UniquePtr<LayerUserData> WebRenderLayerManager::RemoveUserData(void* aKey) {
+ UniquePtr<LayerUserData> d(static_cast<LayerUserData*>(
+ mUserData.Remove(static_cast<gfx::UserDataKey*>(aKey))));
+ return d;
+}
+
+std::unordered_set<ScrollableLayerGuid::ViewID>
+WebRenderLayerManager::ClearPendingScrollInfoUpdate() {
+ std::unordered_set<ScrollableLayerGuid::ViewID> scrollIds(
+ mPendingScrollUpdates.Keys().cbegin(),
+ mPendingScrollUpdates.Keys().cend());
+ mPendingScrollUpdates.Clear();
+ return scrollIds;
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/wr/WebRenderLayerManager.h b/gfx/layers/wr/WebRenderLayerManager.h
new file mode 100644
index 0000000000..e9809ea223
--- /dev/null
+++ b/gfx/layers/wr/WebRenderLayerManager.h
@@ -0,0 +1,282 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GFX_WEBRENDERLAYERMANAGER_H
+#define GFX_WEBRENDERLAYERMANAGER_H
+
+#include <cstddef> // for size_t
+#include <cstdint> // for uint32_t, int32_t, INT32_MAX
+#include <string> // for string
+#include "Units.h" // for LayoutDeviceIntSize
+#include "mozilla/AlreadyAddRefed.h" // for already_AddRefed
+#include "mozilla/Assertions.h" // for AssertionConditionType, MOZ_ASSERT, MOZ_ASSERT_HELPER2
+#include "mozilla/Attributes.h" // for MOZ_NON_OWNING_REF
+#include "mozilla/RefPtr.h" // for RefPtr
+#include "mozilla/StaticPrefs_apz.h" // for apz_test_logging_enabled
+#include "mozilla/TimeStamp.h" // for TimeStamp
+#include "mozilla/gfx/Point.h" // for IntSize
+#include "mozilla/gfx/Types.h" // for SurfaceFormat
+#include "mozilla/layers/APZTestData.h" // for APZTestData
+#include "mozilla/layers/CompositorTypes.h" // for TextureFactoryIdentifier
+#include "mozilla/layers/DisplayItemCache.h" // for DisplayItemCache
+#include "mozilla/layers/FocusTarget.h" // for FocusTarget
+#include "mozilla/layers/LayersTypes.h" // for TransactionId, LayersBackend, CompositionPayload (ptr only), LayersBackend::...
+#include "mozilla/layers/RenderRootStateManager.h" // for RenderRootStateManager
+#include "mozilla/layers/ScrollableLayerGuid.h" // for ScrollableLayerGuid, ScrollableLayerGuid::ViewID
+#include "mozilla/layers/WebRenderCommandBuilder.h" // for WebRenderCommandBuilder
+#include "mozilla/layers/WebRenderScrollData.h" // for WebRenderScrollData
+#include "WindowRenderer.h"
+#include "nsHashKeys.h" // for nsRefPtrHashKey
+#include "nsRegion.h" // for nsIntRegion
+#include "nsStringFwd.h" // for nsCString, nsAString
+#include "nsTArray.h" // for nsTArray
+#include "nsTHashSet.h"
+
+class gfxContext;
+class nsIWidget;
+
+namespace mozilla {
+
+class nsDisplayList;
+class nsDisplayListBuilder;
+struct ActiveScrolledRoot;
+
+namespace layers {
+
+class CompositorBridgeChild;
+class KnowsCompositor;
+class Layer;
+class PCompositorBridgeChild;
+class WebRenderBridgeChild;
+class WebRenderParentCommand;
+class TransactionIdAllocator;
+class LayerUserData;
+
+class WebRenderLayerManager final : public WindowRenderer {
+ typedef nsTHashSet<RefPtr<WebRenderUserData>> WebRenderUserDataRefTable;
+
+ public:
+ explicit WebRenderLayerManager(nsIWidget* aWidget);
+ bool Initialize(PCompositorBridgeChild* aCBChild, wr::PipelineId aLayersId,
+ TextureFactoryIdentifier* aTextureFactoryIdentifier,
+ nsCString& aError);
+
+ void Destroy() override;
+ bool IsDestroyed() { return mDestroyed; }
+
+ void DoDestroy(bool aIsSync);
+
+ protected:
+ virtual ~WebRenderLayerManager();
+
+ public:
+ KnowsCompositor* AsKnowsCompositor() override;
+ WebRenderLayerManager* AsWebRender() override { return this; }
+ CompositorBridgeChild* GetCompositorBridgeChild() override;
+
+ // WebRender can handle images larger than the max texture size via tiling.
+ int32_t GetMaxTextureSize() const override { return INT32_MAX; }
+
+ bool BeginTransactionWithTarget(gfxContext* aTarget, const nsCString& aURL);
+ bool BeginTransaction(const nsCString& aURL) override;
+ bool EndEmptyTransaction(EndTransactionFlags aFlags = END_DEFAULT) override;
+ void EndTransactionWithoutLayer(nsDisplayList* aDisplayList,
+ nsDisplayListBuilder* aDisplayListBuilder,
+ WrFiltersHolder&& aFilters,
+ WebRenderBackgroundData* aBackground,
+ const double aGeckoDLBuildTime);
+
+ LayersBackend GetBackendType() override { return LayersBackend::LAYERS_WR; }
+ void GetBackendName(nsAString& name) override;
+
+ bool NeedsWidgetInvalidation() override { return false; }
+
+ void SetLayersObserverEpoch(LayersObserverEpoch aEpoch);
+
+ void DidComposite(TransactionId aTransactionId,
+ const mozilla::TimeStamp& aCompositeStart,
+ const mozilla::TimeStamp& aCompositeEnd);
+
+ void ClearCachedResources();
+ void ClearAnimationResources();
+ void UpdateTextureFactoryIdentifier(
+ const TextureFactoryIdentifier& aNewIdentifier);
+ TextureFactoryIdentifier GetTextureFactoryIdentifier();
+
+ void SetTransactionIdAllocator(TransactionIdAllocator* aAllocator);
+ TransactionId GetLastTransactionId();
+
+ void FlushRendering(wr::RenderReasons aReasons) override;
+ void WaitOnTransactionProcessed() override;
+
+ void SendInvalidRegion(const nsIntRegion& aRegion);
+
+ void ScheduleComposite(wr::RenderReasons aReasons);
+
+ void SetNeedsComposite(bool aNeedsComposite) {
+ mNeedsComposite = aNeedsComposite;
+ }
+ bool NeedsComposite() const { return mNeedsComposite; }
+ void SetIsFirstPaint() { mIsFirstPaint = true; }
+ bool GetIsFirstPaint() const { return mIsFirstPaint; }
+ void SetFocusTarget(const FocusTarget& aFocusTarget);
+
+ already_AddRefed<PersistentBufferProvider> CreatePersistentBufferProvider(
+ const gfx::IntSize& aSize, gfx::SurfaceFormat aFormat) override;
+
+ bool AsyncPanZoomEnabled() const;
+
+ // adds an imagekey to a list of keys that will be discarded on the next
+ // transaction or destruction
+ void DiscardImages();
+ void DiscardLocalImages();
+
+ void ClearAsyncAnimations();
+ void WrReleasedImages(const nsTArray<wr::ExternalImageKeyPair>& aPairs);
+
+ WebRenderBridgeChild* WrBridge() const { return mWrChild; }
+
+ // See equivalent function in ClientLayerManager
+ void LogTestDataForCurrentPaint(ScrollableLayerGuid::ViewID aScrollId,
+ const std::string& aKey,
+ const std::string& aValue) {
+ MOZ_ASSERT(StaticPrefs::apz_test_logging_enabled(), "don't call me");
+ mApzTestData.LogTestDataForPaint(mPaintSequenceNumber, aScrollId, aKey,
+ aValue);
+ }
+ void LogAdditionalTestData(const std::string& aKey,
+ const std::string& aValue) {
+ MOZ_ASSERT(StaticPrefs::apz_test_logging_enabled(), "don't call me");
+ mApzTestData.RecordAdditionalData(aKey, aValue);
+ }
+
+ // See equivalent function in ClientLayerManager
+ const APZTestData& GetAPZTestData() const { return mApzTestData; }
+
+ WebRenderCommandBuilder& CommandBuilder() { return mWebRenderCommandBuilder; }
+ WebRenderUserDataRefTable* GetWebRenderUserDataTable() {
+ return mWebRenderCommandBuilder.GetWebRenderUserDataTable();
+ }
+ WebRenderScrollData& GetScrollData() { return mScrollData; }
+
+ void WrUpdated();
+ nsIWidget* GetWidget() { return mWidget; }
+
+ uint32_t StartFrameTimeRecording(int32_t aBufferSize) override;
+ void StopFrameTimeRecording(uint32_t aStartIndex,
+ nsTArray<float>& aFrameIntervals) override;
+
+ RenderRootStateManager* GetRenderRootStateManager() { return &mStateManager; }
+
+ void TakeCompositionPayloads(nsTArray<CompositionPayload>& aPayloads);
+
+ void GetFrameUniformity(FrameUniformityData* aOutData) override;
+
+ void RegisterPayloads(const nsTArray<CompositionPayload>& aPayload) {
+ mPayload.AppendElements(aPayload);
+ MOZ_ASSERT(mPayload.Length() < 10000);
+ }
+
+ static void LayerUserDataDestroy(void* data);
+ /**
+ * This setter can be used anytime. The user data for all keys is
+ * initially null. Ownership pases to the layer manager.
+ */
+ void SetUserData(void* aKey, LayerUserData* aData) {
+ mUserData.Add(static_cast<gfx::UserDataKey*>(aKey), aData,
+ LayerUserDataDestroy);
+ }
+ /**
+ * This can be used anytime. Ownership passes to the caller!
+ */
+ UniquePtr<LayerUserData> RemoveUserData(void* aKey);
+
+ /**
+ * This getter can be used anytime.
+ */
+ bool HasUserData(void* aKey) {
+ return mUserData.Has(static_cast<gfx::UserDataKey*>(aKey));
+ }
+ /**
+ * This getter can be used anytime. Ownership is retained by the layer
+ * manager.
+ */
+ LayerUserData* GetUserData(void* aKey) const {
+ return static_cast<LayerUserData*>(
+ mUserData.Get(static_cast<gfx::UserDataKey*>(aKey)));
+ }
+
+ std::unordered_set<ScrollableLayerGuid::ViewID>
+ ClearPendingScrollInfoUpdate();
+
+#ifdef DEBUG
+ gfxContext* GetTarget() const { return mTarget; }
+#endif
+
+ private:
+ /**
+ * Take a snapshot of the parent context, and copy
+ * it into mTarget.
+ */
+ void MakeSnapshotIfRequired(LayoutDeviceIntSize aSize);
+
+ private:
+ nsIWidget* MOZ_NON_OWNING_REF mWidget;
+
+ RefPtr<WebRenderBridgeChild> mWrChild;
+
+ RefPtr<TransactionIdAllocator> mTransactionIdAllocator;
+ TransactionId mLatestTransactionId;
+
+ gfx::UserData mUserData;
+
+ // This holds the scroll data that we need to send to the compositor for
+ // APZ to do it's job
+ WebRenderScrollData mScrollData;
+
+ bool mNeedsComposite;
+ bool mIsFirstPaint;
+ bool mDestroyed;
+ FocusTarget mFocusTarget;
+
+ // The payload associated with currently pending painting work, for
+ // client layer managers that typically means payload that is part of the
+ // 'upcoming transaction', for HostLayerManagers this typically means
+ // what has been included in received transactions to be presented on the
+ // next composite.
+ // IMPORTANT: Clients should take care to clear this or risk it slowly
+ // growing out of control.
+ nsTArray<CompositionPayload> mPayload;
+
+ // When we're doing a transaction in order to draw to a non-default
+ // target, the layers transaction is only performed in order to send
+ // a PLayers:Update. We save the original non-default target to
+ // mTarget, and then perform the transaction. After the transaction ends,
+ // we send a message to our remote side to capture the actual pixels
+ // being drawn to the default target, and then copy those pixels
+ // back to mTarget.
+ gfxContext* mTarget;
+
+ // See equivalent field in ClientLayerManager
+ uint32_t mPaintSequenceNumber;
+ // See equivalent field in ClientLayerManager
+ APZTestData mApzTestData;
+
+ TimeStamp mTransactionStart;
+ nsCString mURL;
+ WebRenderCommandBuilder mWebRenderCommandBuilder;
+
+ RenderRootStateManager mStateManager;
+ DisplayItemCache mDisplayItemCache;
+ UniquePtr<wr::DisplayListBuilder> mDLBuilder;
+
+ ScrollUpdatesMap mPendingScrollUpdates;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif /* GFX_WEBRENDERLAYERMANAGER_H */
diff --git a/gfx/layers/wr/WebRenderMessageUtils.h b/gfx/layers/wr/WebRenderMessageUtils.h
new file mode 100644
index 0000000000..40edf6676d
--- /dev/null
+++ b/gfx/layers/wr/WebRenderMessageUtils.h
@@ -0,0 +1,217 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GFX_WEBRENDERMESSAGEUTILS_H
+#define GFX_WEBRENDERMESSAGEUTILS_H
+
+#include "chrome/common/ipc_message_utils.h"
+
+#include "ipc/EnumSerializer.h"
+#include "ipc/IPCMessageUtils.h"
+#include "mozilla/webrender/webrender_ffi.h"
+#include "mozilla/webrender/WebRenderTypes.h"
+#include "mozilla/dom/MediaIPCUtils.h"
+
+namespace IPC {
+
+template <>
+struct ParamTraits<mozilla::wr::ByteBuffer> {
+ typedef mozilla::wr::ByteBuffer paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.mLength);
+ aWriter->WriteBytes(aParam.mData, aParam.mLength);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ size_t length;
+ return ReadParam(aReader, &length) && aResult->Allocate(length) &&
+ aReader->ReadBytesInto(aResult->mData, length);
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::wr::ImageDescriptor> {
+ typedef mozilla::wr::ImageDescriptor paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.format);
+ WriteParam(aWriter, aParam.width);
+ WriteParam(aWriter, aParam.height);
+ WriteParam(aWriter, aParam.stride);
+ WriteParam(aWriter, aParam.opacity);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ return ReadParam(aReader, &aResult->format) &&
+ ReadParam(aReader, &aResult->width) &&
+ ReadParam(aReader, &aResult->height) &&
+ ReadParam(aReader, &aResult->stride) &&
+ ReadParam(aReader, &aResult->opacity);
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::wr::GeckoDisplayListType::Tag>
+ : public ContiguousEnumSerializer<
+ mozilla::wr::GeckoDisplayListType::Tag,
+ mozilla::wr::GeckoDisplayListType::Tag::None,
+ mozilla::wr::GeckoDisplayListType::Tag::Sentinel> {};
+
+template <>
+struct ParamTraits<mozilla::wr::GeckoDisplayListType> {
+ typedef mozilla::wr::GeckoDisplayListType paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.tag);
+ switch (aParam.tag) {
+ case mozilla::wr::GeckoDisplayListType::Tag::None:
+ break;
+ case mozilla::wr::GeckoDisplayListType::Tag::Partial:
+ WriteParam(aWriter, aParam.partial._0);
+ break;
+ case mozilla::wr::GeckoDisplayListType::Tag::Full:
+ WriteParam(aWriter, aParam.full._0);
+ break;
+ default:
+ MOZ_RELEASE_ASSERT(false, "bad enum variant");
+ }
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ if (!ReadParam(aReader, &aResult->tag)) {
+ return false;
+ }
+ switch (aResult->tag) {
+ case mozilla::wr::GeckoDisplayListType::Tag::None:
+ return true;
+ case mozilla::wr::GeckoDisplayListType::Tag::Partial:
+ return ReadParam(aReader, &aResult->partial._0);
+ case mozilla::wr::GeckoDisplayListType::Tag::Full:
+ return ReadParam(aReader, &aResult->full._0);
+ default:
+ MOZ_RELEASE_ASSERT(false, "bad enum variant");
+ }
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::wr::BuiltDisplayListDescriptor> {
+ typedef mozilla::wr::BuiltDisplayListDescriptor paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.gecko_display_list_type);
+ WriteParam(aWriter, aParam.builder_start_time);
+ WriteParam(aWriter, aParam.builder_finish_time);
+ WriteParam(aWriter, aParam.send_start_time);
+ WriteParam(aWriter, aParam.total_clip_nodes);
+ WriteParam(aWriter, aParam.total_spatial_nodes);
+ WriteParam(aWriter, aParam.cache_size);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ return ReadParam(aReader, &aResult->gecko_display_list_type) &&
+ ReadParam(aReader, &aResult->builder_start_time) &&
+ ReadParam(aReader, &aResult->builder_finish_time) &&
+ ReadParam(aReader, &aResult->send_start_time) &&
+ ReadParam(aReader, &aResult->total_clip_nodes) &&
+ ReadParam(aReader, &aResult->total_spatial_nodes) &&
+ ReadParam(aReader, &aResult->cache_size);
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::wr::IdNamespace>
+ : public PlainOldDataSerializer<mozilla::wr::IdNamespace> {};
+
+template <>
+struct ParamTraits<mozilla::wr::ImageKey>
+ : public PlainOldDataSerializer<mozilla::wr::ImageKey> {};
+
+template <>
+struct ParamTraits<mozilla::wr::BlobImageKey>
+ : public PlainOldDataSerializer<mozilla::wr::BlobImageKey> {};
+
+template <>
+struct ParamTraits<mozilla::wr::FontKey>
+ : public PlainOldDataSerializer<mozilla::wr::FontKey> {};
+
+template <>
+struct ParamTraits<mozilla::wr::FontInstanceKey>
+ : public PlainOldDataSerializer<mozilla::wr::FontInstanceKey> {};
+
+template <>
+struct ParamTraits<mozilla::wr::FontInstanceOptions>
+ : public PlainOldDataSerializer<mozilla::wr::FontInstanceOptions> {};
+
+template <>
+struct ParamTraits<mozilla::wr::FontInstancePlatformOptions>
+ : public PlainOldDataSerializer<mozilla::wr::FontInstancePlatformOptions> {
+};
+
+template <>
+struct ParamTraits<mozilla::wr::ExternalImageId>
+ : public PlainOldDataSerializer<mozilla::wr::ExternalImageId> {};
+
+template <>
+struct ParamTraits<mozilla::wr::PipelineId>
+ : public PlainOldDataSerializer<mozilla::wr::PipelineId> {};
+
+template <>
+struct ParamTraits<mozilla::wr::ImageFormat>
+ : public ContiguousEnumSerializer<mozilla::wr::ImageFormat,
+ mozilla::wr::ImageFormat::R8,
+ mozilla::wr::ImageFormat::Sentinel> {};
+
+template <>
+struct ParamTraits<mozilla::wr::LayoutSize>
+ : public PlainOldDataSerializer<mozilla::wr::LayoutSize> {};
+
+template <>
+struct ParamTraits<mozilla::wr::LayoutRect>
+ : public PlainOldDataSerializer<mozilla::wr::LayoutRect> {};
+
+template <>
+struct ParamTraits<mozilla::wr::LayoutPoint>
+ : public PlainOldDataSerializer<mozilla::wr::LayoutPoint> {};
+
+template <>
+struct ParamTraits<mozilla::wr::ImageRendering>
+ : public ContiguousEnumSerializer<mozilla::wr::ImageRendering,
+ mozilla::wr::ImageRendering::Auto,
+ mozilla::wr::ImageRendering::Sentinel> {};
+
+template <>
+struct ParamTraits<mozilla::wr::MixBlendMode>
+ : public ContiguousEnumSerializer<mozilla::wr::MixBlendMode,
+ mozilla::wr::MixBlendMode::Normal,
+ mozilla::wr::MixBlendMode::Sentinel> {};
+
+template <>
+struct ParamTraits<mozilla::wr::WebRenderError>
+ : public ContiguousEnumSerializer<mozilla::wr::WebRenderError,
+ mozilla::wr::WebRenderError::INITIALIZE,
+ mozilla::wr::WebRenderError::Sentinel> {};
+
+template <>
+struct ParamTraits<mozilla::wr::MemoryReport>
+ : public PlainOldDataSerializer<mozilla::wr::MemoryReport> {};
+
+template <>
+struct ParamTraits<mozilla::wr::OpacityType>
+ : public PlainOldDataSerializer<mozilla::wr::OpacityType> {};
+
+template <>
+struct ParamTraits<mozilla::wr::ExternalImageKeyPair>
+ : public PlainOldDataSerializer<mozilla::wr::ExternalImageKeyPair> {};
+
+template <>
+struct ParamTraits<mozilla::wr::RenderReasons>
+ : public PlainOldDataSerializer<mozilla::wr::RenderReasons> {};
+
+} // namespace IPC
+
+#endif // GFX_WEBRENDERMESSAGEUTILS_H
diff --git a/gfx/layers/wr/WebRenderScrollData.cpp b/gfx/layers/wr/WebRenderScrollData.cpp
new file mode 100644
index 0000000000..d380d227d2
--- /dev/null
+++ b/gfx/layers/wr/WebRenderScrollData.cpp
@@ -0,0 +1,508 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/layers/WebRenderScrollData.h"
+
+#include <ostream>
+
+#include "Units.h"
+#include "mozilla/layers/LayersMessageUtils.h"
+#include "mozilla/layers/WebRenderLayerManager.h"
+#include "mozilla/ToString.h"
+#include "mozilla/Unused.h"
+#include "nsDisplayList.h"
+#include "nsTArray.h"
+#include "UnitTransforms.h"
+
+namespace mozilla {
+namespace layers {
+
+WebRenderLayerScrollData::WebRenderLayerScrollData()
+ : mDescendantCount(-1),
+ mAncestorTransformId(ScrollableLayerGuid::NULL_SCROLL_ID),
+ mTransformIsPerspective(false),
+ mResolution(1.f),
+ mEventRegionsOverride(EventRegionsOverride::NoOverride),
+ mFixedPositionSides(mozilla::SideBits::eNone),
+ mFixedPosScrollContainerId(ScrollableLayerGuid::NULL_SCROLL_ID),
+ mStickyPosScrollContainerId(ScrollableLayerGuid::NULL_SCROLL_ID) {}
+
+WebRenderLayerScrollData::~WebRenderLayerScrollData() = default;
+
+void WebRenderLayerScrollData::InitializeRoot(int32_t aDescendantCount) {
+ mDescendantCount = aDescendantCount;
+}
+
+void WebRenderLayerScrollData::InitializeForTest(int32_t aDescendantCount) {
+ mDescendantCount = aDescendantCount;
+}
+
+void WebRenderLayerScrollData::Initialize(
+ WebRenderScrollData& aOwner, nsDisplayItem* aItem, int32_t aDescendantCount,
+ const ActiveScrolledRoot* aStopAtAsr,
+ const Maybe<gfx::Matrix4x4>& aAncestorTransform,
+ const ViewID& aAncestorTransformId) {
+ MOZ_ASSERT(aDescendantCount >= 0); // Ensure value is valid
+ MOZ_ASSERT(mDescendantCount ==
+ -1); // Don't allow re-setting an already set value
+ mDescendantCount = aDescendantCount;
+
+#if defined(DEBUG) || defined(MOZ_DUMP_PAINTING)
+ mInitializedFrom = aItem;
+#endif
+
+ MOZ_ASSERT(aItem);
+ aItem->UpdateScrollData(&aOwner, this);
+
+ const ActiveScrolledRoot* asr = aItem->GetActiveScrolledRoot();
+ if (ActiveScrolledRoot::IsAncestor(asr, aStopAtAsr)) {
+ // If the item's ASR is an ancestor of the stop-at ASR, then we don't need
+ // any more metrics information because we'll end up duplicating what the
+ // ancestor WebRenderLayerScrollData already has.
+ asr = nullptr;
+ }
+
+ while (asr && asr != aStopAtAsr) {
+ MOZ_ASSERT(aOwner.GetManager());
+ ScrollableLayerGuid::ViewID scrollId = asr->GetViewId();
+ if (Maybe<size_t> index = aOwner.HasMetadataFor(scrollId)) {
+ mScrollIds.AppendElement(index.ref());
+ } else {
+ Maybe<ScrollMetadata> metadata =
+ asr->mScrollableFrame->ComputeScrollMetadata(
+ aOwner.GetManager(), aItem->Frame(), aItem->ToReferenceFrame());
+ aOwner.GetBuilder()->AddScrollFrameToNotify(asr->mScrollableFrame);
+ if (metadata) {
+ MOZ_ASSERT(metadata->GetMetrics().GetScrollId() == scrollId);
+ mScrollIds.AppendElement(aOwner.AddMetadata(metadata.ref()));
+ } else {
+ MOZ_ASSERT_UNREACHABLE("Expected scroll metadata to be available!");
+ }
+ }
+ asr = asr->mParent;
+ }
+
+#ifdef DEBUG
+ // Sanity check: if we have an ancestor transform, its scroll id should
+ // match one of the scroll metadatas on this node (WebRenderScrollDataWrapper
+ // will then use the ancestor transform at the level of that scroll metadata).
+ // One exception to this is if we have no scroll metadatas, which can happen
+ // if the scroll id of the transform is on an enclosing node.
+ if (aAncestorTransformId != ScrollableLayerGuid::NULL_SCROLL_ID &&
+ !mScrollIds.IsEmpty()) {
+ bool seenAncestorTransformId = false;
+ for (size_t scrollIdIndex : mScrollIds) {
+ if (aAncestorTransformId ==
+ aOwner.GetScrollMetadata(scrollIdIndex).GetMetrics().GetScrollId()) {
+ seenAncestorTransformId = true;
+ }
+ }
+ MOZ_ASSERT(
+ seenAncestorTransformId,
+ "The ancestor transform's view ID should match one of the metrics "
+ "on this node");
+ }
+#endif
+
+ // See the comments on StackingContextHelper::mDeferredTransformItem for an
+ // overview of what deferred transforms are.
+ // aAncestorTransform, if present, is the transform from a deferred transform
+ // item that is an ancestor of |aItem|. We store this transform value
+ // separately from mTransform because in the case where we have multiple
+ // scroll metadata on this layer item, the mAncestorTransform is associated
+ // with the "topmost" scroll metadata, and the mTransform is associated with
+ // the "bottommost" scroll metadata. The code in
+ // WebRenderScrollDataWrapper::GetTransform() is responsible for combining
+ // these transforms and exposing them appropriately. Also, we don't save the
+ // ancestor transform for thumb layers, because those are a special case in
+ // APZ; we need to keep the ancestor transform for the scrollable content that
+ // the thumb scrolls, but not for the thumb itself, as it will result in
+ // incorrect visual positioning of the thumb.
+ if (aAncestorTransform &&
+ mScrollbarData.mScrollbarLayerType != ScrollbarLayerType::Thumb) {
+ mAncestorTransform = *aAncestorTransform;
+ mAncestorTransformId = aAncestorTransformId;
+ }
+}
+
+int32_t WebRenderLayerScrollData::GetDescendantCount() const {
+ MOZ_ASSERT(mDescendantCount >= 0); // check that it was set
+ return mDescendantCount;
+}
+
+size_t WebRenderLayerScrollData::GetScrollMetadataCount() const {
+ return mScrollIds.Length();
+}
+
+void WebRenderLayerScrollData::AppendScrollMetadata(
+ WebRenderScrollData& aOwner, const ScrollMetadata& aData) {
+ mScrollIds.AppendElement(aOwner.AddMetadata(aData));
+}
+
+const ScrollMetadata& WebRenderLayerScrollData::GetScrollMetadata(
+ const WebRenderScrollData& aOwner, size_t aIndex) const {
+ MOZ_ASSERT(aIndex < mScrollIds.Length());
+ return aOwner.GetScrollMetadata(mScrollIds[aIndex]);
+}
+
+ScrollMetadata& WebRenderLayerScrollData::GetScrollMetadataMut(
+ WebRenderScrollData& aOwner, size_t aIndex) {
+ MOZ_ASSERT(aIndex < mScrollIds.Length());
+ return aOwner.GetScrollMetadataMut(mScrollIds[aIndex]);
+}
+
+CSSTransformMatrix WebRenderLayerScrollData::GetTransformTyped() const {
+ return ViewAs<CSSTransformMatrix>(GetTransform());
+}
+
+void WebRenderLayerScrollData::Dump(std::ostream& aOut,
+ const WebRenderScrollData& aOwner) const {
+ aOut << "WebRenderLayerScrollData(" << this
+ << "), descendantCount=" << mDescendantCount;
+#if defined(DEBUG) || defined(MOZ_DUMP_PAINTING)
+ if (mInitializedFrom) {
+ aOut << ", item=" << (void*)mInitializedFrom;
+ }
+#endif
+ if (mAsyncZoomContainerId) {
+ aOut << ", asyncZoomContainer";
+ }
+ for (size_t i = 0; i < mScrollIds.Length(); i++) {
+ aOut << ", metadata" << i << "=" << aOwner.GetScrollMetadata(mScrollIds[i]);
+ }
+ if (!mAncestorTransform.IsIdentity()) {
+ aOut << ", ancestorTransform=" << mAncestorTransform
+ << " (asr=" << mAncestorTransformId << ")";
+ }
+ if (!mTransform.IsIdentity()) {
+ aOut << ", transform=" << mTransform;
+ if (mTransformIsPerspective) {
+ aOut << ", transformIsPerspective";
+ }
+ }
+ if (mResolution != 1.f) {
+ aOut << ", resolution=" << mResolution;
+ }
+ aOut << ", visible=" << mVisibleRegion;
+ if (mReferentId) {
+ aOut << ", refLayersId=" << *mReferentId;
+ }
+ if (mEventRegionsOverride) {
+ aOut << std::hex << ", eventRegionsOverride=0x"
+ << (int)mEventRegionsOverride << std::dec;
+ }
+ if (mScrollbarData.mScrollbarLayerType != ScrollbarLayerType::None) {
+ aOut << ", scrollbarType=" << (int)mScrollbarData.mScrollbarLayerType
+ << std::hex << ", scrollbarAnimationId=0x"
+ << mScrollbarAnimationId.valueOr(0) << std::dec;
+ }
+ if (mFixedPosScrollContainerId != ScrollableLayerGuid::NULL_SCROLL_ID) {
+ aOut << ", fixedContainer=" << mFixedPosScrollContainerId << std::hex
+ << ", fixedAnimation=0x" << mFixedPositionAnimationId.valueOr(0)
+ << ", sideBits=0x" << (int)mFixedPositionSides << std::dec;
+ }
+ if (mStickyPosScrollContainerId != ScrollableLayerGuid::NULL_SCROLL_ID) {
+ aOut << ", stickyContainer=" << mStickyPosScrollContainerId << std::hex
+ << ", stickyAnimation=" << mStickyPositionAnimationId.valueOr(0)
+ << std::dec << ", stickyInner=" << mStickyScrollRangeInner
+ << ", stickyOuter=" << mStickyScrollRangeOuter;
+ }
+}
+
+WebRenderScrollData::WebRenderScrollData()
+ : mManager(nullptr),
+ mBuilder(nullptr),
+ mIsFirstPaint(false),
+ mPaintSequenceNumber(0) {}
+
+WebRenderScrollData::WebRenderScrollData(WebRenderLayerManager* aManager,
+ nsDisplayListBuilder* aBuilder)
+ : mManager(aManager),
+ mBuilder(aBuilder),
+ mIsFirstPaint(false),
+ mPaintSequenceNumber(0) {}
+
+bool WebRenderScrollData::Validate() const {
+ // Attempt to traverse the tree structure encoded by the descendant counts,
+ // validating as we go that everything is within bounds and properly nested.
+ // In addition, check that the traversal visits every node exactly once.
+ std::vector<size_t> visitCounts(mLayerScrollData.Length(), 0);
+ if (mLayerScrollData.Length() > 0) {
+ if (!mLayerScrollData[0].ValidateSubtree(*this, visitCounts, 0)) {
+ return false;
+ }
+ }
+ for (size_t visitCount : visitCounts) {
+ if (visitCount != 1) {
+ return false;
+ }
+ }
+ return true;
+}
+
+WebRenderLayerManager* WebRenderScrollData::GetManager() const {
+ return mManager;
+}
+
+nsDisplayListBuilder* WebRenderScrollData::GetBuilder() const {
+ return mBuilder;
+}
+
+size_t WebRenderScrollData::AddMetadata(const ScrollMetadata& aMetadata) {
+ ScrollableLayerGuid::ViewID scrollId = aMetadata.GetMetrics().GetScrollId();
+ auto p = mScrollIdMap.lookupForAdd(scrollId);
+ if (!p) {
+ // It's a scrollId we hadn't seen before
+ bool ok = mScrollIdMap.add(p, scrollId, mScrollMetadatas.Length());
+ MOZ_RELEASE_ASSERT(ok);
+ mScrollMetadatas.AppendElement(aMetadata);
+ } // else we didn't insert, because it already existed
+ return p->value();
+}
+
+size_t WebRenderScrollData::AddLayerData(WebRenderLayerScrollData&& aData) {
+ mLayerScrollData.AppendElement(std::move(aData));
+ return mLayerScrollData.Length() - 1;
+}
+
+size_t WebRenderScrollData::GetLayerCount() const {
+ return mLayerScrollData.Length();
+}
+
+bool WebRenderLayerScrollData::ValidateSubtree(
+ const WebRenderScrollData& aParent, std::vector<size_t>& aVisitCounts,
+ size_t aCurrentIndex) const {
+ ++aVisitCounts[aCurrentIndex];
+
+ // All scroll ids must be in bounds.
+ for (size_t scrollMetadataIndex : mScrollIds) {
+ if (scrollMetadataIndex >= aParent.mScrollMetadatas.Length()) {
+ return false;
+ }
+ }
+
+ // Descendant count must be nonnegative.
+ if (mDescendantCount < 0) {
+ return false;
+ }
+ size_t descendantCount = static_cast<size_t>(mDescendantCount);
+
+ // Bounds check: for every layer, its index + its mDescendantCount
+ // must be within bounds.
+ if (aCurrentIndex + descendantCount >= aParent.mLayerScrollData.Length()) {
+ return false;
+ }
+
+ // Recurse over our children, accumulating a count of our children
+ // and their descendants as we go.
+ size_t childCount = 0;
+ size_t childDescendantCounts = 0;
+ size_t currentChildIndex = aCurrentIndex + 1;
+ while (currentChildIndex < (aCurrentIndex + descendantCount + 1)) {
+ ++childCount;
+
+ const WebRenderLayerScrollData* currentChild =
+ &aParent.mLayerScrollData[currentChildIndex];
+ childDescendantCounts += currentChild->mDescendantCount;
+ currentChild->ValidateSubtree(aParent, aVisitCounts, currentChildIndex);
+
+ // The current child's descendants come first in the array, and the next
+ // element after that is our next child.
+ currentChildIndex += (currentChild->mDescendantCount + 1);
+ }
+
+ // For a given layer, its descendant count must equal the number of
+ // children + the descendant counts of its children added together.
+ return descendantCount == (childCount + childDescendantCounts);
+}
+
+const WebRenderLayerScrollData* WebRenderScrollData::GetLayerData(
+ size_t aIndex) const {
+ if (aIndex >= mLayerScrollData.Length()) {
+ return nullptr;
+ }
+ return &(mLayerScrollData.ElementAt(aIndex));
+}
+
+WebRenderLayerScrollData* WebRenderScrollData::GetLayerData(size_t aIndex) {
+ if (aIndex >= mLayerScrollData.Length()) {
+ return nullptr;
+ }
+ return &(mLayerScrollData.ElementAt(aIndex));
+}
+
+const ScrollMetadata& WebRenderScrollData::GetScrollMetadata(
+ size_t aIndex) const {
+ MOZ_ASSERT(aIndex < mScrollMetadatas.Length());
+ return mScrollMetadatas[aIndex];
+}
+
+ScrollMetadata& WebRenderScrollData::GetScrollMetadataMut(size_t aIndex) {
+ MOZ_ASSERT(aIndex < mScrollMetadatas.Length());
+ return mScrollMetadatas[aIndex];
+}
+
+Maybe<size_t> WebRenderScrollData::HasMetadataFor(
+ const ScrollableLayerGuid::ViewID& aScrollId) const {
+ auto ptr = mScrollIdMap.lookup(aScrollId);
+ return (ptr ? Some(ptr->value()) : Nothing());
+}
+
+void WebRenderScrollData::SetIsFirstPaint() { mIsFirstPaint = true; }
+
+bool WebRenderScrollData::IsFirstPaint() const { return mIsFirstPaint; }
+
+void WebRenderScrollData::SetPaintSequenceNumber(
+ uint32_t aPaintSequenceNumber) {
+ mPaintSequenceNumber = aPaintSequenceNumber;
+}
+
+uint32_t WebRenderScrollData::GetPaintSequenceNumber() const {
+ return mPaintSequenceNumber;
+}
+
+void WebRenderScrollData::ApplyUpdates(ScrollUpdatesMap&& aUpdates,
+ uint32_t aPaintSequenceNumber) {
+ for (auto it = aUpdates.Iter(); !it.Done(); it.Next()) {
+ if (Maybe<size_t> index = HasMetadataFor(it.Key())) {
+ mScrollMetadatas[*index].UpdatePendingScrollInfo(std::move(it.Data()));
+ }
+ }
+ mPaintSequenceNumber = aPaintSequenceNumber;
+}
+
+void WebRenderScrollData::DumpSubtree(std::ostream& aOut, size_t aIndex,
+ const std::string& aIndent) const {
+ aOut << aIndent;
+ mLayerScrollData.ElementAt(aIndex).Dump(aOut, *this);
+ aOut << std::endl;
+
+ int32_t descendants = mLayerScrollData.ElementAt(aIndex).GetDescendantCount();
+ if (descendants == 0) {
+ return;
+ }
+
+ // Build a stack of indices at which this aIndex's children live. We do
+ // this because we want to dump them first-to-last but they are stored
+ // last-to-first.
+ std::stack<size_t> childIndices;
+ size_t childIndex = aIndex + 1;
+ while (descendants > 0) {
+ childIndices.push(childIndex);
+ // "1" for the child itelf, plus whatever descendants it has
+ int32_t subtreeSize =
+ 1 + mLayerScrollData.ElementAt(childIndex).GetDescendantCount();
+ childIndex += subtreeSize;
+ descendants -= subtreeSize;
+ MOZ_ASSERT(descendants >= 0);
+ }
+
+ std::string indent = aIndent + " ";
+ while (!childIndices.empty()) {
+ size_t child = childIndices.top();
+ childIndices.pop();
+ DumpSubtree(aOut, child, indent);
+ }
+}
+
+std::ostream& operator<<(std::ostream& aOut, const WebRenderScrollData& aData) {
+ aOut << "--- WebRenderScrollData (firstPaint=" << aData.mIsFirstPaint
+ << ") ---" << std::endl;
+
+ if (aData.mLayerScrollData.Length() > 0) {
+ aData.DumpSubtree(aOut, 0, std::string());
+ }
+ return aOut;
+}
+
+bool WebRenderScrollData::RepopulateMap() {
+ MOZ_ASSERT(mScrollIdMap.empty());
+ for (size_t i = 0; i < mScrollMetadatas.Length(); i++) {
+ ScrollableLayerGuid::ViewID scrollId =
+ mScrollMetadatas[i].GetMetrics().GetScrollId();
+ bool ok = mScrollIdMap.putNew(scrollId, i);
+ MOZ_RELEASE_ASSERT(ok);
+ }
+ return true;
+}
+
+} // namespace layers
+} // namespace mozilla
+
+namespace IPC {
+
+void ParamTraits<mozilla::layers::WebRenderLayerScrollData>::Write(
+ MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.mDescendantCount);
+ WriteParam(aWriter, aParam.mScrollIds);
+ WriteParam(aWriter, aParam.mAncestorTransform);
+ WriteParam(aWriter, aParam.mAncestorTransformId);
+ WriteParam(aWriter, aParam.mTransform);
+ WriteParam(aWriter, aParam.mTransformIsPerspective);
+ WriteParam(aWriter, aParam.mResolution);
+ WriteParam(aWriter, aParam.mVisibleRegion);
+ WriteParam(aWriter, aParam.mRemoteDocumentSize);
+ WriteParam(aWriter, aParam.mReferentId);
+ WriteParam(aWriter, aParam.mEventRegionsOverride);
+ WriteParam(aWriter, aParam.mScrollbarData);
+ WriteParam(aWriter, aParam.mScrollbarAnimationId);
+ WriteParam(aWriter, aParam.mFixedPositionAnimationId);
+ WriteParam(aWriter, aParam.mFixedPositionSides);
+ WriteParam(aWriter, aParam.mFixedPosScrollContainerId);
+ WriteParam(aWriter, aParam.mStickyPosScrollContainerId);
+ WriteParam(aWriter, aParam.mStickyScrollRangeOuter);
+ WriteParam(aWriter, aParam.mStickyScrollRangeInner);
+ WriteParam(aWriter, aParam.mStickyPositionAnimationId);
+ WriteParam(aWriter, aParam.mZoomAnimationId);
+ WriteParam(aWriter, aParam.mAsyncZoomContainerId);
+ // Do not write |mInitializedFrom|, the pointer wouldn't be valid
+ // on the compositor side.
+}
+
+bool ParamTraits<mozilla::layers::WebRenderLayerScrollData>::Read(
+ MessageReader* aReader, paramType* aResult) {
+ return ReadParam(aReader, &aResult->mDescendantCount) &&
+ ReadParam(aReader, &aResult->mScrollIds) &&
+ ReadParam(aReader, &aResult->mAncestorTransform) &&
+ ReadParam(aReader, &aResult->mAncestorTransformId) &&
+ ReadParam(aReader, &aResult->mTransform) &&
+ ReadParam(aReader, &aResult->mTransformIsPerspective) &&
+ ReadParam(aReader, &aResult->mResolution) &&
+ ReadParam(aReader, &aResult->mVisibleRegion) &&
+ ReadParam(aReader, &aResult->mRemoteDocumentSize) &&
+ ReadParam(aReader, &aResult->mReferentId) &&
+ ReadParam(aReader, &aResult->mEventRegionsOverride) &&
+ ReadParam(aReader, &aResult->mScrollbarData) &&
+ ReadParam(aReader, &aResult->mScrollbarAnimationId) &&
+ ReadParam(aReader, &aResult->mFixedPositionAnimationId) &&
+ ReadParam(aReader, &aResult->mFixedPositionSides) &&
+ ReadParam(aReader, &aResult->mFixedPosScrollContainerId) &&
+ ReadParam(aReader, &aResult->mStickyPosScrollContainerId) &&
+ ReadParam(aReader, &aResult->mStickyScrollRangeOuter) &&
+ ReadParam(aReader, &aResult->mStickyScrollRangeInner) &&
+ ReadParam(aReader, &aResult->mStickyPositionAnimationId) &&
+ ReadParam(aReader, &aResult->mZoomAnimationId) &&
+ ReadParam(aReader, &aResult->mAsyncZoomContainerId);
+}
+
+void ParamTraits<mozilla::layers::WebRenderScrollData>::Write(
+ MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.mScrollMetadatas);
+ WriteParam(aWriter, aParam.mLayerScrollData);
+ WriteParam(aWriter, aParam.mIsFirstPaint);
+ WriteParam(aWriter, aParam.mPaintSequenceNumber);
+}
+
+bool ParamTraits<mozilla::layers::WebRenderScrollData>::Read(
+ MessageReader* aReader, paramType* aResult) {
+ return ReadParam(aReader, &aResult->mScrollMetadatas) &&
+ ReadParam(aReader, &aResult->mLayerScrollData) &&
+ ReadParam(aReader, &aResult->mIsFirstPaint) &&
+ ReadParam(aReader, &aResult->mPaintSequenceNumber) &&
+ aResult->RepopulateMap();
+}
+
+} // namespace IPC
diff --git a/gfx/layers/wr/WebRenderScrollData.h b/gfx/layers/wr/WebRenderScrollData.h
new file mode 100644
index 0000000000..1df766a3bf
--- /dev/null
+++ b/gfx/layers/wr/WebRenderScrollData.h
@@ -0,0 +1,363 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GFX_WEBRENDERSCROLLDATA_H
+#define GFX_WEBRENDERSCROLLDATA_H
+
+#include <map>
+#include <iosfwd>
+
+#include "chrome/common/ipc_message_utils.h"
+#include "FrameMetrics.h"
+#include "ipc/IPCMessageUtils.h"
+#include "LayersTypes.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/GfxMessageUtils.h"
+#include "mozilla/layers/FocusTarget.h"
+#include "mozilla/layers/ScrollbarData.h"
+#include "mozilla/layers/WebRenderMessageUtils.h"
+#include "mozilla/webrender/WebRenderTypes.h"
+#include "mozilla/HashTable.h"
+#include "mozilla/Maybe.h"
+#include "nsTArrayForwardDeclare.h"
+
+namespace mozilla {
+
+class nsDisplayItem;
+class nsDisplayListBuilder;
+struct ActiveScrolledRoot;
+
+namespace layers {
+
+class APZTestAccess;
+class Layer;
+class WebRenderLayerManager;
+class WebRenderScrollData;
+
+// Data needed by APZ, per layer. One instance of this class is created for
+// each layer in the layer tree and sent over PWebRenderBridge to the APZ code.
+// Each WebRenderLayerScrollData is conceptually associated with an "owning"
+// WebRenderScrollData.
+class WebRenderLayerScrollData final {
+ public:
+ WebRenderLayerScrollData(); // needed for IPC purposes
+ WebRenderLayerScrollData(WebRenderLayerScrollData&& aOther) = default;
+ ~WebRenderLayerScrollData();
+
+ using ViewID = ScrollableLayerGuid::ViewID;
+
+ // Helper function for WebRenderScrollData::Validate().
+ bool ValidateSubtree(const WebRenderScrollData& aParent,
+ std::vector<size_t>& aVisitCounts,
+ size_t aCurrentIndex) const;
+
+ void InitializeRoot(int32_t aDescendantCount);
+ void Initialize(WebRenderScrollData& aOwner, nsDisplayItem* aItem,
+ int32_t aDescendantCount,
+ const ActiveScrolledRoot* aStopAtAsr,
+ const Maybe<gfx::Matrix4x4>& aAncestorTransform,
+ const ViewID& aAncestorTransformId);
+
+ int32_t GetDescendantCount() const;
+ size_t GetScrollMetadataCount() const;
+
+ void AppendScrollMetadata(WebRenderScrollData& aOwner,
+ const ScrollMetadata& aData);
+ // Return the ScrollMetadata object that used to be on the original Layer
+ // at the given index. Since we deduplicate the ScrollMetadata objects into
+ // the array in the owning WebRenderScrollData object, we need to be passed
+ // in a reference to that owner as well.
+ const ScrollMetadata& GetScrollMetadata(const WebRenderScrollData& aOwner,
+ size_t aIndex) const;
+
+ gfx::Matrix4x4 GetAncestorTransform() const { return mAncestorTransform; }
+ ViewID GetAncestorTransformId() const { return mAncestorTransformId; }
+ void SetTransform(const gfx::Matrix4x4& aTransform) {
+ mTransform = aTransform;
+ }
+ gfx::Matrix4x4 GetTransform() const { return mTransform; }
+ CSSTransformMatrix GetTransformTyped() const;
+ void SetTransformIsPerspective(bool aTransformIsPerspective) {
+ mTransformIsPerspective = aTransformIsPerspective;
+ }
+ bool GetTransformIsPerspective() const { return mTransformIsPerspective; }
+ void SetResolution(float aResolution) { mResolution = aResolution; }
+ float GetResolution() const { return mResolution; }
+
+ void SetEventRegionsOverride(const EventRegionsOverride& aOverride) {
+ mEventRegionsOverride = aOverride;
+ }
+ EventRegionsOverride GetEventRegionsOverride() const {
+ return mEventRegionsOverride;
+ }
+
+ void SetVisibleRegion(const LayerIntRegion& aRegion) {
+ mVisibleRegion = aRegion;
+ }
+ const LayerIntRegion& GetVisibleRegion() const { return mVisibleRegion; }
+ void SetRemoteDocumentSize(const LayerIntSize& aRemoteDocumentSize) {
+ mRemoteDocumentSize = aRemoteDocumentSize;
+ }
+ const LayerIntSize& GetRemoteDocumentSize() const {
+ return mRemoteDocumentSize;
+ }
+ void SetReferentId(LayersId aReferentId) { mReferentId = Some(aReferentId); }
+ Maybe<LayersId> GetReferentId() const { return mReferentId; }
+
+ void SetScrollbarData(const ScrollbarData& aData) { mScrollbarData = aData; }
+ const ScrollbarData& GetScrollbarData() const { return mScrollbarData; }
+ void SetScrollbarAnimationId(const uint64_t& aId) {
+ mScrollbarAnimationId = Some(aId);
+ }
+ Maybe<uint64_t> GetScrollbarAnimationId() const {
+ return mScrollbarAnimationId;
+ }
+
+ void SetFixedPositionAnimationId(const uint64_t& aId) {
+ mFixedPositionAnimationId = Some(aId);
+ }
+ Maybe<uint64_t> GetFixedPositionAnimationId() const {
+ return mFixedPositionAnimationId;
+ }
+
+ void SetFixedPositionSides(const SideBits& aSideBits) {
+ mFixedPositionSides = aSideBits;
+ }
+ SideBits GetFixedPositionSides() const { return mFixedPositionSides; }
+
+ void SetFixedPositionScrollContainerId(ViewID aId) {
+ mFixedPosScrollContainerId = aId;
+ }
+ ViewID GetFixedPositionScrollContainerId() const {
+ return mFixedPosScrollContainerId;
+ }
+
+ void SetStickyPositionScrollContainerId(ViewID aId) {
+ mStickyPosScrollContainerId = aId;
+ }
+ ViewID GetStickyPositionScrollContainerId() const {
+ return mStickyPosScrollContainerId;
+ }
+
+ void SetStickyScrollRangeOuter(const LayerRectAbsolute& scrollRange) {
+ mStickyScrollRangeOuter = scrollRange;
+ }
+ const LayerRectAbsolute& GetStickyScrollRangeOuter() const {
+ return mStickyScrollRangeOuter;
+ }
+
+ void SetStickyScrollRangeInner(const LayerRectAbsolute& scrollRange) {
+ mStickyScrollRangeInner = scrollRange;
+ }
+ const LayerRectAbsolute& GetStickyScrollRangeInner() const {
+ return mStickyScrollRangeInner;
+ }
+
+ void SetStickyPositionAnimationId(const uint64_t& aId) {
+ mStickyPositionAnimationId = Some(aId);
+ }
+ Maybe<uint64_t> GetStickyPositionAnimationId() const {
+ return mStickyPositionAnimationId;
+ }
+
+ void SetZoomAnimationId(const uint64_t& aId) { mZoomAnimationId = Some(aId); }
+ Maybe<uint64_t> GetZoomAnimationId() const { return mZoomAnimationId; }
+
+ void SetAsyncZoomContainerId(const ViewID& aId) {
+ mAsyncZoomContainerId = Some(aId);
+ }
+ Maybe<ViewID> GetAsyncZoomContainerId() const {
+ return mAsyncZoomContainerId;
+ }
+
+ void Dump(std::ostream& aOut, const WebRenderScrollData& aOwner) const;
+
+ friend struct IPC::ParamTraits<WebRenderLayerScrollData>;
+
+ private:
+ // For test use only
+ friend class APZTestAccess;
+
+ // For use by GTests in building WebRenderLayerScrollData trees.
+ // GTests don't have a display list so they can't use Initialize().
+ void InitializeForTest(int32_t aDescendantCount);
+
+ ScrollMetadata& GetScrollMetadataMut(WebRenderScrollData& aOwner,
+ size_t aIndex);
+
+ private:
+ // The number of descendants this layer has (not including the layer itself).
+ // This is needed to reconstruct the depth-first layer tree traversal
+ // efficiently. Leaf layers should always have 0 descendants.
+ int32_t mDescendantCount;
+
+ // Handles to the ScrollMetadata objects that were on this layer. The values
+ // stored in this array are indices into the owning WebRenderScrollData's
+ // mScrollMetadatas array. This indirection is used to deduplicate the
+ // ScrollMetadata objects, since there is usually heavy duplication of them
+ // within a layer tree.
+ CopyableTArray<size_t> mScrollIds;
+
+ // Various data that we collect from the Layer in Initialize(), serialize
+ // over IPC, and use on the parent side in APZ.
+
+ gfx::Matrix4x4 mAncestorTransform;
+ ViewID mAncestorTransformId;
+ gfx::Matrix4x4 mTransform;
+ bool mTransformIsPerspective;
+ float mResolution;
+ LayerIntRegion mVisibleRegion;
+ // The remote documents only need their size because their origin is always
+ // (0, 0).
+ LayerIntSize mRemoteDocumentSize;
+ Maybe<LayersId> mReferentId;
+ EventRegionsOverride mEventRegionsOverride;
+ ScrollbarData mScrollbarData;
+ Maybe<uint64_t> mScrollbarAnimationId;
+ Maybe<uint64_t> mFixedPositionAnimationId;
+ SideBits mFixedPositionSides;
+ ViewID mFixedPosScrollContainerId;
+ ViewID mStickyPosScrollContainerId;
+ LayerRectAbsolute mStickyScrollRangeOuter;
+ LayerRectAbsolute mStickyScrollRangeInner;
+ Maybe<uint64_t> mStickyPositionAnimationId;
+ Maybe<uint64_t> mZoomAnimationId;
+ Maybe<ViewID> mAsyncZoomContainerId;
+
+#if defined(DEBUG) || defined(MOZ_DUMP_PAINTING)
+ // The display item for which this layer was built.
+ // This is only set on the content side.
+ nsDisplayItem* mInitializedFrom = nullptr;
+#endif
+};
+
+// Data needed by APZ, for the whole layer tree. One instance of this class
+// is created for each transaction sent over PWebRenderBridge. It is populated
+// with information from the WebRender layer tree on the client side and the
+// information is used by APZ on the parent side.
+class WebRenderScrollData {
+ public:
+ WebRenderScrollData();
+ explicit WebRenderScrollData(WebRenderLayerManager* aManager,
+ nsDisplayListBuilder* aBuilder);
+ WebRenderScrollData(WebRenderScrollData&& aOther) = default;
+ WebRenderScrollData& operator=(WebRenderScrollData&& aOther) = default;
+ virtual ~WebRenderScrollData() = default;
+
+ // Validate that the scroll data is well-formed, and particularly that
+ // |mLayerScrollData| encodes a valid tree. This is necessary because
+ // the data can be sent over IPC from a less-trusted content process.
+ bool Validate() const;
+
+ WebRenderLayerManager* GetManager() const;
+
+ nsDisplayListBuilder* GetBuilder() const;
+
+ // Add the given ScrollMetadata if it doesn't already exist. Return an index
+ // that can be used to look up the metadata later.
+ size_t AddMetadata(const ScrollMetadata& aMetadata);
+ // Add the provided WebRenderLayerScrollData and return the index that can
+ // be used to look it up via GetLayerData.
+ size_t AddLayerData(WebRenderLayerScrollData&& aData);
+
+ size_t GetLayerCount() const;
+
+ // Return a pointer to the scroll data at the given index. Use with caution,
+ // as the pointer may be invalidated if this WebRenderScrollData is mutated.
+ const WebRenderLayerScrollData* GetLayerData(size_t aIndex) const;
+ WebRenderLayerScrollData* GetLayerData(size_t aIndex);
+
+ const ScrollMetadata& GetScrollMetadata(size_t aIndex) const;
+ Maybe<size_t> HasMetadataFor(
+ const ScrollableLayerGuid::ViewID& aScrollId) const;
+
+ void SetIsFirstPaint();
+ bool IsFirstPaint() const;
+ void SetPaintSequenceNumber(uint32_t aPaintSequenceNumber);
+ uint32_t GetPaintSequenceNumber() const;
+
+ void ApplyUpdates(ScrollUpdatesMap&& aUpdates, uint32_t aPaintSequenceNumber);
+
+ friend struct IPC::ParamTraits<WebRenderScrollData>;
+
+ friend std::ostream& operator<<(std::ostream& aOut,
+ const WebRenderScrollData& aData);
+
+ private:
+ // For test use only.
+ friend class WebRenderLayerScrollData;
+ ScrollMetadata& GetScrollMetadataMut(size_t aIndex);
+
+ private:
+ // This is called by the ParamTraits implementation to rebuild mScrollIdMap
+ // based on mScrollMetadatas
+ bool RepopulateMap();
+
+ // This is a helper for the dumping code
+ void DumpSubtree(std::ostream& aOut, size_t aIndex,
+ const std::string& aIndent) const;
+
+ private:
+ // Pointer back to the layer manager; if this is non-null, it will always be
+ // valid, because the WebRenderLayerManager that created |this| will
+ // outlive |this|.
+ WebRenderLayerManager* MOZ_NON_OWNING_REF mManager;
+
+ // Pointer to the display list builder; if this is non-null, it will always be
+ // valid, because the nsDisplayListBuilder that created the layer manager will
+ // outlive |this|.
+ nsDisplayListBuilder* MOZ_NON_OWNING_REF mBuilder;
+
+ // Internal data structure used to maintain uniqueness of mScrollMetadatas.
+ // This is not serialized/deserialized over IPC, but it is rebuilt on the
+ // parent side when mScrollMetadatas is deserialized. So it should always be
+ // valid on both the child and parent.
+ // The key into this map is the scrollId of a ScrollMetadata, and the value is
+ // an index into the mScrollMetadatas array.
+ HashMap<ScrollableLayerGuid::ViewID, size_t> mScrollIdMap;
+
+ // A list of all the unique ScrollMetadata objects from the layer tree. Each
+ // ScrollMetadata in this list must have a unique scroll id.
+ nsTArray<ScrollMetadata> mScrollMetadatas;
+
+ // A list of per-layer scroll data objects, generated via a depth-first,
+ // pre-order, last-to-first traversal of the layer tree (i.e. a recursive
+ // traversal where a node N first pushes itself, followed by its children in
+ // last-to-first order). Each layer's scroll data object knows how many
+ // descendants that layer had, which allows reconstructing the traversal on
+ // the other side.
+ nsTArray<WebRenderLayerScrollData> mLayerScrollData;
+
+ bool mIsFirstPaint;
+ uint32_t mPaintSequenceNumber;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+namespace IPC {
+
+template <>
+struct ParamTraits<mozilla::layers::WebRenderLayerScrollData> {
+ typedef mozilla::layers::WebRenderLayerScrollData paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam);
+
+ static bool Read(MessageReader* aReader, paramType* aResult);
+};
+
+template <>
+struct ParamTraits<mozilla::layers::WebRenderScrollData> {
+ typedef mozilla::layers::WebRenderScrollData paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam);
+
+ static bool Read(MessageReader* aReader, paramType* aResult);
+};
+
+} // namespace IPC
+
+#endif /* GFX_WEBRENDERSCROLLDATA_H */
diff --git a/gfx/layers/wr/WebRenderScrollDataWrapper.h b/gfx/layers/wr/WebRenderScrollDataWrapper.h
new file mode 100644
index 0000000000..9cbec76371
--- /dev/null
+++ b/gfx/layers/wr/WebRenderScrollDataWrapper.h
@@ -0,0 +1,542 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GFX_WEBRENDERSCROLLDATAWRAPPER_H
+#define GFX_WEBRENDERSCROLLDATAWRAPPER_H
+
+#include "FrameMetrics.h"
+#include "mozilla/layers/APZUpdater.h"
+#include "mozilla/layers/CompositorBridgeParent.h"
+#include "mozilla/layers/WebRenderBridgeParent.h"
+#include "mozilla/layers/WebRenderScrollData.h"
+
+namespace mozilla {
+namespace layers {
+
+/**
+ * A wrapper class around a target WebRenderLayerScrollData (henceforth,
+ * "layer") that allows user code to walk through the ScrollMetadata objects
+ * on the layer the same way it would walk through a layer tree.
+ * Consider the following layer tree:
+ *
+ * +---+
+ * | A |
+ * +---+
+ * / | \
+ * / | \
+ * / | \
+ * +---+ +-----+ +---+
+ * | B | | C | | D |
+ * +---+ +-----+ +---+
+ * | SMn |
+ * | . |
+ * | . |
+ * | . |
+ * | SM1 |
+ * | SM0 |
+ * +-----+
+ * / \
+ * / \
+ * +---+ +---+
+ * | E | | F |
+ * +---+ +---+
+ *
+ * In this layer tree, there are six layers with A being the root and B,D,E,F
+ * being leaf nodes. Layer C is in the middle and has n+1 ScrollMetadata,
+ * labelled SM0...SMn. SM0 is the ScrollMetadata you get by calling
+ * c->GetScrollMetadata(0) and SMn is the ScrollMetadata you can obtain by
+ * calling c->GetScrollMetadata(c->GetScrollMetadataCount() - 1). This layer
+ * tree is conceptually equivalent to this one below:
+ *
+ * +---+
+ * | A |
+ * +---+
+ * / | \
+ * / | \
+ * / | \
+ * +---+ +-----+ +---+
+ * | B | | Cn | | D |
+ * +---+ +-----+ +---+
+ * |
+ * .
+ * .
+ * .
+ * |
+ * +-----+
+ * | C1 |
+ * +-----+
+ * |
+ * +-----+
+ * | C0 |
+ * +-----+
+ * / \
+ * / \
+ * +---+ +---+
+ * | E | | F |
+ * +---+ +---+
+ *
+ * In this layer tree, the layer C has been expanded into a stack of layers
+ * C1...Cn, where C1 has ScrollMetadata SM1 and Cn has ScrollMetdata Fn.
+ *
+ * The WebRenderScrollDataWrapper class allows client code to treat the first
+ * layer tree as though it were the second. That is, instead of client code
+ * having to iterate through the ScrollMetadata objects directly, it can use a
+ * WebRenderScrollDataWrapper to encapsulate that aspect of the layer tree and
+ * just walk the tree as if it were a stack of layers.
+ *
+ * The functions on this class do different things depending on which
+ * simulated layer is being wrapped. For example, if the
+ * WebRenderScrollDataWrapper is pretending to be C0, the GetPrevSibling()
+ * function will return null even though the underlying layer C does actually
+ * have a prev sibling. The WebRenderScrollDataWrapper pretending to be Cn will
+ * return B as the prev sibling.
+ *
+ * Implementation notes:
+ *
+ * The AtTopLayer() and AtBottomLayer() functions in this class refer to
+ * Cn and C0 in the second layer tree above; that is, they are predicates
+ * to test if the wrapper is simulating the topmost or bottommost layer, as
+ * those can have special behaviour.
+ *
+ * It is possible to wrap a nullptr in a WebRenderScrollDataWrapper, in which
+ * case the IsValid() function will return false. This is required to allow
+ * WebRenderScrollDataWrapper to be a MOZ_STACK_CLASS (desirable because it is
+ * used in loops and recursion).
+ *
+ * This class purposely does not expose the wrapped layer directly to avoid
+ * user code from accidentally calling functions directly on it. Instead
+ * any necessary functions should be wrapped in this class. It does expose
+ * the wrapped layer as a void* for printf purposes.
+ *
+ * The implementation may look like it special-cases mIndex == 0 and/or
+ * GetScrollMetadataCount() == 0. This is an artifact of the fact that both
+ * mIndex and GetScrollMetadataCount() are uint32_t and GetScrollMetadataCount()
+ * can return 0 but mIndex cannot store -1. This seems better than the
+ * alternative of making mIndex a int32_t that can store -1, but then having
+ * to cast to uint32_t all over the place.
+ *
+ * Note that WebRenderLayerScrollData objects are owned by WebRenderScrollData,
+ * which stores them in a flattened representation. The field mData,
+ * mLayerIndex, and mContainingSubtreeIndex are used to move around the "layers"
+ * given the flattened representation. The mMetadataIndex is used to move around
+ * the ScrollMetadata within a single layer.
+ *
+ * One important note here is that this class holds a pointer to the "owning"
+ * WebRenderScrollData. The caller must ensure that this class does not outlive
+ * the owning WebRenderScrollData, or this may result in use-after-free errors.
+ * This class being declared a MOZ_STACK_CLASS should help with that.
+ */
+class MOZ_STACK_CLASS WebRenderScrollDataWrapper final {
+ public:
+ // Basic constructor for external callers. Starts the walker at the root of
+ // the tree.
+ explicit WebRenderScrollDataWrapper(
+ const APZUpdater& aUpdater, const WebRenderScrollData* aData = nullptr)
+ : mUpdater(&aUpdater),
+ mData(aData),
+ mLayerIndex(0),
+ mContainingSubtreeLastIndex(0),
+ mLayer(nullptr),
+ mMetadataIndex(0) {
+ if (!mData) {
+ return;
+ }
+ mLayer = mData->GetLayerData(mLayerIndex);
+ if (!mLayer) {
+ return;
+ }
+
+ // sanity check on the data
+ MOZ_ASSERT(mData->GetLayerCount() ==
+ (size_t)(1 + mLayer->GetDescendantCount()));
+ mContainingSubtreeLastIndex = mData->GetLayerCount();
+
+ // See documentation in LayerMetricsWrapper.h about this. mMetadataIndex
+ // in this class is equivalent to mIndex in that class.
+ mMetadataIndex = mLayer->GetScrollMetadataCount();
+ if (mMetadataIndex > 0) {
+ mMetadataIndex--;
+ }
+ }
+
+ private:
+ // Internal constructor for walking from one WebRenderLayerScrollData to
+ // another. In this case we need to recompute the mMetadataIndex to be the
+ // "topmost" scroll metadata on the new layer.
+ WebRenderScrollDataWrapper(const APZUpdater* aUpdater,
+ const WebRenderScrollData* aData,
+ size_t aLayerIndex,
+ size_t aContainingSubtreeLastIndex)
+ : mUpdater(aUpdater),
+ mData(aData),
+ mLayerIndex(aLayerIndex),
+ mContainingSubtreeLastIndex(aContainingSubtreeLastIndex),
+ mLayer(nullptr),
+ mMetadataIndex(0) {
+ MOZ_ASSERT(mData);
+ mLayer = mData->GetLayerData(mLayerIndex);
+ MOZ_ASSERT(mLayer);
+
+ // See documentation in LayerMetricsWrapper.h about this. mMetadataIndex
+ // in this class is equivalent to mIndex in that class.
+ mMetadataIndex = mLayer->GetScrollMetadataCount();
+ if (mMetadataIndex > 0) {
+ mMetadataIndex--;
+ }
+ }
+
+ // Internal constructor for walking from one metadata to another metadata on
+ // the same WebRenderLayerScrollData.
+ WebRenderScrollDataWrapper(const APZUpdater* aUpdater,
+ const WebRenderScrollData* aData,
+ size_t aLayerIndex,
+ size_t aContainingSubtreeLastIndex,
+ const WebRenderLayerScrollData* aLayer,
+ uint32_t aMetadataIndex)
+ : mUpdater(aUpdater),
+ mData(aData),
+ mLayerIndex(aLayerIndex),
+ mContainingSubtreeLastIndex(aContainingSubtreeLastIndex),
+ mLayer(aLayer),
+ mMetadataIndex(aMetadataIndex) {
+ MOZ_ASSERT(mData);
+ MOZ_ASSERT(mLayer);
+ MOZ_ASSERT(mLayer == mData->GetLayerData(mLayerIndex));
+ MOZ_ASSERT(mMetadataIndex == 0 ||
+ mMetadataIndex < mLayer->GetScrollMetadataCount());
+ }
+
+ public:
+ bool IsValid() const { return mLayer != nullptr; }
+
+ explicit operator bool() const { return IsValid(); }
+
+ WebRenderScrollDataWrapper GetLastChild() const {
+ MOZ_ASSERT(IsValid());
+
+ if (!AtBottomLayer()) {
+ // If we're still walking around in the virtual container layers created
+ // by the ScrollMetadata array, we just need to update the metadata index
+ // and that's it.
+ return WebRenderScrollDataWrapper(mUpdater, mData, mLayerIndex,
+ mContainingSubtreeLastIndex, mLayer,
+ mMetadataIndex - 1);
+ }
+
+ // Otherwise, we need to walk to a different WebRenderLayerScrollData in
+ // mData.
+
+ // Since mData contains the layer in depth-first, last-to-first order,
+ // the index after mLayerIndex must be mLayerIndex's last child, if it
+ // has any children (indicated by GetDescendantCount() > 0). Furthermore
+ // we compute the first index outside the subtree rooted at this node
+ // (in |subtreeLastIndex|) and pass that in to the child wrapper to use as
+ // its mContainingSubtreeLastIndex.
+ if (mLayer->GetDescendantCount() > 0) {
+ size_t prevSiblingIndex = mLayerIndex + 1 + mLayer->GetDescendantCount();
+ // TODO(botond): Replace the min() with just prevSiblingIndex (which
+ // should be <= mContainingSubtreeLastIndex).
+ MOZ_ASSERT(prevSiblingIndex <= mContainingSubtreeLastIndex);
+ size_t subtreeLastIndex =
+ std::min(mContainingSubtreeLastIndex, prevSiblingIndex);
+ return WebRenderScrollDataWrapper(mUpdater, mData, mLayerIndex + 1,
+ subtreeLastIndex);
+ }
+
+ // We've run out of descendants. But! If the original layer was a RefLayer,
+ // then it connects to another layer tree and we need to traverse that too.
+ // So return a WebRenderScrollDataWrapper for the root of the child layer
+ // tree.
+ if (mLayer->GetReferentId()) {
+ return WebRenderScrollDataWrapper(
+ *mUpdater, mUpdater->GetScrollData(*mLayer->GetReferentId()));
+ }
+
+ return WebRenderScrollDataWrapper(*mUpdater);
+ }
+
+ WebRenderScrollDataWrapper GetPrevSibling() const {
+ MOZ_ASSERT(IsValid());
+
+ if (!AtTopLayer()) {
+ // The virtual container layers don't have siblings
+ return WebRenderScrollDataWrapper(*mUpdater);
+ }
+
+ // Skip past the descendants to get to the previous sibling. However, we
+ // might be at the last sibling already.
+ size_t prevSiblingIndex = mLayerIndex + 1 + mLayer->GetDescendantCount();
+ if (prevSiblingIndex < mContainingSubtreeLastIndex) {
+ return WebRenderScrollDataWrapper(mUpdater, mData, prevSiblingIndex,
+ mContainingSubtreeLastIndex);
+ }
+ return WebRenderScrollDataWrapper(*mUpdater);
+ }
+
+ const ScrollMetadata& Metadata() const {
+ MOZ_ASSERT(IsValid());
+
+ if (mMetadataIndex >= mLayer->GetScrollMetadataCount()) {
+ return *ScrollMetadata::sNullMetadata;
+ }
+ return mLayer->GetScrollMetadata(*mData, mMetadataIndex);
+ }
+
+ const FrameMetrics& Metrics() const { return Metadata().GetMetrics(); }
+
+ AsyncPanZoomController* GetApzc() const { return nullptr; }
+
+ void SetApzc(AsyncPanZoomController* aApzc) const {}
+
+ const char* Name() const { return "WebRenderScrollDataWrapper"; }
+
+ gfx::Matrix4x4 GetTransform() const {
+ MOZ_ASSERT(IsValid());
+
+ // See WebRenderLayerScrollData::Initialize for more context.
+ // * The ancestor transform is associated with whichever layer has scroll
+ // id matching GetAncestorTransformId().
+ // * The resolution is associated with the "topmost" layer.
+ // * The transform is associated with the "bottommost" layer.
+ // Multiple transforms may apply to the same layer (e.g. if there is only
+ // one scrollmetadata on the layer, then it is both "topmost" and
+ // "bottommost"), so we may need to combine the transforms.
+
+ gfx::Matrix4x4 transform;
+ // The ancestor transform is usually emitted at the layer with the
+ // matching scroll id. However, sometimes the transform ends up on
+ // a node with no scroll metadata at all. In such cases we generate
+ // a single layer, and the ancestor transform needs to be on that layer,
+ // otherwise it will be lost.
+ bool emitAncestorTransform =
+ !Metrics().IsScrollable() ||
+ Metrics().GetScrollId() == mLayer->GetAncestorTransformId();
+ if (emitAncestorTransform) {
+ transform = mLayer->GetAncestorTransform();
+ }
+ if (AtTopLayer()) {
+ float resolution = mLayer->GetResolution();
+ transform =
+ transform * gfx::Matrix4x4::Scaling(resolution, resolution, 1.f);
+ }
+ if (AtBottomLayer()) {
+ transform = mLayer->GetTransform() * transform;
+ }
+ return transform;
+ }
+
+ CSSTransformMatrix GetTransformTyped() const {
+ return ViewAs<CSSTransformMatrix>(GetTransform());
+ }
+
+ bool TransformIsPerspective() const {
+ MOZ_ASSERT(IsValid());
+
+ // mLayer->GetTransformIsPerspective() tells us whether
+ // mLayer->GetTransform() is a perspective transform. Since
+ // mLayer->GetTransform() is only used at the bottom layer, we only
+ // need to check GetTransformIsPerspective() at the bottom layer too.
+ if (AtBottomLayer()) {
+ return mLayer->GetTransformIsPerspective();
+ }
+ return false;
+ }
+
+ LayerIntRegion GetVisibleRegion() const {
+ MOZ_ASSERT(IsValid());
+
+ if (AtBottomLayer()) {
+ return mLayer->GetVisibleRegion();
+ }
+
+ return ViewAs<LayerPixel>(
+ TransformBy(mLayer->GetTransformTyped(), mLayer->GetVisibleRegion()),
+ PixelCastJustification::MovingDownToChildren);
+ }
+
+ LayerIntSize GetRemoteDocumentSize() const {
+ MOZ_ASSERT(IsValid());
+
+ if (mLayer->GetReferentId().isNothing()) {
+ return LayerIntSize();
+ }
+
+ if (AtBottomLayer()) {
+ return mLayer->GetRemoteDocumentSize();
+ }
+
+ return ViewAs<LayerPixel>(mLayer->GetRemoteDocumentSize(),
+ PixelCastJustification::MovingDownToChildren);
+ }
+
+ Maybe<LayersId> GetReferentId() const {
+ MOZ_ASSERT(IsValid());
+
+ if (AtBottomLayer()) {
+ return mLayer->GetReferentId();
+ }
+ return Nothing();
+ }
+
+ EventRegionsOverride GetEventRegionsOverride() const {
+ MOZ_ASSERT(IsValid());
+ // Only ref layers can have an event regions override.
+ if (GetReferentId()) {
+ return mLayer->GetEventRegionsOverride();
+ }
+ return EventRegionsOverride::NoOverride;
+ }
+
+ const ScrollbarData& GetScrollbarData() const {
+ MOZ_ASSERT(IsValid());
+ return mLayer->GetScrollbarData();
+ }
+
+ Maybe<uint64_t> GetScrollbarAnimationId() const {
+ MOZ_ASSERT(IsValid());
+ return mLayer->GetScrollbarAnimationId();
+ }
+
+ Maybe<uint64_t> GetFixedPositionAnimationId() const {
+ MOZ_ASSERT(IsValid());
+
+ if (AtBottomLayer()) {
+ return mLayer->GetFixedPositionAnimationId();
+ }
+ return Nothing();
+ }
+
+ ScrollableLayerGuid::ViewID GetFixedPositionScrollContainerId() const {
+ MOZ_ASSERT(IsValid());
+
+ if (AtBottomLayer()) {
+ return mLayer->GetFixedPositionScrollContainerId();
+ }
+ return ScrollableLayerGuid::NULL_SCROLL_ID;
+ }
+
+ SideBits GetFixedPositionSides() const {
+ MOZ_ASSERT(IsValid());
+
+ if (AtBottomLayer()) {
+ return mLayer->GetFixedPositionSides();
+ }
+ return SideBits::eNone;
+ }
+
+ ScrollableLayerGuid::ViewID GetStickyScrollContainerId() const {
+ MOZ_ASSERT(IsValid());
+
+ if (AtBottomLayer()) {
+ return mLayer->GetStickyPositionScrollContainerId();
+ }
+ return ScrollableLayerGuid::NULL_SCROLL_ID;
+ }
+
+ const LayerRectAbsolute& GetStickyScrollRangeOuter() const {
+ MOZ_ASSERT(IsValid());
+
+ if (AtBottomLayer()) {
+ return mLayer->GetStickyScrollRangeOuter();
+ }
+
+ static const LayerRectAbsolute empty;
+ return empty;
+ }
+
+ const LayerRectAbsolute& GetStickyScrollRangeInner() const {
+ MOZ_ASSERT(IsValid());
+
+ if (AtBottomLayer()) {
+ return mLayer->GetStickyScrollRangeInner();
+ }
+
+ static const LayerRectAbsolute empty;
+ return empty;
+ }
+
+ Maybe<uint64_t> GetStickyPositionAnimationId() const {
+ MOZ_ASSERT(IsValid());
+
+ if (AtBottomLayer()) {
+ return mLayer->GetStickyPositionAnimationId();
+ }
+ return Nothing();
+ }
+
+ Maybe<uint64_t> GetZoomAnimationId() const {
+ MOZ_ASSERT(IsValid());
+ return mLayer->GetZoomAnimationId();
+ }
+
+ Maybe<ScrollableLayerGuid::ViewID> GetAsyncZoomContainerId() const {
+ MOZ_ASSERT(IsValid());
+ return mLayer->GetAsyncZoomContainerId();
+ }
+
+ // Expose an opaque pointer to the layer. Mostly used for printf
+ // purposes. This is not intended to be a general-purpose accessor
+ // for the underlying layer.
+ const void* GetLayer() const {
+ MOZ_ASSERT(IsValid());
+ return mLayer;
+ }
+
+ template <int Level>
+ size_t Dump(gfx::TreeLog<Level>& aOut) const {
+ std::string result = "(invalid)";
+ if (!IsValid()) {
+ aOut << result;
+ return result.length();
+ }
+ if (AtBottomLayer()) {
+ if (mData != nullptr) {
+ const WebRenderLayerScrollData* layerData =
+ mData->GetLayerData(mLayerIndex);
+ if (layerData != nullptr) {
+ std::stringstream ss;
+ layerData->Dump(ss, *mData);
+ result = ss.str();
+ aOut << result;
+ return result.length();
+ }
+ }
+ }
+ return 0;
+ }
+
+ private:
+ bool AtBottomLayer() const { return mMetadataIndex == 0; }
+
+ bool AtTopLayer() const {
+ return mLayer->GetScrollMetadataCount() == 0 ||
+ mMetadataIndex == mLayer->GetScrollMetadataCount() - 1;
+ }
+
+ private:
+ const APZUpdater* mUpdater;
+ const WebRenderScrollData* mData;
+ // The index (in mData->mLayerScrollData) of the WebRenderLayerScrollData this
+ // wrapper is pointing to.
+ size_t mLayerIndex;
+ // The upper bound on the set of valid indices inside the subtree rooted at
+ // the parent of this "layer". That is, any layer index |i| in the range
+ // mLayerIndex <= i < mContainingSubtreeLastIndex is guaranteed to point to
+ // a layer that is a descendant of "parent", where "parent" is the parent
+ // layer of the layer at mLayerIndex. This is needed in order to implement
+ // GetPrevSibling() correctly.
+ size_t mContainingSubtreeLastIndex;
+ // The WebRenderLayerScrollData this wrapper is pointing to.
+ const WebRenderLayerScrollData* mLayer;
+ // The index of the scroll metadata within mLayer that this wrapper is
+ // pointing to.
+ uint32_t mMetadataIndex;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif /* GFX_WEBRENDERSCROLLDATAWRAPPER_H */
diff --git a/gfx/layers/wr/WebRenderTextureHost.cpp b/gfx/layers/wr/WebRenderTextureHost.cpp
new file mode 100644
index 0000000000..aa4215453f
--- /dev/null
+++ b/gfx/layers/wr/WebRenderTextureHost.cpp
@@ -0,0 +1,212 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "WebRenderTextureHost.h"
+
+#include "mozilla/layers/ImageDataSerializer.h"
+#include "mozilla/layers/LayersSurfaces.h"
+#include "mozilla/layers/TextureSourceProvider.h"
+#include "mozilla/webrender/RenderThread.h"
+#include "mozilla/webrender/WebRenderAPI.h"
+
+#ifdef MOZ_WIDGET_ANDROID
+# include "mozilla/layers/TextureHostOGL.h"
+#endif
+
+namespace mozilla::layers {
+
+class ScheduleHandleRenderTextureOps : public wr::NotificationHandler {
+ public:
+ explicit ScheduleHandleRenderTextureOps() {}
+
+ virtual void Notify(wr::Checkpoint aCheckpoint) override {
+ if (aCheckpoint == wr::Checkpoint::FrameTexturesUpdated) {
+ MOZ_ASSERT(wr::RenderThread::IsInRenderThread());
+ wr::RenderThread::Get()->HandleRenderTextureOps();
+ } else {
+ MOZ_ASSERT(aCheckpoint == wr::Checkpoint::TransactionDropped);
+ }
+ }
+
+ protected:
+};
+
+WebRenderTextureHost::WebRenderTextureHost(
+ TextureFlags aFlags, TextureHost* aTexture,
+ const wr::ExternalImageId& aExternalImageId)
+ : TextureHost(TextureHostType::Unknown, aFlags),
+ mWrappedTextureHost(aTexture) {
+ MOZ_ASSERT(mWrappedTextureHost);
+ // The wrapped textureHost will be used in WebRender, and the WebRender could
+ // run at another thread. It's hard to control the life-time when gecko
+ // receives PTextureParent destroy message. It's possible that textureHost is
+ // still used by WebRender. So, we only accept the textureHost without
+ // DEALLOCATE_CLIENT flag here. If the buffer deallocation is controlled by
+ // parent, we could do something to make sure the wrapped textureHost is not
+ // used by WebRender and then release it.
+ MOZ_ASSERT(!(aFlags & TextureFlags::DEALLOCATE_CLIENT));
+ MOZ_COUNT_CTOR(WebRenderTextureHost);
+
+ mExternalImageId = Some(aExternalImageId);
+}
+
+WebRenderTextureHost::~WebRenderTextureHost() {
+ MOZ_COUNT_DTOR(WebRenderTextureHost);
+}
+
+wr::ExternalImageId WebRenderTextureHost::GetExternalImageKey() {
+ if (IsValid()) {
+ mWrappedTextureHost->EnsureRenderTexture(mExternalImageId);
+ }
+ MOZ_ASSERT(mWrappedTextureHost->mExternalImageId.isSome());
+ return mWrappedTextureHost->mExternalImageId.ref();
+}
+
+bool WebRenderTextureHost::IsValid() { return mWrappedTextureHost->IsValid(); }
+
+void WebRenderTextureHost::UnbindTextureSource() {
+ if (mWrappedTextureHost->AsBufferTextureHost()) {
+ mWrappedTextureHost->UnbindTextureSource();
+ }
+ // Handle read unlock
+ TextureHost::UnbindTextureSource();
+}
+
+already_AddRefed<gfx::DataSourceSurface> WebRenderTextureHost::GetAsSurface() {
+ return mWrappedTextureHost->GetAsSurface();
+}
+
+gfx::ColorDepth WebRenderTextureHost::GetColorDepth() const {
+ return mWrappedTextureHost->GetColorDepth();
+}
+
+gfx::YUVColorSpace WebRenderTextureHost::GetYUVColorSpace() const {
+ return mWrappedTextureHost->GetYUVColorSpace();
+}
+
+gfx::ColorRange WebRenderTextureHost::GetColorRange() const {
+ return mWrappedTextureHost->GetColorRange();
+}
+
+gfx::IntSize WebRenderTextureHost::GetSize() const {
+ return mWrappedTextureHost->GetSize();
+}
+
+gfx::SurfaceFormat WebRenderTextureHost::GetFormat() const {
+ return mWrappedTextureHost->GetFormat();
+}
+
+void WebRenderTextureHost::NotifyNotUsed() {
+#ifdef MOZ_WIDGET_ANDROID
+ // When SurfaceTextureHost is wrapped by RemoteTextureHostWrapper,
+ // NotifyNotUsed() is handled by SurfaceTextureHost.
+ if (IsWrappingSurfaceTextureHost() &&
+ !mWrappedTextureHost->AsRemoteTextureHostWrapper()) {
+ wr::RenderThread::Get()->NotifyNotUsed(GetExternalImageKey());
+ }
+#endif
+ if (mWrappedTextureHost->AsRemoteTextureHostWrapper()) {
+ mWrappedTextureHost->NotifyNotUsed();
+ }
+ TextureHost::NotifyNotUsed();
+}
+
+void WebRenderTextureHost::MaybeNotifyForUse(wr::TransactionBuilder& aTxn) {
+#if defined(MOZ_WIDGET_ANDROID)
+ if (IsWrappingSurfaceTextureHost() &&
+ !mWrappedTextureHost->AsRemoteTextureHostWrapper()) {
+ wr::RenderThread::Get()->NotifyForUse(GetExternalImageKey());
+ aTxn.Notify(wr::Checkpoint::FrameTexturesUpdated,
+ MakeUnique<ScheduleHandleRenderTextureOps>());
+ }
+#endif
+}
+
+bool WebRenderTextureHost::IsWrappingSurfaceTextureHost() {
+ return mWrappedTextureHost->IsWrappingSurfaceTextureHost();
+}
+
+void WebRenderTextureHost::PrepareForUse() {
+ // When SurfaceTextureHost is wrapped by RemoteTextureHostWrapper,
+ // PrepareForUse() is handled by SurfaceTextureHost.
+ if ((IsWrappingSurfaceTextureHost() &&
+ !mWrappedTextureHost->AsRemoteTextureHostWrapper()) ||
+ mWrappedTextureHost->AsBufferTextureHost()) {
+ // Call PrepareForUse on render thread.
+ // See RenderAndroidSurfaceTextureHostOGL::PrepareForUse.
+ wr::RenderThread::Get()->PrepareForUse(GetExternalImageKey());
+ }
+}
+
+gfx::SurfaceFormat WebRenderTextureHost::GetReadFormat() const {
+ return mWrappedTextureHost->GetReadFormat();
+}
+
+int32_t WebRenderTextureHost::GetRGBStride() {
+ gfx::SurfaceFormat format = GetFormat();
+ if (GetFormat() == gfx::SurfaceFormat::YUV) {
+ // XXX this stride is used until yuv image rendering by webrender is used.
+ // Software converted RGB buffers strides are aliened to 16
+ return gfx::GetAlignedStride<16>(
+ GetSize().width, BytesPerPixel(gfx::SurfaceFormat::B8G8R8A8));
+ }
+ return ImageDataSerializer::ComputeRGBStride(format, GetSize().width);
+}
+
+bool WebRenderTextureHost::NeedsDeferredDeletion() const {
+ return mWrappedTextureHost->NeedsDeferredDeletion();
+}
+
+uint32_t WebRenderTextureHost::NumSubTextures() {
+ return mWrappedTextureHost->NumSubTextures();
+}
+
+void WebRenderTextureHost::PushResourceUpdates(
+ wr::TransactionBuilder& aResources, ResourceUpdateOp aOp,
+ const Range<wr::ImageKey>& aImageKeys, const wr::ExternalImageId& aExtID) {
+ MOZ_ASSERT(GetExternalImageKey() == aExtID);
+
+ mWrappedTextureHost->PushResourceUpdates(aResources, aOp, aImageKeys, aExtID);
+}
+
+void WebRenderTextureHost::PushDisplayItems(
+ wr::DisplayListBuilder& aBuilder, const wr::LayoutRect& aBounds,
+ const wr::LayoutRect& aClip, wr::ImageRendering aFilter,
+ const Range<wr::ImageKey>& aImageKeys, PushDisplayItemFlagSet aFlags) {
+ MOZ_ASSERT(aImageKeys.length() > 0);
+
+ mWrappedTextureHost->PushDisplayItems(aBuilder, aBounds, aClip, aFilter,
+ aImageKeys, aFlags);
+}
+
+bool WebRenderTextureHost::SupportsExternalCompositing(
+ WebRenderBackend aBackend) {
+ return mWrappedTextureHost->SupportsExternalCompositing(aBackend);
+}
+
+void WebRenderTextureHost::SetAcquireFence(
+ mozilla::ipc::FileDescriptor&& aFenceFd) {
+ mWrappedTextureHost->SetAcquireFence(std::move(aFenceFd));
+}
+
+void WebRenderTextureHost::SetReleaseFence(
+ mozilla::ipc::FileDescriptor&& aFenceFd) {
+ mWrappedTextureHost->SetReleaseFence(std::move(aFenceFd));
+}
+
+mozilla::ipc::FileDescriptor WebRenderTextureHost::GetAndResetReleaseFence() {
+ return mWrappedTextureHost->GetAndResetReleaseFence();
+}
+
+AndroidHardwareBuffer* WebRenderTextureHost::GetAndroidHardwareBuffer() const {
+ return mWrappedTextureHost->GetAndroidHardwareBuffer();
+}
+
+TextureHostType WebRenderTextureHost::GetTextureHostType() {
+ return mWrappedTextureHost->GetTextureHostType();
+}
+
+} // namespace mozilla::layers
diff --git a/gfx/layers/wr/WebRenderTextureHost.h b/gfx/layers/wr/WebRenderTextureHost.h
new file mode 100644
index 0000000000..d24d9c7304
--- /dev/null
+++ b/gfx/layers/wr/WebRenderTextureHost.h
@@ -0,0 +1,111 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_GFX_WEBRENDERTEXTUREHOST_H
+#define MOZILLA_GFX_WEBRENDERTEXTUREHOST_H
+
+#include "mozilla/layers/TextureHost.h"
+#include "mozilla/webrender/WebRenderTypes.h"
+
+namespace mozilla {
+namespace layers {
+
+class SurfaceDescriptor;
+
+// This textureHost is specialized for WebRender usage. With WebRender, there is
+// no Compositor during composition. Instead, we use RendererOGL for
+// composition. So, there are some UNREACHABLE asserts for the original
+// Compositor related code path in this class. Furthermore, the RendererOGL runs
+// at RenderThead instead of Compositor thread. This class is also creating the
+// corresponding RenderXXXTextureHost used by RendererOGL at RenderThread.
+class WebRenderTextureHost : public TextureHost {
+ public:
+ WebRenderTextureHost(TextureFlags aFlags, TextureHost* aTexture,
+ const wr::ExternalImageId& aExternalImageId);
+ virtual ~WebRenderTextureHost();
+
+ void DeallocateDeviceData() override {}
+
+ void UnbindTextureSource() override;
+
+ gfx::SurfaceFormat GetFormat() const override;
+
+ virtual void NotifyNotUsed() override;
+
+ virtual bool IsValid() override;
+
+ // Return the format used for reading the texture. Some hardware specific
+ // textureHosts use their special data representation internally, but we could
+ // treat these textureHost as the read-format when we read them.
+ // Please check TextureHost::GetReadFormat().
+ gfx::SurfaceFormat GetReadFormat() const override;
+
+ already_AddRefed<gfx::DataSourceSurface> GetAsSurface() override;
+
+ gfx::ColorDepth GetColorDepth() const override;
+ gfx::YUVColorSpace GetYUVColorSpace() const override;
+ gfx::ColorRange GetColorRange() const override;
+
+ gfx::IntSize GetSize() const override;
+
+#ifdef MOZ_LAYERS_HAVE_LOG
+ const char* Name() override { return "WebRenderTextureHost"; }
+#endif
+
+ WebRenderTextureHost* AsWebRenderTextureHost() override { return this; }
+
+ RemoteTextureHostWrapper* AsRemoteTextureHostWrapper() override {
+ return mWrappedTextureHost->AsRemoteTextureHostWrapper();
+ }
+
+ BufferTextureHost* AsBufferTextureHost() override {
+ return mWrappedTextureHost->AsBufferTextureHost();
+ }
+
+ bool IsWrappingSurfaceTextureHost() override;
+
+ virtual void PrepareForUse() override;
+
+ wr::ExternalImageId GetExternalImageKey();
+
+ int32_t GetRGBStride();
+
+ bool NeedsDeferredDeletion() const override;
+
+ uint32_t NumSubTextures() override;
+
+ void PushResourceUpdates(wr::TransactionBuilder& aResources,
+ ResourceUpdateOp aOp,
+ const Range<wr::ImageKey>& aImageKeys,
+ const wr::ExternalImageId& aExtID) override;
+
+ void PushDisplayItems(wr::DisplayListBuilder& aBuilder,
+ const wr::LayoutRect& aBounds,
+ const wr::LayoutRect& aClip, wr::ImageRendering aFilter,
+ const Range<wr::ImageKey>& aImageKeys,
+ PushDisplayItemFlagSet aFlags) override;
+
+ bool SupportsExternalCompositing(WebRenderBackend aBackend) override;
+
+ void SetAcquireFence(mozilla::ipc::FileDescriptor&& aFenceFd) override;
+
+ void SetReleaseFence(mozilla::ipc::FileDescriptor&& aFenceFd) override;
+
+ mozilla::ipc::FileDescriptor GetAndResetReleaseFence() override;
+
+ AndroidHardwareBuffer* GetAndroidHardwareBuffer() const override;
+
+ void MaybeNotifyForUse(wr::TransactionBuilder& aTxn);
+
+ TextureHostType GetTextureHostType() override;
+
+ const RefPtr<TextureHost> mWrappedTextureHost;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // MOZILLA_GFX_WEBRENDERTEXTUREHOST_H
diff --git a/gfx/layers/wr/WebRenderUserData.cpp b/gfx/layers/wr/WebRenderUserData.cpp
new file mode 100644
index 0000000000..2a767a6d18
--- /dev/null
+++ b/gfx/layers/wr/WebRenderUserData.cpp
@@ -0,0 +1,427 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "WebRenderUserData.h"
+
+#include "mozilla/image/WebRenderImageProvider.h"
+#include "mozilla/layers/AnimationHelper.h"
+#include "mozilla/layers/CompositorBridgeChild.h"
+#include "mozilla/layers/ImageClient.h"
+#include "mozilla/layers/WebRenderBridgeChild.h"
+#include "mozilla/layers/RenderRootStateManager.h"
+#include "mozilla/layers/WebRenderMessages.h"
+#include "mozilla/layers/IpcResourceUpdateQueue.h"
+#include "mozilla/layers/SharedSurfacesChild.h"
+#include "mozilla/webgpu/WebGPUChild.h"
+#include "nsDisplayListInvalidation.h"
+#include "nsIFrame.h"
+#include "WebRenderCanvasRenderer.h"
+
+using namespace mozilla::image;
+
+namespace mozilla {
+namespace layers {
+
+void WebRenderBackgroundData::AddWebRenderCommands(
+ wr::DisplayListBuilder& aBuilder) {
+ aBuilder.PushRect(mBounds, mBounds, true, true, false, mColor);
+}
+
+/* static */
+bool WebRenderUserData::SupportsAsyncUpdate(nsIFrame* aFrame) {
+ if (!aFrame) {
+ return false;
+ }
+ RefPtr<WebRenderImageData> data = GetWebRenderUserData<WebRenderImageData>(
+ aFrame, static_cast<uint32_t>(DisplayItemType::TYPE_VIDEO));
+ if (data) {
+ return data->IsAsync();
+ }
+
+ return false;
+}
+
+/* static */
+bool WebRenderUserData::ProcessInvalidateForImage(nsIFrame* aFrame,
+ DisplayItemType aType,
+ ImageProviderId aProviderId) {
+ MOZ_ASSERT(aFrame);
+
+ if (!aFrame->HasProperty(WebRenderUserDataProperty::Key())) {
+ aFrame->SchedulePaint();
+ return false;
+ }
+
+ auto type = static_cast<uint32_t>(aType);
+ RefPtr<WebRenderFallbackData> fallback =
+ GetWebRenderUserData<WebRenderFallbackData>(aFrame, type);
+ if (fallback) {
+ fallback->SetInvalid(true);
+ aFrame->SchedulePaint();
+ return true;
+ }
+
+ RefPtr<WebRenderImageProviderData> image =
+ GetWebRenderUserData<WebRenderImageProviderData>(aFrame, type);
+ if (image && image->Invalidate(aProviderId)) {
+ return true;
+ }
+
+ aFrame->SchedulePaint();
+ return false;
+}
+
+WebRenderUserData::WebRenderUserData(RenderRootStateManager* aManager,
+ uint32_t aDisplayItemKey, nsIFrame* aFrame)
+ : mManager(aManager),
+ mFrame(aFrame),
+ mDisplayItemKey(aDisplayItemKey),
+ mTable(aManager->GetWebRenderUserDataTable()),
+ mUsed(false) {}
+
+WebRenderUserData::WebRenderUserData(RenderRootStateManager* aManager,
+ nsDisplayItem* aItem)
+ : mManager(aManager),
+ mFrame(aItem->Frame()),
+ mDisplayItemKey(aItem->GetPerFrameKey()),
+ mTable(aManager->GetWebRenderUserDataTable()),
+ mUsed(false) {}
+
+WebRenderUserData::~WebRenderUserData() = default;
+
+void WebRenderUserData::RemoveFromTable() { mTable->Remove(this); }
+
+WebRenderBridgeChild* WebRenderUserData::WrBridge() const {
+ return mManager->WrBridge();
+}
+
+WebRenderImageData::WebRenderImageData(RenderRootStateManager* aManager,
+ nsDisplayItem* aItem)
+ : WebRenderUserData(aManager, aItem) {}
+
+WebRenderImageData::WebRenderImageData(RenderRootStateManager* aManager,
+ uint32_t aDisplayItemKey,
+ nsIFrame* aFrame)
+ : WebRenderUserData(aManager, aDisplayItemKey, aFrame) {}
+
+WebRenderImageData::~WebRenderImageData() {
+ ClearImageKey();
+
+ if (mPipelineId) {
+ mManager->RemovePipelineIdForCompositable(mPipelineId.ref());
+ }
+}
+
+void WebRenderImageData::ClearImageKey() {
+ if (mKey) {
+ mManager->AddImageKeyForDiscard(mKey.value());
+ if (mTextureOfImage) {
+ WrBridge()->ReleaseTextureOfImage(mKey.value());
+ mTextureOfImage = nullptr;
+ }
+ mKey.reset();
+ }
+ MOZ_ASSERT(!mTextureOfImage);
+}
+
+Maybe<wr::ImageKey> WebRenderImageData::UpdateImageKey(
+ ImageContainer* aContainer, wr::IpcResourceUpdateQueue& aResources,
+ bool aFallback) {
+ MOZ_ASSERT(aContainer);
+
+ if (mContainer != aContainer) {
+ mContainer = aContainer;
+ }
+
+ CreateImageClientIfNeeded();
+ if (!mImageClient) {
+ return Nothing();
+ }
+
+ MOZ_ASSERT(mImageClient->AsImageClientSingle());
+
+ ImageClientSingle* imageClient = mImageClient->AsImageClientSingle();
+ uint32_t oldCounter = imageClient->GetLastUpdateGenerationCounter();
+
+ bool ret = imageClient->UpdateImage(aContainer);
+ RefPtr<TextureClient> currentTexture = imageClient->GetForwardedTexture();
+ if (!ret || !currentTexture) {
+ // Delete old key
+ ClearImageKey();
+ return Nothing();
+ }
+
+ // Reuse old key if generation is not updated.
+ if (!aFallback &&
+ oldCounter == imageClient->GetLastUpdateGenerationCounter() && mKey) {
+ return mKey;
+ }
+
+ // If we already had a texture and the format hasn't changed, better to reuse
+ // the image keys than create new ones.
+ bool useUpdate = mKey.isSome() && !!mTextureOfImage && !!currentTexture &&
+ mTextureOfImage->GetSize() == currentTexture->GetSize() &&
+ mTextureOfImage->GetFormat() == currentTexture->GetFormat();
+
+ wr::MaybeExternalImageId extId = currentTexture->GetExternalImageKey();
+ MOZ_RELEASE_ASSERT(extId.isSome());
+
+ if (useUpdate) {
+ MOZ_ASSERT(mKey.isSome());
+ MOZ_ASSERT(mTextureOfImage);
+ aResources.PushExternalImageForTexture(
+ extId.ref(), mKey.ref(), currentTexture, /* aIsUpdate */ true);
+ } else {
+ ClearImageKey();
+ wr::WrImageKey key = WrBridge()->GetNextImageKey();
+ aResources.PushExternalImageForTexture(extId.ref(), key, currentTexture,
+ /* aIsUpdate */ false);
+ mKey = Some(key);
+ }
+
+ mTextureOfImage = currentTexture;
+ return mKey;
+}
+
+already_AddRefed<ImageClient> WebRenderImageData::GetImageClient() {
+ RefPtr<ImageClient> imageClient = mImageClient;
+ return imageClient.forget();
+}
+
+void WebRenderImageData::CreateAsyncImageWebRenderCommands(
+ mozilla::wr::DisplayListBuilder& aBuilder, ImageContainer* aContainer,
+ const StackingContextHelper& aSc, const LayoutDeviceRect& aBounds,
+ const LayoutDeviceRect& aSCBounds, VideoInfo::Rotation aRotation,
+ const wr::ImageRendering& aFilter, const wr::MixBlendMode& aMixBlendMode,
+ bool aIsBackfaceVisible) {
+ MOZ_ASSERT(aContainer->IsAsync());
+
+ if (mPipelineId.isSome() && mContainer != aContainer) {
+ // In this case, we need to remove the existed pipeline and create new one
+ // because the ImageContainer is changed.
+ WrBridge()->RemovePipelineIdForCompositable(mPipelineId.ref());
+ mPipelineId.reset();
+ }
+
+ if (!mPipelineId) {
+ // Alloc async image pipeline id.
+ mPipelineId =
+ Some(WrBridge()->GetCompositorBridgeChild()->GetNextPipelineId());
+ WrBridge()->AddPipelineIdForCompositable(
+ mPipelineId.ref(), aContainer->GetAsyncContainerHandle(),
+ CompositableHandleOwner::ImageBridge);
+ mContainer = aContainer;
+ }
+ MOZ_ASSERT(!mImageClient);
+
+ // Push IFrame for async image pipeline.
+ //
+ // We don't push a stacking context for this async image pipeline here.
+ // Instead, we do it inside the iframe that hosts the image. As a result,
+ // a bunch of the calculations normally done as part of that stacking
+ // context need to be done manually and pushed over to the parent side,
+ // where it will be done when we build the display list for the iframe.
+ // That happens in AsyncImagePipelineManager.
+ aBuilder.PushIFrame(aBounds, aIsBackfaceVisible, mPipelineId.ref(),
+ /*ignoreMissingPipelines*/ false);
+
+ WrBridge()->AddWebRenderParentCommand(OpUpdateAsyncImagePipeline(
+ mPipelineId.value(), aSCBounds, aRotation, aFilter, aMixBlendMode));
+}
+
+void WebRenderImageData::CreateImageClientIfNeeded() {
+ if (!mImageClient) {
+ mImageClient = ImageClient::CreateImageClient(
+ CompositableType::IMAGE, WrBridge(), TextureFlags::DEFAULT);
+ if (!mImageClient) {
+ return;
+ }
+
+ mImageClient->Connect();
+ }
+}
+
+WebRenderImageProviderData::WebRenderImageProviderData(
+ RenderRootStateManager* aManager, nsDisplayItem* aItem)
+ : WebRenderUserData(aManager, aItem) {}
+
+WebRenderImageProviderData::WebRenderImageProviderData(
+ RenderRootStateManager* aManager, uint32_t aDisplayItemKey,
+ nsIFrame* aFrame)
+ : WebRenderUserData(aManager, aDisplayItemKey, aFrame) {}
+
+WebRenderImageProviderData::~WebRenderImageProviderData() = default;
+
+Maybe<wr::ImageKey> WebRenderImageProviderData::UpdateImageKey(
+ WebRenderImageProvider* aProvider, ImgDrawResult aDrawResult,
+ wr::IpcResourceUpdateQueue& aResources) {
+ if (mProvider != aProvider) {
+ mProvider = aProvider;
+ }
+
+ wr::ImageKey key = {};
+ nsresult rv = mProvider ? mProvider->UpdateKey(mManager, aResources, key)
+ : NS_ERROR_FAILURE;
+ mKey = NS_SUCCEEDED(rv) ? Some(key) : Nothing();
+ mDrawResult = aDrawResult;
+ return mKey;
+}
+
+bool WebRenderImageProviderData::Invalidate(ImageProviderId aProviderId) const {
+ if (!aProviderId || !mProvider || mProvider->GetProviderId() != aProviderId ||
+ !mKey) {
+ return false;
+ }
+
+ if (mDrawResult != ImgDrawResult::SUCCESS &&
+ mDrawResult != ImgDrawResult::BAD_IMAGE) {
+ return false;
+ }
+
+ wr::ImageKey key = {};
+ nsresult rv =
+ mProvider->UpdateKey(mManager, mManager->AsyncResourceUpdates(), key);
+ return NS_SUCCEEDED(rv) && mKey.ref() == key;
+}
+
+WebRenderFallbackData::WebRenderFallbackData(RenderRootStateManager* aManager,
+ nsDisplayItem* aItem)
+ : WebRenderUserData(aManager, aItem), mOpacity(1.0f), mInvalid(false) {}
+
+WebRenderFallbackData::~WebRenderFallbackData() { ClearImageKey(); }
+
+void WebRenderFallbackData::SetBlobImageKey(const wr::BlobImageKey& aKey) {
+ ClearImageKey();
+ mBlobKey = Some(aKey);
+}
+
+Maybe<wr::ImageKey> WebRenderFallbackData::GetImageKey() {
+ if (mBlobKey) {
+ return Some(wr::AsImageKey(mBlobKey.value()));
+ }
+
+ if (mImageData) {
+ return mImageData->GetImageKey();
+ }
+
+ return Nothing();
+}
+
+void WebRenderFallbackData::ClearImageKey() {
+ if (mImageData) {
+ mImageData->ClearImageKey();
+ mImageData = nullptr;
+ }
+
+ if (mBlobKey) {
+ mManager->AddBlobImageKeyForDiscard(mBlobKey.value());
+ mBlobKey.reset();
+ }
+}
+
+WebRenderImageData* WebRenderFallbackData::PaintIntoImage() {
+ if (mBlobKey) {
+ mManager->AddBlobImageKeyForDiscard(mBlobKey.value());
+ mBlobKey.reset();
+ }
+
+ if (mImageData) {
+ return mImageData.get();
+ }
+
+ mImageData = MakeAndAddRef<WebRenderImageData>(mManager.get(),
+ mDisplayItemKey, mFrame);
+
+ return mImageData.get();
+}
+
+WebRenderAPZAnimationData::WebRenderAPZAnimationData(
+ RenderRootStateManager* aManager, nsDisplayItem* aItem)
+ : WebRenderUserData(aManager, aItem),
+ mAnimationId(AnimationHelper::GetNextCompositorAnimationsId()) {}
+
+WebRenderAnimationData::WebRenderAnimationData(RenderRootStateManager* aManager,
+ nsDisplayItem* aItem)
+ : WebRenderUserData(aManager, aItem) {}
+
+WebRenderAnimationData::~WebRenderAnimationData() {
+ // It may be the case that nsDisplayItem that created this WebRenderUserData
+ // gets destroyed without getting a chance to discard the compositor animation
+ // id, so we should do it as part of cleanup here.
+ uint64_t animationId = mAnimationInfo.GetCompositorAnimationsId();
+ // animationId might be 0 if mAnimationInfo never held any active animations.
+ if (animationId) {
+ mManager->AddCompositorAnimationsIdForDiscard(animationId);
+ }
+}
+
+WebRenderCanvasData::WebRenderCanvasData(RenderRootStateManager* aManager,
+ nsDisplayItem* aItem)
+ : WebRenderUserData(aManager, aItem) {}
+
+WebRenderCanvasData::~WebRenderCanvasData() {
+ if (mCanvasRenderer) {
+ mCanvasRenderer->ClearCachedResources();
+ }
+}
+
+void WebRenderCanvasData::ClearCanvasRenderer() { mCanvasRenderer = nullptr; }
+
+WebRenderCanvasRendererAsync* WebRenderCanvasData::GetCanvasRenderer() {
+ return mCanvasRenderer.get();
+}
+
+WebRenderCanvasRendererAsync* WebRenderCanvasData::CreateCanvasRenderer() {
+ mCanvasRenderer = new WebRenderCanvasRendererAsync(mManager);
+ return mCanvasRenderer.get();
+}
+
+bool WebRenderCanvasData::SetCanvasRenderer(CanvasRenderer* aCanvasRenderer) {
+ if (!aCanvasRenderer || !aCanvasRenderer->AsWebRenderCanvasRendererAsync()) {
+ return false;
+ }
+
+ auto* renderer = aCanvasRenderer->AsWebRenderCanvasRendererAsync();
+ if (mManager != renderer->GetRenderRootStateManager()) {
+ return false;
+ }
+
+ mCanvasRenderer = renderer;
+ return true;
+}
+
+void WebRenderCanvasData::SetImageContainer(ImageContainer* aImageContainer) {
+ mContainer = aImageContainer;
+}
+
+ImageContainer* WebRenderCanvasData::GetImageContainer() {
+ if (!mContainer) {
+ mContainer = MakeAndAddRef<ImageContainer>();
+ }
+ return mContainer;
+}
+
+void WebRenderCanvasData::ClearImageContainer() { mContainer = nullptr; }
+
+WebRenderRemoteData::WebRenderRemoteData(RenderRootStateManager* aManager,
+ nsDisplayItem* aItem)
+ : WebRenderUserData(aManager, aItem) {}
+
+WebRenderRemoteData::~WebRenderRemoteData() {
+ if (mRemoteBrowser) {
+ mRemoteBrowser->UpdateEffects(mozilla::dom::EffectsInfo::FullyHidden());
+ }
+}
+
+void DestroyWebRenderUserDataTable(WebRenderUserDataTable* aTable) {
+ for (const auto& value : aTable->Values()) {
+ value->RemoveFromTable();
+ }
+ delete aTable;
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/wr/WebRenderUserData.h b/gfx/layers/wr/WebRenderUserData.h
new file mode 100644
index 0000000000..c8b6b33002
--- /dev/null
+++ b/gfx/layers/wr/WebRenderUserData.h
@@ -0,0 +1,385 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GFX_WEBRENDERUSERDATA_H
+#define GFX_WEBRENDERUSERDATA_H
+
+#include <vector>
+#include "mozilla/webrender/WebRenderAPI.h"
+#include "mozilla/image/WebRenderImageProvider.h"
+#include "mozilla/layers/AnimationInfo.h"
+#include "mozilla/layers/LayersTypes.h"
+#include "mozilla/dom/RemoteBrowser.h"
+#include "mozilla/UniquePtr.h"
+#include "nsIFrame.h"
+#include "nsRefPtrHashtable.h"
+#include "nsTHashSet.h"
+#include "ImageTypes.h"
+#include "ImgDrawResult.h"
+#include "DisplayItemClip.h"
+
+namespace mozilla {
+
+class nsDisplayItemGeometry;
+
+namespace webgpu {
+class WebGPUChild;
+}
+
+namespace wr {
+class IpcResourceUpdateQueue;
+}
+
+namespace gfx {
+class SourceSurface;
+}
+
+namespace layers {
+
+class BasicLayerManager;
+class CanvasRenderer;
+class ImageClient;
+class ImageContainer;
+class WebRenderBridgeChild;
+class WebRenderCanvasData;
+class WebRenderCanvasRenderer;
+class WebRenderCanvasRendererAsync;
+class WebRenderImageData;
+class WebRenderImageProviderData;
+class WebRenderFallbackData;
+class RenderRootStateManager;
+class WebRenderGroupData;
+
+class WebRenderBackgroundData {
+ public:
+ WebRenderBackgroundData(wr::LayoutRect aBounds, wr::ColorF aColor)
+ : mBounds(aBounds), mColor(aColor) {}
+ void AddWebRenderCommands(wr::DisplayListBuilder& aBuilder);
+
+ protected:
+ wr::LayoutRect mBounds;
+ wr::ColorF mColor;
+};
+
+/// Parent class for arbitrary WebRender-specific data that can be associated
+/// to an nsFrame.
+class WebRenderUserData {
+ public:
+ typedef nsTHashSet<RefPtr<WebRenderUserData>> WebRenderUserDataRefTable;
+
+ static bool SupportsAsyncUpdate(nsIFrame* aFrame);
+
+ static bool ProcessInvalidateForImage(nsIFrame* aFrame, DisplayItemType aType,
+ image::ImageProviderId aProviderId);
+
+ NS_INLINE_DECL_REFCOUNTING(WebRenderUserData)
+
+ WebRenderUserData(RenderRootStateManager* aManager, nsDisplayItem* aItem);
+ WebRenderUserData(RenderRootStateManager* aManager, uint32_t mDisplayItemKey,
+ nsIFrame* aFrame);
+
+ virtual WebRenderImageData* AsImageData() { return nullptr; }
+ virtual WebRenderImageProviderData* AsImageProviderData() { return nullptr; }
+ virtual WebRenderFallbackData* AsFallbackData() { return nullptr; }
+ virtual WebRenderCanvasData* AsCanvasData() { return nullptr; }
+ virtual WebRenderGroupData* AsGroupData() { return nullptr; }
+
+ enum class UserDataType {
+ eImage,
+ eFallback,
+ eAPZAnimation,
+ eAnimation,
+ eCanvas,
+ eRemote,
+ eGroup,
+ eMask,
+ eImageProvider, // ImageLib
+ eInProcessImage,
+ };
+
+ virtual UserDataType GetType() = 0;
+ bool IsUsed() { return mUsed; }
+ void SetUsed(bool aUsed) { mUsed = aUsed; }
+ nsIFrame* GetFrame() { return mFrame; }
+ uint32_t GetDisplayItemKey() { return mDisplayItemKey; }
+ void RemoveFromTable();
+ virtual nsDisplayItemGeometry* GetGeometry() { return nullptr; }
+
+ protected:
+ virtual ~WebRenderUserData();
+
+ WebRenderBridgeChild* WrBridge() const;
+
+ RefPtr<RenderRootStateManager> mManager;
+ nsIFrame* mFrame;
+ uint32_t mDisplayItemKey;
+ WebRenderUserDataRefTable* mTable;
+ bool mUsed;
+};
+
+struct WebRenderUserDataKey {
+ WebRenderUserDataKey(uint32_t aFrameKey,
+ WebRenderUserData::UserDataType aType)
+ : mFrameKey(aFrameKey), mType(aType) {}
+
+ bool operator==(const WebRenderUserDataKey& other) const {
+ return mFrameKey == other.mFrameKey && mType == other.mType;
+ }
+ PLDHashNumber Hash() const {
+ return HashGeneric(
+ mFrameKey,
+ static_cast<std::underlying_type<decltype(mType)>::type>(mType));
+ }
+
+ uint32_t mFrameKey;
+ WebRenderUserData::UserDataType mType;
+};
+
+typedef nsRefPtrHashtable<
+ nsGenericHashKey<mozilla::layers::WebRenderUserDataKey>, WebRenderUserData>
+ WebRenderUserDataTable;
+
+/// Holds some data used to share TextureClient/ImageClient with the parent
+/// process.
+class WebRenderImageData : public WebRenderUserData {
+ public:
+ WebRenderImageData(RenderRootStateManager* aManager, nsDisplayItem* aItem);
+ WebRenderImageData(RenderRootStateManager* aManager, uint32_t aDisplayItemKey,
+ nsIFrame* aFrame);
+ virtual ~WebRenderImageData();
+
+ WebRenderImageData* AsImageData() override { return this; }
+ UserDataType GetType() override { return UserDataType::eImage; }
+ static UserDataType Type() { return UserDataType::eImage; }
+ Maybe<wr::ImageKey> GetImageKey() { return mKey; }
+ void SetImageKey(const wr::ImageKey& aKey);
+ already_AddRefed<ImageClient> GetImageClient();
+
+ Maybe<wr::ImageKey> UpdateImageKey(ImageContainer* aContainer,
+ wr::IpcResourceUpdateQueue& aResources,
+ bool aFallback = false);
+
+ void CreateAsyncImageWebRenderCommands(
+ mozilla::wr::DisplayListBuilder& aBuilder, ImageContainer* aContainer,
+ const StackingContextHelper& aSc, const LayoutDeviceRect& aBounds,
+ const LayoutDeviceRect& aSCBounds, VideoInfo::Rotation aRotation,
+ const wr::ImageRendering& aFilter, const wr::MixBlendMode& aMixBlendMode,
+ bool aIsBackfaceVisible);
+
+ void CreateImageClientIfNeeded();
+
+ bool IsAsync() { return mPipelineId.isSome(); }
+
+ void ClearImageKey();
+
+ protected:
+ Maybe<wr::ImageKey> mKey;
+ RefPtr<TextureClient> mTextureOfImage;
+ RefPtr<ImageClient> mImageClient;
+ Maybe<wr::PipelineId> mPipelineId;
+ RefPtr<ImageContainer> mContainer;
+};
+
+/// Holds some data used to share ImageLib results with the parent process.
+/// This may be either in the form of a blob recording or a rasterized surface.
+class WebRenderImageProviderData final : public WebRenderUserData {
+ public:
+ WebRenderImageProviderData(RenderRootStateManager* aManager,
+ nsDisplayItem* aItem);
+ WebRenderImageProviderData(RenderRootStateManager* aManager,
+ uint32_t aDisplayItemKey, nsIFrame* aFrame);
+ ~WebRenderImageProviderData() override;
+
+ WebRenderImageProviderData* AsImageProviderData() override { return this; }
+ UserDataType GetType() override { return UserDataType::eImageProvider; }
+ static UserDataType Type() { return UserDataType::eImageProvider; }
+
+ Maybe<wr::ImageKey> UpdateImageKey(image::WebRenderImageProvider* aProvider,
+ image::ImgDrawResult aDrawResult,
+ wr::IpcResourceUpdateQueue& aResources);
+
+ bool Invalidate(image::ImageProviderId aProviderId) const;
+
+ protected:
+ RefPtr<image::WebRenderImageProvider> mProvider;
+ Maybe<wr::ImageKey> mKey;
+ image::ImgDrawResult mDrawResult = image::ImgDrawResult::NOT_READY;
+};
+
+/// Used for fallback rendering.
+///
+/// In most cases this uses blob images but it can also render on the content
+/// side directly into a texture.
+class WebRenderFallbackData : public WebRenderUserData {
+ public:
+ WebRenderFallbackData(RenderRootStateManager* aManager, nsDisplayItem* aItem);
+ virtual ~WebRenderFallbackData();
+
+ WebRenderFallbackData* AsFallbackData() override { return this; }
+ UserDataType GetType() override { return UserDataType::eFallback; }
+ static UserDataType Type() { return UserDataType::eFallback; }
+ nsDisplayItemGeometry* GetGeometry() override { return mGeometry.get(); }
+
+ void SetInvalid(bool aInvalid) { mInvalid = aInvalid; }
+ bool IsInvalid() { return mInvalid; }
+ void SetFonts(const std::vector<RefPtr<gfx::ScaledFont>>& aFonts) {
+ mFonts = aFonts;
+ }
+ Maybe<wr::BlobImageKey> GetBlobImageKey() { return mBlobKey; }
+ void SetBlobImageKey(const wr::BlobImageKey& aKey);
+ Maybe<wr::ImageKey> GetImageKey();
+
+ /// Create a WebRenderImageData to manage the image we are about to render
+ /// into.
+ WebRenderImageData* PaintIntoImage();
+
+ std::vector<RefPtr<gfx::SourceSurface>> mExternalSurfaces;
+ UniquePtr<nsDisplayItemGeometry> mGeometry;
+ DisplayItemClip mClip;
+ nsRect mBounds;
+ nsRect mBuildingRect;
+ gfx::MatrixScales mScale;
+ float mOpacity;
+
+ protected:
+ void ClearImageKey();
+
+ std::vector<RefPtr<gfx::ScaledFont>> mFonts;
+ Maybe<wr::BlobImageKey> mBlobKey;
+ // When rendering into a blob image, mImageData is null. It is non-null only
+ // when we render directly into a texture on the content side.
+ RefPtr<WebRenderImageData> mImageData;
+ bool mInvalid;
+};
+
+class WebRenderAPZAnimationData : public WebRenderUserData {
+ public:
+ WebRenderAPZAnimationData(RenderRootStateManager* aManager,
+ nsDisplayItem* aItem);
+ virtual ~WebRenderAPZAnimationData() = default;
+
+ UserDataType GetType() override { return UserDataType::eAPZAnimation; }
+ static UserDataType Type() { return UserDataType::eAPZAnimation; }
+ uint64_t GetAnimationId() { return mAnimationId; }
+
+ private:
+ uint64_t mAnimationId;
+};
+
+class WebRenderAnimationData : public WebRenderUserData {
+ public:
+ WebRenderAnimationData(RenderRootStateManager* aManager,
+ nsDisplayItem* aItem);
+ virtual ~WebRenderAnimationData();
+
+ UserDataType GetType() override { return UserDataType::eAnimation; }
+ static UserDataType Type() { return UserDataType::eAnimation; }
+ AnimationInfo& GetAnimationInfo() { return mAnimationInfo; }
+
+ protected:
+ AnimationInfo mAnimationInfo;
+};
+
+class WebRenderCanvasData : public WebRenderUserData {
+ public:
+ WebRenderCanvasData(RenderRootStateManager* aManager, nsDisplayItem* aItem);
+ virtual ~WebRenderCanvasData();
+
+ WebRenderCanvasData* AsCanvasData() override { return this; }
+ UserDataType GetType() override { return UserDataType::eCanvas; }
+ static UserDataType Type() { return UserDataType::eCanvas; }
+
+ void ClearCanvasRenderer();
+ WebRenderCanvasRendererAsync* GetCanvasRenderer();
+ WebRenderCanvasRendererAsync* CreateCanvasRenderer();
+ bool SetCanvasRenderer(CanvasRenderer* aCanvasRenderer);
+
+ void SetImageContainer(ImageContainer* aImageContainer);
+ ImageContainer* GetImageContainer();
+ void ClearImageContainer();
+
+ protected:
+ RefPtr<WebRenderCanvasRendererAsync> mCanvasRenderer;
+ RefPtr<ImageContainer> mContainer;
+};
+
+class WebRenderRemoteData : public WebRenderUserData {
+ public:
+ WebRenderRemoteData(RenderRootStateManager* aManager, nsDisplayItem* aItem);
+ virtual ~WebRenderRemoteData();
+
+ UserDataType GetType() override { return UserDataType::eRemote; }
+ static UserDataType Type() { return UserDataType::eRemote; }
+
+ void SetRemoteBrowser(dom::RemoteBrowser* aBrowser) {
+ mRemoteBrowser = aBrowser;
+ }
+
+ protected:
+ RefPtr<dom::RemoteBrowser> mRemoteBrowser;
+};
+
+class WebRenderMaskData : public WebRenderUserData {
+ public:
+ explicit WebRenderMaskData(RenderRootStateManager* aManager,
+ nsDisplayItem* aItem)
+ : WebRenderUserData(aManager, aItem),
+ mMaskStyle(nsStyleImageLayers::LayerType::Mask),
+ mShouldHandleOpacity(false) {
+ MOZ_COUNT_CTOR(WebRenderMaskData);
+ }
+ virtual ~WebRenderMaskData() {
+ MOZ_COUNT_DTOR(WebRenderMaskData);
+ ClearImageKey();
+ }
+
+ void ClearImageKey();
+ void Invalidate();
+
+ UserDataType GetType() override { return UserDataType::eMask; }
+ static UserDataType Type() { return UserDataType::eMask; }
+
+ Maybe<wr::BlobImageKey> mBlobKey;
+ std::vector<RefPtr<gfx::ScaledFont>> mFonts;
+ std::vector<RefPtr<gfx::SourceSurface>> mExternalSurfaces;
+ LayerIntRect mItemRect;
+ nsPoint mMaskOffset;
+ nsStyleImageLayers mMaskStyle;
+ gfx::MatrixScales mScale;
+ bool mShouldHandleOpacity;
+};
+
+extern void DestroyWebRenderUserDataTable(WebRenderUserDataTable* aTable);
+
+struct WebRenderUserDataProperty {
+ NS_DECLARE_FRAME_PROPERTY_WITH_DTOR(Key, WebRenderUserDataTable,
+ DestroyWebRenderUserDataTable)
+};
+
+template <class T>
+already_AddRefed<T> GetWebRenderUserData(const nsIFrame* aFrame,
+ uint32_t aPerFrameKey) {
+ MOZ_ASSERT(aFrame);
+ WebRenderUserDataTable* userDataTable =
+ aFrame->GetProperty(WebRenderUserDataProperty::Key());
+ if (!userDataTable) {
+ return nullptr;
+ }
+
+ WebRenderUserData* data =
+ userDataTable->GetWeak(WebRenderUserDataKey(aPerFrameKey, T::Type()));
+ if (data) {
+ RefPtr<T> result = static_cast<T*>(data);
+ return result.forget();
+ }
+
+ return nullptr;
+}
+
+} // namespace layers
+} // namespace mozilla
+
+#endif /* GFX_WEBRENDERUSERDATA_H */